diff --git a/src/elements.ts b/src/elements.ts index 63bec52..035522f 100644 --- a/src/elements.ts +++ b/src/elements.ts @@ -42,6 +42,8 @@ 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; @@ -592,12 +594,31 @@ export abstract class BaseView { } } + /** Removes listeners and model data from el */ + private unsubscribe() { + this.model?.clear(); + this._mutationObserver?.disconnect(); + this.offAll(); + + for (const child of this.children) { + child.unsubscribe(); + } + + // hack to avoid TS notification + // about setting undefined value to readonly properties + delete (this as any)._el; + delete (this as any)._data; + delete (this as any)._events; + + // remove mutation observer instances + delete (this as any)._mutationObserver; + 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. */ @@ -643,13 +664,24 @@ 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; } - unbindEvent(this, e, callback); + for (const eventsCallback of this._events[e]) unbindEvent(this, e, eventsCallback); } } + /** + * 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)) { @@ -667,28 +699,41 @@ 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 === name) { + for (const attributeCallback of this._mutationObserverCallbacks[name]) { + attributeCallback(this.attr(name)); + } + } } - } - }); + }); + this._mutationObserver.observe(this._el, {attributes: true}); + } + + if (!(name in this._mutationObserverCallbacks)) this._mutationObserverCallbacks[name] = []; + this._mutationObserverCallbacks[name].push(callback); - observer.observe(this._el, {attributes: true}); callback(this.attr(name), true); } @@ -1140,7 +1185,7 @@ export class WindowView extends HTMLBaseView { } get scrollTop() { - return window.pageYOffset; + return window.scrollY; } set scrollTop(y) { @@ -1149,7 +1194,7 @@ export class WindowView extends HTMLBaseView { } get scrollLeft() { - return window.pageXOffset; + return window.scrollX; } set scrollLeft(x) { diff --git a/src/observable.ts b/src/observable.ts index 07f83e9..789235f 100644 --- a/src/observable.ts +++ b/src/observable.ts @@ -119,6 +119,7 @@ export function observe(state: T, parentModel?: Observab state = {} as T; callbackMap.clear(); computedKeys.clear(); + watchAllCallbacks.clear(); lastKey = 0; }