|
<script lang="ts"> |
|
import { getAllTags } from '$lib/apis/chats'; |
|
import { tags } from '$lib/stores'; |
|
import { getContext, createEventDispatcher, onMount, onDestroy, tick } from 'svelte'; |
|
import { fade } from 'svelte/transition'; |
|
|
|
const dispatch = createEventDispatcher(); |
|
const i18n = getContext('i18n'); |
|
|
|
export let placeholder = ''; |
|
export let value = ''; |
|
|
|
let selectedIdx = 0; |
|
|
|
let lastWord = ''; |
|
$: lastWord = value ? value.split(' ').at(-1) : value; |
|
|
|
let options = [ |
|
{ |
|
name: 'tag:', |
|
description: $i18n.t('search for tags') |
|
} |
|
]; |
|
let focused = false; |
|
let loading = false; |
|
|
|
let filteredOptions = options; |
|
$: filteredOptions = options.filter((option) => { |
|
return option.name.startsWith(lastWord); |
|
}); |
|
|
|
let filteredTags = []; |
|
$: filteredTags = lastWord.startsWith('tag:') |
|
? [ |
|
...$tags, |
|
{ |
|
id: 'none', |
|
name: $i18n.t('Untagged') |
|
} |
|
].filter((tag) => { |
|
const tagName = lastWord.slice(4); |
|
if (tagName) { |
|
const tagId = tagName.replace(' ', '_').toLowerCase(); |
|
|
|
if (tag.id !== tagId) { |
|
return tag.id.startsWith(tagId); |
|
} else { |
|
return false; |
|
} |
|
} else { |
|
return true; |
|
} |
|
}) |
|
: []; |
|
|
|
const initTags = async () => { |
|
loading = true; |
|
await tags.set(await getAllTags(localStorage.token)); |
|
loading = false; |
|
}; |
|
|
|
const documentClickHandler = (e) => { |
|
const searchContainer = document.getElementById('search-container'); |
|
const chatSearch = document.getElementById('chat-search'); |
|
|
|
if (!searchContainer.contains(e.target) && !chatSearch.contains(e.target)) { |
|
if (e.target.id.startsWith('search-tag-') || e.target.id.startsWith('search-option-')) { |
|
return; |
|
} |
|
focused = false; |
|
} |
|
}; |
|
|
|
onMount(() => { |
|
document.addEventListener('click', documentClickHandler); |
|
}); |
|
|
|
onDestroy(() => { |
|
document.removeEventListener('click', documentClickHandler); |
|
}); |
|
</script> |
|
|
|
<div class="px-1 mb-1 flex justify-center space-x-2 relative z-10" id="search-container"> |
|
<div class="flex w-full rounded-xl" id="chat-search"> |
|
<div class="self-center pl-3 py-2 rounded-l-xl bg-transparent"> |
|
<svg |
|
xmlns="http://www.w3.org/2000/svg" |
|
viewBox="0 0 20 20" |
|
fill="currentColor" |
|
class="w-4 h-4" |
|
> |
|
<path |
|
fill-rule="evenodd" |
|
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" |
|
clip-rule="evenodd" |
|
/> |
|
</svg> |
|
</div> |
|
|
|
<input |
|
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-hidden" |
|
placeholder={placeholder ? placeholder : $i18n.t('Search')} |
|
bind:value |
|
on:input={() => { |
|
dispatch('input'); |
|
}} |
|
on:focus={() => { |
|
focused = true; |
|
initTags(); |
|
}} |
|
on:keydown={(e) => { |
|
if (e.key === 'Enter') { |
|
if (filteredTags.length > 0) { |
|
const tagElement = document.getElementById(`search-tag-${selectedIdx}`); |
|
tagElement.click(); |
|
return; |
|
} |
|
|
|
if (filteredOptions.length > 0) { |
|
const optionElement = document.getElementById(`search-option-${selectedIdx}`); |
|
optionElement.click(); |
|
return; |
|
} |
|
} |
|
|
|
if (e.key === 'ArrowUp') { |
|
e.preventDefault(); |
|
selectedIdx = Math.max(0, selectedIdx - 1); |
|
} else if (e.key === 'ArrowDown') { |
|
e.preventDefault(); |
|
|
|
if (filteredTags.length > 0) { |
|
selectedIdx = Math.min(selectedIdx + 1, filteredTags.length - 1); |
|
} else { |
|
selectedIdx = Math.min(selectedIdx + 1, filteredOptions.length - 1); |
|
} |
|
} else { |
|
// if the user types something, reset to the top selection. |
|
selectedIdx = 0; |
|
} |
|
}} |
|
/> |
|
</div> |
|
|
|
{#if focused && (filteredOptions.length > 0 || filteredTags.length > 0)} |
|
<!-- svelte-ignore a11y-no-static-element-interactions --> |
|
<div |
|
class="absolute top-0 mt-8 left-0 right-1 border dark:border-gray-900 bg-gray-50 dark:bg-gray-950 rounded-lg z-10 shadow-lg" |
|
in:fade={{ duration: 50 }} |
|
on:mouseenter={() => { |
|
selectedIdx = null; |
|
}} |
|
on:mouseleave={() => { |
|
selectedIdx = 0; |
|
}} |
|
> |
|
<div class="px-2 py-2 text-xs group"> |
|
{#if filteredTags.length > 0} |
|
<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1">Tags</div> |
|
|
|
<div class="max-h-60 overflow-auto"> |
|
{#each filteredTags as tag, tagIdx} |
|
<button |
|
class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx === |
|
tagIdx |
|
? 'bg-gray-100 dark:bg-gray-900' |
|
: ''}" |
|
id="search-tag-{tagIdx}" |
|
on:click|stopPropagation={async () => { |
|
const words = value.split(' '); |
|
|
|
words.pop(); |
|
words.push(`tag:${tag.id} `); |
|
|
|
value = words.join(' '); |
|
|
|
dispatch('input'); |
|
}} |
|
> |
|
<div class="dark:text-gray-300 text-gray-700 font-medium line-clamp-1 shrink-0"> |
|
{tag.name} |
|
</div> |
|
|
|
<div class=" text-gray-500 line-clamp-1"> |
|
{tag.id} |
|
</div> |
|
</button> |
|
{/each} |
|
</div> |
|
{:else if filteredOptions.length > 0} |
|
<div class="px-1 font-medium dark:text-gray-300 text-gray-700 mb-1"> |
|
{$i18n.t('Search options')} |
|
</div> |
|
|
|
<div class=" max-h-60 overflow-auto"> |
|
{#each filteredOptions as option, optionIdx} |
|
<button |
|
class=" px-1.5 py-0.5 flex gap-1 hover:bg-gray-100 dark:hover:bg-gray-900 w-full rounded {selectedIdx === |
|
optionIdx |
|
? 'bg-gray-100 dark:bg-gray-900' |
|
: ''}" |
|
id="search-option-{optionIdx}" |
|
on:click|stopPropagation={async () => { |
|
const words = value.split(' '); |
|
|
|
words.pop(); |
|
words.push('tag:'); |
|
|
|
value = words.join(' '); |
|
|
|
dispatch('input'); |
|
}} |
|
> |
|
<div class="dark:text-gray-300 text-gray-700 font-medium">{option.name}</div> |
|
|
|
<div class=" text-gray-500 line-clamp-1"> |
|
{option.description} |
|
</div> |
|
</button> |
|
{/each} |
|
</div> |
|
{/if} |
|
</div> |
|
</div> |
|
{/if} |
|
</div> |
|
|