@wordpress/interactivity
Version:
Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.
743 lines (742 loc) • 23.4 kB
JavaScript
// packages/interactivity/src/directives.tsx
import {
h as createElement,
cloneElement
} from "preact";
import { useContext, useLayoutEffect, useMemo, useRef } from "preact/hooks";
import { signal } from "@preact/signals";
import {
useWatch,
useInit,
kebabToCamelCase,
warn,
splitTask,
isPlainObject,
deepClone
} from "./utils";
import {
directive,
getEvaluate,
isDefaultDirectiveSuffix,
isNonDefaultDirectiveSuffix
} from "./hooks";
import { getScope, navigationContextSignal } from "./scopes";
import { proxifyState, proxifyContext, deepMerge } from "./proxies";
import { PENDING_GETTER } from "./proxies/state";
var warnUniqueIdWithTwoHyphens = (prefix, suffix, uniqueId) => {
if (globalThis.SCRIPT_DEBUG) {
warn(
`The usage of data-wp-${prefix}--${suffix}${uniqueId ? `--${uniqueId}` : ""} (two hyphens for unique ID) is deprecated and will stop working in WordPress 7.0. Please use data-wp-${prefix}${uniqueId ? `--${suffix}---${uniqueId}` : `---${suffix}`} (three hyphens for unique ID) from now on.`
);
}
};
var warnUniqueIdNotSupported = (prefix, uniqueId) => {
if (globalThis.SCRIPT_DEBUG) {
warn(
`Unique IDs are not supported for the data-wp-${prefix} directive. Ignoring the directive with unique ID "${uniqueId}".`
);
}
};
var warnWithSyncEvent = (wrongPrefix, rightPrefix) => {
if (globalThis.SCRIPT_DEBUG) {
warn(
`The usage of data-wp-${wrongPrefix} is deprecated and will stop working in WordPress 7.0. Please, use data-wp-${rightPrefix} with the withSyncEvent() helper from now on.`
);
}
};
function wrapEventAsync(event) {
const handler = {
get(target, prop, receiver) {
const value = target[prop];
switch (prop) {
case "currentTarget":
if (globalThis.SCRIPT_DEBUG) {
warn(
`Accessing the synchronous event.${prop} property in a store action without wrapping it in withSyncEvent() is deprecated and will stop working in WordPress 7.0. Please wrap the store action in withSyncEvent().`
);
}
break;
case "preventDefault":
case "stopImmediatePropagation":
case "stopPropagation":
if (globalThis.SCRIPT_DEBUG) {
warn(
`Using the synchronous event.${prop}() function in a store action without wrapping it in withSyncEvent() is deprecated and will stop working in WordPress 7.0. Please wrap the store action in withSyncEvent().`
);
}
break;
}
if (value instanceof Function) {
return function(...args) {
return value.apply(
this === receiver ? target : this,
args
);
};
}
return value;
}
};
return new Proxy(event, handler);
}
var newRule = /(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g;
var ruleClean = /\/\*[^]*?\*\/| +/g;
var ruleNewline = /\n+/g;
var empty = " ";
var cssStringToObject = (val) => {
const tree = [{}];
let block, left;
while (block = newRule.exec(val.replace(ruleClean, ""))) {
if (block[4]) {
tree.shift();
} else if (block[3]) {
left = block[3].replace(ruleNewline, empty).trim();
tree.unshift(tree[0][left] = tree[0][left] || {});
} else {
tree[0][block[1]] = block[2].replace(ruleNewline, empty).trim();
}
}
return tree[0];
};
var getGlobalEventDirective = (type) => {
return ({ directives, evaluate }) => {
directives[`on-${type}`].filter(isNonDefaultDirectiveSuffix).forEach((entry) => {
const suffixParts = entry.suffix.split("--", 2);
const eventName = suffixParts[0];
if (globalThis.SCRIPT_DEBUG) {
if (suffixParts[1]) {
warnUniqueIdWithTwoHyphens(
`on-${type}`,
suffixParts[0],
suffixParts[1]
);
}
}
useInit(() => {
const cb = (event) => {
const result = evaluate(entry);
if (typeof result === "function") {
if (!result?.sync) {
event = wrapEventAsync(event);
}
result(event);
}
};
const globalVar = type === "window" ? window : document;
globalVar.addEventListener(eventName, cb);
return () => globalVar.removeEventListener(eventName, cb);
});
});
};
};
var evaluateItemKey = (inheritedValue, namespace, item, itemProp, eachKey) => {
const clientContextWithItem = {
...inheritedValue.client,
[namespace]: {
...inheritedValue.client[namespace],
[itemProp]: item
}
};
const scope = {
...getScope(),
context: clientContextWithItem,
serverContext: inheritedValue.server
};
return eachKey ? getEvaluate({ scope })(eachKey) : item;
};
var useItemContexts = function* (inheritedValue, namespace, items, itemProp, eachKey) {
const { current: itemContexts } = useRef(/* @__PURE__ */ new Map());
for (const item of items) {
const key = evaluateItemKey(
inheritedValue,
namespace,
item,
itemProp,
eachKey
);
if (!itemContexts.has(key)) {
itemContexts.set(
key,
proxifyContext(
proxifyState(namespace, {
// Inits the item prop in the context to shadow it in case
// it was inherited from the parent context. The actual
// value is set in the `wp-each` directive later on.
[itemProp]: void 0
}),
inheritedValue.client[namespace]
)
);
}
yield [item, itemContexts.get(key), key];
}
};
var getGlobalAsyncEventDirective = (type) => {
return ({ directives, evaluate }) => {
directives[`on-async-${type}`].filter(isNonDefaultDirectiveSuffix).forEach((entry) => {
if (globalThis.SCRIPT_DEBUG) {
warnWithSyncEvent(`on-async-${type}`, `on-${type}`);
}
const eventName = entry.suffix.split("--", 1)[0];
useInit(() => {
const cb = async (event) => {
await splitTask();
const result = evaluate(entry);
if (typeof result === "function") {
result(event);
}
};
const globalVar = type === "window" ? window : document;
globalVar.addEventListener(eventName, cb, {
passive: true
});
return () => globalVar.removeEventListener(eventName, cb);
});
});
};
};
var routerRegions = /* @__PURE__ */ new Map();
var directives_default = () => {
directive(
"context",
({
directives: { context },
props: { children },
context: inheritedContext
}) => {
const entries = context.filter(isDefaultDirectiveSuffix).reverse();
if (!entries.length) {
if (globalThis.SCRIPT_DEBUG) {
warn(
"The usage of data-wp-context--unique-id (two hyphens) is not supported. To add a unique ID to the directive, please use data-wp-context---unique-id (three hyphens) instead."
);
}
return;
}
const { Provider } = inheritedContext;
const { client: inheritedClient, server: inheritedServer } = useContext(inheritedContext);
const client = useRef({});
const server = {};
const result = {
client: { ...inheritedClient },
server: { ...inheritedServer }
};
const namespaces = /* @__PURE__ */ new Set();
entries.forEach(({ value, namespace, uniqueId }) => {
if (!isPlainObject(value)) {
if (globalThis.SCRIPT_DEBUG) {
warn(
`The value of data-wp-context${uniqueId ? `---${uniqueId}` : ""} on the ${namespace} namespace must be a valid stringified JSON object.`
);
}
return;
}
if (!client.current[namespace]) {
client.current[namespace] = proxifyState(namespace, {});
}
deepMerge(
client.current[namespace],
deepClone(value),
false
);
server[namespace] = value;
namespaces.add(namespace);
});
namespaces.forEach((namespace) => {
result.client[namespace] = proxifyContext(
client.current[namespace],
inheritedClient[namespace]
);
result.server[namespace] = proxifyContext(
server[namespace],
inheritedServer[namespace]
);
});
return createElement(Provider, { value: result }, children);
},
{ priority: 5 }
);
directive("watch", ({ directives: { watch }, evaluate }) => {
watch.forEach((entry) => {
if (globalThis.SCRIPT_DEBUG) {
if (entry.suffix) {
warnUniqueIdWithTwoHyphens("watch", entry.suffix);
}
}
useWatch(() => {
let start;
if (globalThis.IS_GUTENBERG_PLUGIN) {
if (globalThis.SCRIPT_DEBUG) {
start = performance.now();
}
}
let result = evaluate(entry);
if (typeof result === "function") {
result = result();
}
if (globalThis.IS_GUTENBERG_PLUGIN) {
if (globalThis.SCRIPT_DEBUG) {
performance.measure(
`interactivity api watch ${entry.namespace}`,
{
start,
end: performance.now(),
detail: {
devtools: {
track: `IA: watch ${entry.namespace}`
}
}
}
);
}
}
return result;
});
});
});
directive("init", ({ directives: { init }, evaluate }) => {
init.forEach((entry) => {
if (globalThis.SCRIPT_DEBUG) {
if (entry.suffix) {
warnUniqueIdWithTwoHyphens("init", entry.suffix);
}
}
useInit(() => {
let start;
if (globalThis.IS_GUTENBERG_PLUGIN) {
if (globalThis.SCRIPT_DEBUG) {
start = performance.now();
}
}
let result = evaluate(entry);
if (typeof result === "function") {
result = result();
}
if (globalThis.IS_GUTENBERG_PLUGIN) {
if (globalThis.SCRIPT_DEBUG) {
performance.measure(
`interactivity api init ${entry.namespace}`,
{
start,
end: performance.now(),
detail: {
devtools: {
track: `IA: init ${entry.namespace}`
}
}
}
);
}
}
return result;
});
});
});
directive("on", ({ directives: { on }, element, evaluate }) => {
const events = /* @__PURE__ */ new Map();
on.filter(isNonDefaultDirectiveSuffix).forEach((entry) => {
const suffixParts = entry.suffix.split("--", 2);
if (globalThis.SCRIPT_DEBUG) {
if (suffixParts[1]) {
warnUniqueIdWithTwoHyphens(
"on",
suffixParts[0],
suffixParts[1]
);
}
}
if (!events.has(suffixParts[0])) {
events.set(suffixParts[0], /* @__PURE__ */ new Set());
}
events.get(suffixParts[0]).add(entry);
});
events.forEach((entries, eventType) => {
const existingHandler = element.props[`on${eventType}`];
element.props[`on${eventType}`] = (event) => {
if (existingHandler) {
existingHandler(event);
}
entries.forEach((entry) => {
let start;
if (globalThis.IS_GUTENBERG_PLUGIN) {
if (globalThis.SCRIPT_DEBUG) {
start = performance.now();
}
}
const result = evaluate(entry);
if (typeof result === "function") {
if (!result?.sync) {
event = wrapEventAsync(event);
}
result(event);
}
if (globalThis.IS_GUTENBERG_PLUGIN) {
if (globalThis.SCRIPT_DEBUG) {
performance.measure(
`interactivity api on ${entry.namespace}`,
{
start,
end: performance.now(),
detail: {
devtools: {
track: `IA: on ${entry.namespace}`
}
}
}
);
}
}
});
};
});
});
directive(
"on-async",
({ directives: { "on-async": onAsync }, element, evaluate }) => {
if (globalThis.SCRIPT_DEBUG) {
warnWithSyncEvent("on-async", "on");
}
const events = /* @__PURE__ */ new Map();
onAsync.filter(isNonDefaultDirectiveSuffix).forEach((entry) => {
const event = entry.suffix.split("--", 1)[0];
if (!events.has(event)) {
events.set(event, /* @__PURE__ */ new Set());
}
events.get(event).add(entry);
});
events.forEach((entries, eventType) => {
const existingHandler = element.props[`on${eventType}`];
element.props[`on${eventType}`] = (event) => {
if (existingHandler) {
existingHandler(event);
}
entries.forEach(async (entry) => {
await splitTask();
const result = evaluate(entry);
if (typeof result === "function") {
result(event);
}
});
};
});
}
);
directive("on-window", getGlobalEventDirective("window"));
directive("on-document", getGlobalEventDirective("document"));
directive("on-async-window", getGlobalAsyncEventDirective("window"));
directive(
"on-async-document",
getGlobalAsyncEventDirective("document")
);
directive(
"class",
({ directives: { class: classNames }, element, evaluate }) => {
classNames.filter(isNonDefaultDirectiveSuffix).forEach((entry) => {
const className = entry.uniqueId ? `${entry.suffix}---${entry.uniqueId}` : entry.suffix;
let result = evaluate(entry);
if (result === PENDING_GETTER) {
return;
}
if (typeof result === "function") {
result = result();
}
const currentClass = element.props.class || "";
const classFinder = new RegExp(
`(^|\\s)${className}(\\s|$)`,
"g"
);
if (!result) {
element.props.class = currentClass.replace(classFinder, " ").trim();
} else if (!classFinder.test(currentClass)) {
element.props.class = currentClass ? `${currentClass} ${className}` : className;
}
useInit(() => {
if (!result) {
element.ref.current.classList.remove(className);
} else {
element.ref.current.classList.add(className);
}
});
});
}
);
directive("style", ({ directives: { style }, element, evaluate }) => {
style.filter(isNonDefaultDirectiveSuffix).forEach((entry) => {
if (entry.uniqueId) {
if (globalThis.SCRIPT_DEBUG) {
warnUniqueIdNotSupported("style", entry.uniqueId);
}
return;
}
const styleProp = entry.suffix;
let result = evaluate(entry);
if (result === PENDING_GETTER) {
return;
}
if (typeof result === "function") {
result = result();
}
element.props.style = element.props.style || {};
if (typeof element.props.style === "string") {
element.props.style = cssStringToObject(element.props.style);
}
if (!result) {
delete element.props.style[styleProp];
} else {
element.props.style[styleProp] = result;
}
useInit(() => {
if (!result) {
element.ref.current.style.removeProperty(styleProp);
} else {
element.ref.current.style.setProperty(styleProp, result);
}
});
});
});
directive("bind", ({ directives: { bind }, element, evaluate }) => {
bind.filter(isNonDefaultDirectiveSuffix).forEach((entry) => {
if (entry.uniqueId) {
if (globalThis.SCRIPT_DEBUG) {
warnUniqueIdNotSupported("bind", entry.uniqueId);
}
return;
}
const attribute = entry.suffix;
let result = evaluate(entry);
if (result === PENDING_GETTER) {
return;
}
if (typeof result === "function") {
result = result();
}
element.props[attribute] = result;
useInit(() => {
const el = element.ref.current;
if (attribute === "style") {
if (typeof result === "string") {
el.style.cssText = result;
}
return;
} else if (attribute !== "width" && attribute !== "height" && attribute !== "href" && attribute !== "list" && attribute !== "form" && /*
* The value for `tabindex` follows the parsing rules for an
* integer. If that fails, or if the attribute isn't present, then
* the browsers should "follow platform conventions to determine if
* the element should be considered as a focusable area",
* practically meaning that most elements get a default of `-1` (not
* focusable), but several also get a default of `0` (focusable in
* order after all elements with a positive `tabindex` value).
*
* @see https://html.spec.whatwg.org/#tabindex-value
*/
attribute !== "tabIndex" && attribute !== "download" && attribute !== "rowSpan" && attribute !== "colSpan" && attribute !== "role" && attribute in el) {
try {
el[attribute] = result === null || result === void 0 ? "" : result;
return;
} catch (err) {
}
}
if (result !== null && result !== void 0 && (result !== false || attribute[4] === "-")) {
el.setAttribute(attribute, result);
} else {
el.removeAttribute(attribute);
}
});
});
});
directive(
"ignore",
({
element: {
type: Type,
props: { innerHTML, ...rest }
}
}) => {
if (globalThis.SCRIPT_DEBUG) {
warn(
"The data-wp-ignore directive is deprecated and will be removed in version 7.0."
);
}
const cached = useMemo(() => innerHTML, []);
return createElement(Type, {
dangerouslySetInnerHTML: { __html: cached },
...rest
});
}
);
directive("text", ({ directives: { text }, element, evaluate }) => {
const entries = text.filter(isDefaultDirectiveSuffix);
if (!entries.length) {
if (globalThis.SCRIPT_DEBUG) {
warn(
"The usage of data-wp-text--suffix is not supported. Please use data-wp-text instead."
);
}
return;
}
entries.forEach((entry) => {
if (entry.uniqueId) {
if (globalThis.SCRIPT_DEBUG) {
warnUniqueIdNotSupported("text", entry.uniqueId);
}
return;
}
try {
let result = evaluate(entry);
if (result === PENDING_GETTER) {
return;
}
if (typeof result === "function") {
result = result();
}
element.props.children = typeof result === "object" ? null : result.toString();
} catch (e) {
element.props.children = null;
}
});
});
directive("run", ({ directives: { run }, evaluate }) => {
run.forEach((entry) => {
if (globalThis.SCRIPT_DEBUG) {
if (entry.suffix) {
warnUniqueIdWithTwoHyphens("run", entry.suffix);
}
}
let result = evaluate(entry);
if (typeof result === "function") {
result = result();
}
return result;
});
});
directive(
"each",
({
directives: { each, "each-key": eachKey },
context: inheritedContext,
element,
evaluate
}) => {
if (element.type !== "template") {
if (globalThis.SCRIPT_DEBUG) {
warn(
"The data-wp-each directive can only be used on <template> elements."
);
}
return;
}
const { Provider } = inheritedContext;
const inheritedValue = useContext(inheritedContext);
const [entry] = each;
const { namespace, suffix, uniqueId } = entry;
if (each.length > 1) {
if (globalThis.SCRIPT_DEBUG) {
warn(
"The usage of multiple data-wp-each directives on the same element is not supported. Please pick only one."
);
}
return;
}
if (uniqueId) {
if (globalThis.SCRIPT_DEBUG) {
warnUniqueIdNotSupported("each", uniqueId);
}
return;
}
let iterable = evaluate(entry);
if (iterable === PENDING_GETTER) {
return;
}
if (typeof iterable === "function") {
iterable = iterable();
}
if (typeof iterable?.[Symbol.iterator] !== "function") {
return;
}
const itemProp = suffix ? kebabToCamelCase(suffix) : "item";
const result = [];
const itemContexts = useItemContexts(
inheritedValue,
namespace,
iterable,
itemProp,
eachKey?.[0]
);
for (const [item, itemContext, key] of itemContexts) {
const mergedContext = {
client: {
...inheritedValue.client,
[namespace]: itemContext
},
server: { ...inheritedValue.server }
};
mergedContext.client[namespace][itemProp] = item;
result.push(
createElement(
Provider,
{ value: mergedContext, key },
element.props.content
)
);
}
return result;
},
{ priority: 20 }
);
directive(
"each-child",
({ directives: { "each-child": eachChild }, element, evaluate }) => {
const entry = eachChild.find(isDefaultDirectiveSuffix);
if (!entry) {
return;
}
const iterable = evaluate(entry);
return iterable === PENDING_GETTER ? element : null;
},
{ priority: 1 }
);
directive(
"router-region",
({ directives: { "router-region": routerRegion } }) => {
const entry = routerRegion.find(isDefaultDirectiveSuffix);
if (!entry) {
return;
}
if (entry.suffix) {
if (globalThis.SCRIPT_DEBUG) {
warn(
`Suffixes for the data-wp-router-region directive are not supported. Ignoring the directive with suffix "${entry.suffix}".`
);
}
return;
}
if (entry.uniqueId) {
if (globalThis.SCRIPT_DEBUG) {
warnUniqueIdNotSupported("router-region", entry.uniqueId);
}
return;
}
const regionId = typeof entry.value === "string" ? entry.value : entry.value.id;
if (!routerRegions.has(regionId)) {
routerRegions.set(regionId, signal());
}
const vdom = routerRegions.get(regionId).value;
useLayoutEffect(() => {
if (vdom && typeof vdom.type !== "string") {
navigationContextSignal.value = navigationContextSignal.peek() + 1;
}
}, [vdom]);
if (vdom && typeof vdom.type !== "string") {
const previousScope = getScope();
return cloneElement(vdom, { previousScope });
}
return vdom;
},
{ priority: 1 }
);
};
export {
directives_default as default,
routerRegions
};
//# sourceMappingURL=directives.js.map