Browse Source

Merge pull request #3165 from artf/symbols

Symbols
pull/3167/head
Artur Arseniev 6 years ago
committed by GitHub
parent
commit
78386c9621
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      dist/css/grapes.min.css
  2. 7574
      dist/grapes.js
  3. 6
      dist/grapes.min.js
  4. 2
      dist/grapes.min.js.map
  5. 2
      package-lock.json
  6. 2
      package.json
  7. 2
      src/commands/view/SelectComponent.js
  8. 20
      src/css_composer/model/CssRule.js
  9. 2
      src/css_composer/model/CssRules.js
  10. 190
      src/dom_components/model/Component.js
  11. 42
      src/dom_components/model/Components.js
  12. 2
      src/dom_components/view/ComponentTextView.js
  13. 35
      src/selector_manager/model/Selector.js
  14. 2
      src/undo_manager/index.js
  15. 2
      src/utils/mixins.js
  16. 11
      test/specs/css_composer/e2e/CssComposer.js
  17. 6
      test/specs/dom_components/model/Component.js
  18. 38
      test/specs/selector_manager/e2e/ClassManager.js

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

7574
dist/grapes.js

File diff suppressed because it is too large

6
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

2
package-lock.json

@ -1,6 +1,6 @@
{
"name": "grapesjs",
"version": "0.16.27",
"version": "0.16.28",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

2
package.json

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

2
src/commands/view/SelectComponent.js

@ -731,9 +731,9 @@ export default {
stop(ed, sender, opts = {}) {
const { em, editor } = this;
this.onHovered(); // force to hide toolbar
this.stopSelectComponent();
!opts.preserveSelected && em.setSelected(null);
this.onOut();
this.toggleToolsEl();
editor && editor.stopCommand('resize');
}

20
src/css_composer/model/CssRule.js

@ -55,12 +55,22 @@ export default Backbone.Model.extend(Styleable).extend({
ensureSelectors() {
const { em } = this;
const result = [];
const sm = em && em.get('SelectorManager');
const selectors = this.getSelectors();
const toInit = Array.isArray(selectors);
selectors.forEach(sel => result.push(sm ? sm.add(sel) : sel));
toInit && this.set('selectors', new Selectors(result));
const toListen = [this, 'change:selectors', this.ensureSelectors];
let sels = this.getSelectors();
this.stopListening(...toListen);
if (sels.models) {
sels = [...sels.models];
}
if (Array.isArray(sels)) {
const res = sels.filter(i => i).map(i => (sm ? sm.add(i) : i));
sels = new Selectors(res);
}
this.set('selectors', sels);
this.listenTo(...toListen);
},
/**

2
src/css_composer/model/CssRules.js

@ -16,7 +16,7 @@ export default Backbone.Collection.extend({
},
onAdd(model) {
model.ensureSelectors();
model.ensureSelectors(); // required for undo
},
onRemove(removed) {

190
src/dom_components/model/Component.js

@ -9,9 +9,10 @@ import {
isString,
forEach,
result,
bindAll,
keys
} from 'underscore';
import { shallowDiff, capitalize } from 'utils/mixins';
import { shallowDiff, capitalize, isEmptyObj } from 'utils/mixins';
import Styleable from 'domain_abstract/model/Styleable';
import Backbone from 'backbone';
import Components from './Components';
@ -142,6 +143,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
removed() {},
initialize(props = {}, opt = {}) {
bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps');
const em = opt.em;
// Propagate properties from parent if indicated
@ -190,6 +192,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
this.listenTo(this, 'change:attributes:id', this._idUpdated);
this.set('status', '');
this.views = [];
this.__isSymbol() && this.__initSymb();
// Register global updates for collection properties
['classes', 'traits', 'components'].forEach(name => {
@ -320,6 +323,26 @@ const Component = Backbone.Model.extend(Styleable).extend(
return parent;
},
/**
* The method returns a Boolean value indicating whether the passed
* component is a descendant of a given component
* @param {Component} component Component to check
* @returns {Boolean}
*/
contains(component) {
let result = !1;
if (!component) return result;
const contains = components => {
!result &&
components.forEach(item => {
if (item === component) result = !0;
!result && contains(item.components());
});
};
contains(this.components());
return result;
},
/**
* Once the tag is updated I have to remove the node and replace it
* @private
@ -552,6 +575,95 @@ const Component = Backbone.Model.extend(Styleable).extend(
return classStr ? classStr.split(' ') : [];
},
__initSymb() {
if (this.__symbReady) return;
this.on('change', this.__upSymbProps);
this.__symbReady = 1;
},
__isSymbol() {
return isArray(this.get('__symbol'));
},
__isSymbolTop() {
const parent = this.parent();
return this.__isSymbol() && parent && !parent.__isSymbol();
},
__getSymbolOf() {
return this.get('__symbolOf');
},
__getSymbToUp() {
const symbol = this.get('__symbol');
return !this.__isSymbol()
? []
: symbol.filter(item => item.collection || item.prevColl);
},
__getSymbTop(opts) {
const isSymbol = this.__isSymbol();
let result = this;
let parent = this.parent(opts);
while (
parent &&
(isSymbol ? parent.__isSymbol() : parent.__getSymbolOf())
) {
result = parent;
parent = parent.parent(opts);
}
return result;
},
__upSymbProps() {
const changed = this.changedAttributes();
const attrs = changed.attributes || {};
delete changed.status;
delete changed.open;
delete changed.__symbol;
delete changed.__symbolOf;
delete changed.attributes;
delete attrs.id;
if (!isEmptyObj(attrs)) changed.attributes = attrs;
!isEmptyObj(changed) &&
this.__getSymbToUp().forEach(child => child.set(changed));
},
__upSymbCls() {
this.__getSymbToUp().forEach(child => {
child.set({ classes: this.get('classes') });
});
},
__upSymbComps(m, c, o) {
if (!o) {
// Reset
this.__getSymbToUp().forEach(item => {
const newMods = m.models.map(mod => mod.clone({ symbol: 1 }));
item.components().reset(newMods, c);
});
} else if (o.add) {
// Add
const addedInstances = m.__getSymbToUp();
// console.log('Added', m.getId(), m.toHTML(), o, 'toUp', addedInstances);
this.__getSymbToUp().forEach(symbInst => {
const symbTop = symbInst.__getSymbTop();
const inner = addedInstances.filter(addedInst => {
const addedTop = addedInst.__getSymbTop({ prev: 1 });
return symbTop && addedTop && addedTop === symbTop;
})[0];
const toAppend = inner || m.clone({ symbol: 1 });
// console.log('Added inner', toAppend.getId(), toAppend.toHTML(), inner);
symbInst.append(toAppend, o);
});
} else {
// Remove
m.__getSymbToUp().forEach(item => item.remove(o));
}
},
initClasses() {
const event = 'change:classes';
const attrCls = this.get('attributes').class || [];
@ -563,6 +675,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
const selectors = new Selectors([]);
this.set('classes', selectors);
selectors.add(classes);
selectors.on('add remove reset', this.__upSymbCls);
this.listenTo(...toListen);
return this;
},
@ -583,6 +696,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
isFunction(components) ? components(this) : components,
this.opt
);
comps.on('add remove reset', this.__upSymbComps);
this.listenTo(...toListen);
return this;
},
@ -667,8 +781,8 @@ const Component = Backbone.Model.extend(Styleable).extend(
* component.parent();
* // -> Component
*/
parent() {
const coll = this.collection;
parent(opts = {}) {
const coll = this.collection || (opts.prev && this.prevColl);
return coll && coll.parent;
},
@ -838,23 +952,12 @@ const Component = Backbone.Model.extend(Styleable).extend(
* @private
*/
normalizeClasses(arr) {
var res = [];
const em = this.em;
if (!em) return;
var clm = em.get('SelectorManager');
const res = [];
const { em } = this;
const clm = em && em.get('SelectorManager');
if (!clm) return;
arr.forEach(val => {
var name = '';
if (typeof val === 'string') name = val;
else name = val.name;
var model = clm.add(name);
res.push(model);
});
if (arr.models) return [...arr.models];
arr.forEach(val => res.push(clm.add(val)));
return res;
},
@ -862,7 +965,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
* Override original clone method
* @private
*/
clone() {
clone(opt = {}) {
const em = this.em;
const attr = { ...this.attributes };
const opts = { ...this.opt };
@ -874,8 +977,12 @@ const Component = Backbone.Model.extend(Styleable).extend(
attr.classes = [];
attr.traits = [];
if (this.__isSymbolTop()) {
opt.symbol = 1;
}
this.get('components').each((md, i) => {
attr.components[i] = md.clone();
attr.components[i] = md.clone({ ...opt, _inner: 1 });
});
this.get('traits').each((md, i) => {
attr.traits[i] = md.clone();
@ -889,9 +996,6 @@ const Component = Backbone.Model.extend(Styleable).extend(
opts.collection = null;
const cloned = new this.constructor(attr, opts);
const event = 'component:clone';
em && em.trigger(event, cloned);
this.trigger(event, cloned);
// Clone component specific rules
const newId = `#${cloned.getId()}`;
@ -902,6 +1006,22 @@ const Component = Backbone.Model.extend(Styleable).extend(
cssc.getAll().add(newRule);
});
// Symbols
// If I clone an inner symbol, I have to reset it
cloned.unset('__symbol');
if (opt.symbol) {
// TODO Check if trying to clone a Symbol (check if parent is symbol)
const symbols = this.get('__symbol') || [];
symbols.push(cloned);
this.set('__symbol', symbols);
this.__initSymb();
cloned.set('__symbolOf', this);
}
const event = 'component:clone';
em && em.trigger(event, cloned);
this.trigger(event, cloned);
return cloned;
},
@ -1030,12 +1150,22 @@ const Component = Backbone.Model.extend(Styleable).extend(
* @return {Object}
* @private
*/
toJSON(...args) {
const obj = Backbone.Model.prototype.toJSON.apply(this, args);
toJSON(opts = {}) {
const obj = Backbone.Model.prototype.toJSON.call(this, opts);
obj.attributes = this.getAttributes();
delete obj.attributes.class;
delete obj.toolbar;
delete obj.traits;
delete obj.status;
if (!opts.keepSymbols) {
if (obj.__symbol) {
obj.__symbol = this.__getSymbToUp().map(i => i.getId());
}
if (obj.__symbolOf) {
obj.__symbolOf = obj.__symbolOf.getId();
}
}
if (this.em.getConfig('avoidDefaults')) {
this.getChangedProps(obj);
@ -1052,7 +1182,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
const defaults = result(this, 'defaults');
forEach(defaults, (value, key) => {
if (['type', 'content'].indexOf(key) === -1 && obj[key] === value) {
if (['type'].indexOf(key) === -1 && obj[key] === value) {
delete obj[key];
}
});
@ -1204,9 +1334,9 @@ const Component = Backbone.Model.extend(Styleable).extend(
* Remove the component
* @return {this}
*/
remove() {
remove(opts = {}) {
const coll = this.collection;
return coll && coll.remove(this);
return coll && coll.remove(this, opts);
},
/**
@ -1247,7 +1377,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
const list = Component.getList(this);
// If the ID already exists I need to rollback to the old one
if (list[id]) {
if (list[id] || (!id && idPrev)) {
return this.setId(idPrev, { idUpdate: 1 });
}

42
src/dom_components/model/Components.js

@ -6,7 +6,8 @@ import {
each,
includes,
extend,
flatten
flatten,
debounce
} from 'underscore';
let Component;
@ -58,8 +59,10 @@ export default Backbone.Collection.extend({
const { domc, em } = this;
const allByID = domc ? domc.allById() : {};
const isTemp = opts.temporary;
removed.prevColl = this; // This one is required for symbols
if (!opts.temporary) {
if (!isTemp) {
// Remove the component from the global list
const id = removed.getId();
const sels = em.get('SelectorManager').getAll();
@ -171,8 +174,9 @@ export default Backbone.Collection.extend({
.filter(i => i)
.map(model => this.processDef(model));
models = isMult ? flatten(models, 1) : models[0];
return Backbone.Collection.prototype.add.apply(this, [models, opt]);
const result = Backbone.Collection.prototype.add.apply(this, [models, opt]);
this.__firstAdd = result;
return result;
},
/**
@ -247,5 +251,33 @@ export default Backbone.Collection.extend({
model.setStyle({});
model.addClass(name);
}
}
this.__onAddEnd();
},
__onAddEnd: debounce(function() {
const { domc } = this;
const allComp = (domc && domc.allById()) || {};
const firstAdd = this.__firstAdd;
const toCheck = isArray(firstAdd) ? firstAdd : [firstAdd];
const silent = { silent: true };
const onAll = comps => {
comps.forEach(comp => {
const symbol = comp.get('__symbol');
const symbolOf = comp.get('__symbolOf');
if (symbol && isArray(symbol) && isString(symbol[0])) {
comp.set(
'__symbol',
symbol.map(smb => allComp[smb]).filter(i => i),
silent
);
}
if (isString(symbolOf)) {
comp.set('__symbolOf', allComp[symbolOf], silent);
}
onAll(comp.components());
});
};
onAll(toCheck);
})
});

2
src/dom_components/view/ComponentTextView.js

@ -131,8 +131,6 @@ export default ComponentView.extend({
model.get('components').each(model => clean(model));
};
// Avoid re-render on reset with silent option
!opts.silent && model.trigger('change:content', model, '', contentOpt);
comps.reset(content, opts);
comps.each(model => clean(model));
comps.trigger('resetNavigator');

35
src/selector_manager/model/Selector.js

@ -1,9 +1,11 @@
import Backbone from 'backbone';
import { result, forEach, keys } from 'underscore';
const TYPE_CLASS = 1;
const TYPE_ID = 2;
const { Model } = Backbone;
const Selector = Backbone.Model.extend(
const Selector = Model.extend(
{
idAttribute: 'name',
@ -43,6 +45,7 @@ const Selector = Backbone.Model.extend(
? escapeName(namePreEsc)
: Selector.escapeName(namePreEsc);
this.set('name', nameEsc);
this.em = config.em;
},
/**
@ -64,6 +67,36 @@ const Selector = Backbone.Model.extend(
}
return init + (escape ? escape(name) : name);
},
toJSON(opts = {}) {
const { em } = this;
let obj = Model.prototype.toJSON.call(this, [opts]);
const defaults = result(this, 'defaults');
if (em && em.getConfig('avoidDefaults')) {
forEach(defaults, (value, key) => {
if (obj[key] === value) {
delete obj[key];
}
});
if (obj.label === obj.name) {
delete obj.label;
}
const objLen = keys(obj).length;
if (objLen === 1 && obj.name) {
obj = obj.name;
}
if (objLen === 2 && obj.name && obj.type) {
obj = this.getFullName();
}
}
return obj;
}
},
{

2
src/undo_manager/index.js

@ -81,7 +81,7 @@ export default () => {
const result = {
object,
before: beforeCache,
after: object.toJSON()
after: object.toJSON({ keepSymbols: 1 })
};
beforeCache = null;
return result;

2
src/utils/mixins.js

@ -212,6 +212,7 @@ const getKeyChar = ev => String.fromCharCode(getKeyCode(ev));
const isEscKey = ev => getKeyCode(ev) === 27;
const isObject = val =>
val !== null && !Array.isArray(val) && typeof val === 'object';
const isEmptyObj = val => Object.keys(val).length <= 0;
const capitalize = str => str && str.charAt(0).toUpperCase() + str.substring(1);
const isComponent = obj => obj && obj.toHTML;
@ -245,6 +246,7 @@ export {
setViewEl,
appendStyles,
isObject,
isEmptyObj,
isComponent,
isRule
};

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

@ -128,16 +128,7 @@ describe('E2E tests', () => {
// ruleOut is a Model
ruleOut = JSON.parse(JSON.stringify(ruleOut));
var ruleResult = {
selectors: [
{
active: true,
label: 'test1',
name: 'test1',
type: clsm.Selector.TYPE_CLASS,
private: false,
protected: false
}
],
selectors: ['test1'],
style: {
color: 'red',
width: '10px'

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

@ -274,9 +274,9 @@ describe('Component', () => {
});
test('setAttributes overwrites correctly', () => {
obj.setAttributes({ id: 'test', 'data-test': 'value', a: 'b' });
obj.setAttributes({ 'data-test': 'value2' });
expect(obj.getAttributes()).toEqual({ 'data-test': 'value2' });
obj.setAttributes({ id: 'test', 'data-test': 'value', a: 'b', b: 'c' });
obj.setAttributes({ id: 'test2', 'data-test': 'value2' });
expect(obj.getAttributes()).toEqual({ id: 'test2', 'data-test': 'value2' });
});
test('append() returns always an array', () => {

38
test/specs/selector_manager/e2e/ClassManager.js

@ -1,3 +1,4 @@
import Selector from 'selector_manager/model/Selector';
import Selectors from 'selector_manager/model/Selectors';
import ClassTagsView from 'selector_manager/view/ClassTagsView';
@ -117,5 +118,42 @@ describe('E2E tests', () => {
tagEl.addNewTag('test2');
expect(spy.called).toEqual(true);
});
test('Selectors are properly transformed to JSON', () => {
const model = components.add({
classes: [
'test1',
'.test1a',
'#test2',
{ name: 'test3', label: 'test3' },
{ name: 'test4', label: 'test4a' },
{ name: 'test5' },
{ name: 'test6', type: Selector.TYPE_CLASS },
{ name: 'test7', type: Selector.TYPE_ID },
{ name: 'test8', type: Selector.TYPE_CLASS, protected: 1 },
{ name: 'test9', type: Selector.TYPE_ID, protected: 1 },
{ label: 'test10' },
{ label: 'test11', type: Selector.TYPE_ID },
{ label: 'test12', protected: 1 }
]
});
const modelTr = JSON.parse(JSON.stringify(model));
expect(modelTr.classes).toEqual([
'test1',
'test1a',
'#test2',
'test3',
{ name: 'test4', label: 'test4a' },
'test5',
'test6',
'#test7',
{ name: 'test8', protected: 1 },
{ name: 'test9', type: Selector.TYPE_ID, protected: 1 },
'test10',
'#test11',
{ name: 'test12', protected: 1 }
]);
});
});
});

Loading…
Cancel
Save