{"version":3,"file":"OctopusMultiselect-CzuaVZsy.mjs","names":[],"sources":["../src/components/form/OctopusMultiselect.vue","../src/components/form/OctopusMultiselect.vue"],"sourcesContent":["<template>\n    <div\n        ref=\"containerRef\"\n        class=\"octopus-multiselect\"\n        :class=\"{ 'form-margin': label }\"\n    >\n        <label\n            v-if=\"label\"\n            :for=\"computedId\"\n            class=\"form-label\"\n        >\n            {{ label }}\n        </label>\n\n        <div\n            class=\"octopus-multiselect-field\"\n            :class=\"{ disabled, open: isOpen, noBorder }\"\n            @click=\"openDropdown\"\n            @mouseenter=\"onFieldMouseEnter\"\n            @mouseleave=\"isHovered = false\"\n        >\n            <div\n                v-show=\"hasSelected && !isOpen\"\n                ref=\"selectionRef\"\n                class=\"octopus-multiselect-selection\"\n            >\n                <span class=\"octopus-multiselect-selection-text\">{{ selectionLabels }}</span>\n                <span\n                    v-if=\"overflowCount > 0\"\n                    class=\"octopus-multiselect-selection-count\"\n                >(+{{ overflowCount }})</span>\n            </div>\n            <input\n                v-show=\"!hasSelected || isOpen\"\n                :id=\"computedId\"\n                ref=\"inputRef\"\n                v-model=\"searchQuery\"\n                type=\"text\"\n                class=\"octopus-multiselect-input\"\n                :placeholder=\"inputPlaceholder\"\n                :disabled=\"disabled\"\n                @focus=\"openDropdown\"\n                @input=\"handleInput\"\n                @keydown.enter=\"handleCustomValueEnter\"\n            >\n            <button\n                class=\"btn-transparent octopus-multiselect-chevron\"\n                :disabled=\"disabled\"\n                @click.stop=\"toggleDropdown\"\n            >\n                <ChevronDownIcon />\n            </button>\n        </div>\n\n        <Teleport :to=\"teleportTarget\">\n            <div\n                v-if=\"expandOnHover && isHovered && !isOpen && overflowCount > 0\"\n                class=\"octopus-multiselect-hover-tooltip\"\n                :style=\"dropdownStyle\"\n            >\n                {{ allLabelsText }}\n            </div>\n\n            <div\n                v-if=\"isOpen\"\n                ref=\"dropdownRef\"\n                class=\"octopus-multiselect-dropdown\"\n                :style=\"dropdownStyle\"\n            >\n                <ClassicCheckbox\n                    :text-init=\"allSelected\"\n                    :label=\"selectAllText ?? t('All')\"\n                    :is-disabled=\"disabled\"\n                    @update:text-init=\"toggleAll\"\n                />\n\n                <div class=\"octopus-multiselect-options\">\n                    <ClassicCheckbox\n                        v-for=\"(option, index) in visibleOptions\"\n                        :key=\"index\"\n                        :text-init=\"isSelected(option)\"\n                        :label=\"getLabel(option)\"\n                        :is-disabled=\"disabled\"\n                        @update:text-init=\"toggleOption(option)\"\n                    />\n                    <template v-if=\"allowCustomValue && searchQuery.trim()\">\n                        <hr v-if=\"visibleOptions.length > 0\">\n                        <span class=\"text-indic px-2\">\n                            {{ t('Press Enter to add this value') }}\n                        </span>\n                    </template>\n                    <span v-else-if=\"visibleOptions.length === 0\" class=\"text-indic px-2\">\n                        {{ t('No elements found. Consider changing the search query.') }}\n                    </span>\n                </div>\n            </div>\n        </Teleport>\n    </div>\n</template>\n\n<script setup lang=\"ts\" generic=\"T\">\nimport { type CSSProperties, computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport ChevronDownIcon from 'vue-material-design-icons/ChevronDown.vue';\nimport ClassicCheckbox from './ClassicCheckbox.vue';\nimport { useOctopusDropdown } from '../composable/form/useOctopusDropdown';\n\nconst props = defineProps<{\n    /** Optional label displayed above the field. */\n    label?: string;\n    /** Currently selected items. Bind with `v-model:selected`. */\n    selected?: T[];\n    /** Full list of options to display or filter. */\n    options: T[];\n    /** Key of each option object to use as the ID */\n    optionKey?: keyof T;\n    /** Key of each option object to use as the display label. Omit when `options` is a\n     *  plain `string[]` — each string is used as its own label. Note: omitting this for\n     *  an object array is a runtime mistake (not caught at compile time) and will render\n     *  \"[object Object]\". */\n    optionLabel?: keyof T & string;\n    /** When true, pressing Enter in the search input adds the typed text as a new\n     *  selected value, even if it doesn't match any option. Intended for use when\n     *  options are plain strings (`optionLabel` omitted) — casting arbitrary typed\n     *  text into an object-shaped T would not produce a valid option. */\n    allowCustomValue?: boolean;\n    /** Disables the field and all checkboxes when true. */\n    disabled?: boolean;\n    /** Placeholder shown in the input when no items are selected. Defaults to the translated \"Search\" string. */\n    placeholder?: string;\n    /** Label for the \"select all\" checkbox. Defaults to the translated \"All\" string. */\n    selectAllText?: string;\n    /** Disable the border around the input */\n    noBorder?: boolean;\n    /** When true, hovering the closed field with overflow shows a tooltip listing all selected items. */\n    expandOnHover?: boolean;\n    /** When true, options selected at the moment the dropdown opens are moved to the\n     *  top of the list. This is a snapshot taken at open time — it does not live-reorder\n     *  while the dropdown stays open, only on the next closed→open transition. */\n    pullSelectedToTop?: boolean;\n}>();\n\nconst emit = defineEmits<{\n    /** Emitted when the selection changes. */\n    (e: 'update:selected', value: T[]): void;\n    /** Emitted on every input change. When listened to, the parent is responsible for\n     *  updating `options`; otherwise the component filters `options` client-side. */\n    (e: 'search', query: string): void;\n}>();\n\nconst { t } = useI18n();\n\n// Ref on the teleported dropdown div — passed as ignored element to useOctopusDropdown\n// so clicks inside the dropdown don't trigger the click-outside handler.\nconst dropdownRef = ref<HTMLElement | null>(null);\n\nconst {\n    searchQuery,\n    isOpen,\n    isHovered,\n    containerRef,\n    inputRef,\n    teleportTarget,\n    computedId,\n    displayedOptions,\n    inputPlaceholder,\n    getLabel,\n    openDropdown,\n    toggleDropdown,\n    handleInput,\n} = useOctopusDropdown(props, (query) => emit('search', query), 'multiselect', [dropdownRef]);\n\nconst selectionRef = ref<HTMLElement | null>(null);\n\n// Position of the teleported dropdown (position: fixed, anchored below the trigger field)\nconst dropdownStyle = ref<CSSProperties>({});\n\n// Also used to position the hover tooltip: both are teleported and anchored the same way\n// (fixed, directly below the field, same width), and are never shown at the same time.\nfunction updateDropdownPosition(): void {\n    if (!containerRef.value) { return; }\n    const rect = containerRef.value.getBoundingClientRect();\n    dropdownStyle.value = {\n        position: 'fixed',\n        top: `${rect.bottom + 2}px`,\n        left: `${rect.left}px`,\n        width: `${rect.width}px`,\n    };\n}\n\nfunction onFieldMouseEnter(): void {\n    isHovered.value = true;\n    if (props.expandOnHover) {\n        nextTick(updateDropdownPosition);\n    }\n}\n\nconst visibleCount = ref(2);\n\n// Selection snapshot captured the instant the dropdown opens, used only to freeze the\n// sort order when pullSelectedToTop is set — does not react to later `selected` changes\n// while the dropdown stays open.\nconst pinnedSnapshot = ref<T[]>([]);\n\n// Selected values not present in `options` — only populated when allowCustomValue is set,\n// so a custom value typed via handleCustomValueEnter still shows (checked) in the dropdown.\nconst customSelectedOptions = computed<T[]>(() => {\n    if (!props.allowCustomValue) { return []; }\n    return (props.selected ?? []).filter((item) => !isInOptions(item));\n});\n\nconst visibleOptions = computed<T[]>(() => {\n    const query = searchQuery.value.toLowerCase();\n    const filteredCustom = query\n        ? customSelectedOptions.value.filter((option) => getLabel(option).toLowerCase().includes(query))\n        : customSelectedOptions.value;\n    const combined = [...displayedOptions.value, ...filteredCustom];\n    if (!props.pullSelectedToTop) {\n        return combined;\n    }\n    return [...combined.filter(isPinned), ...combined.filter((option) => !isPinned(option))];\n});\n\nconst allSelected = computed(() => {\n    if (visibleOptions.value.length === 0) {\n        return false;\n    }\n    return visibleOptions.value.every((option) => isSelected(option));\n});\n\nconst hasSelected = computed(() => (props.selected?.length ?? 0) > 0);\n\nconst selectionLabels = computed(() =>\n    (props.selected ?? []).slice(0, visibleCount.value).map(getLabel).join(', ')\n);\n\nconst overflowCount = computed(() =>\n    Math.max(0, (props.selected?.length ?? 0) - visibleCount.value)\n);\n\nconst allLabelsText = computed(() =>\n    (props.selected ?? []).map(getLabel).join(', ')\n);\n\nfunction isSelected(option: T): boolean {\n    if (props.optionKey) {\n        return props.selected?.find(s => s[props.optionKey] === option[props.optionKey]) !== undefined;\n    } else {\n        return props.selected?.includes(option) ?? false;\n    }\n}\n\nfunction isInOptions(option: T): boolean {\n    if (props.optionKey) {\n        return props.options.some((opt) => opt[props.optionKey] === option[props.optionKey]);\n    } else {\n        return props.options.includes(option);\n    }\n}\n\nfunction isPinned(option: T): boolean {\n    if (props.optionKey) {\n        return pinnedSnapshot.value.some((s) => s[props.optionKey] === option[props.optionKey]);\n    } else {\n        return pinnedSnapshot.value.includes(option);\n    }\n}\n\nfunction toggleOption(option: T): void {\n    const current = props.selected ?? [];\n    if (isSelected(option)) {\n        const key = props.optionKey;\n        emit('update:selected', key\n            ? current.filter((item) => item[key] !== option[key])\n            : current.filter((item) => item !== option)\n        );\n    } else {\n        emit('update:selected', [...current, option]);\n    }\n}\n\nfunction handleCustomValueEnter(): void {\n    if (!props.allowCustomValue) { return; }\n    const value = searchQuery.value.trim();\n    if (!value) { return; }\n    const customOption = value as unknown as T;\n    if (!isSelected(customOption)) {\n        emit('update:selected', [...(props.selected ?? []), customOption]);\n    }\n    searchQuery.value = '';\n}\n\nfunction toggleAll(val: boolean): void {\n    const current = props.selected ?? [];\n    if (val) {\n        const toAdd = visibleOptions.value.filter((option: T) => !isSelected(option));\n        emit('update:selected', [...current, ...toAdd]);\n    } else {\n        const key = props.optionKey;\n        emit('update:selected', current.filter((item: T) => key\n            ? !visibleOptions.value.some((opt) => opt[key] === item[key])\n            : !visibleOptions.value.includes(item)\n        ));\n    }\n}\n\nfunction updateVisibleCount(): void {\n    const container = selectionRef.value;\n    const selected = props.selected ?? [];\n    if (!container || selected.length < 2) {\n        visibleCount.value = selected.length;\n        return;\n    }\n\n    const availableWidth = container.offsetWidth;\n    if (availableWidth === 0) {\n        return;\n    }\n\n    const labels = selected.map(getLabel);\n    const measurer = document.createElement('span');\n    measurer.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;pointer-events:none;';\n    container.appendChild(measurer);\n\n    measurer.textContent = labels.join(', ');\n    if (measurer.offsetWidth <= availableWidth) {\n        visibleCount.value = labels.length;\n        container.removeChild(measurer);\n        return;\n    }\n\n    measurer.textContent = `(+${labels.length})`;\n    const badgeWidth = measurer.offsetWidth + 4;\n    const textAvailable = availableWidth - badgeWidth;\n\n    let count = 0;\n    for (let i = 0; i < labels.length; i++) {\n        measurer.textContent = labels.slice(0, i + 1).join(', ');\n        if (measurer.offsetWidth > textAvailable) {\n            break;\n        }\n        count = i + 1;\n    }\n\n    container.removeChild(measurer);\n    visibleCount.value = Math.max(1, count);\n}\n\nlet resizeObserver: ResizeObserver | null = null;\n\nonMounted(() => {\n    if (selectionRef.value) {\n        resizeObserver = new ResizeObserver(updateVisibleCount);\n        resizeObserver.observe(selectionRef.value);\n    }\n    updateVisibleCount();\n    // Keep the teleported dropdown aligned when the page scrolls or the viewport resizes\n    window.addEventListener('scroll', updateDropdownPosition, true);\n    window.addEventListener('resize', updateDropdownPosition);\n});\n\nonUnmounted(() => {\n    resizeObserver?.disconnect();\n    window.removeEventListener('scroll', updateDropdownPosition, true);\n    window.removeEventListener('resize', updateDropdownPosition);\n});\n\nwatch(() => props.selected, updateVisibleCount);\n\nwatch(isOpen, (val) => {\n    if (val) {\n        pinnedSnapshot.value = [...(props.selected ?? [])];\n        nextTick(updateDropdownPosition);\n    } else {\n        nextTick(updateVisibleCount);\n    }\n});\n</script>\n\n<style scoped lang=\"scss\">\n.octopus-multiselect {\n    position: relative;\n\n    .octopus-multiselect-field {\n        display: flex;\n        align-items: center;\n        border: 1px solid var(--octopus-border-default);\n        border-radius: var(--octopus-border-radius);\n        background: white;\n        cursor: pointer;\n\n        &.open {\n            border-color: var(--octopus-primary);\n        }\n\n        &.disabled {\n            background: var(--octopus-secondary-lighter);\n            cursor: default;\n        }\n\n        &.noBorder {\n            border: none;\n        }\n    }\n\n    .octopus-multiselect-selection {\n        display: flex;\n        align-items: center;\n        flex: 1;\n        min-width: 0;\n        padding: 0.4rem 0.5rem;\n        height: 2rem;\n        gap: 0.25rem;\n    }\n\n    .octopus-multiselect-selection-text {\n        flex: 1;\n        min-width: 0;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n    }\n\n    .octopus-multiselect-selection-count {\n        flex-shrink: 0;\n        white-space: nowrap;\n        color: var(--octopus-primary);\n    }\n\n    .octopus-multiselect-input {\n        flex: 1;\n        border: none;\n        background: transparent;\n        padding: 0.4rem 0.5rem;\n        padding-right: 0;\n        height: 2rem;\n        outline: none;\n        cursor: inherit;\n        min-width: 0;\n    }\n\n    .octopus-multiselect-chevron {\n        padding: 0.25rem 0.5rem;\n        display: flex;\n        align-items: center;\n    }\n\n}\n\n// Dropdown/tooltip are teleported to body — scoped rules must be top-level so that\n// [data-v-xxxx] is matched directly on the element rather than via a descendant-of-.octopus-multiselect selector.\n.octopus-multiselect-hover-tooltip {\n    z-index: 101;\n    background: white;\n    border: 1px solid var(--octopus-border-default);\n    border-radius: var(--octopus-border-radius);\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n    padding: 0.5rem;\n    word-break: break-word;\n    pointer-events: none;\n}\n\n.octopus-multiselect-dropdown {\n    z-index: 100;\n    background: white;\n    border: 1px solid var(--octopus-border-default);\n    border-radius: var(--octopus-border-radius);\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n    padding: 0.25rem 0;\n\n    > .octopus-form-item {\n        padding: 0.25rem 0.5rem;\n        border-bottom: 1px solid var(--octopus-secondary);\n    }\n}\n\n.octopus-multiselect-options {\n    max-height: 14rem;\n    overflow-y: auto;\n\n    .octopus-form-item {\n        padding: 0.25rem 0.5rem;\n    }\n}\n\nhr {\n    border-top: 1px solid var(--octopus-secondary);\n    border-bottom: none;\n    margin: 0;\n}\n</style>\n","<template>\n    <div\n        ref=\"containerRef\"\n        class=\"octopus-multiselect\"\n        :class=\"{ 'form-margin': label }\"\n    >\n        <label\n            v-if=\"label\"\n            :for=\"computedId\"\n            class=\"form-label\"\n        >\n            {{ label }}\n        </label>\n\n        <div\n            class=\"octopus-multiselect-field\"\n            :class=\"{ disabled, open: isOpen, noBorder }\"\n            @click=\"openDropdown\"\n            @mouseenter=\"onFieldMouseEnter\"\n            @mouseleave=\"isHovered = false\"\n        >\n            <div\n                v-show=\"hasSelected && !isOpen\"\n                ref=\"selectionRef\"\n                class=\"octopus-multiselect-selection\"\n            >\n                <span class=\"octopus-multiselect-selection-text\">{{ selectionLabels }}</span>\n                <span\n                    v-if=\"overflowCount > 0\"\n                    class=\"octopus-multiselect-selection-count\"\n                >(+{{ overflowCount }})</span>\n            </div>\n            <input\n                v-show=\"!hasSelected || isOpen\"\n                :id=\"computedId\"\n                ref=\"inputRef\"\n                v-model=\"searchQuery\"\n                type=\"text\"\n                class=\"octopus-multiselect-input\"\n                :placeholder=\"inputPlaceholder\"\n                :disabled=\"disabled\"\n                @focus=\"openDropdown\"\n                @input=\"handleInput\"\n                @keydown.enter=\"handleCustomValueEnter\"\n            >\n            <button\n                class=\"btn-transparent octopus-multiselect-chevron\"\n                :disabled=\"disabled\"\n                @click.stop=\"toggleDropdown\"\n            >\n                <ChevronDownIcon />\n            </button>\n        </div>\n\n        <Teleport :to=\"teleportTarget\">\n            <div\n                v-if=\"expandOnHover && isHovered && !isOpen && overflowCount > 0\"\n                class=\"octopus-multiselect-hover-tooltip\"\n                :style=\"dropdownStyle\"\n            >\n                {{ allLabelsText }}\n            </div>\n\n            <div\n                v-if=\"isOpen\"\n                ref=\"dropdownRef\"\n                class=\"octopus-multiselect-dropdown\"\n                :style=\"dropdownStyle\"\n            >\n                <ClassicCheckbox\n                    :text-init=\"allSelected\"\n                    :label=\"selectAllText ?? t('All')\"\n                    :is-disabled=\"disabled\"\n                    @update:text-init=\"toggleAll\"\n                />\n\n                <div class=\"octopus-multiselect-options\">\n                    <ClassicCheckbox\n                        v-for=\"(option, index) in visibleOptions\"\n                        :key=\"index\"\n                        :text-init=\"isSelected(option)\"\n                        :label=\"getLabel(option)\"\n                        :is-disabled=\"disabled\"\n                        @update:text-init=\"toggleOption(option)\"\n                    />\n                    <template v-if=\"allowCustomValue && searchQuery.trim()\">\n                        <hr v-if=\"visibleOptions.length > 0\">\n                        <span class=\"text-indic px-2\">\n                            {{ t('Press Enter to add this value') }}\n                        </span>\n                    </template>\n                    <span v-else-if=\"visibleOptions.length === 0\" class=\"text-indic px-2\">\n                        {{ t('No elements found. Consider changing the search query.') }}\n                    </span>\n                </div>\n            </div>\n        </Teleport>\n    </div>\n</template>\n\n<script setup lang=\"ts\" generic=\"T\">\nimport { type CSSProperties, computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport ChevronDownIcon from 'vue-material-design-icons/ChevronDown.vue';\nimport ClassicCheckbox from './ClassicCheckbox.vue';\nimport { useOctopusDropdown } from '../composable/form/useOctopusDropdown';\n\nconst props = defineProps<{\n    /** Optional label displayed above the field. */\n    label?: string;\n    /** Currently selected items. Bind with `v-model:selected`. */\n    selected?: T[];\n    /** Full list of options to display or filter. */\n    options: T[];\n    /** Key of each option object to use as the ID */\n    optionKey?: keyof T;\n    /** Key of each option object to use as the display label. Omit when `options` is a\n     *  plain `string[]` — each string is used as its own label. Note: omitting this for\n     *  an object array is a runtime mistake (not caught at compile time) and will render\n     *  \"[object Object]\". */\n    optionLabel?: keyof T & string;\n    /** When true, pressing Enter in the search input adds the typed text as a new\n     *  selected value, even if it doesn't match any option. Intended for use when\n     *  options are plain strings (`optionLabel` omitted) — casting arbitrary typed\n     *  text into an object-shaped T would not produce a valid option. */\n    allowCustomValue?: boolean;\n    /** Disables the field and all checkboxes when true. */\n    disabled?: boolean;\n    /** Placeholder shown in the input when no items are selected. Defaults to the translated \"Search\" string. */\n    placeholder?: string;\n    /** Label for the \"select all\" checkbox. Defaults to the translated \"All\" string. */\n    selectAllText?: string;\n    /** Disable the border around the input */\n    noBorder?: boolean;\n    /** When true, hovering the closed field with overflow shows a tooltip listing all selected items. */\n    expandOnHover?: boolean;\n    /** When true, options selected at the moment the dropdown opens are moved to the\n     *  top of the list. This is a snapshot taken at open time — it does not live-reorder\n     *  while the dropdown stays open, only on the next closed→open transition. */\n    pullSelectedToTop?: boolean;\n}>();\n\nconst emit = defineEmits<{\n    /** Emitted when the selection changes. */\n    (e: 'update:selected', value: T[]): void;\n    /** Emitted on every input change. When listened to, the parent is responsible for\n     *  updating `options`; otherwise the component filters `options` client-side. */\n    (e: 'search', query: string): void;\n}>();\n\nconst { t } = useI18n();\n\n// Ref on the teleported dropdown div — passed as ignored element to useOctopusDropdown\n// so clicks inside the dropdown don't trigger the click-outside handler.\nconst dropdownRef = ref<HTMLElement | null>(null);\n\nconst {\n    searchQuery,\n    isOpen,\n    isHovered,\n    containerRef,\n    inputRef,\n    teleportTarget,\n    computedId,\n    displayedOptions,\n    inputPlaceholder,\n    getLabel,\n    openDropdown,\n    toggleDropdown,\n    handleInput,\n} = useOctopusDropdown(props, (query) => emit('search', query), 'multiselect', [dropdownRef]);\n\nconst selectionRef = ref<HTMLElement | null>(null);\n\n// Position of the teleported dropdown (position: fixed, anchored below the trigger field)\nconst dropdownStyle = ref<CSSProperties>({});\n\n// Also used to position the hover tooltip: both are teleported and anchored the same way\n// (fixed, directly below the field, same width), and are never shown at the same time.\nfunction updateDropdownPosition(): void {\n    if (!containerRef.value) { return; }\n    const rect = containerRef.value.getBoundingClientRect();\n    dropdownStyle.value = {\n        position: 'fixed',\n        top: `${rect.bottom + 2}px`,\n        left: `${rect.left}px`,\n        width: `${rect.width}px`,\n    };\n}\n\nfunction onFieldMouseEnter(): void {\n    isHovered.value = true;\n    if (props.expandOnHover) {\n        nextTick(updateDropdownPosition);\n    }\n}\n\nconst visibleCount = ref(2);\n\n// Selection snapshot captured the instant the dropdown opens, used only to freeze the\n// sort order when pullSelectedToTop is set — does not react to later `selected` changes\n// while the dropdown stays open.\nconst pinnedSnapshot = ref<T[]>([]);\n\n// Selected values not present in `options` — only populated when allowCustomValue is set,\n// so a custom value typed via handleCustomValueEnter still shows (checked) in the dropdown.\nconst customSelectedOptions = computed<T[]>(() => {\n    if (!props.allowCustomValue) { return []; }\n    return (props.selected ?? []).filter((item) => !isInOptions(item));\n});\n\nconst visibleOptions = computed<T[]>(() => {\n    const query = searchQuery.value.toLowerCase();\n    const filteredCustom = query\n        ? customSelectedOptions.value.filter((option) => getLabel(option).toLowerCase().includes(query))\n        : customSelectedOptions.value;\n    const combined = [...displayedOptions.value, ...filteredCustom];\n    if (!props.pullSelectedToTop) {\n        return combined;\n    }\n    return [...combined.filter(isPinned), ...combined.filter((option) => !isPinned(option))];\n});\n\nconst allSelected = computed(() => {\n    if (visibleOptions.value.length === 0) {\n        return false;\n    }\n    return visibleOptions.value.every((option) => isSelected(option));\n});\n\nconst hasSelected = computed(() => (props.selected?.length ?? 0) > 0);\n\nconst selectionLabels = computed(() =>\n    (props.selected ?? []).slice(0, visibleCount.value).map(getLabel).join(', ')\n);\n\nconst overflowCount = computed(() =>\n    Math.max(0, (props.selected?.length ?? 0) - visibleCount.value)\n);\n\nconst allLabelsText = computed(() =>\n    (props.selected ?? []).map(getLabel).join(', ')\n);\n\nfunction isSelected(option: T): boolean {\n    if (props.optionKey) {\n        return props.selected?.find(s => s[props.optionKey] === option[props.optionKey]) !== undefined;\n    } else {\n        return props.selected?.includes(option) ?? false;\n    }\n}\n\nfunction isInOptions(option: T): boolean {\n    if (props.optionKey) {\n        return props.options.some((opt) => opt[props.optionKey] === option[props.optionKey]);\n    } else {\n        return props.options.includes(option);\n    }\n}\n\nfunction isPinned(option: T): boolean {\n    if (props.optionKey) {\n        return pinnedSnapshot.value.some((s) => s[props.optionKey] === option[props.optionKey]);\n    } else {\n        return pinnedSnapshot.value.includes(option);\n    }\n}\n\nfunction toggleOption(option: T): void {\n    const current = props.selected ?? [];\n    if (isSelected(option)) {\n        const key = props.optionKey;\n        emit('update:selected', key\n            ? current.filter((item) => item[key] !== option[key])\n            : current.filter((item) => item !== option)\n        );\n    } else {\n        emit('update:selected', [...current, option]);\n    }\n}\n\nfunction handleCustomValueEnter(): void {\n    if (!props.allowCustomValue) { return; }\n    const value = searchQuery.value.trim();\n    if (!value) { return; }\n    const customOption = value as unknown as T;\n    if (!isSelected(customOption)) {\n        emit('update:selected', [...(props.selected ?? []), customOption]);\n    }\n    searchQuery.value = '';\n}\n\nfunction toggleAll(val: boolean): void {\n    const current = props.selected ?? [];\n    if (val) {\n        const toAdd = visibleOptions.value.filter((option: T) => !isSelected(option));\n        emit('update:selected', [...current, ...toAdd]);\n    } else {\n        const key = props.optionKey;\n        emit('update:selected', current.filter((item: T) => key\n            ? !visibleOptions.value.some((opt) => opt[key] === item[key])\n            : !visibleOptions.value.includes(item)\n        ));\n    }\n}\n\nfunction updateVisibleCount(): void {\n    const container = selectionRef.value;\n    const selected = props.selected ?? [];\n    if (!container || selected.length < 2) {\n        visibleCount.value = selected.length;\n        return;\n    }\n\n    const availableWidth = container.offsetWidth;\n    if (availableWidth === 0) {\n        return;\n    }\n\n    const labels = selected.map(getLabel);\n    const measurer = document.createElement('span');\n    measurer.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;pointer-events:none;';\n    container.appendChild(measurer);\n\n    measurer.textContent = labels.join(', ');\n    if (measurer.offsetWidth <= availableWidth) {\n        visibleCount.value = labels.length;\n        container.removeChild(measurer);\n        return;\n    }\n\n    measurer.textContent = `(+${labels.length})`;\n    const badgeWidth = measurer.offsetWidth + 4;\n    const textAvailable = availableWidth - badgeWidth;\n\n    let count = 0;\n    for (let i = 0; i < labels.length; i++) {\n        measurer.textContent = labels.slice(0, i + 1).join(', ');\n        if (measurer.offsetWidth > textAvailable) {\n            break;\n        }\n        count = i + 1;\n    }\n\n    container.removeChild(measurer);\n    visibleCount.value = Math.max(1, count);\n}\n\nlet resizeObserver: ResizeObserver | null = null;\n\nonMounted(() => {\n    if (selectionRef.value) {\n        resizeObserver = new ResizeObserver(updateVisibleCount);\n        resizeObserver.observe(selectionRef.value);\n    }\n    updateVisibleCount();\n    // Keep the teleported dropdown aligned when the page scrolls or the viewport resizes\n    window.addEventListener('scroll', updateDropdownPosition, true);\n    window.addEventListener('resize', updateDropdownPosition);\n});\n\nonUnmounted(() => {\n    resizeObserver?.disconnect();\n    window.removeEventListener('scroll', updateDropdownPosition, true);\n    window.removeEventListener('resize', updateDropdownPosition);\n});\n\nwatch(() => props.selected, updateVisibleCount);\n\nwatch(isOpen, (val) => {\n    if (val) {\n        pinnedSnapshot.value = [...(props.selected ?? [])];\n        nextTick(updateDropdownPosition);\n    } else {\n        nextTick(updateVisibleCount);\n    }\n});\n</script>\n\n<style scoped lang=\"scss\">\n.octopus-multiselect {\n    position: relative;\n\n    .octopus-multiselect-field {\n        display: flex;\n        align-items: center;\n        border: 1px solid var(--octopus-border-default);\n        border-radius: var(--octopus-border-radius);\n        background: white;\n        cursor: pointer;\n\n        &.open {\n            border-color: var(--octopus-primary);\n        }\n\n        &.disabled {\n            background: var(--octopus-secondary-lighter);\n            cursor: default;\n        }\n\n        &.noBorder {\n            border: none;\n        }\n    }\n\n    .octopus-multiselect-selection {\n        display: flex;\n        align-items: center;\n        flex: 1;\n        min-width: 0;\n        padding: 0.4rem 0.5rem;\n        height: 2rem;\n        gap: 0.25rem;\n    }\n\n    .octopus-multiselect-selection-text {\n        flex: 1;\n        min-width: 0;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n    }\n\n    .octopus-multiselect-selection-count {\n        flex-shrink: 0;\n        white-space: nowrap;\n        color: var(--octopus-primary);\n    }\n\n    .octopus-multiselect-input {\n        flex: 1;\n        border: none;\n        background: transparent;\n        padding: 0.4rem 0.5rem;\n        padding-right: 0;\n        height: 2rem;\n        outline: none;\n        cursor: inherit;\n        min-width: 0;\n    }\n\n    .octopus-multiselect-chevron {\n        padding: 0.25rem 0.5rem;\n        display: flex;\n        align-items: center;\n    }\n\n}\n\n// Dropdown/tooltip are teleported to body — scoped rules must be top-level so that\n// [data-v-xxxx] is matched directly on the element rather than via a descendant-of-.octopus-multiselect selector.\n.octopus-multiselect-hover-tooltip {\n    z-index: 101;\n    background: white;\n    border: 1px solid var(--octopus-border-default);\n    border-radius: var(--octopus-border-radius);\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n    padding: 0.5rem;\n    word-break: break-word;\n    pointer-events: none;\n}\n\n.octopus-multiselect-dropdown {\n    z-index: 100;\n    background: white;\n    border: 1px solid var(--octopus-border-default);\n    border-radius: var(--octopus-border-radius);\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n    padding: 0.25rem 0;\n\n    > .octopus-form-item {\n        padding: 0.25rem 0.5rem;\n        border-bottom: 1px solid var(--octopus-secondary);\n    }\n}\n\n.octopus-multiselect-options {\n    max-height: 14rem;\n    overflow-y: auto;\n\n    .octopus-form-item {\n        padding: 0.25rem 0.5rem;\n    }\n}\n\nhr {\n    border-top: 1px solid var(--octopus-secondary);\n    border-bottom: none;\n    margin: 0;\n}\n</style>\n"],"mappings":"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"}