Browse Source

Merge pull request #3725 from artf/dev

Merge from dev
pull/3968/head
Artur Arseniev 5 years ago
committed by GitHub
parent
commit
46be8ca441
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      dist/css/grapes.min.css
  2. 4
      dist/grapes.min.js
  3. 2
      dist/grapes.min.js.map
  4. 8
      docs/.vuepress/config.js
  5. 85
      docs/api.js
  6. 4
      docs/api/assets.md
  7. 28
      docs/api/block.md
  8. 10
      docs/api/block_manager.md
  9. 144
      docs/api/canvas.md
  10. 6
      docs/api/component.md
  11. 83
      docs/api/css_rule.md
  12. 17
      docs/api/device.md
  13. 125
      docs/api/device_manager.md
  14. 247
      docs/api/editor.md
  15. 40
      docs/api/page.md
  16. 18
      docs/api/pages.md
  17. 102
      docs/api/parser.md
  18. 2
      docs/getting-started.md
  19. 6
      docs/modules/Assets.md
  20. 2
      package.json
  21. 47
      src/asset_manager/index.js
  22. 32
      src/asset_manager/view/AssetImageView.js
  23. 1
      src/asset_manager/view/AssetsView.js
  24. 25
      src/asset_manager/view/FileUploader.js
  25. 14
      src/block_manager/config/config.js
  26. 9
      src/block_manager/index.js
  27. 47
      src/block_manager/model/Block.js
  28. 23
      src/block_manager/view/BlockView.js
  29. 52
      src/block_manager/view/CategoryView.js
  30. 144
      src/canvas/index.js
  31. 15
      src/canvas/model/Canvas.js
  32. 5
      src/canvas/view/CanvasView.js
  33. 9
      src/canvas/view/FrameView.js
  34. 11
      src/canvas/view/FrameWrapView.js
  35. 14
      src/code_manager/model/CodeMirrorEditor.js
  36. 30
      src/code_manager/model/CssGenerator.js
  37. 10
      src/code_manager/model/HtmlGenerator.js
  38. 34
      src/code_manager/view/EditorView.js
  39. 5
      src/commands/view/CommandAbstract.js
  40. 29
      src/commands/view/ComponentDrag.js
  41. 20
      src/commands/view/OpenAssets.js
  42. 3
      src/commands/view/OpenTraitManager.js
  43. 15
      src/commands/view/SelectComponent.js
  44. 1
      src/common/Collection.js
  45. 1
      src/common/Model.js
  46. 2
      src/common/index.js
  47. 63
      src/common/module.js
  48. 11
      src/css_composer/index.js
  49. 126
      src/css_composer/model/CssRule.js
  50. 30
      src/device_manager/config/config.js
  51. 234
      src/device_manager/index.js
  52. 45
      src/device_manager/model/Device.js
  53. 14
      src/device_manager/model/Devices.js
  54. 62
      src/device_manager/view/DevicesView.js
  55. 3
      src/dom_components/config/config.js
  56. 2
      src/dom_components/index.js
  57. 3437
      src/dom_components/model/Component.js
  58. 3
      src/dom_components/model/ComponentFrame.js
  59. 17
      src/dom_components/model/ComponentImage.js
  60. 12
      src/dom_components/model/ComponentLabel.js
  61. 4
      src/dom_components/model/ComponentLink.js
  62. 6
      src/dom_components/model/ComponentMap.js
  63. 10
      src/dom_components/model/ComponentScript.js
  64. 18
      src/dom_components/model/ComponentSvg.js
  65. 9
      src/dom_components/model/ComponentSvgIn.js
  66. 17
      src/dom_components/model/ComponentTable.js
  67. 17
      src/dom_components/model/ComponentTableBody.js
  68. 15
      src/dom_components/model/ComponentTableCell.js
  69. 17
      src/dom_components/model/ComponentTableFoot.js
  70. 17
      src/dom_components/model/ComponentTableHead.js
  71. 7
      src/dom_components/model/ComponentTableRow.js
  72. 10
      src/dom_components/model/ComponentTextNode.js
  73. 24
      src/dom_components/model/ComponentVideo.js
  74. 14
      src/dom_components/view/ComponentImageView.js
  75. 59
      src/dom_components/view/ComponentScriptView.js
  76. 11
      src/dom_components/view/ComponentView.js
  77. 2
      src/dom_components/view/ComponentsView.js
  78. 28
      src/editor/config/config.js
  79. 98
      src/editor/index.js
  80. 105
      src/editor/model/Editor.js
  81. 3
      src/i18n/index.js
  82. 9
      src/index.js
  83. 5
      src/keymaps/index.js
  84. 13
      src/modal_dialog/config/config.js
  85. 17
      src/modal_dialog/index.js
  86. 24
      src/modal_dialog/model/Modal.js
  87. 47
      src/modal_dialog/view/ModalView.js
  88. 19
      src/navigator/index.js
  89. 18
      src/pages/index.js
  90. 23
      src/pages/model/Page.js
  91. 13
      src/panels/index.js
  92. 14
      src/parser/config/config.js
  93. 99
      src/parser/index.js
  94. 80
      src/parser/model/BrowserParserHtml.js
  95. 40
      src/parser/model/ParserHtml.js
  96. 3
      src/rich_text_editor/index.js
  97. 26
      src/selector_manager/index.js
  98. 10
      src/selector_manager/view/ClassTagsView.js
  99. 32
      src/storage_manager/index.js
  100. 52
      src/storage_manager/model/LocalStorage.js

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

4
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

8
docs/.vuepress/config.js

