From b46eb09afd5027616845aa1688960cf55807fe82 Mon Sep 17 00:00:00 2001 From: Kaleniuk Date: Mon, 17 Nov 2025 11:25:43 +0200 Subject: [PATCH] update --- packages/core/index.html | 65 ++------------- .../core/src/css_composer/model/CssRule.ts | 1 + .../src/dom_components/model/Component.ts | 1 + .../domain_abstract/model/ModelWithPatches.ts | 4 +- .../domain_abstract/model/StyleableModel.ts | 5 +- packages/core/src/editor/config/config.ts | 6 ++ packages/core/src/editor/index.ts | 5 ++ packages/core/src/editor/model/Editor.ts | 6 ++ packages/core/src/editor/types.ts | 37 ++++++++- packages/core/src/patch_manager/index.ts | 40 +++++---- packages/core/src/patch_manager/types.ts | 7 ++ .../core/test/specs/patch_manager/index.ts | 82 +++++++++++++++++++ patch-test.html | 2 +- 13 files changed, 180 insertions(+), 81 deletions(-) create mode 100644 packages/core/test/specs/patch_manager/index.ts diff --git a/packages/core/index.html b/packages/core/index.html index 6dfc5ba11..5c4214211 100755 --- a/packages/core/index.html +++ b/packages/core/index.html @@ -88,68 +88,13 @@ height: '100%', fromElement: true, storageManager: { autoload: 0 }, - styleManager: { - sectors: [ - { - name: 'General', - open: false, - buildProps: ['float', 'display', 'position', 'top', 'right', 'left', 'bottom'], - }, - { - name: 'Flex', - open: false, - buildProps: [ - 'flex-direction', - 'flex-wrap', - 'justify-content', - 'align-items', - 'align-content', - 'order', - 'flex-basis', - 'flex-grow', - 'flex-shrink', - 'align-self', - ], - }, - { - name: 'Dimension', - open: false, - buildProps: ['width', 'height', 'max-width', 'min-height', 'margin', 'padding'], - }, - { - name: 'Typography', - open: false, - buildProps: [ - 'font-family', - 'font-size', - 'font-weight', - 'letter-spacing', - 'color', - 'line-height', - 'text-shadow', - ], - }, - { - name: 'Decorations', - open: false, - buildProps: [ - 'border-radius-c', - 'background-color', - 'border-radius', - 'border', - 'box-shadow', - 'background', - ], - }, - { - name: 'Extra', - open: false, - buildProps: ['transition', 'perspective', 'transform'], - }, - ], - }, + }); + // editor.on('patch:update', ({ patch }) => console.log('patch:update', patch)); + editor.on('patch:undo', ({ patch }) => console.log('patch:undo', patch)); + editor.on('patch:redo', ({ patch }) => console.log('patch:redo', patch)); + editor.BlockManager.add('testBlock', { label: 'Block', attributes: { class: 'gjs-fonts gjs-f-b1' }, diff --git a/packages/core/src/css_composer/model/CssRule.ts b/packages/core/src/css_composer/model/CssRule.ts index 00a732966..21a8a3531 100644 --- a/packages/core/src/css_composer/model/CssRule.ts +++ b/packages/core/src/css_composer/model/CssRule.ts @@ -97,6 +97,7 @@ const { CSS } = hasWin() ? window : {}; * [Component]: component.html */ export default class CssRule extends StyleableModel { + patchObjectType = 'cssRule'; config: CssRuleProperties; em?: EditorModel; opt: any; diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 90729864e..24aff2488 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -154,6 +154,7 @@ type GetComponentStyleOpts = GetStyleOpts & { * @module docsjs.Component */ export default class Component extends StyleableModel { + patchObjectType = 'component'; /** * @private * @ts-ignore */ diff --git a/packages/core/src/domain_abstract/model/ModelWithPatches.ts b/packages/core/src/domain_abstract/model/ModelWithPatches.ts index 1677d29fd..710d3fd48 100644 --- a/packages/core/src/domain_abstract/model/ModelWithPatches.ts +++ b/packages/core/src/domain_abstract/model/ModelWithPatches.ts @@ -1,9 +1,9 @@ // src/domain_abstract/model/ModelWithPatches.ts -import Backbone, { ObjectHash } from 'backbone'; +import { Model, ObjectHash } from '../../common'; import type { JsonPatch } from '../../utils/jsonDiff'; import { diffObjects } from '../../utils/jsonDiff'; -export default class ModelWithPatches extends Backbone.Model { +export default class ModelWithPatches extends Model { patchObjectType = ''; // наприклад 'component' set(key: any, val?: any, opts?: any) { diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index 8e3bca8b8..fb3096ef3 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -1,5 +1,6 @@ import { isArray, isObject, isString, keys } from 'underscore'; -import { Model, ObjectAny, ObjectHash, SetOptions } from '../../common'; +import ModelWithPatches from './ModelWithPatches'; +import { ObjectAny, ObjectHash, SetOptions } from '../../common'; import ParserHtml from '../../parser/model/ParserHtml'; import Selectors from '../../selector_manager/model/Selectors'; import { shallowDiff } from '../../utils/mixins'; @@ -44,7 +45,7 @@ type WithDataResolvers = { [P in keyof T]?: T[P] | DataResolverProps; }; -export default class StyleableModel extends Model { +export default class StyleableModel extends ModelWithPatches { em?: EditorModel; views: StyleableView[] = []; dataResolverWatchers: ModelDataResolverWatchers; diff --git a/packages/core/src/editor/config/config.ts b/packages/core/src/editor/config/config.ts index 6a3f81cdc..b70c3f41c 100644 --- a/packages/core/src/editor/config/config.ts +++ b/packages/core/src/editor/config/config.ts @@ -24,6 +24,7 @@ import { HTMLGeneratorBuildOptions } from '../../code_manager/model/HtmlGenerato import { CssGeneratorBuildOptions } from '../../code_manager/model/CssGenerator'; import { ObjectAny } from '../../common'; import { ColorPickerOptions } from '../../utils/ColorPicker'; +import type { PatchManagerConfig } from '../../patch_manager/types'; export interface EditorConfig { /** @@ -306,6 +307,11 @@ export interface EditorConfig { */ undoManager?: UndoManagerConfig | boolean; + /** + * Configurations for Patch Manager. + */ + patches?: PatchManagerConfig | boolean; + /** * Configurations for Asset Manager. */ diff --git a/packages/core/src/editor/index.ts b/packages/core/src/editor/index.ts index 24fb7a9b3..9a06c66e6 100644 --- a/packages/core/src/editor/index.ts +++ b/packages/core/src/editor/index.ts @@ -75,6 +75,7 @@ import StorageManager, { ProjectData, StorageOptions } from '../storage_manager' import StyleManager from '../style_manager'; import TraitManager from '../trait_manager'; import UndoManagerModule from '../undo_manager'; +import PatchManager from '../patch_manager'; import UtilsModule from '../utils'; import html from '../utils/html'; import defConfig, { EditorConfig, EditorConfigKeys } from './config/config'; @@ -152,6 +153,10 @@ export default class Editor implements IBaseModule { get UndoManager(): UndoManagerModule { return this.em.UndoManager; } + + get Patches(): PatchManager { + return this.em.Patches; + } get RichTextEditor(): RichTextEditorModule { return this.em.RichTextEditor; } diff --git a/packages/core/src/editor/model/Editor.ts b/packages/core/src/editor/model/Editor.ts index 7a7dfcd51..883fcc463 100644 --- a/packages/core/src/editor/model/Editor.ts +++ b/packages/core/src/editor/model/Editor.ts @@ -29,6 +29,7 @@ import KeymapsModule from '../../keymaps'; import ModalModule from '../../modal_dialog'; import PanelManager from '../../panels'; import CodeManagerModule from '../../code_manager'; +import PatchManager from '../../patch_manager'; import UndoManagerModule from '../../undo_manager'; import RichTextEditorModule from '../../rich_text_editor'; import CommandsModule from '../../commands'; @@ -54,6 +55,7 @@ const deps: (new (em: EditorModel) => IModule)[] = [ I18nModule, KeymapsModule, UndoManagerModule, + PatchManager, StorageManager, DeviceManager, ParserModule, @@ -178,6 +180,10 @@ export default class EditorModel extends Model { return this.get('UndoManager'); } + get Patches(): PatchManager { + return this.get('Patches'); + } + get RichTextEditor(): RichTextEditorModule { return this.get('RichTextEditor'); } diff --git a/packages/core/src/editor/types.ts b/packages/core/src/editor/types.ts index be265e786..e197f43ef 100644 --- a/packages/core/src/editor/types.ts +++ b/packages/core/src/editor/types.ts @@ -13,8 +13,17 @@ import { SelectorEvent } from '../selector_manager'; import { StyleManagerEvent } from '../style_manager'; import { EditorConfig } from './config/config'; import EditorModel from './model/Editor'; - -type GeneralEvent = 'canvasScroll' | 'undo' | 'redo' | 'load' | 'update'; +import type { PatchProps } from '../patch_manager/types'; + +type GeneralEvent = + | 'canvasScroll' + | 'undo' + | 'redo' + | 'load' + | 'update' + | 'patch:update' + | 'patch:undo' + | 'patch:redo'; type EditorBuiltInEvents = | DataSourceEvent @@ -38,6 +47,9 @@ export type EditorConfigType = EditorConfig & { pStylePrefix?: string }; export type EditorModelParam = Parameters[N]; export interface EditorEventCallbacks extends BlocksEventCallback, DataSourcesEventCallback { + 'patch:update': [{ patch: PatchProps }]; + 'patch:undo': [{ patch: PatchProps }]; + 'patch:redo': [{ patch: PatchProps }]; [key: string]: any[]; } @@ -54,6 +66,27 @@ export enum EditorEvents { */ update = 'update', + /** + * @event `patch:update` Event triggered when the patch manager produces a new JSON patch with the recorded changes. + * @example + * editor.on('patch:update', ({ patch }) => { ... }); + */ + patchUpdate = 'patch:update', + + /** + * @event `patch:undo` Patch manager undo executed. + * @example + * editor.on('patch:undo', ({ patch }) => { ... }); + */ + patchUndo = 'patch:undo', + + /** + * @event `patch:redo` Patch manager redo executed. + * @example + * editor.on('patch:redo', ({ patch }) => { ... }); + */ + patchRedo = 'patch:redo', + /** * @event `undo` Undo executed. * @example diff --git a/packages/core/src/patch_manager/index.ts b/packages/core/src/patch_manager/index.ts index 25195a08c..f15bf74c4 100644 --- a/packages/core/src/patch_manager/index.ts +++ b/packages/core/src/patch_manager/index.ts @@ -1,6 +1,6 @@ // src/patch_manager/PatchManager.ts import { genId } from '../utils/id'; -import type { PatchProps, JsonPatch } from './types'; +import type { PatchManagerConfig, PatchProps, JsonPatch } from './types'; import { ItemManagerModule } from '../abstract/Module'; import { Collection } from '../common'; import type EditorModel from '../editor/model/Editor'; @@ -25,10 +25,10 @@ export default class PatchManager extends ItemManagerModule { onInit(): void { const cfg = (this.getConfig() as any) ?? {}; const normalized = typeof cfg === 'boolean' ? { enable: cfg } : cfg; - this.init(normalized); + this.init({ enable: true, ...normalized }); } - init(cfg: { enable?: boolean; maxHistory?: number; coalesceMs?: number; debug?: boolean } = {}) { + init(cfg: PatchManagerConfig = {}) { this.isEnabled = !!cfg.enable; this.maxHistory = cfg.maxHistory ?? this.maxHistory; this.coalesceMs = cfg.coalesceMs ?? 0; @@ -63,10 +63,7 @@ export default class PatchManager extends ItemManagerModule { this.em.trigger('patch:update', { patch }); if (this.debug) { - try { - // eslint-disable-next-line no-console - console.log('[Patches] update', patch); - } catch {} + this.logWithEditor('update', patch); } } @@ -111,10 +108,7 @@ export default class PatchManager extends ItemManagerModule { this.applyJsonPatchList(patch.changes); this.em.trigger('patch:applied:external', { patch }); if (this.debug) { - try { - // eslint-disable-next-line no-console - console.log('[Patches] applied external', patch); - } catch {} + this.logWithEditor('applied external', patch); } } finally { this.isApplyingExternal = false; @@ -124,7 +118,12 @@ export default class PatchManager extends ItemManagerModule { undo() { if (!this.isEnabled || this.index < 0) return; const patch = this.history[this.index]; - this.applyJsonPatchList(patch.reverseChanges); + this.isApplyingExternal = true; + try { + this.applyJsonPatchList(patch.reverseChanges); + } finally { + this.isApplyingExternal = false; + } this.index--; this.em.trigger('patch:undo', { patch }); } @@ -132,7 +131,12 @@ export default class PatchManager extends ItemManagerModule { redo() { if (!this.isEnabled || this.index >= this.history.length - 1) return; const patch = this.history[this.index + 1]; - this.applyJsonPatchList(patch.changes); + this.isApplyingExternal = true; + try { + this.applyJsonPatchList(patch.changes); + } finally { + this.isApplyingExternal = false; + } this.index++; this.em.trigger('patch:redo', { patch }); } @@ -232,5 +236,13 @@ export default class PatchManager extends ItemManagerModule { this.isApplyingExternal = false; super.__destroy?.(); } + private logWithEditor(eventName: string, patch: PatchProps) { + try { + this.em.log(`[Patches] ${eventName}`, { + ns: 'patches', + level: 'debug', + patch, + }); + } catch {} + } } - diff --git a/packages/core/src/patch_manager/types.ts b/packages/core/src/patch_manager/types.ts index e4383687c..39bb55e2a 100644 --- a/packages/core/src/patch_manager/types.ts +++ b/packages/core/src/patch_manager/types.ts @@ -15,3 +15,10 @@ export interface PatchProps { reverseChanges: JsonPatch[]; // инверсия для undo meta?: Record; // user, txnId, etc. } + +export interface PatchManagerConfig { + enable?: boolean; + maxHistory?: number; + coalesceMs?: number; + debug?: boolean; +} diff --git a/packages/core/test/specs/patch_manager/index.ts b/packages/core/test/specs/patch_manager/index.ts new file mode 100644 index 000000000..f112f4d1f --- /dev/null +++ b/packages/core/test/specs/patch_manager/index.ts @@ -0,0 +1,82 @@ +import Editor from '../../../src/editor'; +import type { PatchProps } from '../../../src/patch_manager/types'; + +describe('PatchManager', () => { + let editor: Editor; + + beforeEach(async () => { + editor = new Editor(); + const ready = new Promise((resolve) => + editor.getModel().once('change:readyLoad', () => resolve()), + ); + editor.getModel().initModules(); + editor.getModel().loadOnStart(); + await ready; + const pm = editor.Patches as any; + pm.history = []; + pm.index = -1; + pm.active = null; + }); + + afterEach(() => { + editor.destroy(); + }); + + test('records JSON patches for component updates', () => { + const patches: PatchProps[] = []; + editor.on('patch:update', ({ patch }) => { + patches.push(patch); + }); + const wrapper = editor.getWrapper()!; + const id = wrapper.getId(); + wrapper.set('tagName', 'section'); + expect(patches).toHaveLength(1); + expect(patches[0].changes[0].path).toBe(`/component/${id}/tagName`); + }); + + test('update() batches multiple mutations into a single patch', () => { + const updates: PatchProps[] = []; + editor.on('patch:update', ({ patch }) => updates.push(patch)); + const wrapper = editor.getWrapper()!; + editor.Patches.update(() => { + wrapper.set('tagName', 'section'); + wrapper.set('attributes', { 'data-test': 'updated' }); + }); + expect(updates).toHaveLength(1); + expect(updates[0].changes.length).toBeGreaterThanOrEqual(2); + }); + + test('apply() restores recorded patch data', () => { + let recorded: PatchProps | undefined; + editor.once('patch:update', ({ patch }) => { + recorded = patch; + }); + const wrapper = editor.getWrapper()!; + wrapper.set('tagName', 'section'); + expect(recorded).toBeDefined(); + wrapper.set('tagName', 'span'); + editor.Patches.apply(recorded!); + expect(wrapper.get('tagName')).toBe('section'); + }); + + test('undo/redo emit patch events and rollback state', () => { + const undo: PatchProps[] = []; + const redo: PatchProps[] = []; + editor.on('patch:undo', ({ patch }) => { + undo.push(patch); + }); + editor.on('patch:redo', ({ patch }) => { + redo.push(patch); + }); + const wrapper = editor.getWrapper()!; + const original = wrapper.get('tagName'); + wrapper.set('tagName', 'section'); + expect(wrapper.get('tagName')).toBe('section'); + editor.Patches.undo(); + expect(wrapper.get('tagName')).toBe(original); + expect(undo).toHaveLength(1); + editor.Patches.redo(); + expect(wrapper.get('tagName')).toBe('section'); + expect(redo).toHaveLength(1); + }); +}); diff --git a/patch-test.html b/patch-test.html index eab3c19af..32f6f5c4a 100644 --- a/patch-test.html +++ b/patch-test.html @@ -24,7 +24,7 @@ container: '#gjs', height: '100%', headless: true, - patches: { enable: true, debug: true }, + patches: { debug: true }, projectData: { pages: [ {