Browse Source

Merge pull request #3901 from artf/dev

Merge dev
pull/3968/head
Artur Arseniev 5 years ago
committed by GitHub
parent
commit
e52b5c29e4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      dist/grapes.min.js
  2. 2
      dist/grapes.min.js.map
  3. 13
      docs/.vuepress/config.js
  4. BIN
      docs/.vuepress/public/selector-manager.jpg
  5. BIN
      docs/.vuepress/public/sm-component-first.jpg
  6. BIN
      docs/.vuepress/public/sm-disable-selector.jpg
  7. BIN
      docs/.vuepress/public/sm-selected-component.jpg
  8. 5
      docs/api.js
  9. 1
      docs/api/assets.md
  10. 40
      docs/api/block.md
  11. 63
      docs/api/block_manager.md
  12. 4
      docs/api/canvas.md
  13. 43
      docs/api/component.md
  14. 200
      docs/api/css_composer.md
  15. 56
      docs/api/css_rule.md
  16. 216
      docs/api/editor.md
  17. 19
      docs/api/frame.md
  18. 32
      docs/api/page.md
  19. 55
      docs/api/pages.md
  20. 60
      docs/api/sector.md
  21. 79
      docs/api/selector.md
  22. 234
      docs/api/selector_manager.md
  23. 24
      docs/api/state.md
  24. 43
      docs/api/style_manager.md
  25. 2
      docs/modules/Assets.md
  26. 340
      docs/modules/Blocks.md
  27. 112
      docs/modules/Components.md
  28. 150
      docs/modules/Selectors.md
  29. 3
      package.json
  30. 28
      scripts/build-locale.js
  31. 1
      src/asset_manager/index.js
  32. 6
      src/block_manager/config/config.js
  33. 260
      src/block_manager/index.js
  34. 52
      src/block_manager/model/Block.js
  35. 8
      src/block_manager/model/Blocks.js
  36. 8
      src/block_manager/model/Categories.js
  37. 18
      src/block_manager/model/Category.js
  38. 50
      src/block_manager/view/BlockView.js
  39. 2
      src/block_manager/view/BlocksView.js
  40. 6
      src/canvas/index.js
  41. 2
      src/canvas/model/Canvas.js
  42. 12
      src/canvas/model/Frame.js
  43. 2
      src/canvas/model/Frames.js
  44. 1
      src/canvas/view/FrameView.js
  45. 2
      src/commands/index.js
  46. 22
      src/commands/view/ComponentNext.js
  47. 21
      src/commands/view/ComponentPrev.js
  48. 27
      src/commands/view/ComponentStyleClear.js
  49. 58
      src/commands/view/OpenBlocks.js
  50. 9
      src/commands/view/OpenLayers.js
  51. 72
      src/commands/view/OpenStyleManager.js
  52. 12
      src/commands/view/OpenTraitManager.js
  53. 52
      src/common/module.js
  54. 184
      src/css_composer/index.js
  55. 96
      src/css_composer/model/CssRule.js
  56. 60
      src/dom_components/model/Component.js
  57. 9
      src/dom_components/model/Components.js
  58. 1
      src/dom_components/view/ComponentImageView.js
  59. 5
      src/domain_abstract/model/Styleable.js
  60. 6
      src/domain_abstract/ui/Input.js
  61. 37
      src/domain_abstract/ui/InputColor.js
  62. 32
      src/editor/index.js
  63. 11
      src/editor/model/Editor.js
  64. 8
      src/index.js
  65. 27
      src/pages/index.js
  66. 28
      src/pages/model/Page.js
  67. 1
      src/parser/model/BrowserParserCss.js
  68. 4
      src/parser/model/ParserHtml.js
  69. 8
      src/rich_text_editor/index.js
  70. 9
      src/selector_manager/config/config.js
  71. 433
      src/selector_manager/index.js
  72. 249
      src/selector_manager/model/Selector.js
  73. 18
      src/selector_manager/model/Selectors.js
  74. 33
      src/selector_manager/model/State.js
  75. 9
      src/selector_manager/view/ClassTagView.js
  76. 107
      src/selector_manager/view/ClassTagsView.js
  77. 2
      src/style_manager/config/config.js
  78. 131
      src/style_manager/index.js
  79. 478
      src/style_manager/model/Property.js
  80. 16
      src/style_manager/model/PropertyInteger.js
  81. 86
      src/style_manager/model/Sector.js
  82. 12
      src/style_manager/model/Sectors.js
  83. 2
      src/utils/ColorPicker.js
  84. 55
      src/utils/Droppable.js
  85. 86
      src/utils/Sorter.js
  86. 8
      test/setup.js
  87. 67
      test/specs/block_manager/index.js
  88. 2
      test/specs/commands/index.js
  89. 62
      test/specs/css_composer/index.js
  90. 1
      test/specs/css_composer/model/CssModels.js
  91. 1
      test/specs/dom_components/model/Component.js
  92. 34
      test/specs/parser/model/ParserHtml.js
  93. 75
      test/specs/selector_manager/e2e/ClassManager.js
  94. 101
      test/specs/selector_manager/index.js
  95. 44
      test/specs/selector_manager/view/ClassTagsView.js
  96. 82
      test/specs/style_manager/index.js
  97. 1
      test/specs/style_manager/view/PropertyColorView.js
  98. 1
      test/specs/style_manager/view/PropertyIntegerView.js
  99. 1
      test/specs/style_manager/view/PropertyRadioView.js
  100. 1
      test/specs/style_manager/view/PropertySelectView.js

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

13
docs/.vuepress/config.js

