Browse Source

Merge pull request #3671 from artf/headless-option

Headless option
pull/3676/head
Artur Arseniev 5 years ago
committed by GitHub
parent
commit
ae1950761e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      dist/grapes.min.js
  2. 2
      dist/grapes.min.js.map
  3. 2
      package.json
  4. 47
      src/asset_manager/index.js
  5. 1
      src/asset_manager/view/AssetsView.js
  6. 11
      src/canvas/index.js
  7. 2
      src/canvas/view/CanvasView.js
  8. 14
      src/code_manager/model/CodeMirrorEditor.js
  9. 5
      src/commands/view/CommandAbstract.js
  10. 20
      src/commands/view/OpenAssets.js
  11. 3
      src/commands/view/OpenTraitManager.js
  12. 5
      src/commands/view/SelectComponent.js
  13. 11
      src/css_composer/index.js
  14. 4
      src/css_composer/model/CssRule.js
  15. 12
      src/device_manager/index.js
  16. 39
      src/editor/index.js
  17. 50
      src/editor/model/Editor.js
  18. 3
      src/i18n/index.js
  19. 9
      src/index.js
  20. 5
      src/keymaps/index.js
  21. 13
      src/modal_dialog/config/config.js
  22. 17
      src/modal_dialog/index.js
  23. 24
      src/modal_dialog/model/Modal.js
  24. 47
      src/modal_dialog/view/ModalView.js
  25. 19
      src/navigator/index.js
  26. 13
      src/panels/index.js
  27. 3
      src/rich_text_editor/index.js
  28. 26
      src/selector_manager/index.js
  29. 10
      src/selector_manager/view/ClassTagsView.js
  30. 32
      src/storage_manager/index.js
  31. 52
      src/storage_manager/model/LocalStorage.js
  32. 13
      src/style_manager/index.js
  33. 3
      src/style_manager/model/PropertyInteger.js
  34. 1
      src/style_manager/view/SectorsView.js
  35. 40
      src/trait_manager/index.js
  36. 14
      src/trait_manager/view/TraitsView.js
  37. 2
      src/utils/ColorPicker.js
  38. 2
      src/utils/extender.js
  39. 9
      src/utils/fetch.js
  40. 336
      src/utils/keymaster.js
  41. 4
      src/utils/mixins.js
  42. 3
      src/utils/polyfills.js
  43. 2
      test/specs/css_composer/e2e/CssComposer.js
  44. 1
      test/specs/dom_components/view/ComponentV.js
  45. 62
      test/specs/grapesjs/headless.js
  46. 1
      test/specs/keymaps/index.js
  47. 1
      test/specs/modal/index.js

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

2
package.json

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

47
src/asset_manager/index.js

