UNPKG

@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
// 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