2023-01-26 11:39:25 +00:00
|
|
|
import { useContext, useMemo, useEffect } from 'preact/hooks';
|
|
|
|
import { useSignalEffect } from '@preact/signals';
|
2023-02-20 16:48:33 +00:00
|
|
|
import { deepSignal, peek } from 'deepsignal';
|
2023-01-26 11:39:25 +00:00
|
|
|
import { directive } from './hooks';
|
|
|
|
import { prefetch, navigate, hasClientSideTransitions } from './router';
|
|
|
|
|
|
|
|
// Until useSignalEffects is fixed:
|
|
|
|
// https://github.com/preactjs/signals/issues/228
|
|
|
|
const raf = window.requestAnimationFrame;
|
|
|
|
const tick = () => new Promise( ( r ) => raf( () => raf( r ) ) );
|
|
|
|
|
|
|
|
// Check if current page has client-side transitions enabled.
|
|
|
|
const clientSideTransitions = hasClientSideTransitions( document.head );
|
|
|
|
|
2023-02-20 16:48:33 +00:00
|
|
|
const isObject = ( item ) =>
|
|
|
|
item && typeof item === 'object' && ! Array.isArray( item );
|
|
|
|
|
|
|
|
const mergeDeepSignals = ( target, source ) => {
|
|
|
|
for ( const k in source ) {
|
|
|
|
if ( typeof peek( target, k ) === 'undefined' ) {
|
|
|
|
target[ `$${ k }` ] = source[ `$${ k }` ];
|
|
|
|
} else if (
|
|
|
|
isObject( peek( target, k ) ) &&
|
|
|
|
isObject( peek( source, k ) )
|
|
|
|
) {
|
|
|
|
mergeDeepSignals(
|
|
|
|
target[ `$${ k }` ].peek(),
|
|
|
|
source[ `$${ k }` ].peek()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-01-26 11:39:25 +00:00
|
|
|
export default () => {
|
|
|
|
// wp-context
|
|
|
|
directive(
|
|
|
|
'context',
|
|
|
|
( {
|
|
|
|
directives: {
|
|
|
|
context: { default: context },
|
|
|
|
},
|
|
|
|
props: { children },
|
2023-02-20 16:48:33 +00:00
|
|
|
context: inherited,
|
2023-01-26 11:39:25 +00:00
|
|
|
} ) => {
|
2023-02-20 16:48:33 +00:00
|
|
|
const { Provider } = inherited;
|
|
|
|
const inheritedValue = useContext( inherited );
|
|
|
|
const value = useMemo( () => {
|
|
|
|
const localValue = deepSignal( context );
|
|
|
|
mergeDeepSignals( localValue, inheritedValue );
|
|
|
|
return localValue;
|
|
|
|
}, [ context, inheritedValue ] );
|
|
|
|
|
|
|
|
return <Provider value={ value }>{ children }</Provider>;
|
2023-01-26 11:39:25 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
// wp-effect:[name]
|
|
|
|
directive( 'effect', ( { directives: { effect }, context, evaluate } ) => {
|
|
|
|
const contextValue = useContext( context );
|
|
|
|
Object.values( effect ).forEach( ( path ) => {
|
|
|
|
useSignalEffect( () => {
|
|
|
|
evaluate( path, { context: contextValue } );
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
// wp-on:[event]
|
|
|
|
directive( 'on', ( { directives: { on }, element, evaluate, context } ) => {
|
|
|
|
const contextValue = useContext( context );
|
|
|
|
Object.entries( on ).forEach( ( [ name, path ] ) => {
|
|
|
|
element.props[ `on${ name }` ] = ( event ) => {
|
|
|
|
evaluate( path, { event, context: contextValue } );
|
|
|
|
};
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
// wp-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,
|
|
|
|
} );
|
|
|
|
if ( ! result )
|
|
|
|
element.props.class = element.props.class
|
|
|
|
.replace(
|
|
|
|
new RegExp( `(^|\\s)${ name }(\\s|$)`, 'g' ),
|
|
|
|
' '
|
|
|
|
)
|
|
|
|
.trim();
|
|
|
|
else if (
|
|
|
|
! new RegExp( `(^|\\s)${ name }(\\s|$)` ).test(
|
|
|
|
element.props.class
|
|
|
|
)
|
|
|
|
)
|
|
|
|
element.props.class += ` ${ 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 );
|
|
|
|
}
|
|
|
|
}, [] );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
// wp-bind:[attribute]
|
|
|
|
directive(
|
|
|
|
'bind',
|
|
|
|
( { directives: { bind }, element, context, evaluate } ) => {
|
|
|
|
const contextValue = useContext( context );
|
|
|
|
Object.entries( bind )
|
|
|
|
.filter( ( n ) => n !== 'default' )
|
|
|
|
.forEach( ( [ attribute, path ] ) => {
|
|
|
|
element.props[ attribute ] = evaluate( path, {
|
|
|
|
context: contextValue,
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
// wp-link
|
|
|
|
directive(
|
|
|
|
'link',
|
|
|
|
( {
|
|
|
|
directives: {
|
|
|
|
link: { default: link },
|
|
|
|
},
|
|
|
|
props: { href },
|
|
|
|
element,
|
|
|
|
} ) => {
|
|
|
|
useEffect( () => {
|
|
|
|
// Prefetch the page if it is in the directive options.
|
|
|
|
if ( clientSideTransitions && link?.prefetch ) {
|
|
|
|
prefetch( href );
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
|
|
|
// Don't do anything if it's falsy.
|
|
|
|
if ( clientSideTransitions && 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 );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|