Browse Source

Merge branch 'dev' into feature/data-sources

ci/dependabot
danstarns 2 years ago
parent
commit
5f8c188367
  1. 30
      CONTRIBUTING.md
  2. 10
      README.md
  3. 2
      dist/css/grapes.min.css
  4. 4
      dist/grapes.min.js
  5. 2
      dist/grapes.min.js.map
  6. 16407
      dist/grapes.mjs
  7. 2
      dist/grapes.mjs.map
  8. 4940
      dist/index.d.ts
  9. 9
      docs/.vuepress/components/DemoViewer.vue
  10. 5
      docs/.vuepress/config.js
  11. 11
      docs/.vuepress/public/symbols-model.svg
  12. 77
      docs/api/assets.md
  13. 6
      docs/api/block_manager.md
  14. 18
      docs/api/canvas.md
  15. 61
      docs/api/commands.md
  16. 30
      docs/api/component.md
  17. 71
      docs/api/components.md
  18. 21
      docs/api/i18n.md
  19. 6
      docs/api/trait_manager.md
  20. 18
      docs/api/undo_manager.md
  21. 12
      docs/deploy.sh
  22. 306
      docs/guides/Symbols.md
  23. 2
      docs/modules/Assets.md
  24. 16
      docs/modules/Commands.md
  25. 4
      docs/modules/I18n.md
  26. 2
      docs/modules/Plugins.md
  27. 2
      package.json
  28. 20
      src/abstract/ModuleCategories.ts
  29. 153
      src/asset_manager/index.ts
  30. 95
      src/asset_manager/types.ts
  31. 11
      src/asset_manager/view/FileUploader.ts
  32. 5
      src/block_manager/index.ts
  33. 2
      src/block_manager/model/Block.ts
  34. 7
      src/block_manager/types.ts
  35. 20
      src/canvas/index.ts
  36. 2
      src/canvas/model/Canvas.ts
  37. 4
      src/canvas/model/CanvasSpots.ts
  38. 18
      src/canvas/types.ts
  39. 2
      src/canvas/view/CanvasView.ts
  40. 18
      src/canvas/view/FrameView.ts
  41. 11
      src/code_manager/model/HtmlGenerator.ts
  42. 23
      src/commands/index.ts
  43. 71
      src/commands/types.ts
  44. 33
      src/commands/view/CommandAbstract.ts
  45. 2
      src/commands/view/Fullscreen.ts
  46. 49
      src/commands/view/SelectComponent.ts
  47. 3
      src/commands/view/ShowOffset.ts
  48. 9
      src/common/index.ts
  49. 5
      src/css_composer/index.ts
  50. 18
      src/css_composer/model/CssRule.ts
  51. 151
      src/dom_components/index.ts
  52. 426
      src/dom_components/model/Component.ts
  53. 24
      src/dom_components/model/ComponentHead.ts
  54. 5
      src/dom_components/model/ComponentImage.ts
  55. 6
      src/dom_components/model/ComponentMap.ts
  56. 5
      src/dom_components/model/ComponentTable.ts
  57. 5
      src/dom_components/model/ComponentTableBody.ts
  58. 5
      src/dom_components/model/ComponentText.ts
  59. 6
      src/dom_components/model/ComponentVideo.ts
  60. 47
      src/dom_components/model/ComponentWrapper.ts
  61. 145
      src/dom_components/model/Components.ts
  62. 282
      src/dom_components/model/SymbolUtils.ts
  63. 56
      src/dom_components/model/Symbols.ts
  64. 33
      src/dom_components/model/types.ts
  65. 106
      src/dom_components/types.ts
  66. 2
      src/dom_components/view/ComponentImageView.ts
  67. 4
      src/dom_components/view/ComponentLabelView.ts
  68. 4
      src/dom_components/view/ComponentLinkView.ts
  69. 2
      src/dom_components/view/ComponentTextView.ts
  70. 3
      src/dom_components/view/ComponentView.ts
  71. 18
      src/dom_components/view/ComponentsView.ts
  72. 6
      src/domain_abstract/model/StyleableModel.ts
  73. 10
      src/domain_abstract/ui/InputColor.ts
  74. 4
      src/domain_abstract/ui/InputNumber.ts
  75. 10
      src/editor/config/config.ts
  76. 3
      src/editor/index.ts
  77. 4
      src/editor/model/Editor.ts
  78. 22
      src/editor/view/EditorView.ts
  79. 29
      src/i18n/index.ts
  80. 169
      src/i18n/locale/id.js
  81. 31
      src/i18n/types.ts
  82. 8
      src/navigator/index.ts
  83. 39
      src/navigator/view/ItemView.ts
  84. 59
      src/navigator/view/ItemsView.ts
  85. 8
      src/pages/index.ts
  86. 11
      src/pages/types.ts
  87. 2
      src/panels/view/PanelView.ts
  88. 33
      src/parser/config/config.ts
  89. 2
      src/parser/index.ts
  90. 2
      src/parser/model/BrowserParserHtml.ts
  91. 338
      src/parser/model/ParserHtml.ts
  92. 4
      src/rich_text_editor/index.ts
  93. 7
      src/selector_manager/index.ts
  94. 20
      src/selector_manager/model/Selector.ts
  95. 7
      src/selector_manager/view/ClassTagsView.ts
  96. 38
      src/style_manager/index.ts
  97. 4
      src/styles/scss/_gjs_canvas.scss
  98. 32
      src/styles/scss/main.scss
  99. 9
      src/trait_manager/model/Traits.ts
  100. 7
      src/trait_manager/types.ts

30
CONTRIBUTING.md

