UNPKG

@wordpress/interactivity

Version:

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

136 lines (122 loc) 4.33 kB
/** * External dependencies */ import type { h as createElement, RefObject } from 'preact'; import { signal } from '@preact/signals'; /** * Internal dependencies */ import { getNamespace } from './namespaces'; import { deepReadOnly, deepClone } from './utils'; import type { Evaluate } from './hooks'; export interface Scope { evaluate: Evaluate; context: object; serverContext: object; ref: RefObject< HTMLElement >; attributes: createElement.JSX.HTMLAttributes; } // Store stacks for the current scope and the default namespaces and export APIs // to interact with them. const scopeStack: Scope[] = []; export const getScope = () => scopeStack.slice( -1 )[ 0 ]; export const setScope = ( scope: Scope ) => { scopeStack.push( scope ); }; export const resetScope = () => { scopeStack.pop(); }; const throwNotInScope = ( method: string ) => { throw Error( `Cannot call \`${ method }()\` when there is no scope. If you are using an async function, please consider using a generator instead. If you are using some sort of async callbacks, like \`setTimeout\`, please wrap the callback with \`withScope(callback)\`.` ); }; /** * Retrieves the context inherited by the element evaluating a function from the * store. The returned value depends on the element and the namespace where the * function calling `getContext()` exists. * * @param namespace Store namespace. By default, the namespace where the calling * function exists is used. * @return The context content. */ export const getContext = < T extends object >( namespace?: string ): T => { const scope = getScope(); if ( globalThis.SCRIPT_DEBUG ) { if ( ! scope ) { throwNotInScope( 'getContext' ); } } return scope.context[ namespace || getNamespace() ]; }; /** * Retrieves a representation of the element where a function from the store * is being evaluated. Such representation is read-only, and contains a * reference to the DOM element, its props and a local reactive state. * * @return Element representation. */ export const getElement = () => { const scope = getScope(); let deepReadOnlyOptions = {}; if ( globalThis.SCRIPT_DEBUG ) { if ( ! scope ) { throwNotInScope( 'getElement' ); } deepReadOnlyOptions = { errorMessage: "Don't mutate the attributes from `getElement`, use `data-wp-bind` to modify the attributes of an element instead.", }; } const { ref, attributes } = scope; return Object.freeze( { ref: ref.current, attributes: deepReadOnly( attributes, deepReadOnlyOptions ), } ); }; export const navigationContextSignal = signal( 0 ); /** * Gets the context defined and updated from the server. * * The object returned is a deep clone of the context defined in PHP with * `data-wp-context` directives, including the corresponding inherited * properties. When `actions.navigate()` is called, this object is updated to * reflect the changes in the new visited page, without affecting the context * returned by `getContext()`. Directives can subscribe to those changes to * update the context if needed. * * @example * ```js * store( 'myPlugin', { * callbacks: { * updateServerContext() { * const context = getContext(); * const serverContext = getServerContext(); * // Override some property with the new value that came from the server. * context.overridableProp = serverContext.overridableProp; * }, * }, * } ); * ``` * * @param namespace Store namespace. By default, it inherits the namespace of * the store where it is defined. * @return The server context content for the given namespace. */ export function getServerContext( namespace?: string ): Record< string, unknown >; export function getServerContext< T extends object >( namespace?: string ): T; export function getServerContext< T extends object >( namespace?: string ): T { const scope = getScope(); if ( globalThis.SCRIPT_DEBUG ) { if ( ! scope ) { throwNotInScope( 'getServerContext' ); } } // Accesses the signal to make this reactive. It assigns it to `subscribe` // to prevent the JavaScript minifier from removing this line. getServerContext.subscribe = navigationContextSignal.value; return deepClone( scope.serverContext[ namespace || getNamespace() ] ); } getServerContext.subscribe = 0;