2021-12-31 00:46:19 +01:00
|
|
|
<script lang="ts">
|
2022-01-09 21:24:49 +01:00
|
|
|
import { debounce } from "lodash";
|
2022-01-30 16:30:14 +01:00
|
|
|
import { createEventDispatcher } from "svelte";
|
2022-02-07 16:32:49 +01:00
|
|
|
import type { IValue, VALUE_TYPE } from "upend/types";
|
2022-02-20 13:06:01 +01:00
|
|
|
import { fetchAllAttributes } from "../../lib/api";
|
2022-02-06 14:16:56 +01:00
|
|
|
import {
|
|
|
|
baseSearchOnce,
|
|
|
|
createLabelled,
|
|
|
|
getObjects,
|
|
|
|
} from "../../util/search";
|
2022-01-27 19:23:09 +01:00
|
|
|
import UpObject from "../display/UpObject.svelte";
|
|
|
|
import IconButton from "./IconButton.svelte";
|
2021-12-31 00:46:19 +01:00
|
|
|
import Input from "./Input.svelte";
|
2022-01-30 16:30:14 +01:00
|
|
|
const dispatch = createEventDispatcher();
|
2021-12-31 00:46:19 +01:00
|
|
|
|
2022-01-09 21:24:49 +01:00
|
|
|
export let attribute: string | undefined = undefined;
|
|
|
|
export let value: IValue | undefined = undefined;
|
2022-02-07 16:32:49 +01:00
|
|
|
export let type: "attribute" | "value";
|
|
|
|
export let valueTypes: VALUE_TYPE[] | undefined = undefined;
|
2022-01-30 16:30:14 +01:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2021-12-31 00:46:19 +01:00
|
|
|
|
2022-01-09 21:24:49 +01:00
|
|
|
interface SelectorOption {
|
|
|
|
attribute?: string;
|
|
|
|
value?: IValue;
|
2022-02-06 14:16:56 +01:00
|
|
|
labelToCreate?: string;
|
2022-01-09 21:24:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
let options: SelectorOption[] = [];
|
2022-02-07 16:32:49 +01:00
|
|
|
let objects = [];
|
|
|
|
const updateOptions = debounce(async (query: string, doSearch: boolean) => {
|
2022-01-27 21:28:42 +01:00
|
|
|
if (query.length == 0) {
|
|
|
|
options = [];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-12-31 00:46:19 +01:00
|
|
|
switch (type) {
|
2022-01-28 16:46:08 +01:00
|
|
|
case "attribute": {
|
2022-02-20 13:06:01 +01:00
|
|
|
const allAttributes = await fetchAllAttributes();
|
2022-01-09 21:24:49 +01:00
|
|
|
options = allAttributes
|
2022-03-02 22:39:05 +01:00
|
|
|
.map((attr) => attr.name)
|
2022-01-09 21:24:49 +01:00
|
|
|
.filter((attr) => attr.toLowerCase().includes(query.toLowerCase()))
|
|
|
|
.map((attribute) => {
|
|
|
|
return {
|
|
|
|
attribute,
|
|
|
|
};
|
|
|
|
});
|
2021-12-31 00:46:19 +01:00
|
|
|
break;
|
2022-01-28 16:46:08 +01:00
|
|
|
}
|
2022-02-07 16:32:49 +01:00
|
|
|
case "value": {
|
2022-01-30 16:30:14 +01:00
|
|
|
options = [];
|
|
|
|
|
2022-02-07 16:32:49 +01:00
|
|
|
if (valueTypes === undefined || valueTypes.includes("Number")) {
|
2022-02-02 17:58:30 +01:00
|
|
|
const number = parseFloat(query);
|
|
|
|
if (!Number.isNaN(number)) {
|
|
|
|
options.push({
|
|
|
|
value: {
|
|
|
|
t: "Number",
|
|
|
|
c: number,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2022-02-07 16:32:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (valueTypes === undefined || valueTypes.includes("String")) {
|
2022-01-30 16:30:14 +01:00
|
|
|
options.push({
|
2022-01-27 21:28:42 +01:00
|
|
|
value: {
|
2022-01-28 20:51:34 +01:00
|
|
|
t: "String",
|
2022-01-27 21:28:42 +01:00
|
|
|
c: query,
|
|
|
|
},
|
2022-01-30 16:30:14 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-02-07 16:32:49 +01:00
|
|
|
if (valueTypes === undefined || valueTypes.includes("Address")) {
|
|
|
|
if (doSearch) {
|
|
|
|
const result = await baseSearchOnce(query);
|
|
|
|
objects = await getObjects(result.entries);
|
|
|
|
}
|
2022-02-06 14:16:56 +01:00
|
|
|
|
2022-02-07 16:32:49 +01:00
|
|
|
let exactHits = Object.entries(addressToLabels)
|
|
|
|
.filter(([_, labels]) =>
|
|
|
|
labels
|
|
|
|
.map((l) => l.toLowerCase())
|
|
|
|
.includes(inputValue.toLowerCase())
|
|
|
|
)
|
|
|
|
.map(([addr, _]) => addr);
|
|
|
|
|
|
|
|
if (exactHits.length) {
|
|
|
|
exactHits.forEach((addr) =>
|
|
|
|
options.push({
|
2022-02-06 14:16:56 +01:00
|
|
|
value: {
|
|
|
|
t: "Address",
|
2022-02-07 16:32:49 +01:00
|
|
|
c: addr,
|
2022-02-06 14:16:56 +01:00
|
|
|
},
|
2022-02-07 16:32:49 +01:00
|
|
|
})
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
options.push({
|
|
|
|
labelToCreate: inputValue,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
options.push(
|
|
|
|
...objects
|
|
|
|
.filter(([addr, _]) => !exactHits.includes(addr))
|
|
|
|
.slice(0, 25)
|
|
|
|
.map(([address, _]) => {
|
|
|
|
return {
|
|
|
|
value: {
|
|
|
|
t: "Address",
|
|
|
|
c: address,
|
|
|
|
},
|
|
|
|
} as SelectorOption;
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
2022-01-30 16:30:14 +01:00
|
|
|
|
|
|
|
options = options;
|
2022-01-09 21:24:49 +01:00
|
|
|
break;
|
2022-01-28 16:46:08 +01:00
|
|
|
}
|
2022-01-09 21:24:49 +01:00
|
|
|
}
|
|
|
|
}, 200);
|
2022-02-07 16:32:49 +01:00
|
|
|
|
2022-02-06 14:16:56 +01:00
|
|
|
$: {
|
2022-02-06 14:44:07 +01:00
|
|
|
if (inputFocused) {
|
2022-02-07 16:32:49 +01:00
|
|
|
updateOptions(inputValue, true);
|
2022-02-06 14:44:07 +01:00
|
|
|
addressToLabels = {};
|
|
|
|
}
|
2022-02-06 14:16:56 +01:00
|
|
|
}
|
2022-01-09 21:24:49 +01:00
|
|
|
|
2022-02-06 14:16:56 +01:00
|
|
|
let addressToLabels: { [key: string]: string[] } = {};
|
|
|
|
function onAddressResolved(address: string, ev: CustomEvent<string[]>) {
|
|
|
|
addressToLabels[address] = ev.detail;
|
2022-02-07 16:32:49 +01:00
|
|
|
updateOptions(inputValue, false);
|
2022-02-06 14:16:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function set(option: SelectorOption) {
|
2022-01-27 19:23:09 +01:00
|
|
|
switch (type) {
|
|
|
|
case "attribute":
|
|
|
|
attribute = option.attribute;
|
|
|
|
inputValue = option.attribute;
|
|
|
|
break;
|
|
|
|
case "value":
|
2022-02-06 14:16:56 +01:00
|
|
|
if (!option.labelToCreate) {
|
|
|
|
value = option.value;
|
|
|
|
inputValue = String(option.value.c);
|
|
|
|
} else {
|
|
|
|
const addr = await createLabelled(option.labelToCreate);
|
|
|
|
value = {
|
|
|
|
t: "Address",
|
|
|
|
c: addr,
|
|
|
|
};
|
|
|
|
inputValue = addr;
|
|
|
|
}
|
2022-01-27 19:23:09 +01:00
|
|
|
break;
|
2022-01-09 21:24:49 +01:00
|
|
|
}
|
2022-01-30 16:30:14 +01:00
|
|
|
dispatch("input", value);
|
2022-01-09 21:24:49 +01:00
|
|
|
visible = false;
|
|
|
|
}
|
|
|
|
|
2021-12-31 00:46:19 +01:00
|
|
|
let inputFocused = false;
|
|
|
|
let hover = false;
|
|
|
|
$: visible = (inputFocused || hover) && Boolean(options.length);
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<div class="selector">
|
2022-01-27 19:23:09 +01:00
|
|
|
{#if value?.t == "Address" && inputValue}
|
|
|
|
<div class="input">
|
|
|
|
<div class="label">
|
2022-02-06 14:41:04 +01:00
|
|
|
<UpObject link address={String(value.c)} />
|
2022-01-27 19:23:09 +01:00
|
|
|
</div>
|
2022-02-20 19:11:34 +01:00
|
|
|
<IconButton name="x" on:click={() => (inputValue = "")} />
|
2022-01-27 19:23:09 +01:00
|
|
|
</div>
|
|
|
|
{:else}
|
|
|
|
<Input
|
|
|
|
bind:value={inputValue}
|
|
|
|
on:input={onInput}
|
|
|
|
on:focusChange={(ev) => (inputFocused = ev.detail)}
|
2022-01-30 16:30:14 +01:00
|
|
|
{placeholder}
|
2022-01-27 19:23:09 +01:00
|
|
|
/>
|
|
|
|
{/if}
|
2021-12-31 00:46:19 +01:00
|
|
|
<ul
|
|
|
|
class="options"
|
|
|
|
class:visible
|
|
|
|
on:mouseenter={() => (hover = true)}
|
|
|
|
on:mouseleave={() => (hover = false)}
|
|
|
|
>
|
|
|
|
{#each options as option}
|
2022-01-09 21:24:49 +01:00
|
|
|
<li on:click={() => set(option)}>
|
|
|
|
{#if option.attribute}
|
|
|
|
{option.attribute}
|
|
|
|
{:else if option.value}
|
2022-01-27 21:28:42 +01:00
|
|
|
{#if option.value.t == "Address"}
|
2022-02-06 14:16:56 +01:00
|
|
|
<UpObject
|
|
|
|
address={String(option.value.c)}
|
|
|
|
on:resolved={(ev) =>
|
|
|
|
onAddressResolved(String(option.value.c), ev)}
|
|
|
|
/>
|
2022-01-27 21:28:42 +01:00
|
|
|
{:else}
|
2022-02-02 17:58:30 +01:00
|
|
|
<div class="type">{option.value.t}</div>
|
|
|
|
<div class="content">{option.value.c}</div>
|
2022-01-27 21:28:42 +01:00
|
|
|
{/if}
|
2022-02-06 14:16:56 +01:00
|
|
|
{:else if option.labelToCreate}
|
|
|
|
<div class="type">Create object</div>
|
|
|
|
<div class="content">{option.labelToCreate}</div>
|
2022-01-09 21:24:49 +01:00
|
|
|
{/if}
|
2021-12-31 00:46:19 +01:00
|
|
|
</li>
|
|
|
|
{/each}
|
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
.selector {
|
|
|
|
position: relative;
|
|
|
|
}
|
|
|
|
|
2022-01-27 19:23:09 +01:00
|
|
|
.input {
|
|
|
|
display: flex;
|
|
|
|
min-width: 0;
|
|
|
|
.label {
|
|
|
|
flex: 1;
|
|
|
|
min-width: 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-31 00:46:19 +01:00
|
|
|
.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);
|
|
|
|
}
|
2022-02-02 17:58:30 +01:00
|
|
|
|
|
|
|
.type,
|
|
|
|
.content {
|
|
|
|
display: inline-block;
|
|
|
|
}
|
|
|
|
|
|
|
|
.type {
|
|
|
|
opacity: 0.8;
|
|
|
|
font-size: smaller;
|
|
|
|
}
|
2021-12-31 00:46:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|