export function equals(a, b) {
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
return a === b;
let seen = new Set;
for (let key in a) {
seen.add(key);
if (!(key in b))
return false;
if (!equals(a[key], b[key]))
return false;
}
for (let key in b) {
if (!seen.has(key))
return false;
}
return true;
}
export function getOrCreate(object, key, createCallback) {
console.assert(typeof object === "object", object);
console.assert(typeof createCallback === "function", createCallback);
if (!(key in object))
object[key] = createCallback(object, key);
return object[key];
}
export function isEmpty(object) {
console.assert(typeof object === "object", object);
for (let key in object)
return false;
return true;
}
export function merge(destination, ...sources) {
for (let source of sources) {
for (let key in source) {
if (source.hasOwnProperty(key))
destination[key] = source[key];
}
}
}
export function weak(creator) {
let wrapper = null;
return function() {
let instance = wrapper?.deref();
if (!instance) {
instance = creator();
wrapper = new WeakRef(instance);
}
return instance;
};
}
import MultiMap from "js/MultiMap.js";
const ObjectListenersSymbol = Symbol("object-listeners");
export class ObjectEvent {
constructor(type, options) {
console.assert(type);
this._type = type;
this._detail = (typeof options === "object" && options !== null && "detail" in options) ? options.detail : null;
this._target = null;
this._currentTarget = null;
this._propagationStopped = false;
this._defaultPrevented = false;
}
// Public
get type() { return this._type; }
get detail() { return this._detail; }
get target() { return this._target; }
get currentTarget() { return this._currentTarget; }
stopPropagation() {
this._propagationStopped = true;
}
preventDefault() {
this._defaultPrevented = true;
}
}
export function addObjectEventListener(target, type, listener, options) {
target[ObjectListenersSymbol] ??= new MultiMap;
for (let listenerForType of target[ObjectListenersSymbol].get(type)) {
if (listenerForType.listener === listener && listenerForType.capture === options?.capture)
return;
}
target[ObjectListenersSymbol].add(type, {listener, options});
}
export function removeObjectEventListener(target, type, listener, options) {
if (!target[ObjectListenersSymbol])
return;
for (let listenerForType of target[ObjectListenersSymbol].get(type)) {
if (listenerForType.listener === listener && listenerForType.capture === options?.capture)
target[ObjectListenersSymbol].delete(type, listenerForType);
}
}
export function dispatchObjectEvent(target, event) {
console.assert(event instanceof ObjectEvent, event);
console.assert(!event._target);
console.assert(!event._currentTarget);
console.assert(!event._propagationStopped);
console.assert(!event._defaultPrevented);
event._target = target;
event._currentTarget = target;
while (event._currentTarget && !event._propagationStopped) {
if (event._currentTarget.hasOwnProperty(ObjectListenersSymbol)) {
for (let listenerForType of Array.from(event._currentTarget[ObjectListenersSymbol].get(type))) {
listenerForType.listener.call(event._currentTarget, event);
if (listenerForType.options?.once)
event._currentTarget[ObjectListenersSymbol].delete(type, listenerForType);
if (event._propagationStopped)
break;
}
}
event._currentTarget = Object.getPrototypeOf(event._currentTarget);
if (event._currentTarget.constructor !== Function)
event._currentTarget = event._currentTarget.constructor;
}
return event._defaultPrevented;
}
import {
addObjectEventListener
} from "js/Object.js";
export function awaitObjectEvent(type, options) {
let predicate = (typeof options === "object" && options !== null && typeof options.predicate === "function") ? options.predicate : function() { return true; };
return new Promise((resolve, reject) => {
addObjectEventListener(target, type, function wrappedCallback(...args) {
if (!predicate(...args))
return;
removeObjectEventListener(target, type, wrappedCallback);
resolve(...args);
});
});
}