From 771c7518e62a4d18abbb5d2b2078469aadb1314c Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 30 Sep 2020 04:06:03 -0500 Subject: [PATCH] Custom Lovelace View Layouts (#6557) Co-authored-by: Bram Kragten --- cast/src/receiver/layout/hc-lovelace.ts | 25 +- src/data/lovelace.ts | 16 + .../create-element/create-element-base.ts | 9 +- .../create-element/create-view-element.ts | 23 ++ src/panels/lovelace/hui-root.ts | 28 +- ...w-editable.ts => default-view-editable.ts} | 0 src/panels/lovelace/views/hui-masonry-view.ts | 317 ++++++++++++++++++ src/panels/lovelace/views/hui-panel-view.ts | 157 +++++---- src/panels/lovelace/views/hui-view.ts | 313 ++++------------- 9 files changed, 537 insertions(+), 351 deletions(-) create mode 100644 src/panels/lovelace/create-element/create-view-element.ts rename src/panels/lovelace/views/{hui-view-editable.ts => default-view-editable.ts} (100%) create mode 100644 src/panels/lovelace/views/hui-masonry-view.ts diff --git a/cast/src/receiver/layout/hc-lovelace.ts b/cast/src/receiver/layout/hc-lovelace.ts index c0d1dc1dfe..0151f39073 100644 --- a/cast/src/receiver/layout/hc-lovelace.ts +++ b/cast/src/receiver/layout/hc-lovelace.ts @@ -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,22 +44,14 @@ class HcLovelace extends LitElement { deleteConfig: async () => undefined, setEditMode: () => undefined, }; - return this.lovelaceConfig.views[index].panel - ? html` - - ` - : html` - - `; + return html` + + `; } protected updated(changedProps) { diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 4431730218..bf8f4c726f 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -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; 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; + 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; } diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts index b1afedcb28..23b2083e1e 100644 --- a/src/panels/lovelace/create-element/create-element-base.ts +++ b/src/panels/lovelace/create-element/create-element-base.ts @@ -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) => { diff --git a/src/panels/lovelace/create-element/create-view-element.ts b/src/panels/lovelace/create-element/create-view-element.ts new file mode 100644 index 0000000000..d8cec5fa48 --- /dev/null +++ b/src/panels/lovelace/create-element/create-view-element.ts @@ -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 + ); +}; diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 01c55e5e95..a0f0a6dfa3 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -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; @@ -675,15 +664,8 @@ 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; - } + view = document.createElement("hui-view"); + view.index = viewIndex; this._viewCache![viewIndex] = view; } diff --git a/src/panels/lovelace/views/hui-view-editable.ts b/src/panels/lovelace/views/default-view-editable.ts similarity index 100% rename from src/panels/lovelace/views/hui-view-editable.ts rename to src/panels/lovelace/views/default-view-editable.ts diff --git a/src/panels/lovelace/views/hui-masonry-view.ts b/src/panels/lovelace/views/hui-masonry-view.ts new file mode 100644 index 0000000000..30b9f61395 --- /dev/null +++ b/src/panels/lovelace/views/hui-masonry-view.ts @@ -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` +
0 ? "display: block" : "display: none"} + > + ${this.badges.map((badge) => html`${badge}`)} +
+
+ ${this.lovelace?.editMode + ? html` + + + + ` + : ""} + `; + } + + 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 | 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 | 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); diff --git a/src/panels/lovelace/views/hui-panel-view.ts b/src/panels/lovelace/views/hui-panel-view.ts index 87feecb634..44f9d27fe3 100644 --- a/src/panels/lovelace/views/hui-panel-view.ts +++ b/src/panels/lovelace/views/hui-panel-view.ts @@ -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; - } + const oldLovelace = changedProperties.get("lovelace") as Lovelace; 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(); } } - private _createCard(): void { - while (this.lastChild) { - this.removeChild(this.lastChild); - } + protected render(): TemplateResult { + return html` + ${this._card} + ${this.lovelace?.editMode && this.cards.length === 0 + ? html` + + + + ` + : ""} + `; + } - const card: LovelaceCard = createCardElement(this.config!.cards![0]); - card.hass = this.hass; + private _addCard(): void { + showCreateCardDialog(this, { + lovelaceConfig: this.lovelace!.config, + saveConfig: this.lovelace!.saveConfig, + path: [this.index!], + }); + } + + private _createCard(): void { + 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); diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index eb627cf1ff..de0bae5ea6 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -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 = []; @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` -
-
- ${this.lovelace!.editMode - ? html` - - - - ` - : ""} - `; - } - 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 | 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 | 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);