Browse Source

Merge pull request #2732 from artf/dev

Merge dev
pull/2886/head
Artur Arseniev 6 years ago
committed by GitHub
parent
commit
405348e186
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 29
      .npmignore
  2. 2
      dist/css/grapes.min.css
  3. 940
      dist/grapes.js
  4. 6
      dist/grapes.min.js
  5. 2
      dist/grapes.min.js.map
  6. 4
      docs/.vuepress/config.js
  7. 14
      docs/.vuepress/styles/index.styl
  8. 5
      docs/.vuepress/styles/palette.styl
  9. 17
      docs/.vuepress/theme/Layout.vue
  10. 3
      docs/.vuepress/theme/index.js
  11. 3
      docs/.vuepress/theme/layouts/CarbonAds.vue
  12. 21
      docs/.vuepress/theme/layouts/Layout.vue
  13. 32
      docs/api/canvas.md
  14. 42
      docs/api/component.md
  15. 8
      docs/api/editor.md
  16. 26
      docs/api/rich_text_editor.md
  17. 62
      docs/api/selector_manager.md
  18. 1
      docs/api/style_manager.md
  19. 22
      docs/api/undo_manager.md
  20. 14605
      package-lock.json
  21. 17
      package.json
  22. 2
      src/block_manager/model/Block.js
  23. 14
      src/block_manager/view/BlockView.js
  24. 4
      src/canvas/index.js
  25. 4
      src/canvas/model/Canvas.js
  26. 16
      src/canvas/view/CanvasView.js
  27. 1
      src/canvas/view/FrameView.js
  28. 3
      src/commands/view/OpenStyleManager.js
  29. 19
      src/commands/view/Preview.js
  30. 14
      src/commands/view/SelectComponent.js
  31. 11
      src/commands/view/SwitchVisibility.js
  32. 21
      src/css_composer/model/CssRules.js
  33. 4
      src/dom_components/index.js
  34. 55
      src/dom_components/model/Component.js
  35. 21
      src/dom_components/model/ComponentTableRow.js
  36. 109
      src/dom_components/model/Components.js
  37. 25
      src/dom_components/view/ComponentTextView.js
  38. 34
      src/dom_components/view/ComponentView.js
  39. 53
      src/dom_components/view/ComponentsView.js
  40. 15
      src/domain_abstract/ui/InputColor.js
  41. 23
      src/domain_abstract/view/DomainViews.js
  42. 177
      src/editor/index.js
  43. 19
      src/editor/model/Editor.js
  44. 6
      src/i18n/locale/nl.js
  45. 200
      src/i18n/locale/pt.js
  46. 2
      src/index.js
  47. 3
      src/navigator/view/ItemsView.js
  48. 3
      src/style_manager/index.js
  49. 1
      src/style_manager/model/Layers.js
  50. 4
      src/style_manager/model/Property.js
  51. 19
      src/style_manager/model/PropertyStack.js
  52. 28
      src/style_manager/view/LayerView.js
  53. 23
      src/style_manager/view/LayersView.js
  54. 12
      src/style_manager/view/PropertiesView.js
  55. 90
      src/style_manager/view/PropertyStackView.js
  56. 32
      src/style_manager/view/PropertyView.js
  57. 2
      src/style_manager/view/SectorsView.js
  58. 3
      src/styles/scss/_gjs_variables.scss
  59. 19
      src/styles/scss/main.scss
  60. 41
      src/undo_manager/index.js
  61. 18
      src/utils/Sorter.js
  62. 40
      test/specs/commands/view/Preview.js
  63. 29
      test/specs/commands/view/SwitchVisibility.js
  64. 3
      test/specs/dom_components/view/ComponentsView.js
  65. 116
      test/specs/editor/index.js

29
.npmignore

@ -0,0 +1,29 @@
.DS_Store
.settings/
.sass-cache/
.project
.idea
npm-debug.log*
yarn-error.log
yarn.lock
style/.sass-cache/
stats.json
img/
images/
private/
vendor/
coverage/
node_modules/
bower_components/
grapesjs-*.tgz
_index.html
index.html
docs
.github
test
.editorconfig
.eslintrc
.travis.yml
*.md
webpack.config.js

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

940
dist/grapes.js

File diff suppressed because it is too large

6
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

4
docs/.vuepress/config.js

@ -6,7 +6,6 @@ module.exports = {
title: 'GrapesJS',
description: 'GrapesJS documentation',
base: '/docs/',
ga: 'UA-74284223-1',
serviceWorker: false, // Enable Service Worker for offline usage
head: [
['link', { rel: 'icon', href: '/logo-icon.png' }],
@ -106,4 +105,7 @@ module.exports = {
],
}
},
plugins: [
[ '@vuepress/google-analytics', { ga: 'UA-74284223-1' } ],
],
}

14
docs/.vuepress/override.styl → docs/.vuepress/styles/index.styl

@ -1,9 +1,3 @@
$accentColor = #e2627f
$accentColor = #e67891
$navBarColor = white
$scrollBarSize = 8px
$pageWidth = 900px
.img-ctr {
margin: 0 auto;
display: block;
@ -20,7 +14,11 @@ $pageWidth = 900px
}
.site-name {
color: $navBarColor;
color: $navBarColor !important;
}
.links {
background-color: transparent !important;
}
}
@ -31,7 +29,7 @@ $pageWidth = 900px
@media (min-width: 719px) {
.nav-links a:hover,
.nav-links a.router-link-active {
color: #ffeff2;
color: #ffeff2 !important;
}
}

5
docs/.vuepress/styles/palette.styl

@ -0,0 +1,5 @@
$accentColor = #e2627f
$accentColor = #e67891
$navBarColor = white
$scrollBarSize = 8px
$pageWidth = 900px

17
docs/.vuepress/theme/Layout.vue

@ -1,17 +0,0 @@
<template>
<Layout>
<CarbonAds slot="sidebar-top"/>
</Layout>
</template>
<script>
var Layout = require('@default-theme/Layout.vue').default;
var CarbonAds = require('./CarbonAds.vue').default;
export default {
components: {
Layout,
CarbonAds,
}
}
</script>

3
docs/.vuepress/theme/index.js

@ -0,0 +1,3 @@
module.exports = {
extend: '@vuepress/theme-default'
}

3
docs/.vuepress/theme/CarbonAds.vue → docs/.vuepress/theme/layouts/CarbonAds.vue

@ -47,11 +47,10 @@ export default {
}
}
}
// @import "~@default-theme/styles/config.styl"
</script>
<style lang="stylus">
@import "~@default-theme/styles/config.styl"
.carbon-ads
min-height 102px
padding 1.5rem 1.5rem 0

21
docs/.vuepress/theme/layouts/Layout.vue

@ -0,0 +1,21 @@
<template>
<Layout>
<CarbonAds slot="sidebar-top"/>
</Layout>
</template>
<script>
// var Layout = require('@vuepress/theme-vue/layouts/Layout.vue').default;
// var CarbonAds = require('./CarbonAds.vue').default;
// import Layout from '@vuepress/theme-default/layouts/Layout.vue';
// import CarbonAds from './CarbonAds.vue';
import Layout from '@parent-theme/layouts/Layout.vue'
import CarbonAds from './CarbonAds.vue';
export default {
components: {
Layout,
CarbonAds,
}
}
</script>

32
docs/api/canvas.md

@ -140,6 +140,38 @@ Get zoom value
Returns **[Number][21]**
## addFrame
Add new frame to the canvas
### Parameters
- `props` **[Object][14]** Frame properties (optional, default `{}`)
- `opts` (optional, default `{}`)
### Examples
```javascript
editor.Canvas.addFrame({
name: 'Mobile home page',
x: 100, // Position in canvas
y: 100,
width: 500, // Frame dimensions
height: 600,
// device: 'DEVICE-ID',
components: [
'<h1 class="testh">Title frame</h1>',
'<p class="testp">Paragraph frame</p>',
],
styles: `
.testh { color: red; }
.testp { color: blue; }
`,
});
```
Returns **Frame**
[1]: https://github.com/artf/grapesjs/blob/master/src/canvas/config/config.js
[2]: #getconfig

42
docs/api/component.md

@ -137,13 +137,13 @@ Returns **[Array][4]** Array of components
## findType
Find all inner components by component id.
Find all inner components by component type.
The advantage of this method over `find` is that you can use it
also before rendering the component
### Parameters
- `id` **[String][1]** Component id
- `type` **[String][1]** Component type
### Examples
@ -172,6 +172,25 @@ component.closest('div.some-class');
Returns **[Component][9]**
## closestType
Find the closest parent component by its type.
The advantage of this method over `closest` is that you can use it
also before rendering the component
### Parameters
- `type` **[String][1]** Component type
### Examples
```javascript
const Section = component.closestType('section');
console.log(Section);
```
Returns **[Component][9]** Found component, otherwise `undefined`
## replaceWith
Replace a component with another one
@ -359,6 +378,16 @@ console.log(collection.length);
Returns **(Collection | [Array][4]&lt;[Component][9]>)**
## empty
Remove all inner components
- @return {this}
### Parameters
- `opts` (optional, default `{}`)
## parent
Get the parent component, if exists
@ -485,6 +514,7 @@ Return HTML string of the component
### Parameters
- `opts` **[Object][2]** Options (optional, default `{}`)
- `opts.tag` **[String][1]?** Custom tagName
- `opts.attributes` **([Object][2] \| [Function][6])** You can pass an object of custom attributes to replace
with the current one or you can even pass a function to generate attributes dynamically (optional, default `null`)
@ -537,6 +567,10 @@ Returns **this**
Get the DOM element of the component.
This works only if the component is already rendered
### Parameters
- `frame` **Frame** Specific frame from which taking the element
Returns **[HTMLElement][12]**
## getView
@ -544,6 +578,10 @@ Returns **[HTMLElement][12]**
Get the View of the component.
This works only if the component is already rendered
### Parameters
- `frame` **Frame** Get View of a specific frame
Returns **ComponentView**
## onAll

