import { h } from "@stencil/core";
import { createPopper, } from "@popperjs/core";
const IS_VISIBLE_CLASS = 'is-visible';
const IS_HIDING_CLASS = 'is-hiding';
const hideAnimationDuration = 300;
/**
 * The portal component provides a way to render children into a DOM node that
 * exist outside the DOM hierarchy of the parent component.
 *
 * When the limel-portal component is used, it creates a new DOM node (a div element)
 * and appends it to a parent element (by default, the body of the document).
 * The child elements of the limel-portal are then moved from
 * their original location in the DOM to this new div element.
 *
 * This technique is often used to overcome CSS stacking context issues,
 * or to render UI elements like modals, dropdowns, tooltips, etc.,
 * that need to visually "break out" of their container.
 *
 * Using this component, we ensure that the content is always rendered in the
 * correct position, and never covers its own trigger, or another component
 * that is opened in the stacking layer. This way, we don't need to worry about
 * z-indexes, or other stacking context issues.
 *
 * :::important
 * There are some caveats when using this component
 *
 * 1. Events might not bubble up as expected since the content is moved out to
 * another DOM node.
 * 2. Any styling that is applied to content from the parent will be lost, if the
 * content is just another web-component it will work without any issues.
 * Alternatively, use the `style=""` html attribute.
 * 3. Any component that is placed inside the container must have a style of
 * `max-height: inherit`. This ensures that its placement is calculated
 * correctly in relation to the trigger, and that it never covers its own
 * trigger.
 * 4. When the node is moved in the DOM, `disconnectedCallback` and
 * `connectedCallback` will be invoked, so if `disconnectedCallback` is used
 * to do any tear-down, the appropriate setup will have to be done again on
 * `connectedCallback`.
 * :::
 *
 * @slot - Content to put inside the portal
 * @private
 * @exampleComponent limel-example-portal-basic
 */
