/** * 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) * * [addType](#addtype) * * [getType](#gettype) * * [getTypes](#gettypes) * * [render](#render) * * @module DomComponents */ import Backbone from 'backbone'; import { isEmpty, isObject, isArray, result } from 'underscore'; import defaults from './config/config'; import Component from './model/Component'; import Components from './model/Components'; import ComponentView from './view/ComponentView'; import ComponentsView from './view/ComponentsView'; import ComponentTableCell from './model/ComponentTableCell'; import ComponentTableCellView from './view/ComponentTableCellView'; import ComponentTableRow from './model/ComponentTableRow'; import ComponentTableRowView from './view/ComponentTableRowView'; import ComponentTable from './model/ComponentTable'; import ComponentTableView from './view/ComponentTableView'; import ComponentTableHead from './model/ComponentTableHead'; import ComponentTableHeadView from './view/ComponentTableHeadView'; import ComponentTableBody from './model/ComponentTableBody'; import ComponentTableBodyView from './view/ComponentTableBodyView'; import ComponentTableFoot from './model/ComponentTableFoot'; import ComponentTableFootView from './view/ComponentTableFootView'; import ComponentMap from './model/ComponentMap'; import ComponentMapView from './view/ComponentMapView'; import ComponentLink from './model/ComponentLink'; import ComponentLinkView from './view/ComponentLinkView'; import ComponentLabel from './model/ComponentLabel'; import ComponentLabelView from './view/ComponentLabelView'; import ComponentVideo from './model/ComponentVideo'; import ComponentVideoView from './view/ComponentVideoView'; import ComponentImage from './model/ComponentImage'; import ComponentImageView from './view/ComponentImageView'; import ComponentScript from './model/ComponentScript'; import ComponentScriptView from './view/ComponentScriptView'; import ComponentSvg from './model/ComponentSvg'; import ComponentSvgIn from './model/ComponentSvgIn'; import ComponentSvgView from './view/ComponentSvgView'; import ComponentComment from './model/ComponentComment'; import ComponentCommentView from './view/ComponentCommentView'; import ComponentTextNode from './model/ComponentTextNode'; import ComponentTextNodeView from './view/ComponentTextNodeView'; import ComponentText from './model/ComponentText'; import ComponentTextView from './view/ComponentTextView'; import ComponentWrapper from './model/ComponentWrapper'; export default () => { var c = {}; let em; const componentsById = {}; var component, componentView; var componentTypes = [ { id: 'cell', model: ComponentTableCell, view: ComponentTableCellView }, { id: 'row', model: ComponentTableRow, view: ComponentTableRowView }, { id: 'table', model: ComponentTable, view: ComponentTableView }, { id: 'thead', model: ComponentTableHead, view: ComponentTableHeadView }, { id: 'tbody', model: ComponentTableBody, view: ComponentTableBodyView }, { id: 'tfoot', model: ComponentTableFoot, view: ComponentTableFootView }, { id: 'map', model: ComponentMap, view: ComponentMapView }, { id: 'link', model: ComponentLink, view: ComponentLinkView }, { id: 'label', model: ComponentLabel, view: ComponentLabelView }, { id: 'video', model: ComponentVideo, view: ComponentVideoView }, { id: 'image', model: ComponentImage, view: ComponentImageView }, { id: 'script', model: ComponentScript, view: ComponentScriptView }, { id: 'svg-in', model: ComponentSvgIn, view: ComponentSvgView }, { id: 'svg', model: ComponentSvg, view: ComponentSvgView }, { id: 'comment', model: ComponentComment, view: ComponentCommentView }, { id: 'textnode', model: ComponentTextNode, view: ComponentTextNodeView }, { id: 'text', model: ComponentText, view: ComponentTextView }, { id: 'wrapper', model: ComponentWrapper, view: ComponentView }, { id: 'default', model: Component, view: ComponentView } ]; return { Component, Components, ComponentsView, componentTypes, componentsById, /** * 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; 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) => 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, domc: this }); component.set({ attributes: { id: '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 reset', 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 reset', 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(m, value, opt) { const opts = opt || value; // in case of reset em.handleUpdates(m, 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 `
` 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