8
docs/api/editor.md

@ -37,6 +37,9 @@ editor.on('EVENT-NAME', (some, argument) => {
- `component:toggled` - Component selection changed, toggled model is passed as an argument to the callback
- `component:type:add` - New component type added, the new type is passed as an argument to the callback
- `component:type:update` - Component type updated, the updated type is passed as an argument to the callback
- `component:drag:start` - Component drag started. Passed an object, to the callback, containing the `target` (component to drag), `parent` (parent of the component) and `index` (component index in the parent)
- `component:drag` - During component drag. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the current pointer
- `component:drag:end` - Component drag ended. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the final pointer
### Blocks
@ -93,7 +96,10 @@ editor.on('EVENT-NAME', (some, argument) => {
### Selectors
- `selector:add` - Triggers when a new selector/class is created
- `selector:add` - New selector is add. Passes the new selector as an argument
- `selector:remove` - Selector removed. Passes the removed selector as an argument
- `selector:update` - Selector updated. Passes the updated selector as an argument
- `selector:state` - State changed. Passes the new state value as an argument
### RTE

26
docs/api/rich_text_editor.md

@ -68,6 +68,32 @@ rte.add('fontSize', {
}
}
})
// An example with state
const isValidAnchor = (rte) => {
// a utility function to help determine if the selected is a valid anchor node
const anchor = rte.selection().anchorNode;
const parentNode = anchor && anchor.parentNode;
const nextSibling = anchor && anchor.nextSibling;
return (parentNode && parentNode.nodeName == 'A') || (nextSibling && nextSibling.nodeName == 'A')
}
rte.add('toggleAnchor', {
icon: `<span style="transform:rotate(45deg)">&supdsub;</span>`,
state: (rte, doc) => {
if (rte && rte.selection()) {
// `btnState` is a integer, -1 for disabled, 0 for inactive, 1 for active
return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE;
} else {
return btnState.INACTIVE;
}
},
result: (rte, action) => {
if (isValidAnchor(rte)) {
rte.exec('unlink');
} else {
rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`);
}
}
})
```
## get

62
docs/api/selector_manager.md

@ -46,12 +46,36 @@ const selectorManager = editor.SelectorManager;
- [addClass][4]
- [get][5]
- [getAll][6]
- [setState][7]
- [getState][8]
## getConfig
Get configuration object
Returns **[Object][7]**
Returns **[Object][9]**
## setState
Change the selector state
### Parameters
- `value` **[String][10]** State value
### Examples
```javascript
selectorManager.setState('hover');
```
Returns **this**
## getState
Get the current selector state
Returns **[String][10]**
## add
@ -59,10 +83,10 @@ Add a new selector to collection if it's not already exists. Class type is a def
### Parameters
- `name` **([String][8] \| [Array][9])** Selector/s name
- `opts` **[Object][7]** Selector options (optional, default `{}`)
- `opts.label` **[String][8]** Label for the selector, if it's not provided the label will be the same as the name (optional, default `''`)
- `opts.type` **[String][8]** Type of the selector. At the moment, only 'class' (1) is available (optional, default `1`)
- `name` **([String][10] \| [Array][11])** Selector/s name
- `opts` **[Object][9]** Selector options (optional, default `{}`)
- `opts.label` **[String][10]** Label for the selector, if it's not provided the label will be the same as the name (optional, default `''`)
- `opts.type` **[String][10]** Type of the selector. At the moment, only 'class' (1) is available (optional, default `1`)
### Examples
@ -77,7 +101,7 @@ const selector = selectorManager.add('selectorName', {
const selectors = selectorManager.add(['.class1', '.class2', '#id1']);
```
Returns **(Model | [Array][9])**
Returns **(Model | [Array][11])**
## addClass
@ -85,7 +109,7 @@ Add class selectors
### Parameters
- `classes` **([Array][9] \| [string][8])** Array or string of classes
- `classes` **([Array][11] \| [string][10])** Array or string of classes
### Examples
@ -96,7 +120,7 @@ sm.addClass(['class1', 'class2']);
// -> [SelectorObject, ...]
```
Returns **[Array][9]** Array of added selectors
Returns **[Array][11]** Array of added selectors
## get
@ -104,8 +128,8 @@ Get the selector by its name
### Parameters
- `name` **([String][8] \| [Array][9])** Selector name
- `type` **[String][8]** Selector type
- `name` **([String][10] \| [Array][11])** Selector name
- `type` **[String][10]** Selector type
### Examples
@ -115,7 +139,7 @@ const selector = selectorManager.get('selectorName');
const selectors = selectorManager.get(['class1', 'class2']);
```
Returns **(Model | [Array][9])**
Returns **(Model | [Array][11])**
## getAll
@ -129,9 +153,9 @@ Return escaped selector name
### Parameters
- `name` **[String][8]** Selector name to escape
- `name` **[String][10]** Selector name to escape
Returns **[String][8]** Escaped name
Returns **[String][10]** Escaped name
[1]: https://github.com/artf/grapesjs/blob/master/src/selector_manager/config/config.js
@ -143,10 +167,14 @@ Returns **[String][8]** Escaped name
[5]: #get
[6]: #getAll
[6]: #getall
[7]: #setstate
[8]: #getstate
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

1
docs/api/style_manager.md

@ -214,6 +214,7 @@ one or more classes, the function will return the corresponding CSS Rule
### Parameters
- `model` **Model**
- `options` (optional, default `{}`)
Returns **Model**

22
docs/api/undo_manager.md

@ -110,6 +110,10 @@ Returns **this**
Undo last change
### Parameters
- `all` (optional, default `true`)
### Examples
```javascript
@ -134,6 +138,10 @@ Returns **this**
Redo last change
### Parameters
- `all` (optional, default `true`)
### Examples
```javascript
@ -191,6 +199,18 @@ stack.each(item => ...);
Returns **Collection**
## getStackGroup
Get grouped undo manager stack.
The difference between `getStack` is when you do multiple operations at a time,
like appending multiple components:
`editor.getWrapper().append(`<div>C1</div><div>C2</div>`);`
`getStack` will return a collection length of 2.
`getStackGroup` instead will group them as a single operation (the first
inserted component will be returned in the list) by returning an array length of 1.
Returns **[Array][17]**
## clear
Clear the stack
@ -234,3 +254,5 @@ Returns **this**
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

14605
package-lock.json

File diff suppressed because it is too large

17
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.16.3",
"version": "0.16.12",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",
@ -16,7 +16,7 @@
"url": "https://github.com/artf/grapesjs.git"
},
"dependencies": {
"@babel/runtime": "^7.7.1",
"@babel/runtime": "^7.9.2",
"backbone": "1.3.3",
"backbone-undo": "^0.2.5",
"cash-dom": "^2.3.9",
@ -28,11 +28,12 @@
"underscore": "^1.9.1"
},
"devDependencies": {
"@babel/cli": "^7.7.0",
"@babel/core": "^7.7.0",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.7.1",
"babel-loader": "^8.0.6",
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.0",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"@vuepress/plugin-google-analytics": "^1.4.1",
"babel-loader": "^8.1.0",
"documentation": "^8.1.2",
"eslint": "^6.6.0",
"html-webpack-plugin": "^3.2.0",
@ -44,7 +45,7 @@
"prettier": "^1.18.2",
"sinon": "^7.5.0",
"string-replace-loader": "^2.2.0",
"vuepress": "^0.10.2",
"vuepress": "^1.4.1",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",

2
src/block_manager/model/Block.js

@ -11,6 +11,8 @@ export default Backbone.Model.extend({
resetId: 0,
// Block label
label: '',
// Disable the drag of the block
disable: 0,
// HTML string for the media of the block, eg. SVG icon, image, etc.
media: '',
content: '',

14
src/block_manager/view/BlockView.js

@ -60,9 +60,11 @@ export default Backbone.View.extend({
* @private
*/
startDrag(e) {
const { config, em } = this;
const { config, em, model } = this;
const disable = model.get('disable');
//Right or middel click
if (e.button !== 0 || !config.getSorter || this.el.draggable) return;
if (e.button !== 0 || !config.getSorter || this.el.draggable || disable)
return;
em.refreshCanvas();
const sorter = config.getSorter();
sorter.setDragHelper(this.el, e);
@ -138,18 +140,22 @@ export default Backbone.View.extend({
render() {
const { em, el, ppfx, model } = this;
const disable = model.get('disable');
const attr = model.get('attributes') || {};
const cls = attr.class || '';
const className = `${ppfx}block`;
const label =
(em && em.t(`blockManager.labels.${model.id}`)) || model.get('label');
const render = model.get('render');
const media = model.get('media');
el.className += ` ${className} ${ppfx}one-bg ${ppfx}four-color-h`;
const clsAdd = disable ? `${className}--disable` : `${ppfx}four-color-h`;
el.className = `${cls} ${className} ${ppfx}one-bg ${clsAdd}`.trim();
el.innerHTML = `
${media ? `<div class="${className}__media">${media}</div>` : ''}
<div class="${className}-label">${label}</div>
`;
el.title = el.textContent.trim();
hasDnd(em) && el.setAttribute('draggable', true);
el.setAttribute('draggable', hasDnd(em) && !disable ? true : false);
const result = render && render({ el, model, className, prefix: ppfx });
if (result) el.innerHTML = result;
return this;

4
src/canvas/index.js

@ -124,8 +124,8 @@ export default () => {
return CanvasView.el;
},
getFrame() {
return canvas.get('frame');
getFrame(index) {
return index ? this.getFrames()[index] : canvas.get('frame');
},
/**

4
src/canvas/model/Canvas.js

@ -16,7 +16,9 @@ export default Backbone.Model.extend({
initialize(config = {}) {
const { em } = config;
const { styles = [], scripts = [] } = config;
const frame = new Frame({}, config);
const root = em && em.getWrapper();
const css = em && em.getStyle();
const frame = new Frame({ root, styles: css }, config);
styles.forEach(style => frame.addLink(style));
scripts.forEach(script => frame.addScript(script));
this.em = em;

16
src/canvas/view/CanvasView.js

@ -27,7 +27,7 @@ export default Backbone.View.extend({
},
initialize(o) {
bindAll(this, 'clearOff', 'onKeyPress');
bindAll(this, 'clearOff', 'onKeyPress', 'onCanvasMove');
on(window, 'scroll resize', this.clearOff);
const { model } = this;
const frames = model.get('frames');
@ -74,10 +74,18 @@ export default Backbone.View.extend({
}
},
onCanvasMove(ev) {
// const data = { x: ev.clientX, y: ev.clientY };
// const data2 = this.em.get('Canvas').getMouseRelativeCanvas(ev);
// const data3 = this.em.get('Canvas').getMouseRelativePos(ev);
// this.em.trigger('canvas:over', data, data2, data3);
},
toggleListeners(enable) {
const method = enable ? 'on' : 'off';
const methods = { on, off };
methods[method](document, 'keypress', this.onKeyPress);
const { el } = this;
const fn = enable ? on : off;
fn(document, 'keypress', this.onKeyPress);
// fn(el, 'mousemove dragover', this.onCanvasMove);
},
onKeyPress(ev) {

1
src/canvas/view/FrameView.js

@ -385,6 +385,7 @@ export default Backbone.View.extend({
// I need to delegate all events to the parent document
[
{ event: 'keydown keyup keypress', class: 'KeyboardEvent' },
{ event: 'mousemove', class: 'MouseEvent' },
{ event: 'wheel', class: 'WheelEvent' }
].forEach(obj =>
obj.event.split(' ').forEach(event => {

3
src/commands/view/OpenStyleManager.js

@ -1,6 +1,4 @@
import Backbone from 'backbone';
import StyleManager from 'style_manager';
const $ = Backbone.$;
export default {
@ -27,7 +25,6 @@ export default {
// Class Manager container
var clm = em.SelectorManager;
if (clm) this.$cn2.append(clm.render([]));
this.$cn2.append(em.StyleManager.render());
var smConfig = em.StyleManager.getConfig();
const pfx = smConfig.stylePrefix;

19
src/commands/view/Preview.js

@ -1,5 +1,7 @@
import { each } from 'underscore';
const cmdVis = 'sw-visibility';
export default {
getPanels(editor) {
if (!this.panels) {
@ -18,7 +20,11 @@ export default {
run(editor, sender) {
this.sender = sender;
editor.stopCommand('sw-visibility');
if (!this.shouldRunSwVisibility) {
this.shouldRunSwVisibility = editor.Commands.isActive(cmdVis);
}
this.shouldRunSwVisibility && editor.stopCommand(cmdVis);
editor.getModel().stopDefault();
const panels = this.getPanels(editor);
@ -54,17 +60,12 @@ export default {
sender.set && sender.set('active', 0);
const panels = this.getPanels(editor);
const swVisibilityButton = editor.Panels.getButton(
'options',
'sw-visibility'
);
if (swVisibilityButton && swVisibilityButton.get('active')) {
editor.runCommand('sw-visibility');
if (this.shouldRunSwVisibility) {
editor.runCommand(cmdVis);
this.shouldRunSwVisibility = false;
}
editor.getModel().runDefault();
panels.forEach(panel => panel.set('visible', true));
const canvas = editor.Canvas.getElement();

14
src/commands/view/SelectComponent.js

@ -85,7 +85,7 @@ export default {
em[method]('component:toggled', this.onSelect, this);
em[method]('change:componentHovered', this.onHovered, this);
em[method](
'component:resize component:styleUpdate',
'component:resize component:styleUpdate component:input',
this.updateGlobalPos,
this
);
@ -95,7 +95,7 @@ export default {
.getFrames()
.forEach(frame => {
const { view } = frame;
trigger(view.getWindow(), view.getBody());
view && trigger(view.getWindow(), view.getBody());
});
},
@ -204,15 +204,16 @@ export default {
this.currentDoc = null;
this.em.setHovered(0);
this.canvas.getFrames().forEach(frame => {
const el = frame.view.getToolsEl();
this.toggleToolsEl(0, 0, { el });
const { view } = frame;
const el = view && view.getToolsEl();
el && this.toggleToolsEl(0, 0, { el });
});
},
toggleToolsEl(on, view, opts = {}) {
const el = opts.el || this.canvas.getToolsEl(view);
el.style.opacity = on ? 1 : 0;
return el;
el && (el.style.opacity = on ? 1 : 0);
return el || {};
},
/**
@ -780,5 +781,6 @@ export default {
this.onOut();
this.toggleToolsEl();
editor && editor.stopCommand('resize');
this.editor = 0;
}
};

11
src/commands/view/SwitchVisibility.js

@ -8,9 +8,12 @@ export default {
},
toggleVis(ed, active = 1) {
const method = active ? 'add' : 'remove';
ed.Canvas.getFrames().forEach(frame => {
frame.view.getBody().classList[method](`${this.ppfx}dashed`);
});
if (!ed.Commands.isActive('preview')) {
const method = active ? 'add' : 'remove';
ed.Canvas.getFrames().forEach(frame => {
frame.view.getBody().classList[method](`${this.ppfx}dashed`);
});
}
}
};

21
src/css_composer/model/CssRules.js

@ -2,23 +2,20 @@ import Backbone from 'backbone';
import CssRule from './CssRule';
export default Backbone.Collection.extend({
model: CssRule,
initialize(models, opt) {
// Inject editor
if (opt && opt.em) this.editor = opt.em;
// Not used
this.model = (attrs, options) => {
var model;
if (!options.em && opt && opt.em) options.em = opt.em;
switch (1) {
default:
model = new CssRule(attrs, options);
}
// This will put the listener post CssComposer.postLoad
setTimeout(() => this.on('remove', this.onRemove));
},
return model;
};
onRemove(removed) {
const em = this.editor;
em.stopListening(removed);
em.get('UndoManager').remove(removed);
},
add(models, opt = {}) {

4
src/dom_components/index.js

@ -727,6 +727,10 @@ export default () => {
});
model && isEmpty(model.get('status')) && model.set('status', state);
},
allById() {
return componentsById;
}
};
};

55
src/dom_components/model/Component.js

@ -266,20 +266,20 @@ const Component = Backbone.Model.extend(Styleable).extend(
},
/**
* Find all inner components by component id.
* Find all inner components by component type.
* The advantage of this method over `find` is that you can use it
* also before rendering the component
* @param {String} id Component id
* @param {String} type Component type
* @returns {Array<Component>}
* @example
* const allImages = component.findType('image');
* console.log(allImages[0]) // prints the first found component
*/
findType(id) {
findType(type) {
const result = [];
const find = components =>
components.forEach(item => {
item.is(id) && result.push(item);
item.is(type) && result.push(item);
find(item.components());
});
find(this.components());
@ -300,6 +300,26 @@ const Component = Backbone.Model.extend(Styleable).extend(
return result.length && result.data('model');
},
/**
* Find the closest parent component by its type.
* The advantage of this method over `closest` is that you can use it
* also before rendering the component
* @param {String} type Component type
* @returns {Component} Found component, otherwise `undefined`
* @example
* const Section = component.closestType('section');
* console.log(Section);
*/
closestType(type) {
let parent = this.parent();
while (parent && !parent.is(type)) {
parent = parent.parent();
}
return parent;
},
/**
* Once the tag is updated I have to remove the node and replace it
* @private
@ -625,6 +645,15 @@ const Component = Backbone.Model.extend(Styleable).extend(
}
},
/**
* Remove all inner components
* * @return {this}
*/
empty(opts = {}) {
this.components().reset(null, opts);
return this;
},
/**
* Get the parent component, if exists
* @return {Component}
@ -1223,6 +1252,24 @@ const Component = Backbone.Model.extend(Styleable).extend(
return { tagName: el.tagName ? el.tagName.toLowerCase() : '' };
},
ensureInList(model) {
const list = Component.getList(model);
const id = model.getId();
const current = list[id];
if (!current) {
// Insert in list
list[id] = model;
} else if (current !== model) {
// Create new ID
const nextId = Component.getIncrementId(id, list);
model.setId(nextId);
list[nextId] = model;
}
model.components().forEach(i => Component.ensureInList(i));
},
/**
* Relying simply on the number of components becomes a problem when you
* store and load them back, you might hit collisions with new components

21
src/dom_components/model/ComponentTableRow.js

@ -4,31 +4,12 @@ export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
type: 'row',
tagName: 'tr',
draggable: ['thead', 'tbody', 'tfoot'],
droppable: ['th', 'td']
},
initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments);
// Clean the row from non cell components
const cells = [];
const components = this.get('components');
components.each(model => model.is('cell') && cells.push(model));
components.reset(cells);
}
},
{
isComponent(el) {
let result = '';
if (el.tagName == 'TR') {
result = { type: 'row' };
}
return result;
}
isComponent: el => el.tagName == 'TR' && true
}
);

109
src/dom_components/model/Components.js

@ -7,39 +7,93 @@ export default Backbone.Collection.extend({
initialize(models, opt = {}) {
this.opt = opt;
this.listenTo(this, 'add', this.onAdd);
this.listenTo(this, 'remove', this.removeChildren);
this.listenTo(this, 'reset', this.resetChildren);
this.config = opt.config;
this.em = opt.em;
const { em } = this;
this.domc = opt.domc;
},
this.model = (attrs, options) => {
var model;
const df = opt.em.get('DomComponents').componentTypes;
options.em = opt.em;
options.config = opt.config;
options.componentTypes = df;
options.domc = opt.domc;
for (var it = 0; it < df.length; it++) {
var dfId = df[it].id;
if (dfId == attrs.type) {
model = df[it].model;
break;
}
resetChildren(models, opts = {}) {
const coll = this;
const { previousModels = [] } = opts;
previousModels.forEach(md => this.removeChildren(md, coll, opts));
models.each(model => this.onAdd(model));
},
removeChildren(removed, coll, opts = {}) {
const { domc, em } = this;
const allByID = domc ? domc.allById() : {};
if (!opts.temporary) {
// Remove the component from the gloabl list
const id = removed.getId();
const sels = em.get('SelectorManager').getAll();
const rules = em.get('CssComposer').getAll();
delete allByID[id];
// Remove all component related styles
const rulesRemoved = rules.remove(
rules.filter(r => r.getSelectors().getFullString() === `#${id}`)
);
// Clean selectors
sels.remove(rulesRemoved.map(rule => rule.getSelectors().at(0)));
if (!removed.opt.temporary) {
const cm = em.get('Commands');
const hasSign = removed.get('style-signature');
const optStyle = { target: removed };
hasSign && cm.run('core:component-style-clear', optStyle);
removed.removed();
em.trigger('component:remove', removed);
}
if (!model) {
// get the last one
model = df[df.length - 1].model;
em &&
attrs.type &&
em.logWarning(`Component type '${attrs.type}' not found`, {
attrs,
options
});
const inner = removed.components();
inner.forEach(it => this.removeChildren(it, coll, opts));
// removed.empty(opts);
}
// Remove stuff registered in DomComponents.handleChanges
const inner = removed.components();
const um = em.get('UndoManager');
em.stopListening(inner);
em.stopListening(removed);
em.stopListening(removed.get('classes'));
um.remove(removed);
um.remove(inner);
},
model(attrs, options) {
const { opt } = options.collection;
const { em } = opt;
let model;
const df = em.get('DomComponents').componentTypes;
options.em = em;
options.config = opt.config;
options.componentTypes = df;
options.domc = opt.domc;
for (let it = 0; it < df.length; it++) {
const dfId = df[it].id;
if (dfId == attrs.type) {
model = df[it].model;
break;
}
}
// If no model found, get the default one
if (!model) {
model = df[df.length - 1].model;
em &&
attrs.type &&
em.logWarning(`Component type '${attrs.type}' not found`, {
attrs,
options
});
}
return new model(attrs, options);
};
return new model(attrs, options);
},
parseString(value, opt = {}) {
@ -135,9 +189,10 @@ export default Backbone.Collection.extend({
},
onAdd(model, c, opts = {}) {
const em = this.em;
const { domc, em } = this;
const style = model.getStyle();
const avoidInline = em && em.getConfig('avoidInlineStyle');
domc && domc.Component.ensureInList(model);
if (
!isEmpty(style) &&

25
src/dom_components/view/ComponentTextView.js

@ -137,15 +137,38 @@ export default ComponentView.extend({
}
},
getModelsFromEl(el) {
const result = [];
const children = (el || this.el).childNodes;
for (let index = 0; index < children.length; index++) {
const child = children[index];
const model = child.__cashData && child.__cashData.model;
if (model) {
model.components = this.getModelsFromEl(child);
if (model.get('content')) {
model.attributes.content = child.textContent;
}
// TODO add attributes;
result.push(model);
}
}
return result;
},
/**
* Callback on input event
* @param {Event} e
*/
onInput() {
const { em } = this;
const evPfx = 'component';
const ev = [`${evPfx}:update`, `${evPfx}:input`].join(' ');
// Update toolbars
em && em.trigger('component:update', this.model);
em && em.trigger(ev, this.model);
},
/**

34
src/dom_components/view/ComponentView.js

@ -87,18 +87,27 @@ export default Backbone.View.extend({
onDisable() {},
remove() {
Backbone.View.prototype.remove.apply(this, arguments);
const { model } = this;
const frame = this._getFrame() || {};
const view = this;
Backbone.View.prototype.remove.apply(view, arguments);
const { model } = view;
const frame = view._getFrame() || {};
const frameM = frame.model;
model.components().forEach(comp => {
const view = comp.getView(frameM);
view && view.remove();
});
const { views } = model;
views.splice(views.indexOf(this), 1);
this.removed(this._clbObj());
return this;
views.splice(views.indexOf(view), 1);
view.removed(view._clbObj());
view.$el.data({ model: '', collection: '', view: '' });
delete view.model;
delete view.$el;
delete view.el.__gjsv;
delete view.childrenView;
delete view.scriptContainer;
delete view.opts;
// delete view.el;
return view;
},
handleDragStart(event) {
@ -474,11 +483,13 @@ export default Backbone.View.extend({
renderChildren() {
this.updateContent();
const container = this.getChildrenContainer();
const view = new ComponentsView({
collection: this.model.get('components'),
config: this.config,
componentTypes: this.opts.componentTypes
});
const view =
this.childrenView ||
new ComponentsView({
collection: this.model.get('components'),
config: this.config,
componentTypes: this.opts.componentTypes
});
view.render(container);
this.childrenView = view;
@ -499,6 +510,7 @@ export default Backbone.View.extend({
if (this.modelOpt.temporary) return this;
this.renderChildren();
this.updateScript();
setViewEl(this.el, this);
this.postRender();
return this;

53
src/dom_components/view/ComponentsView.js

@ -13,43 +13,16 @@ export default Backbone.View.extend({
},
removeChildren(removed, coll, opts = {}) {
const { em } = this.config;
const tempRemove = opts.temporary;
removed.views.forEach(view => {
if (!view) return;
view.remove.apply(view);
const { childrenView, scriptContainer } = view;
childrenView && childrenView.stopListening();
scriptContainer && scriptContainer.remove();
view.remove.apply(view);
});
removed.components().forEach(it => this.removeChildren(it, coll, opts));
if (em && !tempRemove) {
// Remove the component from the global list
const id = removed.getId();
const domc = em.get('DomComponents');
delete domc.componentsById[id];
// Remove all related CSS rules
// TODO: remove from the frame container
const allRules = em.get('CssComposer').getAll();
allRules.remove(
allRules.filter(
rule => rule.getSelectors().getFullString() === `#${id}`
)
);
if (!removed.opt.temporary) {
const cm = em.get('Commands');
const hasSign = removed.get('style-signature');
const optStyle = { target: removed };
hasSign && cm.run('core:component-style-clear', optStyle);
removed.removed();
em.trigger('component:remove', removed);
}
}
const inner = removed.components();
inner.forEach(it => this.removeChildren(it, coll, opts));
},
/**
@ -86,6 +59,8 @@ export default Backbone.View.extend({
if (!this.compView) this.compView = require('./ComponentView').default;
const { config, opts, em } = this;
const fragment = fragmentEl || null;
const { frameView = {} } = config;
const sameFrameView = frameView.model && model.getView(frameView.model);
const dt =
opts.componentTypes || (em && em.get('DomComponents').getTypes());
const type = model.get('type');
@ -97,12 +72,13 @@ export default Backbone.View.extend({
break;
}
}
const view = new viewObject({
model,
config,
componentTypes: dt
});
const view =
sameFrameView ||
new viewObject({
model,
config,
componentTypes: dt
});
let rendered = view.render().el;
if (fragment) {
@ -134,9 +110,10 @@ export default Backbone.View.extend({
return rendered;
},
resetChildren() {
resetChildren(models, { previousModels = [] } = {}) {
this.parentEl.innerHTML = '';
this.collection.each(model => this.addToCollection(model));
previousModels.forEach(md => this.removeChildren(md, this.collection));
models.each(model => this.addToCollection(model));
},
render(parent) {

15
src/domain_abstract/ui/InputColor.js

@ -44,7 +44,7 @@ export default Input.extend({
colorEl.get(0).style.backgroundColor = valueClr;
// This prevents from adding multiple thumbs in spectrum
if (opts.fromTarget) {
if (opts.fromTarget || (opts.fromInput && !opts.avoidStore)) {
colorEl.spectrum('set', valueClr);
this.noneColor = value == 'none';
}
@ -56,16 +56,16 @@ export default Input.extend({
*/
getColorEl() {
if (!this.colorEl) {
const { em } = this;
const self = this;
const ppfx = this.ppfx;
var model = this.model;
var colorEl = $(`<div class="${this.ppfx}field-color-picker"></div>`);
var cpStyle = colorEl.get(0).style;
var elToAppend = this.em && this.em.config ? this.em.config.el : '';
var elToAppend = em && em.config ? em.config.el : '';
var colorPickerConfig =
(this.em && this.em.getConfig && this.em.getConfig('colorPicker')) ||
{};
(em && em.getConfig && em.getConfig('colorPicker')) || {};
const getColor = color => {
let cl =
color.getAlpha() == 1 ? color.toHexString() : color.toRgbString();
@ -117,6 +117,13 @@ export default Input.extend({
}
});
em &&
em.on &&
em.on('component:selected', () => {
changed = 1;
colorEl.spectrum('hide');
});
this.colorEl = colorEl;
}
return this.colorEl;

23
src/domain_abstract/view/DomainViews.js

@ -14,9 +14,8 @@ export default Backbone.View.extend({
initialize(opts = {}, config) {
this.config = config || opts.config || {};
this.autoAdd && this.listenTo(this.collection, 'add', this.addTo);
this.items = [];
this.init();
},
@ -45,7 +44,7 @@ export default Backbone.View.extend({
* @private
* */
add(model, fragment) {
const { config, reuseView, itemsView = {} } = this;
const { config, reuseView, items, itemsView = {} } = this;
const inputTypes = [
'button',
'checkbox',
@ -91,7 +90,8 @@ export default Backbone.View.extend({
view = new itemView({ model, config }, config);
}
var rendered = view.render().el;
items && items.push(view);
const rendered = view.render().el;
if (frag) frag.appendChild(rendered);
else this.$el.append(rendered);
@ -99,6 +99,7 @@ export default Backbone.View.extend({
render() {
var frag = document.createDocumentFragment();
this.clearItems();
this.$el.empty();
if (this.collection.length)
@ -111,5 +112,17 @@ export default Backbone.View.extend({
return this;
},
onRender() {}
onRender() {},
remove() {
this.clearItems();
Backbone.View.prototype.remove.apply(this, arguments);
},
clearItems() {
const items = this.items || [];
// TODO Traits do not update the target anymore
// items.forEach(item => item.remove());
// this.items = [];
}
});

177
src/editor/index.js

@ -131,145 +131,54 @@ export default (config = {}) => {
*/
editor: em,
/**
* @property {I18n}
* @private
*/
I18n: em.get('I18n'),
/**
* @property {DomComponents}
* @private
*/
DomComponents: em.get('DomComponents'),
/**
* @property {LayerManager}
* @private
*/
LayerManager: em.get('LayerManager'),
/**
* @property {CssComposer}
* @private
*/
CssComposer: em.get('CssComposer'),
/**
* @property {StorageManager}
* @private
*/
StorageManager: em.get('StorageManager'),
/**
* @property {AssetManager}
* @private
*/
AssetManager: em.get('AssetManager'),
/**
* @property {BlockManager}
* @private
*/
BlockManager: em.get('BlockManager'),
/**
* @property {TraitManager}
* @private
*/
TraitManager: em.get('TraitManager'),
/**
* @property {SelectorManager}
* @private
*/
SelectorManager: em.get('SelectorManager'),
/**
* @property {CodeManager}
* @private
*/
CodeManager: em.get('CodeManager'),
/**
* @property {Commands}
* @private
*/
Commands: em.get('Commands'),
/**
* @property {Keymaps}
* @private
*/
Keymaps: em.get('Keymaps'),
/**
* @property {Modal}
* @private
*/
Modal: em.get('Modal'),
/**
* @property {Panels}
* @private
*/
Panels: em.get('Panels'),
/**
* @property {StyleManager}
* @private
*/
StyleManager: em.get('StyleManager'),
/**
* @property {Canvas}
* @private
*/
Canvas: em.get('Canvas'),
/**
* @property {UndoManager}
* @private
*/
UndoManager: em.get('UndoManager'),
/**
* @property {DeviceManager}
* @private
*/
DeviceManager: em.get('DeviceManager'),
/**
* @property {RichTextEditor}
* @private
*/
RichTextEditor: em.get('RichTextEditor'),
/**
* @property {Parser}
* @private
*/
Parser: em.get('Parser'),
/**
* @property {Utils}
* @private
*/
Utils: em.get('Utils'),
/**
* @property {Utils}
* @private
*/
Config: em.get('Config'),
/**
* Initialize editor model
* @return {this}
* @private
*/
init() {
em.init(this);
init(opts = {}) {
em.init(this, { ...c, ...opts });
[
'I18n',
'Utils',
'Config',
'Commands',
'Keymaps',
'Modal',
'Panels',
'Canvas',
'Parser',
'CodeManager',
'UndoManager',
'RichTextEditor',
'DomComponents',
['Components', 'DomComponents'],
'LayerManager',
['Layers', 'LayerManager'],
'CssComposer',
['Css', 'CssComposer'],
'StorageManager',
['Storage', 'StorageManager'],
'AssetManager',
['Assets', 'AssetManager'],
'BlockManager',
['Blocks', 'BlockManager'],
'TraitManager',
['Traits', 'TraitManager'],
'SelectorManager',
['Selectors', 'SelectorManager'],
'StyleManager',
['Styles', 'StyleManager'],
'DeviceManager',
['Devices', 'DeviceManager']
].forEach(prop => {
if (Array.isArray(prop)) {
this[prop[0]] = em.get(prop[1]);
} else {
this[prop] = em.get(prop);
}
});
// Do post render stuff after the iframe is loaded otherwise it'll
// be empty during tests

19
src/editor/model/Editor.js

@ -77,6 +77,7 @@ export default Backbone.Model.extend({
this.set('modules', []);
this.set('toLoad', []);
this.set('storables', []);
this.set('selected', new Collection());
this.set('dmode', c.dragMode);
const el = c.el;
const log = c.log;
@ -224,7 +225,11 @@ export default Backbone.Model.extend({
* @return {this}
* @private
*/
init(editor) {
init(editor, opts = {}) {
if (this.destroyed) {
this.initialize(opts);
this.destroyed = 0;
}
this.set('Editor', editor);
},
@ -592,7 +597,8 @@ export default Backbone.Model.extend({
* @private
*/
stopDefault(opts = {}) {
var command = this.get('Commands').get(this.config.defaultCommand);
const commands = this.get('Commands');
const command = commands.get(this.config.defaultCommand);
if (!command) return;
command.stop(this, this, opts);
this.defaultRunning = 0;
@ -688,6 +694,9 @@ export default Backbone.Model.extend({
* Destroy editor
*/
destroyAll() {
const { config } = this;
const editor = this.getEditor();
const { editors = [] } = config.grapesjs || {};
const {
DomComponents,
CssComposer,
@ -697,6 +706,7 @@ export default Backbone.Model.extend({
Keymaps,
RichTextEditor
} = this.attributes;
this.stopDefault();
DomComponents.clear();
CssComposer.clear();
UndoManager.clear().removeAll();
@ -706,7 +716,10 @@ export default Backbone.Model.extend({
RichTextEditor.destroy();
this.view.remove();
this.stopListening();
$(this.config.el)
this.clear({ silent: true });
this.destroyed = 1;
editors.splice(editors.indexOf(editor), 1);
$(config.el)
.empty()
.attr(this.attrsOrig);
},

6
src/i18n/locale/nl → src/i18n/locale/nl.js

@ -134,9 +134,9 @@ export default {
'box-shadow-h': 'Box schaduw: Horizontaal',
'box-shadow-v': 'Box schaduw: Verticaal',
'box-shadow-blur': 'Box schaduw: Vervagen',
'box-shadow-spread': "Box schaduw: Verspreiding",
'box-shadow-color': "Box schaduw: Kleur",
'box-shadow-type': "Box schaduw: Type",
'box-shadow-spread': 'Box schaduw: Verspreiding',
'box-shadow-color': 'Box schaduw: Kleur',
'box-shadow-type': 'Box schaduw: Type',
background: 'Achtergrond',
'background-image': 'Achtergrond afbeelding',
'background-repeat': 'Achtergrond herhalen',

200
src/i18n/locale/pt.js

@ -0,0 +1,200 @@
const traitInputAttr = { placeholder: 'ex: Insira o texto' };
export default {
assetManager: {
addButton: 'Adicionar imagem',
inputPlh: 'http://caminho/para/a/imagem.jpg',
modalTitle: 'Selecionar imagem',
uploadTitle: 'Solte os arquivos aqui ou clique para enviar'
},
// 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: 'Corpo',
text: 'Texto',
comment: 'Comentário',
image: 'Imagem',
video: 'Vídeo',
label: 'Label',
link: 'Link',
map: 'Mapa',
tfoot: 'Rodapé da tabela',
tbody: 'Corpo da tabela',
thead: 'Cabeçalho da tabela',
table: 'Tabela',
row: 'Linha da tabela',
cell: 'Célula da tabela',
section: 'Seção',
body: 'Corpo'
}
},
deviceManager: {
device: 'Dispositivo',
devices: {
desktop: 'Desktop',
tablet: 'Tablet',
mobileLandscape: 'Celular, modo panorâmico',
mobilePortrait: 'Celular, modo retrato'
}
},
panels: {
buttons: {
titles: {
preview: 'Pré-visualização',
fullscreen: 'Tela cheia',
'sw-visibility': 'Ver componentes',
'export-template': 'Ver código',
'open-sm': 'Abrir gerenciador de estilos',
'open-tm': 'Configurações',
'open-layers': 'Abrir gerenciador de camadas',
'open-blocks': 'Abrir blocos'
}
}
},
selectorManager: {
label: 'Classes',
selected: 'Selecionado',
emptyState: '- Estado -',
states: {
hover: 'Hover',
active: 'Click',
'nth-of-type(2n)': 'Even/Odd'
}
},
styleManager: {
empty: 'Selecione um elemento para usar o gerenciador de estilos',
layer: 'Camada',
fileButton: 'Imagens',
sectors: {
general: 'Geral',
layout: 'Disposição',
typography: 'Tipografia',
decorations: 'Decorações',
extra: 'Extra',
flex: 'Flex',
dimension: 'Dimensão'
},
// The core library generates the name by their `property` name
properties: {
float: 'Float',
display: 'Exibição',
position: 'Posição',
top: 'Topo',
right: 'Direita',
left: 'Esquerda',
bottom: 'Inferior',
width: 'Largura',
height: 'Altura',
'max-width': 'Largura Max.',
'max-height': 'Altura Max.',
margin: 'Margem',
'margin-top': 'Margem Superior',
'margin-right': 'Margem a Direita',
'margin-left': 'Margem a Esquerda',
'margin-bottom': 'Margem Inferior',
padding: 'Padding',
'padding-top': 'Padding Superior',
'padding-left': 'Padding a Esquerda',
'padding-right': 'Padding a Direita',
'padding-bottom': 'Padding Inferior',
'font-family': 'Tipo de letra',
'font-size': 'Tamanho da fonte',
'font-weight': 'Espessura da fonte',
'letter-spacing': 'Espaço entre letras',
color: 'Cor',
'line-height': 'Altura da linha',
'text-align': 'Alinhamento do texto',
'text-shadow': 'Sombra do texto',
'text-shadow-h': 'Sombra do texto: horizontal',
'text-shadow-v': 'Sombra do texto: vertical',
'text-shadow-blur': 'Desfoque da sombra do texto',
'text-shadow-color': 'Cor da sombra da fonte',
'border-top-left': 'Borda superior a esquerda',
'border-top-right': 'Borda superior a direita',
'border-bottom-left': 'Borda inferior a esquerda',
'border-bottom-right': 'Borda inferior a direita',
'border-radius-top-left': 'Raio da borda superior esquerda',
'border-radius-top-right': 'Raio da borda superior direita',
'border-radius-bottom-left': 'Raio da borda inferior esquerda',
'border-radius-bottom-right': 'Raio da borda inferior direita',
'border-radius': 'Raio da borda',
border: 'Borda',
'border-width': 'Largura da borda',
'border-style': 'Estilo da borda',
'border-color': 'Cor da borda',
'box-shadow': 'Sombra da box',
'box-shadow-h': 'Sombra da box: horizontal',
'box-shadow-v': 'Sombra da box: vertical',
'box-shadow-blur': 'Desfoque da sombra da box',
'box-shadow-spread': 'Extensão da sombra da box',
'box-shadow-color': 'Cor da sombra da box',
'box-shadow-type': 'Tipo de sombra da box',
background: 'Fundo',
'background-color': 'Cor de fundo',
'background-image': 'Imagem de fundo',
'background-repeat': 'Repetir fundo',
'background-position': 'Posição do fundo',
'background-attachment': 'Plugin de fundo',
'background-size': 'Tamanho do fundo',
transition: 'Transição',
'transition-property': 'Tipo de transição',
'transition-duration': 'Tempo de transição',
'transition-timing-function': 'Função do tempo da transição',
perspective: 'Perspectiva',
transform: 'Transformação',
'transform-rotate-x': 'Rotacionar horizontalmente',
'transform-rotate-y': 'Rotacionar verticalmente',
'transform-rotate-z': 'Rotacionar profundidade',
'transform-scale-x': 'Escalar horizontalmente',
'transform-scale-y': 'Escalar verticalmente',
'transform-scale-z': 'Escalar profundidade',
'flex-direction': 'Direção Flex',
'flex-wrap': 'Flex wrap',
'justify-content': 'Ajustar conteúdo',
'align-items': 'Alinhar elementos',
'align-content': 'Alinhar conteúdo',
order: 'Ordem',
'flex-basis': 'Base Flex',
'flex-grow': 'Crescimento Flex',
'flex-shrink': 'Contração Flex',
'align-self': 'Alinhar-se',
}
},
traitManager: {
empty: 'Selecione um elemento para usar o gerenciador de características',
label: 'Configurações do componente',
traits: {
// The core library generates the name by their `name` property
labels: {
// id: 'Id',
// alt: 'Alt',
// title: 'Title',
// href: 'Href',
},
// In a simple trait, like text input, these are used on input attributes
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'ex: https://google.com' }
},
// In a trait like select, these are used to translate option names
options: {
target: {
false: 'Esta janela',
_blank: 'Nova janela'
}
}
}
}
};

2
src/index.js

@ -47,7 +47,7 @@ export default {
init(config = {}) {
const els = config.container;
if (!els) throw new Error("'container' is required");
config = { ...defaultConfig, ...config };
config = { ...defaultConfig, ...config, grapesjs: this };
config.el = isElement(els) ? els : document.querySelector(els);
const editor = new Editor(config).init();

3
src/navigator/view/ItemsView.js

@ -52,7 +52,8 @@ export default Backbone.View.extend({
removeChildren(removed) {
const view = removed.viewLayer;
if (!view) return;
view.remove.apply(view);
view.remove();
removed.viewLayer = 0;
},
/**

3
src/style_manager/index.js

@ -82,7 +82,8 @@ export default () => {
},
onLoad() {
sectors.add(c.sectors);
// Use silent as sectors' view will be created and rendered on StyleManager.render
sectors.add(c.sectors, { silent: true });
},
postRender() {

1
src/style_manager/model/Layers.js

@ -64,7 +64,6 @@ export default Backbone.Collection.extend({
getLayersFromStyle(styleObj) {
const layers = [];
const properties = this.properties;
const propNames = properties.pluck('property');
properties.each(propModel => {
const style = styleObj[propModel.get('property')];

4
src/style_manager/model/Property.js

@ -73,7 +73,9 @@ const Property = Backbone.Model.extend(
*/
setValue(value, complete = 1, opts = {}) {
const parsed = this.parseValue(value);
this.set(parsed, { ...opts, avoidStore: !complete });
const avoidStore = !complete;
!avoidStore && this.set({ value: '' }, { avoidStore, silent: true });
this.set(parsed, { avoidStore, ...opts });
},
/**

19
src/style_manager/model/PropertyStack.js

@ -1,3 +1,4 @@
import { keys } from 'underscore';
import Property from './PropertyComposite';
import Layers from './Layers';
@ -49,6 +50,24 @@ export default Property.extend({
return Property.prototype.clearValue.apply(this, arguments);
},
getValueFromTarget(target) {
const { detached, property, properties } = this.attributes;
const style = target.getStyle();
const validStyles = {};
properties.forEach(prop => {
const name = prop.get('property');
const value = style[name];
if (value) validStyles[name] = value;
});
return !detached
? style[property]
: keys(validStyles).length
? validStyles
: '';
},
/**
* This method allows to customize layers returned from the target
* @param {Object} target

28
src/style_manager/view/LayerView.js

@ -5,7 +5,7 @@ import PropertiesView from './PropertiesView';
export default Backbone.View.extend({
events: {
click: 'active',
'click [data-close-layer]': 'remove',
'click [data-close-layer]': 'removeItem',
'mousedown [data-move-layer]': 'initSorter',
'touchstart [data-move-layer]': 'initSorter'
},
@ -58,23 +58,25 @@ export default Backbone.View.extend({
if (this.sorter) this.sorter.startSort(this.el);
},
remove(e) {
if (e && e.stopPropagation) e.stopPropagation();
removeItem(ev) {
ev && ev.stopPropagation();
this.remove();
},
const model = this.model;
const collection = model.collection;
remove(opts = {}) {
const { model, props } = this;
const coll = model.collection;
const stackModel = this.stackModel;
Backbone.View.prototype.remove.apply(this, arguments);
if (collection.contains(model)) {
collection.remove(model);
}
coll && coll.contains(model) && coll.remove(model);
if (stackModel && stackModel.set) {
stackModel.set({ stackIndex: null }, { silent: true });
stackModel.trigger('updateValue');
!opts.fromTarget && stackModel.trigger('updateValue');
}
props && props.remove();
},
/**
@ -172,11 +174,13 @@ export default Backbone.View.extend({
customValue: propsConfig.customValue,
propTarget: propsConfig.propTarget,
onChange: propsConfig.onChange
}).render().el;
});
const propsEl = properties.render().el;
el.innerHTML = this.template(model);
el.className = `${pfx}layer${!preview ? ` ${pfx}no-preview` : ''}`;
this.getPropertiesWrapper().appendChild(properties);
this.props = properties;
this.getPropertiesWrapper().appendChild(propsEl);
this.updateVisibility();
this.updatePreview();
return this;

23
src/style_manager/view/LayersView.js

@ -15,7 +15,8 @@ export default Backbone.View.extend({
this.className = `${pfx}layers ${ppfx}field`;
this.listenTo(collection, 'add', this.addTo);
this.listenTo(collection, 'deselectAll', this.deselectAll);
this.listenTo(collection, 'reset', this.render);
this.listenTo(collection, 'reset', this.reset);
this.items = [];
var em = this.config.em || '';
var utils = em ? em.get('Utils') : '';
@ -66,14 +67,15 @@ export default Backbone.View.extend({
model.set('preview', this.preview);
}
var view = new LayerView({
const view = new LayerView({
model,
config,
sorter,
stackModel,
propsConfig
});
var rendered = view.render().el;
const rendered = view.render().el;
this.items.push(view);
if (fragment) {
fragment.appendChild(rendered);
@ -109,6 +111,11 @@ export default Backbone.View.extend({
this.$el.find('.' + this.pfx + 'layer').removeClass(this.pfx + 'active');
},
reset(coll, opts) {
this.clearItems(opts);
this.render();
},
render() {
var fragment = document.createDocumentFragment();
this.$el.empty();
@ -123,5 +130,15 @@ export default Backbone.View.extend({
if (this.sorter) this.sorter.plh = null;
return this;
},
remove() {
this.clearItems();
Backbone.View.prototype.remove.apply(this, arguments);
},
clearItems(opts) {
this.items.forEach(item => item.remove(opts));
this.items = [];
}
});

12
src/style_manager/view/PropertiesView.js

@ -47,12 +47,22 @@ export default Backbone.View.extend({
render() {
const { $el } = this;
this.properties = [];
this.clearItems();
const fragment = document.createDocumentFragment();
this.collection.each(model => this.add(model, fragment));
$el.empty();
$el.append(fragment);
$el.attr('class', `${this.pfx}properties`);
return this;
},
remove() {
Backbone.View.prototype.remove.apply(this, arguments);
this.clearItems();
},
clearItems() {
this.properties.forEach(item => item.remove());
this.properties = [];
}
});

90
src/style_manager/view/PropertyStackView.js

@ -25,6 +25,24 @@ export default PropertyCompositeView.extend({
this.listenTo(model, 'change:stackIndex', this.indexChanged);
this.listenTo(model, 'updateValue', this.inputValueChanged);
this.delegateEvents();
const propsConfig = this.getPropsConfig();
this.layers = new LayersView({
collection: this.getLayers(),
stackModel: model,
preview: model.get('preview'),
config: this.config,
propsConfig
});
const PropertiesView = require('./PropertiesView').default;
this.propsView = new PropertiesView({
target: this.target,
collection: model.get('properties'),
stackModel: model,
config: this.config,
onChange: propsConfig.onChange,
propTarget: propsConfig.propTarget
});
},
/**
@ -33,15 +51,18 @@ export default PropertyCompositeView.extend({
* so we gonna check all props and find if it has any difference
* */
targetUpdated(...args) {
let data;
if (!this.model.get('detached')) {
PropertyCompositeView.prototype.targetUpdated.apply(this, args);
data = PropertyCompositeView.prototype.targetUpdated.apply(this, args);
} else {
const { status } = this._getTargetData();
this.setStatus(status);
data = this._getTargetData();
this.setStatus(data.status);
this.checkVisibility();
}
this.refreshLayers();
// I have to wait the update of inner properites (like visibility)
// before render layers
setTimeout(() => this.refreshLayers(data));
},
/**
@ -81,15 +102,15 @@ export default PropertyCompositeView.extend({
// In detached mode inputValueChanged will add new 'layer value'
// to all subprops
this.inputValueChanged();
this.inputValueChanged({ up: 1 });
// This will set subprops with a new default values
model.set('stackIndex', layers.indexOf(layer));
},
inputValueChanged() {
inputValueChanged(opts = {}) {
const model = this.model;
this.elementUpdated();
opts.up && this.elementUpdated();
// If not detached I'll just put all the values from layers to property
// eg. background: layer1Value, layer2Value, layer3Value, ...
@ -163,7 +184,7 @@ export default PropertyCompositeView.extend({
/**
* Refresh layers
* */
refreshLayers() {
refreshLayers(opts = {}) {
let layersObj = [];
const { model, em } = this;
const layers = this.getLayers();
@ -172,6 +193,7 @@ export default PropertyCompositeView.extend({
const target = this.getTarget();
const valueComput = this.getComputedValue();
const selected = em.getSelected();
const updateOpts = { fromTarget: 1 };
let resultValue,
style,
targetAlt,
@ -181,7 +203,7 @@ export default PropertyCompositeView.extend({
// With detached layers values will be assigned to their properties
if (detached) {
style = target ? target.getStyle() : {};
style = opts.targetValue || {};
const hasDetachedStyle = rule => {
const name = model
.get('properties')
@ -250,34 +272,37 @@ export default PropertyCompositeView.extend({
const toAdd =
model.getLayersFromTarget(target, { resultValue, layersObj }) ||
layersObj;
layers.reset();
layers.add(toAdd);
layers.reset(null, updateOpts);
layers.add(toAdd, updateOpts);
model.set({ stackIndex: null }, { silent: true });
},
getTargetValue(opts = {}) {
const { model } = this;
const { detached } = model.attributes;
const target = this.getTarget();
let result = PropertyCompositeView.prototype.getTargetValue.call(
this,
opts
);
const { detached } = this.model.attributes;
// It might happen that the browser split properties on CSSOM parse
if (isUndefined(result) && !detached) {
result = this.model.getValueFromStyle(this.getTarget().getStyle());
result = model.getValueFromStyle(target.getStyle());
} else if (detached) {
result = model.getValueFromTarget(target);
}
return result;
},
onRender() {
getPropsConfig() {
const self = this;
const model = this.model;
const fieldEl = this.el.querySelector('[data-layers-wrapper]');
const PropertiesView = require('./PropertiesView').default;
const propsConfig = {
target: this.target,
propTarget: this.propTarget,
const { model } = self;
return {
target: self.target,
propTarget: self.propTarget,
// Things to do when a single sub-property is changed
onChange(el, view, opt) {
@ -300,25 +325,12 @@ export default PropertyCompositeView.extend({
}
}
};
const layers = new LayersView({
collection: this.getLayers(),
stackModel: model,
preview: model.get('preview'),
config: this.config,
propsConfig
}).render().el;
// Will use it to propogate changes
new PropertiesView({
target: this.target,
collection: this.model.get('properties'),
stackModel: model,
config: this.config,
onChange: propsConfig.onChange,
propTarget: propsConfig.propTarget
}).render();
},
//model.get('properties')
fieldEl.appendChild(layers);
onRender() {
const { el, layers, propsView } = this;
const fieldEl = el.querySelector('[data-layers-wrapper]');
propsView.render(); // Will use it to propogate changes
fieldEl.appendChild(layers.render().el);
}
});

32
src/style_manager/view/PropertyView.js

@ -189,9 +189,9 @@ export default Backbone.View.extend({
* Triggers when the value of element input/s is changed, so have to update
* the value of the model which will propogate those changes to the target
*/
inputValueChanged(e) {
e && e.stopPropagation();
this.model.setValue(this.getInputValue(), 1, { fromInput: 1 });
inputValueChanged(ev) {
ev && ev.stopPropagation();
this.model.setValueFromInput(this.getInputValue());
this.elementUpdated();
},
@ -266,20 +266,22 @@ export default Backbone.View.extend({
const { model } = this;
const property = model.get('property');
const { status, value, ...targetData } = this._getTargetData();
const data = {
status,
value,
...targetData
};
this.setStatus(status);
model.setValue(value, 0, { fromTarget: 1, ...opts });
if (em) {
const data = {
status,
value,
...targetData
};
em.trigger('styleManager:change', this, property, value, data);
em.trigger(`styleManager:change:${property}`, this, value, data);
this._emitUpdate(data);
}
return data;
},
_emitUpdate(addData = {}) {
@ -405,7 +407,10 @@ export default Backbone.View.extend({
this.setValue(value);
}
this.getTargets().forEach(target => this.__updateTarget(target, opt));
// Avoid target update if the changes comes from it
if (!opt.fromTarget) {
this.getTargets().forEach(target => this.__updateTarget(target, opt));
}
},
__updateTarget(target, opt = {}) {
@ -464,6 +469,13 @@ export default Backbone.View.extend({
delete style[property];
}
// Forces to trigger the change (for UndoManager)
if (opts.avoidStore) {
style.__ = 1;
} else {
delete style.__;
}
target.setStyle(style, opts);
// Helper is used by `states` like ':hover' to show its preview
@ -631,6 +643,6 @@ export default Backbone.View.extend({
const onRender = this.onRender && this.onRender.bind(this);
onRender && onRender();
this.setValue(model.get('value'), { targetUpdate: 1 });
this.setValue(model.get('value'), { fromTarget: 1 });
}
});

2
src/style_manager/view/SectorsView.js

@ -44,7 +44,7 @@ export default Backbone.View.extend({
toggleStateCls(targets = [], enable) {
targets.forEach(trg => {
const el = trg.getEl();
el && el.classList[enable ? 'add' : 'remove'](helperCls);
el && el.classList && el.classList[enable ? 'add' : 'remove'](helperCls);
});
},

3
src/styles/scss/_gjs_variables.scss

@ -76,3 +76,6 @@ $fontName: 'main-fonts' !default;
$fontSize: 0.7rem !default;
$fontSizeS: 0.75rem !default;
$fontV: 20 !default;//random(1000)
/* Tools */
$placeholderColor: $colorGreen !default;

19
src/styles/scss/main.scss

@ -304,17 +304,32 @@ $colorsAll: (one, $primaryColor),
.#{$app-prefix}placeholder,
.#{$nv-prefix}placeholder {
border-style: solid !important;
border-color: $colorGreen;
outline: none;
box-sizing: border-box;
transition: top $animSpeed, left $animSpeed,
width $animSpeed, height $animSpeed;
}
.#{$app-prefix}placeholder.horizontal,
.#{$com-prefix}placeholder.horizontal,
.#{$nv-prefix}placeholder.horizontal {
border-color: transparent $placeholderColor;
border-width: 3px 5px;
margin: -3px 0 0;
}
.#{$app-prefix}placeholder.vertical,
.#{$com-prefix}placeholder.vertical,
.#{$nv-prefix}placeholder.vertical {
border-color: $placeholderColor transparent;
border-width: 5px 3px;
margin: 0 0 0 -3px;
}
.#{$app-prefix}placeholder-int,
.#{$com-prefix}placeholder-int,
.#{$nv-prefix}placeholder-int {
background-color: $colorGreen;
background-color: $placeholderColor;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
height: 100%; width: 100%;
pointer-events: none;

41
src/undo_manager/index.js

@ -31,7 +31,9 @@ export default () => {
let um;
let config;
let beforeCache;
const configDef = {};
const configDef = {
maximumStackLength: 500
};
return {
name: 'UndoManager',
@ -180,8 +182,8 @@ export default () => {
* @example
* um.undo();
*/
undo() {
!em.isEditing() && um.undo(1);
undo(all = true) {
!em.isEditing() && um.undo(all);
return this;
},
@ -202,8 +204,8 @@ export default () => {
* @example
* um.redo();
*/
redo() {
!em.isEditing() && um.redo(1);
redo(all = true) {
!em.isEditing() && um.redo(all);
return this;
},
@ -249,6 +251,35 @@ export default () => {
return um.stack;
},
/**
* Get grouped undo manager stack.
* The difference between `getStack` is when you do multiple operations at a time,
* like appending multiple components:
* `editor.getWrapper().append(`<div>C1</div><div>C2</div>`);`
* `getStack` will return a collection length of 2.
* `getStackGroup` instead will group them as a single operation (the first
* inserted component will be returned in the list) by returning an array length of 1.
* @return {Array}
*/
getStackGroup() {
const result = [];
const inserted = [];
this.getStack().forEach(item => {
const index = item.get('magicFusionIndex');
if (inserted.indexOf(index) < 0) {
inserted.push(index);
result.push(item);
}
});
return result;
},
getPointer() {
return this.getStack().pointer;
},
/**
* Clear the stack
* @return {this}

18
src/utils/Sorter.js

@ -966,13 +966,13 @@ export default Backbone.View.extend({
h = 0,
un = 'px',
margI = 5,
brdCol = '#62c462',
brd = 3,
method = pos.method;
var elDim = dims[pos.index];
plh.style.borderColor = 'transparent ' + brdCol;
plh.style.borderWidth = brd + un + ' ' + (brd + 2) + un;
plh.style.margin = '-' + brd + 'px 0 0';
// Placeholder orientation
plh.classList.remove('vertical');
plh.classList.add('horizontal');
if (elDim) {
// If it's not in flow (like 'float' element)
if (!elDim[4]) {
@ -980,9 +980,9 @@ export default Backbone.View.extend({
h = elDim[2] - marg * 2 + un;
t = elDim[0] + marg;
l = method == 'before' ? elDim[1] - marg : elDim[1] + elDim[3] - marg;
plh.style.borderColor = brdCol + ' transparent';
plh.style.borderWidth = brd + 2 + un + ' ' + brd + un;
plh.style.margin = '0 0 0 -' + brd + 'px';
plh.classList.remove('horizontal');
plh.classList.add('vertical');
} else {
w = elDim[3] + un;
h = 'auto';
@ -1055,6 +1055,8 @@ export default Backbone.View.extend({
this.toggleSortCursor();
this.toMove = null;
this.eventMove = 0;
this.dropModel = null;
if (isFunction(onEndMove)) {
const data = {

40
test/specs/commands/view/Preview.js

@ -1,17 +1,17 @@
import Panel from 'panels/model/Panel';
import Preview from 'commands/view/Preview';
import Button from 'panels/model/Button';
describe('Preview command', () => {
let fakeButton, fakePanels, fakeEditor;
let fakePanels, fakeEditor, fakeIsActive;
beforeEach(() => {
fakeButton = new Button();
fakePanels = [new Panel(), new Panel(), new Panel()];
fakeIsActive = false;
fakeEditor = {
getEl: jest.fn(),
refresh: jest.fn(),
runCommand: jest.fn(),
stopCommand: jest.fn(),
getModel: jest.fn().mockReturnValue({
@ -28,13 +28,17 @@ describe('Preview command', () => {
})
},
Commands: {
isActive: jest.fn(() => fakeIsActive)
},
Panels: {
getButton: jest.fn(() => fakeButton),
getPanels: jest.fn(() => fakePanels)
}
};
Preview.panels = undefined;
Preview.shouldRunSwVisibility = undefined;
spyOn(Preview, 'tglPointers');
});
@ -57,6 +61,24 @@ describe('Preview command', () => {
Preview.run(fakeEditor);
fakePanels.forEach(panel => expect(panel.get('visible')).toEqual(false));
});
it("should stop the 'sw-visibility' command if active", () => {
Preview.run(fakeEditor);
expect(fakeEditor.stopCommand).not.toHaveBeenCalled();
fakeIsActive = true;
Preview.run(fakeEditor);
expect(fakeEditor.stopCommand).toHaveBeenCalledWith('sw-visibility');
});
it('should not reset the `shouldRunSwVisibility` state once active if run multiple times', () => {
expect(Preview.shouldRunSwVisibility).toBeUndefined();
fakeIsActive = true;
Preview.run(fakeEditor);
expect(Preview.shouldRunSwVisibility).toEqual(true);
fakeIsActive = false;
Preview.run(fakeEditor);
expect(Preview.shouldRunSwVisibility).toEqual(true);
});
});
describe('.stop', () => {
@ -66,9 +88,13 @@ describe('Preview command', () => {
fakePanels.forEach(panel => expect(panel.get('visible')).toEqual(true));
});
it('should not break when the sw-visibility button is not present', () => {
fakeButton = null;
expect(() => Preview.stop(fakeEditor)).not.toThrow();
it("should run the 'sw-visibility' command if it was active before run", () => {
Preview.stop(fakeEditor);
expect(fakeEditor.runCommand).not.toHaveBeenCalled();
Preview.shouldRunSwVisibility = true;
Preview.stop(fakeEditor);
expect(fakeEditor.runCommand).toHaveBeenCalledWith('sw-visibility');
expect(Preview.shouldRunSwVisibility).toEqual(false);
});
});
});

29
test/specs/commands/view/SwitchVisibility.js

@ -0,0 +1,29 @@
import SwitchVisibility from 'commands/view/SwitchVisibility';
describe('SwitchVisibility command', () => {
let fakeEditor, fakeFrames, fakeIsActive;
beforeEach(() => {
fakeFrames = [];
fakeIsActive = false;
fakeEditor = {
Canvas: {
getFrames: jest.fn(() => fakeFrames)
},
Commands: {
isActive: jest.fn(() => fakeIsActive)
}
};
});
describe('.toggleVis', () => {
it('should do nothing if the preview command is active', () => {
expect(fakeEditor.Canvas.getFrames).not.toHaveBeenCalled();
fakeIsActive = true;
SwitchVisibility.toggleVis(fakeEditor);
expect(fakeEditor.Canvas.getFrames).not.toHaveBeenCalled();
});
});
});

3
test/specs/dom_components/view/ComponentsView.js

@ -19,7 +19,8 @@ describe('ComponentsView', () => {
model = new Components([], compOpts);
view = new ComponentsView({
collection: model,
componentTypes: dcomp.componentTypes
componentTypes: dcomp.componentTypes,
config: { em }
});
document.body.innerHTML = '<div id="fixtures"></div>';
document.body.querySelector('#fixtures').appendChild(view.render().el);

116
test/specs/editor/index.js

@ -0,0 +1,116 @@
import Editor from 'editor';
const { keys } = Object;
const initComps = 1;
describe('Editor', () => {
const editor = new Editor();
beforeEach(() => {
editor.init();
});
afterEach(() => {
editor.destroy();
});
test('Object exists', () => {
expect(editor).toBeTruthy();
});
test('Has no components', () => {
const all = editor.Components.allById();
const allKeys = keys(all);
// By default 1 wrapper components is created
expect(allKeys.length).toBe(initComps);
expect(allKeys[0]).toBe('wrapper');
});
test('Has no CSS rules', () => {
const all = editor.Css.getAll();
expect(all.length).toBe(0);
});
test('Has one default frame', () => {
const all = editor.Canvas.getFrames();
expect(all.length).toBe(1);
});
test('The default frame has the same main component and css', () => {
const wrapper = editor.getWrapper();
const style = editor.getStyle();
const frame = editor.Canvas.getFrame();
expect(wrapper).toBe(frame.get('root'));
expect(style).toBe(frame.get('styles'));
});
test('Components are correctly tracked on add', () => {
const all = editor.Components.allById();
const wrapper = editor.getWrapper();
wrapper.append('<div>Component</div>');
expect(keys(all).length).toBe(1 + initComps);
});
test('Components are correctly tracked on add and remove', () => {
const all = editor.Components.allById();
const wrapper = editor.getWrapper();
const added = wrapper.append(`
<div>Component 1</div>
<div></div>
`);
expect(keys(all).length).toBe(2 + initComps);
const secComp = added[1];
secComp.append(`
<div>Component 2</div>
<div>Component 3</div>
`);
expect(keys(all).length).toBe(4 + initComps);
wrapper.empty();
expect(wrapper.components().length).toBe(0);
expect(keys(all).length).toBe(initComps);
});
test('Components are correctly tracked with UndoManager', () => {
editor.Components.postLoad(); // Init UndoManager
const all = editor.Components.allById();
const um = editor.UndoManager;
const umStack = um.getStack();
const wrapper = editor.getWrapper();
expect(umStack.length).toBe(0);
const comp = wrapper.append(`<div>Component 1</div>`)[0];
expect(umStack.length).toBe(1);
wrapper.empty();
expect(umStack.length).toBe(2);
expect(keys(all).length).toBe(initComps);
um.undo(false);
expect(keys(all).length).toBe(1 + initComps);
});
test('Components are correctly tracked with UndoManager and mutiple operations', () => {
editor.Components.postLoad(); // Init UndoManager
const all = editor.Components.allById();
const um = editor.UndoManager;
const umStack = um.getStack();
const wrapper = editor.getWrapper();
expect(umStack.length).toBe(0);
wrapper.append(`<div>
<div>Component 1</div>
<div>Component 2</div>
</div>`);
expect(umStack.length).toBe(1); // UM counts first children
expect(keys(all).length).toBe(3 + initComps);
wrapper
.components()
.at(0)
.components()
.at(0)
.remove(); // Remove 1 component
// UM registers 2 identical remove undoTypes as Backbone triggers remove from the
// collection and the model
expect(umStack.length).toBe(3);
expect(keys(all).length).toBe(2 + initComps);
wrapper.empty();
expect(umStack.length).toBe(4);
expect(keys(all).length).toBe(initComps);
});
});
Loading…
Cancel
Save