diff --git a/src/common/util/render-status.ts b/src/common/util/render-status.ts new file mode 100644 index 0000000000..4564c29cd6 --- /dev/null +++ b/src/common/util/render-status.ts @@ -0,0 +1,3 @@ +export const afterNextRender = (cb: () => void): void => { + requestAnimationFrame(() => setTimeout(cb, 0)); +}; diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 1f17be363c..db59a82ea6 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -84,7 +84,9 @@ export class HaStateLabelBadge extends hassLocalizeLitMixin(LitElement) { super.firstUpdated(changedProperties); this.addEventListener("click", (ev) => { ev.stopPropagation(); - fireEvent(this, "hass-more-info", { entityId: this.state!.entity_id }); + if (this.state) { + fireEvent(this, "hass-more-info", { entityId: this.state.entity_id }); + } }); } diff --git a/src/mixins/lit-localize-mixin.ts b/src/mixins/lit-localize-mixin.ts index ef81a75b3c..e16a739f46 100644 --- a/src/mixins/lit-localize-mixin.ts +++ b/src/mixins/lit-localize-mixin.ts @@ -37,13 +37,15 @@ export const hassLocalizeLitMixin = ( public connectedCallback(): void { super.connectedCallback(); - let language; - let resources; - if (this.hass) { - language = this.hass.language; - resources = this.hass.resources; + if (this.localize === empty) { + let language; + let resources; + if (this.hass) { + language = this.hass.language; + resources = this.hass.resources; + } + this.localize = this.__computeLocalize(language, resources); } - this.localize = this.__computeLocalize(language, resources); } public updated(changedProperties: PropertyValues) { diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index 3afff64e5b..45e9351fb7 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -19,6 +19,7 @@ import { LovelaceCardConfig } from "../../../data/lovelace"; import "../../../components/ha-card"; import "../../../components/ha-icon"; import { loadRoundslider } from "../../../resources/jquery.roundslider.ondemand"; +import { afterNextRender } from "../../../common/util/render-status"; const thermostatConfig = { radius: 150, @@ -68,6 +69,7 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement) private _config?: Config; private _roundSliderStyle?: TemplateResult; private _jQuery?: any; + private _broadCard?: boolean; static get properties(): PropertyDeclarations { return { @@ -95,7 +97,6 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement) return html``; } const stateObj = this.hass.states[this._config.entity] as ClimateEntity; - const broadCard = this.clientWidth > 390; const mode = modeIcons[stateObj.attributes.operation_mode || ""] ? stateObj.attributes.operation_mode! : "unknown-mode"; @@ -104,8 +105,8 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement)
@@ -146,8 +147,41 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement) return hasConfigOrEntityChanged(this, changedProps); } - protected async firstUpdated(): Promise { + protected firstUpdated(): void { + this._initialLoad(); + } + + protected updated(changedProps: PropertyValues): void { + if (!this._config || !this.hass || !changedProps.has("hass")) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + + if (!oldHass || oldHass.themes !== this.hass.themes) { + applyThemesOnElement(this, this.hass.themes, this._config.theme); + } + + const stateObj = this.hass.states[this._config.entity] as ClimateEntity; + + if ( + this._jQuery && + // If jQuery changed, we just rendered in firstUpdated + !changedProps.has("_jQuery") && + (!oldHass || oldHass.states[this._config.entity] !== stateObj) + ) { + const [sliderValue, uiValue] = this._genSliderValue(stateObj); + + this._jQuery("#thermostat", this.shadowRoot).roundSlider({ + value: sliderValue, + }); + this._updateSetTemp(uiValue); + } + } + + private async _initialLoad(): Promise { const loaded = await loadRoundslider(); + await new Promise((resolve) => afterNextRender(resolve)); this._roundSliderStyle = loaded.roundSliderStyle; this._jQuery = loaded.jQuery; @@ -160,6 +194,8 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement) ? "range" : "min-range"; + const [sliderValue, uiValue] = this._genSliderValue(stateObj); + this._broadCard = this.clientWidth > 390; this._jQuery("#thermostat", this.shadowRoot).roundSlider({ ...thermostatConfig, radius: this.clientWidth / 3, @@ -168,18 +204,14 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement) sliderType: _sliderType, change: (value) => this._setTemperature(value), drag: (value) => this._dragEvent(value), + value: sliderValue, }); + this._updateSetTemp(uiValue); } - protected updated(changedProps: PropertyValues): void { - if (!this._config || !this.hass || !this._jQuery) { - return; - } - - const stateObj = this.hass.states[this._config.entity] as ClimateEntity; - - let sliderValue; - let uiValue; + private _genSliderValue(stateObj: ClimateEntity): [string | number, string] { + let sliderValue: string | number; + let uiValue: string; if ( stateObj.attributes.target_temp_low && @@ -193,18 +225,11 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement) String(stateObj.attributes.target_temp_high), ]); } else { - sliderValue = uiValue = stateObj.attributes.temperature; + sliderValue = stateObj.attributes.temperature; + uiValue = "" + stateObj.attributes.temperature; } - this._jQuery("#thermostat", this.shadowRoot).roundSlider({ - value: sliderValue, - }); - this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = uiValue; - - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (!oldHass || oldHass.themes !== this.hass.themes) { - applyThemesOnElement(this, this.hass.themes, this._config.theme); - } + return [sliderValue, uiValue]; } private renderStyle(): TemplateResult { @@ -383,10 +408,12 @@ export class HuiThermostatCard extends hassLocalizeLitMixin(LitElement) `; } + private _updateSetTemp(value: string): void { + this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = value; + } + private _dragEvent(e): void { - this.shadowRoot!.querySelector("#set-temperature")!.innerHTML = formatTemp( - String(e.value).split(",") - ); + this._updateSetTemp(formatTemp(String(e.value).split(","))); } private _setTemperature(e): void { diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index f794001823..04621e7e5c 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -44,6 +44,7 @@ import { HUIView } from "./hui-view"; import createCardElement from "./common/create-card-element"; import { showEditViewDialog } from "./editor/view-editor/show-edit-view-dialog"; import { Lovelace } from "./types"; +import { afterNextRender } from "../../common/util/render-status"; // CSS and JS should only be imported once. Modules and HTML are safe. const CSS_CACHE = {}; @@ -57,10 +58,11 @@ class HUIRoot extends hassLocalizeLitMixin(LitElement) { public columns?: number; public route?: { path: string; prefix: string }; private _routeData?: { view: string }; - private _curView: number | "unused"; + private _curView?: number | "unused"; private notificationsOpen?: boolean; private _persistentNotifications?: Notification[]; private _haStyle?: DocumentFragment; + private _viewCache?: { [viewId: string]: HUIView }; private _debouncedConfigChanged: () => void; private _unsubNotifications?: () => void; @@ -82,9 +84,8 @@ class HUIRoot extends hassLocalizeLitMixin(LitElement) { constructor() { super(); - this._curView = 0; this._debouncedConfigChanged = debounce( - () => this._selectView(this._curView), + () => this._selectView(this._curView, true), 100 ); } @@ -349,6 +350,9 @@ class HUIRoot extends hassLocalizeLitMixin(LitElement) { huiView.hass = this.hass; } + let newSelectView; + let force = false; + if (changedProperties.has("route")) { const views = this.config && this.config.views; if ( @@ -367,9 +371,7 @@ class HUIRoot extends hassLocalizeLitMixin(LitElement) { break; } } - if (index !== this._curView) { - this._selectView(index); - } + newSelectView = index; } } @@ -379,15 +381,20 @@ class HUIRoot extends hassLocalizeLitMixin(LitElement) { | undefined; if (!oldLovelace || oldLovelace.config !== this.lovelace!.config) { + this._viewCache = {}; this._loadResources(this.lovelace!.config.resources || []); // On config change, recreate the view from scratch. - this._selectView(this._curView); + force = true; } if (!oldLovelace || oldLovelace.editMode !== this.lovelace!.editMode) { - this._editModeChanged(); + force = true; } } + + if (newSelectView !== undefined || force) { + this._selectView(newSelectView, force); + } } private get _notifications() { @@ -442,7 +449,7 @@ class HUIRoot extends hassLocalizeLitMixin(LitElement) { } private _handleUnusedEntities(): void { - this._selectView("unused"); + this._selectView("unused", false); } private _deselect(ev): void { @@ -471,10 +478,6 @@ class HUIRoot extends hassLocalizeLitMixin(LitElement) { } } - private _editModeChanged(): void { - this._selectView(this._curView); - } - private _editView() { showEditViewDialog(this, { lovelace: this.lovelace!, @@ -501,45 +504,61 @@ class HUIRoot extends hassLocalizeLitMixin(LitElement) { scrollToTarget(this, this._layout.header.scrollTarget); } - private _selectView(viewIndex: HUIRoot["_curView"]): void { + private async _selectView( + viewIndex: HUIRoot["_curView"], + force: boolean + ): Promise { + if (!force && this._curView === viewIndex) { + return; + } + + viewIndex = viewIndex === undefined ? 0 : viewIndex; + this._curView = viewIndex; // Recreate a new element to clear the applied themes. const root = this._view; + if (root.lastChild) { root.removeChild(root.lastChild); } - let view; - let background = this.config.background || ""; - if (viewIndex === "unused") { - view = document.createElement("hui-unused-entities"); - view.setConfig(this.config); + const unusedEntities = document.createElement("hui-unused-entities"); + unusedEntities.setConfig(this.config); + root.style.background = this.config.background || ""; + root.appendChild(unusedEntities); + return; + } + + let view; + const viewConfig = this.config.views[viewIndex]; + + if (!viewConfig) { + this._editModeEnable(); + return; + } + + if (!force && this._viewCache![viewIndex]) { + view = this._viewCache![viewIndex]; } else { - const viewConfig = this.config.views[this._curView]; - if (!viewConfig) { - this._editModeEnable(); - return; - } + await new Promise((resolve) => afterNextRender(resolve)); + if (viewConfig.panel && viewConfig.cards && viewConfig.cards.length > 0) { view = createCardElement(viewConfig.cards[0]); view.isPanel = true; } else { view = document.createElement("hui-view"); view.lovelace = this.lovelace; - view.config = viewConfig; view.columns = this.columns; view.index = viewIndex; } - if (viewConfig.background) { - background = viewConfig.background; - } + this._viewCache![viewIndex] = view; } - this._view.style.background = background; - view.hass = this.hass; + root.style.background = + viewConfig.background || this.config.background || ""; root.appendChild(view); } diff --git a/src/panels/lovelace/hui-view.ts b/src/panels/lovelace/hui-view.ts index 8fc3cd6d01..4bbe6b3b15 100644 --- a/src/panels/lovelace/hui-view.ts +++ b/src/panels/lovelace/hui-view.ts @@ -24,6 +24,24 @@ import { showEditCardDialog } from "./editor/card-editor/show-edit-card-dialog"; let editCodeLoaded = false; +// Find column with < 5 entities, else column with lowest count +const getColumnIndex = (columnEntityCount: number[], size: number) => { + let minIndex = 0; + for (let i = 0; i < columnEntityCount.length; i++) { + if (columnEntityCount[i] < 5) { + minIndex = i; + break; + } + if (columnEntityCount[i] < columnEntityCount[minIndex]) { + minIndex = i; + } + } + + columnEntityCount[minIndex] += size; + + return minIndex; +}; + export class HUIView extends hassLocalizeLitMixin(LitElement) { public hass?: HomeAssistant; public lovelace?: Lovelace; @@ -245,28 +263,12 @@ export class HUIView extends hassLocalizeLitMixin(LitElement) { columnEntityCount.push(0); } - // Find column with < 5 entities, else column with lowest count - function getColumnIndex(size) { - let minIndex = 0; - for (let i = 0; i < columnEntityCount.length; i++) { - if (columnEntityCount[i] < 5) { - minIndex = i; - break; - } - if (columnEntityCount[i] < columnEntityCount[minIndex]) { - minIndex = i; - } - } - - columnEntityCount[minIndex] += size; - - return minIndex; - } - elements.forEach((el, index) => { const cardSize = computeCardSize(el); // Element to append might be the wrapped card when we're editing. - columns[getColumnIndex(cardSize)].push(elementsToAppend[index]); + columns[getColumnIndex(columnEntityCount, cardSize)].push( + elementsToAppend[index] + ); }); // Remove empty columns