# Source: Dialogs.js

    /**
     * Low-level dialog management.
     * Also used for menus as a menu is a kind of dialog.
     * <p>Part of the XUI module which, for now, has an undocumented API.
     */
    export class Dialogs {
        /**
         * Create and show a dialog containing specified element.
         */
        static open(options) {
            let opts = Object.assign({
                form: null,
                type: "modal",
                position: "center", reference: null,
                classes: null
            }, options);
            let form = opts.form;
            let type = opts.type;
            let position = opts.position;
            let reference = opts.reference;
            let classes = opts.classes;

            // --------------
            // Implementation
            // --------------
            // * Each layer has Dialogs.LAYER_CAPACITY z-index capacity.
            // * Layer #0 contains all non modal dialogs (type=dialog),
            // each having a different z-index.
            // * Layer #ODD_NUMBER contains a glass pane which blocks
            // user interaction such as mouse clicks.
            // There is always a glass pane in the layer which is just below
            // the layer containing a modal dialog or a group of popup dialogs.
            // * Layer #EVEN_NUMBER contains either:
            // - a single modal dialog (type=modal),
            // - one of more popup dialogs (type=popup),
            //   each having a different z-index.

            switch (type) {
            case "dialog":
            case "popup":
                break;
            default:
                type = "modal";
                break;
            }

            if (!Array.isArray(position)) {
                switch (position) {
                case "menu":
                case "submenu":
                case "comboboxmenu":
                case "startmenu":
                case "stopmenu":
                    if (reference === null) {
                        // Specified position does not make sense.
                        position = "center";
                    }
                    break;
                case "topleft":
                case "topright":
                case "bottomleft":
                case "bottomright":
                case "top":
                case "bottom":
                case "left":
                case "right":
                    break;
                default:
                    position = "center";
                    break;
                }
            }

            const html = document.documentElement;
            const body = document.body;
            let zIndex = 0;

            let [max1, topGlass, topGlassType, topGlassZIndex, max2] =
                Dialogs.scanExisting(body);
            switch (type) {
            case "dialog":
                // No glass.
                zIndex = max1 + 1;
                Dialogs.checkNextZIndex(zIndex);
                break;
            case "popup":
                if (topGlassType === "popup") {
                    // Glass already exists because other popups already exist in
                    // this layer.
                    zIndex = max2 + 1;
                    Dialogs.checkNextZIndex(zIndex);
                    break;
                }
                //FALLTHROUGH
            default: // modal.
                {
                    zIndex = (Math.floor(max2 / Dialogs.LAYER_CAPACITY) *
                              Dialogs.LAYER_CAPACITY) + Dialogs.LAYER_CAPACITY;
                    let glass = document.createElement("div");
                    glass.setAttribute("data-xui-glass", `${zIndex} ${type}`);

                    const [top, right, bottom, left] =
                      Util.getPxProperties(body, "margin-top", 0, "margin-right", 0,
                                           "margin-bottom", 0, "margin-left", 0);

                    // position:absolute, top:0, left:0 in xui.css.
                    glass.setAttribute("style", `z-index: ${zIndex}; \
    width: ${body.scrollWidth + right + left - 1}px; \
    height: ${body.scrollHeight + top + bottom - 1}px;`);

                    body.appendChild(glass);

                    for (let eventType of [ "click", "auxclick", "contextmenu" ]) {
                        glass.addEventListener(eventType, Dialogs.onClickGlass,
                                               /*capture*/ true);
                    }

                    zIndex += Dialogs.LAYER_CAPACITY;
                }
                break;
            }

            let dialog = document.createElement("div");
            dialog.setAttribute("data-xui-dialog", `${zIndex} ${type}`);
            dialog.setAttribute("tabindex", "-1");
            if (classes !== null && (classes = classes.trim()).length > 0) {
                dialog.setAttribute("class", classes);
            }
            dialog.setAttribute("style", `position: absolute; left: 0px; top: 0px; \
    z-index: ${zIndex}; visibility: hidden;`);

            dialog.addEventListener("keydown", Dialogs.onKeydownDialog);

            if (form !== null) {
                dialog.appendChild(form);
            }
            body.appendChild(dialog);

            if (type !== "dialog") {
                let focused = document.activeElement;
                if (focused !== null && !(focused instanceof HTMLBodyElement)) {
                    dialog.xuiWasFocused = focused;
                }
            }

            // Do not focus a non-modal dialog added below a modal or
            // popup dialog.
            const focusDialog = (zIndex > max2);

            // doOpen after 100ms is needed because otherwise we have
            // a FOUC and/or an incorrect dialog.getBoundingClientRect.
            // (Just Oms  does not work.)
            setTimeout(() => {
                Dialogs.doOpen(dialog, focusDialog, position, reference);
            }, 100 /*ms*/);

            return dialog;
        }

        static doOpen(dialog, focusDialog, position, reference) {
            const html = document.documentElement;
            let htmlClientWidth =  html.clientWidth;
            if (htmlClientWidth > 1) {
                --htmlClientWidth;
            }
            let htmlClientHeight = html.clientHeight;
            if (htmlClientHeight > 1) {
                --htmlClientHeight;
            }
            const htmlRect = new DOMRect(0, 0, htmlClientWidth, htmlClientHeight);

            let refRect = null;
            if (reference !== null && Util.pageContains(reference)) {
                refRect = reference.getBoundingClientRect();
                if (!Util.intersects(refRect, htmlRect)) {
                    refRect = null;
                }
            }
            if (refRect === null) {
                // Fallback to using viewport rect.
                refRect = htmlRect;
            }

            const rect = dialog.getBoundingClientRect();

            // left and top are relative to the body=dialog.offsetParent
            // (because dialog is added to the body and absolute position
            // is relative to the nearest positioned ancestor).
            //
            // However at first, we'll use client coordinates.
            // (client rectangle = element rectangle, contents+padding+border,
            //  expressed in window coordinates, that is,
            //  the top/left corner of the window viewport is point 0,0.)
            let left = 0;
            let top = 0;

            if (Array.isArray(position)) {
                // Generally comes from a MouseEvent.clientX/Y.
                left = position[0] + 1;
                top = position[1] + 1;
            } else {
                switch (position) {
                case "menu":
                    left = refRect.left;
                    top = refRect.bottom;
                    break;
                case "submenu":
                    left = refRect.right;
                    top = refRect.top;
                    break;
                case "comboboxmenu":
                    left = refRect.right - rect.width;
                    top = refRect.bottom;
                    break;
                case "startmenu":
                    left = refRect.left;
                    top = refRect.top - rect.height;
                    break;
                case "stopmenu":
                    // A "startmenu" which would found at the bottom/right corner of
                    // a window.
                    left = refRect.right - rect.width;
                    top = refRect.top - rect.height;
                    break;
                case "topleft":
                    left = refRect.left;
                    top = refRect.top;
                    break;
                case "topright":
                    left = refRect.right - rect.width;
                    top = refRect.top;
                    break;
                case "bottomleft":
                    left = refRect.left;
                    top = refRect.bottom - rect.height;
                    break;
                case "bottomright":
                    left = refRect.right - rect.width;
                    top = refRect.bottom - rect.height;
                    break;
                case "top":
                    left = refRect.left + ((refRect.width - rect.width)/2);
                    top = refRect.top;
                    break;
                case "bottom":
                    left = refRect.left + ((refRect.width - rect.width)/2);
                    top = refRect.bottom - rect.height;
                    break;
                case "left":
                    left = refRect.left;
                    top = refRect.top + ((refRect.height - rect.height)/2);
                    break;
                case "right":
                    left = refRect.right - rect.width;
                    top = refRect.top + ((refRect.height - rect.height)/2);
                    break;
                case "center":
                    left = refRect.left + ((refRect.width - rect.width)/2);
                    top = refRect.top + ((refRect.height - rect.height)/2);
                    break;
                }
            }

            // We favor the fact that the dialog is entirely visible over the fact
            // it is positioned as specified by client code.
            if (left < 0) {
                left = 0;
            } else if (left + rect.width > htmlClientWidth &&
                       htmlClientWidth - rect.width >= 0) {
                left = htmlClientWidth - rect.width;
            }
            if (top < 0) {
                top = 0;
            } else if (top + rect.height > htmlClientHeight &&
                       htmlClientHeight - rect.height >= 0) {
                top = htmlClientHeight - rect.height;
            }

            /* What follows is not practical to use because the body
               almost always has a margin (8px by default):
               const bodyRect = body.getBoundingClientRect();
               left = left - bodyRect.left + LEFT_BODY_MARGIN;
               top = top - bodyRect.top + TOP_BODY_MARGIN;
            */
            left += html.scrollLeft;
            top += html.scrollTop;

            dialog.style.visibility = "visible";
            dialog.style.left = `${left}px`;
            dialog.style.top = `${top}px`;

            // Wait until the dialog is visible to focus a field in it.
            if (focusDialog) {
                setTimeout(() => { Dialogs.focus(dialog); }, 100 /*ms*/);
            }
        }

        static checkNextZIndex(zIndex) {
            if ((zIndex % Dialogs.LAYER_CAPACITY) === 0) {
                throw new Error(`Dialogs.open: INTERNAL ERROR: cannot open dialog: \
    reached maximum layer capacity=${Dialogs.LAYER_CAPACITY}`);
            }
        }

        static scanExisting(body) {
            let max1 = 0; // zIndex of the topmost NON-MODAL dialog.
            let topGlass = null;
            let topGlassType = null;
            let topGlassZIndex = 0;
            let max2 = 0; // zIndex of the topmost dialog, WHATEVER ITS TYPE.

            let node = body.lastChild;
            while (node !== null) {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    let [kind, type, zIndex] = Dialogs.getData(node);
                    if (kind === null) {
                        // Done.
                        break;
                    }

                    if (kind === "glass") {
                        if (zIndex > topGlassZIndex) {
                            topGlass = node;
                            topGlassType = type;
                            topGlassZIndex = zIndex;
                        }
                    } else {
                        // dialog kind ---

                        if (zIndex > max2) {
                            max2 = zIndex;
                        }
                        if (zIndex < Dialogs.LAYER_CAPACITY && zIndex > max1) {
                            max1 = zIndex;
                        }
                    }
                }

                node = node.previousSibling;
            }

            return [max1, topGlass, topGlassType, topGlassZIndex, max2];
        }

        static getData(elem) {
            let kind = null;
            let type = null;
            let zIndex = 0;

            if (elem !== null && elem.nodeType === Node.ELEMENT_NODE) {
                let attr = elem.getAttribute("data-xui-dialog");
                if (attr) {
                    kind = "dialog";
                } else {
                    attr = elem.getAttribute("data-xui-glass");
                    if (attr) {
                        kind = "glass";
                    }
                }

                if (kind !== null) {
                    let split = attr.split(' ');
                    if (split.length !== 2) {
                        kind = null;
                    } else {
                        zIndex = parseInt(split[0]);
                        if (isNaN(zIndex) || zIndex <= 0) {
                            kind = null;
                        }

                        type = split[1];
                        switch (type) {
                        case "dialog":
                        case "popup":
                        case "modal":
                            break;
                        default:
                            kind = null;
                            break;
                        }
                    }
                }
            }

            let data = [null, null, 0];
            if (kind !== null) {
                data[0] = kind;
                data[1] = type;
                data[2] = zIndex;
            }

            return data;
        }

        static onClickGlass(event) {
            Dialogs.closeAllPopups(event.currentTarget);
            event.preventDefault();
            event.stopPropagation();
        }

        static closeAllPopups(glass=null) {
            if (glass === null) {
                let [max1, topGlass, topGlassType, topGlassZIndex, max2] =
                    Dialogs.scanExisting(document.body);
                if (topGlass === null || topGlassType !== "popup") {
                    // Nothing to do.
                    return;
                }
                glass = topGlass;
            }

            let [kind, type, zIndex] = Dialogs.getData(glass);
            assertOrError(kind === "glass", `${glass}, not a glass pane`);

            if (type === "popup") {
                const layerZIndex = zIndex + Dialogs.LAYER_CAPACITY;
                let popups = Dialogs.getPopups(layerZIndex);
                assertOrError(popups.length > 0);

                // Closing the popup having the smallest zIndex
                // 1) closes all popups in this popup layer;
                // 2) removes the corresponding glass.
                let closed = Dialogs.doClose(popups[popups.length-1].dialog);

                assertOrError(glass.parentNode === null || !closed);
            }
        }

        static onKeydownDialog(event) {
            let dialog = event.currentTarget;
            let [kind, type, zIndex] = Dialogs.getData(dialog);
            assertOrError(kind === "dialog", `${dialog}, not a dialog`);

            if (event.key === "Escape" &&
                !event.altKey && !event.ctrlKey &&
                !event.metaKey && !event.shiftKey) {
                Dialogs.doClose(dialog);

                event.preventDefault();
                event.stopPropagation();
            }
        }

        static getPopups(minZIndex) {
            let popups = [];

            const layerZIndex =
                  (Math.floor(minZIndex / Dialogs.LAYER_CAPACITY) *
                   Dialogs.LAYER_CAPACITY);
            const nextLayerZIndex = layerZIndex + Dialogs.LAYER_CAPACITY;

            let node = document.body.lastChild;
            while (node !== null) {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    let [kind, type, zIndex] = Dialogs.getData(node);
                    if (kind === null) {
                        // Done.
                        break;
                    }

                    if (kind === "dialog" && type === "popup" &&
                        zIndex >= minZIndex && zIndex < nextLayerZIndex) {
                        popups.push({ dialog: node, zIndex: zIndex });
                    }
                }

                node = node.previousSibling;
            }

            if (popups.length > 1) {
                // The popups having the largest zIndex come first.
                popups.sort((popup1, popup2) => {
                    return popup2.zIndex - popup1.zIndex;
                });
            }

            return popups;
        }

        /**
         * Close dialog containing specified element.
         */
        static close(elem, result=null) {
            let dialog = Dialogs.lookup(elem);
            if (dialog === null) {
                return false;
            }

            return Dialogs.doClose(dialog, result);
        }

        static lookup(elem) {
            let dialog = null;

            while (elem !== null) {
                if (elem.hasAttribute("data-xui-dialog")) {
                    dialog = elem;
                    break;
                }

                elem = elem.parentElement;
            }

            return dialog;
        }

        static doClose(dialog, result=null) {
            let [kind, type, zIndex] = Dialogs.getData(dialog);
            assertOrError(kind === "dialog", `${dialog}, not a dialog`);

            let glassZIndex = 0;
            switch (type) {
            case "popup":
                {
                    if (!Dialogs.closeSubsequentPopups(dialog)) {
                        return false;
                    }

                    const layerZIndex =
                          (Math.floor(zIndex / Dialogs.LAYER_CAPACITY) *
                           Dialogs.LAYER_CAPACITY);
                    if (zIndex === layerZIndex) {
                        // Closing last popup of the layer. Close corresponding
                        // glass too.
                        glassZIndex = layerZIndex - Dialogs.LAYER_CAPACITY;
                    }
                }
                break;
            case "modal":
                glassZIndex = zIndex - Dialogs.LAYER_CAPACITY;
                break;
            }

            if (!Dialogs.notifyListeners(dialog, result)) {
                // Close canceled by a listener. Give up.
                return false;
            }

            if (glassZIndex > 0) {
                let glass = document.body.querySelector(
                    `[data-xui-glass='${glassZIndex} ${type}']`);
                assertOrError(glass !== null,
                              `no ${type} glass at z-index=${glassZIndex}`);

                glass.parentNode.removeChild(glass);
            }

            Dialogs.discard(dialog);

            Dialogs.restoreFocus(dialog);

            return true;
        }

        static closeSubsequentPopups(dialog) {
            let [kind, type, zIndex] = Dialogs.getData(dialog);
            assertOrError(kind === "dialog", `${dialog}, not a dialog`);

            if (type === "popup") {
                const layerZIndex = (Math.floor(zIndex / Dialogs.LAYER_CAPACITY) *
                                     Dialogs.LAYER_CAPACITY);
                let popups = Dialogs.getPopups(layerZIndex);
                for (let popup of popups) {
                    if (popup.zIndex === zIndex) {
                        assertOrError(popup.dialog === dialog);
                        break;
                    }

                    if (!Dialogs.notifyListeners(popup.dialog, /*result*/ null)) {
                        // Close canceled by a listener. Give up.
                        return false;
                    }
                    Dialogs.discard(popup.dialog);
                }
            }

            return true;
        }

        static notifyListeners(dialog, result) {
            const event = new CustomEvent("dialogclosed",
                                          { bubbles: false, composed: false,
                                            cancelable: true,
                                            detail: { dialogResult: result } });
            dialog.dispatchEvent(event);

            return !event.defaultPrevented;
        }

        static discard(dialog) {
            dialog.parentNode.removeChild(dialog);

            // Detach form so it can be reused in a new dialog.
            const form = dialog.firstElementChild;
            if (form !== null) {
                dialog.removeChild(form);
            }
        }

        static restoreFocus(closedDialog) {
            if (closedDialog.xuiWasFocused) {
                closedDialog.xuiWasFocused.focus();
            } else {
                let [max1, topGlass, topGlassType, topGlassZIndex, max2] =
                    Dialogs.scanExisting(document.body);
                if (max2 >= 2*Dialogs.LAYER_CAPACITY) {
                    let dialog =
                        document.body.querySelector(`[data-xui-dialog^='${max2}']`);
                    assertOrError(dialog !== null,
                                  `no modal or popup dialog at z-index=${max2}`);

                    Dialogs.focus(dialog);
                }
            }
        }

        static focus(dialog) {
            let focusable = dialog.querySelector("[autofocus]");
            if (focusable === null) {
                focusable = dialog.querySelector(
                    `input:not([disabled]):not([tabindex='-1']),\
    select:not([disabled]):not([tabindex='-1']),\
    textarea:not([disabled]):not([tabindex='-1']),\
    button:not([disabled]):not([tabindex='-1']),
    a[href]:not([tabindex='-1']),\
    area[href]:not([tabindex='-1']),\
    [tabindex]:not([tabindex='-1']),\
    [contenteditable=true]:not([tabindex='-1'])`);

                if (focusable === null) {
                    focusable = dialog;
                }
            }

            if (focusable !== null) {
                focusable.focus();
            }
        }
    }
    Dialogs.LAYER_CAPACITY = 100;