147 lines
4.4 KiB
Svelte
147 lines
4.4 KiB
Svelte
<script>
|
|
import { t } from '../translations.svelte.js'
|
|
import { tick } from "svelte";
|
|
|
|
|
|
let {
|
|
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;
|
|
|
|
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!`)
|
|
}
|
|
|
|
async function ondblclick(evt){
|
|
const select = evt.target;
|
|
const idx = select.value;
|
|
candidate = candidates[idx];
|
|
candidates = [];
|
|
selected = null;
|
|
onSelect(candidate);
|
|
}
|
|
|
|
async function fetchCandidates(){
|
|
candidates = await getCandidates(candidate.display);
|
|
selected = null;
|
|
if (selected>candidates.length) selected = candidates.length;
|
|
}
|
|
|
|
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;
|
|
return false;
|
|
}
|
|
if (ev.key == 'ArrowUp'){
|
|
ev.preventDefault();
|
|
selected = selected == null ? candidates.length -1 : selected -1;
|
|
if (selected < 0) selected = candidates.length -1;
|
|
return false;
|
|
}
|
|
if (ev.key == 'Enter'|| ev.key == 'Tab'){
|
|
ev.preventDefault();
|
|
if (selected != null && selected < candidates.length) {
|
|
candidate = candidates[selected];
|
|
candidates = [];
|
|
selected = null;
|
|
onSelect(candidate);
|
|
return false;
|
|
}
|
|
if (ev.key == 'Enter') {
|
|
candidates = [];
|
|
selected = null;
|
|
if (onCommit(candidate)) candidate = { display : '' };
|
|
}
|
|
return false;
|
|
}
|
|
if (ev.key == 'Escape'){
|
|
ev.preventDefault();
|
|
candidates = [];
|
|
selected = null;
|
|
return false;
|
|
}
|
|
|
|
candidate = { display : candidate.display };
|
|
if (timer) clearTimeout(timer);
|
|
timer = setTimeout(fetchCandidates,400);
|
|
return false;
|
|
}
|
|
|
|
function select(index){
|
|
candidate = candidates[index];
|
|
selected = null;
|
|
candidates = [];
|
|
onSelect(candidate);
|
|
|
|
}
|
|
</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: 50;
|
|
list-style: none;
|
|
padding: 4px;
|
|
margin: 0;
|
|
}
|
|
.highlight { background: orange; color: black; }
|
|
|
|
</style>
|
|
|
|
<span>
|
|
<input type="text" bind:value={candidate.display} {onkeyup} autofocus={autofocus} {id} />
|
|
{#if candidates && candidates.length > 0}
|
|
<ul>
|
|
{#each candidates as candidate,i}
|
|
<li class="option {selected==i?'highlight':''}" onclick={e => selected = i} ondblclick={e => select(i)}>{candidate.display}</li>
|
|
{/each}
|
|
</ul>
|
|
{/if}
|
|
</span>
|