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/components', 'DOM Components'],
['/api/component', ' - Component'], ['/api/component', ' - Component'],
['/api/panels', 'Panels'], ['/api/panels', 'Panels'],
['/api/pages', 'Pages'],
['/api/style_manager', 'Style Manager'], ['/api/style_manager', 'Style Manager'],
['/api/storage_manager', 'Storage Manager'], ['/api/storage_manager', 'Storage Manager'],
['/api/device_manager', 'Device Manager'], ['/api/device_manager', 'Device Manager'],

1
docs/api.js

@ -23,6 +23,7 @@ const cmds = [
['undo_manager/index.js', 'undo_manager.md'], ['undo_manager/index.js', 'undo_manager.md'],
['canvas/index.js', 'canvas.md'], ['canvas/index.js', 'canvas.md'],
['i18n/index.js', 'i18n.md'], ['i18n/index.js', 'i18n.md'],
['pages/index.js', 'pages.md'],
].map(entry => ].map(entry =>
`${binRoot}documentation build ${srcRoot}/${entry[0]} -o ${docRoot}/api/${entry[1]} -f md --shallow --markdown-toc false`) `${binRoot}documentation build ${srcRoot}/${entry[0]} -o ${docRoot}/api/${entry[1]} -f md --shallow --markdown-toc false`)
.join(' && '); .join(' && ');

73
docs/api/canvas.md

