Refresh file list when there are queued snapshots (#697)

* add destroy hook

* refresh details modal in interval

* refactor to refresh assets list

* disable create snapshot button when there is a pending snapshot
This commit is contained in:
Sascha Ißbrücker
2024-04-14 14:41:22 +02:00
committed by GitHub
parent df9f0095cc
commit 1b7731e506
16 changed files with 267 additions and 85 deletions

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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");
});
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 }));

View File

@@ -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));
}

View File

@@ -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) {

View File

@@ -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");

View File

@@ -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);