From 33277454fa4b0d40a69881f642cbe9bed0a49672 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Mon, 10 Apr 2023 23:04:16 +0200 Subject: [PATCH] Try to remove as many leaky references from event listeners as possible --- web/static/numerus.js | 195 ++++++++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 83 deletions(-) diff --git a/web/static/numerus.js b/web/static/numerus.js index c0ab0cb..9cf075f 100644 --- a/web/static/numerus.js +++ b/web/static/numerus.js @@ -4,7 +4,6 @@ class Multiselect extends HTMLDivElement { this.initialized = false; this.toSearch = ''; this.highlighted = 0; - this.onFocusOutHandler = this.onFocusOut.bind(this); } connectedCallback() { @@ -26,19 +25,73 @@ class Multiselect extends HTMLDivElement { this.init(); } } - window.addEventListener('focusin', this.onFocusOutHandler); - document.addEventListener('click', this.onFocusOutHandler); + if (this.initialized) { + 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() { - document.removeEventListener('click', this.onFocusOutHandler); - window.removeEventListener('focusin', this.onFocusOutHandler); - } - - onFocusOut(e) { - if (this.contains(e.target)) return; - if (!e.target.isConnected) return; - this.close(); + if (this.initialized) { + this.removeTags(); + this.options.innerHTML = ''; + document.removeEventListener('click', this.onFocusOutHandler); + window.removeEventListener('focusin', this.onFocusOutHandler); + this.onFocusOutHandler = null; + this.search.removeEventListener('keydown', this.onSearchKeydownHandler); + this.search.removeEventListener('input', this.onSearchInputHandler); + this.onSearchInputHandler = null; + this.onSearchKeydownHandler = null; + this.tags.removeEventListener('click', this.onClickHandler); + this.onClickHandler = null; + } } init() { @@ -52,7 +105,6 @@ class Multiselect extends HTMLDivElement { this.tags = document.createElement('div'); this.append(this.tags); this.tags.classList.add('tags'); - this.tags.addEventListener('click', () => this.toggle()); this.search = document.createElement('input'); this.tags.append(this.search); @@ -62,42 +114,6 @@ class Multiselect extends HTMLDivElement { this.search.setAttribute('spellcheck', 'false'); this.search.setAttribute('autocomplete', 'false'); 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 this.tags.append(this.label); @@ -109,8 +125,6 @@ class Multiselect extends HTMLDivElement { this.options.classList.add('options'); this.options.setAttribute('role', 'listbox'); this.close(); - - this.rebuild(); } rebuildOptions() { @@ -214,8 +228,12 @@ class Multiselect extends HTMLDivElement { this.rebuildOptions(); } - rebuildTags() { + removeTags() { this.tags.querySelectorAll('.tag').forEach((tag) => tag.remove()); + } + + rebuildTags() { + this.removeTags(); let empty = true; for (const option of this.select.options) { if (!option.selected) { @@ -284,7 +302,6 @@ class Tags extends HTMLDivElement { super(); this.initialized = false; this.tags = []; - this.onFocusOutHandler = this.onFocusOut.bind(this); } connectedCallback() { @@ -306,17 +323,45 @@ class Tags extends HTMLDivElement { 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() { - window.removeEventListener('focusin', this.onFocusOutHandler); - } - - onFocusOut(e) { - if (this.contains(e.target)) return; - if (e.target.value && e.target.value.trim() !== '') { - this.createTag(); + if (this.initialized) { + this.removeTags(); + window.removeEventListener('focusin', this.onFocusOutHandler); + this.onFocusOutHandler = null; + this.search.removeEventListener('keydown', this.onSearchKeydownHandler); + this.onSearchKeydownHandler = null; } } @@ -343,29 +388,13 @@ class Tags extends HTMLDivElement { this.input.setAttribute('aria-hidden', 'true'); this.search.setAttribute('spellcheck', '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 this.tagList.append(this.label); + } - this.rebuild(); + removeTags() { + this.tagList.querySelectorAll('.tag').forEach((tag) => tag.remove()); } rebuild() { @@ -374,7 +403,7 @@ class Tags extends HTMLDivElement { this.input.value = newValue; this.input.dispatchEvent(new Event('change', {bubbles: true})); } - this.tagList.querySelectorAll('.tag').forEach((tag) => tag.remove()); + this.removeTags(); if (this.tags.length === 0) { this.search.setAttribute('placeholder', this.label.textContent); } else { @@ -440,9 +469,9 @@ htmx.onLoad((target) => { target.showModal(); const button = target.querySelector('.close-dialog'); if (button) { - button.addEventListener('click', () => { - htmx.trigger(target, 'closeModal'); - }); + button.addEventListener('click', (e) => { + htmx.trigger(e.target, 'closeModal'); + }, {once: true}); } } })