Browse Source

Merge pull request #712 from artf/dev

Merge from dev
pull/883/head
Artur Arseniev 9 years ago
committed by GitHub
parent
commit
42b168ce9f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      README.md
  2. 2
      dist/css/grapes.min.css
  3. 35215
      dist/grapes.js
  4. 6
      dist/grapes.min.js
  5. 2
      package-lock.json
  6. 4
      package.json
  7. 2
      src/block_manager/view/BlocksView.js
  8. 9
      src/canvas/index.js
  9. 10
      src/canvas/view/FrameView.js
  10. 30
      src/commands/index.js
  11. 3
      src/commands/view/InsertCustom.js
  12. 75
      src/commands/view/SelectComponent.js
  13. 12
      src/commands/view/SelectParent.js
  14. 34
      src/css_composer/index.js
  15. 9
      src/device_manager/index.js
  16. 3
      src/device_manager/model/Device.js
  17. 58
      src/dom_components/index.js
  18. 102
      src/dom_components/model/Component.js
  19. 84
      src/dom_components/model/ComponentTable.js
  20. 58
      src/dom_components/model/ComponentTableBody.js
  21. 25
      src/dom_components/model/ComponentTableCell.js
  22. 21
      src/dom_components/model/ComponentTableFoot.js
  23. 21
      src/dom_components/model/ComponentTableHead.js
  24. 37
      src/dom_components/model/ComponentTableRow.js
  25. 4
      src/dom_components/view/ComponentLinkView.js
  26. 4
      src/dom_components/view/ComponentTableBodyView.js
  27. 4
      src/dom_components/view/ComponentTableFootView.js
  28. 4
      src/dom_components/view/ComponentTableHeadView.js
  29. 6
      src/dom_components/view/ComponentTextView.js
  30. 4
      src/dom_components/view/ComponentVideoView.js
  31. 11
      src/dom_components/view/ComponentView.js
  32. 2
      src/domain_abstract/ui/Input.js
  33. 7
      src/domain_abstract/ui/InputColor.js
  34. 9
      src/editor/config/config.js
  35. 4
      src/editor/index.js
  36. 194
      src/editor/model/Editor.js
  37. 5
      src/grapesjs/config/config.js
  38. 4
      src/grapesjs/index.js
  39. 38
      src/keymaps/index.js
  40. 82
      src/navigator/view/ItemView.js
  41. 8
      src/panels/view/ButtonView.js
  42. 2
      src/rich_text_editor/index.js
  43. 15
      src/selector_manager/model/Selectors.js
  44. 118
      src/selector_manager/view/ClassTagView.js
  45. 12
      src/selector_manager/view/ClassTagsView.js
  46. 2
      src/storage_manager/index.js
  47. 2
      src/style_manager/config/config.js
  48. 64
      src/style_manager/index.js
  49. 17
      src/style_manager/view/PropertyView.js
  50. 1
      src/style_manager/view/SectorView.js
  51. 8
      src/style_manager/view/SectorsView.js
  52. 33
      src/styles/scss/_gjs_canvas.scss
  53. 9
      src/styles/scss/main.scss
  54. 45
      src/trait_manager/model/Trait.js
  55. 1
      src/trait_manager/view/TraitColorView.js
  56. 48
      src/trait_manager/view/TraitView.js
  57. 242
      src/undo_manager/index.js
  58. 19
      src/utils/Sorter.js
  59. 38
      src/utils/polyfills.js
  60. 35
      test/specs/dom_components/model/Component.js
  61. 13
      test/specs/dom_components/view/ComponentV.js
  62. 2
      test/specs/selector_manager/e2e/ClassManager.js
  63. 26
      test/specs/selector_manager/view/ClassTagView.js
  64. 2
      test/specs/selector_manager/view/ClassTagsView.js

18
README.md

