UNPKG

@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
"use strict"; 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