2023-10-10 14:25:18 +02:00
|
|
|
import $ from "jquery";
|
|
|
|
import Micromodal from "micromodal";
|
2023-10-25 02:23:11 +02:00
|
|
|
import assert from "minimalistic-assert";
|
2023-10-10 14:25:18 +02:00
|
|
|
|
|
|
|
import * as blueslip from "./blueslip";
|
|
|
|
import * as overlay_util from "./overlay_util";
|
|
|
|
|
|
|
|
type Hook = () => void;
|
|
|
|
|
|
|
|
export type ModalConfig = {
|
|
|
|
autoremove?: boolean;
|
|
|
|
on_show?: () => void;
|
|
|
|
on_shown?: () => void;
|
|
|
|
on_hide?: () => void;
|
|
|
|
on_hidden?: () => void;
|
|
|
|
};
|
|
|
|
|
|
|
|
const pre_open_hooks: Hook[] = [];
|
|
|
|
const pre_close_hooks: Hook[] = [];
|
|
|
|
|
|
|
|
export function register_pre_open_hook(func: Hook): void {
|
|
|
|
pre_open_hooks.push(func);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function register_pre_close_hook(func: Hook): void {
|
|
|
|
pre_close_hooks.push(func);
|
|
|
|
}
|
|
|
|
|
|
|
|
function call_hooks(func_list: Hook[]): void {
|
|
|
|
for (const element of func_list) {
|
|
|
|
element();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-11 11:19:13 +02:00
|
|
|
export function any_active(): boolean {
|
2023-10-10 14:25:18 +02:00
|
|
|
return $(".micromodal").hasClass("modal--open");
|
|
|
|
}
|
|
|
|
|
|
|
|
export function active_modal(): string | undefined {
|
2023-10-11 11:19:13 +02:00
|
|
|
if (!any_active()) {
|
2023-10-10 14:25:18 +02:00
|
|
|
blueslip.error("Programming error — Called active_modal when there is no modal open");
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const $micromodal = $(".micromodal.modal--open");
|
|
|
|
return `#${CSS.escape($micromodal.attr("id")!)}`;
|
|
|
|
}
|
|
|
|
|
2024-05-15 08:39:25 +02:00
|
|
|
export function is_active(modal_id: string): boolean {
|
|
|
|
const $micromodal = $(".micromodal.modal--open");
|
|
|
|
return $micromodal.attr("id") === modal_id;
|
|
|
|
}
|
|
|
|
|
2023-10-10 14:25:18 +02:00
|
|
|
// If conf.autoremove is true, the modal element will be removed from the DOM
|
|
|
|
// once the modal is hidden.
|
|
|
|
// conf also accepts the following optional properties:
|
|
|
|
// on_show: Callback to run when the modal is triggered to show.
|
|
|
|
// on_shown: Callback to run when the modal is shown.
|
|
|
|
// on_hide: Callback to run when the modal is triggered to hide.
|
|
|
|
// on_hidden: Callback to run when the modal is hidden.
|
2023-10-10 14:33:52 +02:00
|
|
|
export function open(
|
2023-10-10 14:25:18 +02:00
|
|
|
modal_id: string,
|
|
|
|
conf: ModalConfig & {recursive_call_count?: number} = {},
|
|
|
|
): void {
|
|
|
|
if (modal_id === undefined) {
|
2023-10-10 14:33:52 +02:00
|
|
|
blueslip.error("Undefined id was passed into open");
|
2023-10-10 14:25:18 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't accept hash-based selector to enforce modals to have unique ids and
|
|
|
|
// since micromodal doesn't accept hash based selectors.
|
|
|
|
if (modal_id.startsWith("#")) {
|
2023-10-10 14:33:52 +02:00
|
|
|
blueslip.error("hash-based selector passed in to open", {modal_id});
|
2023-10-10 14:25:18 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-10-11 11:19:13 +02:00
|
|
|
if (any_active()) {
|
2023-10-10 14:25:18 +02:00
|
|
|
/*
|
|
|
|
Our modal system doesn't directly support opening a modal
|
2023-10-11 11:19:13 +02:00
|
|
|
when one is already open, because the `any_active` CSS
|
2023-10-10 14:25:18 +02:00
|
|
|
class doesn't update until Micromodal has finished its
|
|
|
|
animations, which can take 100ms or more.
|
|
|
|
|
|
|
|
We can likely fix that, but in the meantime, we should
|
|
|
|
handle this situation correctly, by closing the current
|
|
|
|
modal, waiting for it to finish closing, and then attempting
|
|
|
|
to open the current modal again.
|
|
|
|
*/
|
|
|
|
if (!conf.recursive_call_count) {
|
|
|
|
conf.recursive_call_count = 1;
|
|
|
|
} else {
|
|
|
|
conf.recursive_call_count += 1;
|
|
|
|
}
|
|
|
|
if (conf.recursive_call_count > 50) {
|
|
|
|
blueslip.error("Modal incorrectly is still open", {modal_id});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-10-10 14:31:02 +02:00
|
|
|
close_active();
|
2023-10-10 14:25:18 +02:00
|
|
|
setTimeout(() => {
|
2023-10-10 14:33:52 +02:00
|
|
|
open(modal_id, conf);
|
2023-10-10 14:25:18 +02:00
|
|
|
}, 10);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
blueslip.debug("open modal: " + modal_id);
|
|
|
|
|
|
|
|
// Micromodal gets elements using the getElementById DOM function
|
|
|
|
// which doesn't require the hash. We add it manually here.
|
|
|
|
const id_selector = `#${CSS.escape(modal_id)}`;
|
|
|
|
const $micromodal = $(id_selector);
|
|
|
|
|
|
|
|
$micromodal.find(".modal__container").on("animationend", (event) => {
|
2023-10-25 02:23:11 +02:00
|
|
|
assert(event.originalEvent instanceof AnimationEvent);
|
|
|
|
const animation_name = event.originalEvent.animationName;
|
2023-10-10 14:25:18 +02:00
|
|
|
if (animation_name === "mmfadeIn") {
|
|
|
|
// Micromodal adds the is-open class before the modal animation
|
|
|
|
// is complete, which isn't really helpful since a modal is open after the
|
|
|
|
// animation is complete. So, we manually add a class after the
|
|
|
|
// animation is complete.
|
|
|
|
$micromodal.addClass("modal--open");
|
|
|
|
$micromodal.removeClass("modal--opening");
|
|
|
|
|
|
|
|
if (conf.on_shown) {
|
|
|
|
conf.on_shown();
|
|
|
|
}
|
|
|
|
} else if (animation_name === "mmfadeOut") {
|
|
|
|
// Call the on_hidden callback after the modal finishes hiding.
|
|
|
|
|
|
|
|
$micromodal.removeClass("modal--open");
|
|
|
|
if (conf.autoremove) {
|
|
|
|
$micromodal.remove();
|
|
|
|
}
|
|
|
|
if (conf.on_hidden) {
|
|
|
|
conf.on_hidden();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
$micromodal.find(".modal__overlay").on("click", (e) => {
|
|
|
|
/* Micromodal's data-micromodal-close feature doesn't check for
|
|
|
|
range selections; this means dragging a selection of text in an
|
|
|
|
input inside the modal too far will weirdly close the modal.
|
|
|
|
See https://github.com/ghosh/Micromodal/issues/505.
|
|
|
|
Work around this with our own implementation. */
|
|
|
|
if (!$(e.target).is(".modal__overlay")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (document.getSelection()?.type === "Range") {
|
|
|
|
return;
|
|
|
|
}
|
2023-10-10 14:32:20 +02:00
|
|
|
close(modal_id);
|
2023-10-10 14:25:18 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
function on_show_callback(): void {
|
|
|
|
if (conf.on_show) {
|
|
|
|
conf.on_show();
|
|
|
|
}
|
|
|
|
overlay_util.disable_scrolling();
|
|
|
|
call_hooks(pre_open_hooks);
|
|
|
|
}
|
|
|
|
|
|
|
|
function on_close_callback(): void {
|
|
|
|
if (conf.on_hide) {
|
|
|
|
conf.on_hide();
|
|
|
|
}
|
|
|
|
overlay_util.enable_scrolling();
|
|
|
|
call_hooks(pre_close_hooks);
|
|
|
|
}
|
|
|
|
|
|
|
|
Micromodal.show(modal_id, {
|
|
|
|
disableFocus: true,
|
|
|
|
openClass: "modal--opening",
|
|
|
|
onShow: on_show_callback,
|
|
|
|
onClose: on_close_callback,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// `conf` is an object with the following optional properties:
|
|
|
|
// * on_hidden: Callback to run when the modal finishes hiding.
|
2023-10-10 14:32:20 +02:00
|
|
|
export function close(modal_id: string, conf: Pick<ModalConfig, "on_hidden"> = {}): void {
|
2023-10-10 14:25:18 +02:00
|
|
|
if (modal_id === undefined) {
|
2023-10-10 14:32:20 +02:00
|
|
|
blueslip.error("Undefined id was passed into close");
|
2023-10-10 14:25:18 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-10-11 11:19:13 +02:00
|
|
|
if (!any_active()) {
|
|
|
|
blueslip.warn("close_active() called without checking any_active()");
|
2023-10-10 14:25:18 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (active_modal() !== `#${CSS.escape(modal_id)}`) {
|
|
|
|
blueslip.error("Trying to close modal when other is open", {modal_id, active_modal});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
blueslip.debug("close modal: " + modal_id);
|
|
|
|
|
|
|
|
const id_selector = `#${CSS.escape(modal_id)}`;
|
|
|
|
const $micromodal = $(id_selector);
|
|
|
|
|
|
|
|
// On-hidden hooks should typically be registered in
|
2023-10-10 14:33:52 +02:00
|
|
|
// overlays.open. However, we offer this alternative
|
2023-10-10 14:25:18 +02:00
|
|
|
// mechanism as a convenience for hooks only known when
|
|
|
|
// closing the modal.
|
|
|
|
$micromodal.find(".modal__container").on("animationend", (event) => {
|
2023-10-25 02:23:11 +02:00
|
|
|
assert(event.originalEvent instanceof AnimationEvent);
|
|
|
|
const animation_name = event.originalEvent.animationName;
|
2023-10-10 14:25:18 +02:00
|
|
|
if (animation_name === "mmfadeOut" && conf.on_hidden) {
|
|
|
|
conf.on_hidden();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
Micromodal.close(modal_id);
|
|
|
|
}
|
|
|
|
|
2023-10-10 14:31:36 +02:00
|
|
|
export function close_if_open(modal_id: string): void {
|
2023-10-10 14:25:18 +02:00
|
|
|
if (modal_id === undefined) {
|
2023-10-10 14:31:36 +02:00
|
|
|
blueslip.error("Undefined id was passed into close_if_open");
|
2023-10-10 14:25:18 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-10-11 11:19:13 +02:00
|
|
|
if (!any_active()) {
|
2023-10-10 14:25:18 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const $micromodal = $(".micromodal.modal--open");
|
2024-01-29 20:25:24 +01:00
|
|
|
const active_modal_id = CSS.escape(CSS.escape($micromodal.attr("id") ?? ""));
|
|
|
|
if (active_modal_id === CSS.escape(modal_id)) {
|
|
|
|
Micromodal.close(CSS.escape($micromodal.attr("id") ?? ""));
|
2023-10-10 14:25:18 +02:00
|
|
|
} else {
|
|
|
|
blueslip.info(
|
|
|
|
`${active_modal_id} is the currently active modal and ${modal_id} is already closed.`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-10 14:31:02 +02:00
|
|
|
export function close_active(): void {
|
2023-10-11 11:19:13 +02:00
|
|
|
if (!any_active()) {
|
|
|
|
blueslip.warn("close_active() called without checking any_active()");
|
2023-10-10 14:25:18 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const $micromodal = $(".micromodal.modal--open");
|
2024-01-29 20:25:24 +01:00
|
|
|
Micromodal.close(CSS.escape($micromodal.attr("id") ?? ""));
|
2023-10-10 14:25:18 +02:00
|
|
|
}
|
2023-10-22 21:58:33 +02:00
|
|
|
|
|
|
|
export function close_active_if_any(): void {
|
|
|
|
if (any_active()) {
|
|
|
|
close_active();
|
|
|
|
}
|
|
|
|
}
|