export class Portal {
    constructor() {
        /**
         * Decides which direction the portal content should open.
         */
        this.openDirection = 'bottom';
        /**
         * Position of the content.
         */
        this.position = 'absolute';
        /**
         * Dynamic styling that can be applied to the container holding the content.
         */
        this.containerStyle = {};
        /**
         * Used to make a dropdown have the same width as the trigger, for example
         * in `limel-picker`.
         */
        this.inheritParentWidth = false;
        /**
         * True if the content within the portal should be visible.
         *
         * If the content is from within a dialog for instance, this can be set to
         * true from false when the dialog opens to position the content properly.
         */
        this.visible = false;
        /**
         * The element that the content should be positioned relative to.
         * Defaults to the limel-portal element.
         */
        this.anchor = null;
        this.loaded = false;
        this.parents = new WeakMap();
    }
    disconnectedCallback() {
        this.removeContainer();
        this.destroyPopper();
        if (this.observer && this.container) {
            this.observer.unobserve(this.container);
        }
        this.container = null;
    }
    connectedCallback() {
        if (!this.loaded) {
            return;
        }
        if (this.visible) {
            this.init();
        }
    }
    componentDidLoad() {
        this.loaded = true;
        this.connectedCallback();
    }
    init() {
        if (!this.host.isConnected) {
            return;
        }
        this.createContainer();
        this.hideContainer();
        this.attachContainer();
        this.styleContainer();
        if (this.visible) {
            this.createPopper();
            this.showContainer();
        }
        if ('ResizeObserver' in window) {
            this.observer = new ResizeObserver(() => {
                if (this.popperInstance) {
                    this.styleContainer();
                    this.popperInstance.update();
                }
            });
            this.observer.observe(this.container);
        }
    }
    render() {
        return h("slot", { key: 'c8e7ba60cdab45dd239eae6c9123f673a48e8e22' });
    }
    onVisible() {
        if (!this.container && this.visible) {
            this.init();
            return;
        }
        if (!this.visible) {
            this.animateHideAndCleanup();
            return;
        }
        this.styleContainer();
        this.createPopper();
        requestAnimationFrame(() => {
            this.showContainer();
        });
    }
    createContainer() {
        const slot = this.host.shadowRoot.querySelector('slot');
        const content = (slot.assignedElements && slot.assignedElements()) || [];
        this.container = document.createElement('div');
        this.container.setAttribute('id', this.containerId);
        this.container.setAttribute('class', 'limel-portal--container');
        Object.assign(this.container, {
            portalSource: this.host,
        });
        // eslint-disable-next-line unicorn/no-array-for-each
        content.forEach((element) => {
            this.parents.set(element, element.parentElement);
            this.container.append(element);
        });
    }
    attachContainer() {
        this.getParent().append(this.container);
    }
    removeContainer() {
        if (!this.container) {
            return;
        }
        // eslint-disable-next-line unicorn/no-array-for-each
        [...this.container.children].forEach((element) => {
            const parent = this.parents.get(element);
            if (!parent) {
                return;
            }
            parent.append(element);
        });
        this.container.remove();
    }
    hideContainer() {
        if (!this.container) {
            return;
        }
        this.container.classList.remove(IS_VISIBLE_CLASS);
    }
    showContainer() {
        this.container.classList.add(IS_VISIBLE_CLASS);
    }
    animateHideAndCleanup() {
        if (!this.container) {
            return;
        }
        this.container.classList.add(IS_HIDING_CLASS);
        this.styleContainer();
        setTimeout(() => {
            if (!this.container) {
                return;
            }
            this.container.classList.remove(IS_HIDING_CLASS);
            if (!this.visible) {
                this.container.classList.remove(IS_VISIBLE_CLASS);
                this.destroyPopper();
            }
        }, hideAnimationDuration);
    }
    styleContainer() {
        this.setContainerWidth();
        this.setContainerHeight();
        this.setContainerStyles();
    }
    setContainerWidth() {
        const hostWidth = this.host.getBoundingClientRect().width;
        if (this.inheritParentWidth) {
            const containerWidth = this.getContentWidth(this.container);
            let width = containerWidth;
            if (hostWidth > 0) {
                width = hostWidth;
            }
            this.container.style.width = `${width}px`;
        }
    }
    getContentWidth(element) {
        if (!element) {
            return null;
        }
        const width = element.getBoundingClientRect().width;
        if (width !== 0) {
            return width;
        }
        const elementContent = element.querySelector('*');
        return this.getContentWidth(elementContent);
    }
    setContainerStyles() {
        for (const property of Object.keys(this.containerStyle)) {
            this.container.style[property] = this.containerStyle[property];
        }
    }
    createPopper() {
        const config = this.createPopperConfig();
        this.popperInstance = createPopper(this.anchor || this.host, this.container, config);
    }
    destroyPopper() {
        var _a;
        (_a = this.popperInstance) === null || _a === void 0 ? void 0 : _a.destroy();
        this.popperInstance = null;
    }
    createPopperConfig() {
        const placement = this.getPlacement(this.openDirection);
        const flipPlacement = this.getFlipPlacement(this.openDirection);
        return {
            strategy: this.position,
            placement: placement,
            modifiers: [
                {
                    name: 'flip',
                    options: {
                        fallbackPlacements: [flipPlacement],
                    },
                },
            ],
        };
    }
    getPlacement(direction) {
        const placements = {
            'left-start': 'left-start',
            left: 'left',
            'left-end': 'left-end',
            'right-start': 'right-start',
            right: 'right',
            'right-end': 'right-end',
            'top-start': 'top-start',
            top: 'top',
            'top-end': 'top-end',
            'bottom-start': 'bottom-start',
            bottom: 'bottom',
            'bottom-end': 'bottom-end',
        };
        return placements[direction];
    }
    getFlipPlacement(direction) {
        const flipPlacements = {
            'left-start': 'right-start',
            left: 'right',
            'left-end': 'right-end',
            'right-start': 'left-start',
            right: 'left',
            'right-end': 'left-end',
            'top-start': 'bottom-start',
            top: 'bottom',
            'top-end': 'bottom-end',
            'bottom-start': 'top-start',
            bottom: 'top',
            'bottom-end': 'top-end',
        };
        return flipPlacements[direction];
    }
    setContainerHeight() {
        const viewHeight = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
        const { top, bottom } = this.host.getBoundingClientRect();
        const spaceAboveTopOfSurface = Math.max(top, 0);
        const spaceBelowTopOfSurface = Math.max(viewHeight - bottom, 0);
        const extraCosmeticSpace = 16;
        const maxHeight = Math.max(spaceAboveTopOfSurface, spaceBelowTopOfSurface) -
            extraCosmeticSpace;
        this.container.style.maxHeight = `${maxHeight}px`;
    }
    // Returns the parent element where the content of the portal will be moved to.
    // It needs to have styling of the portal container.
    getParent() {
        let element = this.anchor || this.host;
        while (element) {
            const parent = element.closest('.limel-portal--parent');
            if (parent) {
                return parent;
            }
            element = element.getRootNode().host;
        }
        return document.body;
    }
    static get is() { return "limel-portal"; }
    static get encapsulation() { return "shadow"; }
    static get originalStyleUrls() {
        return {
            "$": ["portal.scss"]
        };
    }
    static get styleUrls() {
        return {
            "$": ["portal.css"]
        };
    }
    static get properties() {
        return {
            "openDirection": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "OpenDirection",
                    "resolved": "\"bottom\" | \"bottom-end\" | \"bottom-start\" | \"left\" | \"left-end\" | \"left-start\" | \"right\" | \"right-end\" | \"right-start\" | \"top\" | \"top-end\" | \"top-start\"",
                    "references": {
                        "OpenDirection": {
                            "location": "import",
                            "path": "../menu/menu.types",
                            "id": "src/components/menu/menu.types.ts::OpenDirection",
                            "referenceLocation": "OpenDirection"
                        }
                    }
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Decides which direction the portal content should open."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "open-direction",
                "defaultValue": "'bottom'"
            },
            "position": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "'fixed' | 'absolute'",
                    "resolved": "\"absolute\" | \"fixed\"",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Position of the content."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "position",
                "defaultValue": "'absolute'"
            },
            "containerId": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "A unique ID."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "container-id"
            },
            "containerStyle": {
                "type": "unknown",
                "mutable": false,
                "complexType": {
                    "original": "object",
                    "resolved": "object",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Dynamic styling that can be applied to the container holding the content."
                },
                "getter": false,
                "setter": false,
                "defaultValue": "{}"
            },
            "inheritParentWidth": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Used to make a dropdown have the same width as the trigger, for example\nin `limel-picker`."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "inherit-parent-width",
                "defaultValue": "false"
            },
            "visible": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "True if the content within the portal should be visible.\n\nIf the content is from within a dialog for instance, this can be set to\ntrue from false when the dialog opens to position the content properly."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "visible",
                "defaultValue": "false"
            },
            "anchor": {
                "type": "unknown",
                "mutable": false,
                "complexType": {
                    "original": "HTMLElement",
                    "resolved": "HTMLElement",
                    "references": {
                        "HTMLElement": {
                            "location": "global",
                            "id": "global::HTMLElement"
                        }
                    }
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "The element that the content should be positioned relative to.\nDefaults to the limel-portal element."
                },
                "getter": false,
                "setter": false,
                "defaultValue": "null"
            }
        };
    }
    static get elementRef() { return "host"; }
    static get watchers() {
        return [{
                "propName": "visible",
                "methodName": "onVisible"
            }];
    }
}
