From 7c8dc7e3756c6e4b371f74cf996d82acd603ebb3 Mon Sep 17 00:00:00 2001 From: Feoktist Shovchko 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 { + return JSON.parse(localStorage.getItem(EditorStorage.STORAGE_KEY) || '{}'); + } + + protected static set(value: Record): 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 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 { + return JSON.parse(localStorage.getItem(this.STORAGE_KEY) || '{}'); + } + + protected static lsSet(value: Record): 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 { - return JSON.parse(localStorage.getItem(EditorStorage.STORAGE_KEY) || '{}'); - } - - protected static set(value: Record): 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 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 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(); + + 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 (
{this.showCopy ? : ''} + {this.$root?.storeKey ? : ''}
) 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 => ( + + + +) 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 { + 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 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 (
{this.showCopy ? : ''} - {this.$root?.storeKey ? : ''} + {this.$root?.storeKey ? : ''}
) 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 => ( - + ) 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 { + 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 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 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 { - return JSON.parse(localStorage.getItem(this.STORAGE_KEY) || '{}'); + protected lsGet(): Record { + return JSON.parse(localStorage.getItem(UIPStateStorage.STORAGE_KEY) || '{}'); } - protected static lsSet(value: Record): void { - localStorage.setItem(this.STORAGE_KEY, JSON.stringify(value)); + protected lsSet(value: Record): 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 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(); - - 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): 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 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 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): 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 { + protected get lsState(): Record { return JSON.parse(localStorage.getItem(UIPStateStorage.STORAGE_KEY) || '{}'); } - - protected lsSet(value: Record): void { + + protected set lsState(value: Record) { 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 { - 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 { - 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 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 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 { + protected get _lsState(): Record { return JSON.parse(localStorage.getItem(UIPStateStorage.STORAGE_KEY) || '{}'); } - protected set lsState(value: Record) { + protected set _lsState(value: Record) { 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 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 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 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 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); }