diff --git a/bookmarks/frontend/behaviors/bookmark-page.js b/bookmarks/frontend/behaviors/bookmark-page.js index 304d8ce..6f77973 100644 --- a/bookmarks/frontend/behaviors/bookmark-page.js +++ b/bookmarks/frontend/behaviors/bookmark-page.js @@ -1,8 +1,8 @@ -import { registerBehavior } from "./index"; +import { Behavior, registerBehavior } from "./index"; -class BookmarkItem { +class BookmarkItem extends Behavior { constructor(element) { - this.element = element; + super(element); // Toggle notes const notesToggle = element.querySelector(".toggle-notes"); @@ -13,9 +13,11 @@ class BookmarkItem { // Add tooltip to title if it is truncated const titleAnchor = element.querySelector(".title > a"); const titleSpan = titleAnchor.querySelector("span"); - if (titleSpan.offsetWidth > titleAnchor.offsetWidth) { - titleAnchor.dataset.tooltip = titleSpan.textContent; - } + requestAnimationFrame(() => { + if (titleSpan.offsetWidth > titleAnchor.offsetWidth) { + titleAnchor.dataset.tooltip = titleSpan.textContent; + } + }); } onToggleNotes(event) { diff --git a/bookmarks/frontend/behaviors/bulk-edit.js b/bookmarks/frontend/behaviors/bulk-edit.js index 57af913..6d9d0bc 100644 --- a/bookmarks/frontend/behaviors/bulk-edit.js +++ b/bookmarks/frontend/behaviors/bulk-edit.js @@ -1,8 +1,9 @@ -import { registerBehavior } from "./index"; +import { Behavior, registerBehavior } from "./index"; -class BulkEdit { +class BulkEdit extends Behavior { constructor(element) { - this.element = element; + super(element); + this.active = false; this.onToggleActive = this.onToggleActive.bind(this); diff --git a/bookmarks/frontend/behaviors/confirm-button.js b/bookmarks/frontend/behaviors/confirm-button.js index 7bf0b1a..059e938 100644 --- a/bookmarks/frontend/behaviors/confirm-button.js +++ b/bookmarks/frontend/behaviors/confirm-button.js @@ -1,25 +1,29 @@ -import { registerBehavior } from "./index"; +import { Behavior, registerBehavior } from "./index"; -class ConfirmButtonBehavior { +class ConfirmButtonBehavior extends Behavior { constructor(element) { - const button = element; - button.dataset.type = button.type; - button.dataset.name = button.name; - button.dataset.value = button.value; - button.removeAttribute("type"); - button.removeAttribute("name"); - button.removeAttribute("value"); - button.addEventListener("click", this.onClick.bind(this)); - this.button = button; + super(element); + element.dataset.type = element.type; + element.dataset.name = element.name; + element.dataset.value = element.value; + element.removeAttribute("type"); + element.removeAttribute("name"); + element.removeAttribute("value"); + element.addEventListener("click", this.onClick.bind(this)); + } + + destroy() { + Behavior.interacting = false; } onClick(event) { event.preventDefault(); + Behavior.interacting = true; const container = document.createElement("span"); container.className = "confirmation"; - const icon = this.button.getAttribute("ld-confirm-icon"); + const icon = this.element.getAttribute("ld-confirm-icon"); if (icon) { const iconElement = document.createElementNS( "http://www.w3.org/2000/svg", @@ -31,27 +35,27 @@ class ConfirmButtonBehavior { container.append(iconElement); } - const question = this.button.getAttribute("ld-confirm-question"); + const question = this.element.getAttribute("ld-confirm-question"); if (question) { const questionElement = document.createElement("span"); questionElement.innerText = question; container.append(question); } - const buttonClasses = Array.from(this.button.classList.values()) + const buttonClasses = Array.from(this.element.classList.values()) .filter((cls) => cls.startsWith("btn")) .join(" "); - const cancelButton = document.createElement(this.button.nodeName); + const cancelButton = document.createElement(this.element.nodeName); cancelButton.type = "button"; cancelButton.innerText = question ? "No" : "Cancel"; cancelButton.className = `${buttonClasses} mr-1`; cancelButton.addEventListener("click", this.reset.bind(this)); - const confirmButton = document.createElement(this.button.nodeName); - confirmButton.type = this.button.dataset.type; - confirmButton.name = this.button.dataset.name; - confirmButton.value = this.button.dataset.value; + const confirmButton = document.createElement(this.element.nodeName); + confirmButton.type = this.element.dataset.type; + confirmButton.name = this.element.dataset.name; + confirmButton.value = this.element.dataset.value; confirmButton.innerText = question ? "Yes" : "Confirm"; confirmButton.className = buttonClasses; confirmButton.addEventListener("click", this.reset.bind(this)); @@ -59,14 +63,15 @@ class ConfirmButtonBehavior { container.append(cancelButton, confirmButton); this.container = container; - this.button.before(container); - this.button.classList.add("d-none"); + this.element.before(container); + this.element.classList.add("d-none"); } reset() { setTimeout(() => { + Behavior.interacting = false; this.container.remove(); - this.button.classList.remove("d-none"); + this.element.classList.remove("d-none"); }); } } diff --git a/bookmarks/frontend/behaviors/dropdown.js b/bookmarks/frontend/behaviors/dropdown.js index bf142e3..60a4787 100644 --- a/bookmarks/frontend/behaviors/dropdown.js +++ b/bookmarks/frontend/behaviors/dropdown.js @@ -1,8 +1,8 @@ -import { registerBehavior } from "./index"; +import { Behavior, registerBehavior } from "./index"; -class DropdownBehavior { +class DropdownBehavior extends Behavior { constructor(element) { - this.element = element; + super(element); this.opened = false; this.onOutsideClick = this.onOutsideClick.bind(this); diff --git a/bookmarks/frontend/behaviors/fetch.js b/bookmarks/frontend/behaviors/fetch.js index 3a65511..32b7184 100644 --- a/bookmarks/frontend/behaviors/fetch.js +++ b/bookmarks/frontend/behaviors/fetch.js @@ -1,15 +1,31 @@ -import { fireEvents, registerBehavior, swap } from "./index"; +import { Behavior, fireEvents, registerBehavior, swap } from "./index"; -class FetchBehavior { +class FetchBehavior extends Behavior { constructor(element) { - this.element = element; - const eventName = element.getAttribute("ld-on"); + super(element); - element.addEventListener(eventName, this.onFetch.bind(this)); + const eventName = element.getAttribute("ld-on"); + const interval = parseInt(element.getAttribute("ld-interval")) * 1000; + + this.onFetch = this.onFetch.bind(this); + this.onInterval = this.onInterval.bind(this); + + element.addEventListener(eventName, this.onFetch); + if (interval) { + this.intervalId = setInterval(this.onInterval, interval); + } } - async onFetch(event) { - event.preventDefault(); + destroy() { + if (this.intervalId) { + clearInterval(this.intervalId); + } + } + + async onFetch(maybeEvent) { + if (maybeEvent) { + maybeEvent.preventDefault(); + } const url = this.element.getAttribute("ld-fetch"); const html = await fetch(url).then((response) => response.text()); @@ -20,6 +36,13 @@ class FetchBehavior { const events = this.element.getAttribute("ld-fire"); fireEvents(events); } + + onInterval() { + if (Behavior.interacting) { + return; + } + this.onFetch(); + } } registerBehavior("ld-fetch", FetchBehavior); diff --git a/bookmarks/frontend/behaviors/form.js b/bookmarks/frontend/behaviors/form.js index 7a30f4c..ae5520d 100644 --- a/bookmarks/frontend/behaviors/form.js +++ b/bookmarks/frontend/behaviors/form.js @@ -1,8 +1,9 @@ -import { fireEvents, registerBehavior } from "./index"; +import { Behavior, fireEvents, registerBehavior } from "./index"; -class FormBehavior { +class FormBehavior extends Behavior { constructor(element) { - this.element = element; + super(element); + element.addEventListener("submit", this.onSubmit.bind(this)); } @@ -28,8 +29,10 @@ class FormBehavior { } } -class AutoSubmitBehavior { +class AutoSubmitBehavior extends Behavior { constructor(element) { + super(element); + element.addEventListener("change", () => { const form = element.closest("form"); form.dispatchEvent(new Event("submit", { cancelable: true })); diff --git a/bookmarks/frontend/behaviors/global-shortcuts.js b/bookmarks/frontend/behaviors/global-shortcuts.js index f1700b4..fba6ab1 100644 --- a/bookmarks/frontend/behaviors/global-shortcuts.js +++ b/bookmarks/frontend/behaviors/global-shortcuts.js @@ -1,7 +1,9 @@ -import { registerBehavior } from "./index"; +import { Behavior, registerBehavior } from "./index"; + +class GlobalShortcuts extends Behavior { + constructor(element) { + super(element); -class GlobalShortcuts { - constructor() { document.addEventListener("keydown", this.onKeyDown.bind(this)); } diff --git a/bookmarks/frontend/behaviors/index.js b/bookmarks/frontend/behaviors/index.js index 6a87653..0ddbde3 100644 --- a/bookmarks/frontend/behaviors/index.js +++ b/bookmarks/frontend/behaviors/index.js @@ -1,4 +1,35 @@ const behaviorRegistry = {}; +const debug = false; + +const mutationObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.removedNodes.forEach((node) => { + if (node instanceof HTMLElement && !node.isConnected) { + destroyBehaviors(node); + } + }); + mutation.addedNodes.forEach((node) => { + if (node instanceof HTMLElement && node.isConnected) { + applyBehaviors(node); + } + }); + }); +}); + +mutationObserver.observe(document.body, { + childList: true, + subtree: true, +}); + +export class Behavior { + constructor(element) { + this.element = element; + } + + destroy() {} +} + +Behavior.interacting = false; export function registerBehavior(name, behavior) { behaviorRegistry[name] = behavior; @@ -33,6 +64,34 @@ export function applyBehaviors(container, behaviorNames = null) { const behaviorInstance = new behavior(element); element.__behaviors.push(behaviorInstance); + if (debug) { + console.log( + `[Behavior] ${behaviorInstance.constructor.name} initialized`, + ); + } + }); + }); +} + +export function destroyBehaviors(element) { + const behaviorNames = Object.keys(behaviorRegistry); + + behaviorNames.forEach((behaviorName) => { + const elements = Array.from(element.querySelectorAll(`[${behaviorName}]`)); + elements.push(element); + + elements.forEach((element) => { + if (!element.__behaviors) { + return; + } + + element.__behaviors.forEach((behavior) => { + behavior.destroy(); + if (debug) { + console.log(`[Behavior] ${behavior.constructor.name} destroyed`); + } + }); + delete element.__behaviors; }); }); } @@ -63,10 +122,11 @@ export function swap(element, html, options) { break; case "innerHTML": default: - targetElement.innerHTML = ""; + Array.from(targetElement.children).forEach((child) => { + child.remove(); + }); targetElement.append(...contents); } - contents.forEach((content) => applyBehaviors(content)); } export function fireEvents(events) { diff --git a/bookmarks/frontend/behaviors/modal.js b/bookmarks/frontend/behaviors/modal.js index b83bb0e..37b6da6 100644 --- a/bookmarks/frontend/behaviors/modal.js +++ b/bookmarks/frontend/behaviors/modal.js @@ -1,8 +1,8 @@ -import { registerBehavior } from "./index"; +import { Behavior, registerBehavior } from "./index"; -class ModalBehavior { +class ModalBehavior extends Behavior { constructor(element) { - this.element = element; + super(element); const modalOverlay = element.querySelector(".modal-overlay"); const closeButton = element.querySelector("button.close"); diff --git a/bookmarks/frontend/behaviors/tag-autocomplete.js b/bookmarks/frontend/behaviors/tag-autocomplete.js index 24e81a0..58e8e97 100644 --- a/bookmarks/frontend/behaviors/tag-autocomplete.js +++ b/bookmarks/frontend/behaviors/tag-autocomplete.js @@ -1,9 +1,10 @@ -import { registerBehavior } from "./index"; +import { Behavior, registerBehavior } from "./index"; import TagAutoCompleteComponent from "../components/TagAutocomplete.svelte"; import { ApiClient } from "../api"; -class TagAutocomplete { +class TagAutocomplete extends Behavior { constructor(element) { + super(element); const wrapper = document.createElement("div"); const apiBaseUrl = document.documentElement.dataset.apiBaseUrl || ""; const apiClient = new ApiClient(apiBaseUrl); diff --git a/bookmarks/templates/bookmarks/details/assets.html b/bookmarks/templates/bookmarks/details/assets.html index fde3c4f..d857657 100644 --- a/bookmarks/templates/bookmarks/details/assets.html +++ b/bookmarks/templates/bookmarks/details/assets.html @@ -1,37 +1,44 @@ -{% if details.assets %} -