Try to remove as many leaky references from event listeners as possible

This commit is contained in:
jordi fita mas 2023-04-10 23:04:16 +02:00
parent f945051f4a
commit 33277454fa
1 changed files with 112 additions and 83 deletions

View File

@ -4,7 +4,6 @@ class Multiselect extends HTMLDivElement {
this.initialized = false; this.initialized = false;
this.toSearch = ''; this.toSearch = '';
this.highlighted = 0; this.highlighted = 0;
this.onFocusOutHandler = this.onFocusOut.bind(this);
} }
connectedCallback() { connectedCallback() {
@ -26,19 +25,73 @@ class Multiselect extends HTMLDivElement {
this.init(); this.init();
} }
} }
window.addEventListener('focusin', this.onFocusOutHandler); if (this.initialized) {
document.addEventListener('click', this.onFocusOutHandler); this.onClickHandler = () => this.toggle();
this.tags.addEventListener('click', this.onClickHandler);
this.onSearchInputHandler = (e) => {
this.open();
this.filter(e.target.value);
}
this.onSearchKeydownHandler = (e) => {
switch (e.code) {
case "Enter":
e.preventDefault();
this.selectHighlighted();
break;
case "ArrowDown":
if (e.altKey) {
this.open();
} else {
e.preventDefault();
this.highlightNext();
}
break;
case "ArrowUp":
if (e.altKey) {
this.close();
} else {
e.preventDefault();
this.highlightPrev();
}
break;
case "Backspace":
if (e.target.value === '') {
this.deselectLast();
}
break;
case "Escape":
this.close();
break;
}
};
this.search.addEventListener('input', this.onSearchInputHandler);
this.search.addEventListener('keydown', this.onSearchKeydownHandler);
this.onFocusOutHandler = (e) => {
if (this.contains(e.target)) return;
if (!e.target.isConnected) return;
this.close();
}
window.addEventListener('focusin', this.onFocusOutHandler);
document.addEventListener('click', this.onFocusOutHandler);
this.rebuild()
}
} }
disconnectedCallback() { disconnectedCallback() {
document.removeEventListener('click', this.onFocusOutHandler); if (this.initialized) {
window.removeEventListener('focusin', this.onFocusOutHandler); this.removeTags();
} this.options.innerHTML = '';
document.removeEventListener('click', this.onFocusOutHandler);
onFocusOut(e) { window.removeEventListener('focusin', this.onFocusOutHandler);
if (this.contains(e.target)) return; this.onFocusOutHandler = null;
if (!e.target.isConnected) return; this.search.removeEventListener('keydown', this.onSearchKeydownHandler);
this.close(); this.search.removeEventListener('input', this.onSearchInputHandler);
this.onSearchInputHandler = null;
this.onSearchKeydownHandler = null;
this.tags.removeEventListener('click', this.onClickHandler);
this.onClickHandler = null;
}
} }
init() { init() {
@ -52,7 +105,6 @@ class Multiselect extends HTMLDivElement {
this.tags = document.createElement('div'); this.tags = document.createElement('div');
this.append(this.tags); this.append(this.tags);
this.tags.classList.add('tags'); this.tags.classList.add('tags');
this.tags.addEventListener('click', () => this.toggle());
this.search = document.createElement('input'); this.search = document.createElement('input');
this.tags.append(this.search); this.tags.append(this.search);
@ -62,42 +114,6 @@ class Multiselect extends HTMLDivElement {
this.search.setAttribute('spellcheck', 'false'); this.search.setAttribute('spellcheck', 'false');
this.search.setAttribute('autocomplete', 'false'); this.search.setAttribute('autocomplete', 'false');
this.search.setAttribute('role', 'combobox'); this.search.setAttribute('role', 'combobox');
this.search.addEventListener('input', (e) => {
this.open();
this.filter(e.target.value);
});
this.search.addEventListener('keydown', (e) => {
switch (e.code) {
case "Enter":
e.preventDefault();
this.selectHighlighted();
break;
case "ArrowDown":
if (e.altKey) {
this.open();
} else {
e.preventDefault();
this.highlightNext();
}
break;
case "ArrowUp":
if (e.altKey) {
this.close();
} else {
e.preventDefault();
this.highlightPrev();
}
break;
case "Backspace":
if (e.target.value === '') {
this.deselectLast();
}
break;
case "Escape":
this.close();
break;
}
});
// Must come after the search input // Must come after the search input
this.tags.append(this.label); this.tags.append(this.label);
@ -109,8 +125,6 @@ class Multiselect extends HTMLDivElement {
this.options.classList.add('options'); this.options.classList.add('options');
this.options.setAttribute('role', 'listbox'); this.options.setAttribute('role', 'listbox');
this.close(); this.close();
this.rebuild();
} }
rebuildOptions() { rebuildOptions() {
@ -214,8 +228,12 @@ class Multiselect extends HTMLDivElement {
this.rebuildOptions(); this.rebuildOptions();
} }
rebuildTags() { removeTags() {
this.tags.querySelectorAll('.tag').forEach((tag) => tag.remove()); this.tags.querySelectorAll('.tag').forEach((tag) => tag.remove());
}
rebuildTags() {
this.removeTags();
let empty = true; let empty = true;
for (const option of this.select.options) { for (const option of this.select.options) {
if (!option.selected) { if (!option.selected) {
@ -284,7 +302,6 @@ class Tags extends HTMLDivElement {
super(); super();
this.initialized = false; this.initialized = false;
this.tags = []; this.tags = [];
this.onFocusOutHandler = this.onFocusOut.bind(this);
} }
connectedCallback() { connectedCallback() {
@ -306,17 +323,45 @@ class Tags extends HTMLDivElement {
this.init(); this.init();
} }
} }
window.addEventListener('focusin', this.onFocusOutHandler); if (this.initialized) {
this.onSearchKeydownHandler = (e) => {
switch (e.code) {
case "Space":
e.preventDefault();
// fallthrough
case "Enter":
if (e.target.value.trim() !== '') {
e.preventDefault();
this.createTag();
}
break;
case "Backspace":
if (e.target.value === '') {
this.removeLastTag();
}
break;
}
}
this.search.addEventListener('keydown', this.onSearchKeydownHandler);
this.onFocusOutHandler = (e) => {
if (this.contains(e.target)) return;
if (e.target.value && e.target.value.trim() !== '') {
this.createTag();
}
};
window.addEventListener('focusin', this.onFocusOutHandler);
this.rebuild();
}
} }
disconnectedCallback() { disconnectedCallback() {
window.removeEventListener('focusin', this.onFocusOutHandler); if (this.initialized) {
} this.removeTags();
window.removeEventListener('focusin', this.onFocusOutHandler);
onFocusOut(e) { this.onFocusOutHandler = null;
if (this.contains(e.target)) return; this.search.removeEventListener('keydown', this.onSearchKeydownHandler);
if (e.target.value && e.target.value.trim() !== '') { this.onSearchKeydownHandler = null;
this.createTag();
} }
} }
@ -343,29 +388,13 @@ class Tags extends HTMLDivElement {
this.input.setAttribute('aria-hidden', 'true'); this.input.setAttribute('aria-hidden', 'true');
this.search.setAttribute('spellcheck', 'false'); this.search.setAttribute('spellcheck', 'false');
this.search.setAttribute('autocomplete', 'false'); this.search.setAttribute('autocomplete', 'false');
this.search.addEventListener('keydown', (e) => {
switch (e.code) {
case "Space":
e.preventDefault();
// fallthrough
case "Enter":
if (e.target.value.trim() !== '') {
e.preventDefault();
this.createTag();
}
break;
case "Backspace":
if (e.target.value === '') {
this.removeLastTag();
}
break;
}
});
// Must come after the search input // Must come after the search input
this.tagList.append(this.label); this.tagList.append(this.label);
}
this.rebuild(); removeTags() {
this.tagList.querySelectorAll('.tag').forEach((tag) => tag.remove());
} }
rebuild() { rebuild() {
@ -374,7 +403,7 @@ class Tags extends HTMLDivElement {
this.input.value = newValue; this.input.value = newValue;
this.input.dispatchEvent(new Event('change', {bubbles: true})); this.input.dispatchEvent(new Event('change', {bubbles: true}));
} }
this.tagList.querySelectorAll('.tag').forEach((tag) => tag.remove()); this.removeTags();
if (this.tags.length === 0) { if (this.tags.length === 0) {
this.search.setAttribute('placeholder', this.label.textContent); this.search.setAttribute('placeholder', this.label.textContent);
} else { } else {
@ -440,9 +469,9 @@ htmx.onLoad((target) => {
target.showModal(); target.showModal();
const button = target.querySelector('.close-dialog'); const button = target.querySelector('.close-dialog');
if (button) { if (button) {
button.addEventListener('click', () => { button.addEventListener('click', (e) => {
htmx.trigger(target, 'closeModal'); htmx.trigger(e.target, 'closeModal');
}); }, {once: true});
} }
} }
}) })