Spaces:
Running
Running
<script lang="ts"> | |
import { tags } from '$lib/stores'; | |
import { stringify } from 'postcss'; | |
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 focused = false; | |
let options = [ | |
{ | |
name: 'tag:', | |
description: $i18n.t('search for tags') | |
} | |
]; | |
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 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-none" | |
placeholder={placeholder ? placeholder : $i18n.t('Search')} | |
bind:value | |
on:input={() => { | |
dispatch('input'); | |
}} | |
on:focus={() => { | |
focused = true; | |
}} | |
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 flex-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> | |