grapesjs-clot
Version:
Free and Open Source Web Builder Framework
359 lines (313 loc) • 11.5 kB
JavaScript
import Backbone from 'backbone';
import { isEmpty, isArray, isString, isFunction, each, includes, extend, flatten, debounce } from 'underscore';
import Component, { keySymbol, keySymbols } from './Component';
export const getComponentIds = (cmp, res = []) => {
if (!cmp) return [];
const cmps = isArray(cmp) || isFunction(cmp.map) ? cmp : [cmp];
cmps.map(cmp => {
res.push(cmp.getId());
getComponentIds(cmp.components().models, res);
});
return res;
};
export const setComponentIds = cmp => {
const cmps = isArray(cmp) || isFunction(cmp.map) ? cmp : [cmp];
cmps.map(cmp => {
let attributes = cmp.get('attributes');
attributes.id = cmp.getId();
cmp.set('attributes', attributes);
setComponentIds(cmp.components().models);
});
};
export const setComponentIdsWithArray = (cmp, idArray) => {
const cmps = isArray(cmp) || isFunction(cmp.map) ? cmp : [cmp];
cmps.map(cmp => {
let id = idArray.shift();
cmp.set('attributes', { id: id });
setComponentIdsWithArray(cmp.components().models, idArray);
});
};
export const setComponentRemoteUnSelected = (cmp, username) => {
const cmps = isArray(cmp) || isFunction(cmp.map) ? cmp : [cmp];
cmps.map(cmp => {
if (cmp.get('chooser') == username) {
cmp.set('status', '');
cmp.set('chooser', '');
}
setComponentRemoteUnSelected(cmp.components().models, username);
});
};
export const setComponentRemoteSelected = cmp => {
const cmps = isArray(cmp) || isFunction(cmp.map) ? cmp : [cmp];
cmps.map(cmp => {
if (cmp.get('chooser').length > 0) {
cmp.set('status', 'freezed-remote-selected');
}
setComponentRemoteSelected(cmp.components().models);
});
};
export const checkComponentsChooser = cmp => {
const cmps = isArray(cmp) || isFunction(cmp.map) ? cmp : [cmp];
cmps.map(cmp => {
if (cmp.get('chooser').length > 0) {
if (cmp.get('status') != 'selected' && cmp.get('status') != 'freezed-remote-selected') cmp.set('chooser', '');
}
checkComponentsChooser(cmp.components().models);
});
};
const getComponentsFromDefs = (items, all = {}, opts = {}) => {
const itms = isArray(items) ? items : [items];
return itms.map(item => {
const { attributes = {}, components, tagName } = item;
const { id } = attributes;
let result = item;
if (id && all[id]) {
result = all[id];
tagName && result.set({ tagName }, { ...opts, silent: true });
}
if (components) {
const newComponents = getComponentsFromDefs(components, all);
if (isFunction(result.components)) {
const cmps = result.components();
cmps.length > 0 && cmps.reset(newComponents, opts);
} else {
result.components = newComponents;
}
}
return result;
});
};
export default Backbone.Collection.extend({
initialize(models, opt = {}) {
this.opt = opt;
this.listenTo(this, 'add', this.onAdd);
this.listenTo(this, 'remove', this.removeChildren);
this.listenTo(this, 'reset', this.resetChildren);
const { em, config } = opt;
this.config = config;
this.em = em;
this.domc = opt.domc || (em && em.get('DomComponents'));
},
resetChildren(models, opts = {}) {
const coll = this;
const prev = opts.previousModels || [];
const toRemove = prev.filter(prev => !models.get(prev.cid));
const newIds = getComponentIds(models);
opts.keepIds = getComponentIds(prev).filter(pr => newIds.indexOf(pr) >= 0);
toRemove.forEach(md => this.removeChildren(md, coll, opts));
models.each(model => this.onAdd(model));
},
resetFromString(input = '', opts = {}) {
//console.log('dom_components/model/Components => resetFromString start');
opts.keepIds = getComponentIds(this);
const { domc } = this;
const allByID = domc?.allById() || {};
const parsed = this.parseString(input, opts);
const cmps = isArray(parsed) ? parsed : [parsed];
const newCmps = getComponentsFromDefs(cmps, allByID, opts);
this.reset(newCmps, opts);
this.em?.trigger('component:content', this.parent, opts, input);
//console.log('dom_components/model/Components => resetFromString end');
},
removeChildren(removed, coll, opts = {}) {
// Removing a parent component can cause this function
// to be called with an already removed child element
//console.log('Components.js => removeChildren start');
if (!removed) {
return;
}
const { domc, em } = this;
const isTemp = opts.temporary || opts.fromUndo;
removed.prevColl = this; // This one is required for symbols
if (!isTemp) {
// Remove the component from the global list
const id = removed.getId();
const sels = em.get('SelectorManager').getAll();
const rules = em.get('CssComposer').getAll();
const canRemoveStyle = (opts.keepIds || []).indexOf(id) < 0;
const allByID = domc ? domc.allById() : {};
delete allByID[id];
// Remove all component related styles
const rulesRemoved = canRemoveStyle
? rules.remove(
rules.filter(r => r.getSelectors().getFullString() === `#${id}`),
opts
)
: [];
// Clean selectors
sels.remove(rulesRemoved.map(rule => rule.getSelectors().at(0)));
if (!removed.opt.temporary) {
em.get('Commands').run('core:component-style-clear', {
target: removed,
});
removed.removed();
removed.trigger('removed');
em.trigger('component:remove', removed);
}
const inner = removed.components();
inner.forEach(it => this.removeChildren(it, coll, opts));
}
// Remove stuff registered in DomComponents.handleChanges
const inner = removed.components();
em.stopListening(inner);
em.stopListening(removed);
em.stopListening(removed.get('classes'));
removed.__postRemove();
//console.log('Components.js => removeChildren end');
},
model(attrs, options) {
const { opt } = options.collection;
const { em } = opt;
let model;
const df = em.get('DomComponents').componentTypes;
options.em = em;
options.config = opt.config;
options.componentTypes = df;
options.domc = opt.domc;
for (let it = 0; it < df.length; it++) {
const dfId = df[it].id;
if (dfId == attrs.type) {
model = df[it].model;
break;
}
}
// If no model found, get the default one
if (!model) {
model = df[df.length - 1].model;
em &&
attrs.type &&
em.logWarning(`Component type '${attrs.type}' not found`, {
attrs,
options,
});
}
return new model(attrs, options);
},
parseString(value, opt = {}) {
const { em, domc } = this;
const cssc = em.get('CssComposer');
const parsed = em.get('Parser').parseHtml(value);
// We need this to avoid duplicate IDs
Component.checkId(parsed.html, parsed.css, domc.componentsById, opt);
if (parsed.css && cssc && !opt.temporary) {
const { at, ...optsToPass } = opt;
cssc.addCollection(parsed.css, {
...optsToPass,
extend: 1,
});
}
return parsed.html;
},
add(models, opt = {}) {
//console.trace();
//console.log('Components.js => add start');
opt.keepIds = [...(opt.keepIds || []), ...getComponentIds(opt.previousModels)];
if (isString(models)) {
models = this.parseString(models, opt);
} else if (isArray(models)) {
models = [...models];
models.forEach((item, index) => {
if (isString(item)) {
const nodes = this.parseString(item, opt);
models[index] = isArray(nodes) && !nodes.length ? null : nodes;
}
});
}
const isMult = isArray(models);
models = (isMult ? models : [models]).filter(i => i).map(model => this.processDef(model));
models = isMult ? flatten(models, 1) : models[0];
const result = Backbone.Collection.prototype.add.apply(this, [models, opt]);
this.__firstAdd = result;
//console.log('Components.js => add end');
return result;
},
/**
* Process component definition.
*/
processDef(mdl) {
// Avoid processing Models
if (mdl.cid && mdl.ccid) return mdl;
const { em, config = {} } = this;
const { processor } = config;
let model = mdl;
if (processor) {
model = { ...model }; // Avoid 'Cannot delete property ...'
const modelPr = processor(model);
if (modelPr) {
each(model, (val, key) => delete model[key]);
extend(model, modelPr);
}
}
// React JSX preset
if (model.$$typeof && typeof model.props == 'object') {
model = { ...model };
model.props = { ...model.props };
const domc = em.get('DomComponents');
const parser = em.get('Parser');
const { parserHtml } = parser;
each(model, (value, key) => {
if (!includes(['props', 'type'], key)) delete model[key];
});
const { props } = model;
const comps = props.children;
delete props.children;
delete model.props;
const res = parserHtml.splitPropsFromAttr(props);
model.attributes = res.attrs;
if (comps) {
model.components = comps;
}
if (!model.type) {
model.type = 'textnode';
} else if (!domc.getType(model.type)) {
model.tagName = model.type;
delete model.type;
}
extend(model, res.props);
}
return model;
},
onAdd(model, c, opts = {}) {
//console.log('Components.js => onAdd start');
const { domc, em } = this;
const style = model.getStyle();
const avoidInline = em && em.getConfig('avoidInlineStyle');
domc && domc.Component.ensureInList(model);
if (!isEmpty(style) && !avoidInline && em && em.get && em.getConfig('forceClass') && !opts.temporary) {
const name = model.cid;
const rule = em.get('CssComposer').setClassRule(name, style);
model.setStyle({});
model.addClass(name);
}
model.__postAdd({ recursive: 1 });
this.__onAddEnd();
//console.log('Components.js => onAdd end');
},
__onAddEnd: debounce(function () {
// TODO to check symbols on load, probably this might be removed as symbols
// are always recovered from the model
// 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(keySymbols);
// const symbolOf = comp.get(keySymbol);
// if (symbol && isArray(symbol) && isString(symbol[0])) {
// comp.set(
// keySymbols,
// symbol.map(smb => allComp[smb]).filter(i => i),
// silent
// );
// }
// if (isString(symbolOf)) {
// comp.set(keySymbol, allComp[symbolOf], silent);
// }
// onAll(comp.components());
// });
// };
// onAll(toCheck);
}),
});