@ -37,7 +37,7 @@ import FileUpload from './view/FileUploader';
export default () => {
let c = {};
let assets, am, fu;
let assets, assetsVis, am, fu;
return {
/**
@ -79,15 +79,7 @@ export default () => {
// Global assets collection
assets = new Assets([]);
const obj = {
// Collection visible in asset manager
collection: new Assets([]),
globalCollection: assets,
config: c
};
fu = new FileUpload(obj);
obj.fu = fu;
am = new AssetsView(obj);
assetsVis = new Assets([]);
// Setup the sync between the global and public collections
assets.listenTo(assets, 'add', model => {
@ -159,7 +151,7 @@ export default () => {
* @return {Collection}
*/
getAllVisible() {
return am.collection;
return assetsVis;
},
/**
@ -236,8 +228,7 @@ export default () => {
/**
* Render assets
* @param {array} assets Assets to render, without the argument will render
* all global assets
* @param {array} assets Assets to render, without the argument will render all global assets
* @return {HTMLElement}
* @example
* // Render all assets
@ -249,14 +240,22 @@ export default () => {
* asset => asset.get('category') == 'cats'
* ));
*/
render(assets) {
const toRender = assets || this.getAll().models;
if (!am.rendered) {
render(assts) {
const toRender = assts || this.getAll().models;
if (!am) {
const obj = {
collection: assetsVis, // Collection visible in asset manager
globalCollection: assets,
config: c
};
fu = new FileUpload(obj);
obj.fu = fu;
am = new AssetsView(obj);
am.render();
}
am.collection.reset(toRender);
assetsVis.reset(toRender);
return this.getContainer();
},
@ -310,7 +309,7 @@ export default () => {
},
postRender(editorView) {
c.dropzone && fu.initDropzone(editorView);
c.dropzone && fu && fu.initDropzone(editorView);
},
/**
@ -319,7 +318,7 @@ export default () => {
* @private
* */
setTarget(m) {
am.collection.target = m;
assetsVis.target = m;
},
/**
@ -328,7 +327,7 @@ export default () => {
* @private
* */
onSelect(f) {
am.collection.onSelect = f;
assetsVis.onSelect = f;
},
/**
@ -351,9 +350,9 @@ export default () => {
destroy() {
assets.reset();
fu.collection.reset();
fu.remove();
am.remove();
assetsVis.reset();
fu && fu.remove();
am && am.remove();
[assets, am, fu].forEach(i => (i = null));
c = {};
}

1
src/asset_manager/view/AssetsView.js

@ -182,7 +182,6 @@ export default Backbone.View.extend({
this.$el.append(fuRendered).append(this.template(this));
this.el.className = `${this.ppfx}asset-manager`;
this.renderAssets();
this.rendered = 1;
return this;
}
});

11
src/canvas/index.js

@ -80,10 +80,6 @@ export default () => {
onLoad() {
this.model.init();
CanvasView = new canvasView({
model: canvas,
config: c
});
},
getModel() {
@ -115,7 +111,7 @@ export default () => {
* @return {HTMLIFrameElement}
*/
getFrameEl() {
const { frame } = CanvasView;
const { frame } = CanvasView || {};
return frame && frame.el;
},
@ -260,6 +256,11 @@ export default () => {
* @private
* */
render() {
CanvasView && CanvasView.remove();
CanvasView = new canvasView({
model: canvas,
config: c
});
return CanvasView.render().el;
},

2
src/canvas/view/CanvasView.js

@ -75,8 +75,6 @@ export default Backbone.View.extend({
remove() {
this.frames.remove();
const frm = this.model.get('frames');
frm.remove(frm.models);
this.frames = {};
Backbone.View.prototype.remove.apply(this, arguments);
this.toggleListeners();

14
src/code_manager/model/CodeMirrorEditor.js

@ -1,9 +1,15 @@
import { bindAll } from 'underscore';
import Backbone from 'backbone';
import CodeMirror from 'codemirror/lib/codemirror';
import 'codemirror/mode/htmlmixed/htmlmixed';
import 'codemirror/mode/css/css';
import 'codemirror-formatting';
import { hasWin } from 'utils/mixins';
let CodeMirror;
if (hasWin()) {
CodeMirror = require('codemirror/lib/codemirror');
require('codemirror/mode/htmlmixed/htmlmixed');
require('codemirror/mode/css/css');
require('codemirror-formatting');
}
export default Backbone.Model.extend({
CodeMirror,

5
src/commands/view/CommandAbstract.js

@ -1,7 +1,6 @@
import Backbone from 'backbone';
const $ = Backbone.$;
import { Model } from 'backbone';
export default Backbone.View.extend({
export default Model.extend({
/**
* Initialize method that can't be removed
* @param {Object} o Options

20
src/commands/view/OpenAssets.js

@ -3,7 +3,6 @@ export default {
const modal = editor.Modal;
const am = editor.AssetManager;
const config = am.getConfig();
const amContainer = am.getContainer();
const title = opts.modalTitle || editor.t('assetManager.modalTitle') || '';
const types = opts.types;
const accept = opts.accept;
@ -14,35 +13,24 @@ export default {
am.onSelect(opts.onSelect);
if (!this.rendered || types) {
let assets = am.getAll().filter(i => 1);
let assets = am.getAll().filter(i => i);
if (types && types.length) {
assets = assets.filter(a => types.indexOf(a.get('type')) !== -1);
}
am.render(assets);
this.rendered = 1;
this.rendered = am.getContainer();
}
if (accept) {
const uploadEl = amContainer.querySelector(
const uploadEl = this.rendered.querySelector(
`input#${config.stylePrefix}uploadFile`
);
uploadEl && uploadEl.setAttribute('accept', accept);
}
modal
.open({
title,
content: amContainer
})
.getModel()
.once('change:open', () => editor.stopCommand(this.id));
return this;
},
stop(editor) {
editor.Modal.close();
modal.open({ title, content: this.rendered });
return this;
}
};

3
src/commands/view/OpenTraitManager.js

@ -13,7 +13,6 @@ export default {
var panelC;
if (!this.$cn) {
var tmView = tm.getTraitsViewer();
var confTm = tm.getConfig();
this.$cn = $('<div></div>');
this.$cn2 = $('<div></div>');
@ -27,7 +26,7 @@ export default {
this.$cn2.append(
`<div class="${pfx}traits-label">${em.t('traitManager.label')}</div>`
);
this.$cn2.append(tmView.render().el);
this.$cn2.append(tm.render());
var panels = editor.Panels;
if (!panels.getPanel('views-container'))

5
src/commands/view/SelectComponent.js

@ -5,7 +5,8 @@ import {
off,
getUnitFromValue,
isTaggableNode,
getViewEl
getViewEl,
hasWin
} from 'utils/mixins';
import { isVisible, isDoc } from 'utils/dom';
import ToolbarView from 'dom_components/view/ToolbarView';
@ -746,11 +747,13 @@ export default {
},
run(editor) {
if (!hasWin()) return;
this.editor = editor && editor.get('Editor');
this.enable();
},
stop(ed, sender, opts = {}) {
if (!hasWin()) return;
const { em, editor } = this;
this.onHovered(); // force to hide toolbar
this.stopSelectComponent();

11
src/css_composer/index.js

@ -86,10 +86,6 @@ export default () => {
em = c.em;
rules = new CssRules([], c);
rulesView = new CssRulesView({
collection: rules,
config: c
});
return this;
},
@ -479,13 +475,18 @@ export default () => {
* @private
*/
render() {
rulesView && rulesView.remove();
rulesView = new CssRulesView({
collection: rules,
config: c
});
return rulesView.render().el;
},
destroy() {
rules.reset();
rules.stopListening();
rulesView.remove();
rulesView && rulesView.remove();
[em, rules, rulesView].forEach(i => (i = null));
c = {};
}

4
src/css_composer/model/CssRule.js

@ -3,9 +3,9 @@ import { Model } from 'backbone';
import Styleable from 'domain_abstract/model/Styleable';
import { isEmpty, forEach, isString } from 'underscore';
import Selectors from 'selector_manager/model/Selectors';
import { isEmptyObj } from 'utils/mixins';
import { isEmptyObj, hasWin } from 'utils/mixins';
const { CSS } = window;
const { CSS } = hasWin() ? window : {};
export default class CssRule extends Model.extend(Styleable) {
defaults() {

12
src/device_manager/index.js

@ -60,10 +60,7 @@ export default () => {
devices = new Devices();
(c.devices || []).forEach(dv => this.add(dv.id || dv.name, dv.width, dv));
view = new DevicesView({
collection: devices,
config: c
});
return this;
},
@ -123,13 +120,18 @@ export default () => {
* @private
*/
render() {
view && view.remove();
view = new DevicesView({
collection: devices,
config: c
});
return view.render().el;
},
destroy() {
devices.reset();
devices.stopListening();
view.remove();
view && view.remove();
[devices, view].forEach(i => (i = null));
c = {};
}

39
src/editor/index.js

@ -123,11 +123,8 @@ export default (config = {}) => {
};
c.pStylePrefix = c.stylePrefix;
var em = new EditorModel(c);
var editorView = new EditorView({
model: em,
config: c
});
let em = new EditorModel(c);
let editorView;
return {
$,
@ -489,6 +486,17 @@ export default (config = {}) => {
return em.store(clb);
},
/**
* Get the JSON data object, which could be stored and loaded back with `editor.loadData(json)`
* @returns {Object}
* @example
* console.log(editor.storeData());
* // { pages: [...], styles: [...], ... }
*/
storeData() {
return em.storeData();
},
/**
* Load data from the current storage
* @param {Function} clb Callback function
@ -498,6 +506,17 @@ export default (config = {}) => {
return em.load(clb);
},
/**
* Load data from the JSON data object
* @param {Object} data Data to load
* @return {Object} Loaded object
* @example
* editor.loadData({ pages: [...], styles: [...], ... })
*/
loadData(data) {
return em.loadData(data);
},
/**
* Returns container element. The one which was indicated as 'container'
* on init method
@ -691,7 +710,7 @@ export default (config = {}) => {
* @private
*/
getEl() {
return editorView.el;
return editorView && editorView.el;
},
/**
@ -708,8 +727,12 @@ export default (config = {}) => {
* @return {HTMLElement}
*/
render() {
editorView.render();
return editorView.el;
editorView && editorView.remove();
editorView = new EditorView({
model: em,
config: c
});
return editorView.render().el;
},
/**

50
src/editor/model/Editor.js

@ -10,7 +10,7 @@ import {
import $ from 'cash-dom';
import Backbone from 'backbone';
import Extender from 'utils/extender';
import { getModel } from 'utils/mixins';
import { getModel, hasWin } from 'utils/mixins';
import Selected from './Selected';
Backbone.$ = $;
@ -597,16 +597,10 @@ export default Backbone.Model.extend({
* @private
*/
store(clb) {
var sm = this.get('StorageManager');
var store = {};
const sm = this.get('StorageManager');
if (!sm) return;
// Fetch what to store
this.get('storables').forEach(m => {
var obj = m.store(1);
for (var el in obj) store[el] = obj[el];
});
const store = this.storeData();
sm.store(store, res => {
clb && clb(res, store);
this.set('changesCount', 0);
@ -616,6 +610,14 @@ export default Backbone.Model.extend({
return store;
},
storeData() {
let result = {};
this.get('storables').forEach(m => {
result = { ...result, ...m.store(1) };
});
return result;
},
/**
* Load data from the current storage
* @param {Function} clb Callback function
@ -623,14 +625,23 @@ export default Backbone.Model.extend({
*/
load(clb = null) {
this.getCacheLoad(1, res => {
this.get('storables').forEach(module => {
module.load(res);
module.postLoad && module.postLoad(this);
});
this.loadData(res);
clb && clb(res);
});
},
loadData(data = {}) {
const sm = this.get('StorageManager');
const result = sm.__clearKeys(data);
this.get('storables').forEach(module => {
module.load(result);
module.postLoad && module.postLoad(this);
});
return result;
},
/**
* Returns cached load
* @param {Boolean} force Force to reload
@ -690,7 +701,7 @@ export default Backbone.Model.extend({
stopDefault(opts = {}) {
const commands = this.get('Commands');
const command = commands.get(this.config.defaultCommand);
if (!command) return;
if (!command || !this.defaultRunning) return;
command.stop(this, this, opts);
this.defaultRunning = 0;
},
@ -786,7 +797,7 @@ export default Backbone.Model.extend({
* Destroy editor
*/
destroyAll() {
const { config } = this;
const { config, view } = this;
const editor = this.getEditor();
const { editors = [] } = config.grapesjs || {};
this.stopDefault();
@ -794,7 +805,7 @@ export default Backbone.Model.extend({
.slice()
.reverse()
.forEach(mod => mod.destroy());
this.view.remove();
view && view.remove();
this.stopListening();
this.clear({ silent: true });
this.destroyed = 1;
@ -802,9 +813,10 @@ export default Backbone.Model.extend({
i => (this[i] = {})
);
editors.splice(editors.indexOf(editor), 1);
$(config.el)
.empty()
.attr(this.attrsOrig);
hasWin() &&
$(config.el)
.empty()
.attr(this.attrsOrig);
},
getEditing() {

3
src/i18n/index.js

@ -27,6 +27,7 @@
* @module I18n
*/
import { isUndefined, isString } from 'underscore';
import { hasWin } from 'utils/mixins';
import config from './config';
const isObj = el => !Array.isArray(el) && el !== null && typeof el === 'object';
@ -210,7 +211,7 @@ export default () => {
},
_localLang() {
const nav = window.navigator || {};
const nav = (hasWin() && window.navigator) || {};
const lang = nav.language || nav.userLanguage;
return lang ? lang.split('-')[0] : 'en';
},

9
src/index.js

@ -36,6 +36,7 @@ export default {
* @param {Boolean} [config.autorender=true] If true, auto-render the content
* @param {Array} [config.plugins=[]] Array of plugins to execute on start
* @param {Object} [config.pluginsOpts={}] Custom options for plugins
* @param {Boolean} [config.headless=false] Init headless editor
* @return {Editor} Editor instance
* @example
* var editor = grapesjs.init({
@ -45,10 +46,12 @@ export default {
* })
*/
init(config = {}) {
const { headless } = config;
const els = config.container;
if (!els) throw new Error("'container' is required");
if (!els && !headless) throw new Error("'container' is required");
config = { ...defaultConfig, ...config, grapesjs: this };
config.el = isElement(els) ? els : document.querySelector(els);
config.el =
!headless && (isElement(els) ? els : document.querySelector(els));
const editor = new Editor(config).init();
const em = editor.getModel();
@ -79,7 +82,7 @@ export default {
// A plugin might have extended/added some custom type so this
// is a good point to load stuff like components, css rules, etc.
em.loadOnStart();
config.autorender && editor.render();
config.autorender && !headless && editor.render();
editors.push(editor);
return editor;

5
src/keymaps/index.js

@ -32,7 +32,10 @@
*/
import { isString } from 'underscore';
import keymaster from 'keymaster';
import { hasWin } from 'utils/mixins';
import keymaster from 'utils/keymaster';
hasWin() && keymaster.init(window);
export default () => {
let em;

13
src/modal_dialog/config/config.js

@ -5,5 +5,16 @@ export default {
content: '',
backdrop: true
backdrop: true,
/**
* Extend ModalView object (view/ModalView.js)
* @example
* extend: {
* template() {
* return '<div>...New modal template...</div>';
* },
* },
*/
extend: {}
};

17
src/modal_dialog/index.js

@ -69,10 +69,6 @@ export default () => {
model = new ModalM(c);
model.on('change:open', (m, enb) => triggerEvent(enb, em));
modal = new ModalView({
model,
config: c
});
return this;
},
@ -93,7 +89,8 @@ export default () => {
open(opts = {}) {
opts.title && this.setTitle(opts.title);
opts.content && this.setContent(opts.content);
modal.show(opts);
model.open();
modal && modal.updateAttr(opts.attributes);
return this;
},
@ -102,7 +99,7 @@ export default () => {
* @return {this}
*/
close() {
modal.hide();
model.close();
return this;
},
@ -201,11 +198,17 @@ export default () => {
* @private
*/
render() {
const View = ModalView.extend(c.extend);
modal && modal.remove();
modal = new View({
model,
config: c
});
return modal.render().$el;
},
destroy() {
modal.remove();
modal && modal.remove();
[c, model, modal].forEach(i => (i = {}));
this.em = {};
}

24
src/modal_dialog/model/Modal.js

@ -1,9 +1,19 @@
import Backbone from 'backbone';
import { Model } from 'backbone';
export default Backbone.Model.extend({
defaults: {
title: '',
content: '',
open: false
export default class Modal extends Model {
defaults() {
return {
title: '',
content: '',
open: false
};
}
});
open() {
this.set('open', true);
}
close() {
this.set('open', false);
}
}

47
src/modal_dialog/view/ModalView.js

@ -1,6 +1,6 @@
import Backbone from 'backbone';
import { View } from 'backbone';
export default Backbone.View.extend({
export default class ModalView extends View {
template({ pfx, ppfx, content, title }) {
return `<div class="${pfx}dialog ${ppfx}one-bg ${ppfx}two-color">
<div class="${pfx}header">
@ -13,12 +13,14 @@ export default Backbone.View.extend({
</div>
</div>
<div class="${pfx}collector" style="display: none"></div>`;
},
}
events: {
click: 'onClick',
'click [data-close-modal]': 'hide'
},
events() {
return {
click: 'onClick',
'click [data-close-modal]': 'hide'
};
}
initialize(o) {
const model = this.model;
@ -30,12 +32,12 @@ export default Backbone.View.extend({
this.listenTo(model, 'change:open', this.updateOpen);
this.listenTo(model, 'change:title', this.updateTitle);
this.listenTo(model, 'change:content', this.updateContent);
},
}
onClick(e) {
const bkd = this.config.backdrop;
bkd && e.target === this.el && this.hide();
},
}
/**
* Returns collector element
@ -46,7 +48,7 @@ export default Backbone.View.extend({
if (!this.$collector)
this.$collector = this.$el.find('.' + this.pfx + 'collector');
return this.$collector;
},
}
/**
* Returns content element
@ -61,7 +63,7 @@ export default Backbone.View.extend({
}
return this.$content;
},
}
/**
* Returns title element
@ -71,7 +73,7 @@ export default Backbone.View.extend({
getTitle() {
if (!this.$title) this.$title = this.$el.find('.' + this.pfx + 'title');
return this.$title.get(0);
},
}
/**
* Update content
@ -84,7 +86,7 @@ export default Backbone.View.extend({
const body = this.model.get('content');
children.length && coll.append(children);
content.empty().append(body);
},
}
/**
* Update title
@ -93,7 +95,7 @@ export default Backbone.View.extend({
updateTitle() {
var title = this.getTitle();
if (title) title.innerHTML = this.model.get('title');
},
}
/**
* Update open
@ -101,24 +103,23 @@ export default Backbone.View.extend({
* */
updateOpen() {
this.el.style.display = this.model.get('open') ? '' : 'none';
},
}
/**
* Hide modal
* @private
* */
hide() {
this.model.set('open', 0);
},
this.model.close();
}
/**
* Show modal
* @private
* */
show(opts = {}) {
this.model.set('open', 1);
this.updateAttr(opts.attributes);
},
show() {
this.model.open();
}
updateAttr(attr) {
const { pfx, $el, el } = this;
@ -128,7 +129,7 @@ export default Backbone.View.extend({
...(attr || {}),
class: `${pfx}container ${(attr && attr.class) || ''}`.trim()
});
},
}
render() {
const el = this.$el;
@ -140,4 +141,4 @@ export default Backbone.View.extend({
this.updateOpen();
return this;
}
});
}

19
src/navigator/index.js

@ -22,14 +22,6 @@ export default () => {
},
onLoad() {
const ItemView = View.extend(config.extend);
layers = new ItemView({
ItemView,
level: 0,
config,
opened: config.opened || {},
model: em.get('DomComponents').getWrapper()
});
em && em.on('component:selected', this.componentChanged);
this.componentChanged();
},
@ -60,7 +52,7 @@ export default () => {
* @return {Component}
*/
getRoot() {
return layers.model;
return layers && layers.model;
},
/**
@ -96,6 +88,15 @@ export default () => {
},
render() {
const ItemView = View.extend(config.extend);
layers && layers.remove();
layers = new ItemView({
ItemView,
level: 0,
config,
opened: config.opened || {},
model: em.get('DomComponents').getWrapper()
});
return layers.render().el;
},

13
src/panels/index.js

@ -57,10 +57,6 @@ export default () => {
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
panels = new Panels(c.defaults);
PanelsViewObj = new PanelsView({
collection: panels,
config: c
});
return this;
},
@ -77,7 +73,7 @@ export default () => {
* @return {HTMLElement}
*/
getPanelsEl() {
return PanelsViewObj.el;
return PanelsViewObj && PanelsViewObj.el;
},
/**
@ -205,6 +201,11 @@ export default () => {
* @private
*/
render() {
PanelsViewObj && PanelsViewObj.remove();
PanelsViewObj = new PanelsView({
collection: panels,
config: c
});
return PanelsViewObj.render().el;
},
@ -235,7 +236,7 @@ export default () => {
destroy() {
panels.reset();
panels.stopListening();
PanelsViewObj.remove();
PanelsViewObj && PanelsViewObj.remove();
[c, panels, PanelsViewObj].forEach(i => (i = {}));
},

3
src/rich_text_editor/index.js

@ -27,7 +27,7 @@
*/
import RichTextEditor from './model/RichTextEditor';
import { on, off } from 'utils/mixins';
import { on, hasWin } from 'utils/mixins';
import defaults from './config/config';
export default () => {
@ -75,6 +75,7 @@ export default () => {
this.pfx = config.stylePrefix;
actions = config.actions || [];
if (!hasWin()) return this;
toolbar = document.createElement('div');
toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`;
globalRte = this.initRte(document.createElement('div'));

26
src/selector_manager/index.js

@ -99,11 +99,6 @@ export default config => {
c.stylePrefix = ppfx + c.stylePrefix;
}
this.selectorTags = new ClassTagsView({
collection: new Selectors([], { em, config: c }),
config: c
});
// Global selectors container
selectors = new Selectors(c.selectors);
selectors.on('add', model => em.trigger('selector:add', model));
@ -133,6 +128,7 @@ export default config => {
select(value, opts = {}) {
const targets = Array.isArray(value) ? value : [value];
const toSelect = this.em.get('StyleManager').setTarget(targets, opts);
const selTags = this.selectorTags;
const res = toSelect
.filter(i => i)
.map(sel =>
@ -142,7 +138,7 @@ export default config => {
? sel
: sel.getSelectorsString()
);
this.selectorTags.componentChanged({ targets: res });
selTags && selTags.componentChanged({ targets: res });
return this;
},
@ -305,19 +301,21 @@ export default config => {
* @private
*/
render(selectors) {
if (selectors) {
this.selectorTags = new ClassTagsView({
collection: new Selectors(selectors),
config: c
});
return this.selectorTags.render().el;
} else return this.selectorTags.render().el;
const { em, selectorTags } = this;
selectorTags && selectorTags.remove();
this.selectorTags = new ClassTagsView({
collection: new Selectors(selectors || [], { em, config: c }),
config: c
});
return this.selectorTags.render().el;
},
destroy() {
const { selectorTags } = this;
selectors.reset();
selectors.stopListening();
this.selectorTags.remove();
selectorTags && selectorTags.remove();
[c, selectors].forEach(i => (i = {}));
this.em = {};
this.selectorTags = {};

10
src/selector_manager/view/ClassTagsView.js

@ -67,11 +67,10 @@ export default Backbone.View.extend({
const coll = this.collection;
this.target = this.config.em;
this.em = em;
const emitter = this.getStyleEmitter();
const toList = 'component:toggled component:update:classes';
const toListCls = 'component:update:classes change:state';
this.listenTo(em, toList, this.componentChanged);
this.listenTo(emitter, 'update', this.componentChanged);
this.listenTo(em, 'styleManager:update', this.componentChanged);
this.listenTo(em, toListCls, this.__handleStateChange);
this.listenTo(em, 'styleable:change change:device', this.checkSync); // component:styleUpdate
this.listenTo(coll, 'add', this.addNew);
@ -116,13 +115,6 @@ export default Backbone.View.extend({
});
},
getStyleEmitter() {
const { em } = this;
const sm = em && em.get('StyleManager');
const emitter = sm && sm.getEmitter();
return emitter || {};
},
/**
* Triggered when a tag is removed from collection
* @param {Object} model Removed model

32
src/storage_manager/index.js

@ -239,9 +239,9 @@ export default () => {
* });
* */
load(keys, clb) {
var st = this.get(this.getCurrent());
var keysF = [];
var result = {};
const st = this.get(this.getCurrent());
const keysF = [];
let result = {};
if (typeof keys === 'string') keys = [keys];
this.onStart('load', keys);
@ -254,13 +254,7 @@ export default () => {
st.load(
keysF,
res => {
// Restore keys name
var reg = new RegExp('^' + c.id + '');
for (var itemKey in res) {
var itemKeyR = itemKey.replace(reg, '');
result[itemKeyR] = res[itemKey];
}
result = this.__clearKeys(res);
this.onAfter('load', result);
clb && clb(result);
this.onEnd('load', result);
@ -275,6 +269,24 @@ export default () => {
}
},
/**
* Restore key names
* @param {Object} data
* @returns {Object}
* @private
*/
__clearKeys(data = {}) {
const result = {};
const reg = new RegExp('^' + c.id + '');
for (let itemKey in data) {
const itemKeyR = itemKey.replace(reg, '');
result[itemKeyR] = data[itemKey];
}
return result;
},
/**
* Load default storages
* @return {this}

52
src/storage_manager/model/LocalStorage.js

@ -1,6 +1,7 @@
import Backbone from 'backbone';
import { Model } from 'backbone';
import { hasWin } from 'utils/mixins';
export default Backbone.Model.extend({
export default Model.extend({
defaults: {
checkLocal: true
},
@ -8,31 +9,28 @@ export default Backbone.Model.extend({
/**
* @private
*/
store(data, clb) {
this.checkStorageEnvironment();
for (var key in data) localStorage.setItem(key, data[key]);
if (typeof clb == 'function') {
clb();
store(data, clb = () => {}) {
if (this.hasLocal()) {
for (let key in data) localStorage.setItem(key, data[key]);
}
clb && clb();
},
/**
* @private
*/
load(keys, clb) {
this.checkStorageEnvironment();
var result = {};
for (var i = 0, len = keys.length; i < len; i++) {
var value = localStorage.getItem(keys[i]);
if (value) result[keys[i]] = value;
load(keys, clb = () => {}) {
const result = {};
if (this.hasLocal()) {
for (let i = 0, len = keys.length; i < len; i++) {
const value = localStorage.getItem(keys[i]);
if (value) result[keys[i]] = value;
}
}
if (typeof clb == 'function') {
clb(result);
}
clb && clb(result);
return result;
},
@ -41,9 +39,9 @@ export default Backbone.Model.extend({
* @private
*/
remove(keys) {
this.checkStorageEnvironment();
if (!this.hasLocal()) return;
for (var i = 0, len = keys.length; i < len; i++)
for (let i = 0, len = keys.length; i < len; i++)
localStorage.removeItem(keys[i]);
},
@ -51,8 +49,14 @@ export default Backbone.Model.extend({
* Check storage environment
* @private
* */
checkStorageEnvironment() {
if (this.get('checkLocal') && !localStorage)
console.warn("Your browser doesn't support localStorage");
hasLocal() {
const win = hasWin();
if (this.get('checkLocal') && (!win || !localStorage)) {
win && console.warn("Your browser doesn't support localStorage");
return false;
}
return true;
}
});

13
src/style_manager/index.js

@ -75,11 +75,6 @@ export default () => {
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
properties = new Properties();
sectors = new Sectors([], c);
SectView = new SectorsView({
collection: sectors,
target: c.em,
config: c
});
return this;
},
@ -427,6 +422,12 @@ export default () => {
* @private
* */
render() {
SectView && SectView.remove();
SectView = new SectorsView({
collection: sectors,
target: c.em,
config: c
});
return SectView.render().el;
},
@ -440,7 +441,7 @@ export default () => {
coll.reset();
coll.stopListening();
});
SectView.remove();
SectView && SectView.remove();
[c, properties, sectors, SectView].forEach(i => (i = {}));
this.em = {};
}

3
src/style_manager/model/PropertyInteger.js

@ -1,6 +1,7 @@
import { isUndefined } from 'underscore';
import Property from './Property';
import InputNumber from 'domain_abstract/ui/InputNumber';
import { hasWin } from 'utils/mixins';
export default Property.extend({
defaults: {
@ -25,7 +26,7 @@ export default Property.extend({
Property.callParentInit(Property, this, props, opts);
const unit = this.get('unit');
const units = this.get('units');
this.input = new InputNumber({ model: this });
this.input = hasWin() && new InputNumber({ model: this });
if (units.length && !unit) {
this.set('unit', units[0]);

1
src/style_manager/view/SectorsView.js

@ -114,6 +114,7 @@ export default Backbone.View.extend({
pt.targets = targets.map(t => sm.getModelToStyle(t)).filter(Boolean);
}
pt.trigger('update');
em.trigger('styleManager:update');
},
/**

40
src/trait_manager/index.js

@ -1,14 +1,31 @@
import { defaults, isElement } from 'underscore';
import defaultOpts from './config/config';
import TraitsView from './view/TraitsView';
import TraitView from './view/TraitView';
import TraitSelectView from './view/TraitSelectView';
import TraitCheckboxView from './view/TraitCheckboxView';
import TraitNumberView from './view/TraitNumberView';
import TraitColorView from './view/TraitColorView';
import TraitButtonView from './view/TraitButtonView';
export default () => {
let c = {};
let types = {};
let TraitsViewer;
const typesDef = {
text: TraitView,
number: TraitNumberView,
select: TraitSelectView,
checkbox: TraitCheckboxView,
color: TraitColorView,
button: TraitButtonView
};
return {
TraitsView,
types,
/**
* Name of the module
* @type {String}
@ -33,12 +50,8 @@ export default () => {
c = config;
defaults(c, defaultOpts);
const ppfx = c.pStylePrefix;
types = { ...typesDef };
ppfx && (c.stylePrefix = `${ppfx}${c.stylePrefix}`);
TraitsViewer = new TraitsView({
collection: [],
editor: c.em,
config: c
});
return this;
},
@ -66,8 +79,8 @@ export default () => {
* @param {Object} methods Object representing the trait
*/
addType(name, trait) {
var itemView = TraitsViewer.itemView;
TraitsViewer.itemsView[name] = itemView.extend(trait);
const baseView = types.text;
types[name] = baseView.extend(trait);
},
/**
@ -76,16 +89,23 @@ export default () => {
* @return {Object}
*/
getType(name) {
return TraitsViewer.itemsView[name];
return types[name];
},
render() {
TraitsViewer && TraitsViewer.remove();
TraitsViewer = new TraitsView({
collection: [],
editor: c.em,
config: c
});
TraitsViewer.itemsView = types;
return TraitsViewer.render().el;
},
destroy() {
TraitsViewer.remove();
[c, TraitsViewer].forEach(i => (i = {}));
TraitsViewer && TraitsViewer.remove();
[c, types, TraitsViewer].forEach(i => (i = {}));
}
};
};

14
src/trait_manager/view/TraitsView.js

@ -1,25 +1,11 @@
import DomainViews from 'domain_abstract/view/DomainViews';
import TraitView from './TraitView';
import TraitSelectView from './TraitSelectView';
import TraitCheckboxView from './TraitCheckboxView';
import TraitNumberView from './TraitNumberView';
import TraitColorView from './TraitColorView';
import TraitButtonView from './TraitButtonView';
export default DomainViews.extend({
ns: 'Traits',
itemView: TraitView,
reuseView: 1,
itemsView: {
text: TraitView,
number: TraitNumberView,
select: TraitSelectView,
checkbox: TraitCheckboxView,
color: TraitColorView,
button: TraitButtonView
},
initialize(o = {}) {
const config = o.config || {};
this.config = config;

2
src/utils/ColorPicker.js

@ -4,9 +4,11 @@
// https://github.com/bgrins/spectrum
// Author: Brian Grinstead
// License: MIT
import { hasWin } from 'utils/mixins';
export default function($, undefined) {
'use strict';
if (!hasWin()) return;
var defaultOpts = {
// Callbacks

2
src/utils/extender.js

@ -1,7 +1,7 @@
import { isObject, isString, each, isUndefined } from 'underscore';
export default ({ $ }) => {
if ($ && $.prototype.constructor.name !== 'jQuery') {
if ($ && $.prototype && $.prototype.constructor.name !== 'jQuery') {
const fn = $.fn;
// Additional helpers

9
src/utils/fetch.js

@ -1,8 +1,11 @@
import Promise from 'promise-polyfill';
import { hasWin } from 'utils/mixins';
window.Promise = window.Promise || Promise;
if (hasWin()) {
window.Promise = window.Promise || Promise;
}
export default (typeof fetch == 'function'
export default typeof fetch == 'function'
? fetch.bind()
: (url, options) => {
return new Promise((res, rej) => {
@ -30,4 +33,4 @@ export default (typeof fetch == 'function'
// Include body only if present
options.body ? req.send(options.body) : req.send();
});
});
};

336
src/utils/keymaster.js

@ -0,0 +1,336 @@
// The initial version of this library was borrowed from https://github.com/madrobby/keymaster
// and adapted to the GrapesJS's need
var k,
_handlers = {},
_mods = {
16: false,
18: false,
17: false,
91: false
},
_scope = 'all',
// modifier keys
_MODIFIERS = {
'⇧': 16,
shift: 16,
'⌥': 18,
alt: 18,
option: 18,
'⌃': 17,
ctrl: 17,
control: 17,
'⌘': 91,
command: 91
},
// special keys
_MAP = {
backspace: 8,
tab: 9,
clear: 12,
enter: 13,
return: 13,
esc: 27,
escape: 27,
space: 32,
left: 37,
up: 38,
right: 39,
down: 40,
del: 46,
delete: 46,
home: 36,
end: 35,
pageup: 33,
pagedown: 34,
',': 188,
'.': 190,
'/': 191,
'`': 192,
'-': 189,
'=': 187,
';': 186,
"'": 222,
'[': 219,
']': 221,
'\\': 220
},
code = function(x) {
return _MAP[x] || x.toUpperCase().charCodeAt(0);
},
_downKeys = [];
for (k = 1; k < 20; k++) _MAP['f' + k] = 111 + k;
// IE doesn't support Array#indexOf, so have a simple replacement
function index(array, item) {
var i = array.length;
while (i--) if (array[i] === item) return i;
return -1;
}
// for comparing mods before unassignment
function compareArray(a1, a2) {
if (a1.length != a2.length) return false;
for (var i = 0; i < a1.length; i++) {
if (a1[i] !== a2[i]) return false;
}
return true;
}
var modifierMap = {
16: 'shiftKey',
18: 'altKey',
17: 'ctrlKey',
91: 'metaKey'
};
function updateModifierKey(event) {
for (k in _mods) _mods[k] = event[modifierMap[k]];
}
// handle keydown event
function dispatch(event) {
var key, handler, k, i, modifiersMatch, scope;
key = event.keyCode;
if (index(_downKeys, key) == -1) {
_downKeys.push(key);
}
// if a modifier key, set the key.<modifierkeyname> property to true and return
if (key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko
if (key in _mods) {
_mods[key] = true;
// 'assignKey' from inside this closure is exported to window.key
for (k in _MODIFIERS) if (_MODIFIERS[k] == key) assignKey[k] = true;
return;
}
updateModifierKey(event);
// see if we need to ignore the keypress (filter() can can be overridden)
// by default ignore key presses if a select, textarea, or input is focused
if (!assignKey.filter.call(this, event)) return;
// abort if no potentially matching shortcuts found
if (!(key in _handlers)) return;
scope = getScope();
// for each potential shortcut
for (i = 0; i < _handlers[key].length; i++) {
handler = _handlers[key][i];
// see if it's in the current scope
if (handler.scope == scope || handler.scope == 'all') {
// check if modifiers match if any
modifiersMatch = handler.mods.length > 0;
for (k in _mods)
if (
(!_mods[k] && index(handler.mods, +k) > -1) ||
(_mods[k] && index(handler.mods, +k) == -1)
)
modifiersMatch = false;
// call the handler and stop the event if neccessary
if (
(handler.mods.length == 0 &&
!_mods[16] &&
!_mods[18] &&
!_mods[17] &&
!_mods[91]) ||
modifiersMatch
) {
if (handler.method(event, handler) === false) {
if (event.preventDefault) event.preventDefault();
else event.returnValue = false;
if (event.stopPropagation) event.stopPropagation();
if (event.cancelBubble) event.cancelBubble = true;
}
}
}
}
}
// unset modifier keys on keyup
function clearModifier(event) {
var key = event.keyCode,
k,
i = index(_downKeys, key);
// remove key from _downKeys
if (i >= 0) {
_downKeys.splice(i, 1);
}
if (key == 93 || key == 224) key = 91;
if (key in _mods) {
_mods[key] = false;
for (k in _MODIFIERS) if (_MODIFIERS[k] == key) assignKey[k] = false;
}
}
function resetModifiers() {
for (k in _mods) _mods[k] = false;
for (k in _MODIFIERS) assignKey[k] = false;
}
// parse and assign shortcut
function assignKey(key, scope, method) {
var keys, mods;
keys = getKeys(key);
if (method === undefined) {
method = scope;
scope = 'all';
}
// for each shortcut
for (var i = 0; i < keys.length; i++) {
// set modifier keys if any
mods = [];
key = keys[i].split('+');
if (key.length > 1) {
mods = getMods(key);
key = [key[key.length - 1]];
}
// convert to keycode and...
key = key[0];
key = code(key);
// ...store handler
if (!(key in _handlers)) _handlers[key] = [];
_handlers[key].push({
shortcut: keys[i],
scope: scope,
method: method,
key: keys[i],
mods: mods
});
}
}
// unbind all handlers for given key in current scope
function unbindKey(key, scope) {
var multipleKeys,
keys,
mods = [],
i,
j,
obj;
multipleKeys = getKeys(key);
for (j = 0; j < multipleKeys.length; j++) {
keys = multipleKeys[j].split('+');
if (keys.length > 1) {
mods = getMods(keys);
}
key = keys[keys.length - 1];
key = code(key);
if (scope === undefined) {
scope = getScope();
}
if (!_handlers[key]) {
return;
}
for (i = 0; i < _handlers[key].length; i++) {
obj = _handlers[key][i];
// only clear handlers if correct scope and mods match
if (obj.scope === scope && compareArray(obj.mods, mods)) {
_handlers[key][i] = {};
}
}
}
}
// Returns true if the key with code 'keyCode' is currently down
// Converts strings into key codes.
function isPressed(keyCode) {
if (typeof keyCode == 'string') {
keyCode = code(keyCode);
}
return index(_downKeys, keyCode) != -1;
}
function getPressedKeyCodes() {
return _downKeys.slice(0);
}
function filter(event) {
var tagName = (event.target || event.srcElement).tagName;
// ignore keypressed in any elements that support keyboard data input
return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA');
}
// initialize key.<modifier> to false
for (k in _MODIFIERS) assignKey[k] = false;
// set current scope (default 'all')
function setScope(scope) {
_scope = scope || 'all';
}
function getScope() {
return _scope || 'all';
}
// delete all handlers for a given scope
function deleteScope(scope) {
var key, handlers, i;
for (key in _handlers) {
handlers = _handlers[key];
for (i = 0; i < handlers.length; ) {
if (handlers[i].scope === scope) handlers.splice(i, 1);
else i++;
}
}
}
// abstract key logic for assign and unassign
function getKeys(key) {
var keys;
key = key.replace(/\s/g, '');
keys = key.split(',');
if (keys[keys.length - 1] == '') {
keys[keys.length - 2] += ',';
}
return keys;
}
// abstract mods logic for assign and unassign
function getMods(key) {
var mods = key.slice(0, key.length - 1);
for (var mi = 0; mi < mods.length; mi++) mods[mi] = _MODIFIERS[mods[mi]];
return mods;
}
// cross-browser events
function addEvent(object, event, method) {
if (object.addEventListener) object.addEventListener(event, method, false);
else if (object.attachEvent)
object.attachEvent('on' + event, function() {
method(window.event);
});
}
// set window.key and window.key.set/get/deleteScope, and the default filter
assignKey.setScope = setScope;
assignKey.getScope = getScope;
assignKey.deleteScope = deleteScope;
assignKey.filter = filter;
assignKey.isPressed = isPressed;
assignKey.getPressedKeyCodes = getPressedKeyCodes;
assignKey.unbind = unbindKey;
assignKey.init = win => {
// set the handlers globally on document
// Passing _scope to a callback to ensure it remains the same by execution. Fixes #48
addEvent(win.document, 'keydown', function(event) {
dispatch(event);
});
addEvent(win.document, 'keyup', clearModifier);
addEvent(win, 'focus', resetModifiers);
};
export default assignKey;

4
src/utils/mixins.js

@ -1,6 +1,8 @@
import { keys, isUndefined, isElement, isArray } from 'underscore';
const elProt = window.Element.prototype;
export const hasWin = () => typeof window !== 'undefined';
const elProt = hasWin() ? window.Element.prototype : {};
const matches =
elProt.matches ||
elProt.webkitMatchesSelector ||

3
src/utils/polyfills.js

@ -2,6 +2,7 @@
* File made for IE/Edge support
* https://github.com/artf/grapesjs/issues/214
*/
import { hasWin } from './mixins';
export default () => {
/**
@ -27,7 +28,7 @@ export default () => {
return !!match;
};
if (isIE()) {
if (hasWin() && isIE()) {
const originalCreateHTMLDocument =
DOMImplementation.prototype.createHTMLDocument;
DOMImplementation.prototype.createHTMLDocument = title => {

2
test/specs/css_composer/e2e/CssComposer.js

@ -17,7 +17,6 @@ describe('E2E tests', () => {
});
beforeEach(done => {
//this.timeout(5000);
gjs = grapesjs.init({
stylePrefix: '',
storageManager: { autoload: 0, type: 'none' },
@ -28,7 +27,6 @@ describe('E2E tests', () => {
clsm = gjs.SelectorManager;
domc = gjs.DomComponents;
fixture.empty().appendTo(fixtures);
gjs.render();
rulesSet = [
{ selectors: [{ name: 'test1' }, { name: 'test2' }] },
{ selectors: [{ name: 'test2' }, { name: 'test3' }] },

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

@ -23,6 +23,7 @@ describe('ComponentView', () => {
view = new ComponentView({
model
});
em.get('StyleManager').render(); // Enable to listen em.setState
document.body.innerHTML = '<div id="fixtures"></div>';
fixtures = document.body.querySelector('#fixtures');
fixtures.appendChild(view.render().el);

62
test/specs/grapesjs/headless.js

@ -0,0 +1,62 @@
/**
* @jest-environment node
*/
describe('GrapesJS Headless', () => {
test('Can init and destroy an editor', () => {
const editor = grapesjs.init({ headless: true });
expect(editor).toBeTruthy();
editor.destroy();
});
describe('Headless operations', () => {
let editor;
const cmpObj = {
attributes: { class: 'cls', test: 'value' },
components: { type: 'textnode', content: 'Test' }
};
const cmpStr = '<div class="cls" test="value">Test</div>';
const styleObj = {
selectors: [{ name: 'cls' }],
style: { color: 'red' }
};
const styleStr = '.cls{color:red;}';
beforeEach(() => {
editor = grapesjs.init({ headless: true, protectedCss: '' });
});
afterEach(() => {
editor.destroy();
});
test('Add components', () => {
const res = editor.addComponents(cmpObj);
expect(res.length).toBe(1);
const comp = res[0];
expect(comp.toHTML()).toBe(cmpStr);
expect(editor.Selectors.getAll().length).toBe(1); // 1 selector is created
expect(editor.Css.getAll().length).toBe(0); // No CSS
expect(editor.getHtml()).toBe(cmpStr);
expect(editor.getCss()).toBe(''); // same as default
});
test('Add styles', () => {
const res = editor.addStyle(styleObj);
expect(res.length).toBe(1);
const rule = res[0];
expect(rule.toCSS()).toBe(styleStr);
expect(editor.Selectors.getAll().length).toBe(1); // 1 selector is created
});
test('Load data', () => {
editor.loadData({
components: [cmpObj],
styles: [styleObj]
});
expect(editor.Selectors.getAll().length).toBe(1);
expect(editor.getHtml()).toBe(cmpStr);
expect(editor.getCss()).toBe(styleStr);
});
});
});

1
test/specs/keymaps/index.js

@ -1,5 +1,4 @@
import Editor from 'editor/index';
import Keymaps from 'keymaps';
describe('Keymaps', () => {
describe('Main', () => {

1
test/specs/modal/index.js

@ -1,5 +1,4 @@
import Modal from 'modal_dialog';
import ModalView from './view/ModalView';
describe('Modal dialog', () => {
describe('Main', () => {

Loading…
Cancel
Save