/*!
 * Custom elements for Nanopore-Digital Web applications
 */
import { Host, Build, } from "@stencil/core";
import Modal from "../../utils/modal";
import { lockBodyScrolling, unlockBodyScrolling } from "../../utils/scroll";
import { transformTag, h } from "../../utils/renderer";
import { blurActiveElement } from "../../utils/active-element";
import { HasSlotController } from "../../utils/slot";
let toastStackTr;
let toastStackTl;
let toastStackBl;
let toastStackBr;
if (globalThis['document']) {
    toastStackTr = Object.assign(document.createElement('div'), {
        className: 'nano-toast-stack nano-toast-stack--tr',
    });
    toastStackTl = Object.assign(document.createElement('div'), {
        className: 'nano-toast-stack nano-toast-stack--tl',
    });
    toastStackBl = Object.assign(document.createElement('div'), {
        className: 'nano-toast-stack nano-toast-stack--bl',
    });
    toastStackBr = Object.assign(document.createElement('div'), {
        className: 'nano-toast-stack nano-toast-stack--br',
    });
}
/**
 * Alerts are used to display important messages either inline, as toast notifications or as modals requiring action.
 * @status stable
 * @version 2.0.0
 *
 * @slot - The alert's body content.
 * @slot heading - a themed piece of text, normally used to quick flag to the user the type of alert (e.g. 'Error', 'Warning', 'Success')
 * @slot title - A title for the alert.
 * @slot icon - An icon to show in the alert.
 * @slot footer - Place items at the bottom of the alert. Best used with `button` elements - esp when used as with the `alert()` method.
 *
 * @part panel - the root, wrapper element
 * @part main - wrapper around the main content
 * @part icon - wrapper around the icon slot
 * @part content - wrapper around all text content
 * @part close - wrapper around the close button
 * @part footer - wrapper around the footer slot
 * @part overlay - present when the `alert()` method is called. The backdrop behind the modal.
 */
