import { mdiDragHorizontalVariant } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { SelectOption, SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-checkbox";
import "../ha-formfield";
import "../ha-generic-picker";
import "../ha-input-helper-text";
import "../ha-list-item";
import "../ha-radio";
import "../ha-select";
import "../ha-select-box";
import "../ha-sortable";
@customElement("ha-selector-select")
export class HaSelectSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: SelectSelector;
@property() public value?: string | string[];
@property() public label?: string;
@property() public helper?: string;
@property({ attribute: false })
public localizeValue?: (key: string) => string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
private _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
this._move(oldIndex!, newIndex);
}
private _move(index: number, newIndex: number) {
const value = this.value as string[];
const newValue = value.concat();
const element = newValue.splice(index, 1)[0];
newValue.splice(newIndex, 0, element);
this.value = newValue;
fireEvent(this, "value-changed", {
value: newValue,
});
}
protected render() {
const options = this._getOptions(this.selector);
const translationKey = this.selector.select?.translation_key;
if (this.localizeValue && translationKey) {
options.forEach((option) => {
const localizedLabel = this.localizeValue!(
`${translationKey}.options.${option.value}`
);
if (localizedLabel) {
option.label = localizedLabel;
}
});
}
if (this.selector.select?.sort) {
options.sort((a, b) =>
caseInsensitiveStringCompare(
a.label,
b.label,
this.hass.locale.language
)
);
}
if (
!this.selector.select?.multiple &&
!this.selector.select?.reorder &&
!this.selector.select?.custom_value &&
this._mode === "box"
) {
return html`
${this.label ? html`${this.label}` : nothing}
${this._renderHelper()}
`;
}
if (
!this.selector.select?.custom_value &&
!this.selector.select?.reorder &&
this._mode === "list"
) {
if (!this.selector.select?.multiple) {
return html`
${this.label}
${options.map(
(item: SelectOption) => html`
`
)}
${this._renderHelper()}
`;
}
const value =
!this.value || this.value === "" ? [] : ensureArray(this.value);
return html`
${this.label}
${options.map(
(item: SelectOption) => html`
`
)}
${this._renderHelper()}
`;
}
if (this.selector.select?.multiple) {
const value =
!this.value || this.value === "" ? [] : ensureArray(this.value);
return html`
${value?.length
? html`
${repeat(
value,
(item) => item,
(item, idx) => {
const label =
options.find((option) => option.value === item)
?.label || item;
return html`
${this.selector.select?.reorder
? html`
`
: nothing}
${options.find((option) => option.value === item)
?.label || item}
`;
}
)}
`
: nothing}
`;
}
if (this.selector.select?.custom_value) {
return html`
`;
}
return html`
${options.map(
(item: SelectOption) => html`
${item.label}
`
)}
`;
}
private _renderHelper() {
return this.helper
? html`${this.helper}`
: "";
}
private _getOptions = memoizeOne(
(selector: SelectSelector) =>
selector.select?.options?.map((option) =>
typeof option === "object"
? (option as SelectOption)
: ({ value: option, label: option } as SelectOption)
) || []
);
private _getItems = memoizeOne(
(options: SelectOption[], value?: string[], multiple = false) => {
const filteredOptions = options.filter((option) =>
!option.disabled && !multiple ? true : !value?.includes(option.value)
);
return () =>
filteredOptions.map((option) => ({
id: option.value,
primary: option.label,
sorting_label: option.label,
}));
}
);
private get _mode(): "list" | "dropdown" | "box" {
return (
this.selector.select?.mode ||
((this.selector.select?.options?.length || 0) < 6 ? "list" : "dropdown")
);
}
private _valueChanged(ev) {
ev.stopPropagation();
if (ev.detail?.index === -1 && this.value !== undefined) {
fireEvent(this, "value-changed", {
value: undefined,
});
return;
}
const value = ev.detail?.value || ev.target.value;
if (this.disabled || value === undefined || value === (this.value ?? "")) {
return;
}
fireEvent(this, "value-changed", {
value: value,
});
}
private _checkboxChanged(ev) {
ev.stopPropagation();
if (this.disabled) {
return;
}
let newValue: string[];
const value: string = ev.target.value;
const checked = ev.target.checked;
const oldValue =
!this.value || this.value === "" ? [] : ensureArray(this.value);
if (checked) {
if (oldValue.includes(value)) {
return;
}
newValue = [...oldValue, value];
} else {
if (!oldValue?.includes(value)) {
return;
}
newValue = oldValue.filter((v) => v !== value);
}
fireEvent(this, "value-changed", {
value: newValue,
});
}
private async _removeItem(ev) {
ev.stopPropagation();
const value: string[] = [...ensureArray(this.value!)];
value.splice(ev.target.idx, 1);
fireEvent(this, "value-changed", {
value,
});
}
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;
}
const currentValue = !this.value ? [] : ensureArray(this.value);
if (newValue !== undefined && currentValue.includes(newValue)) {
return;
}
fireEvent(this, "value-changed", {
value: [...currentValue, newValue],
});
}
static styles = css`
:host {
position: relative;
}
ha-select,
ha-formfield {
display: block;
}
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-chip-set {
padding: 8px 0;
}
.label {
display: block;
margin: 0 0 8px;
}
ha-select-box + ha-input-helper-text {
margin-top: 4px;
}
.sortable-fallback {
display: none;
opacity: 0;
}
.sortable-ghost {
opacity: 0.4;
}
.sortable-drag {
cursor: grabbing;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-select": HaSelectSelector;
}
}