Compare commits
4 commits
74de53874b
...
aa27cc0b34
Author | SHA1 | Date | |
---|---|---|---|
aa27cc0b34 | |||
a08d51e4db | |||
e9e8aeba4f | |||
8de5303550 |
2 changed files with 70 additions and 15 deletions
2
.editorconfig
Normal file
2
.editorconfig
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
|
@ -12,17 +12,31 @@ const target = Symbol("Proxy Target")
|
||||||
|
|
||||||
/* Custom Event Classes */
|
/* Custom Event Classes */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Change
|
||||||
|
* @property {string} property
|
||||||
|
* @property {any} from
|
||||||
|
* @property {any} to
|
||||||
|
* @property {boolean} mutation - The change happened inside the value without a new assignment
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Event fired for every change before the internal state has been updated that can be canceled. */
|
||||||
export class SynchronousChangeEvent extends Event {
|
export class SynchronousChangeEvent extends Event {
|
||||||
|
/** @param {Change} change */
|
||||||
constructor(change) {
|
constructor(change) {
|
||||||
super('synchronous', {cancelable: true})
|
super('synchronous', {cancelable: true})
|
||||||
this.change = change
|
this.change = Object.freeze(change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Event fired for one or more changed values after the internal state has been updated. */
|
||||||
export class MultiChangeEvent extends Event {
|
export class MultiChangeEvent extends Event {
|
||||||
|
/** @type {any} */
|
||||||
#final
|
#final
|
||||||
|
/** @type {any} */
|
||||||
#values
|
#values
|
||||||
|
|
||||||
|
/** @param {Change[]} changes */
|
||||||
constructor(...changes) {
|
constructor(...changes) {
|
||||||
super('change')
|
super('change')
|
||||||
this.changes = changes
|
this.changes = changes
|
||||||
|
@ -67,17 +81,17 @@ export class ValueChangeEvent extends MultiChangeEvent {
|
||||||
|
|
||||||
export class Observable extends EventTarget {
|
export class Observable extends EventTarget {
|
||||||
#synchronous
|
#synchronous
|
||||||
/** @type Array<{name:string, from, to}> */
|
/** @type Change[]> */
|
||||||
#queue
|
#queue
|
||||||
#abortController = new AbortController
|
#abortController = new AbortController
|
||||||
|
|
||||||
#ref = new WeakRef(this)
|
#ref = new WeakRef(this)
|
||||||
get ref() { return this.#ref }
|
get ref() { return this.#ref }
|
||||||
|
|
||||||
observable = true
|
constructor({synchronous=false}={}) {
|
||||||
|
|
||||||
constructor({synchronous}={}) {
|
|
||||||
super()
|
super()
|
||||||
|
Object.defineProperty(this, "observable", {value: true, configurable: false, writable: false})
|
||||||
|
|
||||||
if (this.constructor === Observable) {
|
if (this.constructor === Observable) {
|
||||||
throw new TypeError("Cannot instantiate abstract class")
|
throw new TypeError("Cannot instantiate abstract class")
|
||||||
}
|
}
|
||||||
|
@ -89,24 +103,37 @@ export class Observable extends EventTarget {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy(prop, {get, set, ...options}={}) {
|
/**
|
||||||
|
* @param {string} prop
|
||||||
|
*/
|
||||||
|
proxy(prop, {get=undefined, set=undefined, ...options}={}) {
|
||||||
const proxy = new ProxiedObservableValue(this, prop, options)
|
const proxy = new ProxiedObservableValue(this, prop, options)
|
||||||
if (get) proxy.get = get
|
if (get) proxy.get = get
|
||||||
if (set) proxy.set = set
|
if (set) proxy.set = set
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} prop
|
||||||
|
* @param {function} callback
|
||||||
|
*/
|
||||||
subscribe(prop, callback) {
|
subscribe(prop, callback) {
|
||||||
if (!callback) return this.subscribe("value", prop)
|
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
|
// @ts-ignore
|
||||||
this.addEventListener("change", ({final}) => {
|
this.addEventListener("change", ({final}) => {
|
||||||
if (final.has(prop)) return callback(final.get(prop))
|
if (final.has(prop)) return callback(final.get(prop))
|
||||||
}, {signal: controller.signal})
|
}, {signal: controller.signal})
|
||||||
callback(this.value)
|
|
||||||
|
callback(this[prop])
|
||||||
return () => controller.abort()
|
return () => controller.abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Queues up a change event
|
||||||
|
* @param {string} property - Name of the changed property
|
||||||
|
* @param {any} from
|
||||||
|
* @param {any} to
|
||||||
|
* @param {boolean} mutation - whether a change was an assignment or a mutation (nested change)
|
||||||
|
*/
|
||||||
enqueue(property, from, to, mutation=false) {
|
enqueue(property, from, to, mutation=false) {
|
||||||
const change = {property, from, to, mutation}
|
const change = {property, from, to, mutation}
|
||||||
if (!this.dispatchEvent(new SynchronousChangeEvent(change))) return false
|
if (!this.dispatchEvent(new SynchronousChangeEvent(change))) return false
|
||||||
|
@ -125,7 +152,8 @@ export class Observable extends EventTarget {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
emit() {
|
/** @param {any[]} _args */
|
||||||
|
emit(..._args) {
|
||||||
throw new TypeError(`${this.constructor.name} did not define an 'emit' method`)
|
throw new TypeError(`${this.constructor.name} did not define an 'emit' method`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,6 +244,14 @@ export class ObservableValue extends Observable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {function(any):undefined} callback
|
||||||
|
* @param {function(any):undefined} _callback
|
||||||
|
*/
|
||||||
|
subscribe(callback, _callback) {
|
||||||
|
this.constructor.prototype.subscribe.call(this, "value", callback)
|
||||||
|
}
|
||||||
|
|
||||||
emit(...changes) {
|
emit(...changes) {
|
||||||
this.dispatchEvent(new ValueChangeEvent(...changes))
|
this.dispatchEvent(new ValueChangeEvent(...changes))
|
||||||
}
|
}
|
||||||
|
@ -361,9 +397,13 @@ export class ObservableElement extends Observable {
|
||||||
#value
|
#value
|
||||||
#changedValue = false
|
#changedValue = false
|
||||||
|
|
||||||
constructor(target, {get, equal, ...options}={}) {
|
/**
|
||||||
|
* @param {HTMLElement} target
|
||||||
|
*/
|
||||||
|
constructor(target, {get=undefined, equal=undefined, ...options}={}) {
|
||||||
|
// @ts-ignore
|
||||||
super(options)
|
super(options)
|
||||||
this[target] = target
|
Object.defineProperty(this, "target", {value: target, configurable: false, writable: false})
|
||||||
|
|
||||||
this.#getValue = get ?? (target => target.value)
|
this.#getValue = get ?? (target => target.value)
|
||||||
this.#equal = equal ?? ((a, b) => a===b)
|
this.#equal = equal ?? ((a, b) => a===b)
|
||||||
|
@ -385,19 +425,19 @@ export class ObservableElement extends Observable {
|
||||||
get value() { return this.#value }
|
get value() { return this.#value }
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
const current = this.#getValue(this[target])
|
const current = this.#getValue(this.target)
|
||||||
|
|
||||||
if (this.#equal(this.#value, current)) return
|
if (this.#equal(this.#value, current)) return
|
||||||
|
|
||||||
this.#value = current
|
this.#value = current
|
||||||
|
|
||||||
if (this.synchronous) {
|
if (this.synchronous) {
|
||||||
this.dispatchEvent(new MultiChangeEvent(["value", current]))
|
this.dispatchEvent(new ValueChangeEvent(["value", current]))
|
||||||
} else {
|
} else {
|
||||||
if (!this.#changedValue) {
|
if (!this.#changedValue) {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
this.#changedValue = false
|
this.#changedValue = false
|
||||||
this.dispatchEvent(new MultiChangeEvent(["value", this.#changedValue]))
|
this.dispatchEvent(new ValueChangeEvent(["value", this.#changedValue]))
|
||||||
})
|
})
|
||||||
this.#changedValue = current
|
this.#changedValue = current
|
||||||
}
|
}
|
||||||
|
@ -407,18 +447,31 @@ export class ObservableElement extends Observable {
|
||||||
|
|
||||||
export class MapStorage extends Storage {
|
export class MapStorage extends Storage {
|
||||||
#map = new Map()
|
#map = new Map()
|
||||||
|
/**
|
||||||
|
* @param {number} index
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
key(index) {
|
key(index) {
|
||||||
return [...this.#map.keys()][index]
|
return [...this.#map.keys()][index]
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} keyName
|
||||||
|
* @return {any}
|
||||||
|
*/
|
||||||
getItem(keyName) {
|
getItem(keyName) {
|
||||||
if (this.#map.has(keyName))
|
if (this.#map.has(keyName))
|
||||||
return this.#map.get(keyName)
|
return this.#map.get(keyName)
|
||||||
else
|
else
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} keyName
|
||||||
|
* @param {any} keyValue
|
||||||
|
*/
|
||||||
setItem(keyName, keyValue) {
|
setItem(keyName, keyValue) {
|
||||||
this.#map.set(keyName, String(keyValue))
|
this.#map.set(keyName, String(keyValue))
|
||||||
}
|
}
|
||||||
|
/** @param {string} keyName */
|
||||||
removeItem(keyName) {
|
removeItem(keyName) {
|
||||||
this.#map.delete(keyName)
|
this.#map.delete(keyName)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue