Vaadin ComboBox Multiselect 用于管理标签

发布于 2025-01-09 02:28:27 字数 250 浏览 1 评论 0原文

是否有 Vaadin 组件或附加组件提供具有多选功能的 ComboBox,其工作方式与大多数标记系统类似? (见图)很像 Stackoverflow 的标签。 如果您也可以通过这种方式添加新标签,那就完美了。

输入图片此处描述

Is there a Vaadin component or add-on that provides a ComboBox with multiselect, that works like most tagging systems work? (see picture) Pretty much like Stackoverflow's tagging.
It would be perfect if you could also add new tags this way.

enter image description here

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

三生殊途 2025-01-16 02:28:27

我在 Typescript 中为 Vaadin 22.0.5 制作了这样一个组件。如果您需要 Flow 组件,您可以在它周围添加一个小的 Java 包装器。

输入图片此处描述

token-field.ts 的内容

import "@vaadin/combo-box";
import "@vaadin/icons";
import "@vaadin/custom-field";
import {customElement, state, property, query} from "lit/decorators";
import {Layout} from "Frontend/views/view";
import {css, html, PropertyValues} from "lit";
import {repeat} from "lit/directives/repeat";
import {ComboBox} from "@vaadin/combo-box";
import styles from "./token-field.css";
import {registerStyles} from "@vaadin/vaadin-themable-mixin/register-styles";

@customElement('token-field')
export class TokenField extends Layout {
    private readonly focusEntered = (e: FocusEvent) => {
        const tokenSelection = this.shadowRoot?.querySelector('vaadin-combo-box') as ComboBox<string> | null | undefined;
        tokenSelection?.focus();
    }

    @property({type: Boolean, reflect: true}) required: boolean = false;
    @property({type: Boolean, reflect: true}) invalid: boolean = false;
    @property({type: Boolean, reflect: true}) unique: boolean = false;
    @property({type: String, reflect: true}) label: string = '';
    @property({type: String, reflect: true, attribute: 'helper-text'}) helperText: string = '';
    @property({type: String, reflect: true, attribute: 'error-message'}) errorMessage: string = '';
    @property({type: Array}) knownTokens: Array<string> = ["IT Sicherheit", "Sicherheit", "Umwelt"];
    @property({type: Array}) tokens: Array<string> = ["IT Sicherheit"];
    @state() private filteredTokens: Array<string> = [];
    @query('vaadin-combo-box') private tokenSelectionComboBox!: ComboBox<string>;

    static get styles() {
        return [styles];
    }

    protected render(): unknown {
        return html`
            <vaadin-custom-field label=${this.label} helper-text=${this.helperText} error-message="${this.errorMessage}" ?required=${this.required} ?invalid=${this.invalid}>
                <div class="input">
                    ${repeat(this.tokens, (token, index) => html`
                        <span class="badge" theme="badge pill">
                            <span>${token}</span>
                            <vaadin-button theme="contrast tertiary-inline" title="Remove token: ${token}" @click="${() => this.tokenRemoveClicked(index)}">
                                <vaadin-icon icon="vaadin:close-small"></vaadin-icon>
                            </vaadin-button>
                        </span>
                    `)}
                    <vaadin-combo-box .items="${this.filteredTokens}" allow-custom-value @change=${this.tokenSelectionChanged} @custom-value-set=${this.tokenSelectionCustomValueSet} theme="small transparent"></vaadin-combo-box>
                </div>
            </vaadin-custom-field>
        `;
    }

    connectedCallback() {
        super.connectedCallback();
        this.addEventListener('focus', this.focusEntered);
    }

    disconnectedCallback() {
        super.disconnectedCallback();
        this.removeEventListener('focus', this.focusEntered);
    }

    protected firstUpdated(_changedProperties: PropertyValues): void {
        super.firstUpdated(_changedProperties);
        this.updateFilteredTokens();
    }

    private tokenRemoveClicked(index: number): void {
        this.tokens.splice(index, 1);
        this.requestUpdate('tokens');
        this.updateFilteredTokens();
    }

    private tokenSelectionChanged(event: Event): void {
        const tokenSelection = event.currentTarget as ComboBox<string>;
        const newToken = tokenSelection.value.trim();

        if (this.unique) {
            const index = this.tokens.indexOf(newToken);

            if (index >= 0) {
                this.tokens.splice(index, 1);
            }
        }

        this.tokens.push(newToken);
        this.updateFilteredTokens();

        tokenSelection.value = '';
    }

