"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 _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.__unstableIsPreviewMode
    };
  }, []);
  const {
    styles = '',
    scripts = ''
  } = resolvedAssets;
  const [iframeDocument, setIframeDocument] = (0, _element.useState)();
  const prevContainerWidth = (0, _element.useRef)();
  const [bodyClasses, setBodyClasses] = (0, _element.useState)([]);
  const clearerRef = (0, _blockSelectionClearer.useBlockSelectionClearer)();
  const [before, writingFlowRef, after] = (0, _writingFlow.useWritingFlow)();
  const [contentResizeListener, {
    height: contentHeight
  }] = (0, _compose.useResizeObserver)();
  const [containerResizeListener, {
    width: containerWidth
  }] = (0, _compose.useResizeObserver)();
  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();
    }
    function onLoad() {
      const {
        contentDocument,
        ownerDocument
      } = node;
      const {
        documentElement
      } = contentDocument;
      iFrameDocument = contentDocument;
      documentElement.classList.add('block-editor-iframe__html');
      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.startsWith('post-type-') || name === 'wp-embed-responsive'));
      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);
    }
    node.addEventListener('load', onLoad);
    return () => {
      delete node._load;
      node.removeEventListener('load', onLoad);
      iFrameDocument?.removeEventListener('dragover', preventFileDropDefault);
      iFrameDocument?.removeEventListener('drop', preventFileDropDefault);
    };
  }, []);
  const [iframeWindowInnerHeight, setIframeWindowInnerHeight] = (0, _element.useState)();
  const iframeResizeRef = (0, _compose.useRefEffect)(node => {
    const nodeWindow = node.ownerDocument.defaultView;
    setIframeWindowInnerHeight(nodeWindow.innerHeight);
    const onResize = () => {
      setIframeWindowInnerHeight(nodeWindow.innerHeight);
    };
    nodeWindow.addEventListener('resize', onResize);
    return () => {
      nodeWindow.removeEventListener('resize', onResize);
    };
  }, []);
  const [windowInnerWidth, setWindowInnerWidth] = (0, _element.useState)();
  const windowResizeRef = (0, _compose.useRefEffect)(node => {
    const nodeWindow = node.ownerDocument.defaultView;
    setWindowInnerWidth(nodeWindow.innerWidth);
    const onResize = () => {
      setWindowInnerWidth(nodeWindow.innerWidth);
    };
    nodeWindow.addEventListener('resize', onResize);
    return () => {
      nodeWindow.removeEventListener('resize', onResize);
    };
  }, []);
  const isZoomedOut = scale !== 1;
  (0, _element.useEffect)(() => {
    if (!isZoomedOut) {
      prevContainerWidth.current = containerWidth;
    }
  }, [containerWidth, isZoomedOut]);
  const disabledRef = (0, _compose.useDisabled)({
    isDisabled: !readonly
  });
  const bodyRef = (0, _compose.useMergeRefs)([useBubbleEvents(iframeDocument), contentRef, clearerRef, writingFlowRef, disabledRef,
  // Avoid resize listeners when not needed, these will trigger
  // unnecessary re-renders when animating the iframe width, or when
  // expanding preview iframes.
  isZoomedOut ? iframeResizeRef : null]);

  // 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">
		<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]);
  (0, _element.useEffect)(() => {
    if (!iframeDocument || !isZoomedOut) {
      return;
    }
    iframeDocument.documentElement.classList.add('is-zoomed-out');
    const maxWidth = 800;
    iframeDocument.documentElement.style.setProperty('--wp-block-editor-iframe-zoom-out-scale', scale === 'default' ? Math.min(containerWidth, maxWidth) / prevContainerWidth.current : scale);
    iframeDocument.documentElement.style.setProperty('--wp-block-editor-iframe-zoom-out-frame-size', typeof frameSize === 'number' ? `${frameSize}px` : frameSize);
    iframeDocument.documentElement.style.setProperty('--wp-block-editor-iframe-zoom-out-content-height', `${contentHeight}px`);
    iframeDocument.documentElement.style.setProperty('--wp-block-editor-iframe-zoom-out-inner-height', `${iframeWindowInnerHeight}px`);
    iframeDocument.documentElement.style.setProperty('--wp-block-editor-iframe-zoom-out-container-width', `${containerWidth}px`);
    iframeDocument.documentElement.style.setProperty('--wp-block-editor-iframe-zoom-out-prev-container-width', `${prevContainerWidth.current}px`);
    return () => {
      iframeDocument.documentElement.classList.remove('is-zoomed-out');
      iframeDocument.documentElement.style.removeProperty('--wp-block-editor-iframe-zoom-out-scale');
      iframeDocument.documentElement.style.removeProperty('--wp-block-editor-iframe-zoom-out-frame-size');
      iframeDocument.documentElement.style.removeProperty('--wp-block-editor-iframe-zoom-out-content-height');
      iframeDocument.documentElement.style.removeProperty('--wp-block-editor-iframe-zoom-out-inner-height');
      iframeDocument.documentElement.style.removeProperty('--wp-block-editor-iframe-zoom-out-container-width');
      iframeDocument.documentElement.style.removeProperty('--wp-block-editor-iframe-zoom-out-prev-container-width');
    };
  }, [scale, frameSize, iframeDocument, iframeWindowInnerHeight, contentHeight, containerWidth, windowInnerWidth, isZoomedOut]);

  // 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: {
        border: 0,
        ...props.style,
        height: props.style?.height,
        transition: 'all .3s'
      },
      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 throught 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",
    ref: windowResizeRef,
    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-container-width': isZoomedOut && `${containerWidth}px`,
        '--wp-block-editor-iframe-zoom-out-prev-container-width': isZoomedOut && `${prevContainerWidth.current}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