Browse Source

Merge pull request #4214 from artf/dev

Merge dev
pull/4254/head
Artur Arseniev 5 years ago
committed by GitHub
parent
commit
0ca43b041f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CONTRIBUTING.md
  2. 2
      dist/css/grapes.min.css
  3. 4
      dist/grapes.min.js
  4. 2
      dist/grapes.min.js.map
  5. 1
      docs/api/storage_manager.md
  6. 5288
      index.d.ts
  7. 4
      package.json
  8. 6
      src/code_manager/model/CssGenerator.js
  9. 16
      src/code_manager/model/HtmlGenerator.js
  10. 18
      src/commands/view/ComponentDelete.js
  11. 3
      src/commands/view/SelectComponent.js
  12. 1
      src/common/index.js
  13. 4
      src/common/module.js
  14. 22
      src/css_composer/model/CssRule.js
  15. 4
      src/css_composer/model/CssRules.js
  16. 3
      src/dom_components/index.js
  17. 29
      src/dom_components/model/Component.js
  18. 86
      src/dom_components/model/ComponentLink.js
  19. 48
      src/dom_components/model/ComponentWrapper.js
  20. 7
      src/dom_components/view/ComponentTextView.js
  21. 7
      src/dom_components/view/ComponentWrapperView.js
  22. 11
      src/editor/config/config.js
  23. 23
      src/editor/index.js
  24. 12
      src/editor/model/Editor.js
  25. 6
      src/parser/model/ParserHtml.js
  26. 14
      src/rich_text_editor/model/RichTextEditor.js
  27. 8
      src/selector_manager/view/ClassTagsView.js
  28. 16
      src/storage_manager/config/config.js
  29. 1
      src/storage_manager/index.js
  30. 17
      src/style_manager/index.js
  31. 6
      src/styles/scss/_gjs_canvas.scss
  32. 2
      src/styles/scss/_gjs_rte.scss
  33. 99
      src/trait_manager/index.js
  34. 147
      src/trait_manager/model/Trait.js
  35. 23
      src/trait_manager/model/Traits.js
  36. 74
      test/specs/code_manager/model/CodeModels.js
  37. 4
      test/specs/dom_components/index.js
  38. 4
      test/specs/dom_components/model/Component.js
  39. 21
      test/specs/grapesjs/headless.js
  40. 95
      test/specs/grapesjs/index.js
  41. 222
      test/specs/parser/model/ParserHtml.js
  42. 29
      webpack.config.js
  43. 2250
      yarn.lock

2
CONTRIBUTING.md

@ -18,7 +18,7 @@ Working on your first Pull Request? You can learn how from this **free** series,
## Submitting code ## Submitting code
Any code change should be submitted as a pull request. Before start working on something make always a search in opened issues and pull requests, this might help you to avoid waisting time. Any code change should be submitted as a pull request. Before start working on something make always a search in opened issues and pull requests, this might help you to avoid wasting time.
A pull request could be a bug fix, new feature and much more, but in all cases, **open a new issue** and talk about what you want to do. Often happens to work on something already fixed (ready to release) or in progress. A pull request could be a bug fix, new feature and much more, but in all cases, **open a new issue** and talk about what you want to do. Often happens to work on something already fixed (ready to release) or in progress.

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

4
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

1
docs/api/storage_manager.md

@ -44,7 +44,6 @@ storageManager.add(...);
* [setAutosave][4] * [setAutosave][4]
* [getStepsBeforeSave][5] * [getStepsBeforeSave][5]
* [setStepsBeforeSave][6] * [setStepsBeforeSave][6]
* [setStepsBeforeSave][6]
* [getStorages][7] * [getStorages][7]
* [getCurrent][8] * [getCurrent][8]
* [getCurrentStorage][9] * [getCurrentStorage][9]

5288
index.d.ts

File diff suppressed because it is too large

4
package.json

@ -1,7 +1,7 @@
{ {
"name": "grapesjs", "name": "grapesjs",
"description": "Free and Open Source Web Builder Framework", "description": "Free and Open Source Web Builder Framework",
"version": "0.18.3", "version": "0.18.4",
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",
@ -28,7 +28,7 @@
"@vuepress/plugin-google-analytics": "^1.8.2", "@vuepress/plugin-google-analytics": "^1.8.2",
"documentation": "^13.2.5", "documentation": "^13.2.5",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"grapesjs-cli": "^2.0.0", "grapesjs-cli": "^3.0.0",
"husky": "^2.7.0", "husky": "^2.7.0",
"jest": "^24.9.0", "jest": "^24.9.0",
"lint-staged": "^8.2.1", "lint-staged": "^8.2.1",

6
src/code_manager/model/CssGenerator.js

@ -27,17 +27,13 @@ export default Backbone.Model.extend({
const avoidInline = em && em.getConfig('avoidInlineStyle'); const avoidInline = em && em.getConfig('avoidInlineStyle');
const style = model.styleToString(); const style = model.styleToString();
const classes = model.get('classes'); const classes = model.get('classes');
const wrapperIsBody = opts.wrapperIsBody;
const isWrapper = model.get('wrapper');
this.ids.push(`#${model.getId()}`); this.ids.push(`#${model.getId()}`);
// Let's know what classes I've found // Let's know what classes I've found
classes.each(model => this.compCls.push(model.getFullName())); classes.each(model => this.compCls.push(model.getFullName()));
if (!avoidInline && style) { if (!avoidInline && style) {
let selector = `#${model.getId()}`; code = `#${model.getId()}{${style}}`;
selector = wrapperIsBody && isWrapper ? 'body' : selector;
code = `${selector}{${style}}`;
} }
const components = model.components(); const components = model.components();

16
src/code_manager/model/HtmlGenerator.js

@ -2,7 +2,6 @@ import { Model } from 'backbone';
export default class HTMLGenerator extends Model { export default class HTMLGenerator extends Model {
build(model, opts = {}) { build(model, opts = {}) {
const models = model.components();
const htmlOpts = {}; const htmlOpts = {};
const { em } = opts; const { em } = opts;
@ -33,19 +32,6 @@ export default class HTMLGenerator extends Model {
}; };
} }
if (opts.exportWrapper) { return model.toHTML(htmlOpts);
return model.toHTML({
...htmlOpts,
...(opts.wrapperIsBody && model.is('wrapper') && { tag: 'body' })
});
}
return this.buildModels(models, htmlOpts);
}
buildModels(models, opts = {}) {
let code = '';
models.forEach(mod => (code += mod.toHTML(opts)));
return code;
} }
} }

18
src/commands/view/ComponentDelete.js

@ -1,27 +1,23 @@
import { isArray } from 'underscore'; import { isArray } from 'underscore';
export default { export default {
run(ed, sender, opts = {}) { run(ed, s, opts = {}) {
const toSelect = []; const toSelect = [];
let components = opts.component || ed.getSelectedAll(); let components = opts.component || ed.getSelectedAll();
components = isArray(components) ? [...components] : [components]; components = isArray(components) ? [...components] : [components];
// It's important to deselect components first otherwise, components.filter(Boolean).forEach(component => {
// with undo, the component will be set with the wrong `collection` if (!component.get('removable')) {
ed.select(null); toSelect.push(component);
components.forEach(component => {
if (!component || !component.get('removable')) {
return this.em.logWarning('The element is not removable', { return this.em.logWarning('The element is not removable', {
component component,
}); });
} }
component.remove(); component.remove();
component.collection && toSelect.push(component);
}); });
toSelect.length && ed.select(toSelect); ed.select(toSelect);
return components; return components;
} },
}; };

3
src/commands/view/SelectComponent.js

@ -89,7 +89,8 @@ export default {
.getFrames() .getFrames()
.forEach(frame => { .forEach(frame => {
const { view } = frame; const { view } = frame;
view && trigger(view.getWindow(), view.getBody()); const win = view?.getWindow();
win && trigger(win, view.getBody());
}); });
}, },

1
src/common/index.js

@ -1,3 +1,4 @@
export { default as Model } from './Model'; export { default as Model } from './Model';
export { default as Collection } from './Collection'; export { default as Collection } from './Collection';
export { default as View } from './View'; export { default as View } from './View';
export { default as Module } from './module';

4
src/common/module.js

@ -28,6 +28,7 @@ export default {
...conf, ...conf,
}; };
this.em = this.config.em; this.em = this.config.em;
this.cls = [];
}, },
__initListen(opts = {}) { __initListen(opts = {}) {
@ -116,5 +117,8 @@ export default {
coll.reset(); coll.reset();
}); });
this.em = 0; this.em = 0;
this.config = 0;
this.view?.remove();
this.view = 0;
}, },
}; };

22
src/css_composer/model/CssRule.js