    private tokenSelectionCustomValueSet(event: CustomEvent<string>): void {
        const newToken = event.detail.trim();

        if (this.knownTokens.indexOf(newToken) < 0) {
            this.knownTokens.push(newToken);
        }
    }

    private updateFilteredTokens(): void {
        this.filteredTokens = this.knownTokens.filter(token => this.tokens.indexOf(token) < 0);
    }
}

registerStyles(
    'vaadin-combo-box',
    css`
        :host([theme~='transparent']) [part='input-field'] {
            background-color: transparent;
        }

        :host([theme~='transparent'][focus-ring]) [part='input-field'] {
            box-shadow: initial;
        }
        

        :host(:hover[theme~='transparent']:not([readonly]):not([focused])) [part='input-field']::after {
            opacity: 0;
        }
  `,
    { moduleId: 'token-custom-field-styles' }
);

token-field.css 的内容

.input {
    min-height: var(--lumo-text-field-size, var(--lumo-size-m));
    background-color: green;
    border-radius: var(--lumo-border-radius-m);
    background-color: var(--lumo-contrast-10pct);
    padding: 0 0 0 3px;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0 var(--lumo-space-xs);
    position: relative;
}

.input::after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    border-radius: inherit;
    pointer-events: none;
    background-color: var(--lumo-contrast-50pct);
    opacity: 0;
    transition: transform 0.15s, opacity 0.2s;
    transform-origin: 100% 0;
}

.badge {
    margin-top: 4px;
    margin-bottom: 4px;
}

.badge vaadin-button {
    margin-inline-start: var(--lumo-space-xs);
}

vaadin-custom-field {
    width: inherit;
}

vaadin-custom-field[focus-ring] .input {
    box-shadow: 0 0 0 2px var(--lumo-primary-color-50pct);
}

vaadin-custom-field[invalid] .input {
    background-color: var(--lumo-error-color-10pct);
}

vaadin-custom-field[invalid] .input::after {
    background-color: var(--lumo-error-color-50pct);
}

vaadin-custom-field[invalid][focus-ring] .input {
    box-shadow: 0 0 0 2px var(--lumo-error-color-50pct);
}

vaadin-custom-field:hover:not([readonly]):not([focused]) .input::after {
    opacity: 0.1;
}

vaadin-combo-box {
    padding-top: 0;
    padding-bottom: 0;
    margin-top: 3px;
    margin-bottom: 3px;
    flex: 1 1 auto;
}

I made such a component for Vaadin 22.0.5, in Typescript. If you need a Flow component you may add a small Java wrapper around it.

enter image description here

Content of token-field.ts

import "@vaadin/combo-box";
import "@vaadin/icons";
import "@vaadin/custom-field";
import {customElement, state, property, query} from "lit/decorators";
import {Layout} from "Frontend/views/view";
import {css, html, PropertyValues} from "lit";
import {repeat} from "lit/directives/repeat";
import {ComboBox} from "@vaadin/combo-box";
import styles from "./token-field.css";
import {registerStyles} from "@vaadin/vaadin-themable-mixin/register-styles";

@customElement('token-field')
export class TokenField extends Layout {
    private readonly focusEntered = (e: FocusEvent) => {
        const tokenSelection = this.shadowRoot?.querySelector('vaadin-combo-box') as ComboBox<string> | null | undefined;
        tokenSelection?.focus();
    }

    @property({type: Boolean, reflect: true}) required: boolean = false;
    @property({type: Boolean, reflect: true}) invalid: boolean = false;
    @property({type: Boolean, reflect: true}) unique: boolean = false;
    @property({type: String, reflect: true}) label: string = '';
    @property({type: String, reflect: true, attribute: 'helper-text'}) helperText: string = '';
    @property({type: String, reflect: true, attribute: 'error-message'}) errorMessage: string = '';
    @property({type: Array}) knownTokens: Array<string> = ["IT Sicherheit", "Sicherheit", "Umwelt"];
    @property({type: Array}) tokens: Array<string> = ["IT Sicherheit"];
    @state() private filteredTokens: Array<string> = [];
    @query('vaadin-combo-box') private tokenSelectionComboBox!: ComboBox<string>;

    static get styles() {
        return [styles];
    }

