From 1c582ed1b5b2af8a454962ba9fe5c6a63cbe71d2 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 12:21:29 +0200 Subject: [PATCH 01/23] Update Interactivity API JS files --- assets/js/interactivity/components.js | 26 ----- assets/js/interactivity/constants.js | 3 +- assets/js/interactivity/directives.js | 105 +++++++++++++++++--- assets/js/interactivity/hooks.js | 138 ++++++++++++++++++-------- assets/js/interactivity/index.js | 8 +- assets/js/interactivity/router.js | 112 +++++++++++++++------ assets/js/interactivity/store.js | 1 + assets/js/interactivity/utils.js | 45 ++++++++- assets/js/interactivity/vdom.js | 138 ++++++++++++++++---------- 9 files changed, 407 insertions(+), 169 deletions(-) delete mode 100644 assets/js/interactivity/components.js diff --git a/assets/js/interactivity/components.js b/assets/js/interactivity/components.js deleted file mode 100644 index 7b234c818d9..00000000000 --- a/assets/js/interactivity/components.js +++ /dev/null @@ -1,26 +0,0 @@ -import { useMemo, useContext } from 'preact/hooks'; -import { deepSignal } from 'deepsignal'; -import { component } from './hooks'; - -export default () => { - // - const Context = ( { children, data, context: { Provider } } ) => { - const signals = useMemo( - () => deepSignal( JSON.parse( data ) ), - [ data ] - ); - return { children }; - }; - component( 'context', Context ); - - // - const Show = ( { children, when, evaluate, context } ) => { - const contextValue = useContext( context ); - if ( evaluate( when, { context: contextValue } ) ) { - return children; - } else { - return ; - } - }; - component( 'show', Show ); -}; diff --git a/assets/js/interactivity/constants.js b/assets/js/interactivity/constants.js index e2de6acee8b..9b198b11f6d 100644 --- a/assets/js/interactivity/constants.js +++ b/assets/js/interactivity/constants.js @@ -1,3 +1,2 @@ export const csnMetaTagItemprop = 'woo-client-side-navigation'; -export const componentPrefix = 'woo-'; -export const directivePrefix = 'data-woo-'; +export const directivePrefix = 'woo'; diff --git a/assets/js/interactivity/directives.js b/assets/js/interactivity/directives.js index 4f6775db73e..3ae1e4db6a6 100644 --- a/assets/js/interactivity/directives.js +++ b/assets/js/interactivity/directives.js @@ -1,14 +1,9 @@ import { useContext, useMemo, useEffect } from 'preact/hooks'; -import { useSignalEffect } from '@preact/signals'; import { deepSignal, peek } from 'deepsignal'; +import { useSignalEffect } from './utils'; import { directive } from './hooks'; import { prefetch, navigate, canDoClientSideNavigation } 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 can do client-side navigation. const clientSideNavigation = canDoClientSideNavigation( document.head ); @@ -32,7 +27,7 @@ const mergeDeepSignals = ( target, source ) => { }; export default () => { - // wp-context + // data-woo-context directive( 'context', ( { @@ -51,20 +46,21 @@ export default () => { }, [ context, inheritedValue ] ); return { children }; - } + }, + { priority: 5 } ); - // wp-effect:[name] + // data-woo-effect--[name] directive( 'effect', ( { directives: { effect }, context, evaluate } ) => { const contextValue = useContext( context ); Object.values( effect ).forEach( ( path ) => { useSignalEffect( () => { - evaluate( path, { context: contextValue } ); + return evaluate( path, { context: contextValue } ); } ); } ); } ); - // wp-on:[event] + // data-woo-on--[event] directive( 'on', ( { directives: { on }, element, evaluate, context } ) => { const contextValue = useContext( context ); Object.entries( on ).forEach( ( [ name, path ] ) => { @@ -74,7 +70,7 @@ export default () => { } ); } ); - // wp-class:[classname] + // data-woo-class--[classname] directive( 'class', ( { @@ -119,7 +115,7 @@ export default () => { } ); - // wp-bind:[attribute] + // data-woo-bind--[attribute] directive( 'bind', ( { directives: { bind }, element, context, evaluate } ) => { @@ -127,14 +123,35 @@ export default () => { Object.entries( bind ) .filter( ( n ) => n !== 'default' ) .forEach( ( [ attribute, path ] ) => { - element.props[ attribute ] = evaluate( 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 + ); + } + }, [] ); } ); } ); - // wp-link + // data-woo-link directive( 'link', ( { @@ -173,4 +190,62 @@ export default () => { } } ); + + // data-woo-show + directive( + 'show', + ( { + directives: { + show: { default: show }, + }, + element, + evaluate, + context, + } ) => { + const contextValue = useContext( context ); + + if ( ! evaluate( show, { context: contextValue } ) ) + element.props.children = ( + + ); + } + ); + + // data-woo-ignore + directive( + 'ignore', + ( { + element: { + type: Type, + props: { innerHTML, ...rest }, + }, + } ) => { + // Preserve the initial inner HTML. + const cached = useMemo( () => innerHTML, [] ); + return ( + + ); + } + ); + + // data-woo-text + directive( + 'text', + ( { + directives: { + text: { default: text }, + }, + element, + evaluate, + context, + } ) => { + const contextValue = useContext( context ); + element.props.children = evaluate( text, { + context: contextValue, + } ); + } + ); }; diff --git a/assets/js/interactivity/hooks.js b/assets/js/interactivity/hooks.js index 51fd9059dc3..a055c111bf7 100644 --- a/assets/js/interactivity/hooks.js +++ b/assets/js/interactivity/hooks.js @@ -1,26 +1,21 @@ -import { h, options, createContext } from 'preact'; -import { useRef } from 'preact/hooks'; +import { h, options, createContext, cloneElement } from 'preact'; +import { useRef, useMemo } from 'preact/hooks'; import { rawStore as store } from './store'; -import { componentPrefix } from './constants'; // Main context. const context = createContext( {} ); // WordPress Directives. const directiveMap = {}; -export const directive = ( name, cb ) => { +const directivePriorities = {}; +export const directive = ( name, cb, { priority = 10 } = {} ) => { directiveMap[ name ] = cb; -}; - -// WordPress Components. -const componentMap = {}; -export const component = ( name, Comp ) => { - componentMap[ name ] = Comp; + directivePriorities[ name ] = priority; }; // Resolve the path to some property of the store object. -const resolve = ( path, context ) => { - let current = { ...store, context }; +const resolve = ( path, ctx ) => { + let current = { ...store, context: ctx }; path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) ); return current; }; @@ -29,22 +24,92 @@ const resolve = ( path, context ) => { 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 ); - return typeof value === 'function' - ? value( { - state: store.state, - ...( ref !== undefined ? { ref } : {} ), - ...extraArgs, - } ) - : value; + 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, _wrapped: true } ); - const props = { ...originalProps, children: element }; - const evaluate = getEvaluate( { ref: ref.current } ); + const element = h( type, { ...originalProps, ref } ); + const evaluate = useMemo( () => getEvaluate( { ref } ), [] ); + + // Add wrappers recursively for each priority level. + const byPriorityLevel = usePriorityLevels( directives ); + return ( + + ); +}; + +// 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 ? ( + + ) : ( + element + ); + + const props = { ...originalProps, children }; const directiveArgs = { directives, props, element, context, evaluate }; for ( const d in directives ) { @@ -58,27 +123,16 @@ const Directive = ( { type, directives, props: originalProps } ) => { // Preact Options Hook called each time a vnode is created. const old = options.vnode; options.vnode = ( vnode ) => { - const type = vnode.type; - const { directives } = vnode.props; - - if ( - typeof type === 'string' && - type.slice( 0, componentPrefix.length ) === componentPrefix - ) { - vnode.props.children = h( - componentMap[ type.slice( componentPrefix.length ) ], - { ...vnode.props, context, evaluate: getEvaluate() }, - vnode.props.children - ); - } else if ( directives ) { + if ( vnode.props.__directives ) { const props = vnode.props; - delete props.directives; - if ( ! props._wrapped ) { - vnode.props = { type: vnode.type, directives, props }; - vnode.type = Directive; - } else { - delete props._wrapped; - } + const directives = props.__directives; + delete props.__directives; + vnode.props = { + type: vnode.type, + directives, + props, + }; + vnode.type = Directive; } if ( old ) old( vnode ); diff --git a/assets/js/interactivity/index.js b/assets/js/interactivity/index.js index cc1357d15e7..67d56e8e9cb 100644 --- a/assets/js/interactivity/index.js +++ b/assets/js/interactivity/index.js @@ -1,14 +1,14 @@ import registerDirectives from './directives'; -import registerComponents from './components'; import { init } from './router'; export { store } from './store'; +export { navigate } from './router'; /** - * Initialize the initial vDOM. + * Initialize the Interactivity API. */ document.addEventListener( 'DOMContentLoaded', async () => { registerDirectives(); - registerComponents(); await init(); - console.log( 'hydrated!' ); + // eslint-disable-next-line no-console + console.log( 'Interactivity API started' ); } ); diff --git a/assets/js/interactivity/router.js b/assets/js/interactivity/router.js index 8b441a95363..780371dd764 100644 --- a/assets/js/interactivity/router.js +++ b/assets/js/interactivity/router.js @@ -6,9 +6,10 @@ import { csnMetaTagItemprop, directivePrefix } from './constants'; // The root to render the vdom (document.body). let rootFragment; -// The cache of visited and prefetched pages and stylesheets. +// The cache of visited and prefetched pages, stylesheets and scripts. const pages = new Map(); const stylesheets = new Map(); +const scripts = new Map(); // Helper to remove domain and hash from the URL. We are only interesting in // caching the path and the query. @@ -23,30 +24,77 @@ export const canDoClientSideNavigation = ( dom ) => .querySelector( `meta[itemprop='${ csnMetaTagItemprop }']` ) ?.getAttribute( 'content' ) === 'active'; -// Fetch styles of a new page. -const fetchHead = async ( head ) => { - const sheets = await Promise.all( - [].map.call( - head.querySelectorAll( "link[rel='stylesheet']" ), - ( link ) => { - const href = link.getAttribute( 'href' ); - if ( ! stylesheets.has( href ) ) - stylesheets.set( - href, - fetch( href ).then( ( r ) => r.text() ) - ); - return stylesheets.get( href ); - } - ) +/** + * 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 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 ); + } ) ); - const stylesFromSheets = sheets.map( ( sheet ) => { - const style = document.createElement( 'style' ); - style.textContent = sheet; - return style; + + 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 [ - head.querySelector( 'title' ), - ...head.querySelectorAll( 'style' ), + ...scriptTags, + document.querySelector( 'title' ), + ...document.querySelectorAll( 'style' ), ...stylesFromSheets, ]; }; @@ -56,7 +104,7 @@ 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 fetchHead( dom.head ); + const head = await fetchAssets( dom ); return { head, body: toVdom( dom.body ) }; }; @@ -70,14 +118,18 @@ export const prefetch = ( url ) => { }; // Navigate to a new page. -export const navigate = async ( href ) => { +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 ); - window.history.pushState( {}, '', href ); + window.history[ replace ? 'replaceState' : 'pushState' ]( + {}, + '', + href + ); } else { window.location.assign( href ); } @@ -104,18 +156,22 @@ export const init = async () => { document.documentElement, document.body ); - const body = toVdom( document.body ); hydrate( body, rootFragment ); - const head = await fetchHead( document.head ); + // 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 ); + } ); + + const head = await fetchAssets( document ); pages.set( cleanUrl( window.location ), Promise.resolve( { body, head } ) ); } else { document - .querySelectorAll( `[${ directivePrefix }island]` ) + .querySelectorAll( `[data-${ directivePrefix }-interactive]` ) .forEach( ( node ) => { if ( ! hydratedIslands.has( node ) ) { const fragment = createRootFragment( diff --git a/assets/js/interactivity/store.js b/assets/js/interactivity/store.js index 14df2a369d6..f7f544914a6 100644 --- a/assets/js/interactivity/store.js +++ b/assets/js/interactivity/store.js @@ -26,6 +26,7 @@ const getSerializedState = () => { 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 {}; diff --git a/assets/js/interactivity/utils.js b/assets/js/interactivity/utils.js index 04ebe313630..e064397a334 100644 --- a/assets/js/interactivity/utils.js +++ b/assets/js/interactivity/utils.js @@ -1,4 +1,47 @@ -// For wrapperless hydration of document.body. +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 ); diff --git a/assets/js/interactivity/vdom.js b/assets/js/interactivity/vdom.js index 48c335ca342..3d15d24fc07 100644 --- a/assets/js/interactivity/vdom.js +++ b/assets/js/interactivity/vdom.js @@ -1,69 +1,105 @@ import { h } from 'preact'; import { directivePrefix as p } from './constants'; -const ignoreAttr = `${ p }ignore`; -const islandAttr = `${ p }island`; -const directiveParser = new RegExp( `${ p }([^:]+):?(.*)$` ); +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][a-z0-9-]+))?$', + 'i' // Case insensitive. +); export const hydratedIslands = new WeakSet(); -// Recursive function that transfoms a DOM tree into vDOM. -export function toVdom( node ) { - const props = {}; - const { attributes, childNodes } = node; - const directives = {}; - let hasDirectives = false; - let ignore = false; - let island = false; +// 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 + ); - if ( node.nodeType === 3 ) return node.data; - if ( node.nodeType === 4 ) { - node.replaceWith( new Text( node.nodeValue ) ); - return node.nodeValue; - } + 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 ]; + } - for ( let i = 0; i < attributes.length; i++ ) { - const n = attributes[ i ].name; - if ( n[ p.length ] && n.slice( 0, p.length ) === p ) { - 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; + 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; } - } else if ( n === 'ref' ) { - continue; - } else { props[ n ] = attributes[ i ].value; } - } - if ( ignore && ! island ) - return h( node.localName, { - dangerouslySetInnerHTML: { __html: node.innerHTML }, - } ); - if ( island ) hydratedIslands.add( node ); + if ( ignore && ! island ) + return [ + h( node.localName, { + ...props, + innerHTML: node.innerHTML, + __directives: { ignore: true }, + } ), + ]; + if ( island ) hydratedIslands.add( node ); - if ( hasDirectives ) props.directives = directives; + if ( hasDirectives ) props.__directives = directives; - const children = []; - for ( let i = 0; i < childNodes.length; i++ ) { - const child = childNodes[ i ]; - if ( child.nodeType === 8 || child.nodeType === 7 ) { - child.remove(); - i--; - } else { - children.push( toVdom( child ) ); + 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 h( node.localName, props, children ); + return walk( treeWalker.currentNode ); } From cd0af51eb13a881fa271e1bca52a46a422af1c14 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 12:21:47 +0200 Subject: [PATCH 02/23] Disable TS checks in the Interactivity API for now --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 2a1a5b6c03b..e961b8bd641 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "./storybook/build", "./storybook/webpack.config.js", "./storybook/preview.js", - "./bin" + "./bin", + "./assets/js/interactivity/*" ] } From 5528a2f3453680a3876178327c2f83d6b5b9a359 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 12:31:45 +0200 Subject: [PATCH 03/23] Add new SSR files --- src/Interactivity/blocks.php | 25 +++ ...ext.php => class-wp-directive-context.php} | 20 +- .../class-wp-directive-processor.php | 193 ++++++++++++++++++ .../class-wp-interactivity-store.php | 73 +++++++ src/Interactivity/directive-processing.php | 170 +++++++++++++++ .../directives/attributes/woo-bind.php | 22 -- .../directives/attributes/woo-class.php | 26 --- .../directives/attributes/woo-context.php | 18 -- .../directives/attributes/woo-style.php | 29 --- .../directives/class-woo-directive-store.php | 31 --- .../directives/tags/woo-context.php | 18 -- src/Interactivity/directives/utils.php | 49 ----- .../directives/woo-process-directives.php | 85 -------- src/Interactivity/directives/wp-bind.php | 32 +++ src/Interactivity/directives/wp-class.php | 36 ++++ src/Interactivity/directives/wp-context.php | 33 +++ src/Interactivity/directives/wp-style.php | 72 +++++++ src/Interactivity/directives/wp-text.php | 27 +++ src/Interactivity/load.php | 12 ++ src/Interactivity/scripts.php | 28 +++ src/Interactivity/store.php | 26 +++ src/Interactivity/woo-directives.php | 121 ----------- 22 files changed, 739 insertions(+), 407 deletions(-) create mode 100644 src/Interactivity/blocks.php rename src/Interactivity/{directives/class-woo-directive-context.php => class-wp-directive-context.php} (77%) create mode 100644 src/Interactivity/class-wp-directive-processor.php create mode 100644 src/Interactivity/class-wp-interactivity-store.php create mode 100644 src/Interactivity/directive-processing.php delete mode 100644 src/Interactivity/directives/attributes/woo-bind.php delete mode 100644 src/Interactivity/directives/attributes/woo-class.php delete mode 100644 src/Interactivity/directives/attributes/woo-context.php delete mode 100644 src/Interactivity/directives/attributes/woo-style.php delete mode 100644 src/Interactivity/directives/class-woo-directive-store.php delete mode 100644 src/Interactivity/directives/tags/woo-context.php delete mode 100644 src/Interactivity/directives/utils.php delete mode 100644 src/Interactivity/directives/woo-process-directives.php create mode 100644 src/Interactivity/directives/wp-bind.php create mode 100644 src/Interactivity/directives/wp-class.php create mode 100644 src/Interactivity/directives/wp-context.php create mode 100644 src/Interactivity/directives/wp-style.php create mode 100644 src/Interactivity/directives/wp-text.php create mode 100644 src/Interactivity/load.php create mode 100644 src/Interactivity/scripts.php create mode 100644 src/Interactivity/store.php delete mode 100644 src/Interactivity/woo-directives.php diff --git a/src/Interactivity/blocks.php b/src/Interactivity/blocks.php new file mode 100644 index 00000000000..76955357e6c --- /dev/null +++ b/src/Interactivity/blocks.php @@ -0,0 +1,25 @@ + + *
* - * + *
* - * + *
* - *
+ *
*/ -class Woo_Directive_Context { +class WP_Directive_Context { /** * The stack used to store contexts internally. * @@ -56,10 +57,13 @@ public function get_context() { * Set the current context. * * @param array $context The context to be set. + * * @return void */ public function set_context( $context ) { - array_push( $this->stack, array_replace_recursive( $this->get_context(), $context ) ); + if ( $context ) { + array_push( $this->stack, array_replace_recursive( $this->get_context(), $context ) ); + } } /** diff --git a/src/Interactivity/class-wp-directive-processor.php b/src/Interactivity/class-wp-directive-processor.php new file mode 100644 index 00000000000..608466a9c6e --- /dev/null +++ b/src/Interactivity/class-wp-directive-processor.php @@ -0,0 +1,193 @@ +get_tag(); + + if ( self::is_html_void_element( $tag_name ) ) { + return false; + } + + while ( $this->next_tag( + array( + 'tag_name' => $tag_name, + 'tag_closers' => 'visit', + ) + ) ) { + if ( ! $this->is_tag_closer() ) { + $depth++; + continue; + } + + if ( 0 === $depth ) { + return true; + } + + $depth--; + } + + return false; + } + + /** + * Return the content between two balanced tags. + * + * When called on an opening tag, return the HTML content found between that + * opening tag and its matching closing tag. + * + * @return string The content between the current opening and its matching + * closing tag. + */ + public function get_inner_html() { + $bookmarks = $this->get_balanced_tag_bookmarks(); + if ( ! $bookmarks ) { + return false; + } + list( $start_name, $end_name ) = $bookmarks; + + $start = $this->bookmarks[ $start_name ]->end + 1; + $end = $this->bookmarks[ $end_name ]->start; + + $this->seek( $start_name ); // Return to original position. + $this->release_bookmark( $start_name ); + $this->release_bookmark( $end_name ); + + return substr( $this->html, $start, $end - $start ); + } + + /** + * Set the content between two balanced tags. + * + * When called on an opening tag, set the HTML content found between that + * opening tag and its matching closing tag. + * + * @param string $new_html The string to replace the content between the + * matching tags with. + * + * @return bool Whether the content was successfully replaced. + */ + public function set_inner_html( $new_html ) { + $this->get_updated_html(); // Apply potential previous updates. + + $bookmarks = $this->get_balanced_tag_bookmarks(); + if ( ! $bookmarks ) { + return false; + } + list( $start_name, $end_name ) = $bookmarks; + + $start = $this->bookmarks[ $start_name ]->end + 1; + $end = $this->bookmarks[ $end_name ]->start; + + $this->seek( $start_name ); // Return to original position. + $this->release_bookmark( $start_name ); + $this->release_bookmark( $end_name ); + + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $new_html ); + return true; + } + + /** + * Return a pair of bookmarks for the current opening tag and the matching + * closing tag. + * + * @return array|false A pair of bookmarks, or false if there's no matching + * closing tag. + */ + public function get_balanced_tag_bookmarks() { + $i = 0; + while ( array_key_exists( 'start' . $i, $this->bookmarks ) ) { + ++$i; + } + $start_name = 'start' . $i; + + $this->set_bookmark( $start_name ); + if ( ! $this->next_balanced_closer() ) { + $this->release_bookmark( $start_name ); + return false; + } + + $i = 0; + while ( array_key_exists( 'end' . $i, $this->bookmarks ) ) { + ++$i; + } + $end_name = 'end' . $i; + $this->set_bookmark( $end_name ); + + return array( $start_name, $end_name ); + } + + /** + * Whether a given HTML element is void (e.g.
). + * + * @param string $tag_name The element in question. + * @return bool True if the element is void. + * + * @see https://html.spec.whatwg.org/#elements-2 + */ + public static function is_html_void_element( $tag_name ) { + switch ( $tag_name ) { + case 'AREA': + case 'BASE': + case 'BR': + case 'COL': + case 'EMBED': + case 'HR': + case 'IMG': + case 'INPUT': + case 'LINK': + case 'META': + case 'SOURCE': + case 'TRACK': + case 'WBR': + return true; + + default: + return false; + } + } + + /** + * Extract and return the directive type and the the part after the double + * hyphen from an attribute name (if present), in an array format. + * + * Examples: + * + * 'wp-island' => array( 'wp-island', null ) + * 'wp-bind--src' => array( 'wp-bind', 'src' ) + * 'wp-thing--and--thang' => array( 'wp-thing', 'and--thang' ) + * + * @param string $name The attribute name. + * @return array The resulting array + */ + public static function parse_attribute_name( $name ) { + return explode( '--', $name, 2 ); + } +} diff --git a/src/Interactivity/class-wp-interactivity-store.php b/src/Interactivity/class-wp-interactivity-store.php new file mode 100644 index 00000000000..46ca574e667 --- /dev/null +++ b/src/Interactivity/class-wp-interactivity-store.php @@ -0,0 +1,73 @@ +$store"; + } +} diff --git a/src/Interactivity/directive-processing.php b/src/Interactivity/directive-processing.php new file mode 100644 index 00000000000..88161223063 --- /dev/null +++ b/src/Interactivity/directive-processing.php @@ -0,0 +1,170 @@ + 'gutenberg_interactivity_process_wp_bind', + 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', + 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', + 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', + 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', + ); + + $tags = new WP_Directive_Processor( $block_content ); + $tags = gutenberg_interactivity_process_directives( $tags, 'data-wp-', $directives ); + return $tags->get_updated_html(); +} +add_filter( 'render_block', 'gutenberg_interactivity_process_directives_in_root_blocks', 10, 2 ); + +/** + * Mark the inner blocks with a temporary property so we can discard them later, + * and process only the root blocks. + * + * @param array $parsed_block The parsed block. + * @param array $source_block The source block. + * @param array $parent_block The parent block. + * + * @return array The parsed block. + */ +function gutenberg_interactivity_mark_inner_blocks( $parsed_block, $source_block, $parent_block ) { + if ( isset( $parent_block ) ) { + $parsed_block['is_inner_block'] = true; + } + return $parsed_block; +} +add_filter( 'render_block_data', 'gutenberg_interactivity_mark_inner_blocks', 10, 3 ); + +/** + * Process directives. + * + * @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor. + * @param string $prefix Attribute prefix. + * @param string[] $directives Directives. + * + * @return WP_Directive_Processor The modified instance of the + * WP_Directive_Processor. + */ +function gutenberg_interactivity_process_directives( $tags, $prefix, $directives ) { + $context = new WP_Directive_Context; + $tag_stack = array(); + + while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { + $tag_name = $tags->get_tag(); + + // Is this a tag that closes the latest opening tag? + if ( $tags->is_tag_closer() ) { + if ( 0 === count( $tag_stack ) ) { + continue; + } + + list( $latest_opening_tag_name, $attributes ) = end( $tag_stack ); + if ( $latest_opening_tag_name === $tag_name ) { + array_pop( $tag_stack ); + + // If the matching opening tag didn't have any directives, we move on. + if ( 0 === count( $attributes ) ) { + continue; + } + } + } else { + $attributes = array(); + foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) { + /* + * Removes the part after the double hyphen before looking for + * the directive processor inside `$directives`, e.g., "wp-bind" + * from "wp-bind--src" and "wp-context" from "wp-context" etc... + */ + list( $type ) = WP_Directive_Processor::parse_attribute_name( $name ); + if ( array_key_exists( $type, $directives ) ) { + $attributes[] = $type; + } + } + + /* + * If this is an open tag, and if it either has directives, or if + * we're inside a tag that does, take note of this tag and its + * directives so we can call its directive processor once we + * encounter the matching closing tag. + */ + if ( + ! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) && + ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) + ) { + $tag_stack[] = array( $tag_name, $attributes ); + } + } + + foreach ( $attributes as $attribute ) { + call_user_func( $directives[ $attribute ], $tags, $context ); + } + } + + return $tags; +} + +/** + * Resolve the reference using the store and the context from the provided path. + * + * @param string $path Path. + * @param array $context Context data. + * @return mixed + */ +function gutenberg_interactivity_evaluate_reference( $path, array $context = array() ) { + $store = array_merge( + WP_Interactivity_Store::get_data(), + array( 'context' => $context ) + ); + + /* + * Check first if the directive path is preceded by a negator operator (!), + * indicating that the value obtained from the Interactivity Store (or the + * passed context) using the subsequent path should be negated. + */ + $should_negate_value = '!' === $path[0]; + + $path = $should_negate_value ? substr( $path, 1 ) : $path; + $path_segments = explode( '.', $path ); + $current = $store; + foreach ( $path_segments as $p ) { + if ( isset( $current[ $p ] ) ) { + $current = $current[ $p ]; + } else { + return null; + } + } + + /* + * Check if $current is an anonymous function or an arrow function, and if + * so, call it passing the store. Other types of callables are ignored in + * purpose, as arbitrary strings or arrays could be wrongly evaluated as + * "callables". + * + * E.g., "file" is an string and a "callable" (the "file" function exists). + */ + if ( $current instanceof Closure ) { + $current = call_user_func( $current, $store ); + } + + // Return the opposite if it has a negator operator (!). + return $should_negate_value ? ! $current : $current; +} diff --git a/src/Interactivity/directives/attributes/woo-bind.php b/src/Interactivity/directives/attributes/woo-bind.php deleted file mode 100644 index 1990be52987..00000000000 --- a/src/Interactivity/directives/attributes/woo-bind.php +++ /dev/null @@ -1,22 +0,0 @@ -is_tag_closer() ) { - return; - } - - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-bind:' ); - - foreach ( $prefixed_attributes as $attr ) { - list( , $bound_attr ) = explode( ':', $attr ); - if ( empty( $bound_attr ) ) { - continue; - } - - $expr = $tags->get_attribute( $attr ); - $value = woo_directives_evaluate( $expr, $context->get_context() ); - $tags->set_attribute( $bound_attr, $value ); - } -} diff --git a/src/Interactivity/directives/attributes/woo-class.php b/src/Interactivity/directives/attributes/woo-class.php deleted file mode 100644 index be0858bb60d..00000000000 --- a/src/Interactivity/directives/attributes/woo-class.php +++ /dev/null @@ -1,26 +0,0 @@ -is_tag_closer() ) { - return; - } - - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-class:' ); - - foreach ( $prefixed_attributes as $attr ) { - list( , $class_name ) = explode( ':', $attr ); - if ( empty( $class_name ) ) { - continue; - } - - $expr = $tags->get_attribute( $attr ); - $add_class = woo_directives_evaluate( $expr, $context->get_context() ); - if ( $add_class ) { - $tags->add_class( $class_name ); - } else { - $tags->remove_class( $class_name ); - } - } -} diff --git a/src/Interactivity/directives/attributes/woo-context.php b/src/Interactivity/directives/attributes/woo-context.php deleted file mode 100644 index 823ab9683b0..00000000000 --- a/src/Interactivity/directives/attributes/woo-context.php +++ /dev/null @@ -1,18 +0,0 @@ -is_tag_closer() ) { - $context->rewind_context(); - return; - } - - $value = $tags->get_attribute( 'data-woo-context' ); - if ( null === $value ) { - // No woo-context directive. - return; - } - - $new_context = json_decode( $value, true ); - - $context->set_context( $new_context ); -} diff --git a/src/Interactivity/directives/attributes/woo-style.php b/src/Interactivity/directives/attributes/woo-style.php deleted file mode 100644 index 4cd38ced4dc..00000000000 --- a/src/Interactivity/directives/attributes/woo-style.php +++ /dev/null @@ -1,29 +0,0 @@ -is_tag_closer() ) { - return; - } - - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-style:' ); - - foreach ( $prefixed_attributes as $attr ) { - list( , $style_name ) = explode( ':', $attr ); - if ( empty( $style_name ) ) { - continue; - } - - $expr = $tags->get_attribute( $attr ); - $style_value = woo_directives_evaluate( $expr, $context->get_context() ); - if ( $style_value ) { - $style_attr = $tags->get_attribute( 'style' ); - $style_attr = woo_directives_set_style( $style_attr, $style_name, $style_value ); - $tags->set_attribute( 'style', $style_attr ); - } else { - // $tags->remove_class( $style_name ); - } - } -} - diff --git a/src/Interactivity/directives/class-woo-directive-store.php b/src/Interactivity/directives/class-woo-directive-store.php deleted file mode 100644 index 3b213d281d3..00000000000 --- a/src/Interactivity/directives/class-woo-directive-store.php +++ /dev/null @@ -1,31 +0,0 @@ -$store"; - } -} diff --git a/src/Interactivity/directives/tags/woo-context.php b/src/Interactivity/directives/tags/woo-context.php deleted file mode 100644 index 13a4a74cc3f..00000000000 --- a/src/Interactivity/directives/tags/woo-context.php +++ /dev/null @@ -1,18 +0,0 @@ -is_tag_closer() ) { - $context->rewind_context(); - return; - } - - $value = $tags->get_attribute( 'data' ); - if ( null === $value ) { - // No woo-context directive. - return; - } - - $new_context = json_decode( $value, true ); - - $context->set_context( $new_context ); -} diff --git a/src/Interactivity/directives/utils.php b/src/Interactivity/directives/utils.php deleted file mode 100644 index 5d072e69da9..00000000000 --- a/src/Interactivity/directives/utils.php +++ /dev/null @@ -1,49 +0,0 @@ - $context ) - ); - - $array = explode( '.', $path ); - foreach ( $array as $p ) { - if ( isset( $current[ $p ] ) ) { - $current = $current[ $p ]; - } else { - return null; - } - } - return $current; -} - -function woo_directives_set_style( $style, $name, $value ) { - $style_assignments = explode( ';', $style ); - $modified = false; - foreach ( $style_assignments as $style_assignment ) { - list( $style_name ) = explode( ':', $style_assignment ); - if ( trim( $style_name ) === $name ) { - $style_assignment = $style_name . ': ' . $value; - $modified = true; - break; - } - } - - if ( ! $modified ) { - $new_style_assignment = $name . ': ' . $value; - // If the last element is empty or whitespace-only, we insert - // the new "key: value" pair before it. - if ( empty( trim( end( $style_assignments ) ) ) ) { - array_splice( $style_assignments, - 1, 0, $new_style_assignment ); - } else { - array_push( $style_assignments, $new_style_assignment ); - } - } - return implode( ';', $style_assignments ); -} diff --git a/src/Interactivity/directives/woo-process-directives.php b/src/Interactivity/directives/woo-process-directives.php deleted file mode 100644 index dcfe317d235..00000000000 --- a/src/Interactivity/directives/woo-process-directives.php +++ /dev/null @@ -1,85 +0,0 @@ -next_tag( array( 'tag_closers' => 'visit' ) ) ) { - $tag_name = strtolower( $tags->get_tag() ); - if ( array_key_exists( $tag_name, $tag_directives ) ) { - call_user_func( $tag_directives[ $tag_name ], $tags, $context ); - } else { - // Components can't have directives (unless we change our mind about this). - - // Is this a tag that closes the latest opening tag? - if ( $tags->is_tag_closer() ) { - if ( 0 === count( $tag_stack ) ) { - continue; - } - - list( $latest_opening_tag_name, $attributes ) = end( $tag_stack ); - if ( $latest_opening_tag_name === $tag_name ) { - array_pop( $tag_stack ); - - // If the matching opening tag didn't have any attribute directives, - // we move on. - if ( 0 === count( $attributes ) ) { - continue; - } - } - } else { - // Helper that removes the part after the colon before looking - // for the directive processor inside `$attribute_directives`. - $get_directive_type = function ( $attr ) { - return strtok( $attr, ':' ); - }; - - $attributes = $tags->get_attribute_names_with_prefix( $prefix ); - $attributes = array_map( $get_directive_type, $attributes ); - $attributes = array_intersect( $attributes, array_keys( $attribute_directives ) ); - - // If this is an open tag, and if it either has attribute directives, - // or if we're inside a tag that does, take note of this tag and its attribute - // directives so we can call its directive processor once we encounter the - // matching closing tag. - if ( - ! woo_directives_is_html_void_element( $tags->get_tag() ) && - ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) - ) { - $tag_stack[] = array( $tag_name, $attributes ); - } - } - - foreach ( $attributes as $attribute ) { - call_user_func( $attribute_directives[ $attribute ], $tags, $context ); - } - } - } - - return $tags; -} - -// See e.g. https://github.com/WordPress/gutenberg/pull/47573. -function woo_directives_is_html_void_element( $tag_name ) { - switch ( $tag_name ) { - case 'AREA': - case 'BASE': - case 'BR': - case 'COL': - case 'EMBED': - case 'HR': - case 'IMG': - case 'INPUT': - case 'LINK': - case 'META': - case 'SOURCE': - case 'TRACK': - case 'WBR': - return true; - - default: - return false; - } -} diff --git a/src/Interactivity/directives/wp-bind.php b/src/Interactivity/directives/wp-bind.php new file mode 100644 index 00000000000..54be4a9faeb --- /dev/null +++ b/src/Interactivity/directives/wp-bind.php @@ -0,0 +1,32 @@ +is_tag_closer() ) { + return; + } + + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-bind--' ); + + foreach ( $prefixed_attributes as $attr ) { + list( , $bound_attr ) = WP_Directive_Processor::parse_attribute_name( $attr ); + if ( empty( $bound_attr ) ) { + continue; + } + + $expr = $tags->get_attribute( $attr ); + $value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $tags->set_attribute( $bound_attr, $value ); + } +} diff --git a/src/Interactivity/directives/wp-class.php b/src/Interactivity/directives/wp-class.php new file mode 100644 index 00000000000..741cc75b42c --- /dev/null +++ b/src/Interactivity/directives/wp-class.php @@ -0,0 +1,36 @@ +is_tag_closer() ) { + return; + } + + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-class--' ); + + foreach ( $prefixed_attributes as $attr ) { + list( , $class_name ) = WP_Directive_Processor::parse_attribute_name( $attr ); + if ( empty( $class_name ) ) { + continue; + } + + $expr = $tags->get_attribute( $attr ); + $add_class = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + if ( $add_class ) { + $tags->add_class( $class_name ); + } else { + $tags->remove_class( $class_name ); + } + } +} diff --git a/src/Interactivity/directives/wp-context.php b/src/Interactivity/directives/wp-context.php new file mode 100644 index 00000000000..5e3c5a140b2 --- /dev/null +++ b/src/Interactivity/directives/wp-context.php @@ -0,0 +1,33 @@ +is_tag_closer() ) { + $context->rewind_context(); + return; + } + + $value = $tags->get_attribute( 'data-wp-context' ); + if ( null === $value ) { + // No data-wp-context directive. + return; + } + + $new_context = json_decode( $value, true ); + if ( null === $new_context ) { + // Invalid JSON defined in the directive. + return; + } + + $context->set_context( $new_context ); +} diff --git a/src/Interactivity/directives/wp-style.php b/src/Interactivity/directives/wp-style.php new file mode 100644 index 00000000000..9c37f9082c2 --- /dev/null +++ b/src/Interactivity/directives/wp-style.php @@ -0,0 +1,72 @@ +is_tag_closer() ) { + return; + } + + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-style--' ); + + foreach ( $prefixed_attributes as $attr ) { + list( , $style_name ) = WP_Directive_Processor::parse_attribute_name( $attr ); + if ( empty( $style_name ) ) { + continue; + } + + $expr = $tags->get_attribute( $attr ); + $style_value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + if ( $style_value ) { + $style_attr = $tags->get_attribute( 'style' ); + $style_attr = gutenberg_interactivity_set_style( $style_attr, $style_name, $style_value ); + $tags->set_attribute( 'style', $style_attr ); + } else { + // TODO: Do we want to unset styles if they're null? + } + } +} + +/** + * Set style. + * + * @param string $style Existing style to amend. + * @param string $name Style property name. + * @param string $value Style property value. + * @return string Amended styles. + */ +function gutenberg_interactivity_set_style( $style, $name, $value ) { + $style_assignments = explode( ';', $style ); + $modified = false; + foreach ( $style_assignments as $style_assignment ) { + list( $style_name ) = explode( ':', $style_assignment ); + if ( trim( $style_name ) === $name ) { + // TODO: Retain surrounding whitespace from $style_value, if any. + $style_assignment = $style_name . ': ' . $value; + $modified = true; + break; + } + } + + if ( ! $modified ) { + $new_style_assignment = $name . ': ' . $value; + // If the last element is empty or whitespace-only, we insert + // the new "key: value" pair before it. + if ( empty( trim( end( $style_assignments ) ) ) ) { + array_splice( $style_assignments, - 1, 0, $new_style_assignment ); + } else { + array_push( $style_assignments, $new_style_assignment ); + } + } + return implode( ';', $style_assignments ); +} diff --git a/src/Interactivity/directives/wp-text.php b/src/Interactivity/directives/wp-text.php new file mode 100644 index 00000000000..b0cfc98a74e --- /dev/null +++ b/src/Interactivity/directives/wp-text.php @@ -0,0 +1,27 @@ +is_tag_closer() ) { + return; + } + + $value = $tags->get_attribute( 'data-wp-text' ); + if ( null === $value ) { + return; + } + + $text = gutenberg_interactivity_evaluate_reference( $value, $context->get_context() ); + $tags->set_inner_html( esc_html( $text ) ); +} diff --git a/src/Interactivity/load.php b/src/Interactivity/load.php new file mode 100644 index 00000000000..81bbd260bc0 --- /dev/null +++ b/src/Interactivity/load.php @@ -0,0 +1,12 @@ +get_all_registered(); + foreach ( array_values( $registered_blocks ) as $block ) { + if ( isset( $block->supports['interactivity'] ) && $block->supports['interactivity'] ) { + foreach ( $block->view_script_handles as $handle ) { + wp_script_add_data( $handle, 'group', 1 ); + } + } + } +} +add_action( 'wp_enqueue_scripts', 'gutenberg_interactivity_move_interactive_scripts_to_the_footer', 11 ); diff --git a/src/Interactivity/store.php b/src/Interactivity/store.php new file mode 100644 index 00000000000..88c4b2ebd10 --- /dev/null +++ b/src/Interactivity/store.php @@ -0,0 +1,26 @@ +'; - } -} -add_action( 'wp_head', 'woo_directives_add_client_side_navigation_meta_tag' ); - - -function woo_directives_mark_interactive_blocks( $block_content, $block, $instance ) { - if ( woo_directives_get_client_side_navigation() ) { - return $block_content; - } - - // Append the `data-woo-ignore` attribute for inner blocks of interactive blocks. - if ( isset( $instance->parsed_block['isolated'] ) ) { - $w = new WP_HTML_Tag_Processor( $block_content ); - $w->next_tag(); - $w->set_attribute( 'data-woo-ignore', true ); - $block_content = (string) $w; - } - - // Return if it's not interactive. - if ( ! block_has_support( $instance->block_type, array( 'interactivity' ) ) ) { - return $block_content; - } - - // Add the `data-woo-island` attribute if it's interactive. - $w = new WP_HTML_Tag_Processor( $block_content ); - $w->next_tag(); - $w->set_attribute( 'data-woo-island', true ); - - return (string) $w; -} -add_filter( 'render_block', 'woo_directives_mark_interactive_blocks', 10, 3 ); - -/** - * Add a flag to mark inner blocks of isolated interactive blocks. - */ -function woo_directives_inner_blocks( $parsed_block, $source_block, $parent_block ) { - if ( - isset( $parent_block ) && - block_has_support( - $parent_block->block_type, - array( - 'interactivity', - 'isolated', - ) - ) - ) { - $parsed_block['isolated'] = true; - } - return $parsed_block; -} -add_filter( 'render_block_data', 'woo_directives_inner_blocks', 10, 3 ); - -function woo_process_directives_in_block( $block_content ) { - $tag_directives = array( - 'woo-context' => 'process_woo_context_tag', - ); - - $attribute_directives = array( - 'data-woo-context' => 'process_woo_context_attribute', - 'data-woo-bind' => 'process_woo_bind', - 'data-woo-class' => 'process_woo_class', - 'data-woo-style' => 'process_woo_style', - ); - - $tags = new WP_HTML_Tag_Processor( $block_content ); - $tags = woo_process_directives( $tags, 'data-woo-', $tag_directives, $attribute_directives ); - return $tags->get_updated_html(); -} -add_filter( - 'render_block', - 'woo_process_directives_in_block', - 10, - 1 -); - -add_action( 'wp_footer', array( 'Woo_Directive_Store', 'render' ), 9 ); From e6d5a25db33cdcffc3fc032b765c722e063529ef Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 12:42:52 +0200 Subject: [PATCH 04/23] Replace wp_ prefixes with wc_ ones --- .../class-wp-directive-context.php | 2 +- .../class-wp-directive-processor.php | 4 +-- .../class-wp-interactivity-store.php | 6 ++--- src/Interactivity/directive-processing.php | 26 +++++++++---------- src/Interactivity/directives/wp-bind.php | 8 +++--- src/Interactivity/directives/wp-class.php | 8 +++--- src/Interactivity/directives/wp-context.php | 6 ++--- src/Interactivity/directives/wp-style.php | 8 +++--- src/Interactivity/directives/wp-text.php | 6 ++--- src/Interactivity/scripts.php | 2 +- src/Interactivity/store.php | 10 +++---- 11 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/Interactivity/class-wp-directive-context.php b/src/Interactivity/class-wp-directive-context.php index 7186922d137..02a8e51c45e 100644 --- a/src/Interactivity/class-wp-directive-context.php +++ b/src/Interactivity/class-wp-directive-context.php @@ -25,7 +25,7 @@ * * */ -class WP_Directive_Context { +class WC_Directive_Context { /** * The stack used to store contexts internally. * diff --git a/src/Interactivity/class-wp-directive-processor.php b/src/Interactivity/class-wp-directive-processor.php index 608466a9c6e..4fe1f8bc518 100644 --- a/src/Interactivity/class-wp-directive-processor.php +++ b/src/Interactivity/class-wp-directive-processor.php @@ -1,6 +1,6 @@ 'gutenberg_interactivity_process_wp_bind', - 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', - 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', - 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', - 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', + 'data-wp-bind' => 'gutenberg_interactivity_process_wc_bind', + 'data-wp-context' => 'gutenberg_interactivity_process_wc_context', + 'data-wp-class' => 'gutenberg_interactivity_process_wc_class', + 'data-wp-style' => 'gutenberg_interactivity_process_wc_style', + 'data-wp-text' => 'gutenberg_interactivity_process_wc_text', ); - $tags = new WP_Directive_Processor( $block_content ); + $tags = new WC_Directive_Processor( $block_content ); $tags = gutenberg_interactivity_process_directives( $tags, 'data-wp-', $directives ); return $tags->get_updated_html(); } @@ -57,15 +57,15 @@ function gutenberg_interactivity_mark_inner_blocks( $parsed_block, $source_block /** * Process directives. * - * @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor. + * @param WC_Directive_Processor $tags An instance of the WC_Directive_Processor. * @param string $prefix Attribute prefix. * @param string[] $directives Directives. * - * @return WP_Directive_Processor The modified instance of the - * WP_Directive_Processor. + * @return WC_Directive_Processor The modified instance of the + * WC_Directive_Processor. */ function gutenberg_interactivity_process_directives( $tags, $prefix, $directives ) { - $context = new WP_Directive_Context; + $context = new WC_Directive_Context; $tag_stack = array(); while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { @@ -94,7 +94,7 @@ function gutenberg_interactivity_process_directives( $tags, $prefix, $directives * the directive processor inside `$directives`, e.g., "wp-bind" * from "wp-bind--src" and "wp-context" from "wp-context" etc... */ - list( $type ) = WP_Directive_Processor::parse_attribute_name( $name ); + list( $type ) = WC_Directive_Processor::parse_attribute_name( $name ); if ( array_key_exists( $type, $directives ) ) { $attributes[] = $type; } @@ -107,7 +107,7 @@ function gutenberg_interactivity_process_directives( $tags, $prefix, $directives * encounter the matching closing tag. */ if ( - ! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) && + ! WC_Directive_Processor::is_html_void_element( $tags->get_tag() ) && ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) ) { $tag_stack[] = array( $tag_name, $attributes ); @@ -131,7 +131,7 @@ function gutenberg_interactivity_process_directives( $tags, $prefix, $directives */ function gutenberg_interactivity_evaluate_reference( $path, array $context = array() ) { $store = array_merge( - WP_Interactivity_Store::get_data(), + WC_Interactivity_Store::get_data(), array( 'context' => $context ) ); diff --git a/src/Interactivity/directives/wp-bind.php b/src/Interactivity/directives/wp-bind.php index 54be4a9faeb..875a5206c21 100644 --- a/src/Interactivity/directives/wp-bind.php +++ b/src/Interactivity/directives/wp-bind.php @@ -9,10 +9,10 @@ /** * Process wp-bind directive attribute. * - * @param WP_Directive_Processor $tags Tags. - * @param WP_Directive_Context $context Directive context. + * @param WC_Directive_Processor $tags Tags. + * @param WC_Directive_Context $context Directive context. */ -function gutenberg_interactivity_process_wp_bind( $tags, $context ) { +function gutenberg_interactivity_process_wc_bind( $tags, $context ) { if ( $tags->is_tag_closer() ) { return; } @@ -20,7 +20,7 @@ function gutenberg_interactivity_process_wp_bind( $tags, $context ) { $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-bind--' ); foreach ( $prefixed_attributes as $attr ) { - list( , $bound_attr ) = WP_Directive_Processor::parse_attribute_name( $attr ); + list( , $bound_attr ) = WC_Directive_Processor::parse_attribute_name( $attr ); if ( empty( $bound_attr ) ) { continue; } diff --git a/src/Interactivity/directives/wp-class.php b/src/Interactivity/directives/wp-class.php index 741cc75b42c..d07b372f398 100644 --- a/src/Interactivity/directives/wp-class.php +++ b/src/Interactivity/directives/wp-class.php @@ -9,10 +9,10 @@ /** * Process wp-class directive attribute. * - * @param WP_Directive_Processor $tags Tags. - * @param WP_Directive_Context $context Directive context. + * @param WC_Directive_Processor $tags Tags. + * @param WC_Directive_Context $context Directive context. */ -function gutenberg_interactivity_process_wp_class( $tags, $context ) { +function gutenberg_interactivity_process_wc_class( $tags, $context ) { if ( $tags->is_tag_closer() ) { return; } @@ -20,7 +20,7 @@ function gutenberg_interactivity_process_wp_class( $tags, $context ) { $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-class--' ); foreach ( $prefixed_attributes as $attr ) { - list( , $class_name ) = WP_Directive_Processor::parse_attribute_name( $attr ); + list( , $class_name ) = WC_Directive_Processor::parse_attribute_name( $attr ); if ( empty( $class_name ) ) { continue; } diff --git a/src/Interactivity/directives/wp-context.php b/src/Interactivity/directives/wp-context.php index 5e3c5a140b2..e09b7b9fbd6 100644 --- a/src/Interactivity/directives/wp-context.php +++ b/src/Interactivity/directives/wp-context.php @@ -8,10 +8,10 @@ /** * Process wp-context directive attribute. * - * @param WP_Directive_Processor $tags Tags. - * @param WP_Directive_Context $context Directive context. + * @param WC_Directive_Processor $tags Tags. + * @param WC_Directive_Context $context Directive context. */ -function gutenberg_interactivity_process_wp_context( $tags, $context ) { +function gutenberg_interactivity_process_wc_context( $tags, $context ) { if ( $tags->is_tag_closer() ) { $context->rewind_context(); return; diff --git a/src/Interactivity/directives/wp-style.php b/src/Interactivity/directives/wp-style.php index 9c37f9082c2..f844743f1c9 100644 --- a/src/Interactivity/directives/wp-style.php +++ b/src/Interactivity/directives/wp-style.php @@ -9,10 +9,10 @@ /** * Process wp-style directive attribute. * - * @param WP_Directive_Processor $tags Tags. - * @param WP_Directive_Context $context Directive context. + * @param WC_Directive_Processor $tags Tags. + * @param WC_Directive_Context $context Directive context. */ -function gutenberg_interactivity_process_wp_style( $tags, $context ) { +function gutenberg_interactivity_process_wc_style( $tags, $context ) { if ( $tags->is_tag_closer() ) { return; } @@ -20,7 +20,7 @@ function gutenberg_interactivity_process_wp_style( $tags, $context ) { $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wp-style--' ); foreach ( $prefixed_attributes as $attr ) { - list( , $style_name ) = WP_Directive_Processor::parse_attribute_name( $attr ); + list( , $style_name ) = WC_Directive_Processor::parse_attribute_name( $attr ); if ( empty( $style_name ) ) { continue; } diff --git a/src/Interactivity/directives/wp-text.php b/src/Interactivity/directives/wp-text.php index b0cfc98a74e..2a3106a5810 100644 --- a/src/Interactivity/directives/wp-text.php +++ b/src/Interactivity/directives/wp-text.php @@ -9,10 +9,10 @@ /** * Process wp-text directive attribute. * - * @param WP_Directive_Processor $tags Tags. - * @param WP_Directive_Context $context Directive context. + * @param WC_Directive_Processor $tags Tags. + * @param WC_Directive_Context $context Directive context. */ -function gutenberg_interactivity_process_wp_text( $tags, $context ) { +function gutenberg_interactivity_process_wc_text( $tags, $context ) { if ( $tags->is_tag_closer() ) { return; } diff --git a/src/Interactivity/scripts.php b/src/Interactivity/scripts.php index e95bf518c75..ce8d52d62c6 100644 --- a/src/Interactivity/scripts.php +++ b/src/Interactivity/scripts.php @@ -8,7 +8,7 @@ /** * Move interactive scripts to the footer. This is a temporary measure to make - * it work with `wp_store` and it should be replaced with deferred scripts or + * it work with `wc_store` and it should be replaced with deferred scripts or * modules. */ function gutenberg_interactivity_move_interactive_scripts_to_the_footer() { diff --git a/src/Interactivity/store.php b/src/Interactivity/store.php index 88c4b2ebd10..5d7668d65f9 100644 --- a/src/Interactivity/store.php +++ b/src/Interactivity/store.php @@ -1,6 +1,6 @@ Date: Mon, 19 Jun 2023 12:47:29 +0200 Subject: [PATCH 05/23] Replace wp- prefix with wc- --- .../class-wp-directive-context.php | 6 +++--- .../class-wp-directive-processor.php | 6 +++--- .../class-wp-interactivity-store.php | 2 +- src/Interactivity/directive-processing.php | 18 +++++++++--------- src/Interactivity/directives/wp-bind.php | 6 +++--- src/Interactivity/directives/wp-class.php | 6 +++--- src/Interactivity/directives/wp-context.php | 10 +++++----- src/Interactivity/directives/wp-style.php | 6 +++--- src/Interactivity/directives/wp-text.php | 6 +++--- src/Interactivity/load.php | 16 ++++++++-------- src/Interactivity/scripts.php | 2 +- 11 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/Interactivity/class-wp-directive-context.php b/src/Interactivity/class-wp-directive-context.php index 02a8e51c45e..d75295ec111 100644 --- a/src/Interactivity/class-wp-directive-context.php +++ b/src/Interactivity/class-wp-directive-context.php @@ -9,7 +9,7 @@ /** * This is a data structure to hold the current context. * - * Whenever encountering a `data-wp-context` directive, we need to update + * Whenever encountering a `data-wc-context` directive, we need to update * the context with the data found in that directive. Conversely, * when "leaving" that context (by encountering a closing tag), we * need to reset the context to its previous state. This means that @@ -17,9 +17,9 @@ * * Example: * - *
+ *
* - *
+ *
* *
* diff --git a/src/Interactivity/class-wp-directive-processor.php b/src/Interactivity/class-wp-directive-processor.php index 4fe1f8bc518..7ce3101a440 100644 --- a/src/Interactivity/class-wp-directive-processor.php +++ b/src/Interactivity/class-wp-directive-processor.php @@ -180,9 +180,9 @@ public static function is_html_void_element( $tag_name ) { * * Examples: * - * 'wp-island' => array( 'wp-island', null ) - * 'wp-bind--src' => array( 'wp-bind', 'src' ) - * 'wp-thing--and--thang' => array( 'wp-thing', 'and--thang' ) + * 'wc-island' => array( 'wc-island', null ) + * 'wc-bind--src' => array( 'wc-bind', 'src' ) + * 'wc-thing--and--thang' => array( 'wc-thing', 'and--thang' ) * * @param string $name The attribute name. * @return array The resulting array diff --git a/src/Interactivity/class-wp-interactivity-store.php b/src/Interactivity/class-wp-interactivity-store.php index 53ce335e0f8..69e6f464fb0 100644 --- a/src/Interactivity/class-wp-interactivity-store.php +++ b/src/Interactivity/class-wp-interactivity-store.php @@ -68,6 +68,6 @@ static function render() { return; } $store = self::serialize(); - echo ""; + echo ""; } } diff --git a/src/Interactivity/directive-processing.php b/src/Interactivity/directive-processing.php index 6597ac0a9e0..369a6a8ef7e 100644 --- a/src/Interactivity/directive-processing.php +++ b/src/Interactivity/directive-processing.php @@ -17,21 +17,21 @@ */ function gutenberg_interactivity_process_directives_in_root_blocks( $block_content, $block ) { // Don't process inner blocks or root blocks that don't contain directives. - if ( isset( $block['is_inner_block'] ) || strpos( $block_content, 'data-wp-' ) === false ) { + if ( isset( $block['is_inner_block'] ) || strpos( $block_content, 'data-wc-' ) === false ) { return $block_content; } // TODO: Add some directive/components registration mechanism. $directives = array( - 'data-wp-bind' => 'gutenberg_interactivity_process_wc_bind', - 'data-wp-context' => 'gutenberg_interactivity_process_wc_context', - 'data-wp-class' => 'gutenberg_interactivity_process_wc_class', - 'data-wp-style' => 'gutenberg_interactivity_process_wc_style', - 'data-wp-text' => 'gutenberg_interactivity_process_wc_text', + 'data-wc-bind' => 'gutenberg_interactivity_process_wc_bind', + 'data-wc-context' => 'gutenberg_interactivity_process_wc_context', + 'data-wc-class' => 'gutenberg_interactivity_process_wc_class', + 'data-wc-style' => 'gutenberg_interactivity_process_wc_style', + 'data-wc-text' => 'gutenberg_interactivity_process_wc_text', ); $tags = new WC_Directive_Processor( $block_content ); - $tags = gutenberg_interactivity_process_directives( $tags, 'data-wp-', $directives ); + $tags = gutenberg_interactivity_process_directives( $tags, 'data-wc-', $directives ); return $tags->get_updated_html(); } add_filter( 'render_block', 'gutenberg_interactivity_process_directives_in_root_blocks', 10, 2 ); @@ -91,8 +91,8 @@ function gutenberg_interactivity_process_directives( $tags, $prefix, $directives foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) { /* * Removes the part after the double hyphen before looking for - * the directive processor inside `$directives`, e.g., "wp-bind" - * from "wp-bind--src" and "wp-context" from "wp-context" etc... + * the directive processor inside `$directives`, e.g., "wc-bind" + * from "wc-bind--src" and "wc-context" from "wc-context" etc... */ list( $type ) = WC_Directive_Processor::parse_attribute_name( $name ); if ( array_key_exists( $type, $directives ) ) { diff --git a/src/Interactivity/directives/wp-bind.php b/src/Interactivity/directives/wp-bind.php index 875a5206c21..3fd5d9c0773 100644 --- a/src/Interactivity/directives/wp-bind.php +++ b/src/Interactivity/directives/wp-bind.php @@ -1,13 +1,13 @@ get_attribute_names_with_prefix( 'data-wp-bind--' ); + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wc-bind--' ); foreach ( $prefixed_attributes as $attr ) { list( , $bound_attr ) = WC_Directive_Processor::parse_attribute_name( $attr ); diff --git a/src/Interactivity/directives/wp-class.php b/src/Interactivity/directives/wp-class.php index d07b372f398..30315cba895 100644 --- a/src/Interactivity/directives/wp-class.php +++ b/src/Interactivity/directives/wp-class.php @@ -1,13 +1,13 @@ get_attribute_names_with_prefix( 'data-wp-class--' ); + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wc-class--' ); foreach ( $prefixed_attributes as $attr ) { list( , $class_name ) = WC_Directive_Processor::parse_attribute_name( $attr ); diff --git a/src/Interactivity/directives/wp-context.php b/src/Interactivity/directives/wp-context.php index e09b7b9fbd6..d5db3f60fc2 100644 --- a/src/Interactivity/directives/wp-context.php +++ b/src/Interactivity/directives/wp-context.php @@ -1,12 +1,12 @@ get_attribute( 'data-wp-context' ); + $value = $tags->get_attribute( 'data-wc-context' ); if ( null === $value ) { - // No data-wp-context directive. + // No data-wc-context directive. return; } diff --git a/src/Interactivity/directives/wp-style.php b/src/Interactivity/directives/wp-style.php index f844743f1c9..cb87906388f 100644 --- a/src/Interactivity/directives/wp-style.php +++ b/src/Interactivity/directives/wp-style.php @@ -1,13 +1,13 @@ get_attribute_names_with_prefix( 'data-wp-style--' ); + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wc-style--' ); foreach ( $prefixed_attributes as $attr ) { list( , $style_name ) = WC_Directive_Processor::parse_attribute_name( $attr ); diff --git a/src/Interactivity/directives/wp-text.php b/src/Interactivity/directives/wp-text.php index 2a3106a5810..eeb603cc9d4 100644 --- a/src/Interactivity/directives/wp-text.php +++ b/src/Interactivity/directives/wp-text.php @@ -1,13 +1,13 @@ get_attribute( 'data-wp-text' ); + $value = $tags->get_attribute( 'data-wc-text' ); if ( null === $value ) { return; } diff --git a/src/Interactivity/load.php b/src/Interactivity/load.php index 81bbd260bc0..0bfa10e5191 100644 --- a/src/Interactivity/load.php +++ b/src/Interactivity/load.php @@ -1,12 +1,12 @@ get_all_registered(); From bf443b8b63dae88659f8fc67bd93082dcde4f984 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 12:50:30 +0200 Subject: [PATCH 06/23] Replace guternberg_ prefix with woocommerce_ --- src/Interactivity/blocks.php | 4 ++-- src/Interactivity/directive-processing.php | 24 ++++++++++----------- src/Interactivity/directives/wp-bind.php | 4 ++-- src/Interactivity/directives/wp-class.php | 4 ++-- src/Interactivity/directives/wp-context.php | 2 +- src/Interactivity/directives/wp-style.php | 8 +++---- src/Interactivity/directives/wp-text.php | 4 ++-- src/Interactivity/scripts.php | 4 ++-- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Interactivity/blocks.php b/src/Interactivity/blocks.php index 76955357e6c..3df2f2d5966 100644 --- a/src/Interactivity/blocks.php +++ b/src/Interactivity/blocks.php @@ -13,7 +13,7 @@ * * @return array Filtered block type metadata. */ -function gutenberg_block_update_interactive_view_script( $metadata ) { +function woocommerce_block_update_interactive_view_script( $metadata ) { if ( in_array( $metadata['name'], array( 'core/image' ), true ) && str_contains( $metadata['file'], 'build/block-library/blocks' ) @@ -22,4 +22,4 @@ function gutenberg_block_update_interactive_view_script( $metadata ) { } return $metadata; } -add_filter( 'block_type_metadata', 'gutenberg_block_update_interactive_view_script', 10, 1 ); +add_filter( 'block_type_metadata', 'woocommerce_block_update_interactive_view_script', 10, 1 ); diff --git a/src/Interactivity/directive-processing.php b/src/Interactivity/directive-processing.php index 369a6a8ef7e..9f9769692ed 100644 --- a/src/Interactivity/directive-processing.php +++ b/src/Interactivity/directive-processing.php @@ -15,7 +15,7 @@ * * @return string Filtered block content. */ -function gutenberg_interactivity_process_directives_in_root_blocks( $block_content, $block ) { +function woocommerce_interactivity_process_directives_in_root_blocks( $block_content, $block ) { // Don't process inner blocks or root blocks that don't contain directives. if ( isset( $block['is_inner_block'] ) || strpos( $block_content, 'data-wc-' ) === false ) { return $block_content; @@ -23,18 +23,18 @@ function gutenberg_interactivity_process_directives_in_root_blocks( $block_conte // TODO: Add some directive/components registration mechanism. $directives = array( - 'data-wc-bind' => 'gutenberg_interactivity_process_wc_bind', - 'data-wc-context' => 'gutenberg_interactivity_process_wc_context', - 'data-wc-class' => 'gutenberg_interactivity_process_wc_class', - 'data-wc-style' => 'gutenberg_interactivity_process_wc_style', - 'data-wc-text' => 'gutenberg_interactivity_process_wc_text', + 'data-wc-bind' => 'woocommerce_interactivity_process_wc_bind', + 'data-wc-context' => 'woocommerce_interactivity_process_wc_context', + 'data-wc-class' => 'woocommerce_interactivity_process_wc_class', + 'data-wc-style' => 'woocommerce_interactivity_process_wc_style', + 'data-wc-text' => 'woocommerce_interactivity_process_wc_text', ); $tags = new WC_Directive_Processor( $block_content ); - $tags = gutenberg_interactivity_process_directives( $tags, 'data-wc-', $directives ); + $tags = woocommerce_interactivity_process_directives( $tags, 'data-wc-', $directives ); return $tags->get_updated_html(); } -add_filter( 'render_block', 'gutenberg_interactivity_process_directives_in_root_blocks', 10, 2 ); +add_filter( 'render_block', 'woocommerce_interactivity_process_directives_in_root_blocks', 10, 2 ); /** * Mark the inner blocks with a temporary property so we can discard them later, @@ -46,13 +46,13 @@ function gutenberg_interactivity_process_directives_in_root_blocks( $block_conte * * @return array The parsed block. */ -function gutenberg_interactivity_mark_inner_blocks( $parsed_block, $source_block, $parent_block ) { +function woocommerce_interactivity_mark_inner_blocks( $parsed_block, $source_block, $parent_block ) { if ( isset( $parent_block ) ) { $parsed_block['is_inner_block'] = true; } return $parsed_block; } -add_filter( 'render_block_data', 'gutenberg_interactivity_mark_inner_blocks', 10, 3 ); +add_filter( 'render_block_data', 'woocommerce_interactivity_mark_inner_blocks', 10, 3 ); /** * Process directives. @@ -64,7 +64,7 @@ function gutenberg_interactivity_mark_inner_blocks( $parsed_block, $source_block * @return WC_Directive_Processor The modified instance of the * WC_Directive_Processor. */ -function gutenberg_interactivity_process_directives( $tags, $prefix, $directives ) { +function woocommerce_interactivity_process_directives( $tags, $prefix, $directives ) { $context = new WC_Directive_Context; $tag_stack = array(); @@ -129,7 +129,7 @@ function gutenberg_interactivity_process_directives( $tags, $prefix, $directives * @param array $context Context data. * @return mixed */ -function gutenberg_interactivity_evaluate_reference( $path, array $context = array() ) { +function woocommerce_interactivity_evaluate_reference( $path, array $context = array() ) { $store = array_merge( WC_Interactivity_Store::get_data(), array( 'context' => $context ) diff --git a/src/Interactivity/directives/wp-bind.php b/src/Interactivity/directives/wp-bind.php index 3fd5d9c0773..0b9333c2f44 100644 --- a/src/Interactivity/directives/wp-bind.php +++ b/src/Interactivity/directives/wp-bind.php @@ -12,7 +12,7 @@ * @param WC_Directive_Processor $tags Tags. * @param WC_Directive_Context $context Directive context. */ -function gutenberg_interactivity_process_wc_bind( $tags, $context ) { +function woocommerce_interactivity_process_wc_bind( $tags, $context ) { if ( $tags->is_tag_closer() ) { return; } @@ -26,7 +26,7 @@ function gutenberg_interactivity_process_wc_bind( $tags, $context ) { } $expr = $tags->get_attribute( $attr ); - $value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $value = woocommerce_interactivity_evaluate_reference( $expr, $context->get_context() ); $tags->set_attribute( $bound_attr, $value ); } } diff --git a/src/Interactivity/directives/wp-class.php b/src/Interactivity/directives/wp-class.php index 30315cba895..2cf605ba7ee 100644 --- a/src/Interactivity/directives/wp-class.php +++ b/src/Interactivity/directives/wp-class.php @@ -12,7 +12,7 @@ * @param WC_Directive_Processor $tags Tags. * @param WC_Directive_Context $context Directive context. */ -function gutenberg_interactivity_process_wc_class( $tags, $context ) { +function woocommerce_interactivity_process_wc_class( $tags, $context ) { if ( $tags->is_tag_closer() ) { return; } @@ -26,7 +26,7 @@ function gutenberg_interactivity_process_wc_class( $tags, $context ) { } $expr = $tags->get_attribute( $attr ); - $add_class = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $add_class = woocommerce_interactivity_evaluate_reference( $expr, $context->get_context() ); if ( $add_class ) { $tags->add_class( $class_name ); } else { diff --git a/src/Interactivity/directives/wp-context.php b/src/Interactivity/directives/wp-context.php index d5db3f60fc2..2d543d5b861 100644 --- a/src/Interactivity/directives/wp-context.php +++ b/src/Interactivity/directives/wp-context.php @@ -11,7 +11,7 @@ * @param WC_Directive_Processor $tags Tags. * @param WC_Directive_Context $context Directive context. */ -function gutenberg_interactivity_process_wc_context( $tags, $context ) { +function woocommerce_interactivity_process_wc_context( $tags, $context ) { if ( $tags->is_tag_closer() ) { $context->rewind_context(); return; diff --git a/src/Interactivity/directives/wp-style.php b/src/Interactivity/directives/wp-style.php index cb87906388f..436dd99809e 100644 --- a/src/Interactivity/directives/wp-style.php +++ b/src/Interactivity/directives/wp-style.php @@ -12,7 +12,7 @@ * @param WC_Directive_Processor $tags Tags. * @param WC_Directive_Context $context Directive context. */ -function gutenberg_interactivity_process_wc_style( $tags, $context ) { +function woocommerce_interactivity_process_wc_style( $tags, $context ) { if ( $tags->is_tag_closer() ) { return; } @@ -26,10 +26,10 @@ function gutenberg_interactivity_process_wc_style( $tags, $context ) { } $expr = $tags->get_attribute( $attr ); - $style_value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $style_value = woocommerce_interactivity_evaluate_reference( $expr, $context->get_context() ); if ( $style_value ) { $style_attr = $tags->get_attribute( 'style' ); - $style_attr = gutenberg_interactivity_set_style( $style_attr, $style_name, $style_value ); + $style_attr = woocommerce_interactivity_set_style( $style_attr, $style_name, $style_value ); $tags->set_attribute( 'style', $style_attr ); } else { // TODO: Do we want to unset styles if they're null? @@ -45,7 +45,7 @@ function gutenberg_interactivity_process_wc_style( $tags, $context ) { * @param string $value Style property value. * @return string Amended styles. */ -function gutenberg_interactivity_set_style( $style, $name, $value ) { +function woocommerce_interactivity_set_style( $style, $name, $value ) { $style_assignments = explode( ';', $style ); $modified = false; foreach ( $style_assignments as $style_assignment ) { diff --git a/src/Interactivity/directives/wp-text.php b/src/Interactivity/directives/wp-text.php index eeb603cc9d4..99ae96460e7 100644 --- a/src/Interactivity/directives/wp-text.php +++ b/src/Interactivity/directives/wp-text.php @@ -12,7 +12,7 @@ * @param WC_Directive_Processor $tags Tags. * @param WC_Directive_Context $context Directive context. */ -function gutenberg_interactivity_process_wc_text( $tags, $context ) { +function woocommerce_interactivity_process_wc_text( $tags, $context ) { if ( $tags->is_tag_closer() ) { return; } @@ -22,6 +22,6 @@ function gutenberg_interactivity_process_wc_text( $tags, $context ) { return; } - $text = gutenberg_interactivity_evaluate_reference( $value, $context->get_context() ); + $text = woocommerce_interactivity_evaluate_reference( $value, $context->get_context() ); $tags->set_inner_html( esc_html( $text ) ); } diff --git a/src/Interactivity/scripts.php b/src/Interactivity/scripts.php index f09e6cebb82..a82e547e2f7 100644 --- a/src/Interactivity/scripts.php +++ b/src/Interactivity/scripts.php @@ -11,7 +11,7 @@ * it work with `wc_store` and it should be replaced with deferred scripts or * modules. */ -function gutenberg_interactivity_move_interactive_scripts_to_the_footer() { +function woocommerce_interactivity_move_interactive_scripts_to_the_footer() { // Move the @wordpress/interactivity package to the footer. wp_script_add_data( 'wc-interactivity', 'group', 1 ); @@ -25,4 +25,4 @@ function gutenberg_interactivity_move_interactive_scripts_to_the_footer() { } } } -add_action( 'wp_enqueue_scripts', 'gutenberg_interactivity_move_interactive_scripts_to_the_footer', 11 ); +add_action( 'wp_enqueue_scripts', 'woocommerce_interactivity_move_interactive_scripts_to_the_footer', 11 ); From 2e070ffda5d8f61c92b5ae1c4cc81ed7b3c12553 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 12:55:28 +0200 Subject: [PATCH 07/23] Remove file comments from Gutenberg --- src/Interactivity/blocks.php | 7 ------- src/Interactivity/class-wp-directive-context.php | 7 ------- src/Interactivity/class-wp-directive-processor.php | 7 ------- src/Interactivity/class-wp-interactivity-store.php | 10 ---------- src/Interactivity/directive-processing.php | 8 -------- src/Interactivity/directives/wp-bind.php | 7 ------- src/Interactivity/directives/wp-class.php | 7 ------- src/Interactivity/directives/wp-context.php | 6 ------ src/Interactivity/directives/wp-style.php | 7 ------- src/Interactivity/directives/wp-text.php | 7 ------- src/Interactivity/scripts.php | 7 ------- src/Interactivity/store.php | 7 ------- 12 files changed, 87 deletions(-) diff --git a/src/Interactivity/blocks.php b/src/Interactivity/blocks.php index 3df2f2d5966..6e625296353 100644 --- a/src/Interactivity/blocks.php +++ b/src/Interactivity/blocks.php @@ -1,11 +1,4 @@ Date: Mon, 19 Jun 2023 12:56:45 +0200 Subject: [PATCH 08/23] Rename files with `wp` prefix --- ...ss-wp-directive-context.php => class-wc-directive-context.php} | 0 ...p-directive-processor.php => class-wc-directive-processor.php} | 0 ...p-interactivity-store.php => class-wc-interactivity-store.php} | 0 src/Interactivity/directives/{wp-bind.php => wc-bind.php} | 0 src/Interactivity/directives/{wp-class.php => wc-class.php} | 0 src/Interactivity/directives/{wp-context.php => wc-context.php} | 0 src/Interactivity/directives/{wp-style.php => wc-style.php} | 0 src/Interactivity/directives/{wp-text.php => wc-text.php} | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename src/Interactivity/{class-wp-directive-context.php => class-wc-directive-context.php} (100%) rename src/Interactivity/{class-wp-directive-processor.php => class-wc-directive-processor.php} (100%) rename src/Interactivity/{class-wp-interactivity-store.php => class-wc-interactivity-store.php} (100%) rename src/Interactivity/directives/{wp-bind.php => wc-bind.php} (100%) rename src/Interactivity/directives/{wp-class.php => wc-class.php} (100%) rename src/Interactivity/directives/{wp-context.php => wc-context.php} (100%) rename src/Interactivity/directives/{wp-style.php => wc-style.php} (100%) rename src/Interactivity/directives/{wp-text.php => wc-text.php} (100%) diff --git a/src/Interactivity/class-wp-directive-context.php b/src/Interactivity/class-wc-directive-context.php similarity index 100% rename from src/Interactivity/class-wp-directive-context.php rename to src/Interactivity/class-wc-directive-context.php diff --git a/src/Interactivity/class-wp-directive-processor.php b/src/Interactivity/class-wc-directive-processor.php similarity index 100% rename from src/Interactivity/class-wp-directive-processor.php rename to src/Interactivity/class-wc-directive-processor.php diff --git a/src/Interactivity/class-wp-interactivity-store.php b/src/Interactivity/class-wc-interactivity-store.php similarity index 100% rename from src/Interactivity/class-wp-interactivity-store.php rename to src/Interactivity/class-wc-interactivity-store.php diff --git a/src/Interactivity/directives/wp-bind.php b/src/Interactivity/directives/wc-bind.php similarity index 100% rename from src/Interactivity/directives/wp-bind.php rename to src/Interactivity/directives/wc-bind.php diff --git a/src/Interactivity/directives/wp-class.php b/src/Interactivity/directives/wc-class.php similarity index 100% rename from src/Interactivity/directives/wp-class.php rename to src/Interactivity/directives/wc-class.php diff --git a/src/Interactivity/directives/wp-context.php b/src/Interactivity/directives/wc-context.php similarity index 100% rename from src/Interactivity/directives/wp-context.php rename to src/Interactivity/directives/wc-context.php diff --git a/src/Interactivity/directives/wp-style.php b/src/Interactivity/directives/wc-style.php similarity index 100% rename from src/Interactivity/directives/wp-style.php rename to src/Interactivity/directives/wc-style.php diff --git a/src/Interactivity/directives/wp-text.php b/src/Interactivity/directives/wc-text.php similarity index 100% rename from src/Interactivity/directives/wp-text.php rename to src/Interactivity/directives/wc-text.php From 91851997f33c599e2d9cc2659e0ee9a20c853a02 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 12:58:53 +0200 Subject: [PATCH 09/23] Fix code to load Interactivity API php files --- src/Interactivity/load.php | 22 +++++++++++----------- woocommerce-gutenberg-products-block.php | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Interactivity/load.php b/src/Interactivity/load.php index 0bfa10e5191..07cd41e7cc6 100644 --- a/src/Interactivity/load.php +++ b/src/Interactivity/load.php @@ -1,12 +1,12 @@ Date: Mon, 19 Jun 2023 13:00:29 +0200 Subject: [PATCH 10/23] Remove TODO comments --- src/Interactivity/class-wc-interactivity-store.php | 3 +-- src/Interactivity/directive-processing.php | 3 +-- src/Interactivity/directives/wc-style.php | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Interactivity/class-wc-interactivity-store.php b/src/Interactivity/class-wc-interactivity-store.php index df9721538e8..cb2191420bf 100644 --- a/src/Interactivity/class-wc-interactivity-store.php +++ b/src/Interactivity/class-wc-interactivity-store.php @@ -39,8 +39,7 @@ static function merge_data( $data ) { * @return string|false Serialized JSON data. */ static function serialize() { - // TODO: Escape? - return wp_json_encode( self::$store ); + return wp_json_encode( self::$store ); } /** diff --git a/src/Interactivity/directive-processing.php b/src/Interactivity/directive-processing.php index 05b6571393e..9f92ad02d40 100644 --- a/src/Interactivity/directive-processing.php +++ b/src/Interactivity/directive-processing.php @@ -13,8 +13,7 @@ function woocommerce_interactivity_process_directives_in_root_blocks( $block_con return $block_content; } - // TODO: Add some directive/components registration mechanism. - $directives = array( + $directives = array( 'data-wc-bind' => 'woocommerce_interactivity_process_wc_bind', 'data-wc-context' => 'woocommerce_interactivity_process_wc_context', 'data-wc-class' => 'woocommerce_interactivity_process_wc_class', diff --git a/src/Interactivity/directives/wc-style.php b/src/Interactivity/directives/wc-style.php index 609331dee9a..bfb8d8e86df 100644 --- a/src/Interactivity/directives/wc-style.php +++ b/src/Interactivity/directives/wc-style.php @@ -25,8 +25,7 @@ function woocommerce_interactivity_process_wc_style( $tags, $context ) { $style_attr = woocommerce_interactivity_set_style( $style_attr, $style_name, $style_value ); $tags->set_attribute( 'style', $style_attr ); } else { - // TODO: Do we want to unset styles if they're null? - } + } } } @@ -44,8 +43,7 @@ function woocommerce_interactivity_set_style( $style, $name, $value ) { foreach ( $style_assignments as $style_assignment ) { list( $style_name ) = explode( ':', $style_assignment ); if ( trim( $style_name ) === $name ) { - // TODO: Retain surrounding whitespace from $style_value, if any. - $style_assignment = $style_name . ': ' . $value; + $style_assignment = $style_name . ': ' . $value; $modified = true; break; } From 901151b91ee8f36b44f0072d97dcb538a1f5a5e6 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 16:20:31 +0200 Subject: [PATCH 11/23] Replace @wordpress with @woocommerce --- src/Interactivity/scripts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Interactivity/scripts.php b/src/Interactivity/scripts.php index 8efe1dc69ab..0264dbb608a 100644 --- a/src/Interactivity/scripts.php +++ b/src/Interactivity/scripts.php @@ -5,7 +5,7 @@ * modules. */ function woocommerce_interactivity_move_interactive_scripts_to_the_footer() { - // Move the @wordpress/interactivity package to the footer. + // Move the @woocommerce/interactivity package to the footer. wp_script_add_data( 'wc-interactivity', 'group', 1 ); // Move all the view scripts of the interactive blocks to the footer. From 4f653f5e3be537413ef4ec4a0245675a9f79cf7b Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 16:58:39 +0200 Subject: [PATCH 12/23] Update Webpack configuration --- bin/webpack-configs.js | 26 +++++++++----------------- bin/webpack-helpers.js | 2 ++ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/bin/webpack-configs.js b/bin/webpack-configs.js index 41c3bcfb6fa..b5fb6f3f82d 100644 --- a/bin/webpack-configs.js +++ b/bin/webpack-configs.js @@ -785,11 +785,17 @@ const getInteractivityAPIConfig = ( options = {} ) => { const { alias, resolvePlugins = [] } = options; return { entry: { - runtime: './assets/js/interactivity', + 'wc-interactivity': './assets/js/interactivity', }, output: { - filename: 'woo-directives-[name].js', + filename: '[name].js', path: path.resolve( __dirname, '../build/' ), + library: [ 'wc', '__experimentalInteractivity' ], + libraryTarget: 'this', + // This fixes an issue with multiple webpack projects using chunking + // overwriting each other's chunk loader function. + // See https://webpack.js.org/configuration/output/#outputjsonpfunction + jsonpFunction: 'webpackWcBlocksJsonp', }, resolve: { alias, @@ -804,21 +810,6 @@ const getInteractivityAPIConfig = ( options = {} ) => { getProgressBarPluginConfig( 'WP directives' ) ), ], - optimization: { - runtimeChunk: { - name: 'vendors', - }, - splitChunks: { - cacheGroups: { - vendors: { - test: /[\\/]node_modules[\\/]/, - name: 'vendors', - minSize: 0, - chunks: 'all', - }, - }, - }, - }, module: { rules: [ { @@ -841,6 +832,7 @@ const getInteractivityAPIConfig = ( options = {} ) => { }, ], ], + // Required until Webpack is updated to ^5.0.0 plugins: [ '@babel/plugin-proposal-optional-chaining', ], diff --git a/bin/webpack-helpers.js b/bin/webpack-helpers.js index 0eaa4926eb0..deb1b16f905 100644 --- a/bin/webpack-helpers.js +++ b/bin/webpack-helpers.js @@ -17,6 +17,7 @@ const wcDepMap = { '@woocommerce/shared-hocs': [ 'wc', 'wcBlocksSharedHocs' ], '@woocommerce/price-format': [ 'wc', 'priceFormat' ], '@woocommerce/blocks-checkout': [ 'wc', 'blocksCheckout' ], + '@woocommerce/interactivity': [ 'wc', '__experimentalInteractivity' ], }; const wcHandleMap = { @@ -28,6 +29,7 @@ const wcHandleMap = { '@woocommerce/shared-hocs': 'wc-blocks-shared-hocs', '@woocommerce/price-format': 'wc-price-format', '@woocommerce/blocks-checkout': 'wc-blocks-checkout', + '@woocommerce/interactivity': 'wc-interactivity', }; const getAlias = ( options = {} ) => { From 892012017510dbf336073fd0d6ee8fb3373f7633 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 17:05:24 +0200 Subject: [PATCH 13/23] Fix directive prefix --- assets/js/interactivity/constants.js | 4 ++-- assets/js/interactivity/directives.js | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/assets/js/interactivity/constants.js b/assets/js/interactivity/constants.js index 9b198b11f6d..2802f9103fe 100644 --- a/assets/js/interactivity/constants.js +++ b/assets/js/interactivity/constants.js @@ -1,2 +1,2 @@ -export const csnMetaTagItemprop = 'woo-client-side-navigation'; -export const directivePrefix = 'woo'; +export const csnMetaTagItemprop = 'wc-client-side-navigation'; +export const directivePrefix = 'wc'; diff --git a/assets/js/interactivity/directives.js b/assets/js/interactivity/directives.js index 3ae1e4db6a6..a306b7df9fe 100644 --- a/assets/js/interactivity/directives.js +++ b/assets/js/interactivity/directives.js @@ -27,7 +27,7 @@ const mergeDeepSignals = ( target, source ) => { }; export default () => { - // data-woo-context + // data-wc-context directive( 'context', ( { @@ -50,7 +50,7 @@ export default () => { { priority: 5 } ); - // data-woo-effect--[name] + // data-wc-effect--[name] directive( 'effect', ( { directives: { effect }, context, evaluate } ) => { const contextValue = useContext( context ); Object.values( effect ).forEach( ( path ) => { @@ -60,7 +60,7 @@ export default () => { } ); } ); - // data-woo-on--[event] + // data-wc-on--[event] directive( 'on', ( { directives: { on }, element, evaluate, context } ) => { const contextValue = useContext( context ); Object.entries( on ).forEach( ( [ name, path ] ) => { @@ -70,7 +70,7 @@ export default () => { } ); } ); - // data-woo-class--[classname] + // data-wc-class--[classname] directive( 'class', ( { @@ -115,7 +115,7 @@ export default () => { } ); - // data-woo-bind--[attribute] + // data-wc-bind--[attribute] directive( 'bind', ( { directives: { bind }, element, context, evaluate } ) => { @@ -151,7 +151,7 @@ export default () => { } ); - // data-woo-link + // data-wc-link directive( 'link', ( { @@ -191,7 +191,7 @@ export default () => { } ); - // data-woo-show + // data-wc-show directive( 'show', ( { @@ -211,7 +211,7 @@ export default () => { } ); - // data-woo-ignore + // data-wc-ignore directive( 'ignore', ( { @@ -231,7 +231,7 @@ export default () => { } ); - // data-woo-text + // data-wc-text directive( 'text', ( { From 9d14a7e718a1eb51119b4b0b6217dc2f26633932 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 19:38:46 +0200 Subject: [PATCH 14/23] Remove interactivity folder from tsconfig exclude --- tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index e961b8bd641..2a1a5b6c03b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,6 @@ "./storybook/build", "./storybook/webpack.config.js", "./storybook/preview.js", - "./bin", - "./assets/js/interactivity/*" + "./bin" ] } From 872e8277d29e51b13c4f26dcaee1370f7e4e4a5c Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 19:39:19 +0200 Subject: [PATCH 15/23] Add client-side navigation meta tag code --- src/Interactivity/client-side-navigation.php | 9 +++++++++ src/Interactivity/load.php | 1 + 2 files changed, 10 insertions(+) create mode 100644 src/Interactivity/client-side-navigation.php diff --git a/src/Interactivity/client-side-navigation.php b/src/Interactivity/client-side-navigation.php new file mode 100644 index 00000000000..ab550070f61 --- /dev/null +++ b/src/Interactivity/client-side-navigation.php @@ -0,0 +1,9 @@ +'; +} +add_action( 'wp_head', 'woocommerce_interactivity_add_client_side_navigation_meta_tag' ); diff --git a/src/Interactivity/load.php b/src/Interactivity/load.php index 07cd41e7cc6..6da25e9eb04 100644 --- a/src/Interactivity/load.php +++ b/src/Interactivity/load.php @@ -2,6 +2,7 @@ require __DIR__ . '/class-wc-interactivity-store.php'; require __DIR__ . '/store.php'; require __DIR__ . '/scripts.php'; +require __DIR__ . '/client-side-navigation.php'; require __DIR__ . '/class-wc-directive-processor.php'; require __DIR__ . '/class-wc-directive-context.php'; require __DIR__ . '/directive-processing.php'; From ad7d14a8980d8c271f0be976bff6a28b0243b16e Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 19 Jun 2023 20:19:10 +0200 Subject: [PATCH 16/23] Remove unneeded blocks.php file --- src/Interactivity/blocks.php | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/Interactivity/blocks.php diff --git a/src/Interactivity/blocks.php b/src/Interactivity/blocks.php deleted file mode 100644 index 6e625296353..00000000000 --- a/src/Interactivity/blocks.php +++ /dev/null @@ -1,18 +0,0 @@ - Date: Tue, 20 Jun 2023 14:41:41 +0200 Subject: [PATCH 17/23] Fix store tag id --- assets/js/interactivity/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/interactivity/store.js b/assets/js/interactivity/store.js index f7f544914a6..d00308f8012 100644 --- a/assets/js/interactivity/store.js +++ b/assets/js/interactivity/store.js @@ -18,7 +18,7 @@ export const deepMerge = ( target, source ) => { const getSerializedState = () => { const storeTag = document.querySelector( - `script[type="application/json"]#store` + `script[type="application/json"]#wc-interactivity-store-data` ); if ( ! storeTag ) return {}; try { From 7ae8b81d8b8edbf3ba80ae6d5532ca2242c2a139 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 20 Jun 2023 14:42:02 +0200 Subject: [PATCH 18/23] Register Interactivity API runtime script --- src/Interactivity/scripts.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Interactivity/scripts.php b/src/Interactivity/scripts.php index 0264dbb608a..42712fbfe4c 100644 --- a/src/Interactivity/scripts.php +++ b/src/Interactivity/scripts.php @@ -19,3 +19,19 @@ function woocommerce_interactivity_move_interactive_scripts_to_the_footer() { } } add_action( 'wp_enqueue_scripts', 'woocommerce_interactivity_move_interactive_scripts_to_the_footer', 11 ); + +/** + * Register the Interactivity API runtime and make it available to be enqueued + * as a dependency in interactive blocks. + */ +function woocommerce_interactivity_register_runtime() { + $script_path = plugins_url( '../../build/wc-interactivity.js', __FILE__ ); + wp_register_script( + 'wc-interactivity', + $script_path, + array(), + filemtime( $script_path ), + true + ); +} +add_action( 'wp_enqueue_scripts', 'woocommerce_interactivity_register_runtime' ); From b27eedfd7078750afb9e59016d47ffb80c1d239e Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 20 Jun 2023 19:03:04 +0200 Subject: [PATCH 19/23] Fix Interactivity API runtime registering --- src/Interactivity/scripts.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Interactivity/scripts.php b/src/Interactivity/scripts.php index 42712fbfe4c..7b48834807c 100644 --- a/src/Interactivity/scripts.php +++ b/src/Interactivity/scripts.php @@ -25,12 +25,25 @@ function woocommerce_interactivity_move_interactive_scripts_to_the_footer() { * as a dependency in interactive blocks. */ function woocommerce_interactivity_register_runtime() { - $script_path = plugins_url( '../../build/wc-interactivity.js', __FILE__ ); + $plugin_path = \Automattic\WooCommerce\Blocks\Package::get_path(); + $plugin_url = plugin_dir_url( $plugin_path . '/index.php' ); + + $file = 'build/wc-interactivity.js'; + + $file_path = $plugin_path . $file; + $file_url = $plugin_url . $file; + + if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $file_path ) ) { + $version = filemtime( $file_path ); + } else { + $version = \Automattic\WooCommerce\Blocks\Package::get_version(); + } + wp_register_script( 'wc-interactivity', - $script_path, + $file_url, array(), - filemtime( $script_path ), + $version, true ); } From 98c6bf4133e13477be5df6b2f11567d361964657 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 20 Jun 2023 21:02:31 +0200 Subject: [PATCH 20/23] Add Simple Price Filter block --- .../js/blocks/simple-price-filter/block.json | 18 ++++ .../js/blocks/simple-price-filter/frontend.js | 71 +++++++++++++ assets/js/blocks/simple-price-filter/index.js | 9 ++ .../js/blocks/simple-price-filter/style.scss | 67 +++++++++++++ bin/webpack-entries.js | 1 + src/BlockTypes/SimplePriceFilter.php | 99 +++++++++++++++++++ src/BlockTypesController.php | 1 + woocommerce-gutenberg-products-block.php | 3 + 8 files changed, 269 insertions(+) create mode 100644 assets/js/blocks/simple-price-filter/block.json create mode 100644 assets/js/blocks/simple-price-filter/frontend.js create mode 100644 assets/js/blocks/simple-price-filter/index.js create mode 100644 assets/js/blocks/simple-price-filter/style.scss create mode 100644 src/BlockTypes/SimplePriceFilter.php diff --git a/assets/js/blocks/simple-price-filter/block.json b/assets/js/blocks/simple-price-filter/block.json new file mode 100644 index 00000000000..d7a9774666c --- /dev/null +++ b/assets/js/blocks/simple-price-filter/block.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "name": "woocommerce/simple-price-filter", + "version": "1.0.0", + "title": "Simple Price filter", + "description": "Enable customers to filter the product grid by choosing a price range.", + "category": "woocommerce", + "keywords": [ "WooCommerce" ], + "textdomain": "woo-gutenberg-products-block", + "apiVersion": 2, + "viewScript": [ + "wc-simple-price-filter-block-frontend", + "wc-interactivity" + ], + "supports": { + "interactivity": true + } +} diff --git a/assets/js/blocks/simple-price-filter/frontend.js b/assets/js/blocks/simple-price-filter/frontend.js new file mode 100644 index 00000000000..e6639add867 --- /dev/null +++ b/assets/js/blocks/simple-price-filter/frontend.js @@ -0,0 +1,71 @@ +/** + * External dependencies + */ +import { store, navigate } from '@woocommerce/interactivity'; + +const getHrefWithFilters = ( { state } ) => { + const { minPrice, maxPrice } = state.filters; + const url = new URL( window.location.href ); + const { searchParams } = url; + + if ( minPrice > 0 ) { + searchParams.set( 'min_price', minPrice ); + } else { + searchParams.delete( 'min_price' ); + } + + if ( maxPrice < state.filters.maxRange ) { + searchParams.set( 'max_price', maxPrice ); + } else { + searchParams.delete( 'max_price' ); + } + + return url.href; +}; + +store( { + state: { + filters: { + rangeStyle: ( { state } ) => { + const { minPrice, maxPrice, maxRange } = state.filters; + return [ + `--low: ${ ( 100 * minPrice ) / maxRange }%`, + `--high: ${ ( 100 * maxPrice ) / maxRange }%`, + ].join( ';' ); + }, + }, + }, + actions: { + filters: { + setMinPrice: ( { state, event } ) => { + const value = parseFloat( event.target.value ) || 0; + state.filters.minPrice = value; + }, + setMaxPrice: ( { state, event } ) => { + const value = + parseFloat( event.target.value ) || state.filters.maxRange; + state.filters.maxPrice = value; + }, + updateProducts: ( { state } ) => { + navigate( getHrefWithFilters( { state } ) ); + }, + reset: ( { state } ) => { + state.filters.minPrice = 0; + state.filters.maxPrice = state.filters.maxRange; + navigate( getHrefWithFilters( { state } ) ); + }, + updateActiveHandle: ( { state, event } ) => { + const { minPrice, maxPrice, maxRange } = state.filters; + const { target, offsetX } = event; + const xPos = offsetX / target.offsetWidth; + const minPos = minPrice / maxRange; + const maxPos = maxPrice / maxRange; + + state.filters.isMinActive = + Math.abs( xPos - minPos ) < Math.abs( xPos - maxPos ); + + state.filters.isMaxActive = ! state.filters.isMinActive; + }, + }, + }, +} ); diff --git a/assets/js/blocks/simple-price-filter/index.js b/assets/js/blocks/simple-price-filter/index.js new file mode 100644 index 00000000000..de30e2ed56f --- /dev/null +++ b/assets/js/blocks/simple-price-filter/index.js @@ -0,0 +1,9 @@ +/** + * External dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; + +registerBlockType( 'woocommerce/simple-price-filter', { + edit: () =>
Simple price filter
, + save: () => null, +} ); diff --git a/assets/js/blocks/simple-price-filter/style.scss b/assets/js/blocks/simple-price-filter/style.scss new file mode 100644 index 00000000000..0a92f493293 --- /dev/null +++ b/assets/js/blocks/simple-price-filter/style.scss @@ -0,0 +1,67 @@ +.wp-block-woocommerce-simple-price-filter { + --low: 0%; + --high: 100%; + --range-color: currentColor; + + .range { + position: relative; + margin: 15px 0; + + .range-bar { + position: relative; + height: 4px; + background: linear-gradient(90deg, transparent var(--low), var(--range-color) 0, var(--range-color) var(--high), transparent 0) no-repeat 0 100% / 100% 100%; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: currentColor; + opacity: 0.2; + } + } + + input[type="range"] { + position: absolute; + top: 50%; + left: 0; + width: 100%; + height: 0; + margin: 0; + padding: 0; + + &.active { + z-index: 10; + } + } + + input[type="range" i] { + color: -internal-light-dark(rgb(16, 16, 16), rgb(255, 255, 255)); + padding: initial; + } + } + + .text { + display: flex; + align-items: center; + justify-content: space-between; + margin: 16px 0; + gap: 8px; + + input[type="text"] { + padding: 8px; + margin: 0; + width: auto; + max-width: 60px; + min-width: 0; + font-size: 0.875em; + border-width: 1px; + border-style: solid; + border-color: currentColor; + border-radius: 4px; + } + } +} diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js index 996f24b89a9..d1e69f8027d 100644 --- a/bin/webpack-entries.js +++ b/bin/webpack-entries.js @@ -64,6 +64,7 @@ const blocks = { 'reviews-by-product': { customDir: 'reviews/reviews-by-product', }, + 'simple-price-filter': {}, 'single-product': {}, 'stock-filter': {}, 'product-collection': { diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php new file mode 100644 index 00000000000..851fb4c97e1 --- /dev/null +++ b/src/BlockTypes/SimplePriceFilter.php @@ -0,0 +1,99 @@ + array( + 'filters' => array( + 'minPrice' => $min_price, + 'maxPrice' => $max_price, + 'maxRange' => $max_range, + 'rangeStyle' => $range_style, + 'isMinActive' => true, + 'isMaxActive' => false, + ), + ), + ) + ); + + return << +

