Browse Source

Merge pull request #3487 from artf/dev

Merge dev
pull/3550/head
Artur Arseniev 5 years ago
committed by GitHub
parent
commit
07061ae130
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      .github/ISSUE_TEMPLATE/01_bug_report.md
  2. 8
      .github/ISSUE_TEMPLATE/02_feature_request.md
  3. 2
      dist/css/grapes.min.css
  4. 4
      dist/grapes.min.js
  5. 2
      dist/grapes.min.js.map
  6. 1
      docs/.vuepress/config.js
  7. 1
      docs/api.js
  8. 73
      docs/api/canvas.md
  9. 28
      docs/api/component.md
  10. 4
      docs/api/css_composer.md
  11. 142
      docs/api/editor.md
  12. 177
      docs/api/pages.md
  13. 12
      docs/api/undo_manager.md
  14. 44
      package-lock.json
  15. 4
      package.json
  16. 14
      src/block_manager/model/Block.js
  17. 3
      src/block_manager/view/BlocksView.js
  18. 11
      src/block_manager/view/CategoryView.js
  19. 3
      src/canvas/config/config.js
  20. 57
      src/canvas/index.js
  21. 34
      src/canvas/model/Canvas.js
  22. 149
      src/canvas/model/Frame.js
  23. 23
      src/canvas/model/Frames.js
  24. 72
      src/canvas/view/CanvasView.js
  25. 23
      src/canvas/view/FrameView.js
  26. 55
      src/canvas/view/FrameWrapView.js
  27. 4
      src/canvas/view/FramesView.js
  28. 2
      src/code_manager/model/CodeMirrorEditor.js
  29. 2
      src/code_manager/model/CssGenerator.js
  30. 2
      src/code_manager/model/JsGenerator.js
  31. 19
      src/commands/view/CommandAbstract.js
  32. 44
      src/commands/view/SelectComponent.js
  33. 24
      src/commands/view/SwitchVisibility.js
  34. 3
      src/css_composer/config/config.js
  35. 55
      src/css_composer/index.js
  36. 32
      src/css_composer/model/CssRule.js
  37. 4
      src/css_composer/model/CssRules.js
  38. 9
      src/css_composer/view/CssRuleView.js
  39. 22
      src/dom_components/config/config.js
  40. 123
      src/dom_components/index.js
  41. 307
      src/dom_components/model/Component.js
  42. 1
      src/dom_components/model/ComponentText.js
  43. 31
      src/dom_components/model/ComponentWrapper.js
  44. 63
      src/dom_components/model/Components.js
  45. 11
      src/dom_components/view/ComponentView.js
  46. 8
      src/domain_abstract/view/DomainViews.js
  47. 5
      src/editor/index.js
  48. 58
      src/editor/model/Editor.js
  49. 44
      src/editor/model/Selected.js
  50. 4
      src/editor/view/EditorView.js
  51. 2
      src/i18n/locale/de.js
  52. 117
      src/i18n/locale/el.js
  53. 210
      src/i18n/locale/se.js
  54. 14
      src/navigator/config/config.js
  55. 7
      src/navigator/index.js
  56. 27
      src/navigator/view/ItemView.js
  57. 14
      src/navigator/view/ItemsView.js
  58. 297
      src/pages/index.js
  59. 61
      src/pages/model/Page.js
  60. 26
      src/pages/model/Pages.js
  61. 3
      src/panels/model/Buttons.js
  62. 50
      src/panels/view/ButtonView.js
  63. 2
      src/panels/view/PanelView.js
  64. 14
      src/rich_text_editor/index.js
  65. 14
      src/storage_manager/index.js
  66. 2
      src/style_manager/index.js
  67. 11
      src/styles/scss/_gjs_canvas.scss
  68. 1
      src/trait_manager/view/TraitView.js
  69. 126
      src/undo_manager/index.js
  70. 7
      src/utils/Droppable.js
  71. 48
      src/utils/Resizer.js
  72. 33
      src/utils/Sorter.js
  73. 22
      src/utils/mixins.js
  74. 20
      test/specs/css_composer/index.js
  75. 22
      test/specs/dom_components/index.js
  76. 13
      test/specs/dom_components/model/Component.js
  77. 279
      test/specs/dom_components/model/Symbols.js
  78. 14
      test/specs/editor/index.js
  79. 3
      test/specs/keymaps/index.js
  80. 252
      test/specs/pages/index.js
  81. 228
      test/specs/utils/Sorter.js

10
.github/ISSUE_TEMPLATE/01_bug_report.md

@ -19,8 +19,8 @@ READ and FOLLOW next 3 steps, then REMOVE them before posting the issue
**Are you able to reproduce the bug from the demo?** **Are you able to reproduce the bug from the demo?**
[ ] Yes - [ ] Yes
[ ] No - [ ] No
**What is the expected behavior?** **What is the expected behavior?**
@ -28,9 +28,9 @@ READ and FOLLOW next 3 steps, then REMOVE them before posting the issue
**What is the current behavior?** **What is the current behavior?**
*Descripe the bug **detailed*** *Describe the bug **detailed***
**Are you able to attach screenshots, screencasts or a live demo?** **Are you able to attach screenshots, screencasts or a live demo?**
[ ] Yes (attach) - [ ] Yes (attach)
[ ] No - [ ] No

8
.github/ISSUE_TEMPLATE/02_feature_request.md

@ -19,10 +19,10 @@ READ and FOLLOW next 3 steps, then REMOVE them before posting the issue
**Is there an alternative at the latest version?** **Is there an alternative at the latest version?**
[ ] Yes (descripe the alternative) - [ ] Yes (describe the alternative)
[ ] No - [ ] No
**Is this related to an issue?** **Is this related to an issue?**
[ ] Yes (Give a link to the issue) - [ ] Yes (Give a link to the issue)
[ ] No - [ ] No

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

44
package-lock.json

