Svelte组件: Tags
简介
最近写程序时,需要一个多标签的输入框。发现了一个可利用的组件。不过该组件过时了,不支持最新的Svelte5
版本。
我就根据这个组件代码,更新以支持Svelte5
版本。不过呢,做了精简,够用就好。
使用效果
使用起来非常简单。
<script>
import Tags from '$lib/components/Tags.svelte';
let tags: string[] = $state(["Rust", "Python", "Java", "C++"]);
</script>
<Tags bind:tags={tags}>
代码
Steper组件定义在文件Tags.svelte
中,代码如下:
<script lang="ts">
interface TagsProps {
tags: string[];
addKeys?: string[];
removeKeys?: string[];
onlyUnique?: boolean;
placeholder?: string;
splitWith?: string;
allowPaste?: boolean;
readonly?: boolean;
disable?: boolean;
name?: string;
maxTags?: number;
labelText?: string;
labelShow?: boolean;
cleanOnBlur?: boolean;
}
let {tags = $bindable(),
addKeys = ["Enter"], removeKeys = ["Backspace"],
placeholder = "", splitWith = ",",
allowPaste = true, readonly = false, disable = false, onlyUnique = true,
name = "svelte-tags-input",
labelText = name, labelShow = false,
maxTags = 10,
cleanOnBlur = true,
}: TagsProps = $props();
let id = uniqueID();
let tag = $state("");
let storePlaceholder = $derived(placeholder);
let layoutElement: HTMLElement;
let inputElement: HTMLInputElement;
function setTag(e: KeyboardEvent) {
const currentTag = (e.target as HTMLInputElement).value;
if (addKeys) {
addKeys.forEach(function(key) {
if (key === e.code) {
if (currentTag) e.preventDefault();
addTag(currentTag);
}
});
}
if (removeKeys) {
removeKeys.forEach(function(key) {
if (key === e.code && tag === "") {
tags.pop();
placeholder = storePlaceholder;
inputElement.readOnly = false;
inputElement.focus();
}
});
}
if (e.code === "Escape") {
inputElement.focus();
}
}
function addTag(currentTag: string) {
currentTag = currentTag.trim();
if (currentTag == "") return;
if (maxTags && tags.length == maxTags) return;
if (onlyUnique && tags.includes(currentTag)) return;
tags.push(currentTag)
tag = "";
inputElement.focus();
if (maxTags && tags.length == maxTags) {
inputElement.readOnly = true;
placeholder = "";
}
}
function removeTag(i: number) {
tags.splice(i, 1);
// Focus on svelte tags input
placeholder = storePlaceholder;
inputElement.readOnly = false;
inputElement.focus();
}
function onPaste(e: ClipboardEvent) {
if(!allowPaste) return;
e.preventDefault();
const data = getClipboardData(e);
splitTags(data).map(tag => addTag(tag));
}
// 在元素获取焦点时触发的事件
function onFocus() {
layoutElement.classList.add('focus');
}
// 在元素失去焦点时触发的事件
function onBlur() {
layoutElement.classList.remove('focus');
if (cleanOnBlur) tag = "";
}
function getClipboardData(e: ClipboardEvent) {
if (e.clipboardData) {
return e.clipboardData.getData('text/plain')
}
return ''
}
function splitTags(data: string) {
return data.split(splitWith).map(tag => tag.trim());
}
function uniqueID() {
return 'sti_' + Math.random().toString(36).substring(2, 11);
}
</script>
<div class="svelte-tags-input-layout"
class:sti-layout-disable={disable}
class:sti-layout-readonly={readonly}
bind:this={layoutElement}>
<label for={id} class={labelShow ? "" : "sr-only"}>{labelText}</label>
{#if tags.length > 0}
{#each tags as tag, i}
<button type="button" class="svelte-tags-input-tag">
{tag}
{#if !disable && !readonly}
<span class="svelte-tags-input-tag-remove" onpointerdown={() => removeTag(i)}> ×</span>
{/if}
</button>
{/each}
{/if}
<input
class="svelte-tags-input"
type="text"
id={id}
name={name}
bind:this={inputElement}
bind:value={tag}
onkeydown={setTag}
onpaste={onPaste}
onfocus={onFocus}
onblur={onBlur}
placeholder={placeholder}
disabled={disable || readonly}
autocomplete="off"
>
</div>
<style>
/* CSS svelte-tags-input */
.svelte-tags-input,
.svelte-tags-input-tag,
.svelte-tags-input-layout label {
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",sans-serif;
font-size: 14px;
padding: 2px 5px;
}
.svelte-tags-input-layout label {
margin: 4px 5px 0 0;
padding:0;
font-weight:500;
}
/* svelte-tags-input-layout */
.svelte-tags-input-layout {
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-ms-flex-wrap:wrap;
flex-wrap:wrap;
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
padding: 0px 5px 5px 5px;
border: solid 1px #CCC;
background: #FFF;
border-radius: 2px;
}
.svelte-tags-input-layout:focus,
.svelte-tags-input-layout:hover {
border: solid 1px #000;
}
.svelte-tags-input-layout:focus-within {
outline: 5px auto -webkit-focus-ring-color;
}
/* svelte-tags-input */
.svelte-tags-input {
/* Parent handles background */
background: unset;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
margin: 5px 0 0;
border:none;
}
.svelte-tags-input:focus {
outline:0;
}
/* svelte-tags-input-tag */
.svelte-tags-input-tag {
cursor: text;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
white-space: nowrap;
user-select: text;
list-style:none;
background: #000;
border: none;
color: #FFF;
border-radius: 2px;
margin-right: 5px;
margin-top: 5px;
font-weight: 400;
}
.svelte-tags-input-tag-remove {
cursor:pointer;
margin-left: 5px;
}
.svelte-tags-input-matchs li {
list-style:none;
padding:5px;
border-radius: 2px;
cursor:pointer;
}
.svelte-tags-input-matchs li:hover,
.svelte-tags-input-matchs li.focus {
background:#000;
color:#FFF;
outline:none;
}
/* svelte-tags-input disabled */
.svelte-tags-input:disabled {
background: transparent;
}
.svelte-tags-input-layout.sti-layout-disable,
.svelte-tags-input-layout.sti-layout-disable input {
cursor: not-allowed;
background: #EAEAEA;
}
.svelte-tags-input-layout.sti-layout-disable:hover,
.svelte-tags-input-layout.sti-layout-disable:focus,
.svelte-tags-input-layout.sti-layout-readonly:hover,
.svelte-tags-input-layout.sti-layout-readonly:focus {
border-color:#CCC;
}
.svelte-tags-input-layout.sti-layout-disable .svelte-tags-input-tag {
background: #AEAEAE;
}
.svelte-tags-input-layout.sti-layout-disable .svelte-tags-input-tag-remove {
cursor: not-allowed;
}
.svelte-tags-input-layout label.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>