web-ui-pack
Version:
Web package with UI elements
213 lines (212 loc) • 7.26 kB
JavaScript
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;