Browse Source

Fix some issues with the undo manager

pull/67/head
Artur Arseniev 9 years ago
parent
commit
133e6a175f
  1. 11
      src/block_manager/view/BlocksView.js
  2. 34
      src/demo.js
  3. 472
      src/dom_components/main.js
  4. 184
      src/dom_components/view/ComponentsView.js
  5. 978
      src/editor/model/Editor.js
  6. 95
      src/editor/view/EditorView.js

11
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);
}
},

34
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();
});
});

472
src/dom_components/main.js

@ -21,7 +21,7 @@
* @example
* ...
* domComponents: {
* components: '<div>Hello world!</div>',
* components: '<div>Hello world!</div>',
* }
* // 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 <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: function(){
return this.getComponent();
},
/**
* 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: 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<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: 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<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: 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;
},
};
};
};
};
});

184
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;
}
});
});
});

978
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();
},
});
});

95
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;
}
});
});

Loading…
Cancel
Save