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)
[![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>
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.
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
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
<br/>
<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-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-blocks-flexbox](https://github.com/artf/grapesjs-blocks-flexbox) - Add the flexbox block
### Presets
* [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/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/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/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/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",
"version": "0.12.50",
"version": "0.12.58",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

4
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.12.50",
"version": "0.12.58",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",
@ -80,7 +80,7 @@
"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",
"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: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);
}
// Register all its components (eg. for the Undo Manager)
em.initChildrenComp(model);
em.trigger('block:drag:stop', model);
}
},

9
src/canvas/index.js

@ -351,8 +351,13 @@ module.exports = () => {
this.dragging = 1;
let toListen = this.getScrollListeners();
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) {

10
src/canvas/view/FrameView.js

@ -16,17 +16,19 @@ module.exports = require('backbone').View.extend({
this.config = o.config || {};
this.ppfx = this.config.pStylePrefix || '';
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
*/
updateWidth(model) {
updateDim(model) {
const em = this.em;
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();
em.stopDefault({ preserveSelected: 1 });
this.$el.on(motionsEv, this.udpateOffset);

30
src/commands/index.js

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

3
src/commands/view/InsertCustom.js

@ -47,9 +47,6 @@ module.exports = _.extend({}, CreateComponent, {
if(!model)
return;
if(this.em)
this.em.editor.initChildrenComp(model);
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 Toolbar = require('dom_components/model/Toolbar');
const key = require('keymaster');
const Backbone = require('backbone');
const $ = require('backbone').$;
let showOffsets;
const $ = Backbone.$;
module.exports = {
init(o) {
bindAll(this, 'onHover', 'onOut', 'onClick', 'onKeyPress',
'copyComp', 'pasteComp', 'onFrameScroll');
bindAll(this, 'onHover', 'onOut', 'onClick', 'onKeyPress', 'onFrameScroll');
},
@ -20,7 +18,6 @@ module.exports = {
this.frameOff = this.canvasOff = this.adjScroll = null;
var config = this.config.em.get('Config');
this.startSelectComponent();
this.toggleClipboard(config.copyPaste);
var em = this.config.em;
showOffsets = 1;
@ -28,47 +25,6 @@ module.exports = {
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
* @private
@ -136,7 +92,8 @@ module.exports = {
*/
onHover(e) {
e.stopPropagation();
var trg = e.target;
let trg = e.target;
const model = $(trg).data('model');
// Adjust tools scroll top
if (!this.adjScroll) {
@ -145,7 +102,13 @@ module.exports = {
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.updateHighlighter(trg, pos);
this.showElementOffset(trg, pos);
@ -230,7 +193,17 @@ module.exports = {
onClick(e) {
e.stopPropagation();
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 toolbarEl = this.canvas.getToolbarEl();
var toolbarStyle = toolbarEl.style;
const origDisp = toolbarStyle.display;
toolbarStyle.display = 'block';
var pos = this.canvas.getTargetToElementDim(toolbarEl, el, {
elPos,
@ -471,7 +445,7 @@ module.exports = {
var leftPos = pos.left + pos.elementWidth - pos.targetWidth;
toolbarStyle.top = pos.top + unit;
toolbarStyle.left = leftPos + unit;
toolbarStyle.display = '';
toolbarStyle.display = origDisp;
},
/**
@ -580,7 +554,6 @@ module.exports = {
this.stopSelectComponent();
!opts.preserveSelected && em.setSelected(null);
this.clean();
this.toggleClipboard();
this.hideBadge();
this.hideFixedElementOffset();
this.canvas.getToolbarEl().style.display = 'none';

12
src/commands/view/SelectParent.js

@ -1,9 +1,15 @@
module.exports = {
run(editor) {
const comp = editor.getSelected();
const coll = comp && comp.collection;
coll && coll.parent && editor.select(coll.parent);
const sel = editor.getSelected();
let comp = sel && sel.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
* 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);
},
/**
* Do stuff after load
* @param {Editor} em
* @private
*/
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
* 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:
*
* ```js
@ -41,6 +37,7 @@ module.exports = () => {
* }
* ...
* @return {this}
* @private
*/
init(config) {
c = config || {};
@ -65,6 +62,10 @@ module.exports = () => {
* @return {Device} Added device
* @example
* 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) {
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: '',
// Height to set for the editor iframe
height: '',
// The width which will be used in media queries,
// If empty the width will be used
widthMedia: null,

58
src/dom_components/index.js

@ -34,6 +34,7 @@
*/
module.exports = () => {
var c = {};
let em;
const defaults = require('./config/config');
const Component = require('./model/Component');
const ComponentView = require('./view/ComponentView');
@ -57,6 +58,21 @@ module.exports = () => {
model: require('./model/ComponentTable'),
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',
model: require('./model/ComponentMap'),
@ -151,7 +167,7 @@ module.exports = () => {
*/
init(config) {
c = config || {};
const em = c.em;
em = c.em;
if (em) {
c.components = em.config.components || c.components;
@ -222,9 +238,47 @@ module.exports = () => {
* @private
*/
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
* 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 Components = require('./Components');
const Selector = require('selector_manager/model/Selector');
const Selectors = require('selector_manager/model/Selectors');
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.listenTo(this, 'change:script', this.scriptUpdated);
this.listenTo(this, 'change:traits', this.traitsUpdated);
this.listenTo(this, 'change:tagName', this.tagUpdated);
this.listenTo(this, 'change:attributes', this.attrUpdated);
this.loadTraits();
this.initClasses();
this.initComponents();
this.initToolbar();
this.set('status', '');
this.listenTo(this.get('classes'), 'add remove change',
() => this.emitUpdate('classes'));
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
@ -236,7 +296,7 @@ module.exports = Backbone.Model.extend(Styleable).extend({
*/
getAttributes() {
const classes = [];
const attributes = this.get('attributes') || {};
const attributes = { ...this.get('attributes') };
// Add classes
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() {
const classes = this.normalizeClasses(this.get('classes') || []);
this.set('classes', new Selectors(classes));
@ -488,7 +576,8 @@ module.exports = Backbone.Model.extend(Styleable).extend({
clone(reset) {
const em = this.em;
const style = this.getStyle();
const attr = clone(this.attributes);
const attr = { ...this.attributes };
attr.attributes = { ...attr.attributes };
delete attr.attributes.id;
attr.components = [];
attr.classes = [];
@ -648,7 +737,14 @@ module.exports = Backbone.Model.extend(Styleable).extend({
})
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({
defaults: _.extend({}, Component.prototype.defaults, {
defaults: { ...Component.prototype.defaults,
type: 'table',
tagName: 'table',
droppable: ['tr', 'tbody', 'thead', 'tfoot'],
columns: 3,
rows: 2,
/*
traits: [{
label: 'Columns',
name: 'columns',
changeProp: 1,
},{
label: 'Rows',
name: 'rows',
changeProp: 1,
}]
*/
}),
droppable: ['tbody', 'thead', 'tfoot'],
},
initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments);
var components = this.get('components');
var rows = this.get('rows');
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);
const components = this.get('components');
!components.length && components.add({ type: 'tbody' });
},
},{
}, {
/**
* 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) {
var result = '';
if(el.tagName == 'TABLE'){
result = {type: 'table'};
let result = '';
if (el.tagName == 'TABLE') {
result = { type: 'table' };
}
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({
defaults: _.extend({}, Component.prototype.defaults, {
defaults: { ...Component.prototype.defaults,
type: 'cell',
tagName: 'td',
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) {
var result = '';
var tag = el.tagName;
if(tag == 'TD' || tag == 'TH'){
let result = '';
const tag = el.tagName;
if (tag == 'TD' || tag == 'TH') {
result = {
type: 'cell',
tagName: tag.toLowerCase()
};
}
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({
defaults: _.extend({}, Component.prototype.defaults, {
defaults: { ...Component.prototype.defaults,
type: 'row',
tagName: 'tr',
draggable: ['table', 'tbody', 'thead'],
draggable: ['thead', 'tbody', 'tfoot'],
droppable: ['th', 'td']
}),
},
initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments);
// Clean the row from non cell components
var cells = [];
var components = this.get('components');
components.each(model => {
if(model.get('type') == 'cell'){
cells.push(model);
}
});
const cells = [];
const components = this.get('components');
components.each(model => model.is('cell') && cells.push(model));
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) {
var result = '';
if(el.tagName == 'TR'){
result = {type: 'row'};
let result = '';
if (el.tagName == 'TR') {
result = { type: 'row' };
}
return result;
},

4
src/dom_components/view/ComponentLinkView.js

@ -3,10 +3,6 @@ var ComponentView = require('./ComponentTextView');
module.exports = ComponentView.extend({
events: {
'dblclick': 'enableEditing',
},
render(...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 comps = model.get('components');
comps.length && comps.reset();
model.set('content', '');
// If there is a custom RTE the content is just baked staticly
// inside 'content'
if (rte.customRte) {
// Avoid double content by removing its children components
// and force to trigger change
model.set('content', '')
.set('content', content);
model.set('content', content);
} else {
const clean = model => {
model.set({
@ -82,7 +82,7 @@ module.exports = ComponentView.extend({
}
// Avoid re-render on reset with silent option
model.set('content', '').trigger('change:content', model);
model.trigger('change:content', model);
comps.add(content);
comps.each(model => clean(model));
comps.trigger('resetNavigator');

4
src/dom_components/view/ComponentVideoView.js

@ -1,5 +1,6 @@
var Backbone = require('backbone');
var ComponentView = require('./ComponentImageView');
var OComponentView = require('./ComponentView');
module.exports = ComponentView.extend({
@ -8,7 +9,8 @@ module.exports = ComponentView.extend({
events: {},
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:provider', this.updateProvider);
},

11
src/dom_components/view/ComponentView.js

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

2
src/domain_abstract/ui/Input.js

@ -25,7 +25,7 @@ module.exports = Backbone.View.extend({
const ppfx = opts.ppfx || '';
this.opts = opts;
this.ppfx = ppfx;
this.target = opts.target || {};
this.em = opts.target || {};
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 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 => {
let cl = color.getAlpha() == 1 ? color.toHexString() : color.toRgbString();
return cl.replace(/ /g, '');
@ -76,6 +77,10 @@ module.exports = Input.extend({
chooseText: 'Ok',
cancelText: '⨯',
palette: [],
// config expanded here so that the functions below are not overridden
...colorPickerConfig,
move(color) {
const cl = getColor(color);
cpStyle.backgroundColor = cl;

9
src/editor/config/config.js

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

4
src/editor/index.js

@ -51,6 +51,8 @@
* * `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview'))
* ## General
* * `canvasScroll` - Triggered when the canvas is scrolle
* * `undo` - Undo executed
* * `redo` - Redo executed
* * `load` - When the editor is loaded
*
* @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.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.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.autorender=true] If true renders editor on init
* @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
// be empty during tests
em.on('loaded', () => {
this.UndoManager.clear();
em.get('modules').forEach(module => {
module.postRender && module.postRender(editorView);
});

194
src/editor/model/Editor.js

@ -3,6 +3,7 @@ import { isUndefined, defaults } from 'underscore';
const deps = [
require('utils'),
require('keymaps'),
require('undo_manager'),
require('storage_manager'),
require('device_manager'),
require('parser'),
@ -22,8 +23,6 @@ const deps = [
];
const Backbone = require('backbone');
const UndoManager = require('backbone-undo');
const key = require('keymaster');
let timedInterval;
require('utils/extender')({
@ -57,12 +56,7 @@ module.exports = Backbone.Model.extend({
this.config.components = c.el.innerHTML;
// Load modules
deps.forEach(function(name){
this.loadModule(name);
}, this);
this.initUndoManager();
deps.forEach(name => this.loadModule(name));
this.on('change:selectedComponent', this.componentSelected, this);
this.on('change:changesCount', this.updateChanges, this);
},
@ -94,16 +88,10 @@ module.exports = Backbone.Model.extend({
module.onLoad();
});
// Stuff to do post load (eg. init undo manager for loaded components)
// Stuff to do post load
const postLoad = () => {
// I've initialized undo manager in initialize() because otherwise the
// editor will unable to fetch the instance via 'editor.UndoManager' but
// 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)
);
const modules = this.get('modules');
modules.forEach(module => module.postLoad && module.postLoad(this));
clb && clb();
};
@ -151,22 +139,20 @@ module.exports = Backbone.Model.extend({
// Check if module is storable
var sm = this.get('StorageManager');
if(Mod.storageKey && Mod.store && Mod.load && sm){
if (Mod.storageKey && Mod.store && Mod.load && sm) {
cfg.stm = sm;
var storables = this.get('storables');
storables.push(Mod);
this.set('storables', storables);
}
cfg.em = this;
Mod.init({ ...cfg });
// Bind the module to the editor model if public
if(!Mod.private)
this.set(Mod.name, Mod);
if(Mod.onLoad)
this.get('toLoad').push(Mod);
!Mod.private && this.set(Mod.name, Mod);
Mod.onLoad && this.get('toLoad').push(Mod);
this.get('modules').push(Mod);
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
* if requested and if the changesCount is exceeded
@ -232,82 +195,6 @@ module.exports = Backbone.Model.extend({
}, 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
@ -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

5
src/grapesjs/config/config.js

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

4
src/grapesjs/index.js

@ -1,5 +1,8 @@
import $ from 'cash-dom';
import { defaults } from 'underscore';
import polyfills from 'utils/polyfills';
polyfills();
module.exports = (() => {
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.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.copyPaste=true] Enable/Disable the possibility to copy(ctrl+c) & paste(ctrl+v) components
* @param {Boolean} [config.undoManager=true] Enable/Disable undo manager
* @param {Array} [config.plugins=[]] Array of plugins to execute on start
* @return {grapesjs.Editor} GrapesJS editor instance

38
src/keymaps/index.js

@ -12,9 +12,29 @@ import { isString } from 'underscore';
const keymaster = require('keymaster');
module.exports = () => {
let em;
let config;
const configDef = {};
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 {
@ -39,11 +59,23 @@ module.exports = () => {
* @private
*/
init(opts = {}) {
config = { ...opts, ...configDef };
this.em = config.em;
config = { ...configDef, ...opts };
em = config.em;
this.em = em;
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
* @param {string} id Keymap id

82
src/navigator/view/ItemView.js

@ -1,8 +1,8 @@
var Backbone = require('backbone');
var ComponentView = require('dom_components/view/ComponentView');
var ItemsView;
import { isUndefined } from 'underscore';
const ComponentView = require('dom_components/view/ComponentView');
let ItemsView;
module.exports = Backbone.View.extend({
module.exports = require('backbone').View.extend({
events: {
'mousedown [data-toggle-move]': 'startSort',
@ -13,28 +13,36 @@ module.exports = Backbone.View.extend({
'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>
<i class="fa fa-arrows"></i>
</div>
template(model) {
const pfx = this.pfx;
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 = {}) {
this.opt = o;
@ -291,29 +299,14 @@ module.exports = Backbone.View.extend({
},
render() {
let model = this.model;
const model = this.model;
var pfx = this.pfx;
var vis = this.isVisible();
var count = this.countChildren(model);
const el = this.$el;
const level = this.level + 1;
el.html(this.template(model));
el.html(this.template({
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') {
if (isUndefined(ItemsView)) {
ItemsView = require('./ItemsView');
}
@ -331,9 +324,8 @@ module.exports = Backbone.View.extend({
el.children(`#${pfx}move`).remove();
}
if(!vis)
this.className += ' ' + pfx + 'hide';
el.attr('class', _.result(this, 'className'));
!vis && (this.className += ` ${pfx}hide`);
el.attr('class', this.className);
this.updateOpening();
this.updateStatus();
this.updateVisibility();

8
src/panels/view/ButtonView.js

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

2
src/rich_text_editor/index.js

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

15
src/selector_manager/model/Selectors.js

@ -1,21 +1,22 @@
var Backbone = require('backbone');
var Selector = require('./Selector');
import { filter } from 'underscore';
const Selector = require('./Selector');
module.exports = Backbone.Collection.extend({
module.exports = require('backbone').Collection.extend({
model: Selector,
getStyleable() {
return _.filter(this.models, item =>
return filter(this.models, item =>
item.get('active') && !item.get('private'));
},
getValid() {
return _.filter(this.models, item => !item.get('private'));
return filter(this.models, item => !item.get('private'));
},
getFullString() {
getFullString(collection) {
const result = [];
this.each(selector => result.push(selector.getFullName()));
const coll = collection || this;
coll.forEach(selector => result.push(selector.getFullName()));
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 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) {
this.config = o.config || {};
this.coll = o.coll || null;
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.inputProp = 'readonly';
this.target = this.config.em;
this.closeId = this.pfx + 'close';
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();
this.listenTo(this.model, 'change:active', this.updateStatus);
},
/**
* 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
* @private
*/
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
* old one will be restored otherwise will be changed
* @private
*/
endEditTag() {
var value = this.$labelInput.val();
var next = Selector.escapeName(value);
if(this.target){
var clsm = this.target.get('SelectorManager');
if(clsm){
if(clsm.get(next))
this.$labelInput.val(this.model.get('label'));
else
this.model.set({ name: next, label: value});
const model = this.model;
const inputEl = this.getInputEl();
const label = inputEl.textContent;
const name = Selector.escapeName(label);
const em = this.target;
const sm = em && em.get('SelectorManager');
inputEl[inputProp] = false;
if (sm) {
if (sm.get(name)) {
inputEl.innerText = model.get('label');
} else {
model.set({ name, label });
}
}
this.$labelInput.prop(this.inputProp, true);
},
/**
* Update status of the tag
* @private
*/
changeStatus() {
this.model.set('active', !this.model.get('active'));
this.target.trigger('targetClassUpdated');
},
/**
* Remove tag from the selected component
* @param {Object} e
@ -83,9 +106,9 @@ module.exports = Backbone.View.extend({
sel && sel.get & sel.get('classes').remove(model);
coll && coll.remove(model);
setTimeout(() => this.remove(), 0);
em && em.trigger('targetClassRemoved');
},
/**
* Update status of the checkbox
* @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() {
const pfx = this.pfx;
const ppfx = this.ppfx;
this.$el.html( this.template({
label: this.model.get('label'),
pfx,
ppfx,
inputProp: this.inputProp,
}));
this.$el.html(this.template());
this.$el.attr('class', `${pfx}tag ${ppfx}three-bg`);
this.updateStatus();
this.updateInputLabel();
return this;
},

12
src/selector_manager/view/ClassTagsView.js

@ -52,7 +52,7 @@ module.exports = Backbone.View.extend({
this.em = this.target;
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, 'reset', this.renderClasses);
@ -174,7 +174,8 @@ module.exports = Backbone.View.extend({
}
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 += state ? `:${state}` : '';
const el = this.el.querySelector('#' + this.pfx + 'sel');
@ -190,8 +191,6 @@ module.exports = Backbone.View.extend({
stateChanged(e) {
if(this.compTarget){
this.compTarget.set('state', this.$states.val());
if(this.target)
this.target.trigger('targetStateUpdated');
this.updateSelector();
}
},
@ -219,11 +218,6 @@ module.exports = Backbone.View.extend({
compCls.add(model);
var lenA = compCls.length;
this.collection.add(model);
if (lenA > lenB) {
target.trigger('targetClassAdded');
}
this.updateStateVis();
}
}

2
src/storage_manager/index.js

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

2
src/style_manager/config/config.js

@ -21,7 +21,7 @@ module.exports = {
showComputed: true,
// Adds the possibility to clear property value from the target style
clearProperties: false,
clearProperties: 0,
// Properties not to take in account for computed styles
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
* be used to custom components and classes.
* 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
* @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
* @param {string} sectorId Sector id
@ -249,19 +261,25 @@ module.exports = () => {
* @return {Model}
*/
getModelToStyle(model) {
const classes = model.get('classes');
const em = c.em;
const classes = model.get('classes');
const id = model.getId();
if (em && classes && classes.length) {
const conf = em.get('Config');
const state = !conf.devicePreviewMode ? model.get('state') : '';
const deviceW = em.getCurrentMedia();
if (em) {
const config = em.getConfig();
const cssC = em.get('CssComposer');
const state = !config.devicePreviewMode ? model.get('state') : '';
const valid = classes.getStyleable();
const CssRule = cssC.get(valid, state, deviceW);
if(CssRule && valid.length) {
return CssRule;
const hasClasses = valid.length;
const opts = { state };
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 { camelCase } from 'utils/mixins';
const clearProp = 'data-clear-style';
module.exports = Backbone.View.extend({
template(model) {
@ -23,7 +25,7 @@ module.exports = Backbone.View.extend({
<span class="${pfx}icon ${icon}" title="${info}">
${model.get('name')}
</span>
<b class="${pfx}clear">&Cross;</b>
<b class="${pfx}clear" ${clearProp}>&Cross;</b>
`;
},
@ -36,7 +38,8 @@ module.exports = Backbone.View.extend({
},
events: {
'change': 'inputValueChanged'
'change': 'inputValueChanged',
[`click [${clearProp}]`]: 'clear',
},
initialize(o = {}) {
@ -70,8 +73,6 @@ module.exports = Backbone.View.extend({
this.listenTo(model, 'targetUpdated', this.targetUpdated);
this.listenTo(model, 'change:visible', this.updateVisibility);
this.listenTo(model, 'change:status', this.updateStatus);
this.events[`click .${pfx}clear`] = 'clear';
this.delegateEvents();
const init = this.init && this.init.bind(this);
init && init();
@ -122,7 +123,7 @@ module.exports = Backbone.View.extend({
* @return {HTMLElement}
*/
getClearEl() {
return this.el.querySelector(`.${this.pfx}clear`);
return this.el.querySelector(`[${clearProp}]`);
},
/**
@ -159,14 +160,12 @@ module.exports = Backbone.View.extend({
this.elementUpdated();
},
/**
* Fired when the element of the property is updated
*/
elementUpdated() {
this.model.set('status', 'updated');
const parent = this.model.parent;
const parentView = parent && parent.view;
parentView && parentView.elementUpdated();
this.setStatus('updated');
},

1
src/style_manager/view/SectorView.js

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

33
src/styles/scss/_gjs_canvas.scss

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

9
src/styles/scss/main.scss

@ -573,13 +573,8 @@ $prefix: $app-prefix;
}
##{$clm-prefix}tag-label {
cursor: text;
@extend .no-select;
input {
width: auto;
padding: 0 4px;
}
padding: 0 3px;
cursor: text;
}
}
/********* 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: {
type: 'text', // text, number, range, select
@ -16,13 +16,50 @@ module.exports = Backbone.Model.extend({
options: [],
},
initialize() {
if (this.get('target')) {
this.target = this.get('target');
const target = this.get('target');
const name = this.get('name');
const changeProp = this.get('changeProp');
if (target) {
this.target = 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
* @return {string}

1
src/trait_manager/view/TraitColorView.js

@ -11,6 +11,7 @@ module.exports = TraitView.extend({
if (!this.$input) {
var value = this.getModelValue();
var inputNumber = new InputColor({
target: this.config.em,
contClass: this.ppfx + 'field-color',
model: this.model,
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.$;
module.exports = Backbone.View.extend({
@ -6,21 +9,28 @@ module.exports = Backbone.View.extend({
'change': 'onChange'
},
attributes() {
return this.model.get('attributes');
},
initialize(o) {
var md = this.model;
const model = this.model;
const name = model.get('name');
const target = model.target;
this.config = o.config || {};
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.target = md.target;
this.target = target;
this.className = this.pfx + 'trait';
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';
md.off('change:value', this.onValueChange);
this.listenTo(md, 'change:value', this.onValueChange);
model.off('change:value', this.onValueChange);
this.listenTo(model, 'change:value', this.onValueChange);
this.tmpl = '<div class="' + this.fieldClass +'"><div class="' + this.inputhClass +'"></div></div>';
},
/**
* Fires when the input is changed
* @private
@ -33,22 +43,26 @@ module.exports = Backbone.View.extend({
return this.model.get('value');
},
setInputValue(value) {
this.getInputEl().value = value;
},
/**
* On change callback
* @private
*/
onValueChange() {
var m = this.model;
var trg = this.target;
var name = m.get('name');
var value = this.getValueForTarget();
// Chabge property if requested otherwise attributes
if(m.get('changeProp')){
trg.set(name, value);
}else{
var attrs = _.clone(trg.get('attributes'));
attrs[name] = value;
trg.set('attributes', attrs);
onValueChange(model, value, opts = {}) {
const mod = this.model;
const trg = this.target;
const name = mod.get('name');
if (opts.fromTarget) {
this.setInputValue(mod.get('value'));
} else {
const value = this.getValueForTarget();
mod.setTargetValue(value);
}
},

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 sortCls = pfx + 'grabbing';
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();
body.className += ' ' + sortCls;
if(em) {
emBody.className += ' ' + sortCls;
}
//body.className += ' ' + sortCls;
//if (em) emBody.className += ' ' + sortCls;
} else {
em && em.get('Canvas').stopAutoscroll();
body.className = body.className.replace(sortCls, '').trim();
if(em) {
emBody.className = emBody.className.replace(sortCls, '').trim();
}
//body.className = body.className.replace(sortCls, '').trim();
//if(em) emBody.className = emBody.className.replace(sortCls, '').trim();
}
},
@ -925,7 +924,7 @@ module.exports = Backbone.View.extend({
var dragHelper = this.dragHelper;
if(dragHelper) {
dragHelper.remove();
dragHelper.parentNode.removeChild(dragHelper);
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);
});
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', () => {
obj.setAttributes({
id: 'test',

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

@ -1,6 +1,7 @@
const ComponentView = require('dom_components/view/ComponentView');
const Component = require('dom_components/model/Component');
const DomComponents = require('dom_components');
const Editor = require('editor/model/Editor');
module.exports = {
run() {
@ -13,13 +14,16 @@ module.exports = {
var hClass = 'hc-state';
var dcomp;
var compOpts;
let em;
beforeEach(() => {
em = new Editor({});
dcomp = new DomComponents();
compOpts = {
em,
componentTypes: dcomp.componentTypes,
};
model = new Component();
model = new Component({}, compOpts);
view = new ComponentView({
model
});
@ -121,6 +125,13 @@ module.exports = {
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({});
gjs.editor.set('selectedComponent', model);
tagEl.addNewTag('test');
gjs.editor.on("targetClassAdded", spy);
gjs.editor.on("component:update:classes", spy);
tagEl.addNewTag('test');
expect(spy.called).toEqual(false);
tagEl.addNewTag('test2');

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

@ -69,14 +69,6 @@ module.exports = {
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', () => {
var spy = sinon.spy();
obj.model.on("change:active", spy);
@ -86,31 +78,19 @@ module.exports = {
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', () => {
var inputProp = obj.inputProp;
var label = obj.$labelInput.get(0);
expect(obj.$labelInput.prop(inputProp)).toEqual(true);
expect(obj.getInputEl().contentEditable).toNotEqual(true);
});
it('On double click label input is enable', () => {
var inputProp = obj.inputProp;
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', () => {
var inputProp = obj.inputProp;
obj.$el.find('#tag-label').trigger('dblclick');
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() {
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() {

Loading…
Cancel
Save