| <script lang="ts"> |
| import { createEventDispatcher } from "svelte"; |
|
|
| import type { MenuListEntry } from "@graphite/messages"; |
|
|
| import MenuList from "@graphite/components/floating-menus/MenuList.svelte"; |
| import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; |
| import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; |
| import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; |
|
|
| const DASH_ENTRY = { value: "", label: "-" }; |
|
|
| const dispatch = createEventDispatcher<{ selectedIndex: number; hoverInEntry: number; hoverOutEntry: number }>(); |
|
|
| let menuList: MenuList | undefined; |
| let self: LayoutRow | undefined; |
|
|
| export let entries: MenuListEntry[][]; |
| export let selectedIndex: number | undefined = undefined; |
| export let drawIcon = false; |
| export let interactive = true; |
| export let disabled = false; |
| export let tooltip: string | undefined = undefined; |
| export let minWidth = 0; |
| export let maxWidth = 0; |
|
|
| let activeEntry = makeActiveEntry(); |
| let activeEntrySkipWatcher = false; |
| let initialSelectedIndex: number | undefined = undefined; |
| let open = false; |
|
|
| $: watchSelectedIndex(selectedIndex); |
| $: watchEntries(entries); |
| $: watchActiveEntry(activeEntry); |
| $: watchOpen(open); |
|
|
| function watchOpen(open: boolean) { |
| initialSelectedIndex = open ? selectedIndex : undefined; |
| } |
|
|
| |
| function watchSelectedIndex(_?: typeof selectedIndex) { |
| activeEntrySkipWatcher = true; |
| activeEntry = makeActiveEntry(); |
| } |
|
|
| |
| function watchEntries(_?: typeof entries) { |
| activeEntrySkipWatcher = true; |
| activeEntry = makeActiveEntry(); |
| } |
|
|
| |
| function watchActiveEntry(activeEntry: MenuListEntry) { |
| if (activeEntrySkipWatcher) { |
| activeEntrySkipWatcher = false; |
| } else if (activeEntry !== DASH_ENTRY) { |
| |
| if (initialSelectedIndex !== undefined) dispatch("hoverInEntry", initialSelectedIndex); |
| dispatch("selectedIndex", entries.flat().indexOf(activeEntry)); |
| } |
| } |
|
|
| function dispatchHoverInEntry(hoveredEntry: MenuListEntry) { |
| dispatch("hoverInEntry", entries.flat().indexOf(hoveredEntry)); |
| } |
|
|
| function dispatchHoverOutEntry() { |
| if (initialSelectedIndex !== undefined) dispatch("hoverOutEntry", initialSelectedIndex); |
| } |
|
|
| function makeActiveEntry(): MenuListEntry { |
| const allEntries = entries.flat(); |
|
|
| if (selectedIndex !== undefined && selectedIndex >= 0 && selectedIndex < allEntries.length) { |
| return allEntries[selectedIndex]; |
| } |
| return DASH_ENTRY; |
| } |
|
|
| function unFocusDropdownBox(e: FocusEvent) { |
| const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]") || undefined; |
| if (blurTarget !== self?.div?.()) open = false; |
| } |
| </script> |
|
|
| <LayoutRow |
| class="dropdown-input" |
| styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}), ...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}) }} |
| bind:this={self} |
| data-dropdown-input |
| > |
| <LayoutRow |
| class="dropdown-box" |
| classes={{ disabled, open }} |
| {tooltip} |
| on:click={() => !disabled && (open = true)} |
| on:blur={unFocusDropdownBox} |
| tabindex={disabled ? -1 : 0} |
| data-floating-menu-spawner |
| > |
| {#if activeEntry.icon} |
| <IconLabel class="dropdown-icon" icon={activeEntry.icon} /> |
| {/if} |
| <TextLabel class="dropdown-label">{activeEntry.label}</TextLabel> |
| <IconLabel class="dropdown-arrow" icon="DropdownArrow" /> |
| </LayoutRow> |
| <MenuList |
| on:naturalWidth={({ detail }) => (minWidth = detail)} |
| {activeEntry} |
| on:activeEntry={({ detail }) => (activeEntry = detail)} |
| on:hoverInEntry={({ detail }) => dispatchHoverInEntry(detail)} |
| on:hoverOutEntry={() => dispatchHoverOutEntry()} |
| {open} |
| on:open={({ detail }) => (open = detail)} |
| {entries} |
| {drawIcon} |
| {interactive} |
| direction="Bottom" |
| scrollableY={true} |
| bind:this={menuList} |
| /> |
| </LayoutRow> |
|
|
| <style lang="scss" global> |
| .dropdown-input { |
| position: relative; |
| |
| .dropdown-box { |
| align-items: center; |
| white-space: nowrap; |
| background: var(--color-1-nearblack); |
| height: 24px; |
| border-radius: 2px; |
| |
| .dropdown-label { |
| margin: 0; |
| margin-left: 8px; |
| flex: 1 1 100%; |
| } |
| |
| .dropdown-icon { |
| margin: 4px 8px; |
| flex: 0 0 auto; |
| |
| & + .dropdown-label { |
| margin-left: 0; |
| } |
| } |
| |
| .dropdown-arrow { |
| margin: 4px; |
| margin-right: 2px; |
| flex: 0 0 auto; |
| } |
| |
| &:hover, |
| &.open { |
| background: var(--color-4-dimgray); |
| } |
| |
| &.disabled { |
| background: var(--color-2-mildblack); |
| |
| .text-label { |
| color: var(--color-8-uppergray); |
| } |
| |
| svg { |
| fill: var(--color-8-uppergray); |
| } |
| } |
| } |
| |
| .menu-list .floating-menu-container .floating-menu-content { |
| max-height: 400px; |
| } |
| } |
| </style> |
|
|