UNPKG

@wordpress/block-editor

Version:
337 lines (326 loc) 12.2 kB
/* wp:polyfill */ "use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _clsx = _interopRequireDefault(require("clsx")); var _element = require("@wordpress/element"); var _i18n = require("@wordpress/i18n"); var _compose = require("@wordpress/compose"); var _components = require("@wordpress/components"); var _data = require("@wordpress/data"); var _blockSelectionClearer = require("../block-selection-clearer"); var _writingFlow = require("../writing-flow"); var _getCompatibilityStyles = require("./get-compatibility-styles"); var _useScaleCanvas = require("./use-scale-canvas"); var _store = require("../../store"); var _jsxRuntime = require("react/jsx-runtime"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ function bubbleEvent(event, Constructor, frame) { const init = {}; for (const key in event) { init[key] = event[key]; } // Check if the event is a MouseEvent generated within the iframe. // If so, adjust the coordinates to be relative to the position of // the iframe. This ensures that components such as Draggable // receive coordinates relative to the window, instead of relative // to the iframe. Without this, the Draggable event handler would // result in components "jumping" position as soon as the user // drags over the iframe. if (event instanceof frame.contentDocument.defaultView.MouseEvent) { const rect = frame.getBoundingClientRect(); init.clientX += rect.left; init.clientY += rect.top; } const newEvent = new Constructor(event.type, init); if (init.defaultPrevented) { newEvent.preventDefault(); } const cancelled = !frame.dispatchEvent(newEvent); if (cancelled) { event.preventDefault(); } } /** * Bubbles some event types (keydown, keypress, and dragover) to parent document * document to ensure that the keyboard shortcuts and drag and drop work. * * Ideally, we should remove event bubbling in the future. Keyboard shortcuts * should be context dependent, e.g. actions on blocks like Cmd+A should not * work globally outside the block editor. * * @param {Document} iframeDocument Document to attach listeners to. */ function useBubbleEvents(iframeDocument) { return (0, _compose.useRefEffect)(() => { const { defaultView } = iframeDocument; if (!defaultView) { return; } const { frameElement } = defaultView; const html = iframeDocument.documentElement; const eventTypes = ['dragover', 'mousemove']; const handlers = {}; for (const name of eventTypes) { handlers[name] = event => { const prototype = Object.getPrototypeOf(event); const constructorName = prototype.constructor.name; const Constructor = window[constructorName]; bubbleEvent(event, Constructor, frameElement); }; html.addEventListener(name, handlers[name]); } return () => { for (const name of eventTypes) { html.removeEventListener(name, handlers[name]); } }; }); } function Iframe({ contentRef, children, tabIndex = 0, scale = 1, frameSize = 0, readonly, forwardedRef: ref, title = (0, _i18n.__)('Editor canvas'), ...props }) { const { resolvedAssets, isPreviewMode } = (0, _data.useSelect)(select => { const { getSettings } = select(_store.store); const settings = getSettings(); return { resolvedAssets: settings.__unstableResolvedAssets, isPreviewMode: settings.isPreviewMode }; }, []); const { styles = '', scripts = '' } = resolvedAssets; /** @type {[Document, import('react').Dispatch<Document>]} */ const [iframeDocument, setIframeDocument] = (0, _element.useState)(); const [bodyClasses, setBodyClasses] = (0, _element.useState)([]); const clearerRef = (0, _blockSelectionClearer.useBlockSelectionClearer)(); const [before, writingFlowRef, after] = (0, _writingFlow.useWritingFlow)(); const setRef = (0, _compose.useRefEffect)(node => { node._load = () => { setIframeDocument(node.contentDocument); }; let iFrameDocument; // Prevent the default browser action for files dropped outside of dropzones. function preventFileDropDefault(event) { event.preventDefault(); } const { ownerDocument } = node; // Ideally ALL classes that are added through get_body_class should // be added in the editor too, which we'll somehow have to get from // the server in the future (which will run the PHP filters). setBodyClasses(Array.from(ownerDocument.body.classList).filter(name => name.startsWith('admin-color-') || name.startsWith('post-type-') || name === 'wp-embed-responsive')); function onLoad() { const { contentDocument } = node; const { documentElement } = contentDocument; iFrameDocument = contentDocument; documentElement.classList.add('block-editor-iframe__html'); clearerRef(documentElement); contentDocument.dir = ownerDocument.dir; for (const compatStyle of (0, _getCompatibilityStyles.getCompatibilityStyles)()) { if (contentDocument.getElementById(compatStyle.id)) { continue; } contentDocument.head.appendChild(compatStyle.cloneNode(true)); if (!isPreviewMode) { // eslint-disable-next-line no-console console.warn(`${compatStyle.id} was added to the iframe incorrectly. Please use block.json or enqueue_block_assets to add styles to the iframe.`, compatStyle); } } iFrameDocument.addEventListener('dragover', preventFileDropDefault, false); iFrameDocument.addEventListener('drop', preventFileDropDefault, false); // Prevent clicks on links from navigating away. Note that links // inside `contenteditable` are already disabled by the browser, so // this is for links in blocks outside of `contenteditable`. iFrameDocument.addEventListener('click', event => { if (event.target.tagName === 'A') { event.preventDefault(); // Appending a hash to the current URL will not reload the // page. This is useful for e.g. footnotes. const href = event.target.getAttribute('href'); if (href?.startsWith('#')) { iFrameDocument.defaultView.location.hash = href.slice(1); } } }); } node.addEventListener('load', onLoad); return () => { delete node._load; node.removeEventListener('load', onLoad); iFrameDocument?.removeEventListener('dragover', preventFileDropDefault); iFrameDocument?.removeEventListener('drop', preventFileDropDefault); }; }, []); const { contentResizeListener, containerResizeListener, isZoomedOut, scaleContainerWidth } = (0, _useScaleCanvas.useScaleCanvas)({ scale, frameSize: parseInt(frameSize), iframeDocument }); const disabledRef = (0, _compose.useDisabled)({ isDisabled: !readonly }); const bodyRef = (0, _compose.useMergeRefs)([useBubbleEvents(iframeDocument), contentRef, clearerRef, writingFlowRef, disabledRef]); // Correct doctype is required to enable rendering in standards // mode. Also preload the styles to avoid a flash of unstyled // content. const html = `<!doctype html> <html> <head> <meta charset="utf-8"> <base href="${window.location.origin}"> <script>window.frameElement._load()</script> <style> html{ height: auto !important; min-height: 100%; } /* Lowest specificity to not override global styles */ :where(body) { margin: 0; /* Default background color in case zoom out mode background colors the html element */ background-color: white; } </style> ${styles} ${scripts} </head> <body> <script>document.currentScript.parentElement.remove()</script> </body> </html>`; const [src, cleanup] = (0, _element.useMemo)(() => { const _src = URL.createObjectURL(new window.Blob([html], { type: 'text/html' })); return [_src, () => URL.revokeObjectURL(_src)]; }, [html]); (0, _element.useEffect)(() => cleanup, [cleanup]); // Make sure to not render the before and after focusable div elements in view // mode. They're only needed to capture focus in edit mode. const shouldRenderFocusCaptureElements = tabIndex >= 0 && !isPreviewMode; const iframe = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [shouldRenderFocusCaptureElements && before, /*#__PURE__*/(0, _jsxRuntime.jsx)("iframe", { ...props, style: { ...props.style, height: props.style?.height, border: 0 }, ref: (0, _compose.useMergeRefs)([ref, setRef]), tabIndex: tabIndex // Correct doctype is required to enable rendering in standards // mode. Also preload the styles to avoid a flash of unstyled // content. , src: src, title: title, onKeyDown: event => { if (props.onKeyDown) { props.onKeyDown(event); } // If the event originates from inside the iframe, it means // it bubbled through the portal, but only with React // events. We need to to bubble native events as well, // though by doing so we also trigger another React event, // so we need to stop the propagation of this event to avoid // duplication. if (event.currentTarget.ownerDocument !== event.target.ownerDocument) { // We should only stop propagation of the React event, // the native event should further bubble inside the // iframe to the document and window. // Alternatively, we could consider redispatching the // native event in the iframe. const { stopPropagation } = event.nativeEvent; event.nativeEvent.stopPropagation = () => {}; event.stopPropagation(); event.nativeEvent.stopPropagation = stopPropagation; bubbleEvent(event, window.KeyboardEvent, event.currentTarget); } }, children: iframeDocument && (0, _element.createPortal)( /*#__PURE__*/ // We want to prevent React events from bubbling through the iframe // we bubble these manually. /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */ (0, _jsxRuntime.jsxs)("body", { ref: bodyRef, className: (0, _clsx.default)('block-editor-iframe__body', 'editor-styles-wrapper', ...bodyClasses), children: [contentResizeListener, /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.__experimentalStyleProvider, { document: iframeDocument, children: children })] }), iframeDocument.documentElement) }), shouldRenderFocusCaptureElements && after] }); return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: "block-editor-iframe__container", children: [containerResizeListener, /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: (0, _clsx.default)('block-editor-iframe__scale-container', isZoomedOut && 'is-zoomed-out'), style: { '--wp-block-editor-iframe-zoom-out-scale-container-width': isZoomedOut && `${scaleContainerWidth}px` }, children: iframe })] }); } function IframeIfReady(props, ref) { const isInitialised = (0, _data.useSelect)(select => select(_store.store).getSettings().__internalIsInitialized, []); // We shouldn't render the iframe until the editor settings are initialised. // The initial settings are needed to get the styles for the srcDoc, which // cannot be changed after the iframe is mounted. srcDoc is used to to set // the initial iframe HTML, which is required to avoid a flash of unstyled // content. if (!isInitialised) { return null; } return /*#__PURE__*/(0, _jsxRuntime.jsx)(Iframe, { ...props, forwardedRef: ref }); } var _default = exports.default = (0, _element.forwardRef)(IframeIfReady); //# sourceMappingURL=index.js.map