Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 62 additions & 18 deletions src/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@
export abstract class BaseView<T extends HTMLElement|SVGElement> {
readonly _data: Obj<unknown> = {};
readonly _events: Obj<EventCallback[]> = {};
_mutationObserver: MutationObserver|undefined;
private readonly _mutationObserverCallbacks: Obj<EventCallback[]> = {};
readonly type: string = 'default';
model?: Observable;
isDeleted?: boolean;

constructor(readonly _el: T) {
// Store a reference to this element within the native browser DOM.
Expand Down Expand Up @@ -628,12 +631,27 @@
}
}

/** 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();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cause Safari issues

this._mutationObserver?.disconnect();
this.offAll();

for (const child of this.children) {
child.unsubscribe();
}

delete (this as any)._data;

Check warning on line 646 in src/elements.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20

Unexpected any. Specify a different type

Check warning on line 646 in src/elements.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18

Unexpected any. Specify a different type
delete (this as any)._events;

Check warning on line 647 in src/elements.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20

Unexpected any. Specify a different type

Check warning on line 647 in src/elements.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18

Unexpected any. Specify a different type
delete (this as any)._mutationObserverCallbacks;

Check warning on line 648 in src/elements.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20

Unexpected any. Specify a different type

Check warning on line 648 in src/elements.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18

Unexpected any. Specify a different type
}

/** 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. */
Expand Down Expand Up @@ -679,13 +697,27 @@
*/
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)) {
Expand All @@ -703,28 +735,40 @@
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);
}

Expand Down Expand Up @@ -1030,7 +1074,7 @@
if (!obj) return this.setAttr('d', '');
const attributes: SVGDrawingOptions = {};
for (const p of ['mark', 'arrows', 'round'] as const) {
if (this.hasAttr(p)) attributes[p] = this.attr(p) as any;

Check warning on line 1077 in src/elements.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20

Unexpected any. Specify a different type

Check warning on line 1077 in src/elements.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18

Unexpected any. Specify a different type
}
if (this.hasClass('fill')) attributes.fill = 'fill';
if (this.hasAttr('size')) attributes.size = (+this.attr('size')) || undefined;
Expand Down
2 changes: 2 additions & 0 deletions src/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// (c) Mathigon
// =============================================================================

import {Browser} from './browser';


type Callback<T> = (state: T, initial?: boolean) => void;
type Expr<T> = (state: T) => void;
Expand Down
Loading