diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index fe349178c1..01e84f32ca 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -48,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"]; @@ -404,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) diff --git a/src/mixins/prevent-unsaved-mixin.ts b/src/mixins/prevent-unsaved-mixin.ts new file mode 100644 index 0000000000..a1c7b5af92 --- /dev/null +++ b/src/mixins/prevent-unsaved-mixin.ts @@ -0,0 +1,57 @@ +import type { LitElement, PropertyValues } from "lit"; +import { isNavigationClick } from "../common/dom/is-navigation-click"; +import type { Constructor } from "../types"; + +export const PreventUnsavedMixin = >( + superClass: T +) => + 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()[0]; + if (!isNavigationClick(e)) { + return; + } + + const result = await this.promptDiscardChanges(); + if (result) { + this._removeListeners(); + if (target) { + const newEvent = new MouseEvent(e.type, e); + target.dispatchEvent(newEvent); + } + } + }; + + 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) { + window.addEventListener("click", this._handleClick, true); + window.addEventListener("beforeunload", this._handleUnload); + } else { + this._removeListeners(); + } + } + + public disconnectedCallback(): void { + super.disconnectedCallback(); + + this._removeListeners(); + } + + protected get 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..6f0a809257 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 get 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..23eaea4601 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 get 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..9e65ec9b33 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 get isDirty() { + return this._dirty; + } + + protected async promptDiscardChanges() { + return this._confirmUnsavedChanged(); + } + static get styles(): CSSResultGroup { return [ haStyle,