Files
Umbrella/frontend/src/Components/Autocomplete.svelte
T
StephanRichter 75814a86eb altered tag search in order to preserve case of input
re-set focus on inpt when selection option on autocomplete field

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-10-05 14:37:25 +02:00

162 lines
4.9 KiB
Svelte

<script>
import { t } from '../translations.svelte.js'
import { tick } from "svelte";
let {
enabled = true,
id = null,
autofocus = false,
getCandidates = dummyGetCandidates,
onCommit = dummyOnCommit,
onSelect = dummyOnSelect,
candidate = $bindable({ display : '' })
} = $props();
const ignore = ['ArrowLeft','ArrowRight'];
//let candidate = $state({ display : '' });
let selected = $state(null);
let candidates = $state([]);
let timer = null;
let input, list_elem;
async function dummyGetCandidates(text){
console.warn(`getCandidates(${text}) not overridden!`);
if (!text) return [];
return [
{
display : 'candidate 1',
explanation : 'candidates need to have a display field'
},
{
display : 'candidate 2',
additional : 'other fields are optional',
more : 'and may be domain specific'
},
{ display : text }
];
}
function dummyOnCommit(candidate){
// if Enter is pressed on the input field, this method gets called with
// either the selected candidate or
// an anonymous object with the entered text in the display field
console.warn(`onCommit(${JSON.stringify(candidate)}) not overridden!`);
// if the method returns true, the field will be cleared after committing
return true;
}
function dummyOnSelect(candidate){
console.warn(`${candidate.display} selected, but onSelect not overridden!`)
}
function disableDropdown(){
candidates = [];
selected = null;
}
async function ondblclick(evt){
const select = evt.target;
const idx = select.value;
candidate = candidates[idx];
disableDropdown();
onSelect(candidate);
}
async function fetchCandidates(){
candidates = candidate.display ? await getCandidates(candidate.display) : [];
selected = null;
}
function onblur(ev){
setTimeout(disableDropdown,400);
}
async function onkeyup(ev){
if (ignore.includes(ev.key)) return;
if (ev.key == 'ArrowDown'){
ev.preventDefault();
selected = selected == null ? 0: selected +1;
if (selected >= candidates.length) selected = 0;
scrollTo(selected);
return false;
}
if (ev.key == 'ArrowUp'){
ev.preventDefault();
selected = selected == null ? candidates.length -1 : selected -1;
if (selected < 0) selected = candidates.length -1;
scrollTo(selected);
return false;
}
if (ev.key == 'Enter'|| ev.key == 'Tab'){
ev.preventDefault();
if (selected != null && selected < candidates.length) {
candidate = candidates[selected];
disableDropdown();
onSelect(candidate);
return false;
}
if (ev.key == 'Enter') {
disableDropdown();
if (onCommit(candidate)) candidate = { display : '' };
}
return false;
}
if (ev.key == 'Escape'){
ev.preventDefault();
disableDropdown();
return false;
}
candidate = { display : candidate.display };
if (timer) clearTimeout(timer);
timer = setTimeout(fetchCandidates,400);
return false;
}
function select(index){
candidate = candidates[index];
disableDropdown();
input.focus();
onSelect(candidate);
}
function scrollTo(index){
let list_elements = list_elem.children;
if (list_elements) list_elements[index].scrollIntoView({block:'center'});
}
</script>
<style>
span { position : relative }
ul {
position : absolute;
top: 30px;
left: 3px;
background: black;
color: orange;
border: 1px solid orange;
border-radius: 5px;
z-index: 65;
list-style: none;
padding: 4px;
margin: 0;
min-width: 400px;
max-height: 200px;
overflow: scroll;
}
.highlight { background: orange; color: black; }
</style>
<span class="autocomplete">
<input bind:this={input} type="text" bind:value={candidate.display} {onkeyup} autofocus={autofocus} {id} {onblur} disabled={!enabled} />
{#if candidates && candidates.length > 0}
<ul bind:this={list_elem} class="suggestions" tabindex="-1">
{#each candidates as candidate,i}
<li class="option {selected==i?'highlight':''}" tabindex="-1" onclick={e => select(i)} ondblclick={e => select(i)}>{candidate.display}</li>
{/each}
</ul>
{/if}
</span>