mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-19 10:57:19 +00:00
258 lines
7.1 KiB
TypeScript
258 lines
7.1 KiB
TypeScript
import "@material/mwc-list/mwc-list";
|
|
import { mdiFilterVariantRemove } from "@mdi/js";
|
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
|
import { css, html, LitElement, nothing } from "lit";
|
|
import { customElement, property, state } from "lit/decorators";
|
|
import memoizeOne from "memoize-one";
|
|
import { fireEvent } from "../common/dom/fire_event";
|
|
import { stringCompare } from "../common/string/compare";
|
|
import { computeDeviceName } from "../data/device_registry";
|
|
import type { RelatedResult } from "../data/search";
|
|
import { findRelated } from "../data/search";
|
|
import { haStyleScrollbar } from "../resources/styles";
|
|
import { loadVirtualizer } from "../resources/virtualizer";
|
|
import type { HomeAssistant } from "../types";
|
|
import "./ha-check-list-item";
|
|
import "./ha-expansion-panel";
|
|
import "./search-input-outlined";
|
|
|
|
@customElement("ha-filter-devices")
|
|
export class HaFilterDevices extends LitElement {
|
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
|
|
@property({ attribute: false }) public value?: string[];
|
|
|
|
@property() public type?: keyof RelatedResult;
|
|
|
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
|
|
|
@property({ type: Boolean }) public narrow = false;
|
|
|
|
@state() private _shouldRender = false;
|
|
|
|
@state() private _filter?: string;
|
|
|
|
public willUpdate(properties: PropertyValues) {
|
|
super.willUpdate(properties);
|
|
|
|
if (!this.hasUpdated) {
|
|
loadVirtualizer();
|
|
if (this.value?.length) {
|
|
this._findRelated();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected render() {
|
|
return html`
|
|
<ha-expansion-panel
|
|
left-chevron
|
|
.expanded=${this.expanded}
|
|
@expanded-will-change=${this._expandedWillChange}
|
|
@expanded-changed=${this._expandedChanged}
|
|
>
|
|
<div slot="header" class="header">
|
|
${this.hass.localize("ui.panel.config.devices.caption")}
|
|
${this.value?.length
|
|
? html`<div class="badge">${this.value?.length}</div>
|
|
<ha-icon-button
|
|
.path=${mdiFilterVariantRemove}
|
|
@click=${this._clearFilter}
|
|
></ha-icon-button>`
|
|
: nothing}
|
|
</div>
|
|
${this._shouldRender
|
|
? html`<search-input-outlined
|
|
.hass=${this.hass}
|
|
.filter=${this._filter}
|
|
@value-changed=${this._handleSearchChange}
|
|
>
|
|
</search-input-outlined>
|
|
<mwc-list class="ha-scrollbar" multi>
|
|
<lit-virtualizer
|
|
.items=${this._devices(
|
|
this.hass.devices,
|
|
this._filter || "",
|
|
this.value
|
|
)}
|
|
.keyFunction=${this._keyFunction}
|
|
.renderItem=${this._renderItem}
|
|
@click=${this._handleItemClick}
|
|
>
|
|
</lit-virtualizer>
|
|
</mwc-list>`
|
|
: nothing}
|
|
</ha-expansion-panel>
|
|
`;
|
|
}
|
|
|
|
private _keyFunction = (device) => device?.id;
|
|
|
|
private _renderItem = (device) =>
|
|
!device
|
|
? nothing
|
|
: html`<ha-check-list-item
|
|
.value=${device.id}
|
|
.selected=${this.value?.includes(device.id) ?? false}
|
|
>
|
|
${computeDeviceName(device, this.hass)}
|
|
</ha-check-list-item>`;
|
|
|
|
private _handleItemClick(ev) {
|
|
const listItem = ev.target.closest("ha-check-list-item");
|
|
const value = listItem?.value;
|
|
if (!value) {
|
|
return;
|
|
}
|
|
if (this.value?.includes(value)) {
|
|
this.value = this.value?.filter((val) => val !== value);
|
|
} else {
|
|
this.value = [...(this.value || []), value];
|
|
}
|
|
listItem.selected = this.value?.includes(value);
|
|
this._findRelated();
|
|
}
|
|
|
|
protected updated(changed) {
|
|
if (changed.has("expanded") && this.expanded) {
|
|
setTimeout(() => {
|
|
if (!this.expanded) return;
|
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
|
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
private _expandedWillChange(ev) {
|
|
this._shouldRender = ev.detail.expanded;
|
|
}
|
|
|
|
private _expandedChanged(ev) {
|
|
this.expanded = ev.detail.expanded;
|
|
}
|
|
|
|
private _handleSearchChange(ev: CustomEvent) {
|
|
this._filter = ev.detail.value.toLowerCase();
|
|
}
|
|
|
|
private _devices = memoizeOne(
|
|
(devices: HomeAssistant["devices"], filter: string, _value) => {
|
|
const values = Object.values(devices);
|
|
return values
|
|
.filter(
|
|
(device) =>
|
|
!filter ||
|
|
computeDeviceName(device, this.hass).toLowerCase().includes(filter)
|
|
)
|
|
.sort((a, b) =>
|
|
stringCompare(
|
|
computeDeviceName(a, this.hass),
|
|
computeDeviceName(b, this.hass),
|
|
this.hass.locale.language
|
|
)
|
|
);
|
|
}
|
|
);
|
|
|
|
private async _findRelated() {
|
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
|
|
|
if (!this.value?.length) {
|
|
fireEvent(this, "data-table-filter-changed", {
|
|
value: [],
|
|
items: undefined,
|
|
});
|
|
this.value = [];
|
|
return;
|
|
}
|
|
|
|
const value: string[] = [];
|
|
|
|
for (const deviceId of this.value) {
|
|
value.push(deviceId);
|
|
if (this.type) {
|
|
relatedPromises.push(findRelated(this.hass, "device", deviceId));
|
|
}
|
|
}
|
|
this.value = value;
|
|
const results = await Promise.all(relatedPromises);
|
|
const items = new Set<string>();
|
|
for (const result of results) {
|
|
if (result[this.type!]) {
|
|
result[this.type!]!.forEach((item) => items.add(item));
|
|
}
|
|
}
|
|
|
|
fireEvent(this, "data-table-filter-changed", {
|
|
value,
|
|
items: this.type ? items : undefined,
|
|
});
|
|
}
|
|
|
|
private _clearFilter(ev) {
|
|
ev.preventDefault();
|
|
this.value = undefined;
|
|
fireEvent(this, "data-table-filter-changed", {
|
|
value: undefined,
|
|
items: undefined,
|
|
});
|
|
}
|
|
|
|
static get styles(): CSSResultGroup {
|
|
return [
|
|
haStyleScrollbar,
|
|
css`
|
|
:host {
|
|
border-bottom: 1px solid var(--divider-color);
|
|
}
|
|
:host([expanded]) {
|
|
flex: 1;
|
|
height: 0;
|
|
}
|
|
|
|
ha-expansion-panel {
|
|
--ha-card-border-radius: 0;
|
|
--expansion-panel-content-padding: 0;
|
|
}
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.header ha-icon-button {
|
|
margin-inline-start: auto;
|
|
margin-inline-end: 8px;
|
|
}
|
|
.badge {
|
|
display: inline-block;
|
|
margin-left: 8px;
|
|
margin-inline-start: 8px;
|
|
margin-inline-end: 0;
|
|
min-width: 16px;
|
|
box-sizing: border-box;
|
|
border-radius: 50%;
|
|
font-weight: 400;
|
|
font-size: 11px;
|
|
background-color: var(--primary-color);
|
|
line-height: 16px;
|
|
text-align: center;
|
|
padding: 0px 2px;
|
|
color: var(--text-primary-color);
|
|
}
|
|
ha-check-list-item {
|
|
width: 100%;
|
|
}
|
|
search-input-outlined {
|
|
display: block;
|
|
padding: 0 8px;
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"ha-filter-devices": HaFilterDevices;
|
|
}
|
|
}
|