mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-08 18:39:40 +00:00
Compare commits
3 Commits
claude/add
...
target-pic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04070d3819 | ||
|
|
8c5a35735d | ||
|
|
b7004cbdea |
@@ -35,6 +35,7 @@ export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
|||||||
const LEGEND_OVERFLOW_LIMIT = 10;
|
const LEGEND_OVERFLOW_LIMIT = 10;
|
||||||
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
|
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
|
||||||
const DOUBLE_TAP_TIME = 300;
|
const DOUBLE_TAP_TIME = 300;
|
||||||
|
const RESIZE_ANIMATION_DURATION = 250;
|
||||||
|
|
||||||
export type CustomLegendOption = ECOption["legend"] & {
|
export type CustomLegendOption = ECOption["legend"] & {
|
||||||
type: "custom";
|
type: "custom";
|
||||||
@@ -90,8 +91,6 @@ export class HaChartBase extends LitElement {
|
|||||||
|
|
||||||
private _shouldResizeChart = false;
|
private _shouldResizeChart = false;
|
||||||
|
|
||||||
private _resizeAnimationDuration?: number;
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
private _resizeController = new ResizeController(this, {
|
private _resizeController = new ResizeController(this, {
|
||||||
callback: () => {
|
callback: () => {
|
||||||
@@ -215,7 +214,6 @@ export class HaChartBase extends LitElement {
|
|||||||
) {
|
) {
|
||||||
// custom legend changes may require a resize to layout properly
|
// custom legend changes may require a resize to layout properly
|
||||||
this._shouldResizeChart = true;
|
this._shouldResizeChart = true;
|
||||||
this._resizeAnimationDuration = 250;
|
|
||||||
}
|
}
|
||||||
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
|
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
|
||||||
chartOptions.dataZoom = this._getDataZoomConfig();
|
chartOptions.dataZoom = this._getDataZoomConfig();
|
||||||
@@ -979,14 +977,11 @@ export class HaChartBase extends LitElement {
|
|||||||
private _handleChartRenderFinished = () => {
|
private _handleChartRenderFinished = () => {
|
||||||
if (this._shouldResizeChart) {
|
if (this._shouldResizeChart) {
|
||||||
this.chart?.resize({
|
this.chart?.resize({
|
||||||
animation:
|
animation: this._reducedMotion
|
||||||
this._reducedMotion ||
|
? undefined
|
||||||
typeof this._resizeAnimationDuration !== "number"
|
: { duration: RESIZE_ANIMATION_DURATION },
|
||||||
? undefined
|
|
||||||
: { duration: this._resizeAnimationDuration },
|
|
||||||
});
|
});
|
||||||
this._shouldResizeChart = false;
|
this._shouldResizeChart = false;
|
||||||
this._resizeAnimationDuration = undefined;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -197,9 +197,6 @@ export class HaDevicePicker extends LitElement {
|
|||||||
const placeholder =
|
const placeholder =
|
||||||
this.placeholder ??
|
this.placeholder ??
|
||||||
this.hass.localize("ui.components.device-picker.placeholder");
|
this.hass.localize("ui.components.device-picker.placeholder");
|
||||||
const notFoundLabel = this.hass.localize(
|
|
||||||
"ui.components.device-picker.no_match"
|
|
||||||
);
|
|
||||||
|
|
||||||
const valueRenderer = this._valueRenderer(this._configEntryLookup);
|
const valueRenderer = this._valueRenderer(this._configEntryLookup);
|
||||||
|
|
||||||
@@ -209,7 +206,10 @@ export class HaDevicePicker extends LitElement {
|
|||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.searchLabel=${this.searchLabel}
|
.searchLabel=${this.searchLabel}
|
||||||
.notFoundLabel=${notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
|
.emptyLabel=${this.hass.localize(
|
||||||
|
"ui.components.device-picker.no_devices"
|
||||||
|
)}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
@@ -233,6 +233,11 @@ export class HaDevicePicker extends LitElement {
|
|||||||
this.value = value;
|
this.value = value;
|
||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _notFoundLabel = (search: string) =>
|
||||||
|
this.hass.localize("ui.components.device-picker.no_match", {
|
||||||
|
term: html`<b>‘${search}’</b>`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -269,9 +269,6 @@ export class HaEntityPicker extends LitElement {
|
|||||||
const placeholder =
|
const placeholder =
|
||||||
this.placeholder ??
|
this.placeholder ??
|
||||||
this.hass.localize("ui.components.entity.entity-picker.placeholder");
|
this.hass.localize("ui.components.entity.entity-picker.placeholder");
|
||||||
const notFoundLabel = this.hass.localize(
|
|
||||||
"ui.components.entity.entity-picker.no_match"
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-generic-picker
|
||||||
@@ -282,7 +279,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.searchLabel=${this.searchLabel}
|
.searchLabel=${this.searchLabel}
|
||||||
.notFoundLabel=${notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.addButton ? undefined : this.value}
|
.value=${this.addButton ? undefined : this.value}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
@@ -356,6 +353,11 @@ export class HaEntityPicker extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _notFoundLabel = (search: string) =>
|
||||||
|
this.hass.localize("ui.components.entity.entity-picker.no_match", {
|
||||||
|
term: html`<b>‘${search}’</b>`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -271,7 +271,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(isRTL ? " ◂ " : " ▸ ");
|
.join(isRTL ? " ◂ " : " ▸ ");
|
||||||
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
|
|
||||||
|
|
||||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
||||||
output.push({
|
output.push({
|
||||||
@@ -279,7 +278,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
statistic_id: id,
|
statistic_id: id,
|
||||||
primary,
|
primary,
|
||||||
secondary,
|
secondary,
|
||||||
a11y_label: a11yLabel,
|
|
||||||
stateObj: stateObj,
|
stateObj: stateObj,
|
||||||
type: "entity",
|
type: "entity",
|
||||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||||
@@ -458,9 +456,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
const placeholder =
|
const placeholder =
|
||||||
this.placeholder ??
|
this.placeholder ??
|
||||||
this.hass.localize("ui.components.statistic-picker.placeholder");
|
this.hass.localize("ui.components.statistic-picker.placeholder");
|
||||||
const notFoundLabel = this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.no_match"
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-generic-picker
|
||||||
@@ -468,7 +463,10 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
.allowCustomValue=${this.allowCustomEntity}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.notFoundLabel=${notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
|
.emptyLabel=${this.hass.localize(
|
||||||
|
"ui.components.statistic-picker.no_statistics"
|
||||||
|
)}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
@@ -521,6 +519,11 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
await this._picker?.open();
|
await this._picker?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _notFoundLabel = (search: string) =>
|
||||||
|
this.hass.localize("ui.components.statistic-picker.no_match", {
|
||||||
|
term: html`<b>‘${search}’</b>`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -369,9 +369,10 @@ export class HaAreaPicker extends LitElement {
|
|||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.notFoundLabel=${this.hass.localize(
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
"ui.components.area-picker.no_match"
|
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
|
||||||
)}
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
@@ -425,6 +426,11 @@ export class HaAreaPicker extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _notFoundLabel = (search: string) =>
|
||||||
|
this.hass.localize("ui.components.area-picker.no_match", {
|
||||||
|
term: html`<b>‘${search}’</b>`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -383,8 +383,9 @@ export class HaFloorPicker extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.notFoundLabel=${this.hass.localize(
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
"ui.components.floor-picker.no_match"
|
.emptyLabel=${this.hass.localize(
|
||||||
|
"ui.components.floor-picker.no_floors"
|
||||||
)}
|
)}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
@@ -444,6 +445,11 @@ export class HaFloorPicker extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _notFoundLabel = (search: string) =>
|
||||||
|
this.hass.localize("ui.components.floor-picker.no_match", {
|
||||||
|
term: html`<b>‘${search}’</b>`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ import "./ha-svg-icon";
|
|||||||
export class HaGenericPicker extends LitElement {
|
export class HaGenericPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
// eslint-disable-next-line lit/no-native-attributes
|
|
||||||
@property({ type: Boolean }) public autofocus = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
@@ -49,8 +46,11 @@ export class HaGenericPicker extends LitElement {
|
|||||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||||
public hideClearIcon = false;
|
public hideClearIcon = false;
|
||||||
|
|
||||||
@property({ attribute: false, type: Array })
|
@property({ attribute: false })
|
||||||
public getItems?: () => PickerComboBoxItem[];
|
public getItems?: (
|
||||||
|
searchString?: string,
|
||||||
|
section?: string
|
||||||
|
) => (PickerComboBoxItem | string)[];
|
||||||
|
|
||||||
@property({ attribute: false, type: Array })
|
@property({ attribute: false, type: Array })
|
||||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||||
@@ -64,8 +64,11 @@ export class HaGenericPicker extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||||
|
|
||||||
@property({ attribute: "not-found-label", type: String })
|
@property({ attribute: false })
|
||||||
public notFoundLabel?: string;
|
public notFoundLabel?: string | ((search: string) => string);
|
||||||
|
|
||||||
|
@property({ attribute: "empty-label" })
|
||||||
|
public emptyLabel?: string;
|
||||||
|
|
||||||
@property({ attribute: "popover-placement" })
|
@property({ attribute: "popover-placement" })
|
||||||
public popoverPlacement:
|
public popoverPlacement:
|
||||||
@@ -85,6 +88,25 @@ export class HaGenericPicker extends LitElement {
|
|||||||
/** If set picker shows an add button instead of textbox when value isn't set */
|
/** If set picker shows an add button instead of textbox when value isn't set */
|
||||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||||
|
|
||||||
|
/** Section filter buttons for the list, section headers needs to be defined in getItems as strings */
|
||||||
|
@property({ attribute: false }) public sections?: (
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
| "separator"
|
||||||
|
)[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
|
||||||
|
firstIndex: number;
|
||||||
|
lastIndex: number;
|
||||||
|
firstItem: PickerComboBoxItem | string;
|
||||||
|
secondItem: PickerComboBoxItem | string;
|
||||||
|
itemsCount: number;
|
||||||
|
}) => string | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||||
|
|
||||||
@query(".container") private _containerElement?: HTMLDivElement;
|
@query(".container") private _containerElement?: HTMLDivElement;
|
||||||
|
|
||||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||||
@@ -97,6 +119,11 @@ export class HaGenericPicker extends LitElement {
|
|||||||
|
|
||||||
@state() private _openedNarrow = false;
|
@state() private _openedNarrow = false;
|
||||||
|
|
||||||
|
static shadowRootOptions = {
|
||||||
|
...LitElement.shadowRootOptions,
|
||||||
|
delegatesFocus: true,
|
||||||
|
};
|
||||||
|
|
||||||
private _narrow = false;
|
private _narrow = false;
|
||||||
|
|
||||||
// helper to set new value after closing picker, to avoid flicker
|
// helper to set new value after closing picker, to avoid flicker
|
||||||
@@ -189,16 +216,19 @@ export class HaGenericPicker extends LitElement {
|
|||||||
<ha-picker-combo-box
|
<ha-picker-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
.label=${this.searchLabel ??
|
.label=${this.searchLabel}
|
||||||
(this.hass?.localize("ui.common.search") || "Search")}
|
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
.rowRenderer=${this.rowRenderer}
|
.rowRenderer=${this.rowRenderer}
|
||||||
.notFoundLabel=${this.notFoundLabel}
|
.notFoundLabel=${this.notFoundLabel}
|
||||||
|
.emptyLabel=${this.emptyLabel}
|
||||||
.getItems=${this.getItems}
|
.getItems=${this.getItems}
|
||||||
.getAdditionalItems=${this.getAdditionalItems}
|
.getAdditionalItems=${this.getAdditionalItems}
|
||||||
.searchFn=${this.searchFn}
|
.searchFn=${this.searchFn}
|
||||||
.mode=${dialogMode ? "dialog" : "popover"}
|
.mode=${dialogMode ? "dialog" : "popover"}
|
||||||
|
.sections=${this.sections}
|
||||||
|
.sectionTitleFunction=${this.sectionTitleFunction}
|
||||||
|
.selectedSection=${this.selectedSection}
|
||||||
></ha-picker-combo-box>
|
></ha-picker-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,8 +224,9 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.notFoundLabel=${this.hass.localize(
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
"ui.components.label-picker.no_match"
|
.emptyLabel=${this.hass.localize(
|
||||||
|
"ui.components.label-picker.no_labels"
|
||||||
)}
|
)}
|
||||||
.addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
|
.addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
@@ -288,6 +289,11 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _notFoundLabel = (search: string) =>
|
||||||
|
this.hass.localize("ui.components.label-picker.no_match", {
|
||||||
|
term: html`<b>‘${search}’</b>`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -125,9 +125,10 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
popover-placement="bottom-end"
|
popover-placement="bottom-end"
|
||||||
.notFoundLabel=${this.hass?.localize(
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
"ui.components.language-picker.no_match"
|
.emptyLabel=${this.hass?.localize(
|
||||||
)}
|
"ui.components.language-picker.no_languages"
|
||||||
|
) || "No languages available"}
|
||||||
.placeholder=${this.label ??
|
.placeholder=${this.label ??
|
||||||
(this.hass?.localize("ui.components.language-picker.language") ||
|
(this.hass?.localize("ui.components.language-picker.language") ||
|
||||||
"Language")}
|
"Language")}
|
||||||
@@ -172,6 +173,15 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
this.value = ev.detail.value;
|
this.value = ev.detail.value;
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _notFoundLabel = (search: string) => {
|
||||||
|
const term = html`<b>‘${search}’</b>`;
|
||||||
|
return this.hass
|
||||||
|
? this.hass.localize("ui.components.language-picker.no_match", {
|
||||||
|
term,
|
||||||
|
})
|
||||||
|
: html`No languages found for ${term}`;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||||
import { mdiMagnify } from "@mdi/js";
|
import { mdiMagnify, mdiMinusBoxOutline } from "@mdi/js";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import {
|
import {
|
||||||
@@ -14,11 +14,12 @@ import memoizeOne from "memoize-one";
|
|||||||
import { tinykeys } from "tinykeys";
|
import { tinykeys } from "tinykeys";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
|
||||||
import { HaFuse } from "../resources/fuse";
|
import { HaFuse } from "../resources/fuse";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import { loadVirtualizer } from "../resources/virtualizer";
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./chips/ha-chip-set";
|
||||||
|
import "./chips/ha-filter-chip";
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
import "./ha-textfield";
|
import "./ha-textfield";
|
||||||
@@ -27,28 +28,18 @@ import type { HaTextField } from "./ha-textfield";
|
|||||||
export interface PickerComboBoxItem {
|
export interface PickerComboBoxItem {
|
||||||
id: string;
|
id: string;
|
||||||
primary: string;
|
primary: string;
|
||||||
a11y_label?: string;
|
|
||||||
secondary?: string;
|
secondary?: string;
|
||||||
search_labels?: string[];
|
search_labels?: string[];
|
||||||
sorting_label?: string;
|
sorting_label?: string;
|
||||||
icon_path?: string;
|
icon_path?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
||||||
// Hack to force empty label to always display empty value by default in the search field
|
|
||||||
export interface PickerComboBoxItemWithLabel extends PickerComboBoxItem {
|
|
||||||
a11y_label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___";
|
|
||||||
|
|
||||||
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
|
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
|
||||||
item
|
item
|
||||||
) => html`
|
) => html`
|
||||||
<ha-combo-box-item
|
<ha-combo-box-item type="button" compact>
|
||||||
.type=${item.id === NO_MATCHING_ITEMS_FOUND_ID ? "text" : "button"}
|
|
||||||
compact
|
|
||||||
>
|
|
||||||
${item.icon
|
${item.icon
|
||||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||||
: item.icon_path
|
: item.icon_path
|
||||||
@@ -87,8 +78,11 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
@state() private _listScrolled = false;
|
@state() private _listScrolled = false;
|
||||||
|
|
||||||
@property({ attribute: false, type: Array })
|
@property({ attribute: false })
|
||||||
public getItems?: () => PickerComboBoxItem[];
|
public getItems?: (
|
||||||
|
searchString?: string,
|
||||||
|
section?: string
|
||||||
|
) => (PickerComboBoxItem | string)[];
|
||||||
|
|
||||||
@property({ attribute: false, type: Array })
|
@property({ attribute: false, type: Array })
|
||||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||||
@@ -96,21 +90,45 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
|
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
|
||||||
|
|
||||||
@property({ attribute: "not-found-label", type: String })
|
@property({ attribute: false })
|
||||||
public notFoundLabel?: string;
|
public notFoundLabel?: string | ((search: string) => string);
|
||||||
|
|
||||||
|
@property({ attribute: "empty-label" })
|
||||||
|
public emptyLabel?: string;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||||
|
|
||||||
@property({ reflect: true }) public mode: "popover" | "dialog" = "popover";
|
@property({ reflect: true }) public mode: "popover" | "dialog" = "popover";
|
||||||
|
|
||||||
|
/** Section filter buttons for the list, section headers needs to be defined in getItems as strings */
|
||||||
|
@property({ attribute: false }) public sections?: (
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
| "separator"
|
||||||
|
)[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
|
||||||
|
firstIndex: number;
|
||||||
|
lastIndex: number;
|
||||||
|
firstItem: PickerComboBoxItem | string;
|
||||||
|
secondItem: PickerComboBoxItem | string;
|
||||||
|
itemsCount: number;
|
||||||
|
}) => string | undefined;
|
||||||
|
|
||||||
|
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||||
|
|
||||||
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
||||||
|
|
||||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||||
|
|
||||||
@state() private _items: PickerComboBoxItemWithLabel[] = [];
|
@state() private _items: (PickerComboBoxItem | string)[] = [];
|
||||||
|
|
||||||
private _allItems: PickerComboBoxItemWithLabel[] = [];
|
@state() private _sectionTitle?: string;
|
||||||
|
|
||||||
|
private _allItems: (PickerComboBoxItem | string)[] = [];
|
||||||
|
|
||||||
private _selectedItemIndex = -1;
|
private _selectedItemIndex = -1;
|
||||||
|
|
||||||
@@ -121,6 +139,8 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
private _removeKeyboardShortcuts?: () => void;
|
private _removeKeyboardShortcuts?: () => void;
|
||||||
|
|
||||||
|
private _search = "";
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
this._registerKeyboardShortcuts();
|
this._registerKeyboardShortcuts();
|
||||||
}
|
}
|
||||||
@@ -145,74 +165,142 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
"Search"}
|
"Search"}
|
||||||
@input=${this._filterChanged}
|
@input=${this._filterChanged}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
|
${this._renderSectionButtons()}
|
||||||
|
${this.sections?.length
|
||||||
|
? html`
|
||||||
|
<div class="section-title-wrapper">
|
||||||
|
<div
|
||||||
|
class="section-title ${!this.selectedSection &&
|
||||||
|
this._sectionTitle
|
||||||
|
? "show"
|
||||||
|
: ""}"
|
||||||
|
>
|
||||||
|
${this._sectionTitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
<lit-virtualizer
|
<lit-virtualizer
|
||||||
@scroll=${this._onScrollList}
|
.keyFunction=${this._keyFunction}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
scroller
|
scroller
|
||||||
.items=${this._items}
|
.items=${this._items}
|
||||||
.renderItem=${this._renderItem}
|
.renderItem=${this._renderItem}
|
||||||
style="min-height: 36px;"
|
style="min-height: 36px;"
|
||||||
class=${this._listScrolled ? "scrolled" : ""}
|
class=${this._listScrolled ? "scrolled" : ""}
|
||||||
|
@scroll=${this._onScrollList}
|
||||||
@focus=${this._focusList}
|
@focus=${this._focusList}
|
||||||
|
@visibilityChanged=${this._visibilityChanged}
|
||||||
>
|
>
|
||||||
</lit-virtualizer> `;
|
</lit-virtualizer> `;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _defaultNotFoundItem = memoizeOne(
|
private _renderSectionButtons() {
|
||||||
(
|
if (!this.sections || this.sections.length === 0) {
|
||||||
label: this["notFoundLabel"],
|
return nothing;
|
||||||
localize?: LocalizeFunc
|
}
|
||||||
): PickerComboBoxItemWithLabel => ({
|
|
||||||
id: NO_MATCHING_ITEMS_FOUND_ID,
|
|
||||||
primary:
|
|
||||||
label ||
|
|
||||||
(localize && localize("ui.components.combo-box.no_match")) ||
|
|
||||||
"No matching items found",
|
|
||||||
icon_path: mdiMagnify,
|
|
||||||
a11y_label:
|
|
||||||
label ||
|
|
||||||
(localize && localize("ui.components.combo-box.no_match")) ||
|
|
||||||
"No matching items found",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getAdditionalItems = (searchString?: string) => {
|
return html`
|
||||||
const items = this.getAdditionalItems?.(searchString) || [];
|
<ha-chip-set class="sections">
|
||||||
|
${this.sections.map((section) =>
|
||||||
|
section === "separator"
|
||||||
|
? html`<div class="separator"></div>`
|
||||||
|
: html`<ha-filter-chip
|
||||||
|
@click=${this._toggleSection}
|
||||||
|
.section-id=${section.id}
|
||||||
|
.selected=${this.selectedSection === section.id}
|
||||||
|
.label=${section.label}
|
||||||
|
>
|
||||||
|
</ha-filter-chip>`
|
||||||
|
)}
|
||||||
|
</ha-chip-set>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return items.map<PickerComboBoxItemWithLabel>((item) => ({
|
@eventOptions({ passive: true })
|
||||||
...item,
|
private _visibilityChanged(ev) {
|
||||||
a11y_label: item.a11y_label || item.primary,
|
if (
|
||||||
}));
|
this._virtualizerElement &&
|
||||||
};
|
this.sectionTitleFunction &&
|
||||||
|
this.sections?.length
|
||||||
|
) {
|
||||||
|
const firstItem = this._virtualizerElement.items[ev.first];
|
||||||
|
const secondItem = this._virtualizerElement.items[ev.first + 1];
|
||||||
|
this._sectionTitle = this.sectionTitleFunction({
|
||||||
|
firstIndex: ev.first,
|
||||||
|
lastIndex: ev.last,
|
||||||
|
firstItem: firstItem as PickerComboBoxItem | string,
|
||||||
|
secondItem: secondItem as PickerComboBoxItem | string,
|
||||||
|
itemsCount: this._virtualizerElement.items.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _getItems = (): PickerComboBoxItemWithLabel[] => {
|
private _getAdditionalItems = (searchString?: string) =>
|
||||||
const items = this.getItems ? this.getItems() : [];
|
this.getAdditionalItems?.(searchString) || [];
|
||||||
|
|
||||||
const sortedItems = items
|
private _getItems = () => {
|
||||||
.map<PickerComboBoxItemWithLabel>((item) => ({
|
let items = [
|
||||||
...item,
|
...(this.getItems
|
||||||
a11y_label: item.a11y_label || item.primary,
|
? this.getItems(this._search, this.selectedSection)
|
||||||
}))
|
: []),
|
||||||
.sort((entityA, entityB) =>
|
];
|
||||||
|
|
||||||
|
if (!this.sections?.length) {
|
||||||
|
items = items.sort((entityA, entityB) =>
|
||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(
|
||||||
entityA.sorting_label!,
|
(entityA as PickerComboBoxItem).sorting_label!,
|
||||||
entityB.sorting_label!,
|
(entityB as PickerComboBoxItem).sorting_label!,
|
||||||
this.hass?.locale.language ?? navigator.language
|
this.hass?.locale.language ?? navigator.language
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!sortedItems.length) {
|
if (!items.length) {
|
||||||
sortedItems.push(
|
items.push(NO_ITEMS_AVAILABLE_ID);
|
||||||
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalItems = this._getAdditionalItems();
|
const additionalItems = this._getAdditionalItems();
|
||||||
sortedItems.push(...additionalItems);
|
items.push(...additionalItems);
|
||||||
return sortedItems;
|
|
||||||
|
if (this.mode === "dialog") {
|
||||||
|
items.push("padding"); // padding for safe area inset
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
private _renderItem = (item: PickerComboBoxItem, index: number) => {
|
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
||||||
|
if (item === "padding") {
|
||||||
|
return html`<div class="bottom-padding"></div>`;
|
||||||
|
}
|
||||||
|
if (item === NO_ITEMS_AVAILABLE_ID) {
|
||||||
|
return html`
|
||||||
|
<div class="combo-box-row">
|
||||||
|
<ha-combo-box-item type="text" compact>
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${this._search ? mdiMagnify : mdiMinusBoxOutline}
|
||||||
|
></ha-svg-icon>
|
||||||
|
<span slot="headline"
|
||||||
|
>${this._search
|
||||||
|
? typeof this.notFoundLabel === "function"
|
||||||
|
? this.notFoundLabel(this._search)
|
||||||
|
: this.notFoundLabel ||
|
||||||
|
this.hass?.localize("ui.components.combo-box.no_match") ||
|
||||||
|
"No matching items found"
|
||||||
|
: this.emptyLabel ||
|
||||||
|
this.hass?.localize("ui.components.combo-box.no_items") ||
|
||||||
|
"No items available"}</span
|
||||||
|
>
|
||||||
|
</ha-combo-box-item>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (typeof item === "string") {
|
||||||
|
return html`<div class="title">${item}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER;
|
const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER;
|
||||||
return html`<div
|
return html`<div
|
||||||
id=${`list-item-${index}`}
|
id=${`list-item-${index}`}
|
||||||
@@ -221,9 +309,7 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
.index=${index}
|
.index=${index}
|
||||||
@click=${this._valueSelected}
|
@click=${this._valueSelected}
|
||||||
>
|
>
|
||||||
${item.id === NO_MATCHING_ITEMS_FOUND_ID
|
${renderer(item, index)}
|
||||||
? DEFAULT_ROW_RENDERER(item, index)
|
|
||||||
: renderer(item, index)}
|
|
||||||
</div>`;
|
</div>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -242,10 +328,6 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
const value = (ev.currentTarget as any).value as string;
|
const value = (ev.currentTarget as any).value as string;
|
||||||
const newValue = value?.trim();
|
const newValue = value?.trim();
|
||||||
|
|
||||||
if (newValue === NO_MATCHING_ITEMS_FOUND_ID) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fireEvent(this, "value-changed", { value: newValue });
|
fireEvent(this, "value-changed", { value: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -256,51 +338,83 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
private _filterChanged = (ev: Event) => {
|
private _filterChanged = (ev: Event) => {
|
||||||
const textfield = ev.target as HaTextField;
|
const textfield = ev.target as HaTextField;
|
||||||
const searchString = textfield.value.trim();
|
const searchString = textfield.value.trim();
|
||||||
|
this._search = searchString;
|
||||||
|
|
||||||
if (!searchString) {
|
if (this.sections?.length) {
|
||||||
this._items = this._allItems;
|
this._items = this._getItems();
|
||||||
return;
|
} else {
|
||||||
}
|
if (!searchString) {
|
||||||
|
this._items = this._allItems;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const index = this._fuseIndex(this._allItems);
|
const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
|
||||||
const fuse = new HaFuse(
|
const fuse = new HaFuse(
|
||||||
this._allItems,
|
this._allItems as PickerComboBoxItem[],
|
||||||
{
|
{
|
||||||
shouldSort: false,
|
shouldSort: false,
|
||||||
minMatchCharLength: Math.min(searchString.length, 2),
|
minMatchCharLength: Math.min(searchString.length, 2),
|
||||||
},
|
},
|
||||||
index
|
index
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = fuse.multiTermsSearch(searchString);
|
const results = fuse.multiTermsSearch(searchString);
|
||||||
let filteredItems = this._allItems as PickerComboBoxItem[];
|
let filteredItems = this._allItems;
|
||||||
if (results) {
|
|
||||||
const items = results.map((result) => result.item);
|
if (results) {
|
||||||
if (items.length === 0) {
|
const items: (PickerComboBoxItem | string)[] = results.map(
|
||||||
items.push(
|
(result) => result.item
|
||||||
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
);
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const additionalItems = this._getAdditionalItems();
|
||||||
|
items.push(...additionalItems);
|
||||||
|
|
||||||
|
filteredItems = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.searchFn) {
|
||||||
|
filteredItems = this.searchFn(
|
||||||
|
searchString,
|
||||||
|
filteredItems as PickerComboBoxItem[],
|
||||||
|
this._allItems as PickerComboBoxItem[]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const additionalItems = this._getAdditionalItems(searchString);
|
|
||||||
items.push(...additionalItems);
|
this._items = filteredItems as PickerComboBoxItem[];
|
||||||
filteredItems = items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.searchFn) {
|
|
||||||
filteredItems = this.searchFn(
|
|
||||||
searchString,
|
|
||||||
filteredItems,
|
|
||||||
this._allItems
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._items = filteredItems as PickerComboBoxItemWithLabel[];
|
|
||||||
this._selectedItemIndex = -1;
|
this._selectedItemIndex = -1;
|
||||||
if (this._virtualizerElement) {
|
if (this._virtualizerElement) {
|
||||||
this._virtualizerElement.scrollTo(0, 0);
|
this._virtualizerElement.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _toggleSection(ev: any) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._resetSelectedItem();
|
||||||
|
this._sectionTitle = undefined;
|
||||||
|
const section = ev.target["section-id"] as string;
|
||||||
|
if (!section) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.selectedSection === section) {
|
||||||
|
this.selectedSection = undefined;
|
||||||
|
} else {
|
||||||
|
this.selectedSection = section;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._items = this._getItems();
|
||||||
|
|
||||||
|
// Reset scroll position when filter changes
|
||||||
|
if (this._virtualizerElement) {
|
||||||
|
this._virtualizerElement.scrollToIndex(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _registerKeyboardShortcuts() {
|
private _registerKeyboardShortcuts() {
|
||||||
this._removeKeyboardShortcuts = tinykeys(this, {
|
this._removeKeyboardShortcuts = tinykeys(this, {
|
||||||
ArrowUp: this._selectPreviousItem,
|
ArrowUp: this._selectPreviousItem,
|
||||||
@@ -344,7 +458,7 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items[nextIndex].id === NO_MATCHING_ITEMS_FOUND_ID) {
|
if (typeof items[nextIndex] === "string") {
|
||||||
// Skip titles, padding and empty search
|
// Skip titles, padding and empty search
|
||||||
if (nextIndex === maxItems) {
|
if (nextIndex === maxItems) {
|
||||||
return;
|
return;
|
||||||
@@ -373,7 +487,7 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items[nextIndex]?.id === NO_MATCHING_ITEMS_FOUND_ID) {
|
if (typeof items[nextIndex] === "string") {
|
||||||
// Skip titles, padding and empty search
|
// Skip titles, padding and empty search
|
||||||
if (nextIndex === 0) {
|
if (nextIndex === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -395,13 +509,6 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
const nextIndex = 0;
|
const nextIndex = 0;
|
||||||
|
|
||||||
if (
|
|
||||||
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
|
|
||||||
NO_MATCHING_ITEMS_FOUND_ID
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||||
this._selectedItemIndex = nextIndex + 1;
|
this._selectedItemIndex = nextIndex + 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -419,13 +526,6 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
const nextIndex = this._virtualizerElement.items.length - 1;
|
const nextIndex = this._virtualizerElement.items.length - 1;
|
||||||
|
|
||||||
if (
|
|
||||||
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
|
|
||||||
NO_MATCHING_ITEMS_FOUND_ID
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||||
this._selectedItemIndex = nextIndex - 1;
|
this._selectedItemIndex = nextIndex - 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -453,10 +553,7 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
|
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
|
||||||
|
|
||||||
if (
|
if (this._virtualizerElement?.items.length === 1) {
|
||||||
this._virtualizerElement?.items.length === 1 &&
|
|
||||||
firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID
|
|
||||||
) {
|
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: firstItem.id,
|
value: firstItem.id,
|
||||||
});
|
});
|
||||||
@@ -472,7 +569,7 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
const item = this._virtualizerElement?.items[
|
const item = this._virtualizerElement?.items[
|
||||||
this._selectedItemIndex
|
this._selectedItemIndex
|
||||||
] as PickerComboBoxItem;
|
] as PickerComboBoxItem;
|
||||||
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
|
if (item) {
|
||||||
fireEvent(this, "value-changed", { value: item.id });
|
fireEvent(this, "value-changed", { value: item.id });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -484,6 +581,9 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
this._selectedItemIndex = -1;
|
this._selectedItemIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
||||||
|
typeof item === "string" ? item : item.id;
|
||||||
|
|
||||||
static styles = [
|
static styles = [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
css`
|
css`
|
||||||
@@ -558,6 +658,80 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
background-color: var(--ha-color-fill-neutral-normal-hover);
|
background-color: var(--ha-color-fill-neutral-normal-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sections {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: var(--ha-space-2);
|
||||||
|
padding: var(--ha-space-3) var(--ha-space-3);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([mode="dialog"]) .sections {
|
||||||
|
padding: var(--ha-space-3) var(--ha-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sections ha-filter-chip {
|
||||||
|
flex-shrink: 0;
|
||||||
|
--md-filter-chip-selected-container-color: var(
|
||||||
|
--ha-color-fill-primary-normal-hover
|
||||||
|
);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sections .separator {
|
||||||
|
height: var(--ha-space-8);
|
||||||
|
width: 0;
|
||||||
|
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title,
|
||||||
|
.title {
|
||||||
|
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||||
|
padding: var(--ha-space-1) var(--ha-space-2);
|
||||||
|
font-weight: var(--ha-font-weight-bold);
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
min-height: var(--ha-space-6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([mode="dialog"]) .title {
|
||||||
|
padding: var(--ha-space-1) var(--ha-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([mode="dialog"]) ha-textfield {
|
||||||
|
padding: 0 var(--ha-space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title-wrapper {
|
||||||
|
height: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
width: calc(100% - var(--ha-space-8));
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title.show {
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-search {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--ha-space-3);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,31 @@
|
|||||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||||
|
import { consume } from "@lit/context";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
|
||||||
import { mdiPlaylistPlus } from "@mdi/js";
|
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup } from "lit";
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||||
import { LitElement, css, html, nothing, unsafeCSS } from "lit";
|
import { LitElement, css, html, nothing, unsafeCSS } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../common/array/ensure-array";
|
import { ensureArray } from "../common/array/ensure-array";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||||
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
|
import {
|
||||||
|
getAreasAndFloors,
|
||||||
|
type AreaFloorValue,
|
||||||
|
type FloorComboBoxItem,
|
||||||
|
} from "../data/area_floor";
|
||||||
|
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
|
||||||
|
import { labelsContext } from "../data/context";
|
||||||
|
import { getDevices, type DevicePickerItem } from "../data/device_registry";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "../data/entity";
|
import type { HaEntityPickerEntityFilterFunc } from "../data/entity";
|
||||||
|
import { getEntities, type EntityComboBoxItem } from "../data/entity_registry";
|
||||||
|
import { domainToName } from "../data/integration";
|
||||||
|
import { getLabels, type LabelRegistryEntry } from "../data/label_registry";
|
||||||
import {
|
import {
|
||||||
areaMeetsFilter,
|
areaMeetsFilter,
|
||||||
deviceMeetsFilter,
|
deviceMeetsFilter,
|
||||||
@@ -18,18 +34,23 @@ import {
|
|||||||
type TargetTypeFloorless,
|
type TargetTypeFloorless,
|
||||||
} from "../data/target";
|
} from "../data/target";
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||||
import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail";
|
import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail";
|
||||||
|
import { HaFuse } from "../resources/fuse";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
import { brandsUrl } from "../util/brands-url";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
import "./ha-bottom-sheet";
|
import "./ha-generic-picker";
|
||||||
import "./ha-button";
|
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||||
import "./ha-input-helper-text";
|
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
import "./ha-tree-indicator";
|
||||||
import "./target-picker/ha-target-picker-item-group";
|
import "./target-picker/ha-target-picker-item-group";
|
||||||
import "./target-picker/ha-target-picker-selector";
|
|
||||||
import type { HaTargetPickerSelector } from "./target-picker/ha-target-picker-selector";
|
|
||||||
import "./target-picker/ha-target-picker-value-chip";
|
import "./target-picker/ha-target-picker-value-chip";
|
||||||
|
|
||||||
|
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
|
||||||
|
const SEPARATOR = "________";
|
||||||
|
const CREATE_ID = "___create-new-entity___";
|
||||||
|
|
||||||
@customElement("ha-target-picker")
|
@customElement("ha-target-picker")
|
||||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -68,23 +89,54 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property({ attribute: "add-on-top", type: Boolean }) public addOnTop = false;
|
@property({ attribute: "add-on-top", type: Boolean }) public addOnTop = false;
|
||||||
|
|
||||||
@state() private _open = false;
|
@state() private _selectedSection?: TargetTypeFloorless;
|
||||||
|
|
||||||
@state() private _addTargetWidth = 0;
|
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||||
|
|
||||||
@state() private _narrow = false;
|
@state()
|
||||||
|
@consume({ context: labelsContext, subscribe: true })
|
||||||
@state() private _pickerFilter?: TargetTypeFloorless;
|
private _labelRegistry!: LabelRegistryEntry[];
|
||||||
|
|
||||||
@state() private _pickerWrapperOpen = false;
|
|
||||||
|
|
||||||
@query(".add-target-wrapper") private _addTargetWrapper?: HTMLDivElement;
|
|
||||||
|
|
||||||
@query("ha-target-picker-selector")
|
|
||||||
private _targetPickerSelectorElement?: HaTargetPickerSelector;
|
|
||||||
|
|
||||||
private _newTarget?: { type: TargetType; id: string };
|
private _newTarget?: { type: TargetType; id: string };
|
||||||
|
|
||||||
|
private _getDevicesMemoized = memoizeOne(getDevices);
|
||||||
|
|
||||||
|
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||||
|
|
||||||
|
private _getEntitiesMemoized = memoizeOne(getEntities);
|
||||||
|
|
||||||
|
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
|
||||||
|
|
||||||
|
private get _showEntityId() {
|
||||||
|
return this.hass.userData?.showEntityIdPicker;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _fuseIndexes = {
|
||||||
|
area: memoizeOne((states: FloorComboBoxItem[]) =>
|
||||||
|
this._createFuseIndex(states)
|
||||||
|
),
|
||||||
|
entity: memoizeOne((states: EntityComboBoxItem[]) =>
|
||||||
|
this._createFuseIndex(states)
|
||||||
|
),
|
||||||
|
device: memoizeOne((states: DevicePickerItem[]) =>
|
||||||
|
this._createFuseIndex(states)
|
||||||
|
),
|
||||||
|
label: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||||
|
this._createFuseIndex(states)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
this._loadConfigEntries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createFuseIndex = (states) =>
|
||||||
|
Fuse.createIndex(["search_labels"], states);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (this.addOnTop) {
|
if (this.addOnTop) {
|
||||||
return html` ${this._renderPicker()} ${this._renderItems()} `;
|
return html` ${this._renderPicker()} ${this._renderItems()} `;
|
||||||
@@ -289,137 +341,63 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderPicker() {
|
private _renderPicker() {
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
id: "entity",
|
||||||
|
label: this.hass.localize("ui.components.target-picker.type.entities"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "device",
|
||||||
|
label: this.hass.localize("ui.components.target-picker.type.devices"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "area",
|
||||||
|
label: this.hass.localize("ui.components.target-picker.type.areas"),
|
||||||
|
},
|
||||||
|
"separator" as const,
|
||||||
|
{
|
||||||
|
id: "label",
|
||||||
|
label: this.hass.localize("ui.components.target-picker.type.labels"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="add-target-wrapper">
|
<div class="add-target-wrapper">
|
||||||
<ha-button
|
<ha-generic-picker
|
||||||
id="add-target-button"
|
.hass=${this.hass}
|
||||||
size="small"
|
.disabled=${this.disabled}
|
||||||
appearance="filled"
|
.autofocus=${this.autofocus}
|
||||||
@click=${this._showPicker}
|
.helper=${this.helper}
|
||||||
|
.sections=${sections}
|
||||||
|
.notFoundLabel=${this._noTargetFoundLabel}
|
||||||
|
.emptyLabel=${this.hass.localize(
|
||||||
|
"ui.components.target-picker.no_targets"
|
||||||
|
)}
|
||||||
|
.sectionTitleFunction=${this._sectionTitleFunction}
|
||||||
|
.selectedSection=${this._selectedSection}
|
||||||
|
.rowRenderer=${this._renderRow}
|
||||||
|
.getItems=${this._getItems}
|
||||||
|
@value-changed=${this._targetPicked}
|
||||||
|
.addButtonLabel=${this.hass.localize(
|
||||||
|
"ui.components.target-picker.add_target"
|
||||||
|
)}
|
||||||
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
>
|
>
|
||||||
<ha-svg-icon .path=${mdiPlaylistPlus} slot="start"></ha-svg-icon>
|
</ha-generic-picker>
|
||||||
${this.hass.localize("ui.components.target-picker.add_target")}
|
|
||||||
</ha-button>
|
|
||||||
${!this._narrow && (this._pickerWrapperOpen || this._open)
|
|
||||||
? html`
|
|
||||||
<wa-popover
|
|
||||||
.open=${this._pickerWrapperOpen}
|
|
||||||
style="--body-width: ${this._addTargetWidth}px;"
|
|
||||||
without-arrow
|
|
||||||
distance="-4"
|
|
||||||
placement="bottom-start"
|
|
||||||
for="add-target-button"
|
|
||||||
auto-size="vertical"
|
|
||||||
auto-size-padding="16"
|
|
||||||
@wa-after-show=${this._showSelector}
|
|
||||||
@wa-after-hide=${this._hidePicker}
|
|
||||||
trap-focus
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label=${this.hass.localize(
|
|
||||||
"ui.components.target-picker.add_target"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
${this._renderTargetSelector()}
|
|
||||||
</wa-popover>
|
|
||||||
`
|
|
||||||
: this._pickerWrapperOpen || this._open
|
|
||||||
? html`<ha-bottom-sheet
|
|
||||||
flexcontent
|
|
||||||
.open=${this._pickerWrapperOpen}
|
|
||||||
@wa-after-show=${this._showSelector}
|
|
||||||
@closed=${this._hidePicker}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label=${this.hass.localize(
|
|
||||||
"ui.components.target-picker.add_target"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
${this._renderTargetSelector(true)}
|
|
||||||
</ha-bottom-sheet>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
</div>
|
||||||
${this.helper
|
|
||||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
|
||||||
>${this.helper}</ha-input-helper-text
|
|
||||||
>`
|
|
||||||
: nothing}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
private _targetPicked(ev: CustomEvent<{ value: string }>) {
|
||||||
super.connectedCallback();
|
|
||||||
this._handleResize();
|
|
||||||
window.addEventListener("resize", this._handleResize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
window.removeEventListener("resize", this._handleResize);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleResize = () => {
|
|
||||||
this._narrow =
|
|
||||||
window.matchMedia("(max-width: 870px)").matches ||
|
|
||||||
window.matchMedia("(max-height: 500px)").matches;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _showPicker() {
|
|
||||||
this._addTargetWidth = this._addTargetWrapper?.offsetWidth || 0;
|
|
||||||
this._pickerWrapperOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for drawer animation to finish
|
|
||||||
private _showSelector = () => {
|
|
||||||
this._open = true;
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this._targetPickerSelectorElement?.focus();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private _handleUpdatePickerFilter(
|
|
||||||
ev: CustomEvent<TargetTypeFloorless | undefined>
|
|
||||||
) {
|
|
||||||
this._updatePickerFilter(
|
|
||||||
typeof ev.detail === "string" ? ev.detail : undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updatePickerFilter = (filter?: TargetTypeFloorless) => {
|
|
||||||
this._pickerFilter = filter;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _hidePicker(ev) {
|
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this._open = false;
|
const value = ev.detail.value;
|
||||||
this._pickerWrapperOpen = false;
|
if (value.startsWith(CREATE_ID)) {
|
||||||
|
this._createNewDomainElement(value.substring(CREATE_ID.length));
|
||||||
if (this._newTarget) {
|
return;
|
||||||
this._addTarget(this._newTarget.id, this._newTarget.type);
|
|
||||||
this._newTarget = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private _renderTargetSelector(dialogMode = false) {
|
const [type, id] = ev.detail.value.split(SEPARATOR);
|
||||||
if (!this._open) {
|
this._addTarget(id, type as TargetType);
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
return html`
|
|
||||||
<ha-target-picker-selector
|
|
||||||
.hass=${this.hass}
|
|
||||||
@filter-type-changed=${this._handleUpdatePickerFilter}
|
|
||||||
.filterType=${this._pickerFilter}
|
|
||||||
@target-picked=${this._handleTargetPicked}
|
|
||||||
@create-domain-picked=${this._handleCreateDomain}
|
|
||||||
.targetValue=${this.value}
|
|
||||||
.deviceFilter=${this.deviceFilter}
|
|
||||||
.entityFilter=${this.entityFilter}
|
|
||||||
.includeDomains=${this.includeDomains}
|
|
||||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
|
||||||
.createDomains=${this.createDomains}
|
|
||||||
.mode=${dialogMode ? "dialog" : "popover"}
|
|
||||||
></ha-target-picker-selector>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addTarget(id: string, type: TargetType) {
|
private _addTarget(id: string, type: TargetType) {
|
||||||
@@ -454,26 +432,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
?.removeAttribute("collapsed");
|
?.removeAttribute("collapsed");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleTargetPicked = async (
|
private _createNewDomainElement = (domain: string) => {
|
||||||
ev: CustomEvent<{ type: TargetType; id: string }>
|
|
||||||
) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
this._pickerWrapperOpen = false;
|
|
||||||
|
|
||||||
if (!ev.detail.type || !ev.detail.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// save new target temporarily to add it after dialog closes
|
|
||||||
this._newTarget = ev.detail;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _handleCreateDomain = (ev: CustomEvent<string>) => {
|
|
||||||
this._pickerWrapperOpen = false;
|
|
||||||
|
|
||||||
const domain = ev.detail;
|
|
||||||
|
|
||||||
showHelperDetailDialog(this, {
|
showHelperDetailDialog(this, {
|
||||||
domain,
|
domain,
|
||||||
dialogClosedCallback: (item) => {
|
dialogClosedCallback: (item) => {
|
||||||
@@ -675,6 +634,465 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getRowType = (
|
||||||
|
item:
|
||||||
|
| PickerComboBoxItem
|
||||||
|
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||||
|
| EntityComboBoxItem
|
||||||
|
| DevicePickerItem
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
(item as FloorComboBoxItem).type === "area" ||
|
||||||
|
(item as FloorComboBoxItem).type === "floor"
|
||||||
|
) {
|
||||||
|
return (item as FloorComboBoxItem).type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("domain" in item) {
|
||||||
|
return "device";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("stateObj" in item) {
|
||||||
|
return "entity";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.id === EMPTY_SEARCH) {
|
||||||
|
return "empty";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "label";
|
||||||
|
};
|
||||||
|
|
||||||
|
private _sectionTitleFunction = ({
|
||||||
|
firstIndex,
|
||||||
|
lastIndex,
|
||||||
|
firstItem,
|
||||||
|
secondItem,
|
||||||
|
itemsCount,
|
||||||
|
}: {
|
||||||
|
firstIndex: number;
|
||||||
|
lastIndex: number;
|
||||||
|
firstItem: PickerComboBoxItem | string;
|
||||||
|
secondItem: PickerComboBoxItem | string;
|
||||||
|
itemsCount: number;
|
||||||
|
}) => {
|
||||||
|
if (
|
||||||
|
firstItem === undefined ||
|
||||||
|
secondItem === undefined ||
|
||||||
|
typeof firstItem === "string" ||
|
||||||
|
(typeof secondItem === "string" && secondItem !== "padding") ||
|
||||||
|
(firstIndex === 0 && lastIndex === itemsCount - 1)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = this._getRowType(firstItem as PickerComboBoxItem);
|
||||||
|
const translationType:
|
||||||
|
| "areas"
|
||||||
|
| "entities"
|
||||||
|
| "devices"
|
||||||
|
| "labels"
|
||||||
|
| undefined =
|
||||||
|
type === "area" || type === "floor"
|
||||||
|
? "areas"
|
||||||
|
: type === "entity"
|
||||||
|
? "entities"
|
||||||
|
: type && type !== "empty"
|
||||||
|
? `${type}s`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return translationType
|
||||||
|
? this.hass.localize(
|
||||||
|
`ui.components.target-picker.type.${translationType}`
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getItems = (searchString: string, section: string) => {
|
||||||
|
this._selectedSection = section as TargetTypeFloorless | undefined;
|
||||||
|
|
||||||
|
return this._getItemsMemoized(
|
||||||
|
this.entityFilter,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.includeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.value,
|
||||||
|
searchString,
|
||||||
|
this._configEntryLookup,
|
||||||
|
this._selectedSection
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getItemsMemoized = memoizeOne(
|
||||||
|
(
|
||||||
|
entityFilter: this["entityFilter"],
|
||||||
|
deviceFilter: this["deviceFilter"],
|
||||||
|
includeDomains: this["includeDomains"],
|
||||||
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
|
targetValue: this["value"],
|
||||||
|
searchTerm: string,
|
||||||
|
configEntryLookup: Record<string, ConfigEntry>,
|
||||||
|
filterType?: TargetTypeFloorless
|
||||||
|
) => {
|
||||||
|
const items: (
|
||||||
|
| string
|
||||||
|
| FloorComboBoxItem
|
||||||
|
| EntityComboBoxItem
|
||||||
|
| PickerComboBoxItem
|
||||||
|
)[] = [];
|
||||||
|
|
||||||
|
if (!filterType || filterType === "entity") {
|
||||||
|
let entities = this._getEntitiesMemoized(
|
||||||
|
this.hass,
|
||||||
|
includeDomains,
|
||||||
|
undefined,
|
||||||
|
entityFilter,
|
||||||
|
includeDeviceClasses,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
targetValue?.entity_id
|
||||||
|
? ensureArray(targetValue.entity_id)
|
||||||
|
: undefined,
|
||||||
|
undefined,
|
||||||
|
`entity${SEPARATOR}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
entities = this._filterGroup(
|
||||||
|
"entity",
|
||||||
|
entities,
|
||||||
|
searchTerm,
|
||||||
|
(item: EntityComboBoxItem) =>
|
||||||
|
item.stateObj?.entity_id === searchTerm
|
||||||
|
) as EntityComboBoxItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filterType && entities.length) {
|
||||||
|
// show group title
|
||||||
|
items.push(
|
||||||
|
this.hass.localize("ui.components.target-picker.type.entities")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(...entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filterType || filterType === "device") {
|
||||||
|
let devices = this._getDevicesMemoized(
|
||||||
|
this.hass,
|
||||||
|
configEntryLookup,
|
||||||
|
includeDomains,
|
||||||
|
undefined,
|
||||||
|
includeDeviceClasses,
|
||||||
|
deviceFilter,
|
||||||
|
entityFilter,
|
||||||
|
targetValue?.device_id
|
||||||
|
? ensureArray(targetValue.device_id)
|
||||||
|
: undefined,
|
||||||
|
undefined,
|
||||||
|
`device${SEPARATOR}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
devices = this._filterGroup("device", devices, searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filterType && devices.length) {
|
||||||
|
// show group title
|
||||||
|
items.push(
|
||||||
|
this.hass.localize("ui.components.target-picker.type.devices")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(...devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filterType || filterType === "area") {
|
||||||
|
let areasAndFloors = this._getAreasAndFloorsMemoized(
|
||||||
|
this.hass.states,
|
||||||
|
this.hass.floors,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.entities,
|
||||||
|
memoizeOne((value: AreaFloorValue): string =>
|
||||||
|
[value.type, value.id].join(SEPARATOR)
|
||||||
|
),
|
||||||
|
includeDomains,
|
||||||
|
undefined,
|
||||||
|
includeDeviceClasses,
|
||||||
|
deviceFilter,
|
||||||
|
entityFilter,
|
||||||
|
targetValue?.area_id ? ensureArray(targetValue.area_id) : undefined,
|
||||||
|
targetValue?.floor_id ? ensureArray(targetValue.floor_id) : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
areasAndFloors = this._filterGroup(
|
||||||
|
"area",
|
||||||
|
areasAndFloors,
|
||||||
|
searchTerm
|
||||||
|
) as FloorComboBoxItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filterType && areasAndFloors.length) {
|
||||||
|
// show group title
|
||||||
|
items.push(
|
||||||
|
this.hass.localize("ui.components.target-picker.type.areas")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
...areasAndFloors.map((item, index) => {
|
||||||
|
const nextItem = areasAndFloors[index + 1];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!nextItem ||
|
||||||
|
(item.type === "area" && nextItem.type === "floor")
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
last: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filterType || filterType === "label") {
|
||||||
|
let labels = this._getLabelsMemoized(
|
||||||
|
this.hass,
|
||||||
|
this._labelRegistry,
|
||||||
|
includeDomains,
|
||||||
|
undefined,
|
||||||
|
includeDeviceClasses,
|
||||||
|
deviceFilter,
|
||||||
|
entityFilter,
|
||||||
|
targetValue?.label_id ? ensureArray(targetValue.label_id) : undefined,
|
||||||
|
`label${SEPARATOR}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
labels = this._filterGroup("label", labels, searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filterType && labels.length) {
|
||||||
|
// show group title
|
||||||
|
items.push(
|
||||||
|
this.hass.localize("ui.components.target-picker.type.labels")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(...labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _filterGroup(
|
||||||
|
type: TargetType,
|
||||||
|
items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[],
|
||||||
|
searchTerm: string,
|
||||||
|
checkExact?: (
|
||||||
|
item: FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem
|
||||||
|
) => boolean
|
||||||
|
) {
|
||||||
|
const fuseIndex = this._fuseIndexes[type](items);
|
||||||
|
const fuse = new HaFuse(
|
||||||
|
items,
|
||||||
|
{
|
||||||
|
shouldSort: false,
|
||||||
|
minMatchCharLength: Math.min(searchTerm.length, 2),
|
||||||
|
},
|
||||||
|
fuseIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = fuse.multiTermsSearch(searchTerm);
|
||||||
|
let filteredItems = items;
|
||||||
|
if (results) {
|
||||||
|
filteredItems = results.map((result) => result.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkExact) {
|
||||||
|
return filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is exact match for entity id, put it first
|
||||||
|
const index = filteredItems.findIndex((item) => checkExact(item));
|
||||||
|
if (index === -1) {
|
||||||
|
return filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [exactMatch] = filteredItems.splice(index, 1);
|
||||||
|
filteredItems.unshift(exactMatch);
|
||||||
|
|
||||||
|
return filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getAdditionalItems = () => this._getCreateItems(this.createDomains);
|
||||||
|
|
||||||
|
private _getCreateItems = memoizeOne(
|
||||||
|
(createDomains: this["createDomains"]) => {
|
||||||
|
if (!createDomains?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return createDomains.map((domain) => {
|
||||||
|
const primary = this.hass.localize(
|
||||||
|
"ui.components.entity.entity-picker.create_helper",
|
||||||
|
{
|
||||||
|
domain: isHelperDomain(domain)
|
||||||
|
? this.hass.localize(`ui.panel.config.helpers.types.${domain}`)
|
||||||
|
: domainToName(this.hass.localize, domain),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: CREATE_ID + domain,
|
||||||
|
primary: primary,
|
||||||
|
secondary: this.hass.localize(
|
||||||
|
"ui.components.entity.entity-picker.new_entity"
|
||||||
|
),
|
||||||
|
icon_path: mdiPlus,
|
||||||
|
} satisfies EntityComboBoxItem;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private async _loadConfigEntries() {
|
||||||
|
const configEntries = await getConfigEntries(this.hass);
|
||||||
|
this._configEntryLookup = Object.fromEntries(
|
||||||
|
configEntries.map((entry) => [entry.entry_id, entry])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderRow = (
|
||||||
|
item:
|
||||||
|
| PickerComboBoxItem
|
||||||
|
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||||
|
| EntityComboBoxItem
|
||||||
|
| DevicePickerItem,
|
||||||
|
index: number
|
||||||
|
) => {
|
||||||
|
if (!item) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = this._getRowType(item);
|
||||||
|
let hasFloor = false;
|
||||||
|
let rtl = false;
|
||||||
|
let showEntityId = false;
|
||||||
|
|
||||||
|
if (type === "area" || type === "floor") {
|
||||||
|
item.id = item[type]?.[`${type}_id`];
|
||||||
|
|
||||||
|
rtl = computeRTL(this.hass);
|
||||||
|
hasFloor =
|
||||||
|
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "entity") {
|
||||||
|
showEntityId = !!this._showEntityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-combo-box-item
|
||||||
|
id=${`list-item-${index}`}
|
||||||
|
tabindex="-1"
|
||||||
|
.type=${type === "empty" ? "text" : "button"}
|
||||||
|
class=${type === "empty" ? "empty" : ""}
|
||||||
|
style=${(item as FloorComboBoxItem).type === "area" && hasFloor
|
||||||
|
? "--md-list-item-leading-space: var(--ha-space-12);"
|
||||||
|
: ""}
|
||||||
|
>
|
||||||
|
${(item as FloorComboBoxItem).type === "area" && hasFloor
|
||||||
|
? html`
|
||||||
|
<ha-tree-indicator
|
||||||
|
style=${styleMap({
|
||||||
|
width: "var(--ha-space-12)",
|
||||||
|
position: "absolute",
|
||||||
|
top: "var(--ha-space-0)",
|
||||||
|
left: rtl ? undefined : "var(--ha-space-1)",
|
||||||
|
right: rtl ? "var(--ha-space-1)" : undefined,
|
||||||
|
transform: rtl ? "scaleX(-1)" : "",
|
||||||
|
})}
|
||||||
|
.end=${(
|
||||||
|
item as FloorComboBoxItem & { last?: boolean | undefined }
|
||||||
|
).last}
|
||||||
|
slot="start"
|
||||||
|
></ha-tree-indicator>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${item.icon
|
||||||
|
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||||
|
: item.icon_path
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${item.icon_path}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: type === "entity" && (item as EntityComboBoxItem).stateObj
|
||||||
|
? html`
|
||||||
|
<state-badge
|
||||||
|
slot="start"
|
||||||
|
.stateObj=${(item as EntityComboBoxItem).stateObj}
|
||||||
|
.hass=${this.hass}
|
||||||
|
></state-badge>
|
||||||
|
`
|
||||||
|
: type === "device" && (item as DevicePickerItem).domain
|
||||||
|
? html`
|
||||||
|
<img
|
||||||
|
slot="start"
|
||||||
|
alt=""
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: (item as DevicePickerItem).domain!,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes.darkMode,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: type === "floor"
|
||||||
|
? html`<ha-floor-icon
|
||||||
|
slot="start"
|
||||||
|
.floor=${(item as FloorComboBoxItem).floor!}
|
||||||
|
></ha-floor-icon>`
|
||||||
|
: type === "area"
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${item.icon_path || mdiTextureBox}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: nothing}
|
||||||
|
<span slot="headline">${item.primary}</span>
|
||||||
|
${item.secondary
|
||||||
|
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||||
|
: nothing}
|
||||||
|
${(item as EntityComboBoxItem).stateObj && showEntityId
|
||||||
|
? html`
|
||||||
|
<span slot="supporting-text" class="code">
|
||||||
|
${(item as EntityComboBoxItem).stateObj?.entity_id}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${(item as EntityComboBoxItem).domain_name &&
|
||||||
|
(type !== "entity" || !showEntityId)
|
||||||
|
? html`
|
||||||
|
<div slot="trailing-supporting-text" class="domain">
|
||||||
|
${(item as EntityComboBoxItem).domain_name}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _noTargetFoundLabel = (search: string) =>
|
||||||
|
this.hass.localize("ui.components.target-picker.no_target_found", {
|
||||||
|
term: html`<b>‘${search}’</b>`,
|
||||||
|
});
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
.add-target-wrapper {
|
.add-target-wrapper {
|
||||||
@@ -683,31 +1101,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
margin-top: var(--ha-space-3);
|
margin-top: var(--ha-space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
wa-popover {
|
ha-generic-picker {
|
||||||
--wa-space-l: var(--ha-space-0);
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
wa-popover::part(body) {
|
|
||||||
width: min(max(var(--body-width), 336px), 600px);
|
|
||||||
max-width: min(max(var(--body-width), 336px), 600px);
|
|
||||||
max-height: 500px;
|
|
||||||
height: 70vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-height: 1000px) {
|
|
||||||
wa-popover::part(body) {
|
|
||||||
max-height: 400px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-bottom-sheet {
|
|
||||||
--ha-bottom-sheet-height: 90vh;
|
|
||||||
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
|
|
||||||
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
|
|
||||||
--ha-bottom-sheet-max-width: 600px;
|
|
||||||
--ha-bottom-sheet-padding: var(--ha-space-0);
|
|
||||||
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
${unsafeCSS(chipStyles)}
|
${unsafeCSS(chipStyles)}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -128,9 +128,7 @@ class HaUserPicker extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.notFoundLabel=${this.hass.localize(
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
"ui.components.user-picker.no_match"
|
|
||||||
)}
|
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
@@ -149,6 +147,11 @@ class HaUserPicker extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _notFoundLabel = (search: string) =>
|
||||||
|
this.hass.localize("ui.components.user-picker.no_match", {
|
||||||
|
term: html`<b>‘${search}’</b>`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -186,7 +186,8 @@ export const getDevices = (
|
|||||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
excludeDevices?: string[],
|
excludeDevices?: string[],
|
||||||
value?: string
|
value?: string,
|
||||||
|
idPrefix = ""
|
||||||
): DevicePickerItem[] => {
|
): DevicePickerItem[] => {
|
||||||
const devices = Object.values(hass.devices);
|
const devices = Object.values(hass.devices);
|
||||||
const entities = Object.values(hass.entities);
|
const entities = Object.values(hass.entities);
|
||||||
@@ -298,7 +299,7 @@ export const getDevices = (
|
|||||||
const domainName = domain ? domainToName(hass.localize, domain) : undefined;
|
const domainName = domain ? domainToName(hass.localize, domain) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: device.id,
|
id: `${idPrefix}${device.id}`,
|
||||||
label: "",
|
label: "",
|
||||||
primary:
|
primary:
|
||||||
deviceName ||
|
deviceName ||
|
||||||
|
|||||||
@@ -344,7 +344,8 @@ export const getEntities = (
|
|||||||
includeUnitOfMeasurement?: string[],
|
includeUnitOfMeasurement?: string[],
|
||||||
includeEntities?: string[],
|
includeEntities?: string[],
|
||||||
excludeEntities?: string[],
|
excludeEntities?: string[],
|
||||||
value?: string
|
value?: string,
|
||||||
|
idPrefix = ""
|
||||||
): EntityComboBoxItem[] => {
|
): EntityComboBoxItem[] => {
|
||||||
let items: EntityComboBoxItem[] = [];
|
let items: EntityComboBoxItem[] = [];
|
||||||
|
|
||||||
@@ -395,10 +396,9 @@ export const getEntities = (
|
|||||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(isRTL ? " ◂ " : " ▸ ");
|
.join(isRTL ? " ◂ " : " ▸ ");
|
||||||
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: entityId,
|
id: `${idPrefix}${entityId}`,
|
||||||
primary: primary,
|
primary: primary,
|
||||||
secondary: secondary,
|
secondary: secondary,
|
||||||
domain_name: domainName,
|
domain_name: domainName,
|
||||||
@@ -411,7 +411,6 @@ export const getEntities = (
|
|||||||
friendlyName,
|
friendlyName,
|
||||||
entityId,
|
entityId,
|
||||||
].filter(Boolean) as string[],
|
].filter(Boolean) as string[],
|
||||||
a11y_label: a11yLabel,
|
|
||||||
stateObj: stateObj,
|
stateObj: stateObj,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ export const getLabels = (
|
|||||||
includeDeviceClasses?: string[],
|
includeDeviceClasses?: string[],
|
||||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||||
excludeLabels?: string[]
|
excludeLabels?: string[],
|
||||||
|
idPrefix = ""
|
||||||
): PickerComboBoxItem[] => {
|
): PickerComboBoxItem[] => {
|
||||||
if (!labels || labels.length === 0) {
|
if (!labels || labels.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@@ -262,7 +263,7 @@ export const getLabels = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const items = outputLabels.map<PickerComboBoxItem>((label) => ({
|
const items = outputLabels.map<PickerComboBoxItem>((label) => ({
|
||||||
id: label.label_id,
|
id: `${idPrefix}${label.label_id}`,
|
||||||
primary: label.name,
|
primary: label.name,
|
||||||
secondary: label.description ?? "",
|
secondary: label.description ?? "",
|
||||||
icon: label.icon || undefined,
|
icon: label.icon || undefined,
|
||||||
|
|||||||
@@ -11,14 +11,10 @@ class HaInitPage extends LitElement {
|
|||||||
|
|
||||||
@state() private _retryInSeconds = 60;
|
@state() private _retryInSeconds = 60;
|
||||||
|
|
||||||
@state() private _loadingTimeout = false;
|
|
||||||
|
|
||||||
private _showProgressIndicatorTimeout?: number;
|
private _showProgressIndicatorTimeout?: number;
|
||||||
|
|
||||||
private _retryInterval?: number;
|
private _retryInterval?: number;
|
||||||
|
|
||||||
private _loadingTimeoutHandle?: number;
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return this.error
|
return this.error
|
||||||
? html`
|
? html`
|
||||||
@@ -55,35 +51,8 @@ class HaInitPage extends LitElement {
|
|||||||
The upgrade may need a long time to complete, please be
|
The upgrade may need a long time to complete, please be
|
||||||
patient.
|
patient.
|
||||||
`
|
`
|
||||||
: this._loadingTimeout
|
|
||||||
? html`
|
|
||||||
<p>Loading is taking longer than expected.</p>
|
|
||||||
<p class="hint">
|
|
||||||
This could be caused by a slow connection or cached data.
|
|
||||||
</p>
|
|
||||||
`
|
|
||||||
: "Loading data"}
|
: "Loading data"}
|
||||||
</div>
|
</div>
|
||||||
${this._loadingTimeout && !this.migration
|
|
||||||
? html`
|
|
||||||
<div class="button-row">
|
|
||||||
<ha-button
|
|
||||||
size="small"
|
|
||||||
appearance="plain"
|
|
||||||
@click=${this._retry}
|
|
||||||
>
|
|
||||||
Retry now
|
|
||||||
</ha-button>
|
|
||||||
<ha-button
|
|
||||||
size="small"
|
|
||||||
appearance="plain"
|
|
||||||
@click=${this._clearCacheAndRetry}
|
|
||||||
>
|
|
||||||
Clear cache and retry
|
|
||||||
</ha-button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,16 +64,10 @@ class HaInitPage extends LitElement {
|
|||||||
if (this._retryInterval) {
|
if (this._retryInterval) {
|
||||||
clearInterval(this._retryInterval);
|
clearInterval(this._retryInterval);
|
||||||
}
|
}
|
||||||
if (this._loadingTimeoutHandle) {
|
|
||||||
clearTimeout(this._loadingTimeoutHandle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||||
if (
|
if (changedProperties.has("error") && this.error) {
|
||||||
(changedProperties.has("error") && this.error) ||
|
|
||||||
(changedProperties.has("_loadingTimeout") && this._loadingTimeout)
|
|
||||||
) {
|
|
||||||
import("../components/ha-button");
|
import("../components/ha-button");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,22 +77,12 @@ class HaInitPage extends LitElement {
|
|||||||
import("../components/ha-spinner");
|
import("../components/ha-spinner");
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
// Only start retry interval for error state
|
this._retryInterval = window.setInterval(() => {
|
||||||
if (this.error) {
|
const remainingSeconds = this._retryInSeconds--;
|
||||||
this._retryInterval = window.setInterval(() => {
|
if (remainingSeconds <= 0) {
|
||||||
const remainingSeconds = this._retryInSeconds--;
|
this._retry();
|
||||||
if (remainingSeconds <= 0) {
|
}
|
||||||
this._retry();
|
}, 1000);
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start loading timeout for normal loading (not error, not migration)
|
|
||||||
if (!this.error && !this.migration) {
|
|
||||||
this._loadingTimeoutHandle = window.setTimeout(() => {
|
|
||||||
this._loadingTimeout = true;
|
|
||||||
}, 30000); // 30 seconds
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _retry() {
|
private _retry() {
|
||||||
@@ -139,25 +92,6 @@ class HaInitPage extends LitElement {
|
|||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _clearCacheAndRetry() {
|
|
||||||
// Deregister service worker and clear caches
|
|
||||||
if ("serviceWorker" in navigator) {
|
|
||||||
try {
|
|
||||||
const registration = await navigator.serviceWorker.getRegistration();
|
|
||||||
if (registration) {
|
|
||||||
await registration.unregister();
|
|
||||||
}
|
|
||||||
if ("caches" in window) {
|
|
||||||
const cacheNames = await caches.keys();
|
|
||||||
await Promise.all(cacheNames.map((name) => caches.delete(name)));
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
// Ignore errors, we'll reload anyway
|
|
||||||
}
|
|
||||||
}
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
flex: 0;
|
flex: 0;
|
||||||
@@ -177,23 +111,12 @@ class HaInitPage extends LitElement {
|
|||||||
.retry-text {
|
.retry-text {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.hint {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
p,
|
p,
|
||||||
#loading-text {
|
#loading-text {
|
||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.button-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import type { ReactiveElement } from "lit";
|
|
||||||
import { listenMediaQuery } from "../common/dom/media_query";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import type { Condition } from "../panels/lovelace/common/validate-condition";
|
|
||||||
import { checkConditionsMet } from "../panels/lovelace/common/validate-condition";
|
|
||||||
|
|
||||||
type Constructor<T> = abstract new (...args: any[]) => T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract media queries from conditions recursively
|
|
||||||
*/
|
|
||||||
export function extractMediaQueries(conditions: Condition[]): string[] {
|
|
||||||
return conditions.reduce<string[]>((array, c) => {
|
|
||||||
if ("conditions" in c && c.conditions) {
|
|
||||||
array.push(...extractMediaQueries(c.conditions));
|
|
||||||
}
|
|
||||||
if (c.condition === "screen" && c.media_query) {
|
|
||||||
array.push(c.media_query);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to setup media query listeners for conditional visibility
|
|
||||||
*/
|
|
||||||
export function setupMediaQueryListeners(
|
|
||||||
conditions: Condition[],
|
|
||||||
hass: HomeAssistant,
|
|
||||||
addListener: (unsub: () => void) => void,
|
|
||||||
onUpdate: (conditionsMet: boolean) => void
|
|
||||||
): void {
|
|
||||||
const mediaQueries = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
if (mediaQueries.length === 0) return;
|
|
||||||
|
|
||||||
// Optimization for single media query
|
|
||||||
const hasOnlyMediaQuery =
|
|
||||||
conditions.length === 1 &&
|
|
||||||
conditions[0].condition === "screen" &&
|
|
||||||
!!conditions[0].media_query;
|
|
||||||
|
|
||||||
mediaQueries.forEach((mediaQuery) => {
|
|
||||||
const unsub = listenMediaQuery(mediaQuery, (matches) => {
|
|
||||||
if (hasOnlyMediaQuery) {
|
|
||||||
onUpdate(matches);
|
|
||||||
} else {
|
|
||||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
|
||||||
onUpdate(conditionsMet);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
addListener(unsub);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mixin to handle conditional listeners for visibility control
|
|
||||||
*
|
|
||||||
* Provides lifecycle management for listeners (media queries, time-based, state changes, etc.)
|
|
||||||
* that control conditional visibility of components.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* 1. Extend your component with ConditionalListenerMixin(ReactiveElement)
|
|
||||||
* 2. Override setupConditionalListeners() to setup your listeners
|
|
||||||
* 3. Use addConditionalListener() to register unsubscribe functions
|
|
||||||
* 4. Call clearConditionalListeners() and setupConditionalListeners() when config changes
|
|
||||||
*
|
|
||||||
* The mixin automatically:
|
|
||||||
* - Sets up listeners when component connects to DOM
|
|
||||||
* - Cleans up listeners when component disconnects from DOM
|
|
||||||
*/
|
|
||||||
export const ConditionalListenerMixin = <
|
|
||||||
T extends Constructor<ReactiveElement>,
|
|
||||||
>(
|
|
||||||
superClass: T
|
|
||||||
) => {
|
|
||||||
abstract class ConditionalListenerClass extends superClass {
|
|
||||||
private __listeners: (() => void)[] = [];
|
|
||||||
|
|
||||||
public connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.setupConditionalListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this.clearConditionalListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected clearConditionalListeners(): void {
|
|
||||||
this.__listeners.forEach((unsub) => unsub());
|
|
||||||
this.__listeners = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addConditionalListener(unsubscribe: () => void): void {
|
|
||||||
this.__listeners.push(unsubscribe);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected setupConditionalListeners(): void {
|
|
||||||
// Override in subclass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ConditionalListenerClass;
|
|
||||||
};
|
|
||||||
@@ -20,8 +20,7 @@ import "../../../../components/ha-md-divider";
|
|||||||
import "../../../../components/ha-md-menu-item";
|
import "../../../../components/ha-md-menu-item";
|
||||||
import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
|
import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
|
||||||
import type { ActionSidebarConfig } from "../../../../data/automation";
|
import type { ActionSidebarConfig } from "../../../../data/automation";
|
||||||
import { domainToName } from "../../../../data/integration";
|
import type { RepeatAction } from "../../../../data/script";
|
||||||
import type { RepeatAction, ServiceAction } from "../../../../data/script";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { isMac } from "../../../../util/is_mac";
|
import { isMac } from "../../../../util/is_mac";
|
||||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
||||||
@@ -84,23 +83,11 @@ export default class HaAutomationSidebarAction extends LitElement {
|
|||||||
"ui.panel.config.automation.editor.actions.action"
|
"ui.panel.config.automation.editor.actions.action"
|
||||||
);
|
);
|
||||||
|
|
||||||
let title =
|
const title =
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
`ui.panel.config.automation.editor.actions.type.${type}.label` as LocalizeKeys
|
`ui.panel.config.automation.editor.actions.type.${type}.label` as LocalizeKeys
|
||||||
) || type;
|
) || type;
|
||||||
|
|
||||||
if (type === "service" && (actionConfig as ServiceAction).action) {
|
|
||||||
const [domain, service] = (actionConfig as ServiceAction).action!.split(
|
|
||||||
".",
|
|
||||||
2
|
|
||||||
);
|
|
||||||
title = `${domainToName(this.hass.localize, domain)}: ${
|
|
||||||
this.hass.localize(`component.${domain}.services.${service}.name`) ||
|
|
||||||
this.hass.services[domain][service]?.name ||
|
|
||||||
title
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const description = isBuildingBlock
|
const description = isBuildingBlock
|
||||||
? this.hass.localize(
|
? this.hass.localize(
|
||||||
`ui.panel.config.automation.editor.actions.type.${type}.description.picker` as LocalizeKeys
|
`ui.panel.config.automation.editor.actions.type.${type}.description.picker` as LocalizeKeys
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mdiTag, mdiPlus } from "@mdi/js";
|
import { mdiPlus, mdiTag } from "@mdi/js";
|
||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
@@ -194,8 +194,9 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.notFoundLabel=${this.hass.localize(
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
"ui.components.category-picker.no_match"
|
.emptyLabel=${this.hass.localize(
|
||||||
|
"ui.components.category-picker.no_categories"
|
||||||
)}
|
)}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
@@ -254,6 +255,11 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
|||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _notFoundLabel = (search: string) =>
|
||||||
|
this.hass.localize("ui.components.category-picker.no_match", {
|
||||||
|
term: html`<b>‘${search}’</b>`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import type { PropertyValues } from "lit";
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import type { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
import {
|
||||||
ConditionalListenerMixin,
|
attachConditionMediaQueriesListeners,
|
||||||
setupMediaQueryListeners,
|
checkConditionsMet,
|
||||||
} from "../../../mixins/conditional-listener-mixin";
|
} from "../common/validate-condition";
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
|
||||||
import { createBadgeElement } from "../create-element/create-badge-element";
|
import { createBadgeElement } from "../create-element/create-badge-element";
|
||||||
import { createErrorBadgeConfig } from "../create-element/create-element-base";
|
import { createErrorBadgeConfig } from "../create-element/create-element-base";
|
||||||
import type { LovelaceBadge } from "../types";
|
import type { LovelaceBadge } from "../types";
|
||||||
@@ -22,7 +22,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-badge")
|
@customElement("hui-badge")
|
||||||
export class HuiBadge extends ConditionalListenerMixin(ReactiveElement) {
|
export class HuiBadge extends ReactiveElement {
|
||||||
@property({ type: Boolean }) public preview = false;
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public config?: LovelaceBadgeConfig;
|
@property({ attribute: false }) public config?: LovelaceBadgeConfig;
|
||||||
@@ -40,16 +40,20 @@ export class HuiBadge extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
|
|
||||||
private _element?: LovelaceBadge;
|
private _element?: LovelaceBadge;
|
||||||
|
|
||||||
|
private _listeners: MediaQueriesListener[] = [];
|
||||||
|
|
||||||
protected createRenderRoot() {
|
protected createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
this._clearMediaQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
this._listenMediaQueries();
|
||||||
this._updateVisibility();
|
this._updateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,17 +137,26 @@ export class HuiBadge extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupConditionalListeners() {
|
private _clearMediaQueries() {
|
||||||
if (!this.config?.visibility || !this.hass) {
|
this._listeners.forEach((unsub) => unsub());
|
||||||
|
this._listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listenMediaQueries() {
|
||||||
|
this._clearMediaQueries();
|
||||||
|
if (!this.config?.visibility) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const conditions = this.config.visibility;
|
||||||
|
const hasOnlyMediaQuery =
|
||||||
|
conditions.length === 1 &&
|
||||||
|
conditions[0].condition === "screen" &&
|
||||||
|
!!conditions[0].media_query;
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
this._listeners = attachConditionMediaQueriesListeners(
|
||||||
this.config.visibility,
|
this.config.visibility,
|
||||||
this.hass,
|
(matches) => {
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
this._updateVisibility(hasOnlyMediaQuery && matches);
|
||||||
(conditionsMet) => {
|
|
||||||
this._updateVisibility(conditionsMet);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import type { PropertyValues } from "lit";
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import type { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
|
||||||
ConditionalListenerMixin,
|
|
||||||
setupMediaQueryListeners,
|
|
||||||
} from "../../../mixins/conditional-listener-mixin";
|
|
||||||
import { migrateLayoutToGridOptions } from "../common/compute-card-grid-size";
|
import { migrateLayoutToGridOptions } from "../common/compute-card-grid-size";
|
||||||
import { computeCardSize } from "../common/compute-card-size";
|
import { computeCardSize } from "../common/compute-card-size";
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
import {
|
||||||
|
attachConditionMediaQueriesListeners,
|
||||||
|
checkConditionsMet,
|
||||||
|
} from "../common/validate-condition";
|
||||||
import { tryCreateCardElement } from "../create-element/create-card-element";
|
import { tryCreateCardElement } from "../create-element/create-card-element";
|
||||||
import { createErrorCardElement } from "../create-element/create-element-base";
|
import { createErrorCardElement } from "../create-element/create-element-base";
|
||||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||||
@@ -24,7 +24,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-card")
|
@customElement("hui-card")
|
||||||
export class HuiCard extends ConditionalListenerMixin(ReactiveElement) {
|
export class HuiCard extends ReactiveElement {
|
||||||
@property({ type: Boolean }) public preview = false;
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public config?: LovelaceCardConfig;
|
@property({ attribute: false }) public config?: LovelaceCardConfig;
|
||||||
@@ -44,16 +44,20 @@ export class HuiCard extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
|
|
||||||
private _element?: LovelaceCard;
|
private _element?: LovelaceCard;
|
||||||
|
|
||||||
|
private _listeners: MediaQueriesListener[] = [];
|
||||||
|
|
||||||
protected createRenderRoot() {
|
protected createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
this._clearMediaQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
this._listenMediaQueries();
|
||||||
this._updateVisibility();
|
this._updateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,17 +251,26 @@ export class HuiCard extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupConditionalListeners() {
|
private _clearMediaQueries() {
|
||||||
if (!this.config?.visibility || !this.hass) {
|
this._listeners.forEach((unsub) => unsub());
|
||||||
|
this._listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listenMediaQueries() {
|
||||||
|
this._clearMediaQueries();
|
||||||
|
if (!this.config?.visibility) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const conditions = this.config.visibility;
|
||||||
|
const hasOnlyMediaQuery =
|
||||||
|
conditions.length === 1 &&
|
||||||
|
conditions[0].condition === "screen" &&
|
||||||
|
!!conditions[0].media_query;
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
this._listeners = attachConditionMediaQueriesListeners(
|
||||||
this.config.visibility,
|
this.config.visibility,
|
||||||
this.hass,
|
(matches) => {
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
this._updateVisibility(hasOnlyMediaQuery && matches);
|
||||||
(conditionsMet) => {
|
|
||||||
this._updateVisibility(conditionsMet);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ const calcPoints = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const rangeY = maxY - minY || minY * 0.1;
|
const rangeY = maxY - minY || minY * 0.1;
|
||||||
// add top and bottom margins to prevent cropping
|
|
||||||
maxY += rangeY * 0.1;
|
|
||||||
minY -= rangeY * 0.1;
|
|
||||||
if (maxY < 0) {
|
if (maxY < 0) {
|
||||||
// all values are negative
|
// all values are negative
|
||||||
|
// add margin
|
||||||
|
maxY += rangeY * 0.1;
|
||||||
maxY = Math.min(0, maxY);
|
maxY = Math.min(0, maxY);
|
||||||
yAxisOrigin = 0;
|
yAxisOrigin = 0;
|
||||||
} else if (minY < 0) {
|
} else if (minY < 0) {
|
||||||
@@ -32,6 +31,8 @@ const calcPoints = (
|
|||||||
yAxisOrigin = (maxY / (maxY - minY || 1)) * height;
|
yAxisOrigin = (maxY / (maxY - minY || 1)) * height;
|
||||||
} else {
|
} else {
|
||||||
// all values are positive
|
// all values are positive
|
||||||
|
// add margin
|
||||||
|
minY -= rangeY * 0.1;
|
||||||
minY = Math.max(0, minY);
|
minY = Math.max(0, minY);
|
||||||
}
|
}
|
||||||
const yDenom = maxY - minY || 1;
|
const yDenom = maxY - minY || 1;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { ensureArray } from "../../../common/array/ensure-array";
|
import { ensureArray } from "../../../common/array/ensure-array";
|
||||||
|
import type { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||||
|
import { listenMediaQuery } from "../../../common/dom/media_query";
|
||||||
|
|
||||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||||
import { UNKNOWN } from "../../../data/entity";
|
import { UNKNOWN } from "../../../data/entity";
|
||||||
@@ -360,3 +362,31 @@ export function addEntityToCondition(
|
|||||||
}
|
}
|
||||||
return condition;
|
return condition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractMediaQueries(conditions: Condition[]): string[] {
|
||||||
|
return conditions.reduce<string[]>((array, c) => {
|
||||||
|
if ("conditions" in c && c.conditions) {
|
||||||
|
array.push(...extractMediaQueries(c.conditions));
|
||||||
|
}
|
||||||
|
if (c.condition === "screen" && c.media_query) {
|
||||||
|
array.push(c.media_query);
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attachConditionMediaQueriesListeners(
|
||||||
|
conditions: Condition[],
|
||||||
|
onChange: (visibility: boolean) => void
|
||||||
|
): MediaQueriesListener[] {
|
||||||
|
const mediaQueries = extractMediaQueries(conditions);
|
||||||
|
|
||||||
|
const listeners = mediaQueries.map((query) => {
|
||||||
|
const listener = listenMediaQuery(query, (matches) => {
|
||||||
|
onChange(matches);
|
||||||
|
});
|
||||||
|
return listener;
|
||||||
|
});
|
||||||
|
|
||||||
|
return listeners;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import type { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||||
|
import { deepEqual } from "../../../common/util/deep-equal";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
|
||||||
ConditionalListenerMixin,
|
|
||||||
setupMediaQueryListeners,
|
|
||||||
} from "../../../mixins/conditional-listener-mixin";
|
|
||||||
import type { HuiCard } from "../cards/hui-card";
|
import type { HuiCard } from "../cards/hui-card";
|
||||||
import type { ConditionalCardConfig } from "../cards/types";
|
import type { ConditionalCardConfig } from "../cards/types";
|
||||||
import type { Condition } from "../common/validate-condition";
|
import type { Condition } from "../common/validate-condition";
|
||||||
import {
|
import {
|
||||||
|
attachConditionMediaQueriesListeners,
|
||||||
checkConditionsMet,
|
checkConditionsMet,
|
||||||
|
extractMediaQueries,
|
||||||
validateConditionalConfig,
|
validateConditionalConfig,
|
||||||
} from "../common/validate-condition";
|
} from "../common/validate-condition";
|
||||||
import type { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
|
import type { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
|
||||||
@@ -22,9 +22,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-conditional-base")
|
@customElement("hui-conditional-base")
|
||||||
export class HuiConditionalBase extends ConditionalListenerMixin(
|
export class HuiConditionalBase extends ReactiveElement {
|
||||||
ReactiveElement
|
|
||||||
) {
|
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Boolean }) public preview = false;
|
@property({ type: Boolean }) public preview = false;
|
||||||
@@ -33,6 +31,10 @@ export class HuiConditionalBase extends ConditionalListenerMixin(
|
|||||||
|
|
||||||
protected _element?: HuiCard | LovelaceRow;
|
protected _element?: HuiCard | LovelaceRow;
|
||||||
|
|
||||||
|
private _listeners: MediaQueriesListener[] = [];
|
||||||
|
|
||||||
|
private _mediaQueries: string[] = [];
|
||||||
|
|
||||||
protected createRenderRoot() {
|
protected createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -61,14 +63,21 @@ export class HuiConditionalBase extends ConditionalListenerMixin(
|
|||||||
|
|
||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
this._clearMediaQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
this._listenMediaQueries();
|
||||||
this._updateVisibility();
|
this._updateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupConditionalListeners() {
|
private _clearMediaQueries() {
|
||||||
|
this._listeners.forEach((unsub) => unsub());
|
||||||
|
this._listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listenMediaQueries() {
|
||||||
if (!this._config || !this.hass) {
|
if (!this._config || !this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -76,13 +85,27 @@ export class HuiConditionalBase extends ConditionalListenerMixin(
|
|||||||
const supportedConditions = this._config.conditions.filter(
|
const supportedConditions = this._config.conditions.filter(
|
||||||
(c) => "condition" in c
|
(c) => "condition" in c
|
||||||
) as Condition[];
|
) as Condition[];
|
||||||
|
const mediaQueries = extractMediaQueries(supportedConditions);
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
if (deepEqual(mediaQueries, this._mediaQueries)) return;
|
||||||
|
|
||||||
|
this._clearMediaQueries();
|
||||||
|
|
||||||
|
const conditions = this._config.conditions;
|
||||||
|
const hasOnlyMediaQuery =
|
||||||
|
conditions.length === 1 &&
|
||||||
|
"condition" in conditions[0] &&
|
||||||
|
conditions[0].condition === "screen" &&
|
||||||
|
!!conditions[0].media_query;
|
||||||
|
|
||||||
|
this._listeners = attachConditionMediaQueriesListeners(
|
||||||
supportedConditions,
|
supportedConditions,
|
||||||
this.hass,
|
(matches) => {
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
if (hasOnlyMediaQuery) {
|
||||||
(conditionsMet) => {
|
this.setVisibility(matches);
|
||||||
this.setVisibility(conditionsMet);
|
return;
|
||||||
|
}
|
||||||
|
this._updateVisibility();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -96,8 +119,7 @@ export class HuiConditionalBase extends ConditionalListenerMixin(
|
|||||||
changed.has("hass") ||
|
changed.has("hass") ||
|
||||||
changed.has("preview")
|
changed.has("preview")
|
||||||
) {
|
) {
|
||||||
this.clearConditionalListeners();
|
this._listenMediaQueries();
|
||||||
this.setupConditionalListeners();
|
|
||||||
this._updateVisibility();
|
this._updateVisibility();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import type { PropertyValues } from "lit";
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import type { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
import {
|
||||||
ConditionalListenerMixin,
|
attachConditionMediaQueriesListeners,
|
||||||
setupMediaQueryListeners,
|
checkConditionsMet,
|
||||||
} from "../../../mixins/conditional-listener-mixin";
|
} from "../common/validate-condition";
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
|
||||||
import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
|
import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
|
||||||
import type { LovelaceHeadingBadge } from "../types";
|
import type { LovelaceHeadingBadge } from "../types";
|
||||||
import type { LovelaceHeadingBadgeConfig } from "./types";
|
import type { LovelaceHeadingBadgeConfig } from "./types";
|
||||||
@@ -21,7 +21,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-heading-badge")
|
@customElement("hui-heading-badge")
|
||||||
export class HuiHeadingBadge extends ConditionalListenerMixin(ReactiveElement) {
|
export class HuiHeadingBadge extends ReactiveElement {
|
||||||
@property({ type: Boolean }) public preview = false;
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public config?: LovelaceHeadingBadgeConfig;
|
@property({ attribute: false }) public config?: LovelaceHeadingBadgeConfig;
|
||||||
@@ -39,16 +39,20 @@ export class HuiHeadingBadge extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
|
|
||||||
private _element?: LovelaceHeadingBadge;
|
private _element?: LovelaceHeadingBadge;
|
||||||
|
|
||||||
|
private _listeners: MediaQueriesListener[] = [];
|
||||||
|
|
||||||
protected createRenderRoot() {
|
protected createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
this._clearMediaQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
this._listenMediaQueries();
|
||||||
this._updateVisibility();
|
this._updateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,17 +137,26 @@ export class HuiHeadingBadge extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupConditionalListeners() {
|
private _clearMediaQueries() {
|
||||||
if (!this.config?.visibility || !this.hass) {
|
this._listeners.forEach((unsub) => unsub());
|
||||||
|
this._listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listenMediaQueries() {
|
||||||
|
this._clearMediaQueries();
|
||||||
|
if (!this.config?.visibility) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const conditions = this.config.visibility;
|
||||||
|
const hasOnlyMediaQuery =
|
||||||
|
conditions.length === 1 &&
|
||||||
|
conditions[0].condition === "screen" &&
|
||||||
|
!!conditions[0].media_query;
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
this._listeners = attachConditionMediaQueriesListeners(
|
||||||
this.config.visibility,
|
this.config.visibility,
|
||||||
this.hass,
|
(matches) => {
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
this._updateVisibility(hasOnlyMediaQuery && matches);
|
||||||
(conditionsMet) => {
|
|
||||||
this._updateVisibility(conditionsMet);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ReactiveElement } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { storage } from "../../../common/decorators/storage";
|
import { storage } from "../../../common/decorators/storage";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import type { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { LovelaceSectionElement } from "../../../data/lovelace";
|
import type { LovelaceSectionElement } from "../../../data/lovelace";
|
||||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
@@ -13,13 +14,12 @@ import type {
|
|||||||
} from "../../../data/lovelace/config/section";
|
} from "../../../data/lovelace/config/section";
|
||||||
import { isStrategySection } from "../../../data/lovelace/config/section";
|
import { isStrategySection } from "../../../data/lovelace/config/section";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
|
||||||
ConditionalListenerMixin,
|
|
||||||
setupMediaQueryListeners,
|
|
||||||
} from "../../../mixins/conditional-listener-mixin";
|
|
||||||
import "../cards/hui-card";
|
import "../cards/hui-card";
|
||||||
import type { HuiCard } from "../cards/hui-card";
|
import type { HuiCard } from "../cards/hui-card";
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
import {
|
||||||
|
attachConditionMediaQueriesListeners,
|
||||||
|
checkConditionsMet,
|
||||||
|
} from "../common/validate-condition";
|
||||||
import { createSectionElement } from "../create-element/create-section-element";
|
import { createSectionElement } from "../create-element/create-section-element";
|
||||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||||
@@ -37,7 +37,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-section")
|
@customElement("hui-section")
|
||||||
export class HuiSection extends ConditionalListenerMixin(ReactiveElement) {
|
export class HuiSection extends ReactiveElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public config!: LovelaceSectionRawConfig;
|
@property({ attribute: false }) public config!: LovelaceSectionRawConfig;
|
||||||
@@ -59,6 +59,8 @@ export class HuiSection extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
|
|
||||||
private _layoutElement?: LovelaceSectionElement;
|
private _layoutElement?: LovelaceSectionElement;
|
||||||
|
|
||||||
|
private _listeners: MediaQueriesListener[] = [];
|
||||||
|
|
||||||
private _config: LovelaceSectionConfig | undefined;
|
private _config: LovelaceSectionConfig | undefined;
|
||||||
|
|
||||||
@storage({
|
@storage({
|
||||||
@@ -112,10 +114,14 @@ export class HuiSection extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
|
|
||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
this._clearMediaQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
if (this.hasUpdated) {
|
||||||
|
this._listenMediaQueries();
|
||||||
|
}
|
||||||
this._updateElement();
|
this._updateElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,17 +158,26 @@ export class HuiSection extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupConditionalListeners() {
|
private _clearMediaQueries() {
|
||||||
if (!this._config?.visibility || !this.hass) {
|
this._listeners.forEach((unsub) => unsub());
|
||||||
|
this._listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listenMediaQueries() {
|
||||||
|
this._clearMediaQueries();
|
||||||
|
if (!this._config?.visibility) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const conditions = this._config.visibility;
|
||||||
|
const hasOnlyMediaQuery =
|
||||||
|
conditions.length === 1 &&
|
||||||
|
conditions[0].condition === "screen" &&
|
||||||
|
conditions[0].media_query != null;
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
this._listeners = attachConditionMediaQueriesListeners(
|
||||||
this._config.visibility,
|
this._config.visibility,
|
||||||
this.hass,
|
(matches) => {
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
this._updateElement(hasOnlyMediaQuery && matches);
|
||||||
(conditionsMet) => {
|
|
||||||
this._updateElement(conditionsMet);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -184,6 +199,9 @@ export class HuiSection extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
type: sectionConfig.type || DEFAULT_SECTION_LAYOUT,
|
type: sectionConfig.type || DEFAULT_SECTION_LAYOUT,
|
||||||
};
|
};
|
||||||
this._config = sectionConfig;
|
this._config = sectionConfig;
|
||||||
|
if (this.isConnected) {
|
||||||
|
this._listenMediaQueries();
|
||||||
|
}
|
||||||
|
|
||||||
// Create a new layout element if necessary.
|
// Create a new layout element if necessary.
|
||||||
let addLayoutElement = false;
|
let addLayoutElement = false;
|
||||||
|
|||||||
@@ -652,7 +652,7 @@
|
|||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"no_entities": "You don't have any entities",
|
"no_entities": "You don't have any entities",
|
||||||
"no_match": "No matching entities found",
|
"no_match": "No entities found for {term}",
|
||||||
"show_entities": "Show entities",
|
"show_entities": "Show entities",
|
||||||
"new_entity": "Create a new entity",
|
"new_entity": "Create a new entity",
|
||||||
"placeholder": "Select an entity",
|
"placeholder": "Select an entity",
|
||||||
@@ -763,7 +763,7 @@
|
|||||||
},
|
},
|
||||||
"language-picker": {
|
"language-picker": {
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"no_match": "No matching languages found",
|
"no_match": "No languages found for {term}",
|
||||||
"no_languages": "No languages available"
|
"no_languages": "No languages available"
|
||||||
},
|
},
|
||||||
"tts-picker": {
|
"tts-picker": {
|
||||||
@@ -775,7 +775,7 @@
|
|||||||
"none": "None"
|
"none": "None"
|
||||||
},
|
},
|
||||||
"user-picker": {
|
"user-picker": {
|
||||||
"no_match": "No matching users found",
|
"no_match": "No users found for {term}",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"add_user": "Add user"
|
"add_user": "Add user"
|
||||||
},
|
},
|
||||||
@@ -786,8 +786,8 @@
|
|||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"toggle": "Toggle",
|
"toggle": "Toggle",
|
||||||
"show_devices": "Show devices",
|
"show_devices": "Show devices",
|
||||||
"no_devices": "You don't have any devices",
|
"no_devices": "No devices available",
|
||||||
"no_match": "No matching devices found",
|
"no_match": "No devices found for {term}",
|
||||||
"device": "Device",
|
"device": "Device",
|
||||||
"unnamed_device": "Unnamed device",
|
"unnamed_device": "Unnamed device",
|
||||||
"no_area": "No area",
|
"no_area": "No area",
|
||||||
@@ -801,8 +801,8 @@
|
|||||||
"add_category": "Add category",
|
"add_category": "Add category",
|
||||||
"add_new_sugestion": "Add new category ''{name}''",
|
"add_new_sugestion": "Add new category ''{name}''",
|
||||||
"add_new": "Add new category…",
|
"add_new": "Add new category…",
|
||||||
"no_categories": "You don't have any categories",
|
"no_categories": "No categories available",
|
||||||
"no_match": "No matching categories found",
|
"no_match": "No categories found for {term}",
|
||||||
"add_dialog": {
|
"add_dialog": {
|
||||||
"title": "Add new category",
|
"title": "Add new category",
|
||||||
"text": "Enter the name of the new category.",
|
"text": "Enter the name of the new category.",
|
||||||
@@ -817,8 +817,8 @@
|
|||||||
"add_new_sugestion": "Add new label ''{name}''",
|
"add_new_sugestion": "Add new label ''{name}''",
|
||||||
"add_new": "Add new label…",
|
"add_new": "Add new label…",
|
||||||
"add": "Add label",
|
"add": "Add label",
|
||||||
"no_labels": "You don't have any labels",
|
"no_labels": "No labels available",
|
||||||
"no_match": "No matching labels found",
|
"no_match": "No labels found for {term}",
|
||||||
"failed_create_label": "Failed to create label."
|
"failed_create_label": "Failed to create label."
|
||||||
},
|
},
|
||||||
"area-picker": {
|
"area-picker": {
|
||||||
@@ -827,8 +827,8 @@
|
|||||||
"area": "Area",
|
"area": "Area",
|
||||||
"add_new_sugestion": "Add new area ''{name}''",
|
"add_new_sugestion": "Add new area ''{name}''",
|
||||||
"add_new": "Add new area…",
|
"add_new": "Add new area…",
|
||||||
"no_areas": "You don't have any areas",
|
"no_areas": "No areas available",
|
||||||
"no_match": "No matching areas found",
|
"no_match": "No areas found for {term}",
|
||||||
"unassigned_areas": "Unassigned areas",
|
"unassigned_areas": "Unassigned areas",
|
||||||
"failed_create_area": "Failed to create area."
|
"failed_create_area": "Failed to create area."
|
||||||
},
|
},
|
||||||
@@ -838,8 +838,8 @@
|
|||||||
"floor": "Floor",
|
"floor": "Floor",
|
||||||
"add_new_sugestion": "Add new floor ''{name}''",
|
"add_new_sugestion": "Add new floor ''{name}''",
|
||||||
"add_new": "Add new floor…",
|
"add_new": "Add new floor…",
|
||||||
"no_floors": "You don't have any floors",
|
"no_floors": "No floors available",
|
||||||
"no_match": "No matching floors found",
|
"no_match": "No floors found for {term}",
|
||||||
"failed_create_floor": "Failed to create floor."
|
"failed_create_floor": "Failed to create floor."
|
||||||
},
|
},
|
||||||
"area-filter": {
|
"area-filter": {
|
||||||
@@ -853,8 +853,8 @@
|
|||||||
"statistic-picker": {
|
"statistic-picker": {
|
||||||
"statistic": "Statistic",
|
"statistic": "Statistic",
|
||||||
"placeholder": "Select a statistic",
|
"placeholder": "Select a statistic",
|
||||||
"no_statistics": "You don't have any statistics",
|
"no_statistics": "No statistics available",
|
||||||
"no_match": "No matching statistics found",
|
"no_match": "No statistics found for {term}",
|
||||||
"no_state": "Entity without state",
|
"no_state": "Entity without state",
|
||||||
"missing_entity": "Why is my entity not listed?",
|
"missing_entity": "Why is my entity not listed?",
|
||||||
"learn_more": "Learn more about statistics"
|
"learn_more": "Learn more about statistics"
|
||||||
@@ -1292,7 +1292,8 @@
|
|||||||
"add": "Add interaction"
|
"add": "Add interaction"
|
||||||
},
|
},
|
||||||
"combo-box": {
|
"combo-box": {
|
||||||
"no_match": "No matching items found"
|
"no_match": "No matching items found",
|
||||||
|
"no_items": "No items available"
|
||||||
},
|
},
|
||||||
"suggest_with_ai": {
|
"suggest_with_ai": {
|
||||||
"label": "Suggest",
|
"label": "Suggest",
|
||||||
|
|||||||
Reference in New Issue
Block a user