forked from retoor/devplacepy
A member whose account has not accepted the terms in force now gets one dialog on the action they attempted instead of a dead-end refusal. The client handler is the single TermsGate, wired into every Http POST helper so the four optimistic controllers cannot swallow the gate into an error flash, and the original request is replayed once the acceptance is recorded. Reading the site and deleting an account stay unblocked. apple.md is the source brief the compliance research documents reference. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
169 lines
6.4 KiB
JavaScript
169 lines
6.4 KiB
JavaScript
// retoor <retoor@molodetz.nl>
|
|
|
|
import { Component } from "./Component.js";
|
|
|
|
export class AppDialog extends Component {
|
|
connectedCallback() {
|
|
if (this._built) {
|
|
return;
|
|
}
|
|
this._built = true;
|
|
this.resolver = null;
|
|
this.mode = "confirm";
|
|
this.lastFocus = null;
|
|
this.build();
|
|
}
|
|
|
|
build() {
|
|
const overlay = document.createElement("div");
|
|
overlay.className = "dialog-overlay";
|
|
overlay.setAttribute("role", "dialog");
|
|
overlay.setAttribute("aria-modal", "true");
|
|
overlay.innerHTML =
|
|
'<div class="card dialog-card">' +
|
|
'<div class="modal-header"><h3 class="dialog-title"></h3>' +
|
|
'<button type="button" class="modal-close btn-ghost btn-icon dialog-close">×</button></div>' +
|
|
'<p class="dialog-message"></p>' +
|
|
'<ul class="dialog-links" hidden></ul>' +
|
|
'<div class="dialog-field" hidden><label class="dialog-field-label"></label>' +
|
|
'<input type="text" class="dialog-input" autocomplete="off"></div>' +
|
|
'<div class="modal-footer">' +
|
|
'<button type="button" class="btn btn-secondary dialog-cancel">Cancel</button>' +
|
|
'<button type="button" class="btn btn-primary dialog-confirm">OK</button>' +
|
|
"</div></div>";
|
|
this.appendChild(overlay);
|
|
|
|
this.overlay = overlay;
|
|
this.titleEl = overlay.querySelector(".dialog-title");
|
|
this.messageEl = overlay.querySelector(".dialog-message");
|
|
const uid = Math.random().toString(36).slice(2, 9);
|
|
this.titleEl.id = `dialog-title-${uid}`;
|
|
this.messageEl.id = `dialog-message-${uid}`;
|
|
overlay.setAttribute("aria-labelledby", this.titleEl.id);
|
|
overlay.setAttribute("aria-describedby", this.messageEl.id);
|
|
this.links = overlay.querySelector(".dialog-links");
|
|
this.field = overlay.querySelector(".dialog-field");
|
|
this.fieldLabel = overlay.querySelector(".dialog-field-label");
|
|
this.input = overlay.querySelector(".dialog-input");
|
|
this.cancelBtn = overlay.querySelector(".dialog-cancel");
|
|
this.confirmBtn = overlay.querySelector(".dialog-confirm");
|
|
|
|
overlay.addEventListener("click", (e) => {
|
|
if (e.target === overlay) this.dismiss();
|
|
});
|
|
overlay.querySelector(".dialog-close").addEventListener("click", () => this.dismiss());
|
|
this.cancelBtn.addEventListener("click", () => this.dismiss());
|
|
this.confirmBtn.addEventListener("click", () => this.accept());
|
|
this.input.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
this.accept();
|
|
}
|
|
});
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape" && this.overlay.classList.contains("visible")) {
|
|
e.preventDefault();
|
|
this.dismiss();
|
|
}
|
|
});
|
|
overlay.addEventListener("keydown", (e) => {
|
|
if (e.key !== "Tab") return;
|
|
const items = [
|
|
...overlay.querySelectorAll(
|
|
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
),
|
|
].filter((el) => el.offsetParent !== null);
|
|
if (!items.length) return;
|
|
const first = items[0];
|
|
const last = items[items.length - 1];
|
|
if (e.shiftKey && document.activeElement === first) {
|
|
e.preventDefault();
|
|
last.focus();
|
|
} else if (!e.shiftKey && document.activeElement === last) {
|
|
e.preventDefault();
|
|
first.focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
open(mode, options) {
|
|
const opts = options || {};
|
|
this.mode = mode;
|
|
this.titleEl.textContent = opts.title || (mode === "prompt" ? "Rename" : mode === "alert" ? "Notice" : "Confirm");
|
|
this.messageEl.textContent = opts.message || "";
|
|
this.messageEl.hidden = !opts.message;
|
|
this.confirmBtn.textContent = opts.confirmLabel || (mode === "alert" ? "OK" : mode === "prompt" ? "Save" : "Confirm");
|
|
this.cancelBtn.textContent = opts.cancelLabel || "Cancel";
|
|
this.cancelBtn.style.display = mode === "alert" ? "none" : "";
|
|
this.confirmBtn.classList.toggle("dialog-danger", !!opts.danger);
|
|
this.renderLinks(opts.links);
|
|
|
|
if (mode === "prompt") {
|
|
this.field.hidden = false;
|
|
this.fieldLabel.textContent = opts.label || "";
|
|
this.fieldLabel.hidden = !opts.label;
|
|
this.input.value = opts.value || "";
|
|
this.input.placeholder = opts.placeholder || "";
|
|
} else {
|
|
this.field.hidden = true;
|
|
}
|
|
|
|
this.lastFocus = document.activeElement;
|
|
this.overlay.classList.add("visible");
|
|
const focusTarget = mode === "prompt" ? this.input : this.confirmBtn;
|
|
setTimeout(() => {
|
|
focusTarget.focus();
|
|
if (mode === "prompt") this.input.select();
|
|
}, 20);
|
|
|
|
return new Promise((resolve) => { this.resolver = resolve; });
|
|
}
|
|
|
|
renderLinks(links) {
|
|
const items = Array.isArray(links) ? links : [];
|
|
this.links.replaceChildren();
|
|
this.links.hidden = !items.length;
|
|
for (const item of items) {
|
|
const anchor = document.createElement("a");
|
|
anchor.href = item.href;
|
|
anchor.textContent = item.label;
|
|
anchor.target = "_blank";
|
|
anchor.rel = "noopener";
|
|
const row = document.createElement("li");
|
|
row.appendChild(anchor);
|
|
this.links.appendChild(row);
|
|
}
|
|
}
|
|
|
|
accept() {
|
|
const value = this.mode === "prompt" ? this.input.value : true;
|
|
this.close(value);
|
|
}
|
|
|
|
dismiss() {
|
|
this.close(this.mode === "prompt" ? null : this.mode === "alert" ? undefined : false);
|
|
}
|
|
|
|
close(value) {
|
|
this.overlay.classList.remove("visible");
|
|
if (this.lastFocus && this.lastFocus.focus) this.lastFocus.focus();
|
|
const resolve = this.resolver;
|
|
this.resolver = null;
|
|
if (resolve) resolve(value);
|
|
}
|
|
|
|
confirm(options) {
|
|
return this.open("confirm", options);
|
|
}
|
|
|
|
prompt(options) {
|
|
return this.open("prompt", options);
|
|
}
|
|
|
|
alert(options) {
|
|
return this.open("alert", options);
|
|
}
|
|
}
|
|
|
|
customElements.define("dp-dialog", AppDialog);
|