diff --git a/src/elements.ts b/src/elements.ts index 64c4b1c..db0dd1c 100644 --- a/src/elements.ts +++ b/src/elements.ts @@ -43,8 +43,11 @@ interface EventListenerOptions { export abstract class BaseView { readonly _data: Obj = {}; readonly _events: Obj = {}; + _mutationObserver: MutationObserver|undefined; + private readonly _mutationObserverCallbacks: Obj = {}; readonly type: string = 'default'; model?: Observable; + isDeleted?: boolean; constructor(readonly _el: T) { // Store a reference to this element within the native browser DOM. @@ -628,12 +631,27 @@ export abstract class BaseView { } } + /** Removes listeners and model data from el */ + private unsubscribe() { + this.isDeleted = true; + // this causes elements freeze in Safari so we skip proxy model clearing there + if (!Browser.isSafari) this.model?.clear(); + this._mutationObserver?.disconnect(); + this.offAll(); + + for (const child of this.children) { + child.unsubscribe(); + } + + delete (this as any)._data; + delete (this as any)._events; + delete (this as any)._mutationObserverCallbacks; + } + /** Removes this element. */ remove() { this.detach(); - // TODO Remove event listeners (including children) - // TODO Remove model bindings (including children) - // this._el = this._data = this._events = undefined; + this.unsubscribe(); } /** Removes all children of this element. */ @@ -679,13 +697,27 @@ export abstract class BaseView { */ off(events: string, callback?: EventCallback) { for (const e of words(events)) { - if (e in this._events) { - this._events[e] = callback ? this._events[e].filter(fn => fn !== callback) : []; + if (callback) { + this._events[e] = this._events[e].filter(fn => fn !== callback); + unbindEvent(this, e, callback); + continue; + } + if (this._events[e]) { + for (const eventsCallback of this._events[e]) unbindEvent(this, e, eventsCallback); } - unbindEvent(this, e, callback); + this._events[e] = []; } } + /** + * Removes all event listeners from this element + */ + offAll() { + Object.entries(this._events || {}).forEach(([eventName, callbacks]) => { + callbacks.forEach((callback) => this.off(eventName, callback)); + }); + } + /** Triggers a specific event on this element. */ trigger(events: string, args: unknown = {}) { for (const e of words(events)) { @@ -703,28 +735,40 @@ export abstract class BaseView { const keyNames = new Set(words(keys)); const event = options?.up ? 'keyup' : 'keydown'; - const target = (this._el === document.body ? document : this._el) as HTMLElement; - target.addEventListener(event, (e: KeyboardEvent) => { + const eventFunction = (e: KeyboardEvent) => { const key = keyCode(e); if (options?.meta ? !e.ctrlKey && !e.metaKey : e.ctrlKey || e.metaKey) return; if (!key || !keyNames.has(key)) return; if (document.activeElement !== this._el && document.activeElement?.shadowRoot?.activeElement !== this._el && Browser.formIsActive) return; callback(e as KeyboardEvent, key); - }); + }; + + const target = (this._el === document.body ? document : this._el) as HTMLElement; + target.addEventListener(event, eventFunction); + + if (!(event in this._events)) this._events[event] = []; + this._events[event].push(eventFunction); } + /** + * Bind an listener when element attribute changed + */ onAttr(name: string, callback: (value: string, initial?: boolean) => void) { - // TODO Reuse existing observers, remove events, disconnect when deleting. - - const observer = new MutationObserver((mutations) => { - for (const m of mutations) { - if (m.type === 'attributes' && m.attributeName === name) { - callback(this.attr(name)); + if (!this._mutationObserver) { + this._mutationObserver = new MutationObserver((mutations) => { + for (const m of mutations) { + if (m.type === 'attributes' && m.attributeName && m.attributeName in this._mutationObserverCallbacks) { + for (const attributeCallback of this._mutationObserverCallbacks[m.attributeName]) { + attributeCallback(this.attr(m.attributeName)); + } + } } - } - }); + }); + this._mutationObserver.observe(this._el, {attributes: true}); + } - observer.observe(this._el, {attributes: true}); + if (!(name in this._mutationObserverCallbacks)) this._mutationObserverCallbacks[name] = []; + this._mutationObserverCallbacks[name].push(callback); callback(this.attr(name), true); } diff --git a/src/observable.ts b/src/observable.ts index 8f26ca5..05e7237 100644 --- a/src/observable.ts +++ b/src/observable.ts @@ -3,6 +3,8 @@ // (c) Mathigon // ============================================================================= +import {Browser} from './browser'; + type Callback = (state: T, initial?: boolean) => void; type Expr = (state: T) => void;