UNPKG

@gechiui/block-editor

Version:
296 lines (245 loc) 8.92 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _element = require("@gechiui/element"); var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _classnames = _interopRequireDefault(require("classnames")); var _i18n = require("@gechiui/i18n"); var _compose = require("@gechiui/compose"); var _components = require("@gechiui/components"); var _blockSelectionClearer = require("../block-selection-clearer"); var _writingFlow = require("../writing-flow"); /** * External dependencies */ /** * GeChiUI dependencies */ /** * Internal dependencies */ const BODY_CLASS_NAME = 'editor-styles-wrapper'; const BLOCK_PREFIX = 'gc-block'; /** * Clones stylesheets targetting the editor canvas to the given document. A * stylesheet is considered targetting the editor a canvas if it contains the * `editor-styles-wrapper`, `gc-block`, or `gc-block-*` class selectors. * * Ideally, this hook should be removed in the future and styles should be added * explicitly as editor styles. * * @param {Document} doc The document to append cloned stylesheets to. */ function styleSheetsCompat(doc) { // Search the document for stylesheets targetting the editor canvas. Array.from(document.styleSheets).forEach(styleSheet => { try { // May fail for external styles. // eslint-disable-next-line no-unused-expressions styleSheet.cssRules; } catch (e) { return; } const { ownerNode, cssRules } = styleSheet; if (!cssRules) { return; } // Generally, ignore inline styles. We add inline styles belonging to a // stylesheet later, which may or may not match the selectors. if (ownerNode.tagName !== 'LINK') { return; } // Don't try to add the reset styles, which were removed as a dependency // from `edit-blocks` for the iframe since we don't need to reset admin // styles. if (ownerNode.id === 'gc-reset-editor-styles-css') { return; } const isMatch = Array.from(cssRules).find(_ref => { let { selectorText } = _ref; return selectorText && (selectorText.includes(`.${BODY_CLASS_NAME}`) || selectorText.includes(`.${BLOCK_PREFIX}`)); }); if (isMatch && !doc.getElementById(ownerNode.id)) { // Display warning once we have a way to add style dependencies to the editor. // See: https://github.com/GeChiUI/gutenberg/pull/37466. doc.head.appendChild(ownerNode.cloneNode(true)); // Add inline styles belonging to the stylesheet. const inlineCssId = ownerNode.id.replace('-css', '-inline-css'); const inlineCssElement = document.getElementById(inlineCssId); if (inlineCssElement) { doc.head.appendChild(inlineCssElement.cloneNode(true)); } } }); } /** * 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} doc Document to attach listeners to. */ function bubbleEvents(doc) { const { defaultView } = doc; const { frameElement } = defaultView; function bubbleEvent(event) { const prototype = Object.getPrototypeOf(event); const constructorName = prototype.constructor.name; const Constructor = window[constructorName]; const init = {}; for (const key in event) { init[key] = event[key]; } if (event instanceof defaultView.MouseEvent) { const rect = frameElement.getBoundingClientRect(); init.clientX += rect.left; init.clientY += rect.top; } const newEvent = new Constructor(event.type, init); const cancelled = !frameElement.dispatchEvent(newEvent); if (cancelled) { event.preventDefault(); } } const eventTypes = ['dragover']; for (const name of eventTypes) { doc.addEventListener(name, bubbleEvent); } } function useParsedAssets(html) { return (0, _element.useMemo)(() => { const doc = document.implementation.createHTMLDocument(''); doc.body.innerHTML = html; return Array.from(doc.body.children); }, [html]); } async function loadScript(head, _ref2) { let { id, src } = _ref2; return new Promise((resolve, reject) => { const script = head.ownerDocument.createElement('script'); script.id = id; if (src) { script.src = src; script.onload = () => resolve(); script.onerror = () => reject(); } else { resolve(); } head.appendChild(script); }); } function Iframe(_ref3, ref) { var _window$__editorAsset, _window$__editorAsset2; let { contentRef, children, head, tabIndex = 0, ...props } = _ref3; const [, forceRender] = (0, _element.useReducer)(() => ({})); const [iframeDocument, setIframeDocument] = (0, _element.useState)(); const [bodyClasses, setBodyClasses] = (0, _element.useState)([]); const styles = useParsedAssets((_window$__editorAsset = window.__editorAssets) === null || _window$__editorAsset === void 0 ? void 0 : _window$__editorAsset.styles); const scripts = useParsedAssets((_window$__editorAsset2 = window.__editorAssets) === null || _window$__editorAsset2 === void 0 ? void 0 : _window$__editorAsset2.scripts); const clearerRef = (0, _blockSelectionClearer.useBlockSelectionClearer)(); const [before, writingFlowRef, after] = (0, _writingFlow.useWritingFlow)(); const setRef = (0, _compose.useRefEffect)(node => { function setDocumentIfReady() { const { contentDocument, ownerDocument } = node; const { readyState, documentElement } = contentDocument; if (readyState !== 'interactive' && readyState !== 'complete') { return false; } bubbleEvents(contentDocument); setIframeDocument(contentDocument); clearerRef(documentElement); // 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 === 'gc-embed-responsive')); contentDocument.dir = ownerDocument.dir; documentElement.removeChild(contentDocument.head); documentElement.removeChild(contentDocument.body); return true; } if (setDocumentIfReady()) { return; } // Document is not immediately loaded in Firefox. node.addEventListener('load', () => { setDocumentIfReady(); }); }, []); const headRef = (0, _compose.useRefEffect)(element => { scripts.reduce((promise, script) => promise.then(() => loadScript(element, script)), Promise.resolve()).finally(() => { // When script are loaded, re-render blocks to allow them // to initialise. forceRender(); }); }, []); const bodyRef = (0, _compose.useMergeRefs)([contentRef, clearerRef, writingFlowRef]); (0, _element.useEffect)(() => { if (iframeDocument) { styleSheetsCompat(iframeDocument); } }, [iframeDocument]); head = (0, _element.createElement)(_element.Fragment, null, (0, _element.createElement)("style", null, 'body{margin:0}'), styles.map(_ref4 => { let { tagName, href, id, rel, media, textContent } = _ref4; const TagName = tagName.toLowerCase(); if (TagName === 'style') { return (0, _element.createElement)(TagName, { id, key: id }, textContent); } return (0, _element.createElement)(TagName, { href, id, rel, media, key: id }); }), head); return (0, _element.createElement)(_element.Fragment, null, tabIndex >= 0 && before, (0, _element.createElement)("iframe", (0, _extends2.default)({}, props, { ref: (0, _compose.useMergeRefs)([ref, setRef]), tabIndex: tabIndex, title: (0, _i18n.__)('编辑器画布') }), iframeDocument && (0, _element.createPortal)((0, _element.createElement)(_element.Fragment, null, (0, _element.createElement)("head", { ref: headRef }, head), (0, _element.createElement)("body", { ref: bodyRef, className: (0, _classnames.default)(BODY_CLASS_NAME, ...bodyClasses) }, (0, _element.createElement)(_components.__experimentalStyleProvider, { document: iframeDocument }, children))), iframeDocument.documentElement)), tabIndex >= 0 && after); } var _default = (0, _element.forwardRef)(Iframe); exports.default = _default; //# sourceMappingURL=index.js.map