mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
Switched from Shadow DOM back to Iframes
refs https://github.com/TryGhost/Team/issues/1701 The Selection API is missing in Safari in Shadow DOM: https://bugs.webkit.org/show_bug.cgi?id=163921. So we need to move away from it for now. - Adds a new component: Modal, which makes sure we can style modals using tailwind, while displaying them outside of the main iframe (inside a different iframe that is positioned fixed) - Updated GenericDialog to use the new Modal component - Removed ShadowDOM
This commit is contained in:
parent
a9c3ef5444
commit
0b8f92ddbe
7 changed files with 201 additions and 60 deletions
|
@ -1,4 +1,4 @@
|
|||
import ShadowRoot from './components/ShadowRoot';
|
||||
import Frame from './components/Frame';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import React from 'react';
|
||||
import ActionHandler from './actions';
|
||||
|
@ -20,9 +20,9 @@ function AuthFrame({adminUrl, onLoad}) {
|
|||
|
||||
function CommentsBoxContainer({done, appVersion}) {
|
||||
return (
|
||||
<ShadowRoot appVersion={appVersion}>
|
||||
<Frame>
|
||||
<CommentsBox done={done} />
|
||||
</ShadowRoot>
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -273,6 +273,7 @@ export default class App extends React.Component {
|
|||
avatarSaturation: this.props.avatarSaturation,
|
||||
accentColor: this.props.accentColor,
|
||||
commentsEnabled: this.props.commentsEnabled,
|
||||
appVersion: this.props.appVersion,
|
||||
dispatchAction: (_action, data) => this.dispatchAction(_action, data),
|
||||
|
||||
/**
|
||||
|
@ -294,8 +295,9 @@ export default class App extends React.Component {
|
|||
return (
|
||||
<SentryErrorBoundary dsn={this.props.sentryDsn}>
|
||||
<AppContext.Provider value={this.getContextFromState()}>
|
||||
<CommentsBoxContainer done={done} appVersion={this.props.appVersion} />
|
||||
<CommentsBoxContainer done={done} />
|
||||
<AuthFrame adminUrl={this.props.adminUrl} onLoad={this.initSetup.bind(this)} initStatus={this.state.initStatus}/>
|
||||
<div id="ghost-comments-modal-root"></div>
|
||||
</AppContext.Provider>
|
||||
</SentryErrorBoundary>
|
||||
);
|
||||
|
|
78
apps/comments-ui/src/components/Frame.js
Normal file
78
apps/comments-ui/src/components/Frame.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
import React, {useContext, useState} from 'react';
|
||||
import {getBundledCssLink} from '../utils/helpers';
|
||||
import AppContext from '../AppContext';
|
||||
import IFrame from './IFrame';
|
||||
|
||||
const Frame = ({
|
||||
children,
|
||||
type,
|
||||
style,
|
||||
...props
|
||||
}) => {
|
||||
const {appVersion} = useContext(AppContext);
|
||||
const cssLink = getBundledCssLink({appVersion});
|
||||
|
||||
const styles = `
|
||||
body, html {
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
// We have two types of frames:
|
||||
// - A full width + content fitted height one
|
||||
// - A fixed positioned one for modals
|
||||
/**
|
||||
* @type {'dynamic'|'fixed'}
|
||||
*/
|
||||
type = type ?? 'dynamic';
|
||||
|
||||
// For now we don't listen for type changes, we could consider adding useEffect, but that won't be used
|
||||
const defaultStyle = type === 'dynamic' ? {
|
||||
width: '100%',
|
||||
height: '400px'
|
||||
} : {
|
||||
zIndex: '3999999',
|
||||
position: 'fixed',
|
||||
left: '0',
|
||||
top: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
const [iframeStyle, setIframeStyle] = useState(defaultStyle);
|
||||
|
||||
const onResize = (iframeRoot) => {
|
||||
setIframeStyle((current) => {
|
||||
return {
|
||||
...current,
|
||||
height: `${iframeRoot.scrollHeight}px`
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const [cssLoaded, setCssLoaded] = useState(false);
|
||||
|
||||
const onLoadCSS = () => {
|
||||
setCssLoaded(true);
|
||||
};
|
||||
|
||||
const head = (
|
||||
<>
|
||||
<link rel="stylesheet" href={cssLink} onLoad={onLoadCSS} />
|
||||
<style dangerouslySetInnerHTML={{__html: styles}} />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
</>
|
||||
);
|
||||
|
||||
const mergedStyle = {...iframeStyle, ...style};
|
||||
|
||||
// For now we're using <NewFrame> because using a functional component with portal caused some weird issues with modals
|
||||
return (
|
||||
<IFrame {...props} head={head} style={mergedStyle} onResize={type === 'dynamic' ? onResize : null}>
|
||||
{cssLoaded && children}
|
||||
</IFrame>
|
||||
);
|
||||
};
|
||||
|
||||
export default Frame;
|
39
apps/comments-ui/src/components/IFrame.js
Normal file
39
apps/comments-ui/src/components/IFrame.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import React, {Component} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
|
||||
export default class IFrame extends Component {
|
||||
componentDidMount() {
|
||||
this.node.addEventListener('load', this.handleLoad);
|
||||
}
|
||||
|
||||
handleLoad = () => {
|
||||
this.setupFrameBaseStyle();
|
||||
};
|
||||
|
||||
componentWillUnmout() {
|
||||
this.node.removeEventListener('load', this.handleLoad);
|
||||
}
|
||||
|
||||
setupFrameBaseStyle() {
|
||||
if (this.node.contentDocument) {
|
||||
this.iframeHtml = this.node.contentDocument.documentElement;
|
||||
this.iframeHead = this.node.contentDocument.head;
|
||||
this.iframeRoot = this.node.contentDocument.body;
|
||||
this.forceUpdate();
|
||||
|
||||
if (this.props.onResize) {
|
||||
(new ResizeObserver(_ => this.props.onResize(this.iframeRoot))).observe(this.iframeRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children, head, title = '', style = {}, onResize, ...rest} = this.props;
|
||||
return (
|
||||
<iframe srcDoc={`<!DOCTYPE html>`} {...rest} ref={node => (this.node = node)} title={title} style={style} frameBorder="0">
|
||||
{this.iframeHead && createPortal(head, this.iframeHead)}
|
||||
{this.iframeRoot && createPortal(children, this.iframeRoot)}
|
||||
</iframe>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import React from 'react';
|
||||
import root from 'react-shadow';
|
||||
import {getBundledCssLink} from '../utils/helpers';
|
||||
|
||||
const ShadowRoot = ({
|
||||
children,
|
||||
appVersion,
|
||||
...props
|
||||
}) => {
|
||||
const cssLink = getBundledCssLink({appVersion});
|
||||
|
||||
const styles = `
|
||||
.ghost-display {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const head = (
|
||||
<>
|
||||
<link rel="stylesheet" href={cssLink} />
|
||||
<style dangerouslySetInnerHTML={{__html: styles}} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<root.div {...props} mode={'closed'}>
|
||||
{head}
|
||||
{children}
|
||||
</root.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShadowRoot;
|
|
@ -1,33 +1,42 @@
|
|||
import React from 'react';
|
||||
import {Transition} from '@headlessui/react';
|
||||
import Modal from './Modal';
|
||||
|
||||
const GenericDialog = (props) => {
|
||||
// The modal will cover the whole screen, so while it is hidden, we need to disable pointer events
|
||||
const style = props.show ? {} : {
|
||||
pointerEvents: 'none'
|
||||
};
|
||||
return (
|
||||
<Transition
|
||||
show={props.show}
|
||||
enter="transition duration-200 linear"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition duration-200 linear"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed top-0 left-0 overflow-hidden w-screen h-screen z-[99999999] flex pt-12 justify-center bg-gradient-to-b from-[rgba(0,0,0,0.2)] to-rgba(0,0,0,0.1) backdrop-blur-[2px]" onClick={props.cancel}>
|
||||
<Transition.Child
|
||||
enter="transition duration-200 delay-150 linear"
|
||||
enterFrom="translate-y-4 opacity-0"
|
||||
enterTo="translate-y-0 opacity-100"
|
||||
<Modal show={props.show} style={style}>
|
||||
<div>
|
||||
<Transition
|
||||
show={props.show}
|
||||
enter="transition duration-200 linear"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition duration-200 linear"
|
||||
leaveFrom="translate-y-0 opacity-100"
|
||||
leaveTo="translate-y-4 opacity-0"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="bg-white w-[500px] p-8 rounded-xl text-center shadow-modal">
|
||||
{props.children}
|
||||
<div className="fixed top-0 left-0 overflow-hidden w-screen h-screen flex pt-12 justify-center bg-gradient-to-b from-[rgba(0,0,0,0.2)] to-rgba(0,0,0,0.1) backdrop-blur-[2px]" onClick={props.cancel}>
|
||||
<Transition.Child
|
||||
enter="transition duration-200 delay-150 linear"
|
||||
enterFrom="translate-y-4 opacity-0"
|
||||
enterTo="translate-y-0 opacity-100"
|
||||
leave="transition duration-200 linear"
|
||||
leaveFrom="translate-y-0 opacity-100"
|
||||
leaveTo="translate-y-4 opacity-0"
|
||||
>
|
||||
<div className="bg-white w-[500px] p-8 rounded-xl text-center shadow-modal">
|
||||
{props.children}
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenericDialog;
|
||||
export default GenericDialog;
|
||||
|
|
43
apps/comments-ui/src/components/modals/Modal.js
Normal file
43
apps/comments-ui/src/components/modals/Modal.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import {Component} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
import Frame from '../Frame';
|
||||
|
||||
/**
|
||||
* Full screen iframe, that is displayed fixed, and that can be used anywhere ('portalled' outside of existing iframes)
|
||||
*/
|
||||
export default class Modal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.el = document.createElement('div');
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// The portal element is inserted in the DOM tree after
|
||||
// the Modal's children are mounted, meaning that children
|
||||
// will be mounted on a detached DOM node. If a child
|
||||
// component requires to be attached to the DOM tree
|
||||
// immediately when mounted, for example to measure a
|
||||
// DOM node, or uses 'autoFocus' in a descendant, add
|
||||
// state to Modal and only render the children when Modal
|
||||
// is inserted in the DOM tree.
|
||||
const modalRoot = document.getElementById('ghost-comments-modal-root');
|
||||
modalRoot.appendChild(this.el);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const modalRoot = document.getElementById('ghost-comments-modal-root');
|
||||
modalRoot.removeChild(this.el);
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = (
|
||||
<Frame type="fixed" style={this.props.style}>
|
||||
{this.props.children}
|
||||
</Frame>
|
||||
);
|
||||
return createPortal(
|
||||
content,
|
||||
this.el
|
||||
);
|
||||
}
|
||||
}
|
|
@ -105,13 +105,16 @@ export function getInitials(name) {
|
|||
return parts[0].substring(0, 1).toLocaleUpperCase() + parts[parts.length - 1].substring(0, 1).toLocaleUpperCase();
|
||||
}
|
||||
|
||||
// Keep a reference outside, because document.currentScript is only returned on the initial script load.
|
||||
const currentScript = document.currentScript;
|
||||
|
||||
export function getBundledCssLink({appVersion}) {
|
||||
if (process.env.NODE_ENV === 'production' && appVersion) {
|
||||
return `https://unpkg.com/@tryghost/comments-ui@~${appVersion}/umd/main.css`;
|
||||
} else {
|
||||
if (document.currentScript) {
|
||||
if (currentScript) {
|
||||
// Dynamically determine the current path
|
||||
const url = new URL(document.currentScript.src);
|
||||
const url = new URL(currentScript.src);
|
||||
return url.origin + '/main.css';
|
||||
}
|
||||
return 'http://localhost:4000/main.css';
|
||||
|
|
Loading…
Add table
Reference in a new issue