Browse Source

Merge pull request #4080 from artf/dev

Merge dev
pull/4254/head
Artur Arseniev 5 years ago
committed by GitHub
parent
commit
1018588b9c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .eslintrc
  2. 1
      .npmignore
  3. 2
      dist/css/grapes.min.css
  4. 4
      dist/grapes.min.js
  5. 2
      dist/grapes.min.js.map
  6. 6
      docs/.vuepress/config.js
  7. BIN
      docs/.vuepress/public/sm-base-type.jpg
  8. BIN
      docs/.vuepress/public/sm-type-color.jpg
  9. BIN
      docs/.vuepress/public/sm-type-composite.jpg
  10. BIN
      docs/.vuepress/public/sm-type-number.jpg
  11. BIN
      docs/.vuepress/public/sm-type-radio.jpg
  12. BIN
      docs/.vuepress/public/sm-type-select.jpg
  13. BIN
      docs/.vuepress/public/sm-type-slider.jpg
  14. BIN
      docs/.vuepress/public/sm-type-stack.jpg
  15. BIN
      docs/.vuepress/public/style-manager.jpg
  16. 4
      docs/README.md
  17. 7
      docs/api.js
  18. 19
      docs/api/canvas.md
  19. 18
      docs/api/commands.md
  20. 71
      docs/api/component.md
  21. 84
      docs/api/components.md
  22. 1
      docs/api/css_composer.md
  23. 41
      docs/api/css_rule.md
  24. 270
      docs/api/editor.md
  25. 16
      docs/api/keymaps.md
  26. 77
      docs/api/layer.md
  27. 164
      docs/api/property.md
  28. 113
      docs/api/property_composite.md
  29. 66
      docs/api/property_number.md
  30. 78
      docs/api/property_select.md
  31. 214
      docs/api/property_stack.md
  32. 14
      docs/api/rich_text_editor.md
  33. 17
      docs/api/sector.md
  34. 23
      docs/api/storage_manager.md
  35. 370
      docs/api/style_manager.md
  36. 2
      docs/modules/Blocks.md
  37. 2
      docs/modules/Commands.md
  38. 2
      docs/modules/Components.md
  39. 871
      docs/modules/Style-manager.md
  40. 12
      package.json
  41. 25
      src/asset_manager/index.js
  42. 2
      src/asset_manager/view/FileUploader.js
  43. 2
      src/block_manager/view/BlockView.js
  44. 53
      src/canvas/index.js
  45. 2
      src/canvas/view/CanvasView.js
  46. 5
      src/code_manager/model/CssGenerator.js
  47. 43
      src/commands/index.js
  48. 16
      src/commands/view/ComponentDrag.js
  49. 54
      src/commands/view/OpenStyleManager.js
  50. 8
      src/commands/view/PasteComponent.js
  51. 90
      src/commands/view/SelectComponent.js
  52. 1
      src/common/View.js
  53. 1
      src/common/index.js
  54. 20
      src/common/module.js
  55. 17
      src/css_composer/config/config.js
  56. 31
      src/css_composer/index.js
  57. 60
      src/css_composer/model/CssRule.js
  58. 14
      src/device_manager/config/config.js
  59. 8
      src/device_manager/model/Device.js
  60. 2
      src/dom_components/config/config.js
  61. 145
      src/dom_components/index.js
  62. 237
      src/dom_components/model/Component.js
  63. 37
      src/domain_abstract/model/Styleable.js
  64. 30
      src/domain_abstract/model/TypeableCollection.js
  65. 12
      src/domain_abstract/ui/Input.js
  66. 42
      src/domain_abstract/ui/InputColor.js
  67. 36
      src/domain_abstract/ui/InputNumber.js
  68. 102
      src/editor/config/config.js
  69. 80
      src/editor/index.js
  70. 97
      src/editor/model/Editor.js
  71. 11
      src/i18n/config.js
  72. 18
      src/i18n/index.js
  73. 88
      src/i18n/locale/en.js
  74. 13
      src/index.js
  75. 42
      src/keymaps/index.js
  76. 2
      src/navigator/config/config.js
  77. 54
      src/navigator/view/ItemView.js
  78. 33
      src/rich_text_editor/index.js
  79. 55
      src/selector_manager/index.js
  80. 25
      src/selector_manager/model/Selectors.js
  81. 33
      src/selector_manager/view/ClassTagsView.js
  82. 2
      src/storage_manager/config/config.js
  83. 23
      src/storage_manager/index.js
  84. 70
      src/style_manager/config/config.js
  85. 687
      src/style_manager/index.js
  86. 153
      src/style_manager/model/Layer.js
  87. 127
      src/style_manager/model/Layers.js
  88. 82
      src/style_manager/model/Properties.js
  89. 363
      src/style_manager/model/Property.js
  90. 373
      src/style_manager/model/PropertyComposite.js
  91. 1127
      src/style_manager/model/PropertyFactory.js
  92. 76
      src/style_manager/model/PropertyInteger.js
  93. 116
      src/style_manager/model/PropertyNumber.js
  94. 45
      src/style_manager/model/PropertyRadio.js
  95. 107
      src/style_manager/model/PropertySelect.js
  96. 14
      src/style_manager/model/PropertySlider.js
  97. 533
      src/style_manager/model/PropertyStack.js
  98. 97
      src/style_manager/model/Sector.js
  99. 12
      src/style_manager/model/Sectors.js
  100. 238
      src/style_manager/view/LayerView.js

2
.eslintrc

