Browse Source

Merge branch 'dev' of https://github.com/artf/grapesjs into dev

pull/1028/head
Michal Tomaszewski 8 years ago
parent
commit
365fbcb09c
  1. 13
      .github/no-response.yml
  2. 2
      .gitignore
  3. 5
      README.md
  4. 1259
      dist/grapes.js
  5. 6
      dist/grapes.min.js
  6. 2
      package-lock.json
  7. 2
      package.json
  8. 10
      src/code_manager/model/CssGenerator.js
  9. 2
      src/commands/view/CommandAbstract.js
  10. 1
      src/commands/view/Resize.js
  11. 36
      src/commands/view/SelectComponent.js
  12. 16
      src/css_composer/index.js
  13. 49
      src/css_composer/view/CssRulesView.js
  14. 32
      src/dom_components/model/Component.js
  15. 4
      src/domain_abstract/model/Styleable.js
  16. 14
      src/editor/config/config.js
  17. 6
      src/editor/index.js
  18. 17
      src/editor/model/Editor.js
  19. 1
      src/panels/model/Button.js
  20. 182
      src/panels/view/ButtonView.js
  21. 8
      src/parser/model/ParserHtml.js
  22. 9
      src/selector_manager/index.js
  23. 47
      src/storage_manager/index.js
  24. 13
      src/storage_manager/model/RemoteStorage.js
  25. 2
      test/specs/commands/index.js
  26. 83
      test/specs/commands/view/CommandAbstract.js
  27. 31
      test/specs/css_composer/view/CssRulesView.js
  28. 15
      test/specs/dom_components/model/Component.js
  29. 43
      test/specs/grapesjs/index.js
  30. 33
      test/specs/parser/model/ParserCss.js
  31. 44
      test/specs/parser/model/ParserHtml.js

13
.github/no-response.yml

@ -0,0 +1,13 @@
# Configuration for probot-no-response - https://github.com/probot/no-response
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 10
# Label requiring a response
responseRequiredLabel: more-information-needed
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. With only the
information that is currently in the issue, we don't have enough information
to take action. Please reach out if you have or find the answers we need so
that we can investigate further.

2
.gitignore

@ -4,6 +4,8 @@
.project
.idea
npm-debug.log*
yarn-error.log
yarn.lock
style/.sass-cache/
img/

5
README.md

