From 133e6a175fd1b816d71aea4a0cd7b8fc5eede0ac Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Sat, 4 Mar 2017 15:00:43 +0100 Subject: [PATCH] Fix some issues with the undo manager --- src/block_manager/view/BlocksView.js | 11 +- src/demo.js | 34 +- src/dom_components/main.js | 472 +++++------ src/dom_components/view/ComponentsView.js | 184 ++-- src/editor/model/Editor.js | 978 +++++++++++----------- src/editor/view/EditorView.js | 95 ++- 6 files changed, 910 insertions(+), 864 deletions(-) diff --git a/src/block_manager/view/BlocksView.js b/src/block_manager/view/BlocksView.js index a0b7ccaa5..199d9f0a8 100644 --- a/src/block_manager/view/BlocksView.js +++ b/src/block_manager/view/BlocksView.js @@ -61,9 +61,14 @@ function(Backbone, BlockView) { onDrop: function(model){ this.em.runDefault(); - if (model && model.get && model.get('activeOnRender')) { - model.trigger('active'); - model.set('activeOnRender', 0); + if (model && model.get) { + if(model.get('activeOnRender')) { + model.trigger('active'); + model.set('activeOnRender', 0); + } + + // Register all its components (eg. for the Undo Manager) + this.em.initChildrenComp(model); } }, diff --git a/src/demo.js b/src/demo.js index 13f36fb6c..9cfc1352e 100644 --- a/src/demo.js +++ b/src/demo.js @@ -36,12 +36,13 @@ require(['config/require-config'], function() { content: " More text node --- ", }], }],*/ - + /* storageManager:{ autoload: 0, storeComponents: 1, storeStyles: 1, }, + */ commands: { defaults : [{ id: 'open-github', @@ -334,6 +335,37 @@ require(['config/require-config'], function() { window.editor = editor; + var pnm = editor.Panels; + pnm.addButton('options', [{ + id: 'undo', + className: 'fa fa-undo icon-undo', + command: function(editor, sender) { + sender.set('active', 0); + editor.UndoManager.undo(1); + }, + attributes: { title: 'Undo (CTRL/CMD + Z)'} + },{ + id: 'redo', + className: 'fa fa-repeat icon-redo', + command: function(editor, sender) { + sender.set('active', 0); + editor.UndoManager.redo(1); + }, + attributes: { title: 'Redo (CTRL/CMD + SHIFT + Z)' } + },{ + id: 'clean-all', + className: 'fa fa-trash icon-blank', + command: function(editor, sender) { + if(sender) sender.set('active',false); + if(confirm('Are you sure to clean the canvas?')){ + var comps = editor.DomComponents.clear(); + localStorage.setItem('gjs-css', ''); + localStorage.setItem('gjs-html', ''); + } + }, + attributes: { title: 'Empty canvas' } + }]); + editor.render(); }); }); diff --git a/src/dom_components/main.js b/src/dom_components/main.js index da7afdd98..fcc88cfc3 100644 --- a/src/dom_components/main.js +++ b/src/dom_components/main.js @@ -21,7 +21,7 @@ * @example * ... * domComponents: { - * components: '
Hello world!
', + * components: '
Hello world!
', * } * // Or * domComponents: { @@ -34,86 +34,86 @@ */ define(function(require) { - return function (){ - var c = {}, - componentTypes = {}, - defaults = require('./config/config'), - Component = require('./model/Component'), - ComponentView = require('./view/ComponentView'); + return function (){ + var c = {}, + componentTypes = {}, + defaults = require('./config/config'), + Component = require('./model/Component'), + ComponentView = require('./view/ComponentView'); - var component, componentView; - var defaultTypes = [ - { - 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: 'map', - model: require('./model/ComponentMap'), - view: require('./view/ComponentMapView'), - }, - { - id: 'link', - model: require('./model/ComponentLink'), - view: require('./view/ComponentLinkView'), - }, - { - id: 'video', - model: require('./model/ComponentVideo'), - view: require('./view/ComponentVideoView'), - }, - { - id: 'image', - model: require('./model/ComponentImage'), - view: require('./view/ComponentImageView'), - }, - { - id: 'textnode', - model: require('./model/ComponentTextNode'), - view: require('./view/ComponentTextNodeView'), - }, - { - id: 'text', - model: require('./model/ComponentText'), - view: require('./view/ComponentTextView'), - }, - { - id: 'default', - model: Component, - view: ComponentView, - }, - ]; + var component, componentView; + var defaultTypes = [ + { + 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: 'map', + model: require('./model/ComponentMap'), + view: require('./view/ComponentMapView'), + }, + { + id: 'link', + model: require('./model/ComponentLink'), + view: require('./view/ComponentLinkView'), + }, + { + id: 'video', + model: require('./model/ComponentVideo'), + view: require('./view/ComponentVideoView'), + }, + { + id: 'image', + model: require('./model/ComponentImage'), + view: require('./view/ComponentImageView'), + }, + { + id: 'textnode', + model: require('./model/ComponentTextNode'), + view: require('./view/ComponentTextNodeView'), + }, + { + id: 'text', + model: require('./model/ComponentText'), + view: require('./view/ComponentTextView'), + }, + { + id: 'default', + model: Component, + view: ComponentView, + }, + ]; - return { + return { - componentTypes: defaultTypes, + componentTypes: defaultTypes, - /** + /** * Name of the module * @type {String} * @private */ name: 'DomComponents', - /** - * Returns config - * @return {Object} Config object - * @private - */ - getConfig: function () { - return c; - }, + /** + * Returns config + * @return {Object} Config object + * @private + */ + getConfig: function () { + return c; + }, /** * Mandatory for the storage manager @@ -121,13 +121,13 @@ define(function(require) { * @private */ storageKey: function(){ - var keys = []; - var smc = (c.stm && c.stm.getConfig()) || {}; + var keys = []; + var smc = (c.stm && c.stm.getConfig()) || {}; if(smc.storeHtml) keys.push('html'); if(smc.storeComponents) keys.push('components'); - return keys; + return keys; }, /** @@ -153,29 +153,29 @@ define(function(require) { // Load dependencies if(c.em){ c.rte = c.em.get('rte') || ''; - c.modal = c.em.get('Modal') || ''; - c.am = c.em.get('AssetManager') || ''; - c.em.get('Parser').compTypes = defaultTypes; + c.modal = c.em.get('Modal') || ''; + c.am = c.em.get('AssetManager') || ''; + c.em.get('Parser').compTypes = defaultTypes; } component = new Component(c.wrapper, { - sm: c.em, - config: c, - defaultTypes: defaultTypes, - componentTypes: componentTypes, - }); - component.set({ attributes: {id: 'wrapper'}}); + sm: c.em, + config: c, + defaultTypes: defaultTypes, + componentTypes: componentTypes, + }); + component.set({ attributes: {id: 'wrapper'}}); - if(c.em && !c.em.config.loadCompsOnRender) { - component.get('components').add(c.components); - } + if(c.em && !c.em.config.loadCompsOnRender) { + component.get('components').add(c.components); + } - componentView = new ComponentView({ - model: component, - config: c, - defaultTypes: defaultTypes, - componentTypes: componentTypes, - }); + componentView = new ComponentView({ + model: component, + config: c, + defaultTypes: defaultTypes, + componentTypes: componentTypes, + }); return this; }, @@ -236,158 +236,158 @@ define(function(require) { return obj; }, - /** - * Returns privately the main wrapper - * @return {Object} - * @private - */ - getComponent : function(){ - return component; - }, + /** + * Returns privately the main wrapper + * @return {Object} + * @private + */ + getComponent : function(){ + 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: function(){ - return this.getComponent(); - }, + /** + * 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: function(){ + 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: function(){ - return this.getWrapper().get('components'); - }, + /** + * 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: function(){ + 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} 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/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: function(component){ - return this.getComponents().add(component); - }, + /** + * Add new components to the wrapper's children. It's the same + * as 'domComponents.getComponents().add(...)' + * @param {Object|Component|Array} 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/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: function(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: function(){ - return componentView.render().el; - }, + /** + * 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: function(){ + return componentView.render().el; + }, - /** - * Remove all components - * @return {this} - */ - clear: function(){ - var c = this.getComponents(); - for(var i = 0, len = c.length; i < len; i++) - c.pop(); - return this; - }, + /** + * Remove all components + * @return {this} + */ + clear: function(){ + var c = this.getComponents(); + for(var i = 0, len = c.length; i < len; i++) + c.pop(); + return this; + }, - /** - * Set components - * @param {Object|string} components HTML string or components model - * @return {this} - * @private - */ - setComponents: function(components){ - this.clear().addComponent(components); - }, + /** + * Set components + * @param {Object|string} components HTML string or components model + * @return {this} + * @private + */ + setComponents: function(components){ + this.clear().addComponent(components); + }, - /** - * Add new component type - * @param {string} type - * @param {Object} methods - * @private - */ - addType: function(type, methods) { - var compType = this.getType(type); - if(compType) { - compType.model = methods.model; - compType.view = methods.view; - } else { - methods.id = type; - defaultTypes.unshift(methods); - } - }, + /** + * Add new component type + * @param {string} type + * @param {Object} methods + * @private + */ + addType: function(type, methods) { + var compType = this.getType(type); + if(compType) { + compType.model = methods.model; + compType.view = methods.view; + } else { + methods.id = type; + defaultTypes.unshift(methods); + } + }, - /** - * Get component type - * @param {string} type - * @private - */ - getType: function(type) { - var df = defaultTypes; + /** + * Get component type + * @param {string} type + * @private + */ + getType: function(type) { + var df = defaultTypes; - for (var it = 0; it < df.length; it++) { - var dfId = df[it].id; - if(dfId == type) { - return df[it]; - } - } - return; - }, + for (var it = 0; it < df.length; it++) { + var dfId = df[it].id; + if(dfId == type) { + return df[it]; + } + } + return; + }, - }; - }; + }; + }; }); diff --git a/src/dom_components/view/ComponentsView.js b/src/dom_components/view/ComponentsView.js index 70b8ce795..79553fdb4 100644 --- a/src/dom_components/view/ComponentsView.js +++ b/src/dom_components/view/ComponentsView.js @@ -1,107 +1,111 @@ define(['backbone','require'], function(Backbone, require) { - return Backbone.View.extend({ + return Backbone.View.extend({ - initialize: function(o) { - this.opts = o || {}; - this.config = o.config || {}; - this.listenTo( this.collection, 'add', this.addTo ); - this.listenTo( this.collection, 'reset', this.render ); - }, + initialize: function(o) { + this.opts = o || {}; + this.config = o.config || {}; + this.listenTo( this.collection, 'add', this.addTo ); + this.listenTo( this.collection, 'reset', this.render ); + }, - /** - * Add to collection - * @param {Object} Model - * - * @return void - * @private - * */ - addTo: function(model) { - var i = this.collection.indexOf(model); - this.addToCollection(model, null, i); + /** + * Add to collection + * @param {Object} Model + * + * @return void + * @private + * */ + addTo: function(model) { + var i = this.collection.indexOf(model); + this.addToCollection(model, null, i); - if(this.config.em) { - this.config.em.trigger('add:component', model); - } - }, + if(this.config.em) { + this.config.em.trigger('add:component', model); + } + }, - /** - * Add new object to collection - * @param {Object} Model - * @param {Object} Fragment collection - * @param {Integer} Index of append - * - * @return {Object} Object rendered - * @private - * */ - addToCollection: function(model, fragmentEl, index){ - if(!this.compView) - this.compView = require('./ComponentView'); - var fragment = fragmentEl || null, - viewObject = this.compView; - //console.log('Add to collection', model, 'Index',i); + /** + * Add new object to collection + * @param {Object} Model + * @param {Object} Fragment collection + * @param {Integer} Index of append + * + * @return {Object} Object rendered + * @private + * */ + addToCollection: function(model, fragmentEl, index){ + if(!this.compView) + this.compView = require('./ComponentView'); + var fragment = fragmentEl || null, + viewObject = this.compView; + //console.log('Add to collection', model, 'Index',i); - var dt = this.opts.defaultTypes; - var ct = this.opts.componentTypes; + var dt = this.opts.defaultTypes; + var ct = this.opts.componentTypes; - var type = model.get('type'); + var type = model.get('type'); - for (var it = 0; it < dt.length; it++) { - var dtId = dt[it].id; - if(dtId == type) { - viewObject = dt[it].view; - break; - } - } - //viewObject = dt[type] ? dt[type].view : dt.default.view; + for (var it = 0; it < dt.length; it++) { + var dtId = dt[it].id; + if(dtId == type) { + viewObject = dt[it].view; + break; + } + } + //viewObject = dt[type] ? dt[type].view : dt.default.view; - var view = new viewObject({ - model: model, - config: this.config, - defaultTypes: dt, - componentTypes: ct, - }); - var rendered = view.render().el; - if(view.model.get('type') == 'textnode') - rendered = document.createTextNode(view.model.get('content')); + var view = new viewObject({ + model: model, + config: this.config, + defaultTypes: dt, + componentTypes: ct, + }); + var rendered = view.render().el; + if(view.model.get('type') == 'textnode') + rendered = document.createTextNode(view.model.get('content')); - if(fragment){ - fragment.appendChild(rendered); - }else{ - var p = this.$parent; - if(typeof index != 'undefined'){ - var method = 'before'; - // If the added model is the last of collection - // need to change the logic of append - if(p.children().length == index){ - index--; - method = 'after'; - } - // In case the added is new in the collection index will be -1 - if(index < 0){ - p.append(rendered); - }else - p.children().eq(index)[method](rendered); - }else{ - p.append(rendered); - } - } + if(fragment){ + fragment.appendChild(rendered); + }else{ + var p = this.$parent; + var pc = p.children; + if(typeof index != 'undefined'){ + var method = 'before'; + // If the added model is the last of collection + // need to change the logic of append + if(pc && p.children().length == index){ + index--; + method = 'after'; + } + // In case the added is new in the collection index will be -1 + if(index < 0) { + p.append(rendered); + }else { + if(pc) { + p.children().eq(index)[method](rendered); + } + } + }else{ + p.append(rendered); + } + } - return rendered; - }, + return rendered; + }, - render: function($p) { - var fragment = document.createDocumentFragment(); - this.$parent = $p || this.$el; - this.$el.empty(); - this.collection.each(function(model){ - this.addToCollection(model, fragment); - },this); - this.$el.append(fragment); + render: function($p) { + var fragment = document.createDocumentFragment(); + this.$parent = $p || this.$el; + this.$el.empty(); + this.collection.each(function(model){ + this.addToCollection(model, fragment); + },this); + this.$el.append(fragment); - return this; - } + return this; + } - }); + }); }); diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index 67410e870..aae71d87f 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -1,46 +1,46 @@ var deps = ['Utils', 'StorageManager', 'DeviceManager', 'Parser', 'SelectorManager', 'ModalDialog', 'CodeManager', 'Panels', - 'RichTextEditor', 'StyleManager', 'AssetManager', 'CssComposer', 'DomComponents', 'Canvas', 'Commands', 'BlockManager', 'TraitManager']; + 'RichTextEditor', 'StyleManager', 'AssetManager', 'CssComposer', 'DomComponents', 'Canvas', 'Commands', 'BlockManager', 'TraitManager']; // r.js do not see deps if I pass them as a variable // http://stackoverflow.com/questions/27545412/optimization-fails-when-passing-a-variable-with-a-list-of-dependencies-to-define define(['backbone', 'backboneUndo', 'keymaster', 'Utils', 'StorageManager', 'DeviceManager', 'Parser', 'SelectorManager', 'ModalDialog', 'CodeManager', 'Panels', 'RichTextEditor', 'StyleManager', 'AssetManager', 'CssComposer', 'DomComponents', 'Canvas', 'Commands', 'BlockManager', 'TraitManager'], function(){ - return Backbone.Model.extend({ - - defaults: { - clipboard: null, - selectedComponent: null, - previousModel: null, - changesCount: 0, - storables: [], - toLoad: [], - opened: {}, - device: '', - }, - - initialize: function(c) { - this.config = c; - this.set('Config', c); - - if(c.el && c.fromElement) - this.config.components = c.el.innerHTML; - - // Load modules - deps.forEach(function(name){ - this.loadModule(name); - }, this); - - // Call modules with onLoad callback - this.get('toLoad').forEach(function(M){ - M.onLoad(); - }); - - this.initUndoManager(); // Is already called (inside components and css composer) - - this.on('change:selectedComponent', this.componentSelected, this); + return Backbone.Model.extend({ + + defaults: { + clipboard: null, + selectedComponent: null, + previousModel: null, + changesCount: 0, + storables: [], + toLoad: [], + opened: {}, + device: '', + }, + + initialize: function(c) { + this.config = c; + this.set('Config', c); + + if(c.el && c.fromElement) + this.config.components = c.el.innerHTML; + + // Load modules + deps.forEach(function(name){ + this.loadModule(name); + }, this); + + // Call modules with onLoad callback + this.get('toLoad').forEach(function(M){ + M.onLoad(); + }); + + this.initUndoManager(); // Is already called (inside components and css composer) + + this.on('change:selectedComponent', this.componentSelected, this); this.on('change:changesCount', this.updateBeforeUnload, this); - }, + }, /** * Set the alert before unload in case it's requested @@ -56,457 +56,459 @@ define(['backbone', 'backboneUndo', 'keymaster', 'Utils', 'StorageManager', 'Dev } }, - /** - * Load generic module - * @param {String} moduleName Module name - * @return {this} - */ - loadModule: function(moduleName) { - var c = this.config; - var M = new require(moduleName)(); - var name = M.name.charAt(0).toLowerCase() + M.name.slice(1); - var cfg = c[name] || c[M.name] || {}; - cfg.pStylePrefix = c.pStylePrefix || ''; - - // Check if module is storable - var sm = this.get('StorageManager'); - if(M.storageKey && M.store && M.load && sm){ - cfg.stm = sm; - var storables = this.get('storables'); - storables.push(M); - this.set('storables', storables); - } - cfg.em = this; - M.init(Object.create(cfg)); - - // Bind the module to the editor model if public - if(!M.private) - this.set(M.name, M); - - if(M.onLoad) - this.get('toLoad').push(M); - - return this; - }, - - /** - * Initialize editor model and set editor instance - * @param {Editor} editor Editor instance - * @return {this} - * @private - */ - init: function(editor){ - this.set('Editor', editor); - }, - - /** - * Listen for new rules - * @param {Object} collection - * @private - */ - listenRules: function(collection) { - this.stopListening(collection, 'add remove', this.listenRule); - this.listenTo(collection, 'add remove', this.listenRule); - collection.each(function(model){ - this.listenRule(model); - }, this); - }, - - /** - * Listen for rule changes - * @param {Object} model - * @private - */ - listenRule: function(model) { - this.stopListening(model, 'change:style', this.ruleUpdated); - this.listenTo(model, 'change:style', this.ruleUpdated); - }, - - /** - * Triggered when rule is updated - * @param {Object} model - * @param {Mixed} val Value - * @param {Object} opt Options - * @private - * */ - ruleUpdated: function(model, val, opt) { - var count = this.get('changesCount') + 1, - avSt = opt ? opt.avoidStore : 0; - this.set('changesCount', count); + /** + * Load generic module + * @param {String} moduleName Module name + * @return {this} + */ + loadModule: function(moduleName) { + var c = this.config; + var M = new require(moduleName)(); + var name = M.name.charAt(0).toLowerCase() + M.name.slice(1); + var cfg = c[name] || c[M.name] || {}; + cfg.pStylePrefix = c.pStylePrefix || ''; + + // Check if module is storable + var sm = this.get('StorageManager'); + if(M.storageKey && M.store && M.load && sm){ + cfg.stm = sm; + var storables = this.get('storables'); + storables.push(M); + this.set('storables', storables); + } + cfg.em = this; + M.init(Object.create(cfg)); + + // Bind the module to the editor model if public + if(!M.private) + this.set(M.name, M); + + if(M.onLoad) + this.get('toLoad').push(M); + + return this; + }, + + /** + * Initialize editor model and set editor instance + * @param {Editor} editor Editor instance + * @return {this} + * @private + */ + init: function(editor){ + this.set('Editor', editor); + }, + + /** + * Listen for new rules + * @param {Object} collection + * @private + */ + listenRules: function(collection) { + this.stopListening(collection, 'add remove', this.listenRule); + this.listenTo(collection, 'add remove', this.listenRule); + collection.each(function(model){ + this.listenRule(model); + }, this); + }, + + /** + * Listen for rule changes + * @param {Object} model + * @private + */ + listenRule: function(model) { + this.stopListening(model, 'change:style', this.ruleUpdated); + this.listenTo(model, 'change:style', this.ruleUpdated); + }, + + /** + * Triggered when rule is updated + * @param {Object} model + * @param {Mixed} val Value + * @param {Object} opt Options + * @private + * */ + ruleUpdated: function(model, val, opt) { + var count = this.get('changesCount') + 1, + avSt = opt ? opt.avoidStore : 0; + this.set('changesCount', count); + var stm = this.get('StorageManager'); + if(stm.isAutosave() && count < stm.getStepsBeforeSave()) + return; + + if(!avSt){ + this.store(); + this.set('changesCount', 0); + } + }, + + /** + * Initialize Undo manager + * @private + * */ + initUndoManager: function() { + if(this.um) + return; + var cmp = this.get('DomComponents'); + if(cmp && this.config.undoManager){ + var that = this; + this.um = new Backbone.UndoManager({ + register: [cmp.getComponents(), this.get('CssComposer').getAll()], + track: true + }); + this.UndoManager = this.um; + this.set('UndoManager', this.um); + key('⌘+z, ctrl+z', function(){ + that.um.undo(true); + that.trigger('component:update'); + }); + key('⌘+shift+z, ctrl+shift+z', function(){ + that.um.redo(true); + that.trigger('component:update'); + }); + + Backbone.UndoManager.removeUndoType("change"); + var beforeCache; + Backbone.UndoManager.addUndoType("change:style", { + "on": function (model, value, opts) { + var opt = opts || {}; + if(!beforeCache){ + beforeCache = model.previousAttributes(); + } + if (opt && opt.avoidStore) { + return; + } else { + var obj = { + "object": model, + "before": beforeCache, + "after": model.toJSON() + }; + beforeCache = null; + return obj; + } + }, + "undo": function (model, bf, af, opt) { + model.set(bf); + // Update also inputs inside Style Manager + that.trigger('change:selectedComponent'); + }, + "redo": function (model, bf, af, opt) { + model.set(af); + // Update also inputs inside Style Manager + that.trigger('change:selectedComponent'); + } + }); + } + }, + + /** + * Triggered when components are updated + * @param {Object} model + * @param {Mixed} val Value + * @param {Object} opt Options + * @private + * */ + componentsUpdated: function(model, val, opt){ + var updatedCount = this.get('changesCount') + 1, + avSt = opt ? opt.avoidStore : 0; + this.set('changesCount', updatedCount); var stm = this.get('StorageManager'); - if(stm.isAutosave() && count < stm.getStepsBeforeSave()) - return; - - if(!avSt){ - this.store(); - this.set('changesCount', 0); - } - }, - - /** - * Initialize Undo manager - * @private - * */ - initUndoManager: function() { - if(this.um) - return; - var cmp = this.get('DomComponents'); - if(cmp && this.config.undoManager){ - var that = this; - this.um = new Backbone.UndoManager({ - register: [cmp.getComponents(), this.get('CssComposer').getAll()], - track: true - }); - this.UndoManager = this.um; - this.set('UndoManager', this.um); - key('⌘+z, ctrl+z', function(){ - that.um.undo(true); - }); - key('⌘+shift+z, ctrl+shift+z', function(){ - that.um.redo(true); - }); - - Backbone.UndoManager.removeUndoType("change"); - var beforeCache; - Backbone.UndoManager.addUndoType("change:style", { - "on": function (model, value, opts) { - var opt = opts || {}; - if(!beforeCache){ - beforeCache = model.previousAttributes(); - } - if (opt && opt.avoidStore) { - return; - } else { - var obj = { - "object": model, - "before": beforeCache, - "after": model.toJSON() - }; - beforeCache = null; - return obj; - } - }, - "undo": function (model, bf, af, opt) { - model.set(bf); - // Update also inputs inside Style Manager - that.trigger('change:selectedComponent'); - }, - "redo": function (model, bf, af, opt) { - model.set(af); - // Update also inputs inside Style Manager - that.trigger('change:selectedComponent'); - } - }); - } - }, - - /** - * Triggered when components are updated - * @param {Object} model - * @param {Mixed} val Value - * @param {Object} opt Options - * @private - * */ - componentsUpdated: function(model, val, opt){ - var updatedCount = this.get('changesCount') + 1, - avSt = opt ? opt.avoidStore : 0; - this.set('changesCount', updatedCount); - var stm = this.get('StorageManager'); - if(stm.isAutosave() && updatedCount < stm.getStepsBeforeSave()){ - return; - } - - if(!avSt){ - this.store(); - this.set('changesCount', 0); - } - }, - - /** - * Callback on component selection - * @param {Object} Model - * @param {Mixed} New value - * @param {Object} Options - * @private - * */ - componentSelected: function(model, val, options){ - if(!this.get('selectedComponent')) - this.trigger('deselect-comp'); - else - this.trigger('select-comp',[model,val,options]); - }, - - /** - * Triggered when components are updated - * @param {Object} model - * @param {Mixed} val Value - * @param {Object} opt Options - * @private - * */ - updateComponents: function(model, val, opt) { - var comps = model.get('components'), - classes = model.get('classes'), - avSt = opt ? opt.avoidStore : 0; - - // Observe component with Undo Manager - if(this.um) - this.um.register(comps); - - // Call stopListening for not creating nested listeners - this.stopListening(comps, 'add', this.updateComponents); - this.stopListening(comps, 'remove', this.rmComponents); - this.listenTo(comps, 'add', this.updateComponents); - this.listenTo(comps, 'remove', this.rmComponents); - - this.stopListening(classes, 'add remove', this.componentsUpdated); - this.listenTo(classes, 'add remove', this.componentsUpdated); - - var evn = 'change:style change:content'; - this.stopListening(model, evn, this.componentsUpdated); - this.listenTo(model, evn, this.componentsUpdated); - - if(!avSt) - this.componentsUpdated(); - }, - - /** - * Init stuff like storage for already existing elements - * @param {Object} model - * @private - */ - initChildrenComp: function(model) { - var comps = model.get('components'); - this.updateComponents(model, null, { avoidStore : 1 }); - comps.each(function(md){ - this.initChildrenComp(md); - if(this.um) - this.um.register(md); - }, this); - }, - - /** - * Triggered when some component is removed updated - * @param {Object} model - * @param {Mixed} val Value - * @param {Object} opt Options - * @private - * */ - rmComponents: function(model, val, opt){ - var avSt = opt ? opt.avoidStore : 0; - - if(!avSt) - this.componentsUpdated(); - }, - - /** - * Returns model of the selected component - * @return {Component|null} - * @private - */ - getSelected: function(){ - return this.get('selectedComponent'); - }, - - /** - * Set components inside editor's canvas. This method overrides actual components - * @param {Object|string} components HTML string or components model - * @return {this} - * @private - */ - setComponents: function(components){ - return this.get('DomComponents').setComponents(components); - }, - - /** - * Returns components model from the editor's canvas - * @return {Components} - * @private - */ - getComponents: function(){ - var cmp = this.get('DomComponents'); - var cm = this.get('CodeManager'); - - if(!cmp || !cm) - return; - - var wrp = cmp.getComponents(); - return cm.getCode(wrp, 'json'); - }, - - /** - * Set style inside editor's canvas. This method overrides actual style - * @param {Object|string} style CSS string or style model - * @return {this} - * @private - */ - setStyle: function(style){ - var rules = this.get('CssComposer').getAll(); - for(var i = 0, len = rules.length; i < len; i++) - rules.pop(); - rules.add(style); - return this; - }, - - /** - * Returns rules/style model from the editor's canvas - * @return {Rules} - * @private - */ - getStyle: function(){ - return this.get('CssComposer').getAll(); - }, - - /** - * Returns HTML built inside canvas - * @return {string} HTML string - * @private - */ - getHtml: function(){ - var cmp = this.get('DomComponents'); - var cm = this.get('CodeManager'); - - if(!cmp || !cm) - return; - - var wrp = cmp.getComponent(); - return cm.getCode(wrp, 'html'); - }, - - /** - * Returns CSS built inside canvas - * @return {string} CSS string - * @private - */ - getCss: function(){ - var cmp = this.get('DomComponents'); - var cm = this.get('CodeManager'); - var cssc = this.get('CssComposer'); - - if(!cmp || !cm || !cssc) - return; - - var wrp = cmp.getComponent(); - var protCss = this.config.protectedCss; - - return protCss + cm.getCode(wrp, 'css', cssc); - }, - - /** - * Store data to the current storage - * @return {Object} Stored data - * @private - */ - store: function(){ - var sm = this.get('StorageManager'); - var store = {}; - if(!sm) - return; - - // Fetch what to store - this.get('storables').forEach(function(m){ - var obj = m.store(1); - for(var el in obj) - store[el] = obj[el]; - }); - - sm.store(store); - return store; - }, - - /** - * Load data from the current storage - * @return {Object} Loaded data - * @private - */ - load: function(){ - var result = this.getCacheLoad(1); - this.get('storables').forEach(function(m){ - m.load(result); - }); - return result; - }, - - /** - * Returns cached load - * @param {Boolean} force Force to reload - * @return {Object} - * @private - */ - getCacheLoad: function(force){ - var f = force ? 1 : 0; - if(this.cacheLoad && !f) - return this.cacheLoad; - var sm = this.get('StorageManager'); - var load = []; - - if(!sm) - return {}; - - this.get('storables').forEach(function(m){ - var key = m.storageKey; - key = typeof key === 'function' ? key() : key; - keys = key instanceof Array ? key : [key]; - keys.forEach(function(k){ - load.push(k); - }); - }); - - this.cacheLoad = sm.load(load); - return this.cacheLoad; - }, - - /** - * Returns device model by name - * @return {Device|null} - */ - getDeviceModel: function(){ - var name = this.get('device'); - return this.get('DeviceManager').get(name); - }, - - /** - * Run default command if setted - * @private - */ - runDefault: function(){ - var command = this.get('Commands').get(this.config.defaultCommand); - if(!command || this.defaultRunning) - return; - command.stop(this, this); - command.run(this, this); - this.defaultRunning = 1; - }, - - /** - * Stop default command - * @private - */ - stopDefault: function(){ - var command = this.get('Commands').get(this.config.defaultCommand); - if(!command) - return; - command.stop(this, this); - this.defaultRunning = 0; - }, - - /** - * Update canvas dimensions and refresh data useful for tools positioning - * @private - */ - refreshCanvas: function () { - this.set('canvasOffset', this.get('Canvas').getOffset()); - }, - - /** - * Clear all selected stuf inside the window, sometimes is useful to call before - * doing some dragging opearation - * @param {Window} win If not passed the current one will be used - * @private - */ - clearSelection: function (win) { - var w = win || window; - w.getSelection().removeAllRanges(); - }, - - }); - }); + if(stm.isAutosave() && updatedCount < stm.getStepsBeforeSave()){ + return; + } + + if(!avSt){ + this.store(); + this.set('changesCount', 0); + } + }, + + /** + * Callback on component selection + * @param {Object} Model + * @param {Mixed} New value + * @param {Object} Options + * @private + * */ + componentSelected: function(model, val, options){ + if(!this.get('selectedComponent')) + this.trigger('deselect-comp'); + else + this.trigger('select-comp',[model,val,options]); + }, + + /** + * Triggered when components are updated + * @param {Object} model + * @param {Mixed} val Value + * @param {Object} opt Options + * @private + * */ + updateComponents: function(model, val, opt) { + var comps = model.get('components'), + classes = model.get('classes'), + avSt = opt ? opt.avoidStore : 0; + + // Observe component with Undo Manager + if(this.um) + this.um.register(comps); + + // Call stopListening for not creating nested listeners + this.stopListening(comps, 'add', this.updateComponents); + this.stopListening(comps, 'remove', this.rmComponents); + this.listenTo(comps, 'add', this.updateComponents); + this.listenTo(comps, 'remove', this.rmComponents); + + this.stopListening(classes, 'add remove', this.componentsUpdated); + this.listenTo(classes, 'add remove', this.componentsUpdated); + + var evn = 'change:style change:content'; + this.stopListening(model, evn, this.componentsUpdated); + this.listenTo(model, evn, this.componentsUpdated); + + if(!avSt) + this.componentsUpdated(); + }, + + /** + * Init stuff like storage for already existing elements + * @param {Object} model + * @private + */ + initChildrenComp: function(model) { + var comps = model.get('components'); + this.updateComponents(model, null, { avoidStore : 1 }); + comps.each(function(md) { + this.initChildrenComp(md); + if(this.um) + this.um.register(md); + }, this); + }, + + /** + * Triggered when some component is removed updated + * @param {Object} model + * @param {Mixed} val Value + * @param {Object} opt Options + * @private + * */ + rmComponents: function(model, val, opt){ + var avSt = opt ? opt.avoidStore : 0; + + if(!avSt) + this.componentsUpdated(); + }, + + /** + * Returns model of the selected component + * @return {Component|null} + * @private + */ + getSelected: function(){ + return this.get('selectedComponent'); + }, + + /** + * Set components inside editor's canvas. This method overrides actual components + * @param {Object|string} components HTML string or components model + * @return {this} + * @private + */ + setComponents: function(components){ + return this.get('DomComponents').setComponents(components); + }, + + /** + * Returns components model from the editor's canvas + * @return {Components} + * @private + */ + getComponents: function(){ + var cmp = this.get('DomComponents'); + var cm = this.get('CodeManager'); + + if(!cmp || !cm) + return; + + var wrp = cmp.getComponents(); + return cm.getCode(wrp, 'json'); + }, + + /** + * Set style inside editor's canvas. This method overrides actual style + * @param {Object|string} style CSS string or style model + * @return {this} + * @private + */ + setStyle: function(style){ + var rules = this.get('CssComposer').getAll(); + for(var i = 0, len = rules.length; i < len; i++) + rules.pop(); + rules.add(style); + return this; + }, + + /** + * Returns rules/style model from the editor's canvas + * @return {Rules} + * @private + */ + getStyle: function(){ + return this.get('CssComposer').getAll(); + }, + + /** + * Returns HTML built inside canvas + * @return {string} HTML string + * @private + */ + getHtml: function(){ + var cmp = this.get('DomComponents'); + var cm = this.get('CodeManager'); + + if(!cmp || !cm) + return; + + var wrp = cmp.getComponent(); + return cm.getCode(wrp, 'html'); + }, + + /** + * Returns CSS built inside canvas + * @return {string} CSS string + * @private + */ + getCss: function(){ + var cmp = this.get('DomComponents'); + var cm = this.get('CodeManager'); + var cssc = this.get('CssComposer'); + + if(!cmp || !cm || !cssc) + return; + + var wrp = cmp.getComponent(); + var protCss = this.config.protectedCss; + + return protCss + cm.getCode(wrp, 'css', cssc); + }, + + /** + * Store data to the current storage + * @return {Object} Stored data + * @private + */ + store: function(){ + var sm = this.get('StorageManager'); + var store = {}; + if(!sm) + return; + + // Fetch what to store + this.get('storables').forEach(function(m){ + var obj = m.store(1); + for(var el in obj) + store[el] = obj[el]; + }); + + sm.store(store); + return store; + }, + + /** + * Load data from the current storage + * @return {Object} Loaded data + * @private + */ + load: function(){ + var result = this.getCacheLoad(1); + this.get('storables').forEach(function(m){ + m.load(result); + }); + return result; + }, + + /** + * Returns cached load + * @param {Boolean} force Force to reload + * @return {Object} + * @private + */ + getCacheLoad: function(force){ + var f = force ? 1 : 0; + if(this.cacheLoad && !f) + return this.cacheLoad; + var sm = this.get('StorageManager'); + var load = []; + + if(!sm) + return {}; + + this.get('storables').forEach(function(m){ + var key = m.storageKey; + key = typeof key === 'function' ? key() : key; + keys = key instanceof Array ? key : [key]; + keys.forEach(function(k){ + load.push(k); + }); + }); + + this.cacheLoad = sm.load(load); + return this.cacheLoad; + }, + + /** + * Returns device model by name + * @return {Device|null} + */ + getDeviceModel: function(){ + var name = this.get('device'); + return this.get('DeviceManager').get(name); + }, + + /** + * Run default command if setted + * @private + */ + runDefault: function(){ + var command = this.get('Commands').get(this.config.defaultCommand); + if(!command || this.defaultRunning) + return; + command.stop(this, this); + command.run(this, this); + this.defaultRunning = 1; + }, + + /** + * Stop default command + * @private + */ + stopDefault: function(){ + var command = this.get('Commands').get(this.config.defaultCommand); + if(!command) + return; + command.stop(this, this); + this.defaultRunning = 0; + }, + + /** + * Update canvas dimensions and refresh data useful for tools positioning + * @private + */ + refreshCanvas: function () { + this.set('canvasOffset', this.get('Canvas').getOffset()); + }, + + /** + * Clear all selected stuf inside the window, sometimes is useful to call before + * doing some dragging opearation + * @param {Window} win If not passed the current one will be used + * @private + */ + clearSelection: function (win) { + var w = win || window; + w.getSelection().removeAllRanges(); + }, + + }); + }); diff --git a/src/editor/view/EditorView.js b/src/editor/view/EditorView.js index 971b122ec..633a9ca75 100644 --- a/src/editor/view/EditorView.js +++ b/src/editor/view/EditorView.js @@ -1,50 +1,53 @@ define(['backbone'], function(Backbone){ - return Backbone.View.extend({ - - initialize: function() { - this.pn = this.model.get('Panels'); - this.conf = this.model.config; - this.className = this.conf.stylePrefix + 'editor'; - this.model.on('loaded', function(){ - this.pn.active(); - this.model.runDefault(); - this.model.trigger('load'); - }, this); - }, - - render: function() { - var model = this.model; - var dComps = model.get('DomComponents'); - var config = model.get('Config'); - - if(config.loadCompsOnRender) { - dComps.getComponents().add(config.components); - dComps.onLoad(); - } - - var conf = this.conf; - var contEl = $(conf.el || ('body ' + conf.container)); - this.$el.empty(); - - if(conf.width) - contEl.css('width', conf.width); - - if(conf.height) - contEl.css('height', conf.height); - - // Canvas - this.$el.append(model.get('Canvas').render()); - - // Panels - this.$el.append(this.pn.render()); - this.$el.attr('class', this.className); - - contEl.addClass(conf.stylePrefix + 'editor-cont'); - contEl.html(this.$el); - - return this; - } - }); + return Backbone.View.extend({ + + initialize: function() { + this.pn = this.model.get('Panels'); + this.conf = this.model.config; + this.className = this.conf.stylePrefix + 'editor'; + this.model.on('loaded', function(){ + this.pn.active(); + this.model.runDefault(); + this.model.trigger('load'); + }, this); + }, + + render: function() { + var model = this.model; + var um = model.get('UndoManager'); + var dComps = model.get('DomComponents'); + var config = model.get('Config'); + + if(config.loadCompsOnRender) { + dComps.clear(); + dComps.getComponents().add(config.components); + um.clear(); + dComps.onLoad(); + } + + var conf = this.conf; + var contEl = $(conf.el || ('body ' + conf.container)); + this.$el.empty(); + + if(conf.width) + contEl.css('width', conf.width); + + if(conf.height) + contEl.css('height', conf.height); + + // Canvas + this.$el.append(model.get('Canvas').render()); + + // Panels + this.$el.append(this.pn.render()); + this.$el.attr('class', this.className); + + contEl.addClass(conf.stylePrefix + 'editor-cont'); + contEl.html(this.$el); + + return this; + } + }); });