Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@wordpress/interactivity

Package Overview
Dependencies
Maintainers
25
Versions
74
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@wordpress/interactivity - npm Package Compare versions

Comparing version 2.7.0 to 3.0.0

build-types/constants.d.ts

179

build-module/directives.js

@@ -15,2 +15,3 @@ import { createElement, Fragment } from "react";

import { SlotProvider, Slot, Fill } from './slots';
import { navigate } from './router';
const isObject = item => item && typeof item === 'object' && !Array.isArray(item);

@@ -30,5 +31,3 @@ const mergeDeepSignals = (target, source, overwrite) => {

directives: {
context: {
default: newContext
}
context
},

@@ -45,8 +44,13 @@ props: {

const currentValue = useRef(deepSignal({}));
const passedValues = context.map(({
value
}) => value);
currentValue.current = useMemo(() => {
const newValue = deepSignal(newContext);
const newValue = context.map(c => deepSignal({
[c.namespace]: c.value
})).reduceRight(mergeDeepSignals);
mergeDeepSignals(newValue, inheritedValue);
mergeDeepSignals(currentValue.current, newValue, true);
return currentValue.current;
}, [newContext, inheritedValue]);
}, [inheritedValue, ...passedValues]);
return createElement(Provider, {

@@ -68,17 +72,11 @@ value: currentValue.current

// data-wp-effect--[name]
directive('effect', ({
// data-wp-watch--[name]
directive('watch', ({
directives: {
effect
watch
},
context,
evaluate
}) => {
const contextValue = useContext(context);
Object.values(effect).forEach(path => {
useSignalEffect(() => {
return evaluate(path, {
context: contextValue
});
});
watch.forEach(entry => {
useSignalEffect(() => evaluate(entry));
});

@@ -92,12 +90,6 @@ });

},
context,
evaluate
}) => {
const contextValue = useContext(context);
Object.values(init).forEach(path => {
useEffect(() => {
return evaluate(path, {
context: contextValue
});
}, []);
init.forEach(entry => {
useEffect(() => evaluate(entry), []);
});

@@ -112,12 +104,7 @@ });

element,
evaluate,
context
evaluate
}) => {
const contextValue = useContext(context);
Object.entries(on).forEach(([name, path]) => {
element.props[`on${name}`] = event => {
evaluate(path, {
event,
context: contextValue
});
on.forEach(entry => {
element.props[`on${entry.suffix}`] = event => {
evaluate(entry, event);
};

@@ -133,10 +120,10 @@ });

element,
evaluate,
context
evaluate
}) => {
const contextValue = useContext(context);
Object.keys(className).filter(n => n !== 'default').forEach(name => {
const result = evaluate(className[name], {
className: name,
context: contextValue
className.filter(({
suffix
}) => suffix !== 'default').forEach(entry => {
const name = entry.suffix;
const result = evaluate(entry, {
className: name
});

@@ -194,10 +181,10 @@ const currentClass = element.props.class || '';

element,
evaluate,
context
evaluate
}) => {
const contextValue = useContext(context);
Object.keys(style).filter(n => n !== 'default').forEach(key => {
const result = evaluate(style[key], {
key,
context: contextValue
style.filter(({
suffix
}) => suffix !== 'default').forEach(entry => {
const key = entry.suffix;
const result = evaluate(entry, {
key
});

@@ -226,10 +213,9 @@ element.props.style = element.props.style || {};

element,
context,
evaluate
}) => {
const contextValue = useContext(context);
Object.entries(bind).filter(n => n !== 'default').forEach(([attribute, path]) => {
const result = evaluate(path, {
context: contextValue
});
bind.filter(({
suffix
}) => suffix !== 'default').forEach(entry => {
const attribute = entry.suffix;
const result = evaluate(entry);
element.props[attribute] = result;

@@ -277,2 +263,46 @@ // Preact doesn't handle the `role` attribute properly, as it doesn't remove it when `null`.

// data-wp-navigation-link
directive('navigation-link', ({
directives: {
'navigation-link': navigationLink
},
props: {
href
},
element
}) => {
const {
value: link
} = navigationLink.find(({
suffix
}) => suffix === 'default');
useEffect(() => {
// Prefetch the page if it is in the directive options.
if (link?.prefetch) {
// prefetch( href );
}
});
// Don't do anything if it's falsy.
if (link !== false) {
element.props.onclick = async event => {
event.preventDefault();
// Fetch the page (or return it from cache).
await navigate(href);
// Update the scroll, depending on the option. True by default.
if (link?.scroll === 'smooth') {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
});
} else if (link?.scroll !== false) {
window.scrollTo(0, 0);
}
};
}
});
// data-wp-ignore

@@ -301,14 +331,11 @@ directive('ignore', ({

directives: {
text: {
default: text
}
text
},
element,
evaluate,
context
evaluate
}) => {
const contextValue = useContext(context);
element.props.children = evaluate(text, {
context: contextValue
});
const entry = text.find(({
suffix
}) => suffix === 'default');
element.props.children = evaluate(entry);
});

@@ -319,5 +346,3 @@

directives: {
slot: {
default: slot
}
slot
},

@@ -329,4 +354,9 @@ props: {

}) => {
const name = typeof slot === 'string' ? slot : slot.name;
const position = slot.position || 'children';
const {
value
} = slot.find(({
suffix
}) => suffix === 'default');
const name = typeof value === 'string' ? value : value.name;
const position = value.position || 'children';
if (position === 'before') {

@@ -359,5 +389,3 @@ return createElement(Fragment, null, createElement(Slot, {

directives: {
fill: {
default: fill
}
fill
},

@@ -367,9 +395,8 @@ props: {

},
evaluate,
context
evaluate
}) => {
const contextValue = useContext(context);
const slot = evaluate(fill, {
context: contextValue
});
const entry = fill.find(({
suffix
}) => suffix === 'default');
const slot = evaluate(entry);
return createElement(Fill, {

@@ -376,0 +403,0 @@ slot: slot

import { createElement } from "react";
// @ts-nocheck
/**

@@ -6,7 +8,8 @@ * External dependencies

import { h, options, createContext, cloneElement } from 'preact';
import { useRef, useCallback } from 'preact/hooks';
import { useRef, useCallback, useContext } from 'preact/hooks';
import { deepSignal } from 'deepsignal';
/**
* Internal dependencies
*/
import { rawStore as store } from './store';
import { stores } from './store';

@@ -42,2 +45,54 @@ /** @typedef {import('preact').VNode} VNode */

// Wrap the element props to prevent modifications.
const immutableMap = new WeakMap();
const immutableError = () => {
throw new Error('Please use `data-wp-bind` to modify the attributes of an element.');
};
const immutableHandlers = {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver);
return !!value && typeof value === 'object' ? deepImmutable(value) : value;
},
set: immutableError,
deleteProperty: immutableError
};
const deepImmutable = target => {
if (!immutableMap.has(target)) immutableMap.set(target, new Proxy(target, immutableHandlers));
return immutableMap.get(target);
};
// Store stacks for the current scope and the default namespaces and export APIs
// to interact with them.
const scopeStack = [];
const namespaceStack = [];
export const getContext = namespace => getScope()?.context[namespace || namespaceStack.slice(-1)[0]];
export const getElement = () => {
if (!getScope()) {
throw Error('Cannot call `getElement()` outside getters and actions used by directives.');
}
const {
ref,
state,
props
} = getScope();
return Object.freeze({
ref: ref.current,
state,
props: deepImmutable(props)
});
};
export const getScope = () => scopeStack.slice(-1)[0];
export const setScope = scope => {
scopeStack.push(scope);
};
export const resetScope = () => {
scopeStack.pop();
};
export const setNamespace = namespace => {
namespaceStack.push(namespace);
};
export const resetNamespace = () => {
namespaceStack.pop();
};
// WordPress Directives.

@@ -120,6 +175,6 @@ const directiveCallbacks = {};

// Resolve the path to some property of the store object.
const resolve = (path, ctx) => {
const resolve = (path, namespace) => {
let current = {
...store,
context: ctx
...stores.get(namespace),
context: getScope().context[namespace]
};

@@ -132,13 +187,15 @@ path.split('.').forEach(p => current = current[p]);

const getEvaluate = ({
ref
} = {}) => (path, extraArgs = {}) => {
scope
} = {}) => (entry, ...args) => {
let {
value: path,
namespace
} = entry;
// If path starts with !, remove it and save a flag.
const hasNegationOperator = path[0] === '!' && !!(path = path.slice(1));
const value = resolve(path, extraArgs.context);
const returnValue = typeof value === 'function' ? value({
ref: ref.current,
...store,
...extraArgs
}) : value;
return hasNegationOperator ? !returnValue : returnValue;
setScope(scope);
const value = resolve(path, namespace);
const result = typeof value === 'function' ? value(...args) : value;
resetScope();
return hasNegationOperator ? !result : result;
};

@@ -159,3 +216,3 @@

// Priority level wrapper.
// Component that wraps each priority level of directives of an element.
const Directives = ({

@@ -165,20 +222,23 @@ directives,

element,
evaluate,
originalProps,
elemRef
previousScope = {}
}) => {
// Initialize the DOM reference.
// eslint-disable-next-line react-hooks/rules-of-hooks
elemRef = elemRef || useRef(null);
// Create a reference to the evaluate function using the DOM reference.
// eslint-disable-next-line react-hooks/rules-of-hooks, react-hooks/exhaustive-deps
evaluate = evaluate || useCallback(getEvaluate({
ref: elemRef
// 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 = useRef({}).current;
scope.evaluate = useCallback(getEvaluate({
scope
}), []);
scope.context = useContext(context);
/* eslint-disable react-hooks/rules-of-hooks */
scope.ref = previousScope.ref || useRef(null);
scope.state = previousScope.state || useRef(deepSignal({})).current;
/* eslint-enable react-hooks/rules-of-hooks */
// Create a fresh copy of the vnode element.
// Create a fresh copy of the vnode element and add the props to the scope.
element = cloneElement(element, {
ref: elemRef
ref: scope.ref
});
scope.props = element.props;

@@ -190,5 +250,4 @@ // Recursively render the wrapper for the next priority level.

element: element,
evaluate: evaluate,
originalProps: originalProps,
elemRef: elemRef
previousScope: scope
}) : element;

@@ -204,4 +263,5 @@ const props = {

context,
evaluate
evaluate: scope.evaluate
};
setScope(scope);
for (const directiveName of currentPriorityLevel) {

@@ -211,2 +271,3 @@ const wrapper = directiveCallbacks[directiveName]?.(directiveArgs);

}
resetScope();
return props.children;

@@ -221,3 +282,5 @@ };

const directives = props.__directives;
if (directives.key) vnode.key = directives.key.default;
if (directives.key) vnode.key = directives.key.find(({
suffix
}) => suffix === 'default').value;
delete props.__directives;

@@ -224,0 +287,0 @@ const priorityLevels = getPriorityLevels(directives);

@@ -6,5 +6,4 @@ /**

import { init } from './router';
import { rawStore, afterLoads } from './store';
export { store } from './store';
export { directive } from './hooks';
export { directive, getContext, getElement } from './hooks';
export { navigate, prefetch } from './router';

@@ -17,4 +16,3 @@ export { h as createElement } from 'preact';

await init();
afterLoads.forEach(afterLoad => afterLoad(rawStore));
});
//# sourceMappingURL=index.js.map

@@ -5,7 +5,18 @@ /**

import { deepSignal } from 'deepsignal';
const isObject = item => item && typeof item === 'object' && !Array.isArray(item);
import { computed } from '@preact/signals';
/**
* Internal dependencies
*/
import { getScope, setScope, resetScope, setNamespace, resetNamespace } from './hooks';
const isObject = item => !!item && typeof item === 'object' && !Array.isArray(item);
const deepMerge = (target, source) => {
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
const getter = Object.getOwnPropertyDescriptor(source, key)?.get;
if (typeof getter === 'function') {
Object.defineProperty(target, key, {
get: getter
});
} else if (isObject(source[key])) {
if (!target[key]) Object.assign(target, {

@@ -23,10 +34,8 @@ [key]: {}

};
const getSerializedState = () => {
const storeTag = document.querySelector(`script[type="application/json"]#wp-interactivity-store-data`);
if (!storeTag) return {};
const parseInitialState = () => {
const storeTag = document.querySelector(`script[type="application/json"]#wp-interactivity-initial-state`);
if (!storeTag?.textContent) return {};
try {
const {
state
} = JSON.parse(storeTag.textContent);
if (isObject(state)) return state;
const initialState = JSON.parse(storeTag.textContent);
if (isObject(initialState)) return initialState;
throw Error('Parsed state is not an object');

@@ -39,8 +48,102 @@ } catch (e) {

};
export const afterLoads = new Set();
const rawState = getSerializedState();
export const rawStore = {
state: deepSignal(rawState)
export const stores = new Map();
const rawStores = new Map();
const storeLocks = new Map();
const objToProxy = new WeakMap();
const proxyToNs = new WeakMap();
const scopeToGetters = new WeakMap();
const proxify = (obj, ns) => {
if (!objToProxy.has(obj)) {
const proxy = new Proxy(obj, handlers);
objToProxy.set(obj, proxy);
proxyToNs.set(proxy, ns);
}
return objToProxy.get(obj);
};
const handlers = {
get: (target, key, receiver) => {
const ns = proxyToNs.get(receiver);
// Check if the property is a getter and we are inside an scope. If that is
// the case, we clone the getter to avoid overwriting the scoped
// dependencies of the computed each time that getter runs.
const getter = Object.getOwnPropertyDescriptor(target, key)?.get;
if (getter) {
const scope = getScope();
if (scope) {
const getters = scopeToGetters.get(scope) || scopeToGetters.set(scope, new Map()).get(scope);
if (!getters.has(getter)) {
getters.set(getter, computed(() => {
setNamespace(ns);
setScope(scope);
try {
return getter.call(target);
} finally {
resetScope();
resetNamespace();
}
}));
}
return getters.get(getter).value;
}
}
const result = Reflect.get(target, key, receiver);
// Check if the proxy is the store root and no key with that name exist. In
// that case, return an empty object for the requested key.
if (typeof result === 'undefined' && receiver === stores.get(ns)) {
const obj = {};
Reflect.set(target, key, obj, receiver);
return proxify(obj, ns);
}
// Check if the property is a generator. If it is, we turn it into an
// asynchronous function where we restore the default namespace and scope
// each time it awaits/yields.
if (result?.constructor?.name === 'GeneratorFunction') {
return async (...args) => {
const scope = getScope();
const gen = result(...args);
let value;
let it;
while (true) {
setNamespace(ns);
setScope(scope);
try {
it = gen.next(value);
} finally {
resetScope();
resetNamespace();
}
try {
value = await it.value;
} catch (e) {
gen.throw(e);
}
if (it.done) break;
}
return value;
};
}
// Check if the property is a synchronous function. If it is, set the
// default namespace. Synchronous functions always run in the proper scope,
// which is set by the Directives component.
if (typeof result === 'function') {
return (...args) => {
setNamespace(ns);
try {
return result(...args);
} finally {
resetNamespace();
}
};
}
// Check if the property is an object. If it is, proxyify it.
if (isObject(result)) return proxify(result, ns);
return result;
}
};
/**

@@ -54,6 +157,2 @@ * @typedef StoreProps Properties object passed to `store`.

* @typedef StoreOptions Options object.
* @property {(store:any) => void} [afterLoad] Callback to be executed after the
* Interactivity API has been set up
* and the store is ready. It
* receives the store as argument.
*/

@@ -102,12 +201,55 @@

*/
export const store = ({
state,
const universalUnlock = 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.';
export function store(namespace, {
state = {},
...block
}, {
afterLoad
} = {}) => {
deepMerge(rawStore, block);
deepMerge(rawState, state);
if (afterLoad) afterLoads.add(afterLoad);
};
} = {}, {
lock = false
} = {}) {
if (!stores.has(namespace)) {
// Lock the store if the passed lock is different from the universal
// unlock. Once the lock is set (either false, true, or a given string),
// it cannot change.
if (lock !== universalUnlock) {
storeLocks.set(namespace, lock);
}
const rawStore = {
state: deepSignal(state),
...block
};
const proxiedStore = new Proxy(rawStore, handlers);
rawStores.set(namespace, rawStore);
stores.set(namespace, proxiedStore);
proxyToNs.set(proxiedStore, namespace);
} else {
// Lock the store if it wasn't locked yet and the passed lock is
// different from the universal unlock. If no lock is given, the store
// will be public and won't accept any lock from now on.
if (lock !== universalUnlock && !storeLocks.has(namespace)) {
storeLocks.set(namespace, lock);
} else {
const storeLock = storeLocks.get(namespace);
const isLockValid = lock === universalUnlock || lock !== true && lock === storeLock;
if (!isLockValid) {
if (!storeLock) {
throw Error('Cannot lock a public store');
} else {
throw Error('Cannot unlock a private store with an invalid lock code');
}
}
}
const target = rawStores.get(namespace);
deepMerge(target, block);
deepMerge(target.state, state);
}
return stores.get(namespace);
}
// Parse and populate the initial state.
Object.entries(parseInitialState()).forEach(([namespace, state]) => {
store(namespace, {
state
});
});
//# sourceMappingURL=store.js.map

@@ -12,2 +12,3 @@ /**

const fullPrefix = `data-${p}-`;
let namespace = null;

@@ -27,2 +28,7 @@ // Regular expression for directive parsing.

// Regular expression for reference parsing. It can contain a namespace before
// the reference, separated by `::`, like `some-namespace::state.somePath`.
// Namespaces can contain any alphanumeric characters, hyphens, underscores or
// forward slashes. References don't have any restrictions.
const nsPathRegExp = /^([\w-_\/]+)::(.+)$/;
export const hydratedIslands = new WeakSet();

@@ -53,4 +59,3 @@

const children = [];
const directives = {};
let hasDirectives = false;
const directives = [];
let ignore = false;

@@ -63,13 +68,15 @@ let island = false;

ignore = true;
} else if (n === islandAttr) {
island = true;
} else {
hasDirectives = true;
let val = attributes[i].value;
var _nsPathRegExp$exec$sl;
let [ns, value] = (_nsPathRegExp$exec$sl = nsPathRegExp.exec(attributes[i].value)?.slice(1)) !== null && _nsPathRegExp$exec$sl !== void 0 ? _nsPathRegExp$exec$sl : [null, attributes[i].value];
try {
val = JSON.parse(val);
value = JSON.parse(value);
} catch (e) {}
const [, prefix, suffix] = directiveParser.exec(n);
directives[prefix] = directives[prefix] || {};
directives[prefix][suffix || 'default'] = val;
if (n === islandAttr) {
var _value$namespace;
island = true;
namespace = (_value$namespace = value?.namespace) !== null && _value$namespace !== void 0 ? _value$namespace : null;
} else {
directives.push([n, ns, value]);
}
}

@@ -89,3 +96,14 @@ } else if (n === 'ref') {

if (island) hydratedIslands.add(node);
if (hasDirectives) props.__directives = directives;
if (directives.length) {
props.__directives = directives.reduce((obj, [name, ns, value]) => {
const [, prefix, suffix = 'default'] = directiveParser.exec(name);
if (!obj[prefix]) obj[prefix] = [];
obj[prefix].push({
namespace: ns !== null && ns !== void 0 ? ns : namespace,
value,
suffix
});
return obj;
}, {});
}
let child = treeWalker.firstChild();

@@ -92,0 +110,0 @@ if (child) {

@@ -14,2 +14,3 @@ "use strict";

var _slots = require("./slots");
var _router = require("./router");
/**

@@ -37,5 +38,3 @@ * External dependencies

directives: {
context: {
default: newContext
}
context
},

@@ -52,8 +51,13 @@ props: {

const currentValue = (0, _hooks.useRef)((0, _deepsignal.deepSignal)({}));
const passedValues = context.map(({
value
}) => value);
currentValue.current = (0, _hooks.useMemo)(() => {
const newValue = (0, _deepsignal.deepSignal)(newContext);
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;
}, [newContext, inheritedValue]);
}, [inheritedValue, ...passedValues]);
return (0, _react.createElement)(Provider, {

@@ -75,17 +79,11 @@ value: currentValue.current

// data-wp-effect--[name]
(0, _hooks2.directive)('effect', ({
// data-wp-watch--[name]
(0, _hooks2.directive)('watch', ({
directives: {
effect
watch
},
context,
evaluate
}) => {
const contextValue = (0, _hooks.useContext)(context);
Object.values(effect).forEach(path => {
(0, _utils.useSignalEffect)(() => {
return evaluate(path, {
context: contextValue
});
});
watch.forEach(entry => {
(0, _utils.useSignalEffect)(() => evaluate(entry));
});

@@ -99,12 +97,6 @@ });

},
context,
evaluate
}) => {
const contextValue = (0, _hooks.useContext)(context);
Object.values(init).forEach(path => {
(0, _hooks.useEffect)(() => {
return evaluate(path, {
context: contextValue
});
}, []);
init.forEach(entry => {
(0, _hooks.useEffect)(() => evaluate(entry), []);
});

@@ -119,12 +111,7 @@ });

element,
evaluate,
context
evaluate
}) => {
const contextValue = (0, _hooks.useContext)(context);
Object.entries(on).forEach(([name, path]) => {
element.props[`on${name}`] = event => {
evaluate(path, {
event,
context: contextValue
});
on.forEach(entry => {
element.props[`on${entry.suffix}`] = event => {
evaluate(entry, event);
};

@@ -140,10 +127,10 @@ });

element,
evaluate,
context
evaluate
}) => {
const contextValue = (0, _hooks.useContext)(context);
Object.keys(className).filter(n => n !== 'default').forEach(name => {
const result = evaluate(className[name], {
className: name,
context: contextValue
className.filter(({
suffix
}) => suffix !== 'default').forEach(entry => {
const name = entry.suffix;
const result = evaluate(entry, {
className: name
});

@@ -201,10 +188,10 @@ const currentClass = element.props.class || '';

element,
evaluate,
context
evaluate
}) => {
const contextValue = (0, _hooks.useContext)(context);
Object.keys(style).filter(n => n !== 'default').forEach(key => {
const result = evaluate(style[key], {
key,
context: contextValue
style.filter(({
suffix
}) => suffix !== 'default').forEach(entry => {
const key = entry.suffix;
const result = evaluate(entry, {
key
});

@@ -233,10 +220,9 @@ element.props.style = element.props.style || {};

element,
context,
evaluate
}) => {
const contextValue = (0, _hooks.useContext)(context);
Object.entries(bind).filter(n => n !== 'default').forEach(([attribute, path]) => {
const result = evaluate(path, {
context: contextValue
});
bind.filter(({
suffix
}) => suffix !== 'default').forEach(entry => {
const attribute = entry.suffix;
const result = evaluate(entry);
element.props[attribute] = result;

@@ -284,2 +270,46 @@ // Preact doesn't handle the `role` attribute properly, as it doesn't remove it when `null`.

// data-wp-navigation-link
(0, _hooks2.directive)('navigation-link', ({
directives: {
'navigation-link': navigationLink
},
props: {
href
},
element
}) => {
const {
value: link
} = navigationLink.find(({
suffix
}) => suffix === 'default');
(0, _hooks.useEffect)(() => {
// Prefetch the page if it is in the directive options.
if (link?.prefetch) {
// prefetch( href );
}
});
// Don't do anything if it's falsy.
if (link !== false) {
element.props.onclick = async event => {
event.preventDefault();
// Fetch the page (or return it from cache).
await (0, _router.navigate)(href);
// Update the scroll, depending on the option. True by default.
if (link?.scroll === 'smooth') {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
});
} else if (link?.scroll !== false) {
window.scrollTo(0, 0);
}
};
}
});
// data-wp-ignore

@@ -308,14 +338,11 @@ (0, _hooks2.directive)('ignore', ({

directives: {
text: {
default: text
}
text
},
element,
evaluate,
context
evaluate
}) => {
const contextValue = (0, _hooks.useContext)(context);
element.props.children = evaluate(text, {
context: contextValue
});
const entry = text.find(({
suffix
}) => suffix === 'default');
element.props.children = evaluate(entry);
});

@@ -326,5 +353,3 @@

directives: {
slot: {
default: slot
}
slot
},

@@ -336,4 +361,9 @@ props: {

}) => {
const name = typeof slot === 'string' ? slot : slot.name;
const position = slot.position || 'children';
const {
value
} = slot.find(({
suffix
}) => suffix === 'default');
const name = typeof value === 'string' ? value : value.name;
const position = value.position || 'children';
if (position === 'before') {

@@ -366,5 +396,3 @@ return (0, _react.createElement)(_react.Fragment, null, (0, _react.createElement)(_slots.Slot, {

directives: {
fill: {
default: fill
}
fill
},

@@ -374,9 +402,8 @@ props: {

},
evaluate,
context
evaluate
}) => {
const contextValue = (0, _hooks.useContext)(context);
const slot = evaluate(fill, {
context: contextValue
});
const entry = fill.find(({
suffix
}) => suffix === 'default');
const slot = evaluate(entry);
return (0, _react.createElement)(_slots.Fill, {

@@ -383,0 +410,0 @@ slot: slot

@@ -6,7 +6,10 @@ "use strict";

});
exports.directive = void 0;
exports.setScope = exports.setNamespace = exports.resetScope = exports.resetNamespace = exports.getScope = exports.getElement = exports.getContext = exports.directive = void 0;
var _react = require("react");
var _preact = require("preact");
var _hooks = require("preact/hooks");
var _deepsignal = require("deepsignal");
var _store = require("./store");
// @ts-nocheck
/**

@@ -49,3 +52,62 @@ * External dependencies

// Wrap the element props to prevent modifications.
const immutableMap = new WeakMap();
const immutableError = () => {
throw new Error('Please use `data-wp-bind` to modify the attributes of an element.');
};
const immutableHandlers = {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver);
return !!value && typeof value === 'object' ? deepImmutable(value) : value;
},
set: immutableError,
deleteProperty: immutableError
};
const deepImmutable = target => {
if (!immutableMap.has(target)) immutableMap.set(target, new Proxy(target, immutableHandlers));
return immutableMap.get(target);
};
// Store stacks for the current scope and the default namespaces and export APIs
// to interact with them.
const scopeStack = [];
const namespaceStack = [];
const getContext = namespace => getScope()?.context[namespace || namespaceStack.slice(-1)[0]];
exports.getContext = getContext;
const getElement = () => {
if (!getScope()) {
throw Error('Cannot call `getElement()` outside getters and actions used by directives.');
}
const {
ref,
state,
props
} = getScope();
return Object.freeze({
ref: ref.current,
state,
props: deepImmutable(props)
});
};
exports.getElement = getElement;
const getScope = () => scopeStack.slice(-1)[0];
exports.getScope = getScope;
const setScope = scope => {
scopeStack.push(scope);
};
exports.setScope = setScope;
const resetScope = () => {
scopeStack.pop();
};
exports.resetScope = resetScope;
const setNamespace = namespace => {
namespaceStack.push(namespace);
};
exports.setNamespace = setNamespace;
const resetNamespace = () => {
namespaceStack.pop();
};
// WordPress Directives.
exports.resetNamespace = resetNamespace;
const directiveCallbacks = {};

@@ -128,6 +190,6 @@ const directivePriorities = {};

exports.directive = directive;
const resolve = (path, ctx) => {
const resolve = (path, namespace) => {
let current = {
..._store.rawStore,
context: ctx
..._store.stores.get(namespace),
context: getScope().context[namespace]
};

@@ -140,13 +202,15 @@ path.split('.').forEach(p => current = current[p]);

const getEvaluate = ({
ref
} = {}) => (path, extraArgs = {}) => {
scope
} = {}) => (entry, ...args) => {
let {
value: path,
namespace
} = entry;
// If path starts with !, remove it and save a flag.
const hasNegationOperator = path[0] === '!' && !!(path = path.slice(1));
const value = resolve(path, extraArgs.context);
const returnValue = typeof value === 'function' ? value({
ref: ref.current,
..._store.rawStore,
...extraArgs
}) : value;
return hasNegationOperator ? !returnValue : returnValue;
setScope(scope);
const value = resolve(path, namespace);
const result = typeof value === 'function' ? value(...args) : value;
resetScope();
return hasNegationOperator ? !result : result;
};

@@ -167,3 +231,3 @@

// Priority level wrapper.
// Component that wraps each priority level of directives of an element.
const Directives = ({

@@ -173,20 +237,23 @@ directives,

element,
evaluate,
originalProps,
elemRef
previousScope = {}
}) => {
// Initialize the DOM reference.
// eslint-disable-next-line react-hooks/rules-of-hooks
elemRef = elemRef || (0, _hooks.useRef)(null);
// Create a reference to the evaluate function using the DOM reference.
// eslint-disable-next-line react-hooks/rules-of-hooks, react-hooks/exhaustive-deps
evaluate = evaluate || (0, _hooks.useCallback)(getEvaluate({
ref: elemRef
// 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
}), []);
scope.context = (0, _hooks.useContext)(context);
/* eslint-disable react-hooks/rules-of-hooks */
scope.ref = previousScope.ref || (0, _hooks.useRef)(null);
scope.state = previousScope.state || (0, _hooks.useRef)((0, _deepsignal.deepSignal)({})).current;
/* eslint-enable react-hooks/rules-of-hooks */
// Create a fresh copy of the vnode element.
// Create a fresh copy of the vnode element and add the props to the scope.
element = (0, _preact.cloneElement)(element, {
ref: elemRef
ref: scope.ref
});
scope.props = element.props;

@@ -198,5 +265,4 @@ // Recursively render the wrapper for the next priority level.

element: element,
evaluate: evaluate,
originalProps: originalProps,
elemRef: elemRef
previousScope: scope
}) : element;

@@ -212,4 +278,5 @@ const props = {

context,
evaluate
evaluate: scope.evaluate
};
setScope(scope);
for (const directiveName of currentPriorityLevel) {

@@ -219,2 +286,3 @@ const wrapper = directiveCallbacks[directiveName]?.(directiveArgs);

}
resetScope();
return props.children;

@@ -229,3 +297,5 @@ };

const directives = props.__directives;
if (directives.key) vnode.key = directives.key.default;
if (directives.key) vnode.key = directives.key.find(({
suffix
}) => suffix === 'default').value;
delete props.__directives;

@@ -232,0 +302,0 @@ const priorityLevels = getPriorityLevels(directives);

@@ -25,2 +25,14 @@ "use strict";

});
Object.defineProperty(exports, "getContext", {
enumerable: true,
get: function () {
return _hooks.getContext;
}
});
Object.defineProperty(exports, "getElement", {
enumerable: true,
get: function () {
return _hooks.getElement;
}
});
Object.defineProperty(exports, "navigate", {

@@ -76,4 +88,3 @@ enumerable: true,

await (0, _router.init)();
_store.afterLoads.forEach(afterLoad => afterLoad(_store.rawStore));
});
//# sourceMappingURL=index.js.map

@@ -6,4 +6,7 @@ "use strict";

});
exports.store = exports.rawStore = exports.afterLoads = void 0;
exports.store = store;
exports.stores = void 0;
var _deepsignal = require("deepsignal");
var _signals = require("@preact/signals");
var _hooks = require("./hooks");
/**

@@ -13,7 +16,16 @@ * External dependencies

const isObject = item => item && typeof item === 'object' && !Array.isArray(item);
/**
* Internal dependencies
*/
const isObject = item => !!item && typeof item === 'object' && !Array.isArray(item);
const deepMerge = (target, source) => {
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
const getter = Object.getOwnPropertyDescriptor(source, key)?.get;
if (typeof getter === 'function') {
Object.defineProperty(target, key, {
get: getter
});
} else if (isObject(source[key])) {
if (!target[key]) Object.assign(target, {

@@ -31,10 +43,8 @@ [key]: {}

};
const getSerializedState = () => {
const storeTag = document.querySelector(`script[type="application/json"]#wp-interactivity-store-data`);
if (!storeTag) return {};
const parseInitialState = () => {
const storeTag = document.querySelector(`script[type="application/json"]#wp-interactivity-initial-state`);
if (!storeTag?.textContent) return {};
try {
const {
state
} = JSON.parse(storeTag.textContent);
if (isObject(state)) return state;
const initialState = JSON.parse(storeTag.textContent);
if (isObject(initialState)) return initialState;
throw Error('Parsed state is not an object');

@@ -47,9 +57,103 @@ } catch (e) {

};
const afterLoads = new Set();
exports.afterLoads = afterLoads;
const rawState = getSerializedState();
const rawStore = {
state: (0, _deepsignal.deepSignal)(rawState)
const stores = new Map();
exports.stores = stores;
const rawStores = new Map();
const storeLocks = new Map();
const objToProxy = new WeakMap();
const proxyToNs = new WeakMap();
const scopeToGetters = new WeakMap();
const proxify = (obj, ns) => {
if (!objToProxy.has(obj)) {
const proxy = new Proxy(obj, handlers);
objToProxy.set(obj, proxy);
proxyToNs.set(proxy, ns);
}
return objToProxy.get(obj);
};
const handlers = {
get: (target, key, receiver) => {
const ns = proxyToNs.get(receiver);
// Check if the property is a getter and we are inside an scope. If that is
// the case, we clone the getter to avoid overwriting the scoped
// dependencies of the computed each time that getter runs.
const getter = Object.getOwnPropertyDescriptor(target, key)?.get;
if (getter) {
const scope = (0, _hooks.getScope)();
if (scope) {
const getters = scopeToGetters.get(scope) || scopeToGetters.set(scope, new Map()).get(scope);
if (!getters.has(getter)) {
getters.set(getter, (0, _signals.computed)(() => {
(0, _hooks.setNamespace)(ns);
(0, _hooks.setScope)(scope);
try {
return getter.call(target);
} finally {
(0, _hooks.resetScope)();
(0, _hooks.resetNamespace)();
}
}));
}
return getters.get(getter).value;
}
}
const result = Reflect.get(target, key, receiver);
// Check if the proxy is the store root and no key with that name exist. In
// that case, return an empty object for the requested key.
if (typeof result === 'undefined' && receiver === stores.get(ns)) {
const obj = {};
Reflect.set(target, key, obj, receiver);
return proxify(obj, ns);
}
// Check if the property is a generator. If it is, we turn it into an
// asynchronous function where we restore the default namespace and scope
// each time it awaits/yields.
if (result?.constructor?.name === 'GeneratorFunction') {
return async (...args) => {
const scope = (0, _hooks.getScope)();
const gen = result(...args);
let value;
let it;
while (true) {
(0, _hooks.setNamespace)(ns);
(0, _hooks.setScope)(scope);
try {
it = gen.next(value);
} finally {
(0, _hooks.resetScope)();
(0, _hooks.resetNamespace)();
}
try {
value = await it.value;
} catch (e) {
gen.throw(e);
}
if (it.done) break;
}
return value;
};
}
// Check if the property is a synchronous function. If it is, set the
// default namespace. Synchronous functions always run in the proper scope,
// which is set by the Directives component.
if (typeof result === 'function') {
return (...args) => {
(0, _hooks.setNamespace)(ns);
try {
return result(...args);
} finally {
(0, _hooks.resetNamespace)();
}
};
}
// Check if the property is an object. If it is, proxyify it.
if (isObject(result)) return proxify(result, ns);
return result;
}
};
/**

@@ -63,6 +167,2 @@ * @typedef StoreProps Properties object passed to `store`.

* @typedef StoreOptions Options object.
* @property {(store:any) => void} [afterLoad] Callback to be executed after the
* Interactivity API has been set up
* and the store is ready. It
* receives the store as argument.
*/

@@ -111,14 +211,55 @@

*/
exports.rawStore = rawStore;
const store = ({
state,
const universalUnlock = 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.';
function store(namespace, {
state = {},
...block
}, {
afterLoad
} = {}) => {
deepMerge(rawStore, block);
deepMerge(rawState, state);
if (afterLoad) afterLoads.add(afterLoad);
};
exports.store = store;
} = {}, {
lock = false
} = {}) {
if (!stores.has(namespace)) {
// Lock the store if the passed lock is different from the universal
// unlock. Once the lock is set (either false, true, or a given string),
// it cannot change.
if (lock !== universalUnlock) {
storeLocks.set(namespace, lock);
}
const rawStore = {
state: (0, _deepsignal.deepSignal)(state),
...block
};
const proxiedStore = new Proxy(rawStore, handlers);
rawStores.set(namespace, rawStore);
stores.set(namespace, proxiedStore);
proxyToNs.set(proxiedStore, namespace);
} else {
// Lock the store if it wasn't locked yet and the passed lock is
// different from the universal unlock. If no lock is given, the store
// will be public and won't accept any lock from now on.
if (lock !== universalUnlock && !storeLocks.has(namespace)) {
storeLocks.set(namespace, lock);
} else {
const storeLock = storeLocks.get(namespace);
const isLockValid = lock === universalUnlock || lock !== true && lock === storeLock;
if (!isLockValid) {
if (!storeLock) {
throw Error('Cannot lock a public store');
} else {
throw Error('Cannot unlock a private store with an invalid lock code');
}
}
}
const target = rawStores.get(namespace);
deepMerge(target, block);
deepMerge(target.state, state);
}
return stores.get(namespace);
}
// Parse and populate the initial state.
Object.entries(parseInitialState()).forEach(([namespace, state]) => {
store(namespace, {
state
});
});
//# sourceMappingURL=store.js.map

@@ -21,2 +21,3 @@ "use strict";

const fullPrefix = `data-${_constants.directivePrefix}-`;
let namespace = null;

@@ -36,2 +37,7 @@ // Regular expression for directive parsing.

// Regular expression for reference parsing. It can contain a namespace before
// the reference, separated by `::`, like `some-namespace::state.somePath`.
// Namespaces can contain any alphanumeric characters, hyphens, underscores or
// forward slashes. References don't have any restrictions.
const nsPathRegExp = /^([\w-_\/]+)::(.+)$/;
const hydratedIslands = new WeakSet();

@@ -63,4 +69,3 @@

const children = [];
const directives = {};
let hasDirectives = false;
const directives = [];
let ignore = false;

@@ -73,13 +78,15 @@ let island = false;

ignore = true;
} else if (n === islandAttr) {
island = true;
} else {
hasDirectives = true;
let val = attributes[i].value;
var _nsPathRegExp$exec$sl;
let [ns, value] = (_nsPathRegExp$exec$sl = nsPathRegExp.exec(attributes[i].value)?.slice(1)) !== null && _nsPathRegExp$exec$sl !== void 0 ? _nsPathRegExp$exec$sl : [null, attributes[i].value];
try {
val = JSON.parse(val);
value = JSON.parse(value);
} catch (e) {}
const [, prefix, suffix] = directiveParser.exec(n);
directives[prefix] = directives[prefix] || {};
directives[prefix][suffix || 'default'] = val;
if (n === islandAttr) {
var _value$namespace;
island = true;
namespace = (_value$namespace = value?.namespace) !== null && _value$namespace !== void 0 ? _value$namespace : null;
} else {
directives.push([n, ns, value]);
}
}

@@ -99,3 +106,14 @@ } else if (n === 'ref') {

if (island) hydratedIslands.add(node);
if (hasDirectives) props.__directives = directives;
if (directives.length) {
props.__directives = directives.reduce((obj, [name, ns, value]) => {
const [, prefix, suffix = 'default'] = directiveParser.exec(name);
if (!obj[prefix]) obj[prefix] = [];
obj[prefix].push({
namespace: ns !== null && ns !== void 0 ? ns : namespace,
value,
suffix
});
return obj;
}, {});
}
let child = treeWalker.firstChild();

@@ -102,0 +120,0 @@ if (child) {

@@ -5,2 +5,8 @@ <!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/HEAD/packages#maintaining-changelogs. -->

## 3.0.0 (2023-11-29)
### Breaking Change
- Implement the new `store()` API as specified in the [proposal](https://github.com/WordPress/gutenberg/discussions/53586). ([#55459](https://github.com/WordPress/gutenberg/pull/55459))
## 2.7.0 (2023-11-16)

@@ -7,0 +13,0 @@

{
"name": "@wordpress/interactivity",
"version": "2.7.0",
"version": "3.0.0",
"description": "Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.",

@@ -27,2 +27,3 @@ "author": "The WordPress Contributors",

"react-native": "src/index",
"types": "build-types",
"dependencies": {

@@ -36,3 +37,3 @@ "@preact/signals": "^1.1.3",

},
"gitHead": "839018ff6029ba749780e288e08ff9cd898e50e8"
"gitHead": "d98dff8ea96f29cfea045bf964269f46f040d539"
}

@@ -20,2 +20,3 @@ /**

import { SlotProvider, Slot, Fill } from './slots';
import { navigate } from './router';

@@ -44,5 +45,3 @@ const isObject = ( item ) =>

( {
directives: {
context: { default: newContext },
},
directives: { context },
props: { children },

@@ -54,8 +53,13 @@ context: inheritedContext,

const currentValue = useRef( deepSignal( {} ) );
const passedValues = context.map( ( { value } ) => value );
currentValue.current = useMemo( () => {
const newValue = deepSignal( newContext );
const newValue = context
.map( ( c ) => deepSignal( { [ c.namespace ]: c.value } ) )
.reduceRight( mergeDeepSignals );
mergeDeepSignals( newValue, inheritedValue );
mergeDeepSignals( currentValue.current, newValue, true );
return currentValue.current;
}, [ newContext, inheritedValue ] );
}, [ inheritedValue, ...passedValues ] );

@@ -74,9 +78,6 @@ return (

// data-wp-effect--[name]
directive( 'effect', ( { directives: { effect }, context, evaluate } ) => {
const contextValue = useContext( context );
Object.values( effect ).forEach( ( path ) => {
useSignalEffect( () => {
return evaluate( path, { context: contextValue } );
} );
// data-wp-watch--[name]
directive( 'watch', ( { directives: { watch }, evaluate } ) => {
watch.forEach( ( entry ) => {
useSignalEffect( () => evaluate( entry ) );
} );

@@ -86,8 +87,5 @@ } );

// data-wp-init--[name]
directive( 'init', ( { directives: { init }, context, evaluate } ) => {
const contextValue = useContext( context );
Object.values( init ).forEach( ( path ) => {
useEffect( () => {
return evaluate( path, { context: contextValue } );
}, [] );
directive( 'init', ( { directives: { init }, evaluate } ) => {
init.forEach( ( entry ) => {
useEffect( () => evaluate( entry ), [] );
} );

@@ -97,7 +95,6 @@ } );

// data-wp-on--[event]
directive( 'on', ( { directives: { on }, element, evaluate, context } ) => {
const contextValue = useContext( context );
Object.entries( on ).forEach( ( [ name, path ] ) => {
element.props[ `on${ name }` ] = ( event ) => {
evaluate( path, { event, context: contextValue } );
directive( 'on', ( { directives: { on }, element, evaluate } ) => {
on.forEach( ( entry ) => {
element.props[ `on${ entry.suffix }` ] = ( event ) => {
evaluate( entry, event );
};

@@ -110,16 +107,8 @@ } );

'class',
( {
directives: { class: className },
element,
evaluate,
context,
} ) => {
const contextValue = useContext( context );
Object.keys( className )
.filter( ( n ) => n !== 'default' )
.forEach( ( name ) => {
const result = evaluate( className[ name ], {
className: name,
context: contextValue,
} );
( { 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 || '';

@@ -189,107 +178,138 @@ const classFinder = new RegExp(

// data-wp-style--[style-key]
directive(
'style',
( { directives: { style }, element, evaluate, context } ) => {
const contextValue = useContext( context );
Object.keys( style )
.filter( ( n ) => n !== 'default' )
.forEach( ( key ) => {
const result = evaluate( style[ key ], {
key,
context: contextValue,
} );
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;
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;
useEffect( () => {
// 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;
}
}, [] );
} );
}
);
useEffect( () => {
// 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]
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;
// Preact doesn't handle the `role` attribute properly, as it doesn't remove it when `null`.
// We need this workaround until the following issue is solved:
// https://github.com/preactjs/preact/issues/4136
useLayoutEffect( () => {
if (
attribute === 'role' &&
( result === null || result === undefined )
) {
element.ref.current.removeAttribute( attribute );
}
}, [ attribute, result ] );
// This seems necessary because Preact doesn't change the attributes
// 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.
useEffect( () => {
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' &&
// Default value in browsers is `-1` and an empty string is
// cast to `0` instead
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-navigation-link
directive(
'bind',
( { directives: { bind }, element, context, evaluate } ) => {
const contextValue = useContext( context );
Object.entries( bind )
.filter( ( n ) => n !== 'default' )
.forEach( ( [ attribute, path ] ) => {
const result = evaluate( path, {
context: contextValue,
} );
element.props[ attribute ] = result;
// Preact doesn't handle the `role` attribute properly, as it doesn't remove it when `null`.
// We need this workaround until the following issue is solved:
// https://github.com/preactjs/preact/issues/4136
useLayoutEffect( () => {
if (
attribute === 'role' &&
( result === null || result === undefined )
) {
element.ref.current.removeAttribute( attribute );
}
}, [ attribute, result ] );
'navigation-link',
( {
directives: { 'navigation-link': navigationLink },
props: { href },
element,
} ) => {
const { value: link } = navigationLink.find(
( { suffix } ) => suffix === 'default'
);
// This seems necessary because Preact doesn't change the attributes
// 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.
useEffect( () => {
const el = element.ref.current;
useEffect( () => {
// Prefetch the page if it is in the directive options.
if ( link?.prefetch ) {
// prefetch( href );
}
} );
// 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' &&
// Default value in browsers is `-1` and an empty string is
// cast to `0` instead
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 );
}
}, [] );
} );
// Don't do anything if it's falsy.
if ( link !== false ) {
element.props.onclick = async ( event ) => {
event.preventDefault();
// Fetch the page (or return it from cache).
await navigate( href );
// Update the scroll, depending on the option. True by default.
if ( link?.scroll === 'smooth' ) {
window.scrollTo( {
top: 0,
left: 0,
behavior: 'smooth',
} );
} else if ( link?.scroll !== false ) {
window.scrollTo( 0, 0 );
}
};
}
}

@@ -319,18 +339,6 @@ );

// data-wp-text
directive(
'text',
( {
directives: {
text: { default: text },
},
element,
evaluate,
context,
} ) => {
const contextValue = useContext( context );
element.props.children = evaluate( text, {
context: contextValue,
} );
}
);
directive( 'text', ( { directives: { text }, element, evaluate } ) => {
const entry = text.find( ( { suffix } ) => suffix === 'default' );
element.props.children = evaluate( entry );
} );

@@ -340,11 +348,8 @@ // data-wp-slot

'slot',
( {
directives: {
slot: { default: slot },
},
props: { children },
element,
} ) => {
const name = typeof slot === 'string' ? slot : slot.name;
const position = slot.position || 'children';
( { directives: { slot }, props: { children }, element } ) => {
const { value } = slot.find(
( { suffix } ) => suffix === 'default'
);
const name = typeof value === 'string' ? value : value.name;
const position = value.position || 'children';

@@ -382,12 +387,5 @@ if ( position === 'before' ) {

'fill',
( {
directives: {
fill: { default: fill },
},
props: { children },
evaluate,
context,
} ) => {
const contextValue = useContext( context );
const slot = evaluate( fill, { context: contextValue } );
( { directives: { fill }, props: { children }, evaluate } ) => {
const entry = fill.find( ( { suffix } ) => suffix === 'default' );
const slot = evaluate( entry );
return <Fill slot={ slot }>{ children }</Fill>;

@@ -394,0 +392,0 @@ },

@@ -6,5 +6,5 @@ /**

import { init } from './router';
import { rawStore, afterLoads } from './store';
export { store } from './store';
export { directive } from './hooks';
export { directive, getContext, getElement } from './hooks';
export { navigate, prefetch } from './router';

@@ -18,3 +18,2 @@ export { h as createElement } from 'preact';

await init();
afterLoads.forEach( ( afterLoad ) => afterLoad( rawStore ) );
} );

@@ -13,2 +13,3 @@ /**

const fullPrefix = `data-${ p }-`;
let namespace = null;

@@ -29,2 +30,8 @@ // Regular expression for directive parsing.

// Regular expression for reference parsing. It can contain a namespace before
// the reference, separated by `::`, like `some-namespace::state.somePath`.
// Namespaces can contain any alphanumeric characters, hyphens, underscores or
// forward slashes. References don't have any restrictions.
const nsPathRegExp = /^([\w-_\/]+)::(.+)$/;
export const hydratedIslands = new WeakSet();

@@ -56,4 +63,3 @@

const children = [];
const directives = {};
let hasDirectives = false;
const directives = [];
let ignore = false;

@@ -70,13 +76,15 @@ let island = false;

ignore = true;
} else if ( n === islandAttr ) {
island = true;
} else {
hasDirectives = true;
let val = attributes[ i ].value;
let [ ns, value ] = nsPathRegExp
.exec( attributes[ i ].value )
?.slice( 1 ) ?? [ null, attributes[ i ].value ];
try {
val = JSON.parse( val );
value = JSON.parse( value );
} catch ( e ) {}
const [ , prefix, suffix ] = directiveParser.exec( n );
directives[ prefix ] = directives[ prefix ] || {};
directives[ prefix ][ suffix || 'default' ] = val;
if ( n === islandAttr ) {
island = true;
namespace = value?.namespace ?? null;
} else {
directives.push( [ n, ns, value ] );
}
}

@@ -99,3 +107,18 @@ } else if ( n === 'ref' ) {

if ( hasDirectives ) props.__directives = directives;
if ( directives.length ) {
props.__directives = directives.reduce(
( obj, [ name, ns, value ] ) => {
const [ , prefix, suffix = 'default' ] =
directiveParser.exec( name );
if ( ! obj[ prefix ] ) obj[ prefix ] = [];
obj[ prefix ].push( {
namespace: ns ?? namespace,
value,
suffix,
} );
return obj;
},
{}
);
}

@@ -102,0 +125,0 @@ let child = treeWalker.firstChild();

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc