// @ts-nocheck /** * External dependencies */ import { useContext, useMemo, useEffect, useRef, useLayoutEffect, } from 'preact/hooks'; import { deepSignal, peek } from 'deepsignal'; /** * Internal dependencies */ import { createPortal } from './portals'; import { useSignalEffect } from './utils'; import { directive, getScope, getEvaluate } from './hooks'; import { SlotProvider, Slot, Fill } from './slots'; import { navigate } from './router'; const isObject = ( item ) => item && typeof item === 'object' && ! Array.isArray( item ); const mergeDeepSignals = ( target, source, overwrite ) => { for ( const k in source ) { if ( isObject( peek( target, k ) ) && isObject( peek( source, k ) ) ) { mergeDeepSignals( target[ `$${ k }` ].peek(), source[ `$${ k }` ].peek(), overwrite ); } else if ( typeof peek( target, k ) === 'undefined' ) { target[ `$${ k }` ] = source[ `$${ k }` ]; } else if ( overwrite ) { target[ k ] = peek( source, k ); } } }; const deepClone = ( o ) => { if ( isObject( o ) ) return Object.fromEntries( Object.entries( o ).map( ( [ k, v ] ) => [ k, deepClone( v ) ] ) ); if ( Array.isArray( o ) ) { return [ ...o.map( ( i ) => deepClone( i ) ) ]; } return o; }; export default () => { // data-wc-context directive( 'context', ( { directives: { context }, props: { children }, context: inheritedContext, } ) => { const { Provider } = inheritedContext; const inheritedValue = useContext( inheritedContext ); const currentValue = useRef( deepSignal( {} ) ); const passedValues = context.map( ( { value } ) => value ); currentValue.current = useMemo( () => { const newValue = context .map( ( c ) => deepSignal( { [ c.namespace ]: deepClone( c.value ) } ) ) .reduceRight( mergeDeepSignals ); mergeDeepSignals( newValue, inheritedValue ); mergeDeepSignals( currentValue.current, newValue, true ); return currentValue.current; }, [ inheritedValue, ...passedValues ] ); return ( { children } ); }, { priority: 5 } ); // data-wc-body directive( 'body', ( { props: { children } } ) => { return createPortal( children, document.body ); } ); // data-wc-watch--[name] directive( 'watch', ( { directives: { watch }, evaluate } ) => { watch.forEach( ( entry ) => { useSignalEffect( () => evaluate( entry ) ); } ); } ); // data-wc-layout-init--[name] directive( 'layout-init', ( { directives: { 'layout-init': layoutInit }, evaluate } ) => { layoutInit.forEach( ( entry ) => { useLayoutEffect( () => evaluate( entry ), [] ); } ); } ); // data-wc-init--[name] directive( 'init', ( { directives: { init }, evaluate } ) => { init.forEach( ( entry ) => { useEffect( () => evaluate( entry ), [] ); } ); } ); // data-wc-on--[event] directive( 'on', ( { directives: { on }, element, evaluate } ) => { const events = new Map(); on.forEach( ( entry ) => { const event = entry.suffix.split( '--' )[ 0 ]; if ( ! events.has( event ) ) events.set( event, new Set() ); events.get( event ).add( entry ); } ); events.forEach( ( entries, event ) => { element.props[ `on${ event }` ] = ( event ) => { entries.forEach( ( entry ) => { evaluate( entry, event ); } ); }; } ); } ); // data-wc-class--[classname] directive( 'class', ( { directives: { class: className }, element, evaluate } ) => { className .filter( ( { suffix } ) => suffix !== 'default' ) .forEach( ( entry ) => { const name = entry.suffix; const result = evaluate( entry, { className: name } ); const currentClass = element.props.class || ''; const classFinder = new RegExp( `(^|\\s)${ name }(\\s|$)`, 'g' ); if ( ! result ) element.props.class = currentClass .replace( classFinder, ' ' ) .trim(); else if ( ! classFinder.test( currentClass ) ) element.props.class = currentClass ? `${ currentClass } ${ name }` : name; useEffect( () => { // This seems necessary because Preact doesn't change the class // names on the hydration, so we have to do it manually. It doesn't // need deps because it only needs to do it the first time. if ( ! result ) { element.ref.current.classList.remove( name ); } else { element.ref.current.classList.add( name ); } }, [] ); } ); } ); const newRule = /(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g; const ruleClean = /\/\*[^]*?\*\/| +/g; const ruleNewline = /\n+/g; const empty = ' '; /** * Convert a css style string into a object. * * Made by Cristian Bote (@cristianbote) for Goober. * https://unpkg.com/browse/goober@2.1.13/src/core/astish.js * * @param {string} val CSS string. * @return {Object} CSS object. */ const cssStringToObject = ( val ) => { const tree = [ {} ]; let block, left; while ( ( block = newRule.exec( val.replace( ruleClean, '' ) ) ) ) { if ( block[ 4 ] ) { tree.shift(); } else if ( block[ 3 ] ) { left = block[ 3 ].replace( ruleNewline, empty ).trim(); tree.unshift( ( tree[ 0 ][ left ] = tree[ 0 ][ left ] || {} ) ); } else { tree[ 0 ][ block[ 1 ] ] = block[ 2 ] .replace( ruleNewline, empty ) .trim(); } } return tree[ 0 ]; }; // data-wc-style--[style-key] directive( 'style', ( { directives: { style }, element, evaluate } ) => { style .filter( ( { suffix } ) => suffix !== 'default' ) .forEach( ( entry ) => { const key = entry.suffix; const result = evaluate( entry, { key } ); element.props.style = element.props.style || {}; if ( typeof element.props.style === 'string' ) element.props.style = cssStringToObject( element.props.style ); if ( ! result ) delete element.props.style[ key ]; else element.props.style[ key ] = result; useEffect( () => { // This seems necessary because Preact doesn't change the styles on // the hydration, so we have to do it manually. It doesn't need deps // because it only needs to do it the first time. if ( ! result ) { element.ref.current.style.removeProperty( key ); } else { element.ref.current.style[ key ] = result; } }, [] ); } ); } ); // data-wc-bind--[attribute] directive( 'bind', ( { directives: { bind }, element, evaluate } ) => { bind.filter( ( { suffix } ) => suffix !== 'default' ).forEach( ( entry ) => { const attribute = entry.suffix; const result = evaluate( entry ); element.props[ attribute ] = result; // Preact doesn't handle the `role` attribute properly, as it doesn't remove it when `null`. // We need this workaround until the following issue is solved: // https://github.com/preactjs/preact/issues/4136 useLayoutEffect( () => { if ( attribute === 'role' && ( result === null || result === undefined ) ) { element.ref.current.removeAttribute( attribute ); } }, [ attribute, result ] ); // This seems necessary because Preact doesn't change the attributes // on the hydration, so we have to do it manually. It doesn't need // deps because it only needs to do it the first time. useEffect( () => { const el = element.ref.current; // We set the value directly to the corresponding // HTMLElement instance property excluding the following // special cases. // We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L110-L129 if ( attribute !== 'width' && attribute !== 'height' && attribute !== 'href' && attribute !== 'list' && attribute !== 'form' && // Default value in browsers is `-1` and an empty string is // cast to `0` instead attribute !== 'tabIndex' && attribute !== 'download' && attribute !== 'rowSpan' && attribute !== 'colSpan' && attribute !== 'role' && attribute in el ) { try { el[ attribute ] = result === null || result === undefined ? '' : result; return; } catch ( err ) {} } // aria- and data- attributes have no boolean representation. // A `false` value is different from the attribute not being // present, so we can't remove it. // We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136 if ( result !== null && result !== undefined && ( result !== false || attribute[ 4 ] === '-' ) ) { el.setAttribute( attribute, result ); } else { el.removeAttribute( attribute ); } }, [] ); } ); } ); // data-wc-navigation-link directive( 'navigation-link', ( { directives: { 'navigation-link': navigationLink }, props: { href }, element, } ) => { const { value: link } = navigationLink.find( ( { suffix } ) => suffix === 'default' ); // For some reason this useEffect crashes in Preact internals in some cases. // Since it is a no-op right now, we can just comment it out. // useEffect( () => { // // Prefetch the page if it is in the directive options. // if ( link?.prefetch ) { // // prefetch( href ); // } // } ); // Don't do anything if it's falsy. if ( link !== false ) { element.props.onclick = async ( event ) => { event.preventDefault(); // Fetch the page (or return it from cache). await navigate( href ); // Update the scroll, depending on the option. True by default. if ( link?.scroll === 'smooth' ) { window.scrollTo( { top: 0, left: 0, behavior: 'smooth', } ); } else if ( link?.scroll !== false ) { window.scrollTo( 0, 0 ); } }; } } ); // data-wc-ignore directive( 'ignore', ( { element: { type: Type, props: { innerHTML, ...rest }, }, } ) => { // Preserve the initial inner HTML. const cached = useMemo( () => innerHTML, [] ); return ( ); } ); // data-wc-text directive( 'text', ( { directives: { text }, element, evaluate } ) => { const entry = text.find( ( { suffix } ) => suffix === 'default' ); element.props.children = evaluate( entry ); } ); // data-wc-slot directive( 'slot', ( { directives: { slot }, props: { children }, element } ) => { const { value } = slot.find( ( { suffix } ) => suffix === 'default' ); const name = typeof value === 'string' ? value : value.name; const position = value.position || 'children'; if ( position === 'before' ) { return ( <> { children } ); } if ( position === 'after' ) { return ( <> { children } ); } if ( position === 'replace' ) { return { children }; } if ( position === 'children' ) { element.props.children = ( { element.props.children } ); } }, { priority: 4 } ); // data-wc-fill directive( 'fill', ( { directives: { fill }, props: { children }, evaluate } ) => { const entry = fill.find( ( { suffix } ) => suffix === 'default' ); const slot = evaluate( entry ); return { children }; }, { priority: 4 } ); // data-wc-slot-provider directive( 'slot-provider', ( { props: { children } } ) => ( { children } ), { priority: 4 } ); directive( 'each', ( { directives: { each, 'each-key': [ eachKey ], }, context: inheritedContext, element, evaluate, } ) => { const { Provider } = inheritedContext; const inheritedValue = useContext( inheritedContext ); const [ entry ] = each; const { namespace, suffix } = entry; const list = evaluate( entry ); return list.map( ( item ) => { const mergedContext = deepSignal( {} ); const itemProp = suffix === 'default' ? 'item' : suffix; const newValue = deepSignal( { [ namespace ]: { [ itemProp ]: item }, } ); mergeDeepSignals( newValue, inheritedValue ); mergeDeepSignals( mergedContext, newValue, true ); const scope = { ...getScope(), context: mergedContext }; const key = getEvaluate( { scope } )( eachKey ); return ( { element.props.content } ); } ); }, { priority: 20 } ); directive( 'each-child', () => null ); };