├── .gitignore ├── .hsdoc ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── bower.json ├── mixen.coffee ├── mixen.js ├── mixen.min.js ├── package.json └── spec ├── mixen.spec.coffee ├── mixen.spec.js └── vendor ├── backbone-1.0.0 └── backbone.js ├── jasmine-1.3.1 ├── SpecRunner.html └── lib │ └── jasmine-1.3.1 │ ├── MIT.LICENSE │ ├── jasmine-html.js │ ├── jasmine.css │ └── jasmine.js ├── jquery-1.10.2 └── jquery.js └── underscore-1.5.2 └── underscore.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.hsdoc: -------------------------------------------------------------------------------- 1 | source: "{mixen.coffee,spec/mixen.spec.coffee}" 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | - "0.8" 6 | notifications: 7 | email: 8 | - zbloom@hubspot.com 9 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | grunt.initConfig 3 | pkg: grunt.file.readJSON("package.json") 4 | coffee: 5 | compile: 6 | files: 7 | 'mixen.js': 'mixen.coffee' 8 | 'spec/mixen.spec.js': 'spec/mixen.spec.coffee' 9 | 10 | watch: 11 | coffee: 12 | files: ['mixen.coffee', 'spec/mixen.spec.coffee'] 13 | tasks: ["coffee", "uglify"] 14 | 15 | uglify: 16 | options: 17 | banner: "/*! <%= pkg.name %> <%= pkg.version %> */\n" 18 | 19 | dist: 20 | src: 'mixen.js' 21 | dest: 'mixen.min.js' 22 | 23 | jasmine: 24 | options: 25 | specs: ['spec/mixen.spec.js'] 26 | src: [ 27 | 'spec/vendor/jquery-1.10.2/jquery.js', 28 | 'spec/vendor/underscore-1.5.2/underscore.js', 29 | 'spec/vendor/backbone-1.0.0/backbone.js', 30 | 'mixen.js' 31 | ] 32 | 33 | grunt.loadNpmTasks 'grunt-contrib-watch' 34 | grunt.loadNpmTasks 'grunt-contrib-uglify' 35 | grunt.loadNpmTasks 'grunt-contrib-coffee' 36 | grunt.loadNpmTasks 'grunt-contrib-jasmine' 37 | 38 | grunt.registerTask 'default', ['coffee', 'uglify'] 39 | grunt.registerTask 'build', ['coffee', 'uglify', 'jasmine'] 40 | grunt.registerTask 'test', ['coffee', 'uglify', 'jasmine'] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 HubSpot, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mixen 2 | ==================== 3 | 4 | Mixen lets you combine classes on the fly. With it you can build smaller, easier to understand and 5 | more testable components, and more easily share code with others. **It does not just merge the prototypes.** 6 | 7 | ```coffeescript 8 | class MyModel extends Mixen(Throttle, APIBinding, Validate, Backbone.Model) 9 | # Inheritance Chain: 10 | # 11 | # MyModel -> Throttle -> APIBinding -> Validate -> Backbone.Model 12 | 13 | class MyOtherModel extends Mixen(APIBinding, Backbone.Model) 14 | # Inheritance Chain: 15 | # 16 | # MyOtherModel -> APIBinding -> Backbone.Model 17 | ``` 18 | 19 | The 2kb library only exposes a single function, `Mixen`. This function allows you to combine 20 | classes together in such a way that the `super` keyword will dynamically call the appropriate method in the 21 | next mixin you're using. 22 | 23 | > Note: 24 | > 25 | > These examples are in CoffeeScript. Skip down to the bottom for a short description of how 26 | > this can be done with JavaScript. 27 | 28 | ### Usage 29 | 30 | Feel free to [start playing with Mixen](http://jsfiddle.net/4XgaR/7/) right now. 31 | 32 | On the browser include [mixen.min.js](https://raw.github.com/HubSpot/mixen/v0.5.2/mixen.min.js), and the `Mixen` function will be globally available. 33 | You can also use AMD. 34 | 35 | On node: 36 | 37 | ```bash 38 | npm install mixen 39 | ``` 40 | 41 | ```coffeescript 42 | Mixen = require('mixen') 43 | ``` 44 | 45 | The Mixen function takes in any number of classes, and returns an object: 46 | 47 | ```coffeescript 48 | MyObject = Mixen(Object1, Object2, ...) 49 | ``` 50 | 51 | Skip down for a list of the publicly available mixins. 52 | 53 | ### Example 54 | 55 | A mixin is just a class: 56 | 57 | ```coffeescript 58 | class OnlyRenderWithModel 59 | render: -> 60 | return unless @model 61 | 62 | super 63 | ``` 64 | 65 | Any view who would like your method can now use Mixen to mix you in: 66 | 67 | ```coffeescript 68 | class MyView extends Mixen(OnlyRenderWithModel, Backbone.View) 69 | ``` 70 | 71 | You can now replace your BaseModels and BaseViews with modular components. 72 | 73 | ### Multiple Mixins Which Share Methods 74 | 75 | Mixen adds one very important capability to inheritance, the ability to have multiple mixins all implement the same method. 76 | 77 | ```coffeescript 78 | class CountSyncs 79 | sync: -> 80 | @syncs = (@syncs or 0) + 1 81 | 82 | super 83 | ``` 84 | 85 | ```coffeescript 86 | class ThrottleSyncs 87 | sync: -> 88 | return if @syncing 89 | @syncing = true 90 | 91 | super.finally => 92 | @syncing = false 93 | ``` 94 | 95 | Now, you can mix in both classes. When the first mixin calls `super`, it will dynamically find and call the second 96 | mixin's `sync` method. 97 | 98 | ```coffeescript 99 | class MyModel extends Mixen(ThrottleSyncs, CountSyncs, Backbone.Model) 100 | ``` 101 | 102 | `MyModel` will both throttle it's sync's and keep track of it's sync count. 103 | 104 | Note that the count `CountSyncs` will change depending on if it is listed before or after 105 | `ThrottleSyncs`. All methods are resolved from left to right. In other words, 106 | when you call `super`, you are calling the mixin to the current mixins right. 107 | 108 | #### The End of the Chain 109 | 110 | When you're developing a mixin, you don't know if your mixin will be the last in a chain used 111 | to create a class or not. Therefore you must always call super (unless you want to break the chain), and 112 | you must always be ready for `super` to return undefined (as it will if there are no more classes mixed in 113 | which implement that method). 114 | 115 | ```coffeescript 116 | class UserInContext 117 | getContext: -> 118 | context = super ? {} 119 | context.user = 'bob smith' 120 | context 121 | 122 | class AuthInContext 123 | getContext: -> 124 | context = super ? {} 125 | context.auth = 'logged-in' 126 | context 127 | ``` 128 | 129 | Each getContext method will be called, in the order they are defined in the Mixen call: 130 | 131 | ```coffeescript 132 | class MyView extends Mixen(AuthInContext, UserInContext, Backbone.View) 133 | getContext: -> 134 | context = super 135 | context.x = 2 136 | context 137 | ``` 138 | 139 | ### Mixening in Constructors 140 | 141 | Mixins can have constructors. As long as the resultant class either does not have a constructor, 142 | or calls `super` in it's constructor, all of the mixins constructors will be called in the order 143 | they are defined. If you do not wish for the constructors to be called, simply don't call super 144 | in the constructor of the class extending the mixen. 145 | 146 | ```coffeescript 147 | class CallInitialize 148 | constructor: -> 149 | @initialize?() 150 | ``` 151 | 152 | ```coffeescript 153 | # initialize will be called 154 | class MyThing extends Mixen(CallInitialize) 155 | 156 | # initialize will be called 157 | class MyThing extends Mixen(CallInitialize) 158 | constructor: -> 159 | # Do whatever other stuff you want... 160 | 161 | super 162 | 163 | # initialize WON'T be called 164 | class MyThing extends Mixen(CallInitialize) 165 | constructor: -> 166 | # Never called super... 167 | ``` 168 | 169 | Note, that unlike the other methods, mixins should not call `super` in their constructors. This is 170 | necessary because, unlike with standard methods, all classes have a constructor, even if you never 171 | explicitly implemented one. This means that if we made you call `super`, you would have to explicitly 172 | call `super` in each constructor, even when you don't care to specify one. To keep things simple, we 173 | always call all the mixin's constructors in the order they are specified, provided the mixing class doesn't 174 | explicitly prevent it. 175 | 176 | ### Aliases 177 | 178 | Mixen doesn't create them for you, but you're more than welcome to create some helpful aliases as you need: 179 | 180 | ```coffeescript 181 | Mixen.View = (modules...) -> 182 | Mixen(modules..., Backbone.View) 183 | ``` 184 | 185 | You can do a similar thing to create a default list of mixins for your application: 186 | 187 | ```coffeescript 188 | ViewMixen = (modules...) -> 189 | Mixen(modules..., EventJanitor, Backbone.View) 190 | ``` 191 | 192 | ### Composition 193 | 194 | You can safely mixin other mixens: 195 | 196 | ```coffeescript 197 | BaseView = Mixen(EventJanitor, Backbone.View) 198 | 199 | class MyView extends Mixen(SuperSpecialModule, BaseView) 200 | ``` 201 | 202 | Diamond inheritance is not supported yet. 203 | 204 | ### Not Using CoffeeScript? 205 | 206 | If you're not using CoffeeScript, it is possible to write the necessary js manually. Replicating CoffeeScript's 207 | inheritance mechanism is fairly complicated however. It requires a robust extension mechanism, and replacing every 208 | `super` call used above with `ModuleName.__super__.methodName`. 209 | 210 | ```javascript 211 | var AuthInContext, MyView, UserInContext; 212 | 213 | UserInContext = function (){} 214 | 215 | UserInContext.prototype.getContext = function(){ 216 | var context = UserInContext.__super__.getContext.apply(this, arguments) || {}; 217 | context.user = 'bob smith'; 218 | return context; 219 | }; 220 | 221 | AuthInContext = function (){} 222 | 223 | AuthInContext.prototype.getContext = function(){ 224 | var context = AuthInContext.__super__.getContext.apply(this, arguments) || {}; 225 | context.auth = 'logged-in'; 226 | return context; 227 | }; 228 | 229 | MyView = function (){ 230 | return MyView.__super__.constructor.apply(this, arguments); 231 | } 232 | 233 | __extends(MyView, Mixen(AuthInContext, UserInContext, Backbone.View)); 234 | 235 | MyView.prototype.getContext = function(){ 236 | var context = MyView.__super__.getContext.apply(this, arguments); 237 | context.x = 2; 238 | return context; 239 | }; 240 | ``` 241 | 242 | Where `__extends` is implemented as: 243 | 244 | ```javascript 245 | var __hasProp = {}.hasOwnProperty, 246 | __extends = function(child, parent){ 247 | for (var key in parent) { 248 | if (__hasProp.call(parent, key)) 249 | child[key] = parent[key]; 250 | } 251 | 252 | function ctor() { 253 | this.constructor = child; 254 | } 255 | 256 | ctor.prototype = parent.prototype; 257 | child.prototype = new ctor(); 258 | child.__super__ = parent.prototype; 259 | 260 | return child; 261 | }; 262 | ``` 263 | 264 | ### Debugging 265 | 266 | If it's not working the way you expect, it's usually because you forgot to call `super` in one of your methods. 267 | 268 | Take a look at the tests for complete examples of how things should work. 269 | 270 | You can always ask us for help in GitHub Issues. 271 | 272 | ### Support 273 | 274 | Mixen is tested in IE6+, Firefox 3+, Chrome 14+, Safari 4+, Opera 10+, Safari on iOS 3+, Android 2.2+ and Node 0.8+. 275 | 276 | ### Contributing 277 | 278 | We welcome pull requests and discussion using GitHub Issues. 279 | 280 | To get setup for development, run this in the project directory: 281 | 282 | ```bash 283 | npm install 284 | ``` 285 | 286 | Then, you can run `grunt watch` to have it watch the source files for changes. 287 | Run `grunt test` to ensure that the tests still pass. 288 | You can also open `spec/vendor/jasmine-1.3.1/SpecRunner.html` in your browser to check the tests (after doing a `grunt` build). 289 | 290 | If you create a mixin which others might find useful, please name it `mixen--`, where type identifies what sort 291 | of thing this mixin is designed to extend (leave type out of it's general-purpose). 292 | 293 | Examples of good names: 294 | 295 | mixen-view-eventjanitor 296 | mixen-model-throttle 297 | 298 | ### Mixins 299 | 300 | #### Backbone 301 | 302 | ##### View 303 | 304 | - [Event Janitor](http://github.com/HubSpot/mixen-view-eventjanitor) 305 | 306 | ##### Model 307 | 308 | - [Throttle](http://github.com/HubSpot/mixen-model-throttle) 309 | 310 | Please let us know of any interesting Mixen's you make! 311 | 312 | ### Changelog 313 | 314 | - 0.5.0 - Initial public release 315 | - 0.5.1 - Fix bug with interoperability with Backbone.extend 316 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mixen", 3 | "main": "mixen.coffee", 4 | "version": "0.5.4", 5 | "homepage": "http://github.hubspot.com/mixen/", 6 | "authors": [ 7 | "Zack Bloom " 8 | ], 9 | "description": "Combine Javascript classes on the fly", 10 | "license": "MIT", 11 | "ignore": [ 12 | "node_modules", 13 | "bower_components" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /mixen.coffee: -------------------------------------------------------------------------------- 1 | # Mixen lets you combine classes together as needed. With it you can build smaller, 2 | # easier to understand and more testable components, and more easily share code with others. 3 | 4 | indexOf = (haystack, needle) -> 5 | for stalk, i in haystack 6 | return i if stalk is needle 7 | 8 | return -1 9 | 10 | uniqueId = do -> 11 | id = 0 12 | -> id++ 13 | 14 | Mixen = -> 15 | Mixen.createMixen arguments... 16 | 17 | stack = [] 18 | 19 | Mixen.createMixen = (mods...) -> 20 | # Since a single mixin module can be used multiple times, we need to 21 | # store the id of this instance on the outputted object, so we can 22 | # figure out which modules were included with it when it comes 23 | # time to resolve super calls. 24 | # 25 | # We could also iterate over every mixen we've created so far, but 26 | # that could have performance implications if you have lots of mixens. 27 | Last = mods[mods.length - 1] 28 | for module in mods.slice(0).reverse() 29 | class Inst extends Last 30 | # If the extending class calls `super`, or doesn't have a constructor, 31 | # this will be called to call each mixin's constructor. 32 | constructor: (args...) -> 33 | for mod in mods 34 | mod.apply @, args 35 | 36 | for own k, v of Inst.__super__ 37 | continue unless typeof v is 'function' 38 | 39 | do (k, v, module) -> 40 | Inst.__super__[k] = (args...) -> 41 | stack.unshift module 42 | ret = v.apply @, args 43 | stack.shift() 44 | return ret 45 | 46 | Last = Inst 47 | 48 | for method of module:: 49 | Inst::[method] = module::[method] 50 | 51 | for own method of module:: 52 | continue if method is 'constructor' 53 | continue if typeof(module::[method]) isnt 'function' 54 | 55 | # Coffeescript expands super calls into `ModuleName.__super__.methodName` 56 | # 57 | # Dynamically composing a bunch of inheriting classes out 58 | # of the mixins seems like a good idea, but it doesn't work because 59 | # CoffeeScript rewrites `__super__` calls statically based on the 60 | # class super is in, so they would not respect these classes. 61 | 62 | if module.__super__? 63 | # We don't want to mutate the actual module's super, as we could be messing with some 64 | # random unassuming class 65 | class NewSuper extends module.__super__ 66 | module.__super__ = NewSuper 67 | else 68 | module.__super__ = {} 69 | 70 | module.__super__[method] ?= moduleSuper(module, method) 71 | 72 | Last._mixen_modules = mods 73 | 74 | Last 75 | 76 | moduleSuper = (module, method) -> 77 | # This is called when super gets called in one of our mixin'ed modules. 78 | # 79 | # It resolves what the next module in the module list which has 80 | # this method defined and calls that method. 81 | (args...) -> 82 | modules = stack[0]?._mixen_modules or @constructor._mixen_modules 83 | return unless modules? 84 | 85 | pos = indexOf modules, module 86 | nextModule = null 87 | 88 | return if pos is -1 89 | 90 | while pos++ < modules.length - 1 91 | # Look through the remaining modules for the next one which implements 92 | # the method we're calling. 93 | nextModule = modules[pos] 94 | 95 | break if nextModule::[method]? 96 | 97 | if nextModule? and nextModule::? and nextModule::[method]? 98 | return nextModule::[method].apply @, args 99 | 100 | if typeof define is 'function' and define.amd 101 | # AMD 102 | define -> Mixen 103 | else if typeof exports is 'object' 104 | # Node 105 | module.exports = Mixen 106 | else 107 | # Global 108 | window.Mixen = Mixen 109 | -------------------------------------------------------------------------------- /mixen.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Mixen, indexOf, moduleSuper, stack, uniqueId, 3 | __slice = [].slice, 4 | __hasProp = {}.hasOwnProperty, 5 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 6 | 7 | indexOf = function(haystack, needle) { 8 | var i, stalk, _i, _len; 9 | for (i = _i = 0, _len = haystack.length; _i < _len; i = ++_i) { 10 | stalk = haystack[i]; 11 | if (stalk === needle) { 12 | return i; 13 | } 14 | } 15 | return -1; 16 | }; 17 | 18 | uniqueId = (function() { 19 | var id; 20 | id = 0; 21 | return function() { 22 | return id++; 23 | }; 24 | })(); 25 | 26 | Mixen = function() { 27 | return Mixen.createMixen.apply(Mixen, arguments); 28 | }; 29 | 30 | stack = []; 31 | 32 | Mixen.createMixen = function() { 33 | var Inst, Last, NewSuper, k, method, mods, module, v, _base, _fn, _i, _len, _ref, _ref1, _ref2, _ref3; 34 | mods = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 35 | Last = mods[mods.length - 1]; 36 | _ref = mods.slice(0).reverse(); 37 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 38 | module = _ref[_i]; 39 | Inst = (function(_super) { 40 | __extends(Inst, _super); 41 | 42 | function Inst() { 43 | var args, mod, _j, _len1; 44 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 45 | for (_j = 0, _len1 = mods.length; _j < _len1; _j++) { 46 | mod = mods[_j]; 47 | mod.apply(this, args); 48 | } 49 | } 50 | 51 | return Inst; 52 | 53 | })(Last); 54 | _ref1 = Inst.__super__; 55 | _fn = function(k, v, module) { 56 | return Inst.__super__[k] = function() { 57 | var args, ret; 58 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 59 | stack.unshift(module); 60 | ret = v.apply(this, args); 61 | stack.shift(); 62 | return ret; 63 | }; 64 | }; 65 | for (k in _ref1) { 66 | if (!__hasProp.call(_ref1, k)) continue; 67 | v = _ref1[k]; 68 | if (typeof v !== 'function') { 69 | continue; 70 | } 71 | _fn(k, v, module); 72 | } 73 | Last = Inst; 74 | for (method in module.prototype) { 75 | Inst.prototype[method] = module.prototype[method]; 76 | } 77 | _ref2 = module.prototype; 78 | for (method in _ref2) { 79 | if (!__hasProp.call(_ref2, method)) continue; 80 | if (method === 'constructor') { 81 | continue; 82 | } 83 | if (typeof module.prototype[method] !== 'function') { 84 | continue; 85 | } 86 | if (module.__super__ != null) { 87 | NewSuper = (function(_super) { 88 | __extends(NewSuper, _super); 89 | 90 | function NewSuper() { 91 | _ref3 = NewSuper.__super__.constructor.apply(this, arguments); 92 | return _ref3; 93 | } 94 | 95 | return NewSuper; 96 | 97 | })(module.__super__); 98 | module.__super__ = NewSuper; 99 | } else { 100 | module.__super__ = {}; 101 | } 102 | if ((_base = module.__super__)[method] == null) { 103 | _base[method] = moduleSuper(module, method); 104 | } 105 | } 106 | } 107 | Last._mixen_modules = mods; 108 | return Last; 109 | }; 110 | 111 | moduleSuper = function(module, method) { 112 | return function() { 113 | var args, modules, nextModule, pos, _ref; 114 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 115 | modules = ((_ref = stack[0]) != null ? _ref._mixen_modules : void 0) || this.constructor._mixen_modules; 116 | if (modules == null) { 117 | return; 118 | } 119 | pos = indexOf(modules, module); 120 | nextModule = null; 121 | if (pos === -1) { 122 | return; 123 | } 124 | while (pos++ < modules.length - 1) { 125 | nextModule = modules[pos]; 126 | if (nextModule.prototype[method] != null) { 127 | break; 128 | } 129 | } 130 | if ((nextModule != null) && (nextModule.prototype != null) && (nextModule.prototype[method] != null)) { 131 | return nextModule.prototype[method].apply(this, args); 132 | } 133 | }; 134 | }; 135 | 136 | if (typeof define === 'function' && define.amd) { 137 | define(function() { 138 | return Mixen; 139 | }); 140 | } else if (typeof exports === 'object') { 141 | module.exports = Mixen; 142 | } else { 143 | window.Mixen = Mixen; 144 | } 145 | 146 | }).call(this); 147 | -------------------------------------------------------------------------------- /mixen.min.js: -------------------------------------------------------------------------------- 1 | /*! mixen 0.5.3 */ 2 | (function(){var a,b,c,d,e,f=[].slice,g={}.hasOwnProperty,h=function(a,b){function c(){this.constructor=a}for(var d in b)g.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a};b=function(a,b){var c,d,e,f;for(c=e=0,f=a.length;f>e;c=++e)if(d=a[c],d===b)return c;return-1},e=function(){var a;return a=0,function(){return a++}}(),a=function(){return a.createMixen.apply(a,arguments)},d=[],a.createMixen=function(){var a,b,e,i,j,k,l,m,n,o,p,q,r,s,t,u;for(k=1<=arguments.length?f.call(arguments,0):[],b=k[k.length-1],r=k.slice(0).reverse(),p=0,q=r.length;q>p;p++){l=r[p],a=function(a){function b(){var a,b,c,d;for(a=1<=arguments.length?f.call(arguments,0):[],c=0,d=k.length;d>c;c++)b=k[c],b.apply(this,a)}return h(b,a),b}(b),s=a.__super__,o=function(b,c,e){return a.__super__[b]=function(){var a,b;return a=1<=arguments.length?f.call(arguments,0):[],d.unshift(e),b=c.apply(this,a),d.shift(),b}};for(i in s)g.call(s,i)&&(m=s[i],"function"==typeof m&&o(i,m,l));b=a;for(j in l.prototype)a.prototype[j]=l.prototype[j];t=l.prototype;for(j in t)g.call(t,j)&&"constructor"!==j&&"function"==typeof l.prototype[j]&&(null!=l.__super__?(e=function(a){function b(){return u=b.__super__.constructor.apply(this,arguments)}return h(b,a),b}(l.__super__),l.__super__=e):l.__super__={},null==(n=l.__super__)[j]&&(n[j]=c(l,j)))}return b._mixen_modules=k,b},c=function(a,c){return function(){var e,g,h,i,j;if(e=1<=arguments.length?f.call(arguments,0):[],g=(null!=(j=d[0])?j._mixen_modules:void 0)||this.constructor._mixen_modules,null!=g&&(i=b(g,a),h=null,-1!==i)){for(;i++", 24 | "maintainers": [ 25 | "Zack Bloom ", 26 | "Greg Sabo " 27 | ], 28 | "license": "MIT", 29 | "readmeFilename": "README.md", 30 | "dependencies": {}, 31 | "devDependencies": { 32 | "coffee-script": "~1.6.3", 33 | "grunt": "~0.4.1", 34 | "grunt-cli": "~0.1.9", 35 | "grunt-contrib-coffee": "~0.7.0", 36 | "grunt-contrib-uglify": "~0.2.2", 37 | "grunt-contrib-watch": "~0.5.3", 38 | "grunt-contrib-jasmine": "~0.5.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spec/mixen.spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'Mixen', -> 2 | it 'should be defined', -> 3 | expect(Mixen).toBeDefined() 4 | 5 | it 'should be possible to compose a class', -> 6 | class MyModule 7 | x: -> 3 8 | 9 | X = Mixen(MyModule) 10 | 11 | inst = new X 12 | 13 | expect(inst.x()).toBe(3) 14 | 15 | it 'should be possible to compose multiple classes together', -> 16 | class Module1 17 | x: -> (super ? 0) + 5 18 | 19 | class Module2 20 | x: -> (super ? 0) + 2 21 | 22 | X = Mixen(Module1, Module2) 23 | 24 | inst = new X 25 | 26 | expect(inst.x()).toBe(7) 27 | 28 | it 'should be possible to pass through a method from a mixin', -> 29 | class MyModule 30 | x: -> 3 31 | 32 | class X extends Mixen(MyModule) 33 | 34 | inst = new X 35 | 36 | expect(inst.x()).toBe(3) 37 | 38 | it 'should pass references to the previous function', -> 39 | class MyModule 40 | x: (arg) -> 41 | (super(arg) ? 0) + 2 42 | 43 | class X extends Mixen(MyModule) 44 | x: (arg) -> super(arg) + 8 45 | 46 | inst = new X 47 | 48 | expect(inst.x(0)).toBe(10) 49 | 50 | it 'should end up calling the parent classes methods', -> 51 | class Parent 52 | x: (a) -> a + 2 53 | 54 | class X extends Mixen(Parent) 55 | 56 | inst = new X 57 | 58 | expect(inst.x(2)).toBe(4) 59 | 60 | it 'should pass references to the chain of previous functions in the order defined', -> 61 | class Module1 62 | x: (arg) -> 63 | (super ? 5) + arg + '1' 64 | 65 | class Module2 66 | x: (arg) -> 67 | (super ? 4) + arg + '2' 68 | 69 | class X extends Mixen(Module2, Module1) 70 | x: (arg) -> super + arg + '0' 71 | 72 | inst = new X 73 | 74 | expect(inst.x('-')).toBe('5-1-2-0') 75 | 76 | it 'should play nice with backbone extend', -> 77 | X = Backbone.Model.extend({ 78 | x: -> 3 79 | y: -> 12 80 | }) 81 | 82 | Y = X.extend({ 83 | x: -> 6 84 | }) 85 | 86 | Z = Mixen(Y) 87 | 88 | expect((new Z).x()).toBe(6) 89 | expect((new Z).y()).toBe(12) 90 | expect((new Z).idAttribute).toBe('id') 91 | expect((new Z).cid).toBe('c4') 92 | 93 | it 'should call all constructors in the right order', -> 94 | order = '' 95 | 96 | class Module1 97 | constructor: -> 98 | expect(@ instanceof Module).toBe(true) 99 | 100 | order += '1' 101 | 102 | class Module2 103 | 104 | class Module3 105 | constructor: -> 106 | expect(@ instanceof Module).toBe(true) 107 | 108 | order += '3' 109 | 110 | class Module extends Mixen(Module1, Module2, Module3) 111 | constructor: -> 112 | super 113 | 114 | expect(@ instanceof Module).toBe(true) 115 | 116 | order += '4' 117 | 118 | inst = new Module 119 | 120 | expect(order).toBe('134') 121 | 122 | it 'should not call mixen constructors if super is not called', -> 123 | class Module1 124 | constructor: -> 125 | expect(true).toBe(false) 126 | 127 | class Module extends Mixen(Module1) 128 | constructor: -> 129 | expect(true).toBe(true) 130 | 131 | inst = new Module 132 | 133 | it 'should call mixen constructors if no constructor is defined', -> 134 | called = false 135 | 136 | class Module1 137 | constructor: -> 138 | called = true 139 | 140 | class Module extends Mixen(Module1) 141 | 142 | inst = new Module 143 | 144 | expect(called).toBe(true) 145 | 146 | it 'should pass through non-function properties', -> 147 | class Module1 148 | property: 4 149 | 150 | class X extends Mixen(Module1) 151 | 152 | inst = new X 153 | 154 | expect(inst.property).toBe(4) 155 | 156 | it 'should handle multiple mixins', -> 157 | class Module1 158 | x: -> 3 159 | 160 | class Module2 161 | y: -> 5 162 | 163 | class X extends Mixen(Module1, Module2) 164 | 165 | inst = new X 166 | 167 | expect(inst.x).toBeDefined() 168 | expect(inst.y).toBeDefined() 169 | expect(inst.x()).toBe(3) 170 | expect(inst.y()).toBe(5) 171 | 172 | it 'should be able to handle multiple mixens existing at once', -> 173 | class Module1 174 | x: -> '1' + (super ? '') 175 | 176 | class Module2 177 | x: -> '2' + (super ? '') 178 | 179 | class Module3 180 | x: -> '3' + (super ? '') 181 | 182 | class Module4 183 | 184 | class Module5 185 | x: -> '5' + (super ? '') 186 | 187 | class Module6 188 | x: -> '6' + (super ? '') 189 | 190 | class A extends Mixen(Module4, Module3, Module1) 191 | 192 | class B extends Mixen(Module5, Module3, Module2) 193 | 194 | class C extends Mixen(Module6, Module3, Module1) 195 | 196 | class D extends Mixen(Module6, Module3, Module1) 197 | 198 | class E extends Mixen(Module6, Module4, Module5) 199 | 200 | class F extends Mixen(Module1, Module6, Module5) 201 | 202 | a = new A 203 | b = new B 204 | d = new D 205 | c = new C 206 | e2 = new E 207 | f = new F 208 | e1 = new E 209 | 210 | expect(a.x()).toBe('31') 211 | expect(b.x()).toBe('532') 212 | expect(e1.x()).toBe('65') 213 | expect(c.x()).toBe('631') 214 | expect(d.x()).toBe('631') 215 | expect(e2.x()).toBe('65') 216 | expect(f.x()).toBe('165') 217 | 218 | it 'should be able to mixin mixens (constructor)', -> 219 | order = '' 220 | 221 | class Module1 222 | constructor: -> 223 | order += '-1-' 224 | 225 | class Module2 226 | constructor: -> 227 | order += '-2-' 228 | 229 | class Module12 extends Mixen(Module1, Module2) 230 | constructor: -> 231 | super 232 | order += '-12-' 233 | 234 | class Module3 235 | constructor: -> 236 | order += '-3-' 237 | 238 | class Module312 extends Mixen(Module3, Module12) 239 | constructor: -> 240 | super 241 | order += '-312-' 242 | 243 | inst = new Module312 244 | 245 | expect(order).toBe('-3--1--2--12--312-') 246 | 247 | it 'should be able to mixin mixens (not constructor)', -> 248 | order = '' 249 | 250 | class Module1 251 | init: -> 252 | super 253 | order += '-1-' 254 | 255 | class Module2 256 | init: -> 257 | super 258 | order += '-2-' 259 | 260 | class Module12 extends Mixen(Module1, Module2) 261 | init: -> 262 | super 263 | order += '-12-' 264 | 265 | class Module3 266 | init: -> 267 | super 268 | order += '-3-' 269 | 270 | class Module312 extends Mixen(Module3, Module12) 271 | init: -> 272 | super 273 | order += '-312-' 274 | 275 | inst = new Module312 276 | inst.init() 277 | 278 | expect(order).toBe('-2--1--12--3--312-') 279 | 280 | it 'should be able to mixin mixens (not constructor), order flipped', -> 281 | order = '' 282 | 283 | class Module1 284 | init: -> 285 | order += '-1-' 286 | super 287 | 288 | class Module2 289 | init: -> 290 | order += '-2-' 291 | super 292 | 293 | class Module12 extends Mixen(Module1, Module2) 294 | init: -> 295 | order += '-12-' 296 | super 297 | 298 | class Module3 299 | init: -> 300 | order += '-3-' 301 | super 302 | 303 | class Module312 extends Mixen(Module3, Module12) 304 | init: -> 305 | order += '-312-' 306 | super 307 | 308 | inst = new Module312 309 | inst.init() 310 | 311 | expect(order).toBe('-312--3--12--1--2-') 312 | 313 | it 'should be able to mixin mixens of mixens (not constructor)', -> 314 | order = '' 315 | 316 | class Module1 317 | init: -> 318 | super 319 | order += '-1-' 320 | 321 | class Module2 322 | init: -> 323 | super 324 | order += '-2-' 325 | 326 | class Module12 extends Mixen(Module1, Module2) 327 | init: -> 328 | super 329 | order += '-12-' 330 | 331 | class Module3 332 | init: -> 333 | super 334 | order += '-3-' 335 | 336 | class Module312 extends Mixen(Module3, Module12) 337 | init: -> 338 | super 339 | order += '-312-' 340 | 341 | class Module4 342 | init: -> 343 | super 344 | order += '-4-' 345 | 346 | class Module4312 extends Mixen(Module4, Module312) 347 | 348 | inst = new Module4312 349 | inst.init() 350 | 351 | expect(order).toBe('-2--1--12--3--312--4-') 352 | -------------------------------------------------------------------------------- /spec/mixen.spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var __hasProp = {}.hasOwnProperty, 3 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 4 | 5 | describe('Mixen', function() { 6 | it('should be defined', function() { 7 | return expect(Mixen).toBeDefined(); 8 | }); 9 | it('should be possible to compose a class', function() { 10 | var MyModule, X, inst; 11 | MyModule = (function() { 12 | function MyModule() {} 13 | 14 | MyModule.prototype.x = function() { 15 | return 3; 16 | }; 17 | 18 | return MyModule; 19 | 20 | })(); 21 | X = Mixen(MyModule); 22 | inst = new X; 23 | return expect(inst.x()).toBe(3); 24 | }); 25 | it('should be possible to compose multiple classes together', function() { 26 | var Module1, Module2, X, inst; 27 | Module1 = (function() { 28 | function Module1() {} 29 | 30 | Module1.prototype.x = function() { 31 | var _ref; 32 | return ((_ref = Module1.__super__.x.apply(this, arguments)) != null ? _ref : 0) + 5; 33 | }; 34 | 35 | return Module1; 36 | 37 | })(); 38 | Module2 = (function() { 39 | function Module2() {} 40 | 41 | Module2.prototype.x = function() { 42 | var _ref; 43 | return ((_ref = Module2.__super__.x.apply(this, arguments)) != null ? _ref : 0) + 2; 44 | }; 45 | 46 | return Module2; 47 | 48 | })(); 49 | X = Mixen(Module1, Module2); 50 | inst = new X; 51 | return expect(inst.x()).toBe(7); 52 | }); 53 | it('should be possible to pass through a method from a mixin', function() { 54 | var MyModule, X, inst, _ref; 55 | MyModule = (function() { 56 | function MyModule() {} 57 | 58 | MyModule.prototype.x = function() { 59 | return 3; 60 | }; 61 | 62 | return MyModule; 63 | 64 | })(); 65 | X = (function(_super) { 66 | __extends(X, _super); 67 | 68 | function X() { 69 | _ref = X.__super__.constructor.apply(this, arguments); 70 | return _ref; 71 | } 72 | 73 | return X; 74 | 75 | })(Mixen(MyModule)); 76 | inst = new X; 77 | return expect(inst.x()).toBe(3); 78 | }); 79 | it('should pass references to the previous function', function() { 80 | var MyModule, X, inst, _ref; 81 | MyModule = (function() { 82 | function MyModule() {} 83 | 84 | MyModule.prototype.x = function(arg) { 85 | var _ref; 86 | return ((_ref = MyModule.__super__.x.call(this, arg)) != null ? _ref : 0) + 2; 87 | }; 88 | 89 | return MyModule; 90 | 91 | })(); 92 | X = (function(_super) { 93 | __extends(X, _super); 94 | 95 | function X() { 96 | _ref = X.__super__.constructor.apply(this, arguments); 97 | return _ref; 98 | } 99 | 100 | X.prototype.x = function(arg) { 101 | return X.__super__.x.call(this, arg) + 8; 102 | }; 103 | 104 | return X; 105 | 106 | })(Mixen(MyModule)); 107 | inst = new X; 108 | return expect(inst.x(0)).toBe(10); 109 | }); 110 | it('should end up calling the parent classes methods', function() { 111 | var Parent, X, inst, _ref; 112 | Parent = (function() { 113 | function Parent() {} 114 | 115 | Parent.prototype.x = function(a) { 116 | return a + 2; 117 | }; 118 | 119 | return Parent; 120 | 121 | })(); 122 | X = (function(_super) { 123 | __extends(X, _super); 124 | 125 | function X() { 126 | _ref = X.__super__.constructor.apply(this, arguments); 127 | return _ref; 128 | } 129 | 130 | return X; 131 | 132 | })(Mixen(Parent)); 133 | inst = new X; 134 | return expect(inst.x(2)).toBe(4); 135 | }); 136 | it('should pass references to the chain of previous functions in the order defined', function() { 137 | var Module1, Module2, X, inst, _ref; 138 | Module1 = (function() { 139 | function Module1() {} 140 | 141 | Module1.prototype.x = function(arg) { 142 | var _ref; 143 | return ((_ref = Module1.__super__.x.apply(this, arguments)) != null ? _ref : 5) + arg + '1'; 144 | }; 145 | 146 | return Module1; 147 | 148 | })(); 149 | Module2 = (function() { 150 | function Module2() {} 151 | 152 | Module2.prototype.x = function(arg) { 153 | var _ref; 154 | return ((_ref = Module2.__super__.x.apply(this, arguments)) != null ? _ref : 4) + arg + '2'; 155 | }; 156 | 157 | return Module2; 158 | 159 | })(); 160 | X = (function(_super) { 161 | __extends(X, _super); 162 | 163 | function X() { 164 | _ref = X.__super__.constructor.apply(this, arguments); 165 | return _ref; 166 | } 167 | 168 | X.prototype.x = function(arg) { 169 | return X.__super__.x.apply(this, arguments) + arg + '0'; 170 | }; 171 | 172 | return X; 173 | 174 | })(Mixen(Module2, Module1)); 175 | inst = new X; 176 | return expect(inst.x('-')).toBe('5-1-2-0'); 177 | }); 178 | it('should play nice with backbone extend', function() { 179 | var X, Y, Z; 180 | X = Backbone.Model.extend({ 181 | x: function() { 182 | return 3; 183 | }, 184 | y: function() { 185 | return 12; 186 | } 187 | }); 188 | Y = X.extend({ 189 | x: function() { 190 | return 6; 191 | } 192 | }); 193 | Z = Mixen(Y); 194 | expect((new Z).x()).toBe(6); 195 | expect((new Z).y()).toBe(12); 196 | expect((new Z).idAttribute).toBe('id'); 197 | return expect((new Z).cid).toBe('c4'); 198 | }); 199 | it('should call all constructors in the right order', function() { 200 | var Module, Module1, Module2, Module3, inst, order; 201 | order = ''; 202 | Module1 = (function() { 203 | function Module1() { 204 | expect(this instanceof Module).toBe(true); 205 | order += '1'; 206 | } 207 | 208 | return Module1; 209 | 210 | })(); 211 | Module2 = (function() { 212 | function Module2() {} 213 | 214 | return Module2; 215 | 216 | })(); 217 | Module3 = (function() { 218 | function Module3() { 219 | expect(this instanceof Module).toBe(true); 220 | order += '3'; 221 | } 222 | 223 | return Module3; 224 | 225 | })(); 226 | Module = (function(_super) { 227 | __extends(Module, _super); 228 | 229 | function Module() { 230 | Module.__super__.constructor.apply(this, arguments); 231 | expect(this instanceof Module).toBe(true); 232 | order += '4'; 233 | } 234 | 235 | return Module; 236 | 237 | })(Mixen(Module1, Module2, Module3)); 238 | inst = new Module; 239 | return expect(order).toBe('134'); 240 | }); 241 | it('should not call mixen constructors if super is not called', function() { 242 | var Module, Module1, inst; 243 | Module1 = (function() { 244 | function Module1() { 245 | expect(true).toBe(false); 246 | } 247 | 248 | return Module1; 249 | 250 | })(); 251 | Module = (function(_super) { 252 | __extends(Module, _super); 253 | 254 | function Module() { 255 | expect(true).toBe(true); 256 | } 257 | 258 | return Module; 259 | 260 | })(Mixen(Module1)); 261 | return inst = new Module; 262 | }); 263 | it('should call mixen constructors if no constructor is defined', function() { 264 | var Module, Module1, called, inst, _ref; 265 | called = false; 266 | Module1 = (function() { 267 | function Module1() { 268 | called = true; 269 | } 270 | 271 | return Module1; 272 | 273 | })(); 274 | Module = (function(_super) { 275 | __extends(Module, _super); 276 | 277 | function Module() { 278 | _ref = Module.__super__.constructor.apply(this, arguments); 279 | return _ref; 280 | } 281 | 282 | return Module; 283 | 284 | })(Mixen(Module1)); 285 | inst = new Module; 286 | return expect(called).toBe(true); 287 | }); 288 | it('should pass through non-function properties', function() { 289 | var Module1, X, inst, _ref; 290 | Module1 = (function() { 291 | function Module1() {} 292 | 293 | Module1.prototype.property = 4; 294 | 295 | return Module1; 296 | 297 | })(); 298 | X = (function(_super) { 299 | __extends(X, _super); 300 | 301 | function X() { 302 | _ref = X.__super__.constructor.apply(this, arguments); 303 | return _ref; 304 | } 305 | 306 | return X; 307 | 308 | })(Mixen(Module1)); 309 | inst = new X; 310 | return expect(inst.property).toBe(4); 311 | }); 312 | it('should handle multiple mixins', function() { 313 | var Module1, Module2, X, inst, _ref; 314 | Module1 = (function() { 315 | function Module1() {} 316 | 317 | Module1.prototype.x = function() { 318 | return 3; 319 | }; 320 | 321 | return Module1; 322 | 323 | })(); 324 | Module2 = (function() { 325 | function Module2() {} 326 | 327 | Module2.prototype.y = function() { 328 | return 5; 329 | }; 330 | 331 | return Module2; 332 | 333 | })(); 334 | X = (function(_super) { 335 | __extends(X, _super); 336 | 337 | function X() { 338 | _ref = X.__super__.constructor.apply(this, arguments); 339 | return _ref; 340 | } 341 | 342 | return X; 343 | 344 | })(Mixen(Module1, Module2)); 345 | inst = new X; 346 | expect(inst.x).toBeDefined(); 347 | expect(inst.y).toBeDefined(); 348 | expect(inst.x()).toBe(3); 349 | return expect(inst.y()).toBe(5); 350 | }); 351 | it('should be able to handle multiple mixens existing at once', function() { 352 | var A, B, C, D, E, F, Module1, Module2, Module3, Module4, Module5, Module6, a, b, c, d, e1, e2, f, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; 353 | Module1 = (function() { 354 | function Module1() {} 355 | 356 | Module1.prototype.x = function() { 357 | var _ref; 358 | return '1' + ((_ref = Module1.__super__.x.apply(this, arguments)) != null ? _ref : ''); 359 | }; 360 | 361 | return Module1; 362 | 363 | })(); 364 | Module2 = (function() { 365 | function Module2() {} 366 | 367 | Module2.prototype.x = function() { 368 | var _ref; 369 | return '2' + ((_ref = Module2.__super__.x.apply(this, arguments)) != null ? _ref : ''); 370 | }; 371 | 372 | return Module2; 373 | 374 | })(); 375 | Module3 = (function() { 376 | function Module3() {} 377 | 378 | Module3.prototype.x = function() { 379 | var _ref; 380 | return '3' + ((_ref = Module3.__super__.x.apply(this, arguments)) != null ? _ref : ''); 381 | }; 382 | 383 | return Module3; 384 | 385 | })(); 386 | Module4 = (function() { 387 | function Module4() {} 388 | 389 | return Module4; 390 | 391 | })(); 392 | Module5 = (function() { 393 | function Module5() {} 394 | 395 | Module5.prototype.x = function() { 396 | var _ref; 397 | return '5' + ((_ref = Module5.__super__.x.apply(this, arguments)) != null ? _ref : ''); 398 | }; 399 | 400 | return Module5; 401 | 402 | })(); 403 | Module6 = (function() { 404 | function Module6() {} 405 | 406 | Module6.prototype.x = function() { 407 | var _ref; 408 | return '6' + ((_ref = Module6.__super__.x.apply(this, arguments)) != null ? _ref : ''); 409 | }; 410 | 411 | return Module6; 412 | 413 | })(); 414 | A = (function(_super) { 415 | __extends(A, _super); 416 | 417 | function A() { 418 | _ref = A.__super__.constructor.apply(this, arguments); 419 | return _ref; 420 | } 421 | 422 | return A; 423 | 424 | })(Mixen(Module4, Module3, Module1)); 425 | B = (function(_super) { 426 | __extends(B, _super); 427 | 428 | function B() { 429 | _ref1 = B.__super__.constructor.apply(this, arguments); 430 | return _ref1; 431 | } 432 | 433 | return B; 434 | 435 | })(Mixen(Module5, Module3, Module2)); 436 | C = (function(_super) { 437 | __extends(C, _super); 438 | 439 | function C() { 440 | _ref2 = C.__super__.constructor.apply(this, arguments); 441 | return _ref2; 442 | } 443 | 444 | return C; 445 | 446 | })(Mixen(Module6, Module3, Module1)); 447 | D = (function(_super) { 448 | __extends(D, _super); 449 | 450 | function D() { 451 | _ref3 = D.__super__.constructor.apply(this, arguments); 452 | return _ref3; 453 | } 454 | 455 | return D; 456 | 457 | })(Mixen(Module6, Module3, Module1)); 458 | E = (function(_super) { 459 | __extends(E, _super); 460 | 461 | function E() { 462 | _ref4 = E.__super__.constructor.apply(this, arguments); 463 | return _ref4; 464 | } 465 | 466 | return E; 467 | 468 | })(Mixen(Module6, Module4, Module5)); 469 | F = (function(_super) { 470 | __extends(F, _super); 471 | 472 | function F() { 473 | _ref5 = F.__super__.constructor.apply(this, arguments); 474 | return _ref5; 475 | } 476 | 477 | return F; 478 | 479 | })(Mixen(Module1, Module6, Module5)); 480 | a = new A; 481 | b = new B; 482 | d = new D; 483 | c = new C; 484 | e2 = new E; 485 | f = new F; 486 | e1 = new E; 487 | expect(a.x()).toBe('31'); 488 | expect(b.x()).toBe('532'); 489 | expect(e1.x()).toBe('65'); 490 | expect(c.x()).toBe('631'); 491 | expect(d.x()).toBe('631'); 492 | expect(e2.x()).toBe('65'); 493 | return expect(f.x()).toBe('165'); 494 | }); 495 | it('should be able to mixin mixens (constructor)', function() { 496 | var Module1, Module12, Module2, Module3, Module312, inst, order; 497 | order = ''; 498 | Module1 = (function() { 499 | function Module1() { 500 | order += '-1-'; 501 | } 502 | 503 | return Module1; 504 | 505 | })(); 506 | Module2 = (function() { 507 | function Module2() { 508 | order += '-2-'; 509 | } 510 | 511 | return Module2; 512 | 513 | })(); 514 | Module12 = (function(_super) { 515 | __extends(Module12, _super); 516 | 517 | function Module12() { 518 | Module12.__super__.constructor.apply(this, arguments); 519 | order += '-12-'; 520 | } 521 | 522 | return Module12; 523 | 524 | })(Mixen(Module1, Module2)); 525 | Module3 = (function() { 526 | function Module3() { 527 | order += '-3-'; 528 | } 529 | 530 | return Module3; 531 | 532 | })(); 533 | Module312 = (function(_super) { 534 | __extends(Module312, _super); 535 | 536 | function Module312() { 537 | Module312.__super__.constructor.apply(this, arguments); 538 | order += '-312-'; 539 | } 540 | 541 | return Module312; 542 | 543 | })(Mixen(Module3, Module12)); 544 | inst = new Module312; 545 | return expect(order).toBe('-3--1--2--12--312-'); 546 | }); 547 | it('should be able to mixin mixens (not constructor)', function() { 548 | var Module1, Module12, Module2, Module3, Module312, inst, order, _ref, _ref1; 549 | order = ''; 550 | Module1 = (function() { 551 | function Module1() {} 552 | 553 | Module1.prototype.init = function() { 554 | Module1.__super__.init.apply(this, arguments); 555 | return order += '-1-'; 556 | }; 557 | 558 | return Module1; 559 | 560 | })(); 561 | Module2 = (function() { 562 | function Module2() {} 563 | 564 | Module2.prototype.init = function() { 565 | Module2.__super__.init.apply(this, arguments); 566 | return order += '-2-'; 567 | }; 568 | 569 | return Module2; 570 | 571 | })(); 572 | Module12 = (function(_super) { 573 | __extends(Module12, _super); 574 | 575 | function Module12() { 576 | _ref = Module12.__super__.constructor.apply(this, arguments); 577 | return _ref; 578 | } 579 | 580 | Module12.prototype.init = function() { 581 | Module12.__super__.init.apply(this, arguments); 582 | return order += '-12-'; 583 | }; 584 | 585 | return Module12; 586 | 587 | })(Mixen(Module1, Module2)); 588 | Module3 = (function() { 589 | function Module3() {} 590 | 591 | Module3.prototype.init = function() { 592 | Module3.__super__.init.apply(this, arguments); 593 | return order += '-3-'; 594 | }; 595 | 596 | return Module3; 597 | 598 | })(); 599 | Module312 = (function(_super) { 600 | __extends(Module312, _super); 601 | 602 | function Module312() { 603 | _ref1 = Module312.__super__.constructor.apply(this, arguments); 604 | return _ref1; 605 | } 606 | 607 | Module312.prototype.init = function() { 608 | Module312.__super__.init.apply(this, arguments); 609 | return order += '-312-'; 610 | }; 611 | 612 | return Module312; 613 | 614 | })(Mixen(Module3, Module12)); 615 | inst = new Module312; 616 | inst.init(); 617 | return expect(order).toBe('-2--1--12--3--312-'); 618 | }); 619 | it('should be able to mixin mixens (not constructor), order flipped', function() { 620 | var Module1, Module12, Module2, Module3, Module312, inst, order, _ref, _ref1; 621 | order = ''; 622 | Module1 = (function() { 623 | function Module1() {} 624 | 625 | Module1.prototype.init = function() { 626 | order += '-1-'; 627 | return Module1.__super__.init.apply(this, arguments); 628 | }; 629 | 630 | return Module1; 631 | 632 | })(); 633 | Module2 = (function() { 634 | function Module2() {} 635 | 636 | Module2.prototype.init = function() { 637 | order += '-2-'; 638 | return Module2.__super__.init.apply(this, arguments); 639 | }; 640 | 641 | return Module2; 642 | 643 | })(); 644 | Module12 = (function(_super) { 645 | __extends(Module12, _super); 646 | 647 | function Module12() { 648 | _ref = Module12.__super__.constructor.apply(this, arguments); 649 | return _ref; 650 | } 651 | 652 | Module12.prototype.init = function() { 653 | order += '-12-'; 654 | return Module12.__super__.init.apply(this, arguments); 655 | }; 656 | 657 | return Module12; 658 | 659 | })(Mixen(Module1, Module2)); 660 | Module3 = (function() { 661 | function Module3() {} 662 | 663 | Module3.prototype.init = function() { 664 | order += '-3-'; 665 | return Module3.__super__.init.apply(this, arguments); 666 | }; 667 | 668 | return Module3; 669 | 670 | })(); 671 | Module312 = (function(_super) { 672 | __extends(Module312, _super); 673 | 674 | function Module312() { 675 | _ref1 = Module312.__super__.constructor.apply(this, arguments); 676 | return _ref1; 677 | } 678 | 679 | Module312.prototype.init = function() { 680 | order += '-312-'; 681 | return Module312.__super__.init.apply(this, arguments); 682 | }; 683 | 684 | return Module312; 685 | 686 | })(Mixen(Module3, Module12)); 687 | inst = new Module312; 688 | inst.init(); 689 | return expect(order).toBe('-312--3--12--1--2-'); 690 | }); 691 | return it('should be able to mixin mixens of mixens (not constructor)', function() { 692 | var Module1, Module12, Module2, Module3, Module312, Module4, Module4312, inst, order, _ref, _ref1, _ref2; 693 | order = ''; 694 | Module1 = (function() { 695 | function Module1() {} 696 | 697 | Module1.prototype.init = function() { 698 | Module1.__super__.init.apply(this, arguments); 699 | return order += '-1-'; 700 | }; 701 | 702 | return Module1; 703 | 704 | })(); 705 | Module2 = (function() { 706 | function Module2() {} 707 | 708 | Module2.prototype.init = function() { 709 | Module2.__super__.init.apply(this, arguments); 710 | return order += '-2-'; 711 | }; 712 | 713 | return Module2; 714 | 715 | })(); 716 | Module12 = (function(_super) { 717 | __extends(Module12, _super); 718 | 719 | function Module12() { 720 | _ref = Module12.__super__.constructor.apply(this, arguments); 721 | return _ref; 722 | } 723 | 724 | Module12.prototype.init = function() { 725 | Module12.__super__.init.apply(this, arguments); 726 | return order += '-12-'; 727 | }; 728 | 729 | return Module12; 730 | 731 | })(Mixen(Module1, Module2)); 732 | Module3 = (function() { 733 | function Module3() {} 734 | 735 | Module3.prototype.init = function() { 736 | Module3.__super__.init.apply(this, arguments); 737 | return order += '-3-'; 738 | }; 739 | 740 | return Module3; 741 | 742 | })(); 743 | Module312 = (function(_super) { 744 | __extends(Module312, _super); 745 | 746 | function Module312() { 747 | _ref1 = Module312.__super__.constructor.apply(this, arguments); 748 | return _ref1; 749 | } 750 | 751 | Module312.prototype.init = function() { 752 | Module312.__super__.init.apply(this, arguments); 753 | return order += '-312-'; 754 | }; 755 | 756 | return Module312; 757 | 758 | })(Mixen(Module3, Module12)); 759 | Module4 = (function() { 760 | function Module4() {} 761 | 762 | Module4.prototype.init = function() { 763 | Module4.__super__.init.apply(this, arguments); 764 | return order += '-4-'; 765 | }; 766 | 767 | return Module4; 768 | 769 | })(); 770 | Module4312 = (function(_super) { 771 | __extends(Module4312, _super); 772 | 773 | function Module4312() { 774 | _ref2 = Module4312.__super__.constructor.apply(this, arguments); 775 | return _ref2; 776 | } 777 | 778 | return Module4312; 779 | 780 | })(Mixen(Module4, Module312)); 781 | inst = new Module4312; 782 | inst.init(); 783 | return expect(order).toBe('-2--1--12--3--312--4-'); 784 | }); 785 | }); 786 | 787 | }).call(this); 788 | -------------------------------------------------------------------------------- /spec/vendor/backbone-1.0.0/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 1.0.0 2 | 3 | // (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. 4 | // Backbone may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://backbonejs.org 7 | 8 | (function(){ 9 | 10 | // Initial Setup 11 | // ------------- 12 | 13 | // Save a reference to the global object (`window` in the browser, `exports` 14 | // on the server). 15 | var root = this; 16 | 17 | // Save the previous value of the `Backbone` variable, so that it can be 18 | // restored later on, if `noConflict` is used. 19 | var previousBackbone = root.Backbone; 20 | 21 | // Create local references to array methods we'll want to use later. 22 | var array = []; 23 | var push = array.push; 24 | var slice = array.slice; 25 | var splice = array.splice; 26 | 27 | // The top-level namespace. All public Backbone classes and modules will 28 | // be attached to this. Exported for both the browser and the server. 29 | var Backbone; 30 | if (typeof exports !== 'undefined') { 31 | Backbone = exports; 32 | } else { 33 | Backbone = root.Backbone = {}; 34 | } 35 | 36 | // Current version of the library. Keep in sync with `package.json`. 37 | Backbone.VERSION = '1.0.0'; 38 | 39 | // Require Underscore, if we're on the server, and it's not already present. 40 | var _ = root._; 41 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); 42 | 43 | // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns 44 | // the `$` variable. 45 | Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; 46 | 47 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 48 | // to its previous owner. Returns a reference to this Backbone object. 49 | Backbone.noConflict = function() { 50 | root.Backbone = previousBackbone; 51 | return this; 52 | }; 53 | 54 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 55 | // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and 56 | // set a `X-Http-Method-Override` header. 57 | Backbone.emulateHTTP = false; 58 | 59 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 60 | // `application/json` requests ... will encode the body as 61 | // `application/x-www-form-urlencoded` instead and will send the model in a 62 | // form param named `model`. 63 | Backbone.emulateJSON = false; 64 | 65 | // Backbone.Events 66 | // --------------- 67 | 68 | // A module that can be mixed in to *any object* in order to provide it with 69 | // custom events. You may bind with `on` or remove with `off` callback 70 | // functions to an event; `trigger`-ing an event fires all callbacks in 71 | // succession. 72 | // 73 | // var object = {}; 74 | // _.extend(object, Backbone.Events); 75 | // object.on('expand', function(){ alert('expanded'); }); 76 | // object.trigger('expand'); 77 | // 78 | var Events = Backbone.Events = { 79 | 80 | // Bind an event to a `callback` function. Passing `"all"` will bind 81 | // the callback to all events fired. 82 | on: function(name, callback, context) { 83 | if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 84 | this._events || (this._events = {}); 85 | var events = this._events[name] || (this._events[name] = []); 86 | events.push({callback: callback, context: context, ctx: context || this}); 87 | return this; 88 | }, 89 | 90 | // Bind an event to only be triggered a single time. After the first time 91 | // the callback is invoked, it will be removed. 92 | once: function(name, callback, context) { 93 | if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 94 | var self = this; 95 | var once = _.once(function() { 96 | self.off(name, once); 97 | callback.apply(this, arguments); 98 | }); 99 | once._callback = callback; 100 | return this.on(name, once, context); 101 | }, 102 | 103 | // Remove one or many callbacks. If `context` is null, removes all 104 | // callbacks with that function. If `callback` is null, removes all 105 | // callbacks for the event. If `name` is null, removes all bound 106 | // callbacks for all events. 107 | off: function(name, callback, context) { 108 | var retain, ev, events, names, i, l, j, k; 109 | if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 110 | if (!name && !callback && !context) { 111 | this._events = {}; 112 | return this; 113 | } 114 | 115 | names = name ? [name] : _.keys(this._events); 116 | for (i = 0, l = names.length; i < l; i++) { 117 | name = names[i]; 118 | if (events = this._events[name]) { 119 | this._events[name] = retain = []; 120 | if (callback || context) { 121 | for (j = 0, k = events.length; j < k; j++) { 122 | ev = events[j]; 123 | if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || 124 | (context && context !== ev.context)) { 125 | retain.push(ev); 126 | } 127 | } 128 | } 129 | if (!retain.length) delete this._events[name]; 130 | } 131 | } 132 | 133 | return this; 134 | }, 135 | 136 | // Trigger one or many events, firing all bound callbacks. Callbacks are 137 | // passed the same arguments as `trigger` is, apart from the event name 138 | // (unless you're listening on `"all"`, which will cause your callback to 139 | // receive the true name of the event as the first argument). 140 | trigger: function(name) { 141 | if (!this._events) return this; 142 | var args = slice.call(arguments, 1); 143 | if (!eventsApi(this, 'trigger', name, args)) return this; 144 | var events = this._events[name]; 145 | var allEvents = this._events.all; 146 | if (events) triggerEvents(events, args); 147 | if (allEvents) triggerEvents(allEvents, arguments); 148 | return this; 149 | }, 150 | 151 | // Tell this object to stop listening to either specific events ... or 152 | // to every object it's currently listening to. 153 | stopListening: function(obj, name, callback) { 154 | var listeners = this._listeners; 155 | if (!listeners) return this; 156 | var deleteListener = !name && !callback; 157 | if (typeof name === 'object') callback = this; 158 | if (obj) (listeners = {})[obj._listenerId] = obj; 159 | for (var id in listeners) { 160 | listeners[id].off(name, callback, this); 161 | if (deleteListener) delete this._listeners[id]; 162 | } 163 | return this; 164 | } 165 | 166 | }; 167 | 168 | // Regular expression used to split event strings. 169 | var eventSplitter = /\s+/; 170 | 171 | // Implement fancy features of the Events API such as multiple event 172 | // names `"change blur"` and jQuery-style event maps `{change: action}` 173 | // in terms of the existing API. 174 | var eventsApi = function(obj, action, name, rest) { 175 | if (!name) return true; 176 | 177 | // Handle event maps. 178 | if (typeof name === 'object') { 179 | for (var key in name) { 180 | obj[action].apply(obj, [key, name[key]].concat(rest)); 181 | } 182 | return false; 183 | } 184 | 185 | // Handle space separated event names. 186 | if (eventSplitter.test(name)) { 187 | var names = name.split(eventSplitter); 188 | for (var i = 0, l = names.length; i < l; i++) { 189 | obj[action].apply(obj, [names[i]].concat(rest)); 190 | } 191 | return false; 192 | } 193 | 194 | return true; 195 | }; 196 | 197 | // A difficult-to-believe, but optimized internal dispatch function for 198 | // triggering events. Tries to keep the usual cases speedy (most internal 199 | // Backbone events have 3 arguments). 200 | var triggerEvents = function(events, args) { 201 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 202 | switch (args.length) { 203 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 204 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 205 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 206 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 207 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); 208 | } 209 | }; 210 | 211 | var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; 212 | 213 | // Inversion-of-control versions of `on` and `once`. Tell *this* object to 214 | // listen to an event in another object ... keeping track of what it's 215 | // listening to. 216 | _.each(listenMethods, function(implementation, method) { 217 | Events[method] = function(obj, name, callback) { 218 | var listeners = this._listeners || (this._listeners = {}); 219 | var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); 220 | listeners[id] = obj; 221 | if (typeof name === 'object') callback = this; 222 | obj[implementation](name, callback, this); 223 | return this; 224 | }; 225 | }); 226 | 227 | // Aliases for backwards compatibility. 228 | Events.bind = Events.on; 229 | Events.unbind = Events.off; 230 | 231 | // Allow the `Backbone` object to serve as a global event bus, for folks who 232 | // want global "pubsub" in a convenient place. 233 | _.extend(Backbone, Events); 234 | 235 | // Backbone.Model 236 | // -------------- 237 | 238 | // Backbone **Models** are the basic data object in the framework -- 239 | // frequently representing a row in a table in a database on your server. 240 | // A discrete chunk of data and a bunch of useful, related methods for 241 | // performing computations and transformations on that data. 242 | 243 | // Create a new model with the specified attributes. A client id (`cid`) 244 | // is automatically generated and assigned for you. 245 | var Model = Backbone.Model = function(attributes, options) { 246 | var defaults; 247 | var attrs = attributes || {}; 248 | options || (options = {}); 249 | this.cid = _.uniqueId('c'); 250 | this.attributes = {}; 251 | _.extend(this, _.pick(options, modelOptions)); 252 | if (options.parse) attrs = this.parse(attrs, options) || {}; 253 | if (defaults = _.result(this, 'defaults')) { 254 | attrs = _.defaults({}, attrs, defaults); 255 | } 256 | this.set(attrs, options); 257 | this.changed = {}; 258 | this.initialize.apply(this, arguments); 259 | }; 260 | 261 | // A list of options to be attached directly to the model, if provided. 262 | var modelOptions = ['url', 'urlRoot', 'collection']; 263 | 264 | // Attach all inheritable methods to the Model prototype. 265 | _.extend(Model.prototype, Events, { 266 | 267 | // A hash of attributes whose current and previous value differ. 268 | changed: null, 269 | 270 | // The value returned during the last failed validation. 271 | validationError: null, 272 | 273 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 274 | // CouchDB users may want to set this to `"_id"`. 275 | idAttribute: 'id', 276 | 277 | // Initialize is an empty function by default. Override it with your own 278 | // initialization logic. 279 | initialize: function(){}, 280 | 281 | // Return a copy of the model's `attributes` object. 282 | toJSON: function(options) { 283 | return _.clone(this.attributes); 284 | }, 285 | 286 | // Proxy `Backbone.sync` by default -- but override this if you need 287 | // custom syncing semantics for *this* particular model. 288 | sync: function() { 289 | return Backbone.sync.apply(this, arguments); 290 | }, 291 | 292 | // Get the value of an attribute. 293 | get: function(attr) { 294 | return this.attributes[attr]; 295 | }, 296 | 297 | // Get the HTML-escaped value of an attribute. 298 | escape: function(attr) { 299 | return _.escape(this.get(attr)); 300 | }, 301 | 302 | // Returns `true` if the attribute contains a value that is not null 303 | // or undefined. 304 | has: function(attr) { 305 | return this.get(attr) != null; 306 | }, 307 | 308 | // Set a hash of model attributes on the object, firing `"change"`. This is 309 | // the core primitive operation of a model, updating the data and notifying 310 | // anyone who needs to know about the change in state. The heart of the beast. 311 | set: function(key, val, options) { 312 | var attr, attrs, unset, changes, silent, changing, prev, current; 313 | if (key == null) return this; 314 | 315 | // Handle both `"key", value` and `{key: value}` -style arguments. 316 | if (typeof key === 'object') { 317 | attrs = key; 318 | options = val; 319 | } else { 320 | (attrs = {})[key] = val; 321 | } 322 | 323 | options || (options = {}); 324 | 325 | // Run validation. 326 | if (!this._validate(attrs, options)) return false; 327 | 328 | // Extract attributes and options. 329 | unset = options.unset; 330 | silent = options.silent; 331 | changes = []; 332 | changing = this._changing; 333 | this._changing = true; 334 | 335 | if (!changing) { 336 | this._previousAttributes = _.clone(this.attributes); 337 | this.changed = {}; 338 | } 339 | current = this.attributes, prev = this._previousAttributes; 340 | 341 | // Check for changes of `id`. 342 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 343 | 344 | // For each `set` attribute, update or delete the current value. 345 | for (attr in attrs) { 346 | val = attrs[attr]; 347 | if (!_.isEqual(current[attr], val)) changes.push(attr); 348 | if (!_.isEqual(prev[attr], val)) { 349 | this.changed[attr] = val; 350 | } else { 351 | delete this.changed[attr]; 352 | } 353 | unset ? delete current[attr] : current[attr] = val; 354 | } 355 | 356 | // Trigger all relevant attribute changes. 357 | if (!silent) { 358 | if (changes.length) this._pending = true; 359 | for (var i = 0, l = changes.length; i < l; i++) { 360 | this.trigger('change:' + changes[i], this, current[changes[i]], options); 361 | } 362 | } 363 | 364 | // You might be wondering why there's a `while` loop here. Changes can 365 | // be recursively nested within `"change"` events. 366 | if (changing) return this; 367 | if (!silent) { 368 | while (this._pending) { 369 | this._pending = false; 370 | this.trigger('change', this, options); 371 | } 372 | } 373 | this._pending = false; 374 | this._changing = false; 375 | return this; 376 | }, 377 | 378 | // Remove an attribute from the model, firing `"change"`. `unset` is a noop 379 | // if the attribute doesn't exist. 380 | unset: function(attr, options) { 381 | return this.set(attr, void 0, _.extend({}, options, {unset: true})); 382 | }, 383 | 384 | // Clear all attributes on the model, firing `"change"`. 385 | clear: function(options) { 386 | var attrs = {}; 387 | for (var key in this.attributes) attrs[key] = void 0; 388 | return this.set(attrs, _.extend({}, options, {unset: true})); 389 | }, 390 | 391 | // Determine if the model has changed since the last `"change"` event. 392 | // If you specify an attribute name, determine if that attribute has changed. 393 | hasChanged: function(attr) { 394 | if (attr == null) return !_.isEmpty(this.changed); 395 | return _.has(this.changed, attr); 396 | }, 397 | 398 | // Return an object containing all the attributes that have changed, or 399 | // false if there are no changed attributes. Useful for determining what 400 | // parts of a view need to be updated and/or what attributes need to be 401 | // persisted to the server. Unset attributes will be set to undefined. 402 | // You can also pass an attributes object to diff against the model, 403 | // determining if there *would be* a change. 404 | changedAttributes: function(diff) { 405 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 406 | var val, changed = false; 407 | var old = this._changing ? this._previousAttributes : this.attributes; 408 | for (var attr in diff) { 409 | if (_.isEqual(old[attr], (val = diff[attr]))) continue; 410 | (changed || (changed = {}))[attr] = val; 411 | } 412 | return changed; 413 | }, 414 | 415 | // Get the previous value of an attribute, recorded at the time the last 416 | // `"change"` event was fired. 417 | previous: function(attr) { 418 | if (attr == null || !this._previousAttributes) return null; 419 | return this._previousAttributes[attr]; 420 | }, 421 | 422 | // Get all of the attributes of the model at the time of the previous 423 | // `"change"` event. 424 | previousAttributes: function() { 425 | return _.clone(this._previousAttributes); 426 | }, 427 | 428 | // Fetch the model from the server. If the server's representation of the 429 | // model differs from its current attributes, they will be overridden, 430 | // triggering a `"change"` event. 431 | fetch: function(options) { 432 | options = options ? _.clone(options) : {}; 433 | if (options.parse === void 0) options.parse = true; 434 | var model = this; 435 | var success = options.success; 436 | options.success = function(resp) { 437 | if (!model.set(model.parse(resp, options), options)) return false; 438 | if (success) success(model, resp, options); 439 | model.trigger('sync', model, resp, options); 440 | }; 441 | wrapError(this, options); 442 | return this.sync('read', this, options); 443 | }, 444 | 445 | // Set a hash of model attributes, and sync the model to the server. 446 | // If the server returns an attributes hash that differs, the model's 447 | // state will be `set` again. 448 | save: function(key, val, options) { 449 | var attrs, method, xhr, attributes = this.attributes; 450 | 451 | // Handle both `"key", value` and `{key: value}` -style arguments. 452 | if (key == null || typeof key === 'object') { 453 | attrs = key; 454 | options = val; 455 | } else { 456 | (attrs = {})[key] = val; 457 | } 458 | 459 | // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. 460 | if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; 461 | 462 | options = _.extend({validate: true}, options); 463 | 464 | // Do not persist invalid models. 465 | if (!this._validate(attrs, options)) return false; 466 | 467 | // Set temporary attributes if `{wait: true}`. 468 | if (attrs && options.wait) { 469 | this.attributes = _.extend({}, attributes, attrs); 470 | } 471 | 472 | // After a successful server-side save, the client is (optionally) 473 | // updated with the server-side state. 474 | if (options.parse === void 0) options.parse = true; 475 | var model = this; 476 | var success = options.success; 477 | options.success = function(resp) { 478 | // Ensure attributes are restored during synchronous saves. 479 | model.attributes = attributes; 480 | var serverAttrs = model.parse(resp, options); 481 | if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); 482 | if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { 483 | return false; 484 | } 485 | if (success) success(model, resp, options); 486 | model.trigger('sync', model, resp, options); 487 | }; 488 | wrapError(this, options); 489 | 490 | method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); 491 | if (method === 'patch') options.attrs = attrs; 492 | xhr = this.sync(method, this, options); 493 | 494 | // Restore attributes. 495 | if (attrs && options.wait) this.attributes = attributes; 496 | 497 | return xhr; 498 | }, 499 | 500 | // Destroy this model on the server if it was already persisted. 501 | // Optimistically removes the model from its collection, if it has one. 502 | // If `wait: true` is passed, waits for the server to respond before removal. 503 | destroy: function(options) { 504 | options = options ? _.clone(options) : {}; 505 | var model = this; 506 | var success = options.success; 507 | 508 | var destroy = function() { 509 | model.trigger('destroy', model, model.collection, options); 510 | }; 511 | 512 | options.success = function(resp) { 513 | if (options.wait || model.isNew()) destroy(); 514 | if (success) success(model, resp, options); 515 | if (!model.isNew()) model.trigger('sync', model, resp, options); 516 | }; 517 | 518 | if (this.isNew()) { 519 | options.success(); 520 | return false; 521 | } 522 | wrapError(this, options); 523 | 524 | var xhr = this.sync('delete', this, options); 525 | if (!options.wait) destroy(); 526 | return xhr; 527 | }, 528 | 529 | // Default URL for the model's representation on the server -- if you're 530 | // using Backbone's restful methods, override this to change the endpoint 531 | // that will be called. 532 | url: function() { 533 | var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); 534 | if (this.isNew()) return base; 535 | return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); 536 | }, 537 | 538 | // **parse** converts a response into the hash of attributes to be `set` on 539 | // the model. The default implementation is just to pass the response along. 540 | parse: function(resp, options) { 541 | return resp; 542 | }, 543 | 544 | // Create a new model with identical attributes to this one. 545 | clone: function() { 546 | return new this.constructor(this.attributes); 547 | }, 548 | 549 | // A model is new if it has never been saved to the server, and lacks an id. 550 | isNew: function() { 551 | return this.id == null; 552 | }, 553 | 554 | // Check if the model is currently in a valid state. 555 | isValid: function(options) { 556 | return this._validate({}, _.extend(options || {}, { validate: true })); 557 | }, 558 | 559 | // Run validation against the next complete set of model attributes, 560 | // returning `true` if all is well. Otherwise, fire an `"invalid"` event. 561 | _validate: function(attrs, options) { 562 | if (!options.validate || !this.validate) return true; 563 | attrs = _.extend({}, this.attributes, attrs); 564 | var error = this.validationError = this.validate(attrs, options) || null; 565 | if (!error) return true; 566 | this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); 567 | return false; 568 | } 569 | 570 | }); 571 | 572 | // Underscore methods that we want to implement on the Model. 573 | var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; 574 | 575 | // Mix in each Underscore method as a proxy to `Model#attributes`. 576 | _.each(modelMethods, function(method) { 577 | Model.prototype[method] = function() { 578 | var args = slice.call(arguments); 579 | args.unshift(this.attributes); 580 | return _[method].apply(_, args); 581 | }; 582 | }); 583 | 584 | // Backbone.Collection 585 | // ------------------- 586 | 587 | // If models tend to represent a single row of data, a Backbone Collection is 588 | // more analagous to a table full of data ... or a small slice or page of that 589 | // table, or a collection of rows that belong together for a particular reason 590 | // -- all of the messages in this particular folder, all of the documents 591 | // belonging to this particular author, and so on. Collections maintain 592 | // indexes of their models, both in order, and for lookup by `id`. 593 | 594 | // Create a new **Collection**, perhaps to contain a specific type of `model`. 595 | // If a `comparator` is specified, the Collection will maintain 596 | // its models in sort order, as they're added and removed. 597 | var Collection = Backbone.Collection = function(models, options) { 598 | options || (options = {}); 599 | if (options.url) this.url = options.url; 600 | if (options.model) this.model = options.model; 601 | if (options.comparator !== void 0) this.comparator = options.comparator; 602 | this._reset(); 603 | this.initialize.apply(this, arguments); 604 | if (models) this.reset(models, _.extend({silent: true}, options)); 605 | }; 606 | 607 | // Default options for `Collection#set`. 608 | var setOptions = {add: true, remove: true, merge: true}; 609 | var addOptions = {add: true, merge: false, remove: false}; 610 | 611 | // Define the Collection's inheritable methods. 612 | _.extend(Collection.prototype, Events, { 613 | 614 | // The default model for a collection is just a **Backbone.Model**. 615 | // This should be overridden in most cases. 616 | model: Model, 617 | 618 | // Initialize is an empty function by default. Override it with your own 619 | // initialization logic. 620 | initialize: function(){}, 621 | 622 | // The JSON representation of a Collection is an array of the 623 | // models' attributes. 624 | toJSON: function(options) { 625 | return this.map(function(model){ return model.toJSON(options); }); 626 | }, 627 | 628 | // Proxy `Backbone.sync` by default. 629 | sync: function() { 630 | return Backbone.sync.apply(this, arguments); 631 | }, 632 | 633 | // Add a model, or list of models to the set. 634 | add: function(models, options) { 635 | return this.set(models, _.defaults(options || {}, addOptions)); 636 | }, 637 | 638 | // Remove a model, or a list of models from the set. 639 | remove: function(models, options) { 640 | models = _.isArray(models) ? models.slice() : [models]; 641 | options || (options = {}); 642 | var i, l, index, model; 643 | for (i = 0, l = models.length; i < l; i++) { 644 | model = this.get(models[i]); 645 | if (!model) continue; 646 | delete this._byId[model.id]; 647 | delete this._byId[model.cid]; 648 | index = this.indexOf(model); 649 | this.models.splice(index, 1); 650 | this.length--; 651 | if (!options.silent) { 652 | options.index = index; 653 | model.trigger('remove', model, this, options); 654 | } 655 | this._removeReference(model); 656 | } 657 | return this; 658 | }, 659 | 660 | // Update a collection by `set`-ing a new list of models, adding new ones, 661 | // removing models that are no longer present, and merging models that 662 | // already exist in the collection, as necessary. Similar to **Model#set**, 663 | // the core operation for updating the data contained by the collection. 664 | set: function(models, options) { 665 | options = _.defaults(options || {}, setOptions); 666 | if (options.parse) models = this.parse(models, options); 667 | if (!_.isArray(models)) models = models ? [models] : []; 668 | var i, l, model, attrs, existing, sort; 669 | var at = options.at; 670 | var sortable = this.comparator && (at == null) && options.sort !== false; 671 | var sortAttr = _.isString(this.comparator) ? this.comparator : null; 672 | var toAdd = [], toRemove = [], modelMap = {}; 673 | 674 | // Turn bare objects into model references, and prevent invalid models 675 | // from being added. 676 | for (i = 0, l = models.length; i < l; i++) { 677 | if (!(model = this._prepareModel(models[i], options))) continue; 678 | 679 | // If a duplicate is found, prevent it from being added and 680 | // optionally merge it into the existing model. 681 | if (existing = this.get(model)) { 682 | if (options.remove) modelMap[existing.cid] = true; 683 | if (options.merge) { 684 | existing.set(model.attributes, options); 685 | if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; 686 | } 687 | 688 | // This is a new model, push it to the `toAdd` list. 689 | } else if (options.add) { 690 | toAdd.push(model); 691 | 692 | // Listen to added models' events, and index models for lookup by 693 | // `id` and by `cid`. 694 | model.on('all', this._onModelEvent, this); 695 | this._byId[model.cid] = model; 696 | if (model.id != null) this._byId[model.id] = model; 697 | } 698 | } 699 | 700 | // Remove nonexistent models if appropriate. 701 | if (options.remove) { 702 | for (i = 0, l = this.length; i < l; ++i) { 703 | if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); 704 | } 705 | if (toRemove.length) this.remove(toRemove, options); 706 | } 707 | 708 | // See if sorting is needed, update `length` and splice in new models. 709 | if (toAdd.length) { 710 | if (sortable) sort = true; 711 | this.length += toAdd.length; 712 | if (at != null) { 713 | splice.apply(this.models, [at, 0].concat(toAdd)); 714 | } else { 715 | push.apply(this.models, toAdd); 716 | } 717 | } 718 | 719 | // Silently sort the collection if appropriate. 720 | if (sort) this.sort({silent: true}); 721 | 722 | if (options.silent) return this; 723 | 724 | // Trigger `add` events. 725 | for (i = 0, l = toAdd.length; i < l; i++) { 726 | (model = toAdd[i]).trigger('add', model, this, options); 727 | } 728 | 729 | // Trigger `sort` if the collection was sorted. 730 | if (sort) this.trigger('sort', this, options); 731 | return this; 732 | }, 733 | 734 | // When you have more items than you want to add or remove individually, 735 | // you can reset the entire set with a new list of models, without firing 736 | // any granular `add` or `remove` events. Fires `reset` when finished. 737 | // Useful for bulk operations and optimizations. 738 | reset: function(models, options) { 739 | options || (options = {}); 740 | for (var i = 0, l = this.models.length; i < l; i++) { 741 | this._removeReference(this.models[i]); 742 | } 743 | options.previousModels = this.models; 744 | this._reset(); 745 | this.add(models, _.extend({silent: true}, options)); 746 | if (!options.silent) this.trigger('reset', this, options); 747 | return this; 748 | }, 749 | 750 | // Add a model to the end of the collection. 751 | push: function(model, options) { 752 | model = this._prepareModel(model, options); 753 | this.add(model, _.extend({at: this.length}, options)); 754 | return model; 755 | }, 756 | 757 | // Remove a model from the end of the collection. 758 | pop: function(options) { 759 | var model = this.at(this.length - 1); 760 | this.remove(model, options); 761 | return model; 762 | }, 763 | 764 | // Add a model to the beginning of the collection. 765 | unshift: function(model, options) { 766 | model = this._prepareModel(model, options); 767 | this.add(model, _.extend({at: 0}, options)); 768 | return model; 769 | }, 770 | 771 | // Remove a model from the beginning of the collection. 772 | shift: function(options) { 773 | var model = this.at(0); 774 | this.remove(model, options); 775 | return model; 776 | }, 777 | 778 | // Slice out a sub-array of models from the collection. 779 | slice: function(begin, end) { 780 | return this.models.slice(begin, end); 781 | }, 782 | 783 | // Get a model from the set by id. 784 | get: function(obj) { 785 | if (obj == null) return void 0; 786 | return this._byId[obj.id != null ? obj.id : obj.cid || obj]; 787 | }, 788 | 789 | // Get the model at the given index. 790 | at: function(index) { 791 | return this.models[index]; 792 | }, 793 | 794 | // Return models with matching attributes. Useful for simple cases of 795 | // `filter`. 796 | where: function(attrs, first) { 797 | if (_.isEmpty(attrs)) return first ? void 0 : []; 798 | return this[first ? 'find' : 'filter'](function(model) { 799 | for (var key in attrs) { 800 | if (attrs[key] !== model.get(key)) return false; 801 | } 802 | return true; 803 | }); 804 | }, 805 | 806 | // Return the first model with matching attributes. Useful for simple cases 807 | // of `find`. 808 | findWhere: function(attrs) { 809 | return this.where(attrs, true); 810 | }, 811 | 812 | // Force the collection to re-sort itself. You don't need to call this under 813 | // normal circumstances, as the set will maintain sort order as each item 814 | // is added. 815 | sort: function(options) { 816 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 817 | options || (options = {}); 818 | 819 | // Run sort based on type of `comparator`. 820 | if (_.isString(this.comparator) || this.comparator.length === 1) { 821 | this.models = this.sortBy(this.comparator, this); 822 | } else { 823 | this.models.sort(_.bind(this.comparator, this)); 824 | } 825 | 826 | if (!options.silent) this.trigger('sort', this, options); 827 | return this; 828 | }, 829 | 830 | // Figure out the smallest index at which a model should be inserted so as 831 | // to maintain order. 832 | sortedIndex: function(model, value, context) { 833 | value || (value = this.comparator); 834 | var iterator = _.isFunction(value) ? value : function(model) { 835 | return model.get(value); 836 | }; 837 | return _.sortedIndex(this.models, model, iterator, context); 838 | }, 839 | 840 | // Pluck an attribute from each model in the collection. 841 | pluck: function(attr) { 842 | return _.invoke(this.models, 'get', attr); 843 | }, 844 | 845 | // Fetch the default set of models for this collection, resetting the 846 | // collection when they arrive. If `reset: true` is passed, the response 847 | // data will be passed through the `reset` method instead of `set`. 848 | fetch: function(options) { 849 | options = options ? _.clone(options) : {}; 850 | if (options.parse === void 0) options.parse = true; 851 | var success = options.success; 852 | var collection = this; 853 | options.success = function(resp) { 854 | var method = options.reset ? 'reset' : 'set'; 855 | collection[method](resp, options); 856 | if (success) success(collection, resp, options); 857 | collection.trigger('sync', collection, resp, options); 858 | }; 859 | wrapError(this, options); 860 | return this.sync('read', this, options); 861 | }, 862 | 863 | // Create a new instance of a model in this collection. Add the model to the 864 | // collection immediately, unless `wait: true` is passed, in which case we 865 | // wait for the server to agree. 866 | create: function(model, options) { 867 | options = options ? _.clone(options) : {}; 868 | if (!(model = this._prepareModel(model, options))) return false; 869 | if (!options.wait) this.add(model, options); 870 | var collection = this; 871 | var success = options.success; 872 | options.success = function(resp) { 873 | if (options.wait) collection.add(model, options); 874 | if (success) success(model, resp, options); 875 | }; 876 | model.save(null, options); 877 | return model; 878 | }, 879 | 880 | // **parse** converts a response into a list of models to be added to the 881 | // collection. The default implementation is just to pass it through. 882 | parse: function(resp, options) { 883 | return resp; 884 | }, 885 | 886 | // Create a new collection with an identical list of models as this one. 887 | clone: function() { 888 | return new this.constructor(this.models); 889 | }, 890 | 891 | // Private method to reset all internal state. Called when the collection 892 | // is first initialized or reset. 893 | _reset: function() { 894 | this.length = 0; 895 | this.models = []; 896 | this._byId = {}; 897 | }, 898 | 899 | // Prepare a hash of attributes (or other model) to be added to this 900 | // collection. 901 | _prepareModel: function(attrs, options) { 902 | if (attrs instanceof Model) { 903 | if (!attrs.collection) attrs.collection = this; 904 | return attrs; 905 | } 906 | options || (options = {}); 907 | options.collection = this; 908 | var model = new this.model(attrs, options); 909 | if (!model._validate(attrs, options)) { 910 | this.trigger('invalid', this, attrs, options); 911 | return false; 912 | } 913 | return model; 914 | }, 915 | 916 | // Internal method to sever a model's ties to a collection. 917 | _removeReference: function(model) { 918 | if (this === model.collection) delete model.collection; 919 | model.off('all', this._onModelEvent, this); 920 | }, 921 | 922 | // Internal method called every time a model in the set fires an event. 923 | // Sets need to update their indexes when models change ids. All other 924 | // events simply proxy through. "add" and "remove" events that originate 925 | // in other collections are ignored. 926 | _onModelEvent: function(event, model, collection, options) { 927 | if ((event === 'add' || event === 'remove') && collection !== this) return; 928 | if (event === 'destroy') this.remove(model, options); 929 | if (model && event === 'change:' + model.idAttribute) { 930 | delete this._byId[model.previous(model.idAttribute)]; 931 | if (model.id != null) this._byId[model.id] = model; 932 | } 933 | this.trigger.apply(this, arguments); 934 | } 935 | 936 | }); 937 | 938 | // Underscore methods that we want to implement on the Collection. 939 | // 90% of the core usefulness of Backbone Collections is actually implemented 940 | // right here: 941 | var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 942 | 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 943 | 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 944 | 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 945 | 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 946 | 'isEmpty', 'chain']; 947 | 948 | // Mix in each Underscore method as a proxy to `Collection#models`. 949 | _.each(methods, function(method) { 950 | Collection.prototype[method] = function() { 951 | var args = slice.call(arguments); 952 | args.unshift(this.models); 953 | return _[method].apply(_, args); 954 | }; 955 | }); 956 | 957 | // Underscore methods that take a property name as an argument. 958 | var attributeMethods = ['groupBy', 'countBy', 'sortBy']; 959 | 960 | // Use attributes instead of properties. 961 | _.each(attributeMethods, function(method) { 962 | Collection.prototype[method] = function(value, context) { 963 | var iterator = _.isFunction(value) ? value : function(model) { 964 | return model.get(value); 965 | }; 966 | return _[method](this.models, iterator, context); 967 | }; 968 | }); 969 | 970 | // Backbone.View 971 | // ------------- 972 | 973 | // Backbone Views are almost more convention than they are actual code. A View 974 | // is simply a JavaScript object that represents a logical chunk of UI in the 975 | // DOM. This might be a single item, an entire list, a sidebar or panel, or 976 | // even the surrounding frame which wraps your whole app. Defining a chunk of 977 | // UI as a **View** allows you to define your DOM events declaratively, without 978 | // having to worry about render order ... and makes it easy for the view to 979 | // react to specific changes in the state of your models. 980 | 981 | // Creating a Backbone.View creates its initial element outside of the DOM, 982 | // if an existing element is not provided... 983 | var View = Backbone.View = function(options) { 984 | this.cid = _.uniqueId('view'); 985 | this._configure(options || {}); 986 | this._ensureElement(); 987 | this.initialize.apply(this, arguments); 988 | this.delegateEvents(); 989 | }; 990 | 991 | // Cached regex to split keys for `delegate`. 992 | var delegateEventSplitter = /^(\S+)\s*(.*)$/; 993 | 994 | // List of view options to be merged as properties. 995 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; 996 | 997 | // Set up all inheritable **Backbone.View** properties and methods. 998 | _.extend(View.prototype, Events, { 999 | 1000 | // The default `tagName` of a View's element is `"div"`. 1001 | tagName: 'div', 1002 | 1003 | // jQuery delegate for element lookup, scoped to DOM elements within the 1004 | // current view. This should be prefered to global lookups where possible. 1005 | $: function(selector) { 1006 | return this.$el.find(selector); 1007 | }, 1008 | 1009 | // Initialize is an empty function by default. Override it with your own 1010 | // initialization logic. 1011 | initialize: function(){}, 1012 | 1013 | // **render** is the core function that your view should override, in order 1014 | // to populate its element (`this.el`), with the appropriate HTML. The 1015 | // convention is for **render** to always return `this`. 1016 | render: function() { 1017 | return this; 1018 | }, 1019 | 1020 | // Remove this view by taking the element out of the DOM, and removing any 1021 | // applicable Backbone.Events listeners. 1022 | remove: function() { 1023 | this.$el.remove(); 1024 | this.stopListening(); 1025 | return this; 1026 | }, 1027 | 1028 | // Change the view's element (`this.el` property), including event 1029 | // re-delegation. 1030 | setElement: function(element, delegate) { 1031 | if (this.$el) this.undelegateEvents(); 1032 | this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); 1033 | this.el = this.$el[0]; 1034 | if (delegate !== false) this.delegateEvents(); 1035 | return this; 1036 | }, 1037 | 1038 | // Set callbacks, where `this.events` is a hash of 1039 | // 1040 | // *{"event selector": "callback"}* 1041 | // 1042 | // { 1043 | // 'mousedown .title': 'edit', 1044 | // 'click .button': 'save' 1045 | // 'click .open': function(e) { ... } 1046 | // } 1047 | // 1048 | // pairs. Callbacks will be bound to the view, with `this` set properly. 1049 | // Uses event delegation for efficiency. 1050 | // Omitting the selector binds the event to `this.el`. 1051 | // This only works for delegate-able events: not `focus`, `blur`, and 1052 | // not `change`, `submit`, and `reset` in Internet Explorer. 1053 | delegateEvents: function(events) { 1054 | if (!(events || (events = _.result(this, 'events')))) return this; 1055 | this.undelegateEvents(); 1056 | for (var key in events) { 1057 | var method = events[key]; 1058 | if (!_.isFunction(method)) method = this[events[key]]; 1059 | if (!method) continue; 1060 | 1061 | var match = key.match(delegateEventSplitter); 1062 | var eventName = match[1], selector = match[2]; 1063 | method = _.bind(method, this); 1064 | eventName += '.delegateEvents' + this.cid; 1065 | if (selector === '') { 1066 | this.$el.on(eventName, method); 1067 | } else { 1068 | this.$el.on(eventName, selector, method); 1069 | } 1070 | } 1071 | return this; 1072 | }, 1073 | 1074 | // Clears all callbacks previously bound to the view with `delegateEvents`. 1075 | // You usually don't need to use this, but may wish to if you have multiple 1076 | // Backbone views attached to the same DOM element. 1077 | undelegateEvents: function() { 1078 | this.$el.off('.delegateEvents' + this.cid); 1079 | return this; 1080 | }, 1081 | 1082 | // Performs the initial configuration of a View with a set of options. 1083 | // Keys with special meaning *(e.g. model, collection, id, className)* are 1084 | // attached directly to the view. See `viewOptions` for an exhaustive 1085 | // list. 1086 | _configure: function(options) { 1087 | if (this.options) options = _.extend({}, _.result(this, 'options'), options); 1088 | _.extend(this, _.pick(options, viewOptions)); 1089 | this.options = options; 1090 | }, 1091 | 1092 | // Ensure that the View has a DOM element to render into. 1093 | // If `this.el` is a string, pass it through `$()`, take the first 1094 | // matching element, and re-assign it to `el`. Otherwise, create 1095 | // an element from the `id`, `className` and `tagName` properties. 1096 | _ensureElement: function() { 1097 | if (!this.el) { 1098 | var attrs = _.extend({}, _.result(this, 'attributes')); 1099 | if (this.id) attrs.id = _.result(this, 'id'); 1100 | if (this.className) attrs['class'] = _.result(this, 'className'); 1101 | var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); 1102 | this.setElement($el, false); 1103 | } else { 1104 | this.setElement(_.result(this, 'el'), false); 1105 | } 1106 | } 1107 | 1108 | }); 1109 | 1110 | // Backbone.sync 1111 | // ------------- 1112 | 1113 | // Override this function to change the manner in which Backbone persists 1114 | // models to the server. You will be passed the type of request, and the 1115 | // model in question. By default, makes a RESTful Ajax request 1116 | // to the model's `url()`. Some possible customizations could be: 1117 | // 1118 | // * Use `setTimeout` to batch rapid-fire updates into a single request. 1119 | // * Send up the models as XML instead of JSON. 1120 | // * Persist models via WebSockets instead of Ajax. 1121 | // 1122 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests 1123 | // as `POST`, with a `_method` parameter containing the true HTTP method, 1124 | // as well as all requests with the body as `application/x-www-form-urlencoded` 1125 | // instead of `application/json` with the model in a param named `model`. 1126 | // Useful when interfacing with server-side languages like **PHP** that make 1127 | // it difficult to read the body of `PUT` requests. 1128 | Backbone.sync = function(method, model, options) { 1129 | var type = methodMap[method]; 1130 | 1131 | // Default options, unless specified. 1132 | _.defaults(options || (options = {}), { 1133 | emulateHTTP: Backbone.emulateHTTP, 1134 | emulateJSON: Backbone.emulateJSON 1135 | }); 1136 | 1137 | // Default JSON-request options. 1138 | var params = {type: type, dataType: 'json'}; 1139 | 1140 | // Ensure that we have a URL. 1141 | if (!options.url) { 1142 | params.url = _.result(model, 'url') || urlError(); 1143 | } 1144 | 1145 | // Ensure that we have the appropriate request data. 1146 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { 1147 | params.contentType = 'application/json'; 1148 | params.data = JSON.stringify(options.attrs || model.toJSON(options)); 1149 | } 1150 | 1151 | // For older servers, emulate JSON by encoding the request into an HTML-form. 1152 | if (options.emulateJSON) { 1153 | params.contentType = 'application/x-www-form-urlencoded'; 1154 | params.data = params.data ? {model: params.data} : {}; 1155 | } 1156 | 1157 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method` 1158 | // And an `X-HTTP-Method-Override` header. 1159 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { 1160 | params.type = 'POST'; 1161 | if (options.emulateJSON) params.data._method = type; 1162 | var beforeSend = options.beforeSend; 1163 | options.beforeSend = function(xhr) { 1164 | xhr.setRequestHeader('X-HTTP-Method-Override', type); 1165 | if (beforeSend) return beforeSend.apply(this, arguments); 1166 | }; 1167 | } 1168 | 1169 | // Don't process data on a non-GET request. 1170 | if (params.type !== 'GET' && !options.emulateJSON) { 1171 | params.processData = false; 1172 | } 1173 | 1174 | // If we're sending a `PATCH` request, and we're in an old Internet Explorer 1175 | // that still has ActiveX enabled by default, override jQuery to use that 1176 | // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. 1177 | if (params.type === 'PATCH' && window.ActiveXObject && 1178 | !(window.external && window.external.msActiveXFilteringEnabled)) { 1179 | params.xhr = function() { 1180 | return new ActiveXObject("Microsoft.XMLHTTP"); 1181 | }; 1182 | } 1183 | 1184 | // Make the request, allowing the user to override any Ajax options. 1185 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); 1186 | model.trigger('request', model, xhr, options); 1187 | return xhr; 1188 | }; 1189 | 1190 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation. 1191 | var methodMap = { 1192 | 'create': 'POST', 1193 | 'update': 'PUT', 1194 | 'patch': 'PATCH', 1195 | 'delete': 'DELETE', 1196 | 'read': 'GET' 1197 | }; 1198 | 1199 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`. 1200 | // Override this if you'd like to use a different library. 1201 | Backbone.ajax = function() { 1202 | return Backbone.$.ajax.apply(Backbone.$, arguments); 1203 | }; 1204 | 1205 | // Backbone.Router 1206 | // --------------- 1207 | 1208 | // Routers map faux-URLs to actions, and fire events when routes are 1209 | // matched. Creating a new one sets its `routes` hash, if not set statically. 1210 | var Router = Backbone.Router = function(options) { 1211 | options || (options = {}); 1212 | if (options.routes) this.routes = options.routes; 1213 | this._bindRoutes(); 1214 | this.initialize.apply(this, arguments); 1215 | }; 1216 | 1217 | // Cached regular expressions for matching named param parts and splatted 1218 | // parts of route strings. 1219 | var optionalParam = /\((.*?)\)/g; 1220 | var namedParam = /(\(\?)?:\w+/g; 1221 | var splatParam = /\*\w+/g; 1222 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 1223 | 1224 | // Set up all inheritable **Backbone.Router** properties and methods. 1225 | _.extend(Router.prototype, Events, { 1226 | 1227 | // Initialize is an empty function by default. Override it with your own 1228 | // initialization logic. 1229 | initialize: function(){}, 1230 | 1231 | // Manually bind a single named route to a callback. For example: 1232 | // 1233 | // this.route('search/:query/p:num', 'search', function(query, num) { 1234 | // ... 1235 | // }); 1236 | // 1237 | route: function(route, name, callback) { 1238 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 1239 | if (_.isFunction(name)) { 1240 | callback = name; 1241 | name = ''; 1242 | } 1243 | if (!callback) callback = this[name]; 1244 | var router = this; 1245 | Backbone.history.route(route, function(fragment) { 1246 | var args = router._extractParameters(route, fragment); 1247 | callback && callback.apply(router, args); 1248 | router.trigger.apply(router, ['route:' + name].concat(args)); 1249 | router.trigger('route', name, args); 1250 | Backbone.history.trigger('route', router, name, args); 1251 | }); 1252 | return this; 1253 | }, 1254 | 1255 | // Simple proxy to `Backbone.history` to save a fragment into the history. 1256 | navigate: function(fragment, options) { 1257 | Backbone.history.navigate(fragment, options); 1258 | return this; 1259 | }, 1260 | 1261 | // Bind all defined routes to `Backbone.history`. We have to reverse the 1262 | // order of the routes here to support behavior where the most general 1263 | // routes can be defined at the bottom of the route map. 1264 | _bindRoutes: function() { 1265 | if (!this.routes) return; 1266 | this.routes = _.result(this, 'routes'); 1267 | var route, routes = _.keys(this.routes); 1268 | while ((route = routes.pop()) != null) { 1269 | this.route(route, this.routes[route]); 1270 | } 1271 | }, 1272 | 1273 | // Convert a route string into a regular expression, suitable for matching 1274 | // against the current location hash. 1275 | _routeToRegExp: function(route) { 1276 | route = route.replace(escapeRegExp, '\\$&') 1277 | .replace(optionalParam, '(?:$1)?') 1278 | .replace(namedParam, function(match, optional){ 1279 | return optional ? match : '([^\/]+)'; 1280 | }) 1281 | .replace(splatParam, '(.*?)'); 1282 | return new RegExp('^' + route + '$'); 1283 | }, 1284 | 1285 | // Given a route, and a URL fragment that it matches, return the array of 1286 | // extracted decoded parameters. Empty or unmatched parameters will be 1287 | // treated as `null` to normalize cross-browser behavior. 1288 | _extractParameters: function(route, fragment) { 1289 | var params = route.exec(fragment).slice(1); 1290 | return _.map(params, function(param) { 1291 | return param ? decodeURIComponent(param) : null; 1292 | }); 1293 | } 1294 | 1295 | }); 1296 | 1297 | // Backbone.History 1298 | // ---------------- 1299 | 1300 | // Handles cross-browser history management, based on either 1301 | // [pushState](http://diveintohtml5.info/history.html) and real URLs, or 1302 | // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) 1303 | // and URL fragments. If the browser supports neither (old IE, natch), 1304 | // falls back to polling. 1305 | var History = Backbone.History = function() { 1306 | this.handlers = []; 1307 | _.bindAll(this, 'checkUrl'); 1308 | 1309 | // Ensure that `History` can be used outside of the browser. 1310 | if (typeof window !== 'undefined') { 1311 | this.location = window.location; 1312 | this.history = window.history; 1313 | } 1314 | }; 1315 | 1316 | // Cached regex for stripping a leading hash/slash and trailing space. 1317 | var routeStripper = /^[#\/]|\s+$/g; 1318 | 1319 | // Cached regex for stripping leading and trailing slashes. 1320 | var rootStripper = /^\/+|\/+$/g; 1321 | 1322 | // Cached regex for detecting MSIE. 1323 | var isExplorer = /msie [\w.]+/; 1324 | 1325 | // Cached regex for removing a trailing slash. 1326 | var trailingSlash = /\/$/; 1327 | 1328 | // Has the history handling already been started? 1329 | History.started = false; 1330 | 1331 | // Set up all inheritable **Backbone.History** properties and methods. 1332 | _.extend(History.prototype, Events, { 1333 | 1334 | // The default interval to poll for hash changes, if necessary, is 1335 | // twenty times a second. 1336 | interval: 50, 1337 | 1338 | // Gets the true hash value. Cannot use location.hash directly due to bug 1339 | // in Firefox where location.hash will always be decoded. 1340 | getHash: function(window) { 1341 | var match = (window || this).location.href.match(/#(.*)$/); 1342 | return match ? match[1] : ''; 1343 | }, 1344 | 1345 | // Get the cross-browser normalized URL fragment, either from the URL, 1346 | // the hash, or the override. 1347 | getFragment: function(fragment, forcePushState) { 1348 | if (fragment == null) { 1349 | if (this._hasPushState || !this._wantsHashChange || forcePushState) { 1350 | fragment = this.location.pathname; 1351 | var root = this.root.replace(trailingSlash, ''); 1352 | if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); 1353 | } else { 1354 | fragment = this.getHash(); 1355 | } 1356 | } 1357 | return fragment.replace(routeStripper, ''); 1358 | }, 1359 | 1360 | // Start the hash change handling, returning `true` if the current URL matches 1361 | // an existing route, and `false` otherwise. 1362 | start: function(options) { 1363 | if (History.started) throw new Error("Backbone.history has already been started"); 1364 | History.started = true; 1365 | 1366 | // Figure out the initial configuration. Do we need an iframe? 1367 | // Is pushState desired ... is it available? 1368 | this.options = _.extend({}, {root: '/'}, this.options, options); 1369 | this.root = this.options.root; 1370 | this._wantsHashChange = this.options.hashChange !== false; 1371 | this._wantsPushState = !!this.options.pushState; 1372 | this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); 1373 | var fragment = this.getFragment(); 1374 | var docMode = document.documentMode; 1375 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 1376 | 1377 | // Normalize root to always include a leading and trailing slash. 1378 | this.root = ('/' + this.root + '/').replace(rootStripper, '/'); 1379 | 1380 | if (oldIE && this._wantsHashChange) { 1381 | this.iframe = Backbone.$('