Browse Source

Merge pull request #1045 from artf/dev

v0.14.9
pull/1362/head
Artur Arseniev 9 years ago
committed by GitHub
parent
commit
4cfc3d5ad8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .gitignore
  2. 3
      README.md
  3. 1491
      dist/grapes.js
  4. 6
      dist/grapes.min.js
  5. 2
      package-lock.json
  6. 2
      package.json
  7. 4
      src/block_manager/config/config.js
  8. 3
      src/block_manager/view/BlockView.js
  9. 44
      src/canvas/view/CanvasView.js
  10. 25
      src/commands/index.js
  11. 32
      src/commands/view/CommandAbstract.js
  12. 8
      src/commands/view/SelectComponent.js
  13. 18
      src/css_composer/index.js
  14. 69
      src/css_composer/view/CssRulesView.js
  15. 42
      src/dom_components/model/Component.js
  16. 14
      src/dom_components/model/ComponentVideo.js
  17. 5
      src/dom_components/view/ComponentView.js
  18. 42
      src/editor/config/config.js
  19. 30
      src/editor/index.js
  20. 7
      src/editor/model/Editor.js
  21. 1
      src/panels/model/Button.js
  22. 182
      src/panels/view/ButtonView.js
  23. 8
      src/parser/model/ParserHtml.js
  24. 2
      src/rich_text_editor/model/RichTextEditor.js
  25. 72
      src/storage_manager/index.js
  26. 13
      src/storage_manager/model/RemoteStorage.js
  27. 6
      src/style_manager/view/PropertyView.js
  28. 4
      src/styles/scss/_gjs_variables.scss
  29. 9
      src/trait_manager/model/Traits.js
  30. 6
      src/trait_manager/view/TraitsView.js
  31. 8
      src/utils/Droppable.js
  32. 35
      src/utils/extender.js
  33. 4
      test/helper.js
  34. 5
      test/specs/code_manager/model/CodeModels.js
  35. 79
      test/specs/css_composer/view/CssRulesView.js
  36. 15
      test/specs/dom_components/model/Component.js
  37. 166
      test/specs/grapesjs/index.js
  38. 33
      test/specs/parser/model/ParserCss.js
  39. 44
      test/specs/parser/model/ParserHtml.js

2
.gitignore

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

3
README.md

