Merged in feature/MAW-855-import-code-into-aws (pull request #2)

code import from pantheon

* code import from pantheon
This commit is contained in:
Tony Volpe
2023-12-04 23:08:14 +00:00
parent 8c9b1312bc
commit 8f4b5efda6
4766 changed files with 185592 additions and 239967 deletions

View File

@@ -1,11 +1,8 @@
import { useContext, useMemo, useEffect } from 'preact/hooks';
import { useContext, useMemo, useEffect, useLayoutEffect } from 'preact/hooks';
import { deepSignal, peek } from 'deepsignal';
import { useSignalEffect } from './utils';
import { directive } from './hooks';
import { prefetch, navigate, canDoClientSideNavigation } from './router';
// Check if current page can do client-side navigation.
const clientSideNavigation = canDoClientSideNavigation( document.head );
import { prefetch, navigate } from './router';
const isObject = ( item ) =>
item && typeof item === 'object' && ! Array.isArray( item );
@@ -60,6 +57,23 @@ export default () => {
} );
} );
// 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-on--[event]
directive( 'on', ( { directives: { on }, element, evaluate, context } ) => {
const contextValue = useContext( context );
@@ -151,25 +165,25 @@ export default () => {
}
);
// data-wc-link
// data-wc-navigation-link
directive(
'link',
'navigation-link',
( {
directives: {
link: { default: link },
'navigation-link': { default: link },
},
props: { href },
element,
} ) => {
useEffect( () => {
// Prefetch the page if it is in the directive options.
if ( clientSideNavigation && link?.prefetch ) {
if ( link?.prefetch ) {
prefetch( href );
}
} );
// Don't do anything if it's falsy.
if ( clientSideNavigation && link !== false ) {
if ( link !== false ) {
element.props.onclick = async ( event ) => {
event.preventDefault();

View File

@@ -126,6 +126,7 @@ 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,

View File

@@ -1,7 +1,9 @@
import registerDirectives from './directives';
import { init } from './router';
export { store } from './store';
import { rawStore, afterLoads } from './store';
export { navigate } from './router';
export { store } from './store';
/**
* Initialize the Interactivity API.
@@ -9,6 +11,5 @@ export { navigate } from './router';
document.addEventListener( 'DOMContentLoaded', async () => {
registerDirectives();
await init();
// eslint-disable-next-line no-console
console.log( 'Interactivity API started' );
afterLoads.forEach( ( afterLoad ) => afterLoad( rawStore ) );
} );

View File

@@ -1,15 +1,22 @@
import { hydrate, render } from 'preact';
import { toVdom, hydratedIslands } from './vdom';
import { createRootFragment } from './utils';
import { csnMetaTagItemprop, directivePrefix } from './constants';
import { directivePrefix } from './constants';
// The root to render the vdom (document.body).
let rootFragment;
// The cache of visited and prefetched pages, stylesheets and scripts.
// The cache of visited and prefetched pages.
const pages = new Map();
const stylesheets = new Map();
const scripts = 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.
@@ -18,94 +25,32 @@ const cleanUrl = ( url ) => {
return u.pathname + u.search;
};
// Helper to check if a page can do client-side navigation.
export const canDoClientSideNavigation = ( dom ) =>
dom
.querySelector( `meta[itemprop='${ csnMetaTagItemprop }']` )
?.getAttribute( 'content' ) === 'active';
/**
* Finds the elements in the document that match the selector and fetch them.
* For each element found, fetch the content and store it in the cache.
* Returns an array of elements to add to the document.
*
* @param {Document} document
* @param {string} selector - CSS selector used to find the elements.
* @param {'href'|'src'} attribute - Attribute that determines where to fetch
* the styles or scripts from. Also used as the key for the cache.
* @param {Map} cache - Cache to use for the elements. Can be `stylesheets` or `scripts`.
* @param {'style'|'script'} elementToCreate - Element to create for each fetched
* item. Can be 'style' or 'script'.
* @return {Promise<Array<HTMLElement>>} - Array of elements to add to the document.
*/
const fetchScriptOrStyle = async (
document,
selector,
attribute,
cache,
elementToCreate
) => {
const fetchedItems = await Promise.all(
[].map.call( document.querySelectorAll( selector ), ( el ) => {
const attributeValue = el.getAttribute( attribute );
if ( ! cache.has( attributeValue ) )
cache.set(
attributeValue,
fetch( attributeValue ).then( ( r ) => r.text() )
);
return cache.get( attributeValue );
} )
);
return fetchedItems.map( ( item ) => {
const element = document.createElement( elementToCreate );
element.textContent = item;
return element;
} );
};
// Fetch styles of a new page.
const fetchAssets = async ( document ) => {
const stylesFromSheets = await fetchScriptOrStyle(
document,
'link[rel=stylesheet]',
'href',
stylesheets,
'style'
);
const scriptTags = await fetchScriptOrStyle(
document,
'script[src]',
'src',
scripts,
'script'
);
const moduleScripts = await fetchScriptOrStyle(
document,
'script[type=module]',
'src',
scripts,
'script'
);
moduleScripts.forEach( ( script ) =>
script.setAttribute( 'type', 'module' )
);
return [
...scriptTags,
document.querySelector( 'title' ),
...document.querySelectorAll( 'style' ),
...stylesFromSheets,
];
};
// Fetch a new page and convert it to a static virtual DOM.
const fetchPage = async ( url ) => {
const html = await window.fetch( url ).then( ( r ) => r.text() );
const dom = new window.DOMParser().parseFromString( html, 'text/html' );
if ( ! canDoClientSideNavigation( dom.head ) ) return false;
const head = await fetchAssets( dom );
return { head, body: toVdom( dom.body ) };
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
@@ -117,14 +62,23 @@ export const prefetch = ( 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 ) {
document.head.replaceChildren( ...page.head );
render( page.body, rootFragment );
renderRegions( page );
window.history[ replace ? 'replaceState' : 'pushState' ](
{},
'',
@@ -141,8 +95,7 @@ window.addEventListener( 'popstate', async () => {
const url = cleanUrl( window.location ); // Remove hash.
const page = pages.has( url ) && ( await pages.get( url ) );
if ( page ) {
document.head.replaceChildren( ...page.head );
render( page.body, rootFragment );
renderRegions( page );
} else {
window.location.reload();
}
@@ -150,37 +103,19 @@ window.addEventListener( 'popstate', async () => {
// Initialize the router with the initial DOM.
export const init = async () => {
if ( canDoClientSideNavigation( document.head ) ) {
// Create the root fragment to hydrate everything.
rootFragment = createRootFragment(
document.documentElement,
document.body
);
const body = toVdom( document.body );
hydrate( body, rootFragment );
// Cache the scripts. Has to be called before fetching the assets.
[].map.call( document.querySelectorAll( 'script[src]' ), ( script ) => {
scripts.set( script.getAttribute( 'src' ), script.textContent );
document
.querySelectorAll( `[data-${ directivePrefix }-interactive]` )
.forEach( ( node ) => {
if ( ! hydratedIslands.has( node ) ) {
const fragment = getRegionRootFragment( node );
const vdom = toVdom( node );
hydrate( vdom, fragment );
}
} );
const head = await fetchAssets( document );
pages.set(
cleanUrl( window.location ),
Promise.resolve( { body, head } )
);
} else {
document
.querySelectorAll( `[data-${ directivePrefix }-interactive]` )
.forEach( ( node ) => {
if ( ! hydratedIslands.has( node ) ) {
const fragment = createRootFragment(
node.parentNode,
node
);
const vdom = toVdom( node );
hydrate( vdom, fragment );
}
} );
}
// Cache the current regions.
pages.set(
cleanUrl( window.location ),
Promise.resolve( regionsToVdom( document ) )
);
};

View File

@@ -32,12 +32,15 @@ const getSerializedState = () => {
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 } ) => {
export const store = ( { state, ...block }, { afterLoad } = {} ) => {
deepMerge( rawStore, block );
deepMerge( rawState, state );
if ( afterLoad ) afterLoads.add( afterLoad );
};

View File

@@ -15,7 +15,7 @@ const directiveParser = new RegExp(
// (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][a-z0-9-]+))?$',
'(?:--([a-z0-9_-]+))?$',
'i' // Case insensitive.
);