mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-02-28 06:53:12 +08:00
Fix web component initialization timing
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
class BookmarkPage extends HTMLElement {
|
import { HeadlessElement } from "../utils/element.js";
|
||||||
connectedCallback() {
|
|
||||||
|
class BookmarkPage extends HeadlessElement {
|
||||||
|
init() {
|
||||||
this.update = this.update.bind(this);
|
this.update = this.update.bind(this);
|
||||||
this.onToggleNotes = this.onToggleNotes.bind(this);
|
this.onToggleNotes = this.onToggleNotes.bind(this);
|
||||||
this.onToggleBulkEdit = this.onToggleBulkEdit.bind(this);
|
this.onToggleBulkEdit = this.onToggleBulkEdit.bind(this);
|
||||||
@@ -17,13 +19,11 @@ class BookmarkPage extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
requestAnimationFrame(() => {
|
const items = this.querySelectorAll("ul.bookmark-list > li");
|
||||||
const items = this.querySelectorAll("ul.bookmark-list > li");
|
this.updateTooltips(items);
|
||||||
this.updateTooltips(items);
|
this.updateNotesToggles(items, this.oldItems);
|
||||||
this.updateNotesToggles(items, this.oldItems);
|
this.updateBulkEdit(items, this.oldItems);
|
||||||
this.updateBulkEdit(items, this.oldItems);
|
this.oldItems = items;
|
||||||
this.oldItems = items;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTooltips(items) {
|
updateTooltips(items) {
|
||||||
|
|||||||
@@ -1,28 +1,19 @@
|
|||||||
class ClearButton extends HTMLElement {
|
import { HeadlessElement } from "../utils/element";
|
||||||
connectedCallback() {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.field = document.getElementById(this.dataset.for);
|
|
||||||
if (!this.field) {
|
|
||||||
console.error(`Field with ID ${this.dataset.for} not found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.update = this.update.bind(this);
|
|
||||||
this.clear = this.clear.bind(this);
|
|
||||||
|
|
||||||
this.addEventListener("click", this.clear);
|
class ClearButton extends HeadlessElement {
|
||||||
this.field.addEventListener("input", this.update);
|
init() {
|
||||||
this.field.addEventListener("value-changed", this.update);
|
this.field = document.getElementById(this.dataset.for);
|
||||||
this.update();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
if (!this.field) {
|
if (!this.field) {
|
||||||
|
console.error(`Field with ID ${this.dataset.for} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.removeEventListener("click", this.clear);
|
this.update = this.update.bind(this);
|
||||||
this.field.removeEventListener("input", this.update);
|
this.clear = this.clear.bind(this);
|
||||||
this.field.removeEventListener("value-changed", this.update);
|
|
||||||
|
this.addEventListener("click", this.clear);
|
||||||
|
this.field.addEventListener("input", this.update);
|
||||||
|
this.field.addEventListener("value-changed", this.update);
|
||||||
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class Dropdown extends HTMLElement {
|
import { HeadlessElement } from "../utils/element.js";
|
||||||
|
|
||||||
|
class Dropdown extends HeadlessElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
@@ -8,26 +10,20 @@ class Dropdown extends HTMLElement {
|
|||||||
this.onFocusOut = this.onFocusOut.bind(this);
|
this.onFocusOut = this.onFocusOut.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
init() {
|
||||||
// Defer setup to next frame when children are available in the DOM
|
// Prevent opening the dropdown automatically on focus, so that it only
|
||||||
requestAnimationFrame(() => {
|
// opens on click when JS is enabled
|
||||||
// Prevent opening the dropdown automatically on focus, so that it only
|
this.style.setProperty("--dropdown-focus-display", "none");
|
||||||
// opens on click when JS is enabled
|
this.addEventListener("keydown", this.onEscape);
|
||||||
this.style.setProperty("--dropdown-focus-display", "none");
|
this.addEventListener("focusout", this.onFocusOut);
|
||||||
this.addEventListener("keydown", this.onEscape);
|
|
||||||
this.addEventListener("focusout", this.onFocusOut);
|
|
||||||
|
|
||||||
this.toggle = this.querySelector(".dropdown-toggle");
|
this.toggle = this.querySelector(".dropdown-toggle");
|
||||||
this.toggle.setAttribute("aria-expanded", "false");
|
this.toggle.setAttribute("aria-expanded", "false");
|
||||||
this.toggle.addEventListener("click", this.onClick);
|
this.toggle.addEventListener("click", this.onClick);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this.close();
|
this.close();
|
||||||
this.toggle?.removeEventListener("click", this.onClick);
|
|
||||||
this.removeEventListener("keydown", this.onEscape);
|
|
||||||
this.removeEventListener("focusout", this.onFocusOut);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { html, render } from "lit";
|
import { html, render } from "lit";
|
||||||
import { Modal } from "./modal.js";
|
import { Modal } from "./modal.js";
|
||||||
|
import { HeadlessElement } from "../utils/element.js";
|
||||||
import { isKeyboardActive } from "../utils/focus.js";
|
import { isKeyboardActive } from "../utils/focus.js";
|
||||||
|
|
||||||
class FilterDrawerTrigger extends HTMLElement {
|
class FilterDrawerTrigger extends HeadlessElement {
|
||||||
connectedCallback() {
|
init() {
|
||||||
this.onClick = this.onClick.bind(this);
|
this.onClick = this.onClick.bind(this);
|
||||||
this.addEventListener("click", this.onClick.bind(this));
|
this.addEventListener("click", this.onClick.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
this.removeEventListener("click", this.onClick.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
onClick() {
|
onClick() {
|
||||||
const modal = document.createElement("ld-filter-drawer");
|
const modal = document.createElement("ld-filter-drawer");
|
||||||
document.body.querySelector(".modals").appendChild(modal);
|
document.body.querySelector(".modals").appendChild(modal);
|
||||||
@@ -67,8 +64,8 @@ class FilterDrawer extends Modal {
|
|||||||
this.getBoundingClientRect();
|
this.getBoundingClientRect();
|
||||||
// Add active class to start slide-in animation
|
// Add active class to start slide-in animation
|
||||||
requestAnimationFrame(() => this.classList.add("active"));
|
requestAnimationFrame(() => this.classList.add("active"));
|
||||||
// Call super after rendering to ensure elements are available
|
// Call super.init() after rendering to ensure elements are available
|
||||||
super.connectedCallback();
|
super.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
class Form extends HTMLElement {
|
import { HeadlessElement } from "../utils/element.js";
|
||||||
|
|
||||||
|
class Form extends HeadlessElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.onKeyDown = this.onKeyDown.bind(this);
|
this.onKeyDown = this.onKeyDown.bind(this);
|
||||||
this.onChange = this.onChange.bind(this);
|
this.onChange = this.onChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
init() {
|
||||||
this.addEventListener("keydown", this.onKeyDown);
|
this.addEventListener("keydown", this.onKeyDown);
|
||||||
this.addEventListener("change", this.onChange);
|
this.addEventListener("change", this.onChange);
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
if (this.hasAttribute("data-form-reset")) {
|
||||||
if (this.hasAttribute("data-form-reset")) {
|
// Resets form controls to their initial values before Turbo caches the DOM.
|
||||||
// Resets form controls to their initial values before Turbo caches the DOM.
|
// Useful for filter forms where navigating back would otherwise still show
|
||||||
// Useful for filter forms where navigating back would otherwise still show
|
// values from after the form submission, which means the filters would be out
|
||||||
// values from after the form submission, which means the filters would be out
|
// of sync with the URL.
|
||||||
// of sync with the URL.
|
this.initFormReset();
|
||||||
this.initFormReset();
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this.removeEventListener("keydown", this.onKeyDown);
|
|
||||||
this.removeEventListener("change", this.onChange);
|
|
||||||
if (this.hasAttribute("data-form-reset")) {
|
if (this.hasAttribute("data-form-reset")) {
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,23 @@
|
|||||||
import { FocusTrapController } from "../utils/focus.js";
|
import { FocusTrapController } from "../utils/focus.js";
|
||||||
|
import { HeadlessElement } from "../utils/element.js";
|
||||||
|
|
||||||
export class Modal extends HTMLElement {
|
export class Modal extends HeadlessElement {
|
||||||
connectedCallback() {
|
init() {
|
||||||
requestAnimationFrame(() => {
|
this.onClose = this.onClose.bind(this);
|
||||||
this.onClose = this.onClose.bind(this);
|
this.onKeyDown = this.onKeyDown.bind(this);
|
||||||
this.onKeyDown = this.onKeyDown.bind(this);
|
|
||||||
|
|
||||||
this.querySelectorAll("[data-close-modal]").forEach((btn) => {
|
this.querySelectorAll("[data-close-modal]").forEach((btn) => {
|
||||||
btn.addEventListener("click", this.onClose);
|
btn.addEventListener("click", this.onClose);
|
||||||
});
|
|
||||||
document.addEventListener("keydown", this.onKeyDown);
|
|
||||||
|
|
||||||
this.setupScrollLock();
|
|
||||||
this.focusTrap = new FocusTrapController(
|
|
||||||
this.querySelector(".modal-container"),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
this.addEventListener("keydown", this.onKeyDown);
|
||||||
|
|
||||||
|
this.setupScrollLock();
|
||||||
|
this.focusTrap = new FocusTrapController(
|
||||||
|
this.querySelector(".modal-container"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this.querySelectorAll("[data-close-modal]").forEach((btn) => {
|
|
||||||
btn.removeEventListener("click", this.onClose);
|
|
||||||
});
|
|
||||||
document.removeEventListener("keydown", this.onKeyDown);
|
|
||||||
|
|
||||||
this.removeScrollLock();
|
this.removeScrollLock();
|
||||||
this.focusTrap.destroy();
|
this.focusTrap.destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
class UploadButton extends HTMLElement {
|
import { HeadlessElement } from "../utils/element.js";
|
||||||
connectedCallback() {
|
|
||||||
|
class UploadButton extends HeadlessElement {
|
||||||
|
init() {
|
||||||
this.onClick = this.onClick.bind(this);
|
this.onClick = this.onClick.bind(this);
|
||||||
this.onChange = this.onChange.bind(this);
|
this.onChange = this.onChange.bind(this);
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
this.button = this.querySelector('button[type="submit"]');
|
||||||
this.button = this.querySelector('button[type="submit"]');
|
this.button.addEventListener("click", this.onClick);
|
||||||
this.button.addEventListener("click", this.onClick);
|
|
||||||
|
|
||||||
this.fileInput = this.querySelector('input[type="file"]');
|
this.fileInput = this.querySelector('input[type="file"]');
|
||||||
this.fileInput.addEventListener("change", this.onChange);
|
this.fileInput.addEventListener("change", this.onChange);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
this.button.removeEventListener("click", this.onClick);
|
|
||||||
this.fileInput.removeEventListener("change", this.onChange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(event) {
|
onClick(event) {
|
||||||
|
|||||||
@@ -1,5 +1,34 @@
|
|||||||
import { LitElement } from "lit";
|
import { LitElement } from "lit";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for custom elements that wrap existing server-rendered DOM.
|
||||||
|
*
|
||||||
|
* Handles timing issues where connectedCallback fires before child elements
|
||||||
|
* are parsed during initial page load. With Turbo navigation, children are
|
||||||
|
* always available, but on fresh page loads they may not be.
|
||||||
|
*
|
||||||
|
* Subclasses should override init() instead of connectedCallback().
|
||||||
|
*/
|
||||||
|
export class HeadlessElement extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
if (this.__initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.__initialized = true;
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("turbo:load", () => this.init(), {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Override in subclass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let isTopFrameVisit = false;
|
let isTopFrameVisit = false;
|
||||||
|
|
||||||
document.addEventListener("turbo:visit", (event) => {
|
document.addEventListener("turbo:visit", (event) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user