1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-17 07:34:21 +01:00
Files
frontend/src/components/input/ha-input-multi.ts
Wendelin a12543fe74 Add ha-input-docs (#30315)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-03-25 15:46:28 +01:00

239 lines
7.3 KiB
TypeScript

import { consume, type ContextType } from "@lit/context";
import { mdiDeleteOutline, mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { localizeContext } from "../../data/context";
import { haStyle } from "../../resources/styles";
import "../ha-button";
import "../ha-icon-button";
import "../ha-input-helper-text";
import "../ha-sortable";
import "./ha-input";
import type { HaInput, InputType } from "./ha-input";
/**
* Home Assistant multi-value input component
*
* @element ha-input-multi
* @extends {LitElement}
*
* @summary
* A dynamic list of text inputs that allows adding, removing, and optionally reordering values.
* Useful for managing arrays of strings such as URLs, tags, or other repeated text values.
*
* @attr {boolean} disabled - Disables all inputs and buttons.
* @attr {boolean} sortable - Enables drag-and-drop reordering of items.
* @attr {boolean} item-index - Appends a 1-based index number to each item's label.
* @attr {boolean} update-on-blur - Fires value-changed on blur instead of on input.
*
* @fires value-changed - Fired when the list of values changes. `event.detail.value` contains the new string array.
*/
@customElement("ha-input-multi")
class HaInputMulti extends LitElement {
@property({ attribute: false }) public value?: string[];
@property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@property() public helper?: string;
@property({ attribute: "input-type" }) public inputType?: InputType;
@property({ attribute: "input-suffix" }) public inputSuffix?: string;
@property({ attribute: "input-prefix" }) public inputPrefix?: string;
@property() public autocomplete?: string;
@property({ attribute: "add-label" }) public addLabel?: string;
@property({ attribute: "remove-label" }) public removeLabel?: string;
@property({ attribute: "item-index", type: Boolean })
public itemIndex = false;
@property({ type: Number }) public max?: number;
@property({ type: Boolean }) public sortable = false;
@property({ type: Boolean, attribute: "update-on-blur" })
public updateOnBlur = false;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize?: ContextType<typeof localizeContext>;
protected render() {
return html`
<ha-sortable
handle-selector=".handle"
draggable-selector=".row"
.disabled=${!this.sortable || this.disabled}
@item-moved=${this._itemMoved}
>
<div class="items">
${repeat(
this._items,
(item, index) => `${item}-${index}`,
(item, index) => {
const indexSuffix = `${this.itemIndex ? ` ${index + 1}` : ""}`;
return html`
<div class="layout horizontal center-center row">
<ha-input
.type=${this.inputType}
.autocomplete=${this.autocomplete}
.disabled=${this.disabled}
dialogInitialFocus=${index}
.index=${index}
class="flex-auto"
.label=${`${this.label ? `${this.label}${indexSuffix}` : ""}`}
.value=${item}
?data-last=${index === this._items.length - 1}
@input=${this._editItem}
@change=${this._editItem}
@keydown=${this._keyDown}
>
${this.inputPrefix
? html`<span slot="start">${this.inputPrefix}</span>`
: nothing}
${this.inputSuffix
? html`<span slot="end">${this.inputSuffix}</span>`
: nothing}
</ha-input>
<ha-icon-button
.disabled=${this.disabled}
.index=${index}
slot="navigationIcon"
.label=${this.removeLabel ??
this.localize?.("ui.common.remove") ??
"Remove"}
@click=${this._removeItem}
.path=${mdiDeleteOutline}
></ha-icon-button>
${this.sortable
? html`<ha-svg-icon
class="handle"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>`
: nothing}
</div>
`;
}
)}
</div>
</ha-sortable>
<div class="layout horizontal">
<ha-button
size="small"
appearance="filled"
@click=${this._addItem}
.disabled=${this.disabled ||
(this.max != null && this._items.length >= this.max)}
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.addLabel ??
(this.label
? this.localize?.("ui.components.multi-textfield.add_item", {
item: this.label,
})
: this.localize?.("ui.common.add")) ??
"Add"}
</ha-button>
</div>
${this.helper
? html`<ha-input-helper-text .disabled=${this.disabled}
>${this.helper}</ha-input-helper-text
>`
: nothing}
`;
}
private get _items() {
return this.value ?? [];
}
private async _addItem() {
if (this.max != null && this._items.length >= this.max) {
return;
}
const items = [...this._items, ""];
this._fireChanged(items);
await this.updateComplete;
const field = this.shadowRoot?.querySelector(`ha-input[data-last]`) as
| HaInput
| undefined;
field?.focus();
}
private async _editItem(ev: Event) {
if (this.updateOnBlur && ev.type === "input") {
return;
}
if (!this.updateOnBlur && ev.type === "change") {
return;
}
const index = (ev.target as any).index;
const items = [...this._items];
items[index] = (ev.target as any).value;
this._fireChanged(items);
}
private async _keyDown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.stopPropagation();
this._addItem();
}
}
private _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const items = [...this._items];
const [moved] = items.splice(oldIndex, 1);
items.splice(newIndex, 0, moved);
this._fireChanged(items);
}
private async _removeItem(ev: Event) {
const index = (ev.target as any).index;
const items = [...this._items];
items.splice(index, 1);
this._fireChanged(items);
}
private _fireChanged(value) {
this.value = value;
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.row {
margin-bottom: 8px;
--ha-input-padding-bottom: 0;
}
ha-icon-button {
display: block;
}
.handle {
cursor: grab;
padding: 8px;
margin: -8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-input-multi": HaInputMulti;
}
}