@ -1,6 +1,6 @@
{ {
"name": "grapesjs", "name": "grapesjs",
"version": "0.16.41", "version": "0.17.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -8517,9 +8517,9 @@
} }
}, },
"hosted-git-info": { "hosted-git-info": {
"version": "2.8.8", "version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true "dev": true
}, },
"hpack.js": { "hpack.js": {
@ -11227,9 +11227,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.20", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true "dev": true
}, },
"lodash._reinterpolate": { "lodash._reinterpolate": {
@ -13949,9 +13949,9 @@
"dev": true "dev": true
}, },
"prismjs": { "prismjs": {
"version": "1.22.0", "version": "1.23.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.22.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz",
"integrity": "sha512-lLJ/Wt9yy0AiSYBf212kK3mM5L8ycwlyTlSxHBAneXLR0nzFMlZ5y7riFPF3E33zXOF2IH95xdY5jIyZbM9z/w==", "integrity": "sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==",
"dev": true, "dev": true,
"requires": { "requires": {
"clipboard": "^2.0.0" "clipboard": "^2.0.0"
@ -15691,9 +15691,9 @@
} }
}, },
"ssri": { "ssri": {
"version": "6.0.1", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"figgy-pudding": "^3.5.1" "figgy-pudding": "^3.5.1"
@ -16775,9 +16775,9 @@
"dev": true "dev": true
}, },
"underscore": { "underscore": {
"version": "1.12.0", "version": "1.12.1",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.0.tgz", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz",
"integrity": "sha512-21rQzss/XPMjolTiIezSu3JAjgagXKROtNrYFEOWK109qY1Uv2tVjPTZ1ci2HgvQDA16gHYSthQIJfB+XId/rQ==" "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw=="
}, },
"unherit": { "unherit": {
"version": "1.1.3", "version": "1.1.3",
@ -17152,9 +17152,9 @@
} }
}, },
"url-parse": { "url-parse": {
"version": "1.4.7", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"querystringify": "^2.1.1", "querystringify": "^2.1.1",
@ -18570,9 +18570,9 @@
"dev": true "dev": true
}, },
"y18n": { "y18n": {
"version": "3.2.1", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==",
"dev": true "dev": true
}, },
"yallist": { "yallist": {

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.41", "version": "0.17.4",
"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:*",

14
src/block_manager/model/Block.js

@ -1,5 +1,4 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import Category from './Category';
export default Backbone.Model.extend({ export default Backbone.Model.extend({
defaults: { defaults: {
@ -18,18 +17,5 @@ export default Backbone.Model.extend({
content: '', content: '',
category: '', category: '',
attributes: {} attributes: {}
},
initialize(opts = {}) {
let category = this.get('category');
if (category) {
if (typeof category == 'string') {
var catObj = new Category({
id: category,
label: category
});
}
}
} }
}); });

3
src/block_manager/view/BlocksView.js

@ -139,7 +139,7 @@ export default Backbone.View.extend({
var catId = catModel.get('id'); var catId = catModel.get('id');
var catView = this.renderedCategories[catId]; var catView = this.renderedCategories[catId];
var categories = this.getCategoriesEl(); var categories = this.getCategoriesEl();
model.set('category', catModel); model.set('category', catModel, { silent: true });
if (!catView && categories) { if (!catView && categories) {
catView = new CategoryView( catView = new CategoryView(
@ -200,6 +200,7 @@ export default Backbone.View.extend({
this.append(frag); this.append(frag);
const cls = `${this.blockContClass}s ${ppfx}one-bg ${ppfx}two-color`; const cls = `${this.blockContClass}s ${ppfx}one-bg ${ppfx}two-color`;
this.$el.addClass(cls); this.$el.addClass(cls);
this.rendered = true;
return this; return this;
} }
}); });

11
src/block_manager/view/CategoryView.js

@ -12,6 +12,10 @@ export default Backbone.View.extend({
events: {}, events: {},
attributes() {
return this.model.get('attributes');
},
initialize(o = {}, config = {}) { initialize(o = {}, config = {}) {
this.config = config; this.config = config;
const pfx = config.pStylePrefix || ''; const pfx = config.pStylePrefix || '';
@ -25,6 +29,7 @@ export default Backbone.View.extend({
this.events[`click .${pfx}title`] = 'toggle'; this.events[`click .${pfx}title`] = 'toggle';
this.listenTo(this.model, 'change:open', this.updateVisibility); this.listenTo(this.model, 'change:open', this.updateVisibility);
this.delegateEvents(); this.delegateEvents();
this.model.view = this;
}, },
updateVisibility() { updateVisibility() {
@ -33,13 +38,13 @@ export default Backbone.View.extend({
}, },
open() { open() {
this.el.className = `${this.className} ${this.activeClass}`; this.$el.addClass(this.activeClass);
this.getIconEl().className = `${this.iconClass} ${this.caretD}`; this.getIconEl().className = `${this.iconClass} ${this.caretD}`;
this.getBlocksEl().style.display = ''; this.getBlocksEl().style.display = '';
}, },
close() { close() {
this.el.className = this.className; this.$el.removeClass(this.activeClass);
this.getIconEl().className = `${this.iconClass} ${this.caretR}`; this.getIconEl().className = `${this.iconClass} ${this.caretR}`;
this.getBlocksEl().style.display = 'none'; this.getBlocksEl().style.display = 'none';
}, },
@ -77,7 +82,7 @@ export default Backbone.View.extend({
pfx: this.pfx, pfx: this.pfx,
label label
}); });
el.className = this.className; $el.addClass(this.className);
$el.css({ order: model.get('order') }); $el.css({ order: model.get('order') });
this.updateVisibility(); this.updateVisibility();

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).

57
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);
}, },
@ -677,7 +646,7 @@ export default () => {
destroy() { destroy() {
canvas.stopListening(); canvas.stopListening();
CanvasView.remove(); CanvasView && CanvasView.remove();
[c, canvas, CanvasView].forEach(i => (i = {})); [c, canvas, CanvasView].forEach(i => (i = {}));
['em', 'model', 'droppable'].forEach(i => (this[i] = {})); ['em', 'model', 'droppable'].forEach(i => (this[i] = {}));
} }

34
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() {
@ -35,7 +47,7 @@ export default Backbone.Model.extend({
if (model && device) { if (model && device) {
const { width, height } = device.attributes; const { width, height } = device.attributes;
model.set({ width, height }); model.set({ width, height }, { noUndo: 1 });
} }
}, },

149
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() {
@ -50,7 +78,8 @@ export default Backbone.Model.extend({
}, },
getHead() { getHead() {
return [...this.get('head')]; const head = this.get('head') || [];
return [...head];
}, },
setHead(value) { setHead(value) {
@ -113,7 +142,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 });
} }
}); });

72
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 = {}) {
@ -63,9 +75,9 @@ export default Backbone.View.extend({
}, },
remove() { remove() {
this.frames.remove();
const frm = this.model.get('frames'); const frm = this.model.get('frames');
frm.remove(frm.models); frm.remove(frm.models);
this.frames.remove();
this.frames = {}; this.frames = {};
Backbone.View.prototype.remove.apply(this, arguments); Backbone.View.prototype.remove.apply(this, arguments);
this.toggleListeners(); this.toggleListeners();
@ -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);
}, },
@ -299,7 +312,7 @@ export default Backbone.View.extend({
const scriptFn = model.getScriptString(); const scriptFn = model.getScriptString();
const scriptFnStr = model.get('script-props') const scriptFnStr = model.get('script-props')
? scriptFn ? scriptFn
: `function(){${scriptFn};}`; : `function(){\n${scriptFn}\n;}`;
const scriptProps = JSON.stringify(model.__getScriptProps()); const scriptProps = JSON.stringify(model.__getScriptProps());
script.innerHTML = ` script.innerHTML = `
setTimeout(function() { setTimeout(function() {
@ -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/CodeMirrorEditor.js

@ -6,6 +6,8 @@ import 'codemirror/mode/css/css';
import 'codemirror-formatting'; import 'codemirror-formatting';
export default Backbone.Model.extend({ export default Backbone.Model.extend({
CodeMirror,
defaults: { defaults: {
input: '', input: '',
label: '', label: '',

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);

2
src/code_manager/model/JsGenerator.js

@ -69,7 +69,7 @@ export default Backbone.Model.extend({
code += ` code += `
var items = document.querySelectorAll('${ids}'); var items = document.querySelectorAll('${ids}');
for (var i = 0, len = items.length; i < len; i++) { for (var i = 0, len = items.length; i < len; i++) {
(function(){${mapType.code}}.bind(items[i]))(); (function(){\n${mapType.code}\n}.bind(items[i]))();
}`; }`;
} }
} }

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}

44
src/commands/view/SelectComponent.js

@ -84,11 +84,11 @@ export default {
methods[method](body, 'mouseover', this.onHover); methods[method](body, 'mouseover', this.onHover);
methods[method](body, 'mouseleave', this.onOut); methods[method](body, 'mouseleave', this.onOut);
methods[method](body, 'click touchend', this.onClick); methods[method](body, 'click touchend', this.onClick);
methods[method](win, 'scroll', this.onFrameScroll); methods[method](win, 'scroll', this.onFrameScroll, true);
}; };
methods[method](window, 'resize', this.onFrameUpdated); methods[method](window, 'resize', this.onFrameUpdated);
methods[method](listenToEl, 'scroll', this.onContainerChange); methods[method](listenToEl, 'scroll', this.onContainerChange);
em[method]('component:toggled component:remove', this.onSelect, this); em[method]('component:toggled component:update', this.onSelect, this);
em[method]('change:componentHovered', this.onHovered, this); em[method]('change:componentHovered', this.onHovered, this);
em[method]( em[method](
'component:resize component:styleUpdate component:input', 'component:resize component:styleUpdate component:input',
@ -192,7 +192,7 @@ export default {
this.elSelected = result; this.elSelected = result;
this.updateToolsGlobal(); this.updateToolsGlobal();
// This will hide some elements from the select component // This will hide some elements from the select component
this.updateToolsLocal(result); this.updateLocalPos(result);
this.initResize(component); this.initResize(component);
}), }),
@ -203,11 +203,11 @@ export default {
this.updateToolsGlobal(); this.updateToolsGlobal();
}, },
updateLocalPos() { updateLocalPos(data) {
const sel = this.getElHovered(); const sel = this.getElHovered();
if (!sel.el) return; if (!sel.el) return;
sel.pos = this.getElementPos(sel.el); sel.pos = this.getElementPos(sel.el);
this.updateToolsLocal(); this.updateToolsLocal(data);
}, },
getElHovered() { getElHovered() {
@ -494,7 +494,7 @@ export default {
}; };
if (typeof resizable == 'object') { if (typeof resizable == 'object') {
options = { ...options, ...resizable }; options = { ...options, ...resizable, parent: options };
} }
this.resizer = editor.runCommand('resize', { el, options, force: 1 }); this.resizer = editor.runCommand('resize', { el, options, force: 1 });
@ -576,7 +576,7 @@ export default {
}, },
updateTools() { updateTools() {
this.updateToolsLocal(); this.updateLocalPos();
this.updateGlobalPos(); this.updateGlobalPos();
}, },
@ -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`);
} }
}; };

3
src/css_composer/config/config.js

@ -2,9 +2,6 @@ export default {
// Style prefix // Style prefix
stylePrefix: 'css-', stylePrefix: 'css-',
// Custom CSS string to render on top
staticRules: '',
// Default CSS style // Default CSS style
rules: [] rules: []
}; };

55
src/css_composer/index.js

@ -27,7 +27,7 @@
* @module CssComposer * @module CssComposer
*/ */
import { isArray } from 'underscore'; import { isArray, isUndefined } from 'underscore';
import defaults from './config/config'; import defaults from './config/config';
import CssRule from './model/CssRule'; import CssRule from './model/CssRule';
import CssRules from './model/CssRules'; import CssRules from './model/CssRules';
@ -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);
@ -475,6 +457,21 @@ export default () => {
return selector && this.get(selector, state, media); return selector && this.get(selector, state, media);
}, },
getComponentRules(cmp, opts = {}) {
let { state, mediaText, current } = opts;
if (current) {
state = em.get('state') || '';
mediaText = em.getCurrentMedia();
}
const id = cmp.getId();
const rules = this.getAll().filter(r => {
if (!isUndefined(state) && r.get('state') !== state) return;
if (!isUndefined(mediaText) && r.get('mediaText') !== mediaText) return;
return r.getSelectorsString() === `#${id}`;
});
return rules;
},
/** /**
* Render the block of CSS rules * Render the block of CSS rules
* @return {HTMLElement} * @return {HTMLElement}

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;
} }
}); });

22
src/dom_components/config/config.js

@ -1,28 +1,6 @@
export default { export default {
stylePrefix: 'comp-', stylePrefix: 'comp-',
wrapperId: 'wrapper',
wrapperName: 'Body',
// Default wrapper configuration
wrapper: {
removable: false,
copyable: false,
draggable: false,
components: [],
traits: [],
stylable: [
'background',
'background-color',
'background-image',
'background-repeat',
'background-attachment',
'background-position',
'background-size'
]
},
// Could be used for default components // Could be used for default components
components: [], components: [],

123
src/dom_components/index.js

@ -244,50 +244,17 @@ export default () => {
const selected = em.get('selected'); const selected = em.get('selected');
em.listenTo(selected, 'add', (sel, c, opts) => em.listenTo(selected, 'add', (sel, c, opts) =>
this.selectAdd(sel, opts) this.selectAdd(selected.getComponent(sel), opts)
); );
em.listenTo(selected, 'remove', (sel, c, opts) => em.listenTo(selected, 'remove', (sel, c, opts) =>
this.selectRemove(sel, opts) this.selectRemove(selected.getComponent(sel), opts)
); );
} }
// 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 = {};
} }

307
src/dom_components/model/Component.js

@ -29,7 +29,9 @@ const avoidInline = em => em && em.getConfig('avoidInlineStyle');
export const eventDrag = 'component:drag'; 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 keySymbolOvrd = '__symbol_ovrd';
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
@ -56,8 +58,8 @@ export const keySymbol2w = '__symbol2w';
* You can also specify a query string to indentify elements, * You can also specify a query string to indentify elements,
* eg. `'.some-class[title=Hello], [data-gjs-type=column]'` means you can drag the component only inside elements * eg. `'.some-class[title=Hello], [data-gjs-type=column]'` means you can drag the component only inside elements
* containing `some-class` class and `Hello` title, and `column` components. Default: `true` * containing `some-class` class and `Hello` title, and `column` components. Default: `true`
* @property {Boolean|String} [droppable=true] Indicates if it's possible to drop other components inside. You can use * @property {Boolean|String|Function} [droppable=true] Indicates if it's possible to drop other components inside. You can use
* a query string as with `draggable`. Default: `true` * a query string as with `draggable`. In the case of a function, target and destination components are passed as arguments, return a Boolean to indicate if the drop is possible. Default: `true`
* @property {Boolean} [badgable=true] Set to false if you don't want to see the badge (with the name) over the component. Default: `true` * @property {Boolean} [badgable=true] Set to false if you don't want to see the badge (with the name) over the component. Default: `true`
* @property {Boolean|Array<String>} [stylable=true] True if it's possible to style the component. * @property {Boolean|Array<String>} [stylable=true] True if it's possible to style the component.
* You can also indicate an array of CSS properties which is possible to style, eg. `['color', 'width']`, all other properties * You can also indicate an array of CSS properties which is possible to style, eg. `['color', 'width']`, all other properties
@ -122,7 +124,12 @@ 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,
[keySymbolOvrd]: 0,
_undo: true,
_undoexc: ['status', 'open']
}, },
/** /**
@ -193,6 +200,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 +214,60 @@ const Component = Backbone.Model.extend(Styleable).extend(
}); });
if (!opt.temporary) { if (!opt.temporary) {
this.__postAdd();
this.init(); this.init();
this.__isSymbol() && 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');
}, },
@ -619,34 +676,99 @@ const Component = Backbone.Model.extend(Styleable).extend(
return isArray(this.get(keySymbols)); return isArray(this.get(keySymbols));
}, },
__isSymbolOrInst() {
return !!(this.__isSymbol() || this.get(keySymbol));
},
__isSymbolTop() { __isSymbolTop() {
const parent = this.parent(); const parent = this.parent();
const symb = this.__isSymbolOrInst();
return ( return (
!parent || (parent && !parent.__isSymbol() && !parent.__getSymbol()) symb &&
(!parent || (parent && !parent.__isSymbol() && !parent.__getSymbol()))
); );
}, },
__isSymbolNested() {
if (!this.__isSymbolOrInst() || this.__isSymbolTop()) return false;
const symbTopSelf = (this.__isSymbol()
? this
: this.__getSymbol()
).__getSymbTop();
const symbTop = this.__getSymbTop();
const symbTopMain = symbTop.__isSymbol()
? symbTop
: symbTop.__getSymbol();
return symbTopMain !== symbTopSelf;
},
__getAllById() {
const { em } = this;
return em ? em.get('DomComponents').allById() : {};
},
__getSymbol() { __getSymbol() {
return this.get(keySymbol); let symb = this.get(keySymbol);
if (symb && isString(symb)) {
const ref = this.__getAllById()[symb];
if (ref) {
symb = ref;
this.set(keySymbol, ref);
} else {
symb = 0;
}
}
return symb;
}, },
__getSymbols() { __getSymbols() {
return this.get(keySymbols); let symbs = this.get(keySymbols);
if (symbs && isArray(symbs)) {
symbs.forEach((symb, idx) => {
if (symb && isString(symb)) {
symbs[idx] = this.__getAllById()[symb];
}
});
symbs = symbs.filter(symb => symb && !isString(symb));
}
return symbs;
},
__isSymbOvrd(prop = '') {
const ovrd = this.get(keySymbolOvrd);
const [prp] = prop.split(':');
const props = prop !== prp ? [prop, prp] : [prop];
return (
ovrd === true ||
(isArray(ovrd) && props.some(p => ovrd.indexOf(p) >= 0))
);
}, },
__getSymbToUp(opts = {}) { __getSymbToUp(opts = {}) {
const { fromInstance } = opts; let result = [];
const symbols = this.get(keySymbols) || []; const { em } = this;
const symbol = this.__getSymbol(); const { changed } = opts;
let result = const symbEnabled = em && em.get('symbols');
symbol && !fromInstance
? [symbol]
: symbols.filter(md => md.collection || md.prevColl);
if (fromInstance) { if (
result = result.filter(i => i !== fromInstance); opts.fromInstance ||
opts.noPropagate ||
opts.fromUndo ||
!symbEnabled ||
// Avoid updating others if the current component has override
(changed && this.__isSymbOvrd(changed))
) {
return result;
} }
const symbols = this.__getSymbols() || [];
const symbol = this.__getSymbol();
const all = symbol ? [symbol, ...(symbol.__getSymbols() || [])] : symbols;
result = all
.filter(s => s !== this)
// Avoid updating those with override
.filter(s => !(changed && s.__isSymbOvrd(changed)));
return result; return result;
}, },
@ -669,44 +791,64 @@ const Component = Backbone.Model.extend(Styleable).extend(
delete changed.open; delete changed.open;
delete changed[keySymbols]; delete changed[keySymbols];
delete changed[keySymbol]; delete changed[keySymbol];
delete changed[keySymbolOvrd];
delete changed.attributes; delete changed.attributes;
delete attrs.id; delete attrs.id;
if (!isEmptyObj(attrs)) changed.attributes = attrs; if (!isEmptyObj(attrs)) changed.attributes = attrs;
if (!isEmptyObj(changed)) { if (!isEmptyObj(changed)) {
const toUp = this.__getSymbToUp(opts); const toUp = this.__getSymbToUp(opts);
// Avoid propagating overrides to other symbols
keys(changed).map(prop => {
if (this.__isSymbOvrd(prop)) delete changed[prop];
});
this.__logSymbol('props', toUp, { opts, changed }); this.__logSymbol('props', toUp, { opts, changed });
toUp.forEach(child => toUp.forEach(child => {
child.set(changed, { fromInstance: this, ...opts }) const propsChanged = { ...changed };
); // Avoid updating those with override
keys(propsChanged).map(prop => {
if (child.__isSymbOvrd(prop)) delete propsChanged[prop];
});
child.set(propsChanged, { fromInstance: this, ...opts });
});
} }
}, },
__upSymbCls(m, c, opts = {}) { __upSymbCls(m, c, opts = {}) {
const toUp = this.__getSymbToUp(); const toUp = this.__getSymbToUp(opts);
this.__logSymbol('classes', toUp, { opts }); this.__logSymbol('classes', toUp, { opts });
toUp.forEach(child => { toUp.forEach(child => {
// 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 toUpOpts = { fromInstance }; const { fromInstance, fromUndo } = optUp;
const toUpOpts = { fromInstance, fromUndo };
const isTemp = m.opt.temporary; const isTemp = m.opt.temporary;
// Reset
if (!o) { if (!o) {
const toUp = this.__getSymbToUp(toUpOpts); const toUp = this.__getSymbToUp({
this.__logSymbol('reset', toUp); ...toUpOpts,
changed: 'components:reset'
});
this.__logSymbol('reset', toUp, { components: m.models });
toUp.forEach(symb => { toUp.forEach(symb => {
const newMods = m.models.map(mod => mod.clone({ symbol: 1 })); const newMods = m.models.map(mod => mod.clone({ symbol: 1 }));
symb.components().reset(newMods, { fromInstance: this, ...c }); symb.components().reset(newMods, { fromInstance: this, ...c });
}); });
} else if (o.add) {
// Add // Add
} else if (o.add) {
let addedInstances = []; let addedInstances = [];
const isMainSymb = !!this.__getSymbols(); const isMainSymb = !!this.__getSymbols();
const toUp = this.__getSymbToUp(toUpOpts); const toUp = this.__getSymbToUp({
...toUpOpts,
changed: 'components:add'
});
if (toUp.length) { if (toUp.length) {
const addSymb = m.__getSymbol(); const addSymb = m.__getSymbol();
addedInstances = addedInstances =
@ -732,33 +874,52 @@ const Component = Backbone.Model.extend(Styleable).extend(
symbPrev || m.clone({ symbol: 1, symbolInv: isMainSymb }); symbPrev || m.clone({ symbol: 1, symbolInv: isMainSymb });
symb.append(toAppend, { fromInstance: this, ...o }); symb.append(toAppend, { fromInstance: this, ...o });
}); });
// Remove
} else { } else {
// Allow removing single instances // Remove instance reference from the symbol
const symb = m.__getSymbol();
symb &&
!o.temporary &&
symb.set(
keySymbols,
symb.__getSymbols().filter(i => i !== m)
);
// Propagate remove only if the component is an inner symbol
if (!m.__isSymbolTop()) { if (!m.__isSymbolTop()) {
const toUp = m.__getSymbToUp(toUpOpts); const changed = 'components:remove';
const { index } = o;
const parent = m.parent();
const opts = { fromInstance: m, ...o };
const isSymbNested = m.__isSymbolNested();
let toUpFn = symb => {
const symbPrnt = symb.parent();
symbPrnt && !symbPrnt.__isSymbOvrd(changed) && symb.remove(opts);
};
// Check if the parent allows the removing
let toUp = !parent.__isSymbOvrd(changed)
? m.__getSymbToUp(toUpOpts)
: [];
if (isSymbNested) {
toUp = parent.__getSymbToUp({ ...toUpOpts, changed });
toUpFn = symb => {
const toRemove = symb.components().at(index);
toRemove && toRemove.remove({ fromInstance: parent, ...opts });
};
}
!isTemp && !isTemp &&
this.__logSymbol('remove', toUp, { opts: o, removed: m.cid }); this.__logSymbol('remove', toUp, {
toUp.forEach(symb => { opts: o,
const opts = { fromInstance: m, ...o }; removed: m.cid,
// In case of nested symbols, I only need to propagate changes to its instances isSymbNested
if (symb.__isSymbolTop() && symb.__getSymbols()) { });
const toUpInst = symb.__getSymbToUp({ toUp.forEach(toUpFn);
fromInstance: m,
...toUpOpts
});
this.__logSymbol('remove-inst', toUpInst, {
opts: o,
symbol: symb
});
toUpInst.forEach(inst => {
inst.remove(opts);
});
} else {
symb.remove(opts);
}
});
} }
} }
this.__changesUp(optUp);
}, },
initClasses(m, c, opts = {}) { initClasses(m, c, opts = {}) {
@ -789,6 +950,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
@ -1136,22 +1298,22 @@ const Component = Backbone.Model.extend(Styleable).extend(
// Symbols // Symbols
// If I clone an inner symbol, I have to reset it // If I clone an inner symbol, I have to reset it
cloned.unset(keySymbols); cloned.set(keySymbols, 0);
const symbol = this.__getSymbol(); const symbol = this.__getSymbol();
const symbols = this.__getSymbols(); const symbols = this.__getSymbols();
if (!opt.symbol && (symbol || symbols)) { if (!opt.symbol && (symbol || symbols)) {
cloned.unset(keySymbol); cloned.set(keySymbol, 0);
cloned.unset(keySymbols); cloned.set(keySymbols, 0);
} 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) {
@ -1165,7 +1327,6 @@ const Component = Backbone.Model.extend(Styleable).extend(
[this, cloned].map(i => i.__initSymb()); [this, cloned].map(i => i.__initSymb());
this.set(keySymbol, cloned); this.set(keySymbol, cloned);
} }
// opt.symbol2w && cloned.set(keySymbol2w, 1);
} }
const event = 'component:clone'; const event = 'component:clone';
@ -1309,12 +1470,14 @@ 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]) { const symbol = obj[keySymbol];
obj[keySymbols] = this.__getSymbToUp().map(i => i.getId()); const symbols = obj[keySymbols];
if (symbols && isArray(symbols)) {
obj[keySymbols] = symbols.map(i => (i.getId ? i.getId() : i));
} }
if (obj[keySymbol]) { if (symbol && !isString(symbol)) {
obj[keySymbol] = obj[keySymbol].getId(); obj[keySymbol] = symbol.getId();
} }
} }
@ -1464,17 +1627,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] || {}
});
}, },
/** /**
@ -1501,7 +1671,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)
@ -1667,8 +1840,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;

63
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,32 +252,35 @@ export default Backbone.Collection.extend({
model.addClass(name); model.addClass(name);
} }
model.__postAdd({ recursive: 1 });
this.__onAddEnd(); this.__onAddEnd();
}, },
__onAddEnd: debounce(function() { __onAddEnd: debounce(function() {
const { domc } = this; // TODO to check symbols on load, probably this might be removed as symbols
const allComp = (domc && domc.allById()) || {}; // are always recovered from the model
const firstAdd = this.__firstAdd; // const { domc } = this;
const toCheck = isArray(firstAdd) ? firstAdd : [firstAdd]; // const allComp = (domc && domc.allById()) || {};
const silent = { silent: true }; // const firstAdd = this.__firstAdd;
const onAll = comps => { // const toCheck = isArray(firstAdd) ? firstAdd : [firstAdd];
comps.forEach(comp => { // const silent = { silent: true };
const symbol = comp.get(keySymbols); // const onAll = comps => {
const symbolOf = comp.get(keySymbol); // comps.forEach(comp => {
if (symbol && isArray(symbol) && isString(symbol[0])) { // const symbol = comp.get(keySymbols);
comp.set( // const symbolOf = comp.get(keySymbol);
keySymbols, // if (symbol && isArray(symbol) && isString(symbol[0])) {
symbol.map(smb => allComp[smb]).filter(i => i), // comp.set(
silent // keySymbols,
); // symbol.map(smb => allComp[smb]).filter(i => i),
} // silent
if (isString(symbolOf)) { // );
comp.set(keySymbol, allComp[symbolOf], silent); // }
} // if (isString(symbolOf)) {
onAll(comp.components()); // comp.set(keySymbol, allComp[symbolOf], silent);
}); // }
}; // onAll(comp.components());
onAll(toCheck); // });
// };
// onAll(toCheck);
}) })
}); });

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);

58
src/editor/model/Editor.js

@ -11,6 +11,7 @@ import $ from 'cash-dom';
import Backbone from 'backbone'; import Backbone from 'backbone';
import Extender from 'utils/extender'; import Extender from 'utils/extender';
import { getModel } from 'utils/mixins'; import { getModel } from 'utils/mixins';
import Selected from './Selected';
Backbone.$ = $; Backbone.$ = $;
const deps = [ const deps = [
@ -28,6 +29,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'),
@ -57,7 +59,7 @@ export default Backbone.Model.extend({
defaults() { defaults() {
return { return {
editing: 0, editing: 0,
selected: new Collection(), selected: 0,
clipboard: null, clipboard: null,
dmode: 0, dmode: 0,
componentHovered: null, componentHovered: null,
@ -77,8 +79,9 @@ export default Backbone.Model.extend({
this.set('modules', []); this.set('modules', []);
this.set('toLoad', []); this.set('toLoad', []);
this.set('storables', []); this.set('storables', []);
this.set('selected', new Collection()); this.set('selected', new Selected());
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 +99,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 +117,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 +164,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 +209,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 || '';
@ -253,10 +268,16 @@ export default Backbone.Model.extend({
timedInterval && clearTimeout(timedInterval); timedInterval && clearTimeout(timedInterval);
timedInterval = setTimeout(() => { timedInterval = setTimeout(() => {
this.set('changesCount', this.get('changesCount') + 1, opt); const curr = this.get('changesCount') || 0;
const { unset, ...opts } = opt;
this.set('changesCount', curr + 1, opts);
}, 0); }, 0);
}, },
changesUp(opts) {
this.handleUpdates(0, 0, opts);
},
/** /**
* Callback on component hover * Callback on component hover
* @param {Object} Model * @param {Object} Model
@ -276,7 +297,7 @@ export default Backbone.Model.extend({
* @private * @private
*/ */
getSelected() { getSelected() {
return this.get('selected').last(); return this.get('selected').lastComponent();
}, },
/** /**
@ -285,8 +306,7 @@ export default Backbone.Model.extend({
* @private * @private
*/ */
getSelectedAll() { getSelectedAll() {
const sel = this.get('selected'); return this.get('selected').allComponents();
return (sel && sel.models) || [];
}, },
/** /**
@ -300,8 +320,8 @@ export default Backbone.Model.extend({
const ctrlKey = event && (event.ctrlKey || event.metaKey); const ctrlKey = event && (event.ctrlKey || event.metaKey);
const { shiftKey } = event || {}; const { shiftKey } = event || {};
const multiple = isArray(el); const multiple = isArray(el);
const els = multiple ? el : [el]; const els = (multiple ? el : [el]).map(el => getModel(el, $));
const selected = this.get('selected'); const selected = this.getSelectedAll();
const mltSel = this.getConfig('multipleSelection'); const mltSel = this.getConfig('multipleSelection');
let added; let added;
@ -373,8 +393,8 @@ export default Backbone.Model.extend({
models.forEach(model => { models.forEach(model => {
if (model && !model.get('selectable')) return; if (model && !model.get('selectable')) return;
const selected = this.get('selected'); const selected = this.get('selected');
opts.forceChange && selected.remove(model, opts); opts.forceChange && this.removeSelected(model, opts);
selected.push(model, opts); selected.addComponent(model, opts);
}); });
}, },
@ -385,7 +405,7 @@ export default Backbone.Model.extend({
* @private * @private
*/ */
removeSelected(el, opts = {}) { removeSelected(el, opts = {}) {
this.get('selected').remove(getModel(el, $), opts); this.get('selected').removeComponent(getModel(el, $), opts);
}, },
/** /**
@ -399,7 +419,7 @@ export default Backbone.Model.extend({
const models = isArray(model) ? model : [model]; const models = isArray(model) ? model : [model];
models.forEach(model => { models.forEach(model => {
if (this.get('selected').contains(model)) { if (this.get('selected').hasComponent(model)) {
this.removeSelected(model, opts); this.removeSelected(model, opts);
} else { } else {
this.addSelected(model, opts); this.addSelected(model, opts);
@ -502,8 +522,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 +549,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 +568,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 +593,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);
}); });

44
src/editor/model/Selected.js

@ -0,0 +1,44 @@
import { Collection, Model } from 'backbone';
import { isArray } from 'underscore';
export class Selectable extends Model {}
export default class Selected extends Collection {
getByComponent(component) {
return this.filter(s => this.getComponent(s) === component)[0];
}
addComponent(component, opts) {
const toAdd = (isArray(component) ? component : [component])
.filter(c => !this.hasComponent(c))
.map(component => ({ component }));
return this.push(toAdd, opts);
}
getComponent(model) {
return model.get('component');
}
hasComponent(component) {
const model = this.getByComponent(component);
return model && this.contains(model);
}
lastComponent() {
const last = this.last();
return last && this.getComponent(last);
}
allComponents() {
return this.map(s => this.getComponent(s)).filter(i => i);
}
removeComponent(component, opts) {
const toRemove = (isArray(component) ? component : [component]).map(c =>
this.getByComponent(c)
);
return this.remove(toRemove, opts);
}
}
Selected.prototype.model = Selectable;

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/i18n/locale/de.js

@ -72,7 +72,7 @@ export default {
styleManager: { styleManager: {
empty: empty:
"Wählen Sie ein Element aus bevor Sie den Stil Manager nutzen", "Wählen Sie ein Element aus bevor Sie den Stil Manager nutzen",
layer: 'Evene', layer: 'Ebene',
fileButton: 'Bilder', fileButton: 'Bilder',
sectors: { sectors: {
general: 'Allgemein', general: 'Allgemein',

117
src/i18n/locale/el.js

@ -0,0 +1,117 @@
const traitInputAttr = { placeholder: 'eg. Text here' };
export default {
assetManager: {
addButton: 'Προσθήκη Εικόνας',
inputPlh: 'http://διαδρομή/μέχρι/την/εικόνα.jpg',
modalTitle: 'Επιλογή Εικόνας',
uploadTitle: 'Αφήστε τα αρχεία εδώ ή κάντε κλικ για ανέβασμα'
},
// Εδώ υπάρχει απλά αναφορά, ο πυρήνας του GrapesJS δεν διαθέτει κανένα πλαίσιο,
// οπότε αυτό θα πρέπει να αγνοηθεί από τα υπόλοιπα αρχεία μετάφρασης
blockManager: {
labels: {
// 'block-id': 'Ετικέτα Πλαισίου',
},
categories: {
// 'category-id': 'Ετικέτα Κατηγορίας',
}
},
domComponents: {
names: {
'': 'Κουτί',
wrapper: 'Κορμός',
text: 'Κείμενο',
comment: 'Σχόλιο',
image: 'Εικόνα',
video: 'Βίντεο',
label: 'Ετικέτα',
link: 'Σύνδεσμος',
map: 'Χάρτης',
tfoot: 'Υποσέλιδο πίνακα',
tbody: 'Κορμός πίνακα',
thead: 'Κεφαλίδα πίνακα',
table: 'Πίνακας',
row: 'Γραμμή πίνακα',
cell: 'Κελί πίνακα'
}
},
deviceManager: {
device: 'Συσκευή',
devices: {
desktop: 'Σθαθερός Υπολογιστής',
tablet: 'Τάμπλετ',
mobileLandscape: 'Κινητό Οριζόντια',
mobilePortrait: 'Κινητό Κάθετα'
}
},
panels: {
buttons: {
titles: {
preview: 'Προεπισκόπηση',
fullscreen: 'Πλήρη Οθόνη',
'sw-visibility': 'Προβολή συστατικών',
'export-template': 'Προβολή κώδικα',
'open-sm': 'Άνοιγμα Του Διαχειριστή Μορφοποίησης',
'open-tm': 'Ρυθμίσεις',
'open-layers': 'Άνοιγμα Του Διαχειριστή Επιπέδων',
'open-blocks': 'Άνοιγμα Πλαισίων'
}
}
},
selectorManager: {
label: 'Κλάσεις',
selected: 'Επιλεγμένο',
emptyState: '- Κατάσταση -',
states: {
hover: 'Αιώρηση',
active: 'Κλικ',
'nth-of-type(2n)': 'Μονές/Ζυγές'
}
},
styleManager: {
empty: 'Επιλέξτε ένα στοιχεία πριν χρησιμοποιήσετε τον διαχειριστή μορφοποίησης',
layer: 'Επίπεδο',
fileButton: 'Εικόνες',
sectors: {
general: 'Γενικά',
layout: 'Δομή',
typography: 'Τυπογραφία',
decorations: 'Μορφοποίηση',
extra: 'Επιπρόσθετα',
flex: 'Φλεξ',
dimension: 'Διάσταση'
},
// Η βασική βιβλιοθήκη παράγει το όνομα από την δικού του `ιδιότητα` name
properties: {
// float: 'Float',
}
},
traitManager: {
empty: 'Επιλέξτε ένα στοιχεία πριν χρησιμοποιήσετε τον Διαχειριστή Χαρακτηριστικών',
label: 'Ρυθμίσεις Συστατικού',
traits: {
// Η βασική βιβλιοθήκη παράγει το όνομα από την δικού του `ιδιότητα` name
labels: {
// id: 'Id',
// alt: 'Εναλλακτικό Κείμενο',
// title: 'Τίτλος',
// href: 'Href',
},
// Σε ένα απλό χαρακτηριστικό, όπως ένα πεδίο κείμενο, χρησιμοποιούνται στις ιδιότητες του πεδίου εισαγωγής
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'πχ. https://google.gr' }
},
// Σε χαρακτηριστικό όπως το select, αυτά χρησιμοποιούνται για την μετάφραση των ονομάτων των επιλογών
options: {
target: {
false: 'Στο ίδιο παράθυρο',
_blank: 'Σε νέο παράθυρο'
}
}
}
}
};

210
src/i18n/locale/se.js

@ -0,0 +1,210 @@
const traitInputAttr = { placeholder: 't.ex. Text här' };
export default {
assetManager: {
addButton: 'Lägg till bild',
inputPlh: 'http://adress/till/bilden.jpg',
modalTitle: 'Välj bild',
uploadTitle: 'Dra och släpp filer här eller klicka för att ladda upp'
},
// Here just as a reference, GrapesJS core doesn't contain any block,
// so this should be omitted from other local files
blockManager: {
labels: {
// 'block-id': 'Block Label',
},
categories: {
// 'category-id': 'Category Label',
}
},
domComponents: {
names: {
'': 'Box',
wrapper: 'Body',
text: 'Text',
comment: 'Kommentar',
image: 'Bild',
video: 'Video',
label: 'Etikett',
link: 'Länk',
map: 'Karta',
tfoot: 'Tabellfot',
tbody: 'Tabellinnehåll',
thead: 'Tabellhuvud',
table: 'Tabell',
row: 'Tabellrad',
cell: 'Tabellcell'
}
},
deviceManager: {
device: 'Enhet',
devices: {
desktop: 'Dator',
tablet: 'Surfplatta',
mobileLandscape: 'Mobil liggande',
mobilePortrait: 'Mobil stående'
}
},
panels: {
buttons: {
titles: {
preview: 'Förhandsgranska',
fullscreen: 'Helskärm',
'sw-visibility': 'Visa komponenter',
'export-template': 'Visa kod',
'open-sm': 'Öppna stilhanterare',
'open-tm': 'Inställningar',
'open-layers': 'Öppna lagerhanterare',
'open-blocks': 'Öppna block'
}
}
},
selectorManager: {
label: 'Klasser',
selected: 'Markerad',
emptyState: '- Status -',
states: {
hover: 'Mus över',
active: 'Klick',
'nth-of-type(2n)': 'Jämn/udda'
}
},
styleManager: {
empty: 'Markera ett element innan du använder stilhanteraren',
layer: 'Lager',
fileButton: 'Bilder',
sectors: {
general: 'Allmänt',
layout: 'Layout',
typography: 'Typografi',
decorations: 'Dekoration',
extra: 'Extra',
flex: 'Flex',
dimension: 'Dimension'
},
// The core library generates the name by their `property` name
properties: {
float: 'Justering',
display: 'Visning',
position: 'Position',
top: 'Topp',
right: 'Höger',
left: 'Vänster',
bottom: 'Botten',
width: 'Bredd',
height: 'Höjd',
'max-width': 'Maxbredd',
'max-height': 'Maxhöjd',
margin: 'Marginal',
'margin-top': 'Övre marginal',
'margin-right': 'Höger marginal',
'margin-left': 'Vänster marginal',
'margin-bottom': 'Undre marginal',
padding: 'Utfyllnad',
'padding-top': 'Övre utfyllnad',
'padding-left': 'Vänster utfyllnad',
'padding-right': 'Höger utfyllnad',
'padding-bottom': 'Nedre utfyllnad',
'font-family': 'Typsnitt',
'font-size': 'Teckenstorlek',
'font-weight': 'Texttyngd',
'letter-spacing': 'Textmellanrum',
color: 'Färg',
'line-height': 'Radhöjd',
'text-align': 'Textjustering',
'text-shadow': 'Textskugga',
'text-shadow-h': 'Horisontell textskugga',
'text-shadow-v': 'Vertikal textskugga',
'text-shadow-blur': 'Textskuggans luddighet',
'text-shadow-color': 'Textskuggans färg',
'border-top-left': 'Vänster övre kantlinje',
'border-top-right': 'Höger övre kantlinje',
'border-bottom-left': 'Vänster nedre kantlinje',
'border-bottom-right': 'Höger nedre kantlinje',
'border-left': 'Vänster kantlinje',
'border-top': 'Övre kantlinje',
'border-right': 'Höger kantlinje',
'border-bottom': 'Nedre kantlinje',
'border-radius-top-left': 'Vänster övre hörnradie',
'border-radius-top-right': 'Höger övre hörnradie',
'border-radius-bottom-left': 'Vänster nedre hörnradie',
'border-radius-bottom-right': 'Höger nedre hörnradie',
'border-radius': 'Hörnradie',
border: 'Kantlinje',
'border-width': 'Kantbredd',
'border-style': 'Kantstil',
'border-color': 'Kantfärg',
'box-shadow': 'Skugga',
'box-shadow-h': 'Horisontell skugga',
'box-shadow-v': 'Vertikal skugga',
'box-shadow-blur': 'Skuggans luddighet',
'box-shadow-spread': "Skuggans spridning",
'box-shadow-color': "Skuggans färg",
'box-shadow-type': "Typ av skugga",
background: 'Bakgrund',
'background-image': 'Bakgrundsbild',
'background-repeat': 'Upprepa bakgrund',
'background-position': 'Bakgrundsplacering',
'background-attachment': 'Bakgrundslåsning',
'background-size': 'Bakgrundsstorlek',
'background-color': 'Bakgrundsfärg',
transition: 'Övergång',
'transition-property': 'Övergångstyp',
'transition-duration': 'Övergångens varaktighet',
'transition-timing-function': 'Övergångens tidsfunktion',
perspective: 'Perspektiv',
transform: 'Transformera',
'transform-rotate-x': 'Rotera längs X-axeln',
'transform-rotate-y': 'Rotera längs Y-axeln',
'transform-rotate-z': 'Rotera längs Z-axeln',
'transform-scale-x': 'Skala längs X-axeln',
'transform-scale-y': 'Skala längs Y-axeln',
'transform-scale-z': 'Skala längs Z-axeln',
'flex-direction': 'Flexriktning',
'flex-wrap': 'Flexbrytning',
'justify-content': 'Justera innehåll',
'align-items': 'Elementjustering',
'align-content': 'Innehållsjustering',
order: 'Ordning',
'flex-basis': 'Flexbas',
'flex-grow': 'Flex väx',
'flex-shrink': 'Flex krymp',
'align-self': 'Självjustering'
}
},
traitManager: {
empty: 'Markera ett element innan du använder egenskapshanteraren',
label: 'Komponentinställningar',
traits: {
// The core library generates the name by their `name` property
labels: {
// id: 'Id',
// alt: 'Alt',
title: 'Titel',
placeholder: 'Platshållartext',
value: 'Värde',
required: 'Obligatorisk',
selected: 'Markerad',
checked: 'Ikryssad',
type: 'Typ',
style: 'Stil',
class: 'Klass',
// href: 'Href',
},
// In a simple trait, like text input, these are used on input attributes
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 't.ex. https://google.com' }
},
// In a trait like select, these are used to translate option names
options: {
target: {
false: 'Detta fönster',
_blank: 'Nytt fönster'
}
}
}
}
};

14
src/navigator/config/config.js

@ -58,5 +58,17 @@ export default {
* } * }
* } * }
*/ */
onRender: () => {} onRender: () => {},
/**
* Extend Layer view object (view/ItemView.js)
* @example
* extend: {
* setName(name) {
* // this.model is the component of the layer
* this.model.set('another-prop-for-name', name);
* },
* },
*/
extend: {}
}; };

7
src/navigator/index.js

@ -1,5 +1,5 @@
import defaults from './config/config'; import defaults from './config/config';
import ItemView from './view/ItemView'; import View from './view/ItemView';
import { isElement } from 'underscore'; import { isElement } from 'underscore';
export default () => { export default () => {
@ -14,7 +14,6 @@ export default () => {
config = { ...defaults, ...opts }; config = { ...defaults, ...opts };
config.stylePrefix = opts.pStylePrefix; config.stylePrefix = opts.pStylePrefix;
em = config.em; em = config.em;
return this; return this;
}, },
@ -23,7 +22,9 @@ export default () => {
}, },
onLoad() { onLoad() {
const ItemView = View.extend(config.extend);
layers = new ItemView({ layers = new ItemView({
ItemView,
level: 0, level: 0,
config, config,
opened: config.opened || {}, opened: config.opened || {},
@ -50,7 +51,7 @@ export default () => {
* @return {self} * @return {self}
*/ */
setRoot(el) { setRoot(el) {
layers.setRoot(el); layers && layers.setRoot(el);
return this; return this;
}, },

27
src/navigator/view/ItemView.js

@ -1,5 +1,5 @@
import { isUndefined, isString, bindAll } from 'underscore'; import { isUndefined, isString, bindAll } from 'underscore';
import { getModel } from 'utils/mixins'; import { getModel, isEscKey, isEnterKey } from 'utils/mixins';
import Backbone from 'backbone'; import Backbone from 'backbone';
import ComponentView from 'dom_components/view/ComponentView'; import ComponentView from 'dom_components/view/ComponentView';
import { eventDrag } from 'dom_components/model/Component'; import { eventDrag } from 'dom_components/model/Component';
@ -18,6 +18,7 @@ export default Backbone.View.extend({
'mouseover [data-toggle-select]': 'handleHover', 'mouseover [data-toggle-select]': 'handleHover',
'mouseout [data-toggle-select]': 'handleHoverOut', 'mouseout [data-toggle-select]': 'handleHoverOut',
'dblclick [data-name]': 'handleEdit', 'dblclick [data-name]': 'handleEdit',
'keydown [data-name]': 'handleEditKey',
'focusout [data-name]': 'handleEditEnd' 'focusout [data-name]': 'handleEditEnd'
}, },
@ -46,7 +47,7 @@ export default Backbone.View.extend({
} }
<div class="${clsTitleC}"> <div class="${clsTitleC}">
<div class="${clsTitle}" style="padding-left: ${gut}" data-toggle-select> <div class="${clsTitle}" style="padding-left: ${gut}" data-toggle-select>
<div class="${pfx}layer-title-inn"> <div class="${pfx}layer-title-inn" title="${name}">
<i class="${clsCaret}" data-toggle-open></i> <i class="${clsCaret}" data-toggle-open></i>
${icon ? `<span class="${clsBase}__icon">${icon}</span>` : ''} ${icon ? `<span class="${clsBase}__icon">${icon}</span>` : ''}
<span class="${clsInput}" data-name>${name}</span> <span class="${clsInput}" data-name>${name}</span>
@ -83,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`;
@ -163,6 +165,7 @@ export default Backbone.View.extend({
const inputEl = this.getInputName(); const inputEl = this.getInputName();
inputEl[inputProp] = true; inputEl[inputProp] = true;
inputEl.focus(); inputEl.focus();
document.execCommand('selectAll', false, null);
em && em.setEditing(1); em && em.setEditing(1);
$el $el
.find(`.${this.inputNameCls}`) .find(`.${this.inputNameCls}`)
@ -170,6 +173,11 @@ export default Backbone.View.extend({
.addClass(clsEdit); .addClass(clsEdit);
}, },
handleEditKey(ev) {
ev.stopPropagation();
(isEscKey(ev) || isEnterKey(ev)) && this.handleEditEnd(ev);
},
/** /**
* Handle with the end of editing of the component name * Handle with the end of editing of the component name
*/ */
@ -180,7 +188,7 @@ export default Backbone.View.extend({
const name = inputEl.textContent; const name = inputEl.textContent;
inputEl.scrollLeft = 0; inputEl.scrollLeft = 0;
inputEl[inputProp] = false; inputEl[inputProp] = false;
this.model.set({ 'custom-name': name }); this.setName(name, { component: this.model, propName: 'custom-name' });
em && em.setEditing(0); em && em.setEditing(0);
$el $el
.find(`.${this.inputNameCls}`) .find(`.${this.inputNameCls}`)
@ -188,6 +196,10 @@ export default Backbone.View.extend({
.removeClass(clsEdit); .removeClass(clsEdit);
}, },
setName(name, { propName }) {
this.model.set(propName, name);
},
/** /**
* Get the input containing the name of the component * Get the input containing the name of the component
* @return {HTMLElement} * @return {HTMLElement}
@ -305,7 +317,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
} }
]); ]);
}, },
@ -395,12 +408,14 @@ export default Backbone.View.extend({
const vis = this.isVisible(); const vis = this.isVisible();
const el = this.$el.empty(); const el = this.$el.empty();
const level = this.level + 1; const level = this.level + 1;
this.inputName = 0;
if (isUndefined(ItemsView)) { if (isUndefined(ItemsView)) {
ItemsView = require('./ItemsView').default; ItemsView = require('./ItemsView').default;
} }
const children = new ItemsView({ const children = new ItemsView({
ItemView: opt.ItemView,
collection: model.get('components'), collection: model.get('components'),
config: this.config, config: this.config,
sorter: this.sorter, sorter: this.sorter,
@ -434,6 +449,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);
} }
}); });

14
src/navigator/view/ItemsView.js

@ -1,5 +1,4 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import ItemView from './ItemView';
import { eventDrag } from 'dom_components/model/Component'; import { eventDrag } from 'dom_components/model/Component';
export default Backbone.View.extend({ export default Backbone.View.extend({
@ -76,11 +75,11 @@ export default Backbone.View.extend({
* @return Object Object created * @return Object Object created
* */ * */
addToCollection(model, fragmentEl, index) { addToCollection(model, fragmentEl, index) {
const { level, parentView } = this; const { level, parentView, opt } = this;
var fragment = fragmentEl || null; const { ItemView } = opt;
var viewObject = ItemView; const fragment = fragmentEl || null;
const rendered = new ItemView({
var view = new viewObject({ ItemView,
level, level,
model, model,
parentView, parentView,
@ -88,8 +87,7 @@ export default Backbone.View.extend({
sorter: this.sorter, sorter: this.sorter,
isCountable: this.isCountable, isCountable: this.isCountable,
opened: this.opt.opened opened: this.opt.opened
}); }).render().el;
var rendered = view.render().el;
if (fragment) { if (fragment) {
fragment.appendChild(rendered); fragment.appendChild(rendered);

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 });
} }
}, },

2
src/panels/view/PanelView.js

@ -120,6 +120,8 @@ export default Backbone.View.extend({
const cls = `${this.className} ${this.id} ${ppfx}one-bg ${ppfx}two-color`; const cls = `${this.className} ${this.id} ${ppfx}one-bg ${ppfx}two-color`;
$el.addClass(cls); $el.addClass(cls);
this.toggleVisible();
if (this.buttons.length) { if (this.buttons.length) {
var buttons = new ButtonsView({ var buttons = new ButtonsView({
collection: this.buttons, collection: this.buttons,

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);

11
src/styles/scss/_gjs_canvas.scss

@ -1,6 +1,6 @@
$frameAnimation: 0.35s ease; $frameAnimation: 0.35s ease !default;
$canvasTop: 40px; $canvasTop: 40px !default;
$guide_pad: 5px; $guide_pad: 5px !default;
.#{$prefix} { .#{$prefix} {
&guide-info { &guide-info {
@ -201,7 +201,7 @@ $guide_pad: 5px;
z-index: 1; z-index: 1;
position: absolute; position: absolute;
left: 0; left: 0;
top: 40px; top: $canvasTop;
&#{gjs-is(grab)}, &#{gjs-is(grab)},
&#{gjs-is(grabbing)} { &#{gjs-is(grabbing)} {
@ -244,7 +244,8 @@ $guide_pad: 5px;
} }
.#{$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 {

1
src/trait_manager/view/TraitView.js

@ -47,6 +47,7 @@ export default Backbone.View.extend({
model.view = this; model.view = this;
this.listenTo(model, 'change:label', this.render); this.listenTo(model, 'change:label', this.render);
this.listenTo(model, 'change:placeholder', this.rerender); this.listenTo(model, 'change:placeholder', this.rerender);
this.events = {};
eventCapture.forEach(event => (this.events[event] = 'onChange')); eventCapture.forEach(event => (this.events[event] = 'onChange'));
this.delegateEvents(); this.delegateEvents();
this.init(); this.init();

126
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',
@ -45,69 +48,84 @@ export default () => {
* @private * @private
*/ */
init(opts = {}) { init(opts = {}) {
config = { ...opts, ...configDef }; config = { ...configDef, ...opts };
em = config.em; em = config.em;
this.em = em; this.em = em;
const fromUndo = true;
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 || {};
opt.noUndo &&
setTimeout(() => {
beforeCache = null;
});
if (hasSkip(opt)) {
return;
} else {
const result = {
object,
before: beforeCache,
after: object.toJSON({ fromUndo })
};
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,
after: model, after: model,
options: { ...options } options: { ...options, fromUndo }
}; };
} }
}); });
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,
after: undefined, after: undefined,
options: { ...options } options: { ...options, fromUndo }
}; };
} }
}); });
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 +259,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 +305,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();

48
src/utils/Resizer.js

@ -18,7 +18,7 @@ var defaultOpts = {
step: 1, step: 1,
// Minimum dimension // Minimum dimension
minDim: 32, minDim: 10,
// Maximum dimension // Maximum dimension
maxDim: '', maxDim: '',
@ -192,6 +192,14 @@ class Resizer {
return this.el; return this.el;
} }
/**
* Returns the parent of the focused element
* @return {HTMLElement}
*/
getParentEl() {
return this.el.parentElement;
}
/** /**
* Returns documents * Returns documents
*/ */
@ -247,10 +255,12 @@ class Resizer {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const el = this.el; const el = this.el;
const parentEl = this.getParentEl();
const resizer = this; const resizer = this;
const config = this.opts || {}; const config = this.opts || {};
var attrName = 'data-' + config.prefix + 'handler'; var attrName = 'data-' + config.prefix + 'handler';
var rect = this.getElementPos(el, { target: 'el' }); var rect = this.getElementPos(el, { target: 'el' });
var parentRect = this.getElementPos(parentEl);
this.handlerAttr = e.target.getAttribute(attrName); this.handlerAttr = e.target.getAttribute(attrName);
this.clickedHandler = e.target; this.clickedHandler = e.target;
this.startDim = { this.startDim = {
@ -269,6 +279,12 @@ class Resizer {
x: e.clientX, x: e.clientX,
y: e.clientY y: e.clientY
}; };
this.parentDim = {
t: parentRect.top,
l: parentRect.left,
w: parentRect.width,
h: parentRect.height
};
// Listen events // Listen events
var doc = this.getDocumentEl(); var doc = this.getDocumentEl();
@ -441,8 +457,14 @@ class Resizer {
const maxDim = opts.maxDim; const maxDim = opts.maxDim;
const deltaX = data.delta.x; const deltaX = data.delta.x;
const deltaY = data.delta.y; const deltaY = data.delta.y;
const startW = startDim.w; const parentW = this.parentDim.w;
const startH = startDim.h; const parentH = this.parentDim.h;
const unitWidth = this.opts.unitWidth;
const unitHeight = this.opts.unitHeight;
const startW =
unitWidth === '%' ? (startDim.w / 100) * parentW : startDim.w;
const startH =
unitHeight === '%' ? (startDim.h / 100) * parentH : startDim.h;
var box = { var box = {
t: 0, t: 0,
l: 0, l: 0,
@ -454,25 +476,37 @@ class Resizer {
var attr = data.handlerAttr; var attr = data.handlerAttr;
if (~attr.indexOf('r')) { if (~attr.indexOf('r')) {
value = normalizeFloat(startW + deltaX * step, step); value =
unitWidth === '%'
? normalizeFloat(((startW + deltaX * step) / parentW) * 100, 0.01)
: normalizeFloat(startW + deltaX * step, step);
value = Math.max(minDim, value); value = Math.max(minDim, value);
maxDim && (value = Math.min(maxDim, value)); maxDim && (value = Math.min(maxDim, value));
box.w = value; box.w = value;
} }
if (~attr.indexOf('b')) { if (~attr.indexOf('b')) {
value = normalizeFloat(startH + deltaY * step, step); value =
unitHeight === '%'
? normalizeFloat(((startH + deltaY * step) / parentH) * 100, 0.01)
: normalizeFloat(startH + deltaY * step, step);
value = Math.max(minDim, value); value = Math.max(minDim, value);
maxDim && (value = Math.min(maxDim, value)); maxDim && (value = Math.min(maxDim, value));
box.h = value; box.h = value;
} }
if (~attr.indexOf('l')) { if (~attr.indexOf('l')) {
value = normalizeFloat(startW - deltaX * step, step); value =
unitWidth === '%'
? normalizeFloat(((startW - deltaX * step) / parentW) * 100, 0.01)
: normalizeFloat(startW - deltaX * step, step);
value = Math.max(minDim, value); value = Math.max(minDim, value);
maxDim && (value = Math.min(maxDim, value)); maxDim && (value = Math.min(maxDim, value));
box.w = value; box.w = value;
} }
if (~attr.indexOf('t')) { if (~attr.indexOf('t')) {
value = normalizeFloat(startH - deltaY * step, step); value =
unitHeight === '%'
? normalizeFloat(((startH - deltaY * step) / parentH) * 100, 0.01)
: normalizeFloat(startH - deltaY * step, step);
value = Math.max(minDim, value); value = Math.max(minDim, value);
maxDim && (value = Math.min(maxDim, value)); maxDim && (value = Math.min(maxDim, value));
box.h = value; box.h = value;

33
src/utils/Sorter.js

@ -558,7 +558,8 @@ export default Backbone.View.extend({
const $parent = parent && $(parent); const $parent = parent && $(parent);
if (style.overflow && style.overflow !== 'visible') return; if (style.overflow && style.overflow !== 'visible') return;
if ($el.css('float') !== 'none') return; const propFloat = $el.css('float');
if (propFloat && propFloat !== 'none') return;
if ( if (
$parent && $parent &&
$parent.css('display') == 'flex' && $parent.css('display') == 'flex' &&
@ -622,13 +623,23 @@ export default Backbone.View.extend({
// Check if the target could accept the source // Check if the target could accept the source
let droppable = trgModel.get('droppable'); let droppable = trgModel.get('droppable');
droppable = droppable instanceof Backbone.Collection ? 1 : droppable;
droppable = droppable instanceof Array ? droppable.join(', ') : droppable; if (isFunction(droppable)) {
result.dropInfo = droppable; const res = droppable(srcModel, trgModel);
droppable = isString(droppable) ? this.matches(src, droppable) : droppable; result.droppable = res;
droppable = result.dropInfo = res;
draggable && this.isTextableActive(srcModel, trgModel) ? 1 : droppable; droppable = res;
result.droppable = droppable; } else {
droppable = droppable instanceof Backbone.Collection ? 1 : droppable;
droppable = droppable instanceof Array ? droppable.join(', ') : droppable;
result.dropInfo = droppable;
droppable = isString(droppable)
? this.matches(src, droppable)
: droppable;
droppable =
draggable && this.isTextableActive(srcModel, trgModel) ? 1 : droppable;
result.droppable = droppable;
}
if (!droppable || !draggable) { if (!droppable || !draggable) {
result.valid = false; result.valid = false;
@ -1039,7 +1050,7 @@ export default Backbone.View.extend({
} }
} }
if (this.moved) { if (this.moved && target) {
const toMove = this.toMove; const toMove = this.toMove;
const toMoveArr = isArray(toMove) ? toMove : toMove ? [toMove] : [src]; const toMoveArr = isArray(toMove) ? toMove : toMove ? [toMove] : [src];
toMoveArr.forEach(model => { toMoveArr.forEach(model => {
@ -1112,7 +1123,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 +1167,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,

22
src/utils/mixins.js

@ -78,21 +78,21 @@ const shallowDiff = (objOrig, objNew) => {
return result; return result;
}; };
const on = (el, ev, fn) => { const on = (el, ev, fn, opts) => {
ev = ev.split(/\s+/); ev = ev.split(/\s+/);
el = el instanceof Array ? el : [el]; el = el instanceof Array ? el : [el];
for (let i = 0; i < ev.length; ++i) { for (let i = 0; i < ev.length; ++i) {
el.forEach(elem => elem.addEventListener(ev[i], fn)); el.forEach(elem => elem.addEventListener(ev[i], fn, opts));
} }
}; };
const off = (el, ev, fn) => { const off = (el, ev, fn, opts) => {
ev = ev.split(/\s+/); ev = ev.split(/\s+/);
el = el instanceof Array ? el : [el]; el = el instanceof Array ? el : [el];
for (let i = 0; i < ev.length; ++i) { for (let i = 0; i < ev.length; ++i) {
el.forEach(elem => elem.removeEventListener(ev[i], fn)); el.forEach(elem => elem.removeEventListener(ev[i], fn, opts));
} }
}; };
@ -210,6 +210,7 @@ const getPointerEvent = ev =>
const getKeyCode = ev => ev.which || ev.keyCode; const getKeyCode = ev => ev.which || ev.keyCode;
const getKeyChar = ev => String.fromCharCode(getKeyCode(ev)); const getKeyChar = ev => String.fromCharCode(getKeyCode(ev));
const isEscKey = ev => getKeyCode(ev) === 27; const isEscKey = ev => getKeyCode(ev) === 27;
const isEnterKey = ev => getKeyCode(ev) === 13;
const isObject = val => const isObject = val =>
val !== null && !Array.isArray(val) && typeof val === 'object'; val !== null && !Array.isArray(val) && typeof val === 'object';
const isEmptyObj = val => Object.keys(val).length <= 0; const isEmptyObj = val => Object.keys(val).length <= 0;
@ -223,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,
@ -236,6 +248,7 @@ export {
getKeyCode, getKeyCode,
getKeyChar, getKeyChar,
isEscKey, isEscKey,
isEnterKey,
getElement, getElement,
shallowDiff, shallowDiff,
normalizeFloat, normalizeFloat,
@ -248,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';

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

@ -1,5 +1,9 @@
import Editor from 'editor'; import Editor from 'editor';
import { keySymbol, keySymbols } from 'dom_components/model/Component'; import {
keySymbol,
keySymbols,
keySymbolOvrd
} from 'dom_components/model/Component';
describe('Symbols', () => { describe('Symbols', () => {
let editor; let editor;
@ -14,6 +18,10 @@ describe('Symbols', () => {
comp.parent().append(cloned, { at: comp.index() + 1 }); comp.parent().append(cloned, { at: comp.index() + 1 });
return cloned; return cloned;
}; };
const simpleCompDef = {
type: 'text',
components: [{ type: 'textnode', content: 'Component' }]
};
const simpleComp = '<div data-a="b">Component</div>'; const simpleComp = '<div data-a="b">Component</div>';
const simpleComp2 = '<div data-b="c">Component 3</div>'; const simpleComp2 = '<div data-b="c">Component 3</div>';
const compMultipleNodes = `<div data-v="a"> const compMultipleNodes = `<div data-v="a">
@ -23,12 +31,33 @@ describe('Symbols', () => {
let allInst, all, comp, symbol, compInitChild; let allInst, all, comp, symbol, compInitChild;
let secComp, secSymbol; let secComp, secSymbol;
const getUm = cmp => cmp.em.get('UndoManager');
const getInnerComp = (cmp, i = 0) => cmp.components().at(i); const getInnerComp = (cmp, i = 0) => cmp.components().at(i);
const getFirstInnSymbol = cmp => getInnerComp(cmp).__getSymbol(); const getFirstInnSymbol = cmp => getInnerComp(cmp).__getSymbol();
const getInnSymbol = (cmp, i = 0) => getInnerComp(cmp, i).__getSymbol(); const getInnSymbol = (cmp, i = 0) => getInnerComp(cmp, i).__getSymbol();
const basicSymbUpdate = (cFrom, cTo) => {
const rand = (Math.random() + 1).toString(36).slice(-7);
const newAttr = { class: `cls-${rand}`, [`myattr-${rand}`]: `val-${rand}` };
cFrom.setAttributes(newAttr);
cFrom.components(`New text content ${rand}`);
const toAttr = cTo.getAttributes();
delete toAttr.id;
const htmlOpts = {
attributes: (m, attr) => {
delete attr.id;
return attr;
}
};
expect(toAttr).toEqual(newAttr);
expect(cFrom.toHTML(htmlOpts)).toBe(cTo.toHTML(htmlOpts));
};
beforeAll(() => { beforeAll(() => {
editor = new Editor(); editor = new Editor({ symbols: 1 });
editor
.getModel()
.get('PageManager')
.onLoad();
wrapper = editor.getWrapper(); wrapper = editor.getWrapper();
}); });
@ -43,6 +72,15 @@ describe('Symbols', () => {
wrapper.components().reset(); wrapper.components().reset();
}); });
test("Simple clone doesn't create any symbol", () => {
const comp = wrapper.append(simpleComp)[0];
const cloned = comp.clone();
[comp, cloned].forEach(item => {
expect(item.__getSymbol()).toBeFalsy();
expect(item.__getSymbols()).toBeFalsy();
});
});
test('Create symbol from a component', () => { test('Create symbol from a component', () => {
const comp = wrapper.append(simpleComp)[0]; const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp); const symbol = createSymbol(comp);
@ -89,6 +127,42 @@ describe('Symbols', () => {
expect(jsonSymb[keySymbols]).toEqual([idComp]); expect(jsonSymb[keySymbols]).toEqual([idComp]);
}); });
test('Serialized symbol references are always recovered', () => {
const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp);
const idComp = comp.getId();
const idSymb = symbol.getId();
// Serialize symbols
comp.set(keySymbol, idSymb);
symbol.set(keySymbols, [idComp]);
// Check updates from instance
basicSymbUpdate(comp, symbol);
// Check updates from symbol
basicSymbUpdate(symbol, comp);
});
test('Symbols recovers correctly from serialization', () => {
const idComp = 'c1';
const idSymb = 's1';
const defComp = {
...simpleCompDef,
[keySymbol]: idSymb,
attributes: { id: idComp }
};
const defSymb = {
...simpleCompDef,
[keySymbols]: [idComp],
attributes: { id: idSymb }
};
const [comp, symbol] = wrapper.append([defComp, defSymb]);
expect(comp.__getSymbol()).toBe(symbol);
expect(comp.get(keySymbol)).toBe(symbol);
expect(symbol.__getSymbols()[0]).toBe(comp);
expect(symbol.get(keySymbols)[0]).toBe(comp);
basicSymbUpdate(comp, symbol);
basicSymbUpdate(symbol, comp);
});
test("Removing one instance doesn't affect others", () => { test("Removing one instance doesn't affect others", () => {
const comp = wrapper.append(simpleComp)[0]; const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp); const symbol = createSymbol(comp);
@ -153,6 +227,23 @@ describe('Symbols', () => {
all.forEach(cmp => expect(cmp.components().length).toBe(compInitChild)); all.forEach(cmp => expect(cmp.components().length).toBe(compInitChild));
}); });
test('Removing one instance, will remove the reference from the symbol', () => {
expect(symbol.__getSymbols().length).toBe(allInst.length);
allInst[2].remove();
expect(symbol.__getSymbols().length).toBe(allInst.length - 1);
});
test('Removing one instance, works with UndoManager', done => {
setTimeout(() => {
// This will commit the undo
const um = getUm(comp);
allInst[0].remove();
um.undo();
expect(symbol.__getSymbols().length).toBe(allInst.length);
done();
});
});
test('Adding a new component to a symbol, it will be propogated to all instances', () => { test('Adding a new component to a symbol, it will be propogated to all instances', () => {
const added = symbol.append(simpleComp, { at: 0 })[0]; const added = symbol.append(simpleComp, { at: 0 })[0];
all.forEach(cmp => all.forEach(cmp =>
@ -174,6 +265,22 @@ describe('Symbols', () => {
allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(addSymb)); allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(addSymb));
}); });
test('Adding a new component to an instance of the symbol, works correctly with Undo Manager', () => {
const added = comp.append(simpleComp, { at: 0 })[0];
const um = getUm(added);
um.undo();
all.forEach(cmp => expect(cmp.components().length).toBe(compInitChild));
um.redo();
um.undo();
um.redo(); // check multiple undo/redo
all.forEach(cmp =>
expect(cmp.components().length).toBe(compInitChild + 1)
);
// Check symbol references
const addSymbs = added.__getSymbol().__getSymbols();
expect(addSymbs.length).toBe(allInst.length);
});
test('Moving a new added component in the instance, will propagate the action in all symbols', () => { test('Moving a new added component in the instance, will propagate the action in all symbols', () => {
const added = comp.append(simpleComp)[0]; const added = comp.append(simpleComp)[0];
expect(added.index()).toBe(compInitChild); expect(added.index()).toBe(compInitChild);
@ -278,6 +385,159 @@ describe('Symbols', () => {
const innerSymb = allInst.map(i => getInnerComp(i, 1)); const innerSymb = allInst.map(i => getInnerComp(i, 1));
expect(clonedSymb.__getSymbols()).toEqual(innerSymb); expect(clonedSymb.__getSymbols()).toEqual(innerSymb);
}); });
describe('Symbols override', () => {
test('Symbol with override returns correctly instances to update', () => {
expect(symbol.__getSymbToUp().length).toBe(allInst.length);
// With override as `true`, it will return empty array with any 'changed'
symbol.set(keySymbolOvrd, true);
expect(symbol.__getSymbToUp({ changed: 'anything' }).length).toBe(0);
// With override as an array with props, changed option will count
symbol.set(keySymbolOvrd, ['components']);
expect(symbol.__getSymbToUp({ changed: 'anything' }).length).toBe(
allInst.length
);
symbol.set(keySymbolOvrd, ['components']);
expect(symbol.__getSymbToUp({ changed: 'components' }).length).toBe(0);
expect(
symbol.__getSymbToUp({ changed: 'components:reset' }).length
).toBe(0);
// Support also overrides with type of actions
symbol.set(keySymbolOvrd, ['components:change']); // specific change
expect(symbol.__getSymbToUp({ changed: 'components' }).length).toBe(
allInst.length
);
expect(
symbol.__getSymbToUp({ changed: 'components:change' }).length
).toBe(0);
});
test('Symbol is not propagating props data if override is set', () => {
const propKey = 'someprop';
const propValue = 'somevalue';
symbol.set(keySymbolOvrd, true);
// Single prop update
symbol.set(propKey, propValue);
allInst.forEach(cmp => expect(cmp.get(propKey)).toBeFalsy());
// Multiple props
symbol.set({ prop1: 'value1', prop2: 'value2' });
allInst.forEach(cmp => {
expect(cmp.get('prop1')).toBeFalsy();
expect(cmp.get('prop2')).toBeFalsy();
});
// Override applied on specific properties
symbol.set(keySymbolOvrd, ['prop1']);
symbol.set({ prop1: 'value1-2', prop2: 'value2-2' });
allInst.forEach(cmp => {
expect(cmp.get('prop1')).toBeFalsy();
expect(cmp.get('prop2')).toBe('value2-2');
});
});
test('On symbol props update, those having override are ignored', () => {
const propKey = 'someprop';
const propValue = 'somevalue';
comp.set(keySymbolOvrd, true);
symbol.set(propKey, propValue);
// All symbols are updated except the one with override
all.forEach(cmp => {
if (cmp === comp) {
expect(cmp.get(propKey)).toBeFalsy();
} else {
expect(cmp.get(propKey)).toBe(propValue);
}
});
comp.set(keySymbolOvrd, ['prop1']);
symbol.set({ prop1: 'value1', prop2: 'value2' });
// Only the overrided property is ignored
all.forEach(cmp => {
if (cmp === comp) {
expect(cmp.get('prop1')).toBeFalsy();
expect(cmp.get('prop2')).toBe('value2');
} else {
expect(cmp.get('prop1')).toBe('value1');
expect(cmp.get('prop2')).toBe('value2');
}
});
});
test('Symbol is not propagating components data if override is set', () => {
symbol.set(keySymbolOvrd, ['components']);
const innCompsLen = symbol.components().length;
all.forEach(cmp => expect(cmp.components().length).toBe(innCompsLen));
symbol.components('Test text');
// The symbol has changed, but istances should remain the same
expect(symbol.components().length).toBe(1);
allInst.forEach(cmp => expect(cmp.toHTML()).toBe(comp.toHTML()));
allInst.forEach(cmp =>
expect(cmp.components().length).toBe(innCompsLen)
);
// Check for add action
symbol.append('<div>B</div><div>C</div>');
expect(symbol.components().length).toBe(3);
allInst.forEach(cmp =>
expect(cmp.components().length).toBe(innCompsLen)
);
});
test('Symbol is not removing components data if override is set', () => {
symbol.set(keySymbolOvrd, ['components']);
const innCompsLen = symbol.components().length;
symbol
.components()
.at(0)
.remove();
expect(symbol.components().length).toBe(innCompsLen - 1);
allInst.forEach(cmp =>
expect(cmp.components().length).toBe(innCompsLen)
);
});
test('Symbol is not propagating remove on instances with ovverride', () => {
comp.set(keySymbolOvrd, ['components']);
const innCompsLen = symbol.components().length;
symbol
.components()
.at(0)
.remove();
all.forEach(cmp =>
expect(cmp.components().length).toBe(
cmp === comp ? innCompsLen : innCompsLen - 1
)
);
});
test('On symbol components update, those having override are ignored', () => {
comp.set(keySymbolOvrd, ['components']);
const innCompsLen = comp.components().length;
// Check reset action
symbol.components('Test text');
all.forEach(cmp => {
expect(cmp.components().length).toBe(cmp === comp ? innCompsLen : 1);
});
// Align comp with others
comp.components('Test text');
all.forEach(cmp => expect(cmp.components().length).toBe(1));
// Check add action
symbol.append('<div>A</div>');
all.forEach(cmp => {
expect(cmp.components().length).toBe(cmp === comp ? 1 : 2);
});
// Align comp with others
comp.append('<div>A</div>');
all.forEach(cmp => expect(cmp.components().length).toBe(2));
// Check remove action
symbol
.components()
.at(0)
.remove();
all.forEach(cmp => {
expect(cmp.components().length).toBe(cmp === comp ? 2 : 1);
});
});
});
}); });
describe('Nested symbols', () => { describe('Nested symbols', () => {
@ -309,6 +569,7 @@ describe('Symbols', () => {
test('Adding the instance, of the second symbol, inside the first symbol, propagates correctly to all first instances', () => { test('Adding the instance, of the second symbol, inside the first symbol, propagates correctly to all first instances', () => {
const added = symbol.append(secComp)[0]; const added = symbol.append(secComp)[0];
expect(added.__isSymbolNested()).toBe(true);
// The added component is still the second instance // The added component is still the second instance
expect(added).toBe(secComp); expect(added).toBe(secComp);
// The added component still has the reference to the second symbol // The added component still has the reference to the second symbol
@ -337,6 +598,20 @@ describe('Symbols', () => {
); );
}); });
test('Adding the instance, of the second symbol, inside one of the first instances, and then removing it, will not affect second instances outside', () => {
const secComp2 = createSymbol(secComp);
const added = comp.append(secComp)[0];
expect(secComp2.__isSymbolNested()).toBe(false);
const secInstans = secSymbol.__getSymbols();
expect(secInstans.length).toBe(all.length + 1); // + 1 is secComp2
// Remove the second instance, added in one of the first instances
added.remove();
// All first symbols will remove their copy and only the secComp2 will remain
expect(secSymbol.__getSymbols().length).toBe(1);
// First symbols has the previous number of components inside
all.forEach(s => expect(s.components().length).toBe(compInitChild));
});
test('Moving the second instance inside first instances, propagates correctly to all other first symbols', () => { test('Moving the second instance inside first instances, propagates correctly to all other first symbols', () => {
const added = comp.append(secComp)[0]; const added = comp.append(secComp)[0];
expect(added.parent()).toBe(comp); // extra checks expect(added.parent()).toBe(comp); // extra checks

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);
});
});

228
test/specs/utils/Sorter.js

@ -1,8 +1,13 @@
// import Sorter from '../../../src/utils/Sorter.js' import Backbone from 'backbone';
import Sorter from '../../../src/utils/Sorter.js';
import ComponentView from 'dom_components/view/ComponentView';
import ComponentTextView from 'dom_components/view/ComponentTextView';
import Component from 'dom_components/model/Component';
const $ = Backbone.$;
// TODO: Migrate this file to Jest // TODO: Migrate this file to Jest
describe.skip('Sorter', () => { describe('Sorter', () => {
var fixture; var fixture;
var obj; var obj;
var parent; var parent;
@ -21,32 +26,32 @@ describe.skip('Sorter', () => {
obj = new Sorter({ container: '.parent1' }); obj = new Sorter({ container: '.parent1' });
document.body.appendChild(fixture); document.body.appendChild(fixture);
fixture.appendChild(parent); fixture.appendChild(parent);
html = html = `
'<div id="el1" style="overflow: hidden;">' + <div id="el1" style="overflow: hidden;">
'<div id="el2">ba' + <div id="el2">ba
'<p id="baa">baa</p>' + <p id="baa">baa</p>
'<span id="elspan">bab</span>' + <span id="elspan">bab</span>
'<span id="bac" style="display:block;">bac</span>' + <span id="bac" style="display:block;">bac</span>
'<div id="eldiv">eldiv</div>' + <div id="eldiv">eldiv</div>
'</div>' + </div>
'</div>' + </div>
'<div id="a">' + <div id="a">
'<div id="aa">aa' + <div id="aa">aa
'<p id="aaa">aaa</p>' + <p id="aaa">aaa</p>
'<span id="aab">aab</span>' + <span id="aab">aab</span>
'<span id="aac" style="display:block;">aac</span>' + <span id="aac" style="display:block;">aac</span>
'</div>' + </div>
'<div id="ab" style="float: left;">ab</div>' + <div id="ab" style="float: left;">ab</div>
'<div id="ac" style="position: absolute;">ac' + <div id="ac" style="position: absolute;">ac
'<div id="aca" style="float: left;">aca</div>' + <div id="aca" style="float: left;">aca</div>
'<div id="acb">acb</div>' + <div id="acb">acb</div>
'</div>' + </div>
'<div id="ad" style="overflow: hidden;">ad' + <div id="ad" style="overflow: hidden;">ad
'<p id="ada">ada</p>' + <p id="ada">ada</p>
'<span id="adb">adb</span>' + <span id="adb">adb</span>
'<span id="adc" style="display:block;">adc</span>' + <span id="adc" style="display:block;">adc</span>
'</div>' + </div>
'</div>'; </div>`;
}); });
afterEach(() => { afterEach(() => {
@ -60,36 +65,36 @@ describe.skip('Sorter', () => {
var el = document.createElement('div'); var el = document.createElement('div');
el.setAttribute('class', 'test test2'); el.setAttribute('class', 'test test2');
parent.appendChild(el); parent.appendChild(el);
obj.matches(el, '.test').should.equal(true); expect(obj.matches(el, '.test')).toEqual(true);
obj.matches(el, '.test2').should.equal(true); expect(obj.matches(el, '.test2')).toEqual(true);
obj.matches(el, '.test3').should.equal(false); expect(obj.matches(el, '.test3')).toEqual(false);
}); });
test('matches id', () => { test('matches id', () => {
var el = document.createElement('div'); var el = document.createElement('div');
el.setAttribute('id', 'test2'); el.setAttribute('id', 'test2');
parent.appendChild(el); parent.appendChild(el);
obj.matches(el, '#test2').should.equal(true); expect(obj.matches(el, '#test2')).toEqual(true);
obj.matches(el, '.test2').should.equal(false); expect(obj.matches(el, '.test2')).toEqual(false);
obj.matches(el, '#test').should.equal(false); expect(obj.matches(el, '#test')).toEqual(false);
}); });
test('matches tag', () => { test('matches tag', () => {
var el = document.createElement('span'); var el = document.createElement('span');
parent.appendChild(el); parent.appendChild(el);
obj.matches(el, 'span').should.equal(true); expect(obj.matches(el, 'span')).toEqual(true);
obj.matches(el, 'div').should.equal(false); expect(obj.matches(el, 'div')).toEqual(false);
obj.matches(el, '*').should.equal(true); expect(obj.matches(el, '*')).toEqual(true);
}); });
test('Creates placeholder', () => { test('Creates placeholder', () => {
obj.createPlaceholder().className.should.equal('placeholder'); expect(obj.createPlaceholder().className).toEqual('placeholder');
}); });
test('isInFlow to overflow hidden', () => { test('isInFlow to overflow hidden', () => {
parent.innerHTML = html; parent.innerHTML = html;
var el = parent.querySelector('#el1'); var el = parent.querySelector('#el1');
obj.isInFlow(el).should.equal(false); expect(obj.isInFlow(el)).toEqual(false);
}); });
test('isInFlow inner to overflow', () => { test('isInFlow inner to overflow', () => {
@ -99,13 +104,13 @@ describe.skip('Sorter', () => {
console.log('phantom issue'); console.log('phantom issue');
return; return;
} }
obj.isInFlow(el).should.equal(true); expect(obj.isInFlow(el)).toEqual(true);
}); });
test('isInFlow for span', () => { test('isInFlow for span', () => {
parent.innerHTML = html; parent.innerHTML = html;
var el = parent.querySelector('#elspan'); var el = parent.querySelector('#elspan');
obj.isInFlow(el).should.equal(false); expect(obj.isInFlow(el)).toEqual(false);
}); });
test('isInFlow for div #a', () => { test('isInFlow for div #a', () => {
@ -115,7 +120,7 @@ describe.skip('Sorter', () => {
console.log('phantom issue'); console.log('phantom issue');
return; return;
} }
obj.isInFlow(el).should.equal(true); expect(obj.isInFlow(el)).toEqual(true);
}); });
test('isInFlow for div #aa', () => { test('isInFlow for div #aa', () => {
@ -125,7 +130,7 @@ describe.skip('Sorter', () => {
console.log('phantom issue'); console.log('phantom issue');
return; return;
} }
obj.isInFlow(el).should.equal(true); expect(obj.isInFlow(el)).toEqual(true);
}); });
test('isInFlow for p #aaa', () => { test('isInFlow for p #aaa', () => {
@ -135,13 +140,13 @@ describe.skip('Sorter', () => {
console.log('phantom issue'); console.log('phantom issue');
return; return;
} }
obj.isInFlow(el).should.equal(true); expect(obj.isInFlow(el)).toEqual(true);
}); });
test('isInFlow for span #aab', () => { test('isInFlow for span #aab', () => {
parent.innerHTML = html; parent.innerHTML = html;
var el = parent.querySelector('#aab'); var el = parent.querySelector('#aab');
obj.isInFlow(el).should.equal(false); expect(obj.isInFlow(el)).toEqual(false);
}); });
test('isInFlow for span #aac with display block', () => { test('isInFlow for span #aac with display block', () => {
@ -150,19 +155,19 @@ describe.skip('Sorter', () => {
if (!el) if (!el)
// in phantom doesnt work // in phantom doesnt work
return; return;
obj.isInFlow(el).should.equal(true); expect(obj.isInFlow(el)).toEqual(true);
}); });
test('isInFlow for div #ab with float left', () => { test('isInFlow for div #ab with float left', () => {
parent.innerHTML = html; parent.innerHTML = html;
var el = parent.querySelector('#ab'); var el = parent.querySelector('#ab');
obj.isInFlow(el).should.equal(false); expect(obj.isInFlow(el)).toEqual(false);
}); });
test('isInFlow for div #ac in absolute', () => { test('isInFlow for div #ac in absolute', () => {
parent.innerHTML = html; parent.innerHTML = html;
var el = parent.querySelector('#ac'); var el = parent.querySelector('#ac');
obj.isInFlow(el).should.equal(false); expect(obj.isInFlow(el)).toEqual(false);
}); });
test('isInFlow for div #acb inside absolute', () => { test('isInFlow for div #acb inside absolute', () => {
@ -172,13 +177,13 @@ describe.skip('Sorter', () => {
console.log('phantom issue'); console.log('phantom issue');
return; return;
} }
obj.isInFlow(el).should.equal(true); expect(obj.isInFlow(el)).toEqual(true);
}); });
test('isInFlow for div #ad overflow hidden', () => { test('isInFlow for div #ad overflow hidden', () => {
parent.innerHTML = html; parent.innerHTML = html;
var el = parent.querySelector('#ad'); var el = parent.querySelector('#ad');
obj.isInFlow(el).should.equal(false); expect(obj.isInFlow(el)).toEqual(false);
}); });
describe('Closest method', () => { describe('Closest method', () => {
@ -197,22 +202,22 @@ describe.skip('Sorter', () => {
test('Closest by class', () => { test('Closest by class', () => {
var el = document.createElement('div'); var el = document.createElement('div');
parent3.appendChild(el); parent3.appendChild(el);
obj.closest(el, '.parent2').should.deep.equal(parent2); expect(obj.closest(el, '.parent2')).toEqual(parent2);
obj.closest(el, '.parent3').should.deep.equal(parent3); expect(obj.closest(el, '.parent3')).toEqual(parent3);
obj.closest(el, '.parent1').should.deep.equal(parent); expect(obj.closest(el, '.parent1')).toEqual(parent);
}); });
test('Closest by tag', () => { test('Closest by tag', () => {
var el = document.createElement('div'); var el = document.createElement('div');
el.setAttribute('class', 'el'); el.setAttribute('class', 'el');
parent3.appendChild(el); parent3.appendChild(el);
obj.closest(el, 'span').should.deep.equal(parent2); expect(obj.closest(el, 'span')).toEqual(parent2);
obj.closest(el, 'div').should.deep.equal(parent3); expect(obj.closest(el, 'div')).toEqual(parent3);
obj.closest(el, '*').should.deep.equal(parent3); expect(obj.closest(el, '*')).toEqual(parent3);
}); });
}); });
describe('With elements', () => { describe.skip('With elements', () => {
var vertDims; var vertDims;
var parent2; var parent2;
var parent3; var parent3;
@ -262,8 +267,8 @@ describe.skip('Sorter', () => {
test('startSort inits correctly inits', () => { test('startSort inits correctly inits', () => {
obj.startSort(el); obj.startSort(el);
obj.moved.should.equal(0); expect(obj.moved).toEqual(0);
obj.plh.style.display.should.equal('none'); expect(obj.plh.style.display).toEqual('none');
}); });
test('onMove', () => { test('onMove', () => {
@ -274,7 +279,7 @@ describe.skip('Sorter', () => {
pageY: 0, pageY: 0,
target: target target: target
}); });
obj.moved.should.equal(1); expect(obj.moved).toEqual(1);
}); });
test('getDim from element', () => { test('getDim from element', () => {
@ -282,7 +287,7 @@ describe.skip('Sorter', () => {
var top = subPos.top; var top = subPos.top;
var left = subPos.left; var left = subPos.left;
var result = [top, left, 50, 100]; var result = [top, left, 50, 100];
obj.getDim(sib1).should.deep.equal(result); expect(obj.getDim(sib1)).toEqual(result);
}); });
test('getChildrenDim from element', () => { test('getChildrenDim from element', () => {
@ -301,15 +306,15 @@ describe.skip('Sorter', () => {
[top + 100, left + 0, 50, 100, true], [top + 100, left + 0, 50, 100, true],
[top + 100, left + 100, 50, 70, true] [top + 100, left + 100, 50, 70, true]
]; ];
ch.should.deep.equal(result); expect(ch).toEqual(result);
}); });
test('nearBorders', () => { test('nearBorders', () => {
obj.borderOffset = 10; obj.borderOffset = 10;
var dim = [0, 0, 100, 200]; var dim = [0, 0, 100, 200];
obj.nearBorders(dim, 20, 15).should.equal(false); expect(obj.nearBorders(dim, 20, 15)).toEqual(false);
obj.nearBorders(dim, 3, 4).should.equal(true); expect(obj.nearBorders(dim, 3, 4)).toEqual(true);
obj.nearBorders(dim, 500, 500).should.equal(true); expect(obj.nearBorders(dim, 500, 500)).toEqual(true);
}); });
test('dimsFromTarget', () => { test('dimsFromTarget', () => {
@ -342,14 +347,15 @@ describe.skip('Sorter', () => {
dims = dims.map(function(v) { dims = dims.map(function(v) {
return v.slice(0, 5); return v.slice(0, 5);
}); });
dims.should.deep.equal(resultParent);
expect(dims).toEqual(resultParent);
// Inside target // Inside target
var dims = obj.dimsFromTarget(sib3, leftSib3 + 15, topSib3 + 15); var dims = obj.dimsFromTarget(sib3, leftSib3 + 15, topSib3 + 15);
dims = dims.map(function(v) { dims = dims.map(function(v) {
return v.slice(0, 5); return v.slice(0, 5);
}); });
dims.should.deep.equal(resultChildren); expect(dims).toEqual(resultChildren);
// Exactly on border // Exactly on border
var bOffset = obj.borderOffset; var bOffset = obj.borderOffset;
@ -361,7 +367,7 @@ describe.skip('Sorter', () => {
dims = dims.map(function(v) { dims = dims.map(function(v) {
return v.slice(0, 5); return v.slice(0, 5);
}); });
dims.should.deep.equal(resultChildren); expect(dims).toEqual(resultChildren);
// Slightly near border // Slightly near border
var dims = obj.dimsFromTarget( var dims = obj.dimsFromTarget(
@ -372,7 +378,7 @@ describe.skip('Sorter', () => {
dims = dims.map(function(v) { dims = dims.map(function(v) {
return v.slice(0, 5); return v.slice(0, 5);
}); });
dims.should.deep.equal(resultParent); expect(dims).toEqual(resultParent);
}); });
describe('findPosition', () => { describe('findPosition', () => {
@ -380,23 +386,23 @@ describe.skip('Sorter', () => {
test('Vertical dimensions', () => { test('Vertical dimensions', () => {
var result = { index: 0, method: 'before' }; var result = { index: 0, method: 'before' };
obj.findPosition(vertDims, -10, -10).should.deep.equal(result); expect(obj.findPosition(vertDims, -10, -10)).toEqual(result);
obj.findPosition(vertDims, 0, 0).should.deep.equal(result); expect(obj.findPosition(vertDims, 0, 0)).toEqual(result);
obj.findPosition(vertDims, 10, 10).should.deep.equal(result); expect(obj.findPosition(vertDims, 10, 10)).toEqual(result);
var result = { index: 1, method: 'before' }; var result = { index: 1, method: 'before' };
obj.findPosition(vertDims, 10, 30).should.deep.equal(result); expect(obj.findPosition(vertDims, 10, 30)).toEqual(result);
obj.findPosition(vertDims, 10, 70).should.deep.equal(result); expect(obj.findPosition(vertDims, 10, 70)).toEqual(result);
var result = { index: 2, method: 'before' }; var result = { index: 2, method: 'before' };
obj.findPosition(vertDims, 10, 76).should.deep.equal(result); expect(obj.findPosition(vertDims, 10, 76)).toEqual(result);
var result = { index: 3, method: 'before' }; var result = { index: 3, method: 'before' };
obj.findPosition(vertDims, 100, 140).should.deep.equal(result); expect(obj.findPosition(vertDims, 100, 140)).toEqual(result);
obj.findPosition(vertDims, 100, 160).should.deep.equal(result); expect(obj.findPosition(vertDims, 100, 160)).toEqual(result);
var result = { index: 3, method: 'after' }; var result = { index: 3, method: 'after' };
obj.findPosition(vertDims, 1000, 1000).should.deep.equal(result); expect(obj.findPosition(vertDims, 1000, 1000)).toEqual(result);
}); });
}); });
@ -414,19 +420,73 @@ describe.skip('Sorter', () => {
var pos = { index: 2, method: 'before' }; var pos = { index: 2, method: 'before' };
obj.movePlaceholder(plh, vertDims, pos); obj.movePlaceholder(plh, vertDims, pos);
var style = plh.style; var style = plh.style;
style.top.should.equal('100px'); expect(style.top).toEqual('100px');
style.left.should.equal('30px'); expect(style.left).toEqual('30px');
style.width.should.equal('100px'); expect(style.width).toEqual('100px');
}); });
test('Vertical dimensions with after position', () => { test('Vertical dimensions with after position', () => {
var pos = { index: 1, method: 'after' }; var pos = { index: 1, method: 'after' };
obj.movePlaceholder(plh, vertDims, pos); obj.movePlaceholder(plh, vertDims, pos);
var style = plh.style; var style = plh.style;
style.top.should.equal('100px'); expect(style.top).toEqual('100px');
style.left.should.equal('20px'); expect(style.left).toEqual('20px');
style.width.should.equal('70px'); expect(style.width).toEqual('70px');
}); });
}); });
}); });
describe('Valid Target with components', () => {
var parentModel;
var parentView;
var sorter;
beforeEach(() => {
parentModel = new Component({
droppable: (srcModel, trgModel) => {
if (srcModel.getEl().className === 'canDrop') {
return true;
}
return false;
}
});
parentView = new ComponentTextView({
model: parentModel
});
sorter = new Sorter({ container: parentView.el });
});
afterEach(() => {
parentView.remove();
});
test('Droppable function', () => {
var srcModel = new Component({
tagName: 'div',
draggable: true, // Can't move it
content: 'Content text', // Text inside component
attributes: { class: 'canDrop' }
});
var srcView = new ComponentTextView({
model: srcModel
});
expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(true);
});
test('Not droppable function', () => {
var srcModel = new Component({
tagName: 'div',
draggable: true,
content: 'Content text',
attributes: { class: 'cannotDrop' }
});
var srcView = new ComponentTextView({
model: srcModel
});
expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(false);
});
});
}); });

Loading…
Cancel
Save