Browse Source

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
release-v0.22.8
Artur Arseniev 1 year ago
committed by GitHub
parent
commit
eb55b3900b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 64
      docs/api/datasources.md
  2. 7
      packages/core/src/abstract/Module.ts
  3. 2
      packages/core/src/canvas/model/CanvasSpots.ts
  4. 2
      packages/core/src/code_manager/model/CssGenerator.ts
  5. 2
      packages/core/src/commands/view/SelectComponent.ts
  6. 11
      packages/core/src/css_composer/index.ts
  7. 15
      packages/core/src/css_composer/model/CssRule.ts
  8. 9
      packages/core/src/css_composer/types.ts
  9. 14
      packages/core/src/css_composer/view/CssRuleView.ts
  10. 24
      packages/core/src/data_sources/index.ts
  11. 10
      packages/core/src/data_sources/model/DataRecord.ts
  12. 2
      packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts
  13. 1
      packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts
  14. 56
      packages/core/src/data_sources/types.ts
  15. 14
      packages/core/src/dom_components/types.ts
  16. 9
      packages/core/src/dom_components/view/ComponentTextView.ts
  17. 3
      packages/core/src/dom_components/view/ComponentsView.ts
  18. 5
      packages/core/src/domain_abstract/model/StyleableModel.ts
  19. 10
      packages/core/src/index.ts
  20. 2
      packages/core/test/specs/css_composer/index.ts

64
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

7
packages/core/src/abstract/Module.ts

@ -44,7 +44,7 @@ export default abstract class Module<T extends ModuleConfig = ModuleConfig> 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<string, string>;
constructor(
em: EditorModel,
moduleName: string,
all: any,
events?: any,
events?: Record<string, string>,
defaults?: TConf,
opts: { skipListen?: boolean } = {},
) {
super(em, moduleName, defaults);
this.all = all;
this.events = events;
if (events) this.events = events;
!opts.skipListen && this.__initListen();
}

2
packages/core/src/canvas/model/CanvasSpots.ts

@ -17,7 +17,7 @@ export default class CanvasSpots extends ModuleCollection<CanvasSpot> {
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());
}

2
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);

2
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);

11
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<CssRuleProperties>['style'];
export default class CssComposer extends ItemManagerModule<CssComposerConfig & { pStylePrefix?: string }> {
classes = {
CssRule,
CssRules,
CssRuleView,
CssRulesView,
};
rules: CssRules;
rulesView?: CssRulesView;
events = CssEvents;
Selectors = Selectors;
@ -85,7 +94,7 @@ export default class CssComposer extends ItemManagerModule<CssComposerConfig & {
* @private
*/
constructor(em: EditorModel) {
super(em, 'CssComposer', null, {}, defConfig());
super(em, 'CssComposer', null, CssEvents, defConfig());
const { config } = this;
const ppfx = config.pStylePrefix;

15
packages/core/src/css_composer/model/CssRule.ts

@ -1,6 +1,6 @@
import { isEmpty, forEach, isString, isArray } from 'underscore';
import { Model, ObjectAny, View } from '../../common';
import StyleableModel from '../../domain_abstract/model/StyleableModel';
import { Model, ObjectAny } from '../../common';
import StyleableModel, { StyleProps } from '../../domain_abstract/model/StyleableModel';
import Selectors from '../../selector_manager/model/Selectors';
import { getMediaLength } from '../../code_manager/model/CssGenerator';
import { isEmptyObj, hasWin } from '../../utils/mixins';
@ -8,6 +8,13 @@ import Selector, { SelectorProps } from '../../selector_manager/model/Selector';
import EditorModel from '../../editor/model/Editor';
import CssRuleView from '../view/CssRuleView';
export interface ToCssOptions {
important?: boolean | string[];
allowEmpty?: boolean;
style?: StyleProps;
inline?: boolean;
}
/** @private */
export interface CssRuleProperties {
/**
@ -214,7 +221,7 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
* });
* 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<CssRuleProperties> {
* });
* 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);

9
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',
}

14
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<CssRule> {
config: any;
@ -19,6 +21,10 @@ export default class CssRuleView extends View<CssRule> {
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<CssRule> {
}
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;
}
}

24
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';

10
packages/core/src/data_sources/model/DataRecord.ts

@ -63,9 +63,9 @@ export default class DataRecord<T extends DataRecordProps = DataRecordProps> 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<T extends DataRecordProps = DataRecordProps> 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 }));
}

2
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 {

1
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 {

56
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> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? `${K}` | `${K}.${DotSeparatedKeys<T[K]>}`
: `${K}`
: never;
}[keyof T]
: never;
export type DeepPartialDot<T> = {
[P in DotSeparatedKeys<T>]?: P extends `${infer K}.${infer Rest}`
? K extends keyof T
? Rest extends DotSeparatedKeys<T[K]>
? DeepPartialDot<T[K]>[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> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? `${K}` | `${K}.${DotSeparatedKeys<T[K]>}`
: `${K}`
: never;
}[keyof T]
: never;
export type DeepPartialDot<T> = {
[P in DotSeparatedKeys<T>]?: P extends `${infer K}.${infer Rest}`
? K extends keyof T
? Rest extends DotSeparatedKeys<T[K]>
? DeepPartialDot<T[K]>[Rest]
: never
: never
: P extends keyof T
? T[P]
: never;
};
// need this to avoid the TS documentation generator to break
export default DataSourcesEvents;

14
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

9
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<TComp extends ComponentText = ComponentText> extends ComponentView<TComp> {
rte?: RichTextEditorModule;
@ -217,11 +218,9 @@ export default class ComponentTextView<TComp extends ComponentText = ComponentTe
* @param {Event} e
*/
onInput() {
const evPfx = 'component';
const ev = [`${evPfx}:update`, `${evPfx}:input`].join(' ');
// Update toolbars
this.em?.trigger(ev, this.model);
const { model } = this;
const events = [ComponentsEvents.update, ComponentsEvents.input];
events.forEach((ev) => model.emitWithEditor(ev, model));
}
/**

3
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;

5
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<string, string | string[] | DataVariableProps | DataConditionProps>;
@ -241,9 +242,9 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
* @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) {

10
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;

2
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;');
});

Loading…
Cancel
Save