Browse Source

Merge pull request #3411 from artf/page-manager

Page manager
pull/3436/head v0.17.3
Artur Arseniev 5 years ago
committed by GitHub
parent
commit
e914a3fe49
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. 1
      docs/.vuepress/config.js
  5. 1
      docs/api.js
  6. 73
      docs/api/canvas.md
  7. 28
      docs/api/component.md
  8. 4
      docs/api/css_composer.md
  9. 142
      docs/api/editor.md
  10. 177
      docs/api/pages.md
  11. 12
      docs/api/undo_manager.md
  12. 26
      package-lock.json
  13. 4
      package.json
  14. 3
      src/canvas/config/config.js
  15. 55
      src/canvas/index.js
  16. 32
      src/canvas/model/Canvas.js
  17. 146
      src/canvas/model/Frame.js
  18. 23
      src/canvas/model/Frames.js
  19. 68
      src/canvas/view/CanvasView.js
  20. 23
      src/canvas/view/FrameView.js
  21. 55
      src/canvas/view/FrameWrapView.js
  22. 4
      src/canvas/view/FramesView.js
  23. 2
      src/code_manager/model/CssGenerator.js
  24. 19
      src/commands/view/CommandAbstract.js
  25. 30
      src/commands/view/SelectComponent.js
  26. 24
      src/commands/view/SwitchVisibility.js
  27. 38
      src/css_composer/index.js
  28. 32
      src/css_composer/model/CssRule.js
  29. 4
      src/css_composer/model/CssRules.js
  30. 9
      src/css_composer/view/CssRuleView.js
  31. 119
      src/dom_components/index.js
  32. 95
      src/dom_components/model/Component.js
  33. 1
      src/dom_components/model/ComponentText.js
  34. 31
      src/dom_components/model/ComponentWrapper.js
  35. 15
      src/dom_components/model/Components.js
  36. 11
      src/dom_components/view/ComponentView.js
  37. 8
      src/domain_abstract/view/DomainViews.js
  38. 5
      src/editor/index.js
  39. 32
      src/editor/model/Editor.js
  40. 4
      src/editor/view/EditorView.js
  41. 2
      src/navigator/index.js
  42. 8
      src/navigator/view/ItemView.js
  43. 297
      src/pages/index.js
  44. 61
      src/pages/model/Page.js
  45. 26
      src/pages/model/Pages.js
  46. 3
      src/panels/model/Buttons.js
  47. 50
      src/panels/view/ButtonView.js
  48. 14
      src/rich_text_editor/index.js
  49. 14
      src/storage_manager/index.js
  50. 2
      src/style_manager/index.js
  51. 3
      src/styles/scss/_gjs_canvas.scss
  52. 115
      src/undo_manager/index.js
  53. 7
      src/utils/Droppable.js
  54. 4
      src/utils/Sorter.js
  55. 12
      src/utils/mixins.js
  56. 20
      test/specs/css_composer/index.js
  57. 22
      test/specs/dom_components/index.js
  58. 13
      test/specs/dom_components/model/Component.js
  59. 4
      test/specs/dom_components/model/Symbols.js
  60. 14
      test/specs/editor/index.js
  61. 3
      test/specs/keymaps/index.js
  62. 252
      test/specs/pages/index.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

1
docs/.vuepress/config.js

