diff --git a/bower.json b/bower.json index 77690c52..d9e32e1f 100644 --- a/bower.json +++ b/bower.json @@ -23,8 +23,7 @@ "api" ], "dependencies": { - "backbone": "~1.1.2", - "jquery": "~2.1.4", - "underscore": "~1.8.3" + "backbone": "~1.2.3", + "underscore": "^1.8.3" } } diff --git a/karma.conf.js b/karma.conf.js index d8cdc74d..0c45dbcf 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -15,10 +15,10 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ - 'test/vendor/js/jquery.js', - 'test/vendor/js/underscore.js', - 'test/vendor/js/backbone.js', - 'test/vendor/js/backbone-pageable.js', + 'node_modules/jquery/dist/jquery.js', + 'node_modules/underscore/underscore.js', + 'node_modules/backbone/backbone.js', + 'node_modules/backbone.paginator/lib/backbone.paginator.js', 'lib/backgrid.js', 'test/preamble.js', 'test/column.js', @@ -39,7 +39,7 @@ module.exports = function(config) { // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { - + }, diff --git a/package.json b/package.json index b5b5d651..c092abc8 100644 --- a/package.json +++ b/package.json @@ -18,18 +18,20 @@ "url": "http://github.com/wyuenho/backgrid" }, "devDependencies": { - "grunt": "~0.4.5", - "grunt-cli": "~0.1.13", - "grunt-contrib-clean": "~1.0.0", - "grunt-contrib-concat": "~1.0.0", - "grunt-contrib-connect": "~0.11.2", - "grunt-contrib-jasmine": "~1.0.0", - "grunt-contrib-uglify": "~1.0.0", - "grunt-contrib-watch": "~0.6.1", - "grunt-jsduck": "~1.0.1", + "backbone.paginator": "^2.0.0", + "grunt": "^0.4.5", + "grunt-cli": "^0.1.13", + "grunt-contrib-clean": "^1.0.0", + "grunt-contrib-concat": "^1.0.0", + "grunt-contrib-connect": "^0.11.2", + "grunt-contrib-jasmine": "^1.0.0", + "grunt-contrib-uglify": "^1.0.0", + "grunt-contrib-watch": "^0.6.1", + "grunt-jsduck": "^1.0.1", "grunt-karma": "^0.12.1", - "grunt-recess": "~1.0.1", - "grunt-template-jasmine-istanbul": "~0.4.0", + "grunt-recess": "^1.0.1", + "grunt-template-jasmine-istanbul": "^0.4.0", + "jquery": "2.1.0", "karma": "^0.13.22", "karma-chrome-launcher": "^0.2.2", "karma-firefox-launcher": "^0.1.7", @@ -41,7 +43,7 @@ "node": ">=0.10" }, "dependencies": { - "backbone": "~1.1.2", - "underscore": "~1.8.3" + "backbone": "~1.2.3", + "underscore": "^1.8.0" } } diff --git a/test/vendor/css/jasmine.css b/test/vendor/css/jasmine.css deleted file mode 100644 index 8c008dc7..00000000 --- a/test/vendor/css/jasmine.css +++ /dev/null @@ -1,82 +0,0 @@ -body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } - -#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } -#HTMLReporter a { text-decoration: none; } -#HTMLReporter a:hover { text-decoration: underline; } -#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; } -#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; } -#HTMLReporter #jasmine_content { position: fixed; right: 100%; } -#HTMLReporter .version { color: #aaaaaa; } -#HTMLReporter .banner { margin-top: 14px; } -#HTMLReporter .duration { color: #aaaaaa; float: right; } -#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; } -#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; } -#HTMLReporter .symbolSummary li.passed { font-size: 14px; } -#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; } -#HTMLReporter .symbolSummary li.failed { line-height: 9px; } -#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } -#HTMLReporter .symbolSummary li.skipped { font-size: 14px; } -#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } -#HTMLReporter .symbolSummary li.pending { line-height: 11px; } -#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } -#HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } -#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } -#HTMLReporter .runningAlert { background-color: #666666; } -#HTMLReporter .skippedAlert { background-color: #aaaaaa; } -#HTMLReporter .skippedAlert:first-child { background-color: #333333; } -#HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; } -#HTMLReporter .passingAlert { background-color: #a6b779; } -#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; } -#HTMLReporter .failingAlert { background-color: #cf867e; } -#HTMLReporter .failingAlert:first-child { background-color: #b03911; } -#HTMLReporter .results { margin-top: 14px; } -#HTMLReporter #details { display: none; } -#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; } -#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } -#HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } -#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } -#HTMLReporter.showDetails .summary { display: none; } -#HTMLReporter.showDetails #details { display: block; } -#HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } -#HTMLReporter .summary { margin-top: 14px; } -#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; } -#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; } -#HTMLReporter .summary .specSummary.failed a { color: #b03911; } -#HTMLReporter .description + .suite { margin-top: 0; } -#HTMLReporter .suite { margin-top: 14px; } -#HTMLReporter .suite a { color: #333333; } -#HTMLReporter #details .specDetail { margin-bottom: 28px; } -#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; } -#HTMLReporter .resultMessage { padding-top: 14px; color: #333333; } -#HTMLReporter .resultMessage span.result { display: block; } -#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } - -#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ } -#TrivialReporter a:visited, #TrivialReporter a { color: #303; } -#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; } -#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; } -#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; } -#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; } -#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; } -#TrivialReporter .runner.running { background-color: yellow; } -#TrivialReporter .options { text-align: right; font-size: .8em; } -#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } -#TrivialReporter .suite .suite { margin: 5px; } -#TrivialReporter .suite.passed { background-color: #dfd; } -#TrivialReporter .suite.failed { background-color: #fdd; } -#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; } -#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } -#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; } -#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; } -#TrivialReporter .spec.skipped { background-color: #bbb; } -#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } -#TrivialReporter .passed { background-color: #cfc; display: none; } -#TrivialReporter .failed { background-color: #fbb; } -#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; } -#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; } -#TrivialReporter .resultMessage .mismatch { color: black; } -#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } -#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; } -#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; } -#TrivialReporter #jasmine_content { position: fixed; right: 100%; } -#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } diff --git a/test/vendor/js/backbone-pageable.js b/test/vendor/js/backbone-pageable.js deleted file mode 100644 index 90942624..00000000 --- a/test/vendor/js/backbone-pageable.js +++ /dev/null @@ -1,1318 +0,0 @@ -/* - backbone-pageable 1.4.5 - http://github.com/backbone-paginator/backbone-pageable - - Copyright (c) 2013 Jimmy Yuen Ho Wong - Licensed under the MIT @license. -*/ - -(function (factory) { - - // CommonJS - if (typeof exports == "object") { - module.exports = factory(require("underscore"), require("backbone")); - } - // AMD - else if (typeof define == "function" && define.amd) { - define(["underscore", "backbone"], factory); - } - // Browser - else if (typeof _ !== "undefined" && typeof Backbone !== "undefined") { - var oldPageableCollection = Backbone.PageableCollection; - var PageableCollection = factory(_, Backbone); - - /** - __BROWSER ONLY__ - - If you already have an object named `PageableCollection` attached to the - `Backbone` module, you can use this to return a local reference to this - Backbone.PageableCollection class and reset the name - Backbone.PageableCollection to its previous definition. - - // The left hand side gives you a reference to this - // Backbone.PageableCollection implementation, the right hand side - // resets Backbone.PageableCollection to your other - // Backbone.PageableCollection. - var PageableCollection = Backbone.PageableCollection.noConflict(); - - @static - @member Backbone.PageableCollection - @return {Backbone.PageableCollection} - */ - Backbone.PageableCollection.noConflict = function () { - Backbone.PageableCollection = oldPageableCollection; - return PageableCollection; - }; - } - -}(function (_, Backbone) { - - "use strict"; - - var _extend = _.extend; - var _omit = _.omit; - var _clone = _.clone; - var _each = _.each; - var _pick = _.pick; - var _contains = _.contains; - var _isEmpty = _.isEmpty; - var _pairs = _.pairs; - var _invert = _.invert; - var _isArray = _.isArray; - var _isFunction = _.isFunction; - var _isObject = _.isObject; - var _keys = _.keys; - var _isUndefined = _.isUndefined; - var _result = _.result; - var ceil = Math.ceil; - var floor = Math.floor; - var max = Math.max; - - var BBColProto = Backbone.Collection.prototype; - - function finiteInt (val, name) { - if (!_.isNumber(val) || _.isNaN(val) || !_.isFinite(val) || ~~val !== val) { - throw new TypeError("`" + name + "` must be a finite integer"); - } - return val; - } - - function queryStringToParams (qs) { - var kvp, k, v, ls, params = {}, decode = decodeURIComponent; - var kvps = qs.split('&'); - for (var i = 0, l = kvps.length; i < l; i++) { - var param = kvps[i]; - kvp = param.split('='), k = kvp[0], v = kvp[1] || true; - k = decode(k), v = decode(v), ls = params[k]; - if (_isArray(ls)) ls.push(v); - else if (ls) params[k] = [ls, v]; - else params[k] = v; - } - return params; - } - - // hack to make sure the whatever event handlers for this event is run - // before func is, and the event handlers that func will trigger. - function runOnceAtLastHandler (col, event, func) { - var eventHandlers = col._events[event]; - if (eventHandlers && eventHandlers.length) { - var lastHandler = eventHandlers[eventHandlers.length - 1]; - var oldCallback = lastHandler.callback; - lastHandler.callback = function () { - try { - oldCallback.apply(this, arguments); - func(); - } - catch (e) { - throw e; - } - finally { - lastHandler.callback = oldCallback; - } - }; - } - else func(); - } - - var PARAM_TRIM_RE = /[\s'"]/g; - var URL_TRIM_RE = /[<>\s'"]/g; - - /** - Drop-in replacement for Backbone.Collection. Supports server-side and - client-side pagination and sorting. Client-side mode also support fully - multi-directional synchronization of changes between pages. - - @class Backbone.PageableCollection - @extends Backbone.Collection - */ - var PageableCollection = Backbone.PageableCollection = Backbone.Collection.extend({ - - /** - The container object to store all pagination states. - - You can override the default state by extending this class or specifying - them in an `options` hash to the constructor. - - @property {Object} state - - @property {0|1} [state.firstPage=1] The first page index. Set to 0 if - your server API uses 0-based indices. You should only override this value - during extension, initialization or reset by the server after - fetching. This value should be read only at other times. - - @property {number} [state.lastPage=null] The last page index. This value - is __read only__ and it's calculated based on whether `firstPage` is 0 or - 1, during bootstrapping, fetching and resetting. Please don't change this - value under any circumstances. - - @property {number} [state.currentPage=null] The current page index. You - should only override this value during extension, initialization or reset - by the server after fetching. This value should be read only at other - times. Can be a 0-based or 1-based index, depending on whether - `firstPage` is 0 or 1. If left as default, it will be set to `firstPage` - on initialization. - - @property {number} [state.pageSize=25] How many records to show per - page. This value is __read only__ after initialization, if you want to - change the page size after initialization, you must call #setPageSize. - - @property {number} [state.totalPages=null] How many pages there are. This - value is __read only__ and it is calculated from `totalRecords`. - - @property {number} [state.totalRecords=null] How many records there - are. This value is __required__ under server mode. This value is optional - for client mode as the number will be the same as the number of models - during bootstrapping and during fetching, either supplied by the server - in the metadata, or calculated from the size of the response. - - @property {string} [state.sortKey=null] The model attribute to use for - sorting. - - @property {-1|0|1} [state.order=-1] The order to use for sorting. Specify - -1 for ascending order or 1 for descending order. If 0, no client side - sorting will be done and the order query parameter will not be sent to - the server during a fetch. - */ - state: { - firstPage: 1, - lastPage: null, - currentPage: null, - pageSize: 25, - totalPages: null, - totalRecords: null, - sortKey: null, - order: -1 - }, - - /** - @property {"server"|"client"|"infinite"} [mode="server"] The mode of - operations for this collection. `"server"` paginates on the server-side, - `"client"` paginates on the client-side and `"infinite"` paginates on the - server-side for APIs that do not support `totalRecords`. - */ - mode: "server", - - /** - A translation map to convert Backbone.PageableCollection state attributes - to the query parameters accepted by your server API. - - You can override the default state by extending this class or specifying - them in `options.queryParams` object hash to the constructor. - - @property {Object} queryParams - @property {string} [queryParams.currentPage="page"] - @property {string} [queryParams.pageSize="per_page"] - @property {string} [queryParams.totalPages="total_pages"] - @property {string} [queryParams.totalRecords="total_entries"] - @property {string} [queryParams.sortKey="sort_by"] - @property {string} [queryParams.order="order"] - @property {string} [queryParams.directions={"-1": "asc", "1": "desc"}] A - map for translating a Backbone.PageableCollection#state.order constant to - the ones your server API accepts. - */ - queryParams: { - currentPage: "page", - pageSize: "per_page", - totalPages: "total_pages", - totalRecords: "total_entries", - sortKey: "sort_by", - order: "order", - directions: { - "-1": "asc", - "1": "desc" - } - }, - - /** - __CLIENT MODE ONLY__ - - This collection is the internal storage for the bootstrapped or fetched - models. You can use this if you want to operate on all the pages. - - @property {Backbone.Collection} fullCollection - */ - - /** - Given a list of models or model attributues, bootstraps the full - collection in client mode or infinite mode, or just the page you want in - server mode. - - If you want to initialize a collection to a different state than the - default, you can specify them in `options.state`. Any state parameters - supplied will be merged with the default. If you want to change the - default mapping from #state keys to your server API's query parameter - names, you can specifiy an object hash in `option.queryParams`. Likewise, - any mapping provided will be merged with the default. Lastly, all - Backbone.Collection constructor options are also accepted. - - See: - - - Backbone.PageableCollection#state - - Backbone.PageableCollection#queryParams - - [Backbone.Collection#initialize](http://backbonejs.org/#Collection-constructor) - - @param {Array.} [models] - - @param {Object} [options] - - @param {function(*, *): number} [options.comparator] If specified, this - comparator is set to the current page under server mode, or the #fullCollection - otherwise. - - @param {boolean} [options.full] If `false` and either a - `options.comparator` or `sortKey` is defined, the comparator is attached - to the current page. Default is `true` under client or infinite mode and - the comparator will be attached to the #fullCollection. - - @param {Object} [options.state] The state attributes overriding the defaults. - - @param {string} [options.state.sortKey] The model attribute to use for - sorting. If specified instead of `options.comparator`, a comparator will - be automatically created using this value, and optionally a sorting order - specified in `options.state.order`. The comparator is then attached to - the new collection instance. - - @param {-1|1} [options.state.order] The order to use for sorting. Specify - -1 for ascending order and 1 for descending order. - - @param {Object} [options.queryParam] - */ - constructor: function (models, options) { - - BBColProto.constructor.apply(this, arguments); - - options = options || {}; - - var mode = this.mode = options.mode || this.mode || PageableProto.mode; - - var queryParams = _extend({}, PageableProto.queryParams, this.queryParams, - options.queryParams || {}); - - queryParams.directions = _extend({}, - PageableProto.queryParams.directions, - this.queryParams.directions, - queryParams.directions || {}); - - this.queryParams = queryParams; - - var state = this.state = _extend({}, PageableProto.state, this.state, - options.state || {}); - - state.currentPage = state.currentPage == null ? - state.firstPage : - state.currentPage; - - if (!_isArray(models)) models = models ? [models] : []; - - if (mode != "server" && state.totalRecords == null && !_isEmpty(models)) { - state.totalRecords = models.length; - } - - this.switchMode(mode, _extend({fetch: false, - resetState: false, - models: models}, options)); - - var comparator = options.comparator; - - if (state.sortKey && !comparator) { - this.setSorting(state.sortKey, state.order, options); - } - - if (mode != "server") { - var fullCollection = this.fullCollection; - - if (comparator && options.full) { - this.comparator = null; - fullCollection.comparator = comparator; - } - - if (options.full) fullCollection.sort(); - - // make sure the models in the current page and full collection have the - // same references - if (models && !_isEmpty(models)) { - this.reset([].slice.call(models), _extend({silent: true}, options)); - this.getPage(state.currentPage); - models.splice.apply(models, [0, models.length].concat(this.models)); - } - } - - this._initState = _clone(this.state); - }, - - /** - Makes a Backbone.Collection that contains all the pages. - - @private - @param {Array.} models - @param {Object} options Options for Backbone.Collection constructor. - @return {Backbone.Collection} - */ - _makeFullCollection: function (models, options) { - - var properties = ["url", "model", "sync", "comparator"]; - var thisProto = this.constructor.prototype; - var i, length, prop; - - var proto = {}; - for (i = 0, length = properties.length; i < length; i++) { - prop = properties[i]; - if (!_isUndefined(thisProto[prop])) { - proto[prop] = thisProto[prop]; - } - } - - var fullCollection = new (Backbone.Collection.extend(proto))(models, options); - - for (i = 0, length = properties.length; i < length; i++) { - prop = properties[i]; - if (this[prop] !== thisProto[prop]) { - fullCollection[prop] = this[prop]; - } - } - - return fullCollection; - }, - - /** - Factory method that returns a Backbone event handler that responses to - the `add`, `remove`, `reset`, and the `sort` events. The returned event - handler will synchronize the current page collection and the full - collection's models. - - @private - - @param {Backbone.PageableCollection} pageCol - @param {Backbone.Collection} fullCol - - @return {function(string, Backbone.Model, Backbone.Collection, Object)} - Collection event handler - */ - _makeCollectionEventHandler: function (pageCol, fullCol) { - - return function collectionEventHandler (event, model, collection, options) { - - var handlers = pageCol._handlers; - _each(_keys(handlers), function (event) { - var handler = handlers[event]; - pageCol.off(event, handler); - fullCol.off(event, handler); - }); - - var state = _clone(pageCol.state); - var firstPage = state.firstPage; - var currentPage = firstPage === 0 ? - state.currentPage : - state.currentPage - 1; - var pageSize = state.pageSize; - var pageStart = currentPage * pageSize, pageEnd = pageStart + pageSize; - - if (event == "add") { - var pageIndex, fullIndex, addAt, colToAdd, options = options || {}; - if (collection == fullCol) { - fullIndex = fullCol.indexOf(model); - if (fullIndex >= pageStart && fullIndex < pageEnd) { - colToAdd = pageCol; - pageIndex = addAt = fullIndex - pageStart; - } - } - else { - pageIndex = pageCol.indexOf(model); - fullIndex = pageStart + pageIndex; - colToAdd = fullCol; - var addAt = !_isUndefined(options.at) ? - options.at + pageStart : - fullIndex; - } - - if (!options.onRemove) { - ++state.totalRecords; - delete options.onRemove; - } - - pageCol.state = pageCol._checkState(state); - - if (colToAdd) { - colToAdd.add(model, _extend({}, options || {}, {at: addAt})); - var modelToRemove = pageIndex >= pageSize ? - model : - !_isUndefined(options.at) && addAt < pageEnd && pageCol.length > pageSize ? - pageCol.at(pageSize) : - null; - if (modelToRemove) { - runOnceAtLastHandler(collection, event, function () { - pageCol.remove(modelToRemove, {onAdd: true}); - }); - } - } - } - - // remove the model from the other collection as well - if (event == "remove") { - if (!options.onAdd) { - // decrement totalRecords and update totalPages and lastPage - if (!--state.totalRecords) { - state.totalRecords = null; - state.totalPages = null; - } - else { - var totalPages = state.totalPages = ceil(state.totalRecords / pageSize); - state.lastPage = firstPage === 0 ? totalPages - 1 : totalPages || firstPage; - if (state.currentPage > totalPages) state.currentPage = state.lastPage; - } - pageCol.state = pageCol._checkState(state); - - var nextModel, removedIndex = options.index; - if (collection == pageCol) { - if (nextModel = fullCol.at(pageEnd)) { - runOnceAtLastHandler(pageCol, event, function () { - pageCol.push(nextModel, {onRemove: true}); - }); - } - fullCol.remove(model); - } - else if (removedIndex >= pageStart && removedIndex < pageEnd) { - if (nextModel = fullCol.at(pageEnd - 1)) { - runOnceAtLastHandler(pageCol, event, function() { - pageCol.push(nextModel, {onRemove: true}); - }); - } - pageCol.remove(model); - } - } - else delete options.onAdd; - } - - if (event == "reset") { - options = collection; - collection = model; - - // Reset that's not a result of getPage - if (collection == pageCol && options.from == null && - options.to == null) { - var head = fullCol.models.slice(0, pageStart); - var tail = fullCol.models.slice(pageStart + pageCol.models.length); - fullCol.reset(head.concat(pageCol.models).concat(tail), options); - } - else if (collection == fullCol) { - if (!(state.totalRecords = fullCol.models.length)) { - state.totalRecords = null; - state.totalPages = null; - } - if (pageCol.mode == "client") { - state.lastPage = state.currentPage = state.firstPage; - } - pageCol.state = pageCol._checkState(state); - pageCol.reset(fullCol.models.slice(pageStart, pageEnd), - _extend({}, options, {parse: false})); - } - } - - if (event == "sort") { - options = collection; - collection = model; - if (collection === fullCol) { - pageCol.reset(fullCol.models.slice(pageStart, pageEnd), - _extend({}, options, {parse: false})); - } - } - - _each(_keys(handlers), function (event) { - var handler = handlers[event]; - _each([pageCol, fullCol], function (col) { - col.on(event, handler); - var callbacks = col._events[event] || []; - callbacks.unshift(callbacks.pop()); - }); - }); - }; - }, - - /** - Sanity check this collection's pagination states. Only perform checks - when all the required pagination state values are defined and not null. - If `totalPages` is undefined or null, it is set to `totalRecords` / - `pageSize`. `lastPage` is set according to whether `firstPage` is 0 or 1 - when no error occurs. - - @private - - @throws {TypeError} If `totalRecords`, `pageSize`, `currentPage` or - `firstPage` is not a finite integer. - - @throws {RangeError} If `pageSize`, `currentPage` or `firstPage` is out - of bounds. - - @return {Object} Returns the `state` object if no error was found. - */ - _checkState: function (state) { - - var mode = this.mode; - var links = this.links; - var totalRecords = state.totalRecords; - var pageSize = state.pageSize; - var currentPage = state.currentPage; - var firstPage = state.firstPage; - var totalPages = state.totalPages; - - if (totalRecords != null && pageSize != null && currentPage != null && - firstPage != null && (mode == "infinite" ? links : true)) { - - totalRecords = finiteInt(totalRecords, "totalRecords"); - pageSize = finiteInt(pageSize, "pageSize"); - currentPage = finiteInt(currentPage, "currentPage"); - firstPage = finiteInt(firstPage, "firstPage"); - - if (pageSize < 1) { - throw new RangeError("`pageSize` must be >= 1"); - } - - totalPages = state.totalPages = ceil(totalRecords / pageSize); - - if (firstPage < 0 || firstPage > 1) { - throw new RangeError("`firstPage must be 0 or 1`"); - } - - state.lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage; - - if (mode == "infinite") { - if (!links[currentPage + '']) { - throw new RangeError("No link found for page " + currentPage); - } - } - else if (currentPage < firstPage || - (totalPages > 0 && - (firstPage ? currentPage > totalPages : currentPage >= totalPages))) { - throw new RangeError("`currentPage` must be firstPage <= currentPage " + - (firstPage ? ">" : ">=") + - " totalPages if " + firstPage + "-based. Got " + - currentPage + '.'); - } - } - - return state; - }, - - /** - Change the page size of this collection. - - Under most if not all circumstances, you should call this method to - change the page size of a pageable collection because it will keep the - pagination state sane. By default, the method will recalculate the - current page number to one that will retain the current page's models - when increasing the page size. When decreasing the page size, this method - will retain the last models to the current page that will fit into the - smaller page size. - - If `options.first` is true, changing the page size will also reset the - current page back to the first page instead of trying to be smart. - - For server mode operations, changing the page size will trigger a #fetch - and subsequently a `reset` event. - - For client mode operations, changing the page size will `reset` the - current page by recalculating the current page boundary on the client - side. - - If `options.fetch` is true, a fetch can be forced if the collection is in - client mode. - - @param {number} pageSize The new page size to set to #state. - @param {Object} [options] {@link #fetch} options. - @param {boolean} [options.first=false] Reset the current page number to - the first page if `true`. - @param {boolean} [options.fetch] If `true`, force a fetch in client mode. - - @throws {TypeError} If `pageSize` is not a finite integer. - @throws {RangeError} If `pageSize` is less than 1. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - setPageSize: function (pageSize, options) { - pageSize = finiteInt(pageSize, "pageSize"); - - options = options || {first: false}; - - var state = this.state; - var totalPages = ceil(state.totalRecords / pageSize); - var currentPage = totalPages ? - max(state.firstPage, - floor(totalPages * - (state.firstPage ? - state.currentPage : - state.currentPage + 1) / - state.totalPages)) : - state.firstPage; - - state = this.state = this._checkState(_extend({}, state, { - pageSize: pageSize, - currentPage: options.first ? state.firstPage : currentPage, - totalPages: totalPages - })); - - return this.getPage(state.currentPage, _omit(options, ["first"])); - }, - - /** - Switching between client, server and infinite mode. - - If switching from client to server mode, the #fullCollection is emptied - first and then deleted and a fetch is immediately issued for the current - page from the server. Pass `false` to `options.fetch` to skip fetching. - - If switching to infinite mode, and if `options.models` is given for an - array of models, #links will be populated with a URL per page, using the - default URL for this collection. - - If switching from server to client mode, all of the pages are immediately - refetched. If you have too many pages, you can pass `false` to - `options.fetch` to skip fetching. - - If switching to any mode from infinite mode, the #links will be deleted. - - @param {"server"|"client"|"infinite"} [mode] The mode to switch to. - - @param {Object} [options] - - @param {boolean} [options.fetch=true] If `false`, no fetching is done. - - @param {boolean} [options.resetState=true] If 'false', the state is not - reset, but checked for sanity instead. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this if `options.fetch` is `false`. - */ - switchMode: function (mode, options) { - - if (!_contains(["server", "client", "infinite"], mode)) { - throw new TypeError('`mode` must be one of "server", "client" or "infinite"'); - } - - options = options || {fetch: true, resetState: true}; - - var state = this.state = options.resetState ? - _clone(this._initState) : - this._checkState(_extend({}, this.state)); - - this.mode = mode; - - var self = this; - var fullCollection = this.fullCollection; - var handlers = this._handlers = this._handlers || {}, handler; - if (mode != "server" && !fullCollection) { - fullCollection = this._makeFullCollection(options.models || [], options); - fullCollection.pageableCollection = this; - this.fullCollection = fullCollection; - var allHandler = this._makeCollectionEventHandler(this, fullCollection); - _each(["add", "remove", "reset", "sort"], function (event) { - handlers[event] = handler = _.bind(allHandler, {}, event); - self.on(event, handler); - fullCollection.on(event, handler); - }); - fullCollection.comparator = this._fullComparator; - } - else if (mode == "server" && fullCollection) { - _each(_keys(handlers), function (event) { - handler = handlers[event]; - self.off(event, handler); - fullCollection.off(event, handler); - }); - delete this._handlers; - this._fullComparator = fullCollection.comparator; - delete this.fullCollection; - } - - if (mode == "infinite") { - var links = this.links = {}; - var firstPage = state.firstPage; - var totalPages = ceil(state.totalRecords / state.pageSize); - var lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage; - for (var i = state.firstPage; i <= lastPage; i++) { - links[i] = this.url; - } - } - else if (this.links) delete this.links; - - return options.fetch ? - this.fetch(_omit(options, "fetch", "resetState")) : - this; - }, - - /** - @return {boolean} `true` if this collection can page backward, `false` - otherwise. - */ - hasPrevious: function () { - var state = this.state; - var currentPage = state.currentPage; - if (this.mode != "infinite") return currentPage > state.firstPage; - return !!this.links[currentPage - 1]; - }, - - /** - @return {boolean} `true` if this collection can page forward, `false` - otherwise. - */ - hasNext: function () { - var state = this.state; - var currentPage = this.state.currentPage; - if (this.mode != "infinite") return currentPage < state.lastPage; - return !!this.links[currentPage + 1]; - }, - - /** - Fetch the first page in server mode, or reset the current page of this - collection to the first page in client or infinite mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getFirstPage: function (options) { - return this.getPage("first", options); - }, - - /** - Fetch the previous page in server mode, or reset the current page of this - collection to the previous page in client or infinite mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getPreviousPage: function (options) { - return this.getPage("prev", options); - }, - - /** - Fetch the next page in server mode, or reset the current page of this - collection to the next page in client mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getNextPage: function (options) { - return this.getPage("next", options); - }, - - /** - Fetch the last page in server mode, or reset the current page of this - collection to the last page in client mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getLastPage: function (options) { - return this.getPage("last", options); - }, - - /** - Given a page index, set #state.currentPage to that index. If this - collection is in server mode, fetch the page using the updated state, - otherwise, reset the current page of this collection to the page - specified by `index` in client mode. If `options.fetch` is true, a fetch - can be forced in client mode before resetting the current page. Under - infinite mode, if the index is less than the current page, a reset is - done as in client mode. If the index is greater than the current page - number, a fetch is made with the results **appended** to #fullCollection. - The current page will then be reset after fetching. - - @param {number|string} index The page index to go to, or the page name to - look up from #links in infinite mode. - @param {Object} [options] {@link #fetch} options or - [reset](http://backbonejs.org/#Collection-reset) options for client mode - when `options.fetch` is `false`. - @param {boolean} [options.fetch=false] If true, force a {@link #fetch} in - client mode. - - @throws {TypeError} If `index` is not a finite integer under server or - client mode, or does not yield a URL from #links under infinite mode. - - @throws {RangeError} If `index` is out of bounds. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getPage: function (index, options) { - - var mode = this.mode, fullCollection = this.fullCollection; - - options = options || {fetch: false}; - - var state = this.state, - firstPage = state.firstPage, - currentPage = state.currentPage, - lastPage = state.lastPage, - pageSize = state.pageSize; - - var pageNum = index; - switch (index) { - case "first": pageNum = firstPage; break; - case "prev": pageNum = currentPage - 1; break; - case "next": pageNum = currentPage + 1; break; - case "last": pageNum = lastPage; break; - default: pageNum = finiteInt(index, "index"); - } - - this.state = this._checkState(_extend({}, state, {currentPage: pageNum})); - - options.from = currentPage, options.to = pageNum; - - var pageStart = (firstPage === 0 ? pageNum : pageNum - 1) * pageSize; - var pageModels = fullCollection && fullCollection.length ? - fullCollection.models.slice(pageStart, pageStart + pageSize) : - []; - if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) && - !options.fetch) { - this.reset(pageModels, _omit(options, "fetch")); - return this; - } - - if (mode == "infinite") options.url = this.links[pageNum]; - - return this.fetch(_omit(options, "fetch")); - }, - - /** - Fetch the page for the provided item offset in server mode, or reset the current page of this - collection to the page for the provided item offset in client mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getPageByOffset: function (offset, options) { - if (offset < 0) { - throw new RangeError("`offset must be > 0`"); - } - offset = finiteInt(offset); - - var page = floor(offset / this.state.pageSize); - if (this.state.firstPage !== 0) page++; - if (page > this.state.lastPage) page = this.state.lastPage; - return this.getPage(page, options); - }, - - /** - Overidden to make `getPage` compatible with Zepto. - - @param {string} method - @param {Backbone.Model|Backbone.Collection} model - @param {Object} [options] - - @return {XMLHttpRequest} - */ - sync: function (method, model, options) { - var self = this; - if (self.mode == "infinite") { - var success = options.success; - var currentPage = self.state.currentPage; - options.success = function (resp, status, xhr) { - var links = self.links; - var newLinks = self.parseLinks(resp, _extend({xhr: xhr}, options)); - if (newLinks.first) links[self.state.firstPage] = newLinks.first; - if (newLinks.prev) links[currentPage - 1] = newLinks.prev; - if (newLinks.next) links[currentPage + 1] = newLinks.next; - if (success) success(resp, status, xhr); - }; - } - - return (BBColProto.sync || Backbone.sync).call(self, method, model, options); - }, - - /** - Parse pagination links from the server response. Only valid under - infinite mode. - - Given a response body and a XMLHttpRequest object, extract pagination - links from them for infinite paging. - - This default implementation parses the RFC 5988 `Link` header and extract - 3 links from it - `first`, `prev`, `next`. Any subclasses overriding this - method __must__ return an object hash having only the keys - above. However, simply returning a `next` link or an empty hash if there - are no more links should be enough for most implementations. - - @param {*} resp The deserialized response body. - @param {Object} [options] - @param {XMLHttpRequest} [options.xhr] The XMLHttpRequest object for this - response. - @return {Object} - */ - parseLinks: function (resp, options) { - var links = {}; - var linkHeader = options.xhr.getResponseHeader("Link"); - if (linkHeader) { - var relations = ["first", "prev", "next"]; - _each(linkHeader.split(","), function (linkValue) { - var linkParts = linkValue.split(";"); - var url = linkParts[0].replace(URL_TRIM_RE, ''); - var params = linkParts.slice(1); - _each(params, function (param) { - var paramParts = param.split("="); - var key = paramParts[0].replace(PARAM_TRIM_RE, ''); - var value = paramParts[1].replace(PARAM_TRIM_RE, ''); - if (key == "rel" && _contains(relations, value)) links[value] = url; - }); - }); - } - - return links; - }, - - /** - Parse server response data. - - This default implementation assumes the response data is in one of two - structures: - - [ - {}, // Your new pagination state - [{}, ...] // An array of JSON objects - ] - - Or, - - [{}] // An array of JSON objects - - The first structure is the preferred form because the pagination states - may have been updated on the server side, sending them down again allows - this collection to update its states. If the response has a pagination - state object, it is checked for errors. - - The second structure is the - [Backbone.Collection#parse](http://backbonejs.org/#Collection-parse) - default. - - **Note:** this method has been further simplified since 1.1.7. While - existing #parse implementations will continue to work, new code is - encouraged to override #parseState and #parseRecords instead. - - @param {Object} resp The deserialized response data from the server. - @param {Object} the options for the ajax request - - @return {Array.} An array of model objects - */ - parse: function (resp, options) { - var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state), options); - if (newState) this.state = this._checkState(_extend({}, this.state, newState)); - return this.parseRecords(resp, options); - }, - - /** - Parse server response for server pagination state updates. Not applicable - under infinite mode. - - This default implementation first checks whether the response has any - state object as documented in #parse. If it exists, a state object is - returned by mapping the server state keys to this pageable collection - instance's query parameter keys using `queryParams`. - - It is __NOT__ neccessary to return a full state object complete with all - the mappings defined in #queryParams. Any state object resulted is merged - with a copy of the current pageable collection state and checked for - sanity before actually updating. Most of the time, simply providing a new - `totalRecords` value is enough to trigger a full pagination state - recalculation. - - parseState: function (resp, queryParams, state, options) { - return {totalRecords: resp.total_entries}; - } - - If you want to use header fields use: - - parseState: function (resp, queryParams, state, options) { - return {totalRecords: options.xhr.getResponseHeader("X-total")}; - } - - This method __MUST__ return a new state object instead of directly - modifying the #state object. The behavior of directly modifying #state is - undefined. - - @param {Object} resp The deserialized response data from the server. - @param {Object} queryParams A copy of #queryParams. - @param {Object} state A copy of #state. - @param {Object} [options] The options passed through from - `parse`. (backbone >= 0.9.10 only) - - @return {Object} A new (partial) state object. - */ - parseState: function (resp, queryParams, state, options) { - if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) { - - var newState = _clone(state); - var serverState = resp[0]; - - _each(_pairs(_omit(queryParams, "directions")), function (kvp) { - var k = kvp[0], v = kvp[1]; - var serverVal = serverState[v]; - if (!_isUndefined(serverVal) && !_.isNull(serverVal)) newState[k] = serverState[v]; - }); - - if (serverState.order) { - newState.order = _invert(queryParams.directions)[serverState.order] * 1; - } - - return newState; - } - }, - - /** - Parse server response for an array of model objects. - - This default implementation first checks whether the response has any - state object as documented in #parse. If it exists, the array of model - objects is assumed to be the second element, otherwise the entire - response is returned directly. - - @param {Object} resp The deserialized response data from the server. - @param {Object} [options] The options passed through from the - `parse`. (backbone >= 0.9.10 only) - - @return {Array.} An array of model objects - */ - parseRecords: function (resp, options) { - if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) { - return resp[1]; - } - - return resp; - }, - - /** - Fetch a page from the server in server mode, or all the pages in client - mode. Under infinite mode, the current page is refetched by default and - then reset. - - The query string is constructed by translating the current pagination - state to your server API query parameter using #queryParams. The current - page will reset after fetch. - - @param {Object} [options] Accepts all - [Backbone.Collection#fetch](http://backbonejs.org/#Collection-fetch) - options. - - @return {XMLHttpRequest} - */ - fetch: function (options) { - - options = options || {}; - - var state = this._checkState(this.state); - - var mode = this.mode; - - if (mode == "infinite" && !options.url) { - options.url = this.links[state.currentPage]; - } - - var data = options.data || {}; - - // dedup query params - var url = _result(options, "url") || _result(this, "url") || ''; - var qsi = url.indexOf('?'); - if (qsi != -1) { - _extend(data, queryStringToParams(url.slice(qsi + 1))); - url = url.slice(0, qsi); - } - - options.url = url; - options.data = data; - - // map params except directions - var queryParams = this.mode == "client" ? - _pick(this.queryParams, "sortKey", "order") : - _omit(_pick(this.queryParams, _keys(PageableProto.queryParams)), - "directions"); - - var i, kvp, k, v, kvps = _pairs(queryParams), thisCopy = _clone(this); - for (i = 0; i < kvps.length; i++) { - kvp = kvps[i], k = kvp[0], v = kvp[1]; - v = _isFunction(v) ? v.call(thisCopy) : v; - if (state[k] != null && v != null) { - data[v] = state[k]; - } - } - - // fix up sorting parameters - if (state.sortKey && state.order) { - data[queryParams.order] = this.queryParams.directions[state.order + ""]; - } - else if (!state.sortKey) delete data[queryParams.order]; - - // map extra query parameters - var extraKvps = _pairs(_omit(this.queryParams, - _keys(PageableProto.queryParams))); - for (i = 0; i < extraKvps.length; i++) { - kvp = extraKvps[i]; - v = kvp[1]; - v = _isFunction(v) ? v.call(thisCopy) : v; - if (v != null) data[kvp[0]] = v; - } - - if (mode != "server") { - var self = this, fullCol = this.fullCollection; - var success = options.success; - options.success = function (col, resp, opts) { - - // make sure the caller's intent is obeyed - opts = opts || {}; - if (_isUndefined(options.silent)) delete opts.silent; - else opts.silent = options.silent; - - var models = col.models; - if (mode == "client") fullCol.reset(models, opts); - else { - fullCol.add(models, _extend({at: fullCol.length}, - _extend(opts, {parse: false}))); - self.trigger("reset", self, opts); - } - - if (success) success(col, resp, opts); - }; - - // silent the first reset from backbone - return BBColProto.fetch.call(this, _extend({}, options, {silent: true})); - } - - return BBColProto.fetch.call(this, options); - }, - - /** - Convenient method for making a `comparator` sorted by a model attribute - identified by `sortKey` and ordered by `order`. - - Like a Backbone.Collection, a Backbone.PageableCollection will maintain - the __current page__ in sorted order on the client side if a `comparator` - is attached to it. If the collection is in client mode, you can attach a - comparator to #fullCollection to have all the pages reflect the global - sorting order by specifying an option `full` to `true`. You __must__ call - `sort` manually or #fullCollection.sort after calling this method to - force a resort. - - While you can use this method to sort the current page in server mode, - the sorting order may not reflect the global sorting order due to the - additions or removals of the records on the server since the last - fetch. If you want the most updated page in a global sorting order, it is - recommended that you set #state.sortKey and optionally #state.order, and - then call #fetch. - - @protected - - @param {string} [sortKey=this.state.sortKey] See `state.sortKey`. - @param {number} [order=this.state.order] See `state.order`. - @param {(function(Backbone.Model, string): Object) | string} [sortValue] See #setSorting. - - See [Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator). - */ - _makeComparator: function (sortKey, order, sortValue) { - var state = this.state; - - sortKey = sortKey || state.sortKey; - order = order || state.order; - - if (!sortKey || !order) return; - - if (!sortValue) sortValue = function (model, attr) { - return model.get(attr); - }; - - return function (left, right) { - var l = sortValue(left, sortKey), r = sortValue(right, sortKey), t; - if (order === 1) t = l, l = r, r = t; - if (l === r) return 0; - else if (l < r) return -1; - return 1; - }; - }, - - /** - Adjusts the sorting for this pageable collection. - - Given a `sortKey` and an `order`, sets `state.sortKey` and - `state.order`. A comparator can be applied on the client side to sort in - the order defined if `options.side` is `"client"`. By default the - comparator is applied to the #fullCollection. Set `options.full` to - `false` to apply a comparator to the current page under any mode. Setting - `sortKey` to `null` removes the comparator from both the current page and - the full collection. - - If a `sortValue` function is given, it will be passed the `(model, - sortKey)` arguments and is used to extract a value from the model during - comparison sorts. If `sortValue` is not given, `model.get(sortKey)` is - used for sorting. - - @chainable - - @param {string} sortKey See `state.sortKey`. - @param {number} [order=this.state.order] See `state.order`. - @param {Object} [options] - @param {"server"|"client"} [options.side] By default, `"client"` if - `mode` is `"client"`, `"server"` otherwise. - @param {boolean} [options.full=true] - @param {(function(Backbone.Model, string): Object) | string} [options.sortValue] - */ - setSorting: function (sortKey, order, options) { - - var state = this.state; - - state.sortKey = sortKey; - state.order = order = order || state.order; - - var fullCollection = this.fullCollection; - - var delComp = false, delFullComp = false; - - if (!sortKey) delComp = delFullComp = true; - - var mode = this.mode; - options = _extend({side: mode == "client" ? mode : "server", full: true}, - options); - - var comparator = this._makeComparator(sortKey, order, options.sortValue); - - var full = options.full, side = options.side; - - if (side == "client") { - if (full) { - if (fullCollection) fullCollection.comparator = comparator; - delComp = true; - } - else { - this.comparator = comparator; - delFullComp = true; - } - } - else if (side == "server" && !full) { - this.comparator = comparator; - } - - if (delComp) this.comparator = null; - if (delFullComp && fullCollection) fullCollection.comparator = null; - - return this; - } - - }); - - var PageableProto = PageableCollection.prototype; - - return PageableCollection; - -})); diff --git a/test/vendor/js/backbone.js b/test/vendor/js/backbone.js deleted file mode 100644 index 24a550a0..00000000 --- a/test/vendor/js/backbone.js +++ /dev/null @@ -1,1608 +0,0 @@ -// Backbone.js 1.1.2 - -// (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors -// Backbone may be freely distributed under the MIT license. -// For all details and documentation: -// http://backbonejs.org - -(function(root, factory) { - - // Set up Backbone appropriately for the environment. Start with AMD. - if (typeof define === 'function' && define.amd) { - define(['underscore', 'jquery', 'exports'], function(_, $, exports) { - // Export global even in AMD case in case this script is loaded with - // others that may still expect a global Backbone. - root.Backbone = factory(root, exports, _, $); - }); - - // Next for Node.js or CommonJS. jQuery may not be needed as a module. - } else if (typeof exports !== 'undefined') { - var _ = require('underscore'); - factory(root, exports, _); - - // Finally, as a browser global. - } else { - root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); - } - -}(this, function(root, Backbone, _, $) { - - // Initial Setup - // ------------- - - // Save the previous value of the `Backbone` variable, so that it can be - // restored later on, if `noConflict` is used. - var previousBackbone = root.Backbone; - - // Create local references to array methods we'll want to use later. - var array = []; - var push = array.push; - var slice = array.slice; - var splice = array.splice; - - // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '1.1.2'; - - // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns - // the `$` variable. - Backbone.$ = $; - - // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable - // to its previous owner. Returns a reference to this Backbone object. - Backbone.noConflict = function() { - root.Backbone = previousBackbone; - return this; - }; - - // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option - // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and - // set a `X-Http-Method-Override` header. - Backbone.emulateHTTP = false; - - // Turn on `emulateJSON` to support legacy servers that can't deal with direct - // `application/json` requests ... will encode the body as - // `application/x-www-form-urlencoded` instead and will send the model in a - // form param named `model`. - Backbone.emulateJSON = false; - - // Backbone.Events - // --------------- - - // A module that can be mixed in to *any object* in order to provide it with - // custom events. You may bind with `on` or remove with `off` callback - // functions to an event; `trigger`-ing an event fires all callbacks in - // succession. - // - // var object = {}; - // _.extend(object, Backbone.Events); - // object.on('expand', function(){ alert('expanded'); }); - // object.trigger('expand'); - // - var Events = Backbone.Events = { - - // Bind an event to a `callback` function. Passing `"all"` will bind - // the callback to all events fired. - on: function(name, callback, context) { - if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; - this._events || (this._events = {}); - var events = this._events[name] || (this._events[name] = []); - events.push({callback: callback, context: context, ctx: context || this}); - return this; - }, - - // Bind an event to only be triggered a single time. After the first time - // the callback is invoked, it will be removed. - once: function(name, callback, context) { - if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; - var self = this; - var once = _.once(function() { - self.off(name, once); - callback.apply(this, arguments); - }); - once._callback = callback; - return this.on(name, once, context); - }, - - // Remove one or many callbacks. If `context` is null, removes all - // callbacks with that function. If `callback` is null, removes all - // callbacks for the event. If `name` is null, removes all bound - // callbacks for all events. - off: function(name, callback, context) { - var retain, ev, events, names, i, l, j, k; - if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; - if (!name && !callback && !context) { - this._events = void 0; - return this; - } - names = name ? [name] : _.keys(this._events); - for (i = 0, l = names.length; i < l; i++) { - name = names[i]; - if (events = this._events[name]) { - this._events[name] = retain = []; - if (callback || context) { - for (j = 0, k = events.length; j < k; j++) { - ev = events[j]; - if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || - (context && context !== ev.context)) { - retain.push(ev); - } - } - } - if (!retain.length) delete this._events[name]; - } - } - - return this; - }, - - // Trigger one or many events, firing all bound callbacks. Callbacks are - // passed the same arguments as `trigger` is, apart from the event name - // (unless you're listening on `"all"`, which will cause your callback to - // receive the true name of the event as the first argument). - trigger: function(name) { - if (!this._events) return this; - var args = slice.call(arguments, 1); - if (!eventsApi(this, 'trigger', name, args)) return this; - var events = this._events[name]; - var allEvents = this._events.all; - if (events) triggerEvents(events, args); - if (allEvents) triggerEvents(allEvents, arguments); - return this; - }, - - // Tell this object to stop listening to either specific events ... or - // to every object it's currently listening to. - stopListening: function(obj, name, callback) { - var listeningTo = this._listeningTo; - if (!listeningTo) return this; - var remove = !name && !callback; - if (!callback && typeof name === 'object') callback = this; - if (obj) (listeningTo = {})[obj._listenId] = obj; - for (var id in listeningTo) { - obj = listeningTo[id]; - obj.off(name, callback, this); - if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; - } - return this; - } - - }; - - // Regular expression used to split event strings. - var eventSplitter = /\s+/; - - // Implement fancy features of the Events API such as multiple event - // names `"change blur"` and jQuery-style event maps `{change: action}` - // in terms of the existing API. - var eventsApi = function(obj, action, name, rest) { - if (!name) return true; - - // Handle event maps. - if (typeof name === 'object') { - for (var key in name) { - obj[action].apply(obj, [key, name[key]].concat(rest)); - } - return false; - } - - // Handle space separated event names. - if (eventSplitter.test(name)) { - var names = name.split(eventSplitter); - for (var i = 0, l = names.length; i < l; i++) { - obj[action].apply(obj, [names[i]].concat(rest)); - } - return false; - } - - return true; - }; - - // A difficult-to-believe, but optimized internal dispatch function for - // triggering events. Tries to keep the usual cases speedy (most internal - // Backbone events have 3 arguments). - var triggerEvents = function(events, args) { - var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; - switch (args.length) { - case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; - case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; - case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; - case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; - default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; - } - }; - - var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; - - // Inversion-of-control versions of `on` and `once`. Tell *this* object to - // listen to an event in another object ... keeping track of what it's - // listening to. - _.each(listenMethods, function(implementation, method) { - Events[method] = function(obj, name, callback) { - var listeningTo = this._listeningTo || (this._listeningTo = {}); - var id = obj._listenId || (obj._listenId = _.uniqueId('l')); - listeningTo[id] = obj; - if (!callback && typeof name === 'object') callback = this; - obj[implementation](name, callback, this); - return this; - }; - }); - - // Aliases for backwards compatibility. - Events.bind = Events.on; - Events.unbind = Events.off; - - // Allow the `Backbone` object to serve as a global event bus, for folks who - // want global "pubsub" in a convenient place. - _.extend(Backbone, Events); - - // Backbone.Model - // -------------- - - // Backbone **Models** are the basic data object in the framework -- - // frequently representing a row in a table in a database on your server. - // A discrete chunk of data and a bunch of useful, related methods for - // performing computations and transformations on that data. - - // Create a new model with the specified attributes. A client id (`cid`) - // is automatically generated and assigned for you. - var Model = Backbone.Model = function(attributes, options) { - var attrs = attributes || {}; - options || (options = {}); - this.cid = _.uniqueId('c'); - this.attributes = {}; - if (options.collection) this.collection = options.collection; - if (options.parse) attrs = this.parse(attrs, options) || {}; - attrs = _.defaults({}, attrs, _.result(this, 'defaults')); - this.set(attrs, options); - this.changed = {}; - this.initialize.apply(this, arguments); - }; - - // Attach all inheritable methods to the Model prototype. - _.extend(Model.prototype, Events, { - - // A hash of attributes whose current and previous value differ. - changed: null, - - // The value returned during the last failed validation. - validationError: null, - - // The default name for the JSON `id` attribute is `"id"`. MongoDB and - // CouchDB users may want to set this to `"_id"`. - idAttribute: 'id', - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // Return a copy of the model's `attributes` object. - toJSON: function(options) { - return _.clone(this.attributes); - }, - - // Proxy `Backbone.sync` by default -- but override this if you need - // custom syncing semantics for *this* particular model. - sync: function() { - return Backbone.sync.apply(this, arguments); - }, - - // Get the value of an attribute. - get: function(attr) { - return this.attributes[attr]; - }, - - // Get the HTML-escaped value of an attribute. - escape: function(attr) { - return _.escape(this.get(attr)); - }, - - // Returns `true` if the attribute contains a value that is not null - // or undefined. - has: function(attr) { - return this.get(attr) != null; - }, - - // Set a hash of model attributes on the object, firing `"change"`. This is - // the core primitive operation of a model, updating the data and notifying - // anyone who needs to know about the change in state. The heart of the beast. - set: function(key, val, options) { - var attr, attrs, unset, changes, silent, changing, prev, current; - if (key == null) return this; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - options || (options = {}); - - // Run validation. - if (!this._validate(attrs, options)) return false; - - // Extract attributes and options. - unset = options.unset; - silent = options.silent; - changes = []; - changing = this._changing; - this._changing = true; - - if (!changing) { - this._previousAttributes = _.clone(this.attributes); - this.changed = {}; - } - current = this.attributes, prev = this._previousAttributes; - - // Check for changes of `id`. - if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; - - // For each `set` attribute, update or delete the current value. - for (attr in attrs) { - val = attrs[attr]; - if (!_.isEqual(current[attr], val)) changes.push(attr); - if (!_.isEqual(prev[attr], val)) { - this.changed[attr] = val; - } else { - delete this.changed[attr]; - } - unset ? delete current[attr] : current[attr] = val; - } - - // Trigger all relevant attribute changes. - if (!silent) { - if (changes.length) this._pending = options; - for (var i = 0, l = changes.length; i < l; i++) { - this.trigger('change:' + changes[i], this, current[changes[i]], options); - } - } - - // You might be wondering why there's a `while` loop here. Changes can - // be recursively nested within `"change"` events. - if (changing) return this; - if (!silent) { - while (this._pending) { - options = this._pending; - this._pending = false; - this.trigger('change', this, options); - } - } - this._pending = false; - this._changing = false; - return this; - }, - - // Remove an attribute from the model, firing `"change"`. `unset` is a noop - // if the attribute doesn't exist. - unset: function(attr, options) { - return this.set(attr, void 0, _.extend({}, options, {unset: true})); - }, - - // Clear all attributes on the model, firing `"change"`. - clear: function(options) { - var attrs = {}; - for (var key in this.attributes) attrs[key] = void 0; - return this.set(attrs, _.extend({}, options, {unset: true})); - }, - - // Determine if the model has changed since the last `"change"` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged: function(attr) { - if (attr == null) return !_.isEmpty(this.changed); - return _.has(this.changed, attr); - }, - - // Return an object containing all the attributes that have changed, or - // false if there are no changed attributes. Useful for determining what - // parts of a view need to be updated and/or what attributes need to be - // persisted to the server. Unset attributes will be set to undefined. - // You can also pass an attributes object to diff against the model, - // determining if there *would be* a change. - changedAttributes: function(diff) { - if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; - var val, changed = false; - var old = this._changing ? this._previousAttributes : this.attributes; - for (var attr in diff) { - if (_.isEqual(old[attr], (val = diff[attr]))) continue; - (changed || (changed = {}))[attr] = val; - } - return changed; - }, - - // Get the previous value of an attribute, recorded at the time the last - // `"change"` event was fired. - previous: function(attr) { - if (attr == null || !this._previousAttributes) return null; - return this._previousAttributes[attr]; - }, - - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes: function() { - return _.clone(this._previousAttributes); - }, - - // Fetch the model from the server. If the server's representation of the - // model differs from its current attributes, they will be overridden, - // triggering a `"change"` event. - fetch: function(options) { - options = options ? _.clone(options) : {}; - if (options.parse === void 0) options.parse = true; - var model = this; - var success = options.success; - options.success = function(resp) { - if (!model.set(model.parse(resp, options), options)) return false; - if (success) success(model, resp, options); - model.trigger('sync', model, resp, options); - }; - wrapError(this, options); - return this.sync('read', this, options); - }, - - // Set a hash of model attributes, and sync the model to the server. - // If the server returns an attributes hash that differs, the model's - // state will be `set` again. - save: function(key, val, options) { - var attrs, method, xhr, attributes = this.attributes; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (key == null || typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - options = _.extend({validate: true}, options); - - // If we're not waiting and attributes exist, save acts as - // `set(attr).save(null, opts)` with validation. Otherwise, check if - // the model will be valid when the attributes, if any, are set. - if (attrs && !options.wait) { - if (!this.set(attrs, options)) return false; - } else { - if (!this._validate(attrs, options)) return false; - } - - // Set temporary attributes if `{wait: true}`. - if (attrs && options.wait) { - this.attributes = _.extend({}, attributes, attrs); - } - - // After a successful server-side save, the client is (optionally) - // updated with the server-side state. - if (options.parse === void 0) options.parse = true; - var model = this; - var success = options.success; - options.success = function(resp) { - // Ensure attributes are restored during synchronous saves. - model.attributes = attributes; - var serverAttrs = model.parse(resp, options); - if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); - if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { - return false; - } - if (success) success(model, resp, options); - model.trigger('sync', model, resp, options); - }; - wrapError(this, options); - - method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); - if (method === 'patch') options.attrs = attrs; - xhr = this.sync(method, this, options); - - // Restore attributes. - if (attrs && options.wait) this.attributes = attributes; - - return xhr; - }, - - // Destroy this model on the server if it was already persisted. - // Optimistically removes the model from its collection, if it has one. - // If `wait: true` is passed, waits for the server to respond before removal. - destroy: function(options) { - options = options ? _.clone(options) : {}; - var model = this; - var success = options.success; - - var destroy = function() { - model.trigger('destroy', model, model.collection, options); - }; - - options.success = function(resp) { - if (options.wait || model.isNew()) destroy(); - if (success) success(model, resp, options); - if (!model.isNew()) model.trigger('sync', model, resp, options); - }; - - if (this.isNew()) { - options.success(); - return false; - } - wrapError(this, options); - - var xhr = this.sync('delete', this, options); - if (!options.wait) destroy(); - return xhr; - }, - - // Default URL for the model's representation on the server -- if you're - // using Backbone's restful methods, override this to change the endpoint - // that will be called. - url: function() { - var base = - _.result(this, 'urlRoot') || - _.result(this.collection, 'url') || - urlError(); - if (this.isNew()) return base; - return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); - }, - - // **parse** converts a response into the hash of attributes to be `set` on - // the model. The default implementation is just to pass the response along. - parse: function(resp, options) { - return resp; - }, - - // Create a new model with identical attributes to this one. - clone: function() { - return new this.constructor(this.attributes); - }, - - // A model is new if it has never been saved to the server, and lacks an id. - isNew: function() { - return !this.has(this.idAttribute); - }, - - // Check if the model is currently in a valid state. - isValid: function(options) { - return this._validate({}, _.extend(options || {}, { validate: true })); - }, - - // Run validation against the next complete set of model attributes, - // returning `true` if all is well. Otherwise, fire an `"invalid"` event. - _validate: function(attrs, options) { - if (!options.validate || !this.validate) return true; - attrs = _.extend({}, this.attributes, attrs); - var error = this.validationError = this.validate(attrs, options) || null; - if (!error) return true; - this.trigger('invalid', this, error, _.extend(options, {validationError: error})); - return false; - } - - }); - - // Underscore methods that we want to implement on the Model. - var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; - - // Mix in each Underscore method as a proxy to `Model#attributes`. - _.each(modelMethods, function(method) { - Model.prototype[method] = function() { - var args = slice.call(arguments); - args.unshift(this.attributes); - return _[method].apply(_, args); - }; - }); - - // Backbone.Collection - // ------------------- - - // If models tend to represent a single row of data, a Backbone Collection is - // more analagous to a table full of data ... or a small slice or page of that - // table, or a collection of rows that belong together for a particular reason - // -- all of the messages in this particular folder, all of the documents - // belonging to this particular author, and so on. Collections maintain - // indexes of their models, both in order, and for lookup by `id`. - - // Create a new **Collection**, perhaps to contain a specific type of `model`. - // If a `comparator` is specified, the Collection will maintain - // its models in sort order, as they're added and removed. - var Collection = Backbone.Collection = function(models, options) { - options || (options = {}); - if (options.model) this.model = options.model; - if (options.comparator !== void 0) this.comparator = options.comparator; - this._reset(); - this.initialize.apply(this, arguments); - if (models) this.reset(models, _.extend({silent: true}, options)); - }; - - // Default options for `Collection#set`. - var setOptions = {add: true, remove: true, merge: true}; - var addOptions = {add: true, remove: false}; - - // Define the Collection's inheritable methods. - _.extend(Collection.prototype, Events, { - - // The default model for a collection is just a **Backbone.Model**. - // This should be overridden in most cases. - model: Model, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // The JSON representation of a Collection is an array of the - // models' attributes. - toJSON: function(options) { - return this.map(function(model){ return model.toJSON(options); }); - }, - - // Proxy `Backbone.sync` by default. - sync: function() { - return Backbone.sync.apply(this, arguments); - }, - - // Add a model, or list of models to the set. - add: function(models, options) { - return this.set(models, _.extend({merge: false}, options, addOptions)); - }, - - // Remove a model, or a list of models from the set. - remove: function(models, options) { - var singular = !_.isArray(models); - models = singular ? [models] : _.clone(models); - options || (options = {}); - var i, l, index, model; - for (i = 0, l = models.length; i < l; i++) { - model = models[i] = this.get(models[i]); - if (!model) continue; - delete this._byId[model.id]; - delete this._byId[model.cid]; - index = this.indexOf(model); - this.models.splice(index, 1); - this.length--; - if (!options.silent) { - options.index = index; - model.trigger('remove', model, this, options); - } - this._removeReference(model, options); - } - return singular ? models[0] : models; - }, - - // Update a collection by `set`-ing a new list of models, adding new ones, - // removing models that are no longer present, and merging models that - // already exist in the collection, as necessary. Similar to **Model#set**, - // the core operation for updating the data contained by the collection. - set: function(models, options) { - options = _.defaults({}, options, setOptions); - if (options.parse) models = this.parse(models, options); - var singular = !_.isArray(models); - models = singular ? (models ? [models] : []) : _.clone(models); - var i, l, id, model, attrs, existing, sort; - var at = options.at; - var targetModel = this.model; - var sortable = this.comparator && (at == null) && options.sort !== false; - var sortAttr = _.isString(this.comparator) ? this.comparator : null; - var toAdd = [], toRemove = [], modelMap = {}; - var add = options.add, merge = options.merge, remove = options.remove; - var order = !sortable && add && remove ? [] : false; - - // Turn bare objects into model references, and prevent invalid models - // from being added. - for (i = 0, l = models.length; i < l; i++) { - attrs = models[i] || {}; - if (attrs instanceof Model) { - id = model = attrs; - } else { - id = attrs[targetModel.prototype.idAttribute || 'id']; - } - - // If a duplicate is found, prevent it from being added and - // optionally merge it into the existing model. - if (existing = this.get(id)) { - if (remove) modelMap[existing.cid] = true; - if (merge) { - attrs = attrs === model ? model.attributes : attrs; - if (options.parse) attrs = existing.parse(attrs, options); - existing.set(attrs, options); - if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; - } - models[i] = existing; - - // If this is a new, valid model, push it to the `toAdd` list. - } else if (add) { - model = models[i] = this._prepareModel(attrs, options); - if (!model) continue; - toAdd.push(model); - this._addReference(model, options); - } - - // Do not add multiple models with the same `id`. - model = existing || model; - if (order && (model.isNew() || !modelMap[model.id])) order.push(model); - modelMap[model.id] = true; - } - - // Remove nonexistent models if appropriate. - if (remove) { - for (i = 0, l = this.length; i < l; ++i) { - if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); - } - if (toRemove.length) this.remove(toRemove, options); - } - - // See if sorting is needed, update `length` and splice in new models. - if (toAdd.length || (order && order.length)) { - if (sortable) sort = true; - this.length += toAdd.length; - if (at != null) { - for (i = 0, l = toAdd.length; i < l; i++) { - this.models.splice(at + i, 0, toAdd[i]); - } - } else { - if (order) this.models.length = 0; - var orderedModels = order || toAdd; - for (i = 0, l = orderedModels.length; i < l; i++) { - this.models.push(orderedModels[i]); - } - } - } - - // Silently sort the collection if appropriate. - if (sort) this.sort({silent: true}); - - // Unless silenced, it's time to fire all appropriate add/sort events. - if (!options.silent) { - for (i = 0, l = toAdd.length; i < l; i++) { - (model = toAdd[i]).trigger('add', model, this, options); - } - if (sort || (order && order.length)) this.trigger('sort', this, options); - } - - // Return the added (or merged) model (or models). - return singular ? models[0] : models; - }, - - // When you have more items than you want to add or remove individually, - // you can reset the entire set with a new list of models, without firing - // any granular `add` or `remove` events. Fires `reset` when finished. - // Useful for bulk operations and optimizations. - reset: function(models, options) { - options || (options = {}); - for (var i = 0, l = this.models.length; i < l; i++) { - this._removeReference(this.models[i], options); - } - options.previousModels = this.models; - this._reset(); - models = this.add(models, _.extend({silent: true}, options)); - if (!options.silent) this.trigger('reset', this, options); - return models; - }, - - // Add a model to the end of the collection. - push: function(model, options) { - return this.add(model, _.extend({at: this.length}, options)); - }, - - // Remove a model from the end of the collection. - pop: function(options) { - var model = this.at(this.length - 1); - this.remove(model, options); - return model; - }, - - // Add a model to the beginning of the collection. - unshift: function(model, options) { - return this.add(model, _.extend({at: 0}, options)); - }, - - // Remove a model from the beginning of the collection. - shift: function(options) { - var model = this.at(0); - this.remove(model, options); - return model; - }, - - // Slice out a sub-array of models from the collection. - slice: function() { - return slice.apply(this.models, arguments); - }, - - // Get a model from the set by id. - get: function(obj) { - if (obj == null) return void 0; - return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid]; - }, - - // Get the model at the given index. - at: function(index) { - return this.models[index]; - }, - - // Return models with matching attributes. Useful for simple cases of - // `filter`. - where: function(attrs, first) { - if (_.isEmpty(attrs)) return first ? void 0 : []; - return this[first ? 'find' : 'filter'](function(model) { - for (var key in attrs) { - if (attrs[key] !== model.get(key)) return false; - } - return true; - }); - }, - - // Return the first model with matching attributes. Useful for simple cases - // of `find`. - findWhere: function(attrs) { - return this.where(attrs, true); - }, - - // Force the collection to re-sort itself. You don't need to call this under - // normal circumstances, as the set will maintain sort order as each item - // is added. - sort: function(options) { - if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); - options || (options = {}); - - // Run sort based on type of `comparator`. - if (_.isString(this.comparator) || this.comparator.length === 1) { - this.models = this.sortBy(this.comparator, this); - } else { - this.models.sort(_.bind(this.comparator, this)); - } - - if (!options.silent) this.trigger('sort', this, options); - return this; - }, - - // Pluck an attribute from each model in the collection. - pluck: function(attr) { - return _.invoke(this.models, 'get', attr); - }, - - // Fetch the default set of models for this collection, resetting the - // collection when they arrive. If `reset: true` is passed, the response - // data will be passed through the `reset` method instead of `set`. - fetch: function(options) { - options = options ? _.clone(options) : {}; - if (options.parse === void 0) options.parse = true; - var success = options.success; - var collection = this; - options.success = function(resp) { - var method = options.reset ? 'reset' : 'set'; - collection[method](resp, options); - if (success) success(collection, resp, options); - collection.trigger('sync', collection, resp, options); - }; - wrapError(this, options); - return this.sync('read', this, options); - }, - - // Create a new instance of a model in this collection. Add the model to the - // collection immediately, unless `wait: true` is passed, in which case we - // wait for the server to agree. - create: function(model, options) { - options = options ? _.clone(options) : {}; - if (!(model = this._prepareModel(model, options))) return false; - if (!options.wait) this.add(model, options); - var collection = this; - var success = options.success; - options.success = function(model, resp) { - if (options.wait) collection.add(model, options); - if (success) success(model, resp, options); - }; - model.save(null, options); - return model; - }, - - // **parse** converts a response into a list of models to be added to the - // collection. The default implementation is just to pass it through. - parse: function(resp, options) { - return resp; - }, - - // Create a new collection with an identical list of models as this one. - clone: function() { - return new this.constructor(this.models); - }, - - // Private method to reset all internal state. Called when the collection - // is first initialized or reset. - _reset: function() { - this.length = 0; - this.models = []; - this._byId = {}; - }, - - // Prepare a hash of attributes (or other model) to be added to this - // collection. - _prepareModel: function(attrs, options) { - if (attrs instanceof Model) return attrs; - options = options ? _.clone(options) : {}; - options.collection = this; - var model = new this.model(attrs, options); - if (!model.validationError) return model; - this.trigger('invalid', this, model.validationError, options); - return false; - }, - - // Internal method to create a model's ties to a collection. - _addReference: function(model, options) { - this._byId[model.cid] = model; - if (model.id != null) this._byId[model.id] = model; - if (!model.collection) model.collection = this; - model.on('all', this._onModelEvent, this); - }, - - // Internal method to sever a model's ties to a collection. - _removeReference: function(model, options) { - if (this === model.collection) delete model.collection; - model.off('all', this._onModelEvent, this); - }, - - // Internal method called every time a model in the set fires an event. - // Sets need to update their indexes when models change ids. All other - // events simply proxy through. "add" and "remove" events that originate - // in other collections are ignored. - _onModelEvent: function(event, model, collection, options) { - if ((event === 'add' || event === 'remove') && collection !== this) return; - if (event === 'destroy') this.remove(model, options); - if (model && event === 'change:' + model.idAttribute) { - delete this._byId[model.previous(model.idAttribute)]; - if (model.id != null) this._byId[model.id] = model; - } - this.trigger.apply(this, arguments); - } - - }); - - // Underscore methods that we want to implement on the Collection. - // 90% of the core usefulness of Backbone Collections is actually implemented - // right here: - 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', 'difference', 'indexOf', 'shuffle', - 'lastIndexOf', 'isEmpty', 'chain', 'sample']; - - // Mix in each Underscore method as a proxy to `Collection#models`. - _.each(methods, function(method) { - Collection.prototype[method] = function() { - var args = slice.call(arguments); - args.unshift(this.models); - return _[method].apply(_, args); - }; - }); - - // Underscore methods that take a property name as an argument. - var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy']; - - // Use attributes instead of properties. - _.each(attributeMethods, function(method) { - Collection.prototype[method] = function(value, context) { - var iterator = _.isFunction(value) ? value : function(model) { - return model.get(value); - }; - return _[method](this.models, iterator, context); - }; - }); - - // Backbone.View - // ------------- - - // Backbone Views are almost more convention than they are actual code. A View - // is simply a JavaScript object that represents a logical chunk of UI in the - // DOM. This might be a single item, an entire list, a sidebar or panel, or - // even the surrounding frame which wraps your whole app. Defining a chunk of - // UI as a **View** allows you to define your DOM events declaratively, without - // having to worry about render order ... and makes it easy for the view to - // react to specific changes in the state of your models. - - // Creating a Backbone.View creates its initial element outside of the DOM, - // if an existing element is not provided... - var View = Backbone.View = function(options) { - this.cid = _.uniqueId('view'); - options || (options = {}); - _.extend(this, _.pick(options, viewOptions)); - this._ensureElement(); - this.initialize.apply(this, arguments); - this.delegateEvents(); - }; - - // Cached regex to split keys for `delegate`. - var delegateEventSplitter = /^(\S+)\s*(.*)$/; - - // List of view options to be merged as properties. - var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; - - // Set up all inheritable **Backbone.View** properties and methods. - _.extend(View.prototype, Events, { - - // The default `tagName` of a View's element is `"div"`. - tagName: 'div', - - // jQuery delegate for element lookup, scoped to DOM elements within the - // current view. This should be preferred to global lookups where possible. - $: function(selector) { - return this.$el.find(selector); - }, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // **render** is the core function that your view should override, in order - // to populate its element (`this.el`), with the appropriate HTML. The - // convention is for **render** to always return `this`. - render: function() { - return this; - }, - - // Remove this view by taking the element out of the DOM, and removing any - // applicable Backbone.Events listeners. - remove: function() { - this.$el.remove(); - this.stopListening(); - return this; - }, - - // Change the view's element (`this.el` property), including event - // re-delegation. - setElement: function(element, delegate) { - if (this.$el) this.undelegateEvents(); - this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); - this.el = this.$el[0]; - if (delegate !== false) this.delegateEvents(); - return this; - }, - - // Set callbacks, where `this.events` is a hash of - // - // *{"event selector": "callback"}* - // - // { - // 'mousedown .title': 'edit', - // 'click .button': 'save', - // 'click .open': function(e) { ... } - // } - // - // pairs. Callbacks will be bound to the view, with `this` set properly. - // Uses event delegation for efficiency. - // Omitting the selector binds the event to `this.el`. - // This only works for delegate-able events: not `focus`, `blur`, and - // not `change`, `submit`, and `reset` in Internet Explorer. - delegateEvents: function(events) { - if (!(events || (events = _.result(this, 'events')))) return this; - this.undelegateEvents(); - for (var key in events) { - var method = events[key]; - if (!_.isFunction(method)) method = this[events[key]]; - if (!method) continue; - - var match = key.match(delegateEventSplitter); - var eventName = match[1], selector = match[2]; - method = _.bind(method, this); - eventName += '.delegateEvents' + this.cid; - if (selector === '') { - this.$el.on(eventName, method); - } else { - this.$el.on(eventName, selector, method); - } - } - return this; - }, - - // Clears all callbacks previously bound to the view with `delegateEvents`. - // You usually don't need to use this, but may wish to if you have multiple - // Backbone views attached to the same DOM element. - undelegateEvents: function() { - this.$el.off('.delegateEvents' + this.cid); - return this; - }, - - // Ensure that the View has a DOM element to render into. - // If `this.el` is a string, pass it through `$()`, take the first - // matching element, and re-assign it to `el`. Otherwise, create - // an element from the `id`, `className` and `tagName` properties. - _ensureElement: function() { - if (!this.el) { - var attrs = _.extend({}, _.result(this, 'attributes')); - if (this.id) attrs.id = _.result(this, 'id'); - if (this.className) attrs['class'] = _.result(this, 'className'); - var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); - this.setElement($el, false); - } else { - this.setElement(_.result(this, 'el'), false); - } - } - - }); - - // Backbone.sync - // ------------- - - // Override this function to change the manner in which Backbone persists - // models to the server. You will be passed the type of request, and the - // model in question. By default, makes a RESTful Ajax request - // to the model's `url()`. Some possible customizations could be: - // - // * Use `setTimeout` to batch rapid-fire updates into a single request. - // * Send up the models as XML instead of JSON. - // * Persist models via WebSockets instead of Ajax. - // - // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests - // as `POST`, with a `_method` parameter containing the true HTTP method, - // as well as all requests with the body as `application/x-www-form-urlencoded` - // instead of `application/json` with the model in a param named `model`. - // Useful when interfacing with server-side languages like **PHP** that make - // it difficult to read the body of `PUT` requests. - Backbone.sync = function(method, model, options) { - var type = methodMap[method]; - - // Default options, unless specified. - _.defaults(options || (options = {}), { - emulateHTTP: Backbone.emulateHTTP, - emulateJSON: Backbone.emulateJSON - }); - - // Default JSON-request options. - var params = {type: type, dataType: 'json'}; - - // Ensure that we have a URL. - if (!options.url) { - params.url = _.result(model, 'url') || urlError(); - } - - // Ensure that we have the appropriate request data. - if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { - params.contentType = 'application/json'; - params.data = JSON.stringify(options.attrs || model.toJSON(options)); - } - - // For older servers, emulate JSON by encoding the request into an HTML-form. - if (options.emulateJSON) { - params.contentType = 'application/x-www-form-urlencoded'; - params.data = params.data ? {model: params.data} : {}; - } - - // For older servers, emulate HTTP by mimicking the HTTP method with `_method` - // And an `X-HTTP-Method-Override` header. - if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { - params.type = 'POST'; - if (options.emulateJSON) params.data._method = type; - var beforeSend = options.beforeSend; - options.beforeSend = function(xhr) { - xhr.setRequestHeader('X-HTTP-Method-Override', type); - if (beforeSend) return beforeSend.apply(this, arguments); - }; - } - - // Don't process data on a non-GET request. - if (params.type !== 'GET' && !options.emulateJSON) { - params.processData = false; - } - - // If we're sending a `PATCH` request, and we're in an old Internet Explorer - // that still has ActiveX enabled by default, override jQuery to use that - // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. - if (params.type === 'PATCH' && noXhrPatch) { - params.xhr = function() { - return new ActiveXObject("Microsoft.XMLHTTP"); - }; - } - - // Make the request, allowing the user to override any Ajax options. - var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); - model.trigger('request', model, xhr, options); - return xhr; - }; - - var noXhrPatch = - typeof window !== 'undefined' && !!window.ActiveXObject && - !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); - - // Map from CRUD to HTTP for our default `Backbone.sync` implementation. - var methodMap = { - 'create': 'POST', - 'update': 'PUT', - 'patch': 'PATCH', - 'delete': 'DELETE', - 'read': 'GET' - }; - - // Set the default implementation of `Backbone.ajax` to proxy through to `$`. - // Override this if you'd like to use a different library. - Backbone.ajax = function() { - return Backbone.$.ajax.apply(Backbone.$, arguments); - }; - - // Backbone.Router - // --------------- - - // Routers map faux-URLs to actions, and fire events when routes are - // matched. Creating a new one sets its `routes` hash, if not set statically. - var Router = Backbone.Router = function(options) { - options || (options = {}); - if (options.routes) this.routes = options.routes; - this._bindRoutes(); - this.initialize.apply(this, arguments); - }; - - // Cached regular expressions for matching named param parts and splatted - // parts of route strings. - var optionalParam = /\((.*?)\)/g; - var namedParam = /(\(\?)?:\w+/g; - var splatParam = /\*\w+/g; - var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; - - // Set up all inheritable **Backbone.Router** properties and methods. - _.extend(Router.prototype, Events, { - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // Manually bind a single named route to a callback. For example: - // - // this.route('search/:query/p:num', 'search', function(query, num) { - // ... - // }); - // - route: function(route, name, callback) { - if (!_.isRegExp(route)) route = this._routeToRegExp(route); - if (_.isFunction(name)) { - callback = name; - name = ''; - } - if (!callback) callback = this[name]; - var router = this; - Backbone.history.route(route, function(fragment) { - var args = router._extractParameters(route, fragment); - router.execute(callback, args); - router.trigger.apply(router, ['route:' + name].concat(args)); - router.trigger('route', name, args); - Backbone.history.trigger('route', router, name, args); - }); - return this; - }, - - // Execute a route handler with the provided parameters. This is an - // excellent place to do pre-route setup or post-route cleanup. - execute: function(callback, args) { - if (callback) callback.apply(this, args); - }, - - // Simple proxy to `Backbone.history` to save a fragment into the history. - navigate: function(fragment, options) { - Backbone.history.navigate(fragment, options); - return this; - }, - - // Bind all defined routes to `Backbone.history`. We have to reverse the - // order of the routes here to support behavior where the most general - // routes can be defined at the bottom of the route map. - _bindRoutes: function() { - if (!this.routes) return; - this.routes = _.result(this, 'routes'); - var route, routes = _.keys(this.routes); - while ((route = routes.pop()) != null) { - this.route(route, this.routes[route]); - } - }, - - // Convert a route string into a regular expression, suitable for matching - // against the current location hash. - _routeToRegExp: function(route) { - route = route.replace(escapeRegExp, '\\$&') - .replace(optionalParam, '(?:$1)?') - .replace(namedParam, function(match, optional) { - return optional ? match : '([^/?]+)'; - }) - .replace(splatParam, '([^?]*?)'); - return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); - }, - - // Given a route, and a URL fragment that it matches, return the array of - // extracted decoded parameters. Empty or unmatched parameters will be - // treated as `null` to normalize cross-browser behavior. - _extractParameters: function(route, fragment) { - var params = route.exec(fragment).slice(1); - return _.map(params, function(param, i) { - // Don't decode the search params. - if (i === params.length - 1) return param || null; - return param ? decodeURIComponent(param) : null; - }); - } - - }); - - // Backbone.History - // ---------------- - - // Handles cross-browser history management, based on either - // [pushState](http://diveintohtml5.info/history.html) and real URLs, or - // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) - // and URL fragments. If the browser supports neither (old IE, natch), - // falls back to polling. - var History = Backbone.History = function() { - this.handlers = []; - _.bindAll(this, 'checkUrl'); - - // Ensure that `History` can be used outside of the browser. - if (typeof window !== 'undefined') { - this.location = window.location; - this.history = window.history; - } - }; - - // Cached regex for stripping a leading hash/slash and trailing space. - var routeStripper = /^[#\/]|\s+$/g; - - // Cached regex for stripping leading and trailing slashes. - var rootStripper = /^\/+|\/+$/g; - - // Cached regex for detecting MSIE. - var isExplorer = /msie [\w.]+/; - - // Cached regex for removing a trailing slash. - var trailingSlash = /\/$/; - - // Cached regex for stripping urls of hash. - var pathStripper = /#.*$/; - - // Has the history handling already been started? - History.started = false; - - // Set up all inheritable **Backbone.History** properties and methods. - _.extend(History.prototype, Events, { - - // The default interval to poll for hash changes, if necessary, is - // twenty times a second. - interval: 50, - - // Are we at the app root? - atRoot: function() { - return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; - }, - - // Gets the true hash value. Cannot use location.hash directly due to bug - // in Firefox where location.hash will always be decoded. - getHash: function(window) { - var match = (window || this).location.href.match(/#(.*)$/); - return match ? match[1] : ''; - }, - - // Get the cross-browser normalized URL fragment, either from the URL, - // the hash, or the override. - getFragment: function(fragment, forcePushState) { - if (fragment == null) { - if (this._hasPushState || !this._wantsHashChange || forcePushState) { - fragment = decodeURI(this.location.pathname + this.location.search); - var root = this.root.replace(trailingSlash, ''); - if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); - } else { - fragment = this.getHash(); - } - } - return fragment.replace(routeStripper, ''); - }, - - // Start the hash change handling, returning `true` if the current URL matches - // an existing route, and `false` otherwise. - start: function(options) { - if (History.started) throw new Error("Backbone.history has already been started"); - History.started = true; - - // Figure out the initial configuration. Do we need an iframe? - // Is pushState desired ... is it available? - this.options = _.extend({root: '/'}, this.options, options); - this.root = this.options.root; - this._wantsHashChange = this.options.hashChange !== false; - this._wantsPushState = !!this.options.pushState; - this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); - var fragment = this.getFragment(); - var docMode = document.documentMode; - var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); - - // Normalize root to always include a leading and trailing slash. - this.root = ('/' + this.root + '/').replace(rootStripper, '/'); - - if (oldIE && this._wantsHashChange) { - var frame = Backbone.$('