UNPKG

@wordpress/interactivity

Version:

Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.

8 lines (7 loc) 15.5 kB
{ "version": 3, "sources": ["../src/hooks.tsx"], "sourcesContent": ["// eslint-disable-next-line eslint-comments/disable-enable-pair\n/* eslint-disable react-hooks/exhaustive-deps */\n\n/**\n * External dependencies\n */\nimport {\n\th as createElement,\n\toptions,\n\tcreateContext,\n\tcloneElement,\n\ttype ComponentChildren,\n} from 'preact';\nimport { useRef, useCallback, useContext } from 'preact/hooks';\nimport type { VNode, Context } from 'preact';\n\n/**\n * Internal dependencies\n */\nimport { store, stores, universalUnlock } from './store';\nimport { warn, type SyncAwareFunction } from './utils';\nimport { getScope, setScope, resetScope, type Scope } from './scopes';\nimport { PENDING_GETTER } from './proxies/state';\nexport interface DirectiveEntry {\n\tvalue: string | object;\n\tnamespace: string;\n\tsuffix: string | null;\n\tuniqueId: string | null;\n}\n\nexport interface NonDefaultSuffixDirectiveEntry extends DirectiveEntry {\n\tsuffix: string;\n}\n\nexport interface DefaultSuffixDirectiveEntry extends DirectiveEntry {\n\tsuffix: null;\n}\n\nexport function isNonDefaultDirectiveSuffix(\n\tentry: DirectiveEntry\n): entry is NonDefaultSuffixDirectiveEntry {\n\treturn entry.suffix !== null;\n}\n\nexport function isDefaultDirectiveSuffix(\n\tentry: DirectiveEntry\n): entry is DefaultSuffixDirectiveEntry {\n\treturn entry.suffix === null;\n}\n\ntype DirectiveEntries = Record< string, DirectiveEntry[] >;\n\ninterface DirectiveArgs {\n\t/**\n\t * Object map with the defined directives of the element being evaluated.\n\t */\n\tdirectives: DirectiveEntries;\n\t/**\n\t * Props present in the current element.\n\t */\n\tprops: { children?: ComponentChildren };\n\t/**\n\t * Virtual node representing the element.\n\t */\n\telement: VNode< {\n\t\tclass?: string;\n\t\tstyle?: string | Record< string, string | number >;\n\t\tcontent?: ComponentChildren;\n\t} >;\n\t/**\n\t * The inherited context.\n\t */\n\tcontext: Context< any >;\n\t/**\n\t * Function that resolves a given path to a value either in the store or the\n\t * context.\n\t */\n\tevaluate: Evaluate;\n}\n\nexport interface DirectiveCallback {\n\t( args: DirectiveArgs ): VNode< any > | VNode< any >[] | null | void;\n}\n\ninterface DirectiveOptions {\n\t/**\n\t * Value that specifies the priority to evaluate directives of this type.\n\t * Lower numbers correspond with earlier execution.\n\t *\n\t * @default 10\n\t */\n\tpriority?: number;\n}\n\nexport interface Evaluate {\n\t( entry: DirectiveEntry, ...args: any[] ): any;\n}\n\ninterface GetEvaluate {\n\t( args: { scope: Scope } ): Evaluate;\n}\n\ntype PriorityLevel = string[];\n\ninterface GetPriorityLevels {\n\t( directives: DirectiveEntries ): PriorityLevel[];\n}\n\ninterface DirectivesProps {\n\tdirectives: DirectiveEntries;\n\tpriorityLevels: PriorityLevel[];\n\telement: VNode;\n\toriginalProps: any;\n\tpreviousScope?: Scope;\n}\n\n// Main context.\nconst context = createContext< any >( { client: {}, server: {} } );\n\n// WordPress Directives.\nconst directiveCallbacks: Record< string, DirectiveCallback > = {};\nconst directivePriorities: Record< string, number > = {};\n\n/**\n * Registers a new directive type in the Interactivity API runtime.\n *\n * @example\n * ```js\n * directive(\n * 'alert', // Name without the `data-wp-` prefix.\n * ( { directives: { alert }, element, evaluate } ) => {\n * const defaultEntry = alert.find( isDefaultDirectiveSuffix );\n * element.props.onclick = () => { alert( evaluate( defaultEntry ) ); }\n * }\n * )\n * ```\n *\n * The previous code registers a custom directive type for displaying an alert\n * message whenever an element using it is clicked. The message text is obtained\n * from the store under the inherited namespace, using `evaluate`.\n *\n * When the HTML is processed by the Interactivity API, any element containing\n * the `data-wp-alert` directive will have the `onclick` event handler, e.g.,\n *\n * ```html\n * <div data-wp-interactive=\"messages\">\n * <button data-wp-alert=\"state.alert\">Click me!</button>\n * </div>\n * ```\n * Note that, in the previous example, the directive callback gets the path\n * value (`state.alert`) from the directive entry with suffix `null`. A\n * custom suffix can also be specified by appending `--` to the directive\n * attribute, followed by the suffix, like in the following HTML snippet:\n *\n * ```html\n * <div data-wp-interactive=\"myblock\">\n * <button\n * data-wp-color--text=\"state.text\"\n * data-wp-color--background=\"state.background\"\n * >Click me!</button>\n * </div>\n * ```\n *\n * This could be an hypothetical implementation of the custom directive used in\n * the snippet above.\n *\n * @example\n * ```js\n * directive(\n * 'color', // Name without prefix and suffix.\n * ( { directives: { color: colors }, ref, evaluate } ) =>\n * colors.forEach( ( color ) => {\n * if ( color.suffix = 'text' ) {\n * ref.style.setProperty(\n * 'color',\n * evaluate( color.text )\n * );\n * }\n * if ( color.suffix = 'background' ) {\n * ref.style.setProperty(\n * 'background-color',\n * evaluate( color.background )\n * );\n * }\n * } );\n * }\n * )\n * ```\n *\n * @param name Directive name, without the `data-wp-` prefix.\n * @param callback Function that runs the directive logic.\n * @param options Options object.\n * @param options.priority Option to control the directive execution order. The\n * lesser, the highest priority. Default is `10`.\n */\nexport const directive = (\n\tname: string,\n\tcallback: DirectiveCallback,\n\t{ priority = 10 }: DirectiveOptions = {}\n) => {\n\tdirectiveCallbacks[ name ] = callback;\n\tdirectivePriorities[ name ] = priority;\n};\n\n// Resolve the path to some property of the store object.\nconst resolve = ( path: string, namespace: string ) => {\n\tif ( ! namespace ) {\n\t\twarn(\n\t\t\t`Namespace missing for \"${ path }\". The value for that path won't be resolved.`\n\t\t);\n\t\treturn;\n\t}\n\tlet resolvedStore = stores.get( namespace );\n\tif ( typeof resolvedStore === 'undefined' ) {\n\t\tresolvedStore = store(\n\t\t\tnamespace,\n\t\t\t{},\n\t\t\t{\n\t\t\t\tlock: universalUnlock,\n\t\t\t}\n\t\t);\n\t}\n\tconst current = {\n\t\t...resolvedStore,\n\t\tcontext: getScope().context[ namespace ],\n\t};\n\n\ttry {\n\t\tconst pathParts = path.split( '.' );\n\t\treturn pathParts.reduce( ( acc, key ) => acc[ key ], current );\n\t} catch ( e ) {\n\t\tif ( e === PENDING_GETTER ) {\n\t\t\treturn PENDING_GETTER;\n\t\t}\n\t}\n};\n\n// Generate the evaluate function.\nexport const getEvaluate: GetEvaluate =\n\t( { scope } ) =>\n\t// TODO: When removing the temporarily remaining `value( ...args )` call below, remove the `...args` parameter too.\n\t( entry, ...args ) => {\n\t\tlet { value: path, namespace } = entry;\n\t\tif ( typeof path !== 'string' ) {\n\t\t\tthrow new Error( 'The `value` prop should be a string path' );\n\t\t}\n\t\t// If path starts with !, remove it and save a flag.\n\t\tconst hasNegationOperator =\n\t\t\tpath[ 0 ] === '!' && !! ( path = path.slice( 1 ) );\n\t\tsetScope( scope );\n\t\tconst value = resolve( path, namespace );\n\t\t// Functions are returned without invoking them.\n\t\tif ( typeof value === 'function' ) {\n\t\t\t// Except if they have a negation operator present, for backward compatibility.\n\t\t\t// This pattern is strongly discouraged and deprecated, and it will be removed in a near future release.\n\t\t\t// TODO: Remove this condition to effectively ignore negation operator when provided with a function.\n\t\t\tif ( hasNegationOperator ) {\n\t\t\t\twarn(\n\t\t\t\t\t'Using a function with a negation operator is deprecated and will stop working in WordPress 6.9. Please use derived state instead.'\n\t\t\t\t);\n\t\t\t\tconst functionResult = ! value( ...args );\n\t\t\t\tresetScope();\n\t\t\t\treturn functionResult;\n\t\t\t}\n\t\t\t// Reset scope before return and wrap the function so it will still run within the correct scope.\n\t\t\tresetScope();\n\t\t\tconst wrappedFunction: Function = ( ...functionArgs: any[] ) => {\n\t\t\t\tsetScope( scope );\n\t\t\t\tconst functionResult = value( ...functionArgs );\n\t\t\t\tresetScope();\n\t\t\t\treturn functionResult;\n\t\t\t};\n\t\t\t// Preserve the sync property from the original function\n\t\t\tif ( value.sync ) {\n\t\t\t\tconst syncAwareFunction = wrappedFunction as SyncAwareFunction;\n\t\t\t\tsyncAwareFunction.sync = true;\n\t\t\t}\n\t\t\treturn wrappedFunction;\n\t\t}\n\t\tconst result = value;\n\t\tresetScope();\n\t\treturn hasNegationOperator && value !== PENDING_GETTER\n\t\t\t? ! result\n\t\t\t: result;\n\t};\n\n// Separate directives by priority. The resulting array contains objects\n// of directives grouped by same priority, and sorted in ascending order.\nconst getPriorityLevels: GetPriorityLevels = ( directives ) => {\n\tconst byPriority = Object.keys( directives ).reduce<\n\t\tRecord< number, string[] >\n\t>( ( obj, name ) => {\n\t\tif ( directiveCallbacks[ name ] ) {\n\t\t\tconst priority = directivePriorities[ name ];\n\t\t\t( obj[ priority ] = obj[ priority ] || [] ).push( name );\n\t\t}\n\t\treturn obj;\n\t}, {} );\n\n\treturn Object.entries( byPriority )\n\t\t.sort( ( [ p1 ], [ p2 ] ) => parseInt( p1 ) - parseInt( p2 ) )\n\t\t.map( ( [ , arr ] ) => arr );\n};\n\n// Component that wraps each priority level of directives of an element.\nconst Directives = ( {\n\tdirectives,\n\tpriorityLevels: [ currentPriorityLevel, ...nextPriorityLevels ],\n\telement,\n\toriginalProps,\n\tpreviousScope,\n}: DirectivesProps ) => {\n\t// Initialize the scope of this element. These scopes are different per each\n\t// level because each level has a different context, but they share the same\n\t// element ref, state and props.\n\tconst scope = useRef< Scope >( {} as Scope ).current;\n\tscope.evaluate = useCallback( getEvaluate( { scope } ), [] );\n\tconst { client, server } = useContext( context );\n\tscope.context = client;\n\tscope.serverContext = server;\n\t/* eslint-disable react-hooks/rules-of-hooks */\n\tscope.ref = previousScope?.ref || useRef( null );\n\t/* eslint-enable react-hooks/rules-of-hooks */\n\n\t// Create a fresh copy of the vnode element and add the props to the scope,\n\t// named as attributes (HTML Attributes).\n\telement = cloneElement( element, { ref: scope.ref } );\n\tscope.attributes = element.props;\n\n\t// Recursively render the wrapper for the next priority level.\n\tconst children =\n\t\tnextPriorityLevels.length > 0\n\t\t\t? createElement( Directives, {\n\t\t\t\t\tdirectives,\n\t\t\t\t\tpriorityLevels: nextPriorityLevels,\n\t\t\t\t\telement,\n\t\t\t\t\toriginalProps,\n\t\t\t\t\tpreviousScope: scope,\n\t\t\t } )\n\t\t\t: element;\n\n\tconst props = { ...originalProps, children };\n\tconst directiveArgs = {\n\t\tdirectives,\n\t\tprops,\n\t\telement,\n\t\tcontext,\n\t\tevaluate: scope.evaluate,\n\t};\n\n\tsetScope( scope );\n\n\tfor ( const directiveName of currentPriorityLevel ) {\n\t\tconst wrapper = directiveCallbacks[ directiveName ]?.( directiveArgs );\n\t\tif ( wrapper !== undefined ) {\n\t\t\tprops.children = wrapper;\n\t\t}\n\t}\n\n\tresetScope();\n\n\treturn props.children;\n};\n\n// Preact Options Hook called each time a vnode is created.\nconst old = options.vnode;\noptions.vnode = ( vnode: VNode< any > ) => {\n\tif ( vnode.props.__directives ) {\n\t\tconst props = vnode.props;\n\t\tconst directives = props.__directives;\n\t\tif ( directives.key ) {\n\t\t\tvnode.key = directives.key.find( isDefaultDirectiveSuffix ).value;\n\t\t}\n\t\tdelete props.__directives;\n\t\tconst priorityLevels = getPriorityLevels( directives );\n\t\tif ( priorityLevels.length > 0 ) {\n\t\t\tvnode.props = {\n\t\t\t\tdirectives,\n\t\t\t\tpriorityLevels,\n\t\t\t\toriginalProps: props,\n\t\t\t\ttype: vnode.type,\n\t\t\t\telement: createElement( vnode.type as any, props ),\n\t\t\t\ttop: true,\n\t\t\t};\n\t\t\tvnode.type = Directives;\n\t\t}\n\t}\n\n\tif ( old ) {\n\t\told( vnode );\n\t}\n};\n"], "mappings": "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", "names": [] }