@ -65,6 +65,7 @@ module.exports = {
['/api/components', 'DOM Components'],
['/api/component', ' - Component'],
['/api/panels', 'Panels'],
['/api/pages', 'Pages'],
['/api/style_manager', 'Style Manager'],
['/api/storage_manager', 'Storage Manager'],
['/api/device_manager', 'Device Manager'],

1
docs/api.js

@ -23,6 +23,7 @@ const cmds = [
['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(' && ');

73
docs/api/canvas.md

@ -24,36 +24,35 @@ const canvas = editor.Canvas;
- [getWindow][5]
- [getDocument][6]
- [getBody][7]
- [getWrapperEl][8]
- [setCustomBadgeLabel][9]
- [hasFocus][10]
- [scrollTo][11]
- [setZoom][12]
- [getZoom][13]
- [setCustomBadgeLabel][8]
- [hasFocus][9]
- [scrollTo][10]
- [setZoom][11]
- [getZoom][12]
## getConfig
Get the configuration object
Returns **[Object][14]**
Returns **[Object][13]**
## getElement
Get the canvas element
Returns **[HTMLElement][15]**
Returns **[HTMLElement][14]**
## getFrameEl
Get the iframe element of the canvas
Returns **[HTMLIFrameElement][16]**
Returns **[HTMLIFrameElement][15]**
## getWindow
Get the window instance of the iframe element
Returns **[Window][17]**
Returns **[Window][16]**
## getDocument
@ -65,13 +64,7 @@ Returns **HTMLDocument**
Get the body of the iframe element
Returns **[HTMLBodyElement][18]**
## getWrapperEl
Get the wrapper element containing all the components
Returns **[HTMLElement][15]**
Returns **[HTMLBodyElement][17]**
## setCustomBadgeLabel
@ -79,7 +72,7 @@ Set custom badge naming strategy
### Parameters
- `f` **[Function][19]**
- `f` **[Function][18]**
### Examples
@ -93,13 +86,13 @@ canvas.setCustomBadgeLabel(function(component){
Get canvas rectangular data
Returns **[Object][14]**
Returns **[Object][13]**
## hasFocus
Check if the canvas is focused
Returns **[Boolean][20]**
Returns **[Boolean][19]**
## scrollTo
@ -110,9 +103,9 @@ passed to it. For instance, you can scroll smoothly by using
### Parameters
- `el` **([HTMLElement][15] | Component)**
- `opts` **[Object][14]** Options, same as options for `scrollIntoView` (optional, default `{}`)
- `opts.force` **[Boolean][20]** Force the scroll, even if the element is already visible (optional, default `false`)
- `el` **([HTMLElement][14] | Component)**
- `opts` **[Object][13]** 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`)
### Examples
@ -130,7 +123,7 @@ Set zoom value
### Parameters
- `value` **[Number][21]** The zoom value, from 0 to 100
- `value` **[Number][20]** The zoom value, from 0 to 100
Returns **this**
@ -138,7 +131,7 @@ Returns **this**
Get zoom value
Returns **[Number][21]**
Returns **[Number][20]**
## addFrame
@ -146,7 +139,7 @@ Add new frame to the canvas
### Parameters
- `props` **[Object][14]** Frame properties (optional, default `{}`)
- `props` **[Object][13]** Frame properties (optional, default `{}`)
- `opts` (optional, default `{}`)
### Examples
@ -186,30 +179,28 @@ Returns **Frame**
[7]: #getbody
[8]: #getwrapperel
[9]: #setcustombadgelabel
[8]: #setcustombadgelabel
[10]: #hasfocus
[9]: #hasfocus
[11]: #scrollto
[10]: #scrollto
[12]: #setzoom
[11]: #setzoom
[13]: #getzoom
[12]: #getzoom
[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[15]: https://developer.mozilla.org/docs/Web/HTML/Element
[14]: https://developer.mozilla.org/docs/Web/HTML/Element
[16]: https://developer.mozilla.org/docs/Web/API/HTMLIFrameElement
[15]: https://developer.mozilla.org/docs/Web/API/HTMLIFrameElement
[17]: https://developer.mozilla.org/docs/Web/API/Window
[16]: https://developer.mozilla.org/docs/Web/API/Window
[18]: https://developer.mozilla.org/docs/Web/HTML/Element/body
[17]: https://developer.mozilla.org/docs/Web/HTML/Element/body
[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[21]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number

28
docs/api/component.md

@ -255,6 +255,25 @@ component.addAttributes({ 'data-key': 'value' });
Returns **this**
## removeAttributes
Remove attributes from the component
### Parameters
- `attrs` **([String][1] \| [Array][4]<[String][1]>)** Array of attributes to remove (optional, default `[]`)
- `opts` (optional, default `{}`)
- `options` **[Object][2]** Options for the model update
### Examples
```javascript
component.removeAttributes('some-attr');
component.removeAttributes(['some-attr1', 'some-attr2']);
```
Returns **this**
## getStyle
Get the style of the component
@ -282,6 +301,10 @@ Returns **[Object][2]**
Return all component's attributes
### Parameters
- `opts` (optional, default `{}`)
Returns **[Object][2]**
## addClass
@ -354,7 +377,7 @@ Add new component children
### Parameters
- `components` **([Component][9] \| [String][1])** Component to add
- `opts` **[Object][2]** Options, same as in `model.add()`(from backbone) (optional, default `{}`)
- `opts` **[Object][2]** Options for the append action (optional, default `{}`)
### Examples
@ -366,6 +389,8 @@ someComponent.get('components').length // -> 2
// You can pass components directly
otherComponent.append(otherComponent2);
otherComponent.append([otherComponent3, otherComponent4]);
// append at specific index (eg. at the beginning)
someComponent.append(otherComponent, { at: 0 });
```
Returns **[Array][4]** Array of appended components
@ -378,6 +403,7 @@ current collection is returned
### Parameters
- `components` **([Component][9] \| [String][1])?** Components to set
- `opts` **[Object][2]** Options, same as in `Component.append()` (optional, default `{}`)
### Examples

4
docs/api/css_composer.md

@ -115,6 +115,10 @@ Returns **Collection**
Remove all rules
### Parameters
- `opts` (optional, default `{}`)
Returns **this**
## setRule

142
docs/api/editor.md

@ -27,6 +27,7 @@ editor.on('EVENT-NAME', (some, argument) => {
- `component:mount` - Component is mounted to an element and rendered in canvas
- `component:add` - Triggered when a new component is added to the editor, the model is passed as an argument to the callback
- `component:remove` - Triggered when a component is removed, the model is passed as an argument to the callback
- `component:remove:before` - Triggered before the remove of the component, the model, remove function (if aborted via options, with this function you can complete the remove) and options (use options.abort = true to prevent remove), are passed as arguments to the callback
- `component:clone` - Triggered when a component is cloned, the new model is passed as an argument to the callback
- `component:update` - Triggered when a component is updated (moved, styled, etc.), the model is passed as an argument to the callback
- `component:update:{propertyName}` - Listen any property change, the model is passed as an argument to the callback
@ -126,6 +127,10 @@ editor.on('EVENT-NAME', (some, argument) => {
- `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
### Pages
Check the [Pages][2] module.
### General
- `canvasScroll` - Canvas is scrolled
@ -140,7 +145,7 @@ Returns configuration object
### Parameters
- `prop` **[string][2]?** Property name
- `prop` **[string][3]?** Property name
Returns **any** Returns the configuration object or
the value of the specified property
@ -151,9 +156,10 @@ Returns HTML built inside canvas
### Parameters
- `opts`
- `opts` **[Object][4]** Options (optional, default `{}`)
- `opts.cleanId` **[Boolean][5]** Remove unnecessary IDs (eg. those created automatically) (optional, default `false`)
Returns **[string][2]** HTML string
Returns **[string][3]** HTML string
## getCss
@ -161,16 +167,16 @@ Returns CSS built inside canvas
### Parameters
- `opts` **[Object][3]** Options (optional, default `{}`)
- `opts.avoidProtected` **[Boolean][4]** Don't include protected CSS (optional, default `false`)
- `opts` **[Object][4]** Options (optional, default `{}`)
- `opts.avoidProtected` **[Boolean][5]** Don't include protected CSS (optional, default `false`)
Returns **[string][2]** CSS string
Returns **[string][3]** CSS string
## getJs
Returns JS of all components
Returns **[string][2]** JS string
Returns **[string][3]** JS string
## getComponents
@ -190,8 +196,8 @@ Set components inside editor's canvas. This method overrides actual components
### Parameters
- `components` **([Array][5]<[Object][3]> | [Object][3] \| [string][2])** HTML string or components model
- `opt` **[Object][3]** the options object to be used by the [setComponents][em#setComponents][6] method (optional, default `{}`)
- `components` **([Array][6]<[Object][4]> | [Object][4] \| [string][3])** HTML string or components model
- `opt` **[Object][4]** the options object to be used by the [setComponents][em#setComponents][7] method (optional, default `{}`)
### Examples
@ -213,9 +219,9 @@ Add components
### Parameters
- `components` **([Array][5]<[Object][3]> | [Object][3] \| [string][2])** HTML string or components model
- `opts` **[Object][3]** Options
- `opts.avoidUpdateStyle` **[Boolean][4]** If the HTML string contains styles,
- `components` **([Array][6]<[Object][4]> | [Object][4] \| [string][3])** HTML string or components model
- `opts` **[Object][4]** Options
- `opts.avoidUpdateStyle` **[Boolean][5]** If the HTML string contains styles,
by default, they will be created and, if already exist, updated. When this option
is true, styles already created will not be updated. (optional, default `false`)
@ -231,13 +237,13 @@ editor.addComponents({
});
```
Returns **[Array][5]<Component>**
Returns **[Array][6]<Component>**
## getStyle
Returns style in JSON format object
Returns **[Object][3]**
Returns **[Object][4]**
## setStyle
@ -245,8 +251,8 @@ Set style inside editor's canvas. This method overrides actual style
### Parameters
- `style` **([Array][5]<[Object][3]> | [Object][3] \| [string][2])** CSS string or style model
- `opt` **[Object][3]** the options object to be used by the [setStyle][em#setStyle][7] method (optional, default `{}`)
- `style` **([Array][6]<[Object][4]> | [Object][4] \| [string][3])** CSS string or style model
- `opt` **[Object][4]** the options object to be used by the [setStyle][em#setStyle][8] method (optional, default `{}`)
### Examples
@ -271,7 +277,7 @@ Returns **Model**
Returns an array of all selected components
Returns **[Array][5]**
Returns **[Array][6]**
## getSelectedToStyle
@ -289,9 +295,9 @@ Select a component
### Parameters
- `el` **(Component | [HTMLElement][8])** Component to select
- `opts` **[Object][3]?** Options
- `opts.scroll` **[Boolean][4]?** Scroll canvas to the selected element
- `el` **(Component | [HTMLElement][9])** Component to select
- `opts` **[Object][4]?** Options
- `opts.scroll` **[Boolean][5]?** Scroll canvas to the selected element
### Examples
@ -310,7 +316,7 @@ Add component to selection
### Parameters
- `el` **(Component | [HTMLElement][8] \| [Array][5])** Component to select
- `el` **(Component | [HTMLElement][9] \| [Array][6])** Component to select
### Examples
@ -326,7 +332,7 @@ Remove component from selection
### Parameters
- `el` **(Component | [HTMLElement][8] \| [Array][5])** Component to select
- `el` **(Component | [HTMLElement][9] \| [Array][6])** Component to select
### Examples
@ -342,7 +348,7 @@ Toggle component selection
### Parameters
- `el` **(Component | [HTMLElement][8] \| [Array][5])** Component to select
- `el` **(Component | [HTMLElement][9] \| [Array][6])** Component to select
### Examples
@ -359,7 +365,7 @@ change the canvas to the proper width
### Parameters
- `name` **[string][2]** Name of the device
- `name` **[string][3]** Name of the device
### Examples
@ -381,7 +387,7 @@ console.log(device);
// 'Tablet'
```
Returns **[string][2]** Device name
Returns **[string][3]** Device name
## runCommand
@ -389,8 +395,8 @@ Execute command
### Parameters
- `id` **[string][2]** Command ID
- `options` **[Object][3]** Custom options (optional, default `{}`)
- `id` **[string][3]** Command ID
- `options` **[Object][4]** Custom options (optional, default `{}`)
### Examples
@ -406,8 +412,8 @@ Stop the command if stop method was provided
### Parameters
- `id` **[string][2]** Command ID
- `options` **[Object][3]** Custom options (optional, default `{}`)
- `id` **[string][3]** Command ID
- `options` **[Object][4]** Custom options (optional, default `{}`)
### Examples
@ -423,9 +429,9 @@ Store data to the current storage
### Parameters
- `clb` **[Function][9]** Callback function
- `clb` **[Function][10]** Callback function
Returns **[Object][3]** Stored data
Returns **[Object][4]** Stored data
## load
@ -433,23 +439,23 @@ Load data from the current storage
### Parameters
- `clb` **[Function][9]** Callback function
- `clb` **[Function][10]** Callback function
Returns **[Object][3]** Stored data
Returns **[Object][4]** Stored data
## getContainer
Returns container element. The one which was indicated as 'container'
on init method
Returns **[HTMLElement][8]**
Returns **[HTMLElement][9]**
## getDirtyCount
Return the count of changes made to the content and not yet stored.
This count resets at any `store()`
Returns **[number][10]**
Returns **[number][11]**
## refresh
@ -462,8 +468,8 @@ refresh you'll get misleading position of tools
### Parameters
- `opts`
- `options` **[Object][3]?** Options
- `options.tools` **[Boolean][4]** Update the position of tools (eg. rich text editor, component highlighter, etc.) (optional, default `false`)
- `options` **[Object][4]?** Options
- `options.tools` **[Boolean][5]** Update the position of tools (eg. rich text editor, component highlighter, etc.) (optional, default `false`)
## setCustomRte
@ -471,7 +477,7 @@ Replace the built-in Rich Text Editor with a custom one.
### Parameters
- `obj` **[Object][3]** Custom RTE Interface
- `obj` **[Object][4]** Custom RTE Interface
### Examples
@ -511,7 +517,7 @@ custom parser, pass `null` as the argument
### Parameters
- `parser` **([Function][9] | null)** Parser function
- `parser` **([Function][10] | null)** Parser function
### Examples
@ -533,11 +539,11 @@ Returns **this**
## setDragMode
Change the global drag mode of components.
To get more about this feature read: [https://github.com/artf/grapesjs/issues/1936][11]
To get more about this feature read: [https://github.com/artf/grapesjs/issues/1936][12]
### Parameters
- `value` **[String][2]** Drag mode, options: 'absolute' | 'translate'
- `value` **[String][3]** Drag mode, options: 'absolute' | 'translate'
Returns **this**
@ -548,9 +554,9 @@ Trigger event log message
### Parameters
- `msg` **any** Message to log
- `opts` **[Object][3]** Custom options (optional, default `{}`)
- `opts.ns` **[String][2]** Namespace of the log (eg. to use in plugins) (optional, default `''`)
- `opts.level` **[String][2]** Level of the log, `debug`, `info`, `warning`, `error` (optional, default `'debug'`)
- `opts` **[Object][4]** Custom options (optional, default `{}`)
- `opts.ns` **[String][3]** 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'`)
### Examples
@ -572,10 +578,10 @@ Translate label
### Parameters
- `args` **...any**
- `key` **[String][2]** Label to translate
- `opts` **[Object][3]?** Options for the translation
- `opts.params` **[Object][3]?** Params for the translation
- `opts.noWarn` **[Boolean][4]?** Avoid warnings in case of missing resources
- `key` **[String][3]** Label to translate
- `opts` **[Object][4]?** Options for the translation
- `opts.params` **[Object][4]?** Params for the translation
- `opts.noWarn` **[Boolean][5]?** Avoid warnings in case of missing resources
### Examples
@ -587,7 +593,7 @@ editor.t('msg2', { params: { test: 'hello' } });
editor.t('msg2', { params: { test: 'hello' }, l: 'it' });
```
Returns **[String][2]**
Returns **[String][3]**
## on
@ -595,8 +601,8 @@ Attach event
### Parameters
- `event` **[string][2]** Event name
- `callback` **[Function][9]** Callback function
- `event` **[string][3]** Event name
- `callback` **[Function][10]** Callback function
Returns **this**
@ -606,8 +612,8 @@ Attach event and detach it after the first run
### Parameters
- `event` **[string][2]** Event name
- `callback` **[Function][9]** Callback function
- `event` **[string][3]** Event name
- `callback` **[Function][10]** Callback function
Returns **this**
@ -617,8 +623,8 @@ Detach event
### Parameters
- `event` **[string][2]** Event name
- `callback` **[Function][9]** Callback function
- `event` **[string][3]** Event name
- `callback` **[Function][10]** Callback function
Returns **this**
@ -628,7 +634,7 @@ Trigger event
### Parameters
- `event` **[string][2]** Event to trigger
- `event` **[string][3]** Event to trigger
Returns **this**
@ -640,26 +646,28 @@ Destroy the editor
Render editor
Returns **[HTMLElement][8]**
Returns **[HTMLElement][9]**
[1]: https://github.com/artf/grapesjs/blob/master/src/editor/config/config.js
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: /api/pages.html
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[6]: em#setComponents
[7]: em#setComponents
[7]: em#setStyle
[8]: em#setStyle
[8]: https://developer.mozilla.org/docs/Web/HTML/Element
[9]: https://developer.mozilla.org/docs/Web/HTML/Element
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[11]: https://github.com/artf/grapesjs/issues/1936
[12]: https://github.com/artf/grapesjs/issues/1936

177
docs/api/pages.md

@ -0,0 +1,177 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## Pages
You can customize the initial state of the module from the editor initialization
```js
const editor = grapesjs.init({
....
pageManager: {
pages: [
{
id: 'page-id',
styles: `.my-class { color: red }`, // or a JSON of styles
component: '<div class="my-class">My element</div>', // or a JSON of components
}
]
},
})
```
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
```js
const pageManager = editor.Pages;
```
## Available Events
- `page:add` - Added new page. The page is passed as an argument to the callback
- `page:remove` - Page removed. The page is passed as an argument to the callback
- `page:select` - New page selected. The newly selected page and the previous one, are passed as arguments to the callback
- `page:update` - Page updated. The updated page and the object containing changes are passed as arguments to the callback
- `page` - 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][1]
- [get][2]
- [getAll][3]
- [getMain][4]
- [remove][5]
- [select][6]
- [getSelected][7]
## add
Add new page
### Parameters
- `props` **[Object][8]** Page properties
- `opts` **[Object][8]?** Options (optional, default `{}`)
### Examples
```javascript
const newPage = pageManager.add({
id: 'new-page-id', // without an explicit ID, a random one will be created
styles: `.my-class { color: red }`, // or a JSON of styles
component: '<div class="my-class">My element</div>', // or a JSON of components
});
```
Returns **Page**
## remove
Remove page
### Parameters
- `page` **([String][9] | Page)** Page or page id
- `opts` (optional, default `{}`)
### Examples
```javascript
const removedPage = pageManager.remove('page-id');
// or by passing the page
const somePage = pageManager.get('page-id');
pageManager.remove(somePage);
```
Returns **Page**
## get
Get page by id
### Parameters
- `id` **[String][9]** Page id
### Examples
```javascript
const somePage = pageManager.get('page-id');
```
Returns **Page**
## getMain
Get main page (the first one available)
### Examples
```javascript
const mainPage = pageManager.getMain();
```
Returns **Page**
## getAll
Get all pages
### Examples
```javascript
const arrayOfPages = pageManager.getAll();
```
Returns **[Array][10]&lt;Page>**
## select
Change the selected page. This will switch the page rendered in canvas
### Parameters
- `page` **([String][9] | Page)** Page or page id
- `opts` (optional, default `{}`)
### Examples
```javascript
pageManager.select('page-id');
// or by passing the page
const somePage = pageManager.get('page-id');
pageManager.select(somePage);
```
Returns **this**
## getSelected
Get the selected page
### Examples
```javascript
const selectedPage = pageManager.getSelected();
```
Returns **Page**
[1]: #add
[2]: #get
[3]: #getall
[4]: #getmain
[5]: #remove
[6]: #select
[7]: #getselected
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

12
docs/api/undo_manager.md

@ -186,6 +186,18 @@ um.hasRedo();
Returns **[Boolean][16]**
## isRegistered
Check if the entity (Model/Collection) to tracked
Note: New Components and CSSRules will be added automatically
### Parameters
- `obj`
- `entity` **(Model | Collection)** Entity to track
Returns **[Boolean][16]**
## getStack
Get stack of changes

26
package-lock.json

@ -1,6 +1,6 @@
{
"name": "grapesjs",
"version": "0.16.45",
"version": "0.17.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -6693,24 +6693,24 @@
"dev": true
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"dev": true
}
}

4
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.16.45",
"version": "0.17.3",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",
@ -113,7 +113,7 @@
"lint": "eslint src",
"check": "npm run lint && npm run test",
"build": "npm run check && run-s build:*",
"build:js": "grapesjs-cli build --targets=\"> 1%, ie 11, safari 8, not dead\" --statsOutput=\"stats.json\"",
"build:js": "grapesjs-cli build --targets=\"> 1%, ie 11, safari 8, not dead\" --statsOutput=\"stats.json\" --localePath=\"src/i18n/locale\"",
"build:css": "sass src/styles/scss/main.scss dist/css/grapes.min.css --no-source-map --style=compressed --load-path=node_modules",
"build:locale": "rm -rf ./locale && node scripts/build-locale.js && babel locale -d locale --copy-files --no-comments",
"start": "run-p start:*",

3
src/canvas/config/config.js

@ -35,6 +35,9 @@ export default {
*/
autoscrollLimit: 50,
// Experimental: external highlighter box
extHl: 0,
/**
* When some textable component is selected and focused (eg. input or text component) the editor
* stops some commands (eg. disables the copy/paste of components with CTRL+C/V to allow the copy/paste of the text).

55
src/canvas/index.js

@ -20,7 +20,6 @@
* * [getWindow](#getwindow)
* * [getDocument](#getdocument)
* * [getBody](#getbody)
* * [getWrapperEl](#getwrapperel)
* * [setCustomBadgeLabel](#setcustombadgelabel)
* * [hasFocus](#hasfocus)
* * [scrollTo](#scrollto)
@ -30,8 +29,7 @@
* @module Canvas
*/
import { hasDnd, getElement, getViewEl } from 'utils/mixins';
import Droppable from 'utils/Droppable';
import { getElement, getViewEl } from 'utils/mixins';
import defaults from './config/config';
import Canvas from './model/Canvas';
import canvasView from './view/CanvasView';
@ -72,20 +70,23 @@ export default () => {
this.em = c.em;
const ppfx = c.pStylePrefix;
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
canvas = new Canvas(config);
this.model = canvas;
this.startAutoscroll = this.startAutoscroll.bind(this);
this.stopAutoscroll = this.stopAutoscroll.bind(this);
return this;
},
onLoad() {
this.model.init();
CanvasView = new canvasView({
model: canvas,
config: c
});
},
var cm = c.em.get('DomComponents');
if (cm) this.setWrapper(cm);
this.model = canvas;
this.startAutoscroll = this.startAutoscroll.bind(this);
this.stopAutoscroll = this.stopAutoscroll.bind(this);
return this;
getModel() {
return canvas;
},
/**
@ -96,15 +97,6 @@ export default () => {
return c;
},
/**
* Add wrapper
* @param {Object} wrp Wrapper
* @private
* */
setWrapper(wrp) {
canvas.set('wrapper', wrp);
},
/**
* Get the canvas element
* @return {HTMLElement}
@ -156,15 +148,6 @@ export default () => {
return doc && doc.body;
},
/**
* Get the wrapper element containing all the components
* @return {HTMLElement}
*/
getWrapperEl() {
const body = this.getBody();
return body && body.querySelector('#wrapper');
},
_getCompFrame(compView) {
return compView && compView._getFrame();
},
@ -590,10 +573,6 @@ export default () => {
fr && fr.stopAutoscroll();
},
postRender() {
if (hasDnd(c.em)) this.droppable = new Droppable(c.em);
},
/**
* Set zoom value
* @param {Number} value The zoom value, from 0 to 100
@ -626,16 +605,6 @@ export default () => {
style.pointerEvents = on ? '' : 'none';
},
/**
* Returns wrapper element
* @return {HTMLElement}
* ????
* @private
*/
getFrameWrapperEl() {
return CanvasView.frame.getWrapper();
},
getFrames() {
return canvas.get('frames').map(item => item);
},

32
src/canvas/model/Canvas.js

@ -1,12 +1,10 @@
import Backbone from 'backbone';
import Frame from './Frame';
import Frames from './Frames';
import { evPageSelect } from 'pages';
export default Backbone.Model.extend({
defaults: {
frame: '',
frames: '',
wrapper: '',
rulers: false,
zoom: 100,
x: 0,
@ -15,17 +13,31 @@ export default Backbone.Model.extend({
initialize(config = {}) {
const { em } = config;
this.config = config;
this.em = em;
this.listenTo(this, 'change:zoom', this.onZoomChange);
this.listenTo(em, 'change:device', this.updateDevice);
this.listenTo(em, evPageSelect, this._pageUpdated);
},
init() {
const { em, config } = this;
const { styles = [], scripts = [] } = config;
const root = em && em.getWrapper();
const css = em && em.getStyle();
const frame = new Frame({ root, styles: css }, config);
const mainPage = em.get('PageManager').getMain();
const frames = mainPage.getFrames();
const frame = mainPage.getMainFrame();
styles.forEach(style => frame.addLink(style));
scripts.forEach(script => frame.addScript(script));
this.em = em;
this.set('frame', frame);
this.set('frames', new Frames([frame], config));
this.listenTo(this, 'change:zoom', this.onZoomChange);
this.listenTo(em, 'change:device', this.updateDevice);
this.set('frames', frames);
},
_pageUpdated(page, prev) {
const { em } = this;
em.setSelected();
em.get('readyCanvas') && em.stopDefault(); // We have to stop before changing current frames
prev && prev.getFrames().map(frame => frame.disable());
this.set('frames', page.getFrames());
},
updateDevice() {

146
src/canvas/model/Frame.js

@ -1,46 +1,74 @@
import Backbone from 'backbone';
import Component from 'dom_components/model/Component';
import CssRules from 'css_composer/model/CssRules';
import { isString } from 'underscore';
export default Backbone.Model.extend({
defaults: {
wrapper: '',
width: null,
height: null,
head: '',
import { Model } from 'backbone';
import { result, forEach, isEmpty, debounce } from 'underscore';
import { isComponent, isObject } from 'utils/mixins';
const keyAutoW = '__aw';
const keyAutoH = '__ah';
export default Model.extend({
defaults: () => ({
x: 0,
y: 0,
root: 0,
components: 0,
styles: 0,
attributes: {}
},
changesCount: 0,
attributes: {},
width: null,
height: null,
head: [],
component: '',
styles: '',
_undo: true,
_undoexc: ['changesCount']
}),
initialize(props, opts = {}) {
const { root, styles, components } = this.attributes;
this.set('head', []);
this.em = opts.em;
const modOpts = {
em: opts.em,
config: opts.em.get('DomComponents').getConfig(),
frame: this
};
!root &&
this.set(
'root',
new Component(
{
type: 'wrapper',
components: components || []
},
modOpts
)
);
(!styles || isString(styles)) &&
this.set('styles', new CssRules(styles, modOpts));
const { config } = opts;
const { em } = config;
const { styles, component } = this.attributes;
const domc = em.get('DomComponents');
const conf = domc.getConfig();
const allRules = em.get('CssComposer').getAll();
this.em = em;
const modOpts = { em, config: conf, frame: this };
if (!isComponent(component)) {
const wrp = isObject(component) ? component : { components: component };
!wrp.type && (wrp.type = 'wrapper');
const Wrapper = domc.getType('wrapper').model;
this.set('component', new Wrapper(wrp, modOpts));
}
if (!styles) {
this.set('styles', allRules);
} else if (!isObject(styles)) {
allRules.add(styles);
this.set('styles', allRules);
}
!props.width && this.set(keyAutoW, 1);
!props.height && this.set(keyAutoH, 1);
},
onRemove() {
this.getComponent().remove({ root: 1 });
},
changesUp: debounce(function(opt = {}) {
if (opt.temporary || opt.noCount || opt.avoidStore) {
return;
}
this.set('changesCount', this.get('changesCount') + 1);
}),
getComponent() {
return this.get('component');
},
getStyles() {
return this.get('styles');
},
disable() {
this.trigger('disable');
},
remove() {
@ -113,7 +141,47 @@ export default Backbone.Model.extend({
this.removeHeadByAttr('src', src, 'script');
},
getPage() {
const coll = this.collection;
return coll && coll.page;
},
_emitUpdated(data = {}) {
this.em.trigger('frame:updated', { frame: this, ...data });
},
toJSON(opts = {}) {
const obj = Model.prototype.toJSON.call(this, opts);
const { em } = this;
const sm = em && em.get('StorageManager');
const smc = sm && sm.getConfig();
const defaults = result(this, 'defaults');
if (smc && !opts.fromUndo) {
const opts = { component: this.getComponent() };
if (smc.storeHtml) obj.html = em.getHtml(opts);
if (smc.storeCss) obj.css = em.getCss(opts);
}
if (opts.fromUndo) delete obj.component;
delete obj.styles;
delete obj.changesCount;
obj[keyAutoW] && delete obj.width;
obj[keyAutoH] && delete obj.height;
// Remove private keys
forEach(obj, (value, key) => {
key.indexOf('_') === 0 && delete obj[key];
});
forEach(defaults, (value, key) => {
if (obj[key] === value) delete obj[key];
});
forEach(['attributes', 'head'], prop => {
if (isEmpty(obj[prop])) delete obj[prop];
});
return obj;
}
});

23
src/canvas/model/Frames.js

@ -1,12 +1,24 @@
import { bindAll } from 'underscore';
import Backbone from 'backbone';
import { Collection } from 'backbone';
import model from './Frame';
export default Backbone.Collection.extend({
export default Collection.extend({
model,
initialize() {
initialize(models, config = {}) {
bindAll(this, 'itemLoaded');
this.config = config;
this.on('reset', this.onReset);
this.on('remove', this.onRemove);
},
onReset(m, opts = {}) {
const prev = opts.previousModels || [];
prev.map(p => this.onRemove(p));
},
onRemove(removed) {
removed && removed.onRemove();
},
itemLoaded() {
@ -26,5 +38,10 @@ export default Backbone.Collection.extend({
listenToLoadItems(on) {
this.forEach(item => item[on ? 'on' : 'off']('loaded', this.itemLoaded));
},
add(m, o = {}) {
const { config } = this;
return Collection.prototype.add.call(this, m, { ...o, config });
}
});

68
src/canvas/view/CanvasView.js

@ -28,28 +28,40 @@ export default Backbone.View.extend({
initialize(o) {
bindAll(this, 'clearOff', 'onKeyPress', 'onCanvasMove');
on(window, 'scroll resize', this.clearOff);
const { model } = this;
const frames = model.get('frames');
this.config = o.config || {};
this.em = this.config.em || {};
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.className = this.config.stylePrefix + 'canvas';
const { em, config } = this;
const { em } = this;
this._initFrames();
this.listenTo(em, 'change:canvasOffset', this.clearOff);
this.listenTo(em, 'component:selected', this.checkSelected);
this.listenTo(model, 'change:zoom change:x change:y', this.updateFrames);
this.listenTo(model, 'change:frames', this._onFramesUpdate);
this.toggleListeners(1);
},
_onFramesUpdate() {
this._initFrames();
this._renderFrames();
},
_initFrames() {
const { frames, model, config, em } = this;
const collection = model.get('frames');
em.set('readyCanvas', 0);
collection.once('loaded:all', () => em.set('readyCanvas', 1));
frames && frames.remove();
this.frames = new FramesView({
collection: frames,
collection,
config: {
...config,
canvasView: this,
renderContent: 1
}
});
this.listenTo(em, 'change:canvasOffset', this.clearOff);
this.listenTo(em, 'component:selected', this.checkSelected);
this.listenTo(model, 'change:zoom change:x change:y', this.updateFrames);
this.listenTo(frames, 'loaded:all', () => em.trigger('loaded'));
this.toggleListeners(1);
},
checkSelected(component, opts = {}) {
@ -89,6 +101,7 @@ export default Backbone.View.extend({
const { el } = this;
const fn = enable ? on : off;
fn(document, 'keypress', this.onKeyPress);
fn(window, 'scroll resize', this.clearOff);
// fn(el, 'mousemove dragover', this.onCanvasMove);
},
@ -328,22 +341,24 @@ export default Backbone.View.extend({
return (view && view._getFrame()) || this.em.get('currentFrame');
},
_renderFrames() {
if (!this.ready) return;
const { model, frames, em, framesArea } = this;
const frms = model.get('frames');
frms.listenToLoad();
frames.render();
const mainFrame = frms.at(0);
const currFrame = mainFrame && mainFrame.view;
em.setCurrentFrame(currFrame);
framesArea && framesArea.appendChild(frames.el);
this.frame = currFrame;
},
render() {
const { el, $el, ppfx, model, em, frames } = this;
const cssc = em.get('CssComposer');
const wrapper = model.get('wrapper');
const { el, $el, ppfx, config } = this;
$el.html(this.template());
const $frames = $el.find('[data-frames]');
this.framesArea = $frames.get(0);
this.wrapper = wrapper;
if (wrapper && typeof wrapper.render == 'function') {
model.get('frame').set({
wrapper,
root: wrapper.getWrapper(),
styles: cssc.getAll()
});
}
const toolsWrp = $el.find('[data-tools]');
this.toolsWrapper = toolsWrp.get(0);
@ -354,6 +369,7 @@ export default Backbone.View.extend({
</div>
</div>
<div id="${ppfx}tools" style="pointer-events:none">
${config.extHl ? `<div class="${ppfx}highlighter-sel"></div>` : ''}
<div class="${ppfx}badge"></div>
<div class="${ppfx}ghost"></div>
<div class="${ppfx}toolbar" style="pointer-events:all"></div>
@ -374,14 +390,8 @@ export default Backbone.View.extend({
this.toolsGlobEl = el.querySelector(`.${ppfx}tools-gl`);
this.toolsEl = toolsEl;
this.el.className = this.className;
// Render all frames
const frms = model.get('frames');
frms.listenToLoad();
frames.render();
em.setCurrentFrame(frms.at(0).view);
$frames.append(frames.el);
this.frame = frms.at(0).view;
this.ready = 1;
this._renderFrames();
return this;
}

23
src/canvas/view/FrameView.js

@ -2,6 +2,7 @@ import Backbone from 'backbone';
import { bindAll, isString, debounce, isUndefined } from 'underscore';
import CssRulesView from 'css_composer/view/CssRulesView';
import ComponentView from 'dom_components/view/ComponentView';
import Droppable from 'utils/Droppable';
import {
appendVNodes,
empty,
@ -10,7 +11,7 @@ import {
createCustomEvent,
motionsEv
} from 'utils/dom';
import { on, off, setViewEl, getPointerEvent } from 'utils/mixins';
import { on, off, setViewEl, hasDnd, getPointerEvent } from 'utils/mixins';
export default Backbone.View.extend({
tagName: 'iframe',
@ -142,11 +143,10 @@ export default Backbone.View.extend({
},
remove() {
const { root, model } = this;
const wrp = this.wrapper;
this._toggleEffects();
wrp && wrp.remove();
Backbone.View.prototype.remove.apply(this, arguments);
root.remove();
model.remove();
},
startAutoscroll() {
@ -252,8 +252,7 @@ export default Backbone.View.extend({
renderBody() {
const { config, model, ppfx } = this;
const root = model.get('root');
const styles = model.get('styles');
const styles = model.getStyles();
const { em } = config;
const doc = this.getDoc();
const head = this.getHead();
@ -354,14 +353,15 @@ export default Backbone.View.extend({
${conf.protectedCss || ''}
</style>`
);
this.root = new ComponentView({
model: root,
const component = model.getComponent();
this.wrapper = new ComponentView({
model: component,
config: {
...root.config,
...component.config,
frameView: this
}
}).render();
append(body, this.root.el);
append(body, this.wrapper.el);
append(
body,
new CssRulesView({
@ -399,13 +399,14 @@ export default Backbone.View.extend({
);
this._toggleEffects(1);
this.droppable = hasDnd(em) && new Droppable(em, this.wrapper.el);
model.trigger('loaded');
},
_toggleEffects(enable) {
const method = enable ? on : off;
const win = this.getWindow();
method(win, `${motionsEv} resize`, this._emitUpdate);
win && method(win, `${motionsEv} resize`, this._emitUpdate);
},
_emitUpdate() {

55
src/canvas/view/FrameWrapView.js

@ -1,7 +1,7 @@
import Backbone from 'backbone';
import FrameView from './FrameView';
import { bindAll, isNumber, isNull, debounce } from 'underscore';
import { createEl, motionsEv } from 'utils/dom';
import { createEl } from 'utils/dom';
import Dragger from 'utils/Dragger';
export default Backbone.View.extend({
@ -69,15 +69,18 @@ export default Backbone.View.extend({
ev && this.dragger.start(ev);
},
remove() {
this.frame.remove();
this.frame = {};
remove(opts) {
this.frame.remove(opts);
Backbone.View.prototype.remove.apply(this, arguments);
['frame', 'dragger', 'cv', 'em', 'canvas', 'elTools'].forEach(
i => (this[i] = 0)
);
return this;
},
updateOffset: debounce(function() {
const { em, $el, frame } = this;
if (!em) return;
em.runDefault({ preserveSelected: 1 });
$el.removeClass(this.classAnim);
frame.model._emitUpdated();
@ -103,32 +106,25 @@ export default Backbone.View.extend({
*/
updateDim() {
const { em, el, $el, model, classAnim } = this;
const { width, height } = model.attributes;
const { style } = el;
const currW = style.width || '';
const currH = style.height || '';
const newW = width || '';
const newH = height || '';
const noChanges = currW == newW && currH == newH;
const un = 'px';
this.frame.rect = 0;
$el.addClass(classAnim);
style.width = isNumber(newW) ? `${newW}${un}` : newW;
style.height = isNumber(newH) ? `${newH}${un}` : newH;
const { noChanges, width, height } = this.__handleSize();
// Set width and height from DOM (should be done only once)
if (isNull(width) || isNull(height)) {
const newDims = {
...(!width ? { width: el.offsetWidth } : {}),
...(!height ? { height: el.offsetHeight } : {})
};
model.set(newDims, { silent: 1 });
model.set(
{
...(!width ? { width: el.offsetWidth } : {}),
...(!height ? { height: el.offsetHeight } : {})
},
{ silent: 1 }
);
}
// Prevent fixed highlighting box which appears when on
// component hover during the animation
em.stopDefault({ preserveSelected: 1 });
noChanges ? this.updateOffset() : $el.one(motionsEv, this.updateOffset);
noChanges ? this.updateOffset() : setTimeout(this.updateOffset, 350);
},
onScroll() {
@ -146,9 +142,25 @@ export default Backbone.View.extend({
this.updateDim();
},
__handleSize() {
const un = 'px';
const { model, el } = this;
const { style } = el;
const { width, height } = model.attributes;
const currW = style.width || '';
const currH = style.height || '';
const newW = width || '';
const newH = height || '';
const noChanges = currW == newW && currH == newH;
style.width = isNumber(newW) ? `${newW}${un}` : newW;
style.height = isNumber(newH) ? `${newH}${un}` : newH;
return { noChanges, width, height, newW, newH };
},
render() {
const { frame, $el, ppfx, cv, model, el } = this;
const { onRender } = model.attributes;
this.__handleSize();
frame.render();
$el
.empty()
@ -204,7 +216,8 @@ export default Backbone.View.extend({
`
);
this.elTools = elTools;
cv.toolsWrapper.appendChild(elTools); // TODO remove on frame remove
const twrp = cv.toolsWrapper;
twrp && twrp.appendChild(elTools); // TODO remove on frame remove
onRender &&
onRender({
el,

4
src/canvas/view/FramesView.js

@ -9,6 +9,10 @@ export default DomainViews.extend({
this.listenTo(this.collection, 'reset', this.render);
},
onRemoveBefore(items, opts) {
items.forEach(item => item.remove(opts));
},
onRender() {
const { config, $el } = this;
const { em } = config;

2
src/code_manager/model/CssGenerator.js

@ -122,7 +122,7 @@ export default Backbone.Model.extend({
});
if ((selectorStrNoAdd && found) || selectorsAdd || singleAtRule) {
const block = rule.getDeclaration();
const block = rule.getDeclaration({ body: 1 });
block && (result += block);
} else {
dump.push(rule);

19
src/commands/view/CommandAbstract.js

@ -18,17 +18,6 @@ export default Backbone.View.extend({
this.freezClass = this.ppfx + 'freezed';
this.canvas = this.em.get && this.em.get('Canvas');
if (this.em.get) this.setElement(this.getCanvas());
if (this.canvas) {
this.$canvas = this.$el;
// this.$wrapper = $(this.getCanvasWrapper());
// this.frameEl = this.canvas.getFrameEl();
this.canvasTool = this.getCanvasTools();
// this.bodyEl = this.getCanvasBody();
}
this.init(this.config);
},
@ -55,14 +44,6 @@ export default Backbone.View.extend({
return this.canvas.getBody();
},
/**
* Get canvas wrapper element
* @return {HTMLElement}
*/
getCanvasWrapper() {
return this.canvas.getWrapperEl();
},
/**
* Get canvas wrapper element
* @return {HTMLElement}

30
src/commands/view/SelectComponent.js

@ -613,7 +613,8 @@ export default {
}
const unit = 'px';
const { style } = this.toggleToolsEl(1, view);
const toolsEl = this.toggleToolsEl(1, view);
const { style } = toolsEl;
const frameOff = this.canvas.canvasRectOffset(el, pos);
const topOff = frameOff.top;
const leftOff = frameOff.left;
@ -629,12 +630,28 @@ export default {
style.left = leftOff + unit;
style.width = pos.width + unit;
style.height = pos.height + unit;
this._trgToolUp('local', {
component,
el: toolsEl,
top: topOff,
left: leftOff,
width: pos.width,
height: pos.height
});
},
_upToolbar: debounce(function() {
this.updateToolsGlobal({ force: 1 });
}),
_trgToolUp(type, opts = {}) {
this.em.trigger('canvas:tools:update', {
type,
...opts
});
},
updateToolsGlobal(opts = {}) {
const { el, pos, component } = this.getElSelected();
@ -653,7 +670,8 @@ export default {
}
const unit = 'px';
const { style } = this.toggleToolsEl(1);
const toolsEl = this.toggleToolsEl(1);
const { style } = toolsEl;
const targetToElem = canvas.getTargetToElementFixed(
el,
canvas.getToolbarEl(),
@ -667,6 +685,14 @@ export default {
style.height = pos.height + unit;
this.updateToolbarPos({ top: targetToElem.top, left: targetToElem.left });
this._trgToolUp('global', {
component,
el: toolsEl,
top: topOff,
left: leftOff,
width: pos.width,
height: pos.height
});
},
/**

24
src/commands/view/SwitchVisibility.js

@ -1,4 +1,10 @@
import { bindAll } from 'underscore';
export default {
init() {
bindAll(this, '_onFramesChange');
},
run(ed) {
this.toggleVis(ed);
},
@ -9,11 +15,19 @@ export default {
toggleVis(ed, active = 1) {
if (!ed.Commands.isActive('preview')) {
const method = active ? 'add' : 'remove';
ed.Canvas.getFrames().forEach(frame => {
frame.view.getBody().classList[method](`${this.ppfx}dashed`);
});
const cv = ed.Canvas;
const mth = active ? 'on' : 'off';
cv.getFrames().forEach(frame => this._upFrame(frame, active));
cv.getModel()[mth]('change:frames', this._onFramesChange);
}
},
_onFramesChange(m, frames) {
frames.forEach(frame => this._upFrame(frame, 1));
},
_upFrame(frame, active) {
const method = active ? 'add' : 'remove';
frame.view.getBody().classList[method](`${this.ppfx}dashed`);
}
};

38
src/css_composer/index.js

@ -98,7 +98,7 @@ export default () => {
* @private
*/
onLoad() {
rules.add(c.rules);
rules.add(c.rules, { silent: 1 });
},
/**
@ -106,28 +106,9 @@ export default () => {
* @param {Editor} em
* @private
*/
postLoad(em) {
const ev = 'add remove';
const rules = this.getAll();
const um = em.get('UndoManager');
um && um.add(rules);
em.stopListening(rules, ev, this.handleChange);
em.listenTo(rules, ev, this.handleChange);
rules.each(rule => this.handleChange(rule, null, { avoidStore: 1 }));
},
/**
* Handle rule changes
* @private
*/
handleChange(model, val, opts = {}) {
const ev = 'change:style';
const um = em.get('UndoManager');
um && um.add(model);
const handleUpdates = em.handleUpdates.bind(em);
em.stopListening(model, ev, handleUpdates);
em.listenTo(model, ev, handleUpdates);
!opts.avoidStore && handleUpdates('', '', opts);
postLoad() {
const um = em && em.get('UndoManager');
um && um.add(this.getAll());
},
/**
@ -170,9 +151,10 @@ export default () => {
*/
store(noStore) {
if (!c.stm) return;
var obj = {};
var keys = this.storageKey();
if (keys.indexOf('css') >= 0) obj.css = c.em.getCss();
const obj = {};
const keys = this.storageKey();
const hasPages = em && em.get('hasPages');
if (keys.indexOf('css') >= 0 && !hasPages) obj.css = c.em.getCss();
if (keys.indexOf('styles') >= 0) obj.styles = JSON.stringify(rules);
if (!noStore) c.stm.store(obj);
return obj;
@ -289,7 +271,7 @@ export default () => {
}
var modelExists = this.get(newSels, rule.state, rule.mediaText, rule);
var model = this.add(newSels, rule.state, rule.mediaText, rule);
var model = this.add(newSels, rule.state, rule.mediaText, rule, opts);
var updateStyle = !modelExists || !opts.avoidUpdateStyle;
const style = rule.style || {};
@ -297,7 +279,7 @@ export default () => {
let styleUpdate = opts.extend
? { ...model.get('style'), ...style }
: style;
model.set('style', styleUpdate);
model.set('style', styleUpdate, opts);
}
result.push(model);

32
src/css_composer/model/CssRule.js

@ -1,8 +1,9 @@
import { map } from 'underscore';
import Backbone from 'backbone';
import Styleable from 'domain_abstract/model/Styleable';
import { isEmpty, forEach } from 'underscore';
import { isEmpty, forEach, isString } from 'underscore';
import Selectors from 'selector_manager/model/Selectors';
import { isEmptyObj } from 'utils/mixins';
const { CSS } = window;
@ -36,7 +37,9 @@ export default Backbone.Model.extend(Styleable).extend({
// If true, sets '!important' on all properties
// You can use an array to specify properties to set important
// Used in view
important: 0
important: 0,
_undo: true
},
initialize(c, opt = {}) {
@ -44,6 +47,13 @@ export default Backbone.Model.extend(Styleable).extend({
this.opt = opt;
this.em = opt.em;
this.ensureSelectors();
this.on('change', this.__onChange);
},
__onChange(m, opts) {
const { em } = this;
const changed = this.changedAttributes();
!isEmptyObj(changed) && em && em.changesUp(opts);
},
clone() {
@ -53,7 +63,7 @@ export default Backbone.Model.extend(Styleable).extend({
return new this.constructor(attr, opts);
},
ensureSelectors() {
ensureSelectors(m, c, opts) {
const { em } = this;
const sm = em && em.get('SelectorManager');
const toListen = [this, 'change:selectors', this.ensureSelectors];
@ -64,12 +74,14 @@ export default Backbone.Model.extend(Styleable).extend({
sels = [...sels.models];
}
sels = isString(sels) ? [sels] : sels;
if (Array.isArray(sels)) {
const res = sels.filter(i => i).map(i => (sm ? sm.add(i) : i));
sels = new Selectors(res);
}
this.set('selectors', sels);
this.set('selectors', sels, opts);
this.listenTo(...toListen);
},
@ -92,11 +104,10 @@ export default Backbone.Model.extend(Styleable).extend({
*/
selectorsToString(opts = {}) {
const result = [];
const { em } = this;
const state = this.get('state');
const wrapper = this.get('wrapper');
const addSelector = this.get('selectorsAdd');
const isBody = wrapper && em && em.getConfig('wrapperIsBody');
const isBody = wrapper && opts.body;
const selOpts = {
escape: str => (CSS && CSS.escape ? CSS.escape(str) : str)
};
@ -116,7 +127,7 @@ export default Backbone.Model.extend(Styleable).extend({
*/
getDeclaration(opts = {}) {
let result = '';
const selectors = this.selectorsToString();
const selectors = this.selectorsToString(opts);
const style = this.styleToString(opts);
const singleAtRule = this.get('singleAtRule');
@ -178,13 +189,10 @@ export default Backbone.Model.extend(Styleable).extend({
var wd = width || '';
var selectorsAdd = ruleProps.selectorsAdd || '';
var atRuleType = ruleProps.atRuleType || '';
var cId = 'cid';
//var a1 = _.pluck(selectors.models || selectors, cId);
//var a2 = _.pluck(this.get('selectors').models, cId);
if (!(selectors instanceof Array) && !selectors.models)
selectors = [selectors];
var a1 = map(selectors.models || selectors, model => model.get('name'));
var a2 = map(this.get('selectors').models, model => model.get('name'));
var a1 = map(selectors.models || selectors, model => model.getFullName());
var a2 = map(this.get('selectors').models, model => model.getFullName());
var f = false;
if (a1.length !== a2.length) return f;

4
src/css_composer/model/CssRules.js

@ -22,8 +22,8 @@ export default Collection.extend({
return result.filter(i => i.style);
},
onAdd(model) {
model.ensureSelectors(); // required for undo
onAdd(model, c, o) {
model.ensureSelectors(model, c, o); // required for undo
},
onRemove(removed) {

9
src/css_composer/view/CssRuleView.js

@ -5,17 +5,16 @@ export default Backbone.View.extend({
initialize(o = {}) {
this.config = o.config || {};
const model = this.model;
const toTrack = 'change:style change:state change:mediaText';
this.listenTo(model, toTrack, this.render);
const { model } = this;
this.listenTo(model, 'change', this.render);
this.listenTo(model, 'destroy remove', this.remove);
this.listenTo(model.get('selectors'), 'change', this.render);
},
render() {
const model = this.model;
const { model, el } = this;
const important = model.get('important');
this.el.innerHTML = this.model.toCSS({ important });
el.innerHTML = model.toCSS({ important });
return this;
}
});

119
src/dom_components/index.js

@ -251,43 +251,10 @@ export default () => {
);
}
// Build wrapper
let components = c.components;
let wrapper = { ...c.wrapper };
wrapper['custom-name'] = c.wrapperName;
wrapper.wrapper = 1;
wrapper.type = 'wrapper';
// Components might be a wrapper
if (
components &&
components.constructor === Object &&
components.wrapper
) {
wrapper = { ...components };
components = components.components || [];
wrapper.components = [];
// Have to put back the real object of components
if (em) {
em.config.components = components;
c.components = components;
}
if (em.get('hasPages')) {
c.components = '';
}
component = new Component(wrapper, {
em,
config: c,
componentTypes,
domc: this
});
component.set({ attributes: { id: 'wrapper' } });
componentView = new ComponentView({
model: component,
config: c,
componentTypes
});
return this;
},
@ -296,71 +263,7 @@ export default () => {
* @private
*/
onLoad() {
this.setComponents(c.components);
},
/**
* Do stuff after load
* @param {Editor} em
* @private
*/
postLoad(em) {
this.handleChanges(this.getWrapper(), null, { avoidStore: 1 });
},
/**
* Handle component changes
* @private
*/
handleChanges(model, value, opts = {}) {
const comps = model.components();
const um = em.get('UndoManager');
const handleUpdates = em.handleUpdates.bind(em);
const handleChanges = this.handleChanges.bind(this);
const handleChangesColl = this.handleChangesColl.bind(this);
const handleRemoves = this.handleRemoves.bind(this);
um && um.add(model);
um && comps && um.add(comps);
const evn = 'change:style change:content change:attributes change:src';
[
[model, evn, handleUpdates],
[model, 'change:components', handleChangesColl],
[comps, 'add', handleChanges],
[comps, 'remove reset', handleRemoves],
[model.get('classes'), 'add remove', handleUpdates]
].forEach(els => {
em.stopListening(els[0], els[1], els[2]);
em.listenTo(els[0], els[1], els[2]);
});
!opts.avoidStore && handleUpdates('', '', opts);
comps.each(model => this.handleChanges(model, value, opts));
},
handleChangesColl(model, coll) {
const um = em.get('UndoManager');
if (um && coll instanceof Backbone.Collection) {
const handleChanges = this.handleChanges.bind(this);
const handleRemoves = this.handleRemoves.bind(this);
um.add(coll);
[
[coll, 'add', handleChanges],
[coll, 'remove reset', handleRemoves]
].forEach(els => {
em.stopListening(els[0], els[1], els[2]);
em.listenTo(els[0], els[1], els[2]);
});
}
},
/**
* Triggered when some component is removed
* @private
* */
handleRemoves(m, value, opt) {
const opts = opt || value; // in case of reset
em.handleUpdates(m, value, opts);
c.components && this.setComponents(c.components, { silent: 1 });
},
/**
@ -416,8 +319,8 @@ export default () => {
* @return {Object} Data to store
*/
store(noStore) {
if (!c.stm) {
return;
if (!c.stm || this.em.get('hasPages')) {
return {};
}
var obj = {};
@ -428,7 +331,6 @@ export default () => {
}
if (keys.indexOf('components') >= 0) {
const { em } = this;
// const storeWrap = (em && !em.getConfig('avoidInlineStyle')) || c.storeWrapper;
const storeWrap = c.storeWrapper;
const toStore = storeWrap ? this.getWrapper() : this.getComponents();
@ -448,7 +350,11 @@ export default () => {
* @private
*/
getComponent() {
return component;
return this.em
.get('PageManager')
.getSelected()
.getMainFrame()
.getComponent();
},
/**
@ -738,8 +644,9 @@ export default () => {
},
destroy() {
this.clear();
componentView.remove();
const all = this.allById();
Object.keys(all).forEach(id => all[id] && all[id].remove());
componentView && componentView.remove();
[c, em, componentsById, component, componentView].forEach(i => (i = {}));
this.em = {};
}

95
src/dom_components/model/Component.js

@ -30,6 +30,8 @@ export const eventDrag = 'component:drag';
export const keySymbols = '__symbols';
export const keySymbol = '__symbol';
export const keySymbol2w = '__symbol2w';
export const keyUpdate = 'component:update';
export const keyUpdateInside = `${keyUpdate}-inside`;
/**
* The Component object represents a single node of our template structure, so when you update its properties the changes are
@ -122,7 +124,11 @@ const Component = Backbone.Model.extend(Styleable).extend(
traits: ['id', 'title'],
propagate: '',
dmode: '',
toolbar: null
toolbar: null,
[keySymbol]: 0,
[keySymbols]: 0,
_undo: true,
_undoexc: ['status', 'open']
},
/**
@ -193,6 +199,8 @@ const Component = Backbone.Model.extend(Styleable).extend(
this.listenTo(this, 'change:attributes', this.attrUpdated);
this.listenTo(this, 'change:attributes:id', this._idUpdated);
this.on('change:toolbar', this.__emitUpdateTlb);
this.on('change', this.__onChange);
this.on(keyUpdateInside, this.__propToParent);
this.set('status', '');
this.views = [];
@ -205,12 +213,60 @@ const Component = Backbone.Model.extend(Styleable).extend(
});
if (!opt.temporary) {
this.__postAdd();
this.init();
this.__isSymbolOrInst() && this.__initSymb();
em && em.trigger('component:create', this);
}
},
__postAdd(opts = {}) {
const { em } = this;
const um = em && em.get('UndoManager');
const comps = this.components();
if (um && !this.__hasUm) {
um.add(comps);
this.__hasUm = 1;
}
opts.recursive && comps.map(c => c.__postAdd(opts));
},
__postRemove() {
const { em } = this;
const um = em && em.get('UndoManager');
if (um) {
um.remove(this.components());
delete this.__hasUm;
}
},
__onChange(m, opts) {
const changed = this.changedAttributes();
['status', 'open', 'toolbar', 'traits'].forEach(
name => delete changed[name]
);
// Propagate component prop changes
if (!isEmptyObj(changed)) {
this.__changesUp(opts);
this.__propSelfToParent({ component: this, changed, options: opts });
}
},
__changesUp(opts) {
const { em, frame } = this;
[frame, em].forEach(md => md && md.changesUp(opts));
},
__propSelfToParent(props) {
this.trigger(keyUpdate, props);
this.__propToParent(props);
},
__propToParent(props) {
const parent = this.parent();
parent && parent.trigger(keyUpdateInside, props);
},
__emitUpdateTlb() {
this.emitUpdate('toolbar');
},
@ -720,10 +776,12 @@ const Component = Backbone.Model.extend(Styleable).extend(
// This will propagate the change up to __upSymbProps
child.set('classes', this.get('classes'), { fromInstance: this });
});
this.__changesUp(opts);
},
__upSymbComps(m, c, o) {
const { fromInstance } = o || c || {};
const optUp = o || c || {};
const { fromInstance } = optUp;
const toUpOpts = { fromInstance };
const isTemp = m.opt.temporary;
@ -791,6 +849,8 @@ const Component = Backbone.Model.extend(Styleable).extend(
});
}
}
this.__changesUp(optUp);
},
initClasses(m, c, opts = {}) {
@ -821,6 +881,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
const addChild = !this.opt.avoidChildren;
this.set('components', comps);
addChild &&
components &&
comps.add(
isFunction(components) ? components(this) : components,
this.opt
@ -1177,13 +1238,13 @@ const Component = Backbone.Model.extend(Styleable).extend(
cloned.unset(keySymbols);
} else if (symbol) {
// Contains already a reference to a symbol
symbol.get(keySymbols).push(cloned);
symbol.set(keySymbols, [...symbol.__getSymbols(), cloned]);
cloned.__initSymb();
} else if (opt.symbol) {
// Request to create a symbol
if (this.__isSymbol()) {
// Already a symbol, cloned should be an instance
this.get(keySymbols).push(cloned);
this.set(keySymbols, [...symbols, cloned]);
cloned.set(keySymbol, this);
cloned.__initSymb();
} else if (opt.symbolInv) {
@ -1341,7 +1402,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
delete obj.status;
delete obj.open; // used in Layers
if (!opts.keepSymbols) {
if (!opts.fromUndo) {
if (obj[keySymbols]) {
obj[keySymbols] = this.__getSymbToUp().map(i => i.getId());
}
@ -1496,17 +1557,24 @@ const Component = Backbone.Model.extend(Styleable).extend(
},
emitUpdate(property, ...args) {
const em = this.em;
const event = 'component:update' + (property ? `:${property}` : '');
const { em } = this;
const event = keyUpdate + (property ? `:${property}` : '');
const item = property && this.get(property);
property &&
this.updated(
property,
property && this.get(property),
item,
property && this.previous(property),
...args
);
this.trigger(event, ...args);
em && em.trigger(event, this, ...args);
['components', 'classes'].indexOf(property) >= 0 &&
this.__propSelfToParent({
component: this,
changed: { [property]: item },
options: args[2] || args[1] || {}
});
},
/**
@ -1533,7 +1601,10 @@ const Component = Backbone.Model.extend(Styleable).extend(
remove(opts = {}) {
const { em } = this;
const coll = this.collection;
const remove = () => coll && coll.remove(this, opts);
const remove = () => {
coll && coll.remove(this, opts);
opts.root && this.components('');
};
const rmOpts = { ...opts };
[this, em].map(i =>
i.trigger('component:remove:before', this, remove, rmOpts)
@ -1699,8 +1770,10 @@ const Component = Backbone.Model.extend(Styleable).extend(
* not ok, as it was shared between multiple editor instances
*/
getList(model) {
const domc = model.opt && model.opt.domc;
return domc ? domc.componentsById : {};
const { opt = {} } = model;
const { domc, em } = opt;
const dm = domc || (em && em.get('DomComponents'));
return dm ? dm.componentsById : {};
},
/**

1
src/dom_components/model/ComponentText.js

@ -9,7 +9,6 @@ export default Component.extend({
},
toHTML() {
this.trigger('sync:content', { silent: 1 });
return Component.prototype.toHTML.apply(this, arguments);
}
});

31
src/dom_components/model/ComponentWrapper.js

@ -2,7 +2,36 @@
import Component from './Component';
export default Component.extend(
{},
{
defaults: {
...Component.prototype.defaults,
__wrapper: 1,
removable: false,
copyable: false,
draggable: false,
components: [],
traits: [],
stylable: [
'background',
'background-color',
'background-image',
'background-repeat',
'background-attachment',
'background-position',
'background-size'
]
},
__postAdd() {
const um = this.em && this.em.get('UndoManager');
um && !this.__hasUm && um.add(this);
return Component.prototype.__postAdd.call(this, arguments);
},
__postRemove() {
const um = this.em && this.em.get('UndoManager');
um && um.remove(this);
return Component.prototype.__postRemove.call(this, arguments);
}
},
{
isComponent() {
return false;

15
src/dom_components/model/Components.js

@ -34,9 +34,10 @@ export default Backbone.Collection.extend({
this.listenTo(this, 'add', this.onAdd);
this.listenTo(this, 'remove', this.removeChildren);
this.listenTo(this, 'reset', this.resetChildren);
this.config = opt.config;
this.em = opt.em;
this.domc = opt.domc;
const { em, config } = opt;
this.config = config;
this.em = em;
this.domc = opt.domc || (em && em.get('DomComponents'));
},
resetChildren(models, opts = {}) {
@ -97,12 +98,10 @@ export default Backbone.Collection.extend({
// Remove stuff registered in DomComponents.handleChanges
const inner = removed.components();
const um = em.get('UndoManager');
em.stopListening(inner);
em.stopListening(removed);
em.stopListening(removed.get('classes'));
um.remove(removed);
um.remove(inner);
removed.__postRemove();
},
model(attrs, options) {
@ -138,8 +137,7 @@ export default Backbone.Collection.extend({
},
parseString(value, opt = {}) {
const { em } = this;
const { domc } = this.opt;
const { em, domc } = this;
const cssc = em.get('CssComposer');
const parsed = em.get('Parser').parseHtml(value);
// We need this to avoid duplicate IDs
@ -254,6 +252,7 @@ export default Backbone.Collection.extend({
model.addClass(name);
}
model.__postAdd({ recursive: 1 });
this.__onAddEnd();
},

11
src/dom_components/view/ComponentView.js

@ -102,6 +102,8 @@ export default Backbone.View.extend({
const view = comp.getView(frameM);
view && view.remove();
});
const cv = view.childrenView;
cv && cv.remove();
const { views } = model;
views.splice(views.indexOf(view), 1);
view.removed(view._clbObj());
@ -188,23 +190,24 @@ export default Backbone.View.extend({
* @private
* */
updateStatus(opts = {}) {
const em = this.em;
const { em } = this;
const { extHl } = em ? em.get('Canvas').getConfig() : {};
const el = this.el;
const status = this.model.get('status');
const pfx = this.pfx;
const ppfx = this.ppfx;
const selectedCls = `${ppfx}selected`;
const selectedParentCls = `${selectedCls}-parent`;
const freezedCls = `${ppfx}freezed`;
const hoveredCls = `${ppfx}hovered`;
const toRemove = [selectedCls, selectedParentCls, freezedCls, hoveredCls];
const selCls = extHl && !opts.noExtHl ? '' : selectedCls;
this.$el.removeClass(toRemove.join(' '));
var actualCls = el.getAttribute('class') || '';
var cls = '';
switch (status) {
case 'selected':
cls = `${actualCls} ${selectedCls}`;
cls = `${actualCls} ${selCls}`;
break;
case 'selected-parent':
cls = `${actualCls} ${selectedParentCls}`;
@ -213,7 +216,7 @@ export default Backbone.View.extend({
cls = `${actualCls} ${freezedCls}`;
break;
case 'freezed-selected':
cls = `${actualCls} ${freezedCls} ${selectedCls}`;
cls = `${actualCls} ${freezedCls} ${selCls}`;
break;
case 'hovered':
cls = !opts.avoidHover ? `${actualCls} ${hoveredCls}` : '';

8
src/domain_abstract/view/DomainViews.js

@ -114,9 +114,15 @@ export default Backbone.View.extend({
onRender() {},
remove() {
onRemoveBefore() {},
onRemove() {},
remove(opts = {}) {
const { items } = this;
this.onRemoveBefore(items, opts);
this.clearItems();
Backbone.View.prototype.remove.apply(this, arguments);
this.onRemove(items, opts);
},
clearItems() {

5
src/editor/index.js

@ -99,6 +99,8 @@
* * `abort:{commandName}` - Triggered when the command execution is aborted (`editor.on(`run:preview:before`, opts => opts.abort = 1);`)
* * `run` - Triggered on run of any command. The id and the result are passed as arguments to the callback
* * `stop` - Triggered on stop of any command. The id and the result are passed as arguments to the callback
* ### Pages
* Check the [Pages](/api/pages.html) module.
* ### General
* * `canvasScroll` - Canvas is scrolled
* * `update` - The structure of the template is updated (its HTML/CSS)
@ -156,6 +158,7 @@ export default (config = {}) => {
'CodeManager',
'UndoManager',
'RichTextEditor',
['Pages', 'PageManager'],
'DomComponents',
['Components', 'DomComponents'],
'LayerManager',
@ -186,7 +189,7 @@ export default (config = {}) => {
// Do post render stuff after the iframe is loaded otherwise it'll
// be empty during tests
em.on('loaded', () => {
em.once('change:ready', () => {
this.UndoManager.clear();
em.get('modules').forEach(module => {
module.postRender && module.postRender(editorView);

32
src/editor/model/Editor.js

@ -28,6 +28,7 @@ const deps = [
require('panels'),
require('rich_text_editor'),
require('asset_manager'),
require('pages'),
require('css_composer'),
require('trait_manager'),
require('dom_components'),
@ -79,6 +80,7 @@ export default Backbone.Model.extend({
this.set('storables', []);
this.set('selected', new Collection());
this.set('dmode', c.dragMode);
this.set('hasPages', !!c.pageManager);
const el = c.el;
const log = c.log;
const toLog = log === true ? keys(logs) : isArray(log) ? log : [];
@ -96,6 +98,7 @@ export default Backbone.Model.extend({
deps.forEach(name => this.loadModule(name));
this.on('change:componentHovered', this.componentHovered, this);
this.on('change:changesCount', this.updateChanges, this);
this.on('change:readyLoad change:readyCanvas', this._checkReady, this);
toLog.forEach(e => this.listenLog(e));
// Deprecations
@ -113,6 +116,16 @@ export default Backbone.Model.extend({
);
},
_checkReady() {
if (
this.get('readyLoad') &&
this.get('readyCanvas') &&
!this.get('ready')
) {
this.set('ready', 1);
}
},
getContainer() {
return this.config.el;
},
@ -150,6 +163,7 @@ export default Backbone.Model.extend({
const postLoad = () => {
const modules = this.get('modules');
modules.forEach(module => module.postLoad && module.postLoad(this));
this.set('readyLoad', 1);
clb && clb();
};
@ -194,7 +208,7 @@ export default Backbone.Model.extend({
const cfgParent = !isUndefined(config[name])
? config[name]
: config[Mod.name];
const cfg = cfgParent || {};
const cfg = cfgParent === true ? {} : cfgParent || {};
const sm = this.get('StorageManager');
cfg.pStylePrefix = config.pStylePrefix || '';
@ -257,6 +271,10 @@ export default Backbone.Model.extend({
}, 0);
},
changesUp(opts) {
this.handleUpdates(0, 0, opts);
},
/**
* Callback on component hover
* @param {Object} Model
@ -502,8 +520,8 @@ export default Backbone.Model.extend({
const { optsHtml } = config;
const exportWrapper = config.exportWrapper;
const wrapperIsBody = config.wrapperIsBody;
const js = config.jsInHtml ? this.getJs() : '';
var wrp = this.get('DomComponents').getComponent();
const js = config.jsInHtml ? this.getJs(opts) : '';
var wrp = opts.component || this.get('DomComponents').getComponent();
var html = this.get('CodeManager').getCode(wrp, 'html', {
exportWrapper,
wrapperIsBody,
@ -529,7 +547,7 @@ export default Backbone.Model.extend({
? opts.keepUnusedStyles
: config.keepUnusedStyles;
const cssc = this.get('CssComposer');
const wrp = this.get('DomComponents').getComponent();
const wrp = opts.component || this.get('DomComponents').getComponent();
const protCss = !avoidProt ? config.protectedCss : '';
return (
@ -548,8 +566,8 @@ export default Backbone.Model.extend({
* @return {string} JS string
* @private
*/
getJs() {
var wrp = this.get('DomComponents').getWrapper();
getJs(opts = {}) {
var wrp = opts.component || this.get('DomComponents').getWrapper();
return this.get('CodeManager')
.getCode(wrp, 'js')
.trim();
@ -573,7 +591,7 @@ export default Backbone.Model.extend({
});
sm.store(store, res => {
clb && clb(res);
clb && clb(res, store);
this.set('changesCount', 0);
this.trigger('storage:store', store);
});

4
src/editor/view/EditorView.js

@ -10,11 +10,11 @@ export default Backbone.View.extend({
this.conf = model.config;
this.pn = model.get('Panels');
this.cv = model.get('Canvas');
model.on('loaded', () => {
model.once('change:ready', () => {
this.pn.active();
this.pn.disableButtons();
model.set('changesCount', 0);
setTimeout(() => {
model.runDefault();
model.trigger('load', model.get('Editor'));
});
});

2
src/navigator/index.js

@ -50,7 +50,7 @@ export default () => {
* @return {self}
*/
setRoot(el) {
layers.setRoot(el);
layers && layers.setRoot(el);
return this;
},

8
src/navigator/view/ItemView.js

@ -84,6 +84,7 @@ export default Backbone.View.extend({
this.listenTo(model, 'change:open', this.updateOpening);
this.listenTo(model, 'change:layerable', this.updateLayerable);
this.listenTo(model, 'change:style:display', this.updateVisibility);
this.listenTo(model, 'rerender:layer', this.render);
this.className = `${pfx}layer ${pfx}layer__t-${type} no-select ${ppfx}two-color`;
this.inputNameCls = `${ppfx}layer-name`;
this.clsTitleC = `${pfx}layer-title-c`;
@ -312,7 +313,8 @@ export default Backbone.View.extend({
updateStatus(e) {
ComponentView.prototype.updateStatus.apply(this, [
{
avoidHover: !this.config.highlightHover
avoidHover: !this.config.highlightHover,
noExtHl: 1
}
]);
},
@ -441,6 +443,8 @@ export default Backbone.View.extend({
__render() {
const { model, config, el } = this;
const { onRender } = config;
onRender.bind(this)({ component: model, el });
const opt = { component: model, el };
onRender.bind(this)(opt);
this.em.trigger('layer:render', opt);
}
});

297
src/pages/index.js

@ -0,0 +1,297 @@
/**
* You can customize the initial state of the module from the editor initialization
* ```js
* const editor = grapesjs.init({
* ....
* pageManager: {
* pages: [
* {
* id: 'page-id',
* styles: `.my-class { color: red }`, // or a JSON of styles
* component: '<div class="my-class">My element</div>', // or a JSON of components
* }
* ]
* },
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const pageManager = editor.Pages;
* ```
*
* ## Available Events
* * `page:add` - Added new page. The page is passed as an argument to the callback
* * `page:remove` - Page removed. The page is passed as an argument to the callback
* * `page:select` - New page selected. The newly selected page and the previous one, are passed as arguments to the callback
* * `page:update` - Page updated. The updated page and the object containing changes are passed as arguments to the callback
* * `page` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback
*
* ## Methods
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
* * [getMain](#getmain)
* * [remove](#remove)
* * [select](#select)
* * [getSelected](#getselected)
*
* @module Pages
*/
import { isString, bindAll } from 'underscore';
import { createId } from 'utils/mixins';
import { Model } from 'backbone';
import Pages from './model/Pages';
import Page from './model/Page';
export const evAll = 'page';
export const evPfx = `${evAll}:`;
export const evPageSelect = `${evPfx}select`;
export const evPageSelectBefore = `${evPageSelect}:before`;
export const evPageUpdate = `${evPfx}update`;
export const evPageAdd = `${evPfx}add`;
export const evPageAddBefore = `${evPageAdd}:before`;
export const evPageRemove = `${evPfx}remove`;
export const evPageRemoveBefore = `${evPageRemove}:before`;
const chnSel = 'change:selected';
const typeMain = 'main';
export default () => {
return {
name: 'PageManager',
storageKey: 'pages',
Page,
Pages,
events: {
all: evAll,
select: evPageSelect,
selectBefore: evPageSelectBefore,
update: evPageUpdate,
add: evPageAdd,
addBefore: evPageAddBefore,
remove: evPageRemove,
removeBefore: evPageRemoveBefore
},
/**
* Initialize module
* @param {Object} config Configurations
* @private
*/
init(opts = {}) {
bindAll(this, '_onPageChange');
const { em } = opts;
const cnf = { ...opts };
this.config = cnf;
this.em = em;
const pages = new Pages([], cnf);
this.pages = pages;
const model = new Model({ _undo: true });
this.model = model;
pages.on('add', (p, c, o) => em.trigger(evPageAdd, p, o));
pages.on('remove', (p, c, o) => em.trigger(evPageRemove, p, o));
pages.on('change', (p, c) => {
em.trigger(evPageUpdate, p, p.changedAttributes(), c);
});
pages.on('reset', coll => coll.at(0) && this.select(coll.at(0)));
pages.on('all', this.__onChange, this);
model.on(chnSel, this._onPageChange);
return this;
},
__onChange(event, page, coll, opts) {
const options = opts || coll;
this.em.trigger(evAll, { event, page, options });
},
onLoad() {
const { pages } = this;
const opt = { silent: true };
pages.add(this.config.pages || [], opt);
const mainPage = !pages.length
? this.add({ type: typeMain }, opt)
: this.getMain();
this.select(mainPage, opt);
},
_onPageChange(m, page, opts) {
const { em } = this;
const lm = em.get('LayerManager');
const mainComp = page.getMainComponent();
lm && mainComp && lm.setRoot(mainComp);
em.trigger(evPageSelect, page, m.previous('selected'));
this.__onChange(chnSel, page, opts);
},
postLoad() {
const { em, model } = this;
const um = em.get('UndoManager');
um && um.add(model);
um && um.add(this.pages);
},
/**
* Add new page
* @param {Object} props Page properties
* @param {Object} [opts] Options
* @returns {Page}
* @example
* const newPage = pageManager.add({
* id: 'new-page-id', // without an explicit ID, a random one will be created
* styles: `.my-class { color: red }`, // or a JSON of styles
* component: '<div class="my-class">My element</div>', // or a JSON of components
* });
*/
add(props, opts = {}) {
const { em } = this;
props.id = props.id || this._createId();
const add = () => {
const page = this.pages.add(props, opts);
opts.select && this.select(page);
return page;
};
!opts.silent && em.trigger(evPageAddBefore, props, add, opts);
return !opts.abort && add();
},
/**
* Remove page
* @param {String|Page} page Page or page id
* @returns {Page}
* @example
* const removedPage = pageManager.remove('page-id');
* // or by passing the page
* const somePage = pageManager.get('page-id');
* pageManager.remove(somePage);
*/
remove(page, opts = {}) {
const { em } = this;
const pg = isString(page) ? this.get(page) : page;
const rm = () => {
pg && this.pages.remove(pg, opts);
return pg;
};
!opts.silent && em.trigger(evPageRemoveBefore, pg, rm, opts);
return !opts.abort && rm();
},
/**
* Get page by id
* @param {String} id Page id
* @returns {Page}
* @example
* const somePage = pageManager.get('page-id');
*/
get(id) {
return this.pages.filter(p => p.get('id') === id)[0];
},
/**
* Get main page (the first one available)
* @returns {Page}
* @example
* const mainPage = pageManager.getMain();
*/
getMain() {
const { pages } = this;
return pages.filter(p => p.get('type') === typeMain)[0] || pages.at(0);
},
/**
* Get all pages
* @returns {Array<Page>}
* @example
* const arrayOfPages = pageManager.getAll();
*/
getAll() {
return this.pages.models;
},
getAllMap() {
return this.getAll().reduce((acc, i) => {
acc[i.get('id')] = i;
return acc;
}, {});
},
/**
* Change the selected page. This will switch the page rendered in canvas
* @param {String|Page} page Page or page id
* @returns {this}
* @example
* pageManager.select('page-id');
* // or by passing the page
* const somePage = pageManager.get('page-id');
* pageManager.select(somePage);
*/
select(page, opts = {}) {
const pg = isString(page) ? this.get(page) : page;
if (pg) {
this.em.trigger(evPageSelectBefore, pg, opts);
this.model.set('selected', pg, opts);
}
return this;
},
/**
* Get the selected page
* @returns {Page}
* @example
* const selectedPage = pageManager.getSelected();
*/
getSelected() {
return this.model.get('selected');
},
destroy() {
this.pages.off().reset();
this.model.stopListening();
this.model.clear({ silent: true });
['selected', 'config', 'em', 'pages', 'model'].map(i => (this[i] = 0));
},
store(noStore) {
if (!this.em.get('hasPages')) return {};
const obj = {};
const cnf = this.config;
obj[this.storageKey] = JSON.stringify(this.getAll());
if (!noStore && cnf.stm) cnf.stm.store(obj);
return obj;
},
load(data = {}) {
const key = this.storageKey;
let res = data[key] || [];
if (typeof res == 'string') {
try {
res = JSON.parse(data[key]);
} catch (err) {}
}
res && res.length && this.pages.reset(res);
return res;
},
_createId() {
const pages = this.getAll();
const len = pages.length + 16;
const pagesMap = this.getAllMap();
let id;
do {
id = createId(len);
} while (pagesMap[id]);
return id;
}
};
};

61
src/pages/model/Page.js

@ -0,0 +1,61 @@
import { Model } from 'backbone';
import { result, forEach } from 'underscore';
import Frames from 'canvas/model/Frames';
export default Model.extend({
defaults: () => ({
frames: [],
_undo: true
}),
initialize(props, opts = {}) {
const { config = {} } = opts;
const { em } = config;
const defFrame = {};
this.em = em;
if (!props.frames) {
defFrame.component = props.component;
defFrame.styles = props.styles;
['component', 'styles'].map(i => this.unset(i));
}
const frms = props.frames || [defFrame];
const frames = new Frames(frms, config);
frames.page = this;
this.set('frames', frames);
const um = em && em.get('UndoManager');
um && um.add(frames);
},
onRemove() {
this.get('frames').reset();
},
getFrames() {
return this.get('frames');
},
getMainFrame() {
return this.getFrames().at(0);
},
getMainComponent() {
const frame = this.getMainFrame();
return frame && frame.getComponent();
},
toJSON(opts = {}) {
const obj = Model.prototype.toJSON.call(this, opts);
const defaults = result(this, 'defaults');
// Remove private keys
forEach(obj, (value, key) => {
key.indexOf('_') === 0 && delete obj[key];
});
forEach(defaults, (value, key) => {
if (obj[key] === value) delete obj[key];
});
return obj;
}
});

26
src/pages/model/Pages.js

@ -0,0 +1,26 @@
import { Collection } from 'backbone';
import Page from './Page';
export default Collection.extend({
model: Page,
initialize(models, config = {}) {
this.config = config;
this.on('reset', this.onReset);
this.on('remove', this.onRemove);
},
onReset(m, opts = {}) {
const prev = opts.previousModels || [];
prev.map(p => this.onRemove(p));
},
onRemove(removed) {
removed && removed.onRemove();
},
add(m, o = {}) {
const { config } = this;
return Collection.prototype.add.call(this, m, { ...o, config });
}
});

3
src/panels/model/Buttons.js

@ -31,8 +31,7 @@ export default Backbone.Collection.extend({
const context = ctx || '';
this.forEach(model => {
if (model.get('context') == context && model !== sender) {
model.set('active', false, { silent: 1 });
model.trigger('updateActive', { fromCollection: 1 });
model.set('active', false, { fromCollection: 1 });
}
});
},

50
src/panels/view/ButtonView.js

@ -13,27 +13,41 @@ export default Backbone.View.extend({
},
initialize(o) {
var cls = this.model.get('className');
this.config = o.config || {};
this.em = this.config.em || {};
const { model } = this;
const cls = model.get('className');
const { command, listen } = model.attributes;
const config = o.config || {};
const { em } = config;
this.config = config;
this.em = em;
const pfx = this.config.stylePrefix || '';
const ppfx = this.config.pStylePrefix || '';
this.pfx = pfx;
this.ppfx = this.config.pStylePrefix || '';
this.id = pfx + this.model.get('id');
this.id = pfx + model.get('id');
this.activeCls = `${pfx}active ${ppfx}four-color`;
this.disableCls = `${ppfx}disabled`;
this.btnsVisCls = `${pfx}visible`;
this.className = pfx + 'btn' + (cls ? ' ' + cls : '');
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'change:active updateActive', this.updateActive);
this.listenTo(this.model, 'checkActive', this.checkActive);
this.listenTo(this.model, 'change:bntsVis', this.updateBtnsVis);
this.listenTo(this.model, 'change:attributes', this.updateAttributes);
this.listenTo(this.model, 'change:className', this.updateClassName);
this.listenTo(this.model, 'change:disable', this.updateDisable);
if (this.em && this.em.get) this.commands = this.em.get('Commands');
this.listenTo(model, 'change', this.render);
this.listenTo(model, 'change:active updateActive', this.updateActive);
this.listenTo(model, 'checkActive', this.checkActive);
this.listenTo(model, 'change:bntsVis', this.updateBtnsVis);
this.listenTo(model, 'change:attributes', this.updateAttributes);
this.listenTo(model, 'change:className', this.updateClassName);
this.listenTo(model, 'change:disable', this.updateDisable);
if (em && isString(command) && listen) {
const chnOpt = { fromListen: 1 };
this.listenTo(em, `run:${command}`, () =>
model.set('active', true, chnOpt)
);
this.listenTo(em, `stop:${command}`, () =>
model.set('active', false, chnOpt)
);
}
if (em && em.get) this.commands = em.get('Commands');
},
/**
@ -81,9 +95,9 @@ export default Backbone.View.extend({
*
* @return void
* */
updateActive(opts = {}) {
updateActive(m, v, opts = {}) {
const { model, commands, $el, activeCls } = this;
const { fromCollection } = opts;
const { fromCollection, fromListen } = opts;
const context = model.get('context');
const options = model.get('options');
const commandName = model.get('command');
@ -102,13 +116,15 @@ export default Backbone.View.extend({
if (model.get('active')) {
!fromCollection && model.collection.deactivateAll(context, model);
model.set('active', true, { silent: true }).trigger('checkActive');
commands.runCommand(command, { ...options, sender: model });
!fromListen &&
commands.runCommand(command, { ...options, sender: model });
// Disable button if the command has no stop method
command.noStop && model.set('active', false);
} else {
$el.removeClass(activeCls);
commands.stopCommand(command, { ...options, sender: model, force: 1 });
!fromListen &&
commands.stopCommand(command, { ...options, sender: model, force: 1 });
}
},

14
src/rich_text_editor/index.js

@ -33,7 +33,8 @@ import defaults from './config/config';
export default () => {
let config = {};
let toolbar, actions, lastEl, lastElPos, globalRte;
const eventsUp =
'change:canvasOffset canvasScroll frame:scroll component:update';
const hideToolbar = () => {
const style = toolbar.style;
const size = '-1000px';
@ -301,10 +302,8 @@ export default () => {
if (em) {
setTimeout(this.updatePosition.bind(this), 0);
const event =
'change:canvasOffset canvasScroll frame:scroll component:update';
em.off(event, this.updatePosition, this);
em.on(event, this.updatePosition, this);
em.off(eventsUp, this.updatePosition, this);
em.on(eventsUp, this.updatePosition, this);
em.trigger('rte:enable', view, rte);
}
@ -329,7 +328,10 @@ export default () => {
}
hideToolbar();
em && em.trigger('rte:disable', view, rte);
if (em) {
em.off(eventsUp, this.updatePosition, this);
em.trigger('rte:disable', view, rte);
}
}
};
};

14
src/storage_manager/index.js

@ -37,6 +37,7 @@ import LocalStorage from './model/LocalStorage';
import RemoteStorage from './model/RemoteStorage';
const eventStart = 'storage:start';
const eventAfter = 'storage:after';
const eventEnd = 'storage:end';
const eventError = 'storage:error';
@ -214,6 +215,7 @@ export default () => {
? st.store(
toStore,
res => {
this.onAfter('store', res);
clb && clb(res);
this.onEnd('store', res);
},
@ -259,6 +261,7 @@ export default () => {
result[itemKeyR] = res[itemKey];
}
this.onAfter('load', result);
clb && clb(result);
this.onEnd('load', result);
},
@ -301,6 +304,17 @@ export default () => {
}
},
/**
* On after callback (before passing data to the callback)
* @private
*/
onAfter(ctx, data) {
if (em) {
em.trigger(eventAfter);
ctx && em.trigger(`${eventAfter}:${ctx}`, data);
}
},
/**
* On end callback
* @private

2
src/style_manager/index.js

@ -299,7 +299,7 @@ export default () => {
} else if (config.avoidInlineStyle) {
rule = cssC.getIdRule(id, opts);
!rule && !skipAdd && (rule = cssC.setIdRule(id, {}, opts));
if (model.is('wrapper')) rule.set('wrapper', 1);
if (model.is('wrapper')) rule.set('wrapper', 1, addOpts);
}
rule && (model = rule);

3
src/styles/scss/_gjs_canvas.scss

@ -244,7 +244,8 @@ $guide_pad: 5px !default;
}
.#{$app-prefix}highlighter-sel {
outline: 3px solid $colorBlue;
outline: 2px solid $colorBlue;
outline-offset: -2px;
}
##{$app-prefix}tools, .#{$app-prefix}tools {

115
src/undo_manager/index.js

@ -25,6 +25,7 @@
*/
import UndoManager from 'backbone-undo';
import { isArray, isBoolean } from 'underscore';
export default () => {
let em;
@ -32,9 +33,11 @@ export default () => {
let config;
let beforeCache;
const configDef = {
maximumStackLength: 500
maximumStackLength: 500,
trackSelection: 1
};
const hasSkip = opts => opts.avoidStore || opts.noUndo;
const getChanged = obj => Object.keys(obj.changedAttributes());
return {
name: 'UndoManager',
@ -49,10 +52,42 @@ export default () => {
em = config.em;
this.em = em;
um = new UndoManager({ track: true, register: [], ...config });
um.changeUndoType('change', { condition: false });
um.changeUndoType('change', {
condition: object => {
const hasUndo = object.get('_undo');
if (hasUndo) {
const undoExc = object.get('_undoexc');
if (isArray(undoExc)) {
if (getChanged(object).some(chn => undoExc.indexOf(chn) >= 0))
return false;
}
if (isBoolean(hasUndo)) return true;
if (isArray(hasUndo)) {
if (getChanged(object).some(chn => hasUndo.indexOf(chn) >= 0))
return true;
}
}
return false;
},
on(object, v, opts) {
!beforeCache && (beforeCache = object.previousAttributes());
const opt = opts || v || {};
if (hasSkip(opt)) {
return;
} else {
const result = {
object,
before: beforeCache,
after: object.toJSON({ fromUndo: 1 })
};
beforeCache = null;
return result;
}
}
});
um.changeUndoType('add', {
on(model, collection, options = {}) {
if (hasSkip(options)) return;
on: (model, collection, options = {}) => {
if (hasSkip(options) || !this.isRegistered(collection)) return;
return {
object: collection,
before: undefined,
@ -62,8 +97,8 @@ export default () => {
}
});
um.changeUndoType('remove', {
on(model, collection, options = {}) {
if (hasSkip(options)) return;
on: (model, collection, options = {}) => {
if (hasSkip(options) || !this.isRegistered(collection)) return;
return {
object: collection,
before: model,
@ -72,42 +107,20 @@ export default () => {
};
}
});
const customUndoType = {
on(object, value, opt = {}) {
!beforeCache && (beforeCache = object.previousAttributes());
if (hasSkip(opt)) {
return;
} else {
const result = {
object,
before: beforeCache,
after: object.toJSON({ keepSymbols: 1 })
};
beforeCache = null;
return result;
}
},
undo(model, bf, af, opt) {
model.set(bf);
},
redo(model, bf, af, opt) {
model.set(af);
}
};
const events = ['style', 'attributes', 'content', 'src'];
events.forEach(ev => um.addUndoType(`change:${ev}`, customUndoType));
um.on('undo redo', () =>
em.trigger('component:toggled change:canvasOffset')
);
um.on('undo redo', () => {
em.trigger('component:toggled change:canvasOffset');
em.getSelectedAll().map(c => c.trigger('rerender:layer'));
});
['undo', 'redo'].forEach(ev => um.on(ev, () => em.trigger(ev)));
return this;
},
postLoad() {
config.trackSelection && em && this.add(em.get('selected'));
},
/**
* Get module configurations
* @return {Object} Configuration object
@ -241,6 +254,16 @@ export default () => {
return um.isAvailable('redo');
},
/**
* Check if the entity (Model/Collection) to tracked
* Note: New Components and CSSRules will be added automatically
* @param {Model|Collection} entity Entity to track
* @returns {Boolean}
*/
isRegistered(obj) {
return !!this.getInstance().objectRegistry.isRegistered(obj);
},
/**
* Get stack of changes
* @return {Collection}
@ -277,6 +300,26 @@ export default () => {
return result;
},
__getStackRead() {
const result = {};
const createItem = item => {
const { type, after, before, object } = item.attributes;
return {
type,
after,
before,
object
};
};
this.getStack().forEach(item => {
const index = item.get('magicFusionIndex');
const value = createItem(item);
if (!result[index]) result[index] = [value];
else result[index].push(value);
});
return Object.keys(result).map(i => result[i]);
},
getPointer() {
return this.getStack().pointer;
},

7
src/utils/Droppable.js

@ -13,7 +13,7 @@ export default class Droppable {
em
.get('Canvas')
.getFrames()
.map(frame => frame.get('root').getEl());
.map(frame => frame.getComponent().getEl());
const els = Array.isArray(el) ? el : [el];
this.el = el;
this.counter = 0;
@ -62,7 +62,6 @@ export default class Droppable {
this.over = 1;
const utils = em.get('Utils');
const canvas = em.get('Canvas');
const container = canvas.getBody();
// For security reason I can't read the drag data on dragenter, but
// as I need it for the Sorter context I will use `dragContent` or just
// any not empty element
@ -99,13 +98,13 @@ export default class Droppable {
nested: 1,
canvasRelative: 1,
direction: 'a',
container,
container: this.el,
placer: canvas.getPlacerEl(),
containerSel: '*',
itemSel: '*',
pfx: 'gjs-',
onEndMove: model => this.handleDragEnd(model, dt),
document: canvas.getFrameEl().contentDocument
document: this.el.ownerDocument
});
sorter.setDropContent(content);
sorter.startSort();

4
src/utils/Sorter.js

@ -1112,7 +1112,7 @@ export default Backbone.View.extend({
modelToDrop = model.collection.remove(model, { temporary: 1 });
}
} else {
modelToDrop = dropContent;
modelToDrop = isFunction(dropContent) ? dropContent() : dropContent;
opts.silent = false;
opts.avoidUpdateStyle = 1;
}
@ -1156,7 +1156,7 @@ export default Backbone.View.extend({
}
if (em) {
em.trigger('component:dragEnd', targetCollection, modelToDrop, warns); // @depricated
em.trigger('component:dragEnd', targetCollection, modelToDrop, warns); // @deprecated
em.trigger('sorter:drag:end', {
targetCollection,
modelToDrop,

12
src/utils/mixins.js

@ -224,6 +224,17 @@ const setViewEl = (el, view) => {
el.__gjsv = view;
};
const createId = (length = 16) => {
let result = '';
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const len = chars.length;
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * len));
}
return result;
};
export {
on,
off,
@ -250,5 +261,6 @@ export {
isObject,
isEmptyObj,
isComponent,
createId,
isRule
};

20
test/specs/css_composer/index.js

@ -321,5 +321,25 @@ describe('Css Composer', () => {
});
expect(obj.getAll().length).toEqual(toTest.length);
});
test('Get the right rule, containg similar selector names', () => {
const all = obj.getAll();
const name = 'rule-test';
const selClass = `.${name}`;
const selId = `#${name}`;
const decl = `{colore:red;}`;
all.add(`${selClass}${decl} ${selId}${decl}`);
expect(all.length).toBe(2);
const ruleClass = all.at(0);
const ruleId = all.at(1);
// Pre-check
expect(ruleClass.selectorsToString()).toBe(selClass);
expect(ruleId.selectorsToString()).toBe(selId);
expect(ruleClass.toCSS()).toBe(`${selClass}${decl}`);
expect(ruleId.toCSS()).toBe(`${selId}${decl}`);
// Check the get with the right rule
expect(obj.get(ruleClass.getSelectors())).toBe(ruleClass);
expect(obj.get(ruleId.getSelectors())).toBe(ruleId);
});
});
});

22
test/specs/dom_components/index.js

@ -19,7 +19,13 @@ describe('DOM Components', () => {
loadCompsOnRender: 0
},
get() {
return;
return {};
},
on() {
return this;
},
listenTo() {
return this;
},
getHtml() {
return 'testHtml';
@ -47,11 +53,13 @@ describe('DOM Components', () => {
em = new Editor({
avoidInlineStyle: 1
});
em.get('PageManager').onLoad();
config = {
em,
storeWrapper: 1
};
obj = new DomComponents().init(config);
obj = em.get('DomComponents');
// obj = new DomComponents().init(config);
});
afterEach(() => {
@ -80,8 +88,10 @@ describe('DOM Components', () => {
test('Store data', () => {
setSmConfig();
setEm();
//obj.getWrapper().get('components').add({});
(em.getHtml = () => {
return 'testHtml';
}),
(obj = em.get('DomComponents').init(config));
var expected = {
html: 'testHtml',
components: JSON.stringify(obj.getWrapper())
@ -143,10 +153,6 @@ describe('DOM Components', () => {
expect(obj.getComponents().length).toEqual(2);
});
test('Render wrapper', () => {
expect(obj.render()).toBeTruthy();
});
test('Import propertly components and styles with the same ids', () => {
obj = em.get('DomComponents');
const cc = em.get('CssComposer');

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

@ -15,11 +15,14 @@ const $ = Backbone.$;
let obj;
let dcomp;
let compOpts;
let em = new Editor({});
let em;
describe('Component', () => {
beforeEach(() => {
dcomp = new DomComponents();
em = new Editor();
dcomp = em.get('DomComponents');
em.get('PageManager').onLoad();
// dcomp = new DomComponents();
compOpts = {
em,
componentTypes: dcomp.componentTypes,
@ -589,7 +592,8 @@ describe('Video Component', () => {
describe('Components', () => {
beforeEach(() => {
em = new Editor({});
dcomp = new DomComponents();
dcomp = em.get('DomComponents');
em.get('PageManager').onLoad();
compOpts = {
em,
componentTypes: dcomp.componentTypes
@ -616,7 +620,8 @@ describe('Components', () => {
test('Avoid conflicting components with the same ID', () => {
const em = new Editor({});
dcomp = new DomComponents();
dcomp = em.get('DomComponents');
em.get('PageManager').onLoad();
dcomp.init({ em });
const id = 'myid';
const idB = 'myid2';

4
test/specs/dom_components/model/Symbols.js

@ -49,6 +49,10 @@ describe('Symbols', () => {
beforeAll(() => {
editor = new Editor({ symbols: 1 });
editor
.getModel()
.get('PageManager')
.onLoad();
wrapper = editor.getWrapper();
});

14
test/specs/editor/index.js

@ -9,6 +9,7 @@ describe('Editor', () => {
beforeEach(() => {
editor = new Editor();
editor.init();
editor.getModel().loadOnStart();
});
afterEach(() => {
@ -24,7 +25,7 @@ describe('Editor', () => {
const allKeys = keys(all);
// By default 1 wrapper components is created
expect(allKeys.length).toBe(initComps);
expect(allKeys[0]).toBe('wrapper');
expect(all[allKeys[0]].get('type')).toBe('wrapper');
});
test('Has no CSS rules', () => {
@ -41,7 +42,7 @@ describe('Editor', () => {
const wrapper = editor.getWrapper();
const style = editor.getStyle();
const frame = editor.Canvas.getFrame();
expect(wrapper).toBe(frame.get('root'));
expect(wrapper).toBe(frame.getComponent());
expect(style).toBe(frame.get('styles'));
});
@ -72,7 +73,6 @@ describe('Editor', () => {
});
test('Components are correctly tracked with UndoManager', () => {
editor.Components.postLoad(); // Init UndoManager
const all = editor.Components.allById();
const um = editor.UndoManager;
const umStack = um.getStack();
@ -88,7 +88,6 @@ describe('Editor', () => {
});
test('Components are correctly tracked with UndoManager and mutiple operations', () => {
editor.Components.postLoad(); // Init UndoManager
const all = editor.Components.allById();
const um = editor.UndoManager;
const umStack = um.getStack();
@ -106,12 +105,11 @@ describe('Editor', () => {
.components()
.at(0)
.remove(); // Remove 1 component
// UM registers 2 identical remove undoTypes as Backbone triggers remove from the
// collection and the model
expect(umStack.length).toBe(3);
expect(umStack.length).toBe(2);
expect(keys(all).length).toBe(3 + initComps);
wrapper.empty();
expect(umStack.length).toBe(4);
expect(umStack.length).toBe(3);
expect(keys(all).length).toBe(initComps);
});
});

3
test/specs/keymaps/index.js

@ -8,8 +8,9 @@ describe('Keymaps', () => {
let editor;
beforeEach(() => {
editor = Editor().init();
editor = Editor({ keymaps: { defaults: [] } }).init();
em = editor.getModel();
em.loadOnStart();
obj = editor.Keymaps;
});

252
test/specs/pages/index.js

@ -0,0 +1,252 @@
import Editor from 'editor';
describe('Pages', () => {
let editor;
let em;
let domc;
let initCmpLen;
let pm;
beforeAll(() => {
editor = new Editor({ pageManager: true });
em = editor.getModel();
domc = em.get('DomComponents');
pm = em.get('PageManager');
pm.onLoad();
initCmpLen = Object.keys(domc.allById()).length;
});
afterAll(() => {
editor.destroy();
pm = 0;
em = 0;
domc = 0;
});
beforeEach(() => {});
afterEach(() => {});
test('Pages module exists', () => {
expect(pm).toBeTruthy();
});
test('Has by default one page created', () => {
expect(pm.getAll().length).toBe(1);
});
test('The default page is selected', () => {
expect(pm.getMain()).toBe(pm.getSelected());
});
test('The default page has one frame', () => {
expect(pm.getMain().getFrames().length).toBe(1);
});
test('The default frame has the wrapper component', () => {
const frame = pm
.getMain()
.getFrames()
.at(0);
const frameCmp = frame.getComponent();
expect(frameCmp.is('wrapper')).toBe(true);
});
test('The default wrapper has no content', () => {
const frame = pm
.getMain()
.getFrames()
.at(0);
const frameCmp = frame.getComponent();
expect(frameCmp.components().length).toBe(0);
expect(frame.getStyles().length).toBe(0);
expect(initCmpLen).toBe(1);
});
test('Adding new page with selection', () => {
const name = 'Test page';
const page = pm.add({ name }, { select: 1 });
expect(page.id).toBeTruthy();
expect(page.get('name')).toBe(name);
expect(pm.getSelected()).toBe(page);
expect(pm.getAll().length).toBe(2);
const pageComp = page.getMainComponent();
expect(pageComp.is('wrapper')).toBe(true);
expect(pageComp.components().length).toBe(0);
});
describe('Init with pages', () => {
let idPage1, idComp1, idComp2, comp1, comp2, initPages, allbyId;
const createCompDef = id => ({
attributes: {
id,
class: id,
customattr: id
},
components: `Component ${id}`
});
beforeAll(() => {
idPage1 = 'page-1';
idComp1 = 'comp1';
idComp2 = 'comp2';
comp1 = createCompDef(idComp1);
comp2 = createCompDef(idComp2);
initPages = [
{
id: idPage1,
component: [comp1],
styles: `#${idComp1} { color: red }`
},
{
id: 'page-2',
frames: [
{
component: [comp2],
styles: `#${idComp2} { color: blue }`
}
]
},
{
id: 'page-3',
frames: [
{
component: '<div id="comp3">Component 3</div>',
styles: `#comp3 { color: green }`
}
]
}
];
editor = new Editor({
pageManager: {
pages: initPages
}
});
em = editor.getModel();
domc = em.get('DomComponents');
pm = em.get('PageManager');
pm.onLoad();
allbyId = domc.allById();
initCmpLen = Object.keys(allbyId).length;
});
afterAll(() => {
editor.destroy();
pm = 0;
em = 0;
domc = 0;
});
test('Pages are created correctly', () => {
const pages = pm.getAll();
expect(pages.length).toBe(initPages.length);
pages.map(page => {
// All pages should have an ID
expect(page.get('id')).toBeTruthy();
// The main component is always a wrapper
expect(
page
.getMainFrame()
.getComponent()
.is('wrapper')
).toBe(true);
});
// Components container should contain the same amount of wrappers as pages
const wrappers = Object.keys(allbyId)
.map(id => allbyId[id])
.filter(i => i.is('wrapper'));
expect(wrappers.length).toBe(initPages.length);
// Components container should contain the right amount of components
// Number of wrappers (eg. 3) where each one containes 1 component and 1 textnode (3 * 3)
expect(initCmpLen).toBe(initPages.length * 3);
// Each page contains 1 rule per component
expect(em.get('CssComposer').getAll().length).toBe(initPages.length);
});
});
});
describe('Managing pages', () => {
let editor;
let em;
let domc;
let initCmpLen;
let pm;
beforeEach(() => {
editor = new Editor({ pageManager: true });
em = editor.getModel();
domc = em.get('DomComponents');
pm = em.get('PageManager');
editor.getModel().loadOnStart();
initCmpLen = Object.keys(domc.allById()).length;
});
afterEach(() => {
editor.destroy();
pm = 0;
em = 0;
domc = 0;
});
test('Add page', () => {
const eventAdd = jest.fn();
em.on(pm.events.add, eventAdd);
pm.add({});
expect(pm.getAll().length).toBe(2);
expect(eventAdd).toBeCalledTimes(1);
});
test('Abort add page', () => {
em.on(pm.events.addBefore, (p, c, opts) => {
opts.abort = 1;
});
pm.add({});
expect(pm.getAll().length).toBe(1);
});
test('Abort add page and complete', () => {
em.on(pm.events.addBefore, (p, complete, opts) => {
opts.abort = 1;
complete();
});
pm.add({});
expect(pm.getAll().length).toBe(2);
});
test('Remove page', () => {
const eventRm = jest.fn();
em.on(pm.events.remove, eventRm);
const page = pm.add({});
pm.remove(page.id);
expect(pm.getAll().length).toBe(1);
expect(eventRm).toBeCalledTimes(1);
});
test('Abort remove page', () => {
em.on(pm.events.removeBefore, (p, c, opts) => {
opts.abort = 1;
});
const page = pm.add({});
pm.remove(page.id);
expect(pm.getAll().length).toBe(2);
});
test('Abort remove page and complete', () => {
em.on(pm.events.removeBefore, (p, complete, opts) => {
opts.abort = 1;
complete();
});
const page = pm.add({});
pm.remove(page.id);
expect(pm.getAll().length).toBe(1);
});
test('Change page', () => {
const event = jest.fn();
em.on(pm.events.update, event);
const page = pm.add({});
const up = { name: 'Test' };
const opts = { myopts: 1 };
page.set(up, opts);
expect(event).toBeCalledTimes(1);
expect(event).toBeCalledWith(page, up, opts);
});
});
Loading…
Cancel
Save