@ -36,7 +36,9 @@ export default class CssRule extends Model.extend(Styleable) {
singleAtRule: false, singleAtRule: false,
important: false, important: false,
group: '', group: '',
_undo: true // If true, won't be stored in JSON or showed in CSS
shallow: false,
_undo: true,
}; };
} }
@ -115,15 +117,11 @@ export default class CssRule extends Model.extend(Styleable) {
selectorsToString(opts = {}) { selectorsToString(opts = {}) {
const result = []; const result = [];
const state = this.get('state'); const state = this.get('state');
const wrapper = this.get('wrapper');
const addSelector = this.get('selectorsAdd'); const addSelector = this.get('selectorsAdd');
const isBody = wrapper && opts.body;
const selOpts = { const selOpts = {
escape: str => (CSS && CSS.escape ? CSS.escape(str) : str) escape: str => (CSS && CSS.escape ? CSS.escape(str) : str),
}; };
const selectors = isBody const selectors = this.get('selectors').getFullString(0, selOpts);
? 'body'
: this.get('selectors').getFullString(0, selOpts);
const stateStr = state && !opts.skipState ? `:${state}` : ''; const stateStr = state && !opts.skipState ? `:${state}` : '';
selectors && result.push(`${selectors}${stateStr}`); selectors && result.push(`${selectors}${stateStr}`);
addSelector && !opts.skipAdd && result.push(addSelector); addSelector && !opts.skipAdd && result.push(addSelector);
@ -169,10 +167,7 @@ export default class CssRule extends Model.extend(Styleable) {
if (atRuleType !== 'media' || !mediaText) { if (atRuleType !== 'media' || !mediaText) {
return deviceDefault || null; return deviceDefault || null;
} }
return ( return devices.filter(d => d.getWidthMedia() === getMediaLength(mediaText))[0] || null;
devices.filter(d => d.getWidthMedia() === getMediaLength(mediaText))[0] ||
null
);
} }
/** /**
@ -262,10 +257,7 @@ export default class CssRule extends Model.extend(Styleable) {
const wd = width || ''; const wd = width || '';
const selAdd = ruleProps.selectorsAdd || ''; const selAdd = ruleProps.selectorsAdd || '';
let atRule = ruleProps.atRuleType || ''; let atRule = ruleProps.atRuleType || '';
const sel = const sel = !isArray(selectors) && !selectors.models ? [selectors] : selectors.models || selectors;
!isArray(selectors) && !selectors.models
? [selectors]
: selectors.models || selectors;
// Fix atRuleType in case is not specified with width // Fix atRuleType in case is not specified with width
if (wd && !atRule) atRule = 'media'; if (wd && !atRule) atRule = 'media';

4
src/css_composer/model/CssRules.js

@ -19,7 +19,7 @@ export default Collection.extend({
toJSON(opts) { toJSON(opts) {
const result = Collection.prototype.toJSON.call(this, opts); const result = Collection.prototype.toJSON.call(this, opts);
return result.filter(i => i.style); return result.filter(rule => rule.style && !rule.shallow);
}, },
onAdd(model, c, o) { onAdd(model, c, o) {
@ -38,5 +38,5 @@ export default Collection.extend({
} }
opt.em = this.editor; opt.em = this.editor;
return Collection.prototype.add.apply(this, [models, opt]); return Collection.prototype.add.apply(this, [models, opt]);
} },
}); });

3
src/dom_components/index.js

@ -58,6 +58,7 @@ import defaults from './config/config';
import Component, { keyUpdate, keyUpdateInside } from './model/Component'; import Component, { keyUpdate, keyUpdateInside } from './model/Component';
import Components from './model/Components'; import Components from './model/Components';
import ComponentView from './view/ComponentView'; import ComponentView from './view/ComponentView';
import ComponentWrapperView from './view/ComponentWrapperView';
import ComponentsView from './view/ComponentsView'; import ComponentsView from './view/ComponentsView';
import ComponentTableCell from './model/ComponentTableCell'; import ComponentTableCell from './model/ComponentTableCell';
import ComponentTableCellView from './view/ComponentTableCellView'; import ComponentTableCellView from './view/ComponentTableCellView';
@ -196,7 +197,7 @@ export default () => {
{ {
id: 'wrapper', id: 'wrapper',
model: ComponentWrapper, model: ComponentWrapper,
view: ComponentView, view: ComponentWrapperView,
}, },
{ {
id: 'default', id: 'default',

29
src/dom_components/model/Component.js

@ -1064,36 +1064,29 @@ export default class Component extends Model.extend(Styleable) {
const model = this; const model = this;
const ppfx = (em && em.getConfig('stylePrefix')) || ''; const ppfx = (em && em.getConfig('stylePrefix')) || '';
if (!model.get('toolbar')) { if (!model.get('toolbar') && em) {
var tb = []; const tb = [];
if (model.collection) { model.collection &&
tb.push({ tb.push({
attributes: { class: 'fa fa-arrow-up' }, label: em.getIcon('arrowUp'),
command: ed => ed.runCommand('core:component-exit', { force: 1 }), command: ed => ed.runCommand('core:component-exit', { force: 1 }),
}); });
} model.get('draggable') &&
if (model.get('draggable')) {
tb.push({ tb.push({
attributes: { attributes: { class: `${ppfx}no-touch-actions`, draggable: true },
class: `fa fa-arrows ${ppfx}no-touch-actions`, label: em.getIcon('move'),
draggable: true,
},
//events: hasDnd(this.em) ? { dragstart: 'execCommand' } : '',
command: 'tlb-move', command: 'tlb-move',
}); });
} model.get('copyable') &&
if (model.get('copyable')) {
tb.push({ tb.push({
attributes: { class: 'fa fa-clone' }, label: em.getIcon('copy'),
command: 'tlb-clone', command: 'tlb-clone',
}); });
} model.get('removable') &&
if (model.get('removable')) {
tb.push({ tb.push({
attributes: { class: 'fa fa-trash-o' }, label: em.getIcon('delete'),
command: 'tlb-delete', command: 'tlb-delete',
}); });
}
model.set('toolbar', tb); model.set('toolbar', tb);
} }
} }

86
src/dom_components/model/ComponentLink.js

@ -1,53 +1,41 @@
import Component from './ComponentText'; import { forEach } from 'underscore';
import { toLowerCase } from 'utils/mixins'; import { toLowerCase } from 'utils/mixins';
import ComponentText from './ComponentText';
export default Component.extend( const type = 'link';
{
defaults: { export default class ComponentLink extends ComponentText {}
...Component.prototype.defaults,
type: 'link', ComponentLink.prototype.defaults = {
tagName: 'a', ...ComponentText.getDefaults(),
traits: ['title', 'href', 'target'] type,
}, tagName: 'a',
traits: ['title', 'href', 'target'],
/** };
* Returns object of attributes for HTML
* @return {Object} ComponentLink.isComponent = (el, opts = {}) => {
* @private let result;
*/
getAttrToHTML(...args) {
const attr = Component.prototype.getAttrToHTML.apply(this, args);
delete attr.onmousedown;
return attr;
}
},
{
isComponent(el) {
let result;
if (toLowerCase(el.tagName) === 'a') {
result = {
type: 'link',
editable: 0
};
// The link is editable only if, at least, one of its
// children is a text node (not empty one)
const children = el.childNodes;
const len = children.length;
if (!len) delete result.editable;
for (let i = 0; i < len; i++) {
const child = children[i];
if (child.nodeType == 3 && child.textContent.trim() != '') {
delete result.editable;
break;
}
}
}
return result; if (toLowerCase(el.tagName) === 'a') {
} const textTags = opts.textTags || [];
result = { type, editable: false };
// The link is editable only if, at least, one of its
// children is a text node (not empty one)
const children = el.childNodes;
const len = children.length;
if (!len) delete result.editable;
forEach(children, child => {
const { tagName } = child;
if (
(child.nodeType == 3 && child.textContent.trim() !== '') ||
(tagName && textTags.indexOf(toLowerCase(tagName)) >= 0)
) {
delete result.editable;
}
});
} }
);
return result;
};

48
src/dom_components/model/ComponentWrapper.js

@ -1,11 +1,10 @@
// We need this one just to identify better the wrapper type
import Component from './Component'; import Component from './Component';
export default Component.extend( export default class ComponentWrapper extends Component {
{ defaults() {
defaults: { return {
...Component.prototype.defaults, ...Component.getDefaults(),
__wrapper: 1, tagName: 'body',
removable: false, removable: false,
copyable: false, copyable: false,
draggable: false, draggable: false,
@ -18,23 +17,22 @@ export default Component.extend(
'background-repeat', 'background-repeat',
'background-attachment', 'background-attachment',
'background-position', 'background-position',
'background-size' 'background-size',
] ],
}, };
__postAdd() {
const um = this.em && this.em.get('UndoManager');
um && !this.__hasUm && um.add(this);
return Component.prototype.__postAdd.call(this, arguments);
},
__postRemove() {
const um = this.em && this.em.get('UndoManager');
um && um.remove(this);
return Component.prototype.__postRemove.call(this, arguments);
}
},
{
isComponent() {
return false;
}
} }
);
__postAdd() {
const um = this.em && this.em.get('UndoManager');
um && !this.__hasUm && um.add(this);
return Component.prototype.__postAdd.call(this, arguments);
}
__postRemove() {
const um = this.em && this.em.get('UndoManager');
um && um.remove(this);
return Component.prototype.__postRemove.call(this, arguments);
}
}
ComponentWrapper.isComponent = () => false;

7
src/dom_components/view/ComponentTextView.js

