UNPKG

@wordpress/interactivity

Version:

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

288 lines (275 loc) 9.09 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getEvaluate = exports.directive = void 0; exports.isDefaultDirectiveSuffix = isDefaultDirectiveSuffix; exports.isNonDefaultDirectiveSuffix = isNonDefaultDirectiveSuffix; var _preact = require("preact"); var _hooks = require("preact/hooks"); var _store = require("./store"); var _utils = require("./utils"); var _scopes = require("./scopes"); // eslint-disable-next-line eslint-comments/disable-enable-pair /* eslint-disable react-hooks/exhaustive-deps */ /** * External dependencies */ /** * Internal dependencies */ function isNonDefaultDirectiveSuffix(entry) { return entry.suffix !== null; } function isDefaultDirectiveSuffix(entry) { return entry.suffix === null; } // Main context. const context = (0, _preact.createContext)({ client: {}, server: {} }); // WordPress Directives. const directiveCallbacks = {}; const directivePriorities = {}; /** * Registers a new directive type in the Interactivity API runtime. * * @example * ```js * directive( * 'alert', // Name without the `data-wp-` prefix. * ( { directives: { alert }, element, evaluate } ) => { * const defaultEntry = alert.find( isDefaultDirectiveSuffix ); * element.props.onclick = () => { alert( evaluate( defaultEntry ) ); } * } * ) * ``` * * The previous code registers a custom directive type for displaying an alert * message whenever an element using it is clicked. The message text is obtained * from the store under the inherited namespace, using `evaluate`. * * When the HTML is processed by the Interactivity API, any element containing * the `data-wp-alert` directive will have the `onclick` event handler, e.g., * * ```html * <div data-wp-interactive="messages"> * <button data-wp-alert="state.alert">Click me!</button> * </div> * ``` * Note that, in the previous example, the directive callback gets the path * value (`state.alert`) from the directive entry with suffix `null`. A * custom suffix can also be specified by appending `--` to the directive * attribute, followed by the suffix, like in the following HTML snippet: * * ```html * <div data-wp-interactive="myblock"> * <button * data-wp-color--text="state.text" * data-wp-color--background="state.background" * >Click me!</button> * </div> * ``` * * This could be an hypothetical implementation of the custom directive used in * the snippet above. * * @example * ```js * directive( * 'color', // Name without prefix and suffix. * ( { directives: { color: colors }, ref, evaluate } ) => * colors.forEach( ( color ) => { * if ( color.suffix = 'text' ) { * ref.style.setProperty( * 'color', * evaluate( color.text ) * ); * } * if ( color.suffix = 'background' ) { * ref.style.setProperty( * 'background-color', * evaluate( color.background ) * ); * } * } ); * } * ) * ``` * * @param name Directive name, without the `data-wp-` prefix. * @param callback Function that runs the directive logic. * @param options Options object. * @param options.priority Option to control the directive execution order. The * lesser, the highest priority. Default is `10`. */ const directive = (name, callback, { priority = 10 } = {}) => { directiveCallbacks[name] = callback; directivePriorities[name] = priority; }; // Resolve the path to some property of the store object. exports.directive = directive; const resolve = (path, namespace) => { if (!namespace) { (0, _utils.warn)(`Namespace missing for "${path}". The value for that path won't be resolved.`); return; } let resolvedStore = _store.stores.get(namespace); if (typeof resolvedStore === 'undefined') { resolvedStore = (0, _store.store)(namespace, {}, { lock: _store.universalUnlock }); } const current = { ...resolvedStore, context: (0, _scopes.getScope)().context[namespace] }; try { // TODO: Support lazy/dynamically initialized stores return path.split('.').reduce((acc, key) => acc[key], current); } catch (e) {} }; // Generate the evaluate function. const getEvaluate = ({ scope }) => // TODO: When removing the temporarily remaining `value( ...args )` call below, remove the `...args` parameter too. (entry, ...args) => { let { value: path, namespace } = entry; if (typeof path !== 'string') { throw new Error('The `value` prop should be a string path'); } // If path starts with !, remove it and save a flag. const hasNegationOperator = path[0] === '!' && !!(path = path.slice(1)); (0, _scopes.setScope)(scope); const value = resolve(path, namespace); // Functions are returned without invoking them. if (typeof value === 'function') { // Except if they have a negation operator present, for backward compatibility. // This pattern is strongly discouraged and deprecated, and it will be removed in a near future release. // TODO: Remove this condition to effectively ignore negation operator when provided with a function. if (hasNegationOperator) { (0, _utils.warn)('Using a function with a negation operator is deprecated and will stop working in WordPress 6.9. Please use derived state instead.'); const functionResult = !value(...args); (0, _scopes.resetScope)(); return functionResult; } // Reset scope before return and wrap the function so it will still run within the correct scope. (0, _scopes.resetScope)(); return (...functionArgs) => { (0, _scopes.setScope)(scope); const functionResult = value(...functionArgs); (0, _scopes.resetScope)(); return functionResult; }; } const result = value; (0, _scopes.resetScope)(); return hasNegationOperator ? !result : result; }; // Separate directives by priority. The resulting array contains objects // of directives grouped by same priority, and sorted in ascending order. exports.getEvaluate = getEvaluate; const getPriorityLevels = directives => { const byPriority = Object.keys(directives).reduce((obj, name) => { if (directiveCallbacks[name]) { const priority = directivePriorities[name]; (obj[priority] = obj[priority] || []).push(name); } return obj; }, {}); return Object.entries(byPriority).sort(([p1], [p2]) => parseInt(p1) - parseInt(p2)).map(([, arr]) => arr); }; // Component that wraps each priority level of directives of an element. const Directives = ({ directives, priorityLevels: [currentPriorityLevel, ...nextPriorityLevels], element, originalProps, previousScope }) => { // Initialize the scope of this element. These scopes are different per each // level because each level has a different context, but they share the same // element ref, state and props. const scope = (0, _hooks.useRef)({}).current; scope.evaluate = (0, _hooks.useCallback)(getEvaluate({ scope }), []); const { client, server } = (0, _hooks.useContext)(context); scope.context = client; scope.serverContext = server; /* eslint-disable react-hooks/rules-of-hooks */ scope.ref = previousScope?.ref || (0, _hooks.useRef)(null); /* eslint-enable react-hooks/rules-of-hooks */ // Create a fresh copy of the vnode element and add the props to the scope, // named as attributes (HTML Attributes). element = (0, _preact.cloneElement)(element, { ref: scope.ref }); scope.attributes = element.props; // Recursively render the wrapper for the next priority level. const children = nextPriorityLevels.length > 0 ? (0, _preact.h)(Directives, { directives, priorityLevels: nextPriorityLevels, element, originalProps, previousScope: scope }) : element; const props = { ...originalProps, children }; const directiveArgs = { directives, props, element, context, evaluate: scope.evaluate }; (0, _scopes.setScope)(scope); for (const directiveName of currentPriorityLevel) { const wrapper = directiveCallbacks[directiveName]?.(directiveArgs); if (wrapper !== undefined) { props.children = wrapper; } } (0, _scopes.resetScope)(); return props.children; }; // Preact Options Hook called each time a vnode is created. const old = _preact.options.vnode; _preact.options.vnode = vnode => { if (vnode.props.__directives) { const props = vnode.props; const directives = props.__directives; if (directives.key) { vnode.key = directives.key.find(isDefaultDirectiveSuffix).value; } delete props.__directives; const priorityLevels = getPriorityLevels(directives); if (priorityLevels.length > 0) { vnode.props = { directives, priorityLevels, originalProps: props, type: vnode.type, element: (0, _preact.h)(vnode.type, props), top: true }; vnode.type = Directives; } } if (old) { old(vnode); } }; //# sourceMappingURL=hooks.js.map