@wordpress/interactivity
Version:
Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.
336 lines (317 loc) • 11.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.proxifyState = exports.peek = exports.hasPropSignal = exports.deepMerge = void 0;
var _signals = require("@preact/signals");
var _registry = require("./registry");
var _signals2 = require("./signals");
var _namespaces = require("../namespaces");
var _utils = require("../utils");
/**
* External dependencies
*/
/**
* Internal dependencies
*/
/**
* Set of built-in symbols.
*/
const wellKnownSymbols = new Set(Object.getOwnPropertyNames(Symbol).map(key => Symbol[key]).filter(value => typeof value === 'symbol'));
/**
* Relates each proxy with a map of {@link PropSignal} instances, representing
* the proxy's accessed properties.
*/
const proxyToProps = new WeakMap();
/**
* Checks whether a {@link PropSignal | `PropSignal`} instance exists for the
* given property in the passed proxy.
*
* @param proxy Proxy of a state object or array.
* @param key The property key.
* @return `true` when it exists; false otherwise.
*/
const hasPropSignal = (proxy, key) => proxyToProps.has(proxy) && proxyToProps.get(proxy).has(key);
exports.hasPropSignal = hasPropSignal;
const readOnlyProxies = new WeakSet();
/**
* Returns the {@link PropSignal | `PropSignal`} instance associated with the
* specified prop in the passed proxy.
*
* The `PropSignal` instance is generated if it doesn't exist yet, using the
* `initial` parameter to initialize the internal signals.
*
* @param proxy Proxy of a state object or array.
* @param key The property key.
* @param initial Initial data for the `PropSignal` instance.
* @return The `PropSignal` instance.
*/
const getPropSignal = (proxy, key, initial) => {
if (!proxyToProps.has(proxy)) {
proxyToProps.set(proxy, new Map());
}
key = typeof key === 'number' ? `${key}` : key;
const props = proxyToProps.get(proxy);
if (!props.has(key)) {
const ns = (0, _registry.getNamespaceFromProxy)(proxy);
const prop = new _signals2.PropSignal(proxy);
props.set(key, prop);
if (initial) {
const {
get,
value
} = initial;
if (get) {
prop.setGetter(get);
} else {
const readOnly = readOnlyProxies.has(proxy);
prop.setValue((0, _registry.shouldProxy)(value) ? proxifyState(ns, value, {
readOnly
}) : value);
}
}
}
return props.get(key);
};
/**
* Relates each proxied object (i.e., the original object) with a signal that
* tracks changes in the number of properties.
*/
const objToIterable = new WeakMap();
/**
* When this flag is `true`, it avoids any signal subscription, overriding state
* props' "reactive" behavior.
*/
let peeking = false;
/**
* Handlers for reactive objects and arrays in the state.
*/
const stateHandlers = {
get(target, key, receiver) {
/*
* The property should not be reactive for the following cases:
* 1. While using the `peek` function to read the property.
* 2. The property exists but comes from the Object or Array prototypes.
* 3. The property key is a known symbol.
*/
if (peeking || !target.hasOwnProperty(key) && key in target || typeof key === 'symbol' && wellKnownSymbols.has(key)) {
return Reflect.get(target, key, receiver);
}
// At this point, the property should be reactive.
const desc = Object.getOwnPropertyDescriptor(target, key);
const prop = getPropSignal(receiver, key, desc);
const result = prop.getComputed().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') {
const ns = (0, _registry.getNamespaceFromProxy)(receiver);
return (...args) => {
(0, _namespaces.setNamespace)(ns);
try {
return result.call(receiver, ...args);
} finally {
(0, _namespaces.resetNamespace)();
}
};
}
return result;
},
set(target, key, value, receiver) {
if (readOnlyProxies.has(receiver)) {
return false;
}
(0, _namespaces.setNamespace)((0, _registry.getNamespaceFromProxy)(receiver));
try {
return Reflect.set(target, key, value, receiver);
} finally {
(0, _namespaces.resetNamespace)();
}
},
defineProperty(target, key, desc) {
if (readOnlyProxies.has((0, _registry.getProxyFromObject)(target))) {
return false;
}
const isNew = !(key in target);
const result = Reflect.defineProperty(target, key, desc);
if (result) {
const receiver = (0, _registry.getProxyFromObject)(target);
const prop = getPropSignal(receiver, key);
const {
get,
value
} = desc;
if (get) {
prop.setGetter(get);
} else {
const ns = (0, _registry.getNamespaceFromProxy)(receiver);
prop.setValue((0, _registry.shouldProxy)(value) ? proxifyState(ns, value) : value);
}
if (isNew && objToIterable.has(target)) {
objToIterable.get(target).value++;
}
/*
* Modify the `length` property value only if the related
* `PropSignal` exists, which means that there are subscriptions to
* this property.
*/
if (Array.isArray(target) && proxyToProps.get(receiver)?.has('length')) {
const length = getPropSignal(receiver, 'length');
length.setValue(target.length);
}
}
return result;
},
deleteProperty(target, key) {
if (readOnlyProxies.has((0, _registry.getProxyFromObject)(target))) {
return false;
}
const result = Reflect.deleteProperty(target, key);
if (result) {
const prop = getPropSignal((0, _registry.getProxyFromObject)(target), key);
prop.setValue(undefined);
if (objToIterable.has(target)) {
objToIterable.get(target).value++;
}
}
return result;
},
ownKeys(target) {
if (!objToIterable.has(target)) {
objToIterable.set(target, (0, _signals.signal)(0));
}
/*
*This subscribes to the signal while preventing the minifier from
* deleting this line in production.
*/
objToIterable._ = objToIterable.get(target).value;
return Reflect.ownKeys(target);
}
};
/**
* Returns the proxy associated with the given state object, creating it if it
* does not exist.
*
* @param namespace The namespace that will be associated to this proxy.
* @param obj The object to proxify.
* @param options Options.
* @param options.readOnly Read-only.
*
* @throws Error if the object cannot be proxified. Use {@link shouldProxy} to
* check if a proxy can be created for a specific object.
*
* @return The associated proxy.
*/
const proxifyState = (namespace, obj, options) => {
const proxy = (0, _registry.createProxy)(namespace, obj, stateHandlers);
if (options?.readOnly) {
readOnlyProxies.add(proxy);
}
return proxy;
};
/**
* Reads the value of the specified property without subscribing to it.
*
* @param obj The object to read the property from.
* @param key The property key.
* @return The property value.
*/
exports.proxifyState = proxifyState;
const peek = (obj, key) => {
peeking = true;
try {
return obj[key];
} finally {
peeking = false;
}
};
/**
* Internal recursive implementation for {@link deepMerge | `deepMerge`}.
*
* @param target The target object.
* @param source The source object containing new values and props.
* @param override Whether existing props should be overwritten or not (`true`
* by default).
*/
exports.peek = peek;
const deepMergeRecursive = (target, source, override = true) => {
// If target is not a plain object and the source is, we don't need to merge
// them because the source will be used as the new value of the target.
if (!((0, _utils.isPlainObject)(target) && (0, _utils.isPlainObject)(source))) {
return;
}
let hasNewKeys = false;
for (const key in source) {
const isNew = !(key in target);
hasNewKeys = hasNewKeys || isNew;
const desc = Object.getOwnPropertyDescriptor(source, key);
const proxy = (0, _registry.getProxyFromObject)(target);
const propSignal = !!proxy && hasPropSignal(proxy, key) && getPropSignal(proxy, key);
// Handle getters and setters
if (typeof desc.get === 'function' || typeof desc.set === 'function') {
if (override || isNew) {
// Because we are setting a getter or setter, we need to use
// Object.defineProperty to define the property on the target object.
Object.defineProperty(target, key, {
...desc,
configurable: true,
enumerable: true
});
// Update the getter in the property signal if it exists
if (desc.get && propSignal) {
propSignal.setGetter(desc.get);
}
}
// Handle nested objects
} else if ((0, _utils.isPlainObject)(source[key])) {
const targetValue = Object.getOwnPropertyDescriptor(target, key)?.value;
if (isNew || override && !(0, _utils.isPlainObject)(targetValue)) {
// Create a new object if the property is new or needs to be overridden
target[key] = {};
if (propSignal) {
// Create a new proxified state for the nested object
const ns = (0, _registry.getNamespaceFromProxy)(proxy);
propSignal.setValue(proxifyState(ns, target[key]));
}
deepMergeRecursive(target[key], source[key], override);
}
// Both target and source are plain objects, merge them recursively
else if ((0, _utils.isPlainObject)(targetValue)) {
deepMergeRecursive(target[key], source[key], override);
}
// Handle primitive values and non-plain objects
} else if (override || isNew) {
Object.defineProperty(target, key, desc);
if (propSignal) {
const {
value
} = desc;
const ns = (0, _registry.getNamespaceFromProxy)(proxy);
// Proxify the value if necessary before setting it in the signal
propSignal.setValue((0, _registry.shouldProxy)(value) ? proxifyState(ns, value) : value);
}
}
}
if (hasNewKeys && objToIterable.has(target)) {
objToIterable.get(target).value++;
}
};
/**
* Recursively updates prop values inside the passed `target` and nested plain
* objects, using the values present in `source`. References to plain objects
* are kept, only updating props containing primitives or arrays. Arrays are
* replaced instead of merged or concatenated.
*
* If the `override` parameter is set to `false`, then all values in `target`
* are preserved, and only new properties from `source` are added.
*
* @param target The target object.
* @param source The source object containing new values and props.
* @param override Whether existing props should be overwritten or not (`true`
* by default).
*/
const deepMerge = (target, source, override = true) => (0, _signals.batch)(() => deepMergeRecursive((0, _registry.getObjectFromProxy)(target) || target, source, override));
exports.deepMerge = deepMerge;
//# sourceMappingURL=state.js.map