@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
JavaScript
;
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