180 lines
5.8 KiB
Svelte
180 lines
5.8 KiB
Svelte
<script context="module">import { fly, scale } from "svelte/transition";
|
|
import { prefersReducedMotionStore } from "../../index.js";
|
|
import { dynamicTransition } from "../../internal/transitions.js";
|
|
</script>
|
|
|
|
<script
|
|
|
|
generics="ListTransitionIn extends Transition = FlyTransition, ListTransitionOut extends Transition = FlyTransition, ChipTransitionIn extends Transition = ScaleTransition, ChipTransitionOut extends Transition = ScaleTransition"
|
|
>import { createEventDispatcher, onMount } from "svelte";
|
|
import { flip } from "svelte/animate";
|
|
const dispatch = createEventDispatcher();
|
|
export let input = "";
|
|
export let name;
|
|
export let value = [];
|
|
export let whitelist = [];
|
|
export let max = -1;
|
|
export let minlength = -1;
|
|
export let maxlength = -1;
|
|
export let allowUpperCase = false;
|
|
export let allowDuplicates = false;
|
|
export let validation = () => true;
|
|
export let duration = 150;
|
|
export let required = false;
|
|
export let chips = "variant-filled";
|
|
export let invalid = "input-error";
|
|
export let padding = "p-2";
|
|
export let rounded = "rounded-container-token";
|
|
export let regionChipWrapper = "";
|
|
export let regionChipList = "";
|
|
export let regionInput = "";
|
|
export let transitions = !$prefersReducedMotionStore;
|
|
export let listTransitionIn = fly;
|
|
export let listTransitionInParams = { duration: 150, opacity: 0, y: -20 };
|
|
export let listTransitionOut = fly;
|
|
export let listTransitionOutParams = { duration: 150, opacity: 0, y: -20 };
|
|
export let chipTransitionIn = scale;
|
|
export let chipTransitionInParams = { duration: 150, opacity: 0 };
|
|
export let chipTransitionOut = scale;
|
|
export let chipTransitionOutParams = { duration: 150, opacity: 0 };
|
|
const cBase = "textarea cursor-pointer";
|
|
const cChipWrapper = "space-y-4";
|
|
const cChipList = "flex flex-wrap gap-2";
|
|
const cInputField = "unstyled bg-transparent border-0 !ring-0 p-0 w-full";
|
|
let inputValid = true;
|
|
let chipValues = value?.map((val) => {
|
|
return { val, id: Math.random() };
|
|
}) || [];
|
|
function resetFormHandler() {
|
|
value = [];
|
|
}
|
|
let selectElement;
|
|
onMount(() => {
|
|
if (!selectElement.form)
|
|
return;
|
|
const externalForm = selectElement.form;
|
|
externalForm.addEventListener("reset", resetFormHandler);
|
|
return () => {
|
|
externalForm.removeEventListener("reset", resetFormHandler);
|
|
};
|
|
});
|
|
function onInputHandler() {
|
|
inputValid = true;
|
|
}
|
|
function validate() {
|
|
if (!input)
|
|
return false;
|
|
input = input.trim();
|
|
if (validation !== void 0 && !validation(input))
|
|
return false;
|
|
if (max !== -1 && value.length >= max)
|
|
return false;
|
|
if (minlength !== -1 && input.length < minlength)
|
|
return false;
|
|
if (maxlength !== -1 && input.length > maxlength)
|
|
return false;
|
|
if (whitelist.length > 0 && !whitelist.includes(input))
|
|
return false;
|
|
if (allowDuplicates === false && value.includes(input))
|
|
return false;
|
|
return true;
|
|
}
|
|
function addChip(event) {
|
|
event.preventDefault();
|
|
inputValid = validate();
|
|
if (inputValid === false) {
|
|
dispatch("invalid", { event, input });
|
|
return;
|
|
}
|
|
input = allowUpperCase ? input : input.toLowerCase();
|
|
value.push(input);
|
|
value = value;
|
|
chipValues.push({ val: input, id: Math.random() });
|
|
chipValues = chipValues;
|
|
dispatch("add", { event, chipIndex: value.length - 1, chipValue: input });
|
|
input = "";
|
|
}
|
|
function removeChip(event, chipIndex, chipValue) {
|
|
if ($$restProps.disabled)
|
|
return;
|
|
value.splice(chipIndex, 1);
|
|
value = value;
|
|
chipValues.splice(chipIndex, 1);
|
|
chipValues = chipValues;
|
|
dispatch("remove", { event, chipIndex, chipValue });
|
|
}
|
|
$:
|
|
classesInvalid = inputValid === false ? invalid : "";
|
|
$:
|
|
classesBase = `${cBase} ${padding} ${rounded} ${$$props.class ?? ""} ${classesInvalid}`;
|
|
$:
|
|
classesChipWrapper = `${cChipWrapper} ${regionChipWrapper}`;
|
|
$:
|
|
classesChipList = `${cChipList} ${regionChipList}`;
|
|
$:
|
|
classesInput = `${cInputField} ${regionInput}`;
|
|
$:
|
|
chipValues = value?.map((val, i) => {
|
|
if (chipValues[i]?.val === val)
|
|
return chipValues[i];
|
|
return { id: Math.random(), val };
|
|
}) || [];
|
|
</script>
|
|
|
|
<div class="input-chip {classesBase}" class:opacity-50={$$restProps.disabled}>
|
|
<!-- NOTE: Don't use `hidden` as it prevents `required` from operating -->
|
|
<div class="h-0 overflow-hidden">
|
|
<select bind:this={selectElement} bind:value {name} multiple {required} tabindex="-1">
|
|
<!-- NOTE: options are required! -->
|
|
{#each value as option}<option value={option}>{option}</option>{/each}
|
|
</select>
|
|
</div>
|
|
<!-- Chip Wrapper -->
|
|
<div class="input-chip-wrapper {classesChipWrapper}">
|
|
<!-- Input Field -->
|
|
<form on:submit={addChip}>
|
|
<input
|
|
type="text"
|
|
bind:value={input}
|
|
placeholder={$$restProps.placeholder ?? 'Enter values...'}
|
|
class="input-chip-field {classesInput}"
|
|
on:input={onInputHandler}
|
|
on:input
|
|
on:focus
|
|
on:blur
|
|
disabled={$$restProps.disabled}
|
|
/>
|
|
</form>
|
|
<!-- Chip List -->
|
|
{#if chipValues.length}
|
|
<div
|
|
class="input-chip-list {classesChipList}"
|
|
in:dynamicTransition|local={{ transition: listTransitionIn, params: listTransitionInParams, enabled: transitions }}
|
|
out:dynamicTransition|local={{ transition: listTransitionOut, params: listTransitionOutParams, enabled: transitions }}
|
|
>
|
|
{#each chipValues as { id, val }, i (id)}
|
|
<!-- Wrapping div required for FLIP animation -->
|
|
<div animate:flip={{ duration }}>
|
|
<button
|
|
type="button"
|
|
class="chip {chips}"
|
|
on:click={(e) => {
|
|
removeChip(e, i, val);
|
|
}}
|
|
on:click
|
|
on:keypress
|
|
on:keydown
|
|
on:keyup
|
|
in:dynamicTransition|local={{ transition: chipTransitionIn, params: chipTransitionInParams, enabled: transitions }}
|
|
out:dynamicTransition|local={{ transition: chipTransitionOut, params: chipTransitionOutParams, enabled: transitions }}
|
|
>
|
|
<span>{val}</span>
|
|
<span>✕</span>
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|