Custom Lovelace View Layouts (#6557)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Zack Barett 2020-09-30 04:06:03 -05:00 committed by GitHub
parent 4f2bad034a
commit 771c7518e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 537 additions and 351 deletions

View File

@ -9,7 +9,6 @@ import {
} from "lit-element";
import { LovelaceConfig } from "../../../../src/data/lovelace";
import { Lovelace } from "../../../../src/panels/lovelace/types";
import "../../../../src/panels/lovelace/views/hui-panel-view";
import "../../../../src/panels/lovelace/views/hui-view";
import { HomeAssistant } from "../../../../src/types";
import "./hc-launch-screen";
@ -45,15 +44,7 @@ class HcLovelace extends LitElement {
deleteConfig: async () => undefined,
setEditMode: () => undefined,
};
return this.lovelaceConfig.views[index].panel
? html`
<hui-panel-view
.hass=${this.hass}
.lovelace=${lovelace}
.config=${this.lovelaceConfig.views[index]}
></hui-panel-view>
`
: html`
return html`
<hui-view
.hass=${this.hass}
.lovelace=${lovelace}

View File

@ -4,6 +4,12 @@ import {
HassEventBase,
} from "home-assistant-js-websocket";
import { HASSDomEvent } from "../common/dom/fire_event";
import { HuiErrorCard } from "../panels/lovelace/cards/hui-error-card";
import {
Lovelace,
LovelaceBadge,
LovelaceCard,
} from "../panels/lovelace/types";
import { HomeAssistant } from "../types";
export interface LovelacePanelConfig {
@ -69,6 +75,7 @@ export interface LovelaceDashboardCreateParams
export interface LovelaceViewConfig {
index?: number;
title?: string;
type?: string;
badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[];
path?: string;
@ -79,6 +86,14 @@ export interface LovelaceViewConfig {
visible?: boolean | ShowViewConfig[];
}
export interface LovelaceViewElement extends HTMLElement {
hass?: HomeAssistant;
lovelace?: Lovelace;
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
badges?: LovelaceBadge[];
}
export interface ShowViewConfig {
user?: string;
}
@ -91,6 +106,7 @@ export interface LovelaceBadgeConfig {
export interface LovelaceCardConfig {
index?: number;
view_index?: number;
layout?: any;
type: string;
[key: string]: any;
}

View File

@ -2,9 +2,12 @@ import { fireEvent } from "../../../common/dom/fire_event";
import {
LovelaceBadgeConfig,
LovelaceCardConfig,
LovelaceViewConfig,
LovelaceViewElement,
} from "../../../data/lovelace";
import { CUSTOM_TYPE_PREFIX } from "../../../data/lovelace_custom_cards";
import type { HuiErrorCard } from "../cards/hui-error-card";
import type { ErrorCardConfig } from "../cards/types";
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
@ -14,7 +17,6 @@ import {
LovelaceCardConstructor,
LovelaceHeaderFooter,
} from "../types";
import type { ErrorCardConfig } from "../cards/types";
const TIMEOUT = 2000;
@ -44,6 +46,11 @@ interface CreateElementConfigTypes {
element: LovelaceHeaderFooter;
constructor: unknown;
};
view: {
config: LovelaceViewConfig;
element: LovelaceViewElement;
constructor: unknown;
};
}
export const createErrorCardElement = (config: ErrorCardConfig) => {

View File

@ -0,0 +1,23 @@
import {
LovelaceViewConfig,
LovelaceViewElement,
} from "../../../data/lovelace";
import "../views/hui-masonry-view";
import { createLovelaceElement } from "./create-element-base";
const ALWAYS_LOADED_LAYOUTS = new Set(["masonry"]);
const LAZY_LOAD_LAYOUTS = {
panel: () => import("../views/hui-panel-view"),
};
export const createViewElement = (
config: LovelaceViewConfig
): LovelaceViewElement => {
return createLovelaceElement(
"view",
config,
ALWAYS_LOADED_LAYOUTS,
LAZY_LOAD_LAYOUTS
);
};

View File

@ -58,17 +58,14 @@ import { swapView } from "./editor/config-util";
import { showEditLovelaceDialog } from "./editor/lovelace-editor/show-edit-lovelace-dialog";
import { showEditViewDialog } from "./editor/view-editor/show-edit-view-dialog";
import type { Lovelace } from "./types";
import "./views/hui-panel-view";
import type { HUIPanelView } from "./views/hui-panel-view";
import { HUIView } from "./views/hui-view";
import "./views/hui-view";
import type { HUIView } from "./views/hui-view";
class HUIRoot extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public lovelace?: Lovelace;
@property() public columns?: number;
@property({ type: Boolean }) public narrow = false;
@property() public route?: { path: string; prefix: string };
@ -396,15 +393,7 @@ class HUIRoot extends LitElement {
super.updated(changedProperties);
const view = this._viewRoot;
const huiView = view.lastChild as HUIView | HUIPanelView;
if (
changedProperties.has("columns") &&
huiView &&
huiView instanceof HUIView
) {
huiView.columns = this.columns;
}
const huiView = view.lastChild as HUIView;
if (changedProperties.has("hass") && huiView) {
huiView.hass = this.hass;
@ -674,16 +663,9 @@ class HUIRoot extends LitElement {
if (!force && this._viewCache![viewIndex]) {
view = this._viewCache![viewIndex];
} else {
if (viewConfig.panel && viewConfig.cards && viewConfig.cards.length > 0) {
view = document.createElement("hui-panel-view");
view.config = viewConfig;
view.index = viewIndex;
} else {
view = document.createElement("hui-view");
view.columns = this.columns;
view.index = viewIndex;
}
this._viewCache![viewIndex] = view;
}

View File

@ -0,0 +1,317 @@
import { mdiPlus } from "@mdi/js";
import {
css,
CSSResult,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { computeRTL } from "../../../common/util/compute_rtl";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/entity/ha-state-label-badge";
import "../../../components/ha-svg-icon";
import type {
LovelaceViewConfig,
LovelaceViewElement,
} from "../../../data/lovelace";
import type { HomeAssistant } from "../../../types";
import type { HuiErrorCard } from "../cards/hui-error-card";
import { computeCardSize } from "../common/compute-card-size";
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
let editCodeLoaded = false;
// Find column with < 5 size, else smallest column
const getColumnIndex = (columnSizes: number[], size: number) => {
let minIndex = 0;
for (let i = 0; i < columnSizes.length; i++) {
if (columnSizes[i] < 5) {
minIndex = i;
break;
}
if (columnSizes[i] < columnSizes[minIndex]) {
minIndex = i;
}
}
columnSizes[minIndex] += size;
return minIndex;
};
export class MasonryView extends LitElement implements LovelaceViewElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public lovelace?: Lovelace;
@property({ type: Number }) public index?: number;
@property({ attribute: false }) public cards: Array<
LovelaceCard | HuiErrorCard
> = [];
@property({ attribute: false }) public badges: LovelaceBadge[] = [];
@internalProperty() private _columns?: number;
private _createColumnsIteration = 0;
private _mqls?: MediaQueryList[];
public constructor() {
super();
this.addEventListener("iron-resize", (ev: Event) => ev.stopPropagation());
}
public setConfig(_config: LovelaceViewConfig): void {}
protected render(): TemplateResult {
return html`
<div
id="badges"
style=${this.badges.length > 0 ? "display: block" : "display: none"}
>
${this.badges.map((badge) => html`${badge}`)}
</div>
<div id="columns"></div>
${this.lovelace?.editMode
? html`
<mwc-fab
title=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.add"
)}
@click=${this._addCard}
class=${classMap({
rtl: computeRTL(this.hass!),
})}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`
: ""}
`;
}
protected firstUpdated(): void {
this._mqls = [300, 600, 900, 1200].map((width) => {
const mql = matchMedia(`(min-width: ${width}px)`);
mql.addEventListener("change", this._updateColumns);
return mql;
});
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (this.lovelace?.editMode && !editCodeLoaded) {
editCodeLoaded = true;
import(
/* webpackChunkName: "default-layout-editable" */ "./default-view-editable"
);
}
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as HomeAssistant;
if (
(oldHass && this.hass!.dockedSidebar !== oldHass.dockedSidebar) ||
(!oldHass && this.hass)
) {
this._updateColumns();
}
if (changedProperties.size === 1) {
return;
}
}
const oldLovelace = changedProperties.get("lovelace") as Lovelace;
if (
oldLovelace?.config !== this.lovelace?.config ||
oldLovelace?.editMode !== this.lovelace?.editMode ||
changedProperties.has("_columns")
) {
this._createColumns();
}
}
private _addCard(): void {
showCreateCardDialog(this, {
lovelaceConfig: this.lovelace!.config,
saveConfig: this.lovelace!.saveConfig,
path: [this.index!],
});
}
private async _createColumns() {
this._createColumnsIteration++;
const iteration = this._createColumnsIteration;
const root = this.shadowRoot!.getElementById("columns")!;
// Remove old columns
while (root.lastChild) {
root.removeChild(root.lastChild);
}
// Track the total height of cards in a columns
const columnSizes: number[] = [];
const columnElements: HTMLDivElement[] = [];
// Add columns to DOM, limit number of columns to the number of cards
for (let i = 0; i < Math.min(this._columns!, this.cards.length); i++) {
const columnEl = document.createElement("div");
columnEl.classList.add("column");
root.appendChild(columnEl);
columnSizes.push(0);
columnElements.push(columnEl);
}
let tillNextRender: Promise<unknown> | undefined;
let start: Date | undefined;
// Calculate the size of every card and determine in what column it should go
for (const [index, el] of this.cards.entries()) {
if (tillNextRender === undefined) {
// eslint-disable-next-line no-loop-func
tillNextRender = nextRender().then(() => {
tillNextRender = undefined;
start = undefined;
});
}
let waitProm: Promise<unknown> | undefined;
// We should work for max 16ms (60fps) before allowing a frame to render
if (start === undefined) {
// Save the time we start for this frame, no need to wait yet
start = new Date();
} else if (new Date().getTime() - start.getTime() > 16) {
// We are working too long, we will prevent a render, wait to allow for a render
waitProm = tillNextRender;
}
const cardSizeProm = computeCardSize(el);
// @ts-ignore
// eslint-disable-next-line no-await-in-loop
const [cardSize] = await Promise.all([cardSizeProm, waitProm]);
if (iteration !== this._createColumnsIteration) {
// An other create columns is started, abort this one
return;
}
// Calculate in wich column the card should go based on the size and the cards already in there
this._addCardToColumn(
columnElements[getColumnIndex(columnSizes, cardSize as number)],
index,
this.lovelace!.editMode
);
}
// Remove empty columns
columnElements.forEach((column) => {
if (!column.lastChild) {
column.parentElement!.removeChild(column);
}
});
}
private _addCardToColumn(columnEl, index, editMode) {
const card: LovelaceCard = this.cards[index];
if (!editMode) {
card.editMode = false;
columnEl.appendChild(card);
} else {
const wrapper = document.createElement("hui-card-options");
wrapper.hass = this.hass;
wrapper.lovelace = this.lovelace;
wrapper.path = [this.index!, index];
card.editMode = true;
wrapper.appendChild(card);
columnEl.appendChild(wrapper);
}
}
private _updateColumns() {
const matchColumns = this._mqls!.reduce(
(cols, mql) => cols + Number(mql.matches),
0
);
// Do -1 column if the menu is docked and open
this._columns = Math.max(
1,
matchColumns - Number(this.hass!.dockedSidebar === "docked")
);
}
static get styles(): CSSResult {
return css`
#badges {
margin: 8px 16px;
font-size: 85%;
text-align: center;
}
#columns {
display: flex;
flex-direction: row;
justify-content: center;
}
.column {
flex: 1 0 0;
max-width: 500px;
min-width: 0;
}
.column > * {
display: block;
margin: 4px 4px 8px;
}
mwc-fab {
position: sticky;
float: right;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
mwc-fab.rtl {
float: left;
right: auto;
left: calc(16px + env(safe-area-inset-left));
}
@media (max-width: 500px) {
:host {
padding-left: 0;
padding-right: 0;
}
.column > * {
margin-left: 0;
margin-right: 0;
}
}
@media (max-width: 599px) {
.column {
max-width: 600px;
}
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-masonry-view": MasonryView;
}
}
customElements.define("hui-masonry-view", MasonryView);

View File

@ -1,93 +1,118 @@
import { mdiPlus } from "@mdi/js";
import {
customElement,
css,
CSSResult,
html,
internalProperty,
LitElement,
property,
PropertyValues,
UpdatingElement,
TemplateResult,
} from "lit-element";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { LovelaceViewConfig } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { createCardElement } from "../create-element/create-card-element";
import { Lovelace, LovelaceCard } from "../types";
import { classMap } from "lit-html/directives/class-map";
import { computeRTL } from "../../../common/util/compute_rtl";
import type {
LovelaceViewConfig,
LovelaceViewElement,
} from "../../../data/lovelace";
import type { HomeAssistant } from "../../../types";
import { HuiErrorCard } from "../cards/hui-error-card";
import { HuiCardOptions } from "../components/hui-card-options";
import { HuiWarning } from "../components/hui-warning";
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
import type { Lovelace, LovelaceCard } from "../types";
let editCodeLoaded = false;
@customElement("hui-panel-view")
export class HUIPanelView extends UpdatingElement {
@property({ attribute: false }) public hass?: HomeAssistant;
export class PanelView extends LitElement implements LovelaceViewElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public lovelace?: Lovelace;
@property() public config?: LovelaceViewConfig;
@property({ type: Number }) public index?: number;
@property({ type: Number }) public index!: number;
@property({ attribute: false }) public cards: Array<
LovelaceCard | HuiErrorCard
> = [];
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
@internalProperty() private _card?:
| LovelaceCard
| HuiWarning
| HuiCardOptions;
public constructor() {
super();
this.style.setProperty("background", "var(--lovelace-background)");
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
public setConfig(_config: LovelaceViewConfig): void {}
const hass = this.hass!;
const lovelace = this.lovelace!;
const hassChanged = changedProperties.has("hass");
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
const configChanged = changedProperties.has("config");
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (lovelace.editMode && !editCodeLoaded) {
if (this.lovelace?.editMode && !editCodeLoaded) {
editCodeLoaded = true;
import(/* webpackChunkName: "hui-view-editable" */ "./hui-view-editable");
import(
/* webpackChunkName: "default-layout-editable" */ "./default-view-editable"
);
}
let editModeChanged = false;
if (changedProperties.has("lovelace")) {
const oldLovelace = changedProperties.get("lovelace") as Lovelace;
editModeChanged =
!oldLovelace || lovelace.editMode !== oldLovelace.editMode;
}
if (editModeChanged || configChanged) {
this._createCard();
} else if (hassChanged) {
(this.lastChild! as LovelaceCard).hass = this.hass;
}
if (
configChanged ||
(hassChanged &&
oldHass &&
(hass.themes !== oldHass.themes ||
hass.selectedTheme !== oldHass.selectedTheme))
oldLovelace?.config !== this.lovelace?.config ||
(oldLovelace && oldLovelace?.editMode !== this.lovelace?.editMode)
) {
applyThemesOnElement(this, hass.themes, this.config!.theme);
this._createCard();
}
}
protected render(): TemplateResult {
return html`
${this._card}
${this.lovelace?.editMode && this.cards.length === 0
? html`
<mwc-fab
title=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.add"
)}
@click=${this._addCard}
class=${classMap({
rtl: computeRTL(this.hass!),
})}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`
: ""}
`;
}
private _addCard(): void {
showCreateCardDialog(this, {
lovelaceConfig: this.lovelace!.config,
saveConfig: this.lovelace!.saveConfig,
path: [this.index!],
});
}
private _createCard(): void {
while (this.lastChild) {
this.removeChild(this.lastChild);
}
const card: LovelaceCard = createCardElement(this.config!.cards![0]);
card.hass = this.hass;
const card: LovelaceCard = this.cards[0];
card.isPanel = true;
if (!this.lovelace!.editMode) {
this.appendChild(card);
return;
if (!this.lovelace?.editMode) {
this._card = card;
}
const wrapper = document.createElement("hui-card-options");
wrapper.hass = this.hass;
wrapper.lovelace = this.lovelace;
wrapper.path = [this.index, 0];
wrapper.path = [this.index!, 0];
card.editMode = true;
wrapper.appendChild(card);
this.appendChild(wrapper);
if (this.config!.cards!.length > 1) {
this._card = wrapper;
if (this.cards!.length > 1) {
const warning = document.createElement("hui-warning");
warning.setAttribute(
"style",
@ -96,13 +121,33 @@ export class HUIPanelView extends UpdatingElement {
warning.innerText = this.hass!.localize(
"ui.panel.lovelace.editor.view.panel_mode.warning_multiple_cards"
);
this.appendChild(warning);
this._card = warning;
}
}
static get styles(): CSSResult {
return css`
mwc-fab {
position: sticky;
float: right;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
mwc-fab.rtl {
float: left;
right: auto;
left: calc(16px + env(safe-area-inset-left));
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-panel-view": HUIPanelView;
"hui-panel-view": PanelView;
}
}
customElements.define("hui-panel-view", PanelView);

View File

@ -1,73 +1,43 @@
import {
html,
LitElement,
property,
customElement,
internalProperty,
property,
PropertyValues,
TemplateResult,
CSSResult,
css,
UpdatingElement,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/entity/ha-state-label-badge";
import {
import "../../../components/ha-svg-icon";
import type {
LovelaceBadgeConfig,
LovelaceCardConfig,
LovelaceViewConfig,
LovelaceViewElement,
} from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { HuiErrorCard } from "../cards/hui-error-card";
import { computeCardSize } from "../common/compute-card-size";
import type { HomeAssistant } from "../../../types";
import type { HuiErrorCard } from "../cards/hui-error-card";
import { processConfigEntities } from "../common/process-config-entities";
import { createBadgeElement } from "../create-element/create-badge-element";
import { createCardElement } from "../create-element/create-card-element";
import { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
import "../../../components/ha-svg-icon";
import { mdiPlus } from "@mdi/js";
import { nextRender } from "../../../common/util/render-status";
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
import { createViewElement } from "../create-element/create-view-element";
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
let editCodeLoaded = false;
const DEFAULT_VIEW_LAYOUT = "masonry";
const PANEL_VIEW_LAYOUT = "panel";
// Find column with < 5 size, else smallest column
const getColumnIndex = (columnSizes: number[], size: number) => {
let minIndex = 0;
for (let i = 0; i < columnSizes.length; i++) {
if (columnSizes[i] < 5) {
minIndex = i;
break;
}
if (columnSizes[i] < columnSizes[minIndex]) {
minIndex = i;
}
}
columnSizes[minIndex] += size;
return minIndex;
};
export class HUIView extends LitElement {
@customElement("hui-view")
export class HUIView extends UpdatingElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public lovelace?: Lovelace;
@property({ type: Number }) public columns?: number;
@property({ type: Number }) public index?: number;
@internalProperty() private _cards: Array<LovelaceCard | HuiErrorCard> = [];
@internalProperty() private _badges: LovelaceBadge[] = [];
private _createColumnsIteration = 0;
public constructor() {
super();
this.addEventListener("iron-resize", (ev) => ev.stopPropagation());
}
private _layoutElement?: LovelaceViewElement;
// Public to make demo happy
public createCardElement(cardConfig: LovelaceCardConfig) {
@ -75,7 +45,7 @@ export class HUIView extends LitElement {
element.hass = this.hass;
element.addEventListener(
"ll-rebuild",
(ev) => {
(ev: Event) => {
// In edit mode let it go to hui-root and rebuild whole view.
if (!this.lovelace!.editMode) {
ev.stopPropagation();
@ -100,40 +70,14 @@ export class HUIView extends LitElement {
return element;
}
protected render(): TemplateResult {
return html`
<div id="badges"></div>
<div id="columns"></div>
${this.lovelace!.editMode
? html`
<mwc-fab
title="${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.add"
)}"
@click="${this._addCard}"
class="${classMap({
rtl: computeRTL(this.hass!),
})}"
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`
: ""}
`;
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
const hass = this.hass!;
const lovelace = this.lovelace!;
if (lovelace.editMode && !editCodeLoaded) {
editCodeLoaded = true;
import(/* webpackChunkName: "hui-view-editable" */ "./hui-view-editable");
}
const hassChanged = changedProperties.has("hass");
const oldLovelace = changedProperties.get("lovelace") as Lovelace;
let editModeChanged = false;
let configChanged = false;
@ -141,30 +85,55 @@ export class HUIView extends LitElement {
if (changedProperties.has("index")) {
configChanged = true;
} else if (changedProperties.has("lovelace")) {
const oldLovelace = changedProperties.get("lovelace") as Lovelace;
editModeChanged =
oldLovelace && lovelace.editMode !== oldLovelace.editMode;
configChanged = !oldLovelace || lovelace.config !== oldLovelace.config;
}
let viewConfig: LovelaceViewConfig | undefined;
if (configChanged) {
this._createBadges(lovelace.config.views[this.index!]);
} else if (hassChanged) {
viewConfig = lovelace.config.views[this.index!];
viewConfig = {
...viewConfig,
type: viewConfig.panel
? PANEL_VIEW_LAYOUT
: viewConfig.type || DEFAULT_VIEW_LAYOUT,
};
}
if (configChanged && !this._layoutElement) {
this._layoutElement = createViewElement(viewConfig!);
}
if (configChanged) {
this._createBadges(viewConfig!);
this._createCards(viewConfig!);
this._layoutElement!.hass = this.hass;
this._layoutElement!.lovelace = lovelace;
this._layoutElement!.index = this.index;
}
if (hassChanged) {
this._badges.forEach((badge) => {
badge.hass = hass;
});
}
if (configChanged) {
this._createCards(lovelace.config.views[this.index!]);
} else if (editModeChanged || changedProperties.has("columns")) {
this._createColumns();
}
if (hassChanged && !configChanged) {
this._cards.forEach((element) => {
element.hass = hass;
});
this._layoutElement!.hass = this.hass;
}
if (editModeChanged) {
this._layoutElement!.lovelace = lovelace;
}
if (configChanged || hassChanged || editModeChanged) {
this._layoutElement!.cards = this._cards;
this._layoutElement!.badges = this._badges;
}
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
@ -183,25 +152,14 @@ export class HUIView extends LitElement {
lovelace.config.views[this.index!].theme
);
}
}
private _addCard(): void {
showCreateCardDialog(this, {
lovelaceConfig: this.lovelace!.config,
saveConfig: this.lovelace!.saveConfig,
path: [this.index!],
});
if (this._layoutElement && !this.lastChild) {
this.appendChild(this._layoutElement);
}
}
private _createBadges(config: LovelaceViewConfig): void {
const root = this.shadowRoot!.getElementById("badges")!;
while (root.lastChild) {
root.removeChild(root.lastChild);
}
if (!config || !config.badges || !Array.isArray(config.badges)) {
root.style.display = "none";
this._badges = [];
return;
}
@ -212,97 +170,8 @@ export class HUIView extends LitElement {
const element = createBadgeElement(badge);
element.hass = this.hass;
elements.push(element);
root.appendChild(element);
});
this._badges = elements;
root.style.display = elements.length > 0 ? "block" : "none";
}
private async _createColumns() {
this._createColumnsIteration++;
const iteration = this._createColumnsIteration;
const root = this.shadowRoot!.getElementById("columns")!;
// Remove old columns
while (root.lastChild) {
root.removeChild(root.lastChild);
}
// Track the total height of cards in a columns
const columnSizes: number[] = [];
const columnElements: HTMLDivElement[] = [];
// Add columns to DOM, limit number of columns to the number of cards
for (let i = 0; i < Math.min(this.columns!, this._cards.length); i++) {
const columnEl = document.createElement("div");
columnEl.classList.add("column");
root.appendChild(columnEl);
columnSizes.push(0);
columnElements.push(columnEl);
}
let tillNextRender: Promise<unknown> | undefined;
let start: Date | undefined;
// Calculate the size of every card and determine in what column it should go
for (const [index, el] of this._cards.entries()) {
if (tillNextRender === undefined) {
// eslint-disable-next-line no-loop-func
tillNextRender = nextRender().then(() => {
tillNextRender = undefined;
start = undefined;
});
}
let waitProm: Promise<unknown> | undefined;
// We should work for max 16ms (60fps) before allowing a frame to render
if (start === undefined) {
// Save the time we start for this frame, no need to wait yet
start = new Date();
} else if (new Date().getTime() - start.getTime() > 16) {
// We are working too long, we will prevent a render, wait to allow for a render
waitProm = tillNextRender;
}
const cardSizeProm = computeCardSize(el);
// @ts-ignore
// eslint-disable-next-line no-await-in-loop
const [cardSize] = await Promise.all([cardSizeProm, waitProm]);
if (iteration !== this._createColumnsIteration) {
// An other create columns is started, abort this one
return;
}
// Calculate in wich column the card should go based on the size and the cards already in there
this._addCardToColumn(
columnElements[getColumnIndex(columnSizes, cardSize as number)],
index,
this.lovelace!.editMode
);
}
// Remove empty columns
columnElements.forEach((column) => {
if (!column.lastChild) {
column.parentElement!.removeChild(column);
}
});
}
private _addCardToColumn(columnEl, index, editMode) {
const card: LovelaceCard = this._cards[index];
if (!editMode) {
card.editMode = false;
columnEl.appendChild(card);
} else {
const wrapper = document.createElement("hui-card-options");
wrapper.hass = this.hass;
wrapper.lovelace = this.lovelace;
wrapper.path = [this.index!, index];
card.editMode = true;
wrapper.appendChild(card);
columnEl.appendChild(wrapper);
}
}
private _createCards(config: LovelaceViewConfig): void {
@ -311,14 +180,9 @@ export class HUIView extends LitElement {
return;
}
const elements: LovelaceCard[] = [];
config.cards.forEach((cardConfig) => {
const element = this.createCardElement(cardConfig);
elements.push(element);
});
this._cards = elements;
this._createColumns();
this._cards = config.cards.map((cardConfig) =>
this.createCardElement(cardConfig)
);
}
private _rebuildCard(
@ -344,63 +208,6 @@ export class HUIView extends LitElement {
curBadgeEl === badgeElToReplace ? newBadgeEl : curBadgeEl
);
}
static get styles(): CSSResult {
return css`
:host {
display: block;
box-sizing: border-box;
padding: 4px 4px env(safe-area-inset-bottom);
transform: translateZ(0);
position: relative;
color: var(--primary-text-color);
background: var(--lovelace-background, var(--primary-background-color));
}
#badges {
margin: 8px 16px;
font-size: 85%;
text-align: center;
}
#columns {
display: flex;
flex-direction: row;
justify-content: center;
}
.column {
flex: 1 0 0;
max-width: 500px;
min-width: 0;
}
.column > * {
display: block;
margin: 4px 4px 8px;
}
mwc-fab {
position: sticky;
float: right;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
mwc-fab.rtl {
float: left;
right: auto;
left: calc(16px + env(safe-area-inset-left));
}
@media (max-width: 599px) {
.column {
max-width: 600px;
}
}
`;
}
}
declare global {
@ -408,5 +215,3 @@ declare global {
"hui-view": HUIView;
}
}
customElements.define("hui-view", HUIView);