mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-24 13:22:23 +02:00
226 lines
6.9 KiB
Svelte
226 lines
6.9 KiB
Svelte
<!--
|
|
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
|
// Copyright © 2021 Hardcore Engineering Inc.
|
|
//
|
|
// 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 { Ref, RelatedDocument, DocumentQuery } from '@hcengineering/core'
|
|
|
|
import { getResource, IntlString } from '@hcengineering/platform'
|
|
import ui, {
|
|
createFocusManager,
|
|
deviceOptionsStore,
|
|
FocusHandler,
|
|
IconSearch,
|
|
Label,
|
|
ListView,
|
|
resizeObserver,
|
|
Icon,
|
|
tooltip,
|
|
EditWithIcon
|
|
} from '@hcengineering/ui'
|
|
import { createEventDispatcher } from 'svelte'
|
|
import presentation from '../plugin'
|
|
import { ObjectSearchCategory, ObjectSearchResult } from '../types'
|
|
import { getClient } from '../utils'
|
|
import { hasResource, reduceCalls } from '..'
|
|
|
|
export let query: string = ''
|
|
export let label: IntlString | undefined = undefined
|
|
export let relatedDocuments: RelatedDocument[] | undefined = undefined
|
|
export let ignore: RelatedDocument[] | undefined = undefined
|
|
export let allowCategory: Ref<ObjectSearchCategory>[] | undefined = undefined
|
|
// export let hideButtons = false
|
|
|
|
let items: ObjectSearchResult[] = []
|
|
|
|
let categories: ObjectSearchCategory[] = []
|
|
let categoryStatus: Record<Ref<ObjectSearchCategory>, number> = {}
|
|
|
|
let category: ObjectSearchCategory | undefined
|
|
const categoryQuery: DocumentQuery<ObjectSearchCategory> = {
|
|
context: 'search'
|
|
}
|
|
if (allowCategory !== undefined) {
|
|
categoryQuery._id = { $in: allowCategory }
|
|
}
|
|
|
|
void getClient()
|
|
.findAll(presentation.class.ObjectSearchCategory, categoryQuery)
|
|
.then((r) => {
|
|
categories = r.filter((it) => hasResource(it.query))
|
|
category = categories[0]
|
|
})
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
let list: ListView
|
|
let selection = 0
|
|
|
|
function dispatchItem (item: ObjectSearchResult): void {
|
|
dispatch('close', item)
|
|
}
|
|
|
|
export function onKeyDown (key: KeyboardEvent): boolean {
|
|
if (key.key === 'ArrowDown') {
|
|
key.stopPropagation()
|
|
key.preventDefault()
|
|
list.select(selection + 1)
|
|
return true
|
|
}
|
|
if (key.key === 'ArrowUp') {
|
|
key.stopPropagation()
|
|
key.preventDefault()
|
|
list.select(selection - 1)
|
|
}
|
|
if (key.key === 'Tab') {
|
|
key.stopPropagation()
|
|
key.preventDefault()
|
|
const visibleCategory = categories.filter((it) => (categoryStatus[it._id] ?? 0) > 0)
|
|
const pos = category !== undefined ? visibleCategory.findIndex((it) => it._id === category?._id) : -1
|
|
if (pos >= 0) {
|
|
category = visibleCategory[(pos + 1) % visibleCategory.length]
|
|
return true
|
|
}
|
|
}
|
|
if (key.key === 'Enter') {
|
|
key.preventDefault()
|
|
key.stopPropagation()
|
|
const item = items[selection]
|
|
if (item != null) {
|
|
dispatchItem(item)
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function done (): void {}
|
|
|
|
const updateItems = reduceCalls(async function updateItems (
|
|
cat: ObjectSearchCategory | undefined,
|
|
query: string,
|
|
relatedDocuments?: RelatedDocument[]
|
|
): Promise<void> {
|
|
if (cat === undefined) {
|
|
return
|
|
}
|
|
|
|
const newCategoryStatus: Record<Ref<ObjectSearchCategory>, number> = {}
|
|
const f = await getResource(cat.query)
|
|
|
|
const client = getClient()
|
|
const result = await f(client, query, { in: relatedDocuments, nin: ignore })
|
|
// We need to sure, results we return is for proper category.
|
|
if (cat._id === category?._id) {
|
|
items = result
|
|
if (selection > items.length) {
|
|
selection = 0
|
|
}
|
|
newCategoryStatus[cat._id] = result.length
|
|
}
|
|
|
|
// Automatic try next category and show available items if so
|
|
for (const c of categories) {
|
|
if (c._id !== cat._id) {
|
|
const ff = await getResource(c.query)
|
|
const cresult = await ff(client, query, { in: relatedDocuments, nin: ignore })
|
|
newCategoryStatus[c._id] = cresult.length
|
|
}
|
|
}
|
|
categoryStatus = newCategoryStatus
|
|
|
|
if (items.length === 0) {
|
|
for (const c of categories) {
|
|
if (category !== c && (categoryStatus[c._id] ?? 0) > 0) {
|
|
category = c
|
|
break
|
|
}
|
|
}
|
|
}
|
|
})
|
|
$: void updateItems(category, query, relatedDocuments)
|
|
|
|
const manager = createFocusManager()
|
|
|
|
const isStatusDisabled = (status: number) => status === 0
|
|
</script>
|
|
|
|
<FocusHandler {manager} />
|
|
|
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
|
<form class="antiPopup" on:keydown={onKeyDown} use:resizeObserver={() => dispatch('changeSize')}>
|
|
<div class="ap-menuHeader">
|
|
{#if label}
|
|
<div class="ap-subheader caption">
|
|
<Label {label} />
|
|
</div>
|
|
{/if}
|
|
<div class="tabs">
|
|
{#each categories as c, i}
|
|
{@const status = categoryStatus[c._id] ?? 0}
|
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
<div
|
|
class="tab"
|
|
class:selected={category?._id === c._id}
|
|
class:disabled={isStatusDisabled(status)}
|
|
use:tooltip={{ label: c.label }}
|
|
tabindex={i + 1}
|
|
on:click={() => (category = c)}
|
|
>
|
|
<Icon icon={c.icon} size={'small'} />
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
<div class="pt-2 pb-1 pl-2 pr-2">
|
|
<EditWithIcon
|
|
icon={IconSearch}
|
|
size={'large'}
|
|
width={'100%'}
|
|
autoFocus={!$deviceOptionsStore.isMobile}
|
|
bind:value={query}
|
|
placeholder={category?.label}
|
|
on:input={() => updateItems(category, query, relatedDocuments)}
|
|
/>
|
|
</div>
|
|
<div class="ap-menuItem separator halfMargin" />
|
|
<div class="ap-subheader">
|
|
<Label label={ui.string.Suggested} />
|
|
</div>
|
|
<div class="ap-scroll">
|
|
<div class="ap-box">
|
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
<ListView bind:this={list} bind:selection count={items.length}>
|
|
<svelte:fragment slot="item" let:item>
|
|
{@const doc = items[item]}
|
|
<div
|
|
class="ap-menuItem withComp"
|
|
on:click={() => {
|
|
dispatchItem(doc)
|
|
}}
|
|
>
|
|
<svelte:component this={doc.component} value={doc.doc} {...doc.componentProps ?? {}} />
|
|
</div>
|
|
</svelte:fragment>
|
|
</ListView>
|
|
</div>
|
|
</div>
|
|
<div class="ap-space x2" />
|
|
</form>
|