@ -24,36 +24,35 @@ const canvas = editor.Canvas;
- [getWindow][5] - [getWindow][5]
- [getDocument][6] - [getDocument][6]
- [getBody][7] - [getBody][7]
- [getWrapperEl][8] - [setCustomBadgeLabel][8]
- [setCustomBadgeLabel][9] - [hasFocus][9]
- [hasFocus][10] - [scrollTo][10]
- [scrollTo][11] - [setZoom][11]
- [setZoom][12] - [getZoom][12]
- [getZoom][13]
## getConfig ## getConfig
Get the configuration object Get the configuration object
Returns **[Object][14]** Returns **[Object][13]**
## getElement ## getElement
Get the canvas element Get the canvas element
Returns **[HTMLElement][15]** Returns **[HTMLElement][14]**
## getFrameEl ## getFrameEl
Get the iframe element of the canvas Get the iframe element of the canvas
Returns **[HTMLIFrameElement][16]** Returns **[HTMLIFrameElement][15]**
## getWindow ## getWindow
Get the window instance of the iframe element Get the window instance of the iframe element
Returns **[Window][17]** Returns **[Window][16]**
## getDocument ## getDocument
@ -65,13 +64,7 @@ Returns **HTMLDocument**
Get the body of the iframe element Get the body of the iframe element
Returns **[HTMLBodyElement][18]** Returns **[HTMLBodyElement][17]**
## getWrapperEl
Get the wrapper element containing all the components
Returns **[HTMLElement][15]**
## setCustomBadgeLabel ## setCustomBadgeLabel
@ -79,7 +72,7 @@ Set custom badge naming strategy
### Parameters ### Parameters
- `f` **[Function][19]** - `f` **[Function][18]**
### Examples ### Examples
@ -93,13 +86,13 @@ canvas.setCustomBadgeLabel(function(component){
Get canvas rectangular data Get canvas rectangular data
Returns **[Object][14]** Returns **[Object][13]**
## hasFocus ## hasFocus
Check if the canvas is focused Check if the canvas is focused
Returns **[Boolean][20]** Returns **[Boolean][19]**
## scrollTo ## scrollTo
@ -110,9 +103,9 @@ passed to it. For instance, you can scroll smoothly by using
### Parameters ### Parameters
- `el` **([HTMLElement][15] | Component)** - `el` **([HTMLElement][14] | Component)**
- `opts` **[Object][14]** Options, same as options for `scrollIntoView` (optional, default `{}`) - `opts` **[Object][13]** 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`) - `opts.force` **[Boolean][19]** Force the scroll, even if the element is already visible (optional, default `false`)
### Examples ### Examples
@ -130,7 +123,7 @@ Set zoom value
### Parameters ### Parameters
- `value` **[Number][21]** The zoom value, from 0 to 100 - `value` **[Number][20]** The zoom value, from 0 to 100
Returns **this** Returns **this**
@ -138,7 +131,7 @@ Returns **this**
Get zoom value Get zoom value
Returns **[Number][21]** Returns **[Number][20]**
## addFrame ## addFrame
@ -146,7 +139,7 @@ Add new frame to the canvas
### Parameters ### Parameters
- `props` **[Object][14]** Frame properties (optional, default `{}`) - `props` **[Object][13]** Frame properties (optional, default `{}`)
- `opts` (optional, default `{}`) - `opts` (optional, default `{}`)
### Examples ### Examples
@ -186,30 +179,28 @@ Returns **Frame**
[7]: #getbody [7]: #getbody
[8]: #getwrapperel [8]: #setcustombadgelabel
[9]: #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** 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 ## getStyle
Get the style of the component Get the style of the component
@ -282,6 +301,10 @@ Returns **[Object][2]**
Return all component's attributes Return all component's attributes
### Parameters
- `opts` (optional, default `{}`)
Returns **[Object][2]** Returns **[Object][2]**
## addClass ## addClass
@ -354,7 +377,7 @@ Add new component children
### Parameters ### Parameters
- `components` **([Component][9] \| [String][1])** Component to add - `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 ### Examples
@ -366,6 +389,8 @@ someComponent.get('components').length // -> 2
// You can pass components directly // You can pass components directly
otherComponent.append(otherComponent2); otherComponent.append(otherComponent2);
otherComponent.append([otherComponent3, otherComponent4]); otherComponent.append([otherComponent3, otherComponent4]);
// append at specific index (eg. at the beginning)
someComponent.append(otherComponent, { at: 0 });
``` ```
Returns **[Array][4]** Array of appended components Returns **[Array][4]** Array of appended components
@ -378,6 +403,7 @@ current collection is returned
### Parameters ### Parameters
- `components` **([Component][9] \| [String][1])?** Components to set - `components` **([Component][9] \| [String][1])?** Components to set
- `opts` **[Object][2]** Options, same as in `Component.append()` (optional, default `{}`)
### Examples ### Examples

4
docs/api/css_composer.md

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

26
package-lock.json

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

4
package.json

@ -1,7 +1,7 @@
{ {
"name": "grapesjs", "name": "grapesjs",
"description": "Free and Open Source Web Builder Framework", "description": "Free and Open Source Web Builder Framework",
"version": "0.16.45", "version": "0.17.3",
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",
@ -113,7 +113,7 @@
"lint": "eslint src", "lint": "eslint src",
"check": "npm run lint && npm run test", "check": "npm run lint && npm run test",
"build": "npm run check && run-s build:*", "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: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", "build:locale": "rm -rf ./locale && node scripts/build-locale.js && babel locale -d locale --copy-files --no-comments",
"start": "run-p start:*", "start": "run-p start:*",

3
src/canvas/config/config.js

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

32
src/canvas/model/Canvas.js

@ -1,12 +1,10 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import Frame from './Frame'; import { evPageSelect } from 'pages';
import Frames from './Frames';
export default Backbone.Model.extend({ export default Backbone.Model.extend({
defaults: { defaults: {
frame: '', frame: '',
frames: '', frames: '',
wrapper: '',
rulers: false, rulers: false,
zoom: 100, zoom: 100,
x: 0, x: 0,
@ -15,17 +13,31 @@ export default Backbone.Model.extend({
initialize(config = {}) { initialize(config = {}) {
const { em } = 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 { styles = [], scripts = [] } = config;
const root = em && em.getWrapper(); const mainPage = em.get('PageManager').getMain();
const css = em && em.getStyle(); const frames = mainPage.getFrames();
const frame = new Frame({ root, styles: css }, config); const frame = mainPage.getMainFrame();
styles.forEach(style => frame.addLink(style)); styles.forEach(style => frame.addLink(style));
scripts.forEach(script => frame.addScript(script)); scripts.forEach(script => frame.addScript(script));
this.em = em;
this.set('frame', frame); this.set('frame', frame);
this.set('frames', new Frames([frame], config)); this.set('frames', frames);
this.listenTo(this, 'change:zoom', this.onZoomChange); },
this.listenTo(em, 'change:device', this.updateDevice);
_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() { updateDevice() {

146
src/canvas/model/Frame.js

@ -1,46 +1,74 @@
import Backbone from 'backbone'; import { Model } from 'backbone';
import Component from 'dom_components/model/Component'; import { result, forEach, isEmpty, debounce } from 'underscore';
import CssRules from 'css_composer/model/CssRules'; import { isComponent, isObject } from 'utils/mixins';
import { isString } from 'underscore';
const keyAutoW = '__aw';
export default Backbone.Model.extend({ const keyAutoH = '__ah';
defaults: {
wrapper: '', export default Model.extend({
width: null, defaults: () => ({
height: null,
head: '',
x: 0, x: 0,
y: 0, y: 0,
root: 0, changesCount: 0,
components: 0, attributes: {},
styles: 0, width: null,
attributes: {} height: null,
}, head: [],
component: '',
styles: '',
_undo: true,
_undoexc: ['changesCount']
}),
initialize(props, opts = {}) { initialize(props, opts = {}) {
const { root, styles, components } = this.attributes; const { config } = opts;
this.set('head', []); const { em } = config;
this.em = opts.em; const { styles, component } = this.attributes;
const modOpts = { const domc = em.get('DomComponents');
em: opts.em, const conf = domc.getConfig();
config: opts.em.get('DomComponents').getConfig(), const allRules = em.get('CssComposer').getAll();
frame: this this.em = em;
}; const modOpts = { em, config: conf, frame: this };
!root && if (!isComponent(component)) {
this.set( const wrp = isObject(component) ? component : { components: component };
'root', !wrp.type && (wrp.type = 'wrapper');
new Component( const Wrapper = domc.getType('wrapper').model;
{ this.set('component', new Wrapper(wrp, modOpts));
type: 'wrapper', }
components: components || []
}, if (!styles) {
modOpts this.set('styles', allRules);
) } else if (!isObject(styles)) {
); allRules.add(styles);
this.set('styles', allRules);
(!styles || isString(styles)) && }
this.set('styles', new CssRules(styles, modOpts));
!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() { remove() {
@ -113,7 +141,47 @@ export default Backbone.Model.extend({
this.removeHeadByAttr('src', src, 'script'); this.removeHeadByAttr('src', src, 'script');
}, },
getPage() {
const coll = this.collection;
return coll && coll.page;
},
_emitUpdated(data = {}) { _emitUpdated(data = {}) {
this.em.trigger('frame:updated', { frame: this, ...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 { bindAll } from 'underscore';
import Backbone from 'backbone'; import { Collection } from 'backbone';
import model from './Frame'; import model from './Frame';
export default Backbone.Collection.extend({ export default Collection.extend({
model, model,
initialize() { initialize(models, config = {}) {
bindAll(this, 'itemLoaded'); 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() { itemLoaded() {
@ -26,5 +38,10 @@ export default Backbone.Collection.extend({
listenToLoadItems(on) { listenToLoadItems(on) {
this.forEach(item => item[on ? 'on' : 'off']('loaded', this.itemLoaded)); 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) { initialize(o) {
bindAll(this, 'clearOff', 'onKeyPress', 'onCanvasMove'); bindAll(this, 'clearOff', 'onKeyPress', 'onCanvasMove');
on(window, 'scroll resize', this.clearOff);
const { model } = this; const { model } = this;
const frames = model.get('frames');
this.config = o.config || {}; this.config = o.config || {};
this.em = this.config.em || {}; this.em = this.config.em || {};
this.pfx = this.config.stylePrefix || ''; this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || ''; this.ppfx = this.config.pStylePrefix || '';
this.className = this.config.stylePrefix + 'canvas'; 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({ this.frames = new FramesView({
collection: frames, collection,
config: { config: {
...config, ...config,
canvasView: this, canvasView: this,
renderContent: 1 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 = {}) { checkSelected(component, opts = {}) {
@ -89,6 +101,7 @@ export default Backbone.View.extend({
const { el } = this; const { el } = this;
const fn = enable ? on : off; const fn = enable ? on : off;
fn(document, 'keypress', this.onKeyPress); fn(document, 'keypress', this.onKeyPress);
fn(window, 'scroll resize', this.clearOff);
// fn(el, 'mousemove dragover', this.onCanvasMove); // fn(el, 'mousemove dragover', this.onCanvasMove);
}, },
@ -328,22 +341,24 @@ export default Backbone.View.extend({
return (view && view._getFrame()) || this.em.get('currentFrame'); 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() { render() {
const { el, $el, ppfx, model, em, frames } = this; const { el, $el, ppfx, config } = this;
const cssc = em.get('CssComposer');
const wrapper = model.get('wrapper');
$el.html(this.template()); $el.html(this.template());
const $frames = $el.find('[data-frames]'); const $frames = $el.find('[data-frames]');
this.framesArea = $frames.get(0); 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]'); const toolsWrp = $el.find('[data-tools]');
this.toolsWrapper = toolsWrp.get(0); this.toolsWrapper = toolsWrp.get(0);
@ -354,6 +369,7 @@ export default Backbone.View.extend({
</div> </div>
</div> </div>
<div id="${ppfx}tools" style="pointer-events:none"> <div id="${ppfx}tools" style="pointer-events:none">
${config.extHl ? `<div class="${ppfx}highlighter-sel"></div>` : ''}
<div class="${ppfx}badge"></div> <div class="${ppfx}badge"></div>
<div class="${ppfx}ghost"></div> <div class="${ppfx}ghost"></div>
<div class="${ppfx}toolbar" style="pointer-events:all"></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.toolsGlobEl = el.querySelector(`.${ppfx}tools-gl`);
this.toolsEl = toolsEl; this.toolsEl = toolsEl;
this.el.className = this.className; this.el.className = this.className;
this.ready = 1;
// Render all frames this._renderFrames();
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;
return this; return this;
} }

23
src/canvas/view/FrameView.js

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

55
src/canvas/view/FrameWrapView.js

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

4
src/canvas/view/FramesView.js

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

2
src/code_manager/model/CssGenerator.js

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

19
src/commands/view/CommandAbstract.js

@ -18,17 +18,6 @@ export default Backbone.View.extend({
this.freezClass = this.ppfx + 'freezed'; this.freezClass = this.ppfx + 'freezed';
this.canvas = this.em.get && this.em.get('Canvas'); 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); this.init(this.config);
}, },
@ -55,14 +44,6 @@ export default Backbone.View.extend({
return this.canvas.getBody(); return this.canvas.getBody();
}, },
/**
* Get canvas wrapper element
* @return {HTMLElement}
*/
getCanvasWrapper() {
return this.canvas.getWrapperEl();
},
/** /**
* Get canvas wrapper element * Get canvas wrapper element
* @return {HTMLElement} * @return {HTMLElement}

30
src/commands/view/SelectComponent.js

@ -613,7 +613,8 @@ export default {
} }
const unit = 'px'; 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 frameOff = this.canvas.canvasRectOffset(el, pos);
const topOff = frameOff.top; const topOff = frameOff.top;
const leftOff = frameOff.left; const leftOff = frameOff.left;
@ -629,12 +630,28 @@ export default {
style.left = leftOff + unit; style.left = leftOff + unit;
style.width = pos.width + unit; style.width = pos.width + unit;
style.height = pos.height + 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() { _upToolbar: debounce(function() {
this.updateToolsGlobal({ force: 1 }); this.updateToolsGlobal({ force: 1 });
}), }),
_trgToolUp(type, opts = {}) {
this.em.trigger('canvas:tools:update', {
type,
...opts
});
},
updateToolsGlobal(opts = {}) { updateToolsGlobal(opts = {}) {
const { el, pos, component } = this.getElSelected(); const { el, pos, component } = this.getElSelected();
@ -653,7 +670,8 @@ export default {
} }
const unit = 'px'; const unit = 'px';
const { style } = this.toggleToolsEl(1); const toolsEl = this.toggleToolsEl(1);
const { style } = toolsEl;
const targetToElem = canvas.getTargetToElementFixed( const targetToElem = canvas.getTargetToElementFixed(
el, el,
canvas.getToolbarEl(), canvas.getToolbarEl(),
@ -667,6 +685,14 @@ export default {
style.height = pos.height + unit; style.height = pos.height + unit;
this.updateToolbarPos({ top: targetToElem.top, left: targetToElem.left }); 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 { export default {
init() {
bindAll(this, '_onFramesChange');
},
run(ed) { run(ed) {
this.toggleVis(ed); this.toggleVis(ed);
}, },
@ -9,11 +15,19 @@ export default {
toggleVis(ed, active = 1) { toggleVis(ed, active = 1) {
if (!ed.Commands.isActive('preview')) { if (!ed.Commands.isActive('preview')) {
const method = active ? 'add' : 'remove'; const cv = ed.Canvas;
const mth = active ? 'on' : 'off';
ed.Canvas.getFrames().forEach(frame => { cv.getFrames().forEach(frame => this._upFrame(frame, active));
frame.view.getBody().classList[method](`${this.ppfx}dashed`); 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 * @private
*/ */
onLoad() { onLoad() {
rules.add(c.rules); rules.add(c.rules, { silent: 1 });
}, },
/** /**
@ -106,28 +106,9 @@ export default () => {
* @param {Editor} em * @param {Editor} em
* @private * @private
*/ */
postLoad(em) { postLoad() {
const ev = 'add remove'; const um = em && em.get('UndoManager');
const rules = this.getAll(); um && um.add(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);
}, },
/** /**
@ -170,9 +151,10 @@ export default () => {
*/ */
store(noStore) { store(noStore) {
if (!c.stm) return; if (!c.stm) return;
var obj = {}; const obj = {};
var keys = this.storageKey(); const keys = this.storageKey();
if (keys.indexOf('css') >= 0) obj.css = c.em.getCss(); 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 (keys.indexOf('styles') >= 0) obj.styles = JSON.stringify(rules);
if (!noStore) c.stm.store(obj); if (!noStore) c.stm.store(obj);
return obj; return obj;
@ -289,7 +271,7 @@ export default () => {
} }
var modelExists = this.get(newSels, rule.state, rule.mediaText, rule); 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; var updateStyle = !modelExists || !opts.avoidUpdateStyle;
const style = rule.style || {}; const style = rule.style || {};
@ -297,7 +279,7 @@ export default () => {
let styleUpdate = opts.extend let styleUpdate = opts.extend
? { ...model.get('style'), ...style } ? { ...model.get('style'), ...style }
: style; : style;
model.set('style', styleUpdate); model.set('style', styleUpdate, opts);
} }
result.push(model); result.push(model);

32
src/css_composer/model/CssRule.js

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

9
src/css_composer/view/CssRuleView.js

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

119
src/dom_components/index.js

@ -251,43 +251,10 @@ export default () => {
); );
} }
// Build wrapper if (em.get('hasPages')) {
let components = c.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;
}
} }
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; return this;
}, },
@ -296,71 +263,7 @@ export default () => {
* @private * @private
*/ */
onLoad() { onLoad() {
this.setComponents(c.components); c.components && this.setComponents(c.components, { silent: 1 });
},
/**
* 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);
}, },
/** /**
@ -416,8 +319,8 @@ export default () => {
* @return {Object} Data to store * @return {Object} Data to store
*/ */
store(noStore) { store(noStore) {
if (!c.stm) { if (!c.stm || this.em.get('hasPages')) {
return; return {};
} }
var obj = {}; var obj = {};
@ -428,7 +331,6 @@ export default () => {
} }
if (keys.indexOf('components') >= 0) { if (keys.indexOf('components') >= 0) {
const { em } = this;
// const storeWrap = (em && !em.getConfig('avoidInlineStyle')) || c.storeWrapper; // const storeWrap = (em && !em.getConfig('avoidInlineStyle')) || c.storeWrapper;
const storeWrap = c.storeWrapper; const storeWrap = c.storeWrapper;
const toStore = storeWrap ? this.getWrapper() : this.getComponents(); const toStore = storeWrap ? this.getWrapper() : this.getComponents();
@ -448,7 +350,11 @@ export default () => {
* @private * @private
*/ */
getComponent() { getComponent() {
return component; return this.em
.get('PageManager')
.getSelected()
.getMainFrame()
.getComponent();
}, },
/** /**
@ -738,8 +644,9 @@ export default () => {
}, },
destroy() { destroy() {
this.clear(); const all = this.allById();
componentView.remove(); Object.keys(all).forEach(id => all[id] && all[id].remove());
componentView && componentView.remove();
[c, em, componentsById, component, componentView].forEach(i => (i = {})); [c, em, componentsById, component, componentView].forEach(i => (i = {}));
this.em = {}; this.em = {};
} }

95
src/dom_components/model/Component.js

@ -30,6 +30,8 @@ export const eventDrag = 'component:drag';
export const keySymbols = '__symbols'; export const keySymbols = '__symbols';
export const keySymbol = '__symbol'; export const keySymbol = '__symbol';
export const keySymbol2w = '__symbol2w'; 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 * 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'], traits: ['id', 'title'],
propagate: '', propagate: '',
dmode: '', 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', this.attrUpdated);
this.listenTo(this, 'change:attributes:id', this._idUpdated); this.listenTo(this, 'change:attributes:id', this._idUpdated);
this.on('change:toolbar', this.__emitUpdateTlb); this.on('change:toolbar', this.__emitUpdateTlb);
this.on('change', this.__onChange);
this.on(keyUpdateInside, this.__propToParent);
this.set('status', ''); this.set('status', '');
this.views = []; this.views = [];
@ -205,12 +213,60 @@ const Component = Backbone.Model.extend(Styleable).extend(
}); });
if (!opt.temporary) { if (!opt.temporary) {
this.__postAdd();
this.init(); this.init();
this.__isSymbolOrInst() && this.__initSymb(); this.__isSymbolOrInst() && this.__initSymb();
em && em.trigger('component:create', this); 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() { __emitUpdateTlb() {
this.emitUpdate('toolbar'); this.emitUpdate('toolbar');
}, },
@ -720,10 +776,12 @@ const Component = Backbone.Model.extend(Styleable).extend(
// This will propagate the change up to __upSymbProps // This will propagate the change up to __upSymbProps
child.set('classes', this.get('classes'), { fromInstance: this }); child.set('classes', this.get('classes'), { fromInstance: this });
}); });
this.__changesUp(opts);
}, },
__upSymbComps(m, c, o) { __upSymbComps(m, c, o) {
const { fromInstance } = o || c || {}; const optUp = o || c || {};
const { fromInstance } = optUp;
const toUpOpts = { fromInstance }; const toUpOpts = { fromInstance };
const isTemp = m.opt.temporary; const isTemp = m.opt.temporary;
@ -791,6 +849,8 @@ const Component = Backbone.Model.extend(Styleable).extend(
}); });
} }
} }
this.__changesUp(optUp);
}, },
initClasses(m, c, opts = {}) { initClasses(m, c, opts = {}) {
@ -821,6 +881,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
const addChild = !this.opt.avoidChildren; const addChild = !this.opt.avoidChildren;
this.set('components', comps); this.set('components', comps);
addChild && addChild &&
components &&
comps.add( comps.add(
isFunction(components) ? components(this) : components, isFunction(components) ? components(this) : components,
this.opt this.opt
@ -1177,13 +1238,13 @@ const Component = Backbone.Model.extend(Styleable).extend(
cloned.unset(keySymbols); cloned.unset(keySymbols);
} else if (symbol) { } else if (symbol) {
// Contains already a reference to a symbol // Contains already a reference to a symbol
symbol.get(keySymbols).push(cloned); symbol.set(keySymbols, [...symbol.__getSymbols(), cloned]);
cloned.__initSymb(); cloned.__initSymb();
} else if (opt.symbol) { } else if (opt.symbol) {
// Request to create a symbol // Request to create a symbol
if (this.__isSymbol()) { if (this.__isSymbol()) {
// Already a symbol, cloned should be an instance // Already a symbol, cloned should be an instance
this.get(keySymbols).push(cloned); this.set(keySymbols, [...symbols, cloned]);
cloned.set(keySymbol, this); cloned.set(keySymbol, this);
cloned.__initSymb(); cloned.__initSymb();
} else if (opt.symbolInv) { } else if (opt.symbolInv) {
@ -1341,7 +1402,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
delete obj.status; delete obj.status;
delete obj.open; // used in Layers delete obj.open; // used in Layers
if (!opts.keepSymbols) { if (!opts.fromUndo) {
if (obj[keySymbols]) { if (obj[keySymbols]) {
obj[keySymbols] = this.__getSymbToUp().map(i => i.getId()); obj[keySymbols] = this.__getSymbToUp().map(i => i.getId());
} }
@ -1496,17 +1557,24 @@ const Component = Backbone.Model.extend(Styleable).extend(
}, },
emitUpdate(property, ...args) { emitUpdate(property, ...args) {
const em = this.em; const { em } = this;
const event = 'component:update' + (property ? `:${property}` : ''); const event = keyUpdate + (property ? `:${property}` : '');
const item = property && this.get(property);
property && property &&
this.updated( this.updated(
property, property,
property && this.get(property), item,
property && this.previous(property), property && this.previous(property),
...args ...args
); );
this.trigger(event, ...args); this.trigger(event, ...args);
em && em.trigger(event, this, ...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 = {}) { remove(opts = {}) {
const { em } = this; const { em } = this;
const coll = this.collection; 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 }; const rmOpts = { ...opts };
[this, em].map(i => [this, em].map(i =>
i.trigger('component:remove:before', this, remove, rmOpts) 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 * not ok, as it was shared between multiple editor instances
*/ */
getList(model) { getList(model) {
const domc = model.opt && model.opt.domc; const { opt = {} } = model;
return domc ? domc.componentsById : {}; 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() { toHTML() {
this.trigger('sync:content', { silent: 1 });
return Component.prototype.toHTML.apply(this, arguments); return Component.prototype.toHTML.apply(this, arguments);
} }
}); });

31
src/dom_components/model/ComponentWrapper.js

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

11
src/dom_components/view/ComponentView.js

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

8
src/domain_abstract/view/DomainViews.js

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

32
src/editor/model/Editor.js

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

2
src/navigator/index.js

@ -50,7 +50,7 @@ export default () => {
* @return {self} * @return {self}
*/ */
setRoot(el) { setRoot(el) {
layers.setRoot(el); layers && layers.setRoot(el);
return this; 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:open', this.updateOpening);
this.listenTo(model, 'change:layerable', this.updateLayerable); this.listenTo(model, 'change:layerable', this.updateLayerable);
this.listenTo(model, 'change:style:display', this.updateVisibility); 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.className = `${pfx}layer ${pfx}layer__t-${type} no-select ${ppfx}two-color`;
this.inputNameCls = `${ppfx}layer-name`; this.inputNameCls = `${ppfx}layer-name`;
this.clsTitleC = `${pfx}layer-title-c`; this.clsTitleC = `${pfx}layer-title-c`;
@ -312,7 +313,8 @@ export default Backbone.View.extend({
updateStatus(e) { updateStatus(e) {
ComponentView.prototype.updateStatus.apply(this, [ 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() { __render() {
const { model, config, el } = this; const { model, config, el } = this;
const { onRender } = config; 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 || ''; const context = ctx || '';
this.forEach(model => { this.forEach(model => {
if (model.get('context') == context && model !== sender) { if (model.get('context') == context && model !== sender) {
model.set('active', false, { silent: 1 }); model.set('active', false, { fromCollection: 1 });
model.trigger('updateActive', { fromCollection: 1 });
} }
}); });
}, },

50
src/panels/view/ButtonView.js

@ -13,27 +13,41 @@ export default Backbone.View.extend({
}, },
initialize(o) { initialize(o) {
var cls = this.model.get('className'); const { model } = this;
this.config = o.config || {}; const cls = model.get('className');
this.em = this.config.em || {}; const { command, listen } = model.attributes;
const config = o.config || {};
const { em } = config;
this.config = config;
this.em = em;
const pfx = this.config.stylePrefix || ''; const pfx = this.config.stylePrefix || '';
const ppfx = this.config.pStylePrefix || ''; const ppfx = this.config.pStylePrefix || '';
this.pfx = pfx; this.pfx = pfx;
this.ppfx = this.config.pStylePrefix || ''; 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.activeCls = `${pfx}active ${ppfx}four-color`;
this.disableCls = `${ppfx}disabled`; this.disableCls = `${ppfx}disabled`;
this.btnsVisCls = `${pfx}visible`; this.btnsVisCls = `${pfx}visible`;
this.className = pfx + 'btn' + (cls ? ' ' + cls : ''); this.className = pfx + 'btn' + (cls ? ' ' + cls : '');
this.listenTo(this.model, 'change', this.render); this.listenTo(model, 'change', this.render);
this.listenTo(this.model, 'change:active updateActive', this.updateActive); this.listenTo(model, 'change:active updateActive', this.updateActive);
this.listenTo(this.model, 'checkActive', this.checkActive); this.listenTo(model, 'checkActive', this.checkActive);
this.listenTo(this.model, 'change:bntsVis', this.updateBtnsVis); this.listenTo(model, 'change:bntsVis', this.updateBtnsVis);
this.listenTo(this.model, 'change:attributes', this.updateAttributes); this.listenTo(model, 'change:attributes', this.updateAttributes);
this.listenTo(this.model, 'change:className', this.updateClassName); this.listenTo(model, 'change:className', this.updateClassName);
this.listenTo(this.model, 'change:disable', this.updateDisable); this.listenTo(model, 'change:disable', this.updateDisable);
if (this.em && this.em.get) this.commands = this.em.get('Commands'); 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 * @return void
* */ * */
updateActive(opts = {}) { updateActive(m, v, opts = {}) {
const { model, commands, $el, activeCls } = this; const { model, commands, $el, activeCls } = this;
const { fromCollection } = opts; const { fromCollection, fromListen } = opts;
const context = model.get('context'); const context = model.get('context');
const options = model.get('options'); const options = model.get('options');
const commandName = model.get('command'); const commandName = model.get('command');
@ -102,13 +116,15 @@ export default Backbone.View.extend({
if (model.get('active')) { if (model.get('active')) {
!fromCollection && model.collection.deactivateAll(context, model); !fromCollection && model.collection.deactivateAll(context, model);
model.set('active', true, { silent: true }).trigger('checkActive'); 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 // Disable button if the command has no stop method
command.noStop && model.set('active', false); command.noStop && model.set('active', false);
} else { } else {
$el.removeClass(activeCls); $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 () => { export default () => {
let config = {}; let config = {};
let toolbar, actions, lastEl, lastElPos, globalRte; let toolbar, actions, lastEl, lastElPos, globalRte;
const eventsUp =
'change:canvasOffset canvasScroll frame:scroll component:update';
const hideToolbar = () => { const hideToolbar = () => {
const style = toolbar.style; const style = toolbar.style;
const size = '-1000px'; const size = '-1000px';
@ -301,10 +302,8 @@ export default () => {
if (em) { if (em) {
setTimeout(this.updatePosition.bind(this), 0); setTimeout(this.updatePosition.bind(this), 0);
const event = em.off(eventsUp, this.updatePosition, this);
'change:canvasOffset canvasScroll frame:scroll component:update'; em.on(eventsUp, this.updatePosition, this);
em.off(event, this.updatePosition, this);
em.on(event, this.updatePosition, this);
em.trigger('rte:enable', view, rte); em.trigger('rte:enable', view, rte);
} }
@ -329,7 +328,10 @@ export default () => {
} }
hideToolbar(); 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'; import RemoteStorage from './model/RemoteStorage';
const eventStart = 'storage:start'; const eventStart = 'storage:start';
const eventAfter = 'storage:after';
const eventEnd = 'storage:end'; const eventEnd = 'storage:end';
const eventError = 'storage:error'; const eventError = 'storage:error';
@ -214,6 +215,7 @@ export default () => {
? st.store( ? st.store(
toStore, toStore,
res => { res => {
this.onAfter('store', res);
clb && clb(res); clb && clb(res);
this.onEnd('store', res); this.onEnd('store', res);
}, },
@ -259,6 +261,7 @@ export default () => {
result[itemKeyR] = res[itemKey]; result[itemKeyR] = res[itemKey];
} }
this.onAfter('load', result);
clb && clb(result); clb && clb(result);
this.onEnd('load', 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 * On end callback
* @private * @private

2
src/style_manager/index.js

@ -299,7 +299,7 @@ export default () => {
} else if (config.avoidInlineStyle) { } else if (config.avoidInlineStyle) {
rule = cssC.getIdRule(id, opts); rule = cssC.getIdRule(id, opts);
!rule && !skipAdd && (rule = cssC.setIdRule(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); rule && (model = rule);

3
src/styles/scss/_gjs_canvas.scss

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

115
src/undo_manager/index.js

@ -25,6 +25,7 @@
*/ */
import UndoManager from 'backbone-undo'; import UndoManager from 'backbone-undo';
import { isArray, isBoolean } from 'underscore';
export default () => { export default () => {
let em; let em;
@ -32,9 +33,11 @@ export default () => {
let config; let config;
let beforeCache; let beforeCache;
const configDef = { const configDef = {
maximumStackLength: 500 maximumStackLength: 500,
trackSelection: 1
}; };
const hasSkip = opts => opts.avoidStore || opts.noUndo; const hasSkip = opts => opts.avoidStore || opts.noUndo;
const getChanged = obj => Object.keys(obj.changedAttributes());
return { return {
name: 'UndoManager', name: 'UndoManager',
@ -49,10 +52,42 @@ export default () => {
em = config.em; em = config.em;
this.em = em; this.em = em;
um = new UndoManager({ track: true, register: [], ...config }); 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', { um.changeUndoType('add', {
on(model, collection, options = {}) { on: (model, collection, options = {}) => {
if (hasSkip(options)) return; if (hasSkip(options) || !this.isRegistered(collection)) return;
return { return {
object: collection, object: collection,
before: undefined, before: undefined,
@ -62,8 +97,8 @@ export default () => {
} }
}); });
um.changeUndoType('remove', { um.changeUndoType('remove', {
on(model, collection, options = {}) { on: (model, collection, options = {}) => {
if (hasSkip(options)) return; if (hasSkip(options) || !this.isRegistered(collection)) return;
return { return {
object: collection, object: collection,
before: model, 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) { um.on('undo redo', () => {
model.set(af); em.trigger('component:toggled change:canvasOffset');
} em.getSelectedAll().map(c => c.trigger('rerender:layer'));
}; });
const events = ['style', 'attributes', 'content', 'src'];
events.forEach(ev => um.addUndoType(`change:${ev}`, customUndoType));
um.on('undo redo', () =>
em.trigger('component:toggled change:canvasOffset')
);
['undo', 'redo'].forEach(ev => um.on(ev, () => em.trigger(ev))); ['undo', 'redo'].forEach(ev => um.on(ev, () => em.trigger(ev)));
return this; return this;
}, },
postLoad() {
config.trackSelection && em && this.add(em.get('selected'));
},
/** /**
* Get module configurations * Get module configurations
* @return {Object} Configuration object * @return {Object} Configuration object
@ -241,6 +254,16 @@ export default () => {
return um.isAvailable('redo'); 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 * Get stack of changes
* @return {Collection} * @return {Collection}
@ -277,6 +300,26 @@ export default () => {
return result; 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() { getPointer() {
return this.getStack().pointer; return this.getStack().pointer;
}, },

7
src/utils/Droppable.js

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

4
src/utils/Sorter.js

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

12
src/utils/mixins.js

@ -224,6 +224,17 @@ const setViewEl = (el, view) => {
el.__gjsv = 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 { export {
on, on,
off, off,
@ -250,5 +261,6 @@ export {
isObject, isObject,
isEmptyObj, isEmptyObj,
isComponent, isComponent,
createId,
isRule isRule
}; };

20
test/specs/css_composer/index.js

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

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

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

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

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

14
test/specs/editor/index.js

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

3
test/specs/keymaps/index.js

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