@ -43,7 +43,11 @@ export default ComponentView.extend({
parent = parent.parent(); parent = parent.parent();
} }
delegate = parent; if (parent && parent.get('editable')) {
delegate = parent;
} else {
result = true;
}
} }
} }
@ -62,6 +66,7 @@ export default ComponentView.extend({
// text components will not block the editing (#1394) // text components will not block the editing (#1394)
if (!result) { if (!result) {
if (delegate) { if (delegate) {
ev?.stopPropagation?.();
em.setSelected(delegate); em.setSelected(delegate);
delegate.trigger('active', ev); delegate.trigger('active', ev);
} }

7
src/dom_components/view/ComponentWrapperView.js

@ -0,0 +1,7 @@
import ComponentView from './ComponentView';
export default class ComponentWrapperView extends ComponentView {
tagName() {
return 'div';
}
}

11
src/editor/config/config.js

@ -114,12 +114,6 @@ export default {
// Enable multiple selection // Enable multiple selection
multipleSelection: 1, multipleSelection: 1,
// Show the wrapper component in the final code, eg. in editor.getHtml()
exportWrapper: 0,
// The wrapper, if visible, will be shown as a `<body>`
wrapperIsBody: 1,
// Pass default available options wherever `editor.getHtml()` is called // Pass default available options wherever `editor.getHtml()` is called
optsHtml: {}, optsHtml: {},
@ -173,6 +167,11 @@ export default {
move: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M13,6V11H18V7.75L22.25,12L18,16.25V13H13V18H16.25L12,22.25L7.75,18H11V13H6V16.25L1.75,12L6,7.75V11H11V6H7.75L12,1.75L16.25,6H13Z"/></svg>', move: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M13,6V11H18V7.75L22.25,12L18,16.25V13H13V18H16.25L12,22.25L7.75,18H11V13H6V16.25L1.75,12L6,7.75V11H11V6H7.75L12,1.75L16.25,6H13Z"/></svg>',
plus: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" /></svg>', plus: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" /></svg>',
caret: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M7,10L12,15L17,10H7Z" /></svg>', caret: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M7,10L12,15L17,10H7Z" /></svg>',
delete:
'<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" /></svg>',
copy: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" /></svg>',
arrowUp:
'<svg viewBox="0 0 24 24"><path fill="currentColor" d="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z" /></svg>',
}, },
// Dom element // Dom element

23
src/editor/index.js

@ -438,6 +438,18 @@ export default (config = {}, opts = {}) => {
return em.store(clb); return em.store(clb);
}, },
/**
* Get the JSON project data, which could be stored and loaded back with `editor.loadProject(json)`
* @returns {Object}
* @example
* @private
* console.log(editor.getProject());
* // { pages: [...], styles: [...], ... }
*/
getProject() {
return em.storeData();
},
/** /**
* Get the JSON data object, which could be stored and loaded back with `editor.loadData(json)` * Get the JSON data object, which could be stored and loaded back with `editor.loadData(json)`
* @returns {Object} * @returns {Object}
@ -458,6 +470,17 @@ export default (config = {}, opts = {}) => {
return em.load(clb); return em.load(clb);
}, },
/**
* Load data from the JSON project
* @param {Object} data Project to load
* @example
* @private
* editor.loadProject({ pages: [...], styles: [...], ... })
*/
loadProject(data) {
return em.loadData(data);
},
/** /**
* Load data from the JSON data object * Load data from the JSON data object
* @param {Object} data Data to load * @param {Object} data Data to load

12
src/editor/model/Editor.js

@ -557,13 +557,11 @@ export default class EditorModel extends Model {
*/ */
getHtml(opts = {}) { getHtml(opts = {}) {
const { config } = this; const { config } = this;
const { optsHtml, exportWrapper, wrapperIsBody } = config; const { optsHtml } = config;
const js = config.jsInHtml ? this.getJs(opts) : ''; const js = config.jsInHtml ? this.getJs(opts) : '';
const cmp = opts.component || this.get('DomComponents').getComponent(); const cmp = opts.component || this.get('DomComponents').getComponent();
let html = cmp let html = cmp
? this.get('CodeManager').getCode(cmp, 'html', { ? this.get('CodeManager').getCode(cmp, 'html', {
exportWrapper,
wrapperIsBody,
...optsHtml, ...optsHtml,
...opts, ...opts,
}) })
@ -580,7 +578,7 @@ export default class EditorModel extends Model {
*/ */
getCss(opts = {}) { getCss(opts = {}) {
const config = this.config; const config = this.config;
const { optsCss, wrapperIsBody } = config; const { optsCss } = config;
const avoidProt = opts.avoidProtected; const avoidProt = opts.avoidProtected;
const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) ? opts.keepUnusedStyles : config.keepUnusedStyles; const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) ? opts.keepUnusedStyles : config.keepUnusedStyles;
const cssc = this.get('CssComposer'); const cssc = this.get('CssComposer');
@ -590,7 +588,6 @@ export default class EditorModel extends Model {
wrp && wrp &&
this.get('CodeManager').getCode(wrp, 'css', { this.get('CodeManager').getCode(wrp, 'css', {
cssc, cssc,
wrapperIsBody,
keepUnusedStyles, keepUnusedStyles,
...optsCss, ...optsCss,
...opts, ...opts,
@ -782,6 +779,11 @@ export default class EditorModel extends Model {
return (this.getCurrentFrame() || {}).model; return (this.getCurrentFrame() || {}).model;
} }
getIcon(icon) {
const icons = this.getConfig('icons') || {};
return icons[icon] || '';
}
/** /**
* Return the count of changes made to the content and not yet stored. * Return the count of changes made to the content and not yet stored.
* This count resets at any `store()` * This count resets at any `store()`

6
src/parser/model/ParserHtml.js

@ -140,7 +140,9 @@ export default config => {
// Set tag name if not yet done // Set tag name if not yet done
if (!model.tagName) { if (!model.tagName) {
model.tagName = node.tagName ? node.tagName.toLowerCase() : ''; const tag = node.tagName || '';
const ns = node.namespaceURI || '';
model.tagName = tag && ns === 'http://www.w3.org/1999/xhtml' ? tag.toLowerCase() : tag;
} }
if (attrsLen) { if (attrsLen) {
@ -316,7 +318,7 @@ export default config => {
} }
em && em.trigger(`${event}:root`, { input: str, root: el }); em && em.trigger(`${event}:root`, { input: str, root: el });
const result = this.parseNode(el); const result = this.parseNode(el, cf);
// I have to keep it otherwise it breaks the DomComponents.addComponent (returns always array) // I have to keep it otherwise it breaks the DomComponents.addComponent (returns always array)
const resHtml = result.length === 1 && !c.returnArray ? result[0] : result; const resHtml = result.length === 1 && !c.returnArray ? result[0] : result;
res.html = resHtml; res.html = resHtml;

14
src/rich_text_editor/model/RichTextEditor.js

@ -92,6 +92,7 @@ export default class RichTextEditor {
this.setEl(el); this.setEl(el);
this.updateActiveActions = this.updateActiveActions.bind(this); this.updateActiveActions = this.updateActiveActions.bind(this);
this.__onKeydown = this.__onKeydown.bind(this); this.__onKeydown = this.__onKeydown.bind(this);
this.__onPaste = this.__onPaste.bind(this);
const acts = (settings.actions || []).map(action => { const acts = (settings.actions || []).map(action => {
let result = action; let result = action;
@ -201,6 +202,7 @@ export default class RichTextEditor {
el.contentEditable = !!enable; el.contentEditable = !!enable;
method(el, 'mouseup keyup', this.updateActiveActions); method(el, 'mouseup keyup', this.updateActiveActions);
method(doc, 'keydown', this.__onKeydown); method(doc, 'keydown', this.__onKeydown);
method(doc, 'paste', this.__onPaste);
this.enabled = enable; this.enabled = enable;
if (enable) { if (enable) {
@ -237,6 +239,18 @@ export default class RichTextEditor {
} }
} }
__onPaste(ev) {
const clipboardData = ev.clipboardData || window.clipboardData;
const text = clipboardData.getData('text');
const textHtml = clipboardData.getData('text/html');
// Replace \n with <br> in case of plain text
if (text && !textHtml) {
ev.preventDefault();
const html = text.replace(/(?:\r\n|\r|\n)/g, '<br/>');
this.doc.execCommand('insertHTML', false, html);
}
}
/** /**
* Sync actions with the current RTE * Sync actions with the current RTE
*/ */

8
src/selector_manager/view/ClassTagsView.js

@ -154,8 +154,12 @@ export default Backbone.View.extend({
* @private * @private
*/ */
onInputKeyUp(e) { onInputKeyUp(e) {
if (e.keyCode === 13) this.addNewTag(this.$input.val()); if (e.keyCode === 13) {
else if (e.keyCode === 27) this.endNewTag(); e.preventDefault();
this.addNewTag(this.$input.val());
} else if (e.keyCode === 27) {
this.endNewTag();
}
}, },
checkStates() { checkStates() {

16
src/storage_manager/config/config.js

@ -3,10 +3,10 @@ export default {
id: 'gjs-', id: 'gjs-',
// Enable/Disable autosaving // Enable/Disable autosaving
autosave: 1, autosave: true,
// Indicates if load data inside editor after init // Indicates if load data inside editor after init
autoload: 1, autoload: true,
// Indicates which storage to use. Available: local | remote // Indicates which storage to use. Available: local | remote
type: 'local', type: 'local',
@ -16,20 +16,20 @@ export default {
stepsBeforeSave: 1, stepsBeforeSave: 1,
//Enable/Disable components model (JSON format) //Enable/Disable components model (JSON format)
storeComponents: 1, storeComponents: true,
//Enable/Disable styles model (JSON format) //Enable/Disable styles model (JSON format)
storeStyles: 1, storeStyles: true,
//Enable/Disable saving HTML template //Enable/Disable saving HTML template
storeHtml: 1, storeHtml: true,
//Enable/Disable saving CSS template //Enable/Disable saving CSS template
storeCss: 1, storeCss: true,
// ONLY FOR LOCAL STORAGE // ONLY FOR LOCAL STORAGE
// If enabled, checks if browser supports Local Storage // If enabled, checks if browser supports Local Storage
checkLocal: 1, checkLocal: true,
// ONLY FOR REMOTE STORAGE // ONLY FOR REMOTE STORAGE
// Custom parameters to pass with the remote storage request, eg. csrf token // Custom parameters to pass with the remote storage request, eg. csrf token
@ -63,5 +63,5 @@ export default {
// currentOpts => { // currentOpts => {
// return currentOpts.method === 'post' ? { method: 'patch' } : {}; // return currentOpts.method === 'post' ? { method: 'patch' } : {};
// } // }
fetchOptions: '' fetchOptions: '',
}; };

1
src/storage_manager/index.js

@ -38,7 +38,6 @@
* * [setAutosave](#setautosave) * * [setAutosave](#setautosave)
* * [getStepsBeforeSave](#getstepsbeforesave) * * [getStepsBeforeSave](#getstepsbeforesave)
* * [setStepsBeforeSave](#setstepsbeforesave) * * [setStepsBeforeSave](#setstepsbeforesave)
* * [setStepsBeforeSave](#setstepsbeforesave)
* * [getStorages](#getstorages) * * [getStorages](#getstorages)
* * [getCurrent](#getcurrent) * * [getCurrent](#getcurrent)
* * [getCurrentStorage](#getcurrentstorage) * * [getCurrentStorage](#getcurrentstorage)

17
src/style_manager/index.js

@ -63,7 +63,7 @@
* @module StyleManager * @module StyleManager
*/ */
import { isElement, isUndefined, isArray, isString, debounce } from 'underscore'; import { isUndefined, isArray, isString, debounce, bindAll } from 'underscore';
import { isComponent } from 'utils/mixins'; import { isComponent } from 'utils/mixins';
import Module from 'common/module'; import Module from 'common/module';
import { Model } from 'common'; import { Model } from 'common';
@ -127,6 +127,7 @@ export default () => {
* @private * @private
*/ */
init(config = {}) { init(config = {}) {
bindAll(this, '__clearStateTarget');
this.__initConfig(defaults, config); this.__initConfig(defaults, config);
const c = this.config; const c = this.config;
const { em } = c; const { em } = c;
@ -145,6 +146,8 @@ export default () => {
const ev = 'component:toggled component:update:classes change:state change:device frame:resized selector:type'; const ev = 'component:toggled component:update:classes change:state change:device frame:resized selector:type';
const upAll = debounce(() => this.__upSel()); const upAll = debounce(() => this.__upSel());
model.listenTo(em, ev, upAll); model.listenTo(em, ev, upAll);
// Clear state target on any component selection change, without debounce (#4208)
model.listenTo(em, 'component:toggled', this.__clearStateTarget);
// Triggers only for properties (avoid selection refresh) // Triggers only for properties (avoid selection refresh)
const upProps = debounce(() => { const upProps = debounce(() => {
@ -176,6 +179,16 @@ export default () => {
this.em.trigger(event, ...data); this.em.trigger(event, ...data);
}, },
__clearStateTarget() {
const { em } = this;
const stateTarget = this.__getStateTarget();
stateTarget &&
em?.skip(() => {
em.get('CssComposer').remove(stateTarget);
this.model.set({ stateTarget: null });
});
},
onLoad() { onLoad() {
// Use silent as sectors' view will be created and rendered on StyleManager.render // Use silent as sectors' view will be created and rendered on StyleManager.render
sectors.add(this.config.sectors, { silent: true }); sectors.add(this.config.sectors, { silent: true });
@ -367,7 +380,7 @@ export default () => {
if (state && lastTarget?.getState?.()) { if (state && lastTarget?.getState?.()) {
const style = lastTarget.getStyle(); const style = lastTarget.getStyle();
if (!stateTarget) { if (!stateTarget) {
stateTarget = cssc.getAll().add({ selectors: 'gjs-selected', style, important: true }); stateTarget = cssc.getAll().add({ selectors: 'gjs-selected', style, shallow: true, important: true });
} else { } else {
stateTarget.setStyle(style); stateTarget.setStyle(style);
} }

6
src/styles/scss/_gjs_canvas.scss

@ -304,11 +304,9 @@ $guide_pad: 5px !default;
} }
.#{$app-prefix}toolbar-item { .#{$app-prefix}toolbar-item {
padding: 5px 7px;
font-size: 0.8rem;
cursor: pointer;
width: 26px; width: 26px;
vertical-align: middle; padding: 5px;
cursor: pointer;
display: inline-block; display: inline-block;
svg { svg {

2
src/styles/scss/_gjs_rte.scss

@ -17,7 +17,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 5px; padding: 5px;
min-width: 25px; width: 25px;
border-right: 1px solid $mainDkColor; border-right: 1px solid $mainDkColor;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;

99
src/trait_manager/index.js

@ -1,5 +1,5 @@
import { defaults, isElement } from 'underscore'; import { debounce } from 'underscore';
import defaultOpts from './config/config'; import defaults from './config/config';
import TraitsView from './view/TraitsView'; import TraitsView from './view/TraitsView';
import TraitView from './view/TraitView'; import TraitView from './view/TraitView';
import TraitSelectView from './view/TraitSelectView'; import TraitSelectView from './view/TraitSelectView';
@ -7,22 +7,32 @@ import TraitCheckboxView from './view/TraitCheckboxView';
import TraitNumberView from './view/TraitNumberView'; import TraitNumberView from './view/TraitNumberView';
import TraitColorView from './view/TraitColorView'; import TraitColorView from './view/TraitColorView';
import TraitButtonView from './view/TraitButtonView'; import TraitButtonView from './view/TraitButtonView';
import { Model, Module } from 'common';
export const evAll = 'trait';
export const evPfx = `${evAll}:`;
export const evCustom = `${evPfx}custom`;
export default () => { export default () => {
let c = {};
let TraitsViewer;
const typesDef = { const typesDef = {
text: TraitView, text: TraitView,
number: TraitNumberView, number: TraitNumberView,
select: TraitSelectView, select: TraitSelectView,
checkbox: TraitCheckboxView, checkbox: TraitCheckboxView,
color: TraitColorView, color: TraitColorView,
button: TraitButtonView button: TraitButtonView,
}; };
return { return {
...Module,
TraitsView, TraitsView,
events: {
all: evAll,
custom: evCustom,
},
/** /**
* Name of the module * Name of the module
* @type {String} * @type {String}
@ -32,33 +42,64 @@ export default () => {
/** /**
* Get configuration object * Get configuration object
* @name getConfig
* @function
* @return {Object} * @return {Object}
* @private
*/ */
getConfig() {
return c;
},
/** /**
* Initialize module. Automatically called with a new instance of the editor * Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations * @param {Object} config Configurations
* @private
*/ */
init(config = {}) { init(config = {}) {
c = config; this.__initConfig(defaults, config);
defaults(c, defaultOpts); const c = this.config;
const model = new Model();
this.model = model;
const { em } = this;
const ppfx = c.pStylePrefix; const ppfx = c.pStylePrefix;
this.types = { ...typesDef }; this.types = { ...typesDef };
ppfx && (c.stylePrefix = `${ppfx}${c.stylePrefix}`); ppfx && (c.stylePrefix = `${ppfx}${c.stylePrefix}`);
const upAll = debounce(() => this.__upSel());
model.listenTo(em, 'component:toggled', upAll);
const update = debounce(() => this.__onUp());
model.listenTo(em, 'trait:update', update);
return this; return this;
}, },
postRender() { __upSel() {
const elTo = this.getConfig().appendTo; this.select(this.em.getSelected());
},
if (elTo) { __onUp() {
const el = isElement(elTo) ? elTo : document.querySelector(elTo); this.select(this.getSelected());
el.appendChild(this.render()); },
}
select(component) {
const traits = component ? component.getTraits() : [];
this.model.set({ component, traits });
this.__trgCustom();
},
getSelected() {
return this.model.get('component') || null;
},
getCurrent() {
return this.model.get('traits') || [];
},
__trgCustom(opts = {}) {
this.__ctn = this.__ctn || opts.container;
this.em.trigger(this.events.custom, { container: this.__ctn });
},
postRender() {
this.__appendTo();
}, },
/** /**
@ -67,7 +108,7 @@ export default () => {
* @private * @private
*/ */
getTraitsViewer() { getTraitsViewer() {
return TraitsViewer; return this.view;
}, },
/** /**
@ -98,21 +139,23 @@ export default () => {
}, },
render() { render() {
const el = TraitsViewer && TraitsViewer.el; let { view } = this;
TraitsViewer = new TraitsView({ const config = this.getConfig();
const el = view && view.el;
view = new TraitsView({
el, el,
collection: [], collection: [],
editor: c.em, editor: config.em,
config: c config,
}); });
TraitsViewer.itemsView = this.getTypes(); view.itemsView = this.getTypes();
TraitsViewer.updatedCollection(); view.updatedCollection();
return TraitsViewer.el; this.view = view;
return view.el;
}, },
destroy() { destroy() {
TraitsViewer && TraitsViewer.remove(); this.__destroy();
[c, TraitsViewer].forEach(i => (i = {})); },
}
}; };
}; };

147
src/trait_manager/model/Trait.js

@ -1,55 +1,108 @@
import Backbone from 'backbone'; import { Model } from 'common';
import { isUndefined } from 'underscore'; import { isUndefined } from 'underscore';
export default Backbone.Model.extend({ /**
defaults: { * @typedef Trait
type: 'text', // text, number, range, select * @property {String} id Trait id, eg. `my-trait-id`.
label: '', * @property {String} type Trait type, defines how the trait should rendered. Possible values: `text` (default), `number`, `select`, `checkbox`, `color`, `button`
name: '', * @property {String} label The trait label to show for the rendered trait.
min: '', * @property {String} name The name of the trait used as a key for the attribute/property. By default, the name is used as attribute name or property in case `changeProp` in enabled.
max: '', * @property {Boolean} changeProp If `true` the trait value is applied on component
unit: '', *
step: 1, */
value: '', export default class Trait extends Model {
target: '',
default: '',
placeholder: '',
changeProp: 0,
options: []
},
initialize() { initialize() {
const target = this.get('target'); const { target, name, changeProp } = this.attributes;
const name = this.get('name');
const changeProp = this.get('changeProp');
!this.get('id') && this.set('id', name); !this.get('id') && this.set('id', name);
if (target) { if (target) {
this.target = target; this.target = target;
this.unset('target'); this.unset('target');
const targetEvent = changeProp const targetEvent = changeProp ? `change:${name}` : `change:attributes:${name}`;
? `change:${name}`
: `change:attributes:${name}`;
this.listenTo(target, targetEvent, this.targetUpdated); this.listenTo(target, targetEvent, this.targetUpdated);
} }
}, }
/**
* Get the trait id.
* @returns {String}
*/
getId() {
return this.get('id');
}
/**
* Get the trait type.
* @returns {String}
*/
getType() {
return this.get('type');
}
/**
* Get the trait name.
* @returns {String}
*/
getName() {
return this.get('name');
}
/**
* Get the trait label.
* @param {Object} [opts={}] Options.
* @param {Boolean} [opts.locale=true] Use the locale string from i18n module.
* @returns {String}
*/
getLabel(opts = {}) {
const { locale = true } = opts;
const id = this.getId();
const name = this.get('label') || this.getName();
return (locale && this.em?.t(`traitManager.traits.labels.${id}`)) || name;
}
/**
* Get the trait value.
* The value is taken from component attributes by default or from properties if the trait has the `changeProp` enabled.
* @returns {any}
*/
getValue() {
return this.getTargetValue();
}
/** /**
* Return all the propeties * Update the trait value.
* @returns {Object} * The value is applied on component attributes by default or on properties if the trait has the `changeProp` enabled.
* @param {any} value Value of the trait.
* @param {Object} [opts={}] Options.
* @param {Boolean} [opts.partial] If `true` the update won't be considered complete (not stored in UndoManager).
*/ */
setValue(value, opts = {}) {
const valueOpts = {};
if (opts.partial) {
valueOpts.avoidStore = true;
}
this.setTargetValue(value, valueOpts);
if (opts.partial === false) {
this.setTargetValue('');
this.setTargetValue(value);
}
}
props() { props() {
return this.attributes; return this.attributes;
}, }
targetUpdated() { targetUpdated() {
const value = this.getTargetValue(); const value = this.getTargetValue();
this.set({ value }, { fromTarget: 1 }); this.set({ value }, { fromTarget: 1 });
}, this.em?.trigger('trait:update', {
trait: this,
getValue() { component: this.target,
return this.getTargetValue(); });
}, }
getTargetValue() { getTargetValue() {
const name = this.get('name'); const name = this.get('name');
@ -63,7 +116,7 @@ export default Backbone.Model.extend({
} }
return !isUndefined(value) ? value : ''; return !isUndefined(value) ? value : '';
}, }
setTargetValue(value, opts = {}) { setTargetValue(value, opts = {}) {
const target = this.target; const target = this.target;
@ -84,7 +137,7 @@ export default Backbone.Model.extend({
attrs[name] = valueToSet; attrs[name] = valueToSet;
target.set('attributes', attrs, opts); target.set('attributes', attrs, opts);
} }
}, }
setValueFromInput(value, final = 1, opts = {}) { setValueFromInput(value, final = 1, opts = {}) {
const toSet = { value }; const toSet = { value };
@ -95,12 +148,8 @@ export default Backbone.Model.extend({
this.set('value', '', opts); this.set('value', '', opts);
this.set(toSet, opts); this.set(toSet, opts);
} }
}, }
/**
* Get the initial value of the trait
* @return {string}
*/
getInitValue() { getInitValue() {
const target = this.target; const target = this.target;
const name = this.get('name'); const name = this.get('name');
@ -113,4 +162,20 @@ export default Backbone.Model.extend({
return value || this.get('value') || this.get('default'); return value || this.get('value') || this.get('default');
} }
}); }
Trait.prototype.defaults = {
type: 'text',
label: '',
name: '',
min: '',
max: '',
unit: '',
step: 1,
value: '',
target: '',
default: '',
placeholder: '',
changeProp: 0,
options: [],
};

23
src/trait_manager/model/Traits.js

@ -1,32 +1,31 @@
import Backbone from 'backbone'; import { Collection } from 'common';
import { isString, isArray } from 'underscore'; import { isString, isArray } from 'underscore';
import Trait from './Trait'; import Trait from './Trait';
import TraitFactory from './TraitFactory'; import TraitFactory from './TraitFactory';
export default Backbone.Collection.extend({ export default class Traits extends Collection {
model: Trait,
initialize(coll, options = {}) { initialize(coll, options = {}) {
this.em = options.em || ''; this.em = options.em;
this.listenTo(this, 'add', this.handleAdd); this.listenTo(this, 'add', this.handleAdd);
this.listenTo(this, 'reset', this.handleReset); this.listenTo(this, 'reset', this.handleReset);
}, }
handleReset(coll, { previousModels = [] } = {}) { handleReset(coll, { previousModels = [] } = {}) {
previousModels.forEach(model => model.trigger('remove')); previousModels.forEach(model => model.trigger('remove'));
}, }
handleAdd(model) { handleAdd(model) {
model.em = this.em;
const target = this.target; const target = this.target;
if (target) { if (target) {
model.target = target; model.target = target;
} }
}, }
setTarget(target) { setTarget(target) {
this.target = target; this.target = target;
}, }
add(models, opt) { add(models, opt) {
const em = this.em; const em = this.em;
@ -49,6 +48,8 @@ export default Backbone.Collection.extend({
} }
} }
return Backbone.Collection.prototype.add.apply(this, [models, opt]); return Collection.prototype.add.apply(this, [models, opt]);
} }
}); }
Traits.prototype.model = Trait;

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

@ -20,7 +20,7 @@ describe('HtmlGenerator', () => {
{}, {},
{ {
em, em,
componentTypes: dcomp.componentTypes componentTypes: dcomp.componentTypes,
} }
); );
}); });
@ -30,41 +30,37 @@ describe('HtmlGenerator', () => {
}); });
test('Build correctly one component', () => { test('Build correctly one component', () => {
expect(obj.build(comp)).toEqual(''); expect(obj.build(comp)).toEqual('<div></div>');
}); });
test('Build correctly empty component inside', () => { test('Build correctly empty component inside', () => {
var m1 = comp.get('components').add({}); comp.get('components').add({});
expect(obj.build(comp)).toEqual('<div></div>'); expect(obj.build(comp)).toEqual('<div><div></div></div>');
}); });
test('Build correctly not empty component inside', () => { test('Build correctly not empty component inside', () => {
var m1 = comp.get('components').add({ const m1 = comp.get('components').add({
tagName: 'article', tagName: 'article',
attributes: { attributes: {
'data-test1': 'value1', 'data-test1': 'value1',
'data-test2': 'value2' 'data-test2': 'value2',
} },
}); });
expect(obj.build(comp)).toEqual( expect(obj.build(m1)).toEqual('<article data-test1="value1" data-test2="value2"></article>');
'<article data-test1="value1" data-test2="value2"></article>'
);
}); });
test('Build correctly component with classes', () => { test('Build correctly component with classes', () => {
var m1 = comp.get('components').add({ const m1 = comp.get('components').add({
tagName: 'article', tagName: 'article',
attributes: { attributes: {
'data-test1': 'value1', 'data-test1': 'value1',
'data-test2': 'value2' 'data-test2': 'value2',
} },
}); });
['class1', 'class2'].forEach(item => { ['class1', 'class2'].forEach(item => {
m1.get('classes').add({ name: item }); m1.get('classes').add({ name: item });
}); });
expect(obj.build(comp)).toEqual( expect(obj.build(m1)).toEqual('<article data-test1="value1" data-test2="value2" class="class1 class2"></article>');
'<article data-test1="value1" data-test2="value2" class="class1 class2"></article>'
);
}); });
}); });
@ -82,7 +78,7 @@ describe('CssGenerator', () => {
{}, {},
{ {
em, em,
componentTypes: dcomp.componentTypes componentTypes: dcomp.componentTypes,
} }
); );
}); });
@ -105,12 +101,10 @@ describe('CssGenerator', () => {
tagName: 'article', tagName: 'article',
style: { style: {
prop1: 'value1', prop1: 'value1',
prop2: 'value2' prop2: 'value2',
} },
}); });
expect(obj.build(comp)).toEqual( expect(obj.build(comp)).toEqual('#' + m1.getId() + '{prop1:value1;prop2:value2;}');
'#' + m1.getId() + '{prop1:value1;prop2:value2;}'
);
}); });
test('Build correctly component with class styled', () => { test('Build correctly component with class styled', () => {
@ -121,9 +115,7 @@ describe('CssGenerator', () => {
var rule = cssc.add(cls1); var rule = cssc.add(cls1);
rule.set('style', { prop1: 'value1', prop2: 'value2' }); rule.set('style', { prop1: 'value1', prop2: 'value2' });
expect(obj.build(comp, { cssc })).toEqual( expect(obj.build(comp, { cssc })).toEqual('.class1{prop1:value1;prop2:value2;}');
'.class1{prop1:value1;prop2:value2;}'
);
}); });
test('Build correctly component styled with class and state', () => { test('Build correctly component styled with class and state', () => {
@ -135,9 +127,7 @@ describe('CssGenerator', () => {
rule.set('style', { prop1: 'value1', prop2: 'value2' }); rule.set('style', { prop1: 'value1', prop2: 'value2' });
rule.set('state', 'hover'); rule.set('state', 'hover');
expect(obj.build(comp, { cssc })).toEqual( expect(obj.build(comp, { cssc })).toEqual('.class1:hover{prop1:value1;prop2:value2;}');
'.class1:hover{prop1:value1;prop2:value2;}'
);
}); });
test('Build correctly with more classes', () => { test('Build correctly with more classes', () => {
@ -149,9 +139,7 @@ describe('CssGenerator', () => {
var rule = cssc.add([cls1, cls2]); var rule = cssc.add([cls1, cls2]);
rule.set('style', { prop1: 'value1', prop2: 'value2' }); rule.set('style', { prop1: 'value1', prop2: 'value2' });
expect(obj.build(comp, { cssc })).toEqual( expect(obj.build(comp, { cssc })).toEqual('.class1.class2{prop1:value1;prop2:value2;}');
'.class1.class2{prop1:value1;prop2:value2;}'
);
}); });
test('Build rules with mixed classes', () => { test('Build rules with mixed classes', () => {
@ -175,9 +163,7 @@ describe('CssGenerator', () => {
rule.set('style', { prop1: 'value1', prop2: 'value2' }); rule.set('style', { prop1: 'value1', prop2: 'value2' });
rule.set('selectorsAdd', '.class1 .class2, div > .class4'); rule.set('selectorsAdd', '.class1 .class2, div > .class4');
expect(obj.build(comp, { cssc })).toEqual( expect(obj.build(comp, { cssc })).toEqual('.class1 .class2, div > .class4{prop1:value1;prop2:value2;}');
'.class1 .class2, div > .class4{prop1:value1;prop2:value2;}'
);
}); });
test('Build correctly with class styled out', () => { test('Build correctly with class styled out', () => {
@ -191,9 +177,7 @@ describe('CssGenerator', () => {
var rule2 = cssc.add(cls2); var rule2 = cssc.add(cls2);
rule2.set('style', { prop2: 'value2' }); rule2.set('style', { prop2: 'value2' });
expect(obj.build(comp, { cssc })).toEqual( expect(obj.build(comp, { cssc })).toEqual('.class1.class2{prop1:value1;}.class2{prop2:value2;}');
'.class1.class2{prop1:value1;}.class2{prop2:value2;}'
);
}); });
test('Rule with media query', () => { test('Rule with media query', () => {
@ -206,9 +190,7 @@ describe('CssGenerator', () => {
rule.set('style', { prop1: 'value1' }); rule.set('style', { prop1: 'value1' });
rule.set('mediaText', '(max-width: 999px)'); rule.set('mediaText', '(max-width: 999px)');
expect(obj.build(comp, { cssc })).toEqual( expect(obj.build(comp, { cssc })).toEqual('@media (max-width: 999px){.class1.class2{prop1:value1;}}');
'@media (max-width: 999px){.class1.class2{prop1:value1;}}'
);
}); });
test('Rules mixed with media queries', () => { test('Rules mixed with media queries', () => {
@ -263,7 +245,7 @@ describe('CssGenerator', () => {
{}, {},
{ {
em, em,
componentTypes: dcomp.componentTypes componentTypes: dcomp.componentTypes,
} }
); );
comp.setStyle({ color: 'red' }); comp.setStyle({ color: 'red' });
@ -299,7 +281,7 @@ describe('CssGenerator', () => {
[ [
['@media (max-width: 999px)', 999], ['@media (max-width: 999px)', 999],
['@media (min-width: 123%)', 123], ['@media (min-width: 123%)', 123],
['@media (min-width: 1040rem)', 1040] ['@media (min-width: 1040rem)', 1040],
].forEach(item => { ].forEach(item => {
expect(obj.getQueryLength(item[0])).toBe(item[1]); expect(obj.getQueryLength(item[0])).toBe(item[1]);
}); });
@ -312,14 +294,14 @@ describe('CssGenerator', () => {
'@font-face': 2, '@font-face': 2,
'@media (max-width: 768px)': 3, '@media (max-width: 768px)': 3,
'@media (max-width: 1020ch)': 4, '@media (max-width: 1020ch)': 4,
'@media (max-width: 10%)': 5 '@media (max-width: 10%)': 5,
}) })
).toEqual([ ).toEqual([
{ key: '@font-face', value: 2 }, { key: '@font-face', value: 2 },
{ key: '@media (max-width: 1020ch)', value: 4 }, { key: '@media (max-width: 1020ch)', value: 4 },
{ key: '@media (max-width: 768px)', value: 3 }, { key: '@media (max-width: 768px)', value: 3 },
{ key: '@media (max-width: 480px)', value: 1 }, { key: '@media (max-width: 480px)', value: 1 },
{ key: '@media (max-width: 10%)', value: 5 } { key: '@media (max-width: 10%)', value: 5 },
]); ]);
}); });
@ -330,14 +312,14 @@ describe('CssGenerator', () => {
'@font-face': 2, '@font-face': 2,
'@media (min-width: 768px)': 3, '@media (min-width: 768px)': 3,
'@media (min-width: 1020ch)': 4, '@media (min-width: 1020ch)': 4,
'@media (min-width: 10%)': 5 '@media (min-width: 10%)': 5,
}) })
).toEqual([ ).toEqual([
{ key: '@font-face', value: 2 }, { key: '@font-face', value: 2 },
{ key: '@media (min-width: 10%)', value: 5 }, { key: '@media (min-width: 10%)', value: 5 },
{ key: '@media (min-width: 480px)', value: 1 }, { key: '@media (min-width: 480px)', value: 1 },
{ key: '@media (min-width: 768px)', value: 3 }, { key: '@media (min-width: 768px)', value: 3 },
{ key: '@media (min-width: 1020ch)', value: 4 } { key: '@media (min-width: 1020ch)', value: 4 },
]); ]);
}); });
}); });

4
test/specs/dom_components/index.js

@ -152,12 +152,12 @@ describe('DOM Components', () => {
obj = em.get('DomComponents'); obj = em.get('DomComponents');
const cc = em.get('CssComposer'); const cc = em.get('CssComposer');
const id = 'idtest'; const id = 'idtest';
const comp = obj.addComponent(` const component = obj.addComponent(`
<div id="${id}" style="color:red; padding: 50px 100px">Text</div> <div id="${id}" style="color:red; padding: 50px 100px">Text</div>
<style> <style>
#${id} { background-color: red } #${id} { background-color: red }
</style>`); </style>`);
expect(em.getHtml()).toEqual(`<div id="${id}">Text</div>`); expect(em.getHtml({ component })).toEqual(`<div id="${id}">Text</div>`);
expect(obj.getComponents().length).toEqual(1); expect(obj.getComponents().length).toEqual(1);
obj.getComponents().first().addStyle({ margin: '10px' }); obj.getComponents().first().addStyle({ margin: '10px' });
expect(cc.getAll().length).toEqual(1); expect(cc.getAll().length).toEqual(1);

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

@ -547,13 +547,13 @@ describe('Link Component', () => {
<div>text</div> <div>text</div>
<div>here </div>`; <div>here </div>`;
obj = ComponentLink.isComponent(aEl); obj = ComponentLink.isComponent(aEl);
expect(obj).toEqual({ type: 'link', editable: 0 }); expect(obj).toEqual({ type: 'link', editable: false });
}); });
test('Link element with only an image inside is not editable', () => { test('Link element with only an image inside is not editable', () => {
aEl.innerHTML = '<img src="##"/>'; aEl.innerHTML = '<img src="##"/>';
obj = ComponentLink.isComponent(aEl); obj = ComponentLink.isComponent(aEl);
expect(obj).toEqual({ type: 'link', editable: 0 }); expect(obj).toEqual({ type: 'link', editable: false });
}); });
}); });

21
test/specs/grapesjs/headless.js

@ -13,12 +13,13 @@ describe('GrapesJS Headless', () => {
let editor; let editor;
const cmpObj = { const cmpObj = {
attributes: { class: 'cls', test: 'value' }, attributes: { class: 'cls', test: 'value' },
components: { type: 'textnode', content: 'Test' } components: { type: 'textnode', content: 'Test' },
}; };
const cmpStr = '<div class="cls" test="value">Test</div>'; const cmpStr = '<div class="cls" test="value">Test</div>';
const fullHtml = `<body>${cmpStr}</body>`;
const styleObj = { const styleObj = {
selectors: [{ name: 'cls' }], selectors: [{ name: 'cls' }],
style: { color: 'red' } style: { color: 'red' },
}; };
const styleStr = '.cls{color:red;}'; const styleStr = '.cls{color:red;}';
@ -37,7 +38,7 @@ describe('GrapesJS Headless', () => {
expect(comp.toHTML()).toBe(cmpStr); expect(comp.toHTML()).toBe(cmpStr);
expect(editor.Selectors.getAll().length).toBe(1); // 1 selector is created expect(editor.Selectors.getAll().length).toBe(1); // 1 selector is created
expect(editor.Css.getAll().length).toBe(0); // No CSS expect(editor.Css.getAll().length).toBe(0); // No CSS
expect(editor.getHtml()).toBe(cmpStr); expect(editor.getHtml()).toBe(fullHtml);
expect(editor.getCss()).toBe(''); // same as default expect(editor.getCss()).toBe(''); // same as default
}); });
@ -55,10 +56,10 @@ describe('GrapesJS Headless', () => {
copyable: false, copyable: false,
selectable: true, selectable: true,
content: 'Hello!', content: 'Hello!',
_innertext: false _innertext: false,
} },
] ],
} },
]); ]);
expect(res.length).toBe(1); expect(res.length).toBe(1);
const resHtml = '<h1>Hello!</h1>'; const resHtml = '<h1>Hello!</h1>';
@ -66,7 +67,7 @@ describe('GrapesJS Headless', () => {
expect(comp.toHTML()).toBe(resHtml); expect(comp.toHTML()).toBe(resHtml);
expect(editor.Selectors.getAll().length).toBe(0); expect(editor.Selectors.getAll().length).toBe(0);
expect(editor.Css.getAll().length).toBe(0); expect(editor.Css.getAll().length).toBe(0);
expect(editor.getHtml()).toBe(resHtml); expect(editor.getHtml()).toBe(`<body>${resHtml}</body>`);
expect(editor.getCss()).toBe(''); expect(editor.getCss()).toBe('');
}); });
@ -81,10 +82,10 @@ describe('GrapesJS Headless', () => {
test('Load data', () => { test('Load data', () => {
editor.loadData({ editor.loadData({
components: [cmpObj], components: [cmpObj],
styles: [styleObj] styles: [styleObj],
}); });
expect(editor.Selectors.getAll().length).toBe(1); expect(editor.Selectors.getAll().length).toBe(1);
expect(editor.getHtml()).toBe(cmpStr); expect(editor.getHtml()).toBe(fullHtml);
expect(editor.getCss()).toBe(styleStr); expect(editor.getCss()).toBe(styleStr);
}); });
}); });

