|
13 | 13 | // limitations under the License. |
14 | 14 | --> |
15 | 15 | <script lang="ts"> |
16 | | - import { createEventDispatcher } from 'svelte' |
| 16 | + import { type Ref, SortingOrder } from '@hcengineering/core' |
| 17 | + import { createEventDispatcher, onDestroy } from 'svelte' |
17 | 18 | import textEditor from '@hcengineering/text-editor' |
18 | 19 | import { getEmbeddedLabel } from '@hcengineering/platform' |
19 | | - import { Card } from '@hcengineering/presentation' |
20 | | - import { EditBox } from '@hcengineering/ui' |
| 20 | + import presentation, { Card, getClient } from '@hcengineering/presentation' |
| 21 | + import { EditBox, Label, ListView } from '@hcengineering/ui' |
| 22 | + import { buildReferenceUrl } from './extension/reference' |
| 23 | + import document, { type Document } from '@hcengineering/document' |
21 | 24 |
|
22 | 25 | export let link = '' |
23 | 26 |
|
24 | 27 | const dispatch = createEventDispatcher() |
25 | | - const linkPlaceholder = getEmbeddedLabel('http://my.link.net') |
| 28 | + const client = getClient() |
| 29 | + const linkPlaceholder = getEmbeddedLabel('URL or document name') |
| 30 | +
|
| 31 | + let items: Document[] = [] |
| 32 | + let list: ListView |
| 33 | + let selection = 0 |
| 34 | + let searchQuery = '' |
| 35 | + let debounceTimer: any |
| 36 | +
|
| 37 | + function isUrl (text: string): boolean { |
| 38 | + if (text.length === 0) return false |
| 39 | + return text.includes('://') || text.startsWith('http') || text.startsWith('www.') |
| 40 | + } |
26 | 41 |
|
27 | 42 | function save (): void { |
28 | 43 | dispatch('update', link) |
29 | 44 | } |
30 | 45 |
|
31 | | - $: canSave = link === '' || URL.canParse(link) |
| 46 | + function selectItem (doc: Document): void { |
| 47 | + const refUrl = buildReferenceUrl({ |
| 48 | + id: doc._id, |
| 49 | + objectclass: doc._class, |
| 50 | + label: doc.title |
| 51 | + }) |
| 52 | + if (refUrl !== undefined) { |
| 53 | + link = refUrl |
| 54 | + } |
| 55 | + } |
| 56 | +
|
| 57 | + async function doSearch (query: string): Promise<void> { |
| 58 | + if (query.length === 0 || isUrl(query)) { |
| 59 | + items = [] |
| 60 | + return |
| 61 | + } |
| 62 | + try { |
| 63 | + const r = await client.findAll( |
| 64 | + document.class.Document, |
| 65 | + { title: { $like: `%${query}%` } }, |
| 66 | + { limit: 10, sort: { title: SortingOrder.Ascending } } |
| 67 | + ) |
| 68 | + if (query === searchQuery) { |
| 69 | + items = r |
| 70 | + selection = 0 |
| 71 | + } |
| 72 | + } catch (e) { |
| 73 | + console.error('LinkPopup search error:', e) |
| 74 | + items = [] |
| 75 | + } |
| 76 | + } |
| 77 | +
|
| 78 | + function onInput (value: string): void { |
| 79 | + searchQuery = value |
| 80 | + clearTimeout(debounceTimer) |
| 81 | + if (value.length === 0 || isUrl(value) || value.startsWith('ref://')) { |
| 82 | + items = [] |
| 83 | + return |
| 84 | + } |
| 85 | + debounceTimer = setTimeout(() => { |
| 86 | + void doSearch(value) |
| 87 | + }, 200) |
| 88 | + } |
| 89 | +
|
| 90 | + $: onInput(link) |
| 91 | +
|
| 92 | + onDestroy(() => { |
| 93 | + clearTimeout(debounceTimer) |
| 94 | + }) |
| 95 | +
|
| 96 | + function handleKeydown (event: KeyboardEvent): void { |
| 97 | + if (items.length === 0) return |
| 98 | + if (event.key === 'ArrowDown') { |
| 99 | + event.preventDefault() |
| 100 | + event.stopPropagation() |
| 101 | + selection = Math.min(selection + 1, items.length - 1) |
| 102 | + list?.select(selection) |
| 103 | + } else if (event.key === 'ArrowUp') { |
| 104 | + event.preventDefault() |
| 105 | + event.stopPropagation() |
| 106 | + selection = Math.max(selection - 1, 0) |
| 107 | + list?.select(selection) |
| 108 | + } else if (event.key === 'Enter') { |
| 109 | + event.preventDefault() |
| 110 | + event.stopPropagation() |
| 111 | + selectItem(items[selection]) |
| 112 | + } |
| 113 | + } |
| 114 | +
|
| 115 | + $: canSave = link === '' || link.startsWith('ref://') || URL.canParse(link) |
32 | 116 | </script> |
33 | 117 |
|
34 | | -<Card |
35 | | - label={textEditor.string.Link} |
36 | | - okLabel={textEditor.string.Save} |
37 | | - okAction={save} |
38 | | - {canSave} |
39 | | - on:close={() => { |
40 | | - dispatch('close') |
41 | | - }} |
42 | | - on:changeContent |
43 | | -> |
44 | | - <EditBox placeholder={linkPlaceholder} bind:value={link} autoFocus /> |
45 | | -</Card> |
| 118 | +<!-- svelte-ignore a11y-no-static-element-interactions --> |
| 119 | +<div on:keydown|capture={handleKeydown}> |
| 120 | + <Card |
| 121 | + label={textEditor.string.Link} |
| 122 | + okLabel={textEditor.string.Save} |
| 123 | + okAction={save} |
| 124 | + {canSave} |
| 125 | + on:close={() => { |
| 126 | + dispatch('close') |
| 127 | + }} |
| 128 | + on:changeContent |
| 129 | + > |
| 130 | + <EditBox placeholder={linkPlaceholder} bind:value={link} autoFocus /> |
| 131 | + {#if items.length > 0} |
| 132 | + <div class="searchResults"> |
| 133 | + <ListView bind:this={list} bind:selection count={items.length}> |
| 134 | + <svelte:fragment slot="item" let:item={num}> |
| 135 | + {@const item = items[num]} |
| 136 | + <!-- svelte-ignore a11y-click-events-have-key-events --> |
| 137 | + <!-- svelte-ignore a11y-no-static-element-interactions --> |
| 138 | + <div |
| 139 | + class="ap-menuItem withComp h-8" |
| 140 | + style="padding-left: 0.75rem; display: flex; align-items: center;" |
| 141 | + on:click={() => { |
| 142 | + selectItem(item) |
| 143 | + }} |
| 144 | + > |
| 145 | + <span class="overflow-label">{item.title}</span> |
| 146 | + </div> |
| 147 | + </svelte:fragment> |
| 148 | + </ListView> |
| 149 | + </div> |
| 150 | + {:else if searchQuery.length > 0 && !isUrl(searchQuery) && !searchQuery.startsWith('ref://')} |
| 151 | + <div class="noResults"><Label label={presentation.string.NoResults} /></div> |
| 152 | + {/if} |
| 153 | + </Card> |
| 154 | +</div> |
| 155 | + |
| 156 | +<style lang="scss"> |
| 157 | + .searchResults { |
| 158 | + max-height: 15rem; |
| 159 | + overflow-y: auto; |
| 160 | + margin-top: 0.5rem; |
| 161 | + } |
| 162 | +
|
| 163 | + .noResults { |
| 164 | + display: flex; |
| 165 | + padding: 0.25rem 0; |
| 166 | + color: var(--theme-dark-color); |
| 167 | + } |
| 168 | +</style> |
0 commit comments