1

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

4

1 回答 1

2

我在 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;
}
于 2022-02-22T18:23:26.583 回答