Files
olivv-cs-ComfyUI-FunPack/app/ui/composer/internals/dismiss.js
T

98 lines
3.2 KiB
JavaScript

// One dismissal stack for the whole app.
//
// Each overlay in v4 wired its own outside-click and Escape handler, so Escape
// inside a modal with an open autocomplete closed the modal -- every listener
// fired, and none of them knew about the others. Here there is a single pair of
// document listeners over a stack, and Escape pops the TOP entry only.
const stack = [];
let listening = false;
function onKeyDown(event) {
if (event.key !== "Escape" || !stack.length) return;
const top = stack[stack.length - 1];
if (!top.closeOnEsc) return;
// stopPropagation matters: without it a parent's own key handler also runs
// and closes a second layer on one press.
event.stopPropagation();
event.preventDefault();
top.dismiss("escape");
}
function onPointerDown(event) {
if (!stack.length) return;
const top = stack[stack.length - 1];
if (!top.closeOnOutside) return;
if (event.target.closest && event.target.closest(".cx-toasts")) return; // a toast belongs to no overlay
for (const node of top.nodes) {
// Guarded: an anchor can be a VIRTUAL point (a context menu is anchored to
// coordinates, not an element), and those have no .contains. Letting one
// through threw on the first click anywhere in the app.
if (node && typeof node.contains === "function" && node.contains(event.target)) return;
}
top.dismiss("outside");
}
function listen() {
if (listening) return;
// Capture phase, so an overlay that stops propagation on its own content
// cannot accidentally suppress dismissal of something above it.
document.addEventListener("keydown", onKeyDown, true);
document.addEventListener("pointerdown", onPointerDown, true);
listening = true;
}
function unlisten() {
if (!listening) return;
document.removeEventListener("keydown", onKeyDown, true);
document.removeEventListener("pointerdown", onPointerDown, true);
listening = false;
}
/**
* push({ nodes, onDismiss, closeOnEsc, closeOnOutside }) -> handle
*
* `nodes` are the regions that count as "inside" -- a popover plus the control
* that opened it, so clicking the trigger again does not dismiss-then-reopen.
*/
export function push({ nodes = [], onDismiss, closeOnEsc = true, closeOnOutside = true } = {}) {
const entry = {
// Only real nodes can answer "was the click inside me".
nodes: [].concat(nodes).filter((n) => n && typeof n.contains === "function"),
closeOnEsc,
closeOnOutside,
done: false,
dismiss(reason) {
if (entry.done) return;
entry.done = true;
remove(entry);
if (onDismiss) onDismiss(reason);
},
};
stack.push(entry);
listen();
return {
dismiss: (reason = "manual") => entry.dismiss(reason),
// Closing without running onDismiss: the caller is already tearing down.
release() {
entry.done = true;
remove(entry);
},
get depth() { return stack.indexOf(entry) + 1; },
get isTop() { return stack[stack.length - 1] === entry; },
};
}
function remove(entry) {
const i = stack.indexOf(entry);
if (i !== -1) stack.splice(i, 1);
if (!stack.length) unlisten();
}
export const depth = () => stack.length;
export function _resetDismiss() {
stack.length = 0;
unlisten();
}