From 580778969e506da56396caf4dd03ce9843c1c7ab Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 21 Dec 2022 16:59:03 +0100 Subject: [PATCH 01/54] Add Interactivity API scripts --- assets/js/base/interactivity/components.js | 21 ++++ assets/js/base/interactivity/deepsignal.js | 51 ++++++++ assets/js/base/interactivity/directives.js | 140 +++++++++++++++++++++ assets/js/base/interactivity/hooks.js | 63 ++++++++++ assets/js/base/interactivity/index.js | 18 +++ assets/js/base/interactivity/router.js | 124 ++++++++++++++++++ assets/js/base/interactivity/utils.js | 76 +++++++++++ assets/js/base/interactivity/vdom.js | 53 ++++++++ bin/webpack-entries.js | 1 + bin/webpack-helpers.js | 4 + package-lock.json | 54 ++++++++ package.json | 3 + tsconfig.base.json | 1 + 13 files changed, 609 insertions(+) create mode 100644 assets/js/base/interactivity/components.js create mode 100644 assets/js/base/interactivity/deepsignal.js create mode 100644 assets/js/base/interactivity/directives.js create mode 100644 assets/js/base/interactivity/hooks.js create mode 100644 assets/js/base/interactivity/index.js create mode 100644 assets/js/base/interactivity/router.js create mode 100644 assets/js/base/interactivity/utils.js create mode 100644 assets/js/base/interactivity/vdom.js diff --git a/assets/js/base/interactivity/components.js b/assets/js/base/interactivity/components.js new file mode 100644 index 00000000000..aa248d2613e --- /dev/null +++ b/assets/js/base/interactivity/components.js @@ -0,0 +1,21 @@ +/** + * External dependencies + */ +import { useMemo } from 'preact/hooks'; + +/** + * Internal dependencies + */ +import { deepSignal } from './deepsignal'; +import { component } from './hooks'; + +export default () => { + const WpContext = ( { children, data, context: { Provider } } ) => { + const signals = useMemo( + () => deepSignal( JSON.parse( data ) ), + [ data ] + ); + return { children }; + }; + component( 'wp-context', WpContext ); +}; diff --git a/assets/js/base/interactivity/deepsignal.js b/assets/js/base/interactivity/deepsignal.js new file mode 100644 index 00000000000..ceaa36af2ad --- /dev/null +++ b/assets/js/base/interactivity/deepsignal.js @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import { signal } from '@preact/signals'; + +/** + * Internal dependencies + */ +import { knownSymbols, shouldWrap } from './utils'; + +const proxyToSignals = new WeakMap(); +const objToProxy = new WeakMap(); +const returnSignal = /^\$/; + +export const deepSignal = ( obj ) => new Proxy( obj, handlers ); + +const handlers = { + get( target, prop, receiver ) { + if ( typeof prop === 'symbol' && knownSymbols.has( prop ) ) + return Reflect.get( target, prop, receiver ); + const shouldReturnSignal = returnSignal.test( prop ); + const key = shouldReturnSignal + ? prop.replace( returnSignal, '' ) + : prop; + if ( ! proxyToSignals.has( receiver ) ) + proxyToSignals.set( receiver, new Map() ); + const signals = proxyToSignals.get( receiver ); + if ( ! signals.has( key ) ) { + let val = Reflect.get( target, key, receiver ); + if ( typeof val === 'object' && val !== null && shouldWrap( val ) ) + val = new Proxy( val, handlers ); + signals.set( key, signal( val ) ); + } + return returnSignal ? signals.get( key ) : signals.get( key ).value; + }, + + set( target, prop, val, receiver ) { + let internal = val; + if ( typeof val === 'object' && val !== null && shouldWrap( val ) ) { + if ( ! objToProxy.has( val ) ) + objToProxy.set( val, new Proxy( val, handlers ) ); + internal = objToProxy.get( val ); + } + if ( ! proxyToSignals.has( receiver ) ) + proxyToSignals.set( receiver, new Map() ); + const signals = proxyToSignals.get( receiver ); + if ( ! signals.has( prop ) ) signals.set( prop, signal( internal ) ); + else signals.get( prop ).value = internal; + return Reflect.set( target, prop, val, receiver ); + }, +}; diff --git a/assets/js/base/interactivity/directives.js b/assets/js/base/interactivity/directives.js new file mode 100644 index 00000000000..7af929514d4 --- /dev/null +++ b/assets/js/base/interactivity/directives.js @@ -0,0 +1,140 @@ +/** + * External dependencies + */ +import { useContext, useMemo, useEffect } from 'preact/hooks'; +import { useSignalEffect } from '@preact/signals'; + +/** + * Internal dependencies + */ +import { directive } from './hooks'; +import { deepSignal } from './deepsignal'; +import { prefetch, navigate, hasClientSideTransitions } from './router'; +import { getCallback } from './utils'; + +const raf = window.requestAnimationFrame; +// Until useSignalEffects is fixed: https://github.com/preactjs/signals/issues/228 +const tick = () => new Promise( ( r ) => raf( () => raf( r ) ) ); + +// Check if current page has client-side transitions enabled. +const clientSideTransitions = hasClientSideTransitions( document.head ); + +export default () => { + // wp-context + directive( + 'context', + ( { + directives: { + context: { default: context }, + }, + props: { children }, + context: { Provider }, + } ) => { + const signals = useMemo( () => deepSignal( context ), [ context ] ); + return { children }; + } + ); + + // wp-effect + directive( + 'effect', + ( { directives: { effect }, element, context: mainContext } ) => { + const context = useContext( mainContext ); + Object.values( effect ).forEach( ( callback ) => { + useSignalEffect( () => { + const cb = getCallback( callback ); + cb( { context, tick, ref: element.ref.current } ); + } ); + } ); + } + ); + + // wp-on:[event] + directive( + 'on', + ( { directives: { on }, element, context: mainContext } ) => { + const context = useContext( mainContext ); + Object.entries( on ).forEach( ( [ name, callback ] ) => { + element.props[ `on${ name }` ] = ( event ) => { + const cb = getCallback( callback ); + cb( { context, event } ); + }; + } ); + } + ); + + // wp-class:[classname] + directive( + 'class', + ( { + directives: { class: className }, + element, + context: mainContext, + } ) => { + const context = useContext( mainContext ); + Object.keys( className ) + .filter( ( n ) => n !== 'default' ) + .forEach( ( name ) => { + const cb = getCallback( className[ name ] ); + const result = cb( { context } ); + if ( ! result ) element.props.class.replace( name, '' ); + else if ( ! element.props.class.includes( name ) ) + element.props.class += ` ${ name }`; + } ); + } + ); + + // wp-bind:[attribute] + directive( + 'bind', + ( { directives: { bind }, element, context: mainContext } ) => { + const context = useContext( mainContext ); + Object.entries( bind ) + .filter( ( n ) => n !== 'default' ) + .forEach( ( [ attribute, callback ] ) => { + const cb = getCallback( callback ); + element.props[ attribute ] = cb( { context } ); + } ); + } + ); + + // The `wp-link` directive. + directive( + 'link', + ( { + directives: { + link: { default: link }, + }, + props: { href }, + element, + } ) => { + useEffect( () => { + // Prefetch the page if it is in the directive options. + if ( clientSideTransitions && link?.prefetch ) { + prefetch( href ); + } + } ); + + // Don't do anything if it's falsy. + if ( clientSideTransitions && link !== false ) { + element.props.onclick = async ( event ) => { + event.preventDefault(); + + // Fetch the page (or return it from cache). + await navigate( href ); + + // Update the scroll, depending on the option. True by default. + if ( link?.scroll === 'smooth' ) { + window.scrollTo( { + top: 0, + left: 0, + behavior: 'smooth', + } ); + } else if ( link?.scroll !== false ) { + window.scrollTo( 0, 0 ); + } + }; + } + } + ); +}; diff --git a/assets/js/base/interactivity/hooks.js b/assets/js/base/interactivity/hooks.js new file mode 100644 index 00000000000..0ae7381d571 --- /dev/null +++ b/assets/js/base/interactivity/hooks.js @@ -0,0 +1,63 @@ +/** + * External dependencies + */ +import { h, options, createContext } from 'preact'; +import { useRef } from 'preact/hooks'; + +// Main context +const context = createContext( {} ); + +// WordPress Directives. +const directives = {}; +export const directive = ( name, cb ) => { + directives[ name ] = cb; +}; + +// WordPress Components. +const components = {}; +export const component = ( name, Comp ) => { + components[ name ] = Comp; +}; + +// Directive wrapper. +const WpDirective = ( { type, wp, props: originalProps } ) => { + const ref = useRef( null ); + const element = h( type, { ...originalProps, ref, _wrapped: true } ); + const props = { ...originalProps, children: element }; + const directiveArgs = { directives: wp, props, element, context }; + + for ( const d in wp ) { + const wrapper = directives[ d ]?.( directiveArgs ); + if ( wrapper !== undefined ) props.children = wrapper; + } + + return props.children; +}; + +// Preact Options Hook called each time a vnode is created. +const old = options.vnode; +options.vnode = ( vnode ) => { + const type = vnode.type; + const wp = vnode.props.wp; + + if ( typeof type === 'string' && type.startsWith( 'wp-' ) ) { + vnode.props.children = h( + components[ type ], + { ...vnode.props, context }, + vnode.props.children + ); + } + + if ( wp ) { + const props = vnode.props; + delete props.wp; + if ( ! props._wrapped ) { + vnode.props = { type: vnode.type, wp, props }; + vnode.type = WpDirective; + } else { + delete props._wrapped; + } + } + + if ( old ) old( vnode ); +}; diff --git a/assets/js/base/interactivity/index.js b/assets/js/base/interactivity/index.js new file mode 100644 index 00000000000..59e112f2c1c --- /dev/null +++ b/assets/js/base/interactivity/index.js @@ -0,0 +1,18 @@ +/* eslint-disable no-console */ + +/** + * Internal dependencies + */ +import registerDirectives from './directives'; +import registerComponents from './components'; +import { init } from './router'; + +/** + * Initialize the initial vDOM. + */ +document.addEventListener( 'DOMContentLoaded', async () => { + registerDirectives(); + registerComponents(); + await init(); + console.log( 'hydrated!' ); +} ); diff --git a/assets/js/base/interactivity/router.js b/assets/js/base/interactivity/router.js new file mode 100644 index 00000000000..f7e40b6b987 --- /dev/null +++ b/assets/js/base/interactivity/router.js @@ -0,0 +1,124 @@ +/* eslint-disable no-undef */ + +/** + * External dependencies + */ +import { hydrate, render } from 'preact'; + +/** + * Internal dependencies + */ +import toVdom from './vdom'; +import { createRootFragment } from './utils'; + +// The root to render the vdom (document.body). +let rootFragment; + +// The cache of visited and prefetched pages and stylesheets. +const pages = new Map(); +const stylesheets = new Map(); + +// Helper to remove domain and hash from the URL. We are only interesting in +// caching the path and the query. +const cleanUrl = ( url ) => { + const u = new URL( url, 'http://a.bc' ); + return u.pathname + u.search; +}; + +// Helper to check if a page has client-side transitions activated. +export const hasClientSideTransitions = ( dom ) => + dom + .querySelector( "meta[itemprop='wp-client-side-transitions']" ) + ?.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 ); + } + ) + ); + const stylesFromSheets = sheets.map( ( sheet ) => { + const style = document.createElement( 'style' ); + style.textContent = sheet; + return style; + } ); + return [ + head.querySelector( 'title' ), + ...head.querySelectorAll( 'style' ), + ...stylesFromSheets, + ]; +}; + +// Fetch a new page and convert it to a static virtual DOM. +const fetchPage = async ( url ) => { + const html = await window.fetch( url ).then( ( r ) => r.text() ); + const dom = new window.DOMParser().parseFromString( html, 'text/html' ); + if ( ! hasClientSideTransitions( dom.head ) ) return false; + const head = await fetchHead( dom.head ); + return { head, body: toVdom( dom.body ) }; +}; + +// Prefetch a page. We store the promise to avoid triggering a second fetch for +// a page if a fetching has already started. +export const prefetch = ( url ) => { + url = cleanUrl( url ); + if ( ! pages.has( url ) ) { + pages.set( url, fetchPage( url ) ); + } +}; + +// Navigate to a new page. +export const navigate = async ( href ) => { + 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 ); + } else { + window.location.assign( href ); + } +}; + +// Listen to the back and forward buttons and restore the page if it's in the +// cache. +window.addEventListener( 'popstate', async () => { + const url = cleanUrl( window.location ); // Remove hash. + const page = pages.has( url ) && ( await pages.get( url ) ); + if ( page ) { + document.head.replaceChildren( ...page.head ); + render( page.body, rootFragment ); + } else { + window.location.reload(); + } +} ); + +// Initialize the router with the initial DOM. +export const init = async () => { + // Create the root fragment to hydrate everything. + rootFragment = createRootFragment( + document.documentElement, + document.body + ); + const body = toVdom( document.body ); + hydrate( body, rootFragment ); + + if ( hasClientSideTransitions( document.head ) ) { + const head = await fetchHead( document.head ); + pages.set( + cleanUrl( window.location ), + Promise.resolve( { body, head } ) + ); + } +}; diff --git a/assets/js/base/interactivity/utils.js b/assets/js/base/interactivity/utils.js new file mode 100644 index 00000000000..7f078703ccd --- /dev/null +++ b/assets/js/base/interactivity/utils.js @@ -0,0 +1,76 @@ +/* eslint-disable no-undef */ + +// For wrapperless hydration of document.body. +// See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c +export const createRootFragment = ( parent, replaceNode ) => { + replaceNode = [].concat( replaceNode ); + const s = replaceNode[ replaceNode.length - 1 ].nextSibling; + function insert( c, r ) { + parent.insertBefore( c, r || s ); + } + return ( parent.__k = { + nodeType: 1, + parentNode: parent, + firstChild: replaceNode[ 0 ], + childNodes: replaceNode, + insertBefore: insert, + appendChild: insert, + removeChild( c ) { + parent.removeChild( c ); + }, + } ); +}; + +// Helper function to await until the CPU is idle. +export const idle = () => + new Promise( ( resolve ) => window.requestIdleCallback( resolve ) ); + +export const knownSymbols = new Set( + Object.getOwnPropertyNames( Symbol ) + .map( ( key ) => Symbol[ key ] ) + .filter( ( value ) => typeof value === 'symbol' ) +); +const supported = new Set( [ + Object, + Array, + Int8Array, + Uint8Array, + Uint8ClampedArray, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, +] ); +export const shouldWrap = ( { constructor } ) => { + const isBuiltIn = + typeof constructor === 'function' && + constructor.name in globalThis && + globalThis[ constructor.name ] === constructor; + return ! isBuiltIn || supported.has( constructor ); +}; + +// Deep Merge +const isObject = ( item ) => + item && typeof item === 'object' && ! Array.isArray( item ); + +export const deepMerge = ( target, source ) => { + if ( isObject( target ) && isObject( source ) ) { + for ( const key in source ) { + if ( isObject( source[ key ] ) ) { + if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); + deepMerge( target[ key ], source[ key ] ); + } else { + Object.assign( target, { [ key ]: source[ key ] } ); + } + } + } +}; + +// Get callback. +export const getCallback = ( path ) => { + let current = window.wpx; + path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) ); + return current; +}; diff --git a/assets/js/base/interactivity/vdom.js b/assets/js/base/interactivity/vdom.js new file mode 100644 index 00000000000..b2fb8781887 --- /dev/null +++ b/assets/js/base/interactivity/vdom.js @@ -0,0 +1,53 @@ +/* eslint-disable no-undef */ + +/** + * External dependencies + */ +import { h } from 'preact'; + +// Recursive function that transfoms a DOM tree into vDOM. +export default function toVdom( node ) { + const props = {}; + const { attributes, childNodes } = node; + const wpDirectives = {}; + let hasWpDirectives = false; + + if ( node.nodeType === 3 ) return node.data; + if ( node.nodeType === 4 ) { + node.replaceWith( new Text( node.nodeValue ) ); + return node.nodeValue; + } + + for ( let i = 0; i < attributes.length; i++ ) { + const n = attributes[ i ].name; + if ( n[ 0 ] === 'w' && n[ 1 ] === 'p' && n[ 2 ] === '-' && n[ 3 ] ) { + hasWpDirectives = true; + let val = attributes[ i ].value; + try { + val = JSON.parse( val ); + } catch ( e ) {} + const [ , prefix, suffix ] = /wp-([^:]+):?(.*)$/.exec( n ); + wpDirectives[ prefix ] = wpDirectives[ prefix ] || {}; + wpDirectives[ prefix ][ suffix || 'default' ] = val; + } else if ( n === 'ref' ) { + continue; + } else { + props[ n ] = attributes[ i ].value; + } + } + + if ( hasWpDirectives ) props.wp = wpDirectives; + + 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 ) ); + } + } + + return h( node.localName, props, children ); +} diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js index 5cc83129c3b..9baf8442039 100644 --- a/bin/webpack-entries.js +++ b/bin/webpack-entries.js @@ -121,6 +121,7 @@ const entries = { wcSettings: './assets/js/settings/shared/index.ts', wcBlocksData: './assets/js/data/index.ts', wcBlocksMiddleware: './assets/js/middleware/index.js', + wcBlocksInteractivity: './assets/js/base/interactivity/index.js', wcBlocksSharedContext: './assets/js/shared/context/index.js', wcBlocksSharedHocs: './assets/js/shared/hocs/index.js', priceFormat: './packages/prices/index.js', diff --git a/bin/webpack-helpers.js b/bin/webpack-helpers.js index 81a05928760..faf9c4bbb3f 100644 --- a/bin/webpack-helpers.js +++ b/bin/webpack-helpers.js @@ -63,6 +63,10 @@ const getAlias = ( options = {} ) => { __dirname, `../assets/js/${ pathPart }base/hooks/` ), + '@woocommerce/base-interactivity': path.resolve( + __dirname, + `../assets/js/${ pathPart }base/interactivity/` + ), '@woocommerce/base-utils': path.resolve( __dirname, `../assets/js/${ pathPart }base/utils/` diff --git a/package-lock.json b/package-lock.json index 52922618ef4..e635e119960 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "GPL-3.0+", "dependencies": { + "@preact/signals": "^1.1.2", "@wordpress/autop": "3.16.0", "@wordpress/compose": "5.5.0", "@wordpress/deprecated": "3.16.0", @@ -27,7 +28,9 @@ "dinero.js": "1.9.1", "dompurify": "^2.4.0", "downshift": "6.1.7", + "hpq": "^1.3.0", "html-react-parser": "3.0.4", + "preact": "^10.11.3", "react-number-format": "4.9.3", "reakit": "1.3.11", "snakecase-keys": "5.4.2", @@ -4731,6 +4734,30 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@preact/signals": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.1.2.tgz", + "integrity": "sha512-MLNNrICSllHBhpXBvXbl7K5L1HmIjuTzgBw+zdODqjM/cLGPXdYiAWt4lqXlrxNavYdoU4eljb+TLE+DRL+6yw==", + "dependencies": { + "@preact/signals-core": "^1.2.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact": "10.x" + } + }, + "node_modules/@preact/signals-core": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.2.2.tgz", + "integrity": "sha512-z3/bCj7rRA21RJb4FeJ4guCrD1CQbaURHkCTunUWQpxUMAFOPXCD8tSFqERyGrrcSb4T3Hrmdc1OAl0LXBHwiw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/@react-native-community/cli": { "version": "9.1.3", "dev": true, @@ -40932,6 +40959,15 @@ "dev": true, "license": "ISC" }, + "node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -53217,6 +53253,19 @@ "@popperjs/core": { "version": "2.11.5" }, + "@preact/signals": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.1.2.tgz", + "integrity": "sha512-MLNNrICSllHBhpXBvXbl7K5L1HmIjuTzgBw+zdODqjM/cLGPXdYiAWt4lqXlrxNavYdoU4eljb+TLE+DRL+6yw==", + "requires": { + "@preact/signals-core": "^1.2.2" + } + }, + "@preact/signals-core": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.2.2.tgz", + "integrity": "sha512-z3/bCj7rRA21RJb4FeJ4guCrD1CQbaURHkCTunUWQpxUMAFOPXCD8tSFqERyGrrcSb4T3Hrmdc1OAl0LXBHwiw==" + }, "@react-native-community/cli": { "version": "9.1.3", "dev": true, @@ -78157,6 +78206,11 @@ "version": "4.2.0", "dev": true }, + "preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==" + }, "prelude-ls": { "version": "1.2.1", "dev": true diff --git a/package.json b/package.json index 223dd247ae6..a8e3272ef80 100644 --- a/package.json +++ b/package.json @@ -224,6 +224,7 @@ "npm": "^8.0.0" }, "dependencies": { + "@preact/signals": "^1.1.2", "@wordpress/autop": "3.16.0", "@wordpress/compose": "5.5.0", "@wordpress/deprecated": "3.16.0", @@ -241,7 +242,9 @@ "dinero.js": "1.9.1", "dompurify": "^2.4.0", "downshift": "6.1.7", + "hpq": "^1.3.0", "html-react-parser": "3.0.4", + "preact": "^10.11.3", "react-number-format": "4.9.3", "reakit": "1.3.11", "snakecase-keys": "5.4.2", diff --git a/tsconfig.base.json b/tsconfig.base.json index 74a49e9e57a..6091fd7252a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -39,6 +39,7 @@ ], "@woocommerce/base-hocs/*": [ "assets/js/base/hocs/*" ], "@woocommerce/base-hooks": [ "assets/js/base/hooks" ], + "@woocommerce/base-interactivity": [ "assets/js/base/interactivity" ], "@woocommerce/base-utils": [ "assets/js/base/utils" ], "@woocommerce/base-utils/*": [ "assets/js/base/utils/*" ], "@woocommerce/blocks/*": [ "assets/js/blocks/*" ], From 53dc29daf5a90f0f122325dcaeb331ecde1e1bd0 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 21 Dec 2022 17:07:15 +0100 Subject: [PATCH 02/54] Enqueue scripts if Products exists for testing --- woocommerce-gutenberg-products-block.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 4d734db933c..9d8767cdfa6 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -286,3 +286,15 @@ function woocommerce_blocks_plugin_outdated_notice() { } add_action( 'admin_notices', 'woocommerce_blocks_plugin_outdated_notice' ); + +function add_interactiviry_scripts( $block_content , $block , $instance){ + if ( + isset( $block['attrs']['namespace'] ) && + $block['attrs']['namespace'] === 'woocommerce/product-query' + ) { + wp_enqueue_script( 'wc-interactivity' , plugin_dir_url(__FILE__) . '/build/wc-blocks-interactivity.js' ); + } + return $block_content; +} + +add_filter( 'render_block_core/query', 'add_interactiviry_scripts', 10, 3 ); \ No newline at end of file From f316faa460f11075a88131551de046b15f954f83 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 22 Dec 2022 12:57:22 +0100 Subject: [PATCH 03/54] Test client-side transitions --- woocommerce-gutenberg-products-block.php | 33 ++++++++++++++++++------ 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 9d8767cdfa6..9f3492ee3bd 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -287,14 +287,31 @@ function woocommerce_blocks_plugin_outdated_notice() { add_action( 'admin_notices', 'woocommerce_blocks_plugin_outdated_notice' ); -function add_interactiviry_scripts( $block_content , $block , $instance){ - if ( - isset( $block['attrs']['namespace'] ) && - $block['attrs']['namespace'] === 'woocommerce/product-query' - ) { - wp_enqueue_script( 'wc-interactivity' , plugin_dir_url(__FILE__) . '/build/wc-blocks-interactivity.js' ); - } +function add_cst_meta_tag() +{ + echo ''; + add_filter( + 'client_side_transitions', + true + ); +} + +add_action('wp_head', 'add_cst_meta_tag'); + +function add_interactivity_scripts( ){ + wp_enqueue_script( 'wc-interactivity' , plugin_dir_url(__FILE__) . '/build/wc-blocks-interactivity.js' ); +} + +add_filter( 'wp_head', 'add_interactivity_scripts', 10, 3 ); + +function prefetch_navigation_links($block_content, $block, $instance) +{ + $block_content = str_replace( + ' Date: Fri, 23 Dec 2022 13:06:46 +0100 Subject: [PATCH 04/54] Remove script enqueue --- woocommerce-gutenberg-products-block.php | 31 +----------------------- 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 9f3492ee3bd..f6cb14076f0 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -285,33 +285,4 @@ function woocommerce_blocks_plugin_outdated_notice() { } } -add_action( 'admin_notices', 'woocommerce_blocks_plugin_outdated_notice' ); - -function add_cst_meta_tag() -{ - echo ''; - add_filter( - 'client_side_transitions', - true - ); -} - -add_action('wp_head', 'add_cst_meta_tag'); - -function add_interactivity_scripts( ){ - wp_enqueue_script( 'wc-interactivity' , plugin_dir_url(__FILE__) . '/build/wc-blocks-interactivity.js' ); -} - -add_filter( 'wp_head', 'add_interactivity_scripts', 10, 3 ); - -function prefetch_navigation_links($block_content, $block, $instance) -{ - $block_content = str_replace( - ' Date: Fri, 23 Dec 2022 13:16:18 +0100 Subject: [PATCH 05/54] Remove hpq dependency --- package-lock.json | 1 - package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index e635e119960..4fd95a6a12c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "dinero.js": "1.9.1", "dompurify": "^2.4.0", "downshift": "6.1.7", - "hpq": "^1.3.0", "html-react-parser": "3.0.4", "preact": "^10.11.3", "react-number-format": "4.9.3", diff --git a/package.json b/package.json index a8e3272ef80..319653cdf52 100644 --- a/package.json +++ b/package.json @@ -242,7 +242,6 @@ "dinero.js": "1.9.1", "dompurify": "^2.4.0", "downshift": "6.1.7", - "hpq": "^1.3.0", "html-react-parser": "3.0.4", "preact": "^10.11.3", "react-number-format": "4.9.3", From f9f8c6e01a47c27210c1f0fe2d0f8f92c9378725 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Fri, 23 Dec 2022 13:20:26 +0100 Subject: [PATCH 06/54] Update Interactivity scripts to latest version --- assets/js/base/interactivity/components.js | 13 +++++++++ assets/js/base/interactivity/deepsignal.js | 24 +++++++-------- assets/js/base/interactivity/hooks.js | 4 +-- assets/js/base/interactivity/router.js | 27 +++++++++++------ assets/js/base/interactivity/vdom.js | 34 ++++++++++++++++------ assets/js/base/interactivity/wpx.js | 13 +++++++++ 6 files changed, 80 insertions(+), 35 deletions(-) create mode 100644 assets/js/base/interactivity/wpx.js diff --git a/assets/js/base/interactivity/components.js b/assets/js/base/interactivity/components.js index aa248d2613e..c660be2d240 100644 --- a/assets/js/base/interactivity/components.js +++ b/assets/js/base/interactivity/components.js @@ -8,6 +8,7 @@ import { useMemo } from 'preact/hooks'; */ import { deepSignal } from './deepsignal'; import { component } from './hooks'; +import { getCallback } from './utils'; export default () => { const WpContext = ( { children, data, context: { Provider } } ) => { @@ -18,4 +19,16 @@ export default () => { return { children }; }; component( 'wp-context', WpContext ); + + const WpShow = ( { children, when } ) => { + const cb = getCallback( when ); + const value = + typeof cb === 'function' ? cb( { state: window.wpx.state } ) : cb; + if ( value ) { + return children; + } else { + return ; + } + }; + component( 'wp-show', WpShow ); }; diff --git a/assets/js/base/interactivity/deepsignal.js b/assets/js/base/interactivity/deepsignal.js index ceaa36af2ad..dddb02821ed 100644 --- a/assets/js/base/interactivity/deepsignal.js +++ b/assets/js/base/interactivity/deepsignal.js @@ -3,32 +3,28 @@ */ import { signal } from '@preact/signals'; -/** - * Internal dependencies - */ -import { knownSymbols, shouldWrap } from './utils'; - const proxyToSignals = new WeakMap(); const objToProxy = new WeakMap(); -const returnSignal = /^\$/; export const deepSignal = ( obj ) => new Proxy( obj, handlers ); +export const options = { returnSignal: /^\$/ }; const handlers = { get( target, prop, receiver ) { - if ( typeof prop === 'symbol' && knownSymbols.has( prop ) ) - return Reflect.get( target, prop, receiver ); - const shouldReturnSignal = returnSignal.test( prop ); - const key = shouldReturnSignal - ? prop.replace( returnSignal, '' ) + const returnSignal = options.returnSignal.test( prop ); + const key = returnSignal + ? prop.replace( options.returnSignal, '' ) : prop; if ( ! proxyToSignals.has( receiver ) ) proxyToSignals.set( receiver, new Map() ); const signals = proxyToSignals.get( receiver ); if ( ! signals.has( key ) ) { let val = Reflect.get( target, key, receiver ); - if ( typeof val === 'object' && val !== null && shouldWrap( val ) ) - val = new Proxy( val, handlers ); + if ( typeof val === 'object' && val !== null ) { + if ( ! objToProxy.has( val ) ) + objToProxy.set( val, new Proxy( val, handlers ) ); + val = objToProxy.get( val ); + } signals.set( key, signal( val ) ); } return returnSignal ? signals.get( key ) : signals.get( key ).value; @@ -36,7 +32,7 @@ const handlers = { set( target, prop, val, receiver ) { let internal = val; - if ( typeof val === 'object' && val !== null && shouldWrap( val ) ) { + if ( typeof val === 'object' && val !== null ) { if ( ! objToProxy.has( val ) ) objToProxy.set( val, new Proxy( val, handlers ) ); internal = objToProxy.get( val ); diff --git a/assets/js/base/interactivity/hooks.js b/assets/js/base/interactivity/hooks.js index 0ae7381d571..1f767e03439 100644 --- a/assets/js/base/interactivity/hooks.js +++ b/assets/js/base/interactivity/hooks.js @@ -46,9 +46,7 @@ options.vnode = ( vnode ) => { { ...vnode.props, context }, vnode.props.children ); - } - - if ( wp ) { + } else if ( wp ) { const props = vnode.props; delete props.wp; if ( ! props._wrapped ) { diff --git a/assets/js/base/interactivity/router.js b/assets/js/base/interactivity/router.js index f7e40b6b987..36721d8aa63 100644 --- a/assets/js/base/interactivity/router.js +++ b/assets/js/base/interactivity/router.js @@ -8,7 +8,7 @@ import { hydrate, render } from 'preact'; /** * Internal dependencies */ -import toVdom from './vdom'; +import { toVdom, hydratedIslands } from './vdom'; import { createRootFragment } from './utils'; // The root to render the vdom (document.body). @@ -106,19 +106,28 @@ window.addEventListener( 'popstate', async () => { // Initialize the router with the initial DOM. export const init = async () => { - // Create the root fragment to hydrate everything. - rootFragment = createRootFragment( - document.documentElement, - document.body - ); - const body = toVdom( document.body ); - hydrate( body, rootFragment ); - if ( hasClientSideTransitions( document.head ) ) { + // Create the root fragment to hydrate everything. + rootFragment = createRootFragment( + document.documentElement, + document.body + ); + + const body = toVdom( document.body ); + hydrate( body, rootFragment ); + const head = await fetchHead( document.head ); pages.set( cleanUrl( window.location ), Promise.resolve( { body, head } ) ); + } else { + document.querySelectorAll( '[wp-island]' ).forEach( ( node ) => { + if ( ! hydratedIslands.has( node ) ) { + const fragment = createRootFragment( node.parentNode, node ); + const vdom = toVdom( node ); + hydrate( vdom, fragment ); + } + } ); } }; diff --git a/assets/js/base/interactivity/vdom.js b/assets/js/base/interactivity/vdom.js index b2fb8781887..dd50662d0bd 100644 --- a/assets/js/base/interactivity/vdom.js +++ b/assets/js/base/interactivity/vdom.js @@ -5,12 +5,16 @@ */ import { h } from 'preact'; +export const hydratedIslands = new WeakSet(); + // Recursive function that transfoms a DOM tree into vDOM. -export default function toVdom( node ) { +export function toVdom( node ) { const props = {}; const { attributes, childNodes } = node; const wpDirectives = {}; let hasWpDirectives = false; + let ignore = false; + let island = false; if ( node.nodeType === 3 ) return node.data; if ( node.nodeType === 4 ) { @@ -21,14 +25,20 @@ export default function toVdom( node ) { for ( let i = 0; i < attributes.length; i++ ) { const n = attributes[ i ].name; if ( n[ 0 ] === 'w' && n[ 1 ] === 'p' && n[ 2 ] === '-' && n[ 3 ] ) { - hasWpDirectives = true; - let val = attributes[ i ].value; - try { - val = JSON.parse( val ); - } catch ( e ) {} - const [ , prefix, suffix ] = /wp-([^:]+):?(.*)$/.exec( n ); - wpDirectives[ prefix ] = wpDirectives[ prefix ] || {}; - wpDirectives[ prefix ][ suffix || 'default' ] = val; + if ( n === 'wp-ignore' ) { + ignore = true; + } else if ( n === 'wp-island' ) { + island = true; + } else { + hasWpDirectives = true; + let val = attributes[ i ].value; + try { + val = JSON.parse( val ); + } catch ( e ) {} + const [ , prefix, suffix ] = /wp-([^:]+):?(.*)$/.exec( n ); + wpDirectives[ prefix ] = wpDirectives[ prefix ] || {}; + wpDirectives[ prefix ][ suffix || 'default' ] = val; + } } else if ( n === 'ref' ) { continue; } else { @@ -36,6 +46,12 @@ export default function toVdom( node ) { } } + if ( ignore && ! island ) + return h( node.localName, { + dangerouslySetInnerHTML: { __html: node.innerHTML }, + } ); + if ( island ) hydratedIslands.add( node ); + if ( hasWpDirectives ) props.wp = wpDirectives; const children = []; diff --git a/assets/js/base/interactivity/wpx.js b/assets/js/base/interactivity/wpx.js new file mode 100644 index 00000000000..3494816e2e6 --- /dev/null +++ b/assets/js/base/interactivity/wpx.js @@ -0,0 +1,13 @@ +/** + * Internal dependencies + */ +import { deepSignal } from './deepsignal'; +import { deepMerge } from './utils'; + +const rawState = {}; +window.wpx = { state: deepSignal( rawState ) }; + +export default ( { state, ...block } ) => { + deepMerge( window.wpx, block ); + deepMerge( rawState, state ); +}; From 8c467ec110031e55f815ace4ca7fc14b8bc22a0f Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 12 Jan 2023 11:22:35 +0100 Subject: [PATCH 07/54] Remove interactivity scripts from core entries --- bin/webpack-entries.js | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js index b42c36a2227..40e30c2cd12 100644 --- a/bin/webpack-entries.js +++ b/bin/webpack-entries.js @@ -122,7 +122,6 @@ const entries = { wcSettings: './assets/js/settings/shared/index.ts', wcBlocksData: './assets/js/data/index.ts', wcBlocksMiddleware: './assets/js/middleware/index.js', - wcBlocksInteractivity: './assets/js/base/interactivity/index.js', wcBlocksSharedContext: './assets/js/shared/context/index.js', wcBlocksSharedHocs: './assets/js/shared/hocs/index.js', priceFormat: './packages/prices/index.js', From 6c9f92bfad720b0afbc052f7a3ad424c9f320e47 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 12 Jan 2023 11:25:42 +0100 Subject: [PATCH 08/54] Create webpack config for interactivity api A plugin for optional chaining is required as the repo uses Webpack 4 for now. --- bin/webpack-configs.js | 73 +++++++++++++++++++++++++++++++++++++++ package-lock.json | 77 +++++++++++++++++++++++++++++------------- package.json | 1 + webpack.config.js | 10 ++++++ 4 files changed, 138 insertions(+), 23 deletions(-) diff --git a/bin/webpack-configs.js b/bin/webpack-configs.js index 88452e34f11..392e1191301 100644 --- a/bin/webpack-configs.js +++ b/bin/webpack-configs.js @@ -775,6 +775,78 @@ const getStylingConfig = ( options = {} ) => { }; }; +const getInteractivityAPIConfig = ( options = {} ) => { + const { alias, resolvePlugins = [] } = options; + return { + entry: { + runtime: './assets/js/base/interactivity', + }, + output: { + filename: 'wp-directives-[name].js', + path: path.resolve( __dirname, '../build/' ), + }, + resolve: { + alias, + plugins: resolvePlugins, + extensions: [ '.js', '.ts', '.tsx' ], + }, + plugins: [ + ...getSharedPlugins( { + bundleAnalyzerReportTitle: 'WP directives', + } ), + new ProgressBarPlugin( + getProgressBarPluginConfig( 'WP directives' ) + ), + ], + optimization: { + runtimeChunk: { + name: 'vendors', + }, + splitChunks: { + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + minSize: 0, + chunks: 'all', + }, + }, + }, + }, + module: { + rules: [ + { + test: /\.(j|t)sx?$/, + exclude: /node_modules/, + use: [ + { + loader: require.resolve( 'babel-loader' ), + options: { + cacheDirectory: + process.env.BABEL_CACHE_DIRECTORY || true, + babelrc: false, + configFile: false, + presets: [ + [ + '@babel/preset-react', + { + runtime: 'automatic', + importSource: 'preact', + }, + ], + ], + plugins: [ + '@babel/plugin-proposal-optional-chaining', + ], + }, + }, + ], + }, + ], + }, + }; +}; + module.exports = { getCoreConfig, getFrontConfig, @@ -782,4 +854,5 @@ module.exports = { getPaymentsConfig, getExtensionsConfig, getStylingConfig, + getInteractivityAPIConfig, }; diff --git a/package-lock.json b/package-lock.json index 184e092724b..c08a66db2d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@babel/cli": "7.18.9", "@babel/core": "7.18.9", "@babel/plugin-proposal-class-properties": "7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.20.7", "@babel/plugin-syntax-jsx": "7.18.6", "@babel/polyfill": "7.12.1", "@babel/preset-typescript": "7.18.6", @@ -556,8 +557,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.18.9", - "license": "MIT", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", "engines": { "node": ">=6.9.0" } @@ -601,11 +603,12 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.16.0", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.16.0" + "@babel/types": "^7.20.0" }, "engines": { "node": ">=6.9.0" @@ -621,9 +624,18 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.18.6", - "license": "MIT", + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", "engines": { "node": ">=6.9.0" } @@ -974,12 +986,13 @@ } }, "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.16.7", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz", + "integrity": "sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { @@ -2183,10 +2196,12 @@ } }, "node_modules/@babel/types": { - "version": "7.18.9", - "license": "MIT", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", + "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" }, "engines": { @@ -50424,7 +50439,9 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.18.9" + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==" }, "@babel/helper-remap-async-to-generator": { "version": "7.16.8", @@ -50453,10 +50470,12 @@ } }, "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.16.0", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", "dev": true, "requires": { - "@babel/types": "^7.16.0" + "@babel/types": "^7.20.0" } }, "@babel/helper-split-export-declaration": { @@ -50465,8 +50484,15 @@ "@babel/types": "^7.18.6" } }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + }, "@babel/helper-validator-identifier": { - "version": "7.18.6" + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" }, "@babel/helper-validator-option": { "version": "7.18.6" @@ -50666,11 +50692,13 @@ } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.16.7", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz", + "integrity": "sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/plugin-syntax-optional-chaining": "^7.8.3" } }, @@ -51385,9 +51413,12 @@ } }, "@babel/types": { - "version": "7.18.9", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", + "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", "requires": { - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" } }, diff --git a/package.json b/package.json index 66f6de8502c..9cb47803a37 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@babel/cli": "7.18.9", "@babel/core": "7.18.9", "@babel/plugin-proposal-class-properties": "7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.20.7", "@babel/plugin-syntax-jsx": "7.18.6", "@babel/polyfill": "7.12.1", "@babel/preset-typescript": "7.18.6", diff --git a/webpack.config.js b/webpack.config.js index 4f48de6403d..de1edb08bad 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ const { getPaymentsConfig, getExtensionsConfig, getStylingConfig, + getInteractivityAPIConfig, } = require( './bin/webpack-configs.js' ); // Only options shared between all configs should be defined here. @@ -76,6 +77,14 @@ const StylingConfig = { ...getStylingConfig( { alias: getAlias() } ), }; +/** + * Config to generate the Interactivity API runtime. + */ +const InteractivityConfig = { + ...sharedConfig, + ...getInteractivityAPIConfig( { alias: getAlias() } ), +}; + module.exports = [ CoreConfig, MainConfig, @@ -83,4 +92,5 @@ module.exports = [ ExtensionsConfig, PaymentsConfig, StylingConfig, + InteractivityConfig, ]; From 419aff631adb9299efc75b72cbcf30389cad129e Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 12 Jan 2023 11:26:37 +0100 Subject: [PATCH 09/54] Enqueue the directives runtime --- woocommerce-gutenberg-products-block.php | 28 +++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 017a610a699..519d031621e 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -285,4 +285,30 @@ function woocommerce_blocks_plugin_outdated_notice() { } } -add_action( 'admin_notices', 'woocommerce_blocks_plugin_outdated_notice' ); \ No newline at end of file +add_action( 'admin_notices', 'woocommerce_blocks_plugin_outdated_notice' ); + +/** + * Register the scripts. This code is basically the same we use in + * WordPress/block-hydration-experiments for the directives plugin. + * + * TODO: add a filter to control when these scripts should be enqueued. + */ +function wp_directives_register_scripts() { + wp_register_script( + 'wp-directives-vendors', + plugins_url( 'build/wp-directives-vendors.js', __FILE__ ), + array(), + '1.0.0', + true + ); + wp_register_script( + 'wp-directives-runtime', + plugins_url( 'build/wp-directives-runtime.js', __FILE__ ), + array( 'wp-directives-vendors' ), + '1.0.0', + true + ); + + wp_enqueue_script( 'wp-directives-runtime' ); +} +add_action( 'wp_enqueue_scripts', 'wp_directives_register_scripts' ); From 2a215b0b31605d635377f1b4fed46ea56a8f7a91 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 12 Jan 2023 11:41:00 +0100 Subject: [PATCH 10/54] Updated wp directives code --- assets/js/base/interactivity/components.js | 16 ++-- assets/js/base/interactivity/deepsignal.js | 47 ----------- assets/js/base/interactivity/directives.js | 96 +++++++++++++--------- assets/js/base/interactivity/hooks.js | 33 +++++++- assets/js/base/interactivity/index.js | 2 - assets/js/base/interactivity/router.js | 2 - assets/js/base/interactivity/utils.js | 56 ------------- assets/js/base/interactivity/vdom.js | 2 - assets/js/base/interactivity/wpx.js | 29 +++++-- package-lock.json | 22 +++++ package.json | 1 + 11 files changed, 139 insertions(+), 167 deletions(-) delete mode 100644 assets/js/base/interactivity/deepsignal.js diff --git a/assets/js/base/interactivity/components.js b/assets/js/base/interactivity/components.js index c660be2d240..e047f9654c5 100644 --- a/assets/js/base/interactivity/components.js +++ b/assets/js/base/interactivity/components.js @@ -1,14 +1,13 @@ /** * External dependencies */ -import { useMemo } from 'preact/hooks'; +import { useMemo, useContext } from 'preact/hooks'; +import { deepSignal } from 'deepsignal'; /** * Internal dependencies */ -import { deepSignal } from './deepsignal'; import { component } from './hooks'; -import { getCallback } from './utils'; export default () => { const WpContext = ( { children, data, context: { Provider } } ) => { @@ -20,15 +19,12 @@ export default () => { }; component( 'wp-context', WpContext ); - const WpShow = ( { children, when } ) => { - const cb = getCallback( when ); - const value = - typeof cb === 'function' ? cb( { state: window.wpx.state } ) : cb; - if ( value ) { + const WpShow = ( { children, when, evaluate, context } ) => { + const contextValue = useContext( context ); + if ( evaluate( when, { context: contextValue } ) ) { return children; - } else { - return ; } + return ; }; component( 'wp-show', WpShow ); }; diff --git a/assets/js/base/interactivity/deepsignal.js b/assets/js/base/interactivity/deepsignal.js deleted file mode 100644 index dddb02821ed..00000000000 --- a/assets/js/base/interactivity/deepsignal.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * External dependencies - */ -import { signal } from '@preact/signals'; - -const proxyToSignals = new WeakMap(); -const objToProxy = new WeakMap(); - -export const deepSignal = ( obj ) => new Proxy( obj, handlers ); -export const options = { returnSignal: /^\$/ }; - -const handlers = { - get( target, prop, receiver ) { - const returnSignal = options.returnSignal.test( prop ); - const key = returnSignal - ? prop.replace( options.returnSignal, '' ) - : prop; - if ( ! proxyToSignals.has( receiver ) ) - proxyToSignals.set( receiver, new Map() ); - const signals = proxyToSignals.get( receiver ); - if ( ! signals.has( key ) ) { - let val = Reflect.get( target, key, receiver ); - if ( typeof val === 'object' && val !== null ) { - if ( ! objToProxy.has( val ) ) - objToProxy.set( val, new Proxy( val, handlers ) ); - val = objToProxy.get( val ); - } - signals.set( key, signal( val ) ); - } - return returnSignal ? signals.get( key ) : signals.get( key ).value; - }, - - set( target, prop, val, receiver ) { - let internal = val; - if ( typeof val === 'object' && val !== null ) { - if ( ! objToProxy.has( val ) ) - objToProxy.set( val, new Proxy( val, handlers ) ); - internal = objToProxy.get( val ); - } - if ( ! proxyToSignals.has( receiver ) ) - proxyToSignals.set( receiver, new Map() ); - const signals = proxyToSignals.get( receiver ); - if ( ! signals.has( prop ) ) signals.set( prop, signal( internal ) ); - else signals.get( prop ).value = internal; - return Reflect.set( target, prop, val, receiver ); - }, -}; diff --git a/assets/js/base/interactivity/directives.js b/assets/js/base/interactivity/directives.js index 7af929514d4..d758e38181e 100644 --- a/assets/js/base/interactivity/directives.js +++ b/assets/js/base/interactivity/directives.js @@ -1,19 +1,20 @@ /** * External dependencies */ + import { useContext, useMemo, useEffect } from 'preact/hooks'; import { useSignalEffect } from '@preact/signals'; +import { deepSignal } from 'deepsignal'; /** * Internal dependencies */ import { directive } from './hooks'; -import { deepSignal } from './deepsignal'; import { prefetch, navigate, hasClientSideTransitions } from './router'; -import { getCallback } from './utils'; +// Until useSignalEffects is fixed: +// https://github.com/preactjs/signals/issues/228 const raf = window.requestAnimationFrame; -// Until useSignalEffects is fixed: https://github.com/preactjs/signals/issues/228 const tick = () => new Promise( ( r ) => raf( () => raf( r ) ) ); // Check if current page has client-side transitions enabled. @@ -35,33 +36,25 @@ export default () => { } ); - // wp-effect - directive( - 'effect', - ( { directives: { effect }, element, context: mainContext } ) => { - const context = useContext( mainContext ); - Object.values( effect ).forEach( ( callback ) => { - useSignalEffect( () => { - const cb = getCallback( callback ); - cb( { context, tick, ref: element.ref.current } ); - } ); + // wp-effect:[name] + directive( 'effect', ( { directives: { effect }, context, evaluate } ) => { + const contextValue = useContext( context ); + Object.values( effect ).forEach( ( path ) => { + useSignalEffect( () => { + evaluate( path, { context: contextValue } ); } ); - } - ); + } ); + } ); // wp-on:[event] - directive( - 'on', - ( { directives: { on }, element, context: mainContext } ) => { - const context = useContext( mainContext ); - Object.entries( on ).forEach( ( [ name, callback ] ) => { - element.props[ `on${ name }` ] = ( event ) => { - const cb = getCallback( callback ); - cb( { context, event } ); - }; - } ); - } - ); + directive( 'on', ( { directives: { on }, element, evaluate, context } ) => { + const contextValue = useContext( context ); + Object.entries( on ).forEach( ( [ name, path ] ) => { + element.props[ `on${ name }` ] = ( event ) => { + evaluate( path, { event, context: contextValue } ); + }; + } ); + } ); // wp-class:[classname] directive( @@ -69,17 +62,41 @@ export default () => { ( { directives: { class: className }, element, - context: mainContext, + evaluate, + context, } ) => { - const context = useContext( mainContext ); + const contextValue = useContext( context ); Object.keys( className ) .filter( ( n ) => n !== 'default' ) .forEach( ( name ) => { - const cb = getCallback( className[ name ] ); - const result = cb( { context } ); - if ( ! result ) element.props.class.replace( name, '' ); - else if ( ! element.props.class.includes( name ) ) + const result = evaluate( className[ name ], { + className: name, + context: contextValue, + } ); + if ( ! result ) + element.props.class = element.props.class + .replace( + new RegExp( `(^|\\s)${ name }(\\s|$)`, 'g' ), + ' ' + ) + .trim(); + else if ( + ! new RegExp( `(^|\\s)${ name }(\\s|$)` ).test( + element.props.class + ) + ) element.props.class += ` ${ name }`; + + useEffect( () => { + // This seems necessary because Preact doesn't change the class names + // on the hydration, so we have to do it manually. It doesn't need + // deps because it only needs to do it the first time. + if ( ! result ) { + element.ref.current.classList.remove( name ); + } else { + element.ref.current.classList.add( name ); + } + }, [] ); } ); } ); @@ -87,18 +104,19 @@ export default () => { // wp-bind:[attribute] directive( 'bind', - ( { directives: { bind }, element, context: mainContext } ) => { - const context = useContext( mainContext ); + ( { directives: { bind }, element, context, evaluate } ) => { + const contextValue = useContext( context ); Object.entries( bind ) .filter( ( n ) => n !== 'default' ) - .forEach( ( [ attribute, callback ] ) => { - const cb = getCallback( callback ); - element.props[ attribute ] = cb( { context } ); + .forEach( ( [ attribute, path ] ) => { + element.props[ attribute ] = evaluate( path, { + context: contextValue, + } ); } ); } ); - // The `wp-link` directive. + // wp-link directive( 'link', ( { diff --git a/assets/js/base/interactivity/hooks.js b/assets/js/base/interactivity/hooks.js index 1f767e03439..0ee06c62914 100644 --- a/assets/js/base/interactivity/hooks.js +++ b/assets/js/base/interactivity/hooks.js @@ -4,7 +4,12 @@ import { h, options, createContext } from 'preact'; import { useRef } from 'preact/hooks'; -// Main context +/** + * Internal dependencies + */ +import { store } from './wpx'; + +// Main context. const context = createContext( {} ); // WordPress Directives. @@ -19,12 +24,34 @@ export const component = ( name, Comp ) => { components[ name ] = Comp; }; +// Resolve the path to some property of the wpx object. +const resolve = ( path, context ) => { + let current = { ...store, context }; + path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) ); + return current; +}; + +// Generate the evaluate function. +const getEvaluate = + ( { ref } = {} ) => + ( path, extraArgs = {} ) => { + const value = resolve( path, extraArgs.context ); + return typeof value === 'function' + ? value( { + state: store.state, + ...( ref !== undefined ? { ref } : {} ), + ...extraArgs, + } ) + : value; + }; + // Directive wrapper. const WpDirective = ( { type, wp, props: originalProps } ) => { const ref = useRef( null ); const element = h( type, { ...originalProps, ref, _wrapped: true } ); const props = { ...originalProps, children: element }; - const directiveArgs = { directives: wp, props, element, context }; + const evaluate = getEvaluate( { ref: ref.current } ); + const directiveArgs = { directives: wp, props, element, context, evaluate }; for ( const d in wp ) { const wrapper = directives[ d ]?.( directiveArgs ); @@ -43,7 +70,7 @@ options.vnode = ( vnode ) => { if ( typeof type === 'string' && type.startsWith( 'wp-' ) ) { vnode.props.children = h( components[ type ], - { ...vnode.props, context }, + { ...vnode.props, context, evaluate: getEvaluate() }, vnode.props.children ); } else if ( wp ) { diff --git a/assets/js/base/interactivity/index.js b/assets/js/base/interactivity/index.js index 59e112f2c1c..b993b43da9f 100644 --- a/assets/js/base/interactivity/index.js +++ b/assets/js/base/interactivity/index.js @@ -1,5 +1,3 @@ -/* eslint-disable no-console */ - /** * Internal dependencies */ diff --git a/assets/js/base/interactivity/router.js b/assets/js/base/interactivity/router.js index 36721d8aa63..b17a62804af 100644 --- a/assets/js/base/interactivity/router.js +++ b/assets/js/base/interactivity/router.js @@ -1,5 +1,3 @@ -/* eslint-disable no-undef */ - /** * External dependencies */ diff --git a/assets/js/base/interactivity/utils.js b/assets/js/base/interactivity/utils.js index 7f078703ccd..04ebe313630 100644 --- a/assets/js/base/interactivity/utils.js +++ b/assets/js/base/interactivity/utils.js @@ -1,5 +1,3 @@ -/* eslint-disable no-undef */ - // For wrapperless hydration of document.body. // See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c export const createRootFragment = ( parent, replaceNode ) => { @@ -20,57 +18,3 @@ export const createRootFragment = ( parent, replaceNode ) => { }, } ); }; - -// Helper function to await until the CPU is idle. -export const idle = () => - new Promise( ( resolve ) => window.requestIdleCallback( resolve ) ); - -export const knownSymbols = new Set( - Object.getOwnPropertyNames( Symbol ) - .map( ( key ) => Symbol[ key ] ) - .filter( ( value ) => typeof value === 'symbol' ) -); -const supported = new Set( [ - Object, - Array, - Int8Array, - Uint8Array, - Uint8ClampedArray, - Int16Array, - Uint16Array, - Int32Array, - Uint32Array, - Float32Array, - Float64Array, -] ); -export const shouldWrap = ( { constructor } ) => { - const isBuiltIn = - typeof constructor === 'function' && - constructor.name in globalThis && - globalThis[ constructor.name ] === constructor; - return ! isBuiltIn || supported.has( constructor ); -}; - -// Deep Merge -const isObject = ( item ) => - item && typeof item === 'object' && ! Array.isArray( item ); - -export const deepMerge = ( target, source ) => { - if ( isObject( target ) && isObject( source ) ) { - for ( const key in source ) { - if ( isObject( source[ key ] ) ) { - if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); - deepMerge( target[ key ], source[ key ] ); - } else { - Object.assign( target, { [ key ]: source[ key ] } ); - } - } - } -}; - -// Get callback. -export const getCallback = ( path ) => { - let current = window.wpx; - path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) ); - return current; -}; diff --git a/assets/js/base/interactivity/vdom.js b/assets/js/base/interactivity/vdom.js index dd50662d0bd..f41044897cf 100644 --- a/assets/js/base/interactivity/vdom.js +++ b/assets/js/base/interactivity/vdom.js @@ -1,5 +1,3 @@ -/* eslint-disable no-undef */ - /** * External dependencies */ diff --git a/assets/js/base/interactivity/wpx.js b/assets/js/base/interactivity/wpx.js index 3494816e2e6..df2b7001a9a 100644 --- a/assets/js/base/interactivity/wpx.js +++ b/assets/js/base/interactivity/wpx.js @@ -1,13 +1,30 @@ /** - * Internal dependencies + * External dependencies */ -import { deepSignal } from './deepsignal'; -import { deepMerge } from './utils'; +import { deepSignal } from 'deepsignal'; + +const isObject = ( item ) => + item && typeof item === 'object' && ! Array.isArray( item ); + +export const deepMerge = ( target, source ) => { + if ( isObject( target ) && isObject( source ) ) { + for ( const key in source ) { + if ( isObject( source[ key ] ) ) { + if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); + deepMerge( target[ key ], source[ key ] ); + } else { + Object.assign( target, { [ key ]: source[ key ] } ); + } + } + } +}; const rawState = {}; -window.wpx = { state: deepSignal( rawState ) }; +export const store = { state: deepSignal( rawState ) }; + +if ( typeof window !== 'undefined' ) window.wpx = store; -export default ( { state, ...block } ) => { - deepMerge( window.wpx, block ); +export const wpx = ( { state, ...block } ) => { + deepMerge( store, block ); deepMerge( rawState, state ); }; diff --git a/package-lock.json b/package-lock.json index c08a66db2d9..8ffc3ba5704 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "compare-versions": "4.1.3", "config": "3.3.7", "dataloader": "2.1.0", + "deepsignal": "^1.1.0", "dinero.js": "1.9.1", "dompurify": "^2.4.0", "downshift": "6.1.7", @@ -23566,6 +23567,18 @@ "node": ">=0.10.0" } }, + "node_modules/deepsignal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deepsignal/-/deepsignal-1.1.1.tgz", + "integrity": "sha512-ZcRi5KogA7hQiJcnUDGrDno7OCwfyBdcP96yf4BHF3JsUc8SRt3w24RZX6L0Ejijnol8Jt5cV5X8h0dotixHHA==", + "dependencies": { + "@preact/signals": "^1.0.0", + "@preact/signals-core": "^1.0.0" + }, + "peerDependencies": { + "preact": "10.x" + } + }, "node_modules/default-browser-id": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-1.0.4.tgz", @@ -66360,6 +66373,15 @@ "version": "4.2.2", "dev": true }, + "deepsignal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deepsignal/-/deepsignal-1.1.1.tgz", + "integrity": "sha512-ZcRi5KogA7hQiJcnUDGrDno7OCwfyBdcP96yf4BHF3JsUc8SRt3w24RZX6L0Ejijnol8Jt5cV5X8h0dotixHHA==", + "requires": { + "@preact/signals": "^1.0.0", + "@preact/signals-core": "^1.0.0" + } + }, "default-browser-id": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-1.0.4.tgz", diff --git a/package.json b/package.json index 9cb47803a37..c35cc8ba713 100644 --- a/package.json +++ b/package.json @@ -240,6 +240,7 @@ "compare-versions": "4.1.3", "config": "3.3.7", "dataloader": "2.1.0", + "deepsignal": "^1.1.0", "dinero.js": "1.9.1", "dompurify": "^2.4.0", "downshift": "6.1.7", From e6bafe50e663565ca363acbfef57b9939a4eca58 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Thu, 12 Jan 2023 12:39:48 +0100 Subject: [PATCH 11/54] Use a filter to enque the directives runtime --- woocommerce-gutenberg-products-block.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 519d031621e..e2d22105e4e 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -289,11 +289,16 @@ function woocommerce_blocks_plugin_outdated_notice() { /** * Register the scripts. This code is basically the same we use in - * WordPress/block-hydration-experiments for the directives plugin. - * - * TODO: add a filter to control when these scripts should be enqueued. + * WordPress/block-hydration-experiments for the directives plugin with a filter + * to control if the runtime scripts should be enqueued. */ function wp_directives_register_scripts() { + // Enqueuing the directives runtime is `false` by default. + $should_enqueue = apply_filters( '__experimental_woocommerce_blocks_enqueue_directives_runtime', false ); + if ( ! $should_enqueue ) { + return; + } + wp_register_script( 'wp-directives-vendors', plugins_url( 'build/wp-directives-vendors.js', __FILE__ ), From 7c6cbee372c65e430590d98c1819b6b4b0a97d93 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 13 Jan 2023 16:28:19 +0100 Subject: [PATCH 12/54] Remove base-interactivity alias for now --- bin/webpack-helpers.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bin/webpack-helpers.js b/bin/webpack-helpers.js index f9321c8621b..02fd4669238 100644 --- a/bin/webpack-helpers.js +++ b/bin/webpack-helpers.js @@ -59,10 +59,6 @@ const getAlias = ( options = {} ) => { __dirname, `../assets/js/${ pathPart }base/hooks/` ), - '@woocommerce/base-interactivity': path.resolve( - __dirname, - `../assets/js/${ pathPart }base/interactivity/` - ), '@woocommerce/base-utils': path.resolve( __dirname, `../assets/js/${ pathPart }base/utils/` From 655b12513e15dd9e1388be9e70ed51b8b2e2bcde Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 13 Jan 2023 17:05:53 +0100 Subject: [PATCH 13/54] Add path for modules inside base-interactivity --- tsconfig.base.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.base.json b/tsconfig.base.json index 6091fd7252a..6905d90ac6d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -40,6 +40,7 @@ "@woocommerce/base-hocs/*": [ "assets/js/base/hocs/*" ], "@woocommerce/base-hooks": [ "assets/js/base/hooks" ], "@woocommerce/base-interactivity": [ "assets/js/base/interactivity" ], + "@woocommerce/base-interactivity/*": [ "assets/js/base/interactivity/*" ], "@woocommerce/base-utils": [ "assets/js/base/utils" ], "@woocommerce/base-utils/*": [ "assets/js/base/utils/*" ], "@woocommerce/blocks/*": [ "assets/js/blocks/*" ], From 20bae4c5db1976e4063f7ef14d1d78141a6c5d34 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 16 Jan 2023 18:57:46 +0100 Subject: [PATCH 14/54] Create Simple Price Filter block Co-authored-by: Luis Herranz --- .../js/blocks/simple-price-filter/block.json | 11 ++++ assets/js/blocks/simple-price-filter/index.js | 9 ++++ assets/js/blocks/simple-price-filter/view.js | 52 +++++++++++++++++++ bin/webpack-configs.js | 2 + bin/webpack-entries.js | 1 + bin/webpack-helpers.js | 4 ++ src/BlockTypes/SimplePriceFilter.php | 38 ++++++++++++++ src/BlockTypesController.php | 1 + 8 files changed, 118 insertions(+) create mode 100644 assets/js/blocks/simple-price-filter/block.json create mode 100644 assets/js/blocks/simple-price-filter/index.js create mode 100644 assets/js/blocks/simple-price-filter/view.js 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..48d173205ee --- /dev/null +++ b/assets/js/blocks/simple-price-filter/block.json @@ -0,0 +1,11 @@ +{ + "$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 +} 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/view.js b/assets/js/blocks/simple-price-filter/view.js new file mode 100644 index 00000000000..5cb46d1e7a4 --- /dev/null +++ b/assets/js/blocks/simple-price-filter/view.js @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import { wpx } from '@woocommerce/base-interactivity/wpx'; +import { navigate } from '@woocommerce/base-interactivity/router'; + +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 < Infinity ) { + searchParams.set( 'max_price', maxPrice ); + } else { + searchParams.delete( 'max_price' ); + } + + return url.href; +}; + +const initialSearchParams = new URL( window.location.href ).searchParams; +const initialMinPrice = initialSearchParams.get( 'min_price' ) || ''; +const initialMaxPrice = initialSearchParams.get( 'max_price' ) || ''; + +wpx( { + state: { + filters: { + minPrice: parseFloat( initialMinPrice ) || 0, + maxPrice: parseFloat( initialMaxPrice ) || Infinity, + }, + }, + actions: { + filters: { + setMinPrice: ( { state, event } ) => { + const value = parseFloat( event.target.value ) || 0; + state.filters.minPrice = value; + navigate( getHrefWithFilters( { state } ) ); + }, + setMaxPrice: ( { state, event } ) => { + const value = parseFloat( event.target.value ) || Infinity; + state.filters.maxPrice = value; + navigate( getHrefWithFilters( { state } ) ); + }, + }, + }, +} ); diff --git a/bin/webpack-configs.js b/bin/webpack-configs.js index 392e1191301..6ee1cb9e095 100644 --- a/bin/webpack-configs.js +++ b/bin/webpack-configs.js @@ -780,6 +780,8 @@ const getInteractivityAPIConfig = ( options = {} ) => { return { entry: { runtime: './assets/js/base/interactivity', + 'simple-price-filter': + './assets/js/blocks/simple-price-filter/view.js', }, output: { filename: 'wp-directives-[name].js', diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js index 40e30c2cd12..0bbf1a41388 100644 --- a/bin/webpack-entries.js +++ b/bin/webpack-entries.js @@ -41,6 +41,7 @@ const blocks = { customDir: 'products/all-products', }, 'price-filter': {}, + 'simple-price-filter': {}, 'attribute-filter': {}, 'stock-filter': {}, 'active-filters': {}, diff --git a/bin/webpack-helpers.js b/bin/webpack-helpers.js index 02fd4669238..ea52b6f38e0 100644 --- a/bin/webpack-helpers.js +++ b/bin/webpack-helpers.js @@ -63,6 +63,10 @@ const getAlias = ( options = {} ) => { __dirname, `../assets/js/${ pathPart }base/utils/` ), + '@woocommerce/base-interactivity': path.resolve( + __dirname, + `../assets/js/${ pathPart }base/interactivity/` + ), '@woocommerce/blocks': path.resolve( __dirname, `../assets/js/${ pathPart }/blocks` diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php new file mode 100644 index 00000000000..d15478b6149 --- /dev/null +++ b/src/BlockTypes/SimplePriceFilter.php @@ -0,0 +1,38 @@ + + + + "; + } + + /** + * Block type script definition. + */ + public function get_block_type_script( $key = null ) { + $script = [ + 'handle' => 'simple-price-filter-view', + 'path' => 'build/wp-directives-simple-price-filter.js', + 'dependencies' => [ 'wp-directives-runtime' ], + ]; + return $key ? $script[ $key ] : $script; + } +} diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php index 615f5894823..818386b2d38 100644 --- a/src/BlockTypesController.php +++ b/src/BlockTypesController.php @@ -181,6 +181,7 @@ protected function get_block_types() { 'ProductTag', 'AllProducts', 'PriceFilter', + 'SimplePriceFilter', 'AttributeFilter', 'StockFilter', 'RatingFilter', From 3717d0fa8f21783c2077e3fc525523b9dbeda9ac Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 17 Jan 2023 12:17:08 +0100 Subject: [PATCH 15/54] Add ssr for min and max price --- src/BlockTypes/SimplePriceFilter.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php index d15478b6149..2394dd4691d 100644 --- a/src/BlockTypes/SimplePriceFilter.php +++ b/src/BlockTypes/SimplePriceFilter.php @@ -12,15 +12,20 @@ class SimplePriceFilter extends AbstractBlock { * @var string */ protected $block_name = 'simple-price-filter'; + const MIN_PRICE_QUERY_VAR = 'min_price'; + const MAX_PRICE_QUERY_VAR = 'max_price'; /** * Render function. */ public function render( $attributes = [], $content = '', $block = null ) { $wrapper_attributes = get_block_wrapper_attributes(); + $min_price = get_query_var( self::MIN_PRICE_QUERY_VAR ); + $max_price = get_query_var( self::MAX_PRICE_QUERY_VAR ); return "
- - + + +
"; } From 6b691329be1b1b1bceeb653c6fce54e9378315f6 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 17 Jan 2023 12:17:24 +0100 Subject: [PATCH 16/54] Add logic to reset button --- assets/js/blocks/simple-price-filter/view.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets/js/blocks/simple-price-filter/view.js b/assets/js/blocks/simple-price-filter/view.js index 5cb46d1e7a4..814594d296d 100644 --- a/assets/js/blocks/simple-price-filter/view.js +++ b/assets/js/blocks/simple-price-filter/view.js @@ -47,6 +47,11 @@ wpx( { state.filters.maxPrice = value; navigate( getHrefWithFilters( { state } ) ); }, + reset: ( { state } ) => { + state.filters.minPrice = 0; + state.filters.maxPrice = Infinity; + navigate( getHrefWithFilters( { state } ) ); + }, }, }, } ); From 748a1e9a1f12166ca014bfc4472cf5167d916e30 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 17 Jan 2023 12:19:02 +0100 Subject: [PATCH 17/54] Enable directives and client transitions --- woocommerce-gutenberg-products-block.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index e2d22105e4e..4b6b7a4c26b 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -317,3 +317,17 @@ function wp_directives_register_scripts() { wp_enqueue_script( 'wp-directives-runtime' ); } add_action( 'wp_enqueue_scripts', 'wp_directives_register_scripts' ); + +// Enqueue the WP directives runtime. +add_filter( '__experimental_woocommerce_blocks_enqueue_directives_runtime', function () { return true; } ); + +// Insert the required meta tag for client-side transitions. +function add_cst_meta_tag() { + echo ''; + add_filter( + 'client_side_transitions', + true + ); +} + +add_action('wp_head', 'add_cst_meta_tag'); From b9519c60bbedad5b6e8d133041eabcb1311eaead Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 18 Jan 2023 12:43:37 +0100 Subject: [PATCH 18/54] Add a functional range bar --- .../js/blocks/simple-price-filter/style.scss | 41 +++++++++++++ assets/js/blocks/simple-price-filter/view.js | 32 +++++++++- src/BlockTypes/SimplePriceFilter.php | 61 +++++++++++++++++-- 3 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 assets/js/blocks/simple-price-filter/style.scss 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..762875b9679 --- /dev/null +++ b/assets/js/blocks/simple-price-filter/style.scss @@ -0,0 +1,41 @@ +.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: .2; + } + } + + input[type="range"] { + position: absolute; + top: 50%; + left: 0; + width: 100%; + height: 0; + margin: 0; + padding: 0; + + &.active { + z-index: 10; + } + } + } +} diff --git a/assets/js/blocks/simple-price-filter/view.js b/assets/js/blocks/simple-price-filter/view.js index 814594d296d..3648cb2b0cd 100644 --- a/assets/js/blocks/simple-price-filter/view.js +++ b/assets/js/blocks/simple-price-filter/view.js @@ -33,6 +33,20 @@ wpx( { filters: { minPrice: parseFloat( initialMinPrice ) || 0, maxPrice: parseFloat( initialMaxPrice ) || Infinity, + maxRange: 90, // TODO: get this value from SSR. + isMinActive: true, + isMaxActive: false, + }, + }, + derived: { + filters: { + rangeStyle: ( { state } ) => { + const { minPrice, maxPrice, maxRange } = state.filters; + return { + '--low': `${ ( 100 * minPrice ) / maxRange }%`, + '--high': `${ ( 100 * maxPrice ) / maxRange }%`, + }; + }, }, }, actions: { @@ -40,11 +54,13 @@ wpx( { setMinPrice: ( { state, event } ) => { const value = parseFloat( event.target.value ) || 0; state.filters.minPrice = value; - navigate( getHrefWithFilters( { state } ) ); }, setMaxPrice: ( { state, event } ) => { - const value = parseFloat( event.target.value ) || Infinity; + const value = + parseFloat( event.target.value ) || state.filters.maxRange; state.filters.maxPrice = value; + }, + updateProducts: ( { state } ) => { navigate( getHrefWithFilters( { state } ) ); }, reset: ( { state } ) => { @@ -52,6 +68,18 @@ wpx( { state.filters.maxPrice = Infinity; 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/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php index 2394dd4691d..87daeca82ed 100644 --- a/src/BlockTypes/SimplePriceFilter.php +++ b/src/BlockTypes/SimplePriceFilter.php @@ -22,11 +22,64 @@ public function render( $attributes = [], $content = '', $block = null ) { $wrapper_attributes = get_block_wrapper_attributes(); $min_price = get_query_var( self::MIN_PRICE_QUERY_VAR ); $max_price = get_query_var( self::MAX_PRICE_QUERY_VAR ); - return "
- - + $max_range = 90; // TODO: get this value from DB. + + // CSS variables for the range bar style. + $__low = 100 * $min_price / $max_range; + $__high = 100 * $max_price / $max_range; + $range_style = "--low: $__low%; --high: $__high%"; + + return " +
+
+
+ + +
+
+ + +
-
"; +
+ "; } /** From 1d135d4eb270c0da9a98295ecd1133a5d9780f58 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 18 Jan 2023 12:47:32 +0100 Subject: [PATCH 19/54] Revert "Remove base-interactivity alias for now" This reverts commit 7c6cbee372c65e430590d98c1819b6b4b0a97d93. --- bin/webpack-helpers.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bin/webpack-helpers.js b/bin/webpack-helpers.js index 02fd4669238..f9321c8621b 100644 --- a/bin/webpack-helpers.js +++ b/bin/webpack-helpers.js @@ -59,6 +59,10 @@ const getAlias = ( options = {} ) => { __dirname, `../assets/js/${ pathPart }base/hooks/` ), + '@woocommerce/base-interactivity': path.resolve( + __dirname, + `../assets/js/${ pathPart }base/interactivity/` + ), '@woocommerce/base-utils': path.resolve( __dirname, `../assets/js/${ pathPart }base/utils/` From 2688b495f491390044329f83db8acc6ce73d252c Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 18 Jan 2023 13:21:55 +0100 Subject: [PATCH 20/54] Fix issues with range values --- assets/js/blocks/simple-price-filter/view.js | 17 +++++++++++------ src/BlockTypes/SimplePriceFilter.php | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/assets/js/blocks/simple-price-filter/view.js b/assets/js/blocks/simple-price-filter/view.js index 3648cb2b0cd..f1ce856babd 100644 --- a/assets/js/blocks/simple-price-filter/view.js +++ b/assets/js/blocks/simple-price-filter/view.js @@ -25,15 +25,20 @@ const getHrefWithFilters = ( { state } ) => { }; const initialSearchParams = new URL( window.location.href ).searchParams; -const initialMinPrice = initialSearchParams.get( 'min_price' ) || ''; -const initialMaxPrice = initialSearchParams.get( 'max_price' ) || ''; + +// TODO: get this values from SSR +const ssrMaxRange = 90; +const ssrMinPrice = + parseFloat( initialSearchParams.get( 'min_price' ) || '' ) || 0; +const ssrMaxPrice = + parseFloat( initialSearchParams.get( 'max_price' ) || '' ) || ssrMaxRange; wpx( { state: { filters: { - minPrice: parseFloat( initialMinPrice ) || 0, - maxPrice: parseFloat( initialMaxPrice ) || Infinity, - maxRange: 90, // TODO: get this value from SSR. + minPrice: ssrMinPrice, + maxPrice: ssrMaxPrice, + maxRange: ssrMaxRange, isMinActive: true, isMaxActive: false, }, @@ -65,7 +70,7 @@ wpx( { }, reset: ( { state } ) => { state.filters.minPrice = 0; - state.filters.maxPrice = Infinity; + state.filters.maxPrice = state.filters.maxRange; navigate( getHrefWithFilters( { state } ) ); }, updateActiveHandle: ( { state, event } ) => { diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php index 87daeca82ed..03c59618d7b 100644 --- a/src/BlockTypes/SimplePriceFilter.php +++ b/src/BlockTypes/SimplePriceFilter.php @@ -20,9 +20,9 @@ class SimplePriceFilter extends AbstractBlock { */ public function render( $attributes = [], $content = '', $block = null ) { $wrapper_attributes = get_block_wrapper_attributes(); - $min_price = get_query_var( self::MIN_PRICE_QUERY_VAR ); - $max_price = get_query_var( self::MAX_PRICE_QUERY_VAR ); $max_range = 90; // TODO: get this value 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 ); // CSS variables for the range bar style. $__low = 100 * $min_price / $max_range; From e95b46a4aec592e9daa01595d4d51b267e3600a1 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 18 Jan 2023 13:23:20 +0100 Subject: [PATCH 21/54] Slightly improve appearance --- .../js/blocks/simple-price-filter/style.scss | 26 +++++++++++++++++++ src/BlockTypes/SimplePriceFilter.php | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/assets/js/blocks/simple-price-filter/style.scss b/assets/js/blocks/simple-price-filter/style.scss index 762875b9679..b6b7ce007c4 100644 --- a/assets/js/blocks/simple-price-filter/style.scss +++ b/assets/js/blocks/simple-price-filter/style.scss @@ -37,5 +37,31 @@ 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: .875em; + border-width: 1px; + border-style: solid; + border-color: currentColor; + border-radius: 4px; + } } } diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php index 03c59618d7b..4c2f93fdae2 100644 --- a/src/BlockTypes/SimplePriceFilter.php +++ b/src/BlockTypes/SimplePriceFilter.php @@ -31,6 +31,7 @@ public function render( $attributes = [], $content = '', $block = null ) { return "
+

