Files
frontend/src/components/ha-areas-floors-display-editor.ts
2025-10-16 11:33:25 +02:00

288 lines
8.8 KiB
TypeScript

import { mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context";
import { areaCompare } from "../data/area_registry";
import type { FloorRegistryEntry } from "../data/floor_registry";
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
import type { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-floor-icon";
import "./ha-items-display-editor";
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
import "./ha-svg-icon";
import "./ha-textfield";
export interface AreasFloorsDisplayValue {
areas_display?: {
hidden?: string[];
order?: string[];
};
floors_display?: {
order?: string[];
};
}
const UNASSIGNED_FLOOR = "__unassigned__";
@customElement("ha-areas-floors-display-editor")
export class HaAreasFloorsDisplayEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property({ attribute: false }) public value?: AreasFloorsDisplayValue;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ attribute: false }) public actionsRenderer?: () =>
| TemplateResult<1>
| typeof nothing;
@property({ type: Boolean, attribute: "show-navigation-button" })
public showNavigationButton = false;
protected render(): TemplateResult {
const groupedAreasItems = this._groupedAreasItems(
this.hass.areas,
this.hass.floors
);
const filteredFloors = this._sortedFloors(
this.hass.floors,
this.value?.floors_display?.order
).filter(
(floor) =>
// Only include floors that have areas assigned to them
groupedAreasItems[floor.floor_id]?.length > 0
);
const value: DisplayValue = {
order: this.value?.areas_display?.order ?? [],
hidden: this.value?.areas_display?.hidden ?? [],
};
const canReorderFloors =
filteredFloors.filter((floor) => floor.floor_id !== UNASSIGNED_FLOOR)
.length > 1;
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-sortable
draggable-selector=".draggable"
handle-selector=".handle"
@item-moved=${this._floorMoved}
.disabled=${this.disabled || !canReorderFloors}
invert-swap
>
<div>
${repeat(
filteredFloors,
(floor) => floor.floor_id,
(floor: FloorRegistryEntry) => html`
<ha-expansion-panel
outlined
.header=${computeFloorName(floor)}
left-chevron
class=${floor.floor_id === UNASSIGNED_FLOOR ? "" : "draggable"}
>
<ha-floor-icon
slot="leading-icon"
.floor=${floor}
></ha-floor-icon>
${floor.floor_id === UNASSIGNED_FLOOR || !canReorderFloors
? nothing
: html`
<ha-svg-icon
class="handle"
slot="icons"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
`}
<ha-items-display-editor
.hass=${this.hass}
.items=${groupedAreasItems[floor.floor_id]}
.value=${value}
.floorId=${floor.floor_id}
.actionsRenderer=${this.actionsRenderer}
@value-changed=${this._areaDisplayChanged}
.showNavigationButton=${this.showNavigationButton}
></ha-items-display-editor>
</ha-expansion-panel>
`
)}
</div>
</ha-sortable>
`;
}
private _groupedAreasItems = memoizeOne(
(
hassAreas: HomeAssistant["areas"],
// update items if floors change
_hassFloors: HomeAssistant["floors"]
): Record<string, DisplayItem[]> => {
const compare = areaCompare(hassAreas);
const areas = Object.values(hassAreas).sort((areaA, areaB) =>
compare(areaA.area_id, areaB.area_id)
);
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
(acc, area) => {
const { floor } = getAreaContext(area, this.hass.floors);
const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
if (!acc[floorId]) {
acc[floorId] = [];
}
acc[floorId].push({
value: area.area_id,
label: area.name,
icon: area.icon ?? undefined,
iconPath: mdiTextureBox,
});
return acc;
},
{} as Record<string, DisplayItem[]>
);
return groupedItems;
}
);
private _sortedFloors = memoizeOne(
(
hassFloors: HomeAssistant["floors"],
order: string[] | undefined
): FloorRegistryEntry[] => {
const floors = getFloors(hassFloors, order);
const noFloors = floors.length === 0;
floors.push({
floor_id: UNASSIGNED_FLOOR,
name: noFloors
? this.hass.localize("ui.panel.lovelace.strategy.areas.areas")
: this.hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
icon: null,
level: null,
aliases: [],
created_at: 0,
modified_at: 0,
});
return floors;
}
);
private _floorMoved(ev: CustomEvent<HASSDomEvents["item-moved"]>) {
ev.stopPropagation();
const newIndex = ev.detail.newIndex;
const oldIndex = ev.detail.oldIndex;
const floorIds = this._sortedFloors(
this.hass.floors,
this.value?.floors_display?.order
).map((floor) => floor.floor_id);
const newOrder = [...floorIds];
const movedFloorId = newOrder.splice(oldIndex, 1)[0];
newOrder.splice(newIndex, 0, movedFloorId);
const newValue: AreasFloorsDisplayValue = {
areas_display: this.value?.areas_display,
floors_display: {
order: newOrder,
},
};
if (newValue.floors_display?.order?.length === 0) {
delete newValue.floors_display.order;
}
fireEvent(this, "value-changed", { value: newValue });
}
private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) {
ev.stopPropagation();
const value = ev.detail.value;
const currentFloorId = (ev.currentTarget as any).floorId;
const floorIds = this._sortedFloors(
this.hass.floors,
this.value?.floors_display?.order
).map((floor) => floor.floor_id);
const oldAreaDisplay = this.value?.areas_display ?? {};
const oldHidden = oldAreaDisplay?.hidden ?? [];
const oldOrder = oldAreaDisplay?.order ?? [];
const newHidden: string[] = [];
const newOrder: string[] = [];
for (const floorId of floorIds) {
if ((currentFloorId ?? UNASSIGNED_FLOOR) === floorId) {
newHidden.push(...(value.hidden ?? []));
newOrder.push(...(value.order ?? []));
continue;
}
const hidden = oldHidden.filter((areaId) => {
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
return id === floorId;
});
if (hidden?.length) {
newHidden.push(...hidden);
}
const order = oldOrder.filter((areaId) => {
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
return id === floorId;
});
if (order?.length) {
newOrder.push(...order);
}
}
const newValue: AreasFloorsDisplayValue = {
areas_display: {
hidden: newHidden,
order: newOrder,
},
floors_display: this.value?.floors_display,
};
if (newValue.areas_display?.hidden?.length === 0) {
delete newValue.areas_display.hidden;
}
if (newValue.areas_display?.order?.length === 0) {
delete newValue.areas_display.order;
}
if (newValue.floors_display?.order?.length === 0) {
delete newValue.floors_display.order;
}
fireEvent(this, "value-changed", { value: newValue });
}
static styles = css`
ha-expansion-panel {
margin-bottom: 8px;
--expansion-panel-summary-padding: 0 16px;
}
ha-expansion-panel [slot="leading-icon"] {
margin-inline-end: 16px;
}
label {
display: block;
font-weight: var(--ha-font-weight-bold);
margin-bottom: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-areas-floors-display-editor": HaAreasFloorsDisplayEditor;
}
}