diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 5cb369c0f0..9ad6c60a4b 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -35,6 +35,7 @@ import "./step-flow-external"; import "./step-flow-form"; import "./step-flow-loading"; import "./step-flow-pick-handler"; +import { fireEvent } from "../../common/dom/fire_event"; import { computeRTL } from "../../common/util/compute_rtl"; let instance = 0; @@ -114,6 +115,17 @@ class DataEntryFlowDialog extends LitElement { this._loading = false; } + public closeDialog() { + if (this._step) { + this._flowDone(); + } else if (this._step === null) { + // Flow aborted during picking flow + this._step = undefined; + this._params = undefined; + } + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + protected render(): TemplateResult { if (!this._params) { return html``; @@ -122,7 +134,7 @@ class DataEntryFlowDialog extends LitElement { return html` { + private _dismiss(): void { if (this._params!.cancel) { this._params!.cancel(); } - this._params = undefined; + this._close(); } private _handleKeyUp(ev: KeyboardEvent) { @@ -113,15 +125,16 @@ class DialogBox extends LitElement { } } - private async _confirm(): Promise { + private _confirm(): void { if (this._params!.confirm) { this._params!.confirm(this._value); } - this._dismiss(); + this._close(); } private _close(): void { this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); } static get styles(): CSSResult[] { diff --git a/src/dialogs/make-dialog-manager.ts b/src/dialogs/make-dialog-manager.ts index 02af7d4e96..5baf4d3e65 100644 --- a/src/dialogs/make-dialog-manager.ts +++ b/src/dialogs/make-dialog-manager.ts @@ -6,15 +6,18 @@ declare global { interface HASSDomEvents { "show-dialog": ShowDialogParams; "close-dialog": undefined; + "dialog-closed": DialogClosedParams; } // for add event listener interface HTMLElementEventMap { "show-dialog": HASSDomEvent>; + "dialog-closed": HASSDomEvent; } } interface HassDialog extends HTMLElement { showDialog(params: T); + closeDialog?: () => boolean | void; } interface ShowDialogParams { @@ -23,16 +26,30 @@ interface ShowDialogParams { dialogParams: T; } +export interface DialogClosedParams { + dialog: string; +} + +export interface DialogState { + dialog: string; + open: boolean; + oldState: null | DialogState; + dialogParams?: unknown; +} + const LOADED = {}; export const showDialog = async ( element: HTMLElement & ProvideHassElement, root: ShadowRoot | HTMLElement, - dialogImport: () => Promise, dialogTag: string, - dialogParams: unknown + dialogParams: unknown, + dialogImport?: () => Promise ) => { if (!(dialogTag in LOADED)) { + if (!dialogImport) { + return; + } LOADED[dialogTag] = dialogImport().then(() => { const dialogEl = document.createElement(dialogTag) as HassDialog; element.provideHass(dialogEl); @@ -40,19 +57,55 @@ export const showDialog = async ( return dialogEl; }); } + + history.replaceState( + { + dialog: dialogTag, + open: false, + oldState: + history.state?.open && history.state?.dialog !== dialogTag + ? history.state + : null, + }, + "" + ); + try { + history.pushState( + { dialog: dialogTag, dialogParams: dialogParams, open: true }, + "" + ); + } catch (err) { + // dialogParams could not be cloned, probably contains callback + history.pushState( + { dialog: dialogTag, dialogParams: null, open: true }, + "" + ); + } + const dialogElement = await LOADED[dialogTag]; dialogElement.showDialog(dialogParams); }; +export const closeDialog = async (dialogTag: string): Promise => { + if (!(dialogTag in LOADED)) { + return true; + } + const dialogElement = await LOADED[dialogTag]; + if (dialogElement.closeDialog) { + return dialogElement.closeDialog() !== false; + } + return true; +}; + export const makeDialogManager = ( element: HTMLElement & ProvideHassElement, root: ShadowRoot | HTMLElement ) => { element.addEventListener( "show-dialog", - async (e: HASSDomEvent>) => { + (e: HASSDomEvent>) => { const { dialogTag, dialogImport, dialogParams } = e.detail; - showDialog(element, root, dialogImport, dialogTag, dialogParams); + showDialog(element, root, dialogTag, dialogParams, dialogImport); } ); }; diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index ef0a1fc9f7..2375bcbb10 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -8,6 +8,7 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const"; import { computeStateName } from "../../common/entity/compute_state_name"; import { navigate } from "../../common/navigate"; +import { fireEvent } from "../../common/dom/fire_event"; import "../../components/state-history-charts"; import { removeEntityRegistryEntry } from "../../data/entity_registry"; import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor"; @@ -24,7 +25,6 @@ import { } from "lit-element"; import { haStyleDialog } from "../../resources/styles"; import { HomeAssistant } from "../../types"; -import { fireEvent } from "../../common/dom/fire_event"; import { getRecentWithCache } from "../../data/cached-history"; import { computeDomain } from "../../common/entity/compute_domain"; import { mdiClose, mdiCog, mdiPencil } from "@mdi/js"; @@ -34,6 +34,10 @@ const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"]; const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"]; const EDITABLE_DOMAINS = ["script"]; +export interface MoreInfoDialogParams { + entityId: string | null; +} + @customElement("ha-more-info-dialog") export class MoreInfoDialog extends LitElement { @property() public hass!: HomeAssistant; @@ -42,39 +46,39 @@ export class MoreInfoDialog extends LitElement { @internalProperty() private _stateHistory?: HistoryResult; + @internalProperty() private _entityId?: string | null; + private _historyRefreshInterval?: number; - protected updated(changedProperties) { - super.updated(changedProperties); - if (!changedProperties.has("hass")) { - return; + public showDialog(params: MoreInfoDialogParams) { + this._entityId = params.entityId; + if (!this._entityId) { + this.closeDialog(); } - const oldHass = changedProperties.get("hass"); - if (oldHass && oldHass.moreInfoEntityId === this.hass.moreInfoEntityId) { - return; - } - if (this.hass.moreInfoEntityId) { - this.large = false; - this._stateHistory = undefined; - if (this._computeShowHistoryComponent(this.hass.moreInfoEntityId)) { - this._getStateHistory(); - clearInterval(this._historyRefreshInterval); - this._historyRefreshInterval = window.setInterval(() => { - this._getStateHistory(); - }, 60 * 1000); - } - } else { - this._stateHistory = undefined; + this.large = false; + this._stateHistory = undefined; + if (this._computeShowHistoryComponent(this._entityId)) { + this._getStateHistory(); clearInterval(this._historyRefreshInterval); - this._historyRefreshInterval = undefined; + this._historyRefreshInterval = window.setInterval(() => { + this._getStateHistory(); + }, 60 * 1000); } } + public closeDialog() { + this._entityId = undefined; + this._stateHistory = undefined; + clearInterval(this._historyRefreshInterval); + this._historyRefreshInterval = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + protected render() { - if (!this.hass.moreInfoEntityId) { + if (!this._entityId) { return html``; } - const entityId = this.hass.moreInfoEntityId; + const entityId = this._entityId; const stateObj = this.hass.states[entityId]; const domain = computeDomain(entityId); @@ -85,7 +89,7 @@ export class MoreInfoDialog extends LitElement { return html` { - if (!this.hass.moreInfoEntityId) { + if (!this._entityId) { return; } this._stateHistory = await getRecentWithCache( this.hass!, - this.hass.moreInfoEntityId, + this._entityId, { refresh: 60, - cacheKey: `more_info.${this.hass.moreInfoEntityId}`, + cacheKey: `more_info.${this._entityId}`, hoursToShow: 24, }, this.hass!.localize, @@ -198,7 +202,7 @@ export class MoreInfoDialog extends LitElement { } private _removeEntity() { - const entityId = this.hass.moreInfoEntityId!; + const entityId = this._entityId!; showConfirmationDialog(this, { title: this.hass.localize( "ui.dialogs.more_info_control.restored.confirm_remove_title" @@ -216,14 +220,14 @@ export class MoreInfoDialog extends LitElement { private _gotoSettings() { showEntityEditorDialog(this, { - entity_id: this.hass.moreInfoEntityId!, + entity_id: this._entityId!, }); - fireEvent(this, "hass-more-info", { entityId: null }); + this.closeDialog(); } private _gotoEdit() { - const stateObj = this.hass.states[this.hass.moreInfoEntityId!]; - const domain = computeDomain(this.hass.moreInfoEntityId!); + const stateObj = this.hass.states[this._entityId!]; + const domain = computeDomain(this._entityId!); navigate( this, `/config/${domain}/edit/${ @@ -232,11 +236,7 @@ export class MoreInfoDialog extends LitElement { : stateObj.entity_id }` ); - this._close(); - } - - private _close() { - fireEvent(this, "hass-more-info", { entityId: null }); + this.closeDialog(); } static get styles() { @@ -274,7 +274,7 @@ export class MoreInfoDialog extends LitElement { --mdc-dialog-max-width: 90vw; } - app-toolbar { + ha-dialog:not([data-domain="camera"]) app-toolbar { max-width: 368px; } diff --git a/src/panels/config/entities/dialog-entity-editor.ts b/src/panels/config/entities/dialog-entity-editor.ts index e765ddffb8..41fdcc279d 100644 --- a/src/panels/config/entities/dialog-entity-editor.ts +++ b/src/panels/config/entities/dialog-entity-editor.ts @@ -13,8 +13,8 @@ import { TemplateResult, } from "lit-element"; import { cache } from "lit-html/directives/cache"; -import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../common/dom/fire_event"; +import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/ha-dialog"; import "../../../components/ha-svg-icon"; @@ -72,6 +72,7 @@ export class DialogEntityEditor extends LitElement { public closeDialog(): void { this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render(): TemplateResult { diff --git a/src/panels/config/logs/dialog-system-log-detail.ts b/src/panels/config/logs/dialog-system-log-detail.ts index 237a42e90e..01c6ee0355 100644 --- a/src/panels/config/logs/dialog-system-log-detail.ts +++ b/src/panels/config/logs/dialog-system-log-detail.ts @@ -20,6 +20,7 @@ import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail"; import { formatSystemLogTime } from "./util"; +import { fireEvent } from "../../../common/dom/fire_event"; class DialogSystemLogDetail extends LitElement { @property() public hass!: HomeAssistant; @@ -34,6 +35,11 @@ class DialogSystemLogDetail extends LitElement { await this.updateComplete; } + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + protected updated(changedProps) { super.updated(changedProps); if (!changedProps.has("_params") || !this._params) { @@ -137,7 +143,7 @@ class DialogSystemLogDetail extends LitElement { private _openedChanged(ev: PolymerChangedEvent): void { if (!(ev.detail as any).value) { - this._params = undefined; + this.closeDialog(); } } diff --git a/src/panels/config/zone/dialog-zone-detail.ts b/src/panels/config/zone/dialog-zone-detail.ts index 6899d1ed29..8de08f7218 100644 --- a/src/panels/config/zone/dialog-zone-detail.ts +++ b/src/panels/config/zone/dialog-zone-detail.ts @@ -8,6 +8,7 @@ import { property, TemplateResult, } from "lit-element"; +import { fireEvent } from "../../../common/dom/fire_event"; import { addDistanceToCoord } from "../../../common/location/add_distance_to_coord"; import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-switch"; @@ -45,7 +46,7 @@ class DialogZoneDetail extends LitElement { @property() private _submitting = false; - public async showDialog(params: ZoneDetailDialogParams): Promise { + public showDialog(params: ZoneDetailDialogParams): void { this._params = params; this._error = undefined; if (this._params.entry) { @@ -74,7 +75,11 @@ class DialogZoneDetail extends LitElement { this._passive = false; this._radius = 100; } - await this.updateComplete; + } + + public closeDialog(): void { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render(): TemplateResult { @@ -93,7 +98,7 @@ class DialogZoneDetail extends LitElement { return html` >( superClass: T ) => class extends superClass { - protected firstUpdated(changedProps) { + protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); // deprecated this.addEventListener("register-dialog", (e) => @@ -42,9 +43,9 @@ export const dialogManagerMixin = >( showDialog( this, this.shadowRoot!, - dialogImport, dialogTag, - (showEv as HASSDomEvent).detail + (showEv as HASSDomEvent).detail, + dialogImport ); }); } diff --git a/src/state/more-info-mixin.ts b/src/state/more-info-mixin.ts index 3dd6b3ed02..2fc9088eb1 100644 --- a/src/state/more-info-mixin.ts +++ b/src/state/more-info-mixin.ts @@ -1,35 +1,46 @@ -import { Constructor } from "../types"; -import { HassBaseEl } from "./hass-base-mixin"; +import { showDialog } from "../dialogs/make-dialog-manager"; +import type { Constructor } from "../types"; +import type { HassBaseEl } from "./hass-base-mixin"; +import type { MoreInfoDialogParams } from "../dialogs/more-info/ha-more-info-dialog"; +import type { PropertyValues } from "lit-element"; +import type { HASSDomEvent } from "../common/dom/fire_event"; declare global { // for fire event interface HASSDomEvents { - "hass-more-info": { - entityId: string | null; - }; + "hass-more-info": MoreInfoDialogParams; } } +let moreInfoImportPromise; +const importMoreInfo = () => { + if (!moreInfoImportPromise) { + moreInfoImportPromise = import( + /* webpackChunkName: "more-info-dialog" */ "../dialogs/more-info/ha-more-info-dialog" + ); + } + return moreInfoImportPromise; +}; + export default >(superClass: T) => class extends superClass { - private _moreInfoEl?: any; - - protected firstUpdated(changedProps) { + protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); - this.addEventListener("hass-more-info", (e) => this._handleMoreInfo(e)); + this.addEventListener("hass-more-info", (ev) => this._handleMoreInfo(ev)); // Load it once we are having the initial rendering done. - import( - /* webpackChunkName: "more-info-dialog" */ "../dialogs/more-info/ha-more-info-dialog" - ); + importMoreInfo(); } - private async _handleMoreInfo(ev) { - if (!this._moreInfoEl) { - this._moreInfoEl = document.createElement("ha-more-info-dialog"); - this.shadowRoot!.appendChild(this._moreInfoEl); - this.provideHass(this._moreInfoEl); - } - this._updateHass({ moreInfoEntityId: ev.detail.entityId }); + private async _handleMoreInfo(ev: HASSDomEvent) { + showDialog( + this, + this.shadowRoot!, + "ha-more-info-dialog", + { + entityId: ev.detail.entityId, + }, + importMoreInfo + ); } }; diff --git a/src/state/url-sync-mixin.ts b/src/state/url-sync-mixin.ts index d7178b3fe0..ae5078e5d0 100644 --- a/src/state/url-sync-mixin.ts +++ b/src/state/url-sync-mixin.ts @@ -1,9 +1,15 @@ /* eslint-disable no-console */ -import { fireEvent } from "../common/dom/fire_event"; +import { + closeDialog, + showDialog, + DialogState, + DialogClosedParams, +} from "../dialogs/make-dialog-manager"; import { Constructor } from "../types"; import { HassBaseEl } from "./hass-base-mixin"; +import { HASSDomEvent } from "../common/dom/fire_event"; -const DEBUG = false; +const DEBUG = true; export const urlSyncMixin = >( superClass: T @@ -12,81 +18,77 @@ export const urlSyncMixin = >( __DEMO__ ? superClass : class extends superClass { - private _ignoreNextHassChange = false; - - private _ignoreNextPopstate = false; - - private _moreInfoOpenedFromPath?: string; + private _ignoreNextPopState = false; public connectedCallback(): void { super.connectedCallback(); window.addEventListener("popstate", this._popstateChangeListener); + this.addEventListener("dialog-closed", this._dialogClosedListener); } public disconnectedCallback(): void { super.disconnectedCallback(); window.removeEventListener("popstate", this._popstateChangeListener); + this.removeEventListener("dialog-closed", this._dialogClosedListener); } - protected hassChanged(newHass, oldHass): void { - super.hassChanged(newHass, oldHass); - - if (this._ignoreNextHassChange) { - if (DEBUG) { - console.log("ignore hasschange"); - } - this._ignoreNextHassChange = false; - return; - } + private _dialogClosedListener = ( + ev: HASSDomEvent + ) => { + // If not closed by navigating back, and not a new dialog is open, remove the open state from history if ( - !oldHass || - oldHass.moreInfoEntityId === newHass.moreInfoEntityId + history.state?.open && + history.state?.dialog === ev.detail.dialog ) { - if (DEBUG) { - console.log("ignoring hass change"); - } - return; - } - - if (newHass.moreInfoEntityId) { - if (DEBUG) { - console.log("pushing state"); - } - // We keep track of where we opened moreInfo from so that we don't - // pop the state when we close the modal if the modal has navigated - // us away. - this._moreInfoOpenedFromPath = window.location.pathname; - history.pushState(null, "", window.location.pathname); - } else if ( - window.location.pathname === this._moreInfoOpenedFromPath - ) { - if (DEBUG) { - console.log("history back"); - } - this._ignoreNextPopstate = true; + this._ignoreNextPopState = true; history.back(); } - } + }; - private _popstateChangeListener = (ev) => { - if (this._ignoreNextPopstate) { - if (DEBUG) { - console.log("ignore popstate"); - } - this._ignoreNextPopstate = false; + private _popstateChangeListener = (ev: PopStateEvent) => { + if (this._ignoreNextPopState) { + this._ignoreNextPopState = false; return; } - - if (DEBUG) { - console.log("popstate", ev); - } - - if (this.hass && this.hass.moreInfoEntityId) { + if (ev.state && "dialog" in ev.state) { if (DEBUG) { - console.log("deselect entity"); + console.log("popstate", ev); } - this._ignoreNextHassChange = true; - fireEvent(this, "hass-more-info", { entityId: null }); + this._handleDialogStateChange(ev.state); } }; + + private async _handleDialogStateChange(state: DialogState) { + if (DEBUG) { + console.log("handle state", state); + } + if (!state.open) { + const closed = await closeDialog(state.dialog); + if (!closed) { + // dialog could not be closed, push state again + history.pushState( + { + dialog: state.dialog, + open: true, + dialogParams: null, + oldState: null, + }, + "" + ); + return; + } + if (state.oldState) { + this._handleDialogStateChange(state.oldState); + } + return; + } + if (state.dialogParams !== null) { + showDialog( + this, + this.shadowRoot!, + state.dialog, + state.dialogParams + ); + } + } };