grapesjs_codeapps
Version:
Free and Open Source Web Builder Framework/SC Modification
590 lines (533 loc) • 16.4 kB
JavaScript
/**
* With this module is possible to manage components inside the canvas. You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/dom_components/config/config.js)
* ```js
* const editor = grapesjs.init({
* domComponents: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const domComponents = editor.DomComponents;
* ```
*
* * [getWrapper](#getwrapper)
* * [getComponents](#getcomponents)
* * [addComponent](#addcomponent)
* * [clear](#clear)
* * [load](#load)
* * [store](#store)
* * [render](#render)
*
* @module DomComponents
*/
import Backbone from 'backbone';
import { isEmpty, isString, isObject, isArray } from 'underscore';
module.exports = () => {
var c = {};
let em;
const defaults = require('./config/config');
const Component = require('./model/Component');
const ComponentView = require('./view/ComponentView');
const Components = require('./model/Components');
const ComponentsView = require('./view/ComponentsView');
var component, componentView;
var componentTypes = [
{
id: 'cell',
model: require('./model/ComponentTableCell'),
view: require('./view/ComponentTableCellView')
},
{
id: 'row',
model: require('./model/ComponentTableRow'),
view: require('./view/ComponentTableRowView')
},
{
id: 'table',
model: require('./model/ComponentTable'),
view: require('./view/ComponentTableView')
},
{
id: 'thead',
model: require('./model/ComponentTableHead'),
view: require('./view/ComponentTableHeadView')
},
{
id: 'tbody',
model: require('./model/ComponentTableBody'),
view: require('./view/ComponentTableBodyView')
},
{
id: 'tfoot',
model: require('./model/ComponentTableFoot'),
view: require('./view/ComponentTableFootView')
},
{
id: 'map',
model: require('./model/ComponentMap'),
view: require('./view/ComponentMapView')
},
{
id: 'link',
model: require('./model/ComponentLink'),
view: require('./view/ComponentLinkView')
},
{
id: 'label',
model: require('./model/ComponentLabel'),
view: require('./view/ComponentLabelView')
},
{
id: 'video',
model: require('./model/ComponentVideo'),
view: require('./view/ComponentVideoView')
},
{
id: 'image',
model: require('./model/ComponentImage'),
view: require('./view/ComponentImageView')
},
{
id: 'script',
model: require('./model/ComponentScript'),
view: require('./view/ComponentScriptView')
},
{
id: 'svg',
model: require('./model/ComponentSvg'),
view: require('./view/ComponentSvgView')
},
{
id: 'textnode',
model: require('./model/ComponentTextNode'),
view: require('./view/ComponentTextNodeView')
},
{
id: 'text',
model: require('./model/ComponentText'),
view: require('./view/ComponentTextView')
},
{
id: 'sc-wrapper',
model: require('./model/ComponentWrapper'),
view: ComponentView
},
{
id: 'default',
model: Component,
view: ComponentView
}
];
return {
Component,
Components,
ComponentsView,
componentTypes,
/**
* Name of the module
* @type {String}
* @private
*/
name: 'DomComponents',
/**
* Returns config
* @return {Object} Config object
* @private
*/
getConfig() {
return c;
},
/**
* Mandatory for the storage manager
* @type {String}
* @private
*/
storageKey() {
var keys = [];
var smc = (c.stm && c.stm.getConfig()) || {};
if (smc.storeHtml) keys.push('html');
if (smc.storeComponents) keys.push('components');
return keys;
},
/**
* Initialize module. Called on a new instance of the editor with configurations passed
* inside 'domComponents' field
* @param {Object} config Configurations
* @private
*/
init(config) {
c = config || {};
em = c.em;
this.em = em;
this.rte = em && em.get('RichTextEditor');
if (em) {
c.components = em.config.components || c.components;
}
for (var name in defaults) {
if (!(name in c)) c[name] = defaults[name];
}
var ppfx = c.pStylePrefix;
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
// Load dependencies
if (em) {
c.modal = em.get('Modal') || '';
c.am = em.get('AssetManager') || '';
em.get('Parser').compTypes = componentTypes;
em.on('change:componentHovered', this.componentHovered, this);
const selected = em.get('selected');
em.listenTo(selected, 'add', (sel, c, opts) => {
sel.trigger('focus');
this.selectAdd(sel, opts);
});
em.listenTo(selected, 'remove', (sel, c, opts) =>
this.selectRemove(sel, opts)
);
}
// Build wrapper
let components = c.components;
let wrapper = { ...c.wrapper };
wrapper['custom-name'] = c.wrapperName;
wrapper.wrapper = 1;
wrapper.type = 'wrapper';
// Components might be a wrapper
if (
components &&
components.constructor === Object &&
components.wrapper
) {
wrapper = { ...components };
components = components.components || [];
wrapper.components = [];
// Have to put back the real object of components
if (em) {
em.config.components = components;
c.components = components;
}
}
component = new Component(wrapper, {
em,
config: c,
componentTypes
});
component.set({ attributes: { id: 'sc-wrapper' } });
componentView = new ComponentView({
model: component,
config: c,
componentTypes
});
return this;
},
/**
* On load callback
* @private
*/
onLoad() {
this.setComponents(c.components);
},
/**
* Do stuff after load
* @param {Editor} em
* @private
*/
postLoad(em) {
this.handleChanges(this.getWrapper(), null, { avoidStore: 1 });
},
/**
* Handle component changes
* @private
*/
handleChanges(model, value, opts = {}) {
const comps = model.components();
const um = em.get('UndoManager');
const handleUpdates = em.handleUpdates.bind(em);
const handleChanges = this.handleChanges.bind(this);
const handleChangesColl = this.handleChangesColl.bind(this);
const handleRemoves = this.handleRemoves.bind(this);
um && um.add(model);
um && comps && um.add(comps);
const evn = 'change:style change:content change:attributes change:src';
[
[model, evn, handleUpdates],
[model, 'change:components', handleChangesColl],
[comps, 'add', handleChanges],
[comps, 'remove', handleRemoves],
[model.get('classes'), 'add remove', handleUpdates]
].forEach(els => {
em.stopListening(els[0], els[1], els[2]);
em.listenTo(els[0], els[1], els[2]);
});
!opts.avoidStore && handleUpdates('', '', opts);
comps.each(model => this.handleChanges(model, value, opts));
},
handleChangesColl(model, coll) {
const um = em.get('UndoManager');
if (um && coll instanceof Backbone.Collection) {
const handleChanges = this.handleChanges.bind(this);
const handleRemoves = this.handleRemoves.bind(this);
um.add(coll);
[[coll, 'add', handleChanges], [coll, 'remove', handleRemoves]].forEach(
els => {
em.stopListening(els[0], els[1], els[2]);
em.listenTo(els[0], els[1], els[2]);
}
);
}
},
/**
* Triggered when some component is removed
* @private
* */
handleRemoves(model, value, opts = {}) {
!opts.avoidStore && em.handleUpdates(model, value, opts);
},
/**
* Load components from the passed object, if the object is empty will try to fetch them
* autonomously from the selected storage
* The fetched data will be added to the collection
* @param {Object} data Object of data to load
* @return {Object} Loaded data
*/
load(data = '') {
const { em } = this;
let result = '';
if (!data && c.stm) {
data = c.em.getCacheLoad();
}
const { components, html } = data;
if (components) {
if (isObject(components) || isArray(components)) {
result = components;
} else {
try {
result = JSON.parse(components);
} catch (err) {
em && em.logError(err);
}
}
} else if (html) {
result = html;
}
const isObj = result && result.constructor === Object;
if ((result && result.length) || isObj) {
this.clear();
// If the result is an object I consider it the wrapper
if (isObj) {
this.getWrapper().set(result);
} else {
this.getComponents().add(result);
}
}
return result;
},
/**
* Store components on the selected storage
* @param {Boolean} noStore If true, won't store
* @return {Object} Data to store
*/
store(noStore) {
if (!c.stm) {
return;
}
var obj = {};
var keys = this.storageKey();
if (keys.indexOf('html') >= 0) {
obj.html = c.em.getHtml();
}
if (keys.indexOf('components') >= 0) {
const { em } = this;
// const storeWrap = (em && !em.getConfig('avoidInlineStyle')) || c.storeWrapper;
const storeWrap = c.storeWrapper;
const toStore = storeWrap ? this.getWrapper() : this.getComponents();
obj.components = JSON.stringify(toStore);
}
if (!noStore) {
c.stm.store(obj);
}
return obj;
},
/**
* Returns privately the main wrapper
* @return {Object}
* @private
*/
getComponent() {
return component;
},
/**
* Returns root component inside the canvas. Something like `<body>` inside HTML page
* The wrapper doesn't differ from the original Component Model
* @return {Component} Root Component
* @example
* // Change background of the wrapper and set some attribute
* var wrapper = domComponents.getWrapper();
* wrapper.set('style', {'background-color': 'red'});
* wrapper.set('attributes', {'title': 'Hello!'});
*/
getWrapper() {
return this.getComponent();
},
/**
* Returns wrapper's children collection. Once you have the collection you can
* add other Components(Models) inside. Each component can have several nested
* components inside and you can nest them as more as you wish.
* @return {Components} Collection of components
* @example
* // Let's add some component
* var wrapperChildren = domComponents.getComponents();
* var comp1 = wrapperChildren.add({
* style: { 'background-color': 'red'}
* });
* var comp2 = wrapperChildren.add({
* tagName: 'span',
* attributes: { title: 'Hello!'}
* });
* // Now let's add an other one inside first component
* // First we have to get the collection inside. Each
* // component has 'components' property
* var comp1Children = comp1.get('components');
* // Procede as before. You could also add multiple objects
* comp1Children.add([
* { style: { 'background-color': 'blue'}},
* { style: { height: '100px', width: '100px'}}
* ]);
* // Remove comp2
* wrapperChildren.remove(comp2);
*/
getComponents() {
return this.getWrapper().get('components');
},
/**
* Add new components to the wrapper's children. It's the same
* as 'domComponents.getComponents().add(...)'
* @param {Object|Component|Array<Object>} component Component/s to add
* @param {string} [component.tagName='div'] Tag name
* @param {string} [component.type=''] Type of the component. Available: ''(default), 'text', 'image'
* @param {boolean} [component.removable=true] If component is removable
* @param {boolean} [component.draggable=true] If is possible to move the component around the structure
* @param {boolean} [component.droppable=true] If is possible to drop inside other components
* @param {boolean} [component.badgable=true] If the badge is visible when the component is selected
* @param {boolean} [component.stylable=true] If is possible to style component
* @param {boolean} [component.copyable=true] If is possible to copy&paste the component
* @param {string} [component.content=''] String inside component
* @param {Object} [component.style={}] Style object
* @param {Object} [component.attributes={}] Attribute object
* @return {Component|Array<Component>} Component/s added
* @example
* // Example of a new component with some extra property
* var comp1 = domComponents.addComponent({
* tagName: 'div',
* removable: true, // Can't remove it
* draggable: true, // Can't move it
* copyable: true, // Disable copy/past
* content: 'Content text', // Text inside component
* style: { color: 'red'},
* attributes: { title: 'here' }
* });
*/
addComponent(component) {
return this.getComponents().add(component);
},
/**
* Render and returns wrapper element with all components inside.
* Once the wrapper is rendered, and it's what happens when you init the editor,
* the all new components will be added automatically and property changes are all
* updated immediately
* @return {HTMLElement}
*/
render() {
return componentView.render().el;
},
/**
* Remove all components
* @return {this}
*/
clear() {
this.getComponents().reset();
return this;
},
/**
* Set components
* @param {Object|string} components HTML string or components model
* @return {this}
* @private
*/
setComponents(components) {
this.clear().addComponent(components);
},
/**
* Add new component type
* @param {string} type
* @param {Object} methods
* @private
*/
addType(type, methods) {
var compType = this.getType(type);
if (compType) {
compType.model = methods.model;
compType.view = methods.view;
} else {
methods.id = type;
componentTypes.unshift(methods);
}
},
/**
* Get component type
* @param {string} type
* @private
*/
getType(type) {
var df = componentTypes;
for (var it = 0; it < df.length; it++) {
var dfId = df[it].id;
if (dfId == type) {
return df[it];
}
}
return;
},
selectAdd(component, opts = {}) {
if (component) {
component.set({
status: 'selected'
});
['component:selected', 'component:toggled'].forEach(event =>
this.em.trigger(event, component, opts)
);
}
},
selectRemove(component, opts = {}) {
if (component) {
const { em } = this;
component.set({
status: '',
state: ''
});
['component:deselected', 'component:toggled'].forEach(event =>
this.em.trigger(event, component, opts)
);
}
},
/**
* Triggered when the component is hovered
* @private
*/
componentHovered() {
const em = c.em;
const model = em.get('componentHovered');
const previous = em.previous('componentHovered');
const state = 'hovered';
// Deselect the previous component
previous &&
previous.get('status') == state &&
previous.set({
status: '',
state: ''
});
model && isEmpty(model.get('status')) && model.set('status', state);
}
};
};