Filter by price

-
+
Date: Wed, 18 Jan 2023 13:39:37 +0100 Subject: [PATCH 22/54] Replace Infinity with maxRange --- assets/js/blocks/simple-price-filter/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/blocks/simple-price-filter/view.js b/assets/js/blocks/simple-price-filter/view.js index f1ce856babd..4a5e2cb9681 100644 --- a/assets/js/blocks/simple-price-filter/view.js +++ b/assets/js/blocks/simple-price-filter/view.js @@ -15,7 +15,7 @@ const getHrefWithFilters = ( { state } ) => { searchParams.delete( 'min_price' ); } - if ( maxPrice < Infinity ) { + if ( maxPrice < state.filters.maxRange ) { searchParams.set( 'max_price', maxPrice ); } else { searchParams.delete( 'max_price' ); From 18c74364e28abd47ff37c2eb23a1d882267dc85f Mon Sep 17 00:00:00 2001 From: David Arenas Date: Wed, 18 Jan 2023 15:59:19 +0100 Subject: [PATCH 23/54] Remove unnecessary filter and enqueue --- woocommerce-gutenberg-products-block.php | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index e2d22105e4e..3bab415ae9a 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -288,17 +288,10 @@ function woocommerce_blocks_plugin_outdated_notice() { add_action( 'admin_notices', 'woocommerce_blocks_plugin_outdated_notice' ); /** - * Register the scripts. This code is basically the same we use in - * WordPress/block-hydration-experiments for the directives plugin with a filter - * to control if the runtime scripts should be enqueued. + * Register the Interactivity API scripts. These files are enqueued when a block + * defines `wp-directives-runtime` as a dependency. */ function wp_directives_register_scripts() { - // Enqueuing the directives runtime is `false` by default. - $should_enqueue = apply_filters( '__experimental_woocommerce_blocks_enqueue_directives_runtime', false ); - if ( ! $should_enqueue ) { - return; - } - wp_register_script( 'wp-directives-vendors', plugins_url( 'build/wp-directives-vendors.js', __FILE__ ), @@ -313,7 +306,5 @@ function wp_directives_register_scripts() { '1.0.0', true ); - - wp_enqueue_script( 'wp-directives-runtime' ); } add_action( 'wp_enqueue_scripts', 'wp_directives_register_scripts' ); From e397551a693385b216c4949cba75e6b35f13455f Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 20 Jan 2023 13:24:15 +0100 Subject: [PATCH 24/54] Update router code --- assets/js/base/interactivity/router.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/base/interactivity/router.js b/assets/js/base/interactivity/router.js index b17a62804af..a43e2a65605 100644 --- a/assets/js/base/interactivity/router.js +++ b/assets/js/base/interactivity/router.js @@ -19,7 +19,7 @@ const stylesheets = new Map(); // Helper to remove domain and hash from the URL. We are only interesting in // caching the path and the query. const cleanUrl = ( url ) => { - const u = new URL( url, 'http://a.bc' ); + const u = new URL( url, window.location ); return u.pathname + u.search; }; From 58231927e902f9ab80278e5277bdc443611d3c9a Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 20 Jan 2023 13:32:00 +0100 Subject: [PATCH 25/54] Update Interactivity location and alias --- assets/js/{base => }/interactivity/components.js | 0 assets/js/{base => }/interactivity/directives.js | 0 assets/js/{base => }/interactivity/hooks.js | 0 assets/js/{base => }/interactivity/index.js | 0 assets/js/{base => }/interactivity/router.js | 0 assets/js/{base => }/interactivity/utils.js | 0 assets/js/{base => }/interactivity/vdom.js | 0 assets/js/{base => }/interactivity/wpx.js | 0 bin/webpack-configs.js | 2 +- bin/webpack-helpers.js | 4 ++-- tsconfig.base.json | 4 ++-- 11 files changed, 5 insertions(+), 5 deletions(-) rename assets/js/{base => }/interactivity/components.js (100%) rename assets/js/{base => }/interactivity/directives.js (100%) rename assets/js/{base => }/interactivity/hooks.js (100%) rename assets/js/{base => }/interactivity/index.js (100%) rename assets/js/{base => }/interactivity/router.js (100%) rename assets/js/{base => }/interactivity/utils.js (100%) rename assets/js/{base => }/interactivity/vdom.js (100%) rename assets/js/{base => }/interactivity/wpx.js (100%) diff --git a/assets/js/base/interactivity/components.js b/assets/js/interactivity/components.js similarity index 100% rename from assets/js/base/interactivity/components.js rename to assets/js/interactivity/components.js diff --git a/assets/js/base/interactivity/directives.js b/assets/js/interactivity/directives.js similarity index 100% rename from assets/js/base/interactivity/directives.js rename to assets/js/interactivity/directives.js diff --git a/assets/js/base/interactivity/hooks.js b/assets/js/interactivity/hooks.js similarity index 100% rename from assets/js/base/interactivity/hooks.js rename to assets/js/interactivity/hooks.js diff --git a/assets/js/base/interactivity/index.js b/assets/js/interactivity/index.js similarity index 100% rename from assets/js/base/interactivity/index.js rename to assets/js/interactivity/index.js diff --git a/assets/js/base/interactivity/router.js b/assets/js/interactivity/router.js similarity index 100% rename from assets/js/base/interactivity/router.js rename to assets/js/interactivity/router.js diff --git a/assets/js/base/interactivity/utils.js b/assets/js/interactivity/utils.js similarity index 100% rename from assets/js/base/interactivity/utils.js rename to assets/js/interactivity/utils.js diff --git a/assets/js/base/interactivity/vdom.js b/assets/js/interactivity/vdom.js similarity index 100% rename from assets/js/base/interactivity/vdom.js rename to assets/js/interactivity/vdom.js diff --git a/assets/js/base/interactivity/wpx.js b/assets/js/interactivity/wpx.js similarity index 100% rename from assets/js/base/interactivity/wpx.js rename to assets/js/interactivity/wpx.js diff --git a/bin/webpack-configs.js b/bin/webpack-configs.js index 392e1191301..850e1a8e0c8 100644 --- a/bin/webpack-configs.js +++ b/bin/webpack-configs.js @@ -779,7 +779,7 @@ const getInteractivityAPIConfig = ( options = {} ) => { const { alias, resolvePlugins = [] } = options; return { entry: { - runtime: './assets/js/base/interactivity', + runtime: './assets/js/interactivity', }, output: { filename: 'wp-directives-[name].js', diff --git a/bin/webpack-helpers.js b/bin/webpack-helpers.js index f9321c8621b..6678a150ea3 100644 --- a/bin/webpack-helpers.js +++ b/bin/webpack-helpers.js @@ -59,9 +59,9 @@ const getAlias = ( options = {} ) => { __dirname, `../assets/js/${ pathPart }base/hooks/` ), - '@woocommerce/base-interactivity': path.resolve( + '@woocommerce/interactivity': path.resolve( __dirname, - `../assets/js/${ pathPart }base/interactivity/` + `../assets/js/${ pathPart }interactivity/` ), '@woocommerce/base-utils': path.resolve( __dirname, diff --git a/tsconfig.base.json b/tsconfig.base.json index 6905d90ac6d..51f44814c19 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -39,8 +39,8 @@ ], "@woocommerce/base-hocs/*": [ "assets/js/base/hocs/*" ], "@woocommerce/base-hooks": [ "assets/js/base/hooks" ], - "@woocommerce/base-interactivity": [ "assets/js/base/interactivity" ], - "@woocommerce/base-interactivity/*": [ "assets/js/base/interactivity/*" ], + "@woocommerce/interactivity": [ "assets/js/interactivity" ], + "@woocommerce/interactivity/*": [ "assets/js/interactivity/*" ], "@woocommerce/base-utils": [ "assets/js/base/utils" ], "@woocommerce/base-utils/*": [ "assets/js/base/utils/*" ], "@woocommerce/blocks/*": [ "assets/js/blocks/*" ], From 035ba3ce350825d349353bf2ef33f03c35a3a1a2 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 20 Jan 2023 13:38:02 +0100 Subject: [PATCH 26/54] Update imports in simple price filter --- assets/js/blocks/simple-price-filter/view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/blocks/simple-price-filter/view.js b/assets/js/blocks/simple-price-filter/view.js index 4a5e2cb9681..dd87deca3ba 100644 --- a/assets/js/blocks/simple-price-filter/view.js +++ b/assets/js/blocks/simple-price-filter/view.js @@ -1,8 +1,8 @@ /** * External dependencies */ -import { wpx } from '@woocommerce/base-interactivity/wpx'; -import { navigate } from '@woocommerce/base-interactivity/router'; +import { wpx } from '@woocommerce/interactivity/wpx'; +import { navigate } from '@woocommerce/interactivity/router'; const getHrefWithFilters = ( { state } ) => { const { minPrice, maxPrice } = state.filters; From 5067e7a8bfc18f0893b96c4fed541c20c9d1ff04 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 27 Jan 2023 17:21:58 +0100 Subject: [PATCH 27/54] Update Interactivity API --- assets/js/interactivity/components.js | 20 +++++-------- assets/js/interactivity/constants.js | 3 ++ assets/js/interactivity/directives.js | 42 +++++++++++++++++++-------- assets/js/interactivity/hooks.js | 41 ++++++++++++-------------- assets/js/interactivity/index.js | 3 -- assets/js/interactivity/router.js | 29 +++++++++--------- assets/js/interactivity/vdom.js | 28 +++++++++--------- assets/js/interactivity/wpx.js | 3 -- 8 files changed, 89 insertions(+), 80 deletions(-) create mode 100644 assets/js/interactivity/constants.js diff --git a/assets/js/interactivity/components.js b/assets/js/interactivity/components.js index e047f9654c5..7b234c818d9 100644 --- a/assets/js/interactivity/components.js +++ b/assets/js/interactivity/components.js @@ -1,30 +1,26 @@ -/** - * External dependencies - */ import { useMemo, useContext } from 'preact/hooks'; import { deepSignal } from 'deepsignal'; - -/** - * Internal dependencies - */ import { component } from './hooks'; export default () => { - const WpContext = ( { children, data, context: { Provider } } ) => { + // + const Context = ( { children, data, context: { Provider } } ) => { const signals = useMemo( () => deepSignal( JSON.parse( data ) ), [ data ] ); return { children }; }; - component( 'wp-context', WpContext ); + component( 'context', Context ); - const WpShow = ( { children, when, evaluate, context } ) => { + // + const Show = ( { children, when, evaluate, context } ) => { const contextValue = useContext( context ); if ( evaluate( when, { context: contextValue } ) ) { return children; + } else { + return ; } - return ; }; - component( 'wp-show', WpShow ); + component( 'show', Show ); }; diff --git a/assets/js/interactivity/constants.js b/assets/js/interactivity/constants.js new file mode 100644 index 00000000000..b01a50c65a8 --- /dev/null +++ b/assets/js/interactivity/constants.js @@ -0,0 +1,3 @@ +export const cstMetaTagItemprop = 'wp-client-side-transitions'; +export const componentPrefix = 'wp-'; +export const directivePrefix = 'wp-'; diff --git a/assets/js/interactivity/directives.js b/assets/js/interactivity/directives.js index d758e38181e..4eb4269da44 100644 --- a/assets/js/interactivity/directives.js +++ b/assets/js/interactivity/directives.js @@ -1,14 +1,6 @@ -/** - * External dependencies - */ - import { useContext, useMemo, useEffect } from 'preact/hooks'; import { useSignalEffect } from '@preact/signals'; -import { deepSignal } from 'deepsignal'; - -/** - * Internal dependencies - */ +import { deepSignal, peek } from 'deepsignal'; import { directive } from './hooks'; import { prefetch, navigate, hasClientSideTransitions } from './router'; @@ -20,6 +12,25 @@ const tick = () => new Promise( ( r ) => raf( () => raf( r ) ) ); // Check if current page has client-side transitions enabled. const clientSideTransitions = hasClientSideTransitions( document.head ); +const isObject = ( item ) => + item && typeof item === 'object' && ! Array.isArray( item ); + +const mergeDeepSignals = ( target, source ) => { + for ( const k in source ) { + if ( typeof peek( target, k ) === 'undefined' ) { + target[ `$${ k }` ] = source[ `$${ k }` ]; + } else if ( + isObject( peek( target, k ) ) && + isObject( peek( source, k ) ) + ) { + mergeDeepSignals( + target[ `$${ k }` ].peek(), + source[ `$${ k }` ].peek() + ); + } + } +}; + export default () => { // wp-context directive( @@ -29,10 +40,17 @@ export default () => { context: { default: context }, }, props: { children }, - context: { Provider }, + context: inherited, } ) => { - const signals = useMemo( () => deepSignal( context ), [ context ] ); - return { children }; + const { Provider } = inherited; + const inheritedValue = useContext( inherited ); + const value = useMemo( () => { + const localValue = deepSignal( context ); + mergeDeepSignals( localValue, inheritedValue ); + return localValue; + }, [ context, inheritedValue ] ); + + return { children }; } ); diff --git a/assets/js/interactivity/hooks.js b/assets/js/interactivity/hooks.js index 0ee06c62914..0f98c94ee98 100644 --- a/assets/js/interactivity/hooks.js +++ b/assets/js/interactivity/hooks.js @@ -1,27 +1,21 @@ -/** - * External dependencies - */ import { h, options, createContext } from 'preact'; import { useRef } from 'preact/hooks'; - -/** - * Internal dependencies - */ import { store } from './wpx'; +import { componentPrefix } from './constants'; // Main context. const context = createContext( {} ); // WordPress Directives. -const directives = {}; +const directiveMap = {}; export const directive = ( name, cb ) => { - directives[ name ] = cb; + directiveMap[ name ] = cb; }; // WordPress Components. -const components = {}; +const componentMap = {}; export const component = ( name, Comp ) => { - components[ name ] = Comp; + componentMap[ name ] = Comp; }; // Resolve the path to some property of the wpx object. @@ -46,15 +40,15 @@ const getEvaluate = }; // Directive wrapper. -const WpDirective = ( { type, wp, props: originalProps } ) => { +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 directiveArgs = { directives: wp, props, element, context, evaluate }; + const directiveArgs = { directives, props, element, context, evaluate }; - for ( const d in wp ) { - const wrapper = directives[ d ]?.( directiveArgs ); + for ( const d in directives ) { + const wrapper = directiveMap[ d ]?.( directiveArgs ); if ( wrapper !== undefined ) props.children = wrapper; } @@ -65,20 +59,23 @@ const WpDirective = ( { type, wp, props: originalProps } ) => { const old = options.vnode; options.vnode = ( vnode ) => { const type = vnode.type; - const wp = vnode.props.wp; + const { directives } = vnode.props; - if ( typeof type === 'string' && type.startsWith( 'wp-' ) ) { + if ( + typeof type === 'string' && + type.slice( 0, componentPrefix.length ) === componentPrefix + ) { vnode.props.children = h( - components[ type ], + componentMap[ type.slice( componentPrefix.length ) ], { ...vnode.props, context, evaluate: getEvaluate() }, vnode.props.children ); - } else if ( wp ) { + } else if ( directives ) { const props = vnode.props; - delete props.wp; + delete props.directives; if ( ! props._wrapped ) { - vnode.props = { type: vnode.type, wp, props }; - vnode.type = WpDirective; + vnode.props = { type: vnode.type, directives, props }; + vnode.type = Directive; } else { delete props._wrapped; } diff --git a/assets/js/interactivity/index.js b/assets/js/interactivity/index.js index b993b43da9f..853aa26a30a 100644 --- a/assets/js/interactivity/index.js +++ b/assets/js/interactivity/index.js @@ -1,6 +1,3 @@ -/** - * Internal dependencies - */ import registerDirectives from './directives'; import registerComponents from './components'; import { init } from './router'; diff --git a/assets/js/interactivity/router.js b/assets/js/interactivity/router.js index a43e2a65605..39b85b8a06e 100644 --- a/assets/js/interactivity/router.js +++ b/assets/js/interactivity/router.js @@ -1,13 +1,7 @@ -/** - * External dependencies - */ import { hydrate, render } from 'preact'; - -/** - * Internal dependencies - */ import { toVdom, hydratedIslands } from './vdom'; import { createRootFragment } from './utils'; +import { cstMetaTagItemprop, directivePrefix } from './constants'; // The root to render the vdom (document.body). let rootFragment; @@ -26,7 +20,7 @@ const cleanUrl = ( url ) => { // Helper to check if a page has client-side transitions activated. export const hasClientSideTransitions = ( dom ) => dom - .querySelector( "meta[itemprop='wp-client-side-transitions']" ) + .querySelector( `meta[itemprop='${ cstMetaTagItemprop }']` ) ?.getAttribute( 'content' ) === 'active'; // Fetch styles of a new page. @@ -120,12 +114,17 @@ export const init = async () => { Promise.resolve( { body, head } ) ); } else { - document.querySelectorAll( '[wp-island]' ).forEach( ( node ) => { - if ( ! hydratedIslands.has( node ) ) { - const fragment = createRootFragment( node.parentNode, node ); - const vdom = toVdom( node ); - hydrate( vdom, fragment ); - } - } ); + document + .querySelectorAll( `[${ directivePrefix }island]` ) + .forEach( ( node ) => { + if ( ! hydratedIslands.has( node ) ) { + const fragment = createRootFragment( + node.parentNode, + node + ); + const vdom = toVdom( node ); + hydrate( vdom, fragment ); + } + } ); } }; diff --git a/assets/js/interactivity/vdom.js b/assets/js/interactivity/vdom.js index f41044897cf..48c335ca342 100644 --- a/assets/js/interactivity/vdom.js +++ b/assets/js/interactivity/vdom.js @@ -1,7 +1,9 @@ -/** - * External dependencies - */ import { h } from 'preact'; +import { directivePrefix as p } from './constants'; + +const ignoreAttr = `${ p }ignore`; +const islandAttr = `${ p }island`; +const directiveParser = new RegExp( `${ p }([^:]+):?(.*)$` ); export const hydratedIslands = new WeakSet(); @@ -9,8 +11,8 @@ export const hydratedIslands = new WeakSet(); export function toVdom( node ) { const props = {}; const { attributes, childNodes } = node; - const wpDirectives = {}; - let hasWpDirectives = false; + const directives = {}; + let hasDirectives = false; let ignore = false; let island = false; @@ -22,20 +24,20 @@ export function toVdom( node ) { for ( let i = 0; i < attributes.length; i++ ) { const n = attributes[ i ].name; - if ( n[ 0 ] === 'w' && n[ 1 ] === 'p' && n[ 2 ] === '-' && n[ 3 ] ) { - if ( n === 'wp-ignore' ) { + if ( n[ p.length ] && n.slice( 0, p.length ) === p ) { + if ( n === ignoreAttr ) { ignore = true; - } else if ( n === 'wp-island' ) { + } else if ( n === islandAttr ) { island = true; } else { - hasWpDirectives = true; + hasDirectives = true; let val = attributes[ i ].value; try { val = JSON.parse( val ); } catch ( e ) {} - const [ , prefix, suffix ] = /wp-([^:]+):?(.*)$/.exec( n ); - wpDirectives[ prefix ] = wpDirectives[ prefix ] || {}; - wpDirectives[ prefix ][ suffix || 'default' ] = val; + const [ , prefix, suffix ] = directiveParser.exec( n ); + directives[ prefix ] = directives[ prefix ] || {}; + directives[ prefix ][ suffix || 'default' ] = val; } } else if ( n === 'ref' ) { continue; @@ -50,7 +52,7 @@ export function toVdom( node ) { } ); if ( island ) hydratedIslands.add( node ); - if ( hasWpDirectives ) props.wp = wpDirectives; + if ( hasDirectives ) props.directives = directives; const children = []; for ( let i = 0; i < childNodes.length; i++ ) { diff --git a/assets/js/interactivity/wpx.js b/assets/js/interactivity/wpx.js index df2b7001a9a..7a49e171809 100644 --- a/assets/js/interactivity/wpx.js +++ b/assets/js/interactivity/wpx.js @@ -1,6 +1,3 @@ -/** - * External dependencies - */ import { deepSignal } from 'deepsignal'; const isObject = ( item ) => From 92d0d9df2993f7e9c23cf500094aea3169d6ceff Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 27 Jan 2023 17:22:51 +0100 Subject: [PATCH 28/54] Change `wp` prefixes to `woo` --- assets/js/interactivity/constants.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/js/interactivity/constants.js b/assets/js/interactivity/constants.js index b01a50c65a8..5780fe9657e 100644 --- a/assets/js/interactivity/constants.js +++ b/assets/js/interactivity/constants.js @@ -1,3 +1,3 @@ -export const cstMetaTagItemprop = 'wp-client-side-transitions'; -export const componentPrefix = 'wp-'; -export const directivePrefix = 'wp-'; +export const cstMetaTagItemprop = 'woo-client-side-transitions'; +export const componentPrefix = 'woo-'; +export const directivePrefix = 'data-woo-'; From 0199adef77e8ea8545730b10a84b96616f80807f Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 27 Jan 2023 17:48:41 +0100 Subject: [PATCH 29/54] Use `woo` prefix for the directives runtime bundle --- bin/webpack-configs.js | 2 +- woocommerce-gutenberg-products-block.php | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/webpack-configs.js b/bin/webpack-configs.js index 48e02675033..1cae71dab42 100644 --- a/bin/webpack-configs.js +++ b/bin/webpack-configs.js @@ -788,7 +788,7 @@ const getInteractivityAPIConfig = ( options = {} ) => { runtime: './assets/js/interactivity', }, output: { - filename: 'wp-directives-[name].js', + filename: 'woo-directives-[name].js', path: path.resolve( __dirname, '../build/' ), }, resolve: { diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 6d4093bde73..f600e7698fe 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -289,22 +289,22 @@ function woocommerce_blocks_plugin_outdated_notice() { /** * Register the Interactivity API scripts. These files are enqueued when a block - * defines `wp-directives-runtime` as a dependency. + * defines `woo-directives-runtime` as a dependency. */ -function wp_directives_register_scripts() { +function woo_directives_register_scripts() { wp_register_script( - 'wp-directives-vendors', - plugins_url( 'build/wp-directives-vendors.js', __FILE__ ), + 'woo-directives-vendors', + plugins_url( 'build/woo-directives-vendors.js', __FILE__ ), array(), '1.0.0', true ); wp_register_script( - 'wp-directives-runtime', - plugins_url( 'build/wp-directives-runtime.js', __FILE__ ), - array( 'wp-directives-vendors' ), + 'woo-directives-runtime', + plugins_url( 'build/woo-directives-runtime.js', __FILE__ ), + array( 'woo-directives-vendors' ), '1.0.0', true ); } -add_action( 'init', 'wp_directives_register_scripts' ); +add_action( 'init', 'woo_directives_register_scripts' ); From 08e715fab13aa6320de62aaf426bffa14c0077b2 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 27 Jan 2023 18:42:29 +0100 Subject: [PATCH 30/54] Update cst meta tag --- woocommerce-gutenberg-products-block.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index cf3d5a629e0..c9f5dc7fb77 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -321,7 +321,7 @@ function () { * Insert the required meta tag for client-side transitions. */ function add_cst_meta_tag() { - echo ''; + echo ''; add_filter( 'client_side_transitions', true From 70bc51b1fb434728a6f001d51cbad4212b40e850 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 27 Jan 2023 18:54:00 +0100 Subject: [PATCH 31/54] Use `data-woo-` prefixed directives --- src/BlockTypes/SimplePriceFilter.php | 64 ++++++++++++++++------------ 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php index 4c2f93fdae2..a625e6e76a6 100644 --- a/src/BlockTypes/SimplePriceFilter.php +++ b/src/BlockTypes/SimplePriceFilter.php @@ -11,22 +11,28 @@ class SimplePriceFilter extends AbstractBlock { * * @var string */ - protected $block_name = 'simple-price-filter'; + protected $block_name = 'simple-price-filter'; const MIN_PRICE_QUERY_VAR = 'min_price'; const MAX_PRICE_QUERY_VAR = 'max_price'; /** - * Render function. + * Render the block. + * + * @param array $attributes Block attributes. + * @param string $content Block content. + * @param WP_Block $block Block instance. + * + * @return string Rendered block output. */ public function render( $attributes = [], $content = '', $block = null ) { $wrapper_attributes = get_block_wrapper_attributes(); - $max_range = 90; // TODO: get this value 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 ); + $max_range = 90; // TODO: get this value 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 ); // CSS variables for the range bar style. - $__low = 100 * $min_price / $max_range; - $__high = 100 * $max_price / $max_range; + $__low = 100 * $min_price / $max_range; + $__high = 100 * $max_price / $max_range; $range_style = "--low: $__low%; --high: $__high%"; return " @@ -35,8 +41,8 @@ public function render( $attributes = [], $content = '', $block = null ) {
- +
"; } /** - * Block type script definition. + * Get the frontend script handle for this block type. + * + * @param string $key Data to get, or default to everything. + * + * @return null */ public function get_block_type_script( $key = null ) { $script = [ 'handle' => 'simple-price-filter-view', - 'path' => 'build/wp-directives-simple-price-filter.js', - 'dependencies' => [ 'wp-directives-runtime' ], + 'path' => 'build/woo-directives-simple-price-filter.js', + 'dependencies' => [ 'woo-directives-runtime' ], ]; return $key ? $script[ $key ] : $script; } From 6277a3ce2b77cf103a9b2546aa40b18b47b898d1 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 10 Feb 2023 12:24:50 +0100 Subject: [PATCH 32/54] Update Interactivity API runtime --- assets/js/interactivity/directives.js | 10 +++---- assets/js/interactivity/hooks.js | 4 +-- assets/js/interactivity/index.js | 1 + assets/js/interactivity/router.js | 12 ++++---- assets/js/interactivity/store.js | 43 +++++++++++++++++++++++++++ assets/js/interactivity/wpx.js | 27 ----------------- 6 files changed, 57 insertions(+), 40 deletions(-) create mode 100644 assets/js/interactivity/store.js delete mode 100644 assets/js/interactivity/wpx.js diff --git a/assets/js/interactivity/directives.js b/assets/js/interactivity/directives.js index 4eb4269da44..a4286358171 100644 --- a/assets/js/interactivity/directives.js +++ b/assets/js/interactivity/directives.js @@ -2,15 +2,15 @@ import { useContext, useMemo, useEffect } from 'preact/hooks'; import { useSignalEffect } from '@preact/signals'; import { deepSignal, peek } from 'deepsignal'; import { directive } from './hooks'; -import { prefetch, navigate, hasClientSideTransitions } from './router'; +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 has client-side transitions enabled. -const clientSideTransitions = hasClientSideTransitions( document.head ); +// Check if current page can do client-side navigation. +const clientSideNavigation = canDoClientSideNavigation( document.head ); const isObject = ( item ) => item && typeof item === 'object' && ! Array.isArray( item ); @@ -146,13 +146,13 @@ export default () => { } ) => { useEffect( () => { // Prefetch the page if it is in the directive options. - if ( clientSideTransitions && link?.prefetch ) { + if ( clientSideNavigation && link?.prefetch ) { prefetch( href ); } } ); // Don't do anything if it's falsy. - if ( clientSideTransitions && link !== false ) { + if ( clientSideNavigation && link !== false ) { element.props.onclick = async ( event ) => { event.preventDefault(); diff --git a/assets/js/interactivity/hooks.js b/assets/js/interactivity/hooks.js index 0f98c94ee98..51fd9059dc3 100644 --- a/assets/js/interactivity/hooks.js +++ b/assets/js/interactivity/hooks.js @@ -1,6 +1,6 @@ import { h, options, createContext } from 'preact'; import { useRef } from 'preact/hooks'; -import { store } from './wpx'; +import { rawStore as store } from './store'; import { componentPrefix } from './constants'; // Main context. @@ -18,7 +18,7 @@ export const component = ( name, Comp ) => { componentMap[ name ] = Comp; }; -// Resolve the path to some property of the wpx object. +// Resolve the path to some property of the store object. const resolve = ( path, context ) => { let current = { ...store, context }; path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) ); diff --git a/assets/js/interactivity/index.js b/assets/js/interactivity/index.js index 853aa26a30a..cc1357d15e7 100644 --- a/assets/js/interactivity/index.js +++ b/assets/js/interactivity/index.js @@ -1,6 +1,7 @@ import registerDirectives from './directives'; import registerComponents from './components'; import { init } from './router'; +export { store } from './store'; /** * Initialize the initial vDOM. diff --git a/assets/js/interactivity/router.js b/assets/js/interactivity/router.js index 39b85b8a06e..8b441a95363 100644 --- a/assets/js/interactivity/router.js +++ b/assets/js/interactivity/router.js @@ -1,7 +1,7 @@ import { hydrate, render } from 'preact'; import { toVdom, hydratedIslands } from './vdom'; import { createRootFragment } from './utils'; -import { cstMetaTagItemprop, directivePrefix } from './constants'; +import { csnMetaTagItemprop, directivePrefix } from './constants'; // The root to render the vdom (document.body). let rootFragment; @@ -17,10 +17,10 @@ const cleanUrl = ( url ) => { return u.pathname + u.search; }; -// Helper to check if a page has client-side transitions activated. -export const hasClientSideTransitions = ( dom ) => +// Helper to check if a page can do client-side navigation. +export const canDoClientSideNavigation = ( dom ) => dom - .querySelector( `meta[itemprop='${ cstMetaTagItemprop }']` ) + .querySelector( `meta[itemprop='${ csnMetaTagItemprop }']` ) ?.getAttribute( 'content' ) === 'active'; // Fetch styles of a new page. @@ -55,7 +55,7 @@ const fetchHead = async ( head ) => { const fetchPage = async ( url ) => { const html = await window.fetch( url ).then( ( r ) => r.text() ); const dom = new window.DOMParser().parseFromString( html, 'text/html' ); - if ( ! hasClientSideTransitions( dom.head ) ) return false; + if ( ! canDoClientSideNavigation( dom.head ) ) return false; const head = await fetchHead( dom.head ); return { head, body: toVdom( dom.body ) }; }; @@ -98,7 +98,7 @@ window.addEventListener( 'popstate', async () => { // Initialize the router with the initial DOM. export const init = async () => { - if ( hasClientSideTransitions( document.head ) ) { + if ( canDoClientSideNavigation( document.head ) ) { // Create the root fragment to hydrate everything. rootFragment = createRootFragment( document.documentElement, diff --git a/assets/js/interactivity/store.js b/assets/js/interactivity/store.js new file mode 100644 index 00000000000..9bb51e87de9 --- /dev/null +++ b/assets/js/interactivity/store.js @@ -0,0 +1,43 @@ +import { deepSignal } from 'deepsignal'; + +const isObject = ( item ) => + item && typeof item === 'object' && ! Array.isArray( item ); + +export const deepMerge = ( target, source ) => { + if ( isObject( target ) && isObject( source ) ) { + for ( const key in source ) { + if ( isObject( source[ key ] ) ) { + if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); + deepMerge( target[ key ], source[ key ] ); + } else { + Object.assign( target, { [ key ]: source[ key ] } ); + } + } + } +}; + +const getSerializedState = () => { + // TODO: change the store tag ID for a better one. + const storeTag = document.querySelector( + `script[type="application/json"]#store` + ); + if ( ! storeTag ) return {}; + try { + const { state } = JSON.parse( storeTag.textContent ); + if ( isObject( state ) ) return state; + throw Error( 'Parsed state is not an object' ); + } catch ( e ) { + console.log( e ); + } + return {}; +}; + +const rawState = getSerializedState(); +export const rawStore = { state: deepSignal( rawState ) }; + +if ( typeof window !== 'undefined' ) window.store = rawStore; + +export const store = ( { state, ...block } ) => { + deepMerge( rawStore, block ); + deepMerge( rawState, state ); +}; diff --git a/assets/js/interactivity/wpx.js b/assets/js/interactivity/wpx.js deleted file mode 100644 index 7a49e171809..00000000000 --- a/assets/js/interactivity/wpx.js +++ /dev/null @@ -1,27 +0,0 @@ -import { deepSignal } from 'deepsignal'; - -const isObject = ( item ) => - item && typeof item === 'object' && ! Array.isArray( item ); - -export const deepMerge = ( target, source ) => { - if ( isObject( target ) && isObject( source ) ) { - for ( const key in source ) { - if ( isObject( source[ key ] ) ) { - if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); - deepMerge( target[ key ], source[ key ] ); - } else { - Object.assign( target, { [ key ]: source[ key ] } ); - } - } - } -}; - -const rawState = {}; -export const store = { state: deepSignal( rawState ) }; - -if ( typeof window !== 'undefined' ) window.wpx = store; - -export const wpx = ( { state, ...block } ) => { - deepMerge( store, block ); - deepMerge( rawState, state ); -}; From 423c858cf4dff718e7ef4b53e465fe6459051d62 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 14 Feb 2023 10:36:28 +0100 Subject: [PATCH 33/54] Hardcode php from interactivity API --- .../directives/attributes/woo-bind.php | 22 +++ .../directives/attributes/woo-class.php | 26 +++ .../directives/attributes/woo-context.php | 19 +++ .../directives/attributes/woo-style.php | 30 ++++ .../class-woo-directive-context.php | 73 ++++++++ .../directives/class-woo-directive-store.php | 32 ++++ .../directives/tags/woo-context.php | 19 +++ src/Interactivity/directives/utils.php | 51 ++++++ src/Interactivity/woo-directives.php | 159 ++++++++++++++++++ woocommerce-gutenberg-products-block.php | 24 +-- 10 files changed, 434 insertions(+), 21 deletions(-) create mode 100644 src/Interactivity/directives/attributes/woo-bind.php create mode 100644 src/Interactivity/directives/attributes/woo-class.php create mode 100644 src/Interactivity/directives/attributes/woo-context.php create mode 100644 src/Interactivity/directives/attributes/woo-style.php create mode 100644 src/Interactivity/directives/class-woo-directive-context.php create mode 100644 src/Interactivity/directives/class-woo-directive-store.php create mode 100644 src/Interactivity/directives/tags/woo-context.php create mode 100644 src/Interactivity/directives/utils.php create mode 100644 src/Interactivity/woo-directives.php diff --git a/src/Interactivity/directives/attributes/woo-bind.php b/src/Interactivity/directives/attributes/woo-bind.php new file mode 100644 index 00000000000..0b81c471e8b --- /dev/null +++ b/src/Interactivity/directives/attributes/woo-bind.php @@ -0,0 +1,22 @@ +is_tag_closer() ) { + return; + } + + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'woo-bind:' ); + + foreach ( $prefixed_attributes as $attr ) { + list( , $bound_attr ) = explode( ':', $attr ); + if ( empty( $bound_attr ) ) { + continue; + } + + $expr = $tags->get_attribute( $attr ); + $value = 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 new file mode 100644 index 00000000000..f00a0da818c --- /dev/null +++ b/src/Interactivity/directives/attributes/woo-class.php @@ -0,0 +1,26 @@ +is_tag_closer() ) { + return; + } + + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'woo-class:' ); + + foreach ( $prefixed_attributes as $attr ) { + list( , $class_name ) = explode( ':', $attr ); + if ( empty( $class_name ) ) { + continue; + } + + $expr = $tags->get_attribute( $attr ); + $add_class = 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 new file mode 100644 index 00000000000..00cbb3d8672 --- /dev/null +++ b/src/Interactivity/directives/attributes/woo-context.php @@ -0,0 +1,19 @@ +is_tag_closer() ) { + $context->rewind_context(); + return; + } + + $value = $tags->get_attribute( 'woo-context' ); + if ( null === $value ) { + // No woo-context directive. + return; + } + + $new_context = json_decode( $value, true ); + // TODO: Error handling. + + $context->set_context( $new_context ); +} diff --git a/src/Interactivity/directives/attributes/woo-style.php b/src/Interactivity/directives/attributes/woo-style.php new file mode 100644 index 00000000000..b8e7b8cbce8 --- /dev/null +++ b/src/Interactivity/directives/attributes/woo-style.php @@ -0,0 +1,30 @@ +is_tag_closer() ) { + return; + } + + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'woo-style:' ); + + foreach ( $prefixed_attributes as $attr ) { + list( , $style_name ) = explode( ':', $attr ); + if ( empty( $style_name ) ) { + continue; + } + + $expr = $tags->get_attribute( $attr ); + $style_value = evaluate( $expr, $context->get_context() ); + if ( $style_value ) { + $style_attr = $tags->get_attribute( 'style' ); + $style_attr = 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? + // $tags->remove_class( $style_name ); + } + } +} + diff --git a/src/Interactivity/directives/class-woo-directive-context.php b/src/Interactivity/directives/class-woo-directive-context.php new file mode 100644 index 00000000000..a4074ca40f1 --- /dev/null +++ b/src/Interactivity/directives/class-woo-directive-context.php @@ -0,0 +1,73 @@ + + * + * + * + * + * + * + */ +class Woo_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 ) { + 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/directives/class-woo-directive-store.php b/src/Interactivity/directives/class-woo-directive-store.php new file mode 100644 index 00000000000..595c44df589 --- /dev/null +++ b/src/Interactivity/directives/class-woo-directive-store.php @@ -0,0 +1,32 @@ +$store"; + } +} diff --git a/src/Interactivity/directives/tags/woo-context.php b/src/Interactivity/directives/tags/woo-context.php new file mode 100644 index 00000000000..65429884528 --- /dev/null +++ b/src/Interactivity/directives/tags/woo-context.php @@ -0,0 +1,19 @@ +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 ); + // TODO: Error handling. + + $context->set_context( $new_context ); +} diff --git a/src/Interactivity/directives/utils.php b/src/Interactivity/directives/utils.php new file mode 100644 index 00000000000..514291acb0a --- /dev/null +++ b/src/Interactivity/directives/utils.php @@ -0,0 +1,51 @@ + $context ) + ); + + $array = explode( '.', $path ); + foreach ( $array as $p ) { + if ( isset( $current[ $p ] ) ) { + $current = $current[ $p ]; + } else { + return null; + } + } + return $current; +} + +function 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/woo-directives.php b/src/Interactivity/woo-directives.php new file mode 100644 index 00000000000..775f78d45ba --- /dev/null +++ b/src/Interactivity/woo-directives.php @@ -0,0 +1,159 @@ +'; + } +} +add_action( 'wp_head', 'woo_directives_add_client_side_navigation_meta_tag' ); + +function woo_directives_client_site_navigation_option() { + // $options = get_option( 'woo_directives_plugin_settings' ); + // return $options['client_side_navigation']; + return true; +} +add_filter( + 'client_side_navigation', + 'woo_directives_client_site_navigation_option', + 9 +); + +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', '' ); + $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', '' ); + + 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( $block_content ) { + // TODO: Add some directive/components registration mechanism. + $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 ); + + $context = new Woo_Directive_Context(); + while ( $tags->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). + $attributes = $tags->get_attribute_names_with_prefix( 'data-woo-' ); + $attributes = $attributes ?? array(); + + foreach ( $attributes as $attribute ) { + if ( ! array_key_exists( $attribute, $attribute_directives ) ) { + continue; + } + + call_user_func( + $attribute_directives[ $attribute ], + $tags, + $context + ); + } + } + } + + return $block_content; +} +add_filter( + 'render_block', + 'woo_process_directives', + 10, + 1 +); + +// TODO: check if priority 9 is enough. +// TODO: check if `wp_footer` is the most appropriate hook. +add_action( 'wp_footer', array( 'Woo_Directive_Store', 'render' ), 9 ); diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 977a9497a1e..7f2fc5e471b 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -287,24 +287,6 @@ function woocommerce_blocks_plugin_outdated_notice() { add_action( 'admin_notices', 'woocommerce_blocks_plugin_outdated_notice' ); -/** - * Register the Interactivity API scripts. These files are enqueued when a block - * defines `woo-directives-runtime` as a dependency. - */ -function woo_directives_register_scripts() { - wp_register_script( - 'woo-directives-vendors', - plugins_url( 'build/woo-directives-vendors.js', __FILE__ ), - array(), - '1.0.0', - true - ); - wp_register_script( - 'woo-directives-runtime', - plugins_url( 'build/woo-directives-runtime.js', __FILE__ ), - array( 'woo-directives-vendors' ), - '1.0.0', - true - ); -} -add_action( 'init', 'woo_directives_register_scripts' ); +// Include the Interactivity API. +require_once __DIR__ . '/src/Interactivity/woo-directives.php'; + From 9af64783ef376034d51ec0c4700e2239bd3257dd Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 14 Feb 2023 10:37:15 +0100 Subject: [PATCH 34/54] Temporarily add gutenberg plugin as dependency --- .wp-env.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.wp-env.json b/.wp-env.json index 8a150435888..3aca2bc3c53 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,6 +1,7 @@ { "core": null, "plugins": [ + "https://downloads.wordpress.org/plugin/gutenberg.latest-stable.zip", "https://downloads.wordpress.org/plugin/woocommerce.latest-stable.zip", "https://github.com/WP-API/Basic-Auth/archive/master.zip", "./tests/mocks/woo-test-helper", From 78b7db2d6c45d8d3bd3cb5e8338a7a87a2fb0c98 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 20 Feb 2023 18:31:51 +0100 Subject: [PATCH 35/54] Exclude Interactivity API files from phpcs checks --- phpcs.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/phpcs.xml b/phpcs.xml index 85a09f9abf6..dd624e0ce2b 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -66,4 +66,7 @@ src/* + + + ./src/Interactivity/* From 92f5a8519436ae8c8501d4717419b0513e004fd0 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Mon, 20 Feb 2023 18:46:35 +0100 Subject: [PATCH 36/54] Update Interactivity API js files --- assets/js/interactivity/constants.js | 4 +-- assets/js/interactivity/directives.js | 41 +++++++-------------------- 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/assets/js/interactivity/constants.js b/assets/js/interactivity/constants.js index 5780fe9657e..72a842ed08e 100644 --- a/assets/js/interactivity/constants.js +++ b/assets/js/interactivity/constants.js @@ -1,3 +1,3 @@ -export const cstMetaTagItemprop = 'woo-client-side-transitions'; +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 c3776bec768..4f6775db73e 100644 --- a/assets/js/interactivity/directives.js +++ b/assets/js/interactivity/directives.js @@ -31,25 +31,6 @@ const mergeDeepSignals = ( target, source ) => { } }; -const isObject = ( item ) => - item && typeof item === 'object' && ! Array.isArray( item ); - -const mergeDeepSignals = ( target, source ) => { - for ( const k in source ) { - if ( typeof peek( target, k ) === 'undefined' ) { - target[ `$${ k }` ] = source[ `$${ k }` ]; - } else if ( - isObject( peek( target, k ) ) && - isObject( peek( source, k ) ) - ) { - mergeDeepSignals( - target[ `$${ k }` ].peek(), - source[ `$${ k }` ].peek() - ); - } - } -}; - export default () => { // wp-context directive( @@ -110,19 +91,19 @@ export default () => { className: name, context: contextValue, } ); + const currentClass = element.props.class || ''; + const classFinder = new RegExp( + `(^|\\s)${ name }(\\s|$)`, + 'g' + ); if ( ! result ) - element.props.class = element.props.class - .replace( - new RegExp( `(^|\\s)${ name }(\\s|$)`, 'g' ), - ' ' - ) + element.props.class = currentClass + .replace( classFinder, ' ' ) .trim(); - else if ( - ! new RegExp( `(^|\\s)${ name }(\\s|$)` ).test( - element.props.class - ) - ) - element.props.class += ` ${ name }`; + else if ( ! classFinder.test( currentClass ) ) + element.props.class = currentClass + ? `${ currentClass } ${ name }` + : name; useEffect( () => { // This seems necessary because Preact doesn't change the class names From 0c11440174b057dba45f1011eebd1838178c3f50 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 21 Feb 2023 10:45:54 +0100 Subject: [PATCH 37/54] Update Interactivity API php files --- .../directives/woo-process-directives.php | 86 +++++++++++++++++++ src/Interactivity/woo-directives.php | 43 ++-------- 2 files changed, 91 insertions(+), 38 deletions(-) create mode 100644 src/Interactivity/directives/woo-process-directives.php diff --git a/src/Interactivity/directives/woo-process-directives.php b/src/Interactivity/directives/woo-process-directives.php new file mode 100644 index 00000000000..6c5d7fcfacb --- /dev/null +++ b/src/Interactivity/directives/woo-process-directives.php @@ -0,0 +1,86 @@ +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 ( + ! 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; +} + +// TODO: Move into `WP_HTML_Tag_Processor` (or `WP_HTML_Processor`). +// See e.g. https://github.com/WordPress/gutenberg/pull/47573. +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; + } +} diff --git a/src/Interactivity/woo-directives.php b/src/Interactivity/woo-directives.php index 775f78d45ba..bad674918d0 100644 --- a/src/Interactivity/woo-directives.php +++ b/src/Interactivity/woo-directives.php @@ -3,6 +3,7 @@ require_once __DIR__ . '/directives/class-woo-directive-context.php'; require_once __DIR__ . '/directives/class-woo-directive-store.php'; +require_once __DIR__ . '/directives/woo-process-directives.php'; require_once __DIR__ . '/directives/attributes/woo-bind.php'; require_once __DIR__ . '/directives/attributes/woo-class.php'; @@ -48,16 +49,6 @@ function woo_directives_add_client_side_navigation_meta_tag() { } add_action( 'wp_head', 'woo_directives_add_client_side_navigation_meta_tag' ); -function woo_directives_client_site_navigation_option() { - // $options = get_option( 'woo_directives_plugin_settings' ); - // return $options['client_side_navigation']; - return true; -} -add_filter( - 'client_side_navigation', - 'woo_directives_client_site_navigation_option', - 9 -); function woo_directives_mark_interactive_blocks( $block_content, $block, $instance ) { if ( woo_directives_get_client_side_navigation() ) { @@ -106,7 +97,7 @@ function woo_directives_inner_blocks( $parsed_block, $source_block, $parent_bloc } add_filter( 'render_block_data', 'woo_directives_inner_blocks', 10, 3 ); -function woo_process_directives( $block_content ) { +function woo_process_directives_in_block( $block_content ) { // TODO: Add some directive/components registration mechanism. $tag_directives = array( 'woo-context' => 'process_woo_context_tag', @@ -120,36 +111,12 @@ function woo_process_directives( $block_content ) { ); $tags = new WP_HTML_Tag_Processor( $block_content ); - - $context = new Woo_Directive_Context(); - while ( $tags->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). - $attributes = $tags->get_attribute_names_with_prefix( 'data-woo-' ); - $attributes = $attributes ?? array(); - - foreach ( $attributes as $attribute ) { - if ( ! array_key_exists( $attribute, $attribute_directives ) ) { - continue; - } - - call_user_func( - $attribute_directives[ $attribute ], - $tags, - $context - ); - } - } - } - - return $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', + 'woo_process_directives_in_block', 10, 1 ); From 97f39c1c138c6a57944ea57f58f7467cf392d424 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 21 Feb 2023 10:46:43 +0100 Subject: [PATCH 38/54] Remove gutenberg from wp-env plugins --- .wp-env.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.wp-env.json b/.wp-env.json index 3aca2bc3c53..8a150435888 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,7 +1,6 @@ { "core": null, "plugins": [ - "https://downloads.wordpress.org/plugin/gutenberg.latest-stable.zip", "https://downloads.wordpress.org/plugin/woocommerce.latest-stable.zip", "https://github.com/WP-API/Basic-Auth/archive/master.zip", "./tests/mocks/woo-test-helper", From fb4a9dfc472db232bc229bfdeb1cf06a8001fa22 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 21 Feb 2023 11:11:19 +0100 Subject: [PATCH 39/54] Enable client side navigation --- woocommerce-gutenberg-products-block.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/woocommerce-gutenberg-products-block.php b/woocommerce-gutenberg-products-block.php index 2572935d2f6..45aa2bc4280 100644 --- a/woocommerce-gutenberg-products-block.php +++ b/woocommerce-gutenberg-products-block.php @@ -290,3 +290,10 @@ function woocommerce_blocks_plugin_outdated_notice() { // Include the Interactivity API. require_once __DIR__ . '/src/Interactivity/woo-directives.php'; +// Enable client-side navigation. +add_filter( + 'client_side_navigation', + function () { + return true; + } +); From 25693ea99698a987ce4ddd3e1708908ec639b650 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 14 Feb 2023 10:43:49 +0100 Subject: [PATCH 40/54] Fix store dependency in simple-price-filter --- assets/js/blocks/simple-price-filter/view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/blocks/simple-price-filter/view.js b/assets/js/blocks/simple-price-filter/view.js index dd87deca3ba..4ac82e14ef0 100644 --- a/assets/js/blocks/simple-price-filter/view.js +++ b/assets/js/blocks/simple-price-filter/view.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { wpx } from '@woocommerce/interactivity/wpx'; +import { store } from '@woocommerce/interactivity/store'; import { navigate } from '@woocommerce/interactivity/router'; const getHrefWithFilters = ( { state } ) => { @@ -33,7 +33,7 @@ const ssrMinPrice = const ssrMaxPrice = parseFloat( initialSearchParams.get( 'max_price' ) || '' ) || ssrMaxRange; -wpx( { +store( { state: { filters: { minPrice: ssrMinPrice, From cd95d9980226779fc08fb3934626e8e3a26d117b Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 14 Feb 2023 15:40:04 +0100 Subject: [PATCH 41/54] Remove SSRed state --- assets/js/blocks/simple-price-filter/view.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/assets/js/blocks/simple-price-filter/view.js b/assets/js/blocks/simple-price-filter/view.js index 4ac82e14ef0..7fe9e756672 100644 --- a/assets/js/blocks/simple-price-filter/view.js +++ b/assets/js/blocks/simple-price-filter/view.js @@ -24,21 +24,9 @@ const getHrefWithFilters = ( { state } ) => { return url.href; }; -const initialSearchParams = new URL( window.location.href ).searchParams; - -// TODO: get this values from SSR -const ssrMaxRange = 90; -const ssrMinPrice = - parseFloat( initialSearchParams.get( 'min_price' ) || '' ) || 0; -const ssrMaxPrice = - parseFloat( initialSearchParams.get( 'max_price' ) || '' ) || ssrMaxRange; - store( { state: { filters: { - minPrice: ssrMinPrice, - maxPrice: ssrMaxPrice, - maxRange: ssrMaxRange, isMinActive: true, isMaxActive: false, }, From a6dc2e27a0655cc232a7376f63ac289f7f826a15 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 14 Feb 2023 15:42:07 +0100 Subject: [PATCH 42/54] Fix registered runtime paths --- src/Interactivity/woo-directives.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Interactivity/woo-directives.php b/src/Interactivity/woo-directives.php index bad674918d0..feebee02289 100644 --- a/src/Interactivity/woo-directives.php +++ b/src/Interactivity/woo-directives.php @@ -18,14 +18,14 @@ function woo_directives_register_scripts() { wp_register_script( 'woo-directives-vendors', - plugins_url( 'build/woo-directives-vendors.js', __FILE__ ), + plugins_url( '../../build/woo-directives-vendors.js', __FILE__ ), array(), '1.0.0', true ); wp_register_script( 'woo-directives-runtime', - plugins_url( 'build/woo-directives-runtime.js', __FILE__ ), + plugins_url( '../../build/woo-directives-runtime.js', __FILE__ ), array( 'woo-directives-vendors' ), '1.0.0', true From 28dd2adfa3c98e388bbb8f52f6ec74fba953aa8a Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 14 Feb 2023 15:42:07 +0100 Subject: [PATCH 43/54] Fix registered runtime paths --- src/Interactivity/woo-directives.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Interactivity/woo-directives.php b/src/Interactivity/woo-directives.php index bad674918d0..feebee02289 100644 --- a/src/Interactivity/woo-directives.php +++ b/src/Interactivity/woo-directives.php @@ -18,14 +18,14 @@ function woo_directives_register_scripts() { wp_register_script( 'woo-directives-vendors', - plugins_url( 'build/woo-directives-vendors.js', __FILE__ ), + plugins_url( '../../build/woo-directives-vendors.js', __FILE__ ), array(), '1.0.0', true ); wp_register_script( 'woo-directives-runtime', - plugins_url( 'build/woo-directives-runtime.js', __FILE__ ), + plugins_url( '../../build/woo-directives-runtime.js', __FILE__ ), array( 'woo-directives-vendors' ), '1.0.0', true From 042100c6b4c33a489f7722b72a5356bf93a18fbf Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 14 Feb 2023 15:44:02 +0100 Subject: [PATCH 44/54] Init SimplePriceFilter store in php --- src/BlockTypes/SimplePriceFilter.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php index a625e6e76a6..82a960117d3 100644 --- a/src/BlockTypes/SimplePriceFilter.php +++ b/src/BlockTypes/SimplePriceFilter.php @@ -30,6 +30,18 @@ public function render( $attributes = [], $content = '', $block = null ) { $min_price = get_query_var( self::MIN_PRICE_QUERY_VAR, 0 ); $max_price = get_query_var( self::MAX_PRICE_QUERY_VAR, $max_range ); + store( + array( + 'state' => array( + 'filters' => array( + 'minPrice' => $min_price, + 'maxPrice' => $max_price, + 'maxRange' => $max_range, + ) + ) + ) + ); + // CSS variables for the range bar style. $__low = 100 * $min_price / $max_range; $__high = 100 * $max_price / $max_range; From 5453d6b58e73c1e872348cae259d50329ee50931 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 14 Feb 2023 19:41:32 +0100 Subject: [PATCH 45/54] Fix prefixes when getting attributes in directives --- src/Interactivity/directives/attributes/woo-bind.php | 2 +- src/Interactivity/directives/attributes/woo-class.php | 2 +- src/Interactivity/directives/attributes/woo-context.php | 2 +- src/Interactivity/directives/attributes/woo-style.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Interactivity/directives/attributes/woo-bind.php b/src/Interactivity/directives/attributes/woo-bind.php index 0b81c471e8b..3b9b01cc231 100644 --- a/src/Interactivity/directives/attributes/woo-bind.php +++ b/src/Interactivity/directives/attributes/woo-bind.php @@ -7,7 +7,7 @@ function process_woo_bind( $tags, $context ) { return; } - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'woo-bind:' ); + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-bind:' ); foreach ( $prefixed_attributes as $attr ) { list( , $bound_attr ) = explode( ':', $attr ); diff --git a/src/Interactivity/directives/attributes/woo-class.php b/src/Interactivity/directives/attributes/woo-class.php index f00a0da818c..e9cb60f893e 100644 --- a/src/Interactivity/directives/attributes/woo-class.php +++ b/src/Interactivity/directives/attributes/woo-class.php @@ -7,7 +7,7 @@ function process_woo_class( $tags, $context ) { return; } - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'woo-class:' ); + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-class:' ); foreach ( $prefixed_attributes as $attr ) { list( , $class_name ) = explode( ':', $attr ); diff --git a/src/Interactivity/directives/attributes/woo-context.php b/src/Interactivity/directives/attributes/woo-context.php index 00cbb3d8672..8a0cd433219 100644 --- a/src/Interactivity/directives/attributes/woo-context.php +++ b/src/Interactivity/directives/attributes/woo-context.php @@ -6,7 +6,7 @@ function process_woo_context_attribute( $tags, $context ) { return; } - $value = $tags->get_attribute( 'woo-context' ); + $value = $tags->get_attribute( 'data-woo-context' ); if ( null === $value ) { // No woo-context directive. return; diff --git a/src/Interactivity/directives/attributes/woo-style.php b/src/Interactivity/directives/attributes/woo-style.php index b8e7b8cbce8..3bb5dc15026 100644 --- a/src/Interactivity/directives/attributes/woo-style.php +++ b/src/Interactivity/directives/attributes/woo-style.php @@ -7,7 +7,7 @@ function process_woo_style( $tags, $context ) { return; } - $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'woo-style:' ); + $prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-style:' ); foreach ( $prefixed_attributes as $attr ) { list( , $style_name ) = explode( ':', $attr ); From 72bd2d9c0b3714650822384655f3579b8947d8c6 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 14 Feb 2023 19:43:38 +0100 Subject: [PATCH 46/54] Remove unnecessary state --- assets/js/blocks/simple-price-filter/view.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/assets/js/blocks/simple-price-filter/view.js b/assets/js/blocks/simple-price-filter/view.js index 7fe9e756672..fd7e72edf89 100644 --- a/assets/js/blocks/simple-price-filter/view.js +++ b/assets/js/blocks/simple-price-filter/view.js @@ -26,12 +26,6 @@ const getHrefWithFilters = ( { state } ) => { store( { state: { - filters: { - isMinActive: true, - isMaxActive: false, - }, - }, - derived: { filters: { rangeStyle: ( { state } ) => { const { minPrice, maxPrice, maxRange } = state.filters; From 9d77a527936f64e80587310ece5737ecfb6efe30 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 14 Feb 2023 19:45:01 +0100 Subject: [PATCH 47/54] Add missing initial state in Simple Price Filter --- src/BlockTypes/SimplePriceFilter.php | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php index 82a960117d3..0ce0e19f82a 100644 --- a/src/BlockTypes/SimplePriceFilter.php +++ b/src/BlockTypes/SimplePriceFilter.php @@ -30,35 +30,37 @@ public function render( $attributes = [], $content = '', $block = null ) { $min_price = get_query_var( self::MIN_PRICE_QUERY_VAR, 0 ); $max_price = get_query_var( self::MAX_PRICE_QUERY_VAR, $max_range ); + // CSS variables for the range bar style. + // TODO: Use style directive instead when available. + $__low = 100 * $min_price / $max_range; + $__high = 100 * $max_price / $max_range; + $range_style = "--low: $__low%; --high: $__high%"; + store( array( 'state' => array( 'filters' => array( - 'minPrice' => $min_price, - 'maxPrice' => $max_price, - 'maxRange' => $max_range, + 'minPrice' => $min_price, + 'maxPrice' => $max_price, + 'maxRange' => $max_range, + 'rangeStyle' => $range_style, + 'isMinActive' => true, + 'isMaxActive' => false ) ) ) ); - // CSS variables for the range bar style. - $__low = 100 * $min_price / $max_range; - $__high = 100 * $max_price / $max_range; - $range_style = "--low: $__low%; --high: $__high%"; - return "

Filter by price

Date: Tue, 14 Feb 2023 19:49:16 +0100 Subject: [PATCH 48/54] Remove bound attributes --- src/BlockTypes/SimplePriceFilter.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php index 0ce0e19f82a..c93485273b4 100644 --- a/src/BlockTypes/SimplePriceFilter.php +++ b/src/BlockTypes/SimplePriceFilter.php @@ -62,9 +62,8 @@ class='range'
Date: Tue, 21 Feb 2023 11:27:06 +0100 Subject: [PATCH 49/54] Add gutenberg plugin to wp-env --- .wp-env.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.wp-env.json b/.wp-env.json index 8a150435888..3aca2bc3c53 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,6 +1,7 @@ { "core": null, "plugins": [ + "https://downloads.wordpress.org/plugin/gutenberg.latest-stable.zip", "https://downloads.wordpress.org/plugin/woocommerce.latest-stable.zip", "https://github.com/WP-API/Basic-Auth/archive/master.zip", "./tests/mocks/woo-test-helper", From 1874a4c427b6637fee2030ad57bbdd19e34c876a Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 21 Feb 2023 12:58:30 +0100 Subject: [PATCH 50/54] Fix directive prefix in constants --- assets/js/interactivity/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/interactivity/constants.js b/assets/js/interactivity/constants.js index 72a842ed08e..e2de6acee8b 100644 --- a/assets/js/interactivity/constants.js +++ b/assets/js/interactivity/constants.js @@ -1,3 +1,3 @@ export const csnMetaTagItemprop = 'woo-client-side-navigation'; export const componentPrefix = 'woo-'; -export const directivePrefix = 'woo-'; +export const directivePrefix = 'data-woo-'; From 0cd9a2337f8277ed3c0d9722fb97f7c3f995349d Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 21 Feb 2023 14:05:31 +0100 Subject: [PATCH 51/54] Avoid a Fatal error when importing `wp-html` --- src/Interactivity/woo-directives.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Interactivity/woo-directives.php b/src/Interactivity/woo-directives.php index feebee02289..e92dafff09f 100644 --- a/src/Interactivity/woo-directives.php +++ b/src/Interactivity/woo-directives.php @@ -1,5 +1,5 @@ Date: Tue, 21 Feb 2023 16:40:01 +0100 Subject: [PATCH 52/54] Remove TODO comments from Interactivity API files --- assets/js/interactivity/store.js | 1 - src/Interactivity/directives/attributes/woo-context.php | 1 - src/Interactivity/directives/attributes/woo-style.php | 1 - src/Interactivity/directives/class-woo-directive-store.php | 1 - src/Interactivity/directives/tags/woo-context.php | 1 - src/Interactivity/directives/utils.php | 2 -- src/Interactivity/directives/woo-process-directives.php | 1 - src/Interactivity/woo-directives.php | 3 --- 8 files changed, 11 deletions(-) diff --git a/assets/js/interactivity/store.js b/assets/js/interactivity/store.js index 9bb51e87de9..14df2a369d6 100644 --- a/assets/js/interactivity/store.js +++ b/assets/js/interactivity/store.js @@ -17,7 +17,6 @@ export const deepMerge = ( target, source ) => { }; const getSerializedState = () => { - // TODO: change the store tag ID for a better one. const storeTag = document.querySelector( `script[type="application/json"]#store` ); diff --git a/src/Interactivity/directives/attributes/woo-context.php b/src/Interactivity/directives/attributes/woo-context.php index 8a0cd433219..823ab9683b0 100644 --- a/src/Interactivity/directives/attributes/woo-context.php +++ b/src/Interactivity/directives/attributes/woo-context.php @@ -13,7 +13,6 @@ function process_woo_context_attribute( $tags, $context ) { } $new_context = json_decode( $value, true ); - // TODO: Error handling. $context->set_context( $new_context ); } diff --git a/src/Interactivity/directives/attributes/woo-style.php b/src/Interactivity/directives/attributes/woo-style.php index 3bb5dc15026..cf3e7f9388e 100644 --- a/src/Interactivity/directives/attributes/woo-style.php +++ b/src/Interactivity/directives/attributes/woo-style.php @@ -22,7 +22,6 @@ function process_woo_style( $tags, $context ) { $style_attr = 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? // $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 index 595c44df589..57b8b3c3fc9 100644 --- a/src/Interactivity/directives/class-woo-directive-store.php +++ b/src/Interactivity/directives/class-woo-directive-store.php @@ -24,7 +24,6 @@ static function render() { return; } - // TODO: find a better ID for the script tag. $id = 'store'; $store = self::serialize(); echo ""; diff --git a/src/Interactivity/directives/tags/woo-context.php b/src/Interactivity/directives/tags/woo-context.php index 65429884528..13a4a74cc3f 100644 --- a/src/Interactivity/directives/tags/woo-context.php +++ b/src/Interactivity/directives/tags/woo-context.php @@ -13,7 +13,6 @@ function process_woo_context_tag( $tags, $context ) { } $new_context = json_decode( $value, true ); - // TODO: Error handling. $context->set_context( $new_context ); } diff --git a/src/Interactivity/directives/utils.php b/src/Interactivity/directives/utils.php index 514291acb0a..b639bad63f7 100644 --- a/src/Interactivity/directives/utils.php +++ b/src/Interactivity/directives/utils.php @@ -6,7 +6,6 @@ function store( $data ) { Woo_Directive_Store::merge_data( $data ); } -// TODO: Implement evaluation of complex logical expressions. function evaluate( string $path, array $context = array() ) { $current = array_merge( Woo_Directive_Store::get_data(), @@ -30,7 +29,6 @@ function 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; $modified = true; break; diff --git a/src/Interactivity/directives/woo-process-directives.php b/src/Interactivity/directives/woo-process-directives.php index 6c5d7fcfacb..ecc55b930a4 100644 --- a/src/Interactivity/directives/woo-process-directives.php +++ b/src/Interactivity/directives/woo-process-directives.php @@ -61,7 +61,6 @@ function woo_process_directives( $tags, $prefix, $tag_directives, $attribute_dir return $tags; } -// TODO: Move into `WP_HTML_Tag_Processor` (or `WP_HTML_Processor`). // See e.g. https://github.com/WordPress/gutenberg/pull/47573. function is_html_void_element( $tag_name ) { switch ( $tag_name ) { diff --git a/src/Interactivity/woo-directives.php b/src/Interactivity/woo-directives.php index e92dafff09f..7c21ee06597 100644 --- a/src/Interactivity/woo-directives.php +++ b/src/Interactivity/woo-directives.php @@ -98,7 +98,6 @@ function woo_directives_inner_blocks( $parsed_block, $source_block, $parent_bloc add_filter( 'render_block_data', 'woo_directives_inner_blocks', 10, 3 ); function woo_process_directives_in_block( $block_content ) { - // TODO: Add some directive/components registration mechanism. $tag_directives = array( 'woo-context' => 'process_woo_context_tag', ); @@ -121,6 +120,4 @@ function woo_process_directives_in_block( $block_content ) { 1 ); -// TODO: check if priority 9 is enough. -// TODO: check if `wp_footer` is the most appropriate hook. add_action( 'wp_footer', array( 'Woo_Directive_Store', 'render' ), 9 ); From c1b8397f58d84602569e056b0417f6ecc4ce63ee Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 28 Feb 2023 18:21:33 +0100 Subject: [PATCH 53/54] Use `woo_directives_store` --- src/BlockTypes/SimplePriceFilter.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BlockTypes/SimplePriceFilter.php b/src/BlockTypes/SimplePriceFilter.php index c93485273b4..aeae0fdfef8 100644 --- a/src/BlockTypes/SimplePriceFilter.php +++ b/src/BlockTypes/SimplePriceFilter.php @@ -36,7 +36,7 @@ public function render( $attributes = [], $content = '', $block = null ) { $__high = 100 * $max_price / $max_range; $range_style = "--low: $__low%; --high: $__high%"; - store( + woo_directives_store( array( 'state' => array( 'filters' => array( @@ -45,9 +45,9 @@ public function render( $attributes = [], $content = '', $block = null ) { 'maxRange' => $max_range, 'rangeStyle' => $range_style, 'isMinActive' => true, - 'isMaxActive' => false - ) - ) + 'isMaxActive' => false, + ), + ), ) ); From 6653daa4102c030d6d580c7a4e41b4707ff8c207 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 28 Feb 2023 18:27:03 +0100 Subject: [PATCH 54/54] Remove unnecessary alias from webpack helpers --- bin/webpack-helpers.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bin/webpack-helpers.js b/bin/webpack-helpers.js index b26eb4f052b..5dad251446a 100644 --- a/bin/webpack-helpers.js +++ b/bin/webpack-helpers.js @@ -66,10 +66,6 @@ const getAlias = ( options = {} ) => { __dirname, `../assets/js/${ pathPart }base/utils/` ), - '@woocommerce/base-interactivity': path.resolve( - __dirname, - `../assets/js/${ pathPart }base/interactivity/` - ), '@woocommerce/blocks': path.resolve( __dirname, `../assets/js/${ pathPart }/blocks`