@ -4,6 +4,8 @@
[![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)
[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=QksxaStYaGI3eE5VMDlPTEh0Z3hYOXEwRWNMc1ZYT0lNbEJxMWdOZWFDZz0tLWlqcFVWb05PMmlQMmU3emFIZkFNWVE9PQ==--e89345be5e303d515276e3accd6f1316dfa857ab)](https://www.browserstack.com/automate/public-build/QksxaStYaGI3eE5VMDlPTEh0Z3hYOXEwRWNMc1ZYT0lNbEJxMWdOZWFDZz0tLWlqcFVWb05PMmlQMmU3emFIZkFNWVE9PQ==--e89345be5e303d515276e3accd6f1316dfa857ab)
<p align="center"><img src="http://grapesjs.com/img/grapesjs-front-page-m.jpg" alt="GrapesJS" width="500" align="center"/></p>
@ -229,6 +231,9 @@ If you like the project support it with a donation of your choice or become a ba
<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>
<a href="https://opencollective.com/grapesjs/backers/10/website"><img src="https://opencollective.com/grapesjs/backers/10/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/11/website"><img src="https://opencollective.com/grapesjs/backers/11/avatar"></a>
<a href="https://opencollective.com/grapesjs/backers/12/website"><img src="https://opencollective.com/grapesjs/backers/12/avatar"></a>

1259
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.14.8",
"version": "0.14.9",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

2
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.14.8",
"version": "0.14.9",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",

10
src/code_manager/model/CssGenerator.js

@ -59,7 +59,7 @@ module.exports = require('backbone').Model.extend({
return;
}
code += this.buildFromRule(rule, dump);
code += this.buildFromRule(rule, dump, opts);
});
// Get at-rules
@ -84,7 +84,7 @@ module.exports = require('backbone').Model.extend({
* @param {Model} rule
* @return {string} CSS string
*/
buildFromRule(rule, dump) {
buildFromRule(rule, dump, opts = {}) {
let result = '';
const selectorStrNoAdd = rule.selectorsToString({ skipAdd: 1 });
const selectorsAdd = rule.get('selectorsAdd');
@ -94,7 +94,11 @@ module.exports = require('backbone').Model.extend({
// This will not render a rule if there is no its component
rule.get('selectors').each(selector => {
const name = selector.getFullName();
if (this.compCls.indexOf(name) >= 0 || this.ids.indexOf(name) >= 0) {
if (
this.compCls.indexOf(name) >= 0 ||
this.ids.indexOf(name) >= 0 ||
opts.dumpUnusedStyles
) {
found = 1;
}
});

2
src/commands/view/CommandAbstract.js

@ -107,6 +107,7 @@ module.exports = Backbone.View.extend({
const result = this.run(editor, editor, options);
editor.trigger(`run:${id}`, result, options);
return result;
},
/**
@ -120,6 +121,7 @@ module.exports = Backbone.View.extend({
editor.trigger(`stop:${id}:before`, options);
const result = this.stop(editor, editor, options);
editor.trigger(`stop:${id}`, result, options);
return result;
},
/**

1
src/commands/view/Resize.js

@ -19,6 +19,7 @@ module.exports = {
}
canvasResizer.setOptions(options);
canvasResizer.blur();
canvasResizer.focus(el);
return canvasResizer;
},

36
src/commands/view/SelectComponent.js

@ -1,4 +1,4 @@
import { bindAll } from 'underscore';
import { bindAll, isElement } from 'underscore';
import { on, off, getUnitFromValue } from 'utils/mixins';
const ToolbarView = require('dom_components/view/ToolbarView');
@ -186,6 +186,7 @@ module.exports = {
if (model) {
if (model.get('selectable')) {
editor.select(model);
this.initResize(model);
} else {
let parent = model.parent();
while (parent && !parent.get('selectable')) parent = parent.parent();
@ -262,7 +263,8 @@ module.exports = {
* @private
* */
onSelect() {
const editor = this.editor;
// Get the selected model directly from the Editor as the event might
// be triggered manually without the model
const model = this.em.getSelected();
this.updateToolbar(model);
@ -273,26 +275,27 @@ module.exports = {
this.hideHighlighter();
this.initResize(el);
} else {
editor.stopCommand('resize');
this.editor.stopCommand('resize');
}
},
/**
* Init resizer on the element if possible
* @param {HTMLElement} el
* @param {HTMLElement|Component} elem
* @private
*/
initResize(el) {
var em = this.em;
var editor = em ? em.get('Editor') : '';
var config = em ? em.get('Config') : '';
var pfx = config.stylePrefix || '';
var attrName = `data-${pfx}handler`;
var resizeClass = `${pfx}resizing`;
var model = em.get('selectedComponent');
var resizable = model.get('resizable');
var options = {};
var modelToStyle;
initResize(elem) {
const em = this.em;
const editor = em ? em.get('Editor') : '';
const config = em ? em.get('Config') : '';
const pfx = config.stylePrefix || '';
const attrName = `data-${pfx}handler`;
const resizeClass = `${pfx}resizing`;
const model = !isElement(elem) ? elem : em.getSelected();
const resizable = model.get('resizable');
const el = isElement(elem) ? elem : model.getEl();
let options = {};
let modelToStyle;
var toggleBodyClass = (method, e, opts) => {
const docs = opts.docs;
@ -376,11 +379,12 @@ module.exports = {
if (typeof resizable == 'object') {
options = { ...options, ...resizable };
}
editor.runCommand('resize', { el, options });
// On undo/redo the resizer rect is not updating, need somehow to call
// this.updateRect on undo/redo action
} else {
editor.stopCommand('resize');
}
},

16
src/css_composer/index.js

@ -94,22 +94,25 @@ module.exports = () => {
postLoad(em) {
const ev = 'add remove';
const rules = this.getAll();
const um = em.get('UndoManager');
um && um.add(rules);
em.stopListening(rules, ev, this.handleChange);
em.listenTo(rules, ev, this.handleChange);
rules.each(rule => this.handleChange(rule));
rules.each(rule => this.handleChange(rule, { avoidStore: 1 }));
},
/**
* Handle rule changes
* @private
*/
handleChange(model) {
handleChange(model, opts = {}) {
const ev = 'change:style';
const um = em.get('UndoManager');
um && um.add(model);
const handleUpdates = em.handleUpdates.bind(em);
em.stopListening(model, ev, handleUpdates);
em.listenTo(model, ev, handleUpdates);
!opts.avoidStore && handleUpdates('', '', opts);
},
/**
@ -182,8 +185,13 @@ module.exports = () => {
var w = width || '';
var opt = { ...opts };
var rule = this.get(selectors, s, w, opt);
if (rule) return rule;
else {
// do not create rules that were found before
// unless this is an at-rule, for which multiple declarations
// make sense (e.g. multiple `@font-type`s)
if (rule && rule.config && !rule.config.atRuleType) {
return rule;
} else {
opt.state = s;
opt.mediaText = w;
opt.selectors = '';

49
src/css_composer/view/CssRulesView.js

@ -2,6 +2,10 @@ const CssRuleView = require('./CssRuleView');
const CssGroupRuleView = require('./CssGroupRuleView');
const $ = Backbone.$;
// % is not a valid character for classes
const getBlockId = (pfx, widthMedia) =>
`${pfx}${widthMedia ? `-${widthMedia.replace('%', 'pc')}` : ''}`;
module.exports = require('backbone').View.extend({
initialize(o) {
const config = o.config || {};
@ -32,6 +36,11 @@ module.exports = require('backbone').View.extend({
* @private
* */
addToCollection(model, fragmentEl) {
// If the render is not yet started
if (!this.renderStarted) {
return;
}
var fragment = fragmentEl || null;
var viewObject = CssRuleView;
var config = this.config;
@ -62,16 +71,32 @@ module.exports = require('backbone').View.extend({
rendered = view.render().el;
}
const mediaWidth = this.getMediaWidth(model.get('mediaText'));
const styleBlockId = `#${this.pfx}rules-${mediaWidth}`;
const clsName = this.className;
const mediaText = model.get('mediaText');
const defaultBlockId = getBlockId(clsName);
let blockId = defaultBlockId;
// If the rule contains a media query it might have a different container
// for it (eg. rules created with Device Manager)
if (mediaText) {
blockId = getBlockId(clsName, this.getMediaWidth(mediaText));
}
if (rendered) {
if (fragment) {
fragment.querySelector(styleBlockId).appendChild(rendered);
} else {
let $stylesContainer = this.$el.find(styleBlockId);
$stylesContainer.append(rendered);
const container = fragment || this.el;
let contRules;
// Try to find a specific container for the rule (if it
// containes a media query), otherwise get the default one
try {
contRules = container.querySelector(`#${blockId}`);
} catch (e) {}
if (!contRules) {
contRules = container.querySelector(`#${defaultBlockId}`);
}
contRules.appendChild(rendered);
}
return rendered;
@ -87,13 +112,14 @@ module.exports = require('backbone').View.extend({
},
render() {
this.renderStarted = 1;
this.atRules = {};
const $el = this.$el;
const frag = document.createDocumentFragment();
const className = this.className;
$el.empty();
// Create devices related DOM structure
const pfx = this.pfx;
this.em
.get('DeviceManager')
.getAll()
@ -104,13 +130,14 @@ module.exports = require('backbone').View.extend({
((left && left.replace('px', '')) || Number.MAX_VALUE)
)
.forEach(widthMedia => {
const blockId = pfx + 'rules-' + widthMedia;
$(`<div id="${blockId}"></div>`).appendTo(frag);
$(`<div id="${getBlockId(className, widthMedia)}"></div>`).appendTo(
frag
);
});
this.collection.each(model => this.addToCollection(model, frag));
$el.append(frag);
$el.attr('class', this.className);
$el.attr('class', className);
return this;
}
});

32
src/dom_components/model/Component.js

@ -6,6 +6,7 @@ import {
has,
clone,
isString,
forEach,
keys
} from 'underscore';
import { shallowDiff, hasDnd } from 'utils/mixins';
@ -513,9 +514,10 @@ const Component = Backbone.Model.extend(Styleable).extend(
traits.each(trait => {
found = 1;
if (!trait.get('changeProp')) {
const name = trait.get('name');
const value = trait.getInitValue();
if (value) {
attrs[trait.get('name')] = value;
if (name && value) {
attrs[name] = value;
}
}
});
@ -722,6 +724,32 @@ const Component = Backbone.Model.extend(Styleable).extend(
delete obj.attributes.class;
delete obj.toolbar;
if (this.em.getConfig('avoidDefaults')) {
const defaults = this.defaults;
forEach(defaults, (value, key) => {
if (key !== 'type' && obj[key] === value) {
delete obj[key];
}
});
if (isEmpty(obj.type)) {
delete obj.type;
}
forEach(['attributes', 'style'], prop => {
if (isEmpty(defaults[prop]) && isEmpty(obj[prop])) {
delete obj[prop];
}
});
forEach(['classes', 'components'], prop => {
if (isEmpty(defaults[prop]) && !obj[prop].length) {
delete obj[prop];
}
});
}
return obj;
},

4
src/domain_abstract/model/Styleable.js

@ -43,8 +43,8 @@ export default {
const em = this.em;
this.trigger(`change:style:${pr}`);
if (em) {
em.trigger(`styleable:change`);
em.trigger(`styleable:change:${pr}`);
em.trigger(`styleable:change`, this, pr);
em.trigger(`styleable:change:${pr}`, this, pr);
}
});

14
src/editor/config/config.js

@ -50,7 +50,7 @@ module.exports = {
overflow: auto;
overflow-x: hidden;
}
* ::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1)
}
@ -96,6 +96,9 @@ module.exports = {
// Ending tag for variable inside scripts in Components
tagVarEnd: ' ]}',
// When false, removes empty text nodes when parsed, unless they contain a space
keepEmptyTextNodes: 0,
// Return JS of components inside HTML from 'editor.getHtml()'
jsInHtml: true,
@ -114,6 +117,11 @@ module.exports = {
// When `avoidInlineStyle` is true all styles are inserted inside the css rule
avoidInlineStyle: 0,
// Avoid default properties from storable JSON data, like `components` and `styles`.
// With this option enabled your data will be smaller (usefull if need to
// save some storage space)
avoidDefaults: 0,
// (experimental)
// The structure of components is always on the screen but it's not the same
// for style rules. When you delete a component you might leave a lot of styles
@ -271,6 +279,8 @@ module.exports = {
traitManager: {},
// Texts
textViewCode: 'Code',
textViewCode: 'Code'
// Dump unused styles within the editor
dumpUnusedStyles: 0
};

6
src/editor/index.js

@ -17,6 +17,7 @@
* * `component:styleUpdate` - Triggered when the style of the component is updated, the model is passed as an argument to the callback
* * `component:styleUpdate:{propertyName}` - Listen for a specific style property change, the model is passed as an argument to the callback
* * `component:selected` - New component selected, the selected model is passed as an argument to the callback
* * `component:deselected` - Component deselected, the deselected model is passed as an argument to the callback
* ## Blocks
* * `block:add` - New block added
* * `block:remove` - Block removed
@ -39,9 +40,13 @@
* * `styleManager:change:{propertyName}` - As above but for a specific style property
* ## Storages
* * `storage:start` - Before the storage request is started
* * `storage:start:store` - Before the store request. The object to store is passed as an argumnet (which you can edit)
* * `storage:start:load` - Before the load request. Items to load are passed as an argumnet (which you can edit)
* * `storage:load` - Triggered when something was loaded from the storage, loaded object passed as an argumnet
* * `storage:store` - Triggered when something is stored to the storage, stored object passed as an argumnet
* * `storage:end` - After the storage request is ended
* * `storage:end:store` - After the store request
* * `storage:end:load` - After the load request
* * `storage:error` - On any error on storage request, passes the error as an argument
* ## Canvas
* * `canvas:dragenter` - When something is dragged inside the canvas, `DataTransfer` instance passed as an argument
@ -83,6 +88,7 @@
* @param {Object} [config.domComponents={}] Components configuration, see the relative documentation
* @param {Object} [config.panels={}] Panels configuration, see the relative documentation
* @param {Object} [config.showDevices=true] If true render a select of available devices inside style manager panel
* @param {Boolean} [config.keepEmptyTextNodes=false] If false, removes empty text nodes when parsed (unless they contain a space)
* @param {string} [config.defaultCommand='select-comp'] Command to execute when no other command is running
* @param {Array} [config.plugins=[]] Array of plugins to execute on start
* @param {Object} [config.pluginsOpts={}] Custom options for plugins

17
src/editor/model/Editor.js

@ -197,13 +197,10 @@ module.exports = Backbone.Model.extend({
* @param {Object} Options
* @private
* */
componentSelected(model, val, options) {
if (!this.get('selectedComponent')) {
this.trigger('deselect-comp');
} else {
this.trigger('select-comp', [model, val, options]);
this.trigger('component:selected', arguments);
}
componentSelected(editor, selected, options) {
const prev = this.previous('selectedComponent');
prev && this.trigger('component:deselected', prev, options);
selected && this.trigger('component:selected', selected, options);
},
/**
@ -305,6 +302,9 @@ module.exports = Backbone.Model.extend({
const config = this.config;
const wrappesIsBody = config.wrappesIsBody;
const avoidProt = opts.avoidProtected;
const dumpUnusedStyles = !isUndefined(opts.dumpUnusedStyles)
? opts.dumpUnusedStyles
: config.dumpUnusedStyles;
const cssc = this.get('CssComposer');
const wrp = this.get('DomComponents').getComponent();
const protCss = !avoidProt ? config.protectedCss : '';
@ -313,7 +313,8 @@ module.exports = Backbone.Model.extend({
protCss +
this.get('CodeManager').getCode(wrp, 'css', {
cssc,
wrappesIsBody
wrappesIsBody,
dumpUnusedStyles
})
);
},

1
src/panels/model/Button.js

@ -3,6 +3,7 @@ var Backbone = require('backbone');
module.exports = Backbone.Model.extend({
defaults: {
id: '',
label: '',
className: '',
command: '',
context: '',

182
src/panels/view/ButtonView.js

@ -4,18 +4,11 @@ const $ = Backbone.$;
module.exports = Backbone.View.extend({
tagName: 'span',
events: {
click: 'clicked'
},
initialize(o) {
_.bindAll(
this,
'startTimer',
'stopTimer',
'showButtons',
'hideButtons',
'closeOnKeyPress',
'onDrop',
'initSorter',
'stopDrag'
);
var cls = this.model.get('className');
this.config = o.config || {};
this.em = this.config.em || {};
@ -27,7 +20,6 @@ module.exports = Backbone.View.extend({
this.activeCls = `${pfx}active ${ppfx}four-color`;
this.disableCls = pfx + 'active';
this.btnsVisCls = pfx + 'visible';
this.parentM = o.parentM || null;
this.className = pfx + 'btn' + (cls ? ' ' + cls : '');
this.listenTo(this.model, 'change:active updateActive', this.updateActive);
this.listenTo(this.model, 'checkActive', this.checkActive);
@ -36,78 +28,7 @@ module.exports = Backbone.View.extend({
this.listenTo(this.model, 'change:className', this.updateClassName);
this.listenTo(this.model, 'change:disable', this.updateDisable);
if (this.model.get('buttons').length) {
this.$el.on('mousedown', this.startTimer);
this.$el.append($('<div>', { class: pfx + 'arrow-rd' }));
}
if (this.em && this.em.get) this.commands = this.em.get('Commands');
this.events = {};
if (this.model.get('dragDrop')) {
this.events.mousedown = 'initDrag';
this.em.on('loaded', this.initSorter);
} else this.events.click = 'clicked';
this.delegateEvents();
},
initSorter() {
if (this.em.Canvas) {
var canvas = this.em.Canvas;
this.canvasEl = canvas.getBody();
this.sorter = new this.em.Utils.Sorter({
container: this.canvasEl,
placer: canvas.getPlacerEl(),
containerSel: '*',
itemSel: '*',
pfx: this.ppfx,
onMove: this.onDrag,
onEndMove: this.onDrop,
document: canvas.getFrameEl().contentDocument,
direction: 'a',
wmargin: 1,
nested: 1
});
var offDim = canvas.getOffset();
this.sorter.offTop = offDim.top;
this.sorter.offLeft = offDim.left;
}
},
/**
* Init dragging element
* @private
*/
initDrag() {
this.model.collection.deactivateAll(this.model.get('context'));
this.sorter.startSort(this.el);
this.sorter.setDropContent(this.model.get('options').content);
this.canvasEl.style.cursor = 'grabbing';
$(document).on('mouseup', this.stopDrag);
},
/**
* Stop dragging
* @private
*/
stopDrag() {
$(document).off('mouseup', this.stopDrag);
this.sorter.endMove();
},
/**
* During drag method
* @private
*/
onDrag(e) {},
/**
* During drag method
* @private
*/
onDrop(e) {
this.canvasEl.style.cursor = 'default';
},
/**
@ -141,63 +62,6 @@ module.exports = Backbone.View.extend({
else this.$buttons.removeClass(this.btnsVisCls);
},
/**
* Start timer for showing children buttons
*
* @return void
* */
startTimer() {
this.timeout = setTimeout(this.showButtons, this.config.delayBtnsShow);
$(document).on('mouseup', this.stopTimer);
},
/**
* Stop timer for showing children buttons
*
* @return void
* */
stopTimer() {
$(document).off('mouseup', this.stopTimer);
if (this.timeout) clearTimeout(this.timeout);
},
/**
* Show children buttons
*
* @return void
* */
showButtons() {
clearTimeout(this.timeout);
this.model.set('bntsVis', true);
$(document).on('mousedown', this.hideButtons);
$(document).on('keypress', this.closeOnKeyPress);
},
/**
* Hide children buttons
*
* @return void
* */
hideButtons(e) {
if (e) {
$(e.target).trigger('click');
}
this.model.set('bntsVis', false);
$(document).off('mousedown', this.hideButtons);
$(document).off('keypress', this.closeOnKeyPress);
},
/**
* Close buttons on ESC key press
* @param {Object} e Event
*
* @return void
* */
closeOnKeyPress(e) {
var key = e.which || e.keyCode;
if (key == 27) this.hideButtons();
},
/**
* Update active status of the button
*
@ -206,7 +70,6 @@ module.exports = Backbone.View.extend({
updateActive() {
const model = this.model;
const context = model.get('context');
const parent = this.parentM;
let command = {};
var editor = this.em && this.em.get ? this.em.get('Editor') : null;
var commandName = model.get('command');
@ -222,8 +85,6 @@ module.exports = Backbone.View.extend({
if (model.get('active')) {
model.collection.deactivateAll(context);
model.set('active', true, { silent: true }).trigger('checkActive');
parent &&
parent.set('active', true, { silent: true }).trigger('checkActive');
if (command.run) {
command.run(editor, model, model.get('options'));
@ -235,8 +96,6 @@ module.exports = Backbone.View.extend({
} else {
this.$el.removeClass(this.activeCls);
model.collection.deactivateAll(context);
parent &&
parent.set('active', false, { silent: true }).trigger('checkActive');
if (command.stop) {
command.stop(editor, model, model.get('options'));
@ -278,8 +137,6 @@ module.exports = Backbone.View.extend({
},
toogleActive() {
if (this.parentM) this.swapParent();
var active = this.model.get('active');
this.model.set('active', !active);
@ -293,35 +150,12 @@ module.exports = Backbone.View.extend({
}
},
/**
* Updates parent model swapping properties
*
* @return void
* */
swapParent() {
this.parentM.collection.deactivateAll(this.model.get('context'));
this.parentM.set('attributes', this.model.get('attributes'));
this.parentM.set('options', this.model.get('options'));
this.parentM.set('command', this.model.get('command'));
this.parentM.set('className', this.model.get('className'));
this.parentM.set('active', true, { silent: true }).trigger('checkActive');
},
render() {
const label = this.model.get('label');
const $el = this.$el;
this.updateAttributes();
this.$el.attr('class', this.className);
if (this.model.get('buttons').length) {
var btnsView = require('./ButtonsView'); //Avoid Circular Dependencies
var view = new btnsView({
collection: this.model.get('buttons'),
config: this.config,
parentM: this.model
});
this.$buttons = view.render().$el;
this.$buttons.append($('<div>', { class: this.pfx + 'arrow-l' }));
this.$el.append(this.$buttons); //childNodes avoids wrapping 'div'
}
$el.attr('class', this.className);
label && $el.append(label);
return this;
}

8
src/parser/model/ParserHtml.js

@ -152,9 +152,11 @@ module.exports = config => {
}
// Throw away empty nodes (keep spaces)
const content = node.nodeValue;
if (content != ' ' && !content.trim()) {
continue;
if (!config.keepEmptyTextNodes) {
const content = node.nodeValue;
if (content != ' ' && !content.trim()) {
continue;
}
}
}

9
src/selector_manager/index.js

@ -73,6 +73,15 @@ module.exports = config => {
* @private
*/
name: 'SelectorManager',
/**
* Get configuration object
* @return {Object}
* @private
*/
getConfig() {
return c;
},
getConfig() {
return c;

47
src/storage_manager/index.js

@ -11,8 +11,11 @@ module.exports = () => {
LocalStorage = require('./model/LocalStorage'),
RemoteStorage = require('./model/RemoteStorage');
let em;
var storages = {};
var defaultStorages = {};
const eventStart = 'storage:start';
const eventEnd = 'storage:end';
return {
/**
@ -42,6 +45,7 @@ module.exports = () => {
*/
init(config) {
c = config || {};
em = c.em;
for (var name in defaults) {
if (!(name in c)) c[name] = defaults[name];
@ -171,12 +175,20 @@ module.exports = () => {
* storageManager.store({item1: value1, item2: value2});
* */
store(data, clb) {
var st = this.get(this.getCurrent());
var dataF = {};
const st = this.get(this.getCurrent());
const toStore = {};
this.onStart('store', data);
for (var key in data) dataF[c.id + key] = data[key];
for (let key in data) {
toStore[c.id + key] = data[key];
}
return st ? st.store(dataF, clb) : null;
return st
? st.store(toStore, res => {
clb && clb(res);
this.onEnd('store', res);
})
: null;
},
/**
@ -197,9 +209,11 @@ module.exports = () => {
var result = {};
if (typeof keys === 'string') keys = [keys];
this.onStart('load', keys);
for (var i = 0, len = keys.length; i < len; i++)
for (var i = 0, len = keys.length; i < len; i++) {
keysF.push(c.id + keys[i]);
}
if (st) {
st.load(keysF, res => {
@ -211,6 +225,7 @@ module.exports = () => {
}
clb && clb(result);
this.onEnd('load', result);
});
} else {
clb && clb(result);
@ -235,6 +250,28 @@ module.exports = () => {
return this.get(this.getCurrent());
},
/**
* On start callback
* @private
*/
onStart(ctx, data) {
if (em) {
em.trigger(eventStart);
ctx && em.trigger(`${eventStart}:${ctx}`, data);
}
},
/**
* On end callback
* @private
*/
onEnd(ctx, data) {
if (em) {
em.trigger(eventEnd);
ctx && em.trigger(`${eventEnd}:${ctx}`, data);
}
},
/**
* Check if autoload is possible
* @return {Boolean}

13
src/storage_manager/model/RemoteStorage.js

@ -21,7 +21,6 @@ module.exports = require('backbone').Model.extend({
const em = this.get('em');
const before = this.get('beforeSend');
before && before();
em && em.trigger('storage:start');
},
/**
@ -33,17 +32,6 @@ module.exports = require('backbone').Model.extend({
const em = this.get('em');
console.error(err);
em && em.trigger('storage:error', err);
this.onEnd(err);
},
/**
* Triggered after the request is ended
* @param {Object|string} res End result
* @private
*/
onEnd(res) {
const em = this.get('em');
em && em.trigger('storage:end', res);
},
/**
@ -60,7 +48,6 @@ module.exports = require('backbone').Model.extend({
complete && complete(res);
clb && clb(res);
em && em.trigger('storage:response', res);
this.onEnd(text);
},
store(data, clb) {

2
test/specs/commands/index.js

@ -1,5 +1,6 @@
var Commands = require('commands');
var Models = require('./model/CommandModels');
var CommandAbstract = require('./view/CommandAbstract');
describe('Commands', () => {
describe('Main', () => {
@ -61,3 +62,4 @@ describe('Commands', () => {
});
Models.run();
CommandAbstract.run();

83
test/specs/commands/view/CommandAbstract.js

@ -0,0 +1,83 @@
const CommandAbstract = require('commands/view/CommandAbstract');
const Editor = require('editor/model/Editor');
module.exports = {
run() {
describe('CommandAbstract', () => {
let editor, editorTriggerSpy, command;
beforeEach(() => {
editor = new Editor();
editorTriggerSpy = sinon.spy(editor, 'trigger');
command = new CommandAbstract();
command.id = 'test';
});
afterEach(() => {
command = null;
editorTriggerSpy = null;
editor = null;
});
it('callRun returns result when no "abort" option specified', () => {
const runStub = sinon.stub(command, 'run').returns('result');
const result = command.callRun(editor);
expect(editorTriggerSpy.calledTwice).toEqual(true);
expect(editorTriggerSpy.getCall(0).args).toEqual([
'run:test:before',
{}
]);
expect(editorTriggerSpy.getCall(1).args).toEqual([
'run:test',
'result',
{}
]);
expect(result).toEqual('result');
expect(runStub.calledOnce).toEqual(true);
});
it('callRun returns undefined when "abort" option is specified', () => {
const runStub = sinon.stub(command, 'run').returns('result');
const result = command.callRun(editor, { abort: true });
expect(editorTriggerSpy.calledTwice).toEqual(true);
expect(editorTriggerSpy.getCall(0).args).toEqual([
'run:test:before',
{ abort: true }
]);
expect(editorTriggerSpy.getCall(1).args).toEqual([
'abort:test',
{ abort: true }
]);
expect(result).toEqual(undefined);
expect(runStub.notCalled).toEqual(true);
});
it('callStop returns result', () => {
const stopStub = sinon.stub(command, 'stop').returns('stopped');
const result = command.callStop(editor);
expect(editorTriggerSpy.calledTwice).toEqual(true);
expect(editorTriggerSpy.getCall(0).args).toEqual([
'stop:test:before',
{}
]);
expect(editorTriggerSpy.getCall(1).args).toEqual([
'stop:test',
'stopped',
{}
]);
expect(result).toEqual('stopped');
expect(stopStub.calledOnce).toEqual(true);
});
});
}
};

31
test/specs/css_composer/view/CssRulesView.js

@ -6,7 +6,7 @@ module.exports = {
run() {
describe('CssRulesView', () => {
let obj;
const prefix = 'rules-';
const prefix = 'rules';
const devices = [
{
name: 'Mobile portrait',
@ -62,8 +62,9 @@ module.exports = {
((left && left.replace('px', '')) || Number.MAX_VALUE)
);
});
foundStylesContainers.each(function($styleC, idx) {
expect($styleC.id).toEqual(prefix + sortedDevicesWidthMedia[idx]);
foundStylesContainers.each(($styleC, idx) => {
const width = sortedDevicesWidthMedia[idx];
expect($styleC.id).toEqual(`${prefix}${width ? `-${width}` : ''}`);
});
});
@ -73,6 +74,30 @@ module.exports = {
expect(obj.addToCollection.calledOnce).toExist(true);
});
it('Add correctly rules with different media queries', () => {
const foundStylesContainers = obj.$el.find('div');
const rules = [
{
selectorsAdd: '#testid'
},
{
selectorsAdd: '#testid2',
mediaText: '(max-width: 1000px)'
},
{
selectorsAdd: '#testid3',
mediaText: '(min-width: 900px)'
},
{
selectorsAdd: '#testid4',
mediaText: 'screen and (max-width: 900px) and (min-width: 600px)'
}
];
obj.collection.add(rules);
const stylesCont = obj.el.querySelector(`#${obj.className}`);
expect(stylesCont.children.length).toEqual(rules.length);
});
it('Render new rule', () => {
obj.collection.add({});
expect(obj.$el.find(`#${prefix}`).html()).toExist();

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

@ -67,6 +67,21 @@ module.exports = {
expect(obj.get('stylable')).toEqual(true);
});
it('Sets attributes correctly from traits', () => {
obj.set('traits', [
{
label: 'Title',
name: 'title',
value: 'The title'
},
{
label: 'Context',
value: 'primary'
}
]);
expect(obj.get('attributes')).toEqual({ title: 'The title' });
});
it('Has expected name', () => {
expect(obj.getName()).toEqual('Box');
});

43
test/specs/grapesjs/index.js

@ -157,6 +157,27 @@ describe('GrapesJS', () => {
expect(editor.getStyle().length).toEqual(2);
});
it('Init editor from element with multiple font-face at-rules', () => {
config.fromElement = 1;
config.storageManager = { type: 0 };
fixture.innerHTML =
`
<style>
@font-face {
font-family: 'Glyphicons Halflings';
src: url(https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2) format('woff2');
}
@font-face {
font-family: 'Droid Sans';
src: url(https://fonts.gstatic.com/s/droidsans/v8/SlGVmQWMvZQIdix7AFxXkHNSbRYXags.woff2) format('woff2');
}
</style>` + htmlString;
const editor = obj.init(config);
const css = editor.getCss();
const styles = editor.getStyle();
expect(styles.length).toEqual(2);
});
it('Set components as HTML', () => {
var editor = obj.init(config);
editor.setComponents(htmlString);
@ -355,5 +376,27 @@ describe('GrapesJS', () => {
editor = obj.init(config);
expect(editor.Commands.get('export-template').test).toEqual(1);
});
it('Dump unused css classes/selectors', () => {
config.fromElement = 1;
config.storageManager = { type: 0 };
fixture.innerHTML = documentEl;
const editor = obj.init(config);
const css = editor.getCss({ dumpUnusedStyles: 1 });
const protCss = editor.getConfig().protectedCss;
expect(editor.getStyle().length).toEqual(2);
expect(css).toEqual(`${protCss}.test2{color:red;}.test3{color:blue;}`);
});
it('Dump unused css classes/selectors using the init option', () => {
config.fromElement = 1;
config.storageManager = { type: 0 };
fixture.innerHTML = documentEl;
const editor = obj.init({ ...config, dumpUnusedStyles: 1 });
const css = editor.getCss();
const protCss = editor.getConfig().protectedCss;
expect(editor.getStyle().length).toEqual(2);
expect(css).toEqual(`${protCss}.test2{color:red;}.test3{color:blue;}`);
});
});
});

33
test/specs/parser/model/ParserCss.js

@ -277,6 +277,39 @@ module.exports = {
expect(obj.parse(str)).toEqual(result);
});
it('Parses multiple font-face at-rules', () => {
const str = `
@font-face {
font-family: "Open Sans";
}
@font-face {
font-family: 'Glyphicons Halflings';
src:url(https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot)
}`;
const result = [
{
selectors: [],
selectorsAdd: '',
style: { 'font-family': '"Open Sans"' },
singleAtRule: 1,
atRuleType: 'font-face'
},
{
selectors: [],
selectorsAdd: '',
style: {
'font-family': "'Glyphicons Halflings'",
src:
'url(https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot)'
},
singleAtRule: 1,
atRuleType: 'font-face'
}
];
const parsed = obj.parse(str);
expect(parsed).toEqual(result);
});
it('Parse ID rule', () => {
var str = `#test { color: red }`;
var result = {

44
test/specs/parser/model/ParserHtml.js

@ -372,6 +372,50 @@ module.exports = {
expect(res.css).toEqual(resCss);
});
it('Respect multiple font-faces contained in styles in html', () => {
const str = `
<style>
@font-face {
font-family: "Open Sans";
src:url(https://fonts.gstatic.com/s/droidsans/v8/SlGVmQWMvZQIdix7AFxXkHNSbRYXags.woff2)
}
@font-face {
font-family: 'Glyphicons Halflings';
src:url(https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot)
}
</style>
<div>a div</div>
`;
const expected = [
{
selectors: [],
selectorsAdd: '',
style: {
'font-family': '"Open Sans"',
src:
'url(https://fonts.gstatic.com/s/droidsans/v8/SlGVmQWMvZQIdix7AFxXkHNSbRYXags.woff2)'
},
singleAtRule: 1,
atRuleType: 'font-face'
},
{
selectors: [],
selectorsAdd: '',
style: {
'font-family': "'Glyphicons Halflings'",
src:
'url(https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot)'
},
singleAtRule: 1,
atRuleType: 'font-face'
}
];
const res = obj.parse(str, new ParserCss());
expect(res.css).toEqual(expected);
});
it('Parse nested div with text and spaces', () => {
var str = '<div> <p>TestText</p> </div>';
var result = {

Loading…
Cancel
Save