diff --git a/dist/grid.comb.js b/dist/grid.comb.js index 8af33e9e4..f7d1ae972 100644 --- a/dist/grid.comb.js +++ b/dist/grid.comb.js @@ -1,6 +1,6 @@ /*! - * bundle created at "Thu Dec 15 2016 17:38:35 GMT+0900 (KST)" - * version: 1.6.2 + * bundle created at "Wed Jan 04 2017 12:39:59 GMT+0900 (KST)" + * version: 1.7.0 */ /******/ (function(modules) { // webpackBootstrap /******/ // The module cache @@ -58,20 +58,20 @@ var View = __webpack_require__(2); var ModelManager = __webpack_require__(6); - var ViewFactory = __webpack_require__(27); - var DomState = __webpack_require__(48); - var PublicEventEmitter = __webpack_require__(49); - var PainterManager = __webpack_require__(50); - var PainterController = __webpack_require__(60); - var NetAddOn = __webpack_require__(61); - var ComponentHolder = __webpack_require__(64); + var ViewFactory = __webpack_require__(29); + var DomState = __webpack_require__(50); + var PublicEventEmitter = __webpack_require__(51); + var PainterManager = __webpack_require__(52); + var PainterController = __webpack_require__(62); + var NetAddOn = __webpack_require__(63); + var ComponentHolder = __webpack_require__(66); var util = __webpack_require__(9); - var themeManager = __webpack_require__(65); + var themeManager = __webpack_require__(67); var themeNameConst = __webpack_require__(10).themeName; var instanceMap = {}; - __webpack_require__(71); + __webpack_require__(73); /** * Toast UI Namespace @@ -313,7 +313,6 @@ /** * Disables all rows. - * @api */ disable: function() { this.modelManager.dataModel.setDisabled(true); @@ -321,7 +320,6 @@ /** * Enables all rows. - * @api */ enable: function() { this.modelManager.dataModel.setDisabled(false); @@ -329,7 +327,6 @@ /** * Disables the row identified by the rowkey. - * @api * @param {(number|string)} rowKey - The unique key of the target row */ disableRow: function(rowKey) { @@ -338,7 +335,6 @@ /** * Enables the row identified by the rowKey. - * @api * @param {(number|string)} rowKey - The unique key of the target row */ enableRow: function(rowKey) { @@ -347,7 +343,6 @@ /** * Returns the value of the cell identified by the rowKey and columnName. - * @api * @param {(number|string)} rowKey - The unique key of the target row. * @param {string} columnName - The name of the column * @param {boolean} [isOriginal] - It set to true, the original value will be return. @@ -359,7 +354,6 @@ /** * Returns a list of all values in the specified column. - * @api * @param {string} columnName - The name of the column * @param {boolean} [isJsonString=false] - It set to true, return value will be converted to JSON string. * @returns {(Array|string)} - A List of all values in the specified column. (or JSON string of the list) @@ -370,7 +364,6 @@ /** * Returns the object that contains all values in the specified row. - * @api * @param {(number|string)} rowKey - The unique key of the target row * @param {boolean} [isJsonString=false] - If set to true, return value will be converted to JSON string. * @returns {(Object|string)} - The object that contains all values in the row. (or JSON string of the object) @@ -381,7 +374,6 @@ /** * Returns the object that contains all values in the row at specified index. - * @api * @param {number} index - The index of the row * @param {Boolean} [isJsonString=false] - If set to true, return value will be converted to JSON string. * @returns {Object|string} - The object that contains all values in the row. (or JSON string of the object) @@ -392,7 +384,6 @@ /** * Returns the total number of the rows. - * @api * @returns {number} - The total number of the rows */ getRowCount: function() { @@ -401,7 +392,6 @@ /** * Returns the rowKey of the currently selected row. - * @api * @returns {(number|string)} - The rowKey of the row */ getSelectedRowKey: function() { @@ -410,7 +400,6 @@ /** * Returns the jquery object of the cell identified by the rowKey and columnName. - * @api * @param {(number|string)} rowKey - The unique key of the row * @param {string} columnName - The name of the column * @returns {jQuery} - The jquery object of the cell element @@ -421,7 +410,6 @@ /** * Sets the value of the cell identified by the specified rowKey and columnName. - * @api * @param {(number|string)} rowKey - The unique key of the row * @param {string} columnName - The name of the column * @param {(number|string)} columnValue - The value to be set @@ -432,7 +420,6 @@ /** * Sets the all values in the specified column. - * @api * @param {string} columnName - The name of the column * @param {(number|string)} columnValue - The value to be set * @param {Boolean} [isCheckCellState=true] - If set to true, only editable and not disabled cells will be affected. @@ -443,7 +430,6 @@ /** * Replace all rows with the specified list. This will not change the original data. - * @api * @param {Array} rowList - A list of new rows */ replaceRowList: function(rowList) { @@ -452,7 +438,6 @@ /** * Replace all rows with the specified list. This will change the original data. - * @api * @param {Array} rowList - A list of new rows * @param {function} callback - The function that will be called when done. */ @@ -460,9 +445,16 @@ this.modelManager.dataModel.setRowList(rowList, true, callback); }, + /** + * Sets the height of body-area. + * @param {number} value - The number of pixel + */ + setBodyHeight: function(value) { + this.modelManager.dimensionModel.set('bodyHeight', value); + }, + /** * Sets focus on the cell identified by the specified rowKey and columnName. - * @api * @param {(number|string)} rowKey - The unique key of the row * @param {string} columnName - The name of the column * @param {boolean} [isScrollable=false] - If set to true, the view will scroll to the cell element. @@ -474,7 +466,6 @@ /** * Sets focus on the cell at the specified index of row and column. - * @api * @param {(number|string)} rowIndex - The index of the row * @param {string} columnIndex - The index of the column * @param {boolean} [isScrollable=false] - If set to true, the view will scroll to the cell element. @@ -485,7 +476,6 @@ /** * Sets focus on the cell at the specified index of row and column and starts to edit. - * @api * @param {(number|string)} rowKey - The unique key of the row * @param {string} columnName - The name of the column * @param {boolean} [isScrollable=false] - If set to true, the view will scroll to the cell element. @@ -496,17 +486,16 @@ /** * Sets focus on the cell at the specified index of row and column and starts to edit. - * @api * @param {(number|string)} rowIndex - The index of the row * @param {string} columnIndex - The index of the column - * @param {boolean} [isScrollable=false] - If set to true, the view will scroll to the cell element. */ + * @param {boolean} [isScrollable=false] - If set to true, the view will scroll to the cell element. + */ focusInAt: function(rowIndex, columnIndex, isScrollable) { this.modelManager.focusModel.focusInAt(rowIndex, columnIndex, isScrollable); }, /** * Makes view ready to get keyboard input. - * @api */ readyForKeyControl: function() { this.modelManager.focusModel.focusClipboard(); @@ -514,7 +503,6 @@ /** * Removes focus from the focused cell. - * @api */ blur: function() { this.modelManager.focusModel.blur(); @@ -522,7 +510,6 @@ /** * Checks all rows. - * @api */ checkAll: function() { this.modelManager.dataModel.checkAll(); @@ -530,7 +517,6 @@ /** * Checks the row identified by the specified rowKey. - * @api * @param {(number|string)} rowKey - The unique key of the row */ check: function(rowKey) { @@ -539,7 +525,6 @@ /** * Unchecks all rows. - * @api */ uncheckAll: function() { this.modelManager.dataModel.uncheckAll(); @@ -547,7 +532,6 @@ /** * Unchecks the row identified by the specified rowKey. - * @api * @param {(number|string)} rowKey - The unique key of the row */ uncheck: function(rowKey) { @@ -556,7 +540,6 @@ /** * Removes all rows. - * @api */ clear: function() { this.modelManager.dataModel.setRowList([]); @@ -564,7 +547,6 @@ /** * Removes the row identified by the specified rowKey. - * @api * @param {(number|string)} rowKey - The unique key of the row * @param {(boolean|object)} [options] - Options. If the type is boolean, this value is equivalent to * options.removeOriginalData. @@ -583,7 +565,6 @@ /** * Removes all checked rows. - * @api * @param {boolean} isConfirm - If set to true, confirm message will be shown before remove. * @returns {boolean} - True if there's at least one row removed. */ @@ -602,7 +583,6 @@ /** * Enables the row identified by the rowKey to be able to check. - * @api * @param {(number|string)} rowKey - The unique key of the row */ enableCheck: function(rowKey) { @@ -611,7 +591,6 @@ /** * Disables the row identified by the spcified rowKey to not be abled to check. - * @api * @param {(number|string)} rowKey - The unique keyof the row. */ disableCheck: function(rowKey) { @@ -620,7 +599,6 @@ /** * Returns a list of the rowKey of checked rows. - * @api * @param {Boolean} [isJsonString=false] - If set to true, return value will be converted to JSON string. * @returns {Array|string} - A list of the rowKey. (or JSON string of the list) */ @@ -633,7 +611,6 @@ /** * Returns a list of the checked rows. - * @api * @param {Boolean} [isJsonString=false] - If set to true, return value will be converted to JSON string. * @returns {Array|string} - A list of the checked rows. (or JSON string of the list) */ @@ -645,7 +622,6 @@ /** * Returns a list of the column model. - * @api * @returns {Array} - A list of the column model. */ getColumnModelList: function() { @@ -655,7 +631,6 @@ /** * Returns the object that contains the lists of changed data compared to the original data. * The object has properties 'createList', 'updateList', 'deleteList'. - * @api * @param {Object} [options] Options * @param {boolean} [options.isOnlyChecked=false] - If set to true, only checked rows will be considered. * @param {boolean} [options.isRaw=false] - If set to true, the data will contains @@ -671,7 +646,6 @@ /** * Insert the new row with specified data to the end of table. - * @api * @param {object} [row] - The data for the new row * @param {object} [options] - Options * @param {number} [options.at] - The index at which new row will be inserted @@ -685,7 +659,6 @@ /** * Insert the new row with specified data to the beginning of table. - * @api * @param {object} [row] - The data for the new row * @param {object} [options] - Options * @param {boolean} [options.focus] - If set to true, move focus to the new row after appending @@ -696,7 +669,6 @@ /** * Returns true if there are at least one row changed. - * @api * @returns {boolean} - True if there are at least one row changed. */ isChanged: function() { @@ -705,7 +677,6 @@ /** * Returns the instance of specified AddOn. - * @api * @param {string} name - The name of the AddOn * @returns {instance} addOn - The instance of the AddOn */ @@ -716,7 +687,6 @@ /** * Restores the data to the original data. * (Original data is set by {@link tui.Grid#setRowList|setRowList} - * @api */ restore: function() { this.modelManager.dataModel.restore(); @@ -724,7 +694,6 @@ /** * Selects the row identified by the rowKey. - * @api * @param {(number|string)} rowKey - The unique key of the row */ select: function(rowKey) { @@ -735,7 +704,6 @@ /** * Unselects selected rows. - * @api */ unselect: function() { this.modelManager.focusModel.unselect(true); @@ -743,7 +711,6 @@ /** * Sets the count of fixed column. - * @api * @param {number} count - The count of column to be fixed */ setColumnFixCount: function(count) { @@ -752,7 +719,6 @@ /** * Sets the list of column model. - * @api * @param {Array} columnModelList - A new list of column model */ setColumnModelList: function(columnModelList) { @@ -761,7 +727,6 @@ /** * Create an specified AddOn and use it on this instance. - * @api * @param {string} name - The name of the AddOn to use. * @param {object} options - The option objects for configuring the AddON. * @returns {tui.Grid} - This instance. @@ -782,7 +747,6 @@ /** * Returns a list of all rows. - * @api * @returns {Array} - A list of all rows */ getRowList: function() { @@ -791,7 +755,6 @@ /** * Sorts all rows by the specified column. - * @api * @param {string} columnName - The name of the column to be used to compare the rows * @param {boolean} [isAscending] - Whether the sort order is ascending. * If not specified, use the negative value of the current order. @@ -802,7 +765,6 @@ /** * Unsorts all rows. (Sorts by rowKey). - * @api */ unSort: function() { this.sort('rowKey'); @@ -810,7 +772,6 @@ /** * Adds the specified css class to cell element identified by the rowKey and className - * @api * @param {(number|string)} rowKey - The unique key of the row * @param {string} columnName - The name of the column * @param {string} className - The css class name to add @@ -821,7 +782,6 @@ /** * Adds the specified css class to all cell elements in the row identified by the rowKey - * @api * @param {(number|string)} rowKey - The unique key of the row * @param {string} className - The css class name to add */ @@ -831,7 +791,6 @@ /** * Removes the specified css class from the cell element indentified by the rowKey and columnName. - * @api * @param {(number|string)} rowKey - The unique key of the row * @param {string} columnName - The name of the column * @param {string} className - The css class name to be removed @@ -842,7 +801,6 @@ /** * Removes the specified css class from all cell elements in the row identified by the rowKey. - * @api * @param {(number|string)} rowKey - The unique key of the row * @param {string} className - The css class name to be removed */ @@ -852,7 +810,6 @@ /** * Returns the rowspan data of the cell identified by the rowKey and columnName. - * @api * @param {(number|string)} rowKey - The unique key of the row * @param {string} columnName - The name of the column * @returns {Object} - Row span data @@ -863,7 +820,6 @@ /** * Returns the index of the row indentified by the rowKey. - * @api * @param {number|string} rowKey - The unique key of the row * @returns {number} - The index of the row */ @@ -881,7 +837,6 @@ /** * Sets the number of rows to be shown in the table area. - * @api * @deprecated * @param {number} count - The number of rows */ @@ -891,7 +846,6 @@ /** * Sets the width and height of the dimension. - * @api * @param {(number|null)} width - The width of the dimension * @param {(number|null)} height - The height of the dimension */ @@ -901,7 +855,6 @@ /** * Refresh the layout view. Use this method when the view was rendered while hidden. - * @api */ refreshLayout: function() { this.modelManager.dimensionModel.refreshLayout(); @@ -909,15 +862,13 @@ /** * Reset the width of each column by using initial setting of column models. - * @api */ resetColumnWidths: function() { - this.modelManager.dimensionModel.resetColumnWidths(); + this.modelManager.coordColumnModel.resetColumnWidths(); }, /** * Show columns - * @api * @param {...string} arguments - Column names to show */ showColumn: function() { @@ -927,7 +878,6 @@ /** * Hide columns - * @api * @param {...string} arguments - Column names to hide */ hideColumn: function() { @@ -939,7 +889,6 @@ * Sets the HTML string of given column footer. * @param {string} columnName - column name * @param {string} contents - HTML string - * @api */ setFooterColumnContent: function(columnName, contents) { this.modelManager.columnModel.setFooterContent(columnName, contents); @@ -949,7 +898,6 @@ * Validates all data and returns the result. * Return value is an array which contains only rows which have invalid cell data. * @returns {Array.} An array of error object - * @api * @example // return value example [ @@ -983,7 +931,6 @@ /** * Destroys the instance. - * @api */ destroy: function() { this.modelManager.destroy(); @@ -2744,14 +2691,19 @@ /* 3 */ /***/ function(module, exports, __webpack_require__) { - var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// Backbone.js 1.1.2 + var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(global) {// Backbone.js 1.3.3 - // (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + // (c) 2010-2016 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) { + (function(factory) { + + // Establish the root object, `window` (`self`) in the browser, or `global` on the server. + // We use `self` instead of `window` for `WebWorker` support. + var root = (typeof self == 'object' && self.self === self && self) || + (typeof global == 'object' && global.global === global && global); // Set up Backbone appropriately for the environment. Start with AMD. if (true) { @@ -2763,15 +2715,16 @@ // 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, _); + var _ = require('underscore'), $; + try { $ = require('jquery'); } catch (e) {} + 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, _, $) { + })(function(root, Backbone, _, $) { // Initial Setup // ------------- @@ -2780,14 +2733,11 @@ // 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; + // Create a local reference to a common array method we'll want to use later. + var slice = Array.prototype.slice; // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '1.1.2'; + Backbone.VERSION = '1.3.3'; // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns // the `$` variable. @@ -2806,17 +2756,65 @@ 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/json` requests ... this 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; + // Proxy Backbone class methods to Underscore functions, wrapping the model's + // `attributes` object or collection's `models` array behind the scenes. + // + // collection.filter(function(model) { return model.get('age') > 10 }); + // collection.each(this.addView); + // + // `Function#apply` can be slow so we use the method's arg count, if we know it. + var addMethod = function(length, method, attribute) { + switch (length) { + case 1: return function() { + return _[method](this[attribute]); + }; + case 2: return function(value) { + return _[method](this[attribute], value); + }; + case 3: return function(iteratee, context) { + return _[method](this[attribute], cb(iteratee, this), context); + }; + case 4: return function(iteratee, defaultVal, context) { + return _[method](this[attribute], cb(iteratee, this), defaultVal, context); + }; + default: return function() { + var args = slice.call(arguments); + args.unshift(this[attribute]); + return _[method].apply(_, args); + }; + } + }; + var addUnderscoreMethods = function(Class, methods, attribute) { + _.each(methods, function(length, method) { + if (_[method]) Class.prototype[method] = addMethod(length, method, attribute); + }); + }; + + // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. + var cb = function(iteratee, instance) { + if (_.isFunction(iteratee)) return iteratee; + if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee); + if (_.isString(iteratee)) return function(model) { return model.get(iteratee); }; + return iteratee; + }; + var modelMatcher = function(attrs) { + var matcher = _.matches(attrs); + return function(model) { + return matcher(model.attributes); + }; + }; + // 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 + // a custom event channel. You may bind a callback to an event with `on` or + // remove with `off`; `trigger`-ing an event fires all callbacks in // succession. // // var object = {}; @@ -2824,123 +2822,234 @@ // 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; - }, + var Events = Backbone.Events = {}; - // 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; + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Iterates over the standard `event, callback` (as well as the fancy multiple + // space-separated events `"change blur", callback` and jQuery-style event + // maps `{event: callback}`). + var eventsApi = function(iteratee, events, name, callback, opts) { + var i = 0, names; + if (name && typeof name === 'object') { + // Handle event maps. + if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; + for (names = _.keys(name); i < names.length ; i++) { + events = eventsApi(iteratee, events, names[i], name[names[i]], opts); } - 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]; - } + } else if (name && eventSplitter.test(name)) { + // Handle space-separated event names by delegating them individually. + for (names = name.split(eventSplitter); i < names.length; i++) { + events = iteratee(events, names[i], callback, opts); } + } else { + // Finally, standard events. + events = iteratee(events, name, callback, opts); + } + return events; + }; - return this; - }, + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + Events.on = function(name, callback, context) { + return internalOn(this, name, callback, context); + }; - // 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; - }, + // Guard the `listening` argument from the public API. + var internalOn = function(obj, name, callback, context, listening) { + obj._events = eventsApi(onApi, obj._events || {}, name, callback, { + context: context, + ctx: obj, + listening: listening + }); - // 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; + if (listening) { + var listeners = obj._listeners || (obj._listeners = {}); + listeners[listening.id] = listening; } + return obj; }; - // Regular expression used to split event strings. - var eventSplitter = /\s+/; + // Inversion-of-control versions of `on`. Tell *this* object to listen to + // an event in another object... keeping track of what it's listening to + // for easier unbinding later. + Events.listenTo = function(obj, name, callback) { + if (!obj) return this; + var id = obj._listenId || (obj._listenId = _.uniqueId('l')); + var listeningTo = this._listeningTo || (this._listeningTo = {}); + var listening = listeningTo[id]; + + // This object is not listening to any other events on `obj` yet. + // Setup the necessary references to track the listening callbacks. + if (!listening) { + var thisId = this._listenId || (this._listenId = _.uniqueId('l')); + listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0}; + } + + // Bind callbacks on obj, and keep track of them on listening. + internalOn(obj, name, callback, this, listening); + return this; + }; + + // The reducing API that adds a callback to the `events` object. + var onApi = function(events, name, callback, options) { + if (callback) { + var handlers = events[name] || (events[name] = []); + var context = options.context, ctx = options.ctx, listening = options.listening; + if (listening) listening.count++; + + handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening}); + } + return events; + }; + + // 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. + Events.off = function(name, callback, context) { + if (!this._events) return this; + this._events = eventsApi(offApi, this._events, name, callback, { + context: context, + listeners: this._listeners + }); + return this; + }; + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + Events.stopListening = function(obj, name, callback) { + var listeningTo = this._listeningTo; + if (!listeningTo) return this; + + var ids = obj ? [obj._listenId] : _.keys(listeningTo); + + for (var i = 0; i < ids.length; i++) { + var listening = listeningTo[ids[i]]; + + // If listening doesn't exist, this object is not currently + // listening to obj. Break out early. + if (!listening) break; - // 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; + listening.obj.off(name, callback, this); + } + + return this; + }; - // Handle event maps. - if (typeof name === 'object') { - for (var key in name) { - obj[action].apply(obj, [key, name[key]].concat(rest)); + // The reducing API that removes a callback from the `events` object. + var offApi = function(events, name, callback, options) { + if (!events) return; + + var i = 0, listening; + var context = options.context, listeners = options.listeners; + + // Delete all events listeners and "drop" events. + if (!name && !callback && !context) { + var ids = _.keys(listeners); + for (; i < ids.length; i++) { + listening = listeners[ids[i]]; + delete listeners[listening.id]; + delete listening.listeningTo[listening.objId]; } - return false; + return; } - // 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)); + var names = name ? [name] : _.keys(events); + for (; i < names.length; i++) { + name = names[i]; + var handlers = events[name]; + + // Bail out if there are no events stored. + if (!handlers) break; + + // Replace events if there are any remaining. Otherwise, clean up. + var remaining = []; + for (var j = 0; j < handlers.length; j++) { + var handler = handlers[j]; + if ( + callback && callback !== handler.callback && + callback !== handler.callback._callback || + context && context !== handler.context + ) { + remaining.push(handler); + } else { + listening = handler.listening; + if (listening && --listening.count === 0) { + delete listeners[listening.id]; + delete listening.listeningTo[listening.objId]; + } + } + } + + // Update tail event if the list has any events. Otherwise, clean up. + if (remaining.length) { + events[name] = remaining; + } else { + delete events[name]; } - return false; } + return events; + }; - return true; + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, its listener will be removed. If multiple events + // are passed in using the space-separated syntax, the handler will fire + // once for each event, not once for a combination of all events. + Events.once = function(name, callback, context) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this)); + if (typeof name === 'string' && context == null) callback = void 0; + return this.on(events, callback, context); + }; + + // Inversion-of-control versions of `once`. + Events.listenToOnce = function(obj, name, callback) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj)); + return this.listenTo(obj, events); + }; + + // Reduces the event callbacks into a map of `{event: onceWrapper}`. + // `offer` unbinds the `onceWrapper` after it has been called. + var onceMap = function(map, name, callback, offer) { + if (callback) { + var once = map[name] = _.once(function() { + offer(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + } + return map; + }; + + // 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). + Events.trigger = function(name) { + if (!this._events) return this; + + var length = Math.max(0, arguments.length - 1); + var args = Array(length); + for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; + + eventsApi(triggerApi, this._events, name, void 0, args); + return this; + }; + + // Handles triggering the appropriate event callbacks. + var triggerApi = function(objEvents, name, callback, args) { + if (objEvents) { + var events = objEvents[name]; + var allEvents = objEvents.all; + if (events && allEvents) allEvents = allEvents.slice(); + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, [name].concat(args)); + } + return objEvents; }; // A difficult-to-believe, but optimized internal dispatch function for @@ -2957,22 +3066,6 @@ } }; - 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; @@ -2994,11 +3087,12 @@ var Model = Backbone.Model = function(attributes, options) { var attrs = attributes || {}; options || (options = {}); - this.cid = _.uniqueId('c'); + this.cid = _.uniqueId(this.cidPrefix); this.attributes = {}; if (options.collection) this.collection = options.collection; if (options.parse) attrs = this.parse(attrs, options) || {}; - attrs = _.defaults({}, attrs, _.result(this, 'defaults')); + var defaults = _.result(this, 'defaults'); + attrs = _.defaults(_.extend({}, defaults, attrs), defaults); this.set(attrs, options); this.changed = {}; this.initialize.apply(this, arguments); @@ -3017,6 +3111,10 @@ // CouchDB users may want to set this to `"_id"`. idAttribute: 'id', + // The prefix is used to create the client id which is used to identify models locally. + // You may want to override this if you're experiencing name clashes with model ids. + cidPrefix: 'c', + // Initialize is an empty function by default. Override it with your own // initialization logic. initialize: function(){}, @@ -3048,14 +3146,19 @@ return this.get(attr) != null; }, + // Special-cased proxy to underscore's `_.matches` method. + matches: function(attrs) { + return !!_.iteratee(attrs, this)(this.attributes); + }, + // 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. + var attrs; if (typeof key === 'object') { attrs = key; options = val; @@ -3069,37 +3172,40 @@ if (!this._validate(attrs, options)) return false; // Extract attributes and options. - unset = options.unset; - silent = options.silent; - changes = []; - changing = this._changing; - this._changing = true; + var unset = options.unset; + var silent = options.silent; + var changes = []; + var 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]; + var current = this.attributes; + var changed = this.changed; + var prev = this._previousAttributes; // For each `set` attribute, update or delete the current value. - for (attr in attrs) { + for (var attr in attrs) { val = attrs[attr]; if (!_.isEqual(current[attr], val)) changes.push(attr); if (!_.isEqual(prev[attr], val)) { - this.changed[attr] = val; + changed[attr] = val; } else { - delete this.changed[attr]; + delete changed[attr]; } unset ? delete current[attr] : current[attr] = val; } + // Update the `id`. + if (this.idAttribute in attrs) this.id = this.get(this.idAttribute); + // Trigger all relevant attribute changes. if (!silent) { if (changes.length) this._pending = options; - for (var i = 0, l = changes.length; i < l; i++) { + for (var i = 0; i < changes.length; i++) { this.trigger('change:' + changes[i], this, current[changes[i]], options); } } @@ -3147,13 +3253,14 @@ // 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; + var changed = {}; for (var attr in diff) { - if (_.isEqual(old[attr], (val = diff[attr]))) continue; - (changed || (changed = {}))[attr] = val; + var val = diff[attr]; + if (_.isEqual(old[attr], val)) continue; + changed[attr] = val; } - return changed; + return _.size(changed) ? changed : false; }, // Get the previous value of an attribute, recorded at the time the last @@ -3169,17 +3276,16 @@ 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 the model from the server, merging the response with the model's + // local attributes. Any changed attributes will trigger a "change" event. fetch: function(options) { - options = options ? _.clone(options) : {}; - if (options.parse === void 0) options.parse = true; + options = _.extend({parse: true}, options); 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); + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (!model.set(serverAttrs, options)) return false; + if (success) success.call(options.context, model, resp, options); model.trigger('sync', model, resp, options); }; wrapError(this, options); @@ -3190,9 +3296,8 @@ // 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. + var attrs; if (key == null || typeof key === 'object') { attrs = key; options = val; @@ -3200,46 +3305,43 @@ (attrs = {})[key] = val; } - options = _.extend({validate: true}, options); + options = _.extend({validate: true, parse: true}, options); + var wait = options.wait; // 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 (attrs && !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); + } else if (!this._validate(attrs, options)) { + return false; } // 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; + var attributes = this.attributes; 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); + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (wait) serverAttrs = _.extend({}, attrs, serverAttrs); + if (serverAttrs && !model.set(serverAttrs, options)) return false; + if (success) success.call(options.context, 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); + // Set temporary attributes if `{wait: true}` to properly find new ids. + if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); + + var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch' && !options.attrs) options.attrs = attrs; + var xhr = this.sync(method, this, options); // Restore attributes. - if (attrs && options.wait) this.attributes = attributes; + this.attributes = attributes; return xhr; }, @@ -3251,25 +3353,27 @@ options = options ? _.clone(options) : {}; var model = this; var success = options.success; + var wait = options.wait; var destroy = function() { + model.stopListening(); model.trigger('destroy', model, model.collection, options); }; options.success = function(resp) { - if (options.wait || model.isNew()) destroy(); - if (success) success(model, resp, options); + if (wait) destroy(); + if (success) success.call(options.context, model, resp, options); if (!model.isNew()) model.trigger('sync', model, resp, options); }; + var xhr = false; if (this.isNew()) { - options.success(); - return false; + _.defer(options.success); + } else { + wrapError(this, options); + xhr = this.sync('delete', this, options); } - wrapError(this, options); - - var xhr = this.sync('delete', this, options); - if (!options.wait) destroy(); + if (!wait) destroy(); return xhr; }, @@ -3282,7 +3386,8 @@ _.result(this.collection, 'url') || urlError(); if (this.isNew()) return base; - return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); + var id = this.get(this.idAttribute); + return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); }, // **parse** converts a response into the hash of attributes to be `set` on @@ -3303,7 +3408,7 @@ // Check if the model is currently in a valid state. isValid: function(options) { - return this._validate({}, _.extend(options || {}, { validate: true })); + return this._validate({}, _.extend({}, options, {validate: true})); }, // Run validation against the next complete set of model attributes, @@ -3319,23 +3424,19 @@ }); - // Underscore methods that we want to implement on the Model. - var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; + // Underscore methods that we want to implement on the Model, mapped to the + // number of arguments they take. + var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, + omit: 0, chain: 1, isEmpty: 1}; // 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); - }; - }); + addUnderscoreMethods(Model, modelMethods, 'attributes'); // 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 + // more analogous 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 @@ -3357,6 +3458,17 @@ var setOptions = {add: true, remove: true, merge: true}; var addOptions = {add: true, remove: false}; + // Splices `insert` into `array` at index `at`. + var splice = function(array, insert, at) { + at = Math.min(Math.max(at, 0), array.length); + var tail = Array(array.length - at); + var length = insert.length; + var i; + for (i = 0; i < tail.length; i++) tail[i] = array[i + at]; + for (i = 0; i < length; i++) array[i + at] = insert[i]; + for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; + }; + // Define the Collection's inheritable methods. _.extend(Collection.prototype, Events, { @@ -3371,7 +3483,7 @@ // 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); }); + return this.map(function(model) { return model.toJSON(options); }); }, // Proxy `Backbone.sync` by default. @@ -3379,32 +3491,24 @@ return Backbone.sync.apply(this, arguments); }, - // Add a model, or list of models to the set. + // Add a model, or list of models to the set. `models` may be Backbone + // Models or raw JavaScript objects to be converted to Models, or any + // combination of the two. 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) { + options = _.extend({}, 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); + models = singular ? [models] : models.slice(); + var removed = this._removeModels(models, options); + if (!options.silent && removed.length) { + options.changes = {added: [], merged: [], removed: removed}; + this.trigger('update', this, options); } - return singular ? models[0] : models; + return singular ? removed[0] : removed; }, // Update a collection by `set`-ing a new list of models, adding new ones, @@ -3412,89 +3516,114 @@ // 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); + if (models == null) return; + + options = _.extend({}, setOptions, options); + if (options.parse && !this._isModel(models)) { + models = this.parse(models, options) || []; + } + var singular = !_.isArray(models); - models = singular ? (models ? [models] : []) : _.clone(models); - var i, l, id, model, attrs, existing, sort; + models = singular ? [models] : models.slice(); + var at = options.at; - var targetModel = this.model; - var sortable = this.comparator && (at == null) && options.sort !== false; + if (at != null) at = +at; + if (at > this.length) at = this.length; + if (at < 0) at += this.length + 1; + + var set = []; + var toAdd = []; + var toMerge = []; + var toRemove = []; + var modelMap = {}; + + var add = options.add; + var merge = options.merge; + var remove = options.remove; + + var sort = false; + 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']; - } + var model, i; + for (i = 0; i < models.length; i++) { + model = models[i]; // 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; + var existing = this.get(model); + if (existing) { + if (merge && model !== existing) { + var attrs = this._isModel(model) ? model.attributes : model; if (options.parse) attrs = existing.parse(attrs, options); existing.set(attrs, options); - if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; + toMerge.push(existing); + if (sortable && !sort) sort = existing.hasChanged(sortAttr); + } + if (!modelMap[existing.cid]) { + modelMap[existing.cid] = true; + set.push(existing); } 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); + model = models[i] = this._prepareModel(model, options); + if (model) { + toAdd.push(model); + this._addReference(model, options); + modelMap[model.cid] = true; + set.push(model); + } } - - // 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. + // Remove stale models. if (remove) { - for (i = 0, l = this.length; i < l; ++i) { - if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); + for (i = 0; i < this.length; i++) { + model = this.models[i]; + if (!modelMap[model.cid]) toRemove.push(model); } - if (toRemove.length) this.remove(toRemove, options); + if (toRemove.length) this._removeModels(toRemove, options); } // See if sorting is needed, update `length` and splice in new models. - if (toAdd.length || (order && order.length)) { + var orderChanged = false; + var replace = !sortable && add && remove; + if (set.length && replace) { + orderChanged = this.length !== set.length || _.some(this.models, function(m, index) { + return m !== set[index]; + }); + this.models.length = 0; + splice(this.models, set, 0); + this.length = this.models.length; + } else if (toAdd.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]); - } - } + splice(this.models, toAdd, at == null ? this.length : at); + this.length = this.models.length; } // Silently sort the collection if appropriate. if (sort) this.sort({silent: true}); - // Unless silenced, it's time to fire all appropriate add/sort events. + // Unless silenced, it's time to fire all appropriate add/sort/update events. if (!options.silent) { - for (i = 0, l = toAdd.length; i < l; i++) { - (model = toAdd[i]).trigger('add', model, this, options); + for (i = 0; i < toAdd.length; i++) { + if (at != null) options.index = at + i; + model = toAdd[i]; + model.trigger('add', model, this, options); + } + if (sort || orderChanged) this.trigger('sort', this, options); + if (toAdd.length || toRemove.length || toMerge.length) { + options.changes = { + added: toAdd, + removed: toRemove, + merged: toMerge + }; + this.trigger('update', this, options); } - if (sort || (order && order.length)) this.trigger('sort', this, options); } // Return the added (or merged) model (or models). @@ -3506,8 +3635,8 @@ // 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++) { + options = options ? _.clone(options) : {}; + for (var i = 0; i < this.models.length; i++) { this._removeReference(this.models[i], options); } options.previousModels = this.models; @@ -3525,8 +3654,7 @@ // 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; + return this.remove(model, options); }, // Add a model to the beginning of the collection. @@ -3537,8 +3665,7 @@ // Remove a model from the beginning of the collection. shift: function(options) { var model = this.at(0); - this.remove(model, options); - return model; + return this.remove(model, options); }, // Slice out a sub-array of models from the collection. @@ -3546,27 +3673,30 @@ return slice.apply(this.models, arguments); }, - // Get a model from the set by id. + // Get a model from the set by id, cid, model object with id or cid + // properties, or an attributes object that is transformed through modelId. get: function(obj) { if (obj == null) return void 0; - return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid]; + return this._byId[obj] || + this._byId[this.modelId(obj.attributes || obj)] || + obj.cid && this._byId[obj.cid]; + }, + + // Returns `true` if the model is in the collection. + has: function(obj) { + return this.get(obj) != null; }, // Get the model at the given index. at: function(index) { + if (index < 0) index += this.length; 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 this[first ? 'find' : 'filter'](attrs); }, // Return the first model with matching attributes. Useful for simple cases @@ -3579,37 +3709,39 @@ // 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'); + var comparator = this.comparator; + if (!comparator) throw new Error('Cannot sort a set without a comparator'); options || (options = {}); + var length = comparator.length; + if (_.isFunction(comparator)) comparator = _.bind(comparator, this); + // Run sort based on type of `comparator`. - if (_.isString(this.comparator) || this.comparator.length === 1) { - this.models = this.sortBy(this.comparator, this); + if (length === 1 || _.isString(comparator)) { + this.models = this.sortBy(comparator); } else { - this.models.sort(_.bind(this.comparator, this)); + this.models.sort(comparator); } - 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); + return this.map(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; + options = _.extend({parse: true}, options); 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); + if (success) success.call(options.context, collection, resp, options); collection.trigger('sync', collection, resp, options); }; wrapError(this, options); @@ -3621,13 +3753,15 @@ // 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 wait = options.wait; + model = this._prepareModel(model, options); + if (!model) return false; + if (!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); + options.success = function(m, resp, callbackOpts) { + if (wait) collection.add(m, callbackOpts); + if (success) success.call(callbackOpts.context, m, resp, callbackOpts); }; model.save(null, options); return model; @@ -3641,7 +3775,15 @@ // Create a new collection with an identical list of models as this one. clone: function() { - return new this.constructor(this.models); + return new this.constructor(this.models, { + model: this.model, + comparator: this.comparator + }); + }, + + // Define how to uniquely identify models in the collection. + modelId: function(attrs) { + return attrs[this.model.prototype.idAttribute || 'id']; }, // Private method to reset all internal state. Called when the collection @@ -3655,7 +3797,10 @@ // Prepare a hash of attributes (or other model) to be added to this // collection. _prepareModel: function(attrs, options) { - if (attrs instanceof Model) return attrs; + if (this._isModel(attrs)) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } options = options ? _.clone(options) : {}; options.collection = this; var model = new this.model(attrs, options); @@ -3664,16 +3809,53 @@ return false; }, + // Internal method called by both remove and set. + _removeModels: function(models, options) { + var removed = []; + for (var i = 0; i < models.length; i++) { + var model = this.get(models[i]); + if (!model) continue; + + var index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + + // Remove references before triggering 'remove' event to prevent an + // infinite loop. #3693 + delete this._byId[model.cid]; + var id = this.modelId(model.attributes); + if (id != null) delete this._byId[id]; + + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + + removed.push(model); + this._removeReference(model, options); + } + return removed; + }, + + // Method for checking whether an object should be considered a model for + // the purposes of adding to the collection. + _isModel: function(model) { + return model instanceof Model; + }, + // 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; + var id = this.modelId(model.attributes); + if (id != null) this._byId[id] = model; model.on('all', this._onModelEvent, this); }, // Internal method to sever a model's ties to a collection. _removeReference: function(model, options) { + delete this._byId[model.cid]; + var id = this.modelId(model.attributes); + if (id != null) delete this._byId[id]; if (this === model.collection) delete model.collection; model.off('all', this._onModelEvent, this); }, @@ -3683,11 +3865,17 @@ // 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; + if (model) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'destroy') this.remove(model, options); + if (event === 'change') { + var prevId = this.modelId(model.previousAttributes()); + var id = this.modelId(model.attributes); + if (prevId !== id) { + if (prevId != null) delete this._byId[prevId]; + if (id != null) this._byId[id] = model; + } + } } this.trigger.apply(this, arguments); } @@ -3697,34 +3885,17 @@ // 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']; + var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0, + foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3, + select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3, + contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3, + head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, + without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, + isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3, + sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3}; // 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); - }; - }); + addUnderscoreMethods(Collection, collectionMethods, 'models'); // Backbone.View // ------------- @@ -3741,17 +3912,15 @@ // 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. + // List of view options to be set as properties. var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; // Set up all inheritable **Backbone.View** properties and methods. @@ -3780,21 +3949,37 @@ // Remove this view by taking the element out of the DOM, and removing any // applicable Backbone.Events listeners. remove: function() { - this.$el.remove(); + this._removeElement(); 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(); + // Remove this view's element from the document and all event listeners + // attached to it. Exposed for subclasses using an alternative DOM + // manipulation API. + _removeElement: function() { + this.$el.remove(); + }, + + // Change the view's element (`this.el` property) and re-delegate the + // view's events on the new element. + setElement: function(element) { + this.undelegateEvents(); + this._setElement(element); + this.delegateEvents(); return this; }, + // Creates the `this.el` and `this.$el` references for this view using the + // given `el`. `el` can be a CSS selector or an HTML string, a jQuery + // context or an element. Subclasses can override this to utilize an + // alternative DOM manipulation API and are only required to set the + // `this.el` property. + _setElement: function(el) { + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.el = this.$el[0]; + }, + // Set callbacks, where `this.events` is a hash of // // *{"event selector": "callback"}* @@ -3808,37 +3993,49 @@ // 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; + events || (events = _.result(this, 'events')); + if (!events) return this; this.undelegateEvents(); for (var key in events) { var method = events[key]; - if (!_.isFunction(method)) method = this[events[key]]; + if (!_.isFunction(method)) method = this[method]; 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); - } + this.delegate(match[1], match[2], _.bind(method, this)); } return this; }, - // Clears all callbacks previously bound to the view with `delegateEvents`. + // Add a single event listener to the view's element (or a child element + // using `selector`). This only works for delegate-able events: not `focus`, + // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. + delegate: function(eventName, selector, listener) { + this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; + }, + + // Clears all callbacks previously bound to the view by `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); + if (this.$el) this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // A finer-grained `undelegateEvents` for removing a single delegated event. + // `selector` and `listener` are both optional. + undelegate: function(eventName, selector, listener) { + this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); return this; }, + // Produces a DOM element to be assigned to your view. Exposed for + // subclasses using an alternative DOM manipulation API. + _createElement: function(tagName) { + return document.createElement(tagName); + }, + // 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 @@ -3848,11 +4045,17 @@ 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); + this.setElement(this._createElement(_.result(this, 'tagName'))); + this._setAttributes(attrs); } else { - this.setElement(_.result(this, 'el'), false); + this.setElement(_.result(this, 'el')); } + }, + + // Set attributes from a hash on this view's element. Exposed for + // subclasses using an alternative DOM manipulation API. + _setAttributes: function(attributes) { + this.$el.attr(attributes); } }); @@ -3921,14 +4124,13 @@ 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"); - }; - } + // Pass along `textStatus` and `errorThrown` from jQuery. + var error = options.error; + options.error = function(xhr, textStatus, errorThrown) { + options.textStatus = textStatus; + options.errorThrown = errorThrown; + if (error) error.call(options.context, xhr, textStatus, errorThrown); + }; // Make the request, allowing the user to override any Ajax options. var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); @@ -3936,17 +4138,13 @@ 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', + 'patch': 'PATCH', 'delete': 'DELETE', - 'read': 'GET' + 'read': 'GET' }; // Set the default implementation of `Backbone.ajax` to proxy through to `$`. @@ -3997,17 +4195,18 @@ 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); + if (router.execute(callback, args, name) !== false) { + 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) { + execute: function(callback, args, name) { if (callback) callback.apply(this, args); }, @@ -4065,7 +4264,7 @@ // falls back to polling. var History = Backbone.History = function() { this.handlers = []; - _.bindAll(this, 'checkUrl'); + this.checkUrl = _.bind(this.checkUrl, this); // Ensure that `History` can be used outside of the browser. if (typeof window !== 'undefined') { @@ -4080,12 +4279,6 @@ // 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 = /#.*$/; @@ -4101,7 +4294,29 @@ // Are we at the app root? atRoot: function() { - return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; + var path = this.location.pathname.replace(/[^\/]$/, '$&/'); + return path === this.root && !this.getSearch(); + }, + + // Does the pathname match the root? + matchRoot: function() { + var path = this.decodeFragment(this.location.pathname); + var rootPath = path.slice(0, this.root.length - 1) + '/'; + return rootPath === this.root; + }, + + // Unicode characters in `location.pathname` are percent encoded so they're + // decoded for comparison. `%25` should not be decoded since it may be part + // of an encoded parameter. + decodeFragment: function(fragment) { + return decodeURI(fragment.replace(/%25/g, '%2525')); + }, + + // In IE6, the hash fragment and search params are incorrect if the + // fragment contains `?`. + getSearch: function() { + var match = this.location.href.replace(/#.*/, '').match(/\?.+/); + return match ? match[0] : ''; }, // Gets the true hash value. Cannot use location.hash directly due to bug @@ -4111,14 +4326,19 @@ return match ? match[1] : ''; }, - // Get the cross-browser normalized URL fragment, either from the URL, - // the hash, or the override. - getFragment: function(fragment, forcePushState) { + // Get the pathname and search params, without the root. + getPath: function() { + var path = this.decodeFragment( + this.location.pathname + this.getSearch() + ).slice(this.root.length - 1); + return path.charAt(0) === '/' ? path.slice(1) : path; + }, + + // Get the cross-browser normalized URL fragment from the path or hash. + getFragment: function(fragment) { 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); + if (this._usePushState || !this._wantsHashChange) { + fragment = this.getPath(); } else { fragment = this.getHash(); } @@ -4129,7 +4349,7 @@ // 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"); + 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? @@ -4137,36 +4357,16 @@ this.options = _.extend({root: '/'}, this.options, options); this.root = this.options.root; this._wantsHashChange = this.options.hashChange !== false; + this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7); + this._useHashChange = this._wantsHashChange && this._hasHashChange; 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)); + this._hasPushState = !!(this.history && this.history.pushState); + this._usePushState = this._wantsPushState && this._hasPushState; + this.fragment = this.getFragment(); // Normalize root to always include a leading and trailing slash. this.root = ('/' + this.root + '/').replace(rootStripper, '/'); - if (oldIE && this._wantsHashChange) { - var frame = Backbone.$('