<!--

    Simple popup attached to a reference element. Most common scenarios would be tooltips or menus.
    Uses Floating UI for placement and PxFloat for the popup itself. Has built in support for click
    and hover triggers.

    Usage as a menu:

    <px-popup tag="button" trigger="click">

        Button Label

        <template #popup>
            <div class="popup-menu">
                <button>Menu Option 1</button>
                <button>Menu Option 2</button>
            </div>
        </template>
    </px-popup>

    Usage as a tooltip:

    <px-popup trigger="hover" tag="a" href="https://thinkpixellab.com">
        Pixel Lab
        <template #popup>
            <div class="tooltip">
                Best website ever!
            </div>
        </template>
    </px-popup>

-->
<template>
    <component
        :is="tag"
        :class="bem()"
        :aria-label="label"
        :aria-expanded="syncPopupVisible"
        @click="onClick"
        @contextmenu.stop="onContextMenu"
        @mouseenter="onMouseEnter"
        @mouseleave="onMouseLeave"
    >
        <slot name="default" />
        <px-float
            v-if="calcUseModal"
            v-model:visible="syncPopupVisible"
            v-bind="modalAttrs"
            variant="none"
        >
            <div
                ref="popup"
                :class="[bem('popup'), popupClass]"
                :style="{
                    minWidth: cssPx(popupMinWidth),
                    maxWidth: setPopupMaxWidth ? cssPx(popupMinWidth) : null,
                }"
            >
                <slot name="popup" />
            </div>
        </px-float>
        <div v-else>
            <div
                ref="popup"
                :class="[bem('popup', { nonModal: true }), popupClass]"
                :style="{
                    minWidth: cssPx(popupMinWidth),
                    maxWidth: setPopupMaxWidth ? cssPx(popupMinWidth) : null,
                }"
                v-show="syncPopupVisible"
            >
                <slot name="popup" />
            </div>
        </div>
    </component>
</template>

<script>
import PxFloat from './PxFloat.vue';
import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom';
export default {
    name: 'px-popup',
    components: { PxFloat },
    props: {
        // the tag to use for the primary / root / reference element
        tag: { type: String, default: 'button' },

        // automatically toggle popup visibility on click or hover, possible values: click, hover, contextmenu
        trigger: { type: String, default: 'click' },

        // whether the popup is visible (syncable)
        popupVisible: { type: Boolean, default: false },

        // the positioning strategy, possible values: fixed, absolute
        strategy: { type: String, default: 'fixed' },

        // the placement of the popup relative to the reference element, possible values are: top,
        // top-start, top-end, bottom, bottom-start, bottom-end, right, right-start, right-end, left,
        // left-start, left-end
        placement: { type: String, default: 'bottom' },

        // the number of pixels to shift the popup in the direction perpendicular to it's alignment,
        // e.g. if placement is bottom-start then a value of -10 would shift the popup 10px to the
        // left (in non-rtl layouts)
        skidding: { type: Number, default: 0 },

        // additional spacing between the main element and popup in px
        distance: { type: Number, default: 0 },

        // whether to use PxModal for the popup, leave as null for auto based on trigger (click =>
        // true, hover => false)
        useModal: { type: Boolean, default: null },

        // properties that get passed to the modal element (see PxFloat for details)
        modalAttrs: { default: null },

        // class that gets set on the popup element
        popupClass: { default: null },

        // for the popup to be at least as wide as the reference element
        setPopupMinWidth: { type: Boolean, default: true },

        // for the popup to never be wider than the reference element
        setPopupMaxWidth: { type: Boolean, default: false },

        label: { type: String, default: null },
    },

    data() {
        return { syncPopupVisible: this.popupVisible, popupMinWidth: null };
    },

    watch: {
        popupVisible(nv) {
            // update syncPopupVisible when visible changes
            this.syncPopupVisible = nv;
        },

        async syncPopupVisible(nv) {
            // emit update when syncPopupVisible changes (enables .sync on parent)
            this.$emit('update:popupVisible', nv);

            if (nv) {
                if (this.setPopupMinWidth) {
                    this.popupMinWidth = this.$el.getBoundingClientRect().width;
                } else {
                    this.popupMinWidth = null;
                }

                this.stopPositioning();

                if (this.calcUseModal) {
                    // if we're using a modal we need to delay positioning so that the popup element
                    // is in the right place in the DOM (PxFloat teleports it)
                    await this.$nextTick();
                    await this.$nextTick();
                }

                this.startPositioning();
            } else {
                this.stopPositioning();
            }
        },
    },
    beforeUnmount() {
        this.stopPositioning();
    },
    computed: {
        calcUseModal() {
            if (this.useModal === null) {
                return this.trigger != 'hover';
            }
            return this.useModal;
        },
    },
    methods: {
        openPopup() {
            this.syncPopupVisible = true;
        },
        closePopup() {
            this.syncPopupVisible = false;
        },
        togglePopup() {
            this.syncPopupVisible = !this.syncPopupVisible;
        },
        onClick() {
            if (this.trigger == 'click') {
                this.syncPopupVisible = !this.syncPopupVisible;
            }
        },
        onContextMenu(e) {
            if (this.trigger == 'contextmenu') {
                e.stopPropagation();
                e.preventDefault();
                this.syncPopupVisible = !this.syncPopupVisible;
            }
        },
        onMouseEnter() {
            if (this.trigger == 'hover') {
                this.syncPopupVisible = true;
            }
        },
        onMouseLeave() {
            if (this.trigger == 'hover') {
                this.syncPopupVisible = false;
            }
        },
        startPositioning() {
            let reference = this.$el;
            let popup = this.$refs.popup;
            if (!reference || !popup) {
                return;
            }

            // establish the strategy up front so measurements aren't affected by normal flow
            popup.style.position = this.strategy;
            popup.style.left = '0';
            popup.style.top = '0';

            // autoUpdate positions immediately and keeps it in sync on scroll/resize
            this.cleanup = autoUpdate(reference, popup, () =>
                this.updatePosition(reference, popup),
            );
        },

        updatePosition(reference, popup) {
            computePosition(reference, popup, {
                placement: this.placement,
                strategy: this.strategy,
                middleware: [
                    // popper's offset was [skidding, distance] => [crossAxis, mainAxis]
                    offset({ mainAxis: this.distance, crossAxis: this.skidding }),
                    flip(),
                    shift(),
                ],
            }).then(({ x, y }) => {
                Object.assign(popup.style, { left: `${x}px`, top: `${y}px` });
            });
        },

        stopPositioning() {
            if (this.cleanup) {
                this.cleanup();
                this.cleanup = null;
            }
        },
    },
};
</script>

<style lang="scss">
@use '../styles/px.scss' as *;

.px-popup {
    display: inline-block;

    @include bem('popup', 'non-modal') {
        position: absolute;
        z-index: 1;
    }
}
</style>
