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/editor', 'Editor'],
['/api/i18n', 'I18n'], ['/api/i18n', 'I18n'],
['/api/canvas', 'Canvas'], ['/api/canvas', 'Canvas'],
['/api/frame', `${subDivider}Frame`],
['/api/assets', 'Asset Manager'], ['/api/assets', 'Asset Manager'],
['/api/asset', `${subDivider}Asset`], ['/api/asset', `${subDivider}Asset`],
['/api/block_manager', 'Block Manager'], ['/api/block_manager', 'Block Manager'],
@ -71,10 +72,13 @@ module.exports = {
['/api/pages', 'Pages'], ['/api/pages', 'Pages'],
['/api/page', `${subDivider}Page`], ['/api/page', `${subDivider}Page`],
['/api/style_manager', 'Style Manager'], ['/api/style_manager', 'Style Manager'],
['/api/sector', `${subDivider}Sector`],
['/api/storage_manager', 'Storage Manager'], ['/api/storage_manager', 'Storage Manager'],
['/api/device_manager', 'Device Manager'], ['/api/device_manager', 'Device Manager'],
['/api/device', `${subDivider}Device`], ['/api/device', `${subDivider}Device`],
['/api/selector_manager', 'Selector Manager'], ['/api/selector_manager', 'Selector Manager'],
['/api/selector', `${subDivider}Selector`],
['/api/state', `${subDivider}State`],
['/api/css_composer', 'CSS Composer'], ['/api/css_composer', 'CSS Composer'],
['/api/css_rule', `${subDivider}CssRule`], ['/api/css_rule', `${subDivider}CssRule`],
['/api/modal_dialog', 'Modal'], ['/api/modal_dialog', 'Modal'],
@ -91,13 +95,14 @@ module.exports = {
title: 'Modules', title: 'Modules',
collapsable: false, collapsable: false,
children: [ children: [
['/modules/Assets', 'Assets'],
['/modules/Blocks', 'Blocks'],
['/modules/Commands', 'Commands'],
['/modules/Components', 'Components'], ['/modules/Components', 'Components'],
['/modules/Components-js', 'Components & JS'], ['/modules/Components-js', 'Components & JS'],
['/modules/I18n', 'I18n'],
['/modules/Traits', 'Traits'], ['/modules/Traits', 'Traits'],
['/modules/Blocks', 'Blocks'],
['/modules/Assets', 'Assets'],
['/modules/Commands', 'Commands'],
['/modules/I18n', 'I18n'],
['/modules/Selectors', 'Selectors'],
['/modules/Style-manager', 'Style Manager'], ['/modules/Style-manager', 'Style Manager'],
['/modules/Storage', 'Storage Manager'], ['/modules/Storage', 'Storage Manager'],
['/modules/Modal', 'Modal'], ['/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'], ['dom_components/model/Component.js', 'component.md'],
['panels/index.js', 'panels.md'], ['panels/index.js', 'panels.md'],
['style_manager/index.js', 'style_manager.md'], ['style_manager/index.js', 'style_manager.md'],
['style_manager/model/Sector.js', 'sector.md'],
['storage_manager/index.js', 'storage_manager.md'], ['storage_manager/index.js', 'storage_manager.md'],
['device_manager/index.js', 'device_manager.md'], ['device_manager/index.js', 'device_manager.md'],
['device_manager/model/Device.js', 'device.md'], ['device_manager/model/Device.js', 'device.md'],
['selector_manager/index.js', 'selector_manager.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/index.js', 'css_composer.md'],
['css_composer/model/CssRule.js', 'css_rule.md'], ['css_composer/model/CssRule.js', 'css_rule.md'],
['modal_dialog/index.js', 'modal_dialog.md'], ['modal_dialog/index.js', 'modal_dialog.md'],
@ -32,6 +35,7 @@ async function generateDocs () {
['keymaps/index.js', 'keymaps.md'], ['keymaps/index.js', 'keymaps.md'],
['undo_manager/index.js', 'undo_manager.md'], ['undo_manager/index.js', 'undo_manager.md'],
['canvas/index.js', 'canvas.md'], ['canvas/index.js', 'canvas.md'],
['canvas/model/Frame.js', 'frame.md'],
['i18n/index.js', 'i18n.md'], ['i18n/index.js', 'i18n.md'],
['pages/index.js', 'pages.md'], ['pages/index.js', 'pages.md'],
['pages/model/Page.js', 'page.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(/<\\\[/g, '<[')
.replace(/<\(\\\[/g, '<([')
.replace(/\| \\\[/g, '| [') .replace(/\| \\\[/g, '| [')
.replace('**Extends Model**', ''); .replace('**Extends Model**', '');
fs.writeFileSync(`${docRoot}/api/${file[1]}`, res); 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:open` - Asset Manager opened.
* `asset:close` - Asset Manager closed. * `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: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:update` - Asset updated. The updated [Asset] and the object containing changes are passed as arguments to the callback.
* `asset:upload:start` - Before the upload is started. * `asset:upload:start` - Before the upload is started.

40
docs/api/block.md

@ -9,14 +9,44 @@
* `label` **[String][1]** Block label, eg. `My block` * `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] * `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. * `media` **[String][1]?** HTML string for the media/icon of the block, eg. `<svg ...`, `<img ...`, etc.
* `category` **[String][1]?** Block category * `category` **[String][1]?** Block category, eg. `Basic blocks`
* `activate` **[Boolean][4]?** If true, triggers an the `active` event on dropped component * `activate` **[Boolean][4]?** If true, triggers the `active` event on the dropped component.
* `select` **[Boolean][4]?** If true, the dropped component will be selected * `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 * `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 * `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'))` * `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 * `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 [1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object [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 [4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function [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 ```js
// Listen to events
editor.on('block:add', (block) => { ... });
// Use the API
const blockManager = editor.BlockManager; 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] * [add][2]
* [get][3] * [get][3]
* [getAll][4] * [getAll][4]
@ -30,33 +47,23 @@ const blockManager = editor.BlockManager;
[Block]: block.html [Block]: block.html
[Component]: component.html
## getConfig ## getConfig
Get configuration object Get configuration object
Returns **[Object][11]** Returns **[Object][11]**
## onLoad
Load default blocks if the collection is empty
## add ## add
Add new block to the collection. Add new block.
### Parameters ### Parameters
* `id` **[string][12]** Block id * `id` **[String][12]** Block ID
* `opts` **[Object][11]** Options * `props` **[Block]** Block properties
* `opts` (optional, default `{}`)
* `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 `{}`)
### Examples ### Examples
@ -75,11 +82,11 @@ Returns **[Block]** Added block
## get ## get
Return the block by id Get the block by id.
### Parameters ### Parameters
* `id` **[string][12]** Block id * `id` **[String][12]** Block id
### Examples ### Examples
@ -93,7 +100,7 @@ Returns **[Block]**
## getAll ## getAll
Return all blocks Return all blocks.
### Examples ### Examples
@ -103,28 +110,30 @@ console.log(JSON.stringify(blocks));
// [{label: 'Heading', content: '<h1>Put your ...'}, ...] // [{label: 'Heading', content: '<h1>Put your ...'}, ...]
``` ```
Returns **Collection** Returns **Collection<[Block]>**
## getAllVisible ## getAllVisible
Return the visible collection, which containes blocks actually rendered Return the visible collection, which containes blocks actually rendered
Returns **Collection** Returns **Collection<[Block]>**
## remove ## remove
Remove a block by id Remove block.
### Parameters ### Parameters
* `id` **[string][12]** Block id * `block` **([String][12] | [Block])** Block or block ID
* `opts` (optional, default `{}`)
### Examples ### Examples
```javascript ```javascript
// Id of the block which need to be removed const removed = blockManager.remove('BLOCK_ID');
const id = 'button'; // or by passing the Block
blockManager.remove(id); const block = blockManager.get('BLOCK_ID');
blockManager.remove(block);
``` ```
Returns **[Block]** Removed block Returns **[Block]** Removed block

4
docs/api/canvas.md

@ -34,6 +34,8 @@ const canvas = editor.Canvas;
[Component]: component.html [Component]: component.html
[Frame]: frame.html
## getConfig ## getConfig
Get the configuration object 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 [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' // -> 'span'
``` ```
[Component]: component.html
### Properties ### Properties
* `type` **[String][1]?** Component type, eg. `text`, `image`, `video`, etc. * `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` * `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` * `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` * `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: `''` * `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: `''` * `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: `''` * `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 #### Parameters
* `opts` (optional, default `{}`) * `options` (optional, default `{}`)
* `optsAdd` (optional, default `{}`)
Returns **[Object][2]** Returns **[Object][2]**
@ -407,7 +411,7 @@ current collection is returned
#### Parameters #### 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 `{}`) * `opts` **[Object][2]** Options, same as in `Component.append()` (optional, default `{}`)
#### Examples #### Examples
@ -421,7 +425,38 @@ console.log(collection.length);
// -> 2 // -> 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 ### empty
@ -448,7 +483,7 @@ component.parent();
// -> Component // -> Component
``` ```
Returns **[Component][9]** Returns **([Component][9] | null)**
### getTrait ### getTrait

200
docs/api/css_composer.md

@ -2,7 +2,7 @@
## CssComposer ## 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] You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object][1]
```js ```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 Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
```js ```js
const cssComposer = editor.CssComposer; const css = editor.Css;
``` ```
* [load][2] * [addRules][2]
* [store][3] * [setRule][3]
* [add][4] * [getRule][4]
* [get][5] * [getRules][5]
* [getAll][6] * [remove][6]
* [clear][7] * [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 ## addRules
autonomously from the storage manager.
The fetched data will be added to the collection
### Parameters Add CssRules via CSS string.
* `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
### Parameters ### Parameters
* `selectors` **[Array][12]\<Selector>** Array of selectors * `css` **[String][8]** CSS string of rules to add.
* `state` **[String][13]** Css rule state
* `width` **[String][13]** For which device this style is oriented
* `ruleProps` **[Object][10]** Other rule props
### Examples ### Examples
```javascript ```javascript
var sm = editor.SelectorManager; const addedRules = css.addRules('.my-cls{ color: red } @media (max-width: 992px) { .my-cls{ color: darkred } }');
var sel1 = sm.add('myClass1'); // Check rules
var sel2 = sm.add('myClass2'); console.log(addedRules.map(rule => rule.toCSS()));
var rule = cssComposer.get([sel1, sel2], 'hover');
// Update the style
rule.set('style', {
width: '300px',
color: '#000',
});
``` ```
Returns **(Model | null)** Returns **[Array][9]<[CssRule]>** Array of rules
## getAll
Get the collection of rules
Returns **Collection**
## clear
Remove all rules
### Parameters
* `opts` (optional, default `{}`)
Returns **this**
## setRule ## setRule
Add/update the CSS rule with a generic selector Add/update the CssRule.
### Parameters ### Parameters
* `selectors` **[string][13]** Selector, eg. '.myclass' * `selectors` **[String][8]** Selector string, eg. `.myclass`
* `style` **[Object][10]** Style properties and values * `style` **[Object][10]** Style properties and values
* `opts` **[Object][10]** Additional properties (optional, default `{}`) * `opts` **[Object][10]** Additional properties (optional, default `{}`)
* `opts.atRuleType` **[String][13]** At-rule type, eg. 'media' (optional, default `''`) * `opts.atRuleType` **[String][8]** At-rule type, eg. `media` (optional, default `''`)
* `opts.atRuleParams` **[String][13]** At-rule parameters, eg. '(min-width: 500px)' (optional, default `''`) * `opts.atRuleParams` **[String][8]** At-rule parameters, eg. `(min-width: 500px)` (optional, default `''`)
### Examples ### Examples
```javascript ```javascript
// Simple class-based rule // 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 } console.log(rule.toCSS()) // output: .class1.class2 { color: red }
// With state and other mixed selector // 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 } // output: .class1.class2:hover, div#myid { color: red }
// With media // With media
const rule = cc.setRule('.class1:hover', { color: 'red' }, { const rule = css.setRule('.class1:hover', { color: 'red' }, {
atRuleType: 'media', atRuleType: 'media',
atRuleParams: '(min-width: 500px)', atRuleParams: '(min-width: 500px)',
}); });
// output: @media (min-width: 500px) { .class1:hover { color: red } } // output: @media (min-width: 500px) { .class1:hover { color: red } }
``` ```
Returns **CssRule** The new/updated rule Returns **[CssRule]** The new/updated CssRule
## getRule ## getRule
Get the CSS rule by a generic selector Get the CssRule.
### Parameters ### Parameters
* `selectors` **[string][13]** Selector, eg. '.myclass:hover' * `selectors` **[String][8]** Selector string, eg. `.myclass:hover`
* `opts` (optional, default `{}`) * `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 ### Examples
```javascript ```javascript
const rule = cc.getRule('.myclass1:hover'); const rule = css.getRule('.myclass1:hover');
const rule2 = cc.getRule('.myclass1:hover, div#myid'); const rule2 = css.getRule('.myclass1:hover, div#myid');
const rule3 = cc.getRule('.myclass1', { const rule3 = css.getRule('.myclass1', {
atRuleType: 'media', atRuleType: 'media',
atRuleParams: '(min-width: 500px)', atRuleParams: '(min-width: 500px)',
}); });
``` ```
Returns **CssRule** Returns **[CssRule]**
## getRules ## 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 ### Parameters
* `selector` **[string][13]** Selector, eg. '.myclass' * `selector` **[String][8]** Selector, eg. `.myclass` (optional, default `''`)
### Examples ### Examples
```javascript ```javascript
// Common scenario, take all the component specific rules // Take all the component specific rules
const id = someComponent.getId(); const id = someComponent.getId();
const rules = cc.getRules(`#${id}`); const rules = css.getRules(`#${id}`);
console.log(rules.map(rule => rule.toCSS())) 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. --> <!-- 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 ```javascript
const cssRule = editor.Css.setRule('.class1', { color: 'red' }, { 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)" cssRule.getAtRule(); // "@media (min-width: 500px)"
``` ```
Returns **[String][1]** Returns **[String][3]**
## selectorsToString ### selectorsToString
Return selectors of the rule as a string Return selectors of the rule as a string
### Parameters #### Parameters
* `opts` **[Object][2]?** Options (optional, default `{}`) * `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 ```javascript
const cssRule = editor.Css.setRule('.class1:hover', { color: 'red' }); const cssRule = editor.Css.setRule('.class1:hover', { color: 'red' });
@ -34,17 +50,17 @@ cssRule.selectorsToString(); // ".class1:hover"
cssRule.selectorsToString({ skipState: true }); // ".class1" cssRule.selectorsToString({ skipState: true }); // ".class1"
``` ```
Returns **[String][1]** Returns **[String][3]**
## getDeclaration ### getDeclaration
Get declaration block (without the at-rule statement) Get declaration block (without the at-rule statement)
### Parameters #### Parameters
* `opts` **[Object][2]** Options (same as in `selectorsToString`) (optional, default `{}`) * `opts` **[Object][2]** Options (same as in `selectorsToString`) (optional, default `{}`)
### Examples #### Examples
```javascript ```javascript
const cssRule = editor.Css.setRule('.class1', { color: 'red' }, { 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;}" cssRule.getDeclaration() // ".class1{color:red;}"
``` ```
Returns **[String][1]** Returns **[String][3]**
## toCSS ### toCSS
Return the CSS string of the rule Return the CSS string of the rule
### Parameters #### Parameters
* `opts` **[Object][2]** Options (same as in `getDeclaration`) (optional, default `{}`) * `opts` **[Object][2]** Options (same as in `getDeclaration`) (optional, default `{}`)
### Examples #### Examples
```javascript ```javascript
const cssRule = editor.Css.setRule('.class1', { color: 'red' }, { 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;}}" 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 [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` - 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 * `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 ### Keymaps
* `keymap:add` - New keymap added. The new keyamp object is passed as an argument * `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 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
* `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument * `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 * `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 * `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 ### Assets
Check the [Assets][2] module. Check the [Assets][4] module.
### Modal ### Modal
Check the [Modal][3] module. Check the [Modal][5] module.
### Devices ### Devices
Check the [Devices][4] module. Check the [Devices][6] module.
### Parser ### Parser
Check the [Parser][5] module. Check the [Parser][7] module.
### Pages ### Pages
Check the [Pages][6] module. Check the [Pages][8] module.
### General ### General
@ -143,7 +136,7 @@ Returns configuration object
### Parameters ### Parameters
* `prop` **[string][7]?** Property name * `prop` **[string][9]?** Property name
Returns **any** Returns the configuration object or Returns **any** Returns the configuration object or
the value of the specified property the value of the specified property
@ -154,12 +147,12 @@ Returns HTML built inside canvas
### Parameters ### Parameters
* `opts` **[Object][8]** Options (optional, default `{}`) * `opts` **[Object][10]** Options (optional, default `{}`)
* `opts.component` **Component?** Return the HTML of a specific Component * `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 ## getCss
@ -167,13 +160,13 @@ Returns CSS built inside canvas
### Parameters ### Parameters
* `opts` **[Object][8]** Options (optional, default `{}`) * `opts` **[Object][10]** Options (optional, default `{}`)
* `opts.component` **Component?** Return the CSS of a specific Component * `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.json` **[Boolean][11]** 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.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 ## getJs
@ -181,11 +174,11 @@ Returns JS of all components
### Parameters ### Parameters
* `opts` **[Object][8]** Options (optional, default `{}`) * `opts` **[Object][10]** Options (optional, default `{}`)
* `opts.component` **Component?** Get the JS of a specific component * `opts.component` **Component?** Get the JS of a specific component
Returns **[String][7]** JS string Returns **[String][9]** JS string
## getComponents ## getComponents
@ -205,8 +198,8 @@ Set components inside editor's canvas. This method overrides actual components
### Parameters ### Parameters
* `components` **([Array][10]<[Object][8]> | [Object][8] | [string][7])** HTML string or components model * `components` **([Array][12]<[Object][10]> | [Object][10] | [string][9])** HTML string or components model
* `opt` **[Object][8]** the options object to be used by the \[setComponents][em#setComponents][11] method (optional, default `{}`) * `opt` **[Object][10]** the options object to be used by the \[setComponents][em#setComponents][13] method (optional, default `{}`)
### Examples ### Examples
@ -228,10 +221,10 @@ Add components
### Parameters ### Parameters
* `components` **([Array][10]<[Object][8]> | [Object][8] | [string][7])** HTML string or components model * `components` **([Array][12]<[Object][10]> | [Object][10] | [string][9])** HTML string or components model
* `opts` **[Object][8]** Options * `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 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`) 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 ## getStyle
Returns style in JSON format object Returns style in JSON format object
Returns **[Object][8]** Returns **[Object][10]**
## setStyle ## setStyle
@ -261,7 +254,7 @@ Set style inside editor's canvas. This method overrides actual style
### Parameters ### 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 `{}`) * `opt` (optional, default `{}`)
### Examples ### Examples
@ -283,7 +276,7 @@ Add styles to the editor
### Parameters ### 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 `{}`) * `opts` (optional, default `{}`)
### Examples ### Examples
@ -292,7 +285,7 @@ Add styles to the editor
editor.addStyle('.cls{color: red}'); editor.addStyle('.cls{color: red}');
``` ```
Returns **[Array][10]\<CssRule>** Array of created CssRule instances Returns **[Array][12]\<CssRule>** Array of created CssRule instances
## getSelected ## getSelected
@ -304,7 +297,7 @@ Returns **Model**
Returns an array of all selected components Returns an array of all selected components
Returns **[Array][10]** Returns **[Array][12]**
## getSelectedToStyle ## getSelectedToStyle
@ -322,10 +315,10 @@ Select a component
### Parameters ### Parameters
* `el` **(Component | [HTMLElement][12])** Component to select * `el` **(Component | [HTMLElement][14])** Component to select
* `opts` **[Object][8]?** Options * `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 ### Examples
@ -344,7 +337,7 @@ Add component to selection
### Parameters ### Parameters
* `el` **(Component | [HTMLElement][12] | [Array][10])** Component to select * `el` **(Component | [HTMLElement][14] | [Array][12])** Component to select
### Examples ### Examples
@ -360,7 +353,7 @@ Remove component from selection
### Parameters ### Parameters
* `el` **(Component | [HTMLElement][12] | [Array][10])** Component to select * `el` **(Component | [HTMLElement][14] | [Array][12])** Component to select
### Examples ### Examples
@ -376,7 +369,7 @@ Toggle component selection
### Parameters ### Parameters
* `el` **(Component | [HTMLElement][12] | [Array][10])** Component to select * `el` **(Component | [HTMLElement][14] | [Array][12])** Component to select
### Examples ### Examples
@ -408,7 +401,7 @@ change the canvas to the proper width
### Parameters ### Parameters
* `name` **[string][7]** Name of the device * `name` **[string][9]** Name of the device
### Examples ### Examples
@ -430,7 +423,7 @@ console.log(device);
// 'Tablet' // 'Tablet'
``` ```
Returns **[string][7]** Device name Returns **[string][9]** Device name
## runCommand ## runCommand
@ -438,8 +431,8 @@ Execute command
### Parameters ### Parameters
* `id` **[string][7]** Command ID * `id` **[string][9]** Command ID
* `options` **[Object][8]** Custom options (optional, default `{}`) * `options` **[Object][10]** Custom options (optional, default `{}`)
### Examples ### Examples
@ -455,8 +448,8 @@ Stop the command if stop method was provided
### Parameters ### Parameters
* `id` **[string][7]** Command ID * `id` **[string][9]** Command ID
* `options` **[Object][8]** Custom options (optional, default `{}`) * `options` **[Object][10]** Custom options (optional, default `{}`)
### Examples ### Examples
@ -472,9 +465,9 @@ Store data to the current storage
### Parameters ### Parameters
* `clb` **[Function][13]** Callback function * `clb` **[Function][15]** Callback function
Returns **[Object][8]** Stored data Returns **[Object][10]** Stored data
## storeData ## storeData
@ -487,7 +480,7 @@ console.log(editor.storeData());
// { pages: [...], styles: [...], ... } // { pages: [...], styles: [...], ... }
``` ```
Returns **[Object][8]** Returns **[Object][10]**
## load ## load
@ -495,9 +488,9 @@ Load data from the current storage
### Parameters ### Parameters
* `clb` **[Function][13]** Callback function * `clb` **[Function][15]** Callback function
Returns **[Object][8]** Stored data Returns **[Object][10]** Stored data
## loadData ## loadData
@ -505,7 +498,7 @@ Load data from the JSON data object
### Parameters ### Parameters
* `data` **[Object][8]** Data to load * `data` **[Object][10]** Data to load
### Examples ### Examples
@ -513,21 +506,21 @@ Load data from the JSON data object
editor.loadData({ pages: [...], styles: [...], ... }) editor.loadData({ pages: [...], styles: [...], ... })
``` ```
Returns **[Object][8]** Loaded object Returns **[Object][10]** Loaded object
## getContainer ## getContainer
Returns container element. The one which was indicated as 'container' Returns container element. The one which was indicated as 'container'
on init method on init method
Returns **[HTMLElement][12]** Returns **[HTMLElement][14]**
## getDirtyCount ## getDirtyCount
Return the count of changes made to the content and not yet stored. Return the count of changes made to the content and not yet stored.
This count resets at any `store()` This count resets at any `store()`
Returns **[number][14]** Returns **[number][16]**
## refresh ## refresh
@ -540,9 +533,9 @@ refresh you'll get misleading position of tools
### Parameters ### Parameters
* `opts` * `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 ## setCustomRte
@ -550,7 +543,7 @@ Replace the built-in Rich Text Editor with a custom one.
### Parameters ### Parameters
* `obj` **[Object][8]** Custom RTE Interface * `obj` **[Object][10]** Custom RTE Interface
### Examples ### Examples
@ -590,7 +583,7 @@ custom parser, pass `null` as the argument
### Parameters ### Parameters
* `parser` **([Function][13] | null)** Parser function * `parser` **([Function][15] | null)** Parser function
### Examples ### Examples
@ -612,11 +605,11 @@ Returns **this**
## setDragMode ## setDragMode
Change the global drag mode of components. 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 ### Parameters
* `value` **[String][7]** Drag mode, options: 'absolute' | 'translate' * `value` **[String][9]** Drag mode, options: 'absolute' | 'translate'
Returns **this** Returns **this**
@ -627,10 +620,10 @@ Trigger event log message
### Parameters ### Parameters
* `msg` **any** Message to log * `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.ns` **[String][9]** 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.level` **[String][9]** Level of the log, `debug`, `info`, `warning`, `error` (optional, default `'debug'`)
### Examples ### Examples
@ -652,11 +645,11 @@ Translate label
### Parameters ### Parameters
* `args` **...any** * `args` **...any**
* `key` **[String][7]** Label to translate * `key` **[String][9]** Label to translate
* `opts` **[Object][8]?** Options for the translation * `opts` **[Object][10]?** Options for the translation
* `opts.params` **[Object][8]?** Params for the translation * `opts.params` **[Object][10]?** Params for the translation
* `opts.noWarn` **[Boolean][9]?** Avoid warnings in case of missing resources * `opts.noWarn` **[Boolean][11]?** Avoid warnings in case of missing resources
### Examples ### Examples
@ -668,7 +661,7 @@ editor.t('msg2', { params: { test: 'hello' } });
editor.t('msg2', { params: { test: 'hello' }, l: 'it' }); editor.t('msg2', { params: { test: 'hello' }, l: 'it' });
``` ```
Returns **[String][7]** Returns **[String][9]**
## on ## on
@ -676,8 +669,8 @@ Attach event
### Parameters ### Parameters
* `event` **[string][7]** Event name * `event` **[string][9]** Event name
* `callback` **[Function][13]** Callback function * `callback` **[Function][15]** Callback function
Returns **this** Returns **this**
@ -687,8 +680,8 @@ Attach event and detach it after the first run
### Parameters ### Parameters
* `event` **[string][7]** Event name * `event` **[string][9]** Event name
* `callback` **[Function][13]** Callback function * `callback` **[Function][15]** Callback function
Returns **this** Returns **this**
@ -698,8 +691,8 @@ Detach event
### Parameters ### Parameters
* `event` **[string][7]** Event name * `event` **[string][9]** Event name
* `callback` **[Function][13]** Callback function * `callback` **[Function][15]** Callback function
Returns **this** Returns **this**
@ -709,7 +702,7 @@ Trigger event
### Parameters ### Parameters
* `event` **[string][7]** Event to trigger * `event` **[string][9]** Event to trigger
Returns **this** Returns **this**
@ -721,7 +714,24 @@ Destroy the editor
Render 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 ## html
@ -729,8 +739,8 @@ Print safe HTML by using ES6 tagged template strings.
### Parameters ### Parameters
* `literals` **[Array][10]<[String][7]>** * `literals` **[Array][12]<[String][9]>**
* `substs` **[Array][10]<[String][7]>** * `substs` **[Array][12]<[String][9]>**
### Examples ### Examples
@ -741,34 +751,38 @@ const safeStr = '<b>Hello</b>';
const strHtml = editor.html`Escaped ${unsafeStr}, unescaped $${safeStr}`; 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 [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. --> <!-- 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 ## getAllFrames
Get all frames Get all frames
@ -10,7 +36,7 @@ Get all frames
const arrayOfFrames = page.getAllFrames(); const arrayOfFrames = page.getAllFrames();
``` ```
Returns **[Array][1]\<Frame>** Returns **[Array][2]\<Frame>**
## getMainFrame ## getMainFrame
@ -37,4 +63,6 @@ console.log(rootComponent.toHTML());
Returns **Component** 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] * [add][1]
* [get][2] * [get][2]
* [getAll][3] * [getAll][3]
* [getMain][4] * [getAllWrappers][4]
* [remove][5] * [getMain][5]
* [select][6] * [remove][6]
* [getSelected][7] * [select][7]
* [getSelected][8]
[Page]: page.html [Page]: page.html
[Component]: component.html
## add ## add
Add new page Add new page
### Parameters ### Parameters
* `props` **[Object][8]** Page properties * `props` **[Object][9]** Page properties
* `opts` **[Object][8]?** Options (optional, default `{}`) * `opts` **[Object][9]?** Options (optional, default `{}`)
### Examples ### Examples
@ -72,7 +75,7 @@ Remove page
### Parameters ### Parameters
* `page` **([String][9] | [Page])** Page or page id * `page` **([String][10] | [Page])** Page or page id
* `opts` (optional, default `{}`) * `opts` (optional, default `{}`)
### Examples ### Examples
@ -84,7 +87,7 @@ const somePage = pageManager.get('page-id');
pageManager.remove(somePage); pageManager.remove(somePage);
``` ```
Returns **[Page]** Returns **[Page]** Removed Page
## get ## get
@ -92,7 +95,7 @@ Get page by id
### Parameters ### Parameters
* `id` **[String][9]** Page id * `id` **[String][10]** Page id
### Examples ### Examples
@ -124,7 +127,21 @@ Get all pages
const arrayOfPages = pageManager.getAll(); 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 ## select
@ -132,7 +149,7 @@ Change the selected page. This will switch the page rendered in canvas
### Parameters ### Parameters
* `page` **([String][9] | [Page])** Page or page id * `page` **([String][10] | [Page])** Page or page id
* `opts` (optional, default `{}`) * `opts` (optional, default `{}`)
### Examples ### Examples
@ -164,16 +181,18 @@ Returns **[Page]**
[3]: #getall [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 ```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] * [getConfig][2]
* [add][3] * [add][3]
* [addClass][4] * [get][4]
* [get][5] * [remove][5]
* [getAll][6] * [getAll][6]
* [setState][7] * [setState][7]
* [getState][8] * [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 ## getConfig
Get configuration object 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 ## setState
@ -61,7 +152,7 @@ Change the selector state
### Parameters ### Parameters
* `value` **[String][10]** State value * `value` **[String][18]** State value
### Examples ### Examples
@ -73,90 +164,111 @@ Returns **this**
## getState ## 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 ### Parameters
* `name` **([String][10] | [Array][11])** Selector/s name * `states` **[Array][19]<[Object][17]>** Array of new states
* `opts` **[Object][9]** Selector options (optional, default `{}`) * `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 `''`) Get commonly selected selectors, based on all selected components.
* `opts.type` **[String][10]** Type of the selector. At the moment, only 'class' (1) is available (optional, default `1`)
### Examples ### Examples
```javascript ```javascript
const selector = selectorManager.add('selectorName'); const selected = selectorManager.getSelected();
// Same as console.log(selected.map(s => s.toString()))
const selector = selectorManager.add('selectorName', {
type: 1,
label: 'selectorName'
});
// Multiple selectors
const selectors = selectorManager.add(['.class1', '.class2', '#id1']);
``` ```
Returns **(Model | [Array][11])** Returns **[Array][19]<[Selector]>**
## addClass ## addSelected
Add class selectors Add new selector to all selected components.
### Parameters ### 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 ### Examples
```javascript ```javascript
sm.addClass('class1'); selectorManager.addSelected('.new-class');
sm.addClass('class1 class2');
sm.addClass(['class1', 'class2']);
// -> [SelectorObject, ...]
``` ```
Returns **[Array][11]** Array of added selectors ## removeSelected
## get
Get the selector by its name Remove a common selector from all selected components.
### Parameters ### Parameters
* `name` **([String][10] | [Array][11])** Selector name * `selector` **([String][18] | [Selector])** Selector instance or Selector string identifier
* `type` **[String][10]** Selector type
### Examples ### Examples
```javascript ```javascript
const selector = selectorManager.get('selectorName'); selectorManager.removeSelected('.myclass');
// or get an array
const selectors = selectorManager.get(['class1', 'class2']);
``` ```
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 ### 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 [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 [3]: #add
[4]: #addclass [4]: #get
[5]: #get [5]: #remove
[6]: #getall [6]: #getall
@ -174,8 +286,26 @@ Returns **[String][10]** Escaped name
[8]: #getstate [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] * [getTypes][14]
* [createType][15] * [createType][15]
[Sector]: sector.html
[CssRule]: css_rule.html
[Component]: component.html
## getConfig ## getConfig
Get configuration object Get configuration object
@ -43,7 +49,7 @@ Returns **[Object][16]**
## addSector ## addSector
Add new sector to the collection. If the sector with the same id already exists, 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 ### Parameters
@ -53,13 +59,12 @@ that one will be returned
* `sector.name` **[string][17]** Sector's label (optional, default `''`) * `sector.name` **[string][17]** Sector's label (optional, default `''`)
* `sector.open` **[Boolean][18]** Indicates if the sector should be opened (optional, default `true`) * `sector.open` **[Boolean][18]** Indicates if the sector should be opened (optional, default `true`)
* `sector.properties` **[Array][19]<[Object][16]>** Array of properties (optional, default `[]`) * `sector.properties` **[Array][19]<[Object][16]>** Array of properties (optional, default `[]`)
* `opts` (optional, default `{}`)
* `options` **[Object][16]** Options (optional, default `{}`) * `options` **[Object][16]** Options (optional, default `{}`)
### Examples ### Examples
```javascript ```javascript
var sector = styleManager.addSector('mySector',{ const sector = styleManager.addSector('mySector',{
name: 'My sector', name: 'My sector',
open: true, open: true,
properties: [{ name: 'My property'}] 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 // With `at: 0` we place the new sector at the beginning of the collection
``` ```
Returns **Sector** Added Sector Returns **[Sector]** Added Sector
## getSector ## getSector
@ -81,10 +86,10 @@ Get sector by id
### Examples ### Examples
```javascript ```javascript
var sector = styleManager.getSector('mySector'); const sector = styleManager.getSector('mySector');
``` ```
Returns **(Sector | null)** Returns **([Sector] | null)**
## removeSector ## removeSector
@ -100,13 +105,23 @@ Remove a sector by id
const removed = styleManager.removeSector('mySector'); const removed = styleManager.removeSector('mySector');
``` ```
Returns **Sector** Removed sector Returns **[Sector]** Removed sector
## getSectors ## getSectors
Get all sectors 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 ## addProperty
@ -301,18 +316,6 @@ someContainer.appendChild(propView.el);
Returns **PropertyView** 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 [1]: https://github.com/artf/grapesjs/blob/master/src/style_manager/config/config.js
[2]: #getconfig [2]: #getconfig

2
docs/modules/Assets.md

@ -316,7 +316,7 @@ am.open({
## Customization ## 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. 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> <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 ::: 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]] [[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 ```js
var editor = grapesjs.init({...}); // Your components
var blockManager = editor.BlockManager; 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 ```js
blockManager.add('my-first-block', { // Your components
label: 'Simple block', editor.Components.addType('my-cmp', { ... });
content: '<div class="my-block">This is a simple block</div>', 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 ```js
blockManager.get('my-first-block').set({ // Your component
label: 'Updated simple block', editor.Components.addType('cmp-Y', {
attributes: { // Detect '.el-Y' elements
title: 'My title' 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. ::: tip
If you want you could also pass an object representing the [Component]. You can use most of the available [Component properties](/api/component.html#properties).
:::
```js ```js
blockManager.add('my-map-block', { // -- [Option 1]: Declare type in HTML strings --
label: 'Simple map block', {
content: { // ...
type: 'map', // Built-in 'map' component content: `<div class="el-X">
style: { <div data-gjs-type="cmp-Y" class="el-Y el-A">Element A</div>
height: '350px' <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>
removable: false, // Once inserted it can't be removed </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 ```js
blockManager.add('the-row-block', { const editor = grapesjs.init({
label: '2 Columns', // ...
content: '<div class="row" data-gjs-droppable=".row-cell" data-gjs-custom-name="Row">' + blockManager: {
'<div class="row-cell" data-gjs-draggable=".row"></div>' + // ...
'<div class="row-cell" data-gjs-draggable=".row"></div>' + custom: true,
'</div>', },
});
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. Here an example of using custom Block Manager with a Vue component.
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) <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+"/> ## 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. 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')"> <img :src="$withBase('/block-custom-render2.jpg')">
-->
[Block]: </api/block.html>
[Component]: </api/component.html> [Component]: </api/component.html>
[Components]: <Components.html> [Components]: <Components.html>
[Getting Started]: </getting-started.html>
[Blocks API]: </api/block_manager.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 ::: 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 ```js
component.append('<div>...', { at: 0 }) 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 ## Components & JS
If you want to know how to create Components with javascript attached (eg. counters, galleries, slideshows, etc.) check the dedicated page 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 Definition]: <#component-definition>
[Component]: </api/component.html> [Component]: </api/component.html>
[CssRule]: </api/css_rule.html>
[Component API]: </api/component.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", "name": "grapesjs",
"description": "Free and Open Source Web Builder Framework", "description": "Free and Open Source Web Builder Framework",
"version": "0.17.26", "version": "0.17.28",
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",
@ -38,7 +38,6 @@
"prettier": "^1.18.2", "prettier": "^1.18.2",
"sass": "^1.42.1", "sass": "^1.42.1",
"sinon": "^7.5.0", "sinon": "^7.5.0",
"string-replace-loader": "^2.2.0",
"vuepress": "^1.8.2", "vuepress": "^1.8.2",
"whatwg-fetch": "^3.6.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 * ## Available Events
* * `asset:open` - Asset Manager opened. * * `asset:open` - Asset Manager opened.
* * `asset:close` - Asset Manager closed. * * `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: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:update` - Asset updated. The updated [Asset] and the object containing changes are passed as arguments to the callback.
* * `asset:upload:start` - Before the upload is started. * * `asset:upload:start` - Before the upload is started.

6
src/block_manager/config/config.js

@ -16,5 +16,9 @@ export default {
// } // }
appendOnClick: false, 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 * ```js
* // Listen to events
* editor.on('block:add', (block) => { ... });
*
* // Use the API
* const blockManager = editor.BlockManager; * 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) * * [add](#add)
* * [get](#get) * * [get](#get)
* * [getAll](#getall) * * [getAll](#getall)
@ -24,65 +40,167 @@
* * [render](#render) * * [render](#render)
* *
* [Block]: block.html * [Block]: block.html
* [Component]: component.html
* *
* @module BlockManager * @module BlockManager
*/ */
import { isElement } from 'underscore'; import { isElement, isArray } from 'underscore';
import Module from 'common/module';
import defaults from './config/config'; import defaults from './config/config';
import Block from './model/Block';
import Blocks from './model/Blocks'; 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'; 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 () => { export default () => {
var c = {}; var c = {};
var blocks, blocksVisible, blocksView; var blocks, blocksVisible, blocksView;
var categories = []; var categories = [];
return { return {
/** ...Module,
* Name of the module
* @type {String}
* @private
*/
name: 'BlockManager', name: 'BlockManager',
/** Block,
* 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;
for (let name in defaults) { Blocks,
if (!(name in c)) {
c[name] = defaults[name]; 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 // Global blocks collection
blocks = new Blocks([]); blocks = new Blocks(c.blocks);
blocksVisible = new Blocks([]); blocksVisible = new Blocks(blocks.models);
categories = new BlockCategories(); categories = new Categories();
this.all = blocks;
this.__initListen();
// Setup the sync between the global and public collections // Setup the sync between the global and public collections
blocks.listenTo(blocks, 'add', model => { blocks.on('add', model => blocksVisible.add(model));
blocksVisible.add(model); blocks.on('remove', model => blocksVisible.remove(model));
em && em.trigger('block:add', model); blocks.on('reset', coll => blocksVisible.reset(coll.models));
});
blocks.listenTo(blocks, 'remove', model => { return this;
blocksVisible.remove(model); },
em && em.trigger('block:remove', model);
}); __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 => { __behaviour(opts = {}) {
blocksVisible.reset(coll.models); 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; 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() { postRender() {
const collection = blocksVisible; const collection = blocksVisible;
blocksView = new BlocksView({ collection, categories }, c); blocksView = new BlocksView({ collection, categories }, c);
@ -112,22 +218,17 @@ export default () => {
if (elTo) { if (elTo) {
const el = isElement(elTo) ? elTo : document.querySelector(elTo); const el = isElement(elTo) ? elTo : document.querySelector(elTo);
if (!el) return this.__logWarn('"appendTo" element not found');
el.appendChild(this.render(blocksVisible.models)); el.appendChild(this.render(blocksVisible.models));
} }
this.__trgCustom();
}, },
/** /**
* Add new block to the collection. * Add new block.
* @param {string} id Block id * @param {String} id Block ID
* @param {Object} opts Options * @param {[Block]} props Block properties
* @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
* @returns {[Block]} Added block * @returns {[Block]} Added block
* @example * @example
* blockManager.add('h1-block', { * blockManager.add('h1-block', {
@ -139,15 +240,15 @@ export default () => {
* } * }
* }); * });
*/ */
add(id, opts) { add(id, props, opts = {}) {
var obj = opts || {}; const prp = props || {};
obj.id = id; prp.id = id;
return blocks.add(obj); return blocks.add(prp, opts);
}, },
/** /**
* Return the block by id * Get the block by id.
* @param {string} id Block id * @param {String} id Block id
* @returns {[Block]} * @returns {[Block]}
* @example * @example
* const block = blockManager.get('h1-block'); * const block = blockManager.get('h1-block');
@ -159,8 +260,8 @@ export default () => {
}, },
/** /**
* Return all blocks * Return all blocks.
* @return {Collection} * @returns {Collection<[Block]>}
* @example * @example
* const blocks = blockManager.getAll(); * const blocks = blockManager.getAll();
* console.log(JSON.stringify(blocks)); * console.log(JSON.stringify(blocks));
@ -172,23 +273,24 @@ export default () => {
/** /**
* Return the visible collection, which containes blocks actually rendered * Return the visible collection, which containes blocks actually rendered
* @return {Collection} * @returns {Collection<[Block]>}
*/ */
getAllVisible() { getAllVisible() {
return blocksVisible; return blocksVisible;
}, },
/** /**
* Remove a block by id * Remove block.
* @param {string} id Block id * @param {String|[Block]} block Block or block ID
* @returns {[Block]} Removed block * @returns {[Block]} Removed block
* @example * @example
* // Id of the block which need to be removed * const removed = blockManager.remove('BLOCK_ID');
* const id = 'button'; * // or by passing the Block
* blockManager.remove(id); * const block = blockManager.get('BLOCK_ID');
* blockManager.remove(block);
*/ */
remove(id) { remove(block, opts = {}) {
return blocks.remove(id); return this.__remove(block, opts);
}, },
/** /**
@ -261,12 +363,10 @@ export default () => {
}, },
destroy() { destroy() {
blocks.reset(); const colls = [blocks, blocksVisible, categories];
blocks.stopListening(); colls.map(c => c.stopListening());
blocksVisible.reset(); colls.map(c => c.reset());
categories.reset();
blocksView && blocksView.remove(); blocksView && blocksView.remove();
[blocks, blocksVisible, categories, blocksView].forEach(i => (i = null));
c = {}; c = {};
} }
}; };

52
src/block_manager/model/Block.js

@ -1,13 +1,14 @@
import { Model } from 'common'; import { Model } from 'common';
import { isFunction } from 'underscore';
/** /**
* @property {String} label Block label, eg. `My block` * @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|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} [media=''] HTML string for the media/icon of the block, eg. `<svg ...`, `<img ...`, etc.
* @property {String} [category=''] Block category * @property {String} [category=''] Block category, eg. `Basic blocks`
* @property {Boolean} [activate=false] If true, triggers an the `active` event on dropped component * @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} [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} [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 {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 {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 * @property {Object} [attributes={}] Block attributes to apply in the view element
@ -20,11 +21,52 @@ export default class Block extends Model {
media: '', media: '',
category: '', category: '',
activate: false, activate: false,
select: false, select: null,
resetId: false, resetId: false,
disable: false, disable: false,
onClick: null, onClick: null,
attributes: {} 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'; import Block from './Block';
export default Backbone.Collection.extend({ export default class Blocks extends Collection {}
model: Block
}); 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'; import Category from './Category';
export default Backbone.Collection.extend({ export default class Categories extends Collection {}
model: Category
}); 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({ export default class Category extends Model {
defaults: { defaults() {
id: '', return {
label: '', id: '',
open: true, label: '',
attributes: {} open: true,
attributes: {}
};
} }
}); }

50
src/block_manager/view/BlockView.js

@ -1,5 +1,5 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import { isFunction, isObject } from 'underscore'; import { isFunction } from 'underscore';
import { on, off, hasDnd } from 'utils/mixins'; import { on, off, hasDnd } from 'utils/mixins';
export default Backbone.View.extend({ export default Backbone.View.extend({
@ -21,6 +21,10 @@ export default Backbone.View.extend({
this.listenTo(model, 'change', this.render); this.listenTo(model, 'change', this.render);
}, },
__getModule() {
return this.em.get('BlockManager');
},
handleClick(ev) { handleClick(ev) {
const { config, model, em } = this; const { config, model, em } = this;
const onClick = model.get('onClick') || config.appendOnClick; const onClick = model.get('onClick') || config.appendOnClick;
@ -80,53 +84,15 @@ export default Backbone.View.extend({
}, },
handleDragStart(ev) { handleDragStart(ev) {
const { em, model } = this; this.__getModule().__startDrag(this.model, ev);
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);
}, },
handleDrag(ev) { handleDrag(ev) {
this.em.trigger('block:drag', this.model, ev); this.__getModule().__drag(ev);
}, },
handleDragEnd() { handleDragEnd() {
const { em, model } = this; this.__getModule().__endDrag();
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);
}, },
/** /**

2
src/block_manager/view/BlocksView.js

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

6
src/canvas/index.js

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

12
src/canvas/model/Frame.js

@ -1,10 +1,18 @@
import { Model } from 'backbone'; import { Model } from 'common';
import { result, forEach, isEmpty, debounce, isString } from 'underscore'; import { result, forEach, isEmpty, isString } from 'underscore';
import { isComponent, isObject } from 'utils/mixins'; import { isComponent, isObject } from 'utils/mixins';
const keyAutoW = '__aw'; const keyAutoW = '__aw';
const keyAutoH = '__ah'; 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 { export default class Frame extends Model {
defaults() { defaults() {
return { return {

2
src/canvas/model/Frames.js

@ -1,5 +1,5 @@
import { bindAll } from 'underscore'; import { bindAll } from 'underscore';
import { Collection } from 'backbone'; import { Collection } from 'common';
import model from './Frame'; import model from './Frame';
export default class Frames extends Collection { 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: 'keydown keyup keypress', class: 'KeyboardEvent' },
{ event: 'mousedown mousemove mouseup', class: 'MouseEvent' }, { event: 'mousedown mousemove mouseup', class: 'MouseEvent' },
{ event: 'pointerdown pointermove pointerup', class: 'PointerEvent' },
{ event: 'wheel', class: 'WheelEvent' } { event: 'wheel', class: 'WheelEvent' }
].forEach(obj => ].forEach(obj =>
obj.event.split(' ').forEach(event => { obj.event.split(' ').forEach(event => {

2
src/commands/index.js

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

22
src/commands/view/ComponentNext.js

@ -3,11 +3,23 @@ export default {
if (!ed.Canvas.hasFocus()) return; if (!ed.Canvas.hasFocus()) return;
const toSelect = []; const toSelect = [];
ed.getSelectedAll().forEach(component => { ed.getSelectedAll().forEach(cmp => {
const coll = component.collection; const parent = cmp.parent();
const at = coll.indexOf(component); if (!parent) return;
const next = coll.at(at + 1);
toSelect.push(next || component); 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); toSelect.length && ed.select(toSelect);

21
src/commands/view/ComponentPrev.js

@ -3,11 +3,22 @@ export default {
if (!ed.Canvas.hasFocus()) return; if (!ed.Canvas.hasFocus()) return;
const toSelect = []; const toSelect = [];
ed.getSelectedAll().forEach(component => { ed.getSelectedAll().forEach(cmp => {
const coll = component.collection; const parent = cmp.parent();
const at = coll.indexOf(component); if (!parent) return;
const next = coll.at(at - 1);
toSelect.push(next && at - 1 >= 0 ? next : component); 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); 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 { export default {
run(ed, sender, opts = {}) { run(ed, s, opts = {}) {
const { target } = 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 type = target.get('type');
const len = dc.getWrapper().findType(type).length; const wrappers = ed.Pages.getAllWrappers();
const toRemove = []; 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) { if (!len) {
const rules = ed.CssComposer.getAll(); const rules = ed.CssComposer.getAll();
let toClear = target.get('style-signature'); toRemove = rules.filter(rule => rule.get('group') === `cmp:${type}`);
toClear = isArray(toClear) ? toClear : [toClear];
rules.forEach(rule => {
const selector = rule.selectorsToString();
toClear.forEach(part => {
part && selector.indexOf(part) >= 0 && toRemove.push(rule);
});
});
rules.remove(toRemove); rules.remove(toRemove);
} }

58
src/commands/view/OpenBlocks.js

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

9
src/commands/view/OpenLayers.js

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

72
src/commands/view/OpenStyleManager.js

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

12
src/commands/view/OpenTraitManager.js

@ -7,13 +7,15 @@ export default {
this.sender = sender; this.sender = sender;
const em = editor.getModel(); const em = editor.getModel();
var config = editor.Config; const config = editor.Config;
var pfx = config.stylePrefix; const pfx = config.stylePrefix;
var tm = editor.TraitManager; const tm = editor.TraitManager;
var panelC; const confTm = tm.getConfig();
let panelC;
if (confTm.appendTo) return;
if (!this.$cn) { if (!this.$cn) {
var confTm = tm.getConfig();
this.$cn = $('<div></div>'); this.$cn = $('<div></div>');
this.$cn2 = $('<div></div>'); this.$cn2 = $('<div></div>');
this.$cn.append(this.$cn2); 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'; import { createId } from 'utils/mixins';
export default { export default {
getConfig(name) { getConfig(name) {
return this.__getConfig(name);
},
__getConfig(name) {
const res = this.config || {}; const res = this.config || {};
return name ? res[name] : res; return name ? res[name] : res;
}, },
getAll() { getAll(opts = {}) {
return this.all || []; return this.all ? (opts.array ? [...this.all.models] : this.all) : [];
}, },
getAllMap() { 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; const { all, em, events } = this;
all && all &&
em && em &&
@ -29,6 +41,16 @@ export default {
em.trigger(events.update, p, p.changedAttributes(), c) em.trigger(events.update, p, p.changedAttributes(), c)
) )
.on('all', this.__catchAllEvent, this); .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 = {}) { __remove(model, opts = {}) {
@ -49,8 +71,22 @@ export default {
this.__onAllEvent(); 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() {}, __onAllEvent() {},
__logWarn(str) {
this.em.logWarning(`[${this.name}]: ${str}`);
},
_createId(len = 16) { _createId(len = 16) {
const all = this.getAll(); const all = this.getAll();
const ln = all.length + len; const ln = all.length + len;
@ -62,5 +98,13 @@ export default {
} while (allMap[id]); } while (allMap[id]);
return 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) * 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 * ```js
* const editor = grapesjs.init({ * 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 * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
* *
* ```js * ```js
* const cssComposer = editor.CssComposer; * const css = editor.Css;
* ``` * ```
* *
* * [load](#load) * * [addRules](#addrules)
* * [store](#store)
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
* * [clear](#clear)
* * [setRule](#setrule) * * [setRule](#setrule)
* * [getRule](#getrule) * * [getRule](#getrule)
* * [getRules](#getrules)
* * [remove](#remove)
* * [clear](#clear)
*
* [CssRule]: css_rule.html
* *
* @module CssComposer * @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 defaults from './config/config';
import CssRule from './model/CssRule'; import CssRule from './model/CssRule';
import CssRules from './model/CssRules'; import CssRules from './model/CssRules';
@ -113,6 +114,7 @@ export default () => {
* The fetched data will be added to the collection * The fetched data will be added to the collection
* @param {Object} data Object of data to load * @param {Object} data Object of data to load
* @return {Object} Loaded rules * @return {Object} Loaded rules
* @private
*/ */
load(data) { load(data) {
var d = data || ''; var d = data || '';
@ -144,6 +146,7 @@ export default () => {
* Store data to the selected storage * Store data to the selected storage
* @param {Boolean} noStore If true, won't store * @param {Boolean} noStore If true, won't store
* @return {Object} Data to store * @return {Object} Data to store
* @private
*/ */
store(noStore) { store(noStore) {
if (!c.stm) return; if (!c.stm) return;
@ -164,6 +167,7 @@ export default () => {
* @param {Object} props Other props for the rule * @param {Object} props Other props for the rule
* @param {Object} opts Options for the add of new rule * @param {Object} opts Options for the add of new rule
* @return {Model} * @return {Model}
* @private
* @example * @example
* var sm = editor.SelectorManager; * var sm = editor.SelectorManager;
* var sel1 = sm.add('myClass1'); * var sel1 = sm.add('myClass1');
@ -189,6 +193,7 @@ export default () => {
opt.state = s; opt.state = s;
opt.mediaText = w; opt.mediaText = w;
opt.selectors = []; opt.selectors = [];
w && (opt.atRuleType = 'media');
rule = new CssRule(opt, c); rule = new CssRule(opt, c);
rule.get('selectors').add(selectors, addOpts); rule.get('selectors').add(selectors, addOpts);
rules.add(rule, addOpts); rules.add(rule, addOpts);
@ -198,16 +203,17 @@ export default () => {
/** /**
* Get the rule * Get the rule
* @param {Array<Selector>} selectors Array of selectors * @param {String|Array<Selector>} selectors Array of selectors or selector string, eg `.myClass1.myClass2`
* @param {String} state Css rule state * @param {String} state Css rule state, eg. 'hover'
* @param {String} width For which device this style is oriented * @param {String} width Media rule value, eg. '(max-width: 992px)'
* @param {Object} ruleProps Other rule props * @param {Object} ruleProps Other rule props
* @return {Model|null} * @return {Model|null}
* @private
* @example * @example
* var sm = editor.SelectorManager; * const sm = editor.SelectorManager;
* var sel1 = sm.add('myClass1'); * const sel1 = sm.add('myClass1');
* var sel2 = sm.add('myClass2'); * const sel2 = sm.add('myClass2');
* var rule = cssComposer.get([sel1, sel2], 'hover'); * const rule = cssComposer.get([sel1, sel2], 'hover', '(max-width: 992px)');
* // Update the style * // Update the style
* rule.set('style', { * rule.set('style', {
* width: '300px', * width: '300px',
@ -215,42 +221,41 @@ export default () => {
* }); * });
* */ * */
get(selectors, state, width, ruleProps) { get(selectors, state, width, ruleProps) {
var rule = null; let slc = selectors;
rules.each(m => { if (isString(selectors)) {
if (rule) return; const sm = em.get('SelectorManager');
if (m.compare(selectors, state, width, ruleProps)) rule = m; const singleSel = selectors.split(',')[0].trim();
}); const node = em
return rule; .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() { getAll() {
return rules; return rules;
}, },
/**
* Remove all rules
* @return {this}
*/
clear(opts = {}) {
this.getAll().reset(null, opts);
return this;
},
/** /**
* Add a raw collection of rule objects * Add a raw collection of rule objects
* This method overrides styles, in case, of already defined rule * 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} opts Options
* @param {Object} props Additional properties to add on rules
* @return {Array<Model>} * @return {Array<Model>}
* @private * @private
*/ */
addCollection(data, opts = {}) { addCollection(data, opts = {}, props = {}) {
var result = []; const result = [];
var d = data instanceof Array ? data : [data];
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++) { for (var i = 0, l = d.length; i < l; i++) {
var rule = d[i] || {}; var rule = d[i] || {};
@ -271,6 +276,8 @@ export default () => {
var updateStyle = !modelExists || !opts.avoidUpdateStyle; var updateStyle = !modelExists || !opts.avoidUpdateStyle;
const style = rule.style || {}; const style = rule.style || {};
isObject(props) && model.set(props, opts);
if (updateStyle) { if (updateStyle) {
let styleUpdate = opts.extend let styleUpdate = opts.extend
? { ...model.get('style'), ...style } ? { ...model.get('style'), ...style }
@ -285,22 +292,35 @@ export default () => {
}, },
/** /**
* Add/update the CSS rule with a generic selector * Add CssRules via CSS string.
* @param {string} selectors Selector, eg. '.myclass' * @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} style Style properties and values
* @param {Object} [opts={}] Additional properties * @param {Object} [opts={}] Additional properties
* @param {String} [opts.atRuleType=''] At-rule type, eg. 'media' * @param {String} [opts.atRuleType=''] At-rule type, eg. `media`
* @param {String} [opts.atRuleParams=''] At-rule parameters, eg. '(min-width: 500px)' * @param {String} [opts.atRuleParams=''] At-rule parameters, eg. `(min-width: 500px)`
* @return {CssRule} The new/updated rule * @returns {[CssRule]} The new/updated CssRule
* @example * @example
* // Simple class-based rule * // 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 } * console.log(rule.toCSS()) // output: .class1.class2 { color: red }
* // With state and other mixed selector * // 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 } * // output: .class1.class2:hover, div#myid { color: red }
* // With media * // With media
* const rule = cc.setRule('.class1:hover', { color: 'red' }, { * const rule = css.setRule('.class1:hover', { color: 'red' }, {
* atRuleType: 'media', * atRuleType: 'media',
* atRuleParams: '(min-width: 500px)', * atRuleParams: '(min-width: 500px)',
* }); * });
@ -324,15 +344,16 @@ export default () => {
}, },
/** /**
* Get the CSS rule by a generic selector * Get the CssRule.
* @param {string} selectors Selector, eg. '.myclass:hover' * @param {String} selectors Selector string, eg. `.myclass:hover`
* @param {String} [opts.atRuleType=''] At-rule type, eg. 'media' * @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)' * @param {String} [opts.atRuleParams=''] At-rule parameters, eg. '(min-width: 500px)'
* @return {CssRule} * @returns {[CssRule]}
* @example * @example
* const rule = cc.getRule('.myclass1:hover'); * const rule = css.getRule('.myclass1:hover');
* const rule2 = cc.getRule('.myclass1:hover, div#myid'); * const rule2 = css.getRule('.myclass1:hover, div#myid');
* const rule3 = cc.getRule('.myclass1', { * const rule3 = css.getRule('.myclass1', {
* atRuleType: 'media', * atRuleType: 'media',
* atRuleParams: '(min-width: 500px)', * atRuleParams: '(min-width: 500px)',
* }); * });
@ -353,17 +374,20 @@ export default () => {
}, },
/** /**
* Find rules, in different states (eg. like `:hover`) and media queries, matching the selector. * Get all rules or filtered by a matching selector.
* @param {string} selector Selector, eg. '.myclass' * @param {String} [selector=''] Selector, eg. `.myclass`
* @returns {Array<CssRule>} * @returns {Array<[CssRule]>}
* @example * @example
* // Common scenario, take all the component specific rules * // Take all the component specific rules
* const id = someComponent.getId(); * const id = someComponent.getId();
* const rules = cc.getRules(`#${id}`); * const rules = css.getRules(`#${id}`);
* console.log(rules.map(rule => rule.toCSS())) * console.log(rules.map(rule => rule.toCSS()))
* // All rules in the project
* console.log(css.getRules())
*/ */
getRules(selector) { getRules(selector) {
const rules = this.getAll(); const rules = this.getAll();
if (!selector) return [...rules.models];
const result = rules.filter( const result = rules.filter(
r => r.getSelectors().getFullString() === selector r => r.getSelectors().getFullString() === selector
); );
@ -378,8 +402,8 @@ export default () => {
* @return {CssRule} The new/updated rule * @return {CssRule} The new/updated rule
* @private * @private
* @example * @example
* const rule = cc.setIdRule('myid', { color: 'red' }); * const rule = css.setIdRule('myid', { color: 'red' });
* const ruleHover = cc.setIdRule('myid', { color: 'blue' }, { state: 'hover' }); * const ruleHover = css.setIdRule('myid', { color: 'blue' }, { state: 'hover' });
* // This will add current CSS: * // This will add current CSS:
* // #myid { color: red } * // #myid { color: red }
* // #myid:hover { color: blue } * // #myid:hover { color: blue }
@ -402,8 +426,8 @@ export default () => {
* @return {CssRule} * @return {CssRule}
* @private * @private
* @example * @example
* const rule = cc.getIdRule('myid'); * const rule = css.getIdRule('myid');
* const ruleHover = cc.setIdRule('myid', { state: 'hover' }); * const ruleHover = css.setIdRule('myid', { state: 'hover' });
*/ */
getIdRule(name, opts = {}) { getIdRule(name, opts = {}) {
const { mediaText } = opts; const { mediaText } = opts;
@ -421,8 +445,8 @@ export default () => {
* @return {CssRule} The new/updated rule * @return {CssRule} The new/updated rule
* @private * @private
* @example * @example
* const rule = cc.setClassRule('myclass', { color: 'red' }); * const rule = css.setClassRule('myclass', { color: 'red' });
* const ruleHover = cc.setClassRule('myclass', { color: 'blue' }, { state: 'hover' }); * const ruleHover = css.setClassRule('myclass', { color: 'blue' }, { state: 'hover' });
* // This will add current CSS: * // This will add current CSS:
* // .myclass { color: red } * // .myclass { color: red }
* // .myclass:hover { color: blue } * // .myclass:hover { color: blue }
@ -444,8 +468,8 @@ export default () => {
* @return {CssRule} * @return {CssRule}
* @private * @private
* @example * @example
* const rule = cc.getClassRule('myclass'); * const rule = css.getClassRule('myclass');
* const ruleHover = cc.getClassRule('myclass', { state: 'hover' }); * const ruleHover = css.getClassRule('myclass', { state: 'hover' });
*/ */
getClassRule(name, opts = {}) { getClassRule(name, opts = {}) {
const state = opts.state || ''; const state = opts.state || '';
@ -454,6 +478,32 @@ export default () => {
return selector && this.get(selector, state, media); 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 = {}) { getComponentRules(cmp, opts = {}) {
let { state, mediaText, current } = opts; let { state, mediaText, current } = opts;
if (current) { if (current) {

96
src/css_composer/model/CssRule.js

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

60
src/dom_components/model/Component.js

@ -53,6 +53,8 @@ export const keyUpdateInside = `${keyUpdate}-inside`;
* // -> 'span' * // -> 'span'
* ``` * ```
* *
* [Component]: component.html
*
* @typedef Component * @typedef Component
* @property {String} [type=''] Component type, eg. `text`, `image`, `video`, etc. * @property {String} [type=''] Component type, eg. `text`, `image`, `video`, etc.
* @property {String} [tagName='div'] HTML tag of the component, eg. `span`. Default: `div` * @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} [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} [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 {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} [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} [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: `''` * @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) { 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.__postAdd();
this.init(); this.init();
this.__isSymbolOrInst() && this.__initSymb(); this.__isSymbolOrInst() && this.__initSymb();
@ -189,6 +199,7 @@ export default class Component extends Model.extend(Styleable) {
const comps = this.components(); const comps = this.components();
if (um && !this.__hasUm) { if (um && !this.__hasUm) {
um.add(comps); um.add(comps);
um.add(this.getSelectors());
this.__hasUm = 1; this.__hasUm = 1;
} }
opts.recursive && comps.map(c => c.__postAdd(opts)); 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'); const um = em && em.get('UndoManager');
if (um) { if (um) {
um.remove(this.components()); um.remove(this.components());
um.remove(this.getSelectors());
delete this.__hasUm; delete this.__hasUm;
} }
} }
@ -260,7 +272,7 @@ export default class Component extends Model.extend(Styleable) {
*/ */
index() { index() {
const { collection } = this; 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 * Get the style of the component
* @return {Object} * @return {Object}
*/ */
getStyle(opts = {}) { getStyle(options = {}, optsAdd = {}) {
const em = this.em; const em = this.em;
const prop = isString(options) ? options : '';
const opts = prop ? optsAdd : options;
if (em && em.getConfig('avoidInlineStyle') && !opts.inline) { if (em && em.getConfig('avoidInlineStyle') && !opts.inline) {
const state = em.get('state'); const state = em.get('state');
@ -476,11 +490,11 @@ export default class Component extends Model.extend(Styleable) {
this.rule = rule; this.rule = rule;
if (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 * Set new collection if `components` are provided, otherwise the
* current collection is returned * 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()` * @param {Object} [opts={}] Options, same as in `Component.append()`
* @return {Collection|Array<Component>} * @returns {Collection|Array<[Component]>}
* @example * @example
* // Set new collection * // Set new collection
* component.components('<span></span><div></div>'); * component.components('<span></span><div></div>');
@ -1026,10 +1040,35 @@ export default class Component extends Model.extend(Styleable) {
return coll; return coll;
} else { } else {
coll.reset(null, opts); 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 * Remove all inner components
* * @return {this} * * @return {this}
@ -1041,14 +1080,14 @@ export default class Component extends Model.extend(Styleable) {
/** /**
* Get the parent component, if exists * Get the parent component, if exists
* @return {Component} * @return {Component|null}
* @example * @example
* component.parent(); * component.parent();
* // -> Component * // -> Component
*/ */
parent(opts = {}) { parent(opts = {}) {
const coll = this.collection || (opts.prev && this.prevColl); 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' status: '', // State, eg. 'selected'
content: '', content: '',
icon: '', icon: '',
style: '', // Component related style style: '',
styles: '', // Component related styles
classes: '', // Array of classes classes: '', // Array of classes
script: '', script: '',
'script-props': '', '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))); sels.remove(rulesRemoved.map(rule => rule.getSelectors().at(0)));
if (!removed.opt.temporary) { if (!removed.opt.temporary) {
// Deprecate 'style-signature' em.get('Commands').run('core:component-style-clear', {
// const cm = em.get('Commands'); target: removed
// const hasSign = removed.get('style-signature'); });
// const optStyle = { target: removed };
// hasSign && cm.run('core:component-style-clear', optStyle);
removed.removed(); removed.removed();
removed.trigger('removed'); removed.trigger('removed');
em.trigger('component:remove', removed); em.trigger('component:remove', removed);
@ -162,6 +160,7 @@ export default Backbone.Collection.extend({
if (isString(models)) { if (isString(models)) {
models = this.parseString(models, opt); models = this.parseString(models, opt);
} else if (isArray(models)) { } else if (isArray(models)) {
models = [...models];
models.forEach((item, index) => { models.forEach((item, index) => {
if (isString(item)) { if (isString(item)) {
const nodes = this.parseString(item, opt); const nodes = this.parseString(item, opt);

1
src/dom_components/view/ComponentImageView.js

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

5
src/domain_abstract/model/Styleable.js

@ -21,9 +21,10 @@ export default {
* Get style object * Get style object
* @return {Object} * @return {Object}
*/ */
getStyle() { getStyle(prop) {
const style = this.get('style') || {}; 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) { handleChange(e) {
e.stopPropagation(); e.stopPropagation();
const value = this.getInputEl().value; const value = this.getInputEl().value;
this.model.set({ value }, { fromInput: 1 }); this.__onInputChange(value);
this.elementUpdated(); this.elementUpdated();
}, },
__onInputChange(value) {
this.model.set({ value }, { fromInput: 1 });
},
/** /**
* Get the input element * Get the input element
* @return {HTMLElement} * @return {HTMLElement}

37
src/domain_abstract/ui/InputColor.js

@ -3,8 +3,14 @@ import { isUndefined } from 'underscore';
import ColorPicker from 'utils/ColorPicker'; import ColorPicker from 'utils/ColorPicker';
import Input from './Input'; import Input from './Input';
const $ = Backbone.$; const { $ } = Backbone;
ColorPicker($); $ && 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({ export default Input.extend({
template() { template() {
@ -33,6 +39,21 @@ export default Input.extend({
this.colorEl.spectrum('destroy'); 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 * Set value to the model
* @param {string} val * @param {string} val
@ -71,16 +92,12 @@ export default Input.extend({
var elToAppend = em && em.config ? em.config.el : ''; var elToAppend = em && em.config ? em.config.el : '';
var colorPickerConfig = var colorPickerConfig =
(em && em.getConfig && em.getConfig('colorPicker')) || {}; (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 changed = 0;
let previousColor; let previousColor;
this.$el.find(`[data-colorp-c]`).append(colorEl); this.$el.find(`[data-colorp-c]`).append(colorEl);
colorEl.spectrum({ colorEl.spectrum({
color: model.getValue() || false,
containerClassName: `${ppfx}one-bg ${ppfx}two-color`, containerClassName: `${ppfx}one-bg ${ppfx}two-color`,
appendTo: elToAppend || 'body', appendTo: elToAppend || 'body',
maxSelectionSize: 8, maxSelectionSize: 8,
@ -123,12 +140,12 @@ export default Input.extend({
} }
}); });
em && if (em && em.on) {
em.on && this.listenTo(em, 'component:selected', () => {
em.on('component:selected', () => {
changed = 1; changed = 1;
colorEl.spectrum('hide'); colorEl.spectrum('hide');
}); });
}
this.colorEl = colorEl; 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: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` - 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 * * `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 * ### Keymaps
* * `keymap:add` - New keymap added. The new keyamp object is passed as an argument * * `keymap:add` - New keymap added. The new keyamp object is passed as an argument
* * `keymap:remove` - Keymap removed. The removed keyamp object is passed as an argument * * `keymap: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: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. * * `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 * 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
* * `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument * * `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 * * `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);`) * * `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 * * `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 * * `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 * ### Assets
* Check the [Assets](/api/assets.html) module. * Check the [Assets](/api/assets.html) module.
* ### Modal * ### Modal
@ -105,13 +98,13 @@
* *
* @module Editor * @module Editor
*/ */
import $ from 'cash-dom';
import defaults from './config/config'; import defaults from './config/config';
import EditorModel from './model/Editor'; import EditorModel from './model/Editor';
import EditorView from './view/EditorView'; import EditorView from './view/EditorView';
import html from 'utils/html'; import html from 'utils/html';
export default (config = {}) => { export default (config = {}, opts = {}) => {
const { $ } = opts;
const c = { const c = {
...defaults, ...defaults,
...config ...config
@ -732,6 +725,19 @@ export default (config = {}) => {
return editorView.render().el; 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. * Print safe HTML by using ES6 tagged template strings.
* @param {Array<String>} literals * @param {Array<String>} literals

11
src/editor/model/Editor.js

@ -7,13 +7,15 @@ import {
keys, keys,
bindAll bindAll
} from 'underscore'; } from 'underscore';
import $ from 'cash-dom';
import Backbone from 'backbone'; import Backbone from 'backbone';
import Extender from 'utils/extender'; import Extender from 'utils/extender';
import { getModel, hasWin } from 'utils/mixins'; import { getModel, hasWin } from 'utils/mixins';
import Selected from './Selected'; import Selected from './Selected';
const cash = hasWin() ? require('cash-dom') : null;
const $ = (cash && cash.default) || cash;
Backbone.$ = $; Backbone.$ = $;
const deps = [ const deps = [
require('utils'), require('utils'),
require('i18n'), require('i18n'),
@ -39,7 +41,6 @@ const deps = [
require('block_manager') require('block_manager')
]; ];
const { Collection } = Backbone;
let timedInterval; let timedInterval;
let updateItr; let updateItr;
@ -123,7 +124,7 @@ export default Backbone.Model.extend({
this.get('readyCanvas') && this.get('readyCanvas') &&
!this.get('ready') !this.get('ready')
) { ) {
this.set('ready', 1); this.set('ready', true);
} }
}, },
@ -171,7 +172,7 @@ export default Backbone.Model.extend({
if (sm && sm.canAutoload()) { if (sm && sm.canAutoload()) {
this.load(postLoad); this.load(postLoad);
} else { } else {
postLoad(); setTimeout(postLoad);
} }
}, },
@ -262,7 +263,7 @@ export default Backbone.Model.extend({
* */ * */
handleUpdates(model, val, opt = {}) { handleUpdates(model, val, opt = {}) {
// Component has been added temporarily - do not update storage or record changes // 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; return;
} }

8
src/index.js

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

27
src/pages/index.js

@ -32,17 +32,19 @@
* * [add](#add) * * [add](#add)
* * [get](#get) * * [get](#get)
* * [getAll](#getall) * * [getAll](#getall)
* * [getAllWrappers](#getallwrappers)
* * [getMain](#getmain) * * [getMain](#getmain)
* * [remove](#remove) * * [remove](#remove)
* * [select](#select) * * [select](#select)
* * [getSelected](#getselected) * * [getSelected](#getselected)
* *
* [Page]: page.html * [Page]: page.html
* [Component]: component.html
* *
* @module Pages * @module Pages
*/ */
import { isString, bindAll } from 'underscore'; import { isString, bindAll, unique, flatten } from 'underscore';
import { createId } from 'utils/mixins'; import { createId } from 'utils/mixins';
import { Model } from 'backbone'; import { Model } from 'backbone';
import Pages from './model/Pages'; import Pages from './model/Pages';
@ -166,7 +168,7 @@ export default () => {
/** /**
* Remove page * Remove page
* @param {String|[Page]} page Page or page id * @param {String|[Page]} page Page or page id
* @returns {[Page]} * @returns {[Page]} Removed Page
* @example * @example
* const removedPage = pageManager.remove('page-id'); * const removedPage = pageManager.remove('page-id');
* // or by passing the page * // or by passing the page
@ -213,7 +215,26 @@ export default () => {
* const arrayOfPages = pageManager.getAll(); * const arrayOfPages = pageManager.getAll();
*/ */
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() { 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 { result, forEach } from 'underscore';
import Frames from 'canvas/model/Frames'; import Frames from 'canvas/model/Frames';
@ -36,6 +36,32 @@ export default class Page extends Model {
return this.get('frames'); 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 * Get all frames
* @returns {Array<Frame>} * @returns {Array<Frame>}

1
src/parser/model/BrowserParserCss.js

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

4
src/parser/model/ParserHtml.js

@ -209,8 +209,8 @@ export default config => {
}; };
} else { } else {
model.components = this.parseNode(node, { 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 canvas = config.em.get('Canvas');
const { style } = toolbar; const { style } = toolbar;
const pos = canvas.getTargetToElementFixed(lastEl, toolbar, { const pos = canvas.getTargetToElementFixed(lastEl, toolbar, {
event: 'rteToolbarPosUpdate' event: 'rteToolbarPosUpdate',
left: 0
}); });
style.top = (pos.top || 0) + un;
style.top = pos.top + un; style.left = (pos.left || 0) + un;
style.left = 0 + un;
}, },
/** /**

9
src/selector_manager/config/config.js

@ -9,7 +9,7 @@ export default {
// Default selectors // Default selectors
selectors: [], selectors: [],
// States // Default states
states: [{ name: 'hover' }, { name: 'active' }, { name: 'nth-of-type(2n)' }], states: [{ name: 'hover' }, { name: 'active' }, { name: 'nth-of-type(2n)' }],
// Custom selector name escaping strategy, eg. // 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>', '<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 * @example
* render: ({ el, labelHead, labelStates, labelInfo, }) => { * render: ({ el, labelHead, labelStates, labelInfo, }) => {
* // You can use the default `el` to extend/edit the current * // 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. // 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 // 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) // 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 * ```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) * * [getConfig](#getconfig)
* * [add](#add) * * [add](#add)
* * [addClass](#addclass)
* * [get](#get) * * [get](#get)
* * [remove](#remove)
* * [getAll](#getall) * * [getAll](#getall)
* * [setState](#setstate) * * [setState](#setstate)
* * [getState](#getstate) * * [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 * @module SelectorManager
*/ */
import { isString, isElement, isObject, isArray } from 'underscore'; import { isString, debounce, isObject, isArray } from 'underscore';
import { isComponent, isRule } from 'utils/mixins'; import { isComponent, isRule } from 'utils/mixins';
import { Model, Collection } from 'common';
import Module from 'common/module';
import defaults from './config/config'; import defaults from './config/config';
import Selector from './model/Selector'; import Selector from './model/Selector';
import Selectors from './model/Selectors'; import Selectors from './model/Selectors';
import State from './model/State';
import ClassTagsView from './view/ClassTagsView'; import ClassTagsView from './view/ClassTagsView';
const isId = str => isString(str) && str[0] == '#'; const isId = str => isString(str) && str[0] == '#';
const isClass = str => isString(str) && str[0] == '.'; const isClass = str => isString(str) && str[0] == '.';
export default config => { export const evAll = 'selector';
var c = config || {}; export const evPfx = `${evAll}:`;
var selectors; 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 { return {
...Module,
name: 'SelectorManager',
Selector, Selector,
Selectors, Selectors,
/** events: {
* Name of the module all: evAll,
* @type {String} update: evUpdate,
* @private add: evAdd,
*/ remove: evRemove,
name: 'SelectorManager', removeBefore: evRemoveBefore,
state: evState,
custom: evCustom
},
/** /**
* Get configuration object * Get configuration object
* @name getConfig
* @function
* @return {Object} * @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 = {}) { init(conf = {}) {
c = { this.__initConfig(defaults, conf);
...defaults, const config = this.getConfig();
...conf const em = config.em;
}; const ppfx = config.pStylePrefix;
const em = c.em;
const ppfx = c.pStylePrefix;
this.em = em;
if (ppfx) { if (ppfx) {
c.stylePrefix = ppfx + c.stylePrefix; config.stylePrefix = ppfx + config.stylePrefix;
} }
// Global selectors container // Global selectors container
selectors = new Selectors(c.selectors); this.all = new Selectors(config.selectors);
selectors.on('add', model => em.trigger('selector:add', model)); this.selected = new Selectors([], { em, config });
selectors.on('remove', model => em.trigger('selector:remove', model)); this.states = new Collection(config.states, { model: State });
selectors.on('change', model => this.model = new Model({ cFirst: config.componentFirst, _undo: true });
em.trigger( this.__initListen({
'selector:update', collections: [this.states, this.selected],
model, propagate: [{ entity: this.states, event: this.events.state }]
model.previousAttributes(), });
model.changedAttributes()
)
);
em.on('change:state', (m, value) => em.trigger('selector:state', value)); 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; return this;
}, },
postRender() { __update: debounce(function() {
const elTo = this.getConfig().appendTo; this.__trgCustom();
}),
if (elTo) { __trgCustom(opts) {
const el = isElement(elTo) ? elTo : document.querySelector(elTo); this.em.trigger(this.events.custom, this.__customData(opts));
el.appendChild(this.render([])); },
}
__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 = {}) { select(value, opts = {}) {
@ -142,53 +198,35 @@ export default config => {
return this; return this;
}, },
/** addSelector(name, opts = {}, cOpts = {}) {
* Change the selector state let props = { ...opts };
* @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 };
if (isObject(name)) { if (isObject(name)) {
opts = name; props = name;
} else { } else {
opts.name = name; props.name = name;
} }
if (isId(opts.name)) { if (isId(props.name)) {
opts.name = opts.name.substr(1); props.name = props.name.substr(1);
opts.type = Selector.TYPE_ID; props.type = Selector.TYPE_ID;
} else if (isClass(opts.name)) { } else if (isClass(props.name)) {
opts.name = opts.name.substr(1); props.name = props.name.substr(1);
} }
if (opts.label && !opts.name) { if (props.label && !props.name) {
opts.name = this.escapeName(opts.label); 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 const selector = cname
? this.get(cname, opts.type) ? this.get(cname, props.type)
: selectors.where(opts)[0]; : all.where(props)[0];
if (!selector) { if (!selector) {
return selectors.add(opts, { config: c }); return all.add(props, { ...cOpts, config });
} }
return selector; return selector;
@ -202,31 +240,29 @@ export default config => {
name = name.substr(1); 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 * Add a new selector to the collection if it does not already exist.
* @param {String|Array} name Selector/s name * You can pass selectors properties or string identifiers.
* @param {Object} opts Selector options * @param {Object|String} props Selector properties or string identifiers, eg. `{ name: 'my-class', label: 'My class' }`, `.my-cls`
* @param {String} [opts.label=''] Label for the selector, if it's not provided the label will be the same as the name * @param {Object} [opts] Selector options
* @param {String} [opts.type=1] Type of the selector. At the moment, only 'class' (1) is available * @return {[Selector]}
* @return {Model|Array}
* @example * @example
* const selector = selectorManager.add('selectorName'); * const selector = selectorManager.add({ name: 'my-class', label: 'My class' });
* console.log(selector.toString()) // `.my-class`
* // Same as * // Same as
* const selector = selectorManager.add('selectorName', { * const selector = selectorManager.add('.my-class');
* type: 1, * console.log(selector.toString()) // `.my-class`
* label: 'selectorName'
* });
* // Multiple selectors
* const selectors = selectorManager.add(['.class1', '.class2', '#id1']);
* */ * */
add(name, opts = {}) { add(props, opts = {}) {
if (isArray(name)) { const cOpts = isString(props) ? {} : opts;
return name.map(item => this.addSelector(item, opts)); // Keep support for arrays but avoid it in docs
if (isArray(props)) {
return props.map(item => this.addSelector(item, opts, cOpts));
} else { } else {
return this.addSelector(name, opts); return this.addSelector(props, opts, cOpts);
} }
}, },
@ -234,6 +270,7 @@ export default config => {
* Add class selectors * Add class selectors
* @param {Array|string} classes Array or string of classes * @param {Array|string} classes Array or string of classes
* @return {Array} Array of added selectors * @return {Array} Array of added selectors
* @private
* @example * @example
* sm.addClass('class1'); * sm.addClass('class1');
* sm.addClass('class1 class2'); * sm.addClass('class1 class2');
@ -252,16 +289,16 @@ export default config => {
}, },
/** /**
* Get the selector by its name * Get the selector by its name/type
* @param {String|Array} name Selector name * @param {String} name Selector name or string identifier
* @param {String} type Selector type * @returns {[Selector]|null}
* @return {Model|Array}
* @example * @example
* const selector = selectorManager.get('selectorName'); * const selector = selectorManager.get('.my-class');
* // or get an array * // Get Id
* const selectors = selectorManager.get(['class1', 'class2']); * const selectorId = selectorManager.get('#my-id');
* */ * */
get(name, type) { get(name, type) {
// Keep support for arrays but avoid it in docs
if (isArray(name)) { if (isArray(name)) {
const result = []; const result = [];
const selectors = name const selectors = name
@ -272,25 +309,150 @@ export default config => {
); );
return result; return result;
} else { } 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 * Get all selectors
* @return {Collection} * @name getAll
* @function
* @return {Collection<[Selector]>}
* */ * */
getAll() {
return selectors;
},
/** /**
* Return escaped selector name * Return escaped selector name
* @param {String} name Selector name to escape * @param {String} name Selector name to escape
* @returns {String} Escaped name * @returns {String} Escaped name
* @private
*/ */
escapeName(name) { escapeName(name) {
const { escapeName } = c; const { escapeName } = this.getConfig();
return escapeName ? escapeName(name) : Selector.escapeName(name); return escapeName ? escapeName(name) : Selector.escapeName(name);
}, },
@ -302,23 +464,52 @@ export default config => {
*/ */
render(selectors) { render(selectors) {
const { em, selectorTags } = this; const { em, selectorTags } = this;
selectorTags && selectorTags.remove(); const config = this.getConfig();
const el = selectorTags && selectorTags.el;
this.selected.reset(selectors);
this.selectorTags = new ClassTagsView({ this.selectorTags = new ClassTagsView({
collection: new Selectors(selectors || [], { em, config: c }), el,
config: c collection: this.selected,
module: this,
config
}); });
return this.selectorTags.render().el; return this.selectorTags.render().el;
}, },
destroy() { destroy() {
const { selectorTags } = this; const { selectorTags, model } = this;
selectors.reset(); model.stopListening();
selectors.stopListening(); this.__destroy();
selectorTags && selectorTags.remove(); selectorTags && selectorTags.remove();
[c, selectors].forEach(i => (i = {}));
this.em = {};
this.selectorTags = {}; 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'; import { result, forEach, keys } from 'underscore';
const TYPE_CLASS = 1; const TYPE_CLASS = 1;
const TYPE_ID = 2; const TYPE_ID = 2;
const { Model } = Backbone;
const Selector = Model.extend( /**
{ * @typedef Selector
idAttribute: 'name', * @property {String} name Selector name, eg. `my-class`
* @property {String} label Selector label, eg. `My Class`
defaults: { * @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: '', name: '',
label: '', label: '',
// Type of the selector
type: TYPE_CLASS, type: TYPE_CLASS,
// If not active it's not selectable by the style manager (uncheckboxed)
active: true, active: true,
// Can't be seen by the style manager, therefore even by the user
// Will be rendered only in export code
private: false, private: false,
protected: false,
_undo: true
};
}
// If true, can't be removed from the attacched element initialize(props, opts = {}) {
protected: false const { config = {} } = opts;
}, const name = this.get('name');
const label = this.get('label');
initialize(props, opts = {}) { if (!name) {
const { config = {} } = opts; this.set('name', label);
const name = this.get('name'); } else if (!label) {
const label = this.get('label'); this.set('label', name);
}
if (!name) { const namePreEsc = this.get('name');
this.set('name', label); const { escapeName } = config;
} else if (!label) { const nameEsc = escapeName
this.set('label', name); ? escapeName(namePreEsc)
} : Selector.escapeName(namePreEsc);
this.set('name', nameEsc);
this.em = config.em;
}
const namePreEsc = this.get('name'); isId() {
const { escapeName } = config; return this.get('type') === TYPE_ID;
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;
}
return init + (escape ? escape(name) : name); isClass() {
}, return this.get('type') === TYPE_CLASS;
}
toJSON(opts = {}) { getFullName(opts = {}) {
const { em } = this; const { escape } = opts;
let obj = Model.prototype.toJSON.call(this, [opts]); const name = this.get('name');
const defaults = result(this, 'defaults'); let pfx = '';
switch (this.get('type')) {
case TYPE_CLASS:
pfx = '.';
break;
case TYPE_ID:
pfx = '#';
break;
}
if (em && em.getConfig('avoidDefaults')) { return pfx + (escape ? escape(name) : name);
forEach(defaults, (value, key) => { }
if (obj[key] === value) {
delete obj[key];
}
});
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) { if (em && em.getConfig('avoidDefaults')) {
obj = this.getFullName(); forEach(defaults, (value, key) => {
if (obj[key] === value) {
delete obj[key];
} }
});
if (obj.label === obj.name) {
delete obj.label;
} }
return obj; const objLen = keys(obj).length;
}
}, if (objLen === 1 && obj.name) {
{ obj = obj.name;
// All type selectors: https://developer.mozilla.org/it/docs/Web/CSS/CSS_Selectors }
// Here I define only what I need
TYPE_CLASS, if (objLen === 2 && obj.name && obj.type) {
obj = this.getFullName();
TYPE_ID, }
/**
* Escape string
* @param {string} name
* @return {string}
* @private
*/
escapeName(name) {
return `${name}`.trim().replace(/([^a-z0-9\w-\:]+)/gi, '-');
} }
}
);
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 { filter } from 'underscore';
import Backbone from 'backbone'; import { Collection } from 'common';
import Selector from './Selector'; import Selector from './Selector';
export default Backbone.Collection.extend({ export default class Selectors extends Collection {
model: Selector, modelId(attr) {
return `${attr.name}_${attr.type || Selector.TYPE_CLASS}`;
modelId: attr => `${attr.name}_${attr.type || Selector.TYPE_CLASS}`, }
getStyleable() { getStyleable() {
return filter( return filter(
this.models, this.models,
item => item.get('active') && !item.get('private') item => item.get('active') && !item.get('private')
); );
}, }
getValid({ noDisabled } = {}) { getValid({ noDisabled } = {}) {
return filter(this.models, item => !item.get('private')).filter(item => return filter(this.models, item => !item.get('private')).filter(item =>
noDisabled ? item.get('active') : 1 noDisabled ? item.get('active') : 1
); );
}, }
getFullString(collection, opts = {}) { getFullString(collection, opts = {}) {
const result = []; const result = [];
@ -26,4 +26,6 @@ export default Backbone.Collection.extend({
coll.forEach(selector => result.push(selector.getFullName(opts))); coll.forEach(selector => result.push(selector.getFullName(opts)));
return result.join('').trim(); 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' 'focusout [data-tag-name]': 'endEditTag'
}, },
initialize(o) { initialize(o = {}) {
const config = o.config || {}; const config = o.config || {};
this.config = config; this.config = config;
this.module = o.module;
this.coll = o.coll || null; this.coll = o.coll || null;
this.pfx = config.stylePrefix || ''; this.pfx = config.stylePrefix || '';
this.ppfx = config.pStylePrefix || ''; this.ppfx = config.pStylePrefix || '';
@ -97,11 +98,7 @@ export default Backbone.View.extend({
* @private * @private
*/ */
removeTag() { removeTag() {
const { em, model } = this; this.module.removeSelected(this.model);
const targets = em && em.getSelectedAll();
targets.forEach(sel => {
!model.get('protected') && sel && sel.getSelectors().remove(model);
});
}, },
/** /**

107
src/selector_manager/view/ClassTagsView.js

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

2
src/style_manager/config/config.js

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

131
src/style_manager/index.js

@ -30,11 +30,24 @@
* * [getTypes](#gettypes) * * [getTypes](#gettypes)
* * [createType](#createtype) * * [createType](#createtype)
* *
* [Sector]: sector.html
* [CssRule]: css_rule.html
* [Component]: component.html
*
* @module StyleManager * @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 defaults from './config/config';
import Sector from './model/Sector';
import Sectors from './model/Sectors'; import Sectors from './model/Sectors';
import Properties from './model/Properties'; import Properties from './model/Properties';
import PropertyFactory from './model/PropertyFactory'; import PropertyFactory from './model/PropertyFactory';
@ -46,6 +59,10 @@ export default () => {
var sectors, SectView; var sectors, SectView;
return { return {
...Module,
Sector,
PropertyFactory: PropertyFactory(), PropertyFactory: PropertyFactory(),
/** /**
@ -71,10 +88,21 @@ export default () => {
init(config) { init(config) {
c = { ...defaults, ...config }; c = { ...defaults, ...config };
const ppfx = c.pStylePrefix; const ppfx = c.pStylePrefix;
this.em = c.em; const { em } = c;
this.em = em;
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix; if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
properties = new Properties(); properties = new Properties();
sectors = new Sectors([], c); 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; return this;
}, },
@ -89,34 +117,35 @@ export default () => {
if (elTo) { if (elTo) {
const el = isElement(elTo) ? elTo : document.querySelector(elTo); const el = isElement(elTo) ? elTo : document.querySelector(elTo);
if (!el) return this.__logWarn('"appendTo" element not found');
el.appendChild(this.render()); el.appendChild(this.render());
} }
}, },
/** /**
* Add new sector to the collection. If the sector with the same id already exists, * 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 {string} id Sector id
* @param {Object} sector Object representing sector * @param {Object} sector Object representing sector
* @param {string} [sector.name=''] Sector's label * @param {string} [sector.name=''] Sector's label
* @param {Boolean} [sector.open=true] Indicates if the sector should be opened * @param {Boolean} [sector.open=true] Indicates if the sector should be opened
* @param {Array<Object>} [sector.properties=[]] Array of properties * @param {Array<Object>} [sector.properties=[]] Array of properties
* @param {Object} [options={}] Options * @param {Object} [options={}] Options
* @return {Sector} Added Sector * @return {[Sector]} Added Sector
* @example * @example
* var sector = styleManager.addSector('mySector',{ * const sector = styleManager.addSector('mySector',{
* name: 'My sector', * name: 'My sector',
* open: true, * open: true,
* properties: [{ name: 'My property'}] * properties: [{ name: 'My property'}]
* }, { at: 0 }); * }, { at: 0 });
* // With `at: 0` we place the new sector at the beginning of the collection * // With `at: 0` we place the new sector at the beginning of the collection
* */ * */
addSector(id, sector, opts = {}) { addSector(id, sector, options = {}) {
let result = this.getSector(id); let result = this.getSector(id);
if (!result) { if (!result) {
sector.id = id; sector.id = id;
result = sectors.add(sector, opts); result = sectors.add(sector, options);
} }
return result; return result;
@ -125,20 +154,20 @@ export default () => {
/** /**
* Get sector by id * Get sector by id
* @param {string} id Sector id * @param {string} id Sector id
* @return {Sector|null} * @return {[Sector]|null}
* @example * @example
* var sector = styleManager.getSector('mySector'); * const sector = styleManager.getSector('mySector');
* */ * */
getSector(id, opts = {}) { getSector(id, opts = {}) {
const res = sectors.where({ id })[0]; const res = sectors.where({ id })[0];
!res && opts.warn && this._logNoSector(id); !res && opts.warn && this._logNoSector(id);
return res; return res || null;
}, },
/** /**
* Remove a sector by id * Remove a sector by id
* @param {string} id Sector id * @param {string} id Sector id
* @return {Sector} Removed sector * @return {[Sector]} Removed sector
* @example * @example
* const removed = styleManager.removeSector('mySector'); * const removed = styleManager.removeSector('mySector');
*/ */
@ -148,10 +177,16 @@ export default () => {
/** /**
* Get all sectors * Get all sectors
* @return {Sectors} Collection of sectors * @returns {Collection<[Sector]>} Collection of sectors
* @example
* const sectors = styleManager.getSectors();
* */ * */
getSectors() { getSectors(opts = {}) {
return sectors; return sectors && sectors.models
? opts.array
? [...sectors.models]
: sectors
: [];
}, },
/** /**
@ -264,7 +299,7 @@ export default () => {
const classes = model.get('classes'); const classes = model.get('classes');
const id = model.getId(); const id = model.getId();
if (em) { if (em && classes) {
const config = em.getConfig(); const config = em.getConfig();
const um = em.get('UndoManager'); const um = em.get('UndoManager');
const cssC = em.get('CssComposer'); const cssC = em.get('CssComposer');
@ -308,7 +343,7 @@ export default () => {
const { em } = c; const { em } = c;
let result = []; let result = [];
if (em) { if (em && target) {
const cssC = em.get('CssComposer'); const cssC = em.get('CssComposer');
const cssGen = em.get('CodeManager').getGenerator('css'); const cssGen = em.get('CodeManager').getGenerator('css');
const all = cssC 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. * Select different target for the Style Manager.
* It could be a Component, CSSRule, or a string of any CSS selector * It could be a Component, CSSRule, or a string of any CSS selector
* @param {Component|CSSRule|String} target * @param {[Component]|[CSSRule]|String} target
* @return {Styleable} A Component or CSSRule * @return {Array<[Component]|[CSSRule]>} Array containing selected Components or CSSRules
* @private
*/ */
setTarget(target, opts) { select(target, opts = {}) {
return SectView.setTarget(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() { getEmitter() {

478
src/style_manager/model/Property.js

@ -1,234 +1,276 @@
import Backbone from 'backbone'; import { Model } from 'common';
import { isUndefined, isString } from 'underscore'; import { isUndefined, isString, result } from 'underscore';
import { capitalize } from 'utils/mixins'; import { capitalize } from 'utils/mixins';
const Property = Backbone.Model.extend( export default class Property extends Model {
{ initialize(props = {}, opts = {}) {
defaults: { const id = this.get('id') || '';
name: '', const name = this.get('name') || '';
property: '', !this.get('property') &&
type: '', this.set('property', (name || id).replace(/ /g, '-'));
defaults: '', const prop = this.get('property');
info: '', !this.get('id') && this.set('id', prop);
value: '', !name && this.set('name', capitalize(prop).replace(/-/g, ' '));
icon: '', Property.callInit(this, props, opts);
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;
}
if (!this.get('functionName') && !opts.complete) { init() {}
return result;
}
const args = []; /**
let valueStr = `${result.value}`; * Get property id.
let start = valueStr.indexOf('(') + 1; * @returns {String}
let end = valueStr.lastIndexOf(')'); */
const functionName = valueStr.substring(0, start - 1); getId() {
if (functionName) result.functionName = functionName; return this.get('id');
args.push(start); }
// Will try even if the last closing parentheses is not found
if (end >= 0) {
args.push(end);
}
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); * Get property name (usually is the CSS property name).
result.unit = result.value.replace(num, ''); * @returns {String}
result.value = num; */
} 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; 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 args = [];
const fnParameter = let valueStr = `${result.value}`;
fn === 'url' ? `'${value.replace(/'/g, '')}'` : value; let start = valueStr.indexOf('(') + 1;
value = `${fn}(${fnParameter})`; 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')) { result.value = String.prototype.substring.apply(valueStr, args);
value = `${value} !important`;
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;
} }
},
{ if (fn && hasValue) {
callParentInit(property, ctx, props, opts = {}) { const fnParameter = fn === 'url' ? `'${value.replace(/'/g, '')}'` : value;
property.prototype.initialize.apply(ctx, [ value = `${fn}(${fnParameter})`;
props,
{
...opts,
skipInit: 1
}
]);
},
callInit(context, props, opts = {}) {
!opts.skipInit && context.init(props, opts);
} }
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: '' 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 = {}) { initialize(props = {}, opts = {}) {
Property.callParentInit(Property, this, props, opts); Property.callParentInit(Property, this, props, opts);
const unit = this.get('unit'); 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 { extend } from 'underscore';
import Properties from './Properties'; import Properties from './Properties';
import PropertyFactory from './PropertyFactory'; import PropertyFactory from './PropertyFactory';
export default Backbone.Model.extend({ /**
defaults: { * @typedef Sector
id: '', * @property {String} id Sector id, eg. `typography`
name: '', * @property {String} name Sector name, eg. `Typography`
open: true, * @property {Boolean} [open=true] Indicates the open state.
buildProps: '', * @property {Array<Object>} [properties=[]] Indicate an array of Property defintions
extendBuilded: 1, *
properties: [] * [Property]: property.html
}, */
export default class Sector extends Model {
defaults() {
return {
id: '',
name: '',
open: true,
buildProps: '',
extendBuilded: 1,
properties: []
};
}
initialize(opts) { initialize(opts) {
const o = opts || {}; const o = opts || {};
@ -26,7 +37,56 @@ export default Backbone.Model.extend({
const propsModel = new Properties(props); const propsModel = new Properties(props);
propsModel.sector = this; propsModel.sector = this;
this.set('properties', propsModel); 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 * Extend properties
@ -72,7 +132,7 @@ export default Backbone.Model.extend({
} }
return ex ? isolated.filter(i => i) : props; return ex ? isolated.filter(i => i) : props;
}, }
/** /**
* Build properties * Build properties
@ -92,4 +152,4 @@ export default Backbone.Model.extend({
return r; 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'; import Sector from './Sector';
export default Backbone.Collection.extend({ export default class Sectors extends Collection {
model: Sector,
initialize() { initialize() {
this.listenTo(this, 'reset', this.onReset); this.listenTo(this, 'reset', this.onReset);
}, }
onReset(models, opts = {}) { onReset(models, opts = {}) {
const prev = opts.previousModels || []; const prev = opts.previousModels || [];
prev.forEach(sect => sect.get('properties').reset()); 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). // since the set function will not run (default color is black).
updateUI(); updateUI();
currentPreferredFormat = currentPreferredFormat =
opts.preferredFormat || tinycolor(initialColor).format; opts.preferredFormat || tinycolor(initialColor).getFormat();
addColorToSelectionPalette(initialColor); addColorToSelectionPalette(initialColor);
} else { } else {

55
src/utils/Droppable.js

@ -38,10 +38,46 @@ export default class Droppable {
methods[method](el, 'dragleave', this.handleDragLeave); 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) { endDrop(cancel, ev) {
const { em, dragStop } = this; const { em, dragStop } = this;
this.counter = 0; this.counter = 0;
dragStop && dragStop(cancel); dragStop && dragStop(cancel);
this.__customTglEff(false);
em.trigger('canvas:dragend', ev); em.trigger('canvas:dragend', ev);
} }
@ -104,7 +140,8 @@ export default class Droppable {
itemSel: '*', itemSel: '*',
pfx: 'gjs-', pfx: 'gjs-',
onEndMove: model => this.handleDragEnd(model, dt), onEndMove: model => this.handleDragEnd(model, dt),
document: this.el.ownerDocument document: this.el.ownerDocument,
...(this.sortOpts || {})
}); });
sorter.setDropContent(content); sorter.setDropContent(content);
sorter.startSort(); sorter.startSort();
@ -154,12 +191,12 @@ export default class Droppable {
this.endDrop(!content, ev); this.endDrop(!content, ev);
} }
getContentByData(dataTransfer) { getContentByData(dt) {
const em = this.em; const em = this.em;
const types = dataTransfer.types; const types = dt && dt.types;
const files = dataTransfer.files || []; const files = (dt && dt.files) || [];
const dragContent = em.get('dragContent'); const dragContent = em.get('dragContent');
let content = dataTransfer.getData('text'); let content = dt && dt.getData('text');
if (files.length) { if (files.length) {
content = []; content = [];
@ -178,9 +215,7 @@ export default class Droppable {
} else if (dragContent) { } else if (dragContent) {
content = dragContent; content = dragContent;
} else if (indexOf(types, 'text/html') >= 0) { } else if (indexOf(types, 'text/html') >= 0) {
content = dataTransfer content = dt && dt.getData('text/html').replace(/<\/?meta[^>]*>/g, '');
.getData('text/html')
.replace(/<\/?meta[^>]*>/g, '');
} else if (indexOf(types, 'text/uri-list') >= 0) { } else if (indexOf(types, 'text/uri-list') >= 0) {
content = { content = {
type: 'link', type: 'link',
@ -188,7 +223,7 @@ export default class Droppable {
content: content content: content
}; };
} else if (indexOf(types, 'text/json') >= 0) { } 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)); json && (content = JSON.parse(json));
} else if (types.length === 1 && types[0] === 'text/plain') { } else if (types.length === 1 && types[0] === 'text/plain') {
// Avoid dropping non-selectable and non-editable text nodes inside the editor // Avoid dropping non-selectable and non-editable text nodes inside the editor
@ -196,7 +231,7 @@ export default class Droppable {
} }
const result = { content }; const result = { content };
em.trigger('canvas:dragdata', dataTransfer, result); em.trigger('canvas:dragdata', dt, result);
return 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.freezeClass = o.freezeClass || this.pfx + 'freezed';
this.onStart = o.onStart || ''; this.onStart = o.onStart || '';
this.onEndMove = o.onEndMove || ''; this.onEndMove = o.onEndMove || '';
this.customTarget = o.customTarget;
this.onEnd = o.onEnd;
this.direction = o.direction || 'v'; // v (vertical), h (horizontal), a (auto) this.direction = o.direction || 'v'; // v (vertical), h (horizontal), a (auto)
this.onMoveClb = o.onMove || ''; this.onMoveClb = o.onMove || '';
this.relative = o.relative || 0; this.relative = o.relative || 0;
@ -329,6 +331,7 @@ export default Backbone.View.extend({
on(docs, 'keydown', this.rollback); on(docs, 'keydown', this.rollback);
onStart && onStart &&
onStart({ onStart({
sorter: this,
target: srcModel, target: srcModel,
parent: srcModel && srcModel.parent(), parent: srcModel && srcModel.parent(),
index: srcModel && srcModel.index() index: srcModel && srcModel.index()
@ -426,7 +429,7 @@ export default Backbone.View.extend({
* */ * */
onMove(e) { onMove(e) {
const ev = e; const ev = e;
const { em, onMoveClb, plh } = this; const { em, onMoveClb, plh, customTarget } = this;
this.moved = 1; this.moved = 1;
// Turn placeholder visibile // Turn placeholder visibile
@ -454,13 +457,15 @@ export default Backbone.View.extend({
//var targetNew = this.getTargetFromEl(e.target); //var targetNew = this.getTargetFromEl(e.target);
const sourceModel = this.getSourceModel(); 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 target = this.target;
const targetModel = target && this.getTargetModel(target); const targetModel = target && this.getTargetModel(target);
this.selectTargetModel(targetModel, sourceModel); this.selectTargetModel(targetModel, sourceModel);
if (!targetModel) plh.style.display = 'none'; if (!targetModel) plh.style.display = 'none';
if (!target) return; if (!target) return;
this.lastDims = dims; this.lastDims = dims;
const pos = this.findPosition(dims, rX, rY); const pos = this.findPosition(dims, rX, rY);
@ -821,16 +826,16 @@ export default Backbone.View.extend({
*/ */
getDim(el) { getDim(el) {
const { em, canvasRelative } = this; 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) { if (canvasRelative && em) {
const canvas = em.get('Canvas');
const pos = canvas.getElementPos(el, { noScroll: 1 }); const pos = canvas.getElementPos(el, { noScroll: 1 });
const elOffsets = canvas.getElementOffsets(el); top = pos.top; // - offsets.marginTop;
top = pos.top - elOffsets.marginTop; left = pos.left; // - offsets.marginLeft;
left = pos.left - elOffsets.marginLeft; height = pos.height; // + offsets.marginTop + offsets.marginBottom;
height = pos.height + elOffsets.marginTop + elOffsets.marginBottom; width = pos.width; // + offsets.marginLeft + offsets.marginRight;
width = pos.width + elOffsets.marginLeft + elOffsets.marginRight;
} else { } else {
var o = this.offset(el); var o = this.offset(el);
top = this.relative top = this.relative
@ -843,7 +848,7 @@ export default Backbone.View.extend({
width = el.offsetWidth; 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 if (dir == 'h') dir = false;
else dir = this.isInFlow(el, trg); else dir = this.isInFlow(el, trg);
dim.push(dir, el, elIndex); dim.dir = dir;
dim.el = el;
dim.indexEl = elIndex;
dims.push(dim); dims.push(dim);
}); });
@ -898,10 +905,10 @@ export default Backbone.View.extend({
var off = this.borderOffset; var off = this.borderOffset;
var x = rX || 0; var x = rX || 0;
var y = rY || 0; var y = rY || 0;
var t = dim[0]; var t = dim.top;
var l = dim[1]; var l = dim.left;
var h = dim[2]; var h = dim.height;
var w = dim[3]; var w = dim.width;
if (t + off > y || y > t + h - off || l + off > x || x > l + w - off) if (t + off > y || y > t + h - off || l + off > x || x > l + w - off)
result = 1; result = 1;
@ -928,25 +935,26 @@ export default Backbone.View.extend({
// Each dim is: Top, Left, Height, Width // Each dim is: Top, Left, Height, Width
for (var i = 0, len = dims.length; i < len; i++) { for (var i = 0, len = dims.length; i < len; i++) {
dim = dims[i]; dim = dims[i];
const { top, left, height, width } = dim;
// Right position of the element. Left + Width // Right position of the element. Left + Width
dimRight = dim[1] + dim[3]; dimRight = left + width;
// Bottom position of the element. Top + Height // Bottom position of the element. Top + Height
dimDown = dim[0] + dim[2]; dimDown = top + height;
// X center position of the element. Left + (Width / 2) // 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) // Y center position of the element. Top + (Height / 2)
yCenter = dim[0] + dim[2] / 2; yCenter = top + height / 2;
// Skip if over the limits // Skip if over the limits
if ( if (
(xLimit && dim[1] > xLimit) || (xLimit && left > xLimit) ||
(yLimit && yCenter >= yLimit) || // >= avoid issue with clearfixes (yLimit && yCenter >= yLimit) || // >= avoid issue with clearfixes
(leftLimit && dimRight < leftLimit) (leftLimit && dimRight < leftLimit)
) )
continue; continue;
result.index = i; result.index = i;
result.indexEl = dim[6]; result.indexEl = dim.indexEl;
// If it's not in flow (like 'float' element) // If it's not in flow (like 'float' element)
if (!dim[4]) { if (!dim.dir) {
if (posY < dimDown) yLimit = dimDown; if (posY < dimDown) yLimit = dimDown;
//If x lefter than center //If x lefter than center
if (posX < xCenter) { if (posX < xCenter) {
@ -972,7 +980,7 @@ export default Backbone.View.extend({
* @param {HTMLElement} phl * @param {HTMLElement} phl
* @param {Array<Array>} dims * @param {Array<Array>} dims
* @param {Object} pos Position object * @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) { movePlaceholder(plh, dims, pos, trgDim) {
var marg = 0, var marg = 0,
@ -983,7 +991,7 @@ export default Backbone.View.extend({
un = 'px', un = 'px',
margI = 5, margI = 5,
method = pos.method; method = pos.method;
var elDim = dims[pos.index]; const elDim = dims[pos.index];
// Placeholder orientation // Placeholder orientation
plh.classList.remove('vertical'); plh.classList.remove('vertical');
@ -991,29 +999,34 @@ export default Backbone.View.extend({
if (elDim) { if (elDim) {
// If it's not in flow (like 'float' element) // If it's not in flow (like 'float' element)
if (!elDim[4]) { const { top, left, height, width } = elDim;
if (!elDim.dir) {
w = 'auto'; w = 'auto';
h = elDim[2] - marg * 2 + un; h = height - marg * 2 + un;
t = elDim[0] + marg; t = top + marg;
l = method == 'before' ? elDim[1] - marg : elDim[1] + elDim[3] - marg; l = method == 'before' ? left - marg : left + width - marg;
plh.classList.remove('horizontal'); plh.classList.remove('horizontal');
plh.classList.add('vertical'); plh.classList.add('vertical');
} else { } else {
w = elDim[3] + un; w = width + un;
h = 'auto'; h = 'auto';
t = method == 'before' ? elDim[0] - marg : elDim[0] + elDim[2] - marg; t = method == 'before' ? top - marg : top + height - marg;
l = elDim[1]; l = left;
} }
} else { } else {
// Placeholder inside the component
if (!this.nested) { if (!this.nested) {
plh.style.display = 'none'; plh.style.display = 'none';
return; return;
} }
if (trgDim) { if (trgDim) {
t = trgDim[0] + margI; const offset = trgDim.offsets || {};
l = trgDim[1] + margI; const pT = offset.paddingTop || margI;
w = parseInt(trgDim[3]) - margI * 2 + un; const pL = offset.paddingLeft || margI;
t = trgDim.top + pT;
l = trgDim.left + pL;
w = parseInt(trgDim.width) - pL * 2 + un;
h = 'auto'; h = 'auto';
} }
} }
@ -1035,6 +1048,7 @@ export default Backbone.View.extend({
const docs = this.getDocuments(); const docs = this.getDocuments();
const container = this.getContainerEl(); const container = this.getContainerEl();
const onEndMove = this.onEndMove; const onEndMove = this.onEndMove;
const onEnd = this.onEnd;
const { target, lastPos } = this; const { target, lastPos } = this;
let srcModel; let srcModel;
off(container, 'mousemove dragover', this.onMove); off(container, 'mousemove dragover', this.onMove);
@ -1084,6 +1098,8 @@ export default Backbone.View.extend({
? moved.forEach(m => onEndMove(m, this, data)) ? moved.forEach(m => onEndMove(m, this, data))
: onEndMove(null, this, { ...data, cancelled: 1 }); : onEndMove(null, this, { ...data, cancelled: 1 });
} }
isFunction(onEnd) && onEnd({ sorter: this });
}, },
/** /**

8
test/setup.js

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

67
test/specs/block_manager/index.js

@ -1,24 +1,30 @@
import BlockManager from 'block_manager'; import Editor from 'editor';
import BlocksView from './view/BlocksView';
describe('BlockManager', () => { describe('BlockManager', () => {
describe('Main', () => { describe('Main', () => {
var obj; let obj;
var idTest; let idTest;
var optsTest; let optsTest;
let editor;
beforeEach(() => { beforeEach(() => {
editor = new Editor({
blockManager: {
blocks: []
}
}).init();
idTest = 'h1-block'; idTest = 'h1-block';
optsTest = { optsTest = {
label: 'Heading', label: 'Heading',
content: '<h1>Test</h1>' content: '<h1>Test</h1>'
}; };
obj = new BlockManager().init();
obj.postRender(); obj = editor.Blocks;
obj.render();
}); });
afterEach(() => { afterEach(() => {
editor.destroy();
obj = null; obj = null;
}); });
@ -35,7 +41,7 @@ describe('BlockManager', () => {
}); });
test('Add new block', () => { test('Add new block', () => {
var model = obj.add(idTest, optsTest); obj.add(idTest, optsTest);
expect(obj.getAll().length).toEqual(1); expect(obj.getAll().length).toEqual(1);
}); });
@ -64,8 +70,51 @@ describe('BlockManager', () => {
}); });
test('Render blocks', () => { test('Render blocks', () => {
obj.postRender();
obj.render(); obj.render();
expect(obj.getContainer()).toBeTruthy(); 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 Backbone from 'backbone';
import Commands from 'commands'; import Commands from 'commands';
import Models from './model/CommandModels';
import CommandAbstract from './view/CommandAbstract';
describe('Commands', () => { describe('Commands', () => {
describe('Main', () => { 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 CssComposer from 'css_composer';
import e2e from './e2e/CssComposer';
import utils from './../../test_utils.js'; import utils from './../../test_utils.js';
import Editor from 'editor/model/Editor'; import Editor from 'editor/model/Editor';
@ -32,6 +28,12 @@ describe('Css Composer', () => {
config.em = editorModel; config.em = editorModel;
}; };
const getCSS = obj =>
obj
.getAll()
.map(r => r.toCSS())
.join('');
beforeEach(() => { beforeEach(() => {
em = new Editor({}); em = new Editor({});
config = { em }; config = { em };
@ -39,7 +41,7 @@ describe('Css Composer', () => {
}); });
afterEach(() => { afterEach(() => {
obj = null; em.destroy();
}); });
test('Object exists', () => { test('Object exists', () => {
@ -341,5 +343,55 @@ describe('Css Composer', () => {
expect(obj.get(ruleClass.getSelectors())).toBe(ruleClass); expect(obj.get(ruleClass.getSelectors())).toBe(ruleClass);
expect(obj.get(ruleId.getSelectors())).toBe(ruleId); 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' }); var s2 = obj.get('selectors').add({ name: 'test2' });
obj.set('state', 'hover'); obj.set('state', 'hover');
obj.set('mediaText', '1000'); obj.set('mediaText', '1000');
obj.set('atRuleType', 'media');
expect(obj.compare([s1, s2])).toEqual(false); expect(obj.compare([s1, s2])).toEqual(false);
expect(obj.compare([s1, s2], 'hover')).toEqual(false); expect(obj.compare([s1, s2], 'hover')).toEqual(false);
expect(obj.compare([s2, s1], 'hover', '1000')).toEqual(true); 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 Backbone from 'backbone';
import DomComponents from 'dom_components';
import Component from 'dom_components/model/Component'; import Component from 'dom_components/model/Component';
import ComponentImage from 'dom_components/model/ComponentImage'; import ComponentImage from 'dom_components/model/ComponentImage';
import ComponentText from 'dom_components/model/ComponentText'; 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); 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 Selector from 'selector_manager/model/Selector';
import Selectors from 'selector_manager/model/Selectors';
import ClassTagsView from 'selector_manager/view/ClassTagsView';
describe('E2E tests', () => { describe('E2E tests', () => {
var fixtures; let fixtures;
var components; let components;
var tagEl; let tagEl;
var gjs; let gjs;
let module;
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'
});
});
describe('Interaction with Components', () => { describe('Interaction with Components', () => {
beforeEach(() => { 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(); 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', () => { test('Assign correctly new class to component', () => {
@ -85,15 +65,18 @@ describe('E2E tests', () => {
gjs.select(model); gjs.select(model);
tagEl.addNewTag('test'); tagEl.addNewTag('test');
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( expect(
model module
.getSelectors() .getAll()
.at(0) .at(0)
.get('name') .get('name')
).toEqual('test'); ).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', () => { test('Removing from container removes also from selected component', () => {

101
test/specs/selector_manager/index.js

@ -14,6 +14,7 @@ describe('SelectorManager', () => {
afterEach(() => { afterEach(() => {
obj = null; obj = null;
em.destroy();
}); });
test('Object exists', () => { test('Object exists', () => {
@ -38,8 +39,9 @@ describe('SelectorManager', () => {
}); });
test('Default new selector is a class type', () => { 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(obj.get('test').get('type')).toEqual(obj.Selector.TYPE_CLASS);
expect(added.getFullName()).toBe('.test');
}); });
test('Add a selector as an id', () => { test('Add a selector as an id', () => {
@ -48,6 +50,7 @@ describe('SelectorManager', () => {
expect(sel.get('name')).toEqual(name); expect(sel.get('name')).toEqual(name);
expect(sel.get('label')).toEqual(name); expect(sel.get('label')).toEqual(name);
expect(obj.get(`#${name}`).get('type')).toEqual(obj.Selector.TYPE_ID); expect(obj.get(`#${name}`).get('type')).toEqual(obj.Selector.TYPE_ID);
expect(sel.getFullName()).toBe('#test');
}); });
test('Check name property', () => { test('Check name property', () => {
@ -71,9 +74,10 @@ describe('SelectorManager', () => {
}); });
test('Adding 2 selectors with the same name is not possible', () => { test('Adding 2 selectors with the same name is not possible', () => {
obj.add('test'); const add1 = obj.add('test');
obj.add('test'); const add2 = obj.add('test');
expect(obj.getAll().length).toEqual(1); expect(obj.getAll().length).toEqual(1);
expect(add1).toBe(add2);
}); });
test('Add multiple selectors', () => { test('Add multiple selectors', () => {
@ -136,7 +140,51 @@ describe('SelectorManager', () => {
}); });
test('Get empty class', () => { 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', () => { test('addClass single class string', () => {
@ -168,5 +216,50 @@ describe('SelectorManager', () => {
expect(obj.getAll().length).toEqual(1); expect(obj.getAll().length).toEqual(1);
expect(result.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(); coll = new Selectors();
view = new ClassTagsView({ view = new ClassTagsView({
config: { em }, config: { em },
collection: coll collection: coll,
module: em.get('SelectorManager')
}); });
testContext.targetStub = { testContext.targetStub = {
@ -60,6 +61,7 @@ describe('ClassTagsView', () => {
}); });
afterEach(() => { afterEach(() => {
target.destroy();
delete view.collection; delete view.collection;
}); });
@ -133,11 +135,14 @@ describe('ClassTagsView', () => {
expect(testContext.$tags.html()).toEqual(''); expect(testContext.$tags.html()).toEqual('');
}); });
test('Accept new tags', () => { test('Accept new tags', done => {
em.setSelected(compTest); em.setSelected(compTest);
view.addNewTag('test'); view.addNewTag('test');
view.addNewTag('test2'); view.addNewTag('test2');
expect(testContext.$tags.children().length).toEqual(2); setTimeout(() => {
expect(testContext.$tags.children().length).toEqual(2);
done();
});
}); });
test('New tag correctly added', () => { test('New tag correctly added', () => {
@ -162,32 +167,37 @@ describe('ClassTagsView', () => {
expect(testContext.$statesC.css('display')).toEqual(''); 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'); sinon.stub(view, 'updateStateVis');
em.setSelected(compTest); em.setSelected(compTest);
view.addNewTag('test'); 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); em.setSelected(compTest);
view.addNewTag('test'); view.addNewTag('test');
sinon.stub(view, 'updateStateVis'); sinon.stub(view, 'updateStateVis');
coll.remove(coll.at(0)); coll.remove(coll.at(0));
expect(view.updateStateVis.calledOnce).toEqual(true); setTimeout(() => {
expect(view.updateStateVis.calledOnce).toEqual(true);
done();
});
}); });
test('Output correctly state options', () => { test('Output correctly state options', done => {
var view = new ClassTagsView({ target
config: { .get('SelectorManager')
em: target, .setStates([{ name: 'testName', label: 'testLabel' }]);
states: [{ name: 'testName', label: 'testLabel' }] setTimeout(() => {
}, const res =
collection: coll '<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', () => { describe('_commonSelectors', () => {

82
test/specs/style_manager/index.js

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

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

Loading…
Cancel
Save