UNPKG

@gechiui/block-editor

Version:
290 lines (251 loc) 7.81 kB
/** * External dependencies */ import classnames from 'classnames'; /** * GeChiUI dependencies */ import { useState, createPortal, forwardRef, useEffect, useMemo, useReducer, } from '@gechiui/element'; import { __ } from '@gechiui/i18n'; import { useMergeRefs, useRefEffect } from '@gechiui/compose'; import { __experimentalStyleProvider as StyleProvider } from '@gechiui/components'; /** * Internal dependencies */ import { useBlockSelectionClearer } from '../block-selection-clearer'; import { useWritingFlow } from '../writing-flow'; 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( ( { selectorText } ) => 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 useMemo( () => { const doc = document.implementation.createHTMLDocument( '' ); doc.body.innerHTML = html; return Array.from( doc.body.children ); }, [ html ] ); } async function loadScript( head, { id, src } ) { 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( { contentRef, children, head, tabIndex = 0, ...props }, ref ) { const [ , forceRender ] = useReducer( () => ( {} ) ); const [ iframeDocument, setIframeDocument ] = useState(); const [ bodyClasses, setBodyClasses ] = useState( [] ); const styles = useParsedAssets( window.__editorAssets?.styles ); const scripts = useParsedAssets( window.__editorAssets?.scripts ); const clearerRef = useBlockSelectionClearer(); const [ before, writingFlowRef, after ] = useWritingFlow(); const setRef = 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 = 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 = useMergeRefs( [ contentRef, clearerRef, writingFlowRef ] ); useEffect( () => { if ( iframeDocument ) { styleSheetsCompat( iframeDocument ); } }, [ iframeDocument ] ); head = ( <> <style>{ 'body{margin:0}' }</style> { styles.map( ( { tagName, href, id, rel, media, textContent } ) => { const TagName = tagName.toLowerCase(); if ( TagName === 'style' ) { return ( <TagName { ...{ id } } key={ id }> { textContent } </TagName> ); } return ( <TagName { ...{ href, id, rel, media } } key={ id } /> ); } ) } { head } </> ); return ( <> { tabIndex >= 0 && before } <iframe { ...props } ref={ useMergeRefs( [ ref, setRef ] ) } tabIndex={ tabIndex } title={ __( '编辑器画布' ) } > { iframeDocument && createPortal( <> <head ref={ headRef }>{ head }</head> <body ref={ bodyRef } className={ classnames( BODY_CLASS_NAME, ...bodyClasses ) } > <StyleProvider document={ iframeDocument }> { children } </StyleProvider> </body> </>, iframeDocument.documentElement ) } </iframe> { tabIndex >= 0 && after } </> ); } export default forwardRef( Iframe );