mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-28 21:06:39 +02:00
174 lines
4.6 KiB
JavaScript
174 lines
4.6 KiB
JavaScript
import { Behavior, registerBehavior } from "./index";
|
|
import { FocusTrapController, isKeyboardActive } from "./focus-utils";
|
|
|
|
let confirmId = 0;
|
|
|
|
function nextConfirmId() {
|
|
return `confirm-${confirmId++}`;
|
|
}
|
|
|
|
class ConfirmButtonBehavior extends Behavior {
|
|
constructor(element) {
|
|
super(element);
|
|
|
|
this.onClick = this.onClick.bind(this);
|
|
this.element.addEventListener("click", this.onClick);
|
|
}
|
|
|
|
destroy() {
|
|
if (this.opened) {
|
|
this.close();
|
|
}
|
|
this.element.removeEventListener("click", this.onClick);
|
|
}
|
|
|
|
onClick(event) {
|
|
event.preventDefault();
|
|
|
|
if (this.opened) {
|
|
this.close();
|
|
} else {
|
|
this.open();
|
|
}
|
|
}
|
|
|
|
open() {
|
|
const dropdown = document.createElement("div");
|
|
dropdown.className = "dropdown confirm-dropdown active";
|
|
|
|
const confirmId = nextConfirmId();
|
|
const questionId = `${confirmId}-question`;
|
|
|
|
const menu = document.createElement("div");
|
|
menu.className = "menu with-arrow";
|
|
menu.role = "alertdialog";
|
|
menu.setAttribute("aria-modal", "true");
|
|
menu.setAttribute("aria-labelledby", questionId);
|
|
menu.addEventListener("keydown", this.onMenuKeyDown.bind(this));
|
|
|
|
const question = document.createElement("span");
|
|
question.id = questionId;
|
|
question.textContent =
|
|
this.element.getAttribute("ld-confirm-question") || "Are you sure?";
|
|
question.style.fontWeight = "bold";
|
|
|
|
const cancelButton = document.createElement("button");
|
|
cancelButton.textContent = "Cancel";
|
|
cancelButton.type = "button";
|
|
cancelButton.className = "btn";
|
|
cancelButton.tabIndex = 0;
|
|
cancelButton.addEventListener("click", () => this.close());
|
|
|
|
const confirmButton = document.createElement("button");
|
|
confirmButton.textContent = "Confirm";
|
|
confirmButton.type = "submit";
|
|
confirmButton.name = this.element.name;
|
|
confirmButton.value = this.element.value;
|
|
confirmButton.className = "btn btn-error";
|
|
confirmButton.addEventListener("click", () => this.confirm());
|
|
|
|
const arrow = document.createElement("div");
|
|
arrow.className = "menu-arrow";
|
|
|
|
menu.append(question, cancelButton, confirmButton, arrow);
|
|
dropdown.append(menu);
|
|
document.body.append(dropdown);
|
|
|
|
this.positionController = new AnchorPositionController(this.element, menu);
|
|
this.focusTrap = new FocusTrapController(menu);
|
|
this.dropdown = dropdown;
|
|
this.opened = true;
|
|
}
|
|
|
|
onMenuKeyDown(event) {
|
|
if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
confirm() {
|
|
this.element.closest("form").requestSubmit(this.element);
|
|
this.close();
|
|
}
|
|
|
|
close() {
|
|
if (!this.opened) return;
|
|
this.positionController.destroy();
|
|
this.focusTrap.destroy();
|
|
this.dropdown.remove();
|
|
this.element.focus({ focusVisible: isKeyboardActive() });
|
|
this.opened = false;
|
|
}
|
|
}
|
|
|
|
class AnchorPositionController {
|
|
constructor(anchor, overlay) {
|
|
this.anchor = anchor;
|
|
this.overlay = overlay;
|
|
|
|
this.handleScroll = this.handleScroll.bind(this);
|
|
window.addEventListener("scroll", this.handleScroll, { capture: true });
|
|
|
|
this.updatePosition();
|
|
}
|
|
|
|
handleScroll() {
|
|
if (this.debounce) {
|
|
return;
|
|
}
|
|
|
|
this.debounce = true;
|
|
|
|
requestAnimationFrame(() => {
|
|
this.updatePosition();
|
|
this.debounce = false;
|
|
});
|
|
}
|
|
|
|
updatePosition() {
|
|
const anchorRect = this.anchor.getBoundingClientRect();
|
|
const overlayRect = this.overlay.getBoundingClientRect();
|
|
const bufferX = 10;
|
|
const bufferY = 30;
|
|
|
|
let left = anchorRect.left - (overlayRect.width - anchorRect.width) / 2;
|
|
const initialLeft = left;
|
|
const overflowLeft = left < bufferX;
|
|
const overflowRight =
|
|
left + overlayRect.width > window.innerWidth - bufferX;
|
|
|
|
if (overflowLeft) {
|
|
left = bufferX;
|
|
} else if (overflowRight) {
|
|
left = window.innerWidth - overlayRect.width - bufferX;
|
|
}
|
|
|
|
const delta = initialLeft - left;
|
|
this.overlay.style.setProperty("--arrow-offset", `${delta}px`);
|
|
|
|
let top = anchorRect.bottom;
|
|
const overflowBottom =
|
|
top + overlayRect.height > window.innerHeight - bufferY;
|
|
|
|
if (overflowBottom) {
|
|
top = anchorRect.top - overlayRect.height;
|
|
this.overlay.classList.remove("top-aligned");
|
|
this.overlay.classList.add("bottom-aligned");
|
|
} else {
|
|
this.overlay.classList.remove("bottom-aligned");
|
|
this.overlay.classList.add("top-aligned");
|
|
}
|
|
|
|
this.overlay.style.left = `${left}px`;
|
|
this.overlay.style.top = `${top}px`;
|
|
}
|
|
|
|
destroy() {
|
|
window.removeEventListener("scroll", this.handleScroll, { capture: true });
|
|
}
|
|
}
|
|
|
|
registerBehavior("ld-confirm-button", ConfirmButtonBehavior);
|