Filter by price

+
+
+ + +
+
+ + +
+ +
+HTML; + } +} diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php index 5e1fdc677db..a5ac22adb19 100644 --- a/src/BlockTypesController.php +++ b/src/BlockTypesController.php @@ -213,6 +213,7 @@ protected function get_block_types() { 'ReviewsByProduct', 'RelatedProducts', 'ProductDetails', + 'SimplePriceFilter', 'SingleProduct', 'StockFilter', ]; diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 7fe8c4c6450..d05109ee304 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -318,3 +318,6 @@ function woocommerce_blocks_interactivity_setup() { } } add_action( 'plugins_loaded', 'woocommerce_blocks_interactivity_setup' ); + +// Enable the interactivity API. +add_filter( 'woocommerce_blocks_enable_interactivity_api', '__return_true' ); From f08b67007256ec3b2c84c3a7f921c38b5aac99a2 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 21 Jun 2023 18:41:07 +0200 Subject: [PATCH 21/23] Remove all files related to directive processing in PHP --- .../class-wc-directive-context.php | 70 ------- .../class-wc-directive-processor.php | 186 ------------------ src/Interactivity/directive-processing.php | 161 --------------- src/Interactivity/directives/wc-bind.php | 25 --- src/Interactivity/directives/wc-class.php | 29 --- src/Interactivity/directives/wc-context.php | 27 --- src/Interactivity/directives/wc-style.php | 63 ------ src/Interactivity/directives/wc-text.php | 20 -- src/Interactivity/load.php | 8 - 9 files changed, 589 deletions(-) delete mode 100644 src/Interactivity/class-wc-directive-context.php delete mode 100644 src/Interactivity/class-wc-directive-processor.php delete mode 100644 src/Interactivity/directive-processing.php delete mode 100644 src/Interactivity/directives/wc-bind.php delete mode 100644 src/Interactivity/directives/wc-class.php delete mode 100644 src/Interactivity/directives/wc-context.php delete mode 100644 src/Interactivity/directives/wc-style.php delete mode 100644 src/Interactivity/directives/wc-text.php diff --git a/src/Interactivity/class-wc-directive-context.php b/src/Interactivity/class-wc-directive-context.php deleted file mode 100644 index 18a23ed961a..00000000000 --- a/src/Interactivity/class-wc-directive-context.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - *
- * - *
- * - *
- */ -class WC_Directive_Context { - /** - * The stack used to store contexts internally. - * - * @var array An array of contexts. - */ - protected $stack = array( array() ); - - /** - * Constructor. - * - * Accepts a context as an argument to initialize this with. - * - * @param array $context A context. - */ - function __construct( $context = array() ) { - $this->set_context( $context ); - } - - /** - * Return the current context. - * - * @return array The current context. - */ - public function get_context() { - return end( $this->stack ); - } - - /** - * Set the current context. - * - * @param array $context The context to be set. - * - * @return void - */ - public function set_context( $context ) { - if ( $context ) { - array_push( $this->stack, array_replace_recursive( $this->get_context(), $context ) ); - } - } - - /** - * Reset the context to its previous state. - * - * @return void - */ - public function rewind_context() { - array_pop( $this->stack ); - } -} diff --git a/src/Interactivity/class-wc-directive-processor.php b/src/Interactivity/class-wc-directive-processor.php deleted file mode 100644 index aa444abcf6f..00000000000 --- a/src/Interactivity/class-wc-directive-processor.php +++ /dev/null @@ -1,186 +0,0 @@ -get_tag(); - - if ( self::is_html_void_element( $tag_name ) ) { - return false; - } - - while ( $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) ) { - if ( ! $this->is_tag_closer() ) { - $depth++; - continue; - } - - if ( 0 === $depth ) { - return true; - } - - $depth--; - } - - return false; - } - - /** - * Return the content between two balanced tags. - * - * When called on an opening tag, return the HTML content found between that - * opening tag and its matching closing tag. - * - * @return string The content between the current opening and its matching - * closing tag. - */ - public function get_inner_html() { - $bookmarks = $this->get_balanced_tag_bookmarks(); - if ( ! $bookmarks ) { - return false; - } - list( $start_name, $end_name ) = $bookmarks; - - $start = $this->bookmarks[ $start_name ]->end + 1; - $end = $this->bookmarks[ $end_name ]->start; - - $this->seek( $start_name ); // Return to original position. - $this->release_bookmark( $start_name ); - $this->release_bookmark( $end_name ); - - return substr( $this->html, $start, $end - $start ); - } - - /** - * Set the content between two balanced tags. - * - * When called on an opening tag, set the HTML content found between that - * opening tag and its matching closing tag. - * - * @param string $new_html The string to replace the content between the - * matching tags with. - * - * @return bool Whether the content was successfully replaced. - */ - public function set_inner_html( $new_html ) { - $this->get_updated_html(); // Apply potential previous updates. - - $bookmarks = $this->get_balanced_tag_bookmarks(); - if ( ! $bookmarks ) { - return false; - } - list( $start_name, $end_name ) = $bookmarks; - - $start = $this->bookmarks[ $start_name ]->end + 1; - $end = $this->bookmarks[ $end_name ]->start; - - $this->seek( $start_name ); // Return to original position. - $this->release_bookmark( $start_name ); - $this->release_bookmark( $end_name ); - - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $new_html ); - return true; - } - - /** - * Return a pair of bookmarks for the current opening tag and the matching - * closing tag. - * - * @return array|false A pair of bookmarks, or false if there's no matching - * closing tag. - */ - public function get_balanced_tag_bookmarks() { - $i = 0; - while ( array_key_exists( 'start' . $i, $this->bookmarks ) ) { - ++$i; - } - $start_name = 'start' . $i; - - $this->set_bookmark( $start_name ); - if ( ! $this->next_balanced_closer() ) { - $this->release_bookmark( $start_name ); - return false; - } - - $i = 0; - while ( array_key_exists( 'end' . $i, $this->bookmarks ) ) { - ++$i; - } - $end_name = 'end' . $i; - $this->set_bookmark( $end_name ); - - return array( $start_name, $end_name ); - } - - /** - * Whether a given HTML element is void (e.g.
). - * - * @param string $tag_name The element in question. - * @return bool True if the element is void. - * - * @see https://html.spec.whatwg.org/#elements-2 - */ - public static function is_html_void_element( $tag_name ) { - switch ( $tag_name ) { - case 'AREA': - case 'BASE': - case 'BR': - case 'COL': - case 'EMBED': - case 'HR': - case 'IMG': - case 'INPUT': - case 'LINK': - case 'META': - case 'SOURCE': - case 'TRACK': - case 'WBR': - return true; - - default: - return false; - } - } - - /** - * Extract and return the directive type and the the part after the double - * hyphen from an attribute name (if present), in an array format. - * - * Examples: - * - * 'wc-island' => array( 'wc-island', null ) - * 'wc-bind--src' => array( 'wc-bind', 'src' ) - * 'wc-thing--and--thang' => array( 'wc-thing', 'and--thang' ) - * - * @param string $name The attribute name. - * @return array The resulting array - */ - public static function parse_attribute_name( $name ) { - return explode( '--', $name, 2 ); - } -} diff --git a/src/Interactivity/directive-processing.php b/src/Interactivity/directive-processing.php deleted file mode 100644 index 9f92ad02d40..00000000000 --- a/src/Interactivity/directive-processing.php +++ /dev/null @@ -1,161 +0,0 @@ - 'woocommerce_interactivity_process_wc_bind', - 'data-wc-context' => 'woocommerce_interactivity_process_wc_context', - 'data-wc-class' => 'woocommerce_interactivity_process_wc_class', - 'data-wc-style' => 'woocommerce_interactivity_process_wc_style', - 'data-wc-text' => 'woocommerce_interactivity_process_wc_text', - ); - - $tags = new WC_Directive_Processor( $block_content ); - $tags = woocommerce_interactivity_process_directives( $tags, 'data-wc-', $directives ); - return $tags->get_updated_html(); -} -add_filter( 'render_block', 'woocommerce_interactivity_process_directives_in_root_blocks', 10, 2 ); - -/** - * Mark the inner blocks with a temporary property so we can discard them later, - * and process only the root blocks. - * - * @param array $parsed_block The parsed block. - * @param array $source_block The source block. - * @param array $parent_block The parent block. - * - * @return array The parsed block. - */ -function woocommerce_interactivity_mark_inner_blocks( $parsed_block, $source_block, $parent_block ) { - if ( isset( $parent_block ) ) { - $parsed_block['is_inner_block'] = true; - } - return $parsed_block; -} -add_filter( 'render_block_data', 'woocommerce_interactivity_mark_inner_blocks', 10, 3 ); - -/** - * Process directives. - * - * @param WC_Directive_Processor $tags An instance of the WC_Directive_Processor. - * @param string $prefix Attribute prefix. - * @param string[] $directives Directives. - * - * @return WC_Directive_Processor The modified instance of the - * WC_Directive_Processor. - */ -function woocommerce_interactivity_process_directives( $tags, $prefix, $directives ) { - $context = new WC_Directive_Context; - $tag_stack = array(); - - while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - $tag_name = $tags->get_tag(); - - // Is this a tag that closes the latest opening tag? - if ( $tags->is_tag_closer() ) { - if ( 0 === count( $tag_stack ) ) { - continue; - } - - list( $latest_opening_tag_name, $attributes ) = end( $tag_stack ); - if ( $latest_opening_tag_name === $tag_name ) { - array_pop( $tag_stack ); - - // If the matching opening tag didn't have any directives, we move on. - if ( 0 === count( $attributes ) ) { - continue; - } - } - } else { - $attributes = array(); - foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) { - /* - * Removes the part after the double hyphen before looking for - * the directive processor inside `$directives`, e.g., "wc-bind" - * from "wc-bind--src" and "wc-context" from "wc-context" etc... - */ - list( $type ) = WC_Directive_Processor::parse_attribute_name( $name ); - if ( array_key_exists( $type, $directives ) ) { - $attributes[] = $type; - } - } - - /* - * If this is an open tag, and if it either has directives, or if - * we're inside a tag that does, take note of this tag and its - * directives so we can call its directive processor once we - * encounter the matching closing tag. - */ - if ( - ! WC_Directive_Processor::is_html_void_element( $tags->get_tag() ) && - ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) - ) { - $tag_stack[] = array( $tag_name, $attributes ); - } - } - - foreach ( $attributes as $attribute ) { - call_user_func( $directives[ $attribute ], $tags, $context ); - } - } - - return $tags; -} - -/** - * Resolve the reference using the store and the context from the provided path. - * - * @param string $path Path. - * @param array $context Context data. - * @return mixed - */ -function woocommerce_interactivity_evaluate_reference( $path, array $context = array() ) { - $store = array_merge( - WC_Interactivity_Store::get_data(), - array( 'context' => $context ) - ); - - /* - * Check first if the directive path is preceded by a negator operator (!), - * indicating that the value obtained from the Interactivity Store (or the - * passed context) using the subsequent path should be negated. - */ - $should_negate_value = '!' === $path[0]; - - $path = $should_negate_value ? substr( $path, 1 ) : $path; - $path_segments = explode( '.', $path ); - $current = $store; - foreach ( $path_segments as $p ) { - if ( isset( $current[ $p ] ) ) { - $current = $current[ $p ]; - } else { - return null; - } - } - - /* - * Check if $current is an anonymous function or an arrow function, and if - * so, call it passing the store. Other types of callables are ignored in - * purpose, as arbitrary strings or arrays could be wrongly evaluated as - * "callables". - * - * E.g., "file" is an string and a "callable" (the "file" function exists). - */ - if ( $current instanceof Closure ) { - $current = call_user_func( $current, $store ); - } - - // Return the opposite if it has a negator operator (!). - return $should_negate_value ? ! $current : $current; -} diff --git a/src/Interactivity/directives/wc-bind.php b/src/Interactivity/directives/wc-bind.php deleted file mode 100644 index c41fc875495..00000000000 --- a/src/Interactivity/directives/wc-bind.php +++ /dev/null @@ -1,25 +0,0 @@ -is_tag_closer() ) { - return; - } - - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wc-bind--' ); - - foreach ( $prefixed_attributes as $attr ) { - list( , $bound_attr ) = WC_Directive_Processor::parse_attribute_name( $attr ); - if ( empty( $bound_attr ) ) { - continue; - } - - $expr = $tags->get_attribute( $attr ); - $value = woocommerce_interactivity_evaluate_reference( $expr, $context->get_context() ); - $tags->set_attribute( $bound_attr, $value ); - } -} diff --git a/src/Interactivity/directives/wc-class.php b/src/Interactivity/directives/wc-class.php deleted file mode 100644 index 4ad1ea3df98..00000000000 --- a/src/Interactivity/directives/wc-class.php +++ /dev/null @@ -1,29 +0,0 @@ -is_tag_closer() ) { - return; - } - - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wc-class--' ); - - foreach ( $prefixed_attributes as $attr ) { - list( , $class_name ) = WC_Directive_Processor::parse_attribute_name( $attr ); - if ( empty( $class_name ) ) { - continue; - } - - $expr = $tags->get_attribute( $attr ); - $add_class = woocommerce_interactivity_evaluate_reference( $expr, $context->get_context() ); - if ( $add_class ) { - $tags->add_class( $class_name ); - } else { - $tags->remove_class( $class_name ); - } - } -} diff --git a/src/Interactivity/directives/wc-context.php b/src/Interactivity/directives/wc-context.php deleted file mode 100644 index d55c429ce64..00000000000 --- a/src/Interactivity/directives/wc-context.php +++ /dev/null @@ -1,27 +0,0 @@ -is_tag_closer() ) { - $context->rewind_context(); - return; - } - - $value = $tags->get_attribute( 'data-wc-context' ); - if ( null === $value ) { - // No data-wc-context directive. - return; - } - - $new_context = json_decode( $value, true ); - if ( null === $new_context ) { - // Invalid JSON defined in the directive. - return; - } - - $context->set_context( $new_context ); -} diff --git a/src/Interactivity/directives/wc-style.php b/src/Interactivity/directives/wc-style.php deleted file mode 100644 index bfb8d8e86df..00000000000 --- a/src/Interactivity/directives/wc-style.php +++ /dev/null @@ -1,63 +0,0 @@ -is_tag_closer() ) { - return; - } - - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-wc-style--' ); - - foreach ( $prefixed_attributes as $attr ) { - list( , $style_name ) = WC_Directive_Processor::parse_attribute_name( $attr ); - if ( empty( $style_name ) ) { - continue; - } - - $expr = $tags->get_attribute( $attr ); - $style_value = woocommerce_interactivity_evaluate_reference( $expr, $context->get_context() ); - if ( $style_value ) { - $style_attr = $tags->get_attribute( 'style' ); - $style_attr = woocommerce_interactivity_set_style( $style_attr, $style_name, $style_value ); - $tags->set_attribute( 'style', $style_attr ); - } else { - } - } -} - -/** - * Set style. - * - * @param string $style Existing style to amend. - * @param string $name Style property name. - * @param string $value Style property value. - * @return string Amended styles. - */ -function woocommerce_interactivity_set_style( $style, $name, $value ) { - $style_assignments = explode( ';', $style ); - $modified = false; - foreach ( $style_assignments as $style_assignment ) { - list( $style_name ) = explode( ':', $style_assignment ); - if ( trim( $style_name ) === $name ) { - $style_assignment = $style_name . ': ' . $value; - $modified = true; - break; - } - } - - if ( ! $modified ) { - $new_style_assignment = $name . ': ' . $value; - // If the last element is empty or whitespace-only, we insert - // the new "key: value" pair before it. - if ( empty( trim( end( $style_assignments ) ) ) ) { - array_splice( $style_assignments, - 1, 0, $new_style_assignment ); - } else { - array_push( $style_assignments, $new_style_assignment ); - } - } - return implode( ';', $style_assignments ); -} diff --git a/src/Interactivity/directives/wc-text.php b/src/Interactivity/directives/wc-text.php deleted file mode 100644 index 85a2b5b7b2d..00000000000 --- a/src/Interactivity/directives/wc-text.php +++ /dev/null @@ -1,20 +0,0 @@ -is_tag_closer() ) { - return; - } - - $value = $tags->get_attribute( 'data-wc-text' ); - if ( null === $value ) { - return; - } - - $text = woocommerce_interactivity_evaluate_reference( $value, $context->get_context() ); - $tags->set_inner_html( esc_html( $text ) ); -} diff --git a/src/Interactivity/load.php b/src/Interactivity/load.php index 6da25e9eb04..8838971f099 100644 --- a/src/Interactivity/load.php +++ b/src/Interactivity/load.php @@ -3,11 +3,3 @@ require __DIR__ . '/store.php'; require __DIR__ . '/scripts.php'; require __DIR__ . '/client-side-navigation.php'; -require __DIR__ . '/class-wc-directive-processor.php'; -require __DIR__ . '/class-wc-directive-context.php'; -require __DIR__ . '/directive-processing.php'; -require __DIR__ . '/directives/wc-bind.php'; -require __DIR__ . '/directives/wc-context.php'; -require __DIR__ . '/directives/wc-class.php'; -require __DIR__ . '/directives/wc-style.php'; -require __DIR__ . '/directives/wc-text.php'; From 5b1eea06e1b17134d547d464ff73d4a49fee89a1 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 21 Jun 2023 18:42:49 +0200 Subject: [PATCH 22/23] Use values directly for SimplePriceFilter SSR --- src/BlockTypes/SimplePriceFilter.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php index 851fb4c97e1..d8528620990 100644 --- a/src/BlockTypes/SimplePriceFilter.php +++ b/src/BlockTypes/SimplePriceFilter.php @@ -26,7 +26,7 @@ class SimplePriceFilter extends AbstractBlock { */ public function render( $attributes = [], $content = '', $block = null ) { $wrapper_attributes = get_block_wrapper_attributes(); - $max_range = 90; // TODO: get this value from DB. + $max_range = 90; // This value should come from DB. $min_price = get_query_var( self::MIN_PRICE_QUERY_VAR, 0 ); $max_price = get_query_var( self::MAX_PRICE_QUERY_VAR, $max_range ); @@ -55,6 +55,7 @@ public function render( $attributes = [], $content = '', $block = null ) {

Filter by price

@@ -62,6 +63,9 @@ class='range' Date: Tue, 15 Aug 2023 15:52:37 +0200 Subject: [PATCH 23/23] Reset pages to 0 when changing filter Note: we also need to do this with `/page/x` --- assets/js/blocks/simple-price-filter/frontend.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/js/blocks/simple-price-filter/frontend.js b/assets/js/blocks/simple-price-filter/frontend.js index e6639add867..275ec49e2a5 100644 --- a/assets/js/blocks/simple-price-filter/frontend.js +++ b/assets/js/blocks/simple-price-filter/frontend.js @@ -20,6 +20,10 @@ const getHrefWithFilters = ( { state } ) => { searchParams.delete( 'max_price' ); } + searchParams.forEach( ( _, key ) => { + if ( /query-[0-9]+-page/.test( key ) ) searchParams.delete( key ); + } ); + return url.href; };