95
test/specs/grapesjs/index.js

@ -18,7 +18,7 @@ describe('GrapesJS', () => {
}, },
load(keys, clb) { load(keys, clb) {
return clb(storage); return clb(storage);
} },
}; };
beforeAll(() => { beforeAll(() => {
@ -27,16 +27,17 @@ describe('GrapesJS', () => {
beforeEach(() => { beforeEach(() => {
storage = {}; storage = {};
htmlString = '<div class="test1"></div><div class="test2"></div>'; const initHtml = '<div class="test1"></div><div class="test2"></div>';
htmlString = `<body>${initHtml}</body>`;
cssString = '.test2{color:red}.test3{color:blue}'; cssString = '.test2{color:red}.test3{color:blue}';
documentEl = '<style>' + cssString + '</style>' + htmlString; documentEl = '<style>' + cssString + '</style>' + initHtml;
config = { config = {
container: '#' + editorName, container: '#' + editorName,
storageManager: { storageManager: {
autoload: 0, autoload: 0,
autosave: 0, autosave: 0,
type: 0 type: 0,
} },
}; };
obj = grapesjs; obj = grapesjs;
document.body.innerHTML = `<div id="fixtures"><div id="${editorName}"></div></div>`; document.body.innerHTML = `<div id="fixtures"><div id="${editorName}"></div></div>`;
@ -67,19 +68,19 @@ describe('GrapesJS', () => {
container: document.createElement('div'), container: document.createElement('div'),
storageManager: { storageManager: {
autoload: 0, autoload: 0,
type: 'none' type: 'none',
} },
}; };
var editor = obj.init(configAlt); var editor = obj.init(configAlt);
expect(editor).toBeTruthy(); expect(editor).toBeTruthy();
}); });
test('New editor is empty', () => { test('New editor is empty', () => {
var editor = obj.init(config); const editor = obj.init(config);
var html = editor.getHtml(); const html = editor.getHtml();
//var css = editor.getCss(); //const css = editor.getCss();
var protCss = editor.getConfig().protectedCss; const protCss = editor.getConfig().protectedCss;
expect(html ? html : '').toBeFalsy(); expect(html).toBe('<body></body>');
//expect((css ? css : '')).toEqual(protCss); //expect((css ? css : '')).toEqual(protCss);
expect(editor.getComponents().length).toEqual(0); expect(editor.getComponents().length).toEqual(0);
expect(editor.getStyle().length).toEqual(0); expect(editor.getStyle().length).toEqual(0);
@ -93,9 +94,7 @@ describe('GrapesJS', () => {
const body = editor.Canvas.getBody(); const body = editor.Canvas.getBody();
expect(body.outerHTML).toContain(config.baseCss); expect(body.outerHTML).toContain(config.baseCss);
expect(body.outerHTML.replace(/\s+/g, ` `)).not.toContain( expect(body.outerHTML.replace(/\s+/g, ` `)).not.toContain(`body { margin: 0;`);
`body { margin: 0;`
);
}); });
test('Editor canvas baseCSS defaults to sensible values if not defined', () => { test('Editor canvas baseCSS defaults to sensible values if not defined', () => {
@ -104,9 +103,7 @@ describe('GrapesJS', () => {
var editor = obj.init(config); var editor = obj.init(config);
expect( expect(window.frames[0].document.documentElement.outerHTML.replace(/\s+/g, ` `)).toContain(`body { margin: 0;`);
window.frames[0].document.documentElement.outerHTML.replace(/\s+/g, ` `)
).toContain(`body { margin: 0;`);
}); });
test('Init editor with html', () => { test('Init editor with html', () => {
@ -114,13 +111,7 @@ describe('GrapesJS', () => {
var editor = obj.init(config); var editor = obj.init(config);
var comps = editor.DomComponents.getComponents(); var comps = editor.DomComponents.getComponents();
expect(comps.length).toEqual(2); expect(comps.length).toEqual(2);
expect( expect(comps.at(0).get('classes').at(0).get('name')).toEqual('test1');
comps
.at(0)
.get('classes')
.at(0)
.get('name')
).toEqual('test1');
}); });
test('Init editor with css', () => { test('Init editor with css', () => {
@ -128,13 +119,7 @@ describe('GrapesJS', () => {
var editor = obj.init(config); var editor = obj.init(config);
var rules = editor.CssComposer.getAll(); var rules = editor.CssComposer.getAll();
expect(rules.length).toEqual(2); expect(rules.length).toEqual(2);
expect( expect(rules.at(0).get('selectors').at(0).get('name')).toEqual('test2');
rules
.at(0)
.get('selectors')
.at(0)
.get('name')
).toEqual('test2');
}); });
test('Init editor from element', () => { test('Init editor from element', () => {
@ -193,13 +178,7 @@ describe('GrapesJS', () => {
editor.setStyle(cssString); editor.setStyle(cssString);
var styles = editor.getStyle(); var styles = editor.getStyle();
expect(styles.length).toEqual(2); expect(styles.length).toEqual(2);
expect( expect(styles.at(1).get('selectors').at(0).get('name')).toEqual('test3');
styles
.at(1)
.get('selectors')
.at(0)
.get('name')
).toEqual('test3');
}); });
test('Set style as as array of objects', () => { test('Set style as as array of objects', () => {
@ -207,20 +186,12 @@ describe('GrapesJS', () => {
editor.setStyle([{ selectors: ['test4'] }, { selectors: ['test5'] }]); editor.setStyle([{ selectors: ['test4'] }, { selectors: ['test5'] }]);
var styles = editor.getStyle(); var styles = editor.getStyle();
expect(styles.length).toEqual(2); expect(styles.length).toEqual(2);
expect( expect(styles.at(1).get('selectors').at(0).get('name')).toEqual('test5');
styles
.at(1)
.get('selectors')
.at(0)
.get('name')
).toEqual('test5');
}); });
test.skip('Adds new storage as plugin and store data there', done => { test.skip('Adds new storage as plugin and store data there', done => {
const pluginName = storageId + '-p2'; const pluginName = storageId + '-p2';
obj.plugins.add(pluginName, e => obj.plugins.add(pluginName, e => e.StorageManager.add(storageId, storageMock));
e.StorageManager.add(storageId, storageMock)
);
config.storageManager.type = storageId; config.storageManager.type = storageId;
config.plugins = [pluginName]; config.plugins = [pluginName];
const editor = obj.init(config); const editor = obj.init(config);
@ -239,26 +210,24 @@ describe('GrapesJS', () => {
const style = [ const style = [
{ {
selectors: [{ name: 'sclass1' }], selectors: [{ name: 'sclass1' }],
style: { color: 'green' } style: { color: 'green' },
}, },
{ {
selectors: [{ name: 'test2' }], selectors: [{ name: 'test2' }],
style: styleResult style: styleResult,
}, },
{ {
selectors: [{ name: 'test3' }], selectors: [{ name: 'test3' }],
style: { color: 'black', display: 'block' } style: { color: 'black', display: 'block' },
} },
]; ];
storage = { storage = {
css: '* { box-sizing: border-box; } body {margin: 0;}', css: '* { box-sizing: border-box; } body {margin: 0;}',
styles: JSON.stringify(style) styles: JSON.stringify(style),
}; };
const pluginName = storageId + '-p'; const pluginName = storageId + '-p';
obj.plugins.add(pluginName, e => obj.plugins.add(pluginName, e => e.StorageManager.add(storageId, storageMock));
e.StorageManager.add(storageId, storageMock)
);
config.fromElement = 1; config.fromElement = 1;
config.storageManager.type = storageId; config.storageManager.type = storageId;
config.plugins = [pluginName]; config.plugins = [pluginName];
@ -326,7 +295,7 @@ describe('GrapesJS', () => {
editor.Commands.add('test-command', { editor.Commands.add('test-command', {
run(ed, caller, opts) { run(ed, caller, opts) {
ed.testVal = ed.getHtml() + opts.val; ed.testVal = ed.getHtml() + opts.val;
} },
}); });
editor.runCommand('test-command', { val: 5 }); editor.runCommand('test-command', { val: 5 });
expect(editor.testVal).toEqual(htmlString + '5'); expect(editor.testVal).toEqual(htmlString + '5');
@ -339,7 +308,7 @@ describe('GrapesJS', () => {
editor.Commands.add('test-command', { editor.Commands.add('test-command', {
stop(ed, caller, opts) { stop(ed, caller, opts) {
ed.testVal = ed.getHtml() + opts.val; ed.testVal = ed.getHtml() + opts.val;
} },
}); });
editor.stopCommand('test-command', { val: 5, force: 1 }); editor.stopCommand('test-command', { val: 5, force: 1 });
expect(editor.testVal).toEqual(htmlString + '5'); expect(editor.testVal).toEqual(htmlString + '5');
@ -356,7 +325,7 @@ describe('GrapesJS', () => {
editor.on(`abort:${id}`, () => (result.abort = 1)); editor.on(`abort:${id}`, () => (result.abort = 1));
editor.Commands.add(id, { editor.Commands.add(id, {
run() {}, run() {},
stop() {} stop() {},
}); });
editor.runCommand(id); editor.runCommand(id);
editor.stopCommand(id); editor.stopCommand(id);
@ -367,7 +336,7 @@ describe('GrapesJS', () => {
runBefore: 1, runBefore: 1,
stop: 1, stop: 1,
stopBefore: 1, stopBefore: 1,
abort: 1 abort: 1,
}); });
}); });
@ -375,7 +344,7 @@ describe('GrapesJS', () => {
config.deviceManager = {}; config.deviceManager = {};
config.deviceManager.devices = [ config.deviceManager.devices = [
{ name: '1', width: '2' }, { name: '1', width: '2' },
{ name: '3', width: '4' } { name: '3', width: '4' },
]; ];
var editor = obj.init(config); var editor = obj.init(config);
expect(editor.DeviceManager.getAll().length).toEqual(2); expect(editor.DeviceManager.getAll().length).toEqual(2);
@ -516,7 +485,7 @@ describe('GrapesJS', () => {
const toSpy = { const toSpy = {
selected() {}, selected() {},
deselected() {}, deselected() {},
toggled() {} toggled() {},
}; };
const selected = jest.spyOn(toSpy, 'selected'); const selected = jest.spyOn(toSpy, 'selected');
const deselected = jest.spyOn(toSpy, 'deselected'); const deselected = jest.spyOn(toSpy, 'deselected');

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

@ -10,7 +10,7 @@ describe('ParserHtml', () => {
obj = new ParserHtml({ obj = new ParserHtml({
textTags: ['br', 'b', 'i', 'u'], textTags: ['br', 'b', 'i', 'u'],
pStylePrefix: 'gjs-', pStylePrefix: 'gjs-',
returnArray: 1 returnArray: 1,
}); });
obj.compTypes = dom.componentTypes; obj.compTypes = dom.componentTypes;
}); });
@ -32,8 +32,7 @@ describe('ParserHtml', () => {
}); });
test('Node with attributes', () => { test('Node with attributes', () => {
var str = var str = '<div id="test1" class="test2 test3" data-one="test4" strange="test5"></div>';
'<div id="test1" class="test2 test3" data-one="test4" strange="test5"></div>';
var result = [ var result = [
{ {
tagName: 'div', tagName: 'div',
@ -41,9 +40,9 @@ describe('ParserHtml', () => {
attributes: { attributes: {
'data-one': 'test4', 'data-one': 'test4',
id: 'test1', id: 'test1',
strange: 'test5' strange: 'test5',
} },
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -53,7 +52,7 @@ describe('ParserHtml', () => {
var result = { var result = {
color: 'black', color: 'black',
width: '100px', width: '100px',
test: 'value' test: 'value',
}; };
expect(obj.parseStyle(str)).toEqual(result); expect(obj.parseStyle(str)).toEqual(result);
}); });
@ -62,7 +61,7 @@ describe('ParserHtml', () => {
var str = 'background-image:url("https://some-website.ex"); test:value;'; var str = 'background-image:url("https://some-website.ex"); test:value;';
var result = { var result = {
'background-image': 'url("https://some-website.ex")', 'background-image': 'url("https://some-website.ex")',
test: 'value' test: 'value',
}; };
expect(obj.parseStyle(str)).toEqual(result); expect(obj.parseStyle(str)).toEqual(result);
}); });
@ -80,8 +79,7 @@ describe('ParserHtml', () => {
}); });
test('Style attribute is isolated', () => { test('Style attribute is isolated', () => {
var str = var str = '<div id="test1" style="color:black; width:100px; test:value;"></div>';
'<div id="test1" style="color:black; width:100px; test:value;"></div>';
var result = [ var result = [
{ {
tagName: 'div', tagName: 'div',
@ -89,9 +87,9 @@ describe('ParserHtml', () => {
style: { style: {
color: 'black', color: 'black',
width: '100px', width: '100px',
test: 'value' test: 'value',
} },
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -102,8 +100,8 @@ describe('ParserHtml', () => {
{ {
tagName: 'div', tagName: 'div',
attributes: { id: 'test1' }, attributes: { id: 'test1' },
classes: ['test2', 'test3', 'test4'] classes: ['test2', 'test3', 'test4'],
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -116,9 +114,9 @@ describe('ParserHtml', () => {
type: 'image', type: 'image',
attributes: { attributes: {
id: 'test1', id: 'test1',
src: './index.html' src: './index.html',
} },
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -130,15 +128,14 @@ describe('ParserHtml', () => {
tagName: 'div', tagName: 'div',
attributes: { id: 'test1' }, attributes: { id: 'test1' },
type: 'text', type: 'text',
components: { type: 'textnode', content: 'test2 ' } components: { type: 'textnode', content: 'test2 ' },
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
test('Parse text with few text tags', () => { test('Parse text with few text tags', () => {
var str = var str = '<div id="test1"><br/> test2 <br/> a b <b>b</b> <i>i</i> <u>u</u> test </div>';
'<div id="test1"><br/> test2 <br/> a b <b>b</b> <i>i</i> <u>u</u> test </div>';
var result = [ var result = [
{ {
tagName: 'div', tagName: 'div',
@ -149,53 +146,52 @@ describe('ParserHtml', () => {
{ {
content: ' test2 ', content: ' test2 ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
}, },
{ tagName: 'br' }, { tagName: 'br' },
{ {
content: ' a b ', content: ' a b ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
}, },
{ {
components: { type: 'textnode', content: 'b' }, components: { type: 'textnode', content: 'b' },
type: 'text', type: 'text',
tagName: 'b' tagName: 'b',
}, },
{ {
content: ' ', content: ' ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
}, },
{ {
components: { type: 'textnode', content: 'i' }, components: { type: 'textnode', content: 'i' },
tagName: 'i', tagName: 'i',
type: 'text' type: 'text',
}, },
{ {
content: ' ', content: ' ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
}, },
{ {
components: { type: 'textnode', content: 'u' }, components: { type: 'textnode', content: 'u' },
tagName: 'u', tagName: 'u',
type: 'text' type: 'text',
}, },
{ {
content: ' test ', content: ' test ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
} },
] ],
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
test('Parse text with few text tags and nested node', () => { test('Parse text with few text tags and nested node', () => {
var str = var str = '<div id="test1">a b <b>b</b> <i>i</i>c <div>ABC</div> <i>i</i> <u>u</u> test </div>';
'<div id="test1">a b <b>b</b> <i>i</i>c <div>ABC</div> <i>i</i> <u>u</u> test </div>';
var result = [ var result = [
{ {
tagName: 'div', tagName: 'div',
@ -205,60 +201,60 @@ describe('ParserHtml', () => {
{ {
content: 'a b ', content: 'a b ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
}, },
{ {
components: { type: 'textnode', content: 'b' }, components: { type: 'textnode', content: 'b' },
tagName: 'b', tagName: 'b',
type: 'text' type: 'text',
}, },
{ {
content: ' ', content: ' ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
}, },
{ {
components: { type: 'textnode', content: 'i' }, components: { type: 'textnode', content: 'i' },
tagName: 'i', tagName: 'i',
type: 'text' type: 'text',
}, },
{ {
content: 'c ', content: 'c ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
}, },
{ {
tagName: 'div', tagName: 'div',
type: 'text', type: 'text',
components: { type: 'textnode', content: 'ABC' } components: { type: 'textnode', content: 'ABC' },
}, },
{ {
content: ' ', content: ' ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
}, },
{ {
components: { type: 'textnode', content: 'i' }, components: { type: 'textnode', content: 'i' },
tagName: 'i', tagName: 'i',
type: 'text' type: 'text',
}, },
{ {
content: ' ', content: ' ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
}, },
{ {
components: { type: 'textnode', content: 'u' }, components: { type: 'textnode', content: 'u' },
tagName: 'u', tagName: 'u',
type: 'text' type: 'text',
}, },
{ {
content: ' test ', content: ' test ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
} },
] ],
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -272,28 +268,28 @@ describe('ParserHtml', () => {
attributes: { id: 'test1' }, attributes: { id: 'test1' },
components: [ components: [
{ {
tagName: 'div' tagName: 'div',
}, },
{ {
content: ' ', content: ' ',
type: 'textnode', type: 'textnode',
tagName: '' tagName: '',
}, },
{ {
tagName: 'footer', tagName: 'footer',
attributes: { id: 'test2' } attributes: { id: 'test2' },
}, },
{ {
tagName: '', tagName: '',
type: 'textnode', type: 'textnode',
content: ' Text mid ' content: ' Text mid ',
}, },
{ {
tagName: 'div', tagName: 'div',
attributes: { id: 'last' } attributes: { id: 'last' },
} },
] ],
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -308,20 +304,20 @@ describe('ParserHtml', () => {
{ {
tagName: '', tagName: '',
type: 'textnode', type: 'textnode',
content: 'content1 ' content: 'content1 ',
}, },
{ {
tagName: 'div', tagName: 'div',
type: 'text', type: 'text',
components: { type: 'textnode', content: 'nested' } components: { type: 'textnode', content: 'nested' },
}, },
{ {
tagName: '', tagName: '',
type: 'textnode', type: 'textnode',
content: ' content2' content: ' content2',
} },
] ],
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -335,7 +331,7 @@ describe('ParserHtml', () => {
{ {
tagName: '', tagName: '',
type: 'textnode', type: 'textnode',
content: 'content1 ' content: 'content1 ',
}, },
{ {
tagName: 'div', tagName: 'div',
@ -343,17 +339,17 @@ describe('ParserHtml', () => {
{ {
tagName: 'span', tagName: 'span',
type: 'text', type: 'text',
components: { type: 'textnode', content: 'nested' } components: { type: 'textnode', content: 'nested' },
} },
] ],
}, },
{ {
tagName: '', tagName: '',
type: 'textnode', type: 'textnode',
content: ' content2' content: ' content2',
} },
] ],
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -365,25 +361,23 @@ describe('ParserHtml', () => {
}); });
test('Remove script tags', () => { test('Remove script tags', () => {
var str = var str = '<div><script>var test;</script></div><div></div><script>var test2;</script>';
'<div><script>var test;</script></div><div></div><script>var test2;</script>';
var result = [{ tagName: 'div' }, { tagName: 'div' }]; var result = [{ tagName: 'div' }, { tagName: 'div' }];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
test('Isolate styles', () => { test('Isolate styles', () => {
var str = var str = '<div><style>.a{color: red}</style></div><div></div><style>.b{color: blue}</style>';
'<div><style>.a{color: red}</style></div><div></div><style>.b{color: blue}</style>';
var resHtml = [{ tagName: 'div' }, { tagName: 'div' }]; var resHtml = [{ tagName: 'div' }, { tagName: 'div' }];
var resCss = [ var resCss = [
{ {
selectors: ['a'], selectors: ['a'],
style: { color: 'red' } style: { color: 'red' },
}, },
{ {
selectors: ['b'], selectors: ['b'],
style: { color: 'blue' } style: { color: 'blue' },
} },
]; ];
var res = obj.parse(str, new ParserCss()); var res = obj.parse(str, new ParserCss());
expect(res.html).toEqual(resHtml); expect(res.html).toEqual(resHtml);
@ -411,23 +405,21 @@ describe('ParserHtml', () => {
selectorsAdd: '', selectorsAdd: '',
style: { style: {
'font-family': '"Open Sans"', 'font-family': '"Open Sans"',
src: src: 'url(https://fonts.gstatic.com/s/droidsans/v8/SlGVmQWMvZQIdix7AFxXkHNSbRYXags.woff2)',
'url(https://fonts.gstatic.com/s/droidsans/v8/SlGVmQWMvZQIdix7AFxXkHNSbRYXags.woff2)'
}, },
singleAtRule: 1, singleAtRule: 1,
atRuleType: 'font-face' atRuleType: 'font-face',
}, },
{ {
selectors: [], selectors: [],
selectorsAdd: '', selectorsAdd: '',
style: { style: {
'font-family': "'Glyphicons Halflings'", 'font-family': "'Glyphicons Halflings'",
src: src: 'url(https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot)',
'url(https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot)'
}, },
singleAtRule: 1, singleAtRule: 1,
atRuleType: 'font-face' atRuleType: 'font-face',
} },
]; ];
const res = obj.parse(str, new ParserCss()); const res = obj.parse(str, new ParserCss());
@ -444,20 +436,20 @@ describe('ParserHtml', () => {
{ {
tagName: '', tagName: '',
type: 'textnode', type: 'textnode',
content: ' ' content: ' ',
}, },
{ {
tagName: 'p', tagName: 'p',
components: { type: 'textnode', content: 'TestText' }, components: { type: 'textnode', content: 'TestText' },
type: 'text' type: 'text',
}, },
{ {
tagName: '', tagName: '',
type: 'textnode', type: 'textnode',
content: ' ' content: ' ',
} },
] ],
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -472,18 +464,17 @@ describe('ParserHtml', () => {
stuff: 'test', stuff: 'test',
attributes: { attributes: {
id: 'test1', id: 'test1',
'data-test': 'test-value' 'data-test': 'test-value',
}, },
type: 'text', type: 'text',
components: { type: 'textnode', content: 'test2 ' } components: { type: 'textnode', content: 'test2 ' },
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
test('Parse model attributes with true and false', () => { test('Parse model attributes with true and false', () => {
var str = var str = '<div id="test1" data-test="test-value" data-gjs-draggable="true" data-gjs-stuff="false">test2 </div>';
'<div id="test1" data-test="test-value" data-gjs-draggable="true" data-gjs-stuff="false">test2 </div>';
var result = [ var result = [
{ {
tagName: 'div', tagName: 'div',
@ -491,11 +482,11 @@ describe('ParserHtml', () => {
stuff: false, stuff: false,
attributes: { attributes: {
id: 'test1', id: 'test1',
'data-test': 'test-value' 'data-test': 'test-value',
}, },
type: 'text', type: 'text',
components: { type: 'textnode', content: 'test2 ' } components: { type: 'textnode', content: 'test2 ' },
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -510,10 +501,10 @@ describe('ParserHtml', () => {
test: { test: {
prop1: 'value1', prop1: 'value1',
prop2: 10, prop2: 10,
prop3: true prop3: true,
}, },
components: { type: 'textnode', content: 'test2 ' } components: { type: 'textnode', content: 'test2 ' },
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -526,8 +517,8 @@ describe('ParserHtml', () => {
attributes: {}, attributes: {},
type: 'text', type: 'text',
test: ['value1', 'value2'], test: ['value1', 'value2'],
components: { type: 'textnode', content: 'test2 ' } components: { type: 'textnode', content: 'test2 ' },
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });
@ -535,6 +526,7 @@ describe('ParserHtml', () => {
test('SVG is properly parsed', () => { test('SVG is properly parsed', () => {
const str = `<div> const str = `<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<linearGradient x1="0%" y1="0%"/>
<path d="M13 12h7v1.5h-7m0-4h7V11h-7m0 3.5h7V16h-7m8-12H3c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2m0 15h-9V6h9"></path> <path d="M13 12h7v1.5h-7m0-4h7V11h-7m0 3.5h7V16h-7m8-12H3c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2m0 15h-9V6h9"></path>
</svg> </svg>
</div>`; </div>`;
@ -547,21 +539,25 @@ describe('ParserHtml', () => {
tagName: 'svg', tagName: 'svg',
attributes: { attributes: {
xmlns: 'http://www.w3.org/2000/svg', xmlns: 'http://www.w3.org/2000/svg',
viewBox: '0 0 24 24' viewBox: '0 0 24 24',
}, },
components: [ components: [
{
tagName: 'linearGradient',
attributes: { x1: '0%', y1: '0%' },
type: 'svg-in',
},
{ {
tagName: 'path', tagName: 'path',
attributes: { attributes: {
d: d: 'M13 12h7v1.5h-7m0-4h7V11h-7m0 3.5h7V16h-7m8-12H3c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2m0 15h-9V6h9',
'M13 12h7v1.5h-7m0-4h7V11h-7m0 3.5h7V16h-7m8-12H3c-1.1 0-2 .9-2 2v13c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2m0 15h-9V6h9'
}, },
type: 'svg-in' type: 'svg-in',
} },
] ],
} },
] ],
} },
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
}); });

29
webpack.config.js

@ -1,6 +1,7 @@
import path from 'path'; import path from 'path';
import webpack from 'webpack'; import webpack from 'webpack';
import pkg from './package.json'; import pkg from './package.json';
import TerserPlugin from 'grapesjs-cli/node_modules/terser-webpack-plugin';
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
@ -11,18 +12,32 @@ export default ({ config }) => ({
filename: 'grapes.min.js', filename: 'grapes.min.js',
libraryExport: 'default', libraryExport: 'default',
}, },
optimization: {
minimizer: [new TerserPlugin({
extractComments: false,
terserOptions: {
output: {
comments: false,
quote_style: 3, // Preserve original quotes
preamble: `/*! grapesjs - ${pkg.version} */`,
}
}
})],
},
devServer: { devServer: {
...config.devServer,
static: [rootDir],
headers: { 'Access-Control-Allow-Origin': '*' }, headers: { 'Access-Control-Allow-Origin': '*' },
disableHostCheck: true, allowedHosts: 'all',
},
module: {
rules: [
...config.module.rules,
],
}, },
resolve: { resolve: {
modules: ['src', 'node_modules'], ...config.resolve,
modules: [
...(config.resolve && config.resolve.modules),
'src'
],
alias: { alias: {
...(config.resolve && config.resolve.alias),
jquery: 'utils/cash-dom', jquery: 'utils/cash-dom',
backbone: `${rootDir}/node_modules/backbone`, backbone: `${rootDir}/node_modules/backbone`,
underscore: `${rootDir}/node_modules/underscore`, underscore: `${rootDir}/node_modules/underscore`,

2250
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save