From eb55b3900b9dc9d18defe2dcd7059578e279e1a7 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 12 Jun 2025 11:31:28 +0400 Subject: [PATCH] Add css mount events (#6540) * Add component:input to ComponentsEvents * Add additional `data:path` events * Add CssRule mount event * Switch to a new pathSource event. The `data:path:SOURCE_ID` doesn't work as it triggers rebuilds of symbols in ComponentDataCollection * Up comment * Up data_sources docs * Up data_sources jsdoc * Up Module.events TS * Export types data components * More TS export * Up TS * Up docs API --- docs/api/datasources.md | 64 +++++++++++++------ packages/core/src/abstract/Module.ts | 7 +- packages/core/src/canvas/model/CanvasSpots.ts | 2 +- .../src/code_manager/model/CssGenerator.ts | 2 +- .../core/src/commands/view/SelectComponent.ts | 2 +- packages/core/src/css_composer/index.ts | 11 +++- .../core/src/css_composer/model/CssRule.ts | 15 +++-- packages/core/src/css_composer/types.ts | 9 +++ .../core/src/css_composer/view/CssRuleView.ts | 14 +++- packages/core/src/data_sources/index.ts | 24 ++----- .../core/src/data_sources/model/DataRecord.ts | 10 +-- .../operators/BooleanOperator.ts | 2 - .../operators/NumberOperator.ts | 1 - packages/core/src/data_sources/types.ts | 56 ++++++++++------ packages/core/src/dom_components/types.ts | 14 ++++ .../dom_components/view/ComponentTextView.ts | 9 ++- .../src/dom_components/view/ComponentsView.ts | 3 +- .../domain_abstract/model/StyleableModel.ts | 5 +- packages/core/src/index.ts | 10 +++ .../core/test/specs/css_composer/index.ts | 2 +- 20 files changed, 172 insertions(+), 90 deletions(-) create mode 100644 packages/core/src/css_composer/types.ts diff --git a/docs/api/datasources.md b/docs/api/datasources.md index b044ad288..cfe8080c6 100644 --- a/docs/api/datasources.md +++ b/docs/api/datasources.md @@ -3,40 +3,62 @@ ## DataSources This module manages data sources within the editor. -You can initialize the module with the editor by passing an instance of `EditorModel`. +Once the editor is instantiated, you can use the following API to manage data sources: ```js -const editor = new EditorModel(); -const dsm = new DataSourceManager(editor); +const editor = grapesjs.init({ ... }); +const dsm = editor.DataSources; ``` -Once the editor is instantiated, you can use the following API to manage data sources: +## Available Events +* `data:add` Added new data source. -```js -const dsm = editor.DataSources; +```javascript +editor.on('data:add', (dataSource) => { ... }); ``` -* [add][1] - Add a new data source. -* [get][2] - Retrieve a data source by its ID. -* [getAll][3] - Retrieve all data sources. -* [remove][4] - Remove a data source by its ID. -* [clear][5] - Remove all data sources. +* `data:remove` Data source removed. -Example of adding a data source: +```javascript +editor.on('data:remove', (dataSource) => { ... }); +``` -```js -const ds = dsm.add({ - id: 'my_data_source_id', - records: [ - { id: 'id1', name: 'value1' }, - { id: 'id2', name: 'value2' } - ] +* `data:update` Data source updated. + +```javascript +editor.on('data:update', (dataSource, changes) => { ... }); +``` + +* `data:path` Data record path update. + +```javascript +editor.on('data:path:SOURCE_ID.RECORD_ID.PROP_NAME', ({ dataSource, dataRecord, path }) => { ... }); +editor.on('data:path', ({ dataSource, dataRecord, path }) => { + console.log('Path update in any data source') }); ``` -### Parameters +* `data:pathSource` Data record path update per source. + +```javascript +editor.on('data:pathSource:SOURCE_ID', ({ dataSource, dataRecord, path }) => { ... }); +``` + +* `data` Catch-all event for all the events mentioned above. + +```javascript +editor.on('data', ({ event, model, ... }) => { ... }); +``` + +## Methods + +* [add][1] - Add a new data source. +* [get][2] - Retrieve a data source by its ID. +* [getAll][3] - Retrieve all data sources. +* [remove][4] - Remove a data source by its ID. +* [clear][5] - Remove all data sources. -* `em` **EditorModel** Editor model. +[DataSource]: datasource.html ## add diff --git a/packages/core/src/abstract/Module.ts b/packages/core/src/abstract/Module.ts index 6533ebea1..8889ea433 100644 --- a/packages/core/src/abstract/Module.ts +++ b/packages/core/src/abstract/Module.ts @@ -44,7 +44,7 @@ export default abstract class Module impl collections: Collection[] = []; cls: any[] = []; state?: Model; - events: any; + events: object = {}; model?: any; view?: any; @@ -129,18 +129,19 @@ export abstract class ItemManagerModule< cls: any[] = []; all: TCollection; view?: View; + events!: Record; constructor( em: EditorModel, moduleName: string, all: any, - events?: any, + events?: Record, defaults?: TConf, opts: { skipListen?: boolean } = {}, ) { super(em, moduleName, defaults); this.all = all; - this.events = events; + if (events) this.events = events; !opts.skipListen && this.__initListen(); } diff --git a/packages/core/src/canvas/model/CanvasSpots.ts b/packages/core/src/canvas/model/CanvasSpots.ts index 165657988..e175570c0 100644 --- a/packages/core/src/canvas/model/CanvasSpots.ts +++ b/packages/core/src/canvas/model/CanvasSpots.ts @@ -17,7 +17,7 @@ export default class CanvasSpots extends ModuleCollection { const { em } = this; this.refreshDbn = debounce(() => this.refresh(), 0); - const evToRefreshDbn = `${ComponentsEvents.resize} styleable:change component:input ${ComponentsEvents.update} frame:updated undo redo`; + const evToRefreshDbn = `${ComponentsEvents.resize} styleable:change ${ComponentsEvents.input} ${ComponentsEvents.update} frame:updated undo redo`; this.listenTo(em, evToRefreshDbn, () => this.refreshDbn()); } diff --git a/packages/core/src/code_manager/model/CssGenerator.ts b/packages/core/src/code_manager/model/CssGenerator.ts index 3bcfbc401..ee98afc47 100644 --- a/packages/core/src/code_manager/model/CssGenerator.ts +++ b/packages/core/src/code_manager/model/CssGenerator.ts @@ -172,7 +172,7 @@ export default class CssGenerator extends Model { }); if ((selectorStrNoAdd && found) || selectorsAdd || singleAtRule || !model) { - const block = rule.getDeclaration({ body: 1 }); + const block = rule.getDeclaration(); block && (opts.json ? (result = rule) : (result += block)); } else { dump.push(rule); diff --git a/packages/core/src/commands/view/SelectComponent.ts b/packages/core/src/commands/view/SelectComponent.ts index b97e645ec..8d79ddc7c 100644 --- a/packages/core/src/commands/view/SelectComponent.ts +++ b/packages/core/src/commands/view/SelectComponent.ts @@ -96,7 +96,7 @@ export default { methods[method](listenToEl, 'scroll', this.onContainerChange); em[method](`component:toggled ${eventCmpUpdate} undo redo`, this.onSelect, this); em[method]('change:componentHovered', this.onHovered, this); - em[method](`${ComponentsEvents.resize} styleable:change component:input`, this.updateGlobalPos, this); + em[method](`${ComponentsEvents.resize} styleable:change ${ComponentsEvents.input}`, this.updateGlobalPos, this); em[method](`${eventCmpUpdate}:toolbar`, this._upToolbar, this); em[method]('frame:updated', this.onFrameUpdated, this); em[method]('canvas:updateTools', this.onFrameUpdated, this); diff --git a/packages/core/src/css_composer/index.ts b/packages/core/src/css_composer/index.ts index 562b77bff..47f9c1719 100644 --- a/packages/core/src/css_composer/index.ts +++ b/packages/core/src/css_composer/index.ts @@ -40,6 +40,8 @@ import EditorModel from '../editor/model/Editor'; import Component from '../dom_components/model/Component'; import { ObjectAny, PrevToNewIdMap } from '../common'; import { UpdateStyleOptions } from '../domain_abstract/model/StyleableModel'; +import { CssEvents } from './types'; +import CssRuleView from './view/CssRuleView'; /** @private */ interface RuleOptions { @@ -72,8 +74,15 @@ export interface GetSetRuleOptions extends UpdateStyleOptions { type CssRuleStyle = Required['style']; export default class CssComposer extends ItemManagerModule { + classes = { + CssRule, + CssRules, + CssRuleView, + CssRulesView, + }; rules: CssRules; rulesView?: CssRulesView; + events = CssEvents; Selectors = Selectors; @@ -85,7 +94,7 @@ export default class CssComposer extends ItemManagerModule { * }); * cssRule.getDeclaration() // ".class1{color:red;}" */ - getDeclaration(opts: ObjectAny = {}) { + getDeclaration(opts: ToCssOptions = {}) { let result = ''; const { important } = this.attributes; const selectors = this.selectorsToString(opts); @@ -285,7 +292,7 @@ export default class CssRule extends StyleableModel { * }); * cssRule.toCSS() // "@media (min-width: 500px){.class1{color:red;}}" */ - toCSS(opts: ObjectAny = {}) { + toCSS(opts: ToCssOptions = {}) { let result = ''; const atRule = this.getAtRule(); const block = this.getDeclaration(opts); diff --git a/packages/core/src/css_composer/types.ts b/packages/core/src/css_composer/types.ts new file mode 100644 index 000000000..708950c53 --- /dev/null +++ b/packages/core/src/css_composer/types.ts @@ -0,0 +1,9 @@ +export enum CssEvents { + /** + * @event `css:mount` CSS rule is mounted in the canvas. + * @example + * editor.on('css:mount', ({ rule }) => { ... }); + */ + mount = 'css:mount', + mountBefore = 'css:mount:before', +} diff --git a/packages/core/src/css_composer/view/CssRuleView.ts b/packages/core/src/css_composer/view/CssRuleView.ts index 16dd8e6c9..903127437 100644 --- a/packages/core/src/css_composer/view/CssRuleView.ts +++ b/packages/core/src/css_composer/view/CssRuleView.ts @@ -1,6 +1,8 @@ import FrameView from '../../canvas/view/FrameView'; import { View } from '../../common'; +import EditorModel from '../../editor/model/Editor'; import CssRule from '../model/CssRule'; +import { CssEvents } from '../types'; export default class CssRuleView extends View { config: any; @@ -19,6 +21,10 @@ export default class CssRuleView extends View { return this.config.frameView; } + get em(): EditorModel { + return this.model.em!; + } + remove() { super.remove(); this.model.removeView(this); @@ -35,9 +41,13 @@ export default class CssRuleView extends View { } render() { - const { model, el } = this; + const { model, el, em } = this; const important = model.get('important'); - el.innerHTML = model.toCSS({ important }); + const css = model.toCSS({ important }); + const mountProps = { rule: model, ruleView: this, css }; + em?.trigger(CssEvents.mountBefore, mountProps); + el.innerHTML = mountProps.css; + em?.trigger(CssEvents.mount, mountProps); return this; } } diff --git a/packages/core/src/data_sources/index.ts b/packages/core/src/data_sources/index.ts index 11b5de277..63837cdff 100644 --- a/packages/core/src/data_sources/index.ts +++ b/packages/core/src/data_sources/index.ts @@ -1,38 +1,24 @@ /** * This module manages data sources within the editor. - * You can initialize the module with the editor by passing an instance of `EditorModel`. - * - * ```js - * const editor = new EditorModel(); - * const dsm = new DataSourceManager(editor); - * ``` - * * Once the editor is instantiated, you can use the following API to manage data sources: * * ```js + * const editor = grapesjs.init({ ... }); * const dsm = editor.DataSources; * ``` * + * {REPLACE_EVENTS} + * + * ## Methods * * [add](#add) - Add a new data source. * * [get](#get) - Retrieve a data source by its ID. * * [getAll](#getall) - Retrieve all data sources. * * [remove](#remove) - Remove a data source by its ID. * * [clear](#clear) - Remove all data sources. * - * Example of adding a data source: - * - * ```js - * const ds = dsm.add({ - * id: 'my_data_source_id', - * records: [ - * { id: 'id1', name: 'value1' }, - * { id: 'id2', name: 'value2' } - * ] - * }); - * ``` + * [DataSource]: datasource.html * * @module DataSources - * @param {EditorModel} em - Editor model. */ import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; diff --git a/packages/core/src/data_sources/model/DataRecord.ts b/packages/core/src/data_sources/model/DataRecord.ts index 3c8c5fe93..d0ce687e3 100644 --- a/packages/core/src/data_sources/model/DataRecord.ts +++ b/packages/core/src/data_sources/model/DataRecord.ts @@ -63,9 +63,9 @@ export default class DataRecord ext * @private * @name handleChange */ - handleChange() { + handleChange(m: DataRecord, opts: SetOptions) { const changed = this.changedAttributes(); - keys(changed).forEach((prop) => this.triggerChange(prop)); + keys(changed).forEach((prop) => this.triggerChange(prop, opts)); } /** @@ -113,10 +113,12 @@ export default class DataRecord ext * @param {String} [prop] - Optional property name to trigger a change event for a specific property. * @name triggerChange */ - triggerChange(prop?: string) { + triggerChange(prop?: string, options: SetOptions = {}) { const { dataSource, em } = this; - const data = { dataSource, dataRecord: this }; const paths = this.getPaths(prop); + const data = { dataSource, dataRecord: this, path: paths[0], options }; + em.trigger(DataSourcesEvents.path, data); + em.trigger(`${DataSourcesEvents.pathSource}:${dataSource.id}`, data); paths.forEach((path) => em.trigger(`${DataSourcesEvents.path}:${path}`, { ...data, path })); } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts index 413495694..638fa484f 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts @@ -1,5 +1,3 @@ -import { enumToArray } from '../../../utils'; -import { DataConditionSimpleOperation } from '../types'; import { SimpleOperator } from './BaseOperator'; export enum BooleanOperation { diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts index 04f4da498..4aa9838d4 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts @@ -1,4 +1,3 @@ -import { enumToArray } from '../../../utils'; import { SimpleOperator } from './BaseOperator'; export enum NumberOperation { diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index ad88c6409..4e7098d94 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -58,6 +58,28 @@ export interface DataSourceTransformers { onRecordSetValue?: (args: { id: string | number; key: string; value: any }) => any; } +type DotSeparatedKeys = T extends object + ? { + [K in keyof T]: K extends string + ? T[K] extends object + ? `${K}` | `${K}.${DotSeparatedKeys}` + : `${K}` + : never; + }[keyof T] + : never; + +export type DeepPartialDot = { + [P in DotSeparatedKeys]?: P extends `${infer K}.${infer Rest}` + ? K extends keyof T + ? Rest extends DotSeparatedKeys + ? DeepPartialDot[Rest] + : never + : never + : P extends keyof T + ? T[P] + : never; +}; + /**{START_EVENTS}*/ export enum DataSourcesEvents { /** @@ -86,10 +108,20 @@ export enum DataSourcesEvents { /** * @event `data:path` Data record path update. * @example - * editor.on('data:path:SOURCE_ID:RECORD_ID:PROP_NAME', ({ dataSource, dataRecord, path }) => { ... }); + * editor.on('data:path:SOURCE_ID.RECORD_ID.PROP_NAME', ({ dataSource, dataRecord, path }) => { ... }); + * editor.on('data:path', ({ dataSource, dataRecord, path }) => { + * console.log('Path update in any data source') + * }); */ path = 'data:path', + /** + * @event `data:pathSource` Data record path update per source. + * @example + * editor.on('data:pathSource:SOURCE_ID', ({ dataSource, dataRecord, path }) => { ... }); + */ + pathSource = 'data:pathSource:', + /** * @event `data` Catch-all event for all the events mentioned above. * @example @@ -98,24 +130,6 @@ export enum DataSourcesEvents { all = 'data', } /**{END_EVENTS}*/ -type DotSeparatedKeys = T extends object - ? { - [K in keyof T]: K extends string - ? T[K] extends object - ? `${K}` | `${K}.${DotSeparatedKeys}` - : `${K}` - : never; - }[keyof T] - : never; -export type DeepPartialDot = { - [P in DotSeparatedKeys]?: P extends `${infer K}.${infer Rest}` - ? K extends keyof T - ? Rest extends DotSeparatedKeys - ? DeepPartialDot[Rest] - : never - : never - : P extends keyof T - ? T[P] - : never; -}; +// need this to avoid the TS documentation generator to break +export default DataSourcesEvents; diff --git a/packages/core/src/dom_components/types.ts b/packages/core/src/dom_components/types.ts index 9e2bc1293..dc367099a 100644 --- a/packages/core/src/dom_components/types.ts +++ b/packages/core/src/dom_components/types.ts @@ -69,6 +69,13 @@ export enum ComponentsEvents { select = 'component:select', selectBefore = 'component:select:before', + /** + * @event `component:mount` Component is mounted in the canvas. + * @example + * editor.on('component:mount', (component) => { ... }); + */ + mount = 'component:mount', + /** * @event `component:script:mount` Component with script is mounted. * @example @@ -91,6 +98,13 @@ export enum ComponentsEvents { */ render = 'component:render', + /** + * @event `component:input` Event triggered on `input` DOM event. This is useful to catch direct input changes in the component (eg. Text component). + * @example + * editor.on('component:input', (component) => { ... }); + */ + input = 'component:input', + /** * @event `component:resize` Component resized. This event is triggered when the component is resized in the canvas. * @example diff --git a/packages/core/src/dom_components/view/ComponentTextView.ts b/packages/core/src/dom_components/view/ComponentTextView.ts index f1f1e5442..a5c301f0d 100644 --- a/packages/core/src/dom_components/view/ComponentTextView.ts +++ b/packages/core/src/dom_components/view/ComponentTextView.ts @@ -9,6 +9,7 @@ import { getComponentIds } from '../model/Components'; import ComponentText from '../model/ComponentText'; import { ComponentDefinition } from '../model/types'; import ComponentView from './ComponentView'; +import { ComponentsEvents } from '../types'; export default class ComponentTextView extends ComponentView { rte?: RichTextEditorModule; @@ -217,11 +218,9 @@ export default class ComponentTextView model.emitWithEditor(ev, model)); } /** diff --git a/packages/core/src/dom_components/view/ComponentsView.ts b/packages/core/src/dom_components/view/ComponentsView.ts index 7ae09e0b0..7daf853b3 100644 --- a/packages/core/src/dom_components/view/ComponentsView.ts +++ b/packages/core/src/dom_components/view/ComponentsView.ts @@ -8,6 +8,7 @@ import ComponentView from './ComponentView'; import FrameView from '../../canvas/view/FrameView'; import Components from '../model/Components'; import { ResetComponentsOptions } from '../model/types'; +import { ComponentsEvents } from '../types'; export default class ComponentsView extends View { opts!: any; @@ -119,7 +120,7 @@ export default class ComponentsView extends View { } if (!model.opt.temporary) { - em?.trigger('component:mount', model); + em?.trigger(ComponentsEvents.mount, model); } return rendered; diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index fcf5fa952..f50769902 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -17,6 +17,7 @@ import { isDataResolverProps, } from '../../data_sources/utils'; import { DataResolver } from '../../data_sources/types'; +import { ToCssOptions } from '../../css_composer/model/CssRule'; export type StyleProps = Record; @@ -241,9 +242,9 @@ export default class StyleableModel extends Model * @param {Object} [opts={}] Options * @return {String} */ - styleToString(opts: ObjectAny = {}) { + styleToString(opts: ToCssOptions = {}) { const result: string[] = []; - const style = this.getStyle(opts); + const style = opts.style || this.getStyle(opts); const imp = opts.important; for (let prop in style) { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fc7dd4439..7a5941c07 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -147,5 +147,15 @@ export type { default as DataSources } from './data_sources/model/DataSources'; export type { default as DataSource } from './data_sources/model/DataSource'; export type { default as DataRecord } from './data_sources/model/DataRecord'; export type { default as DataRecords } from './data_sources/model/DataRecords'; +export type { default as DataVariable } from './data_sources/model/DataVariable'; +export type { default as ComponentDataVariable } from './data_sources/model/ComponentDataVariable'; +export type { default as ComponentDataCollection } from './data_sources/model/data_collection/ComponentDataCollection'; +export type { default as ComponentDataCondition } from './data_sources/model/conditional_variables/ComponentDataCondition'; +export type { + DataCondition, + LogicGroupProps, + DataConditionProps, + ExpressionProps, +} from './data_sources/model/conditional_variables/DataCondition'; export default grapesjs; diff --git a/packages/core/test/specs/css_composer/index.ts b/packages/core/test/specs/css_composer/index.ts index 1714a71d9..2728a5e61 100644 --- a/packages/core/test/specs/css_composer/index.ts +++ b/packages/core/test/specs/css_composer/index.ts @@ -162,7 +162,7 @@ describe('Css Composer', () => { const rule = obj.getIdRule(name)!; expect(rule.selectorsToString()).toEqual(`#${name}`); expect(rule.styleToString()).toEqual('color:red;'); - expect(rule.styleToString({ important: 1 })).toEqual('color:red !important;'); + expect(rule.styleToString({ important: true })).toEqual('color:red !important;'); expect(rule.styleToString({ important: ['color'] })).toEqual('color:red !important;'); });