@ -1,6 +1,7 @@
const version = require('./../../package.json').version; const version = require('./../../package.json').version;
const isDev = process.argv[2] === 'dev'; const isDev = process.argv[2] === 'dev';
const devPath = 'http://localhost:8080/dist'; const devPath = 'http://localhost:8080/dist';
const subDivider = " ‍ ‍ ‍ ";
module.exports = { module.exports = {
title: 'GrapesJS', title: 'GrapesJS',
@ -61,20 +62,25 @@ module.exports = {
['/api/canvas', 'Canvas'], ['/api/canvas', 'Canvas'],
['/api/assets', 'Asset Manager'], ['/api/assets', 'Asset Manager'],
['/api/block_manager', 'Block Manager'], ['/api/block_manager', 'Block Manager'],
['/api/block', `${subDivider}Block`],
['/api/commands', 'Commands'], ['/api/commands', 'Commands'],
['/api/components', 'DOM Components'], ['/api/components', 'DOM Components'],
['/api/component', ' - Component'], ['/api/component', `${subDivider}Component`],
['/api/panels', 'Panels'], ['/api/panels', 'Panels'],
['/api/pages', 'Pages'], ['/api/pages', 'Pages'],
['/api/page', `${subDivider}Page`],
['/api/style_manager', 'Style Manager'], ['/api/style_manager', 'Style Manager'],
['/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/selector_manager', 'Selector Manager'], ['/api/selector_manager', 'Selector Manager'],
['/api/css_composer', 'CSS Composer'], ['/api/css_composer', 'CSS Composer'],
['/api/css_rule', `${subDivider}CssRule`],
['/api/modal_dialog', 'Modal'], ['/api/modal_dialog', 'Modal'],
['/api/rich_text_editor', 'Rich Text Editor'], ['/api/rich_text_editor', 'Rich Text Editor'],
['/api/keymaps', 'Keymaps'], ['/api/keymaps', 'Keymaps'],
['/api/undo_manager', 'Undo Manager'], ['/api/undo_manager', 'Undo Manager'],
['/api/parser', 'Parser'],
], ],
'/': [ '/': [
'', '',

85
docs/api.js

@ -1,43 +1,56 @@
// This script uses documentation to generate API Reference files // This script uses documentation to generate API Reference files
const path = require('path'); const path = require('path');
const exec = require('child_process').exec; const documentation = require('documentation');
const fs = require('fs');
const docRoot = __dirname; const docRoot = __dirname;
const srcRoot = path.join(docRoot, '../src/'); const srcRoot = path.join(docRoot, '../src/');
const binRoot = path.join(docRoot, '../node_modules/.bin/');
const cmds = [
['editor/index.js', 'editor.md'],
['asset_manager/index.js', 'assets.md'],
['block_manager/index.js', 'block_manager.md'],
['commands/index.js', 'commands.md'],
['dom_components/index.js', 'components.md'],
['dom_components/model/Component.js', 'component.md'],
['panels/index.js', 'panels.md'],
['style_manager/index.js', 'style_manager.md'],
['storage_manager/index.js', 'storage_manager.md'],
['device_manager/index.js', 'device_manager.md'],
['selector_manager/index.js', 'selector_manager.md'],
['css_composer/index.js', 'css_composer.md'],
['modal_dialog/index.js', 'modal_dialog.md'],
['rich_text_editor/index.js', 'rich_text_editor.md'],
['keymaps/index.js', 'keymaps.md'],
['undo_manager/index.js', 'undo_manager.md'],
['canvas/index.js', 'canvas.md'],
['i18n/index.js', 'i18n.md'],
['pages/index.js', 'pages.md'],
].map(entry =>
`${binRoot}documentation build ${srcRoot}/${entry[0]} -o ${docRoot}/api/${entry[1]} -f md --shallow --markdown-toc false`)
.join(' && ');
console.log('Start API Reference generation...'); const log = (...args) => console.log(...args);
exec(cmds, (error, stdout, stderr) => {
if (error) {
console.error( 'Failed to update API Reference: ', error);
return;
}
stdout.trim().split('\n').forEach(function (line) { async function generateDocs () {
console.info(line); log('Start API Reference generation...');
});
console.log('API Reference generation done!'); await Promise.all([
}); ['editor/index.js', 'editor.md'],
['asset_manager/index.js', 'assets.md'],
['block_manager/index.js', 'block_manager.md'],
['block_manager/model/Block.js', 'block.md'],
['commands/index.js', 'commands.md'],
['dom_components/index.js', 'components.md'],
['dom_components/model/Component.js', 'component.md'],
['panels/index.js', 'panels.md'],
['style_manager/index.js', 'style_manager.md'],
['storage_manager/index.js', 'storage_manager.md'],
['device_manager/index.js', 'device_manager.md'],
['device_manager/model/Device.js', 'device.md'],
['selector_manager/index.js', 'selector_manager.md'],
['css_composer/index.js', 'css_composer.md'],
['css_composer/model/CssRule.js', 'css_rule.md'],
['modal_dialog/index.js', 'modal_dialog.md'],
['rich_text_editor/index.js', 'rich_text_editor.md'],
['keymaps/index.js', 'keymaps.md'],
['undo_manager/index.js', 'undo_manager.md'],
['canvas/index.js', 'canvas.md'],
['i18n/index.js', 'i18n.md'],
['pages/index.js', 'pages.md'],
['pages/model/Page.js', 'page.md'],
['parser/index.js', 'parser.md'],
].map(async (file) =>
documentation.build([`${srcRoot}/${file[0]}`], { shallow: true })
.then(cm => documentation.formats.md(cm, /*{ markdownToc: true }*/))
.then(output => {
const res = output
.replace(/\*\*\\\[/g, '**[')
.replace(/\*\*\(\\\[/g, '**([')
.replace(/<\\\[/g, '<[')
.replace(/\| \\\[/g, '| [')
.replace('**Extends Model**', '');
fs.writeFileSync(`${docRoot}/api/${file[1]}`, res);
log('Created', file[1]);
})
));
log('API Reference generation done!');
};
generateDocs();

4
docs/api/assets.md

@ -160,8 +160,8 @@ Render assets
### Parameters ### Parameters
* `assets` **[array][16]** Assets to render, without the argument will render * `assts`
all global assets * `assets` **[array][16]** Assets to render, without the argument will render all global assets
### Examples ### Examples

28
docs/api/block.md

@ -0,0 +1,28 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## Block
### Properties
* `label` **[String][1]** Block label, eg. `My block`
* `content` **([String][1] | [Object][2])** The content of the block. Might be an HTML string or a [Component Defintion][3]
* `media` **[String][1]?** HTML string for the media/icon of the block, eg. `<svg ...`, `<img ...`, etc.
* `category` **[String][1]?** Block category
* `activate` **[Boolean][4]?** If true, triggers an the `active` event on dropped component
* `select` **[Boolean][4]?** If true, the dropped component will be selected
* `resetId` **[Boolean][4]?** If true, all IDs of dropped components and their styles will be changed
* `disable` **[Boolean][4]?** Disable the block from being interacted
* `onClick` **[Function][5]?** Custom behavior on click, eg. `(block, editor) => editor.getWrapper().append(block.get('content'))`
* `attributes` **[Object][2]?** Block attributes to apply in the view element
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[3]: /modules/Components.html#component-definition
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function

10
docs/api/block_manager.md

@ -28,6 +28,8 @@ const blockManager = editor.BlockManager;
* [getContainer][9] * [getContainer][9]
* [render][10] * [render][10]
[Block]: block.html
## getConfig ## getConfig
Get configuration object Get configuration object
@ -49,7 +51,7 @@ Add new block to the collection.
* `opts.label` **[string][12]** Name of the block * `opts.label` **[string][12]** Name of the block
* `opts.content` **[string][12]** HTML content * `opts.content` **[string][12]** HTML content
* `opts.category` **([string][12] | [Object][11])** Group the block inside a catgegory. * `opts.category` **([string][12] | [Object][11])** Group the block inside a category.
You should pass objects with id property, eg: You should pass objects with id property, eg:
{id: 'some-uid', label: 'My category'} {id: 'some-uid', label: 'My category'}
The string will be converted in: The string will be converted in:
@ -69,7 +71,7 @@ blockManager.add('h1-block', {
}); });
``` ```
Returns **Block** Added block Returns **[Block]** Added block
## get ## get
@ -87,6 +89,8 @@ console.log(JSON.stringify(block));
// {label: 'Heading', content: '<h1>Put your ...', ...} // {label: 'Heading', content: '<h1>Put your ...', ...}
``` ```
Returns **[Block]**
## getAll ## getAll
Return all blocks Return all blocks
@ -123,7 +127,7 @@ const id = 'button';
blockManager.remove(id); blockManager.remove(id);
``` ```
Returns **Block** Removed block Returns **[Block]** Removed block
## getCategories ## getCategories

144
docs/api/canvas.md

@ -29,42 +29,52 @@ const canvas = editor.Canvas;
* [scrollTo][10] * [scrollTo][10]
* [setZoom][11] * [setZoom][11]
* [getZoom][12] * [getZoom][12]
* [getCoords][13]
* [setCoords][14]
[Component]: component.html
## getConfig ## getConfig
Get the configuration object Get the configuration object
Returns **[Object][13]** ### Examples
```javascript
console.log(canvas.getConfig())
```
Returns **[Object][15]** Configuration object
## getElement ## getElement
Get the canvas element Get the canvas element
Returns **[HTMLElement][14]** Returns **[HTMLElement][16]**
## getFrameEl ## getFrameEl
Get the iframe element of the canvas Get the main frame element of the canvas
Returns **[HTMLIFrameElement][15]** Returns **[HTMLIFrameElement][17]**
## getWindow ## getWindow
Get the window instance of the iframe element Get the main frame window instance
Returns **[Window][16]** Returns **[Window][18]**
## getDocument ## getDocument
Get the document of the iframe element Get the main frame document element
Returns **HTMLDocument** Returns **HTMLDocument**
## getBody ## getBody
Get the body of the iframe element Get the main frame body element
Returns **[HTMLBodyElement][17]** Returns **[HTMLBodyElement][19]**
## setCustomBadgeLabel ## setCustomBadgeLabel
@ -72,7 +82,7 @@ Set custom badge naming strategy
### Parameters ### Parameters
* `f` **[Function][18]** * `f` **[Function][20]**
### Examples ### Examples
@ -86,13 +96,13 @@ canvas.setCustomBadgeLabel(function(component){
Get canvas rectangular data Get canvas rectangular data
Returns **[Object][13]** Returns **[Object][15]**
## hasFocus ## hasFocus
Check if the canvas is focused Check if the canvas is focused
Returns **[Boolean][19]** Returns **[Boolean][21]**
## scrollTo ## scrollTo
@ -103,10 +113,10 @@ passed to it. For instance, you can scroll smoothly by using
### Parameters ### Parameters
* `el` **([HTMLElement][14] | Component)** * `el` **([HTMLElement][16] | [Component])**
* `opts` **[Object][13]** Options, same as options for `scrollIntoView` (optional, default `{}`) * `opts` **[Object][15]** Options, same as options for `scrollIntoView` (optional, default `{}`)
* `opts.force` **[Boolean][19]** Force the scroll, even if the element is already visible (optional, default `false`) * `opts.force` **[Boolean][21]** Force the scroll, even if the element is already visible (optional, default `false`)
### Examples ### Examples
@ -120,19 +130,63 @@ canvas.scrollTo(selected, { force: true });
## setZoom ## setZoom
Set zoom value Set canvas zoom value
### Parameters ### Parameters
* `value` **[Number][20]** The zoom value, from 0 to 100 * `value` **[Number][22]** The zoom value, from 0 to 100
### Examples
```javascript
canvas.setZoom(50); // set zoom to 50%
```
Returns **this** Returns **this**
## getZoom ## getZoom
Get zoom value Get canvas zoom value
### Examples
```javascript
canvas.setZoom(50); // set zoom to 50%
const zoom = canvas.getZoom(); // 50
```
Returns **[Number][22]**
## setCoords
Set canvas position coordinates
Returns **[Number][20]** ### Parameters
* `x` **[Number][22]** Horizontal position
* `y` **[Number][22]** Vertical position
### Examples
```javascript
canvas.setCoords(100, 100);
```
Returns **this**
## getCoords
Get canvas position coordinates
### Examples
```javascript
canvas.setCoords(100, 100);
const coords = canvas.getCoords();
// { x: 100, y: 100 }
```
Returns **[Object][15]** Object containing coordinates
## addFrame ## addFrame
@ -140,27 +194,27 @@ Add new frame to the canvas
### Parameters ### Parameters
* `props` **[Object][13]** Frame properties (optional, default `{}`) * `props` **[Object][15]** Frame properties (optional, default `{}`)
* `opts` (optional, default `{}`) * `opts` (optional, default `{}`)
### Examples ### Examples
```javascript ```javascript
editor.Canvas.addFrame({ canvas.addFrame({
name: 'Mobile home page', name: 'Mobile home page',
x: 100, // Position in canvas x: 100, // Position in canvas
y: 100, y: 100,
width: 500, // Frame dimensions width: 500, // Frame dimensions
height: 600, height: 600,
// device: 'DEVICE-ID', // device: 'DEVICE-ID',
components: [ components: [
'<h1 class="testh">Title frame</h1>', '<h1 class="testh">Title frame</h1>',
'<p class="testp">Paragraph frame</p>', '<p class="testp">Paragraph frame</p>',
], ],
styles: ` styles: `
.testh { color: red; } .testh { color: red; }
.testp { color: blue; } .testp { color: blue; }
`, `,
}); });
``` ```
@ -190,18 +244,22 @@ Returns **Frame**
[12]: #getzoom [12]: #getzoom
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object [13]: #getcoords
[14]: #setcoords
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[14]: https://developer.mozilla.org/docs/Web/HTML/Element [16]: https://developer.mozilla.org/docs/Web/HTML/Element
[15]: https://developer.mozilla.org/docs/Web/API/HTMLIFrameElement [17]: https://developer.mozilla.org/docs/Web/API/HTMLIFrameElement
[16]: https://developer.mozilla.org/docs/Web/API/Window [18]: https://developer.mozilla.org/docs/Web/API/Window
[17]: https://developer.mozilla.org/docs/Web/HTML/Element/body [19]: https://developer.mozilla.org/docs/Web/HTML/Element/body
[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function [20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean [21]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number [22]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number

6
docs/api/component.md

@ -2,6 +2,8 @@
## Component ## Component
**Extends Model.extend(Styleable)**
The Component object represents a single node of our template structure, so when you update its properties the changes are The Component object represents a single node of our template structure, so when you update its properties the changes are
immediately reflected on the canvas and in the code to export (indeed, when you ask to export the code we just go through all immediately reflected on the canvas and in the code to export (indeed, when you ask to export the code we just go through all
the tree of nodes). the tree of nodes).
@ -277,6 +279,10 @@ Returns **this**
Get the style of the component Get the style of the component
#### Parameters
* `opts` (optional, default `{}`)
Returns **[Object][2]** Returns **[Object][2]**
### setStyle ### setStyle

83
docs/api/css_rule.md

@ -0,0 +1,83 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## getAtRule
Return the at-rule statement when exists, eg. '@media (...)', '@keyframes'
### Examples
```javascript
const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
atRuleType: 'media',
atRuleParams: '(min-width: 500px)'
});
cssRule.getAtRule(); // "@media (min-width: 500px)"
```
Returns **[String][1]**
## selectorsToString
Return selectors of the rule as a string
### Parameters
* `opts` **[Object][2]?** Options (optional, default `{}`)
* `opts.skipState` **[Boolean][3]?** Skip state from the result
### Examples
```javascript
const cssRule = editor.Css.setRule('.class1:hover', { color: 'red' });
cssRule.selectorsToString(); // ".class1:hover"
cssRule.selectorsToString({ skipState: true }); // ".class1"
```
Returns **[String][1]**
## getDeclaration
Get declaration block (without the at-rule statement)
### Parameters
* `opts` **[Object][2]** Options (same as in `selectorsToString`) (optional, default `{}`)
### Examples
```javascript
const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
atRuleType: 'media',
atRuleParams: '(min-width: 500px)'
});
cssRule.getDeclaration() // ".class1{color:red;}"
```
Returns **[String][1]**
## toCSS
Return the CSS string of the rule
### Parameters
* `opts` **[Object][2]** Options (same as in `getDeclaration`) (optional, default `{}`)
### Examples
```javascript
const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
atRuleType: 'media',
atRuleParams: '(min-width: 500px)'
});
cssRule.toCSS() // "@media (min-width: 500px){.class1{color:red;}}"
```
Returns **[String][1]** CSS 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
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean

17
docs/api/device.md

@ -0,0 +1,17 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## Device
### Properties
* `name` **[String][1]?** Device type, eg. `Mobile`
* `width` **[String][1]?** Width to set for the editor iframe, eg. '900px'
* `height` **[String][1]?** Height to set for the editor iframe, eg. '600px'
* `widthMedia` **[String][1]?** The width which will be used in media queries, If empty the width will be used
* `priority` **[Number][2]?** Setup the order of media queries
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number

125
docs/api/device_manager.md

@ -1,6 +1,6 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. --> <!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## DeviceManager ## Devices
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]
@ -15,67 +15,138 @@ 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 deviceManager = editor.DeviceManager; const deviceManager = editor.Devices;
``` ```
## Available Events
* `device:add` - Added new device. The [Device] is passed as an argument to the callback
* `device:remove` - Device removed. The [Device] is passed as an argument to the callback
* `device:select` - New device selected. The newly selected [Device] and the previous one, are passed as arguments to the callback
* `device:update` - Device updated. The updated [Device] and the object containing changes are passed as arguments to the callback
* `device` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback
## Methods
* [add][2] * [add][2]
* [get][3] * [get][3]
* [getAll][4] * [getDevices][4]
* [remove][5]
* [select][6]
* [getSelected][7]
[Device]: device.html
## add ## add
Add new device to the collection. URLs are supposed to be unique Add new device
### Parameters ### Parameters
* `id` **[String][5]** Device id * `props` **[Object][8]** Device properties
* `width` **[String][5]** Width of the device * `options` (optional, default `{}`)
* `opts` **[Object][6]?** Custom options (optional, default `{}`)
### Examples ### Examples
```javascript ```javascript
deviceManager.add('tablet', '900px'); const device1 = deviceManager.add({
deviceManager.add('tablet2', '900px', { // Without an explicit ID, the `name` will be taken. In case of missing `name`, a random ID will be created.
height: '300px', id: 'tablet',
// At first, GrapesJS tries to localize the name by device id. name: 'Tablet',
// In case is not found, the `name` property is used (or `id` if name is missing) width: '900px', // This width will be applied on the canvas frame and for the CSS media
});
const device2 = deviceManager.add({
id: 'tablet2',
name: 'Tablet 2', name: 'Tablet 2',
widthMedia: '810px', // the width that will be used for the CSS media width: '800px', // This width will be applied on the canvas frame
widthMedia: '810px', // This width that will be used for the CSS media
height: '600px', // Height will be applied on the canvas frame
}); });
``` ```
Returns **Device** Added device Returns **[Device]** Added device
## get ## get
Return device by name Return device by ID
### Parameters ### Parameters
* `name` **[string][5]** Name of the device * `id` **[String][9]** ID of the device
### Examples ### Examples
```javascript ```javascript
var device = deviceManager.get('Tablet'); const device = deviceManager.get('Tablet');
console.log(JSON.stringify(device)); console.log(JSON.stringify(device));
// {name: 'Tablet', width: '900px'} // {name: 'Tablet', width: '900px'}
``` ```
## getAll Returns **([Device] | null)**
## remove
Remove device
### Parameters
* `device` **([String][9] | [Device])** Device or device id
* `opts` (optional, default `{}`)
### Examples
```javascript
const removed = deviceManager.remove('device-id');
// or by passing the Device
const device = deviceManager.get('device-id');
deviceManager.remove(device);
```
Returns **[Device]** Removed device
## getDevices
Return all devices Return all devices
### Examples ### Examples
```javascript ```javascript
var devices = deviceManager.getAll(); const devices = deviceManager.getDevices();
console.log(JSON.stringify(devices)); console.log(JSON.stringify(devices));
// [{name: 'Desktop', width: ''}, ...] // [{name: 'Desktop', width: ''}, ...]
``` ```
Returns **Collection** Returns **[Array][10]<[Device]>**
## select
Change the selected device. This will update the frame in the canvas
### Parameters
* `device` **([String][9] | [Device])** Device or device id
* `opts` (optional, default `{}`)
### Examples
```javascript
deviceManager.select('some-id');
// or by passing the page
const device = deviceManager.get('some-id');
deviceManager.select(device);
```
## getSelected
Get the selected device
### Examples
```javascript
const selected = deviceManager.getSelected();
```
Returns **[Device]**
[1]: https://github.com/artf/grapesjs/blob/master/src/device_manager/config/config.js [1]: https://github.com/artf/grapesjs/blob/master/src/device_manager/config/config.js
@ -83,8 +154,16 @@ Returns **Collection**
[3]: #get [3]: #get
[4]: #getAll [4]: #getdevices
[5]: #remove
[6]: #select
[7]: #getselected
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String [9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object [10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

247
docs/api/editor.md

@ -113,11 +113,6 @@ By changing `result.content` you're able to customize what is dropped
* `modal:open` - Modal is opened * `modal:open` - Modal is opened
* `modal:close` - Modal is closed * `modal:close` - Modal is closed
### Parser
* `parse:html` - On HTML parse, an object containing the input and the output of the parser is passed as an argument
* `parse:css` - On CSS parse, an object containing the input and the output of the parser is passed as an argument
### Commands ### Commands
* `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview')) * `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview'))
@ -128,9 +123,17 @@ 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
### Devices
Check the [Devices][2] module.
### Parser
Check the [Parser][3] module.
### Pages ### Pages
Check the [Pages][2] module. Check the [Pages][4] module.
### General ### General
@ -146,7 +149,7 @@ Returns configuration object
### Parameters ### Parameters
* `prop` **[string][3]?** Property name * `prop` **[string][5]?** 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
@ -157,11 +160,12 @@ Returns HTML built inside canvas
### Parameters ### Parameters
* `opts` **[Object][4]** Options (optional, default `{}`) * `opts` **[Object][6]** Options (optional, default `{}`)
* `opts.cleanId` **[Boolean][5]** Remove unnecessary IDs (eg. those created automatically) (optional, default `false`) * `opts.component` **Component?** Return the HTML of a specific Component
* `opts.cleanId` **[Boolean][7]** Remove unnecessary IDs (eg. those created automatically) (optional, default `false`)
Returns **[string][3]** HTML string Returns **[string][5]** HTML string
## getCss ## getCss
@ -169,17 +173,25 @@ Returns CSS built inside canvas
### Parameters ### Parameters
* `opts` **[Object][4]** Options (optional, default `{}`) * `opts` **[Object][6]** Options (optional, default `{}`)
* `opts.avoidProtected` **[Boolean][5]** Don't include protected CSS (optional, default `false`) * `opts.component` **Component?** Return the CSS of a specific Component
* `opts.json` **[Boolean][7]** Return an array of CssRules instead of the CSS string (optional, default `false`)
* `opts.avoidProtected` **[Boolean][7]** Don't include protected CSS (optional, default `false`)
Returns **[string][3]** CSS string Returns **([String][5] | [Array][8]\<CssRule>)** CSS string or array of CssRules
## getJs ## getJs
Returns JS of all components Returns JS of all components
Returns **[string][3]** JS string ### Parameters
* `opts` **[Object][6]** Options (optional, default `{}`)
* `opts.component` **Component?** Get the JS of a specific component
Returns **[String][5]** JS string
## getComponents ## getComponents
@ -199,8 +211,8 @@ Set components inside editor's canvas. This method overrides actual components
### Parameters ### Parameters
* `components` **([Array][6]<[Object][4]> | [Object][4] | [string][3])** HTML string or components model * `components` **([Array][8]<[Object][6]> | [Object][6] | [string][5])** HTML string or components model
* `opt` **[Object][4]** the options object to be used by the \[setComponents][em#setComponents][7] method (optional, default `{}`) * `opt` **[Object][6]** the options object to be used by the \[setComponents][em#setComponents][9] method (optional, default `{}`)
### Examples ### Examples
@ -222,10 +234,10 @@ Add components
### Parameters ### Parameters
* `components` **([Array][6]<[Object][4]> | [Object][4] | [string][3])** HTML string or components model * `components` **([Array][8]<[Object][6]> | [Object][6] | [string][5])** HTML string or components model
* `opts` **[Object][4]** Options * `opts` **[Object][6]** Options
* `opts.avoidUpdateStyle` **[Boolean][5]** If the HTML string contains styles, * `opts.avoidUpdateStyle` **[Boolean][7]** 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`)
@ -241,13 +253,13 @@ editor.addComponents({
}); });
``` ```
Returns **[Array][6]\<Component>** Returns **[Array][8]\<Component>**
## getStyle ## getStyle
Returns style in JSON format object Returns style in JSON format object
Returns **[Object][4]** Returns **[Object][6]**
## setStyle ## setStyle
@ -255,8 +267,8 @@ Set style inside editor's canvas. This method overrides actual style
### Parameters ### Parameters
* `style` **([Array][6]<[Object][4]> | [Object][4] | [string][3])** CSS string or style model * `style` **([Array][8]<[Object][6]> | [Object][6] | [string][5])** CSS string or style model
* `opt` **[Object][4]** the options object to be used by the \[setStyle][em#setStyle][8] method (optional, default `{}`) * `opt` (optional, default `{}`)
### Examples ### Examples
@ -264,13 +276,30 @@ Set style inside editor's canvas. This method overrides actual style
editor.setStyle('.cls{color: red}'); editor.setStyle('.cls{color: red}');
//or //or
editor.setStyle({ editor.setStyle({
selectors: ['cls'] selectors: ['cls'],
style: { color: 'red' } style: { color: 'red' }
}); });
``` ```
Returns **this** Returns **this**
## addStyle
Add styles to the editor
### Parameters
* `style` **([Array][8]<[Object][6]> | [Object][6] | [string][5])** CSS string or style model
* `opts` (optional, default `{}`)
### Examples
```javascript
editor.addStyle('.cls{color: red}');
```
Returns **[Array][8]\<CssRule>** Array of created CssRule instances
## getSelected ## getSelected
Returns the last selected component, if there is one Returns the last selected component, if there is one
@ -281,7 +310,7 @@ Returns **Model**
Returns an array of all selected components Returns an array of all selected components
Returns **[Array][6]** Returns **[Array][8]**
## getSelectedToStyle ## getSelectedToStyle
@ -299,10 +328,10 @@ Select a component
### Parameters ### Parameters
* `el` **(Component | [HTMLElement][9])** Component to select * `el` **(Component | [HTMLElement][10])** Component to select
* `opts` **[Object][4]?** Options * `opts` **[Object][6]?** Options
* `opts.scroll` **[Boolean][5]?** Scroll canvas to the selected element * `opts.scroll` **[Boolean][7]?** Scroll canvas to the selected element
### Examples ### Examples
@ -321,7 +350,7 @@ Add component to selection
### Parameters ### Parameters
* `el` **(Component | [HTMLElement][9] | [Array][6])** Component to select * `el` **(Component | [HTMLElement][10] | [Array][8])** Component to select
### Examples ### Examples
@ -337,7 +366,7 @@ Remove component from selection
### Parameters ### Parameters
* `el` **(Component | [HTMLElement][9] | [Array][6])** Component to select * `el` **(Component | [HTMLElement][10] | [Array][8])** Component to select
### Examples ### Examples
@ -353,7 +382,7 @@ Toggle component selection
### Parameters ### Parameters
* `el` **(Component | [HTMLElement][9] | [Array][6])** Component to select * `el` **(Component | [HTMLElement][10] | [Array][8])** Component to select
### Examples ### Examples
@ -363,6 +392,21 @@ editor.selectToggle(model);
Returns **this** Returns **this**
## getEditing
Returns, if active, the Component enabled in rich text editing mode.
### Examples
```javascript
const textComp = editor.getEditing();
if (textComp) {
console.log('HTML: ', textComp.toHTML());
}
```
Returns **(Component | null)**
## setDevice ## setDevice
Set device to the editor. If the device exists it will Set device to the editor. If the device exists it will
@ -370,7 +414,7 @@ change the canvas to the proper width
### Parameters ### Parameters
* `name` **[string][3]** Name of the device * `name` **[string][5]** Name of the device
### Examples ### Examples
@ -392,7 +436,7 @@ console.log(device);
// 'Tablet' // 'Tablet'
``` ```
Returns **[string][3]** Device name Returns **[string][5]** Device name
## runCommand ## runCommand
@ -400,8 +444,8 @@ Execute command
### Parameters ### Parameters
* `id` **[string][3]** Command ID * `id` **[string][5]** Command ID
* `options` **[Object][4]** Custom options (optional, default `{}`) * `options` **[Object][6]** Custom options (optional, default `{}`)
### Examples ### Examples
@ -417,8 +461,8 @@ Stop the command if stop method was provided
### Parameters ### Parameters
* `id` **[string][3]** Command ID * `id` **[string][5]** Command ID
* `options` **[Object][4]** Custom options (optional, default `{}`) * `options` **[Object][6]** Custom options (optional, default `{}`)
### Examples ### Examples
@ -434,9 +478,22 @@ Store data to the current storage
### Parameters ### Parameters
* `clb` **[Function][10]** Callback function * `clb` **[Function][11]** Callback function
Returns **[Object][4]** Stored data Returns **[Object][6]** Stored data
## storeData
Get the JSON data object, which could be stored and loaded back with `editor.loadData(json)`
### Examples
```javascript
console.log(editor.storeData());
// { pages: [...], styles: [...], ... }
```
Returns **[Object][6]**
## load ## load
@ -444,23 +501,39 @@ Load data from the current storage
### Parameters ### Parameters
* `clb` **[Function][10]** Callback function * `clb` **[Function][11]** Callback function
Returns **[Object][4]** Stored data Returns **[Object][6]** Stored data
## loadData
Load data from the JSON data object
### Parameters
* `data` **[Object][6]** Data to load
### Examples
```javascript
editor.loadData({ pages: [...], styles: [...], ... })
```
Returns **[Object][6]** 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][9]** Returns **[HTMLElement][10]**
## 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][11]** Returns **[number][12]**
## refresh ## refresh
@ -473,9 +546,9 @@ refresh you'll get misleading position of tools
### Parameters ### Parameters
* `opts` * `opts`
* `options` **[Object][4]?** Options * `options` **[Object][6]?** Options
* `options.tools` **[Boolean][5]** Update the position of tools (eg. rich text editor, component highlighter, etc.) (optional, default `false`) * `options.tools` **[Boolean][7]** Update the position of tools (eg. rich text editor, component highlighter, etc.) (optional, default `false`)
## setCustomRte ## setCustomRte
@ -483,7 +556,7 @@ Replace the built-in Rich Text Editor with a custom one.
### Parameters ### Parameters
* `obj` **[Object][4]** Custom RTE Interface * `obj` **[Object][6]** Custom RTE Interface
### Examples ### Examples
@ -523,7 +596,7 @@ custom parser, pass `null` as the argument
### Parameters ### Parameters
* `parser` **([Function][10] | null)** Parser function * `parser` **([Function][11] | null)** Parser function
### Examples ### Examples
@ -545,11 +618,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][12] To get more about this feature read: [https://github.com/artf/grapesjs/issues/1936][13]
### Parameters ### Parameters
* `value` **[String][3]** Drag mode, options: 'absolute' | 'translate' * `value` **[String][5]** Drag mode, options: 'absolute' | 'translate'
Returns **this** Returns **this**
@ -560,10 +633,10 @@ Trigger event log message
### Parameters ### Parameters
* `msg` **any** Message to log * `msg` **any** Message to log
* `opts` **[Object][4]** Custom options (optional, default `{}`) * `opts` **[Object][6]** Custom options (optional, default `{}`)
* `opts.ns` **[String][3]** Namespace of the log (eg. to use in plugins) (optional, default `''`) * `opts.ns` **[String][5]** Namespace of the log (eg. to use in plugins) (optional, default `''`)
* `opts.level` **[String][3]** Level of the log, `debug`, `info`, `warning`, `error` (optional, default `'debug'`) * `opts.level` **[String][5]** Level of the log, `debug`, `info`, `warning`, `error` (optional, default `'debug'`)
### Examples ### Examples
@ -585,11 +658,11 @@ Translate label
### Parameters ### Parameters
* `args` **...any** * `args` **...any**
* `key` **[String][3]** Label to translate * `key` **[String][5]** Label to translate
* `opts` **[Object][4]?** Options for the translation * `opts` **[Object][6]?** Options for the translation
* `opts.params` **[Object][4]?** Params for the translation * `opts.params` **[Object][6]?** Params for the translation
* `opts.noWarn` **[Boolean][5]?** Avoid warnings in case of missing resources * `opts.noWarn` **[Boolean][7]?** Avoid warnings in case of missing resources
### Examples ### Examples
@ -601,7 +674,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][3]** Returns **[String][5]**
## on ## on
@ -609,8 +682,8 @@ Attach event
### Parameters ### Parameters
* `event` **[string][3]** Event name * `event` **[string][5]** Event name
* `callback` **[Function][10]** Callback function * `callback` **[Function][11]** Callback function
Returns **this** Returns **this**
@ -620,8 +693,8 @@ Attach event and detach it after the first run
### Parameters ### Parameters
* `event` **[string][3]** Event name * `event` **[string][5]** Event name
* `callback` **[Function][10]** Callback function * `callback` **[Function][11]** Callback function
Returns **this** Returns **this**
@ -631,8 +704,8 @@ Detach event
### Parameters ### Parameters
* `event` **[string][3]** Event name * `event` **[string][5]** Event name
* `callback` **[Function][10]** Callback function * `callback` **[Function][11]** Callback function
Returns **this** Returns **this**
@ -642,7 +715,7 @@ Trigger event
### Parameters ### Parameters
* `event` **[string][3]** Event to trigger * `event` **[string][5]** Event to trigger
Returns **this** Returns **this**
@ -654,28 +727,50 @@ Destroy the editor
Render editor Render editor
Returns **[HTMLElement][9]** Returns **[HTMLElement][10]**
## html
Print safe HTML by using ES6 tagged template strings.
### Parameters
* `literals` **[Array][8]<[String][5]>**
* `substs` **[Array][8]<[String][5]>**
### Examples
```javascript
const unsafeStr = '<script>....</script>';
const safeStr = '<b>Hello</b>';
// Use `$${var}` to avoid escaping
const strHtml = editor.html`Escaped ${unsafeStr}, unescaped $${safeStr}`;
```
Returns **[String][5]**
[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/pages.html [2]: /api/device_manager.html
[3]: /api/parser.html
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String [4]: /api/pages.html
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object [5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean [6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array [7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[7]: em#setComponents [8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[8]: em#setStyle [9]: em#setComponents
[9]: https://developer.mozilla.org/docs/Web/HTML/Element [10]: https://developer.mozilla.org/docs/Web/HTML/Element
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function [11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number [12]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[12]: https://github.com/artf/grapesjs/issues/1936 [13]: https://github.com/artf/grapesjs/issues/1936

40
docs/api/page.md

@ -0,0 +1,40 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## getAllFrames
Get all frames
### Examples
```javascript
const arrayOfFrames = page.getAllFrames();
```
Returns **[Array][1]\<Frame>**
## getMainFrame
Get the first frame of the page (identified always as the main one)
### Examples
```javascript
const mainFrame = page.getMainFrame();
```
Returns **Frame**
## getMainComponent
Get the root component (usually is the `wrapper` component) from the main frame
### Examples
```javascript
const rootComponent = page.getMainComponent();
console.log(rootComponent.toHTML());
```
Returns **Component**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

18
docs/api/pages.md

@ -43,6 +43,8 @@ const pageManager = editor.Pages;
* [select][6] * [select][6]
* [getSelected][7] * [getSelected][7]
[Page]: page.html
## add ## add
Add new page Add new page
@ -62,7 +64,7 @@ const newPage = pageManager.add({
}); });
``` ```
Returns **Page** Returns **[Page]**
## remove ## remove
@ -70,7 +72,7 @@ Remove page
### Parameters ### Parameters
* `page` **([String][9] | Page)** Page or page id * `page` **([String][9] | [Page])** Page or page id
* `opts` (optional, default `{}`) * `opts` (optional, default `{}`)
### Examples ### Examples
@ -82,7 +84,7 @@ const somePage = pageManager.get('page-id');
pageManager.remove(somePage); pageManager.remove(somePage);
``` ```
Returns **Page** Returns **[Page]**
## get ## get
@ -98,7 +100,7 @@ Get page by id
const somePage = pageManager.get('page-id'); const somePage = pageManager.get('page-id');
``` ```
Returns **Page** Returns **[Page]**
## getMain ## getMain
@ -110,7 +112,7 @@ Get main page (the first one available)
const mainPage = pageManager.getMain(); const mainPage = pageManager.getMain();
``` ```
Returns **Page** Returns **[Page]**
## getAll ## getAll
@ -122,7 +124,7 @@ Get all pages
const arrayOfPages = pageManager.getAll(); const arrayOfPages = pageManager.getAll();
``` ```
Returns **[Array][10]\<Page>** Returns **[Array][10]<[Page]>**
## select ## select
@ -130,7 +132,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][9] | [Page])** Page or page id
* `opts` (optional, default `{}`) * `opts` (optional, default `{}`)
### Examples ### Examples
@ -154,7 +156,7 @@ Get the selected page
const selectedPage = pageManager.getSelected(); const selectedPage = pageManager.getSelected();
``` ```
Returns **Page** Returns **[Page]**
[1]: #add [1]: #add

102
docs/api/parser.md

@ -0,0 +1,102 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## Parser
You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object][1]
```js
const editor = grapesjs.init({
parser: {
// options
}
})
```
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
```js
const { Parser } = editor;
```
## Available Events
* `parse:html` - On HTML parse, an object containing the input and the output of the parser is passed as an argument
* `parse:css` - On CSS parse, an object containing the input and the output of the parser is passed as an argument
## Methods
* [getConfig][2]
* [parseHtml][3]
* [parseCss][4]
## getConfig
Get the configuration object
### Examples
```javascript
console.log(Parser.getConfig())
```
Returns **[Object][5]** Configuration object
## parseHtml
Parse HTML string and return the object containing the Component Definition
### Parameters
* `input` **[String][6]** HTML string to parse
* `options` **[Object][5]?** Options (optional, default `{}`)
* `options.htmlType` **[String][6]?** [HTML mime type][7] to parse
### Examples
```javascript
const resHtml = Parser.parseHtml(`<table><div>Hi</div></table>`, {
htmlType: 'text/html', // default
});
// By using the `text/html`, this will fix automatically all the HTML syntax issues
// Indeed the final representation, in this case, will be `<div>Hi</div><table></table>`
const resXml = Parser.parseHtml(`<table><div>Hi</div></table>`, {
htmlType: 'application/xml',
});
// This will preserve the original format as, from the XML point of view, is a valid format
```
Returns **[Object][5]** Object containing the result `{ html: ..., css: ... }`
## parseCss
Parse CSS string and return an array of valid definition objects for CSSRules
### Parameters
* `input` **[String][6]** CSS string to parse
### Examples
```javascript
const res = Parser.parseCss('.cls { color: red }');
// [{ ... }]
```
Returns **[Array][8]<[Object][5]>** Array containing the result
[1]: https://github.com/artf/grapesjs/blob/master/src/parser/config/config.js
[2]: #getconfig
[3]: #parsehtml
[4]: #parsecss
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[7]: https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#Argument02
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

2
docs/getting-started.md

@ -117,7 +117,7 @@ If you want to learn more about blocks we suggest to read its dedicated article:
Technically, once you drop your HTML block inside the canvas each element of the content is transformed into a GrapesJS Component. A GrapesJS Component is an object containing information about how the element is rendered in the canvas (managed in the View) and how it might look its final code (created by the properties in the Model). Generally, all Model properties are reflected in the View. Therefore, if you add a new attribute to the model, it will be available in the export code (which we will learn more about later), and the element you see in the canvas will be updated with new attributes. Technically, once you drop your HTML block inside the canvas each element of the content is transformed into a GrapesJS Component. A GrapesJS Component is an object containing information about how the element is rendered in the canvas (managed in the View) and how it might look its final code (created by the properties in the Model). Generally, all Model properties are reflected in the View. Therefore, if you add a new attribute to the model, it will be available in the export code (which we will learn more about later), and the element you see in the canvas will be updated with new attributes.
This isn't totally out of the ordinary, but the unique thing about Components that you can create a totally decoupled View. This means you can show the user whatever you desire regardless of what is in the Model. For example, by dragging a placeholder text you can fetch and show instead a dynamic content. If you want to learn more about Custom Components, you should check out [Component Manager Module](modules/Components.html). This isn't totally out of the ordinary, but the unique thing about Components that you can create a totally decoupled View. This means you can show the user whatever you desire regardless of what is in the Model. For example, by dragging a placeholder text you can fetch and show instead a dynamic content. If you want to learn more about Custom Components, you should check out [Component Manager Module](modules/Components.html).
GrapesJS comes with a few [built-in Components](modules/Components.html#built-in-components) that enable different features once rendered in the canvas. For example, by double clicking on an image component you will see the default [Asset Manager](modules/Assets.html), which you can customize or integrate you own. By double clicking on the text component you're able to edit it via the built-in Rich Text Editor, which is also customization and [replaceable](guides/Replace-Rich-Text-Editor.html). GrapesJS comes with a few [built-in Components](modules/Components.html#built-in-component-types) that enable different features once rendered in the canvas. For example, by double clicking on an image component you will see the default [Asset Manager](modules/Assets.html), which you can customize or integrate you own. By double clicking on the text component you're able to edit it via the built-in Rich Text Editor, which is also customization and [replaceable](guides/Replace-Rich-Text-Editor.html).
As we have seen before you can create Blocks directly as Components As we have seen before you can create Blocks directly as Components
```js ```js

6
docs/modules/Assets.md

@ -140,14 +140,16 @@ const editor = grapesjs.init({
type: 'image', type: 'image',
src: 'http://placehold.it/350x250/459ba8/fff/image2.jpg', src: 'http://placehold.it/350x250/459ba8/fff/image2.jpg',
height: 350, height: 350,
width: 250 width: 250,
name: 'displayName'
}, },
{ {
// As the 'image' is the base type of assets, omitting it will // As the 'image' is the base type of assets, omitting it will
// be set as `image` by default // be set as `image` by default
src: 'http://placehold.it/350x250/79c267/fff/image3.jpg', src: 'http://placehold.it/350x250/79c267/fff/image3.jpg',
height: 350, height: 350,
width: 250 width: 250,
name: 'displayName'
}, },
], ],
} }

2
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.19", "version": "0.17.25",
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",

47
src/asset_manager/index.js

@ -37,7 +37,7 @@ import FileUpload from './view/FileUploader';
export default () => { export default () => {
let c = {}; let c = {};
let assets, am, fu; let assets, assetsVis, am, fu;
return { return {
/** /**
@ -79,15 +79,7 @@ export default () => {
// Global assets collection // Global assets collection
assets = new Assets([]); assets = new Assets([]);
const obj = { assetsVis = new Assets([]);
// Collection visible in asset manager
collection: new Assets([]),
globalCollection: assets,
config: c
};
fu = new FileUpload(obj);
obj.fu = fu;
am = new AssetsView(obj);
// Setup the sync between the global and public collections // Setup the sync between the global and public collections
assets.listenTo(assets, 'add', model => { assets.listenTo(assets, 'add', model => {
@ -159,7 +151,7 @@ export default () => {
* @return {Collection} * @return {Collection}
*/ */
getAllVisible() { getAllVisible() {
return am.collection; return assetsVis;
}, },
/** /**
@ -236,8 +228,7 @@ export default () => {
/** /**
* Render assets * Render assets
* @param {array} assets Assets to render, without the argument will render * @param {array} assets Assets to render, without the argument will render all global assets
* all global assets
* @return {HTMLElement} * @return {HTMLElement}
* @example * @example
* // Render all assets * // Render all assets
@ -249,14 +240,22 @@ export default () => {
* asset => asset.get('category') == 'cats' * asset => asset.get('category') == 'cats'
* )); * ));
*/ */
render(assets) { render(assts) {
const toRender = assets || this.getAll().models; const toRender = assts || this.getAll().models;
if (!am.rendered) { if (!am) {
const obj = {
collection: assetsVis, // Collection visible in asset manager
globalCollection: assets,
config: c
};
fu = new FileUpload(obj);
obj.fu = fu;
am = new AssetsView(obj);
am.render(); am.render();
} }
am.collection.reset(toRender); assetsVis.reset(toRender);
return this.getContainer(); return this.getContainer();
}, },
@ -310,7 +309,7 @@ export default () => {
}, },
postRender(editorView) { postRender(editorView) {
c.dropzone && fu.initDropzone(editorView); c.dropzone && fu && fu.initDropzone(editorView);
}, },
/** /**
@ -319,7 +318,7 @@ export default () => {
* @private * @private
* */ * */
setTarget(m) { setTarget(m) {
am.collection.target = m; assetsVis.target = m;
}, },
/** /**
@ -328,7 +327,7 @@ export default () => {
* @private * @private
* */ * */
onSelect(f) { onSelect(f) {
am.collection.onSelect = f; assetsVis.onSelect = f;
}, },
/** /**
@ -351,9 +350,9 @@ export default () => {
destroy() { destroy() {
assets.reset(); assets.reset();
fu.collection.reset(); assetsVis.reset();
fu.remove(); fu && fu.remove();
am.remove(); am && am.remove();
[assets, am, fu].forEach(i => (i = null)); [assets, am, fu].forEach(i => (i = null));
c = {}; c = {};
} }

32
src/asset_manager/view/AssetImageView.js

@ -1,5 +1,6 @@
import { isFunction } from 'underscore'; import { isFunction } from 'underscore';
import AssetView from './AssetView'; import AssetView from './AssetView';
import html from 'utils/html';
export default AssetView.extend({ export default AssetView.extend({
events: { events: {
@ -9,24 +10,23 @@ export default AssetView.extend({
}, },
getPreview() { getPreview() {
const pfx = this.pfx; const { pfx, ppfx, model } = this;
const src = this.model.get('src'); const src = model.get('src');
return ` return html`
<div class="${pfx}preview" style="background-image: url('${src}');"></div> <div class="${pfx}preview" style="background-image: url('${src}');"></div>
<div class="${pfx}preview-bg ${this.ppfx}checker-bg"></div> <div class="${pfx}preview-bg ${ppfx}checker-bg"></div>
`; `;
}, },
getInfo() { getInfo() {
const pfx = this.pfx; const { pfx, model } = this;
const model = this.model;
let name = model.get('name'); let name = model.get('name');
let width = model.get('width'); let width = model.get('width');
let height = model.get('height'); let height = model.get('height');
let unit = model.get('unitDim'); let unit = model.get('unitDim');
let dim = width && height ? `${width}x${height}${unit}` : ''; let dim = width && height ? `${width}x${height}${unit}` : '';
name = name || model.getFilename(); name = name || model.getFilename();
return ` return html`
<div class="${pfx}name">${name}</div> <div class="${pfx}name">${name}</div>
<div class="${pfx}dimensions">${dim}</div> <div class="${pfx}dimensions">${dim}</div>
`; `;
@ -42,15 +42,16 @@ export default AssetView.extend({
* @private * @private
* */ * */
onClick() { onClick() {
var onClick = this.config.onClick; const { model, pfx } = this;
var model = this.model; const { onClick } = this.config;
this.collection.trigger('deselectAll'); const coll = this.collection;
this.$el.addClass(this.pfx + 'highlight'); coll.trigger('deselectAll');
this.$el.addClass(pfx + 'highlight');
if (isFunction(onClick)) { if (isFunction(onClick)) {
onClick(model); onClick(model);
} else { } else {
this.updateTarget(this.collection.target); this.updateTarget(coll.target);
} }
}, },
@ -60,16 +61,15 @@ export default AssetView.extend({
* */ * */
onDblClick() { onDblClick() {
const { em, model } = this; const { em, model } = this;
const onDblClick = this.config.onDblClick; const { onDblClick } = this.config;
const { target, onSelect } = this.collection;
if (isFunction(onDblClick)) { if (isFunction(onDblClick)) {
onDblClick(model); onDblClick(model);
} else { } else {
this.updateTarget(this.collection.target); this.updateTarget(target);
em && em.get('Modal').close(); em && em.get('Modal').close();
} }
var onSelect = this.collection.onSelect;
isFunction(onSelect) && onSelect(model); isFunction(onSelect) && onSelect(model);
}, },

1
src/asset_manager/view/AssetsView.js

@ -182,7 +182,6 @@ export default Backbone.View.extend({
this.$el.append(fuRendered).append(this.template(this)); this.$el.append(fuRendered).append(this.template(this));
this.el.className = `${this.ppfx}asset-manager`; this.el.className = `${this.ppfx}asset-manager`;
this.renderAssets(); this.renderAssets();
this.rendered = 1;
return this; return this;
} }
}); });

25
src/asset_manager/view/FileUploader.js

@ -1,16 +1,25 @@
import { template } from 'underscore';
import Backbone from 'backbone'; import Backbone from 'backbone';
import fetch from 'utils/fetch'; import fetch from 'utils/fetch';
import html from 'utils/html';
export default Backbone.View.extend( export default Backbone.View.extend(
{ {
template: template(` template({ pfx, title, uploadId, disabled, multiUpload }) {
<form> return html`
<div id="<%= pfx %>title"><%= title %></div> <form>
<input type="file" id="<%= uploadId %>" name="file" accept="*/*" <%= disabled ? 'disabled' : '' %> <%= multiUpload ? 'multiple' : '' %>/> <div id="${pfx}title">${title}</div>
<div style="clear:both;"></div> <input
</form> type="file"
`), id="${uploadId}"
name="file"
accept="*/*"
${disabled ? 'disabled' : ''}
${multiUpload ? 'multiple' : ''}
/>
<div style="clear:both;"></div>
</form>
`;
},
events: {}, events: {},

14
src/block_manager/config/config.js

@ -3,8 +3,18 @@ export default {
// With the empty value, nothing will be rendered // With the empty value, nothing will be rendered
appendTo: '', appendTo: '',
// Append blocks to canvas on click // Append blocks to canvas on click.
appendOnClick: 0, // With the `true` value, it will try to append the block to the selected component.
// If there is no selected component, the block will be appened to the wrapper.
// You can also pass a function to this option, use it as a catch-all for all block
// clicks and implement a custom logic for each block.
// appendOnClick: (block, editor) => {
// if (block.get('id') === 'some-id')
// editor.getSelected().append(block.get('content'))
// else
// editor.getWrapper().append(block.get('content'))
// }
appendOnClick: false,
blocks: [] blocks: []
}; };

9
src/block_manager/index.js

@ -23,6 +23,8 @@
* * [getContainer](#getcontainer) * * [getContainer](#getcontainer)
* * [render](#render) * * [render](#render)
* *
* [Block]: block.html
*
* @module BlockManager * @module BlockManager
*/ */
import { isElement } from 'underscore'; import { isElement } from 'underscore';
@ -120,13 +122,13 @@ export default () => {
* @param {Object} opts Options * @param {Object} opts Options
* @param {string} opts.label Name of the block * @param {string} opts.label Name of the block
* @param {string} opts.content HTML content * @param {string} opts.content HTML content
* @param {string|Object} opts.category Group the block inside a catgegory. * @param {string|Object} opts.category Group the block inside a category.
* You should pass objects with id property, eg: * You should pass objects with id property, eg:
* {id: 'some-uid', label: 'My category'} * {id: 'some-uid', label: 'My category'}
* The string will be converted in: * The string will be converted in:
* 'someid' => {id: 'someid', label: 'someid'} * 'someid' => {id: 'someid', label: 'someid'}
* @param {Object} [opts.attributes={}] Block attributes * @param {Object} [opts.attributes={}] Block attributes
* @return {Block} Added block * @returns {[Block]} Added block
* @example * @example
* blockManager.add('h1-block', { * blockManager.add('h1-block', {
* label: 'Heading', * label: 'Heading',
@ -146,6 +148,7 @@ export default () => {
/** /**
* Return the block by id * Return the block by id
* @param {string} id Block id * @param {string} id Block id
* @returns {[Block]}
* @example * @example
* const block = blockManager.get('h1-block'); * const block = blockManager.get('h1-block');
* console.log(JSON.stringify(block)); * console.log(JSON.stringify(block));
@ -178,7 +181,7 @@ export default () => {
/** /**
* Remove a block by id * Remove a block by id
* @param {string} id Block id * @param {string} id Block id
* @return {Block} Removed block * @returns {[Block]} Removed block
* @example * @example
* // Id of the block which need to be removed * // Id of the block which need to be removed
* const id = 'button'; * const id = 'button';

47
src/block_manager/model/Block.js

@ -1,21 +1,30 @@
import Backbone from 'backbone'; import { Model } from 'common';
export default Backbone.Model.extend({ /**
defaults: { * @property {String} label Block label, eg. `My block`
// If true, triggers an 'active' event on dropped component * @property {String|Object} content The content of the block. Might be an HTML string or a [Component Defintion](/modules/Components.html#component-definition)
activate: 0, * @property {String} [media=''] HTML string for the media/icon of the block, eg. `<svg ...`, `<img ...`, etc.
// If true, the dropped component will be selected * @property {String} [category=''] Block category
select: 0, * @property {Boolean} [activate=false] If true, triggers an the `active` event on dropped component
// If true, all IDs of dropped component and its style will be changed * @property {Boolean} [select=false] If true, the dropped component will be selected
resetId: 0, * @property {Boolean} [resetId=false] If true, all IDs of dropped components and their styles will be changed
// Block label * @property {Boolean} [disable=false] Disable the block from being interacted
label: '', * @property {Function} [onClick] Custom behavior on click, eg. `(block, editor) => editor.getWrapper().append(block.get('content'))`
// Disable the drag of the block * @property {Object} [attributes={}] Block attributes to apply in the view element
disable: 0, */
// HTML string for the media of the block, eg. SVG icon, image, etc. export default class Block extends Model {
media: '', defaults() {
content: '', return {
category: '', label: '',
attributes: {} content: '',
media: '',
category: '',
activate: false,
select: false,
resetId: false,
disable: false,
onClick: null,
attributes: {}
};
} }
}); }

23
src/block_manager/view/BlockView.js

@ -1,5 +1,5 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import { isObject } from 'underscore'; import { isFunction, isObject } 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,9 +21,15 @@ export default Backbone.View.extend({
this.listenTo(model, 'change', this.render); this.listenTo(model, 'change', this.render);
}, },
handleClick() { handleClick(ev) {
const { config, model, em } = this; const { config, model, em } = this;
if (!config.appendOnClick) return; const onClick = model.get('onClick') || config.appendOnClick;
em.trigger('block:click', model, ev);
if (!onClick) {
return;
} else if (isFunction(onClick)) {
return onClick(model, em.getEditor(), { event: ev });
}
const sorter = config.getSorter(); const sorter = config.getSorter();
const content = model.get('content'); const content = model.get('content');
const selected = em.getSelected(); const selected = em.getSelected();
@ -99,14 +105,15 @@ export default Backbone.View.extend({
if (result) { if (result) {
const oldKey = 'activeOnRender'; const oldKey = 'activeOnRender';
const oldActive = result.get && result.get(oldKey); const oldActive = result.get && result.get(oldKey);
const toActive = model.get('activate') || oldActive;
if (model.get('activate') || oldActive) { if (model.get('select') || toActive) {
result.trigger('active'); em.setSelected(result);
result.unset(oldKey);
} }
if (model.get('select')) { if (toActive) {
em.setSelected(result); result.trigger('active');
result.unset(oldKey);
} }
if (model.get('resetId')) { if (model.get('resetId')) {

52
src/block_manager/view/CategoryView.js

@ -1,20 +1,20 @@
import { template } from 'underscore'; import { View } from 'backbone';
import Backbone from 'backbone'; import html from 'utils/html';
export default Backbone.View.extend({ export default class CategoryView extends View {
template: template(` template({ pfx, label }) {
<div class="<%= pfx %>title"> return html`
<i class="<%= pfx %>caret-icon"></i> <div class="${pfx}title">
<%= label %> <i class="${pfx}caret-icon"></i>
</div> ${label}
<div class="<%= pfx %>blocks-c"></div> </div>
`), <div class="${pfx}blocks-c"></div>
`;
events: {}, }
attributes() { attributes() {
return this.model.get('attributes'); return this.model.get('attributes');
}, }
initialize(o = {}, config = {}) { initialize(o = {}, config = {}) {
this.config = config; this.config = config;
@ -26,33 +26,34 @@ export default Backbone.View.extend({
this.iconClass = `${pfx}caret-icon`; this.iconClass = `${pfx}caret-icon`;
this.activeClass = `${pfx}open`; this.activeClass = `${pfx}open`;
this.className = `${pfx}block-category`; this.className = `${pfx}block-category`;
this.events = {};
this.events[`click .${pfx}title`] = 'toggle'; this.events[`click .${pfx}title`] = 'toggle';
this.listenTo(this.model, 'change:open', this.updateVisibility); this.listenTo(this.model, 'change:open', this.updateVisibility);
this.delegateEvents(); this.delegateEvents();
this.model.view = this; this.model.view = this;
}, }
updateVisibility() { updateVisibility() {
if (this.model.get('open')) this.open(); if (this.model.get('open')) this.open();
else this.close(); else this.close();
}, }
open() { open() {
this.$el.addClass(this.activeClass); this.$el.addClass(this.activeClass);
this.getIconEl().className = `${this.iconClass} ${this.caretD}`; this.getIconEl().className = `${this.iconClass} ${this.caretD}`;
this.getBlocksEl().style.display = ''; this.getBlocksEl().style.display = '';
}, }
close() { close() {
this.$el.removeClass(this.activeClass); this.$el.removeClass(this.activeClass);
this.getIconEl().className = `${this.iconClass} ${this.caretR}`; this.getIconEl().className = `${this.iconClass} ${this.caretR}`;
this.getBlocksEl().style.display = 'none'; this.getBlocksEl().style.display = 'none';
}, }
toggle() { toggle() {
var model = this.model; var model = this.model;
model.set('open', !model.get('open')); model.set('open', !model.get('open'));
}, }
getIconEl() { getIconEl() {
if (!this.iconEl) { if (!this.iconEl) {
@ -60,7 +61,7 @@ export default Backbone.View.extend({
} }
return this.iconEl; return this.iconEl;
}, }
getBlocksEl() { getBlocksEl() {
if (!this.blocksEl) { if (!this.blocksEl) {
@ -68,24 +69,21 @@ export default Backbone.View.extend({
} }
return this.blocksEl; return this.blocksEl;
}, }
append(el) { append(el) {
this.getBlocksEl().appendChild(el); this.getBlocksEl().appendChild(el);
}, }
render() { render() {
const { em, el, $el, model } = this; const { em, el, $el, model, pfx } = this;
const label = const label =
em.t(`blockManager.categories.${model.id}`) || model.get('label'); em.t(`blockManager.categories.${model.id}`) || model.get('label');
el.innerHTML = this.template({ el.innerHTML = this.template({ pfx, label });
pfx: this.pfx,
label
});
$el.addClass(this.className); $el.addClass(this.className);
$el.css({ order: model.get('order') }); $el.css({ order: model.get('order') });
this.updateVisibility(); this.updateVisibility();
return this; return this;
} }
}); }

144
src/canvas/index.js

@ -25,6 +25,10 @@
* * [scrollTo](#scrollto) * * [scrollTo](#scrollto)
* * [setZoom](#setzoom) * * [setZoom](#setzoom)
* * [getZoom](#getzoom) * * [getZoom](#getzoom)
* * [getCoords](#getcoords)
* * [setCoords](#setcoords)
*
* [Component]: component.html
* *
* @module Canvas * @module Canvas
*/ */
@ -48,11 +52,6 @@ export default () => {
return CanvasView; return CanvasView;
}, },
/**
* Name of the module
* @type {String}
* @private
*/
name: 'Canvas', name: 'Canvas',
/** /**
@ -80,10 +79,6 @@ export default () => {
onLoad() { onLoad() {
this.model.init(); this.model.init();
CanvasView = new canvasView({
model: canvas,
config: c
});
}, },
getModel() { getModel() {
@ -92,7 +87,9 @@ export default () => {
/** /**
* Get the configuration object * Get the configuration object
* @return {Object} * @returns {Object} Configuration object
* @example
* console.log(canvas.getConfig())
*/ */
getConfig() { getConfig() {
return c; return c;
@ -100,7 +97,7 @@ export default () => {
/** /**
* Get the canvas element * Get the canvas element
* @return {HTMLElement} * @returns {HTMLElement}
*/ */
getElement() { getElement() {
return CanvasView.el; return CanvasView.el;
@ -111,11 +108,11 @@ export default () => {
}, },
/** /**
* Get the iframe element of the canvas * Get the main frame element of the canvas
* @return {HTMLIFrameElement} * @returns {HTMLIFrameElement}
*/ */
getFrameEl() { getFrameEl() {
const { frame } = CanvasView; const { frame } = CanvasView || {};
return frame && frame.el; return frame && frame.el;
}, },
@ -124,16 +121,16 @@ export default () => {
}, },
/** /**
* Get the window instance of the iframe element * Get the main frame window instance
* @return {Window} * @returns {Window}
*/ */
getWindow() { getWindow() {
return this.getFrameEl().contentWindow; return this.getFrameEl().contentWindow;
}, },
/** /**
* Get the document of the iframe element * Get the main frame document element
* @return {HTMLDocument} * @returns {HTMLDocument}
*/ */
getDocument() { getDocument() {
const frame = this.getFrameEl(); const frame = this.getFrameEl();
@ -141,7 +138,7 @@ export default () => {
}, },
/** /**
* Get the body of the iframe element * Get the main frame body element
* @return {HTMLBodyElement} * @return {HTMLBodyElement}
*/ */
getBody() { getBody() {
@ -163,7 +160,7 @@ export default () => {
/** /**
* Returns element containing all global canvas tools * Returns element containing all global canvas tools
* @return {HTMLElement} * @returns {HTMLElement}
* @private * @private
*/ */
getGlobalToolsEl() { getGlobalToolsEl() {
@ -172,7 +169,7 @@ export default () => {
/** /**
* Returns element containing all canvas tools * Returns element containing all canvas tools
* @return {HTMLElement} * @returns {HTMLElement}
* @private * @private
*/ */
getToolsEl(compView) { getToolsEl(compView) {
@ -181,7 +178,7 @@ export default () => {
/** /**
* Returns highlighter element * Returns highlighter element
* @return {HTMLElement} * @returns {HTMLElement}
* @private * @private
*/ */
getHighlighter(compView) { getHighlighter(compView) {
@ -190,7 +187,7 @@ export default () => {
/** /**
* Returns badge element * Returns badge element
* @return {HTMLElement} * @returns {HTMLElement}
* @private * @private
*/ */
getBadgeEl(compView) { getBadgeEl(compView) {
@ -199,7 +196,7 @@ export default () => {
/** /**
* Returns placer element * Returns placer element
* @return {HTMLElement} * @returns {HTMLElement}
* @private * @private
*/ */
getPlacerEl() { getPlacerEl() {
@ -208,7 +205,7 @@ export default () => {
/** /**
* Returns ghost element * Returns ghost element
* @return {HTMLElement} * @returns {HTMLElement}
* @private * @private
*/ */
getGhostEl() { getGhostEl() {
@ -217,7 +214,7 @@ export default () => {
/** /**
* Returns toolbar element * Returns toolbar element
* @return {HTMLElement} * @returns {HTMLElement}
* @private * @private
*/ */
getToolbarEl() { getToolbarEl() {
@ -226,7 +223,7 @@ export default () => {
/** /**
* Returns resizer element * Returns resizer element
* @return {HTMLElement} * @returns {HTMLElement}
* @private * @private
*/ */
getResizerEl() { getResizerEl() {
@ -235,7 +232,7 @@ export default () => {
/** /**
* Returns offset viewer element * Returns offset viewer element
* @return {HTMLElement} * @returns {HTMLElement}
* @private * @private
*/ */
getOffsetViewerEl(compView) { getOffsetViewerEl(compView) {
@ -248,24 +245,25 @@ export default () => {
/** /**
* Returns fixed offset viewer element * Returns fixed offset viewer element
* @return {HTMLElement} * @returns {HTMLElement}
* @private * @private
*/ */
getFixedOffsetViewerEl() { getFixedOffsetViewerEl() {
return CanvasView.fixedOffsetEl; return CanvasView.fixedOffsetEl;
}, },
/**
* Render canvas
* @private
* */
render() { render() {
CanvasView && CanvasView.remove();
CanvasView = new canvasView({
model: canvas,
config: c
});
return CanvasView.render().el; return CanvasView.render().el;
}, },
/** /**
* Get frame position * Get frame position
* @return {Object} * @returns {Object}
* @private * @private
*/ */
getOffset() { getOffset() {
@ -280,7 +278,7 @@ export default () => {
/** /**
* Get the offset of the passed component element * Get the offset of the passed component element
* @param {HTMLElement} el * @param {HTMLElement} el
* @return {Object} * @returns {Object}
* @private * @private
*/ */
offset(el) { offset(el) {
@ -302,7 +300,7 @@ export default () => {
/** /**
* Get element position relative to the canvas * Get element position relative to the canvas
* @param {HTMLElement} el * @param {HTMLElement} el
* @return {Object} * @returns {Object}
* @private * @private
*/ */
getElementPos(el, opts) { getElementPos(el, opts) {
@ -312,7 +310,7 @@ export default () => {
/** /**
* Returns element's offsets like margins and paddings * Returns element's offsets like margins and paddings
* @param {HTMLElement} el * @param {HTMLElement} el
* @return {Object} * @returns {Object}
* @private * @private
*/ */
getElementOffsets(el) { getElementOffsets(el) {
@ -512,7 +510,7 @@ export default () => {
/** /**
* Check if the canvas is focused * Check if the canvas is focused
* @return {Boolean} * @returns {Boolean}
*/ */
hasFocus() { hasFocus() {
return this.getDocument().hasFocus(); return this.getDocument().hasFocus();
@ -540,7 +538,7 @@ export default () => {
* executed via `scrollIntoView` API and options of this method are * executed via `scrollIntoView` API and options of this method are
* passed to it. For instance, you can scroll smoothly by using * passed to it. For instance, you can scroll smoothly by using
* `{ behavior: 'smooth' }`. * `{ behavior: 'smooth' }`.
* @param {HTMLElement|Component} el * @param {HTMLElement|[Component]} el
* @param {Object} [opts={}] Options, same as options for `scrollIntoView` * @param {Object} [opts={}] Options, same as options for `scrollIntoView`
* @param {Boolean} [opts.force=false] Force the scroll, even if the element is already visible * @param {Boolean} [opts.force=false] Force the scroll, even if the element is already visible
* @example * @example
@ -575,9 +573,11 @@ export default () => {
}, },
/** /**
* Set zoom value * Set canvas zoom value
* @param {Number} value The zoom value, from 0 to 100 * @param {Number} value The zoom value, from 0 to 100
* @returns {this} * @returns {this}
* @example
* canvas.setZoom(50); // set zoom to 50%
*/ */
setZoom(value) { setZoom(value) {
canvas.set('zoom', parseFloat(value)); canvas.set('zoom', parseFloat(value));
@ -585,13 +585,42 @@ export default () => {
}, },
/** /**
* Get zoom value * Get canvas zoom value
* @returns {Number} * @returns {Number}
* @example
* canvas.setZoom(50); // set zoom to 50%
* const zoom = canvas.getZoom(); // 50
*/ */
getZoom() { getZoom() {
return parseFloat(canvas.get('zoom')); return parseFloat(canvas.get('zoom'));
}, },
/**
* Set canvas position coordinates
* @param {Number} x Horizontal position
* @param {Number} y Vertical position
* @returns {this}
* @example
* canvas.setCoords(100, 100);
*/
setCoords(x, y) {
canvas.set({ x: parseFloat(x), y: parseFloat(y) });
return this;
},
/**
* Get canvas position coordinates
* @returns {Object} Object containing coordinates
* @example
* canvas.setCoords(100, 100);
* const coords = canvas.getCoords();
* // { x: 100, y: 100 }
*/
getCoords() {
const { x, y } = canvas.attributes;
return { x, y };
},
getZoomDecimal() { getZoomDecimal() {
return this.getZoom() / 100; return this.getZoom() / 100;
}, },
@ -615,23 +644,22 @@ export default () => {
* @param {Object} props Frame properties * @param {Object} props Frame properties
* @returns {Frame} * @returns {Frame}
* @example * @example
* * canvas.addFrame({
editor.Canvas.addFrame({ * name: 'Mobile home page',
name: 'Mobile home page', * x: 100, // Position in canvas
x: 100, // Position in canvas * y: 100,
y: 100, * width: 500, // Frame dimensions
width: 500, // Frame dimensions * height: 600,
height: 600, * // device: 'DEVICE-ID',
// device: 'DEVICE-ID', * components: [
components: [ * '<h1 class="testh">Title frame</h1>',
'<h1 class="testh">Title frame</h1>', * '<p class="testp">Paragraph frame</p>',
'<p class="testp">Paragraph frame</p>', * ],
], * styles: `
styles: ` * .testh { color: red; }
.testh { color: red; } * .testp { color: blue; }
.testp { color: blue; } * `,
`, * });
});
*/ */
addFrame(props = {}, opts = {}) { addFrame(props = {}, opts = {}) {
return canvas.get('frames').add( return canvas.get('frames').add(

15
src/canvas/model/Canvas.js

@ -28,13 +28,10 @@ export default class Canvas extends Model {
init() { init() {
const { em } = this; const { em } = this;
this.set( const mainPage = em.get('PageManager').getMain();
'frames', const frame = mainPage.getMainFrame();
em this.set('frames', mainPage.getFrames());
.get('PageManager') this.updateDevice({ frame });
.getMain()
.getFrames()
);
} }
_pageUpdated(page, prev) { _pageUpdated(page, prev) {
@ -45,10 +42,10 @@ export default class Canvas extends Model {
this.set('frames', page.getFrames()); this.set('frames', page.getFrames());
} }
updateDevice() { updateDevice(opts = {}) {
const { em } = this; const { em } = this;
const device = em.getDeviceModel(); const device = em.getDeviceModel();
const model = em.getCurrentFrameModel(); const model = opts.frame || em.getCurrentFrameModel();
if (model && device) { if (model && device) {
const { width, height } = device.attributes; const { width, height } = device.attributes;

5
src/canvas/view/CanvasView.js

@ -75,8 +75,6 @@ export default Backbone.View.extend({
remove() { remove() {
this.frames.remove(); this.frames.remove();
const frm = this.model.get('frames');
frm.remove(frm.models);
this.frames = {}; this.frames = {};
Backbone.View.prototype.remove.apply(this, arguments); Backbone.View.prototype.remove.apply(this, arguments);
this.toggleListeners(); this.toggleListeners();
@ -201,7 +199,8 @@ export default Backbone.View.extend({
getFrameOffset(el) { getFrameOffset(el) {
if (!this.frmOff || el) { if (!this.frmOff || el) {
const frame = this.frame.el; const frame = this.frame.el;
const frEl = el ? el.ownerDocument.defaultView.frameElement : frame; const winEl = el && el.ownerDocument.defaultView;
const frEl = winEl ? winEl.frameElement : frame;
this.frmOff = this.offset(frEl || frame); this.frmOff = this.offset(frEl || frame);
} }
return this.frmOff; return this.frmOff;

9
src/canvas/view/FrameView.js

@ -30,6 +30,7 @@ export default Backbone.View.extend({
'_emitUpdate' '_emitUpdate'
); );
const { model, el } = this; const { model, el } = this;
this.tools = {};
this.config = { this.config = {
...(o.config || {}), ...(o.config || {}),
frameView: this frameView: this
@ -163,18 +164,20 @@ export default Backbone.View.extend({
}, },
_getTool(name) { _getTool(name) {
const { tools } = this;
const toolsEl = this.getToolsEl(); const toolsEl = this.getToolsEl();
if (!this[name]) { if (!tools[name]) {
this[name] = toolsEl.querySelector(name); tools[name] = toolsEl.querySelector(name);
} }
return this[name]; return tools[name];
}, },
remove() { remove() {
const wrp = this.wrapper; const wrp = this.wrapper;
this._toggleEffects(); this._toggleEffects();
this.tools = {};
wrp && wrp.remove(); wrp && wrp.remove();
Backbone.View.prototype.remove.apply(this, arguments); Backbone.View.prototype.remove.apply(this, arguments);
}, },

11
src/canvas/view/FrameWrapView.js

@ -71,9 +71,14 @@ export default Backbone.View.extend({
ev && this.dragger.start(ev); ev && this.dragger.start(ev);
}, },
remove(opts) { __clear(opts) {
this.frame.remove(opts); const { frame } = this;
frame && frame.remove(opts);
removeEl(this.elTools); removeEl(this.elTools);
},
remove(opts) {
this.__clear(opts);
Backbone.View.prototype.remove.apply(this, arguments); Backbone.View.prototype.remove.apply(this, arguments);
['frame', 'dragger', 'cv', 'em', 'canvas', 'elTools'].forEach( ['frame', 'dragger', 'cv', 'em', 'canvas', 'elTools'].forEach(
i => (this[i] = 0) i => (this[i] = 0)
@ -164,7 +169,7 @@ export default Backbone.View.extend({
render() { render() {
const { frame, $el, ppfx, cv, model, el } = this; const { frame, $el, ppfx, cv, model, el } = this;
const { onRender } = model.attributes; const { onRender } = model.attributes;
frame && frame.remove(); this.__clear();
this.__handleSize(); this.__handleSize();
frame.render(); frame.render();
$el $el

14
src/code_manager/model/CodeMirrorEditor.js

@ -1,9 +1,15 @@
import { bindAll } from 'underscore'; import { bindAll } from 'underscore';
import Backbone from 'backbone'; import Backbone from 'backbone';
import CodeMirror from 'codemirror/lib/codemirror'; import { hasWin } from 'utils/mixins';
import 'codemirror/mode/htmlmixed/htmlmixed';
import 'codemirror/mode/css/css'; let CodeMirror;
import 'codemirror-formatting';
if (hasWin()) {
CodeMirror = require('codemirror/lib/codemirror');
require('codemirror/mode/htmlmixed/htmlmixed');
require('codemirror/mode/css/css');
require('codemirror-formatting');
}
export default Backbone.Model.extend({ export default Backbone.Model.extend({
CodeMirror, CodeMirror,

30
src/code_manager/model/CssGenerator.js

@ -40,23 +40,26 @@ export default Backbone.Model.extend({
}, },
build(model, opts = {}) { build(model, opts = {}) {
const cssc = opts.cssc; const { json } = opts;
const em = opts.em || ''; const em = opts.em || '';
const cssc = opts.cssc || (em && em.get('CssComposer'));
this.em = em; this.em = em;
this.compCls = []; this.compCls = [];
this.ids = []; this.ids = [];
var code = this.buildFromModel(model, opts); this.model = model;
const codeJson = [];
let code = model ? this.buildFromModel(model, opts) : '';
const clearStyles = const clearStyles =
isUndefined(opts.clearStyles) && em isUndefined(opts.clearStyles) && em
? em.getConfig('clearStyles') ? em.getConfig('clearStyles')
: opts.clearStyles; : opts.clearStyles;
if (cssc) { if (cssc) {
const rules = cssc.getAll(); const rules = opts.rules || cssc.getAll();
const atRules = {}; const atRules = {};
const dump = []; const dump = [];
rules.each(rule => { rules.forEach(rule => {
const atRule = rule.getAtRule(); const atRule = rule.getAtRule();
if (atRule) { if (atRule) {
@ -69,7 +72,13 @@ export default Backbone.Model.extend({
return; return;
} }
code += this.buildFromRule(rule, dump, opts); const res = this.buildFromRule(rule, dump, opts);
if (json) {
codeJson.push(res);
} else {
code += res;
}
}); });
this.sortMediaObject(atRules).forEach(item => { this.sortMediaObject(atRules).forEach(item => {
@ -85,6 +94,8 @@ export default Backbone.Model.extend({
} else { } else {
rulesStr += ruleStr; rulesStr += ruleStr;
} }
json && codeJson.push(ruleStr);
}); });
if (rulesStr) { if (rulesStr) {
@ -92,10 +103,10 @@ export default Backbone.Model.extend({
} }
}); });
em && clearStyles && rules.remove(dump); em && clearStyles && rules.remove && rules.remove(dump);
} }
return code; return json ? codeJson.filter(r => r) : code;
}, },
/** /**
@ -105,6 +116,7 @@ export default Backbone.Model.extend({
*/ */
buildFromRule(rule, dump, opts = {}) { buildFromRule(rule, dump, opts = {}) {
let result = ''; let result = '';
const { model } = this;
const selectorStrNoAdd = rule.selectorsToString({ skipAdd: 1 }); const selectorStrNoAdd = rule.selectorsToString({ skipAdd: 1 });
const selectorsAdd = rule.get('selectorsAdd'); const selectorsAdd = rule.get('selectorsAdd');
const singleAtRule = rule.get('singleAtRule'); const singleAtRule = rule.get('singleAtRule');
@ -122,9 +134,9 @@ export default Backbone.Model.extend({
} }
}); });
if ((selectorStrNoAdd && found) || selectorsAdd || singleAtRule) { if ((selectorStrNoAdd && found) || selectorsAdd || singleAtRule || !model) {
const block = rule.getDeclaration({ body: 1 }); const block = rule.getDeclaration({ body: 1 });
block && (result += block); block && (opts.json ? (result = rule) : (result += block));
} else { } else {
dump.push(rule); dump.push(rule);
} }

10
src/code_manager/model/HtmlGenerator.js

@ -1,6 +1,6 @@
import Backbone from 'backbone'; import { Model } from 'backbone';
export default Backbone.Model.extend({ export default class HTMLGenerator extends Model {
build(model, opts = {}) { build(model, opts = {}) {
const models = model.components(); const models = model.components();
const htmlOpts = {}; const htmlOpts = {};
@ -36,16 +36,16 @@ export default Backbone.Model.extend({
if (opts.exportWrapper) { if (opts.exportWrapper) {
return model.toHTML({ return model.toHTML({
...htmlOpts, ...htmlOpts,
...(opts.wrapperIsBody && { tag: 'body' }) ...(opts.wrapperIsBody && model.is('wrapper') && { tag: 'body' })
}); });
} }
return this.buildModels(models, htmlOpts); return this.buildModels(models, htmlOpts);
}, }
buildModels(models, opts = {}) { buildModels(models, opts = {}) {
let code = ''; let code = '';
models.forEach(mod => (code += mod.toHTML(opts))); models.forEach(mod => (code += mod.toHTML(opts)));
return code; return code;
} }
}); }

34
src/code_manager/view/EditorView.js

@ -1,24 +1,28 @@
import { template } from 'underscore'; import html from 'utils/html';
import Backbone from 'backbone'; import { View } from 'backbone';
export default Backbone.View.extend({ export default class EditorView extends View {
template: template(` template({ pfx, codeName, label }) {
<div class="<%= pfx %>editor" id="<%= pfx %><%= codeName %>"> return html`
<div id="<%= pfx %>title"><%= label %></div> <div class="${pfx}editor" id="${pfx}${codeName}">
<div id="<%= pfx %>code"></div> <div id="${pfx}title">${label}</div>
</div>`), <div id="${pfx}code"></div>
</div>
`;
}
initialize(o) { initialize(o) {
this.config = o.config || {}; this.config = o.config || {};
this.pfx = this.config.stylePrefix; this.pfx = this.config.stylePrefix;
}, }
render() { render() {
var obj = this.model.toJSON(); const { model, pfx, $el } = this;
obj.pfx = this.pfx; const obj = model.toJSON();
this.$el.html(this.template(obj)); obj.pfx = pfx;
this.$el.attr('class', this.pfx + 'editor-c'); $el.html(this.template(obj));
this.$el.find('#' + this.pfx + 'code').append(this.model.get('input')); $el.attr('class', `${pfx}editor-c`);
$el.find(`#${pfx}code`).append(model.get('input'));
return this; return this;
} }
}); }

5
src/commands/view/CommandAbstract.js

@ -1,7 +1,6 @@
import Backbone from 'backbone'; import { Model } from 'backbone';
const $ = Backbone.$;
export default Backbone.View.extend({ export default Model.extend({
/** /**
* Initialize method that can't be removed * Initialize method that can't be removed
* @param {Object} o Options * @param {Object} o Options

29
src/commands/view/ComponentDrag.js

@ -269,34 +269,41 @@ export default {
x = this.getTranslate(transform); x = this.getTranslate(transform);
y = this.getTranslate(transform, 'y'); y = this.getTranslate(transform, 'y');
} else { } else {
x = parseFloat(left); x = parseFloat(left || 0);
y = parseFloat(top); y = parseFloat(top || 0);
} }
return { x, y }; return { x, y };
}, },
setPosition({ x, y, end, position, width, height }) { setPosition({ x, y, end, position, width, height }) {
const { target, isTran } = this; const { target, isTran, em } = this;
const unit = 'px'; const unit = 'px';
const en = !end ? 1 : ''; // this will trigger the final change const en = !end ? 1 : ''; // this will trigger the final change
const left = `${x}${unit}`; const left = `${x}${unit}`;
const top = `${y}${unit}`; const top = `${y}${unit}`;
let styleUp = {};
if (isTran) { if (isTran) {
let transform = target.getStyle()['transform'] || ''; let transform = target.getStyle()['transform'] || '';
transform = this.setTranslate(transform, 'x', left); transform = this.setTranslate(transform, 'x', left);
transform = this.setTranslate(transform, 'y', top); transform = this.setTranslate(transform, 'y', top);
return target.addStyle({ transform, en }, { avoidStore: !end }); styleUp = { transform, en };
target.addStyle(styleUp, { avoidStore: !end });
} else {
const adds = { position, width, height };
const style = { left, top, en };
keys(adds).forEach(add => {
const prop = adds[add];
if (prop) style[add] = prop;
});
styleUp = style;
target.addStyle(styleUp, { avoidStore: !end });
} }
const adds = { position, width, height }; // Update StyleManager properties
const style = { left, top, en }; em.getSelected() &&
keys(adds).forEach(add => { keys(styleUp).forEach(i => em.trigger(`update:component:style:${i}`));
const prop = adds[add];
if (prop) style[add] = prop;
});
target.addStyle(style, { avoidStore: !end });
}, },
_getDragData() { _getDragData() {

20
src/commands/view/OpenAssets.js

@ -3,7 +3,6 @@ export default {
const modal = editor.Modal; const modal = editor.Modal;
const am = editor.AssetManager; const am = editor.AssetManager;
const config = am.getConfig(); const config = am.getConfig();
const amContainer = am.getContainer();
const title = opts.modalTitle || editor.t('assetManager.modalTitle') || ''; const title = opts.modalTitle || editor.t('assetManager.modalTitle') || '';
const types = opts.types; const types = opts.types;
const accept = opts.accept; const accept = opts.accept;
@ -14,35 +13,24 @@ export default {
am.onSelect(opts.onSelect); am.onSelect(opts.onSelect);
if (!this.rendered || types) { if (!this.rendered || types) {
let assets = am.getAll().filter(i => 1); let assets = am.getAll().filter(i => i);
if (types && types.length) { if (types && types.length) {
assets = assets.filter(a => types.indexOf(a.get('type')) !== -1); assets = assets.filter(a => types.indexOf(a.get('type')) !== -1);
} }
am.render(assets); am.render(assets);
this.rendered = 1; this.rendered = am.getContainer();
} }
if (accept) { if (accept) {
const uploadEl = amContainer.querySelector( const uploadEl = this.rendered.querySelector(
`input#${config.stylePrefix}uploadFile` `input#${config.stylePrefix}uploadFile`
); );
uploadEl && uploadEl.setAttribute('accept', accept); uploadEl && uploadEl.setAttribute('accept', accept);
} }
modal modal.open({ title, content: this.rendered });
.open({
title,
content: amContainer
})
.getModel()
.once('change:open', () => editor.stopCommand(this.id));
return this;
},
stop(editor) {
editor.Modal.close();
return this; return this;
} }
}; };

3
src/commands/view/OpenTraitManager.js

@ -13,7 +13,6 @@ export default {
var panelC; var panelC;
if (!this.$cn) { if (!this.$cn) {
var tmView = tm.getTraitsViewer();
var confTm = tm.getConfig(); var confTm = tm.getConfig();
this.$cn = $('<div></div>'); this.$cn = $('<div></div>');
this.$cn2 = $('<div></div>'); this.$cn2 = $('<div></div>');
@ -27,7 +26,7 @@ export default {
this.$cn2.append( this.$cn2.append(
`<div class="${pfx}traits-label">${em.t('traitManager.label')}</div>` `<div class="${pfx}traits-label">${em.t('traitManager.label')}</div>`
); );
this.$cn2.append(tmView.render().el); this.$cn2.append(tm.render());
var panels = editor.Panels; var panels = editor.Panels;
if (!panels.getPanel('views-container')) if (!panels.getPanel('views-container'))

15
src/commands/view/SelectComponent.js

@ -5,7 +5,8 @@ import {
off, off,
getUnitFromValue, getUnitFromValue,
isTaggableNode, isTaggableNode,
getViewEl getViewEl,
hasWin
} from 'utils/mixins'; } from 'utils/mixins';
import { isVisible, isDoc } from 'utils/dom'; import { isVisible, isDoc } from 'utils/dom';
import ToolbarView from 'dom_components/view/ToolbarView'; import ToolbarView from 'dom_components/view/ToolbarView';
@ -335,6 +336,7 @@ export default {
* @private * @private
* */ * */
updateBadge(el, pos, opts = {}) { updateBadge(el, pos, opts = {}) {
const { canvas } = this;
const model = $(el).data('model'); const model = $(el).data('model');
if (!model || !model.get('badgable')) return; if (!model || !model.get('badgable')) return;
const badge = this.getBadge(opts); const badge = this.getBadge(opts);
@ -355,9 +357,12 @@ export default {
const un = 'px'; const un = 'px';
const bStyle = badge.style; const bStyle = badge.style;
bStyle.display = 'block'; bStyle.display = 'block';
const badgeH = badge ? badge.offsetHeight : 0;
const posTop = 0 - badgeH; const targetToElem = canvas.getTargetToElementFixed(el, badge, {
const top = opts.topOff - badgeH < 0 ? -opts.topOff : posTop; pos: pos
});
const top = targetToElem.top; //opts.topOff - badgeH < 0 ? -opts.topOff : posTop;
const left = opts.leftOff < 0 ? -opts.leftOff : 0; const left = opts.leftOff < 0 ? -opts.leftOff : 0;
bStyle.top = top + un; bStyle.top = top + un;
@ -746,11 +751,13 @@ export default {
}, },
run(editor) { run(editor) {
if (!hasWin()) return;
this.editor = editor && editor.get('Editor'); this.editor = editor && editor.get('Editor');
this.enable(); this.enable();
}, },
stop(ed, sender, opts = {}) { stop(ed, sender, opts = {}) {
if (!hasWin()) return;
const { em, editor } = this; const { em, editor } = this;
this.onHovered(); // force to hide toolbar this.onHovered(); // force to hide toolbar
this.stopSelectComponent(); this.stopSelectComponent();

1
src/common/Collection.js

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

1
src/common/Model.js

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

2
src/common/index.js

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

63
src/common/module.js

@ -0,0 +1,63 @@
import { isString } from 'underscore';
import { createId } from 'utils/mixins';
export default {
getConfig(name) {
const res = this.config || {};
return name ? res[name] : res;
},
getAll() {
return this.all || [];
},
getAllMap() {
return this.getAll().reduce((acc, i) => {
acc[i.get('id')] = i;
return acc;
}, {});
},
__initListen() {
const { all, em, events } = this;
all &&
em &&
all
.on('add', (m, c, o) => em.trigger(events.add, m, o))
.on('remove', (m, c, o) => em.trigger(events.remove, m, o))
.on('change', (p, c) =>
em.trigger(events.update, p, p.changedAttributes(), c)
)
.on('all', this.__catchAllEvent, this);
},
__remove(model, opts = {}) {
const { em } = this;
const md = isString(model) ? this.get(model) : model;
const rm = () => {
md && this.all.remove(md, opts);
return md;
};
!opts.silent && em && em.trigger(this.events.removeBefore, md, rm, opts);
return !opts.abort && rm();
},
__catchAllEvent(event, model, coll, opts) {
const { em, events } = this;
const options = opts || coll;
em && events.all && em.trigger(events.all, { event, model, options });
},
_createId(len = 16) {
const all = this.getAll();
const ln = all.length + len;
const allMap = this.getAllMap();
let id;
do {
id = createId(ln);
} while (allMap[id]);
return id;
}
};

11
src/css_composer/index.js

@ -86,10 +86,6 @@ export default () => {
em = c.em; em = c.em;
rules = new CssRules([], c); rules = new CssRules([], c);
rulesView = new CssRulesView({
collection: rules,
config: c
});
return this; return this;
}, },
@ -479,13 +475,18 @@ export default () => {
* @private * @private
*/ */
render() { render() {
rulesView && rulesView.remove();
rulesView = new CssRulesView({
collection: rules,
config: c
});
return rulesView.render().el; return rulesView.render().el;
}, },
destroy() { destroy() {
rules.reset(); rules.reset();
rules.stopListening(); rules.stopListening();
rulesView.remove(); rulesView && rulesView.remove();
[em, rules, rulesView].forEach(i => (i = null)); [em, rules, rulesView].forEach(i => (i = null));
c = {}; c = {};
} }

126
src/css_composer/model/CssRule.js

@ -1,46 +1,48 @@
import { map } from 'underscore'; import { map } from 'underscore';
import Backbone 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 } from 'underscore';
import Selectors from 'selector_manager/model/Selectors'; import Selectors from 'selector_manager/model/Selectors';
import { isEmptyObj } from 'utils/mixins'; import { isEmptyObj, hasWin } from 'utils/mixins';
const { CSS } = window; const { CSS } = hasWin() ? window : {};
export default Backbone.Model.extend(Styleable).extend({ export default class CssRule extends Model.extend(Styleable) {
defaults: { defaults() {
// Css selectors return {
selectors: [], // Css selectors
selectors: [],
// Additional string css selectors // Additional string css selectors
selectorsAdd: '', selectorsAdd: '',
// Css properties style // Css properties style
style: {}, style: {},
// On which device width this rule should be rendered, eg. @media (max-width: 1000px) // 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 of the rule, eg: hover | pressed | focused
state: '', state: '',
// Indicates if the rule is stylable // Indicates if the rule is stylable
stylable: true, stylable: true,
// Type of at-rule, eg. 'media', 'font-face', etc. // Type of at-rule, eg. 'media', 'font-face', etc.
atRuleType: '', atRuleType: '',
// This particolar property is used only on at-rules, like 'page' or // This particolar property is used only on at-rules, like 'page' or
// 'font-face', where the block containes only style declarations // 'font-face', where the block containes only style declarations
singleAtRule: 0, singleAtRule: 0,
// If true, sets '!important' on all properties // If true, sets '!important' on all properties
// You can use an array to specify properties to set important // You can use an array to specify properties to set important
// Used in view // Used in view
important: 0, important: 0,
_undo: true _undo: true
}, };
}
initialize(c, opt = {}) { initialize(c, opt = {}) {
this.config = c || {}; this.config = c || {};
@ -48,20 +50,20 @@ export default Backbone.Model.extend(Styleable).extend({
this.em = opt.em; this.em = opt.em;
this.ensureSelectors(); this.ensureSelectors();
this.on('change', this.__onChange); this.on('change', this.__onChange);
}, }
__onChange(m, opts) { __onChange(m, opts) {
const { em } = this; const { em } = this;
const changed = this.changedAttributes(); const changed = this.changedAttributes();
!isEmptyObj(changed) && em && em.changesUp(opts); !isEmptyObj(changed) && em && em.changesUp(opts);
}, }
clone() { clone() {
const opts = { ...this.opt }; const opts = { ...this.opt };
const attr = { ...this.attributes }; const attr = { ...this.attributes };
attr.selectors = this.get('selectors').map(s => s.clone()); attr.selectors = this.get('selectors').map(s => s.clone());
return new this.constructor(attr, opts); return new this.constructor(attr, opts);
}, }
ensureSelectors(m, c, opts) { ensureSelectors(m, c, opts) {
const { em } = this; const { em } = this;
@ -83,11 +85,17 @@ export default Backbone.Model.extend(Styleable).extend({
this.set('selectors', sels, opts); this.set('selectors', sels, opts);
this.listenTo(...toListen); this.listenTo(...toListen);
}, }
/** /**
* Returns an at-rule statement if possible, eg. '@media (...)', '@keyframes' * Return the at-rule statement when exists, eg. '@media (...)', '@keyframes'
* @return {string} * @returns {String}
* @example
* const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
* atRuleType: 'media',
* atRuleParams: '(min-width: 500px)'
* });
* cssRule.getAtRule(); // "@media (min-width: 500px)"
*/ */
getAtRule() { getAtRule() {
const type = this.get('atRuleType'); const type = this.get('atRuleType');
@ -96,11 +104,17 @@ export default Backbone.Model.extend(Styleable).extend({
const typeStr = type ? `@${type}` : condition ? '@media' : ''; const typeStr = type ? `@${type}` : condition ? '@media' : '';
return typeStr + (condition && typeStr ? ` ${condition}` : ''); return typeStr + (condition && typeStr ? ` ${condition}` : '');
}, }
/** /**
* Return selectors fo the rule as a string * Return selectors of the rule as a string
* @return {string} * @param {Object} [opts] Options
* @param {Boolean} [opts.skipState] Skip state from the result
* @returns {String}
* @example
* const cssRule = editor.Css.setRule('.class1:hover', { color: 'red' });
* cssRule.selectorsToString(); // ".class1:hover"
* cssRule.selectorsToString({ skipState: true }); // ".class1"
*/ */
selectorsToString(opts = {}) { selectorsToString(opts = {}) {
const result = []; const result = [];
@ -118,12 +132,18 @@ export default Backbone.Model.extend(Styleable).extend({
selectors && result.push(`${selectors}${stateStr}`); selectors && result.push(`${selectors}${stateStr}`);
addSelector && !opts.skipAdd && result.push(addSelector); addSelector && !opts.skipAdd && result.push(addSelector);
return result.join(', '); return result.join(', ');
}, }
/** /**
* Get declaration block * Get declaration block (without the at-rule statement)
* @param {Object} [opts={}] Options * @param {Object} [opts={}] Options (same as in `selectorsToString`)
* @return {string} * @returns {String}
* @example
* const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
* atRuleType: 'media',
* atRuleParams: '(min-width: 500px)'
* });
* cssRule.getDeclaration() // ".class1{color:red;}"
*/ */
getDeclaration(opts = {}) { getDeclaration(opts = {}) {
let result = ''; let result = '';
@ -136,12 +156,18 @@ export default Backbone.Model.extend(Styleable).extend({
} }
return result; return result;
}, }
/** /**
* Returns CSS string of the rule * Return the CSS string of the rule
* @param {Object} [opts={}] Options * @param {Object} [opts={}] Options (same as in `getDeclaration`)
* @return {string} * @return {String} CSS string
* @example
* const cssRule = editor.Css.setRule('.class1', { color: 'red' }, {
* atRuleType: 'media',
* atRuleParams: '(min-width: 500px)'
* });
* cssRule.toCSS() // "@media (min-width: 500px){.class1{color:red;}}"
*/ */
toCSS(opts = {}) { toCSS(opts = {}) {
let result = ''; let result = '';
@ -154,13 +180,13 @@ export default Backbone.Model.extend(Styleable).extend({
} }
return result; return result;
}, }
toJSON(...args) { toJSON(...args) {
const obj = Backbone.Model.prototype.toJSON.apply(this, args); const obj = Model.prototype.toJSON.apply(this, args);
if (this.em.getConfig('avoidDefaults')) { if (this.em.getConfig('avoidDefaults')) {
const defaults = this.defaults; const defaults = this.defaults();
forEach(defaults, (value, key) => { forEach(defaults, (value, key) => {
if (obj[key] === value) { if (obj[key] === value) {
@ -173,7 +199,7 @@ export default Backbone.Model.extend(Styleable).extend({
} }
return obj; return obj;
}, }
/** /**
* Compare the actual model with parameters * Compare the actual model with parameters
@ -216,4 +242,4 @@ export default Backbone.Model.extend(Styleable).extend({
return true; return true;
} }
}); }

30
src/device_manager/config/config.js

@ -1,3 +1,31 @@
export default { export default {
devices: [] // The device `id` to select on start, if not indicated, the first available from `devices` will be used.
default: null,
// Default devices
devices: [
{
id: 'desktop',
name: 'Desktop',
width: ''
},
{
id: 'tablet',
name: 'Tablet',
width: '768px',
widthMedia: '992px'
},
{
id: 'mobileLandscape',
name: 'Mobile landscape',
width: '568px',
widthMedia: '768px'
},
{
id: 'mobilePortrait',
name: 'Mobile portrait',
width: '320px',
widthMedia: '480px'
}
]
}; };

234
src/device_manager/index.js

@ -11,125 +11,221 @@
* 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 deviceManager = editor.DeviceManager; * const deviceManager = editor.Devices;
* ``` * ```
* ## Available Events
* * `device:add` - Added new device. The [Device] is passed as an argument to the callback
* * `device:remove` - Device removed. The [Device] is passed as an argument to the callback
* * `device:select` - New device selected. The newly selected [Device] and the previous one, are passed as arguments to the callback
* * `device:update` - Device updated. The updated [Device] and the object containing changes are passed as arguments to the callback
* * `device` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback
* *
* ## Methods
* * [add](#add) * * [add](#add)
* * [get](#get) * * [get](#get)
* * [getAll](#getAll) * * [getDevices](#getdevices)
* * [remove](#remove)
* * [select](#select)
* * [getSelected](#getselected)
* *
* @module DeviceManager * [Device]: device.html
*
* @module Devices
*/ */
import { isString } from 'underscore';
import Module from 'common/module';
import defaults from './config/config'; import defaults from './config/config';
import Device from './model/Device';
import Devices from './model/Devices'; import Devices from './model/Devices';
import DevicesView from './view/DevicesView'; import DevicesView from './view/DevicesView';
export const evAll = 'device';
export const evPfx = `${evAll}:`;
export const evSelect = `${evPfx}select`;
export const evSelectBefore = `${evSelect}:before`;
export const evUpdate = `${evPfx}update`;
export const evAdd = `${evPfx}add`;
export const evAddBefore = `${evAdd}:before`;
export const evRemove = `${evPfx}remove`;
export const evRemoveBefore = `${evRemove}:before`;
const chnSel = 'change:device';
export default () => { export default () => {
var c = {}; let c = {};
var devices, view; let devices;
let view;
return { return {
/** ...Module,
* Name of the module
* @type {String}
* @private
*/
name: 'DeviceManager', name: 'DeviceManager',
/** Device,
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations Devices,
* @param {Array<Object>} [config.devices=[]] Default devices
* @example events: {
* ... all: evAll,
* { select: evSelect,
* devices: [ // selectBefore: evSelectBefore,
* {name: 'Desktop', width: ''} update: evUpdate,
* {name: 'Tablet', width: '991px'} add: evAdd,
* ], // addBefore: evAddBefore,
* } remove: evRemove,
* ... removeBefore: evRemoveBefore
* @return {this} },
* @private
*/ init(config = {}) {
init(config) { c = { ...defaults, ...config };
c = config || {}; const { em } = c;
for (var name in defaults) {
if (!(name in c)) c[name] = defaults[name];
}
devices = new Devices(); devices = new Devices();
(c.devices || []).forEach(dv => this.add(dv.id || dv.name, dv.width, dv)); c.devices.forEach(dv => this.add(dv));
view = new DevicesView({ this.em = em;
collection: devices, this.all = devices;
config: c this.select(c.default || devices.at(0));
}); this.__initListen();
em.on(chnSel, this._onSelect, this);
return this; return this;
}, },
_onSelect(m, deviceId, opts) {
const { em, events } = this;
const prevId = m.previous('device');
const newDevice = this.get(deviceId);
const ev = events.select;
em.trigger(ev, newDevice, this.get(prevId));
this.__catchAllEvent(ev, newDevice, opts);
},
/** /**
* Add new device to the collection. URLs are supposed to be unique * Add new device
* @param {String} id Device id * @param {Object} props Device properties
* @param {String} width Width of the device * @returns {[Device]} Added device
* @param {Object} [opts] Custom options
* @returns {Device} Added device
* @example * @example
* deviceManager.add('tablet', '900px'); * const device1 = deviceManager.add({
* deviceManager.add('tablet2', '900px', { * // Without an explicit ID, the `name` will be taken. In case of missing `name`, a random ID will be created.
* height: '300px', * id: 'tablet',
* // At first, GrapesJS tries to localize the name by device id. * name: 'Tablet',
* // In case is not found, the `name` property is used (or `id` if name is missing) * width: '900px', // This width will be applied on the canvas frame and for the CSS media
* });
* const device2 = deviceManager.add({
* id: 'tablet2',
* name: 'Tablet 2', * name: 'Tablet 2',
* widthMedia: '810px', // the width that will be used for the CSS media * width: '800px', // This width will be applied on the canvas frame
* widthMedia: '810px', // This width that will be used for the CSS media
* height: '600px', // Height will be applied on the canvas frame
* }); * });
*/ */
add(id, width, opts = {}) { add(props, options = {}) {
const obj = { let result;
...opts, let opts = options;
id,
name: opts.name || id, // Support old API
width: width if (isString(props)) {
}; const width = options;
return devices.add(obj); opts = arguments[2] || {};
result = {
...opts,
id: props,
name: opts.name || props,
width
};
} else {
result = props;
}
if (!result.id) {
result.id = result.name || this._createId();
}
return devices.add(result, opts);
}, },
/** /**
* Return device by name * Return device by ID
* @param {string} name Name of the device * @param {String} id ID of the device
* @returns {[Device]|null}
* @example * @example
* var device = deviceManager.get('Tablet'); * const device = deviceManager.get('Tablet');
* console.log(JSON.stringify(device)); * console.log(JSON.stringify(device));
* // {name: 'Tablet', width: '900px'} * // {name: 'Tablet', width: '900px'}
*/ */
get(name) { get(id) {
return devices.get(name); // Support old API
const byName = this.getAll().filter(d => d.get('name') === id)[0];
return byName || devices.get(id) || null;
},
/**
* Remove device
* @param {String|[Device]} device Device or device id
* @returns {[Device]} Removed device
* @example
* const removed = deviceManager.remove('device-id');
* // or by passing the Device
* const device = deviceManager.get('device-id');
* deviceManager.remove(device);
*/
remove(device, opts = {}) {
return this.__remove(device, opts);
}, },
/** /**
* Return all devices * Return all devices
* @return {Collection} * @returns {Array<[Device]>}
* @example * @example
* var devices = deviceManager.getAll(); * const devices = deviceManager.getDevices();
* console.log(JSON.stringify(devices)); * console.log(JSON.stringify(devices));
* // [{name: 'Desktop', width: ''}, ...] * // [{name: 'Desktop', width: ''}, ...]
*/ */
getAll() { getDevices() {
return devices; return devices.models;
},
/**
* Change the selected device. This will update the frame in the canvas
* @param {String|[Device]} device Device or device id
* @example
* deviceManager.select('some-id');
* // or by passing the page
* const device = deviceManager.get('some-id');
* deviceManager.select(device);
*/
select(device, opts = {}) {
const md = isString(device) ? this.get(device) : device;
md && this.em.set('device', md.get('id'), opts);
return this;
}, },
/** /**
* Render devices * Get the selected device
* @return {string} HTML string * @returns {[Device]}
* @private * @example
* const selected = deviceManager.getSelected();
*/ */
getSelected() {
return this.get(this.em.get('device'));
},
getAll() {
return devices;
},
render() { render() {
view && view.remove();
view = new DevicesView({
collection: devices,
config: c
});
return view.render().el; return view.render().el;
}, },
destroy() { destroy() {
devices.reset();
devices.stopListening(); devices.stopListening();
view.remove(); devices.reset();
view && view.remove();
[devices, view].forEach(i => (i = null)); [devices, view].forEach(i => (i = null));
c = {}; c = {};
} }

45
src/device_manager/model/Device.js

@ -1,24 +1,23 @@
import Backbone from 'backbone'; import { Model } from 'backbone';
export default Backbone.Model.extend({ /**
idAttribute: 'name', * @typedef Device
* @property {String} [name=''] Device type, eg. `Mobile`
defaults: { * @property {String} [width] Width to set for the editor iframe, eg. '900px'
name: '', * @property {String} [height=''] Height to set for the editor iframe, eg. '600px'
* @property {String} [widthMedia=''] The width which will be used in media queries, If empty the width will be used
// Width to set for the editor iframe * @property {Number} [priority=null] Setup the order of media queries
width: null, */
export default class Device extends Model {
// Height to set for the editor iframe defaults() {
height: '', return {
name: '',
// The width which will be used in media queries, width: null,
// If empty the width will be used height: '',
widthMedia: null, widthMedia: null,
priority: null
// Setup the order of media queries };
priority: null }
},
initialize() { initialize() {
this.get('widthMedia') === null && this.get('widthMedia') === null &&
@ -28,11 +27,11 @@ export default Backbone.Model.extend({
this.set('priority', parseFloat(this.get('widthMedia')) || 0); this.set('priority', parseFloat(this.get('widthMedia')) || 0);
const toCheck = ['width', 'height', 'widthMedia']; const toCheck = ['width', 'height', 'widthMedia'];
toCheck.forEach(prop => this.checkUnit(prop)); toCheck.forEach(prop => this.checkUnit(prop));
}, }
checkUnit(prop) { checkUnit(prop) {
const pr = this.get(prop) || ''; const pr = this.get(prop) || '';
const noUnit = (parseFloat(pr) || 0).toString() === pr.toString(); const noUnit = (parseFloat(pr) || 0).toString() === pr.toString();
noUnit && this.set(prop, `${pr}px`); noUnit && this.set(prop, `${pr}px`);
} }
}); }

14
src/device_manager/model/Devices.js

@ -1,15 +1,15 @@
import Backbone from 'backbone'; import { Collection } from 'backbone';
import Device from './Device'; import Device from './Device';
export default Backbone.Collection.extend({ export default class Devices extends Collection {
model: Device, comparator(left, right) {
comparator: (left, right) => {
const max = Number.MAX_VALUE; const max = Number.MAX_VALUE;
return (right.get('priority') || max) - (left.get('priority') || max); return (right.get('priority') || max) - (left.get('priority') || max);
}, }
getSorted() { getSorted() {
return this.sort(); return this.sort();
} }
}); }
Devices.prototype.model = Device;

62
src/device_manager/view/DevicesView.js

@ -1,22 +1,27 @@
import { template } from 'underscore'; import { View } from 'backbone';
import Backbone from 'backbone'; import html from 'utils/html';
export default Backbone.View.extend({ export default class DevicesView extends View {
template: template(` template({ ppfx, label }) {
<div class="<%= ppfx %>device-label"><%= deviceLabel %></div> return html`
<div class="<%= ppfx %>field <%= ppfx %>select"> <div class="${ppfx}device-label">${label}</div>
<span id="<%= ppfx %>input-holder"> <div class="${ppfx}field ${ppfx}select">
<select class="<%= ppfx %>devices"></select> <span id="${ppfx}input-holder">
</span> <select class="${ppfx}devices"></select>
<div class="<%= ppfx %>sel-arrow"> </span>
<div class="<%= ppfx %>d-s-arrow"></div> <div class="${ppfx}sel-arrow">
<div class="${ppfx}d-s-arrow"></div>
</div>
</div> </div>
</div> <button style="display:none" class="${ppfx}add-trasp">+</button>
<button style="display:none" class="<%= ppfx %>add-trasp">+</button>`), `;
}
events: { events() {
change: 'updateDevice' return {
}, change: 'updateDevice'
};
}
initialize(o) { initialize(o) {
this.config = o.config || {}; this.config = o.config || {};
@ -25,14 +30,14 @@ export default Backbone.View.extend({
this.events['click .' + this.ppfx + 'add-trasp'] = this.startAdd; this.events['click .' + this.ppfx + 'add-trasp'] = this.startAdd;
this.listenTo(this.em, 'change:device', this.updateSelect); this.listenTo(this.em, 'change:device', this.updateSelect);
this.delegateEvents(); this.delegateEvents();
}, }
/** /**
* Start adding new device * Start adding new device
* @return {[type]} [description] * @return {[type]} [description]
* @private * @private
*/ */
startAdd() {}, startAdd() {}
/** /**
* Update device of the editor * Update device of the editor
@ -45,7 +50,7 @@ export default Backbone.View.extend({
var val = devEl ? devEl.val() : ''; var val = devEl ? devEl.val() : '';
em.set('device', val); em.set('device', val);
} }
}, }
/** /**
* Update select value on device update * Update select value on device update
@ -56,10 +61,10 @@ export default Backbone.View.extend({
var devEl = this.devicesEl; var devEl = this.devicesEl;
if (em && em.getDeviceModel && devEl) { if (em && em.getDeviceModel && devEl) {
var device = em.getDeviceModel(); var device = em.getDeviceModel();
var name = device ? device.get('name') : ''; var name = device ? device.get('id') : '';
devEl.val(name); devEl.val(name);
} }
}, }
/** /**
* Return devices options * Return devices options
@ -73,23 +78,20 @@ export default Backbone.View.extend({
collection.each(device => { collection.each(device => {
const { name, id } = device.attributes; const { name, id } = device.attributes;
const label = (em && em.t && em.t(`deviceManager.devices.${id}`)) || name; const label = (em && em.t && em.t(`deviceManager.devices.${id}`)) || name;
result += `<option value="${name}">${label}</option>`; result += `<option value="${id || name}">${label}</option>`;
}); });
return result; return result;
}, }
render() { render() {
const { em, ppfx, $el, el } = this; const { em, ppfx, $el, el } = this;
$el.html( const label = em && em.t && em.t('deviceManager.device');
this.template({ $el.html(this.template({ ppfx, label }));
ppfx,
deviceLabel: em && em.t && em.t('deviceManager.device')
})
);
this.devicesEl = $el.find(`.${ppfx}devices`); this.devicesEl = $el.find(`.${ppfx}devices`);
this.devicesEl.append(this.getOptions()); this.devicesEl.append(this.getOptions());
this.devicesEl.val(em.get('device'));
el.className = `${ppfx}devices-c`; el.className = `${ppfx}devices-c`;
return this; return this;
} }
}); }

3
src/dom_components/config/config.js

@ -35,7 +35,8 @@ export default {
*/ */
processor: 0, processor: 0,
// List of void elements // List of HTML void elements
// https://www.w3.org/TR/2011/WD-html-markup-20110113/syntax.html#void-elements
voidElements: [ voidElements: [
'area', 'area',
'base', 'base',

2
src/dom_components/index.js

@ -526,7 +526,7 @@ export default () => {
...model, ...model,
...getExtendedObj(extendFn, model, modelToExt), ...getExtendedObj(extendFn, model, modelToExt),
defaults: { defaults: {
...modelToExt.prototype.defaults, ...(result(modelToExt.prototype, 'defaults') || {}),
...(result(model, 'defaults') || {}) ...(result(model, 'defaults') || {})
} }
}, },

3437
src/dom_components/model/Component.js

File diff suppressed because it is too large

3
src/dom_components/model/ComponentFrame.js

@ -1,4 +1,5 @@
import Component from './Component'; import Component from './Component';
import { toLowerCase } from 'utils/mixins';
const type = 'iframe'; const type = 'iframe';
@ -17,6 +18,6 @@ export default Component.extend(
} }
}, },
{ {
isComponent: el => el.tagName === 'IFRAME' isComponent: el => toLowerCase(el.tagName) === type
} }
); );

17
src/dom_components/model/ComponentImage.js

@ -1,5 +1,6 @@
import { result } from 'underscore'; import { result } from 'underscore';
import Component from './Component'; import Component from './Component';
import { toLowerCase } from 'utils/mixins';
const svgAttrs = const svgAttrs =
'xmlns="http://www.w3.org/2000/svg" width="100" viewBox="0 0 24 24" style="fill: rgba(0,0,0,0.15); transform: scale(0.75)"'; 'xmlns="http://www.w3.org/2000/svg" width="100" viewBox="0 0 24 24" style="fill: rgba(0,0,0,0.15); transform: scale(0.75)"';
@ -138,20 +139,6 @@ export default Component.extend(
} }
}, },
{ {
/** isComponent: el => toLowerCase(el.tagName) === 'img'
* Detect if the passed element is a valid component.
* In case the element is valid an object abstracted
* from the element will be returned
* @param {HTMLElement}
* @return {Object}
* @private
*/
isComponent(el) {
var result = '';
if (el.tagName == 'IMG') {
result = { type: 'image' };
}
return result;
}
} }
); );

12
src/dom_components/model/ComponentLabel.js

@ -1,18 +1,18 @@
import Component from './ComponentText'; import Component from './ComponentText';
import { toLowerCase } from 'utils/mixins';
const type = 'label';
export default Component.extend( export default Component.extend(
{ {
defaults: { defaults: {
...Component.prototype.defaults, ...Component.prototype.defaults,
tagName: 'label', type,
tagName: type,
traits: ['id', 'title', 'for'] traits: ['id', 'title', 'for']
} }
}, },
{ {
isComponent(el) { isComponent: el => toLowerCase(el.tagName) === type
if (el.tagName == 'LABEL') {
return { type: 'label' };
}
}
} }
); );

4
src/dom_components/model/ComponentLink.js

@ -1,4 +1,5 @@
import Component from './ComponentText'; import Component from './ComponentText';
import { toLowerCase } from 'utils/mixins';
export default Component.extend( export default Component.extend(
{ {
@ -23,9 +24,8 @@ export default Component.extend(
{ {
isComponent(el) { isComponent(el) {
let result; let result;
let avoidEdit;
if (el.tagName == 'A') { if (toLowerCase(el.tagName) === 'a') {
result = { result = {
type: 'link', type: 'link',
editable: 0 editable: 0

6
src/dom_components/model/ComponentMap.js

@ -1,5 +1,6 @@
import Component from './ComponentImage'; import Component from './ComponentImage';
import OComponent from './Component'; import OComponent from './Component';
import { toLowerCase } from 'utils/mixins';
export default Component.extend( export default Component.extend(
{ {
@ -100,7 +101,10 @@ export default Component.extend(
*/ */
isComponent(el) { isComponent(el) {
var result = ''; var result = '';
if (el.tagName == 'IFRAME' && /maps\.google\.com/.test(el.src)) { if (
toLowerCase(el.tagName) == 'iframe' &&
/maps\.google\.com/.test(el.src)
) {
result = { type: 'map', src: el.src }; result = { type: 'map', src: el.src };
} }
return result; return result;

10
src/dom_components/model/ComponentScript.js

@ -1,10 +1,14 @@
import Component from './Component'; import Component from './Component';
import { toLowerCase } from 'utils/mixins';
const type = 'script';
export default Component.extend( export default Component.extend(
{ {
defaults: { defaults: {
...Component.prototype.defaults, ...Component.prototype.defaults,
type: 'script', type,
tagName: type,
droppable: false, droppable: false,
draggable: false, draggable: false,
layerable: false layerable: false
@ -12,8 +16,8 @@ export default Component.extend(
}, },
{ {
isComponent(el) { isComponent(el) {
if (el.tagName == 'SCRIPT') { if (toLowerCase(el.tagName) == type) {
var result = { type: 'script' }; const result = { type };
if (el.src) { if (el.src) {
result.src = el.src; result.src = el.src;

18
src/dom_components/model/ComponentSvg.js

@ -1,11 +1,16 @@
import Component from './Component'; import Component from './Component';
import { toLowerCase } from 'utils/mixins';
const type = 'svg';
export default Component.extend( export default Component.extend(
{ {
defaults: { defaults: {
...Component.prototype.defaults, ...Component.prototype.defaults,
resizable: { ratioDefault: 1 }, type,
highlightable: 0 tagName: type,
highlightable: 0,
resizable: { ratioDefault: 1 }
}, },
getName() { getName() {
@ -16,13 +21,6 @@ export default Component.extend(
} }
}, },
{ {
isComponent(el) { isComponent: el => toLowerCase(el.tagName) === type
if (SVGElement && el instanceof SVGElement) {
return {
tagName: el.tagName,
type: 'svg'
};
}
}
} }
); );

9
src/dom_components/model/ComponentSvgIn.js

@ -13,13 +13,6 @@ export default Component.extend(
} }
}, },
{ {
isComponent(el) { isComponent: (el, opts = {}) => !!opts.inSvg
if (Component.isComponent(el) && el.tagName.toLowerCase() !== 'svg') {
return {
tagName: el.tagName,
type: 'svg-in'
};
}
}
} }
); );

17
src/dom_components/model/ComponentTable.js

@ -1,11 +1,14 @@
import Component from './Component'; import Component from './Component';
import { toLowerCase } from 'utils/mixins';
const type = 'table';
export default Component.extend( export default Component.extend(
{ {
defaults: { defaults: {
...Component.prototype.defaults, ...Component.prototype.defaults,
type: 'table', type,
tagName: 'table', tagName: type,
droppable: ['tbody', 'thead', 'tfoot'] droppable: ['tbody', 'thead', 'tfoot']
}, },
@ -16,14 +19,6 @@ export default Component.extend(
} }
}, },
{ {
isComponent(el) { isComponent: el => toLowerCase(el.tagName) === type
let result = '';
if (el.tagName == 'TABLE') {
result = { type: 'table' };
}
return result;
}
} }
); );

17
src/dom_components/model/ComponentTableBody.js

@ -1,11 +1,14 @@
import Component from './Component'; import Component from './Component';
import { toLowerCase } from 'utils/mixins';
const type = 'tbody';
export default Component.extend( export default Component.extend(
{ {
defaults: { defaults: {
...Component.prototype.defaults, ...Component.prototype.defaults,
type: 'tbody', type,
tagName: 'tbody', tagName: type,
draggable: ['table'], draggable: ['table'],
droppable: ['tr'], droppable: ['tr'],
columns: 1, columns: 1,
@ -45,14 +48,6 @@ export default Component.extend(
} }
}, },
{ {
isComponent(el) { isComponent: el => toLowerCase(el.tagName) === type
let result = '';
if (el.tagName == 'TBODY') {
result = { type: 'tbody' };
}
return result;
}
} }
); );

15
src/dom_components/model/ComponentTableCell.js

@ -1,4 +1,5 @@
import Component from './Component'; import Component from './Component';
import { toLowerCase } from 'utils/mixins';
export default Component.extend( export default Component.extend(
{ {
@ -10,18 +11,6 @@ export default Component.extend(
} }
}, },
{ {
isComponent(el) { isComponent: el => ['td', 'th'].indexOf(toLowerCase(el.tagName)) >= 0
let result = '';
const tag = el.tagName;
if (tag == 'TD' || tag == 'TH') {
result = {
type: 'cell',
tagName: tag.toLowerCase()
};
}
return result;
}
} }
); );

17
src/dom_components/model/ComponentTableFoot.js

@ -1,22 +1,17 @@
import ComponentTableBody from './ComponentTableBody'; import ComponentTableBody from './ComponentTableBody';
import { toLowerCase } from 'utils/mixins';
const type = 'tfoot';
export default ComponentTableBody.extend( export default ComponentTableBody.extend(
{ {
defaults: { defaults: {
...ComponentTableBody.prototype.defaults, ...ComponentTableBody.prototype.defaults,
type: 'tfoot', type,
tagName: 'tfoot' tagName: type
} }
}, },
{ {
isComponent(el) { isComponent: el => toLowerCase(el.tagName) === type
let result = '';
if (el.tagName == 'TFOOT') {
result = { type: 'tfoot' };
}
return result;
}
} }
); );

17
src/dom_components/model/ComponentTableHead.js

@ -1,22 +1,17 @@
import ComponentTableBody from './ComponentTableBody'; import ComponentTableBody from './ComponentTableBody';
import { toLowerCase } from 'utils/mixins';
const type = 'thead';
export default ComponentTableBody.extend( export default ComponentTableBody.extend(
{ {
defaults: { defaults: {
...ComponentTableBody.prototype.defaults, ...ComponentTableBody.prototype.defaults,
type: 'thead', type,
tagName: 'thead' tagName: type
} }
}, },
{ {
isComponent(el) { isComponent: el => toLowerCase(el.tagName) === type
let result = '';
if (el.tagName == 'THEAD') {
result = { type: 'thead' };
}
return result;
}
} }
); );

7
src/dom_components/model/ComponentTableRow.js

@ -1,15 +1,18 @@
import Component from './Component'; import Component from './Component';
import { toLowerCase } from 'utils/mixins';
const tagName = 'tr';
export default Component.extend( export default Component.extend(
{ {
defaults: { defaults: {
...Component.prototype.defaults, ...Component.prototype.defaults,
tagName: 'tr', tagName,
draggable: ['thead', 'tbody', 'tfoot'], draggable: ['thead', 'tbody', 'tfoot'],
droppable: ['th', 'td'] droppable: ['th', 'td']
} }
}, },
{ {
isComponent: el => el.tagName == 'TR' && true isComponent: el => toLowerCase(el.tagName) === tagName
} }
); );

10
src/dom_components/model/ComponentTextNode.js

@ -1,4 +1,5 @@
import Component from './Component'; import Component from './Component';
import { escape } from 'utils/mixins';
export default Component.extend( export default Component.extend(
{ {
@ -12,12 +13,9 @@ export default Component.extend(
}, },
toHTML() { toHTML() {
return this.get('content') const parent = this.parent();
.replace(/&/g, '&amp;') const cnt = this.get('content');
.replace(/</g, '&lt;') return parent && parent.is('script') ? cnt : escape(cnt);
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
} }
}, },
{ {

24
src/dom_components/model/ComponentVideo.js

@ -1,5 +1,7 @@
import Component from './ComponentImage'; import Component from './ComponentImage';
import { toLowerCase } from 'utils/mixins';
const type = 'video';
const yt = 'yt'; const yt = 'yt';
const vi = 'vi'; const vi = 'vi';
const ytnc = 'ytnc'; const ytnc = 'ytnc';
@ -8,8 +10,8 @@ export default Component.extend(
{ {
defaults: { defaults: {
...Component.prototype.defaults, ...Component.prototype.defaults,
type: 'video', type,
tagName: 'video', tagName: type,
videoId: '', videoId: '',
void: 0, void: 0,
provider: 'so', // on change of provider, traits are switched provider: 'so', // on change of provider, traits are switched
@ -337,14 +339,18 @@ export default Component.extend(
* @private * @private
*/ */
isComponent(el) { isComponent(el) {
var result = ''; let result = '';
var isYtProv = /youtube\.com\/embed/.test(el.src); const { tagName, src } = el;
var isYtncProv = /youtube-nocookie\.com\/embed/.test(el.src); const isYtProv = /youtube\.com\/embed/.test(src);
var isViProv = /player\.vimeo\.com\/video/.test(el.src); const isYtncProv = /youtube-nocookie\.com\/embed/.test(src);
var isExtProv = isYtProv || isYtncProv || isViProv; const isViProv = /player\.vimeo\.com\/video/.test(src);
if (el.tagName == 'VIDEO' || (el.tagName == 'IFRAME' && isExtProv)) { const isExtProv = isYtProv || isYtncProv || isViProv;
if (
toLowerCase(tagName) == type ||
(toLowerCase(tagName) == 'iframe' && isExtProv)
) {
result = { type: 'video' }; result = { type: 'video' };
if (el.src) result.src = el.src; if (src) result.src = src;
if (isExtProv) { if (isExtProv) {
if (isYtProv) result.provider = yt; if (isYtProv) result.provider = yt;
else if (isYtncProv) result.provider = ytnc; else if (isYtncProv) result.provider = ytnc;

14
src/dom_components/view/ComponentImageView.js

@ -62,17 +62,17 @@ export default ComponentView.extend({
* */ * */
onActive(ev) { onActive(ev) {
ev && ev.stopPropagation(); ev && ev.stopPropagation();
var em = this.opts.config.em; const { em, model } = this;
var editor = em ? em.get('Editor') : ''; const ed = em && em.get('Editor');
if (editor && this.model.get('editable')) { if (ed && model.get('editable')) {
editor.runCommand('open-assets', { ed.runCommand('open-assets', {
target: this.model, target: model,
types: ['image'], types: ['image'],
accept: 'image/*', accept: 'image/*',
onSelect() { onSelect() {
editor.Modal.close(); ed.Modal.close();
editor.AssetManager.setTarget(null); ed.AssetManager.setTarget(null);
} }
}); });
} }

59
src/dom_components/view/ComponentScriptView.js

@ -1,4 +1,3 @@
import Backbone from 'backbone';
import ComponentView from './ComponentImageView'; import ComponentView from './ComponentImageView';
export default ComponentView.extend({ export default ComponentView.extend({
@ -7,45 +6,33 @@ export default ComponentView.extend({
events: {}, events: {},
render() { render() {
var model = this.model; const { model, em } = this;
var src = model.get('src'); const src = model.get('src');
var em = this.em; const scrCnt = em && em.get('scriptCount');
var scriptCount = em && em.get('scriptCount') ? em.get('scriptCount') : 0; const scriptCount = scrCnt ? scrCnt : 0;
var content = ''; let content = '';
// If it's an external script // If it's an external script
if (src) { if (src) {
var onload = model.get('onload'); const onload = model.get('onload');
var svar = 'script' + scriptCount; const svar = `script${scriptCount}`;
var svarNext = 'script' + (scriptCount + 1); const svarNext = `script${scriptCount + 1}`;
content = const svarFn = `${svar}Start`;
'var ' + const svarNextFn = `${svarNext}Start`;
svar + // Load multiple external scripts in the correct order
" = document.createElement('script');\n" + content = `
svar + var ${svar} = document.createElement('script');
'.onload = function(){\n' + ${svar}.onload = function() {
(onload ? onload + '();\n' : '') + ${onload ? `${onload}();\n` : ''}
'typeof ' + typeof ${svarNextFn} == 'function' && ${svarNextFn}();
svarNext + };
"Start == 'function' && " + ${svar}.src = '${src}';
svarNext + function ${svarFn}() { document.body.appendChild(${svar}); };
'Start();\n' + ${!scriptCount ? `${svarFn}();` : ''}
'};\n' + `;
svar + em && em.set('scriptCount', scriptCount + 1);
".src = '" +
src +
"';\n" +
'function ' +
svar +
'Start() { document.body.appendChild(' +
svar +
'); };\n' +
(!scriptCount ? svar + 'Start();' : '');
if (em) {
em.set('scriptCount', scriptCount + 1);
}
} else { } else {
content = model.get('content'); content = model.__innerHTML();
} }
this.el.innerHTML = content; this.el.innerHTML = content;

11
src/dom_components/view/ComponentView.js

@ -108,13 +108,6 @@ export default Backbone.View.extend({
views.splice(views.indexOf(view), 1); views.splice(views.indexOf(view), 1);
view.removed(view._clbObj()); view.removed(view._clbObj());
view.$el.data({ model: '', collection: '', view: '' }); view.$el.data({ model: '', collection: '', view: '' });
delete view.model;
delete view.$el;
delete view.el.__gjsv;
delete view.childrenView;
delete view.scriptContainer;
delete view.opts;
// delete view.el;
return view; return view;
}, },
@ -300,7 +293,7 @@ export default Backbone.View.extend({
* */ * */
updateAttributes() { updateAttributes() {
const attrs = []; const attrs = [];
const { model, $el, el, config } = this; const { model, $el, el } = this;
const { highlightable, textable, type } = model.attributes; const { highlightable, textable, type } = model.attributes;
const defaultAttr = { const defaultAttr = {
@ -318,6 +311,7 @@ export default Backbone.View.extend({
// Remove all current attributes // Remove all current attributes
each(el.attributes, attr => attrs.push(attr.nodeName)); each(el.attributes, attr => attrs.push(attr.nodeName));
attrs.forEach(attr => $el.removeAttr(attr)); attrs.forEach(attr => $el.removeAttr(attr));
this.updateStyle();
const attr = { const attr = {
...defaultAttr, ...defaultAttr,
...model.getAttributes() ...model.getAttributes()
@ -327,7 +321,6 @@ export default Backbone.View.extend({
keys(attr).forEach(key => attr[key] === false && delete attr[key]); keys(attr).forEach(key => attr[key] === false && delete attr[key]);
$el.attr(attr); $el.attr(attr);
this.updateStyle();
}, },
/** /**

2
src/dom_components/view/ComponentsView.js

@ -63,7 +63,7 @@ export default Backbone.View.extend({
const sameFrameView = frameView.model && model.getView(frameView.model); const sameFrameView = frameView.model && model.getView(frameView.model);
const dt = const dt =
opts.componentTypes || (em && em.get('DomComponents').getTypes()); opts.componentTypes || (em && em.get('DomComponents').getTypes());
const type = model.get('type'); const type = model.get('type') || 'default';
let viewObject = this.compView; let viewObject = this.compView;
for (let it = 0; it < dt.length; it++) { for (let it = 0; it < dt.length; it++) {

28
src/editor/config/config.js

@ -212,33 +212,7 @@ export default {
selectorManager: {}, selectorManager: {},
//Configurations for Device Manager //Configurations for Device Manager
deviceManager: { deviceManager: {},
devices: [
{
id: 'desktop',
name: 'Desktop',
width: ''
},
{
id: 'tablet',
name: 'Tablet',
width: '768px',
widthMedia: '992px'
},
{
id: 'mobileLandscape',
name: 'Mobile landscape',
width: '568px',
widthMedia: '768px'
},
{
id: 'mobilePortrait',
name: 'Mobile portrait',
width: '320px',
widthMedia: '480px'
}
]
},
//Configurations for Style Manager //Configurations for Style Manager
styleManager: { styleManager: {

98
src/editor/index.js

@ -88,9 +88,6 @@
* ### Modal * ### Modal
* * `modal:open` - Modal is opened * * `modal:open` - Modal is opened
* * `modal:close` - Modal is closed * * `modal:close` - Modal is closed
* ### Parser
* * `parse:html` - On HTML parse, an object containing the input and the output of the parser is passed as an argument
* * `parse:css` - On CSS parse, an object containing the input and the output of the parser is passed as an argument
* ### Commands * ### Commands
* * `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview')) * * `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview'))
* * `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview')) * * `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview'))
@ -99,6 +96,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
* ### Devices
* Check the [Devices](/api/device_manager.html) module.
* ### Parser
* Check the [Parser](/api/parser.html) module.
* ### Pages * ### Pages
* Check the [Pages](/api/pages.html) module. * Check the [Pages](/api/pages.html) module.
* ### General * ### General
@ -114,6 +115,7 @@ 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';
export default (config = {}) => { export default (config = {}) => {
const c = { const c = {
@ -122,11 +124,8 @@ export default (config = {}) => {
}; };
c.pStylePrefix = c.stylePrefix; c.pStylePrefix = c.stylePrefix;
var em = new EditorModel(c); let em = new EditorModel(c);
var editorView = new EditorView({ let editorView;
model: em,
config: c
});
return { return {
$, $,
@ -212,6 +211,7 @@ export default (config = {}) => {
/** /**
* Returns HTML built inside canvas * Returns HTML built inside canvas
* @param {Object} [opts={}] Options * @param {Object} [opts={}] Options
* @param {Component} [opts.component] Return the HTML of a specific Component
* @param {Boolean} [opts.cleanId=false] Remove unnecessary IDs (eg. those created automatically) * @param {Boolean} [opts.cleanId=false] Remove unnecessary IDs (eg. those created automatically)
* @returns {string} HTML string * @returns {string} HTML string
*/ */
@ -222,8 +222,10 @@ export default (config = {}) => {
/** /**
* Returns CSS built inside canvas * Returns CSS built inside canvas
* @param {Object} [opts={}] Options * @param {Object} [opts={}] Options
* @param {Component} [opts.component] Return the CSS of a specific Component
* @param {Boolean} [opts.json=false] Return an array of CssRules instead of the CSS string
* @param {Boolean} [opts.avoidProtected=false] Don't include protected CSS * @param {Boolean} [opts.avoidProtected=false] Don't include protected CSS
* @returns {string} CSS string * @returns {String|Array<CssRule>} CSS string or array of CssRules
*/ */
getCss(opts) { getCss(opts) {
return em.getCss(opts); return em.getCss(opts);
@ -232,8 +234,8 @@ export default (config = {}) => {
/** /**
* Returns JS of all components * Returns JS of all components
* @param {Object} [opts={}] Options * @param {Object} [opts={}] Options
* @param {Component} [opts.component] Get the JS of a particular component * @param {Component} [opts.component] Get the JS of a specific component
* @returns {string} JS string * @returns {String} JS string
*/ */
getJs(opts) { getJs(opts) {
return em.getJs(opts); return em.getJs(opts);
@ -306,13 +308,12 @@ export default (config = {}) => {
/** /**
* Set style inside editor's canvas. This method overrides actual style * Set style inside editor's canvas. This method overrides actual style
* @param {Array<Object>|Object|string} style CSS string or style model * @param {Array<Object>|Object|string} style CSS string or style model
* @param {Object} opt the options object to be used by the [setStyle]{@link em#setStyle} method
* @return {this} * @return {this}
* @example * @example
* editor.setStyle('.cls{color: red}'); * editor.setStyle('.cls{color: red}');
* //or * //or
* editor.setStyle({ * editor.setStyle({
* selectors: ['cls'] * selectors: ['cls'],
* style: { color: 'red' } * style: { color: 'red' }
* }); * });
*/ */
@ -321,6 +322,17 @@ export default (config = {}) => {
return this; return this;
}, },
/**
* Add styles to the editor
* @param {Array<Object>|Object|string} style CSS string or style model
* @returns {Array<CssRule>} Array of created CssRule instances
* @example
* editor.addStyle('.cls{color: red}');
*/
addStyle(style, opts = {}) {
return em.addStyle(style, opts);
},
/** /**
* Returns the last selected component, if there is one * Returns the last selected component, if there is one
* @return {Model} * @return {Model}
@ -406,6 +418,19 @@ export default (config = {}) => {
return this; return this;
}, },
/**
* Returns, if active, the Component enabled in rich text editing mode.
* @returns {Component|null}
* @example
* const textComp = editor.getEditing();
* if (textComp) {
* console.log('HTML: ', textComp.toHTML());
* }
*/
getEditing() {
return em.getEditing();
},
/** /**
* Set device to the editor. If the device exists it will * Set device to the editor. If the device exists it will
* change the canvas to the proper width * change the canvas to the proper width
@ -464,6 +489,17 @@ export default (config = {}) => {
return em.store(clb); return em.store(clb);
}, },
/**
* Get the JSON data object, which could be stored and loaded back with `editor.loadData(json)`
* @returns {Object}
* @example
* console.log(editor.storeData());
* // { pages: [...], styles: [...], ... }
*/
storeData() {
return em.storeData();
},
/** /**
* Load data from the current storage * Load data from the current storage
* @param {Function} clb Callback function * @param {Function} clb Callback function
@ -473,6 +509,17 @@ export default (config = {}) => {
return em.load(clb); return em.load(clb);
}, },
/**
* Load data from the JSON data object
* @param {Object} data Data to load
* @return {Object} Loaded object
* @example
* editor.loadData({ pages: [...], styles: [...], ... })
*/
loadData(data) {
return em.loadData(data);
},
/** /**
* 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
@ -666,7 +713,7 @@ export default (config = {}) => {
* @private * @private
*/ */
getEl() { getEl() {
return editorView.el; return editorView && editorView.el;
}, },
/** /**
@ -683,8 +730,25 @@ export default (config = {}) => {
* @return {HTMLElement} * @return {HTMLElement}
*/ */
render() { render() {
editorView.render(); editorView && editorView.remove();
return editorView.el; editorView = new EditorView({
} model: em,
config: c
});
return editorView.render().el;
},
/**
* Print safe HTML by using ES6 tagged template strings.
* @param {Array<String>} literals
* @param {Array<String>} substs
* @returns {String}
* @example
* const unsafeStr = '<script>....</script>';
* const safeStr = '<b>Hello</b>';
* // Use `$${var}` to avoid escaping
* const strHtml = editor.html`Escaped ${unsafeStr}, unescaped $${safeStr}`;
*/
html
}; };
}; };

105
src/editor/model/Editor.js

@ -10,7 +10,7 @@ import {
import $ from 'cash-dom'; import $ from 'cash-dom';
import Backbone from 'backbone'; import Backbone from 'backbone';
import Extender from 'utils/extender'; import Extender from 'utils/extender';
import { getModel } from 'utils/mixins'; import { getModel, hasWin } from 'utils/mixins';
import Selected from './Selected'; import Selected from './Selected';
Backbone.$ = $; Backbone.$ = $;
@ -484,6 +484,17 @@ export default Backbone.Model.extend({
return this; return this;
}, },
/**
* Add styles to the editor
* @param {Array<Object>|Object|string} style CSS string or style model
* @returns {Array<CssRule>}
* @private
*/
addStyle(style, opts = {}) {
const res = this.getStyle().add(style, opts);
return isArray(res) ? res : [res];
},
/** /**
* Returns rules/style model from the editor's canvas * Returns rules/style model from the editor's canvas
* @return {Rules} * @return {Rules}
@ -518,14 +529,12 @@ export default Backbone.Model.extend({
* @private * @private
*/ */
getHtml(opts = {}) { getHtml(opts = {}) {
const config = this.config; const { config } = this;
const { optsHtml } = config; const { optsHtml, exportWrapper, wrapperIsBody } = config;
const exportWrapper = config.exportWrapper;
const wrapperIsBody = config.wrapperIsBody;
const js = config.jsInHtml ? this.getJs(opts) : ''; const js = config.jsInHtml ? this.getJs(opts) : '';
var wrp = opts.component || this.get('DomComponents').getComponent(); const cmp = opts.component || this.get('DomComponents').getComponent();
var html = wrp let html = cmp
? this.get('CodeManager').getCode(wrp, 'html', { ? this.get('CodeManager').getCode(cmp, 'html', {
exportWrapper, exportWrapper,
wrapperIsBody, wrapperIsBody,
...optsHtml, ...optsHtml,
@ -544,8 +553,7 @@ export default Backbone.Model.extend({
*/ */
getCss(opts = {}) { getCss(opts = {}) {
const config = this.config; const config = this.config;
const { optsCss } = config; const { optsCss, wrapperIsBody } = config;
const wrapperIsBody = config.wrapperIsBody;
const avoidProt = opts.avoidProtected; const avoidProt = opts.avoidProtected;
const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles)
? opts.keepUnusedStyles ? opts.keepUnusedStyles
@ -553,16 +561,16 @@ export default Backbone.Model.extend({
const cssc = this.get('CssComposer'); const cssc = this.get('CssComposer');
const wrp = opts.component || this.get('DomComponents').getComponent(); const wrp = opts.component || this.get('DomComponents').getComponent();
const protCss = !avoidProt ? config.protectedCss : ''; const protCss = !avoidProt ? config.protectedCss : '';
const css =
return wrp wrp &&
? protCss + this.get('CodeManager').getCode(wrp, 'css', {
this.get('CodeManager').getCode(wrp, 'css', { cssc,
cssc, wrapperIsBody,
wrapperIsBody, keepUnusedStyles,
keepUnusedStyles, ...optsCss,
...optsCss ...opts
}) });
: ''; return wrp ? (opts.json ? css : protCss + css) : '';
}, },
/** /**
@ -586,16 +594,10 @@ export default Backbone.Model.extend({
* @private * @private
*/ */
store(clb) { store(clb) {
var sm = this.get('StorageManager'); const sm = this.get('StorageManager');
var store = {};
if (!sm) return; if (!sm) return;
// Fetch what to store const store = this.storeData();
this.get('storables').forEach(m => {
var obj = m.store(1);
for (var el in obj) store[el] = obj[el];
});
sm.store(store, res => { sm.store(store, res => {
clb && clb(res, store); clb && clb(res, store);
this.set('changesCount', 0); this.set('changesCount', 0);
@ -605,6 +607,18 @@ export default Backbone.Model.extend({
return store; return store;
}, },
storeData() {
let result = {};
// Sync content if there is an active RTE
const editingCmp = this.getEditing();
editingCmp && editingCmp.trigger('sync:content', { noCount: true });
this.get('storables').forEach(m => {
result = { ...result, ...m.store(1) };
});
return result;
},
/** /**
* Load data from the current storage * Load data from the current storage
* @param {Function} clb Callback function * @param {Function} clb Callback function
@ -612,14 +626,23 @@ export default Backbone.Model.extend({
*/ */
load(clb = null) { load(clb = null) {
this.getCacheLoad(1, res => { this.getCacheLoad(1, res => {
this.get('storables').forEach(module => { this.loadData(res);
module.load(res);
module.postLoad && module.postLoad(this);
});
clb && clb(res); clb && clb(res);
}); });
}, },
loadData(data = {}) {
const sm = this.get('StorageManager');
const result = sm.__clearKeys(data);
this.get('storables').forEach(module => {
module.load(result);
module.postLoad && module.postLoad(this);
});
return result;
},
/** /**
* Returns cached load * Returns cached load
* @param {Boolean} force Force to reload * @param {Boolean} force Force to reload
@ -679,7 +702,7 @@ export default Backbone.Model.extend({
stopDefault(opts = {}) { stopDefault(opts = {}) {
const commands = this.get('Commands'); const commands = this.get('Commands');
const command = commands.get(this.config.defaultCommand); const command = commands.get(this.config.defaultCommand);
if (!command) return; if (!command || !this.defaultRunning) return;
command.stop(this, this, opts); command.stop(this, this, opts);
this.defaultRunning = 0; this.defaultRunning = 0;
}, },
@ -775,7 +798,7 @@ export default Backbone.Model.extend({
* Destroy editor * Destroy editor
*/ */
destroyAll() { destroyAll() {
const { config } = this; const { config, view } = this;
const editor = this.getEditor(); const editor = this.getEditor();
const { editors = [] } = config.grapesjs || {}; const { editors = [] } = config.grapesjs || {};
this.stopDefault(); this.stopDefault();
@ -783,7 +806,7 @@ export default Backbone.Model.extend({
.slice() .slice()
.reverse() .reverse()
.forEach(mod => mod.destroy()); .forEach(mod => mod.destroy());
this.view.remove(); view && view.remove();
this.stopListening(); this.stopListening();
this.clear({ silent: true }); this.clear({ silent: true });
this.destroyed = 1; this.destroyed = 1;
@ -791,9 +814,15 @@ export default Backbone.Model.extend({
i => (this[i] = {}) i => (this[i] = {})
); );
editors.splice(editors.indexOf(editor), 1); editors.splice(editors.indexOf(editor), 1);
$(config.el) hasWin() &&
.empty() $(config.el)
.attr(this.attrsOrig); .empty()
.attr(this.attrsOrig);
},
getEditing() {
const res = this.get('editing');
return (res && res.model) || null;
}, },
setEditing(value) { setEditing(value) {

3
src/i18n/index.js

@ -27,6 +27,7 @@
* @module I18n * @module I18n
*/ */
import { isUndefined, isString } from 'underscore'; import { isUndefined, isString } from 'underscore';
import { hasWin } from 'utils/mixins';
import config from './config'; import config from './config';
const isObj = el => !Array.isArray(el) && el !== null && typeof el === 'object'; const isObj = el => !Array.isArray(el) && el !== null && typeof el === 'object';
@ -210,7 +211,7 @@ export default () => {
}, },
_localLang() { _localLang() {
const nav = window.navigator || {}; const nav = (hasWin() && window.navigator) || {};
const lang = nav.language || nav.userLanguage; const lang = nav.language || nav.userLanguage;
return lang ? lang.split('-')[0] : 'en'; return lang ? lang.split('-')[0] : 'en';
}, },

9
src/index.js

@ -36,6 +36,7 @@ export default {
* @param {Boolean} [config.autorender=true] If true, auto-render the content * @param {Boolean} [config.autorender=true] If true, auto-render the content
* @param {Array} [config.plugins=[]] Array of plugins to execute on start * @param {Array} [config.plugins=[]] Array of plugins to execute on start
* @param {Object} [config.pluginsOpts={}] Custom options for plugins * @param {Object} [config.pluginsOpts={}] Custom options for plugins
* @param {Boolean} [config.headless=false] Init headless editor
* @return {Editor} Editor instance * @return {Editor} Editor instance
* @example * @example
* var editor = grapesjs.init({ * var editor = grapesjs.init({
@ -45,10 +46,12 @@ export default {
* }) * })
*/ */
init(config = {}) { init(config = {}) {
const { headless } = config;
const els = config.container; const els = config.container;
if (!els) throw new Error("'container' is required"); if (!els && !headless) throw new Error("'container' is required");
config = { ...defaultConfig, ...config, grapesjs: this }; config = { ...defaultConfig, ...config, grapesjs: this };
config.el = isElement(els) ? els : document.querySelector(els); config.el =
!headless && (isElement(els) ? els : document.querySelector(els));
const editor = new Editor(config).init(); const editor = new Editor(config).init();
const em = editor.getModel(); const em = editor.getModel();
@ -79,7 +82,7 @@ export default {
// A plugin might have extended/added some custom type so this // A plugin might have extended/added some custom type so this
// is a good point to load stuff like components, css rules, etc. // is a good point to load stuff like components, css rules, etc.
em.loadOnStart(); em.loadOnStart();
config.autorender && editor.render(); config.autorender && !headless && editor.render();
editors.push(editor); editors.push(editor);
return editor; return editor;

5
src/keymaps/index.js

@ -32,7 +32,10 @@
*/ */
import { isString } from 'underscore'; import { isString } from 'underscore';
import keymaster from 'keymaster'; import { hasWin } from 'utils/mixins';
import keymaster from 'utils/keymaster';
hasWin() && keymaster.init(window);
export default () => { export default () => {
let em; let em;

13
src/modal_dialog/config/config.js

@ -5,5 +5,16 @@ export default {
content: '', content: '',
backdrop: true backdrop: true,
/**
* Extend ModalView object (view/ModalView.js)
* @example
* extend: {
* template() {
* return '<div>...New modal template...</div>';
* },
* },
*/
extend: {}
}; };

17
src/modal_dialog/index.js

@ -69,10 +69,6 @@ export default () => {
model = new ModalM(c); model = new ModalM(c);
model.on('change:open', (m, enb) => triggerEvent(enb, em)); model.on('change:open', (m, enb) => triggerEvent(enb, em));
modal = new ModalView({
model,
config: c
});
return this; return this;
}, },
@ -93,7 +89,8 @@ export default () => {
open(opts = {}) { open(opts = {}) {
opts.title && this.setTitle(opts.title); opts.title && this.setTitle(opts.title);
opts.content && this.setContent(opts.content); opts.content && this.setContent(opts.content);
modal.show(opts); model.open();
modal && modal.updateAttr(opts.attributes);
return this; return this;
}, },
@ -102,7 +99,7 @@ export default () => {
* @return {this} * @return {this}
*/ */
close() { close() {
modal.hide(); model.close();
return this; return this;
}, },
@ -201,11 +198,17 @@ export default () => {
* @private * @private
*/ */
render() { render() {
const View = ModalView.extend(c.extend);
modal && modal.remove();
modal = new View({
model,
config: c
});
return modal.render().$el; return modal.render().$el;
}, },
destroy() { destroy() {
modal.remove(); modal && modal.remove();
[c, model, modal].forEach(i => (i = {})); [c, model, modal].forEach(i => (i = {}));
this.em = {}; this.em = {};
} }

24
src/modal_dialog/model/Modal.js

@ -1,9 +1,19 @@
import Backbone from 'backbone'; import { Model } from 'backbone';
export default Backbone.Model.extend({ export default class Modal extends Model {
defaults: { defaults() {
title: '', return {
content: '', title: '',
open: false content: '',
open: false
};
} }
});
open() {
this.set('open', true);
}
close() {
this.set('open', false);
}
}

47
src/modal_dialog/view/ModalView.js

@ -1,6 +1,6 @@
import Backbone from 'backbone'; import { View } from 'backbone';
export default Backbone.View.extend({ export default class ModalView extends View {
template({ pfx, ppfx, content, title }) { template({ pfx, ppfx, content, title }) {
return `<div class="${pfx}dialog ${ppfx}one-bg ${ppfx}two-color"> return `<div class="${pfx}dialog ${ppfx}one-bg ${ppfx}two-color">
<div class="${pfx}header"> <div class="${pfx}header">
@ -13,12 +13,14 @@ export default Backbone.View.extend({
</div> </div>
</div> </div>
<div class="${pfx}collector" style="display: none"></div>`; <div class="${pfx}collector" style="display: none"></div>`;
}, }
events: { events() {
click: 'onClick', return {
'click [data-close-modal]': 'hide' click: 'onClick',
}, 'click [data-close-modal]': 'hide'
};
}
initialize(o) { initialize(o) {
const model = this.model; const model = this.model;
@ -30,12 +32,12 @@ export default Backbone.View.extend({
this.listenTo(model, 'change:open', this.updateOpen); this.listenTo(model, 'change:open', this.updateOpen);
this.listenTo(model, 'change:title', this.updateTitle); this.listenTo(model, 'change:title', this.updateTitle);
this.listenTo(model, 'change:content', this.updateContent); this.listenTo(model, 'change:content', this.updateContent);
}, }
onClick(e) { onClick(e) {
const bkd = this.config.backdrop; const bkd = this.config.backdrop;
bkd && e.target === this.el && this.hide(); bkd && e.target === this.el && this.hide();
}, }
/** /**
* Returns collector element * Returns collector element
@ -46,7 +48,7 @@ export default Backbone.View.extend({
if (!this.$collector) if (!this.$collector)
this.$collector = this.$el.find('.' + this.pfx + 'collector'); this.$collector = this.$el.find('.' + this.pfx + 'collector');
return this.$collector; return this.$collector;
}, }
/** /**
* Returns content element * Returns content element
@ -61,7 +63,7 @@ export default Backbone.View.extend({
} }
return this.$content; return this.$content;
}, }
/** /**
* Returns title element * Returns title element
@ -71,7 +73,7 @@ export default Backbone.View.extend({
getTitle() { getTitle() {
if (!this.$title) this.$title = this.$el.find('.' + this.pfx + 'title'); if (!this.$title) this.$title = this.$el.find('.' + this.pfx + 'title');
return this.$title.get(0); return this.$title.get(0);
}, }
/** /**
* Update content * Update content
@ -84,7 +86,7 @@ export default Backbone.View.extend({
const body = this.model.get('content'); const body = this.model.get('content');
children.length && coll.append(children); children.length && coll.append(children);
content.empty().append(body); content.empty().append(body);
}, }
/** /**
* Update title * Update title
@ -93,7 +95,7 @@ export default Backbone.View.extend({
updateTitle() { updateTitle() {
var title = this.getTitle(); var title = this.getTitle();
if (title) title.innerHTML = this.model.get('title'); if (title) title.innerHTML = this.model.get('title');
}, }
/** /**
* Update open * Update open
@ -101,24 +103,23 @@ export default Backbone.View.extend({
* */ * */
updateOpen() { updateOpen() {
this.el.style.display = this.model.get('open') ? '' : 'none'; this.el.style.display = this.model.get('open') ? '' : 'none';
}, }
/** /**
* Hide modal * Hide modal
* @private * @private
* */ * */
hide() { hide() {
this.model.set('open', 0); this.model.close();
}, }
/** /**
* Show modal * Show modal
* @private * @private
* */ * */
show(opts = {}) { show() {
this.model.set('open', 1); this.model.open();
this.updateAttr(opts.attributes); }
},
updateAttr(attr) { updateAttr(attr) {
const { pfx, $el, el } = this; const { pfx, $el, el } = this;
@ -128,7 +129,7 @@ export default Backbone.View.extend({
...(attr || {}), ...(attr || {}),
class: `${pfx}container ${(attr && attr.class) || ''}`.trim() class: `${pfx}container ${(attr && attr.class) || ''}`.trim()
}); });
}, }
render() { render() {
const el = this.$el; const el = this.$el;
@ -140,4 +141,4 @@ export default Backbone.View.extend({
this.updateOpen(); this.updateOpen();
return this; return this;
} }
}); }

19
src/navigator/index.js

@ -22,14 +22,6 @@ export default () => {
}, },
onLoad() { onLoad() {
const ItemView = View.extend(config.extend);
layers = new ItemView({
ItemView,
level: 0,
config,
opened: config.opened || {},
model: em.get('DomComponents').getWrapper()
});
em && em.on('component:selected', this.componentChanged); em && em.on('component:selected', this.componentChanged);
this.componentChanged(); this.componentChanged();
}, },
@ -60,7 +52,7 @@ export default () => {
* @return {Component} * @return {Component}
*/ */
getRoot() { getRoot() {
return layers.model; return layers && layers.model;
}, },
/** /**
@ -96,6 +88,15 @@ export default () => {
}, },
render() { render() {
const ItemView = View.extend(config.extend);
layers && layers.remove();
layers = new ItemView({
ItemView,
level: 0,
config,
opened: config.opened || {},
model: em.get('DomComponents').getWrapper()
});
return layers.render().el; return layers.render().el;
}, },

18
src/pages/index.js

@ -37,6 +37,8 @@
* * [select](#select) * * [select](#select)
* * [getSelected](#getselected) * * [getSelected](#getselected)
* *
* [Page]: page.html
*
* @module Pages * @module Pages
*/ */
@ -141,7 +143,7 @@ export default () => {
* Add new page * Add new page
* @param {Object} props Page properties * @param {Object} props Page properties
* @param {Object} [opts] Options * @param {Object} [opts] Options
* @returns {Page} * @returns {[Page]}
* @example * @example
* const newPage = pageManager.add({ * const newPage = pageManager.add({
* id: 'new-page-id', // without an explicit ID, a random one will be created * id: 'new-page-id', // without an explicit ID, a random one will be created
@ -163,8 +165,8 @@ 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]}
* @example * @example
* const removedPage = pageManager.remove('page-id'); * const removedPage = pageManager.remove('page-id');
* // or by passing the page * // or by passing the page
@ -185,7 +187,7 @@ export default () => {
/** /**
* Get page by id * Get page by id
* @param {String} id Page id * @param {String} id Page id
* @returns {Page} * @returns {[Page]}
* @example * @example
* const somePage = pageManager.get('page-id'); * const somePage = pageManager.get('page-id');
*/ */
@ -195,7 +197,7 @@ export default () => {
/** /**
* Get main page (the first one available) * Get main page (the first one available)
* @returns {Page} * @returns {[Page]}
* @example * @example
* const mainPage = pageManager.getMain(); * const mainPage = pageManager.getMain();
*/ */
@ -206,7 +208,7 @@ export default () => {
/** /**
* Get all pages * Get all pages
* @returns {Array<Page>} * @returns {Array<[Page]>}
* @example * @example
* const arrayOfPages = pageManager.getAll(); * const arrayOfPages = pageManager.getAll();
*/ */
@ -223,7 +225,7 @@ export default () => {
/** /**
* Change the selected page. This will switch the page rendered in canvas * Change the selected page. This will switch the page rendered in canvas
* @param {String|Page} page Page or page id * @param {String|[Page]} page Page or page id
* @returns {this} * @returns {this}
* @example * @example
* pageManager.select('page-id'); * pageManager.select('page-id');
@ -242,7 +244,7 @@ export default () => {
/** /**
* Get the selected page * Get the selected page
* @returns {Page} * @returns {[Page]}
* @example * @example
* const selectedPage = pageManager.getSelected(); * const selectedPage = pageManager.getSelected();
*/ */

23
src/pages/model/Page.js

@ -36,10 +36,33 @@ export default class Page extends Model {
return this.get('frames'); return this.get('frames');
} }
/**
* Get all frames
* @returns {Array<Frame>}
* @example
* const arrayOfFrames = page.getAllFrames();
*/
getAllFrames() {
return this.getFrames().models || [];
}
/**
* Get the first frame of the page (identified always as the main one)
* @returns {Frame}
* @example
* const mainFrame = page.getMainFrame();
*/
getMainFrame() { getMainFrame() {
return this.getFrames().at(0); return this.getFrames().at(0);
} }
/**
* Get the root component (usually is the `wrapper` component) from the main frame
* @returns {Component}
* @example
* const rootComponent = page.getMainComponent();
* console.log(rootComponent.toHTML());
*/
getMainComponent() { getMainComponent() {
const frame = this.getMainFrame(); const frame = this.getMainFrame();
return frame && frame.getComponent(); return frame && frame.getComponent();

13
src/panels/index.js

@ -57,10 +57,6 @@ export default () => {
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix; if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
panels = new Panels(c.defaults); panels = new Panels(c.defaults);
PanelsViewObj = new PanelsView({
collection: panels,
config: c
});
return this; return this;
}, },
@ -77,7 +73,7 @@ export default () => {
* @return {HTMLElement} * @return {HTMLElement}
*/ */
getPanelsEl() { getPanelsEl() {
return PanelsViewObj.el; return PanelsViewObj && PanelsViewObj.el;
}, },
/** /**
@ -205,6 +201,11 @@ export default () => {
* @private * @private
*/ */
render() { render() {
PanelsViewObj && PanelsViewObj.remove();
PanelsViewObj = new PanelsView({
collection: panels,
config: c
});
return PanelsViewObj.render().el; return PanelsViewObj.render().el;
}, },
@ -235,7 +236,7 @@ export default () => {
destroy() { destroy() {
panels.reset(); panels.reset();
panels.stopListening(); panels.stopListening();
PanelsViewObj.remove(); PanelsViewObj && PanelsViewObj.remove();
[c, panels, PanelsViewObj].forEach(i => (i = {})); [c, panels, PanelsViewObj].forEach(i => (i = {}));
}, },

14
src/parser/config/config.js

@ -2,8 +2,20 @@ export default {
textTags: ['br', 'b', 'i', 'u', 'a', 'ul', 'ol'], textTags: ['br', 'b', 'i', 'u', 'a', 'ul', 'ol'],
// Custom CSS parser // Custom CSS parser
// @see https://grapesjs.com/docs/guides/Custom-CSS-parser.html
parserCss: null, parserCss: null,
// Custom HTML parser // Custom HTML parser
parserHtml: null // At the moment, the custom HTML parser should rely on DOM Node instance as the result
// @example
// The return should be an instance of an Node as the root to traverse
// https://developer.mozilla.org/en-US/docs/Web/API/Node
// parserHtml: (input, opts = {}) => (new DOMParser()).parseFromString(input, 'text/xml')
// Here the result will be XMLDocument, which extends Node
parserHtml: null,
// DOMParser mime type (default 'text/html')
// @see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString
// If you use the `text/html` parser, it will fix the invalid syntax automatically
htmlType: null
}; };

99
src/parser/index.js

@ -1,3 +1,29 @@
/**
* 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/parser/config/config.js)
* ```js
* const editor = grapesjs.init({
* parser: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const { Parser } = editor;
* ```
* ## Available Events
* * `parse:html` - On HTML parse, an object containing the input and the output of the parser is passed as an argument
* * `parse:css` - On CSS parse, an object containing the input and the output of the parser is passed as an argument
*
* ## Methods
* * [getConfig](#getconfig)
* * [parseHtml](#parsehtml)
* * [parseCss](#parsecss)
*
* @module Parser
*/
import defaults from './config/config'; import defaults from './config/config';
import parserCss from './model/ParserCss'; import parserCss from './model/ParserCss';
import parserHtml from './model/ParserHtml'; import parserHtml from './model/ParserHtml';
@ -13,36 +39,8 @@ export default () => {
parserHtml: null, parserHtml: null,
/**
* Name of the module
* @type {String}
* @private
*/
name: 'Parser', name: 'Parser',
/**
* Get config object
* @return {Object}
*/
getConfig() {
return conf;
},
/**
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations
* @param {Array<Object>} [config.blocks=[]] Default blocks
* @return {this}
* @example
* ...
* {
* blocks: [
* {id:'h1-block' label: 'Heading', content:'<h1>...</h1>'},
* ...
* ],
* }
* ...
*/
init(config = {}) { init(config = {}) {
conf = { ...defaults, ...config }; conf = { ...defaults, ...config };
conf.Parser = this; conf.Parser = this;
@ -55,23 +53,48 @@ export default () => {
}, },
/** /**
* Parse HTML string and return valid model * Get the configuration object
* @param {string} str HTML string * @returns {Object} Configuration object
* @return {Object} * @example
* console.log(Parser.getConfig())
*/
getConfig() {
return conf;
},
/**
* Parse HTML string and return the object containing the Component Definition
* @param {String} input HTML string to parse
* @param {Object} [options] Options
* @param {String} [options.htmlType] [HTML mime type](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#Argument02) to parse
* @returns {Object} Object containing the result `{ html: ..., css: ... }`
* @example
* const resHtml = Parser.parseHtml(`<table><div>Hi</div></table>`, {
* htmlType: 'text/html', // default
* });
* // By using the `text/html`, this will fix automatically all the HTML syntax issues
* // Indeed the final representation, in this case, will be `<div>Hi</div><table></table>`
* const resXml = Parser.parseHtml(`<table><div>Hi</div></table>`, {
* htmlType: 'application/xml',
* });
* // This will preserve the original format as, from the XML point of view, is a valid format
*/ */
parseHtml(str) { parseHtml(input, options = {}) {
const { em, compTypes } = this; const { em, compTypes } = this;
pHtml.compTypes = em ? em.get('DomComponents').getTypes() : compTypes; pHtml.compTypes = em ? em.get('DomComponents').getTypes() : compTypes;
return pHtml.parse(str, pCss); return pHtml.parse(input, pCss, options);
}, },
/** /**
* Parse CSS string and return valid model * Parse CSS string and return an array of valid definition objects for CSSRules
* @param {string} str CSS string * @param {String} input CSS string to parse
* @return {Array<Object>} * @returns {Array<Object>} Array containing the result
* @example
* const res = Parser.parseCss('.cls { color: red }');
* // [{ ... }]
*/ */
parseCss(str) { parseCss(input) {
return pCss.parse(str); return pCss.parse(input);
}, },
destroy() { destroy() {

80
src/parser/model/BrowserParserHtml.js

@ -1,4 +1,57 @@
// PoC import { each } from 'underscore';
const htmlType = 'text/html';
const defaultType = htmlType; // 'application/xml';
export default (str, config = {}) => {
const parser = new DOMParser();
const mimeType = config.htmlType || defaultType;
const toHTML = mimeType === htmlType;
const strF = toHTML ? str : `<div>${str}</div>`;
const doc = parser.parseFromString(strF, mimeType);
let res;
if (toHTML) {
// Replicate the old parser in order to avoid breaking changes
const { head, body } = doc;
// Move all scripts at the bottom of the page
const scripts = head.querySelectorAll('script');
each(scripts, node => body.appendChild(node));
// Move inside body all head children
const hEls = [];
each(head.children, n => hEls.push(n));
each(hEls, (node, i) => body.insertBefore(node, body.children[i]));
res = body;
} else {
res = doc.firstChild;
}
return res;
};
/**
* POC, custom html parser specs
* Parse an HTML string to an array of nodes
* example
* parse(`<div class="mycls" data-test>Hello</div><span>World <b>example</b></span>`)
* // result
* [
* {
* tagName: 'div',
* attributes: { class: 'mycls', 'data-test': '' },
* childNodes: ['Hello'],
* },{
* tagName: 'span',
* childNodes: [
* 'World ',
* {
* tagName: 'b',
* childNodes: ['example'],
* }
* ],
* }
* ]
*
export const parseNodes = nodes => { export const parseNodes = nodes => {
const result = []; const result = [];
@ -44,29 +97,7 @@ export const parseNode = el => {
}; };
}; };
/** export default (str, config = {}) => {
* Parse an HTML string to an array of nodes
* @example
* parse(`<div class="mycls" data-test>Hello</div><span>World <b>example</b></span>`)
* // result
* [
* {
* tagName: 'div',
* attributes: { class: 'mycls', 'data-test': '' },
* childNodes: ['Hello'],
* },{
* tagName: 'span',
* childNodes: [
* 'World ',
* {
* tagName: 'b',
* childNodes: ['example'],
* }
* ],
* }
* ]
*/
export default (str, config) => {
const result = []; const result = [];
const el = document.createElement('div'); const el = document.createElement('div');
el.innerHTML = str; el.innerHTML = str;
@ -79,3 +110,4 @@ export default (str, config) => {
return result; return result;
}; };
*/

40
src/parser/model/ParserHtml.js

@ -1,9 +1,10 @@
import { each, isString } from 'underscore'; import { each, isString, isFunction } from 'underscore';
import BrowserParserHtml from './BrowserParserHtml';
export default config => { export default config => {
var TEXT_NODE = 'span'; let c = config;
var c = config; const modelAttrStart = 'data-gjs-';
var modelAttrStart = 'data-gjs-'; const event = 'parse:html';
return { return {
compTypes: '', compTypes: '',
@ -101,7 +102,7 @@ export default config => {
* @param {HTMLElement} el DOM element to traverse * @param {HTMLElement} el DOM element to traverse
* @return {Array<Object>} * @return {Array<Object>}
*/ */
parseNode(el) { parseNode(el, opts = {}) {
const result = []; const result = [];
const nodes = el.childNodes; const nodes = el.childNodes;
@ -128,7 +129,7 @@ export default config => {
// the first with a valid result will be that component // the first with a valid result will be that component
for (let it = 0; it < ct.length; it++) { for (let it = 0; it < ct.length; it++) {
const compType = ct[it]; const compType = ct[it];
obj = compType.model.isComponent(node); obj = compType.model.isComponent(node, opts);
if (obj) { if (obj) {
if (typeof obj !== 'object') { if (typeof obj !== 'object') {
@ -207,7 +208,10 @@ export default config => {
content: firstChild.nodeValue content: firstChild.nodeValue
}; };
} else { } else {
model.components = this.parseNode(node); model.components = this.parseNode(node, {
inSvg: model.type === 'svg',
...opts
});
} }
} }
@ -227,6 +231,11 @@ export default config => {
} }
} }
// Check for custom void elements (valid in XML)
if (!nodeChild && `${node.outerHTML}`.slice(-2) === '/>') {
model.void = true;
}
// If all children are texts and there is some textnode the parent should // If all children are texts and there is some textnode the parent should
// be text too otherwise I'm unable to edit texnodes // be text too otherwise I'm unable to edit texnodes
const comps = model.components; const comps = model.components;
@ -273,17 +282,19 @@ export default config => {
* @param {ParserCss} parserCss In case there is style tags inside HTML * @param {ParserCss} parserCss In case there is style tags inside HTML
* @return {Object} * @return {Object}
*/ */
parse(str, parserCss) { parse(str, parserCss, opts = {}) {
const { em } = c; const { em } = c;
const config = (em && em.get('Config')) || {}; const conf = (em && em.get('Config')) || {};
const res = { html: '', css: '' }; const res = { html: null, css: null };
const el = document.createElement('div'); const cf = { ...config, ...opts };
el.innerHTML = str; const el = isFunction(cf.parserHtml)
? cf.parserHtml(str, cf)
: BrowserParserHtml(str, cf);
const scripts = el.querySelectorAll('script'); const scripts = el.querySelectorAll('script');
let i = scripts.length; let i = scripts.length;
// Remove all scripts // Remove all scripts
if (!config.allowScripts) { if (!conf.allowScripts) {
while (i--) scripts[i].parentNode.removeChild(scripts[i]); while (i--) scripts[i].parentNode.removeChild(scripts[i]);
} }
@ -301,12 +312,13 @@ export default config => {
if (styleStr) res.css = parserCss.parse(styleStr); if (styleStr) res.css = parserCss.parse(styleStr);
} }
em && em.trigger(`${event}:root`, { input: str, root: el });
const result = this.parseNode(el); const result = this.parseNode(el);
// I have to keep it otherwise it breaks the DomComponents.addComponent (returns always array) // I have to keep it otherwise it breaks the DomComponents.addComponent (returns always array)
const resHtml = const resHtml =
result.length === 1 && !c.returnArray ? result[0] : result; result.length === 1 && !c.returnArray ? result[0] : result;
res.html = resHtml; res.html = resHtml;
em && em.trigger('parse:html', { input: str, output: res }); em && em.trigger(event, { input: str, output: res });
return res; return res;
} }

3
src/rich_text_editor/index.js

@ -27,7 +27,7 @@
*/ */
import RichTextEditor from './model/RichTextEditor'; import RichTextEditor from './model/RichTextEditor';
import { on, off } from 'utils/mixins'; import { on, hasWin } from 'utils/mixins';
import defaults from './config/config'; import defaults from './config/config';
export default () => { export default () => {
@ -75,6 +75,7 @@ export default () => {
this.pfx = config.stylePrefix; this.pfx = config.stylePrefix;
actions = config.actions || []; actions = config.actions || [];
if (!hasWin()) return this;
toolbar = document.createElement('div'); toolbar = document.createElement('div');
toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`; toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`;
globalRte = this.initRte(document.createElement('div')); globalRte = this.initRte(document.createElement('div'));

26
src/selector_manager/index.js

@ -99,11 +99,6 @@ export default config => {
c.stylePrefix = ppfx + c.stylePrefix; c.stylePrefix = ppfx + c.stylePrefix;
} }
this.selectorTags = new ClassTagsView({
collection: new Selectors([], { em, config: c }),
config: c
});
// Global selectors container // Global selectors container
selectors = new Selectors(c.selectors); selectors = new Selectors(c.selectors);
selectors.on('add', model => em.trigger('selector:add', model)); selectors.on('add', model => em.trigger('selector:add', model));
@ -133,6 +128,7 @@ export default config => {
select(value, opts = {}) { select(value, opts = {}) {
const targets = Array.isArray(value) ? value : [value]; const targets = Array.isArray(value) ? value : [value];
const toSelect = this.em.get('StyleManager').setTarget(targets, opts); const toSelect = this.em.get('StyleManager').setTarget(targets, opts);
const selTags = this.selectorTags;
const res = toSelect const res = toSelect
.filter(i => i) .filter(i => i)
.map(sel => .map(sel =>
@ -142,7 +138,7 @@ export default config => {
? sel ? sel
: sel.getSelectorsString() : sel.getSelectorsString()
); );
this.selectorTags.componentChanged({ targets: res }); selTags && selTags.componentChanged({ targets: res });
return this; return this;
}, },
@ -305,19 +301,21 @@ export default config => {
* @private * @private
*/ */
render(selectors) { render(selectors) {
if (selectors) { const { em, selectorTags } = this;
this.selectorTags = new ClassTagsView({ selectorTags && selectorTags.remove();
collection: new Selectors(selectors), this.selectorTags = new ClassTagsView({
config: c collection: new Selectors(selectors || [], { em, config: c }),
}); config: c
return this.selectorTags.render().el; });
} else return this.selectorTags.render().el;
return this.selectorTags.render().el;
}, },
destroy() { destroy() {
const { selectorTags } = this;
selectors.reset(); selectors.reset();
selectors.stopListening(); selectors.stopListening();
this.selectorTags.remove(); selectorTags && selectorTags.remove();
[c, selectors].forEach(i => (i = {})); [c, selectors].forEach(i => (i = {}));
this.em = {}; this.em = {};
this.selectorTags = {}; this.selectorTags = {};

10
src/selector_manager/view/ClassTagsView.js

@ -67,11 +67,10 @@ export default Backbone.View.extend({
const coll = this.collection; const coll = this.collection;
this.target = this.config.em; this.target = this.config.em;
this.em = em; this.em = em;
const emitter = this.getStyleEmitter();
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';
this.listenTo(em, toList, this.componentChanged); this.listenTo(em, toList, this.componentChanged);
this.listenTo(emitter, 'update', this.componentChanged); this.listenTo(em, 'styleManager:update', this.componentChanged);
this.listenTo(em, toListCls, this.__handleStateChange); this.listenTo(em, toListCls, this.__handleStateChange);
this.listenTo(em, 'styleable:change change:device', this.checkSync); // component:styleUpdate this.listenTo(em, 'styleable:change change:device', this.checkSync); // component:styleUpdate
this.listenTo(coll, 'add', this.addNew); this.listenTo(coll, 'add', this.addNew);
@ -116,13 +115,6 @@ export default Backbone.View.extend({
}); });
}, },
getStyleEmitter() {
const { em } = this;
const sm = em && em.get('StyleManager');
const emitter = sm && sm.getEmitter();
return emitter || {};
},
/** /**
* Triggered when a tag is removed from collection * Triggered when a tag is removed from collection
* @param {Object} model Removed model * @param {Object} model Removed model

32
src/storage_manager/index.js

@ -239,9 +239,9 @@ export default () => {
* }); * });
* */ * */
load(keys, clb) { load(keys, clb) {
var st = this.get(this.getCurrent()); const st = this.get(this.getCurrent());
var keysF = []; const keysF = [];
var result = {}; let result = {};
if (typeof keys === 'string') keys = [keys]; if (typeof keys === 'string') keys = [keys];
this.onStart('load', keys); this.onStart('load', keys);
@ -254,13 +254,7 @@ export default () => {
st.load( st.load(
keysF, keysF,
res => { res => {
// Restore keys name result = this.__clearKeys(res);
var reg = new RegExp('^' + c.id + '');
for (var itemKey in res) {
var itemKeyR = itemKey.replace(reg, '');
result[itemKeyR] = res[itemKey];
}
this.onAfter('load', result); this.onAfter('load', result);
clb && clb(result); clb && clb(result);
this.onEnd('load', result); this.onEnd('load', result);
@ -275,6 +269,24 @@ export default () => {
} }
}, },
/**
* Restore key names
* @param {Object} data
* @returns {Object}
* @private
*/
__clearKeys(data = {}) {
const result = {};
const reg = new RegExp('^' + c.id + '');
for (let itemKey in data) {
const itemKeyR = itemKey.replace(reg, '');
result[itemKeyR] = data[itemKey];
}
return result;
},
/** /**
* Load default storages * Load default storages
* @return {this} * @return {this}

52
src/storage_manager/model/LocalStorage.js

@ -1,6 +1,7 @@
import Backbone from 'backbone'; import { Model } from 'backbone';
import { hasWin } from 'utils/mixins';
export default Backbone.Model.extend({ export default Model.extend({
defaults: { defaults: {
checkLocal: true checkLocal: true
}, },
@ -8,31 +9,28 @@ export default Backbone.Model.extend({
/** /**
* @private * @private
*/ */
store(data, clb) { store(data, clb = () => {}) {
this.checkStorageEnvironment(); if (this.hasLocal()) {
for (let key in data) localStorage.setItem(key, data[key]);
for (var key in data) localStorage.setItem(key, data[key]);
if (typeof clb == 'function') {
clb();
} }
clb && clb();
}, },
/** /**
* @private * @private
*/ */
load(keys, clb) { load(keys, clb = () => {}) {
this.checkStorageEnvironment(); const result = {};
var result = {};
if (this.hasLocal()) {
for (var i = 0, len = keys.length; i < len; i++) { for (let i = 0, len = keys.length; i < len; i++) {
var value = localStorage.getItem(keys[i]); const value = localStorage.getItem(keys[i]);
if (value) result[keys[i]] = value; if (value) result[keys[i]] = value;
}
} }
if (typeof clb == 'function') { clb && clb(result);
clb(result);
}
return result; return result;
}, },
@ -41,9 +39,9 @@ export default Backbone.Model.extend({
* @private * @private
*/ */
remove(keys) { remove(keys) {
this.checkStorageEnvironment(); if (!this.hasLocal()) return;
for (var i = 0, len = keys.length; i < len; i++) for (let i = 0, len = keys.length; i < len; i++)
localStorage.removeItem(keys[i]); localStorage.removeItem(keys[i]);
}, },
@ -51,8 +49,14 @@ export default Backbone.Model.extend({
* Check storage environment * Check storage environment
* @private * @private
* */ * */
checkStorageEnvironment() { hasLocal() {
if (this.get('checkLocal') && !localStorage) const win = hasWin();
console.warn("Your browser doesn't support localStorage");
if (this.get('checkLocal') && (!win || !localStorage)) {
win && console.warn("Your browser doesn't support localStorage");
return false;
}
return true;
} }
}); });

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

Loading…
Cancel
Save