From 7c8dc7e3756c6e4b371f74cf996d82acd603ebb3 Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Fri, 28 Jun 2024 02:10:27 +0300
Subject: [PATCH 01/16] feat(uip-editor): store editor state

---
 src/core/base/root.ts                | 11 +++++---
 src/plugins/editor/editor-storage.ts | 39 ++++++++++++++++++++++++++++
 src/plugins/editor/editor.tsx        | 20 ++++++++++++++
 3 files changed, 66 insertions(+), 4 deletions(-)
 create mode 100644 src/plugins/editor/editor-storage.ts

diff --git a/src/core/base/root.ts b/src/core/base/root.ts
index 1246374f..5cc1521a 100644
--- a/src/core/base/root.ts
+++ b/src/core/base/root.ts
@@ -3,9 +3,10 @@ import {
   memoize,
   boolAttr,
   listen,
-  prop
+  prop,
+  attr
 } from '@exadel/esl/modules/esl-utils/decorators';
-
+import {sequentialUID} from '@exadel/esl/modules/esl-utils/misc';
 import {UIPStateModel} from './model';
 
 import type {UIPSnippetTemplate} from './snippet';
@@ -33,6 +34,8 @@ export class UIPRoot extends ESLBaseElement {
   /** CSS query for snippets */
   public static SNIPPET_SEL = '[uip-snippet]';
 
+  @attr() public uipId: string = sequentialUID(UIPRoot.is);
+
   /** Indicates that the UIP components' theme is dark */
   @boolAttr() public darkTheme: boolean;
 
@@ -50,7 +53,7 @@ export class UIPRoot extends ESLBaseElement {
     return Array.from(this.querySelectorAll(UIPRoot.SNIPPET_SEL));
   }
 
-  protected delyedScrollIntoView(): void {
+  protected delayedScrollIntoView(): void {
     setTimeout(() => {
       this.scrollIntoView({behavior: 'smooth', block: 'start'});
     }, 100);
@@ -64,7 +67,7 @@ export class UIPRoot extends ESLBaseElement {
     this.$$fire(this.READY_EVENT, {bubbles: false});
 
     if (this.model.anchorSnippet) {
-      this.delyedScrollIntoView();
+      this.delayedScrollIntoView();
     }
   }
 
diff --git a/src/plugins/editor/editor-storage.ts b/src/plugins/editor/editor-storage.ts
new file mode 100644
index 00000000..c7668b45
--- /dev/null
+++ b/src/plugins/editor/editor-storage.ts
@@ -0,0 +1,39 @@
+interface EditorStorageEntry {
+  ts: string;
+  data: string;
+}
+
+export class EditorStorage {
+  public static readonly STORAGE_KEY = 'uip-editor-storage';
+
+  protected static get(): Record<string, any> {
+    return JSON.parse(localStorage.getItem(EditorStorage.STORAGE_KEY) || '{}');
+  }
+
+  protected static set(value: Record<string, any>): void {
+    localStorage.setItem(EditorStorage.STORAGE_KEY, JSON.stringify(value));
+  }
+
+  protected static serializeWithPathname(key: string): string {
+    return JSON.stringify({path: location.pathname, key});
+  }
+
+  public static save(key: string, value: string): void {
+    const state = {[EditorStorage.serializeWithPathname(key)]: {ts: Date.now(), value}};
+    EditorStorage.set(Object.assign(EditorStorage.get(), state));
+  }
+
+  public static load(key: string): string | null {
+    const entry = EditorStorage.get()[EditorStorage.serializeWithPathname(key)] || {} as EditorStorageEntry;
+    const expirationTime = 3600000 * 12;
+    if (entry?.ts + expirationTime > Date.now()) return entry.value || null;
+    EditorStorage.remove(key);
+    return null;
+  }
+
+  public static remove(key: string): void {
+    const data = EditorStorage.get();
+    delete data[key];
+    EditorStorage.set(data);
+  }
+}
diff --git a/src/plugins/editor/editor.tsx b/src/plugins/editor/editor.tsx
index 49e19396..8f410db1 100644
--- a/src/plugins/editor/editor.tsx
+++ b/src/plugins/editor/editor.tsx
@@ -13,6 +13,7 @@ import {attr, boolAttr, decorate, listen, memoize} from '@exadel/esl/modules/esl
 import {UIPPluginPanel} from '../../core/panel/plugin-panel';
 import {CopyIcon} from '../copy/copy-button.icon';
 
+import {EditorStorage} from './editor-storage';
 import {EditorIcon} from './editor.icon';
 
 import type {UIPSnippetsList} from '../snippets-list/snippets-list';
@@ -165,12 +166,31 @@ export class UIPEditor extends UIPPluginPanel {
       case 'html':
         if (e && !e.htmlChanges.length) return;
         this.value = this.model!.html;
+        if (e && !e.force) this.saveState();
     }
   }
 
+  protected saveState(): void {
+    const key = this.getStateKey();
+    if (key && this.value) EditorStorage.save(key, this.value);
+  }
+
+  protected getStateKey(): string | null {
+    if (!this.model?.activeSnippet || !this.$root) return null;
+    return JSON.stringify({html: this.model.activeSnippet.html, id: this.$root.uipId});
+  }
+
   /** Handles snippet change to set readonly value */
   @listen({event: 'uip:snippet:change', target: ($this: UIPSnippetsList) => $this.$root})
   protected _onSnippetChange(): void {
     this.editable = this.isSnippetEditable;
+    this.loadState();
+  }
+
+  protected loadState(): void {
+    if (!this.model?.activeSnippet) return;
+    const key = this.getStateKey();
+    const state = key && EditorStorage.load(key);
+    if (state) this.model.setHtml(state, this);
   }
 }

From 06b246c85a20289b528b021ea7b4a33bbf827c82 Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Wed, 3 Jul 2024 14:30:16 +0300
Subject: [PATCH 02/16] chore(uip-editor): code refactoring

---
 src/core/base/model.storage.ts       | 62 ++++++++++++++++++++++++++++
 src/core/base/model.ts               | 14 +++++--
 src/core/base/root.ts                |  9 ++--
 src/plugins/editor/editor-storage.ts | 39 -----------------
 src/plugins/editor/editor.tsx        | 20 ---------
 5 files changed, 76 insertions(+), 68 deletions(-)
 create mode 100644 src/core/base/model.storage.ts
 delete mode 100644 src/plugins/editor/editor-storage.ts

diff --git a/src/core/base/model.storage.ts b/src/core/base/model.storage.ts
new file mode 100644
index 00000000..c241497a
--- /dev/null
+++ b/src/core/base/model.storage.ts
@@ -0,0 +1,62 @@
+import type {UIPStateModel} from './model';
+
+interface UIPStateStorageEntry {
+  ts: string;
+  snippets: string;
+}
+
+interface UIPStateModelSnippets {
+  js: string;
+  html: string;
+  note: string;
+}
+
+export class UIPStateStorage {
+  public static readonly STORAGE_KEY = 'uip-editor-storage';
+
+  public constructor(protected model: UIPStateModel) {}
+
+  protected static loadEntry(key: string): string | null {
+    const entry = (this.lsGet()[key] || {}) as UIPStateStorageEntry;
+    const expirationTime = 3600000 * 12;
+    if (parseInt(entry?.ts, 10) + expirationTime > Date.now()) return entry.snippets || null;
+    this.removeEntry(key);
+    return null;
+  }
+
+  protected static saveEntry(key: string, value: string): void {
+    this.lsSet(Object.assign(this.lsGet(), {[key]: {ts: Date.now(), snippets: value}}));
+  }
+
+  protected static removeEntry(key: string): void {
+    const data = this.lsGet();
+    delete data[key];
+    this.lsSet(data);
+  }
+
+  protected static lsGet(): Record<string, any> {
+    return JSON.parse(localStorage.getItem(this.STORAGE_KEY) || '{}');
+  }
+
+  protected static lsSet(value: Record<string, any>): void {
+    localStorage.setItem(this.STORAGE_KEY, JSON.stringify(value));
+  }
+
+  protected getStateKey(): string | null {
+    if (!this.model?.activeSnippet) return null;
+    const {id, activeSnippet} = this.model;
+    return JSON.stringify({id, path: location.pathname, snippet: activeSnippet.html});
+  }
+
+  public loadState(): UIPStateModelSnippets | undefined {
+    const key = this.getStateKey();
+    const state = key && UIPStateStorage.loadEntry(key);
+    if (state) return JSON.parse(state);
+  }
+
+  public saveState(): void {
+    const {js, html, note} = this.model;
+    const key = this.getStateKey();
+    key && UIPStateStorage.saveEntry(key, JSON.stringify({js, html, note}));
+  }
+}
diff --git a/src/core/base/model.ts b/src/core/base/model.ts
index 868b6c5f..47f57978 100644
--- a/src/core/base/model.ts
+++ b/src/core/base/model.ts
@@ -1,6 +1,7 @@
 import {SyntheticEventTarget} from '@exadel/esl/modules/esl-utils/dom';
 import {decorate} from '@exadel/esl/modules/esl-utils/decorators';
 import {microtask} from '@exadel/esl/modules/esl-utils/async';
+import {sequentialUID} from '@exadel/esl/modules/esl-utils/misc';
 
 import {
   UIPJSNormalizationPreprocessors,
@@ -8,6 +9,7 @@ import {
   UIPNoteNormalizationPreprocessors
 } from '../processors/normalization';
 
+import {UIPStateStorage} from './model.storage';
 import {UIPSnippetItem} from './snippet';
 
 import type {UIPRoot} from './root';
@@ -48,6 +50,8 @@ export class UIPStateModel extends SyntheticEventTarget {
   /** Snippets {@link UIPSnippetItem} value objects */
   private _snippets: UIPSnippetItem[];
 
+  public readonly id = sequentialUID('uip-model-id-');
+
   /** Current js state */
   private _js: string = '';
   /** Current note state */
@@ -58,6 +62,8 @@ export class UIPStateModel extends SyntheticEventTarget {
   /** Last changes history (used to dispatch changes) */
   private _changes: UIPChangeInfo[] = [];
 
+  protected storage = new UIPStateStorage(this);
+
   /**
    * Sets current js state to the passed one
    * @param js - new state
@@ -145,9 +151,10 @@ export class UIPStateModel extends SyntheticEventTarget {
   ): void {
     if (!snippet) return;
     this._snippets.forEach((s) => (s.active = s === snippet));
-    this.setHtml(snippet.html, modifier, true);
-    this.setJS(snippet.js, modifier);
-    this.setNote(snippet.note, modifier);
+    const {js, html, note} = this.storage.loadState() || snippet;
+    this.setHtml(html, modifier, true);
+    this.setJS(js, modifier);
+    this.setNote(note, modifier);
     this.dispatchEvent(
       new CustomEvent('uip:model:snippet:change', {detail: this})
     );
@@ -190,6 +197,7 @@ export class UIPStateModel extends SyntheticEventTarget {
     if (!this._changes.length) return;
     const detail = this._changes;
     this._changes = [];
+    this.storage.saveState();
     this.dispatchEvent(
       new CustomEvent('uip:model:change', {detail})
     );
diff --git a/src/core/base/root.ts b/src/core/base/root.ts
index 5cc1521a..bfbf3ac2 100644
--- a/src/core/base/root.ts
+++ b/src/core/base/root.ts
@@ -3,14 +3,13 @@ import {
   memoize,
   boolAttr,
   listen,
-  prop,
-  attr
+  prop
 } from '@exadel/esl/modules/esl-utils/decorators';
-import {sequentialUID} from '@exadel/esl/modules/esl-utils/misc';
 import {UIPStateModel} from './model';
+import {UIPChangeEvent} from './model.change';
 
+import type {UIPChangeInfo} from './model.change';
 import type {UIPSnippetTemplate} from './snippet';
-import {UIPChangeEvent, UIPChangeInfo} from './model.change';
 
 /**
  * UI Playground root custom element definition
@@ -34,8 +33,6 @@ export class UIPRoot extends ESLBaseElement {
   /** CSS query for snippets */
   public static SNIPPET_SEL = '[uip-snippet]';
 
-  @attr() public uipId: string = sequentialUID(UIPRoot.is);
-
   /** Indicates that the UIP components' theme is dark */
   @boolAttr() public darkTheme: boolean;
 
diff --git a/src/plugins/editor/editor-storage.ts b/src/plugins/editor/editor-storage.ts
deleted file mode 100644
index c7668b45..00000000
--- a/src/plugins/editor/editor-storage.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-interface EditorStorageEntry {
-  ts: string;
-  data: string;
-}
-
-export class EditorStorage {
-  public static readonly STORAGE_KEY = 'uip-editor-storage';
-
-  protected static get(): Record<string, any> {
-    return JSON.parse(localStorage.getItem(EditorStorage.STORAGE_KEY) || '{}');
-  }
-
-  protected static set(value: Record<string, any>): void {
-    localStorage.setItem(EditorStorage.STORAGE_KEY, JSON.stringify(value));
-  }
-
-  protected static serializeWithPathname(key: string): string {
-    return JSON.stringify({path: location.pathname, key});
-  }
-
-  public static save(key: string, value: string): void {
-    const state = {[EditorStorage.serializeWithPathname(key)]: {ts: Date.now(), value}};
-    EditorStorage.set(Object.assign(EditorStorage.get(), state));
-  }
-
-  public static load(key: string): string | null {
-    const entry = EditorStorage.get()[EditorStorage.serializeWithPathname(key)] || {} as EditorStorageEntry;
-    const expirationTime = 3600000 * 12;
-    if (entry?.ts + expirationTime > Date.now()) return entry.value || null;
-    EditorStorage.remove(key);
-    return null;
-  }
-
-  public static remove(key: string): void {
-    const data = EditorStorage.get();
-    delete data[key];
-    EditorStorage.set(data);
-  }
-}
diff --git a/src/plugins/editor/editor.tsx b/src/plugins/editor/editor.tsx
index 8f410db1..49e19396 100644
--- a/src/plugins/editor/editor.tsx
+++ b/src/plugins/editor/editor.tsx
@@ -13,7 +13,6 @@ import {attr, boolAttr, decorate, listen, memoize} from '@exadel/esl/modules/esl
 import {UIPPluginPanel} from '../../core/panel/plugin-panel';
 import {CopyIcon} from '../copy/copy-button.icon';
 
-import {EditorStorage} from './editor-storage';
 import {EditorIcon} from './editor.icon';
 
 import type {UIPSnippetsList} from '../snippets-list/snippets-list';
@@ -166,31 +165,12 @@ export class UIPEditor extends UIPPluginPanel {
       case 'html':
         if (e && !e.htmlChanges.length) return;
         this.value = this.model!.html;
-        if (e && !e.force) this.saveState();
     }
   }
 
-  protected saveState(): void {
-    const key = this.getStateKey();
-    if (key && this.value) EditorStorage.save(key, this.value);
-  }
-
-  protected getStateKey(): string | null {
-    if (!this.model?.activeSnippet || !this.$root) return null;
-    return JSON.stringify({html: this.model.activeSnippet.html, id: this.$root.uipId});
-  }
-
   /** Handles snippet change to set readonly value */
   @listen({event: 'uip:snippet:change', target: ($this: UIPSnippetsList) => $this.$root})
   protected _onSnippetChange(): void {
     this.editable = this.isSnippetEditable;
-    this.loadState();
-  }
-
-  protected loadState(): void {
-    if (!this.model?.activeSnippet) return;
-    const key = this.getStateKey();
-    const state = key && EditorStorage.load(key);
-    if (state) this.model.setHtml(state, this);
   }
 }

From ab1ae158b6486eb954b602a4559737ba8bf5b16a Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Wed, 3 Jul 2024 14:32:28 +0300
Subject: [PATCH 03/16] chore(uip-editor): code refactoring

---
 src/core/base/root.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/core/base/root.ts b/src/core/base/root.ts
index bfbf3ac2..b410ad9a 100644
--- a/src/core/base/root.ts
+++ b/src/core/base/root.ts
@@ -5,6 +5,7 @@ import {
   listen,
   prop
 } from '@exadel/esl/modules/esl-utils/decorators';
+
 import {UIPStateModel} from './model';
 import {UIPChangeEvent} from './model.change';
 

From 77cd6d1d2753c2ac37a3b7f29738db30a3197bf9 Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Tue, 12 Nov 2024 12:59:16 +0200
Subject: [PATCH 04/16] chore(uip-editor): add reset button

---
 src/core/base/model.storage.ts          | 37 ++++++++++++++++++-------
 src/core/base/model.ts                  | 22 +++++++++------
 src/core/base/root.ts                   |  5 +++-
 src/plugins/editor/editor.less          |  2 +-
 src/plugins/editor/editor.tsx           |  3 +-
 src/plugins/registration.less           |  1 +
 src/plugins/registration.ts             |  4 +++
 src/plugins/reset/reset-button.icon.tsx |  7 +++++
 src/plugins/reset/reset-button.less     | 10 +++++++
 src/plugins/reset/reset-button.shape.ts | 14 ++++++++++
 src/plugins/reset/reset-button.ts       | 19 +++++++++++++
 11 files changed, 103 insertions(+), 21 deletions(-)
 create mode 100644 src/plugins/reset/reset-button.icon.tsx
 create mode 100644 src/plugins/reset/reset-button.less
 create mode 100644 src/plugins/reset/reset-button.shape.ts
 create mode 100644 src/plugins/reset/reset-button.ts

diff --git a/src/core/base/model.storage.ts b/src/core/base/model.storage.ts
index c241497a..e103b142 100644
--- a/src/core/base/model.storage.ts
+++ b/src/core/base/model.storage.ts
@@ -14,12 +14,24 @@ interface UIPStateModelSnippets {
 export class UIPStateStorage {
   public static readonly STORAGE_KEY = 'uip-editor-storage';
 
-  public constructor(protected model: UIPStateModel) {}
+  protected static readonly EXPIRATION_TIME = 3600000 * 12; // 12 hours
+
+  private static instances = new Map<string, UIPStateStorage>();
+
+  protected constructor(protected storeKey: string, protected model: UIPStateModel) {}
+
+  public static for(storeKey: string, model: UIPStateModel): UIPStateStorage {
+    const instance = this.instances.get(storeKey);
+    if (instance) return instance;
+
+    const newInstance = new UIPStateStorage(storeKey, model);
+    this.instances.set(storeKey, newInstance);
+    return newInstance;
+  }
 
   protected static loadEntry(key: string): string | null {
     const entry = (this.lsGet()[key] || {}) as UIPStateStorageEntry;
-    const expirationTime = 3600000 * 12;
-    if (parseInt(entry?.ts, 10) + expirationTime > Date.now()) return entry.snippets || null;
+    if (parseInt(entry?.ts, 10) + this.EXPIRATION_TIME > Date.now()) return entry.snippets || null;
     this.removeEntry(key);
     return null;
   }
@@ -43,20 +55,25 @@ export class UIPStateStorage {
   }
 
   protected getStateKey(): string | null {
-    if (!this.model?.activeSnippet) return null;
-    const {id, activeSnippet} = this.model;
-    return JSON.stringify({id, path: location.pathname, snippet: activeSnippet.html});
+    const {activeSnippet} = this.model;
+    if (!activeSnippet) return null;
+    return JSON.stringify({key: this.storeKey, snippet: activeSnippet.html});
   }
 
   public loadState(): UIPStateModelSnippets | undefined {
-    const key = this.getStateKey();
-    const state = key && UIPStateStorage.loadEntry(key);
+    const stateKey = this.getStateKey();
+    const state = stateKey && UIPStateStorage.loadEntry(stateKey);
     if (state) return JSON.parse(state);
   }
 
   public saveState(): void {
     const {js, html, note} = this.model;
-    const key = this.getStateKey();
-    key && UIPStateStorage.saveEntry(key, JSON.stringify({js, html, note}));
+    const stateKey = this.getStateKey();
+    stateKey && UIPStateStorage.saveEntry(stateKey, JSON.stringify({js, html, note}));
+  }
+
+  public resetState(): void {
+    const stateKey = this.getStateKey();
+    stateKey && UIPStateStorage.removeEntry(stateKey);
   }
 }
diff --git a/src/core/base/model.ts b/src/core/base/model.ts
index 7d8ee603..09d964b1 100644
--- a/src/core/base/model.ts
+++ b/src/core/base/model.ts
@@ -12,8 +12,8 @@ import {
 import {UIPStateStorage} from './model.storage';
 import {UIPSnippetItem} from './snippet';
 
-import type {UIPRoot} from './root';
-import type {UIPPlugin} from './plugin';
+import {UIPRoot} from './root';
+import {UIPPlugin} from './plugin';
 import type {UIPSnippetTemplate} from './snippet';
 import type {UIPChangeInfo} from './model.change';
 
@@ -50,8 +50,6 @@ export class UIPStateModel extends SyntheticEventTarget {
   /** Snippets {@link UIPSnippetItem} value objects */
   private _snippets: UIPSnippetItem[];
 
-  public readonly id = sequentialUID('uip-model-id-');
-
   /** Current js state */
   private _js: string = '';
   /** Current note state */
@@ -59,11 +57,11 @@ export class UIPStateModel extends SyntheticEventTarget {
   /** Current markup state */
   private _html = new DOMParser().parseFromString('', 'text/html').body;
 
+  public storage: UIPStateStorage | undefined;
+
   /** Last changes history (used to dispatch changes) */
   private _changes: UIPChangeInfo[] = [];
 
-  protected storage = new UIPStateStorage(this);
-
   /**
    * Sets current js state to the passed one
    * @param js - new state
@@ -153,6 +151,10 @@ export class UIPStateModel extends SyntheticEventTarget {
     return this._snippets.find((snippet) => snippet.anchor === anchor);
   }
 
+  protected getStorageKey(modifier: UIPPlugin | UIPRoot): string {
+    return modifier instanceof UIPRoot ? modifier.storeKey : modifier.$root?.storeKey || '';
+  }
+
   /** Changes current active snippet */
   public applySnippet(
     snippet: UIPSnippetItem,
@@ -160,7 +162,11 @@ export class UIPStateModel extends SyntheticEventTarget {
   ): void {
     if (!snippet) return;
     this._snippets.forEach((s) => (s.active = s === snippet));
-    const {js, html, note} = this.storage.loadState() || snippet;
+
+    const storeKey = this.getStorageKey(modifier);
+    if (storeKey) this.storage = UIPStateStorage.for(storeKey, this);
+
+    const {js, html, note} = this.storage?.loadState() || snippet;
     this.setHtml(html, modifier, true);
     this.setJS(js, modifier);
     this.setNote(note, modifier);
@@ -206,7 +212,7 @@ export class UIPStateModel extends SyntheticEventTarget {
     if (!this._changes.length) return;
     const detail = this._changes;
     this._changes = [];
-    this.storage.saveState();
+    this.storage?.saveState();
     this.dispatchEvent(
       new CustomEvent('uip:model:change', {detail})
     );
diff --git a/src/core/base/root.ts b/src/core/base/root.ts
index b410ad9a..cedff96b 100644
--- a/src/core/base/root.ts
+++ b/src/core/base/root.ts
@@ -3,7 +3,8 @@ import {
   memoize,
   boolAttr,
   listen,
-  prop
+  prop,
+  attr
 } from '@exadel/esl/modules/esl-utils/decorators';
 
 import {UIPStateModel} from './model';
@@ -36,6 +37,8 @@ export class UIPRoot extends ESLBaseElement {
 
   /** Indicates that the UIP components' theme is dark */
   @boolAttr() public darkTheme: boolean;
+  /** Key to store UIP state in the local storage */
+  @attr({defaultValue: ''}) public storeKey: string;
 
   /** Indicates ready state of the uip-root */
   @boolAttr({readonly: true}) public ready: boolean;
diff --git a/src/plugins/editor/editor.less b/src/plugins/editor/editor.less
index 8a31acfe..890baf04 100644
--- a/src/plugins/editor/editor.less
+++ b/src/plugins/editor/editor.less
@@ -9,7 +9,7 @@
     padding: 1em;
   }
 
-  &-header-copy {
+  &-header-copy, &-header-reset {
     position: relative;
     width: 25px;
     height: 25px;
diff --git a/src/plugins/editor/editor.tsx b/src/plugins/editor/editor.tsx
index 49e19396..3e8d34ef 100644
--- a/src/plugins/editor/editor.tsx
+++ b/src/plugins/editor/editor.tsx
@@ -12,7 +12,7 @@ import {attr, boolAttr, decorate, listen, memoize} from '@exadel/esl/modules/esl
 
 import {UIPPluginPanel} from '../../core/panel/plugin-panel';
 import {CopyIcon} from '../copy/copy-button.icon';
-
+import {ResetIcon} from '../reset/reset-button.icon';
 import {EditorIcon} from './editor.icon';
 
 import type {UIPSnippetsList} from '../snippets-list/snippets-list';
@@ -45,6 +45,7 @@ export class UIPEditor extends UIPPluginPanel {
     return (
       <div className={type.is + '-toolbar uip-plugin-header-toolbar'}>
         {this.showCopy ? <uip-copy class={type.is + '-header-copy'} source={this.source}><CopyIcon/></uip-copy> : ''}
+        {this.$root?.storeKey ? <uip-reset class={type.is + '-header-reset'}><ResetIcon/></uip-reset> : ''}
       </div>
     ) as HTMLElement;
   }
diff --git a/src/plugins/registration.less b/src/plugins/registration.less
index d085d27a..10de06e4 100644
--- a/src/plugins/registration.less
+++ b/src/plugins/registration.less
@@ -8,5 +8,6 @@
 @import './settings/settings.less';
 
 @import './copy/copy-button.less';
+@import './reset/reset-button.less';
 @import './theme/theme-toggle.less';
 @import './direction/dir-toggle.less';
diff --git a/src/plugins/registration.ts b/src/plugins/registration.ts
index 09045cfd..ee02c6f8 100644
--- a/src/plugins/registration.ts
+++ b/src/plugins/registration.ts
@@ -17,6 +17,9 @@ export {UIPSetting, UIPSettings, UIPTextSetting, UIPBoolSetting, UIPSelectSettin
 import {UIPCopy} from './copy/copy-button';
 export {UIPCopy};
 
+import {UIPReset} from './reset/reset-button';
+export {UIPReset};
+
 import {UIPNote} from './note/note';
 export {UIPNote};
 
@@ -34,6 +37,7 @@ export const registerSettings = (): void => {
 
 export const registerPlugins = (): void => {
   UIPCopy.register();
+  UIPReset.register();
   UIPDirSwitcher.register();
   UIPThemeSwitcher.register();
 
diff --git a/src/plugins/reset/reset-button.icon.tsx b/src/plugins/reset/reset-button.icon.tsx
new file mode 100644
index 00000000..53a905da
--- /dev/null
+++ b/src/plugins/reset/reset-button.icon.tsx
@@ -0,0 +1,7 @@
+import React from 'jsx-dom';
+
+export const ResetIcon = (): SVGElement => (
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -1 10 10">
+  <path d="M4 0C1.8 0 0 1.8 0 4s1.8 4 4 4c1.1 0 2.12-.43 2.84-1.16l-.72-.72c-.54.54-1.29.88-2.13.88-1.66 0-3-1.34-3-3s1.34-3 3-3c.83 0 1.55.36 2.09.91L4.99 3h3V0L6.8 1.19C6.08.47 5.09 0 3.99 0z"/>
+</svg>
+) as SVGElement;
diff --git a/src/plugins/reset/reset-button.less b/src/plugins/reset/reset-button.less
new file mode 100644
index 00000000..ce231fc3
--- /dev/null
+++ b/src/plugins/reset/reset-button.less
@@ -0,0 +1,10 @@
+.uip-reset {
+  display: inline-flex;
+  cursor: pointer;
+
+  > svg {
+    fill: currentColor;
+    width: 100%;
+    height: 100%;
+  }
+}
diff --git a/src/plugins/reset/reset-button.shape.ts b/src/plugins/reset/reset-button.shape.ts
new file mode 100644
index 00000000..1a7ecb1a
--- /dev/null
+++ b/src/plugins/reset/reset-button.shape.ts
@@ -0,0 +1,14 @@
+import type {ESLBaseElementShape} from '@exadel/esl/modules/esl-base-element/core';
+import type {UIPReset} from './reset-button';
+
+export interface UIPResetShape extends ESLBaseElementShape<UIPReset> {
+  children?: any;
+}
+
+declare global {
+  namespace JSX {
+    interface IntrinsicElements {
+      'uip-reset': UIPResetShape;
+    }
+  }
+}
diff --git a/src/plugins/reset/reset-button.ts b/src/plugins/reset/reset-button.ts
new file mode 100644
index 00000000..effb5709
--- /dev/null
+++ b/src/plugins/reset/reset-button.ts
@@ -0,0 +1,19 @@
+import './reset-button.shape';
+
+import {UIPPluginButton} from '../../core/button/plugin-button';
+
+/** Button-plugin to reset snippet to default settings */
+export class UIPReset extends UIPPluginButton {
+  public static override is = 'uip-reset';
+
+  protected override connectedCallback(): void {
+    super.connectedCallback();
+  }
+
+  public override onAction(): void {
+    const {model} = this;
+    if (!model) return;
+    model.storage?.resetState();
+    this.$root && model.applyCurrentSnippet(this.$root);
+  }
+}

From 55a5d7e50095aecba0c05c7e446dd54638c1fbbe Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Tue, 19 Nov 2024 18:21:10 +0200
Subject: [PATCH 05/16] chore(uip-editor): hide reset button if snippet is
 unmodified

---
 src/plugins/editor/editor.tsx           |  2 +-
 src/plugins/reset/reset-button.icon.tsx |  2 +-
 src/plugins/reset/reset-button.less     |  4 ++
 src/plugins/reset/reset-button.shape.ts |  1 +
 src/plugins/reset/reset-button.ts       | 61 ++++++++++++++++++++++++-
 5 files changed, 66 insertions(+), 4 deletions(-)

diff --git a/src/plugins/editor/editor.tsx b/src/plugins/editor/editor.tsx
index 3e8d34ef..1e6bbcc8 100644
--- a/src/plugins/editor/editor.tsx
+++ b/src/plugins/editor/editor.tsx
@@ -45,7 +45,7 @@ export class UIPEditor extends UIPPluginPanel {
     return (
       <div className={type.is + '-toolbar uip-plugin-header-toolbar'}>
         {this.showCopy ? <uip-copy class={type.is + '-header-copy'} source={this.source}><CopyIcon/></uip-copy> : ''}
-        {this.$root?.storeKey ? <uip-reset class={type.is + '-header-reset'}><ResetIcon/></uip-reset> : ''}
+        {this.$root?.storeKey ? <uip-reset class={type.is + '-header-reset'} source={this.source}><ResetIcon/></uip-reset> : ''}
       </div>
     ) as HTMLElement;
   }
diff --git a/src/plugins/reset/reset-button.icon.tsx b/src/plugins/reset/reset-button.icon.tsx
index 53a905da..b467cbe7 100644
--- a/src/plugins/reset/reset-button.icon.tsx
+++ b/src/plugins/reset/reset-button.icon.tsx
@@ -1,7 +1,7 @@
 import React from 'jsx-dom';
 
 export const ResetIcon = (): SVGElement => (
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -1 10 10">
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 12 12">
   <path d="M4 0C1.8 0 0 1.8 0 4s1.8 4 4 4c1.1 0 2.12-.43 2.84-1.16l-.72-.72c-.54.54-1.29.88-2.13.88-1.66 0-3-1.34-3-3s1.34-3 3-3c.83 0 1.55.36 2.09.91L4.99 3h3V0L6.8 1.19C6.08.47 5.09 0 3.99 0z"/>
 </svg>
 ) as SVGElement;
diff --git a/src/plugins/reset/reset-button.less b/src/plugins/reset/reset-button.less
index ce231fc3..0658976d 100644
--- a/src/plugins/reset/reset-button.less
+++ b/src/plugins/reset/reset-button.less
@@ -7,4 +7,8 @@
     width: 100%;
     height: 100%;
   }
+
+  &-hidden {
+    display: none;
+  }
 }
diff --git a/src/plugins/reset/reset-button.shape.ts b/src/plugins/reset/reset-button.shape.ts
index 1a7ecb1a..269b2bf1 100644
--- a/src/plugins/reset/reset-button.shape.ts
+++ b/src/plugins/reset/reset-button.shape.ts
@@ -2,6 +2,7 @@ import type {ESLBaseElementShape} from '@exadel/esl/modules/esl-base-element/cor
 import type {UIPReset} from './reset-button';
 
 export interface UIPResetShape extends ESLBaseElementShape<UIPReset> {
+  source?: 'javascript' | 'js' | 'html';
   children?: any;
 }
 
diff --git a/src/plugins/reset/reset-button.ts b/src/plugins/reset/reset-button.ts
index effb5709..01211fbc 100644
--- a/src/plugins/reset/reset-button.ts
+++ b/src/plugins/reset/reset-button.ts
@@ -1,19 +1,76 @@
 import './reset-button.shape';
 
+import {listen, attr} from '@exadel/esl/modules/esl-utils/decorators';
+
 import {UIPPluginButton} from '../../core/button/plugin-button';
+import {UIPRoot} from '../../core/base/root';
+import {UIPHTMLNormalizationPreprocessors, UIPJSNormalizationPreprocessors} from '../../core/processors/normalization';
 
 /** Button-plugin to reset snippet to default settings */
 export class UIPReset extends UIPPluginButton {
   public static override is = 'uip-reset';
 
+  /** Source type to copy (html | js) */
+  @attr({defaultValue: 'html'}) public source: string;
+
   protected override connectedCallback(): void {
     super.connectedCallback();
   }
 
   public override onAction(): void {
     const {model} = this;
-    if (!model) return;
+    if (!model || !model.activeSnippet || !this.$root) return;
+    switch (this.source) {
+      case 'js':
+      case 'javascript':
+        model.setJS(model.activeSnippet.js, this.$root);
+        break;
+      case 'html':
+        model.setHtml(model.activeSnippet.html, this.$root);
+        break;
+    }
     model.storage?.resetState();
-    this.$root && model.applyCurrentSnippet(this.$root);
+  }
+
+  @listen({event: 'uip:model:change', target: ($this: UIPRoot) => $this.model})
+  protected onModelChange(): void {
+    if (!this.model || !this.model.activeSnippet) return;
+    switch (this.source) {
+      case 'js':
+      case 'javascript':
+        const processedJs = this.preprocessJs();
+        const isJsUnchanged = this.model.js === processedJs;
+        this.toggleButton(isJsUnchanged);
+        break;
+      case 'html':
+        const processedHtml = this.preprocessHtml();
+        const isHtmlUnchanged = this.model.html === processedHtml;
+        this.toggleButton(isHtmlUnchanged);
+        break;
+    }
+  }
+
+  private preprocessJs(): string | undefined {
+    if (!this.model || !this.model.activeSnippet) return;
+    return UIPJSNormalizationPreprocessors.preprocess(this.model.activeSnippet.js);
+  }
+
+  private preprocessHtml(): string | undefined {
+    if (!this.model || !this.model.activeSnippet) return;
+
+    const html = UIPHTMLNormalizationPreprocessors.preprocess(this.model.activeSnippet.html);
+    const { head, body: root } = new DOMParser().parseFromString(html, 'text/html');
+
+    Array.from(head.children).reverse().forEach((el) => {
+        if (el.tagName !== 'STYLE') return;
+        root.innerHTML = '\n' + root.innerHTML;
+        root.insertBefore(el, root.firstChild);
+    });
+
+    return root.innerHTML;
+  }
+
+  protected toggleButton(state?: boolean): void {
+    this.$$cls('uip-reset-hidden', state);
   }
 }

From 7ff2cd88f2cdd722fc3745560cee2b6a16b7830c Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Tue, 3 Dec 2024 10:07:13 +0200
Subject: [PATCH 06/16] chore(uip-editor): remove duplicate code

---
 src/core/base/model.ts            | 39 ++++++++++++++++-------
 src/plugins/reset/reset-button.ts | 52 ++++++-------------------------
 2 files changed, 37 insertions(+), 54 deletions(-)

diff --git a/src/core/base/model.ts b/src/core/base/model.ts
index 09d964b1..1d39f3dd 100644
--- a/src/core/base/model.ts
+++ b/src/core/base/model.ts
@@ -68,13 +68,17 @@ export class UIPStateModel extends SyntheticEventTarget {
    * @param modifier - plugin, that initiates the change
    */
   public setJS(js: string, modifier: UIPPlugin | UIPRoot): void {
-    const script = UIPJSNormalizationPreprocessors.preprocess(js);
+    const script = this.normalizeJS(js);
     if (this._js === script) return;
     this._js = script;
     this._changes.push({modifier, type: 'js', force: true});
     this.dispatchChange();
   }
 
+  protected normalizeJS(snippet: string): string {
+    return UIPJSNormalizationPreprocessors.preprocess(snippet);
+  }
+
   /**
    * Sets current note state to the passed one
    * @param text - new state
@@ -95,21 +99,34 @@ export class UIPStateModel extends SyntheticEventTarget {
    * @param force - marker, that indicates if html changes require iframe rerender
    */
   public setHtml(markup: string, modifier: UIPPlugin | UIPRoot, force: boolean = false): void {
-    const html = UIPHTMLNormalizationPreprocessors.preprocess(markup);
+    const root = this.normalizeHTML(markup);
+    if (root.innerHTML.trim() === this.html.trim()) return;
+    this._html = root;
+    this._changes.push({modifier, type: 'html', force});
+    this.dispatchChange();
+  }
+
+  protected normalizeHTML(snippet: string): HTMLElement {
+    const html = UIPHTMLNormalizationPreprocessors.preprocess(snippet);
     const {head, body: root} = new DOMParser().parseFromString(html, 'text/html');
 
     Array.from(head.children).reverse().forEach((el) => {
-      if (el.tagName === 'STYLE') {
-        root.innerHTML = '\n' + root.innerHTML;
-        root.insertBefore(el, root.firstChild);
-      }
+      if (el.tagName !== 'STYLE') return;
+      root.innerHTML = '\n' + root.innerHTML;
+      root.insertBefore(el, root.firstChild);
     });
 
-    if (root.innerHTML.trim() !== this.html.trim()) {
-      this._html = root;
-      this._changes.push({modifier, type: 'html', force});
-      this.dispatchChange();
-    }
+    return root;
+  }
+
+  public isHTMLChanged(): boolean {
+    if (!this.activeSnippet) return false;
+    return this.normalizeHTML(this.activeSnippet.html).innerHTML.trim() !== this.html.trim();
+  }
+
+  public isJSChanged(): boolean {
+    if (!this.activeSnippet) return false;
+    return this.normalizeJS(this.activeSnippet.js) !== this.js;
   }
 
   /** Current js state getter */
diff --git a/src/plugins/reset/reset-button.ts b/src/plugins/reset/reset-button.ts
index 01211fbc..7523d08f 100644
--- a/src/plugins/reset/reset-button.ts
+++ b/src/plugins/reset/reset-button.ts
@@ -4,7 +4,6 @@ import {listen, attr} from '@exadel/esl/modules/esl-utils/decorators';
 
 import {UIPPluginButton} from '../../core/button/plugin-button';
 import {UIPRoot} from '../../core/base/root';
-import {UIPHTMLNormalizationPreprocessors, UIPJSNormalizationPreprocessors} from '../../core/processors/normalization';
 
 /** Button-plugin to reset snippet to default settings */
 export class UIPReset extends UIPPluginButton {
@@ -20,54 +19,21 @@ export class UIPReset extends UIPPluginButton {
   public override onAction(): void {
     const {model} = this;
     if (!model || !model.activeSnippet || !this.$root) return;
-    switch (this.source) {
-      case 'js':
-      case 'javascript':
-        model.setJS(model.activeSnippet.js, this.$root);
-        break;
-      case 'html':
-        model.setHtml(model.activeSnippet.html, this.$root);
-        break;
-    }
+    if (this.source === 'js' || this.source === 'javascript') 
+      model.setJS(model.activeSnippet.js, this.$root);
+    else if (this.source === 'html')
+      model.setHtml(model.activeSnippet.html, this.$root);
+    
     model.storage?.resetState();
   }
 
   @listen({event: 'uip:model:change', target: ($this: UIPRoot) => $this.model})
   protected onModelChange(): void {
     if (!this.model || !this.model.activeSnippet) return;
-    switch (this.source) {
-      case 'js':
-      case 'javascript':
-        const processedJs = this.preprocessJs();
-        const isJsUnchanged = this.model.js === processedJs;
-        this.toggleButton(isJsUnchanged);
-        break;
-      case 'html':
-        const processedHtml = this.preprocessHtml();
-        const isHtmlUnchanged = this.model.html === processedHtml;
-        this.toggleButton(isHtmlUnchanged);
-        break;
-    }
-  }
-
-  private preprocessJs(): string | undefined {
-    if (!this.model || !this.model.activeSnippet) return;
-    return UIPJSNormalizationPreprocessors.preprocess(this.model.activeSnippet.js);
-  }
-
-  private preprocessHtml(): string | undefined {
-    if (!this.model || !this.model.activeSnippet) return;
-
-    const html = UIPHTMLNormalizationPreprocessors.preprocess(this.model.activeSnippet.html);
-    const { head, body: root } = new DOMParser().parseFromString(html, 'text/html');
-
-    Array.from(head.children).reverse().forEach((el) => {
-        if (el.tagName !== 'STYLE') return;
-        root.innerHTML = '\n' + root.innerHTML;
-        root.insertBefore(el, root.firstChild);
-    });
-
-    return root.innerHTML;
+    if (this.source === 'js' || this.source === 'javascript') 
+      this.toggleButton(!this.model.isJSChanged());
+    else if (this.source === 'html')
+      this.toggleButton(!this.model.isHTMLChanged());
   }
 
   protected toggleButton(state?: boolean): void {

From 81df5e695b7140a1374d434c7733e2fdfd63c48f Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Thu, 5 Dec 2024 14:05:14 +0200
Subject: [PATCH 07/16] chore(uip-editor): move logic to uip-model

---
 src/core/base/model.storage.ts    | 22 +++++++++++-----------
 src/core/base/model.ts            | 18 +++++++++++++++++-
 src/plugins/reset/reset-button.ts | 11 ++---------
 3 files changed, 30 insertions(+), 21 deletions(-)

diff --git a/src/core/base/model.storage.ts b/src/core/base/model.storage.ts
index e103b142..fcae5286 100644
--- a/src/core/base/model.storage.ts
+++ b/src/core/base/model.storage.ts
@@ -29,29 +29,29 @@ export class UIPStateStorage {
     return newInstance;
   }
 
-  protected static loadEntry(key: string): string | null {
+  protected loadEntry(key: string): string | null {
     const entry = (this.lsGet()[key] || {}) as UIPStateStorageEntry;
-    if (parseInt(entry?.ts, 10) + this.EXPIRATION_TIME > Date.now()) return entry.snippets || null;
+    if (parseInt(entry?.ts, 10) + UIPStateStorage.EXPIRATION_TIME > Date.now()) return entry.snippets || null;
     this.removeEntry(key);
     return null;
   }
 
-  protected static saveEntry(key: string, value: string): void {
+  protected saveEntry(key: string, value: string): void {
     this.lsSet(Object.assign(this.lsGet(), {[key]: {ts: Date.now(), snippets: value}}));
   }
 
-  protected static removeEntry(key: string): void {
+  protected removeEntry(key: string): void {
     const data = this.lsGet();
     delete data[key];
     this.lsSet(data);
   }
 
-  protected static lsGet(): Record<string, any> {
-    return JSON.parse(localStorage.getItem(this.STORAGE_KEY) || '{}');
+  protected lsGet(): Record<string, any> {
+    return JSON.parse(localStorage.getItem(UIPStateStorage.STORAGE_KEY) || '{}');
   }
 
-  protected static lsSet(value: Record<string, any>): void {
-    localStorage.setItem(this.STORAGE_KEY, JSON.stringify(value));
+  protected lsSet(value: Record<string, any>): void {
+    localStorage.setItem(UIPStateStorage.STORAGE_KEY, JSON.stringify(value));
   }
 
   protected getStateKey(): string | null {
@@ -62,18 +62,18 @@ export class UIPStateStorage {
 
   public loadState(): UIPStateModelSnippets | undefined {
     const stateKey = this.getStateKey();
-    const state = stateKey && UIPStateStorage.loadEntry(stateKey);
+    const state = stateKey && this.loadEntry(stateKey);
     if (state) return JSON.parse(state);
   }
 
   public saveState(): void {
     const {js, html, note} = this.model;
     const stateKey = this.getStateKey();
-    stateKey && UIPStateStorage.saveEntry(stateKey, JSON.stringify({js, html, note}));
+    stateKey && this.saveEntry(stateKey, JSON.stringify({js, html, note}));
   }
 
   public resetState(): void {
     const stateKey = this.getStateKey();
-    stateKey && UIPStateStorage.removeEntry(stateKey);
+    stateKey && this.removeEntry(stateKey);
   }
 }
diff --git a/src/core/base/model.ts b/src/core/base/model.ts
index 1d39f3dd..a807ab83 100644
--- a/src/core/base/model.ts
+++ b/src/core/base/model.ts
@@ -1,7 +1,6 @@
 import {SyntheticEventTarget} from '@exadel/esl/modules/esl-utils/dom';
 import {decorate} from '@exadel/esl/modules/esl-utils/decorators';
 import {microtask} from '@exadel/esl/modules/esl-utils/async';
-import {sequentialUID} from '@exadel/esl/modules/esl-utils/misc';
 
 import {
   UIPJSNormalizationPreprocessors,
@@ -129,6 +128,23 @@ export class UIPStateModel extends SyntheticEventTarget {
     return this.normalizeJS(this.activeSnippet.js) !== this.js;
   }
 
+  public resetSnippet(source: 'js' | 'javascript' | 'html', modifier: UIPPlugin | UIPRoot): void {
+    source === 'html' ? this.resetHTML(modifier) : this.resetJS(modifier);
+  }
+
+  protected resetJS(modifier: UIPPlugin | UIPRoot): void {
+    if (!this.activeSnippet) return;
+    this.setJS(this.activeSnippet.js, modifier);
+    this.storage?.resetState();
+  }
+
+  protected resetHTML(modifier: UIPPlugin | UIPRoot): void {
+    if (!this.activeSnippet) return;
+    this.setHtml(this.activeSnippet.html, modifier);
+    this.storage?.resetState();
+  }
+
+
   /** Current js state getter */
   public get js(): string {
     return this._js;
diff --git a/src/plugins/reset/reset-button.ts b/src/plugins/reset/reset-button.ts
index 7523d08f..64e49780 100644
--- a/src/plugins/reset/reset-button.ts
+++ b/src/plugins/reset/reset-button.ts
@@ -10,21 +10,14 @@ export class UIPReset extends UIPPluginButton {
   public static override is = 'uip-reset';
 
   /** Source type to copy (html | js) */
-  @attr({defaultValue: 'html'}) public source: string;
+  @attr({defaultValue: 'html'}) public source: 'js' | 'javascript' | 'html';
 
   protected override connectedCallback(): void {
     super.connectedCallback();
   }
 
   public override onAction(): void {
-    const {model} = this;
-    if (!model || !model.activeSnippet || !this.$root) return;
-    if (this.source === 'js' || this.source === 'javascript') 
-      model.setJS(model.activeSnippet.js, this.$root);
-    else if (this.source === 'html')
-      model.setHtml(model.activeSnippet.html, this.$root);
-    
-    model.storage?.resetState();
+    if (this.$root) this.model?.resetSnippet(this.source, this.$root);
   }
 
   @listen({event: 'uip:model:change', target: ($this: UIPRoot) => $this.model})

From 2f6bd92f89eeca2123e2f9b7d730362d84917bc8 Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Tue, 10 Dec 2024 17:49:48 +0200
Subject: [PATCH 08/16] chore(uip-editor): get rid of cyclic dependency

---
 src/core/base/model.storage.ts      | 32 ++++++++++++-------------
 src/core/base/model.ts              | 36 ++++++++---------------------
 src/core/base/root.ts               | 19 +++++++++++++++
 src/plugins/reset/reset-button.less |  2 +-
 src/plugins/reset/reset-button.ts   | 20 ++++++++--------
 5 files changed, 55 insertions(+), 54 deletions(-)

diff --git a/src/core/base/model.storage.ts b/src/core/base/model.storage.ts
index fcae5286..d964d45c 100644
--- a/src/core/base/model.storage.ts
+++ b/src/core/base/model.storage.ts
@@ -1,4 +1,6 @@
 import type {UIPStateModel} from './model';
+import type {UIPPlugin} from './plugin';
+import type {UIPRoot} from './root';
 
 interface UIPStateStorageEntry {
   ts: string;
@@ -16,18 +18,7 @@ export class UIPStateStorage {
 
   protected static readonly EXPIRATION_TIME = 3600000 * 12; // 12 hours
 
-  private static instances = new Map<string, UIPStateStorage>();
-
-  protected constructor(protected storeKey: string, protected model: UIPStateModel) {}
-
-  public static for(storeKey: string, model: UIPStateModel): UIPStateStorage {
-    const instance = this.instances.get(storeKey);
-    if (instance) return instance;
-
-    const newInstance = new UIPStateStorage(storeKey, model);
-    this.instances.set(storeKey, newInstance);
-    return newInstance;
-  }
+  public constructor(protected storeKey: string, protected model: UIPStateModel) {}
 
   protected loadEntry(key: string): string | null {
     const entry = (this.lsGet()[key] || {}) as UIPStateStorageEntry;
@@ -56,24 +47,31 @@ export class UIPStateStorage {
 
   protected getStateKey(): string | null {
     const {activeSnippet} = this.model;
-    if (!activeSnippet) return null;
+    if (!activeSnippet || !this.storeKey) return null;
     return JSON.stringify({key: this.storeKey, snippet: activeSnippet.html});
   }
 
-  public loadState(): UIPStateModelSnippets | undefined {
+  public loadState(initiator: UIPPlugin | UIPRoot): void {
     const stateKey = this.getStateKey();
     const state = stateKey && this.loadEntry(stateKey);
-    if (state) return JSON.parse(state);
+    if (!state) return; 
+    
+    const stateobj = JSON.parse(state);
+    this.model.setHtml(stateobj.html, initiator, true);
+    this.model.setJS(stateobj.js, initiator);
+    this.model.setNote(stateobj.note, initiator);
   }
 
   public saveState(): void {
-    const {js, html, note} = this.model;
     const stateKey = this.getStateKey();
+    const {js, html, note} = this.model;
     stateKey && this.saveEntry(stateKey, JSON.stringify({js, html, note}));
   }
 
-  public resetState(): void {
+  public resetState(source: 'js' | 'html', modifier: UIPPlugin | UIPRoot): void {
     const stateKey = this.getStateKey();
     stateKey && this.removeEntry(stateKey);
+
+    this.model.resetSnippet(source, modifier);
   }
 }
diff --git a/src/core/base/model.ts b/src/core/base/model.ts
index a807ab83..504b7dfc 100644
--- a/src/core/base/model.ts
+++ b/src/core/base/model.ts
@@ -8,11 +8,10 @@ import {
   UIPNoteNormalizationPreprocessors
 } from '../processors/normalization';
 
-import {UIPStateStorage} from './model.storage';
 import {UIPSnippetItem} from './snippet';
 
-import {UIPRoot} from './root';
-import {UIPPlugin} from './plugin';
+import type {UIPRoot} from './root';
+import type {UIPPlugin} from './plugin';
 import type {UIPSnippetTemplate} from './snippet';
 import type {UIPChangeInfo} from './model.change';
 
@@ -56,8 +55,6 @@ export class UIPStateModel extends SyntheticEventTarget {
   /** Current markup state */
   private _html = new DOMParser().parseFromString('', 'text/html').body;
 
-  public storage: UIPStateStorage | undefined;
-
   /** Last changes history (used to dispatch changes) */
   private _changes: UIPChangeInfo[] = [];
 
@@ -128,20 +125,17 @@ export class UIPStateModel extends SyntheticEventTarget {
     return this.normalizeJS(this.activeSnippet.js) !== this.js;
   }
 
-  public resetSnippet(source: 'js' | 'javascript' | 'html', modifier: UIPPlugin | UIPRoot): void {
-    source === 'html' ? this.resetHTML(modifier) : this.resetJS(modifier);
+  public resetSnippet(source: 'js' | 'html', modifier: UIPPlugin | UIPRoot): void {
+    if (source === 'html') this.resetHTML(modifier);
+    if (source === 'js') this.resetJS(modifier);
   }
 
   protected resetJS(modifier: UIPPlugin | UIPRoot): void {
-    if (!this.activeSnippet) return;
-    this.setJS(this.activeSnippet.js, modifier);
-    this.storage?.resetState();
+    if (this.activeSnippet) this.setJS(this.activeSnippet.js, modifier);
   }
 
   protected resetHTML(modifier: UIPPlugin | UIPRoot): void {
-    if (!this.activeSnippet) return;
-    this.setHtml(this.activeSnippet.html, modifier);
-    this.storage?.resetState();
+    if (this.activeSnippet) this.setHtml(this.activeSnippet.html, modifier);
   }
 
 
@@ -184,10 +178,6 @@ export class UIPStateModel extends SyntheticEventTarget {
     return this._snippets.find((snippet) => snippet.anchor === anchor);
   }
 
-  protected getStorageKey(modifier: UIPPlugin | UIPRoot): string {
-    return modifier instanceof UIPRoot ? modifier.storeKey : modifier.$root?.storeKey || '';
-  }
-
   /** Changes current active snippet */
   public applySnippet(
     snippet: UIPSnippetItem,
@@ -195,14 +185,9 @@ export class UIPStateModel extends SyntheticEventTarget {
   ): void {
     if (!snippet) return;
     this._snippets.forEach((s) => (s.active = s === snippet));
-
-    const storeKey = this.getStorageKey(modifier);
-    if (storeKey) this.storage = UIPStateStorage.for(storeKey, this);
-
-    const {js, html, note} = this.storage?.loadState() || snippet;
-    this.setHtml(html, modifier, true);
-    this.setJS(js, modifier);
-    this.setNote(note, modifier);
+    this.setHtml(snippet.html, modifier, true);
+    this.setJS(snippet.js, modifier);
+    this.setNote(snippet.note, modifier);
     this.dispatchEvent(
       new CustomEvent('uip:model:snippet:change', {detail: this})
     );
@@ -245,7 +230,6 @@ export class UIPStateModel extends SyntheticEventTarget {
     if (!this._changes.length) return;
     const detail = this._changes;
     this._changes = [];
-    this.storage?.saveState();
     this.dispatchEvent(
       new CustomEvent('uip:model:change', {detail})
     );
diff --git a/src/core/base/root.ts b/src/core/base/root.ts
index cedff96b..7302ef10 100644
--- a/src/core/base/root.ts
+++ b/src/core/base/root.ts
@@ -9,6 +9,7 @@ import {
 
 import {UIPStateModel} from './model';
 import {UIPChangeEvent} from './model.change';
+import {UIPStateStorage} from './model.storage';
 
 import type {UIPChangeInfo} from './model.change';
 import type {UIPSnippetTemplate} from './snippet';
@@ -39,6 +40,8 @@ export class UIPRoot extends ESLBaseElement {
   @boolAttr() public darkTheme: boolean;
   /** Key to store UIP state in the local storage */
   @attr({defaultValue: ''}) public storeKey: string;
+  /** State storage based on `storeKey` */
+  protected storage: UIPStateStorage;
 
   /** Indicates ready state of the uip-root */
   @boolAttr({readonly: true}) public ready: boolean;
@@ -62,6 +65,8 @@ export class UIPRoot extends ESLBaseElement {
 
   protected override connectedCallback(): void {
     super.connectedCallback();
+    this.storage = new UIPStateStorage(this.storeKey, this.model);
+
     this.model.snippets = this.$snippets;
     this.model.applyCurrentSnippet(this);
     this.$$attr('ready', true);
@@ -91,8 +96,21 @@ export class UIPRoot extends ESLBaseElement {
     }
   }
 
+  public resetSnippet(source: 'js' | 'html'): void {
+    this.storage.resetState(source, this);
+  }
+
+  public applySnippet(): void {
+    this.storage.loadState(this);
+  }
+
+  public saveSnippet(): void {
+    this.storage.saveState();
+  }
+
   @listen({event: 'uip:model:change', target: ($this: UIPRoot) => $this.model})
   protected onModelChange({detail}: CustomEvent<UIPChangeInfo[]>): void {
+    this.saveSnippet();
     this.dispatchEvent(new UIPChangeEvent(this.CHANGE_EVENT, this, detail));
   }
 
@@ -101,6 +119,7 @@ export class UIPRoot extends ESLBaseElement {
     target: ($this: UIPRoot) => $this.model
   })
   protected onSnippetChange({detail}: CustomEvent): void {
+    this.applySnippet();
     this.$$fire(this.SNIPPET_CHANGE_EVENT, {detail, bubbles: false});
   }
 }
diff --git a/src/plugins/reset/reset-button.less b/src/plugins/reset/reset-button.less
index 0658976d..fd9b6f94 100644
--- a/src/plugins/reset/reset-button.less
+++ b/src/plugins/reset/reset-button.less
@@ -8,7 +8,7 @@
     height: 100%;
   }
 
-  &-hidden {
+  &[disabled] {
     display: none;
   }
 }
diff --git a/src/plugins/reset/reset-button.ts b/src/plugins/reset/reset-button.ts
index 64e49780..a2d0c1f3 100644
--- a/src/plugins/reset/reset-button.ts
+++ b/src/plugins/reset/reset-button.ts
@@ -1,6 +1,6 @@
 import './reset-button.shape';
 
-import {listen, attr} from '@exadel/esl/modules/esl-utils/decorators';
+import {listen, attr, boolAttr} from '@exadel/esl/modules/esl-utils/decorators';
 
 import {UIPPluginButton} from '../../core/button/plugin-button';
 import {UIPRoot} from '../../core/base/root';
@@ -9,27 +9,27 @@ import {UIPRoot} from '../../core/base/root';
 export class UIPReset extends UIPPluginButton {
   public static override is = 'uip-reset';
 
+  @boolAttr() public disabled: boolean;
+
   /** Source type to copy (html | js) */
   @attr({defaultValue: 'html'}) public source: 'js' | 'javascript' | 'html';
 
+  protected get actualSrc(): 'js' | 'html' {
+    return this.source === 'javascript' ? 'js' : this.source;
+  }
+
   protected override connectedCallback(): void {
     super.connectedCallback();
   }
 
   public override onAction(): void {
-    if (this.$root) this.model?.resetSnippet(this.source, this.$root);
+    this.$root?.resetSnippet(this.actualSrc);
   }
 
   @listen({event: 'uip:model:change', target: ($this: UIPRoot) => $this.model})
   protected onModelChange(): void {
     if (!this.model || !this.model.activeSnippet) return;
-    if (this.source === 'js' || this.source === 'javascript') 
-      this.toggleButton(!this.model.isJSChanged());
-    else if (this.source === 'html')
-      this.toggleButton(!this.model.isHTMLChanged());
-  }
-
-  protected toggleButton(state?: boolean): void {
-    this.$$cls('uip-reset-hidden', state);
+    if (this.actualSrc === 'js')  this.disabled = !this.model.isJSChanged();
+    if (this.actualSrc === 'html') this.disabled = !this.model.isHTMLChanged();
   }
 }

From d4f6f114c7e9317833b8dbd493f24a1c0a71ddbb Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Tue, 10 Dec 2024 18:01:44 +0200
Subject: [PATCH 09/16] chore(uip-editor): code refactoring

---
 src/plugins/reset/reset-button.ts | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/src/plugins/reset/reset-button.ts b/src/plugins/reset/reset-button.ts
index a2d0c1f3..eb927cb2 100644
--- a/src/plugins/reset/reset-button.ts
+++ b/src/plugins/reset/reset-button.ts
@@ -18,10 +18,6 @@ export class UIPReset extends UIPPluginButton {
     return this.source === 'javascript' ? 'js' : this.source;
   }
 
-  protected override connectedCallback(): void {
-    super.connectedCallback();
-  }
-
   public override onAction(): void {
     this.$root?.resetSnippet(this.actualSrc);
   }

From e3aa4a3d2e2696770c757d00a812b231f1ceac0a Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Thu, 12 Dec 2024 00:32:45 +0200
Subject: [PATCH 10/16] chore(uip-editor): code refactoring

---
 src/core/base/model.change.ts                 |  3 +-
 src/core/base/model.ts                        |  3 +-
 src/core/base/root.ts                         | 20 ++-------
 src/core/base/source.d.ts                     |  3 ++
 .../{model.storage.ts => state.storage.ts}    | 44 ++++++++++++-------
 src/plugins/copy/copy-button.shape.ts         |  3 +-
 src/plugins/copy/copy-button.ts               | 12 ++---
 src/plugins/editor/editor.tsx                 | 28 ++++--------
 src/plugins/reset/reset-button.shape.ts       |  3 +-
 src/plugins/reset/reset-button.ts             | 14 +++---
 10 files changed, 59 insertions(+), 74 deletions(-)
 create mode 100644 src/core/base/source.d.ts
 rename src/core/base/{model.storage.ts => state.storage.ts} (57%)

diff --git a/src/core/base/model.change.ts b/src/core/base/model.change.ts
index 5393b601..30d4f575 100644
--- a/src/core/base/model.change.ts
+++ b/src/core/base/model.change.ts
@@ -3,10 +3,11 @@ import {overrideEvent} from '@exadel/esl/modules/esl-utils/dom';
 import type {UIPPlugin} from './plugin';
 import type {UIPRoot} from './root';
 import type {UIPStateModel} from './model';
+import type {UIPSource} from './source';
 
 export type UIPChangeInfo = {
   modifier: UIPPlugin | UIPRoot;
-  type: 'html' | 'js' | 'note';
+  type: UIPSource;
   force?: boolean;
 };
 
diff --git a/src/core/base/model.ts b/src/core/base/model.ts
index 504b7dfc..1d245591 100644
--- a/src/core/base/model.ts
+++ b/src/core/base/model.ts
@@ -14,6 +14,7 @@ import type {UIPRoot} from './root';
 import type {UIPPlugin} from './plugin';
 import type {UIPSnippetTemplate} from './snippet';
 import type {UIPChangeInfo} from './model.change';
+import type {UIPEditableSource} from './source';
 
 /** Type for function to change attribute's current value */
 export type TransformSignature = (
@@ -125,7 +126,7 @@ export class UIPStateModel extends SyntheticEventTarget {
     return this.normalizeJS(this.activeSnippet.js) !== this.js;
   }
 
-  public resetSnippet(source: 'js' | 'html', modifier: UIPPlugin | UIPRoot): void {
+  public reset(source: UIPEditableSource, modifier: UIPPlugin | UIPRoot): void {
     if (source === 'html') this.resetHTML(modifier);
     if (source === 'js') this.resetJS(modifier);
   }
diff --git a/src/core/base/root.ts b/src/core/base/root.ts
index 7302ef10..4ba72168 100644
--- a/src/core/base/root.ts
+++ b/src/core/base/root.ts
@@ -9,7 +9,7 @@ import {
 
 import {UIPStateModel} from './model';
 import {UIPChangeEvent} from './model.change';
-import {UIPStateStorage} from './model.storage';
+import {UIPStateStorage} from './state.storage';
 
 import type {UIPChangeInfo} from './model.change';
 import type {UIPSnippetTemplate} from './snippet';
@@ -41,7 +41,7 @@ export class UIPRoot extends ESLBaseElement {
   /** Key to store UIP state in the local storage */
   @attr({defaultValue: ''}) public storeKey: string;
   /** State storage based on `storeKey` */
-  protected storage: UIPStateStorage;
+  public storage: UIPStateStorage | undefined;
 
   /** Indicates ready state of the uip-root */
   @boolAttr({readonly: true}) public ready: boolean;
@@ -65,7 +65,7 @@ export class UIPRoot extends ESLBaseElement {
 
   protected override connectedCallback(): void {
     super.connectedCallback();
-    this.storage = new UIPStateStorage(this.storeKey, this.model);
+    if (this.storeKey) this.storage = new UIPStateStorage(this.storeKey, this);
 
     this.model.snippets = this.$snippets;
     this.model.applyCurrentSnippet(this);
@@ -96,21 +96,8 @@ export class UIPRoot extends ESLBaseElement {
     }
   }
 
-  public resetSnippet(source: 'js' | 'html'): void {
-    this.storage.resetState(source, this);
-  }
-
-  public applySnippet(): void {
-    this.storage.loadState(this);
-  }
-
-  public saveSnippet(): void {
-    this.storage.saveState();
-  }
-
   @listen({event: 'uip:model:change', target: ($this: UIPRoot) => $this.model})
   protected onModelChange({detail}: CustomEvent<UIPChangeInfo[]>): void {
-    this.saveSnippet();
     this.dispatchEvent(new UIPChangeEvent(this.CHANGE_EVENT, this, detail));
   }
 
@@ -119,7 +106,6 @@ export class UIPRoot extends ESLBaseElement {
     target: ($this: UIPRoot) => $this.model
   })
   protected onSnippetChange({detail}: CustomEvent): void {
-    this.applySnippet();
     this.$$fire(this.SNIPPET_CHANGE_EVENT, {detail, bubbles: false});
   }
 }
diff --git a/src/core/base/source.d.ts b/src/core/base/source.d.ts
new file mode 100644
index 00000000..313ae571
--- /dev/null
+++ b/src/core/base/source.d.ts
@@ -0,0 +1,3 @@
+export type UIPEditableSource = 'js' | 'html';
+
+export type UIPSource = UIPEditableSource | 'note';
diff --git a/src/core/base/model.storage.ts b/src/core/base/state.storage.ts
similarity index 57%
rename from src/core/base/model.storage.ts
rename to src/core/base/state.storage.ts
index d964d45c..f557fc8e 100644
--- a/src/core/base/model.storage.ts
+++ b/src/core/base/state.storage.ts
@@ -1,6 +1,6 @@
 import type {UIPStateModel} from './model';
-import type {UIPPlugin} from './plugin';
 import type {UIPRoot} from './root';
+import type {UIPEditableSource} from './source';
 
 interface UIPStateStorageEntry {
   ts: string;
@@ -18,30 +18,40 @@ export class UIPStateStorage {
 
   protected static readonly EXPIRATION_TIME = 3600000 * 12; // 12 hours
 
-  public constructor(protected storeKey: string, protected model: UIPStateModel) {}
+  protected model: UIPStateModel;
+
+  public constructor(protected storeKey: string, protected root: UIPRoot) {
+    this.model = root.model;
+    this.addEventListeners();
+  }
+
+  protected addEventListeners(): void {
+    this.model.addEventListener('uip:model:change', () => this.saveState());
+    this.model.addEventListener('uip:model:snippet:change', () => this.loadState());
+  }
 
   protected loadEntry(key: string): string | null {
-    const entry = (this.lsGet()[key] || {}) as UIPStateStorageEntry;
+    const entry = (this.lsState[key] || {}) as UIPStateStorageEntry;
     if (parseInt(entry?.ts, 10) + UIPStateStorage.EXPIRATION_TIME > Date.now()) return entry.snippets || null;
     this.removeEntry(key);
     return null;
   }
 
   protected saveEntry(key: string, value: string): void {
-    this.lsSet(Object.assign(this.lsGet(), {[key]: {ts: Date.now(), snippets: value}}));
+    this.lsState = Object.assign(this.lsState, {[key]: {ts: Date.now(), snippets: value}});
   }
 
   protected removeEntry(key: string): void {
-    const data = this.lsGet();
-    delete data[key];
-    this.lsSet(data);
+    const data = this.lsState;
+    delete this.lsState[key];
+    this.lsState = data;
   }
 
-  protected lsGet(): Record<string, any> {
+  protected get lsState(): Record<string, any> {
     return JSON.parse(localStorage.getItem(UIPStateStorage.STORAGE_KEY) || '{}');
   }
-
-  protected lsSet(value: Record<string, any>): void {
+  
+  protected set lsState(value: Record<string, any>) {
     localStorage.setItem(UIPStateStorage.STORAGE_KEY, JSON.stringify(value));
   }
 
@@ -51,15 +61,15 @@ export class UIPStateStorage {
     return JSON.stringify({key: this.storeKey, snippet: activeSnippet.html});
   }
 
-  public loadState(initiator: UIPPlugin | UIPRoot): void {
+  public loadState(): void {
     const stateKey = this.getStateKey();
     const state = stateKey && this.loadEntry(stateKey);
     if (!state) return; 
     
-    const stateobj = JSON.parse(state);
-    this.model.setHtml(stateobj.html, initiator, true);
-    this.model.setJS(stateobj.js, initiator);
-    this.model.setNote(stateobj.note, initiator);
+    const stateobj = JSON.parse(state) as UIPStateModelSnippets;
+    this.model.setHtml(stateobj.html, this.root, true);
+    this.model.setJS(stateobj.js, this.root);
+    this.model.setNote(stateobj.note, this.root);
   }
 
   public saveState(): void {
@@ -68,10 +78,10 @@ export class UIPStateStorage {
     stateKey && this.saveEntry(stateKey, JSON.stringify({js, html, note}));
   }
 
-  public resetState(source: 'js' | 'html', modifier: UIPPlugin | UIPRoot): void {
+  public resetState(source: UIPEditableSource): void {
     const stateKey = this.getStateKey();
     stateKey && this.removeEntry(stateKey);
 
-    this.model.resetSnippet(source, modifier);
+    this.model.reset(source, this.root);
   }
 }
diff --git a/src/plugins/copy/copy-button.shape.ts b/src/plugins/copy/copy-button.shape.ts
index 6f947e46..55648d9f 100644
--- a/src/plugins/copy/copy-button.shape.ts
+++ b/src/plugins/copy/copy-button.shape.ts
@@ -1,8 +1,9 @@
 import type {ESLBaseElementShape} from '@exadel/esl/modules/esl-base-element/core';
 import type {UIPCopy} from './copy-button';
+import type {UIPEditableSource} from '../../core/base/source';
 
 export interface UIPCopyShape extends ESLBaseElementShape<UIPCopy> {
-  source?: 'javascript' | 'js' | 'html';
+  source?: UIPEditableSource;
   children?: any;
 }
 
diff --git a/src/plugins/copy/copy-button.ts b/src/plugins/copy/copy-button.ts
index 5eca7d47..7de06749 100644
--- a/src/plugins/copy/copy-button.ts
+++ b/src/plugins/copy/copy-button.ts
@@ -4,6 +4,7 @@ import {attr} from '@exadel/esl/modules/esl-utils/decorators';
 import {UIPPluginButton} from '../../core/button/plugin-button';
 
 import type {ESLAlertActionParams} from '@exadel/esl/modules/esl-alert/core';
+import type {UIPEditableSource} from '../../core/base/source';
 
 /** Button-plugin to copy snippet to clipboard */
 export class UIPCopy extends UIPPluginButton {
@@ -11,7 +12,7 @@ export class UIPCopy extends UIPPluginButton {
   public static override defaultTitle = 'Copy to clipboard';
 
   /** Source type to copy (html | js) */
-  @attr({defaultValue: 'html'}) public source: string;
+  @attr({defaultValue: 'html'}) public source: UIPEditableSource;
 
   public static msgConfig: ESLAlertActionParams = {
     text: 'Playground content copied to clipboard',
@@ -20,14 +21,7 @@ export class UIPCopy extends UIPPluginButton {
 
   /** Content to copy */
   protected get content(): string | undefined {
-    switch (this.source) {
-      case 'js':
-      case 'javascript':
-        return this.model?.js;
-      case 'html':
-      default:
-        return this.model?.html;
-    }
+    if (this.source === 'js' || this.source === 'html') return this.model?.[this.source];
   }
 
   protected override connectedCallback(): void {
diff --git a/src/plugins/editor/editor.tsx b/src/plugins/editor/editor.tsx
index 1e6bbcc8..4a31144f 100644
--- a/src/plugins/editor/editor.tsx
+++ b/src/plugins/editor/editor.tsx
@@ -17,6 +17,7 @@ import {EditorIcon} from './editor.icon';
 
 import type {UIPSnippetsList} from '../snippets-list/snippets-list';
 import type {UIPChangeEvent} from '../../core/base/model.change';
+import type {UIPEditableSource} from '../../core/base/source';
 
 /**
  * Editor {@link UIPPlugin} custom element definition
@@ -30,7 +31,7 @@ export class UIPEditor extends UIPPluginPanel {
   public static highlight = (editor: HTMLElement): void => Prism.highlightElement(editor, false);
 
   /** Source for Editor plugin (default: 'html') */
-  @attr({defaultValue: 'html'}) public source: 'js' | 'javascript' | 'html';
+  @attr({defaultValue: 'html'}) public source: UIPEditableSource;
 
   /** Marker to display copy widget */
   @boolAttr({name: 'copy'}) public showCopy: boolean;
@@ -143,30 +144,19 @@ export class UIPEditor extends UIPPluginPanel {
   @decorate(debounce, 2000)
   protected _onChange(): void {
     if (!this.editable) return;
-    switch (this.source) {
-      case 'js':
-      case 'javascript':
-        this.model!.setJS(this.value, this);
-        break;
-      case 'html':
-        this.model!.setHtml(this.value, this);
-    }
+    if (this.source === 'js') this.model!.setJS(this.value, this);
+    if (this.source === 'html') this.model!.setHtml(this.value, this);
   }
 
   /** Change editor's markup from markup state changes */
   @listen({event: 'uip:change', target: ($this: UIPEditor) => $this.$root})
   protected _onRootStateChange(e?: UIPChangeEvent): void {
     if (e && e.isOnlyModifier(this)) return;
-    switch (this.source) {
-      case 'js':
-      case 'javascript':
-        if (e && !e.jsChanges.length) return;
-        this.value = this.model!.js;
-        break;
-      case 'html':
-        if (e && !e.htmlChanges.length) return;
-        this.value = this.model!.html;
-    }
+
+    if (
+      (this.source === 'js' && (!e || e.jsChanges.length)) ||
+      (this.source === 'html' && (!e || e.htmlChanges.length))
+    ) this.value = this.model![this.source];
   }
 
   /** Handles snippet change to set readonly value */
diff --git a/src/plugins/reset/reset-button.shape.ts b/src/plugins/reset/reset-button.shape.ts
index 269b2bf1..ac487996 100644
--- a/src/plugins/reset/reset-button.shape.ts
+++ b/src/plugins/reset/reset-button.shape.ts
@@ -1,8 +1,9 @@
 import type {ESLBaseElementShape} from '@exadel/esl/modules/esl-base-element/core';
 import type {UIPReset} from './reset-button';
+import type {UIPEditableSource} from '../../core/base/source';
 
 export interface UIPResetShape extends ESLBaseElementShape<UIPReset> {
-  source?: 'javascript' | 'js' | 'html';
+  source?: UIPEditableSource;
   children?: any;
 }
 
diff --git a/src/plugins/reset/reset-button.ts b/src/plugins/reset/reset-button.ts
index eb927cb2..162acc48 100644
--- a/src/plugins/reset/reset-button.ts
+++ b/src/plugins/reset/reset-button.ts
@@ -5,6 +5,8 @@ import {listen, attr, boolAttr} from '@exadel/esl/modules/esl-utils/decorators';
 import {UIPPluginButton} from '../../core/button/plugin-button';
 import {UIPRoot} from '../../core/base/root';
 
+import type {UIPEditableSource} from '../../core/base/source';
+
 /** Button-plugin to reset snippet to default settings */
 export class UIPReset extends UIPPluginButton {
   public static override is = 'uip-reset';
@@ -12,20 +14,16 @@ export class UIPReset extends UIPPluginButton {
   @boolAttr() public disabled: boolean;
 
   /** Source type to copy (html | js) */
-  @attr({defaultValue: 'html'}) public source: 'js' | 'javascript' | 'html';
-
-  protected get actualSrc(): 'js' | 'html' {
-    return this.source === 'javascript' ? 'js' : this.source;
-  }
+  @attr({defaultValue: 'html'}) public source: UIPEditableSource;
 
   public override onAction(): void {
-    this.$root?.resetSnippet(this.actualSrc);
+    this.$root?.storage!.resetState(this.source);
   }
 
   @listen({event: 'uip:model:change', target: ($this: UIPRoot) => $this.model})
   protected onModelChange(): void {
     if (!this.model || !this.model.activeSnippet) return;
-    if (this.actualSrc === 'js')  this.disabled = !this.model.isJSChanged();
-    if (this.actualSrc === 'html') this.disabled = !this.model.isHTMLChanged();
+    if (this.source === 'js')  this.disabled = !this.model.isJSChanged();
+    if (this.source === 'html') this.disabled = !this.model.isHTMLChanged();
   }
 }

From 7d922fb5704463b2d91cf7556ee15d80113f9f7f Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Thu, 12 Dec 2024 00:37:55 +0200
Subject: [PATCH 11/16] chore(uip-editor): code refactoring

---
 src/core/base/root.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/core/base/root.ts b/src/core/base/root.ts
index 4ba72168..d0156164 100644
--- a/src/core/base/root.ts
+++ b/src/core/base/root.ts
@@ -11,8 +11,8 @@ import {UIPStateModel} from './model';
 import {UIPChangeEvent} from './model.change';
 import {UIPStateStorage} from './state.storage';
 
-import type {UIPChangeInfo} from './model.change';
 import type {UIPSnippetTemplate} from './snippet';
+import type {UIPChangeInfo} from './model.change';
 
 /**
  * UI Playground root custom element definition

From 559f53796889f7cdbeafe1142bc4a51d246068d2 Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Fri, 13 Dec 2024 13:06:02 +0200
Subject: [PATCH 12/16] chore(uip-editor): code refactoring

---
 src/core/base/{source.d.ts => source.ts} |  0
 src/core/base/state.storage.ts           | 28 ++++++++++++++----------
 src/plugins/reset/reset-button.ts        |  2 +-
 3 files changed, 18 insertions(+), 12 deletions(-)
 rename src/core/base/{source.d.ts => source.ts} (100%)

diff --git a/src/core/base/source.d.ts b/src/core/base/source.ts
similarity index 100%
rename from src/core/base/source.d.ts
rename to src/core/base/source.ts
diff --git a/src/core/base/state.storage.ts b/src/core/base/state.storage.ts
index f557fc8e..282c2935 100644
--- a/src/core/base/state.storage.ts
+++ b/src/core/base/state.storage.ts
@@ -1,3 +1,4 @@
+import { ESLEventUtils, listen } from '@exadel/esl';
 import type {UIPStateModel} from './model';
 import type {UIPRoot} from './root';
 import type {UIPEditableSource} from './source';
@@ -22,36 +23,41 @@ export class UIPStateStorage {
 
   public constructor(protected storeKey: string, protected root: UIPRoot) {
     this.model = root.model;
-    this.addEventListeners();
+    ESLEventUtils.subscribe(this);
   }
 
-  protected addEventListeners(): void {
-    this.model.addEventListener('uip:model:change', () => this.saveState());
-    this.model.addEventListener('uip:model:snippet:change', () => this.loadState());
+  @listen({event: 'uip:model:change', target: ($this: UIPStateStorage) => $this.model})
+  protected _onModelChange(): void {
+    this.saveState()
+  }
+
+  @listen({event: 'uip:model:snippet:change', target: ($this: UIPStateStorage) => $this.model})
+  protected _onSnippetChange(): void {
+    this.loadState()
   }
 
   protected loadEntry(key: string): string | null {
-    const entry = (this.lsState[key] || {}) as UIPStateStorageEntry;
+    const entry = (this._lsState[key] || {}) as UIPStateStorageEntry;
     if (parseInt(entry?.ts, 10) + UIPStateStorage.EXPIRATION_TIME > Date.now()) return entry.snippets || null;
     this.removeEntry(key);
     return null;
   }
 
   protected saveEntry(key: string, value: string): void {
-    this.lsState = Object.assign(this.lsState, {[key]: {ts: Date.now(), snippets: value}});
+    this._lsState = Object.assign(this._lsState, {[key]: {ts: Date.now(), snippets: value}});
   }
 
   protected removeEntry(key: string): void {
-    const data = this.lsState;
-    delete this.lsState[key];
-    this.lsState = data;
+    const data = this._lsState;
+    delete this._lsState[key];
+    this._lsState = data;
   }
 
-  protected get lsState(): Record<string, any> {
+  protected get _lsState(): Record<string, any> {
     return JSON.parse(localStorage.getItem(UIPStateStorage.STORAGE_KEY) || '{}');
   }
   
-  protected set lsState(value: Record<string, any>) {
+  protected set _lsState(value: Record<string, any>) {
     localStorage.setItem(UIPStateStorage.STORAGE_KEY, JSON.stringify(value));
   }
 
diff --git a/src/plugins/reset/reset-button.ts b/src/plugins/reset/reset-button.ts
index 162acc48..d3c00cc7 100644
--- a/src/plugins/reset/reset-button.ts
+++ b/src/plugins/reset/reset-button.ts
@@ -21,7 +21,7 @@ export class UIPReset extends UIPPluginButton {
   }
 
   @listen({event: 'uip:model:change', target: ($this: UIPRoot) => $this.model})
-  protected onModelChange(): void {
+  protected _onModelChange(): void {
     if (!this.model || !this.model.activeSnippet) return;
     if (this.source === 'js')  this.disabled = !this.model.isJSChanged();
     if (this.source === 'html') this.disabled = !this.model.isHTMLChanged();

From 03cf84c712016ee5ef61ffb84d41decf45dcd6d7 Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Fri, 13 Dec 2024 13:09:29 +0200
Subject: [PATCH 13/16] chore(uip-editor): code refactoring

---
 src/core/base/state.storage.ts | 24 +++++++++++++-----------
 1 file changed, 13 insertions(+), 11 deletions(-)

diff --git a/src/core/base/state.storage.ts b/src/core/base/state.storage.ts
index 282c2935..d1ff3000 100644
--- a/src/core/base/state.storage.ts
+++ b/src/core/base/state.storage.ts
@@ -1,4 +1,6 @@
-import { ESLEventUtils, listen } from '@exadel/esl';
+import {ESLEventUtils} from '@exadel/esl/modules/esl-utils/dom';
+import {listen} from '@exadel/esl/modules/esl-utils/decorators';
+
 import type {UIPStateModel} from './model';
 import type {UIPRoot} from './root';
 import type {UIPEditableSource} from './source';
@@ -26,16 +28,6 @@ export class UIPStateStorage {
     ESLEventUtils.subscribe(this);
   }
 
-  @listen({event: 'uip:model:change', target: ($this: UIPStateStorage) => $this.model})
-  protected _onModelChange(): void {
-    this.saveState()
-  }
-
-  @listen({event: 'uip:model:snippet:change', target: ($this: UIPStateStorage) => $this.model})
-  protected _onSnippetChange(): void {
-    this.loadState()
-  }
-
   protected loadEntry(key: string): string | null {
     const entry = (this._lsState[key] || {}) as UIPStateStorageEntry;
     if (parseInt(entry?.ts, 10) + UIPStateStorage.EXPIRATION_TIME > Date.now()) return entry.snippets || null;
@@ -90,4 +82,14 @@ export class UIPStateStorage {
 
     this.model.reset(source, this.root);
   }
+
+  @listen({event: 'uip:model:change', target: ($this: UIPStateStorage) => $this.model})
+  protected _onModelChange(): void {
+    this.saveState()
+  }
+
+  @listen({event: 'uip:model:snippet:change', target: ($this: UIPStateStorage) => $this.model})
+  protected _onSnippetChange(): void {
+    this.loadState()
+  }
 }

From 13920ba66def6c44f09a659a02507de7d20cff03 Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Wed, 18 Dec 2024 15:10:12 +0200
Subject: [PATCH 14/16] chore(uip-editor): code refactoring

---
 src/core/base/root.ts          |  2 +-
 src/core/base/state.storage.ts | 14 +++++---------
 2 files changed, 6 insertions(+), 10 deletions(-)

diff --git a/src/core/base/root.ts b/src/core/base/root.ts
index d0156164..8c353a7b 100644
--- a/src/core/base/root.ts
+++ b/src/core/base/root.ts
@@ -65,7 +65,7 @@ export class UIPRoot extends ESLBaseElement {
 
   protected override connectedCallback(): void {
     super.connectedCallback();
-    if (this.storeKey) this.storage = new UIPStateStorage(this.storeKey, this);
+    if (this.storeKey) this.storage = new UIPStateStorage(this.storeKey, this.model);
 
     this.model.snippets = this.$snippets;
     this.model.applyCurrentSnippet(this);
diff --git a/src/core/base/state.storage.ts b/src/core/base/state.storage.ts
index d1ff3000..1efdfc40 100644
--- a/src/core/base/state.storage.ts
+++ b/src/core/base/state.storage.ts
@@ -2,7 +2,6 @@ import {ESLEventUtils} from '@exadel/esl/modules/esl-utils/dom';
 import {listen} from '@exadel/esl/modules/esl-utils/decorators';
 
 import type {UIPStateModel} from './model';
-import type {UIPRoot} from './root';
 import type {UIPEditableSource} from './source';
 
 interface UIPStateStorageEntry {
@@ -21,10 +20,7 @@ export class UIPStateStorage {
 
   protected static readonly EXPIRATION_TIME = 3600000 * 12; // 12 hours
 
-  protected model: UIPStateModel;
-
-  public constructor(protected storeKey: string, protected root: UIPRoot) {
-    this.model = root.model;
+  public constructor(protected storeKey: string, protected model: UIPStateModel) {
     ESLEventUtils.subscribe(this);
   }
 
@@ -65,9 +61,9 @@ export class UIPStateStorage {
     if (!state) return; 
     
     const stateobj = JSON.parse(state) as UIPStateModelSnippets;
-    this.model.setHtml(stateobj.html, this.root, true);
-    this.model.setJS(stateobj.js, this.root);
-    this.model.setNote(stateobj.note, this.root);
+    this.model.setHtml(stateobj.html, this as any, true);
+    this.model.setJS(stateobj.js, this as any);
+    this.model.setNote(stateobj.note, this as any);
   }
 
   public saveState(): void {
@@ -80,7 +76,7 @@ export class UIPStateStorage {
     const stateKey = this.getStateKey();
     stateKey && this.removeEntry(stateKey);
 
-    this.model.reset(source, this.root);
+    this.model.reset(source, this as any);
   }
 
   @listen({event: 'uip:model:change', target: ($this: UIPStateStorage) => $this.model})

From d02fc15c2a6524bb7c5d17e1d6374496bbc1e986 Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Wed, 18 Dec 2024 15:47:32 +0200
Subject: [PATCH 15/16] chore(uip-editor): code refactoring

---
 src/core/base/model.change.ts  |  6 ++++--
 src/core/base/model.ts         | 22 ++++++++++------------
 src/core/base/state.storage.ts |  8 ++++----
 3 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/src/core/base/model.change.ts b/src/core/base/model.change.ts
index 30d4f575..645c62a2 100644
--- a/src/core/base/model.change.ts
+++ b/src/core/base/model.change.ts
@@ -5,8 +5,10 @@ import type {UIPRoot} from './root';
 import type {UIPStateModel} from './model';
 import type {UIPSource} from './source';
 
+export type UIPModifier = UIPPlugin | UIPRoot | object;
+
 export type UIPChangeInfo = {
-  modifier: UIPPlugin | UIPRoot;
+  modifier: UIPModifier;
   type: UIPSource;
   force?: boolean;
 };
@@ -39,7 +41,7 @@ export class UIPChangeEvent extends Event {
     return this.changes.filter((change) => change.type === 'html');
   }
 
-  public isOnlyModifier(modifier: UIPPlugin | UIPRoot): boolean {
+  public isOnlyModifier(modifier: UIPModifier): boolean {
     return this.changes.every((change) => change.modifier === modifier);
   }
 }
diff --git a/src/core/base/model.ts b/src/core/base/model.ts
index 1d245591..f9d2ae18 100644
--- a/src/core/base/model.ts
+++ b/src/core/base/model.ts
@@ -10,10 +10,8 @@ import {
 
 import {UIPSnippetItem} from './snippet';
 
-import type {UIPRoot} from './root';
-import type {UIPPlugin} from './plugin';
 import type {UIPSnippetTemplate} from './snippet';
-import type {UIPChangeInfo} from './model.change';
+import type {UIPChangeInfo, UIPModifier} from './model.change';
 import type {UIPEditableSource} from './source';
 
 /** Type for function to change attribute's current value */
@@ -28,7 +26,7 @@ export type ChangeAttrConfig = {
   /** Attribute to change */
   attribute: string;
   /** Changes initiator */
-  modifier: UIPPlugin | UIPRoot;
+  modifier: UIPModifier;
 } & ({
   /** New {@link attribute} value */
   value: string | boolean;
@@ -64,7 +62,7 @@ export class UIPStateModel extends SyntheticEventTarget {
    * @param js - new state
    * @param modifier - plugin, that initiates the change
    */
-  public setJS(js: string, modifier: UIPPlugin | UIPRoot): void {
+  public setJS(js: string, modifier: UIPModifier): void {
     const script = this.normalizeJS(js);
     if (this._js === script) return;
     this._js = script;
@@ -81,7 +79,7 @@ export class UIPStateModel extends SyntheticEventTarget {
    * @param text - new state
    * @param modifier - plugin, that initiates the change
    */
-  public setNote(text: string, modifier: UIPPlugin | UIPRoot): void {
+  public setNote(text: string, modifier: UIPModifier): void {
     const note = UIPNoteNormalizationPreprocessors.preprocess(text);
     if (this._note === note) return;
     this._note = note;
@@ -95,7 +93,7 @@ export class UIPStateModel extends SyntheticEventTarget {
    * @param modifier - plugin, that initiates the change
    * @param force - marker, that indicates if html changes require iframe rerender
    */
-  public setHtml(markup: string, modifier: UIPPlugin | UIPRoot, force: boolean = false): void {
+  public setHtml(markup: string, modifier: UIPModifier, force: boolean = false): void {
     const root = this.normalizeHTML(markup);
     if (root.innerHTML.trim() === this.html.trim()) return;
     this._html = root;
@@ -126,16 +124,16 @@ export class UIPStateModel extends SyntheticEventTarget {
     return this.normalizeJS(this.activeSnippet.js) !== this.js;
   }
 
-  public reset(source: UIPEditableSource, modifier: UIPPlugin | UIPRoot): void {
+  public reset(source: UIPEditableSource, modifier: UIPModifier): void {
     if (source === 'html') this.resetHTML(modifier);
     if (source === 'js') this.resetJS(modifier);
   }
 
-  protected resetJS(modifier: UIPPlugin | UIPRoot): void {
+  protected resetJS(modifier: UIPModifier): void {
     if (this.activeSnippet) this.setJS(this.activeSnippet.js, modifier);
   }
 
-  protected resetHTML(modifier: UIPPlugin | UIPRoot): void {
+  protected resetHTML(modifier: UIPModifier): void {
     if (this.activeSnippet) this.setHtml(this.activeSnippet.html, modifier);
   }
 
@@ -182,7 +180,7 @@ export class UIPStateModel extends SyntheticEventTarget {
   /** Changes current active snippet */
   public applySnippet(
     snippet: UIPSnippetItem,
-    modifier: UIPPlugin | UIPRoot
+    modifier: UIPModifier
   ): void {
     if (!snippet) return;
     this._snippets.forEach((s) => (s.active = s === snippet));
@@ -194,7 +192,7 @@ export class UIPStateModel extends SyntheticEventTarget {
     );
   }
   /** Applies an active snippet from DOM */
-  public applyCurrentSnippet(modifier: UIPPlugin | UIPRoot): void {
+  public applyCurrentSnippet(modifier: UIPModifier): void {
     const activeSnippet = this.anchorSnippet || this.activeSnippet || this.snippets[0];
     this.applySnippet(activeSnippet, modifier);
   }
diff --git a/src/core/base/state.storage.ts b/src/core/base/state.storage.ts
index 1efdfc40..245492be 100644
--- a/src/core/base/state.storage.ts
+++ b/src/core/base/state.storage.ts
@@ -61,9 +61,9 @@ export class UIPStateStorage {
     if (!state) return; 
     
     const stateobj = JSON.parse(state) as UIPStateModelSnippets;
-    this.model.setHtml(stateobj.html, this as any, true);
-    this.model.setJS(stateobj.js, this as any);
-    this.model.setNote(stateobj.note, this as any);
+    this.model.setHtml(stateobj.html, this, true);
+    this.model.setJS(stateobj.js, this);
+    this.model.setNote(stateobj.note, this);
   }
 
   public saveState(): void {
@@ -76,7 +76,7 @@ export class UIPStateStorage {
     const stateKey = this.getStateKey();
     stateKey && this.removeEntry(stateKey);
 
-    this.model.reset(source, this as any);
+    this.model.reset(source, this);
   }
 
   @listen({event: 'uip:model:change', target: ($this: UIPStateStorage) => $this.model})

From af67d3e2dcde675a42ac2a934af6b57f08ce084a Mon Sep 17 00:00:00 2001
From: Feoktist Shovchko <fshovchko@exadel.com>
Date: Wed, 18 Dec 2024 16:33:54 +0200
Subject: [PATCH 16/16] chore(uip-editor): code refactoring

---
 src/core/base/model.change.ts |  7 ++-----
 src/core/base/model.ts        | 20 ++++++++++----------
 2 files changed, 12 insertions(+), 15 deletions(-)

diff --git a/src/core/base/model.change.ts b/src/core/base/model.change.ts
index 645c62a2..e8ee76de 100644
--- a/src/core/base/model.change.ts
+++ b/src/core/base/model.change.ts
@@ -1,14 +1,11 @@
 import {overrideEvent} from '@exadel/esl/modules/esl-utils/dom';
 
-import type {UIPPlugin} from './plugin';
 import type {UIPRoot} from './root';
 import type {UIPStateModel} from './model';
 import type {UIPSource} from './source';
 
-export type UIPModifier = UIPPlugin | UIPRoot | object;
-
 export type UIPChangeInfo = {
-  modifier: UIPModifier;
+  modifier: object;
   type: UIPSource;
   force?: boolean;
 };
@@ -41,7 +38,7 @@ export class UIPChangeEvent extends Event {
     return this.changes.filter((change) => change.type === 'html');
   }
 
-  public isOnlyModifier(modifier: UIPModifier): boolean {
+  public isOnlyModifier(modifier: object): boolean {
     return this.changes.every((change) => change.modifier === modifier);
   }
 }
diff --git a/src/core/base/model.ts b/src/core/base/model.ts
index f9d2ae18..1501edaf 100644
--- a/src/core/base/model.ts
+++ b/src/core/base/model.ts
@@ -11,7 +11,7 @@ import {
 import {UIPSnippetItem} from './snippet';
 
 import type {UIPSnippetTemplate} from './snippet';
-import type {UIPChangeInfo, UIPModifier} from './model.change';
+import type {UIPChangeInfo} from './model.change';
 import type {UIPEditableSource} from './source';
 
 /** Type for function to change attribute's current value */
@@ -26,7 +26,7 @@ export type ChangeAttrConfig = {
   /** Attribute to change */
   attribute: string;
   /** Changes initiator */
-  modifier: UIPModifier;
+  modifier: object;
 } & ({
   /** New {@link attribute} value */
   value: string | boolean;
@@ -62,7 +62,7 @@ export class UIPStateModel extends SyntheticEventTarget {
    * @param js - new state
    * @param modifier - plugin, that initiates the change
    */
-  public setJS(js: string, modifier: UIPModifier): void {
+  public setJS(js: string, modifier: object): void {
     const script = this.normalizeJS(js);
     if (this._js === script) return;
     this._js = script;
@@ -79,7 +79,7 @@ export class UIPStateModel extends SyntheticEventTarget {
    * @param text - new state
    * @param modifier - plugin, that initiates the change
    */
-  public setNote(text: string, modifier: UIPModifier): void {
+  public setNote(text: string, modifier: object): void {
     const note = UIPNoteNormalizationPreprocessors.preprocess(text);
     if (this._note === note) return;
     this._note = note;
@@ -93,7 +93,7 @@ export class UIPStateModel extends SyntheticEventTarget {
    * @param modifier - plugin, that initiates the change
    * @param force - marker, that indicates if html changes require iframe rerender
    */
-  public setHtml(markup: string, modifier: UIPModifier, force: boolean = false): void {
+  public setHtml(markup: string, modifier: object, force: boolean = false): void {
     const root = this.normalizeHTML(markup);
     if (root.innerHTML.trim() === this.html.trim()) return;
     this._html = root;
@@ -124,16 +124,16 @@ export class UIPStateModel extends SyntheticEventTarget {
     return this.normalizeJS(this.activeSnippet.js) !== this.js;
   }
 
-  public reset(source: UIPEditableSource, modifier: UIPModifier): void {
+  public reset(source: UIPEditableSource, modifier: object): void {
     if (source === 'html') this.resetHTML(modifier);
     if (source === 'js') this.resetJS(modifier);
   }
 
-  protected resetJS(modifier: UIPModifier): void {
+  protected resetJS(modifier: object): void {
     if (this.activeSnippet) this.setJS(this.activeSnippet.js, modifier);
   }
 
-  protected resetHTML(modifier: UIPModifier): void {
+  protected resetHTML(modifier: object): void {
     if (this.activeSnippet) this.setHtml(this.activeSnippet.html, modifier);
   }
 
@@ -180,7 +180,7 @@ export class UIPStateModel extends SyntheticEventTarget {
   /** Changes current active snippet */
   public applySnippet(
     snippet: UIPSnippetItem,
-    modifier: UIPModifier
+    modifier: object
   ): void {
     if (!snippet) return;
     this._snippets.forEach((s) => (s.active = s === snippet));
@@ -192,7 +192,7 @@ export class UIPStateModel extends SyntheticEventTarget {
     );
   }
   /** Applies an active snippet from DOM */
-  public applyCurrentSnippet(modifier: UIPModifier): void {
+  public applyCurrentSnippet(modifier: object): void {
     const activeSnippet = this.anchorSnippet || this.activeSnippet || this.snippets[0];
     this.applySnippet(activeSnippet, modifier);
   }