UNPKG

web-ui-pack

Version:
213 lines (212 loc) 7.26 kB
import isEqual from "./isEqual"; const arrRemove = (arr, item) => { const i = arr.indexOf(item); arr.splice(i, 1); }; const some = (m, predicate) => { for (const [key, value] of m.entries()) { if (predicate(key, value)) { return true; } } return false; }; const pathThrough = (fn) => new Promise((resolve) => { resolve(fn()); }); const isRecord = (obj) => obj !== null && typeof obj === "object" && !(obj instanceof HTMLElement) && !(obj instanceof Promise); const isDate = (obj) => Object.prototype.toString.call(obj) === "[object Date]"; const watchSet = [ { is: isDate, keys: new Set([ "setDate", "setFullYear", "setHours", "setMilliseconds", "setMinutes", "setMonth", "setSeconds", "setTime", "setUTCDate", "setUTCFullYear", "setUTCHours", "setUTCMilliseconds", "setUTCMinutes", "setUTCMonth", "setUTCSeconds", "setYear", ]), getVal: (obj) => obj.valueOf(), propKey: "valueOf", }, { is: (obj) => Object.prototype.toString.call(obj) === "[object Set]", keys: new Set(["add", "delete", "clear"]), getVal: (obj) => obj.size, propKey: "size", }, { is: (obj) => Object.prototype.toString.call(obj) === "[object Map]", keys: new Set(["set", "delete", "clear"]), getVal: (obj) => obj.size, propKey: "size", }, ]; const lstObserved = new WeakMap(); const lstObjProxy = new WeakMap(); function isObserved(obj) { return obj && lstObserved.has(obj); } function appendCallback(proxy, callback, setKey) { const o = lstObserved.get(proxy); if (!o) { throw new Error("Observer. Only observed objects expected. Use make() before"); } if (!callback) { throw new Error("Observer. Callback is missed"); } const listeners = o[setKey]; listeners.push(callback); return () => arrRemove(listeners, callback); } function make(obj, parentRef, opts) { const prevProxy = lstObjProxy.get(obj); const isAssigned = lstObserved.get(prevProxy || obj); if (isAssigned) { if (parentRef) { isAssigned.parentRefs.set(parentRef.ref, parentRef.key); } return prevProxy || obj; } const isDateObj = isDate(obj); const ref = { propListeners: [], listeners: [], parentRefs: parentRef ? new Map([[parentRef.ref, parentRef.key]]) : new Map(), hasListeners: () => !!ref.listeners.length || !!ref.propListeners.length || some(ref.parentRefs, (r) => r.hasListeners()), hasObjListeners: () => !!ref.listeners.length || some(ref.parentRefs, (r) => r.hasObjListeners()), onPropChanged: (e) => { const target = proxy; if (e.target === null) { e.prev = target[e.prop]; e.next = target[e.prop]; } e.target = target; ref.propListeners.forEach((f) => pathThrough(() => f(e))); ref.parentRefs.forEach((key, r) => r.onPropChanged({ ...e, prop: key, target: null })); }, onChanged: (e) => { e.target = proxy; ref.listeners.forEach((f) => pathThrough(() => f(e))); ref.parentRefs.forEach((key, r) => r.onChanged({ ...e, props: [key] })); }, }; let changedProps = []; let timeoutId; const propChanged = (e) => { e.target = proxy; ref.onPropChanged(e); if (ref.hasObjListeners()) { changedProps.push(e.prop); if (timeoutId) { return; } timeoutId = setTimeout(() => { timeoutId = undefined; const ev = { props: changedProps, target: proxy, }; changedProps = []; ref.onChanged(ev); }); } }; let isReady = false; const proxyHandler = { set(t, prop, next, receiver) { if (!isReady) { return Reflect.set(t, prop, next, receiver); } const prev = t[prop]; if (prev !== next) { lstObserved.get(prev)?.parentRefs.delete(ref); if (opts?.excludeNested !== true && isRecord(next)) { const exclude = opts?.excludeNested; const optsDeep = Array.isArray(exclude) && exclude.includes(prop) ? { ...opts, excludeNested: true } : opts; next = make(next, { key: prop, ref }, optsDeep); } } const isOk = Reflect.set(t, prop, next, receiver); if (isOk && ref.hasListeners()) { !isEqual(prev, next) && propChanged({ prev, next, prop }); } return isOk; }, deleteProperty(t, prop) { const prev = t[prop]; if (!isDateObj && ref.hasListeners()) { propChanged({ prev, next: undefined, prop }); } if (opts?.excludeNested !== true && isRecord(prev)) { const v = proxy[prop]; lstObserved.get(v).parentRefs.delete(ref); } return true; }, }; const watchObj = watchSet.find((v) => v.is(obj)); if (watchObj) { proxyHandler.get = function get(t, prop, receiver) { const v = Reflect.get(t, prop, receiver); if (typeof v === "function") { if (watchObj.keys.has(prop) && ref.hasListeners()) { return (...args) => { const prev = watchObj.getVal(t); const r = v.apply(t, args); const next = watchObj.getVal(t); !isEqual(prev, next) && propChanged({ prev, next, prop: watchObj.propKey }); return r; }; } return v.bind(t); } return v; }; } const proxy = new Proxy(obj, proxyHandler); lstObserved.set(proxy, ref); lstObjProxy.set(obj, proxy); if (isRecord(obj) && obj.valueOf() === obj) { Object.defineProperty(proxy, "valueOf", { value: () => obj.valueOf, enumerable: false }); } const exclude = opts?.excludeNested; if (exclude !== true) { Object.keys(obj).forEach((k) => { const v = obj[k]; if (isRecord(v)) { const optsDeep = exclude && exclude.includes(k) ? { ...opts, excludeNested: true } : opts; proxy[k] = make(v, { ref, key: k }, optsDeep); } }); } isReady = true; return proxy; } const observer = { isObserved, make(obj, opts) { return make(obj, undefined, opts); }, onPropChanged(target, callback) { return appendCallback(target, callback, "propListeners"); }, onChanged(target, callback) { return appendCallback(target, callback, "listeners"); }, }; Object.seal(observer); export default observer;