| <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-2 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">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> |
|
|