148 lines
5.0 KiB
TypeScript
148 lines
5.0 KiB
TypeScript
import { Fragment, VNode, h } from "preact";
|
|
import { Choice } from "./InputChoice.js";
|
|
import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
|
|
import { useField } from "./useField.js";
|
|
import { useState } from "preact/hooks";
|
|
|
|
export function InputSelectMultiple(
|
|
props: {
|
|
choices: Choice[];
|
|
unique?: boolean;
|
|
} & UIFormProps<Array<string>>,
|
|
): VNode {
|
|
const { name, label, choices, placeholder, tooltip, required, unique } =
|
|
props;
|
|
const { value, onChange } = useField<{ [s: string]: Array<string> }>(name);
|
|
|
|
const [filter, setFilter] = useState<string | undefined>(undefined);
|
|
const regex = new RegExp(`.*${filter}.*`, "i");
|
|
const choiceMap = choices.reduce((prev, curr) => {
|
|
return { ...prev, [curr.value]: curr.label };
|
|
}, {} as Record<string, string>);
|
|
|
|
const list = value ?? [];
|
|
const filteredChoices =
|
|
filter === undefined
|
|
? undefined
|
|
: choices.filter((v) => {
|
|
return regex.test(v.label);
|
|
});
|
|
return (
|
|
<div class="sm:col-span-6">
|
|
<LabelWithTooltipMaybeRequired
|
|
label={label}
|
|
required={required}
|
|
tooltip={tooltip}
|
|
/>
|
|
{list.map((v, idx) => {
|
|
return (
|
|
<span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 text-xs font-medium text-gray-600">
|
|
{choiceMap[v]}
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
const newValue = [...list];
|
|
newValue.splice(idx, 1);
|
|
onChange(newValue);
|
|
setFilter(undefined);
|
|
}}
|
|
class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
|
|
>
|
|
<span class="sr-only">Remove</span>
|
|
<svg
|
|
viewBox="0 0 14 14"
|
|
class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75"
|
|
>
|
|
<path d="M4 4l6 6m0-6l-6 6" />
|
|
</svg>
|
|
<span class="absolute -inset-1"></span>
|
|
</button>
|
|
</span>
|
|
);
|
|
})}
|
|
|
|
<div class="relative mt-2">
|
|
<input
|
|
id="combobox"
|
|
type="text"
|
|
value={filter ?? ""}
|
|
onChange={(e) => {
|
|
setFilter(e.currentTarget.value);
|
|
}}
|
|
placeholder={placeholder}
|
|
class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
role="combobox"
|
|
aria-controls="options"
|
|
aria-expanded="false"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setFilter(filter === undefined ? "" : undefined);
|
|
}}
|
|
class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
|
|
>
|
|
<svg
|
|
class="h-5 w-5 text-gray-400"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z"
|
|
clip-rule="evenodd"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
{filteredChoices !== undefined && (
|
|
<ul
|
|
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
|
id="options"
|
|
role="listbox"
|
|
>
|
|
{filteredChoices.map((v, idx) => {
|
|
return (
|
|
<li
|
|
class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600"
|
|
id="option-0"
|
|
role="option"
|
|
onClick={() => {
|
|
setFilter(undefined);
|
|
if (unique && list.indexOf(v.value) !== -1) {
|
|
return;
|
|
}
|
|
const newValue = [...list];
|
|
newValue.splice(0, 0, v.value);
|
|
onChange(newValue);
|
|
}}
|
|
|
|
// tabindex="-1"
|
|
>
|
|
{/* <!-- Selected: "font-semibold" --> */}
|
|
<span class="block truncate">{v.label}</span>
|
|
|
|
{/* <!--
|
|
Checkmark, only display for selected option.
|
|
|
|
Active: "text-white", Not Active: "text-indigo-600"
|
|
--> */}
|
|
</li>
|
|
);
|
|
})}
|
|
|
|
{/* <!--
|
|
Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation.
|
|
|
|
Active: "text-white bg-indigo-600", Not Active: "text-gray-900"
|
|
--> */}
|
|
|
|
{/* <!-- More items... --> */}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|