@ -4,7 +4,7 @@
"node": true
},
"parserOptions": {
"ecmaVersion": 2018,
"ecmaVersion": 2020,
"sourceType": "module"
},
"rules": {

1
.npmignore

@ -27,3 +27,4 @@ test
.travis.yml
*.md
webpack.config.js
tsconfig.json

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

6
docs/.vuepress/config.js

@ -73,6 +73,12 @@ module.exports = {
['/api/page', `${subDivider}Page`],
['/api/style_manager', 'Style Manager'],
['/api/sector', `${subDivider}Sector`],
['/api/property', `${subDivider}Property`],
['/api/property_number', `${subDivider}PropertyNumber`],
['/api/property_select', `${subDivider}PropertySelect`],
['/api/property_composite', `${subDivider}PropertyComposite`],
['/api/property_stack', `${subDivider}PropertyStack`],
['/api/layer', `${subDivider}Layer`],
['/api/storage_manager', 'Storage Manager'],
['/api/device_manager', 'Device Manager'],
['/api/device', `${subDivider}Device`],

BIN
docs/.vuepress/public/sm-base-type.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
docs/.vuepress/public/sm-type-color.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
docs/.vuepress/public/sm-type-composite.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/.vuepress/public/sm-type-number.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
docs/.vuepress/public/sm-type-radio.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
docs/.vuepress/public/sm-type-select.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
docs/.vuepress/public/sm-type-slider.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
docs/.vuepress/public/sm-type-stack.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/.vuepress/public/style-manager.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

4
docs/README.md

@ -3,7 +3,7 @@
[[toc]]
## What is GrapesJS?
At first glance might think this is just another page/HTML builder, but it's something more. GrapesJS is a multi-purpose, Web Builder Framework, which means it allows you to easily create a drag & drop enabled builder of "things". By "things" we mean anything with HTML-like structure, which entails much more than web pages. We use HTML-like structure basically everywhere: Newsletters (eg. [MJML](https://mjml.io/)), Native Mobile Applications (eg. [React Native](https://github.com/facebook/react-native)), Native Desktop Applications (eg. [Vuido](https://vuido.mimec.org)), PDFs (eg. [React PDF](https://github.com/diegomura/react-pdf)), etc. So, for everything you can imagine as a set of elements like `<tag some="attribute">... other nested elements ...</tag>` you can create easily a GrapesJS builder around it and then use it independently your applications.
At first glance one might think this is just another page/HTML builder, but it's something more. GrapesJS is a multi-purpose, Web Builder Framework, which means it allows you to easily create a drag & drop enabled builder of "things". By "things" we mean anything with HTML-like structure, which entails much more than web pages. We use HTML-like structure basically everywhere: Newsletters (eg. [MJML](https://mjml.io/)), Native Mobile Applications (eg. [React Native](https://github.com/facebook/react-native)), Native Desktop Applications (eg. [Vuido](https://vuido.mimec.org)), PDFs (eg. [React PDF](https://github.com/diegomura/react-pdf)), etc. So, for everything you can imagine as a set of elements like `<tag some="attribute">... other nested elements ...</tag>` you can create easily a GrapesJS builder around it and then use it independently your applications.
GrapesJS ships with features and tools that enable you to craft easy to use builders. Which allows your users to create complex HTML-like templates without any knowledge of coding.
@ -11,7 +11,7 @@ GrapesJS ships with features and tools that enable you to craft easy to use buil
## Why GrapesJS?
GrapesJS was designed primarily for use inside Content Management Systems to speed up the creation of dynamic templates and replace common WYSIWYG editors. Which are good for content editing, but inappropriate for creating HTML structures. Instead of creating an application we decided to create an extensible framework that could be used by anyone for any purpose.
GrapesJS was designed primarily for use inside Content Management Systems to speed up the creation of dynamic templates and replace common WYSIWYG editors, which are good for content editing, but inappropriate for creating HTML structures. Instead of creating an application we decided to create an extensible framework that could be used by anyone for any purpose.

7
docs/api.js

@ -22,6 +22,12 @@ async function generateDocs () {
['panels/index.js', 'panels.md'],
['style_manager/index.js', 'style_manager.md'],
['style_manager/model/Sector.js', 'sector.md'],
['style_manager/model/Property.js', 'property.md'],
['style_manager/model/PropertyNumber.js', 'property_number.md'],
['style_manager/model/PropertySelect.js', 'property_select.md'],
['style_manager/model/PropertyComposite.js', 'property_composite.md'],
['style_manager/model/PropertyStack.js', 'property_stack.md'],
['style_manager/model/Layer.js', 'layer.md'],
['storage_manager/index.js', 'storage_manager.md'],
['device_manager/index.js', 'device_manager.md'],
['device_manager/model/Device.js', 'device.md'],
@ -50,6 +56,7 @@ async function generateDocs () {
.replace(/<\\\[/g, '<[')
.replace(/<\(\\\[/g, '<([')
.replace(/\| \\\[/g, '| [')
.replace(/\\n```js/g, "```js")
.replace('**Extends Model**', '');
fs.writeFileSync(`${docRoot}/api/${file[1]}`, res);
log('Created', file[1]);

19
docs/api/canvas.md

@ -12,12 +12,29 @@ const editor = grapesjs.init({
})
```
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
```js
// Listen to events
editor.on('canvas:drop', () => { ... });
// Use the API
const canvas = editor.Canvas;
canvas.setCoords(...);
```
## Available Events
* `canvas:dragenter` - When something is dragged inside the canvas, `DataTransfer` instance passed as an argument
* `canvas:dragover` - When something is dragging on canvas, `DataTransfer` instance passed as an argument
* `canvas:drop` - Something is dropped in canvas, `DataTransfer` instance and the dropped model are passed as arguments
* `canvas:dragend` - When a drag operation is ended, `DataTransfer` instance passed as an argument
* `canvas:dragdata` - On any dataTransfer parse, `DataTransfer` instance and the `result` are passed as arguments.
By changing `result.content` you're able to customize what is dropped
## Methods
* [getConfig][2]
* [getElement][3]
* [getFrameEl][4]

18
docs/api/commands.md

@ -12,12 +12,28 @@ const editor = grapesjs.init({
})
```
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
```js
// Listen to events
editor.on('run', () => { ... });
// Use the API
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
## Methods
* [add][2]
* [get][3]
* [getAll][4]

71
docs/api/component.md

@ -28,10 +28,10 @@ component.get('tagName');
* `attributes` **[Object][2]?** Key-value object of the component's attributes, eg. `{ title: 'Hello' }` Default: `{}`
* `name` **[String][1]?** Name of the component. Will be used, for example, in Layers and badges
* `removable` **[Boolean][3]?** When `true` the component is removable from the canvas, default: `true`
* `draggable` **([Boolean][3] | [String][1])?** Indicates if it's possible to drag the component inside others.
* `draggable` **([Boolean][3] | [String][1] | [Function][4])?** Indicates if it's possible to drag the component inside others.
You can also specify a query string to indentify elements,
eg. `'.some-class[title=Hello], [data-gjs-type=column]'` means you can drag the component only inside elements
containing `some-class` class and `Hello` title, and `column` components. Default: `true`
containing `some-class` class and `Hello` title, and `column` components. In the case of a function, target and destination components are passed as arguments, return a Boolean to indicate if the drag is possible. Default: `true`
* `droppable` **([Boolean][3] | [String][1] | [Function][4])?** Indicates if it's possible to drop other components inside. You can use
a query string as with `draggable`. In the case of a function, target and destination components are passed as arguments, return a Boolean to indicate if the drop is possible. Default: `true`
* `badgable` **[Boolean][3]?** Set to false if you don't want to see the badge (with the name) over the component. Default: `true`
@ -485,9 +485,41 @@ component.parent();
Returns **([Component][9] | null)**
### getTraits
Get traits.
#### Examples
```javascript
const traits = component.getTraits();
console.log(traits);
// [Trait, Trait, Trait, ...]
```
Returns **[Array][5]\<Trait>**
### setTraits
Replace current collection of traits with a new one.
#### Parameters
* `traits` **[Array][5]<[Object][2]>** Array of trait definitions
#### Examples
```javascript
const traits = component.setTraits([{ type: 'checkbox', name: 'disabled'}, ...]);
console.log(traits);
// [Trait, ...]
```
Returns **[Array][5]\<Trait>**
### getTrait
Get the trait by id/name
Get the trait by id/name.
#### Parameters
@ -500,11 +532,11 @@ const traitTitle = component.getTrait('title');
traitTitle && traitTitle.set('label', 'New label');
```
Returns **Trait** Trait model
Returns **(Trait | null)** Trait getModelToStyle
### updateTrait
Update a trait
Update a trait.
#### Parameters
@ -555,11 +587,11 @@ component.removeTrait('title');
component.removeTrait(['title', 'id']);
```
Returns **[Array][5]** Array of removed traits
Returns **[Array][5]\<Trait>** Array of removed traits
### addTrait
Add trait/s by id/s.
Add new trait/s.
#### Parameters
@ -577,7 +609,7 @@ component.addTrait({
component.addTrait(['title', {...}, ...]);
```
Returns **[Array][5]** Array of added traits
Returns **[Array][5]\<Trait>** Array of added traits
### getName
@ -724,29 +756,6 @@ editor.getSelected().move(dest, { at: 0 });
Returns **this**
### getList
The list of components is taken from the Components module.
Initially, the list, was set statically on the Component object but it was
not ok, as it was shared between multiple editor instances
#### Parameters
* `model`
### checkId
This method checks, for each parsed component and style object
(are not Components/CSSRules yet), for duplicated id and fixes them
This method is used in Components.js just after the parsing
#### Parameters
* `components`
* `styles` (optional, default `[]`)
* `list` (optional, default `{}`)
* `opts` (optional, default `{}`)
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

84
docs/api/components.md

@ -1,6 +1,6 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## DomComponents
## Components
With this module is possible to manage components inside the canvas. You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object][1]
@ -12,12 +12,40 @@ const editor = grapesjs.init({
})
```
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
```js
const domComponents = editor.DomComponents;
// Listen to events
editor.on('component:create', () => { ... });
// Use the API
const cmp = editor.Components;
cmp.addType(...);
```
## Available Events
* `component:create` - Component is created (only the model, is not yet mounted in the canvas), called after the init() method
* `component:mount` - Component is mounted to an element and rendered in canvas
* `component:add` - Triggered when a new component is added to the editor, the model is passed as an argument to the callback
* `component:remove` - Triggered when a component is removed, the model is passed as an argument to the callback
* `component:remove:before` - Triggered before the remove of the component, the model, remove function (if aborted via options, with this function you can complete the remove) and options (use options.abort = true to prevent remove), are passed as arguments to the callback
* `component:clone` - Triggered when a component is cloned, the new model is passed as an argument to the callback
* `component:update` - Triggered when a component is updated (moved, styled, etc.), the model is passed as an argument to the callback
* `component:update:{propertyName}` - Listen any property change, the model is passed as an argument to the callback
* `component:styleUpdate` - Triggered when the style of the component is updated, the model is passed as an argument to the callback
* `component:styleUpdate:{propertyName}` - Listen for a specific style property change, the model is passed as an argument to the callback
* `component:selected` - New component selected, the selected model is passed as an argument to the callback
* `component:deselected` - Component deselected, the deselected model is passed as an argument to the callback
* `component:toggled` - Component selection changed, toggled model is passed as an argument to the callback
* `component:type:add` - New component type added, the new type is passed as an argument to the callback
* `component:type:update` - Component type updated, the updated type is passed as an argument to the callback
* `component:drag:start` - Component drag started. Passed an object, to the callback, containing the `target` (component to drag), `parent` (parent of the component) and `index` (component index in the parent)
* `component:drag` - During component drag. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the current pointer
* `component:drag:end` - Component drag ended. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the final pointer
## Methods
* [getWrapper][2]
* [getComponents][3]
* [addComponent][4]
@ -60,7 +88,7 @@ The wrapper doesn't differ from the original Component Model
```javascript
// Change background of the wrapper and set some attribute
var wrapper = domComponents.getWrapper();
var wrapper = cmp.getWrapper();
wrapper.set('style', {'background-color': 'red'});
wrapper.set('attributes', {'title': 'Hello!'});
```
@ -77,7 +105,7 @@ components inside and you can nest them as more as you wish.
```javascript
// Let's add some component
var wrapperChildren = domComponents.getComponents();
var wrapperChildren = cmp.getComponents();
var comp1 = wrapperChildren.add({
style: { 'background-color': 'red'}
});
@ -98,26 +126,26 @@ comp1Children.add([
wrapperChildren.remove(comp2);
```
Returns **Components** Collection of components
Returns **[Components][14]** Collection of components
## addComponent
Add new components to the wrapper's children. It's the same
as 'domComponents.getComponents().add(...)'
as 'cmp.getComponents().add(...)'
### Parameters
* `component` **([Object][12] | Component | [Array][14]<[Object][12]>)** Component/s to add
* `component` **([Object][12] | Component | [Array][15]<[Object][12]>)** Component/s to add
* `component.tagName` **[string][15]** Tag name (optional, default `'div'`)
* `component.type` **[string][15]** Type of the component. Available: ''(default), 'text', 'image' (optional, default `''`)
* `component.tagName` **[string][16]** Tag name (optional, default `'div'`)
* `component.type` **[string][16]** Type of the component. Available: ''(default), 'text', 'image' (optional, default `''`)
* `component.removable` **[boolean][13]** If component is removable (optional, default `true`)
* `component.draggable` **[boolean][13]** If is possible to move the component around the structure (optional, default `true`)
* `component.droppable` **[boolean][13]** If is possible to drop inside other components (optional, default `true`)
* `component.badgable` **[boolean][13]** If the badge is visible when the component is selected (optional, default `true`)
* `component.stylable` **[boolean][13]** If is possible to style component (optional, default `true`)
* `component.copyable` **[boolean][13]** If is possible to copy\&paste the component (optional, default `true`)
* `component.content` **[string][15]** String inside component (optional, default `''`)
* `component.content` **[string][16]** String inside component (optional, default `''`)
* `component.style` **[Object][12]** Style object (optional, default `{}`)
* `component.attributes` **[Object][12]** Attribute object (optional, default `{}`)
* `opt` **[Object][12]** the options object to be used by the \[Components.add][getComponents][3] method (optional, default `{}`)
@ -126,7 +154,7 @@ as 'domComponents.getComponents().add(...)'
```javascript
// Example of a new component with some extra property
var comp1 = domComponents.addComponent({
var comp1 = cmp.addComponent({
tagName: 'div',
removable: true, // Can't remove it
draggable: true, // Can't move it
@ -137,7 +165,7 @@ var comp1 = domComponents.addComponent({
});
```
Returns **(Component | [Array][14]\<Component>)** Component/s added
Returns **(Component | [Array][15]\<Component>)** Component/s added
## render
@ -146,7 +174,7 @@ Once the wrapper is rendered, and it's what happens when you init the editor,
the all new components will be added automatically and property changes are all
updated immediately
Returns **[HTMLElement][16]**
Returns **[HTMLElement][17]**
## clear
@ -161,11 +189,11 @@ Returns **this**
## addType
Add new component type.
Read more about this in [Define New Component][17]
Read more about this in [Define New Component][18]
### Parameters
* `type` **[string][15]** Component ID
* `type` **[string][16]** Component ID
* `methods` **[Object][12]** Component methods
Returns **this**
@ -173,13 +201,13 @@ Returns **this**
## getType
Get component type.
Read more about this in [Define New Component][17]
Read more about this in [Define New Component][18]
### Parameters
* `type` **[string][15]** Component ID
* `type` **[string][16]** Component ID
Returns **[Object][12]** Component type defintion, eg. `{ model: ..., view: ... }`
Returns **[Object][12]** Component type definition, eg. `{ model: ..., view: ... }`
## removeType
@ -188,15 +216,15 @@ Remove component type
### Parameters
* `id`
* `type` **[string][15]** Component ID
* `type` **[string][16]** Component ID
Returns **([Object][12] | [undefined][18])** Removed component type, undefined otherwise
Returns **([Object][12] | [undefined][19])** Removed component type, undefined otherwise
## getTypes
Return the array of all types
Returns **[Array][14]**
Returns **[Array][15]**
[1]: https://github.com/artf/grapesjs/blob/master/src/dom_components/config/config.js
@ -224,12 +252,14 @@ Returns **[Array][14]**
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[14]: #components
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[16]: https://developer.mozilla.org/docs/Web/HTML/Element
[17]: https://developer.mozilla.org/docs/Web/HTML/Element
[17]: https://grapesjs.com/docs/modules/Components.html#define-new-component
[18]: https://grapesjs.com/docs/modules/Components.html#define-new-component
[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined

1
docs/api/css_composer.md

@ -131,6 +131,7 @@ Remove rule, by CssRule or matching selector (eg. the selector will match also a
### Parameters
* `rule` **([String][8] | [CssRule] | [Array][9]<[CssRule]>)** CssRule or matching selector.
* `opts`
### Examples

41
docs/api/css_rule.md

@ -14,7 +14,7 @@
* `singleAtRule` **[Boolean][4]?** This property is used only on at-rules, like 'page' or 'font-face', where the block containes only style declarations
* `state` **[String][3]?** State of the rule, eg: `hover`, `focused`
* `important` **([Boolean][4] | [Array][1]<[String][3]>)?** If true, sets `!important` on all properties. You can also pass an array to specify properties on which use important
* `stylable` **[Boolean][4]?** Indicates if the rule is stylable from the editor
* `stylable` **[Boolean][4]?** Indicates if the rule is stylable from the editor[Device]: device.html[State]: state.html[Component]: component.html
### getAtRule
@ -72,6 +72,45 @@ cssRule.getDeclaration() // ".class1{color:red;}"
Returns **[String][3]**
### getDevice
Get the Device the rule is related to.
#### Examples
```javascript
const device = rule.getDevice();
console.log(device?.getName());
```
Returns **([Device] | null)**
### getState
Get the State the rule is related to.
#### Examples
```javascript
const state = rule.getState();
console.log(state?.getLabel());
```
Returns **([State] | null)**
### getComponent
Returns the related Component (valid only for component-specific rules).
#### Examples
```javascript
const cmp = rule.getComponent();
console.log(cmp?.toHTML());
```
Returns **([Component] | null)**
### toCSS
Return the CSS string of the rule

270
docs/api/editor.md

@ -21,114 +21,68 @@ editor.on('EVENT-NAME', (some, argument) => {
})
```
* `update` - The structure of the template is updated (its HTML/CSS)
* `undo` - Undo executed
* `redo` - Redo executed
* `load` - Editor is loaded
### Components
* `component:create` - Component is created (only the model, is not yet mounted in the canvas), called after the init() method
* `component:mount` - Component is mounted to an element and rendered in canvas
* `component:add` - Triggered when a new component is added to the editor, the model is passed as an argument to the callback
* `component:remove` - Triggered when a component is removed, the model is passed as an argument to the callback
* `component:remove:before` - Triggered before the remove of the component, the model, remove function (if aborted via options, with this function you can complete the remove) and options (use options.abort = true to prevent remove), are passed as arguments to the callback
* `component:clone` - Triggered when a component is cloned, the new model is passed as an argument to the callback
* `component:update` - Triggered when a component is updated (moved, styled, etc.), the model is passed as an argument to the callback
* `component:update:{propertyName}` - Listen any property change, the model is passed as an argument to the callback
* `component:styleUpdate` - Triggered when the style of the component is updated, the model is passed as an argument to the callback
* `component:styleUpdate:{propertyName}` - Listen for a specific style property change, the model is passed as an argument to the callback
* `component:selected` - New component selected, the selected model is passed as an argument to the callback
* `component:deselected` - Component deselected, the deselected model is passed as an argument to the callback
* `component:toggled` - Component selection changed, toggled model is passed as an argument to the callback
* `component:type:add` - New component type added, the new type is passed as an argument to the callback
* `component:type:update` - Component type updated, the updated type is passed as an argument to the callback
* `component:drag:start` - Component drag started. Passed an object, to the callback, containing the `target` (component to drag), `parent` (parent of the component) and `index` (component index in the parent)
* `component:drag` - During component drag. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the current pointer
* `component:drag:end` - Component drag ended. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the final pointer
Check the [Components][2] module.
### Keymaps
* `keymap:add` - New keymap added. The new keyamp object is passed as an argument
* `keymap:remove` - Keymap removed. The removed keyamp object is passed as an argument
* `keymap:emit` - Some keymap emitted, in arguments you get keymapId, shortcutUsed, Event
* `keymap:emit:{keymapId}` - `keymapId` emitted, in arguments you get keymapId, shortcutUsed, Event
Check the [Keymaps][3] module.
### Style Manager
* `styleManager:update:target` - The target (Component or CSSRule) is changed
* `styleManager:change` - Triggered on style property change from new selected component, the view of the property is passed as an argument to the callback
* `styleManager:change:{propertyName}` - As above but for a specific style property
Check the [Style Manager][4] module.
### Storages
### Storage
* `storage:start` - Before the storage request is started
* `storage:start:store` - Before the store request. The object to store is passed as an argumnet (which you can edit)
* `storage:start:load` - Before the load request. Items to load are passed as an argumnet (which you can edit)
* `storage:load` - Triggered when something was loaded from the storage, loaded object passed as an argumnet
* `storage:store` - Triggered when something is stored to the storage, stored object passed as an argumnet
* `storage:end` - After the storage request is ended
* `storage:end:store` - After the store request
* `storage:end:load` - After the load request
* `storage:error` - On any error on storage request, passes the error as an argument
* `storage:error:store` - Error on store request, passes the error as an argument
* `storage:error:load` - Error on load request, passes the error as an argument
Check the [Storage][5] module.
### Canvas
* `canvas:dragenter` - When something is dragged inside the canvas, `DataTransfer` instance passed as an argument
* `canvas:dragover` - When something is dragging on canvas, `DataTransfer` instance passed as an argument
* `canvas:drop` - Something is dropped in canvas, `DataTransfer` instance and the dropped model are passed as arguments
* `canvas:dragend` - When a drag operation is ended, `DataTransfer` instance passed as an argument
* `canvas:dragdata` - On any dataTransfer parse, `DataTransfer` instance and the `result` are passed as arguments.
By changing `result.content` you're able to customize what is dropped
Check the [Canvas][6] module.
### RTE
* `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
* `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument
Check the [Rich Text Editor][7] module.
### Commands
* `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
Check the [Commands][8] module.
### Selectors
Check the [Selectors][2] module.
Check the [Selectors][9] module.
### Blocks
Check the [Blocks][3] module.
Check the [Blocks][10] module.
### Assets
Check the [Assets][4] module.
Check the [Assets][11] module.
### Modal
Check the [Modal][5] module.
Check the [Modal][12] module.
### Devices
Check the [Devices][6] module.
Check the [Devices][13] module.
### Parser
Check the [Parser][7] module.
Check the [Parser][14] module.
### Pages
Check the [Pages][8] module.
### General
Check the [Pages][15] module.
* `canvasScroll` - Canvas is scrolled
* `update` - The structure of the template is updated (its HTML/CSS)
* `undo` - Undo executed
* `redo` - Redo executed
* `load` - Editor is loaded
## Methods
## getConfig
@ -136,7 +90,7 @@ Returns configuration object
### Parameters
* `prop` **[string][9]?** Property name
* `prop` **[string][16]?** Property name
Returns **any** Returns the configuration object or
the value of the specified property
@ -147,12 +101,12 @@ Returns HTML built inside canvas
### Parameters
* `opts` **[Object][10]** Options (optional, default `{}`)
* `opts` **[Object][17]** Options (optional, default `{}`)
* `opts.component` **Component?** Return the HTML of a specific Component
* `opts.cleanId` **[Boolean][11]** Remove unnecessary IDs (eg. those created automatically) (optional, default `false`)
* `opts.cleanId` **[Boolean][18]** Remove unnecessary IDs (eg. those created automatically) (optional, default `false`)
Returns **[string][9]** HTML string
Returns **[string][16]** HTML string
## getCss
@ -160,13 +114,13 @@ Returns CSS built inside canvas
### Parameters
* `opts` **[Object][10]** Options (optional, default `{}`)
* `opts` **[Object][17]** Options (optional, default `{}`)
* `opts.component` **Component?** Return the CSS of a specific Component
* `opts.json` **[Boolean][11]** Return an array of CssRules instead of the CSS string (optional, default `false`)
* `opts.avoidProtected` **[Boolean][11]** Don't include protected CSS (optional, default `false`)
* `opts.json` **[Boolean][18]** Return an array of CssRules instead of the CSS string (optional, default `false`)
* `opts.avoidProtected` **[Boolean][18]** Don't include protected CSS (optional, default `false`)
Returns **([String][9] | [Array][12]\<CssRule>)** CSS string or array of CssRules
Returns **([String][16] | [Array][19]\<CssRule>)** CSS string or array of CssRules
## getJs
@ -174,11 +128,11 @@ Returns JS of all components
### Parameters
* `opts` **[Object][10]** Options (optional, default `{}`)
* `opts` **[Object][17]** Options (optional, default `{}`)
* `opts.component` **Component?** Get the JS of a specific component
Returns **[String][9]** JS string
Returns **[String][16]** JS string
## getComponents
@ -198,8 +152,8 @@ Set components inside editor's canvas. This method overrides actual components
### Parameters
* `components` **([Array][12]<[Object][10]> | [Object][10] | [string][9])** HTML string or components model
* `opt` **[Object][10]** the options object to be used by the \[setComponents][em#setComponents][13] method (optional, default `{}`)
* `components` **([Array][19]<[Object][17]> | [Object][17] | [string][16])** HTML string or components model
* `opt` **[Object][17]** the options object to be used by the \[setComponents][em#setComponents][20] method (optional, default `{}`)
### Examples
@ -221,10 +175,10 @@ Add components
### Parameters
* `components` **([Array][12]<[Object][10]> | [Object][10] | [string][9])** HTML string or components model
* `opts` **[Object][10]** Options
* `components` **([Array][19]<[Object][17]> | [Object][17] | [string][16])** HTML string or components model
* `opts` **[Object][17]** Options
* `opts.avoidUpdateStyle` **[Boolean][11]** If the HTML string contains styles,
* `opts.avoidUpdateStyle` **[Boolean][18]** If the HTML string contains styles,
by default, they will be created and, if already exist, updated. When this option
is true, styles already created will not be updated. (optional, default `false`)
@ -240,13 +194,13 @@ editor.addComponents({
});
```
Returns **[Array][12]\<Component>**
Returns **[Array][19]\<Component>**
## getStyle
Returns style in JSON format object
Returns **[Object][10]**
Returns **[Object][17]**
## setStyle
@ -254,7 +208,7 @@ Set style inside editor's canvas. This method overrides actual style
### Parameters
* `style` **([Array][12]<[Object][10]> | [Object][10] | [string][9])** CSS string or style model
* `style` **([Array][19]<[Object][17]> | [Object][17] | [string][16])** CSS string or style model
* `opt` (optional, default `{}`)
### Examples
@ -276,7 +230,7 @@ Add styles to the editor
### Parameters
* `style` **([Array][12]<[Object][10]> | [Object][10] | [string][9])** CSS string or style model
* `style` **([Array][19]<[Object][17]> | [Object][17] | [string][16])** CSS string or style model
* `opts` (optional, default `{}`)
### Examples
@ -285,7 +239,7 @@ Add styles to the editor
editor.addStyle('.cls{color: red}');
```
Returns **[Array][12]\<CssRule>** Array of created CssRule instances
Returns **[Array][19]\<CssRule>** Array of created CssRule instances
## getSelected
@ -297,7 +251,7 @@ Returns **Model**
Returns an array of all selected components
Returns **[Array][12]**
Returns **[Array][19]**
## getSelectedToStyle
@ -315,10 +269,10 @@ Select a component
### Parameters
* `el` **(Component | [HTMLElement][14])** Component to select
* `opts` **[Object][10]?** Options
* `el` **(Component | [HTMLElement][21])** Component to select
* `opts` **[Object][17]?** Options
* `opts.scroll` **[Boolean][11]?** Scroll canvas to the selected element
* `opts.scroll` **[Boolean][18]?** Scroll canvas to the selected element
### Examples
@ -337,7 +291,7 @@ Add component to selection
### Parameters
* `el` **(Component | [HTMLElement][14] | [Array][12])** Component to select
* `el` **(Component | [HTMLElement][21] | [Array][19])** Component to select
### Examples
@ -353,7 +307,7 @@ Remove component from selection
### Parameters
* `el` **(Component | [HTMLElement][14] | [Array][12])** Component to select
* `el` **(Component | [HTMLElement][21] | [Array][19])** Component to select
### Examples
@ -369,7 +323,7 @@ Toggle component selection
### Parameters
* `el` **(Component | [HTMLElement][14] | [Array][12])** Component to select
* `el` **(Component | [HTMLElement][21] | [Array][19])** Component to select
### Examples
@ -401,7 +355,7 @@ change the canvas to the proper width
### Parameters
* `name` **[string][9]** Name of the device
* `name` **[string][16]** Name of the device
### Examples
@ -423,7 +377,7 @@ console.log(device);
// 'Tablet'
```
Returns **[string][9]** Device name
Returns **[string][16]** Device name
## runCommand
@ -431,8 +385,8 @@ Execute command
### Parameters
* `id` **[string][9]** Command ID
* `options` **[Object][10]** Custom options (optional, default `{}`)
* `id` **[string][16]** Command ID
* `options` **[Object][17]** Custom options (optional, default `{}`)
### Examples
@ -448,8 +402,8 @@ Stop the command if stop method was provided
### Parameters
* `id` **[string][9]** Command ID
* `options` **[Object][10]** Custom options (optional, default `{}`)
* `id` **[string][16]** Command ID
* `options` **[Object][17]** Custom options (optional, default `{}`)
### Examples
@ -465,9 +419,9 @@ Store data to the current storage
### Parameters
* `clb` **[Function][15]** Callback function
* `clb` **[Function][22]** Callback function
Returns **[Object][10]** Stored data
Returns **[Object][17]** Stored data
## storeData
@ -480,7 +434,7 @@ console.log(editor.storeData());
// { pages: [...], styles: [...], ... }
```
Returns **[Object][10]**
Returns **[Object][17]**
## load
@ -488,9 +442,9 @@ Load data from the current storage
### Parameters
* `clb` **[Function][15]** Callback function
* `clb` **[Function][22]** Callback function
Returns **[Object][10]** Stored data
Returns **[Object][17]** Stored data
## loadData
@ -498,7 +452,7 @@ Load data from the JSON data object
### Parameters
* `data` **[Object][10]** Data to load
* `data` **[Object][17]** Data to load
### Examples
@ -506,21 +460,21 @@ Load data from the JSON data object
editor.loadData({ pages: [...], styles: [...], ... })
```
Returns **[Object][10]** Loaded object
Returns **[Object][17]** Loaded object
## getContainer
Returns container element. The one which was indicated as 'container'
on init method
Returns **[HTMLElement][14]**
Returns **[HTMLElement][21]**
## getDirtyCount
Return the count of changes made to the content and not yet stored.
This count resets at any `store()`
Returns **[number][16]**
Returns **[number][23]**
## refresh
@ -533,9 +487,9 @@ refresh you'll get misleading position of tools
### Parameters
* `opts`
* `options` **[Object][10]?** Options
* `options` **[Object][17]?** Options
* `options.tools` **[Boolean][11]** Update the position of tools (eg. rich text editor, component highlighter, etc.) (optional, default `false`)
* `options.tools` **[Boolean][18]** Update the position of tools (eg. rich text editor, component highlighter, etc.) (optional, default `false`)
## setCustomRte
@ -543,7 +497,7 @@ Replace the built-in Rich Text Editor with a custom one.
### Parameters
* `obj` **[Object][10]** Custom RTE Interface
* `obj` **[Object][17]** Custom RTE Interface
### Examples
@ -583,7 +537,7 @@ custom parser, pass `null` as the argument
### Parameters
* `parser` **([Function][15] | null)** Parser function
* `parser` **([Function][22] | null)** Parser function
### Examples
@ -605,11 +559,11 @@ Returns **this**
## setDragMode
Change the global drag mode of components.
To get more about this feature read: [https://github.com/artf/grapesjs/issues/1936][17]
To get more about this feature read: [https://github.com/artf/grapesjs/issues/1936][24]
### Parameters
* `value` **[String][9]** Drag mode, options: 'absolute' | 'translate'
* `value` **[String][16]** Drag mode, options: 'absolute' | 'translate'
Returns **this**
@ -620,10 +574,10 @@ Trigger event log message
### Parameters
* `msg` **any** Message to log
* `opts` **[Object][10]** Custom options (optional, default `{}`)
* `opts` **[Object][17]** Custom options (optional, default `{}`)
* `opts.ns` **[String][9]** Namespace of the log (eg. to use in plugins) (optional, default `''`)
* `opts.level` **[String][9]** Level of the log, `debug`, `info`, `warning`, `error` (optional, default `'debug'`)
* `opts.ns` **[String][16]** Namespace of the log (eg. to use in plugins) (optional, default `''`)
* `opts.level` **[String][16]** Level of the log, `debug`, `info`, `warning`, `error` (optional, default `'debug'`)
### Examples
@ -645,11 +599,11 @@ Translate label
### Parameters
* `args` **...any**
* `key` **[String][9]** Label to translate
* `opts` **[Object][10]?** Options for the translation
* `key` **[String][16]** Label to translate
* `opts` **[Object][17]?** Options for the translation
* `opts.params` **[Object][10]?** Params for the translation
* `opts.noWarn` **[Boolean][11]?** Avoid warnings in case of missing resources
* `opts.params` **[Object][17]?** Params for the translation
* `opts.noWarn` **[Boolean][18]?** Avoid warnings in case of missing resources
### Examples
@ -661,7 +615,7 @@ editor.t('msg2', { params: { test: 'hello' } });
editor.t('msg2', { params: { test: 'hello' }, l: 'it' });
```
Returns **[String][9]**
Returns **[String][16]**
## on
@ -669,8 +623,8 @@ Attach event
### Parameters
* `event` **[string][9]** Event name
* `callback` **[Function][15]** Callback function
* `event` **[string][16]** Event name
* `callback` **[Function][22]** Callback function
Returns **this**
@ -680,8 +634,8 @@ Attach event and detach it after the first run
### Parameters
* `event` **[string][9]** Event name
* `callback` **[Function][15]** Callback function
* `event` **[string][16]** Event name
* `callback` **[Function][22]** Callback function
Returns **this**
@ -691,8 +645,8 @@ Detach event
### Parameters
* `event` **[string][9]** Event name
* `callback` **[Function][15]** Callback function
* `event` **[string][16]** Event name
* `callback` **[Function][22]** Callback function
Returns **this**
@ -702,7 +656,7 @@ Trigger event
### Parameters
* `event` **[string][9]** Event to trigger
* `event` **[string][16]** Event to trigger
Returns **this**
@ -714,7 +668,7 @@ Destroy the editor
Render editor
Returns **[HTMLElement][14]**
Returns **[HTMLElement][21]**
## onReady
@ -723,7 +677,7 @@ The callback will be executed immediately if the method is called on the already
### Parameters
* `clb` **[Function][15]** Callback to trigger
* `clb` **[Function][22]** Callback to trigger
### Examples
@ -739,8 +693,8 @@ Print safe HTML by using ES6 tagged template strings.
### Parameters
* `literals` **[Array][12]<[String][9]>**
* `substs` **[Array][12]<[String][9]>**
* `literals` **[Array][19]<[String][16]>**
* `substs` **[Array][19]<[String][16]>**
### Examples
@ -751,38 +705,52 @@ const safeStr = '<b>Hello</b>';
const strHtml = editor.html`Escaped ${unsafeStr}, unescaped $${safeStr}`;
```
Returns **[String][9]**
Returns **[String][16]**
[1]: https://github.com/artf/grapesjs/blob/master/src/editor/config/config.js
[2]: /api/selector_manager.html
[2]: /api/components.html
[3]: /api/keymaps.html
[4]: /api/style_manager.html
[5]: /api/storage_manager.html
[6]: /api/canvas.html
[7]: /api/rich_text_editor.html
[8]: /api/commands.html
[9]: /api/selector_manager.html
[3]: /api/block_manager.html
[10]: /api/block_manager.html
[4]: /api/assets.html
[11]: /api/assets.html
[5]: /api/modal_dialog.html
[12]: /api/modal_dialog.html
[6]: /api/device_manager.html
[13]: /api/device_manager.html
[7]: /api/parser.html
[14]: /api/parser.html
[8]: /api/pages.html
[15]: /api/pages.html
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[12]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[13]: em#setComponents
[20]: em#setComponents
[14]: https://developer.mozilla.org/docs/Web/HTML/Element
[21]: https://developer.mozilla.org/docs/Web/HTML/Element
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[22]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[23]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[17]: https://github.com/artf/grapesjs/issues/1936
[24]: https://github.com/artf/grapesjs/issues/1936

16
docs/api/keymaps.md

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

77
docs/api/layer.md

@ -0,0 +1,77 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## getId
Get layer id.
Returns **[String][1]**
## getIndex
Get layer index.
Returns **[Number][2]**
## getValues
Get layer values.
### Parameters
* `opts` **[Object][3]** Options (optional, default `{}`)
* `opts.camelCase` **[Boolean][4]?** Return property names in camelCase.
Returns **[Object][3]**
## getLabel
Get layer label.
Returns **[String][1]**
## isSelected
Check if the layer is selected.
Returns **[Boolean][4]**
## select
Select the layer.
## remove
Remove the layer.
## move
Move layer to a new index.
### Parameters
* `index` **[Number][2]** New index
## getStylePreview
Get style object for the preview.
### Parameters
* `opts` **[Object][3]** Options. Same of `PropertyStack.getStyleFromLayer` (optional, default `{}`)
Returns **[Object][3]** Style object
## hasPreview
Check if the property has the preview enabled for this layer.
Returns **[Boolean][4]**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean

164
docs/api/property.md

@ -0,0 +1,164 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## Property
### Properties
* `id` **[String][1]** Property id, eg. `my-property-id`.
* `property` **[String][1]** Related CSS property name, eg. `text-align`.
* `default` **[String][1]** Defaul value of the property.
* `label` **[String][1]** Label to use in UI, eg. `Text Align`.
* `onChange` **[Function][2]?** Change callback.
```js
onChange: ({ property, from, to }) => {
console.log(`Changed property`, property.getName(), { from, to });
}
```
### getId
Get property id.
Returns **[String][1]**
### getType
Get the property type.
The type of the property is defined on property creation and based on its value the proper Property class is assigned.
The default type is `base`.
Returns **[String][1]**
### getName
Get name (the CSS property name).
Returns **[String][1]**
### getLabel
Get property label.
#### Parameters
* `opts` **[Object][3]** Options (optional, default `{}`)
* `opts.locale` **[Boolean][4]** Use the locale string from i18n module (optional, default `true`)
Returns **[String][1]**
### getValue
Get property value.
#### Parameters
* `opts` **[Object][3]** Options (optional, default `{}`)
* `opts.noDefault` **[Boolean][4]** Avoid returning the default value (optional, default `false`)
Returns **[String][1]**
### hasValue
Check if the property has value.
#### Parameters
* `opts` **[Object][3]** Options (optional, default `{}`)
* `opts.noParent` **[Boolean][4]** Ignore the value if it comes from the parent target. (optional, default `false`)
Returns **[Boolean][4]**
### hasValueParent
Indicates if the current value is coming from a parent target (eg. another CSSRule).
Returns **[Boolean][4]**
### getStyle
Get the CSS style object of the property.
#### Parameters
* `opts` **[Object][3]** Options (optional, default `{}`)
* `opts.camelCase` **[Boolean][4]?** Return property name in camelCase.
#### Examples
```javascript
// In case the property is `color` with a value of `red`.
console.log(property.getStyle());
// { color: 'red' };
```
Returns **[Object][3]**
### getDefaultValue
Get the default value.
Returns **[string][1]**
### upValue
Update the value.
The change is also propagated to the selected targets (eg. CSS rule).
#### Parameters
* `value` **[String][1]** New value
* `opts` **[Object][3]** Options (optional, default `{}`)
* `opts.partial` **[Boolean][4]** If `true` the update on targets won't be considered complete (not stored in UndoManager) (optional, default `false`)
* `opts.noTarget` **[Boolean][4]** If `true` the change won't be propagated to selected targets. (optional, default `false`)
### isVisible
Check if the property is visible
Returns **[Boolean][4]**
### clear
Clear the value.
The change is also propagated to the selected targets (eg. the css property is cleared).
#### Parameters
* `opts` **[Object][3]** Options (optional, default `{}`)
* `opts.noTarget` **[Boolean][4]** If `true` the change won't be propagated to selected targets. (optional, default `false`)
### canClear
Indicates if the current value comes directly from the selected target and so can be cleared.
Returns **[Boolean][4]**
### getParent
If the current property is a sub-property, this will return the parent Property.
Returns **([[Property][5]] | null)**
### isFull
Indicates if the property is full-width in UI.
Returns **[Boolean][4]**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[5]: #property

113
docs/api/property_composite.md

@ -0,0 +1,113 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## PropertyComposite
**Extends Property**
[Property]: property.html
### Properties
* `properties` **[Array][1]<[Object][2]>** Array of sub properties, eg. `[{ type: 'number', property: 'margin-top' }, ...]`
* `detached` **[Boolean][3]?** Indicate if the final CSS property is splitted (detached: `margin-top: X; margin-right: Y; ...`) or combined (not detached: `margin: X Y ...;`)
* `separator` **([String][4] | [RegExp][5])?** Value used to split property values, default `" "`.
* `join` **[String][4]?** Value used to join property values, default `" "`.
* `fromStyle` **[Function][6]?** Custom logic for getting property values from the target style object.
```js
fromStyle: (style) => {
const margins = parseMarginShorthand(style.margin);
return {
'margin-top': margins.top,
// ...
};
}
```
* `toStyle` **[Function][6]?** Custom logic for creating the CSS style object to apply on selected targets.
```js
toStyle: (values) => {
const top = values['margin-top'] || 0;
const right = values['margin-right'] || 0;
// ...
return {
margin: `${top} ${right} ...`,
};
}
```
### getProperties
Get properties.
Returns **[Array][1]<[Property]>**
### getProperty
Get property by id.
#### Parameters
* `id` **[String][4]** Property id.
Returns **([Property] | null)**
### getPropertyAt
Get property at index.
#### Parameters
* `index` **[Number][7]**
Returns **([Property] | null)**
### isDetached
Check if the property is detached.
Returns **[Boolean][3]**
### getValues
Get current values of properties.
#### Parameters
* `opts` **[Object][2]** Options (optional, default `{}`)
* `opts.byName` **[Boolean][3]** Use property names as a key instead of the id. (optional, default `false`)
#### Examples
```javascript
// In case the property is `margin` with sub properties like `margin-top`, `margin-right`, etc.
console.log(property.getValues());
// { 'margin-top': '10px', 'margin-right': '20px', ... };
```
Returns **[Object][2]**
### getSeparator
Get property separator.
Returns **[RegExp][5]**
### getJoin
Get the join value.
Returns **[String][4]**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp
[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number

66
docs/api/property_number.md

@ -0,0 +1,66 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## PropertyNumber
**Extends Property**
### Properties
* `units` **[Array][1]<[String][2]>** Array of units, eg. `['px', '%']`
* `min` **[Number][3]** Minimum value.
* `max` **[Number][3]** Maximum value.
* `step` **[Number][3]** Step value.
### getUnits
Get property units.
Returns **[Array][1]<[String][2]>**
### getUnit
Get property unit value.
Returns **[String][2]**
### getMin
Get min value.
Returns **[Number][3]**
### getMax
Get max value.
Returns **[Number][3]**
### getStep
Get step value.
Returns **[Number][3]**
### upUnit
Update property unit value.
The change is also propagated to the selected targets.
#### Parameters
* `unit` **[String][2]** New unit value
* `opts` **[Object][4]** Options (optional, default `{}`)
* `opts.noTarget` **[Boolean][5]** If `true` the change won't be propagated to selected targets. (optional, default `false`)
Returns **[String][2]**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean

78
docs/api/property_select.md

@ -0,0 +1,78 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## PropertySelect
**Extends Property**
### Properties
* `options` **[Array][1]<[Object][2]>** Array of option definitions.
```js
options: [
{ id: '100', label: 'Set 100' },
{ id: '200', label: 'Set 200' },
]
```
### getOptions
Get available options.
Returns **[Array][1]<[Object][2]>** Array of options
### getOption
Get current selected option or by id.
#### Parameters
* `id` **[String][3]?** Option id.
Returns **([Object][2] | null)**
### setOptions
Update options.
#### Parameters
* `value` **[Array][1]<[Object][2]>** New array of options, eg. `[{ id: 'val-1', label: 'Value 1' }]` (optional, default `[]`)
### addOption
Add new option.
#### Parameters
* `value` **[Object][2]** Option object, eg. `{ id: 'val-1', label: 'Value 1' }`
### getOptionId
Get the option id from the option object.
#### Parameters
* `option` **[Object][2]** Option object
Returns **[String][3]** Option id
### getOptionLabel
Get option label.
#### Parameters
* `id` **([String][3] | [Object][2])** Option id or the option object
* `opts` **[Object][2]** Options (optional, default `{}`)
* `opts.locale` **[Boolean][4]** Use the locale string from i18n module (optional, default `true`)
Returns **[String][3]** Option label
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean

214
docs/api/property_stack.md

@ -0,0 +1,214 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## PropertyStack
**Extends PropertyComposite**
[Layer]: layer.html
### Properties
* `preview` **[Boolean][1]?** Indicate if the layer should display a preview.
* `layerSeparator` **([String][2] | [RegExp][3])?** The separator used to split layer values.
* `layerJoin` **[String][2]?** Value used to join layer values.
* `layerLabel` **[Function][4]?** Custom logic for creating layer labels.
```js
layerLabel: (layer) => {
const values = layer.getValues();
return `A: ${values['prop-a']} B: ${values['prop-b']}`;
}
```
### getLayers
Get all available layers.
Returns **[Array][5]<[Layer]>**
### getLayer
Get layer by index.
#### Parameters
* `index` **[Number][6]** Layer index position. (optional, default `0`)
#### Examples
```javascript
// Get the first layer
const layerFirst = property.getLayer(0);
// Get the last layer
const layers = this.getLayers();
const layerLast = property.getLayer(layers.length - 1);
```
Returns **([Layer] | null)**
### getSelectedLayer
Get selected layer.
Returns **([Layer] | null)**
### selectLayer
Select layer.
Without a selected layer any update made on inner properties has no effect.
#### Parameters
* `layer` **[Layer]** Layer to select
#### Examples
```javascript
const layer = property.getLayer(0);
property.selectLayer(layer);
```
### selectLayerAt
Select layer by index.
#### Parameters
* `index` **[Number][6]** Index of the layer to select. (optional, default `0`)
#### Examples
```javascript
property.selectLayerAt(1);
```
### moveLayer
Move layer by index.
#### Parameters
* `layer` **[Layer]** Layer to move.
* `index` **[Number][6]** New layer index. (optional, default `0`)
#### Examples
```javascript
const layer = property.getLayer(1);
property.moveLayer(layer, 0);
```
### addLayer
Add new layer to the stack.
#### Parameters
* `props` **[Object][7]** Custom property values to use in a new layer. (optional, default `{}`)
* `opts` **[Object][7]** Options (optional, default `{}`)
* `opts.at` **[Number][6]?** Position index (by default the layer will be appended at the end).
#### Examples
```javascript
// Add new layer at the beginning of the stack with custom values
property.addLayer({ 'sub-prop1': 'value1', 'sub-prop2': 'value2' }, { at: 0 });
```
Returns **[Layer]** Added layer.
### removeLayer
Remove layer.
#### Parameters
* `layer` **[Layer]** Layer to remove.
#### Examples
```javascript
const layer = property.getLayer(0);
property.removeLayer(layer);
```
Returns **[Layer]** Removed layer
### removeLayerAt
Remove layer by index.
#### Parameters
* `index` **[Number][6]** Index of the layer to remove (optional, default `0`)
#### Examples
```javascript
property.removeLayerAt(0);
```
Returns **([Layer] | null)** Removed layer
### getLayerLabel
Get the layer label. The label can be customized with the `layerLabel` property.
#### Parameters
* `layer` **[Layer]**
#### Examples
```javascript
const layer = this.getLayer(1);
const label = this.getLayerLabel(layer);
```
Returns **[String][2]**
### getStyleFromLayer
Get style object from the layer.
#### Parameters
* `layer` **[Layer]**
* `opts` **[Object][7]** Options (optional, default `{}`)
* `opts.camelCase` **[Boolean][1]?** Return property names in camelCase.
* `opts.number` **[Object][7]?** Limit the result of the number types, eg. `number: { min: -3, max: 3 }`
Returns **[Object][7]** Style object
### getStylePreview
Get preview style object from the layer.
If the property has `preview: false` the returned object will be empty.
#### Parameters
* `layer` **[Layer]**
* `opts` **[Object][7]** Options. Same of `getStyleFromLayer` (optional, default `{}`)
Returns **[Object][7]** Style object
### getLayerSeparator
Get layer separator.
Returns **[RegExp][3]**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RegExp
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

14
docs/api/rich_text_editor.md

@ -15,12 +15,24 @@ const editor = grapesjs.init({
})
```
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
```js
// Listen to events
editor.on('rte:enable', () => { ... });
// Use the API
const rte = editor.RichTextEditor;
rte.add(...);
```
## Available Events
* `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
* `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument
## Methods
* [add][3]
* [get][4]
* [getAll][5]

17
docs/api/sector.md

@ -4,12 +4,14 @@
[Property]: property.html
### Properties
* `id` **[String][1]** Sector id, eg. `typography`
* `name` **[String][1]** Sector name, eg. `Typography`
* `open` **[Boolean][2]?** Indicates the open state.
* `properties` **[Array][3]<[Object][4]>?** Indicate an array of Property defintions[Property]: property.html
* `properties` **[Array][3]<[Object][4]>?** Indicate an array of Property defintions.
### getId
@ -45,10 +47,23 @@ Update Sector open state
* `value` **[Boolean][2]**
### isVisible
Check if the sector is visible
Returns **[Boolean][2]**
### getProperties
Get sector properties.
#### Parameters
* `opts` **[Object][4]** Options (optional, default `{}`)
* `opts.withValue` **[Boolean][2]** Get only properties with value (optional, default `false`)
* `opts.withParentValue` **[Boolean][2]** Get only properties with parent value (optional, default `false`)
Returns **[Array][3]<[Property]>**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String

23
docs/api/storage_manager.md

@ -12,12 +12,33 @@ const editor = grapesjs.init({
})
```
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
```js
// Listen to events
editor.on('storage:start', () => { ... });
// Use the API
const storageManager = editor.StorageManager;
storageManager.add(...);
```
## Available Events
* `storage:start` - Before the storage request is started
* `storage:start:store` - Before the store request. The object to store is passed as an argumnet (which you can edit)
* `storage:start:load` - Before the load request. Items to load are passed as an argumnet (which you can edit)
* `storage:load` - Triggered when something was loaded from the storage, loaded object passed as an argumnet
* `storage:store` - Triggered when something is stored to the storage, stored object passed as an argumnet
* `storage:end` - After the storage request is ended
* `storage:end:store` - After the store request
* `storage:end:load` - After the load request
* `storage:error` - On any error on storage request, passes the error as an argument
* `storage:error:store` - Error on store request, passes the error as an argument
* `storage:error:load` - Error on load request, passes the error as an argument
## Methods
* [getConfig][2]
* [isAutosave][3]
* [setAutosave][4]

370
docs/api/style_manager.md

@ -13,26 +13,55 @@ const editor = grapesjs.init({
})
```
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
```js
// Listen to events
editor.on('style:sector:add', (sector) => { ... });
// Use the API
const styleManager = editor.StyleManager;
styleManager.addSector(...);
```
## Available Events
* `style:sector:add` - Sector added. The [Sector] is passed as an argument to the callback.
* `style:sector:remove` - Sector removed. The [Sector] is passed as an argument to the callback.
* `style:sector:update` - Sector updated. The [Sector] and the object containing changes are passed as arguments to the callback.
* `style:property:add` - Property added. The [Property] is passed as an argument to the callback.
* `style:property:remove` - Property removed. The [Property] is passed as an argument to the callback.
* `style:property:update` - Property updated. The [Property] and the object containing changes are passed as arguments to the callback.
* `style:target` - Target selection changed. The target (or `null` in case the target is deselected) is passed as an argument to the callback.
<!--
* `styleManager:update:target` - The target (Component or CSSRule) is changed
* `styleManager:change` - Triggered on style property change from new selected component, the view of the property is passed as an argument to the callback
* `styleManager:change:{propertyName}` - As above but for a specific style property
-->
## Methods
* [getConfig][2]
* [addSector][3]
* [getSector][4]
* [removeSector][5]
* [getSectors][6]
* [getSectors][5]
* [removeSector][6]
* [addProperty][7]
* [getProperty][8]
* [removeProperty][9]
* [getProperties][10]
* [getModelToStyle][11]
* [addType][12]
* [getType][13]
* [getTypes][14]
* [createType][15]
* [getProperties][9]
* [removeProperty][10]
* [select][11]
* [getSelected][12]
* [getSelectedAll][13]
* [getSelectedParents][14]
* [addStyleTargets][15]
* [getBuiltIn][16]
* [getBuiltInAll][17]
* [addBuiltIn][18]
* [addType][19]
* [getType][20]
* [getTypes][21]
[Sector]: sector.html
@ -40,26 +69,25 @@ const styleManager = editor.StyleManager;
[Component]: component.html
[Property]: property.html
## getConfig
Get configuration object
Returns **[Object][16]**
Returns **[Object][22]**
## addSector
Add new sector to the collection. If the sector with the same id already exists,
that one will be returned.
Add new sector. If the sector with the same id already exists, that one will be returned.
### Parameters
* `id` **[string][17]** Sector id
* `sector` **[Object][16]** Object representing sector
* `id` **[String][23]** Sector id
* `sector` **[Object][22]** Sector definition. Check the [available properties][24]
* `options` **[Object][22]** Options (optional, default `{}`)
* `sector.name` **[string][17]** Sector's label (optional, default `''`)
* `sector.open` **[Boolean][18]** Indicates if the sector should be opened (optional, default `true`)
* `sector.properties` **[Array][19]<[Object][16]>** Array of properties (optional, default `[]`)
* `options` **[Object][16]** Options (optional, default `{}`)
* `options.at` **[Number][25]?** Position index (by default, will be appended at the end).
### Examples
@ -69,18 +97,18 @@ const sector = styleManager.addSector('mySector',{
open: true,
properties: [{ name: 'My property'}]
}, { at: 0 });
// With `at: 0` we place the new sector at the beginning of the collection
// With `at: 0` we place the new sector at the beginning of the list
```
Returns **[Sector]** Added Sector
## getSector
Get sector by id
Get sector by id.
### Parameters
* `id` **[string][17]** Sector id
* `id` **[String][23]** Sector id
* `opts` (optional, default `{}`)
### Examples
@ -91,111 +119,110 @@ const sector = styleManager.getSector('mySector');
Returns **([Sector] | null)**
## removeSector
## getSectors
Remove a sector by id
Get all sectors.
### Parameters
* `id` **[string][17]** Sector id
* `opts` **[Object][22]** Options (optional, default `{}`)
* `opts.visible` **[Boolean][26]?** Returns only visible sectors
### Examples
```javascript
const removed = styleManager.removeSector('mySector');
const sectors = styleManager.getSectors();
```
Returns **[Sector]** Removed sector
Returns **[Array][27]<[Sector]>**
## getSectors
## removeSector
Get all sectors
Remove sector by id.
### Parameters
* `opts` (optional, default `{}`)
* `id` **[String][23]** Sector id
### Examples
```javascript
const sectors = styleManager.getSectors();
const removed = styleManager.removeSector('mySector');
```
Returns **Collection<[Sector]>** Collection of sectors
Returns **[Sector]** Removed sector
## addProperty
Add property to the sector identified by id
Add new property to the sector.
### Parameters
* `sectorId` **[string][17]** Sector id
* `property` **[Object][16]** Property object
* `property.name` **[string][17]** Name of the property (optional, default `''`)
* `property.property` **[string][17]** CSS property, eg. `min-height` (optional, default `''`)
* `property.type` **[string][17]** Type of the property: integer | radio | select | color | file | composite | stack (optional, default `''`)
* `property.units` **[Array][19]<[string][17]>** Unit of measure available, eg. \['px','%','em']. Only for integer type (optional, default `[]`)
* `property.unit` **[string][17]** Default selected unit from `units`. Only for integer type (optional, default `''`)
* `property.min` **[number][20]** Min possible value. Only for integer type (optional, default `null`)
* `property.max` **[number][20]** Max possible value. Only for integer type (optional, default `null`)
* `property.defaults` **[string][17]** Default value (optional, default `''`)
* `property.info` **[string][17]** Some description (optional, default `''`)
* `property.icon` **[string][17]** Class name. If exists no text will be displayed (optional, default `''`)
* `property.preview` **[Boolean][18]** Show layers preview. Only for stack type (optional, default `false`)
* `property.functionName` **[string][17]** Indicates if value need to be wrapped in some function, for istance `transform: rotate(90deg)` (optional, default `''`)
* `property.properties` **[Array][19]<[Object][16]>** Nested properties for composite and stack type (optional, default `[]`)
* `property.layers` **[Array][19]<[Object][16]>** Layers for stack properties (optional, default `[]`)
* `property.list` **[Array][19]<[Object][16]>** List of possible options for radio and select types (optional, default `[]`)
* `opts` (optional, default `{}`)
* `options` **[Object][16]** Options (optional, default `{}`)
* `sectorId` **[String][23]** Sector id.
* `property` **[Object][22]** Property definition. Check the [base available properties][28] + others based on the `type` of your property.
* `opts` **[Object][22]** Options (optional, default `{}`)
* `opts.at` **[Number][25]?** Position index (by default, will be appended at the end).
### Examples
```javascript
var property = styleManager.addProperty('mySector',{
name: 'Minimum height',
const property = styleManager.addProperty('mySector', {
label: 'Minimum height',
property: 'min-height',
type: 'select',
defaults: '100px',
list: [{
value: '100px',
name: '100',
},{
value: '200px',
name: '200',
}],
default: '100px',
options: [
{ id: '100px', label: '100' },
{ id: '200px', label: '200' },
],
}, { at: 0 });
// With `at: 0` we place the new property at the beginning of the collection
```
Returns **(Property | null)** Added Property or `null` in case sector doesn't exist
Returns **([Property] | null)** Added property or `null` in case the sector doesn't exist.
## getProperty
Get property by its CSS name and sector id
Get the property.
### Parameters
* `sectorId` **[string][17]** Sector id
* `name` **[string][17]** CSS property name (or id), eg. 'min-height'
* `sectorId` **[String][23]** Sector id.
* `id` **[String][23]** Property id.
### Examples
```javascript
var property = styleManager.getProperty('mySector','min-height');
const property = styleManager.getProperty('mySector', 'min-height');
```
Returns **(Property | null)**
Returns **([Property] | null)**
## getProperties
Get all properties of the sector.
### Parameters
* `sectorId` **[String][23]** Sector id.
### Examples
```javascript
const properties = styleManager.getProperties('mySector');
```
Returns **(Collection<[Property]> | null)** Collection of properties
## removeProperty
Remove a property from the sector
Remove the property.
### Parameters
* `sectorId` **[string][17]** Sector id
* `name` **[string][17]** CSS property name, eg. 'min-height'
* `sectorId` **[String][23]** Sector id.
* `id` **[String][23]** Property id.
### Examples
@ -203,37 +230,109 @@ Remove a property from the sector
const property = styleManager.removeProperty('mySector', 'min-height');
```
Returns **Property** Removed property
Returns **([Property] | null)** Removed property
## getProperties
## select
Select new target.
The target could be a Component, CSSRule, or a CSS selector string.
### Parameters
* `target` **([Component] | [CSSRule] | [String][23])**
* `opts` (optional, default `{}`)
### Examples
```javascript
// Select the first button in the current page
const wrapperCmp = editor.Pages.getSelected().getMainComponent();
const btnCmp = wrapperCmp.find('button')[0];
btnCmp && styleManager.select(btnCmp);
// Set as a target the CSS selector
styleManager.select('.btn > span');
```
Returns **[Array][27]<([Component] | [CSSRule])>** Array containing selected Components or CSSRules
Get properties of the sector
## getSelected
Get the last selected target.
By default, the Style Manager shows styles of the last selected target.
Returns **([Component] | [CSSRule] | null)**
## getSelectedAll
Get the array of selected targets.
Returns **[Array][27]<([Component] | [CSSRule])>**
## getSelectedParents
Get parent rules of the last selected target.
Returns **[Array][27]<[CSSRule]>**
## addStyleTargets
Update selected targets with a custom style.
### Parameters
* `sectorId` **[string][17]** Sector id
* `style` **[Object][22]** Style object
* `opts` **[Object][22]** Options (optional, default `{}`)
### Examples
```javascript
var properties = styleManager.getProperties('mySector');
styleManager.addStyleTargets({ color: 'red' });
```
Returns **Properties** Collection of properties
## getBuiltIn
Return built-in property definition
### Parameters
* `prop` **[String][23]** Property name.
### Examples
```javascript
const widthPropDefinition = styleManager.getBuiltIn('width');
```
Returns **([Object][22] | null)** Property definition.
## getBuiltInAll
Get all the available built-in property definitions.
Returns **[Object][22]**
## getModelToStyle
## addBuiltIn
Get what to style inside Style Manager. If you select the component
without classes the entity is the Component itself and all changes will
go inside its 'style' property. Otherwise, if the selected component has
one or more classes, the function will return the corresponding CSS Rule
Add built-in property definition.
If the property exists already, it will extend it.
### Parameters
* `model` **Model**
* `options` (optional, default `{}`)
* `prop` **[String][23]** Property name.
* `definition` **[Object][22]** Property definition.
Returns **Model**
### Examples
```javascript
const sector = styleManager.addBuiltIn('new-property', {
type: 'select',
default: 'value1',
options: [{ id: 'value1', label: 'Some label' }, ...],
})
```
Returns **[Object][22]** Added property definition.
## addType
@ -241,37 +340,33 @@ Add new property type
### Parameters
* `id` **[string][17]** Type ID
* `definition` **[Object][16]** Definition of the type. Each definition contains
`model` (business logic), `view` (presentation logic)
and `isType` function which recognize the type of the
passed entity
* `id` **[string][23]** Type ID
* `definition` **[Object][22]** Definition of the type.
### Examples
```javascript
styleManager.addType('my-custom-prop', {
// Create UI
create({ props, change }) {
const el = document.createElement('div');
el.innerHTML = '<input type="range" class="my-input" min="10" max="50"/>';
const inputEl = el.querySelector('.my-input');
inputEl.addEventListener('change', event => change({ event })); // change will trigger the emit
inputEl.addEventListener('input', event => change({ event, complete: false }));
inputEl.addEventListener('change', event => change({ event }));
inputEl.addEventListener('input', event => change({ event, partial: true }));
return el;
},
emit({ props, updateStyle }, { event, complete }) {
// Propagate UI changes up to the targets
emit({ props, updateStyle }, { event, partial }) {
const { value } = event.target;
const valueRes = value + 'px';
// Pass a string value for the exact CSS property or an object containing multiple properties
// eg. updateStyle({ [props.property]: valueRes, color: 'red' });
updateStyle(valueRes, { complete });
updateStyle(`${value}px`, { partial });
},
// Update UI (eg. when the target is changed)
update({ value, el }) {
el.querySelector('.my-input').value = parseInt(value, 10);
},
destroy() {
// In order to prevent memory leaks, use this method to clean, eventually, created instances, global event listeners, etc.
}
// Clean the memory from side effects if necessary (eg. global event listeners, etc.)
destroy() {}
})
```
@ -281,77 +376,68 @@ Get type
### Parameters
* `id` **[string][17]** Type ID
* `id` **[string][23]** Type ID
Returns **[Object][16]** Type definition
Returns **[Object][22]** Type definition
## getTypes
Get all types
Returns **[Array][19]**
Returns **[Array][27]**
## createType
[1]: https://github.com/artf/grapesjs/blob/master/src/style_manager/config/config.js
Create new property from type
[2]: #getconfig
### Parameters
[3]: #addsector
* `id` **[string][17]** Type ID
* `options` **[Object][16]** Options (optional, default `{}`)
[4]: #getsector
* `options.model` **[Object][16]** Custom model object (optional, default `{}`)
* `options.view` **[Object][16]** Custom view object (optional, default `{}`)
[5]: #getsectors
### Examples
[6]: #removesector
```javascript
const propView = styleManager.createType('integer', {
model: {units: ['px', 'rem']}
});
propView.render();
propView.model.on('change:value', ...);
someContainer.appendChild(propView.el);
```
[7]: #addproperty
Returns **PropertyView**
[8]: #getproperty
[1]: https://github.com/artf/grapesjs/blob/master/src/style_manager/config/config.js
[9]: #getproperties
[2]: #getconfig
[10]: #removeproperty
[3]: #addsector
[11]: #select
[4]: #getsector
[12]: #getselected
[5]: #removesector
[13]: #getselectedall
[6]: #getsectors
[14]: #getselectedparents
[7]: #addproperty
[15]: #addstyletargets
[8]: #getproperty
[16]: #getbuiltin
[9]: #removeproperty
[17]: #getbuiltinall
[10]: #getproperties
[18]: #addbuiltin
[11]: #getmodeltostyle
[19]: #addtype
[12]: #addtype
[20]: #gettype
[13]: #gettype
[21]: #gettypes
[14]: #gettypes
[22]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[15]: #createtype
[23]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[24]: sector.html#properties
[17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[25]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[26]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[27]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[28]: property.html#properties

2
docs/modules/Blocks.md

@ -135,7 +135,7 @@ Using HTML strings as `content` is not wrong, in some cases you don't need the f
```
In such a case, all rendered elements will be converted to the best suited default component (eg. `.el-Y` elements will be treated like `text` components). The user will be able to style and drag them with no particular restrictions.
Thanks to Components' [isComponet](http://localhost:8081/docs/modules/Components.html#iscomponent) feature (executed post parsing), you're still able to bind your rendered elements to components and enforce an extra logic. Here an example how you would enforce all `.el-Y` elements to be placed only inside `.el-X` one, without touching any part of the original HTML used in the `content`.
Thanks to Components' [isComponet](Components.html#iscomponent) feature (executed post parsing), you're still able to bind your rendered elements to components and enforce an extra logic. Here an example how you would enforce all `.el-Y` elements to be placed only inside `.el-X` one, without touching any part of the original HTML used in the `content`.
```js
// Your component

2
docs/modules/Commands.md

@ -264,7 +264,7 @@ The Commands module offers also a set of events that you can use to intercept th
### Intercept run and stop
By using our previosly created `my-command-modal` command let's see which events we can listen to
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', () => {

2
docs/modules/Components.md

@ -872,7 +872,7 @@ If you want to know how to create Components with javascript attached (eg. count
If you're importing big chunks of HTML string into the editor (eg. defined via Blocks) JSX might be a great compromise between perfomances and code readibility as it allows you to skip the parsing and the component recognition steps by keeping the HTML syntax.
By default, GrapesJS understands objects generated from React JSX preset, so, if you're working in the React app probably you're already using JSX and you don't need to do anything else, your environment is already configured to parse JSX in javascript files.
So, intead of writing this:
So, instead of writing this:
```js
// I'm adding a string, so the parsing and the component recognition steps will be executed
editor.addComponents(`<div>

871
docs/modules/Style-manager.md

@ -4,28 +4,867 @@ title: Style Manager
# Style Manager
Coming soon
<p align="center"><img :src="$withBase('/style-manager.jpg')" alt="GrapesJS - Style Manager"/></p>
[[toc]]
The Style Manager module is responsible to show and update style properties relative to your [Components]. In this guide, you will see how to setup and take full advantage of the built-in Style Manager UI in GrapesJS.
The default UI is a lightweight component with built-in properties, but as you'll see next in this guide, it's easy to extend with your own elements or even create the Style Manager UI from scratch by using the [Style Manager API].
::: warning
To get a better understanding of the content in this guide, we recommend reading [Components] first
:::
::: warning
This guide is referring to GrapesJS v0.18.1 or higher
:::
## Built-in properties
[[toc]]
Here you can find all the available built-in properties that you can use inside Style Manager via `buildProps`:
## Configuration
`float`, `position`, `text-align`, `display`, `font-family`, `font-weight`, `border`, `border-style`, `border-color`, `border-width`, `box-shadow`, `background-repeat`, `background-position`, `background-attachment`, `background-size`, `transition`, `transition-duration`, `transition-property`, `transition-timing-function`, `top`, `right`, `bottom`, `left`, `margin`, `margin-top`, `margin-right`, `margin-bottom`, `margin-left`, `padding`, `padding-top`, `padding-right`, `padding-bottom`, `padding-left`, `width`, `height`, `min-width`, `min-height`, `max-width`, `max-height`, `font-size`, `letter-spacing`, `line-height`, `text-shadow`, `border-radius`, `border-top-left-radius`, `border-top-right-radius`, `border-bottom-left-radius`, `border-bottom-right-radius`, `perspective`, `transform`, `transform-rotate-x`, `transform-rotate-y`, `transform-rotate-z`, `transform-scale-x`, `transform-scale-y`, `transform-scale-z`, `color`, `background-color`, `background`, `background-image`, `cursor`, `flex-direction`, `flex-wrap`, `justify-content`, `align-items`, `align-content`, `order`, `flex-basis`, `flex-grow`, `flex-shrink`, `align-self`, `overflow`, `overflow-x`, `overflow-y`
To change the default configurations you have to pass the `styleManager` property with the main configuration object.
Example usage:
```js
...
styleManager : {
sectors: [{
name: 'Dimension',
buildProps: ['width', 'min-height']
},{
name: 'Extra',
buildProps: ['background-color', 'box-shadow']
}]
const editor = grapesjs.init({
...
styleManager: {
sectors: [...],
...
}
...
});
```
Check the full list of available options here: [Style Manager Config](https://github.com/artf/grapesjs/blob/master/src/style_manager/config/config.js)
## Initialization
The Style Manager module is organized in sectors where each sector contains a list of properties to display. The default Style Manager configuration contains already a list of default common property styles and you can use them by simply skipping the `styleManagerConfig.sectors` option.
```js
grapesjs.init({
...
styleManager: {
// With no defined sectors, the default list will be loaded
// sectors: [...],
...
},
});
```
::: danger
It makes sense to show the Style Manager UI only when you have at least one component selected, so by default the Style Manager is hidden if there are no selected components.
:::
### Sector defintions
Each sector is identified by its `name` and a list of `properties` to display. You can also specify the `id` in order to access the sector via API (if not indicated it will be generated from the `name`) and the default `open` state.
```js
grapesjs.init({
// ...
styleManager: {
sectors: [
{
name: 'First sector',
properties: [],
// id: 'first-sector', // Id generated from the name
// open: true, // The sector is open by default
},
{
open: false, // render it closed by default
name: 'Second sector',
properties: [],
},
],
},
});
```
### Property defintions
Once you have defined your sector you can start adding property definitions inside `properties`. Each property has a common set of options (`label`, `default`, etc.) and others specific by their `type`.
Let's see below a simple definition for controlling the padding style.
```js
sectors: [
{
name: 'First sector',
properties: [
{
// Default options
// id: 'padding', // The id of the property, if missing, will be the same as `property` value
type: 'number',
label: 'Padding', // Label for the property
property: 'padding', // CSS property to change
default: '0', // Default value to display
// Additonal `number` options
units: ['px', '%'], // Units (available only for the 'number' type)
min: 0, // Min value (available only for the 'number' type)
}
],
},
]
```
This will render the number input UI and will change the `padding` CSS property on the selected component.
The flexibility of the definition allows you to create easily different UI inputs for any possible CSS property. You're free to decide what will be the best UI for your users. If you take for example a numeric property like `font-size`, you can follow its CSS specification and define it as a `number`
```js
{
type: 'number',
label: 'Font size',
property: 'font-size',
units: ['px', '%', 'em', 'rem', 'vh', 'vw'],
min: 0,
}
```
or you can decide to show it as a `select` and make available only a defined set of values (eg. based on your Design System tokens).
```js
{
type: 'select',
label: 'Font size',
property: 'font-size',
default: '1rem',
options: [
{ id: '0.7rem', label: 'small' },
{ id: '1rem', label: 'medium' },
{ id: '1.2rem', label: 'large' },
]
}
```
Each type defines the specific UI view and a model on how to handle inputs and updates.
Let's see below the list of all available default types with their relative UI and models.
#### Default types
::: tip
Each **Model** describes more in detail available props and their usage.
:::
* `base` - The base type, renders as a simple text input field. **Model**: [Property](/api/property.html)
<img :src="$withBase('/sm-base-type.jpg')"/>
```js
// Example
{
// type: 'base', // Omitting the type in definition will fallback to the 'base'
property: 'some-css-property',
label: 'Base type',
default: 'Default value',
},
```
* `color` - Same props as `base` but the UI is a color picker. **Model**: [Property](/api/property.html)
<img :src="$withBase('/sm-type-color.jpg')"/>
* `number` - Number input field for numeric values. **Model**: [PropertyNumber](/api/property_number.html)
<img :src="$withBase('/sm-type-number.jpg')"/>
```js
// Example
{
type: 'number',
property: 'width',
label: 'Number type',
default: '0%',
// Additional props
units: ['px', '%'],
min: 0,
max: 100,
},
```
* `slider` - Same props as `number` but the UI is a slider. **Model**: [PropertyNumber](/api/property_number.html)
<img :src="$withBase('/sm-type-slider.jpg')"/>
* `select` - Select input with options. **Model**: [PropertySelect](/api/property_select.html)
<img :src="$withBase('/sm-type-select.jpg')"/>
```js
// Example
{
type: 'select',
property: 'display',
label: 'Select type',
default: 'block',
// Additional props
options: [
{id: 'block', label: 'Block'},
{id: 'inline', label: 'Inline'},
{id: 'none', label: 'None'},
]
},
```
* `radio` - Same props as `select` but the UI is a radio button. **Model**: [PropertySelect](/api/property_select.html)
<img :src="$withBase('/sm-type-radio.jpg')"/>
* `composite` - This type is great for CSS shorthand properties, where the final value is a composition of multiple sub properties. **Model**: [PropertyComposite](/api/property_composite.html)
<img :src="$withBase('/sm-type-composite.jpg')"/>
```js
// Example
{
type: 'composite',
property: 'margin',
label: 'Composite type',
// Additional props
properties: [
{ type: 'number', units: ['px'], default: '0', property: 'margin-top' },
{ type: 'number', units: ['px'], default: '0', property: 'margin-right' },
{ type: 'number', units: ['px'], default: '0', property: 'margin-bottom' },
{ type: 'number', units: ['px'], default: '0', property: 'margin-left' },
]
},
```
* `stack` - This type is great for CSS multiple properties like `text-shadow`, `box-shadow`, `transform`, etc. **Model**: [PropertyStack](/api/property_stack.html)
<img :src="$withBase('/sm-type-stack.jpg')"/>
```js
// Example
{
type: 'stack',
property: 'text-shadow',
label: 'Stack type',
// Additional props
properties: [
{ type: 'number', units: ['px'], default: '0', property: 'x' },
{ type: 'number', units: ['px'], default: '0', property: 'y' },
{ type: 'number', units: ['px'], default: '0', property: 'blur' },
{ type: 'color', default: 'black', property: 'color' },
]
},
```
#### Built-in properties
In order to speed up the Style Manager configuration, GrapesJS is shipped with a set of already defined common CSS properties which you can reuse and extend.
```js
sectors: [
{
name: 'First sector',
properties: [
// Pass the built-in CSS property as a string
'width',
'min-width',
// Extend the built-in property with your props
{
extend: 'max-width',
units: ['px', '%'],
},
// If the property doesn't exist it will be converted to a base type
'unknown-property' // -> { type: 'base', property: 'unknown-property' }
],
},
]
```
::: tip
You can check if the property is available by running
```js
editor.StyleManager.getBuiltIn('property-name');
```
or get the list of all available properties with
```js
editor.StyleManager.getBuiltInAll();
```
:::
## I18n
If you're planning to have a multi-language editor you can easily connect sector and property labels to the [I18n] module via their IDs.
```js
grapesjs.init({
styleManager: {
sectors: [
{
id: 'first-sector-id',
// You can leave the name as a fallback in case the i18n definition is missing
name: 'First sector',
properties: [
'width',
{
id: 'display-prop-id', // By default the id is the same as its property name
label: 'Display',
type: 'select',
property: 'display',
default: 'block',
options: [
{id: 'block', label: 'Block'},
{id: 'inline', label: 'Inline'},
{id: 'none', label: 'None'},
]
},
],
},
// ...
],
},
i18n: {
// Use `messagesAdd` in order to extend the default set
messagesAdd: {
en: {
styleManager: {
sectors: {
'first-sector-id': 'First sector EN'
},
properties: {
width: 'Width EN',
'display-prop-id': 'Display EN',
},
options: {
'display-prop-id': {
block: 'Block EN',
inline: 'Inline EN',
none: 'None EN',
}
}
}
}
}
},
});
```
## Component constraints
When you define custom components you can also indicate, via `stylable` and `unstylable` props, which CSS properties should be available for styling. In that case, the Style Manager will only show the available properties. If the sector doesn't contain any available property, it won't be shown.
```js
const customComponents = (editor) => {
// Component A
editor.Components.addType('cmp-a', {
model: {
defaults: {
// When this component is selected, the Style Manager will show only the following properties
stylable: ['width', 'height']
}
}
});
// Component B
editor.Components.addType('cmp-b', {
model: {
defaults: {
// When this component is selected, the Style Manager will hide the following properties
unstylable: ['color']
}
}
});
};
grapesjs.init({
// ...
plugins: [customComponents],
components: [
{ type: 'cmp-a', components: 'Component A' },
{ type: 'cmp-b', components: 'Component B' },
],
styleManager: {
sectors: [
{
name: 'First sector',
properties: [
'width', 'min-width',
'height', 'min-height',
],
},
{
name: 'Second sector',
properties: [
'color', 'font-size',
],
},
],
},
});
```
## Programmatic usage
For a more advanced usage you can rely on the [Style Manager API] to perform different kind of actions related to the module.
* Managing sectors/properties post-initialization.
```js
// Get the module from the editor instance
const sm = editor.StyleManager;
// Add new sector
const newSector = sm.addSector('sector-id', {
name: 'New sector',
open: true,
properties: ['width'],
});
// Add new property to the sector
sm.addProperty('sector-id', {
type: 'number',
property: 'min-width',
});
// Remove sector
sm.removeSector('sector-id');
```
* Managing selected targets.
```js
// Select the first button in the current page
const wrapperCmp = editor.Pages.getSelected().getMainComponent();
const btnCmp = wrapperCmp.find('button')[0];
btnCmp && sm.select(btnCmp);
// Set as a target the CSS selector (the relative CSSRule will be created if doesn't exist yet)
sm.select('.btn > span');
// Get the last selected target
const lastTarget = sm.getLastSelected();
lastTarget?.toCSS && console.log(lastTarget.toCSS());
// Update selected targets with a custom style
sm.addStyleTargets({ color: 'red' });
```
* Adding/extending built-in property definitions.
```js
const myPlugin = (editor) => {
editor.StyleManager.addBuiltIn('new-prop', {
type: 'number',
label: 'New prop',
})
};
grapesjs.init({
// ...
plugins: [myPlugin],
styleManager: {
sectors: [
{
name: 'My sector',
properties: [ 'new-prop', ... ],
},
],
},
})
```
* [Adding new types](#adding-new-types).
## Customization
The default types should cover most of the common styling properties but in case you need a more advanced control over styling you can define your own types or even create a completely custom Style Manager UI from scratch.
### Adding new types
In order to add a new type you have to use the `styleManager.addType` API call and indicate all the necessary methods to make it work properly with the editor. Here an example of implementation by using the native `range` input.
<demo-viewer value="y1mxv6p5" height="500" darkcode/>
<!-- ```js
const customType = (editor) => {
editor.StyleManager.addType('my-custom-prop', {
// Create UI
create({ props, change }) {
const el = document.createElement('div');
el.innerHTML = `<input type="range" class="my-input" min="${props.min}" max="${props.max}"/>`;
const inputEl = el.querySelector('.my-input');
inputEl.addEventListener('change', event => change({ event })); // `change` will trigger the emit
inputEl.addEventListener('input', event => change({ event, partial: true }));
return el;
},
// Propagate UI changes up to the targets
emit({ props, updateStyle }, { event, partial }) {
const { value } = event.target;
updateStyle(`${value}px`, { partial });
},
// Update UI (eg. when the target is changed)
update({ value, el }) {
el.querySelector('.my-input').value = parseInt(value, 10);
},
// Clean the memory from side effects if necessary (eg. global event listeners, etc.)
destroy() {
},
});
};
grapesjs.init({
// ...
plugins: [customType],
styleManager: {
sectors: [
{
name: 'My sector',
properties: [
{
type: 'my-custom-prop',
property: 'font-size',
default: '15',
min: 10,
max: 70,
},
],
},
],
},
})
``` -->
### Custom Style Manager
In case you need a completely custom Style Manager UI (eg. you have to use your UI components), you can create it from scratch by relying on the same sector/propriety architecture or even interfacing directly with selected targets.
All you have to do is to indicate the editor your intent to use a custom UI and then subscribe to the `style:custom` event that will let you know when is the right moment to create/update your UI.
```js
const editor = grapesjs.init({
// ...
styleManager: {
custom: true,
// ...
},
});
editor.on('style:custom', props => {
// props.container (HTMLElement)
// The default element where you can append your
// custom UI in order to render it in the default position.
// Here you would put the logic to render/update your UI by relying on Style Manager API
});
```
Here an example below of a custom Style Manager UI rendered by using Vuetify (Vue Material components) and relying on the default sector/propriety state architecture via API.
<demo-viewer value="46kf7brn" height="500" darkcode/>
From the example above you can notice how we subscribe to `style:custom` and update `this.sectors = sm.getSectors({ visible: true });` on each trigger, and this is enough for the framework to update the rest of the template automatically.
In case you need to get/update the selected style targets directly, you can also rely on these APIs.
```js
// Get the module from the editor instance
const sm = editor.StyleManager;
// Select the first button in the current page
const wrapperCmp = editor.Pages.getSelected().getMainComponent();
const btnCmp = wrapperCmp.find('button')[0];
btnCmp && sm.select(btnCmp);
// You can also select CSS query as a target
sm.select('.btn > span');
// Once the target is selected, you can check its current style object
console.log(sm.getSelected()?.getStyle());
// and update all selected target styles when necessary
sm.addStyleTargets({ color: 'red' });
```
<!--
<style>
.style-manager {
font-size: 12px;
}
.sm-input-color {
width: 16px !important;
height: 15px !important;
opacity: 0 !important;
}
.sm-type-cmp {
background-color: rgba(255,255,255,.05);
border: 1px solid rgba(255,255,255,.25);
border-radius: 3px;
position: relative;
min-height: 45px;
}
.sm-add-layer {
position: absolute !important;
top: -20px;
right: 12px;
}
.sm-layer + .sm-layer {
border-top: 1px solid rgba(255,255,255,.25);
}
.sm-layer-prv,
.sm-btn-prv {
width: 16px;
height: 16px;
border-radius: 3px;
display: inline-block;
}
.sm-layer-prv--text-shadow::after {
color: #000;
content: "T";
font-weight: 900;
font-size: 10px;
text-align: center;
display: block;
}
.gjs-pn-views-container, .gjs-pn-views {
width: 280px;
}
.gjs-pn-options {
right: 280px;
}
.gjs-cv-canvas {
width: calc(100% - 280px);
}
/* Vuetify overrides */
.v-application {
background: transparent !important;
}
.v-application--wrap {
min-height: auto;
}
.v-input__slot {
font-size: 12px;
min-height: 10px !important;
}
.v-select__selections {
flex-wrap: nowrap;
}
.v-text-field .v-input__slot {
padding: 0 10px !important;
}
.v-input--selection-controls {
margin-top: 0;
}
.v-text-field__details, .v-messages {
display: none;
}
</style>
<div>
<div class="style-manager">
<v-app>
<v-main>
<v-expansion-panels accordion multiple>
<v-expansion-panel v-for="sector in sectors" :key="sector.getId()">
<v-expansion-panel-header>
{{ sector.getName() }}
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-row>
<property-field v-for="prop in sector.getProperties()" :key="prop.getId() + prop.canClear()" :prop="prop"/>
</v-row>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-main>
</v-app>
</div>
<div id="property-field" style="display: none;">
<v-col :class="['py-0 px-1 mb-1', prop.isFull() && 'mb-3']" :cols="prop.get('full') ? '12' : '6'">
<v-row :class="labelCls">
<v-col cols="auto pr-0">{{ prop.getLabel() }}</v-col>
<v-col cols="auto" v-if="prop.canClear()">
<v-icon @click="prop.clear()" color="indigo accent-1" small>mdi-close</v-icon>
</v-col>
</v-row>
<div v-if="propType === 'number'">
<v-text-field :placeholder="defValue" :value="inputValue" @change="handleChange" outlined dense/>
</div>
<div v-else-if="propType === 'radio'">
<v-radio-group :value="prop.getValue()" @change="handleChange" row dense>
<v-radio v-for="opt in prop.getOptions()" :key="prop.getOptionId(opt)" :label="prop.getOptionLabel(opt)" :value="prop.getOptionId(opt)"/>
</v-radio-group>
</div>
<div v-else-if="propType === 'select'">
<v-select :items="toOptions" :value="prop.getValue()" @change="handleChange" outlined dense/>
</div>
<div v-else-if="propType === 'color'">
<v-text-field :placeholder="defValue" :value="inputValue" @change="handleChange" outlined dense>
<template v-slot:append>
<div :style="{ backgroundColor: prop.hasValue() ? prop.getValue() : defValue }">
<input class="sm-input-color"
type="color"
:value="prop.hasValue() ? prop.getValue() : defValue"
@change="(ev) => handleChange(ev.target.value)"
@input="(ev) => handleInput(ev.target.value)"
/>
</div>
</template>
</v-text-field>
</div>
<div v-else-if="propType === 'slider'">
<v-slider
track-color="white"
:value="prop.getValue()"
:min="prop.getMin()"
:max="prop.getMax()"
:step="prop.getStep()"
@change="handleChange"
@input="(value) => { console.log('trigger input', value) }"
@start="(value) => { console.log('trigger start', value) }"
/>
</div>
<div v-else-if="propType === 'file'">
<v-btn @click="openAssets(prop)" block>
<v-row>
<v-col v-if="prop.getValue() && prop.getValue() !== defValue" cols="auto">
<div class="sm-btn-prv" :style="{ backgroundImage: `url(${prop.getValue()})` }"></div>
</v-col>
<v-col>Select image</v-col>
</v-row>
</v-btn>
</div>
<div v-else-if="propType === 'composite'">
<v-row no-gutters class="sm-type-cmp pa-2">
<property-field v-for="p in prop.getProperties()" :key="p.getId() + p.canClear()" :prop="p"/>
</v-row>
</div>
<div v-else-if="propType === 'stack'">
<div class="sm-type-cmp pa-3">
<v-icon @click="prop.addLayer({}, { at: 0 })" class="sm-add-layer" small>mdi-plus</v-icon>
<v-row class="sm-layer" v-for="layer in prop.getLayers()" :key="layer.getId()">
<v-col>
<v-row>
<v-col cols="auto" class="pr-1">
<v-icon @click="layer.move(layer.getIndex() - 1)" small>mdi-arrow-up</v-icon>
</v-col>
<v-col cols="auto" class="pl-1">
<v-icon @click="layer.move(layer.getIndex() + 1)" small>mdi-arrow-down</v-icon>
</v-col>
<v-col @click="layer.select()">{{ layer.getLabel() }}</v-col>
<v-col cols="auto">
<div :class="['sm-layer-prv white', `sm-layer-prv--${propName}`]" :style="layer.getStylePreview({ number: { min: -3, max: 3 } })"></div>
</v-col>
<v-col cols="auto">
<v-icon @click="layer.remove()" small>mdi-close</v-icon>
</v-col>
</v-row>
<v-row v-if="layer.isSelected()" no-gutters class="sm-type-cmp pa-2 mt-3">
<property-field v-for="p in prop.getProperties()" :key="p.getId()" :prop="p"/>
</v-row>
</v-col>
</v-row>
</div>
</div>
<div v-else>
<v-text-field :placeholder="defValue" :value="inputValue" @change="handleChange" outlined dense/>
</div>
</v-col>
</div>
</div>
<script>
const sm = editor.StyleManager;
const Observer = (new Vue()).$data.__ob__.constructor; // obj.__ob__ = new Observer({});
Vue.mixin({
data() {
return { editor };
}
});
Vue.component('property-field', {
props: { prop: Object },
template: '#property-field',
computed: {
labelCls() {
const { prop } = this;
const parent = prop.getParent();
const hasParentValue = prop.hasValueParent() && (parent ? parent.isDetached() : true);
return ['flex-nowrap', prop.canClear() && 'indigo--text text--accent-1', hasParentValue && 'orange--text'];
},
inputValue() {
return this.prop.hasValue() ? this.prop.getValue() : '';
},
propName() {
return this.prop.getName();
},
propType() {
return this.prop.getType();
},
defValue() {
return this.prop.getDefaultValue();
},
toOptions() {
const { prop } = this;
return prop.getOptions().map(o => ({ value: prop.getOptionId(o), text: prop.getOptionLabel(o) }))
},
},
methods: {
handleChange(value) {
this.prop.upValue(value);
},
handleInput(value) {
this.prop.upValue(value, { partial: true });
},
openAssets(prop) {
const { Assets } = this.editor;
Assets.open({
select: (asset, complete) => {
prop.upValue(asset.getSrc(), { partial: !complete });
complete && Assets.close();
},
types: ['image'],
accept: 'image/*',
})
}
}
})
const app = new Vue({
vuetify: new Vuetify({
theme: { dark: true },
}),
el: '.style-manager',
data: { sectors: [] },
mounted() {
editor.on('style:custom', this.handleCustom);
},
destroyed() {
editor.off('style:custom', this.handleCustom);
},
methods: {
handleCustom(props) {
if (props.container && !props.container.contains(this.$el)) {
props.container.appendChild(this.$el);
}
this.sectors = sm.getSectors({ visible: true });
},
}
});
</script>
-->
## Events
For a complete list of available events, you can check it [here](/api/style_manager.html#available-events).
[Components]: <Components.html>
[I18n]: <I18n.html>
[Style Manager API]: </api/style_manager.html>

12
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.17.28",
"version": "0.18.1",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",
@ -18,7 +18,6 @@
"dependencies": {
"backbone": "1.3.3",
"backbone-undo": "^0.2.5",
"cash-dom": "^2.3.9",
"codemirror": "^5.63.0",
"codemirror-formatting": "^1.0.0",
"promise-polyfill": "^8.1.3",
@ -35,7 +34,7 @@
"jest": "^24.9.0",
"lint-staged": "^8.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^1.18.2",
"prettier": "^2.4.1",
"sass": "^1.42.1",
"sinon": "^7.5.0",
"vuepress": "^1.8.2",
@ -67,7 +66,7 @@
},
"lint-staged": {
"{src,test}/**/*.js": [
"prettier --single-quote --write",
"prettier --single-quote --print-width 120 --arrow-parens avoid --write",
"git add"
]
},
@ -81,10 +80,7 @@
],
"setupFiles": [
"<rootDir>/test/setup.js"
],
"moduleNameMapper": {
"^jquery$": "cash-dom"
}
]
},
"husky": {
"hooks": {

25
src/asset_manager/index.js

@ -63,6 +63,11 @@ 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`;
export default () => {
let c = {};
@ -89,7 +94,11 @@ export default () => {
removeBefore: evRemoveBefore,
custom: evCustom,
open: evOpen,
close: evClose
close: evClose,
uploadStart: evUploadStart,
uploadEnd: evUploadEnd,
uploadError: evUploadError,
uploadResponse: evUploadRes,
},
init(config = {}) {
@ -121,7 +130,7 @@ export default () => {
this.getAll().trigger(ev, ...data);
},
__onAllEvent: debounce(function() {
__onAllEvent: debounce(function () {
this.__trgCustom();
}),
@ -148,7 +157,7 @@ export default () => {
isFunction(bhv.select) && bhv.select(res, complete);
},
// extra
options: bhv.options || {}
options: bhv.options || {},
};
},
@ -177,7 +186,7 @@ export default () => {
cmd.run(assetCmd, {
types: ['image'],
select: () => {},
...options
...options,
});
},
@ -355,14 +364,14 @@ export default () => {
collection: assetsVis, // Collection visible in asset manager
globalCollection: assets,
config: c,
module: this
module: this,
};
fu = new FileUpload(obj);
obj.fu = fu;
const el = am && am.el;
am = new AssetsView({
el,
...obj
...obj,
});
am.render();
}
@ -469,7 +478,7 @@ export default () => {
__behaviour(opts = {}) {
return (this._bhv = {
...(this._bhv || {}),
...opts
...opts,
});
},
@ -487,6 +496,6 @@ export default () => {
[assets, am, fu].forEach(i => (i = null));
this._bhv = {};
c = {};
}
},
};
};

2
src/asset_manager/view/FileUploader.js

@ -26,7 +26,7 @@ export default Backbone.View.extend(
initialize(opts = {}) {
this.options = opts;
const c = opts.config || {};
this.module = c.module;
this.module = opts.module;
this.config = c;
this.em = this.config.em;
this.pfx = c.stylePrefix || '';

2
src/block_manager/view/BlockView.js

@ -128,7 +128,7 @@ export default Backbone.View.extend({
${media ? `<div class="${className}__media">${media}</div>` : ''}
<div class="${className}-label">${label}</div>
`;
el.title = el.textContent.trim();
el.title = attr.title || el.textContent.trim();
el.setAttribute('draggable', hasDnd(em) && !disable ? true : false);
const result = render && render({ el, model, className, prefix: ppfx });
if (result) el.innerHTML = result;

53
src/canvas/index.js

@ -8,12 +8,25 @@
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('canvas:drop', () => { ... });
*
* // Use the API
* const canvas = editor.Canvas;
* canvas.setCoords(...);
* ```
* ## Available Events
* * `canvas:dragenter` - When something is dragged inside the canvas, `DataTransfer` instance passed as an argument
* * `canvas:dragover` - When something is dragging on canvas, `DataTransfer` instance passed as an argument
* * `canvas:drop` - Something is dropped in canvas, `DataTransfer` instance and the dropped model are passed as arguments
* * `canvas:dragend` - When a drag operation is ended, `DataTransfer` instance passed as an argument
* * `canvas:dragdata` - On any dataTransfer parse, `DataTransfer` instance and the `result` are passed as arguments.
* By changing `result.content` you're able to customize what is dropped
*
* ## Methods
* * [getConfig](#getconfig)
* * [getElement](#getelement)
* * [getFrameEl](#getframeel)
@ -65,7 +78,7 @@ export default () => {
c = {
...defaults,
...config,
module: this
module: this,
};
this.em = c.em;
@ -238,11 +251,7 @@ export default () => {
* @private
*/
getOffsetViewerEl(compView) {
return this._getLocalEl(
CanvasView.offsetEl,
compView,
'getOffsetViewerEl'
);
return this._getLocalEl(CanvasView.offsetEl, compView, 'getOffsetViewerEl');
},
/**
@ -258,7 +267,7 @@ export default () => {
CanvasView && CanvasView.remove();
CanvasView = new canvasView({
model: canvas,
config: c
config: c,
});
return CanvasView.render().el;
},
@ -273,7 +282,7 @@ export default () => {
var canvasOff = this.offset(this.getElement());
return {
top: frameOff.top - canvasOff.top,
left: frameOff.left - canvasOff.left
left: frameOff.left - canvasOff.left,
};
},
@ -328,7 +337,7 @@ export default () => {
return {
...CanvasView.getCanvasOffset(),
topScroll: top,
leftScroll: left
leftScroll: left,
};
},
@ -379,7 +388,7 @@ export default () => {
canvasTop: canvasPos.top,
canvasLeft: canvasPos.left,
canvasWidth: canvasPos.width,
canvasHeight: canvasPos.height
canvasHeight: canvasPos.height,
};
// In this way I can catch data and also change the position strategy
@ -400,9 +409,7 @@ export default () => {
const zoom = this.em.getZoomDecimal();
const side = top ? 'top' : 'left';
const doc = el.ownerDocument;
const { offsetTop = 0, offsetLeft = 0 } = opts.offset
? getFrameElFromDoc(doc)
: {};
const { offsetTop = 0, offsetLeft = 0 } = opts.offset ? getFrameElFromDoc(doc) : {};
const { scrollTop = 0, scrollLeft = 0 } = doc.body || {};
const scroll = top ? scrollTop : scrollLeft;
const offset = top ? offsetTop : offsetLeft;
@ -416,7 +423,7 @@ export default () => {
return {
top: rectOff(el, 1, pos),
left: rectOff(el, 0, pos)
left: rectOff(el, 0, pos),
};
},
@ -453,7 +460,7 @@ export default () => {
top,
left,
canvasOffsetTop: cvOff.top,
canvasOffsetLeft: cvOff.left
canvasOffsetLeft: cvOff.left,
};
// In this way I can catch data and also change the position strategy
@ -490,7 +497,7 @@ export default () => {
return {
y: e.clientY + addTop - yOffset,
x: e.clientX + addLeft - xOffset
x: e.clientX + addLeft - xOffset,
};
},
@ -506,7 +513,7 @@ export default () => {
return {
y: ev.clientY * zoom + top,
x: ev.clientX * zoom + left
x: ev.clientX * zoom + left,
};
},
@ -528,9 +535,7 @@ export default () => {
const frame = this.getFrameEl();
const toIgnore = ['body', ...this.getConfig().notTextable];
const docActive = frame && document.activeElement === frame;
const focused = docActive
? doc && doc.activeElement
: document.activeElement;
const focused = docActive ? doc && doc.activeElement : document.activeElement;
return focused && !toIgnore.some(item => focused.matches(item));
},
@ -666,11 +671,11 @@ export default () => {
addFrame(props = {}, opts = {}) {
return canvas.get('frames').add(
{
...props
...props,
},
{
...opts,
em: this.em
em: this.em,
}
);
},
@ -680,6 +685,6 @@ export default () => {
CanvasView && CanvasView.remove();
[c, canvas, CanvasView].forEach(i => (i = {}));
['em', 'model', 'droppable'].forEach(i => (this[i] = {}));
}
},
};
};

2
src/canvas/view/CanvasView.js

@ -70,7 +70,7 @@ export default Backbone.View.extend({
scroll &&
component.views.forEach(view => {
view._getFrame() !== currFrame && view.scrollIntoView(scroll);
view._getFrame() === currFrame && view.scrollIntoView(scroll);
});
},

5
src/code_manager/model/CssGenerator.js

@ -3,6 +3,11 @@ import { bindAll, isUndefined, each } from 'underscore';
const maxValue = Number.MAX_VALUE;
export const getMediaLength = mediaQuery => {
const length = /(-?\d*\.?\d+)\w{0,}/.exec(mediaQuery);
return !length ? '' : length[0];
};
export default Backbone.Model.extend({
initialize() {
bindAll(this, 'sortRules');

43
src/commands/index.js

@ -8,12 +8,27 @@
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('run', () => { ... });
*
* // Use the API
* 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
*
* ## Methods
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
@ -56,7 +71,7 @@ const commandsDef = [
['component-exit', 'ComponentExit', 'select-parent'],
['component-delete', 'ComponentDelete'],
['component-style-clear', 'ComponentStyleClear'],
['component-drag', 'ComponentDrag']
['component-drag', 'ComponentDrag'],
];
export default () => {
@ -67,7 +82,7 @@ export default () => {
const active = {};
// Need it here as it would be used below
const add = function(id, obj) {
const add = function (id, obj) {
if (isFunction(obj)) obj = { run: obj };
if (!obj.stop) obj.noStop = 1;
delete obj.initialize;
@ -94,7 +109,7 @@ export default () => {
init(config = {}) {
c = {
...defaults,
...config
...config,
};
em = c.em;
const ppfx = c.pStylePrefix;
@ -109,14 +124,14 @@ export default () => {
defaultCommands['tlb-delete'] = {
run(ed) {
return ed.runCommand('core:component-delete');
}
},
};
defaultCommands['tlb-clone'] = {
run(ed) {
ed.runCommand('core:copy');
ed.runCommand('core:paste');
}
},
};
defaultCommands['tlb-move'] = {
@ -171,7 +186,7 @@ export default () => {
onStart,
onDrag,
onEnd,
event
event,
});
} else {
if (nativeDrag) {
@ -187,7 +202,7 @@ export default () => {
}
selAll.forEach(sel => sel.set('status', 'freezed-selected'));
}
},
};
// Core commands
@ -202,9 +217,7 @@ export default () => {
defaultCommands[oldCmd] = cmd;
// Propogate old commands (can be removed once we stop to call old commands)
['run', 'stop'].forEach(name => {
em.on(`${name}:${oldCmd}`, (...args) =>
em.trigger(`${name}:${cmdName}`, ...args)
);
em.on(`${name}:${oldCmd}`, (...args) => em.trigger(`${name}:${cmdName}`, ...args));
});
}
});
@ -274,13 +287,11 @@ export default () => {
if (command) {
const cmdObj = {
...command.constructor.prototype,
...cmd
...cmd,
};
this.add(id, cmdObj);
// Extend also old name commands if exist
const oldCmd = commandsDef.filter(
cmd => `core:${cmd[0]}` === id && cmd[2]
)[0];
const oldCmd = commandsDef.filter(cmd => `core:${cmd[0]}` === id && cmd[2])[0];
oldCmd && this.add(oldCmd[2], cmdObj);
}
return this;
@ -432,6 +443,6 @@ export default () => {
destroy() {
[em, c, commands, defaultCommands, active].forEach(i => (i = {}));
}
},
};
};

16
src/commands/view/ComponentDrag.js

@ -321,17 +321,31 @@ export default {
const { Canvas } = editor;
const style = target.getStyle();
const position = 'absolute';
const relPos = [position, 'relative'];
onStart && onStart(this._getDragData());
if (isTran) return;
if (style.position !== position) {
let { left, top, width, height } = Canvas.offset(target.getEl());
let parent = target.parent();
let parentRel;
// Check if to center the target to the pointer position
// Check for the relative parent
do {
const pStyle = parent.getStyle();
parentRel = relPos.indexOf(pStyle.position) >= 0 ? parent : null;
parent = parent.parent();
} while (parent && !parentRel);
// Center the target to the pointer position (used in Droppable for Blocks)
if (center) {
const { x, y } = Canvas.getMouseRelativeCanvas(event);
left = x;
top = y;
} else if (parentRel) {
const offsetP = Canvas.offset(parentRel.getEl());
left = left - offsetP.left;
top = top - offsetP.top;
}
this.setPosition({

54
src/commands/view/OpenStyleManager.js

@ -2,19 +2,19 @@ import Backbone from 'backbone';
const $ = Backbone.$;
export default {
run(em, sender) {
run(editor, sender) {
this.sender = sender;
if (!this.$cn) {
const config = em.getConfig();
const panels = em.Panels;
const config = editor.getConfig();
const panels = editor.Panels;
const trgEvCnt = 'change:appendContent';
this.$cn = $('<div></div>');
this.$cn2 = $('<div></div>');
this.$cn.append(this.$cn2);
// Device Manager
const dvm = em.DeviceManager;
const dvm = editor.DeviceManager;
if (dvm && config.showDevices) {
const devicePanel = panels.addPanel({ id: 'devices-c' });
const dvEl = dvm.render();
@ -22,7 +22,8 @@ export default {
}
// Selector Manager container
const slm = em.SelectorManager;
const slm = editor.SelectorManager;
this.slm = slm;
const slmConfig = slm.getConfig();
if (slmConfig.custom) {
slm.__trgCustom({ container: this.$cn2.get(0) });
@ -31,15 +32,17 @@ export default {
}
// Style Manager
const sm = em.StyleManager;
const sm = editor.StyleManager;
this.sm = sm;
const smConfig = sm.getConfig();
if (!smConfig.appendTo) {
const pfx = smConfig.stylePrefix;
this.$header = $(`<div class="${pfx}header">${editor.t('styleManager.empty')}</div>`);
this.$cn.append(this.$header);
if (smConfig.custom) {
sm.__trgCustom({ container: this.$cn2.get(0) });
} else if (!smConfig.appendTo) {
this.$cn2.append(sm.render());
const pfx = smConfig.stylePrefix;
this.$header = $(
`<div class="${pfx}header">${em.t('styleManager.empty')}</div>`
);
this.$cn.append(this.$header);
}
// Create panel if not exists
@ -50,8 +53,9 @@ export default {
// Add all containers to the panel
this.panel.set('appendContent', this.$cn).trigger(trgEvCnt);
this.target = em.editor;
this.listenTo(this.target, 'component:toggled', this.toggleSm);
// Toggle Style Manager on target selection
this.em = editor.getModel();
this.listenTo(this.em, sm.events.target, this.toggleSm);
}
this.toggleSm();
@ -62,22 +66,20 @@ export default {
* @private
*/
toggleSm() {
const { target, sender } = this;
if (sender && sender.get && !sender.get('active')) return;
const { componentFirst } = target.get('SelectorManager').getConfig();
const selectedAll = target.getSelectedAll().length;
const { sender, sm } = this;
if ((sender && sender.get && !sender.get('active')) || !sm) return;
if (selectedAll === 1 || (selectedAll > 1 && componentFirst)) {
this.$cn2 && this.$cn2.show();
this.$header && this.$header.hide();
if (sm.getSelected()) {
this.$cn2?.show();
this.$header?.hide();
} else {
this.$cn2 && this.$cn2.hide();
this.$header && this.$header.show();
this.$cn2?.hide();
this.$header?.show();
}
},
stop() {
this.$cn2 && this.$cn2.hide();
this.$header && this.$header.hide();
}
this.$cn2?.hide();
this.$header?.hide();
},
};

8
src/commands/view/PasteComponent.js

@ -9,7 +9,10 @@ export default {
if (clp && selected) {
ed.getSelectedAll().forEach(comp => {
if (!comp) return;
const coll = comp.collection;
if (!coll) return;
const at = coll.indexOf(comp) + 1;
const copyable = clp.filter(cop => cop.get('copyable'));
let added;
@ -17,7 +20,10 @@ export default {
if (contains(clp, comp) && comp.get('copyable')) {
added = coll.add(comp.clone(), { at });
} else {
added = coll.add(copyable.map(cop => cop.clone()), { at });
added = coll.add(
copyable.map(cop => cop.clone()),
{ at }
);
}
added = isArray(added) ? added : [added];

90
src/commands/view/SelectComponent.js

@ -1,13 +1,6 @@
import Backbone from 'backbone';
import { bindAll, isElement, isUndefined, debounce } from 'underscore';
import {
on,
off,
getUnitFromValue,
isTaggableNode,
getViewEl,
hasWin
} from 'utils/mixins';
import { on, off, getUnitFromValue, isTaggableNode, getViewEl, hasWin } from 'utils/mixins';
import { isVisible, isDoc } from 'utils/dom';
import ToolbarView from 'dom_components/view/ToolbarView';
import Toolbar from 'dom_components/model/Toolbar';
@ -36,15 +29,7 @@ let showOffsets;
*/
export default {
init(o) {
bindAll(
this,
'onHover',
'onOut',
'onClick',
'onFrameScroll',
'onFrameUpdated',
'onContainerChange'
);
bindAll(this, 'onHover', 'onOut', 'onClick', 'onFrameScroll', 'onFrameUpdated', 'onContainerChange');
},
enable() {
@ -92,7 +77,7 @@ export default {
em[method]('component:toggled component:update', this.onSelect, this);
em[method]('change:componentHovered', this.onHovered, this);
em[method](
'component:resize component:styleUpdate component:input',
'component:resize styleable:change component:input', // component:styleUpdate
this.updateGlobalPos,
this
);
@ -177,7 +162,7 @@ export default {
* @param {Object} el
* @private
* */
onSelect: debounce(function() {
onSelect: debounce(function () {
const { em } = this;
const component = em.getSelected();
const currentFrame = em.get('currentFrame') || {};
@ -242,7 +227,7 @@ export default {
view: opts.view,
force: 1,
top: 0,
left: 0
left: 0,
});
},
@ -253,7 +238,7 @@ export default {
*/
hideElementOffset(view) {
this.editor.stopCommand('show-offset', {
view
view,
});
},
@ -266,7 +251,7 @@ export default {
this.editor.runCommand('show-offset', {
el,
elPos: pos,
state: 'Fixed'
state: 'Fixed',
});
},
@ -325,7 +310,7 @@ export default {
*/
select(model, event = {}) {
if (!model) return;
this.editor.select(model, { scroll: {}, event });
this.editor.select(model, { event });
this.initResize(model);
},
@ -347,9 +332,7 @@ export default {
const ppfx = config.pStylePrefix || '';
const clsBadge = `${ppfx}badge`;
const customeLabel = config.customBadgeLabel;
const badgeLabel = `${
icon ? `<div class="${clsBadge}__icon">${icon}</div>` : ''
}
const badgeLabel = `${icon ? `<div class="${clsBadge}__icon">${icon}</div>` : ''}
<div class="${clsBadge}__name">${model.getName()}</div>`;
badge.innerHTML = customeLabel ? customeLabel(model) : badgeLabel;
}
@ -359,7 +342,7 @@ export default {
bStyle.display = 'block';
const targetToElem = canvas.getTargetToElementFixed(el, badge, {
pos: pos
pos: pos,
});
const top = targetToElem.top; //opts.topOff - badgeH < 0 ? -opts.topOff : posTop;
@ -390,8 +373,7 @@ export default {
const config = em ? em.get('Config') : '';
const pfx = config.stylePrefix || '';
const resizeClass = `${pfx}resizing`;
const model =
!isElement(elem) && isTaggableNode(elem) ? elem : em.getSelected();
const model = !isElement(elem) && isTaggableNode(elem) ? elem : em.getSelected();
const resizable = model && model.get('resizable');
let options = {};
let modelToStyle;
@ -402,10 +384,7 @@ export default {
docs.forEach(doc => {
const body = doc.body;
const cls = body.className || '';
body.className = (method == 'add'
? `${cls} ${resizeClass}`
: cls.replace(resizeClass, '')
).trim();
body.className = (method == 'add' ? `${cls} ${resizeClass}` : cls.replace(resizeClass, '')).trim();
});
};
@ -415,13 +394,7 @@ export default {
// Here the resizer is updated with the current element height and width
onStart(e, opts = {}) {
const { el, config, resizer } = opts;
const {
keyHeight,
keyWidth,
currentUnit,
keepAutoHeight,
keepAutoWidth
} = config;
const { keyHeight, keyWidth, currentUnit, keepAutoHeight, keepAutoWidth } = config;
toggleBodyClass('add', e, opts);
modelToStyle = em.get('StyleManager').getModelToStyle(model);
canvas.toggleFramesEvents();
@ -468,14 +441,7 @@ export default {
}
const { store, selectedHandler, config } = options;
const {
keyHeight,
keyWidth,
autoHeight,
autoWidth,
unitWidth,
unitHeight
} = config;
const { keyHeight, keyWidth, autoHeight, autoWidth, unitWidth, unitHeight } = config;
const onlyHeight = ['tc', 'bc'].indexOf(selectedHandler) >= 0;
const onlyWidth = ['cl', 'cr'].indexOf(selectedHandler) >= 0;
const style = {};
@ -495,7 +461,7 @@ export default {
const updateEvent = `update:component:style`;
const eventToListen = `${updateEvent}:${keyHeight} ${updateEvent}:${keyWidth}`;
em && em.trigger(eventToListen, null, null, { noEmit: 1 });
}
},
};
if (typeof resizable == 'object') {
@ -529,7 +495,7 @@ export default {
const toolbarView = new ToolbarView({
collection: this.toolbar,
editor: this.editor,
em
em,
});
toolbarEl.appendChild(toolbarView.render().el);
}
@ -628,7 +594,7 @@ export default {
...badgeOpts,
view,
topOff,
leftOff
leftOff,
});
style.top = topOff + unit;
@ -642,18 +608,18 @@ export default {
top: topOff,
left: leftOff,
width: pos.width,
height: pos.height
height: pos.height,
});
},
_upToolbar: debounce(function() {
_upToolbar: debounce(function () {
this.updateToolsGlobal({ force: 1 });
}),
_trgToolUp(type, opts = {}) {
this.em.trigger('canvas:tools:update', {
type,
...opts
...opts,
});
},
@ -677,11 +643,7 @@ export default {
const unit = 'px';
const toolsEl = this.toggleToolsEl(1);
const { style } = toolsEl;
const targetToElem = canvas.getTargetToElementFixed(
el,
canvas.getToolbarEl(),
{ pos }
);
const targetToElem = canvas.getTargetToElementFixed(el, canvas.getToolbarEl(), { pos });
const topOff = targetToElem.canvasOffsetTop;
const leftOff = targetToElem.canvasOffsetLeft;
style.top = topOff + unit;
@ -696,18 +658,18 @@ export default {
top: topOff,
left: leftOff,
width: pos.width,
height: pos.height
height: pos.height,
});
},
/**
* Update attached elements, eg. component toolbar
*/
updateAttached: debounce(function() {
updateAttached: debounce(function () {
this.updateGlobalPos();
}),
onContainerChange: debounce(function() {
onContainerChange: debounce(function () {
this.em.refreshCanvas();
}, 150),
@ -738,7 +700,7 @@ export default {
model &&
model.set({
status: '',
state: ''
state: '',
});
},
@ -764,5 +726,5 @@ export default {
!opts.preserveSelected && em.setSelected(null);
this.toggleToolsEl();
editor && editor.stopCommand('resize');
}
},
};

1
src/common/View.js

@ -0,0 +1 @@
export { View as default } from 'backbone';

1
src/common/index.js

@ -1,2 +1,3 @@
export { default as Model } from './Model';
export { default as Collection } from './Collection';
export { default as View } from './View';

20
src/common/module.js

@ -25,7 +25,7 @@ export default {
__initConfig(def = {}, conf = {}) {
this.config = {
...def,
...conf
...conf,
};
this.em = this.config.em;
},
@ -37,9 +37,7 @@ export default {
all
.on('add', (m, c, o) => em.trigger(events.add, m, o))
.on('remove', (m, c, o) => em.trigger(events.remove, m, o))
.on('change', (p, c) =>
em.trigger(events.update, p, p.changedAttributes(), c)
)
.on('change', (p, c) => em.trigger(events.update, p, p.changedAttributes(), c))
.on('all', this.__catchAllEvent, this);
// Register collections
this.cls = [all].concat(opts.collections || []);
@ -100,11 +98,23 @@ export default {
return id;
},
__listenAdd(model, event) {
model.on('add', (m, c, o) => this.em.trigger(event, m, o));
},
__listenRemove(model, event) {
model.on('remove', (m, c, o) => this.em.trigger(event, m, o));
},
__listenUpdate(model, event) {
model.on('change', (p, c) => this.em.trigger(event, p, p.changedAttributes(), c));
},
__destroy() {
this.cls.forEach(coll => {
coll.stopListening();
coll.reset();
});
this.em = 0;
}
},
};

17
src/css_composer/config/config.js

@ -3,5 +3,20 @@ export default {
stylePrefix: 'css-',
// Default CSS style
rules: []
rules: [],
/**
* Adjust style object before creation/update.
* @example
* onBeforeStyle(style) {
* const padValue = style.padding;
* if (padValue === '10px') {
* delete style.padding;
* style['padding-top'] = padValue;
* // ...
* }
* return style;
* }
*/
onBeforeStyle: null
};

31
src/css_composer/index.js

@ -225,14 +225,10 @@ export default () => {
if (isString(selectors)) {
const sm = em.get('SelectorManager');
const singleSel = selectors.split(',')[0].trim();
const node = em
.get('Parser')
.parserCss.checkNode({ selectors: singleSel })[0];
const node = em.get('Parser').parserCss.checkNode({ selectors: singleSel })[0];
slc = sm.get(node.selectors);
}
return (
rules.find(rule => rule.compare(slc, state, width, ruleProps)) || null
);
return rules.find(rule => rule.compare(slc, state, width, ruleProps)) || null;
},
getAll() {
@ -279,9 +275,7 @@ export default () => {
isObject(props) && model.set(props, opts);
if (updateStyle) {
let styleUpdate = opts.extend
? { ...model.get('style'), ...style }
: style;
let styleUpdate = opts.extend ? { ...model.get('style'), ...style } : style;
model.set('style', styleUpdate, opts);
}
@ -330,14 +324,14 @@ export default () => {
const { atRuleType, atRuleParams } = opts;
const node = em.get('Parser').parserCss.checkNode({
selectors,
style
style,
})[0];
const { state, selectorsAdd } = node;
const sm = em.get('SelectorManager');
const selector = sm.add(node.selectors);
const rule = this.add(selector, state, atRuleParams, {
selectorsAdd,
atRule: atRuleType
atRule: atRuleType,
});
rule.setStyle(style, opts);
return rule;
@ -368,7 +362,7 @@ export default () => {
selector &&
this.get(selector, state, atRuleParams, {
selectorsAdd,
atRule: atRuleType
atRule: atRuleType,
})
);
},
@ -388,9 +382,8 @@ export default () => {
getRules(selector) {
const rules = this.getAll();
if (!selector) return [...rules.models];
const result = rules.filter(
r => r.getSelectors().getFullString() === selector
);
const sels = isString(selector) ? selector.split(',').map(s => s.trim()) : selector;
const result = rules.filter(r => sels.indexOf(r.getSelectors().getFullString()) >= 0);
return result;
},
@ -489,9 +482,9 @@ export default () => {
* // Remove by selector
* css.remove('.my-cls-2');
*/
remove(rule) {
remove(rule, opts) {
const toRemove = isString(rule) ? this.getRules(rule) : rule;
const result = this.getAll().remove(toRemove);
const result = this.getAll().remove(toRemove, opts);
return isArray(result) ? result : [result];
},
@ -528,7 +521,7 @@ export default () => {
rulesView && rulesView.remove();
rulesView = new CssRulesView({
collection: rules,
config: c
config: c,
});
return rulesView.render().el;
},
@ -539,6 +532,6 @@ export default () => {
rulesView && rulesView.remove();
[em, rules, rulesView].forEach(i => (i = null));
c = {};
}
},
};
};

60
src/css_composer/model/CssRule.js

@ -2,6 +2,7 @@ import { Model } from 'backbone';
import Styleable from 'domain_abstract/model/Styleable';
import { isEmpty, forEach, isString, isArray } from 'underscore';
import Selectors from 'selector_manager/model/Selectors';
import { getMediaLength } from 'code_manager/model/CssGenerator';
import { isEmptyObj, hasWin } from 'utils/mixins';
const { CSS } = hasWin() ? window : {};
@ -17,6 +18,10 @@ const { CSS } = hasWin() ? window : {};
* @property {String} [state=''] State of the rule, eg: `hover`, `focused`
* @property {Boolean|Array<String>} [important=false] If true, sets `!important` on all properties. You can also pass an array to specify properties on which use important
* @property {Boolean} [stylable=true] Indicates if the rule is stylable from the editor
*
* [Device]: device.html
* [State]: state.html
* [Component]: component.html
*/
export default class CssRule extends Model.extend(Styleable) {
defaults() {
@ -142,13 +147,62 @@ export default class CssRule extends Model.extend(Styleable) {
const style = this.styleToString(opts);
const singleAtRule = this.get('singleAtRule');
if ((selectors || singleAtRule) && style) {
if ((selectors || singleAtRule) && (style || opts.allowEmpty)) {
result = singleAtRule ? style : `${selectors}{${style}}`;
}
return result;
}
/**
* Get the Device the rule is related to.
* @returns {[Device]|null}
* @example
* const device = rule.getDevice();
* console.log(device?.getName());
*/
getDevice() {
const { em } = this;
const { atRuleType, mediaText } = this.attributes;
const devices = em?.get('DeviceManager').getDevices() || [];
const deviceDefault = devices.filter(d => d.getWidthMedia() === '')[0];
if (atRuleType !== 'media' || !mediaText) {
return deviceDefault || null;
}
return (
devices.filter(d => d.getWidthMedia() === getMediaLength(mediaText))[0] ||
null
);
}
/**
* Get the State the rule is related to.
* @returns {[State]|null}
* @example
* const state = rule.getState();
* console.log(state?.getLabel());
*/
getState() {
const { em } = this;
const stateValue = this.get('state');
const states = em.get('SelectorManager').getStates() || [];
return states.filter(s => s.getName() === stateValue)[0] || null;
}
/**
* Returns the related Component (valid only for component-specific rules).
* @returns {[Component]|null}
* @example
* const cmp = rule.getComponent();
* console.log(cmp?.toHTML());
*/
getComponent() {
const sel = this.getSelectors();
const sngl = sel.length == 1 && sel.at(0);
const cmpId = sngl && sngl.isId() && sngl.get('name');
return (cmpId && this.em?.get('DomComponents').getById(cmpId)) || null;
}
/**
* Return the CSS string of the rule
* @param {Object} [opts={}] Options (same as in `getDeclaration`)
@ -164,7 +218,9 @@ export default class CssRule extends Model.extend(Styleable) {
let result = '';
const atRule = this.getAtRule();
const block = this.getDeclaration(opts);
block && (result = block);
if (block || opts.allowEmpty) {
result = block;
}
if (atRule && result) {
result = `${atRule}{${result}}`;

14
src/device_manager/config/config.js

@ -7,25 +7,25 @@ export default {
{
id: 'desktop',
name: 'Desktop',
width: ''
width: '',
},
{
id: 'tablet',
name: 'Tablet',
width: '768px',
widthMedia: '992px'
width: '770px',
widthMedia: '992px',
},
{
id: 'mobileLandscape',
name: 'Mobile landscape',
width: '568px',
widthMedia: '768px'
widthMedia: '768px',
},
{
id: 'mobilePortrait',
name: 'Mobile portrait',
width: '320px',
widthMedia: '480px'
}
]
widthMedia: '480px',
},
],
};

8
src/device_manager/model/Device.js

@ -34,4 +34,12 @@ export default class Device extends Model {
const noUnit = (parseFloat(pr) || 0).toString() === pr.toString();
noUnit && this.set(prop, `${pr}px`);
}
getName() {
return this.get('name') || this.get('id');
}
getWidthMedia() {
return this.get('widthMedia') || '';
}
}

2
src/dom_components/config/config.js

@ -15,7 +15,7 @@ export default {
storeWrapper: 0,
/**
* You can setup a custom component definiton processor before adding it into the editor.
* You can setup a custom component definition processor before adding it into the editor.
* It might be useful to transform custom objects (es. some framework specific JSX) to GrapesJS component one.
* This custom function will be executed on ANY new added component to the editor so make smart checks/conditions
* to avoid doing useless executions

145
src/dom_components/index.js

@ -8,12 +8,38 @@
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* const domComponents = editor.DomComponents;
* // Listen to events
* editor.on('component:create', () => { ... });
*
* // Use the API
* const cmp = editor.Components;
* cmp.addType(...);
* ```
*
* ## Available Events
* * `component:create` - Component is created (only the model, is not yet mounted in the canvas), called after the init() method
* * `component:mount` - Component is mounted to an element and rendered in canvas
* * `component:add` - Triggered when a new component is added to the editor, the model is passed as an argument to the callback
* * `component:remove` - Triggered when a component is removed, the model is passed as an argument to the callback
* * `component:remove:before` - Triggered before the remove of the component, the model, remove function (if aborted via options, with this function you can complete the remove) and options (use options.abort = true to prevent remove), are passed as arguments to the callback
* * `component:clone` - Triggered when a component is cloned, the new model is passed as an argument to the callback
* * `component:update` - Triggered when a component is updated (moved, styled, etc.), the model is passed as an argument to the callback
* * `component:update:{propertyName}` - Listen any property change, the model is passed as an argument to the callback
* * `component:styleUpdate` - Triggered when the style of the component is updated, the model is passed as an argument to the callback
* * `component:styleUpdate:{propertyName}` - Listen for a specific style property change, the model is passed as an argument to the callback
* * `component:selected` - New component selected, the selected model is passed as an argument to the callback
* * `component:deselected` - Component deselected, the deselected model is passed as an argument to the callback
* * `component:toggled` - Component selection changed, toggled model is passed as an argument to the callback
* * `component:type:add` - New component type added, the new type is passed as an argument to the callback
* * `component:type:update` - Component type updated, the updated type is passed as an argument to the callback
* * `component:drag:start` - Component drag started. Passed an object, to the callback, containing the `target` (component to drag), `parent` (parent of the component) and `index` (component index in the parent)
* * `component:drag` - During component drag. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the current pointer
* * `component:drag:end` - Component drag ended. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the final pointer
*
* ## Methods
* * [getWrapper](#getwrapper)
* * [getComponents](#getcomponents)
* * [addComponent](#addcomponent)
@ -25,7 +51,7 @@
* * [getTypes](#gettypes)
* * [render](#render)
*
* @module DomComponents
* @module Components
*/
import Backbone from 'backbone';
import { isEmpty, isObject, isArray, result } from 'underscore';
@ -81,103 +107,103 @@ export default () => {
{
id: 'cell',
model: ComponentTableCell,
view: ComponentTableCellView
view: ComponentTableCellView,
},
{
id: 'row',
model: ComponentTableRow,
view: ComponentTableRowView
view: ComponentTableRowView,
},
{
id: 'table',
model: ComponentTable,
view: ComponentTableView
view: ComponentTableView,
},
{
id: 'thead',
model: ComponentTableHead,
view: ComponentTableHeadView
view: ComponentTableHeadView,
},
{
id: 'tbody',
model: ComponentTableBody,
view: ComponentTableBodyView
view: ComponentTableBodyView,
},
{
id: 'tfoot',
model: ComponentTableFoot,
view: ComponentTableFootView
view: ComponentTableFootView,
},
{
id: 'map',
model: ComponentMap,
view: ComponentMapView
view: ComponentMapView,
},
{
id: 'link',
model: ComponentLink,
view: ComponentLinkView
view: ComponentLinkView,
},
{
id: 'label',
model: ComponentLabel,
view: ComponentLabelView
view: ComponentLabelView,
},
{
id: 'video',
model: ComponentVideo,
view: ComponentVideoView
view: ComponentVideoView,
},
{
id: 'image',
model: ComponentImage,
view: ComponentImageView
view: ComponentImageView,
},
{
id: 'script',
model: ComponentScript,
view: ComponentScriptView
view: ComponentScriptView,
},
{
id: 'svg-in',
model: ComponentSvgIn,
view: ComponentSvgView
view: ComponentSvgView,
},
{
id: 'svg',
model: ComponentSvg,
view: ComponentSvgView
view: ComponentSvgView,
},
{
id: 'iframe',
model: ComponentFrame,
view: ComponentFrameView
view: ComponentFrameView,
},
{
id: 'comment',
model: ComponentComment,
view: ComponentCommentView
view: ComponentCommentView,
},
{
id: 'textnode',
model: ComponentTextNode,
view: ComponentTextNodeView
view: ComponentTextNodeView,
},
{
id: 'text',
model: ComponentText,
view: ComponentTextView
view: ComponentTextView,
},
{
id: 'wrapper',
model: ComponentWrapper,
view: ComponentView
view: ComponentView,
},
{
id: 'default',
model: Component,
view: ComponentView
}
view: ComponentView,
},
];
return {
@ -250,12 +276,8 @@ export default () => {
em.on('change:componentHovered', this.componentHovered, this);
const selected = em.get('selected');
em.listenTo(selected, 'add', (sel, c, opts) =>
this.selectAdd(selected.getComponent(sel), opts)
);
em.listenTo(selected, 'remove', (sel, c, opts) =>
this.selectRemove(selected.getComponent(sel), opts)
);
em.listenTo(selected, 'add', (sel, c, opts) => this.selectAdd(selected.getComponent(sel), opts));
em.listenTo(selected, 'remove', (sel, c, opts) => this.selectRemove(selected.getComponent(sel), opts));
}
if (em.get('hasPages')) {
@ -368,7 +390,7 @@ export default () => {
* @return {Component} Root Component
* @example
* // Change background of the wrapper and set some attribute
* var wrapper = domComponents.getWrapper();
* var wrapper = cmp.getWrapper();
* wrapper.set('style', {'background-color': 'red'});
* wrapper.set('attributes', {'title': 'Hello!'});
*/
@ -383,7 +405,7 @@ export default () => {
* @return {Components} Collection of components
* @example
* // Let's add some component
* var wrapperChildren = domComponents.getComponents();
* var wrapperChildren = cmp.getComponents();
* var comp1 = wrapperChildren.add({
* style: { 'background-color': 'red'}
* });
@ -410,7 +432,7 @@ export default () => {
/**
* Add new components to the wrapper's children. It's the same
* as 'domComponents.getComponents().add(...)'
* as 'cmp.getComponents().add(...)'
* @param {Object|Component|Array<Object>} component Component/s to add
* @param {string} [component.tagName='div'] Tag name
* @param {string} [component.type=''] Type of the component. Available: ''(default), 'text', 'image'
@ -427,7 +449,7 @@ export default () => {
* @return {Component|Array<Component>} Component/s added
* @example
* // Example of a new component with some extra property
* var comp1 = domComponents.addComponent({
* var comp1 = cmp.addComponent({
* tagName: 'div',
* removable: true, // Can't remove it
* draggable: true, // Can't move it
@ -483,27 +505,13 @@ export default () => {
*/
addType(type, methods) {
const { em } = this;
const {
model = {},
view = {},
isComponent,
extend,
extendView,
extendFn = [],
extendFnView = []
} = methods;
const { model = {}, view = {}, isComponent, extend, extendView, extendFn = [], extendFnView = [] } = 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 typeToExtend = extendType ? extendType : compType ? compType : this.getType('default');
const modelToExt = typeToExtend.model;
const viewToExt = extendViewType
? extendViewType.view
: typeToExtend.view;
const viewToExt = extendViewType ? extendViewType.view : typeToExtend.view;
// Function for extending source object methods
const getExtendedObj = (fns, target, srcToExt) =>
@ -511,7 +519,7 @@ export default () => {
const fn = target[next];
const parentFn = srcToExt.prototype[next];
if (fn && parentFn) {
res[next] = function(...args) {
res[next] = function (...args) {
parentFn.bind(this)(...args);
fn.bind(this)(...args);
};
@ -527,14 +535,11 @@ export default () => {
...getExtendedObj(extendFn, model, modelToExt),
defaults: {
...(result(modelToExt.prototype, 'defaults') || {}),
...(result(model, 'defaults') || {})
}
...(result(model, 'defaults') || {}),
},
},
{
isComponent:
compType && !extendType && !isComponent
? modelToExt.isComponent
: isComponent || (() => 0)
isComponent: compType && !extendType && !isComponent ? modelToExt.isComponent : isComponent || (() => 0),
}
);
}
@ -542,7 +547,7 @@ export default () => {
if (typeof view === 'object') {
methods.view = viewToExt.extend({
...view,
...getExtendedObj(extendFnView, view, viewToExt)
...getExtendedObj(extendFnView, view, viewToExt),
});
}
@ -564,7 +569,7 @@ export default () => {
* Get component type.
* Read more about this in [Define New Component](https://grapesjs.com/docs/modules/Components.html#define-new-component)
* @param {string} type Component ID
* @return {Object} Component type defintion, eg. `{ model: ..., view: ... }`
* @return {Object} Component type definition, eg. `{ model: ..., view: ... }`
*/
getType(type) {
var df = componentTypes;
@ -603,11 +608,9 @@ export default () => {
selectAdd(component, opts = {}) {
if (component) {
component.set({
status: 'selected'
status: 'selected',
});
['component:selected', 'component:toggled'].forEach(event =>
this.em.trigger(event, component, opts)
);
['component:selected', 'component:toggled'].forEach(event => this.em.trigger(event, component, opts));
}
},
@ -616,11 +619,9 @@ export default () => {
const { em } = this;
component.set({
status: '',
state: ''
state: '',
});
['component:deselected', 'component:toggled'].forEach(event =>
this.em.trigger(event, component, opts)
);
['component:deselected', 'component:toggled'].forEach(event => this.em.trigger(event, component, opts));
}
},
@ -639,7 +640,7 @@ export default () => {
previous.get('status') == state &&
previous.set({
status: '',
state: ''
state: '',
});
model && isEmpty(model.get('status')) && model.set('status', state);
@ -649,12 +650,16 @@ export default () => {
return componentsById;
},
getById(id) {
return componentsById[id] || null;
},
destroy() {
const all = this.allById();
Object.keys(all).forEach(id => all[id] && all[id].remove());
componentView && componentView.remove();
[c, em, componentsById, component, componentView].forEach(i => (i = {}));
this.em = {};
}
},
};
};

237
src/dom_components/model/Component.js

@ -9,15 +9,9 @@ import {
forEach,
result,
bindAll,
keys
keys,
} from 'underscore';
import {
shallowDiff,
capitalize,
isEmptyObj,
isObject,
toLowerCase
} from 'utils/mixins';
import { shallowDiff, capitalize, isEmptyObj, isObject, toLowerCase } from 'utils/mixins';
import Styleable from 'domain_abstract/model/Styleable';
import { Model } from 'backbone';
import Components from './Components';
@ -61,10 +55,10 @@ export const keyUpdateInside = `${keyUpdate}-inside`;
* @property {Object} [attributes={}] Key-value object of the component's attributes, eg. `{ title: 'Hello' }` Default: `{}`
* @property {String} [name=''] Name of the component. Will be used, for example, in Layers and badges
* @property {Boolean} [removable=true] When `true` the component is removable from the canvas, default: `true`
* @property {Boolean|String} [draggable=true] Indicates if it's possible to drag the component inside others.
* @property {Boolean|String|Function} [draggable=true] Indicates if it's possible to drag the component inside others.
* You can also specify a query string to indentify elements,
* eg. `'.some-class[title=Hello], [data-gjs-type=column]'` means you can drag the component only inside elements
* containing `some-class` class and `Hello` title, and `column` components. Default: `true`
* containing `some-class` class and `Hello` title, and `column` components. In the case of a function, target and destination components are passed as arguments, return a Boolean to indicate if the drag is possible. Default: `true`
* @property {Boolean|String|Function} [droppable=true] Indicates if it's possible to drop other components inside. You can use
* a query string as with `draggable`. In the case of a function, target and destination components are passed as arguments, return a Boolean to indicate if the drop is possible. Default: `true`
* @property {Boolean} [badgable=true] Set to false if you don't want to see the badge (with the name) over the component. Default: `true`
@ -133,15 +127,10 @@ export default class Component extends Model.extend(Styleable) {
}
const propagate = this.get('propagate');
propagate &&
this.set('propagate', isArray(propagate) ? propagate : [propagate]);
propagate && this.set('propagate', isArray(propagate) ? propagate : [propagate]);
// Check void elements
if (
opt &&
opt.config &&
opt.config.voidElements.indexOf(this.get('tagName')) >= 0
) {
if (opt && opt.config && opt.config.voidElements.indexOf(this.get('tagName')) >= 0) {
this.set('void', true);
}
@ -152,7 +141,7 @@ export default class Component extends Model.extend(Styleable) {
this.config = opt.config || {};
this.set('attributes', {
...(result(this, 'defaults').attributes || {}),
...(this.get('attributes') || {})
...(this.get('attributes') || {}),
});
this.ccid = Component.createId(this, opt);
this.initClasses();
@ -173,9 +162,7 @@ export default class Component extends Model.extend(Styleable) {
// Register global updates for collection properties
['classes', 'traits', 'components'].forEach(name => {
const events = `add remove ${name !== 'components' ? 'change' : ''}`;
this.listenTo(this.get(name), events.trim(), (...args) =>
this.emitUpdate(name, ...args)
);
this.listenTo(this.get(name), events.trim(), (...args) => this.emitUpdate(name, ...args));
});
if (!opt.temporary) {
@ -217,9 +204,7 @@ export default class Component extends Model.extend(Styleable) {
__onChange(m, opts) {
const changed = this.changedAttributes();
['status', 'open', 'toolbar', 'traits'].forEach(
name => delete changed[name]
);
['status', 'open', 'toolbar', 'traits'].forEach(name => delete changed[name]);
// Propagate component prop changes
if (!isEmptyObj(changed)) {
this.__changesUp(opts);
@ -422,9 +407,7 @@ export default class Component extends Model.extend(Styleable) {
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 => this.trigger(`change:attributes:${pr}`, this, diff[pr], opts));
}
/**
@ -452,7 +435,7 @@ export default class Component extends Model.extend(Styleable) {
return this.setAttributes(
{
...this.getAttributes({ noClass: 1 }),
...attrs
...attrs,
},
opts
);
@ -508,12 +491,7 @@ export default class Component extends Model.extend(Styleable) {
const em = this.em;
const { opt } = this;
if (
em &&
em.getConfig('avoidInlineStyle') &&
!opt.temporary &&
!opts.inline
) {
if (em && em.getConfig('avoidInlineStyle') && !opt.temporary && !opts.inline) {
const style = this.get('style') || {};
prop = isString(prop) ? this.parseStyle(prop) : prop;
prop = { ...prop, ...style };
@ -544,9 +522,7 @@ export default class Component extends Model.extend(Styleable) {
// Add classes
if (!opts.noClass) {
this.get('classes').forEach(cls =>
classes.push(isString(cls) ? cls : cls.get('name'))
);
this.get('classes').forEach(cls => classes.push(isString(cls) ? cls : cls.get('name')));
classes.length && (attributes.class = classes.join(' '));
}
@ -674,18 +650,12 @@ export default class Component extends Model.extend(Styleable) {
__isSymbolTop() {
const parent = this.parent();
const symb = this.__isSymbolOrInst();
return (
symb &&
(!parent || (parent && !parent.__isSymbol() && !parent.__getSymbol()))
);
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 symbTopSelf = (this.__isSymbol() ? this : this.__getSymbol()).__getSymbTop();
const symbTop = this.__getSymbTop();
const symbTopMain = symbTop.__isSymbol() ? symbTop : symbTop.__getSymbol();
return symbTopMain !== symbTopSelf;
@ -727,9 +697,7 @@ export default class Component extends Model.extend(Styleable) {
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))
);
return ovrd === true || (isArray(ovrd) && props.some(p => ovrd.indexOf(p) >= 0));
}
__getSymbToUp(opts = {}) {
@ -822,7 +790,7 @@ export default class Component extends Model.extend(Styleable) {
if (!o) {
const toUp = this.__getSymbToUp({
...toUpOpts,
changed: 'components:reset'
changed: 'components:reset',
});
this.__logSymbol('reset', toUp, { components: m.models });
toUp.forEach(symb => {
@ -835,12 +803,11 @@ export default class Component extends Model.extend(Styleable) {
const isMainSymb = !!this.__getSymbols();
const toUp = this.__getSymbToUp({
...toUpOpts,
changed: 'components:add'
changed: 'components:add',
});
if (toUp.length) {
const addSymb = m.__getSymbol();
addedInstances =
(addSymb ? addSymb.__getSymbols() : m.__getSymbols()) || [];
addedInstances = (addSymb ? addSymb.__getSymbols() : m.__getSymbols()) || [];
addedInstances = [...addedInstances];
addedInstances.push(addSymb ? addSymb : m);
}
@ -848,9 +815,9 @@ export default class Component extends Model.extend(Styleable) {
this.__logSymbol('add', toUp, {
opts: o,
addedInstances: addedInstances.map(c => c.cid),
added: m.cid
added: m.cid,
});
// Here, before appending a new symbol, I have to ensure there are no previosly
// 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();
@ -858,8 +825,7 @@ export default class Component extends Model.extend(Styleable) {
const addedTop = addedInst.__getSymbTop({ prev: 1 });
return symbTop && addedTop && addedTop === symbTop;
})[0];
const toAppend =
symbPrev || m.clone({ symbol: 1, symbolInv: isMainSymb });
const toAppend = symbPrev || m.clone({ symbol: 1, symbolInv: isMainSymb });
symb.append(toAppend, { fromInstance: this, ...o });
});
// Remove
@ -885,9 +851,7 @@ export default class Component extends Model.extend(Styleable) {
symbPrnt && !symbPrnt.__isSymbOvrd(changed) && symb.remove(opts);
};
// Check if the parent allows the removing
let toUp = !parent.__isSymbOvrd(changed)
? m.__getSymbToUp(toUpOpts)
: [];
let toUp = !parent.__isSymbOvrd(changed) ? m.__getSymbToUp(toUpOpts) : [];
if (isSymbNested) {
toUp = parent.__getSymbToUp({ ...toUpOpts, changed });
@ -901,7 +865,7 @@ export default class Component extends Model.extend(Styleable) {
this.__logSymbol('remove', toUp, {
opts: o,
removed: m.cid,
isSymbNested
isSymbNested,
});
toUp.forEach(toUpFn);
}
@ -937,12 +901,7 @@ export default class Component extends Model.extend(Styleable) {
const components = this.get('components');
const addChild = !this.opt.avoidChildren;
this.set('components', comps);
addChild &&
components &&
comps.add(
isFunction(components) ? components(this) : components,
this.opt
);
addChild && components && comps.add(isFunction(components) ? components(this) : components, this.opt);
comps.on('add remove reset', this.__upSymbComps);
this.listenTo(...toListen);
return this;
@ -951,9 +910,8 @@ export default class Component extends Model.extend(Styleable) {
initTraits(changed) {
const { em } = this;
const event = 'change:traits';
const toListen = [this, event, this.initTraits];
this.stopListening(...toListen);
this.loadTraits();
this.off(event, this.initTraits);
this.__loadTraits();
const attrs = { ...this.get('attributes') };
const traits = this.get('traits');
traits.each(trait => {
@ -964,7 +922,7 @@ export default class Component extends Model.extend(Styleable) {
}
});
traits.length && this.set('attributes', attrs);
this.listenTo(...toListen);
this.on(event, this.initTraits);
changed && em && em.trigger('component:toggled');
return this;
}
@ -1112,75 +1070,100 @@ export default class Component extends Model.extend(Styleable) {
if (model.collection) {
tb.push({
attributes: { class: 'fa fa-arrow-up' },
command: ed => ed.runCommand('core:component-exit', { force: 1 })
command: ed => ed.runCommand('core:component-exit', { force: 1 }),
});
}
if (model.get('draggable')) {
tb.push({
attributes: {
class: `fa fa-arrows ${ppfx}no-touch-actions`,
draggable: true
draggable: true,
},
//events: hasDnd(this.em) ? { dragstart: 'execCommand' } : '',
command: 'tlb-move'
command: 'tlb-move',
});
}
if (model.get('copyable')) {
tb.push({
attributes: { class: 'fa fa-clone' },
command: 'tlb-clone'
command: 'tlb-clone',
});
}
if (model.get('removable')) {
tb.push({
attributes: { class: 'fa fa-trash-o' },
command: 'tlb-delete'
command: 'tlb-delete',
});
}
model.set('toolbar', tb);
}
}
/**
* Load traits
* @param {Array} traits
* @private
*/
loadTraits(traits, opts = {}) {
traits = traits || this.get('traits');
traits = isFunction(traits) ? traits(this) : traits;
__loadTraits(tr, opts = {}) {
let traitsI = tr || this.get('traits');
if (!(traits instanceof Traits)) {
const trt = new Traits([], this.opt);
trt.setTarget(this);
if (!(traitsI instanceof Traits)) {
traitsI = isFunction(traitsI) ? traitsI(this) : traitsI;
const traits = new Traits([], this.opt);
traits.setTarget(this);
if (traits.length) {
traits.forEach(tr => tr.attributes && delete tr.attributes.value);
trt.add(traits);
if (traitsI.length) {
traitsI.forEach(tr => tr.attributes && delete tr.attributes.value);
traits.add(traitsI);
}
this.set('traits', trt, opts);
this.set({ traits }, opts);
}
return this;
}
/**
* Get the trait by id/name
* Get traits.
* @returns {Array<Trait>}
* @example
* const traits = component.getTraits();
* console.log(traits);
* // [Trait, Trait, Trait, ...]
*/
getTraits() {
this.__loadTraits();
return [...this.get('traits').models];
}
/**
* Replace current collection of traits with a new one.
* @param {Array<Object>} traits Array of trait definitions
* @returns {Array<Trait>}
* @example
* const traits = component.setTraits([{ type: 'checkbox', name: 'disabled'}, ...]);
* console.log(traits);
* // [Trait, ...]
*/
setTraits(traits) {
const tr = isArray(traits) ? traits : [traits];
this.set({ traits: tr });
return this.getTraits();
}
/**
* Get the trait by id/name.
* @param {String} id The `id` or `name` of the trait
* @return {Trait} Trait model
* @return {Trait|null} Trait getModelToStyle
* @example
* const traitTitle = component.getTrait('title');
* traitTitle && traitTitle.set('label', 'New label');
*/
getTrait(id) {
return this.get('traits').filter(trait => {
return trait.get('id') === id || trait.get('name') === id;
})[0];
return (
this.getTraits().filter(trait => {
return trait.get('id') === id || trait.get('name') === id;
})[0] || null
);
}
/**
* Update a trait
* Update a trait.
* @param {String} id The `id` or `name` of the trait
* @param {Object} props Object with the props to update
* @return {this}
@ -1191,10 +1174,9 @@ export default class Component extends Model.extend(Styleable) {
* });
*/
updateTrait(id, props) {
const { em } = this;
const trait = this.getTrait(id);
trait && trait.set(props);
em && em.trigger('component:toggled');
this.em?.trigger('component:toggled');
return this;
}
@ -1215,25 +1197,25 @@ export default class Component extends Model.extend(Styleable) {
/**
* Remove trait/s by id/s.
* @param {String|Array<String>} id The `id`/`name` of the trait (or an array)
* @return {Array} Array of removed traits
* @return {Array<Trait>} Array of removed traits
* @example
* component.removeTrait('title');
* component.removeTrait(['title', 'id']);
*/
removeTrait(id) {
const { em } = this;
const ids = isArray(id) ? id : [id];
const toRemove = ids.map(id => this.getTrait(id));
const removed = this.get('traits').remove(toRemove);
em && em.trigger('component:toggled');
return removed;
const traits = this.get('traits');
const removed = toRemove.length ? traits.remove(toRemove) : [];
this.em?.trigger('component:toggled');
return isArray(removed) ? removed : [removed];
}
/**
* Add trait/s by id/s.
* Add new trait/s.
* @param {String|Object|Array<String|Object>} trait Trait to add (or an array of traits)
* @param {Options} opts Options for the add
* @return {Array} Array of added traits
* @return {Array<Trait>} Array of added traits
* @example
* component.addTrait('title', { at: 1 }); // Add title trait (`at` option is the position index)
* component.addTrait({
@ -1243,10 +1225,10 @@ export default class Component extends Model.extend(Styleable) {
* component.addTrait(['title', {...}, ...]);
*/
addTrait(trait, opts = {}) {
const { em } = this;
this.__loadTraits();
const added = this.get('traits').add(trait, opts);
em && em.trigger('component:toggled');
return added;
this.em?.trigger('component:toggled');
return isArray(added) ? added : [added];
}
/**
@ -1364,8 +1346,7 @@ export default class Component extends Model.extend(Styleable) {
const i18nPfx = 'domComponents.names.';
const i18nName = cName && em && em.t(`${i18nPfx}${cName}`);
const i18nNameTag = nameTag && em && em.t(`${i18nPfx}${nameTag}`);
const i18nDefName =
em && (em.t(`${i18nPfx}${type}`) || em.t(`${i18nPfx}${tagName}`));
const i18nDefName = em && (em.t(`${i18nPfx}${type}`) || em.t(`${i18nPfx}${tagName}`));
return (
this.get('custom-name') || // Used in Layers (when the user changes the name)
i18nName ||
@ -1457,9 +1438,7 @@ export default class Component extends Model.extend(Styleable) {
__innerHTML(opts = {}) {
const cmps = this.components();
return !cmps.length
? this.get('content')
: cmps.map(c => c.toHTML(opts)).join('');
return !cmps.length ? this.get('content') : cmps.map(c => c.toHTML(opts)).join('');
}
/**
@ -1492,9 +1471,7 @@ export default class Component extends Model.extend(Styleable) {
const symbol = obj[keySymbol];
const symbols = obj[keySymbols];
if (symbols && isArray(symbols)) {
obj[keySymbols] = symbols
.filter(i => i)
.map(i => (i.getId ? i.getId() : i));
obj[keySymbols] = symbols.filter(i => i).map(i => (i.getId ? i.getId() : i));
}
if (symbol && !isString(symbol)) {
obj[keySymbol] = symbol.getId();
@ -1623,9 +1600,7 @@ export default class Component extends Model.extend(Styleable) {
// Need to convert script functions to strings
if (typeof scr == 'function') {
var scrStr = scr.toString().trim();
scrStr = scrStr
.replace(/^function[\s\w]*\(\)\s?\{/, '')
.replace(/\}$/, '');
scrStr = scrStr.replace(/^function[\s\w]*\(\)\s?\{/, '').replace(/\}$/, '');
scr = scrStr.trim();
}
@ -1638,9 +1613,7 @@ export default class Component extends Model.extend(Styleable) {
// better optimization inside JS generator
this.scriptUpdated();
const result = this.attributes[v] || '';
return isArray(result) || typeof result == 'object'
? JSON.stringify(result)
: result;
return isArray(result) || typeof result == 'object' ? JSON.stringify(result) : result;
});
}
return scr;
@ -1650,20 +1623,14 @@ export default class Component extends Model.extend(Styleable) {
const { em } = this;
const event = keyUpdate + (property ? `:${property}` : '');
const item = property && this.get(property);
property &&
this.updated(
property,
item,
property && this.previous(property),
...args
);
property && this.updated(property, item, property && this.previous(property), ...args);
this.trigger(event, ...args);
em && em.trigger(event, this, ...args);
['components', 'classes'].indexOf(property) >= 0 &&
this.__propSelfToParent({
component: this,
changed: { [property]: item },
options: args[2] || args[1] || {}
options: args[2] || args[1] || {},
});
}
@ -1696,9 +1663,7 @@ export default class Component extends Model.extend(Styleable) {
opts.root && this.components('');
};
const rmOpts = { ...opts };
[this, em].map(i =>
i.trigger('component:remove:before', this, remove, rmOpts)
);
[this, em].map(i => i.trigger('component:remove:before', this, remove, rmOpts));
!rmOpts.abort && remove();
return this;
}
@ -1860,6 +1825,7 @@ Component.getIncrementId = (id, list, opts = {}) => {
* The list of components is taken from the Components module.
* Initially, the list, was set statically on the Component object but it was
* not ok, as it was shared between multiple editor instances
* @private
*/
Component.getList = model => {
const { opt = {} } = model;
@ -1872,6 +1838,7 @@ Component.getList = model => {
* This method checks, for each parsed component and style object
* (are not Components/CSSRules yet), for duplicated id and fixes them
* This method is used in Components.js just after the parsing
* @private
*/
Component.checkId = (components, styles = [], list = {}, opts = {}) => {
const comps = isArray(components) ? components : [components];
@ -1898,7 +1865,7 @@ Component.checkId = (components, styles = [], list = {}, opts = {}) => {
});
};
Component.getDefaults = function() {
Component.getDefaults = function () {
return result(this.prototype, 'defaults');
};
@ -1941,5 +1908,5 @@ Component.prototype.defaults = {
[keySymbols]: 0,
[keySymbolOvrd]: 0,
_undo: true,
_undoexc: ['status', 'open']
_undoexc: ['status', 'open'],
};

37
src/domain_abstract/model/Styleable.js

@ -1,4 +1,4 @@
import { isString, isArray, keys } from 'underscore';
import { isString, isArray, keys, isUndefined } from 'underscore';
import { shallowDiff } from 'utils/mixins';
import ParserHtml from 'parser/model/ParserHtml';
@ -44,10 +44,11 @@ export default {
const diff = shallowDiff(propOrig, propNew);
keys(diff).forEach(pr => {
const em = this.em;
if (opts.noEvent) return;
this.trigger(`change:style:${pr}`);
if (em) {
em.trigger(`styleable:change`, this, pr);
em.trigger(`styleable:change:${pr}`, this, pr);
em.trigger(`styleable:change`, this, pr, opts);
em.trigger(`styleable:change:${pr}`, this, pr, opts);
}
});
@ -65,7 +66,7 @@ export default {
addStyle(prop, value = '', opts = {}) {
if (typeof prop == 'string') {
prop = {
prop: value
prop: value,
};
} else {
opts = value || {};
@ -109,9 +110,27 @@ export default {
return this.get('selectors') || this.get('classes');
},
getSelectorsString() {
return this.selectorsToString
? this.selectorsToString()
: this.getSelectors().getFullString();
}
getSelectorsString(opts) {
return this.selectorsToString ? this.selectorsToString(opts) : this.getSelectors().getFullString();
},
_validate(attr, opts) {
const { style } = attr;
const em = this.em || opts.em;
const onBeforeStyle = em?.get('CssComposer')?.getConfig().onBeforeStyle;
if (style && onBeforeStyle) {
const newStyle = onBeforeStyle({ ...style });
newStyle &&
keys(style).map(prop => {
if (isUndefined(newStyle[prop])) delete attr.style[prop];
});
newStyle &&
keys(newStyle).map(prop => {
attr.style[prop] = newStyle[prop];
});
}
return true;
},
};

30
src/domain_abstract/model/TypeableCollection.js

@ -7,7 +7,10 @@ const View = Backbone.View;
export default {
types: [],
initialize(models, opts) {
initialize(models, opts = {}) {
const { em } = opts;
this.em = em;
this.opts = opts;
this.model = (attrs = {}, options = {}) => {
let Model, View, type;
@ -24,7 +27,7 @@ export default {
attrs = typeFound.attributes;
}
const model = new Model(attrs, options);
const model = new Model(attrs, { ...options, em });
model.typeView = View;
return model;
};
@ -43,15 +46,12 @@ export default {
for (let i = 0; i < types.length; i++) {
const type = types[i];
let typeFound = type.isType(value);
typeFound =
typeof typeFound == 'boolean' && typeFound
? { type: type.id }
: typeFound;
typeFound = typeof typeFound == 'boolean' && typeFound ? { type: type.id } : typeFound;
if (typeFound) {
return {
type,
attributes: typeFound
attributes: typeFound,
};
}
}
@ -59,7 +59,7 @@ export default {
// If, for any reason, the type is not found it'll return the base one
return {
type: this.getBaseType(),
attributes: value
attributes: value,
};
},
@ -115,14 +115,8 @@ export default {
const ModelInst = type ? type.model : baseType.model;
const ViewInst = type ? type.view : baseType.view;
let { model, view, isType } = definition;
model =
model instanceof Model || isFunction(model)
? model
: ModelInst.extend(model || {});
view =
view instanceof View || isFunction(view)
? view
: ViewInst.extend(view || {});
model = model instanceof Model || isFunction(model) ? model : ModelInst.extend(model || {});
view = view instanceof View || isFunction(view) ? view : ViewInst.extend(view || {});
// New API
if (this.extendViewApi && !definition.model && !definition.view) {
@ -139,12 +133,12 @@ export default {
definition.view = view;
definition.isType =
isType ||
function(value) {
function (value) {
if (value && value.type == id) {
return true;
}
};
this.getTypes().unshift(definition);
}
}
},
};

12
src/domain_abstract/ui/Input.js

@ -4,7 +4,7 @@ const $ = Backbone.$;
export default Backbone.View.extend({
events: {
change: 'handleChange'
change: 'handleChange',
},
template() {
@ -24,7 +24,7 @@ export default Backbone.View.extend({
this.opts = opts;
this.ppfx = ppfx;
this.em = opts.target || {};
this.listenTo(this.model, 'change:value', this.handleModelChange);
!opts.onChange && this.listenTo(this.model, 'change:value', this.handleModelChange);
},
/**
@ -74,11 +74,7 @@ export default Backbone.View.extend({
if (!this.inputEl) {
const { model, opts } = this;
const type = opts.type || 'text';
const plh =
model.get('placeholder') ||
model.get('defaults') ||
model.get('default') ||
'';
const plh = model.get('placeholder') || model.get('defaults') || model.get('default') || '';
this.inputEl = $(`<input type="${type}" placeholder="${plh}">`);
}
@ -92,5 +88,5 @@ export default Backbone.View.extend({
el.html(this.template());
el.find(`.${this.holderClass()}`).append(this.getInputEl());
return this;
}
},
});

42
src/domain_abstract/ui/InputColor.js

@ -39,7 +39,16 @@ export default Input.extend({
this.colorEl.spectrum('destroy');
},
handleChange(e) {
e.stopPropagation();
const { value } = e.target;
if (isUndefined(value)) return;
this.__onInputChange(value);
},
__onInputChange(val) {
const { model, opts } = this;
const { onChange } = opts;
let value = val;
const colorEl = this.getColorEl();
@ -51,7 +60,7 @@ export default Input.extend({
color && (value = color);
}
this.model.set({ value }, { fromInput: 1 });
onChange ? onChange(value) : model.set({ value }, { fromInput: 1 });
},
/**
@ -60,8 +69,8 @@ export default Input.extend({
* @param {Object} opts
*/
setValue(val, opts = {}) {
const model = this.model;
const def = model.get('defaults');
const { model } = this;
const def = !isUndefined(opts.def) ? opts.def : model.get('defaults');
const value = !isUndefined(val) ? val : !isUndefined(def) ? def : '';
const inputEl = this.getInputEl();
const colorEl = this.getColorEl();
@ -82,16 +91,15 @@ export default Input.extend({
*/
getColorEl() {
if (!this.colorEl) {
const { em } = this;
const { em, model, opts } = this;
const self = this;
const ppfx = this.ppfx;
var model = this.model;
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')) || {};
var colorPickerConfig = (em && em.getConfig && em.getConfig('colorPicker')) || {};
let changed = 0;
let previousColor;
@ -114,30 +122,34 @@ export default Input.extend({
move(color) {
const cl = getColor(color);
cpStyle.backgroundColor = cl;
model.setValueFromInput(cl, 0);
onChange ? onChange(cl, true) : model.setValueFromInput(cl, 0);
},
change(color) {
changed = 1;
const cl = getColor(color);
cpStyle.backgroundColor = cl;
model.setValueFromInput(0, 0); // for UndoManager
model.setValueFromInput(cl);
if (onChange) {
onChange(cl);
} else {
model.setValueFromInput(0, 0); // for UndoManager
model.setValueFromInput(cl);
}
self.noneColor = 0;
},
show(color) {
changed = 0;
previousColor = getColor(color);
previousColor = onChange ? model.getValue({ noDefault: true }) : getColor(color);
},
hide(color) {
if (!changed && previousColor) {
if (!changed && (previousColor || onChange)) {
if (self.noneColor) {
previousColor = '';
}
cpStyle.backgroundColor = previousColor;
colorEl.spectrum('set', previousColor);
model.setValueFromInput(previousColor, 0);
onChange ? onChange(previousColor, true) : model.setValueFromInput(previousColor, 0);
}
}
},
});
if (em && em.on) {
@ -157,5 +169,5 @@ export default Input.extend({
// This will make the color input available on render
this.getColorEl();
return this;
}
},
});

36
src/domain_abstract/ui/InputNumber.js

@ -12,7 +12,7 @@ export default Input.extend({
'click [data-arrow-up]': 'upArrowClick',
'click [data-arrow-down]': 'downArrowClick',
'mousedown [data-arrows]': 'downIncrement',
keydown: 'handleKeyDown'
keydown: 'handleKeyDown',
},
template() {
@ -124,7 +124,7 @@ export default Input.extend({
const units = model.get('units') || [];
if (units.length) {
const options = [];
const options = ['<option value="" disabled hidden>-</option>'];
units.forEach(unit => {
const selected = unit == model.get('unit') ? 'selected' : '';
@ -132,9 +132,7 @@ export default Input.extend({
});
const temp = document.createElement('div');
temp.innerHTML = `<select class="${this.ppfx}input-unit">${options.join(
''
)}</select>`;
temp.innerHTML = `<select class="${this.ppfx}input-unit">${options.join('')}</select>`;
this.unitEl = temp.firstChild;
}
}
@ -177,7 +175,7 @@ export default Input.extend({
downIncrement(e) {
e.preventDefault();
this.moved = 0;
var value = this.model.get('value');
var value = this.model.get('value') || 0;
value = this.normalizeValue(value);
this.current = { y: e.pageY, val: value };
on(this.doc, 'mousemove', this.moveIncrement);
@ -241,22 +239,24 @@ export default Input.extend({
* @param {Object} opts Options
* @return {Object} Validated string
*/
validateInputValue(value, opts) {
validateInputValue(value, opts = {}) {
var force = 0;
var opt = opts || {};
var model = this.model;
const defValue = ''; //model.get('defaults');
var val = !isUndefined(value) ? value : defValue;
var units = model.get('units') || [];
var units = opts.units || model.get('units') || [];
var unit = model.get('unit') || (units.length && units[0]) || '';
var max = model.get('max');
var min = model.get('min');
var max = !isUndefined(opts.max) ? opts.max : model.get('max');
var min = !isUndefined(opts.min) ? opts.min : model.get('min');
var limitlessMax = !!model.get('limitlessMax');
var limitlessMin = !!model.get('limitlessMin');
if (opt.deepCheck) {
var fixed = model.get('fixedValues') || [];
if (val === '') unit = '';
if (val) {
// If the value is one of the fixed values I leave it as it is
var regFixed = new RegExp('^' + fixed.join('|'), 'g');
@ -276,15 +276,13 @@ export default Input.extend({
}
}
if (!limitlessMax && !isUndefined(max) && max !== '')
val = val > max ? max : val;
if (!limitlessMin && !isUndefined(min) && min !== '')
val = val < min ? min : val;
if (!limitlessMax && !isUndefined(max) && max !== '') val = val > max ? max : val;
if (!limitlessMin && !isUndefined(min) && min !== '') val = val < min ? min : val;
return {
force,
value: val,
unit
unit,
};
},
@ -292,11 +290,7 @@ export default Input.extend({
Input.prototype.render.call(this);
this.unitEl = null;
const unit = this.getUnitEl();
unit &&
this.$el
.find(`.${this.ppfx}field-units`)
.get(0)
.appendChild(unit);
unit && this.$el.find(`.${this.ppfx}field-units`).get(0).appendChild(unit);
return this;
}
},
});

102
src/editor/config/config.js

@ -163,8 +163,16 @@ export default {
listenToEl: [],
// Import asynchronously CSS to use as icons
cssIcons:
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css',
cssIcons: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css',
// Experimental: don't use.
icons: {
close:
'<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"></path></svg>',
move: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M13,6V11H18V7.75L22.25,12L18,16.25V13H13V18H16.25L12,22.25L7.75,18H11V13H6V16.25L1.75,12L6,7.75V11H11V6H7.75L12,1.75L16.25,6H13Z"/></svg>',
plus: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" /></svg>',
caret: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M7,10L12,15L17,10H7Z" /></svg>',
},
// Dom element
el: '',
@ -215,93 +223,7 @@ export default {
deviceManager: {},
//Configurations for Style Manager
styleManager: {
sectors: [
{
name: 'General',
open: false,
buildProps: [
'float',
'display',
'position',
'top',
'right',
'left',
'bottom'
]
},
{
name: 'Flex',
open: false,
buildProps: [
'flex-direction',
'flex-wrap',
'justify-content',
'align-items',
'align-content',
'order',
'flex-basis',
'flex-grow',
'flex-shrink',
'align-self'
]
},
{
name: 'Dimension',
open: false,
buildProps: [
'width',
'height',
'max-width',
'min-height',
'margin',
'padding'
]
},
{
name: 'Typography',
open: false,
buildProps: [
'font-family',
'font-size',
'font-weight',
'letter-spacing',
'color',
'line-height',
'text-align',
'text-shadow'
],
properties: [
{
property: 'text-align',
list: [
{ value: 'left', className: 'fa fa-align-left' },
{ value: 'center', className: 'fa fa-align-center' },
{ value: 'right', className: 'fa fa-align-right' },
{ value: 'justify', className: 'fa fa-align-justify' }
]
}
]
},
{
name: 'Decorations',
open: false,
buildProps: [
'border-radius-c',
'background-color',
'border-radius',
'border',
'box-shadow',
'background'
]
},
{
name: 'Extra',
open: false,
buildProps: ['transition', 'perspective', 'transform']
}
]
},
styleManager: {},
// Configurations for Block Manager
blockManager: {},
@ -320,5 +242,5 @@ export default {
// Experimental: don't use.
// Avoid default UI styles
customUI: false
customUI: false,
};

80
src/editor/index.js

@ -17,64 +17,25 @@
* })
* ```
*
* * `update` - The structure of the template is updated (its HTML/CSS)
* * `undo` - Undo executed
* * `redo` - Redo executed
* * `load` - Editor is loaded
*
* ### Components
* * `component:create` - Component is created (only the model, is not yet mounted in the canvas), called after the init() method
* * `component:mount` - Component is mounted to an element and rendered in canvas
* * `component:add` - Triggered when a new component is added to the editor, the model is passed as an argument to the callback
* * `component:remove` - Triggered when a component is removed, the model is passed as an argument to the callback
* * `component:remove:before` - Triggered before the remove of the component, the model, remove function (if aborted via options, with this function you can complete the remove) and options (use options.abort = true to prevent remove), are passed as arguments to the callback
* * `component:clone` - Triggered when a component is cloned, the new model is passed as an argument to the callback
* * `component:update` - Triggered when a component is updated (moved, styled, etc.), the model is passed as an argument to the callback
* * `component:update:{propertyName}` - Listen any property change, the model is passed as an argument to the callback
* * `component:styleUpdate` - Triggered when the style of the component is updated, the model is passed as an argument to the callback
* * `component:styleUpdate:{propertyName}` - Listen for a specific style property change, the model is passed as an argument to the callback
* * `component:selected` - New component selected, the selected model is passed as an argument to the callback
* * `component:deselected` - Component deselected, the deselected model is passed as an argument to the callback
* * `component:toggled` - Component selection changed, toggled model is passed as an argument to the callback
* * `component:type:add` - New component type added, the new type is passed as an argument to the callback
* * `component:type:update` - Component type updated, the updated type is passed as an argument to the callback
* * `component:drag:start` - Component drag started. Passed an object, to the callback, containing the `target` (component to drag), `parent` (parent of the component) and `index` (component index in the parent)
* * `component:drag` - During component drag. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the current pointer
* * `component:drag:end` - Component drag ended. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the final pointer
* Check the [Components](/api/components.html) module.
* ### Keymaps
* * `keymap:add` - New keymap added. The new keyamp object is passed as an argument
* * `keymap:remove` - Keymap removed. The removed keyamp object is passed as an argument
* * `keymap:emit` - Some keymap emitted, in arguments you get keymapId, shortcutUsed, Event
* * `keymap:emit:{keymapId}` - `keymapId` emitted, in arguments you get keymapId, shortcutUsed, Event
* Check the [Keymaps](/api/keymaps.html) module.
* ### Style Manager
* * `styleManager:update:target` - The target (Component or CSSRule) is changed
* * `styleManager:change` - Triggered on style property change from new selected component, the view of the property is passed as an argument to the callback
* * `styleManager:change:{propertyName}` - As above but for a specific style property
* ### Storages
* * `storage:start` - Before the storage request is started
* * `storage:start:store` - Before the store request. The object to store is passed as an argumnet (which you can edit)
* * `storage:start:load` - Before the load request. Items to load are passed as an argumnet (which you can edit)
* * `storage:load` - Triggered when something was loaded from the storage, loaded object passed as an argumnet
* * `storage:store` - Triggered when something is stored to the storage, stored object passed as an argumnet
* * `storage:end` - After the storage request is ended
* * `storage:end:store` - After the store request
* * `storage:end:load` - After the load request
* * `storage:error` - On any error on storage request, passes the error as an argument
* * `storage:error:store` - Error on store request, passes the error as an argument
* * `storage:error:load` - Error on load request, passes the error as an argument
* Check the [Style Manager](/api/style_manager.html) module.
* ### Storage
* Check the [Storage](/api/storage_manager.html) module.
* ### Canvas
* * `canvas:dragenter` - When something is dragged inside the canvas, `DataTransfer` instance passed as an argument
* * `canvas:dragover` - When something is dragging on canvas, `DataTransfer` instance passed as an argument
* * `canvas:drop` - Something is dropped in canvas, `DataTransfer` instance and the dropped model are passed as arguments
* * `canvas:dragend` - When a drag operation is ended, `DataTransfer` instance passed as an argument
* * `canvas:dragdata` - On any dataTransfer parse, `DataTransfer` instance and the `result` are passed as arguments.
* By changing `result.content` you're able to customize what is dropped
* Check the [Canvas](/api/canvas.html) module.
* ### RTE
* * `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
* * `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument
* Check the [Rich Text Editor](/api/rich_text_editor.html) module.
* ### Commands
* * `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
* Check the [Commands](/api/commands.html) module.
* ### Selectors
* Check the [Selectors](/api/selector_manager.html) module.
* ### Blocks
@ -89,13 +50,8 @@
* Check the [Parser](/api/parser.html) module.
* ### Pages
* Check the [Pages](/api/pages.html) module.
* ### General
* * `canvasScroll` - Canvas is scrolled
* * `update` - The structure of the template is updated (its HTML/CSS)
* * `undo` - Undo executed
* * `redo` - Redo executed
* * `load` - Editor is loaded
*
* ## Methods
* @module Editor
*/
import defaults from './config/config';
@ -107,7 +63,7 @@ export default (config = {}, opts = {}) => {
const { $ } = opts;
const c = {
...defaults,
...config
...config,
};
c.pStylePrefix = c.stylePrefix;
@ -164,7 +120,7 @@ export default (config = {}, opts = {}) => {
'StyleManager',
['Styles', 'StyleManager'],
'DeviceManager',
['Devices', 'DeviceManager']
['Devices', 'DeviceManager'],
].forEach(prop => {
if (Array.isArray(prop)) {
this[prop[0]] = em.get(prop[1]);
@ -720,7 +676,7 @@ export default (config = {}, opts = {}) => {
editorView && editorView.remove();
editorView = new EditorView({
model: em,
config: c
config: c,
});
return editorView.render().el;
},
@ -749,6 +705,6 @@ export default (config = {}, opts = {}) => {
* // Use `$${var}` to avoid escaping
* const strHtml = editor.html`Escaped ${unsafeStr}, unescaped $${safeStr}`;
*/
html
html,
};
};

97
src/editor/model/Editor.js

@ -1,19 +1,10 @@
import {
isUndefined,
isFunction,
isArray,
contains,
toArray,
keys,
bindAll
} from 'underscore';
import { isUndefined, isFunction, isArray, contains, toArray, keys, bindAll } from 'underscore';
import Backbone from 'backbone';
import $ from 'utils/cash-dom';
import Extender from 'utils/extender';
import { getModel, hasWin } from 'utils/mixins';
import Selected from './Selected';
const cash = hasWin() ? require('cash-dom') : null;
const $ = (cash && cash.default) || cash;
Backbone.$ = $;
const deps = [
@ -38,7 +29,7 @@ const deps = [
require('navigator'),
require('canvas'),
require('commands'),
require('block_manager')
require('block_manager'),
];
let timedInterval;
@ -46,14 +37,14 @@ let updateItr;
Extender({
Backbone: Backbone,
$: Backbone.$
$: Backbone.$,
});
const logs = {
debug: console.log,
info: console.info,
warning: console.warn,
error: console.error
error: console.error,
};
export default Backbone.Model.extend({
@ -70,7 +61,7 @@ export default Backbone.Model.extend({
modules: [],
toLoad: [],
opened: {},
device: ''
device: '',
};
},
@ -104,26 +95,18 @@ export default Backbone.Model.extend({
toLog.forEach(e => this.listenLog(e));
// Deprecations
[{ from: 'change:selectedComponent', to: 'component:toggled' }].forEach(
event => {
const eventFrom = event.from;
const eventTo = event.to;
this.listenTo(this, eventFrom, (...args) => {
this.trigger(eventTo, ...args);
this.logWarning(
`The event '${eventFrom}' is deprecated, replace it with '${eventTo}'`
);
});
}
);
[{ from: 'change:selectedComponent', to: 'component:toggled' }].forEach(event => {
const eventFrom = event.from;
const eventTo = event.to;
this.listenTo(this, eventFrom, (...args) => {
this.trigger(eventTo, ...args);
this.logWarning(`The event '${eventFrom}' is deprecated, replace it with '${eventTo}'`);
});
});
},
_checkReady() {
if (
this.get('readyLoad') &&
this.get('readyCanvas') &&
!this.get('ready')
) {
if (this.get('readyLoad') && this.get('readyCanvas') && !this.get('ready')) {
this.set('ready', true);
}
},
@ -207,9 +190,7 @@ export default Backbone.Model.extend({
const Module = moduleName.default || moduleName;
const Mod = new Module();
const name = Mod.name.charAt(0).toLowerCase() + Mod.name.slice(1);
const cfgParent = !isUndefined(config[name])
? config[name]
: config[Mod.name];
const cfgParent = !isUndefined(config[name]) ? config[name] : config[Mod.name];
const cfg = cfgParent === true ? {} : cfgParent || {};
const sm = this.get('StorageManager');
cfg.pStylePrefix = config.pStylePrefix || '';
@ -263,7 +244,7 @@ export default Backbone.Model.extend({
* */
handleUpdates(model, val, opt = {}) {
// Component has been added temporarily - do not update storage or record changes
if (opt.temporary || opt.noCount || opt.avoidStore || !this.get('ready')) {
if (this.__skip || opt.temporary || opt.noCount || opt.avoidStore || !this.get('ready')) {
return;
}
@ -539,7 +520,7 @@ export default Backbone.Model.extend({
exportWrapper,
wrapperIsBody,
...optsHtml,
...opts
...opts,
})
: '';
html += js ? `<script>${js}</script>` : '';
@ -556,9 +537,7 @@ export default Backbone.Model.extend({
const config = this.config;
const { optsCss, wrapperIsBody } = config;
const avoidProt = opts.avoidProtected;
const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles)
? opts.keepUnusedStyles
: config.keepUnusedStyles;
const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) ? opts.keepUnusedStyles : config.keepUnusedStyles;
const cssc = this.get('CssComposer');
const wrp = opts.component || this.get('DomComponents').getComponent();
const protCss = !avoidProt ? config.protectedCss : '';
@ -569,7 +548,7 @@ export default Backbone.Model.extend({
wrapperIsBody,
keepUnusedStyles,
...optsCss,
...opts
...opts,
});
return wrp ? (opts.json ? css : protCss + css) : '';
},
@ -581,11 +560,7 @@ export default Backbone.Model.extend({
*/
getJs(opts = {}) {
var wrp = opts.component || this.get('DomComponents').getWrapper();
return wrp
? this.get('CodeManager')
.getCode(wrp, 'js')
.trim()
: '';
return wrp ? this.get('CodeManager').getCode(wrp, 'js').trim() : '';
},
/**
@ -784,7 +759,8 @@ export default Backbone.Model.extend({
},
t(...args) {
return this.get('I18n').t(...args);
const i18n = this.get('I18n');
return i18n?.t(...args);
},
/**
@ -802,23 +778,18 @@ export default Backbone.Model.extend({
const { config, view } = this;
const editor = this.getEditor();
const { editors = [] } = config.grapesjs || {};
this.stopListening();
this.stopDefault();
this.get('modules')
.slice()
.reverse()
.forEach(mod => mod.destroy());
view && view.remove();
this.stopListening();
this.clear({ silent: true });
this.destroyed = 1;
['config', 'view', '_previousAttributes', '_events', '_listeners'].forEach(
i => (this[i] = {})
);
['config', 'view', '_previousAttributes', '_events', '_listeners'].forEach(i => (this[i] = {}));
editors.splice(editors.indexOf(editor), 1);
hasWin() &&
$(config.el)
.empty()
.attr(this.attrsOrig);
hasWin() && $(config.el).empty().attr(this.attrsOrig);
},
getEditing() {
@ -875,10 +846,22 @@ export default Backbone.Model.extend({
chooseText: 'Ok',
cancelText: '⨯',
...opts,
...colorPicker
...colorPicker,
});
},
/**
* Execute actions without triggering the storage and undo manager.
* @param {Function} clb
* @private
*/
skip(clb) {
this.__skip = true;
const um = this.get('UndoManager');
um ? um.skip(clb) : clb();
this.__skip = false;
},
/**
* Set/get data from the HTMLElement
* @param {HTMLElement} el
@ -899,5 +882,5 @@ export default Backbone.Model.extend({
} else {
el[varName][name] = value;
}
}
},
});

11
src/i18n/config.js

@ -8,13 +8,16 @@ export default {
localeFallback: 'en',
// Detect locale by checking browser language
detectLocale: 1,
detectLocale: true,
// Show warnings when some of the i18n resources are missing
debug: 0,
debug: false,
// Messages to translate
messages: {
en
}
en,
},
// Additional messages. This allows extending the default `messages` set directly from the configuration.
messagesAdd: null,
};

18
src/i18n/index.js

@ -70,9 +70,11 @@ export default () => {
...opts,
messages: {
...config.messages,
...(opts.messages || {})
}
...(opts.messages || {}),
},
};
const add = this.config.messagesAdd;
add && this.addMessages(add);
if (this.config.detectLocale) {
this.config.locale = this._localLang();
@ -127,9 +129,7 @@ export default () => {
*/
getMessages(lang, opts = {}) {
const { messages } = this.config;
lang &&
!messages[lang] &&
this._debug(`'${lang}' i18n lang not found`, opts);
lang && !messages[lang] && this._debug(`'${lang}' i18n lang not found`, opts);
return lang ? messages[lang] : messages;
},
@ -202,10 +202,8 @@ export default () => {
// Try with fallback
if (!result) result = this._getMsg(key, localeFlb, opts);
!result &&
this._debug(`'${key}' i18n key not found in '${locale}' lang`, opts);
result =
result && isString(result) ? this._addParams(result, param) : result;
!result && this._debug(`'${key}' i18n key not found in '${locale}' lang`, opts);
result = result && isString(result) ? this._addParams(result, param) : result;
return result;
},
@ -248,6 +246,6 @@ export default () => {
destroy() {
this.config = config;
this.em = {};
}
},
};
};

88
src/i18n/locale/en.js

@ -5,7 +5,7 @@ export default {
addButton: 'Add image',
inputPlh: 'http://path/to/the/image.jpg',
modalTitle: 'Select Image',
uploadTitle: 'Drop files here or click to upload'
uploadTitle: 'Drop files here or click to upload',
},
// Here just as a reference, GrapesJS core doesn't contain any block,
// so this should be omitted from other local files
@ -15,7 +15,7 @@ export default {
},
categories: {
// 'category-id': 'Category Label',
}
},
},
domComponents: {
names: {
@ -33,8 +33,8 @@ export default {
thead: 'Table head',
table: 'Table',
row: 'Table row',
cell: 'Table cell'
}
cell: 'Table cell',
},
},
deviceManager: {
device: 'Device',
@ -42,8 +42,8 @@ export default {
desktop: 'Desktop',
tablet: 'Tablet',
mobileLandscape: 'Mobile Landscape',
mobilePortrait: 'Mobile Portrait'
}
mobilePortrait: 'Mobile Portrait',
},
},
panels: {
buttons: {
@ -55,9 +55,9 @@ export default {
'open-sm': 'Open Style Manager',
'open-tm': 'Settings',
'open-layers': 'Open Layer Manager',
'open-blocks': 'Open Blocks'
}
}
'open-blocks': 'Open Blocks',
},
},
},
selectorManager: {
label: 'Classes',
@ -66,8 +66,8 @@ export default {
states: {
hover: 'Hover',
active: 'Click',
'nth-of-type(2n)': 'Even/Odd'
}
'nth-of-type(2n)': 'Even/Odd',
},
},
styleManager: {
empty: 'Select an element before using Style Manager',
@ -80,12 +80,58 @@ export default {
decorations: 'Decorations',
extra: 'Extra',
flex: 'Flex',
dimension: 'Dimension'
dimension: 'Dimension',
},
// The core library generates the name by their `property` name
// 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: {
// float: 'Float',
}
'text-shadow-h': 'X',
'text-shadow-v': 'Y',
'text-shadow-blur': 'Blur',
'text-shadow-color': 'Color',
'box-shadow-h': 'X',
'box-shadow-v': 'Y',
'box-shadow-blur': 'Blur',
'box-shadow-spread': 'Spread',
'box-shadow-color': 'Color',
'box-shadow-type': 'Type',
'margin-top-sub': 'Top',
'margin-right-sub': 'Right',
'margin-bottom-sub': 'Bottom',
'margin-left-sub': 'Left',
'padding-top-sub': 'Top',
'padding-right-sub': 'Right',
'padding-bottom-sub': 'Bottom',
'padding-left-sub': 'Left',
'border-width-sub': 'Width',
'border-style-sub': 'Style',
'border-color-sub': 'Color',
'border-top-left-radius-sub': 'Top Left',
'border-top-right-radius-sub': 'Top Right',
'border-bottom-right-radius-sub': 'Bottom Right',
'border-bottom-left-radius-sub': 'Bottom Left',
'transform-rotate-x': 'Rotate X',
'transform-rotate-y': 'Rotate Y',
'transform-rotate-z': 'Rotate Z',
'transform-scale-x': 'Scale X',
'transform-scale-y': 'Scale Y',
'transform-scale-z': 'Scale Z',
'transition-property-sub': 'Property',
'transition-duration-sub': 'Duration',
'transition-timing-function-sub': 'Timing',
'background-image-sub': 'Image',
'background-repeat-sub': 'Repeat',
'background-position-sub': 'Position',
'background-attachment-sub': 'Attachment',
'background-size-sub': 'Size',
},
// Translate options in style properties
// options: {
// float: { // Id of the property
// ...
// left: 'Left', // {option id}: {Option label}
// }
// }
},
traitManager: {
empty: 'Select an element before using Trait Manager',
@ -103,15 +149,15 @@ export default {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'eg. https://google.com' }
href: { placeholder: 'eg. https://google.com' },
},
// In a trait like select, these are used to translate option names
options: {
target: {
false: 'This window',
_blank: 'New window'
}
}
}
}
_blank: 'New window',
},
},
},
},
};

13
src/index.js

@ -1,15 +1,14 @@
import Editor from './editor';
import { isElement, isFunction } from 'underscore';
import $ from 'utils/cash-dom';
import Editor from './editor';
import polyfills from 'utils/polyfills';
import { getGlobal } from 'utils/mixins';
import PluginManager from './plugin_manager';
import { hasWin } from 'utils/mixins';
polyfills();
const plugins = new PluginManager();
const editors = [];
const cash = hasWin() ? require('cash-dom') : null;
const $ = (cash && cash.default) || cash;
const defaultConfig = {
// If true renders editor on init
autorender: 1,
@ -59,13 +58,13 @@ export default {
// Load plugins
config.plugins.forEach(pluginId => {
let plugin = plugins.get(pluginId);
let plugin = isFunction(pluginId) ? pluginId : plugins.get(pluginId);
const plgOptions = config.pluginsOpts[pluginId] || {};
// Try to search in global context
if (!plugin) {
const wplg = window[pluginId];
plugin = wplg && wplg.default ? wplg.default : wplg;
const wplg = getGlobal()[pluginId];
plugin = wplg?.default || wplg;
}
if (plugin) {

42
src/keymaps/index.js

@ -15,12 +15,24 @@
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('keymap:add', () => { ... });
*
* // Use the API
* const keymaps = editor.Keymaps;
* keymaps.add(...);
* ```
*
* ## Available Events
* * `keymap:add` - New keymap added. The new keyamp object is passed as an argument
* * `keymap:remove` - Keymap removed. The removed keyamp object is passed as an argument
* * `keymap:emit` - Some keymap emitted, in arguments you get keymapId, shortcutUsed, Event
* * `keymap:emit:{keymapId}` - `keymapId` emitted, in arguments you get keymapId, shortcutUsed, Event
*
* ## Methods
* * [getConfig](#getconfig)
* * [add](#add)
* * [get](#get)
@ -45,42 +57,42 @@ export default () => {
defaults: {
'core:undo': {
keys: '⌘+z, ctrl+z',
handler: 'core:undo'
handler: 'core:undo',
},
'core:redo': {
keys: '⌘+shift+z, ctrl+shift+z',
handler: 'core:redo'
handler: 'core:redo',
},
'core:copy': {
keys: '⌘+c, ctrl+c',
handler: 'core:copy'
handler: 'core:copy',
},
'core:paste': {
keys: '⌘+v, ctrl+v',
handler: 'core:paste'
handler: 'core:paste',
},
'core:component-next': {
keys: 's',
handler: 'core:component-next'
handler: 'core:component-next',
},
'core:component-prev': {
keys: 'w',
handler: 'core:component-prev'
handler: 'core:component-prev',
},
'core:component-enter': {
keys: 'd',
handler: 'core:component-enter'
handler: 'core:component-enter',
},
'core:component-exit': {
keys: 'a',
handler: 'core:component-exit'
handler: 'core:component-exit',
},
'core:component-delete': {
keys: 'backspace, delete',
handler: 'core:component-delete',
opts: { prevent: 1 }
}
}
opts: { prevent: 1 },
},
},
};
return {
@ -154,9 +166,7 @@ export default () => {
const ableTorun = !em.isEditing() && !editor.Canvas.isInputFocused();
if (ableTorun || opts.force) {
opts.prevent && canvas.getCanvasView().preventDefault(e);
typeof handler == 'object'
? cmd.runCommand(handler, opt)
: handler(editor, 0, opt);
typeof handler == 'object' ? cmd.runCommand(handler, opt) : handler(editor, 0, opt);
const args = [id, h.shortcut, e];
em.trigger('keymap:emit', ...args);
em.trigger(`keymap:emit:${id}`, ...args);
@ -222,6 +232,6 @@ export default () => {
this.removeAll();
[em, config, keymaps].forEach(i => (i = {}));
this.em = {};
}
},
};
};

2
src/navigator/config/config.js

@ -26,7 +26,7 @@ export default {
// Scroll to selected component in Canvas when it's selected in Layers
// true, false or `scrollIntoView`-like options,
// `block: 'nearest'` avoids the issue of window scolling
// `block: 'nearest'` avoids the issue of window scrolling
scrollCanvas: { behavior: 'smooth', block: 'nearest' },
// Scroll to selected component in Layers when it's selected in Canvas

54
src/navigator/view/ItemView.js

@ -7,6 +7,9 @@ import { eventDrag } from 'dom_components/model/Component';
const inputProp = 'contentEditable';
const styleOpts = { mediaText: '' };
const $ = Backbone.$;
const isStyleHidden = (style = {}) => {
return (style.display || '').trim().indexOf('none') === 0;
};
let ItemsView;
export default Backbone.View.extend({
@ -20,7 +23,7 @@ export default Backbone.View.extend({
'mouseout [data-toggle-select]': 'handleHoverOut',
'dblclick [data-name]': 'handleEdit',
'keydown [data-name]': 'handleEditKey',
'focusout [data-name]': 'handleEditEnd'
'focusout [data-name]': 'handleEditEnd',
},
template(model) {
@ -41,9 +44,7 @@ export default Backbone.View.extend({
return `
${
hidable
? `<i class="${pfx}layer-vis fa fa-eye ${
this.isVisible() ? '' : 'fa-eye-slash'
}" data-toggle-visible></i>`
? `<i class="${pfx}layer-vis fa fa-eye ${this.isVisible() ? '' : 'fa-eye-slash'}" data-toggle-visible></i>`
: ''
}
<div class="${clsTitleC}">
@ -86,7 +87,8 @@ export default Backbone.View.extend({
['change:open', this.updateOpening],
['change:layerable', this.updateLayerable],
['change:style:display', this.updateVisibility],
['rerender:layer', this.render]
['rerender:layer', this.render],
['change:name change:custom-name', this.updateName],
].forEach(item => this.listenTo(model, item[0], item[1]));
this.className = `${pfx}layer ${pfx}layer__t-${type} no-select ${ppfx}two-color`;
this.inputNameCls = `${ppfx}layer-name`;
@ -105,10 +107,14 @@ export default Backbone.View.extend({
onInit.bind(this)({
component: model,
render: this.__render,
listenTo: this.listenTo
listenTo: this.listenTo,
});
},
updateName() {
this.getInputName().innerText = this.model.getName();
},
getVisibilityEl() {
if (!this.eyeEl) {
this.eyeEl = this.$el.children(`.${this.pfx}layer-vis`);
@ -122,7 +128,7 @@ export default Backbone.View.extend({
const model = this.model;
const hClass = `${pfx}layer-hidden`;
const hideIcon = 'fa-eye-slash';
const hidden = model.getStyle(styleOpts).display === 'none';
const hidden = isStyleHidden(model.getStyle(styleOpts));
const method = hidden ? 'addClass' : 'removeClass';
this.$el[method](hClass);
this.getVisibilityEl()[method](hideIcon);
@ -141,7 +147,7 @@ export default Backbone.View.extend({
const prevDisplay = model.get(prevDspKey);
const style = model.getStyle(styleOpts);
const { display } = style;
const hidden = display == 'none';
const hidden = isStyleHidden(style);
if (hidden) {
delete style.display;
@ -170,10 +176,7 @@ export default Backbone.View.extend({
inputEl.focus();
document.execCommand('selectAll', false, null);
em && em.setEditing(1);
$el
.find(`.${this.inputNameCls}`)
.removeClass(clsNoEdit)
.addClass(clsEdit);
$el.find(`.${this.inputNameCls}`).removeClass(clsNoEdit).addClass(clsEdit);
},
handleEditKey(ev) {
@ -193,10 +196,7 @@ export default Backbone.View.extend({
inputEl[inputProp] = false;
this.setName(name, { component: this.model, propName: 'custom-name' });
em && em.setEditing(0);
$el
.find(`.${this.inputNameCls}`)
.addClass(clsNoEdit)
.removeClass(clsEdit);
$el.find(`.${this.inputNameCls}`).addClass(clsNoEdit).removeClass(clsEdit);
},
setName(name, { propName }) {
@ -321,8 +321,8 @@ export default Backbone.View.extend({
ComponentView.prototype.updateStatus.apply(this, [
{
avoidHover: !this.config.highlightHover,
noExtHl: 1
}
noExtHl: 1,
},
]);
},
@ -332,9 +332,7 @@ export default Backbone.View.extend({
* @return boolean
* */
isVisible() {
const { display } = this.model.getStyle();
return !(display && display === 'none');
return !isStyleHidden(this.model.getStyle());
},
/**
@ -345,9 +343,7 @@ export default Backbone.View.extend({
checkChildren() {
const { model, clsNoChild } = this;
const count = this.countChildren(model);
const title = this.$el
.children(`.${this.clsTitleC}`)
.children(`.${this.clsTitle}`);
const title = this.$el.children(`.${this.clsTitleC}`).children(`.${this.clsTitle}`);
let { cnt } = this;
if (!cnt) {
@ -368,7 +364,7 @@ export default Backbone.View.extend({
*/
countChildren(model) {
var count = 0;
model.get('components').each(function(m) {
model.get('components').each(function (m) {
var isCountable = this.opt.isCountable;
var hide = this.config.hideTextnode;
if (isCountable && !isCountable(m, hide)) return;
@ -380,9 +376,7 @@ export default Backbone.View.extend({
getCaret() {
if (!this.caret || !this.caret.length) {
const pfx = this.pfx;
this.caret = this.$el
.children(`.${this.clsTitleC}`)
.find(`.${this.clsCaret}`);
this.caret = this.$el.children(`.${this.clsTitleC}`).find(`.${this.clsCaret}`);
}
return this.caret;
@ -436,7 +430,7 @@ export default Backbone.View.extend({
opened: this.opt.opened,
parentView: this,
parent: model,
level
level,
});
const children = this.items.render().$el;
@ -468,5 +462,5 @@ export default Backbone.View.extend({
const opt = { component: model, el };
onRender.bind(this)(opt);
this.em.trigger('layer:render', opt);
}
},
});

33
src/rich_text_editor/index.js

@ -11,12 +11,22 @@
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('rte:enable', () => { ... });
*
* // Use the API
* const rte = editor.RichTextEditor;
* rte.add(...);
* ```
*
* ## Available Events
* * `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
* * `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument
*
* ## Methods
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
@ -33,8 +43,7 @@ import defaults from './config/config';
export default () => {
let config = {};
let toolbar, actions, lastEl, lastElPos, globalRte;
const eventsUp =
'change:canvasOffset canvasScroll frame:scroll component:update';
const eventsUp = 'change:canvasOffset frame:scroll component:update';
const hideToolbar = () => {
const style = toolbar.style;
const size = '-1000px';
@ -65,7 +74,7 @@ export default () => {
init(opts = {}) {
config = {
...defaults,
...opts
...opts,
};
const ppfx = config.pStylePrefix;
@ -91,9 +100,7 @@ export default () => {
customRte && customRte.destroy && customRte.destroy();
this.actionbar = 0;
this.actions = 0;
[config, toolbar, actions, lastEl, lastElPos, globalRte].forEach(
i => (i = {})
);
[config, toolbar, actions, lastEl, lastElPos, globalRte].forEach(i => (i = {}));
},
/**
@ -124,14 +131,14 @@ export default () => {
button: `${pfx}action`,
active: `${pfx}active`,
inactive: `${pfx}inactive`,
disabled: `${pfx}disabled`
disabled: `${pfx}disabled`,
};
const rte = new RichTextEditor({
el,
classes,
actions,
actionbar,
actionbarContainer
actionbarContainer,
});
globalRte && globalRte.setEl(el);
@ -278,7 +285,7 @@ export default () => {
const { style } = toolbar;
const pos = canvas.getTargetToElementFixed(lastEl, toolbar, {
event: 'rteToolbarPosUpdate',
left: 0
left: 0,
});
style.top = (pos.top || 0) + un;
style.left = (pos.left || 0) + un;
@ -299,9 +306,7 @@ export default () => {
lastElPos = canvas.getElementPos(lastEl);
toolbar.style.display = '';
const rteInst = await (customRte
? customRte.enable(el, rte)
: this.initRte(el).enable());
const rteInst = await (customRte ? customRte.enable(el, rte) : this.initRte(el).enable());
if (em) {
setTimeout(this.updatePosition.bind(this), 0);
@ -335,6 +340,6 @@ export default () => {
em.off(eventsUp, this.updatePosition, this);
em.trigger('rte:disable', view, rte);
}
}
},
};
};

55
src/selector_manager/index.js

@ -111,7 +111,7 @@ export default () => {
remove: evRemove,
removeBefore: evRemoveBefore,
state: evState,
custom: evCustom
custom: evCustom,
},
/**
@ -138,20 +138,18 @@ export default () => {
this.model = new Model({ cFirst: config.componentFirst, _undo: true });
this.__initListen({
collections: [this.states, this.selected],
propagate: [{ entity: this.states, event: this.events.state }]
propagate: [{ entity: this.states, event: this.events.state }],
});
em.on('change:state', (m, value) => em.trigger('selector:state', value));
this.model.on('change:cFirst', (m, value) =>
em.trigger('selector:type', value)
);
em.on('change:state', (m, value) => em.trigger(evState, value));
this.model.on('change:cFirst', (m, value) => em.trigger('selector:type', value));
const listenTo =
'component:toggled component:update:classes styleManager:update selector:state selector:type';
'component:toggled component:update:classes change:device styleManager:update selector:state selector:type';
this.model.listenTo(em, listenTo, () => this.__update());
return this;
},
__update: debounce(function() {
__update: debounce(function () {
this.__trgCustom();
}),
@ -164,7 +162,7 @@ export default () => {
return {
states: this.getStates(),
selected: this.getSelected(),
container
container,
};
},
@ -183,16 +181,12 @@ export default () => {
select(value, opts = {}) {
const targets = Array.isArray(value) ? value : [value];
const toSelect = this.em.get('StyleManager').setTarget(targets, opts);
const toSelect = this.em.get('StyleManager').select(targets, opts);
const selTags = this.selectorTags;
const res = toSelect
.filter(i => i)
.map(sel =>
isComponent(sel)
? sel
: isRule(sel) && !sel.get('selectorsAdd')
? sel
: sel.getSelectorsString()
isComponent(sel) ? sel : isRule(sel) && !sel.get('selectorsAdd') ? sel : sel.getSelectorsString()
);
selTags && selTags.componentChanged({ targets: res });
return this;
@ -221,9 +215,7 @@ export default () => {
const cname = props.name;
const config = this.getConfig();
const all = this.getAll();
const selector = cname
? this.get(cname, props.type)
: all.where(props)[0];
const selector = cname ? this.get(cname, props.type) : all.where(props)[0];
if (!selector) {
return all.add(props, { ...cOpts, config });
@ -301,12 +293,8 @@ export default () => {
// Keep support for arrays but avoid it in docs
if (isArray(name)) {
const result = [];
const selectors = name
.map(item => this.getSelector(item))
.filter(item => item);
selectors.forEach(
item => result.indexOf(item) < 0 && result.push(item)
);
const selectors = name.map(item => this.getSelector(item)).filter(item => item);
selectors.forEach(item => result.indexOf(item) < 0 && result.push(item));
return result;
} else {
return this.getSelector(name, type) || null;
@ -402,9 +390,7 @@ export default () => {
*/
removeSelected(selector) {
this.em.getSelectedAll().forEach(trg => {
!selector.get('protected') &&
trg &&
trg.getSelectors().remove(selector);
!selector.get('protected') && trg && trg.getSelectors().remove(selector);
});
},
@ -471,7 +457,7 @@ export default () => {
el,
collection: this.selected,
module: this,
config
config,
});
return this.selectorTags.render().el;
@ -495,21 +481,16 @@ export default () => {
},
__getCommonSelectors(components, opts = {}) {
const selectors = components
.map(cmp => cmp.getSelectors && cmp.getSelectors().getValid(opts))
.filter(Boolean);
const selectors = components.map(cmp => cmp.getSelectors && cmp.getSelectors().getValid(opts)).filter(Boolean);
return this.__common(...selectors);
},
__common(...args) {
if (!args.length) return [];
if (args.length === 1) return args[0];
if (args.length === 2)
return args[0].filter(item => args[1].indexOf(item) >= 0);
if (args.length === 2) return args[0].filter(item => args[1].indexOf(item) >= 0);
return args
.slice(1)
.reduce((acc, item) => this.__common(acc, item), args[0]);
}
return args.slice(1).reduce((acc, item) => this.__common(acc, item), args[0]);
},
};
};

25
src/selector_manager/model/Selectors.js

@ -2,6 +2,15 @@ import { filter } from 'underscore';
import { Collection } from 'common';
import Selector from './Selector';
const combine = (tail, curr) => {
return tail.reduce(
(acc, item, n) => {
return acc.concat(combine(tail.slice(n + 1), `${curr}${item}`));
},
[curr]
);
};
export default class Selectors extends Collection {
modelId(attr) {
return `${attr.name}_${attr.type || Selector.TYPE_CLASS}`;
@ -26,6 +35,22 @@ export default class Selectors extends Collection {
coll.forEach(selector => result.push(selector.getFullName(opts)));
return result.join('').trim();
}
getFullName(opts = {}) {
const { combination, array } = opts;
let result = [];
const sels = this.map(s => s.getFullName(opts)).sort();
if (combination) {
sels.forEach((sel, n) => {
result = result.concat(combine(sels.slice(n + 1), sel));
});
} else {
result = sels;
}
return array ? result : combination ? result.join(',') : result.join('');
}
}
Selectors.prototype.model = Selector;

33
src/selector_manager/view/ClassTagsView.js

@ -33,7 +33,6 @@ export default Backbone.View.extend({
<div class="${pfx}sels-info">
<div class="${pfx}label-sel">${labelInfo}:</div>
<div class="${pfx}sels" data-selected></div>
<div style="clear:both"></div>
</div>`;
},
@ -42,7 +41,7 @@ export default Backbone.View.extend({
'click [data-add]': 'startNewTag',
'focusout [data-input]': 'endNewTag',
'keyup [data-input]': 'onInputKeyUp',
'click [data-sync-style]': 'syncStyle'
'click [data-sync-style]': 'syncStyle',
},
initialize(o = {}) {
@ -85,15 +84,13 @@ export default Backbone.View.extend({
const state = em.get('state');
const mediaText = em.getCurrentMedia();
const ruleComponents = [];
const rule =
cssC.get(selectors, state, mediaText) ||
cssC.add(selectors, state, mediaText);
const rule = cssC.get(selectors, state, mediaText) || cssC.add(selectors, state, mediaText);
let style;
this.getTargets().forEach(target => {
const ruleComponent = cssC.getIdRule(target.getId(), {
state,
mediaText
mediaText,
});
style = ruleComponent.getStyle();
ruleComponent.setStyle({});
@ -108,7 +105,7 @@ export default Backbone.View.extend({
mediaText,
rule,
ruleComponents,
state
state,
});
},
@ -171,7 +168,7 @@ export default Backbone.View.extend({
* @param {Object} e
* @private
*/
componentChanged: debounce(function({ targets } = {}) {
componentChanged: debounce(function ({ targets } = {}) {
this.updateSelection(targets);
}),
@ -200,7 +197,7 @@ export default Backbone.View.extend({
return this.module.__common(...args);
},
checkSync: debounce(function() {
checkSync: debounce(function () {
const { $btnSyncEl, config, collection } = this;
const target = this.getTarget();
let hasStyle;
@ -270,9 +267,7 @@ export default Backbone.View.extend({
? `<span class="${pfx}sel-cmp">${target.getName()}</span><span class="${pfx}sel-id">#${target.getId()}</span>`
: '';
result = this.collection.getFullString(selectors);
result = result
? `<span class="${pfx}sel-rule">${result}</span>`
: target.get('selectorsAdd') || idRes;
result = result ? `<span class="${pfx}sel-rule">${result}</span>` : target.get('selectorsAdd') || idRes;
result = componentFirst && idRes ? idRes : result;
result += state ? `<span class="${pfx}sel-state">:${state}</span>` : '';
result = selectedName ? selectedName({ result, state, target }) : result;
@ -319,7 +314,7 @@ export default Backbone.View.extend({
model,
config: this.config,
coll: this.collection,
module: this.module
module: this.module,
}).render().el;
fragment ? fragment.appendChild(rendered) : classes.append(rendered);
@ -377,17 +372,13 @@ export default Backbone.View.extend({
const options = module
.getStates()
.map(state => {
const label =
em.t(`selectorManager.states.${state.id}`) ||
state.getLabel() ||
state.id;
const label = em.t(`selectorManager.states.${state.id}`) || state.getLabel() || state.id;
return `<option value="${state.id}">${label}</option>`;
})
.join('');
const statesEl = this.getStates();
statesEl &&
statesEl.html(`<option value="">${labelStates}</option>${options}`);
statesEl && statesEl.html(`<option value="">${labelStates}</option>${options}`);
this.checkStates();
},
@ -401,7 +392,7 @@ export default Backbone.View.extend({
labelInfo: em.t('selectorManager.selected'),
ppfx,
pfx,
el
el,
};
$el.html(this.template(tmpOpts));
const renderRes = render && render(tmpOpts);
@ -415,5 +406,5 @@ export default Backbone.View.extend({
this.renderClasses();
$el.attr('class', `${this.className} ${ppfx}one-bg ${ppfx}two-color`);
return this;
}
},
});

2
src/storage_manager/config/config.js

@ -59,7 +59,7 @@ export default {
// Pass custom options to fetch API (remote storage)
// You can pass a simple object: { someOption: 'someValue' }
// or a function wich returns and object to add:
// or a function which returns and object to add:
// currentOpts => {
// return currentOpts.method === 'post' ? { method: 'patch' } : {};
// }

23
src/storage_manager/index.js

@ -8,12 +8,31 @@
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('storage:start', () => { ... });
*
* // Use the API
* const storageManager = editor.StorageManager;
* storageManager.add(...);
* ```
*
* ## Available Events
* * `storage:start` - Before the storage request is started
* * `storage:start:store` - Before the store request. The object to store is passed as an argumnet (which you can edit)
* * `storage:start:load` - Before the load request. Items to load are passed as an argumnet (which you can edit)
* * `storage:load` - Triggered when something was loaded from the storage, loaded object passed as an argumnet
* * `storage:store` - Triggered when something is stored to the storage, stored object passed as an argumnet
* * `storage:end` - After the storage request is ended
* * `storage:end:store` - After the store request
* * `storage:end:load` - After the load request
* * `storage:error` - On any error on storage request, passes the error as an argument
* * `storage:error:store` - Error on store request, passes the error as an argument
* * `storage:error:load` - Error on load request, passes the error as an argument
*
* ## Methods
* * [getConfig](#getconfig)
* * [isAutosave](#isautosave)
* * [setAutosave](#setautosave)
@ -362,6 +381,6 @@ export default () => {
destroy() {
[c, em, storages, defaultStorages].forEach(i => (i = {}));
}
},
};
};

70
src/style_manager/config/config.js

@ -1,29 +1,91 @@
export default {
stylePrefix: 'sm-',
sectors: [],
// Default sectors and properties
sectors: [
{
name: 'General',
open: false,
properties: ['display', 'float', 'position', 'top', 'right', 'left', 'bottom'],
},
{
name: 'Flex',
open: false,
properties: [
'flex-direction',
'flex-wrap',
'justify-content',
'align-items',
'align-content',
'order',
'flex-basis',
'flex-grow',
'flex-shrink',
'align-self',
],
},
{
name: 'Dimension',
open: false,
properties: ['width', 'height', 'max-width', 'min-height', 'margin', 'padding'],
},
{
name: 'Typography',
open: false,
properties: [
'font-family',
'font-size',
'font-weight',
'letter-spacing',
'color',
'line-height',
'text-align',
'text-shadow',
],
},
{
name: 'Decorations',
open: false,
properties: ['background-color', 'border-radius', 'border', 'box-shadow', 'background'],
},
{
name: 'Extra',
open: false,
properties: ['opacity', 'transition'],
},
],
// Specify the element to use as a container, string (query) or HTMLElement
// With the empty value, nothing will be rendered
appendTo: '',
// Style prefix
stylePrefix: 'sm-',
// Avoid rendering the default style manager.
custom: false,
// Hide the property in case it's not stylable for the
// selected component (each component has 'stylable' property)
// @deprecated
hideNotStylable: true,
// Highlight changed properties of the selected component
// @deprecated
highlightChanged: true,
// Highlight computed properties of the selected component
// @deprecated
highlightComputed: true,
// Show computed properties of the selected component, if this value
// is set to false, highlightComputed will not take effect
// @deprecated
showComputed: true,
// Adds the possibility to clear property value from the target style
// @deprecated
clearProperties: true,
// Properties not to take in account for computed styles
avoidComputed: ['width', 'height']
// @deprecated
avoidComputed: ['width', 'height'],
};

687
src/style_manager/index.js

@ -9,41 +9,62 @@
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('style:sector:add', (sector) => { ... });
*
* // Use the API
* const styleManager = editor.StyleManager;
* styleManager.addSector(...);
* ```
* ## Available Events
* * `style:sector:add` - Sector added. The [Sector] is passed as an argument to the callback.
* * `style:sector:remove` - Sector removed. The [Sector] is passed as an argument to the callback.
* * `style:sector:update` - Sector updated. The [Sector] and the object containing changes are passed as arguments to the callback.
* * `style:property:add` - Property added. The [Property] is passed as an argument to the callback.
* * `style:property:remove` - Property removed. The [Property] is passed as an argument to the callback.
* * `style:property:update` - Property updated. The [Property] and the object containing changes are passed as arguments to the callback.
* * `style:target` - Target selection changed. The target (or `null` in case the target is deselected) is passed as an argument to the callback.
* <!--
* * `styleManager:update:target` - The target (Component or CSSRule) is changed
* * `styleManager:change` - Triggered on style property change from new selected component, the view of the property is passed as an argument to the callback
* * `styleManager:change:{propertyName}` - As above but for a specific style property
* -->
*
* ## Methods
* * [getConfig](#getconfig)
* * [addSector](#addsector)
* * [getSector](#getsector)
* * [removeSector](#removesector)
* * [getSectors](#getsectors)
* * [removeSector](#removesector)
* * [addProperty](#addproperty)
* * [getProperty](#getproperty)
* * [removeProperty](#removeproperty)
* * [getProperties](#getproperties)
* * [getModelToStyle](#getmodeltostyle)
* * [removeProperty](#removeproperty)
* * [select](#select)
* * [getSelected](#getselected)
* * [getSelectedAll](#getselectedall)
* * [getSelectedParents](#getselectedparents)
* * [addStyleTargets](#addstyletargets)
* * [getBuiltIn](#getbuiltin)
* * [getBuiltInAll](#getbuiltinall)
* * [addBuiltIn](#addbuiltin)
* * [addType](#addtype)
* * [getType](#gettype)
* * [getTypes](#gettypes)
* * [createType](#createtype)
*
* [Sector]: sector.html
* [CssRule]: css_rule.html
* [Component]: component.html
* [Property]: property.html
*
* @module StyleManager
*/
import {
isElement,
isUndefined,
isArray,
isString,
debounce
} from 'underscore';
import { isElement, isUndefined, isArray, isString, debounce } from 'underscore';
import { isComponent } from 'utils/mixins';
import Module from 'common/module';
import { Model } from 'common';
import defaults from './config/config';
@ -53,8 +74,23 @@ import Properties from './model/Properties';
import PropertyFactory from './model/PropertyFactory';
import SectorsView from './view/SectorsView';
export const evAll = 'style';
export const evPfx = `${evAll}:`;
export const evSector = `${evPfx}sector`;
export const evSectorAdd = `${evSector}:add`;
export const evSectorRemove = `${evSector}:remove`;
export const evSectorUpdate = `${evSector}:update`;
export const evProp = `${evPfx}property`;
export const evPropAdd = `${evProp}:add`;
export const evPropRemove = `${evProp}:remove`;
export const evPropUp = `${evProp}:update`;
export const evLayerSelect = `${evPfx}layer:select`;
export const evTarget = `${evPfx}target`;
export const evCustom = `${evPfx}custom`;
const propDef = value => value || value === 0;
export default () => {
var c = {};
let properties;
var sectors, SectView;
@ -63,82 +99,106 @@ export default () => {
Sector,
PropertyFactory: PropertyFactory(),
events: {
all: evAll,
sectorAdd: evSectorAdd,
sectorRemove: evSectorRemove,
sectorUpdate: evSectorUpdate,
propertyAdd: evPropAdd,
propertyRemove: evPropRemove,
propertyUpdate: evPropUp,
layerSelect: evLayerSelect,
target: evTarget,
custom: evCustom,
},
/**
* Name of the module
* @type {String}
* @private
*/
name: 'StyleManager',
/**
* Get configuration object
* @name getConfig
* @function
* @return {Object}
*/
getConfig() {
return c;
},
/**
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations
* @private
*/
init(config) {
c = { ...defaults, ...config };
const ppfx = c.pStylePrefix;
init(config = {}) {
this.__initConfig(defaults, config);
const c = this.config;
const { em } = c;
this.em = em;
const ppfx = c.pStylePrefix;
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
properties = new Properties();
sectors = new Sectors([], c);
this.model = new Model({ targets: [] });
const toListen =
'component:toggled component:update:classes change:state change:device frame:resized selector:type';
em.on(
toListen,
debounce(() => {
this.select(em.getSelectedAll());
em.trigger('style:custom');
})
);
this.builtIn = new PropertyFactory();
properties = new Properties([], { em, module: this });
sectors = new Sectors([], { ...c, module: this });
const model = new Model({ targets: [] });
this.model = model;
this.__listenAdd(sectors, evSectorAdd);
this.__listenRemove(sectors, evSectorRemove);
this.__listenUpdate(sectors, evSectorUpdate);
// Triggers for the selection refresh and properties
const ev = 'component:toggled component:update:classes change:state change:device frame:resized selector:type';
const upAll = debounce(() => this.__upSel());
model.listenTo(em, ev, upAll);
// Triggers only for properties (avoid selection refresh)
const upProps = debounce(() => {
this.__upProps();
this.__trgCustom();
});
model.listenTo(em, 'styleable:change undo redo', upProps);
// Triggers only custom event
const trgCustom = debounce(() => this.__trgCustom());
model.listenTo(em, `${evLayerSelect} ${evTarget}`, trgCustom);
// Other listeners
model.on('change:lastTarget', () => em.trigger(evTarget, this.getSelected()));
return this;
},
__upSel() {
this.select(this.em.getSelectedAll());
},
__trgCustom(opts = {}) {
this.__ctn = this.__ctn || opts.container;
this.em.trigger(this.events.custom, { container: this.__ctn });
},
__trgEv(event, ...data) {
this.em.trigger(event, ...data);
},
onLoad() {
// Use silent as sectors' view will be created and rendered on StyleManager.render
sectors.add(c.sectors, { silent: true });
sectors.add(this.config.sectors, { silent: true });
},
postRender() {
const elTo = this.getConfig().appendTo;
if (elTo) {
const el = isElement(elTo) ? elTo : document.querySelector(elTo);
if (!el) return this.__logWarn('"appendTo" element not found');
el.appendChild(this.render());
}
this.__appendTo();
},
/**
* Add new sector to the collection. If the sector with the same id already exists,
* that one will be returned.
* @param {string} id Sector id
* @param {Object} sector Object representing sector
* @param {string} [sector.name=''] Sector's label
* @param {Boolean} [sector.open=true] Indicates if the sector should be opened
* @param {Array<Object>} [sector.properties=[]] Array of properties
* @param {Object} [options={}] Options
* @return {[Sector]} Added Sector
* Add new sector. If the sector with the same id already exists, that one will be returned.
* @param {String} id Sector id
* @param {Object} sector Sector definition. Check the [available properties](sector.html#properties)
* @param {Object} [options={}] Options
* @param {Number} [options.at] Position index (by default, will be appended at the end).
* @returns {[Sector]} Added Sector
* @example
* const sector = styleManager.addSector('mySector',{
* name: 'My sector',
* open: true,
* properties: [{ name: 'My property'}]
* }, { at: 0 });
* // With `at: 0` we place the new sector at the beginning of the collection
* // With `at: 0` we place the new sector at the beginning of the list
* */
addSector(id, sector, options = {}) {
let result = this.getSector(id);
@ -152,9 +212,9 @@ export default () => {
},
/**
* Get sector by id
* @param {string} id Sector id
* @return {[Sector]|null}
* Get sector by id.
* @param {String} id Sector id
* @returns {[Sector]|null}
* @example
* const sector = styleManager.getSector('mySector');
* */
@ -165,66 +225,47 @@ export default () => {
},
/**
* Remove a sector by id
* @param {string} id Sector id
* @return {[Sector]} Removed sector
* Get all sectors.
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.visible] Returns only visible sectors
* @returns {Array<[Sector]>}
* @example
* const removed = styleManager.removeSector('mySector');
*/
removeSector(id) {
return this.getSectors().remove(this.getSector(id, { warn: 1 }));
* const sectors = styleManager.getSectors();
* */
getSectors(opts = {}) {
const res = sectors && sectors.models ? (opts.array ? [...sectors.models] : sectors) : [];
return opts.visible ? res.filter(s => s.isVisible()) : res;
},
/**
* Get all sectors
* @returns {Collection<[Sector]>} Collection of sectors
* Remove sector by id.
* @param {String} id Sector id
* @returns {[Sector]} Removed sector
* @example
* const sectors = styleManager.getSectors();
* */
getSectors(opts = {}) {
return sectors && sectors.models
? opts.array
? [...sectors.models]
: sectors
: [];
* const removed = styleManager.removeSector('mySector');
*/
removeSector(id) {
return this.getSectors().remove(this.getSector(id, { warn: 1 }));
},
/**
* Add property to the sector identified by id
* @param {string} sectorId Sector id
* @param {Object} property Property object
* @param {string} [property.name=''] Name of the property
* @param {string} [property.property=''] CSS property, eg. `min-height`
* @param {string} [property.type=''] Type of the property: integer | radio | select | color | file | composite | stack
* @param {Array<string>} [property.units=[]] Unit of measure available, eg. ['px','%','em']. Only for integer type
* @param {string} [property.unit=''] Default selected unit from `units`. Only for integer type
* @param {number} [property.min=null] Min possible value. Only for integer type
* @param {number} [property.max=null] Max possible value. Only for integer type
* @param {string} [property.defaults=''] Default value
* @param {string} [property.info=''] Some description
* @param {string} [property.icon=''] Class name. If exists no text will be displayed
* @param {Boolean} [property.preview=false] Show layers preview. Only for stack type
* @param {string} [property.functionName=''] Indicates if value need to be wrapped in some function, for istance `transform: rotate(90deg)`
* @param {Array<Object>} [property.properties=[]] Nested properties for composite and stack type
* @param {Array<Object>} [property.layers=[]] Layers for stack properties
* @param {Array<Object>} [property.list=[]] List of possible options for radio and select types
* @param {Object} [options={}] Options
* @return {Property|null} Added Property or `null` in case sector doesn't exist
* Add new property to the sector.
* @param {String} sectorId Sector id.
* @param {Object} property Property definition. Check the [base available properties](property.html#properties) + others based on the `type` of your property.
* @param {Object} [opts={}] Options
* @param {Number} [opts.at] Position index (by default, will be appended at the end).
* @returns {[Property]|null} Added property or `null` in case the sector doesn't exist.
* @example
* var property = styleManager.addProperty('mySector',{
* name: 'Minimum height',
* const property = styleManager.addProperty('mySector', {
* label: 'Minimum height',
* property: 'min-height',
* type: 'select',
* defaults: '100px',
* list: [{
* value: '100px',
* name: '100',
* },{
* value: '200px',
* name: '200',
* }],
* default: '100px',
* options: [
* { id: '100px', label: '100' },
* { id: '200px', label: '200' },
* ],
* }, { at: 0 });
* // With `at: 0` we place the new property at the beginning of the collection
*/
addProperty(sectorId, property, opts = {}) {
const sector = this.getSector(sectorId, { warn: 1 });
@ -235,54 +276,203 @@ export default () => {
},
/**
* Get property by its CSS name and sector id
* @param {string} sectorId Sector id
* @param {string} name CSS property name (or id), eg. 'min-height'
* @return {Property|null}
* Get the property.
* @param {String} sectorId Sector id.
* @param {String} id Property id.
* @returns {[Property]|null}
* @example
* var property = styleManager.getProperty('mySector','min-height');
* const property = styleManager.getProperty('mySector', 'min-height');
*/
getProperty(sectorId, name) {
getProperty(sectorId, id) {
const sector = this.getSector(sectorId, { warn: 1 });
let prop;
if (sector) {
prop = sector
.get('properties')
.filter(
prop => prop.get('property') === name || prop.get('id') === name
)[0];
prop = sector.get('properties').filter(prop => prop.get('property') === id || prop.get('id') === id)[0];
}
return prop || null;
},
/**
* Remove a property from the sector
* @param {string} sectorId Sector id
* @param {string} name CSS property name, eg. 'min-height'
* @return {Property} Removed property
* Get all properties of the sector.
* @param {String} sectorId Sector id.
* @returns {Collection<[Property]>|null} Collection of properties
* @example
* const properties = styleManager.getProperties('mySector');
*/
getProperties(sectorId) {
let props = null;
const sector = this.getSector(sectorId, { warn: 1 });
if (sector) props = sector.get('properties');
return props;
},
/**
* Remove the property.
* @param {String} sectorId Sector id.
* @param {String} id Property id.
* @returns {[Property]|null} Removed property
* @example
* const property = styleManager.removeProperty('mySector', 'min-height');
*/
removeProperty(sectorId, name) {
removeProperty(sectorId, id) {
const props = this.getProperties(sectorId);
return props && props.remove(this.getProperty(sectorId, name));
return props ? props.remove(this.getProperty(sectorId, id)) : null;
},
/**
* Get properties of the sector
* @param {string} sectorId Sector id
* @return {Properties} Collection of properties
* Select new target.
* The target could be a Component, CSSRule, or a CSS selector string.
* @param {[Component]|[CSSRule]|String} target
* @returns {Array<[Component]|[CSSRule]>} Array containing selected Components or CSSRules
* @example
* var properties = styleManager.getProperties('mySector');
* // Select the first button in the current page
* const wrapperCmp = editor.Pages.getSelected().getMainComponent();
* const btnCmp = wrapperCmp.find('button')[0];
* btnCmp && styleManager.select(btnCmp);
*
* // Set as a target the CSS selector
* styleManager.select('.btn > span');
*/
getProperties(sectorId) {
let props = null;
const sector = this.getSector(sectorId, { warn: 1 });
if (sector) props = sector.get('properties');
select(target, opts = {}) {
const { em } = this;
const trgs = isArray(target) ? target : [target];
const { stylable } = opts;
const cssc = em.get('CssComposer');
let targets = [];
return props;
trgs.filter(Boolean).forEach(target => {
let model = target;
if (isString(target)) {
const rule = cssc.getRule(target) || cssc.setRule(target);
!isUndefined(stylable) && rule.set({ stylable });
model = rule;
}
targets.push(model);
});
const component = opts.component || targets.filter(t => isComponent(t)).reverse()[0];
targets = targets.map(t => this.getModelToStyle(t));
const state = em.getState();
const lastTarget = targets.slice().reverse()[0];
const lastTargetParents = this.getParentRules(lastTarget, { state, component });
let stateTarget = this.__getStateTarget();
// Update sectors/properties visibility
sectors.forEach(sector => {
const props = sector.getProperties();
props.forEach(prop => {
const isVisible = prop.__checkVisibility({ target: lastTarget, component, sectors });
prop.set('visible', isVisible);
});
const sectorVisible = props.some(p => p.isVisible());
sector.set('visible', sectorVisible);
});
// Handle the creation and update of the state rule, if enabled.
em.skip(() => {
if (state && lastTarget?.getState?.()) {
const style = lastTarget.getStyle();
if (!stateTarget) {
stateTarget = cssc.getAll().add({ selectors: 'gjs-selected', style, important: true });
} else {
stateTarget.setStyle(style);
}
} else if (stateTarget) {
cssc.remove(stateTarget);
stateTarget = null;
}
});
this.model.set({ targets, lastTarget, lastTargetParents, stateTarget });
this.__upProps(opts);
return targets;
},
/**
* Get the last selected target.
* By default, the Style Manager shows styles of the last selected target.
* @returns {[Component]|[CSSRule]|null}
*/
getSelected() {
return this.model.get('lastTarget') || null;
},
/**
* Get the array of selected targets.
* @returns {Array<[Component]|[CSSRule]>}
*/
getSelectedAll() {
return this.model.get('targets');
},
/**
* Get parent rules of the last selected target.
* @returns {Array<[CSSRule]>}
*/
getSelectedParents() {
return this.model.get('lastTargetParents') || [];
},
__getStateTarget() {
return this.model.get('stateTarget') || null;
},
/**
* Update selected targets with a custom style.
* @param {Object} style Style object
* @param {Object} [opts={}] Options
* @example
* styleManager.addStyleTargets({ color: 'red' });
*/
addStyleTargets(style, opts) {
this.getSelectedAll().map(t => t.addStyle(style, opts));
// Update state rule
const target = this.getSelected();
const targetState = this.__getStateTarget();
target && targetState?.setStyle(target.getStyle(), opts);
},
/**
* Return built-in property definition
* @param {String} prop Property name.
* @returns {Object|null} Property definition.
* @example
* const widthPropDefinition = styleManager.getBuiltIn('width');
*/
getBuiltIn(prop) {
return this.builtIn.get(prop);
},
/**
* Get all the available built-in property definitions.
* @returns {Object}
*/
getBuiltInAll() {
return this.builtIn.props;
},
/**
* Add built-in property definition.
* If the property exists already, it will extend it.
* @param {String} prop Property name.
* @param {Object} definition Property definition.
* @returns {Object} Added property definition.
* @example
* const sector = styleManager.addBuiltIn('new-property', {
* type: 'select',
* default: 'value1',
* options: [{ id: 'value1', label: 'Some label' }, ...],
* })
*/
addBuiltIn(prop, definition) {
return this.builtIn.add(prop, definition);
},
/**
@ -292,20 +482,20 @@ export default () => {
* one or more classes, the function will return the corresponding CSS Rule
* @param {Model} model
* @return {Model}
* @private
*/
getModelToStyle(model, options = {}) {
const em = c.em;
const { em } = this;
const { skipAdd } = options;
const classes = model.get('classes');
const id = model.getId();
if (em && classes) {
if (em && model?.toHTML) {
const config = em.getConfig();
const um = em.get('UndoManager');
const cssC = em.get('CssComposer');
const sm = em.get('SelectorManager');
const smConf = sm ? sm.getConfig() : {};
const state = !config.devicePreviewMode ? em.get('state') : '';
const classes = model.get('classes');
const valid = classes.getStyleable();
const hasClasses = valid.length;
const useClasses = !smConf.componentFirst || options.useClasses;
@ -327,6 +517,7 @@ export default () => {
rule = cssC.add(valid, state, deviceW, {}, addOpts);
}
} else if (config.avoidInlineStyle) {
const id = model.getId();
rule = cssC.getIdRule(id, opts);
!rule && !skipAdd && (rule = cssC.setIdRule(id, {}, opts));
if (model.is('wrapper')) rule.set('wrapper', 1, addOpts);
@ -339,18 +530,37 @@ export default () => {
return model;
},
getParentRules(target, state) {
const { em } = c;
getParentRules(target, { state, component } = {}) {
const { em } = this;
let result = [];
if (em && target) {
const sel = component;
const cssC = em.get('CssComposer');
const cssGen = em.get('CodeManager').getGenerator('css');
const all = cssC
.getRules(target.getSelectors().getFullString())
.filter(rule => (state ? rule.get('state') === state : 1))
const cmp = target.toHTML ? target : target.getComponent();
const optsSel = { combination: true, array: true };
let cmpRules = [];
let otherRules = [];
let rules = [];
// Componente related rule
if (cmp) {
cmpRules = cssC.getRules(`#${cmp.getId()}`);
otherRules = sel ? cssC.getRules(sel.getSelectors().getFullName(optsSel)) : [];
rules = otherRules.concat(cmpRules);
} else {
cmpRules = sel ? cssC.getRules(`#${sel.getId()}`) : [];
otherRules = cssC.getRules(target.getSelectors().getFullName(optsSel));
rules = cmpRules.concat(otherRules);
}
const all = rules
.filter(rule => (!isUndefined(state) ? rule.get('state') === state : 1))
.sort(cssGen.sortRules)
.reverse();
// Slice removes rules not related to the current device
result = all.slice(all.indexOf(target) + 1);
}
@ -360,33 +570,29 @@ export default () => {
/**
* Add new property type
* @param {string} id Type ID
* @param {Object} definition Definition of the type. Each definition contains
* `model` (business logic), `view` (presentation logic)
* and `isType` function which recognize the type of the
* passed entity
*@example
* @param {Object} definition Definition of the type.
* @example
* styleManager.addType('my-custom-prop', {
* // Create UI
* create({ props, change }) {
* const el = document.createElement('div');
* el.innerHTML = '<input type="range" class="my-input" min="10" max="50"/>';
* const inputEl = el.querySelector('.my-input');
* inputEl.addEventListener('change', event => change({ event })); // change will trigger the emit
* inputEl.addEventListener('input', event => change({ event, complete: false }));
* inputEl.addEventListener('change', event => change({ event }));
* inputEl.addEventListener('input', event => change({ event, partial: true }));
* return el;
* },
* emit({ props, updateStyle }, { event, complete }) {
* // Propagate UI changes up to the targets
* emit({ props, updateStyle }, { event, partial }) {
* const { value } = event.target;
* const valueRes = value + 'px';
* // Pass a string value for the exact CSS property or an object containing multiple properties
* // eg. updateStyle({ [props.property]: valueRes, color: 'red' });
* updateStyle(valueRes, { complete });
* updateStyle(`${value}px`, { partial });
* },
* // Update UI (eg. when the target is changed)
* update({ value, el }) {
* el.querySelector('.my-input').value = parseInt(value, 10);
* },
* destroy() {
* // In order to prevent memory leaks, use this method to clean, eventually, created instances, global event listeners, etc.
* }
* // Clean the memory from side effects if necessary (eg. global event listeners, etc.)
* destroy() {}
*})
*/
addType(id, definition) {
@ -411,14 +617,15 @@ export default () => {
},
/**
* Create new property from type
* Create new UI property from type (Experimental)
* @param {string} id Type ID
* @param {Object} [options={}] Options
* @param {Object} [options.model={}] Custom model object
* @param {Object} [options.view={}] Custom view object
* @return {PropertyView}
* @private
* @example
* const propView = styleManager.createType('integer', {
* const propView = styleManager.createType('number', {
* model: {units: ['px', 'rem']}
* });
* propView.render();
@ -426,99 +633,32 @@ export default () => {
* someContainer.appendChild(propView.el);
*/
createType(id, { model = {}, view = {} } = {}) {
const { config } = this;
const type = this.getType(id);
if (type) {
return new type.view({
model: new type.model(model),
config: c,
...view
config,
...view,
});
}
},
setTarget(target, opts) {
return SectView.setTarget(target, opts);
},
getTargets() {
const { propTarget } = SectView || {};
return (propTarget && propTarget.targets) || [];
},
/**
* Select different target for the Style Manager.
* It could be a Component, CSSRule, or a string of any CSS selector
* @param {[Component]|[CSSRule]|String} target
* @return {Array<[Component]|[CSSRule]>} Array containing selected Components or CSSRules
* @private
*/
select(target, opts = {}) {
const { em } = this;
const trgs = isArray(target) ? target : [target];
const { stylable } = opts;
const cssc = em.get('CssComposer');
let targets = [];
trgs.filter(Boolean).forEach(target => {
let model = target;
if (isString(target)) {
const rule = cssc.getRule(target) || cssc.setRule(target);
!isUndefined(stylable) && rule.set({ stylable });
model = rule;
}
targets.push(model);
});
targets = targets.map(t => this.getModelToStyle(t));
const lastTarget = targets.slice().reverse()[0];
const lastTargetParents = this.getParentRules(lastTarget);
this.model.set({ targets, lastTarget, lastTargetParents });
// Update all property values
// if (lastTarget && em.getConfig('customUI')) {
// const style = lastTarget.getStyle();
// sectors.map(sector => {
// sector.getProperties().map(prop => {
// const value = style[prop.getName()];
// !isUndefined(value) && prop.setValue(value);
// })
// })
// }
return targets;
},
getSelected() {
return this.model.get('targets');
},
getLastSelected() {
return this.model.get('lastTarget') || null;
},
getSelectedParents() {
return this.model.get('lastTargetParents') || [];
},
getEmitter() {
return SectView.propTarget;
},
/**
* Render sectors and properties
* @return {HTMLElement}
* @private
* */
render() {
const { config, em } = this;
const el = SectView && SectView.el;
SectView = new SectorsView({
el,
em,
config,
collection: sectors,
target: c.em,
config: c
module: this,
});
return SectView.render().el;
},
@ -528,14 +668,91 @@ export default () => {
em && em.logWarning(`'${sectorId}' sector not found`);
},
__upProps(opts) {
const lastTarget = this.getSelected();
if (!lastTarget) return;
const lastTargetParents = this.getSelectedParents();
const style = lastTarget.getStyle();
const parentStyles = lastTargetParents.map(p => ({
target: p,
style: p.getStyle(),
}));
sectors.map(sector => {
sector.getProperties().map(prop => {
this.__upProp(prop, style, parentStyles, opts);
});
});
},
__upProp(prop, style, parentStyles, opts) {
const name = prop.getName();
const value = style[name];
const hasVal = propDef(value);
const isStack = prop.getType() === 'stack';
const isComposite = prop.getType() === 'composite';
const opt = { ...opts, __up: true };
const canUpdate = !isComposite && !isStack;
let newLayers = isStack ? prop.__getLayersFromStyle(style) : [];
let newProps = isComposite ? prop.__getPropsFromStyle(style) : {};
let newValue = hasVal ? value : null;
let parentTarget = null;
if ((isStack && newLayers === null) || (isComposite && newProps === null)) {
const method = isStack ? '__getLayersFromStyle' : '__getPropsFromStyle';
const parentItem = parentStyles.filter(p => prop[method](p.style) !== null)[0];
if (parentItem) {
newValue = parentItem.style[name];
parentTarget = parentItem.target;
const val = prop[method](parentItem.style);
if (isStack) {
newLayers = val;
} else {
newProps = val;
}
}
} else if (!hasVal) {
newValue = null;
const parentItem = parentStyles.filter(p => propDef(p.style[name]))[0];
if (parentItem) {
newValue = parentItem.style[name];
parentTarget = parentItem.target;
}
}
prop.__setParentTarget(parentTarget);
canUpdate && prop.__getFullValue() !== newValue && prop.upValue(newValue, opt);
isStack && prop.__setLayers(newLayers || []);
if (isComposite) {
const props = prop.getProperties();
// Detached has to be treathed as separate properties
if (prop.isDetached()) {
const newStyle = prop.__getPropsFromStyle(style, { byName: true }) || {};
const newParentStyles = parentStyles.map(p => ({
...p,
style: prop.__getPropsFromStyle(p.style, { byName: true }) || {},
}));
props.map(pr => this.__upProp(pr, newStyle, newParentStyles, opts));
} else {
prop.__setProperties(newProps || {}, opt);
prop.getProperties().map(pr => pr.__setParentTarget(parentTarget));
}
}
},
destroy() {
[properties, sectors].forEach(coll => {
coll.reset();
coll.stopListening();
});
SectView && SectView.remove();
[c, properties, sectors, SectView].forEach(i => (i = {}));
[properties, sectors, SectView].forEach(i => (i = {}));
this.em = {};
}
this.config = {};
},
};
};

153
src/style_manager/model/Layer.js

@ -1,68 +1,111 @@
import Backbone from 'backbone';
import Properties from './Properties';
import { Model } from 'common';
import { camelCase } from 'utils/mixins';
export default Backbone.Model.extend({
defaults: {
index: '',
value: '',
values: {},
active: false,
preview: false,
properties: []
},
export default class Layer extends Model {
defaults() {
return {
values: {},
};
}
initialize() {
const prp = this.get('properties');
var value = this.get('value');
this.set(
'properties',
prp instanceof Properties ? prp : new Properties(prp)
);
const props = this.get('properties');
props.forEach(this.onPropAdd, this);
this.listenTo(props, 'add', this.onPropAdd);
const cl = this.collection;
this.prop = cl?.prop;
}
// If there is no value I'll try to get it from values
// I need value setted to make preview working
if (!value) {
var val = '';
var values = this.get('values');
/**
* Get layer id.
* @returns {String}
*/
getId() {
return this.cid;
}
for (var prop in values) {
val += ' ' + values[prop];
}
/**
* Get layer index.
* @returns {Number}
*/
getIndex() {
const coll = this.collection;
return coll ? coll.indexOf(this) : -1;
}
this.set('value', val.trim());
}
},
/**
* Get layer values.
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.camelCase] Return property names in camelCase.
* @returns {Object}
*/
getValues(opts = {}) {
const values = this.get('values');
onPropAdd(prop) {
const coll = this.collection;
prop.parent = coll && coll.property;
},
return opts.camelCase
? Object.keys(values).reduce((res, key) => {
res[camelCase(key)] = values[key];
return res;
}, {})
: values;
}
/**
* Get property at some index
* @param {Number} index
* @return {Object}
* Get layer label.
* @returns {String}
*/
getPropertyAt(index) {
return this.get('properties').at(index);
},
getLabel() {
return this.prop?.getLayerLabel(this);
}
getPropertyValue(property) {
let result = '';
this.get('properties').each(prop => {
if (prop.get('property') == property) {
result = prop.getFullValue();
}
});
return result;
},
/**
* Check if the layer is selected.
* @returns {Boolean}
*/
isSelected() {
return this.prop?.getSelectedLayer() === this;
}
getFullValue() {
let result = [];
this.get('properties').each(prop => result.push(prop.getFullValue()));
return result.join(' ').trim();
/**
* Select the layer.
*/
select() {
return this.prop?.selectLayer(this);
}
/**
* Remove the layer.
*/
remove() {
return this.prop?.removeLayer(this);
}
/**
* Move layer to a new index.
* @param {Number} index New index
*/
move(index) {
return this.prop?.moveLayer(this, index);
}
/**
* Get style object for the preview.
* @param {Object} [opts={}] Options. Same of `PropertyStack.getStyleFromLayer`
* @returns {Object} Style object
*/
getStylePreview(opts = {}) {
return this.prop?.getStylePreview(this, opts);
}
/**
* Check if the property has the preview enabled for this layer.
* @returns {Boolean}
*/
hasPreview() {
return !!this.prop?.get('preview');
}
upValues(props = {}) {
return this.set('values', {
...this.getValues(),
...props,
});
}
});
}

127
src/style_manager/model/Layers.js

@ -1,123 +1,10 @@
import { isUndefined } from 'underscore';
import Backbone from 'backbone';
import { Collection } from 'common';
import Layer from './Layer';
export default Backbone.Collection.extend({
model: Layer,
initialize() {
this.idx = 1;
this.on('add', this.onAdd);
this.on('reset', this.onReset);
},
onAdd(model, c, opts) {
if (!opts.noIncrement) model.set('index', this.idx++);
opts.active && this.active(this.indexOf(model));
},
onReset() {
this.idx = 1;
},
getSeparator() {
const { property } = this;
return property ? property.get('layerSeparator') : ', ';
},
/**
* Get layers from a value string (for not detached properties),
* example of input:
* `layer1Value, layer2Value, layer3Value, ...`
* @param {string} value
* @return {Array}
* @private
*/
getLayersFromValue(value) {
const layers = [];
// Remove spaces inside functions, eg:
// From: 1px 1px rgba(2px, 2px, 2px), 2px 2px rgba(3px, 3px, 3px)
// To: 1px 1px rgba(2px,2px,2px), 2px 2px rgba(3px,3px,3px)
value.replace(/\(([\w\s,.]*)\)/g, match => {
var cleaned = match.replace(/,\s*/g, ',');
value = value.replace(match, cleaned);
});
const layerValues = value ? value.split(this.getSeparator()) : [];
layerValues.forEach(layerValue => {
layers.push({ properties: this.properties.parseValue(layerValue) });
});
return layers;
},
/**
* Get layers from a style object (for detached properties),
* example of input:
* {
* subPropname1: sub-propvalue11, sub-propvalue12, sub-propvalue13, ...
* subPropname2: sub-propvalue21, sub-propvalue22, sub-propvalue23, ...
* subPropname3: sub-propvalue31, sub-propvalue32, sub-propvalue33, ...
* }
* @param {Object} styleObj
* @return {Array}
* @private
*/
getLayersFromStyle(styleObj) {
const layers = [];
const properties = this.properties;
properties.each(propModel => {
const style = styleObj[propModel.get('property')];
const values = style ? style.split(', ') : [];
values.forEach((value, i) => {
value = propModel.parseValue(value.trim()).value;
const layer = layers[i];
const propertyObj = { ...propModel.attributes, ...{ value } };
if (layer) {
layer.properties.push(propertyObj);
} else {
layers[i] = {
properties: [propertyObj]
};
}
});
});
// Now whit all layers in, will check missing properties
layers.forEach(layer => {
const layerProprs = layer.properties.map(prop => prop.property);
properties.each(propModel => {
const propertyName = propModel.get('property');
if (layerProprs.indexOf(propertyName) < 0) {
layer.properties.push({ ...propModel.attributes });
}
});
});
return layers;
},
active(index) {
this.each(layer => layer.set('active', 0));
const layer = this.at(index);
layer && layer.set('active', 1);
},
getFullValue() {
let result = [];
this.each(layer => result.push(layer.getFullValue()));
return result.join(this.getSeparator());
},
getPropertyValues(property, defValue) {
const result = [];
this.each(layer => {
const value = layer.getPropertyValue(property);
value
? result.push(value)
: !isUndefined(defValue) && result.push(defValue);
});
return result.join(', ');
export default class Layers extends Collection {
initialize(p, opts = {}) {
this.prop = opts.prop;
}
});
}
Layers.prototype.model = Layer;

82
src/style_manager/model/Properties.js

@ -13,13 +13,22 @@ import PropertyRadio from './PropertyRadio';
import PropertyRadioView from './../view/PropertyRadioView';
import PropertySlider from './PropertySlider';
import PropertySliderView from './../view/PropertySliderView';
import PropertyInteger from './PropertyInteger';
import PropertyIntegerView from './../view/PropertyIntegerView';
import PropertyNumber from './PropertyNumber';
import PropertyNumberView from './../view/PropertyNumberView';
import PropertyView from './../view/PropertyView';
export default Backbone.Collection.extend(TypeableCollection).extend({
extendViewApi: 1,
init() {
const { opts, em } = this;
const sm = opts.module || em?.get('StyleManager');
if (sm) {
sm.__listenAdd(this, sm.events.propertyAdd);
sm.__listenRemove(this, sm.events.propertyRemove);
}
},
types: [
{
id: 'stack',
@ -29,7 +38,7 @@ export default Backbone.Collection.extend(TypeableCollection).extend({
if (value && value.type == 'stack') {
return value;
}
}
},
},
{
id: 'composite',
@ -39,7 +48,7 @@ export default Backbone.Collection.extend(TypeableCollection).extend({
if (value && value.type == 'composite') {
return value;
}
}
},
},
{
id: 'file',
@ -49,7 +58,7 @@ export default Backbone.Collection.extend(TypeableCollection).extend({
if (value && value.type == 'file') {
return value;
}
}
},
},
{
id: 'color',
@ -59,7 +68,7 @@ export default Backbone.Collection.extend(TypeableCollection).extend({
if (value && value.type == 'color') {
return value;
}
}
},
},
{
id: 'select',
@ -69,7 +78,7 @@ export default Backbone.Collection.extend(TypeableCollection).extend({
if (value && value.type == 'select') {
return value;
}
}
},
},
{
id: 'radio',
@ -79,7 +88,7 @@ export default Backbone.Collection.extend(TypeableCollection).extend({
if (value && value.type == 'radio') {
return value;
}
}
},
},
{
id: 'slider',
@ -89,17 +98,27 @@ export default Backbone.Collection.extend(TypeableCollection).extend({
if (value && value.type == 'slider') {
return value;
}
}
},
},
{
id: 'integer',
model: PropertyInteger,
view: PropertyIntegerView,
model: PropertyNumber,
view: PropertyNumberView,
isType(value) {
if (value && value.type == 'integer') {
return value;
}
}
},
},
{
id: 'number',
model: PropertyNumber,
view: PropertyNumberView,
isType(value) {
if (value && value.type == 'number') {
return value;
}
},
},
{
id: 'base',
@ -108,42 +127,7 @@ export default Backbone.Collection.extend(TypeableCollection).extend({
isType(value) {
value.type = 'base';
return value;
}
}
},
},
],
deepClone() {
const collection = this.clone();
collection.reset(
collection.map(model => {
const cloned = model.clone();
cloned.typeView = model.typeView;
return cloned;
})
);
return collection;
},
/**
* Parse a value and return an array splitted by properties
* @param {string} value
* @return {Array}
* @return
*/
parseValue(value) {
const properties = [];
const values = value.split(' ');
values.forEach((value, i) => {
const property = this.at(i);
if (!property) return;
properties.push({ ...property.attributes, ...{ value } });
});
return properties;
},
getFullValue() {
let result = '';
this.each(model => (result += `${model.getFullValue()} `));
return result.trim();
}
});

363
src/style_manager/model/Property.js

@ -1,19 +1,79 @@
import { Model } from 'common';
import { isUndefined, isString, result } from 'underscore';
import { capitalize } from 'utils/mixins';
import { isUndefined, isString, isArray, result, keys, each, includes } from 'underscore';
import { capitalize, camelCase, hasWin } from 'utils/mixins';
/**
* @typedef Property
* @property {String} id Property id, eg. `my-property-id`.
* @property {String} property Related CSS property name, eg. `text-align`.
* @property {String} default Defaul value of the property.
* @property {String} label Label to use in UI, eg. `Text Align`.
* @property {Function} [onChange] Change callback.
* \n
* ```js
* onChange: ({ property, from, to }) => {
* console.log(`Changed property`, property.getName(), { from, to });
* }
* ```
*
*/
export default class Property extends Model {
initialize(props = {}, opts = {}) {
this.em = opts.em;
const id = this.get('id') || '';
const name = this.get('name') || '';
!this.get('property') &&
this.set('property', (name || id).replace(/ /g, '-'));
const name = this.get('name') || this.get('label') || '';
!this.get('property') && this.set('property', (name || id).replace(/ /g, '-'));
const prop = this.get('property');
!this.get('id') && this.set('id', prop);
!name && this.set('name', capitalize(prop).replace(/-/g, ' '));
this.on('change', this.__upTargets);
Property.callInit(this, props, opts);
}
__getParentProp() {
return this.collection?.opts?.parentProp;
}
__upTargets(p, opts = {}) {
const { em } = this;
const sm = em.get('StyleManager');
const name = this.getName();
const isClear = opts.__clear;
const value = isClear ? '' : this.__getFullValue(opts);
const parentProp = this.__getParentProp();
const to = this.changedAttributes();
const from = keys(to).reduce((a, i) => {
a[i] = this.previous(i);
return a;
}, {});
const kProps = [...keys(this.__getClearProps()), '__p'];
const toProps = keys(to);
const applyStyle = !opts.__up && !parentProp && (isClear || kProps.some(k => toProps.indexOf(k) >= 0));
const onChange = this.get('onChange');
const evOpts = { property: this, from, to, value, opts };
sm.__trgEv(sm.events.propertyUpdate, evOpts);
onChange && onChange(evOpts);
applyStyle && this.__upTargetsStyle({ [name]: value }, opts);
}
__upTargetsStyle(style, opts) {
const sm = this.em?.get('StyleManager');
sm?.addStyleTargets({ ...style, __p: !!opts.avoidStore }, opts);
}
_up(props, opts = {}) {
if (opts.noTarget) opts.__up = true;
const { partial, ...rest } = opts;
props.__p = !!(rest.avoidStore || partial);
return this.set(props, { ...rest, avoidStore: props.__p });
}
up(props, opts = {}) {
this.set(props, { ...opts, __up: true });
}
init() {}
/**
@ -25,7 +85,9 @@ export default class Property extends Model {
}
/**
* Get property type.
* Get the property type.
* The type of the property is defined on property creation and based on its value the proper Property class is assigned.
* The default type is `base`.
* @returns {String}
*/
getType() {
@ -33,7 +95,7 @@ export default class Property extends Model {
}
/**
* Get property name (usually is the CSS property name).
* Get name (the CSS property name).
* @returns {String}
*/
getName() {
@ -42,29 +104,137 @@ export default class Property extends Model {
/**
* Get property label.
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.locale=true] Use the locale string from i18n module
* @returns {String}
*/
getLabel() {
return this.get('name');
getLabel(opts = {}) {
const { locale = true } = opts;
const id = this.getId();
const name = this.get('name') || this.get('label');
return (locale && this.em?.t(`styleManager.properties.${id}`)) || name;
}
/**
* Get property value.
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.noDefault=false] Avoid returning the default value
* @returns {String}
*/
getValue() {
getValue(opts = {}) {
const { noDefault } = opts;
const val = this.get('value');
return isUndefined(val) ? this.getDefaultValue() : val;
return !this.hasValue() && !noDefault ? this.getDefaultValue() : val;
}
/**
* Clear the value
* @return {this}
* @private
* Check if the property has value.
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.noParent=false] Ignore the value if it comes from the parent target.
* @returns {Boolean}
*/
hasValue(opts = {}) {
const { noParent } = opts;
const parentValue = noParent && this.getParentTarget();
const val = this.get('value');
return !isUndefined(val) && val !== '' && !parentValue;
}
/**
* Indicates if the current value is coming from a parent target (eg. another CSSRule).
* @returns {Boolean}
*/
clearValue(opts = {}) {
this.set({ value: undefined, status: '' }, opts);
return this;
hasValueParent() {
return this.hasValue() && !this.hasValue({ noParent: true });
}
/**
* Get the CSS style object of the property.
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.camelCase] Return property name in camelCase.
* @return {Object}
* @example
* // In case the property is `color` with a value of `red`.
* console.log(property.getStyle());
* // { color: 'red' };
*/
getStyle(opts = {}) {
const name = this.getName();
const key = opts.camelCase ? camelCase(name) : name;
return { [key]: this.__getFullValue(opts) };
}
/**
* Get the default value.
* @return {string}
*/
getDefaultValue() {
const def = this.get('default');
return `${!isUndefined(def) ? def : this.get('defaults')}`;
}
/**
* Update the value.
* The change is also propagated to the selected targets (eg. CSS rule).
* @param {String} value New value
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.partial=false] If `true` the update on targets won't be considered complete (not stored in UndoManager)
* @param {Boolean} [opts.noTarget=false] If `true` the change won't be propagated to selected targets.
*/
upValue(value, opts = {}) {
const parsed = value === null || value === '' ? this.__getClearProps() : this.__parseValue(value, opts);
return this._up(parsed, opts);
}
/**
* Check if the property is visible
* @returns {Boolean}
*/
isVisible() {
return !!this.get('visible');
}
/**
* Clear the value.
* The change is also propagated to the selected targets (eg. the css property is cleared).
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.noTarget=false] If `true` the change won't be propagated to selected targets.
*/
clear(opts = {}) {
this._up(this.__getClearProps(), { ...opts, __clear: true });
}
/**
* Indicates if the current value comes directly from the selected target and so can be cleared.
* @returns {Boolean}
*/
canClear() {
const parent = this.getParent();
return parent ? parent.__canClearProp(this) : this.hasValue({ noParent: true });
}
/**
* If the current property is a sub-property, this will return the parent Property.
* @returns {[Property]|null}
*/
getParent() {
return this.__getParentProp() || null;
}
/**
* Indicates if the property is full-width in UI.
* @returns {Boolean}
*/
isFull() {
return !!this.get('full');
}
__parseValue(value, opts) {
return this.parseValue(value, opts);
}
__getClearProps() {
return { value: '' };
}
/**
@ -88,6 +258,8 @@ export default class Property extends Model {
* @param {any} value
* @param {Boolen} [complete=true] Indicates if it's a final state
* @param {Object} [opts={}] Options
* @private
* @deprecated
*/
setValueFromInput(value, complete, opts = {}) {
this.setValue(value, complete, { ...opts, fromInput: 1 });
@ -151,47 +323,42 @@ export default class Property extends Model {
* @param {String} [separator] Separator
* @private
*/
splitValues(values, separator = ',') {
const res = [];
const op = '(';
const cl = ')';
let curr = '';
let acc = 0;
(values || '').split('').forEach(str => {
if (str == op) {
acc++;
curr = curr + op;
} else if (str == cl && acc > 0) {
acc--;
curr = curr + cl;
} else if (str === separator && acc == 0) {
res.push(curr);
curr = '';
} else {
curr = curr + str;
}
});
curr !== '' && res.push(curr);
return res.map(i => i.trim());
}
/**
* Get the default value
* @return {string}
* @private
*/
getDefaultValue() {
return this.get('defaults');
// splitValues(values, separator = ',') {
// const res = [];
// const op = '(';
// const cl = ')';
// let curr = '';
// let acc = 0;
// (values || '').split('').forEach(str => {
// if (str == op) {
// acc++;
// curr = curr + op;
// } else if (str == cl && acc > 0) {
// acc--;
// curr = curr + cl;
// } else if (str === separator && acc == 0) {
// res.push(curr);
// curr = '';
// } else {
// curr = curr + str;
// }
// });
// curr !== '' && res.push(curr);
// return res.map(i => i.trim());
// }
__getFullValue({ withDefault } = {}) {
return !this.hasValue() && withDefault ? this.getDefaultValue() : this.getFullValue();
}
/**
* Get a complete value of the property.
* This probably will replace the getValue when all
* properties models will be splitted
* @param {string} val Custom value to replace the one on the model
* @param {String} val Custom value to replace the one on the model
* @return {string}
* @private
*/
@ -216,23 +383,100 @@ export default class Property extends Model {
return value || '';
}
__setParentTarget(value) {
this.__parentTarget = value;
}
getParentTarget() {
return this.__parentTarget || null;
}
__parseFn(input = '') {
const start = input.indexOf('(') + 1;
const end = input.lastIndexOf(')');
return {
name: input.substring(0, start - 1).trim(),
value: String.prototype.substring.apply(input, [start, end >= 0 ? end : undefined]).trim(),
};
}
__checkVisibility({ target, component, sectors }) {
const trg = component || target;
if (!trg) return false;
const id = this.getId();
const property = this.getName();
const toRequire = this.get('toRequire');
const requires = this.get('requires');
const requiresParent = this.get('requiresParent');
const unstylable = trg.get('unstylable');
const stylableReq = trg.get('stylable-require');
let stylable = trg.get('stylable');
// Stylable could also be an array indicating with which property
// the target could be styled
if (isArray(stylable)) {
stylable = stylable.indexOf(property) >= 0;
}
// Check if the property was signed as unstylable
if (isArray(unstylable)) {
stylable = unstylable.indexOf(property) < 0;
}
// Check if the property is available only if requested
if (toRequire) {
stylable = !target || (stylableReq && (stylableReq.indexOf(id) >= 0 || stylableReq.indexOf(property) >= 0));
}
// Check if the property is available based on other property's values
if (sectors && requires) {
const properties = keys(requires);
sectors.forEach(sector => {
sector.getProperties().forEach(model => {
if (includes(properties, model.id)) {
const values = requires[model.id];
stylable = stylable && includes(values, model.get('value'));
}
});
});
}
// Check if the property is available based on parent's property values
if (requiresParent) {
const parent = component && component.parent();
const parentEl = parent && parent.getEl();
if (parentEl) {
const styles = hasWin() ? window.getComputedStyle(parentEl) : {};
each(requiresParent, (values, property) => {
stylable = stylable && styles[property] && includes(values, styles[property]);
});
} else {
stylable = false;
}
}
return !!stylable;
}
}
Property.callParentInit = function(property, ctx, props, opts = {}) {
Property.callParentInit = function (property, ctx, props, opts = {}) {
property.prototype.initialize.apply(ctx, [
props,
{
...opts,
skipInit: 1
}
skipInit: 1,
},
]);
};
Property.callInit = function(context, props, opts = {}) {
Property.callInit = function (context, props, opts = {}) {
!opts.skipInit && context.init(props, opts);
};
Property.getDefaults = function() {
Property.getDefaults = function () {
return result(this.prototype, 'defaults');
};
@ -248,6 +492,7 @@ Property.prototype.defaults = {
status: '',
visible: true,
fixedValues: ['initial', 'inherit'],
onChange: null,
// If true, the property will be forced to be full width
full: 0,
@ -272,5 +517,5 @@ Property.prototype.defaults = {
// Specifies dependency on properties of the parent of the selected object.
// Property is shown only when all conditions are matched.
requiresParent: null
requiresParent: null,
};

373
src/style_manager/model/PropertyComposite.js

@ -1,101 +1,324 @@
import { isString, isUndefined, keys } from 'underscore';
import Property from './Property';
import Properties from './Properties';
export default Property.extend({
defaults: {
...Property.prototype.defaults,
// 'background' is a good example where to make a difference
// between detached and not
//
// - NOT detached (default)
// background: url(..) no-repeat center ...;
// - Detached
// background-image: url();
// background-repeat: repeat;
// ...
detached: 0,
// Array of sub properties
properties: [],
// Separator between properties
separator: ' '
},
export const isNumberType = type => type === 'integer' || type === 'number';
/**
*
* [Property]: property.html
*
*
* @typedef PropertyComposite
* @property {Array<Object>} properties Array of sub properties, eg. `[{ type: 'number', property: 'margin-top' }, ...]`
* @property {Boolean} [detached=false] Indicate if the final CSS property is splitted (detached: `margin-top: X; margin-right: Y; ...`) or combined (not detached: `margin: X Y ...;`)
* @property {String|RegExp} [separator=' '] Value used to split property values, default `" "`.
* @property {String} [join=' '] Value used to join property values, default `" "`.
* @property {Function} [fromStyle] Custom logic for getting property values from the target style object.
* \n
* ```js
* fromStyle: (style) => {
* const margins = parseMarginShorthand(style.margin);
* return {
* 'margin-top': margins.top,
* // ...
* };
* }
* ```
* @property {Function} [toStyle] Custom logic for creating the CSS style object to apply on selected targets.
* \n
* ```js
* toStyle: (values) => {
* const top = values['margin-top'] || 0;
* const right = values['margin-right'] || 0;
* // ...
* return {
* margin: `${top} ${right} ...`,
* };
* }
* ```
*/
export default class PropertyComposite extends Property {
defaults() {
return {
...Property.getDefaults(),
detached: false,
properties: [],
separator: ' ',
join: null,
fromStyle: null,
toStyle: null,
full: true,
};
}
initialize(props = {}, opts = {}) {
Property.callParentInit(Property, this, props, opts);
const properties = this.get('properties') || [];
const Properties = require('./Properties').default;
this.set('properties', new Properties(properties));
this.listenTo(this, 'change:value', this.updateValues);
const { em } = this;
const properties = new Properties(this.get('properties') || [], { em, parentProp: this });
this.set('properties', properties, { silent: 1 });
this.listenTo(properties, 'change', this.__upProperties);
Property.callInit(this, props, opts);
},
}
/**
* Clear the value
* @return {this}
* Get properties.
* @returns {Array<[Property]>}
*/
clearValue(opts = {}) {
this.get('properties').each(property => property.clearValue());
return Property.prototype.clearValue.apply(this, arguments);
},
getProperties() {
return [...this.get('properties').models];
}
/**
* Update property values
* Get property by id.
* @param {String} id Property id.
* @returns {[Property]|null}
*/
updateValues() {
const values = this.getFullValue().split(this.getSplitSeparator());
this.get('properties').each((property, i) => {
const len = values.length;
// Try to get value from a shorthand:
// 11px -> 11px 11px 11px 11xp
// 11px 22px -> 11px 22px 11px 22xp
const value =
values[i] || values[(i % len) + (len != 1 && len % 2 ? 1 : 0)];
// There some issue with UndoManager
//property.setValue(value, 0, {fromParent: 1});
});
},
getProperty(id) {
return this.get('properties').filter(prop => prop.getId() === id || prop.getName() === id)[0] || null;
}
/**
* Split by sperator but avoid it inside parenthesis
* @return {RegExp}
* Get property at index.
* @param {Number} index
* @returns {[Property]|null}
*/
getSplitSeparator() {
return new RegExp(`${this.get('separator')}(?![^\\(]*\\))`);
},
getPropertyAt(index) {
return this.get('properties').at(index);
}
/**
* Check if the property is detached.
* @returns {Boolean}
*/
isDetached() {
return !!this.get('detached');
}
/**
* Get current values of properties.
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.byName=false] Use property names as a key instead of the id.
* @returns {Object}
* @example
* // In case the property is `margin` with sub properties like `margin-top`, `margin-right`, etc.
* console.log(property.getValues());
* // { 'margin-top': '10px', 'margin-right': '20px', ... };
*/
getValues({ byName } = {}) {
return this.getProperties().reduce((res, prop) => {
const key = byName ? prop.getName() : prop.getId();
res[key] = `${prop.__getFullValue()}`;
return res;
}, {});
}
/**
* Get property separator.
* @returns {RegExp}
*/
getSeparator() {
return this.getSplitSeparator();
}
/**
* Get the join value.
* @returns {String}
*/
getJoin() {
return this.__getJoin();
}
/**
* Returns default value
* @param {Boolean} defaultProps Force to get defaults from properties
* @return {string}
* Get style object from current properties
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.camelCase] Return property names in camelCase.
* @returns {Object} Style object
* @private
*/
getDefaultValue(defaultProps) {
let value = this.get('defaults');
getStyleFromProps(opts = {}) {
const name = this.getName();
const join = this.__getJoin();
const toStyle = this.get('toStyle');
let values = this.getValues();
let style = {};
if (value && !defaultProps) {
return value;
if (toStyle) {
style = toStyle(values, { join, name, property: this });
} else {
values = this.getValues({ byName: true });
if (this.isDetached()) {
style = values;
} else {
const value = this.getProperties()
.map(p => p.__getFullValue({ withDefault: 1 }))
.filter(Boolean)
.join(join);
style = { [name]: value };
}
}
value = '';
const properties = this.get('properties');
properties.each((prop, index) => (value += `${prop.getDefaultValue()} `));
return value.trim();
},
if (this.isDetached()) {
style[name] = '';
} else {
style[name] = style[name] || '';
style = {
...style,
...this.getProperties().reduce((acc, prop) => {
acc[prop.getName()] = '';
return acc;
}, {}),
};
}
getFullValue() {
if (this.get('detached')) {
return '';
return opts.camelCase
? Object.keys(style).reduce((res, key) => {
res[camelCase(key)] = style[key];
return res;
}, {})
: style;
}
getSplitSeparator() {
return new RegExp(`${this.get('separator')}(?![^\\(]*\\))`);
}
__upProperties(p, opts = {}) {
if (opts.__up || opts.__clearIn) return;
const parentProp = this.__getParentProp();
if (parentProp) return parentProp.__upProperties(this, opts);
this.__upTargetsStyleProps(opts, p);
}
__upTargetsStyleProps(opts = {}, prop) {
let style = this.getStyleFromProps();
if (this.isDetached() && prop) {
const name = prop.getName();
style = { [name]: style[name] };
}
return this.get('properties').getFullValue();
},
this.__upTargetsStyle(style, opts);
}
/**
* Get property at some index
* @param {Number} index
* @return {Object}
*/
getPropertyAt(index) {
return this.get('properties').at(index);
_up(props, opts = {}) {
this.__setProperties(this.__getSplitValue(props.value), opts);
return Property.prototype._up.call(this, props, opts);
}
getStyle(opts) {
return this.getStyleFromProps(opts);
}
__getFullValue(opts = {}) {
if (this.isDetached() || opts.__clear) return '';
return this.getStyleFromProps()[this.getName()] || '';
}
__getJoin() {
const join = this.get('join');
return isString(join) ? join : this.get('separator');
}
__styleHasProps(style = {}) {
const name = this.getName();
const props = this.getProperties();
const nameProps = props.map(prop => prop.getName());
const allNameProps = [name, ...nameProps];
return allNameProps.some(prop => !isUndefined(style[prop]) && style[prop] !== '');
}
__splitValue(value, sep) {
return value
.split(sep)
.map(value => value.trim())
.filter(Boolean);
}
__splitStyleName(style, name, sep) {
return this.__splitValue(style[name] || '', sep);
}
__getSplitValue(value = '', { byName } = {}) {
const props = this.getProperties();
const props4Nums = props.length === 4 && props.every(prop => isNumberType(prop.getType()));
const values = this.__splitValue(value, this.getSplitSeparator());
const result = {};
props.forEach((prop, i) => {
const value = values[i];
let res = !isUndefined(value) ? value : ''; // : prop.getDefaultValue();
if (props4Nums) {
// Try to get value from a shorthand:
// 11px -> 11px 11px 11px 11xp
// 11px 22px -> 11px 22px 11px 22xp
const len = values.length;
res = values[i] || values[(i % len) + (len != 1 && len % 2 ? 1 : 0)] || res;
}
const key = byName ? prop.getName() : prop.getId();
result[key] = res || '';
});
return result;
}
__getPropsFromStyle(style = {}, opts = {}) {
if (!this.__styleHasProps(style)) return null;
const { byName } = opts;
const props = this.getProperties();
const sep = this.getSplitSeparator();
const fromStyle = this.get('fromStyle');
let result = fromStyle ? fromStyle(style, { property: this, separator: sep }) : {};
if (!fromStyle) {
// Get props from the main property
result = this.__getSplitValue(style[this.getName()] || '', { byName });
// Get props from the inner properties
props.forEach(prop => {
const value = style[prop.getName()];
const key = byName ? prop.getName() : prop.getId();
if (!isUndefined(value) && value !== '') result[key] = value;
});
}
return result;
}
__setProperties(values = {}, opts = {}) {
this.getProperties().forEach(prop => {
const value = values[prop.getId()];
prop.__getFullValue() !== value && prop.upValue(value, opts);
});
// Keep track of the values, otherwise clear() will not trigger changes.
const valuesStr = keys(values)
.map(k => values[k])
.join(' ');
this.set('value', valuesStr, { silent: true });
}
clear() {
this.getProperties().map(p => p.clear({ __clearIn: !this.isDetached() }));
return Property.prototype.clear.call(this);
}
hasValue(opts) {
return this.getProperties().some(prop => prop.hasValue(opts));
}
getFullValue() {
return this.__getFullValue();
}
__canClearProp(prop) {
return this.isDetached() && prop.hasValue({ noParent: true });
}
});
}

1127
src/style_manager/model/PropertyFactory.js

File diff suppressed because it is too large

76
src/style_manager/model/PropertyInteger.js

@ -1,76 +0,0 @@
import { isUndefined } from 'underscore';
import Property from './Property';
import InputNumber from 'domain_abstract/ui/InputNumber';
import { hasWin } from 'utils/mixins';
export default Property.extend({
defaults: {
...Property.prototype.defaults,
// Array of units, eg. ['px', '%']
units: [],
// Selected unit, eg. 'px'
unit: '',
// Integer value steps
step: 1,
// Minimum value
min: '',
// Maximum value
max: ''
},
/**
* Get property units.
* @returns {Array<String>}
*/
getUnits() {
return this.get('units') || [];
},
/**
* Get property unit value.
* @returns {String}
*/
getValueUnit() {
return this.get('unit');
},
initialize(props = {}, opts = {}) {
Property.callParentInit(Property, this, props, opts);
const unit = this.get('unit');
const units = this.get('units');
this.input = hasWin() && new InputNumber({ model: this });
if (units.length && !unit) {
this.set('unit', units[0]);
}
Property.callInit(this, props, opts);
},
clearValue(opts = {}) {
this.set({ value: undefined, unit: undefined }, opts);
return this;
},
parseValue(val) {
const parsed = Property.prototype.parseValue.apply(this, arguments);
const { value, unit } = this.input.validateInputValue(parsed.value, {
deepCheck: 1
});
parsed.value = value;
parsed.unit = unit;
return parsed;
},
getFullValue() {
let value = this.get('value');
let unit = this.get('unit');
value = !isUndefined(value) ? value : '';
unit = !isUndefined(unit) && value ? unit : '';
value = `${value}${unit}`;
return Property.prototype.getFullValue.apply(this, [value]);
}
});

116
src/style_manager/model/PropertyNumber.js

@ -0,0 +1,116 @@
import { isUndefined } from 'underscore';
import Property from './Property';
import InputNumber from 'domain_abstract/ui/InputNumber';
import { hasWin } from 'utils/mixins';
/**
* @typedef PropertyNumber
* @property {Array<String>} units Array of units, eg. `['px', '%']`
* @property {Number} min Minimum value.
* @property {Number} max Maximum value.
* @property {Number} step Step value.
*
*/
export default class PropertyNumber extends Property {
defaults() {
return {
...Property.getDefaults(),
units: [],
unit: '',
min: '',
max: '',
step: 1,
};
}
/**
* Get property units.
* @returns {Array<String>}
*/
getUnits() {
return this.get('units') || [];
}
/**
* Get property unit value.
* @returns {String}
*/
getUnit() {
return this.get('unit');
}
/**
* Get min value.
* @returns {Number}
*/
getMin() {
return this.get('min');
}
/**
* Get max value.
* @returns {Number}
*/
getMax() {
return this.get('max');
}
/**
* Get step value.
* @returns {Number}
*/
getStep() {
return this.get('step');
}
/**
* Update property unit value.
* The change is also propagated to the selected targets.
* @param {String} unit New unit value
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.noTarget=false] If `true` the change won't be propagated to selected targets.
* @returns {String}
*/
upUnit(unit, opts) {
return this._up({ unit }, opts);
}
initialize(props = {}, opts = {}) {
Property.callParentInit(Property, this, props, opts);
const unit = this.get('unit');
const units = this.get('units');
this.input = hasWin() && new InputNumber({ model: this });
if (units.length && !unit) {
this.set('unit', units[0], { silent: 1 });
}
Property.callInit(this, props, opts);
}
__getClearProps() {
return {
...Property.prototype.__getClearProps(),
unit: '',
};
}
parseValue(val, opts = {}) {
const parsed = Property.prototype.parseValue.apply(this, arguments);
const { value, unit } = this.input.validateInputValue(parsed.value, {
deepCheck: 1,
...opts,
});
parsed.value = value;
parsed.unit = unit;
return parsed;
}
getFullValue() {
let value = this.get('value');
let unit = this.get('unit');
value = !isUndefined(value) ? value : '';
unit = !isUndefined(unit) && value ? unit : '';
value = `${value}${unit}`;
return Property.prototype.getFullValue.apply(this, [value]);
}
}

45
src/style_manager/model/PropertyRadio.js

@ -1,37 +1,10 @@
import Property from './Property';
export default Property.extend({
defaults: () => ({
...Property.prototype.defaults,
// Array of options, eg. [{name: 'Label ', value: '100'}]
options: [],
full: 1
}),
initialize(...args) {
Property.prototype.initialize.apply(this, args);
this.listenTo(this, 'change:options', this.onOptionChange);
},
onOptionChange() {
this.set('list', this.get('options'));
},
getOptions() {
const { options, list } = this.attributes;
return options && options.length ? options : list;
},
setOptions(opts = []) {
this.set('options', opts);
return this;
},
addOption(opt) {
if (opt) {
const opts = this.getOptions();
this.setOptions([...opts, opt]);
}
return this;
import Property from './PropertySelect';
export default class PropertyRadio extends Property {
defaults() {
return {
...Property.getDefaults(),
full: 1,
};
}
});
}

107
src/style_manager/model/PropertySelect.js

@ -1,8 +1,99 @@
import Property from './PropertyRadio';
export default Property.extend({
defaults: () => ({
...Property.prototype.defaults(),
full: 0
})
});
import Property from './Property';
import { isDef } from 'utils/mixins';
import { isString } from 'underscore';
/**
* @typedef PropertySelect
* @property {Array<Object>} options Array of option definitions.
* \n
* ```js
* options: [
* { id: '100', label: 'Set 100' },
* { id: '200', label: 'Set 200' },
* ]
* ```
*/
export default class PropertySelect extends Property {
defaults() {
return {
...Property.getDefaults(),
options: [],
full: 0,
};
}
/**
* Get available options.
* @returns {Array<Object>} Array of options
*/
getOptions() {
// support old list property
const { options, list } = this.attributes;
return (options && options.length ? options : list) || [];
}
/**
* Get current selected option or by id.
* @param {String} [id] Option id.
* @returns {Object | null}
*/
getOption(id) {
const idSel = id || this.getValue();
return this.getOptions().filter(o => this.getOptionId(o) === idSel)[0] || null;
}
/**
* Update options.
* @param {Array<Object>} value New array of options, eg. `[{ id: 'val-1', label: 'Value 1' }]`
*/
setOptions(value = []) {
this.set('options', value);
return this;
}
/**
* Add new option.
* @param {Object} value Option object, eg. `{ id: 'val-1', label: 'Value 1' }`
*/
addOption(value) {
if (value) {
const opts = this.getOptions();
this.setOptions([...opts, value]);
}
return this;
}
/**
* Get the option id from the option object.
* @param {Object} option Option object
* @returns {String} Option id
*/
getOptionId(option) {
return isDef(option.id) ? option.id : option.value;
}
/**
* Get option label.
* @param {String|Object} id Option id or the option object
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.locale=true] Use the locale string from i18n module
* @returns {String} Option label
*/
getOptionLabel(id, opts = {}) {
const { locale = true } = opts;
const option = (isString(id) ? this.getOption(id) : id) || {};
const optId = this.getOptionId(option);
const label = option.label || option.name || optId;
const propId = this.getId();
return (locale && this.em?.t(`styleManager.options.${propId}.${optId}`)) || label;
}
initialize(...args) {
Property.prototype.initialize.apply(this, args);
this.listenTo(this, 'change:options', this.__onOptionChange);
}
__onOptionChange() {
this.set('list', this.get('options'));
}
}

14
src/style_manager/model/PropertySlider.js

@ -1,8 +1,10 @@
import Property from './PropertyInteger';
import Property from './PropertyNumber';
export default Property.extend({
defaults: {
...Property.prototype.defaults,
showInput: 1
export default class PropertySlider extends Property {
defaults() {
return {
...Property.getDefaults(),
showInput: 1,
};
}
});
}

533
src/style_manager/model/PropertyStack.js

@ -1,89 +1,480 @@
import { keys } from 'underscore';
import Property from './PropertyComposite';
import { keys, isUndefined, isArray, isString, isNumber } from 'underscore';
import PropertyComposite, { isNumberType } from './PropertyComposite';
import PropertyBase from './Property';
import Layers from './Layers';
import { camelCase } from 'utils/mixins';
export default Property.extend({
defaults: {
...Property.prototype.defaults,
// Array of layers (which contain properties)
layers: [],
const VALUES_REG = /,(?![^\(]*\))/;
const PARTS_REG = /\s(?![^(]*\))/;
// The separator used to join layer values
layerSeparator: ', ',
// Prepend new layers in the list
prepend: 0,
// Layer preview
preview: 0
},
/**
*
* [Layer]: layer.html
*
*
* @typedef PropertyStack
* @property {Boolean} [preview=false] Indicate if the layer should display a preview.
* @property {String|RegExp} [layerSeparator=', '] The separator used to split layer values.
* @property {String} [layerJoin=', '] Value used to join layer values.
* @property {Function} [layerLabel] Custom logic for creating layer labels.
* \n
* ```js
* layerLabel: (layer) => {
* const values = layer.getValues();
* return `A: ${values['prop-a']} B: ${values['prop-b']}`;
* }
* ```
*
*/
export default class PropertyStack extends PropertyComposite {
defaults() {
return {
...PropertyComposite.getDefaults(),
layers: [],
layerSeparator: ', ',
layerJoin: '',
prepend: 0,
preview: false,
layerLabel: null,
selectedLayer: null,
};
}
initialize(props = {}, opts = {}) {
Property.callParentInit(Property, this, props, opts);
PropertyComposite.callParentInit(PropertyComposite, this, props, opts);
const layers = this.get('layers');
const layersColl = new Layers(layers);
const layersColl = new Layers(layers, { prop: this });
layersColl.property = this;
layersColl.properties = this.get('properties');
this.set('layers', layersColl);
Property.callInit(this, props, opts);
},
this.set('layers', layersColl, { silent: true });
this.on('change:selectedLayer', this.__upSelected);
this.listenTo(layersColl, 'add remove', this.__upLayers);
PropertyComposite.callInit(this, props, opts);
}
/**
* Get all available layers.
* @returns {Array<[Layer]>}
*/
getLayers() {
return this.__getLayers().models;
}
__getLayers() {
return this.get('layers');
},
getCurrentLayer() {
return this.getLayers().filter(layer => layer.get('active'))[0];
},
getFullValue() {
return this.get('detached') ? '' : this.get('layers').getFullValue();
},
getValueFromStyle(styles = {}) {
const layers = this.getLayers().getLayersFromStyle(styles);
return new Layers(layers).getFullValue();
},
clearValue() {
this.getLayers().reset();
return Property.prototype.clearValue.apply(this, arguments);
},
getValueFromTarget(target) {
const { detached, property, properties } = this.attributes;
const style = target.getStyle();
const validStyles = {};
properties.forEach(prop => {
const name = prop.get('property');
const value = style[name];
if (value) validStyles[name] = value;
}
/**
* Get layer by index.
* @param {Number} [index=0] Layer index position.
* @returns {[Layer]|null}
* @example
* // Get the first layer
* const layerFirst = property.getLayer(0);
* // Get the last layer
* const layers = this.getLayers();
* const layerLast = property.getLayer(layers.length - 1);
*/
getLayer(index = 0) {
return this.__getLayers().at(index) || null;
}
/**
* Get selected layer.
* @returns {[Layer] | null}
*/
getSelectedLayer() {
const layer = this.get('selectedLayer');
return layer && layer.getIndex() >= 0 ? layer : null;
}
/**
* Select layer.
* Without a selected layer any update made on inner properties has no effect.
* @param {[Layer]} layer Layer to select
* @example
* const layer = property.getLayer(0);
* property.selectLayer(layer);
*/
selectLayer(layer) {
return this.set('selectedLayer', layer, { __select: true });
}
/**
* Select layer by index.
* @param {Number} index Index of the layer to select.
* @example
* property.selectLayerAt(1);
*/
selectLayerAt(index = 0) {
const layer = this.getLayer(index);
return layer && this.selectLayer(layer);
}
/**
* Move layer by index.
* @param {[Layer]} layer Layer to move.
* @param {Number} index New layer index.
* @example
* const layer = property.getLayer(1);
* property.moveLayer(layer, 0);
*/
moveLayer(layer, index = 0) {
const currIndex = layer ? layer.getIndex() : -1;
if (currIndex >= 0 && isNumber(index) && index >= 0 && index < this.getLayers().length && currIndex !== index) {
this.removeLayer(layer);
this.__getLayers().add(layer, { at: index });
}
}
/**
* Add new layer to the stack.
* @param {Object} [props={}] Custom property values to use in a new layer.
* @param {Object} [opts={}] Options
* @param {Number} [opts.at] Position index (by default the layer will be appended at the end).
* @returns {[Layer]} Added layer.
* @example
* // Add new layer at the beginning of the stack with custom values
* property.addLayer({ 'sub-prop1': 'value1', 'sub-prop2': 'value2' }, { at: 0 });
*/
addLayer(props = {}, opts = {}) {
const values = {};
this.getProperties().forEach(prop => {
const key = prop.getId();
const value = props[key];
values[key] = isUndefined(value) ? prop.getDefaultValue() : value;
});
const layer = this.get('layers').push({ values }, opts);
return layer;
}
return !detached
? style[property]
: keys(validStyles).length
? validStyles
: '';
},
/**
* Remove layer.
* @param {[Layer]} layer Layer to remove.
* @returns {[Layer]} Removed layer
* @example
* const layer = property.getLayer(0);
* property.removeLayer(layer);
*/
removeLayer(layer) {
return this.get('layers').remove(layer);
}
/**
* This method allows to customize layers returned from the target
* @param {Object} target
* @return {Array} Should return an array of layers
* Remove layer by index.
* @param {Number} index Index of the layer to remove
* @returns {[Layer]|null} Removed layer
* @example
* // return example
* [
* {
* properties: [
* { property: 'width', ... }
* { property: 'height', ... }
* ]
* }
* ]
* property.removeLayerAt(0);
*/
removeLayerAt(index = 0) {
const layer = this.getLayer(index);
return layer ? this.removeLayer(layer) : null;
}
/**
* Get the layer label. The label can be customized with the `layerLabel` property.
* @param {[Layer]} layer
* @returns {String}
* @example
* const layer = this.getLayer(1);
* const label = this.getLayerLabel(layer);
*/
getLayerLabel(layer) {
let result = '';
if (layer) {
const layerLabel = this.get('layerLabel');
const values = layer.getValues();
const index = layer.getIndex();
if (layerLabel) {
result = layerLabel(layer, { index, values, property: this });
} else {
const parts = [];
this.getProperties().map(prop => {
parts.push(values[prop.getId()]);
});
result = parts.filter(Boolean).join(' ');
}
}
return result;
}
/**
* Get style object from the layer.
* @param {[Layer]} layer
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.camelCase] Return property names in camelCase.
* @param {Object} [opts.number] Limit the result of the number types, eg. `number: { min: -3, max: 3 }`
* @returns {Object} Style object
*/
getStyleFromLayer(layer, opts = {}) {
const join = this.__getJoin();
const joinLayers = this.__getJoinLayers();
const toStyle = this.get('toStyle');
const name = this.getName();
const values = layer.getValues();
let style;
if (toStyle) {
style = toStyle(values, { join, joinLayers, name, layer, property: this });
} else {
const result = this.getProperties().map(prop => {
const name = prop.getName();
const val = values[prop.getId()];
let value = isUndefined(val) ? prop.getDefaultValue() : val;
// Limit number values if necessary (useful for previews)
if (opts.number && isNumberType(prop.getType())) {
const newVal = prop.parseValue(val, opts.number);
value = `${newVal.value}${newVal.unit}`;
}
return { name, value };
});
style = this.isDetached()
? result.reduce((acc, item) => {
acc[item.name] = item.value;
return acc;
}, {})
: {
[this.getName()]: result.map(r => r.value).join(join),
};
}
return opts.camelCase
? Object.keys(style).reduce((res, key) => {
res[camelCase(key)] = style[key];
return res;
}, {})
: style;
}
/**
* Get preview style object from the layer.
* If the property has `preview: false` the returned object will be empty.
* @param {[Layer]} layer
* @param {Object} [opts={}] Options. Same of `getStyleFromLayer`
* @returns {Object} Style object
*/
getStylePreview(layer, opts = {}) {
let result = {};
const preview = this.get('preview');
if (preview) {
result = this.getStyleFromLayer(layer, opts);
}
return result;
}
/**
* Get layer separator.
* @return {RegExp}
*/
getLayerSeparator() {
const sep = this.get('layerSeparator');
return isString(sep) ? new RegExp(`${sep}(?![^\\(]*\\))`) : sep;
}
__upProperties(prop, opts = {}) {
const layer = this.getSelectedLayer();
if (!layer) return;
layer.upValues({ [prop.getId()]: prop.__getFullValue() });
if (opts.__up) return;
this.__upTargetsStyleProps(opts);
}
__upLayers(m, c, o) {
this.__upTargetsStyleProps(o || c);
}
__upTargets(p, opts = {}) {
if (opts.__select) return;
return PropertyBase.prototype.__upTargets.call(this, p, opts);
}
__upTargetsStyleProps(opts = {}) {
this.__upTargetsStyle(this.getStyleFromLayers(), opts);
}
__upTargetsStyle(style, opts) {
return PropertyBase.prototype.__upTargetsStyle.call(this, style, opts);
}
__upSelected({ noEvent } = {}, opts = {}) {
const sm = this.em.get('StyleManager');
const selected = this.getSelectedLayer();
const values = selected?.getValues();
// Update properties by layer value
values &&
this.getProperties().forEach(prop => {
const value = values[prop.getId()];
prop.__getFullValue() !== value && prop.upValue(value, { ...opts, __up: true });
});
!noEvent && sm.__trgEv(sm.events.layerSelect, { property: this });
}
_up(props, opts = {}) {
const { __layers = [], ...rest } = props;
// Detached props will update their layers later in sm.__upProp
!this.isDetached() && this.__setLayers(__layers);
this.__upSelected({ noEvent: true }, opts);
return PropertyBase.prototype._up.call(this, rest, opts);
}
__setLayers(newLayers = []) {
const layers = this.__getLayers();
const layersNew = newLayers.map(values => ({ values }));
if (layers.length === layersNew.length) {
layersNew.map((layer, n) => layers.at(n)?.upValues(layer.values));
} else {
this.__getLayers().reset(layersNew);
}
this.__upSelected({ noEvent: true });
}
__parseValue(value) {
const result = this.parseValue(value);
result.__layers = value
.split(VALUES_REG)
.map(v => v.trim())
.map(v => this.__parseLayer(v))
.filter(Boolean);
return result;
}
__parseLayer(value) {
const parseFn = this.get('parseLayer');
const values = value.split(PARTS_REG);
const properties = this.getProperties();
return parseFn
? parseFn({ value, values })
: properties.reduce((acc, prop, i) => {
const value = values[i];
acc[prop.getId()] = !isUndefined(value) ? value : prop.getDefaultValue();
return acc;
}, {});
}
__getLayersFromStyle(style = {}) {
if (!this.__styleHasProps(style)) return null;
const name = this.getName();
const props = this.getProperties();
const sep = this.getLayerSeparator();
const fromStyle = this.get('fromStyle');
let result = fromStyle ? fromStyle(style, { property: this, separatorLayers: sep }) : [];
if (!fromStyle) {
// Get layers from the main property
const layers = this.__splitStyleName(style, name, sep)
.map(value => value.split(this.getSplitSeparator()))
.map(parts => {
const result = {};
props.forEach((prop, i) => {
const value = parts[i];
result[prop.getId()] = !isUndefined(value) ? value : prop.getDefaultValue();
});
return result;
});
// Get layers from the inner properties
props.forEach(prop => {
const id = prop.getId();
this.__splitStyleName(style, prop.getName(), sep)
.map(value => ({ [id]: value || prop.getDefaultValue() }))
.forEach((inLayer, i) => {
layers[i] = layers[i] ? { ...layers[i], ...inLayer } : inLayer;
});
});
result = layers;
}
return isArray(result) ? result : [result];
}
getStyle(opts) {
return this.getStyleFromLayers(opts);
}
getStyleFromLayers(opts) {
let result = {};
const name = this.getName();
const layers = this.getLayers();
const props = this.getProperties();
const styles = layers.map(l => this.getStyleFromLayer(l, opts));
styles.forEach(style => {
keys(style).map(key => {
if (!result[key]) result[key] = [];
result[key].push(style[key]);
});
});
keys(result).map(key => {
result[key] = result[key].join(this.__getJoinLayers());
});
if (this.isDetached()) {
result[name] = '';
!layers.length &&
props.map(prop => {
result[prop.getName()] = '';
});
} else {
const style = props.reduce((acc, prop) => {
acc[prop.getName()] = '';
return acc;
}, {});
result[name] = result[name] || '';
result = { ...result, ...style };
}
return result;
}
__getJoinLayers() {
const join = this.get('layerJoin');
const sep = this.get('layerSeparator');
return join || (isString(sep) ? sep : join);
}
__getFullValue() {
if (this.get('detached')) return '';
const style = this.getStyleFromLayers();
return style[this.getName()];
}
/**
* Extended
* @private
*/
getLayersFromTarget(target) {
return;
hasValue(opts = {}) {
const { noParent } = opts;
const parentValue = noParent && this.getParentTarget();
return this.getLayers().length > 0 && !parentValue;
}
/**
* Extended
* @private
*/
clear(opts = {}) {
this.__getLayers().reset();
this.__upTargetsStyleProps(opts);
return PropertyBase.prototype.clear.call(this);
}
__canClearProp() {
return false;
}
});
}

97
src/style_manager/model/Sector.js

@ -1,16 +1,16 @@
import { Model } from 'common';
import { extend } from 'underscore';
import { extend, isString } from 'underscore';
import Properties from './Properties';
import PropertyFactory from './PropertyFactory';
/**
*
* [Property]: property.html
*
* @typedef Sector
* @property {String} id Sector id, eg. `typography`
* @property {String} name Sector name, eg. `Typography`
* @property {Boolean} [open=true] Indicates the open state.
* @property {Array<Object>} [properties=[]] Indicate an array of Property defintions
*
* [Property]: property.html
* @property {Array<Object>} [properties=[]] Indicate an array of Property defintions.
*/
export default class Sector extends Model {
defaults() {
@ -18,23 +18,33 @@ export default class Sector extends Model {
id: '',
name: '',
open: true,
visible: true,
buildProps: '',
extendBuilded: 1,
properties: []
properties: [],
};
}
initialize(opts) {
const o = opts || {};
initialize(prp, opts = {}) {
const { em } = opts;
this.em = em;
const o = prp || {};
const builded = this.buildProperties(o.buildProps);
const name = this.get('name') || '';
let props = [];
!this.get('id') && this.set('id', name.replace(/ /g, '_').toLowerCase());
if (!builded) props = this.get('properties');
else props = this.extendProperties(builded);
if (!builded) {
props = this.get('properties')
.map(prop => (isString(prop) ? this.buildProperties(prop)[0] : prop))
.filter(Boolean);
} else {
props = this.extendProperties(builded);
}
props = props.map(prop => this.checkExtend(prop));
const propsModel = new Properties(props);
const propsModel = new Properties(props, { em });
propsModel.sector = this;
this.set('properties', propsModel);
}
@ -52,7 +62,8 @@ export default class Sector extends Model {
* @returns {String}
*/
getName() {
return this.get('name');
const id = this.getId();
return this.em?.t(`styleManager.sectors.${id}`) || this.get('name');
}
/**
@ -79,13 +90,42 @@ export default class Sector extends Model {
return this.set('open', value);
}
/**
* Check if the sector is visible
* @returns {Boolean}
*/
isVisible() {
return !!this.get('visible');
}
/**
* Get sector properties.
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.withValue=false] Get only properties with value
* @param {Boolean} [opts.withParentValue=false] Get only properties with parent value
* @returns {Array<[Property]>}
*/
getProperties() {
getProperties(opts = {}) {
const props = this.get('properties');
return props.models ? [...props.models] : props;
const res = props.models ? [...props.models] : props;
return res.filter(prop => {
let result = true;
if (opts.withValue) {
result = prop.hasValue({ noParent: true });
}
if (opts.withParentValue) {
const hasVal = prop.hasValue({ noParent: true });
result = !hasVal && prop.hasValue();
}
return result;
});
}
getProperty(id) {
return this.getProperties().filter(prop => prop.get('id') === id)[0] || null;
}
/**
@ -112,11 +152,7 @@ export default class Sector extends Model {
// Check for nested properties
var mPProps = mProp.properties;
if (mPProps && mPProps.length) {
mProp.properties = this.extendProperties(
prop.properties || [],
mPProps,
1
);
mProp.properties = this.extendProperties(prop.properties || [], mPProps, 1);
}
props[j] = ext ? extend(prop, mProp) : mProp;
isolated[j] = props[j];
@ -134,6 +170,18 @@ export default class Sector extends Model {
return ex ? isolated.filter(i => i) : props;
}
checkExtend(prop) {
const { extend, ...rest } = (isString(prop) ? { extend: prop } : prop) || {};
if (extend) {
return {
...(this.buildProperties([extend])[0] || {}),
...rest,
};
} else {
return prop;
}
}
/**
* Build properties
* @param {Array<string>} propr Array of props as sting
@ -141,15 +189,12 @@ export default class Sector extends Model {
* @private
*/
buildProperties(props) {
var r;
var buildP = props || [];
if (!buildP.length) return;
const buildP = props || [];
if (!this.propFactory) this.propFactory = new PropertyFactory();
if (!buildP.length) return [];
r = this.propFactory.build(buildP);
const builtIn = this.em?.get('StyleManager').builtIn;
return r;
return builtIn?.build(buildP);
}
}

12
src/style_manager/model/Sectors.js

@ -2,14 +2,20 @@ import { Collection } from 'common';
import Sector from './Sector';
export default class Sectors extends Collection {
initialize() {
initialize(prop, opts = {}) {
const { module, em } = opts;
this.em = em;
this.module = module;
this.listenTo(this, 'reset', this.onReset);
}
model(props, opts = {}) {
const { em } = opts.collection;
return new Sector(props, { ...opts, em });
}
onReset(models, opts = {}) {
const prev = opts.previousModels || [];
prev.forEach(sect => sect.get('properties').reset());
}
}
Sectors.prototype.model = Sector;

238
src/style_manager/view/LayerView.js

@ -1,188 +1,120 @@
import { isString, each } from 'underscore';
import Backbone from 'backbone';
import PropertiesView from './PropertiesView';
export default Backbone.View.extend({
events: {
click: 'active',
'click [data-close-layer]': 'removeItem',
'mousedown [data-move-layer]': 'initSorter',
'touchstart [data-move-layer]': 'initSorter'
},
import { View } from 'backbone';
import { keys } from 'underscore';
export default class LayerView extends View {
events() {
return {
click: 'select',
'click [data-close-layer]': 'removeItem',
'mousedown [data-move-layer]': 'initSorter',
'touchstart [data-move-layer]': 'initSorter',
};
}
template(model) {
template() {
const { pfx, ppfx, em } = this;
const label = `${em && em.t('styleManager.layer')} ${model.get('index')}`;
const icons = em?.getConfig('icons');
const iconClose = icons?.close || '';
const iconMove = icons?.move || '';
return `
<div id="${pfx}move" class="${ppfx}no-touch-actions" data-move-layer>
<i class="fa fa-arrows"></i>
</div>
<div id="${pfx}label">${label}</div>
<div id="${pfx}preview-box">
<div id="${pfx}preview" data-preview></div>
</div>
<div id="${pfx}close-layer" class="${pfx}btn-close" data-close-layer>
&Cross;
<div class="${pfx}label-wrp">
<div id="${pfx}move" class="${ppfx}no-touch-actions" data-move-layer>
${iconMove}
</div>
<div id="${pfx}label" data-label></div>
<div id="${pfx}preview-box" class="${pfx}layer-preview" style="display: none" data-preview-box>
<div id="${pfx}preview" class="${pfx}layer-preview-cnt" data-preview></div>
</div>
<div id="${pfx}close-layer" class="${pfx}btn-close" data-close-layer>
${iconClose}
</div>
</div>
<div id="${pfx}inputs" data-properties></div>
<div style="clear:both"></div>
`;
},
}
initialize(o = {}) {
let model = this.model;
this.stackModel = o.stackModel;
this.config = o.config || {};
this.em = this.config.em;
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.sorter = o.sorter || null;
this.propsConfig = o.propsConfig || {};
this.customPreview = o.onPreview;
const { model } = this;
const config = o.config || {};
this.em = config.em;
this.config = config;
this.sorter = o.sorter;
this.pfx = config.stylePrefix || '';
this.ppfx = config.pStylePrefix || '';
this.propertyView = o.propertyView;
const pModel = this.propertyView.model;
this.listenTo(model, 'destroy remove', this.remove);
this.listenTo(model, 'change:active', this.updateVisibility);
this.listenTo(model.get('properties'), 'change', this.updatePreview);
this.listenTo(model, 'change:values', this.updateLabel);
this.listenTo(pModel, 'change:selectedLayer', this.updateVisibility);
// For the sorter
model.view = this;
model.set({ droppable: 0, draggable: 1 });
this.$el.data('model', model);
},
}
/**
* Delegate sorting
* @param {Event} e
* */
initSorter(e) {
if (this.sorter) this.sorter.startSort(this.el);
},
initSorter() {
this.sorter?.startSort(this.el);
}
removeItem(ev) {
ev && ev.stopPropagation();
this.remove();
},
remove(opts = {}) {
const { model, props } = this;
const coll = model.collection;
const stackModel = this.stackModel;
Backbone.View.prototype.remove.apply(this, arguments);
coll && coll.contains(model) && coll.remove(model);
if (stackModel && stackModel.set) {
stackModel.set({ stackIndex: null }, { silent: true });
!opts.fromTarget && stackModel.trigger('updateValue');
}
props && props.remove();
},
/**
* Default method for changing preview box
* @param {Collection} props
* @param {Element} $el
*/
onPreview(value) {
const { stackModel } = this;
const detach = stackModel && stackModel.get('detached');
const values = value.split(' ');
const lim = 3;
const result = [];
const resultObj = {};
this.model.get('properties').each((prop, index) => {
const property = prop.get('property');
let value = detach ? prop.getFullValue() : values[index] || '';
if (value) {
if (prop.get('type') == 'integer') {
let valueInt = parseInt(value, 10);
let unit = value.replace(valueInt, '');
valueInt = !isNaN(valueInt) ? valueInt : 0;
valueInt = valueInt > lim ? lim : valueInt;
valueInt = valueInt < -lim ? -lim : valueInt;
value = valueInt + unit;
}
}
result.push(value);
resultObj[property] = value;
});
return detach ? resultObj : result.join(' ');
},
updatePreview() {
const stackModel = this.stackModel;
const customPreview = this.customPreview;
const previewEl = this.getPreviewEl();
const value = this.model.getFullValue();
const preview = customPreview
? customPreview(value)
: this.onPreview(value);
this.model.remove();
}
if (preview && stackModel && previewEl) {
const { style } = previewEl;
if (isString(preview)) {
style[stackModel.get('property')] = preview;
} else {
let prvStr = [];
each(preview, (val, prop) => prvStr.push(`${prop}:${val}`));
previewEl.setAttribute('style', prvStr.join(';'));
}
}
},
select() {
this.model.select();
}
getPropertiesWrapper() {
if (!this.propsWrapEl) {
this.propsWrapEl = this.el.querySelector('[data-properties]');
}
if (!this.propsWrapEl) this.propsWrapEl = this.el.querySelector('[data-properties]');
return this.propsWrapEl;
},
}
getPreviewEl() {
if (!this.previewEl) {
this.previewEl = this.el.querySelector('[data-preview]');
}
if (!this.previewEl) this.previewEl = this.el.querySelector('[data-preview]');
return this.previewEl;
},
}
getLabelEl() {
if (!this.labelEl) this.labelEl = this.el.querySelector('[data-label]');
return this.labelEl;
}
active() {
const model = this.model;
const collection = model.collection;
collection.active(collection.indexOf(model));
},
updateLabel() {
const { model } = this;
const label = model.getLabel();
this.getLabelEl().innerHTML = label;
if (model.hasPreview()) {
const prvEl = this.getPreviewEl();
const style = model.getStylePreview({ number: { min: -3, max: 3 } });
const styleStr = keys(style)
.map(k => `${k}:${style[k]}`)
.join(';');
prvEl.setAttribute('style', styleStr);
}
}
updateVisibility() {
const pfx = this.pfx;
const { pfx, model, propertyView } = this;
const wrapEl = this.getPropertiesWrapper();
const active = this.model.get('active');
wrapEl.style.display = active ? '' : 'none';
this.$el[active ? 'addClass' : 'removeClass'](`${pfx}active`);
},
const isSelected = model.isSelected();
wrapEl.style.display = isSelected ? '' : 'none';
this.$el[isSelected ? 'addClass' : 'removeClass'](`${pfx}active`);
isSelected && wrapEl.appendChild(propertyView.props.el);
}
render() {
const propsConfig = this.propsConfig;
const { model, el, pfx } = this;
const preview = model.get('preview');
const properties = new PropertiesView({
collection: model.get('properties'),
config: { ...this.config, fromLayer: 1 },
target: propsConfig.target,
customValue: propsConfig.customValue,
propTarget: propsConfig.propTarget,
onChange: propsConfig.onChange
});
const propsEl = properties.render().el;
el.innerHTML = this.template(model);
el.className = `${pfx}layer${!preview ? ` ${pfx}no-preview` : ''}`;
this.props = properties;
this.getPropertiesWrapper().appendChild(propsEl);
const { el, pfx, model } = this;
el.innerHTML = this.template();
el.className = `${pfx}layer`;
if (model.hasPreview()) {
el.querySelector(`[data-preview-box]`).style.display = '';
}
this.updateLabel();
this.updateVisibility();
this.updatePreview();
return this;
}
});
}

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

Loading…
Cancel
Save