Daxia Blog
Uncategorized | Rust | WebUI | FHIR | Javascript | KB

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}>

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)}> &#215;</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>

About Daxia
我是一名独立开发者,国家工信部认证高级系统架构设计师,在健康信息化领域与许多组织合作。具备大型卫生信息化平台产品架构、设计和开发的能力,从事软件研发、服务咨询、解决方案、行业标准编著相关工作。
我对健康信息化非常感兴趣,尤其是与HL7和FHIR标准的健康互操作性。我是HL7中国委员会成员,从事FHIR培训讲师和FHIR测评现场指导。
我还是FHIR Chi的作者,这是一款用于FHIR测评的工具。