From 6205971a4a3ca49f1a33226747d3d57087adbe9f Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 21 Jul 2022 19:15:16 -0500 Subject: [PATCH 01/11] Enqueue script that adds custom element --- assets/js/base/utils/bhe-element.tsx | 64 +++++++++++ assets/js/base/utils/bhe-frontend.tsx | 160 ++++++++++++++++++++++++++ assets/js/base/utils/index.js | 2 + assets/js/base/utils/utils.ts | 33 ++++++ package-lock.json | 1 + package.json | 3 +- 6 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 assets/js/base/utils/bhe-element.tsx create mode 100644 assets/js/base/utils/bhe-frontend.tsx create mode 100644 assets/js/base/utils/utils.ts diff --git a/assets/js/base/utils/bhe-element.tsx b/assets/js/base/utils/bhe-element.tsx new file mode 100644 index 000000000..fa1397bbb --- /dev/null +++ b/assets/js/base/utils/bhe-element.tsx @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { hydrate as ReactHydrate } from 'react-dom'; +import { ReactElement } from 'react'; + +type HydrateOptions = { + technique?: 'media' | 'view' | 'idle'; + media?: string; +}; + +export const hydrate = ( + element: ReactElement, + container: Element, + hydrationOptions: HydrateOptions = {} +) => { + const { technique, media } = hydrationOptions; + const cb = () => { + ReactHydrate( element, container ); + }; + switch ( technique ) { + case 'media': + if ( media ) { + const mql = matchMedia( media ); + if ( mql.matches ) { + cb(); + } else { + mql.addEventListener( 'change', cb, { once: true } ); + } + } + break; + // Hydrate the element when is visible in the viewport. + // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API + case 'view': + try { + const io = new IntersectionObserver( ( entries ) => { + for ( const entry of entries ) { + if ( ! entry.isIntersecting ) { + continue; + } + // As soon as we hydrate, disconnect this IntersectionObserver. + io.disconnect(); + cb(); + break; // break loop on first match + } + } ); + io.observe( container.children[ 0 ] ); + } catch ( e ) { + cb(); + } + break; + case 'idle': + // Safari does not support requestIdleCalback, we use a timeout instead. https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback + if ( 'requestIdleCallback' in window ) { + window.requestIdleCallback( cb ); + } else { + setTimeout( cb, 200 ); + } + break; + // Hydrate this component immediately. + default: + cb(); + } +}; diff --git a/assets/js/base/utils/bhe-frontend.tsx b/assets/js/base/utils/bhe-frontend.tsx new file mode 100644 index 000000000..96c965eed --- /dev/null +++ b/assets/js/base/utils/bhe-frontend.tsx @@ -0,0 +1,160 @@ +/** + * External dependencies + */ +import { ReactElement } from 'react'; + +/** + * Internal dependencies + */ +import { matcherFromSource, pickKeys } from './utils'; +import { hydrate } from './bhe-element'; + +declare global { + interface Window { + blockTypes: Map< string, ReactElement >; + } +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace, @typescript-eslint/no-unused-vars + namespace JSX { + interface IntrinsicElements { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'gutenberg-inner-blocks': React.DetailedHTMLProps< + React.HTMLAttributes< HTMLElement >, + HTMLElement + >; + } + } +} + +// We assign `blockTypes` to window to make sure it's a global singleton. +// +// Have to do this because of the way we are currently bundling the code +// in this repo, each block gets its own copy of this file. +// +// We COULD fix this by doing some webpack magic to spit out the code in +// `gutenberg-packages` to a shared chunk but assigning `blockTypes` to window +// is a cheap hack for now that will be fixed once we can merge this code into Gutenberg. +if ( typeof window.blockTypes === 'undefined' ) { + window.blockTypes = new Map(); +} + +export const registerBlockType = ( name: string, Comp: ReactElement ) => { + window.blockTypes.set( name, Comp ); +}; + +const Children = ( { value, providedContext } ) => { + if ( ! value ) { + return null; + } + return ( + { + if ( el !== null ) { + // listen for the ping from the child + el.addEventListener( 'gutenberg-context', ( event ) => { + // We have to also destructure `event.detail.context` because there can + // already exist a property in the context with the same name. + event.detail.context = { + ...providedContext, + ...event?.detail?.context, + }; + } ); + } + } } + suppressHydrationWarning={ true } + dangerouslySetInnerHTML={ { __html: value } } + /> + ); +}; +Children.shouldComponentUpdate = () => false; + +class GutenbergBlock extends HTMLElement { + connectedCallback() { + setTimeout( () => { + // ping the parent for the context + const event = new CustomEvent( 'gutenberg-context', { + detail: {}, + bubbles: true, + cancelable: true, + } ); + this.dispatchEvent( event ); + + const usesContext = JSON.parse( + this.getAttribute( 'data-gutenberg-context-used' ) as string + ); + const providesContext = JSON.parse( + this.getAttribute( 'data-gutenberg-context-provided' ) as string + ); + const attributes = JSON.parse( + this.getAttribute( 'data-gutenberg-attributes' ) as string + ); + const sourcedAttributes = JSON.parse( + this.getAttribute( + 'data-gutenberg-sourced-attributes' + ) as string + ); + + for ( const attr in sourcedAttributes ) { + attributes[ attr ] = matcherFromSource( + sourcedAttributes[ attr ] + )( this ); + } + + // pass the context to children if needed + const providedContext = + providesContext && + pickKeys( attributes, Object.keys( providesContext ) ); + + // select only the parts of the context that the block declared in + // the `usesContext` of its block.json + const context = pickKeys( event.detail.context, usesContext ); + + const blockType = this.getAttribute( 'data-gutenberg-block-type' ); + const blockProps = { + className: this.children[ 0 ].className, + style: this.children[ 0 ].style, + }; + + const innerBlocks = this.querySelector( + 'template.gutenberg-inner-blocks' + ); + const Comp = window.blockTypes.get( blockType ); + const technique = this.getAttribute( 'data-gutenberg-hydrate' ); + const media = this.getAttribute( 'data-gutenberg-media' ); + const hydrationOptions = { technique, media }; + hydrate( + <> + + + +