@ -124,9 +124,6 @@ You could also grab the content directly from the element with `fromElement` pro
For more practical example I suggest to look up the code inside this demo: http://grapesjs.com/demo.html
## Development
GrapesJS uses [Webpack](https://github.com/webpack/webpack) as a module bundler and [Babel](https://github.com/babel/babel) as a compiler.

1491
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.6",
"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.6",
"version": "0.14.9",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",

4
src/block_manager/config/config.js

@ -3,7 +3,5 @@ module.exports = {
// With the empty value, nothing will be rendered
appendTo: '',
blocks: [],
appendTo: ''
blocks: []
};

3
src/block_manager/view/BlockView.js

@ -40,7 +40,8 @@ module.exports = Backbone.View.extend({
// Note: data are not available on dragenter for security reason,
// but will use dragContent as I need it for the Sorter context
ev.dataTransfer.setData(type, data);
// IE11 supports only 'text' data type
ev.dataTransfer.setData('text', data);
this.em.set('dragContent', content);
},

44
src/canvas/view/CanvasView.js

@ -97,23 +97,7 @@ module.exports = Backbone.View.extend({
const colorWarn = '#ffca6f';
let baseCss = `
* {
box-sizing: border-box;
}
html, body, #wrapper {
min-height: 100%;
}
body {
margin: 0;
height: 100%;
background-color: #fff
}
#wrapper {
overflow: auto;
overflow-x: hidden;
}
`;
// I need all this styles to make the editor work properly
// Remove `html { height: 100%;}` from the baseCss as it gives jumpings
// effects (on ENTER) with RTE like CKEditor (maybe some bug there?!?)
// With `body {height: auto;}` jumps in CKEditor are removed but in
@ -121,10 +105,8 @@ module.exports = Backbone.View.extend({
// `body {height: 100%;}`.
// For the moment I give the priority to Firefox as it might be
// CKEditor's issue
// I need all this styles to make the editor work properly
var frameCss = `
${baseCss}
${em.config.baseCss || ''}
.${ppfx}dashed *[data-highlightable] {
outline: 1px dashed rgba(170,170,170,0.7);
@ -171,18 +153,6 @@ module.exports = Backbone.View.extend({
cursor: -webkit-grabbing;
}
* ::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1)
}
* ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2)
}
* ::-webkit-scrollbar {
width: 10px
}
${conf.canvasCss || ''}
${protCss || ''}
`;
@ -208,7 +178,13 @@ module.exports = Backbone.View.extend({
// property keymaster (and many others) still use it... using `defineProperty`
// hack seems the only way
const createCustomEvent = (e, cls) => {
var oEvent = new window[cls](e.type, e);
let oEvent;
try {
oEvent = new window[cls](e.type, e);
} catch (e) {
oEvent = document.createEvent(cls);
oEvent.initEvent(e.type, true, true);
}
oEvent.keyCodeVal = e.keyCode;
['keyCode', 'which'].forEach(prop => {
Object.defineProperty(oEvent, prop, {
@ -327,7 +303,7 @@ module.exports = Backbone.View.extend({
updateScript(view) {
if (!view.scriptContainer) {
view.scriptContainer = $('<div>');
this.getJsContainer().append(view.scriptContainer.get(0));
this.getJsContainer().appendChild(view.scriptContainer.get(0));
}
const model = view.model;

25
src/commands/index.js

@ -55,6 +55,7 @@ module.exports = () => {
}
delete obj.initialize;
obj.id = id;
commands[id] = AbsCommands.extend(obj);
return this;
};
@ -111,15 +112,7 @@ module.exports = () => {
defaultCommands['tlb-delete'] = {
run(ed) {
var sel = ed.getSelected();
if (!sel || !sel.get('removable')) {
console.warn('The element is not removable');
return;
}
ed.select(null);
sel.destroy();
return ed.runCommand('core:component-delete');
}
};
@ -147,7 +140,7 @@ module.exports = () => {
const event = opts && opts.event;
const sel = ed.getSelected();
const toolbarStyle = ed.Canvas.getToolbarEl().style;
const nativeDrag = event.type == 'dragstart';
const nativeDrag = event && event.type == 'dragstart';
const hideTlb = () => {
toolbarStyle.display = 'none';
@ -234,6 +227,18 @@ module.exports = () => {
coll.add(clp.clone(), { at });
}
};
defaultCommands['core:component-delete'] = (ed, sender, opts = {}) => {
let component = opts.component || ed.getSelected();
if (!component || !component.get('removable')) {
console.warn('The element is not removable');
return;
}
ed.select(null);
component.destroy();
return component;
};
if (c.em) c.model = c.em.get('Canvas');

32
src/commands/view/CommandAbstract.js

@ -90,6 +90,38 @@ module.exports = Backbone.View.extend({
* */
init(o) {},
/**
* Method that run command
* @param {Object} editor Editor instance
* @param {Object} [options={}] Options
* @private
* */
callRun(editor, options = {}) {
const id = this.id;
editor.trigger(`run:${id}:before`, options);
if (options && options.abort) {
editor.trigger(`abort:${id}`, options);
return;
}
const result = this.run(editor, editor, options);
editor.trigger(`run:${id}`, result, options);
},
/**
* Method that run command
* @param {Object} editor Editor instance
* @param {Object} [options={}] Options
* @private
* */
callStop(editor, options = {}) {
const id = this.id;
editor.trigger(`stop:${id}:before`, options);
const result = this.stop(editor, editor, options);
editor.trigger(`stop:${id}`, result, options);
},
/**
* Method that run command
* @param {Object} em Editor model

8
src/commands/view/SelectComponent.js

@ -70,13 +70,7 @@ module.exports = {
if (key == 8 || key == 46) {
if (!focused) e.preventDefault();
if (comp && !focused) {
if (!comp.get('removable')) return;
comp.set('status', '');
comp.destroy();
this.hideBadge();
this.clean();
this.hideHighlighter();
this.editorModel.set('selectedComponent', null);
this.editor.runCommand('core:component-delete');
}
}
},

18
src/css_composer/index.js

@ -15,6 +15,7 @@
* rules: '.myClass{ color: red}',
* }
*/
import { isArray } from 'underscore';
module.exports = () => {
let em;
@ -135,7 +136,9 @@ module.exports = () => {
obj = c.em.get('Parser').parseCss(d.css);
}
if (obj) {
if (isArray(obj)) {
obj.length && rules.reset(obj);
} else if (obj) {
rules.reset(obj);
}
@ -174,13 +177,18 @@ module.exports = () => {
* color: '#fff',
* });
* */
add(selectors, state, width, opts) {
add(selectors, state, width, opts = {}) {
var s = state || '';
var w = width || '';
var opt = opts || {};
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 = '';

69
src/css_composer/view/CssRulesView.js

@ -1,5 +1,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) {
@ -31,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;
@ -40,7 +50,7 @@ module.exports = require('backbone').View.extend({
// I have to render keyframes of the same name together
// Unfortunately at the moment I didn't find the way of appending them
// if not staticly, via appendData
if (model.get('atRuleType') == 'keyframes') {
if (model.get('atRuleType') === 'keyframes') {
const atRule = model.getAtRule();
let atRuleEl = this.atRules[atRule];
@ -61,22 +71,73 @@ module.exports = require('backbone').View.extend({
rendered = view.render().el;
}
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.appendChild(rendered);
else this.$el.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;
},
getMediaWidth(mediaText) {
return (
mediaText &&
mediaText
.replace(`(${this.em.getConfig('mediaCondition')}: `, '')
.replace(')', '')
);
},
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
this.em
.get('DeviceManager')
.getAll()
.map(model => model.get('widthMedia'))
.sort(
(left, right) =>
((right && right.replace('px', '')) || Number.MAX_VALUE) -
((left && left.replace('px', '')) || Number.MAX_VALUE)
)
.forEach(widthMedia => {
$(`<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;
}
});

42
src/dom_components/model/Component.js

@ -6,6 +6,7 @@ import {
has,
clone,
isString,
forEach,
keys
} from 'underscore';
import { shallowDiff, hasDnd } from 'utils/mixins';
@ -186,8 +187,12 @@ const Component = Backbone.Model.extend(Styleable).extend(
this.initComponents();
this.initToolbar();
this.set('status', '');
this.listenTo(this.get('classes'), 'add remove change', () =>
this.emitUpdate('classes')
// Register global updates for collection properties
['classes', 'traits'].forEach(name =>
this.listenTo(this.get(name), 'add remove change', () =>
this.emitUpdate(name)
)
);
this.init();
},
@ -509,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;
}
}
});
@ -714,11 +720,35 @@ const Component = Backbone.Model.extend(Styleable).extend(
*/
toJSON(...args) {
const obj = Backbone.Model.prototype.toJSON.apply(this, args);
const scriptStr = this.getScriptString();
obj.attributes = this.getAttributes();
delete obj.attributes.class;
delete obj.toolbar;
scriptStr && (obj.script = scriptStr);
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;
},

14
src/dom_components/model/ComponentVideo.js

@ -210,8 +210,7 @@ module.exports = Component.extend(
changeProp: 1
},
this.getAutoplayTrait(),
this.getLoopTrait(),
this.getControlsTrait()
this.getLoopTrait()
];
},
@ -263,11 +262,14 @@ module.exports = Component.extend(
* @private
*/
getYoutubeSrc() {
var url = this.get('ytUrl');
url += this.get('videoId') + '?';
const id = this.get('videoId');
let url = this.get('ytUrl');
url += id + '?';
url += this.get('autoplay') ? '&autoplay=1' : '';
url += !this.get('controls') ? '&controls=0' : '';
url += this.get('loop') ? '&loop=1' : '';
url += !this.get('controls') ? '&controls=0&showinfo=0' : '';
// Loop works only with playlist enabled
// https://stackoverflow.com/questions/25779966/youtube-iframe-loop-doesnt-work
url += this.get('loop') ? `&loop=1&playlist=${id}` : '';
return url;
},

5
src/dom_components/view/ComponentView.js

@ -1,4 +1,4 @@
import { isArray } from 'underscore';
import { isArray, isEmpty } from 'underscore';
const ComponentsView = require('./ComponentsView');
@ -151,7 +151,8 @@ module.exports = Backbone.View.extend({
if (em && em.get('avoidInlineStyle')) {
this.el.id = model.getId();
model.setStyle(model.getStyle());
const style = model.getStyle();
!isEmpty(style) && model.setStyle(style);
} else {
this.setAttribute('style', model.styleToString());
}

42
src/editor/config/config.js

@ -30,6 +30,40 @@ module.exports = {
// Width for the editor container
width: '100%',
// By default Grapes injects base CSS into the canvas. For example, it sets body margin to 0
// and sets a default background color of white. This CSS is desired in most cases.
// use this property if you wish to overwrite the base CSS to your own CSS. This is most
// useful if for example your template is not based off a document with 0 as body margin.
baseCss: `
* {
box-sizing: border-box;
}
html, body, #wrapper {
min-height: 100%;
}
body {
margin: 0;
height: 100%;
background-color: #fff
}
#wrapper {
overflow: auto;
overflow-x: hidden;
}
* ::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1)
}
* ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2)
}
* ::-webkit-scrollbar {
width: 10px
}
`,
// CSS that could only be seen (for instance, inside the code viewer)
protectedCss: '* { box-sizing: border-box; } body {margin: 0;}',
@ -62,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,
@ -80,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

30
src/editor/index.js

@ -39,9 +39,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
@ -58,6 +62,9 @@
* ## Commands
* * `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview'))
* * `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview'))
* * `run:{commandName}:before` - Triggered before the command is called
* * `stop:{commandName}:before` - Triggered before the command is called to stop
* * `abort:{commandName}` - Triggered when the command execution is aborted (`editor.on(`run:preview:before`, opts => opts.abort = 1);`)
* ## General
* * `canvasScroll` - Triggered when the canvas is scrolle
* * `undo` - Undo executed
@ -80,6 +87,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
@ -426,14 +434,11 @@ module.exports = config => {
* @example
* editor.runCommand('myCommand', {someValue: 1});
*/
runCommand(id, options) {
var result;
var command = em.get('Commands').get(id);
runCommand(id, options = {}) {
let result;
const command = em.get('Commands').get(id);
if (command) result = command.callRun(this, options);
if (command) {
result = command.run(this, this, options);
this.trigger('run:' + id);
}
return result;
},
@ -445,14 +450,11 @@ module.exports = config => {
* @example
* editor.stopCommand('myCommand', {someValue: 1});
*/
stopCommand(id, options) {
var result;
var command = em.get('Commands').get(id);
stopCommand(id, options = {}) {
let result;
const command = em.get('Commands').get(id);
if (command) result = command.callStop(this, options);
if (command) {
result = command.stop(this, this, options);
this.trigger('stop:' + id);
}
return result;
},

7
src/editor/model/Editor.js

@ -52,6 +52,7 @@ module.exports = Backbone.Model.extend({
this.set('Config', c);
this.set('modules', []);
this.set('toLoad', []);
this.set('storables', []);
if (c.el && c.fromElement) this.config.components = c.el.innerHTML;
@ -93,7 +94,7 @@ module.exports = Backbone.Model.extend({
clb && clb();
};
if (sm && sm.getConfig().autoload) {
if (sm && sm.canAutoload()) {
this.load(postLoad);
} else {
postLoad();
@ -346,7 +347,7 @@ module.exports = Backbone.Model.extend({
for (var el in obj) store[el] = obj[el];
});
sm.store(store, (res) => {
sm.store(store, res => {
clb && clb(res);
this.set('changesCount', 0);
this.trigger('storage:store', store);
@ -394,7 +395,7 @@ module.exports = Backbone.Model.extend({
sm.load(load, res => {
this.cacheLoad = res;
clb && clb(res);
this.trigger('storage:load', res);
setTimeout(() => this.trigger('storage:load', res), 0);
});
},

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

2
src/rich_text_editor/model/RichTextEditor.js

@ -113,7 +113,7 @@ export default class RichTextEditor {
btn.className = btn.className.replace(active, '').trim();
// doc.queryCommandValue(name) != 'false'
if (doc.queryCommandState(name)) {
if (doc.queryCommandSupported(name) && doc.queryCommandState(name)) {
btn.className += ` ${active}`;
}

72
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];
@ -54,6 +58,14 @@ module.exports = () => {
return this;
},
/**
* Get configuration object
* @return {Object}
* */
getConfig() {
return c;
},
/**
* Checks if autosave is enabled
* @return {Boolean}
@ -163,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;
},
/**
@ -189,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 => {
@ -203,6 +225,7 @@ module.exports = () => {
}
clb && clb(result);
this.onEnd('load', result);
});
} else {
clb && clb(result);
@ -220,12 +243,43 @@ module.exports = () => {
},
/**
* Get configuration object
* @return {Object}
* Get current storage
* @return {Storage}
* */
getCurrentStorage() {
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}
* @private
* */
getConfig() {
return c;
canAutoload() {
const storage = this.getCurrentStorage();
return storage && this.getConfig().autoload;
}
};
};

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

6
src/style_manager/view/PropertyView.js

@ -123,7 +123,11 @@ module.exports = Backbone.View.extend({
* @return {HTMLElement}
*/
getClearEl() {
return this.el.querySelector(`[${clearProp}]`);
if (!this.clearEl) {
this.clearEl = this.el.querySelector(`[${clearProp}]`);
}
return this.clearEl;
},
/**

4
src/styles/scss/_gjs_variables.scss

@ -73,6 +73,6 @@ $animSpeed: 0.2s !default;
$mainFont: Helvetica, sans-serif !default;
$fontPath: '../fonts' !default;
$fontName: 'main-fonts' !default;
$fontSize: 0.7rem;
$fontSizeS: 0.75rem;
$fontSize: 0.7rem !default;
$fontSizeS: 0.75rem !default;
$fontV: 20 !default;//random(1000)

9
src/trait_manager/model/Traits.js

@ -8,6 +8,15 @@ module.exports = Backbone.Collection.extend({
initialize(coll, options = {}) {
this.em = options.em || '';
this.listenTo(this, 'add', this.handleAdd);
},
handleAdd(model) {
const target = this.target;
if (target) {
model.target = target;
}
},
setTarget(target) {

6
src/trait_manager/view/TraitsView.js

@ -23,7 +23,8 @@ module.exports = DomainViews.extend({
this.pfx = config.stylePrefix || '';
this.ppfx = config.pStylePrefix || '';
this.className = this.pfx + 'traits';
this.listenTo(this.em, 'change:selectedComponent', this.updatedCollection);
const toListen = 'component:selected component:update:traits';
this.listenTo(this.em, toListen, this.updatedCollection);
this.updatedCollection();
},
@ -33,8 +34,9 @@ module.exports = DomainViews.extend({
*/
updatedCollection() {
const ppfx = this.ppfx;
const comp = this.em.getSelected();
this.el.className = `${this.className} ${ppfx}one-bg ${ppfx}two-color`;
var comp = this.em.get('selectedComponent');
if (comp) {
this.collection = comp.get('traits');
this.render();

8
src/utils/Droppable.js

@ -3,7 +3,7 @@
*/
import { on } from 'utils/mixins';
import { bindAll } from 'underscore';
import { bindAll, indexOf } from 'underscore';
export default class Droppable {
constructor(em) {
@ -138,17 +138,17 @@ export default class Droppable {
}
} else if (dragContent) {
content = dragContent;
} else if (types.indexOf('text/html') >= 0) {
} else if (indexOf(types, 'text/html') >= 0) {
content = dataTransfer
.getData('text/html')
.replace(/<\/?meta[^>]*>/g, '');
} else if (types.indexOf('text/uri-list') >= 0) {
} else if (indexOf(types, 'text/uri-list') >= 0) {
content = {
type: 'link',
attributes: { href: content },
content: content
};
} else if (types.indexOf('text/json') >= 0) {
} else if (indexOf(types, 'text/json') >= 0) {
const json = dataTransfer.getData('text/json');
json && (content = JSON.parse(json));
}

35
src/utils/extender.js

@ -1,4 +1,4 @@
import { isObject } from 'underscore';
import { isObject, isString, each, isUndefined } from 'underscore';
module.exports = ({ $, Backbone }) => {
if (Backbone) {
@ -177,11 +177,36 @@ module.exports = ({ $, Backbone }) => {
return this;
};
(fn.remove = function() {
return this.each(node => {
return node.parentNode && node.parentNode.removeChild(node);
});
// For SVGs in IE
(fn.removeClass = function(c) {
if (!arguments.length) {
return this.attr('class', '');
}
const classes = isString(c) && c.match(/\S+/g);
return classes
? this.each(function(el) {
each(classes, function(c) {
if (el.classList) {
el.classList.remove(c);
} else {
const val = el.className;
const bval = el.className.baseVal;
if (!isUndefined(bval)) {
val.baseVal = bval.replace(c, '');
} else {
el.className = val.replace(c, '');
}
}
});
})
: this;
}),
(fn.remove = function() {
return this.each(node => {
return node.parentNode && node.parentNode.removeChild(node);
});
}),
// For spectrum compatibility
(fn.bind = function(ev, h) {
return this.on(ev, h);

4
test/helper.js

@ -3,7 +3,9 @@ import expect from 'expect';
import sinon from 'sinon';
import { JSDOM } from 'jsdom';
const dom = new JSDOM('<!doctype html><html><body></body></html>');
const dom = new JSDOM('<!doctype html><html><body></body></html>', {
resources: 'usable'
});
const window = dom.window;
// Fix for the require of jquery

5
test/specs/code_manager/model/CodeModels.js

@ -71,9 +71,12 @@ module.exports = {
});
describe('CssGenerator', () => {
var newCssComp = () => new CssComposer().init();
let newCssComp;
beforeEach(() => {
em = new Editor({});
newCssComp = () => new CssComposer().init({ em });
cc = em.get('CssComposer');
obj = new CssGenerator();
dcomp = new DomComponents();

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

@ -1,15 +1,41 @@
var CssRulesView = require('css_composer/view/CssRulesView');
var CssRules = require('css_composer/model/CssRules');
const CssRulesView = require('css_composer/view/CssRulesView');
const CssRules = require('css_composer/model/CssRules');
const Editor = require('editor/model/Editor');
module.exports = {
run() {
describe('CssRulesView', () => {
let obj;
const prefix = 'rules';
const devices = [
{
name: 'Mobile portrait',
width: '320px',
widthMedia: '480px'
},
{
name: 'Tablet',
width: '768px',
widthMedia: '992px'
},
{
name: 'Desktop',
width: '',
widthMedia: ''
}
];
beforeEach(function() {
var col = new CssRules([]);
const col = new CssRules([]);
obj = new CssRulesView({
collection: col
collection: col,
config: {
em: new Editor({
deviceManager: {
devices
}
})
}
});
document.body.innerHTML = '<div id="fixtures"></div>';
document.body.querySelector('#fixtures').appendChild(obj.render().el);
@ -23,8 +49,23 @@ module.exports = {
expect(CssRulesView).toExist();
});
it('Collection is empty', () => {
expect(obj.$el.html()).toNotExist();
it('Collection is empty. Styles structure bootstraped', () => {
expect(obj.$el.html()).toExist();
const foundStylesContainers = obj.$el.find('div');
expect(foundStylesContainers.length).toEqual(devices.length);
const sortedDevicesWidthMedia = devices
.map(dvc => dvc.widthMedia)
.sort((left, right) => {
return (
((right && right.replace('px', '')) || Number.MAX_VALUE) -
((left && left.replace('px', '')) || Number.MAX_VALUE)
);
});
foundStylesContainers.each(($styleC, idx) => {
const width = sortedDevicesWidthMedia[idx];
expect($styleC.id).toEqual(`${prefix}${width ? `-${width}` : ''}`);
});
});
it('Add new rule', () => {
@ -33,9 +74,33 @@ 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.html()).toExist();
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');
});

166
test/specs/grapesjs/index.js

@ -28,6 +28,7 @@ describe('GrapesJS', () => {
});
beforeEach(() => {
storage = {};
htmlString = '<div class="test1"></div><div class="test2"></div>';
cssString = '.test2{color:red}.test3{color:blue}';
documentEl = '<style>' + cssString + '</style>' + htmlString;
@ -36,15 +37,22 @@ describe('GrapesJS', () => {
storageManager: {
autoload: 0,
autosave: 0,
type: ''
type: 0
}
};
obj = grapesjs;
//fixture = $('<div id="' + editorName + '"></div>');
//fixture.empty().appendTo(fixtures);
document.body.innerHTML = `<div id="fixtures"><div id="${editorName}"></div></div>`;
fixtures = document.body.querySelector('#fixtures');
fixture = document.body.querySelector(`#${editorName}`);
});
afterEach(() => {
var plugins = obj.plugins.getAll();
for (let id in plugins) {
if (plugins.hasOwnProperty(id)) {
delete plugins[id];
}
}
});
it('Main object should be loaded', () => {
@ -79,6 +87,32 @@ describe('GrapesJS', () => {
expect(editor.getStyle().length).toEqual(0);
});
it('Editor canvas baseCSS can be overwritten', () => {
config.components = htmlString;
config.baseCss = '#wrapper { background-color: #eee; }';
config.protectedCss = '';
var editor = obj.init(config);
expect(window.frames[0].document.documentElement.outerHTML).toInclude(
config.baseCss
);
expect(window.frames[0].document.documentElement.outerHTML)
.toNotInclude(`body {
margin: 0;`);
});
it('Editor canvas baseCSS defaults to sensible values if not defined', () => {
config.components = htmlString;
config.protectedCss = '';
var editor = obj.init(config);
expect(window.frames[0].document.documentElement.outerHTML)
.toInclude(`body {
margin: 0;`);
});
it('Init editor with html', () => {
config.components = htmlString;
var editor = obj.init(config);
@ -107,28 +141,43 @@ describe('GrapesJS', () => {
).toEqual('test2');
});
it.skip('Init editor from element', () => {
it('Init editor from element', () => {
config.fromElement = 1;
fixtures.innerHTML = documentEl;
var editor = obj.init(config);
var html = editor.getHtml();
var css = editor.getCss();
var protCss = editor.getConfig().protectedCss;
/*
(html ? html : '').should.equal(htmlString);
(css ? css : '').should.equal(protCss + '.test2{color:red;}');// .test3 is discarded in css
editor.getComponents().length.should.equal(2);
editor.getStyle().length.should.equal(2);
*/
expect(html ? html : '').toEqual(htmlString);
config.storageManager = { type: 0 };
fixture.innerHTML = documentEl;
const editor = obj.init(config);
const html = editor.getHtml();
const css = editor.getCss();
const protCss = editor.getConfig().protectedCss;
expect(html).toEqual(htmlString);
expect(editor.getComponents().length).toEqual(2);
// .test3 is discarded in css
expect(css ? css : '').toEqual(protCss + '.test2{color:red;}');
// bust is still here
// .test3 is discarded in CSS
expect(css).toEqual(`${protCss}.test2{color:red;}`);
// but it's still there
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);
@ -171,13 +220,13 @@ describe('GrapesJS', () => {
});
it.skip('Adds new storage as plugin and store data there', done => {
var pluginName = storageId + '-plugin';
obj.plugins.add(pluginName, edt => {
edt.StorageManager.add(storageId, storageMock);
});
const pluginName = storageId + '-p2';
obj.plugins.add(pluginName, e =>
e.StorageManager.add(storageId, storageMock)
);
config.storageManager.type = storageId;
config.plugins = [pluginName];
var editor = obj.init(config);
const editor = obj.init(config);
editor.setComponents(htmlString);
editor.store(() => {
editor.load(data => {
@ -187,6 +236,45 @@ describe('GrapesJS', () => {
});
});
it('Adds a new storage and fetch correctly data from it', done => {
fixture.innerHTML = documentEl;
const styleResult = { color: 'white', display: 'block' };
const style = [
{
selectors: [{ name: 'sclass1' }],
style: { color: 'green' }
},
{
selectors: [{ name: 'test2' }],
style: styleResult
},
{
selectors: [{ name: 'test3' }],
style: { color: 'black', display: 'block' }
}
];
storage = {
css: '* { box-sizing: border-box; } body {margin: 0;}',
styles: JSON.stringify(style)
};
const pluginName = storageId + '-p';
obj.plugins.add(pluginName, e =>
e.StorageManager.add(storageId, storageMock)
);
config.fromElement = 1;
config.storageManager.type = storageId;
config.plugins = [pluginName];
config.storageManager.autoload = 1;
const editor = obj.init(config);
editor.on('load', () => {
const cc = editor.CssComposer;
expect(cc.getAll().length).toEqual(style.length);
// expect(cc.setClassRule('test2').getStyle()).toEqual(styleResult);
done();
});
});
it('Execute plugins with custom options', () => {
var pluginName = storageId + '-plugin-opts';
obj.plugins.add(pluginName, (edt, opts) => {
@ -226,6 +314,32 @@ describe('GrapesJS', () => {
expect(editor.testVal).toEqual(htmlString + '5');
});
it('Trigger custom command events', () => {
const id = 'test-command';
const editor = obj.init(config);
const result = {};
editor.on(`run:${id}`, () => (result.run = 1));
editor.on(`run:${id}:before`, () => (result.runBefore = 1));
editor.on(`stop:${id}`, () => (result.stop = 1));
editor.on(`stop:${id}:before`, () => (result.stopBefore = 1));
editor.on(`abort:${id}`, () => (result.abort = 1));
editor.Commands.add(id, {
run() {},
stop() {}
});
editor.runCommand(id);
editor.stopCommand(id);
editor.on(`run:${id}:before`, opts => (opts.abort = 1));
editor.runCommand(id);
expect(result).toEqual({
run: 1,
runBefore: 1,
stop: 1,
stopBefore: 1,
abort: 1
});
});
it('Set default devices', () => {
config.deviceManager = {};
config.deviceManager.devices = [

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