300 lines
9.1 KiB
JavaScript
300 lines
9.1 KiB
JavaScript
|
|
(function () {
|
||
|
|
const VARIANTS = ["info", "warning", "error"];
|
||
|
|
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||
|
|
|
||
|
|
window.Warpbox = window.Warpbox || {};
|
||
|
|
let dialogIdCounter = 0;
|
||
|
|
|
||
|
|
function defaultTitle(variant) {
|
||
|
|
if (variant === "error") {
|
||
|
|
return "Error";
|
||
|
|
}
|
||
|
|
if (variant === "warning") {
|
||
|
|
return "Warning";
|
||
|
|
}
|
||
|
|
return "Info";
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeOptions(options, message) {
|
||
|
|
if (typeof options === "string") {
|
||
|
|
options = { message: options };
|
||
|
|
} else {
|
||
|
|
options = options || {};
|
||
|
|
}
|
||
|
|
if (message) {
|
||
|
|
options.message = message;
|
||
|
|
}
|
||
|
|
const variant = VARIANTS.includes(options.variant) ? options.variant : "info";
|
||
|
|
return {
|
||
|
|
variant,
|
||
|
|
title: options.title || defaultTitle(variant),
|
||
|
|
message: options.message || "",
|
||
|
|
body: options.body || null,
|
||
|
|
actions: Array.isArray(options.actions) ? options.actions : [],
|
||
|
|
dismissible: options.dismissible !== false,
|
||
|
|
closable: options.closable !== false,
|
||
|
|
onClose: typeof options.onClose === "function" ? options.onClose : null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function focusableElements(container) {
|
||
|
|
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => el.offsetParent !== null);
|
||
|
|
}
|
||
|
|
|
||
|
|
function dialog(options, message) {
|
||
|
|
const config = normalizeOptions(options, message);
|
||
|
|
const previouslyFocused = document.activeElement;
|
||
|
|
dialogIdCounter += 1;
|
||
|
|
const titleId = "warpbox-dialog-title-" + dialogIdCounter;
|
||
|
|
|
||
|
|
const overlay = document.createElement("div");
|
||
|
|
overlay.className = "warpbox-dialog-overlay";
|
||
|
|
|
||
|
|
const card = document.createElement("div");
|
||
|
|
card.className = "warpbox-dialog warpbox-dialog-" + config.variant;
|
||
|
|
card.setAttribute("role", config.variant === "error" ? "alertdialog" : "dialog");
|
||
|
|
card.setAttribute("aria-modal", "true");
|
||
|
|
card.setAttribute("aria-labelledby", titleId);
|
||
|
|
card.setAttribute("tabindex", "-1");
|
||
|
|
|
||
|
|
const head = document.createElement("div");
|
||
|
|
head.className = "warpbox-dialog-head";
|
||
|
|
|
||
|
|
const icon = document.createElement("span");
|
||
|
|
icon.className = "warpbox-dialog-icon";
|
||
|
|
icon.setAttribute("aria-hidden", "true");
|
||
|
|
icon.textContent = config.variant === "error" ? "!" : config.variant === "warning" ? "?" : "i";
|
||
|
|
|
||
|
|
const title = document.createElement("h2");
|
||
|
|
title.id = titleId;
|
||
|
|
title.className = "warpbox-dialog-title";
|
||
|
|
title.textContent = config.title;
|
||
|
|
|
||
|
|
head.append(icon, title);
|
||
|
|
|
||
|
|
if (config.closable) {
|
||
|
|
const close = document.createElement("button");
|
||
|
|
close.type = "button";
|
||
|
|
close.className = "warpbox-dialog-close";
|
||
|
|
close.setAttribute("aria-label", "Close dialog");
|
||
|
|
close.textContent = "x";
|
||
|
|
close.addEventListener("click", () => closeDialog());
|
||
|
|
head.append(close);
|
||
|
|
}
|
||
|
|
|
||
|
|
const body = document.createElement("div");
|
||
|
|
body.className = "warpbox-dialog-body";
|
||
|
|
|
||
|
|
if (config.message) {
|
||
|
|
const text = document.createElement("p");
|
||
|
|
text.className = "warpbox-dialog-message";
|
||
|
|
text.textContent = config.message;
|
||
|
|
body.append(text);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (config.body) {
|
||
|
|
const nodes = Array.isArray(config.body) ? config.body : [config.body];
|
||
|
|
nodes.forEach((node) => {
|
||
|
|
if (node instanceof Node) {
|
||
|
|
body.append(node);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
card.append(head, body);
|
||
|
|
|
||
|
|
let autofocusTarget = null;
|
||
|
|
if (config.actions.length > 0) {
|
||
|
|
const actions = document.createElement("div");
|
||
|
|
actions.className = "warpbox-dialog-actions";
|
||
|
|
config.actions.forEach((action) => {
|
||
|
|
const button = document.createElement("button");
|
||
|
|
button.type = "button";
|
||
|
|
button.className = "button " + (action.kind === "primary" ? "button-primary" : action.kind === "ghost" ? "button-ghost" : "button-outline");
|
||
|
|
button.textContent = action.label || "OK";
|
||
|
|
button.addEventListener("click", () => {
|
||
|
|
if (typeof action.onClick === "function") {
|
||
|
|
action.onClick();
|
||
|
|
}
|
||
|
|
if (action.dismiss !== false) {
|
||
|
|
closeDialog();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
if (action.autofocus) {
|
||
|
|
autofocusTarget = button;
|
||
|
|
}
|
||
|
|
actions.append(button);
|
||
|
|
});
|
||
|
|
card.append(actions);
|
||
|
|
}
|
||
|
|
|
||
|
|
overlay.append(card);
|
||
|
|
document.body.append(overlay);
|
||
|
|
document.documentElement.classList.add("warpbox-dialog-open");
|
||
|
|
window.requestAnimationFrame(() => {
|
||
|
|
overlay.classList.add("is-visible");
|
||
|
|
(autofocusTarget || card).focus();
|
||
|
|
});
|
||
|
|
|
||
|
|
function handleKeydown(event) {
|
||
|
|
if (event.key === "Escape") {
|
||
|
|
if (config.dismissible) {
|
||
|
|
event.preventDefault();
|
||
|
|
closeDialog();
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (event.key !== "Tab") {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const focusable = focusableElements(card);
|
||
|
|
if (focusable.length === 0) {
|
||
|
|
event.preventDefault();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const first = focusable[0];
|
||
|
|
const last = focusable[focusable.length - 1];
|
||
|
|
if (event.shiftKey && document.activeElement === first) {
|
||
|
|
event.preventDefault();
|
||
|
|
last.focus();
|
||
|
|
} else if (!event.shiftKey && document.activeElement === last) {
|
||
|
|
event.preventDefault();
|
||
|
|
first.focus();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleOverlayClick(event) {
|
||
|
|
if (config.dismissible && event.target === overlay) {
|
||
|
|
closeDialog();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
document.addEventListener("keydown", handleKeydown, true);
|
||
|
|
overlay.addEventListener("click", handleOverlayClick);
|
||
|
|
|
||
|
|
let closed = false;
|
||
|
|
function closeDialog() {
|
||
|
|
if (closed) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
closed = true;
|
||
|
|
document.removeEventListener("keydown", handleKeydown, true);
|
||
|
|
overlay.removeEventListener("click", handleOverlayClick);
|
||
|
|
overlay.classList.remove("is-visible");
|
||
|
|
document.documentElement.classList.remove("warpbox-dialog-open");
|
||
|
|
window.setTimeout(() => overlay.remove(), 180);
|
||
|
|
if (previouslyFocused && typeof previouslyFocused.focus === "function") {
|
||
|
|
previouslyFocused.focus();
|
||
|
|
}
|
||
|
|
if (config.onClose) {
|
||
|
|
config.onClose();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
element: overlay,
|
||
|
|
close: closeDialog,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
window.Warpbox.dialog = dialog;
|
||
|
|
|
||
|
|
window.Warpbox.alertDialog = function alertDialog(message, options) {
|
||
|
|
const config = (typeof options === "object" && options) || {};
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
dialog({
|
||
|
|
...config,
|
||
|
|
message: typeof message === "string" ? message : config.message,
|
||
|
|
actions: [{ label: config.okLabel || "OK", kind: "primary", autofocus: true }],
|
||
|
|
onClose: () => {
|
||
|
|
if (typeof config.onClose === "function") {
|
||
|
|
config.onClose();
|
||
|
|
}
|
||
|
|
resolve();
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
window.Warpbox.confirmDialog = function confirmDialog(message, options) {
|
||
|
|
const config = (typeof options === "object" && options) || {};
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
let settled = false;
|
||
|
|
function settle(value) {
|
||
|
|
if (settled) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
settled = true;
|
||
|
|
resolve(value);
|
||
|
|
}
|
||
|
|
dialog({
|
||
|
|
...config,
|
||
|
|
message: typeof message === "string" ? message : config.message,
|
||
|
|
actions: [
|
||
|
|
{ label: config.cancelLabel || "Cancel", kind: "outline", autofocus: true, onClick: () => settle(false) },
|
||
|
|
{ label: config.confirmLabel || "Confirm", kind: "primary", onClick: () => settle(true) },
|
||
|
|
],
|
||
|
|
onClose: () => {
|
||
|
|
if (typeof config.onClose === "function") {
|
||
|
|
config.onClose();
|
||
|
|
}
|
||
|
|
settle(false);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
window.Warpbox.promptDialog = function promptDialog(message, options) {
|
||
|
|
const config = (typeof options === "object" && options) || {};
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
let settled = false;
|
||
|
|
function settle(value) {
|
||
|
|
if (settled) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
settled = true;
|
||
|
|
resolve(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
const field = document.createElement("input");
|
||
|
|
field.type = config.inputType || "text";
|
||
|
|
field.className = "warpbox-dialog-field";
|
||
|
|
if (config.placeholder) {
|
||
|
|
field.placeholder = config.placeholder;
|
||
|
|
}
|
||
|
|
if (typeof config.value === "string") {
|
||
|
|
field.value = config.value;
|
||
|
|
}
|
||
|
|
|
||
|
|
let controller = null;
|
||
|
|
field.addEventListener("keydown", (event) => {
|
||
|
|
if (event.key === "Enter") {
|
||
|
|
event.preventDefault();
|
||
|
|
settle(field.value);
|
||
|
|
if (controller) {
|
||
|
|
controller.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
controller = dialog({
|
||
|
|
...config,
|
||
|
|
message: typeof message === "string" ? message : config.message,
|
||
|
|
body: field,
|
||
|
|
actions: [
|
||
|
|
{ label: config.cancelLabel || "Cancel", kind: "outline", onClick: () => settle(null) },
|
||
|
|
{ label: config.okLabel || "OK", kind: "primary", onClick: () => settle(field.value) },
|
||
|
|
],
|
||
|
|
onClose: () => {
|
||
|
|
if (typeof config.onClose === "function") {
|
||
|
|
config.onClose();
|
||
|
|
}
|
||
|
|
settle(null);
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
window.requestAnimationFrame(() => field.focus());
|
||
|
|
});
|
||
|
|
};
|
||
|
|
})();
|