mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-08 18:06:36 +00:00
Custom Lovelace View Layouts (#6557)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
4f2bad034a
commit
771c7518e6
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
23
src/panels/lovelace/create-element/create-view-element.ts
Normal file
23
src/panels/lovelace/create-element/create-view-element.ts
Normal 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
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
||||
|
317
src/panels/lovelace/views/hui-masonry-view.ts
Normal file
317
src/panels/lovelace/views/hui-masonry-view.ts
Normal 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);
|
@ -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);
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user