@ -11,6 +11,36 @@ We welcome any type of contribution, not only code. Like for example:
- **Money**: We welcome financial contributions in full transparency on our [Open Collective].
## Setting up the repository
This is a Node.js project and you need to have Node.js installed on your machine. You can download it from [here](https://nodejs.org/). We test versions 14 and 16 of Node in the CI, so it's recommended to use one of these versions, or the latest of: 16.20.2
```bash
nvm use 16.20.2
```
You will then use `yarn` to manage the dependencies and run the scripts. You can install it by running:
```bash
npm install -g yarn
```
Then you can clone the repository and install the dependencies:
```bash
git clone __YOUR_FORK__
cd grapesjs
yarn
```
Finally, you can run the development server:
```bash
yarn start
```
Navigate to `http://localhost:8080/` to see the editor in action. The development server will watch for changes in the code and automatically reload the page.
## Your First Contribution
Working on your first Pull Request? You can learn how from this **free** series, [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github).

10
README.md

@ -6,13 +6,13 @@
[![npm](https://img.shields.io/npm/v/grapesjs.svg)](https://www.npmjs.com/package/grapesjs)
<p align="center"><img src="http://grapesjs.com/img/grapesjs-front-page-m.jpg" alt="GrapesJS" width="500" align="center"/></p>
<p align="center"><img src="http://grapesjs.com/assets/images/grapesjs-front-page-m.jpg" alt="GrapesJS" width="500" align="center"/></p>
GrapesJS is a free and open source Web Builder Framework which helps building HTML templates, faster and easily, to be delivered in sites, newsletters or mobile apps. Mainly, GrapesJS was designed to be used inside a [CMS] to speed up the creation of dynamic templates. To better understand this concept check the image below
<br/>
<p align="center"><img src="http://grapesjs.com/img/gjs-concept.png" alt="GrapesJS - Style Manager" height="400" align="center"/></p>
<p align="center"><img src="http://grapesjs.com/assets/images/gjs-concept.png" alt="GrapesJS - Style Manager" height="400" align="center"/></p>
<br/>
Generally any 'template system', that you'd find in various applications like CMS, is composed by the **structure** (HTML), **style** (CSS) and **variables**, which are then replaced with other templates and contents on server-side and rendered on client.
@ -47,11 +47,11 @@ Newsletter Demo - http://grapesjs.com/demo-newsletter-editor.html<br/>
| Blocks | Style Manager | Layer Manager |
|--|--|--|
|<img src="http://grapesjs.com/img/sc-grapesjs-blocks-prp.jpg" alt="GrapesJS - Block Manager" height="400" align="center"/>|<img src="http://grapesjs.com/img/sc-grapesjs-style-2.jpg" alt="GrapesJS - Style Manager" height="400" align="center"/>|<img src="http://grapesjs.com/img/sc-grapesjs-layers-2.jpg" alt="GrapesJS - Layer Manager" height="400" align="center"/>|
|<img src="http://grapesjs.com/assets/images/sc-grapesjs-blocks-prp.jpg" alt="GrapesJS - Block Manager" height="400" align="center"/>|<img src="http://grapesjs.com/assets/images/sc-grapesjs-style-2.jpg" alt="GrapesJS - Style Manager" height="400" align="center"/>|<img src="http://grapesjs.com/assets/images/sc-grapesjs-layers-2.jpg" alt="GrapesJS - Layer Manager" height="400" align="center"/>|
| Code Viewer | Asset Manager |
|--|--|
|<img src="http://grapesjs.com/img/sc-grapesjs-code.jpg" alt="GrapesJS - Code Viewer" height="300" align="center"/>|<img src="http://grapesjs.com/img/sc-grapesjs-assets-1.jpg" alt="GrapesJS - Asset Manager" height="250" align="center"/>|
|<img src="http://grapesjs.com/assets/images/sc-grapesjs-code.jpg" alt="GrapesJS - Code Viewer" height="300" align="center"/>|<img src="http://grapesjs.com/assets/images/sc-grapesjs-assets-1.jpg" alt="GrapesJS - Asset Manager" height="250" align="center"/>|
* Local and remote storage
@ -203,7 +203,7 @@ Find out more about plugins here: [Creating plugins](https://grapesjs.com/docs/m
If you like the project and you wish to see it grow, please consider supporting us with a donation of your choice or become a backer/sponsor via [Open Collective](https://opencollective.com/grapesjs)
[![PayPalMe](http://grapesjs.com/img/ppme.png)](https://paypal.me/grapesjs)
[![PayPalMe](http://grapesjs.com/assets/images/ppme.png)](https://paypal.me/grapesjs)
[![Bitcoin](https://user-images.githubusercontent.com/11614725/52977952-87235f80-33cf-11e9-9607-7a9a354e1155.png)](https://commerce.coinbase.com/checkout/fc90b940-558d-408b-a166-28a823c98173)
<a href="https://opencollective.com/grapesjs"><img src="https://opencollective.com/grapesjs/tiers/sponsors.svg?avatarHeight=64"></a>

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

4
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

16407
dist/grapes.mjs

File diff suppressed because it is too large

2
dist/grapes.mjs.map

File diff suppressed because one or more lines are too long

4940
dist/index.d.ts

File diff suppressed because it is too large

9
docs/.vuepress/components/DemoViewer.vue

@ -26,12 +26,17 @@ export default {
type: Boolean,
default: false,
},
show: {
type: Boolean,
default: false,
},
},
computed: {
src() {
const { value, user, darkcode } = this;
const { value, user, darkcode, show } = this;
const tabs = show ? 'result,js,html,css' : 'js,html,css,result';
const dcStr = darkcode ? '/dark/?menuColor=fff&fontColor=333&accentColor=e67891' : '';
return `//jsfiddle.net/${user}/${value}/embedded/js,html,css,result${dcStr}`;
return `//jsfiddle.net/${user}/${value}/embedded/${tabs}${dcStr}`;
}
}
}

5
docs/.vuepress/config.js

@ -11,8 +11,8 @@ module.exports = {
serviceWorker: false, // Enable Service Worker for offline usage
head: [
['link', { rel: 'icon', href: '/logo-icon.png' }],
['link', { rel: 'stylesheet', href: isDev ? `${devPath}/dist/css/grapes.min.css` : `${baseUrl}/stylesheets/grapes.min.css?v${version}` }],
['script', { src: isDev ? `${devPath}/grapes.min.js` : `${baseUrl}/js/grapes.min.js?v${version}` }],
['link', { rel: 'stylesheet', href: isDev ? `${devPath}/dist/css/grapes.min.css` : `${baseUrl}/assets/styles/grapesjs/grapes.min.css?${version}` }],
['script', { src: isDev ? `${devPath}/grapes.min.js` : `${baseUrl}/assets/scripts/grapesjs/grapes.min.js?${version}` }],
],
localesSKIP: {
'/': {
@ -125,6 +125,7 @@ module.exports = {
title: 'Guides',
collapsable: false,
children: [
['/guides/Symbols', 'Symbols'],
['/guides/Replace-Rich-Text-Editor', 'Replace Rich Text Editor'],
['/guides/Custom-CSS-parser', 'Use Custom CSS Parser'],
]

11
docs/.vuepress/public/symbols-model.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

77
docs/api/assets.md

@ -19,18 +19,71 @@ const assetManager = editor.AssetManager;
```
## Available Events
* `asset:add` New asset added to the collection. The [Asset] is passed as an argument to the callback.
* `asset:open` - Asset Manager opened.
* `asset:close` - Asset Manager closed.
* `asset:add` - Asset added. The [Asset] is passed as an argument to the callback.
* `asset:remove` - Asset removed. The [Asset] is passed as an argument to the callback.
* `asset:update` - Asset updated. The updated [Asset] and the object containing changes are passed as arguments to the callback.
* `asset:upload:start` - Before the upload is started.
* `asset:upload:end` - After the upload is ended.
* `asset:upload:error` - On any error in upload, passes the error as an argument.
* `asset:upload:response` - On upload response, passes the result as an argument.
* `asset` - 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.
* `asset:custom` - Event for handling custom Asset Manager UI.
```javascript
editor.on('asset:add', (asset) => { ... });
```
* `asset:remove` Asset removed from the collection. The [Asset] is passed as an argument to the callback.
```javascript
editor.on('asset:remove', (asset) => { ... });
```
* `asset:update` Asset updated. The [Asset] and the object containing changes are passed as arguments to the callback.
```javascript
editor.on('asset:update', (asset, updatedProps) => { ... });
```
* `asset:open` Asset Manager opened.
```javascript
editor.on('asset:open', () => { ... });
```
* `asset:close` Asset Manager closed.
```javascript
editor.on('asset:close', () => { ... });
```
* `asset:upload:start` Asset upload start.
```javascript
editor.on('asset:upload:start', () => { ... });
```
* `asset:upload:end` Asset upload end.
```javascript
editor.on('asset:upload:end', (result) => { ... });
```
* `asset:upload:error` Asset upload error.
```javascript
editor.on('asset:upload:error', (error) => { ... });
```
* `asset:upload:response` Asset upload response.
```javascript
editor.on('asset:upload:response', (res) => { ... });
```
* `asset:custom` Event to use in case of [custom Asset Manager UI](https://grapesjs.com/docs/modules/Assets.html#customization).
```javascript
editor.on('asset:custom', ({ container, assets, ... }) => { ... });
```
* `asset` 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('asset', ({ event, model, ... }) => { ... });
```
## Methods
@ -160,7 +213,7 @@ Remove asset
### Parameters
* `asset` **([String][13] | [Asset])** Asset or asset URL
* `opts` **Record<[string][13], any>?**
* `opts` **RemoveOptions?**
### Examples

6
docs/api/block_manager.md

@ -66,6 +66,12 @@ editor.on('block:drag', (block) => { ... });
editor.on('block:drag:stop', (component, block) => { ... });
```
* `block:category:update` Block category updated.
```javascript
editor.on('block:category:update', ({ category, changes }) => { ... });
```
* `block:custom` Event to use in case of [custom Block Manager UI](https://grapesjs.com/docs/modules/Blocks.html#customization).
```javascript

18
docs/api/canvas.md

@ -90,6 +90,14 @@ editor.on('canvas:pointer', () => {
});
```
* `canvas:refresh` Canvas was refreshed to update elements on top,&#xA;like spots/tools (eg. via `editor.Canvas.refresh()` or on frame resize).
```javascript
editor.on('canvas:refresh', (canvasRefreshOptions) => {
console.log('Canvas refreshed with options:', canvasRefreshOptions);
});
```
* `canvas:frame:load` Frame loaded in canvas.&#xA;The event is triggered right after iframe's `onload`.
```javascript
@ -396,6 +404,16 @@ Transform a box rect from the world coordinate system to the screen one.
Returns **[Object][2]**
## refresh
Update canvas for spots/tools positioning.
### Parameters
* `opts` **[Object][2]?** Options. (optional, default `{}`)
* `opts.spots` **[Object][2]** Update the position of spots. (optional, default `false`)
[1]: https://github.com/GrapesJS/grapesjs/blob/master/src/canvas/config/config.ts
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

61
docs/api/commands.md

@ -23,14 +23,59 @@ const commands = editor.Commands;
commands.add(...);
```
* ## Available Events
* `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview'))
* `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview'))
* `run:{commandName}:before` - Triggered before the command is called
* `stop:{commandName}:before` - Triggered before the command is called to stop
* `abort:{commandName}` - Triggered when the command execution is aborted (`editor.on(`run:preview:before`, opts => opts.abort = 1);`)
* `run` - Triggered on run of any command. The id and the result are passed as arguments to the callback
* `stop` - Triggered on stop of any command. The id and the result are passed as arguments to the callback
## Available Events
* `command:run` Triggered on run of any command.
```javascript
editor.on('command:run', ({ id, result, options }) => {
console.log('Command id', id, 'command result', result);
});
```
* `command:run:COMMAND_ID` Triggered on run of a specific command.
```javascript
editor.on('command:run:my-command', ({ result, options }) => { ... });
```
* `command:run:before:COMMAND_ID` Triggered before the command is called.
```javascript
editor.on('command:run:before:my-command', ({ options }) => { ... });
```
* `command:abort:COMMAND_ID` Triggered when the command execution is aborted.
```javascript
editor.on('command:abort:my-command', ({ options }) => { ... });
// The command could be aborted during the before event
editor.on('command:run:before:my-command', ({ options }) => {
if (someCondition) {
options.abort = true;
}
});
```
* `command:stop` Triggered on stop of any command.
```javascript
editor.on('command:stop', ({ id, result, options }) => {
console.log('Command id', id, 'command result', result);
});
```
* `command:stop:COMMAND_ID` Triggered on stop of a specific command.
```javascript
editor.on('command:run:my-command', ({ result, options }) => { ... });
```
* `command:stop:before:COMMAND_ID` Triggered before the command is called to stop.
```javascript
editor.on('command:stop:before:my-command', ({ options }) => { ... });
```
## Methods

30
docs/api/component.md

@ -128,6 +128,28 @@ Get the drag mode of the component.
Returns **[String][1]** Drag mode value, options: `'absolute'` | `'translate'` | `''`
## setSymbolOverride
Set symbol override.
By setting override to `true`, none of its property changes will be propagated to relative symbols.
By setting override to specific properties, changes of those properties will be skipped from propagation.
### Parameters
* `value` **([Boolean][3] | [String][1] | [Array][5]<[String][1]>)**
### Examples
```javascript
component.setSymbolOverride(['children', 'classes']);
```
## getSymbolOverride
Get symbol override value.
Returns **([Boolean][3] | [Array][5]<[String][1]>)**
## find
Find inner components by query string.
@ -238,7 +260,7 @@ Update attributes of the component
### Parameters
* `attrs` **[Object][2]** Key value attributes
* `opts` **SetOptions** (optional, default `{}`)
* `opts` **SetAttrOptions** (optional, default `{}`)
* `options` **[Object][2]** Options for the model update
### Examples
@ -256,7 +278,7 @@ Add attributes to the component
### Parameters
* `attrs` **[Object][2]** Key value attributes
* `opts` **SetOptions** (optional, default `{}`)
* `opts` **SetAttrOptions** (optional, default `{}`)
* `options` **[Object][2]** Options for the model update
### Examples
@ -304,7 +326,7 @@ Set the style on the component
### Parameters
* `prop` **[Object][2]** Key value style object (optional, default `{}`)
* `opts` **any** (optional, default `{}`)
* `opts` **UpdateStyleOptions** (optional, default `{}`)
### Examples
@ -859,7 +881,7 @@ Returns **[Boolean][3]**
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[6]: https://github.com/GrapesJS/grapesjs/blob/master/src/utils/Resizer.js
[6]: https://github.com/GrapesJS/grapesjs/blob/master/src/utils/Resizer.ts
[7]: /modules/Components-js.html

71
docs/api/components.md

@ -218,6 +218,77 @@ cmp.isComponent({}); // false
Returns **[Boolean][13]**
## addSymbol
Add a new symbol from a component.
If the passed component is not a symbol, it will be converted to an instance and will return the main symbol.
If the passed component is already an instance, a new instance will be created and returned.
If the passed component is the main symbol, a new instance will be created and returned.
### Parameters
* `component` **[Component]** Component from which create a symbol.
### Examples
```javascript
const symbol = cmp.addSymbol(editor.getSelected());
// cmp.getSymbolInfo(symbol).isSymbol === true;
```
Returns **[Component]**
## getSymbols
Get the array of main symbols.
### Examples
```javascript
const symbols = cmp.getSymbols();
// [Component, Component, ...]
// Removing the main symbol will detach all the relative instances.
symbols[0].remove();
```
Returns **[Array][11]<[Component]>**
## detachSymbol
Detach symbol instance from the main one.
The passed symbol instance will become a regular component.
### Parameters
* `component` **[Component]** The component symbol to detach.
### Examples
```javascript
const cmpInstance = editor.getSelected();
// cmp.getSymbolInfo(cmpInstance).isInstance === true;
cmp.detachSymbol(cmpInstance);
// cmp.getSymbolInfo(cmpInstance).isInstance === false;
```
## getSymbolInfo
Get info about the symbol.
### Parameters
* `component` **[Component]** Component symbol from which to get the info.
* `opts` **{withChanges: [string][12]?}** (optional, default `{}`)
### Examples
```javascript
cmp.getSymbolInfo(editor.getSelected());
// > { isSymbol: true, isMain: false, isInstance: true, ... }
```
Returns **[Object][10]** Object containing symbol info.
## canMove
Check if a component can be moved inside another one.

21
docs/api/i18n.md

@ -23,11 +23,24 @@ Once the editor is instantiated you can use its API. Before using these methods
const i18n = editor.I18n;
```
### Events
## Available Events
* `i18n:add` New set of messages is added.
* `i18n:add` - New set of messages is added
* `i18n:update` - The set of messages is updated
* `i18n:locale` - Locale changed
```javascript
editor.on('i18n:add', (messages) => { ... });
```
* `i18n:update` The set of messages is updated.
```javascript
editor.on('i18n:update', (messages) => { ... });
```
* `i18n:locale` Locale changed.
```javascript
editor.on('i18n:locale', ({ value, valuePrev }) => { ... });
```
## setLocale

6
docs/api/trait_manager.md

@ -36,6 +36,12 @@ editor.on('trait:select', ({ traits, component }) => { ... });
editor.on('trait:value', ({ trait, component, value }) => { ... });
```
* `trait:category:update` Trait category updated.
```javascript
editor.on('trait:category:update', ({ category, changes }) => { ... });
```
* `trait:custom` Event to use in case of [custom Trait Manager UI](https://grapesjs.com/docs/modules/Traits.html#custom-trait-manager).
```javascript

18
docs/api/undo_manager.md

@ -204,6 +204,22 @@ stack.each(item => ...);
Returns **Collection**
## skip
Execute the provided callback temporarily stopping tracking changes
### Parameters
* `clb` **[Function][17]** The callback to execute with changes tracking stopped
### Examples
```javascript
um.skip(() => {
// Do stuff without tracking
});
```
## clear
Clear the stack
@ -247,3 +263,5 @@ Returns **this**
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function

12
docs/deploy.sh

@ -9,20 +9,20 @@ set -e
# navigate into the build output directory
cd docs/.vuepress/dist
# I need to deploy all the documentation inside docs folder
# Need to deploy all the documentation inside docs folder
mkdir docs-new
# move all the files from the current directory in docs
mv `\ls -1 ./ | grep -v docs-new` ./docs-new
# fetch the current site, remove the old docs dir and make current the new one
git clone -b gh-pages https://github.com/GrapesJS/grapesjs.git tmp && mv tmp/* tmp/.* . && rm -rf tmp
rm -fR docs
mv ./docs-new ./docs
git clone -b main https://github.com/GrapesJS/website.git tmp && mv tmp/* tmp/.* . && rm -rf tmp
rm -fR public/docs
mv ./docs-new ./public/docs
# stage all and commit
git add -A
git commit -m 'deploy docs'
git push https://artf@github.com/GrapesJS/grapesjs.git gh-pages
# surge --domain grapesjs.surge.sh
git push https://artf@github.com/GrapesJS/website.git main
cd -

306
docs/guides/Symbols.md

@ -0,0 +1,306 @@
---
title: Symbols
---
# Symbols
::: warning
This feature is released as a beta from GrapesJS v0.21.11
To get a better understanding of the content in this guide we recommend reading [Components] first
:::
Symbols are a special type of [Component] that allows you to easily reuse common elements across your project. They are particularly useful for components that appear multiple times in your project and need to remain consistent. By using Symbols, you can easily update these components in one place and have the changes reflected everywhere they are used.
[[toc]]
## Concept
A Symbol created from a component retains the same shape and uses the same [Components API], but it includes a reference to other related Symbols. When you create a new Symbol from a Component, it creates a Main Symbol, and the original component becomes an Instance Symbol.
When you reuse the Symbol elsewhere, it creates new Instance Symbols. Any updates made to the Main Symbol are automatically replicated in all Instance Symbols, ensuring consistency throughout your project.
Below is a simple representation of the connection between Main and Instance Symbols.
<img :src="$withBase('/symbols-model.svg')">
::: warning Note
This feature operates at a low level, meaning there is no built-in UI for creating and managing symbols. Developers need to implement their own UI to interact with this feature. Below you'll find an example of implementation.
:::
## Programmatic usage
Let's see how to work with and manage Symbols in your project.
### Create symbol
Create a new Symbol from any component in your project:
```js
const anyComponent = editor.getSelected();
const symbolMain = editor.Components.addSymbol(anyComponent);
```
This will transform `anyComponent` to an Instance and the returned `symbolMain` will be the Main Symbol. GrapesJS keeps track of Main Symbols separately in your project JSON, and they will be automatically reconnected when you reload the project.
The `addSymbol` method also handles the creation of Instances. If you call it again by passing `symbolMain` or `anyComponent`, it will create a new Instance of `symbolMain`.
```js
const secondInstance = editor.Components.addSymbol(symbolMain);
```
Now, `symbolMain` references two instances of its shape.
To get all the available Symbols in your project, use `getSymbols`:
```js
const symbols = editor.Components.getSymbols();
const symbolMain = symbols[0];
```
### Symbol details
Once you have Symbols in your project, you might need to know when a Component is a Symbol and get details about it. Use the `getSymbolInfo` method for this:
```js
// Details from the Main Symbol
const symbolMainInfo = editor.Components.getSymbolInfo(symbolMain);
symbolMainInfo.isSymbol; // true; It's a Symbol
symbolMainInfo.isRoot; // true; It's the root of the Symbol
symbolMainInfo.isMain; // true; It's the Main Symbol
symbolMainInfo.isInstance; // false; It's not the Instance Symbol
symbolMainInfo.main; // symbolMainInfo; Reference to the Main Symbol
symbolMainInfo.instances; // [anyComponent, secondInstance]; Reference to Instance Symbols
symbolMainInfo.relatives; // [anyComponent, secondInstance]; Relative Symbols
// Details from the Instance Symbol
const secondInstanceInfo = editor.Components.getSymbolInfo(secondInstance);
symbolMainInfo.isSymbol; // true; It's a Symbol
symbolMainInfo.isRoot; // true; It's the root of the Symbol
symbolMainInfo.isMain; // false; It's not the Main Symbol
symbolMainInfo.isInstance; // true; It's the Instance Symbol
symbolMainInfo.main; // symbolMainInfo; Reference to the Main Symbol
symbolMainInfo.instances; // [anyComponent, secondInstance]; Reference to Instance Symbols
symbolMainInfo.relatives; // [anyComponent, symbolMain]; Relative Symbols
```
### Overrides
When you update a Symbol's properties, changes are propagated to all related Symbols. To avoid propagating specific properties, you can specify at the component level which properties to skip:
```js
anyComponent.set('my-property', true);
secondInstance.get('my-property'); // true; change propagated
anyComponent.setSymbolOverride(['my-property']);
// Get current override value: anyComponent.getSymbolOverride();
anyComponent.set('my-property', false);
secondInstance.get('my-property'); // true; change didn't propagate
```
### Detach symbol
Once you have Symbol instances you might need to disconnect one to create a new custom shape with other components inside, in that case you can use `detachSymbol`.
```js
editor.Components.detachSymbol(anyComponent);
const info = editor.Components.getSymbolInfo(anyComponent);
info.isSymbol; // false; Not a Symbol anymore
const infoMain = editor.Components.getSymbolInfo(symbolMain);
infoMain.instances; // [secondInstance]; Removed the reference
```
### Remove symbol
To remove a Main Symbol and detach all related instances:
```js
const symbolMain = editor.Components.getSymbols()[0];
symbolMain.remove();
```
## Events
The editor triggers several symbol-related events that you can leverage for your integration:
* `symbol:main:add` Added new root main symbol.
```js
editor.on('symbol:main:add', ({ component }) => { ... });
```
* `symbol:main:update` Root main symbol updated.
```js
editor.on('symbol:main:update', ({ component }) => { ... });
```
* `symbol:main:remove` Root main symbol removed.
```js
editor.on('symbol:main:remove', ({ component }) => { ... });
```
* `symbol:main` Catch-all event related to root main symbol updates.
```js
editor.on('symbol:main', ({ event, component }) => { ... });
```
* `symbol:instance:add` Added new root instance symbol.
```js
editor.on('symbol:instance:add', ({ component }) => { ... });
```
* `symbol:instance:remove` Root instance symbol removed.
```js
editor.on('symbol:instance:remove', ({ component }) => { ... });
```
* `symbol:instance` Catch-all event related to root instance symbol updates.
```js
editor.on('symbol:instance', ({ event, component }) => { ... });
```
* `symbol` Catch-all event for any symbol update (main or instance).
```js
editor.on('symbol', () => { ... });
```
## Example
Below is a basic UI implementation leveraging the Symbols API:
<demo-viewer value="ta19s6go" height="500" darkcode show/>
<!-- Demo template, here for reference
<style>
.app-wrapper {
height: 100vh;
display: flex;
flex-direction: column;
}
.vue-app {
padding: 10px;
display: flex;
gap: 10px;
}
.symbols-wrp {
display: flex;
gap: 10px;
width: 100%;
padding: 10px;
flex-direction: column;
border-radius: 3px;
}
.symbols {
display: flex;
gap: 10px;
width: 100%;
}
.symbol {
cursor: pointer;
flex-basis: 100px;
text-align: left;
margin: 0;
}
</style>
<div class="app-wrapper">
<div class="vue-app">
<button @click="createSymbol">Create Symbol</button>
<div class="symbols-wrp gjs-one-bg gjs-two-color">
<div v-if="symbols.length">Click on symbol to append</div>
<div class="symbols">
<div
v-for="symbol in symbols"
class="gjs-block symbol"
@click="createInstance(symbol)"
:key="symbol.getId()"
>
Name: {{ symbol.getName() }}
Instances: {{ getInstancesLength(symbol) }}
</div>
</div>
</div>
</div>
<div id="gjs"></div>
</div>
<script>
const editor = grapesjs.init({
container: '#gjs',
height: '100%',
storageManager: false,
components: `<div style="display: flex">
<article class="card" style="max-width: 300px; padding: 20px">
<img src="https://placehold.co/600x400/000000/FFF" style="max-width: 100%"/>
<h1>Title</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua</p>
</article>
</div>`,
plugins: ['gjs-blocks-basic'],
selectorManager: { componentFirst: true },
});
const { Components } = editor;
const app = new Vue({
el: '.vue-app',
data: { symbols: [] },
mounted() {
editor.on('symbol', this.updateMainSymbolsList);
},
destroyed() {
editor.off('symbol', this.updateMainSymbolsList);
},
methods: {
updateMainSymbolsList() {
this.symbols = Components.getSymbols();
},
createSymbol() {
const selected = editor.getSelected();
if (!selected) return alert('Select a component first!');
const info = Components.getSymbolInfo(selected);
if (info.isSymbol) return alert('Selected component is already a symbol!');
Components.addSymbol(selected);
},
getInstancesLength(symbolMain) {
return Components.getSymbolInfo(symbolMain).instances.length;
},
createInstance(symbolMain) {
const instance = Components.addSymbol(symbolMain);
editor.getWrapper().append(instance, { at: 0 });
}
}
});
</script>
-->
[Component]: </modules/Components.html>
[Components]: </modules/Components.html>
[Components API]: </api/component.html>

2
docs/modules/Assets.md

@ -69,7 +69,7 @@ const editor = grapesjs.init({
```
If you want a complete list of available properties check out the source [AssetImage Model](https://github.com/GrapesJS/grapesjs/blob/dev/src/asset_manager/model/AssetImage.js)
If you want a complete list of available properties check out the source [AssetImage Model](https://github.com/GrapesJS/grapesjs/blob/dev/src/asset_manager/model/AssetImage.ts)
The built-in Asset Manager modal is implemented and is showing up when requested. By default, you can make it appear by dragging Image Components in canvas, double clicking on images and all other stuff related to images (eg. CSS styling)

16
docs/modules/Commands.md

@ -267,20 +267,20 @@ The Commands module offers also a set of events that you can use to intercept th
By using our previously created `my-command-modal` command let's see which events we can listen to
```js
editor.on('run:my-command-modal', () => {
editor.on('command:run:my-command-modal', () => {
console.log('After `my-command-modal` execution');
// For example, you can add extra content to the modal
const modalContent = editor.Modal.getContentEl();
modalContent.insertAdjacentHTML('beforeEnd', '<div>Some content</div>');
});
editor.on('run:my-command-modal:before', () => {
editor.on('command:run:before:my-command-modal', () => {
console.log('Before `my-command-modal` execution');
});
// for stateful commands
editor.on('stop:my-command-modal', () => {
editor.on('command:stop:my-command-modal', () => {
console.log('After `my-command-modal` is stopped');
});
editor.on('stop:my-command-modal:before', () => {
editor.on('command:stop:before:my-command-modal:before', () => {
console.log('Before `my-command-modal` is stopped');
});
```
@ -288,11 +288,11 @@ editor.on('stop:my-command-modal:before', () => {
If you need, you can also listen to all commands
```js
editor.on('run', commandId => {
editor.on('command:run', commandId => {
console.log('Run', commandId);
});
editor.on('stop', commandId => {
editor.on('command:stop', commandId => {
console.log('Stop', commandId);
});
```
@ -300,12 +300,12 @@ editor.on('stop', commandId => {
### Interrupt command flow
Sometimes you might need to interrupt the execution of an existant command due to some condition. In that case, you have to use `run:{COMMAND-ID}:before` event and set to `true` the abort option
Sometimes you might need to interrupt the execution of an existant command due to some condition. In that case, you have to use `command:run:before:{COMMAND-ID}` event and set to `true` the abort option
```js
const condition = 1;
editor.on('run:my-command-modal:before', options => {
editor.on('command:run:before:my-command-modal', options => {
if (condition) {
options.abort = true;
console.log('Prevent `my-command-modal` from execution');

4
docs/modules/I18n.md

@ -44,7 +44,7 @@ Now the editor will be translated in Italian for those browsers which default la
## Update strings
If you need to change some default language strings you can easily update them by using [I18n API](/api/i18n.html).
To find the correth path of the string you can check the [`en` locale file] and follow its inner path inside the locale object.
To find the correct path of the string you can check the [`en` locale file] and follow its inner path inside the locale object.
Let's say we want to update the default message of the empty state in Style Manager when no elements are selected.
@ -167,7 +167,7 @@ plugin-dir
- ...
- src
- index.js
- locale // create the locale foldar in your src
- locale // create the locale folder in your src
- en.js // All default strings should be placed here
```

2
docs/modules/Plugins.md

@ -182,4 +182,4 @@ grapesjs.init({
## Boilerplate
For fast plugin development, we highly recommend using [grapesjs-cli](https://github.com/GrapesJS/cli) which helps to avoid the hassle of setting up all the dependencies and configurations for development and building (no need to touch Webpack o Babel configurations). For more information check the repository.
For fast plugin development, we highly recommend using [grapesjs-cli](https://github.com/GrapesJS/cli) which helps to avoid the hassle of setting up all the dependencies and configurations for development and building (no need to touch Webpack or Babel configurations). For more information check the repository.

2
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.21.8",
"version": "0.21.12",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",

20
src/abstract/ModuleCategories.ts

@ -1,9 +1,29 @@
import { isArray, isString } from 'underscore';
import { AddOptions, Collection } from '../common';
import { normalizeKey } from '../utils/mixins';
import EditorModel from '../editor/model/Editor';
import Category, { CategoryProperties } from './ModuleCategory';
type CategoryCollectionParams = ConstructorParameters<typeof Collection<Category>>;
interface CategoryOptions {
events?: { update?: string };
em?: EditorModel;
}
export default class Categories extends Collection<Category> {
constructor(models?: CategoryCollectionParams[0], opts: CategoryOptions = {}) {
super(models, opts);
const { events, em } = opts;
const evUpdate = events?.update;
if (em) {
evUpdate &&
this.on('change', (category, options) =>
em.trigger(evUpdate, { category, changes: category.changedAttributes(), options })
);
}
}
/** @ts-ignore */
add(model: (CategoryProperties | Category)[] | CategoryProperties | Category, opts?: AddOptions) {
const models = isArray(model) ? model : [model];

153
src/asset_manager/index.ts

@ -14,18 +14,7 @@
* const assetManager = editor.AssetManager;
* ```
*
* ## Available Events
* * `asset:open` - Asset Manager opened.
* * `asset:close` - Asset Manager closed.
* * `asset:add` - Asset added. The [Asset] is passed as an argument to the callback.
* * `asset:remove` - Asset removed. The [Asset] is passed as an argument to the callback.
* * `asset:update` - Asset updated. The updated [Asset] and the object containing changes are passed as arguments to the callback.
* * `asset:upload:start` - Before the upload is started.
* * `asset:upload:end` - After the upload is ended.
* * `asset:upload:error` - On any error in upload, passes the error as an argument.
* * `asset:upload:response` - On upload response, passes the result as an argument.
* * `asset` - 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.
* * `asset:custom` - Event for handling custom Asset Manager UI.
* {REPLACE_EVENTS}
*
* ## Methods
* * [open](#open)
@ -45,67 +34,20 @@
import { debounce, isFunction } from 'underscore';
import { ItemManagerModule } from '../abstract/Module';
import { AddOptions, RemoveOptions } from '../common';
import EditorModel from '../editor/model/Editor';
import { ProjectData } from '../storage_manager';
import defaults, { AssetManagerConfig } from './config/config';
import Asset from './model/Asset';
import Assets from './model/Assets';
import AssetsEvents, { AssetOpenOptions } from './types';
import AssetsView from './view/AssetsView';
import FileUploaderView from './view/FileUploader';
export type AssetEvent =
| 'asset'
| 'asset:open'
| 'asset:close'
| 'asset:add'
| 'asset:remove'
| 'asset:update'
| 'asset:custom'
| 'asset:upload:start'
| 'asset:upload:end'
| 'asset:upload:error'
| 'asset:upload:response';
export const evAll = 'asset';
export const evPfx = `${evAll}:`;
export const evSelect = `${evPfx}select`;
export const evUpdate = `${evPfx}update`;
export const evAdd = `${evPfx}add`;
export const evRemove = `${evPfx}remove`;
export const evRemoveBefore = `${evRemove}:before`;
export const evCustom = `${evPfx}custom`;
export const evOpen = `${evPfx}open`;
export const evClose = `${evPfx}close`;
export const evUpload = `${evPfx}upload`;
export const evUploadStart = `${evUpload}:start`;
export const evUploadEnd = `${evUpload}:end`;
export const evUploadError = `${evUpload}:error`;
export const evUploadRes = `${evUpload}:response`;
const assetCmd = 'open-assets';
const assetEvents = {
all: evAll,
select: evSelect,
update: evUpdate,
add: evAdd,
remove: evRemove,
removeBefore: evRemoveBefore,
custom: evCustom,
open: evOpen,
close: evClose,
uploadStart: evUploadStart,
uploadEnd: evUploadEnd,
uploadError: evUploadError,
uploadResponse: evUploadRes,
};
// TODO
type AssetProps = Record<string, any>;
type OpenOptions = {
select?: (asset: Asset, complete: boolean) => void;
types?: string[];
accept?: string;
target?: any;
};
const assetCmd = 'open-assets';
export default class AssetManager extends ItemManagerModule<AssetManagerConfig, Assets> {
storageKey = 'assets';
@ -115,7 +57,7 @@ export default class AssetManager extends ItemManagerModule<AssetManagerConfig,
am?: AssetsView;
fu?: FileUploaderView;
_bhv?: any;
events!: typeof assetEvents;
events = AssetsEvents;
/**
* Initialize module
@ -124,11 +66,10 @@ export default class AssetManager extends ItemManagerModule<AssetManagerConfig,
*/
constructor(em: EditorModel) {
// @ts-ignore
super(em, 'AssetManager', new Assets([], em), assetEvents, defaults);
super(em, 'AssetManager', new Assets([], em), AssetsEvents, defaults);
const { all, config } = this;
// @ts-ignore
this.assetsVis = new Assets([]);
// @ts-ignore
const ppfx = config.pStylePrefix;
if (ppfx) {
config.stylePrefix = `${ppfx}${config.stylePrefix}`;
@ -143,40 +84,6 @@ export default class AssetManager extends ItemManagerModule<AssetManagerConfig,
return this;
}
__propEv(ev: string, ...data: any[]) {
this.em.trigger(ev, ...data);
this.getAll().trigger(ev, ...data);
}
__trgCustom() {
const bhv = this.__getBehaviour();
const custom = this.getConfig().custom;
if (!bhv.container && !(custom as any).open) {
return;
}
this.em.trigger(this.events.custom, this.__customData());
}
__customData() {
const bhv = this.__getBehaviour();
return {
am: this as AssetManager,
open: this.isOpen(),
assets: this.getAll().models,
types: bhv.types || [],
container: bhv.container,
close: () => this.close(),
remove: (asset: string | Asset, opts?: Record<string, any>) => this.remove(asset, opts),
select: (asset: Asset, complete: boolean) => {
const res = this.add(asset);
isFunction(bhv.select) && bhv.select(res, complete);
},
// extra
options: bhv.options || {},
};
}
/**
* Open the asset manager.
* @param {Object} [options] Options for the asset manager.
@ -197,7 +104,7 @@ export default class AssetManager extends ItemManagerModule<AssetManagerConfig,
* // with your custom types (you should have assets with those types declared)
* assetManager.open({ types: ['doc'], ... });
*/
open(options: OpenOptions = {}) {
open(options: AssetOpenOptions = {}) {
const cmd = this.em.Commands;
cmd.run(assetCmd, {
types: ['image'],
@ -246,7 +153,7 @@ export default class AssetManager extends ItemManagerModule<AssetManagerConfig,
* });
* assetManager.add([{ src: 'img2.jpg' }, { src: 'img2.png' }]);
*/
add(asset: string | AssetProps | (string | AssetProps)[], opts: Record<string, any> = {}) {
add(asset: string | AssetProps | (string | AssetProps)[], opts: AddOptions = {}) {
// Put the model at the beginning
if (typeof opts.at == 'undefined') {
opts.at = 0;
@ -292,7 +199,7 @@ export default class AssetManager extends ItemManagerModule<AssetManagerConfig,
* const asset = assetManager.get('http://img.jpg');
* assetManager.remove(asset);
*/
remove(asset: string | Asset, opts?: Record<string, any>) {
remove(asset: string | Asset, opts?: RemoveOptions) {
return this.__remove(asset, opts);
}
@ -300,7 +207,7 @@ export default class AssetManager extends ItemManagerModule<AssetManagerConfig,
return this.getProjectData();
}
load(data: Record<string, any>) {
load(data: ProjectData) {
return this.loadProjectData(data);
}
@ -416,8 +323,8 @@ export default class AssetManager extends ItemManagerModule<AssetManagerConfig,
onLoad() {
this.getAll().reset(this.config.assets);
const { em, events } = this;
em.on(`run:${assetCmd}`, () => this.__propEv(events.open));
em.on(`stop:${assetCmd}`, () => this.__propEv(events.close));
em.Commands.__onRun(assetCmd, () => this.__propEv(events.open));
em.Commands.__onStop(assetCmd, () => this.__propEv(events.close));
}
postRender(editorView: any) {
@ -462,6 +369,40 @@ export default class AssetManager extends ItemManagerModule<AssetManagerConfig,
this.config.onDblClick = func;
}
__propEv(ev: string, ...data: any[]) {
this.em.trigger(ev, ...data);
this.getAll().trigger(ev, ...data);
}
__trgCustom() {
const bhv = this.__getBehaviour();
const custom = this.getConfig().custom;
if (!bhv.container && !(custom as any).open) {
return;
}
this.em.trigger(this.events.custom, this.__customData());
}
__customData() {
const bhv = this.__getBehaviour();
return {
am: this as AssetManager,
open: this.isOpen(),
assets: this.getAll().models,
types: bhv.types || [],
container: bhv.container,
close: () => this.close(),
remove: (asset: string | Asset, opts?: Record<string, any>) => this.remove(asset, opts),
select: (asset: Asset, complete: boolean) => {
const res = this.add(asset);
isFunction(bhv.select) && bhv.select(res, complete);
},
// extra
options: bhv.options || {},
};
}
__behaviour(opts = {}) {
return (this._bhv = {
...(this._bhv || {}),

95
src/asset_manager/types.ts

@ -0,0 +1,95 @@
import Asset from './model/Asset';
export type AssetEvent = `${AssetsEvents}`;
export interface AssetOpenOptions {
select?: (asset: Asset, complete: boolean) => void;
types?: string[];
accept?: string;
target?: any;
}
/**{START_EVENTS}*/
export enum AssetsEvents {
/**
* @event `asset:add` New asset added to the collection. The [Asset] is passed as an argument to the callback.
* @example
* editor.on('asset:add', (asset) => { ... });
*/
add = 'asset:add',
/**
* @event `asset:remove` Asset removed from the collection. The [Asset] is passed as an argument to the callback.
* @example
* editor.on('asset:remove', (asset) => { ... });
*/
remove = 'asset:remove',
removeBefore = 'asset:remove:before',
/**
* @event `asset:update` Asset updated. The [Asset] and the object containing changes are passed as arguments to the callback.
* @example
* editor.on('asset:update', (asset, updatedProps) => { ... });
*/
update = 'asset:update',
/**
* @event `asset:open` Asset Manager opened.
* @example
* editor.on('asset:open', () => { ... });
*/
open = 'asset:open',
/**
* @event `asset:close` Asset Manager closed.
* @example
* editor.on('asset:close', () => { ... });
*/
close = 'asset:close',
/**
* @event `asset:upload:start` Asset upload start.
* @example
* editor.on('asset:upload:start', () => { ... });
*/
uploadStart = 'asset:upload:start',
/**
* @event `asset:upload:end` Asset upload end.
* @example
* editor.on('asset:upload:end', (result) => { ... });
*/
uploadEnd = 'asset:upload:end',
/**
* @event `asset:upload:error` Asset upload error.
* @example
* editor.on('asset:upload:error', (error) => { ... });
*/
uploadError = 'asset:upload:error',
/**
* @event `asset:upload:response` Asset upload response.
* @example
* editor.on('asset:upload:response', (res) => { ... });
*/
uploadResponse = 'asset:upload:response',
/**
* @event `asset:custom` Event to use in case of [custom Asset Manager UI](https://grapesjs.com/docs/modules/Assets.html#customization).
* @example
* editor.on('asset:custom', ({ container, assets, ... }) => { ... });
*/
custom = 'asset:custom',
/**
* @event `asset` 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('asset', ({ event, model, ... }) => { ... });
*/
all = 'asset',
}
/**{END_EVENTS}*/
// need this to avoid the TS documentation generator to break
export default AssetsEvents;

11
src/asset_manager/view/FileUploader.ts

@ -1,3 +1,4 @@
import AssetManager from '..';
import { View } from '../../common';
import EditorModel from '../../editor/model/Editor';
import fetch from '../../utils/fetch';
@ -18,7 +19,7 @@ export default class FileUploaderView extends View {
pfx: string;
ppfx: string;
em: EditorModel;
module: any;
module: AssetManager;
target: any;
uploadId: string;
disabled: boolean;
@ -80,7 +81,7 @@ export default class FileUploaderView extends View {
*/
onUploadStart() {
const { module } = this;
module && module.__propEv('asset:upload:start');
module?.__propEv(module.events.uploadStart);
}
/**
@ -90,7 +91,7 @@ export default class FileUploaderView extends View {
*/
onUploadEnd(res: any) {
const { $el, module } = this;
module && module.__propEv('asset:upload:end', res);
module?.__propEv(module.events.uploadEnd, res);
const input = $el.find('input');
input && input.val('');
}
@ -104,7 +105,7 @@ export default class FileUploaderView extends View {
const { module } = this;
console.error(err);
this.onUploadEnd(err);
module && module.__propEv('asset:upload:error', err);
module?.__propEv(module.events.uploadError, err);
}
/**
@ -121,7 +122,7 @@ export default class FileUploaderView extends View {
json = text;
}
module && module.__propEv('asset:upload:response', json);
module?.__propEv(module.events.uploadResponse, json);
if (config.autoAdd && target) {
target.add(json.data, { at: 0 });

5
src/block_manager/index.ts

@ -66,7 +66,10 @@ export default class BlockManager extends ItemManagerModule<BlockManagerConfig,
// Global blocks collection
this.blocks = this.all;
this.blocksVisible = new Blocks(this.blocks.models, { em });
this.categories = new Categories();
this.categories = new Categories([], {
em,
events: { update: BlocksEvents.categoryUpdate },
});
// Setup the sync between the global and public collections
this.blocks.on('add', model => this.blocksVisible.add(model));

2
src/block_manager/model/Block.ts

@ -14,7 +14,7 @@ export interface BlockProperties {
/**
* The content of the block. Might be an HTML string or a [Component Defintion](/modules/Components.html#component-definition)
*/
content: string | ComponentDefinition;
content: string | ComponentDefinition | (string | ComponentDefinition)[];
/**
* HTML string for the media/icon of the block, eg. `<svg ...`, `<img ...`, etc.
* @default ''

7
src/block_manager/types.ts

@ -54,6 +54,13 @@ export enum BlocksEvents {
*/
dragEnd = 'block:drag:stop',
/**
* @event `block:category:update` Block category updated.
* @example
* editor.on('block:category:update', ({ category, changes }) => { ... });
*/
categoryUpdate = 'block:category:update',
/**
* @event `block:custom` Event to use in case of [custom Block Manager UI](https://grapesjs.com/docs/modules/Blocks.html#customization).
* @example

20
src/canvas/index.ts

@ -40,7 +40,7 @@ import Canvas from './model/Canvas';
import CanvasSpot, { CanvasSpotBuiltInTypes, CanvasSpotProps } from './model/CanvasSpot';
import CanvasSpots from './model/CanvasSpots';
import Frame from './model/Frame';
import { CanvasEvents, ToWorldOption } from './types';
import { CanvasEvents, CanvasRefreshOptions, ToWorldOption } from './types';
import CanvasView, { FitViewportOptions } from './view/CanvasView';
import FrameView from './view/FrameView';
@ -834,6 +834,24 @@ export default class CanvasModule extends Module<CanvasConfig> {
return this.canvasView?.getRectToScreen(boxRect);
}
/**
* Update canvas for spots/tools positioning.
* @param {Object} [opts] Options.
* @param {Object} [opts.spots=false] Update the position of spots.
*/
refresh(opts: CanvasRefreshOptions = {}) {
const { em, events, canvasView } = this;
canvasView?.clearOff();
if (opts.spots || opts.all) {
this.refreshSpots();
em.trigger('canvas:updateTools'); // this should be deprecated
}
em.set('canvasOffset', this.getOffset()); // this should be deprecated
em.trigger(events.refresh, opts);
}
refreshSpots() {
this.spots.refresh();
}

2
src/canvas/model/Canvas.ts

@ -42,7 +42,7 @@ export default class Canvas extends ModuleModel<CanvasModule> {
init() {
const { em } = this;
const mainPage = em.Pages.getMain();
const mainPage = em.Pages._initPage();
this.set('frames', mainPage.getFrames());
this.updateDevice({ frame: mainPage.getMainFrame() });
}

4
src/canvas/model/CanvasSpots.ts

@ -4,6 +4,7 @@ import { ModuleCollection } from '../../abstract';
import { Debounced, ObjectAny } from '../../common';
import EditorModel from '../../editor/model/Editor';
import CanvasSpot, { CanvasSpotProps } from './CanvasSpot';
import { ComponentsEvents } from '../../dom_components/types';
export default class CanvasSpots extends ModuleCollection<CanvasSpot> {
refreshDbn: Debounced;
@ -15,7 +16,8 @@ export default class CanvasSpots extends ModuleCollection<CanvasSpot> {
this.on('remove', this.onRemove);
const { em } = this;
this.refreshDbn = debounce(() => this.refresh(), 0);
const evToRefreshDbn = 'component:resize styleable:change component:input component:update frame:updated undo redo';
const evToRefreshDbn = `component:resize styleable:change component:input ${ComponentsEvents.update} frame:updated undo redo`;
this.listenTo(em, evToRefreshDbn, () => this.refreshDbn());
}

18
src/canvas/types.ts

@ -10,6 +10,14 @@ export interface GetBoxRectOptions extends ToScreenOption {
local?: boolean;
}
export interface CanvasRefreshOptions {
/**
* Refresh canvas spots.
*/
spots?: boolean;
all?: boolean;
}
/**{START_EVENTS}*/
export enum CanvasEvents {
/**
@ -101,6 +109,16 @@ export enum CanvasEvents {
*/
pointer = 'canvas:pointer',
/**
* @event `canvas:refresh` Canvas was refreshed to update elements on top,
* like spots/tools (eg. via `editor.Canvas.refresh()` or on frame resize).
* @example
* editor.on('canvas:refresh', (canvasRefreshOptions) => {
* console.log('Canvas refreshed with options:', canvasRefreshOptions);
* });
*/
refresh = 'canvas:refresh',
/**
* @event `canvas:frame:load` Frame loaded in canvas.
* The event is triggered right after iframe's `onload`.

2
src/canvas/view/CanvasView.ts

@ -95,7 +95,7 @@ export default class CanvasView extends ModuleView<Canvas> {
this.className = `${pfx}canvas ${ppfx}no-touch-actions${!em.config.customUI ? ` ${pfx}canvas-bg` : ''}`;
this.clsUnscale = `${pfx}unscale`;
this._initFrames();
this.listenTo(em, 'change:canvasOffset', this.clearOff);
this.listenTo(em, events.refresh, this.clearOff);
this.listenTo(em, 'component:selected', this.checkSelected);
this.listenTo(em, `${events.coords} ${events.zoom}`, this.updateFrames);
this.listenTo(model, 'change:frames', this._onFramesUpdate);

18
src/canvas/view/FrameView.ts

@ -3,6 +3,8 @@ import { ModuleView } from '../../abstract';
import { BoxRect, ObjectAny } from '../../common';
import CssRulesView from '../../css_composer/view/CssRulesView';
import ComponentWrapperView from '../../dom_components/view/ComponentWrapperView';
import ComponentView from '../../dom_components/view/ComponentView';
import { type as typeHead } from '../../dom_components/model/ComponentHead';
import Droppable from '../../utils/Droppable';
import {
append,
@ -40,6 +42,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
private jsContainer?: HTMLElement;
private tools: { [key: string]: HTMLElement } = {};
private wrapper?: ComponentWrapperView;
private headView?: ComponentView;
private frameWrapView?: FrameWrapView;
constructor(model: Frame, view?: FrameWrapView) {
@ -333,6 +336,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
evOpts.window = this.getWindow();
em?.trigger(`${evLoad}:before`, evOpts); // deprecated
em?.trigger(CanvasEvents.frameLoad, evOpts);
this.renderHead();
appendScript([...canvas.get('scripts')]);
};
}
@ -368,6 +372,20 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
appendVNodes(head, toAdd);
}
renderHead() {
const { model, em } = this;
const { root } = model;
const HeadView = em.Components.getType(typeHead)!.view;
this.headView = new HeadView({
el: this.getHead(),
model: root.head,
config: {
...root.config,
frameView: this,
},
}).render();
}
renderBody() {
const { config, em, model, ppfx } = this;
const doc = this.getDoc();

11
src/code_manager/model/HtmlGenerator.ts

@ -1,19 +1,14 @@
import { Model } from '../../common';
import Component from '../../dom_components/model/Component';
import { ToHTMLOptions } from '../../dom_components/model/types';
import EditorModel from '../../editor/model/Editor';
export type HTMLGeneratorBuildOptions = {
export interface HTMLGeneratorBuildOptions extends ToHTMLOptions {
/**
* Remove unnecessary IDs (eg. those created automatically).
*/
cleanId?: boolean;
/**
* You can pass an object of custom attributes to replace with the current ones
* or you can even pass a function to generate attributes dynamically.
*/
attributes?: Record<string, any> | ((component: Component, attr: Record<string, any>) => Record<string, any>);
};
}
export default class HTMLGenerator extends Model {
build(model: Component, opts: HTMLGeneratorBuildOptions & { em?: EditorModel } = {}) {

23
src/commands/index.ts

@ -19,14 +19,7 @@
* commands.add(...);
* ```
*
** ## Available Events
* * `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview'))
* * `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview'))
* * `run:{commandName}:before` - Triggered before the command is called
* * `stop:{commandName}:before` - Triggered before the command is called to stop
* * `abort:{commandName}` - Triggered when the command execution is aborted (`editor.on(`run:preview:before`, opts => opts.abort = 1);`)
* * `run` - Triggered on run of any command. The id and the result are passed as arguments to the callback
* * `stop` - Triggered on stop of any command. The id and the result are passed as arguments to the callback
* {REPLACE_EVENTS}
*
* ## Methods
* * [add](#add)
@ -49,6 +42,7 @@ import { Module } from '../abstract';
import Component, { eventDrag } from '../dom_components/model/Component';
import Editor from '../editor/model/Editor';
import { ObjectAny } from '../common';
import CommandsEvents from './types';
export type CommandEvent = 'run' | 'stop' | `run:${string}` | `stop:${string}` | `abort:${string}`;
@ -105,6 +99,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
defaultCommands: Record<string, Command> = {};
commands: Record<string, CommandObject> = {};
active: Record<string, any> = {};
events = CommandsEvents;
/**
* @private
@ -146,7 +141,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
const trg = opts.target as Component | undefined;
const trgs = trg ? [trg] : [...ed.getSelectedAll()];
const targets = trgs.map(trg => trg.delegate?.move?.(trg) || trg).filter(Boolean);
const target = targets[0] as Component | undefined;
const target = targets[targets.length - 1] as Component | undefined;
const nativeDrag = event?.type === 'dragstart';
const modes = ['absolute', 'translate'];
@ -441,6 +436,16 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
return new cmd(this.config);
}
__onRun(id: string, clb: () => void) {
const { em, events } = this;
em.on(`${events.runCommand}${id}`, clb);
}
__onStop(id: string, clb: () => void) {
const { em, events } = this;
em.on(`${events.stopCommand}${id}`, clb);
}
destroy() {
this.defaultCommands = {};
this.commands = {};

71
src/commands/types.ts

@ -0,0 +1,71 @@
/**{START_EVENTS}*/
export enum CommandsEvents {
/**
* @event `command:run` Triggered on run of any command.
* @example
* editor.on('command:run', ({ id, result, options }) => {
* console.log('Command id', id, 'command result', result);
* });
*/
run = 'command:run',
_run = 'run',
/**
* @event `command:run:COMMAND_ID` Triggered on run of a specific command.
* @example
* editor.on('command:run:my-command', ({ result, options }) => { ... });
*/
runCommand = 'command:run:',
_runCommand = 'run:',
/**
* @event `command:run:before:COMMAND_ID` Triggered before the command is called.
* @example
* editor.on('command:run:before:my-command', ({ options }) => { ... });
*/
runBeforeCommand = 'command:run:before:',
/**
* @event `command:abort:COMMAND_ID` Triggered when the command execution is aborted.
* @example
* editor.on('command:abort:my-command', ({ options }) => { ... });
*
* // The command could be aborted during the before event
* editor.on('command:run:before:my-command', ({ options }) => {
* if (someCondition) {
* options.abort = true;
* }
* });
*/
abort = 'command:abort:',
_abort = 'abort:',
/**
* @event `command:stop` Triggered on stop of any command.
* @example
* editor.on('command:stop', ({ id, result, options }) => {
* console.log('Command id', id, 'command result', result);
* });
*/
stop = 'command:stop',
_stop = 'stop',
/**
* @event `command:stop:COMMAND_ID` Triggered on stop of a specific command.
* @example
* editor.on('command:run:my-command', ({ result, options }) => { ... });
*/
stopCommand = 'command:stop:',
_stopCommand = 'stop:',
/**
* @event `command:stop:before:COMMAND_ID` Triggered before the command is called to stop.
* @example
* editor.on('command:stop:before:my-command', ({ options }) => { ... });
*/
stopBeforeCommand = 'command:stop:before:',
}
/**{END_EVENTS}*/
// need this to avoid the TS documentation generator to break
export default CommandsEvents;

33
src/commands/view/CommandAbstract.ts

@ -2,6 +2,7 @@ import CanvasModule from '../../canvas';
import { Model, ObjectAny } from '../../common';
import Editor from '../../editor';
import EditorModel from '../../editor/model/Editor';
import CommandsEvents from '../types';
interface ICommand<O extends ObjectAny = any> {
run?: CommandAbstract<O>['run'];
@ -108,18 +109,24 @@ export default class CommandAbstract<O extends ObjectAny = any> extends Model {
* @private
* */
callRun(editor: Editor, options: any = {}) {
const id = this.id;
editor.trigger(`run:${id}:before`, options);
const { id } = this;
editor.trigger(`${CommandsEvents.runBeforeCommand}${id}`, { options });
editor.trigger(`${CommandsEvents._runCommand}${id}:before`, options);
if (options && options.abort) {
editor.trigger(`abort:${id}`, options);
if (options.abort) {
editor.trigger(`${CommandsEvents.abort}${id}`, { options });
editor.trigger(`${CommandsEvents._abort}${id}`, options);
return;
}
const sender = options.sender || editor;
const result = this.run(editor, sender, options);
editor.trigger(`run:${id}`, result, options);
editor.trigger('run', id, result, options);
const data = { id, result, options };
editor.trigger(`${CommandsEvents.runCommand}${id}`, data);
editor.trigger(CommandsEvents.run, data);
// deprecated
editor.trigger(`${CommandsEvents._runCommand}${id}`, result, options);
editor.trigger(CommandsEvents._run, id, result, options);
return result;
}
@ -130,12 +137,18 @@ export default class CommandAbstract<O extends ObjectAny = any> extends Model {
* @private
* */
callStop(editor: Editor, options: any = {}) {
const id = this.id;
const { id } = this;
const sender = options.sender || editor;
editor.trigger(`stop:${id}:before`, options);
editor.trigger(`${CommandsEvents.stopBeforeCommand}${id}`, { options });
editor.trigger(`${CommandsEvents._stopCommand}${id}:before`, options);
const result = this.stop(editor, sender, options);
editor.trigger(`stop:${id}`, result, options);
editor.trigger('stop', id, result, options);
const data = { id, result, options };
editor.trigger(`${CommandsEvents.stopCommand}${id}`, data);
editor.trigger(CommandsEvents.stop, data);
// deprecated
editor.trigger(`${CommandsEvents._stopCommand}${id}`, result, options);
editor.trigger(CommandsEvents._stop, id, result, options);
return result;
}

2
src/commands/view/Fullscreen.ts

@ -72,12 +72,10 @@ export default {
const pfx = this.enable(targetEl || editor.getContainer());
this.fsChanged = this.fsChanged.bind(this, pfx);
document.addEventListener(pfx + 'fullscreenchange', this.fsChanged);
editor.trigger('change:canvasOffset');
},
stop(editor, sender) {
if (sender && sender.set) sender.set('active', false);
this.disable();
if (editor) editor.trigger('change:canvasOffset');
},
} as CommandObject<{ target?: HTMLElement | string }, { [k: string]: any }>;

49
src/commands/view/SelectComponent.ts

@ -7,6 +7,7 @@ import { getComponentModel, getComponentView, getUnitFromValue, getViewEl, hasWi
import { CommandObject } from './CommandAbstract';
import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot';
import { ResizerOptions } from '../../utils/Resizer';
import { ComponentsEvents } from '../../dom_components/types';
let showOffsets: boolean;
/**
@ -30,6 +31,8 @@ let showOffsets: boolean;
*
*/
export default {
activeResizer: false,
init() {
this.onSelect = debounce(this.onSelect, 0);
bindAll(
@ -77,6 +80,7 @@ export default {
const { parentNode } = em.getContainer()!;
const method = enable ? 'on' : 'off';
const methods = { on, off };
const eventCmpUpdate = ComponentsEvents.update;
!listenToEl.length && parentNode && listenToEl.push(parentNode as HTMLElement);
const trigger = (win: Window, body: HTMLBodyElement) => {
methods[method](body, 'mouseover', this.onHover);
@ -87,13 +91,13 @@ export default {
};
methods[method](window, 'resize', this.onFrameUpdated);
methods[method](listenToEl, 'scroll', this.onContainerChange);
em[method]('component:toggled component:update undo redo', this.onSelect, this);
em[method](`component:toggled ${eventCmpUpdate} undo redo`, this.onSelect, this);
em[method]('change:componentHovered', this.onHovered, this);
em[method]('component:resize styleable:change component:input', this.updateGlobalPos, this);
em[method]('component:update:toolbar', this._upToolbar, this);
em[method]('change:canvasOffset', this.updateAttached, this);
em[method](`${eventCmpUpdate}:toolbar`, this._upToolbar, this);
em[method]('frame:updated', this.onFrameUpdated, this);
em[method]('canvas:updateTools', this.onFrameUpdated, this);
em[method](em.Canvas.events.refresh, this.updateAttached, this);
em.Canvas.getFrames().forEach(frame => {
const { view } = frame;
const win = view?.getWindow();
@ -288,23 +292,30 @@ export default {
if (em.get('_cmpDrag')) return em.set('_cmpDrag');
const el = ev.target as HTMLElement;
let model = getComponentModel(el);
let cmp = getComponentModel(el);
if (!model) {
if (!cmp) {
let parentEl = el.parentNode;
while (!model && parentEl && !isDoc(parentEl)) {
model = getComponentModel(parentEl);
while (!cmp && parentEl && !isDoc(parentEl)) {
cmp = getComponentModel(parentEl);
parentEl = parentEl.parentNode;
}
}
if (model) {
// Avoid selection of inner text components during editing
if (em.isEditing() && !model.get('textable') && model.isChildOf('text')) {
if (cmp) {
if (
em.isEditing() &&
// Avoid selection of inner text components during editing
((!cmp.get('textable') && cmp.isChildOf('text')) ||
// Prevents selecting another component if the pointer was pressed and
// dragged outside of the editing component
em.getEditing() !== cmp)
) {
return;
}
this.select(model, ev);
this.select(cmp, ev);
}
},
@ -396,12 +407,17 @@ export default {
...resizableOpts
} = isObject(resizable) ? resizable : {};
if (hasCustomResize || !el) return;
if (hasCustomResize || !el || this.activeResizer) return;
let modelToStyle: any;
const { config } = em;
const pfx = config.stylePrefix || '';
const resizeClass = `${pfx}resizing`;
const self = this;
const resizeEventOpts = {
component: model,
el,
};
const toggleBodyClass = (method: string, e: any, opts: any) => {
const docs = opts.docs;
@ -445,20 +461,23 @@ export default {
config.unitHeight = getUnitFromValue(currentHeight);
config.unitWidth = getUnitFromValue(currentWidth);
}
self.activeResizer = true;
editor.trigger('component:resize', { ...resizeEventOpts, type: 'start' });
},
// Update all positioned elements (eg. component toolbar)
onMove(ev) {
onMove(ev);
editor.trigger('component:resize');
editor.trigger('component:resize', { ...resizeEventOpts, type: 'move' });
},
onEnd(ev, opts) {
onEnd(ev, opts);
toggleBodyClass('remove', ev, opts);
editor.trigger('component:resize');
editor.trigger('component:resize', { ...resizeEventOpts, type: 'end' });
canvas.toggleFramesEvents(true);
showOffsets = true;
self.activeResizer = false;
},
updateTarget(el, rect, options) {
@ -580,7 +599,7 @@ export default {
},
onFrameResize() {
this.canvas.refreshSpots();
this.canvas.refresh({ all: true });
},
updateTools() {

3
src/commands/view/ShowOffset.ts

@ -1,7 +1,6 @@
import { isUndefined } from 'underscore';
import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot';
import { $ } from '../../common';
import { isTextNode } from '../../utils/dom';
import { CommandObject } from './CommandAbstract';
export default {
@ -18,7 +17,7 @@ export default {
const zoom = this.em.getZoomDecimal();
const el = opt.el as HTMLElement | undefined;
if (!config.showOffsets || !el || isTextNode(el) || (!config.showOffsetsSelected && state == 'Fixed')) {
if (!config.showOffsets || !(el instanceof HTMLElement) || (!config.showOffsetsSelected && state == 'Fixed')) {
editor.stopCommand(`${this.id}`, opts);
return;
}

9
src/common/index.ts

@ -7,7 +7,7 @@ export type Debounced = Function & { cancel(): void };
export type SetOptions = Backbone.ModelSetOptions & { avoidStore?: boolean };
export type AddOptions = Backbone.AddOptions & { temporary?: boolean };
export type AddOptions = Backbone.AddOptions & { temporary?: boolean; action?: string };
export type DisableOptions = { fromMove?: boolean };
@ -25,6 +25,13 @@ export type ObjectStrings = Record<string, string>;
export type Nullable = undefined | null | false;
export interface OptionAsDocument {
/**
* Treat the HTML string as document (option valid on the root component, eg. will include doctype, html, head, etc.).
*/
asDocument?: boolean;
}
// https://github.com/microsoft/TypeScript/issues/29729#issuecomment-1483854699
export type LiteralUnion<T, U> = T | (U & NOOP);

5
src/css_composer/index.ts

@ -39,6 +39,7 @@ import { ItemManagerModule } from '../abstract/Module';
import EditorModel from '../editor/model/Editor';
import Component from '../dom_components/model/Component';
import { ObjectAny, PrevToNewIdMap } from '../common';
import { UpdateStyleOptions } from '../domain_abstract/model/StyleableModel';
/** @private */
interface RuleOptions {
@ -53,7 +54,7 @@ interface RuleOptions {
}
/** @private */
interface SetRuleOptions extends RuleOptions {
interface SetRuleOptions extends RuleOptions, UpdateStyleOptions {
/**
* If the rule exists already, merge passed styles instead of replacing them.
*/
@ -61,7 +62,7 @@ interface SetRuleOptions extends RuleOptions {
}
/** @private */
export interface GetSetRuleOptions {
export interface GetSetRuleOptions extends UpdateStyleOptions {
state?: string;
mediaText?: string;
addOpts?: ObjectAny;

18
src/css_composer/model/CssRule.ts

@ -128,14 +128,14 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
clone(): CssRule {
const opts = { ...this.opt };
const attr = { ...this.attributes };
attr.selectors = this.get('selectors')!.map(s => s.clone()) as Selector[];
attr.selectors = this.get('selectors')!.map(s => s.clone() as Selector);
// @ts-ignore
return new this.constructor(attr, opts);
}
ensureSelectors(m: any, c: any, opts: any) {
const { em } = this;
const sm = em?.get('SelectorManager');
const sm = em?.Selectors;
const toListen = [this, 'change:selectors', this.ensureSelectors];
let sels = this.getSelectors() as any;
this.stopListening(...toListen);
@ -193,7 +193,7 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
escape: (str: string) => (CSS && CSS.escape ? CSS.escape(str) : str),
};
// @ts-ignore
const selectors = this.get('selectors').getFullString(0, selOpts);
const selectors = this.getSelectors().getFullString(0, selOpts);
const stateStr = state && !opts.skipState ? `:${state}` : '';
selectors && result.push(`${selectors}${stateStr}`);
addSelector && !opts.skipAdd && result.push(addSelector);
@ -235,12 +235,12 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
getDevice() {
const { em } = this;
const { atRuleType, mediaText } = this.attributes;
const devices = em?.get('DeviceManager').getDevices() || [];
const deviceDefault = devices.filter((d: any) => d.getWidthMedia() === '')[0];
const devices = em?.Devices.getDevices() || [];
const deviceDefault = devices.filter(d => d.getWidthMedia() === '')[0];
if (atRuleType !== 'media' || !mediaText) {
return deviceDefault || null;
}
return devices.filter((d: any) => d.getWidthMedia() === getMediaLength(mediaText))[0] || null;
return devices.filter(d => d.getWidthMedia() === getMediaLength(mediaText))[0] || null;
}
/**
@ -253,8 +253,8 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
getState() {
const { em } = this;
const stateValue = this.get('state');
const states = em?.get('SelectorManager').getStates() || [];
return states.filter((s: any) => s.getName() === stateValue)[0] || null;
const states = em?.Selectors.getStates() || [];
return states.filter(s => s.getName() === stateValue)[0] || null;
}
/**
@ -265,7 +265,7 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
* console.log(cmp?.toHTML());
*/
getComponent() {
const sel = this.getSelectors() as any;
const sel = this.getSelectors();
const sngl = sel.length == 1 && sel.at(0);
const cmpId = sngl && sngl.isId() && sngl.get('name');
return (cmpId && this.em?.Components.getById(cmpId)) || null;

151
src/dom_components/index.ts

@ -53,7 +53,7 @@
*
* @module Components
*/
import { debounce, isArray, isEmpty, isFunction, isString, result } from 'underscore';
import { debounce, isArray, isBoolean, isEmpty, isFunction, isString, isSymbol, result } from 'underscore';
import { ItemManagerModule } from '../abstract/Module';
import { AddOptions, ObjectAny } from '../common';
import EditorModel from '../editor/model/Editor';
@ -103,6 +103,19 @@ import ComponentWrapperView from './view/ComponentWrapperView';
import ComponentsView from './view/ComponentsView';
import ComponentDataVariable, { type as typeVariable } from './model/ComponentDataVariable';
import ComponentDataVariableView from './view/ComponentDataVariableView';
import ComponentHead, { type as typeHead } from './model/ComponentHead';
import {
getSymbolMain,
getSymbolInstances,
getSymbolsToUpdate,
isSymbolMain,
isSymbolInstance,
detachSymbolInstance,
isSymbolRoot,
} from './model/SymbolUtils';
import { ComponentsEvents, SymbolInfo } from './types';
import Symbols from './model/Symbols';
import { BlockProperties } from '../block_manager/model/Block';
export type ComponentEvent =
| 'component:create'
@ -136,6 +149,7 @@ export interface AddComponentTypeOptions {
isComponent?: (el: HTMLElement) => boolean | ComponentDefinitionDefined | undefined;
model?: Partial<ComponentModelDefinition> & ThisType<ComponentModelDefinition & Component>;
view?: Partial<ComponentViewDefinition> & ThisType<ComponentViewDefinition & ComponentView>;
block?: boolean | Partial<BlockProperties>;
extend?: string;
extendView?: string;
extendFn?: string[];
@ -258,15 +272,20 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
view: ComponentTextNodeView,
},
{
id: 'text',
model: ComponentText,
view: ComponentTextView,
id: typeHead,
model: ComponentHead,
view: ComponentView,
},
{
id: 'wrapper',
model: ComponentWrapper,
view: ComponentWrapperView,
},
{
id: 'text',
model: ComponentText,
view: ComponentTextView,
},
{
id: 'default',
model: Component,
@ -293,8 +312,11 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
//name = "DomComponents";
storageKey = 'components';
keySymbols = 'symbols';
shallow?: Component;
symbols: Symbols;
events = ComponentsEvents;
/**
* Initialize module. Called on a new instance of the editor with configurations passed
@ -304,18 +326,20 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
*/
constructor(em: EditorModel) {
super(em, 'DomComponents', new Components(undefined, { em }));
const { config } = this;
this.symbols = new Symbols([], { em, config, domc: this });
if (em) {
//@ts-ignore
this.config.components = em.config.components || this.config.components;
}
for (var name in defaults) {
for (let name in defaults) {
//@ts-ignore
if (!(name in this.config)) this.config[name] = defaults[name];
}
var ppfx = this.config.pStylePrefix;
const ppfx = this.config.pStylePrefix;
if (ppfx) this.config.stylePrefix = ppfx + this.config.stylePrefix;
// Load dependencies
@ -331,8 +355,14 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
return this;
}
postLoad() {
const { em, symbols } = this;
const { UndoManager } = em;
UndoManager.add(symbols);
}
load(data: any) {
return this.loadProjectData(data, {
const result = this.loadProjectData(data, {
onResult: (result: Component) => {
let wrapper = this.getWrapper()!;
@ -351,10 +381,16 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
}
},
});
this.symbols.reset(data[this.keySymbols] || []);
return result;
}
store() {
return {};
return {
[this.keySymbols]: this.symbols,
};
}
/**
@ -490,12 +526,12 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
*/
addType(type: string, methods: AddComponentTypeOptions) {
const { em } = this;
const { model = {}, view = {}, isComponent, extend, extendView, extendFn = [], extendFnView = [] } = methods;
const { model = {}, view = {}, isComponent, extend, extendView, extendFn = [], extendFnView = [], block } = methods;
const compType = this.getType(type);
const extendType = this.getType(extend!);
const extendViewType = this.getType(extendView!);
const typeToExtend = extendType ? extendType : compType ? compType : this.getType('default');
const modelToExt = typeToExtend.model;
const modelToExt = typeToExtend.model as typeof Component;
const viewToExt = extendViewType ? extendViewType.view : typeToExtend.view;
// Function for extending source object methods
@ -516,12 +552,16 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
if (typeof model === 'object') {
const modelDefaults = { defaults: model.defaults };
delete model.defaults;
const typeExtends = new Set(modelToExt.typeExtends);
typeExtends.add(modelToExt.getDefaults().type);
methods.model = modelToExt.extend(
{
...model,
...getExtendedObj(extendFn, model, modelToExt),
},
{
typeExtends,
isComponent: compType && !extendType && !isComponent ? modelToExt.isComponent : isComponent || (() => 0),
}
);
@ -550,6 +590,16 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
this.componentTypes.unshift(methods as any);
}
if (block) {
const defBlockProps: BlockProperties = {
id: type,
label: type,
content: { type },
};
const blockProps: BlockProperties = block === true ? defBlockProps : { ...defBlockProps, ...block };
em.Blocks.add(blockProps.id || type, blockProps);
}
const event = `component:type:${compType ? 'update' : 'add'}`;
em?.trigger(event, compType || methods);
@ -673,6 +723,87 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
return isComponent(obj);
}
/**
* Add a new symbol from a component.
* If the passed component is not a symbol, it will be converted to an instance and will return the main symbol.
* If the passed component is already an instance, a new instance will be created and returned.
* If the passed component is the main symbol, a new instance will be created and returned.
* @param {[Component]} component Component from which create a symbol.
* @returns {[Component]}
* @example
* const symbol = cmp.addSymbol(editor.getSelected());
* // cmp.getSymbolInfo(symbol).isSymbol === true;
*/
addSymbol(component: Component) {
if (isSymbol(component) && !isSymbolRoot(component)) {
return;
}
const symbol = component.clone({ symbol: true });
isSymbolMain(symbol) && this.symbols.add(symbol);
this.em.trigger('component:toggled');
return symbol;
}
/**
* Get the array of main symbols.
* @returns {Array<[Component]>}
* @example
* const symbols = cmp.getSymbols();
* // [Component, Component, ...]
* // Removing the main symbol will detach all the relative instances.
* symbols[0].remove();
*/
getSymbols() {
return [...this.symbols.models];
}
/**
* Detach symbol instance from the main one.
* The passed symbol instance will become a regular component.
* @param {[Component]} component The component symbol to detach.
* @example
* const cmpInstance = editor.getSelected();
* // cmp.getSymbolInfo(cmpInstance).isInstance === true;
* cmp.detachSymbol(cmpInstance);
* // cmp.getSymbolInfo(cmpInstance).isInstance === false;
*/
detachSymbol(component: Component) {
if (isSymbolInstance(component)) {
detachSymbolInstance(component);
}
}
/**
* Get info about the symbol.
* @param {[Component]} component Component symbol from which to get the info.
* @returns {Object} Object containing symbol info.
* @example
* cmp.getSymbolInfo(editor.getSelected());
* // > { isSymbol: true, isMain: false, isInstance: true, ... }
*/
getSymbolInfo(component: Component, opts: { withChanges?: string } = {}): SymbolInfo {
const isMain = isSymbolMain(component);
const mainRef = getSymbolMain(component);
const isInstance = !!mainRef;
const instances = (isMain ? getSymbolInstances(component) : getSymbolInstances(mainRef)) || [];
const main = mainRef || (isMain ? component : undefined);
const relatives = getSymbolsToUpdate(component, { changed: opts.withChanges });
const isSymbol = isMain || isInstance;
const isRoot = isSymbol && isSymbolRoot(component);
return {
isSymbol,
isMain,
isInstance,
isRoot,
main,
instances: instances,
relatives: relatives || [],
};
}
/**
* Check if a component can be moved inside another one.
* @param {[Component]} target The target component is the one that is supposed to receive the source one.

426
src/dom_components/model/Component.ts

@ -12,7 +12,7 @@ import {
keys,
} from 'underscore';
import { shallowDiff, capitalize, isEmptyObj, isObject, toLowerCase } from '../../utils/mixins';
import StyleableModel, { StyleProps } from '../../domain_abstract/model/StyleableModel';
import StyleableModel, { StyleProps, UpdateStyleOptions } from '../../domain_abstract/model/StyleableModel';
import { Model } from 'backbone';
import Components from './Components';
import Selector from '../../selector_manager/model/Selector';
@ -20,6 +20,7 @@ import Selectors from '../../selector_manager/model/Selectors';
import Traits from '../../trait_manager/model/Traits';
import EditorModel from '../../editor/model/Editor';
import {
AddComponentsOption,
ComponentAdd,
ComponentDefinition,
ComponentDefinitionDefined,
@ -37,9 +38,24 @@ import CssRule, { CssRuleJSON } from '../../css_composer/model/CssRule';
import Trait from '../../trait_manager/model/Trait';
import { ToolbarButtonProps } from './ToolbarButton';
import { TraitProperties } from '../../trait_manager/types';
import { ActionLabelComponents, ComponentsEvents } from '../types';
import ItemView from '../../navigator/view/ItemView';
import {
getSymbolMain,
getSymbolInstances,
initSymbol,
isSymbol,
isSymbolMain,
isSymbolRoot,
updateSymbolCls,
updateSymbolComps,
updateSymbolProps,
} from './SymbolUtils';
export interface IComponent extends ExtractMethods<Component> {}
export interface SetAttrOptions extends SetOptions, UpdateStyleOptions {}
const escapeRegExp = (str: string) => {
return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
};
@ -50,8 +66,8 @@ export const eventDrag = 'component:drag';
export const keySymbols = '__symbols';
export const keySymbol = '__symbol';
export const keySymbolOvrd = '__symbol_ovrd';
export const keyUpdate = 'component:update';
export const keyUpdateInside = `${keyUpdate}-inside`;
export const keyUpdate = ComponentsEvents.update;
export const keyUpdateInside = ComponentsEvents.updateInside;
/**
* The Component object represents a single node of our template structure, so when you update its properties the changes are
@ -89,7 +105,7 @@ export const keyUpdateInside = `${keyUpdate}-inside`;
* @property {Array<String>} [unstylable=[]] Indicate an array of style properties which should be hidden from the style manager. Default: `[]`
* @property {Boolean} [highlightable=true] It can be highlighted with 'dotted' borders if true. Default: `true`
* @property {Boolean} [copyable=true] True if it's possible to clone the component. Default: `true`
* @property {Boolean} [resizable=false] Indicates if it's possible to resize the component. It's also possible to pass an object as [options for the Resizer](https://github.com/GrapesJS/grapesjs/blob/master/src/utils/Resizer.js). Default: `false`
* @property {Boolean} [resizable=false] Indicates if it's possible to resize the component. It's also possible to pass an object as [options for the Resizer](https://github.com/GrapesJS/grapesjs/blob/master/src/utils/Resizer.ts). Default: `false`
* @property {Boolean} [editable=false] Allow to edit the content of the component (used on Text components). Default: `false`
* @property {Boolean} [layerable=true] Set to `false` if you need to hide the component inside Layers. Default: `true`
* @property {Boolean} [selectable=true] Allow component to be selected when clicked. Default: `true`
@ -164,6 +180,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
};
}
get tagName() {
return this.get('tagName')!;
}
get classes() {
return this.get('classes')!;
}
@ -216,6 +236,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
ccid!: string;
views!: ComponentView[];
view?: ComponentView;
viewLayer?: ItemView;
frame?: Frame;
rule?: CssRule;
prevColl?: Components;
@ -226,7 +247,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @ts-ignore */
collection!: Components;
initialize(props = {}, opt: ComponentOptions = {}) {
constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt);
bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps');
const em = opt.em;
@ -290,8 +312,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.__postAdd();
this.init();
this.__isSymbolOrInst() && this.__initSymb();
em && em.trigger('component:create', this);
isSymbol(this) && initSymbol(this);
em?.trigger(ComponentsEvents.create, this, opt);
}
}
@ -309,7 +331,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
__postRemove() {
const { em } = this;
const um = em?.get('UndoManager');
const um = em?.UndoManager;
if (um) {
um.remove(this.components());
um.remove(this.getSelectors());
@ -359,6 +381,23 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.emitUpdate('toolbar');
}
__getAllById() {
const { em } = this;
return em ? em.Components.allById() : {};
}
__upSymbProps(m: any, opts: SymbolToUpOptions = {}) {
updateSymbolProps(this, opts);
}
__upSymbCls(m: any, c: any, opts = {}) {
updateSymbolCls(this, opts);
}
__upSymbComps(m: Component, c: Components, o: any) {
updateSymbolComps(this, m, c, o);
}
/**
* Check component's type
* @param {string} type Component type
@ -406,6 +445,26 @@ export default class Component extends StyleableModel<ComponentProperties> {
return this.get('dmode') || '';
}
/**
* Set symbol override.
* By setting override to `true`, none of its property changes will be propagated to relative symbols.
* By setting override to specific properties, changes of those properties will be skipped from propagation.
* @param {Boolean|String|Array<String>} value
* @example
* component.setSymbolOverride(['children', 'classes']);
*/
setSymbolOverride(value?: boolean | string | string[]) {
this.set(keySymbolOvrd, (isString(value) ? [value] : value) ?? 0);
}
/**
* Get symbol override value.
* @returns {Boolean|Array<String>}
*/
getSymbolOverride(): boolean | string[] | undefined {
return this.get(keySymbolOvrd);
}
/**
* Find inner components by query string.
* **ATTENTION**: this method works only with already rendered component
@ -523,7 +582,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
const coll = this.collection;
const at = coll.indexOf(this);
coll.remove(this);
const result = coll.add(el, { ...opts, at });
const result = coll.add(el, { ...opts, at }) as C | C[];
return isArray(result) ? result : [result];
}
@ -531,7 +590,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* Emit changes for each updated attribute
* @private
*/
attrUpdated(m: any, v: any, opts: any = {}) {
attrUpdated(m: any, v: any, opts: SetAttrOptions = {}) {
const attrs = this.get('attributes')!;
// Handle classes
const classes = attrs.class;
@ -540,12 +599,16 @@ export default class Component extends StyleableModel<ComponentProperties> {
// Handle style
const style = attrs.style;
style && this.setStyle(style);
style && this.setStyle(style, opts);
delete attrs.style;
const attrPrev = { ...this.previous('attributes') };
const diff = shallowDiff(attrPrev, this.get('attributes')!);
keys(diff).forEach(pr => this.trigger(`change:attributes:${pr}`, this, diff[pr], opts));
keys(diff).forEach(pr => {
const attrKey = `attributes:${pr}`;
this.trigger(`change:${attrKey}`, this, diff[pr], opts);
this.em?.trigger(`${keyUpdate}:${attrKey}`, this, diff[pr], opts);
});
}
/**
@ -556,7 +619,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @example
* component.setAttributes({ id: 'test', 'data-key': 'value' });
*/
setAttributes(attrs: ObjectAny, opts: SetOptions = {}) {
setAttributes(attrs: ObjectAny, opts: SetAttrOptions = {}) {
this.set('attributes', { ...attrs }, opts);
return this;
}
@ -569,7 +632,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @example
* component.addAttributes({ 'data-key': 'value' });
*/
addAttributes(attrs: ObjectAny, opts: SetOptions = {}) {
addAttributes(attrs: ObjectAny, opts: SetAttrOptions = {}) {
return this.setAttributes(
{
...this.getAttributes({ noClass: true }),
@ -613,6 +676,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
if (rule) {
return rule.getStyle(prop);
}
// Return empty style if not rule have been found. We cannot return inline style with the next return
// because else on load inline style is set a #id or .class style
return {};
}
return super.getStyle.call(this, prop);
@ -625,7 +692,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @example
* component.setStyle({ color: 'red' });
*/
setStyle(prop: StyleProps = {}, opts: any = {}) {
setStyle(prop: StyleProps = {}, opts: UpdateStyleOptions = {}) {
const { opt, em } = this;
if (avoidInline(em) && !opt.temporary && !opts.inline) {
@ -671,9 +738,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
// Add style
if (!opts.noStyle) {
const style = this.get('style');
const style = this.getStyle({ inline: true });
if (isObject(style) && !isEmptyObj(style)) {
attributes.style = this.styleToString({ inline: 1 });
attributes.style = this.styleToString({ inline: true });
}
}
@ -689,8 +756,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
if (
// Symbols should always have an id
this.__getSymbol() ||
this.__getSymbols() ||
isSymbol(this) ||
// Components with script should always have an id
this.get('script-export') ||
this.get('script')
@ -773,254 +839,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
return classStr ? classStr.split(' ') : [];
}
__logSymbol(type: string, toUp: Component[], opts: any = {}) {
const symbol = this.__getSymbol();
const symbols = this.__getSymbols();
if (!symbol && !symbols) return;
this.em.log(type, { model: this, toUp, context: 'symbols', opts });
}
__initSymb() {
if (this.__symbReady) return;
this.on('change', this.__upSymbProps);
this.__symbReady = true;
}
__isSymbol() {
return isArray(this.get(keySymbols));
}
__isSymbolOrInst() {
return !!(this.__isSymbol() || this.get(keySymbol));
}
__isSymbolTop() {
const parent = this.parent();
const symb = this.__isSymbolOrInst();
return symb && (!parent || (parent && !parent.__isSymbol() && !parent.__getSymbol()));
}
__isSymbolNested() {
if (!this.__isSymbolOrInst() || this.__isSymbolTop()) return false;
const symbTopSelf = (this.__isSymbol() ? this : this.__getSymbol())!.__getSymbTop();
const symbTop = this.__getSymbTop();
const symbTopMain = symbTop.__isSymbol() ? symbTop : symbTop.__getSymbol();
return symbTopMain !== symbTopSelf;
}
__getAllById() {
const { em } = this;
return em ? em.Components.allById() : {};
}
__getSymbol(): Component | undefined {
let symb = this.get(keySymbol);
if (symb && isString(symb)) {
const ref = this.__getAllById()[symb];
if (ref) {
symb = ref;
this.set(keySymbol, ref);
} else {
symb = 0;
}
}
return symb;
}
__getSymbols(): Component[] | undefined {
let symbs = this.get(keySymbols);
if (symbs && isArray(symbs)) {
symbs.forEach((symb, idx) => {
if (symb && isString(symb)) {
symbs[idx] = this.__getAllById()[symb];
}
});
symbs = symbs.filter(symb => symb && !isString(symb));
}
return symbs;
}
__isSymbOvrd(prop = '') {
const ovrd = this.get(keySymbolOvrd);
const [prp] = prop.split(':');
const props = prop !== prp ? [prop, prp] : [prop];
return ovrd === true || (isArray(ovrd) && props.some(p => ovrd.indexOf(p) >= 0));
}
__getSymbToUp(opts: SymbolToUpOptions = {}) {
let result: Component[] = [];
const { changed } = opts;
if (
opts.fromInstance ||
opts.noPropagate ||
opts.fromUndo ||
// Avoid updating others if the current component has override
(changed && this.__isSymbOvrd(changed))
) {
return result;
}
const symbols = this.__getSymbols() || [];
const symbol = this.__getSymbol();
const all = symbol ? [symbol, ...(symbol.__getSymbols() || [])] : symbols;
result = all
.filter(s => s !== this)
// Avoid updating those with override
.filter(s => !(changed && s.__isSymbOvrd(changed)));
return result;
}
__getSymbTop(opts?: any) {
let result: Component = this;
let parent = this.parent(opts);
while (parent && (parent.__isSymbol() || parent.__getSymbol())) {
result = parent;
parent = parent.parent(opts);
}
return result;
}
__upSymbProps(m: any, opts: SymbolToUpOptions = {}) {
const changed = this.changedAttributes() || {};
const attrs = changed.attributes || {};
delete changed.status;
delete changed.open;
delete changed[keySymbols];
delete changed[keySymbol];
delete changed[keySymbolOvrd];
delete changed.attributes;
delete attrs.id;
if (!isEmptyObj(attrs)) changed.attributes = attrs;
if (!isEmptyObj(changed)) {
const toUp = this.__getSymbToUp(opts);
// Avoid propagating overrides to other symbols
keys(changed).map(prop => {
if (this.__isSymbOvrd(prop)) delete changed[prop];
});
this.__logSymbol('props', toUp, { opts, changed });
toUp.forEach(child => {
const propsChanged = { ...changed };
// Avoid updating those with override
keys(propsChanged).map(prop => {
if (child.__isSymbOvrd(prop)) delete propsChanged[prop];
});
child.set(propsChanged, { fromInstance: this, ...opts });
});
}
}
__upSymbCls(m: any, c: any, opts = {}) {
const toUp = this.__getSymbToUp(opts);
this.__logSymbol('classes', toUp, { opts });
toUp.forEach(child => {
// @ts-ignore This will propagate the change up to __upSymbProps
child.set('classes', this.get('classes'), { fromInstance: this });
});
this.__changesUp(opts);
}
__upSymbComps(m: Component, c: Components, o: any) {
const optUp = o || c || {};
const { fromInstance, fromUndo } = optUp;
const toUpOpts = { fromInstance, fromUndo };
const isTemp = m.opt.temporary;
// Reset
if (!o) {
const toUp = this.__getSymbToUp({
...toUpOpts,
changed: 'components:reset',
});
// @ts-ignore
const cmps = m.models as Component[];
this.__logSymbol('reset', toUp, { components: cmps });
toUp.forEach(symb => {
const newMods = cmps.map(mod => mod.clone({ symbol: true }));
// @ts-ignore
symb.components().reset(newMods, { fromInstance: this, ...c });
});
// Add
} else if (o.add) {
let addedInstances: Component[] = [];
const isMainSymb = !!this.__getSymbols();
const toUp = this.__getSymbToUp({
...toUpOpts,
changed: 'components:add',
});
if (toUp.length) {
const addSymb = m.__getSymbol();
addedInstances = (addSymb ? addSymb.__getSymbols() : m.__getSymbols()) || [];
addedInstances = [...addedInstances];
addedInstances.push(addSymb ? addSymb : m);
}
!isTemp &&
this.__logSymbol('add', toUp, {
opts: o,
addedInstances: addedInstances.map(c => c.cid),
added: m.cid,
});
// Here, before appending a new symbol, I have to ensure there are no previously
// created symbols (eg. used mainly when drag components around)
toUp.forEach(symb => {
const symbTop = symb.__getSymbTop();
const symbPrev = addedInstances.filter(addedInst => {
const addedTop = addedInst.__getSymbTop({ prev: 1 });
return symbTop && addedTop && addedTop === symbTop;
})[0];
const toAppend = symbPrev || m.clone({ symbol: true, symbolInv: isMainSymb });
symb.append(toAppend, { fromInstance: this, ...o });
});
// Remove
} else {
// Remove instance reference from the symbol
const symb = m.__getSymbol();
symb &&
!o.temporary &&
symb.set(
keySymbols,
symb.__getSymbols()!.filter(i => i !== m)
);
// Propagate remove only if the component is an inner symbol
if (!m.__isSymbolTop()) {
const changed = 'components:remove';
const { index } = o;
const parent = m.parent();
const opts = { fromInstance: m, ...o };
const isSymbNested = m.__isSymbolNested();
let toUpFn = (symb: Component) => {
const symbPrnt = symb.parent();
symbPrnt && !symbPrnt.__isSymbOvrd(changed) && symb.remove(opts);
};
// Check if the parent allows the removing
let toUp = !parent?.__isSymbOvrd(changed) ? m.__getSymbToUp(toUpOpts) : [];
if (isSymbNested) {
toUp = parent?.__getSymbToUp({ ...toUpOpts, changed })!;
toUpFn = symb => {
const toRemove = symb.components().at(index);
toRemove && toRemove.remove({ fromInstance: parent, ...opts });
};
}
!isTemp &&
this.__logSymbol('remove', toUp, {
opts: o,
removed: m.cid,
isSymbNested,
});
toUp.forEach(toUpFn);
}
}
this.__changesUp(optUp);
}
initClasses(m?: any, c?: any, opts: any = {}) {
const event = 'change:classes';
const { class: attrCls, ...restAttr } = this.get('attributes') || {};
@ -1125,8 +943,11 @@ export default class Component extends StyleableModel<ComponentProperties> {
return comp;
}
});
const result = this.components().add(toAppend, opts);
return isArray(result) ? result : [result];
const result = this.components().add(toAppend, {
action: ActionLabelComponents.add,
...opts,
});
return result as T[];
}
/**
@ -1145,7 +966,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
components<T extends ComponentAdd | undefined>(
components?: T,
opts: any = {}
opts: AddComponentsOption = {}
): undefined extends T ? Components : Component[] {
const coll = this.get('components')!;
@ -1409,8 +1230,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
/**
* Override original clone method
* @private
*/
clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}) {
* @ts-ignore */
clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}): this {
const em = this.em;
const attr = { ...this.attributes };
const opts = { ...this.opt };
@ -1425,7 +1246,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
// @ts-ignore
attr.traits = [];
if (this.__isSymbolTop()) {
if (isSymbolRoot(this)) {
opt.symbol = true;
}
@ -1461,32 +1282,32 @@ export default class Component extends StyleableModel<ComponentProperties> {
// Symbols
// If I clone an inner symbol, I have to reset it
cloned.set(keySymbols, 0);
const symbol = this.__getSymbol();
const symbols = this.__getSymbols();
const symbol = getSymbolMain(this);
const symbols = getSymbolInstances(this);
if (!opt.symbol && (symbol || symbols)) {
cloned.set(keySymbol, 0);
cloned.set(keySymbols, 0);
} else if (symbol) {
// Contains already a reference to a symbol
symbol.set(keySymbols, [...symbol.__getSymbols()!, cloned]);
cloned.__initSymb();
symbol.set(keySymbols, [...getSymbolInstances(symbol)!, cloned]);
initSymbol(cloned);
} else if (opt.symbol) {
// Request to create a symbol
if (this.__isSymbol()) {
if (isSymbolMain(this)) {
// Already a symbol, cloned should be an instance
this.set(keySymbols, [...symbols!, cloned]);
cloned.set(keySymbol, this);
cloned.__initSymb();
initSymbol(cloned);
} else if (opt.symbolInv) {
// Inverted, cloned is the instance, the origin is the main symbol
this.set(keySymbols, [cloned]);
cloned.set(keySymbol, this);
[this, cloned].map(i => i.__initSymb());
[this, cloned].map(i => initSymbol(i));
} else {
// Cloned becomes the main symbol
cloned.set(keySymbols, [this]);
[this, cloned].map(i => i.__initSymb());
[this, cloned].map(i => initSymbol(i));
this.set(keySymbol, cloned);
}
}
@ -1526,6 +1347,14 @@ export default class Component extends StyleableModel<ComponentProperties> {
);
}
/**
* Update component name.
* @param {String} name New name.
*/
setName(name?: string, opts: SetOptions = {}) {
this.set('custom-name', name, opts);
}
/**
* Get the icon string
* @return {String}
@ -1572,7 +1401,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
const tag = customTag || model.get('tagName');
const sTag = model.get('void');
const customAttr = opts.attributes;
let attributes = this.getAttrToHTML();
let attributes = this.getAttrToHTML(opts);
delete opts.tag;
// Get custom attributes if requested
@ -1590,7 +1419,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
forEach(props, (value, key) => {
const skipProps = ['classes', 'attributes', 'components'];
if (key[0] !== '_' && skipProps.indexOf(key) < 0) {
attributes[`data-gjs-${key}`] = isArray(value) || isObject(value) ? JSON.stringify(value) : value;
attributes[`data-gjs-${key}`] =
isArray(value) || isObject(value) ? JSON.stringify(value) : isBoolean(value) ? `${value}` : value;
}
});
}
@ -1642,10 +1472,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @return {Object}
* @private
*/
getAttrToHTML() {
getAttrToHTML(opts?: ToHTMLOptions) {
const attrs = this.getAttributes();
if (avoidInline(this.em)) {
if (avoidInline(this.em) && opts?.keepInlineStyle !== true) {
delete attrs.style;
}
@ -1881,7 +1711,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
const { em } = this;
const coll = this.collection;
const remove = () => {
coll && coll.remove(this, { ...opts, action: 'remove-component' });
coll && coll.remove(this, { action: ActionLabelComponents.remove, ...opts });
// Component without parent
if (!coll) {
this.components('', opts);
@ -1889,7 +1719,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
}
};
const rmOpts = { ...opts };
[this, em].map(i => i.trigger('component:remove:before', this, remove, rmOpts));
[this, em].map(i => i.trigger(ComponentsEvents.removeBefore, this, remove, rmOpts));
!rmOpts.abort && remove();
return this;
}
@ -1915,8 +1745,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
if (sameParent && at && at > index) {
opts.at = at - 1;
}
this.remove({ temporary: 1 });
component.append(this, opts);
const action = ActionLabelComponents.move;
this.remove({ action, temporary: 1 });
component.append(this, { action, ...opts });
this.emitUpdate();
}
}
@ -1940,7 +1771,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
if (!cmp) return false;
return this instanceof cmp;
// A tiny hack to make isInstanceOf work properly where there a multiple inheritance
const { typeExtends } = this.constructor as typeof Component;
return this instanceof cmp || typeExtends.has(type);
}
/**
@ -2030,11 +1864,13 @@ export default class Component extends StyleableModel<ComponentProperties> {
selector && selector.set({ name: id, label: id });
}
static typeExtends = new Set<string>();
static getDefaults() {
return result(this.prototype, 'defaults');
}
static isComponent(el: HTMLElement): ComponentDefinitionDefined | boolean | undefined {
static isComponent(el: HTMLElement, opts?: any): ComponentDefinitionDefined | boolean | undefined {
return { tagName: toLowerCase(el.tagName) };
}
@ -2104,11 +1940,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
}
static getList(model: Component) {
const { opt = {} } = model;
// @ts-ignore
const { domc, em } = opt;
const dm = domc || em?.Components;
return dm ? dm.componentsById : {};
const { em } = model;
const dm = em?.Components;
return dm?.componentsById ?? {};
}
static checkId(

24
src/dom_components/model/ComponentHead.ts

@ -0,0 +1,24 @@
import Component from './Component';
import { toLowerCase } from '../../utils/mixins';
import { DraggableDroppableFn } from './types';
export const type = 'head';
const droppable = ['title', 'style', 'base', 'link', 'meta', 'script', 'noscript'];
export default class ComponentHead extends Component {
get defaults() {
return {
// @ts-ignore
...super.defaults,
type,
tagName: type,
draggable: false,
highlightable: false,
droppable: (({ tagName }) => !tagName || droppable.includes(toLowerCase(tagName))) as DraggableDroppableFn,
};
}
static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === type;
}
}

5
src/dom_components/model/ComponentImage.ts

@ -2,6 +2,7 @@ import { result } from 'underscore';
import Component from './Component';
import { toLowerCase, buildBase64UrlFromSvg, hasWin } from '../../utils/mixins';
import { ObjectStrings } from '../../common';
import { ComponentOptions, ComponentProperties } from './types';
const svgAttrs =
'xmlns="http://www.w3.org/2000/svg" width="100" viewBox="0 0 24 24" style="fill: rgba(0,0,0,0.15); transform: scale(0.75)"';
@ -35,8 +36,8 @@ export default class ComponentImage extends Component {
};
}
initialize(props: any, opts: any) {
super.initialize(props, opts);
constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt);
const { src } = this.get('attributes')!;
if (src && buildBase64UrlFromSvg(result(this, 'defaults').src) !== src) {
this.set('src', src, { silent: true });

6
src/dom_components/model/ComponentMap.ts

@ -1,5 +1,6 @@
import ComponentImage from './ComponentImage';
import { toLowerCase } from '../../utils/mixins';
import { ComponentOptions, ComponentProperties } from './types';
export default class ComponentMap extends ComponentImage {
/** @ts-ignore */
@ -49,10 +50,11 @@ export default class ComponentMap extends ComponentImage {
};
}
initialize(props: any, opts: any) {
constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt);
if (this.get('src')) this.parseFromSrc();
else this.updateSrc();
super.initialize(props, opts);
this.listenTo(this, 'change:address change:zoom change:mapType', this.updateSrc);
}

5
src/dom_components/model/ComponentTable.ts

@ -1,5 +1,6 @@
import Component from './Component';
import { toLowerCase } from '../../utils/mixins';
import { ComponentOptions, ComponentProperties } from './types';
const type = 'table';
@ -14,8 +15,8 @@ export default class ComponentTable extends Component {
};
}
initialize(props: any, opts: any) {
super.initialize(props, opts);
constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt);
const components = this.get('components')!;
!components.length && components.add({ type: 'tbody' });
}

5
src/dom_components/model/ComponentTableBody.ts

@ -1,5 +1,6 @@
import Component from './Component';
import { toLowerCase } from '../../utils/mixins';
import { ComponentOptions, ComponentProperties } from './types';
const type = 'tbody';
@ -17,8 +18,8 @@ export default class ComponentTableBody extends Component {
};
}
initialize(props: any, opts: any) {
super.initialize(props, opts);
constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt);
const components = this.get('components')!;
let columns = this.get('columns');
let rows = this.get('rows');

5
src/dom_components/model/ComponentText.ts

@ -1,5 +1,6 @@
import { isFunction } from 'underscore';
import Component from './Component';
import { ComponentOptions, ComponentProperties } from './types';
export default class ComponentText extends Component {
get defaults() {
@ -12,8 +13,8 @@ export default class ComponentText extends Component {
};
}
initialize(props: any, opts: any) {
super.initialize(props, opts);
constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt);
this.__checkInnerChilds();
}

6
src/dom_components/model/ComponentVideo.ts

@ -1,6 +1,7 @@
import { ObjectAny } from '../../common';
import { isDef, isEmptyObj, toLowerCase } from '../../utils/mixins';
import ComponentImage from './ComponentImage';
import { ComponentOptions, ComponentProperties } from './types';
const type = 'video';
const yt = 'yt';
@ -38,14 +39,13 @@ export default class ComponentVideo extends ComponentImage {
};
}
initialize(props: any, opts: any) {
this.em = opts.em;
constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt);
if (this.get('src')) this.parseFromSrc();
this.updatePropsFromAttr();
this.updateTraits();
this.on('change:provider', this.updateTraits);
this.on('change:videoId change:provider', this.updateSrc);
super.initialize(props, opts);
}
updatePropsFromAttr() {

47
src/dom_components/model/ComponentWrapper.ts

@ -1,4 +1,8 @@
import { isUndefined } from 'underscore';
import { attrToString } from '../../utils/dom';
import Component from './Component';
import ComponentHead, { type as typeHead } from './ComponentHead';
import { ToHTMLOptions } from './types';
export default class ComponentWrapper extends Component {
get defaults() {
@ -11,6 +15,9 @@ export default class ComponentWrapper extends Component {
draggable: false,
components: [],
traits: [],
doctype: '',
head: null,
docEl: null,
stylable: [
'background',
'background-color',
@ -23,6 +30,46 @@ export default class ComponentWrapper extends Component {
};
}
constructor(...args: ConstructorParameters<typeof Component>) {
super(...args);
const opts = args[1];
const cmp = opts?.em?.Components;
const CmpHead = cmp?.getType(typeHead)?.model;
const CmpDef = cmp?.getType('default').model;
if (CmpHead) {
this.set(
{
head: new CmpHead({}, opts),
docEl: new CmpDef({ tagName: 'html' }, opts),
},
{ silent: true }
);
}
}
get head(): ComponentHead {
return this.get('head');
}
get docEl(): Component {
return this.get('docEl');
}
get doctype(): string {
return this.attributes.doctype || '';
}
toHTML(opts: ToHTMLOptions = {}) {
const { doctype } = this;
const asDoc = !isUndefined(opts.asDocument) ? opts.asDocument : !!doctype;
const { head, docEl } = this;
const body = super.toHTML(opts);
const headStr = (asDoc && head?.toHTML(opts)) || '';
const docElAttr = (asDoc && attrToString(docEl?.getAttrToHTML())) || '';
const docElAttrStr = docElAttr ? ` ${docElAttr}` : '';
return asDoc ? `${doctype}<html${docElAttrStr}>${headStr}${body}</html>` : body;
}
__postAdd() {
const um = this.em?.UndoManager;
!this.__hasUm && um?.add(this);

145
src/dom_components/model/Components.ts

@ -1,12 +1,22 @@
import { isEmpty, isArray, isString, isFunction, each, includes, extend, flatten, keys } from 'underscore';
import Component from './Component';
import { AddOptions, Collection, ObjectAny } from '../../common';
import { AddOptions, Collection, OptionAsDocument } from '../../common';
import { DomComponentsConfig } from '../config/config';
import EditorModel from '../../editor/model/Editor';
import ComponentManager from '..';
import CssRule from '../../css_composer/model/CssRule';
import { ComponentAdd, ComponentProperties } from './types';
import {
ComponentAdd,
ComponentAddType,
ComponentDefinition,
ComponentDefinitionDefined,
ComponentProperties,
} from './types';
import ComponentText from './ComponentText';
import ComponentWrapper from './ComponentWrapper';
import { ComponentsEvents } from '../types';
import { isSymbolInstance, isSymbolRoot, updateSymbolComps } from './SymbolUtils';
export const getComponentIds = (cmp?: Component | Component[] | Components, res: string[] = []) => {
if (!cmp) return [];
@ -69,33 +79,42 @@ const getComponentsFromDefs = (
};
export interface ComponentsOptions {
em?: EditorModel;
em: EditorModel;
config?: DomComponentsConfig;
domc?: ComponentManager;
}
interface AddComponentOptions extends AddOptions {
previousModels?: Component[];
keepIds?: string[];
}
export default class Components extends Collection</**
* Keep this format to avoid errors in TS bundler */
/** @ts-ignore */
Component> {
opt!: ComponentsOptions;
config?: DomComponentsConfig;
em!: EditorModel;
em: EditorModel;
domc?: ComponentManager;
parent?: Component;
__firstAdd?: any;
initialize(models: any, opt: ComponentsOptions = {}) {
constructor(models: any, opt: ComponentsOptions) {
super(models, opt);
this.opt = opt;
this.listenTo(this, 'add', this.onAdd);
this.listenTo(this, 'remove', this.removeChildren);
this.listenTo(this, 'reset', this.resetChildren);
const { em, config } = opt;
this.config = config;
this.em = em!;
this.em = em;
this.domc = opt.domc || em?.Components;
}
get events() {
return this.domc?.events!;
}
resetChildren(models: Components, opts: { previousModels?: Component[]; keepIds?: string[] } = {}) {
const coll = this;
const prev = opts.previousModels || [];
@ -175,16 +194,21 @@ Component> {
sels.remove(rulesRemoved.map(rule => rule.getSelectors().at(0)));
if (!removed.opt.temporary) {
em.Commands.run('core:component-style-clear', {
target: removed,
});
em.Commands.run('core:component-style-clear', { target: removed });
removed.removed();
removed.trigger('removed');
em.trigger('component:remove', removed);
em.trigger(ComponentsEvents.remove, removed);
if (domc && isSymbolInstance(removed) && isSymbolRoot(removed)) {
domc.symbols.__trgEvent(domc.events.symbolInstanceRemove, { component: removed }, true);
}
}
const inner = removed.components();
inner.forEach(it => this.removeChildren(it, coll, opts));
inner.forEach(it => {
updateSymbolComps(it, it, inner, { ...opts, skipRefsUp: true });
this.removeChildren(it, coll, opts);
});
}
// Remove stuff registered in DomComponents.handleChanges
@ -228,12 +252,27 @@ Component> {
return new model(attrs, options) as Component;
}
parseString(value: string, opt: AddOptions & { temporary?: boolean; keepIds?: string[] } = {}) {
const { em, domc } = this;
parseString(value: string, opt: AddOptions & OptionAsDocument & { temporary?: boolean; keepIds?: string[] } = {}) {
const { em, domc, parent } = this;
const asDocument = opt.asDocument && parent?.is('wrapper');
const cssc = em.Css;
const parsed = em.Parser.parseHtml(value);
const parsed = em.Parser.parseHtml(value, { asDocument });
let components = parsed.html;
if (asDocument) {
const root = parent as ComponentWrapper;
const { components: bodyCmps, ...restBody } = (parsed.html as ComponentDefinitionDefined) || {};
const { components: headCmps, ...restHead } = parsed.head || {};
components = bodyCmps!;
root.set(restBody as any, opt);
root.head.set(restHead as any, opt);
root.head.components(headCmps, opt);
root.docEl.set(parsed.root as any, opt);
root.set({ doctype: parsed.doctype });
}
// We need this to avoid duplicate IDs
Component.checkId(parsed.html!, parsed.css, domc!.componentsById, opt);
Component.checkId(components, parsed.css, domc!.componentsById, opt);
if (parsed.css && cssc && !opt.temporary) {
const { at, ...optsToPass } = opt;
@ -243,17 +282,20 @@ Component> {
});
}
return parsed.html;
return components;
}
/** @ts-ignore */
add(models: ComponentAdd, opt: AddOptions & { previousModels?: Component[]; keepIds?: string[] } = {}) {
add(model: Exclude<ComponentAddType, string>, opt?: AddComponentOptions): Component;
add(models: ComponentAddType[], opt?: AddComponentOptions): Component[];
add(models: ComponentAdd, opt?: AddComponentOptions): Component | Component[];
add(models: unknown, opt: AddComponentOptions = {}): unknown {
if (models == undefined) return;
opt.keepIds = [...(opt.keepIds || []), ...getComponentIds(opt.previousModels)];
if (isString(models)) {
models = this.parseString(models, opt)!;
} else if (isArray(models)) {
models = [...models];
models.forEach((item: string, index: number) => {
if (isString(item)) {
const nodes = this.parseString(item, opt);
@ -262,21 +304,19 @@ Component> {
});
}
const isMult = isArray(models);
// @ts-ignore
models = (isMult ? models : [models]).filter(Boolean).map((model: any) => this.processDef(model));
// @ts-ignore
models = isMult ? flatten(models as any, 1) : models[0];
const processedModels = (isArray(models) ? models : [models])
.filter(Boolean)
.map((model: any) => this.processDef(model));
const result = Collection.prototype.add.apply(this, [models as any, opt]);
this.__firstAdd = result;
return result;
models = isArray(models) ? flatten(processedModels as any, 1) : processedModels[0];
return super.add(models as any, opt);
}
/**
* Process component definition.
*/
processDef(mdl: any) {
processDef(mdl: Component | ComponentDefinition | ComponentDefinitionDefined) {
// Avoid processing Models
if (mdl.cid && mdl.ccid) return mdl;
const { em, config = {} } = this;
@ -287,12 +327,14 @@ Component> {
model = { ...model }; // Avoid 'Cannot delete property ...'
const modelPr = processor(model);
if (modelPr) {
//@ts-ignore
each(model, (val, key) => delete model[key]);
extend(model, modelPr);
}
}
// React JSX preset
//@ts-ignore
if (model.$$typeof && typeof model.props == 'object') {
model = { ...model };
model.props = { ...model.props };
@ -301,6 +343,7 @@ Component> {
const { parserHtml } = parser;
each(model, (value, key) => {
//@ts-ignore
if (!includes(['props', 'type'], key)) delete model[key];
});
const { props } = model;
@ -332,8 +375,7 @@ Component> {
const avoidInline = em && em.getConfig().avoidInlineStyle;
domc && domc.Component.ensureInList(model);
// @ts-ignore
if (!isEmpty(style) && !avoidInline && em && em.get && em.getConfig().forceClass && !opts.temporary) {
if (!isEmpty(style) && !avoidInline && em && em.getConfig().forceClass && !opts.temporary) {
const name = model.cid;
em.Css.setClassRule(name, style);
model.setStyle({});
@ -341,34 +383,17 @@ Component> {
}
model.__postAdd({ recursive: true });
// this.__onAddEnd();
}
// __onAddEnd = debounce(function () {
// // TODO to check symbols on load, probably this might be removed as symbols
// // are always recovered from the model
// // const { domc } = this;
// // const allComp = (domc && domc.allById()) || {};
// // const firstAdd = this.__firstAdd;
// // const toCheck = isArray(firstAdd) ? firstAdd : [firstAdd];
// // const silent = { silent: true };
// // const onAll = comps => {
// // comps.forEach(comp => {
// // const symbol = comp.get(keySymbols);
// // const symbolOf = comp.get(keySymbol);
// // if (symbol && isArray(symbol) && isString(symbol[0])) {
// // comp.set(
// // keySymbols,
// // symbol.map(smb => allComp[smb]).filter(i => i),
// // silent
// // );
// // }
// // if (isString(symbolOf)) {
// // comp.set(keySymbol, allComp[symbolOf], silent);
// // }
// // onAll(comp.components());
// // });
// // };
// // onAll(toCheck);
// });
if (em && !opts.temporary) {
const triggerAdd = (model: Component) => {
em.trigger(ComponentsEvents.add, model, opts);
model.components().forEach(comp => triggerAdd(comp));
};
triggerAdd(model);
if (domc && isSymbolInstance(model) && isSymbolRoot(model)) {
domc.symbols.__trgEvent(domc.events.symbolInstanceAdd, { component: model }, true);
}
}
}
}

282
src/dom_components/model/SymbolUtils.ts

@ -0,0 +1,282 @@
import { isArray, isString, keys } from 'underscore';
import Component, { keySymbol, keySymbolOvrd, keySymbols } from './Component';
import { SymbolToUpOptions } from './types';
import { isEmptyObj } from '../../utils/mixins';
import Components from './Components';
export const isSymbolMain = (cmp: Component) => isArray(cmp.get(keySymbols));
export const isSymbolInstance = (cmp: Component) => !!cmp.get(keySymbol);
export const isSymbol = (cmp: Component) => !!(isSymbolMain(cmp) || isSymbolInstance(cmp));
export const isSymbolRoot = (symbol: Component) => {
const parent = symbol.parent();
return isSymbol(symbol) && (!parent || !isSymbol(parent));
};
export const isSymbolNested = (symbol: Component) => {
if (!isSymbol(symbol)) return false;
const symbTopSelf = getSymbolTop(isSymbolMain(symbol) ? symbol : getSymbolMain(symbol)!);
const symbTop = getSymbolTop(symbol);
const symbTopMain = isSymbolMain(symbTop) ? symbTop : getSymbolMain(symbTop);
return symbTopMain !== symbTopSelf;
};
export const initSymbol = (symbol: Component) => {
if (symbol.__symbReady) return;
symbol.on('change', symbol.__upSymbProps);
symbol.__symbReady = true;
};
export const getSymbolMain = (symbol: Component): Component | undefined => {
let result = symbol.get(keySymbol);
if (result && isString(result)) {
const ref = symbol.__getAllById()[result];
if (ref) {
result = ref;
symbol.set(keySymbol, ref);
} else {
result = 0;
}
}
return result || undefined;
};
export const getSymbolInstances = (symbol?: Component): Component[] | undefined => {
let symbs = symbol?.get(keySymbols);
if (symbs && isArray(symbs)) {
symbs.forEach((symb, idx) => {
if (symb && isString(symb)) {
symbs[idx] = symbol!.__getAllById()[symb];
}
});
symbs = symbs.filter(symb => symb && !isString(symb));
}
return symbs || undefined;
};
export const isSymbolOverride = (symbol?: Component, prop = '') => {
const ovrd = symbol?.get(keySymbolOvrd);
const [prp] = prop.split(':');
const props = prop !== prp ? [prop, prp] : [prop];
return ovrd === true || (isArray(ovrd) && props.some(p => ovrd.indexOf(p) >= 0));
};
export const getSymbolsToUpdate = (symb: Component, opts: SymbolToUpOptions = {}) => {
let result: Component[] = [];
const { changed } = opts;
if (
opts.fromInstance ||
opts.noPropagate ||
opts.fromUndo ||
// Avoid updating others if the current component has override
(changed && isSymbolOverride(symb, changed))
) {
return result;
}
const symbols = getSymbolInstances(symb) || [];
const symbol = getSymbolMain(symb);
const all = symbol ? [symbol, ...(getSymbolInstances(symbol) || [])] : symbols;
result = all
.filter(s => s !== symb)
// Avoid updating those with override
.filter(s => !(changed && isSymbolOverride(s, changed)));
return result;
};
export const getSymbolTop = (symbol: Component, opts?: any) => {
let result = symbol;
let parent = symbol.parent(opts);
// while (parent && (isSymbolMain(parent) || getSymbol(parent))) {
while (parent && isSymbol(parent)) {
result = parent;
parent = parent.parent(opts);
}
return result;
};
export const detachSymbolInstance = (symbol: Component, opts: { skipRefs?: boolean } = {}) => {
const symbolMain = getSymbolMain(symbol);
const symbs = symbolMain && getSymbolInstances(symbolMain);
!opts.skipRefs &&
symbs &&
symbolMain.set(
keySymbols,
symbs.filter(s => s !== symbol)
);
symbol.set(keySymbol, 0);
symbol.components().forEach(s => detachSymbolInstance(s, opts));
};
export const logSymbol = (symb: Component, type: string, toUp: Component[], opts: any = {}) => {
const symbol = getSymbolMain(symb);
const symbols = getSymbolInstances(symb);
if (!symbol && !symbols) {
return;
}
symb.em.log(type, { model: symb, toUp, context: 'symbols', opts });
};
export const updateSymbolProps = (symbol: Component, opts: SymbolToUpOptions = {}) => {
const changed = symbol.changedAttributes() || {};
const attrs = changed.attributes || {};
delete changed.status;
delete changed.open;
delete changed[keySymbols];
delete changed[keySymbol];
delete changed[keySymbolOvrd];
delete changed.attributes;
delete attrs.id;
if (!isEmptyObj(attrs)) {
changed.attributes = attrs;
}
if (!isEmptyObj(changed)) {
const toUp = getSymbolsToUpdate(symbol, opts);
// Avoid propagating overrides to other symbols
keys(changed).map(prop => {
if (isSymbolOverride(symbol, prop)) delete changed[prop];
});
logSymbol(symbol, 'props', toUp, { opts, changed });
toUp.forEach(child => {
const propsChanged = { ...changed };
// Avoid updating those with override
keys(propsChanged).map(prop => {
if (isSymbolOverride(child, prop)) delete propsChanged[prop];
});
child.set(propsChanged, { fromInstance: symbol, ...opts });
});
}
};
export const updateSymbolCls = (symbol: Component, opts: any = {}) => {
const toUp = getSymbolsToUpdate(symbol, opts);
logSymbol(symbol, 'classes', toUp, { opts });
toUp.forEach(child => {
// @ts-ignore This will propagate the change up to __upSymbProps
child.set('classes', symbol.get('classes'), { fromInstance: symbol });
});
symbol.__changesUp(opts);
};
export const updateSymbolComps = (symbol: Component, m: Component, c: Components, o: any) => {
const optUp = o || c || {};
const { fromInstance, fromUndo } = optUp;
const toUpOpts = { fromInstance, fromUndo };
const isTemp = m.opt.temporary;
// Reset
if (!o) {
const coll = m as unknown as Components;
const toUp = getSymbolsToUpdate(symbol, {
...toUpOpts,
changed: 'components:reset',
});
const cmps = coll.models;
const newSymbols = new Set<Component>();
logSymbol(symbol, 'reset', toUp, { components: cmps });
toUp.forEach(rel => {
const relCmps = rel.components();
const toReset = cmps.map((cmp, i) => {
// This particular case here is to handle reset from `resetFromString`
// where we can receive an array of regulat components or already
// existing symbols (updated already before reset)
if (!isSymbol(cmp) || newSymbols.has(cmp)) {
newSymbols.add(cmp);
return cmp.clone({ symbol: true });
}
return relCmps.at(i);
});
relCmps.reset(toReset, { fromInstance: symbol, ...c } as any);
});
// Add
} else if (o.add) {
let addedInstances: Component[] = [];
const isMainSymb = !!getSymbolInstances(symbol);
const toUp = getSymbolsToUpdate(symbol, {
...toUpOpts,
changed: 'components:add',
});
if (toUp.length) {
const addSymb = getSymbolMain(m);
addedInstances = (addSymb ? getSymbolInstances(addSymb) : getSymbolInstances(m)) || [];
addedInstances = [...addedInstances];
addedInstances.push(addSymb ? addSymb : m);
}
!isTemp &&
logSymbol(symbol, 'add', toUp, {
opts: o,
addedInstances: addedInstances.map(c => c.cid),
added: m.cid,
});
// Here, before appending a new symbol, I have to ensure there are no previously
// created symbols (eg. used mainly when drag components around)
toUp.forEach(symb => {
const symbTop = getSymbolTop(symb);
const symbPrev = addedInstances.filter(addedInst => {
const addedTop = getSymbolTop(addedInst, { prev: 1 });
return symbTop && addedTop && addedTop === symbTop;
})[0];
const toAppend = symbPrev || m.clone({ symbol: true, symbolInv: isMainSymb });
symb.append(toAppend, { fromInstance: symbol, ...o });
});
// Remove
} else {
// Remove instance reference from the symbol
const symb = getSymbolMain(m);
symb &&
!o.temporary &&
symb.set(
keySymbols,
getSymbolInstances(symb)!.filter(i => i !== m)
);
// Propagate remove only if the component is an inner symbol
if (!isSymbolRoot(m) && !o.skipRefsUp) {
const changed = 'components:remove';
const { index } = o;
const parent = m.parent();
const opts = { fromInstance: m, ...o };
const isSymbNested = isSymbolRoot(m);
let toUpFn = (symb: Component) => {
const symbPrnt = symb.parent();
symbPrnt && !isSymbolOverride(symbPrnt, changed) && symb.remove(opts);
};
// Check if the parent allows the removing
let toUp = !isSymbolOverride(parent, changed) ? getSymbolsToUpdate(m, toUpOpts) : [];
if (isSymbNested) {
toUp = parent! && getSymbolsToUpdate(parent, { ...toUpOpts, changed })!;
toUpFn = symb => {
const toRemove = symb.components().at(index);
toRemove && toRemove.remove({ fromInstance: parent, ...opts });
};
}
!isTemp &&
logSymbol(symbol, 'remove', toUp, {
opts: o,
removed: m.cid,
isSymbNested,
});
toUp.forEach(toUpFn);
}
}
symbol.__changesUp(optUp);
};

56
src/dom_components/model/Symbols.ts

@ -0,0 +1,56 @@
import { debounce } from 'underscore';
import { Debounced, ObjectAny } from '../../common';
import Component from './Component';
import Components from './Components';
import { detachSymbolInstance, getSymbolInstances } from './SymbolUtils';
interface PropsComponentUpdate {
component: Component;
changed: ObjectAny;
options: ObjectAny;
}
export default class Symbols extends Components {
refreshDbn: Debounced;
constructor(...args: ConstructorParameters<typeof Components>) {
super(...args);
this.refreshDbn = debounce(() => this.refresh(), 0);
const { events } = this;
this.on(events.update, this.onUpdate);
this.on(events.updateInside, this.onUpdateDeep);
}
removeChildren(component: Component, coll?: Components, opts: any = {}) {
super.removeChildren(component, coll, opts);
getSymbolInstances(component)?.forEach(i => detachSymbolInstance(i, { skipRefs: true }));
this.__trgEvent(this.events.symbolMainRemove, { component });
}
onAdd(...args: Parameters<Components['onAdd']>) {
super.onAdd(...args);
const [component] = args;
this.__trgEvent(this.events.symbolMainAdd, { component });
}
onUpdate(props: PropsComponentUpdate) {
this.__trgEvent(this.events.symbolMainUpdate, props);
}
onUpdateDeep(props: PropsComponentUpdate) {
this.__trgEvent(this.events.symbolMainUpdateDeep, props);
}
refresh() {
const { em, events } = this;
em.trigger(events.symbol);
}
__trgEvent(event: string, props: ObjectAny, isInstance = false) {
const { em, events } = this;
const eventType = isInstance ? events.symbolInstance : events.symbolMain;
em.trigger(event, props);
em.trigger(eventType, { ...props, event });
this.refreshDbn();
}
}

33
src/dom_components/model/types.ts

@ -1,5 +1,5 @@
import Frame from '../../canvas/model/Frame';
import { Nullable } from '../../common';
import { AddOptions, Nullable, OptionAsDocument } from '../../common';
import EditorModel from '../../editor/model/Editor';
import Selectors from '../../selector_manager/model/Selectors';
import { TraitProperties } from '../../trait_manager/types';
@ -10,15 +10,23 @@ import ComponentView from '../view/ComponentView';
import Component from './Component';
import Components from './Components';
import { ToolbarButtonProps } from './ToolbarButton';
import { ParseNodeOptions } from '../../parser/config/config';
export type DragMode = 'translate' | 'absolute' | '';
export type DraggableDroppableFn = (source: Component, target: Component, index?: number) => boolean | void;
export interface ComponentStackItem {
export interface AddComponentsOption extends AddOptions, OptionAsDocument {}
interface ComponentWithCheck<C extends Component> {
new (props: any, opt: ComponentOptions): C;
isComponent(node: HTMLElement, opts?: ParseNodeOptions): ComponentDefinitionDefined | undefined | boolean;
}
export interface ComponentStackItem<C extends Component = Component, CV extends ComponentView<C> = ComponentView<C>> {
id: string;
model: typeof Component;
view: typeof ComponentView<any>;
model: ComponentWithCheck<C>;
view: new (opt: any) => CV;
}
/**
@ -132,7 +140,7 @@ export interface ComponentProperties {
*/
copyable?: boolean;
/**
* Indicates if it's possible to resize the component. It's also possible to pass an object as [options for the Resizer](https://github.com/GrapesJS/grapesjs/blob/master/src/utils/Resizer.js). Default: `false`
* Indicates if it's possible to resize the component. It's also possible to pass an object as [options for the Resizer](https://github.com/GrapesJS/grapesjs/blob/master/src/utils/Resizer.ts). Default: `false`
*/
resizable?: boolean | ResizerOptions;
/**
@ -260,11 +268,11 @@ export interface ComponentModelProperties extends ComponentProperties {
[key: string]: any;
}
type ComponentAddType = Component | ComponentDefinition | ComponentDefinitionDefined | string;
export type ComponentAddType = Component | ComponentDefinition | ComponentDefinitionDefined | string;
export type ComponentAdd = ComponentAddType | ComponentAddType[];
export type ToHTMLOptions = {
export interface ToHTMLOptions extends OptionAsDocument {
/**
* Custom tagName.
*/
@ -280,16 +288,21 @@ export type ToHTMLOptions = {
*/
altQuoteAttr?: boolean;
/**
* Keep inline style set intentionally by users with `setStyle({}, { inline: true })`
*/
keepInlineStyle?: boolean;
/**
* You can pass an object of custom attributes to replace with the current ones
* or you can even pass a function to generate attributes dynamically.
*/
attributes?: Record<string, any> | ((component: Component, attr: Record<string, any>) => Record<string, any>);
};
}
export interface ComponentOptions {
em?: EditorModel;
config?: DomComponentsConfig;
em: EditorModel;
config: DomComponentsConfig;
frame?: Frame;
temporary?: boolean;
avoidChildren?: boolean;

106
src/dom_components/types.ts

@ -0,0 +1,106 @@
import Component from './model/Component';
export enum ActionLabelComponents {
remove = 'component:remove',
add = 'component:add',
move = 'component:move',
}
export interface SymbolInfo {
isSymbol: boolean;
isMain: boolean;
isInstance: boolean;
isRoot: boolean;
main?: Component;
instances: Component[];
relatives: Component[];
}
export enum ComponentsEvents {
/**
* @event `component:add` New component added.
* @example
* editor.on('component:add', (component) => { ... });
*/
add = 'component:add',
/**
* @event `component:remove` Component removed.
* @example
* editor.on('component:remove', (component) => { ... });
*/
remove = 'component:remove',
removeBefore = 'component:remove:before',
/**
* @event `component:create` Component created.
* @example
* editor.on('component:create', (component) => { ... });
*/
create = 'component:create',
/**
* @event `component:update` Component is updated, the component is passed as an argument to the callback.
* @example
* editor.on('component:update', (component) => { ... });
*/
update = 'component:update',
updateInside = 'component:update-inside',
/**
* @event `symbol:main:add` Added new main symbol.
* @example
* editor.on('symbol:main:add', ({ component }) => { ... });
*/
symbolMainAdd = 'symbol:main:add',
/**
* @event `symbol:main:update` Main symbol updated.
* @example
* editor.on('symbol:main:update', ({ component }) => { ... });
*/
symbolMainUpdate = 'symbol:main:update',
symbolMainUpdateDeep = 'symbol:main:update-deep',
/**
* @event `symbol:main:remove` Main symbol removed.
* @example
* editor.on('symbol:main:remove', ({ component }) => { ... });
*/
symbolMainRemove = 'symbol:main:remove',
/**
* @event `symbol:main` Catch-all event related to main symbol updates.
* @example
* editor.on('symbol:main', ({ event, component }) => { ... });
*/
symbolMain = 'symbol:main',
/**
* @event `symbol:instance:add` Added new root instance symbol.
* @example
* editor.on('symbol:instance:add', ({ component }) => { ... });
*/
symbolInstanceAdd = 'symbol:instance:add',
/**
* @event `symbol:instance:remove` Root instance symbol removed.
* @example
* editor.on('symbol:instance:remove', ({ component }) => { ... });
*/
symbolInstanceRemove = 'symbol:instance:remove',
/**
* @event `symbol:instance` Catch-all event related to instance symbol updates.
* @example
* editor.on('symbol:instance', ({ event, component }) => { ... });
*/
symbolInstance = 'symbol:instance',
/**
* @event `symbol` Catch-all event for any symbol update (main or instance).
* @example
* editor.on('symbol', () => { ... });
*/
symbol = 'symbol',
}

2
src/dom_components/view/ComponentImageView.ts

@ -103,7 +103,7 @@ export default class ComponentImageView<TComp extends ComponentImage = Component
onLoad() {
// Used to update component tools box (eg. toolbar, resizer) once the image is loaded
this.em.trigger('change:canvasOffset');
this.em.Canvas.refresh({ all: true });
}
noDrag(ev: Event) {

4
src/dom_components/view/ComponentLabelView.ts

@ -1,4 +1,4 @@
import ComponentLabel from '../model/ComponentLabel';
import ComponentView from './ComponentView';
import ComponentLinkView from './ComponentLinkView';
export default class ComponentLabelView extends ComponentView<ComponentLabel> {}
export default class ComponentLabelView extends ComponentLinkView<ComponentLabel> {}

4
src/dom_components/view/ComponentLinkView.ts

@ -1,7 +1,7 @@
import ComponentLink from '../model/ComponentLink';
import ComponentView from './ComponentView';
import ComponentTextView from './ComponentTextView';
export default class ComponentLinkView extends ComponentView<ComponentLink> {
export default class ComponentLinkView<TComp extends ComponentLink = ComponentLink> extends ComponentTextView<TComp> {
render() {
super.render();
// I need capturing instead of bubbling as bubbled clicks from other

2
src/dom_components/view/ComponentTextView.ts

@ -10,7 +10,7 @@ import ComponentText from '../model/ComponentText';
import { ComponentDefinition } from '../model/types';
import ComponentView from './ComponentView';
export default class ComponentTextView extends ComponentView<ComponentText> {
export default class ComponentTextView<TComp extends ComponentText = ComponentText> extends ComponentView<TComp> {
rte?: RichTextEditorModule;
rteEnabled?: boolean;
activeRte?: RichTextEditor;

3
src/dom_components/view/ComponentView.ts

@ -293,8 +293,9 @@ TComp> {
const { model, em } = this;
if (avoidInline(em) && !opts.inline) {
// Move inline styles to CSSRule
const styleOpts = this.__cmpStyleOpts;
const style = model.getStyle(styleOpts);
const style = model.getStyle({ inline: true, ...styleOpts });
!isEmpty(style) && model.setStyle(style, styleOpts);
} else {
this.setAttribute('style', model.styleToString(opts));

18
src/dom_components/view/ComponentsView.ts

@ -46,18 +46,8 @@ export default class ComponentsView extends View {
* @param {Object} opts
* @private
* */
addTo(model: Component, coll: any = {}, opts: { temporary?: boolean } = {}) {
const { em } = this;
const i = this.collection.indexOf(model);
this.addToCollection(model, null, i);
if (em && !opts.temporary) {
const triggerAdd = (model: Component) => {
em.trigger('component:add', model);
model.components().forEach(comp => triggerAdd(comp));
};
triggerAdd(model);
}
addTo(model: Component) {
this.addToCollection(model, null, this.collection.indexOf(model));
}
/**
@ -69,10 +59,8 @@ export default class ComponentsView extends View {
* @return {Object} Object rendered
* @private
* */
addToCollection(model: Component, fragmentEl?: DocumentFragment | null, index?: number) {
// if (!this.compView) this.compView = require('./ComponentView').default;
addToCollection(model: Component, fragment?: DocumentFragment | null, index?: number) {
const { config, opts, em } = this;
const fragment = fragmentEl || null;
const { frameView } = config;
const sameFrameView = frameView?.model && model.getView(frameView.model);
const dt = opts.componentTypes || em?.Components.getTypes();

6
src/domain_abstract/model/StyleableModel.ts

@ -1,14 +1,16 @@
import { isArray, isString, keys } from 'underscore';
import { Model, ObjectAny, ObjectHash } from '../../common';
import { Model, ObjectAny, ObjectHash, SetOptions } from '../../common';
import ParserHtml from '../../parser/model/ParserHtml';
import Selectors from '../../selector_manager/model/Selectors';
import { shallowDiff } from '../../utils/mixins';
export type StyleProps = Record<string, string | string[]>;
export type UpdateStyleOptions = ObjectAny & {
export type UpdateStyleOptions = SetOptions & {
partial?: boolean;
addStyle?: StyleProps;
inline?: boolean;
noEvent?: boolean;
};
const parserHtml = ParserHtml();

10
src/domain_abstract/ui/InputColor.ts

@ -101,10 +101,9 @@ export default class InputColor extends Input {
const ppfx = this.ppfx;
const { onChange } = opts;
var colorEl = $(`<div class="${this.ppfx}field-color-picker"></div>`);
var cpStyle = colorEl.get(0)!.style;
var elToAppend = em && em.config ? em.config.el : '';
var colorPickerConfig = (em && em.getConfig && em.getConfig().colorPicker) || {};
const colorEl = $(`<div class="${this.ppfx}field-color-picker"></div>`);
const cpStyle = colorEl.get(0)!.style;
const colorPickerConfig = (em && em.getConfig && em.getConfig().colorPicker) || {};
this.movedColor = '';
let changed = false;
@ -123,8 +122,7 @@ export default class InputColor extends Input {
// @ts-ignore
colorEl.spectrum({
color: model.getValue() || false,
containerClassName: `${ppfx}one-bg ${ppfx}two-color`,
appendTo: elToAppend || 'body',
containerClassName: `${ppfx}one-bg ${ppfx}two-color ${ppfx}editor-sp`,
maxSelectionSize: 8,
showPalette: true,
showAlpha: true,

4
src/domain_abstract/ui/InputNumber.ts

@ -140,7 +140,7 @@ export default class InputNumber extends Input {
upArrowClick() {
const { model } = this;
const step = model.get('step');
let value = parseFloat(model.get('value'));
let value = parseFloat(this.getInputEl().value);
this.setValue(this.normalizeValue(value + step));
this.elementUpdated();
}
@ -151,7 +151,7 @@ export default class InputNumber extends Input {
downArrowClick() {
const { model } = this;
const step = model.get('step');
const value = parseFloat(model.get('value'));
const value = parseFloat(this.getInputEl().value);
this.setValue(this.normalizeValue(value - step));
this.elementUpdated();
}

10
src/editor/config/config.ts

@ -7,6 +7,7 @@ import { DeviceManagerConfig } from '../../device_manager/config/config';
import { I18nConfig } from '../../i18n/config';
import { ModalConfig } from '../../modal_dialog/config/config';
import { LayerManagerConfig } from '../../navigator/config/config';
import { KeymapsConfig } from '../../keymaps/config';
import { PageManagerConfig } from '../../pages/types';
import { PanelsConfig } from '../../panels/config/config';
import { ParserConfig } from '../../parser/config/config';
@ -22,6 +23,7 @@ import { DomComponentsConfig } from '../../dom_components/config/config';
import { HTMLGeneratorBuildOptions } from '../../code_manager/model/HtmlGenerator';
import { CssGeneratorBuildOptions } from '../../code_manager/model/CssGenerator';
import { ObjectAny } from '../../common';
import { ColorPickerOptions } from '../../utils/ColorPicker';
export interface EditorConfig {
/**
@ -355,6 +357,11 @@ export interface EditorConfig {
*/
commands?: CommandsConfig;
/**
* Configurations for keymaps
*/
keymaps?: KeymapsConfig;
/**
* Configurations for Css Composer.
*/
@ -415,10 +422,11 @@ export interface EditorConfig {
*/
customUI?: boolean;
el?: HTMLElement;
/**
* Color picker options.
*/
colorPicker?: ObjectAny;
colorPicker?: ColorPickerOptions;
pStylePrefix?: string;
}

3
src/editor/index.ts

@ -55,7 +55,8 @@
* @module docsjs.Editor
*/
import { IBaseModule } from '../abstract/Module';
import AssetManager, { AssetEvent } from '../asset_manager';
import AssetManager from '../asset_manager';
import { AssetEvent } from '../asset_manager/types';
import BlockManager, { BlockEvent } from '../block_manager';
import CanvasModule, { CanvasEvent } from '../canvas';
import CodeManagerModule from '../code_manager';

4
src/editor/model/Editor.ts

@ -913,9 +913,7 @@ export default class EditorModel extends Model {
* @public
*/
refreshCanvas(opts: any = {}) {
this.set('canvasOffset', null);
this.set('canvasOffset', this.Canvas.getOffset());
opts.tools && this.trigger('canvas:updateTools');
this.Canvas.refresh({ spots: opts.tools });
}
/**

22
src/editor/view/EditorView.ts

@ -5,15 +5,14 @@ import EditorModel from '../model/Editor';
export default class EditorView extends View<EditorModel> {
constructor(model: EditorModel) {
super({ model });
//const { model } = this;
const { Panels, UndoManager } = model.attributes;
const { Panels, UndoManager } = model;
model.view = this;
model.once('change:ready', () => {
Panels.active();
Panels.disableButtons();
UndoManager.clear();
setTimeout(() => {
model.trigger('load', model.get('Editor'));
model.trigger('load', model.Editor);
model.clearDirtyCount();
});
});
@ -21,35 +20,30 @@ export default class EditorView extends View<EditorModel> {
render() {
const { $el, model } = this;
const { Panels, Canvas } = model.attributes;
const { config, modules } = model;
const { Panels, Canvas, config, modules } = model;
const pfx = config.stylePrefix;
const classNames = [`${pfx}editor`];
!config.customUI && classNames.push(`${pfx}one-bg ${pfx}two-color`);
// @ts-ignore
const contEl = $(config.el || `body ${config.container}`);
config.cssIcons && appendStyles(config.cssIcons, { unique: true, prepand: true });
$el.empty();
// @ts-ignore
if (config.width) contEl.css('width', config.width);
// @ts-ignore
if (config.height) contEl.css('height', config.height);
config.width && contEl.css('width', config.width);
config.height && contEl.css('height', config.height);
$el.append(Canvas.render());
$el.append(Panels.render());
// Load shallow editor
const shallow = model.get('shallow');
const shallowCanvasEl = shallow.get('Canvas').render();
const { shallow } = model;
const shallowCanvasEl = shallow.Canvas.render();
shallowCanvasEl.style.display = 'none';
$el.append(shallowCanvasEl);
$el.attr('class', classNames.join(' '));
// @ts-ignore
contEl.addClass(`${pfx}editor-cont`).empty().append($el);
modules.forEach(md => md.postRender && md.postRender(this));
modules.forEach(md => md.postRender?.(this));
return this;
}

29
src/i18n/index.ts

@ -19,10 +19,7 @@
* const i18n = editor.I18n;
* ```
*
* ### Events
* * `i18n:add` - New set of messages is added
* * `i18n:update` - The set of messages is updated
* * `i18n:locale` - Locale changed
* {REPLACE_EVENTS}
*
* @module I18n
*/
@ -31,10 +28,11 @@ import { Module } from '../abstract';
import EditorModel from '../editor/model/Editor';
import { hasWin, deepMerge } from '../utils/mixins';
import defaults, { I18nConfig } from './config';
type Messages = Required<I18nConfig>['messages'];
import I18nEvents, { Messages } from './types';
export default class I18nModule extends Module<I18nConfig & { stylePrefix?: string }> {
events = I18nEvents;
/**
* Initialize module
* @param {Object} config Configurations
@ -65,9 +63,8 @@ export default class I18nModule extends Module<I18nConfig & { stylePrefix?: stri
* i18n.setLocale('it');
*/
setLocale(locale: string) {
const { em, config } = this;
const evObj = { value: locale, valuePrev: config.locale };
em && em.trigger('i18n:locale', evObj);
const { em, config, events } = this;
em.trigger(events.locale, { value: locale, valuePrev: config.locale });
config.locale = locale;
return this;
}
@ -77,7 +74,7 @@ export default class I18nModule extends Module<I18nConfig & { stylePrefix?: stri
* @returns {String} Current locale value
*/
getLocale() {
return this.config.locale;
return this.config.locale!;
}
/**
@ -92,7 +89,7 @@ export default class I18nModule extends Module<I18nConfig & { stylePrefix?: stri
* i18n.getMessages('en');
* // -> { hello: '...' }
*/
getMessages(lang: string, opts = {}) {
getMessages(lang?: string, opts = {}) {
const messages = this.config.messages!;
lang && !messages[lang] && this._debug(`'${lang}' i18n lang not found`, opts);
return lang ? messages[lang] : messages;
@ -111,9 +108,9 @@ export default class I18nModule extends Module<I18nConfig & { stylePrefix?: stri
* // -> { en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } }
*/
setMessages(msg: Messages) {
const { em, config } = this;
const { em, config, events } = this;
config.messages = msg;
em && em.trigger('i18n:update', msg);
em.trigger(events.update, msg);
return this;
}
@ -130,9 +127,9 @@ export default class I18nModule extends Module<I18nConfig & { stylePrefix?: stri
* // -> { en: { msg1: 'Msg 1', msg2: 'Msg 2 up', msg3: 'Msg 3', } }
*/
addMessages(msg: Messages) {
const { em } = this;
const { messages } = this.config;
em && em.trigger('i18n:add', msg);
const { em, events, config } = this;
const { messages } = config;
em.trigger(events.add, msg);
this.setMessages(deepMerge(messages!, msg));
return this;

169
src/i18n/locale/id.js

@ -0,0 +1,169 @@
const traitInputAttr = { placeholder: 'cth. Ketik disini' };
export default {
assetManager: {
addButton: 'Tambah gambar',
inputPlh: 'http://path/menuju/gambar.jpg',
modalTitle: 'Pilih Gambar',
uploadTitle: 'Tarik gambar kesini atau klik upload',
},
// Here just as a reference, GrapesJS core doesn't contain any block,
// so this should be omitted from other local files
blockManager: {
labels: {
// 'block-id': 'Block Label',
},
categories: {
// 'category-id': 'Category Label',
},
},
domComponents: {
names: {
'': 'Kotak',
wrapper: 'Badan',
text: 'Teks',
comment: 'Komentar',
image: 'Gambar',
video: 'Video',
label: 'Label',
link: 'Link',
map: 'Peta',
tfoot: 'Kaki tabel',
tbody: 'Badan tabel',
thead: 'Kepala tabel',
table: 'Tabel',
row: 'Baris tabel',
cell: 'Cell tabel',
},
},
deviceManager: {
device: 'Perangkat',
devices: {
desktop: 'Desktop',
tablet: 'Tablet',
mobileLandscape: 'Mobile Landscape',
mobilePortrait: 'Mobile Portrait',
},
},
panels: {
buttons: {
titles: {
preview: 'Pra-tayang',
fullscreen: 'Tampilan penuh',
'sw-visibility': 'Lihat komponen',
'export-template': 'Lihat kode',
'open-sm': 'Buka Manajemen Style',
'open-tm': 'Pengaturan',
'open-layers': 'Buka Layer Manager',
'open-blocks': 'Buka Blocks',
},
},
},
selectorManager: {
label: 'Class',
selected: 'Terpilih',
emptyState: '- State -',
states: {
hover: 'Hover',
active: 'Klik',
'nth-of-type(2n)': 'Rata/Ganjil',
},
},
styleManager: {
empty: 'Pilih elemen sebelum menggunakan Manajemen Style',
layer: 'Layer',
fileButton: 'Gambar',
sectors: {
general: 'Umum',
layout: 'Pemetaan',
typography: 'Tipografi',
decorations: 'Dekorasi',
extra: 'Ekstra',
flex: 'Flex',
dimension: 'Dimensi',
},
// Default names for sub properties in Composite and Stack types.
// Other labels are generated directly from their property names (eg. 'font-size' will be 'Font size').
properties: {
'text-shadow-h': 'X',
'text-shadow-v': 'Y',
'text-shadow-blur': 'Blur',
'text-shadow-color': 'Warna',
'box-shadow-h': 'X',
'box-shadow-v': 'Y',
'box-shadow-blur': 'Blur',
'box-shadow-spread': 'Spread',
'box-shadow-color': 'Warna',
'box-shadow-type': 'Tipe',
'margin-top-sub': 'Atas',
'margin-right-sub': 'Kanan',
'margin-bottom-sub': 'Bawah',
'margin-left-sub': 'Kiri',
'padding-top-sub': 'Atas',
'padding-right-sub': 'Kanan',
'padding-bottom-sub': 'Bawah',
'padding-left-sub': 'Kiri',
'border-width-sub': 'Panjang',
'border-style-sub': 'Gaya',
'border-color-sub': 'Warna',
'border-top-left-radius-sub': 'Atas Kiri',
'border-top-right-radius-sub': 'Atas Kanan',
'border-bottom-right-radius-sub': 'Bawah Kanan',
'border-bottom-left-radius-sub': 'Bawah Kiri',
'transform-rotate-x': 'Putar X',
'transform-rotate-y': 'Putar Y',
'transform-rotate-z': 'Putar Z',
'transform-scale-x': 'Scale X',
'transform-scale-y': 'Scale Y',
'transform-scale-z': 'Scale Z',
'transition-property-sub': 'Properti',
'transition-duration-sub': 'Durasi',
'transition-timing-function-sub': 'Timing',
'background-image-sub': 'Gambar',
'background-repeat-sub': 'Berulang',
'background-position-sub': 'Posisi',
'background-attachment-sub': 'Lampiran',
'background-size-sub': 'Ukuran',
},
// Translate options in style properties
// options: {
// float: { // Id of the property
// ...
// left: 'Left', // {option id}: {Option label}
// }
// }
},
traitManager: {
empty: 'Pilih elemen terlebih dulu sebelum menggunakan Manajemen Trait',
label: 'Pengaturan komponen',
categories: {
categoryId: 'Label kategori',
},
traits: {
// The core library generates the name by their `name` property
labels: {
title: 'Judul',
href: 'Url (href)',
// id: 'Id',
// alt: 'Alt',
},
// In a simple trait, like text input, these are used on input attributes
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'cth. https://google.com' },
},
// In a trait like select, these are used to translate option names
options: {
target: {
false: 'Jendela ini',
_blank: 'Jendela baru',
},
},
},
},
storageManager: {
recover: 'Apakah kamu ingin mengembalikan draft yang belum tersimpan?',
},
};

31
src/i18n/types.ts

@ -0,0 +1,31 @@
import { I18nConfig } from './config';
export type Messages = Required<I18nConfig>['messages'];
/**{START_EVENTS}*/
export enum I18nEvents {
/**
* @event `i18n:add` New set of messages is added.
* @example
* editor.on('i18n:add', (messages) => { ... });
*/
add = 'i18n:add',
/**
* @event `i18n:update` The set of messages is updated.
* @example
* editor.on('i18n:update', (messages) => { ... });
*/
update = 'i18n:update',
/**
* @event `i18n:locale` Locale changed.
* @example
* editor.on('i18n:locale', ({ value, valuePrev }) => { ... });
*/
locale = 'i18n:locale',
}
/**{END_EVENTS}*/
// need this to avoid the TS documentation generator to break
export default I18nEvents;

8
src/navigator/index.ts

@ -47,6 +47,7 @@ import EditorModel from '../editor/model/Editor';
import { hasWin, isComponent, isDef } from '../utils/mixins';
import defaults, { LayerManagerConfig } from './config/config';
import View from './view/ItemView';
import { ComponentsEvents } from '../dom_components/types';
interface LayerData {
name: string;
@ -74,7 +75,7 @@ const events = {
const styleOpts = { mediaText: '' };
const propsToListen = ['open', 'status', 'locked', 'custom-name', 'components', 'classes']
.map(p => `component:update:${p}`)
.map(p => `${ComponentsEvents.update}:${p}`)
.join(' ');
const isStyleHidden = (style: any = {}) => {
@ -195,7 +196,7 @@ export default class LayerManager extends Module<LayerManagerConfig> {
style.display = 'none';
}
component.setStyle(style, styleOpts);
component.setStyle(style, styleOpts as any);
this.updateLayer(component);
this.em.trigger('component:toggled'); // Updates Style Manager #2938
}
@ -324,7 +325,6 @@ export default class LayerManager extends Module<LayerManagerConfig> {
}
if (selected && scrollLayers) {
// @ts-ignore
const el = selected.viewLayer?.el;
el?.scrollIntoView(scrollLayers);
}
@ -367,7 +367,7 @@ export default class LayerManager extends Module<LayerManagerConfig> {
__isLayerable(cmp: Component): boolean {
const tag = cmp.get('tagName');
const hideText = this.config.hideTextnode;
const isValid = !hideText || (!cmp.is('textnode') && tag !== 'br');
const isValid = !hideText || (!cmp.isInstanceOf('textnode') && tag !== 'br');
return isValid && cmp.get('layerable')!;
}

39
src/navigator/view/ItemView.ts

@ -4,7 +4,6 @@ import Component from '../../dom_components/model/Component';
import ComponentView from '../../dom_components/view/ComponentView';
import EditorModel from '../../editor/model/Editor';
import { isEnterKey, isEscKey } from '../../utils/dom';
import { getModel } from '../../utils/mixins';
import LayerManager from '../index';
import ItemsView from './ItemsView';
import { getOnComponentDrag, getOnComponentDragEnd, getOnComponentDragStart } from '../../commands';
@ -21,12 +20,13 @@ export type ItemViewProps = ViewOptions & {
};
const inputProp = 'contentEditable';
const dataToggleMove = 'data-toggle-move';
export default class ItemView extends View {
events() {
return {
'mousedown [data-toggle-move]': 'startSort',
'touchstart [data-toggle-move]': 'startSort',
[`mousedown [${dataToggleMove}]`]: 'startSort',
[`touchstart [${dataToggleMove}]`]: 'startSort',
'click [data-toggle-visible]': 'toggleVisibility',
'click [data-toggle-open]': 'toggleOpening',
'click [data-toggle-select]': 'handleSelect',
@ -77,7 +77,7 @@ export default class ItemView extends View {
</div>
<div class="${pfx}layer-item-right">
<div class="${this.clsCount}" data-count>${count || ''}</div>
<div class="${this.clsMove}" data-toggle-move>${move || ''}</div>
<div class="${this.clsMove}" ${dataToggleMove}>${move || ''}</div>
</div>
</div>
<div class="${this.clsChildren}"></div>
@ -96,8 +96,8 @@ export default class ItemView extends View {
return this.config.stylePrefix;
}
opt: any;
module: any;
opt: ItemViewProps;
module: LayerManager;
config: any;
sorter: any;
/** @ts-ignore */
@ -152,13 +152,13 @@ export default class ItemView extends View {
['change:open', this.updateOpening],
['change:layerable', this.updateLayerable],
['change:style:display', this.updateVisibility],
['change:draggable', this.updateMove],
['rerender:layer', this.render],
['change:name change:custom-name', this.updateName],
// @ts-ignore
].forEach(item => this.listenTo(model, item[0], item[1]));
this.$el.data('model', model);
this.$el.data('collection', components);
// @ts-ignore
model.viewLayer = this;
onInit.bind(this)({
component: model,
@ -184,6 +184,15 @@ export default class ItemView extends View {
this.getVisibilityEl()[method](`${pfx}layer-off`);
}
updateMove() {
const { model, config } = this;
const el = this.getItemContainer().find(`[${dataToggleMove}]`)[0];
if (el) {
const isDraggable = model.get('draggable') && config.sortable;
el.style.display = isDraggable ? '' : 'none';
}
}
/**
* Toggle visibility
* @param Event
@ -220,22 +229,18 @@ export default class ItemView extends View {
*/
handleEditEnd(ev?: KeyboardEvent) {
ev?.stopPropagation();
const { em, $el, clsNoEdit, clsEdit } = this;
const { em, $el, clsNoEdit, clsEdit, model } = this;
const inputEl = this.getInputName();
const name = inputEl.textContent!;
inputEl.scrollLeft = 0;
inputEl[inputProp] = 'false';
this.setName(name, { component: this.model, propName: 'custom-name' });
model.setName(name);
em.setEditing(false);
$el.find(`.${this.inputNameCls}`).addClass(clsNoEdit).removeClass(clsEdit);
// Ensure to always update the layer name #4544
this.updateName();
}
setName(name: string, { propName }: { propName: string; component?: Component }) {
this.model.set(propName, name);
}
/**
* Get the input containing the name of the component
* @return {HTMLElement}
@ -357,7 +362,7 @@ export default class ItemView extends View {
const countEl = itemEl.find('[data-count]');
title[count ? 'removeClass' : 'addClass'](clsNoChild);
countEl.html(count || '');
countEl.html(`${count || ''}`);
!count && module.setOpen(model, false);
}
@ -390,6 +395,7 @@ export default class ItemView extends View {
remove(...args: []) {
View.prototype.remove.apply(this, args);
delete this.model.viewLayer;
this.__clearItems();
return this;
}
@ -422,16 +428,13 @@ export default class ItemView extends View {
el.find(`.${this.clsChildren}`).append(children);
}
if (!model.get('draggable') || !config.sortable) {
el.children(`.${this.clsMove}`).remove();
}
!module.isVisible(model) && (this.className += ` ${pfx}hide`);
hidden && (this.className += ` ${ppfx}hidden`);
el.attr('class', this.className!);
this.updateStatus();
this.updateOpening();
this.updateVisibility();
this.updateMove();
this.__render();
this._rendered = true;
return this;

59
src/navigator/view/ItemsView.ts

@ -1,13 +1,17 @@
import { isUndefined } from 'underscore';
import { View } from '../../common';
import Component from '../../dom_components/model/Component';
import EditorModel from '../../editor/model/Editor';
import ItemView from './ItemView';
import Components from '../../dom_components/model/Components';
export default class ItemsView extends View {
items: ItemView[];
opt: any;
config: any;
parentView: ItemView;
/** @ts-ignore */
collection!: Components;
constructor(opt: any = {}) {
super(opt);
@ -47,11 +51,9 @@ export default class ItemsView extends View {
}
removeChildren(removed: Component) {
// @ts-ignore
const view = removed.viewLayer;
if (!view) return;
view.remove();
// @ts-ignore
delete removed.viewLayer;
}
@ -62,8 +64,7 @@ export default class ItemsView extends View {
* @return Object
* */
addTo(model: Component) {
var i = this.collection.indexOf(model);
this.addToCollection(model, null, i);
this.addToCollection(model, null, this.collection.indexOf(model));
}
/**
@ -74,41 +75,47 @@ export default class ItemsView extends View {
*
* @return Object Object created
* */
addToCollection(model: Component, fragmentEl: DocumentFragment | null, index?: number) {
const { parentView, opt, config } = this;
addToCollection(model: Component, fragment: DocumentFragment | null, index?: number) {
const { parentView, opt, config, el } = this;
const { ItemView, opened, module, level, sorter } = opt;
const fragment = fragmentEl || null;
const item = new ItemView({
ItemView,
level,
model,
parentView,
config,
sorter,
opened,
module,
});
const item: ItemView =
model.viewLayer ||
new ItemView({
ItemView,
level,
model,
parentView,
config,
sorter,
opened,
module,
});
const rendered = item.render().el;
if (fragment) {
fragment.appendChild(rendered);
} else {
if (typeof index !== 'undefined') {
var method = 'before';
const parent = el;
const children = parent.childNodes;
if (!isUndefined(index)) {
const lastIndex = children.length == index;
// If the added model is the last of collection
// need to change the logic of append
if (this.$el.children().length == index) {
if (lastIndex) {
index--;
method = 'after';
}
// In case the added is new in the collection index will be -1
if (index < 0) {
this.$el.append(rendered);
if (lastIndex || !children.length) {
parent.appendChild(rendered);
} else {
// @ts-ignore
this.$el.children().eq(index)[method](rendered);
parent.insertBefore(rendered, children[index]);
}
} else this.$el.append(rendered);
} else {
parent.appendChild(rendered);
}
}
this.items.push(item);
return rendered;

8
src/pages/index.ts

@ -93,7 +93,7 @@ export default class PageManager extends ItemManagerModule<PageManagerConfig, Pa
const opt = { silent: true };
const configPages = config.pages?.map(page => new Page(page, { em, config })) || [];
pages.add(configPages, opt);
const mainPage = !pages.length ? this.add({ type: typeMain }, opt) : this.getMain();
const mainPage = !pages.length ? this.add({ type: typeMain }, opt) : this._initPage();
mainPage && this.select(mainPage, opt);
}
@ -101,7 +101,7 @@ export default class PageManager extends ItemManagerModule<PageManagerConfig, Pa
const { em, events } = this;
const lm = em.Layers;
const mainComp = page.getMainComponent();
lm && mainComp && lm.setRoot(mainComp as any);
lm && mainComp && lm.setRoot(mainComp);
em.trigger(events.select, page, m.previous('selected'));
this.__onChange(chnSel, page, opts);
}
@ -242,6 +242,10 @@ export default class PageManager extends ItemManagerModule<PageManagerConfig, Pa
return result;
}
_initPage() {
return this.get(this.config.selected!) || this.getMain();
}
_createId() {
const pages = this.getAll();
const len = pages.length + 16;

11
src/pages/types.ts

@ -1,7 +1,16 @@
import { ModuleConfig } from '../abstract/Module';
import { PageProperties } from './model/Page';
export interface PageManagerConfig extends ModuleConfig {
pages?: any[];
/**
* Default pages.
*/
pages?: PageProperties[];
/**
* ID of the page to select on editor load.
*/
selected?: string;
}
export interface SelectableOption {

2
src/panels/view/PanelView.ts

@ -83,7 +83,7 @@ export default class PanelView extends ModuleView<Panel> {
avoidContainerUpdate: true,
prefix: editor.getConfig().stylePrefix,
onEnd() {
em && em.trigger('change:canvasOffset');
em.Canvas.refresh({ all: true });
},
posFetcher: (el: HTMLElement, { target }: any) => {
const style = el.style as any;

33
src/parser/config/config.ts

@ -1,3 +1,6 @@
import { OptionAsDocument } from '../../common';
import { CssRuleJSON } from '../../css_composer/model/CssRule';
import { ComponentDefinitionDefined } from '../../dom_components/model/types';
import Editor from '../../editor';
export interface ParsedCssRule {
@ -11,7 +14,20 @@ export type CustomParserCss = (input: string, editor: Editor) => ParsedCssRule[]
export type CustomParserHtml = (input: string, options: HTMLParserOptions) => HTMLElement;
export interface HTMLParserOptions {
export interface HTMLParseResult {
html: ComponentDefinitionDefined | ComponentDefinitionDefined[];
css?: CssRuleJSON[];
doctype?: string;
root?: ComponentDefinitionDefined;
head?: ComponentDefinitionDefined;
}
export interface ParseNodeOptions extends HTMLParserOptions {
inSvg?: boolean;
skipChildren?: boolean;
}
export interface HTMLParserOptions extends OptionAsDocument {
/**
* DOMParser mime type.
* If you use the `text/html` parser, it will fix the invalid syntax automatically.
@ -32,11 +48,25 @@ export interface HTMLParserOptions {
*/
allowUnsafeAttr?: boolean;
/**
* Allow unsafe HTML attribute values (eg. `src="javascript:..."`).
* @default false
*/
allowUnsafeAttrValue?: boolean;
/**
* When false, removes empty text nodes when parsed, unless they contain a space.
* @default false
*/
keepEmptyTextNodes?: boolean;
/**
* Custom transformer to run before passing the input HTML to the parser.
* A common use case might be to sanitize the input string.
* @example
* preParser: htmlString => DOMPurify.sanitize(htmlString)
*/
preParser?: (input: string, opts: { editor: Editor }) => string;
}
export interface ParserConfig {
@ -84,6 +114,7 @@ const config: ParserConfig = {
htmlType: 'text/html',
allowScripts: false,
allowUnsafeAttr: false,
allowUnsafeAttrValue: false,
keepEmptyTextNodes: false,
},
};

2
src/parser/index.ts

@ -69,7 +69,7 @@ export default class ParserModule extends Module<ParserConfig & { name?: string
*/
parseHtml(input: string, options: HTMLParserOptions = {}) {
const { em, parserHtml } = this;
parserHtml.compTypes = (em.Components.getTypes() || {}) as any;
parserHtml.compTypes = em.Components.getTypes() || [];
return parserHtml.parse(input, this.parserCss, options);
}

2
src/parser/model/BrowserParserHtml.ts

@ -13,6 +13,8 @@ export default (str: string, config: HTMLParserOptions = {}) => {
let res: HTMLElement;
if (toHTML) {
if (config.asDocument) return doc;
// Replicate the old parser in order to avoid breaking changes
const { head, body } = doc;
// Move all scripts at the bottom of the page

338
src/parser/model/ParserHtml.ts

@ -1,24 +1,17 @@
import { each, isArray, isFunction, isUndefined } from 'underscore';
import { ObjectAny } from '../../common';
import { CssRuleJSON } from '../../css_composer/model/CssRule';
import { ComponentDefinitionDefined } from '../../dom_components/model/types';
import { ObjectAny, ObjectStrings } from '../../common';
import { ComponentDefinitionDefined, ComponentStackItem } from '../../dom_components/model/types';
import EditorModel from '../../editor/model/Editor';
import { HTMLParserOptions, ParserConfig } from '../config/config';
import { HTMLParseResult, HTMLParserOptions, ParseNodeOptions, ParserConfig } from '../config/config';
import BrowserParserHtml from './BrowserParserHtml';
type StringObject = Record<string, string>;
type HTMLParseResult = {
html: ComponentDefinitionDefined | ComponentDefinitionDefined[]; // TODO replace with components
css?: CssRuleJSON[];
};
import { doctypeToString } from '../../utils/dom';
const modelAttrStart = 'data-gjs-';
const event = 'parse:html';
const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boolean } = {}) => {
return {
compTypes: '',
compTypes: [] as ComponentStackItem[],
modelAttrStart,
@ -50,7 +43,7 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
*/
splitPropsFromAttr(attr: ObjectAny = {}) {
const props: ObjectAny = {};
const attrs: StringObject = {};
const attrs: ObjectStrings = {};
each(attr, (value, key) => {
if (key.indexOf(this.modelAttrStart) === 0) {
@ -131,159 +124,175 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
return result;
},
/**
* Get data from the node element
* @param {HTMLElement} el DOM element to traverse
* @return {Array<Object>}
*/
parseNode(el: HTMLElement, opts: ObjectAny = {}) {
const result: ComponentDefinitionDefined[] = [];
const nodes = el.childNodes;
parseNodeAttr(node: HTMLElement, result?: ComponentDefinitionDefined) {
const model = result || {};
const attrs = node.attributes || [];
const attrsLen = attrs.length;
for (var i = 0, len = nodes.length; i < len; i++) {
const node = nodes[i] as HTMLElement;
const attrs = node.attributes || [];
const attrsLen = attrs.length;
const nodePrev = result[result.length - 1];
const nodeChild = node.childNodes.length;
const ct = this.compTypes;
let model: ComponentDefinitionDefined = {}; // TODO use component properties
// Start with understanding what kind of component it is
if (ct) {
let obj: any = '';
let type = node.getAttribute && node.getAttribute(`${this.modelAttrStart}type`);
// If the type is already defined, use it
if (type) {
model = { type };
} else {
// Iterate over all available Component Types and
// the first with a valid result will be that component
for (let it = 0; it < ct.length; it++) {
const compType = ct[it];
// @ts-ignore
obj = compType.model.isComponent(node, opts);
if (obj) {
if (typeof obj !== 'object') {
// @ts-ignore
obj = { type: compType.id };
}
break;
}
}
for (let i = 0; i < attrsLen; i++) {
const nodeName = attrs[i].nodeName;
let nodeValue: string | boolean = attrs[i].nodeValue!;
model = obj;
if (nodeName == 'style') {
model.style = this.parseStyle(nodeValue);
} else if (nodeName == 'class') {
model.classes = this.parseClass(nodeValue);
} else if (nodeName == 'contenteditable') {
continue;
} else if (nodeName.indexOf(this.modelAttrStart) === 0) {
const propsResult = this.getPropAttribute(nodeName, nodeValue);
model[propsResult.name] = propsResult.value;
} else {
// @ts-ignore Check for attributes from props (eg. required, disabled)
if (nodeValue === '' && node[nodeName] === true) {
nodeValue = true;
}
}
// Set tag name if not yet done
if (!model.tagName) {
const tag = node.tagName || '';
const ns = node.namespaceURI || '';
model.tagName = tag && ns === 'http://www.w3.org/1999/xhtml' ? tag.toLowerCase() : tag;
}
if (!model.attributes) {
model.attributes = {};
}
if (attrsLen) {
model.attributes = {};
model.attributes[nodeName] = nodeValue;
}
}
// Parse attributes
for (let j = 0; j < attrsLen; j++) {
const nodeName = attrs[j].nodeName;
let nodeValue: string | boolean = attrs[j].nodeValue!;
// Isolate attributes
if (nodeName == 'style') {
model.style = this.parseStyle(nodeValue);
} else if (nodeName == 'class') {
model.classes = this.parseClass(nodeValue);
} else if (nodeName == 'contenteditable') {
continue;
} else if (nodeName.indexOf(this.modelAttrStart) === 0) {
const propsResult = this.getPropAttribute(nodeName, nodeValue);
model[propsResult.name] = propsResult.value;
} else {
// @ts-ignore Check for attributes from props (eg. required, disabled)
if (nodeValue === '' && node[nodeName] === true) {
nodeValue = true;
}
return model;
},
model.attributes[nodeName] = nodeValue;
detectNode(node: HTMLElement, opts: ParseNodeOptions = {}) {
const { compTypes } = this;
let result: ComponentDefinitionDefined = {};
if (compTypes) {
const type = node.getAttribute?.(`${this.modelAttrStart}type`);
// If the type is already defined, use it
if (type) {
result = { type };
} else {
// Find the component type
for (let i = 0; i < compTypes.length; i++) {
const compType = compTypes[i];
let obj = compType.model.isComponent(node, opts);
if (obj) {
if (typeof obj !== 'object') {
obj = { type: compType.id };
}
result = obj;
break;
}
}
}
}
// Check for nested elements but avoid it if already provided
if (nodeChild && !model.components) {
// Avoid infinite nested text nodes
const firstChild = node.childNodes[0];
// If there is only one child and it's a TEXTNODE
// just make it content of the current node
if (nodeChild === 1 && firstChild.nodeType === 3) {
!model.type && (model.type = 'text');
model.components = {
type: 'textnode',
content: firstChild.nodeValue,
};
} else {
model.components = this.parseNode(node, {
...opts,
inSvg: opts.inSvg || model.type === 'svg',
});
}
return result;
},
parseNode(node: HTMLElement, opts: ParseNodeOptions = {}) {
const nodes = (node as HTMLTemplateElement).content?.childNodes || node.childNodes;
const nodesLen = nodes.length;
let model = this.detectNode(node, opts);
if (!model.tagName) {
const tag = node.tagName || '';
const ns = node.namespaceURI || '';
model.tagName = tag && ns === 'http://www.w3.org/1999/xhtml' ? tag.toLowerCase() : tag;
}
model = this.parseNodeAttr(node, model);
// Check for custom void elements (valid in XML)
if (!nodesLen && `${node.outerHTML}`.slice(-2) === '/>') {
model.void = true;
}
// Check for nested elements but avoid it if already provided
if (nodesLen && !model.components && !opts.skipChildren) {
// Avoid infinite nested text nodes
const firstChild = nodes[0];
// If there is only one child and it's a TEXTNODE
// just make it content of the current node
if (nodesLen === 1 && firstChild.nodeType === 3) {
!model.type && (model.type = 'text');
model.components = {
type: 'textnode',
content: firstChild.nodeValue,
};
} else {
model.components = this.parseNodes(node, {
...opts,
inSvg: opts.inSvg || model.type === 'svg',
});
}
}
// Check if it's a text node and if could be moved to the prevous model
if (model.type == 'textnode') {
if (nodePrev && nodePrev.type == 'textnode') {
nodePrev.content += model.content;
continue;
// If all children are texts and there is any textnode inside, the parent should
// be text too otherwise it won't be possible to edit texnodes.
const comps = model.components;
if (!model.type && comps?.length) {
const { textTypes = [], textTags = [] } = config;
let allTxt = true;
let foundTextNode = false;
for (let i = 0; i < comps.length; i++) {
const comp = comps[i];
const cType = comp.type;
if (!textTypes.includes(cType) && !textTags.includes(comp.tagName)) {
allTxt = false;
break;
}
// Throw away empty nodes (keep spaces)
if (!opts.keepEmptyTextNodes) {
const content = node.nodeValue;
if (content != ' ' && !content!.trim()) {
continue;
}
if (cType === 'textnode') {
foundTextNode = true;
}
}
// Check for custom void elements (valid in XML)
if (!nodeChild && `${node.outerHTML}`.slice(-2) === '/>') {
model.void = true;
if (allTxt && foundTextNode) {
model.type = 'text';
}
}
// If all children are texts and there is some textnode the parent should
// be text too otherwise I'm unable to edit texnodes
const comps = model.components;
if (!model.type && comps) {
const { textTypes = [], textTags = [] } = config;
let allTxt = 1;
let foundTextNode = 0;
return model;
},
for (let ci = 0; ci < comps.length; ci++) {
const comp = comps[ci];
const cType = comp.type;
/**
* Get data from the node element
* @param {HTMLElement} el DOM element to traverse
* @return {Array<Object>}
*/
parseNodes(el: HTMLElement, opts: ParseNodeOptions = {}) {
const result: ComponentDefinitionDefined[] = [];
const nodes = (el as HTMLTemplateElement).content?.childNodes || el.childNodes;
const nodesLen = nodes.length;
if (!textTypes.includes(cType) && !textTags.includes(comp.tagName)) {
allTxt = 0;
break;
}
for (let i = 0; i < nodesLen; i++) {
const node = nodes[i] as HTMLElement;
const nodePrev = result[result.length - 1];
const model = this.parseNode(node, opts);
if (cType === 'textnode') {
foundTextNode = 1;
}
// Check if it's a text node and if it could be moved to the prevous one
if (model.type === 'textnode') {
if (nodePrev?.type === 'textnode') {
nodePrev.content += model.content;
continue;
}
if (allTxt && foundTextNode) {
model.type = 'text';
// Try to keep meaningful whitespaces when possible (#5984)
// Ref: https://github.com/GrapesJS/grapesjs/pull/5719#discussion_r1518531999
if (!opts.keepEmptyTextNodes) {
const content = node.nodeValue || '';
const isFirstOrLast = i === 0 || i === nodesLen - 1;
const hasNewLive = content.includes('\n');
if (content != ' ' && !content.trim() && (isFirstOrLast || hasNewLive)) {
continue;
}
}
}
// If tagName is still empty and is not a textnode, do not push it
// If the tagName is empty and it's not a textnode, skip it
if (!model.tagName && isUndefined(model.content)) {
continue;
}
@ -303,15 +312,25 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
parse(str: string, parserCss?: any, opts: HTMLParserOptions = {}) {
const conf = em?.get('Config') || {};
const res: HTMLParseResult = { html: [] };
const cf: ObjectAny = { ...config, ...opts };
const cf = { ...config, ...opts };
const options = {
...config.optionsHtml,
// @ts-ignore Support previous `configParser.htmlType` option
htmlType: config.optionsHtml?.htmlType || config.htmlType,
...opts,
};
const el = isFunction(cf.parserHtml) ? cf.parserHtml(str, options) : BrowserParserHtml(str, options);
const scripts = el.querySelectorAll('script');
const { preParser, asDocument } = options;
const input = isFunction(preParser) ? preParser(str, { editor: em?.getEditor()! }) : str;
const parseRes = isFunction(cf.parserHtml) ? cf.parserHtml(input, options) : BrowserParserHtml(input, options);
let root = parseRes as HTMLElement;
const docEl = parseRes as Document;
if (asDocument) {
root = docEl.documentElement;
res.doctype = doctypeToString(docEl.doctype);
}
const scripts = root.querySelectorAll('script');
let i = scripts.length;
// Support previous `configMain.allowScripts` option
@ -319,48 +338,59 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
// Remove script tags
if (!allowScripts) {
while (i--) scripts[i].parentNode.removeChild(scripts[i]);
while (i--) scripts[i].parentNode?.removeChild(scripts[i]);
}
// Remove unsafe attributes
if (!options.allowUnsafeAttr) {
this.__clearUnsafeAttr(el);
if (!options.allowUnsafeAttr || !options.allowUnsafeAttrValue) {
this.__sanitizeNode(root, options);
}
// Detach style tags and parse them
if (parserCss) {
const styles = el.querySelectorAll('style');
const styles = root.querySelectorAll('style');
let j = styles.length;
let styleStr = '';
while (j--) {
styleStr = styles[j].innerHTML + styleStr;
styles[j].parentNode.removeChild(styles[j]);
styles[j].parentNode?.removeChild(styles[j]);
}
if (styleStr) res.css = parserCss.parse(styleStr);
}
em && em.trigger(`${event}:root`, { input: str, root: el });
const result = this.parseNode(el, cf);
// I have to keep it otherwise it breaks the DomComponents.addComponent (returns always array)
const resHtml = result.length === 1 && !cf.returnArray ? result[0] : result;
em?.trigger(`${event}:root`, { input, root: root });
let resHtml: HTMLParseResult['html'] = [];
if (asDocument) {
res.head = this.parseNode(docEl.head, cf);
res.root = this.parseNodeAttr(root);
resHtml = this.parseNode(docEl.body, cf);
} else {
const result = this.parseNodes(root, cf);
// I have to keep it otherwise it breaks the DomComponents.addComponent (returns always array)
resHtml = result.length === 1 && !cf.returnArray ? result[0] : result;
}
res.html = resHtml;
em && em.trigger(event, { input: str, output: res });
em?.trigger(event, { input, output: res });
return res;
},
__clearUnsafeAttr(node: HTMLElement) {
__sanitizeNode(node: HTMLElement, opts: HTMLParserOptions) {
const attrs = node.attributes || [];
const nodes = node.childNodes || [];
const toRemove: string[] = [];
each(attrs, attr => {
const name = attr.nodeName || '';
name.indexOf('on') === 0 && toRemove.push(name);
const value = attr.nodeValue || '';
!opts.allowUnsafeAttr && name.startsWith('on') && toRemove.push(name);
!opts.allowUnsafeAttrValue && value.startsWith('javascript:') && toRemove.push(name);
});
toRemove.map(name => node.removeAttribute(name));
each(nodes, node => this.__clearUnsafeAttr(node as HTMLElement));
each(nodes, node => this.__sanitizeNode(node as HTMLElement, opts));
},
};
};

4
src/rich_text_editor/index.ts

@ -46,10 +46,12 @@ import { createEl, cx, on, removeEl } from '../utils/dom';
import { hasWin, isDef } from '../utils/mixins';
import defaults, { CustomRTE, RichTextEditorConfig } from './config/config';
import RichTextEditor, { RichTextEditorAction } from './model/RichTextEditor';
import CanvasEvents from '../canvas/types';
import { ComponentsEvents } from '../dom_components/types';
export type RichTextEditorEvent = 'rte:enable' | 'rte:disable' | 'rte:custom';
const eventsUp = 'change:canvasOffset frame:scroll component:update';
const eventsUp = `${CanvasEvents.refresh} frame:scroll ${ComponentsEvents.update}`;
export const evEnable = 'rte:enable';
export const evDisable = 'rte:disable';

7
src/selector_manager/index.ts

@ -87,6 +87,7 @@ import { ItemManagerModule } from '../abstract/Module';
import { StyleModuleParam } from '../style_manager';
import StyleableModel from '../domain_abstract/model/StyleableModel';
import CssRule from '../css_composer/model/CssRule';
import { ComponentsEvents } from '../dom_components/types';
export type SelectorEvent = 'selector:add' | 'selector:remove' | 'selector:update' | 'selector:state' | 'selector';
@ -158,9 +159,9 @@ export default class SelectorManager extends ItemManagerModule<SelectorManagerCo
});
em.on('change:state', (m, value) => em.trigger(evState, value));
this.model.on('change:cFirst', (m, value) => em.trigger('selector:type', value));
em.on('component:toggled component:update:classes', this.__updateSelectedByComponents);
const listenTo =
'component:toggled component:update:classes change:device styleManager:update selector:state selector:type style:target';
const eventCmpUpdateCls = `${ComponentsEvents.update}:classes`;
em.on(`component:toggled ${eventCmpUpdateCls}`, this.__updateSelectedByComponents);
const listenTo = `component:toggled ${eventCmpUpdateCls} change:device styleManager:update selector:state selector:type style:target`;
this.model.listenTo(em, listenTo, () => this.__update());
}

20
src/selector_manager/model/Selector.ts

@ -1,6 +1,7 @@
import { result, forEach, keys } from 'underscore';
import { Model } from '../../common';
import EditorModel from '../../editor/model/Editor';
import { SelectorManagerConfig } from '../config/config';
const TYPE_CLASS = 1;
const TYPE_ID = 2;
@ -14,6 +15,15 @@ export interface SelectorProps {
protected?: boolean;
}
export interface SelectorPropsCustom extends SelectorProps {
[key: string]: unknown;
}
export interface SelectorOptions {
config?: SelectorManagerConfig;
em?: EditorModel;
}
/**
* @typedef Selector
* @property {String} name Selector name, eg. `my-class`
@ -23,7 +33,7 @@ export interface SelectorProps {
* @property {Boolean} [private=false] If true, it can't be seen by the Style Manager, but it will be rendered in the canvas and in export code.
* @property {Boolean} [protected=false] If true, it can't be removed from the attached component.
*/
export default class Selector extends Model<SelectorProps & { [key: string]: unknown }> {
export default class Selector extends Model<SelectorPropsCustom> {
defaults() {
return {
name: '',
@ -40,12 +50,12 @@ export default class Selector extends Model<SelectorProps & { [key: string]: unk
static readonly TYPE_CLASS = TYPE_CLASS;
static readonly TYPE_ID = TYPE_ID;
em: EditorModel;
em?: EditorModel;
/**
* @hideconstructor
*/
constructor(props: any, opts: any = {}) {
constructor(props: SelectorPropsCustom, opts: SelectorOptions = {}) {
super(props, opts);
const { config = {} } = opts;
const name = this.get('name');
@ -159,7 +169,7 @@ export default class Selector extends Model<SelectorProps & { [key: string]: unk
let obj = Model.prototype.toJSON.call(this, [opts]);
const defaults = result(this, 'defaults');
if (em && em.getConfig().avoidDefaults) {
if (em?.getConfig().avoidDefaults) {
forEach(defaults, (value, key) => {
if (obj[key] === value) {
delete obj[key];
@ -191,7 +201,7 @@ export default class Selector extends Model<SelectorProps & { [key: string]: unk
* @private
*/
static escapeName(name: string) {
return `${name}`.trim().replace(/([^a-z0-9\w-\\:@\\/]+)/gi, '-');
return `${name}`.trim().replace(/\s+/g, '-');
}
}

7
src/selector_manager/view/ClassTagsView.ts

@ -9,6 +9,7 @@ import Component from '../../dom_components/model/Component';
import Selector from '../model/Selector';
import Selectors from '../model/Selectors';
import CssRule from '../../css_composer/model/CssRule';
import { ComponentsEvents } from '../../dom_components/types';
export default class ClassTagsView extends View<Selector> {
template({ labelInfo, labelHead, iconSync, iconAdd, pfx, ppfx }: any) {
@ -83,8 +84,10 @@ export default class ClassTagsView extends View<Selector> {
this.em = em;
this.componentChanged = debounce(this.componentChanged.bind(this), 0);
this.checkSync = debounce(this.checkSync.bind(this), 0);
const toList = 'component:toggled component:update:classes';
const toListCls = 'component:update:classes change:state';
const eventCmpUpdate = ComponentsEvents.update;
const evClsUp = `${eventCmpUpdate}:classes`;
const toList = `component:toggled ${evClsUp}`;
const toListCls = `${evClsUp} ${eventCmpUpdate}:attributes:id change:state`;
this.listenTo(em, toList, this.componentChanged);
this.listenTo(em, 'styleManager:update', this.componentChanged);
this.listenTo(em, toListCls, this.__handleStateChange);

38
src/style_manager/index.ts

@ -83,6 +83,7 @@ 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';
export type PropertyTypes = PropertyStackProps | PropertySelectProps | PropertyNumberProps;
@ -168,7 +169,8 @@ export default class StyleManager extends ItemManagerModule<
this.model = model;
// Triggers for the selection refresh and properties
const ev = 'component:toggled component:update:classes change:state change:device frame:resized selector:type';
const eventCmpUpdate = ComponentsEvents.update;
const ev = `component:toggled ${eventCmpUpdate}:classes change:state change:device frame:resized selector:type`;
this.upAll = debounce(() => this.__upSel(), 0);
model.listenTo(em, ev, this.upAll as any);
// Clear state target on any component selection change, without debounce (#4208)
@ -523,7 +525,7 @@ export default class StyleManager extends ItemManagerModule<
* options: [{ id: 'value1', label: 'Some label' }, ...],
* })
*/
addBuiltIn(prop: string, definition: Omit<PropertyProps, 'property'> & { proeperty?: 'string' }) {
addBuiltIn(prop: string, definition: PropertyProps) {
return this.builtIn.add(prop, definition);
}
@ -597,6 +599,8 @@ export default class StyleManager extends ItemManagerModule<
const cmp = target.toHTML ? target : target.getComponent();
const optsSel = { array: true } as const;
let cmpRules: CssRule[] = [];
let tagNameRules: CssRule[] = [];
let invisibleAndOtherRules: CssRule[] = [];
let otherRules: CssRule[] = [];
let rules: CssRule[] = [];
@ -605,19 +609,41 @@ export default class StyleManager extends ItemManagerModule<
? []
: cssC.getRules().filter(rule => {
const rSels = rule.getSelectors().map(s => s.getFullName());
return !!rSels.length && rSels.every(rSel => values.indexOf(rSel) >= 0);
// rSels is equal to 0 when rule selectors contain tagName like : p {}, .logo path {}, ul li {}
if (rSels.length === 0) {
return false;
}
return rSels.every(rSel => values.indexOf(rSel) >= 0);
});
};
const rulesByTagName = (tagName: string) => {
return !tagName ? [] : cssC.getRules().filter(rule => rule.selectorsToString() === tagName);
};
// Componente related rule
if (cmp) {
cmpRules = cssC.getRules(`#${cmp.getId()}`);
tagNameRules = rulesByTagName(cmp.get('tagName'));
otherRules = sel ? rulesBySelectors(sel.getSelectors().getFullName(optsSel)) : [];
rules = otherRules.concat(cmpRules);
rules = otherRules.concat(tagNameRules).concat(cmpRules);
} else {
cmpRules = sel ? cssC.getRules(`#${sel.getId()}`) : [];
otherRules = rulesBySelectors(target.getSelectors().getFullName(optsSel));
rules = cmpRules.concat(otherRules);
tagNameRules = rulesByTagName(sel?.get('tagName') || '');
// Get rules set on invisible selectors like private one
const allCmpClasses = sel?.getSelectors().getFullName(optsSel) || [];
const invisibleSel = allCmpClasses.filter(
(item: string) =>
target
.getSelectors()
.getFullName(optsSel)
.findIndex(sel => sel === item) === -1
);
// Get rules set on active and visible selectors
invisibleAndOtherRules = rulesBySelectors(invisibleSel.concat(target.getSelectors().getFullName(optsSel)));
rules = tagNameRules.concat(cmpRules).concat(invisibleAndOtherRules);
}
const all = rules

4
src/styles/scss/_gjs_canvas.scss

@ -271,6 +271,10 @@ $guide_pad: 5px !default;
outline: none;
z-index: 1;
}
/* Always place the tools above the highlighter */
##{$app-prefix}tools {
z-index: 2;
}
/* This simulate body behaviour */
// > div:first-child {

32
src/styles/scss/main.scss

@ -444,17 +444,18 @@ $colorsAll: (one, var(--gjs-primary-color)),
/********* Spectrum **********/
.#{$app-prefix}editor-cont {
.#{$app-prefix}editor-sp {
border: 1px solid var(--gjs-main-dark-color);
box-shadow: 0 0 7px var(--gjs-main-dark-color);
border-radius: 3px;
}
.#{$app-prefix}editor-sp {
.sp-hue, .sp-slider{ cursor: row-resize;}
.sp-color, .sp-dragger{ cursor: crosshair;}
.sp-alpha-inner, .sp-alpha-handle{cursor: col-resize;}
.sp-hue{ left: 90%; }
.sp-color{right: 15%;}
.sp-container {
border: 1px solid var(--gjs-main-dark-color);
box-shadow: 0 0 7px var(--gjs-main-dark-color);
border-radius: 3px;
}
.sp-picker-container{border:none;}
.colpick_dark .colpick_color { outline: 1px solid var(--gjs-main-dark-color);}
.sp-cancel, .sp-cancel:hover {
@ -484,14 +485,17 @@ $colorsAll: (one, var(--gjs-primary-color)),
width: 100%;
position: relative;
text-align: right;
}
.sp-container button, .sp-container button:hover, .sp-container button:active{
background: var(--gjs-main-dark-color);
border-color: var(--gjs-main-dark-color);
color: var(--gjs-font-color);
text-shadow: none;
box-shadow: none;
padding: 3px 5px;
.sp-choose,
.sp-choose:hover,
.sp-choose:active {
background: var(--gjs-main-dark-color);
border-color: var(--gjs-main-dark-color);
color: var(--gjs-font-color);
text-shadow: none;
box-shadow: none;
padding: 3px 5px;
}
}
.sp-palette-container{
border: none;

9
src/trait_manager/model/Traits.ts

@ -5,7 +5,7 @@ import Categories from '../../abstract/ModuleCategories';
import { AddOptions } from '../../common';
import Component from '../../dom_components/model/Component';
import EditorModel from '../../editor/model/Editor';
import { TraitProperties } from '../types';
import TraitsEvents, { TraitProperties } from '../types';
import Trait from './Trait';
import TraitFactory from './TraitFactory';
@ -17,7 +17,12 @@ export default class Traits extends CollectionWithCategories<Trait> {
constructor(coll: TraitProperties[], options: { em: EditorModel }) {
super(coll);
this.em = options.em;
const { em } = options;
this.em = em;
this.categories = new Categories([], {
em,
events: { update: TraitsEvents.categoryUpdate },
});
this.on('add', this.handleAdd);
this.on('reset', this.handleReset);
const tm = this.module;

7
src/trait_manager/types.ts

@ -188,6 +188,13 @@ export enum TraitsEvents {
*/
value = 'trait:value',
/**
* @event `trait:category:update` Trait category updated.
* @example
* editor.on('trait:category:update', ({ category, changes }) => { ... });
*/
categoryUpdate = 'trait:category:update',
/**
* @event `trait:custom` Event to use in case of [custom Trait Manager UI](https://grapesjs.com/docs/modules/Traits.html#custom-trait-manager).
* @example

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save