From 91777d45b0b7af10c4eecf580b20978777294a03 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 5 Dec 2024 19:49:27 +0100 Subject: [PATCH 1/7] Prevent leaving the editor if there are unsaved changes --- src/mixins/prevent-unsaved-mixin.ts | 55 +++++++++++++++++++ .../config/automation/ha-automation-editor.ts | 13 ++++- src/panels/config/scene/ha-scene-editor.ts | 13 ++++- src/panels/config/script/ha-script-editor.ts | 13 ++++- 4 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 src/mixins/prevent-unsaved-mixin.ts diff --git a/src/mixins/prevent-unsaved-mixin.ts b/src/mixins/prevent-unsaved-mixin.ts new file mode 100644 index 0000000000..7bc4ce8a91 --- /dev/null +++ b/src/mixins/prevent-unsaved-mixin.ts @@ -0,0 +1,55 @@ +import type { LitElement } from "lit"; +import type { Constructor } from "../types"; +import { isNavigationClick } from "../common/dom/is-navigation-click"; +import { navigate } from "../common/navigate"; + +export const PreventUnsavedMixin = >( + superClass: T +) => + class extends superClass { + private _handleClick = async (e: MouseEvent) => { + const href = isNavigationClick(e); + + if (!href) { + return; + } + + e.preventDefault(); + const result = await this.promptDiscardChanges(); + if (result) { + navigate(href); + } + }; + + private _handleUnload = (e: BeforeUnloadEvent) => { + if (this.isDirty()) { + e.preventDefault(); + } + }; + + public connectedCallback(): void { + super.connectedCallback(); + + document.body.addEventListener("mousedown", this._handleClick, { + capture: true, + }); + window.addEventListener("beforeunload", this._handleUnload); + } + + public disconnectedCallback(): void { + super.disconnectedCallback(); + + document.body.removeEventListener("click", this._handleClick, { + capture: true, + }); + window.removeEventListener("beforeunload", this._handleUnload); + } + + protected isDirty(): boolean { + return false; + } + + protected async promptDiscardChanges(): Promise { + return true; + } + }; diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index de9ef7e788..fd2c514e43 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -64,6 +64,7 @@ import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-a import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename"; import "./blueprint-automation-editor"; import "./manual-automation-editor"; +import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin"; declare global { interface HTMLElementTagNameMap { @@ -82,7 +83,9 @@ declare global { } } -export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { +export class HaAutomationEditor extends PreventUnsavedMixin( + KeyboardShortcutMixin(LitElement) +) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public automationId: string | null = null; @@ -847,6 +850,14 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { }; } + protected isDirty() { + return this._dirty; + } + + protected async promptDiscardChanges() { + return this.confirmUnsavedChanged(); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index ddcb1ca7e8..f34aeff1a1 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -77,6 +77,7 @@ import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import { showToast } from "../../../util/toast"; import "../ha-config-section"; +import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin"; interface DeviceEntities { id: string; @@ -89,8 +90,8 @@ interface DeviceEntitiesLookup { } @customElement("ha-scene-editor") -export class HaSceneEditor extends SubscribeMixin( - KeyboardShortcutMixin(LitElement) +export class HaSceneEditor extends PreventUnsavedMixin( + SubscribeMixin(KeyboardShortcutMixin(LitElement)) ) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -1225,6 +1226,14 @@ export class HaSceneEditor extends SubscribeMixin( }); } + protected isDirty() { + return this._dirty; + } + + protected async promptDiscardChanges() { + return this.confirmUnsavedChanged(); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 0337604713..a7ab8c131c 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -57,8 +57,11 @@ import "./blueprint-script-editor"; import "./manual-script-editor"; import type { HaManualScriptEditor } from "./manual-script-editor"; import { substituteBlueprint } from "../../../data/blueprint"; +import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin"; -export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { +export class HaScriptEditor extends PreventUnsavedMixin( + KeyboardShortcutMixin(LitElement) +) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public scriptId: string | null = null; @@ -813,6 +816,14 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { }; } + protected isDirty() { + return this._dirty; + } + + protected async promptDiscardChanges() { + return this.confirmUnsavedChanged(); + } + static get styles(): CSSResultGroup { return [ haStyle, From 96b9d25bc57a237eb55a341ca956d6a3ea842e09 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 6 Dec 2024 12:02:26 +0100 Subject: [PATCH 2/7] Process code review --- src/components/ha-sidebar.ts | 13 +++++++ src/mixins/prevent-unsaved-mixin.ts | 57 ++++++++++++++++++----------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index fe349178c1..509996946a 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -27,6 +27,7 @@ import { LitElement, css, html, nothing } from "lit"; import { customElement, eventOptions, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; +import type { PaperListboxElement } from "@polymer/paper-listbox"; import { storage } from "../common/decorators/storage"; import { fireEvent } from "../common/dom/fire_event"; import { toggleAttribute } from "../common/dom/toggle_attribute"; @@ -287,6 +288,13 @@ class HaSidebar extends SubscribeMixin(LitElement) { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); this._subscribePersistentNotifications(); + + window.addEventListener("hass-reset-sidebar", (ev) => { + const sidebar = this.shadowRoot?.getElementById( + "sidebar" + ) as PaperListboxElement; + sidebar.selected = ev.detail; + }); } private _subscribePersistentNotifications(): void { @@ -397,6 +405,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { // prettier-ignore return html` >( superClass: T ) => class extends superClass { private _handleClick = async (e: MouseEvent) => { - const href = isNavigationClick(e); - - if (!href) { + // get the right target, otherwise the composedPath would return in the new event + const target = e + .composedPath() + .find( + (n) => (n as HTMLElement).tagName === "HA-SVG-ICON" + ) as HTMLAnchorElement; + if (!isNavigationClick(e)) { return; } - e.preventDefault(); const result = await this.promptDiscardChanges(); if (result) { - navigate(href); + this._removeListeners(); + if (target) { + const newEvent = new MouseEvent(e.type, e); + target.dispatchEvent(newEvent); + } + } else { + fireEvent(this, "hass-reset-sidebar", this.getPanel()); } }; - private _handleUnload = (e: BeforeUnloadEvent) => { + private _handleUnload = (e: BeforeUnloadEvent) => e.preventDefault(); + + private _removeListeners() { + window.removeEventListener("click", this._handleClick, true); + window.removeEventListener("beforeunload", this._handleUnload); + } + + public willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + if (this.isDirty()) { - e.preventDefault(); + window.addEventListener("click", this._handleClick, true); + window.addEventListener("beforeunload", this._handleUnload); + } else { + this._removeListeners(); } - }; - - public connectedCallback(): void { - super.connectedCallback(); - - document.body.addEventListener("mousedown", this._handleClick, { - capture: true, - }); - window.addEventListener("beforeunload", this._handleUnload); } public disconnectedCallback(): void { super.disconnectedCallback(); - document.body.removeEventListener("click", this._handleClick, { - capture: true, - }); - window.removeEventListener("beforeunload", this._handleUnload); + this._removeListeners(); } protected isDirty(): boolean { @@ -52,4 +61,8 @@ export const PreventUnsavedMixin = >( protected async promptDiscardChanges(): Promise { return true; } + + protected getPanel(): string { + return "config"; + } }; From 70532ac3bf5369a9a669b56163945b2c1a685bc1 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 6 Dec 2024 12:44:53 +0100 Subject: [PATCH 3/7] use first composePath target --- src/mixins/prevent-unsaved-mixin.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/mixins/prevent-unsaved-mixin.ts b/src/mixins/prevent-unsaved-mixin.ts index 8f997ce5f9..7974d3f940 100644 --- a/src/mixins/prevent-unsaved-mixin.ts +++ b/src/mixins/prevent-unsaved-mixin.ts @@ -9,11 +9,7 @@ export const PreventUnsavedMixin = >( class extends superClass { private _handleClick = async (e: MouseEvent) => { // get the right target, otherwise the composedPath would return in the new event - const target = e - .composedPath() - .find( - (n) => (n as HTMLElement).tagName === "HA-SVG-ICON" - ) as HTMLAnchorElement; + const target = e.composedPath()[0]; if (!isNavigationClick(e)) { return; } From 3579d82e8eb59f1f8ef65cac14e4f6f250d5a45b Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 6 Dec 2024 12:58:29 +0100 Subject: [PATCH 4/7] fix function calls --- src/panels/config/automation/ha-automation-editor.ts | 2 +- src/panels/config/scene/ha-scene-editor.ts | 2 +- src/panels/config/script/ha-script-editor.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index fd2c514e43..f6863e3560 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -855,7 +855,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin( } protected async promptDiscardChanges() { - return this.confirmUnsavedChanged(); + return this._confirmUnsavedChanged(); } static get styles(): CSSResultGroup { diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index f34aeff1a1..e6b0e25d34 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -1231,7 +1231,7 @@ export class HaSceneEditor extends PreventUnsavedMixin( } protected async promptDiscardChanges() { - return this.confirmUnsavedChanged(); + return this._confirmUnsavedChanged(); } static get styles(): CSSResultGroup { diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index a7ab8c131c..a9f59bd195 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -821,7 +821,7 @@ export class HaScriptEditor extends PreventUnsavedMixin( } protected async promptDiscardChanges() { - return this.confirmUnsavedChanged(); + return this._confirmUnsavedChanged(); } static get styles(): CSSResultGroup { From ec20f7e2c46375562a9e15be72b712d15224d809 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 9 Dec 2024 08:58:05 +0100 Subject: [PATCH 5/7] Use query instead --- src/components/ha-sidebar.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 509996946a..36cdbf11b9 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -24,7 +24,13 @@ import "@polymer/paper-listbox/paper-listbox"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResult, CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; -import { customElement, eventOptions, property, state } from "lit/decorators"; +import { + customElement, + eventOptions, + property, + query, + state, +} from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import type { PaperListboxElement } from "@polymer/paper-listbox"; @@ -197,6 +203,8 @@ class HaSidebar extends SubscribeMixin(LitElement) { @state() private _issuesCount = 0; + @query("paper-listbox", true) private _sidebar!: PaperListboxElement; + private _mouseLeaveTimeout?: number; private _tooltipHideTimeout?: number; @@ -290,10 +298,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { this._subscribePersistentNotifications(); window.addEventListener("hass-reset-sidebar", (ev) => { - const sidebar = this.shadowRoot?.getElementById( - "sidebar" - ) as PaperListboxElement; - sidebar.selected = ev.detail; + this._sidebar.selected = ev.detail; }); } From 84322a21fe753cf9c5a14ec76c9575eebbedd563 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 10 Dec 2024 09:02:26 +0100 Subject: [PATCH 6/7] Remove id on sidebar --- src/components/ha-sidebar.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 36cdbf11b9..8b9d939aef 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -410,7 +410,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { // prettier-ignore return html` Date: Wed, 11 Dec 2024 16:08:03 +0100 Subject: [PATCH 7/7] suggestions --- src/components/ha-sidebar.ts | 21 +++---------------- src/mixins/prevent-unsaved-mixin.ts | 13 +++--------- .../config/automation/ha-automation-editor.ts | 2 +- src/panels/config/scene/ha-scene-editor.ts | 2 +- src/panels/config/script/ha-script-editor.ts | 2 +- 5 files changed, 9 insertions(+), 31 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 8b9d939aef..01e84f32ca 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -24,16 +24,9 @@ import "@polymer/paper-listbox/paper-listbox"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResult, CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; -import { - customElement, - eventOptions, - property, - query, - state, -} from "lit/decorators"; +import { customElement, eventOptions, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; -import type { PaperListboxElement } from "@polymer/paper-listbox"; import { storage } from "../common/decorators/storage"; import { fireEvent } from "../common/dom/fire_event"; import { toggleAttribute } from "../common/dom/toggle_attribute"; @@ -55,6 +48,7 @@ import "./ha-menu-button"; import "./ha-sortable"; import "./ha-svg-icon"; import "./user/ha-user-badge"; +import { preventDefault } from "../common/dom/prevent_default"; const SHOW_AFTER_SPACER = ["config", "developer-tools"]; @@ -203,8 +197,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { @state() private _issuesCount = 0; - @query("paper-listbox", true) private _sidebar!: PaperListboxElement; - private _mouseLeaveTimeout?: number; private _tooltipHideTimeout?: number; @@ -296,10 +288,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); this._subscribePersistentNotifications(); - - window.addEventListener("hass-reset-sidebar", (ev) => { - this._sidebar.selected = ev.detail; - }); } private _subscribePersistentNotifications(): void { @@ -417,6 +405,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { @focusout=${this._listboxFocusOut} @scroll=${this._listboxScroll} @keydown=${this._listboxKeydown} + @iron-activate=${preventDefault} > ${this.editMode ? this._renderPanelsEdit(beforeSpacer) @@ -1137,8 +1126,4 @@ declare global { interface HTMLElementTagNameMap { "ha-sidebar": HaSidebar; } - - interface HASSDomEvents { - "hass-reset-sidebar": string; - } } diff --git a/src/mixins/prevent-unsaved-mixin.ts b/src/mixins/prevent-unsaved-mixin.ts index 7974d3f940..a1c7b5af92 100644 --- a/src/mixins/prevent-unsaved-mixin.ts +++ b/src/mixins/prevent-unsaved-mixin.ts @@ -1,7 +1,6 @@ import type { LitElement, PropertyValues } from "lit"; -import type { Constructor } from "../types"; import { isNavigationClick } from "../common/dom/is-navigation-click"; -import { fireEvent } from "../common/dom/fire_event"; +import type { Constructor } from "../types"; export const PreventUnsavedMixin = >( superClass: T @@ -21,8 +20,6 @@ export const PreventUnsavedMixin = >( const newEvent = new MouseEvent(e.type, e); target.dispatchEvent(newEvent); } - } else { - fireEvent(this, "hass-reset-sidebar", this.getPanel()); } }; @@ -36,7 +33,7 @@ export const PreventUnsavedMixin = >( public willUpdate(changedProperties: PropertyValues): void { super.willUpdate(changedProperties); - if (this.isDirty()) { + if (this.isDirty) { window.addEventListener("click", this._handleClick, true); window.addEventListener("beforeunload", this._handleUnload); } else { @@ -50,15 +47,11 @@ export const PreventUnsavedMixin = >( this._removeListeners(); } - protected isDirty(): boolean { + protected get isDirty(): boolean { return false; } protected async promptDiscardChanges(): Promise { return true; } - - protected getPanel(): string { - return "config"; - } }; diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index f6863e3560..6f0a809257 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -850,7 +850,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin( }; } - protected isDirty() { + protected get isDirty() { return this._dirty; } diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index e6b0e25d34..23eaea4601 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -1226,7 +1226,7 @@ export class HaSceneEditor extends PreventUnsavedMixin( }); } - protected isDirty() { + protected get isDirty() { return this._dirty; } diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index a9f59bd195..9e65ec9b33 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -816,7 +816,7 @@ export class HaScriptEditor extends PreventUnsavedMixin( }; } - protected isDirty() { + protected get isDirty() { return this._dirty; }