@wordpress/block-editor
Version:
337 lines (326 loc) • 12.2 kB
JavaScript
/* 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