├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── Gruntfile.js ├── HISTORY.md ├── README.md ├── bower.json ├── examples ├── css │ ├── bootstrap-responsive.css │ ├── bootstrap-responsive.min.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── images │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ ├── spacing.css │ └── style.css └── simple.html ├── lib ├── amd │ ├── backbone.computedfields.js │ └── backbone.computedfields.min.js ├── backbone.computedfields.js └── backbone.computedfields.min.js ├── package.json ├── src ├── amd.js └── backbone.computedfields.js └── test ├── index.html ├── lib ├── chai.js ├── expect.js ├── mocha │ ├── mocha.css │ └── mocha.js └── sinon │ └── sinon.js ├── runner └── mocha.js └── spec └── backbone.computedfields.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | *.swo 4 | node_modules 5 | _SpecRunner.html 6 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | lib/ 3 | src/amd.js 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "after", 4 | "afterEach", 5 | "before", 6 | "beforeEach", 7 | "chai", 8 | "describe", 9 | "expect", 10 | "iit", 11 | "it", 12 | "runs", 13 | "sinon", 14 | "spyOn", 15 | "waits", 16 | "waitsFor", 17 | "xit", 18 | "xdescribe", 19 | "module", 20 | "exports", 21 | "require", 22 | "window", 23 | "process", 24 | "console", 25 | "__dirname", 26 | "Buffer", 27 | "define", 28 | "Backbone", 29 | "_" 30 | ], 31 | 32 | "asi" : false, 33 | "bitwise" : true, 34 | "boss" : false, 35 | "curly" : true, 36 | "debug": false, 37 | "devel": false, 38 | "eqeqeq": true, 39 | "evil": false, 40 | "expr": true, 41 | "forin": false, 42 | "immed": true, 43 | "latedef" : false, 44 | "laxbreak": false, 45 | "multistr": true, 46 | "newcap": true, 47 | "noarg": true, 48 | "node" : false, 49 | "browser": true, 50 | "noempty": false, 51 | "nonew": true, 52 | "onevar": false, 53 | "plusplus": false, 54 | "regexp": false, 55 | "strict": false, 56 | "sub": false, 57 | "trailing" : true, 58 | "undef": true, 59 | "unused": "vars" 60 | } 61 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | node_modules 3 | examples 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install -g grunt-cli -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | meta: { 8 | source: 'backbone.computedfields.js', 9 | sourceMin: 'backbone.computedfields.min.js', 10 | banner: '// Backbone.ComputedFields, v<%= pkg.version %>\n' + 11 | '// Copyright (c)<%= grunt.template.today("yyyy") %> alexander.beletsky@gmail.com\n' + 12 | '// Distributed under MIT license\n' + 13 | '// <%= pkg.homepage %>\n\n' 14 | }, 15 | 16 | mocha: { 17 | test: { 18 | src: ['test/index.html'], 19 | options: { 20 | run: true 21 | } 22 | } 23 | }, 24 | 25 | rig: { 26 | standard: { 27 | src: ['src/<%= meta.source %>'], 28 | dest: 'lib/<%= meta.source %>' 29 | }, 30 | amd: { 31 | src: ['src/amd.js'], 32 | dest: 'lib/amd/<%= meta.source %>' 33 | } 34 | }, 35 | 36 | concat: { 37 | options: { 38 | stripBanners: true, 39 | banner: '<%= meta.banner %>' 40 | }, 41 | standard: { 42 | src: ['<%= meta.banner %>', '<%= rig.standard.dest %>'], 43 | dest: '<%= rig.standard.dest %>' 44 | }, 45 | amd: { 46 | src: ['<%= meta.banner %>', '<%= rig.amd.dest %>'], 47 | dest: '<%= rig.amd.dest %>' 48 | } 49 | }, 50 | 51 | uglify: { 52 | standard: { 53 | files: { 54 | 'lib/<%= meta.sourceMin %>': ['<%= concat.standard.dest %>'] 55 | } 56 | }, 57 | amd: { 58 | files: { 59 | 'lib/amd/<%= meta.sourceMin %>': ['<%= concat.amd.dest %>'] 60 | } 61 | } 62 | }, 63 | 64 | jshint: { 65 | options: { 66 | curly: true, 67 | eqeqeq: true, 68 | immed: false, 69 | latedef: true, 70 | newcap: true, 71 | noarg: true, 72 | sub: true, 73 | undef: true, 74 | boss: true, 75 | eqnull: true, 76 | browser: true, 77 | globals: { 78 | jQuery: true, 79 | Backbone: true, 80 | _: true, 81 | Marionette: true, 82 | $: true, 83 | slice: true 84 | } 85 | }, 86 | js: ['src/<%= meta.source %>'] 87 | } 88 | }); 89 | 90 | // Laoded tasks 91 | grunt.loadNpmTasks('grunt-rigger'); 92 | grunt.loadNpmTasks('grunt-mocha'); 93 | grunt.loadNpmTasks('grunt-contrib-concat'); 94 | grunt.loadNpmTasks('grunt-contrib-jshint'); 95 | grunt.loadNpmTasks('grunt-contrib-uglify'); 96 | 97 | // Default task. 98 | grunt.registerTask('default', ['jshint', 'mocha', 'rig', 'concat', 'uglify']); 99 | grunt.registerTask('test', ['mocha']); 100 | }; 101 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## Versions / Changes 2 | 3 | ### v.0.0.9 4 | 5 | * Corrected for `browserify` usage 6 | 7 | ### v.0.0.8 8 | 9 | * Remove project version from code 10 | 11 | ### v.0.0.7 19 November, 2013 12 | 13 | * pass toJSON options and add option to included computed fields [#18](https://github.com/alexanderbeletsky/backbone-computedfields/commit/91e31ce4c5264cd8f9d5df1204539055cfb4e369) 14 | 15 | ### v.0.0.6 23 October, 2013 16 | 17 | * Fix project url 18 | * Fix project npm name 19 | * Fix compatibility with underscore 1.5.0 [#12](https://github.com/alexanderbeletsky/backbone-computedfields/commit/30f8a25346dbb31665ed1f8defeca794a06bac15) [#15](https://github.com/alexanderbeletsky/backbone-computedfields/commit/2e6dcd4ffe991a7d017e0821dc38cd4198070f04) 20 | * Use mocha 21 | * Use travis 22 | * Update Grunt to ~0.4.0 23 | * Add bower.json 24 | 25 | ### v.0.0.5 17 February, 2013 26 | 27 | * AMD support added 28 | 29 | ### v.0.0.4 26 December, 2012 30 | 31 | * Support for Backbone 0.9.9 32 | * Removed 'silent' updates, since it's not supported in 0.9.9 33 | 34 | ### v.0.0.3 12 December, 2012 35 | 36 | * Breaking change: computed fields are wrapped in `computed` object. 37 | * Dependency on external object 38 | 39 | ### v.0.0.2 11 December, 2012 40 | 41 | * Silent fields implemented 42 | * Several bug fixes 43 | 44 | ### v.0.0.1 18 November, 2012 45 | 46 | * Initial version: basic functions, events 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/alexbeletsky/backbone-computedfields.svg?branch=master)](https://travis-ci.org/alexbeletsky/backbone-computedfields) 2 | [![Dependency Status](https://david-dm.org/alexbeletsky/backbone-computedfields.svg)](https://david-dm.org/alexbeletsky/backbone-computedfields) 3 | [![devDependency Status](https://david-dm.org/alexbeletsky/backbone-computedfields/dev-status.svg)](https://david-dm.org/alexbeletsky/backbone-computedfields#info=devDependencies) 4 | 5 | # Backbone.ComputedFields 6 | 7 | 8 | 9 | Inspired by Derik Bailey's [Backbone.Computed](https://github.com/derickbailey/backbone.compute), Backbone.ComputedFields aims the same goal, but polished for real project needs. 10 | 11 | ## Quick start 12 | 13 | Instantiated in `initialize` method, 14 | 15 | ```js 16 | initialize: function () { 17 | this.computedFields = new Backbone.ComputedFields(this); 18 | }, 19 | ``` 20 | 21 | ComputedField is declared as `computed` in model, 22 | 23 | ```js 24 | computed: { 25 | 26 | } 27 | ``` 28 | 29 | All properties inside are treated as computed fields. 30 | 31 | ```js 32 | computed: { 33 | grossPrice: { 34 | get: function () { 35 | return 105; 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | `computed` can also be a function returning an object. 42 | 43 | ```js 44 | computed: function() { 45 | return { 46 | grossPrice: { 47 | get: function () { 48 | return 105; 49 | } 50 | } 51 | }; 52 | } 53 | ``` 54 | 55 | Each property that declares `get` or `set` method is treated as computed. 56 | 57 | Get the value of computed property, 58 | 59 | ```js 60 | model.get('grossPrice'); // -> 105 is returned 61 | ``` 62 | 63 | ## Dependent fields 64 | 65 | In case that computed field depends on some other models fields, 66 | 67 | ```js 68 | computed: { 69 | grossPrice: { 70 | depends: ['netPrice', 'vatRate'], 71 | get: function (fields) { 72 | return fields.netPrice * (1 + fields.vatRate / 100); 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | Add `depends` object into computed field object, as array of dependent fields. Dependent fields are injected into corresponding `get` method, by passing initialized `fields` object inside, 79 | 80 | ```js 81 | var Model = Backbone.Model.extend({ 82 | defaults: { 83 | 'netPrice': 0.0, 84 | 'vatRate': 0.0 85 | }, 86 | 87 | initialize: function () { 88 | this.computedFields = new Backbone.ComputedFields(this); 89 | }, 90 | 91 | computed: { 92 | grossPrice: { 93 | depends: ['netPrice', 'vatRate'], 94 | get: function (fields) { 95 | return fields.netPrice * (1 + fields.vatRate / 100); 96 | } 97 | } 98 | } 99 | }); 100 | 101 | model = new Model({ netPrice: 100, vatRate: 20}); 102 | model.get('grossPrice') // -> 120 is returned 103 | ``` 104 | 105 | ## Setting computed values 106 | 107 | Besides of `get` computed field might have `set` method as well. 108 | 109 | ```js 110 | computed: { 111 | grossPrice: { 112 | depends: ['netPrice', 'vatRate'], 113 | get: function (fields) { 114 | return fields.netPrice * (1 + fields.vatRate / 100); 115 | }, 116 | set: function (value, fields) { 117 | fields.netPrice = value / (1 + fields.vatRate / 100); 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | `set` function receives the `fields` object, with same names of properties as model attributes. If `set` function changes the value of property, the change is propagated to model. Typically, you should change only one field in `set` method. 124 | 125 | ## Model changes 126 | 127 | In case of depended field is changed, computed field is automatically updated 128 | 129 | ```js 130 | model.set({vatRate: 5}); 131 | model.get('grossPrice'); // -> 105 is returned 132 | 133 | // or 134 | 135 | model.set({netPrice: 120}); 136 | model.get('grossPrice'); // -> 126 is returned 137 | ``` 138 | 139 | In case of calculated field is changed, dependent field in automatically updated 140 | 141 | ```js 142 | model.set({grossPrice: 105}); 143 | model.get('netPrice'); // -> 100 is returned 144 | ``` 145 | 146 | ## Model events 147 | 148 | To make views works correctly, it important to keep correct events distribution. 149 | 150 | In case of depended field is changed, 151 | 152 | ```js 153 | model.set({netPrice: 120}); 154 | ``` 155 | 156 | After that call, several events are triggered - `change:netPrice`, as a reaction of `grossPrice` updated, `change:grossPrice` is triggered. 157 | 158 | In case of computed field is changed, 159 | 160 | ```js 161 | model.set({grossPrice: 80}); 162 | ``` 163 | 164 | After that call, several events are triggered - `change:grossPrice`, as a reaction of `netPrice` updated, `change:netPrice` is triggered. 165 | 166 | ## Model validation 167 | 168 | The same rules as for usual Backbone.js model attributes rules are applied for computed ones. If model contains `validate()` method and invalid is being set, the change would not propagate into model attributes, `error` event is triggered instead. 169 | 170 | Say, we have such validation function, 171 | 172 | ```js 173 | validate: function (attrs) { 174 | 175 | var errors = []; 176 | if (!_.isNumber(attrs.netPrice) || attrs.netPrice < 0) { 177 | errors.push('netPrice is invalid'); 178 | } 179 | 180 | if (!_.isNumber(attrs.grossPrice) || attrs.grossPrice < 0) { 181 | errors.push('grossPrice is invalid'); 182 | } 183 | 184 | return errors.length > 0 ? errors : false; 185 | } 186 | ``` 187 | 188 | And change computed field, 189 | 190 | ```js 191 | model.set({grossPrice: ''}); 192 | ``` 193 | 194 | The model is will remain in valid state, `{ netPrice: 100, vatRate: 20, grossPrice: 120 }`. 195 | 196 | ## Dependency function 197 | 198 | Computed field might have dependency not only on internal model attributes, but on external objects too. For instance, the product show price depends on currency selected by user in currency widget. Besides properties names, `depends: []` can accept function, that is responsible to fire callback if change occurred. 199 | 200 | ```js 201 | computed: { 202 | grossPrice: { 203 | depends: ['netPrice', 'vatRate', function (callback) { 204 | this.external.on('change:value', callback); 205 | }], 206 | get: function (fields) { 207 | return this.external.get('value'); 208 | } 209 | } 210 | } 211 | ``` 212 | 213 | ## JSON payload 214 | 215 | By default all computed fields are treated as part of JSON payload, 216 | 217 | ```js 218 | model.toJSON() // -> { "netPrice": 100, "grossPrice": 120, "vatRate": 20 }; 219 | ``` 220 | 221 | To disable that add `toJSON: false` in computed field declaration, 222 | 223 | ```js 224 | computed: { 225 | grossPrice: { 226 | depends: ['netPrice', 'vatRate'], 227 | get: function (fields) { 228 | return fields.netPrice * (1 + fields.vatRate / 100); 229 | }, 230 | set: function (value, fields) { 231 | fields.netPrice = value / (1 + fields.vatRate / 100); 232 | }, 233 | toJSON: false 234 | } 235 | } 236 | ``` 237 | 238 | If you'd like to force the computed fields into the JSON payload even if the `toJSON` option is `false`, pass 239 | `computedFields: true` to the `toJSON` function: 240 | 241 | ```js 242 | model.toJSON({ computedFields: true }) 243 | ``` 244 | 245 | ## More details 246 | 247 | Up-to-date and complete documentation is located at [/test/spec/backbone.computedfields.spec.js](https://github.com/alexanderbeletsky/backbone.computedfields/blob/master/test/spec/backbone.computedfields.spec.js). 248 | 249 | # Legal Info (MIT License) 250 | 251 | Copyright (c) 2012 Alexander Beletsky 252 | 253 | Permission is hereby granted, free of charge, to any person obtaining a copy 254 | of this software and associated documentation files (the "Software"), to deal 255 | in the Software without restriction, including without limitation the rights 256 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 257 | copies of the Software, and to permit persons to whom the Software is 258 | furnished to do so, subject to the following conditions: 259 | 260 | The above copyright notice and this permission notice shall be included in 261 | all copies or substantial portions of the Software. 262 | 263 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 264 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 265 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 266 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 267 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 268 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 269 | THE SOFTWARE. 270 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone-computedfields", 3 | "version": "0.0.7", 4 | "main": "lib/backbone.computedfields.js", 5 | "ignore": [ 6 | "test", 7 | "node_modules" 8 | ], 9 | "dependencies" : { 10 | "underscore": ">=1.5.0", 11 | "backbone": ">=0.1.0" 12 | } 13 | } -------------------------------------------------------------------------------- /examples/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.1.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | .clearfix { 12 | *zoom: 1; 13 | } 14 | 15 | .clearfix:before, 16 | .clearfix:after { 17 | display: table; 18 | line-height: 0; 19 | content: ""; 20 | } 21 | 22 | .clearfix:after { 23 | clear: both; 24 | } 25 | 26 | .hide-text { 27 | font: 0/0 a; 28 | color: transparent; 29 | text-shadow: none; 30 | background-color: transparent; 31 | border: 0; 32 | } 33 | 34 | .input-block-level { 35 | display: block; 36 | width: 100%; 37 | min-height: 30px; 38 | -webkit-box-sizing: border-box; 39 | -moz-box-sizing: border-box; 40 | box-sizing: border-box; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | visibility: hidden; 46 | } 47 | 48 | .visible-phone { 49 | display: none !important; 50 | } 51 | 52 | .visible-tablet { 53 | display: none !important; 54 | } 55 | 56 | .hidden-desktop { 57 | display: none !important; 58 | } 59 | 60 | .visible-desktop { 61 | display: inherit !important; 62 | } 63 | 64 | @media (min-width: 768px) and (max-width: 979px) { 65 | .hidden-desktop { 66 | display: inherit !important; 67 | } 68 | .visible-desktop { 69 | display: none !important ; 70 | } 71 | .visible-tablet { 72 | display: inherit !important; 73 | } 74 | .hidden-tablet { 75 | display: none !important; 76 | } 77 | } 78 | 79 | @media (max-width: 767px) { 80 | .hidden-desktop { 81 | display: inherit !important; 82 | } 83 | .visible-desktop { 84 | display: none !important; 85 | } 86 | .visible-phone { 87 | display: inherit !important; 88 | } 89 | .hidden-phone { 90 | display: none !important; 91 | } 92 | } 93 | 94 | @media (min-width: 1200px) { 95 | .row { 96 | margin-left: -30px; 97 | *zoom: 1; 98 | } 99 | .row:before, 100 | .row:after { 101 | display: table; 102 | line-height: 0; 103 | content: ""; 104 | } 105 | .row:after { 106 | clear: both; 107 | } 108 | [class*="span"] { 109 | float: left; 110 | min-height: 1px; 111 | margin-left: 30px; 112 | } 113 | .container, 114 | .navbar-static-top .container, 115 | .navbar-fixed-top .container, 116 | .navbar-fixed-bottom .container { 117 | width: 1170px; 118 | } 119 | .span12 { 120 | width: 1170px; 121 | } 122 | .span11 { 123 | width: 1070px; 124 | } 125 | .span10 { 126 | width: 970px; 127 | } 128 | .span9 { 129 | width: 870px; 130 | } 131 | .span8 { 132 | width: 770px; 133 | } 134 | .span7 { 135 | width: 670px; 136 | } 137 | .span6 { 138 | width: 570px; 139 | } 140 | .span5 { 141 | width: 470px; 142 | } 143 | .span4 { 144 | width: 370px; 145 | } 146 | .span3 { 147 | width: 270px; 148 | } 149 | .span2 { 150 | width: 170px; 151 | } 152 | .span1 { 153 | width: 70px; 154 | } 155 | .offset12 { 156 | margin-left: 1230px; 157 | } 158 | .offset11 { 159 | margin-left: 1130px; 160 | } 161 | .offset10 { 162 | margin-left: 1030px; 163 | } 164 | .offset9 { 165 | margin-left: 930px; 166 | } 167 | .offset8 { 168 | margin-left: 830px; 169 | } 170 | .offset7 { 171 | margin-left: 730px; 172 | } 173 | .offset6 { 174 | margin-left: 630px; 175 | } 176 | .offset5 { 177 | margin-left: 530px; 178 | } 179 | .offset4 { 180 | margin-left: 430px; 181 | } 182 | .offset3 { 183 | margin-left: 330px; 184 | } 185 | .offset2 { 186 | margin-left: 230px; 187 | } 188 | .offset1 { 189 | margin-left: 130px; 190 | } 191 | .row-fluid { 192 | width: 100%; 193 | *zoom: 1; 194 | } 195 | .row-fluid:before, 196 | .row-fluid:after { 197 | display: table; 198 | line-height: 0; 199 | content: ""; 200 | } 201 | .row-fluid:after { 202 | clear: both; 203 | } 204 | .row-fluid [class*="span"] { 205 | display: block; 206 | float: left; 207 | width: 100%; 208 | min-height: 30px; 209 | margin-left: 2.564102564102564%; 210 | *margin-left: 2.5109110747408616%; 211 | -webkit-box-sizing: border-box; 212 | -moz-box-sizing: border-box; 213 | box-sizing: border-box; 214 | } 215 | .row-fluid [class*="span"]:first-child { 216 | margin-left: 0; 217 | } 218 | .row-fluid .span12 { 219 | width: 100%; 220 | *width: 99.94680851063829%; 221 | } 222 | .row-fluid .span11 { 223 | width: 91.45299145299145%; 224 | *width: 91.39979996362975%; 225 | } 226 | .row-fluid .span10 { 227 | width: 82.90598290598291%; 228 | *width: 82.8527914166212%; 229 | } 230 | .row-fluid .span9 { 231 | width: 74.35897435897436%; 232 | *width: 74.30578286961266%; 233 | } 234 | .row-fluid .span8 { 235 | width: 65.81196581196582%; 236 | *width: 65.75877432260411%; 237 | } 238 | .row-fluid .span7 { 239 | width: 57.26495726495726%; 240 | *width: 57.21176577559556%; 241 | } 242 | .row-fluid .span6 { 243 | width: 48.717948717948715%; 244 | *width: 48.664757228587014%; 245 | } 246 | .row-fluid .span5 { 247 | width: 40.17094017094017%; 248 | *width: 40.11774868157847%; 249 | } 250 | .row-fluid .span4 { 251 | width: 31.623931623931625%; 252 | *width: 31.570740134569924%; 253 | } 254 | .row-fluid .span3 { 255 | width: 23.076923076923077%; 256 | *width: 23.023731587561375%; 257 | } 258 | .row-fluid .span2 { 259 | width: 14.52991452991453%; 260 | *width: 14.476723040552828%; 261 | } 262 | .row-fluid .span1 { 263 | width: 5.982905982905983%; 264 | *width: 5.929714493544281%; 265 | } 266 | .row-fluid .offset12 { 267 | margin-left: 105.12820512820512%; 268 | *margin-left: 105.02182214948171%; 269 | } 270 | .row-fluid .offset12:first-child { 271 | margin-left: 102.56410256410257%; 272 | *margin-left: 102.45771958537915%; 273 | } 274 | .row-fluid .offset11 { 275 | margin-left: 96.58119658119658%; 276 | *margin-left: 96.47481360247316%; 277 | } 278 | .row-fluid .offset11:first-child { 279 | margin-left: 94.01709401709402%; 280 | *margin-left: 93.91071103837061%; 281 | } 282 | .row-fluid .offset10 { 283 | margin-left: 88.03418803418803%; 284 | *margin-left: 87.92780505546462%; 285 | } 286 | .row-fluid .offset10:first-child { 287 | margin-left: 85.47008547008548%; 288 | *margin-left: 85.36370249136206%; 289 | } 290 | .row-fluid .offset9 { 291 | margin-left: 79.48717948717949%; 292 | *margin-left: 79.38079650845607%; 293 | } 294 | .row-fluid .offset9:first-child { 295 | margin-left: 76.92307692307693%; 296 | *margin-left: 76.81669394435352%; 297 | } 298 | .row-fluid .offset8 { 299 | margin-left: 70.94017094017094%; 300 | *margin-left: 70.83378796144753%; 301 | } 302 | .row-fluid .offset8:first-child { 303 | margin-left: 68.37606837606839%; 304 | *margin-left: 68.26968539734497%; 305 | } 306 | .row-fluid .offset7 { 307 | margin-left: 62.393162393162385%; 308 | *margin-left: 62.28677941443899%; 309 | } 310 | .row-fluid .offset7:first-child { 311 | margin-left: 59.82905982905982%; 312 | *margin-left: 59.72267685033642%; 313 | } 314 | .row-fluid .offset6 { 315 | margin-left: 53.84615384615384%; 316 | *margin-left: 53.739770867430444%; 317 | } 318 | .row-fluid .offset6:first-child { 319 | margin-left: 51.28205128205128%; 320 | *margin-left: 51.175668303327875%; 321 | } 322 | .row-fluid .offset5 { 323 | margin-left: 45.299145299145295%; 324 | *margin-left: 45.1927623204219%; 325 | } 326 | .row-fluid .offset5:first-child { 327 | margin-left: 42.73504273504273%; 328 | *margin-left: 42.62865975631933%; 329 | } 330 | .row-fluid .offset4 { 331 | margin-left: 36.75213675213675%; 332 | *margin-left: 36.645753773413354%; 333 | } 334 | .row-fluid .offset4:first-child { 335 | margin-left: 34.18803418803419%; 336 | *margin-left: 34.081651209310785%; 337 | } 338 | .row-fluid .offset3 { 339 | margin-left: 28.205128205128204%; 340 | *margin-left: 28.0987452264048%; 341 | } 342 | .row-fluid .offset3:first-child { 343 | margin-left: 25.641025641025642%; 344 | *margin-left: 25.53464266230224%; 345 | } 346 | .row-fluid .offset2 { 347 | margin-left: 19.65811965811966%; 348 | *margin-left: 19.551736679396257%; 349 | } 350 | .row-fluid .offset2:first-child { 351 | margin-left: 17.094017094017094%; 352 | *margin-left: 16.98763411529369%; 353 | } 354 | .row-fluid .offset1 { 355 | margin-left: 11.11111111111111%; 356 | *margin-left: 11.004728132387708%; 357 | } 358 | .row-fluid .offset1:first-child { 359 | margin-left: 8.547008547008547%; 360 | *margin-left: 8.440625568285142%; 361 | } 362 | input, 363 | textarea, 364 | .uneditable-input { 365 | margin-left: 0; 366 | } 367 | .controls-row [class*="span"] + [class*="span"] { 368 | margin-left: 30px; 369 | } 370 | input.span12, 371 | textarea.span12, 372 | .uneditable-input.span12 { 373 | width: 1156px; 374 | } 375 | input.span11, 376 | textarea.span11, 377 | .uneditable-input.span11 { 378 | width: 1056px; 379 | } 380 | input.span10, 381 | textarea.span10, 382 | .uneditable-input.span10 { 383 | width: 956px; 384 | } 385 | input.span9, 386 | textarea.span9, 387 | .uneditable-input.span9 { 388 | width: 856px; 389 | } 390 | input.span8, 391 | textarea.span8, 392 | .uneditable-input.span8 { 393 | width: 756px; 394 | } 395 | input.span7, 396 | textarea.span7, 397 | .uneditable-input.span7 { 398 | width: 656px; 399 | } 400 | input.span6, 401 | textarea.span6, 402 | .uneditable-input.span6 { 403 | width: 556px; 404 | } 405 | input.span5, 406 | textarea.span5, 407 | .uneditable-input.span5 { 408 | width: 456px; 409 | } 410 | input.span4, 411 | textarea.span4, 412 | .uneditable-input.span4 { 413 | width: 356px; 414 | } 415 | input.span3, 416 | textarea.span3, 417 | .uneditable-input.span3 { 418 | width: 256px; 419 | } 420 | input.span2, 421 | textarea.span2, 422 | .uneditable-input.span2 { 423 | width: 156px; 424 | } 425 | input.span1, 426 | textarea.span1, 427 | .uneditable-input.span1 { 428 | width: 56px; 429 | } 430 | .thumbnails { 431 | margin-left: -30px; 432 | } 433 | .thumbnails > li { 434 | margin-left: 30px; 435 | } 436 | .row-fluid .thumbnails { 437 | margin-left: 0; 438 | } 439 | } 440 | 441 | @media (min-width: 768px) and (max-width: 979px) { 442 | .row { 443 | margin-left: -20px; 444 | *zoom: 1; 445 | } 446 | .row:before, 447 | .row:after { 448 | display: table; 449 | line-height: 0; 450 | content: ""; 451 | } 452 | .row:after { 453 | clear: both; 454 | } 455 | [class*="span"] { 456 | float: left; 457 | min-height: 1px; 458 | margin-left: 20px; 459 | } 460 | .container, 461 | .navbar-static-top .container, 462 | .navbar-fixed-top .container, 463 | .navbar-fixed-bottom .container { 464 | width: 724px; 465 | } 466 | .span12 { 467 | width: 724px; 468 | } 469 | .span11 { 470 | width: 662px; 471 | } 472 | .span10 { 473 | width: 600px; 474 | } 475 | .span9 { 476 | width: 538px; 477 | } 478 | .span8 { 479 | width: 476px; 480 | } 481 | .span7 { 482 | width: 414px; 483 | } 484 | .span6 { 485 | width: 352px; 486 | } 487 | .span5 { 488 | width: 290px; 489 | } 490 | .span4 { 491 | width: 228px; 492 | } 493 | .span3 { 494 | width: 166px; 495 | } 496 | .span2 { 497 | width: 104px; 498 | } 499 | .span1 { 500 | width: 42px; 501 | } 502 | .offset12 { 503 | margin-left: 764px; 504 | } 505 | .offset11 { 506 | margin-left: 702px; 507 | } 508 | .offset10 { 509 | margin-left: 640px; 510 | } 511 | .offset9 { 512 | margin-left: 578px; 513 | } 514 | .offset8 { 515 | margin-left: 516px; 516 | } 517 | .offset7 { 518 | margin-left: 454px; 519 | } 520 | .offset6 { 521 | margin-left: 392px; 522 | } 523 | .offset5 { 524 | margin-left: 330px; 525 | } 526 | .offset4 { 527 | margin-left: 268px; 528 | } 529 | .offset3 { 530 | margin-left: 206px; 531 | } 532 | .offset2 { 533 | margin-left: 144px; 534 | } 535 | .offset1 { 536 | margin-left: 82px; 537 | } 538 | .row-fluid { 539 | width: 100%; 540 | *zoom: 1; 541 | } 542 | .row-fluid:before, 543 | .row-fluid:after { 544 | display: table; 545 | line-height: 0; 546 | content: ""; 547 | } 548 | .row-fluid:after { 549 | clear: both; 550 | } 551 | .row-fluid [class*="span"] { 552 | display: block; 553 | float: left; 554 | width: 100%; 555 | min-height: 30px; 556 | margin-left: 2.7624309392265194%; 557 | *margin-left: 2.709239449864817%; 558 | -webkit-box-sizing: border-box; 559 | -moz-box-sizing: border-box; 560 | box-sizing: border-box; 561 | } 562 | .row-fluid [class*="span"]:first-child { 563 | margin-left: 0; 564 | } 565 | .row-fluid .span12 { 566 | width: 100%; 567 | *width: 99.94680851063829%; 568 | } 569 | .row-fluid .span11 { 570 | width: 91.43646408839778%; 571 | *width: 91.38327259903608%; 572 | } 573 | .row-fluid .span10 { 574 | width: 82.87292817679558%; 575 | *width: 82.81973668743387%; 576 | } 577 | .row-fluid .span9 { 578 | width: 74.30939226519337%; 579 | *width: 74.25620077583166%; 580 | } 581 | .row-fluid .span8 { 582 | width: 65.74585635359117%; 583 | *width: 65.69266486422946%; 584 | } 585 | .row-fluid .span7 { 586 | width: 57.18232044198895%; 587 | *width: 57.12912895262725%; 588 | } 589 | .row-fluid .span6 { 590 | width: 48.61878453038674%; 591 | *width: 48.56559304102504%; 592 | } 593 | .row-fluid .span5 { 594 | width: 40.05524861878453%; 595 | *width: 40.00205712942283%; 596 | } 597 | .row-fluid .span4 { 598 | width: 31.491712707182323%; 599 | *width: 31.43852121782062%; 600 | } 601 | .row-fluid .span3 { 602 | width: 22.92817679558011%; 603 | *width: 22.87498530621841%; 604 | } 605 | .row-fluid .span2 { 606 | width: 14.3646408839779%; 607 | *width: 14.311449394616199%; 608 | } 609 | .row-fluid .span1 { 610 | width: 5.801104972375691%; 611 | *width: 5.747913483013988%; 612 | } 613 | .row-fluid .offset12 { 614 | margin-left: 105.52486187845304%; 615 | *margin-left: 105.41847889972962%; 616 | } 617 | .row-fluid .offset12:first-child { 618 | margin-left: 102.76243093922652%; 619 | *margin-left: 102.6560479605031%; 620 | } 621 | .row-fluid .offset11 { 622 | margin-left: 96.96132596685082%; 623 | *margin-left: 96.8549429881274%; 624 | } 625 | .row-fluid .offset11:first-child { 626 | margin-left: 94.1988950276243%; 627 | *margin-left: 94.09251204890089%; 628 | } 629 | .row-fluid .offset10 { 630 | margin-left: 88.39779005524862%; 631 | *margin-left: 88.2914070765252%; 632 | } 633 | .row-fluid .offset10:first-child { 634 | margin-left: 85.6353591160221%; 635 | *margin-left: 85.52897613729868%; 636 | } 637 | .row-fluid .offset9 { 638 | margin-left: 79.8342541436464%; 639 | *margin-left: 79.72787116492299%; 640 | } 641 | .row-fluid .offset9:first-child { 642 | margin-left: 77.07182320441989%; 643 | *margin-left: 76.96544022569647%; 644 | } 645 | .row-fluid .offset8 { 646 | margin-left: 71.2707182320442%; 647 | *margin-left: 71.16433525332079%; 648 | } 649 | .row-fluid .offset8:first-child { 650 | margin-left: 68.50828729281768%; 651 | *margin-left: 68.40190431409427%; 652 | } 653 | .row-fluid .offset7 { 654 | margin-left: 62.70718232044199%; 655 | *margin-left: 62.600799341718584%; 656 | } 657 | .row-fluid .offset7:first-child { 658 | margin-left: 59.94475138121547%; 659 | *margin-left: 59.838368402492065%; 660 | } 661 | .row-fluid .offset6 { 662 | margin-left: 54.14364640883978%; 663 | *margin-left: 54.037263430116376%; 664 | } 665 | .row-fluid .offset6:first-child { 666 | margin-left: 51.38121546961326%; 667 | *margin-left: 51.27483249088986%; 668 | } 669 | .row-fluid .offset5 { 670 | margin-left: 45.58011049723757%; 671 | *margin-left: 45.47372751851417%; 672 | } 673 | .row-fluid .offset5:first-child { 674 | margin-left: 42.81767955801105%; 675 | *margin-left: 42.71129657928765%; 676 | } 677 | .row-fluid .offset4 { 678 | margin-left: 37.01657458563536%; 679 | *margin-left: 36.91019160691196%; 680 | } 681 | .row-fluid .offset4:first-child { 682 | margin-left: 34.25414364640884%; 683 | *margin-left: 34.14776066768544%; 684 | } 685 | .row-fluid .offset3 { 686 | margin-left: 28.45303867403315%; 687 | *margin-left: 28.346655695309746%; 688 | } 689 | .row-fluid .offset3:first-child { 690 | margin-left: 25.69060773480663%; 691 | *margin-left: 25.584224756083227%; 692 | } 693 | .row-fluid .offset2 { 694 | margin-left: 19.88950276243094%; 695 | *margin-left: 19.783119783707537%; 696 | } 697 | .row-fluid .offset2:first-child { 698 | margin-left: 17.12707182320442%; 699 | *margin-left: 17.02068884448102%; 700 | } 701 | .row-fluid .offset1 { 702 | margin-left: 11.32596685082873%; 703 | *margin-left: 11.219583872105325%; 704 | } 705 | .row-fluid .offset1:first-child { 706 | margin-left: 8.56353591160221%; 707 | *margin-left: 8.457152932878806%; 708 | } 709 | input, 710 | textarea, 711 | .uneditable-input { 712 | margin-left: 0; 713 | } 714 | .controls-row [class*="span"] + [class*="span"] { 715 | margin-left: 20px; 716 | } 717 | input.span12, 718 | textarea.span12, 719 | .uneditable-input.span12 { 720 | width: 710px; 721 | } 722 | input.span11, 723 | textarea.span11, 724 | .uneditable-input.span11 { 725 | width: 648px; 726 | } 727 | input.span10, 728 | textarea.span10, 729 | .uneditable-input.span10 { 730 | width: 586px; 731 | } 732 | input.span9, 733 | textarea.span9, 734 | .uneditable-input.span9 { 735 | width: 524px; 736 | } 737 | input.span8, 738 | textarea.span8, 739 | .uneditable-input.span8 { 740 | width: 462px; 741 | } 742 | input.span7, 743 | textarea.span7, 744 | .uneditable-input.span7 { 745 | width: 400px; 746 | } 747 | input.span6, 748 | textarea.span6, 749 | .uneditable-input.span6 { 750 | width: 338px; 751 | } 752 | input.span5, 753 | textarea.span5, 754 | .uneditable-input.span5 { 755 | width: 276px; 756 | } 757 | input.span4, 758 | textarea.span4, 759 | .uneditable-input.span4 { 760 | width: 214px; 761 | } 762 | input.span3, 763 | textarea.span3, 764 | .uneditable-input.span3 { 765 | width: 152px; 766 | } 767 | input.span2, 768 | textarea.span2, 769 | .uneditable-input.span2 { 770 | width: 90px; 771 | } 772 | input.span1, 773 | textarea.span1, 774 | .uneditable-input.span1 { 775 | width: 28px; 776 | } 777 | } 778 | 779 | @media (max-width: 767px) { 780 | body { 781 | padding-right: 20px; 782 | padding-left: 20px; 783 | } 784 | .navbar-fixed-top, 785 | .navbar-fixed-bottom, 786 | .navbar-static-top { 787 | margin-right: -20px; 788 | margin-left: -20px; 789 | } 790 | .container-fluid { 791 | padding: 0; 792 | } 793 | .dl-horizontal dt { 794 | float: none; 795 | width: auto; 796 | clear: none; 797 | text-align: left; 798 | } 799 | .dl-horizontal dd { 800 | margin-left: 0; 801 | } 802 | .container { 803 | width: auto; 804 | } 805 | .row-fluid { 806 | width: 100%; 807 | } 808 | .row, 809 | .thumbnails { 810 | margin-left: 0; 811 | } 812 | .thumbnails > li { 813 | float: none; 814 | margin-left: 0; 815 | } 816 | [class*="span"], 817 | .row-fluid [class*="span"] { 818 | display: block; 819 | float: none; 820 | width: 100%; 821 | margin-left: 0; 822 | -webkit-box-sizing: border-box; 823 | -moz-box-sizing: border-box; 824 | box-sizing: border-box; 825 | } 826 | .span12, 827 | .row-fluid .span12 { 828 | width: 100%; 829 | -webkit-box-sizing: border-box; 830 | -moz-box-sizing: border-box; 831 | box-sizing: border-box; 832 | } 833 | .input-large, 834 | .input-xlarge, 835 | .input-xxlarge, 836 | input[class*="span"], 837 | select[class*="span"], 838 | textarea[class*="span"], 839 | .uneditable-input { 840 | display: block; 841 | width: 100%; 842 | min-height: 30px; 843 | -webkit-box-sizing: border-box; 844 | -moz-box-sizing: border-box; 845 | box-sizing: border-box; 846 | } 847 | .input-prepend input, 848 | .input-append input, 849 | .input-prepend input[class*="span"], 850 | .input-append input[class*="span"] { 851 | display: inline-block; 852 | width: auto; 853 | } 854 | .controls-row [class*="span"] + [class*="span"] { 855 | margin-left: 0; 856 | } 857 | .modal { 858 | position: fixed; 859 | top: 20px; 860 | right: 20px; 861 | left: 20px; 862 | width: auto; 863 | margin: 0; 864 | } 865 | .modal.fade.in { 866 | top: auto; 867 | } 868 | } 869 | 870 | @media (max-width: 480px) { 871 | .nav-collapse { 872 | -webkit-transform: translate3d(0, 0, 0); 873 | } 874 | .page-header h1 small { 875 | display: block; 876 | line-height: 20px; 877 | } 878 | input[type="checkbox"], 879 | input[type="radio"] { 880 | border: 1px solid #ccc; 881 | } 882 | .form-horizontal .control-label { 883 | float: none; 884 | width: auto; 885 | padding-top: 0; 886 | text-align: left; 887 | } 888 | .form-horizontal .controls { 889 | margin-left: 0; 890 | } 891 | .form-horizontal .control-list { 892 | padding-top: 0; 893 | } 894 | .form-horizontal .form-actions { 895 | padding-right: 10px; 896 | padding-left: 10px; 897 | } 898 | .modal { 899 | top: 10px; 900 | right: 10px; 901 | left: 10px; 902 | } 903 | .modal-header .close { 904 | padding: 10px; 905 | margin: -10px; 906 | } 907 | .carousel-caption { 908 | position: static; 909 | } 910 | } 911 | 912 | @media (max-width: 979px) { 913 | body { 914 | padding-top: 0; 915 | } 916 | .navbar-fixed-top, 917 | .navbar-fixed-bottom { 918 | position: static; 919 | } 920 | .navbar-fixed-top { 921 | margin-bottom: 20px; 922 | } 923 | .navbar-fixed-bottom { 924 | margin-top: 20px; 925 | } 926 | .navbar-fixed-top .navbar-inner, 927 | .navbar-fixed-bottom .navbar-inner { 928 | padding: 5px; 929 | } 930 | .navbar .container { 931 | width: auto; 932 | padding: 0; 933 | } 934 | .navbar .brand { 935 | padding-right: 10px; 936 | padding-left: 10px; 937 | margin: 0 0 0 -5px; 938 | } 939 | .nav-collapse { 940 | clear: both; 941 | } 942 | .nav-collapse .nav { 943 | float: none; 944 | margin: 0 0 10px; 945 | } 946 | .nav-collapse .nav > li { 947 | float: none; 948 | } 949 | .nav-collapse .nav > li > a { 950 | margin-bottom: 2px; 951 | } 952 | .nav-collapse .nav > .divider-vertical { 953 | display: none; 954 | } 955 | .nav-collapse .nav .nav-header { 956 | color: #777777; 957 | text-shadow: none; 958 | } 959 | .nav-collapse .nav > li > a, 960 | .nav-collapse .dropdown-menu a { 961 | padding: 9px 15px; 962 | font-weight: bold; 963 | color: #777777; 964 | -webkit-border-radius: 3px; 965 | -moz-border-radius: 3px; 966 | border-radius: 3px; 967 | } 968 | .nav-collapse .btn { 969 | padding: 4px 10px 4px; 970 | font-weight: normal; 971 | -webkit-border-radius: 4px; 972 | -moz-border-radius: 4px; 973 | border-radius: 4px; 974 | } 975 | .nav-collapse .dropdown-menu li + li a { 976 | margin-bottom: 2px; 977 | } 978 | .nav-collapse .nav > li > a:hover, 979 | .nav-collapse .dropdown-menu a:hover { 980 | background-color: #f2f2f2; 981 | } 982 | .navbar-inverse .nav-collapse .nav > li > a:hover, 983 | .navbar-inverse .nav-collapse .dropdown-menu a:hover { 984 | background-color: #111111; 985 | } 986 | .nav-collapse.in .btn-group { 987 | padding: 0; 988 | margin-top: 5px; 989 | } 990 | .nav-collapse .dropdown-menu { 991 | position: static; 992 | top: auto; 993 | left: auto; 994 | display: block; 995 | float: none; 996 | max-width: none; 997 | padding: 0; 998 | margin: 0 15px; 999 | background-color: transparent; 1000 | border: none; 1001 | -webkit-border-radius: 0; 1002 | -moz-border-radius: 0; 1003 | border-radius: 0; 1004 | -webkit-box-shadow: none; 1005 | -moz-box-shadow: none; 1006 | box-shadow: none; 1007 | } 1008 | .nav-collapse .dropdown-menu:before, 1009 | .nav-collapse .dropdown-menu:after { 1010 | display: none; 1011 | } 1012 | .nav-collapse .dropdown-menu .divider { 1013 | display: none; 1014 | } 1015 | .nav-collapse .nav > li > .dropdown-menu:before, 1016 | .nav-collapse .nav > li > .dropdown-menu:after { 1017 | display: none; 1018 | } 1019 | .nav-collapse .navbar-form, 1020 | .nav-collapse .navbar-search { 1021 | float: none; 1022 | padding: 10px 15px; 1023 | margin: 10px 0; 1024 | border-top: 1px solid #f2f2f2; 1025 | border-bottom: 1px solid #f2f2f2; 1026 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1027 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1028 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1029 | } 1030 | .navbar-inverse .nav-collapse .navbar-form, 1031 | .navbar-inverse .nav-collapse .navbar-search { 1032 | border-top-color: #111111; 1033 | border-bottom-color: #111111; 1034 | } 1035 | .navbar .nav-collapse .nav.pull-right { 1036 | float: none; 1037 | margin-left: 0; 1038 | } 1039 | .nav-collapse, 1040 | .nav-collapse.collapse { 1041 | height: 0; 1042 | overflow: hidden; 1043 | } 1044 | .navbar .btn-navbar { 1045 | display: block; 1046 | } 1047 | .navbar-static .navbar-inner { 1048 | padding-right: 10px; 1049 | padding-left: 10px; 1050 | } 1051 | } 1052 | 1053 | @media (min-width: 980px) { 1054 | .nav-collapse.collapse { 1055 | height: auto !important; 1056 | overflow: visible !important; 1057 | } 1058 | } 1059 | -------------------------------------------------------------------------------- /examples/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.1.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade.in{top:auto}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:block;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /examples/css/images/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexbeletsky/backbone-computedfields/a776c9b80a2daf186290407afe36ae3db334bebb/examples/css/images/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /examples/css/images/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexbeletsky/backbone-computedfields/a776c9b80a2daf186290407afe36ae3db334bebb/examples/css/images/glyphicons-halflings.png -------------------------------------------------------------------------------- /examples/css/spacing.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Spacing classes 3 | * Should be used to modify the default spacing between objects (not between nodes of the same object) 4 | * Please use judiciously. You want to be using defaults most of the time, these are exceptions! 5 | * 6 | */ 7 | /* ====== Default spacing ====== */ 8 | h1, h2, h3, h4, h5, h6, ul, ol,dl, p,blockquote, .media {margin:10px 0;} 9 | h1, h2, h3, h4, h5, h6,img{padding-bottom:0px;} 10 | pre{margin: 10px 0;} 11 | table h1,table h2,table h3, table h4, table h5, table h6, table p, table ul, table ol, table dl{padding:0;} 12 | 13 | /* spacing helpers 14 | p,m = padding,margin 15 | a,t,r,b,l,h,v = all,top,right,bottom,left,horizontal,vertical 16 | s,m,l,n = small(5px),medium(10px),large(20px),none(0px) 17 | */ 18 | 19 | .ptn,.pvn,.pan{padding-top:0px !important} 20 | .pts,.pvs,.pas{padding-top:5px !important} 21 | .ptm,.pvm,.pam{padding-top:10px !important} 22 | .ptl,.pvl,.pal{padding-top:20px !important} 23 | .prn,.phn,.pan{padding-right:0px !important} 24 | .prs,.phs,.pas{padding-right:5px !important} 25 | .prm,.phm,.pam{padding-right:10px !important} 26 | .prl,.phl,.pal{padding-right:20px !important} 27 | .pbn,.pvn,.pan{padding-bottom:0px !important} 28 | .pbs,.pvs,.pas{padding-bottom:5px !important} 29 | .pbm,.pvm,.pam{padding-bottom:10px !important} 30 | .pbl,.pvl,.pal{padding-bottom:20px !important} 31 | .pln,.phn,.pan{padding-left:0px !important} 32 | .pls,.phs,.pas{padding-left:5px !important} 33 | .plm,.phm,.pam{padding-left:10px !important} 34 | .pll,.phl,.pal{padding-left:20px !important} 35 | .mtn,.mvn,.man{margin-top:0px !important} 36 | .mts,.mvs,.mas{margin-top:5px !important} 37 | .mtm,.mvm,.mam{margin-top:10px !important} 38 | .mtl,.mvl,.mal{margin-top:20px !important} 39 | .mrn,.mhn,.man{margin-right:0px !important} 40 | .mrs,.mhs,.mas{margin-right:5px !important} 41 | .mrm,.mhm,.mam{margin-right:10px !important} 42 | .mrl,.mhl,.mal{margin-right:20px !important} 43 | .mbn,.mvn,.man{margin-bottom:0px !important} 44 | .mbs,.mvs,.mas{margin-bottom:5px !important} 45 | .mbm,.mvm,.mam{margin-bottom:10px !important} 46 | .mbl,.mvl,.mal{margin-bottom:20px !important} 47 | .mln,.mhn,.man{margin-left:0px !important} 48 | .mls,.mhs,.mas{margin-left:5px !important} 49 | .mlm,.mhm,.mam{margin-left:10px !important} 50 | .mll,.mhl,.mal{margin-left:20px !important} -------------------------------------------------------------------------------- /examples/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 20px; 3 | } 4 | 5 | #app { 6 | padding: 10px 0 10px 0; 7 | min-height: 320px; 8 | } -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Backbone.ComputedFields - Simple 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 141 | 142 | 163 | 164 | 165 | 166 |
167 |
168 |
169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /lib/amd/backbone.computedfields.js: -------------------------------------------------------------------------------- 1 | // Backbone.ComputedFields, v0.0.12 2 | // Copyright (c)2015 alexander.beletsky@gmail.com 3 | // Distributed under MIT license 4 | // https://github.com/alexanderbeletsky/backbone-computedfields 5 | 6 | (function (root, factory) { 7 | if (typeof exports === 'object') { 8 | 9 | var underscore = require('underscore'); 10 | var backbone = require('backbone'); 11 | 12 | module.exports = factory(underscore, backbone); 13 | 14 | } else if (typeof define === 'function' && define.amd) { 15 | 16 | define(['underscore', 'backbone'], factory); 17 | 18 | } 19 | }(this, function (_, Backbone) { 20 | 21 | Backbone.ComputedFields = (function(Backbone, _){ 22 | 23 | var _isFunction = function(obj) { 24 | // == instead of === and || false are optimizations 25 | // to go around nasty bugs in IE11, Safari 8 and old v8 26 | // see underscore#isFunction 27 | /* jshint eqeqeq: false */ 28 | return typeof obj == 'function' || false; 29 | /* jshint eqeqeq: true */ 30 | }; 31 | 32 | var ComputedFields = function (model) { 33 | this.model = model; 34 | this._computedFields = []; 35 | 36 | this.initialize(); 37 | }; 38 | 39 | _.extend(ComputedFields.prototype, { 40 | initialize: function () { 41 | _.bindAll( 42 | this, 43 | '_bindModelEvents', 44 | '_computeFieldValue', 45 | '_dependentFields', 46 | '_isModelInitialized', 47 | '_lookUpComputedFields', 48 | '_thenComputedChanges', 49 | '_thenDependentChanges', 50 | '_toJSON', 51 | '_wrapJSON', 52 | 'initialize' 53 | ); 54 | 55 | this._lookUpComputedFields(); 56 | this._bindModelEvents(); 57 | this._wrapJSON(); 58 | }, 59 | 60 | _lookUpComputedFields: function () { 61 | var computed = _isFunction(this.model.computed) ? this.model.computed() : this.model.computed; 62 | 63 | for (var obj in computed) { 64 | var field = computed[obj]; 65 | 66 | if (field && (field.set || field.get)) { 67 | this._computedFields.push({name: obj, field: field}); 68 | } 69 | } 70 | }, 71 | 72 | _bindModelEvents: function () { 73 | _.each(this._computedFields, function (computedField) { 74 | var fieldName = computedField.name; 75 | var field = computedField.field; 76 | 77 | var updateComputed = _.bind(function () { 78 | var value = this._computeFieldValue(field); 79 | this.model.set(fieldName, value, { skipChangeEvent: true }); 80 | }, this); 81 | 82 | var updateDependent = _.bind(function (model, value, options) { 83 | if (options && options.skipChangeEvent) { 84 | return; 85 | } 86 | 87 | if (field.set) { 88 | var fields = this._dependentFields(field.depends); 89 | value = value || this.model.get(fieldName); 90 | 91 | field.set.call(this.model, value, fields); 92 | this.model.set(fields, options); 93 | } 94 | }, this); 95 | 96 | this._thenDependentChanges(field.depends, updateComputed); 97 | this._thenComputedChanges(fieldName, updateDependent); 98 | 99 | if (this._isModelInitialized()) { 100 | updateComputed(); 101 | } 102 | }, this); 103 | }, 104 | 105 | _isModelInitialized: function () { 106 | return !_.isEmpty(this.model.attributes); 107 | }, 108 | 109 | _thenDependentChanges: function (depends, callback) { 110 | _.each(depends, function (name) { 111 | if (typeof (name) === 'string') { 112 | this.model.on('change:' + name, callback); 113 | } 114 | 115 | if (typeof (name) === 'function') { 116 | name.call(this.model, callback); 117 | } 118 | }, this); 119 | }, 120 | 121 | _thenComputedChanges: function (fieldName, callback) { 122 | this.model.on('change:' + fieldName, callback); 123 | }, 124 | 125 | _wrapJSON: function () { 126 | this.model.toJSON = _.wrap(this.model.toJSON, this._toJSON); 127 | }, 128 | 129 | _toJSON: function (toJSON) { 130 | var args = Array.prototype.slice.call(arguments, 1), 131 | attributes = toJSON.apply(this.model, args), 132 | strip = !!(args[0] || {}).computedFields; 133 | 134 | var stripped = strip ? {} : _.reduce(this._computedFields, function (memo, computed) { 135 | if (computed.field.toJSON === false) { 136 | memo.push(computed.name); 137 | } 138 | return memo; 139 | },[]); 140 | 141 | return _.omit(attributes, stripped); 142 | }, 143 | 144 | _computeFieldValue: function (computedField) { 145 | if (computedField && computedField.get) { 146 | var fields = this._dependentFields(computedField.depends); 147 | return computedField.get.call(this.model, fields); 148 | } 149 | }, 150 | 151 | _dependentFields: function (depends) { 152 | return _.reduce(depends, function (memo, field) { 153 | memo[field] = this.model.get(field); 154 | return memo; 155 | }, {}, this); 156 | } 157 | 158 | }); 159 | 160 | return ComputedFields; 161 | 162 | })(Backbone, _); 163 | return Backbone.ComputedFields; 164 | 165 | })); -------------------------------------------------------------------------------- /lib/amd/backbone.computedfields.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){if("object"==typeof exports){var c=require("underscore"),d=require("backbone");module.exports=b(c,d)}else"function"==typeof define&&define.amd&&define(["underscore","backbone"],b)}(this,function(a,b){return b.ComputedFields=function(a,b){var c=function(a){return"function"==typeof a||!1},d=function(a){this.model=a,this._computedFields=[],this.initialize()};return b.extend(d.prototype,{initialize:function(){b.bindAll(this,"_bindModelEvents","_computeFieldValue","_dependentFields","_isModelInitialized","_lookUpComputedFields","_thenComputedChanges","_thenDependentChanges","_toJSON","_wrapJSON","initialize"),this._lookUpComputedFields(),this._bindModelEvents(),this._wrapJSON()},_lookUpComputedFields:function(){var a=c(this.model.computed)?this.model.computed():this.model.computed;for(var b in a){var d=a[b];d&&(d.set||d.get)&&this._computedFields.push({name:b,field:d})}},_bindModelEvents:function(){b.each(this._computedFields,function(a){var c=a.name,d=a.field,e=b.bind(function(){var a=this._computeFieldValue(d);this.model.set(c,a,{skipChangeEvent:!0})},this),f=b.bind(function(a,b,e){if((!e||!e.skipChangeEvent)&&d.set){var f=this._dependentFields(d.depends);b=b||this.model.get(c),d.set.call(this.model,b,f),this.model.set(f,e)}},this);this._thenDependentChanges(d.depends,e),this._thenComputedChanges(c,f),this._isModelInitialized()&&e()},this)},_isModelInitialized:function(){return!b.isEmpty(this.model.attributes)},_thenDependentChanges:function(a,c){b.each(a,function(a){"string"==typeof a&&this.model.on("change:"+a,c),"function"==typeof a&&a.call(this.model,c)},this)},_thenComputedChanges:function(a,b){this.model.on("change:"+a,b)},_wrapJSON:function(){this.model.toJSON=b.wrap(this.model.toJSON,this._toJSON)},_toJSON:function(a){var c=Array.prototype.slice.call(arguments,1),d=a.apply(this.model,c),e=!!(c[0]||{}).computedFields,f=e?{}:b.reduce(this._computedFields,function(a,b){return b.field.toJSON===!1&&a.push(b.name),a},[]);return b.omit(d,f)},_computeFieldValue:function(a){if(a&&a.get){var b=this._dependentFields(a.depends);return a.get.call(this.model,b)}},_dependentFields:function(a){return b.reduce(a,function(a,b){return a[b]=this.model.get(b),a},{},this)}}),d}(b,a),b.ComputedFields}); -------------------------------------------------------------------------------- /lib/backbone.computedfields.js: -------------------------------------------------------------------------------- 1 | // Backbone.ComputedFields, v0.0.12 2 | // Copyright (c)2015 alexander.beletsky@gmail.com 3 | // Distributed under MIT license 4 | // https://github.com/alexanderbeletsky/backbone-computedfields 5 | 6 | Backbone.ComputedFields = (function(Backbone, _){ 7 | 8 | var _isFunction = function(obj) { 9 | // == instead of === and || false are optimizations 10 | // to go around nasty bugs in IE11, Safari 8 and old v8 11 | // see underscore#isFunction 12 | /* jshint eqeqeq: false */ 13 | return typeof obj == 'function' || false; 14 | /* jshint eqeqeq: true */ 15 | }; 16 | 17 | var ComputedFields = function (model) { 18 | this.model = model; 19 | this._computedFields = []; 20 | 21 | this.initialize(); 22 | }; 23 | 24 | _.extend(ComputedFields.prototype, { 25 | initialize: function () { 26 | _.bindAll( 27 | this, 28 | '_bindModelEvents', 29 | '_computeFieldValue', 30 | '_dependentFields', 31 | '_isModelInitialized', 32 | '_lookUpComputedFields', 33 | '_thenComputedChanges', 34 | '_thenDependentChanges', 35 | '_toJSON', 36 | '_wrapJSON', 37 | 'initialize' 38 | ); 39 | 40 | this._lookUpComputedFields(); 41 | this._bindModelEvents(); 42 | this._wrapJSON(); 43 | }, 44 | 45 | _lookUpComputedFields: function () { 46 | var computed = _isFunction(this.model.computed) ? this.model.computed() : this.model.computed; 47 | 48 | for (var obj in computed) { 49 | var field = computed[obj]; 50 | 51 | if (field && (field.set || field.get)) { 52 | this._computedFields.push({name: obj, field: field}); 53 | } 54 | } 55 | }, 56 | 57 | _bindModelEvents: function () { 58 | _.each(this._computedFields, function (computedField) { 59 | var fieldName = computedField.name; 60 | var field = computedField.field; 61 | 62 | var updateComputed = _.bind(function () { 63 | var value = this._computeFieldValue(field); 64 | this.model.set(fieldName, value, { skipChangeEvent: true }); 65 | }, this); 66 | 67 | var updateDependent = _.bind(function (model, value, options) { 68 | if (options && options.skipChangeEvent) { 69 | return; 70 | } 71 | 72 | if (field.set) { 73 | var fields = this._dependentFields(field.depends); 74 | value = value || this.model.get(fieldName); 75 | 76 | field.set.call(this.model, value, fields); 77 | this.model.set(fields, options); 78 | } 79 | }, this); 80 | 81 | this._thenDependentChanges(field.depends, updateComputed); 82 | this._thenComputedChanges(fieldName, updateDependent); 83 | 84 | if (this._isModelInitialized()) { 85 | updateComputed(); 86 | } 87 | }, this); 88 | }, 89 | 90 | _isModelInitialized: function () { 91 | return !_.isEmpty(this.model.attributes); 92 | }, 93 | 94 | _thenDependentChanges: function (depends, callback) { 95 | _.each(depends, function (name) { 96 | if (typeof (name) === 'string') { 97 | this.model.on('change:' + name, callback); 98 | } 99 | 100 | if (typeof (name) === 'function') { 101 | name.call(this.model, callback); 102 | } 103 | }, this); 104 | }, 105 | 106 | _thenComputedChanges: function (fieldName, callback) { 107 | this.model.on('change:' + fieldName, callback); 108 | }, 109 | 110 | _wrapJSON: function () { 111 | this.model.toJSON = _.wrap(this.model.toJSON, this._toJSON); 112 | }, 113 | 114 | _toJSON: function (toJSON) { 115 | var args = Array.prototype.slice.call(arguments, 1), 116 | attributes = toJSON.apply(this.model, args), 117 | strip = !!(args[0] || {}).computedFields; 118 | 119 | var stripped = strip ? {} : _.reduce(this._computedFields, function (memo, computed) { 120 | if (computed.field.toJSON === false) { 121 | memo.push(computed.name); 122 | } 123 | return memo; 124 | },[]); 125 | 126 | return _.omit(attributes, stripped); 127 | }, 128 | 129 | _computeFieldValue: function (computedField) { 130 | if (computedField && computedField.get) { 131 | var fields = this._dependentFields(computedField.depends); 132 | return computedField.get.call(this.model, fields); 133 | } 134 | }, 135 | 136 | _dependentFields: function (depends) { 137 | return _.reduce(depends, function (memo, field) { 138 | memo[field] = this.model.get(field); 139 | return memo; 140 | }, {}, this); 141 | } 142 | 143 | }); 144 | 145 | return ComputedFields; 146 | 147 | })(Backbone, _); -------------------------------------------------------------------------------- /lib/backbone.computedfields.min.js: -------------------------------------------------------------------------------- 1 | Backbone.ComputedFields=function(a,b){var c=function(a){return"function"==typeof a||!1},d=function(a){this.model=a,this._computedFields=[],this.initialize()};return b.extend(d.prototype,{initialize:function(){b.bindAll(this,"_bindModelEvents","_computeFieldValue","_dependentFields","_isModelInitialized","_lookUpComputedFields","_thenComputedChanges","_thenDependentChanges","_toJSON","_wrapJSON","initialize"),this._lookUpComputedFields(),this._bindModelEvents(),this._wrapJSON()},_lookUpComputedFields:function(){var a=c(this.model.computed)?this.model.computed():this.model.computed;for(var b in a){var d=a[b];d&&(d.set||d.get)&&this._computedFields.push({name:b,field:d})}},_bindModelEvents:function(){b.each(this._computedFields,function(a){var c=a.name,d=a.field,e=b.bind(function(){var a=this._computeFieldValue(d);this.model.set(c,a,{skipChangeEvent:!0})},this),f=b.bind(function(a,b,e){if((!e||!e.skipChangeEvent)&&d.set){var f=this._dependentFields(d.depends);b=b||this.model.get(c),d.set.call(this.model,b,f),this.model.set(f,e)}},this);this._thenDependentChanges(d.depends,e),this._thenComputedChanges(c,f),this._isModelInitialized()&&e()},this)},_isModelInitialized:function(){return!b.isEmpty(this.model.attributes)},_thenDependentChanges:function(a,c){b.each(a,function(a){"string"==typeof a&&this.model.on("change:"+a,c),"function"==typeof a&&a.call(this.model,c)},this)},_thenComputedChanges:function(a,b){this.model.on("change:"+a,b)},_wrapJSON:function(){this.model.toJSON=b.wrap(this.model.toJSON,this._toJSON)},_toJSON:function(a){var c=Array.prototype.slice.call(arguments,1),d=a.apply(this.model,c),e=!!(c[0]||{}).computedFields,f=e?{}:b.reduce(this._computedFields,function(a,b){return b.field.toJSON===!1&&a.push(b.name),a},[]);return b.omit(d,f)},_computeFieldValue:function(a){if(a&&a.get){var b=this._dependentFields(a.depends);return a.get.call(this.model,b)}},_dependentFields:function(a){return b.reduce(a,function(a,b){return a[b]=this.model.get(b),a},{},this)}}),d}(Backbone,_); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone-computedfields", 3 | "description": "Computed fields for Backbone models", 4 | "keywords": [ 5 | "Backbone", 6 | "model", 7 | "computed", 8 | "field" 9 | ], 10 | "repository": "git://github.com/alexanderbeletsky/backbone.computedfields.git", 11 | "homepage": "https://github.com/alexanderbeletsky/backbone-computedfields", 12 | "author": "Alexander Beletsky ", 13 | "contributors": "Listed at ", 14 | "dependencies": { 15 | "underscore": "^1.5.2", 16 | "backbone": "^1.1.0" 17 | }, 18 | "scripts": { 19 | "test": "grunt mocha" 20 | }, 21 | "lib": "./lib", 22 | "main": "./lib/amd/backbone.computedfields.js", 23 | "version": "0.0.12", 24 | "devDependencies": { 25 | "grunt": "^0.4.5", 26 | "grunt-contrib-concat": "^0.5.1", 27 | "grunt-contrib-jshint": "^0.11.2", 28 | "grunt-contrib-uglify": "^0.9.1", 29 | "grunt-mocha": "^0.4.13", 30 | "grunt-rigger": "^0.6.0", 31 | "jquery": "^2.1.4" 32 | }, 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /src/amd.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof exports === 'object') { 3 | 4 | var underscore = require('underscore'); 5 | var backbone = require('backbone'); 6 | 7 | module.exports = factory(underscore, backbone); 8 | 9 | } else if (typeof define === 'function' && define.amd) { 10 | 11 | define(['underscore', 'backbone'], factory); 12 | 13 | } 14 | }(this, function (_, Backbone) { 15 | 16 | //= backbone.computedfields.js 17 | return Backbone.ComputedFields; 18 | 19 | })); 20 | -------------------------------------------------------------------------------- /src/backbone.computedfields.js: -------------------------------------------------------------------------------- 1 | Backbone.ComputedFields = (function(Backbone, _){ 2 | 3 | var ComputedFields = function (model) { 4 | this.model = model; 5 | this._computedFields = []; 6 | 7 | this.initialize(); 8 | }; 9 | 10 | _.extend(ComputedFields.prototype, { 11 | initialize: function () { 12 | _.bindAll( 13 | this, 14 | '_bindModelEvents', 15 | '_computeFieldValue', 16 | '_dependentFields', 17 | '_isModelInitialized', 18 | '_lookUpComputedFields', 19 | '_thenComputedChanges', 20 | '_thenDependentChanges', 21 | '_toJSON', 22 | '_wrapJSON', 23 | 'initialize' 24 | ); 25 | 26 | this._lookUpComputedFields(); 27 | this._bindModelEvents(); 28 | this._wrapJSON(); 29 | }, 30 | 31 | _lookUpComputedFields: function () { 32 | var computed = _.isFunction(this.model.computed) ? this.model.computed() : this.model.computed; 33 | 34 | for (var obj in computed) { 35 | var field = computed[obj]; 36 | 37 | if (field && (field.set || field.get)) { 38 | this._computedFields.push({name: obj, field: field}); 39 | } 40 | } 41 | }, 42 | 43 | _bindModelEvents: function () { 44 | _.each(this._computedFields, _.bind(function (computedField) { 45 | var fieldName = computedField.name; 46 | var field = computedField.field; 47 | 48 | var updateComputed = _.bind(function () { 49 | var value = this._computeFieldValue(field); 50 | this.model.set(fieldName, value, { skipChangeEvent: true }); 51 | }, this); 52 | 53 | var updateDependent = _.bind(function (model, value, options) { 54 | if (options && options.skipChangeEvent) { 55 | return; 56 | } 57 | 58 | if (field.set) { 59 | var fields = this._dependentFields(field.depends); 60 | value = value || this.model.get(fieldName); 61 | 62 | field.set.call(this.model, value, fields); 63 | this.model.set(fields, options); 64 | } 65 | }, this); 66 | 67 | this._thenDependentChanges(field.depends, updateComputed); 68 | this._thenComputedChanges(fieldName, updateDependent); 69 | 70 | if (this._isModelInitialized()) { 71 | updateComputed(); 72 | } 73 | }, this)); 74 | }, 75 | 76 | _isModelInitialized: function () { 77 | return !_.isEmpty(this.model.attributes); 78 | }, 79 | 80 | _thenDependentChanges: function (depends, callback) { 81 | _.each(depends, _.bind(function (name) { 82 | if (typeof (name) === 'string') { 83 | this.model.on('change:' + name, callback); 84 | } 85 | 86 | if (typeof (name) === 'function') { 87 | name.call(this.model, callback); 88 | } 89 | }, this)); 90 | }, 91 | 92 | _thenComputedChanges: function (fieldName, callback) { 93 | this.model.on('change:' + fieldName, callback); 94 | }, 95 | 96 | _wrapJSON: function () { 97 | this.model.toJSON = _.wrap(this.model.toJSON, this._toJSON); 98 | }, 99 | 100 | _toJSON: function (toJSON) { 101 | var args = Array.prototype.slice.call(arguments, 1), 102 | attributes = toJSON.apply(this.model, args), 103 | strip = !!(args[0] || {}).computedFields; 104 | 105 | var stripped = strip ? {} : _.reduce(this._computedFields, function (memo, computed) { 106 | if (computed.field.toJSON === false) { 107 | memo.push(computed.name); 108 | } 109 | return memo; 110 | }, []); 111 | 112 | return _.omit(attributes, stripped); 113 | }, 114 | 115 | _computeFieldValue: function (computedField) { 116 | if (computedField && computedField.get) { 117 | var fields = this._dependentFields(computedField.depends); 118 | return computedField.get.call(this.model, fields); 119 | } 120 | }, 121 | 122 | _dependentFields: function (depends) { 123 | return _.reduce(depends, _.bind(function (memo, field) { 124 | if (_.isString(field)) { 125 | memo[field] = this.model.get(field); 126 | } 127 | return memo; 128 | }, this), {}); 129 | } 130 | 131 | }); 132 | 133 | return ComputedFields; 134 | 135 | })(Backbone, _); 136 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Spec Runner 6 | 7 | 8 | 9 |
10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/lib/expect.js: -------------------------------------------------------------------------------- 1 | 2 | (function (global, module) { 3 | 4 | if ('undefined' == typeof module) { 5 | var module = { exports: {} } 6 | , exports = module.exports 7 | } 8 | 9 | /** 10 | * Exports. 11 | */ 12 | 13 | module.exports = expect; 14 | expect.Assertion = Assertion; 15 | 16 | /** 17 | * Exports version. 18 | */ 19 | 20 | expect.version = '0.1.2'; 21 | 22 | /** 23 | * Possible assertion flags. 24 | */ 25 | 26 | var flags = { 27 | not: ['to', 'be', 'have', 'include', 'only'] 28 | , to: ['be', 'have', 'include', 'only', 'not'] 29 | , only: ['have'] 30 | , have: ['own'] 31 | , be: ['an'] 32 | }; 33 | 34 | function expect (obj) { 35 | return new Assertion(obj); 36 | } 37 | 38 | /** 39 | * Constructor 40 | * 41 | * @api private 42 | */ 43 | 44 | function Assertion (obj, flag, parent) { 45 | this.obj = obj; 46 | this.flags = {}; 47 | 48 | if (undefined != parent) { 49 | this.flags[flag] = true; 50 | 51 | for (var i in parent.flags) { 52 | if (parent.flags.hasOwnProperty(i)) { 53 | this.flags[i] = true; 54 | } 55 | } 56 | } 57 | 58 | var $flags = flag ? flags[flag] : keys(flags) 59 | , self = this 60 | 61 | if ($flags) { 62 | for (var i = 0, l = $flags.length; i < l; i++) { 63 | // avoid recursion 64 | if (this.flags[$flags[i]]) continue; 65 | 66 | var name = $flags[i] 67 | , assertion = new Assertion(this.obj, name, this) 68 | 69 | if ('function' == typeof Assertion.prototype[name]) { 70 | // clone the function, make sure we dont touch the prot reference 71 | var old = this[name]; 72 | this[name] = function () { 73 | return old.apply(self, arguments); 74 | } 75 | 76 | for (var fn in Assertion.prototype) { 77 | if (Assertion.prototype.hasOwnProperty(fn) && fn != name) { 78 | this[name][fn] = bind(assertion[fn], assertion); 79 | } 80 | } 81 | } else { 82 | this[name] = assertion; 83 | } 84 | } 85 | } 86 | }; 87 | 88 | /** 89 | * Performs an assertion 90 | * 91 | * @api private 92 | */ 93 | 94 | Assertion.prototype.assert = function (truth, msg, error) { 95 | var msg = this.flags.not ? error : msg 96 | , ok = this.flags.not ? !truth : truth; 97 | 98 | if (!ok) { 99 | throw new Error(msg); 100 | } 101 | 102 | this.and = new Assertion(this.obj); 103 | }; 104 | 105 | /** 106 | * Check if the value is truthy 107 | * 108 | * @api public 109 | */ 110 | 111 | Assertion.prototype.ok = function () { 112 | this.assert( 113 | !!this.obj 114 | , 'expected ' + i(this.obj) + ' to be truthy' 115 | , 'expected ' + i(this.obj) + ' to be falsy'); 116 | }; 117 | 118 | /** 119 | * Assert that the function throws. 120 | * 121 | * @param {Function|RegExp} callback, or regexp to match error string against 122 | * @api public 123 | */ 124 | 125 | Assertion.prototype.throwError = 126 | Assertion.prototype.throwException = function (fn) { 127 | expect(this.obj).to.be.a('function'); 128 | 129 | var thrown = false 130 | , not = this.flags.not 131 | 132 | try { 133 | this.obj(); 134 | } catch (e) { 135 | if ('function' == typeof fn) { 136 | fn(e); 137 | } else if ('object' == typeof fn) { 138 | var subject = 'string' == typeof e ? e : e.message; 139 | if (not) { 140 | expect(subject).to.not.match(fn); 141 | } else { 142 | expect(subject).to.match(fn); 143 | } 144 | } 145 | thrown = true; 146 | } 147 | 148 | if ('object' == typeof fn && not) { 149 | // in the presence of a matcher, ensure the `not` only applies to 150 | // the matching. 151 | this.flags.not = false; 152 | } 153 | 154 | var name = this.obj.name || 'fn'; 155 | this.assert( 156 | thrown 157 | , 'expected ' + name + ' to throw an exception' 158 | , 'expected ' + name + ' not to throw an exception'); 159 | }; 160 | 161 | /** 162 | * Checks if the array is empty. 163 | * 164 | * @api public 165 | */ 166 | 167 | Assertion.prototype.empty = function () { 168 | var expectation; 169 | 170 | if ('object' == typeof this.obj && null !== this.obj && !isArray(this.obj)) { 171 | if ('number' == typeof this.obj.length) { 172 | expectation = !this.obj.length; 173 | } else { 174 | expectation = !keys(this.obj).length; 175 | } 176 | } else { 177 | if ('string' != typeof this.obj) { 178 | expect(this.obj).to.be.an('object'); 179 | } 180 | 181 | expect(this.obj).to.have.property('length'); 182 | expectation = !this.obj.length; 183 | } 184 | 185 | this.assert( 186 | expectation 187 | , 'expected ' + i(this.obj) + ' to be empty' 188 | , 'expected ' + i(this.obj) + ' to not be empty'); 189 | return this; 190 | }; 191 | 192 | /** 193 | * Checks if the obj exactly equals another. 194 | * 195 | * @api public 196 | */ 197 | 198 | Assertion.prototype.be = 199 | Assertion.prototype.equal = function (obj) { 200 | this.assert( 201 | obj === this.obj 202 | , 'expected ' + i(this.obj) + ' to equal ' + i(obj) 203 | , 'expected ' + i(this.obj) + ' to not equal ' + i(obj)); 204 | return this; 205 | }; 206 | 207 | /** 208 | * Checks if the obj sortof equals another. 209 | * 210 | * @api public 211 | */ 212 | 213 | Assertion.prototype.eql = function (obj) { 214 | this.assert( 215 | expect.eql(obj, this.obj) 216 | , 'expected ' + i(this.obj) + ' to sort of equal ' + i(obj) 217 | , 'expected ' + i(this.obj) + ' to sort of not equal ' + i(obj)); 218 | return this; 219 | }; 220 | 221 | /** 222 | * Assert within start to finish (inclusive). 223 | * 224 | * @param {Number} start 225 | * @param {Number} finish 226 | * @api public 227 | */ 228 | 229 | Assertion.prototype.within = function (start, finish) { 230 | var range = start + '..' + finish; 231 | this.assert( 232 | this.obj >= start && this.obj <= finish 233 | , 'expected ' + i(this.obj) + ' to be within ' + range 234 | , 'expected ' + i(this.obj) + ' to not be within ' + range); 235 | return this; 236 | }; 237 | 238 | /** 239 | * Assert typeof / instance of 240 | * 241 | * @api public 242 | */ 243 | 244 | Assertion.prototype.a = 245 | Assertion.prototype.an = function (type) { 246 | if ('string' == typeof type) { 247 | // proper english in error msg 248 | var n = /^[aeiou]/.test(type) ? 'n' : ''; 249 | 250 | // typeof with support for 'array' 251 | this.assert( 252 | 'array' == type ? isArray(this.obj) : 253 | 'object' == type 254 | ? 'object' == typeof this.obj && null !== this.obj 255 | : type == typeof this.obj 256 | , 'expected ' + i(this.obj) + ' to be a' + n + ' ' + type 257 | , 'expected ' + i(this.obj) + ' not to be a' + n + ' ' + type); 258 | } else { 259 | // instanceof 260 | var name = type.name || 'supplied constructor'; 261 | this.assert( 262 | this.obj instanceof type 263 | , 'expected ' + i(this.obj) + ' to be an instance of ' + name 264 | , 'expected ' + i(this.obj) + ' not to be an instance of ' + name); 265 | } 266 | 267 | return this; 268 | }; 269 | 270 | /** 271 | * Assert numeric value above _n_. 272 | * 273 | * @param {Number} n 274 | * @api public 275 | */ 276 | 277 | Assertion.prototype.greaterThan = 278 | Assertion.prototype.above = function (n) { 279 | this.assert( 280 | this.obj > n 281 | , 'expected ' + i(this.obj) + ' to be above ' + n 282 | , 'expected ' + i(this.obj) + ' to be below ' + n); 283 | return this; 284 | }; 285 | 286 | /** 287 | * Assert numeric value below _n_. 288 | * 289 | * @param {Number} n 290 | * @api public 291 | */ 292 | 293 | Assertion.prototype.lessThan = 294 | Assertion.prototype.below = function (n) { 295 | this.assert( 296 | this.obj < n 297 | , 'expected ' + i(this.obj) + ' to be below ' + n 298 | , 'expected ' + i(this.obj) + ' to be above ' + n); 299 | return this; 300 | }; 301 | 302 | /** 303 | * Assert string value matches _regexp_. 304 | * 305 | * @param {RegExp} regexp 306 | * @api public 307 | */ 308 | 309 | Assertion.prototype.match = function (regexp) { 310 | this.assert( 311 | regexp.exec(this.obj) 312 | , 'expected ' + i(this.obj) + ' to match ' + regexp 313 | , 'expected ' + i(this.obj) + ' not to match ' + regexp); 314 | return this; 315 | }; 316 | 317 | /** 318 | * Assert property "length" exists and has value of _n_. 319 | * 320 | * @param {Number} n 321 | * @api public 322 | */ 323 | 324 | Assertion.prototype.length = function (n) { 325 | expect(this.obj).to.have.property('length'); 326 | var len = this.obj.length; 327 | this.assert( 328 | n == len 329 | , 'expected ' + i(this.obj) + ' to have a length of ' + n + ' but got ' + len 330 | , 'expected ' + i(this.obj) + ' to not have a length of ' + len); 331 | return this; 332 | }; 333 | 334 | /** 335 | * Assert property _name_ exists, with optional _val_. 336 | * 337 | * @param {String} name 338 | * @param {Mixed} val 339 | * @api public 340 | */ 341 | 342 | Assertion.prototype.property = function (name, val) { 343 | if (this.flags.own) { 344 | this.assert( 345 | Object.prototype.hasOwnProperty.call(this.obj, name) 346 | , 'expected ' + i(this.obj) + ' to have own property ' + i(name) 347 | , 'expected ' + i(this.obj) + ' to not have own property ' + i(name)); 348 | return this; 349 | } 350 | 351 | if (this.flags.not && undefined !== val) { 352 | if (undefined === this.obj[name]) { 353 | throw new Error(i(this.obj) + ' has no property ' + i(name)); 354 | } 355 | } else { 356 | var hasProp; 357 | try { 358 | hasProp = name in this.obj 359 | } catch (e) { 360 | hasProp = undefined !== this.obj[name] 361 | } 362 | 363 | this.assert( 364 | hasProp 365 | , 'expected ' + i(this.obj) + ' to have a property ' + i(name) 366 | , 'expected ' + i(this.obj) + ' to not have a property ' + i(name)); 367 | } 368 | 369 | if (undefined !== val) { 370 | this.assert( 371 | val === this.obj[name] 372 | , 'expected ' + i(this.obj) + ' to have a property ' + i(name) 373 | + ' of ' + i(val) + ', but got ' + i(this.obj[name]) 374 | , 'expected ' + i(this.obj) + ' to not have a property ' + i(name) 375 | + ' of ' + i(val)); 376 | } 377 | 378 | this.obj = this.obj[name]; 379 | return this; 380 | }; 381 | 382 | /** 383 | * Assert that the array contains _obj_ or string contains _obj_. 384 | * 385 | * @param {Mixed} obj|string 386 | * @api public 387 | */ 388 | 389 | Assertion.prototype.string = 390 | Assertion.prototype.contain = function (obj) { 391 | if ('string' == typeof this.obj) { 392 | this.assert( 393 | ~this.obj.indexOf(obj) 394 | , 'expected ' + i(this.obj) + ' to contain ' + i(obj) 395 | , 'expected ' + i(this.obj) + ' to not contain ' + i(obj)); 396 | } else { 397 | this.assert( 398 | ~indexOf(this.obj, obj) 399 | , 'expected ' + i(this.obj) + ' to contain ' + i(obj) 400 | , 'expected ' + i(this.obj) + ' to not contain ' + i(obj)); 401 | } 402 | return this; 403 | }; 404 | 405 | /** 406 | * Assert exact keys or inclusion of keys by using 407 | * the `.own` modifier. 408 | * 409 | * @param {Array|String ...} keys 410 | * @api public 411 | */ 412 | 413 | Assertion.prototype.key = 414 | Assertion.prototype.keys = function ($keys) { 415 | var str 416 | , ok = true; 417 | 418 | $keys = isArray($keys) 419 | ? $keys 420 | : Array.prototype.slice.call(arguments); 421 | 422 | if (!$keys.length) throw new Error('keys required'); 423 | 424 | var actual = keys(this.obj) 425 | , len = $keys.length; 426 | 427 | // Inclusion 428 | ok = every($keys, function (key) { 429 | return ~indexOf(actual, key); 430 | }); 431 | 432 | // Strict 433 | if (!this.flags.not && this.flags.only) { 434 | ok = ok && $keys.length == actual.length; 435 | } 436 | 437 | // Key string 438 | if (len > 1) { 439 | $keys = map($keys, function (key) { 440 | return i(key); 441 | }); 442 | var last = $keys.pop(); 443 | str = $keys.join(', ') + ', and ' + last; 444 | } else { 445 | str = i($keys[0]); 446 | } 447 | 448 | // Form 449 | str = (len > 1 ? 'keys ' : 'key ') + str; 450 | 451 | // Have / include 452 | str = (!this.flags.only ? 'include ' : 'only have ') + str; 453 | 454 | // Assertion 455 | this.assert( 456 | ok 457 | , 'expected ' + i(this.obj) + ' to ' + str 458 | , 'expected ' + i(this.obj) + ' to not ' + str); 459 | 460 | return this; 461 | }; 462 | 463 | /** 464 | * Function bind implementation. 465 | */ 466 | 467 | function bind (fn, scope) { 468 | return function () { 469 | return fn.apply(scope, arguments); 470 | } 471 | } 472 | 473 | /** 474 | * Array every compatibility 475 | * 476 | * @see bit.ly/5Fq1N2 477 | * @api public 478 | */ 479 | 480 | function every (arr, fn, thisObj) { 481 | var scope = thisObj || global; 482 | for (var i = 0, j = arr.length; i < j; ++i) { 483 | if (!fn.call(scope, arr[i], i, arr)) { 484 | return false; 485 | } 486 | } 487 | return true; 488 | }; 489 | 490 | /** 491 | * Array indexOf compatibility. 492 | * 493 | * @see bit.ly/a5Dxa2 494 | * @api public 495 | */ 496 | 497 | function indexOf (arr, o, i) { 498 | if (Array.prototype.indexOf) { 499 | return Array.prototype.indexOf.call(arr, o, i); 500 | } 501 | 502 | if (arr.length === undefined) { 503 | return -1; 504 | } 505 | 506 | for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0 507 | ; i < j && arr[i] !== o; i++); 508 | 509 | return j <= i ? -1 : i; 510 | }; 511 | 512 | /** 513 | * Inspects an object. 514 | * 515 | * @see taken from node.js `util` module (copyright Joyent, MIT license) 516 | * @api private 517 | */ 518 | 519 | function i (obj, showHidden, depth) { 520 | var seen = []; 521 | 522 | function stylize (str) { 523 | return str; 524 | }; 525 | 526 | function format (value, recurseTimes) { 527 | // Provide a hook for user-specified inspect functions. 528 | // Check that value is an object with an inspect function on it 529 | if (value && typeof value.inspect === 'function' && 530 | // Filter out the util module, it's inspect function is special 531 | value !== exports && 532 | // Also filter out any prototype objects using the circular check. 533 | !(value.constructor && value.constructor.prototype === value)) { 534 | return value.inspect(recurseTimes); 535 | } 536 | 537 | // Primitive types cannot have properties 538 | switch (typeof value) { 539 | case 'undefined': 540 | return stylize('undefined', 'undefined'); 541 | 542 | case 'string': 543 | var simple = '\'' + json.stringify(value).replace(/^"|"$/g, '') 544 | .replace(/'/g, "\\'") 545 | .replace(/\\"/g, '"') + '\''; 546 | return stylize(simple, 'string'); 547 | 548 | case 'number': 549 | return stylize('' + value, 'number'); 550 | 551 | case 'boolean': 552 | return stylize('' + value, 'boolean'); 553 | } 554 | // For some reason typeof null is "object", so special case here. 555 | if (value === null) { 556 | return stylize('null', 'null'); 557 | } 558 | 559 | // Look up the keys of the object. 560 | var visible_keys = keys(value); 561 | var $keys = showHidden ? Object.getOwnPropertyNames(value) : visible_keys; 562 | 563 | // Functions without properties can be shortcutted. 564 | if (typeof value === 'function' && $keys.length === 0) { 565 | if (isRegExp(value)) { 566 | return stylize('' + value, 'regexp'); 567 | } else { 568 | var name = value.name ? ': ' + value.name : ''; 569 | return stylize('[Function' + name + ']', 'special'); 570 | } 571 | } 572 | 573 | // Dates without properties can be shortcutted 574 | if (isDate(value) && $keys.length === 0) { 575 | return stylize(value.toUTCString(), 'date'); 576 | } 577 | 578 | var base, type, braces; 579 | // Determine the object type 580 | if (isArray(value)) { 581 | type = 'Array'; 582 | braces = ['[', ']']; 583 | } else { 584 | type = 'Object'; 585 | braces = ['{', '}']; 586 | } 587 | 588 | // Make functions say that they are functions 589 | if (typeof value === 'function') { 590 | var n = value.name ? ': ' + value.name : ''; 591 | base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; 592 | } else { 593 | base = ''; 594 | } 595 | 596 | // Make dates with properties first say the date 597 | if (isDate(value)) { 598 | base = ' ' + value.toUTCString(); 599 | } 600 | 601 | if ($keys.length === 0) { 602 | return braces[0] + base + braces[1]; 603 | } 604 | 605 | if (recurseTimes < 0) { 606 | if (isRegExp(value)) { 607 | return stylize('' + value, 'regexp'); 608 | } else { 609 | return stylize('[Object]', 'special'); 610 | } 611 | } 612 | 613 | seen.push(value); 614 | 615 | var output = map($keys, function (key) { 616 | var name, str; 617 | if (value.__lookupGetter__) { 618 | if (value.__lookupGetter__(key)) { 619 | if (value.__lookupSetter__(key)) { 620 | str = stylize('[Getter/Setter]', 'special'); 621 | } else { 622 | str = stylize('[Getter]', 'special'); 623 | } 624 | } else { 625 | if (value.__lookupSetter__(key)) { 626 | str = stylize('[Setter]', 'special'); 627 | } 628 | } 629 | } 630 | if (indexOf(visible_keys, key) < 0) { 631 | name = '[' + key + ']'; 632 | } 633 | if (!str) { 634 | if (indexOf(seen, value[key]) < 0) { 635 | if (recurseTimes === null) { 636 | str = format(value[key]); 637 | } else { 638 | str = format(value[key], recurseTimes - 1); 639 | } 640 | if (str.indexOf('\n') > -1) { 641 | if (isArray(value)) { 642 | str = map(str.split('\n'), function (line) { 643 | return ' ' + line; 644 | }).join('\n').substr(2); 645 | } else { 646 | str = '\n' + map(str.split('\n'), function (line) { 647 | return ' ' + line; 648 | }).join('\n'); 649 | } 650 | } 651 | } else { 652 | str = stylize('[Circular]', 'special'); 653 | } 654 | } 655 | if (typeof name === 'undefined') { 656 | if (type === 'Array' && key.match(/^\d+$/)) { 657 | return str; 658 | } 659 | name = json.stringify('' + key); 660 | if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { 661 | name = name.substr(1, name.length - 2); 662 | name = stylize(name, 'name'); 663 | } else { 664 | name = name.replace(/'/g, "\\'") 665 | .replace(/\\"/g, '"') 666 | .replace(/(^"|"$)/g, "'"); 667 | name = stylize(name, 'string'); 668 | } 669 | } 670 | 671 | return name + ': ' + str; 672 | }); 673 | 674 | seen.pop(); 675 | 676 | var numLinesEst = 0; 677 | var length = reduce(output, function (prev, cur) { 678 | numLinesEst++; 679 | if (indexOf(cur, '\n') >= 0) numLinesEst++; 680 | return prev + cur.length + 1; 681 | }, 0); 682 | 683 | if (length > 50) { 684 | output = braces[0] + 685 | (base === '' ? '' : base + '\n ') + 686 | ' ' + 687 | output.join(',\n ') + 688 | ' ' + 689 | braces[1]; 690 | 691 | } else { 692 | output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; 693 | } 694 | 695 | return output; 696 | } 697 | return format(obj, (typeof depth === 'undefined' ? 2 : depth)); 698 | }; 699 | 700 | function isArray (ar) { 701 | return Object.prototype.toString.call(ar) == '[object Array]'; 702 | }; 703 | 704 | function isRegExp(re) { 705 | var s = '' + re; 706 | return re instanceof RegExp || // easy case 707 | // duck-type for context-switching evalcx case 708 | typeof(re) === 'function' && 709 | re.constructor.name === 'RegExp' && 710 | re.compile && 711 | re.test && 712 | re.exec && 713 | s.match(/^\/.*\/[gim]{0,3}$/); 714 | }; 715 | 716 | function isDate(d) { 717 | if (d instanceof Date) return true; 718 | return false; 719 | }; 720 | 721 | function keys (obj) { 722 | if (Object.keys) { 723 | return Object.keys(obj); 724 | } 725 | 726 | var keys = []; 727 | 728 | for (var i in obj) { 729 | if (Object.prototype.hasOwnProperty.call(obj, i)) { 730 | keys.push(i); 731 | } 732 | } 733 | 734 | return keys; 735 | } 736 | 737 | function map (arr, mapper, that) { 738 | if (Array.prototype.map) { 739 | return Array.prototype.map.call(arr, mapper, that); 740 | } 741 | 742 | var other= new Array(arr.length); 743 | 744 | for (var i= 0, n = arr.length; i= 2) { 770 | var rv = arguments[1]; 771 | } else { 772 | do { 773 | if (i in this) { 774 | rv = this[i++]; 775 | break; 776 | } 777 | 778 | // if array contains no values, no initial value to return 779 | if (++i >= len) 780 | throw new TypeError(); 781 | } while (true); 782 | } 783 | 784 | for (; i < len; i++) { 785 | if (i in this) 786 | rv = fun.call(null, rv, this[i], i, this); 787 | } 788 | 789 | return rv; 790 | }; 791 | 792 | /** 793 | * Asserts deep equality 794 | * 795 | * @see taken from node.js `assert` module (copyright Joyent, MIT license) 796 | * @api private 797 | */ 798 | 799 | expect.eql = function eql (actual, expected) { 800 | // 7.1. All identical values are equivalent, as determined by ===. 801 | if (actual === expected) { 802 | return true; 803 | } else if ('undefined' != typeof Buffer 804 | && Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { 805 | if (actual.length != expected.length) return false; 806 | 807 | for (var i = 0; i < actual.length; i++) { 808 | if (actual[i] !== expected[i]) return false; 809 | } 810 | 811 | return true; 812 | 813 | // 7.2. If the expected value is a Date object, the actual value is 814 | // equivalent if it is also a Date object that refers to the same time. 815 | } else if (actual instanceof Date && expected instanceof Date) { 816 | return actual.getTime() === expected.getTime(); 817 | 818 | // 7.3. Other pairs that do not both pass typeof value == "object", 819 | // equivalence is determined by ==. 820 | } else if (typeof actual != 'object' && typeof expected != 'object') { 821 | return actual == expected; 822 | 823 | // 7.4. For all other Object pairs, including Array objects, equivalence is 824 | // determined by having the same number of owned properties (as verified 825 | // with Object.prototype.hasOwnProperty.call), the same set of keys 826 | // (although not necessarily the same order), equivalent values for every 827 | // corresponding key, and an identical "prototype" property. Note: this 828 | // accounts for both named and indexed properties on Arrays. 829 | } else { 830 | return objEquiv(actual, expected); 831 | } 832 | } 833 | 834 | function isUndefinedOrNull (value) { 835 | return value === null || value === undefined; 836 | } 837 | 838 | function isArguments (object) { 839 | return Object.prototype.toString.call(object) == '[object Arguments]'; 840 | } 841 | 842 | function objEquiv (a, b) { 843 | if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) 844 | return false; 845 | // an identical "prototype" property. 846 | if (a.prototype !== b.prototype) return false; 847 | //~~~I've managed to break Object.keys through screwy arguments passing. 848 | // Converting to array solves the problem. 849 | if (isArguments(a)) { 850 | if (!isArguments(b)) { 851 | return false; 852 | } 853 | a = pSlice.call(a); 854 | b = pSlice.call(b); 855 | return expect.eql(a, b); 856 | } 857 | try{ 858 | var ka = keys(a), 859 | kb = keys(b), 860 | key, i; 861 | } catch (e) {//happens when one is a string literal and the other isn't 862 | return false; 863 | } 864 | // having the same number of owned properties (keys incorporates hasOwnProperty) 865 | if (ka.length != kb.length) 866 | return false; 867 | //the same set of keys (although not necessarily the same order), 868 | ka.sort(); 869 | kb.sort(); 870 | //~~~cheap key test 871 | for (i = ka.length - 1; i >= 0; i--) { 872 | if (ka[i] != kb[i]) 873 | return false; 874 | } 875 | //equivalent values for every corresponding key, and 876 | //~~~possibly expensive deep test 877 | for (i = ka.length - 1; i >= 0; i--) { 878 | key = ka[i]; 879 | if (!expect.eql(a[key], b[key])) 880 | return false; 881 | } 882 | return true; 883 | } 884 | 885 | var json = (function () { 886 | "use strict"; 887 | 888 | if ('object' == typeof JSON && JSON.parse && JSON.stringify) { 889 | return { 890 | parse: nativeJSON.parse 891 | , stringify: nativeJSON.stringify 892 | } 893 | } 894 | 895 | var JSON = {}; 896 | 897 | function f(n) { 898 | // Format integers to have at least two digits. 899 | return n < 10 ? '0' + n : n; 900 | } 901 | 902 | function date(d, key) { 903 | return isFinite(d.valueOf()) ? 904 | d.getUTCFullYear() + '-' + 905 | f(d.getUTCMonth() + 1) + '-' + 906 | f(d.getUTCDate()) + 'T' + 907 | f(d.getUTCHours()) + ':' + 908 | f(d.getUTCMinutes()) + ':' + 909 | f(d.getUTCSeconds()) + 'Z' : null; 910 | }; 911 | 912 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 913 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 914 | gap, 915 | indent, 916 | meta = { // table of character substitutions 917 | '\b': '\\b', 918 | '\t': '\\t', 919 | '\n': '\\n', 920 | '\f': '\\f', 921 | '\r': '\\r', 922 | '"' : '\\"', 923 | '\\': '\\\\' 924 | }, 925 | rep; 926 | 927 | 928 | function quote(string) { 929 | 930 | // If the string contains no control characters, no quote characters, and no 931 | // backslash characters, then we can safely slap some quotes around it. 932 | // Otherwise we must also replace the offending characters with safe escape 933 | // sequences. 934 | 935 | escapable.lastIndex = 0; 936 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) { 937 | var c = meta[a]; 938 | return typeof c === 'string' ? c : 939 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 940 | }) + '"' : '"' + string + '"'; 941 | } 942 | 943 | 944 | function str(key, holder) { 945 | 946 | // Produce a string from holder[key]. 947 | 948 | var i, // The loop counter. 949 | k, // The member key. 950 | v, // The member value. 951 | length, 952 | mind = gap, 953 | partial, 954 | value = holder[key]; 955 | 956 | // If the value has a toJSON method, call it to obtain a replacement value. 957 | 958 | if (value instanceof Date) { 959 | value = date(key); 960 | } 961 | 962 | // If we were called with a replacer function, then call the replacer to 963 | // obtain a replacement value. 964 | 965 | if (typeof rep === 'function') { 966 | value = rep.call(holder, key, value); 967 | } 968 | 969 | // What happens next depends on the value's type. 970 | 971 | switch (typeof value) { 972 | case 'string': 973 | return quote(value); 974 | 975 | case 'number': 976 | 977 | // JSON numbers must be finite. Encode non-finite numbers as null. 978 | 979 | return isFinite(value) ? String(value) : 'null'; 980 | 981 | case 'boolean': 982 | case 'null': 983 | 984 | // If the value is a boolean or null, convert it to a string. Note: 985 | // typeof null does not produce 'null'. The case is included here in 986 | // the remote chance that this gets fixed someday. 987 | 988 | return String(value); 989 | 990 | // If the type is 'object', we might be dealing with an object or an array or 991 | // null. 992 | 993 | case 'object': 994 | 995 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 996 | // so watch out for that case. 997 | 998 | if (!value) { 999 | return 'null'; 1000 | } 1001 | 1002 | // Make an array to hold the partial results of stringifying this object value. 1003 | 1004 | gap += indent; 1005 | partial = []; 1006 | 1007 | // Is the value an array? 1008 | 1009 | if (Object.prototype.toString.apply(value) === '[object Array]') { 1010 | 1011 | // The value is an array. Stringify every element. Use null as a placeholder 1012 | // for non-JSON values. 1013 | 1014 | length = value.length; 1015 | for (i = 0; i < length; i += 1) { 1016 | partial[i] = str(i, value) || 'null'; 1017 | } 1018 | 1019 | // Join all of the elements together, separated with commas, and wrap them in 1020 | // brackets. 1021 | 1022 | v = partial.length === 0 ? '[]' : gap ? 1023 | '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : 1024 | '[' + partial.join(',') + ']'; 1025 | gap = mind; 1026 | return v; 1027 | } 1028 | 1029 | // If the replacer is an array, use it to select the members to be stringified. 1030 | 1031 | if (rep && typeof rep === 'object') { 1032 | length = rep.length; 1033 | for (i = 0; i < length; i += 1) { 1034 | if (typeof rep[i] === 'string') { 1035 | k = rep[i]; 1036 | v = str(k, value); 1037 | if (v) { 1038 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 1039 | } 1040 | } 1041 | } 1042 | } else { 1043 | 1044 | // Otherwise, iterate through all of the keys in the object. 1045 | 1046 | for (k in value) { 1047 | if (Object.prototype.hasOwnProperty.call(value, k)) { 1048 | v = str(k, value); 1049 | if (v) { 1050 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 1051 | } 1052 | } 1053 | } 1054 | } 1055 | 1056 | // Join all of the member texts together, separated with commas, 1057 | // and wrap them in braces. 1058 | 1059 | v = partial.length === 0 ? '{}' : gap ? 1060 | '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : 1061 | '{' + partial.join(',') + '}'; 1062 | gap = mind; 1063 | return v; 1064 | } 1065 | } 1066 | 1067 | // If the JSON object does not yet have a stringify method, give it one. 1068 | 1069 | JSON.stringify = function (value, replacer, space) { 1070 | 1071 | // The stringify method takes a value and an optional replacer, and an optional 1072 | // space parameter, and returns a JSON text. The replacer can be a function 1073 | // that can replace values, or an array of strings that will select the keys. 1074 | // A default replacer method can be provided. Use of the space parameter can 1075 | // produce text that is more easily readable. 1076 | 1077 | var i; 1078 | gap = ''; 1079 | indent = ''; 1080 | 1081 | // If the space parameter is a number, make an indent string containing that 1082 | // many spaces. 1083 | 1084 | if (typeof space === 'number') { 1085 | for (i = 0; i < space; i += 1) { 1086 | indent += ' '; 1087 | } 1088 | 1089 | // If the space parameter is a string, it will be used as the indent string. 1090 | 1091 | } else if (typeof space === 'string') { 1092 | indent = space; 1093 | } 1094 | 1095 | // If there is a replacer, it must be a function or an array. 1096 | // Otherwise, throw an error. 1097 | 1098 | rep = replacer; 1099 | if (replacer && typeof replacer !== 'function' && 1100 | (typeof replacer !== 'object' || 1101 | typeof replacer.length !== 'number')) { 1102 | throw new Error('JSON.stringify'); 1103 | } 1104 | 1105 | // Make a fake root object containing our value under the key of ''. 1106 | // Return the result of stringifying the value. 1107 | 1108 | return str('', {'': value}); 1109 | }; 1110 | 1111 | // If the JSON object does not yet have a parse method, give it one. 1112 | 1113 | JSON.parse = function (text, reviver) { 1114 | // The parse method takes a text and an optional reviver function, and returns 1115 | // a JavaScript value if the text is a valid JSON text. 1116 | 1117 | var j; 1118 | 1119 | function walk(holder, key) { 1120 | 1121 | // The walk method is used to recursively walk the resulting structure so 1122 | // that modifications can be made. 1123 | 1124 | var k, v, value = holder[key]; 1125 | if (value && typeof value === 'object') { 1126 | for (k in value) { 1127 | if (Object.prototype.hasOwnProperty.call(value, k)) { 1128 | v = walk(value, k); 1129 | if (v !== undefined) { 1130 | value[k] = v; 1131 | } else { 1132 | delete value[k]; 1133 | } 1134 | } 1135 | } 1136 | } 1137 | return reviver.call(holder, key, value); 1138 | } 1139 | 1140 | 1141 | // Parsing happens in four stages. In the first stage, we replace certain 1142 | // Unicode characters with escape sequences. JavaScript handles many characters 1143 | // incorrectly, either silently deleting them, or treating them as line endings. 1144 | 1145 | text = String(text); 1146 | cx.lastIndex = 0; 1147 | if (cx.test(text)) { 1148 | text = text.replace(cx, function (a) { 1149 | return '\\u' + 1150 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 1151 | }); 1152 | } 1153 | 1154 | // In the second stage, we run the text against regular expressions that look 1155 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 1156 | // because they can cause invocation, and '=' because it can cause mutation. 1157 | // But just to be safe, we want to reject all unexpected forms. 1158 | 1159 | // We split the second stage into 4 regexp operations in order to work around 1160 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 1161 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 1162 | // replace all simple value tokens with ']' characters. Third, we delete all 1163 | // open brackets that follow a colon or comma or that begin the text. Finally, 1164 | // we look to see that the remaining characters are only whitespace or ']' or 1165 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 1166 | 1167 | if (/^[\],:{}\s]*$/ 1168 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 1169 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 1170 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 1171 | 1172 | // In the third stage we use the eval function to compile the text into a 1173 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 1174 | // in JavaScript: it can begin a block or an object literal. We wrap the text 1175 | // in parens to eliminate the ambiguity. 1176 | 1177 | j = eval('(' + text + ')'); 1178 | 1179 | // In the optional fourth stage, we recursively walk the new structure, passing 1180 | // each name/value pair to a reviver function for possible transformation. 1181 | 1182 | return typeof reviver === 'function' ? 1183 | walk({'': j}, '') : j; 1184 | } 1185 | 1186 | // If the text is not JSON parseable, then a SyntaxError is thrown. 1187 | 1188 | throw new SyntaxError('JSON.parse'); 1189 | }; 1190 | 1191 | return JSON; 1192 | })(); 1193 | 1194 | if ('undefined' != typeof window) { 1195 | window.expect = module.exports; 1196 | } 1197 | 1198 | })( 1199 | this 1200 | , 'undefined' != typeof module ? module : {} 1201 | , 'undefined' != typeof exports ? exports : {} 1202 | ); 1203 | -------------------------------------------------------------------------------- /test/lib/mocha/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | body { 3 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | padding: 60px 50px; 5 | } 6 | 7 | #mocha ul, #mocha li { 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | #mocha ul { 13 | list-style: none; 14 | } 15 | 16 | #mocha h1, #mocha h2 { 17 | margin: 0; 18 | } 19 | 20 | #mocha h1 { 21 | margin-top: 15px; 22 | font-size: 1em; 23 | font-weight: 200; 24 | } 25 | 26 | #mocha h1 a { 27 | text-decoration: none; 28 | color: inherit; 29 | } 30 | 31 | #mocha h1 a:hover { 32 | text-decoration: underline; 33 | } 34 | 35 | #mocha .suite .suite h1 { 36 | margin-top: 0; 37 | font-size: .8em; 38 | } 39 | 40 | #mocha h2 { 41 | font-size: 12px; 42 | font-weight: normal; 43 | cursor: pointer; 44 | } 45 | 46 | #mocha .suite { 47 | margin-left: 15px; 48 | } 49 | 50 | #mocha .test { 51 | margin-left: 15px; 52 | } 53 | 54 | #mocha .test:hover h2::after { 55 | position: relative; 56 | top: 0; 57 | right: -10px; 58 | content: '(view source)'; 59 | font-size: 12px; 60 | font-family: arial; 61 | color: #888; 62 | } 63 | 64 | #mocha .test.pending:hover h2::after { 65 | content: '(pending)'; 66 | font-family: arial; 67 | } 68 | 69 | #mocha .test.pass.medium .duration { 70 | background: #C09853; 71 | } 72 | 73 | #mocha .test.pass.slow .duration { 74 | background: #B94A48; 75 | } 76 | 77 | #mocha .test.pass::before { 78 | content: '✓'; 79 | font-size: 12px; 80 | display: block; 81 | float: left; 82 | margin-right: 5px; 83 | color: #00d6b2; 84 | } 85 | 86 | #mocha .test.pass .duration { 87 | font-size: 9px; 88 | margin-left: 5px; 89 | padding: 2px 5px; 90 | color: white; 91 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 92 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 93 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -webkit-border-radius: 5px; 95 | -moz-border-radius: 5px; 96 | -ms-border-radius: 5px; 97 | -o-border-radius: 5px; 98 | border-radius: 5px; 99 | } 100 | 101 | #mocha .test.pass.fast .duration { 102 | display: none; 103 | } 104 | 105 | #mocha .test.pending { 106 | color: #0b97c4; 107 | } 108 | 109 | #mocha .test.pending::before { 110 | content: '◦'; 111 | color: #0b97c4; 112 | } 113 | 114 | #mocha .test.fail { 115 | color: #c00; 116 | } 117 | 118 | #mocha .test.fail pre { 119 | color: black; 120 | } 121 | 122 | #mocha .test.fail::before { 123 | content: '✖'; 124 | font-size: 12px; 125 | display: block; 126 | float: left; 127 | margin-right: 5px; 128 | color: #c00; 129 | } 130 | 131 | #mocha .test pre.error { 132 | color: #c00; 133 | } 134 | 135 | #mocha .test pre { 136 | display: inline-block; 137 | font: 12px/1.5 monaco, monospace; 138 | margin: 5px; 139 | padding: 15px; 140 | border: 1px solid #eee; 141 | border-bottom-color: #ddd; 142 | -webkit-border-radius: 3px; 143 | -webkit-box-shadow: 0 1px 3px #eee; 144 | } 145 | 146 | #report.pass .test.fail { 147 | display: none; 148 | } 149 | 150 | #report.fail .test.pass { 151 | display: none; 152 | } 153 | 154 | #error { 155 | color: #c00; 156 | font-size: 1.5 em; 157 | font-weight: 100; 158 | letter-spacing: 1px; 159 | } 160 | 161 | #stats { 162 | position: fixed; 163 | top: 15px; 164 | right: 10px; 165 | font-size: 12px; 166 | margin: 0; 167 | color: #888; 168 | } 169 | 170 | #stats .progress { 171 | float: right; 172 | padding-top: 0; 173 | } 174 | 175 | #stats em { 176 | color: black; 177 | } 178 | 179 | #stats a { 180 | text-decoration: none; 181 | color: inherit; 182 | } 183 | 184 | #stats a:hover { 185 | border-bottom: 1px solid #eee; 186 | } 187 | 188 | #stats li { 189 | display: inline-block; 190 | margin: 0 5px; 191 | list-style: none; 192 | padding-top: 11px; 193 | } 194 | 195 | code .comment { color: #ddd } 196 | code .init { color: #2F6FAD } 197 | code .string { color: #5890AD } 198 | code .keyword { color: #8A6343 } 199 | code .number { color: #2F6FAD } 200 | -------------------------------------------------------------------------------- /test/runner/mocha.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var runner = mocha.run(); 3 | 4 | if(!window.PHANTOMJS) return; 5 | 6 | runner.on('test', function(test) { 7 | sendMessage('testStart', test.title); 8 | }); 9 | 10 | runner.on('test end', function(test) { 11 | sendMessage('testDone', test.title, test.state); 12 | }); 13 | 14 | runner.on('suite', function(suite) { 15 | sendMessage('suiteStart', suite.title); 16 | }); 17 | 18 | runner.on('suite end', function(suite) { 19 | if (suite.root) return; 20 | sendMessage('suiteDone', suite.title); 21 | }); 22 | 23 | runner.on('fail', function(test, err) { 24 | sendMessage('testFail', test.title, err); 25 | }); 26 | 27 | runner.on('end', function() { 28 | var output = { 29 | failed : this.failures, 30 | passed : this.total - this.failures, 31 | total : this.total 32 | }; 33 | 34 | sendMessage('done', output.failed,output.passed, output.total); 35 | }); 36 | 37 | function sendMessage() { 38 | var args = [].slice.call(arguments); 39 | alert(JSON.stringify(args)); 40 | } 41 | })(); 42 | -------------------------------------------------------------------------------- /test/spec/backbone.computedfields.spec.js: -------------------------------------------------------------------------------- 1 | describe('Backbone.ComputedFields spec', function() { 2 | 3 | describe('when ComputedFields initialized', function () { 4 | 5 | var model; 6 | 7 | beforeEach(function () { 8 | var Model = Backbone.Model.extend({ 9 | initialize: function () { 10 | this.computedFields = new Backbone.ComputedFields(this); 11 | }, 12 | 13 | computed: { 14 | grossPrice: { 15 | get: function () { 16 | return 100; 17 | } 18 | } 19 | } 20 | }); 21 | 22 | model = new Model({ netPrice: 100, vatRate: 5}); 23 | }); 24 | 25 | it ('should be initialized', function () { 26 | expect(model.computedFields).to.exist; 27 | expect(model.computedFields._computedFields.length).to.equal(1); 28 | }); 29 | 30 | it ('should access model attributes', function () { 31 | expect(model.get('netPrice')).to.equal(100); 32 | expect(model.get('vatRate')).to.equal(5); 33 | }); 34 | 35 | describe('when initialize with empty', function () { 36 | beforeEach(function () { 37 | var Model = Backbone.Model.extend({ 38 | initialize: function () { 39 | this.getHasBeenCalled = false; 40 | this.computedFields = new Backbone.ComputedFields(this); 41 | }, 42 | 43 | computed: { 44 | grossPrice: { 45 | get: function () { 46 | this.getHasBeenCalled = true; 47 | } 48 | } 49 | } 50 | }); 51 | 52 | model = new Model(); 53 | }); 54 | 55 | it ('should not call computed field getter', function () { 56 | expect(model.getHasBeenCalled).to.equal(false); 57 | }); 58 | }); 59 | 60 | }); 61 | 62 | describe('when ComputedFields initialized and computed is a function', function () { 63 | 64 | var model; 65 | 66 | beforeEach(function () { 67 | var Model = Backbone.Model.extend({ 68 | initialize: function () { 69 | this.computedFields = new Backbone.ComputedFields(this); 70 | }, 71 | 72 | computed: function() { 73 | return { 74 | grossPrice: { 75 | get: function () { 76 | return 100; 77 | } 78 | } 79 | }; 80 | } 81 | }); 82 | 83 | model = new Model({ netPrice: 100, vatRate: 5}); 84 | }); 85 | 86 | it ('should be initialized', function () { 87 | expect(model.computedFields).to.exist; 88 | expect(model.computedFields._computedFields.length).to.equal(1); 89 | }); 90 | 91 | it ('should access model attributes', function () { 92 | expect(model.get('netPrice')).to.equal(100); 93 | expect(model.get('vatRate')).to.equal(5); 94 | }); 95 | 96 | describe('when initialize with empty', function () { 97 | beforeEach(function () { 98 | var Model = Backbone.Model.extend({ 99 | initialize: function () { 100 | this.getHasBeenCalled = false; 101 | this.computedFields = new Backbone.ComputedFields(this); 102 | }, 103 | 104 | computed: function() { 105 | return { 106 | grossPrice: { 107 | get: function () { 108 | this.getHasBeenCalled = true; 109 | } 110 | } 111 | }; 112 | } 113 | }); 114 | 115 | model = new Model(); 116 | }); 117 | 118 | it ('should not call computed field getter', function () { 119 | expect(model.getHasBeenCalled).to.equal(false); 120 | }); 121 | }); 122 | 123 | }); 124 | 125 | describe('when ComputedFields are used', function () { 126 | 127 | var model; 128 | 129 | beforeEach(function () { 130 | var Model = Backbone.Model.extend({ 131 | defaults: { 132 | 'netPrice': 0.0, 133 | 'vatRate': 0.0 134 | }, 135 | 136 | initialize: function () { 137 | this.computedFields = new Backbone.ComputedFields(this); 138 | }, 139 | 140 | computed: { 141 | grossPrice: { 142 | get: function () { 143 | return 105; 144 | } 145 | } 146 | } 147 | 148 | }); 149 | 150 | model = new Model({ netPrice: 100, vatRate: 5}); 151 | }); 152 | 153 | it ('should calculate grossPrice', function () { 154 | expect(model.get('grossPrice')).to.equal(105); 155 | }); 156 | 157 | }); 158 | 159 | describe('when dependent fields are used', function () { 160 | 161 | var model; 162 | 163 | beforeEach(function () { 164 | var Model = Backbone.Model.extend({ 165 | defaults: { 166 | 'netPrice': 0.0, 167 | 'vatRate': 0.0 168 | }, 169 | 170 | initialize: function () { 171 | this.computedFields = new Backbone.ComputedFields(this); 172 | }, 173 | 174 | computed: { 175 | grossPrice: { 176 | depends: ['netPrice', 'vatRate'], 177 | get: function (fields) { 178 | return fields.netPrice * (1 + fields.vatRate / 100); 179 | } 180 | } 181 | } 182 | }); 183 | 184 | model = new Model({ netPrice: 100, vatRate: 20}); 185 | }); 186 | 187 | it ('should used dependent fields for calculation', function () { 188 | expect(model.get('grossPrice')).to.equal(120); 189 | }); 190 | }); 191 | 192 | 193 | describe('when dependent field is changed', function () { 194 | 195 | var model; 196 | 197 | beforeEach(function () { 198 | var Model = Backbone.Model.extend({ 199 | defaults: { 200 | 'netPrice': 0.0, 201 | 'vatRate': 0.0 202 | }, 203 | 204 | initialize: function () { 205 | this.computedFields = new Backbone.ComputedFields(this); 206 | }, 207 | 208 | computed: { 209 | grossPrice: { 210 | depends: ['netPrice', 'vatRate'], 211 | get: function (fields) { 212 | return fields.netPrice * (1 + fields.vatRate / 100); 213 | } 214 | } 215 | } 216 | }); 217 | 218 | model = new Model({ netPrice: 100, vatRate: 20}); 219 | }); 220 | 221 | describe('vatRate changed', function () { 222 | 223 | beforeEach(function () { 224 | model.set({vatRate: 5}); 225 | }); 226 | 227 | it ('should calculate field value updated', function () { 228 | expect(model.get('grossPrice')).to.equal(105); 229 | }); 230 | 231 | it ('dependent field remains the same', function () { 232 | expect(model.get('netPrice')).to.equal(100); 233 | }); 234 | 235 | }); 236 | 237 | describe('netPrice changed', function () { 238 | 239 | beforeEach(function () { 240 | model.set({netPrice: 200}); 241 | }); 242 | 243 | it ('should calculate field value updated', function () { 244 | expect(model.get('grossPrice')).to.equal(240); 245 | }); 246 | 247 | it ('dependent field remains the same', function () { 248 | expect(model.get('vatRate')).to.equal(20); 249 | }); 250 | 251 | }); 252 | 253 | }); 254 | 255 | describe('when calculated field is changed', function () { 256 | 257 | var triggerMethodSpy, model; 258 | 259 | beforeEach(function () { 260 | var Model = Backbone.Model.extend({ 261 | defaults: { 262 | 'netPrice': 0.0, 263 | 'vatRate': 0.0 264 | }, 265 | 266 | initialize: function () { 267 | this.computedFields = new Backbone.ComputedFields(this); 268 | }, 269 | 270 | computed: { 271 | grossPrice: { 272 | depends: ['netPrice', 'vatRate'], 273 | get: function (fields) { 274 | return fields.netPrice * (1 + fields.vatRate / 100); 275 | }, 276 | set: function (value, fields) { 277 | fields.netPrice = value / (1 + fields.vatRate / 100); 278 | } 279 | } 280 | } 281 | }); 282 | 283 | model = new Model({ netPrice: 100, vatRate: 20}); 284 | triggerMethodSpy = sinon.spy(model, 'trigger'); 285 | 286 | model.set({ grossPrice: 80 }); 287 | }); 288 | 289 | it ('should updated dependent field', function () { 290 | expect(model.get('netPrice')).to.equal(80 / (1 + 20 / 100)); 291 | }); 292 | 293 | }); 294 | 295 | describe ('when model changing', function () { 296 | 297 | var model; 298 | 299 | beforeEach(function () { 300 | var Model = Backbone.Model.extend({ 301 | defaults: { 302 | 'netPrice': 0.0, 303 | 'vatRate': 0.0 304 | }, 305 | 306 | initialize: function () { 307 | this.computedFields = new Backbone.ComputedFields(this); 308 | }, 309 | 310 | computed: { 311 | grossPrice: { 312 | depends: ['netPrice', 'vatRate'], 313 | get: function (fields) { 314 | return fields.netPrice * (1 + fields.vatRate / 100); 315 | }, 316 | set: function (value, fields) { 317 | fields.netPrice = value / (1 + fields.vatRate / 100); 318 | } 319 | } 320 | } 321 | }); 322 | 323 | model = new Model({ vatRate: 20}); 324 | sinon.spy(model, 'trigger'); 325 | }); 326 | 327 | describe('when changing dependent field', function () { 328 | 329 | beforeEach(function () { 330 | model.set({ netPrice: 100 }); 331 | }); 332 | 333 | it ('should netPrice change event trigger', function () { 334 | expect(model.trigger.calledWith('change:netPrice')).to.equal(true); 335 | }); 336 | 337 | it ('should grossPrice change event trigger', function () { 338 | expect(model.trigger.calledWith('change:grossPrice')).to.equal(true); 339 | }); 340 | 341 | it ('should vatRate be silent', function () { 342 | expect(model.trigger.calledWith('change:vatRate')).to.equal(false); 343 | }); 344 | 345 | it ('should model change event triggered', function () { 346 | expect(model.trigger.calledWith('change')).to.equal(true); 347 | }); 348 | 349 | describe ('when changing dependent field', function () { 350 | 351 | beforeEach (function () { 352 | model.trigger.reset(); 353 | model.set({ vatRate: 5 }); 354 | }); 355 | 356 | it ('should netPrice be silent', function () { 357 | expect(model.trigger.calledWith('change:netPrice')).to.equal(false); 358 | }); 359 | }); 360 | }); 361 | 362 | describe('when changing calculated field', function () { 363 | 364 | beforeEach(function () { 365 | model.set({grossPrice: 80}); 366 | }); 367 | 368 | it ('should grossPrice change event trigger', function () { 369 | expect(model.trigger.calledWith('change:grossPrice')).to.equal(true); 370 | }); 371 | 372 | it('should netPrice change event trigger', function () { 373 | expect(model.trigger.calledWith('change:netPrice')).to.equal(true); 374 | }); 375 | 376 | it ('should vatRate field remains silent', function () { 377 | expect(model.trigger.calledWith('change:vatRate')).to.equal(false); 378 | }); 379 | 380 | it ('should model change event triggered', function () { 381 | expect(model.trigger.calledWith('change')).to.equal(true); 382 | }); 383 | 384 | }); 385 | 386 | describe('when changing ordinar field', function () { 387 | 388 | beforeEach(function () { 389 | model.set({name: 'Super Product'}); 390 | }); 391 | 392 | it ('should not grossPrice change event trigger', function () { 393 | expect(model.trigger.calledWith('change:grossPrice')).to.equal(false); 394 | }); 395 | 396 | it('should not netPrice change event trigger', function () { 397 | expect(model.trigger.calledWith('change:netPrice')).to.equal(false); 398 | }); 399 | 400 | it ('should not vatRate field remains silent', function () { 401 | expect(model.trigger.calledWith('change:vatRate')).to.equal(false); 402 | }); 403 | 404 | it ('should model change event triggered', function () { 405 | expect(model.trigger.calledWith('change')).to.equal(true); 406 | }); 407 | 408 | }); 409 | 410 | }); 411 | 412 | describe('when model serialized to JSON', function () { 413 | 414 | var json, model; 415 | 416 | beforeEach(function () { 417 | var Model = Backbone.Model.extend({ 418 | defaults: { 419 | 'netPrice': 0.0, 420 | 'vatRate': 0.0 421 | }, 422 | 423 | initialize: function () { 424 | this.computedFields = new Backbone.ComputedFields(this); 425 | }, 426 | 427 | computed: { 428 | grossPrice: { 429 | depends: ['netPrice', 'vatRate'], 430 | get: function (fields) { 431 | return fields.netPrice * (1 + fields.vatRate / 100); 432 | }, 433 | set: function (value, fields) { 434 | fields.netPrice = value / (1 + fields.vatRate / 100); 435 | } 436 | } 437 | } 438 | }); 439 | 440 | model = new Model({ netPrice: 100, vatRate: 20}); 441 | json = model.toJSON(); 442 | }); 443 | 444 | it ('should computed field be part of JSON by default', function () { 445 | expect(json.grossPrice).to.be.ok; 446 | }); 447 | 448 | describe('when computed is stripped out', function () { 449 | 450 | beforeEach(function () { 451 | var Model = Backbone.Model.extend({ 452 | defaults: { 453 | 'netPrice': 0.0, 454 | 'vatRate': 0.0 455 | }, 456 | 457 | initialize: function () { 458 | this.computedFields = new Backbone.ComputedFields(this); 459 | }, 460 | 461 | computed: { 462 | grossPrice: { 463 | depends: ['netPrice', 'vatRate'], 464 | get: function (fields) { 465 | return fields.netPrice * (1 + fields.vatRate / 100); 466 | }, 467 | set: function (value, fields) { 468 | fields.netPrice = value / (1 + fields.vatRate / 100); 469 | }, 470 | toJSON: false 471 | } 472 | } 473 | }); 474 | 475 | model = new Model({ netPrice: 100, vatRate: 20}); 476 | 477 | json = model.toJSON(); 478 | }); 479 | 480 | it ('should computed field stripped out of JSON', function () { 481 | expect(json.grossPrice).to.not.be.ok; 482 | }); 483 | 484 | }); 485 | 486 | describe('when computed is overriden by computedFields option', function () { 487 | 488 | beforeEach(function () { 489 | var Model = Backbone.Model.extend({ 490 | defaults: { 491 | 'netPrice': 0.0, 492 | 'vatRate': 0.0 493 | }, 494 | 495 | initialize: function () { 496 | this.computedFields = new Backbone.ComputedFields(this); 497 | }, 498 | 499 | computed: { 500 | grossPrice: { 501 | depends: ['netPrice', 'vatRate'], 502 | get: function (fields) { 503 | return fields.netPrice * (1 + fields.vatRate / 100); 504 | }, 505 | set: function (value, fields) { 506 | fields.netPrice = value / (1 + fields.vatRate / 100); 507 | }, 508 | toJSON: false 509 | } 510 | } 511 | }); 512 | 513 | model = new Model({ netPrice: 100, vatRate: 20}); 514 | json = model.toJSON({ computedFields: true }); 515 | }); 516 | 517 | it ('should computed field be part of JSON', function () { 518 | expect(json.grossPrice).to.be.ok; 519 | }); 520 | 521 | }); 522 | 523 | }); 524 | 525 | describe('when ComputedFields initialized in Backbone.Model via Backbone.Collection', function () { 526 | 527 | var model, collection; 528 | 529 | beforeEach(function () { 530 | var Model = Backbone.Model.extend({ 531 | defaults: { 532 | 'netPrice': 100 533 | }, 534 | 535 | initialize: function () { 536 | this.computedFields = new Backbone.ComputedFields(this); 537 | }, 538 | 539 | computed: { 540 | grossPrice: { 541 | depends: ['netPrice'], 542 | get: function (fields) { 543 | return fields.netPrice * 2; 544 | } 545 | } 546 | } 547 | }); 548 | 549 | var Collection = Backbone.Collection.extend({ 550 | model: Model 551 | }); 552 | 553 | collection = new Collection(); 554 | collection.push({ netPrice: 100 }, {wait: true}); 555 | model = collection.at(0); 556 | }); 557 | 558 | it ('should be initialized', function () { 559 | expect(model.computedFields).to.exist; 560 | expect(model.computedFields._computedFields.length).to.equal(1); 561 | }); 562 | 563 | it ('should get value of computed field', function () { 564 | expect(model.get('grossPrice')).to.equal(200); 565 | }); 566 | }); 567 | 568 | describe('when computed model is validating', function () { 569 | 570 | var model; 571 | 572 | beforeEach(function () { 573 | 574 | var Model = Backbone.Model.extend({ 575 | defaults: { 576 | 'netPrice': 0.0, 577 | 'vatRate': 0.0 578 | }, 579 | 580 | initialize: function () { 581 | this.computedFields = new Backbone.ComputedFields(this); 582 | }, 583 | 584 | validate: function (attrs) { 585 | 586 | var errors = []; 587 | if (attrs.netPrice && !_.isNumber(attrs.netPrice) || attrs.netPrice < 0) { 588 | errors.push('netPrice is invalid'); 589 | } 590 | 591 | if (attrs.grossPrice && !_.isNumber(attrs.grossPrice) || attrs.grossPrice < 0) { 592 | errors.push('grossPrice is invalid'); 593 | } 594 | 595 | return errors.length > 0 ? errors : false; 596 | }, 597 | 598 | computed: { 599 | grossPrice: { 600 | depends: ['netPrice', 'vatRate'], 601 | get: function (fields) { 602 | return fields.netPrice * (1 + fields.vatRate / 100); 603 | }, 604 | set: function (value, fields) { 605 | fields.netPrice = value / (1 + fields.vatRate / 100); 606 | } 607 | } 608 | } 609 | }); 610 | 611 | model = new Model({ netPrice: 100, vatRate: 20}); 612 | }); 613 | 614 | it ('it should be initially in correct state', function () { 615 | expect(model.get('netPrice')).to.equal(100); 616 | expect(model.get('grossPrice')).to.equal(120); 617 | }); 618 | 619 | }); 620 | 621 | describe ('when depends on external', function () { 622 | 623 | var model; 624 | 625 | beforeEach(function () { 626 | 627 | var Model = Backbone.Model.extend({ 628 | defaults: { 629 | 'name': null, 630 | 'netPrice': 0.0, 631 | 'vatRate': 0.0 632 | }, 633 | 634 | initialize: function () { 635 | this.external = new Backbone.Model({value: 0}); 636 | this.computedFields = new Backbone.ComputedFields(this); 637 | }, 638 | 639 | computed: { 640 | grossPrice: { 641 | depends: ['netPrice', 'vatRate', function (callback) { 642 | this.external.on('change:value', callback); 643 | }], 644 | get: function (fields) { 645 | return this.external.get('value'); 646 | } 647 | } 648 | } 649 | }); 650 | 651 | model = new Model({ netPrice: 100, vatRate: 20 }); 652 | }); 653 | 654 | it ('should have correct external value', function () { 655 | expect(model.get('grossPrice')).to.equal(0); 656 | }); 657 | 658 | describe ('and external changed', function () { 659 | 660 | beforeEach(function () { 661 | model.external.set({value: 1}); 662 | }); 663 | 664 | it ('should computed field updated', function () { 665 | expect(model.get('grossPrice')).to.equal(1); 666 | }); 667 | }); 668 | 669 | it ('should not pass the depends function as a field', function() { 670 | var computedDef = model.computed.grossPrice; 671 | var dependsFunction = computedDef.depends[2]; 672 | 673 | sinon.spy(computedDef, 'get'); 674 | model.set('netPrice', '100'); 675 | expect(computedDef.get.firstCall.args[0]).to.not.contain.key(dependsFunction.toString()); 676 | }); 677 | 678 | }); 679 | 680 | }); 681 | --------------------------------------------------------------------------------