diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 5fc1bfff80..fb837d15b2 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -26,6 +26,10 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { ValueChangedEvent, HomeAssistant } from "../../types"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; +import { + fuzzyFilterSort, + ScorableTextItem, +} from "../../common/string/filter/sequence-matching"; interface Device { name: string; @@ -33,6 +37,8 @@ interface Device { id: string; } +type ScorableDevice = ScorableTextItem & Device; + export type HaDevicePickerDeviceFilterFunc = ( device: DeviceRegistryEntry ) => boolean; @@ -119,13 +125,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { deviceFilter: this["deviceFilter"], entityFilter: this["entityFilter"], excludeDevices: this["excludeDevices"] - ): Device[] => { + ): ScorableDevice[] => { if (!devices.length) { return [ { id: "no_devices", area: "", name: this.hass.localize("ui.components.device-picker.no_devices"), + strings: [], }, ]; } @@ -235,6 +242,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { device.area_id && areaLookup[device.area_id] ? areaLookup[device.area_id].name : this.hass.localize("ui.components.device-picker.no_area"), + strings: [device.name || ""], })); if (!outputDevices.length) { return [ @@ -242,6 +250,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { id: "no_devices", area: "", name: this.hass.localize("ui.components.device-picker.no_match"), + strings: [], }, ]; } @@ -284,7 +293,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { (this._init && changedProps.has("_opened") && this._opened) ) { this._init = true; - (this.comboBox as any).items = this._getDevices( + const devices = this._getDevices( this.devices!, this.areas!, this.entities!, @@ -295,6 +304,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { this.entityFilter, this.excludeDevices ); + this.comboBox.items = devices; + this.comboBox.filteredItems = devices; } } @@ -314,6 +325,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { item-label-path="name" @opened-changed=${this._openedChanged} @value-changed=${this._deviceChanged} + @filter-changed=${this._filterChanged} > `; } @@ -322,6 +334,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { return this.value || ""; } + private _filterChanged(ev: CustomEvent): void { + const target = ev.target as HaComboBox; + const filterString = ev.detail.value.toLowerCase(); + target.filteredItems = filterString.length + ? fuzzyFilterSort(filterString, target.items || []) + : target.items; + } + private _deviceChanged(ev: ValueChangedEvent) { ev.stopPropagation(); let newValue = ev.detail.value; diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index c06d2cd5a9..115e0795aa 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -7,15 +7,19 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; -import { caseInsensitiveStringCompare } from "../../common/string/compare"; +import { + fuzzyFilterSort, + ScorableTextItem, +} from "../../common/string/filter/sequence-matching"; import { ValueChangedEvent, HomeAssistant } from "../../types"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; import "../ha-icon-button"; import "../ha-svg-icon"; import "./state-badge"; +import { caseInsensitiveStringCompare } from "../../common/string/compare"; -interface HassEntityWithCachedName extends HassEntity { +interface HassEntityWithCachedName extends HassEntity, ScorableTextItem { friendly_name: string; } @@ -159,6 +163,7 @@ export class HaEntityPicker extends LitElement { ), icon: "mdi:magnify", }, + strings: [], }, ]; } @@ -169,10 +174,14 @@ export class HaEntityPicker extends LitElement { ); return entityIds - .map((key) => ({ - ...hass!.states[key], - friendly_name: computeStateName(hass!.states[key]) || key, - })) + .map((key) => { + const friendly_name = computeStateName(hass!.states[key]) || key; + return { + ...hass!.states[key], + friendly_name, + strings: [key, friendly_name], + }; + }) .sort((entityA, entityB) => caseInsensitiveStringCompare( entityA.friendly_name, @@ -201,10 +210,14 @@ export class HaEntityPicker extends LitElement { } states = entityIds - .map((key) => ({ - ...hass!.states[key], - friendly_name: computeStateName(hass!.states[key]) || key, - })) + .map((key) => { + const friendly_name = computeStateName(hass!.states[key]) || key; + return { + ...hass!.states[key], + friendly_name, + strings: [key, friendly_name], + }; + }) .sort((entityA, entityB) => caseInsensitiveStringCompare( entityA.friendly_name, @@ -260,6 +273,7 @@ export class HaEntityPicker extends LitElement { ), icon: "mdi:magnify", }, + strings: [], }, ]; } @@ -293,7 +307,7 @@ export class HaEntityPicker extends LitElement { this.excludeEntities ); if (this._initedStates) { - (this.comboBox as any).filteredItems = this._states; + this.comboBox.filteredItems = this._states; } this._initedStates = true; } @@ -340,12 +354,11 @@ export class HaEntityPicker extends LitElement { } private _filterChanged(ev: CustomEvent): void { + const target = ev.target as HaComboBox; const filterString = ev.detail.value.toLowerCase(); - (this.comboBox as any).filteredItems = this._states.filter( - (entityState) => - entityState.entity_id.toLowerCase().includes(filterString) || - computeStateName(entityState).toLowerCase().includes(filterString) - ); + target.filteredItems = filterString.length + ? fuzzyFilterSort(filterString, this._states) + : this._states; } private _setValue(value: string) { diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index 9c06dd8015..1ea409aafe 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -7,6 +7,10 @@ import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; import { computeDomain } from "../common/entity/compute_domain"; +import { + fuzzyFilterSort, + ScorableTextItem, +} from "../common/string/filter/sequence-matching"; import { AreaRegistryEntry, createAreaRegistryEntry, @@ -28,6 +32,8 @@ import type { HaComboBox } from "./ha-combo-box"; import "./ha-icon-button"; import "./ha-svg-icon"; +type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry; + const rowRenderer: ComboBoxLitRenderer = ( item ) => html` ({ + ...area, + strings: [area.area_id, ...area.aliases, area.name], + })); + this.comboBox.items = areas; + this.comboBox.filteredItems = areas; } } @@ -345,8 +354,9 @@ export class HaAreaPicker extends LitElement { return; } - const filteredItems = this.comboBox.items?.filter((item) => - item.name.toLowerCase().includes(filter!.toLowerCase()) + const filteredItems = fuzzyFilterSort( + filter, + this.comboBox?.items || [] ); if (!this.noAdd && filteredItems?.length === 0) { this._suggestion = filter; @@ -409,7 +419,7 @@ export class HaAreaPicker extends LitElement { name, }); const areas = [...Object.values(this.hass.areas), area]; - (this.comboBox as any).filteredItems = this._getAreas( + this.comboBox.filteredItems = this._getAreas( areas, Object.values(this.hass.devices)!, Object.values(this.hass.entities)!,