website/web/static/js/alert-modal.js
2024-12-25 15:38:17 -08:00

138 lines
4.3 KiB
JavaScript

/**
* Alert and Confirm modals.
*
* Usage:
*
* modalAlert({message: "Hello world!"}).then(callback);
* modalConfirm({message: "Are you sure?"}).then(callback);
*
* Available options for modalAlert:
* - message
* - title: Alert
*
* Available options for modalConfirm:
* - message
* - title: Confirm
* - buttons: ["Ok", "Cancel"]
* - event (pass `event` for easy inline onclick handlers)
* - element (pass `this` for easy inline onclick handlers)
*
* Example onclick for modalConfirm:
*
* <button onclick="modalConfirm({message: 'Are you sure?', event, element}">Delete</button>
*
* The `element` is used to find the nearest <form> and submit it on OK.
* The `event` is used to cancel the submit button's default.
*/
document.addEventListener('DOMContentLoaded', () => {
const $modal = document.querySelector("#nonshy-alert-modal"),
$ok = $modal.querySelector("button.nonshy-alert-ok-button"),
$cancel = $modal.querySelector("button.nonshy-alert-cancel-button"),
$title = $modal.querySelector("#nonshy-alert-modal-title"),
$body = $modal.querySelector("#nonshy-alert-modal-body"),
alertIcon = `<i class="fa fa-exclamation-triangle mr-2"></i>`,
confirmIcon = `<i class="fa fa-question-circle mr-2"></i>`,
cls = 'is-active';
// Current caller's promise.
var currentPromise = null;
const hideModal = () => {
currentPromise = null;
$modal.classList.remove(cls);
};
const showModal = ({
message,
title="Alert",
isConfirm=false,
buttons=["Ok", "Cancel"],
}) => {
$ok.innerHTML = buttons[0];
$cancel.innerHTML = buttons[1];
$cancel.style.display = isConfirm ? "" : "none";
// Strip HTML from message but allow line breaks.
message = message.replace(/</g, "&lt;");
message = message.replace(/>/g, "&gt;");
message = message.replace(/\n/g, "<br>");
$title.innerHTML = (isConfirm ? confirmIcon : alertIcon) + title;
$body.innerHTML = message;
// Show the modal.
$modal.classList.add(cls);
// Focus the OK button, e.g. so hitting Enter doesn't accidentally (re)click the same
// link/button which prompted the alert box in the first place.
window.requestAnimationFrame(() => {
$ok.focus();
});
// Return as a promise.
return new Promise((resolve, reject) => {
currentPromise = resolve;
});
};
// Click events for the modal buttons.
$ok.addEventListener('click', (e) => {
if (currentPromise !== null) {
currentPromise();
}
hideModal();
});
$cancel.addEventListener('click', (e) => {
hideModal();
});
// Key bindings to dismiss the modal.
window.addEventListener('keydown', (e) => {
if ($modal.classList.contains(cls)) {
if (e.key == 'Enter') {
$ok.click();
} else if (e.key == 'Escape') {
$cancel.click();
}
}
});
// Inline submit button confirmation prompts, e.g.: many submit buttons have name="intent"
// and want the user to confirm before submitting, and had inline onclick handlers.
(document.querySelectorAll('.nonshy-confirm-submit') || []).forEach(button => {
const message = button.dataset.confirm;
if (!message) return;
const onclick = (e) => {
e.preventDefault();
modalConfirm({
message: message.replace(/\\n/g, '\n'),
}).then(() => {
button.removeEventListener('click', onclick);
window.requestAnimationFrame(() => {
button.click();
});
});
}
button.addEventListener('click', onclick);
});
// Exported global functions to invoke the modal.
window.modalAlert = async ({ message, title="Alert" }) => {
return showModal({
message,
title,
isConfirm: false,
});
};
window.modalConfirm = async ({ message, title="Confirm", buttons=["Ok", "Cancel"] }) => {
return showModal({
message,
title,
isConfirm: true,
buttons,
});
};
});