diff --git a/src/common/controllers/undo-redo-controller.ts b/src/common/controllers/undo-redo-controller.ts new file mode 100644 index 0000000000..df86df97b3 --- /dev/null +++ b/src/common/controllers/undo-redo-controller.ts @@ -0,0 +1,141 @@ +import type { + ReactiveController, + ReactiveControllerHost, +} from "@lit/reactive-element/reactive-controller"; + +const UNDO_REDO_STACK_LIMIT = 75; + +/** + * Configuration options for the UndoRedoController. + * + * @template ConfigType The type of configuration to manage. + */ +export interface UndoRedoControllerConfig { + stackLimit?: number; + currentConfig: () => ConfigType; + apply: (config: ConfigType) => void; +} + +/** + * A controller to manage undo and redo operations for a given configuration type. + * + * @template ConfigType The type of configuration to manage. + */ +export class UndoRedoController implements ReactiveController { + private _host: ReactiveControllerHost; + + private _undoStack: ConfigType[] = []; + + private _redoStack: ConfigType[] = []; + + private readonly _stackLimit: number = UNDO_REDO_STACK_LIMIT; + + private readonly _apply: (config: ConfigType) => void = () => { + throw new Error("No apply function provided"); + }; + + private readonly _currentConfig: () => ConfigType = () => { + throw new Error("No currentConfig function provided"); + }; + + constructor( + host: ReactiveControllerHost, + options: UndoRedoControllerConfig + ) { + if (options.stackLimit !== undefined) { + this._stackLimit = options.stackLimit; + } + + this._apply = options.apply; + this._currentConfig = options.currentConfig; + this._host = host; + host.addController(this); + } + + hostConnected() { + window.addEventListener("undo-change", this._onUndoChange); + } + + hostDisconnected() { + window.removeEventListener("undo-change", this._onUndoChange); + } + + private _onUndoChange = (ev: Event) => { + ev.stopPropagation(); + this.undo(); + this._host.requestUpdate(); + }; + + /** + * Indicates whether there are actions available to undo. + * + * @returns `true` if there are actions to undo, `false` otherwise. + */ + public get canUndo(): boolean { + return this._undoStack.length > 0; + } + + /** + * Indicates whether there are actions available to redo. + * + * @returns `true` if there are actions to redo, `false` otherwise. + */ + public get canRedo(): boolean { + return this._redoStack.length > 0; + } + + /** + * Commits the current configuration to the undo stack and clears the redo stack. + * + * @param config The current configuration to commit. + */ + public commit(config: ConfigType) { + if (this._undoStack.length >= this._stackLimit) { + this._undoStack.shift(); + } + this._undoStack.push({ ...config }); + this._redoStack = []; + } + + /** + * Undoes the last action and applies the previous configuration + * while saving the current configuration to the redo stack. + */ + public undo() { + if (this._undoStack.length === 0) { + return; + } + this._redoStack.push({ ...this._currentConfig() }); + const config = this._undoStack.pop()!; + this._apply(config); + this._host.requestUpdate(); + } + + /** + * Redoes the last undone action and reapplies the configuration + * while saving the current configuration to the undo stack. + */ + public redo() { + if (this._redoStack.length === 0) { + return; + } + this._undoStack.push({ ...this._currentConfig() }); + const config = this._redoStack.pop()!; + this._apply(config); + this._host.requestUpdate(); + } + + /** + * Resets the undo and redo stacks, clearing all history. + */ + public reset() { + this._undoStack = []; + this._redoStack = []; + } +} + +declare global { + interface HASSDomEvents { + "undo-change": undefined; + } +} diff --git a/src/mixins/undo-redo-mixin.ts b/src/mixins/undo-redo-mixin.ts deleted file mode 100644 index 389c286faf..0000000000 --- a/src/mixins/undo-redo-mixin.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { LitElement } from "lit"; -import type { Constructor } from "../types"; - -export const UndoRedoMixin = , ConfigType>( - superClass: T -) => { - class UndoRedoClass extends superClass { - private _undoStack: ConfigType[] = []; - - private _redoStack: ConfigType[] = []; - - protected _undoStackLimit = 75; - - protected pushToUndo(config: ConfigType) { - if (this._undoStack.length >= this._undoStackLimit) { - this._undoStack.shift(); - } - this._undoStack.push({ ...config }); - this._redoStack = []; - } - - public undo() { - const currentConfig = this.currentConfig; - if (this._undoStack.length === 0 || !currentConfig) { - return; - } - this._redoStack.push({ ...currentConfig }); - const config = this._undoStack.pop()!; - this.applyUndoRedo(config); - } - - public redo() { - const currentConfig = this.currentConfig; - if (this._redoStack.length === 0 || !currentConfig) { - return; - } - this._undoStack.push({ ...currentConfig }); - const config = this._redoStack.pop()!; - this.applyUndoRedo(config); - } - - public get canUndo(): boolean { - return this._undoStack.length > 0; - } - - public get canRedo(): boolean { - return this._redoStack.length > 0; - } - - protected get currentConfig(): ConfigType | undefined { - return undefined; - } - - protected applyUndoRedo(_: ConfigType) { - throw new Error("applyUndoRedo not implemented"); - } - } - - return UndoRedoClass; -}; diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index fc43b8fa66..2a412caedc 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -74,7 +74,7 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info import "../../../layouts/hass-subpage"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin"; -import { UndoRedoMixin } from "../../../mixins/undo-redo-mixin"; +import { UndoRedoController } from "../../../common/controllers/undo-redo-controller"; import { haStyle } from "../../../resources/styles"; import type { Entries, HomeAssistant, Route } from "../../../types"; import { isMac } from "../../../util/is_mac"; @@ -111,12 +111,9 @@ declare global { } } -const baseEditorMixins = PreventUnsavedMixin(KeyboardShortcutMixin(LitElement)); - -export class HaAutomationEditor extends UndoRedoMixin< - typeof baseEditorMixins, - AutomationConfig ->(baseEditorMixins) { +export class HaAutomationEditor extends PreventUnsavedMixin( + KeyboardShortcutMixin(LitElement) +) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public automationId: string | null = null; @@ -183,6 +180,11 @@ export class HaAutomationEditor extends UndoRedoMixin< value: PromiseLike | EntityRegistryEntry ) => void; + private _undoRedoController = new UndoRedoController(this, { + apply: (config) => this._applyUndoRedo(config), + currentConfig: () => this._config!, + }); + protected willUpdate(changedProps) { super.willUpdate(changedProps); @@ -235,8 +237,8 @@ export class HaAutomationEditor extends UndoRedoMixin< slot="toolbar-icon" .label=${this.hass.localize("ui.common.undo")} .path=${mdiUndo} - @click=${this.undo} - .disabled=${!this.canUndo} + @click=${this._undo} + .disabled=${!this._undoRedoController.canUndo} id="button-undo" > @@ -253,8 +255,8 @@ export class HaAutomationEditor extends UndoRedoMixin< slot="toolbar-icon" .label=${this.hass.localize("ui.common.redo")} .path=${mdiRedo} - @click=${this.redo} - .disabled=${!this.canRedo} + @click=${this._redo} + .disabled=${!this._undoRedoController.canRedo} id="button-redo" > @@ -298,16 +300,16 @@ export class HaAutomationEditor extends UndoRedoMixin< ${this._mode === "gui" && this.narrow ? html` ${this.hass.localize("ui.common.undo")} ${this.hass.localize("ui.common.redo")} @@ -518,7 +520,6 @@ export class HaAutomationEditor extends UndoRedoMixin< @value-changed=${this._valueChanged} @save-automation=${this._handleSaveAutomation} @editor-save=${this._handleSaveAutomation} - @undo-paste=${this.undo} >
${this._errors || stateObj?.state === UNAVAILABLE @@ -791,7 +792,7 @@ export class HaAutomationEditor extends UndoRedoMixin< ev.stopPropagation(); if (this._config) { - this.pushToUndo(this._config); + this._undoRedoController.commit(this._config); } this._config = ev.detail.value; @@ -1202,9 +1203,9 @@ export class HaAutomationEditor extends UndoRedoMixin< x: () => this._cutSelectedRow(), Delete: () => this._deleteSelectedRow(), Backspace: () => this._deleteSelectedRow(), - z: () => this.undo(), - Z: () => this.redo(), - y: () => this.redo(), + z: () => this._undo(), + Z: () => this._redo(), + y: () => this._redo(), }; } @@ -1238,16 +1239,20 @@ export class HaAutomationEditor extends UndoRedoMixin< this._manualEditor?.deleteSelectedRow(); } - protected get currentConfig() { - return this._config; - } - - protected applyUndoRedo(config: AutomationConfig) { + private _applyUndoRedo(config: AutomationConfig) { this._manualEditor?.triggerCloseSidebar(); this._config = config; this._dirty = true; } + private _undo() { + this._undoRedoController.undo(); + } + + private _redo() { + this._undoRedoController.redo(); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts index 063ecd08e1..ee915bdde5 100644 --- a/src/panels/config/automation/manual-automation-editor.ts +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -616,7 +616,7 @@ export class HaManualAutomationEditor extends LitElement { action: { text: this.hass.localize("ui.common.undo"), action: () => { - fireEvent(this, "undo-paste"); + fireEvent(this, "undo-change"); this._pastedConfig = undefined; }, @@ -742,6 +742,5 @@ declare global { "open-sidebar": SidebarConfig; "request-close-sidebar": undefined; "close-sidebar": undefined; - "undo-paste": undefined; } } diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index e65ea20596..136f4dc2af 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -65,7 +65,7 @@ import "../../../layouts/hass-subpage"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; -import { UndoRedoMixin } from "../../../mixins/undo-redo-mixin"; +import { UndoRedoController } from "../../../common/controllers/undo-redo-controller"; import { haStyle } from "../../../resources/styles"; import type { Entries, HomeAssistant, Route } from "../../../types"; import { isMac } from "../../../util/is_mac"; @@ -78,14 +78,9 @@ import "./blueprint-script-editor"; import "./manual-script-editor"; import type { HaManualScriptEditor } from "./manual-script-editor"; -const baseEditorMixins = SubscribeMixin( +export class HaScriptEditor extends SubscribeMixin( PreventUnsavedMixin(KeyboardShortcutMixin(LitElement)) -); - -export class HaScriptEditor extends UndoRedoMixin< - typeof baseEditorMixins, - ScriptConfig ->(baseEditorMixins) { +) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public scriptId: string | null = null; @@ -141,6 +136,11 @@ export class HaScriptEditor extends UndoRedoMixin< value: PromiseLike | EntityRegistryEntry ) => void; + private _undoRedoController = new UndoRedoController(this, { + apply: (config) => this._applyUndoRedo(config), + currentConfig: () => this._config!, + }); + protected willUpdate(changedProps) { super.willUpdate(changedProps); @@ -188,8 +188,8 @@ export class HaScriptEditor extends UndoRedoMixin< slot="toolbar-icon" .label=${this.hass.localize("ui.common.undo")} .path=${mdiUndo} - @click=${this.undo} - .disabled=${!this.canUndo} + @click=${this._undo} + .disabled=${!this._undoRedoController.canUndo} id="button-undo" > @@ -205,8 +205,8 @@ export class HaScriptEditor extends UndoRedoMixin< slot="toolbar-icon" .label=${this.hass.localize("ui.common.redo")} .path=${mdiRedo} - @click=${this.redo} - .disabled=${!this.canRedo} + @click=${this._redo} + .disabled=${!this._undoRedoController.canRedo} id="button-redo" > @@ -249,16 +249,16 @@ export class HaScriptEditor extends UndoRedoMixin< ${this._mode === "gui" && this.narrow ? html` ${this.hass.localize("ui.common.undo")} ${this.hass.localize("ui.common.redo")} @@ -463,7 +463,6 @@ export class HaScriptEditor extends UndoRedoMixin< @value-changed=${this._valueChanged} @editor-save=${this._handleSaveScript} @save-script=${this._handleSaveScript} - @undo-paste=${this.undo} >
${this._errors || stateObj?.state === UNAVAILABLE @@ -679,7 +678,7 @@ export class HaScriptEditor extends UndoRedoMixin< private _valueChanged(ev) { if (this._config) { - this.pushToUndo(this._config); + this._undoRedoController.commit(this._config); } this._config = ev.detail.value; @@ -776,7 +775,7 @@ export class HaScriptEditor extends UndoRedoMixin< } if (this._config) { - this.pushToUndo(this._config); + this._undoRedoController.commit(this._config); } this._manualEditor?.addFields(); @@ -1110,9 +1109,9 @@ export class HaScriptEditor extends UndoRedoMixin< x: () => this._cutSelectedRow(), Delete: () => this._deleteSelectedRow(), Backspace: () => this._deleteSelectedRow(), - z: () => this.undo(), - Z: () => this.redo(), - y: () => this.redo(), + z: () => this._undo(), + Z: () => this._redo(), + y: () => this._redo(), }; } @@ -1146,16 +1145,20 @@ export class HaScriptEditor extends UndoRedoMixin< this._manualEditor?.deleteSelectedRow(); } - protected get currentConfig() { - return this._config; - } - - protected applyUndoRedo(config: ScriptConfig) { + private _applyUndoRedo(config: ScriptConfig) { this._manualEditor?.triggerCloseSidebar(); this._config = config; this._dirty = true; } + private _undo() { + this._undoRedoController.undo(); + } + + private _redo() { + this._undoRedoController.redo(); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/script/manual-script-editor.ts b/src/panels/config/script/manual-script-editor.ts index ed4d430e1d..2c462ea6ff 100644 --- a/src/panels/config/script/manual-script-editor.ts +++ b/src/panels/config/script/manual-script-editor.ts @@ -491,7 +491,7 @@ export class HaManualScriptEditor extends LitElement { action: { text: this.hass.localize("ui.common.undo"), action: () => { - fireEvent(this, "undo-paste"); + fireEvent(this, "undo-change"); this._pastedConfig = undefined; },