rebase from live enviornment
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export const csnMetaTagItemprop = 'wc-client-side-navigation';
|
||||
export const directivePrefix = 'wc';
|
||||
@@ -0,0 +1,291 @@
|
||||
import {
|
||||
useContext,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
} from 'preact/hooks';
|
||||
import { deepSignal, peek } from 'deepsignal';
|
||||
import { useSignalEffect } from './utils';
|
||||
import { directive } from './hooks';
|
||||
import { prefetch, 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 ( overwrite || typeof peek( target, k ) === 'undefined' ) {
|
||||
target[ `$${ k }` ] = source[ `$${ k }` ];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default () => {
|
||||
// data-wc-context
|
||||
directive(
|
||||
'context',
|
||||
( {
|
||||
directives: {
|
||||
context: { default: newContext },
|
||||
},
|
||||
props: { children },
|
||||
context: inheritedContext,
|
||||
} ) => {
|
||||
const { Provider } = inheritedContext;
|
||||
const inheritedValue = useContext( inheritedContext );
|
||||
const currentValue = useRef( deepSignal( {} ) );
|
||||
currentValue.current = useMemo( () => {
|
||||
const newValue = deepSignal( newContext );
|
||||
mergeDeepSignals( newValue, inheritedValue );
|
||||
mergeDeepSignals( currentValue.current, newValue, true );
|
||||
return currentValue.current;
|
||||
}, [ newContext, inheritedValue ] );
|
||||
|
||||
return (
|
||||
<Provider value={ currentValue.current }>{ children }</Provider>
|
||||
);
|
||||
},
|
||||
{ priority: 5 }
|
||||
);
|
||||
|
||||
// data-wc-effect--[name]
|
||||
directive( 'effect', ( { directives: { effect }, context, evaluate } ) => {
|
||||
const contextValue = useContext( context );
|
||||
Object.values( effect ).forEach( ( path ) => {
|
||||
useSignalEffect( () => {
|
||||
return evaluate( path, { context: contextValue } );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
// data-wc-layout-init--[name]
|
||||
directive(
|
||||
'layout-init',
|
||||
( {
|
||||
directives: { 'layout-init': layoutInit },
|
||||
context,
|
||||
evaluate,
|
||||
} ) => {
|
||||
const contextValue = useContext( context );
|
||||
Object.values( layoutInit ).forEach( ( path ) => {
|
||||
useLayoutEffect( () => {
|
||||
return evaluate( path, { context: contextValue } );
|
||||
}, [] );
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
// data-wc-init--[name]
|
||||
directive( 'init', ( { directives: { init }, context, evaluate } ) => {
|
||||
const contextValue = useContext( context );
|
||||
Object.values( init ).forEach( ( path ) => {
|
||||
useEffect( () => {
|
||||
return evaluate( path, { context: contextValue } );
|
||||
}, [] );
|
||||
} );
|
||||
} );
|
||||
|
||||
// data-wc-on--[event]
|
||||
directive( 'on', ( { directives: { on }, element, evaluate, context } ) => {
|
||||
const contextValue = useContext( context );
|
||||
const events = new Map();
|
||||
Object.entries( on ).forEach( ( [ name, path ] ) => {
|
||||
const event = name.split( '--' )[ 0 ];
|
||||
if ( ! events.has( event ) ) events.set( event, new Set() );
|
||||
events.get( event ).add( path );
|
||||
} );
|
||||
events.forEach( ( paths, event ) => {
|
||||
element.props[ `on${ event }` ] = ( event ) => {
|
||||
paths.forEach( ( path ) => {
|
||||
evaluate( path, { event, context: contextValue } );
|
||||
} );
|
||||
};
|
||||
} );
|
||||
} );
|
||||
|
||||
// data-wc-class--[classname]
|
||||
directive(
|
||||
'class',
|
||||
( {
|
||||
directives: { class: className },
|
||||
element,
|
||||
evaluate,
|
||||
context,
|
||||
} ) => {
|
||||
const contextValue = useContext( context );
|
||||
Object.keys( className )
|
||||
.filter( ( n ) => n !== 'default' )
|
||||
.forEach( ( name ) => {
|
||||
const result = evaluate( className[ name ], {
|
||||
className: name,
|
||||
context: contextValue,
|
||||
} );
|
||||
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 );
|
||||
}
|
||||
}, [] );
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
// data-wc-bind--[attribute]
|
||||
directive(
|
||||
'bind',
|
||||
( { directives: { bind }, element, context, evaluate } ) => {
|
||||
const contextValue = useContext( context );
|
||||
Object.entries( bind )
|
||||
.filter( ( n ) => n !== 'default' )
|
||||
.forEach( ( [ attribute, path ] ) => {
|
||||
const result = evaluate( path, {
|
||||
context: contextValue,
|
||||
} );
|
||||
element.props[ 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( () => {
|
||||
// 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 === false && attribute[ 4 ] !== '-' ) {
|
||||
element.ref.current.removeAttribute( attribute );
|
||||
} else {
|
||||
element.ref.current.setAttribute(
|
||||
attribute,
|
||||
result === true && attribute[ 4 ] !== '-'
|
||||
? ''
|
||||
: result
|
||||
);
|
||||
}
|
||||
}, [] );
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
// data-wc-navigation-link
|
||||
directive(
|
||||
'navigation-link',
|
||||
( {
|
||||
directives: {
|
||||
'navigation-link': { default: link },
|
||||
},
|
||||
props: { href },
|
||||
element,
|
||||
} ) => {
|
||||
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-show
|
||||
directive(
|
||||
'show',
|
||||
( {
|
||||
directives: {
|
||||
show: { default: show },
|
||||
},
|
||||
element,
|
||||
evaluate,
|
||||
context,
|
||||
} ) => {
|
||||
const contextValue = useContext( context );
|
||||
|
||||
if ( ! evaluate( show, { context: contextValue } ) )
|
||||
element.props.children = (
|
||||
<template>{ element.props.children }</template>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// data-wc-ignore
|
||||
directive(
|
||||
'ignore',
|
||||
( {
|
||||
element: {
|
||||
type: Type,
|
||||
props: { innerHTML, ...rest },
|
||||
},
|
||||
} ) => {
|
||||
// Preserve the initial inner HTML.
|
||||
const cached = useMemo( () => innerHTML, [] );
|
||||
return (
|
||||
<Type
|
||||
dangerouslySetInnerHTML={ { __html: cached } }
|
||||
{ ...rest }
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// data-wc-text
|
||||
directive(
|
||||
'text',
|
||||
( {
|
||||
directives: {
|
||||
text: { default: text },
|
||||
},
|
||||
element,
|
||||
evaluate,
|
||||
context,
|
||||
} ) => {
|
||||
const contextValue = useContext( context );
|
||||
element.props.children = evaluate( text, {
|
||||
context: contextValue,
|
||||
} );
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import { h, options, createContext, cloneElement } from 'preact';
|
||||
import { useRef, useMemo } from 'preact/hooks';
|
||||
import { rawStore as store } from './store';
|
||||
|
||||
// Main context.
|
||||
const context = createContext( {} );
|
||||
|
||||
// WordPress Directives.
|
||||
const directiveMap = {};
|
||||
const directivePriorities = {};
|
||||
export const directive = ( name, cb, { priority = 10 } = {} ) => {
|
||||
directiveMap[ name ] = cb;
|
||||
directivePriorities[ name ] = priority;
|
||||
};
|
||||
|
||||
// Resolve the path to some property of the store object.
|
||||
const resolve = ( path, ctx ) => {
|
||||
let current = { ...store, context: ctx };
|
||||
path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) );
|
||||
return current;
|
||||
};
|
||||
|
||||
// Generate the evaluate function.
|
||||
const getEvaluate =
|
||||
( { ref } = {} ) =>
|
||||
( path, extraArgs = {} ) => {
|
||||
// If path starts with !, remove it and save a flag.
|
||||
const hasNegationOperator =
|
||||
path[ 0 ] === '!' && !! ( path = path.slice( 1 ) );
|
||||
const value = resolve( path, extraArgs.context );
|
||||
const returnValue =
|
||||
typeof value === 'function'
|
||||
? value( {
|
||||
ref: ref.current,
|
||||
...store,
|
||||
...extraArgs,
|
||||
} )
|
||||
: value;
|
||||
return hasNegationOperator ? ! returnValue : returnValue;
|
||||
};
|
||||
|
||||
// Separate directives by priority. The resulting array contains objects
|
||||
// of directives grouped by same priority, and sorted in ascending order.
|
||||
const usePriorityLevels = ( directives ) =>
|
||||
useMemo( () => {
|
||||
const byPriority = Object.entries( directives ).reduce(
|
||||
( acc, [ name, values ] ) => {
|
||||
const priority = directivePriorities[ name ];
|
||||
if ( ! acc[ priority ] ) acc[ priority ] = {};
|
||||
acc[ priority ][ name ] = values;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return Object.entries( byPriority )
|
||||
.sort( ( [ p1 ], [ p2 ] ) => p1 - p2 )
|
||||
.map( ( [ , obj ] ) => obj );
|
||||
}, [ directives ] );
|
||||
|
||||
// Directive wrapper.
|
||||
const Directive = ( { type, directives, props: originalProps } ) => {
|
||||
const ref = useRef( null );
|
||||
const element = h( type, { ...originalProps, ref } );
|
||||
const evaluate = useMemo( () => getEvaluate( { ref } ), [] );
|
||||
|
||||
// Add wrappers recursively for each priority level.
|
||||
const byPriorityLevel = usePriorityLevels( directives );
|
||||
return (
|
||||
<RecursivePriorityLevel
|
||||
directives={ byPriorityLevel }
|
||||
element={ element }
|
||||
evaluate={ evaluate }
|
||||
originalProps={ originalProps }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Priority level wrapper.
|
||||
const RecursivePriorityLevel = ( {
|
||||
directives: [ directives, ...rest ],
|
||||
element,
|
||||
evaluate,
|
||||
originalProps,
|
||||
} ) => {
|
||||
// This element needs to be a fresh copy so we are not modifying an already
|
||||
// rendered element with Preact's internal properties initialized. This
|
||||
// prevents an error with changes in `element.props.children` not being
|
||||
// reflected in `element.__k`.
|
||||
element = cloneElement( element );
|
||||
|
||||
// Recursively render the wrapper for the next priority level.
|
||||
//
|
||||
// Note that, even though we're instantiating a vnode with a
|
||||
// `RecursivePriorityLevel` here, its render function will not be executed
|
||||
// just yet. Actually, it will be delayed until the current render function
|
||||
// has finished. That ensures directives in the current priorty level have
|
||||
// run (and thus modified the passed `element`) before the next level.
|
||||
const children =
|
||||
rest.length > 0 ? (
|
||||
<RecursivePriorityLevel
|
||||
directives={ rest }
|
||||
element={ element }
|
||||
evaluate={ evaluate }
|
||||
originalProps={ originalProps }
|
||||
/>
|
||||
) : (
|
||||
element
|
||||
);
|
||||
|
||||
const props = { ...originalProps, children };
|
||||
const directiveArgs = { directives, props, element, context, evaluate };
|
||||
|
||||
for ( const d in directives ) {
|
||||
const wrapper = directiveMap[ d ]?.( directiveArgs );
|
||||
if ( wrapper !== undefined ) props.children = wrapper;
|
||||
}
|
||||
|
||||
return props.children;
|
||||
};
|
||||
|
||||
// Preact Options Hook called each time a vnode is created.
|
||||
const old = options.vnode;
|
||||
options.vnode = ( vnode ) => {
|
||||
if ( vnode.props.__directives ) {
|
||||
const props = vnode.props;
|
||||
const directives = props.__directives;
|
||||
if ( directives.key ) vnode.props.key = directives.key.default;
|
||||
delete props.__directives;
|
||||
vnode.props = {
|
||||
type: vnode.type,
|
||||
directives,
|
||||
props,
|
||||
};
|
||||
vnode.type = Directive;
|
||||
}
|
||||
|
||||
if ( old ) old( vnode );
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import registerDirectives from './directives';
|
||||
import { init } from './router';
|
||||
import { rawStore, afterLoads } from './store';
|
||||
|
||||
export { navigate } from './router';
|
||||
export { store } from './store';
|
||||
|
||||
/**
|
||||
* Initialize the Interactivity API.
|
||||
*/
|
||||
document.addEventListener( 'DOMContentLoaded', async () => {
|
||||
registerDirectives();
|
||||
await init();
|
||||
afterLoads.forEach( ( afterLoad ) => afterLoad( rawStore ) );
|
||||
} );
|
||||
@@ -0,0 +1,121 @@
|
||||
import { hydrate, render } from 'preact';
|
||||
import { toVdom, hydratedIslands } from './vdom';
|
||||
import { createRootFragment } from './utils';
|
||||
import { directivePrefix } from './constants';
|
||||
|
||||
// The cache of visited and prefetched pages.
|
||||
const pages = new Map();
|
||||
|
||||
// Keep the same root fragment for each interactive region node.
|
||||
const regionRootFragments = new WeakMap();
|
||||
const getRegionRootFragment = ( region ) => {
|
||||
if ( ! regionRootFragments.has( region ) ) {
|
||||
regionRootFragments.set(
|
||||
region,
|
||||
createRootFragment( region.parentElement, region )
|
||||
);
|
||||
}
|
||||
return regionRootFragments.get( region );
|
||||
};
|
||||
|
||||
// Helper to remove domain and hash from the URL. We are only interesting in
|
||||
// caching the path and the query.
|
||||
const cleanUrl = ( url ) => {
|
||||
const u = new URL( url, window.location );
|
||||
return u.pathname + u.search;
|
||||
};
|
||||
|
||||
// Fetch a new page and convert it to a static virtual DOM.
|
||||
const fetchPage = async ( url ) => {
|
||||
let dom;
|
||||
try {
|
||||
const res = await window.fetch( url );
|
||||
if ( res.status !== 200 ) return false;
|
||||
const html = await res.text();
|
||||
dom = new window.DOMParser().parseFromString( html, 'text/html' );
|
||||
} catch ( e ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return regionsToVdom( dom );
|
||||
};
|
||||
|
||||
// Return an object with VDOM trees of those HTML regions marked with a
|
||||
// `navigation-id` directive.
|
||||
const regionsToVdom = ( dom ) => {
|
||||
const regions = {};
|
||||
const attrName = `data-${ directivePrefix }-navigation-id`;
|
||||
dom.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => {
|
||||
const id = region.getAttribute( attrName );
|
||||
regions[ id ] = toVdom( region );
|
||||
} );
|
||||
|
||||
return { regions };
|
||||
};
|
||||
|
||||
// Prefetch a page. We store the promise to avoid triggering a second fetch for
|
||||
// a page if a fetching has already started.
|
||||
export const prefetch = ( url ) => {
|
||||
url = cleanUrl( url );
|
||||
if ( ! pages.has( url ) ) {
|
||||
pages.set( url, fetchPage( url ) );
|
||||
}
|
||||
};
|
||||
|
||||
// Render all interactive regions contained in the given page.
|
||||
const renderRegions = ( page ) => {
|
||||
const attrName = `data-${ directivePrefix }-navigation-id`;
|
||||
document.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => {
|
||||
const id = region.getAttribute( attrName );
|
||||
const fragment = getRegionRootFragment( region );
|
||||
render( page.regions[ id ], fragment );
|
||||
} );
|
||||
};
|
||||
|
||||
// Navigate to a new page.
|
||||
export const navigate = async ( href, { replace = false } = {} ) => {
|
||||
const url = cleanUrl( href );
|
||||
prefetch( url );
|
||||
const page = await pages.get( url );
|
||||
if ( page ) {
|
||||
renderRegions( page );
|
||||
window.history[ replace ? 'replaceState' : 'pushState' ](
|
||||
{},
|
||||
'',
|
||||
href
|
||||
);
|
||||
} else {
|
||||
window.location.assign( href );
|
||||
}
|
||||
};
|
||||
|
||||
// Listen to the back and forward buttons and restore the page if it's in the
|
||||
// cache.
|
||||
window.addEventListener( 'popstate', async () => {
|
||||
const url = cleanUrl( window.location ); // Remove hash.
|
||||
const page = pages.has( url ) && ( await pages.get( url ) );
|
||||
if ( page ) {
|
||||
renderRegions( page );
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} );
|
||||
|
||||
// Initialize the router with the initial DOM.
|
||||
export const init = async () => {
|
||||
document
|
||||
.querySelectorAll( `[data-${ directivePrefix }-interactive]` )
|
||||
.forEach( ( node ) => {
|
||||
if ( ! hydratedIslands.has( node ) ) {
|
||||
const fragment = getRegionRootFragment( node );
|
||||
const vdom = toVdom( node );
|
||||
hydrate( vdom, fragment );
|
||||
}
|
||||
} );
|
||||
|
||||
// Cache the current regions.
|
||||
pages.set(
|
||||
cleanUrl( window.location ),
|
||||
Promise.resolve( regionsToVdom( document ) )
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { deepSignal } from 'deepsignal';
|
||||
|
||||
const isObject = ( item ) =>
|
||||
item && typeof item === 'object' && ! Array.isArray( item );
|
||||
|
||||
export const deepMerge = ( target, source ) => {
|
||||
if ( isObject( target ) && isObject( source ) ) {
|
||||
for ( const key in source ) {
|
||||
if ( isObject( source[ key ] ) ) {
|
||||
if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } );
|
||||
deepMerge( target[ key ], source[ key ] );
|
||||
} else {
|
||||
Object.assign( target, { [ key ]: source[ key ] } );
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getSerializedState = () => {
|
||||
const storeTag = document.querySelector(
|
||||
`script[type="application/json"]#wc-interactivity-store-data`
|
||||
);
|
||||
if ( ! storeTag ) return {};
|
||||
try {
|
||||
const { state } = JSON.parse( storeTag.textContent );
|
||||
if ( isObject( state ) ) return state;
|
||||
throw Error( 'Parsed state is not an object' );
|
||||
} catch ( e ) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log( e );
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const afterLoads = new Set();
|
||||
|
||||
const rawState = getSerializedState();
|
||||
export const rawStore = { state: deepSignal( rawState ) };
|
||||
|
||||
if ( typeof window !== 'undefined' ) window.store = rawStore;
|
||||
|
||||
export const store = ( { state, ...block }, { afterLoad } = {} ) => {
|
||||
deepMerge( rawStore, block );
|
||||
deepMerge( rawState, state );
|
||||
if ( afterLoad ) afterLoads.add( afterLoad );
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useRef, useEffect } from 'preact/hooks';
|
||||
import { effect } from '@preact/signals';
|
||||
|
||||
function afterNextFrame( callback ) {
|
||||
const done = () => {
|
||||
cancelAnimationFrame( raf );
|
||||
setTimeout( callback );
|
||||
};
|
||||
const raf = requestAnimationFrame( done );
|
||||
}
|
||||
|
||||
// Using the mangled properties:
|
||||
// this.c: this._callback
|
||||
// this.x: this._compute
|
||||
// https://github.com/preactjs/signals/blob/main/mangle.json
|
||||
function createFlusher( compute, notify ) {
|
||||
let flush;
|
||||
const dispose = effect( function () {
|
||||
flush = this.c.bind( this );
|
||||
this.x = compute;
|
||||
this.c = notify;
|
||||
return compute();
|
||||
} );
|
||||
return { flush, dispose };
|
||||
}
|
||||
|
||||
// Version of `useSignalEffect` with a `useEffect`-like execution. This hook
|
||||
// implementation comes from this PR:
|
||||
// https://github.com/preactjs/signals/pull/290.
|
||||
//
|
||||
// We need to include it here in this repo until the mentioned PR is merged.
|
||||
export function useSignalEffect( cb ) {
|
||||
const callback = useRef( cb );
|
||||
callback.current = cb;
|
||||
|
||||
useEffect( () => {
|
||||
const execute = () => callback.current();
|
||||
const notify = () => afterNextFrame( eff.flush );
|
||||
const eff = createFlusher( execute, notify );
|
||||
return eff.dispose;
|
||||
}, [] );
|
||||
}
|
||||
|
||||
// For wrapperless hydration.
|
||||
// See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
|
||||
export const createRootFragment = ( parent, replaceNode ) => {
|
||||
replaceNode = [].concat( replaceNode );
|
||||
const s = replaceNode[ replaceNode.length - 1 ].nextSibling;
|
||||
function insert( c, r ) {
|
||||
parent.insertBefore( c, r || s );
|
||||
}
|
||||
return ( parent.__k = {
|
||||
nodeType: 1,
|
||||
parentNode: parent,
|
||||
firstChild: replaceNode[ 0 ],
|
||||
childNodes: replaceNode,
|
||||
insertBefore: insert,
|
||||
appendChild: insert,
|
||||
removeChild( c ) {
|
||||
parent.removeChild( c );
|
||||
},
|
||||
} );
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import { h } from 'preact';
|
||||
import { directivePrefix as p } from './constants';
|
||||
|
||||
const ignoreAttr = `data-${ p }-ignore`;
|
||||
const islandAttr = `data-${ p }-interactive`;
|
||||
const fullPrefix = `data-${ p }-`;
|
||||
|
||||
// Regular expression for directive parsing.
|
||||
const directiveParser = new RegExp(
|
||||
`^data-${ p }-` + // ${p} must be a prefix string, like 'wp'.
|
||||
// Match alphanumeric characters including hyphen-separated
|
||||
// segments. It excludes underscore intentionally to prevent confusion.
|
||||
// E.g., "custom-directive".
|
||||
'([a-z0-9]+(?:-[a-z0-9]+)*)' +
|
||||
// (Optional) Match '--' followed by any alphanumeric charachters. It
|
||||
// excludes underscore intentionally to prevent confusion, but it can
|
||||
// contain multiple hyphens. E.g., "--custom-prefix--with-more-info".
|
||||
'(?:--([a-z0-9_-]+))?$',
|
||||
'i' // Case insensitive.
|
||||
);
|
||||
|
||||
export const hydratedIslands = new WeakSet();
|
||||
|
||||
// Recursive function that transforms a DOM tree into vDOM.
|
||||
export function toVdom( root ) {
|
||||
const treeWalker = document.createTreeWalker(
|
||||
root,
|
||||
205 // ELEMENT + TEXT + COMMENT + CDATA_SECTION + PROCESSING_INSTRUCTION
|
||||
);
|
||||
|
||||
function walk( node ) {
|
||||
const { attributes, nodeType } = node;
|
||||
|
||||
if ( nodeType === 3 ) return [ node.data ];
|
||||
if ( nodeType === 4 ) {
|
||||
const next = treeWalker.nextSibling();
|
||||
node.replaceWith( new Text( node.nodeValue ) );
|
||||
return [ node.nodeValue, next ];
|
||||
}
|
||||
if ( nodeType === 8 || nodeType === 7 ) {
|
||||
const next = treeWalker.nextSibling();
|
||||
node.remove();
|
||||
return [ null, next ];
|
||||
}
|
||||
|
||||
const props = {};
|
||||
const children = [];
|
||||
const directives = {};
|
||||
let hasDirectives = false;
|
||||
let ignore = false;
|
||||
let island = false;
|
||||
|
||||
for ( let i = 0; i < attributes.length; i++ ) {
|
||||
const n = attributes[ i ].name;
|
||||
if (
|
||||
n[ fullPrefix.length ] &&
|
||||
n.slice( 0, fullPrefix.length ) === fullPrefix
|
||||
) {
|
||||
if ( n === ignoreAttr ) {
|
||||
ignore = true;
|
||||
} else if ( n === islandAttr ) {
|
||||
island = true;
|
||||
} else {
|
||||
hasDirectives = true;
|
||||
let val = attributes[ i ].value;
|
||||
try {
|
||||
val = JSON.parse( val );
|
||||
} catch ( e ) {}
|
||||
const [ , prefix, suffix ] = directiveParser.exec( n );
|
||||
directives[ prefix ] = directives[ prefix ] || {};
|
||||
directives[ prefix ][ suffix || 'default' ] = val;
|
||||
}
|
||||
} else if ( n === 'ref' ) {
|
||||
continue;
|
||||
}
|
||||
props[ n ] = attributes[ i ].value;
|
||||
}
|
||||
|
||||
if ( ignore && ! island )
|
||||
return [
|
||||
h( node.localName, {
|
||||
...props,
|
||||
innerHTML: node.innerHTML,
|
||||
__directives: { ignore: true },
|
||||
} ),
|
||||
];
|
||||
if ( island ) hydratedIslands.add( node );
|
||||
|
||||
if ( hasDirectives ) props.__directives = directives;
|
||||
|
||||
let child = treeWalker.firstChild();
|
||||
if ( child ) {
|
||||
while ( child ) {
|
||||
const [ vnode, nextChild ] = walk( child );
|
||||
if ( vnode ) children.push( vnode );
|
||||
child = nextChild || treeWalker.nextSibling();
|
||||
}
|
||||
treeWalker.parentNode();
|
||||
}
|
||||
|
||||
return [ h( node.localName, props, children ) ];
|
||||
}
|
||||
|
||||
return walk( treeWalker.currentNode );
|
||||
}
|
||||
Reference in New Issue
Block a user