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.
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 回答