├── dist └── 0.1.0 │ ├── backbone.trackit.min.js.gz │ ├── backbone.trackit.min.js │ ├── backbone.trackit.js │ └── README.md ├── .gitignore ├── package.json ├── test ├── index.html ├── test.html ├── vendor │ ├── zepto-data.js │ ├── qunit.css │ ├── underscore.js │ ├── qunit.js │ ├── zepto.js │ └── backbone.js └── suite.js ├── Gruntfile.js ├── backbone.trackit.js └── README.md /dist/0.1.0/backbone.trackit.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podio/backbone.trackit/master/dist/0.1.0/backbone.trackit.min.js.gz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs 3 | gh-pages 4 | *.diff 5 | *.patch 6 | *.bak 7 | *.log 8 | *.swp 9 | .DS_Store 10 | .project 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.trackit", 3 | "description": "", 4 | "version": "0.1.1", 5 | "author": "Matthew DeLambo ", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/nytimes/backbone.trackit" 9 | }, 10 | "main": "backbone.trackit.js", 11 | "devDependencies": { 12 | "grunt": "~0.4.1", 13 | "grunt-contrib-concat": "~0.3.0", 14 | "grunt-contrib-jshint": "~0.6.2", 15 | "grunt-contrib-uglify": "~0.2.2", 16 | "grunt-contrib-clean": "~0.5.0", 17 | "grunt-contrib-compress": "~0.5.2", 18 | "grunt-contrib-copy": "~0.4.1" 19 | }, 20 | "bugs": { 21 | "url": "http://github.com/nytimes/backbone.trackit/issues" 22 | }, 23 | "licenses": [ 24 | { 25 | "type": "MIT", 26 | "url": "https://github.com/nytimes/backbone.trackit/blob/master/LICENSE" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | backbone.trackit suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

backbone.trackit Test Suite

20 |

21 |

22 |
    23 | 24 |
    25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 |
    19 | 20 | 21 | Route 22 | Has changes! 23 |
    24 |
    25 | 26 | 27 | 66 | 67 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | var js, min, version, path, license, nastyFiles = {}; 4 | 5 | grunt.loadNpmTasks('grunt-contrib-concat'); 6 | grunt.loadNpmTasks('grunt-contrib-jshint'); 7 | grunt.loadNpmTasks('grunt-contrib-uglify'); 8 | grunt.loadNpmTasks('grunt-contrib-compress'); 9 | 10 | 11 | // USAGE: 12 | // 13 | // grunt build --target=0.1.0 14 | // 15 | // Where "0.1.0" is the directory name in /dist that 16 | // files will be built to. If no "--target" is specified 17 | // then files will be built to dist/Master. 18 | grunt.registerTask('build', ['jshint', 'uglify:nasty', 'concat', 'compress:gz']); 19 | 20 | 21 | js = 'backbone.trackit.js'; 22 | min = 'backbone.trackit.min.js'; 23 | version = grunt.option('target') || 'Master'; 24 | path = 'dist/' + version + '/'; 25 | nastyFiles[path + min] = [js]; 26 | license = '//\n' + 27 | '// backbone.trackit - '+version+'\n' + 28 | '// The MIT License\n' + 29 | '// Copyright (c) 2013 The New York Times, CMS Group, Matthew DeLambo \n' + 30 | '//\n'; 31 | 32 | grunt.initConfig({ 33 | 34 | jshint: { 35 | src: [js], 36 | options: { 37 | browser: true, 38 | indent: 2, 39 | white: false, 40 | evil: true, 41 | regexdash: true, 42 | wsh: true, 43 | trailing: true, 44 | eqnull: true, 45 | expr: true, 46 | boss: true, 47 | node: true 48 | } 49 | }, 50 | 51 | concat: { 52 | options: { 53 | stripBanners: true, 54 | banner: license 55 | }, 56 | dist: { 57 | src: js, 58 | dest: path + js 59 | } 60 | }, 61 | 62 | uglify: { 63 | options: { 64 | banner: license 65 | }, 66 | nasty: { 67 | options: { 68 | preserveComments: false 69 | }, 70 | files: nastyFiles 71 | } 72 | }, 73 | 74 | compress: { 75 | gz: { 76 | options: { 77 | mode: 'gzip' 78 | }, 79 | expand: true, 80 | src: [path + min] 81 | } 82 | } 83 | }); 84 | 85 | }; 86 | -------------------------------------------------------------------------------- /test/vendor/zepto-data.js: -------------------------------------------------------------------------------- 1 | // Zepto.js 2 | // (c) 2010-2012 Thomas Fuchs 3 | // Zepto.js may be freely distributed under the MIT license. 4 | 5 | // The following code is heavily inspired by jQuery's $.fn.data(); 6 | (function($) { 7 | var data = {}, 8 | dataAttr = $.fn.data, 9 | camelize = $.zepto.camelize, 10 | exp = $.expando = 'Zepto' + (+new Date()) 11 | 12 | // Get value from node: 13 | // 1. first try key as given, 14 | // 2. then try camelized key, 15 | // 3. fall back to reading "data-*" attribute. 16 | function getData(node, name) { 17 | var id = node[exp], 18 | store = id && data[id] 19 | if (name === undefined) return store || setData(node) 20 | else { 21 | if (store) { 22 | if (name in store) return store[name] 23 | var camelName = camelize(name) 24 | if (camelName in store) return store[camelName] 25 | } 26 | return dataAttr.call($(node), name) 27 | } 28 | } 29 | 30 | // Store value under camelized key on node 31 | function setData(node, name, value) { 32 | var id = node[exp] || (node[exp] = ++$.uuid), 33 | store = data[id] || (data[id] = attributeData(node)) 34 | if (name !== undefined) store[camelize(name)] = value 35 | return store 36 | } 37 | 38 | // Read all "data-*" attributes from a node 39 | function attributeData(node) { 40 | var store = {} 41 | $.each(node.attributes, function(i, attr) { 42 | if (attr.name.indexOf('data-') == 0) store[camelize(attr.name.replace('data-', ''))] = $.zepto.deserializeValue(attr.value) 43 | }) 44 | return store 45 | } 46 | 47 | $.fn.data = function(name, value) { 48 | return value === undefined ? 49 | // set multiple values via object 50 | $.isPlainObject(name) ? this.each(function(i, node) { 51 | $.each(name, function(key, value) { 52 | setData(node, key, value) 53 | }) 54 | }) : 55 | // get value from first element 56 | this.length == 0 ? undefined : getData(this[0], name) : 57 | // set value on all elements 58 | this.each(function() { 59 | setData(this, name, value) 60 | }) 61 | } 62 | 63 | $.fn.removeData = function(names) { 64 | if (typeof names == 'string') names = names.split(/\s+/) 65 | return this.each(function() { 66 | var id = this[exp], 67 | store = id && data[id] 68 | if (store) $.each(names, function() { 69 | delete store[camelize(this)] 70 | }) 71 | }) 72 | } 73 | })(Zepto) -------------------------------------------------------------------------------- /dist/0.1.0/backbone.trackit.min.js: -------------------------------------------------------------------------------- 1 | // 2 | // backbone.trackit - 0.1.0 3 | // The MIT License 4 | // Copyright (c) 2013 The New York Times, CMS Group, Matthew DeLambo 5 | // 6 | !function(){var a=[],b=function(b){_.isEmpty(b._unsavedChanges)?a=_.filter(a,function(a){return b.cid!=a.cid}):_.findWhere(a,{cid:b.cid})||a.push(b)},c=function(b){var c,d=_.rest(arguments),e=function(a,b){return _.isBoolean(b)?b:(_.isString(b)?a[b]:b).apply(a,d)};return _.each(a,function(a){!c&&e(a,a._unsavedConfig[b])&&(c=a._unsavedConfig.prompt)}),c};Backbone.History.prototype.navigate=_.wrap(Backbone.History.prototype.navigate,function(a,b,d){var e=c("unloadRouterPrompt",b,d);e?confirm(e+" \n\nAre you sure you want to leave this page?")&&a.call(this,b,d):a.call(this,b,d)}),window.onbeforeunload=function(a){return c("unloadWindowPrompt",a)},_.extend(Backbone.Model.prototype,{unsaved:{},_trackingChanges:!1,_originalAttrs:{},_unsavedChanges:{},startTracking:function(){return this._unsavedConfig=_.extend({},{prompt:"You have unsaved changes!",unloadRouterPrompt:!1,unloadWindowPrompt:!1},this.unsaved||{}),this._trackingChanges=!0,this._resetTracking(),this._triggerUnsavedChanges(),this},stopTracking:function(){return this._trackingChanges=!1,this._originalAttrs={},this._unsavedChanges={},this._triggerUnsavedChanges(),this},restartTracking:function(){return this._resetTracking(),this._triggerUnsavedChanges(),this},resetAttributes:function(){return this._trackingChanges?(this.attributes=this._originalAttrs,this._resetTracking(),this._triggerUnsavedChanges(),this):void 0},unsavedAttributes:function(a){if(!a)return _.isEmpty(this._unsavedChanges)?!1:_.clone(this._unsavedChanges);var b,c=!1,d=this._unsavedChanges;for(var e in a)_.isEqual(d[e],b=a[e])||((c||(c={}))[e]=b);return c},_resetTracking:function(){this._originalAttrs=_.clone(this.attributes),this._unsavedChanges={}},_triggerUnsavedChanges:function(){this.trigger("unsavedChanges",!_.isEmpty(this._unsavedChanges),_.clone(this._unsavedChanges)),this.unsaved&&b(this)}}),Backbone.Model.prototype.set=_.wrap(Backbone.Model.prototype.set,function(a,b,c,d){var e,f;return null==b?this:("object"==typeof b?(e=b,d=c):(e={})[b]=c,d||(d={}),f=a.call(this,e,d),this._trackingChanges&&!d.silent&&(_.each(e,_.bind(function(a,b){_.isEqual(this._originalAttrs[b],a)?delete this._unsavedChanges[b]:this._unsavedChanges[b]=a},this)),this._triggerUnsavedChanges()),f)}),Backbone.sync=_.wrap(Backbone.sync,function(a,b,c,d){return d||(d={}),"update"==b&&(d.success=_.wrap(d.success,_.bind(function(a,b,d,e){var f;return a&&(f=a.call(this,b,d,e)),c._trackingChanges&&(c._resetTracking(),c._triggerUnsavedChanges()),f},this))),a(b,c,d)})}(); -------------------------------------------------------------------------------- /test/vendor/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-banner { 58 | height: 5px; 59 | } 60 | 61 | #qunit-testrunner-toolbar { 62 | padding: 0.5em 0 0.5em 2em; 63 | color: #5E740B; 64 | background-color: #eee; 65 | } 66 | 67 | #qunit-userAgent { 68 | padding: 0.5em 0 0.5em 2.5em; 69 | background-color: #2b81af; 70 | color: #fff; 71 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 72 | } 73 | 74 | 75 | /** Tests: Pass/Fail */ 76 | 77 | #qunit-tests { 78 | list-style-position: inside; 79 | } 80 | 81 | #qunit-tests li { 82 | padding: 0.4em 0.5em 0.4em 2.5em; 83 | border-bottom: 1px solid #fff; 84 | list-style-position: inside; 85 | } 86 | 87 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 88 | display: none; 89 | } 90 | 91 | #qunit-tests li strong { 92 | cursor: pointer; 93 | } 94 | 95 | #qunit-tests li a { 96 | padding: 0.5em; 97 | color: #c2ccd1; 98 | text-decoration: none; 99 | } 100 | #qunit-tests li a:hover, 101 | #qunit-tests li a:focus { 102 | color: #000; 103 | } 104 | 105 | #qunit-tests ol { 106 | margin-top: 0.5em; 107 | padding: 0.5em; 108 | 109 | background-color: #fff; 110 | 111 | border-radius: 15px; 112 | -moz-border-radius: 15px; 113 | -webkit-border-radius: 15px; 114 | 115 | box-shadow: inset 0px 2px 13px #999; 116 | -moz-box-shadow: inset 0px 2px 13px #999; 117 | -webkit-box-shadow: inset 0px 2px 13px #999; 118 | } 119 | 120 | #qunit-tests table { 121 | border-collapse: collapse; 122 | margin-top: .2em; 123 | } 124 | 125 | #qunit-tests th { 126 | text-align: right; 127 | vertical-align: top; 128 | padding: 0 .5em 0 0; 129 | } 130 | 131 | #qunit-tests td { 132 | vertical-align: top; 133 | } 134 | 135 | #qunit-tests pre { 136 | margin: 0; 137 | white-space: pre-wrap; 138 | word-wrap: break-word; 139 | } 140 | 141 | #qunit-tests del { 142 | background-color: #e0f2be; 143 | color: #374e0c; 144 | text-decoration: none; 145 | } 146 | 147 | #qunit-tests ins { 148 | background-color: #ffcaca; 149 | color: #500; 150 | text-decoration: none; 151 | } 152 | 153 | /*** Test Counts */ 154 | 155 | #qunit-tests b.counts { color: black; } 156 | #qunit-tests b.passed { color: #5E740B; } 157 | #qunit-tests b.failed { color: #710909; } 158 | 159 | #qunit-tests li li { 160 | margin: 0.5em; 161 | padding: 0.4em 0.5em 0.4em 0.5em; 162 | background-color: #fff; 163 | border-bottom: none; 164 | list-style-position: inside; 165 | } 166 | 167 | /*** Passing Styles */ 168 | 169 | #qunit-tests li li.pass { 170 | color: #5E740B; 171 | background-color: #fff; 172 | border-left: 26px solid #C6E746; 173 | } 174 | 175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 176 | #qunit-tests .pass .test-name { color: #366097; } 177 | 178 | #qunit-tests .pass .test-actual, 179 | #qunit-tests .pass .test-expected { color: #999999; } 180 | 181 | #qunit-banner.qunit-pass { background-color: #C6E746; } 182 | 183 | /*** Failing Styles */ 184 | 185 | #qunit-tests li li.fail { 186 | color: #710909; 187 | background-color: #fff; 188 | border-left: 26px solid #EE5757; 189 | } 190 | 191 | #qunit-tests > li:last-child { 192 | border-radius: 0 0 15px 15px; 193 | -moz-border-radius: 0 0 15px 15px; 194 | -webkit-border-bottom-right-radius: 15px; 195 | -webkit-border-bottom-left-radius: 15px; 196 | } 197 | 198 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 199 | #qunit-tests .fail .test-name, 200 | #qunit-tests .fail .module-name { color: #000000; } 201 | 202 | #qunit-tests .fail .test-actual { color: #EE5757; } 203 | #qunit-tests .fail .test-expected { color: green; } 204 | 205 | #qunit-banner.qunit-fail { background-color: #EE5757; } 206 | 207 | 208 | /** Result */ 209 | 210 | #qunit-testresult { 211 | padding: 0.5em 0.5em 0.5em 2.5em; 212 | 213 | color: #2b81af; 214 | background-color: #D2E0E6; 215 | 216 | border-bottom: 1px solid white; 217 | } 218 | 219 | /** Fixture */ 220 | 221 | #qunit-fixture { 222 | position: absolute; 223 | top: -10000px; 224 | left: -10000px; 225 | } 226 | -------------------------------------------------------------------------------- /dist/0.1.0/backbone.trackit.js: -------------------------------------------------------------------------------- 1 | // 2 | // backbone.trackit - 0.1.0 3 | // The MIT License 4 | // Copyright (c) 2013 The New York Times, CMS Group, Matthew DeLambo 5 | // 6 | (function() { 7 | 8 | // Unsaved Record Keeping 9 | // ---------------------- 10 | 11 | // Collection of all models in an app that have unsaved changes. 12 | var unsavedModels = []; 13 | 14 | // If the given model has unsaved changes then add it to 15 | // the `unsavedModels` collection, otherwise remove it. 16 | var updateUnsavedModels = function(model) { 17 | if (!_.isEmpty(model._unsavedChanges)) { 18 | if (!_.findWhere(unsavedModels, {cid:model.cid})) unsavedModels.push(model); 19 | } else { 20 | unsavedModels = _.filter(unsavedModels, function(m) { return model.cid != m.cid; }); 21 | } 22 | }; 23 | 24 | // Unload Handlers 25 | // --------------- 26 | 27 | // Helper which returns a prompt message for an unload handler. 28 | // Uses the given function name (one of the callback names 29 | // from the `model.unsaved` configuration hash) to evaluate 30 | // whether a prompt is needed/returned. 31 | var getPrompt = function(fnName) { 32 | var prompt, args = _.rest(arguments); 33 | // Evaluate and return a boolean result. The given `fn` may be a 34 | // boolean value, a function, or the name of a function on the model. 35 | var evaluateModelFn = function(model, fn) { 36 | if (_.isBoolean(fn)) return fn; 37 | return (_.isString(fn) ? model[fn] : fn).apply(model, args); 38 | }; 39 | _.each(unsavedModels, function(model) { 40 | if (!prompt && evaluateModelFn(model, model._unsavedConfig[fnName])) 41 | prompt = model._unsavedConfig.prompt; 42 | }); 43 | return prompt; 44 | }; 45 | 46 | // Wrap Backbone.History.navigate so that in-app routing 47 | // (`router.navigate('/path')`) can be intercepted with a 48 | // confirmation if there are any unsaved models. 49 | Backbone.History.prototype.navigate = _.wrap(Backbone.History.prototype.navigate, function(oldNav, fragment, options) { 50 | var prompt = getPrompt('unloadRouterPrompt', fragment, options); 51 | if (prompt) { 52 | if (confirm(prompt + ' \n\nAre you sure you want to leave this page?')) { 53 | oldNav.call(this, fragment, options); 54 | } 55 | } else { 56 | oldNav.call(this, fragment, options); 57 | } 58 | }); 59 | 60 | // Create a browser unload handler which is triggered 61 | // on the refresh, back, or forward button. 62 | window.onbeforeunload = function(e) { 63 | return getPrompt('unloadWindowPrompt', e); 64 | }; 65 | 66 | // Backbone.Model API 67 | // ------------------ 68 | 69 | _.extend(Backbone.Model.prototype, { 70 | 71 | unsaved: {}, 72 | _trackingChanges: false, 73 | _originalAttrs: {}, 74 | _unsavedChanges: {}, 75 | 76 | // Opt in to tracking attribute changes 77 | // between saves. 78 | startTracking: function() { 79 | this._unsavedConfig = _.extend({}, { 80 | prompt: 'You have unsaved changes!', 81 | unloadRouterPrompt: false, 82 | unloadWindowPrompt: false 83 | }, this.unsaved || {}); 84 | this._trackingChanges = true; 85 | this._resetTracking(); 86 | this._triggerUnsavedChanges(); 87 | return this; 88 | }, 89 | 90 | // Resets the default tracking values 91 | // and stops tracking attribute changes. 92 | stopTracking: function() { 93 | this._trackingChanges = false; 94 | this._originalAttrs = {}; 95 | this._unsavedChanges = {}; 96 | this._triggerUnsavedChanges(); 97 | return this; 98 | }, 99 | 100 | // Gets rid of accrued changes and 101 | // resets state. 102 | restartTracking: function() { 103 | this._resetTracking(); 104 | this._triggerUnsavedChanges(); 105 | return this; 106 | }, 107 | 108 | // Restores this model's attributes to 109 | // their original values since tracking 110 | // started, the last save, or last restart. 111 | resetAttributes: function() { 112 | if (!this._trackingChanges) return; 113 | this.attributes = this._originalAttrs; 114 | this._resetTracking(); 115 | this._triggerUnsavedChanges(); 116 | return this; 117 | }, 118 | 119 | // Symmetric to Backbone's `model.changedAttributes()`, 120 | // except that this returns a hash of the model's attributes that 121 | // have changed since the last save, or `false` if there are none. 122 | // Like `changedAttributes`, an external attributes hash can be 123 | // passed in, returning the attributes in that hash which differ 124 | // from the model. 125 | unsavedAttributes: function(attrs) { 126 | if (!attrs) return _.isEmpty(this._unsavedChanges) ? false : _.clone(this._unsavedChanges); 127 | var val, changed = false, old = this._unsavedChanges; 128 | for (var attr in attrs) { 129 | if (_.isEqual(old[attr], (val = attrs[attr]))) continue; 130 | (changed || (changed = {}))[attr] = val; 131 | } 132 | return changed; 133 | }, 134 | 135 | _resetTracking: function() { 136 | this._originalAttrs = _.clone(this.attributes); 137 | this._unsavedChanges = {}; 138 | }, 139 | 140 | // Trigger an `unsavedChanges` event on this model, 141 | // supplying the result of whether there are unsaved 142 | // changes and a changed attributes hash. 143 | _triggerUnsavedChanges: function() { 144 | this.trigger('unsavedChanges', !_.isEmpty(this._unsavedChanges), _.clone(this._unsavedChanges)); 145 | if (this.unsaved) updateUnsavedModels(this); 146 | } 147 | }); 148 | 149 | // Wrap `model.set()` and update the internal 150 | // unsaved changes record keeping. 151 | Backbone.Model.prototype.set = _.wrap(Backbone.Model.prototype.set, function(oldSet, key, val, options) { 152 | var attrs, ret; 153 | if (key == null) return this; 154 | // Handle both `"key", value` and `{key: value}` -style arguments. 155 | if (typeof key === 'object') { 156 | attrs = key; 157 | options = val; 158 | } else { 159 | (attrs = {})[key] = val; 160 | } 161 | options || (options = {}); 162 | 163 | // Delegate to Backbone's set. 164 | ret = oldSet.call(this, attrs, options); 165 | 166 | if (this._trackingChanges && !options.silent) { 167 | _.each(attrs, _.bind(function(val, key) { 168 | if (_.isEqual(this._originalAttrs[key], val)) 169 | delete this._unsavedChanges[key]; 170 | else 171 | this._unsavedChanges[key] = val; 172 | }, this)); 173 | this._triggerUnsavedChanges(); 174 | } 175 | return ret; 176 | }); 177 | 178 | // Intercept `model.save()` and reset tracking/unsaved 179 | // changes if it was successful. 180 | Backbone.sync = _.wrap(Backbone.sync, function(oldSync, method, model, options) { 181 | options || (options = {}); 182 | 183 | if (method == 'update') { 184 | options.success = _.wrap(options.success, _.bind(function(oldSuccess, data, textStatus, jqXHR) { 185 | var ret; 186 | if (oldSuccess) ret = oldSuccess.call(this, data, textStatus, jqXHR); 187 | if (model._trackingChanges) { 188 | model._resetTracking(); 189 | model._triggerUnsavedChanges(); 190 | } 191 | return ret; 192 | }, this)); 193 | } 194 | return oldSync(method, model, options); 195 | }); 196 | 197 | })(); -------------------------------------------------------------------------------- /dist/0.1.0/README.md: -------------------------------------------------------------------------------- 1 | # Backbone.trackit 2 | 3 | A small, opinionated [Backbone.js](http://documentcloud.github.com/backbone) plugin that manages model changes that accrue between saves, giving a Model the ability to undo previous changes, trigger events when there are unsaved changes, and opt in to before unload route handling. 4 | 5 | ## Introduction 6 | 7 | At the heart of every JavaScript application is the model, and no frontend framework matches the extensible, well-featured model that Backbone provides. To stay unopinionated, Backbone's model only has a basic set of functionality for managing changes, where the current and previous change values are preserved until the next change. For example: 8 | 9 | ```js 10 | var model = new Backbone.Model({id:1, artist:'John Cage', 'work':'4\'33"'}); 11 | 12 | model.set('work', 'Amores'); 13 | console.log(model.changedAttributes()); // >> Object {work: "Amores"} 14 | console.log(model.previous('work')); // >> 4'33" 15 | 16 | model.set('advisor', 'Arnold Schoenberg'); 17 | console.log(model.changedAttributes()); // >> Object {advisor: "Arnold Schoenberg"} 18 | 19 | ``` 20 | 21 | Backbone's change management handles well for most models, but the ability to manage multiple changes between successful save events is a common pattern, and that's what Backbone.trackit aims to provide. For example, the following demonstrates how to use the api to `startTracking` unsaved changes, get the accrued `unsavedAttributes`, and how a call to `save` the model resets the internal tracking: 22 | 23 | ```js 24 | var model = new Backbone.Model({id:1, artist:'Samuel Beckett', 'work':'Molloy'}); 25 | model.startTracking(); 26 | 27 | model.set('work', 'Malone Dies'); 28 | console.log(model.unsavedAttributes()); // >> Object {work: "Malone Dies"} 29 | 30 | model.set('period', 'Modernism'); 31 | console.log(model.unsavedAttributes()); // >> Object {work: "Malone Dies", period: "Modernism"} 32 | 33 | model.save({}, { 34 | success: function() { 35 | console.log(model.unsavedAttributes()); // >> false 36 | } 37 | }); 38 | 39 | ``` 40 | 41 | In addition, the library adds functionality to `resetAttributes` to their original state since the last save, triggers an event when the state of `unsavedChanges` is updated, and has options to opt into prompting to confirm before routing to a new context. 42 | 43 | 44 | ## Download 45 | 46 | [0.1.0 min](https://raw.github.com/NYTimes/backbone.trackit/master/dist/0.1.0/backbone.trackit.min.js) - 2.6k 47 | 48 | [0.1.0 gz](https://raw.github.com/NYTimes/backbone.trackit/master/dist/0.1.0/backbone.trackit.min.js.gz) - 1k 49 | 50 | [edge](https://raw.github.com/NYTimes/backbone.trackit/master/backbone.trackit.js) 51 | 52 | 53 | ## API 54 | 55 | ### startTracking - *model.startTracking()* 56 | 57 | Start tracking attribute changes between saves. 58 | 59 | ### restartTracking - *model.restartTracking()* 60 | 61 | Restart the current internal tracking of attribute changes and state since tracking was started. 62 | 63 | ### stopTracking - *model.stopTracking()* 64 | 65 | Stop tracking attribute changes between saves. 66 | 67 | If an `unsaved` configuration was defined, it is important to call this when a model goes unused/should be destroyed (see the `unsaved` configuration for more information). 68 | 69 | ### unsavedAttributes - *model.unsavedAttributes([attributes])* 70 | 71 | Symmetric to Backbone's `model.changedAttributes()`, except that this returns a hash of the model's attributes that have changed since the last save, or `false` if there are none. Like `changedAttributes`, an external attributes hash can be passed in, returning the attributes in that hash which differ from the model. 72 | 73 | ### resetAttributes - *model.resetAttributes()* 74 | 75 | Restores this model's attributes to their original values since the last call to `startTracking`, `restartTracking`, `resetAttributes`, or `save`. 76 | 77 | ### unsavedChanges (event) 78 | 79 | Triggered after any changes have been made to the state of unsaved attributes. Passed into the event callback is the boolean value for whether or not the model has unsaved changes, and a cloned hash of the unsaved changes. This event is only triggered after unsaved attribute tracking is started (`startTracking`) and will stop triggering after tracking is turned off (`stopTracking`). 80 | 81 | ```js 82 | model.on('unsavedChanges', function(hasChanges, unsavedAttrs) { 83 | ... 84 | }); 85 | ``` 86 | 87 | ### unsaved (configuration) - *model.unsaved* 88 | 89 | The `unsaved` configuration is optional, and is used to opt into and configure unload handling when route/browser navigation changes and the model has unsaved changes. Unload handling warns the user with a dialog prompt, where the user can choose to continue or stop navigation. Unfortunately, both handlers (browser and in-app; `unloadWindowPrompt` and `unloadRouterPrompt`) are needed becuase they are triggered in different scenarios. 90 | 91 | Note: Any model that defines an `unsaved` configuration and uses `startTracking` should call `stopTracking` (when done and if there are unsaved changes) to remove any internal references used by the library so that it can be garbage collected. 92 | 93 | #### prompt - default: *"You have unsaved changes!"* 94 | 95 | When navigation is blocked because of unsaved changes, the given `prompt` message will be displayed to the user in a confirmation dialog. Note, Firefox (only) will not display customized prompt messages; instead, Firefox will prompt the user with a generic confirmation dialog. 96 | 97 | #### unloadWindowPrompt - default: *false* 98 | 99 | When `true` prompts the user on browser navigation (back, forward, refresh buttons) when there are unsaved changes. This property can be defined with a function callback that should return `true` or `false` depending on whether or not navigation should be blocked. Like most Backbone configuration, the callback may be either the name of a method on the model, or a direct function body. 100 | 101 | #### unloadRouterPrompt - default: *false* 102 | 103 | When `true` prompts the user on in-app navigation (`router.navigate('/path')`) when there are unsaved changes. This property can be defined with a function callback that should return `true` or `false` depending on whether or not navigation should be blocked. Like most Backbone configuration, the callback may be either the name of a method on the model, or a direct function body. 104 | 105 | 106 | ```js 107 | var model = Backbone.Model.extend({ 108 | unsaved: { 109 | prompt: 'Changes exist!', 110 | unloadWindowPrompt: true, 111 | unloadRouterPrompt: 'unloadRouter' 112 | }, 113 | 114 | unloadRouter: function(fragment, options) { 115 | if (fragment == '/article/edit-body') return false; 116 | return true; 117 | } 118 | }); 119 | ``` 120 | 121 | ## FAQ 122 | 123 | - **Not an undo/redo plugin** 124 | If you are looking for an undo/redo plugin, check out [backbone.memento](https://github.com/derickbailey/backbone.memento) 125 | 126 | - **Why are there two unload handlers (`unloadWindowPrompt`, `unloadRouterPrompt`)?** 127 | Since navigation can be triggered by the browser (forward, back, refresh buttons) or through pushstate/hashchange in the app (by Backbone), a handler needs to be created for both methods. 128 | 129 | - **Why doesn't Firefox display my unload `prompt`?** 130 | You can find out their reasoning and leave a message for Mozilla [here](https://bugzilla.mozilla.org/show_bug.cgi?id=588292). 131 | 132 | ## Change log 133 | 134 | ### 0.1.0 135 | - Initial version; extracted from an internal project (Blackbeard) that powers our News Services at The New York Times. 136 | 137 | ## License 138 | 139 | MIT -------------------------------------------------------------------------------- /backbone.trackit.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['backbone'], function (Backbone) { 5 | // Also create a global in case some scripts 6 | // that are loaded still are looking for 7 | // a global even when an AMD loader is in use. 8 | return factory(Backbone); 9 | }); 10 | } else { 11 | // Browser globals 12 | factory(root.Backbone); 13 | } 14 | }(this, function (Backbone) { 15 | // Unsaved Record Keeping 16 | // ---------------------- 17 | 18 | // Collection of all models in an app that have unsaved changes. 19 | var unsavedModels = []; 20 | 21 | // If the given model has unsaved changes then add it to 22 | // the `unsavedModels` collection, otherwise remove it. 23 | var updateUnsavedModels = function(model) { 24 | if (!_.isEmpty(model._unsavedChanges)) { 25 | if (!_.findWhere(unsavedModels, {cid:model.cid})) unsavedModels.push(model); 26 | } else { 27 | unsavedModels = _.filter(unsavedModels, function(m) { return model.cid != m.cid; }); 28 | } 29 | }; 30 | 31 | // Unload Handlers 32 | // --------------- 33 | 34 | // Helper which returns a prompt message for an unload handler. 35 | // Uses the given function name (one of the callback names 36 | // from the `model.unsaved` configuration hash) to evaluate 37 | // whether a prompt is needed/returned. 38 | var getPrompt = function(fnName) { 39 | var prompt, args = _.rest(arguments); 40 | // Evaluate and return a boolean result. The given `fn` may be a 41 | // boolean value, a function, or the name of a function on the model. 42 | var evaluateModelFn = function(model, fn) { 43 | if (_.isBoolean(fn)) return fn; 44 | return (_.isString(fn) ? model[fn] : fn).apply(model, args); 45 | }; 46 | _.each(unsavedModels, function(model) { 47 | if (!prompt && evaluateModelFn(model, model._unsavedConfig[fnName])) 48 | prompt = model._unsavedConfig.prompt; 49 | }); 50 | return prompt; 51 | }; 52 | 53 | // Wrap Backbone.History.navigate so that in-app routing 54 | // (`router.navigate('/path')`) can be intercepted with a 55 | // confirmation if there are any unsaved models. 56 | Backbone.History.prototype.navigate = _.wrap(Backbone.History.prototype.navigate, function(oldNav, fragment, options) { 57 | var prompt = getPrompt('unloadRouterPrompt', fragment, options); 58 | if (prompt) { 59 | if (confirm(prompt + ' \n\nAre you sure you want to leave this page?')) { 60 | oldNav.call(this, fragment, options); 61 | } 62 | } else { 63 | oldNav.call(this, fragment, options); 64 | } 65 | }); 66 | 67 | // Create a browser unload handler which is triggered 68 | // on the refresh, back, or forward button. 69 | window.onbeforeunload = function(e) { 70 | return getPrompt('unloadWindowPrompt', e); 71 | }; 72 | 73 | // Backbone.Model API 74 | // ------------------ 75 | 76 | _.extend(Backbone.Model.prototype, { 77 | 78 | unsaved: {}, 79 | _trackingChanges: false, 80 | _originalAttrs: {}, 81 | _unsavedChanges: {}, 82 | 83 | // Opt in to tracking attribute changes 84 | // between saves. 85 | startTracking: function() { 86 | this._unsavedConfig = _.extend({}, { 87 | prompt: 'You have unsaved changes!', 88 | unloadRouterPrompt: false, 89 | unloadWindowPrompt: false 90 | }, this.unsaved || {}); 91 | this._trackingChanges = true; 92 | this._resetTracking(); 93 | this._triggerUnsavedChanges(); 94 | return this; 95 | }, 96 | 97 | // Resets the default tracking values 98 | // and stops tracking attribute changes. 99 | stopTracking: function() { 100 | this._trackingChanges = false; 101 | this._originalAttrs = {}; 102 | this._unsavedChanges = {}; 103 | this._triggerUnsavedChanges(); 104 | return this; 105 | }, 106 | 107 | // Gets rid of accrued changes and 108 | // resets state. 109 | restartTracking: function() { 110 | this._resetTracking(); 111 | this._triggerUnsavedChanges(); 112 | return this; 113 | }, 114 | 115 | // Restores this model's attributes to 116 | // their original values since tracking 117 | // started, the last save, or last restart. 118 | resetAttributes: function() { 119 | if (!this._trackingChanges) return; 120 | this.attributes = this._originalAttrs; 121 | this._resetTracking(); 122 | this._triggerUnsavedChanges(); 123 | return this; 124 | }, 125 | 126 | // Symmetric to Backbone's `model.changedAttributes()`, 127 | // except that this returns a hash of the model's attributes that 128 | // have changed since the last save, or `false` if there are none. 129 | // Like `changedAttributes`, an external attributes hash can be 130 | // passed in, returning the attributes in that hash which differ 131 | // from the model. 132 | unsavedAttributes: function(attrs) { 133 | if (!attrs) return _.isEmpty(this._unsavedChanges) ? false : _.clone(this._unsavedChanges); 134 | var val, changed = false, old = this._unsavedChanges; 135 | for (var attr in attrs) { 136 | if (_.isEqual(old[attr], (val = attrs[attr]))) continue; 137 | (changed || (changed = {}))[attr] = val; 138 | } 139 | return changed; 140 | }, 141 | 142 | _resetTracking: function() { 143 | this._originalAttrs = _.clone(this.attributes); 144 | this._unsavedChanges = {}; 145 | }, 146 | 147 | // Trigger an `unsavedChanges` event on this model, 148 | // supplying the result of whether there are unsaved 149 | // changes and a changed attributes hash. 150 | _triggerUnsavedChanges: function() { 151 | this.trigger('unsavedChanges', !_.isEmpty(this._unsavedChanges), _.clone(this._unsavedChanges), this); 152 | if (this.unsaved) updateUnsavedModels(this); 153 | } 154 | }); 155 | 156 | // Wrap `model.set()` and update the internal 157 | // unsaved changes record keeping. 158 | Backbone.Model.prototype.set = _.wrap(Backbone.Model.prototype.set, function(oldSet, key, val, options) { 159 | var attrs, ret; 160 | if (key == null) return this; 161 | // Handle both `"key", value` and `{key: value}` -style arguments. 162 | if (typeof key === 'object') { 163 | attrs = key; 164 | options = val; 165 | } else { 166 | (attrs = {})[key] = val; 167 | } 168 | options || (options = {}); 169 | 170 | // Delegate to Backbone's set. 171 | ret = oldSet.call(this, attrs, options); 172 | 173 | if (this._trackingChanges && !options.silent && !options.trackit_silent) { 174 | _.each(attrs, _.bind(function(val, key) { 175 | if (_.isEqual(this._originalAttrs[key], val)) 176 | delete this._unsavedChanges[key]; 177 | else 178 | this._unsavedChanges[key] = val; 179 | }, this)); 180 | this._triggerUnsavedChanges(); 181 | } 182 | return ret; 183 | }); 184 | 185 | // Intercept `model.save()` and reset tracking/unsaved 186 | // changes if it was successful. 187 | Backbone.sync = _.wrap(Backbone.sync, function(oldSync, method, model, options) { 188 | options || (options = {}); 189 | 190 | if (method == 'update' || method == 'create' || method == 'patch') { 191 | options.success = _.wrap(options.success, _.bind(function(oldSuccess, data, textStatus, jqXHR) { 192 | var ret; 193 | if (oldSuccess) ret = oldSuccess.call(this, data, textStatus, jqXHR); 194 | if (model._trackingChanges) { 195 | model._resetTracking(); 196 | model._triggerUnsavedChanges(); 197 | } 198 | return ret; 199 | }, this)); 200 | } 201 | return oldSync(method, model, options); 202 | }); 203 | })); 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backbone.trackit 2 | 3 | A small, opinionated [Backbone.js](http://documentcloud.github.com/backbone) plugin that manages model changes that accrue between saves, giving a Model the ability to undo previous changes, trigger events when there are unsaved changes, and opt in to before unload route handling. 4 | 5 | ## Introduction 6 | 7 | At the heart of every JavaScript application is the model, and no frontend framework matches the extensible, well-featured model that Backbone provides. To stay unopinionated, Backbone's model only has a basic set of functionality for managing changes, where the current and previous change values are preserved until the next change. For example: 8 | 9 | ```js 10 | var model = new Backbone.Model({id:1, artist:'John Cage', 'work':'4\'33"'}); 11 | 12 | model.set('work', 'Amores'); 13 | console.log(model.changedAttributes()); // >> Object {work: "Amores"} 14 | console.log(model.previous('work')); // >> 4'33" 15 | 16 | model.set('advisor', 'Arnold Schoenberg'); 17 | console.log(model.changedAttributes()); // >> Object {advisor: "Arnold Schoenberg"} 18 | 19 | ``` 20 | 21 | Backbone's change management handles well for most models, but the ability to manage multiple changes between successful save events is a common pattern, and that's what Backbone.trackit aims to provide. For example, the following demonstrates how to use the api to `startTracking` unsaved changes, get the accrued `unsavedAttributes`, and how a call to `save` the model resets the internal tracking: 22 | 23 | ```js 24 | var model = new Backbone.Model({id:1, artist:'Samuel Beckett', 'work':'Molloy'}); 25 | model.startTracking(); 26 | 27 | model.set('work', 'Malone Dies'); 28 | console.log(model.unsavedAttributes()); // >> Object {work: "Malone Dies"} 29 | 30 | model.set('period', 'Modernism'); 31 | console.log(model.unsavedAttributes()); // >> Object {work: "Malone Dies", period: "Modernism"} 32 | 33 | model.save({}, { 34 | success: function() { 35 | console.log(model.unsavedAttributes()); // >> false 36 | } 37 | }); 38 | 39 | ``` 40 | 41 | In addition, the library adds functionality to `resetAttributes` to their original state since the last save, triggers an event when the state of `unsavedChanges` is updated, and has options to opt into prompting to confirm before routing to a new context. 42 | 43 | 44 | ## Download 45 | 46 | [0.1.0 min](https://raw.github.com/NYTimes/backbone.trackit/master/dist/0.1.0/backbone.trackit.min.js) - 2.6k 47 | 48 | [0.1.0 gz](https://raw.github.com/NYTimes/backbone.trackit/master/dist/0.1.0/backbone.trackit.min.js.gz) - 1k 49 | 50 | [edge](https://raw.github.com/NYTimes/backbone.trackit/master/backbone.trackit.js) 51 | 52 | 53 | ## API 54 | 55 | ### startTracking - *model.startTracking()* 56 | 57 | Start tracking attribute changes between saves. 58 | 59 | ### restartTracking - *model.restartTracking()* 60 | 61 | Restart the current internal tracking of attribute changes and state since tracking was started. 62 | 63 | ### stopTracking - *model.stopTracking()* 64 | 65 | Stop tracking attribute changes between saves. 66 | 67 | If an `unsaved` configuration was defined, it is important to call this when a model goes unused/should be destroyed (see the `unsaved` configuration for more information). 68 | 69 | ### unsavedAttributes - *model.unsavedAttributes([attributes])* 70 | 71 | Symmetric to Backbone's `model.changedAttributes()`, except that this returns a hash of the model's attributes that have changed since the last save, or `false` if there are none. Like `changedAttributes`, an external attributes hash can be passed in, returning the attributes in that hash which differ from the model. 72 | 73 | ### resetAttributes - *model.resetAttributes()* 74 | 75 | Restores this model's attributes to their original values since the last call to `startTracking`, `restartTracking`, `resetAttributes`, or `save`. 76 | 77 | ### unsavedChanges (event) 78 | 79 | Triggered after any changes have been made to the state of unsaved attributes. Passed into the event callback is the boolean value for whether or not the model has unsaved changes, and a cloned hash of the unsaved changes. This event is only triggered after unsaved attribute tracking is started (`startTracking`) and will stop triggering after tracking is turned off (`stopTracking`). 80 | 81 | ```js 82 | model.on('unsavedChanges', function(hasChanges, unsavedAttrs, model) { 83 | ... 84 | }); 85 | ``` 86 | 87 | ### trackit_silent (option) 88 | 89 | When passed as an option and set to `true`, trackit will not track changes when setting the model. 90 | 91 | ```js 92 | model.fetch({ ..., trackit_silent:true}); 93 | model.set({artist:'John Cage'}, {trackit_silent:true}); 94 | console.log(model.unsavedAttributes()); // false 95 | ``` 96 | 97 | ### unsaved (configuration) - *model.unsaved* 98 | 99 | The `unsaved` configuration is optional, and is used to opt into and configure unload handling when route/browser navigation changes and the model has unsaved changes. Unload handling warns the user with a dialog prompt, where the user can choose to continue or stop navigation. Unfortunately, both handlers (browser and in-app; `unloadWindowPrompt` and `unloadRouterPrompt`) are needed becuase they are triggered in different scenarios. 100 | 101 | Note: Any model that defines an `unsaved` configuration and uses `startTracking` should call `stopTracking` (when done and if there are unsaved changes) to remove any internal references used by the library so that it can be garbage collected. 102 | 103 | #### prompt - default: *"You have unsaved changes!"* 104 | 105 | When navigation is blocked because of unsaved changes, the given `prompt` message will be displayed to the user in a confirmation dialog. Note, Firefox (only) will not display customized prompt messages; instead, Firefox will prompt the user with a generic confirmation dialog. 106 | 107 | #### unloadWindowPrompt - default: *false* 108 | 109 | When `true` prompts the user on browser navigation (back, forward, refresh buttons) when there are unsaved changes. This property can be defined with a function callback that should return `true` or `false` depending on whether or not navigation should be blocked. Like most Backbone configuration, the callback may be either the name of a method on the model, or a direct function body. 110 | 111 | #### unloadRouterPrompt - default: *false* 112 | 113 | When `true` prompts the user on in-app navigation (`router.navigate('/path')`) when there are unsaved changes. This property can be defined with a function callback that should return `true` or `false` depending on whether or not navigation should be blocked. Like most Backbone configuration, the callback may be either the name of a method on the model, or a direct function body. 114 | 115 | 116 | ```js 117 | var model = Backbone.Model.extend({ 118 | unsaved: { 119 | prompt: 'Changes exist!', 120 | unloadWindowPrompt: true, 121 | unloadRouterPrompt: 'unloadRouter' 122 | }, 123 | 124 | unloadRouter: function(fragment, options) { 125 | if (fragment == '/article/edit-body') return false; 126 | return true; 127 | } 128 | }); 129 | ``` 130 | 131 | ## FAQ 132 | 133 | - **Not an undo/redo plugin** 134 | If you are looking for an undo/redo plugin, check out [backbone.memento](https://github.com/derickbailey/backbone.memento) 135 | 136 | - **Why are there two unload handlers (`unloadWindowPrompt`, `unloadRouterPrompt`)?** 137 | Since navigation can be triggered by the browser (forward, back, refresh buttons) or through pushstate/hashchange in the app (by Backbone), a handler needs to be created for both methods. 138 | 139 | - **Why doesn't Firefox display my unload `prompt`?** 140 | You can find out their reasoning and leave a message for Mozilla [here](https://bugzilla.mozilla.org/show_bug.cgi?id=588292). 141 | 142 | ## Change log 143 | 144 | ### Master 145 | 146 | - Added `trackit_silent` option that can be passed in `options` hashes so that attriubutes can be set into a model without being tracked. 147 | 148 | - Added ability for new models (without ids) to be notified of unsaved changes after a successful call to `model.save()`. 149 | 150 | - Added `model` as third parameter to `unsavedChanges` event callback. 151 | 152 | - Added support for the `patch` method on `model#save`. 153 | 154 | ### 0.1.0 155 | 156 | - Initial version; extracted from an internal project (Blackbeard) that powers our News Services at The New York Times. 157 | 158 | ## License 159 | 160 | MIT -------------------------------------------------------------------------------- /test/suite.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | module("model"); 4 | 5 | test('startTracking', 20, function() { 6 | 7 | // Test both with and without an ID assigned to test both new object 8 | // creation and existing object modification. 9 | startTracking(new Backbone.Model({name:'John Cage'})); 10 | startTracking(new Backbone.Model({id:1, name:'John Cage'})); 11 | 12 | function startTracking(m) { 13 | equal(m._trackingChanges, false); 14 | equal(_.keys(m._originalAttrs), 0); 15 | m.set('friend', 'Schoenberg'); 16 | equal(m.unsavedAttributes(), false); 17 | 18 | m.startTracking(); 19 | m.on('unsavedChanges', function(b, ch) { 20 | equal(b, true); 21 | equal(_.keys(ch).length, 1); 22 | }); 23 | m.set('work', '4\'33"'); 24 | 25 | equal(m._trackingChanges, true); 26 | equal(m._originalAttrs.name, 'John Cage'); 27 | equal(m.unsavedAttributes().work, '4\'33"'); 28 | 29 | // Test model.save(), which should reset the unsaved changes. 30 | 31 | // Stubbing without a fancy test framework (I'm so uncool). 32 | var oldAjax = Backbone.$.ajax; 33 | Backbone.$.ajax = function(options) { 34 | var data = JSON.parse(options.data); 35 | if ("POST" == options.type) { 36 | // Spoof object creation by assigning the new model an ID. 37 | data.id = 1; 38 | } 39 | options.success(data, 'aok'); 40 | }; 41 | 42 | m.off('unsavedChanges'); 43 | m.save(null, {url:'none'}); 44 | 45 | equal(m.unsavedAttributes(), false); 46 | equal(m._originalAttrs.work, '4\'33"'); 47 | Backbone.$.ajax = oldAjax; 48 | m.stopTracking(); 49 | 50 | } 51 | }); 52 | 53 | test('stopTracking', 2, function() { 54 | 55 | var m = new Backbone.Model({id:2, name:'Marcel Duchamp'}); 56 | 57 | m.startTracking(); 58 | m.set('work', 'fountain'); 59 | m.on('unsavedChanges', function(b, ch) { 60 | equal(b, false); 61 | equal(_.keys(ch).length, 0); 62 | }); 63 | m.stopTracking(); 64 | 65 | }); 66 | 67 | test('restartTracking', 4, function() { 68 | 69 | var m = new Backbone.Model({id:2, name:'Marcel Duchamp'}); 70 | 71 | m.startTracking(); 72 | m.set('work', 'fountain'); 73 | equal(_.keys(m.unsavedAttributes()).length, 1); 74 | m.on('unsavedChanges', function(b, ch) { 75 | equal(b, false); 76 | equal(_.keys(ch).length, 0); 77 | }); 78 | m.restartTracking(); 79 | equal(m.unsavedAttributes(), false); 80 | m.off().stopTracking(); 81 | 82 | }); 83 | 84 | test('resetAttributes', 3, function() { 85 | 86 | var m = new Backbone.Model({id:3, name:'Harmony Korine', work:'Spring Breakers'}); 87 | 88 | m.startTracking(); 89 | m.set('work', 'Gummo'); 90 | m.on('unsavedChanges', function(b, ch) { 91 | equal(b, false); 92 | equal(_.keys(ch).length, 0); 93 | }); 94 | m.resetAttributes(); 95 | equal(m.get('work'), 'Spring Breakers'); 96 | m.off().stopTracking(); 97 | 98 | }); 99 | 100 | test('unsavedAttributes', 7, function() { 101 | 102 | var m = new Backbone.Model({id:4, name:'Autechre'}); 103 | 104 | m.startTracking(); 105 | equal(m.unsavedAttributes(), false); 106 | 107 | m.set('work', 'Quartice'); 108 | equal(m.unsavedAttributes().work, 'Quartice'); 109 | m.set({'label':'Warp', 'instrument':'computer'}); 110 | equal(m.unsavedAttributes().label, 'Warp'); 111 | equal(m.unsavedAttributes().instrument, 'computer'); 112 | 113 | equal(_.keys(m.unsavedAttributes({work:'Amber', label:'plus8', instrument:'computer'})).length, 2); 114 | equal(m.unsavedAttributes({work:'Amber', label:'plus8', instrument:'computer'}).work, 'Amber'); 115 | equal(m.unsavedAttributes({work:'Amber', label:'plus8', instrument:'computer'}).label, 'plus8'); 116 | m.stopTracking(); 117 | 118 | }); 119 | 120 | test('unsavedChanges', 3, function() { 121 | 122 | var m = new Backbone.Model({id:3, name:'Harmony Korine', work:'Spring Breakers'}); 123 | 124 | m.startTracking(); 125 | m.set('work', 'Gummo'); 126 | m.on('unsavedChanges', function(b, ch, model) { 127 | equal(b, false); 128 | equal(_.keys(ch).length, 0); 129 | equal(model, m); 130 | }); 131 | m.resetAttributes(); 132 | m.off().stopTracking(); 133 | 134 | }); 135 | 136 | module('unload handler'); 137 | 138 | test('window', 1, function() { 139 | var m = new Backbone.Model({id:5, name:'Samuel Beckett'}); 140 | m.unsaved = { 141 | unloadWindowPrompt: true 142 | }; 143 | 144 | // Spying/Stubbing without a fancy test framework (sue me). 145 | var oldUnload = window.onbeforeunload; 146 | window.onbeforeunload = function() { 147 | equal(oldUnload(), 'You have unsaved changes!'); 148 | }; 149 | 150 | m.startTracking(); 151 | m.set('work', 'Molloy'); 152 | window.onbeforeunload(); 153 | window.onbeforeunload = oldUnload; 154 | m.stopTracking(); 155 | 156 | }); 157 | 158 | test('prompt', 1, function() { 159 | var m = new Backbone.Model({id:5, name:'Samuel Beckett'}); 160 | m.unsaved = { 161 | prompt: 'Yes, there were times when I forgot not only who I was but that I was, forgot to be.', 162 | unloadWindowPrompt: true 163 | }; 164 | 165 | // Spying/Stubbing without a fancy test framework ("look ma, no hands"). 166 | var oldUnload = window.onbeforeunload; 167 | window.onbeforeunload = function() { 168 | equal(oldUnload(), 'Yes, there were times when I forgot not only who I was but that I was, forgot to be.'); 169 | }; 170 | 171 | m.startTracking(); 172 | m.set('work', 'Molloy'); 173 | window.onbeforeunload(); 174 | window.onbeforeunload = oldUnload; 175 | m.stopTracking(); 176 | 177 | }); 178 | 179 | test('unloadWindowPrompt callback', 1, function() { 180 | var m = new Backbone.Model({id:5, name:'Samuel Beckett'}); 181 | m.unsaved = { 182 | unloadWindowPrompt: function() { return true; } 183 | }; 184 | 185 | // Spying/Stubbing without a fancy test framework (old fashioned). 186 | var oldUnload = window.onbeforeunload; 187 | window.onbeforeunload = function() { 188 | equal(oldUnload(), 'You have unsaved changes!'); 189 | }; 190 | 191 | m.startTracking(); 192 | m.set('work', 'Molloy'); 193 | window.onbeforeunload(); 194 | window.onbeforeunload = oldUnload; 195 | m.stopTracking(); 196 | 197 | }); 198 | 199 | test('unloadWindowPrompt callback (function ref)', 1, function() { 200 | var m = new Backbone.Model({id:5, name:'Samuel Beckett'}); 201 | m.unsaved = { 202 | unloadWindowPrompt: 'unload' 203 | }; 204 | m.unload = function() {return true}; 205 | 206 | // Spying/Stubbing without a fancy test framework ("you can do that?"). 207 | var oldUnload = window.onbeforeunload; 208 | window.onbeforeunload = function() { 209 | equal(oldUnload(), 'You have unsaved changes!'); 210 | }; 211 | 212 | m.startTracking(); 213 | m.set('work', 'Molloy'); 214 | window.onbeforeunload(); 215 | window.onbeforeunload = oldUnload; 216 | m.stopTracking(); 217 | 218 | }); 219 | 220 | test('Backbone.History.navigate', 1, function() { 221 | 222 | var m = new Backbone.Model({id:6, name:'Issey Miyake'}); 223 | m.unsaved = { 224 | unloadRouterPrompt: true 225 | }; 226 | var r = new Backbone.Router(); 227 | Backbone.history.start(); 228 | 229 | // Spying/Stubbing without a fancy test framework ("nice!"). 230 | var oldConfirm = window.confirm; 231 | window.confirm = function(message) { 232 | equal(message.indexOf('You have unsaved changes!'), 0); 233 | }; 234 | 235 | m.startTracking('prompt for unsaved changes!'); 236 | 237 | m.set('work', 'Final Home'); 238 | r.navigate('#test') 239 | m.stopTracking(); 240 | r.navigate('#') 241 | 242 | window.confirm = oldConfirm; 243 | Backbone.history.stop(); 244 | 245 | }); 246 | 247 | test('unloadRouterPrompt callback (function return true)', 1, function() { 248 | 249 | var m = new Backbone.Model({id:6, name:'Issey Miyake'}); 250 | m.unsaved = { 251 | unloadRouterPrompt: function() { return true; } 252 | }; 253 | var r = new Backbone.Router(); 254 | Backbone.history.start(); 255 | 256 | // Spying/Stubbing without a fancy test framework (don't have a cow, man). 257 | var oldConfirm = window.confirm; 258 | window.confirm = function(message) { 259 | equal(message.indexOf('You have unsaved changes!'), 0); 260 | }; 261 | 262 | m.startTracking('prompt for unsaved changes!'); 263 | 264 | m.set('work', 'Final Home'); 265 | r.navigate('#test') 266 | m.stopTracking(); 267 | r.navigate('#') 268 | 269 | window.confirm = oldConfirm; 270 | Backbone.history.stop(); 271 | 272 | }); 273 | 274 | test('unloadRouterPrompt callback (function return false)', 0, function() { 275 | 276 | var m = new Backbone.Model({id:6, name:'Issey Miyake'}); 277 | m.unsaved = { 278 | unloadRouterPrompt: function() { return false; } 279 | }; 280 | var r = new Backbone.Router(); 281 | Backbone.history.start(); 282 | 283 | // Spying/Stubbing without a fancy test framework (bite me). 284 | var oldConfirm = window.confirm; 285 | window.confirm = function(message) { 286 | // It's expected that we don't get in here... 287 | equal(message.indexOf('You have unsaved changes!'), 0); 288 | }; 289 | 290 | m.startTracking('prompt for unsaved changes!'); 291 | 292 | m.set('work', 'Final Home'); 293 | r.navigate('#test') 294 | m.stopTracking(); 295 | r.navigate('#') 296 | 297 | window.confirm = oldConfirm; 298 | Backbone.history.stop(); 299 | 300 | }); 301 | 302 | test('trackit_silent option', 3, function() { 303 | 304 | var m = new Backbone.Model({id:2, name:'Burial'}); 305 | 306 | m.startTracking(); 307 | m.set('EP', 'Rival Dealer'); 308 | 309 | var oldAjax = Backbone.$.ajax; 310 | Backbone.$.ajax = function(options) { 311 | var data = {}; 312 | data.album = 'Untrue' 313 | options.success({album:'Untrue'}); 314 | }; 315 | 316 | m.fetch({url:'none', trackit_silent:true}); 317 | m.set({song:'Truant'}, {trackit_silent:true}); 318 | 319 | equal(m.get('album'), 'Untrue'); 320 | equal(m.get('song'), 'Truant'); 321 | equal(_.keys(m.unsavedAttributes()).length, 1); 322 | 323 | Backbone.$.ajax = oldAjax; 324 | m.off().stopTracking(); 325 | }); 326 | 327 | }); 328 | -------------------------------------------------------------------------------- /test/vendor/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.5.1 2 | // http://underscorejs.org 3 | // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | !function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,v=e.reduce,h=e.reduceRight,d=e.filter,g=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,w=i.bind,j=function(n){return n instanceof j?n:this instanceof j?(this._wrapped=n,void 0):new j(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=j),exports._=j):n._=j,j.VERSION="1.5.1";var A=j.each=j.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(j.has(n,a)&&t.call(e,n[a],a,n)===r)return};j.map=j.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e.push(t.call(r,n,u,i))}),e)};var E="Reduce of empty array with no initial value";j.reduce=j.foldl=j.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduce===v)return e&&(t=j.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(E);return r},j.reduceRight=j.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduceRight===h)return e&&(t=j.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=j.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(E);return r},j.find=j.detect=function(n,t,r){var e;return O(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},j.filter=j.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&e.push(n)}),e)},j.reject=function(n,t,r){return j.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},j.every=j.all=function(n,t,e){t||(t=j.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var O=j.some=j.any=function(n,t,e){t||(t=j.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};j.contains=j.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:O(n,function(n){return n===t})},j.invoke=function(n,t){var r=o.call(arguments,2),e=j.isFunction(t);return j.map(n,function(n){return(e?t:n[t]).apply(n,r)})},j.pluck=function(n,t){return j.map(n,function(n){return n[t]})},j.where=function(n,t,r){return j.isEmpty(t)?r?void 0:[]:j[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},j.findWhere=function(n,t){return j.where(n,t,!0)},j.max=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.max.apply(Math,n);if(!t&&j.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>e.computed&&(e={value:n,computed:a})}),e.value},j.min=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.min.apply(Math,n);if(!t&&j.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;ae||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.indexi;){var o=i+a>>>1;r.call(e,n[o])=0})})},j.difference=function(n){var t=c.apply(e,o.call(arguments,1));return j.filter(n,function(n){return!j.contains(t,n)})},j.zip=function(){for(var n=j.max(j.pluck(arguments,"length").concat(0)),t=new Array(n),r=0;n>r;r++)t[r]=j.pluck(arguments,""+r);return t},j.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},j.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=j.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},j.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},j.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=new Array(e);e>u;)i[u++]=n,n+=r;return i};var M=function(){};j.bind=function(n,t){var r,e;if(w&&n.bind===w)return w.apply(n,o.call(arguments,1));if(!j.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));M.prototype=n.prototype;var u=new M;M.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},j.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},j.bindAll=function(n){var t=o.call(arguments,1);if(0===t.length)throw new Error("bindAll must be passed function names");return A(t,function(t){n[t]=j.bind(n[t],n)}),n},j.memoize=function(n,t){var r={};return t||(t=j.identity),function(){var e=t.apply(this,arguments);return j.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},j.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},j.defer=function(n){return j.delay.apply(j,[n,1].concat(o.call(arguments,1)))},j.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var c=function(){o=r.leading===!1?0:new Date,a=null,i=n.apply(e,u)};return function(){var l=new Date;o||r.leading!==!1||(o=l);var f=t-(l-o);return e=this,u=arguments,0>=f?(clearTimeout(a),a=null,o=l,i=n.apply(e,u)):a||r.trailing===!1||(a=setTimeout(c,f)),i}},j.debounce=function(n,t,r){var e,u=null;return function(){var i=this,a=arguments,o=function(){u=null,r||(e=n.apply(i,a))},c=r&&!u;return clearTimeout(u),u=setTimeout(o,t),c&&(e=n.apply(i,a)),e}},j.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},j.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},j.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},j.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},j.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)j.has(n,r)&&t.push(r);return t},j.values=function(n){var t=[];for(var r in n)j.has(n,r)&&t.push(n[r]);return t},j.pairs=function(n){var t=[];for(var r in n)j.has(n,r)&&t.push([r,n[r]]);return t},j.invert=function(n){var t={};for(var r in n)j.has(n,r)&&(t[n[r]]=r);return t},j.functions=j.methods=function(n){var t=[];for(var r in n)j.isFunction(n[r])&&t.push(r);return t.sort()},j.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},j.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},j.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)j.contains(r,u)||(t[u]=n[u]);return t},j.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]===void 0&&(n[r]=t[r])}),n},j.clone=function(n){return j.isObject(n)?j.isArray(n)?n.slice():j.extend({},n):n},j.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof j&&(n=n._wrapped),t instanceof j&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==String(t);case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;var a=n.constructor,o=t.constructor;if(a!==o&&!(j.isFunction(a)&&a instanceof a&&j.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c=0,f=!0;if("[object Array]"==u){if(c=n.length,f=c==t.length)for(;c--&&(f=S(n[c],t[c],r,e)););}else{for(var s in n)if(j.has(n,s)&&(c++,!(f=j.has(t,s)&&S(n[s],t[s],r,e))))break;if(f){for(s in t)if(j.has(t,s)&&!c--)break;f=!c}}return r.pop(),e.pop(),f};j.isEqual=function(n,t){return S(n,t,[],[])},j.isEmpty=function(n){if(null==n)return!0;if(j.isArray(n)||j.isString(n))return 0===n.length;for(var t in n)if(j.has(n,t))return!1;return!0},j.isElement=function(n){return!(!n||1!==n.nodeType)},j.isArray=x||function(n){return"[object Array]"==l.call(n)},j.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){j["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),j.isArguments(arguments)||(j.isArguments=function(n){return!(!n||!j.has(n,"callee"))}),"function"!=typeof/./&&(j.isFunction=function(n){return"function"==typeof n}),j.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},j.isNaN=function(n){return j.isNumber(n)&&n!=+n},j.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},j.isNull=function(n){return null===n},j.isUndefined=function(n){return n===void 0},j.has=function(n,t){return f.call(n,t)},j.noConflict=function(){return n._=t,this},j.identity=function(n){return n},j.times=function(n,t,r){for(var e=Array(Math.max(0,n)),u=0;n>u;u++)e[u]=t.call(r,u);return e},j.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var I={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};I.unescape=j.invert(I.escape);var T={escape:new RegExp("["+j.keys(I.escape).join("")+"]","g"),unescape:new RegExp("("+j.keys(I.unescape).join("|")+")","g")};j.each(["escape","unescape"],function(n){j[n]=function(t){return null==t?"":(""+t).replace(T[n],function(t){return I[n][t]})}}),j.result=function(n,t){if(null==n)return void 0;var r=n[t];return j.isFunction(r)?r.call(n):r},j.mixin=function(n){A(j.functions(n),function(t){var r=j[t]=n[t];j.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(j,n))}})};var N=0;j.uniqueId=function(n){var t=++N+"";return n?n+t:t},j.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;j.template=function(n,t,r){var e;r=j.defaults({},r,j.templateSettings);var u=new RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(D,function(n){return"\\"+B[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=new Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,j);var c=function(n){return e.call(this,n,j)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},j.chain=function(n){return j(n).chain()};var z=function(n){return this._chain?j(n).chain():n};j.mixin(j),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];j.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];j.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),j.extend(j.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}.call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /test/vendor/qunit.js: -------------------------------------------------------------------------------- 1 | (function(k){function B(a){p(this,a);this.assertions=[];this.testNumber=++B.count}function P(){d.autorun=!0;d.currentModule&&s("moduleDone",e,{name:d.currentModule,failed:d.moduleStats.bad,passed:d.moduleStats.all-d.moduleStats.bad,total:d.moduleStats.all});delete d.previousModule;var a,c;a=n("qunit-banner");c=n("qunit-tests");var b=+new r-d.started,l=d.stats.all-d.stats.bad,g=["Tests completed in ",b," milliseconds.
    ",l," assertions of ",d.stats.all, 2 | " passed, ",d.stats.bad," failed."].join("");a&&(a.className=d.stats.bad?"qunit-fail":"qunit-pass");c&&(n("qunit-testresult").innerHTML=g);d.altertitle&&("undefined"!==typeof document&&document.title)&&(document.title=[d.stats.bad?"\u2716":"\u2714",document.title.replace(/^[\u2714\u2716] /i,"")].join(" "));if(d.reorder&&u.sessionStorage&&0===d.stats.bad)for(a=0;a&]/g,function(a){switch(a){case "'":return"'";case '"':return"""; 5 | case "<":return"<";case ">":return">";case "&":return"&"}}):""}function y(a,c){d.queue.push(a);d.autorun&&!d.blocking&&D(c)}function D(a){function c(){D(a)}var b=(new r).getTime();for(d.depth=d.depth?d.depth+1:1;d.queue.length&&!d.blocking;)if(!u.setTimeout||0>=d.updateRate||(new r).getTime()-b"+this.nameHtml);this.async&&e.stop();this.callbackStarted=+new r;if(d.notrycatch)this.callback.call(this.testEnvironment, 12 | e.assert),this.callbackRuntime=+new r-this.callbackStarted;else try{this.callback.call(this.testEnvironment,e.assert),this.callbackRuntime=+new r-this.callbackStarted}catch(c){this.callbackRuntime=+new r-this.callbackStarted,e.pushFailure("Died on test #"+(this.assertions.length+1)+" "+this.stack+": "+(c.message||c),C(c,0)),G(),d.blocking&&e.start()}},teardown:function(){d.current=this;if(d.notrycatch)"undefined"===typeof this.callbackRuntime&&(this.callbackRuntime=+new r-this.callbackStarted),this.testEnvironment.teardown.call(this.testEnvironment, 13 | e.assert);else{try{this.testEnvironment.teardown.call(this.testEnvironment,e.assert)}catch(a){e.pushFailure("Teardown failed on "+this.testName+": "+(a.message||a),C(a,1))}var c,b=d.pollution;G();c=M(d.pollution,b);0("+q+", "+b+", "+this.assertions.length+")";w(l,"click",function(){var a=l.parentNode.lastChild;(H(a,"qunit-collapsed")?N:I)(a,"qunit-collapsed")});w(l,"dblclick",function(a){a=a&&a.target?a.target:k.event.srcElement;if("span"===a.nodeName.toLowerCase()||"b"===a.nodeName.toLowerCase())a=a.parentNode;k.location&&"strong"===a.nodeName.toLowerCase()&& 17 | (k.location=e.url({testNumber:h.testNumber}))});a=document.createElement("span");a.className="runtime";a.innerHTML=this.runtime+" ms";g=n(this.id);g.className=q?"fail":"pass";g.removeChild(g.firstChild);b=g.firstChild;g.appendChild(l);g.appendChild(b);g.appendChild(a);g.appendChild(f)}else for(a=0;a";2===arguments.length&&(b=c,c=null);d.currentModule&&(g=""+m(d.currentModule)+": "+g);g=new B({nameHtml:g,testName:a,expected:c,async:e,callback:b,module:d.currentModule,moduleTestEnvironment:d.currentModuleTestEnvironment,stack:v(2)});F(g)&&g.queue()},expect:function(a){if(1===arguments.length)d.current.expected=a;else return d.current.expected},start:function(a){void 0===d.semaphore?e.begin(function(){E(function(){e.start(a)})}): 20 | (d.semaphore-=a||1,0d.semaphore?(d.semaphore=0,e.pushFailure("Called start() while already started (QUnit.config.semaphore was 0 already)",null,v(2))):u.setTimeout?E(function(){0"+m(c)+"";!a&&(b=v(2))&&(l.source=b,c+="
    Source:
    "+m(b)+"
    ");s("log",e,l);d.current.assertions.push({result:a,message:c})},equal:function(a,c,b){e.push(c==a,a,c,b)},notEqual:function(a,c,b){e.push(c!=a,a,c, 22 | b)},propEqual:function(a,c,b){a=A(a);c=A(c);e.push(e.equiv(a,c),a,c,b)},notPropEqual:function(a,c,b){a=A(a);c=A(c);e.push(!e.equiv(a,c),a,c,b)},deepEqual:function(a,c,b){e.push(e.equiv(a,c),a,c,b)},notDeepEqual:function(a,c,b){e.push(!e.equiv(a,c),a,c,b)},strictEqual:function(a,c,b){e.push(c===a,a,c,b)},notStrictEqual:function(a,c,b){e.push(c!==a,a,c,b)},"throws":function(a,c,b){var l,g=c,f=!1;"string"===typeof c&&(b=c,c=null);d.current.ignoreGlobalErrors=!0;try{a.call(d.current.testEnvironment)}catch(h){l= 23 | h}d.current.ignoreGlobalErrors=!1;l?(c?"regexp"===e.objectType(c)?f=c.test(S(l)):l instanceof c?f=!0:!0===c.call({},l)&&(g=null,f=!0):(f=!0,g=null),e.push(f,l,g,b)):e.pushFailure(b,null,"No exception was thrown.")}};p(e,z);e.raises=z["throws"];e.equals=function(){e.push(!1,!1,!1,"QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead")};e.same=function(){e.push(!1,!1,!1,"QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead")};(function(){function a(){} 24 | a.prototype=e;e=new a;e.constructor=a})();d={queue:[],blocking:!0,hidepassed:!1,reorder:!0,altertitle:!0,requireExpects:!1,urlConfig:[{id:"noglobals",label:"Check for Globals",tooltip:"Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings."},{id:"notrycatch",label:"No try-catch",tooltip:"Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings."}],modules:{},begin:[],done:[], 25 | log:[],testStart:[],testDone:[],moduleStart:[],moduleDone:[]};"undefined"===typeof exports&&(p(k,e.constructor.prototype),k.QUnit=e);(function(){var a,c=k.location||{search:"",protocol:"file:"},b=c.search.slice(1).split("&"),l=b.length,g={},f;if(b[0])for(a=0;a"+m(document.title)+"

      ";a=n("qunit-tests");c=n("qunit-banner");b=n("qunit-testresult");a&&(a.innerHTML="");c&&(c.className="");b&&b.parentNode.removeChild(b); 27 | a&&(b=document.createElement("p"),b.id="qunit-testresult",b.className="result",a.parentNode.insertBefore(b,a),b.innerHTML="Running...
       ")},reset:function(){var a=n("qunit-fixture");a&&(a.innerHTML=d.fixture)},triggerEvent:function(a,c,b){document.createEvent?(b=document.createEvent("MouseEvents"),b.initMouseEvent(c,!0,!0,a.ownerDocument.defaultView,0,0,0,0,0,!1,!1,!1,!1,0,null),a.dispatchEvent(b)):a.fireEvent&&a.fireEvent("on"+c)},is:function(a,c){return e.objectType(c)===a},objectType:function(a){if("undefined"=== 28 | typeof a)return"undefined";if(null===a)return"null";var c=O.call(a).match(/^\[object\s(.*)\]$/),c=c&&c[1]||"";switch(c){case "Number":return isNaN(a)?"nan":"number";case "String":case "Boolean":case "Array":case "Date":case "RegExp":case "Function":return c.toLowerCase()}if("object"===typeof a)return"object"},push:function(a,c,b,l){if(!d.current)throw Error("assertion outside test context, was "+v());var g={module:d.current.module,name:d.current.testName,result:a,message:l,actual:c,expected:b};l= 29 | m(l)||(a?"okay":"failed");l=""+l+"";if(!a){b=m(e.jsDump.parse(b));c=m(e.jsDump.parse(c));l+="";c!==b&&(l=l+("")+(""));if(c=v())g.source=c,l+="";l+="
      Expected:
      "+b+"
      Result:
      "+c+"
      Diff:
      "+e.diff(b,c)+"
      Source:
      "+m(c)+"
      "}s("log", 30 | e,g);d.current.assertions.push({result:!!a,message:l})},pushFailure:function(a,c,b){if(!d.current)throw Error("pushFailure() assertion outside test context, was "+v(2));var l={module:d.current.module,name:d.current.testName,result:!1,message:a};a=m(a)||"error";a=""+a+"";a+="";b&&(a+="");c&&(l.source=c,a+="");a+= 31 | "
      Result:
      "+m(b)+"
      Source:
      "+m(c)+"
      ";s("log",e,l);d.current.assertions.push({result:!1,message:a})},url:function(a){a=p(p({},e.urlParams),a);var c,b="?";for(c in a)t.call(a,c)&&(b+=encodeURIComponent(c)+"="+encodeURIComponent(a[c])+"&");return k.location.protocol+"//"+k.location.host+k.location.pathname+b.slice(0,-1)},extend:p,id:n,addEvent:w,addClass:I,hasClass:H,removeClass:N});p(e.constructor.prototype,{begin:x("begin"),done:x("done"),log:x("log"),testStart:x("testStart"),testDone:x("testDone"),moduleStart:x("moduleStart"), 32 | moduleDone:x("moduleDone")});if("undefined"===typeof document||"complete"===document.readyState)d.autorun=!0;e.load=function(){s("begin",e,{});var a,c,b,l,g,f,h,q;l=0;a=[];h=g="";l=p({},d);e.init();p(d,l);d.blocking=!1;l=d.urlConfig.length;for(b=0;b";for(b in d.modules)d.modules.hasOwnProperty(b)&&a.push(b);l=a.length;a.sort(function(a,b){return a.localeCompare(b)});g+="";if(a=n("qunit-userAgent"))a.innerHTML=navigator.userAgent;if(a=n("qunit-header"))a.innerHTML=""+a.innerHTML+" ";if(a=n("qunit-testrunner-toolbar")){c=document.createElement("input");c.type="checkbox";c.id="qunit-filter-pass";w(c,"click",function(){var a,b=document.getElementById("qunit-tests");c.checked?b.className+=" hidepass":(a=" "+b.className.replace(/[\n\t\r]/g," ")+" ",b.className= 35 | a.replace(/ hidepass /," "));u.sessionStorage&&(c.checked?sessionStorage.setItem("qunit-filter-passed-tests","true"):sessionStorage.removeItem("qunit-filter-passed-tests"))});if(d.hidepassed||u.sessionStorage&&sessionStorage.getItem("qunit-filter-passed-tests"))c.checked=!0,b=document.getElementById("qunit-tests"),b.className+=" hidepass";a.appendChild(c);b=document.createElement("label");b.setAttribute("for","qunit-filter-pass");b.setAttribute("title","Only show tests and assertions that fail. Stored in sessionStorage."); 36 | b.innerHTML="Hide passed tests";a.appendChild(b);b=document.createElement("span");b.innerHTML=h;h=b.getElementsByTagName("input");Q(h,"click",function(a){var b={};a=a.target||a.srcElement;b[a.name]=a.checked?!0:void 0;k.location=e.url(b)});a.appendChild(b);1b.length)return!0;var c=b[0],d=b[1];c===d?c=!0:null===c||null===d||"undefined"===typeof c||"undefined"=== 41 | typeof d||e.objectType(c)!==e.objectType(d)?c=!1:(d=[d,c],c=(c=e.objectType(c))?"function"===e.objectType(f[c])?f[c].apply(f,d):f[c]:void 0);return c&&a.apply(this,b.splice(1,b.length-1))}}();e.jsDump=function(){function a(a){return'"'+a.toString().replace(/"/g,'\\"')+'"'}function c(a){return a+""}function b(a,b,c){var d=f.separator(),e=f.indent(),g=f.indent(1);b.join&&(b=b.join(","+d+g));return b?[a,g+b,e+c].join(d):a+c}function d(a,c){var e=a.length,f=Array(e);for(this.up();e--;)f[e]=this.parse(a[e], 42 | void 0,c);this.down();return b("[",f,"]")}var g=/^function (\w+)/,f={parse:function(a,b,c){c=c||[];var d,e=this.parsers[b||this.typeOf(a)];b=typeof e;a:if(d=c,d.indexOf)d=d.indexOf(a);else{for(var f=0,g=d.length;f":"\n":this.HTML?" ": 44 | " "},indent:function(a){if(!this.multiline)return"";var b=this.indentChar;this.HTML&&(b=b.replace(/\t/g," ").replace(/ /g," "));return Array(this.depth+(a||0)).join(b)},up:function(a){this.depth+=a||1},down:function(a){this.depth-=a||1},setParser:function(a,b){this.parsers[a]=b},quote:a,literal:c,join:b,depth:1,parsers:{window:"[Window]",document:"[Document]",error:function(a){return'Error("'+a.message+'")'},unknown:"[Unknown]","null":"null",undefined:"undefined","function":function(a){var c= 45 | "function",d="name"in a?a.name:(g.exec(a)||[])[1];d&&(c+=" "+d);c=[c+"( ",e.jsDump.parse(a,"functionArgs"),"){"].join("");return b(c,e.jsDump.parse(a,"functionCode"),"}")},array:d,nodelist:d,arguments:d,object:function(a,c){var d=[],f,g,l,k;e.jsDump.up();f=[];for(g in a)f.push(g);f.sort();for(k=0;k",l=a.nodeName.toLowerCase(),k=f+l,m=a.attributes;if(m)for(c=0,b=m.length;c"+f[b]+k[b]+"";else{if(null==h[0].text)for(c=0;c"+f[c]+k[c]+"";for(b=0;b"+h[b]+m[b]+"";else{d="";for(c=h[b].row+1;c"+f[c]+k[c]+"";e+=" "+h[b].text+m[b]+d}}return e}}();"undefined"!==typeof exports&&p(exports,e.constructor.prototype)})(function(){return this}.call()); -------------------------------------------------------------------------------- /test/vendor/zepto.js: -------------------------------------------------------------------------------- 1 | /* Zepto v1.0rc1 - polyfill zepto event detect fx ajax form touch - zeptojs.com/license */ (function(a) { 2 | String.prototype.trim === a && (String.prototype.trim = function() { 3 | return this.replace(/^\s+/, "").replace(/\s+$/, "") 4 | }), Array.prototype.reduce === a && (Array.prototype.reduce = function(b) { 5 | if (this === void 0 || this === null) throw new TypeError; 6 | var c = Object(this), 7 | d = c.length >>> 0, 8 | e = 0, 9 | f; 10 | if (typeof b != "function") throw new TypeError; 11 | if (d == 0 && arguments.length == 1) throw new TypeError; 12 | if (arguments.length >= 2) f = arguments[1]; 13 | else do { 14 | if (e in c) { 15 | f = c[e++]; 16 | break 17 | } 18 | if (++e >= d) throw new TypeError 19 | } while (!0); 20 | while (e < d) e in c && (f = b.call(a, f, c[e], e, c)), e++; 21 | return f 22 | }) 23 | })(); 24 | var Zepto = function() { 25 | function A(a) { 26 | return v.call(a) == "[object Function]" 27 | } 28 | function B(a) { 29 | return a instanceof Object 30 | } 31 | function C(b) { 32 | var c, d; 33 | if (v.call(b) !== "[object Object]") return !1; 34 | d = A(b.constructor) && b.constructor.prototype; 35 | if (!d || !hasOwnProperty.call(d, "isPrototypeOf")) return !1; 36 | for (c in b); 37 | return c === a || hasOwnProperty.call(b, c) 38 | } 39 | function D(a) { 40 | return a instanceof Array 41 | } 42 | function E(a) { 43 | return typeof a.length == "number" 44 | } 45 | function F(b) { 46 | return b.filter(function(b) { 47 | return b !== a && b !== null 48 | }) 49 | } 50 | function G(a) { 51 | return a.length > 0 ? [].concat.apply([], a) : a 52 | } 53 | function H(a) { 54 | return a.replace(/::/g, "/").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/_/g, "-").toLowerCase() 55 | } 56 | function I(a) { 57 | return a in i ? i[a] : i[a] = new RegExp("(^|\\s)" + a + "(\\s|$)") 58 | } 59 | function J(a, b) { 60 | return typeof b == "number" && !k[H(a)] ? b + "px" : b 61 | } 62 | function K(a) { 63 | var b, c; 64 | return h[a] || (b = g.createElement(a), g.body.appendChild(b), c = j(b, "").getPropertyValue("display"), b.parentNode.removeChild(b), c == "none" && (c = "block"), h[a] = c), h[a] 65 | } 66 | function L(b, d) { 67 | return d === a ? c(b) : c(b).filter(d) 68 | } 69 | function M(a, b, c, d) { 70 | return A(b) ? b.call(a, c, d) : b 71 | } 72 | function N(a, b, d) { 73 | var e = a % 2 ? b : b.parentNode; 74 | e ? e.insertBefore(d, a ? a == 1 ? e.firstChild : a == 2 ? b : null : b.nextSibling) : c(d).remove() 75 | } 76 | function O(a, b) { 77 | b(a); 78 | for (var c in a.childNodes) O(a.childNodes[c], b) 79 | } 80 | var a, b, c, d, e = [], 81 | f = e.slice, 82 | g = window.document, 83 | h = {}, 84 | i = {}, 85 | j = g.defaultView.getComputedStyle, 86 | k = { 87 | "column-count": 1, 88 | columns: 1, 89 | "font-weight": 1, 90 | "line-height": 1, 91 | opacity: 1, 92 | "z-index": 1, 93 | zoom: 1 94 | }, 95 | l = /^\s*<(\w+|!)[^>]*>/, 96 | m = [1, 3, 8, 9, 11], 97 | n = ["after", "prepend", "before", "append"], 98 | o = g.createElement("table"), 99 | p = g.createElement("tr"), 100 | q = { 101 | tr: g.createElement("tbody"), 102 | tbody: o, 103 | thead: o, 104 | tfoot: o, 105 | td: p, 106 | th: p, 107 | "*": g.createElement("div") 108 | }, 109 | r = /complete|loaded|interactive/, 110 | s = /^\.([\w-]+)$/, 111 | t = /^#([\w-]+)$/, 112 | u = /^[\w-]+$/, 113 | v = {}.toString, 114 | w = {}, 115 | x, y, z = g.createElement("div"); 116 | return w.matches = function(a, b) { 117 | if (!a || a.nodeType !== 1) return !1; 118 | var c = a.webkitMatchesSelector || a.mozMatchesSelector || a.oMatchesSelector || a.matchesSelector; 119 | if (c) return c.call(a, b); 120 | var d, e = a.parentNode, 121 | f = !e; 122 | return f && (e = z).appendChild(a), d = ~w.qsa(e, b).indexOf(a), f && z.removeChild(a), d 123 | }, x = function(a) { 124 | return a.replace(/-+(.)?/g, function(a, b) { 125 | return b ? b.toUpperCase() : "" 126 | }) 127 | }, y = function(a) { 128 | return a.filter(function(b, c) { 129 | return a.indexOf(b) == c 130 | }) 131 | }, w.fragment = function(b, d) { 132 | d === a && (d = l.test(b) && RegExp.$1), d in q || (d = "*"); 133 | var e = q[d]; 134 | return e.innerHTML = "" + b, c.each(f.call(e.childNodes), function() { 135 | e.removeChild(this) 136 | }) 137 | }, w.Z = function(a, b) { 138 | return a = a || [], a.__proto__ = arguments.callee.prototype, a.selector = b || "", a 139 | }, w.isZ = function(a) { 140 | return a instanceof w.Z 141 | }, w.init = function(b, d) { 142 | if (!b) return w.Z(); 143 | if (A(b)) return c(g).ready(b); 144 | if (w.isZ(b)) return b; 145 | var e; 146 | if (D(b)) e = F(b); 147 | else if (C(b)) e = [c.extend({}, b)], b = null; 148 | else if (m.indexOf(b.nodeType) >= 0 || b === window) e = [b], b = null; 149 | else if (l.test(b)) e = w.fragment(b.trim(), RegExp.$1), b = null; 150 | else { 151 | if (d !== a) return c(d).find(b); 152 | e = w.qsa(g, b) 153 | } 154 | return w.Z(e, b) 155 | }, c = function(a, b) { 156 | return w.init(a, b) 157 | }, c.extend = function(c) { 158 | return f.call(arguments, 1).forEach(function(d) { 159 | for (b in d) d[b] !== a && (c[b] = d[b]) 160 | }), c 161 | }, w.qsa = function(a, b) { 162 | var c; 163 | return a === g && t.test(b) ? (c = a.getElementById(RegExp.$1)) ? [c] : e : a.nodeType !== 1 && a.nodeType !== 9 ? e : f.call(s.test(b) ? a.getElementsByClassName(RegExp.$1) : u.test(b) ? a.getElementsByTagName(b) : a.querySelectorAll(b)) 164 | }, c.isFunction = A, c.isObject = B, c.isArray = D, c.isPlainObject = C, c.inArray = function(a, b, c) { 165 | return e.indexOf.call(b, a, c) 166 | }, c.trim = function(a) { 167 | return a.trim() 168 | }, c.uuid = 0, c.map = function(a, b) { 169 | var c, d = [], 170 | e, f; 171 | if (E(a)) for (e = 0; e < a.length; e++) c = b(a[e], e), c != null && d.push(c); 172 | else for (f in a) c = b(a[f], f), c != null && d.push(c); 173 | return G(d) 174 | }, c.each = function(a, b) { 175 | var c, d; 176 | if (E(a)) { 177 | for (c = 0; c < a.length; c++) if (b.call(a[c], c, a[c]) === !1) return a 178 | } else for (d in a) if (b.call(a[d], d, a[d]) === !1) return a; 179 | return a 180 | }, c.fn = { 181 | forEach: e.forEach, 182 | reduce: e.reduce, 183 | push: e.push, 184 | indexOf: e.indexOf, 185 | concat: e.concat, 186 | map: function(a) { 187 | return c.map(this, function(b, c) { 188 | return a.call(b, c, b) 189 | }) 190 | }, 191 | slice: function() { 192 | return c(f.apply(this, arguments)) 193 | }, 194 | ready: function(a) { 195 | return r.test(g.readyState) ? a(c) : g.addEventListener("DOMContentLoaded", function() { 196 | a(c) 197 | }, !1), this 198 | }, 199 | get: function(b) { 200 | return b === a ? f.call(this) : this[b] 201 | }, 202 | toArray: function() { 203 | return this.get() 204 | }, 205 | size: function() { 206 | return this.length 207 | }, 208 | remove: function() { 209 | return this.each(function() { 210 | this.parentNode != null && this.parentNode.removeChild(this) 211 | }) 212 | }, 213 | each: function(a) { 214 | return this.forEach(function(b, c) { 215 | a.call(b, c, b) 216 | }), this 217 | }, 218 | filter: function(a) { 219 | return c([].filter.call(this, function(b) { 220 | return w.matches(b, a) 221 | })) 222 | }, 223 | add: function(a, b) { 224 | return c(y(this.concat(c(a, b)))) 225 | }, 226 | is: function(a) { 227 | return this.length > 0 && w.matches(this[0], a) 228 | }, 229 | not: function(b) { 230 | var d = []; 231 | if (A(b) && b.call !== a) this.each(function(a) { 232 | b.call(this, a) || d.push(this) 233 | }); 234 | else { 235 | var e = typeof b == "string" ? this.filter(b) : E(b) && A(b.item) ? f.call(b) : c(b); 236 | this.forEach(function(a) { 237 | e.indexOf(a) < 0 && d.push(a) 238 | }) 239 | } 240 | return c(d) 241 | }, 242 | eq: function(a) { 243 | return a === -1 ? this.slice(a) : this.slice(a, +a + 1) 244 | }, 245 | first: function() { 246 | var a = this[0]; 247 | return a && !B(a) ? a : c(a) 248 | }, 249 | last: function() { 250 | var a = this[this.length - 1]; 251 | return a && !B(a) ? a : c(a) 252 | }, 253 | find: function(a) { 254 | var b; 255 | return this.length == 1 ? b = w.qsa(this[0], a) : b = this.map(function() { 256 | return w.qsa(this, a) 257 | }), c(b) 258 | }, 259 | closest: function(a, b) { 260 | var d = this[0]; 261 | while (d && !w.matches(d, a)) d = d !== b && d !== g && d.parentNode; 262 | return c(d) 263 | }, 264 | parents: function(a) { 265 | var b = [], 266 | d = this; 267 | while (d.length > 0) d = c.map(d, function(a) { 268 | if ((a = a.parentNode) && a !== g && b.indexOf(a) < 0) return b.push(a), a 269 | }); 270 | return L(b, a) 271 | }, 272 | parent: function(a) { 273 | return L(y(this.pluck("parentNode")), a) 274 | }, 275 | children: function(a) { 276 | return L(this.map(function() { 277 | return f.call(this.children) 278 | }), a) 279 | }, 280 | siblings: function(a) { 281 | return L(this.map(function(a, b) { 282 | return f.call(b.parentNode.children).filter(function(a) { 283 | return a !== b 284 | }) 285 | }), a) 286 | }, 287 | empty: function() { 288 | return this.each(function() { 289 | this.innerHTML = "" 290 | }) 291 | }, 292 | pluck: function(a) { 293 | return this.map(function() { 294 | return this[a] 295 | }) 296 | }, 297 | show: function() { 298 | return this.each(function() { 299 | this.style.display == "none" && (this.style.display = null), j(this, "").getPropertyValue("display") == "none" && (this.style.display = K(this.nodeName)) 300 | }) 301 | }, 302 | replaceWith: function(a) { 303 | return this.before(a).remove() 304 | }, 305 | wrap: function(a) { 306 | return this.each(function() { 307 | c(this).wrapAll(c(a)[0].cloneNode(!1)) 308 | }) 309 | }, 310 | wrapAll: function(a) { 311 | return this[0] && (c(this[0]).before(a = c(a)), a.append(this)), this 312 | }, 313 | unwrap: function() { 314 | return this.parent().each(function() { 315 | c(this).replaceWith(c(this).children()) 316 | }), this 317 | }, 318 | clone: function() { 319 | return c(this.map(function() { 320 | return this.cloneNode(!0) 321 | })) 322 | }, 323 | hide: function() { 324 | return this.css("display", "none") 325 | }, 326 | toggle: function(b) { 327 | return (b === a ? this.css("display") == "none" : b) ? this.show() : this.hide() 328 | }, 329 | prev: function() { 330 | return c(this.pluck("previousElementSibling")) 331 | }, 332 | next: function() { 333 | return c(this.pluck("nextElementSibling")) 334 | }, 335 | html: function(b) { 336 | return b === a ? this.length > 0 ? this[0].innerHTML : null : this.each(function(a) { 337 | var d = this.innerHTML; 338 | c(this).empty().append(M(this, b, a, d)) 339 | }) 340 | }, 341 | text: function(b) { 342 | return b === a ? this.length > 0 ? this[0].textContent : null : this.each(function() { 343 | this.textContent = b 344 | }) 345 | }, 346 | attr: function(c, d) { 347 | var e; 348 | return typeof c == "string" && d === a ? this.length == 0 || this[0].nodeType !== 1 ? a : c == "value" && this[0].nodeName == "INPUT" ? this.val() : !(e = this[0].getAttribute(c)) && c in this[0] ? this[0][c] : e : this.each(function(a) { 349 | if (this.nodeType !== 1) return; 350 | if (B(c)) for (b in c) this.setAttribute(b, c[b]); 351 | else this.setAttribute(c, M(this, d, a, this.getAttribute(c))) 352 | }) 353 | }, 354 | removeAttr: function(a) { 355 | return this.each(function() { 356 | this.nodeType === 1 && this.removeAttribute(a) 357 | }) 358 | }, 359 | prop: function(b, c) { 360 | return c === a ? this[0] ? this[0][b] : a : this.each(function(a) { 361 | this[b] = M(this, c, a, this[b]) 362 | }) 363 | }, 364 | data: function(b, c) { 365 | var d = this.attr("data-" + H(b), c); 366 | return d !== null ? d : a 367 | }, 368 | val: function(b) { 369 | return b === a ? this.length > 0 ? this[0].value : a : this.each(function(a) { 370 | this.value = M(this, b, a, this.value) 371 | }) 372 | }, 373 | offset: function() { 374 | if (this.length == 0) return null; 375 | var a = this[0].getBoundingClientRect(); 376 | return { 377 | left: a.left + window.pageXOffset, 378 | top: a.top + window.pageYOffset, 379 | width: a.width, 380 | height: a.height 381 | } 382 | }, 383 | css: function(c, d) { 384 | if (d === a && typeof c == "string") return this.length == 0 ? a : this[0].style[x(c)] || j(this[0], "").getPropertyValue(c); 385 | var e = ""; 386 | for (b in c) typeof c[b] == "string" && c[b] == "" ? this.each(function() { 387 | this.style.removeProperty(H(b)) 388 | }) : e += H(b) + ":" + J(b, c[b]) + ";"; 389 | return typeof c == "string" && (d == "" ? this.each(function() { 390 | this.style.removeProperty(H(c)) 391 | }) : e = H(c) + ":" + J(c, d)), this.each(function() { 392 | this.style.cssText += ";" + e 393 | }) 394 | }, 395 | index: function(a) { 396 | return a ? this.indexOf(c(a)[0]) : this.parent().children().indexOf(this[0]) 397 | }, 398 | hasClass: function(a) { 399 | return this.length < 1 ? !1 : I(a).test(this[0].className) 400 | }, 401 | addClass: function(a) { 402 | return this.each(function(b) { 403 | d = []; 404 | var e = this.className, 405 | f = M(this, a, b, e); 406 | f.split(/\s+/g).forEach(function(a) { 407 | c(this).hasClass(a) || d.push(a) 408 | }, this), d.length && (this.className += (e ? " " : "") + d.join(" ")) 409 | }) 410 | }, 411 | removeClass: function(b) { 412 | return this.each(function(c) { 413 | if (b === a) return this.className = ""; 414 | d = this.className, M(this, b, c, d).split(/\s+/g).forEach(function(a) { 415 | d = d.replace(I(a), " ") 416 | }), this.className = d.trim() 417 | }) 418 | }, 419 | toggleClass: function(b, d) { 420 | return this.each(function(e) { 421 | var f = M(this, b, e, this.className); 422 | (d === a ? !c(this).hasClass(f) : d) ? c(this).addClass(f) : c(this).removeClass(f) 423 | }) 424 | } 425 | }, ["width", "height"].forEach(function(b) { 426 | c.fn[b] = function(d) { 427 | var e, f = b.replace(/./, function(a) { 428 | return a[0].toUpperCase() 429 | }); 430 | return d === a ? this[0] == window ? window["inner" + f] : this[0] == g ? g.documentElement["offset" + f] : (e = this.offset()) && e[b] : this.each(function(a) { 431 | var e = c(this); 432 | e.css(b, M(this, d, a, e[b]())) 433 | }) 434 | } 435 | }), n.forEach(function(a, b) { 436 | c.fn[a] = function() { 437 | var a = c.map(arguments, function(a) { 438 | return B(a) ? a : w.fragment(a) 439 | }); 440 | if (a.length < 1) return this; 441 | var d = this.length, 442 | e = d > 1, 443 | f = b < 2; 444 | return this.each(function(c, g) { 445 | for (var h = 0; h < a.length; h++) { 446 | var i = a[f ? a.length - h - 1 : h]; 447 | O(i, function(a) { 448 | a.nodeName != null && a.nodeName.toUpperCase() === "SCRIPT" && (!a.type || a.type === "text/javascript") && window.eval.call(window, a.innerHTML) 449 | }), e && c < d - 1 && (i = i.cloneNode(!0)), N(b, g, i) 450 | } 451 | }) 452 | }, c.fn[b % 2 ? a + "To" : "insert" + (b ? "Before" : "After")] = function(b) { 453 | return c(b)[a](this), this 454 | } 455 | }), w.Z.prototype = c.fn, w.camelize = x, w.uniq = y, c.zepto = w, c 456 | }(); 457 | window.Zepto = Zepto, "$" in window || (window.$ = Zepto), function(a) { 458 | function f(a) { 459 | return a._zid || (a._zid = d++) 460 | } 461 | function g(a, b, d, e) { 462 | b = h(b); 463 | if (b.ns) var g = i(b.ns); 464 | return (c[f(a)] || []).filter(function(a) { 465 | return a && (!b.e || a.e == b.e) && (!b.ns || g.test(a.ns)) && (!d || f(a.fn) === f(d)) && (!e || a.sel == e) 466 | }) 467 | } 468 | function h(a) { 469 | var b = ("" + a).split("."); 470 | return { 471 | e: b[0], 472 | ns: b.slice(1).sort().join(" ") 473 | } 474 | } 475 | function i(a) { 476 | return new RegExp("(?:^| )" + a.replace(" ", " .* ?") + "(?: |$)") 477 | } 478 | function j(b, c, d) { 479 | a.isObject(b) ? a.each(b, d) : b.split(/\s/).forEach(function(a) { 480 | d(a, c) 481 | }) 482 | } 483 | function k(b, d, e, g, i, k) { 484 | k = !! k; 485 | var l = f(b), 486 | m = c[l] || (c[l] = []); 487 | j(d, e, function(c, d) { 488 | var e = i && i(d, c), 489 | f = e || d, 490 | j = function(a) { 491 | var c = f.apply(b, [a].concat(a.data)); 492 | return c === !1 && a.preventDefault(), c 493 | }, 494 | l = a.extend(h(c), { 495 | fn: d, 496 | proxy: j, 497 | sel: g, 498 | del: e, 499 | i: m.length 500 | }); 501 | m.push(l), b.addEventListener(l.e, j, k) 502 | }) 503 | } 504 | function l(a, b, d, e) { 505 | var h = f(a); 506 | j(b || "", d, function(b, d) { 507 | g(a, b, d, e).forEach(function(b) { 508 | delete c[h][b.i], a.removeEventListener(b.e, b.proxy, !1) 509 | }) 510 | }) 511 | } 512 | function p(b) { 513 | var c = a.extend({ 514 | originalEvent: b 515 | }, b); 516 | return a.each(o, function(a, d) { 517 | c[a] = function() { 518 | return this[d] = m, b[a].apply(b, arguments) 519 | }, c[d] = n 520 | }), c 521 | } 522 | function q(a) { 523 | if (!("defaultPrevented" in a)) { 524 | a.defaultPrevented = !1; 525 | var b = a.preventDefault; 526 | a.preventDefault = function() { 527 | this.defaultPrevented = !0, b.call(this) 528 | } 529 | } 530 | } 531 | var b = a.zepto.qsa, 532 | c = {}, 533 | d = 1, 534 | e = {}; 535 | e.click = e.mousedown = e.mouseup = e.mousemove = "MouseEvents", a.event = { 536 | add: k, 537 | remove: l 538 | }, a.proxy = function(b, c) { 539 | if (a.isFunction(b)) { 540 | var d = function() { 541 | return b.apply(c, arguments) 542 | }; 543 | return d._zid = f(b), d 544 | } 545 | if (typeof c == "string") return a.proxy(b[c], b); 546 | throw new TypeError("expected function") 547 | }, a.fn.bind = function(a, b) { 548 | return this.each(function() { 549 | k(this, a, b) 550 | }) 551 | }, a.fn.unbind = function(a, b) { 552 | return this.each(function() { 553 | l(this, a, b) 554 | }) 555 | }, a.fn.one = function(a, b) { 556 | return this.each(function(c, d) { 557 | k(this, a, b, null, function(a, b) { 558 | return function() { 559 | var c = a.apply(d, arguments); 560 | return l(d, b, a), c 561 | } 562 | }) 563 | }) 564 | }; 565 | var m = function() { 566 | return !0 567 | }, 568 | n = function() { 569 | return !1 570 | }, 571 | o = { 572 | preventDefault: "isDefaultPrevented", 573 | stopImmediatePropagation: "isImmediatePropagationStopped", 574 | stopPropagation: "isPropagationStopped" 575 | }; 576 | a.fn.delegate = function(b, c, d) { 577 | var e = !1; 578 | if (c == "blur" || c == "focus") a.iswebkit ? c = c == "blur" ? "focusout" : c == "focus" ? "focusin" : c : e = !0; 579 | return this.each(function(f, g) { 580 | k(g, c, d, b, function(c) { 581 | return function(d) { 582 | var e, f = a(d.target).closest(b, g).get(0); 583 | if (f) return e = a.extend(p(d), { 584 | currentTarget: f, 585 | liveFired: g 586 | }), c.apply(f, [e].concat([].slice.call(arguments, 1))) 587 | } 588 | }, e) 589 | }) 590 | }, a.fn.undelegate = function(a, b, c) { 591 | return this.each(function() { 592 | l(this, b, c, a) 593 | }) 594 | }, a.fn.live = function(b, c) { 595 | return a(document.body).delegate(this.selector, b, c), this 596 | }, a.fn.die = function(b, c) { 597 | return a(document.body).undelegate(this.selector, b, c), this 598 | }, a.fn.on = function(b, c, d) { 599 | return c == undefined || a.isFunction(c) ? this.bind(b, c) : this.delegate(c, b, d) 600 | }, a.fn.off = function(b, c, d) { 601 | return c == undefined || a.isFunction(c) ? this.unbind(b, c) : this.undelegate(c, b, d) 602 | }, a.fn.trigger = function(b, c) { 603 | return typeof b == "string" && (b = a.Event(b)), q(b), b.data = c, this.each(function() { 604 | "dispatchEvent" in this && this.dispatchEvent(b) 605 | }) 606 | }, a.fn.triggerHandler = function(b, c) { 607 | var d, e; 608 | return this.each(function(f, h) { 609 | d = p(typeof b == "string" ? a.Event(b) : b), d.data = c, d.target = h, a.each(g(h, b.type || b), function(a, b) { 610 | e = b.proxy(d); 611 | if (d.isImmediatePropagationStopped()) return !1 612 | }) 613 | }), e 614 | }, "focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout change select keydown keypress keyup error".split(" ").forEach(function(b) { 615 | a.fn[b] = function(a) { 616 | return this.bind(b, a) 617 | } 618 | }), ["focus", "blur"].forEach(function(b) { 619 | a.fn[b] = function(a) { 620 | if (a) this.bind(b, a); 621 | else if (this.length) try { 622 | this.get(0)[b]() 623 | } catch (c) {} 624 | return this 625 | } 626 | }), a.Event = function(a, b) { 627 | var c = document.createEvent(e[a] || "Events"), 628 | d = !0; 629 | if (b) for (var f in b) f == "bubbles" ? d = !! b[f] : c[f] = b[f]; 630 | return c.initEvent(a, d, !0, null, null, null, null, null, null, null, null, null, null, null, null), c 631 | } 632 | }(Zepto), function(a) { 633 | function b(a) { 634 | var b = this.os = {}, 635 | c = this.browser = {}, 636 | d = a.match(/WebKit\/([\d.]+)/), 637 | e = a.match(/(Android)\s+([\d.]+)/), 638 | f = a.match(/(iPad).*OS\s([\d_]+)/), 639 | g = !f && a.match(/(iPhone\sOS)\s([\d_]+)/), 640 | h = a.match(/(webOS|hpwOS)[\s\/]([\d.]+)/), 641 | i = h && a.match(/TouchPad/), 642 | j = a.match(/Kindle\/([\d.]+)/), 643 | k = a.match(/Silk\/([\d._]+)/), 644 | l = a.match(/(BlackBerry).*Version\/([\d.]+)/); 645 | if (c.webkit = !! d) c.version = d[1]; 646 | e && (b.android = !0, b.version = e[2]), g && (b.ios = b.iphone = !0, b.version = g[2].replace(/_/g, ".")), f && (b.ios = b.ipad = !0, b.version = f[2].replace(/_/g, ".")), h && (b.webos = !0, b.version = h[2]), i && (b.touchpad = !0), l && (b.blackberry = !0, b.version = l[2]), j && (b.kindle = !0, b.version = j[1]), k && (c.silk = !0, c.version = k[1]), !k && b.android && a.match(/Kindle Fire/) && (c.silk = !0) 647 | } 648 | b.call(a, navigator.userAgent), a.__detect = b 649 | }(Zepto), function(a, b) { 650 | function l(a) { 651 | return a.toLowerCase() 652 | } 653 | function m(a) { 654 | return d ? d + a : l(a) 655 | } 656 | var c = "", 657 | d, e, f, g = { 658 | Webkit: "webkit", 659 | Moz: "", 660 | O: "o", 661 | ms: "MS" 662 | }, 663 | h = window.document, 664 | i = h.createElement("div"), 665 | j = /^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i, 666 | k = {}; 667 | a.each(g, function(a, e) { 668 | if (i.style[a + "TransitionProperty"] !== b) return c = "-" + l(a) + "-", d = e, !1 669 | }), k[c + "transition-property"] = k[c + "transition-duration"] = k[c + "transition-timing-function"] = k[c + "animation-name"] = k[c + "animation-duration"] = "", a.fx = { 670 | off: d === b && i.style.transitionProperty === b, 671 | cssPrefix: c, 672 | transitionEnd: m("TransitionEnd"), 673 | animationEnd: m("AnimationEnd") 674 | }, a.fn.animate = function(b, c, d, e) { 675 | return a.isObject(c) && (d = c.easing, e = c.complete, c = c.duration), c && (c /= 1e3), this.anim(b, c, d, e) 676 | }, a.fn.anim = function(d, e, f, g) { 677 | var h, i = {}, 678 | l, m = this, 679 | n, o = a.fx.transitionEnd; 680 | e === b && (e = .4), a.fx.off && (e = 0); 681 | if (typeof d == "string") i[c + "animation-name"] = d, i[c + "animation-duration"] = e + "s", o = a.fx.animationEnd; 682 | else { 683 | for (l in d) j.test(l) ? (h || (h = []), h.push(l + "(" + d[l] + ")")) : i[l] = d[l]; 684 | h && (i[c + "transform"] = h.join(" ")), !a.fx.off && typeof d == "object" && (i[c + "transition-property"] = Object.keys(d).join(", "), i[c + "transition-duration"] = e + "s", i[c + "transition-timing-function"] = f || "linear") 685 | } 686 | return n = function(b) { 687 | if (typeof b != "undefined") { 688 | if (b.target !== b.currentTarget) return; 689 | a(b.target).unbind(o, arguments.callee) 690 | } 691 | a(this).css(k), g && g.call(this) 692 | }, e > 0 && this.bind(o, n), setTimeout(function() { 693 | m.css(i), e <= 0 && setTimeout(function() { 694 | m.each(function() { 695 | n.call(this) 696 | }) 697 | }, 0) 698 | }, 0), this 699 | }, i = null 700 | }(Zepto), function($) { 701 | function triggerAndReturn(a, b, c) { 702 | var d = $.Event(b); 703 | return $(a).trigger(d, c), !d.defaultPrevented 704 | } 705 | function triggerGlobal(a, b, c, d) { 706 | if (a.global) return triggerAndReturn(b || document, c, d) 707 | } 708 | function ajaxStart(a) { 709 | a.global && $.active++ === 0 && triggerGlobal(a, null, "ajaxStart") 710 | } 711 | function ajaxStop(a) { 712 | a.global && !--$.active && triggerGlobal(a, null, "ajaxStop") 713 | } 714 | function ajaxBeforeSend(a, b) { 715 | var c = b.context; 716 | if (b.beforeSend.call(c, a, b) === !1 || triggerGlobal(b, c, "ajaxBeforeSend", [a, b]) === !1) return !1; 717 | triggerGlobal(b, c, "ajaxSend", [a, b]) 718 | } 719 | function ajaxSuccess(a, b, c) { 720 | var d = c.context, 721 | e = "success"; 722 | c.success.call(d, a, e, b), triggerGlobal(c, d, "ajaxSuccess", [b, c, a]), ajaxComplete(e, b, c) 723 | } 724 | function ajaxError(a, b, c, d) { 725 | var e = d.context; 726 | d.error.call(e, c, b, a), triggerGlobal(d, e, "ajaxError", [c, d, a]), ajaxComplete(b, c, d) 727 | } 728 | function ajaxComplete(a, b, c) { 729 | var d = c.context; 730 | c.complete.call(d, b, a), triggerGlobal(c, d, "ajaxComplete", [b, c]), ajaxStop(c) 731 | } 732 | function empty() {} 733 | function mimeToDataType(a) { 734 | return a && (a == htmlType ? "html" : a == jsonType ? "json" : scriptTypeRE.test(a) ? "script" : xmlTypeRE.test(a) && "xml") || "text" 735 | } 736 | function appendQuery(a, b) { 737 | return (a + "&" + b).replace(/[&?]{1,2}/, "?") 738 | } 739 | function serializeData(a) { 740 | isObject(a.data) && (a.data = $.param(a.data)), a.data && (!a.type || a.type.toUpperCase() == "GET") && (a.url = appendQuery(a.url, a.data)) 741 | } 742 | function serialize(a, b, c, d) { 743 | var e = $.isArray(b); 744 | $.each(b, function(b, f) { 745 | d && (b = c ? d : d + "[" + (e ? "" : b) + "]"), !d && e ? a.add(f.name, f.value) : (c ? $.isArray(f) : isObject(f)) ? serialize(a, f, c, b) : a.add(b, f) 746 | }) 747 | } 748 | var jsonpID = 0, 749 | isObject = $.isObject, 750 | document = window.document, 751 | key, name, rscript = /)<[^<]*)*<\/script>/gi, 752 | scriptTypeRE = /^(?:text|application)\/javascript/i, 753 | xmlTypeRE = /^(?:text|application)\/xml/i, 754 | jsonType = "application/json", 755 | htmlType = "text/html", 756 | blankRE = /^\s*$/; 757 | $.active = 0, $.ajaxJSONP = function(a) { 758 | var b = "jsonp" + ++jsonpID, 759 | c = document.createElement("script"), 760 | d = function() { 761 | $(c).remove(), b in window && (window[b] = empty), ajaxComplete("abort", e, a) 762 | }, 763 | e = { 764 | abort: d 765 | }, 766 | f; 767 | return a.error && (c.onerror = function() { 768 | e.abort(), a.error() 769 | }), window[b] = function(d) { 770 | clearTimeout(f), $(c).remove(), delete window[b], ajaxSuccess(d, e, a) 771 | }, serializeData(a), c.src = a.url.replace(/=\?/, "=" + b), $("head").append(c), a.timeout > 0 && (f = setTimeout(function() { 772 | e.abort(), ajaxComplete("timeout", e, a) 773 | }, a.timeout)), e 774 | }, $.ajaxSettings = { 775 | type: "GET", 776 | beforeSend: empty, 777 | success: empty, 778 | error: empty, 779 | complete: empty, 780 | context: null, 781 | global: !0, 782 | xhr: function() { 783 | return new window.XMLHttpRequest 784 | }, 785 | accepts: { 786 | script: "text/javascript, application/javascript", 787 | json: jsonType, 788 | xml: "application/xml, text/xml", 789 | html: htmlType, 790 | text: "text/plain" 791 | }, 792 | crossDomain: !1, 793 | timeout: 0 794 | }, $.ajax = function(options) { 795 | var settings = $.extend({}, options || {}); 796 | for (key in $.ajaxSettings) settings[key] === undefined && (settings[key] = $.ajaxSettings[key]); 797 | ajaxStart(settings), settings.crossDomain || (settings.crossDomain = /^([\w-]+:)?\/\/([^\/]+)/.test(settings.url) && RegExp.$2 != window.location.host); 798 | var dataType = settings.dataType, 799 | hasPlaceholder = /=\?/.test(settings.url); 800 | if (dataType == "jsonp" || hasPlaceholder) return hasPlaceholder || (settings.url = appendQuery(settings.url, "callback=?")), $.ajaxJSONP(settings); 801 | settings.url || (settings.url = window.location.toString()), serializeData(settings); 802 | var mime = settings.accepts[dataType], 803 | baseHeaders = {}, 804 | protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol, 805 | xhr = $.ajaxSettings.xhr(), 806 | abortTimeout; 807 | settings.crossDomain || (baseHeaders["X-Requested-With"] = "XMLHttpRequest"), mime && (baseHeaders.Accept = mime, mime.indexOf(",") > -1 && (mime = mime.split(",", 2)[0]), xhr.overrideMimeType && xhr.overrideMimeType(mime)); 808 | if (settings.contentType || settings.data && settings.type.toUpperCase() != "GET") baseHeaders["Content-Type"] = settings.contentType || "application/x-www-form-urlencoded"; 809 | settings.headers = $.extend(baseHeaders, settings.headers || {}), xhr.onreadystatechange = function() { 810 | if (xhr.readyState == 4) { 811 | clearTimeout(abortTimeout); 812 | var result, error = !1; 813 | if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304 || xhr.status == 0 && protocol == "file:") { 814 | dataType = dataType || mimeToDataType(xhr.getResponseHeader("content-type")), result = xhr.responseText; 815 | try { 816 | dataType == "script" ? (1, eval)(result) : dataType == "xml" ? result = xhr.responseXML : dataType == "json" && (result = blankRE.test(result) ? null : JSON.parse(result)) 817 | } catch (e) { 818 | error = e 819 | } 820 | error ? ajaxError(error, "parsererror", xhr, settings) : ajaxSuccess(result, xhr, settings) 821 | } else ajaxError(null, "error", xhr, settings) 822 | } 823 | }; 824 | var async = "async" in settings ? settings.async : !0; 825 | xhr.open(settings.type, settings.url, async); 826 | for (name in settings.headers) xhr.setRequestHeader(name, settings.headers[name]); 827 | return ajaxBeforeSend(xhr, settings) === !1 ? (xhr.abort(), !1) : (settings.timeout > 0 && (abortTimeout = setTimeout(function() { 828 | xhr.onreadystatechange = empty, xhr.abort(), ajaxError(null, "timeout", xhr, settings) 829 | }, settings.timeout)), xhr.send(settings.data ? settings.data : null), xhr) 830 | }, $.get = function(a, b) { 831 | return $.ajax({ 832 | url: a, 833 | success: b 834 | }) 835 | }, $.post = function(a, b, c, d) { 836 | return $.isFunction(b) && (d = d || c, c = b, b = null), $.ajax({ 837 | type: "POST", 838 | url: a, 839 | data: b, 840 | success: c, 841 | dataType: d 842 | }) 843 | }, $.getJSON = function(a, b) { 844 | return $.ajax({ 845 | url: a, 846 | success: b, 847 | dataType: "json" 848 | }) 849 | }, $.fn.load = function(a, b) { 850 | if (!this.length) return this; 851 | var c = this, 852 | d = a.split(/\s/), 853 | e; 854 | return d.length > 1 && (a = d[0], e = d[1]), $.get(a, function(a) { 855 | c.html(e ? $(document.createElement("div")).html(a.replace(rscript, "")).find(e).html() : a), b && b.call(c) 856 | }), this 857 | }; 858 | var escape = encodeURIComponent; 859 | $.param = function(a, b) { 860 | var c = []; 861 | return c.add = function(a, b) { 862 | this.push(escape(a) + "=" + escape(b)) 863 | }, serialize(c, a, b), c.join("&").replace("%20", "+") 864 | } 865 | }(Zepto), function(a) { 866 | a.fn.serializeArray = function() { 867 | var b = [], 868 | c; 869 | return a(Array.prototype.slice.call(this.get(0).elements)).each(function() { 870 | c = a(this); 871 | var d = c.attr("type"); 872 | this.nodeName.toLowerCase() != "fieldset" && !this.disabled && d != "submit" && d != "reset" && d != "button" && (d != "radio" && d != "checkbox" || this.checked) && b.push({ 873 | name: c.attr("name"), 874 | value: c.val() 875 | }) 876 | }), b 877 | }, a.fn.serialize = function() { 878 | var a = []; 879 | return this.serializeArray().forEach(function(b) { 880 | a.push(encodeURIComponent(b.name) + "=" + encodeURIComponent(b.value)) 881 | }), a.join("&") 882 | }, a.fn.submit = function(b) { 883 | if (b) this.bind("submit", b); 884 | else if (this.length) { 885 | var c = a.Event("submit"); 886 | this.eq(0).trigger(c), c.defaultPrevented || this.get(0).submit() 887 | } 888 | return this 889 | } 890 | }(Zepto), function(a) { 891 | function d(a) { 892 | return "tagName" in a ? a : a.parentNode 893 | } 894 | function e(a, b, c, d) { 895 | var e = Math.abs(a - b), 896 | f = Math.abs(c - d); 897 | return e >= f ? a - b > 0 ? "Left" : "Right" : c - d > 0 ? "Up" : "Down" 898 | } 899 | function h() { 900 | g = null, b.last && (b.el.trigger("longTap"), b = {}) 901 | } 902 | function i() { 903 | g && clearTimeout(g), g = null 904 | } 905 | var b = {}, 906 | c, f = 750, 907 | g; 908 | a(document).ready(function() { 909 | var j, k; 910 | a(document.body).bind("touchstart", function(e) { 911 | j = Date.now(), k = j - (b.last || j), b.el = a(d(e.touches[0].target)), c && clearTimeout(c), b.x1 = e.touches[0].pageX, b.y1 = e.touches[0].pageY, k > 0 && k <= 250 && (b.isDoubleTap = !0), b.last = j, g = setTimeout(h, f) 912 | }).bind("touchmove", function(a) { 913 | i(), b.x2 = a.touches[0].pageX, b.y2 = a.touches[0].pageY 914 | }).bind("touchend", function(a) { 915 | i(), b.isDoubleTap ? (b.el.trigger("doubleTap"), b = {}) : b.x2 && Math.abs(b.x1 - b.x2) > 30 || b.y2 && Math.abs(b.y1 - b.y2) > 30 ? (b.el.trigger("swipe") && b.el.trigger("swipe" + e(b.x1, b.x2, b.y1, b.y2)), b = {}) : "last" in b && (b.el.trigger("tap"), c = setTimeout(function() { 916 | c = null, b.el.trigger("singleTap"), b = {} 917 | }, 250)) 918 | }).bind("touchcancel", function() { 919 | c && clearTimeout(c), g && clearTimeout(g), g = c = null, b = {} 920 | }) 921 | }), ["swipe", "swipeLeft", "swipeRight", "swipeUp", "swipeDown", "doubleTap", "tap", "singleTap", "longTap"].forEach(function(b) { 922 | a.fn[b] = function(a) { 923 | return this.bind(b, a) 924 | } 925 | }) 926 | }(Zepto); -------------------------------------------------------------------------------- /test/vendor/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 | (function() { 8 | 9 | // Initial Setup 10 | // ------------- 11 | 12 | // Save a reference to the global object (`window` in the browser, `exports` 13 | // on the server). 14 | var root = this; 15 | 16 | // Save the previous value of the `Backbone` variable, so that it can be 17 | // restored later on, if `noConflict` is used. 18 | var previousBackbone = root.Backbone; 19 | 20 | // Create local references to array methods we'll want to use later. 21 | var array = []; 22 | var push = array.push; 23 | var slice = array.slice; 24 | var splice = array.splice; 25 | 26 | // The top-level namespace. All public Backbone classes and modules will 27 | // be attached to this. Exported for both the browser and the server. 28 | var Backbone; 29 | if (typeof exports !== 'undefined') { 30 | Backbone = exports; 31 | } else { 32 | Backbone = root.Backbone = {}; 33 | } 34 | 35 | // Current version of the library. Keep in sync with `package.json`. 36 | Backbone.VERSION = '1.0.0'; 37 | 38 | // Require Underscore, if we're on the server, and it's not already present. 39 | var _ = root._; 40 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); 41 | 42 | // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns 43 | // the `$` variable. 44 | Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; 45 | 46 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 47 | // to its previous owner. Returns a reference to this Backbone object. 48 | Backbone.noConflict = function() { 49 | root.Backbone = previousBackbone; 50 | return this; 51 | }; 52 | 53 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 54 | // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and 55 | // set a `X-Http-Method-Override` header. 56 | Backbone.emulateHTTP = false; 57 | 58 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 59 | // `application/json` requests ... will encode the body as 60 | // `application/x-www-form-urlencoded` instead and will send the model in a 61 | // form param named `model`. 62 | Backbone.emulateJSON = false; 63 | 64 | // Backbone.Events 65 | // --------------- 66 | 67 | // A module that can be mixed in to *any object* in order to provide it with 68 | // custom events. You may bind with `on` or remove with `off` callback 69 | // functions to an event; `trigger`-ing an event fires all callbacks in 70 | // succession. 71 | // 72 | // var object = {}; 73 | // _.extend(object, Backbone.Events); 74 | // object.on('expand', function(){ alert('expanded'); }); 75 | // object.trigger('expand'); 76 | // 77 | var Events = Backbone.Events = { 78 | 79 | // Bind an event to a `callback` function. Passing `"all"` will bind 80 | // the callback to all events fired. 81 | on: function(name, callback, context) { 82 | if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 83 | this._events || (this._events = {}); 84 | var events = this._events[name] || (this._events[name] = []); 85 | events.push({ 86 | callback: callback, 87 | context: context, 88 | ctx: context || this 89 | }); 90 | return this; 91 | }, 92 | 93 | // Bind an event to only be triggered a single time. After the first time 94 | // the callback is invoked, it will be removed. 95 | once: function(name, callback, context) { 96 | if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 97 | var self = this; 98 | var once = _.once(function() { 99 | self.off(name, once); 100 | callback.apply(this, arguments); 101 | }); 102 | once._callback = callback; 103 | return this.on(name, once, context); 104 | }, 105 | 106 | // Remove one or many callbacks. If `context` is null, removes all 107 | // callbacks with that function. If `callback` is null, removes all 108 | // callbacks for the event. If `name` is null, removes all bound 109 | // callbacks for all events. 110 | off: function(name, callback, context) { 111 | var retain, ev, events, names, i, l, j, k; 112 | if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 113 | if (!name && !callback && !context) { 114 | this._events = {}; 115 | return this; 116 | } 117 | 118 | names = name ? [name] : _.keys(this._events); 119 | for (i = 0, l = names.length; i < l; i++) { 120 | name = names[i]; 121 | if (events = this._events[name]) { 122 | this._events[name] = retain = []; 123 | if (callback || context) { 124 | for (j = 0, k = events.length; j < k; j++) { 125 | ev = events[j]; 126 | if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || (context && context !== ev.context)) { 127 | retain.push(ev); 128 | } 129 | } 130 | } 131 | if (!retain.length) delete this._events[name]; 132 | } 133 | } 134 | 135 | return this; 136 | }, 137 | 138 | // Trigger one or many events, firing all bound callbacks. Callbacks are 139 | // passed the same arguments as `trigger` is, apart from the event name 140 | // (unless you're listening on `"all"`, which will cause your callback to 141 | // receive the true name of the event as the first argument). 142 | trigger: function(name) { 143 | if (!this._events) return this; 144 | var args = slice.call(arguments, 1); 145 | if (!eventsApi(this, 'trigger', name, args)) return this; 146 | var events = this._events[name]; 147 | var allEvents = this._events.all; 148 | if (events) triggerEvents(events, args); 149 | if (allEvents) triggerEvents(allEvents, arguments); 150 | return this; 151 | }, 152 | 153 | // Tell this object to stop listening to either specific events ... or 154 | // to every object it's currently listening to. 155 | stopListening: function(obj, name, callback) { 156 | var listeners = this._listeners; 157 | if (!listeners) return this; 158 | var deleteListener = !name && !callback; 159 | if (typeof name === 'object') callback = this; 160 | if (obj)(listeners = {})[obj._listenerId] = obj; 161 | for (var id in listeners) { 162 | listeners[id].off(name, callback, this); 163 | if (deleteListener) delete this._listeners[id]; 164 | } 165 | return this; 166 | } 167 | 168 | }; 169 | 170 | // Regular expression used to split event strings. 171 | var eventSplitter = /\s+/; 172 | 173 | // Implement fancy features of the Events API such as multiple event 174 | // names `"change blur"` and jQuery-style event maps `{change: action}` 175 | // in terms of the existing API. 176 | var eventsApi = function(obj, action, name, rest) { 177 | if (!name) return true; 178 | 179 | // Handle event maps. 180 | if (typeof name === 'object') { 181 | for (var key in name) { 182 | obj[action].apply(obj, [key, name[key]].concat(rest)); 183 | } 184 | return false; 185 | } 186 | 187 | // Handle space separated event names. 188 | if (eventSplitter.test(name)) { 189 | var names = name.split(eventSplitter); 190 | for (var i = 0, l = names.length; i < l; i++) { 191 | obj[action].apply(obj, [names[i]].concat(rest)); 192 | } 193 | return false; 194 | } 195 | 196 | return true; 197 | }; 198 | 199 | // A difficult-to-believe, but optimized internal dispatch function for 200 | // triggering events. Tries to keep the usual cases speedy (most internal 201 | // Backbone events have 3 arguments). 202 | var triggerEvents = function(events, args) { 203 | var ev, i = -1, 204 | l = events.length, 205 | a1 = args[0], 206 | a2 = args[1], 207 | a3 = args[2]; 208 | switch (args.length) { 209 | case 0: 210 | while (++i < l)(ev = events[i]).callback.call(ev.ctx); 211 | return; 212 | case 1: 213 | while (++i < l)(ev = events[i]).callback.call(ev.ctx, a1); 214 | return; 215 | case 2: 216 | while (++i < l)(ev = events[i]).callback.call(ev.ctx, a1, a2); 217 | return; 218 | case 3: 219 | while (++i < l)(ev = events[i]).callback.call(ev.ctx, a1, a2, a3); 220 | return; 221 | default: 222 | while (++i < l)(ev = events[i]).callback.apply(ev.ctx, args); 223 | } 224 | }; 225 | 226 | var listenMethods = { 227 | listenTo: 'on', 228 | listenToOnce: 'once' 229 | }; 230 | 231 | // Inversion-of-control versions of `on` and `once`. Tell *this* object to 232 | // listen to an event in another object ... keeping track of what it's 233 | // listening to. 234 | _.each(listenMethods, function(implementation, method) { 235 | Events[method] = function(obj, name, callback) { 236 | var listeners = this._listeners || (this._listeners = {}); 237 | var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); 238 | listeners[id] = obj; 239 | if (typeof name === 'object') callback = this; 240 | obj[implementation](name, callback, this); 241 | return this; 242 | }; 243 | }); 244 | 245 | // Aliases for backwards compatibility. 246 | Events.bind = Events.on; 247 | Events.unbind = Events.off; 248 | 249 | // Allow the `Backbone` object to serve as a global event bus, for folks who 250 | // want global "pubsub" in a convenient place. 251 | _.extend(Backbone, Events); 252 | 253 | // Backbone.Model 254 | // -------------- 255 | 256 | // Backbone **Models** are the basic data object in the framework -- 257 | // frequently representing a row in a table in a database on your server. 258 | // A discrete chunk of data and a bunch of useful, related methods for 259 | // performing computations and transformations on that data. 260 | 261 | // Create a new model with the specified attributes. A client id (`cid`) 262 | // is automatically generated and assigned for you. 263 | var Model = Backbone.Model = function(attributes, options) { 264 | var defaults; 265 | var attrs = attributes || {}; 266 | options || (options = {}); 267 | this.cid = _.uniqueId('c'); 268 | this.attributes = {}; 269 | _.extend(this, _.pick(options, modelOptions)); 270 | if (options.parse) attrs = this.parse(attrs, options) || {}; 271 | if (defaults = _.result(this, 'defaults')) { 272 | attrs = _.defaults({}, attrs, defaults); 273 | } 274 | this.set(attrs, options); 275 | this.changed = {}; 276 | this.initialize.apply(this, arguments); 277 | }; 278 | 279 | // A list of options to be attached directly to the model, if provided. 280 | var modelOptions = ['url', 'urlRoot', 'collection']; 281 | 282 | // Attach all inheritable methods to the Model prototype. 283 | _.extend(Model.prototype, Events, { 284 | 285 | // A hash of attributes whose current and previous value differ. 286 | changed: null, 287 | 288 | // The value returned during the last failed validation. 289 | validationError: null, 290 | 291 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 292 | // CouchDB users may want to set this to `"_id"`. 293 | idAttribute: 'id', 294 | 295 | // Initialize is an empty function by default. Override it with your own 296 | // initialization logic. 297 | initialize: function() {}, 298 | 299 | // Return a copy of the model's `attributes` object. 300 | toJSON: function(options) { 301 | return _.clone(this.attributes); 302 | }, 303 | 304 | // Proxy `Backbone.sync` by default -- but override this if you need 305 | // custom syncing semantics for *this* particular model. 306 | sync: function() { 307 | return Backbone.sync.apply(this, arguments); 308 | }, 309 | 310 | // Get the value of an attribute. 311 | get: function(attr) { 312 | return this.attributes[attr]; 313 | }, 314 | 315 | // Get the HTML-escaped value of an attribute. 316 | escape: function(attr) { 317 | return _.escape(this.get(attr)); 318 | }, 319 | 320 | // Returns `true` if the attribute contains a value that is not null 321 | // or undefined. 322 | has: function(attr) { 323 | return this.get(attr) != null; 324 | }, 325 | 326 | // Set a hash of model attributes on the object, firing `"change"`. This is 327 | // the core primitive operation of a model, updating the data and notifying 328 | // anyone who needs to know about the change in state. The heart of the beast. 329 | set: function(key, val, options) { 330 | var attr, attrs, unset, changes, silent, changing, prev, current; 331 | if (key == null) return this; 332 | 333 | // Handle both `"key", value` and `{key: value}` -style arguments. 334 | if (typeof key === 'object') { 335 | attrs = key; 336 | options = val; 337 | } else { 338 | (attrs = {})[key] = val; 339 | } 340 | 341 | options || (options = {}); 342 | 343 | // Run validation. 344 | if (!this._validate(attrs, options)) return false; 345 | 346 | // Extract attributes and options. 347 | unset = options.unset; 348 | silent = options.silent; 349 | changes = []; 350 | changing = this._changing; 351 | this._changing = true; 352 | 353 | if (!changing) { 354 | this._previousAttributes = _.clone(this.attributes); 355 | this.changed = {}; 356 | } 357 | current = this.attributes, prev = this._previousAttributes; 358 | 359 | // Check for changes of `id`. 360 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 361 | 362 | // For each `set` attribute, update or delete the current value. 363 | for (attr in attrs) { 364 | val = attrs[attr]; 365 | if (!_.isEqual(current[attr], val)) changes.push(attr); 366 | if (!_.isEqual(prev[attr], val)) { 367 | this.changed[attr] = val; 368 | } else { 369 | delete this.changed[attr]; 370 | } 371 | unset ? delete current[attr] : current[attr] = val; 372 | } 373 | 374 | // Trigger all relevant attribute changes. 375 | if (!silent) { 376 | if (changes.length) this._pending = true; 377 | for (var i = 0, l = changes.length; i < l; i++) { 378 | this.trigger('change:' + changes[i], this, current[changes[i]], options); 379 | } 380 | } 381 | 382 | // You might be wondering why there's a `while` loop here. Changes can 383 | // be recursively nested within `"change"` events. 384 | if (changing) return this; 385 | if (!silent) { 386 | while (this._pending) { 387 | this._pending = false; 388 | this.trigger('change', this, options); 389 | } 390 | } 391 | this._pending = false; 392 | this._changing = false; 393 | return this; 394 | }, 395 | 396 | // Remove an attribute from the model, firing `"change"`. `unset` is a noop 397 | // if the attribute doesn't exist. 398 | unset: function(attr, options) { 399 | return this.set(attr, void 0, _.extend({}, options, { 400 | unset: true 401 | })); 402 | }, 403 | 404 | // Clear all attributes on the model, firing `"change"`. 405 | clear: function(options) { 406 | var attrs = {}; 407 | for (var key in this.attributes) attrs[key] = void 0; 408 | return this.set(attrs, _.extend({}, options, { 409 | unset: true 410 | })); 411 | }, 412 | 413 | // Determine if the model has changed since the last `"change"` event. 414 | // If you specify an attribute name, determine if that attribute has changed. 415 | hasChanged: function(attr) { 416 | if (attr == null) return !_.isEmpty(this.changed); 417 | return _.has(this.changed, attr); 418 | }, 419 | 420 | // Return an object containing all the attributes that have changed, or 421 | // false if there are no changed attributes. Useful for determining what 422 | // parts of a view need to be updated and/or what attributes need to be 423 | // persisted to the server. Unset attributes will be set to undefined. 424 | // You can also pass an attributes object to diff against the model, 425 | // determining if there *would be* a change. 426 | changedAttributes: function(diff) { 427 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 428 | var val, changed = false; 429 | var old = this._changing ? this._previousAttributes : this.attributes; 430 | for (var attr in diff) { 431 | if (_.isEqual(old[attr], (val = diff[attr]))) continue; 432 | (changed || (changed = {}))[attr] = val; 433 | } 434 | return changed; 435 | }, 436 | 437 | // Get the previous value of an attribute, recorded at the time the last 438 | // `"change"` event was fired. 439 | previous: function(attr) { 440 | if (attr == null || !this._previousAttributes) return null; 441 | return this._previousAttributes[attr]; 442 | }, 443 | 444 | // Get all of the attributes of the model at the time of the previous 445 | // `"change"` event. 446 | previousAttributes: function() { 447 | return _.clone(this._previousAttributes); 448 | }, 449 | 450 | // Fetch the model from the server. If the server's representation of the 451 | // model differs from its current attributes, they will be overridden, 452 | // triggering a `"change"` event. 453 | fetch: function(options) { 454 | options = options ? _.clone(options) : {}; 455 | if (options.parse === void 0) options.parse = true; 456 | var model = this; 457 | var success = options.success; 458 | options.success = function(resp) { 459 | if (!model.set(model.parse(resp, options), options)) return false; 460 | if (success) success(model, resp, options); 461 | model.trigger('sync', model, resp, options); 462 | }; 463 | wrapError(this, options); 464 | return this.sync('read', this, options); 465 | }, 466 | 467 | // Set a hash of model attributes, and sync the model to the server. 468 | // If the server returns an attributes hash that differs, the model's 469 | // state will be `set` again. 470 | save: function(key, val, options) { 471 | var attrs, method, xhr, attributes = this.attributes; 472 | 473 | // Handle both `"key", value` and `{key: value}` -style arguments. 474 | if (key == null || typeof key === 'object') { 475 | attrs = key; 476 | options = val; 477 | } else { 478 | (attrs = {})[key] = val; 479 | } 480 | 481 | // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. 482 | if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; 483 | 484 | options = _.extend({ 485 | validate: true 486 | }, options); 487 | 488 | // Do not persist invalid models. 489 | if (!this._validate(attrs, options)) return false; 490 | 491 | // Set temporary attributes if `{wait: true}`. 492 | if (attrs && options.wait) { 493 | this.attributes = _.extend({}, attributes, attrs); 494 | } 495 | 496 | // After a successful server-side save, the client is (optionally) 497 | // updated with the server-side state. 498 | if (options.parse === void 0) options.parse = true; 499 | var model = this; 500 | var success = options.success; 501 | options.success = function(resp) { 502 | // Ensure attributes are restored during synchronous saves. 503 | model.attributes = attributes; 504 | var serverAttrs = model.parse(resp, options); 505 | if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); 506 | if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { 507 | return false; 508 | } 509 | if (success) success(model, resp, options); 510 | model.trigger('sync', model, resp, options); 511 | }; 512 | wrapError(this, options); 513 | 514 | method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); 515 | if (method === 'patch') options.attrs = attrs; 516 | xhr = this.sync(method, this, options); 517 | 518 | // Restore attributes. 519 | if (attrs && options.wait) this.attributes = attributes; 520 | 521 | return xhr; 522 | }, 523 | 524 | // Destroy this model on the server if it was already persisted. 525 | // Optimistically removes the model from its collection, if it has one. 526 | // If `wait: true` is passed, waits for the server to respond before removal. 527 | destroy: function(options) { 528 | options = options ? _.clone(options) : {}; 529 | var model = this; 530 | var success = options.success; 531 | 532 | var destroy = function() { 533 | model.trigger('destroy', model, model.collection, options); 534 | }; 535 | 536 | options.success = function(resp) { 537 | if (options.wait || model.isNew()) destroy(); 538 | if (success) success(model, resp, options); 539 | if (!model.isNew()) model.trigger('sync', model, resp, options); 540 | }; 541 | 542 | if (this.isNew()) { 543 | options.success(); 544 | return false; 545 | } 546 | wrapError(this, options); 547 | 548 | var xhr = this.sync('delete', this, options); 549 | if (!options.wait) destroy(); 550 | return xhr; 551 | }, 552 | 553 | // Default URL for the model's representation on the server -- if you're 554 | // using Backbone's restful methods, override this to change the endpoint 555 | // that will be called. 556 | url: function() { 557 | var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); 558 | if (this.isNew()) return base; 559 | return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); 560 | }, 561 | 562 | // **parse** converts a response into the hash of attributes to be `set` on 563 | // the model. The default implementation is just to pass the response along. 564 | parse: function(resp, options) { 565 | return resp; 566 | }, 567 | 568 | // Create a new model with identical attributes to this one. 569 | clone: function() { 570 | return new this.constructor(this.attributes); 571 | }, 572 | 573 | // A model is new if it has never been saved to the server, and lacks an id. 574 | isNew: function() { 575 | return this.id == null; 576 | }, 577 | 578 | // Check if the model is currently in a valid state. 579 | isValid: function(options) { 580 | return this._validate({}, _.extend(options || {}, { 581 | validate: true 582 | })); 583 | }, 584 | 585 | // Run validation against the next complete set of model attributes, 586 | // returning `true` if all is well. Otherwise, fire an `"invalid"` event. 587 | _validate: function(attrs, options) { 588 | if (!options.validate || !this.validate) return true; 589 | attrs = _.extend({}, this.attributes, attrs); 590 | var error = this.validationError = this.validate(attrs, options) || null; 591 | if (!error) return true; 592 | this.trigger('invalid', this, error, _.extend(options || {}, { 593 | validationError: error 594 | })); 595 | return false; 596 | } 597 | 598 | }); 599 | 600 | // Underscore methods that we want to implement on the Model. 601 | var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; 602 | 603 | // Mix in each Underscore method as a proxy to `Model#attributes`. 604 | _.each(modelMethods, function(method) { 605 | Model.prototype[method] = function() { 606 | var args = slice.call(arguments); 607 | args.unshift(this.attributes); 608 | return _[method].apply(_, args); 609 | }; 610 | }); 611 | 612 | // Backbone.Collection 613 | // ------------------- 614 | 615 | // If models tend to represent a single row of data, a Backbone Collection is 616 | // more analagous to a table full of data ... or a small slice or page of that 617 | // table, or a collection of rows that belong together for a particular reason 618 | // -- all of the messages in this particular folder, all of the documents 619 | // belonging to this particular author, and so on. Collections maintain 620 | // indexes of their models, both in order, and for lookup by `id`. 621 | 622 | // Create a new **Collection**, perhaps to contain a specific type of `model`. 623 | // If a `comparator` is specified, the Collection will maintain 624 | // its models in sort order, as they're added and removed. 625 | var Collection = Backbone.Collection = function(models, options) { 626 | options || (options = {}); 627 | if (options.url) this.url = options.url; 628 | if (options.model) this.model = options.model; 629 | if (options.comparator !== void 0) this.comparator = options.comparator; 630 | this._reset(); 631 | this.initialize.apply(this, arguments); 632 | if (models) this.reset(models, _.extend({ 633 | silent: true 634 | }, options)); 635 | }; 636 | 637 | // Default options for `Collection#set`. 638 | var setOptions = { 639 | add: true, 640 | remove: true, 641 | merge: true 642 | }; 643 | var addOptions = { 644 | add: true, 645 | merge: false, 646 | remove: false 647 | }; 648 | 649 | // Define the Collection's inheritable methods. 650 | _.extend(Collection.prototype, Events, { 651 | 652 | // The default model for a collection is just a **Backbone.Model**. 653 | // This should be overridden in most cases. 654 | model: Model, 655 | 656 | // Initialize is an empty function by default. Override it with your own 657 | // initialization logic. 658 | initialize: function() {}, 659 | 660 | // The JSON representation of a Collection is an array of the 661 | // models' attributes. 662 | toJSON: function(options) { 663 | return this.map(function(model) { 664 | return model.toJSON(options); 665 | }); 666 | }, 667 | 668 | // Proxy `Backbone.sync` by default. 669 | sync: function() { 670 | return Backbone.sync.apply(this, arguments); 671 | }, 672 | 673 | // Add a model, or list of models to the set. 674 | add: function(models, options) { 675 | return this.set(models, _.defaults(options || {}, addOptions)); 676 | }, 677 | 678 | // Remove a model, or a list of models from the set. 679 | remove: function(models, options) { 680 | models = _.isArray(models) ? models.slice() : [models]; 681 | options || (options = {}); 682 | var i, l, index, model; 683 | for (i = 0, l = models.length; i < l; i++) { 684 | model = this.get(models[i]); 685 | if (!model) continue; 686 | delete this._byId[model.id]; 687 | delete this._byId[model.cid]; 688 | index = this.indexOf(model); 689 | this.models.splice(index, 1); 690 | this.length--; 691 | if (!options.silent) { 692 | options.index = index; 693 | model.trigger('remove', model, this, options); 694 | } 695 | this._removeReference(model); 696 | } 697 | return this; 698 | }, 699 | 700 | // Update a collection by `set`-ing a new list of models, adding new ones, 701 | // removing models that are no longer present, and merging models that 702 | // already exist in the collection, as necessary. Similar to **Model#set**, 703 | // the core operation for updating the data contained by the collection. 704 | set: function(models, options) { 705 | options = _.defaults(options || {}, setOptions); 706 | if (options.parse) models = this.parse(models, options); 707 | if (!_.isArray(models)) models = models ? [models] : []; 708 | var i, l, model, attrs, existing, sort; 709 | var at = options.at; 710 | var sortable = this.comparator && (at == null) && options.sort !== false; 711 | var sortAttr = _.isString(this.comparator) ? this.comparator : null; 712 | var toAdd = [], 713 | toRemove = [], 714 | modelMap = {}; 715 | 716 | // Turn bare objects into model references, and prevent invalid models 717 | // from being added. 718 | for (i = 0, l = models.length; i < l; i++) { 719 | if (!(model = this._prepareModel(models[i], options))) continue; 720 | 721 | // If a duplicate is found, prevent it from being added and 722 | // optionally merge it into the existing model. 723 | if (existing = this.get(model)) { 724 | if (options.remove) modelMap[existing.cid] = true; 725 | if (options.merge) { 726 | existing.set(model.attributes, options); 727 | if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; 728 | } 729 | 730 | // This is a new model, push it to the `toAdd` list. 731 | } else if (options.add) { 732 | toAdd.push(model); 733 | 734 | // Listen to added models' events, and index models for lookup by 735 | // `id` and by `cid`. 736 | model.on('all', this._onModelEvent, this); 737 | this._byId[model.cid] = model; 738 | if (model.id != null) this._byId[model.id] = model; 739 | } 740 | } 741 | 742 | // Remove nonexistent models if appropriate. 743 | if (options.remove) { 744 | for (i = 0, l = this.length; i < l; ++i) { 745 | if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); 746 | } 747 | if (toRemove.length) this.remove(toRemove, options); 748 | } 749 | 750 | // See if sorting is needed, update `length` and splice in new models. 751 | if (toAdd.length) { 752 | if (sortable) sort = true; 753 | this.length += toAdd.length; 754 | if (at != null) { 755 | splice.apply(this.models, [at, 0].concat(toAdd)); 756 | } else { 757 | push.apply(this.models, toAdd); 758 | } 759 | } 760 | 761 | // Silently sort the collection if appropriate. 762 | if (sort) this.sort({ 763 | silent: true 764 | }); 765 | 766 | if (options.silent) return this; 767 | 768 | // Trigger `add` events. 769 | for (i = 0, l = toAdd.length; i < l; i++) { 770 | (model = toAdd[i]).trigger('add', model, this, options); 771 | } 772 | 773 | // Trigger `sort` if the collection was sorted. 774 | if (sort) this.trigger('sort', this, options); 775 | return this; 776 | }, 777 | 778 | // When you have more items than you want to add or remove individually, 779 | // you can reset the entire set with a new list of models, without firing 780 | // any granular `add` or `remove` events. Fires `reset` when finished. 781 | // Useful for bulk operations and optimizations. 782 | reset: function(models, options) { 783 | options || (options = {}); 784 | for (var i = 0, l = this.models.length; i < l; i++) { 785 | this._removeReference(this.models[i]); 786 | } 787 | options.previousModels = this.models; 788 | this._reset(); 789 | this.add(models, _.extend({ 790 | silent: true 791 | }, options)); 792 | if (!options.silent) this.trigger('reset', this, options); 793 | return this; 794 | }, 795 | 796 | // Add a model to the end of the collection. 797 | push: function(model, options) { 798 | model = this._prepareModel(model, options); 799 | this.add(model, _.extend({ 800 | at: this.length 801 | }, options)); 802 | return model; 803 | }, 804 | 805 | // Remove a model from the end of the collection. 806 | pop: function(options) { 807 | var model = this.at(this.length - 1); 808 | this.remove(model, options); 809 | return model; 810 | }, 811 | 812 | // Add a model to the beginning of the collection. 813 | unshift: function(model, options) { 814 | model = this._prepareModel(model, options); 815 | this.add(model, _.extend({ 816 | at: 0 817 | }, options)); 818 | return model; 819 | }, 820 | 821 | // Remove a model from the beginning of the collection. 822 | shift: function(options) { 823 | var model = this.at(0); 824 | this.remove(model, options); 825 | return model; 826 | }, 827 | 828 | // Slice out a sub-array of models from the collection. 829 | slice: function(begin, end) { 830 | return this.models.slice(begin, end); 831 | }, 832 | 833 | // Get a model from the set by id. 834 | get: function(obj) { 835 | if (obj == null) return void 0; 836 | return this._byId[obj.id != null ? obj.id : obj.cid || obj]; 837 | }, 838 | 839 | // Get the model at the given index. 840 | at: function(index) { 841 | return this.models[index]; 842 | }, 843 | 844 | // Return models with matching attributes. Useful for simple cases of 845 | // `filter`. 846 | where: function(attrs, first) { 847 | if (_.isEmpty(attrs)) return first ? void 0 : []; 848 | return this[first ? 'find' : 'filter'](function(model) { 849 | for (var key in attrs) { 850 | if (attrs[key] !== model.get(key)) return false; 851 | } 852 | return true; 853 | }); 854 | }, 855 | 856 | // Return the first model with matching attributes. Useful for simple cases 857 | // of `find`. 858 | findWhere: function(attrs) { 859 | return this.where(attrs, true); 860 | }, 861 | 862 | // Force the collection to re-sort itself. You don't need to call this under 863 | // normal circumstances, as the set will maintain sort order as each item 864 | // is added. 865 | sort: function(options) { 866 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 867 | options || (options = {}); 868 | 869 | // Run sort based on type of `comparator`. 870 | if (_.isString(this.comparator) || this.comparator.length === 1) { 871 | this.models = this.sortBy(this.comparator, this); 872 | } else { 873 | this.models.sort(_.bind(this.comparator, this)); 874 | } 875 | 876 | if (!options.silent) this.trigger('sort', this, options); 877 | return this; 878 | }, 879 | 880 | // Figure out the smallest index at which a model should be inserted so as 881 | // to maintain order. 882 | sortedIndex: function(model, value, context) { 883 | value || (value = this.comparator); 884 | var iterator = _.isFunction(value) ? value : function(model) { 885 | return model.get(value); 886 | }; 887 | return _.sortedIndex(this.models, model, iterator, context); 888 | }, 889 | 890 | // Pluck an attribute from each model in the collection. 891 | pluck: function(attr) { 892 | return _.invoke(this.models, 'get', attr); 893 | }, 894 | 895 | // Fetch the default set of models for this collection, resetting the 896 | // collection when they arrive. If `reset: true` is passed, the response 897 | // data will be passed through the `reset` method instead of `set`. 898 | fetch: function(options) { 899 | options = options ? _.clone(options) : {}; 900 | if (options.parse === void 0) options.parse = true; 901 | var success = options.success; 902 | var collection = this; 903 | options.success = function(resp) { 904 | var method = options.reset ? 'reset' : 'set'; 905 | collection[method](resp, options); 906 | if (success) success(collection, resp, options); 907 | collection.trigger('sync', collection, resp, options); 908 | }; 909 | wrapError(this, options); 910 | return this.sync('read', this, options); 911 | }, 912 | 913 | // Create a new instance of a model in this collection. Add the model to the 914 | // collection immediately, unless `wait: true` is passed, in which case we 915 | // wait for the server to agree. 916 | create: function(model, options) { 917 | options = options ? _.clone(options) : {}; 918 | if (!(model = this._prepareModel(model, options))) return false; 919 | if (!options.wait) this.add(model, options); 920 | var collection = this; 921 | var success = options.success; 922 | options.success = function(resp) { 923 | if (options.wait) collection.add(model, options); 924 | if (success) success(model, resp, options); 925 | }; 926 | model.save(null, options); 927 | return model; 928 | }, 929 | 930 | // **parse** converts a response into a list of models to be added to the 931 | // collection. The default implementation is just to pass it through. 932 | parse: function(resp, options) { 933 | return resp; 934 | }, 935 | 936 | // Create a new collection with an identical list of models as this one. 937 | clone: function() { 938 | return new this.constructor(this.models); 939 | }, 940 | 941 | // Private method to reset all internal state. Called when the collection 942 | // is first initialized or reset. 943 | _reset: function() { 944 | this.length = 0; 945 | this.models = []; 946 | this._byId = {}; 947 | }, 948 | 949 | // Prepare a hash of attributes (or other model) to be added to this 950 | // collection. 951 | _prepareModel: function(attrs, options) { 952 | if (attrs instanceof Model) { 953 | if (!attrs.collection) attrs.collection = this; 954 | return attrs; 955 | } 956 | options || (options = {}); 957 | options.collection = this; 958 | var model = new this.model(attrs, options); 959 | if (!model._validate(attrs, options)) { 960 | this.trigger('invalid', this, attrs, options); 961 | return false; 962 | } 963 | return model; 964 | }, 965 | 966 | // Internal method to sever a model's ties to a collection. 967 | _removeReference: function(model) { 968 | if (this === model.collection) delete model.collection; 969 | model.off('all', this._onModelEvent, this); 970 | }, 971 | 972 | // Internal method called every time a model in the set fires an event. 973 | // Sets need to update their indexes when models change ids. All other 974 | // events simply proxy through. "add" and "remove" events that originate 975 | // in other collections are ignored. 976 | _onModelEvent: function(event, model, collection, options) { 977 | if ((event === 'add' || event === 'remove') && collection !== this) return; 978 | if (event === 'destroy') this.remove(model, options); 979 | if (model && event === 'change:' + model.idAttribute) { 980 | delete this._byId[model.previous(model.idAttribute)]; 981 | if (model.id != null) this._byId[model.id] = model; 982 | } 983 | this.trigger.apply(this, arguments); 984 | } 985 | 986 | }); 987 | 988 | // Underscore methods that we want to implement on the Collection. 989 | // 90% of the core usefulness of Backbone Collections is actually implemented 990 | // right here: 991 | var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 'isEmpty', 'chain']; 992 | 993 | // Mix in each Underscore method as a proxy to `Collection#models`. 994 | _.each(methods, function(method) { 995 | Collection.prototype[method] = function() { 996 | var args = slice.call(arguments); 997 | args.unshift(this.models); 998 | return _[method].apply(_, args); 999 | }; 1000 | }); 1001 | 1002 | // Underscore methods that take a property name as an argument. 1003 | var attributeMethods = ['groupBy', 'countBy', 'sortBy']; 1004 | 1005 | // Use attributes instead of properties. 1006 | _.each(attributeMethods, function(method) { 1007 | Collection.prototype[method] = function(value, context) { 1008 | var iterator = _.isFunction(value) ? value : function(model) { 1009 | return model.get(value); 1010 | }; 1011 | return _[method](this.models, iterator, context); 1012 | }; 1013 | }); 1014 | 1015 | // Backbone.View 1016 | // ------------- 1017 | 1018 | // Backbone Views are almost more convention than they are actual code. A View 1019 | // is simply a JavaScript object that represents a logical chunk of UI in the 1020 | // DOM. This might be a single item, an entire list, a sidebar or panel, or 1021 | // even the surrounding frame which wraps your whole app. Defining a chunk of 1022 | // UI as a **View** allows you to define your DOM events declaratively, without 1023 | // having to worry about render order ... and makes it easy for the view to 1024 | // react to specific changes in the state of your models. 1025 | 1026 | // Creating a Backbone.View creates its initial element outside of the DOM, 1027 | // if an existing element is not provided... 1028 | var View = Backbone.View = function(options) { 1029 | this.cid = _.uniqueId('view'); 1030 | this._configure(options || {}); 1031 | this._ensureElement(); 1032 | this.initialize.apply(this, arguments); 1033 | this.delegateEvents(); 1034 | }; 1035 | 1036 | // Cached regex to split keys for `delegate`. 1037 | var delegateEventSplitter = /^(\S+)\s*(.*)$/; 1038 | 1039 | // List of view options to be merged as properties. 1040 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; 1041 | 1042 | // Set up all inheritable **Backbone.View** properties and methods. 1043 | _.extend(View.prototype, Events, { 1044 | 1045 | // The default `tagName` of a View's element is `"div"`. 1046 | tagName: 'div', 1047 | 1048 | // jQuery delegate for element lookup, scoped to DOM elements within the 1049 | // current view. This should be prefered to global lookups where possible. 1050 | $: function(selector) { 1051 | return this.$el.find(selector); 1052 | }, 1053 | 1054 | // Initialize is an empty function by default. Override it with your own 1055 | // initialization logic. 1056 | initialize: function() {}, 1057 | 1058 | // **render** is the core function that your view should override, in order 1059 | // to populate its element (`this.el`), with the appropriate HTML. The 1060 | // convention is for **render** to always return `this`. 1061 | render: function() { 1062 | return this; 1063 | }, 1064 | 1065 | // Remove this view by taking the element out of the DOM, and removing any 1066 | // applicable Backbone.Events listeners. 1067 | remove: function() { 1068 | this.$el.remove(); 1069 | this.stopListening(); 1070 | return this; 1071 | }, 1072 | 1073 | // Change the view's element (`this.el` property), including event 1074 | // re-delegation. 1075 | setElement: function(element, delegate) { 1076 | if (this.$el) this.undelegateEvents(); 1077 | this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); 1078 | this.el = this.$el[0]; 1079 | if (delegate !== false) this.delegateEvents(); 1080 | return this; 1081 | }, 1082 | 1083 | // Set callbacks, where `this.events` is a hash of 1084 | // 1085 | // *{"event selector": "callback"}* 1086 | // 1087 | // { 1088 | // 'mousedown .title': 'edit', 1089 | // 'click .button': 'save' 1090 | // 'click .open': function(e) { ... } 1091 | // } 1092 | // 1093 | // pairs. Callbacks will be bound to the view, with `this` set properly. 1094 | // Uses event delegation for efficiency. 1095 | // Omitting the selector binds the event to `this.el`. 1096 | // This only works for delegate-able events: not `focus`, `blur`, and 1097 | // not `change`, `submit`, and `reset` in Internet Explorer. 1098 | delegateEvents: function(events) { 1099 | if (!(events || (events = _.result(this, 'events')))) return this; 1100 | this.undelegateEvents(); 1101 | for (var key in events) { 1102 | var method = events[key]; 1103 | if (!_.isFunction(method)) method = this[events[key]]; 1104 | if (!method) continue; 1105 | 1106 | var match = key.match(delegateEventSplitter); 1107 | var eventName = match[1], 1108 | selector = match[2]; 1109 | method = _.bind(method, this); 1110 | eventName += '.delegateEvents' + this.cid; 1111 | if (selector === '') { 1112 | this.$el.on(eventName, method); 1113 | } else { 1114 | this.$el.on(eventName, selector, method); 1115 | } 1116 | } 1117 | return this; 1118 | }, 1119 | 1120 | // Clears all callbacks previously bound to the view with `delegateEvents`. 1121 | // You usually don't need to use this, but may wish to if you have multiple 1122 | // Backbone views attached to the same DOM element. 1123 | undelegateEvents: function() { 1124 | this.$el.off('.delegateEvents' + this.cid); 1125 | return this; 1126 | }, 1127 | 1128 | // Performs the initial configuration of a View with a set of options. 1129 | // Keys with special meaning *(e.g. model, collection, id, className)* are 1130 | // attached directly to the view. See `viewOptions` for an exhaustive 1131 | // list. 1132 | _configure: function(options) { 1133 | if (this.options) options = _.extend({}, _.result(this, 'options'), options); 1134 | _.extend(this, _.pick(options, viewOptions)); 1135 | this.options = options; 1136 | }, 1137 | 1138 | // Ensure that the View has a DOM element to render into. 1139 | // If `this.el` is a string, pass it through `$()`, take the first 1140 | // matching element, and re-assign it to `el`. Otherwise, create 1141 | // an element from the `id`, `className` and `tagName` properties. 1142 | _ensureElement: function() { 1143 | if (!this.el) { 1144 | var attrs = _.extend({}, _.result(this, 'attributes')); 1145 | if (this.id) attrs.id = _.result(this, 'id'); 1146 | if (this.className) attrs['class'] = _.result(this, 'className'); 1147 | var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); 1148 | this.setElement($el, false); 1149 | } else { 1150 | this.setElement(_.result(this, 'el'), false); 1151 | } 1152 | } 1153 | 1154 | }); 1155 | 1156 | // Backbone.sync 1157 | // ------------- 1158 | 1159 | // Override this function to change the manner in which Backbone persists 1160 | // models to the server. You will be passed the type of request, and the 1161 | // model in question. By default, makes a RESTful Ajax request 1162 | // to the model's `url()`. Some possible customizations could be: 1163 | // 1164 | // * Use `setTimeout` to batch rapid-fire updates into a single request. 1165 | // * Send up the models as XML instead of JSON. 1166 | // * Persist models via WebSockets instead of Ajax. 1167 | // 1168 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests 1169 | // as `POST`, with a `_method` parameter containing the true HTTP method, 1170 | // as well as all requests with the body as `application/x-www-form-urlencoded` 1171 | // instead of `application/json` with the model in a param named `model`. 1172 | // Useful when interfacing with server-side languages like **PHP** that make 1173 | // it difficult to read the body of `PUT` requests. 1174 | Backbone.sync = function(method, model, options) { 1175 | var type = methodMap[method]; 1176 | 1177 | // Default options, unless specified. 1178 | _.defaults(options || (options = {}), { 1179 | emulateHTTP: Backbone.emulateHTTP, 1180 | emulateJSON: Backbone.emulateJSON 1181 | }); 1182 | 1183 | // Default JSON-request options. 1184 | var params = { 1185 | type: type, 1186 | dataType: 'json' 1187 | }; 1188 | 1189 | // Ensure that we have a URL. 1190 | if (!options.url) { 1191 | params.url = _.result(model, 'url') || urlError(); 1192 | } 1193 | 1194 | // Ensure that we have the appropriate request data. 1195 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { 1196 | params.contentType = 'application/json'; 1197 | params.data = JSON.stringify(options.attrs || model.toJSON(options)); 1198 | } 1199 | 1200 | // For older servers, emulate JSON by encoding the request into an HTML-form. 1201 | if (options.emulateJSON) { 1202 | params.contentType = 'application/x-www-form-urlencoded'; 1203 | params.data = params.data ? { 1204 | model: params.data 1205 | } : {}; 1206 | } 1207 | 1208 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method` 1209 | // And an `X-HTTP-Method-Override` header. 1210 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { 1211 | params.type = 'POST'; 1212 | if (options.emulateJSON) params.data._method = type; 1213 | var beforeSend = options.beforeSend; 1214 | options.beforeSend = function(xhr) { 1215 | xhr.setRequestHeader('X-HTTP-Method-Override', type); 1216 | if (beforeSend) return beforeSend.apply(this, arguments); 1217 | }; 1218 | } 1219 | 1220 | // Don't process data on a non-GET request. 1221 | if (params.type !== 'GET' && !options.emulateJSON) { 1222 | params.processData = false; 1223 | } 1224 | 1225 | // If we're sending a `PATCH` request, and we're in an old Internet Explorer 1226 | // that still has ActiveX enabled by default, override jQuery to use that 1227 | // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. 1228 | if (params.type === 'PATCH' && window.ActiveXObject && !(window.external && window.external.msActiveXFilteringEnabled)) { 1229 | params.xhr = function() { 1230 | return new ActiveXObject("Microsoft.XMLHTTP"); 1231 | }; 1232 | } 1233 | 1234 | // Make the request, allowing the user to override any Ajax options. 1235 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); 1236 | model.trigger('request', model, xhr, options); 1237 | return xhr; 1238 | }; 1239 | 1240 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation. 1241 | var methodMap = { 1242 | 'create': 'POST', 1243 | 'update': 'PUT', 1244 | 'patch': 'PATCH', 1245 | 'delete': 'DELETE', 1246 | 'read': 'GET' 1247 | }; 1248 | 1249 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`. 1250 | // Override this if you'd like to use a different library. 1251 | Backbone.ajax = function() { 1252 | return Backbone.$.ajax.apply(Backbone.$, arguments); 1253 | }; 1254 | 1255 | // Backbone.Router 1256 | // --------------- 1257 | 1258 | // Routers map faux-URLs to actions, and fire events when routes are 1259 | // matched. Creating a new one sets its `routes` hash, if not set statically. 1260 | var Router = Backbone.Router = function(options) { 1261 | options || (options = {}); 1262 | if (options.routes) this.routes = options.routes; 1263 | this._bindRoutes(); 1264 | this.initialize.apply(this, arguments); 1265 | }; 1266 | 1267 | // Cached regular expressions for matching named param parts and splatted 1268 | // parts of route strings. 1269 | var optionalParam = /\((.*?)\)/g; 1270 | var namedParam = /(\(\?)?:\w+/g; 1271 | var splatParam = /\*\w+/g; 1272 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 1273 | 1274 | // Set up all inheritable **Backbone.Router** properties and methods. 1275 | _.extend(Router.prototype, Events, { 1276 | 1277 | // Initialize is an empty function by default. Override it with your own 1278 | // initialization logic. 1279 | initialize: function() {}, 1280 | 1281 | // Manually bind a single named route to a callback. For example: 1282 | // 1283 | // this.route('search/:query/p:num', 'search', function(query, num) { 1284 | // ... 1285 | // }); 1286 | // 1287 | route: function(route, name, callback) { 1288 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 1289 | if (_.isFunction(name)) { 1290 | callback = name; 1291 | name = ''; 1292 | } 1293 | if (!callback) callback = this[name]; 1294 | var router = this; 1295 | Backbone.history.route(route, function(fragment) { 1296 | var args = router._extractParameters(route, fragment); 1297 | callback && callback.apply(router, args); 1298 | router.trigger.apply(router, ['route:' + name].concat(args)); 1299 | router.trigger('route', name, args); 1300 | Backbone.history.trigger('route', router, name, args); 1301 | }); 1302 | return this; 1303 | }, 1304 | 1305 | // Simple proxy to `Backbone.history` to save a fragment into the history. 1306 | navigate: function(fragment, options) { 1307 | Backbone.history.navigate(fragment, options); 1308 | return this; 1309 | }, 1310 | 1311 | // Bind all defined routes to `Backbone.history`. We have to reverse the 1312 | // order of the routes here to support behavior where the most general 1313 | // routes can be defined at the bottom of the route map. 1314 | _bindRoutes: function() { 1315 | if (!this.routes) return; 1316 | this.routes = _.result(this, 'routes'); 1317 | var route, routes = _.keys(this.routes); 1318 | while ((route = routes.pop()) != null) { 1319 | this.route(route, this.routes[route]); 1320 | } 1321 | }, 1322 | 1323 | // Convert a route string into a regular expression, suitable for matching 1324 | // against the current location hash. 1325 | _routeToRegExp: function(route) { 1326 | route = route.replace(escapeRegExp, '\\$&').replace(optionalParam, '(?:$1)?').replace(namedParam, function(match, optional) { 1327 | return optional ? match : '([^\/]+)'; 1328 | }).replace(splatParam, '(.*?)'); 1329 | return new RegExp('^' + route + '$'); 1330 | }, 1331 | 1332 | // Given a route, and a URL fragment that it matches, return the array of 1333 | // extracted decoded parameters. Empty or unmatched parameters will be 1334 | // treated as `null` to normalize cross-browser behavior. 1335 | _extractParameters: function(route, fragment) { 1336 | var params = route.exec(fragment).slice(1); 1337 | return _.map(params, function(param) { 1338 | return param ? decodeURIComponent(param) : null; 1339 | }); 1340 | } 1341 | 1342 | }); 1343 | 1344 | // Backbone.History 1345 | // ---------------- 1346 | 1347 | // Handles cross-browser history management, based on either 1348 | // [pushState](http://diveintohtml5.info/history.html) and real URLs, or 1349 | // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) 1350 | // and URL fragments. If the browser supports neither (old IE, natch), 1351 | // falls back to polling. 1352 | var History = Backbone.History = function() { 1353 | this.handlers = []; 1354 | _.bindAll(this, 'checkUrl'); 1355 | 1356 | // Ensure that `History` can be used outside of the browser. 1357 | if (typeof window !== 'undefined') { 1358 | this.location = window.location; 1359 | this.history = window.history; 1360 | } 1361 | }; 1362 | 1363 | // Cached regex for stripping a leading hash/slash and trailing space. 1364 | var routeStripper = /^[#\/]|\s+$/g; 1365 | 1366 | // Cached regex for stripping leading and trailing slashes. 1367 | var rootStripper = /^\/+|\/+$/g; 1368 | 1369 | // Cached regex for detecting MSIE. 1370 | var isExplorer = /msie [\w.]+/; 1371 | 1372 | // Cached regex for removing a trailing slash. 1373 | var trailingSlash = /\/$/; 1374 | 1375 | // Has the history handling already been started? 1376 | History.started = false; 1377 | 1378 | // Set up all inheritable **Backbone.History** properties and methods. 1379 | _.extend(History.prototype, Events, { 1380 | 1381 | // The default interval to poll for hash changes, if necessary, is 1382 | // twenty times a second. 1383 | interval: 50, 1384 | 1385 | // Gets the true hash value. Cannot use location.hash directly due to bug 1386 | // in Firefox where location.hash will always be decoded. 1387 | getHash: function(window) { 1388 | var match = (window || this).location.href.match(/#(.*)$/); 1389 | return match ? match[1] : ''; 1390 | }, 1391 | 1392 | // Get the cross-browser normalized URL fragment, either from the URL, 1393 | // the hash, or the override. 1394 | getFragment: function(fragment, forcePushState) { 1395 | if (fragment == null) { 1396 | if (this._hasPushState || !this._wantsHashChange || forcePushState) { 1397 | fragment = this.location.pathname; 1398 | var root = this.root.replace(trailingSlash, ''); 1399 | if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); 1400 | } else { 1401 | fragment = this.getHash(); 1402 | } 1403 | } 1404 | return fragment.replace(routeStripper, ''); 1405 | }, 1406 | 1407 | // Start the hash change handling, returning `true` if the current URL matches 1408 | // an existing route, and `false` otherwise. 1409 | start: function(options) { 1410 | if (History.started) throw new Error("Backbone.history has already been started"); 1411 | History.started = true; 1412 | 1413 | // Figure out the initial configuration. Do we need an iframe? 1414 | // Is pushState desired ... is it available? 1415 | this.options = _.extend({}, { 1416 | root: '/' 1417 | }, this.options, options); 1418 | this.root = this.options.root; 1419 | this._wantsHashChange = this.options.hashChange !== false; 1420 | this._wantsPushState = !! this.options.pushState; 1421 | this._hasPushState = !! (this.options.pushState && this.history && this.history.pushState); 1422 | var fragment = this.getFragment(); 1423 | var docMode = document.documentMode; 1424 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 1425 | 1426 | // Normalize root to always include a leading and trailing slash. 1427 | this.root = ('/' + this.root + '/').replace(rootStripper, '/'); 1428 | 1429 | if (oldIE && this._wantsHashChange) { 1430 | this.iframe = Backbone.$('