<!-- // Copyright © 2020 Anticrm Platform Contributors. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. --> <script lang="ts"> import type { IntlString } from '@hcengineering/platform' import { createEventDispatcher } from 'svelte' import { deviceOptionsStore, resizeObserver } from '..' import { createFocusManager } from '../focus' import type { SelectPopupValueType } from '../types' import EditWithIcon from './EditWithIcon.svelte' import FocusHandler from './FocusHandler.svelte' import Icon from './Icon.svelte' import Label from './Label.svelte' import ListView from './ListView.svelte' import Spinner from './Spinner.svelte' import IconCheck from './icons/Check.svelte' import IconSearch from './icons/Search.svelte' import { translate } from '@hcengineering/platform' import { themeStore } from '@hcengineering/theme' export let placeholder: IntlString | undefined = undefined export let placeholderParam: any | undefined = undefined export let searchable: boolean = false export let value: SelectPopupValueType[] export let width: 'medium' | 'large' | 'full' = 'medium' export let size: 'small' | 'medium' | 'large' = 'small' export let onSelect: ((value: SelectPopupValueType['id'], event?: Event) => void) | undefined = undefined export let showShadow: boolean = true export let embedded: boolean = false export let componentLink: boolean = false export let loading: boolean = false let popupElement: HTMLDivElement | undefined = undefined export let search: string = '' const dispatch = createEventDispatcher() $: hasSelected = value.some((v) => v.isSelected) let selection = 0 let list: ListView let selected: any function sendSelect (id: SelectPopupValueType['id']): void { selected = id if (onSelect) { onSelect(id) } else { dispatch('close', id) } } export function onKeydown (key: KeyboardEvent): boolean { if (key.code === 'Tab') { dispatch('close') key.preventDefault() key.stopPropagation() return true } if (key.code === 'ArrowUp') { key.stopPropagation() key.preventDefault() list.select(selection - 1) return true } if (key.code === 'ArrowDown') { key.stopPropagation() key.preventDefault() list.select(selection + 1) return true } if (key.code === 'Enter') { key.preventDefault() key.stopPropagation() sendSelect(filteredObjects[selection].id) return true } return false } const manager = createFocusManager() let itemLabelsTranslation: Map<string, string> = new Map<string, string>() async function translateLabels (values: SelectPopupValueType[]): Promise<void> { const translateValue: (value: SelectPopupValueType) => Promise<[string, string] | null> = async (e) => { if (e.label === undefined) { return null } const text = await translate(e.label, {}, $themeStore.language) return [e.label, text] } const promises = values.map(translateValue) const result: Array<readonly [string, string] | null> = await Promise.all(promises) itemLabelsTranslation = new Map(result.filter((r) => r !== null) as Array<readonly [string, string]>) } $: void translateLabels(value) $: filteredObjects = value.filter((el) => (itemLabelsTranslation.get(el.label ?? '') ?? el.text ?? '').toLowerCase().includes(search.toLowerCase()) ) $: huge = size === 'medium' || size === 'large' $: if (popupElement) { popupElement.focus() } </script> <FocusHandler {manager} /> <!-- svelte-ignore a11y-no-noninteractive-tabindex --> <!-- svelte-ignore a11y-no-static-element-interactions --> <div class="selectPopup" bind:this={popupElement} tabindex="0" class:noShadow={!showShadow} class:full-width={width === 'full'} class:max-width-40={width === 'large'} class:embedded use:resizeObserver={() => { dispatch('changeContent') }} on:keydown={onKeydown} > {#if searchable} <div class="header"> <EditWithIcon icon={IconSearch} size={'large'} width={'100%'} autoFocus={!$deviceOptionsStore.isMobile} bind:value={search} {placeholder} {placeholderParam} on:change /> </div> {:else} <div class="menu-space" /> {/if} <div class="scroll"> <div class="box"> <ListView bind:this={list} count={filteredObjects.length} bind:selection on:changeContent={() => dispatch('changeContent')} > <svelte:fragment slot="item" let:item={itemId}> {@const item = filteredObjects[itemId]} <button class="menu-item withList w-full" on:click={() => { sendSelect(item.id) }} disabled={loading} > <div class="flex-row-center flex-grow" class:pointer-events-none={!componentLink}> {#if item.component} <div class="flex-grow clear-mins"><svelte:component this={item.component} {...item.props} /></div> {:else} {#if item.icon} <div class="icon mr-2"> <Icon icon={item.icon} iconProps={item.iconProps} fill={item.iconColor ?? 'currentColor'} {size} /> </div> {/if} <span class="label overflow-label flex-grow" class:text-base={huge}> {#if item.label} <Label label={item.label} /> {:else if item.text} {item.text} {/if} </span> {/if} {#if hasSelected} <div class="check"> {#if item.isSelected} <Icon icon={IconCheck} size={'small'} /> {/if} </div> {/if} {#if item.id === selected && loading} <Spinner size={'small'} /> {/if} </div> </button> </svelte:fragment> <svelte:fragment slot="category" let:item={row}> {@const obj = filteredObjects[row]} {#if obj.category && ((row === 0 && obj.category.label !== undefined) || obj.category.label !== filteredObjects[row - 1]?.category?.label)} {#if row > 0}<div class="menu-separator" />{/if} <div class="menu-group__header flex-row-center"> <span class="overflow-label"> <Label label={obj.category.label} /> </span> </div> {/if} </svelte:fragment> </ListView> </div> </div> {#if !embedded}<div class="menu-space" />{/if} </div>