@ -60,6 +60,7 @@ module.exports = {
['/api/editor', 'Editor'],
['/api/i18n', 'I18n'],
['/api/canvas', 'Canvas'],
['/api/frame', `${subDivider}Frame`],
['/api/assets', 'Asset Manager'],
['/api/asset', `${subDivider}Asset`],
['/api/block_manager', 'Block Manager'],
@ -71,10 +72,13 @@ module.exports = {
['/api/pages', 'Pages'],
['/api/page', `${subDivider}Page`],
['/api/style_manager', 'Style Manager'],
['/api/sector', `${subDivider}Sector`],
['/api/storage_manager', 'Storage Manager'],
['/api/device_manager', 'Device Manager'],
['/api/device', `${subDivider}Device`],
['/api/selector_manager', 'Selector Manager'],
['/api/selector', `${subDivider}Selector`],
['/api/state', `${subDivider}State`],
['/api/css_composer', 'CSS Composer'],
['/api/css_rule', `${subDivider}CssRule`],
['/api/modal_dialog', 'Modal'],
@ -91,13 +95,14 @@ module.exports = {
title: 'Modules',
collapsable: false,
children: [
['/modules/Assets', 'Assets'],
['/modules/Blocks', 'Blocks'],
['/modules/Commands', 'Commands'],
['/modules/Components', 'Components'],
['/modules/Components-js', 'Components & JS'],
['/modules/I18n', 'I18n'],
['/modules/Traits', 'Traits'],
['/modules/Blocks', 'Blocks'],
['/modules/Assets', 'Assets'],
['/modules/Commands', 'Commands'],
['/modules/I18n', 'I18n'],
['/modules/Selectors', 'Selectors'],
['/modules/Style-manager', 'Style Manager'],
['/modules/Storage', 'Storage Manager'],
['/modules/Modal', 'Modal'],

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/.vuepress/public/sm-component-first.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/.vuepress/public/sm-disable-selector.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/.vuepress/public/sm-selected-component.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

5
docs/api.js

@ -21,10 +21,13 @@ async function generateDocs () {
['dom_components/model/Component.js', 'component.md'],
['panels/index.js', 'panels.md'],
['style_manager/index.js', 'style_manager.md'],
['style_manager/model/Sector.js', 'sector.md'],
['storage_manager/index.js', 'storage_manager.md'],
['device_manager/index.js', 'device_manager.md'],
['device_manager/model/Device.js', 'device.md'],
['selector_manager/index.js', 'selector_manager.md'],
['selector_manager/model/Selector.js', 'selector.md'],
['selector_manager/model/State.js', 'state.md'],
['css_composer/index.js', 'css_composer.md'],
['css_composer/model/CssRule.js', 'css_rule.md'],
['modal_dialog/index.js', 'modal_dialog.md'],
@ -32,6 +35,7 @@ async function generateDocs () {
['keymaps/index.js', 'keymaps.md'],
['undo_manager/index.js', 'undo_manager.md'],
['canvas/index.js', 'canvas.md'],
['canvas/model/Frame.js', 'frame.md'],
['i18n/index.js', 'i18n.md'],
['pages/index.js', 'pages.md'],
['pages/model/Page.js', 'page.md'],
@ -44,6 +48,7 @@ async function generateDocs () {
.replace(/\*\*\\\[/g, '**[')
.replace(/\*\*\(\\\[/g, '**([')
.replace(/<\\\[/g, '<[')
.replace(/<\(\\\[/g, '<([')
.replace(/\| \\\[/g, '| [')
.replace('**Extends Model**', '');
fs.writeFileSync(`${docRoot}/api/${file[1]}`, res);

1
docs/api/assets.md

@ -22,6 +22,7 @@ const assetManager = editor.AssetManager;
* `asset:open` - Asset Manager opened.
* `asset:close` - Asset Manager closed.
* `asset:add` - Asset added. The [Asset] is passed as an argument to the callback.
* `asset:remove` - Asset removed. The [Asset] is passed as an argument to the callback.
* `asset:update` - Asset updated. The updated [Asset] and the object containing changes are passed as arguments to the callback.
* `asset:upload:start` - Before the upload is started.

40
docs/api/block.md

@ -9,14 +9,44 @@
* `label` **[String][1]** Block label, eg. `My block`
* `content` **([String][1] | [Object][2])** The content of the block. Might be an HTML string or a [Component Defintion][3]
* `media` **[String][1]?** HTML string for the media/icon of the block, eg. `<svg ...`, `<img ...`, etc.
* `category` **[String][1]?** Block category
* `activate` **[Boolean][4]?** If true, triggers an the `active` event on dropped component
* `select` **[Boolean][4]?** If true, the dropped component will be selected
* `resetId` **[Boolean][4]?** If true, all IDs of dropped components and their styles will be changed
* `category` **[String][1]?** Block category, eg. `Basic blocks`
* `activate` **[Boolean][4]?** If true, triggers the `active` event on the dropped component.
* `select` **[Boolean][4]?** If true, the dropped component will be selected.
* `resetId` **[Boolean][4]?** If true, all IDs of dropped components and their styles will be changed.
* `disable` **[Boolean][4]?** Disable the block from being interacted
* `onClick` **[Function][5]?** Custom behavior on click, eg. `(block, editor) => editor.getWrapper().append(block.get('content'))`
* `attributes` **[Object][2]?** Block attributes to apply in the view element
### getId
Get block id
Returns **[String][1]**
### getLabel
Get block label
Returns **[String][1]**
### getMedia
Get block media
Returns **[String][1]**
### getContent
Get block content
Returns **([Object][2] | [String][1] | [Array][6]<([Object][2] | [String][1])>)** Component definition | HTML string
### getCategoryLabel
Get block category label
Returns **[String][1]**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
@ -26,3 +56,5 @@
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

63
docs/api/block_manager.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('block:add', (block) => { ... });
// Use the API
const blockManager = editor.BlockManager;
blockManager.add(...);
```
## Available Events
* `block:add` - Block added. The [Block] is passed as an argument to the callback.
* `block:remove` - Block removed. The [Block] is passed as an argument to the callback.
* `block:update` - Block updated. The [Block] and the object containing changes are passed as arguments to the callback.
* `block:drag:start` - Started dragging block, the [Block] is passed as an argument.
* `block:drag` - Dragging block, the [Block] is passed as an argument.
* `block:drag:stop` - Dragging of the block is stopped. The dropped [Component] (if dropped successfully) and the [Block] are passed as arguments.
* `block` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
## Methods
* [add][2]
* [get][3]
* [getAll][4]
@ -30,33 +47,23 @@ const blockManager = editor.BlockManager;
[Block]: block.html
[Component]: component.html
## getConfig
Get configuration object
Returns **[Object][11]**
## onLoad
Load default blocks if the collection is empty
## add
Add new block to the collection.
Add new block.
### Parameters
* `id` **[string][12]** Block id
* `opts` **[Object][11]** Options
* `opts.label` **[string][12]** Name of the block
* `opts.content` **[string][12]** HTML content
* `opts.category` **([string][12] | [Object][11])** Group the block inside a category.
You should pass objects with id property, eg:
{id: 'some-uid', label: 'My category'}
The string will be converted in:
'someid' => {id: 'someid', label: 'someid'}
* `opts.attributes` **[Object][11]** Block attributes (optional, default `{}`)
* `id` **[String][12]** Block ID
* `props` **[Block]** Block properties
* `opts` (optional, default `{}`)
### Examples
@ -75,11 +82,11 @@ Returns **[Block]** Added block
## get
Return the block by id
Get the block by id.
### Parameters
* `id` **[string][12]** Block id
* `id` **[String][12]** Block id
### Examples
@ -93,7 +100,7 @@ Returns **[Block]**
## getAll
Return all blocks
Return all blocks.
### Examples
@ -103,28 +110,30 @@ console.log(JSON.stringify(blocks));
// [{label: 'Heading', content: '<h1>Put your ...'}, ...]
```
Returns **Collection**
Returns **Collection<[Block]>**
## getAllVisible
Return the visible collection, which containes blocks actually rendered
Returns **Collection**
Returns **Collection<[Block]>**
## remove
Remove a block by id
Remove block.
### Parameters
* `id` **[string][12]** Block id
* `block` **([String][12] | [Block])** Block or block ID
* `opts` (optional, default `{}`)
### Examples
```javascript
// Id of the block which need to be removed
const id = 'button';
blockManager.remove(id);
const removed = blockManager.remove('BLOCK_ID');
// or by passing the Block
const block = blockManager.get('BLOCK_ID');
blockManager.remove(block);
```
Returns **[Block]** Removed block

4
docs/api/canvas.md

@ -34,6 +34,8 @@ const canvas = editor.Canvas;
[Component]: component.html
[Frame]: frame.html
## getConfig
Get the configuration object
@ -218,7 +220,7 @@ canvas.addFrame({
});
```
Returns **Frame**
Returns **[Frame]**
[1]: https://github.com/artf/grapesjs/blob/master/src/canvas/config/config.js

43
docs/api/component.md

@ -19,6 +19,8 @@ component.get('tagName');
// -> 'span'
```
[Component]: component.html
### Properties
* `type` **[String][1]?** Component type, eg. `text`, `image`, `video`, etc.
@ -46,6 +48,7 @@ component.get('tagName');
* `selectable` **[Boolean][3]?** Allow component to be selected when clicked. Default: `true`
* `hoverable` **[Boolean][3]?** Shows a highlight outline when hovering on the element if `true`. Default: `true`
* `void` **[Boolean][3]?** This property is used by the HTML exporter as void elements don't have closing tags, eg. `<br/>`, `<hr/>`, etc. Default: `false`
* `styles` **[String][1]?** Component related styles, eg. `.my-component-class { color: red }`
* `content` **[String][1]?** Content of the component (not escaped) which will be appended before children rendering. Default: `''`
* `icon` **[String][1]?** Component's icon, this string will be inserted before the name (in Layers and badge), eg. it can be an HTML string '<i class="fa fa-square-o"></i>'. Default: `''`
* `script` **([String][1] | [Function][4])?** Component's javascript. More about it [here][7]. Default: `''`
@ -281,7 +284,8 @@ Get the style of the component
#### Parameters
* `opts` (optional, default `{}`)
* `options` (optional, default `{}`)
* `optsAdd` (optional, default `{}`)
Returns **[Object][2]**
@ -407,7 +411,7 @@ current collection is returned
#### Parameters
* `components` **([Component][9] | [String][1])?** Components to set
* `components` **([Component][9] | [String][1])?** Component Definitions or HTML string
* `opts` **[Object][2]** Options, same as in `Component.append()` (optional, default `{}`)
#### Examples
@ -421,7 +425,38 @@ console.log(collection.length);
// -> 2
```
Returns **(Collection | [Array][5]<[Component][9]>)**
Returns **(Collection | [Array][5]<[[Component][9]]>)**
### getChildAt
If exists, returns the child component at specific index.
#### Parameters
* `index` **[Number][10]** Index of the component to return
#### Examples
```javascript
// Return first child
component.getChildAt(0);
// Return second child
component.getChildAt(1);
```
Returns **([[Component][9]] | null)**
### getLastChild
If exists, returns the last child component.
#### Examples
```javascript
const lastChild = component.getLastChild();
```
Returns **([[Component][9]] | null)**
### empty
@ -448,7 +483,7 @@ component.parent();
// -> Component
```
Returns **[Component][9]**
Returns **([Component][9] | null)**
### getTrait

200
docs/api/css_composer.md

@ -2,7 +2,7 @@
## CssComposer
This module contains and manage CSS rules for the template inside the canvas.
This module manages CSS rules in the canvas.
You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object][1]
```js
@ -16,206 +16,160 @@ 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
```js
const cssComposer = editor.CssComposer;
const css = editor.Css;
```
* [load][2]
* [store][3]
* [add][4]
* [get][5]
* [getAll][6]
* [addRules][2]
* [setRule][3]
* [getRule][4]
* [getRules][5]
* [remove][6]
* [clear][7]
* [setRule][8]
* [getRule][9]
## load
[CssRule]: css_rule.html
Load data from the passed object, if the object is empty will try to fetch them
autonomously from the storage manager.
The fetched data will be added to the collection
## addRules
### Parameters
* `data` **[Object][10]** Object of data to load
Returns **[Object][10]** Loaded rules
## store
Store data to the selected storage
### Parameters
* `noStore` **[Boolean][11]** If true, won't store
Returns **[Object][10]** Data to store
## add
Add new rule to the collection, if not yet exists with the same selectors
### Parameters
* `selectors` **[Array][12]\<Selector>** Array of selectors
* `state` **[String][13]** Css rule state
* `width` **[String][13]** For which device this style is oriented
* `opts` **[Object][10]** Options for the add of new rule (optional, default `{}`)
* `addOpts` (optional, default `{}`)
* `props` **[Object][10]** Other props for the rule
### Examples
```javascript
var sm = editor.SelectorManager;
var sel1 = sm.add('myClass1');
var sel2 = sm.add('myClass2');
var rule = cssComposer.add([sel1, sel2], 'hover');
rule.set('style', {
width: '100px',
color: '#fff',
});
```
Returns **Model**
## get
Get the rule
Add CssRules via CSS string.
### Parameters
* `selectors` **[Array][12]\<Selector>** Array of selectors
* `state` **[String][13]** Css rule state
* `width` **[String][13]** For which device this style is oriented
* `ruleProps` **[Object][10]** Other rule props
* `css` **[String][8]** CSS string of rules to add.
### Examples
```javascript
var sm = editor.SelectorManager;
var sel1 = sm.add('myClass1');
var sel2 = sm.add('myClass2');
var rule = cssComposer.get([sel1, sel2], 'hover');
// Update the style
rule.set('style', {
width: '300px',
color: '#000',
});
const addedRules = css.addRules('.my-cls{ color: red } @media (max-width: 992px) { .my-cls{ color: darkred } }');
// Check rules
console.log(addedRules.map(rule => rule.toCSS()));
```
Returns **(Model | null)**
## getAll
Get the collection of rules
Returns **Collection**
## clear
Remove all rules
### Parameters
* `opts` (optional, default `{}`)
Returns **this**
Returns **[Array][9]<[CssRule]>** Array of rules
## setRule
Add/update the CSS rule with a generic selector
Add/update the CssRule.
### Parameters
* `selectors` **[string][13]** Selector, eg. '.myclass'
* `selectors` **[String][8]** Selector string, eg. `.myclass`
* `style` **[Object][10]** Style properties and values
* `opts` **[Object][10]** Additional properties (optional, default `{}`)
* `opts.atRuleType` **[String][13]** At-rule type, eg. 'media' (optional, default `''`)
* `opts.atRuleParams` **[String][13]** At-rule parameters, eg. '(min-width: 500px)' (optional, default `''`)
* `opts.atRuleType` **[String][8]** At-rule type, eg. `media` (optional, default `''`)
* `opts.atRuleParams` **[String][8]** At-rule parameters, eg. `(min-width: 500px)` (optional, default `''`)
### Examples
```javascript
// Simple class-based rule
const rule = cc.setRule('.class1.class2', { color: 'red' });
const rule = css.setRule('.class1.class2', { color: 'red' });
console.log(rule.toCSS()) // output: .class1.class2 { color: red }
// With state and other mixed selector
const rule = cc.setRule('.class1.class2:hover, div#myid', { color: 'red' });
const rule = css.setRule('.class1.class2:hover, div#myid', { color: 'red' });
// output: .class1.class2:hover, div#myid { color: red }
// With media
const rule = cc.setRule('.class1:hover', { color: 'red' }, {
const rule = css.setRule('.class1:hover', { color: 'red' }, {
atRuleType: 'media',
atRuleParams: '(min-width: 500px)',
});
// output: @media (min-width: 500px) { .class1:hover { color: red } }
```
Returns **CssRule** The new/updated rule
Returns **[CssRule]** The new/updated CssRule
## getRule
Get the CSS rule by a generic selector
Get the CssRule.
### Parameters
* `selectors` **[string][13]** Selector, eg. '.myclass:hover'
* `opts` (optional, default `{}`)
* `selectors` **[String][8]** Selector string, eg. `.myclass:hover`
* `opts` **[Object][10]** Additional properties (optional, default `{}`)
* `opts.atRuleType` **[String][8]** At-rule type, eg. `media` (optional, default `''`)
* `opts.atRuleParams` **[String][8]** At-rule parameters, eg. '(min-width: 500px)' (optional, default `''`)
### Examples
```javascript
const rule = cc.getRule('.myclass1:hover');
const rule2 = cc.getRule('.myclass1:hover, div#myid');
const rule3 = cc.getRule('.myclass1', {
const rule = css.getRule('.myclass1:hover');
const rule2 = css.getRule('.myclass1:hover, div#myid');
const rule3 = css.getRule('.myclass1', {
atRuleType: 'media',
atRuleParams: '(min-width: 500px)',
});
```
Returns **CssRule**
Returns **[CssRule]**
## getRules
Find rules, in different states (eg. like `:hover`) and media queries, matching the selector.
Get all rules or filtered by a matching selector.
### Parameters
* `selector` **[string][13]** Selector, eg. '.myclass'
* `selector` **[String][8]** Selector, eg. `.myclass` (optional, default `''`)
### Examples
```javascript
// Common scenario, take all the component specific rules
// Take all the component specific rules
const id = someComponent.getId();
const rules = cc.getRules(`#${id}`);
const rules = css.getRules(`#${id}`);
console.log(rules.map(rule => rule.toCSS()))
// All rules in the project
console.log(css.getRules())
```
Returns **[Array][12]\<CssRule>**
Returns **[Array][9]<[CssRule]>**
[1]: https://github.com/artf/grapesjs/blob/master/src/css_composer/config/config.js
## remove
Remove rule, by CssRule or matching selector (eg. the selector will match also at-rules like `@media`)
[2]: #load
### Parameters
[3]: #store
* `rule` **([String][8] | [CssRule] | [Array][9]<[CssRule]>)** CssRule or matching selector.
[4]: #add
### Examples
[5]: #get
```javascript
// Remove by CssRule
const toRemove = css.getRules('.my-cls');
css.remove(toRemove);
// Remove by selector
css.remove('.my-cls-2');
```
[6]: #getall
Returns **[Array][9]<[CssRule]>** Removed rules
[7]: #clear
## clear
[8]: #setrule
Remove all rules
[9]: #getrule
### Parameters
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
* `opts` (optional, default `{}`)
Returns **this**
[1]: https://github.com/artf/grapesjs/blob/master/src/css_composer/config/config.js
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[2]: #addrules
[12]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[3]: #setrule
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[4]: #getrule
[5]: #getrules
[6]: #remove
[7]: #clear
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

56
docs/api/css_rule.md

@ -1,10 +1,26 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## getAtRule
## CssRule
Return the at-rule statement when exists, eg. '@media (...)', '@keyframes'
**Extends Model.extend(Styleable)**
### Examples
### Properties
* `selectors` **[Array][1]\<Selector>** Array of selectors
* `style` **[Object][2]** Object containing style definitions
* `selectorsAdd` **[String][3]?** Additional string css selectors
* `atRuleType` **[String][3]?** Type of at-rule, eg. `media`, 'font-face'
* `mediaText` **[String][3]?** At-rule value, eg. `(max-width: 1000px)`
* `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
### getAtRule
Returns the at-rule statement when exists, eg. `@media (...)`, `@keyframes`
#### Examples
```javascript
const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
@ -14,19 +30,19 @@ const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
cssRule.getAtRule(); // "@media (min-width: 500px)"
```
Returns **[String][1]**
Returns **[String][3]**
## selectorsToString
### selectorsToString
Return selectors of the rule as a string
### Parameters
#### Parameters
* `opts` **[Object][2]?** Options (optional, default `{}`)
* `opts.skipState` **[Boolean][3]?** Skip state from the result
* `opts.skipState` **[Boolean][4]?** Skip state from the result
### Examples
#### Examples
```javascript
const cssRule = editor.Css.setRule('.class1:hover', { color: 'red' });
@ -34,17 +50,17 @@ cssRule.selectorsToString(); // ".class1:hover"
cssRule.selectorsToString({ skipState: true }); // ".class1"
```
Returns **[String][1]**
Returns **[String][3]**
## getDeclaration
### getDeclaration
Get declaration block (without the at-rule statement)
### Parameters
#### Parameters
* `opts` **[Object][2]** Options (same as in `selectorsToString`) (optional, default `{}`)
### Examples
#### Examples
```javascript
const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
@ -54,17 +70,17 @@ const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
cssRule.getDeclaration() // ".class1{color:red;}"
```
Returns **[String][1]**
Returns **[String][3]**
## toCSS
### toCSS
Return the CSS string of the rule
### Parameters
#### Parameters
* `opts` **[Object][2]** Options (same as in `getDeclaration`) (optional, default `{}`)
### Examples
#### Examples
```javascript
const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
@ -74,10 +90,12 @@ const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
cssRule.toCSS() // "@media (min-width: 500px){.class1{color:red;}}"
```
Returns **[String][1]** CSS string
Returns **[String][3]** CSS string
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[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
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean

216
docs/api/editor.md

@ -42,14 +42,6 @@ editor.on('EVENT-NAME', (some, argument) => {
* `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
### Blocks
* `block:add` - New block added
* `block:remove` - Block removed
* `block:drag:start` - Started dragging block, model of the block is passed as an argument
* `block:drag` - Dragging block, the block's model and the drag event are passed as arguments
* `block:drag:stop` - Dragging of the block is stopped. As agruments for the callback you get, the dropped component model (if dropped successfully) and the model of the block
### Keymaps
* `keymap:add` - New keymap added. The new keyamp object is passed as an argument
@ -87,13 +79,6 @@ editor.on('EVENT-NAME', (some, argument) => {
By changing `result.content` you're able to customize what is dropped
### Selectors
* `selector:add` - New selector is add. Passes the new selector as an argument
* `selector:remove` - Selector removed. Passes the removed selector as an argument
* `selector:update` - Selector updated. Passes the updated selector as an argument
* `selector:state` - State changed. Passes the new state value as an argument
### RTE
* `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
@ -109,25 +94,33 @@ By changing `result.content` you're able to customize what is dropped
* `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
### Selectors
Check the [Selectors][2] module.
### Blocks
Check the [Blocks][3] module.
### Assets
Check the [Assets][2] module.
Check the [Assets][4] module.
### Modal
Check the [Modal][3] module.
Check the [Modal][5] module.
### Devices
Check the [Devices][4] module.
Check the [Devices][6] module.
### Parser
Check the [Parser][5] module.
Check the [Parser][7] module.
### Pages
Check the [Pages][6] module.
Check the [Pages][8] module.
### General
@ -143,7 +136,7 @@ Returns configuration object
### Parameters
* `prop` **[string][7]?** Property name
* `prop` **[string][9]?** Property name
Returns **any** Returns the configuration object or
the value of the specified property
@ -154,12 +147,12 @@ Returns HTML built inside canvas
### Parameters
* `opts` **[Object][8]** Options (optional, default `{}`)
* `opts` **[Object][10]** Options (optional, default `{}`)
* `opts.component` **Component?** Return the HTML of a specific Component
* `opts.cleanId` **[Boolean][9]** Remove unnecessary IDs (eg. those created automatically) (optional, default `false`)
* `opts.cleanId` **[Boolean][11]** Remove unnecessary IDs (eg. those created automatically) (optional, default `false`)
Returns **[string][7]** HTML string
Returns **[string][9]** HTML string
## getCss
@ -167,13 +160,13 @@ Returns CSS built inside canvas
### Parameters
* `opts` **[Object][8]** Options (optional, default `{}`)
* `opts` **[Object][10]** Options (optional, default `{}`)
* `opts.component` **Component?** Return the CSS of a specific Component
* `opts.json` **[Boolean][9]** Return an array of CssRules instead of the CSS string (optional, default `false`)
* `opts.avoidProtected` **[Boolean][9]** Don't include protected CSS (optional, default `false`)
* `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`)
Returns **([String][7] | [Array][10]\<CssRule>)** CSS string or array of CssRules
Returns **([String][9] | [Array][12]\<CssRule>)** CSS string or array of CssRules
## getJs
@ -181,11 +174,11 @@ Returns JS of all components
### Parameters
* `opts` **[Object][8]** Options (optional, default `{}`)
* `opts` **[Object][10]** Options (optional, default `{}`)
* `opts.component` **Component?** Get the JS of a specific component
Returns **[String][7]** JS string
Returns **[String][9]** JS string
## getComponents
@ -205,8 +198,8 @@ Set components inside editor's canvas. This method overrides actual components
### Parameters
* `components` **([Array][10]<[Object][8]> | [Object][8] | [string][7])** HTML string or components model
* `opt` **[Object][8]** the options object to be used by the \[setComponents][em#setComponents][11] method (optional, default `{}`)
* `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 `{}`)
### Examples
@ -228,10 +221,10 @@ Add components
### Parameters
* `components` **([Array][10]<[Object][8]> | [Object][8] | [string][7])** HTML string or components model
* `opts` **[Object][8]** Options
* `components` **([Array][12]<[Object][10]> | [Object][10] | [string][9])** HTML string or components model
* `opts` **[Object][10]** Options
* `opts.avoidUpdateStyle` **[Boolean][9]** If the HTML string contains styles,
* `opts.avoidUpdateStyle` **[Boolean][11]** 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`)
@ -247,13 +240,13 @@ editor.addComponents({
});
```
Returns **[Array][10]\<Component>**
Returns **[Array][12]\<Component>**
## getStyle
Returns style in JSON format object
Returns **[Object][8]**
Returns **[Object][10]**
## setStyle
@ -261,7 +254,7 @@ Set style inside editor's canvas. This method overrides actual style
### Parameters
* `style` **([Array][10]<[Object][8]> | [Object][8] | [string][7])** CSS string or style model
* `style` **([Array][12]<[Object][10]> | [Object][10] | [string][9])** CSS string or style model
* `opt` (optional, default `{}`)
### Examples
@ -283,7 +276,7 @@ Add styles to the editor
### Parameters
* `style` **([Array][10]<[Object][8]> | [Object][8] | [string][7])** CSS string or style model
* `style` **([Array][12]<[Object][10]> | [Object][10] | [string][9])** CSS string or style model
* `opts` (optional, default `{}`)
### Examples
@ -292,7 +285,7 @@ Add styles to the editor
editor.addStyle('.cls{color: red}');
```
Returns **[Array][10]\<CssRule>** Array of created CssRule instances
Returns **[Array][12]\<CssRule>** Array of created CssRule instances
## getSelected
@ -304,7 +297,7 @@ Returns **Model**
Returns an array of all selected components
Returns **[Array][10]**
Returns **[Array][12]**
## getSelectedToStyle
@ -322,10 +315,10 @@ Select a component
### Parameters
* `el` **(Component | [HTMLElement][12])** Component to select
* `opts` **[Object][8]?** Options
* `el` **(Component | [HTMLElement][14])** Component to select
* `opts` **[Object][10]?** Options
* `opts.scroll` **[Boolean][9]?** Scroll canvas to the selected element
* `opts.scroll` **[Boolean][11]?** Scroll canvas to the selected element
### Examples
@ -344,7 +337,7 @@ Add component to selection
### Parameters
* `el` **(Component | [HTMLElement][12] | [Array][10])** Component to select
* `el` **(Component | [HTMLElement][14] | [Array][12])** Component to select
### Examples
@ -360,7 +353,7 @@ Remove component from selection
### Parameters
* `el` **(Component | [HTMLElement][12] | [Array][10])** Component to select
* `el` **(Component | [HTMLElement][14] | [Array][12])** Component to select
### Examples
@ -376,7 +369,7 @@ Toggle component selection
### Parameters
* `el` **(Component | [HTMLElement][12] | [Array][10])** Component to select
* `el` **(Component | [HTMLElement][14] | [Array][12])** Component to select
### Examples
@ -408,7 +401,7 @@ change the canvas to the proper width
### Parameters
* `name` **[string][7]** Name of the device
* `name` **[string][9]** Name of the device
### Examples
@ -430,7 +423,7 @@ console.log(device);
// 'Tablet'
```
Returns **[string][7]** Device name
Returns **[string][9]** Device name
## runCommand
@ -438,8 +431,8 @@ Execute command
### Parameters
* `id` **[string][7]** Command ID
* `options` **[Object][8]** Custom options (optional, default `{}`)
* `id` **[string][9]** Command ID
* `options` **[Object][10]** Custom options (optional, default `{}`)
### Examples
@ -455,8 +448,8 @@ Stop the command if stop method was provided
### Parameters
* `id` **[string][7]** Command ID
* `options` **[Object][8]** Custom options (optional, default `{}`)
* `id` **[string][9]** Command ID
* `options` **[Object][10]** Custom options (optional, default `{}`)
### Examples
@ -472,9 +465,9 @@ Store data to the current storage
### Parameters
* `clb` **[Function][13]** Callback function
* `clb` **[Function][15]** Callback function
Returns **[Object][8]** Stored data
Returns **[Object][10]** Stored data
## storeData
@ -487,7 +480,7 @@ console.log(editor.storeData());
// { pages: [...], styles: [...], ... }
```
Returns **[Object][8]**
Returns **[Object][10]**
## load
@ -495,9 +488,9 @@ Load data from the current storage
### Parameters
* `clb` **[Function][13]** Callback function
* `clb` **[Function][15]** Callback function
Returns **[Object][8]** Stored data
Returns **[Object][10]** Stored data
## loadData
@ -505,7 +498,7 @@ Load data from the JSON data object
### Parameters
* `data` **[Object][8]** Data to load
* `data` **[Object][10]** Data to load
### Examples
@ -513,21 +506,21 @@ Load data from the JSON data object
editor.loadData({ pages: [...], styles: [...], ... })
```
Returns **[Object][8]** Loaded object
Returns **[Object][10]** Loaded object
## getContainer
Returns container element. The one which was indicated as 'container'
on init method
Returns **[HTMLElement][12]**
Returns **[HTMLElement][14]**
## getDirtyCount
Return the count of changes made to the content and not yet stored.
This count resets at any `store()`
Returns **[number][14]**
Returns **[number][16]**
## refresh
@ -540,9 +533,9 @@ refresh you'll get misleading position of tools
### Parameters
* `opts`
* `options` **[Object][8]?** Options
* `options` **[Object][10]?** Options
* `options.tools` **[Boolean][9]** Update the position of tools (eg. rich text editor, component highlighter, etc.) (optional, default `false`)
* `options.tools` **[Boolean][11]** Update the position of tools (eg. rich text editor, component highlighter, etc.) (optional, default `false`)
## setCustomRte
@ -550,7 +543,7 @@ Replace the built-in Rich Text Editor with a custom one.
### Parameters
* `obj` **[Object][8]** Custom RTE Interface
* `obj` **[Object][10]** Custom RTE Interface
### Examples
@ -590,7 +583,7 @@ custom parser, pass `null` as the argument
### Parameters
* `parser` **([Function][13] | null)** Parser function
* `parser` **([Function][15] | null)** Parser function
### Examples
@ -612,11 +605,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][15]
To get more about this feature read: [https://github.com/artf/grapesjs/issues/1936][17]
### Parameters
* `value` **[String][7]** Drag mode, options: 'absolute' | 'translate'
* `value` **[String][9]** Drag mode, options: 'absolute' | 'translate'
Returns **this**
@ -627,10 +620,10 @@ Trigger event log message
### Parameters
* `msg` **any** Message to log
* `opts` **[Object][8]** Custom options (optional, default `{}`)
* `opts` **[Object][10]** Custom options (optional, default `{}`)
* `opts.ns` **[String][7]** Namespace of the log (eg. to use in plugins) (optional, default `''`)
* `opts.level` **[String][7]** Level of the log, `debug`, `info`, `warning`, `error` (optional, default `'debug'`)
* `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'`)
### Examples
@ -652,11 +645,11 @@ Translate label
### Parameters
* `args` **...any**
* `key` **[String][7]** Label to translate
* `opts` **[Object][8]?** Options for the translation
* `key` **[String][9]** Label to translate
* `opts` **[Object][10]?** Options for the translation
* `opts.params` **[Object][8]?** Params for the translation
* `opts.noWarn` **[Boolean][9]?** Avoid warnings in case of missing resources
* `opts.params` **[Object][10]?** Params for the translation
* `opts.noWarn` **[Boolean][11]?** Avoid warnings in case of missing resources
### Examples
@ -668,7 +661,7 @@ editor.t('msg2', { params: { test: 'hello' } });
editor.t('msg2', { params: { test: 'hello' }, l: 'it' });
```
Returns **[String][7]**
Returns **[String][9]**
## on
@ -676,8 +669,8 @@ Attach event
### Parameters
* `event` **[string][7]** Event name
* `callback` **[Function][13]** Callback function
* `event` **[string][9]** Event name
* `callback` **[Function][15]** Callback function
Returns **this**
@ -687,8 +680,8 @@ Attach event and detach it after the first run
### Parameters
* `event` **[string][7]** Event name
* `callback` **[Function][13]** Callback function
* `event` **[string][9]** Event name
* `callback` **[Function][15]** Callback function
Returns **this**
@ -698,8 +691,8 @@ Detach event
### Parameters
* `event` **[string][7]** Event name
* `callback` **[Function][13]** Callback function
* `event` **[string][9]** Event name
* `callback` **[Function][15]** Callback function
Returns **this**
@ -709,7 +702,7 @@ Trigger event
### Parameters
* `event` **[string][7]** Event to trigger
* `event` **[string][9]** Event to trigger
Returns **this**
@ -721,7 +714,24 @@ Destroy the editor
Render editor
Returns **[HTMLElement][12]**
Returns **[HTMLElement][14]**
## onReady
Trigger a callback once the editor is loaded and rendered.
The callback will be executed immediately if the method is called on the already rendered editor.
### Parameters
* `clb` **[Function][15]** Callback to trigger
### Examples
```javascript
editor.onReady(() => {
// perform actions
});
```
## html
@ -729,8 +739,8 @@ Print safe HTML by using ES6 tagged template strings.
### Parameters
* `literals` **[Array][10]<[String][7]>**
* `substs` **[Array][10]<[String][7]>**
* `literals` **[Array][12]<[String][9]>**
* `substs` **[Array][12]<[String][9]>**
### Examples
@ -741,34 +751,38 @@ const safeStr = '<b>Hello</b>';
const strHtml = editor.html`Escaped ${unsafeStr}, unescaped $${safeStr}`;
```
Returns **[String][7]**
Returns **[String][9]**
[1]: https://github.com/artf/grapesjs/blob/master/src/editor/config/config.js
[2]: /api/assets.html
[2]: /api/selector_manager.html
[3]: /api/block_manager.html
[4]: /api/assets.html
[3]: /api/modal_dialog.html
[5]: /api/modal_dialog.html
[4]: /api/device_manager.html
[6]: /api/device_manager.html
[5]: /api/parser.html
[7]: /api/parser.html
[6]: /api/pages.html
[8]: /api/pages.html
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[12]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[11]: em#setComponents
[13]: em#setComponents
[12]: https://developer.mozilla.org/docs/Web/HTML/Element
[14]: https://developer.mozilla.org/docs/Web/HTML/Element
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[15]: https://github.com/artf/grapesjs/issues/1936
[17]: https://github.com/artf/grapesjs/issues/1936

19
docs/api/frame.md

@ -0,0 +1,19 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## Frame
### Properties
* `component` **([Object][1] | [String][2])** Wrapper component definition. You can also pass an HTML string as components of the default wrapper component.
* `width` **[String][2]?** Width of the frame. By default, the canvas width will be taken.
* `height` **[String][2]?** Height of the frame. By default, the canvas height will be taken.
* `x` **[Number][3]?** Horizontal position of the frame in the canvas.
* `y` **[Number][3]?** Vertical position of the frame in the canvas.
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number

32
docs/api/page.md

@ -1,5 +1,31 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## getId
Get page id
Returns **[String][1]**
## getName
Get page name
Returns **[String][1]**
## setName
Update page name
### Parameters
* `name` **[String][1]** New page name
### Examples
```javascript
page.setName('New name');
```
## getAllFrames
Get all frames
@ -10,7 +36,7 @@ Get all frames
const arrayOfFrames = page.getAllFrames();
```
Returns **[Array][1]\<Frame>**
Returns **[Array][2]\<Frame>**
## getMainFrame
@ -37,4 +63,6 @@ console.log(rootComponent.toHTML());
Returns **Component**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

55
docs/api/pages.md

@ -38,21 +38,24 @@ const pageManager = editor.Pages;
* [add][1]
* [get][2]
* [getAll][3]
* [getMain][4]
* [remove][5]
* [select][6]
* [getSelected][7]
* [getAllWrappers][4]
* [getMain][5]
* [remove][6]
* [select][7]
* [getSelected][8]
[Page]: page.html
[Component]: component.html
## add
Add new page
### Parameters
* `props` **[Object][8]** Page properties
* `opts` **[Object][8]?** Options (optional, default `{}`)
* `props` **[Object][9]** Page properties
* `opts` **[Object][9]?** Options (optional, default `{}`)
### Examples
@ -72,7 +75,7 @@ Remove page
### Parameters
* `page` **([String][9] | [Page])** Page or page id
* `page` **([String][10] | [Page])** Page or page id
* `opts` (optional, default `{}`)
### Examples
@ -84,7 +87,7 @@ const somePage = pageManager.get('page-id');
pageManager.remove(somePage);
```
Returns **[Page]**
Returns **[Page]** Removed Page
## get
@ -92,7 +95,7 @@ Get page by id
### Parameters
* `id` **[String][9]** Page id
* `id` **[String][10]** Page id
### Examples
@ -124,7 +127,21 @@ Get all pages
const arrayOfPages = pageManager.getAll();
```
Returns **[Array][10]<[Page]>**
Returns **[Array][11]<[Page]>**
## getAllWrappers
Get wrapper components (aka body) from all pages and frames.
### Examples
```javascript
const wrappers = pageManager.getAllWrappers();
// Get all `image` components from the project
const allImages = wrappers.map(wrp => wrp.findType('image')).flat();
```
Returns **[Array][11]<[Component]>**
## select
@ -132,7 +149,7 @@ Change the selected page. This will switch the page rendered in canvas
### Parameters
* `page` **([String][9] | [Page])** Page or page id
* `page` **([String][10] | [Page])** Page or page id
* `opts` (optional, default `{}`)
### Examples
@ -164,16 +181,18 @@ Returns **[Page]**
[3]: #getall
[4]: #getmain
[4]: #getallwrappers
[5]: #getmain
[5]: #remove
[6]: #remove
[6]: #select
[7]: #select
[7]: #getselected
[8]: #getselected
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

60
docs/api/sector.md

@ -0,0 +1,60 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## Sector
### 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
### getId
Get sector id.
Returns **[String][1]**
### getName
Get sector name.
Returns **[String][1]**
### setName
Update sector name.
#### Parameters
* `value` **[String][1]** New sector name
### isOpen
Check if the sector is open
Returns **[Boolean][2]**
### setOpen
Update Sector open state
#### Parameters
* `value` **[Boolean][2]**
### getProperties
Get sector properties.
Returns **[Array][3]<[Property]>**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

79
docs/api/selector.md

@ -0,0 +1,79 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## Selector
### Properties
* `name` **[String][1]** Selector name, eg. `my-class`
* `label` **[String][1]** Selector label, eg. `My Class`
* `type` **[Number][2]?** Type of the selector. 1 (class) | 2 (id)
* `active` **[Boolean][3]?** If not active, it's not selectable by the Style Manager.
* `private` **[Boolean][3]?** If true, it can't be seen by the Style Manager, but it will be rendered in the canvas and in export code.
* `protected` **[Boolean][3]?** If true, it can't be removed from the attacched component.
### toString
Get selector as a string.
#### Examples
```javascript
// Given such selector: { name: 'my-selector', type: 2 }
console.log(selector.toString());
// -> `#my-selector`
```
Returns **[String][1]**
### getLabel
Get selector label.
#### Examples
```javascript
// Given such selector: { name: 'my-selector', label: 'My selector' }
console.log(selector.getLabel());
// -> `My selector`
```
Returns **[String][1]**
### setLabel
Update selector label.
#### Parameters
* `label` **[String][1]** New label
#### Examples
```javascript
// Given such selector: { name: 'my-selector', label: 'My selector' }
selector.setLabel('New Label')
console.log(selector.getLabel());
// -> `New Label`
```
### getActive
Get selector active state.
Returns **[Boolean][3]**
### setActive
Update selector active state.
#### Parameters
* `value` **[Boolean][3]** New active state
[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/Boolean

234
docs/api/selector_manager.md

@ -35,25 +35,116 @@ 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 selectorManager = editor.SelectorManager;
// Listen to events
editor.on('selector:add', (selector) => { ... });
// Use the API
const sm = editor.Selectors;
sm.add(...);
```
## Available Events
* `selector:add` - Selector added. The [Selector] is passed as an argument to the callback.
* `selector:remove` - Selector removed. The [Selector] is passed as an argument to the callback.
* `selector:update` - Selector updated. The [Selector] and the object containing changes are passed as arguments to the callback.
* `selector:state` - States changed. An object containing all the available data about the triggered event is passed as an argument to the callback.
* `selector` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
## Methods
* [getConfig][2]
* [add][3]
* [addClass][4]
* [get][5]
* [get][4]
* [remove][5]
* [getAll][6]
* [setState][7]
* [getState][8]
* [getStates][9]
* [setStates][10]
* [getSelected][11]
* [addSelected][12]
* [removeSelected][13]
* [getSelectedTargets][14]
* [setComponentFirst][15]
* [getComponentFirst][16]
[Selector]: selector.html
[State]: state.html
[Component]: component.html
[CssRule]: css_rule.html
## getConfig
Get configuration object
Returns **[Object][9]**
Returns **[Object][17]**
## add
Add a new selector to the collection if it does not already exist.
You can pass selectors properties or string identifiers.
### Parameters
* `props` **([Object][17] | [String][18])** Selector properties or string identifiers, eg. `{ name: 'my-class', label: 'My class' }`, `.my-cls`
* `opts` **[Object][17]?** Selector options (optional, default `{}`)
### Examples
```javascript
const selector = selectorManager.add({ name: 'my-class', label: 'My class' });
console.log(selector.toString()) // `.my-class`
// Same as
const selector = selectorManager.add('.my-class');
console.log(selector.toString()) // `.my-class`
```
Returns **[Selector]**
## get
Get the selector by its name/type
### Parameters
* `name` **[String][18]** Selector name or string identifier
* `type`
### Examples
```javascript
const selector = selectorManager.get('.my-class');
// Get Id
const selectorId = selectorManager.get('#my-id');
```
Returns **([Selector] | null)**
## remove
Remove Selector.
### Parameters
* `selector` **([String][18] | [Selector])** Selector instance or Selector string identifier
* `opts`
### Examples
```javascript
const removed = selectorManager.remove('.myclass');
// or by passing the Selector
selectorManager.remove(selectorManager.get('.myclass'));
```
Returns **[Selector]** Removed Selector
## setState
@ -61,7 +152,7 @@ Change the selector state
### Parameters
* `value` **[String][10]** State value
* `value` **[String][18]** State value
### Examples
@ -73,90 +164,111 @@ Returns **this**
## getState
Get the current selector state
Get the current selector state value
Returns **[String][10]**
Returns **[String][18]**
## add
## getStates
Add a new selector to collection if it's not already exists. Class type is a default one
Get states
Returns **[Array][19]<[State]>**
## setStates
Set a new collection of states
### Parameters
* `name` **([String][10] | [Array][11])** Selector/s name
* `opts` **[Object][9]** Selector options (optional, default `{}`)
* `states` **[Array][19]<[Object][17]>** Array of new states
* `opts`
### Examples
```javascript
const states = selectorManager.setStates([
{ name: 'hover', label: 'Hover' },
{ name: 'nth-of-type(2n)', label: 'Even/Odd' }
]);
```
Returns **[Array][19]<[State]>**
## getSelected
* `opts.label` **[String][10]** Label for the selector, if it's not provided the label will be the same as the name (optional, default `''`)
* `opts.type` **[String][10]** Type of the selector. At the moment, only 'class' (1) is available (optional, default `1`)
Get commonly selected selectors, based on all selected components.
### Examples
```javascript
const selector = selectorManager.add('selectorName');
// Same as
const selector = selectorManager.add('selectorName', {
type: 1,
label: 'selectorName'
});
// Multiple selectors
const selectors = selectorManager.add(['.class1', '.class2', '#id1']);
const selected = selectorManager.getSelected();
console.log(selected.map(s => s.toString()))
```
Returns **(Model | [Array][11])**
Returns **[Array][19]<[Selector]>**
## addClass
## addSelected
Add class selectors
Add new selector to all selected components.
### Parameters
* `classes` **([Array][11] | [string][10])** Array or string of classes
* `props` **([Object][17] | [String][18])** Selector properties or string identifiers, eg. `{ name: 'my-class', label: 'My class' }`, `.my-cls`
### Examples
```javascript
sm.addClass('class1');
sm.addClass('class1 class2');
sm.addClass(['class1', 'class2']);
// -> [SelectorObject, ...]
selectorManager.addSelected('.new-class');
```
Returns **[Array][11]** Array of added selectors
## get
## removeSelected
Get the selector by its name
Remove a common selector from all selected components.
### Parameters
* `name` **([String][10] | [Array][11])** Selector name
* `type` **[String][10]** Selector type
* `selector` **([String][18] | [Selector])** Selector instance or Selector string identifier
### Examples
```javascript
const selector = selectorManager.get('selectorName');
// or get an array
const selectors = selectorManager.get(['class1', 'class2']);
selectorManager.removeSelected('.myclass');
```
Returns **(Model | [Array][11])**
## getSelectedTargets
## getAll
Get the array of currently selected targets.
Get all selectors
### Examples
Returns **Collection**
```javascript
const targetsToStyle = selectorManager.getSelectedTargets();
console.log(targetsToStyle.map(target => target.getSelectorsString()))
```
## escapeName
Returns **[Array][19]<([Component] | [CssRule])>**
Return escaped selector name
## setComponentFirst
Update component-first option.
If the component-first is enabled, all the style changes will be applied on selected components (ID rules) instead
of selectors (which would change styles on all components with those classes).
### Parameters
* `name` **[String][10]** Selector name to escape
* `value` **[Boolean][20]**
## getComponentFirst
Get the value of component-first option.
Returns **[Boolean][20]**
## getAll
Get all selectors
Returns **[String][10]** Escaped name
Returns **Collection<[Selector]>**
[1]: https://github.com/artf/grapesjs/blob/master/src/selector_manager/config/config.js
@ -164,9 +276,9 @@ Returns **[String][10]** Escaped name
[3]: #add
[4]: #addclass
[4]: #get
[5]: #get
[5]: #remove
[6]: #getall
@ -174,8 +286,26 @@ Returns **[String][10]** Escaped name
[8]: #getstate
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[9]: #getstates
[10]: #setstates
[11]: #getselected
[12]: #addselected
[13]: #removeselected
[14]: #getselectedtargets
[15]: #setcomponentfirst
[16]: #getcomponentfirst
[17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean

24
docs/api/state.md

@ -0,0 +1,24 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## State
### Properties
* `name` **[String][1]** State name, eg. `hover`, `nth-of-type(2n)`
* `label` **[String][1]** State label, eg. `Hover`, `Even/Odd`
### getName
Get state name
Returns **[String][1]**
### getLabel
Get state label. If label was not provided, the name will be returned.
Returns **[String][1]**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String

43
docs/api/style_manager.md

@ -34,6 +34,12 @@ const styleManager = editor.StyleManager;
* [getTypes][14]
* [createType][15]
[Sector]: sector.html
[CssRule]: css_rule.html
[Component]: component.html
## getConfig
Get configuration object
@ -43,7 +49,7 @@ Returns **[Object][16]**
## addSector
Add new sector to the collection. If the sector with the same id already exists,
that one will be returned
that one will be returned.
### Parameters
@ -53,13 +59,12 @@ that one will be returned
* `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 `[]`)
* `opts` (optional, default `{}`)
* `options` **[Object][16]** Options (optional, default `{}`)
### Examples
```javascript
var sector = styleManager.addSector('mySector',{
const sector = styleManager.addSector('mySector',{
name: 'My sector',
open: true,
properties: [{ name: 'My property'}]
@ -67,7 +72,7 @@ var sector = styleManager.addSector('mySector',{
// With `at: 0` we place the new sector at the beginning of the collection
```
Returns **Sector** Added Sector
Returns **[Sector]** Added Sector
## getSector
@ -81,10 +86,10 @@ Get sector by id
### Examples
```javascript
var sector = styleManager.getSector('mySector');
const sector = styleManager.getSector('mySector');
```
Returns **(Sector | null)**
Returns **([Sector] | null)**
## removeSector
@ -100,13 +105,23 @@ Remove a sector by id
const removed = styleManager.removeSector('mySector');
```
Returns **Sector** Removed sector
Returns **[Sector]** Removed sector
## getSectors
Get all sectors
Returns **Sectors** Collection of sectors
### Parameters
* `opts` (optional, default `{}`)
### Examples
```javascript
const sectors = styleManager.getSectors();
```
Returns **Collection<[Sector]>** Collection of sectors
## addProperty
@ -301,18 +316,6 @@ someContainer.appendChild(propView.el);
Returns **PropertyView**
## setTarget
Select different target for the Style Manager.
It could be a Component, CSSRule, or a string of any CSS selector
### Parameters
* `target` **(Component | CSSRule | [String][17])**
* `opts`
Returns **Styleable** A Component or CSSRule
[1]: https://github.com/artf/grapesjs/blob/master/src/style_manager/config/config.js
[2]: #getconfig

2
docs/modules/Assets.md

@ -316,7 +316,7 @@ am.open({
## Customization
The default Asset Manager UI is great for simple things, but except the possibility to tweak some CSS style, adding more complex things like a search input, filters, etc. requires a replace of the defualt UI.
The default Asset Manager UI is great for simple things, but except the possibility to tweak some CSS style, adding more complex things like a search input, filters, etc. requires a replace of the default UI.
All you have to do is to indicate the editor your intent to use a custom UI and then subscribe to the `asset:custom` event that will give you all the information on any requested change.

340
docs/modules/Blocks.md

@ -6,79 +6,339 @@ title: Block Manager
<p align="center"><img src="http://grapesjs.com/img/sc-grapesjs-blocks-prp.jpg" alt="GrapesJS - Block Manager" height="400" align="center"/></p>
A [Block] is a simple object which allows the end-user to reuse your [Components]. It can be connected to a single [Component] or to a complex composition of them. In this guide, you will see how to setup and take full advantage of the built-in Block Manager UI in GrapesJS.
The default UI is a lightweight component with built-in Drag & Drop support, but as you'll see next in this guide, it's easy to extend and create your own UI manager.
::: warning
To get a better understanding of the content in this guide, we recommend reading [Components] first
:::
::: warning
To get a better understanding of the content in this guide, we recommend reading [Components](Components.html) first
This guide is referring to GrapesJS v0.17.27 or higher
:::
[[toc]]
The Block is a group of [Components] and can be easily reused inside templates.
The difference between components and blocks: The component is more atomic, so a single image, a text box or a map is a component. The block is what the end user will drag inside the canvas, so it could contain a single image (single Component) or the entire section like, for example, the footer with a lot of components inside (texts, images, inputs, etc).
## Configuration
To change the default configurations you have to pass the `blockManager` property with the main configuration object.
```js
const editor = grapesjs.init({
...
blockManager: {
blocks: [...],
...
}
});
```
Check the full list of available options here: [Block Manager Config](https://github.com/artf/grapesjs/blob/master/src/block_manager/config/config.js)
## Initialization
By default, Block Manager UI is considered a hidden component. Currently, the GrapesJS core, renders default panels and buttons that allow you to show them, but in long term, this is something that might will change. Here below you can see how to init the editor without default panels and immediately rendered Block Manager UI.
::: tip
Follow the [Getting Started] guide in order to setup properly the editor with custom panels.
:::
```js
const editor = grapesjs.init({
container: '#gjs',
height: '100%',
storageManager: false,
panels: { defaults: [] }, // Avoid default panels
blockManager: {
appendTo: '.myblocks',
blocks: [
{
id: 'image',
label: 'Image',
media: `<svg style="width:24px;height:24px" viewBox="0 0 24 24">
<path d="M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z" />
</svg>`,
// Use `image` component
content: { type: 'image' },
// The component `image` is activatable (shows the Asset Manager).
// We want to activate it once dropped in the canvas.
activate: true,
// select: true, // Default with `activate: true`
}
],
}
});
```
Check out the [Components] page to see the list of built-in components and how to create your own.
Let's see how to add a new block to the editor using the [Blocks API]
## Block content types
The key of connecting blocks to components is the `block.content` property and what we passed in the example above is the [Component Definition]. This is the component-oriented way to create blocks and this is how we highly recommend the creation of your blocks.
### Component-oriented
The `content` can accept different formats, like an HTML string (which will be parsed and converted to components), but the component-oriented approach is the most precise as you can keep the control of your each dropped block in the canvas. Another advice is to keep your blocks' [Component Definition] as light as possible, if you're defining a lot of redundent properties, probably it makes sense to create another dedicated component, this might reduce the size of your project JSON file. Here an example:
```js
var editor = grapesjs.init({...});
var blockManager = editor.BlockManager;
// Your components
editor.Components.addType('my-cmp', {
model: {
defaults: {
prop1: 'value1',
prop2: 'value2',
}
}
});
// Your blocks
[
{ ..., content: { type: 'my-cmp', prop1: 'value1-EXT', prop2: 'value2-EXT' } }
{ ..., content: { type: 'my-cmp', prop1: 'value1-EXT', prop2: 'value2-EXT' } }
{ ..., content: { type: 'my-cmp', prop1: 'value1-EXT', prop2: 'value2-EXT' } }
]
```
Here we're reusing the same component multiple times with the same set of properties (just an example, makes more sense with composed content of components), this can be reduced to something like this.
// 'my-first-block' is the ID of the block
blockManager.add('my-first-block', {
label: 'Simple block',
content: '<div class="my-block">This is a simple block</div>',
```js
// Your components
editor.Components.addType('my-cmp', { ... });
editor.Components.addType('my-cmp-alt', {
extend: 'my-cmp',
model: {
defaults: {
prop1: 'value1-EXT',
prop2: 'value2-EXT'
}
}
});
// Your blocks
[
{ ..., content: { type: 'my-cmp-alt' } }
{ ..., content: { type: 'my-cmp-alt' } }
{ ..., content: { type: 'my-cmp-alt' } }
]
```
With this snippet a new block will be added to the collection. You can also update existent blocks
### HTML strings
Using HTML strings as `content` is not wrong, in some cases you don't need the finest control over components and want to leave the user full freedom on template composition (eg. static site builder editor with HTML copy-pasted from a framework like [Tailwind Components](https://tailwindcomponents.com/))
```js
// Your block
{
// ...
content: `<div class="el-X">
<div class="el-Y el-A">Element A</div>
<div class="el-Y el-B">Element B</div>
<div class="el-Y el-C">Element C</div>
</div>`
}
```
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`.
```js
blockManager.get('my-first-block').set({
label: 'Updated simple block',
attributes: {
title: 'My title'
// Your component
editor.Components.addType('cmp-Y', {
// Detect '.el-Y' elements
isComponent: el => el.classList?.contains('el-Y'),
model: {
defaults: {
name: 'Component Y', // Simple custom name
draggable: '.el-X', // Add `draggable` logic
}
}
})
});
```
Another alternative is to leverage `data-gjs-*` attributes to attach properties to components.
As you see a simple HTML string is enough to create a block, the editor will do the rest.
If you want you could also pass an object representing the [Component].
::: tip
You can use most of the available [Component properties](/api/component.html#properties).
:::
```js
blockManager.add('my-map-block', {
label: 'Simple map block',
content: {
type: 'map', // Built-in 'map' component
style: {
height: '350px'
},
removable: false, // Once inserted it can't be removed
// -- [Option 1]: Declare type in HTML strings --
{
// ...
content: `<div class="el-X">
<div data-gjs-type="cmp-Y" class="el-Y el-A">Element A</div>
<div data-gjs-type="cmp-Y" class="el-Y el-B">Element B</div>
<div data-gjs-type="cmp-Y" class="el-Y el-C">Element C</div>
</div>`
}
// Component
editor.Components.addType('cmp-Y', {
// You don't need `isComponent` anymore as you declare types already on elements
model: {
defaults: {
name: 'Component Y', // Simple custom name
draggable: '.el-X', // Add `draggable` logic
}
}
})
});
// -- [Option 2]: Declare properties in HTML strings (less recommended option) --
{
// ...
content: `<div class="el-X">
<div data-gjs-name="Component Y" data-gjs-draggable=".el-X" class="el-Y el-A">Element A</div>
<div data-gjs-name="Component Y" data-gjs-draggable=".el-X" class="el-Y el-B">Element B</div>
<div data-gjs-name="Component Y" data-gjs-draggable=".el-X" class="el-Y el-C">Element C</div>
</div>`
}
// No need for a custom component.
// You're already defining properties of each element.
```
Here we showed all the possibilities you have with HTML strings, but we strongly advise against the abuse of the `Option 2` and to stick to a more component-oriented approach.
Without a proper component type, not only your HTML will be harder to read, but all those defined properties will be "hard-coded" to a generic component of those elements. So, if one day you decide to "upgrade" the logic of the component (eg. `draggable: '.el-X'` -> `draggable: '.el-X, .el-Z'`), you won't be able.
### Mixed
It's also possible to mix components with HTML strings by passing an array.
```js
{
// ...
// Options like `activate`/`select` will be triggered only on the first component.
activate: true,
content: [
{ type: 'image' },
`<div>Extra</div>`
]
}
```
## Important caveats
::: danger Read carefully
&nbsp;
:::
### Avoid non serializable properties
Don't put non serializable properties, like functions, in your blocks, keep them only in your components.
```js
// Your block
{
content: {
type: 'my-cmp',
script() {...},
},
}
```
This will work, but if you try to save and reload a stored project, those will disappear.
### Avoid styles
Don't put styles in your blocks, keep them always in your components.
```js
// Your block
{
content: [
// BAD: You risk to create conflicting styles
{ type: 'my-cmp', styles: '.cmp { color: red }' },
{ type: 'my-cmp', styles: '.cmp { color: green }' },
// REALLY BAD: In case all related components are removed,
// there is no safe way for the editor to know how to connect
// and clean your styles.
`<div class="el">Element</div>
<div class="el2">Element 2</div>
<style>
.el { color: blue }
.el2 { color: violet }
</style>`,
],
}
```
<!-- Styles imported via component definition, by using the `styles` property, are connected to that specific component type. This allows the editor to remove automatically those styles if all related components are deleted. -->
With the component-oriented approach, you put yourself in a risk of conflicting styles and having a lot of useless redundant styles definitions in your project JSON.
With the HTML string, if you remove all related elements, the editor is not able to clean those styles from the project JSON, as there is no safe way to connect them.
## Programmatic usage
If you need to manage your blocks programmatically you can use its [APIs][Blocks API].
::: warning
All Blocks API methods update mainly your Block Manager UI, it has nothing to do with Components already dropped in the canvas.
:::
Below an example of commonly used methods.
```js
// Get the BlockManager module first
const bm = editor.Blocks; // `Blocks` is an alias of `BlockManager`
// Add a new Block
const block = bm.add('BLOCK-ID', {
// Your block properties...
label: 'My block',
content: '...',
});
// Get the Block
const block2 = bm.get('BLOCK-ID-2');
// Update the Block properties
block2.set({
label: 'Updated block',
});
// Remove the Block
const removedBlock = bm.remove('BLOCK-ID-2');
```
From v0.3.70 it's also possible to pass the HTML string with Component's properties as attributes.
To know more about the available block properties, check the [Block API Reference][Block].
## Customization
The default Block Manager UI is great for simple things, but except the possibility to tweak some CSS style, adding more complex elements requires a replace of the default UI.
All you have to do is to indicate the editor your intent to use a custom UI and then subscribe to the `block:custom` event that will give you all the information on any requested change.
```js
blockManager.add('the-row-block', {
label: '2 Columns',
content: '<div class="row" data-gjs-droppable=".row-cell" data-gjs-custom-name="Row">' +
'<div class="row-cell" data-gjs-draggable=".row"></div>' +
'<div class="row-cell" data-gjs-draggable=".row"></div>' +
'</div>',
const editor = grapesjs.init({
// ...
blockManager: {
// ...
custom: true,
},
});
editor.on('block:custom', props => {
// The `props` will contain all the information you need in order to update your UI.
// props.blocks (Array<Block>) - Array of all blocks
// props.dragStart (Function<Block>) - A callback to trigger the start of block dragging.
// props.dragStop (Function<Block>) - A callback to trigger the stop of block dragging.
// props.container (HTMLElement) - The default element where you can append your UI
// Here you would put the logic to render/update your UI.
});
```
In the example above you're defining a row component which will accept only elements which match '.row-cell' selector and cells which could be dragged only inside '.row' elements. We're also defining the custom name which will be seen inside the Layers panel.
If you want to check the complete list of available Component's properties, check directly the Component model source:
[https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/Component.js](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/Component.js)
Here an example of using custom Block Manager with a Vue component.
<demo-viewer value="xyofm1qr" height="500" darkcode/>
From the demo above you can also see how we decided to hide our custom Block Manager and append it to the default container, but that is up to your preferences.
## Events
For a complete list of available events, you can check it [here](/api/block_manager.html#available-events).
<!--
## Custom render <Badge text="0.14.55+"/>
If you need to customize the aspect of each block you can pass a `render` callback function in the block definition. Let's see how it works.
@ -121,11 +381,15 @@ blockManager.add('some-block-id', {
});
```
<img :src="$withBase('/block-custom-render2.jpg')">
-->
[Block]: </api/block.html>
[Component]: </api/component.html>
[Components]: <Components.html>
[Getting Started]: </getting-started.html>
[Blocks API]: </api/block_manager.html>
[Component Definition]: <Components.html#component-definition>

112
docs/modules/Components.md

@ -42,7 +42,7 @@ editor.getWrapper().append(`<div>...`);
```
::: tip
If you need to append a component in at a specific position, you can use `at` option. So, to add a component on top of all others (in the same collection) you would use
If you need to append a component at a specific position, you can use `at` option. So, to add a component on top of all others (in the same collection) you would use
```js
component.append('<div>...', { at: 0 })
```
@ -747,6 +747,115 @@ editor.on(`component:remove`, model => console.log('Global hook: component:remov
## Components & CSS
::: warning
This section is referring to GrapesJS v0.17.27 or higher
:::
If you need to add component-related styles, you can do it via `styles` property.
```js
domc.addType('component-css', {
model: {
defaults: {
attributes: { class: 'cmp-css' },
components: `
<span>Component with styles<span>
<div class="cmp-css-a">Component A</div>
<div class="cmp-css-b">Component B</div>
`,
styles: `
.cmp-css { color: red }
.cmp-css-a { color: green }
.cmp-css-b { color: blue }
@media (max-width: 992px) {
.cmp-css{ color: darkred; }
.cmp-css-a { color: darkgreen }
.cmp-css-b { color: darkblue }
}
`,
},
},
});
```
This approach allows the editor to group these styles ([CssRule] instances) and remove them accordingly in case all references of the same component are removed.
::: danger Important caveat
&nbsp;
:::
In the example above we used one custom component and default sub-components. Styles are declared on our custom component only, that means if you remove all `.cmp-css-a` and `.cmp-css-b` instances from the canvas, their CssRules will still be stored in the project (**here we're not talking about the CSS export, which is able to skip not used rules, but instances stored in your project JSON**).
The cleanest approach would be to follow component-oriented styling, where you declare styles only in the scope of the component itself. This is how it would look like with the example above.
```js
domc.addType('cmp-a', {
model: {
defaults: {
attributes: { class: 'cmp-css-a' },
components: 'Component A',
styles: `
.cmp-css-a { color: green }
@media (max-width: 992px) {
.cmp-css-a { color: darkgreen }
}
`,
}
},
});
domc.addType('cmp-b', {
model: {
defaults: {
attributes: { class: 'cmp-css-b' },
components: 'Component B',
styles: `
.cmp-css-b { color: blue }
@media (max-width: 992px) {
.cmp-css-b { color: darkblue }
}
`,
}
},
});
domc.addType('component-css', {
model: {
defaults: {
attributes: { class: 'cmp-css' },
components: [
'<span>Component with styles<span>',
{ type: 'cmp-a' },
{ type: 'cmp-b' },
],
styles: `
.cmp-css { color: red }
@media (max-width: 992px) {
.cmp-css{ color: darkred; }
}
`,
},
},
});
```
::: tip Component-first styling
By default, when you select a component in the canvas and apply styles on it, changes will be applied on its existent classes. This will result on changing of all the components with those applied classes. If you need the style to be applied only on the specific selected component you have to select componentFirst strategy in this way.
```js
grapesjs.init({
...
selectorManager: {
componentFirst: true,
},
})
```
:::
### External CSS
If you need to load external component-specific CSS, you have to rely on the `script` property. For more details please refer to [Components & JS](Components-js.html).
## Components & JS
If you want to know how to create Components with javascript attached (eg. counters, galleries, slideshows, etc.) check the dedicated page
@ -839,4 +948,5 @@ For Babel users, it's just a matter of adding few plugins: `@babel/plugin-syntax
[Component Definition]: <#component-definition>
[Component]: </api/component.html>
[CssRule]: </api/css_rule.html>
[Component API]: </api/component.html>

150
docs/modules/Selectors.md

@ -0,0 +1,150 @@
---
title: Selector Manager
---
# Selector Manager
<p align="center"><img :src="$withBase('/selector-manager.jpg')" alt="GrapesJS - Selector Manager"/></p>
The [Selector] allows the reuse of styles across all of your [Components] in the project (exactly what classes do in HTML) and the main goal of the Selector Manager is to collect them and indicate the current state of the selection.
::: warning
This guide is referring to GrapesJS v0.17.28 or higher
:::
[[toc]]
## Configuration
To change the default configurations you have to pass the `selectorManager` property with the main configuration object.
```js
const editor = grapesjs.init({
...
selectorManager: {
...
}
});
```
Check the full list of available options here: [Selector Manager Config](https://github.com/artf/grapesjs/blob/master/src/selector_manager/config/config.js)
## Initialization
The Selector Manager starts to collect data once componenets and styles are loaded. The default UI is displayed along with the default panels provided by GrapesJS core, in case you need to setup the editor with your own panels we recommend following the [Getting Started] guide.
In the example below we init the editor with already provided components and styles.
```js
const editor = grapesjs.init({
container: '#gjs',
height: '100%',
storageManager: false,
components: `
<div class="class-a">Element A</div>
<div class="class-a class-b">Element A-B</div>
<div class="class-a class-b class-c">Element A-B-C</div>
`,
style: `
.class-a { color: red }
.class-b { color: green }
.class-c { color: blue }
`,
});
```
Internally, the example above will provide to Selector Manager 3 selectors: `class-a`, `class-b` and `class-c`.
Without any selected component, the Selector Manager UI is hidden by default (along with the Style Manager). By selecting the `Element A-B-C` you will see the current selection of what will be actually styled.
<img :src="$withBase('/sm-selected-component.jpg')" alt="Selected component" style="display: block; margin: auto"/>
The label **Selected** indicates on which CSS query styles will be applied, so if you try to change the color of the current selection, this is what you'll get in the final code:
```css
.class-a.class-b.class-c {
color: #483acb;
}
```
You can also disable specific selectors and change the state (eg. Hover) in order to switch the target of styling.
<img :src="$withBase('/sm-disable-selector.jpg')" alt="Disabled selectors" style="display: block; margin: auto"/>
## Component-first selectors
By default, selecting components with classes will indicate their selectors as target style. That means that any change in Style Manager will be applied to all components containing those **Selected** classes.
In case you need to select single components as style targets, you can enable `componentFirst` option.
```js
const editor = grapesjs.init({
// ...
selectorManager: {
componentFirst: true,
},
});
```
This option enables also the possibility to style multiple components and the ability to sync common selectors with the current component styles (the refresh icon).
<img :src="$withBase('/sm-component-first.jpg')" alt="Component First" style="display: block; margin: auto"/>
::: warning
With multiple selection, the Style Manager shows always styles of the last selected component.
:::
## Programmatic usage
If you need to manage your selectors programmatically you can use its [APIs][Selector API].
## Customization
The default UI can handle most of the common tasks but in case you need a more advanced logic/elements, that requires a replace of the default UI.
All you have to do is to indicate the editor your intent to use a custom UI and then subscribe to the `selector:custom` event that will trigger on any necessary update of the UI.
```js
const editor = grapesjs.init({
// ...
selectorManager: {
custom: true,
// ...
},
});
editor.on('selector:custom', props => {
// props.container (HTMLElement) - The default element where you can append your UI
// Here you would put the logic to render/update your UI.
});
```
In the example below we'll replicate most of the default functionality by using solely the Selector Manager API.
<demo-viewer value="v8cgkLfr" height="500" darkcode/>
## Events
For a complete list of available events, you can check it [here](/api/selector_manager.html#available-events).
[Selector]: </api/selector.html>
[Style Manager]: <Style-manager.html>
[Components]: <Components.html>
[Getting Started]: </getting-started.html>
[Selector API]: </api/selector_manager.html>

3
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.17.26",
"version": "0.17.28",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",
@ -38,7 +38,6 @@
"prettier": "^1.18.2",
"sass": "^1.42.1",
"sinon": "^7.5.0",
"string-replace-loader": "^2.2.0",
"vuepress": "^1.8.2",
"whatwg-fetch": "^3.6.2"
},

28
scripts/build-locale.js

@ -1,28 +0,0 @@
const fs = require('fs');
const path = require('path');
const localeDir = './locale';
const localeSrcDir = './src/i18n/locale';
const copyRecursiveSync = (src, dest) => {
const exists = fs.existsSync(src);
const isDir = exists && fs.statSync(src).isDirectory();
if (isDir) {
fs.mkdirSync(dest);
fs.readdirSync(src).forEach((file) => {
copyRecursiveSync(path.join(src, file), path.join(dest, file));
});
} else if (exists) {
fs.createReadStream(src).pipe(fs.createWriteStream(dest));
}
};
copyRecursiveSync(localeSrcDir, localeDir);
// Create locale/index.js file
let result = '';
fs.readdirSync(localeDir).forEach(file => {
const name = file.replace('.js', '');
result += `export { default as ${name} } from './${name}'\n`;
});
fs.writeFileSync(`${localeDir}/index.js`, result);

1
src/asset_manager/index.js

@ -17,6 +17,7 @@
* ## Available Events
* * `asset:open` - Asset Manager opened.
* * `asset:close` - Asset Manager closed.
* * `asset:add` - Asset added. The [Asset] is passed as an argument to the callback.
* * `asset:remove` - Asset removed. The [Asset] is passed as an argument to the callback.
* * `asset:update` - Asset updated. The updated [Asset] and the object containing changes are passed as arguments to the callback.
* * `asset:upload:start` - Before the upload is started.

6
src/block_manager/config/config.js

@ -16,5 +16,9 @@ export default {
// }
appendOnClick: false,
blocks: []
// Default blocks
blocks: [],
// Avoid rendering the default block manager.
custom: false
};

260
src/block_manager/index.js

@ -8,11 +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('block:add', (block) => { ... });
*
* // Use the API
* const blockManager = editor.BlockManager;
* blockManager.add(...);
* ```
*
* ## Available Events
* * `block:add` - Block added. The [Block] is passed as an argument to the callback.
* * `block:remove` - Block removed. The [Block] is passed as an argument to the callback.
* * `block:update` - Block updated. The [Block] and the object containing changes are passed as arguments to the callback.
* * `block:drag:start` - Started dragging block, the [Block] is passed as an argument.
* * `block:drag` - Dragging block, the [Block] is passed as an argument.
* * `block:drag:stop` - Dragging of the block is stopped. The dropped [Component] (if dropped successfully) and the [Block] are passed as arguments.
* * `block` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
*
* ## Methods
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
@ -24,65 +40,167 @@
* * [render](#render)
*
* [Block]: block.html
* [Component]: component.html
*
* @module BlockManager
*/
import { isElement } from 'underscore';
import { isElement, isArray } from 'underscore';
import Module from 'common/module';
import defaults from './config/config';
import Block from './model/Block';
import Blocks from './model/Blocks';
import BlockCategories from './model/Categories';
import Category from './model/Category';
import Categories from './model/Categories';
import BlocksView from './view/BlocksView';
export const evAll = 'block';
export const evPfx = `${evAll}:`;
export const evAdd = `${evPfx}add`;
export const evUpdate = `${evPfx}update`;
export const evRemove = `${evPfx}remove`;
export const evRemoveBefore = `${evRemove}:before`;
export const evDrag = `${evPfx}drag`;
export const evDragStart = `${evDrag}:start`;
export const evDragStop = `${evDrag}:stop`;
export const evCustom = `${evPfx}custom`;
export default () => {
var c = {};
var blocks, blocksVisible, blocksView;
var categories = [];
return {
/**
* Name of the module
* @type {String}
* @private
*/
...Module,
name: 'BlockManager',
/**
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations
* @return {this}
* @private
*/
init(config) {
c = config || {};
const em = c.em;
Block,
for (let name in defaults) {
if (!(name in c)) {
c[name] = defaults[name];
}
}
Blocks,
Category,
Categories,
events: {
all: evAll,
update: evUpdate,
add: evAdd,
remove: evRemove,
removeBefore: evRemoveBefore,
drag: evDrag,
dragStart: evDragStart,
dragEnd: evDragStop,
custom: evCustom
},
init(config = {}) {
c = { ...defaults, ...config };
const { em } = c;
this.em = em;
// Global blocks collection
blocks = new Blocks([]);
blocksVisible = new Blocks([]);
categories = new BlockCategories();
blocks = new Blocks(c.blocks);
blocksVisible = new Blocks(blocks.models);
categories = new Categories();
this.all = blocks;
this.__initListen();
// Setup the sync between the global and public collections
blocks.listenTo(blocks, 'add', model => {
blocksVisible.add(model);
em && em.trigger('block:add', model);
});
blocks.on('add', model => blocksVisible.add(model));
blocks.on('remove', model => blocksVisible.remove(model));
blocks.on('reset', coll => blocksVisible.reset(coll.models));
blocks.listenTo(blocks, 'remove', model => {
blocksVisible.remove(model);
em && em.trigger('block:remove', model);
});
return this;
},
__trgCustom() {
this.em.trigger(this.events.custom, this.__customData());
},
__customData() {
const bhv = this.__getBehaviour();
return {
bm: this,
blocks: this.getAll().models,
container: bhv.container,
dragStart: (block, ev) => this.startDrag(block, ev),
drag: ev => this.__drag(ev),
dragStop: cancel => this.endDrag(cancel)
};
},
__startDrag(block, ev) {
const { em, events } = this;
const content = block.getContent ? block.getContent() : block;
this._dragBlock = block;
em.set({ dragResult: null, dragContent: content });
[em, blocks].map(i => i.trigger(events.dragStart, block, ev));
},
__drag(ev) {
const { em, events } = this;
const block = this._dragBlock;
[em, blocks].map(i => i.trigger(events.drag, block, ev));
},
__endDrag() {
const { em, events } = this;
const block = this._dragBlock;
const cmp = em.get('dragResult');
this._dragBlock = null;
if (cmp) {
const oldKey = 'activeOnRender';
const oldActive = cmp.get && cmp.get(oldKey);
const toActive = block.get('activate') || oldActive;
const toSelect = block.get('select');
const first = isArray(cmp) ? cmp[0] : cmp;
if (toSelect || (toActive && toSelect !== false)) {
em.setSelected(first);
}
if (toActive) {
first.trigger('active');
oldActive && first.unset(oldKey);
}
if (block.get('resetId')) {
first.onAll(block => block.resetId());
}
}
em.set({ dragResult: null, dragContent: null });
[em, blocks].map(i => i.trigger(events.dragEnd, cmp, block));
},
__getFrameViews() {
return this.em
.get('Canvas')
.getFrames()
.map(frame => frame.view);
},
blocks.listenTo(blocks, 'reset', coll => {
blocksVisible.reset(coll.models);
__behaviour(opts = {}) {
return (this._bhv = {
...(this._bhv || {}),
...opts
});
},
return this;
__getBehaviour() {
return this._bhv || {};
},
startDrag(block, ev) {
this.__startDrag(block, ev);
this.__getFrameViews().forEach(fv => fv.droppable.startCustom());
},
endDrag(cancel) {
this.__getFrameViews().forEach(fv => fv.droppable.endCustom(cancel));
this.__endDrag();
},
/**
@ -93,18 +211,6 @@ export default () => {
return c;
},
/**
* Load default blocks if the collection is empty
*/
onLoad() {
const blocks = this.getAll();
!blocks.length && blocks.reset(c.blocks);
},
/**
* Executed once the main editor instance is rendered
* @private
*/
postRender() {
const collection = blocksVisible;
blocksView = new BlocksView({ collection, categories }, c);
@ -112,22 +218,17 @@ export default () => {
if (elTo) {
const el = isElement(elTo) ? elTo : document.querySelector(elTo);
if (!el) return this.__logWarn('"appendTo" element not found');
el.appendChild(this.render(blocksVisible.models));
}
this.__trgCustom();
},
/**
* Add new block to the collection.
* @param {string} id Block id
* @param {Object} opts Options
* @param {string} opts.label Name of the block
* @param {string} opts.content HTML content
* @param {string|Object} opts.category Group the block inside a category.
* You should pass objects with id property, eg:
* {id: 'some-uid', label: 'My category'}
* The string will be converted in:
* 'someid' => {id: 'someid', label: 'someid'}
* @param {Object} [opts.attributes={}] Block attributes
* Add new block.
* @param {String} id Block ID
* @param {[Block]} props Block properties
* @returns {[Block]} Added block
* @example
* blockManager.add('h1-block', {
@ -139,15 +240,15 @@ export default () => {
* }
* });
*/
add(id, opts) {
var obj = opts || {};
obj.id = id;
return blocks.add(obj);
add(id, props, opts = {}) {
const prp = props || {};
prp.id = id;
return blocks.add(prp, opts);
},
/**
* Return the block by id
* @param {string} id Block id
* Get the block by id.
* @param {String} id Block id
* @returns {[Block]}
* @example
* const block = blockManager.get('h1-block');
@ -159,8 +260,8 @@ export default () => {
},
/**
* Return all blocks
* @return {Collection}
* Return all blocks.
* @returns {Collection<[Block]>}
* @example
* const blocks = blockManager.getAll();
* console.log(JSON.stringify(blocks));
@ -172,23 +273,24 @@ export default () => {
/**
* Return the visible collection, which containes blocks actually rendered
* @return {Collection}
* @returns {Collection<[Block]>}
*/
getAllVisible() {
return blocksVisible;
},
/**
* Remove a block by id
* @param {string} id Block id
* Remove block.
* @param {String|[Block]} block Block or block ID
* @returns {[Block]} Removed block
* @example
* // Id of the block which need to be removed
* const id = 'button';
* blockManager.remove(id);
* const removed = blockManager.remove('BLOCK_ID');
* // or by passing the Block
* const block = blockManager.get('BLOCK_ID');
* blockManager.remove(block);
*/
remove(id) {
return blocks.remove(id);
remove(block, opts = {}) {
return this.__remove(block, opts);
},
/**
@ -261,12 +363,10 @@ export default () => {
},
destroy() {
blocks.reset();
blocks.stopListening();
blocksVisible.reset();
categories.reset();
const colls = [blocks, blocksVisible, categories];
colls.map(c => c.stopListening());
colls.map(c => c.reset());
blocksView && blocksView.remove();
[blocks, blocksVisible, categories, blocksView].forEach(i => (i = null));
c = {};
}
};

52
src/block_manager/model/Block.js

@ -1,13 +1,14 @@
import { Model } from 'common';
import { isFunction } from 'underscore';
/**
* @property {String} label Block label, eg. `My block`
* @property {String|Object} content The content of the block. Might be an HTML string or a [Component Defintion](/modules/Components.html#component-definition)
* @property {String} [media=''] HTML string for the media/icon of the block, eg. `<svg ...`, `<img ...`, etc.
* @property {String} [category=''] Block category
* @property {Boolean} [activate=false] If true, triggers an the `active` event on dropped component
* @property {Boolean} [select=false] If true, the dropped component will be selected
* @property {Boolean} [resetId=false] If true, all IDs of dropped components and their styles will be changed
* @property {String} [category=''] Block category, eg. `Basic blocks`
* @property {Boolean} [activate=false] If true, triggers the `active` event on the dropped component.
* @property {Boolean} [select=false] If true, the dropped component will be selected.
* @property {Boolean} [resetId=false] If true, all IDs of dropped components and their styles will be changed.
* @property {Boolean} [disable=false] Disable the block from being interacted
* @property {Function} [onClick] Custom behavior on click, eg. `(block, editor) => editor.getWrapper().append(block.get('content'))`
* @property {Object} [attributes={}] Block attributes to apply in the view element
@ -20,11 +21,52 @@ export default class Block extends Model {
media: '',
category: '',
activate: false,
select: false,
select: null,
resetId: false,
disable: false,
onClick: null,
attributes: {}
};
}
/**
* Get block id
* @returns {String}
*/
getId() {
return this.id;
}
/**
* Get block label
* @returns {String}
*/
getLabel() {
return this.get('label');
}
/**
* Get block media
* @returns {String}
*/
getMedia() {
return this.get('media');
}
/**
* Get block content
* @returns {Object|String|Array<Object|String>} Component definition | HTML string
*/
getContent() {
return this.get('content');
}
/**
* Get block category label
* @returns {String}
*/
getCategoryLabel() {
const ctg = this.get('category');
return isFunction(ctg.get) ? ctg.get('label') : ctg.label ? ctg.label : ctg;
}
}

8
src/block_manager/model/Blocks.js

@ -1,6 +1,6 @@
import Backbone from 'backbone';
import { Collection } from 'common';
import Block from './Block';
export default Backbone.Collection.extend({
model: Block
});
export default class Blocks extends Collection {}
Blocks.prototype.model = Block;

8
src/block_manager/model/Categories.js

@ -1,6 +1,6 @@
import Backbone from 'backbone';
import { Collection } from 'common';
import Category from './Category';
export default Backbone.Collection.extend({
model: Category
});
export default class Categories extends Collection {}
Categories.prototype.model = Category;

18
src/block_manager/model/Category.js

@ -1,10 +1,12 @@
import Backbone from 'backbone';
import { Model } from 'common';
export default Backbone.Model.extend({
defaults: {
id: '',
label: '',
open: true,
attributes: {}
export default class Category extends Model {
defaults() {
return {
id: '',
label: '',
open: true,
attributes: {}
};
}
});
}

50
src/block_manager/view/BlockView.js

@ -1,5 +1,5 @@
import Backbone from 'backbone';
import { isFunction, isObject } from 'underscore';
import { isFunction } from 'underscore';
import { on, off, hasDnd } from 'utils/mixins';
export default Backbone.View.extend({
@ -21,6 +21,10 @@ export default Backbone.View.extend({
this.listenTo(model, 'change', this.render);
},
__getModule() {
return this.em.get('BlockManager');
},
handleClick(ev) {
const { config, model, em } = this;
const onClick = model.get('onClick') || config.appendOnClick;
@ -80,53 +84,15 @@ export default Backbone.View.extend({
},
handleDragStart(ev) {
const { em, model } = this;
const content = model.get('content');
const isObj = isObject(content);
const data = isObj ? JSON.stringify(content) : content;
em.set('dragResult');
// Note: data are not available on dragenter for security reason,
// we have to use dragContent as we need it for the Sorter context
// IE11 supports only 'text' data type
ev.dataTransfer.setData('text', data);
em.set('dragContent', content);
em.trigger('block:drag:start', model, ev);
this.__getModule().__startDrag(this.model, ev);
},
handleDrag(ev) {
this.em.trigger('block:drag', this.model, ev);
this.__getModule().__drag(ev);
},
handleDragEnd() {
const { em, model } = this;
const result = em.get('dragResult');
if (result) {
const oldKey = 'activeOnRender';
const oldActive = result.get && result.get(oldKey);
const toActive = model.get('activate') || oldActive;
if (model.get('select') || toActive) {
em.setSelected(result);
}
if (toActive) {
result.trigger('active');
result.unset(oldKey);
}
if (model.get('resetId')) {
result.onAll(model => model.resetId());
}
}
em.set({
dragResult: null,
dragContent: null
});
em.trigger('block:drag:stop', result, model);
this.__getModule().__endDrag();
},
/**

2
src/block_manager/view/BlocksView.js

@ -5,7 +5,7 @@ import CategoryView from './CategoryView';
export default Backbone.View.extend({
initialize(opts, config) {
bindAll(this, 'getSorter', 'onDrag', 'onDrop');
bindAll(this, 'getSorter', 'onDrag', 'onDrop', 'onMove');
this.config = config || {};
this.categories = opts.categories || '';
this.renderedCategories = [];

6
src/canvas/index.js

@ -29,10 +29,12 @@
* * [setCoords](#setcoords)
*
* [Component]: component.html
* [Frame]: frame.html
*
* @module Canvas
*/
import { isUndefined } from 'underscore';
import { getElement, getViewEl } from 'utils/mixins';
import defaults from './config/config';
import Canvas from './model/Canvas';
@ -430,7 +432,7 @@ export default () => {
const { event } = opts;
let top = -toolbarH;
let left = pos.width - toolbarW;
let left = !isUndefined(opts.left) ? opts.left : pos.width - toolbarW;
left = pos.left < -left ? -pos.left : left;
left = elRight > frCvOff.width ? left - (elRight - frCvOff.width) : left;
@ -642,7 +644,7 @@ export default () => {
/**
* Add new frame to the canvas
* @param {Object} props Frame properties
* @returns {Frame}
* @returns {[Frame]}
* @example
* canvas.addFrame({
* name: 'Mobile home page',

2
src/canvas/model/Canvas.js

@ -1,4 +1,4 @@
import { Model } from 'backbone';
import { Model } from 'common';
import { evPageSelect } from 'pages';
export default class Canvas extends Model {

12
src/canvas/model/Frame.js

@ -1,10 +1,18 @@
import { Model } from 'backbone';
import { result, forEach, isEmpty, debounce, isString } from 'underscore';
import { Model } from 'common';
import { result, forEach, isEmpty, isString } from 'underscore';
import { isComponent, isObject } from 'utils/mixins';
const keyAutoW = '__aw';
const keyAutoH = '__ah';
/**
* @property {Object|String} component Wrapper component definition. You can also pass an HTML string as components of the default wrapper component.
* @property {String} [width=''] Width of the frame. By default, the canvas width will be taken.
* @property {String} [height=''] Height of the frame. By default, the canvas height will be taken.
* @property {Number} [x=0] Horizontal position of the frame in the canvas.
* @property {Number} [y=0] Vertical position of the frame in the canvas.
*
*/
export default class Frame extends Model {
defaults() {
return {

2
src/canvas/model/Frames.js

@ -1,5 +1,5 @@
import { bindAll } from 'underscore';
import { Collection } from 'backbone';
import { Collection } from 'common';
import model from './Frame';
export default class Frames extends Collection {

1
src/canvas/view/FrameView.js

@ -434,6 +434,7 @@ export default Backbone.View.extend({
[
{ event: 'keydown keyup keypress', class: 'KeyboardEvent' },
{ event: 'mousedown mousemove mouseup', class: 'MouseEvent' },
{ event: 'pointerdown pointermove pointerup', class: 'PointerEvent' },
{ event: 'wheel', class: 'WheelEvent' }
].forEach(obj =>
obj.event.split(' ').forEach(event => {

2
src/commands/index.js

@ -385,7 +385,7 @@ export default () => {
const editor = em.get('Editor');
if (!this.isActive(id) || options.force || !c.strict) {
result = command.callRun(editor, options);
result = editor && command.callRun(editor, options);
if (id && command.stop && !command.noStop && !options.abort) {
active[id] = result;
}

22
src/commands/view/ComponentNext.js

@ -3,11 +3,23 @@ export default {
if (!ed.Canvas.hasFocus()) return;
const toSelect = [];
ed.getSelectedAll().forEach(component => {
const coll = component.collection;
const at = coll.indexOf(component);
const next = coll.at(at + 1);
toSelect.push(next || component);
ed.getSelectedAll().forEach(cmp => {
const parent = cmp.parent();
if (!parent) return;
const len = parent.components().length;
let incr = 0;
let at = 0;
let next;
// Get the next selectable component
do {
incr++;
at = cmp.index() + incr;
next = at <= len ? parent.getChildAt(at) : null;
} while (next && !next.get('selectable'));
toSelect.push(next || cmp);
});
toSelect.length && ed.select(toSelect);

21
src/commands/view/ComponentPrev.js

@ -3,11 +3,22 @@ export default {
if (!ed.Canvas.hasFocus()) return;
const toSelect = [];
ed.getSelectedAll().forEach(component => {
const coll = component.collection;
const at = coll.indexOf(component);
const next = coll.at(at - 1);
toSelect.push(next && at - 1 >= 0 ? next : component);
ed.getSelectedAll().forEach(cmp => {
const parent = cmp.parent();
if (!parent) return;
let incr = 0;
let at = 0;
let next;
// Get the first selectable component
do {
incr++;
at = cmp.index() - incr;
next = at >= 0 ? parent.getChildAt(at) : null;
} while (next && !next.get('selectable'));
toSelect.push(next || cmp);
});
toSelect.length && ed.select(toSelect);

27
src/commands/view/ComponentStyleClear.js

@ -1,25 +1,22 @@
import { isArray } from 'underscore';
import { isArray, flatten } from 'underscore';
export default {
run(ed, sender, opts = {}) {
run(ed, s, opts = {}) {
const { target } = opts;
const dc = ed.DomComponents;
let toRemove = [];
if (!target.get('styles')) return toRemove;
// Find all components in the project, of the target component type
const type = target.get('type');
const len = dc.getWrapper().findType(type).length;
const toRemove = [];
const wrappers = ed.Pages.getAllWrappers();
const len = flatten(wrappers.map(wrp => wrp.findType(type))).length;
// Remove component related styles only if there are no more components
// of that type in the project
if (!len) {
const rules = ed.CssComposer.getAll();
let toClear = target.get('style-signature');
toClear = isArray(toClear) ? toClear : [toClear];
rules.forEach(rule => {
const selector = rule.selectorsToString();
toClear.forEach(part => {
part && selector.indexOf(part) >= 0 && toRemove.push(rule);
});
});
toRemove = rules.filter(rule => rule.get('group') === `cmp:${type}`);
rules.remove(toRemove);
}

58
src/commands/view/OpenBlocks.js

@ -1,23 +1,57 @@
import { isFunction } from 'underscore';
import { createEl } from '../../utils/dom';
export default {
run(editor, sender) {
const bm = editor.BlockManager;
const pn = editor.Panels;
open() {
const { container, editor, bm, config } = this;
const { custom, appendTo } = config;
if (isFunction(custom.open)) {
return custom.open(bm.__customData());
}
if (!this.blocks) {
bm.render();
if (this.firstRender && !appendTo) {
const id = 'views-container';
const blocks = document.createElement('div');
const pn = editor.Panels;
const panels = pn.getPanel(id) || pn.addPanel({ id });
blocks.appendChild(bm.getContainer());
panels.set('appendContent', blocks).trigger('change:appendContent');
this.blocks = blocks;
panels.set('appendContent', container).trigger('change:appendContent');
if (!custom) container.appendChild(bm.render());
}
if (container) container.style.display = 'block';
},
close() {
const { container, config } = this;
const { custom } = config;
if (isFunction(custom.close)) {
return custom.close(this.bm.__customData());
}
if (container) container.style.display = 'none';
},
run(editor) {
const bm = editor.Blocks;
this.config = bm.getConfig();
this.firstRender = !this.container;
this.container = this.container || createEl('div');
this.editor = editor;
this.bm = bm;
const { container } = this;
bm.__behaviour({
container
});
if (this.config.custom) {
bm.__trgCustom();
}
this.blocks.style.display = 'block';
this.open();
},
stop() {
const blocks = this.blocks;
blocks && (blocks.style.display = 'none');
this.close();
}
};

9
src/commands/view/OpenLayers.js

@ -1,13 +1,10 @@
import Backbone from 'backbone';
import Layers from 'navigator';
const $ = Backbone.$;
export default {
run(editor) {
const lm = editor.LayerManager;
const pn = editor.Panels;
if (lm.getConfig().appendTo) return;
if (!this.layers) {
const id = 'views-container';
const layers = document.createElement('div');
@ -21,7 +18,7 @@ export default {
},
stop() {
const layers = this.layers;
const { layers } = this;
layers && (layers.style.display = 'none');
}
};

72
src/commands/view/OpenStyleManager.js

@ -4,47 +4,56 @@ const $ = Backbone.$;
export default {
run(em, sender) {
this.sender = sender;
if (!this.$cn) {
var config = em.getConfig(),
panels = em.Panels;
// Main container
const config = em.getConfig();
const panels = em.Panels;
const trgEvCnt = 'change:appendContent';
this.$cn = $('<div></div>');
// Secondary container
this.$cn2 = $('<div></div>');
this.$cn.append(this.$cn2);
// Device Manager
var dvm = em.DeviceManager;
const dvm = em.DeviceManager;
if (dvm && config.showDevices) {
var devicePanel = panels.addPanel({ id: 'devices-c' });
devicePanel
.set('appendContent', dvm.render())
.trigger('change:appendContent');
const devicePanel = panels.addPanel({ id: 'devices-c' });
const dvEl = dvm.render();
devicePanel.set('appendContent', dvEl).trigger(trgEvCnt);
}
// Selector Manager container
const slm = em.SelectorManager;
const slmConfig = slm.getConfig();
if (slmConfig.custom) {
slm.__trgCustom({ container: this.$cn2.get(0) });
} else if (!slmConfig.appendTo) {
this.$cn2.append(slm.render([]));
}
// Class Manager container
var clm = em.SelectorManager;
if (clm) this.$cn2.append(clm.render([]));
this.$cn2.append(em.StyleManager.render());
var smConfig = em.StyleManager.getConfig();
const pfx = smConfig.stylePrefix;
// Create header
this.$header = $(
`<div class="${pfx}header">${em.t('styleManager.empty')}</div>`
);
this.$cn.append(this.$header);
// Style Manager
const sm = em.StyleManager;
const smConfig = sm.getConfig();
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
if (!panels.getPanel('views-container'))
this.panel = panels.addPanel({ id: 'views-container' });
else this.panel = panels.getPanel('views-container');
const pnCnt = 'views-container';
this.panel = panels.getPanel(pnCnt);
if (!this.panel) this.panel = panels.addPanel({ id: pnCnt });
// Add all containers to the panel
this.panel.set('appendContent', this.$cn).trigger('change:appendContent');
this.panel.set('appendContent', this.$cn).trigger(trgEvCnt);
this.target = em.editor;
this.listenTo(this.target, 'component:toggled', this.toggleSm);
}
this.toggleSm();
},
@ -59,19 +68,16 @@ export default {
const selectedAll = target.getSelectedAll().length;
if (selectedAll === 1 || (selectedAll > 1 && componentFirst)) {
this.$cn2.show();
this.$header.hide();
this.$cn2 && this.$cn2.show();
this.$header && this.$header.hide();
} else {
this.$cn2.hide();
this.$header.show();
this.$cn2 && this.$cn2.hide();
this.$header && this.$header.show();
}
},
stop() {
// Hide secondary container if exists
if (this.$cn2) this.$cn2.hide();
// Hide header container if exists
if (this.$header) this.$header.hide();
this.$cn2 && this.$cn2.hide();
this.$header && this.$header.hide();
}
};

12
src/commands/view/OpenTraitManager.js

@ -7,13 +7,15 @@ export default {
this.sender = sender;
const em = editor.getModel();
var config = editor.Config;
var pfx = config.stylePrefix;
var tm = editor.TraitManager;
var panelC;
const config = editor.Config;
const pfx = config.stylePrefix;
const tm = editor.TraitManager;
const confTm = tm.getConfig();
let panelC;
if (confTm.appendTo) return;
if (!this.$cn) {
var confTm = tm.getConfig();
this.$cn = $('<div></div>');
this.$cn2 = $('<div></div>');
this.$cn.append(this.$cn2);

52
src/common/module.js

@ -1,14 +1,18 @@
import { isString } from 'underscore';
import { isString, isElement } from 'underscore';
import { createId } from 'utils/mixins';
export default {
getConfig(name) {
return this.__getConfig(name);
},
__getConfig(name) {
const res = this.config || {};
return name ? res[name] : res;
},
getAll() {
return this.all || [];
getAll(opts = {}) {
return this.all ? (opts.array ? [...this.all.models] : this.all) : [];
},
getAllMap() {
@ -18,7 +22,15 @@ export default {
}, {});
},
__initListen() {
__initConfig(def = {}, conf = {}) {
this.config = {
...def,
...conf
};
this.em = this.config.em;
},
__initListen(opts = {}) {
const { all, em, events } = this;
all &&
em &&
@ -29,6 +41,16 @@ export default {
em.trigger(events.update, p, p.changedAttributes(), c)
)
.on('all', this.__catchAllEvent, this);
// Register collections
this.cls = [all].concat(opts.collections || []);
// Propagate events
(opts.propagate || []).forEach(({ entity, event }) => {
entity.on('all', (ev, model, coll, opts) => {
const options = opts || coll;
const opt = { event: ev, ...options };
[em, all].map(md => md.trigger(event, model, opt));
});
});
},
__remove(model, opts = {}) {
@ -49,8 +71,22 @@ export default {
this.__onAllEvent();
},
__appendTo() {
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());
}
},
__onAllEvent() {},
__logWarn(str) {
this.em.logWarning(`[${this.name}]: ${str}`);
},
_createId(len = 16) {
const all = this.getAll();
const ln = all.length + len;
@ -62,5 +98,13 @@ export default {
} while (allMap[id]);
return id;
},
__destroy() {
this.cls.forEach(coll => {
coll.stopListening();
coll.reset();
});
this.em = 0;
}
};

184
src/css_composer/index.js

@ -1,5 +1,5 @@
/**
* This module contains and manage CSS rules for the template inside the canvas.
* This module manages CSS rules in the canvas.
* You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/css_composer/config/config.js)
* ```js
* const editor = grapesjs.init({
@ -12,22 +12,23 @@
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const cssComposer = editor.CssComposer;
* const css = editor.Css;
* ```
*
* * [load](#load)
* * [store](#store)
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
* * [clear](#clear)
* * [addRules](#addrules)
* * [setRule](#setrule)
* * [getRule](#getrule)
* * [getRules](#getrules)
* * [remove](#remove)
* * [clear](#clear)
*
* [CssRule]: css_rule.html
*
* @module CssComposer
*/
import { isArray, isUndefined } from 'underscore';
import { isArray, isString, isUndefined, each } from 'underscore';
import { isObject } from 'utils/mixins';
import defaults from './config/config';
import CssRule from './model/CssRule';
import CssRules from './model/CssRules';
@ -113,6 +114,7 @@ export default () => {
* The fetched data will be added to the collection
* @param {Object} data Object of data to load
* @return {Object} Loaded rules
* @private
*/
load(data) {
var d = data || '';
@ -144,6 +146,7 @@ export default () => {
* Store data to the selected storage
* @param {Boolean} noStore If true, won't store
* @return {Object} Data to store
* @private
*/
store(noStore) {
if (!c.stm) return;
@ -164,6 +167,7 @@ export default () => {
* @param {Object} props Other props for the rule
* @param {Object} opts Options for the add of new rule
* @return {Model}
* @private
* @example
* var sm = editor.SelectorManager;
* var sel1 = sm.add('myClass1');
@ -189,6 +193,7 @@ export default () => {
opt.state = s;
opt.mediaText = w;
opt.selectors = [];
w && (opt.atRuleType = 'media');
rule = new CssRule(opt, c);
rule.get('selectors').add(selectors, addOpts);
rules.add(rule, addOpts);
@ -198,16 +203,17 @@ export default () => {
/**
* Get the rule
* @param {Array<Selector>} selectors Array of selectors
* @param {String} state Css rule state
* @param {String} width For which device this style is oriented
* @param {String|Array<Selector>} selectors Array of selectors or selector string, eg `.myClass1.myClass2`
* @param {String} state Css rule state, eg. 'hover'
* @param {String} width Media rule value, eg. '(max-width: 992px)'
* @param {Object} ruleProps Other rule props
* @return {Model|null}
* @private
* @example
* var sm = editor.SelectorManager;
* var sel1 = sm.add('myClass1');
* var sel2 = sm.add('myClass2');
* var rule = cssComposer.get([sel1, sel2], 'hover');
* const sm = editor.SelectorManager;
* const sel1 = sm.add('myClass1');
* const sel2 = sm.add('myClass2');
* const rule = cssComposer.get([sel1, sel2], 'hover', '(max-width: 992px)');
* // Update the style
* rule.set('style', {
* width: '300px',
@ -215,42 +221,41 @@ export default () => {
* });
* */
get(selectors, state, width, ruleProps) {
var rule = null;
rules.each(m => {
if (rule) return;
if (m.compare(selectors, state, width, ruleProps)) rule = m;
});
return rule;
let slc = selectors;
if (isString(selectors)) {
const sm = em.get('SelectorManager');
const singleSel = selectors.split(',')[0].trim();
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
);
},
/**
* Get the collection of rules
* @return {Collection}
* */
getAll() {
return rules;
},
/**
* Remove all rules
* @return {this}
*/
clear(opts = {}) {
this.getAll().reset(null, opts);
return this;
},
/**
* Add a raw collection of rule objects
* This method overrides styles, in case, of already defined rule
* @param {Array<Object>} data Array of rule objects, eg . [{selectors: ['class1'], style: {....}}, ..]
* @param {String|Array<Object>} data CSS string or an array of rule objects, eg. [{selectors: ['class1'], style: {....}}, ..]
* @param {Object} opts Options
* @param {Object} props Additional properties to add on rules
* @return {Array<Model>}
* @private
*/
addCollection(data, opts = {}) {
var result = [];
var d = data instanceof Array ? data : [data];
addCollection(data, opts = {}, props = {}) {
const result = [];
if (isString(data)) {
data = em.get('Parser').parseCss(data);
}
const d = data instanceof Array ? data : [data];
for (var i = 0, l = d.length; i < l; i++) {
var rule = d[i] || {};
@ -271,6 +276,8 @@ export default () => {
var updateStyle = !modelExists || !opts.avoidUpdateStyle;
const style = rule.style || {};
isObject(props) && model.set(props, opts);
if (updateStyle) {
let styleUpdate = opts.extend
? { ...model.get('style'), ...style }
@ -285,22 +292,35 @@ export default () => {
},
/**
* Add/update the CSS rule with a generic selector
* @param {string} selectors Selector, eg. '.myclass'
* Add CssRules via CSS string.
* @param {String} css CSS string of rules to add.
* @returns {Array<[CssRule]>} Array of rules
* @example
* const addedRules = css.addRules('.my-cls{ color: red } @media (max-width: 992px) { .my-cls{ color: darkred } }');
* // Check rules
* console.log(addedRules.map(rule => rule.toCSS()));
*/
addRules(css) {
return this.addCollection(css);
},
/**
* Add/update the CssRule.
* @param {String} selectors Selector string, eg. `.myclass`
* @param {Object} style Style properties and values
* @param {Object} [opts={}] Additional properties
* @param {String} [opts.atRuleType=''] At-rule type, eg. 'media'
* @param {String} [opts.atRuleParams=''] At-rule parameters, eg. '(min-width: 500px)'
* @return {CssRule} The new/updated rule
* @param {String} [opts.atRuleType=''] At-rule type, eg. `media`
* @param {String} [opts.atRuleParams=''] At-rule parameters, eg. `(min-width: 500px)`
* @returns {[CssRule]} The new/updated CssRule
* @example
* // Simple class-based rule
* const rule = cc.setRule('.class1.class2', { color: 'red' });
* const rule = css.setRule('.class1.class2', { color: 'red' });
* console.log(rule.toCSS()) // output: .class1.class2 { color: red }
* // With state and other mixed selector
* const rule = cc.setRule('.class1.class2:hover, div#myid', { color: 'red' });
* const rule = css.setRule('.class1.class2:hover, div#myid', { color: 'red' });
* // output: .class1.class2:hover, div#myid { color: red }
* // With media
* const rule = cc.setRule('.class1:hover', { color: 'red' }, {
* const rule = css.setRule('.class1:hover', { color: 'red' }, {
* atRuleType: 'media',
* atRuleParams: '(min-width: 500px)',
* });
@ -324,15 +344,16 @@ export default () => {
},
/**
* Get the CSS rule by a generic selector
* @param {string} selectors Selector, eg. '.myclass:hover'
* @param {String} [opts.atRuleType=''] At-rule type, eg. 'media'
* Get the CssRule.
* @param {String} selectors Selector string, eg. `.myclass:hover`
* @param {Object} [opts={}] Additional properties
* @param {String} [opts.atRuleType=''] At-rule type, eg. `media`
* @param {String} [opts.atRuleParams=''] At-rule parameters, eg. '(min-width: 500px)'
* @return {CssRule}
* @returns {[CssRule]}
* @example
* const rule = cc.getRule('.myclass1:hover');
* const rule2 = cc.getRule('.myclass1:hover, div#myid');
* const rule3 = cc.getRule('.myclass1', {
* const rule = css.getRule('.myclass1:hover');
* const rule2 = css.getRule('.myclass1:hover, div#myid');
* const rule3 = css.getRule('.myclass1', {
* atRuleType: 'media',
* atRuleParams: '(min-width: 500px)',
* });
@ -353,17 +374,20 @@ export default () => {
},
/**
* Find rules, in different states (eg. like `:hover`) and media queries, matching the selector.
* @param {string} selector Selector, eg. '.myclass'
* @returns {Array<CssRule>}
* Get all rules or filtered by a matching selector.
* @param {String} [selector=''] Selector, eg. `.myclass`
* @returns {Array<[CssRule]>}
* @example
* // Common scenario, take all the component specific rules
* // Take all the component specific rules
* const id = someComponent.getId();
* const rules = cc.getRules(`#${id}`);
* const rules = css.getRules(`#${id}`);
* console.log(rules.map(rule => rule.toCSS()))
* // All rules in the project
* console.log(css.getRules())
*/
getRules(selector) {
const rules = this.getAll();
if (!selector) return [...rules.models];
const result = rules.filter(
r => r.getSelectors().getFullString() === selector
);
@ -378,8 +402,8 @@ export default () => {
* @return {CssRule} The new/updated rule
* @private
* @example
* const rule = cc.setIdRule('myid', { color: 'red' });
* const ruleHover = cc.setIdRule('myid', { color: 'blue' }, { state: 'hover' });
* const rule = css.setIdRule('myid', { color: 'red' });
* const ruleHover = css.setIdRule('myid', { color: 'blue' }, { state: 'hover' });
* // This will add current CSS:
* // #myid { color: red }
* // #myid:hover { color: blue }
@ -402,8 +426,8 @@ export default () => {
* @return {CssRule}
* @private
* @example
* const rule = cc.getIdRule('myid');
* const ruleHover = cc.setIdRule('myid', { state: 'hover' });
* const rule = css.getIdRule('myid');
* const ruleHover = css.setIdRule('myid', { state: 'hover' });
*/
getIdRule(name, opts = {}) {
const { mediaText } = opts;
@ -421,8 +445,8 @@ export default () => {
* @return {CssRule} The new/updated rule
* @private
* @example
* const rule = cc.setClassRule('myclass', { color: 'red' });
* const ruleHover = cc.setClassRule('myclass', { color: 'blue' }, { state: 'hover' });
* const rule = css.setClassRule('myclass', { color: 'red' });
* const ruleHover = css.setClassRule('myclass', { color: 'blue' }, { state: 'hover' });
* // This will add current CSS:
* // .myclass { color: red }
* // .myclass:hover { color: blue }
@ -444,8 +468,8 @@ export default () => {
* @return {CssRule}
* @private
* @example
* const rule = cc.getClassRule('myclass');
* const ruleHover = cc.getClassRule('myclass', { state: 'hover' });
* const rule = css.getClassRule('myclass');
* const ruleHover = css.getClassRule('myclass', { state: 'hover' });
*/
getClassRule(name, opts = {}) {
const state = opts.state || '';
@ -454,6 +478,32 @@ export default () => {
return selector && this.get(selector, state, media);
},
/**
* Remove rule, by CssRule or matching selector (eg. the selector will match also at-rules like `@media`)
* @param {String|[CssRule]|Array<[CssRule]>} rule CssRule or matching selector.
* @return {Array<[CssRule]>} Removed rules
* @example
* // Remove by CssRule
* const toRemove = css.getRules('.my-cls');
* css.remove(toRemove);
* // Remove by selector
* css.remove('.my-cls-2');
*/
remove(rule) {
const toRemove = isString(rule) ? this.getRules(rule) : rule;
const result = this.getAll().remove(toRemove);
return isArray(result) ? result : [result];
},
/**
* Remove all rules
* @return {this}
*/
clear(opts = {}) {
this.getAll().reset(null, opts);
return this;
},
getComponentRules(cmp, opts = {}) {
let { state, mediaText, current } = opts;
if (current) {

96
src/css_composer/model/CssRule.js

@ -1,45 +1,36 @@
import { map } from 'underscore';
import { Model } from 'backbone';
import Styleable from 'domain_abstract/model/Styleable';
import { isEmpty, forEach, isString } from 'underscore';
import { isEmpty, forEach, isString, isArray } from 'underscore';
import Selectors from 'selector_manager/model/Selectors';
import { isEmptyObj, hasWin } from 'utils/mixins';
const { CSS } = hasWin() ? window : {};
/**
* @typedef CssRule
* @property {Array<Selector>} selectors Array of selectors
* @property {Object} style Object containing style definitions
* @property {String} [selectorsAdd=''] Additional string css selectors
* @property {String} [atRuleType=''] Type of at-rule, eg. `media`, 'font-face'
* @property {String} [mediaText=''] At-rule value, eg. `(max-width: 1000px)`
* @property {Boolean} [singleAtRule=false] This property is used only on at-rules, like 'page' or 'font-face', where the block containes only style declarations
* @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
*/
export default class CssRule extends Model.extend(Styleable) {
defaults() {
return {
// Css selectors
selectors: [],
// Additional string css selectors
selectorsAdd: '',
// Css properties style
style: {},
// On which device width this rule should be rendered, eg. @media (max-width: 1000px)
mediaText: '',
// State of the rule, eg: hover | pressed | focused
state: '',
// Indicates if the rule is stylable
stylable: true,
// Type of at-rule, eg. 'media', 'font-face', etc.
atRuleType: '',
// This particolar property is used only on at-rules, like 'page' or
// 'font-face', where the block containes only style declarations
singleAtRule: 0,
// If true, sets '!important' on all properties
// You can use an array to specify properties to set important
// Used in view
important: 0,
singleAtRule: false,
important: false,
group: '',
_undo: true
};
}
@ -88,7 +79,7 @@ export default class CssRule extends Model.extend(Styleable) {
}
/**
* Return the at-rule statement when exists, eg. '@media (...)', '@keyframes'
* Returns the at-rule statement when exists, eg. `@media (...)`, `@keyframes`
* @returns {String}
* @example
* const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
@ -203,41 +194,44 @@ export default class CssRule extends Model.extend(Styleable) {
/**
* Compare the actual model with parameters
* @param {Object} selectors Collection of selectors
* @param {String} state Css rule state
* @param {String} width For which device this style is oriented
* @param {Object} selectors Collection of selectors
* @param {String} state Css rule state
* @param {String} width For which device this style is oriented
* @param {Object} ruleProps Other rule props
* @return {Boolean}
* @returns {Boolean}
* @private
*/
compare(selectors, state, width, ruleProps = {}) {
var st = state || '';
var wd = width || '';
var selectorsAdd = ruleProps.selectorsAdd || '';
var atRuleType = ruleProps.atRuleType || '';
if (!(selectors instanceof Array) && !selectors.models)
selectors = [selectors];
var a1 = map(selectors.models || selectors, model => model.getFullName());
var a2 = map(this.get('selectors').models, model => model.getFullName());
var f = false;
if (a1.length !== a2.length) return f;
for (var i = 0; i < a1.length; i++) {
var re = 0;
for (var j = 0; j < a2.length; j++) {
if (a1[i] === a2[j]) re = 1;
}
if (re === 0) return f;
const st = state || '';
const wd = width || '';
const selAdd = ruleProps.selectorsAdd || '';
let atRule = ruleProps.atRuleType || '';
const sel =
!isArray(selectors) && !selectors.models
? [selectors]
: selectors.models || selectors;
// Fix atRuleType in case is not specified with width
if (wd && !atRule) atRule = 'media';
const a1 = sel.map(model => model.getFullName());
const a2 = this.get('selectors').map(model => model.getFullName());
// Check selectors
const a1S = a1.slice().sort();
const a2S = a2.slice().sort();
if (a1.length !== a2.length || !a1S.every((v, i) => v === a2S[i])) {
return false;
}
// Check other properties
if (
this.get('state') !== st ||
this.get('mediaText') !== wd ||
this.get('selectorsAdd') !== selectorsAdd ||
this.get('atRuleType') !== atRuleType
this.get('selectorsAdd') !== selAdd ||
this.get('atRuleType') !== atRule
) {
return f;
return false;
}
return true;

60
src/dom_components/model/Component.js

@ -53,6 +53,8 @@ export const keyUpdateInside = `${keyUpdate}-inside`;
* // -> 'span'
* ```
*
* [Component]: component.html
*
* @typedef Component
* @property {String} [type=''] Component type, eg. `text`, `image`, `video`, etc.
* @property {String} [tagName='div'] HTML tag of the component, eg. `span`. Default: `div`
@ -79,6 +81,7 @@ export const keyUpdateInside = `${keyUpdate}-inside`;
* @property {Boolean} [selectable=true] Allow component to be selected when clicked. Default: `true`
* @property {Boolean} [hoverable=true] Shows a highlight outline when hovering on the element if `true`. Default: `true`
* @property {Boolean} [void=false] This property is used by the HTML exporter as void elements don't have closing tags, eg. `<br/>`, `<hr/>`, etc. Default: `false`
* @property {String} [styles=''] Component related styles, eg. `.my-component-class { color: red }`
* @property {String} [content=''] Content of the component (not escaped) which will be appended before children rendering. Default: `''`
* @property {String} [icon=''] Component's icon, this string will be inserted before the name (in Layers and badge), eg. it can be an HTML string '<i class="fa fa-square-o"></i>'. Default: `''`
* @property {String|Function} [script=''] Component's javascript. More about it [here](/modules/Components-js.html). Default: `''`
@ -176,6 +179,13 @@ export default class Component extends Model.extend(Styleable) {
});
if (!opt.temporary) {
// Add component styles
const cssc = em && em.get('CssComposer');
const { styles, type } = this.attributes;
if (styles && cssc) {
cssc.addCollection(styles, {}, { group: `cmp:${type}` });
}
this.__postAdd();
this.init();
this.__isSymbolOrInst() && this.__initSymb();
@ -189,6 +199,7 @@ export default class Component extends Model.extend(Styleable) {
const comps = this.components();
if (um && !this.__hasUm) {
um.add(comps);
um.add(this.getSelectors());
this.__hasUm = 1;
}
opts.recursive && comps.map(c => c.__postAdd(opts));
@ -199,6 +210,7 @@ export default class Component extends Model.extend(Styleable) {
const um = em && em.get('UndoManager');
if (um) {
um.remove(this.components());
um.remove(this.getSelectors());
delete this.__hasUm;
}
}
@ -260,7 +272,7 @@ export default class Component extends Model.extend(Styleable) {
*/
index() {
const { collection } = this;
return collection && collection.indexOf(this);
return collection ? collection.indexOf(this) : 0;
}
/**
@ -466,8 +478,10 @@ export default class Component extends Model.extend(Styleable) {
* Get the style of the component
* @return {Object}
*/
getStyle(opts = {}) {
getStyle(options = {}, optsAdd = {}) {
const em = this.em;
const prop = isString(options) ? options : '';
const opts = prop ? optsAdd : options;
if (em && em.getConfig('avoidInlineStyle') && !opts.inline) {
const state = em.get('state');
@ -476,11 +490,11 @@ export default class Component extends Model.extend(Styleable) {
this.rule = rule;
if (rule) {
return rule.getStyle();
return rule.getStyle(prop);
}
}
return Styleable.getStyle.call(this);
return Styleable.getStyle.call(this, prop);
}
/**
@ -1008,9 +1022,9 @@ export default class Component extends Model.extend(Styleable) {
/**
* Set new collection if `components` are provided, otherwise the
* current collection is returned
* @param {Component|String} [components] Components to set
* @param {Component|String} [components] Component Definitions or HTML string
* @param {Object} [opts={}] Options, same as in `Component.append()`
* @return {Collection|Array<Component>}
* @returns {Collection|Array<[Component]>}
* @example
* // Set new collection
* component.components('<span></span><div></div>');
@ -1026,10 +1040,35 @@ export default class Component extends Model.extend(Styleable) {
return coll;
} else {
coll.reset(null, opts);
return components && this.append(components, opts);
return components ? this.append(components, opts) : [];
}
}
/**
* If exists, returns the child component at specific index.
* @param {Number} index Index of the component to return
* @returns {[Component]|null}
* @example
* // Return first child
* component.getChildAt(0);
* // Return second child
* component.getChildAt(1);
*/
getChildAt(index) {
return this.components().at(index || 0) || null;
}
/**
* If exists, returns the last child component.
* @returns {[Component]|null}
* @example
* const lastChild = component.getLastChild();
*/
getLastChild() {
const children = this.components();
return children.at(children.length - 1) || null;
}
/**
* Remove all inner components
* * @return {this}
@ -1041,14 +1080,14 @@ export default class Component extends Model.extend(Styleable) {
/**
* Get the parent component, if exists
* @return {Component}
* @return {Component|null}
* @example
* component.parent();
* // -> Component
*/
parent(opts = {}) {
const coll = this.collection || (opts.prev && this.prevColl);
return coll && coll.parent;
return coll ? coll.parent : null;
}
/**
@ -1887,7 +1926,8 @@ Component.prototype.defaults = {
status: '', // State, eg. 'selected'
content: '',
icon: '',
style: '', // Component related style
style: '',
styles: '', // Component related styles
classes: '', // Array of classes
script: '',
'script-props': '',

9
src/dom_components/model/Components.js

@ -82,11 +82,9 @@ export default Backbone.Collection.extend({
sels.remove(rulesRemoved.map(rule => rule.getSelectors().at(0)));
if (!removed.opt.temporary) {
// Deprecate 'style-signature'
// const cm = em.get('Commands');
// const hasSign = removed.get('style-signature');
// const optStyle = { target: removed };
// hasSign && cm.run('core:component-style-clear', optStyle);
em.get('Commands').run('core:component-style-clear', {
target: removed
});
removed.removed();
removed.trigger('removed');
em.trigger('component:remove', removed);
@ -162,6 +160,7 @@ export default Backbone.Collection.extend({
if (isString(models)) {
models = this.parseString(models, opt);
} else if (isArray(models)) {
models = [...models];
models.forEach((item, index) => {
if (isString(item)) {
const nodes = this.parseString(item, opt);

1
src/dom_components/view/ComponentImageView.js

@ -95,6 +95,7 @@ export default ComponentView.extend({
render() {
this.renderAttributes();
if (this.modelOpt.temporary) return this;
this.updateSrc();
const { $el, model } = this;
const cls = $el.attr('class') || '';

5
src/domain_abstract/model/Styleable.js

@ -21,9 +21,10 @@ export default {
* Get style object
* @return {Object}
*/
getStyle() {
getStyle(prop) {
const style = this.get('style') || {};
return { ...style };
const result = { ...style };
return prop && isString(prop) ? result[prop] : result;
},
/**

6
src/domain_abstract/ui/Input.js

@ -58,10 +58,14 @@ export default Backbone.View.extend({
handleChange(e) {
e.stopPropagation();
const value = this.getInputEl().value;
this.model.set({ value }, { fromInput: 1 });
this.__onInputChange(value);
this.elementUpdated();
},
__onInputChange(value) {
this.model.set({ value }, { fromInput: 1 });
},
/**
* Get the input element
* @return {HTMLElement}

37
src/domain_abstract/ui/InputColor.js

@ -3,8 +3,14 @@ import { isUndefined } from 'underscore';
import ColorPicker from 'utils/ColorPicker';
import Input from './Input';
const $ = Backbone.$;
ColorPicker($);
const { $ } = Backbone;
$ && ColorPicker($);
const getColor = color => {
const name = color.getFormat() === 'name' && color.toName();
const cl = color.getAlpha() == 1 ? color.toHexString() : color.toRgbString();
return name || cl.replace(/ /g, '');
};
export default Input.extend({
template() {
@ -33,6 +39,21 @@ export default Input.extend({
this.colorEl.spectrum('destroy');
},
__onInputChange(val) {
let value = val;
const colorEl = this.getColorEl();
// Check the color by using the ColorPicker's parser
if (colorEl) {
colorEl.spectrum('set', value);
const tc = colorEl.spectrum('get');
const color = value && getColor(tc);
color && (value = color);
}
this.model.set({ value }, { fromInput: 1 });
},
/**
* Set value to the model
* @param {string} val
@ -71,16 +92,12 @@ export default Input.extend({
var elToAppend = em && em.config ? em.config.el : '';
var colorPickerConfig =
(em && em.getConfig && em.getConfig('colorPicker')) || {};
const getColor = color => {
let cl =
color.getAlpha() == 1 ? color.toHexString() : color.toRgbString();
return cl.replace(/ /g, '');
};
let changed = 0;
let previousColor;
this.$el.find(`[data-colorp-c]`).append(colorEl);
colorEl.spectrum({
color: model.getValue() || false,
containerClassName: `${ppfx}one-bg ${ppfx}two-color`,
appendTo: elToAppend || 'body',
maxSelectionSize: 8,
@ -123,12 +140,12 @@ export default Input.extend({
}
});
em &&
em.on &&
em.on('component:selected', () => {
if (em && em.on) {
this.listenTo(em, 'component:selected', () => {
changed = 1;
colorEl.spectrum('hide');
});
}
this.colorEl = colorEl;
}

32
src/editor/index.js

@ -36,12 +36,6 @@
* * `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
* ### Blocks
* * `block:add` - New block added
* * `block:remove` - Block removed
* * `block:drag:start` - Started dragging block, model of the block is passed as an argument
* * `block:drag` - Dragging block, the block's model and the drag event are passed as arguments
* * `block:drag:stop` - Dragging of the block is stopped. As agruments for the callback you get, the dropped component model (if dropped successfully) and the model of the block
* ### 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
@ -70,11 +64,6 @@
* * `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
* ### Selectors
* * `selector:add` - New selector is add. Passes the new selector as an argument
* * `selector:remove` - Selector removed. Passes the removed selector as an argument
* * `selector:update` - Selector updated. Passes the updated selector as an argument
* * `selector:state` - State changed. Passes the new state value as an argument
* ### 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
@ -86,6 +75,10 @@
* * `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
* ### Selectors
* Check the [Selectors](/api/selector_manager.html) module.
* ### Blocks
* Check the [Blocks](/api/block_manager.html) module.
* ### Assets
* Check the [Assets](/api/assets.html) module.
* ### Modal
@ -105,13 +98,13 @@
*
* @module Editor
*/
import $ from 'cash-dom';
import defaults from './config/config';
import EditorModel from './model/Editor';
import EditorView from './view/EditorView';
import html from 'utils/html';
export default (config = {}) => {
export default (config = {}, opts = {}) => {
const { $ } = opts;
const c = {
...defaults,
...config
@ -732,6 +725,19 @@ export default (config = {}) => {
return editorView.render().el;
},
/**
* Trigger a callback once the editor is loaded and rendered.
* The callback will be executed immediately if the method is called on the already rendered editor.
* @param {Function} clb Callback to trigger
* @example
* editor.onReady(() => {
* // perform actions
* });
*/
onReady(clb) {
em.get('ready') ? clb(this) : em.on('load', clb);
},
/**
* Print safe HTML by using ES6 tagged template strings.
* @param {Array<String>} literals

11
src/editor/model/Editor.js

@ -7,13 +7,15 @@ import {
keys,
bindAll
} from 'underscore';
import $ from 'cash-dom';
import Backbone from 'backbone';
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 = [
require('utils'),
require('i18n'),
@ -39,7 +41,6 @@ const deps = [
require('block_manager')
];
const { Collection } = Backbone;
let timedInterval;
let updateItr;
@ -123,7 +124,7 @@ export default Backbone.Model.extend({
this.get('readyCanvas') &&
!this.get('ready')
) {
this.set('ready', 1);
this.set('ready', true);
}
},
@ -171,7 +172,7 @@ export default Backbone.Model.extend({
if (sm && sm.canAutoload()) {
this.load(postLoad);
} else {
postLoad();
setTimeout(postLoad);
}
},
@ -262,7 +263,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) {
if (opt.temporary || opt.noCount || opt.avoidStore || !this.get('ready')) {
return;
}

8
src/index.js

@ -1,13 +1,15 @@
import $ from 'cash-dom';
import Editor from './editor';
import { isElement, isFunction } from 'underscore';
import polyfills from 'utils/polyfills';
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,
@ -27,7 +29,7 @@ export default {
plugins,
// Will be replaced on build
version: '<# VERSION #>',
version: __GJS_VERSION__,
/**
* Initialize the editor with passed options
@ -52,7 +54,7 @@ export default {
config = { ...defaultConfig, ...config, grapesjs: this };
config.el =
!headless && (isElement(els) ? els : document.querySelector(els));
const editor = new Editor(config).init();
const editor = new Editor(config, { $ }).init();
const em = editor.getModel();
// Load plugins

27
src/pages/index.js

@ -32,17 +32,19 @@
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
* * [getAllWrappers](#getallwrappers)
* * [getMain](#getmain)
* * [remove](#remove)
* * [select](#select)
* * [getSelected](#getselected)
*
* [Page]: page.html
* [Component]: component.html
*
* @module Pages
*/
import { isString, bindAll } from 'underscore';
import { isString, bindAll, unique, flatten } from 'underscore';
import { createId } from 'utils/mixins';
import { Model } from 'backbone';
import Pages from './model/Pages';
@ -166,7 +168,7 @@ export default () => {
/**
* Remove page
* @param {String|[Page]} page Page or page id
* @returns {[Page]}
* @returns {[Page]} Removed Page
* @example
* const removedPage = pageManager.remove('page-id');
* // or by passing the page
@ -213,7 +215,26 @@ export default () => {
* const arrayOfPages = pageManager.getAll();
*/
getAll() {
return this.pages.models;
return [...this.pages.models];
},
/**
* Get wrapper components (aka body) from all pages and frames.
* @returns {Array<[Component]>}
* @example
* const wrappers = pageManager.getAllWrappers();
* // Get all `image` components from the project
* const allImages = wrappers.map(wrp => wrp.findType('image')).flat();
*/
getAllWrappers() {
const pages = this.getAll();
return unique(
flatten(
pages.map(page =>
page.getAllFrames().map(frame => frame.getComponent())
)
)
);
},
getAllMap() {

28
src/pages/model/Page.js

@ -1,4 +1,4 @@
import { Model } from 'backbone';
import { Model } from 'common';
import { result, forEach } from 'underscore';
import Frames from 'canvas/model/Frames';
@ -36,6 +36,32 @@ export default class Page extends Model {
return this.get('frames');
}
/**
* Get page id
* @returns {String}
*/
getId() {
return this.id;
}
/**
* Get page name
* @returns {String}
*/
getName() {
return this.get('name');
}
/**
* Update page name
* @param {String} name New page name
* @example
* page.setName('New name');
*/
setName(name) {
return this.get({ name });
}
/**
* Get all frames
* @returns {Array<Frame>}

1
src/parser/model/BrowserParserCss.js

@ -197,7 +197,6 @@ export const parseNode = el => {
result.push(model);
}
}
// console.log('LAST PUSH', result[result.length - 1]);
}
return result;

4
src/parser/model/ParserHtml.js

@ -209,8 +209,8 @@ export default config => {
};
} else {
model.components = this.parseNode(node, {
inSvg: model.type === 'svg',
...opts
...opts,
inSvg: opts.inSvg || model.type === 'svg'
});
}
}

8
src/rich_text_editor/index.js

@ -277,11 +277,11 @@ export default () => {
const canvas = config.em.get('Canvas');
const { style } = toolbar;
const pos = canvas.getTargetToElementFixed(lastEl, toolbar, {
event: 'rteToolbarPosUpdate'
event: 'rteToolbarPosUpdate',
left: 0
});
style.top = pos.top + un;
style.left = 0 + un;
style.top = (pos.top || 0) + un;
style.left = (pos.left || 0) + un;
},
/**

9
src/selector_manager/config/config.js

@ -9,7 +9,7 @@ export default {
// Default selectors
selectors: [],
// States
// Default states
states: [{ name: 'hover' }, { name: 'active' }, { name: 'nth-of-type(2n)' }],
// Custom selector name escaping strategy, eg.
@ -43,7 +43,7 @@ export default {
'<svg viewBox="0 0 24 24"><path 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>',
/**
* Custom render function for the Select Manager
* Custom render function for the Selector Manager
* @example
* render: ({ el, labelHead, labelStates, labelInfo, }) => {
* // You can use the default `el` to extend/edit the current
@ -98,5 +98,8 @@ export default {
// With this option enabled, also in the second case, the Component will be passed.
// This method allows to avoid styling classes directly and make, for example, some
// unintended changes below the visible canvas area (when components share same classes)
componentFirst: 0
componentFirst: 0,
// Avoid rendering the default Selector Manager UI.
custom: false
};

433
src/selector_manager/index.js

@ -29,100 +29,156 @@
* })
* ```
*
* 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 selectorManager = editor.SelectorManager;
* // Listen to events
* editor.on('selector:add', (selector) => { ... });
*
* // Use the API
* const sm = editor.Selectors;
* sm.add(...);
* ```
*
* ## Available Events
* * `selector:add` - Selector added. The [Selector] is passed as an argument to the callback.
* * `selector:remove` - Selector removed. The [Selector] is passed as an argument to the callback.
* * `selector:update` - Selector updated. The [Selector] and the object containing changes are passed as arguments to the callback.
* * `selector:state` - States changed. An object containing all the available data about the triggered event is passed as an argument to the callback.
* * `selector` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
*
* ## Methods
* * [getConfig](#getconfig)
* * [add](#add)
* * [addClass](#addclass)
* * [get](#get)
* * [remove](#remove)
* * [getAll](#getall)
* * [setState](#setstate)
* * [getState](#getstate)
* * [getStates](#getstates)
* * [setStates](#setstates)
* * [getSelected](#getselected)
* * [addSelected](#addselected)
* * [removeSelected](#removeselected)
* * [getSelectedTargets](#getselectedtargets)
* * [setComponentFirst](#setcomponentfirst)
* * [getComponentFirst](#getcomponentfirst)
*
* [Selector]: selector.html
* [State]: state.html
* [Component]: component.html
* [CssRule]: css_rule.html
*
* @module SelectorManager
*/
import { isString, isElement, isObject, isArray } from 'underscore';
import { isString, debounce, isObject, isArray } from 'underscore';
import { isComponent, isRule } from 'utils/mixins';
import { Model, Collection } from 'common';
import Module from 'common/module';
import defaults from './config/config';
import Selector from './model/Selector';
import Selectors from './model/Selectors';
import State from './model/State';
import ClassTagsView from './view/ClassTagsView';
const isId = str => isString(str) && str[0] == '#';
const isClass = str => isString(str) && str[0] == '.';
export default config => {
var c = config || {};
var selectors;
export const evAll = 'selector';
export const evPfx = `${evAll}:`;
export const evAdd = `${evPfx}add`;
export const evUpdate = `${evPfx}update`;
export const evRemove = `${evPfx}remove`;
export const evRemoveBefore = `${evRemove}:before`;
export const evCustom = `${evPfx}custom`;
export const evState = `${evPfx}state`;
export default () => {
return {
...Module,
name: 'SelectorManager',
Selector,
Selectors,
/**
* Name of the module
* @type {String}
* @private
*/
name: 'SelectorManager',
events: {
all: evAll,
update: evUpdate,
add: evAdd,
remove: evRemove,
removeBefore: evRemoveBefore,
state: evState,
custom: evCustom
},
/**
* 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
* @return {this}
* @private
*/
init(conf = {}) {
c = {
...defaults,
...conf
};
const em = c.em;
const ppfx = c.pStylePrefix;
this.em = em;
this.__initConfig(defaults, conf);
const config = this.getConfig();
const em = config.em;
const ppfx = config.pStylePrefix;
if (ppfx) {
c.stylePrefix = ppfx + c.stylePrefix;
config.stylePrefix = ppfx + config.stylePrefix;
}
// Global selectors container
selectors = new Selectors(c.selectors);
selectors.on('add', model => em.trigger('selector:add', model));
selectors.on('remove', model => em.trigger('selector:remove', model));
selectors.on('change', model =>
em.trigger(
'selector:update',
model,
model.previousAttributes(),
model.changedAttributes()
)
);
this.all = new Selectors(config.selectors);
this.selected = new Selectors([], { em, config });
this.states = new Collection(config.states, { model: State });
this.model = new Model({ cFirst: config.componentFirst, _undo: true });
this.__initListen({
collections: [this.states, this.selected],
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)
);
const listenTo =
'component:toggled component:update:classes styleManager:update selector:state selector:type';
this.model.listenTo(em, listenTo, () => this.__update());
return this;
},
postRender() {
const elTo = this.getConfig().appendTo;
__update: debounce(function() {
this.__trgCustom();
}),
if (elTo) {
const el = isElement(elTo) ? elTo : document.querySelector(elTo);
el.appendChild(this.render([]));
}
__trgCustom(opts) {
this.em.trigger(this.events.custom, this.__customData(opts));
},
__customData(opts = {}) {
const { container } = opts;
return {
states: this.getStates(),
selected: this.getSelected(),
container
};
},
// postLoad() {
// this.__postLoad();
// const { em, model } = this;
// const um = em.get('UndoManager');
// um && um.add(model);
// um && um.add(this.pages);
// },
postRender() {
this.__appendTo();
this.__trgCustom();
},
select(value, opts = {}) {
@ -142,53 +198,35 @@ export default config => {
return this;
},
/**
* Change the selector state
* @param {String} value State value
* @returns {this}
* @example
* selectorManager.setState('hover');
*/
setState(value) {
this.em.setState(value);
return this;
},
/**
* Get the current selector state
* @returns {String}
*/
getState() {
return this.em.getState();
},
addSelector(name, opt = {}) {
let opts = { ...opt };
addSelector(name, opts = {}, cOpts = {}) {
let props = { ...opts };
if (isObject(name)) {
opts = name;
props = name;
} else {
opts.name = name;
props.name = name;
}
if (isId(opts.name)) {
opts.name = opts.name.substr(1);
opts.type = Selector.TYPE_ID;
} else if (isClass(opts.name)) {
opts.name = opts.name.substr(1);
if (isId(props.name)) {
props.name = props.name.substr(1);
props.type = Selector.TYPE_ID;
} else if (isClass(props.name)) {
props.name = props.name.substr(1);
}
if (opts.label && !opts.name) {
opts.name = this.escapeName(opts.label);
if (props.label && !props.name) {
props.name = this.escapeName(props.label);
}
const cname = opts.name;
const cname = props.name;
const config = this.getConfig();
const all = this.getAll();
const selector = cname
? this.get(cname, opts.type)
: selectors.where(opts)[0];
? this.get(cname, props.type)
: all.where(props)[0];
if (!selector) {
return selectors.add(opts, { config: c });
return all.add(props, { ...cOpts, config });
}
return selector;
@ -202,31 +240,29 @@ export default config => {
name = name.substr(1);
}
return selectors.where({ name, type })[0];
return this.getAll().where({ name, type })[0];
},
/**
* Add a new selector to collection if it's not already exists. Class type is a default one
* @param {String|Array} name Selector/s name
* @param {Object} opts Selector options
* @param {String} [opts.label=''] Label for the selector, if it's not provided the label will be the same as the name
* @param {String} [opts.type=1] Type of the selector. At the moment, only 'class' (1) is available
* @return {Model|Array}
* Add a new selector to the collection if it does not already exist.
* You can pass selectors properties or string identifiers.
* @param {Object|String} props Selector properties or string identifiers, eg. `{ name: 'my-class', label: 'My class' }`, `.my-cls`
* @param {Object} [opts] Selector options
* @return {[Selector]}
* @example
* const selector = selectorManager.add('selectorName');
* const selector = selectorManager.add({ name: 'my-class', label: 'My class' });
* console.log(selector.toString()) // `.my-class`
* // Same as
* const selector = selectorManager.add('selectorName', {
* type: 1,
* label: 'selectorName'
* });
* // Multiple selectors
* const selectors = selectorManager.add(['.class1', '.class2', '#id1']);
* const selector = selectorManager.add('.my-class');
* console.log(selector.toString()) // `.my-class`
* */
add(name, opts = {}) {
if (isArray(name)) {
return name.map(item => this.addSelector(item, opts));
add(props, opts = {}) {
const cOpts = isString(props) ? {} : opts;
// Keep support for arrays but avoid it in docs
if (isArray(props)) {
return props.map(item => this.addSelector(item, opts, cOpts));
} else {
return this.addSelector(name, opts);
return this.addSelector(props, opts, cOpts);
}
},
@ -234,6 +270,7 @@ export default config => {
* Add class selectors
* @param {Array|string} classes Array or string of classes
* @return {Array} Array of added selectors
* @private
* @example
* sm.addClass('class1');
* sm.addClass('class1 class2');
@ -252,16 +289,16 @@ export default config => {
},
/**
* Get the selector by its name
* @param {String|Array} name Selector name
* @param {String} type Selector type
* @return {Model|Array}
* Get the selector by its name/type
* @param {String} name Selector name or string identifier
* @returns {[Selector]|null}
* @example
* const selector = selectorManager.get('selectorName');
* // or get an array
* const selectors = selectorManager.get(['class1', 'class2']);
* const selector = selectorManager.get('.my-class');
* // Get Id
* const selectorId = selectorManager.get('#my-id');
* */
get(name, type) {
// Keep support for arrays but avoid it in docs
if (isArray(name)) {
const result = [];
const selectors = name
@ -272,25 +309,150 @@ export default config => {
);
return result;
} else {
return this.getSelector(name, type);
return this.getSelector(name, type) || null;
}
},
/**
* Remove Selector.
* @param {String|[Selector]} selector Selector instance or Selector string identifier
* @returns {[Selector]} Removed Selector
* @example
* const removed = selectorManager.remove('.myclass');
* // or by passing the Selector
* selectorManager.remove(selectorManager.get('.myclass'));
*/
remove(selector, opts) {
return this.__remove(selector, opts);
},
/**
* Change the selector state
* @param {String} value State value
* @returns {this}
* @example
* selectorManager.setState('hover');
*/
setState(value) {
this.em.setState(value);
return this;
},
/**
* Get the current selector state value
* @returns {String}
*/
getState() {
return this.em.getState();
},
/**
* Get states
* @returns {Array<[State]>}
*/
getStates() {
return [...this.states.models];
},
/**
* Set a new collection of states
* @param {Array<Object>} states Array of new states
* @returns {Array<[State]>}
* @example
* const states = selectorManager.setStates([
* { name: 'hover', label: 'Hover' },
* { name: 'nth-of-type(2n)', label: 'Even/Odd' }
* ]);
*/
setStates(states, opts) {
return this.states.reset(states, opts);
},
/**
* Get commonly selected selectors, based on all selected components.
* @returns {Array<[Selector]>}
* @example
* const selected = selectorManager.getSelected();
* console.log(selected.map(s => s.toString()))
*/
getSelected() {
return this.__getCommon();
},
/**
* Add new selector to all selected components.
* @param {Object|String} props Selector properties or string identifiers, eg. `{ name: 'my-class', label: 'My class' }`, `.my-cls`
* @example
* selectorManager.addSelected('.new-class');
*/
addSelected(props) {
const added = this.add(props);
// TODO: target should be the one from StyleManager
this.em.getSelectedAll().forEach(target => {
target.getSelectors().add(added);
});
// TODO: update selected collection
},
/**
* Remove a common selector from all selected components.
* @param {String|[Selector]} selector Selector instance or Selector string identifier
* @example
* selectorManager.removeSelected('.myclass');
*/
removeSelected(selector) {
this.em.getSelectedAll().forEach(trg => {
!selector.get('protected') &&
trg &&
trg.getSelectors().remove(selector);
});
},
/**
* Get the array of currently selected targets.
* @returns {Array<[Component]|[CssRule]>}
* @example
* const targetsToStyle = selectorManager.getSelectedTargets();
* console.log(targetsToStyle.map(target => target.getSelectorsString()))
*/
getSelectedTargets() {
return this.em.get('StyleManager').getSelected();
},
/**
* Update component-first option.
* If the component-first is enabled, all the style changes will be applied on selected components (ID rules) instead
* of selectors (which would change styles on all components with those classes).
* @param {Boolean} value
*/
setComponentFirst(value) {
this.getConfig().componentFirst = value;
this.model.set({ cFirst: value });
},
/**
* Get the value of component-first option.
* @return {Boolean}
*/
getComponentFirst() {
return this.getConfig().componentFirst;
},
/**
* Get all selectors
* @return {Collection}
* @name getAll
* @function
* @return {Collection<[Selector]>}
* */
getAll() {
return selectors;
},
/**
* Return escaped selector name
* @param {String} name Selector name to escape
* @returns {String} Escaped name
* @private
*/
escapeName(name) {
const { escapeName } = c;
const { escapeName } = this.getConfig();
return escapeName ? escapeName(name) : Selector.escapeName(name);
},
@ -302,23 +464,52 @@ export default config => {
*/
render(selectors) {
const { em, selectorTags } = this;
selectorTags && selectorTags.remove();
const config = this.getConfig();
const el = selectorTags && selectorTags.el;
this.selected.reset(selectors);
this.selectorTags = new ClassTagsView({
collection: new Selectors(selectors || [], { em, config: c }),
config: c
el,
collection: this.selected,
module: this,
config
});
return this.selectorTags.render().el;
},
destroy() {
const { selectorTags } = this;
selectors.reset();
selectors.stopListening();
const { selectorTags, model } = this;
model.stopListening();
this.__destroy();
selectorTags && selectorTags.remove();
[c, selectors].forEach(i => (i = {}));
this.em = {};
this.selectorTags = {};
},
/**
* Get common selectors from the current selection.
* @return {Array<Selector>}
* @private
*/
__getCommon() {
return this.__getCommonSelectors(this.em.getSelectedAll());
},
__getCommonSelectors(components, opts = {}) {
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);
return args
.slice(1)
.reduce((acc, item) => this.__common(acc, item), args[0]);
}
};
};

249
src/selector_manager/model/Selector.js

@ -1,129 +1,172 @@
import Backbone from 'backbone';
import { Model } from 'common';
import { result, forEach, keys } from 'underscore';
const TYPE_CLASS = 1;
const TYPE_ID = 2;
const { Model } = Backbone;
const Selector = Model.extend(
{
idAttribute: 'name',
defaults: {
/**
* @typedef Selector
* @property {String} name Selector name, eg. `my-class`
* @property {String} label Selector label, eg. `My Class`
* @property {Number} [type=1] Type of the selector. 1 (class) | 2 (id)
* @property {Boolean} [active=true] If not active, it's not selectable by the Style Manager.
* @property {Boolean} [private=false] If true, it can't be seen by the Style Manager, but it will be rendered in the canvas and in export code.
* @property {Boolean} [protected=false] If true, it can't be removed from the attacched component.
*/
export default class Selector extends Model {
defaults() {
return {
name: '',
label: '',
// Type of the selector
type: TYPE_CLASS,
// If not active it's not selectable by the style manager (uncheckboxed)
active: true,
// Can't be seen by the style manager, therefore even by the user
// Will be rendered only in export code
private: false,
protected: false,
_undo: true
};
}
// If true, can't be removed from the attacched element
protected: false
},
initialize(props, opts = {}) {
const { config = {} } = opts;
const name = this.get('name');
const label = this.get('label');
initialize(props, opts = {}) {
const { config = {} } = opts;
const name = this.get('name');
const label = this.get('label');
if (!name) {
this.set('name', label);
} else if (!label) {
this.set('label', name);
}
if (!name) {
this.set('name', label);
} else if (!label) {
this.set('label', name);
}
const namePreEsc = this.get('name');
const { escapeName } = config;
const nameEsc = escapeName
? escapeName(namePreEsc)
: Selector.escapeName(namePreEsc);
this.set('name', nameEsc);
this.em = config.em;
}
const namePreEsc = this.get('name');
const { escapeName } = config;
const nameEsc = escapeName
? escapeName(namePreEsc)
: Selector.escapeName(namePreEsc);
this.set('name', nameEsc);
this.em = config.em;
},
isId() {
return this.get('type') === TYPE_ID;
},
isClass() {
return this.get('type') === TYPE_CLASS;
},
/**
* Get full selector name
* @return {string}
*/
getFullName(opts = {}) {
const { escape } = opts;
const name = this.get('name');
let init = '';
switch (this.get('type')) {
case TYPE_CLASS:
init = '.';
break;
case TYPE_ID:
init = '#';
break;
}
isId() {
return this.get('type') === TYPE_ID;
}
return init + (escape ? escape(name) : name);
},
isClass() {
return this.get('type') === TYPE_CLASS;
}
toJSON(opts = {}) {
const { em } = this;
let obj = Model.prototype.toJSON.call(this, [opts]);
const defaults = result(this, 'defaults');
getFullName(opts = {}) {
const { escape } = opts;
const name = this.get('name');
let pfx = '';
switch (this.get('type')) {
case TYPE_CLASS:
pfx = '.';
break;
case TYPE_ID:
pfx = '#';
break;
}
if (em && em.getConfig('avoidDefaults')) {
forEach(defaults, (value, key) => {
if (obj[key] === value) {
delete obj[key];
}
});
return pfx + (escape ? escape(name) : name);
}
if (obj.label === obj.name) {
delete obj.label;
}
/**
* Get selector as a string.
* @returns {String}
* @example
* // Given such selector: { name: 'my-selector', type: 2 }
* console.log(selector.toString());
* // -> `#my-selector`
*/
toString() {
return this.getFullName();
}
const objLen = keys(obj).length;
/**
* Get selector label.
* @returns {String}
* @example
* // Given such selector: { name: 'my-selector', label: 'My selector' }
* console.log(selector.getLabel());
* // -> `My selector`
*/
getLabel() {
return this.get('label');
}
if (objLen === 1 && obj.name) {
obj = obj.name;
}
/**
* Update selector label.
* @param {String} label New label
* @example
* // Given such selector: { name: 'my-selector', label: 'My selector' }
* selector.setLabel('New Label')
* console.log(selector.getLabel());
* // -> `New Label`
*/
setLabel(label) {
return this.set('label', label);
}
/**
* Get selector active state.
* @returns {Boolean}
*/
getActive() {
return this.get('active');
}
/**
* Update selector active state.
* @param {Boolean} value New active state
*/
setActive(value) {
return this.set('active', value);
}
toJSON(opts = {}) {
const { em } = this;
let obj = Model.prototype.toJSON.call(this, [opts]);
const defaults = result(this, 'defaults');
if (objLen === 2 && obj.name && obj.type) {
obj = this.getFullName();
if (em && em.getConfig('avoidDefaults')) {
forEach(defaults, (value, key) => {
if (obj[key] === value) {
delete obj[key];
}
});
if (obj.label === obj.name) {
delete obj.label;
}
return obj;
}
},
{
// All type selectors: https://developer.mozilla.org/it/docs/Web/CSS/CSS_Selectors
// Here I define only what I need
TYPE_CLASS,
TYPE_ID,
/**
* Escape string
* @param {string} name
* @return {string}
* @private
*/
escapeName(name) {
return `${name}`.trim().replace(/([^a-z0-9\w-\:]+)/gi, '-');
const objLen = keys(obj).length;
if (objLen === 1 && obj.name) {
obj = obj.name;
}
if (objLen === 2 && obj.name && obj.type) {
obj = this.getFullName();
}
}
}
);
export default Selector;
return obj;
}
}
Selector.prototype.idAttribute = 'name';
// Type selectors: https://developer.mozilla.org/it/docs/Web/CSS/CSS_Selectors
Selector.TYPE_CLASS = TYPE_CLASS;
Selector.TYPE_ID = TYPE_ID;
/**
* Escape string
* @param {string} name
* @return {string}
* @private
*/
Selector.escapeName = name => {
return `${name}`.trim().replace(/([^a-z0-9\w-\:]+)/gi, '-');
};

18
src/selector_manager/model/Selectors.js

@ -1,24 +1,24 @@
import { filter } from 'underscore';
import Backbone from 'backbone';
import { Collection } from 'common';
import Selector from './Selector';
export default Backbone.Collection.extend({
model: Selector,
modelId: attr => `${attr.name}_${attr.type || Selector.TYPE_CLASS}`,
export default class Selectors extends Collection {
modelId(attr) {
return `${attr.name}_${attr.type || Selector.TYPE_CLASS}`;
}
getStyleable() {
return filter(
this.models,
item => item.get('active') && !item.get('private')
);
},
}
getValid({ noDisabled } = {}) {
return filter(this.models, item => !item.get('private')).filter(item =>
noDisabled ? item.get('active') : 1
);
},
}
getFullString(collection, opts = {}) {
const result = [];
@ -26,4 +26,6 @@ export default Backbone.Collection.extend({
coll.forEach(selector => result.push(selector.getFullName(opts)));
return result.join('').trim();
}
});
}
Selectors.prototype.model = Selector;

33
src/selector_manager/model/State.js

@ -0,0 +1,33 @@
import { Model } from 'common';
/**
* @typedef State
* @property {String} name State name, eg. `hover`, `nth-of-type(2n)`
* @property {String} label State label, eg. `Hover`, `Even/Odd`
*/
export default class State extends Model {
defaults() {
return {
name: '',
label: ''
};
}
/**
* Get state name
* @returns {String}
*/
getName() {
return this.get('name');
}
/**
* Get state label. If label was not provided, the name will be returned.
* @returns {String}
*/
getLabel() {
return this.get('label') || this.getName();
}
}
State.prototype.idAttribute = 'name';

9
src/selector_manager/view/ClassTagView.js

@ -23,9 +23,10 @@ export default Backbone.View.extend({
'focusout [data-tag-name]': 'endEditTag'
},
initialize(o) {
initialize(o = {}) {
const config = o.config || {};
this.config = config;
this.module = o.module;
this.coll = o.coll || null;
this.pfx = config.stylePrefix || '';
this.ppfx = config.pStylePrefix || '';
@ -97,11 +98,7 @@ export default Backbone.View.extend({
* @private
*/
removeTag() {
const { em, model } = this;
const targets = em && em.getSelectedAll();
targets.forEach(sel => {
!model.get('protected') && sel && sel.getSelectors().remove(model);
});
this.module.removeSelected(this.model);
},
/**

107
src/selector_manager/view/ClassTagsView.js

@ -3,15 +3,7 @@ import Backbone from 'backbone';
import ClassTagView from './ClassTagView';
export default Backbone.View.extend({
template({
labelInfo,
labelStates,
labelHead,
iconSync,
iconAdd,
pfx,
ppfx
}) {
template({ labelInfo, labelHead, iconSync, iconAdd, pfx, ppfx }) {
return `
<div id="${pfx}up" class="${pfx}header">
<div id="${pfx}label" class="${pfx}header-label">${labelHead}</div>
@ -19,9 +11,7 @@ export default Backbone.View.extend({
<span id="${pfx}input-c" data-states-c>
<div class="${ppfx}field ${ppfx}select">
<span id="${ppfx}input-holder">
<select id="${pfx}states" data-states>
<option value="">${labelStates}</option>
</select>
<select id="${pfx}states" data-states></select>
</span>
<div class="${ppfx}sel-arrow">
<div class="${ppfx}d-s-arrow"></div>
@ -66,6 +56,8 @@ export default Backbone.View.extend({
const { em } = this.config;
const coll = this.collection;
this.target = this.config.em;
const md = o.module;
this.module = md;
this.em = em;
const toList = 'component:toggled component:update:classes';
const toListCls = 'component:update:classes change:state';
@ -76,6 +68,11 @@ export default Backbone.View.extend({
this.listenTo(coll, 'add', this.addNew);
this.listenTo(coll, 'reset', this.renderClasses);
this.listenTo(coll, 'remove', this.tagRemoved);
this.listenTo(
md.getAll(),
md.events.state,
debounce(() => this.renderStates())
);
this.delegateEvents();
},
@ -124,28 +121,6 @@ export default Backbone.View.extend({
this.updateStateVis();
},
/**
* Create select input with states
* @return {string} String of options
* @private
*/
getStateOptions() {
const { states, em } = this;
let result = [];
states.forEach(state =>
result.push(
`<option value="${state.name}">${em.t(
`selectorManager.states.${state.name}`
) ||
state.label ||
state.name}</option>`
)
);
return result.join('');
},
/**
* Add new model
* @param {Object} model
@ -212,27 +187,17 @@ export default Backbone.View.extend({
this.collection.reset(selectors);
this.updateStateVis(trgs);
this.module.__trgCustom();
return selectors;
},
getCommonSelectors({ targets, opts = {} } = {}) {
const trgs = targets || this.getTargets();
const selectors = trgs
.map(tr => tr.getSelectors && tr.getSelectors().getValid(opts))
.filter(i => i);
return this._commonSelectors(...selectors);
return this.module.__getCommonSelectors(trgs, opts);
},
_commonSelectors(...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);
return args
.slice(1)
.reduce((acc, item) => this._commonSelectors(acc, item), args[0]);
return this.module.__common(...args);
},
checkSync: debounce(function() {
@ -332,23 +297,12 @@ export default Backbone.View.extend({
* @param {Object} e
* @private
*/
addNewTag(label) {
const { em } = this;
if (!label.trim()) return;
if (em) {
const sm = em.get('SelectorManager');
const model = sm.add({ label });
this.getTargets().forEach(target => {
target.getSelectors().add(model);
this.collection.add(model);
this.updateStateVis();
});
}
addNewTag(value) {
const label = value.trim();
if (!label) return;
this.module.addSelected({ label });
this.endNewTag();
// this.updateStateVis(); // Check if required
},
/**
@ -364,7 +318,8 @@ export default Backbone.View.extend({
const rendered = new ClassTagView({
model,
config: this.config,
coll: this.collection
coll: this.collection,
module: this.module
}).render().el;
fragment ? fragment.appendChild(rendered) : classes.append(rendered);
@ -416,6 +371,26 @@ export default Backbone.View.extend({
return this.$statesC;
},
renderStates() {
const { module, em } = this;
const labelStates = em.t('selectorManager.emptyState');
const options = module
.getStates()
.map(state => {
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}`);
this.checkStates();
},
render() {
const { em, pfx, ppfx, config, $el, el } = this;
const { render, iconSync, iconAdd } = config;
@ -423,7 +398,6 @@ export default Backbone.View.extend({
iconSync,
iconAdd,
labelHead: em.t('selectorManager.label'),
labelStates: em.t('selectorManager.emptyState'),
labelInfo: em.t('selectorManager.selected'),
ppfx,
pfx,
@ -437,8 +411,7 @@ export default Backbone.View.extend({
this.$classes = $el.find('#' + pfx + 'tags-c');
this.$btnSyncEl = $el.find('[data-sync-style]');
this.$input.hide();
const statesEl = this.getStates();
statesEl && statesEl.append(this.getStateOptions());
this.renderStates();
this.renderClasses();
$el.attr('class', `${this.className} ${ppfx}one-bg ${ppfx}two-color`);
return this;

2
src/style_manager/config/config.js

@ -22,7 +22,7 @@ export default {
showComputed: true,
// Adds the possibility to clear property value from the target style
clearProperties: 0,
clearProperties: true,
// Properties not to take in account for computed styles
avoidComputed: ['width', 'height']

131
src/style_manager/index.js

@ -30,11 +30,24 @@
* * [getTypes](#gettypes)
* * [createType](#createtype)
*
* [Sector]: sector.html
* [CssRule]: css_rule.html
* [Component]: component.html
*
* @module StyleManager
*/
import { isElement } from 'underscore';
import {
isElement,
isUndefined,
isArray,
isString,
debounce
} from 'underscore';
import Module from 'common/module';
import { Model } from 'common';
import defaults from './config/config';
import Sector from './model/Sector';
import Sectors from './model/Sectors';
import Properties from './model/Properties';
import PropertyFactory from './model/PropertyFactory';
@ -46,6 +59,10 @@ export default () => {
var sectors, SectView;
return {
...Module,
Sector,
PropertyFactory: PropertyFactory(),
/**
@ -71,10 +88,21 @@ export default () => {
init(config) {
c = { ...defaults, ...config };
const ppfx = c.pStylePrefix;
this.em = c.em;
const { em } = c;
this.em = em;
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');
})
);
return this;
},
@ -89,34 +117,35 @@ export default () => {
if (elTo) {
const el = isElement(elTo) ? elTo : document.querySelector(elTo);
if (!el) return this.__logWarn('"appendTo" element not found');
el.appendChild(this.render());
}
},
/**
* Add new sector to the collection. If the sector with the same id already exists,
* that one will be returned
* 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
* @return {[Sector]} Added Sector
* @example
* var sector = styleManager.addSector('mySector',{
* 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
* */
addSector(id, sector, opts = {}) {
addSector(id, sector, options = {}) {
let result = this.getSector(id);
if (!result) {
sector.id = id;
result = sectors.add(sector, opts);
result = sectors.add(sector, options);
}
return result;
@ -125,20 +154,20 @@ export default () => {
/**
* Get sector by id
* @param {string} id Sector id
* @return {Sector|null}
* @return {[Sector]|null}
* @example
* var sector = styleManager.getSector('mySector');
* const sector = styleManager.getSector('mySector');
* */
getSector(id, opts = {}) {
const res = sectors.where({ id })[0];
!res && opts.warn && this._logNoSector(id);
return res;
return res || null;
},
/**
* Remove a sector by id
* @param {string} id Sector id
* @return {Sector} Removed sector
* @return {[Sector]} Removed sector
* @example
* const removed = styleManager.removeSector('mySector');
*/
@ -148,10 +177,16 @@ export default () => {
/**
* Get all sectors
* @return {Sectors} Collection of sectors
* @returns {Collection<[Sector]>} Collection of sectors
* @example
* const sectors = styleManager.getSectors();
* */
getSectors() {
return sectors;
getSectors(opts = {}) {
return sectors && sectors.models
? opts.array
? [...sectors.models]
: sectors
: [];
},
/**
@ -264,7 +299,7 @@ export default () => {
const classes = model.get('classes');
const id = model.getId();
if (em) {
if (em && classes) {
const config = em.getConfig();
const um = em.get('UndoManager');
const cssC = em.get('CssComposer');
@ -308,7 +343,7 @@ export default () => {
const { em } = c;
let result = [];
if (em) {
if (em && target) {
const cssC = em.get('CssComposer');
const cssGen = em.get('CodeManager').getGenerator('css');
const all = cssC
@ -402,14 +437,70 @@ export default () => {
}
},
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 {Styleable} A Component or CSSRule
* @param {[Component]|[CSSRule]|String} target
* @return {Array<[Component]|[CSSRule]>} Array containing selected Components or CSSRules
* @private
*/
setTarget(target, opts) {
return SectView.setTarget(target, opts);
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() {

478
src/style_manager/model/Property.js

@ -1,234 +1,276 @@
import Backbone from 'backbone';
import { isUndefined, isString } from 'underscore';
import { Model } from 'common';
import { isUndefined, isString, result } from 'underscore';
import { capitalize } from 'utils/mixins';
const Property = Backbone.Model.extend(
{
defaults: {
name: '',
property: '',
type: '',
defaults: '',
info: '',
value: '',
icon: '',
functionName: '',
status: '',
visible: true,
fixedValues: ['initial', 'inherit'],
// If true, the property will be forced to be full width
full: 0,
// If true to the value will be added '!important'
important: 0,
// If true, will be hidden by default and will show up only for targets
// which require this property (via `stylable-require`)
// Use case:
// you can add all SVG CSS properties with toRequire as true
// and then require them on SVG Components
toRequire: 0,
// Specifies dependency on other properties of the selected object.
// Property is shown only when all conditions are matched.
//
// example: { display: ['flex', 'block'], position: ['absolute'] };
// in this case the property is only shown when display is
// of value 'flex' or 'block' AND position is 'absolute'
requires: null,
// Specifies dependency on properties of the parent of the selected object.
// Property is shown only when all conditions are matched.
requiresParent: null
},
initialize(props = {}, opts = {}) {
const id = this.get('id') || '';
const name = this.get('name') || '';
!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, ' '));
Property.callInit(this, props, opts);
},
init() {},
/**
* Clear the value
* @return {this}
*/
clearValue(opts = {}) {
this.set({ value: undefined, status: '' }, opts);
return this;
},
/**
* Update value
* @param {any} value
* @param {Boolen} [complete=true] Indicates if it's a final state
* @param {Object} [opts={}] Options
*/
setValue(value, complete = 1, opts = {}) {
const parsed = this.parseValue(value);
const avoidStore = !complete;
!avoidStore &&
this.set({ value: undefined }, { avoidStore, silent: true });
this.set(parsed, { avoidStore, ...opts });
},
/**
* Like `setValue` but, in addition, prevents the update of the input element
* as the changes should come from the input itself.
* This method is useful with the definition of custom properties
* @param {any} value
* @param {Boolen} [complete=true] Indicates if it's a final state
* @param {Object} [opts={}] Options
*/
setValueFromInput(value, complete, opts = {}) {
this.setValue(value, complete, { ...opts, fromInput: 1 });
},
/**
* Parse a raw value, generally fetched from the target, for this property
* @param {string} value Raw value string
* @return {Object}
* @example
* // example with an Input type
* prop.parseValue('translateX(10deg)');
* // -> { value: 10, unit: 'deg', functionName: 'translateX' }
*
*/
parseValue(value, opts = {}) {
const result = { value };
const imp = '!important';
if (isString(value) && value.indexOf(imp) !== -1) {
result.value = value.replace(imp, '').trim();
result.important = 1;
}
export default class Property extends Model {
initialize(props = {}, opts = {}) {
const id = this.get('id') || '';
const name = this.get('name') || '';
!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, ' '));
Property.callInit(this, props, opts);
}
if (!this.get('functionName') && !opts.complete) {
return result;
}
init() {}
const args = [];
let valueStr = `${result.value}`;
let start = valueStr.indexOf('(') + 1;
let end = valueStr.lastIndexOf(')');
const functionName = valueStr.substring(0, start - 1);
if (functionName) result.functionName = functionName;
args.push(start);
// Will try even if the last closing parentheses is not found
if (end >= 0) {
args.push(end);
}
/**
* Get property id.
* @returns {String}
*/
getId() {
return this.get('id');
}
result.value = String.prototype.substring.apply(valueStr, args);
/**
* Get property type.
* @returns {String}
*/
getType() {
return this.get('type');
}
if (opts.numeric) {
const num = parseFloat(result.value);
result.unit = result.value.replace(num, '');
result.value = num;
}
/**
* Get property name (usually is the CSS property name).
* @returns {String}
*/
getName() {
return this.get('property');
}
/**
* Get property label.
* @returns {String}
*/
getLabel() {
return this.get('name');
}
/**
* Get property value.
* @returns {String}
*/
getValue() {
const val = this.get('value');
return isUndefined(val) ? this.getDefaultValue() : val;
}
/**
* Clear the value
* @return {this}
* @private
*/
clearValue(opts = {}) {
this.set({ value: undefined, status: '' }, opts);
return this;
}
/**
* Update value
* @param {any} value
* @param {Boolen} [complete=true] Indicates if it's a final state
* @param {Object} [opts={}] Options
* @private
*/
setValue(value, complete = 1, opts = {}) {
const parsed = this.parseValue(value);
const avoidStore = !complete;
!avoidStore && this.set({ value: undefined }, { avoidStore, silent: true });
this.set(parsed, { avoidStore, ...opts });
}
/**
* Like `setValue` but, in addition, prevents the update of the input element
* as the changes should come from the input itself.
* This method is useful with the definition of custom properties
* @param {any} value
* @param {Boolen} [complete=true] Indicates if it's a final state
* @param {Object} [opts={}] Options
*/
setValueFromInput(value, complete, opts = {}) {
this.setValue(value, complete, { ...opts, fromInput: 1 });
}
/**
* Parse a raw value, generally fetched from the target, for this property
* @param {string} value Raw value string
* @return {Object}
* @private
* @example
* // example with an Input type
* prop.parseValue('translateX(10deg)');
* // -> { value: 10, unit: 'deg', functionName: 'translateX' }
*
*/
parseValue(value, opts = {}) {
const result = { value };
const imp = '!important';
if (isString(value) && value.indexOf(imp) !== -1) {
result.value = value.replace(imp, '').trim();
result.important = 1;
}
if (!this.get('functionName') && !opts.complete) {
return result;
},
/**
* Helper function to safely split a string of values.
* Useful when style values are inside functions
* eg:
* -> input: 'value(1,2,4), 123, value(4,5)' -- default separator: ','
* -> output: ['value(1,2,4)', '123', 'value(4,5)']
* @param {String} values Values to split
* @param {String} [separator] Separator
*/
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');
},
/**
* 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
* @return {string}
* @private
*/
getFullValue(val) {
const fn = this.get('functionName');
const def = this.getDefaultValue();
let value = isUndefined(val) ? this.get('value') : val;
const hasValue = !isUndefined(value) && value !== '';
if (value && def && value === def) {
return def;
}
}
if (fn && hasValue) {
const fnParameter =
fn === 'url' ? `'${value.replace(/'/g, '')}'` : value;
value = `${fn}(${fnParameter})`;
}
const args = [];
let valueStr = `${result.value}`;
let start = valueStr.indexOf('(') + 1;
let end = valueStr.lastIndexOf(')');
const functionName = valueStr.substring(0, start - 1);
if (functionName) result.functionName = functionName;
args.push(start);
// Will try even if the last closing parentheses is not found
if (end >= 0) {
args.push(end);
}
if (hasValue && this.get('important')) {
value = `${value} !important`;
result.value = String.prototype.substring.apply(valueStr, args);
if (opts.numeric) {
const num = parseFloat(result.value);
result.unit = result.value.replace(num, '');
result.value = num;
}
return result;
}
/**
* Helper function to safely split a string of values.
* Useful when style values are inside functions
* eg:
* -> input: 'value(1,2,4), 123, value(4,5)' -- default separator: ','
* -> output: ['value(1,2,4)', '123', 'value(4,5)']
* @param {String} values Values to split
* @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 value || '';
return res.map(i => i.trim());
}
/**
* Get the default value
* @return {string}
* @private
*/
getDefaultValue() {
return this.get('defaults');
}
/**
* 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
* @return {string}
* @private
*/
getFullValue(val) {
const fn = this.get('functionName');
const def = this.getDefaultValue();
let value = isUndefined(val) ? this.get('value') : val;
const hasValue = !isUndefined(value) && value !== '';
if (value && def && value === def) {
return def;
}
},
{
callParentInit(property, ctx, props, opts = {}) {
property.prototype.initialize.apply(ctx, [
props,
{
...opts,
skipInit: 1
}
]);
},
callInit(context, props, opts = {}) {
!opts.skipInit && context.init(props, opts);
if (fn && hasValue) {
const fnParameter = fn === 'url' ? `'${value.replace(/'/g, '')}'` : value;
value = `${fn}(${fnParameter})`;
}
if (hasValue && this.get('important')) {
value = `${value} !important`;
}
return value || '';
}
);
}
Property.callParentInit = function(property, ctx, props, opts = {}) {
property.prototype.initialize.apply(ctx, [
props,
{
...opts,
skipInit: 1
}
]);
};
Property.callInit = function(context, props, opts = {}) {
!opts.skipInit && context.init(props, opts);
};
Property.getDefaults = function() {
return result(this.prototype, 'defaults');
};
Property.prototype.defaults = {
name: '',
property: '',
type: '',
defaults: '',
info: '',
value: '',
icon: '',
functionName: '',
status: '',
visible: true,
fixedValues: ['initial', 'inherit'],
// If true, the property will be forced to be full width
full: 0,
// If true to the value will be added '!important'
important: 0,
// If true, will be hidden by default and will show up only for targets
// which require this property (via `stylable-require`)
// Use case:
// you can add all SVG CSS properties with toRequire as true
// and then require them on SVG Components
toRequire: 0,
// Specifies dependency on other properties of the selected object.
// Property is shown only when all conditions are matched.
//
// example: { display: ['flex', 'block'], position: ['absolute'] };
// in this case the property is only shown when display is
// of value 'flex' or 'block' AND position is 'absolute'
requires: null,
export default Property;
// Specifies dependency on properties of the parent of the selected object.
// Property is shown only when all conditions are matched.
requiresParent: null
};

16
src/style_manager/model/PropertyInteger.js

@ -22,6 +22,22 @@ export default Property.extend({
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');

86
src/style_manager/model/Sector.js

@ -1,17 +1,28 @@
import Backbone from 'backbone';
import { Model } from 'common';
import { extend } from 'underscore';
import Properties from './Properties';
import PropertyFactory from './PropertyFactory';
export default Backbone.Model.extend({
defaults: {
id: '',
name: '',
open: true,
buildProps: '',
extendBuilded: 1,
properties: []
},
/**
* @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
*/
export default class Sector extends Model {
defaults() {
return {
id: '',
name: '',
open: true,
buildProps: '',
extendBuilded: 1,
properties: []
};
}
initialize(opts) {
const o = opts || {};
@ -26,7 +37,56 @@ export default Backbone.Model.extend({
const propsModel = new Properties(props);
propsModel.sector = this;
this.set('properties', propsModel);
},
}
/**
* Get sector id.
* @returns {String}
*/
getId() {
return this.get('id');
}
/**
* Get sector name.
* @returns {String}
*/
getName() {
return this.get('name');
}
/**
* Update sector name.
* @param {String} value New sector name
*/
setName(value) {
return this.set('name', value);
}
/**
* Check if the sector is open
* @returns {Boolean}
*/
isOpen() {
return !!this.get('open');
}
/**
* Update Sector open state
* @param {Boolean} value
*/
setOpen(value) {
return this.set('open', value);
}
/**
* Get sector properties.
* @returns {Array<[Property]>}
*/
getProperties() {
const props = this.get('properties');
return props.models ? [...props.models] : props;
}
/**
* Extend properties
@ -72,7 +132,7 @@ export default Backbone.Model.extend({
}
return ex ? isolated.filter(i => i) : props;
},
}
/**
* Build properties
@ -92,4 +152,4 @@ export default Backbone.Model.extend({
return r;
}
});
}

12
src/style_manager/model/Sectors.js

@ -1,15 +1,15 @@
import Backbone from 'backbone';
import { Collection } from 'common';
import Sector from './Sector';
export default Backbone.Collection.extend({
model: Sector,
export default class Sectors extends Collection {
initialize() {
this.listenTo(this, 'reset', this.onReset);
},
}
onReset(models, opts = {}) {
const prev = opts.previousModels || [];
prev.forEach(sect => sect.get('properties').reset());
}
});
}
Sectors.prototype.model = Sector;

2
src/utils/ColorPicker.js

@ -502,7 +502,7 @@ export default function($, undefined) {
// since the set function will not run (default color is black).
updateUI();
currentPreferredFormat =
opts.preferredFormat || tinycolor(initialColor).format;
opts.preferredFormat || tinycolor(initialColor).getFormat();
addColorToSelectionPalette(initialColor);
} else {

55
src/utils/Droppable.js

@ -38,10 +38,46 @@ export default class Droppable {
methods[method](el, 'dragleave', this.handleDragLeave);
}
__customTglEff(enable) {
const method = enable ? on : off;
const doc = this.el.ownerDocument;
const frameEl = doc.defaultView.frameElement;
this.sortOpts = enable
? {
onStart({ sorter }) {
on(frameEl, 'pointermove', sorter.onMove);
},
onEnd({ sorter }) {
off(frameEl, 'pointermove', sorter.onMove);
},
customTarget({ event }) {
return doc.elementFromPoint(event.clientX, event.clientY);
}
}
: null;
method(frameEl, 'pointerenter', this.handleDragEnter);
method(frameEl, 'pointermove', this.handleDragOver);
method(document, 'pointerup', this.handleDrop);
method(frameEl, 'pointerout', this.handleDragLeave);
// Test with touche devices (seems like frameEl is not capturing pointer events).
// on/off(document, 'pointermove', sorter.onMove); // for the sorter
// enable && this.handleDragEnter({}); // no way to use pointerenter/pointerout
}
startCustom() {
this.__customTglEff(true);
}
endCustom(cancel) {
this.over ? this.endDrop(cancel) : this.__customTglEff(false);
}
endDrop(cancel, ev) {
const { em, dragStop } = this;
this.counter = 0;
dragStop && dragStop(cancel);
this.__customTglEff(false);
em.trigger('canvas:dragend', ev);
}
@ -104,7 +140,8 @@ export default class Droppable {
itemSel: '*',
pfx: 'gjs-',
onEndMove: model => this.handleDragEnd(model, dt),
document: this.el.ownerDocument
document: this.el.ownerDocument,
...(this.sortOpts || {})
});
sorter.setDropContent(content);
sorter.startSort();
@ -154,12 +191,12 @@ export default class Droppable {
this.endDrop(!content, ev);
}
getContentByData(dataTransfer) {
getContentByData(dt) {
const em = this.em;
const types = dataTransfer.types;
const files = dataTransfer.files || [];
const types = dt && dt.types;
const files = (dt && dt.files) || [];
const dragContent = em.get('dragContent');
let content = dataTransfer.getData('text');
let content = dt && dt.getData('text');
if (files.length) {
content = [];
@ -178,9 +215,7 @@ export default class Droppable {
} else if (dragContent) {
content = dragContent;
} else if (indexOf(types, 'text/html') >= 0) {
content = dataTransfer
.getData('text/html')
.replace(/<\/?meta[^>]*>/g, '');
content = dt && dt.getData('text/html').replace(/<\/?meta[^>]*>/g, '');
} else if (indexOf(types, 'text/uri-list') >= 0) {
content = {
type: 'link',
@ -188,7 +223,7 @@ export default class Droppable {
content: content
};
} else if (indexOf(types, 'text/json') >= 0) {
const json = dataTransfer.getData('text/json');
const json = dt && dt.getData('text/json');
json && (content = JSON.parse(json));
} else if (types.length === 1 && types[0] === 'text/plain') {
// Avoid dropping non-selectable and non-editable text nodes inside the editor
@ -196,7 +231,7 @@ export default class Droppable {
}
const result = { content };
em.trigger('canvas:dragdata', dataTransfer, result);
em.trigger('canvas:dragdata', dt, result);
return result;
}

86
src/utils/Sorter.js

@ -48,6 +48,8 @@ export default Backbone.View.extend({
this.freezeClass = o.freezeClass || this.pfx + 'freezed';
this.onStart = o.onStart || '';
this.onEndMove = o.onEndMove || '';
this.customTarget = o.customTarget;
this.onEnd = o.onEnd;
this.direction = o.direction || 'v'; // v (vertical), h (horizontal), a (auto)
this.onMoveClb = o.onMove || '';
this.relative = o.relative || 0;
@ -329,6 +331,7 @@ export default Backbone.View.extend({
on(docs, 'keydown', this.rollback);
onStart &&
onStart({
sorter: this,
target: srcModel,
parent: srcModel && srcModel.parent(),
index: srcModel && srcModel.index()
@ -426,7 +429,7 @@ export default Backbone.View.extend({
* */
onMove(e) {
const ev = e;
const { em, onMoveClb, plh } = this;
const { em, onMoveClb, plh, customTarget } = this;
this.moved = 1;
// Turn placeholder visibile
@ -454,13 +457,15 @@ export default Backbone.View.extend({
//var targetNew = this.getTargetFromEl(e.target);
const sourceModel = this.getSourceModel();
const dims = this.dimsFromTarget(e.target, rX, rY);
const targetEl = customTarget
? customTarget({ sorter: this, event: e })
: e.target;
const dims = this.dimsFromTarget(targetEl, rX, rY);
const target = this.target;
const targetModel = target && this.getTargetModel(target);
this.selectTargetModel(targetModel, sourceModel);
if (!targetModel) plh.style.display = 'none';
if (!target) return;
this.lastDims = dims;
const pos = this.findPosition(dims, rX, rY);
@ -821,16 +826,16 @@ export default Backbone.View.extend({
*/
getDim(el) {
const { em, canvasRelative } = this;
var top, left, height, width;
const canvas = em && em.get('Canvas');
const offsets = canvas ? canvas.getElementOffsets(el) : {};
let top, left, height, width;
if (canvasRelative && em) {
const canvas = em.get('Canvas');
const pos = canvas.getElementPos(el, { noScroll: 1 });
const elOffsets = canvas.getElementOffsets(el);
top = pos.top - elOffsets.marginTop;
left = pos.left - elOffsets.marginLeft;
height = pos.height + elOffsets.marginTop + elOffsets.marginBottom;
width = pos.width + elOffsets.marginLeft + elOffsets.marginRight;
top = pos.top; // - offsets.marginTop;
left = pos.left; // - offsets.marginLeft;
height = pos.height; // + offsets.marginTop + offsets.marginBottom;
width = pos.width; // + offsets.marginLeft + offsets.marginRight;
} else {
var o = this.offset(el);
top = this.relative
@ -843,7 +848,7 @@ export default Backbone.View.extend({
width = el.offsetWidth;
}
return [top, left, height, width];
return { top, left, height, width, offsets };
},
/**
@ -879,7 +884,9 @@ export default Backbone.View.extend({
else if (dir == 'h') dir = false;
else dir = this.isInFlow(el, trg);
dim.push(dir, el, elIndex);
dim.dir = dir;
dim.el = el;
dim.indexEl = elIndex;
dims.push(dim);
});
@ -898,10 +905,10 @@ export default Backbone.View.extend({
var off = this.borderOffset;
var x = rX || 0;
var y = rY || 0;
var t = dim[0];
var l = dim[1];
var h = dim[2];
var w = dim[3];
var t = dim.top;
var l = dim.left;
var h = dim.height;
var w = dim.width;
if (t + off > y || y > t + h - off || l + off > x || x > l + w - off)
result = 1;
@ -928,25 +935,26 @@ export default Backbone.View.extend({
// Each dim is: Top, Left, Height, Width
for (var i = 0, len = dims.length; i < len; i++) {
dim = dims[i];
const { top, left, height, width } = dim;
// Right position of the element. Left + Width
dimRight = dim[1] + dim[3];
dimRight = left + width;
// Bottom position of the element. Top + Height
dimDown = dim[0] + dim[2];
dimDown = top + height;
// X center position of the element. Left + (Width / 2)
xCenter = dim[1] + dim[3] / 2;
xCenter = left + width / 2;
// Y center position of the element. Top + (Height / 2)
yCenter = dim[0] + dim[2] / 2;
yCenter = top + height / 2;
// Skip if over the limits
if (
(xLimit && dim[1] > xLimit) ||
(xLimit && left > xLimit) ||
(yLimit && yCenter >= yLimit) || // >= avoid issue with clearfixes
(leftLimit && dimRight < leftLimit)
)
continue;
result.index = i;
result.indexEl = dim[6];
result.indexEl = dim.indexEl;
// If it's not in flow (like 'float' element)
if (!dim[4]) {
if (!dim.dir) {
if (posY < dimDown) yLimit = dimDown;
//If x lefter than center
if (posX < xCenter) {
@ -972,7 +980,7 @@ export default Backbone.View.extend({
* @param {HTMLElement} phl
* @param {Array<Array>} dims
* @param {Object} pos Position object
* @param {Array<number>} trgDim target dimensions
* @param {Array<number>} trgDim target dimensions ([top, left, height, width])
* */
movePlaceholder(plh, dims, pos, trgDim) {
var marg = 0,
@ -983,7 +991,7 @@ export default Backbone.View.extend({
un = 'px',
margI = 5,
method = pos.method;
var elDim = dims[pos.index];
const elDim = dims[pos.index];
// Placeholder orientation
plh.classList.remove('vertical');
@ -991,29 +999,34 @@ export default Backbone.View.extend({
if (elDim) {
// If it's not in flow (like 'float' element)
if (!elDim[4]) {
const { top, left, height, width } = elDim;
if (!elDim.dir) {
w = 'auto';
h = elDim[2] - marg * 2 + un;
t = elDim[0] + marg;
l = method == 'before' ? elDim[1] - marg : elDim[1] + elDim[3] - marg;
h = height - marg * 2 + un;
t = top + marg;
l = method == 'before' ? left - marg : left + width - marg;
plh.classList.remove('horizontal');
plh.classList.add('vertical');
} else {
w = elDim[3] + un;
w = width + un;
h = 'auto';
t = method == 'before' ? elDim[0] - marg : elDim[0] + elDim[2] - marg;
l = elDim[1];
t = method == 'before' ? top - marg : top + height - marg;
l = left;
}
} else {
// Placeholder inside the component
if (!this.nested) {
plh.style.display = 'none';
return;
}
if (trgDim) {
t = trgDim[0] + margI;
l = trgDim[1] + margI;
w = parseInt(trgDim[3]) - margI * 2 + un;
const offset = trgDim.offsets || {};
const pT = offset.paddingTop || margI;
const pL = offset.paddingLeft || margI;
t = trgDim.top + pT;
l = trgDim.left + pL;
w = parseInt(trgDim.width) - pL * 2 + un;
h = 'auto';
}
}
@ -1035,6 +1048,7 @@ export default Backbone.View.extend({
const docs = this.getDocuments();
const container = this.getContainerEl();
const onEndMove = this.onEndMove;
const onEnd = this.onEnd;
const { target, lastPos } = this;
let srcModel;
off(container, 'mousemove dragover', this.onMove);
@ -1084,6 +1098,8 @@ export default Backbone.View.extend({
? moved.forEach(m => onEndMove(m, this, data))
: onEndMove(null, this, { ...data, cancelled: 1 });
}
isFunction(onEnd) && onEnd({ sorter: this });
},
/**

8
test/setup.js

@ -1,8 +1,6 @@
import 'whatwg-fetch';
import _ from 'underscore';
import Backbone from 'backbone';
import sinon from 'sinon';
import grapesjs from './../src';
const localStorage = {
getItem(key) {
@ -16,9 +14,9 @@ const localStorage = {
}
};
global.Backbone = Backbone;
global._ = _;
global.sinon = sinon;
global.grapesjs = grapesjs;
global.$ = Backbone.$;
global.__GJS_VERSION__ = '';
global.grapesjs = require('./../src').default;
global.$ = global.grapesjs.$;
global.localStorage = localStorage;

67
test/specs/block_manager/index.js

@ -1,24 +1,30 @@
import BlockManager from 'block_manager';
import BlocksView from './view/BlocksView';
import Editor from 'editor';
describe('BlockManager', () => {
describe('Main', () => {
var obj;
var idTest;
var optsTest;
let obj;
let idTest;
let optsTest;
let editor;
beforeEach(() => {
editor = new Editor({
blockManager: {
blocks: []
}
}).init();
idTest = 'h1-block';
optsTest = {
label: 'Heading',
content: '<h1>Test</h1>'
};
obj = new BlockManager().init();
obj.postRender();
obj.render();
obj = editor.Blocks;
});
afterEach(() => {
editor.destroy();
obj = null;
});
@ -35,7 +41,7 @@ describe('BlockManager', () => {
});
test('Add new block', () => {
var model = obj.add(idTest, optsTest);
obj.add(idTest, optsTest);
expect(obj.getAll().length).toEqual(1);
});
@ -64,8 +70,51 @@ describe('BlockManager', () => {
});
test('Render blocks', () => {
obj.postRender();
obj.render();
expect(obj.getContainer()).toBeTruthy();
});
describe('Events', () => {
test('Add triggers proper events', () => {
const eventAdd = jest.fn();
const eventAll = jest.fn();
editor.on(obj.events.add, eventAdd);
editor.on(obj.events.all, eventAll);
const added = obj.add(idTest, optsTest);
expect(eventAdd).toBeCalledTimes(1);
expect(eventAdd).toBeCalledWith(added, expect.anything());
expect(eventAll).toBeCalled();
});
test('Remove triggers proper events', () => {
const eventBfRm = jest.fn();
const eventRm = jest.fn();
const eventAll = jest.fn();
obj.add(idTest, optsTest);
editor.on(obj.events.removeBefore, eventBfRm);
editor.on(obj.events.remove, eventRm);
editor.on(obj.events.all, eventAll);
const removed = obj.remove(idTest);
expect(obj.getAll().length).toBe(0);
expect(eventBfRm).toBeCalledTimes(1);
expect(eventRm).toBeCalledTimes(1);
expect(eventRm).toBeCalledWith(removed, expect.anything());
expect(eventAll).toBeCalled();
});
test('Update triggers proper events', () => {
const eventUp = jest.fn();
const eventAll = jest.fn();
const added = obj.add(idTest, optsTest);
const newProps = { label: 'Heading 2' };
editor.on(obj.events.update, eventUp);
editor.on(obj.events.all, eventAll);
added.set(newProps);
expect(eventUp).toBeCalledTimes(1);
expect(eventUp).toBeCalledWith(added, newProps, expect.anything());
expect(eventAll).toBeCalled();
});
});
});
});

2
test/specs/commands/index.js

@ -1,7 +1,5 @@
import Backbone from 'backbone';
import Commands from 'commands';
import Models from './model/CommandModels';
import CommandAbstract from './view/CommandAbstract';
describe('Commands', () => {
describe('Main', () => {

62
test/specs/css_composer/index.js

@ -1,8 +1,4 @@
import Models from './model/CssModels';
import CssRuleView from './view/CssRuleView';
import CssRulesView from './view/CssRulesView';
import CssComposer from 'css_composer';
import e2e from './e2e/CssComposer';
import utils from './../../test_utils.js';
import Editor from 'editor/model/Editor';
@ -32,6 +28,12 @@ describe('Css Composer', () => {
config.em = editorModel;
};
const getCSS = obj =>
obj
.getAll()
.map(r => r.toCSS())
.join('');
beforeEach(() => {
em = new Editor({});
config = { em };
@ -39,7 +41,7 @@ describe('Css Composer', () => {
});
afterEach(() => {
obj = null;
em.destroy();
});
test('Object exists', () => {
@ -341,5 +343,55 @@ describe('Css Composer', () => {
expect(obj.get(ruleClass.getSelectors())).toBe(ruleClass);
expect(obj.get(ruleId.getSelectors())).toBe(ruleId);
});
describe('Collections', () => {
test('Add a single rule as CSS string', () => {
const cssRule = `.test-rule{color:red;}`;
obj.addCollection(cssRule);
expect(obj.getAll().length).toEqual(1);
expect(getCSS(obj)).toEqual(cssRule);
});
test('Add multiple rules as CSS string', () => {
const cssRules = [
`.test-rule{color:red;}`,
`.test-rule:hover{color:blue;}`,
`@media (max-width: 992px){.test-rule{color:darkred;}}`,
`@media (max-width: 992px){.test-rule:hover{color:darkblue;}}`
];
const cssString = cssRules.join('');
obj.addCollection(cssString);
expect(obj.getAll().length).toEqual(cssRules.length);
expect(getCSS(obj)).toEqual(cssString);
});
test('Able to return the rule inserted as string', () => {
const cssRule = `
.test-rule1 {color:red;}
.test-rule2:hover {
color: blue;
}
@media (max-width: 992px) {
.test-rule3 {
color: darkred;
}
.test-rule4:hover {
color: darkblue;
}
}
`;
const result = obj.addCollection(cssRule);
const [rule1, rule2, rule3, rule4] = result;
expect(result.length).toEqual(4);
expect(obj.getAll().length).toEqual(4);
expect(obj.get('.test-rule1')).toBe(rule1);
expect(obj.get('.test-rule1', 'hover')).toBe(null);
expect(obj.get('.test-rule2', 'hover')).toBe(rule2);
expect(rule3.get('mediaText')).toBe('(max-width: 992px)');
expect(obj.get('.test-rule3', null, '(max-width: 992px)')).toBe(rule3);
expect(obj.get('.test-rule4', 'hover', '(max-width: 992px)')).toBe(
rule4
);
});
});
});
});

1
test/specs/css_composer/model/CssModels.js

@ -49,6 +49,7 @@ describe('CssRule', () => {
var s2 = obj.get('selectors').add({ name: 'test2' });
obj.set('state', 'hover');
obj.set('mediaText', '1000');
obj.set('atRuleType', 'media');
expect(obj.compare([s1, s2])).toEqual(false);
expect(obj.compare([s1, s2], 'hover')).toEqual(false);
expect(obj.compare([s2, s1], 'hover', '1000')).toEqual(true);

1
test/specs/dom_components/model/Component.js

@ -1,5 +1,4 @@
import Backbone from 'backbone';
import DomComponents from 'dom_components';
import Component from 'dom_components/model/Component';
import ComponentImage from 'dom_components/model/ComponentImage';
import ComponentText from 'dom_components/model/ComponentText';

34
test/specs/parser/model/ParserHtml.js

@ -531,4 +531,38 @@ describe('ParserHtml', () => {
];
expect(obj.parse(str).html).toEqual(result);
});
test('SVG is properly parsed', () => {
const str = `<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 12h7v1.5h-7m0-4h7V11h-7m0 3.5h7V16h-7m8-12H3c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2m0 15h-9V6h9"></path>
</svg>
</div>`;
const result = [
{
tagName: 'div',
components: [
{
type: 'svg',
tagName: 'svg',
attributes: {
xmlns: 'http://www.w3.org/2000/svg',
viewBox: '0 0 24 24'
},
components: [
{
tagName: 'path',
attributes: {
d:
'M13 12h7v1.5h-7m0-4h7V11h-7m0 3.5h7V16h-7m8-12H3c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2m0 15h-9V6h9'
},
type: 'svg-in'
}
]
}
]
}
];
expect(obj.parse(str).html).toEqual(result);
});
});

75
test/specs/selector_manager/e2e/ClassManager.js

@ -1,51 +1,31 @@
import Selector from 'selector_manager/model/Selector';
import Selectors from 'selector_manager/model/Selectors';
import ClassTagsView from 'selector_manager/view/ClassTagsView';
describe('E2E tests', () => {
var fixtures;
var components;
var tagEl;
var gjs;
var instClassTagViewer = (gjs, fixtures) => {
var tagEl;
var clm = gjs.editor.get('SelectorManager');
if (clm) {
tagEl = new ClassTagsView({
collection: new Selectors([]),
config: { em: gjs.editor }
}).render();
fixtures.appendChild(tagEl.el);
}
return tagEl;
};
/*
before(function () {
this.$fixtures = $("#fixtures");
this.$fixture = $('<div id="SelectorManager-fixture"></div>');
});
*/
beforeEach(() => {
document.body.innerHTML =
'<div id="fixtures"><div id="SelectorManager-fixture"></div></div>';
fixtures = document.body.firstChild;
gjs = grapesjs.init({
stylePrefix: '',
storageManager: { autoload: 0, type: 0 },
assetManager: {
storageType: 'none'
},
container: '#SelectorManager-fixture'
});
});
let fixtures;
let components;
let tagEl;
let gjs;
let module;
describe('Interaction with Components', () => {
beforeEach(() => {
document.body.innerHTML =
'<div id="fixtures"><div id="SelectorManager-fixture"></div></div>';
fixtures = document.body.firstChild;
gjs = grapesjs.init({
stylePrefix: '',
storageManager: { autoload: 0, type: 0 },
assetManager: { storageType: 'none' },
container: '#SelectorManager-fixture'
});
components = gjs.getComponents();
tagEl = instClassTagViewer(gjs, fixtures);
module = gjs.Selectors;
fixtures.appendChild(module.render());
tagEl = module.selectorTags;
});
afterEach(() => {
gjs.destroy();
});
test('Assign correctly new class to component', () => {
@ -85,15 +65,18 @@ describe('E2E tests', () => {
gjs.select(model);
tagEl.addNewTag('test');
tagEl.addNewTag('test');
expect(model.getSelectors().length).toEqual(1);
const sels = model.getSelectors();
// Component has 1 selector
expect(sels.length).toEqual(1);
expect(sels.at(0).get('name')).toEqual('test');
// One only selector added
expect(module.getAll().length).toEqual(1);
expect(
model
.getSelectors()
module
.getAll()
.at(0)
.get('name')
).toEqual('test');
expect(tagEl.collection.length).toEqual(1);
expect(tagEl.collection.at(0).get('name')).toEqual('test');
});
test('Removing from container removes also from selected component', () => {

101
test/specs/selector_manager/index.js

@ -14,6 +14,7 @@ describe('SelectorManager', () => {
afterEach(() => {
obj = null;
em.destroy();
});
test('Object exists', () => {
@ -38,8 +39,9 @@ describe('SelectorManager', () => {
});
test('Default new selector is a class type', () => {
obj.add('test');
const added = obj.add('test');
expect(obj.get('test').get('type')).toEqual(obj.Selector.TYPE_CLASS);
expect(added.getFullName()).toBe('.test');
});
test('Add a selector as an id', () => {
@ -48,6 +50,7 @@ describe('SelectorManager', () => {
expect(sel.get('name')).toEqual(name);
expect(sel.get('label')).toEqual(name);
expect(obj.get(`#${name}`).get('type')).toEqual(obj.Selector.TYPE_ID);
expect(sel.getFullName()).toBe('#test');
});
test('Check name property', () => {
@ -71,9 +74,10 @@ describe('SelectorManager', () => {
});
test('Adding 2 selectors with the same name is not possible', () => {
obj.add('test');
obj.add('test');
const add1 = obj.add('test');
const add2 = obj.add('test');
expect(obj.getAll().length).toEqual(1);
expect(add1).toBe(add2);
});
test('Add multiple selectors', () => {
@ -136,7 +140,51 @@ describe('SelectorManager', () => {
});
test('Get empty class', () => {
expect(obj.get('test')).toEqual(undefined);
expect(obj.get('test')).toEqual(null);
});
test('Get id selector', () => {
const name = 'my-id';
const type = Selector.TYPE_ID;
const added = obj.add({ name, type });
expect(obj.get(name)).toEqual(null);
expect(obj.get(name, type)).toBe(added);
});
test('Add selectors as string identifiers', () => {
const cls = '.my-class';
const id = '#my-id';
const addedCls = obj.add(cls);
const addedId = obj.add(id);
expect(addedCls.toString()).toBe(cls);
expect(addedId.toString()).toBe(id);
});
test('Get selectors as string identifiers', () => {
const cls = '.my-class';
const id = '#my-id';
const addedCls = obj.add(cls);
const addedId = obj.add(id);
expect(obj.get(cls)).toBe(addedCls);
expect(obj.get(id)).toBe(addedId);
});
test('Remove selectors as string identifiers', () => {
const cls = '.my-class';
const id = '#my-id';
const addedCls = obj.add(cls);
const addedId = obj.add(id);
expect(obj.getAll().length).toBe(2);
expect(obj.remove(cls)).toBe(addedCls);
expect(obj.remove(id)).toBe(addedId);
expect(obj.getAll().length).toBe(0);
});
test('Remove selector as instance', () => {
const addedCls = obj.add('.my-class');
expect(obj.getAll().length).toBe(1);
expect(obj.remove(addedCls)).toBe(addedCls);
expect(obj.getAll().length).toBe(0);
});
test('addClass single class string', () => {
@ -168,5 +216,50 @@ describe('SelectorManager', () => {
expect(obj.getAll().length).toEqual(1);
expect(result.length).toEqual(1);
});
describe('Events', () => {
test('Add triggers proper events', () => {
const itemTest = 'sel';
const eventAdd = jest.fn();
const eventAll = jest.fn();
em.on(obj.events.add, eventAdd);
em.on(obj.events.all, eventAll);
const added = obj.add(itemTest);
expect(eventAdd).toBeCalledTimes(1);
expect(eventAdd).toBeCalledWith(added, expect.anything());
expect(eventAll).toBeCalled();
});
test('Remove triggers proper events', () => {
const itemTest = 'sel';
const eventBfRm = jest.fn();
const eventRm = jest.fn();
const eventAll = jest.fn();
obj.add(itemTest);
em.on(obj.events.removeBefore, eventBfRm);
em.on(obj.events.remove, eventRm);
em.on(obj.events.all, eventAll);
const removed = obj.remove(itemTest);
expect(obj.getAll().length).toBe(0);
expect(eventBfRm).toBeCalledTimes(1);
expect(eventRm).toBeCalledTimes(1);
expect(eventRm).toBeCalledWith(removed, expect.anything());
expect(eventAll).toBeCalled();
});
test('Update triggers proper events', () => {
const itemTest = 'sel';
const eventUp = jest.fn();
const eventAll = jest.fn();
const added = obj.add(itemTest);
const newProps = { label: 'Heading 2' };
em.on(obj.events.update, eventUp);
em.on(obj.events.all, eventAll);
added.set(newProps);
expect(eventUp).toBeCalledTimes(1);
expect(eventUp).toBeCalledWith(added, newProps, expect.anything());
expect(eventAll).toBeCalled();
});
});
});
});

44
test/specs/selector_manager/view/ClassTagsView.js

@ -38,7 +38,8 @@ describe('ClassTagsView', () => {
coll = new Selectors();
view = new ClassTagsView({
config: { em },
collection: coll
collection: coll,
module: em.get('SelectorManager')
});
testContext.targetStub = {
@ -60,6 +61,7 @@ describe('ClassTagsView', () => {
});
afterEach(() => {
target.destroy();
delete view.collection;
});
@ -133,11 +135,14 @@ describe('ClassTagsView', () => {
expect(testContext.$tags.html()).toEqual('');
});
test('Accept new tags', () => {
test('Accept new tags', done => {
em.setSelected(compTest);
view.addNewTag('test');
view.addNewTag('test2');
expect(testContext.$tags.children().length).toEqual(2);
setTimeout(() => {
expect(testContext.$tags.children().length).toEqual(2);
done();
});
});
test('New tag correctly added', () => {
@ -162,32 +167,37 @@ describe('ClassTagsView', () => {
expect(testContext.$statesC.css('display')).toEqual('');
});
test('Update state visibility on new tag', () => {
test('Update state visibility on new tag', done => {
sinon.stub(view, 'updateStateVis');
em.setSelected(compTest);
view.addNewTag('test');
expect(view.updateStateVis.called).toEqual(true);
setTimeout(() => {
expect(view.updateStateVis.called).toEqual(true);
done();
});
});
test('Update state visibility on removing of the tag', () => {
test('Update state visibility on removing of the tag', done => {
em.setSelected(compTest);
view.addNewTag('test');
sinon.stub(view, 'updateStateVis');
coll.remove(coll.at(0));
expect(view.updateStateVis.calledOnce).toEqual(true);
setTimeout(() => {
expect(view.updateStateVis.calledOnce).toEqual(true);
done();
});
});
test('Output correctly state options', () => {
var view = new ClassTagsView({
config: {
em: target,
states: [{ name: 'testName', label: 'testLabel' }]
},
collection: coll
test('Output correctly state options', done => {
target
.get('SelectorManager')
.setStates([{ name: 'testName', label: 'testLabel' }]);
setTimeout(() => {
const res =
'<option value="">- State -</option><option value="testName">testLabel</option>';
expect(view.getStates()[0].innerHTML).toEqual(res);
done();
});
expect(view.getStateOptions()).toEqual(
'<option value="testName">testLabel</option>'
);
});
describe('_commonSelectors', () => {

82
test/specs/style_manager/index.js

@ -1,28 +1,18 @@
import StyleManager from 'style_manager';
import Models from './model/Models';
import SectorView from './view/SectorView';
import SectorsView from './view/SectorsView';
import PropertyView from './view/PropertyView';
import PropertySelectView from './view/PropertySelectView';
import PropertyRadioView from './view/PropertyRadioView';
import PropertyIntegerView from './view/PropertyIntegerView';
import PropertyColorView from './view/PropertyColorView';
import PropertyCompositeView from './view/PropertyCompositeView';
import PropertyStackView from './view/PropertyStackView';
import LayerView from './view/LayerView';
import Editor from 'editor/model/Editor';
describe('StyleManager', () => {
describe('Main', () => {
var obj;
let obj;
let em;
beforeEach(() => {
obj = new StyleManager().init({
sectors: []
});
em = new Editor({});
obj = em.get('StyleManager');
});
afterEach(() => {
obj = null;
em.destroy();
});
test('Object exists', () => {
@ -37,7 +27,7 @@ describe('StyleManager', () => {
obj.addSector('test', {
name: 'Test name'
});
var sector = obj.getSectors().at(0);
var sector = obj.getSectors({ array: true })[0];
expect(obj.getSectors().length).toEqual(1);
expect(sector.get('id')).toEqual('test');
expect(sector.get('name')).toEqual('Test name');
@ -127,39 +117,43 @@ describe('StyleManager', () => {
describe('Init with configuration', () => {
beforeEach(() => {
obj = new StyleManager().init({
sectors: [
{
id: 'dim',
name: 'Dimension',
properties: [
{
name: 'Width',
property: 'width'
},
{
name: 'Height',
property: 'height'
}
]
},
{
id: 'pos',
name: 'position',
properties: [
{
name: 'Width',
property: 'width'
}
]
}
]
em = new Editor({
StyleManager: {
sectors: [
{
id: 'dim',
name: 'Dimension',
properties: [
{
name: 'Width',
property: 'width'
},
{
name: 'Height',
property: 'height'
}
]
},
{
id: 'pos',
name: 'position',
properties: [
{
name: 'Width',
property: 'width'
}
]
}
]
}
});
obj = em.get('StyleManager');
obj.onLoad();
});
afterEach(() => {
obj = null;
em.destroy();
});
test('Sectors added', () => {

1
test/specs/style_manager/view/PropertyColorView.js

@ -1,3 +1,4 @@
import Backbone from 'backbone';
import PropertyColorView from 'style_manager/view/PropertyColorView';
import Property from 'style_manager/model/Property';
import Component from 'dom_components/model/Component';

1
test/specs/style_manager/view/PropertyIntegerView.js

@ -1,3 +1,4 @@
import Backbone from 'backbone';
import PropertyIntegerView from 'style_manager/view/PropertyIntegerView';
import PropertyInteger from 'style_manager/model/PropertyInteger';
import Component from 'dom_components/model/Component';

1
test/specs/style_manager/view/PropertyRadioView.js

@ -1,3 +1,4 @@
import Backbone from 'backbone';
import PropertyRadioView from 'style_manager/view/PropertyRadioView';
import Property from 'style_manager/model/Property';
import Component from 'dom_components/model/Component';

1
test/specs/style_manager/view/PropertySelectView.js

@ -1,3 +1,4 @@
import Backbone from 'backbone';
import PropertySelectView from 'style_manager/view/PropertySelectView';
import Property from 'style_manager/model/PropertyRadio';
import Editor from 'editor/model/Editor';

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

Loading…
Cancel
Save