UNPKG

@wordpress/interactivity

Version:

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

398 lines (379 loc) 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = require("react"); var _hooks = require("preact/hooks"); var _deepsignal = require("deepsignal"); var _portals = require("./portals"); var _utils = require("./utils"); var _hooks2 = require("./hooks"); /** * External dependencies */ /** * Internal dependencies */ const isObject = item => item && typeof item === 'object' && !Array.isArray(item); const mergeDeepSignals = (target, source, overwrite) => { for (const k in source) { if (isObject((0, _deepsignal.peek)(target, k)) && isObject((0, _deepsignal.peek)(source, k))) { mergeDeepSignals(target[`$${k}`].peek(), source[`$${k}`].peek(), overwrite); } else if (overwrite || typeof (0, _deepsignal.peek)(target, k) === 'undefined') { target[`$${k}`] = source[`$${k}`]; } } }; const newRule = /(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g; const ruleClean = /\/\*[^]*?\*\/| +/g; const ruleNewline = /\n+/g; const empty = ' '; /** * Convert a css style string into a object. * * Made by Cristian Bote (@cristianbote) for Goober. * https://unpkg.com/browse/goober@2.1.13/src/core/astish.js * * @param {string} val CSS string. * @return {Object} CSS object. */ const 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]; }; /** * Creates a directive that adds an event listener to the global window or * document object. * * @param {string} type 'window' or 'document' * @return {void} */ const getGlobalEventDirective = type => ({ directives, evaluate }) => { directives[`on-${type}`].filter(({ suffix }) => suffix !== 'default').forEach(entry => { (0, _utils.useInit)(() => { const cb = event => evaluate(entry, event); const globalVar = type === 'window' ? window : document; globalVar.addEventListener(entry.suffix, cb); return () => globalVar.removeEventListener(entry.suffix, cb); }, []); }); }; var _default = () => { // data-wp-context (0, _hooks2.directive)('context', ({ directives: { context }, props: { children }, context: inheritedContext }) => { const { Provider } = inheritedContext; const inheritedValue = (0, _hooks.useContext)(inheritedContext); const currentValue = (0, _hooks.useRef)((0, _deepsignal.deepSignal)({})); const passedValues = context.map(({ value }) => value); currentValue.current = (0, _hooks.useMemo)(() => { const newValue = context.map(c => (0, _deepsignal.deepSignal)({ [c.namespace]: c.value })).reduceRight(mergeDeepSignals); mergeDeepSignals(newValue, inheritedValue); mergeDeepSignals(currentValue.current, newValue, true); return currentValue.current; }, [inheritedValue, ...passedValues]); return (0, _react.createElement)(Provider, { value: currentValue.current }, children); }, { priority: 5 }); // data-wp-body (0, _hooks2.directive)('body', ({ props: { children } }) => { return (0, _portals.createPortal)(children, document.body); }); // data-wp-watch--[name] (0, _hooks2.directive)('watch', ({ directives: { watch }, evaluate }) => { watch.forEach(entry => { (0, _utils.useWatch)(() => evaluate(entry)); }); }); // data-wp-init--[name] (0, _hooks2.directive)('init', ({ directives: { init }, evaluate }) => { init.forEach(entry => { // TODO: Replace with useEffect to prevent unneeded scopes. (0, _utils.useInit)(() => evaluate(entry)); }); }); // data-wp-on--[event] (0, _hooks2.directive)('on', ({ directives: { on }, element, evaluate }) => { on.filter(({ suffix }) => suffix !== 'default').forEach(entry => { element.props[`on${entry.suffix}`] = event => { evaluate(entry, event); }; }); }); // data-wp-on-window--[event] (0, _hooks2.directive)('on-window', getGlobalEventDirective('window')); // data-wp-on-document--[event] (0, _hooks2.directive)('on-document', getGlobalEventDirective('document')); // data-wp-class--[classname] (0, _hooks2.directive)('class', ({ directives: { class: className }, element, evaluate }) => { className.filter(({ suffix }) => suffix !== 'default').forEach(entry => { const name = entry.suffix; const result = evaluate(entry, { className: name }); const currentClass = element.props.class || ''; const classFinder = new RegExp(`(^|\\s)${name}(\\s|$)`, 'g'); if (!result) element.props.class = currentClass.replace(classFinder, ' ').trim();else if (!classFinder.test(currentClass)) element.props.class = currentClass ? `${currentClass} ${name}` : name; (0, _utils.useInit)(() => { /* * This seems necessary because Preact doesn't change the class * names on the hydration, so we have to do it manually. It doesn't * need deps because it only needs to do it the first time. */ if (!result) { element.ref.current.classList.remove(name); } else { element.ref.current.classList.add(name); } }); }); }); // data-wp-style--[style-key] (0, _hooks2.directive)('style', ({ directives: { style }, element, evaluate }) => { style.filter(({ suffix }) => suffix !== 'default').forEach(entry => { const key = entry.suffix; const result = evaluate(entry, { key }); 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[key];else element.props.style[key] = result; (0, _utils.useInit)(() => { /* * This seems necessary because Preact doesn't change the styles on * the hydration, so we have to do it manually. It doesn't need deps * because it only needs to do it the first time. */ if (!result) { element.ref.current.style.removeProperty(key); } else { element.ref.current.style[key] = result; } }); }); }); // data-wp-bind--[attribute] (0, _hooks2.directive)('bind', ({ directives: { bind }, element, evaluate }) => { bind.filter(({ suffix }) => suffix !== 'default').forEach(entry => { const attribute = entry.suffix; const result = evaluate(entry); element.props[attribute] = result; /* * This is necessary because Preact doesn't change the attributes on the * hydration, so we have to do it manually. It only needs to do it the * first time. After that, Preact will handle the changes. */ (0, _utils.useInit)(() => { const el = element.ref.current; /* * We set the value directly to the corresponding HTMLElement instance * property excluding the following special cases. We follow Preact's * logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L110-L129 */ 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 === undefined ? '' : result; return; } catch (err) {} } /* * aria- and data- attributes have no boolean representation. * A `false` value is different from the attribute not being * present, so we can't remove it. * We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136 */ if (result !== null && result !== undefined && (result !== false || attribute[4] === '-')) { el.setAttribute(attribute, result); } else { el.removeAttribute(attribute); } }); }); }); // data-wp-ignore (0, _hooks2.directive)('ignore', ({ element: { type: Type, props: { innerHTML, ...rest } } }) => { // Preserve the initial inner HTML. const cached = (0, _hooks.useMemo)(() => innerHTML, []); return (0, _react.createElement)(Type, { dangerouslySetInnerHTML: { __html: cached }, ...rest }); }); // data-wp-text (0, _hooks2.directive)('text', ({ directives: { text }, element, evaluate }) => { const entry = text.find(({ suffix }) => suffix === 'default'); try { const result = evaluate(entry); element.props.children = typeof result === 'object' ? null : result.toString(); } catch (e) { element.props.children = null; } }); // data-wp-run (0, _hooks2.directive)('run', ({ directives: { run }, evaluate }) => { run.forEach(entry => evaluate(entry)); }); // data-wp-each--[item] (0, _hooks2.directive)('each', ({ directives: { each, 'each-key': eachKey }, context: inheritedContext, element, evaluate }) => { if (element.type !== 'template') return; const { Provider } = inheritedContext; const inheritedValue = (0, _hooks.useContext)(inheritedContext); const [entry] = each; const { namespace, suffix } = entry; const list = evaluate(entry); return list.map(item => { const mergedContext = (0, _deepsignal.deepSignal)({}); const itemProp = suffix === 'default' ? 'item' : suffix; const newValue = (0, _deepsignal.deepSignal)({ [namespace]: { [itemProp]: item } }); mergeDeepSignals(newValue, inheritedValue); mergeDeepSignals(mergedContext, newValue, true); const scope = { ...(0, _hooks2.getScope)(), context: mergedContext }; const key = eachKey ? (0, _hooks2.getEvaluate)({ scope })(eachKey[0]) : item; return (0, _react.createElement)(Provider, { value: mergedContext, key: key }, element.props.content); }); }, { priority: 20 }); (0, _hooks2.directive)('each-child', () => null); }; exports.default = _default; //# sourceMappingURL=directives.js.map