upend/webui/src/components/utils/Selector.svelte

202 lines
4.5 KiB
Svelte

<script lang="ts">
import { debounce } from "lodash";
import { createEventDispatcher } from "svelte";
import type { IValue } from "upend/types";
import { baseSearchOnce, getObjects } from "../../util/search";
import UpObject from "../display/UpObject.svelte";
import IconButton from "./IconButton.svelte";
import Input from "./Input.svelte";
const dispatch = createEventDispatcher();
export let attribute: string | undefined = undefined;
export let value: IValue | undefined = undefined;
export let type: "attribute" | "value" | "entity";
export let placeholder = "";
let inputValue = "";
if (type == "attribute") {
inputValue = attribute || "";
} else {
inputValue = String(value?.c || "");
}
$: if (value === undefined) inputValue = undefined;
function onInput(ev: CustomEvent<string>) {
if (type == "attribute") {
attribute = ev.detail;
} else {
value = {
t: "String",
c: ev.detail,
};
}
}
interface SelectorOption {
attribute?: string;
value?: IValue;
}
let options: SelectorOption[] = [];
const updateOptions = debounce(async (query: string) => {
if (query.length == 0) {
options = [];
return;
}
switch (type) {
case "attribute": {
const req = await fetch("/api/all/attributes");
const allAttributes: string[] = await req.json();
options = allAttributes
.filter((attr) => attr.toLowerCase().includes(query.toLowerCase()))
.map((attribute) => {
return {
attribute,
};
});
break;
}
case "value":
case "entity": {
const result = await baseSearchOnce(query);
const objects = await getObjects(result.entries);
options = [];
if (type === "value") {
options.push({
value: {
t: "String",
c: query,
},
});
}
options.push(
...objects.slice(0, 25).map(([address, label]) => {
return {
value: {
t: "Address",
c: address,
},
} as SelectorOption;
})
);
options = options;
break;
}
}
}, 200);
$: updateOptions(inputValue);
function set(option: SelectorOption) {
switch (type) {
case "attribute":
attribute = option.attribute;
inputValue = option.attribute;
break;
case "value":
case "entity":
value = option.value;
inputValue = String(option.value.c);
break;
}
dispatch("input", value);
visible = false;
}
let inputFocused = false;
let hover = false;
$: visible = (inputFocused || hover) && Boolean(options.length);
</script>
<div class="selector">
{#if value?.t == "Address" && inputValue}
<div class="input">
<div class="label">
<UpObject address={String(value.c)} />
</div>
<IconButton name="trash" on:click={() => (inputValue = "")} />
</div>
{:else}
<Input
bind:value={inputValue}
on:input={onInput}
on:focusChange={(ev) => (inputFocused = ev.detail)}
{placeholder}
/>
{/if}
<ul
class="options"
class:visible
on:mouseenter={() => (hover = true)}
on:mouseleave={() => (hover = false)}
>
{#each options as option}
<li on:click={() => set(option)}>
{#if option.attribute}
{option.attribute}
{:else if option.value}
{#if option.value.t == "Address"}
<UpObject address={String(option.value.c)} />
{:else}
{option.value.c}
{/if}
{/if}
</li>
{/each}
</ul>
</div>
<style lang="scss">
.selector {
position: relative;
}
.input {
display: flex;
min-width: 0;
.label {
flex: 1;
min-width: 0;
}
}
.options {
position: absolute;
list-style: none;
margin: 0;
padding: 0;
border: 1px solid var(--foreground-lighter);
width: 100%;
border-radius: 4px;
margin-top: 2px;
background: var(--background);
font-size: smaller;
visibility: hidden;
opacity: 0;
transition: opacity 0.2s;
z-index: 99;
&.visible {
visibility: visible;
opacity: 1;
}
li {
cursor: pointer;
padding: 0.5em;
transition: background-color 0.2s;
&:hover {
background-color: var(--background-lighter);
}
}
}
</style>