Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce withSyncEvent action wrapper utility and proxy event object whenever it is not used #68097

Open
wants to merge 4 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 89 additions & 43 deletions packages/interactivity/src/directives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,39 @@ function deepClone< T >( source: T ): T {
return source;
}

function wrapEventAsync( event: Event ) {
const handler = {
get( target: any, prop: any, receiver: any ) {
const value = target[ prop ];
switch ( prop ) {
case 'currentTarget':
warn(
`Accessing the synchronous event.${ prop } property in a store action without wrapping it in withSyncEvent() is deprecated and will stop working in WordPress 6.9. Please wrap the store action in withSyncEvent().`
);
break;
case 'preventDefault':
case 'stopImmediatePropagation':
case 'stopPropagation':
warn(
`Using the synchronous event.${ prop }() function in a store action without wrapping it in withSyncEvent() is deprecated and will stop working in WordPress 6.9. Please wrap the store action in withSyncEvent().`
);
break;
}
if ( value instanceof Function ) {
return function ( this: any, ...args: any[] ) {
return value.apply(
this === receiver ? target : this,
args
);
};
}
return value;
},
};

return new Proxy( event, handler );
}

const newRule =
/(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g;
const ruleClean = /\/\*[^]*?\*\/| +/g;
Expand Down Expand Up @@ -96,13 +129,19 @@ const cssStringToObject = (
const getGlobalEventDirective = (
type: 'window' | 'document'
): DirectiveCallback => {
return ( { directives, evaluate } ) => {
return ( { directives, resolveEntry, evaluateResolved } ) => {
directives[ `on-${ type }` ]
.filter( isNonDefaultDirectiveSuffix )
.forEach( ( entry ) => {
const eventName = entry.suffix.split( '--', 1 )[ 0 ];
useInit( () => {
const cb = ( event: Event ) => evaluate( entry, event );
const cb = ( event: Event ) => {
const resolved = resolveEntry( entry );
if ( ! resolved.value?.sync ) {
event = wrapEventAsync( event );
}
evaluateResolved( resolved, event );
};
const globalVar = type === 'window' ? window : document;
globalVar.addEventListener( eventName, cb );
return () => globalVar.removeEventListener( eventName, cb );
Expand Down Expand Up @@ -263,51 +302,58 @@ export default () => {
} );

// data-wp-on--[event]
directive( 'on', ( { directives: { on }, element, evaluate } ) => {
const events = new Map< string, Set< DirectiveEntry > >();
on.filter( isNonDefaultDirectiveSuffix ).forEach( ( entry ) => {
const event = entry.suffix.split( '--' )[ 0 ];
if ( ! events.has( event ) ) {
events.set( event, new Set< DirectiveEntry >() );
}
events.get( event )!.add( entry );
} );
directive(
'on',
( { directives: { on }, element, resolveEntry, evaluateResolved } ) => {
const events = new Map< string, Set< DirectiveEntry > >();
on.filter( isNonDefaultDirectiveSuffix ).forEach( ( entry ) => {
const event = entry.suffix.split( '--' )[ 0 ];
if ( ! events.has( event ) ) {
events.set( event, new Set< DirectiveEntry >() );
}
events.get( event )!.add( entry );
} );

events.forEach( ( entries, eventType ) => {
const existingHandler = element.props[ `on${ eventType }` ];
element.props[ `on${ eventType }` ] = ( event: Event ) => {
entries.forEach( ( entry ) => {
if ( existingHandler ) {
existingHandler( event );
}
let start;
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( globalThis.SCRIPT_DEBUG ) {
start = performance.now();
events.forEach( ( entries, eventType ) => {
const existingHandler = element.props[ `on${ eventType }` ];
element.props[ `on${ eventType }` ] = ( event: Event ) => {
entries.forEach( ( entry ) => {
if ( existingHandler ) {
existingHandler( event );
}
}
evaluate( entry, event );
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( globalThis.SCRIPT_DEBUG ) {
performance.measure(
`interactivity api on ${ entry.namespace }`,
{
// eslint-disable-next-line no-undef
start,
end: performance.now(),
detail: {
devtools: {
track: `IA: on ${ entry.namespace }`,
let start;
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( globalThis.SCRIPT_DEBUG ) {
start = performance.now();
}
}
const resolved = resolveEntry( entry );
if ( ! resolved.value?.sync ) {
event = wrapEventAsync( event );
}
evaluateResolved( resolved, event );
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( globalThis.SCRIPT_DEBUG ) {
performance.measure(
`interactivity api on ${ entry.namespace }`,
{
// eslint-disable-next-line no-undef
start,
end: performance.now(),
detail: {
devtools: {
track: `IA: on ${ entry.namespace }`,
},
},
},
}
);
}
);
}
}
}
} );
};
} );
} );
} );
};
} );
}
);

// data-wp-on-async--[event]
directive(
Expand Down
71 changes: 69 additions & 2 deletions packages/interactivity/src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ interface DirectiveArgs {
* context.
*/
evaluate: Evaluate;
/**
* Function that resolves a given path to value data in the store or the
* context.
*/
resolveEntry: ResolveEntry;
/**
* Function that evaluates resolved value data to a value either in the store
* or the context.
*/
evaluateResolved: EvaluateResolved;
}

export interface DirectiveCallback {
Expand All @@ -90,6 +100,17 @@ interface DirectiveOptions {
priority?: number;
}

interface ResolvedEntry {
/**
* Resolved value, either a result or a function to get the result.
*/
value: any;
/**
* Whether a negation operator should be used on the result.
*/
hasNegationOperator: boolean;
}

export interface Evaluate {
( entry: DirectiveEntry, ...args: any[] ): any;
}
Expand All @@ -98,6 +119,22 @@ interface GetEvaluate {
( args: { scope: Scope } ): Evaluate;
}

export interface ResolveEntry {
( entry: DirectiveEntry ): ResolvedEntry;
}

interface GetResolveEntry {
( args: { scope: Scope } ): ResolveEntry;
}

export interface EvaluateResolved {
( resolved: ResolvedEntry, ...args: any[] ): any;
}

interface GetEvaluateResolved {
( args: { scope: Scope } ): EvaluateResolved;
}

type PriorityLevel = string[];

interface GetPriorityLevels {
Expand Down Expand Up @@ -229,9 +266,19 @@ const resolve = ( path: string, namespace: string ) => {
};

// Generate the evaluate function.
export const getEvaluate: GetEvaluate =
export const getEvaluate: GetEvaluate = ( scopeData ) => {
const resolveEntry = getResolveEntry( scopeData );
const evaluateResolved = getEvaluateResolved( scopeData );
return ( entry, ...args ) => {
const resolved = resolveEntry( entry );
return evaluateResolved( resolved, ...args );
};
};

// Generate the resolveEntry function.
export const getResolveEntry: GetResolveEntry =
( { scope } ) =>
( entry, ...args ) => {
( entry ) => {
let { value: path, namespace } = entry;
if ( typeof path !== 'string' ) {
throw new Error( 'The `value` prop should be a string path' );
Expand All @@ -241,6 +288,19 @@ export const getEvaluate: GetEvaluate =
path[ 0 ] === '!' && !! ( path = path.slice( 1 ) );
setScope( scope );
const value = resolve( path, namespace );
resetScope();
return {
value,
hasNegationOperator,
};
};

// Generate the evaluateResolved function.
export const getEvaluateResolved: GetEvaluateResolved =
( { scope } ) =>
( resolved, ...args ) => {
const { value, hasNegationOperator } = resolved;
setScope( scope );
const result = typeof value === 'function' ? value( ...args ) : value;
resetScope();
return hasNegationOperator ? ! result : result;
Expand Down Expand Up @@ -277,6 +337,11 @@ const Directives = ( {
// element ref, state and props.
const scope = useRef< Scope >( {} as Scope ).current;
scope.evaluate = useCallback( getEvaluate( { scope } ), [] );
scope.resolveEntry = useCallback( getResolveEntry( { scope } ), [] );
scope.evaluateResolved = useCallback(
getEvaluateResolved( { scope } ),
[]
);
const { client, server } = useContext( context );
scope.context = client;
scope.serverContext = server;
Expand Down Expand Up @@ -308,6 +373,8 @@ const Directives = ( {
element,
context,
evaluate: scope.evaluate,
resolveEntry: scope.resolveEntry,
evaluateResolved: scope.evaluateResolved,
};

setScope( scope );
Expand Down
1 change: 1 addition & 0 deletions packages/interactivity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
useCallback,
useMemo,
splitTask,
withSyncEvent,
} from './utils';

export { useState, useRef } from 'preact/hooks';
Expand Down
4 changes: 3 additions & 1 deletion packages/interactivity/src/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import type { h as createElement, RefObject } from 'preact';
* Internal dependencies
*/
import { getNamespace } from './namespaces';
import type { Evaluate } from './hooks';
import type { Evaluate, ResolveEntry, EvaluateResolved } from './hooks';

export interface Scope {
evaluate: Evaluate;
resolveEntry: ResolveEntry;
evaluateResolved: EvaluateResolved;
context: object;
serverContext: object;
ref: RefObject< HTMLElement >;
Expand Down
12 changes: 12 additions & 0 deletions packages/interactivity/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,15 @@ export const isPlainObject = (
typeof candidate === 'object' &&
candidate.constructor === Object
);

/**
* Indicates that the passed `callback` requires synchronous access to the event object.
*
* @param callback The event callback.
* @return Wrapped event callback.
*/
export const withSyncEvent = ( callback: Function ): Function => {
const wrapped = ( ...args: any[] ) => callback( ...args );
wrapped.sync = true;
return wrapped;
};
Loading