mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-09 10:26:35 +00:00
List Selector (#12099)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
86afd883a5
commit
9444228907
@ -109,7 +109,7 @@ const AREAS = [
|
|||||||
|
|
||||||
const SCHEMAS: {
|
const SCHEMAS: {
|
||||||
name: string;
|
name: string;
|
||||||
input: Record<string, BlueprintInput | null>;
|
input: Record<string, (BlueprintInput & { required?: boolean }) | null>;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
name: "One of each",
|
name: "One of each",
|
||||||
@ -166,7 +166,9 @@ const SCHEMAS: {
|
|||||||
object: { name: "Object", selector: { object: {} } },
|
object: { name: "Object", selector: { object: {} } },
|
||||||
select_radio: {
|
select_radio: {
|
||||||
name: "Select (Radio)",
|
name: "Select (Radio)",
|
||||||
selector: { select: { options: ["Option 1", "Option 2"] } },
|
selector: {
|
||||||
|
select: { options: ["Option 1", "Option 2"], mode: "list" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
name: "Select",
|
name: "Select",
|
||||||
@ -183,6 +185,22 @@ const SCHEMAS: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
select_custom: {
|
||||||
|
name: "Select (Custom)",
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
custom_value: true,
|
||||||
|
options: [
|
||||||
|
"Option 1",
|
||||||
|
"Option 2",
|
||||||
|
"Option 3",
|
||||||
|
"Option 4",
|
||||||
|
"Option 5",
|
||||||
|
"Option 6",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
icon: { name: "Icon", selector: { icon: {} } },
|
icon: { name: "Icon", selector: { icon: {} } },
|
||||||
media: { name: "Media", selector: { media: {} } },
|
media: { name: "Media", selector: { media: {} } },
|
||||||
location: { name: "Location", selector: { location: {} } },
|
location: { name: "Location", selector: { location: {} } },
|
||||||
@ -203,6 +221,34 @@ const SCHEMAS: {
|
|||||||
entity: { name: "Entity", selector: { entity: { multiple: true } } },
|
entity: { name: "Entity", selector: { entity: { multiple: true } } },
|
||||||
device: { name: "Device", selector: { device: { multiple: true } } },
|
device: { name: "Device", selector: { device: { multiple: true } } },
|
||||||
area: { name: "Area", selector: { area: { multiple: true } } },
|
area: { name: "Area", selector: { area: { multiple: true } } },
|
||||||
|
select: {
|
||||||
|
name: "Select Multiple",
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
multiple: true,
|
||||||
|
custom_value: true,
|
||||||
|
options: [
|
||||||
|
"Option 1",
|
||||||
|
"Option 2",
|
||||||
|
"Option 3",
|
||||||
|
"Option 4",
|
||||||
|
"Option 5",
|
||||||
|
"Option 6",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select_checkbox: {
|
||||||
|
name: "Select Multiple (Checkbox)",
|
||||||
|
required: false,
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
mode: "list",
|
||||||
|
multiple: true,
|
||||||
|
options: ["Option 1", "Option 2", "Option 3", "Option 4"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -25,13 +25,7 @@ export class HaChipSet extends LitElement {
|
|||||||
${unsafeCSS(chipStyles)}
|
${unsafeCSS(chipStyles)}
|
||||||
|
|
||||||
slot::slotted(ha-chip) {
|
slot::slotted(ha-chip) {
|
||||||
margin: 4px;
|
margin: 4px 4px 4px 0;
|
||||||
}
|
|
||||||
slot::slotted(ha-chip:first-of-type) {
|
|
||||||
margin-left: -4px;
|
|
||||||
}
|
|
||||||
slot::slotted(ha-chip:last-of-type) {
|
|
||||||
margin-right: -4px;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ import { customElement, property } from "lit/decorators";
|
|||||||
export class HaChip extends LitElement {
|
export class HaChip extends LitElement {
|
||||||
@property({ type: Boolean }) public hasIcon = false;
|
@property({ type: Boolean }) public hasIcon = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public hasTrailingIcon = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public noText = false;
|
@property({ type: Boolean }) public noText = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
@ -30,6 +32,11 @@ export class HaChip extends LitElement {
|
|||||||
<span class="mdc-chip__text"><slot></slot></span>
|
<span class="mdc-chip__text"><slot></slot></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
${this.hasTrailingIcon
|
||||||
|
? html`<div class="mdc-chip__icon mdc-chip__icon--trailing">
|
||||||
|
<slot name="trailing-icon"></slot>
|
||||||
|
</div>`
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -53,14 +60,20 @@ export class HaChip extends LitElement {
|
|||||||
color: var(--ha-chip-text-color, var(--primary-text-color));
|
color: var(--ha-chip-text-color, var(--primary-text-color));
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-chip__icon--leading {
|
.mdc-chip__icon--leading,
|
||||||
--mdc-icon-size: 20px;
|
.mdc-chip__icon--trailing {
|
||||||
|
--mdc-icon-size: 18px;
|
||||||
|
line-height: 14px;
|
||||||
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
||||||
}
|
}
|
||||||
.mdc-chip.no-text
|
.mdc-chip.no-text
|
||||||
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
|
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
|
||||||
margin-right: -4px;
|
margin-right: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span[role="gridcell"] {
|
||||||
|
line-height: 14px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,14 +110,18 @@ export class HaComboBox extends LitElement {
|
|||||||
return this._comboBox.selectedItem;
|
return this._comboBox.selectedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setInputValue(value: string) {
|
||||||
|
this._comboBox.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<vaadin-combo-box-light
|
<vaadin-combo-box-light
|
||||||
.itemValuePath=${this.itemValuePath}
|
.itemValuePath=${this.itemValuePath}
|
||||||
.itemIdPath=${this.itemIdPath}
|
.itemIdPath=${this.itemIdPath}
|
||||||
.itemLabelPath=${this.itemLabelPath}
|
.itemLabelPath=${this.itemLabelPath}
|
||||||
.value=${this.value || ""}
|
|
||||||
.items=${this.items}
|
.items=${this.items}
|
||||||
|
.value=${this.value || ""}
|
||||||
.filteredItems=${this.filteredItems}
|
.filteredItems=${this.filteredItems}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import "@material/mwc-formfield/mwc-formfield";
|
import "@material/mwc-formfield/mwc-formfield";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
import { mdiClose } from "@mdi/js";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import "../ha-checkbox";
|
||||||
|
import "../ha-chip";
|
||||||
|
import "../ha-chip-set";
|
||||||
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
|
import "../ha-formfield";
|
||||||
import "../ha-radio";
|
import "../ha-radio";
|
||||||
import "../ha-select";
|
import "../ha-select";
|
||||||
|
|
||||||
@ -15,7 +21,7 @@ export class HaSelectSelector extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public selector!: SelectSelector;
|
@property({ attribute: false }) public selector!: SelectSelector;
|
||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string | string[];
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@ -25,30 +31,116 @@ export class HaSelectSelector extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public required = true;
|
@property({ type: Boolean }) public required = true;
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) private comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
private _filter = "";
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (this.required && this.selector.select.options!.length < 6) {
|
const options = this.selector.select.options.map((option) =>
|
||||||
|
typeof option === "object" ? option : { value: option, label: option }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.selector.select.custom_value && this._mode === "list") {
|
||||||
|
if (!this.selector.select.multiple || this.required) {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
${this.label}
|
||||||
|
${options.map(
|
||||||
|
(item: SelectOption) => html`
|
||||||
|
<mwc-formfield .label=${item.label}>
|
||||||
|
<ha-radio
|
||||||
|
.checked=${item.value === this.value}
|
||||||
|
.value=${item.value}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
@change=${this._valueChanged}
|
||||||
|
></ha-radio>
|
||||||
|
</mwc-formfield>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
${this.label}
|
${this.label}${options.map(
|
||||||
${this.selector.select.options.map((item: string | SelectOption) => {
|
(item: SelectOption) => html`
|
||||||
const value = typeof item === "object" ? item.value : item;
|
<ha-formfield .label=${item.label}>
|
||||||
const label = typeof item === "object" ? item.label : item;
|
<ha-checkbox
|
||||||
|
.checked=${this.value?.includes(item.value)}
|
||||||
return html`
|
.value=${item.value}
|
||||||
<mwc-formfield .label=${label}>
|
|
||||||
<ha-radio
|
|
||||||
.checked=${value === this.value}
|
|
||||||
.value=${value}
|
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@change=${this._valueChanged}
|
@change=${this._checkboxChanged}
|
||||||
></ha-radio>
|
></ha-checkbox>
|
||||||
</mwc-formfield>
|
</ha-formfield>
|
||||||
`;
|
`
|
||||||
})}
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.selector.select.multiple) {
|
||||||
|
const value =
|
||||||
|
!this.value || this.value === "" ? [] : (this.value as string[]);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-chip-set>
|
||||||
|
${value?.map(
|
||||||
|
(item, idx) =>
|
||||||
|
html`
|
||||||
|
<ha-chip hasTrailingIcon>
|
||||||
|
${options.find((option) => option.value === item)?.label ||
|
||||||
|
item}
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="trailing-icon"
|
||||||
|
.path=${mdiClose}
|
||||||
|
.idx=${idx}
|
||||||
|
@click=${this._removeItem}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-chip>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-chip-set>
|
||||||
|
|
||||||
|
<ha-combo-box
|
||||||
|
item-value-path="value"
|
||||||
|
item-label-path="label"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.label=${this.label}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
.value=${this._filter}
|
||||||
|
.items=${options.filter((item) => !this.value?.includes(item.value))}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
@value-changed=${this._comboBoxValueChanged}
|
||||||
|
></ha-combo-box>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selector.select.custom_value) {
|
||||||
|
if (
|
||||||
|
this.value !== undefined &&
|
||||||
|
!options.find((option) => option.value === this.value)
|
||||||
|
) {
|
||||||
|
options.unshift({ value: this.value, label: this.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-combo-box
|
||||||
|
item-value-path="value"
|
||||||
|
item-label-path="label"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.label=${this.label}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
.items=${options}
|
||||||
|
.value=${this.value}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
@value-changed=${this._comboBoxValueChanged}
|
||||||
|
></ha-combo-box>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-select
|
<ha-select
|
||||||
fixedMenuPosition
|
fixedMenuPosition
|
||||||
@ -57,40 +149,134 @@ export class HaSelectSelector extends LitElement {
|
|||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
|
||||||
@closed=${stopPropagation}
|
@closed=${stopPropagation}
|
||||||
@selected=${this._valueChanged}
|
@selected=${this._valueChanged}
|
||||||
>
|
>
|
||||||
${this.selector.select.options.map((item: string | SelectOption) => {
|
${options.map(
|
||||||
const value = typeof item === "object" ? item.value : item;
|
(item: SelectOption) => html`
|
||||||
const label = typeof item === "object" ? item.label : item;
|
<mwc-list-item .value=${item.value}>${item.label}</mwc-list-item>
|
||||||
|
`
|
||||||
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
|
)}
|
||||||
})}
|
|
||||||
</ha-select>
|
</ha-select>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _mode(): "list" | "dropdown" {
|
||||||
|
return (
|
||||||
|
this.selector.select.mode ||
|
||||||
|
(this.selector.select.options.length < 6 ? "list" : "dropdown")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private _valueChanged(ev) {
|
private _valueChanged(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (this.disabled || !ev.target.value) {
|
const value = ev.detail?.value || ev.target.value;
|
||||||
|
if (this.disabled || !value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: ev.target.value,
|
value: value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
private _checkboxChanged(ev) {
|
||||||
return css`
|
ev.stopPropagation();
|
||||||
ha-select {
|
if (this.disabled) {
|
||||||
width: 100%;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newValue: string[];
|
||||||
|
const value: string = ev.target.value;
|
||||||
|
const checked = ev.target.checked;
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
if (!this.value) {
|
||||||
|
newValue = [value];
|
||||||
|
} else if (this.value.includes(value)) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
newValue = [...this.value, value];
|
||||||
}
|
}
|
||||||
mwc-formfield {
|
} else {
|
||||||
display: block;
|
if (!this.value?.includes(value)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
`;
|
newValue = (this.value as string[]).filter((v) => v !== value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: newValue,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _removeItem(ev) {
|
||||||
|
const value: string[] = [...(this.value! as string[])];
|
||||||
|
value.splice(ev.target.idx, 1);
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
await this.updateComplete;
|
||||||
|
this._filterChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _comboBoxValueChanged(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
|
||||||
|
if (this.disabled || newValue === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selector.select.multiple) {
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: newValue,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue !== undefined && this.value?.includes(newValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this._filterChanged();
|
||||||
|
this.comboBox.setInputValue("");
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const currentValue =
|
||||||
|
!this.value || this.value === "" ? [] : (this.value as string[]);
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: [...currentValue, newValue],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterChanged(ev?: CustomEvent): void {
|
||||||
|
this._filter = ev?.detail.value || "";
|
||||||
|
|
||||||
|
const filteredItems = this.comboBox.items?.filter((item) => {
|
||||||
|
if (this.selector.select.multiple && this.value?.includes(item.value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const label = item.label || item.value;
|
||||||
|
return label.toLowerCase().includes(this._filter?.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this._filter && this.selector.select.custom_value) {
|
||||||
|
filteredItems?.unshift({ label: this._filter, value: this._filter });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.comboBox.filteredItems = filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
ha-select,
|
||||||
|
mwc-formfield,
|
||||||
|
ha-formfield {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -170,6 +170,9 @@ export interface SelectOption {
|
|||||||
|
|
||||||
export interface SelectSelector {
|
export interface SelectSelector {
|
||||||
select: {
|
select: {
|
||||||
|
multiple?: boolean;
|
||||||
|
custom_value?: boolean;
|
||||||
|
mode?: "list" | "dropdown";
|
||||||
options: string[] | SelectOption[];
|
options: string[] | SelectOption[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -209,6 +212,7 @@ export interface TargetSelector {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemeSelector {
|
export interface ThemeSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
theme: {};
|
theme: {};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user