Browse Source

Data source schema & providers (#6633)

* Setup initial schema types

* Setup initial schema methods

* Add DataSource tests

* Up test

* Add test getValue

* Cleanup

* Up test

* Add setValue method

* Up docs

* Check nested arrays

* Add a test for nested setValue

* Improve setValue for nested values

* Improve setValue for nested values

* Setup relation tests

* Add getResolvedRecords

* Resolve one to many relations in DataSource records

* Add DataSourceSchema

* Up type

* Start data source providers

* Start test provider

* Add tests for loadProvider

* Cleanup

* Skip records if DataSource provider is set

* Load providers on project load

* Type keymap events

* Move modal events

* Move layer events

* Move RTE events

* Move selector events

* Move StyleManager events

* Move editor events

* Start DataSource callbacks

* Add data source callbacks

* Update docs

* Up device_manager jsdoc

* Format
release-v0.22.14-rc.0
Artur Arseniev 3 months ago
committed by GitHub
parent
commit
83bb01b94b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 58
      docs/api.mjs
  2. 2
      docs/api/block_manager.md
  3. 8
      docs/api/canvas.md
  4. 7
      docs/api/component.md
  5. 44
      docs/api/datasource.md
  6. 43
      docs/api/datasources.md
  7. 33
      docs/api/device_manager.md
  8. 12
      docs/api/editor.md
  9. 27
      docs/api/keymaps.md
  10. 20
      docs/api/layer_manager.md
  11. 19
      docs/api/modal_dialog.md
  12. 12
      docs/api/parser.md
  13. 25
      docs/api/rich_text_editor.md
  14. 54
      docs/api/selector_manager.md
  15. 78
      docs/api/style_manager.md
  16. 7
      packages/core/src/block_manager/types.ts
  17. 13
      packages/core/src/data_sources/config/config.ts
  18. 72
      packages/core/src/data_sources/index.ts
  19. 142
      packages/core/src/data_sources/model/DataSource.ts
  20. 137
      packages/core/src/data_sources/types.ts
  21. 2
      packages/core/src/data_sources/utils.ts
  22. 7
      packages/core/src/device_manager/index.ts
  23. 4
      packages/core/src/dom_components/view/ComponentTextView.ts
  24. 73
      packages/core/src/editor/index.ts
  25. 11
      packages/core/src/editor/model/Editor.ts
  26. 47
      packages/core/src/editor/types.ts
  27. 33
      packages/core/src/keymaps/index.ts
  28. 28
      packages/core/src/keymaps/types.ts
  29. 25
      packages/core/src/modal_dialog/index.ts
  30. 27
      packages/core/src/modal_dialog/types.ts
  31. 42
      packages/core/src/navigator/index.ts
  32. 39
      packages/core/src/navigator/types.ts
  33. 48
      packages/core/src/rich_text_editor/index.ts
  34. 39
      packages/core/src/rich_text_editor/types.ts
  35. 64
      packages/core/src/selector_manager/index.ts
  36. 57
      packages/core/src/selector_manager/types.ts
  37. 106
      packages/core/src/style_manager/index.ts
  38. 88
      packages/core/src/style_manager/types.ts
  39. 34
      packages/core/src/utils/mixins.ts
  40. 107
      packages/core/test/specs/data_sources/index.ts
  41. 295
      packages/core/test/specs/data_sources/model/DataSource.ts

58
docs/api.mjs

@ -97,33 +97,43 @@ async function generateDocs() {
throw `File not found '${filePath}'`;
}
return build([filePath], { shallow: true })
.then((cm) => formats.md(cm /*{ markdownToc: true }*/))
.then(async (output) => {
let addLogs = [];
let result = output
.replace(/\*\*\\\[/g, '**[')
.replace(/\*\*\(\\\[/g, '**([')
.replace(/<\\\[/g, '<[')
.replace(/<\(\\\[/g, '<([')
.replace(/\| \\\[/g, '| [')
.replace(/\\n```js/g, '```js')
.replace(/docsjs\./g, '')
.replace('**Extends ModuleModel**', '')
.replace('**Extends Model**', '');
try {
return build([filePath], { shallow: true })
.then((cm) => formats.md(cm /*{ markdownToc: true }*/))
.then(async (output) => {
let addLogs = [];
let result = output
.replace(/\*\*\\\[/g, '**[')
.replace(/\*\*\(\\\[/g, '**([')
.replace(/<\\\[/g, '<[')
.replace(/<\(\\\[/g, '<([')
.replace(/\| \\\[/g, '| [')
.replace(/\\n```js/g, '```js')
.replace(/docsjs\./g, '')
.replace('**Extends ModuleModel**', '')
.replace('**Extends Model**', '');
// Search for module event documentation
if (result.indexOf(REPLACE_EVENTS) >= 0) {
const eventsMd = await getEventsMdFromTypes(filePath);
if (eventsMd && result.indexOf(REPLACE_EVENTS) >= 0) {
addLogs.push('replaced events');
// Search for module event documentation
if (result.indexOf(REPLACE_EVENTS) >= 0) {
try {
const eventsMd = await getEventsMdFromTypes(filePath);
if (eventsMd && result.indexOf(REPLACE_EVENTS) >= 0) {
addLogs.push('replaced events');
}
result = eventsMd ? result.replace(REPLACE_EVENTS, `## Available Events\n${eventsMd}`) : result;
} catch (err) {
console.error(`Failed getting events: ${file[0]}`);
throw err;
}
}
result = eventsMd ? result.replace(REPLACE_EVENTS, `## Available Events\n${eventsMd}`) : result;
}
writeFileSync(`${docRoot}/api/${file[1]}`, result);
log('Created', file[1], addLogs.length ? `(${addLogs.join(', ')})` : '');
});
writeFileSync(`${docRoot}/api/${file[1]}`, result);
log('Created', file[1], addLogs.length ? `(${addLogs.join(', ')})` : '');
});
} catch (err) {
console.error(`Build failed: ${file[0]}`);
throw err;
}
}),
);

2
docs/api/block_manager.md

@ -84,6 +84,8 @@ editor.on('block:custom', ({ container, blocks, ... }) => { ... });
editor.on('block', ({ event, model, ... }) => { ... });
```
* BlocksEventCallback
[Block]: block.html
[Component]: component.html

8
docs/api/canvas.md

@ -122,6 +122,14 @@ editor.on('canvas:frame:load:body', ({ window }) => {
});
```
* `canvas:frame:unload` Frame is unloading from the canvas.
```javascript
editor.on('canvas:frame:unload', ({ frame }) => {
console.log('Unloading frame', frame);
});
```
[Component]: component.html
[Frame]: frame.html

7
docs/api/component.md

@ -137,7 +137,7 @@ By setting override to specific properties, changes of those properties will be
### Parameters
* `value` **([Boolean][3] | [String][1] | [Array][5]<[String][1]>)**&#x20;
* `options` **DynamicWatchersOptions** (optional, default `{}`)
* `options` **DataWatchersOptions** (optional, default `{}`)
### Examples
@ -335,8 +335,7 @@ Get the style of the component
### Parameters
* `options` **any** (optional, default `{}`)
* `optsAdd` **any** (optional, default `{}`)
* `opts` **GetComponentStyleOpts?**&#x20;
Returns **[Object][2]**&#x20;
@ -363,7 +362,7 @@ Return all component's attributes
### Parameters
* `opts` **{noClass: [boolean][3]?, noStyle: [boolean][3]?}** (optional, default `{}`)
* `opts` **{noClass: [boolean][3]?, noStyle: [boolean][3]?, skipResolve: [boolean][3]?}** (optional, default `{}`)
Returns **[Object][2]**&#x20;

44
docs/api/datasource.md

@ -31,6 +31,44 @@ dataSource.addRecord({ id: 'id3', name: 'value3' });
* `props` **DataSourceProps** Properties to initialize the data source.
* `opts` **DataSourceOptions** Options to initialize the data source.
### hasProvider
Indicates if the data source has a provider for records.
### getResolvedRecords
Retrieves all records from the data source with resolved relations based on the schema.
### upSchema
Update the schema.
#### Parameters
* `schema` **Partial\<any>**&#x20;
* `opts` **SetOptions?**&#x20;
#### Examples
```javascript
dataSource.upSchema({ name: { type: 'string' } });
```
### getSchemaField
Get schema field definition.
#### Parameters
* `fieldKey` **any**&#x20;
#### Examples
```javascript
const fieldSchema = dataSource.getSchemaField('name');
fieldSchema.type; // 'string'
```
## defaults
Returns the default properties for the data source.
@ -55,6 +93,12 @@ Retrieves the collection of records associated with this data source.
Returns **DataRecords\<DRProps>** The collection of data records.
## records
Retrieves the collection of records associated with this data source.
Returns **DataRecords\<DRProps>** The collection of data records.
## em
Retrieves the editor model associated with this data source.

43
docs/api/datasources.md

@ -44,12 +44,26 @@ editor.on('data:path', ({ dataSource, dataRecord, path }) => {
editor.on('data:pathSource:SOURCE_ID', ({ dataSource, dataRecord, path }) => { ... });
```
* `data:provider:load` Data source provider load.
```javascript
editor.on('data:provider:load', ({ dataSource, result }) => { ... });
```
* `data:provider:loadAll` Load of all data source providers (eg. on project load).
```javascript
editor.on('data:provider:loadAll', () => { ... });
```
* `data` Catch-all event for all the events mentioned above.
```javascript
editor.on('data', ({ event, model, ... }) => { ... });
```
* DataSourcesEventCallback
## Methods
* [add][1] - Add a new data source.
@ -101,15 +115,32 @@ Returns **[DataSource]** Data source.
## getValue
Get value from data sources by key
Get value from data sources by path.
### Parameters
* `key` **[String][7]** Path to value.
* `defValue` **any**&#x20;
* `path` **[String][7]** Path to value.
* `defValue` **any** Default value if the path is not found.
Returns **any** const value = dsm.getValue('ds\_id.record\_id.propName', 'defaultValue');
## setValue
Set value in data sources by path.
### Parameters
* `path` **[String][7]** Path to value in format 'dataSourceId.recordId.propName'
* `value` **any** Value to set
### Examples
```javascript
dsm.setValue('ds_id.record_id.propName', 'new value');
```
Returns **[Boolean][8]** Returns true if the value was set successfully
## remove
Remove data source.
@ -152,7 +183,7 @@ data record, and optional property path.
Store data sources to a JSON object.
Returns **[Array][8]** Stored data sources.
Returns **[Array][9]** Stored data sources.
## load
@ -178,4 +209,6 @@ Returns **[Object][6]** Loaded data sources.
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

33
docs/api/device_manager.md

@ -19,12 +19,35 @@ const deviceManager = editor.Devices;
```
## Available Events
* `device:add` New device added to the collection. The `Device` is passed as an argument.
* `device:add` - Added new device. The [Device] is passed as an argument to the callback
* `device:remove` - Device removed. The [Device] is passed as an argument to the callback
* `device:select` - New device selected. The newly selected [Device] and the previous one, are passed as arguments to the callback
* `device:update` - Device updated. The updated [Device] and the object containing changes are passed as arguments to the callback
* `device` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback
```javascript
editor.on('device:add', (device) => { ... });
```
* `device:remove` Device removed from the collection. The `Device` is passed as an argument.
```javascript
editor.on('device:remove', (device) => { ... });
```
* `device:select` A new device is selected. The `Device` is passed as an argument.
```javascript
editor.on('device:select', (device) => { ... });
```
* `device:update` Device updated. The `Device` and the object containing changes are passed as arguments.
```javascript
editor.on('device:update', (device) => { ... });
```
* `device` Catch-all event for all the events mentioned above.
```javascript
editor.on('device', ({ event, model, ... }) => { ... });
```
## Methods

12
docs/api/editor.md

@ -42,6 +42,15 @@ editor.on('load', () => { ... });
editor.on('project:load', ({ project, initial }) => { ... });
```
* `project:loaded` Similar to `project:load`, but triggers only if the project is loaded successfully.
```javascript
editor.on('project:loaded', ({ project, initial }) => { ... });
// Loading an empty project, won't trigger this event.
editor.loadProjectData({});
```
* `project:get` Event triggered on request of the project data. This can be used to extend the project with custom data.
```javascript
@ -516,6 +525,7 @@ Load data from the JSON project
### Parameters
* `data` **[Object][16]** Project to load
* `options` **[Object][16]?** Custom options that could be passed to the project load events. (optional, default `{}`)
### Examples
@ -722,7 +732,7 @@ Trigger event
### Parameters
* `event` **[string][18]** Event to trigger
* `args` **...[Array][19]\<any>**&#x20;
* `args` **...any**&#x20;
Returns **this**&#x20;

27
docs/api/keymaps.md

@ -19,23 +19,30 @@ const editor = grapesjs.init({
})
```
Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance.
```js
// Listen to events
editor.on('keymap:add', () => { ... });
// Use the API
const keymaps = editor.Keymaps;
keymaps.add(...);
```
## Available Events
* `keymap:add` New keymap added. The new keymap object is passed as an argument to the callback.
```javascript
editor.on('keymap:add', (keymap) => { ... });
```
* `keymap:remove` Keymap removed. The removed keymap object is passed as an argument to the callback.
* `keymap:add` - New keymap added. The new keyamp object is passed as an argument
* `keymap:remove` - Keymap removed. The removed keyamp object is passed as an argument
* `keymap:emit` - Some keymap emitted, in arguments you get keymapId, shortcutUsed, Event
* `keymap:emit:{keymapId}` - `keymapId` emitted, in arguments you get keymapId, shortcutUsed, Event
```javascript
editor.on('keymap:remove', (keymap) => { ... });
```
* `keymap:emit` Some keymap emitted. The keymapId, shortcutUsed, and Event are passed as arguments to the callback.
```javascript
editor.on('keymap:emit', (keymapId, shortcutUsed, event) => { ... });
```
## Methods

20
docs/api/layer_manager.md

@ -13,16 +13,30 @@ const editor = grapesjs.init({
})
```
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance.
```js
const layers = editor.Layers;
```
## Available Events
* `layer:root` Root layer changed. The new root component is passed as an argument to the callback.
* `layer:root` - Root layer changed. The new root component is passed as an argument to the callback.
* `layer:component` - Component layer is updated. The updated component is passed as an argument to the callback.
```javascript
editor.on('layer:root', (component) => { ... });
```
* `layer:component` Component layer is updated. The updated component is passed as an argument to the callback.
```javascript
editor.on('layer:component', (component, opts) => { ... });
```
* `layer:custom` Custom layer event. Object with container and root is passed as an argument to the callback.
```javascript
editor.on('layer:custom', ({ container, root }) => { ... });
```
## Methods

19
docs/api/modal_dialog.md

@ -19,10 +19,23 @@ const modal = editor.Modal;
```
## Available Events
* `modal:open` Modal is opened
* `modal:open` - Modal is opened
* `modal:close` - Modal is closed
* `modal` - Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback.
```javascript
editor.on('modal:open', () => { ... });
```
* `modal:close` Modal is closed
```javascript
editor.on('modal:close', () => { ... });
```
* `modal` Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback.
```javascript
editor.on('modal', ({ open, title, content, ... }) => { ... });
```
## Methods

12
docs/api/parser.md

@ -81,6 +81,12 @@ Parse HTML string and return the object containing the Component Definition
* `options.htmlType` **[String][6]?** [HTML mime type][7] to parse
* `options.allowScripts` **[Boolean][8]** Allow `<script>` tags (optional, default `false`)
* `options.allowUnsafeAttr` **[Boolean][8]** Allow unsafe HTML attributes (eg. `on*` inline event handlers) (optional, default `false`)
* `options.allowUnsafeAttrValue` **[Boolean][8]** Allow unsafe HTML attribute values (eg. `src="javascript:..."`) (optional, default `false`)
* `options.keepEmptyTextNodes` **[Boolean][8]** Keep whitespaces regardless of whether they are meaningful (optional, default `false`)
* `options.asDocument` **[Boolean][8]?** Treat the HTML string as document
* `options.detectDocument` **([Boolean][8] | [Function][9])?** Indicate if or how to detect if the HTML string should be treated as document
* `options.preParser` **[Function][9]?** How to pre-process the HTML string before parsing
* `options.convertDataGjsAttributesHyphens` **[Boolean][8]** Convert `data-gjs-*` attributes from hyphenated to camelCase (eg. `data-gjs-my-component` to `data-gjs-myComponent`) (optional, default `false`)
### Examples
@ -113,7 +119,7 @@ const res = Parser.parseCss('.cls { color: red }');
// [{ ... }]
```
Returns **[Array][9]<[Object][5]>** Array containing the result
Returns **[Array][10]<[Object][5]>** Array containing the result
[1]: https://github.com/GrapesJS/grapesjs/blob/master/src/parser/config/config.ts
@ -131,4 +137,6 @@ Returns **[Array][9]<[Object][5]>** Array containing the result
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

25
docs/api/rich_text_editor.md

@ -15,21 +15,30 @@ const editor = grapesjs.init({
})
```
Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance.
```js
// Listen to events
editor.on('rte:enable', () => { ... });
// Use the API
const rte = editor.RichTextEditor;
rte.add(...);
```
## Available Events
* `rte:enable` RTE enabled. The view, on which RTE is enabled, and the RTE instance are passed as arguments.
```javascript
editor.on('rte:enable', (view, rte) => { ... });
```
* `rte:disable` RTE disabled. The view, on which RTE is disabled, and the RTE instance are passed as arguments.
* `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
* `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument
```javascript
editor.on('rte:disable', (view, rte) => { ... });
```
* `rte:custom` Custom RTE event. Object with enabled status, container, and actions is passed as an argument.
```javascript
editor.on('rte:custom', ({ enabled, container, actions }) => { ... });
```
## Methods

54
docs/api/selector_manager.md

@ -35,24 +35,56 @@ const editor = grapesjs.init({
})
```
Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance.
```js
// Listen to events
editor.on('selector:add', (selector) => { ... });
// Use the API
const sm = editor.Selectors;
sm.add(...);
```
## Available Events
* `selector:add` Selector added. The Selector is passed as an argument to the callback.
```javascript
editor.on('selector:add', (selector) => { ... });
```
* `selector:remove` Selector removed. The Selector is passed as an argument to the callback.
```javascript
editor.on('selector:remove', (selector) => { ... });
```
* `selector:remove:before` Before selector remove. The Selector is passed as an argument to the callback.
```javascript
editor.on('selector:remove:before', (selector) => { ... });
```
* `selector:update` Selector updated. The Selector and the object containing changes are passed as arguments to the callback.
```javascript
editor.on('selector:update', (selector, changes) => { ... });
```
* `selector:state` States changed. An object containing all the available data about the triggered event is passed as an argument to the callback.
```javascript
editor.on('selector:state', (state) => { ... });
```
* `selector:custom` Custom selector event. An object containing states, selected selectors, and container is passed as an argument.
```javascript
editor.on('selector:custom', ({ states, selected, container }) => { ... });
```
* `selector` Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
```javascript
editor.on('selector', ({ event, selector, changes, ... }) => { ... });
```
* `selector:add` - Selector added. The [Selector] is passed as an argument to the callback.
* `selector:remove` - Selector removed. The [Selector] is passed as an argument to the callback.
* `selector:update` - Selector updated. The [Selector] and the object containing changes are passed as arguments to the callback.
* `selector:state` - States changed. An object containing all the available data about the triggered event is passed as an argument to the callback.
* `selector` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
* SelectorStringObject
## Methods

78
docs/api/style_manager.md

@ -13,32 +13,72 @@ const editor = grapesjs.init({
})
```
Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance.
```js
// Listen to events
editor.on('style:sector:add', (sector) => { ... });
// Use the API
const styleManager = editor.StyleManager;
styleManager.addSector(...);
```
## Available Events
* `style:sector:add` Sector added. The Sector is passed as an argument to the callback.
```javascript
editor.on('style:sector:add', (sector) => { ... });
```
* `style:sector:remove` Sector removed. The Sector is passed as an argument to the callback.
```javascript
editor.on('style:sector:remove', (sector) => { ... });
```
* `style:sector:update` Sector updated. The Sector and the object containing changes are passed as arguments to the callback.
* `style:sector:add` - Sector added. The [Sector] is passed as an argument to the callback.
* `style:sector:remove` - Sector removed. The [Sector] is passed as an argument to the callback.
* `style:sector:update` - Sector updated. The [Sector] and the object containing changes are passed as arguments to the callback.
* `style:property:add` - Property added. The [Property] is passed as an argument to the callback.
* `style:property:remove` - Property removed. The [Property] is passed as an argument to the callback.
* `style:property:update` - Property updated. The [Property] and the object containing changes are passed as arguments to the callback.
* `style:target` - Target selection changed. The target (or `null` in case the target is deselected) is passed as an argument to the callback.
<!--
* `styleManager:update:target` - The target (Component or CSSRule) is changed
* `styleManager:change` - Triggered on style property change from new selected component, the view of the property is passed as an argument to the callback
* `styleManager:change:{propertyName}` - As above but for a specific style property
-->
```javascript
editor.on('style:sector:update', (sector, changes) => { ... });
```
* `style:property:add` Property added. The Property is passed as an argument to the callback.
```javascript
editor.on('style:property:add', (property) => { ... });
```
* `style:property:remove` Property removed. The Property is passed as an argument to the callback.
```javascript
editor.on('style:property:remove', (property) => { ... });
```
* `style:property:update` Property updated. The Property and the object containing changes are passed as arguments to the callback.
```javascript
editor.on('style:property:update', (property, changes) => { ... });
```
* `style:target` Target selection changed. The target (or null in case the target is deselected) is passed as an argument to the callback.
```javascript
editor.on('style:target', (target) => { ... });
```
* `style:layer:select` Layer selected. Object containing layer data is passed as an argument.
```javascript
editor.on('style:layer:select', (data) => { ... });
```
* `style:custom` Custom style event. Object containing all custom data is passed as an argument.
```javascript
editor.on('style:custom', ({ container }) => { ... });
```
* `style` Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
```javascript
editor.on('style', ({ event, sector, property, ... }) => { ... });
```
## Methods

7
packages/core/src/block_manager/types.ts

@ -1,4 +1,5 @@
import { ItemsByCategory } from '../abstract/ModuleCategory';
import { AddOptions, RemoveOptions } from '../common';
import Block from './model/Block';
export interface BlocksByCategory extends ItemsByCategory<Block> {}
@ -77,5 +78,11 @@ export enum BlocksEvents {
}
/**{END_EVENTS}*/
export interface BlocksEventCallback {
[BlocksEvents.add]: [Block, AddOptions];
[BlocksEvents.remove]: [Block, RemoveOptions];
[BlocksEvents.update]: [Block, AddOptions];
}
// need this to avoid the TS documentation generator to break
export default BlocksEvents;

13
packages/core/src/data_sources/config/config.ts

@ -0,0 +1,13 @@
export interface DataSourcesConfig {
/**
* If true, data source providers will be autoloaded on project load.
* @default false
*/
autoloadProviders?: boolean;
}
const config: () => DataSourcesConfig = () => ({
autoloadProviders: false,
});
export default config;

72
packages/core/src/data_sources/index.ts

@ -21,23 +21,25 @@
* @module DataSources
*/
import { isEmpty } from 'underscore';
import { ItemManagerModule, ModuleConfig } from '../abstract/Module';
import { AddOptions, collectionEvents, ObjectAny, RemoveOptions } from '../common';
import EditorModel from '../editor/model/Editor';
import { get, stringToPath } from '../utils/mixins';
import { get, set, stringToPath } from '../utils/mixins';
import defConfig, { DataSourcesConfig } from './config/config';
import DataRecord from './model/DataRecord';
import DataSource from './model/DataSource';
import DataSources from './model/DataSources';
import { DataSourcesEvents, DataSourceProps, DataRecordProps } from './types';
import { Events } from 'backbone';
export default class DataSourceManager extends ItemManagerModule<ModuleConfig, DataSources> {
export default class DataSourceManager extends ItemManagerModule<DataSourcesConfig & ModuleConfig, DataSources> {
storageKey = 'dataSources';
events = DataSourcesEvents;
destroy(): void {}
constructor(em: EditorModel) {
super(em, 'DataSources', new DataSources([], em), DataSourcesEvents);
super(em, 'DataSources', new DataSources([], em), DataSourcesEvents, defConfig());
Object.assign(this, Events); // Mixin Backbone.Events
}
@ -73,14 +75,36 @@ export default class DataSourceManager extends ItemManagerModule<ModuleConfig, D
}
/**
* Get value from data sources by key
* @param {String} key Path to value.
* @param {any} defValue
* Get value from data sources by path.
* @param {String} path Path to value.
* @param {any} defValue Default value if the path is not found.
* @returns {any}
* const value = dsm.getValue('ds_id.record_id.propName', 'defaultValue');
*/
getValue(key: string | string[], defValue: any) {
return get(this.getContext(), key, defValue);
getValue(path: string | string[], defValue?: any) {
return get(this.getContext(), path, defValue);
}
/**
* Set value in data sources by path.
* @param {String} path Path to value in format 'dataSourceId.recordId.propName'
* @param {any} value Value to set
* @returns {Boolean} Returns true if the value was set successfully
* @example
* dsm.setValue('ds_id.record_id.propName', 'new value');
*/
setValue(path: string, value: any) {
const [ds, record, propPath] = this.fromPath(path);
if (record && (propPath || propPath === '')) {
let attrs = { ...record.attributes };
if (set(attrs, propPath || '', value)) {
record.set(attrs);
return true;
}
}
return false;
}
private getContext() {
@ -142,15 +166,16 @@ export default class DataSourceManager extends ItemManagerModule<ModuleConfig, D
* @returns {Array} Stored data sources.
*/
store() {
const data: any[] = [];
const data: DataSourceProps[] = [];
this.all.forEach((dataSource) => {
const skipFromStorage = dataSource.get('skipFromStorage');
const { skipFromStorage, transformers, records, schema, ...rest } = dataSource.attributes;
if (!skipFromStorage) {
data.push({
id: dataSource.id,
name: dataSource.get('name' as any),
records: dataSource.records.toJSON(),
skipFromStorage,
...rest,
id: rest.id!,
schema: !isEmpty(schema) ? schema : undefined,
records: !rest.provider ? records : undefined,
});
}
});
@ -164,7 +189,24 @@ export default class DataSourceManager extends ItemManagerModule<ModuleConfig, D
* @returns {Object} Loaded data sources.
*/
load(data: any) {
return this.loadProjectData(data);
const { config, all, events, em } = this;
const result = this.loadProjectData(data);
if (config.autoloadProviders) {
const dsWithProviders = all.filter((ds) => ds.hasProvider);
if (!!dsWithProviders.length) {
const loadProviders = async () => {
em.trigger(events.providerLoadAllBefore);
const providersToLoad = dsWithProviders.map((ds) => ds.loadProvider());
await Promise.all(providersToLoad);
em.trigger(events.providerLoadAll);
};
loadProviders();
}
}
return result;
}
postLoad() {

142
packages/core/src/data_sources/model/DataSource.ts

@ -29,9 +29,26 @@
* @extends {Model<DataSourceProps>}
*/
import { AddOptions, collectionEvents, CombinedModelConstructorOptions, Model, RemoveOptions } from '../../common';
import { isString } from 'underscore';
import {
AddOptions,
collectionEvents,
CombinedModelConstructorOptions,
Model,
RemoveOptions,
SetOptions,
} from '../../common';
import EditorModel from '../../editor/model/Editor';
import { DataSourceTransformers, DataSourceType, DataSourceProps, DataRecordProps } from '../types';
import {
DataFieldPrimitiveType,
DataFieldSchemaRelation,
DataRecordProps,
DataSourceProps,
DataSourceProviderResult,
DataSourceTransformers,
DataSourceType,
} from '../types';
import { DEF_DATA_FIELD_ID } from '../utils';
import DataRecord from './DataRecord';
import DataRecords from './DataRecords';
import DataSources from './DataSources';
@ -68,6 +85,7 @@ export default class DataSource<DRProps extends DataRecordProps = DataRecordProp
constructor(props: DataSourceProps<DRProps>, opts: DataSourceOptions) {
super(
{
schema: {},
...props,
records: [],
} as unknown as DataSourceType<DRProps>,
@ -94,6 +112,16 @@ export default class DataSource<DRProps extends DataRecordProps = DataRecordProp
return this.attributes.records as NonNullable<DataRecords<DRProps>>;
}
/**
* Retrieves the collection of records associated with this data source.
*
* @returns {DataRecords<DRProps>} The collection of data records.
* @name records
*/
get schema() {
return this.attributes.schema!;
}
/**
* Retrieves the editor model associated with this data source.
*
@ -104,6 +132,13 @@ export default class DataSource<DRProps extends DataRecordProps = DataRecordProp
return (this.collection as unknown as DataSources).em;
}
/**
* Indicates if the data source has a provider for records.
*/
get hasProvider() {
return !!this.attributes.provider;
}
/**
* Handles the `add` event for records in the data source.
* This method triggers a change event on the newly added record.
@ -135,8 +170,8 @@ export default class DataSource<DRProps extends DataRecordProps = DataRecordProp
* @returns {DataRecord<DRProps> | undefined} The data record, or `undefined` if no record is found with the given ID.
* @name getRecord
*/
getRecord(id: string | number) {
return this.records.get(id) as DataRecord | undefined;
getRecord(id: string | number): DataRecord | undefined {
return this.records.get(id);
}
/**
@ -150,6 +185,86 @@ export default class DataSource<DRProps extends DataRecordProps = DataRecordProp
return [...this.records.models].map((record) => this.getRecord(record.id)!);
}
/**
* Retrieves all records from the data source with resolved relations based on the schema.
*/
getResolvedRecords() {
const schemaEntries = Object.entries(this.schema);
const records = this.getRecords().map((record) => {
const result = { ...record.attributes };
if (schemaEntries.length === 0) return result;
schemaEntries.forEach(([fieldName, schema]) => {
const fieldSchema = schema as DataFieldSchemaRelation;
if (fieldSchema?.type === DataFieldPrimitiveType.relation && fieldSchema.target) {
const relationValue = result[fieldName];
if (relationValue) {
const targetDs = this.em.DataSources.get(fieldSchema.target);
if (targetDs) {
const targetRecords = targetDs.records;
const targetField = fieldSchema.targetField || DEF_DATA_FIELD_ID;
if (fieldSchema.isMany) {
const relationValues = Array.isArray(relationValue) ? relationValue : [relationValue];
const relatedRecords = targetRecords.filter((r) => relationValues.includes(r.attributes[targetField]));
result[fieldName] = relatedRecords.map((r) => ({ ...r.attributes }));
} else {
const relatedRecord = targetDs.records.find((r) => r.attributes[targetField] === relationValue);
if (relatedRecord) {
result[fieldName] = { ...relatedRecord.attributes };
}
}
}
}
}
});
return result;
});
return records;
}
async loadProvider() {
const { attributes, em } = this;
const { provider } = attributes;
if (!provider) return;
if (isString(provider)) {
// TODO: implement providers as plugins (later)
return;
}
const providerGet = isString(provider.get) ? { url: provider.get } : provider.get;
const { url, method, headers, body } = providerGet;
const fetchProvider = async () => {
const dataSource = this;
try {
em.trigger(em.DataSources.events.providerLoadBefore, { dataSource });
const response = await fetch(url, { method, headers, body });
if (!response.ok) throw new Error(await response.text());
const result: DataSourceProviderResult = await response.json();
if (result?.records) this.setRecords(result.records as any);
if (result?.schema) this.upSchema(result.schema);
em.trigger(em.DataSources.events.providerLoad, { result, dataSource });
} catch (error: any) {
em.logError(error.message);
em.trigger(em.DataSources.events.providerLoadError, { dataSource, error });
}
};
await fetchProvider();
}
/**
* Removes a record from the data source by its ID.
*
@ -182,6 +297,25 @@ export default class DataSource<DRProps extends DataRecordProps = DataRecordProp
});
}
/**
* Update the schema.
* @example
* dataSource.upSchema({ name: { type: 'string' } });
*/
upSchema(schema: Partial<typeof this.schema>, opts?: SetOptions) {
this.set('schema', { ...this.schema, ...schema }, opts);
}
/**
* Get schema field definition.
* @example
* const fieldSchema = dataSource.getSchemaField('name');
* fieldSchema.type; // 'string'
*/
getSchemaField(fieldKey: keyof DRProps) {
return this.schema[fieldKey];
}
private handleChanges(m: any, c: any, o: any) {
this.em.changesUp(o || c);
}

137
packages/core/src/data_sources/types.ts

@ -1,8 +1,9 @@
import { Model, Collection, ObjectAny } from '../common';
import { AddOptions, Collection, Model, ObjectAny, RemoveOptions, SetOptions } from '../common';
import DataRecord from './model/DataRecord';
import DataRecords from './model/DataRecords';
import DataSource from './model/DataSource';
import DataVariable, { DataVariableProps } from './model/DataVariable';
import { DataConditionProps, DataCondition } from './model/conditional_variables/DataCondition';
import { DataCondition, DataConditionProps } from './model/conditional_variables/DataCondition';
export type DataResolver = DataVariable | DataCondition;
export type DataResolverProps = DataVariableProps | DataConditionProps;
@ -46,12 +47,107 @@ interface BaseDataSource {
* If true will store the data source in the GrapesJS project.json file.
*/
skipFromStorage?: boolean;
[key: string]: unknown;
}
export enum DataFieldPrimitiveType {
string = 'string',
number = 'number',
boolean = 'boolean',
date = 'date',
json = 'json',
relation = 'relation',
}
export interface DataFieldSchemaBase<T = unknown> {
default?: T;
description?: string;
label?: string;
[key: string]: unknown;
// order?: number;
// primary?: boolean;
// required?: boolean;
// unique?: boolean;
// validate?: (value: T, record: Record<string, any>) => boolean;
}
export interface DataFieldSchemaString extends DataFieldSchemaBase<string> {
type: DataFieldPrimitiveType.string;
enum?: string[];
}
export interface DataFieldSchemaNumber extends DataFieldSchemaBase<number> {
type: DataFieldPrimitiveType.number;
}
export interface DataFieldSchemaBoolean extends DataFieldSchemaBase<boolean> {
type: DataFieldPrimitiveType.boolean;
}
export interface DataFieldSchemaDate extends DataFieldSchemaBase<Date> {
type: DataFieldPrimitiveType.date;
}
export interface DataFieldSchemaJSON extends DataFieldSchemaBase<any> {
type: DataFieldPrimitiveType.json;
}
export interface DataFieldSchemaRelation extends DataFieldSchemaBase {
type: DataFieldPrimitiveType.relation;
/**
* The target data source ID
*/
target: string;
/**
* The target field in the data source
*/
targetField?: string;
/**
* If true, the relation is one-to-many
*/
isMany?: boolean;
}
export type DataFieldSchemas =
| DataFieldSchemaString
| DataFieldSchemaNumber
| DataFieldSchemaBoolean
| DataFieldSchemaDate
| DataFieldSchemaJSON
| DataFieldSchemaRelation;
export type DataSourceSchema<DR extends DataRecordProps = DataRecordProps> = {
[K in keyof DR]?: DataFieldSchemas;
};
export interface DataSourceProviderMethodProps {
url: string;
method?: string;
headers?: HeadersInit;
body?: BodyInit;
}
export interface DataSourceProviderDefinitionProps {
get: string | DataSourceProviderMethodProps;
}
export interface DataSourceProviderResult {
records?: DataRecordProps[];
schema?: DataSourceSchema;
}
export type DataSourceProviderProp = string | DataSourceProviderDefinitionProps;
export interface DataSourceType<DR extends DataRecordProps> extends BaseDataSource {
records: DataRecords<DR>;
schema: DataSourceSchema<DR>;
provider?: DataSourceProviderProp;
}
export interface DataSourceProps<DR extends DataRecordProps> extends BaseDataSource {
export interface DataSourceProps<DR extends DataRecordProps = DataRecordProps> extends BaseDataSource {
records?: DataRecords<DR> | DataRecord<DR>[] | DR[];
schema?: DataSourceSchema<DR>;
provider?: DataSourceProviderProp;
}
export type RecordPropsType<T> = T extends DataRecord<infer U> ? U : never;
export interface DataSourceTransformers {
@ -80,6 +176,8 @@ export type DeepPartialDot<T> = {
: never;
};
export type DataSourceEvent = `${DataSourcesEvents}`;
/**{START_EVENTS}*/
export enum DataSourcesEvents {
/**
@ -122,6 +220,23 @@ export enum DataSourcesEvents {
*/
pathSource = 'data:pathSource:',
/**
* @event `data:provider:load` Data source provider load.
* @example
* editor.on('data:provider:load', ({ dataSource, result }) => { ... });
*/
providerLoad = 'data:provider:load',
providerLoadBefore = 'data:provider:load:before',
providerLoadError = 'data:provider:load:error',
/**
* @event `data:provider:loadAll` Load of all data source providers (eg. on project load).
* @example
* editor.on('data:provider:loadAll', () => { ... });
*/
providerLoadAll = 'data:provider:loadAll',
providerLoadAllBefore = 'data:provider:loadAll:before',
/**
* @event `data` Catch-all event for all the events mentioned above.
* @example
@ -131,5 +246,21 @@ export enum DataSourcesEvents {
}
/**{END_EVENTS}*/
export interface DataSourcesEventCallback {
[DataSourcesEvents.add]: [DataSource, AddOptions];
[DataSourcesEvents.remove]: [DataSource, RemoveOptions];
[DataSourcesEvents.update]: [DataSource, AddOptions];
[DataSourcesEvents.path]: [{ dataSource: DataSource; dataRecord: DataRecord; path: string; options: SetOptions }];
[DataSourcesEvents.pathSource]: [
{ dataSource: DataSource; dataRecord: DataRecord; path: string; options: SetOptions },
];
[DataSourcesEvents.providerLoad]: [{ dataSource: DataSource; result: DataSourceProviderResult }];
[DataSourcesEvents.providerLoadBefore]: [{ dataSource: DataSource }];
[DataSourcesEvents.providerLoadError]: [{ dataSource: DataSource; error: Error }];
[DataSourcesEvents.providerLoadAll]: [];
[DataSourcesEvents.providerLoadAllBefore]: [];
[DataSourcesEvents.all]: [{ event: DataSourceEvent; model?: Model; options: ObjectAny }];
}
// need this to avoid the TS documentation generator to break
export default DataSourcesEvents;

2
packages/core/src/data_sources/utils.ts

@ -10,6 +10,8 @@ import { DataConditionIfFalseType, DataConditionIfTrueType } from './model/condi
import { getSymbolMain } from '../dom_components/model/SymbolUtils';
import Component from '../dom_components/model/Component';
export const DEF_DATA_FIELD_ID = 'id';
export function isDataResolverProps(value: any): value is DataResolverProps {
return typeof value === 'object' && [DataVariableType, DataConditionType].includes(value?.type);
}

7
packages/core/src/device_manager/index.ts

@ -13,12 +13,7 @@
* ```js
* const deviceManager = editor.Devices;
* ```
* ## Available Events
* * `device:add` - Added new device. The [Device] is passed as an argument to the callback
* * `device:remove` - Device removed. The [Device] is passed as an argument to the callback
* * `device:select` - New device selected. The newly selected [Device] and the previous one, are passed as arguments to the callback
* * `device:update` - Device updated. The updated [Device] and the object containing changes are passed as arguments to the callback
* * `device` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback
* {REPLACE_EVENTS}
*
* ## Methods
* * [add](#add)

4
packages/core/src/dom_components/view/ComponentTextView.ts

@ -248,9 +248,11 @@ export default class ComponentTextView<TComp extends ComponentText = ComponentTe
mixins.off(elDocs, 'mousedown', this.onDisable as any);
mixins[method](elDocs, 'mousedown', this.onDisable as any);
em[method]('toolbar:run:before', this.onDisable);
if (model) {
const rteEvents = em.RichTextEditor.events;
model[method]('removed', this.onDisable);
model.trigger(`rte:${enable ? 'enable' : 'disable'}`);
model.trigger(enable ? rteEvents.enable : rteEvents.disable);
}
// @ts-ignore Avoid closing edit mode on component click

73
packages/core/src/editor/index.ts

@ -44,43 +44,49 @@
*/
import { IBaseModule } from '../abstract/Module';
import AssetManager from '../asset_manager';
import { AssetEvent } from '../asset_manager/types';
import BlockManager, { BlockEvent } from '../block_manager';
import CanvasModule, { CanvasEvent } from '../canvas';
import BlockManager from '../block_manager';
import CanvasModule from '../canvas';
import CodeManagerModule from '../code_manager';
import CommandsModule, { CommandEvent } from '../commands';
import { AddOptions, EventHandler, LiteralUnion } from '../common';
import CommandsModule from '../commands';
import { AddOptions, EventHandler } from '../common';
import CssComposer from '../css_composer';
import CssRule from '../css_composer/model/CssRule';
import CssRules from '../css_composer/model/CssRules';
import DataSourceManager from '../data_sources';
import DeviceManager from '../device_manager';
import ComponentManager, { ComponentEvent } from '../dom_components';
import ComponentManager from '../dom_components';
import Component from '../dom_components/model/Component';
import Components from '../dom_components/model/Components';
import ComponentWrapper from '../dom_components/model/ComponentWrapper';
import { AddComponentsOption, ComponentAdd, DragMode } from '../dom_components/model/types';
import StyleableModel from '../domain_abstract/model/StyleableModel';
import I18nModule from '../i18n';
import KeymapsModule, { KeymapEvent } from '../keymaps';
import ModalModule, { ModalEvent } from '../modal_dialog';
import KeymapsModule from '../keymaps';
import ModalModule from '../modal_dialog';
import LayerManager from '../navigator';
import PageManager from '../pages';
import PanelManager from '../panels';
import ParserModule from '../parser';
import { CustomParserCss } from '../parser/config/config';
import RichTextEditorModule, { RichTextEditorEvent } from '../rich_text_editor';
import RichTextEditorModule from '../rich_text_editor';
import { CustomRTE } from '../rich_text_editor/config/config';
import SelectorManager, { SelectorEvent } from '../selector_manager';
import StorageManager, { ProjectData, StorageEvent, StorageOptions } from '../storage_manager';
import StyleManager, { StyleManagerEvent } from '../style_manager';
import SelectorManager from '../selector_manager';
import StorageManager, { ProjectData, StorageOptions } from '../storage_manager';
import StyleManager from '../style_manager';
import TraitManager from '../trait_manager';
import UndoManagerModule from '../undo_manager';
import UtilsModule from '../utils';
import html from '../utils/html';
import defConfig, { EditorConfig, EditorConfigKeys } from './config/config';
import EditorModel, { EditorLoadOptions } from './model/Editor';
import { EditorEvents } from './types';
import {
EditorConfigType,
EditorEvent,
EditorEventCallbacks,
EditorEventHandler,
EditorEvents,
EditorModelParam,
} from './types';
import EditorView from './view/EditorView';
export type ParsedRule = {
@ -90,28 +96,6 @@ export type ParsedRule = {
params?: string;
};
type GeneralEvent = 'canvasScroll' | 'undo' | 'redo' | 'load' | 'update';
type EditorBuiltInEvents =
| ComponentEvent
| BlockEvent
| AssetEvent
| KeymapEvent
| StyleManagerEvent
| StorageEvent
| CanvasEvent
| SelectorEvent
| RichTextEditorEvent
| ModalEvent
| CommandEvent
| GeneralEvent;
type EditorEvent = LiteralUnion<EditorBuiltInEvents, string>;
type EditorConfigType = EditorConfig & { pStylePrefix?: string };
type EditorModelParam<T extends keyof EditorModel, N extends number> = Parameters<EditorModel[T]>[N];
export type EditorParam<T extends keyof Editor, N extends number> = Parameters<Editor[T]>[N];
export default class Editor implements IBaseModule<EditorConfig> {
@ -733,8 +717,8 @@ export default class Editor implements IBaseModule<EditorConfig> {
* @param {Function} callback Callback function
* @return {this}
*/
on(event: EditorEvent, callback: EventHandler) {
this.em.on(event, callback);
on<E extends EditorEvent>(event: E, callback: EditorEventHandler<E>) {
this.em.on(event as string, callback);
return this;
}
@ -744,8 +728,8 @@ export default class Editor implements IBaseModule<EditorConfig> {
* @param {Function} callback Callback function
* @return {this}
*/
once(event: EditorEvent, callback: EventHandler) {
this.em.once(event, callback);
once<E extends EditorEvent>(event: E, callback: EditorEventHandler<E>) {
this.em.once(event as string, callback);
return this;
}
@ -755,8 +739,8 @@ export default class Editor implements IBaseModule<EditorConfig> {
* @param {Function} callback Callback function
* @return {this}
*/
off(event: EditorEvent, callback: EventHandler) {
this.em.off(event, callback);
off<E extends EditorEvent>(event: E, callback: EditorEventHandler<E>) {
this.em.off(event as string, callback);
return this;
}
@ -765,8 +749,11 @@ export default class Editor implements IBaseModule<EditorConfig> {
* @param {string} event Event to trigger
* @return {this}
*/
trigger(event: EditorEvent, ...args: any[]) {
this.em.trigger.apply(this.em, [event, ...args]);
trigger<E extends EditorEvent>(
event: E,
...args: E extends keyof EditorEventCallbacks ? EditorEventCallbacks[E] : any[]
) {
this.em.trigger.apply(this.em, [event as string, ...args]);
return this;
}

11
packages/core/src/editor/model/Editor.ts

@ -280,7 +280,7 @@ export default class EditorModel extends Model {
this.on('change:componentHovered', this.componentHovered, this);
this.on('change:changesCount', this.updateChanges, this);
this.on('change:readyLoad change:readyCanvas', this._checkReady, this);
toLog.forEach((e) => this.listenLog(e));
toLog.forEach((e) => this.listenLog(e as keyof typeof logs));
// Deprecations
[{ from: 'change:selectedComponent', to: 'component:toggled' }].forEach((event) => {
@ -303,9 +303,12 @@ export default class EditorModel extends Model {
return this.config.el;
}
listenLog(event: string) {
//@ts-ignore
this.listenTo(this, `log:${event}`, logs[event]);
listenLog(event: keyof typeof logs) {
this.listenTo(this, `log:${event}`, (...args) => {
if (!this.config.log) return;
const logFn = logs[event];
logFn?.(...args);
});
}
get config() {

47
packages/core/src/editor/types.ts

@ -1,3 +1,50 @@
import { AssetEvent } from '../asset_manager/types';
import { BlockEvent } from '../block_manager';
import { BlocksEventCallback } from '../block_manager/types';
import { CanvasEvent } from '../canvas';
import { CommandEvent } from '../commands';
import { LiteralUnion } from '../common';
import { DataSourceEvent, DataSourcesEventCallback } from '../data_sources/types';
import { ComponentEvent } from '../dom_components';
import { KeymapEvent } from '../keymaps';
import { ModalEvent } from '../modal_dialog';
import { RichTextEditorEvent } from '../rich_text_editor';
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';
type EditorBuiltInEvents =
| DataSourceEvent
| ComponentEvent
| BlockEvent
| AssetEvent
| KeymapEvent
| StyleManagerEvent
| StorageEvent
| CanvasEvent
| SelectorEvent
| RichTextEditorEvent
| ModalEvent
| CommandEvent
| GeneralEvent;
export type EditorEvent = LiteralUnion<EditorBuiltInEvents, string>;
export type EditorConfigType = EditorConfig & { pStylePrefix?: string };
export type EditorModelParam<T extends keyof EditorModel, N extends number> = Parameters<EditorModel[T]>[N];
export interface EditorEventCallbacks extends BlocksEventCallback, DataSourcesEventCallback {
[key: string]: any[];
}
export type EditorEventHandler<E extends EditorEvent> = E extends keyof EditorEventCallbacks
? (...args: EditorEventCallbacks[E]) => void
: (...args: any[]) => void;
/**{START_EVENTS}*/
export enum EditorEvents {
/**

33
packages/core/src/keymaps/index.ts

@ -15,22 +15,13 @@
* })
* ```
*
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('keymap:add', () => { ... });
*
* // Use the API
* const keymaps = editor.Keymaps;
* keymaps.add(...);
* ```
*
* ## Available Events
* * `keymap:add` - New keymap added. The new keyamp object is passed as an argument
* * `keymap:remove` - Keymap removed. The removed keyamp object is passed as an argument
* * `keymap:emit` - Some keymap emitted, in arguments you get keymapId, shortcutUsed, Event
* * `keymap:emit:{keymapId}` - `keymapId` emitted, in arguments you get keymapId, shortcutUsed, Event
* {REPLACE_EVENTS}
*
* ## Methods
* * [getConfig](#getconfig)
@ -44,19 +35,21 @@
*/
import { isFunction, isString } from 'underscore';
import { hasWin } from '../utils/mixins';
import keymaster from '../utils/keymaster';
import { Module } from '../abstract';
import EditorModel from '../editor/model/Editor';
import keymaster from '../utils/keymaster';
import { hasWin } from '../utils/mixins';
import defConfig, { Keymap, KeymapOptions, KeymapsConfig } from './config';
import { KeymapsEvents } from './types';
export type KeymapEvent = 'keymap:add' | 'keymap:remove' | 'keymap:emit' | `keymap:emit:${string}`;
export type KeymapEvent = `${KeymapsEvents}`;
hasWin() && keymaster.init(window);
export default class KeymapsModule extends Module<KeymapsConfig & { name?: string }> {
keymaster: any = keymaster;
keymaps: Record<string, Keymap>;
events = KeymapsEvents;
constructor(em: EditorModel) {
super(em, 'Keymaps', defConfig());
@ -106,7 +99,7 @@ export default class KeymapsModule extends Module<KeymapsConfig & { name?: strin
* })
*/
add(id: Keymap['id'], keys: Keymap['keys'], handler: Keymap['handler'], opts: KeymapOptions = {}) {
const { em } = this;
const { em, events } = this;
const cmd = em.Commands;
const editor = em.getEditor();
const canvas = em.Canvas;
@ -125,13 +118,13 @@ export default class KeymapsModule extends Module<KeymapsConfig & { name?: strin
opts.prevent && canvas.getCanvasView()?.preventDefault(e);
isFunction(handlerRes) ? handlerRes(editor, 0, opt) : cmd.runCommand(handlerRes, opt);
const args = [id, h.shortcut, e];
em.trigger('keymap:emit', ...args);
em.trigger(`keymap:emit:${id}`, ...args);
em.trigger(events.emit, ...args);
em.trigger(`${events.emitId}${id}`, ...args);
}
},
undefined,
);
em.trigger('keymap:add', keymap);
em.trigger(events.add, keymap);
return keymap;
}
@ -167,7 +160,7 @@ export default class KeymapsModule extends Module<KeymapsConfig & { name?: strin
* // -> {keys, handler};
*/
remove(id: string) {
const { em } = this;
const { em, events } = this;
const keymap = this.get(id);
if (keymap) {
@ -176,7 +169,7 @@ export default class KeymapsModule extends Module<KeymapsConfig & { name?: strin
// @ts-ignore
keymaster.unbind(k.trim());
});
em?.trigger('keymap:remove', keymap);
em?.trigger(events.remove, keymap);
return keymap;
}
}

28
packages/core/src/keymaps/types.ts

@ -0,0 +1,28 @@
/**{START_EVENTS}*/
export enum KeymapsEvents {
/**
* @event `keymap:add` New keymap added. The new keymap object is passed as an argument to the callback.
* @example
* editor.on('keymap:add', (keymap) => { ... });
*/
add = 'keymap:add',
/**
* @event `keymap:remove` Keymap removed. The removed keymap object is passed as an argument to the callback.
* @example
* editor.on('keymap:remove', (keymap) => { ... });
*/
remove = 'keymap:remove',
/**
* @event `keymap:emit` Some keymap emitted. The keymapId, shortcutUsed, and Event are passed as arguments to the callback.
* @example
* editor.on('keymap:emit', (keymapId, shortcutUsed, event) => { ... });
*/
emit = 'keymap:emit',
emitId = 'keymap:emit:',
}
/**{END_EVENTS}*/
// need this to avoid the TS documentation generator to break
export default KeymapsEvents;

25
packages/core/src/modal_dialog/index.ts

@ -14,10 +14,7 @@
* const modal = editor.Modal;
* ```
*
* ## Available Events
* * `modal:open` - Modal is opened
* * `modal:close` - Modal is closed
* * `modal` - Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback.
* {REPLACE_EVENTS}
*
* ## Methods
* * [open](#open)
@ -35,18 +32,20 @@
import { debounce, isFunction, isString } from 'underscore';
import { Module } from '../abstract';
import EditorView from '../editor/view/EditorView';
import { EventHandler } from '../common';
import EditorModel from '../editor/model/Editor';
import EditorView from '../editor/view/EditorView';
import { createText } from '../utils/dom';
import defConfig, { ModalConfig } from './config/config';
import ModalM from './model/Modal';
import { ModalEvents } from './types';
import ModalView from './view/ModalView';
import { EventHandler } from '../common';
export type ModalEvent = 'modal:open' | 'modal:close' | 'modal';
export type ModalEvent = `${ModalEvents}`;
export default class ModalModule extends Module<ModalConfig> {
modal?: ModalView;
events = ModalEvents;
/**
* Initialize module. Automatically called with a new instance of the editor
@ -55,10 +54,10 @@ export default class ModalModule extends Module<ModalConfig> {
*/
constructor(em: EditorModel) {
super(em, 'Modal', defConfig());
const { events } = this;
this.model = new ModalM(this);
this.model.on('change:open', (m: ModalM, enable: boolean) => {
em.trigger(`modal:${enable ? 'open' : 'close'}`);
em.trigger(enable ? events.open : events.close);
});
this.model.on(
'change',
@ -67,7 +66,7 @@ export default class ModalModule extends Module<ModalConfig> {
const { custom } = this.config;
//@ts-ignore
isFunction(custom) && custom(data);
em.trigger('modal', data);
em.trigger(events.all, data);
}, 0),
);
@ -142,7 +141,8 @@ export default class ModalModule extends Module<ModalConfig> {
* });
*/
onceClose(clb: EventHandler) {
this.em.once('modal:close', clb);
const { em, events } = this;
em.once(events.close, clb);
return this;
}
@ -157,7 +157,8 @@ export default class ModalModule extends Module<ModalConfig> {
* });
*/
onceOpen(clb: EventHandler) {
this.em.once('modal:open', clb);
const { em, events } = this;
em.once(events.open, clb);
return this;
}

27
packages/core/src/modal_dialog/types.ts

@ -0,0 +1,27 @@
/**{START_EVENTS}*/
export enum ModalEvents {
/**
* @event `modal:open` Modal is opened
* @example
* editor.on('modal:open', () => { ... });
*/
open = 'modal:open',
/**
* @event `modal:close` Modal is closed
* @example
* editor.on('modal:close', () => { ... });
*/
close = 'modal:close',
/**
* @event `modal` Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback.
* @example
* editor.on('modal', ({ open, title, content, ... }) => { ... });
*/
all = 'modal',
}
/**{END_EVENTS}*/
// need this to avoid the TS documentation generator to break
export default ModalEvents;

42
packages/core/src/navigator/index.ts

@ -9,15 +9,13 @@
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance.
*
* ```js
* const layers = editor.Layers;
* ```
*
* ## Available Events
* * `layer:root` - Root layer changed. The new root component is passed as an argument to the callback.
* * `layer:component` - Component layer is updated. The updated component is passed as an argument to the callback.
* {REPLACE_EVENTS}
*
* ## Methods
* * [setRoot](#setroot)
@ -39,38 +37,18 @@
* @module Layers
*/
import { isString, bindAll } from 'underscore';
import { bindAll, isString } from 'underscore';
import { ModuleModel } from '../abstract';
import Module from '../abstract/Module';
import Component from '../dom_components/model/Component';
import { ComponentsEvents } from '../dom_components/types';
import EditorModel from '../editor/model/Editor';
import { hasWin, isComponent, isDef } from '../utils/mixins';
import defConfig, { LayerManagerConfig } from './config/config';
import { LayerData, LayerEvents } from './types';
import View from './view/ItemView';
import { ComponentsEvents } from '../dom_components/types';
interface LayerData {
name: string;
open: boolean;
selected: boolean;
hovered: boolean;
visible: boolean;
locked: boolean;
components: Component[];
}
export const evAll = 'layer';
export const evPfx = `${evAll}:`;
export const evRoot = `${evPfx}root`;
export const evComponent = `${evPfx}component`;
export const evCustom = `${evPfx}custom`;
const events = {
all: evAll,
root: evRoot,
component: evComponent,
custom: evCustom,
};
export type LayerEvent = `${LayerEvents}`;
const styleOpts = { mediaText: '' };
@ -88,7 +66,7 @@ export default class LayerManager extends Module<LayerManagerConfig> {
view?: View;
events = events;
events = LayerEvents;
constructor(em: EditorModel) {
super(em, 'LayerManager', defConfig());
@ -358,9 +336,10 @@ export default class LayerManager extends Module<LayerManagerConfig> {
}
__onRootChange() {
const { em, events } = this;
const root = this.getRoot();
this.view?.setRoot(root);
this.em.trigger(evRoot, root);
em.trigger(events.root, root);
this.__trgCustom();
}
@ -390,6 +369,7 @@ export default class LayerManager extends Module<LayerManagerConfig> {
}
updateLayer(component: Component, opts?: any) {
this.em.trigger(evComponent, component, opts);
const { em, events } = this;
em.trigger(events.component, component, opts);
}
}

39
packages/core/src/navigator/types.ts

@ -0,0 +1,39 @@
import Component from '../dom_components/model/Component';
export interface LayerData {
name: string;
open: boolean;
selected: boolean;
hovered: boolean;
visible: boolean;
locked: boolean;
components: Component[];
}
/**{START_EVENTS}*/
export enum LayerEvents {
/**
* @event `layer:root` Root layer changed. The new root component is passed as an argument to the callback.
* @example
* editor.on('layer:root', (component) => { ... });
*/
root = 'layer:root',
/**
* @event `layer:component` Component layer is updated. The updated component is passed as an argument to the callback.
* @example
* editor.on('layer:component', (component, opts) => { ... });
*/
component = 'layer:component',
/**
* @event `layer:custom` Custom layer event. Object with container and root is passed as an argument to the callback.
* @example
* editor.on('layer:custom', ({ container, root }) => { ... });
*/
custom = 'layer:custom',
}
/**{END_EVENTS}*/
// need this to avoid the TS documentation generator to break
export default LayerEvents;

48
packages/core/src/rich_text_editor/index.ts

@ -11,20 +11,13 @@
* })
* ```
*
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('rte:enable', () => { ... });
*
* // Use the API
* const rte = editor.RichTextEditor;
* rte.add(...);
* ```
*
* ## Available Events
* * `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
* * `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument
* {REPLACE_EVENTS}
*
* ## Methods
* * [add](#add)
@ -39,38 +32,21 @@
import { debounce, isFunction, isString } from 'underscore';
import { Module } from '../abstract';
import CanvasEvents from '../canvas/types';
import { Debounced, DisableOptions, Model } from '../common';
import { ComponentsEvents } from '../dom_components/types';
import ComponentTextView from '../dom_components/view/ComponentTextView';
import EditorModel from '../editor/model/Editor';
import { createEl, cx, on, removeEl } from '../utils/dom';
import { hasWin, isDef } from '../utils/mixins';
import defConfig, { CustomRTE, CustomRteOptions, RichTextEditorConfig } from './config/config';
import RichTextEditor, { RichTextEditorAction } from './model/RichTextEditor';
import CanvasEvents from '../canvas/types';
import { ComponentsEvents } from '../dom_components/types';
import ComponentTextView from '../dom_components/view/ComponentTextView';
import { ModelRTE, RichTextEditorEvents, RteDisableResult } from './types';
export type RichTextEditorEvent = 'rte:enable' | 'rte:disable' | 'rte:custom';
export type { RichTextEditorEvent, RteDisableResult } from './types';
const eventsUp = `${CanvasEvents.refresh} frame:scroll ${ComponentsEvents.update}`;
export const evEnable = 'rte:enable';
export const evDisable = 'rte:disable';
export const evCustom = 'rte:custom';
const events = {
enable: evEnable,
disable: evDisable,
custom: evCustom,
};
interface ModelRTE {
currentView?: ComponentTextView;
}
export interface RteDisableResult {
forceSync?: boolean;
}
export default class RichTextEditorModule extends Module<RichTextEditorConfig & { pStylePrefix?: string }> {
pfx: string;
toolbar!: HTMLElement;
@ -81,7 +57,7 @@ export default class RichTextEditorModule extends Module<RichTextEditorConfig &
customRte?: CustomRTE;
model: Model<ModelRTE>;
__dbdTrgCustom: Debounced;
events = events;
events = RichTextEditorEvents;
/**
* Get configuration object
@ -362,7 +338,7 @@ export default class RichTextEditorModule extends Module<RichTextEditorConfig &
* */
async enable(view: ComponentTextView, rte: RichTextEditor, opts: CustomRteOptions) {
this.lastEl = view.el;
const { customRte, em } = this;
const { customRte, em, events } = this;
const el = view.getChildrenContainer();
this.toolbar.style.display = '';
@ -372,7 +348,7 @@ export default class RichTextEditorModule extends Module<RichTextEditorConfig &
setTimeout(this.updatePosition.bind(this), 0);
em.off(eventsUp, this.updatePosition, this);
em.on(eventsUp, this.updatePosition, this);
em.trigger('rte:enable', view, rteInst);
em.trigger(events.enable, view, rteInst);
}
this.model.set({ currentView: view });
@ -407,7 +383,7 @@ export default class RichTextEditorModule extends Module<RichTextEditorConfig &
* */
async disable(view: ComponentTextView, rte?: RichTextEditor, opts: DisableOptions = {}) {
let result: RteDisableResult = {};
const { em } = this;
const { em, events } = this;
const customRte = this.customRte;
if (customRte) {
@ -423,7 +399,7 @@ export default class RichTextEditorModule extends Module<RichTextEditorConfig &
if (em) {
em.off(eventsUp, this.updatePosition, this);
!opts.fromMove && em.trigger('rte:disable', view, rte);
!opts.fromMove && em.trigger(events.disable, view, rte);
}
this.model.unset('currentView');

39
packages/core/src/rich_text_editor/types.ts

@ -0,0 +1,39 @@
import ComponentTextView from '../dom_components/view/ComponentTextView';
export interface ModelRTE {
currentView?: ComponentTextView;
}
export type RichTextEditorEvent = `${RichTextEditorEvents}`;
export interface RteDisableResult {
forceSync?: boolean;
}
/**{START_EVENTS}*/
export enum RichTextEditorEvents {
/**
* @event `rte:enable` RTE enabled. The view, on which RTE is enabled, and the RTE instance are passed as arguments.
* @example
* editor.on('rte:enable', (view, rte) => { ... });
*/
enable = 'rte:enable',
/**
* @event `rte:disable` RTE disabled. The view, on which RTE is disabled, and the RTE instance are passed as arguments.
* @example
* editor.on('rte:disable', (view, rte) => { ... });
*/
disable = 'rte:disable',
/**
* @event `rte:custom` Custom RTE event. Object with enabled status, container, and actions is passed as an argument.
* @example
* editor.on('rte:custom', ({ enabled, container, actions }) => { ... });
*/
custom = 'rte:custom',
}
/**{END_EVENTS}*/
// need this to avoid the TS documentation generator to break
export default RichTextEditorEvents;

64
packages/core/src/selector_manager/index.ts

@ -29,23 +29,13 @@
* })
* ```
*
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('selector:add', (selector) => { ... });
*
* // Use the API
* const sm = editor.Selectors;
* sm.add(...);
* ```
*
* ## Available Events
* * `selector:add` - Selector added. The [Selector] is passed as an argument to the callback.
* * `selector:remove` - Selector removed. The [Selector] is passed as an argument to the callback.
* * `selector:update` - Selector updated. The [Selector] and the object containing changes are passed as arguments to the callback.
* * `selector:state` - States changed. An object containing all the available data about the triggered event is passed as an argument to the callback.
* * `selector` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
* {REPLACE_EVENTS}
*
* ## Methods
* * [getConfig](#getconfig)
@ -73,47 +63,27 @@
* @module Selectors
*/
import { isString, debounce, isObject, isArray, bindAll } from 'underscore';
import { bindAll, debounce, isArray, isObject, isString } from 'underscore';
import { ItemManagerModule } from '../abstract/Module';
import { Collection, Debounced, Model, RemoveOptions, SetOptions } from '../common';
import Component from '../dom_components/model/Component';
import { ComponentsEvents } from '../dom_components/types';
import StyleableModel from '../domain_abstract/model/StyleableModel';
import EditorModel from '../editor/model/Editor';
import { StyleModuleParam } from '../style_manager';
import { isComponent, isRule } from '../utils/mixins';
import { Model, Collection, RemoveOptions, SetOptions, Debounced } from '../common';
import defConfig, { SelectorManagerConfig } from './config/config';
import Selector from './model/Selector';
import Selectors from './model/Selectors';
import State from './model/State';
import { SelectorEvents, SelectorStringObject } from './types';
import ClassTagsView from './view/ClassTagsView';
import EditorModel from '../editor/model/Editor';
import Component from '../dom_components/model/Component';
import { ItemManagerModule } from '../abstract/Module';
import { StyleModuleParam } from '../style_manager';
import StyleableModel from '../domain_abstract/model/StyleableModel';
import { ComponentsEvents } from '../dom_components/types';
export type SelectorEvent = 'selector:add' | 'selector:remove' | 'selector:update' | 'selector:state' | 'selector';
export type SelectorEvent = `${SelectorEvents}`;
const isId = (str: string) => isString(str) && str[0] == '#';
const isClass = (str: string) => isString(str) && str[0] == '.';
export const evAll = 'selector';
export const evPfx = `${evAll}:`;
export const evAdd = `${evPfx}add`;
export const evUpdate = `${evPfx}update`;
export const evRemove = `${evPfx}remove`;
export const evRemoveBefore = `${evRemove}:before`;
export const evCustom = `${evPfx}custom`;
export const evState = `${evPfx}state`;
const selectorEvents = {
all: evAll,
update: evUpdate,
add: evAdd,
remove: evRemove,
removeBefore: evRemoveBefore,
state: evState,
custom: evCustom,
};
type SelectorStringObject = string | { name?: string; label?: string; type?: number };
export default class SelectorManager extends ItemManagerModule<SelectorManagerConfig & { pStylePrefix?: string }> {
Selector = Selector;
@ -124,7 +94,7 @@ export default class SelectorManager extends ItemManagerModule<SelectorManagerCo
selectorTags?: ClassTagsView;
selected: Selectors;
all: Selectors;
events!: typeof selectorEvents;
events = SelectorEvents;
storageKey = '';
__update: Debounced;
__ctn?: HTMLElement;
@ -137,9 +107,9 @@ export default class SelectorManager extends ItemManagerModule<SelectorManagerCo
*/
constructor(em: EditorModel) {
super(em, 'SelectorManager', new Selectors([]), selectorEvents, defConfig(), { skipListen: true });
super(em, 'SelectorManager', new Selectors([]), SelectorEvents, defConfig(), { skipListen: true });
bindAll(this, '__updateSelectedByComponents');
const { config } = this;
const { config, events } = this;
const ppfx = config.pStylePrefix;
if (ppfx) config.stylePrefix = ppfx + config.stylePrefix;
@ -154,9 +124,9 @@ export default class SelectorManager extends ItemManagerModule<SelectorManagerCo
this.__update = debounce(() => this.__trgCustom(), 0);
this.__initListen({
collections: [this.states, this.selected],
propagate: [{ entity: this.states, event: this.events.state }],
propagate: [{ entity: this.states, event: events.state }],
});
em.on('change:state', (m, value) => em.trigger(evState, value));
em.on('change:state', (m, value) => em.trigger(events.state, value));
this.model.on('change:cFirst', (m, value) => em.trigger('selector:type', value));
const eventCmpUpdateCls = `${ComponentsEvents.update}:classes`;
em.on(`component:toggled ${eventCmpUpdateCls}`, this.__updateSelectedByComponents);

57
packages/core/src/selector_manager/types.ts

@ -0,0 +1,57 @@
/**{START_EVENTS}*/
export enum SelectorEvents {
/**
* @event `selector:add` Selector added. The Selector is passed as an argument to the callback.
* @example
* editor.on('selector:add', (selector) => { ... });
*/
add = 'selector:add',
/**
* @event `selector:remove` Selector removed. The Selector is passed as an argument to the callback.
* @example
* editor.on('selector:remove', (selector) => { ... });
*/
remove = 'selector:remove',
/**
* @event `selector:remove:before` Before selector remove. The Selector is passed as an argument to the callback.
* @example
* editor.on('selector:remove:before', (selector) => { ... });
*/
removeBefore = 'selector:remove:before',
/**
* @event `selector:update` Selector updated. The Selector and the object containing changes are passed as arguments to the callback.
* @example
* editor.on('selector:update', (selector, changes) => { ... });
*/
update = 'selector:update',
/**
* @event `selector:state` States changed. An object containing all the available data about the triggered event is passed as an argument to the callback.
* @example
* editor.on('selector:state', (state) => { ... });
*/
state = 'selector:state',
/**
* @event `selector:custom` Custom selector event. An object containing states, selected selectors, and container is passed as an argument.
* @example
* editor.on('selector:custom', ({ states, selected, container }) => { ... });
*/
custom = 'selector:custom',
/**
* @event `selector` Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
* @example
* editor.on('selector', ({ event, selector, changes, ... }) => { ... });
*/
all = 'selector',
}
/**{END_EVENTS}*/
export type SelectorStringObject = string | { name?: string; label?: string; type?: number };
// need this to avoid the TS documentation generator to break
export default SelectorEvents;

106
packages/core/src/style_manager/index.ts

@ -9,29 +9,13 @@
* })
* ```
*
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('style:sector:add', (sector) => { ... });
*
* // Use the API
* const styleManager = editor.StyleManager;
* styleManager.addSector(...);
* ```
* ## Available Events
* * `style:sector:add` - Sector added. The [Sector] is passed as an argument to the callback.
* * `style:sector:remove` - Sector removed. The [Sector] is passed as an argument to the callback.
* * `style:sector:update` - Sector updated. The [Sector] and the object containing changes are passed as arguments to the callback.
* * `style:property:add` - Property added. The [Property] is passed as an argument to the callback.
* * `style:property:remove` - Property removed. The [Property] is passed as an argument to the callback.
* * `style:property:update` - Property updated. The [Property] and the object containing changes are passed as arguments to the callback.
* * `style:target` - Target selection changed. The target (or `null` in case the target is deselected) is passed as an argument to the callback.
* <!--
* * `styleManager:update:target` - The target (Component or CSSRule) is changed
* * `styleManager:change` - Triggered on style property change from new selected component, the view of the property is passed as an argument to the callback
* * `styleManager:change:{propertyName}` - As above but for a specific style property
* -->
*
* {REPLACE_EVENTS}
*
* ## Methods
* * [getConfig](#getconfig)
@ -63,71 +47,32 @@
* @module docsjs.StyleManager
*/
import { isUndefined, isArray, isString, debounce, bindAll } from 'underscore';
import { isComponent } from '../utils/mixins';
import { bindAll, debounce, isArray, isString, isUndefined } from 'underscore';
import { ItemManagerModule } from '../abstract/Module';
import { AddOptions, Debounced, Model } from '../common';
import CssRule from '../css_composer/model/CssRule';
import Component from '../dom_components/model/Component';
import { ComponentsEvents } from '../dom_components/types';
import StyleableModel, { StyleProps } from '../domain_abstract/model/StyleableModel';
import EditorModel from '../editor/model/Editor';
import { isComponent } from '../utils/mixins';
import defConfig, { StyleManagerConfig } from './config/config';
import Sector, { SectorProperties } from './model/Sector';
import Sectors from './model/Sectors';
import Properties from './model/Properties';
import PropertyFactory from './model/PropertyFactory';
import SectorsView from './view/SectorsView';
import { ItemManagerModule } from '../abstract/Module';
import EditorModel from '../editor/model/Editor';
import Property, { PropertyProps } from './model/Property';
import Component from '../dom_components/model/Component';
import CssRule from '../css_composer/model/CssRule';
import StyleableModel, { StyleProps } from '../domain_abstract/model/StyleableModel';
import { CustomPropertyView } from './view/PropertyView';
import { PropertySelectProps } from './model/PropertySelect';
import { PropertyNumberProps } from './model/PropertyNumber';
import PropertyStack, { PropertyStackProps } from './model/PropertyStack';
import PropertyComposite from './model/PropertyComposite';
import { ComponentsEvents } from '../dom_components/types';
import PropertyFactory from './model/PropertyFactory';
import PropertyStack from './model/PropertyStack';
import Sector, { SectorProperties } from './model/Sector';
import Sectors from './model/Sectors';
import { PropertyTypes, StyleManagerEvents, StyleTarget } from './types';
import { CustomPropertyView } from './view/PropertyView';
import SectorsView from './view/SectorsView';
export type PropertyTypes = PropertyStackProps | PropertySelectProps | PropertyNumberProps;
export type StyleManagerEvent =
| 'style:sector:add'
| 'style:sector:remove'
| 'style:sector:update'
| 'style:property:add'
| 'style:property:remove'
| 'style:property:update'
| 'style:target';
export type StyleTarget = StyleableModel;
export const evAll = 'style';
export const evPfx = `${evAll}:`;
export const evSector = `${evPfx}sector`;
export const evSectorAdd = `${evSector}:add`;
export const evSectorRemove = `${evSector}:remove`;
export const evSectorUpdate = `${evSector}:update`;
export const evProp = `${evPfx}property`;
export const evPropAdd = `${evProp}:add`;
export const evPropRemove = `${evProp}:remove`;
export const evPropUp = `${evProp}:update`;
export const evLayerSelect = `${evPfx}layer:select`;
export const evTarget = `${evPfx}target`;
export const evCustom = `${evPfx}custom`;
export type StyleModuleParam<T extends keyof StyleManager, N extends number> = Parameters<StyleManager[T]>[N];
export type { PropertyTypes, StyleModuleParam, StyleTarget } from './types';
const propDef = (value: any) => value || value === 0;
export type StyleManagerEvent = `${StyleManagerEvents}`;
const stylesEvents = {
all: evAll,
sectorAdd: evSectorAdd,
sectorRemove: evSectorRemove,
sectorUpdate: evSectorUpdate,
propertyAdd: evPropAdd,
propertyRemove: evPropRemove,
propertyUpdate: evPropUp,
layerSelect: evLayerSelect,
target: evTarget,
custom: evCustom,
};
const propDef = (value: any) => value || value === 0;
export default class StyleManager extends ItemManagerModule<
StyleManagerConfig,
@ -137,7 +82,7 @@ export default class StyleManager extends ItemManagerModule<
builtIn: PropertyFactory;
upAll: Debounced;
properties: typeof Properties;
events!: typeof stylesEvents;
events = StyleManagerEvents;
sectors: Sectors;
SectView!: SectorsView;
Sector = Sector;
@ -157,8 +102,9 @@ export default class StyleManager extends ItemManagerModule<
* @private
*/
constructor(em: EditorModel) {
super(em, 'StyleManager', new Sectors([], { em }), stylesEvents, defConfig());
super(em, 'StyleManager', new Sectors([], { em }), StyleManagerEvents, defConfig());
bindAll(this, '__clearStateTarget');
const { events } = this;
const c = this.config;
const ppfx = c.pStylePrefix;
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
@ -185,10 +131,10 @@ export default class StyleManager extends ItemManagerModule<
// Triggers only custom event
const trgCustom = debounce(() => this.__trgCustom(), 0);
model.listenTo(em, `${evLayerSelect} ${evTarget}`, trgCustom);
model.listenTo(em, `${events.layerSelect} ${events.target}`, trgCustom);
// Other listeners
model.on('change:lastTarget', () => em.trigger(evTarget, this.getSelected()));
model.on('change:lastTarget', () => em.trigger(events.target, this.getSelected()));
}
__upSel() {

88
packages/core/src/style_manager/types.ts

@ -0,0 +1,88 @@
import StyleManager from '.';
import StyleableModel from '../domain_abstract/model/StyleableModel';
import { PropertyNumberProps } from './model/PropertyNumber';
import { PropertySelectProps } from './model/PropertySelect';
import { PropertyStackProps } from './model/PropertyStack';
export type PropertyTypes = PropertyStackProps | PropertySelectProps | PropertyNumberProps;
export type StyleTarget = StyleableModel;
export type StyleModuleParam<T extends keyof StyleManager, N extends number> = Parameters<StyleManager[T]>[N];
/**{START_EVENTS}*/
export enum StyleManagerEvents {
/**
* @event `style:sector:add` Sector added. The Sector is passed as an argument to the callback.
* @example
* editor.on('style:sector:add', (sector) => { ... });
*/
sectorAdd = 'style:sector:add',
/**
* @event `style:sector:remove` Sector removed. The Sector is passed as an argument to the callback.
* @example
* editor.on('style:sector:remove', (sector) => { ... });
*/
sectorRemove = 'style:sector:remove',
/**
* @event `style:sector:update` Sector updated. The Sector and the object containing changes are passed as arguments to the callback.
* @example
* editor.on('style:sector:update', (sector, changes) => { ... });
*/
sectorUpdate = 'style:sector:update',
/**
* @event `style:property:add` Property added. The Property is passed as an argument to the callback.
* @example
* editor.on('style:property:add', (property) => { ... });
*/
propertyAdd = 'style:property:add',
/**
* @event `style:property:remove` Property removed. The Property is passed as an argument to the callback.
* @example
* editor.on('style:property:remove', (property) => { ... });
*/
propertyRemove = 'style:property:remove',
/**
* @event `style:property:update` Property updated. The Property and the object containing changes are passed as arguments to the callback.
* @example
* editor.on('style:property:update', (property, changes) => { ... });
*/
propertyUpdate = 'style:property:update',
/**
* @event `style:target` Target selection changed. The target (or null in case the target is deselected) is passed as an argument to the callback.
* @example
* editor.on('style:target', (target) => { ... });
*/
target = 'style:target',
/**
* @event `style:layer:select` Layer selected. Object containing layer data is passed as an argument.
* @example
* editor.on('style:layer:select', (data) => { ... });
*/
layerSelect = 'style:layer:select',
/**
* @event `style:custom` Custom style event. Object containing all custom data is passed as an argument.
* @example
* editor.on('style:custom', ({ container }) => { ... });
*/
custom = 'style:custom',
/**
* @event `style` Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
* @example
* editor.on('style', ({ event, sector, property, ... }) => { ... });
*/
all = 'style',
}
/**{END_EVENTS}*/
// need this to avoid the TS documentation generator to break
export default StyleManagerEvents;

34
packages/core/src/utils/mixins.ts

@ -25,7 +25,7 @@ function castPath(value: string | string[], object: ObjectAny) {
return object.hasOwnProperty(value) ? [value] : stringToPath(value);
}
export const get = (object: ObjectAny, path: string | string[], def: any) => {
export const get = (object: ObjectAny, path: string | string[], def?: any) => {
const paths = castPath(path, object);
const length = paths.length;
let index = 0;
@ -36,6 +36,38 @@ export const get = (object: ObjectAny, path: string | string[], def: any) => {
return (index && index == length ? object : undefined) ?? def;
};
export const set = (object: ObjectAny, path: string | string[], value: any): boolean => {
if (!isObject(object)) return false;
const paths = castPath(path, object);
const length = paths.length;
if (length === 0) return false;
if (length === 1) {
object[paths[0]] = value;
return true;
}
const parentPath = paths.slice(0, -1);
const lastKey = paths[length - 1];
const parent = get(object, parentPath);
if (parent) {
if (Array.isArray(parent)) {
const index = +lastKey;
if (!isNaN(index)) {
parent[index] = value;
return true;
}
} else if (isObject(parent)) {
(parent as ObjectAny)[lastKey] = value;
return true;
}
}
return false;
};
export const serialize = (obj: ObjectAny) => JSON.parse(JSON.stringify(obj));
export const isBultInMethod = (key: string) => isFunction(obj[key]);

107
packages/core/test/specs/data_sources/index.ts

@ -1,12 +1,12 @@
import DataSourceManager from '../../../src/data_sources';
import { DataSourceProps } from '../../../src/data_sources/types';
import { setupTestEditor } from '../../common';
import EditorModel from '../../../src/editor/model/Editor';
import { setupTestEditor } from '../../common';
describe('DataSourceManager', () => {
let em: EditorModel;
let dsm: DataSourceManager;
type Record = { id: string; name: string };
type Record = { id: string; name: string; metadata?: any };
const dsTest: DataSourceProps<Record> = {
id: 'ds1',
records: [
@ -35,7 +35,7 @@ describe('DataSourceManager', () => {
em.on(dsm.events.add, eventAdd);
const ds = addDataSource();
expect(dsm.getAll().length).toBe(1);
expect(eventAdd).toBeCalledTimes(1);
expect(eventAdd).toHaveBeenCalledTimes(1);
expect(ds.getRecords().length).toBe(3);
});
@ -50,7 +50,104 @@ describe('DataSourceManager', () => {
const ds = addDataSource();
dsm.remove('ds1');
expect(dsm.getAll().length).toBe(0);
expect(event).toBeCalledTimes(1);
expect(event).toBeCalledWith(ds, expect.any(Object));
expect(event).toHaveBeenCalledTimes(1);
expect(event).toHaveBeenCalledWith(ds, expect.any(Object));
});
describe('getValue', () => {
test('get value from records', () => {
const ds = addDataSource();
const testPath = ds.getRecord('id2')?.getPath('name') || '';
expect(dsm.getValue(testPath)).toBe('Name2');
expect(dsm.getValue(`ds1.id1.name`)).toBe('Name1');
expect(dsm.getValue(`ds1[id1]name`)).toBe('Name1');
expect(dsm.getValue(`ds1.non-existing.name`)).toBeUndefined();
expect(dsm.getValue(`ds1.non-existing.name`, 'Default name')).toBe('Default name');
expect(dsm.getValue(`ds1.id1.nonExisting`)).toBeUndefined();
expect(dsm.getValue('non-existing-ds.id1.name')).toBeUndefined();
});
test('with nested values', () => {
const ds = addDataSource();
const address = { city: 'CityName' };
const roles = ['admin', 'user'];
ds.addRecord({
id: 'id4',
name: 'Name4',
metadata: { address, roles },
});
expect(dsm.getValue(`ds1.id4.metadata.address`)).toEqual(address);
expect(dsm.getValue(`ds1.id4.metadata.address.city`)).toEqual(address.city);
expect(dsm.getValue(`ds1.id4.metadata.roles`)).toEqual(roles);
expect(dsm.getValue(`ds1.id4.metadata.roles[1]`)).toEqual(roles[1]);
});
});
describe('setValue', () => {
test('set value in existing record', () => {
addDataSource();
expect(dsm.setValue('ds1.id1.name', 'Name1 Up')).toBe(true);
expect(dsm.getValue('ds1.id1.name')).toBe('Name1 Up');
expect(dsm.setValue('ds1.id1.newField', 'new field value')).toBe(true);
expect(dsm.getValue('ds1.id1.newField')).toBe('new field value');
expect(dsm.setValue('non-existing-ds.id1.name', 'New Name')).toBe(false);
expect(dsm.setValue('non-existing-ds.none.name', 'New Name')).toBe(false);
expect(dsm.setValue('invalid-path', 'New Name')).toBe(false);
});
test('set nested values', () => {
const ds = addDataSource();
const address = { city: 'CityName' };
const roles = ['admin', 'user', 'member'];
const newObj = { newValue: '1' };
ds.addRecord({
id: 'id4',
name: 'Name4',
metadata: { address, roles },
});
// Check object updates
expect(dsm.setValue('ds1.id4.metadata.address.city', 'NewCity')).toBe(true);
expect(dsm.getValue('ds1.id4.metadata.address.city')).toBe('NewCity');
expect(dsm.setValue('ds1.id4.metadata.newObj', newObj)).toBe(true);
expect(dsm.getValue('ds1.id4.metadata.newObj')).toEqual(newObj);
// Check array updates
expect(dsm.setValue('ds1.id4.metadata.roles[1]', 'editor')).toBe(true);
expect(dsm.getValue('ds1.id4.metadata')).toEqual({
newObj: { newValue: '1' },
address: { city: 'NewCity' },
roles: ['admin', 'editor', 'member'],
});
// Set entirely new nested object
const newAddress = { city: 'AnotherCity', country: 'SomeCountry' };
expect(dsm.setValue('ds1.id4.metadata.address', newAddress)).toBe(true);
expect(dsm.getValue('ds1.id4.metadata.address')).toEqual(newAddress);
const newRoles = ['editor', 'viewer'];
expect(dsm.setValue('ds1.id4.metadata.roles', newRoles)).toBe(true);
expect(dsm.getValue('ds1.id4.metadata.roles')).toEqual(newRoles);
expect(dsm.getValue('ds1.id4.metadata')).toEqual({
newObj: { newValue: '1' },
address: { city: 'AnotherCity', country: 'SomeCountry' },
roles: ['editor', 'viewer'],
});
// Set completely new nested structure
const newMetadata = { tags: ['tag1', 'tag2'], settings: { theme: 'dark' } };
expect(dsm.setValue('ds1.id4.metadata', newMetadata)).toBe(true);
expect(dsm.getValue('ds1.id4.metadata')).toEqual(newMetadata);
expect(dsm.getValue('ds1.id4.metadata.settings.theme')).toBe('dark');
expect(dsm.getValue('ds1.id4.metadata')).toEqual({
tags: ['tag1', 'tag2'],
settings: { theme: 'dark' },
});
});
});
});

295
packages/core/test/specs/data_sources/model/DataSource.ts

@ -0,0 +1,295 @@
import { DataSourceManager } from '../../../../src';
import DataSource from '../../../../src/data_sources/model/DataSource';
import {
DataFieldPrimitiveType,
DataFieldSchemaNumber,
DataFieldSchemaString,
DataRecordProps,
DataSourceProviderResult,
} from '../../../../src/data_sources/types';
import Editor from '../../../../src/editor/model/Editor';
import { setupTestEditor } from '../../../common';
interface TestRecord extends DataRecordProps {
name?: string;
age?: number;
}
const serializeRecords = (records: any[]) => JSON.parse(JSON.stringify(records));
describe('DataSource', () => {
let em: Editor;
let editor: Editor['Editor'];
let dsm: DataSourceManager;
let ds: DataSource<TestRecord>;
const categoryRecords = [
{ id: 'cat1', uid: 'cat1-uid', name: 'Category 1' },
{ id: 'cat2', uid: 'cat2-uid', name: 'Category 2' },
{ id: 'cat3', uid: 'cat3-uid', name: 'Category 3' },
];
const userRecords = [
{ id: 'user1', username: 'user_one' },
{ id: 'user2', username: 'user_two' },
{ id: 'user3', username: 'user_three' },
];
const blogRecords = [
{ id: 'blog1', title: 'First Blog', author: 'user1', categories: ['cat1-uid'] },
{ id: 'blog2', title: 'Second Blog', author: 'user2' },
{ id: 'blog3', title: 'Third Blog', categories: ['cat1-uid', 'cat3-uid'] },
];
const addTestDataSource = (records?: TestRecord[]) => {
return dsm.add<TestRecord>({ id: 'test', records: records || [{ id: 'user1', age: 30 }] });
};
beforeEach(() => {
({ em, dsm, editor } = setupTestEditor());
});
afterEach(() => {
em.destroy();
});
describe('Schema', () => {
const schemaName: DataFieldSchemaString = {
type: DataFieldPrimitiveType.string,
label: 'Name',
};
const schemaAge: DataFieldSchemaNumber = {
type: DataFieldPrimitiveType.number,
label: 'Age',
default: 18,
};
beforeEach(() => {
ds = addTestDataSource();
});
test('Initialize with empty schema', () => {
expect(ds.schema).toEqual({});
});
test('Add and update schema', () => {
const schemaNameDef: typeof ds.schema = { name: schemaName };
const schemaAgeDef: typeof ds.schema = { age: schemaAge };
ds.upSchema(schemaNameDef);
ds.upSchema(schemaAgeDef);
expect(ds.schema).toEqual({ ...schemaNameDef, ...schemaAgeDef });
});
test('Should update existing field schema', () => {
ds.upSchema({ name: schemaName });
const updatedSchema: typeof ds.schema = {
name: {
...schemaName,
description: 'User name field',
},
};
ds.upSchema(updatedSchema);
expect(ds.schema).toEqual(updatedSchema);
});
test('Should get field schema', () => {
ds.upSchema({
name: schemaName,
age: schemaAge,
});
expect(ds.getSchemaField('name')).toEqual(schemaName);
expect(ds.getSchemaField('age')).toEqual(schemaAge);
expect(ds.getSchemaField('nonExistentField')).toBeUndefined();
});
describe('Relations', () => {
beforeEach(() => {
dsm.add({
id: 'categories',
records: categoryRecords,
});
dsm.add({
id: 'users',
records: userRecords,
});
dsm.add({
id: 'blogs',
records: blogRecords,
schema: {
title: {
type: DataFieldPrimitiveType.string,
},
author: {
type: DataFieldPrimitiveType.relation,
target: 'users',
targetField: 'id',
},
},
});
});
test('return default values', () => {
const blogsDS = dsm.get('blogs');
expect(serializeRecords(blogsDS.getRecords())).toEqual(blogRecords);
});
test('return 1:1 resolved values', () => {
const blogsDS = dsm.get('blogs');
const records = blogsDS.getResolvedRecords();
expect(records).toEqual([
{ ...blogRecords[0], author: userRecords[0] },
{ ...blogRecords[1], author: userRecords[1] },
blogRecords[2],
]);
});
test('return 1:many resolved values', () => {
const blogsDS = dsm.get('blogs');
blogsDS.upSchema({
categories: {
type: DataFieldPrimitiveType.relation,
target: 'categories',
targetField: 'uid',
isMany: true,
},
});
const records = blogsDS.getResolvedRecords();
expect(records).toEqual([
{ ...blogRecords[0], author: userRecords[0], categories: [categoryRecords[0]] },
{ ...blogRecords[1], author: userRecords[1] },
{ ...blogRecords[2], categories: [categoryRecords[0], categoryRecords[2]] },
]);
});
});
});
describe('Providers', () => {
const testApiUrl = 'https://api.example.com/data';
const testHeaders = { 'Content-Type': 'application/json' };
const getMockSchema = () => ({
author: {
type: DataFieldPrimitiveType.relation,
target: 'users',
targetField: 'id',
},
});
const getMockProviderResponse: () => DataSourceProviderResult = () => ({
records: blogRecords,
schema: getMockSchema(),
});
const getProviderBlogsGet = () => ({ url: testApiUrl, headers: testHeaders });
const addBlogsWithProvider = () => {
return dsm.add({
id: 'blogs',
name: 'My blogs',
provider: {
get: getProviderBlogsGet(),
},
});
};
beforeEach(() => {
jest.spyOn(global, 'fetch').mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(getMockProviderResponse()),
} as Response),
);
dsm.add({
id: 'categories',
records: categoryRecords,
});
dsm.add({
id: 'users',
records: userRecords,
});
});
afterEach(() => {
jest.restoreAllMocks();
});
test('loadProvider', async () => {
const ds = addBlogsWithProvider();
await ds.loadProvider();
expect(fetch).toHaveBeenCalledWith(testApiUrl, { headers: testHeaders });
expect(ds.schema).toEqual(getMockSchema());
expect(ds.getResolvedRecords()).toEqual([
{ ...blogRecords[0], author: userRecords[0] },
{ ...blogRecords[1], author: userRecords[1] },
blogRecords[2],
]);
});
test('loadProvider with failed fetch', async () => {
jest.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));
em.config.log = false;
const ds = addBlogsWithProvider();
await ds.loadProvider();
expect(fetch).toHaveBeenCalledWith(testApiUrl, { headers: testHeaders });
expect(ds.schema).toEqual({});
expect(ds.getRecords().length).toBe(0);
});
test('records loaded from the provider are not persisted', async () => {
const ds = addBlogsWithProvider();
const eventLoad = jest.fn();
em.on(dsm.events.providerLoad, eventLoad);
await ds.loadProvider();
expect(editor.getProjectData().dataSources).toEqual([
{ id: 'categories', records: categoryRecords },
{ id: 'users', records: userRecords },
{
id: 'blogs',
name: 'My blogs',
schema: getMockSchema(),
provider: { get: getProviderBlogsGet() },
},
]);
expect(eventLoad).toHaveBeenCalledTimes(1);
expect(eventLoad).toHaveBeenCalledWith({
dataSource: ds,
result: getMockProviderResponse(),
});
});
test('load providers on project load', (done) => {
dsm.getConfig().autoloadProviders = true;
editor.on(dsm.events.providerLoadAll, () => {
expect(dsm.get('blogs').getResolvedRecords()).toEqual([
{ ...blogRecords[0], author: userRecords[0] },
{ ...blogRecords[1], author: userRecords[1] },
blogRecords[2],
]);
expect(editor.getProjectData().dataSources).toEqual([
{ id: 'categories', records: categoryRecords },
{ id: 'users', records: userRecords },
{
id: 'blogs',
schema: getMockSchema(),
provider: { get: testApiUrl },
},
]);
done();
});
editor.loadProjectData({
dataSources: [
{ id: 'categories', records: categoryRecords },
{ id: 'users', records: userRecords },
{
id: 'blogs',
provider: { get: testApiUrl },
},
],
});
});
});
});
Loading…
Cancel
Save