    protected render(): unknown {
        return html`
            <vaadin-custom-field label=${this.label} helper-text=${this.helperText} error-message="${this.errorMessage}" ?required=${this.required} ?invalid=${this.invalid}>
                <div class="input">
                    ${repeat(this.tokens, (token, index) => html`
                        <span class="badge" theme="badge pill">
                            <span>${token}</span>
                            <vaadin-button theme="contrast tertiary-inline" title="Remove token: ${token}" @click="${() => this.tokenRemoveClicked(index)}">
                                <vaadin-icon icon="vaadin:close-small"></vaadin-icon>
                            </vaadin-button>
                        </span>
                    `)}
                    <vaadin-combo-box .items="${this.filteredTokens}" allow-custom-value @change=${this.tokenSelectionChanged} @custom-value-set=${this.tokenSelectionCustomValueSet} theme="small transparent"></vaadin-combo-box>
                </div>
            </vaadin-custom-field>
        `;
    }

    connectedCallback() {
        super.connectedCallback();
        this.addEventListener('focus', this.focusEntered);
    }

    disconnectedCallback() {
        super.disconnectedCallback();
        this.removeEventListener('focus', this.focusEntered);
    }

    protected firstUpdated(_changedProperties: PropertyValues): void {
        super.firstUpdated(_changedProperties);
        this.updateFilteredTokens();
    }

    private tokenRemoveClicked(index: number): void {
        this.tokens.splice(index, 1);
        this.requestUpdate('tokens');
        this.updateFilteredTokens();
    }

    private tokenSelectionChanged(event: Event): void {
        const tokenSelection = event.currentTarget as ComboBox<string>;
        const newToken = tokenSelection.value.trim();

        if (this.unique) {
            const index = this.tokens.indexOf(newToken);

            if (index >= 0) {
                this.tokens.splice(index, 1);
            }
        }

        this.tokens.push(newToken);
        this.updateFilteredTokens();

        tokenSelection.value = '';
    }

    private tokenSelectionCustomValueSet(event: CustomEvent<string>): void {
        const newToken = event.detail.trim();

        if (this.knownTokens.indexOf(newToken) < 0) {
            this.knownTokens.push(newToken);
        }
    }

    private updateFilteredTokens(): void {
        this.filteredTokens = this.knownTokens.filter(token => this.tokens.indexOf(token) < 0);
    }
}

registerStyles(
    'vaadin-combo-box',
    css`
        :host([theme~='transparent']) [part='input-field'] {
            background-color: transparent;
        }

        :host([theme~='transparent'][focus-ring]) [part='input-field'] {
            box-shadow: initial;
        }
        

        :host(:hover[theme~='transparent']:not([readonly]):not([focused])) [part='input-field']::after {
            opacity: 0;
        }
  `,
    { moduleId: 'token-custom-field-styles' }
);

Content of token-field.css

.input {
    min-height: var(--lumo-text-field-size, var(--lumo-size-m));
    background-color: green;
    border-radius: var(--lumo-border-radius-m);
    background-color: var(--lumo-contrast-10pct);
    padding: 0 0 0 3px;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0 var(--lumo-space-xs);
    position: relative;
}

.input::after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    border-radius: inherit;
    pointer-events: none;
    background-color: var(--lumo-contrast-50pct);
    opacity: 0;
    transition: transform 0.15s, opacity 0.2s;
    transform-origin: 100% 0;
}

.badge {
    margin-top: 4px;
    margin-bottom: 4px;
}

.badge vaadin-button {
    margin-inline-start: var(--lumo-space-xs);
}

vaadin-custom-field {
    width: inherit;
}

vaadin-custom-field[focus-ring] .input {
    box-shadow: 0 0 0 2px var(--lumo-primary-color-50pct);
}

vaadin-custom-field[invalid] .input {
    background-color: var(--lumo-error-color-10pct);
}

vaadin-custom-field[invalid] .input::after {
    background-color: var(--lumo-error-color-50pct);
}

vaadin-custom-field[invalid][focus-ring] .input {
    box-shadow: 0 0 0 2px var(--lumo-error-color-50pct);
}

vaadin-custom-field:hover:not([readonly]):not([focused]) .input::after {
    opacity: 0.1;
}

vaadin-combo-box {
    padding-top: 0;
    padding-bottom: 0;
    margin-top: 3px;
    margin-bottom: 3px;
    flex: 1 1 auto;
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文