export class Alert {
    panel;
    overlay;
    modal;
    originalTrigger;
    goingToHide = false;
    goingToShow = false;
    autoHideTimeout;
    remainingTimeInterval;
    countdownAnimation;
    countdownElement;
    slotCtrl = new HasSlotController(this, '[default]', 'heading', 'title', 'footer');
    isModal = false;
    isToast = false;
    isShowing = false;
    label;
    noDismiss = false;
    host;
    /** Indicates whether or not the alert is open. You can use this in lieu of the show/hide methods. */
    open = false;
    handleOpenChange() {
        if (this.open)
            this.show();
        else
            this.hide();
    }
    /** Set to true to make the alert closable. */
    closable = false;
    /**
     * The color to use from the application's color palette.
     * @deprecated Use `theme` instead.
     */
    color;
    /** The color to use from the application's color palette.  */
    theme;
    /**
     * The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with the
     * alert before it closes (e.g. moves the mouse over it), the timer will restart.
     */
    duration = Infinity;
    remainingTime = this.duration;
    handleDurationChange() {
        this.restartAutoHide();
    }
    /**
     * Relocate the dialog to the root of the DOM.
     * Most useful in conjunction with `alert()` when parental elements
     * have css stacking constraints which don't allow for `position: fixed`
     */
    hoist = false;
    handleHoistChange() {
        if (!this.hoist || document.body.children[0] === this.host)
            return;
        document.body.prepend(this.host);
    }
    /**
     * Enables a countdown that indicates the remaining time the alert will be displayed.
     * Typically used to indicate the remaining time before a whole app refresh.
     */
    countdown = true;
    /** Emitted when the alert opens. Calling `event.preventDefault()` will prevent it from being opened. */
    nanoShow;
    /** Emitted after the alert opens and all transitions are complete. */
    nanoAfterShow;
    /** Emitted when the alert closes. Calling `event.preventDefault()` will prevent it from being closed. */
    nanoHide;
    /** Emitted after the alert closes and all transitions are complete. */
    nanoAfterHide;
    /**
     * Shows the alert.
     * @returns a promise that resolves after the alert is shown
     */
    async show() {
        if (this.goingToShow)
            return;
        const nanoShow = this.nanoShow.emit();
        if (nanoShow.defaultPrevented) {
            this.open = false;
            return false;
        }
        this.host.hidden = false;
        this.goingToShow = true;
        this.open = true;
        if (Build.isBrowser) {
            requestAnimationFrame(() => {
                this.isShowing = true;
                this.goingToShow = false;
            });
        }
        else {
            this.isShowing = true;
            this.goingToShow = false;
        }
        if (this.duration < Infinity) {
            this.restartAutoHide();
        }
    }
    /**
     * Hides the alert
     * @returns a promise that resolves after the alert is hidden
     */
    async hide() {
        if (this.goingToHide) {
            return;
        }
        const nanoHide = this.nanoHide.emit();
        if (nanoHide.defaultPrevented) {
            this.open = true;
            return false;
        }
        this.goingToHide = true;
        this.open = false;
        blurActiveElement(this.host);
        requestAnimationFrame(() => (this.goingToHide = false));
        clearTimeout(this.autoHideTimeout);
        clearInterval(this.remainingTimeInterval);
    }
    /**
     * Displays the alert as a toast notification. This will move the alert out of its position in the DOM and, when
     * dismissed, it will be removed from the DOM completely. By storing a reference to the alert, you can reuse it by
     * calling this method again. The returned promise will resolve after the alert is hidden.
     *
     * @param position options are tr (top-right - default), tl (top-left), br (bottom-right), bl (bottom-left)
     * @returns a promise which will resolve after the alert has hidden
     */
    async toast(position = 'tr') {
        this.isToast = true;
        return new Promise((resolve) => {
            this.handleCountdownChange();
            let toastStack;
            switch (position) {
                case 'tl':
                    toastStack = toastStackTl;
                    break;
                case 'bl':
                    toastStack = toastStackBl;
                    break;
                case 'br':
                    toastStack = toastStackBr;
                    break;
                default:
                    toastStack = toastStackTr;
                    break;
            }
            if (!toastStack.isConnected) {
                document.body.appendChild(toastStack);
            }
            toastStack.appendChild(this.host);
            this.connectedCallback();
            this.show();
            const onClose = () => {
                this.host.remove();
                this.isToast = false;
                resolve();
                // Remove the toast stack from the DOM when there are no more alerts
                if (!toastStack.querySelector(transformTag('nano-alert'))) {
                    toastStack.remove();
                }
            };
            this.host.addEventListener('nanoAfterHide', onClose, { once: true });
            this.host.addEventListener('nano-after-hide', onClose, { once: true });
        });
    }
    /**
     * Displays the alert as a dialog / modal - more akin to a traditional js alert().
     * @param label a label for assistive technology
     */
    async alert(label) {
        this.isModal = true;
        this.label = label;
        return new Promise((resolve) => {
            let foundHoisted = false;
            for (const child of document.body.children) {
                if (child.tagName === 'NANO-ALERT' && child === this.host) {
                    foundHoisted = true;
                }
            }
            if (!foundHoisted) {
                document.body.append(this.host);
            }
            this.modal.activate();
            lockBodyScrolling(this.host);
            this.originalTrigger = document.activeElement;
            requestAnimationFrame(() => {
                this.show();
            });
            const onOpen = () => requestAnimationFrame(() => this.panel.focus({ preventScroll: true }));
            const onClose = () => {
                this.modal.deactivate();
                this.host.remove();
                this.label = undefined;
                this.isModal = false;
                resolve();
                // Restore focus to the original trigger
                if (this.originalTrigger &&
                    typeof this.originalTrigger.focus === 'function') {
                    setTimeout(() => this.originalTrigger.focus());
                }
            };
            this.host.addEventListener('nanoAfterShow', onOpen, { once: true });
            this.host.addEventListener('nano-after-show', onOpen, { once: true });
            this.host.addEventListener('nanoAfterHide', onClose, {
                once: true,
            });
            this.host.addEventListener('nano-after-hide', onClose, {
                once: true,
            });
        });
    }
    handleCountdownChange() {
        if (this.open && this.duration < Infinity && this.countdown) {
            const start = '100%';
            const end = '0';
            requestAnimationFrame(() => {
                if (!this.countdownElement)
                    return;
                this.countdownAnimation = this.countdownElement.animate([{ width: start }, { width: end }], {
                    duration: this.duration,
                    easing: 'linear',
                });
            });
        }
    }
    restartAutoHide() {
        this.handleCountdownChange();
        clearTimeout(this.autoHideTimeout);
        clearInterval(this.remainingTimeInterval);
        if (this.open && this.duration < Infinity) {
            this.autoHideTimeout = window.setTimeout(() => this.hide(), this.duration);
            this.remainingTime = this.duration;
            this.remainingTimeInterval = window.setInterval(() => {
                this.remainingTime -= 100;
            }, 100);
        }
    }
    handleMouseEnter = () => {
        this.countdownAnimation?.pause();
        clearTimeout(this.autoHideTimeout);
        clearInterval(this.remainingTimeInterval);
    };
    handleMouseLeave = () => {
        if (this.duration < Infinity) {
            this.autoHideTimeout = window.setTimeout(() => this.hide(), this.remainingTime);
            this.remainingTimeInterval = window.setInterval(() => {
                this.remainingTime -= 100;
            }, 100);
            this.countdownAnimation?.play();
        }
    };
    handleCloseClick = () => {
        this.hide();
    };
    handleTransitionEnd = (event) => {
        if (event.propertyName === 'opacity' &&
            (event.target === this.panel || event.target === this.overlay)) {
            this.host.hidden = !this.open;
            this.isShowing = this.open;
            if (this.open)
                this.nanoAfterShow.emit();
            else
                this.nanoAfterHide.emit();
        }
    };
    handleButtonClick = (e) => {
        if (e.defaultPrevented)
            return;
        if (e.target.tagName && e.target.tagName.toLowerCase() === 'button')
            this.hide();
    };
    connectedCallback() {
        this.modal = new Modal(this.host);
        this.handleHoistChange();
        this.host.addEventListener('click', this.handleButtonClick);
        this.isShowing = typeof this.host.getAttribute('showing') === 'string';
    }
    disconnectedCallback() {
        unlockBodyScrolling(this.host);
        this.host.removeEventListener('click', this.handleButtonClick);
    }
    componentDidLoad() {
        // Show on init if open
        if (this.open && !this.isShowing) {
            this.show();
        }
    }
    Panel = () => {
        return (h("div", { ref: (div) => (this.panel = div), part: "panel", class: {
                alert: true,
                'alert--open': this.open,
                'alert--toasty': this.isToast,
                'alert--modal': !!this.isModal,
                'alert--showing': this.isShowing,
                'alert--nodismiss': this.noDismiss,
            }, tabIndex: this.isModal ? 0 : undefined, role: this.isModal ? 'alertdialog' : 'alert', "aria-atomic": "true", "aria-hidden": this.isShowing ? 'false' : 'true', "aria-modal": this.isModal ? 'true' : undefined, "aria-label": this.label ? this.label : undefined, onMouseEnter: this.handleMouseEnter, onMouseLeave: this.handleMouseLeave, onTransitionEnd: this.handleTransitionEnd }, h("div", { class: "alert__main", part: "main" }, h("div", { part: "icon", class: "alert__icon" }, h("slot", { name: "icon" })), h("div", { part: "content", class: "alert__content", "aria-live": this.isModal ? 'assertive' : 'polite' }, this.slotCtrl.has('heading') && (h("div", { class: "alert__heading" }, h("slot", { name: "heading" }))), this.slotCtrl.has('title') && (h("div", { class: "alert__title" }, h("slot", { name: "title" }))), this.slotCtrl.has('[default]') && (h("div", { class: "alert__body" }, h("slot", null))), this.slotCtrl.has('footer') && (h("div", { part: "footer", class: "alert__footer" }, h("slot", { name: "footer" })))), this.closable && (h("div", { class: "alert__close", part: "close" }, h("nano-icon-button", { iconName: "light/xmark", label: "close menu", onClick: this.handleCloseClick })))), this.duration < Infinity && [
            h("div", { role: "timer", class: "alert__timer" }, this.remainingTime),
            this.countdown ? (h("div", { class: "alert__countdown" }, h("div", { ref: (e) => (this.countdownElement = e), class: "alert__countdown-elapsed" }))) : (''),
        ]));
    };
    render() {
        return (h(Host, { key: '026fe28d4b8aed79d75734f67de6192813be44a5', class: { 'nano-alert': true }, showing: this.isShowing }, this.isModal && [
            h("div", { key: 'ac596b1e2b216ee591e2f9b85adf86dbbe27fce1', part: "overlay", class: {
                    alert__overlay: true,
                    'alert__overlay--open': this.open,
                }, ref: (div) => (this.overlay = div) }),
            h("div", { key: '825f8cf0d80d9bfa7bff7710ef349c234c155d53', class: "alert__modal-wrap", onClick: (e) => {
                    if (!e.target.classList.contains('alert__modal-wrap'))
                        return;
                    this.noDismiss = true;
                    setTimeout(() => (this.noDismiss = false), 250);
                } }, h(this.Panel, { key: '7343702088055d21dc6d5252aa108c19c315a20a' })),
        ], !this.isModal && h(this.Panel, { key: 'c3cd08198bb5a87fcc38bea5130d5615608cb1ff' })));
    }
    static get is() { return "nano-alert"; }
    static get encapsulation() { return "shadow"; }
    static get originalStyleUrls() {
        return {
            "$": ["alert.scss"]
        };
    }
    static get styleUrls() {
        return {
            "$": ["alert.css"]
        };
    }
    static get properties() {
        return {
            "open": {
                "type": "boolean",
                "attribute": "open",
                "mutable": true,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Indicates whether or not the alert is open. You can use this in lieu of the show/hide methods."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "defaultValue": "false"
            },
            "closable": {
                "type": "boolean",
                "attribute": "closable",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Set to true to make the alert closable."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "defaultValue": "false"
            },
            "color": {
                "type": "string",
                "attribute": "color",
                "mutable": false,
                "complexType": {
                    "original": "Color",
                    "resolved": "\"danger\" | \"neutral\" | \"primary\" | \"success\" | \"warning\"",
                    "references": {
                        "Color": {
                            "location": "import",
                            "path": "../../types/shared",
                            "id": "src/types/shared.ts::Color"
                        }
                    }
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [{
                            "name": "deprecated",
                            "text": "Use `theme` instead."
                        }],
                    "text": "The color to use from the application's color palette."
                },
                "getter": false,
                "setter": false,
                "reflect": true
            },
            "theme": {
                "type": "string",
                "attribute": "theme",
                "mutable": false,
                "complexType": {
                    "original": "Color",
                    "resolved": "\"danger\" | \"neutral\" | \"primary\" | \"success\" | \"warning\"",
                    "references": {
                        "Color": {
                            "location": "import",
                            "path": "../../types/shared",
                            "id": "src/types/shared.ts::Color"
                        }
                    }
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "The color to use from the application's color palette."
                },
                "getter": false,
                "setter": false,
                "reflect": true
            },
            "duration": {
                "type": "number",
                "attribute": "duration",
                "mutable": false,
                "complexType": {
                    "original": "number",
                    "resolved": "number",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with the\nalert before it closes (e.g. moves the mouse over it), the timer will restart."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "Infinity"
            },
            "hoist": {
                "type": "boolean",
                "attribute": "hoist",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Relocate the dialog to the root of the DOM.\nMost useful in conjunction with `alert()` when parental elements\nhave css stacking constraints which don't allow for `position: fixed`"
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            },
            "countdown": {
                "type": "boolean",
                "attribute": "countdown",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Enables a countdown that indicates the remaining time the alert will be displayed.\nTypically used to indicate the remaining time before a whole app refresh."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "defaultValue": "true"
            }
        };
    }
    static get states() {
        return {
            "isModal": {},
            "isToast": {},
            "isShowing": {},
            "label": {},
            "noDismiss": {},
            "remainingTime": {}
        };
    }
    static get events() {
        return [{
                "method": "nanoShow",
                "name": "nanoShow",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the alert opens. Calling `event.preventDefault()` will prevent it from being opened."
                },
                "complexType": {
                    "original": "any",
                    "resolved": "any",
                    "references": {}
                }
            }, {
                "method": "nanoAfterShow",
                "name": "nanoAfterShow",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted after the alert opens and all transitions are complete."
                },
                "complexType": {
                    "original": "any",
                    "resolved": "any",
                    "references": {}
                }
            }, {
                "method": "nanoHide",
                "name": "nanoHide",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the alert closes. Calling `event.preventDefault()` will prevent it from being closed."
                },
                "complexType": {
                    "original": "any",
                    "resolved": "any",
                    "references": {}
                }
            }, {
                "method": "nanoAfterHide",
                "name": "nanoAfterHide",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted after the alert closes and all transitions are complete."
                },
                "complexType": {
                    "original": "any",
                    "resolved": "any",
                    "references": {}
                }
            }];
    }
    static get methods() {
        return {
            "show": {
                "complexType": {
                    "signature": "() => Promise<boolean>",
                    "parameters": [],
                    "references": {
                        "Promise": {
                            "location": "global",
                            "id": "global::Promise"
                        }
                    },
                    "return": "Promise<boolean>"
                },
                "docs": {
                    "text": "Shows the alert.",
                    "tags": [{
                            "name": "returns",
                            "text": "a promise that resolves after the alert is shown"
                        }]
                }
            },
            "hide": {
                "complexType": {
                    "signature": "() => Promise<boolean>",
                    "parameters": [],
                    "references": {
                        "Promise": {
                            "location": "global",
                            "id": "global::Promise"
                        }
                    },
                    "return": "Promise<boolean>"
                },
                "docs": {
                    "text": "Hides the alert",
                    "tags": [{
                            "name": "returns",
                            "text": "a promise that resolves after the alert is hidden"
                        }]
                }
            },
            "toast": {
                "complexType": {
                    "signature": "(position?: \"tr\" | \"tl\" | \"bl\" | \"br\") => Promise<void>",
                    "parameters": [{
                            "name": "position",
                            "type": "\"br\" | \"tr\" | \"tl\" | \"bl\"",
                            "docs": "options are tr (top-right - default), tl (top-left), br (bottom-right), bl (bottom-left)"
                        }],
                    "references": {
                        "Promise": {
                            "location": "global",
                            "id": "global::Promise"
                        },
                        "HTMLElement": {
                            "location": "global",
                            "id": "global::HTMLElement"
                        }
                    },
                    "return": "Promise<void>"
                },
                "docs": {
                    "text": "Displays the alert as a toast notification. This will move the alert out of its position in the DOM and, when\ndismissed, it will be removed from the DOM completely. By storing a reference to the alert, you can reuse it by\ncalling this method again. The returned promise will resolve after the alert is hidden.",
                    "tags": [{
                            "name": "param",
                            "text": "position options are tr (top-right - default), tl (top-left), br (bottom-right), bl (bottom-left)"
                        }, {
                            "name": "returns",
                            "text": "a promise which will resolve after the alert has hidden"
                        }]
                }
            },
            "alert": {
                "complexType": {
                    "signature": "(label: string) => Promise<void>",
                    "parameters": [{
                            "name": "label",
                            "type": "string",
                            "docs": "a label for assistive technology"
                        }],
                    "references": {
                        "Promise": {
                            "location": "global",
                            "id": "global::Promise"
                        },
                        "HTMLElement": {
                            "location": "global",
                            "id": "global::HTMLElement"
                        }
                    },
                    "return": "Promise<void>"
                },
                "docs": {
                    "text": "Displays the alert as a dialog / modal - more akin to a traditional js alert().",
                    "tags": [{
                            "name": "param",
                            "text": "label a label for assistive technology"
                        }]
                }
            }
        };
    }
    static get elementRef() { return "host"; }
    static get watchers() {
        return [{
                "propName": "open",
                "methodName": "handleOpenChange"
            }, {
                "propName": "duration",
                "methodName": "handleDurationChange"
            }, {
                "propName": "hoist",
                "methodName": "handleHoistChange"
            }];
    }
}
