diff --git a/.gitignore b/.gitignore index d8c5ed254..b084abea3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ grapes.sublime-workspace img/ private/ vendor/ +coverage/ node_modules/ bower_components/ diff --git a/bower.json b/bower.json index 298b06499..1b459e631 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "grapesjs", "description": "Open source Web Template Editor", - "version": "0.1.2", + "version": "0.1.5", "author": "Artur Arseniev", "homepage": "http://grapesjs.com", "main": [ diff --git a/package.json b/package.json index 79293e23e..dc450b412 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "grapesjs", "description": "Open source Web Template Editor", - "version": "0.1.2", + "version": "0.1.5", "author": "Artur Arseniev", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", @@ -28,6 +28,7 @@ "grunt-contrib-watch": "^0.6.1", "grunt-mocha": "^0.4.15", "grunt-sass": "^1.1.0", + "istanbul": "^0.4.2", "node-sass": "^3.4.2" }, "keywords": [ diff --git a/src/class_manager/config/config.js b/src/class_manager/config/config.js index d337a28b4..12464ffd8 100644 --- a/src/class_manager/config/config.js +++ b/src/class_manager/config/config.js @@ -11,7 +11,7 @@ define(function () { label: 'Classes', // Label for states - statesLabel: 'States', + statesLabel: 'State', }; }); \ No newline at end of file diff --git a/src/class_manager/template/classTag.html b/src/class_manager/template/classTag.html index ba7844d0c..1a9156134 100644 --- a/src/class_manager/template/classTag.html +++ b/src/class_manager/template/classTag.html @@ -1,3 +1,3 @@ - <%= label %> + <%= label %> ⨯ \ No newline at end of file diff --git a/src/class_manager/view/ClassTagsView.js b/src/class_manager/view/ClassTagsView.js index ad1b66a35..dab8074cc 100644 --- a/src/class_manager/view/ClassTagsView.js +++ b/src/class_manager/view/ClassTagsView.js @@ -7,13 +7,11 @@ define(['backbone', 'text!./../template/classTags.html', './ClassTagView'], template: _.template(tagsTemplate), - events:{ - 'click .add': 'startNewClass', - }, + events: {}, initialize: function(o) { this.config = o.config || {}; - this.pfx = this.config.stylePrefix; + this.pfx = this.config.stylePrefix || ''; this.className = this.pfx + 'tags'; this.addBtnId = this.pfx + 'add-tag'; this.newInputId = this.pfx + 'new'; @@ -40,7 +38,7 @@ define(['backbone', 'text!./../template/classTags.html', './ClassTagView'], }, /** - * Start new tag event + * Start tag creation * @param {Object} e * */ @@ -50,7 +48,7 @@ define(['backbone', 'text!./../template/classTags.html', './ClassTagView'], }, /** - * Start new tag event + * End tag creation * @param {Object} e * */ @@ -59,14 +57,15 @@ define(['backbone', 'text!./../template/classTags.html', './ClassTagView'], this.$input.hide().val(''); }, - /** - * Add new class tag - * @param {Object} model - * + * Checks what to do on keyup event + * @param {Object} e */ - addTag: function(model){ - + onInputKeyUp: function(e) { + if (e.keyCode === 13) + this.addNewTag(this.$input.val()); + else if(e.keyCode === 27) + this.endNewTag(); }, /** @@ -79,21 +78,6 @@ define(['backbone', 'text!./../template/classTags.html', './ClassTagView'], this.collection.reset(models); }, - /** - * Checks what to do on keyup event - * @param {Object} e - */ - onInputKeyUp: function(e) { - if (e.keyCode === 13) - this.addNewTag(this.$input.val()); - else if(e.keyCode === 27) - this.endNewTag(); - else{ - //this.searchItem(); - //console.log('search'); - } - }, - /** * Add new tag to collection, if possible, and to the component * @param {Object} e diff --git a/src/commands/view/OpenStyleManager.js b/src/commands/view/OpenStyleManager.js index da42937b4..4cab47f1c 100644 --- a/src/commands/view/OpenStyleManager.js +++ b/src/commands/view/OpenStyleManager.js @@ -24,13 +24,11 @@ define(['StyleManager'], function(StyleManager) { // Class Manager container this.clm = em.get('ClassManager'); if(this.clm){ - /* this.$clm = new this.clm.ClassTagsView({ collection: new this.clm.ClassTags([]), config: this.clm.config, }).render().el; this.$cn2.append(this.$clm); - */ } // Style Manager manager container diff --git a/src/config/require-config.js b/src/config/require-config.js index 84e6cc784..3d2836551 100644 --- a/src/config/require-config.js +++ b/src/config/require-config.js @@ -42,6 +42,7 @@ require.config({ { name: 'RichTextEditor', location: 'rich_text_editor', }, { name: 'ModalDialog', location: 'modal_dialog', }, { name: 'CodeManager', location: 'code_manager', }, + { name: 'CssComposer', location: 'css_composer', }, { name: 'Commands', location: 'commands', }, { name: 'Canvas', location: 'canvas', }, { name: 'Panels', location: 'panels', } diff --git a/src/css_composer/config/config.js b/src/css_composer/config/config.js new file mode 100644 index 000000000..ae35a997a --- /dev/null +++ b/src/css_composer/config/config.js @@ -0,0 +1,11 @@ +define(function () { + return { + + // Style prefix + stylePrefix: 'css-', + + // Default CSS style + 'default': '', + + }; +}); \ No newline at end of file diff --git a/src/css_composer/main.js b/src/css_composer/main.js new file mode 100644 index 000000000..3d6099201 --- /dev/null +++ b/src/css_composer/main.js @@ -0,0 +1,134 @@ +define(function(require) { + /** + * @class CssComposer + * @param {Object} config Configurations + * + * */ + var CssComposer = function(config) + { + var c = config || {}, + def = require('./config/config'), + CssRule = require('./model/CssRule'), + CssRules = require('./model/CssRules'), + Selectors = require('./model/Selectors'), + CssRulesView = require('./view/CssRulesView'); + + for (var name in def) { + if (!(name in c)) + c[name] = def[name]; + } + + //this.qset = { '' : CssRules, '340px': CssRules }; + var rules = new CssRules([]), + rulesView = new CssRulesView({ + collection: rules, + config: c, + }); + + return { + + Selectors: Selectors, + + /** + * Create new rule and return it. Don't add it to the collection + * @param {Array} selectors Array of selectors + * @param {String} state Css rule state + * @param {String} width For which device this style is oriented + * + * @return {Object} + * */ + newRule: function(selectors, state, width) { + var s = state || ''; + var w = width || ''; + var rule = new CssRule({ + state: s, + maxWidth: w, + }); + rule.get('selectors').add(selectors); + return rule; + }, + + /** + * Add new rule to the collection if not yet exists + * @param {Object} rule + * + * @return {Object} + * */ + addRule: function(rule){ + var models = rule.get('selectors').models; + var r = this.getRule(models, rule.get('state'), rule.get('maxWidth')); + if(!r) + r = rules.add(rule); + return r; + }, + + /** + * Get class by its name + * @param {Array} selectors Array of selectors + * @param {String} state Rule status + * @param {String} set Query set + * + * @return {Object|null} + * */ + getRule : function(selectors, state, set) { + var req = _.pluck(selectors, 'cid'); + fRule = null; + rules.each(function(rule){ + if(fRule) + return; + var sel = _.pluck(rule.get('selectors').models, 'cid'); + if(this.same(req, sel)) + fRule = rule; + }, this); + return fRule; + }, + + /** + * Compare 2 arrays to check if are the same + * @param {Array} arr1 + * @param {Array} arr2 + * + * @return {Boolean} + */ + same: function(a1, a2){ + if(a1.length !== a2.length) + return; + + for (var i = 0; i < a1.length; i++) { + var f = 0; + + for (var j = 0; j < a2.length; j++) { + if (a1[i] === a2[j]) + f = 1; + } + + if(f === 0) + return; + } + return true; + }, + + /** + * Get collection of css rules + * + * @return {Object} + * */ + getRules : function() { + return rules; + }, + + /** + * Render block of CSS rules + * + * @return {Object} + */ + render: function(){ + return rulesView.render().el; + } + + }; + }; + + return CssComposer; + +}); \ No newline at end of file diff --git a/src/css_composer/model/CssRule.js b/src/css_composer/model/CssRule.js new file mode 100644 index 000000000..169adfeb4 --- /dev/null +++ b/src/css_composer/model/CssRule.js @@ -0,0 +1,28 @@ +define(['backbone', './Selectors'], + function (Backbone, Selectors) { + /** + * @class CssRule + * */ + return Backbone.Model.extend({ + + defaults: { + // Css selectors + selectors: {}, + // Css properties style + style: {}, + // On which device width this rule should be rendered, eg. @media (max-width: 1000px) + maxWidth: '', + // State of the rule, eg: hover | pressed | focused + state: '', + // Indicates if the rule is stylable + stylable: true, + }, + + initialize: function(c, opt) { + this.config = c || {}; + this.slct = this.config.selectors || []; + this.set('selectors', new Selectors(this.slct)); + }, + + }); +}); diff --git a/src/css_manager/model/CssRules.js b/src/css_composer/model/CssRules.js similarity index 100% rename from src/css_manager/model/CssRules.js rename to src/css_composer/model/CssRules.js diff --git a/src/css_composer/model/Selectors.js b/src/css_composer/model/Selectors.js new file mode 100644 index 000000000..62f8511fc --- /dev/null +++ b/src/css_composer/model/Selectors.js @@ -0,0 +1,29 @@ +define([ 'backbone', 'require'], + function (Backbone, require) { + /** + * @class Selectors + * */ + + return Backbone.Collection.extend({ + + initialize: function(models, opt){ + + this.model = function(attrs, opts) { + var model; + + switch(1){ + + default: + if(!this.ClassTag) + this.ClassTag = require("ClassManager/model/ClassTag"); + model = new this.ClassTag(attrs, opts); + + } + + return model; + }; + + }, + + }); +}); diff --git a/src/css_composer/view/CssRuleView.js b/src/css_composer/view/CssRuleView.js new file mode 100644 index 000000000..32c7169ba --- /dev/null +++ b/src/css_composer/view/CssRuleView.js @@ -0,0 +1,53 @@ +define(['backbone'], + function (Backbone) { + /** + * @class CssRuleView + * */ + return Backbone.View.extend({ + + tagName: 'style', + + initialize: function(o) { + this.config = o.config || {}; + this.listenTo(this.model, 'change:style', this.render); + + }, + + /** + * Returns string of selectors + * @return {String} + */ + renderSelectors: function(){ + var sel = []; + this.model.get('selectors').each(function(m){ + sel.push('.' + m.get('name')); + }); + return sel.join(''); + }, + + /** + * Returns string of properties + * @return {String} + */ + renderProperties: function(){ + var sel = [], + props = this.model.get('style'); + for (var prop in props){ + sel.push(prop + ':' + props[prop] + ';'); + } + return sel.join(''); + }, + + /* + http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript + */ + render : function(){ + if(!this.selStr) + this.selStr = this.renderSelectors(); + var prpStr = this.renderProperties(); + this.$el.html(this.selStr + '{' + prpStr + '}'); + return this; + }, + + }); +}); diff --git a/src/css_composer/view/CssRulesView.js b/src/css_composer/view/CssRulesView.js new file mode 100644 index 000000000..f08bb24bb --- /dev/null +++ b/src/css_composer/view/CssRulesView.js @@ -0,0 +1,62 @@ +define(['backbone','./CssRuleView'], + function (Backbone, CssRuleView) { + /** + * @class CssRulesView + * */ + return Backbone.View.extend({ + + initialize: function(o) { + this.config = o.config; + this.preview = o.preview; + this.pfx = this.config.stylePrefix; + this.listenTo( this.collection, 'add', this.addTo ); + this.listenTo( this.collection, 'reset', this.render ); + }, + + /** + * Add to collection + * @param {Object} model + * */ + addTo: function(model){ + //console.log('Added'); + this.addToCollection(model); + }, + + /** + * Add new object to collection + * @param {Object} model + * @param {Object} fragmentEl + * + * @return {Object} + * */ + addToCollection: function(model, fragmentEl){ + var fragment = fragmentEl || null; + var viewObject = CssRuleView; + + var view = new viewObject({ + model: model, + config: this.config, + }); + var rendered = view.render().el; + + if(fragment) + fragment.appendChild( rendered ); + else + this.$el.append(rendered); + + return rendered; + }, + + render: function() { + var fragment = document.createDocumentFragment(); + this.$el.empty(); + + this.collection.each(function(model){ + this.addToCollection(model, fragment); + }, this); + + this.$el.append(fragment); + return this; + } + }); +}); diff --git a/src/css_manager/config/config.js b/src/css_manager/config/config.js deleted file mode 100644 index 7ec604b92..000000000 --- a/src/css_manager/config/config.js +++ /dev/null @@ -1,8 +0,0 @@ -define(function () { - return { - - // Style prefix - stylePrefix : 'css-', - - }; -}); \ No newline at end of file diff --git a/src/css_manager/main.js b/src/css_manager/main.js deleted file mode 100644 index 20e63a356..000000000 --- a/src/css_manager/main.js +++ /dev/null @@ -1,64 +0,0 @@ -define(function(require) { - /** - * @class CssManager - * @param {Object} config Configurations - * - * */ - var CssManager = function(config) - { - var c = config || {}, - def = require('./config/config'); - this.CssRules = require('./model/CssRules'); - - for (var name in def) { - if (!(name in c)) - c[name] = def[name]; - } - - this.rules = new this.CssRules([]); - this.config = c; - }; - - CssManager.prototype = { - - /** - * Add new class to collection only if it's not already exists - * @param {String} name Class name - * - * @return {Object} Model class - * */ - addRule: function(name){ - var label = name; - var c = this.getClass(name); - if(!c) - return this.classes.add({name: name, label: label}); - return c; - }, - - /** - * Get class by its name - * @param {Array[String]} ids Array of ids - * @param {String} status Rule status - * @param {String} set Query set - * - * @return {Object|null} - * */ - getRule : function(ids, status, set) { - var res = this.classes.where({name: id}); - return res.length ? res[0] : null; - }, - - /** - * Get collection of css rules - * - * @return {Object} - * */ - getRules : function() { - return this.rules; - }, - - }; - - return ClassManager; - -}); \ No newline at end of file diff --git a/src/css_manager/model/CssRule.js b/src/css_manager/model/CssRule.js deleted file mode 100644 index f53aba979..000000000 --- a/src/css_manager/model/CssRule.js +++ /dev/null @@ -1,16 +0,0 @@ -define(['backbone'], - function (Backbone) { - /** - * @class CssRule - * */ - return Backbone.Model.extend({ - - defaults: { - classes: {}, - style: {}, - stylable: true, - state: '', - }, - - }); -}); diff --git a/src/editor/config/config.js b/src/editor/config/config.js index 9b06d527b..d5094f37f 100644 --- a/src/editor/config/config.js +++ b/src/editor/config/config.js @@ -64,6 +64,9 @@ define(function () { //Configurations for Class Manager classManager : {}, + //Configurations for Css Composer + cssComposer : {}, + }; return config; }); \ No newline at end of file diff --git a/src/editor/model/Editor.js b/src/editor/model/Editor.js index 6bddebf0b..b33125eee 100644 --- a/src/editor/model/Editor.js +++ b/src/editor/model/Editor.js @@ -6,6 +6,7 @@ define([ 'StorageManager', 'ModalDialog', 'CodeManager', + 'CssComposer', 'Commands', 'Canvas', 'RichTextEditor', @@ -20,6 +21,7 @@ define([ StorageManager, ModalDialog, CodeManager, + CssComposer, Commands, Canvas, RichTextEditor, @@ -53,10 +55,22 @@ define([ this.initComponents(); this.initCanvas(); this.initUndoManager(); + this.initCssComposer(); this.on('change:selectedComponent', this.componentSelected, this); }, + /** + * Initialize Css Composer + * */ + initCssComposer: function() + { + var cfg = this.config.cssComposer, + pfx = cfg.stylePrefix || 'css-'; + cfg.stylePrefix = this.config.stylePrefix + pfx; + this.set('CssComposer', new CssComposer(cfg)); + }, + /** * Initialize Class manager * */ diff --git a/src/editor/view/EditorView.js b/src/editor/view/EditorView.js index b2d9b153a..a09db6319 100644 --- a/src/editor/view/EditorView.js +++ b/src/editor/view/EditorView.js @@ -4,30 +4,34 @@ function(Backbone){ * @class EditorView * */ return Backbone.View.extend({ - + initialize: function() { - this.cv = this.model.get('Canvas'); - this.pn = this.model.get('Panels'); - this.className = this.model.config.stylePrefix + 'editor'; + this.cv = this.model.get('Canvas'); + this.pn = this.model.get('Panels'); + this.css = this.model.get('CssComposer'); + this.className = this.model.config.stylePrefix + 'editor'; }, render: function(){ this.$el.empty(); - + this.$cont = $('body ' + this.model.config.container); - + this.model.set('$editor', this.$el); - + if(this.cv) this.$el.append(this.cv.render()); - + if(this.pn) this.$el.append(this.pn.render()); - + + if(this.css) + this.$el.append(this.css.render()); + this.$el.attr('class', this.className); - + this.$cont.html(this.$el); - + if(this.pn) this.pn.active(); diff --git a/src/style_manager/view/PropertiesView.js b/src/style_manager/view/PropertiesView.js index 846458fbe..492a56d91 100644 --- a/src/style_manager/view/PropertiesView.js +++ b/src/style_manager/view/PropertiesView.js @@ -1,58 +1,60 @@ -define(['backbone','./PropertyView', './PropertyIntegerView', './PropertyRadioView', './PropertySelectView', - './PropertyColorView', './PropertyFileView', './PropertyCompositeView', './PropertyStackView'], - function (Backbone, PropertyView, PropertyIntegerView, PropertyRadioView, PropertySelectView, +define(['backbone','./PropertyView', './PropertyIntegerView', './PropertyRadioView', './PropertySelectView', + './PropertyColorView', './PropertyFileView', './PropertyCompositeView', './PropertyStackView'], + function (Backbone, PropertyView, PropertyIntegerView, PropertyRadioView, PropertySelectView, PropertyColorView, PropertyFileView, PropertyCompositeView, PropertyStackView) { - /** + /** * @class PropertiesView * */ return Backbone.View.extend({ - + initialize: function(o) { this.config = o.config; this.pfx = this.config.stylePrefix; - this.target = o.target || {}; - this.onChange = o.onChange || {}; - this.onInputRender = o.onInputRender || {}; - this.customValue = o.customValue || {}; + this.target = o.target || {}; + this.propTarget = o.propTarget || {}; + this.onChange = o.onChange || {}; + this.onInputRender = o.onInputRender || {}; + this.customValue = o.customValue || {}; }, - + render: function() { var fragment = document.createDocumentFragment(); - + this.collection.each(function(model){ var objView = PropertyView; - + switch(model.get('type')){ - case 'integer': + case 'integer': objView = PropertyIntegerView; break; - case 'radio': + case 'radio': objView = PropertyRadioView; break; - case 'select': + case 'select': objView = PropertySelectView; break; - case 'color': + case 'color': objView = PropertyColorView; break; - case 'file': + case 'file': objView = PropertyFileView; break; - case 'composite': + case 'composite': objView = PropertyCompositeView;break; - case 'stack': + case 'stack': objView = PropertyStackView; break; } - + var view = new objView({ model : model, name : model.get('name'), id : this.pfx + model.get('property'), target : this.target, + propTarget : this.propTarget, onChange : this.onChange, onInputRender : this.onInputRender, config : this.config, }); - + if(model.get('type') != 'composite'){ view.customValue = this.customValue; } - + fragment.appendChild(view.render().el); },this); diff --git a/src/style_manager/view/PropertyCompositeView.js b/src/style_manager/view/PropertyCompositeView.js index af953d2ee..310015858 100644 --- a/src/style_manager/view/PropertyCompositeView.js +++ b/src/style_manager/view/PropertyCompositeView.js @@ -1,36 +1,36 @@ -define(['backbone','./PropertyView', 'text!./../templates/propertyComposite.html','require'], +define(['backbone','./PropertyView', 'text!./../templates/propertyComposite.html','require'], function (Backbone, PropertyView, propertyTemplate, require) { - /** + /** * @class PropertyCompositeView * */ return PropertyView.extend({ - + template: _.template(propertyTemplate), - + initialize: function(o) { PropertyView.prototype.initialize.apply(this, arguments); _.bindAll(this, 'build'); this.config = o.config; this.className = this.className + ' '+ this.pfx +'composite'; }, - + /** * Renders input - * + * * @return void * */ renderInput: function() { var props = this.model.get('properties'); if(props && props.length){ - if(!this.$input) + if(!this.$input) this.$input = $('', {value: 0, type: 'hidden' }); - + if(!this.props){ var Properties = require('./../model/Properties'); this.props = new Properties(props); this.model.set('properties', this.props); } - + if(!this.$props){ //Avoid style for children this.props.each(function(prop, index){ @@ -43,15 +43,16 @@ define(['backbone','./PropertyView', 'text!./../templates/propertyComposite.html console.warn(prop.get('property')+' of type composite not yet allowed.'); } },this); - + var PropertiesView = require('./PropertiesView'); var that = this; var propsView = new PropertiesView({ config : this.config, - collection : this.props, + collection : this.props, target : this.target, + propTarget : this.propTarget, onChange : function(el, model){ - var result = that.build(el, model); + var result = that.build(el, model); that.model.set('value', result); }, onInputRender : function(property, mIndex){ @@ -67,10 +68,10 @@ define(['backbone','./PropertyView', 'text!./../templates/propertyComposite.html } } }, - + /** * Get default value of the property - * + * * @return string * */ getDefaultValue: function(){ @@ -80,12 +81,12 @@ define(['backbone','./PropertyView', 'text!./../templates/propertyComposite.html }); return str.replace(/ +$/,''); }, - + /** * Extract string from composite value * @param integer Index * @param object Property model - * + * * @return string * */ valueOnIndex: function(index, model){ @@ -102,12 +103,12 @@ define(['backbone','./PropertyView', 'text!./../templates/propertyComposite.html } return result; }, - + /** * Build composite value * @param Object Selected element * @param Object Property model - * + * * @return string * */ build: function(selectedEl, propertyModel){ diff --git a/src/style_manager/view/PropertyView.js b/src/style_manager/view/PropertyView.js index ab6670898..b6ea12a46 100644 --- a/src/style_manager/view/PropertyView.js +++ b/src/style_manager/view/PropertyView.js @@ -5,8 +5,8 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], * */ return Backbone.View.extend({ - template: _.template(propertyTemplate), - templateLabel: _.template(propertyTemplate), + template: _.template(propertyTemplate), + templateLabel: _.template(propertyTemplate), events: { 'change' : 'valueChanged', @@ -16,6 +16,7 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], this.config = o.config; this.pfx = this.config.stylePrefix || ''; this.target = o.target || {}; + this.propTarget = o.propTarget || {}; this.onChange = o.onChange || {}; this.onInputRender = o.onInputRender || {}; this.customValue = o.customValue || {}; @@ -29,45 +30,26 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], this.list = this.model.get('list'); this.input = this.$input = null; this.className = this.pfx + 'property'; - this.selectedComponent = this.target.get('selectedComponent'); - - if(this.selectedComponent){ - this.componentValue = this.selectedComponent.get('style')[this.property]; - } - - this.listenTo( this.target ,'change:selectedComponent',this.componentSelected); + this.listenTo( this.propTarget, 'update', this.targetUpdated); this.listenTo( this.model ,'change:value', this.valueChanged); }, /** - * Rerender property for the new selected component, if necessary - * @param {Array[Model, value, options]} e - * + * Fired when the target is updated * */ - componentSelected: function(e){ - this.selectedComponent = this.target.get('selectedComponent'); + targetUpdated: function(){ + this.selectedComponent = this.propTarget.model; if(this.selectedComponent){ - /* - var classes = this.selectedComponent.get('classes'); - if(classes.length){ - var valid = _.filter(classes.models, function(item){ return item.get('active'); }); - var ids = _.pluck(valid, 'cid'); - var cssBlock = '';//this.sm.get('CssManager').getRule(ids, 'status', 'mediaq'); - } - */ - //I will rerender it only if the assigned one is different from the actuale value - //console.log('property '+this.property+" view: "+this.componentValue+" model: "+ this.model.get('value')); - if( !this.sameValue() ){ + if(!this.sameValue()) this.renderInputRequest(); - } } }, /** - * Checks if the value from selected component is the same with - * the value of the model + * Checks if the value from selected component is the + * same of the value of the model * - * @return boolean + * @return {Boolean} * */ sameValue: function(){ return this.getComponentValue() == (this.model.get('value') + this.model.get('unit')); @@ -86,7 +68,7 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], if(this.selectedComponent.get('style')[this.property]) this.componentValue = this.selectedComponent.get('style')[this.property]; else - this.componentValue = this.defaultValue + (this.unit ? this.unit : ''); + this.componentValue = this.defaultValue + (this.unit || ''); // Check if wrap inside function is required if(this.func){ @@ -95,11 +77,11 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], this.componentValue = v; } - //This allow to ovveride the normal flow of selecting component value, - //useful in composite properties + // This allow to ovveride the normal flow of selecting component value, + // useful in composite properties if(this.customValue && typeof this.customValue === "function"){ - var index = this.model.collection.indexOf(this.model); - var t = this.customValue(this, index); + var index = this.model.collection.indexOf(this.model); + var t = this.customValue(this, index); if(t) this.componentValue = t; } @@ -108,7 +90,7 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], }, /** - * Fetch string from function type value + * Fetch the string from function type value * @param {String} v Function type value * * @return {String} @@ -122,8 +104,6 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], * @param {Object} e Events * @param {Mixed} val Value * @param {Object} opt Options - * - * @return void * */ valueChanged: function(e, val, opt){ if(!this.selectedComponent) @@ -154,7 +134,7 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], componentCss[this.property] = value; this.selectedComponent.set('style', componentCss, { avoidStore : avSt}); } - this.selectedValue = value;//TODO ? + this.selectedValue = value; if(this.onChange && typeof this.onChange === "function"){ this.onChange(this.selectedComponent, this.model); @@ -163,9 +143,8 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], /** * Set value to the input - * @param String value - * - * @return void + * @param {String} value + * @param {Boolean} force * */ setValue: function(value, force){ var f = force===0 ? 0 : 1; @@ -175,13 +154,11 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], } if(this.$input) this.$input.val(v); - this.model.set({value: v},{silent: true}); + this.model.set({value: v}, {silent: true}); }, /** * Render label - * - * @return void * */ renderLabel: function(){ this.$el.html( this.templateLabel({ @@ -194,8 +171,6 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], /** * Render field property - * - * @return void * */ renderField : function() { this.renderTemplate(); @@ -205,8 +180,6 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], /** * Render loaded template - * - * @return void * */ renderTemplate: function(){ this.$el.append( this.template({ @@ -219,8 +192,6 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], /** * Renders input, to override - * - * @return void * */ renderInput: function(){ console.warn("No render input implemented for '"+this.model.get('type')+"'"); @@ -228,8 +199,6 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], /** * Request to render input of the property - * - * @return void * */ renderInputRequest: function(){ this.renderInput(); @@ -241,8 +210,6 @@ define(['backbone', 'text!./../templates/propertyLabel.html'], /** * Clean input - * - * @return void * */ cleanValue: function(){ this.setValue(''); diff --git a/src/style_manager/view/SectorView.js b/src/style_manager/view/SectorView.js index dc583cd48..60fc4e608 100644 --- a/src/style_manager/view/SectorView.js +++ b/src/style_manager/view/SectorView.js @@ -13,6 +13,7 @@ define(['backbone', './PropertiesView', 'text!./../templates/sector.html'], this.config = o.config; this.pfx = this.config.stylePrefix; this.target = o.target || {}; + this.propTarget = o.propTarget || {}; this.open = this.model.get('open'); this.caretR = 'fa-caret-right'; this.caretD = 'fa-caret-down'; @@ -82,6 +83,7 @@ define(['backbone', './PropertiesView', 'text!./../templates/sector.html'], var view = new PropertiesView({ collection : objs, target : this.target, + propTarget : this.propTarget, config : this.config, }); this.$el.append(view.render().el); diff --git a/src/style_manager/view/SectorsView.js b/src/style_manager/view/SectorsView.js index dd472d884..3d79e8f0f 100644 --- a/src/style_manager/view/SectorsView.js +++ b/src/style_manager/view/SectorsView.js @@ -9,6 +9,46 @@ define(['backbone','./SectorView'], this.config = o.config; this.pfx = this.config.stylePrefix; this.target = o.target || {}; + + // The taget that will emit events for properties + this.propTarget = {}; + _.extend(this.propTarget, Backbone.Events); + this.listenTo( this.target, 'change:selectedComponent', this.targetUpdated); + + }, + + /** + * Fired when target is updated + */ + targetUpdated: function() { + var el = this.target.get('selectedComponent'); + + if(!el) + return; + + var classes = el.get('classes'); + var pt = this.propTarget; + + if(classes.length){ + var cssC = this.target.get('CssComposer'); + var valid = _.filter(classes.models, function(item){ + return item.get('active'); + }); + var iContainer = cssC.getRule(valid, 'status', 'mediaq'); + if(!iContainer){ + iContainer = cssC.newRule(valid, 'status', 'mediaq'); + // Hydrate styles from component element + iContainer.set('style', el.get('style')); + cssC.addRule(iContainer); + el.set('style', {}); + } + pt.model = iContainer; + pt.trigger('update'); + return; + } + + pt.model = el; + pt.trigger('update'); }, render: function() { @@ -21,6 +61,7 @@ define(['backbone','./SectorView'], name : obj.get('name'), properties : obj.get('properties'), target : this.target, + propTarget : this.propTarget, config : this.config, }); fragment.appendChild(view.render().el); diff --git a/test/runner/main.js b/test/runner/main.js index 24824b47e..45ac312fe 100644 --- a/test/runner/main.js +++ b/test/runner/main.js @@ -13,6 +13,7 @@ require(['../src/config/require-config.js', 'config/config.js'], function() { 'specs/asset_manager/view/FileUploader.js', 'specs/dom_components/main.js', 'specs/class_manager/main.js', + 'specs/css_composer/main.js', ], function(chai) { var should = chai.should(), diff --git a/test/specs/class_manager/main.js b/test/specs/class_manager/main.js index bac48981f..ae40d3507 100644 --- a/test/specs/class_manager/main.js +++ b/test/specs/class_manager/main.js @@ -4,12 +4,14 @@ define([ 'ClassManager', modulePath + '/model/ClassModels', modulePath + '/view/ClassTagView', + modulePath + '/view/ClassTagsView', modulePath + '/e2e/ClassManager' ], function( ClassManager, Models, ClassTagView, + ClassTagsView, e2e ) { @@ -84,6 +86,7 @@ define([ Models.run(); ClassTagView.run(); + ClassTagsView.run(); e2e.run(); }); diff --git a/test/specs/class_manager/view/ClassTagsView.js b/test/specs/class_manager/view/ClassTagsView.js new file mode 100644 index 000000000..deec76b63 --- /dev/null +++ b/test/specs/class_manager/view/ClassTagsView.js @@ -0,0 +1,143 @@ +var path = 'ClassManager/view/'; +define([path + 'ClassTagsView', 'ClassManager/model/ClassTags'], + function(ClassTagsView, ClassTags) { + + return { + run : function(){ + describe('ClassTagsView', function() { + + before(function () { + this.$fixtures = $("#fixtures"); + this.$fixture = $('
'); + }); + + beforeEach(function () { + this.target = { get: function(){} }; + this.coll = new ClassTags(); + _.extend(this.target, Backbone.Events); + + this.view = new ClassTagsView({ + config : { target: this.target }, + collection: this.coll + }); + + this.$fixture.empty().appendTo(this.$fixtures); + this.$fixture.html(this.view.render().el); + this.btnAdd = this.view.$el.find('#' + this.view.addBtnId); + this.input = this.view.$el.find('input#' + this.view.newInputId); + this.$tags = this.$fixture.find('#tags-c'); + }); + + afterEach(function () { + delete this.view.collection; + }); + + after(function () { + this.$fixture.remove(); + }); + + it('Object exists', function() { + ClassTagsView.should.be.exist; + }); + + it('Not tags inside', function() { + this.$tags.html().should.equal(''); + }); + + it('Add new tag triggers correct method', function() { + sinon.stub(this.view, "addToClasses"); + this.coll.add({ name: 'test' }); + this.view.addToClasses.calledOnce.should.equal(true); + }); + + it('Start new tag creation', function() { + this.btnAdd.click(); + (this.btnAdd.css('display') == 'none').should.equal(true); + (this.input.css('display') !== 'none').should.equal(true); + }); + + it('Stop tag creation', function() { + this.btnAdd.click(); + this.input.val('test') + this.input.blur(); + (this.btnAdd.css('display') !== 'none').should.equal(true); + (this.input.css('display') == 'none').should.equal(true); + this.input.val().should.equal(''); + }); + + it('Check keyup of ESC on input', function() { + this.btnAdd.click(); + sinon.stub(this.view, "addNewTag"); + this.input.trigger({ + type: 'keyup', + keyCode: 13 + }); + this.view.addNewTag.calledOnce.should.equal(true); + }); + + it('Check keyup on ENTER on input', function() { + this.btnAdd.click(); + sinon.stub(this.view, "endNewTag"); + this.input.trigger({ + type: 'keyup', + keyCode: 27 + }); + this.view.endNewTag.calledOnce.should.equal(true); + }); + + it('Collection changes on update of target', function() { + this.coll.add({ name: 'test' }); + this.target.trigger('change:selectedComponent'); + this.coll.length.should.equal(0); + }); + + it('Collection reacts on reset', function() { + this.coll.add([{ name: 'test1' }, { name: 'test2' }]); + sinon.stub(this.view, "addToClasses"); + this.coll.trigger('reset'); + this.view.addToClasses.calledTwice.should.equal(true); + }); + + it("Don't accept empty tags", function() { + this.view.addNewTag(''); + this.$tags.html().should.equal(''); + }); + + it("Accept new tags", function() { + sinon.stub(this.target, "get").returns({ + addClass: function(v){ + return {name: v}; + } + }); + this.view.compTarget = { + get: function(){ + return { add: function(){} }; + } + }; + this.view.addNewTag('test'); + this.view.addNewTag('test2'); + this.$tags.children().length.should.equal(2); + }); + + it("New tag correctly added", function() { + this.coll.add({ label: 'test' }); + this.$tags.children().first().find('#tag-label').html().should.equal('test'); + }); + + describe('Should be rendered correctly', function() { + it('Has label', function() { + this.view.$el.find('#label').should.have.property(0); + }); + it('Has tags container', function() { + this.view.$el.find('#tags-c').should.have.property(0); + }); + it('Has add button', function() { + this.view.$el.find('#add-tag').should.have.property(0); + }); + }); + + }); + } + }; + +}); \ No newline at end of file diff --git a/test/specs/css_composer/main.js b/test/specs/css_composer/main.js new file mode 100644 index 000000000..06a8c7e9b --- /dev/null +++ b/test/specs/css_composer/main.js @@ -0,0 +1,84 @@ +var modulePath = './../../../test/specs/css_composer'; + +define([ + 'CssComposer', + modulePath + '/model/CssModels' + ], + function( + CssComposer, + Models, + Selectors + ) { + + describe('Css Composer', function() { + + describe('Main', function() { + + beforeEach(function () { + this.obj = new CssComposer(); + }); + + afterEach(function () { + delete this.obj; + }); + + it('Object exists', function() { + CssComposer.should.be.exist; + }); + + it("Rules are empty", function() { + this.obj.getRules().length.should.equal(0); + }); + + it('Create new rule with correct selectors', function() { + var sel = new this.obj.Selectors(); + var s1 = sel.add({name: 'test1'}); + var rule = this.obj.newRule(sel.models); + rule.get('selectors').at(0).should.deep.equal(s1); + }); + + it('Create new rule correctly', function() { + var sel = new this.obj.Selectors(); + var s1 = sel.add({name: 'test1'}); + var rule = this.obj.newRule(sel.models, 'state1', 'width1'); + rule.get('state').should.equal('state1'); + rule.get('maxWidth').should.equal('width1'); + }); + + it("Add rule to collection", function() { + var sel = new this.obj.Selectors([{name: 'test1'}]); + var rule = this.obj.newRule(sel.models, 'state1', 'width1'); + this.obj.addRule(rule); + this.obj.getRules().length.should.equal(1); + this.obj.getRules().at(0).get('selectors').at(0).get('name').should.equal('test1'); + }); + +/* + it("Don't duplicate rules", function() { + var sel1 = new this.obj.Selectors([{name: 'test1'}]); + var rule1 = this.obj.newRule(sel1.models, 'state1', 'width1'); + var aRule1 = this.obj.addRule(rule1); + + var sel2 = new this.obj.Selectors([{name: 'test1'}]); + var rule2 = this.obj.newRule(sel2.models, 'state1', 'width1'); + var aRule2 = this.obj.addRule(rule2); + + console.log(sel1); + console.log(rule1); + console.log(aRule1); + console.log(sel2); + console.log(rule2); + console.log(aRule2); + console.log(this.obj.getRules().length); + + aRule1.should.deep.equal(aRule2); + + }); +*/ + + }); + + Models.run(); + + }); +}); \ No newline at end of file diff --git a/test/specs/css_composer/model/CssModels.js b/test/specs/css_composer/model/CssModels.js new file mode 100644 index 000000000..4eb223da2 --- /dev/null +++ b/test/specs/css_composer/model/CssModels.js @@ -0,0 +1,60 @@ +var path = 'CssComposer/model/'; +define([path + 'CssRule', + path + 'CssRules', + path + 'Selectors', + 'ClassManager/model/ClassTag'], + function(CssRule, CssRules, Selectors, ClassTag) { + + return { + run : function(){ + describe('CssRule', function() { + + beforeEach(function () { + this.obj = new CssRule(); + }); + + afterEach(function () { + delete this.obj; + }); + + it('Has selectors property', function() { + this.obj.has('selectors').should.equal(true); + }); + + it('Has style property', function() { + this.obj.has('style').should.equal(true); + }); + + it('Has state property', function() { + this.obj.has('state').should.equal(true); + }); + + it('No default selectors', function() { + this.obj.get('selectors').length.should.equal(0); + }); + + }); + + describe('CssRules', function() { + + it('Creates collection item correctly', function() { + var c = new CssRules(); + var m = c.add({}); + m.should.be.an.instanceOf(CssRule); + }); + + }); + + describe('Selectors', function() { + + it('Creates collection item correctly', function() { + var c = new Selectors(); + var m = c.add({}); + m.should.be.an.instanceOf(ClassTag); + }); + + }); + } + }; + +}); \ No newline at end of file