@ -2,12 +2,13 @@
[![Build Status](https://travis-ci.org/artf/grapesjs.svg?branch=master)](https://travis-ci.org/artf/grapesjs) [![Build Status](https://travis-ci.org/artf/grapesjs.svg?branch=master)](https://travis-ci.org/artf/grapesjs)
[![Chat](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/QAbgGXq) [![Chat](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/QAbgGXq)
[![CDNJS](https://img.shields.io/cdnjs/v/grapesjs.svg)](https://cdnjs.com/libraries/grapesjs)
[![npm](https://img.shields.io/npm/v/grapesjs.svg)](https://www.npmjs.com/package/grapesjs)
<p align="center"><img src="http://grapesjs.com/img/grapesjs-front-page-m.jpg" alt="GrapesJS" width="500" align="center"/></p> <p align="center"><img src="http://grapesjs.com/img/grapesjs-front-page-m.jpg" alt="GrapesJS" width="500" align="center"/></p>
GrapesJS is a free and open source Web Builder Framework which helps you building HTML templates to be used inside sites, newsletters and mobile apps. GrapesJS is a free and open source Web Builder Framework which helps building HTML templates, faster and easily, to be delivered in sites, newsletters or mobile apps. Mainly, GrapesJS was designed to be used inside a [CMS] to speed up the creation of dynamic templates. To better understand this concept check the image below
Mainly GrapesJS was designed to be used inside a [CMS] to speed up a creation of dynamic templates. To better understand this concept check the image below
<br/> <br/>
<p align="center"><img src="http://grapesjs.com/img/gjs-concept.png" alt="GrapesJS - Style Manager" height="400" align="center"/></p> <p align="center"><img src="http://grapesjs.com/img/gjs-concept.png" alt="GrapesJS - Style Manager" height="400" align="center"/></p>
@ -186,6 +187,7 @@ $ npm test
* [grapesjs-navbar](https://github.com/artf/grapesjs-navbar) - Simple navbar component * [grapesjs-navbar](https://github.com/artf/grapesjs-navbar) - Simple navbar component
* [grapesjs-component-countdown](https://github.com/artf/grapesjs-component-countdown) - Simple countdown component * [grapesjs-component-countdown](https://github.com/artf/grapesjs-component-countdown) - Simple countdown component
* [grapesjs-style-gradient](https://github.com/artf/grapesjs-style-gradient) - Add a gradient type input * [grapesjs-style-gradient](https://github.com/artf/grapesjs-style-gradient) - Add a gradient type input
* [grapesjs-blocks-flexbox](https://github.com/artf/grapesjs-blocks-flexbox) - Add the flexbox block
### Presets ### Presets
* [grapesjs-preset-webpage](https://github.com/artf/grapesjs-preset-webpage) - Webpage Builder * [grapesjs-preset-webpage](https://github.com/artf/grapesjs-preset-webpage) - Webpage Builder
@ -209,11 +211,23 @@ If you like the project support it with a donation of your choice or become a ba
<a href="https://opencollective.com/grapesjs/sponsors/1/website"><img src="https://opencollective.com/grapesjs/sponsors/1/avatar"></a> <a href="https://opencollective.com/grapesjs/sponsors/1/website"><img src="https://opencollective.com/grapesjs/sponsors/1/avatar"></a>
<a href="https://opencollective.com/grapesjs/sponsors/2/website"><img src="https://opencollective.com/grapesjs/sponsors/2/avatar"></a> <a href="https://opencollective.com/grapesjs/sponsors/2/website"><img src="https://opencollective.com/grapesjs/sponsors/2/avatar"></a>
<a href="https://opencollective.com/grapesjs/sponsors/3/website"><img src="https://opencollective.com/grapesjs/sponsors/3/avatar"></a> <a href="https://opencollective.com/grapesjs/sponsors/3/website"><img src="https://opencollective.com/grapesjs/sponsors/3/avatar"></a>
<a href="https://opencollective.com/grapesjs/sponsors/4/website"><img src="https://opencollective.com/grapesjs/sponsors/4/avatar"></a>
<a href="https://opencollective.com/grapesjs/sponsors/5/website"><img src="https://opencollective.com/grapesjs/sponsors/5/avatar"></a>
<a href="https://opencollective.com/grapesjs/sponsors/6/website"><img src="https://opencollective.com/grapesjs/sponsors/6/avatar"></a>
<a href="https://opencollective.com/grapesjs/sponsors/7/website"><img src="https://opencollective.com/grapesjs/sponsors/7/avatar"></a>
<a href="https://opencollective.com/grapesjs/sponsors/8/website"><img src="https://opencollective.com/grapesjs/sponsors/8/avatar"></a>
<a href="https://opencollective.com/grapesjs/sponsors/9/website"><img src="https://opencollective.com/grapesjs/sponsors/9/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/0/website"><img src="https://opencollective.com/grapesjs/backers/0/avatar"></a> <a href="https://opencollective.com/grapesjs/backers/0/website"><img src="https://opencollective.com/grapesjs/backers/0/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/1/website"><img src="https://opencollective.com/grapesjs/backers/1/avatar"></a> <a href="https://opencollective.com/grapesjs/backers/1/website"><img src="https://opencollective.com/grapesjs/backers/1/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/2/website"><img src="https://opencollective.com/grapesjs/backers/2/avatar"></a> <a href="https://opencollective.com/grapesjs/backers/2/website"><img src="https://opencollective.com/grapesjs/backers/2/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/3/website"><img src="https://opencollective.com/grapesjs/backers/3/avatar"></a> <a href="https://opencollective.com/grapesjs/backers/3/website"><img src="https://opencollective.com/grapesjs/backers/3/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/4/website"><img src="https://opencollective.com/grapesjs/backers/4/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/5/website"><img src="https://opencollective.com/grapesjs/backers/5/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/6/website"><img src="https://opencollective.com/grapesjs/backers/6/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/7/website"><img src="https://opencollective.com/grapesjs/backers/7/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/8/website"><img src="https://opencollective.com/grapesjs/backers/8/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/9/website"><img src="https://opencollective.com/grapesjs/backers/9/avatar"></a>

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

35215
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
package-lock.json

@ -1,6 +1,6 @@
{ {
"name": "grapesjs", "name": "grapesjs",
"version": "0.12.50", "version": "0.12.58",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

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.12.50", "version": "0.12.58",
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",
@ -80,7 +80,7 @@
"build-dev": "webpack --env=dev && npm run build:css", "build-dev": "webpack --env=dev && npm run build:css",
"build:css": "node-sass src/styles/scss/main.scss dist/css/grapes.min.css --output-style compressed", "build:css": "node-sass src/styles/scss/main.scss dist/css/grapes.min.css --output-style compressed",
"v:patch": "npm version --no-git-tag-version patch", "v:patch": "npm version --no-git-tag-version patch",
"start": "webpack-dev-server --open --progress --colors & npm run build:css -- -w", "start": "npm run build:css -- -w & webpack-dev-server --open --progress --colors",
"test": "cross-env NODE_PATH=./src mocha --compilers js:babel-core/register --require test/helper.js --timeout 10000 --recursive test/main.js", "test": "cross-env NODE_PATH=./src mocha --compilers js:babel-core/register --require test/helper.js --timeout 10000 --recursive test/main.js",
"test:dev": "npm test -- -R min -w" "test:dev": "npm test -- -R min -w"
} }

2
src/block_manager/view/BlocksView.js

@ -84,8 +84,6 @@ module.exports = Backbone.View.extend({
model.set('activeOnRender', 0); model.set('activeOnRender', 0);
} }
// Register all its components (eg. for the Undo Manager)
em.initChildrenComp(model);
em.trigger('block:drag:stop', model); em.trigger('block:drag:stop', model);
} }
}, },

9
src/canvas/index.js

@ -351,8 +351,13 @@ module.exports = () => {
this.dragging = 1; this.dragging = 1;
let toListen = this.getScrollListeners(); let toListen = this.getScrollListeners();
frameRect = CanvasView.getFrameOffset(1); frameRect = CanvasView.getFrameOffset(1);
on(toListen, 'mousemove', this.autoscroll);
on(toListen, 'mouseup', this.stopAutoscroll); // By detaching those from the stack avoid browsers lags
// Noticeable with "fast" drag of blocks
setTimeout(() => {
on(toListen, 'mousemove', this.autoscroll);
on(toListen, 'mouseup', this.stopAutoscroll);
}, 0);
}, },
autoscroll(e) { autoscroll(e) {

10
src/canvas/view/FrameView.js

@ -16,17 +16,19 @@ module.exports = require('backbone').View.extend({
this.config = o.config || {}; this.config = o.config || {};
this.ppfx = this.config.pStylePrefix || ''; this.ppfx = this.config.pStylePrefix || '';
this.em = this.config.em; this.em = this.config.em;
this.listenTo(this.em, 'change:device', this.updateWidth); this.listenTo(this.em, 'change:device', this.updateDim);
}, },
/** /**
* Update width of the frame * Update dimensions of the frame
* @private * @private
*/ */
updateWidth(model) { updateDim(model) {
const em = this.em; const em = this.em;
const device = em.getDeviceModel(); const device = em.getDeviceModel();
this.el.style.width = device ? device.get('width') : ''; const style = this.el.style;
style.width = device ? device.get('width') : '';
style.height = device ? device.get('height') : '';
this.udpateOffset(); this.udpateOffset();
em.stopDefault({ preserveSelected: 1 }); em.stopDefault({ preserveSelected: 1 });
this.$el.on(motionsEv, this.udpateOffset); this.$el.on(motionsEv, this.udpateOffset);

30
src/commands/index.js

@ -41,6 +41,7 @@
import { isFunction } from 'underscore'; import { isFunction } from 'underscore';
module.exports = () => { module.exports = () => {
let em;
var c = {}, var c = {},
commands = {}, commands = {},
defaultCommands = {}, defaultCommands = {},
@ -78,7 +79,7 @@ module.exports = () => {
if (!(name in c)) if (!(name in c))
c[name] = defaults[name]; c[name] = defaults[name];
} }
em = c.em;
var ppfx = c.pStylePrefix; var ppfx = c.pStylePrefix;
if(ppfx) if(ppfx)
c.stylePrefix = ppfx + c.stylePrefix; c.stylePrefix = ppfx + c.stylePrefix;
@ -138,7 +139,7 @@ module.exports = () => {
var collection = sel.collection; var collection = sel.collection;
var index = collection.indexOf(sel); var index = collection.indexOf(sel);
collection.add(sel.clone(), {at: index + 1}); collection.add(sel.clone(), {at: index + 1});
ed.trigger('component:update', sel); sel.emitUpdate()
}, },
}; };
@ -163,7 +164,7 @@ module.exports = () => {
const onEnd = (e, opts) => { const onEnd = (e, opts) => {
em.runDefault(); em.runDefault();
em.set('selectedComponent', sel); em.set('selectedComponent', sel);
ed.trigger('component:update', sel); sel.emitUpdate()
dragger && dragger.blur(); dragger && dragger.blur();
}; };
@ -200,12 +201,31 @@ module.exports = () => {
}; };
// Core commands // Core commands
defaultCommands['core:undo'] = e => e.UndoManager.undo(1); defaultCommands['core:undo'] = e => e.UndoManager.undo();
defaultCommands['core:redo'] = e => e.UndoManager.redo(1); defaultCommands['core:redo'] = e => e.UndoManager.redo();
defaultCommands['core:canvas-clear'] = e => { defaultCommands['core:canvas-clear'] = e => {
e.DomComponents.clear(); e.DomComponents.clear();
e.CssComposer.clear(); e.CssComposer.clear();
}; };
defaultCommands['core:copy'] = ed => {
const em = ed.getModel();
const model = ed.getSelected();
if (model && model.get('copyable') && !ed.Canvas.isInputFocused()) {
em.set('clipboard', model);
}
};
defaultCommands['core:paste'] = ed => {
const em = ed.getModel();
const clp = em.get('clipboard');
const model = ed.getSelected();
const coll = model && model.collection;
if (coll && clp && !ed.Canvas.isInputFocused()) {
const at = coll.indexOf(model) + 1;
coll.add(clp.clone(), { at });
}
};
if(c.em) if(c.em)
c.model = c.em.get('Canvas'); c.model = c.em.get('Canvas');

3
src/commands/view/InsertCustom.js

@ -47,9 +47,6 @@ module.exports = _.extend({}, CreateComponent, {
if(!model) if(!model)
return; return;
if(this.em)
this.em.editor.initChildrenComp(model);
this.afterInsert(model, this); this.afterInsert(model, this);
}, },

75
src/commands/view/SelectComponent.js

@ -4,15 +4,13 @@ import { on, off, getUnitFromValue} from 'utils/mixins';
const ToolbarView = require('dom_components/view/ToolbarView'); const ToolbarView = require('dom_components/view/ToolbarView');
const Toolbar = require('dom_components/model/Toolbar'); const Toolbar = require('dom_components/model/Toolbar');
const key = require('keymaster'); const key = require('keymaster');
const Backbone = require('backbone'); const $ = require('backbone').$;
let showOffsets; let showOffsets;
const $ = Backbone.$;
module.exports = { module.exports = {
init(o) { init(o) {
bindAll(this, 'onHover', 'onOut', 'onClick', 'onKeyPress', bindAll(this, 'onHover', 'onOut', 'onClick', 'onKeyPress', 'onFrameScroll');
'copyComp', 'pasteComp', 'onFrameScroll');
}, },
@ -20,7 +18,6 @@ module.exports = {
this.frameOff = this.canvasOff = this.adjScroll = null; this.frameOff = this.canvasOff = this.adjScroll = null;
var config = this.config.em.get('Config'); var config = this.config.em.get('Config');
this.startSelectComponent(); this.startSelectComponent();
this.toggleClipboard(config.copyPaste);
var em = this.config.em; var em = this.config.em;
showOffsets = 1; showOffsets = 1;
@ -28,47 +25,6 @@ module.exports = {
em.on('change:canvasOffset', this.updateAttached, this); em.on('change:canvasOffset', this.updateAttached, this);
}, },
/**
* Toggle clipboard function
* @param {Boolean} active
* @return {this}
* @private
*/
toggleClipboard(active) {
var en = active || 0;
if(en){
key('⌘+c, ctrl+c', this.copyComp);
key('⌘+v, ctrl+v', this.pasteComp);
}else{
key.unbind('⌘+c, ctrl+c');
key.unbind('⌘+v, ctrl+v');
}
},
/**
* Copy component to the clipboard
* @private
*/
copyComp() {
var el = this.editorModel.get('selectedComponent');
if(el && el.get('copyable'))
this.editorModel.set('clipboard', el);
},
/**
* Paste component from clipboard
* @private
*/
pasteComp() {
var clp = this.editorModel.get('clipboard'),
sel = this.editorModel.get('selectedComponent');
if(clp && sel && sel.collection){
var index = sel.collection.indexOf(sel),
clone = clp.clone();
sel.collection.add(clone, { at: index + 1 });
}
},
/** /**
* Start select component event * Start select component event
* @private * @private
@ -136,7 +92,8 @@ module.exports = {
*/ */
onHover(e) { onHover(e) {
e.stopPropagation(); e.stopPropagation();
var trg = e.target; let trg = e.target;
const model = $(trg).data('model');
// Adjust tools scroll top // Adjust tools scroll top
if (!this.adjScroll) { if (!this.adjScroll) {
@ -145,7 +102,13 @@ module.exports = {
this.updateAttached(); this.updateAttached();
} }
var pos = this.getElementPos(trg); if (model && !model.get('hoverable')) {
let comp = model && model.parent();
while (comp && !comp.get('hoverable')) comp = comp.parent();
comp && (trg = comp.view.el);
}
const pos = this.getElementPos(trg);
this.updateBadge(trg, pos); this.updateBadge(trg, pos);
this.updateHighlighter(trg, pos); this.updateHighlighter(trg, pos);
this.showElementOffset(trg, pos); this.showElementOffset(trg, pos);
@ -230,7 +193,17 @@ module.exports = {
onClick(e) { onClick(e) {
e.stopPropagation(); e.stopPropagation();
const model = $(e.target).data('model'); const model = $(e.target).data('model');
model && this.editor.select(model); const editor = this.editor;
if (model) {
if (model.get('selectable')) {
editor.select(model);
} else {
let parent = model.parent();
while (parent && !parent.get('selectable')) parent = parent.parent();
parent && editor.select(parent);
}
}
}, },
/** /**
@ -463,6 +436,7 @@ module.exports = {
var unit = 'px'; var unit = 'px';
var toolbarEl = this.canvas.getToolbarEl(); var toolbarEl = this.canvas.getToolbarEl();
var toolbarStyle = toolbarEl.style; var toolbarStyle = toolbarEl.style;
const origDisp = toolbarStyle.display;
toolbarStyle.display = 'block'; toolbarStyle.display = 'block';
var pos = this.canvas.getTargetToElementDim(toolbarEl, el, { var pos = this.canvas.getTargetToElementDim(toolbarEl, el, {
elPos, elPos,
@ -471,7 +445,7 @@ module.exports = {
var leftPos = pos.left + pos.elementWidth - pos.targetWidth; var leftPos = pos.left + pos.elementWidth - pos.targetWidth;
toolbarStyle.top = pos.top + unit; toolbarStyle.top = pos.top + unit;
toolbarStyle.left = leftPos + unit; toolbarStyle.left = leftPos + unit;
toolbarStyle.display = ''; toolbarStyle.display = origDisp;
}, },
/** /**
@ -580,7 +554,6 @@ module.exports = {
this.stopSelectComponent(); this.stopSelectComponent();
!opts.preserveSelected && em.setSelected(null); !opts.preserveSelected && em.setSelected(null);
this.clean(); this.clean();
this.toggleClipboard();
this.hideBadge(); this.hideBadge();
this.hideFixedElementOffset(); this.hideFixedElementOffset();
this.canvas.getToolbarEl().style.display = 'none'; this.canvas.getToolbarEl().style.display = 'none';

12
src/commands/view/SelectParent.js

@ -1,9 +1,15 @@
module.exports = { module.exports = {
run(editor) { run(editor) {
const comp = editor.getSelected(); const sel = editor.getSelected();
const coll = comp && comp.collection; let comp = sel && sel.parent();
coll && coll.parent && editor.select(coll.parent);
// Recurse through the parent() chain until a selectable parent is found
while (comp && !comp.get("selectable")) {
comp = comp.parent();
}
comp && editor.select(comp);
} }
}; };

34
src/css_composer/index.js

@ -1,16 +1,4 @@
/** /**
* * [add](#add)
* * [get](#get)
* * [set](#set)
* * [getAll](#getall)
* * [clear](#clear)
* * [load](#load)
* * [store](#store)
* * [setIdRule](#setidrule)
* * [getIdRule](#getidrule)
* * [setClassRule](#setclassrule)
* * [getClassRule](#getclassrule)
*
* This module contains and manage CSS rules for the template inside the canvas * This module contains and manage CSS rules for the template inside the canvas
* Before using the methods you should get first the module from the editor instance, in this way: * Before using the methods you should get first the module from the editor instance, in this way:
* *
@ -103,15 +91,35 @@ module.exports = () => {
rules.add(c.rules); rules.add(c.rules);
}, },
/** /**
* Do stuff after load * Do stuff after load
* @param {Editor} em * @param {Editor} em
* @private * @private
*/ */
postLoad(em) { postLoad(em) {
em.listenRules(this.getAll()); const ev = 'add remove';
const rules = this.getAll();
em.stopListening(rules, ev, this.handleChange);
em.listenTo(rules, ev, this.handleChange);
rules.each(rule => this.handleChange(rule));
}, },
/**
* Handle rule changes
* @private
*/
handleChange(model) {
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);
},
/** /**
* Load data from the passed object, if the object is empty will try to fetch them * Load data from the passed object, if the object is empty will try to fetch them
* autonomously from the storage manager. * autonomously from the storage manager.

9
src/device_manager/index.js

@ -1,8 +1,4 @@
/** /**
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
*
* Before using methods you should get first the module from the editor instance, in this way: * Before using methods you should get first the module from the editor instance, in this way:
* *
* ```js * ```js
@ -41,6 +37,7 @@ module.exports = () => {
* } * }
* ... * ...
* @return {this} * @return {this}
* @private
*/ */
init(config) { init(config) {
c = config || {}; c = config || {};
@ -65,6 +62,10 @@ module.exports = () => {
* @return {Device} Added device * @return {Device} Added device
* @example * @example
* deviceManager.add('Tablet', '900px'); * deviceManager.add('Tablet', '900px');
* deviceManager.add('Tablet2', '900px', {
* height: '300px',
* widthMedia: '810px', // the width that will be used for the CSS media
* });
*/ */
add(name, width, opts) { add(name, width, opts) {
var obj = opts || {}; var obj = opts || {};

3
src/device_manager/model/Device.js

@ -10,6 +10,9 @@ module.exports = Backbone.Model.extend({
// Width to set for the editor iframe // Width to set for the editor iframe
width: '', width: '',
// Height to set for the editor iframe
height: '',
// The width which will be used in media queries, // The width which will be used in media queries,
// If empty the width will be used // If empty the width will be used
widthMedia: null, widthMedia: null,

58
src/dom_components/index.js

@ -34,6 +34,7 @@
*/ */
module.exports = () => { module.exports = () => {
var c = {}; var c = {};
let em;
const defaults = require('./config/config'); const defaults = require('./config/config');
const Component = require('./model/Component'); const Component = require('./model/Component');
const ComponentView = require('./view/ComponentView'); const ComponentView = require('./view/ComponentView');
@ -57,6 +58,21 @@ module.exports = () => {
model: require('./model/ComponentTable'), model: require('./model/ComponentTable'),
view: require('./view/ComponentTableView'), view: require('./view/ComponentTableView'),
}, },
{
id: 'thead',
model: require('./model/ComponentTableHead'),
view: require('./view/ComponentTableHeadView'),
},
{
id: 'tbody',
model: require('./model/ComponentTableBody'),
view: require('./view/ComponentTableBodyView'),
},
{
id: 'tfoot',
model: require('./model/ComponentTableFoot'),
view: require('./view/ComponentTableFootView'),
},
{ {
id: 'map', id: 'map',
model: require('./model/ComponentMap'), model: require('./model/ComponentMap'),
@ -151,7 +167,7 @@ module.exports = () => {
*/ */
init(config) { init(config) {
c = config || {}; c = config || {};
const em = c.em; em = c.em;
if (em) { if (em) {
c.components = em.config.components || c.components; c.components = em.config.components || c.components;
@ -222,9 +238,47 @@ module.exports = () => {
* @private * @private
*/ */
postLoad(em) { postLoad(em) {
em.initChildrenComp(this.getWrapper()); 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 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],
[comps, 'add', handleChanges],
[comps, 'remove', 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));
},
/**
* Triggered when some component is removed
* @private
* */
handleRemoves(model, value, opts = {}) {
!opts.avoidStore && em.handleUpdates(model, value, opts);
},
/** /**
* Load components from the passed object, if the object is empty will try to fetch them * Load components from the passed object, if the object is empty will try to fetch them
* autonomously from the selected storage * autonomously from the selected storage

102
src/dom_components/model/Component.js

@ -4,6 +4,7 @@ import Styleable from 'domain_abstract/model/Styleable';
const Backbone = require('backbone'); const Backbone = require('backbone');
const Components = require('./Components'); const Components = require('./Components');
const Selector = require('selector_manager/model/Selector');
const Selectors = require('selector_manager/model/Selectors'); const Selectors = require('selector_manager/model/Selectors');
const Traits = require('trait_manager/model/Traits'); const Traits = require('trait_manager/model/Traits');
@ -161,14 +162,73 @@ module.exports = Backbone.Model.extend(Styleable).extend({
this.set('attributes', this.get('attributes') || {}); this.set('attributes', this.get('attributes') || {});
this.listenTo(this, 'change:script', this.scriptUpdated); this.listenTo(this, 'change:script', this.scriptUpdated);
this.listenTo(this, 'change:traits', this.traitsUpdated); this.listenTo(this, 'change:traits', this.traitsUpdated);
this.listenTo(this, 'change:tagName', this.tagUpdated);
this.listenTo(this, 'change:attributes', this.attrUpdated);
this.loadTraits(); this.loadTraits();
this.initClasses(); this.initClasses();
this.initComponents(); this.initComponents();
this.initToolbar(); this.initToolbar();
this.set('status', ''); this.set('status', '');
this.listenTo(this.get('classes'), 'add remove change',
() => this.emitUpdate('classes'));
this.init(); this.init();
}, },
/**
* Check component's type
* @param {string} type Component type
* @return {Boolean}
* @example
* model.is('image')
* // -> false
*/
is(type) {
return !!(this.get('type') == type);
},
/**
* Find inner models by query string
* ATTENTION: this method works only with alredy rendered component
* @param {string} query Query string
* @return {Array} Array of models
* @example
* model.find('div > .class');
* // -> [Component, Component, ...]
*/
find(query) {
const result = [];
this.view.$el.find(query).each((el, i, $el) => {
const model = $el.data('model');
model && result.push(model);
});
return result;
},
/**
* Once the tag is updated I have to remove the node and replace it
*/
tagUpdated() {
const coll = this.collection;
const at = coll.indexOf(this);
coll.remove(this);
coll.add(this, { at });
},
/**
* Emit changes for each updated attribute
*/
attrUpdated() {
const attrPrev = { ...this.previous('attributes') };
const attrCurrent = { ...this.get('attributes') };
const diff = shallowDiff(attrPrev, attrCurrent);
keys(diff).forEach(pr => this.trigger(`change:attributes:${pr}`));
},
/** /**
* Update attributes of the model * Update attributes of the model
@ -236,7 +296,7 @@ module.exports = Backbone.Model.extend(Styleable).extend({
*/ */
getAttributes() { getAttributes() {
const classes = []; const classes = [];
const attributes = this.get('attributes') || {}; const attributes = { ...this.get('attributes') };
// Add classes // Add classes
this.get('classes').each(cls => classes.push(cls.get('name'))); this.get('classes').each(cls => classes.push(cls.get('name')));
@ -283,6 +343,34 @@ module.exports = Backbone.Model.extend(Styleable).extend({
}, },
/**
* Remove classes
* @param {Array|string} classes Array or string of classes
* @return {Array} Array of removed selectors
* @example
* model.removeClass('class1');
* model.removeClass('class1 class2');
* model.removeClass(['class1', 'class2']);
* // -> [SelectorObject, ...]
*/
removeClass(classes) {
const removed = [];
classes = isArray(classes) ? classes : [classes];
const selectors = this.get('classes');
const type = Selector.TYPE_CLASS;
classes.forEach(classe => {
const classes = classe.split(' ');
classes.forEach(name => {
const selector = selectors.where({ name, type })[0];
selector && removed.push(selectors.remove(selector));
})
});
return removed;
},
initClasses() { initClasses() {
const classes = this.normalizeClasses(this.get('classes') || []); const classes = this.normalizeClasses(this.get('classes') || []);
this.set('classes', new Selectors(classes)); this.set('classes', new Selectors(classes));
@ -488,7 +576,8 @@ module.exports = Backbone.Model.extend(Styleable).extend({
clone(reset) { clone(reset) {
const em = this.em; const em = this.em;
const style = this.getStyle(); const style = this.getStyle();
const attr = clone(this.attributes); const attr = { ...this.attributes };
attr.attributes = { ...attr.attributes };
delete attr.attributes.id; delete attr.attributes.id;
attr.components = []; attr.components = [];
attr.classes = []; attr.classes = [];
@ -648,7 +737,14 @@ module.exports = Backbone.Model.extend(Styleable).extend({
}) })
return scr; return scr;
} },
emitUpdate(property) {
const em = this.em;
const event = 'component:update' + (property ? `:${property}` : '');
em && em.trigger(event, this.model);
},
},{ },{

84
src/dom_components/model/ComponentTable.js

@ -1,88 +1,28 @@
var Component = require('./Component'); const Component = require('./Component');
module.exports = Component.extend({ module.exports = Component.extend({
defaults: _.extend({}, Component.prototype.defaults, { defaults: { ...Component.prototype.defaults,
type: 'table', type: 'table',
tagName: 'table', tagName: 'table',
droppable: ['tr', 'tbody', 'thead', 'tfoot'], droppable: ['tbody', 'thead', 'tfoot'],
columns: 3, },
rows: 2,
/*
traits: [{
label: 'Columns',
name: 'columns',
changeProp: 1,
},{
label: 'Rows',
name: 'rows',
changeProp: 1,
}]
*/
}),
initialize(o, opt) { initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments); Component.prototype.initialize.apply(this, arguments);
var components = this.get('components'); const components = this.get('components');
var rows = this.get('rows'); !components.length && components.add({ type: 'tbody' });
var columns = this.get('columns');
// Init components if empty
if(!components.length){
var rowsToAdd = [];
while(rows--){
var columnsToAdd = [];
var clm = columns;
while (clm--) {
columnsToAdd.push({
type: 'cell',
classes: ['cell']
});
}
rowsToAdd.push({
type: 'row',
classes: ['row'],
components: columnsToAdd
});
}
components.add(rowsToAdd);
}
// Clean table from non rows
var rowsColl = [];
components.each(model => {
if(model.get('type') != 'row'){
model.get('components').each(row => {
if(row.get('type') == 'row'){
row.collection = components;
rowsColl.push(row);
}
});
}else{
rowsColl.push(model);
}
});
components.reset(rowsColl);
}, },
},{ }, {
/**
* Detect if the passed element is a valid component.
* In case the element is valid an object abstracted
* from the element will be returned
* @param {HTMLElement}
* @return {Object}
* @private
*/
isComponent(el) { isComponent(el) {
var result = ''; let result = '';
if(el.tagName == 'TABLE'){
result = {type: 'table'}; if (el.tagName == 'TABLE') {
result = { type: 'table' };
} }
return result; return result;
}, },

58
src/dom_components/model/ComponentTableBody.js

@ -0,0 +1,58 @@
const Component = require('./Component');
module.exports = Component.extend({
defaults: { ...Component.prototype.defaults,
type: 'tbody',
tagName: 'tbody',
draggable: ['table'],
droppable: ['tr'],
columns: 1,
rows: 1,
},
initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments);
const components = this.get('components');
let columns = this.get('columns');
let rows = this.get('rows');
// Init components if empty
if (!components.length) {
const rowsToAdd = [];
while (rows--) {
const columnsToAdd = [];
let clm = columns;
while (clm--) {
columnsToAdd.push({
type: 'cell',
classes: ['cell']
});
}
rowsToAdd.push({
type: 'row',
classes: ['row'],
components: columnsToAdd
});
}
components.add(rowsToAdd);
}
},
},{
isComponent(el) {
let result = '';
if (el.tagName == 'TBODY') {
result = { type: 'tbody' };
}
return result;
},
});

25
src/dom_components/model/ComponentTableCell.js

@ -1,32 +1,25 @@
var Component = require('./Component'); const Component = require('./Component');
module.exports = Component.extend({ module.exports = Component.extend({
defaults: _.extend({}, Component.prototype.defaults, { defaults: { ...Component.prototype.defaults,
type: 'cell', type: 'cell',
tagName: 'td', tagName: 'td',
draggable: ['tr'], draggable: ['tr'],
}), },
},{
/** }, {
* Detect if the passed element is a valid component.
* In case the element is valid an object abstracted
* from the element will be returned
* @param {HTMLElement}
* @return {Object}
* @private
*/
isComponent(el) { isComponent(el) {
var result = ''; let result = '';
var tag = el.tagName; const tag = el.tagName;
if(tag == 'TD' || tag == 'TH'){
if (tag == 'TD' || tag == 'TH') {
result = { result = {
type: 'cell', type: 'cell',
tagName: tag.toLowerCase() tagName: tag.toLowerCase()
}; };
} }
return result; return result;
}, },

21
src/dom_components/model/ComponentTableFoot.js

@ -0,0 +1,21 @@
const ComponentTableBody = require('./ComponentTableBody');
module.exports = ComponentTableBody.extend({
defaults: { ...ComponentTableBody.prototype.defaults,
type: 'tfoot',
tagName: 'tfoot',
},
},{
isComponent(el) {
let result = '';
if (el.tagName == 'TFOOT') {
result = { type: 'tfoot' };
}
return result;
},
});

21
src/dom_components/model/ComponentTableHead.js

@ -0,0 +1,21 @@
const ComponentTableBody = require('./ComponentTableBody');
module.exports = ComponentTableBody.extend({
defaults: { ...ComponentTableBody.prototype.defaults,
type: 'thead',
tagName: 'thead'
},
}, {
isComponent(el) {
let result = '';
if (el.tagName == 'THEAD') {
result = { type: 'thead' };
}
return result;
},
});

37
src/dom_components/model/ComponentTableRow.js

@ -1,43 +1,32 @@
var Component = require('./Component'); const Component = require('./Component');
module.exports = Component.extend({ module.exports = Component.extend({
defaults: _.extend({}, Component.prototype.defaults, { defaults: { ...Component.prototype.defaults,
type: 'row', type: 'row',
tagName: 'tr', tagName: 'tr',
draggable: ['table', 'tbody', 'thead'], draggable: ['thead', 'tbody', 'tfoot'],
droppable: ['th', 'td'] droppable: ['th', 'td']
}), },
initialize(o, opt) { initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments); Component.prototype.initialize.apply(this, arguments);
// Clean the row from non cell components // Clean the row from non cell components
var cells = []; const cells = [];
var components = this.get('components'); const components = this.get('components');
components.each(model => { components.each(model => model.is('cell') && cells.push(model));
if(model.get('type') == 'cell'){
cells.push(model);
}
});
components.reset(cells); components.reset(cells);
} }
},{ }, {
/**
* Detect if the passed element is a valid component.
* In case the element is valid an object abstracted
* from the element will be returned
* @param {HTMLElement}
* @return {Object}
* @private
*/
isComponent(el) { isComponent(el) {
var result = ''; let result = '';
if(el.tagName == 'TR'){
result = {type: 'row'}; if (el.tagName == 'TR') {
result = { type: 'row' };
} }
return result; return result;
}, },

4
src/dom_components/view/ComponentLinkView.js

@ -3,10 +3,6 @@ var ComponentView = require('./ComponentTextView');
module.exports = ComponentView.extend({ module.exports = ComponentView.extend({
events: {
'dblclick': 'enableEditing',
},
render(...args) { render(...args) {
ComponentView.prototype.render.apply(this, args); ComponentView.prototype.render.apply(this, args);

4
src/dom_components/view/ComponentTableBodyView.js

@ -0,0 +1,4 @@
var Backbone = require('backbone');
var ComponentView = require('./ComponentView');
module.exports = ComponentView.extend({});

4
src/dom_components/view/ComponentTableFootView.js

@ -0,0 +1,4 @@
var Backbone = require('backbone');
var ComponentView = require('./ComponentView');
module.exports = ComponentView.extend({});

4
src/dom_components/view/ComponentTableHeadView.js

@ -0,0 +1,4 @@
var Backbone = require('backbone');
var ComponentView = require('./ComponentView');
module.exports = ComponentView.extend({});

6
src/dom_components/view/ComponentTextView.js

@ -60,14 +60,14 @@ module.exports = ComponentView.extend({
const content = this.getChildrenContainer().innerHTML; const content = this.getChildrenContainer().innerHTML;
const comps = model.get('components'); const comps = model.get('components');
comps.length && comps.reset(); comps.length && comps.reset();
model.set('content', '');
// If there is a custom RTE the content is just baked staticly // If there is a custom RTE the content is just baked staticly
// inside 'content' // inside 'content'
if (rte.customRte) { if (rte.customRte) {
// Avoid double content by removing its children components // Avoid double content by removing its children components
// and force to trigger change // and force to trigger change
model.set('content', '') model.set('content', content);
.set('content', content);
} else { } else {
const clean = model => { const clean = model => {
model.set({ model.set({
@ -82,7 +82,7 @@ module.exports = ComponentView.extend({
} }
// Avoid re-render on reset with silent option // Avoid re-render on reset with silent option
model.set('content', '').trigger('change:content', model); model.trigger('change:content', model);
comps.add(content); comps.add(content);
comps.each(model => clean(model)); comps.each(model => clean(model));
comps.trigger('resetNavigator'); comps.trigger('resetNavigator');

4
src/dom_components/view/ComponentVideoView.js

@ -1,5 +1,6 @@
var Backbone = require('backbone'); var Backbone = require('backbone');
var ComponentView = require('./ComponentImageView'); var ComponentView = require('./ComponentImageView');
var OComponentView = require('./ComponentView');
module.exports = ComponentView.extend({ module.exports = ComponentView.extend({
@ -8,7 +9,8 @@ module.exports = ComponentView.extend({
events: {}, events: {},
initialize(o) { initialize(o) {
ComponentView.prototype.initialize.apply(this, arguments); OComponentView.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change:src', this.updateSrc);
this.listenTo(this.model, 'change:loop change:autoplay change:controls change:color', this.updateVideo); this.listenTo(this.model, 'change:loop change:autoplay change:controls change:color', this.updateVideo);
this.listenTo(this.model, 'change:provider', this.updateProvider); this.listenTo(this.model, 'change:provider', this.updateProvider);
}, },

11
src/dom_components/view/ComponentView.js

@ -56,14 +56,11 @@ module.exports = Backbone.View.extend({
* @private * @private
*/ */
handleChange() { handleChange() {
var em = this.em; const model = this.model;
if(em) { model.emitUpdate();
var model = this.model;
em.trigger('component:update', model);
for(var prop in model.changed) { for (let prop in model.changed) {
em.trigger('component:update:' + prop, model); model.emitUpdate(prop);
}
} }
}, },

2
src/domain_abstract/ui/Input.js

@ -25,7 +25,7 @@ module.exports = Backbone.View.extend({
const ppfx = opts.ppfx || ''; const ppfx = opts.ppfx || '';
this.opts = opts; this.opts = opts;
this.ppfx = ppfx; this.ppfx = ppfx;
this.target = opts.target || {}; this.em = opts.target || {};
this.listenTo(this.model, 'change:value', this.handleModelChange); this.listenTo(this.model, 'change:value', this.handleModelChange);
}, },

7
src/domain_abstract/ui/InputColor.js

@ -58,7 +58,8 @@ module.exports = Input.extend({
var colorEl = $(`<div class="${this.ppfx}field-color-picker"></div>`); var colorEl = $(`<div class="${this.ppfx}field-color-picker"></div>`);
var cpStyle = colorEl.get(0).style; var cpStyle = colorEl.get(0).style;
var elToAppend = this.target && this.target.config ? this.target.config.el : ''; var elToAppend = this.em && this.em.config ? this.em.config.el : '';
var colorPickerConfig = this.em && this.em.getConfig && this.em.getConfig("colorPicker") || {};
const getColor = color => { const getColor = color => {
let cl = color.getAlpha() == 1 ? color.toHexString() : color.toRgbString(); let cl = color.getAlpha() == 1 ? color.toHexString() : color.toRgbString();
return cl.replace(/ /g, ''); return cl.replace(/ /g, '');
@ -76,6 +77,10 @@ module.exports = Input.extend({
chooseText: 'Ok', chooseText: 'Ok',
cancelText: '⨯', cancelText: '⨯',
palette: [], palette: [],
// config expanded here so that the functions below are not overridden
...colorPickerConfig,
move(color) { move(color) {
const cl = getColor(color); const cl = getColor(color);
cpStyle.backgroundColor = cl; cpStyle.backgroundColor = cl;

9
src/editor/config/config.js

@ -5,15 +5,9 @@ module.exports = {
//TEMP //TEMP
components: '', components: '',
// Enable/Disable possibility to copy(ctrl + c) & paste(ctrl + v) components
copyPaste: true,
// Show an alert before unload the page with unsaved changes // Show an alert before unload the page with unsaved changes
noticeOnUnload: true, noticeOnUnload: true,
// Enable/Disable undo manager
undoManager: true,
// Show paddings and margins // Show paddings and margins
showOffsets: false, showOffsets: false,
@ -80,6 +74,9 @@ module.exports = {
// Dom element // Dom element
el: '', el: '',
// Configurations for Undo Manager
undoManager: {},
//Configurations for Asset Manager //Configurations for Asset Manager
assetManager: {}, assetManager: {},

4
src/editor/index.js

@ -51,6 +51,8 @@
* * `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview')) * * `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview'))
* ## General * ## General
* * `canvasScroll` - Triggered when the canvas is scrolle * * `canvasScroll` - Triggered when the canvas is scrolle
* * `undo` - Undo executed
* * `redo` - Redo executed
* * `load` - When the editor is loaded * * `load` - When the editor is loaded
* *
* @param {Object} config Configurations * @param {Object} config Configurations
@ -58,7 +60,6 @@
* @param {string|Array<Object>} [config.components=''] HTML string or object of components * @param {string|Array<Object>} [config.components=''] HTML string or object of components
* @param {string|Array<Object>} [config.style=''] CSS string or object of rules * @param {string|Array<Object>} [config.style=''] CSS string or object of rules
* @param {Boolean} [config.fromElement=false] If true, will fetch HTML and CSS from selected container * @param {Boolean} [config.fromElement=false] If true, will fetch HTML and CSS from selected container
* @param {Boolean} [config.copyPaste=true] Enable/Disable the possibility to copy(ctrl + c) & paste(ctrl + v) components
* @param {Boolean} [config.undoManager=true] Enable/Disable undo manager * @param {Boolean} [config.undoManager=true] Enable/Disable undo manager
* @param {Boolean} [config.autorender=true] If true renders editor on init * @param {Boolean} [config.autorender=true] If true renders editor on init
* @param {Boolean} [config.noticeOnUnload=true] Enable/Disable alert message before unload the page * @param {Boolean} [config.noticeOnUnload=true] Enable/Disable alert message before unload the page
@ -571,6 +572,7 @@ module.exports = 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.on('loaded', () => {
this.UndoManager.clear();
em.get('modules').forEach(module => { em.get('modules').forEach(module => {
module.postRender && module.postRender(editorView); module.postRender && module.postRender(editorView);
}); });

194
src/editor/model/Editor.js

@ -3,6 +3,7 @@ import { isUndefined, defaults } from 'underscore';
const deps = [ const deps = [
require('utils'), require('utils'),
require('keymaps'), require('keymaps'),
require('undo_manager'),
require('storage_manager'), require('storage_manager'),
require('device_manager'), require('device_manager'),
require('parser'), require('parser'),
@ -22,8 +23,6 @@ const deps = [
]; ];
const Backbone = require('backbone'); const Backbone = require('backbone');
const UndoManager = require('backbone-undo');
const key = require('keymaster');
let timedInterval; let timedInterval;
require('utils/extender')({ require('utils/extender')({
@ -57,12 +56,7 @@ module.exports = Backbone.Model.extend({
this.config.components = c.el.innerHTML; this.config.components = c.el.innerHTML;
// Load modules // Load modules
deps.forEach(function(name){ deps.forEach(name => this.loadModule(name));
this.loadModule(name);
}, this);
this.initUndoManager();
this.on('change:selectedComponent', this.componentSelected, this); this.on('change:selectedComponent', this.componentSelected, this);
this.on('change:changesCount', this.updateChanges, this); this.on('change:changesCount', this.updateChanges, this);
}, },
@ -94,16 +88,10 @@ module.exports = Backbone.Model.extend({
module.onLoad(); module.onLoad();
}); });
// Stuff to do post load (eg. init undo manager for loaded components) // Stuff to do post load
const postLoad = () => { const postLoad = () => {
// I've initialized undo manager in initialize() because otherwise the const modules = this.get('modules');
// editor will unable to fetch the instance via 'editor.UndoManager' but modules.forEach(module => module.postLoad && module.postLoad(this));
// I need to cleare the stack now as it was dirtied by 'onLoad' method
this.um.clear();
this.initUndoManager();
this.get('modules').forEach(module =>
module.postLoad && module.postLoad(this)
);
clb && clb(); clb && clb();
}; };
@ -151,22 +139,20 @@ module.exports = Backbone.Model.extend({
// Check if module is storable // Check if module is storable
var sm = this.get('StorageManager'); var sm = this.get('StorageManager');
if(Mod.storageKey && Mod.store && Mod.load && sm){
if (Mod.storageKey && Mod.store && Mod.load && sm) {
cfg.stm = sm; cfg.stm = sm;
var storables = this.get('storables'); var storables = this.get('storables');
storables.push(Mod); storables.push(Mod);
this.set('storables', storables); this.set('storables', storables);
} }
cfg.em = this; cfg.em = this;
Mod.init({ ...cfg }); Mod.init({ ...cfg });
// Bind the module to the editor model if public // Bind the module to the editor model if public
if(!Mod.private) !Mod.private && this.set(Mod.name, Mod);
this.set(Mod.name, Mod); Mod.onLoad && this.get('toLoad').push(Mod);
if(Mod.onLoad)
this.get('toLoad').push(Mod);
this.get('modules').push(Mod); this.get('modules').push(Mod);
return this; return this;
}, },
@ -187,29 +173,6 @@ module.exports = Backbone.Model.extend({
}, },
/**
* Listen for new rules
* @param {Object} collection
* @private
*/
listenRules(collection) {
this.stopListening(collection, 'add remove', this.listenRule);
this.listenTo(collection, 'add remove', this.listenRule);
collection.each(function(model){
this.listenRule(model);
}, this);
},
/**
* Listen for rule changes
* @param {Object} model
* @private
*/
listenRule(model) {
this.stopListening(model, 'change:style', this.handleUpdates);
this.listenTo(model, 'change:style', this.handleUpdates);
},
/** /**
* This method handles updates on the editor and tries to store them * This method handles updates on the editor and tries to store them
* if requested and if the changesCount is exceeded * if requested and if the changesCount is exceeded
@ -232,82 +195,6 @@ module.exports = Backbone.Model.extend({
}, 0); }, 0);
}, },
/**
* Initialize Undo manager
* @private
* */
initUndoManager() {
const canvas = this.get('Canvas');
if (this.um) {
return;
}
var cmp = this.get('DomComponents');
if(cmp && this.config.undoManager) {
var that = this;
this.um = new UndoManager({
register: [cmp.getComponents(), this.get('CssComposer').getAll()],
track: true
});
this.UndoManager = this.um;
this.set('UndoManager', this.um);
key('⌘+z, ctrl+z', () => {
if (canvas.isInputFocused()) {
return;
}
that.um.undo(true);
that.trigger('component:update');
});
key('⌘+shift+z, ctrl+shift+z', () => {
if (canvas.isInputFocused()) {
return;
}
that.um.redo(true);
that.trigger('component:update');
});
var beforeCache;
const customUndoType = {
on: function (model, value, opts) {
var opt = opts || {};
if(!beforeCache){
beforeCache = model.previousAttributes();
}
if (opt && opt.avoidStore) {
return;
} else {
var obj = {
object: model,
before: beforeCache,
after: model.toJSON()
};
beforeCache = null;
return obj;
}
},
undo: function (model, bf, af, opt) {
model.set(bf);
// Update also inputs inside Style Manager
that.trigger('change:selectedComponent');
},
redo: function (model, bf, af, opt) {
model.set(af);
// Update also inputs inside Style Manager
that.trigger('change:selectedComponent');
}
};
UndoManager.removeUndoType("change");
UndoManager.addUndoType("change:style", customUndoType);
UndoManager.addUndoType("change:attributes", customUndoType);
UndoManager.addUndoType("change:content", customUndoType);
UndoManager.addUndoType("change:src", customUndoType);
}
},
/** /**
* Callback on component selection * Callback on component selection
@ -325,67 +212,6 @@ module.exports = Backbone.Model.extend({
} }
}, },
/**
* Triggered when components are updated
* @param {Object} model
* @param {Mixed} val Value
* @param {Object} opt Options
* @private
* */
updateComponents(model, val, opt) {
var comps = model.get('components'),
classes = model.get('classes'),
avSt = opt ? opt.avoidStore : 0;
// Observe component with Undo Manager
if(this.um)
this.um.register(comps);
// Call stopListening for not creating nested listeners
this.stopListening(comps, 'add', this.updateComponents);
this.stopListening(comps, 'remove', this.rmComponents);
this.listenTo(comps, 'add', this.updateComponents);
this.listenTo(comps, 'remove', this.rmComponents);
this.stopListening(classes, 'add remove', this.handleUpdates);
this.listenTo(classes, 'add remove', this.handleUpdates);
var evn = 'change:style change:content change:attributes change:src';
this.stopListening(model, evn, this.handleUpdates);
this.listenTo(model, evn, this.handleUpdates);
if(!avSt)
this.handleUpdates(model, val, opt);
},
/**
* Init stuff like storage for already existing elements
* @param {Object} model
* @private
*/
initChildrenComp(model) {
var comps = model.get('components');
this.updateComponents(model, null, { avoidStore : 1 });
comps.each(function(md) {
this.initChildrenComp(md);
if(this.um)
this.um.register(md);
}, this);
},
/**
* Triggered when some component is removed updated
* @param {Object} model
* @param {Mixed} val Value
* @param {Object} opt Options
* @private
* */
rmComponents(model, val, opt) {
var avSt = opt ? opt.avoidStore : 0;
if(!avSt)
this.handleUpdates(model, val, opt);
},
/** /**
* Returns model of the selected component * Returns model of the selected component

5
src/grapesjs/config/config.js

@ -15,11 +15,6 @@ module.exports = {
fromElement: 0, fromElement: 0,
// --- // ---
// Enable/Disable the possibility to copy(ctrl + c) & paste(ctrl + v) components
copyPaste: true,
// Enable/Disable undo manager
undoManager: true,
// Storage Manager // Storage Manager
storageManager: {}, storageManager: {},

4
src/grapesjs/index.js

@ -1,5 +1,8 @@
import $ from 'cash-dom'; import $ from 'cash-dom';
import { defaults } from 'underscore'; import { defaults } from 'underscore';
import polyfills from 'utils/polyfills';
polyfills();
module.exports = (() => { module.exports = (() => {
const defaultConfig = require('./config/config'); const defaultConfig = require('./config/config');
@ -26,7 +29,6 @@ module.exports = (() => {
* @param {Object|string} config.components='' HTML string or Component model in JSON format * @param {Object|string} config.components='' HTML string or Component model in JSON format
* @param {Object|string} config.style='' CSS string or CSS model in JSON format * @param {Object|string} config.style='' CSS string or CSS model in JSON format
* @param {Boolean} [config.fromElement=false] If true, will fetch HTML and CSS from selected container * @param {Boolean} [config.fromElement=false] If true, will fetch HTML and CSS from selected container
* @param {Boolean} [config.copyPaste=true] Enable/Disable the possibility to copy(ctrl+c) & paste(ctrl+v) components
* @param {Boolean} [config.undoManager=true] Enable/Disable undo manager * @param {Boolean} [config.undoManager=true] Enable/Disable undo manager
* @param {Array} [config.plugins=[]] Array of plugins to execute on start * @param {Array} [config.plugins=[]] Array of plugins to execute on start
* @return {grapesjs.Editor} GrapesJS editor instance * @return {grapesjs.Editor} GrapesJS editor instance

38
src/keymaps/index.js

@ -12,9 +12,29 @@ import { isString } from 'underscore';
const keymaster = require('keymaster'); const keymaster = require('keymaster');
module.exports = () => { module.exports = () => {
let em;
let config; let config;
const configDef = {};
const keymaps = {}; const keymaps = {};
const configDef = {
defaults: {
'core:undo': {
keys: '⌘+z, ctrl+z',
handler: 'core:undo',
},
'core:redo': {
keys: '⌘+shift+z, ctrl+shift+z',
handler: 'core:redo',
},
'core:copy': {
keys: '⌘+c, ctrl+c',
handler: 'core:copy',
},
'core:paste': {
keys: '⌘+v, ctrl+v',
handler: 'core:paste',
}
}
};
return { return {
@ -39,11 +59,23 @@ module.exports = () => {
* @private * @private
*/ */
init(opts = {}) { init(opts = {}) {
config = { ...opts, ...configDef }; config = { ...configDef, ...opts };
this.em = config.em; em = config.em;
this.em = em;
return this; return this;
}, },
onLoad() {
const defKeys = config.defaults;
for (let id in defKeys) {
const value = defKeys[id];
this.add(id, value.keys, value.handler);
}
},
/** /**
* Add new keymap * Add new keymap
* @param {string} id Keymap id * @param {string} id Keymap id

82
src/navigator/view/ItemView.js

@ -1,8 +1,8 @@
var Backbone = require('backbone'); import { isUndefined } from 'underscore';
var ComponentView = require('dom_components/view/ComponentView'); const ComponentView = require('dom_components/view/ComponentView');
var ItemsView; let ItemsView;
module.exports = Backbone.View.extend({ module.exports = require('backbone').View.extend({
events: { events: {
'mousedown [data-toggle-move]': 'startSort', 'mousedown [data-toggle-move]': 'startSort',
@ -13,28 +13,36 @@ module.exports = Backbone.View.extend({
'focusout input': 'handleEditEnd', 'focusout input': 'handleEditEnd',
}, },
template: _.template(`
<% if (hidable) { %>
<i id="<%= prefix %>btn-eye" class="btn fa fa-eye <%= (visible ? '' : 'fa-eye-slash') %>" data-toggle-visible></i>
<% } %>
<div class="<%= prefix %>title-c <%= ppfx %>one-bg">
<div class="<%= prefix %>title <%= addClass %>" style="padding-left: <%= 30 + level * 10 %>px" data-toggle-select>
<div class="<%= prefix %>title-inn">
<i id="<%= prefix %>caret" class="fa fa-chevron-right <%= caretCls %>" data-toggle-open></i>
<%= icon %>
<input class="<%= ppfx %>no-app <%= inputNameCls %>" value="<%= title %>" readonly>
</div>
</div>
</div>
<div id="<%= prefix %>counter"><%= (count ? count : '') %></div>
<div id="<%= prefix %>move" data-toggle-move> template(model) {
<i class="fa fa-arrows"></i> const pfx = this.pfx;
</div> const ppfx = this.ppfx;
const hidable = this.config.hidable;
const count = this.countChildren(model);
const addClass = !count ? `${pfx}no-chld` : '';
const level = this.level + 1;
return `
${ hidable ?
`<i id="${pfx}btn-eye" class="${pfx}btn fa fa-eye ${this.isVisible() ? '' : 'fa-eye-slash'}" data-toggle-visible></i>`
: ''}
<div class="${pfx}title-c ${ppfx}one-bg">
<div class="${pfx}title ${addClass}" style="padding-left: ${30 + level * 10}px" data-toggle-select>
<div class="${pfx}title-inn">
<i id="${pfx}caret" class="fa fa-chevron-right ${this.caretCls}" data-toggle-open></i>
${model.getIcon()}
<input class="${ppfx}no-app ${this.inputNameCls}" value="${model.getName()}" readonly>
</div>
</div>
</div>
<div id="${pfx}counter">${ count ? count : '' }</div>
<div id="${pfx}move" data-toggle-move>
<i class="fa fa-arrows"></i>
</div>
<div class="${pfx}children"></div>
`;
},
<div class="<%= prefix %>children"></div>`),
initialize(o = {}) { initialize(o = {}) {
this.opt = o; this.opt = o;
@ -291,29 +299,14 @@ module.exports = Backbone.View.extend({
}, },
render() { render() {
let model = this.model; const model = this.model;
var pfx = this.pfx; var pfx = this.pfx;
var vis = this.isVisible(); var vis = this.isVisible();
var count = this.countChildren(model);
const el = this.$el; const el = this.$el;
const level = this.level + 1; const level = this.level + 1;
el.html(this.template(model));
el.html(this.template({ if (isUndefined(ItemsView)) {
title: model.getName(),
icon: model.getIcon(),
addClass: (count ? '' : pfx+'no-chld'),
editBtnCls: this.editBtnCls,
inputNameCls: this.inputNameCls,
caretCls: this.caretCls,
count,
visible: vis,
hidable: this.config.hidable,
prefix: pfx,
ppfx: this.ppfx,
level
}));
if (typeof ItemsView == 'undefined') {
ItemsView = require('./ItemsView'); ItemsView = require('./ItemsView');
} }
@ -331,9 +324,8 @@ module.exports = Backbone.View.extend({
el.children(`#${pfx}move`).remove(); el.children(`#${pfx}move`).remove();
} }
if(!vis) !vis && (this.className += ` ${pfx}hide`);
this.className += ' ' + pfx + 'hide'; el.attr('class', this.className);
el.attr('class', _.result(this, 'className'));
this.updateOpening(); this.updateOpening();
this.updateStatus(); this.updateStatus();
this.updateVisibility(); this.updateVisibility();

8
src/panels/view/ButtonView.js

@ -203,12 +203,12 @@ module.exports = Backbone.View.extend({
const model = this.model; const model = this.model;
const context = model.get('context'); const context = model.get('context');
const parent = this.parentM; const parent = this.parentM;
var command = null; let command = {};
var editor = this.em && this.em.get ? this.em.get('Editor') : null; var editor = this.em && this.em.get ? this.em.get('Editor') : null;
var commandName = model.get('command'); var commandName = model.get('command');
if (this.commands && isString(commandName)) { if (this.commands && isString(commandName)) {
command = this.commands.get(commandName); command = this.commands.get(commandName) || {};
} else if (isFunction(commandName)) { } else if (isFunction(commandName)) {
command = { run: commandName }; command = { run: commandName };
} else if (commandName !== null && isObject(commandName)) { } else if (commandName !== null && isObject(commandName)) {
@ -220,7 +220,7 @@ module.exports = Backbone.View.extend({
model.set('active', true, { silent: true }).trigger('checkActive'); model.set('active', true, { silent: true }).trigger('checkActive');
parent && parent.set('active', true, { silent: true }).trigger('checkActive'); parent && parent.set('active', true, { silent: true }).trigger('checkActive');
if (command && command.run) { if (command.run) {
command.run(editor, model, model.get('options')); command.run(editor, model, model.get('options'));
editor.trigger('run:' + commandName); editor.trigger('run:' + commandName);
} }
@ -232,7 +232,7 @@ module.exports = Backbone.View.extend({
model.collection.deactivateAll(context); model.collection.deactivateAll(context);
parent && parent.set('active', false, { silent: true }).trigger('checkActive'); parent && parent.set('active', false, { silent: true }).trigger('checkActive');
if (command && command.stop) { if (command.stop) {
command.stop(editor, model, model.get('options')); command.stop(editor, model, model.get('options'));
editor.trigger('stop:' + commandName); editor.trigger('stop:' + commandName);
} }

2
src/rich_text_editor/index.js

@ -280,7 +280,7 @@ module.exports = () => {
if (customRte) { if (customRte) {
customRte.disable(el, rte); customRte.disable(el, rte);
} else { } else {
rte.disable(); rte && rte.disable();
} }
hideToolbar(); hideToolbar();

15
src/selector_manager/model/Selectors.js

@ -1,21 +1,22 @@
var Backbone = require('backbone'); import { filter } from 'underscore';
var Selector = require('./Selector'); const Selector = require('./Selector');
module.exports = Backbone.Collection.extend({ module.exports = require('backbone').Collection.extend({
model: Selector, model: Selector,
getStyleable() { getStyleable() {
return _.filter(this.models, item => return filter(this.models, item =>
item.get('active') && !item.get('private')); item.get('active') && !item.get('private'));
}, },
getValid() { getValid() {
return _.filter(this.models, item => !item.get('private')); return filter(this.models, item => !item.get('private'));
}, },
getFullString() { getFullString(collection) {
const result = []; const result = [];
this.each(selector => result.push(selector.getFullName())); const coll = collection || this;
coll.forEach(selector => result.push(selector.getFullName()));
return result.join('').trim(); return result.join('').trim();
} }
}); });

118
src/selector_manager/view/ClassTagView.js

@ -1,74 +1,97 @@
var Backbone = require('backbone');
const Selector = require('./../model/Selector'); const Selector = require('./../model/Selector');
const inputProp = 'contentEditable';
module.exports = require('backbone').View.extend({
template() {
const pfx = this.pfx;
const ppfx = this.ppfx;
const label = this.model.get('label') || '';
return `
<span id="${pfx}checkbox" class="fa" data-tag-status></span>
<span id="${pfx}tag-label" data-tag-name>${label}</span>
<span id="${pfx}close" data-tag-remove>
&Cross;
</span>
`;
},
events: {
'click [data-tag-remove]': 'removeTag',
'click [data-tag-status]': 'changeStatus',
'dblclick [data-tag-name]': 'startEditTag',
'focusout [data-tag-name]': 'endEditTag',
},
module.exports = Backbone.View.extend({
template: _.template(`
<span id="<%= pfx %>checkbox" class="fa"></span>
<span id="<%= pfx %>tag-label">
<input class="<%= ppfx %>no-app" value="<%= label %>" <%= inputProp %>/>
</span>
<span id="<%= pfx %>close">&Cross;</span>`),
initialize(o) { initialize(o) {
this.config = o.config || {}; this.config = o.config || {};
this.coll = o.coll || null; this.coll = o.coll || null;
this.pfx = this.config.stylePrefix || ''; this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || ''; this.ppfx = this.config.pStylePrefix || '';
this.inputProp = 'readonly';
this.target = this.config.em; this.target = this.config.em;
this.closeId = this.pfx + 'close'; this.listenTo(this.model, 'change:active', this.updateStatus);
this.chkId = this.pfx + 'checkbox';
this.labelId = this.pfx + 'tag-label';
this.events = {};
this.events['click #' + this.closeId ] = 'removeTag';
this.events['click #' + this.chkId ] = 'changeStatus';
this.events['dblclick #' + this.labelId ] = 'startEditTag';
this.events['keypress #' + this.labelId + ' input'] = 'updateInputLabel';
this.events['blur #' + this.labelId + ' input'] = 'endEditTag';
this.listenTo( this.model, 'change:active', this.updateStatus);
this.delegateEvents();
}, },
/**
* Returns the element which containes the anme of the tag
* @return {HTMLElement}
*/
getInputEl() {
if (!this.inputEl) {
this.inputEl = this.el.querySelector('[data-tag-name]');
}
return this.inputEl;
},
/** /**
* Start editing tag * Start editing tag
* @private * @private
*/ */
startEditTag() { startEditTag() {
this.$labelInput.prop(this.inputProp, false); const inputEl = this.getInputEl();
inputEl[inputProp] = true;
inputEl.focus();
}, },
/** /**
* End editing tag. If the class typed already exists the * End editing tag. If the class typed already exists the
* old one will be restored otherwise will be changed * old one will be restored otherwise will be changed
* @private * @private
*/ */
endEditTag() { endEditTag() {
var value = this.$labelInput.val(); const model = this.model;
var next = Selector.escapeName(value); const inputEl = this.getInputEl();
const label = inputEl.textContent;
if(this.target){ const name = Selector.escapeName(label);
var clsm = this.target.get('SelectorManager'); const em = this.target;
const sm = em && em.get('SelectorManager');
if(clsm){ inputEl[inputProp] = false;
if(clsm.get(next))
this.$labelInput.val(this.model.get('label')); if (sm) {
else if (sm.get(name)) {
this.model.set({ name: next, label: value}); inputEl.innerText = model.get('label');
} else {
model.set({ name, label });
} }
} }
this.$labelInput.prop(this.inputProp, true);
}, },
/** /**
* Update status of the tag * Update status of the tag
* @private * @private
*/ */
changeStatus() { changeStatus() {
this.model.set('active', !this.model.get('active')); this.model.set('active', !this.model.get('active'));
this.target.trigger('targetClassUpdated');
}, },
/** /**
* Remove tag from the selected component * Remove tag from the selected component
* @param {Object} e * @param {Object} e
@ -83,9 +106,9 @@ module.exports = Backbone.View.extend({
sel && sel.get & sel.get('classes').remove(model); sel && sel.get & sel.get('classes').remove(model);
coll && coll.remove(model); coll && coll.remove(model);
setTimeout(() => this.remove(), 0); setTimeout(() => this.remove(), 0);
em && em.trigger('targetClassRemoved');
}, },
/** /**
* Update status of the checkbox * Update status of the checkbox
* @private * @private
@ -106,34 +129,13 @@ module.exports = Backbone.View.extend({
} }
}, },
/**
* Update label's input
* @private
*/
updateInputLabel() {
if(!this.$labelInput) {
this.$labelInput = this.$el.find('input');
}
this.$labelInput.prop(this.inputProp, true);
var size = this.$labelInput.val().length - 1;
size = size < 1 ? 1 : size;
this.$labelInput.attr('size', size);
},
render() { render() {
const pfx = this.pfx; const pfx = this.pfx;
const ppfx = this.ppfx; const ppfx = this.ppfx;
this.$el.html( this.template({ this.$el.html(this.template());
label: this.model.get('label'),
pfx,
ppfx,
inputProp: this.inputProp,
}));
this.$el.attr('class', `${pfx}tag ${ppfx}three-bg`); this.$el.attr('class', `${pfx}tag ${ppfx}three-bg`);
this.updateStatus(); this.updateStatus();
this.updateInputLabel();
return this; return this;
}, },

12
src/selector_manager/view/ClassTagsView.js

@ -52,7 +52,7 @@ module.exports = Backbone.View.extend({
this.em = this.target; this.em = this.target;
this.listenTo(this.target ,'change:selectedComponent',this.componentChanged); this.listenTo(this.target ,'change:selectedComponent',this.componentChanged);
this.listenTo(this.target, 'targetClassUpdated', this.updateSelector); this.listenTo(this.target, 'component:update:classes', this.updateSelector);
this.listenTo(this.collection, 'add', this.addNew); this.listenTo(this.collection, 'add', this.addNew);
this.listenTo(this.collection, 'reset', this.renderClasses); this.listenTo(this.collection, 'reset', this.renderClasses);
@ -174,7 +174,8 @@ module.exports = Backbone.View.extend({
} }
const state = selected.get('state'); const state = selected.get('state');
let result = this.collection.getFullString(); const coll = this.collection;
let result = coll.getFullString(coll.getStyleable());
result = result || `#${selected.getId()}`; result = result || `#${selected.getId()}`;
result += state ? `:${state}` : ''; result += state ? `:${state}` : '';
const el = this.el.querySelector('#' + this.pfx + 'sel'); const el = this.el.querySelector('#' + this.pfx + 'sel');
@ -190,8 +191,6 @@ module.exports = Backbone.View.extend({
stateChanged(e) { stateChanged(e) {
if(this.compTarget){ if(this.compTarget){
this.compTarget.set('state', this.$states.val()); this.compTarget.set('state', this.$states.val());
if(this.target)
this.target.trigger('targetStateUpdated');
this.updateSelector(); this.updateSelector();
} }
}, },
@ -219,11 +218,6 @@ module.exports = Backbone.View.extend({
compCls.add(model); compCls.add(model);
var lenA = compCls.length; var lenA = compCls.length;
this.collection.add(model); this.collection.add(model);
if (lenA > lenB) {
target.trigger('targetClassAdded');
}
this.updateStateVis(); this.updateStateVis();
} }
} }

2
src/storage_manager/index.js

@ -199,8 +199,8 @@ module.exports = () => {
st && st.load(keysF, res => { st && st.load(keysF, res => {
// Restore keys name // Restore keys name
var reg = new RegExp('^' + c.id + '');
for (var itemKey in res) { for (var itemKey in res) {
var reg = new RegExp('^' + c.id + '');
var itemKeyR = itemKey.replace(reg, ''); var itemKeyR = itemKey.replace(reg, '');
result[itemKeyR] = res[itemKey]; result[itemKeyR] = res[itemKey];
} }

2
src/style_manager/config/config.js

@ -21,7 +21,7 @@ module.exports = {
showComputed: true, showComputed: true,
// Adds the possibility to clear property value from the target style // Adds the possibility to clear property value from the target style
clearProperties: false, clearProperties: 0,
// Properties not to take in account for computed styles // Properties not to take in account for computed styles
avoidComputed: ['width', 'height'], avoidComputed: ['width', 'height'],

64
src/style_manager/index.js

@ -1,18 +1,4 @@
/** /**
*
* * [addSector](#addsector)
* * [getSector](#getsector)
* * [getSectors](#getsectors)
* * [addProperty](#addproperty)
* * [getProperty](#getproperty)
* * [getProperties](#getproperties)
* * [getModelToStyle](#getmodeltostyle)
* * [addType](#addtype)
* * [getType](#gettype)
* * [getTypes](#gettypes)
* * [createType](#createtype)
* * [render](#render)
*
* With Style Manager you basically build categories (called sectors) of CSS properties which could * With Style Manager you basically build categories (called sectors) of CSS properties which could
* be used to custom components and classes. * be used to custom components and classes.
* You can init the editor with all sectors and properties via configuration * You can init the editor with all sectors and properties via configuration
@ -146,6 +132,18 @@ module.exports = () => {
}, },
/**
* Remove a sector by id
* @param {string} id Sector id
* @return {Sector} Removed sector
* @example
* const removed = styleManager.removeSector('mySector');
*/
removeSector(id) {
return this.getSectors().remove(this.getSector(id));
},
/** /**
* Get all sectors * Get all sectors
* @return {Sectors} Collection of sectors * @return {Sectors} Collection of sectors
@ -222,6 +220,20 @@ module.exports = () => {
}, },
/**
* Remove a property from the sector
* @param {string} sectorId Sector id
* @param {string} name CSS property name, eg. 'min-height'
* @return {Property} Removed property
* @example
* const property = styleManager.removeProperty('mySector', 'min-height');
*/
removeProperty(sectorId, name) {
const props = this.getProperties(sectorId);
return props && props.remove(this.getProperty(sectorId, name));
},
/** /**
* Get properties of the sector * Get properties of the sector
* @param {string} sectorId Sector id * @param {string} sectorId Sector id
@ -249,19 +261,25 @@ module.exports = () => {
* @return {Model} * @return {Model}
*/ */
getModelToStyle(model) { getModelToStyle(model) {
const classes = model.get('classes');
const em = c.em; const em = c.em;
const classes = model.get('classes');
const id = model.getId();
if (em && classes && classes.length) { if (em) {
const conf = em.get('Config'); const config = em.getConfig();
const state = !conf.devicePreviewMode ? model.get('state') : '';
const deviceW = em.getCurrentMedia();
const cssC = em.get('CssComposer'); const cssC = em.get('CssComposer');
const state = !config.devicePreviewMode ? model.get('state') : '';
const valid = classes.getStyleable(); const valid = classes.getStyleable();
const CssRule = cssC.get(valid, state, deviceW); const hasClasses = valid.length;
const opts = { state };
if(CssRule && valid.length) {
return CssRule; if (hasClasses) {
const deviceW = em.getCurrentMedia();
const CssRule = cssC.get(valid, state, deviceW);
if (CssRule) return CssRule;
} else if (config.avoidInlineStyle) {
const rule = cssC.getIdRule(id, opts);
return rule ? rule : cssC.setIdRule(id, {}, opts);
} }
} }

17
src/style_manager/view/PropertyView.js

@ -1,6 +1,8 @@
import { bindAll, isArray } from 'underscore'; import { bindAll, isArray } from 'underscore';
import { camelCase } from 'utils/mixins'; import { camelCase } from 'utils/mixins';
const clearProp = 'data-clear-style';
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
template(model) { template(model) {
@ -23,7 +25,7 @@ module.exports = Backbone.View.extend({
<span class="${pfx}icon ${icon}" title="${info}"> <span class="${pfx}icon ${icon}" title="${info}">
${model.get('name')} ${model.get('name')}
</span> </span>
<b class="${pfx}clear">&Cross;</b> <b class="${pfx}clear" ${clearProp}>&Cross;</b>
`; `;
}, },
@ -36,7 +38,8 @@ module.exports = Backbone.View.extend({
}, },
events: { events: {
'change': 'inputValueChanged' 'change': 'inputValueChanged',
[`click [${clearProp}]`]: 'clear',
}, },
initialize(o = {}) { initialize(o = {}) {
@ -70,8 +73,6 @@ module.exports = Backbone.View.extend({
this.listenTo(model, 'targetUpdated', this.targetUpdated); this.listenTo(model, 'targetUpdated', this.targetUpdated);
this.listenTo(model, 'change:visible', this.updateVisibility); this.listenTo(model, 'change:visible', this.updateVisibility);
this.listenTo(model, 'change:status', this.updateStatus); this.listenTo(model, 'change:status', this.updateStatus);
this.events[`click .${pfx}clear`] = 'clear';
this.delegateEvents();
const init = this.init && this.init.bind(this); const init = this.init && this.init.bind(this);
init && init(); init && init();
@ -122,7 +123,7 @@ module.exports = Backbone.View.extend({
* @return {HTMLElement} * @return {HTMLElement}
*/ */
getClearEl() { getClearEl() {
return this.el.querySelector(`.${this.pfx}clear`); return this.el.querySelector(`[${clearProp}]`);
}, },
/** /**
@ -159,14 +160,12 @@ module.exports = Backbone.View.extend({
this.elementUpdated(); this.elementUpdated();
}, },
/** /**
* Fired when the element of the property is updated * Fired when the element of the property is updated
*/ */
elementUpdated() { elementUpdated() {
this.model.set('status', 'updated'); this.setStatus('updated');
const parent = this.model.parent;
const parentView = parent && parent.view;
parentView && parentView.elementUpdated();
}, },

1
src/style_manager/view/SectorView.js

@ -23,6 +23,7 @@ module.exports = Backbone.View.extend({
const model = this.model; const model = this.model;
this.listenTo(model, 'change:open', this.updateOpen); this.listenTo(model, 'change:open', this.updateOpen);
this.listenTo(model, 'updateVisibility', this.updateVisibility); this.listenTo(model, 'updateVisibility', this.updateVisibility);
this.listenTo(model, 'destroy remove', this.remove);
}, },
/** /**

8
src/style_manager/view/SectorsView.js

@ -19,10 +19,10 @@ module.exports = Backbone.View.extend({
body.removeChild(dummy); body.removeChild(dummy);
this.propTarget = target; this.propTarget = target;
const coll = this.collection; const coll = this.collection;
const events = 'change:selectedComponent component:update:classes change:device';
this.listenTo(coll, 'add', this.addTo); this.listenTo(coll, 'add', this.addTo);
this.listenTo(coll, 'reset', this.render); this.listenTo(coll, 'reset', this.render);
this.listenTo(this.target, 'change:selectedComponent targetClassAdded targetClassRemoved targetClassUpdated ' + this.listenTo(this.target, events, this.targetUpdated);
'targetStateUpdated targetStyleUpdated change:device', this.targetUpdated);
}, },
@ -105,11 +105,11 @@ module.exports = Backbone.View.extend({
// selecting the component) and calling undo() it will remove the rule from // selecting the component) and calling undo() it will remove the rule from
// the collection, therefore updating it in style manager will not affect it // the collection, therefore updating it in style manager will not affect it
// #268 // #268
um.stopTracking(); um.stop();
iContainer = cc.add(valid, state, media); iContainer = cc.add(valid, state, media);
iContainer.setStyle(model.getStyle()); iContainer.setStyle(model.getStyle());
model.setStyle({}); model.setStyle({});
um.startTracking(); um.start();
} }
if (!iContainer) { if (!iContainer) {

33
src/styles/scss/_gjs_canvas.scss

@ -1,23 +1,17 @@
$frameAnimation: 0.35s ease;
$canvasTop: 40px;
.#{$cv-prefix}canvas { .#{$cv-prefix}canvas {
background-color: rgba(0, 0, 0, 0.15); background-color: rgba(0, 0, 0, 0.15);
box-sizing: border-box; box-sizing: border-box;
position: absolute;
width: (100% - $leftWidth); width: (100% - $leftWidth);
height: 100%; height: calc(100% - $canvasTop);
bottom: 0; bottom: 0;
left: 0;
overflow: hidden; overflow: hidden;
padding-top: 40px;
z-index: 1; z-index: 1;
position: absolute;
> iframe { left: 0;
height: 100%; top: 40px;
outline: medium none;
width: 100%;
border: none;
margin: 0 auto;
display: block;
}
.#{$app-prefix}ghost { .#{$app-prefix}ghost {
display: none; display: none;
@ -69,7 +63,18 @@
} }
.#{$app-prefix}frame { .#{$app-prefix}frame {
transition: width 0.35s ease; outline: medium none;
height: 100%;
width: 100%;
border: none;
margin: auto;
display: block;
transition: width $frameAnimation, height $frameAnimation;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
} }
.#{$app-prefix}toolbar { .#{$app-prefix}toolbar {

9
src/styles/scss/main.scss

@ -573,13 +573,8 @@ $prefix: $app-prefix;
} }
##{$clm-prefix}tag-label { ##{$clm-prefix}tag-label {
cursor: text; padding: 0 3px;
@extend .no-select; cursor: text;
input {
width: auto;
padding: 0 4px;
}
} }
} }
/********* END Class manager **********/ /********* END Class manager **********/

45
src/trait_manager/model/Trait.js

@ -1,6 +1,6 @@
var Backbone = require('backbone'); import { isUndefined } from 'underscore';
module.exports = Backbone.Model.extend({ module.exports = require('backbone').Model.extend({
defaults: { defaults: {
type: 'text', // text, number, range, select type: 'text', // text, number, range, select
@ -16,13 +16,50 @@ module.exports = Backbone.Model.extend({
options: [], options: [],
}, },
initialize() { initialize() {
if (this.get('target')) { const target = this.get('target');
this.target = this.get('target'); const name = this.get('name');
const changeProp = this.get('changeProp');
if (target) {
this.target = target;
this.unset('target'); this.unset('target');
const targetEvent = changeProp ? `change:${name}` : `change:attributes:${name}`;
this.listenTo(target, targetEvent, this.targetUpdated);
}
},
targetUpdated() {
const value = this.getTargetValue();
!isUndefined(value) && this.set({ value }, { fromTarget: 1 });
},
getTargetValue() {
const name = this.get('name');
const target = this.target;
const prop = this.get('changeProp');
if (target) return prop ? target.get(name) : target.getAttributes()[name];
},
setTargetValue(value) {
const target = this.target;
const name = this.get('name');
if (isUndefined(value)) return;
if (this.get('changeProp')) {
target.set(name, value);
} else {
const attrs = { ...target.get('attributes') };
attrs[name] = value;
target.set('attributes', attrs);
} }
}, },
/** /**
* Get the initial value of the trait * Get the initial value of the trait
* @return {string} * @return {string}

1
src/trait_manager/view/TraitColorView.js

@ -11,6 +11,7 @@ module.exports = TraitView.extend({
if (!this.$input) { if (!this.$input) {
var value = this.getModelValue(); var value = this.getModelValue();
var inputNumber = new InputColor({ var inputNumber = new InputColor({
target: this.config.em,
contClass: this.ppfx + 'field-color', contClass: this.ppfx + 'field-color',
model: this.model, model: this.model,
ppfx: this.ppfx ppfx: this.ppfx

48
src/trait_manager/view/TraitView.js

@ -1,3 +1,6 @@
import { isUndefined, clone } from 'underscore';
const Backbone = require('backbone');
const $ = Backbone.$; const $ = Backbone.$;
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
@ -6,21 +9,28 @@ module.exports = Backbone.View.extend({
'change': 'onChange' 'change': 'onChange'
}, },
attributes() {
return this.model.get('attributes');
},
initialize(o) { initialize(o) {
var md = this.model; const model = this.model;
const name = model.get('name');
const target = model.target;
this.config = o.config || {}; this.config = o.config || {};
this.pfx = this.config.stylePrefix || ''; this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || ''; this.ppfx = this.config.pStylePrefix || '';
this.target = md.target; this.target = target;
this.className = this.pfx + 'trait'; this.className = this.pfx + 'trait';
this.labelClass = this.ppfx + 'label'; this.labelClass = this.ppfx + 'label';
this.fieldClass = this.ppfx + 'field ' + this.ppfx + 'field-' + md.get('type'); this.fieldClass = this.ppfx + 'field ' + this.ppfx + 'field-' + model.get('type');
this.inputhClass = this.ppfx + 'input-holder'; this.inputhClass = this.ppfx + 'input-holder';
md.off('change:value', this.onValueChange); model.off('change:value', this.onValueChange);
this.listenTo(md, 'change:value', this.onValueChange); this.listenTo(model, 'change:value', this.onValueChange);
this.tmpl = '<div class="' + this.fieldClass +'"><div class="' + this.inputhClass +'"></div></div>'; this.tmpl = '<div class="' + this.fieldClass +'"><div class="' + this.inputhClass +'"></div></div>';
}, },
/** /**
* Fires when the input is changed * Fires when the input is changed
* @private * @private
@ -33,22 +43,26 @@ module.exports = Backbone.View.extend({
return this.model.get('value'); return this.model.get('value');
}, },
setInputValue(value) {
this.getInputEl().value = value;
},
/** /**
* On change callback * On change callback
* @private * @private
*/ */
onValueChange() { onValueChange(model, value, opts = {}) {
var m = this.model; const mod = this.model;
var trg = this.target; const trg = this.target;
var name = m.get('name'); const name = mod.get('name');
var value = this.getValueForTarget();
// Chabge property if requested otherwise attributes if (opts.fromTarget) {
if(m.get('changeProp')){ this.setInputValue(mod.get('value'));
trg.set(name, value); } else {
}else{ const value = this.getValueForTarget();
var attrs = _.clone(trg.get('attributes')); mod.setTargetValue(value);
attrs[name] = value;
trg.set('attributes', attrs);
} }
}, },

242
src/undo_manager/index.js

@ -0,0 +1,242 @@
/**
* This module allows to manage the stack of changes applied in canvas
*
* You can access the module in this way
* ```js
* const um = editor.UndoManager;
* ```
*
*/
import UndoManager from 'backbone-undo';
module.exports = () => {
let em;
let um;
let config;
let beforeCache;
const configDef = {};
return {
name: 'UndoManager',
/**
* Initialize module
* @param {Object} config Configurations
* @private
*/
init(opts = {}) {
config = { ...opts, ...configDef };
em = config.em;
this.em = em;
um = new UndoManager({ track: true, register: [] });
um.changeUndoType('change', { condition: false });
const customUndoType = {
on(object, value, opt = {}) {
!beforeCache && (beforeCache = object.previousAttributes());
if (opt.avoidStore) {
return;
} else {
const result = {
object,
before: beforeCache,
after: object.toJSON()
};
beforeCache = null;
return result;
}
},
undo(model, bf, af, opt) {
model.set(bf);
},
redo(model, bf, af, opt) {
model.set(af);
}
};
const events = ['style', 'attributes', 'content', 'src'];
events.forEach(ev => um.addUndoType(`change:${ev}`, customUndoType));
um.on('undo redo', () => em.trigger('change:selectedComponent change:canvasOffset'));
['undo', 'redo'].forEach(ev => um.on(ev, () => em.trigger(ev)));
return this;
},
/**
* Get module configurations
* @return {Object} Configuration object
* @example
* const config = um.getConfig();
* // { ... }
*/
getConfig() {
return config;
},
/**
* Add an entity (Model/Collection) to track
* Note: New Components and CSSRules will be added automatically
* @param {Model|Collection} entity Entity to track
* @return {this}
* @example
* um.add(someModelOrCollection);
*/
add(entity) {
um.register(entity);
return this;
},
/**
* Remove and stop tracking the entity (Model/Collection)
* @param {Model|Collection} entity Entity to remove
* @return {this}
* @example
* um.remove(someModelOrCollection);
*/
remove(entity) {
um.unregister(entity);
return this;
},
/**
* Remove all entities
* @return {this}
* @example
* um.removeAll();
*/
removeAll() {
um.unregisterAll();
return this;
},
/**
* Start/resume tracking changes
* @return {this}
* @example
* um.start();
*/
start() {
um.startTracking();
return this;
},
/**
* Stop tracking changes
* @return {this}
* @example
* um.stop();
*/
stop() {
um.stopTracking();
return this;
},
/**
* Undo last change
* @return {this}
* @example
* um.undo();
*/
undo() {
if (!em.get('Canvas').isInputFocused()) um.undo(1);
return this;
},
/**
* Undo all changes
* @return {this}
* @example
* um.undoAll();
*/
undoAll() {
um.undoAll();
return this;
},
/**
* Redo last change
* @return {this}
* @example
* um.redo();
*/
redo() {
if (!em.get('Canvas').isInputFocused()) um.redo(1);
return this;
},
/**
* Redo all changes
* @return {this}
* @example
* um.redoAll();
*/
redoAll() {
um.redoAll();
return this;
},
/**
* Checks if exists an available undo
* @return {Boolean}
* @example
* um.hasUndo();
*/
hasUndo() {
return um.isAvailable('undo');
},
/**
* Checks if exists an available redo
* @return {Boolean}
* @example
* um.hasRedo();
*/
hasRedo() {
return um.isAvailable('redo');
},
/**
* Get stack of changes
* @return {Collection}
* @example
* const stack = um.getStack();
* stack.each(item => ...);
*/
getStack() {
return um.stack;
},
/**
* Clear the stack
* @return {this}
* @example
* um.clear();
*/
clear() {
um.clear();
return this;
},
getInstance() {
return um;
}
};
};

19
src/utils/Sorter.js

@ -94,18 +94,17 @@ module.exports = Backbone.View.extend({
var pfx = this.ppfx || this.pfx; var pfx = this.ppfx || this.pfx;
var sortCls = pfx + 'grabbing'; var sortCls = pfx + 'grabbing';
var emBody = em ? em.get('Canvas').getBody() : ''; var emBody = em ? em.get('Canvas').getBody() : '';
if(active) {
// Avoid updating body className as it causes a huge repaint
// Noticeable with "fast" drag of blocks
if (active) {
em && em.get('Canvas').startAutoscroll(); em && em.get('Canvas').startAutoscroll();
body.className += ' ' + sortCls; //body.className += ' ' + sortCls;
if(em) { //if (em) emBody.className += ' ' + sortCls;
emBody.className += ' ' + sortCls;
}
} else { } else {
em && em.get('Canvas').stopAutoscroll(); em && em.get('Canvas').stopAutoscroll();
body.className = body.className.replace(sortCls, '').trim(); //body.className = body.className.replace(sortCls, '').trim();
if(em) { //if(em) emBody.className = emBody.className.replace(sortCls, '').trim();
emBody.className = emBody.className.replace(sortCls, '').trim();
}
} }
}, },
@ -925,7 +924,7 @@ module.exports = Backbone.View.extend({
var dragHelper = this.dragHelper; var dragHelper = this.dragHelper;
if(dragHelper) { if(dragHelper) {
dragHelper.remove(); dragHelper.parentNode.removeChild(dragHelper);
this.dragHelper = null; this.dragHelper = null;
} }

38
src/utils/polyfills.js

@ -0,0 +1,38 @@
/**
* File made for IE/Edge support
* https://github.com/artf/grapesjs/issues/214
*/
export default () => {
/**
* Check if IE/Edge
* @return {Boolean}
*/
const isIE = () => {
let match;
const agent = window.navigator.userAgent;
const rules = [
[ 'edge', /Edge\/([0-9\._]+)/ ],
[ 'ie', /MSIE\s(7\.0)/ ],
[ 'ie', /MSIE\s([0-9\.]+);.*Trident\/[4-7].0/ ],
[ 'ie', /Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/ ],
];
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
match = rule[1].exec(agent);
if (match) break;
}
return !!match;
}
if (isIE()) {
const originalCreateHTMLDocument = DOMImplementation.prototype.createHTMLDocument
DOMImplementation.prototype.createHTMLDocument = (title) => {
if (!title) title = '';
return originalCreateHTMLDocument.apply(document.implementation, [title]);
}
}
}

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

@ -170,6 +170,41 @@ module.exports = {
expect(result.length).toEqual(3); expect(result.length).toEqual(3);
}); });
it('removeClass by string', () => {
obj.addClass(['class1', 'class2']);
obj.removeClass('class2');
const result = obj.get('classes').models;
expect(result.length).toEqual(1);
});
it('removeClass by string with multiple classes', () => {
obj.addClass(['class1', 'class2']);
obj.removeClass('class2 class1');
const result = obj.get('classes').models;
expect(result.length).toEqual(0);
});
it('removeClass by array', () => {
obj.addClass(['class1', 'class2']);
obj.removeClass(['class1', 'class2']);
const result = obj.get('classes').models;
expect(result.length).toEqual(0);
});
it('removeClass do nothing with undefined classes', () => {
obj.addClass(['class1', 'class2']);
obj.removeClass(['class3']);
const result = obj.get('classes').models;
expect(result.length).toEqual(2);
});
it('removeClass actually removes classes from attributes', () => {
obj.addClass('class1');
obj.removeClass('class1');
const result = obj.getAttributes();
expect(result.class).toEqual(undefined);
});
it('setAttributes', () => { it('setAttributes', () => {
obj.setAttributes({ obj.setAttributes({
id: 'test', id: 'test',

13
test/specs/dom_components/view/ComponentV.js

@ -1,6 +1,7 @@
const ComponentView = require('dom_components/view/ComponentView'); const ComponentView = require('dom_components/view/ComponentView');
const Component = require('dom_components/model/Component'); const Component = require('dom_components/model/Component');
const DomComponents = require('dom_components'); const DomComponents = require('dom_components');
const Editor = require('editor/model/Editor');
module.exports = { module.exports = {
run() { run() {
@ -13,13 +14,16 @@ module.exports = {
var hClass = 'hc-state'; var hClass = 'hc-state';
var dcomp; var dcomp;
var compOpts; var compOpts;
let em;
beforeEach(() => { beforeEach(() => {
em = new Editor({});
dcomp = new DomComponents(); dcomp = new DomComponents();
compOpts = { compOpts = {
em,
componentTypes: dcomp.componentTypes, componentTypes: dcomp.componentTypes,
}; };
model = new Component(); model = new Component({}, compOpts);
view = new ComponentView({ view = new ComponentView({
model model
}); });
@ -121,6 +125,13 @@ module.exports = {
expect(view.$el.html()).toEqual('<span data-highlightable="1"></span><div title="test" data-highlightable="1"></div>'); expect(view.$el.html()).toEqual('<span data-highlightable="1"></span><div title="test" data-highlightable="1"></div>');
}); });
it('removeClass removes classes from attributes', () => {
model.addClass('class1');
model.removeClass('class1');
const result = model.getAttributes();
expect(result.class).toEqual(undefined);
});
}); });
} }
}; };

2
test/specs/selector_manager/e2e/ClassManager.js

@ -98,7 +98,7 @@ module.exports = {
var model = components.add({}); var model = components.add({});
gjs.editor.set('selectedComponent', model); gjs.editor.set('selectedComponent', model);
tagEl.addNewTag('test'); tagEl.addNewTag('test');
gjs.editor.on("targetClassAdded", spy); gjs.editor.on("component:update:classes", spy);
tagEl.addNewTag('test'); tagEl.addNewTag('test');
expect(spy.called).toEqual(false); expect(spy.called).toEqual(false);
tagEl.addNewTag('test2'); tagEl.addNewTag('test2');

26
test/specs/selector_manager/view/ClassTagView.js

@ -69,14 +69,6 @@ module.exports = {
setTimeout(() => expect(fixtures.innerHTML).toNotExist(), 0) setTimeout(() => expect(fixtures.innerHTML).toNotExist(), 0)
}); });
it('On remove triggers event', () => {
var spy = sinon.spy();
sinon.stub(obj.target, 'get').returns(0);
obj.target.on("targetClassRemoved", spy);
obj.$el.find('#close').trigger('click');
expect(spy.called).toEqual(true);
});
it('Checkbox toggles status', () => { it('Checkbox toggles status', () => {
var spy = sinon.spy(); var spy = sinon.spy();
obj.model.on("change:active", spy); obj.model.on("change:active", spy);
@ -86,31 +78,19 @@ module.exports = {
expect(spy.called).toEqual(true); expect(spy.called).toEqual(true);
}); });
it('On toggle triggers event', () => {
var spy = sinon.spy();
sinon.stub(obj.target, 'get').returns(0);
obj.target.on("targetClassUpdated", spy);
obj.$el.find('#checkbox').trigger('click');
expect(spy.called).toEqual(true);
});
it('Label input is disabled', () => { it('Label input is disabled', () => {
var inputProp = obj.inputProp; expect(obj.getInputEl().contentEditable).toNotEqual(true);
var label = obj.$labelInput.get(0);
expect(obj.$labelInput.prop(inputProp)).toEqual(true);
}); });
it('On double click label input is enable', () => { it('On double click label input is enable', () => {
var inputProp = obj.inputProp;
obj.$el.find('#tag-label').trigger('dblclick'); obj.$el.find('#tag-label').trigger('dblclick');
expect(obj.$labelInput.prop(inputProp)).toEqual(false); expect(obj.getInputEl().contentEditable).toEqual(true);
}); });
it('On blur label input turns back disabled', () => { it('On blur label input turns back disabled', () => {
var inputProp = obj.inputProp;
obj.$el.find('#tag-label').trigger('dblclick'); obj.$el.find('#tag-label').trigger('dblclick');
obj.endEditTag(); obj.endEditTag();
expect(obj.$labelInput.prop(inputProp)).toEqual(true); expect(obj.getInputEl().contentEditable).toEqual(false);
}); });
}); });

2
test/specs/selector_manager/view/ClassTagsView.js

@ -132,7 +132,7 @@ module.exports = {
it("New tag correctly added", function() { it("New tag correctly added", function() {
coll.add({ label: 'test' }); coll.add({ label: 'test' });
expect(this.$tags.children().first().find('#tag-label input').val()).toEqual('test'); expect(this.$tags.children().first().find('[data-tag-name]').text()).toEqual('test');
}); });
it("States are hidden in case no tags", function() { it("States are hidden in case no tags", function() {

Loading…
Cancel
Save