HTRACE-108. Search Page: Use server side pagination
diff --git a/htrace-core/src/web/app/models/span.js b/htrace-core/src/web/app/models/span.js
index 85c7c72..1231101 100644
--- a/htrace-core/src/web/app/models/span.js
+++ b/htrace-core/src/web/app/models/span.js
@@ -55,25 +55,81 @@
App.Spans = Backbone.PageableCollection.extend({
model: App.Span,
- mode: "client",
- state: {
- pageSize: 2
- },
+ mode: "infinite",
url: "/query",
+ state: {
+ pageSize: 10,
+ lastSpanId: null,
+ predicates: []
+ },
+ queryParams: {
+ totalPages: null,
+ totalRecords: null,
+ firstPage: null,
+ lastPage: null,
+ currentPage: null,
+ pageSize: null,
+ sortKey: null,
+ order: null,
+ directions: null,
- query: function(options, predicates) {
- var query = {
- "lim": 100000
- };
+ /**
+ * Query parameter for htraced.
+ */
+ query: function() {
+ var predicates = this.state.predicates.slice(0);
+ var lastSpanId = this.state.lastSpanId;
- if (predicates && predicates.length > 0) {
- query.pred = predicates;
+ /**
+ * Use last pulled span ID to paginate.
+ * The htraced API works such that order is defined by the first predicate.
+ * Adding a predicate to the end of the predicates list won't change the order.
+ * Providing the predicate on spanid will filter all previous spanids.
+ */
+ if (lastSpanId) {
+ predicates.push({
+ "op": "gt",
+ "field": "spanid",
+ "val": lastSpanId
+ });
+ }
+
+ return JSON.stringify({
+ lim: this.state.pageSize,
+ pred: predicates
+ });
+ }
+ },
+
+ initialize: function() {
+ this.on("sync", function(collection, response, options) {
+ if (response.length == 0) {
+ delete this.links[this.state.currentPage];
+ this.getPreviousPage();
+ }
+ }, this);
+ },
+
+ parseLinks: function(resp, xhr) {
+ if (resp.length >= this.state.pageSize) {
+ this.state.lastSpanId = resp[resp.length - 1].s;
+
+ return {
+ "next": "/query?query=" + this.queryParams.query.call(this)
+ };
+ } else {
+ this.state.lastSpanId = null;
+
+ return {};
+ }
+ },
+
+ setPredicates: function(predicates) {
+ if (!$.isArray(predicates)) {
+ console.error("predicates should be an array");
+ return;
}
- options = options ? _.clone(options) : {};
- options.data = options.data ? _.clone(options.data) : {};
- options.data.query = JSON.stringify(query);
-
- return this.fetch(options);
+ this.state.predicates = predicates;
}
});
diff --git a/htrace-core/src/web/app/setup.js b/htrace-core/src/web/app/setup.js
index 9b78ba3..bb41ad8 100644
--- a/htrace-core/src/web/app/setup.js
+++ b/htrace-core/src/web/app/setup.js
@@ -26,7 +26,7 @@
initialize: function() {
this.spansCollection = new App.Spans();
- this.spansCollection.query();
+ this.spansCollection.fetch();
this.spanViews = {};
diff --git a/htrace-core/src/web/app/views/search.js b/htrace-core/src/web/app/views/search.js
index 40b9309..1016de3 100644
--- a/htrace-core/src/web/app/views/search.js
+++ b/htrace-core/src/web/app/views/search.js
@@ -76,7 +76,13 @@
});
}
- this.collection.query(null, predicates);
+ this.collection.switchMode("infinite", {
+ fetch: false,
+ resetState: true
+ });
+ this.collection.fullCollection.reset();
+ this.collection.setPredicates(predicates);
+ this.collection.fetch();
return false;
}
diff --git a/htrace-core/src/web/app/views/span.js b/htrace-core/src/web/app/views/span.js
index 764b797..cbb694d 100644
--- a/htrace-core/src/web/app/views/span.js
+++ b/htrace-core/src/web/app/views/span.js
@@ -76,7 +76,7 @@
});
this.listSpansPaginator = new Backgrid.Extension.Paginator({
- collection: this.collection
+ collection: this.collection,
});
},
diff --git a/htrace-core/src/web/index.html b/htrace-core/src/web/index.html
index 7f0773c..24ccc65 100644
--- a/htrace-core/src/web/index.html
+++ b/htrace-core/src/web/index.html
@@ -161,11 +161,11 @@
<script src="lib/js/jquery-2.1.3.min.js" type="text/javascript"></script>
<script src="lib/bootstrap-3.3.1/js/bootstrap.min.js" type="text/javascript"></script>
- <script src="lib/js/underscore-1.7.0.min.js" type="text/javascript"></script>
- <script src="lib/js/backbone-1.1.2.min.js" type="text/javascript"></script>
- <script src="lib/js/backbone.paginator-2.0.2.min.js" type="text/javascript"></script>
- <script src="lib/js/backgrid-0.3.5.min.js" type="text/javascript"></script>
- <script src="lib/js/backgrid-paginator.js" type="text/javascript"></script>
+ <script src="lib/js/underscore-1.7.0.js" type="text/javascript"></script>
+ <script src="lib/js/backbone-1.1.2.js" type="text/javascript"></script>
+ <script src="lib/js/backbone.paginator-2.0.2.js" type="text/javascript"></script>
+ <script src="lib/js/backgrid-0.3.5.js" type="text/javascript"></script>
+ <script src="lib/js/backgrid-paginator-0.3.5.js" type="text/javascript"></script>
<script src="lib/js/moment-2.9.0.min.js" type="text/javascript"></script>
<script src="lib/pickadate-3.5.2/picker.js" type="text/javascript"></script>
<script src="lib/pickadate-3.5.2/picker.date.js" type="text/javascript"></script>
@@ -173,9 +173,6 @@
<script src="app/app.js" type="text/javascript"></script>
<script src="app/models/span.js" type="text/javascript"></script>
-
- <script src="app/mock.js" type="text/javascript"></script>
-
<script src="app/views/span.js" type="text/javascript"></script>
<script src="app/views/search.js" type="text/javascript"></script>
<script src="app/setup.js" type="text/javascript"></script>
diff --git a/htrace-core/src/web/lib/js/backbone-1.1.2.js b/htrace-core/src/web/lib/js/backbone-1.1.2.js
new file mode 100644
index 0000000..5da4943
--- /dev/null
+++ b/htrace-core/src/web/lib/js/backbone-1.1.2.js
@@ -0,0 +1,1608 @@
+// 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.$('<iframe src="javascript:0" tabindex="-1">');
+ this.iframe = frame.hide().appendTo('body')[0].contentWindow;
+ this.navigate(fragment);
+ }
+
+ // Depending on whether we're using pushState or hashes, and whether
+ // 'onhashchange' is supported, determine how we check the URL state.
+ if (this._hasPushState) {
+ Backbone.$(window).on('popstate', this.checkUrl);
+ } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
+ Backbone.$(window).on('hashchange', this.checkUrl);
+ } else if (this._wantsHashChange) {
+ this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
+ }
+
+ // Determine if we need to change the base url, for a pushState link
+ // opened by a non-pushState browser.
+ this.fragment = fragment;
+ var loc = this.location;
+
+ // Transition from hashChange to pushState or vice versa if both are
+ // requested.
+ if (this._wantsHashChange && this._wantsPushState) {
+
+ // If we've started off with a route from a `pushState`-enabled
+ // browser, but we're currently in a browser that doesn't support it...
+ if (!this._hasPushState && !this.atRoot()) {
+ this.fragment = this.getFragment(null, true);
+ this.location.replace(this.root + '#' + this.fragment);
+ // Return immediately as browser will do redirect to new url
+ return true;
+
+ // Or if we've started out with a hash-based route, but we're currently
+ // in a browser where it could be `pushState`-based instead...
+ } else if (this._hasPushState && this.atRoot() && loc.hash) {
+ this.fragment = this.getHash().replace(routeStripper, '');
+ this.history.replaceState({}, document.title, this.root + this.fragment);
+ }
+
+ }
+
+ if (!this.options.silent) return this.loadUrl();
+ },
+
+ // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
+ // but possibly useful for unit testing Routers.
+ stop: function() {
+ Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
+ if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
+ History.started = false;
+ },
+
+ // Add a route to be tested when the fragment changes. Routes added later
+ // may override previous routes.
+ route: function(route, callback) {
+ this.handlers.unshift({route: route, callback: callback});
+ },
+
+ // Checks the current URL to see if it has changed, and if it has,
+ // calls `loadUrl`, normalizing across the hidden iframe.
+ checkUrl: function(e) {
+ var current = this.getFragment();
+ if (current === this.fragment && this.iframe) {
+ current = this.getFragment(this.getHash(this.iframe));
+ }
+ if (current === this.fragment) return false;
+ if (this.iframe) this.navigate(current);
+ this.loadUrl();
+ },
+
+ // Attempt to load the current URL fragment. If a route succeeds with a
+ // match, returns `true`. If no defined routes matches the fragment,
+ // returns `false`.
+ loadUrl: function(fragment) {
+ fragment = this.fragment = this.getFragment(fragment);
+ return _.any(this.handlers, function(handler) {
+ if (handler.route.test(fragment)) {
+ handler.callback(fragment);
+ return true;
+ }
+ });
+ },
+
+ // Save a fragment into the hash history, or replace the URL state if the
+ // 'replace' option is passed. You are responsible for properly URL-encoding
+ // the fragment in advance.
+ //
+ // The options object can contain `trigger: true` if you wish to have the
+ // route callback be fired (not usually desirable), or `replace: true`, if
+ // you wish to modify the current URL without adding an entry to the history.
+ navigate: function(fragment, options) {
+ if (!History.started) return false;
+ if (!options || options === true) options = {trigger: !!options};
+
+ var url = this.root + (fragment = this.getFragment(fragment || ''));
+
+ // Strip the hash for matching.
+ fragment = fragment.replace(pathStripper, '');
+
+ if (this.fragment === fragment) return;
+ this.fragment = fragment;
+
+ // Don't include a trailing slash on the root.
+ if (fragment === '' && url !== '/') url = url.slice(0, -1);
+
+ // If pushState is available, we use it to set the fragment as a real URL.
+ if (this._hasPushState) {
+ this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
+
+ // If hash changes haven't been explicitly disabled, update the hash
+ // fragment to store history.
+ } else if (this._wantsHashChange) {
+ this._updateHash(this.location, fragment, options.replace);
+ if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
+ // Opening and closing the iframe tricks IE7 and earlier to push a
+ // history entry on hash-tag change. When replace is true, we don't
+ // want this.
+ if(!options.replace) this.iframe.document.open().close();
+ this._updateHash(this.iframe.location, fragment, options.replace);
+ }
+
+ // If you've told us that you explicitly don't want fallback hashchange-
+ // based history, then `navigate` becomes a page refresh.
+ } else {
+ return this.location.assign(url);
+ }
+ if (options.trigger) return this.loadUrl(fragment);
+ },
+
+ // Update the hash location, either replacing the current entry, or adding
+ // a new one to the browser history.
+ _updateHash: function(location, fragment, replace) {
+ if (replace) {
+ var href = location.href.replace(/(javascript:|#).*$/, '');
+ location.replace(href + '#' + fragment);
+ } else {
+ // Some browsers require that `hash` contains a leading #.
+ location.hash = '#' + fragment;
+ }
+ }
+
+ });
+
+ // Create the default Backbone.history.
+ Backbone.history = new History;
+
+ // Helpers
+ // -------
+
+ // Helper function to correctly set up the prototype chain, for subclasses.
+ // Similar to `goog.inherits`, but uses a hash of prototype properties and
+ // class properties to be extended.
+ var extend = function(protoProps, staticProps) {
+ var parent = this;
+ var child;
+
+ // The constructor function for the new subclass is either defined by you
+ // (the "constructor" property in your `extend` definition), or defaulted
+ // by us to simply call the parent's constructor.
+ if (protoProps && _.has(protoProps, 'constructor')) {
+ child = protoProps.constructor;
+ } else {
+ child = function(){ return parent.apply(this, arguments); };
+ }
+
+ // Add static properties to the constructor function, if supplied.
+ _.extend(child, parent, staticProps);
+
+ // Set the prototype chain to inherit from `parent`, without calling
+ // `parent`'s constructor function.
+ var Surrogate = function(){ this.constructor = child; };
+ Surrogate.prototype = parent.prototype;
+ child.prototype = new Surrogate;
+
+ // Add prototype properties (instance properties) to the subclass,
+ // if supplied.
+ if (protoProps) _.extend(child.prototype, protoProps);
+
+ // Set a convenience property in case the parent's prototype is needed
+ // later.
+ child.__super__ = parent.prototype;
+
+ return child;
+ };
+
+ // Set up inheritance for the model, collection, router, view and history.
+ Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
+
+ // Throw an error when a URL is needed, and none is supplied.
+ var urlError = function() {
+ throw new Error('A "url" property or function must be specified');
+ };
+
+ // Wrap an optional error callback with a fallback error event.
+ var wrapError = function(model, options) {
+ var error = options.error;
+ options.error = function(resp) {
+ if (error) error(model, resp, options);
+ model.trigger('error', model, resp, options);
+ };
+ };
+
+ return Backbone;
+
+}));
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/backbone-1.1.2.min.js b/htrace-core/src/web/lib/js/backbone-1.1.2.min.js
deleted file mode 100644
index 6dbb695..0000000
--- a/htrace-core/src/web/lib/js/backbone-1.1.2.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-(function(t,e){if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(i,r,s){t.Backbone=e(t,s,i,r)})}else if(typeof exports!=="undefined"){var i=require("underscore");e(t,exports,i)}else{t.Backbone=e(t,{},t._,t.jQuery||t.Zepto||t.ender||t.$)}})(this,function(t,e,i,r){var s=t.Backbone;var n=[];var a=n.push;var o=n.slice;var h=n.splice;e.VERSION="1.1.2";e.$=r;e.noConflict=function(){t.Backbone=s;return this};e.emulateHTTP=false;e.emulateJSON=false;var u=e.Events={on:function(t,e,i){if(!c(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,r){if(!c(this,"once",t,[e,r])||!e)return this;var s=this;var n=i.once(function(){s.off(t,n);e.apply(this,arguments)});n._callback=e;return this.on(t,n,r)},off:function(t,e,r){var s,n,a,o,h,u,l,f;if(!this._events||!c(this,"off",t,[e,r]))return this;if(!t&&!e&&!r){this._events=void 0;return this}o=t?[t]:i.keys(this._events);for(h=0,u=o.length;h<u;h++){t=o[h];if(a=this._events[t]){this._events[t]=s=[];if(e||r){for(l=0,f=a.length;l<f;l++){n=a[l];if(e&&e!==n.callback&&e!==n.callback._callback||r&&r!==n.context){s.push(n)}}}if(!s.length)delete this._events[t]}}return this},trigger:function(t){if(!this._events)return this;var e=o.call(arguments,1);if(!c(this,"trigger",t,e))return this;var i=this._events[t];var r=this._events.all;if(i)f(i,e);if(r)f(r,arguments);return this},stopListening:function(t,e,r){var s=this._listeningTo;if(!s)return this;var n=!e&&!r;if(!r&&typeof e==="object")r=this;if(t)(s={})[t._listenId]=t;for(var a in s){t=s[a];t.off(e,r,this);if(n||i.isEmpty(t._events))delete this._listeningTo[a]}return this}};var l=/\s+/;var c=function(t,e,i,r){if(!i)return true;if(typeof i==="object"){for(var s in i){t[e].apply(t,[s,i[s]].concat(r))}return false}if(l.test(i)){var n=i.split(l);for(var a=0,o=n.length;a<o;a++){t[e].apply(t,[n[a]].concat(r))}return false}return true};var f=function(t,e){var i,r=-1,s=t.length,n=e[0],a=e[1],o=e[2];switch(e.length){case 0:while(++r<s)(i=t[r]).callback.call(i.ctx);return;case 1:while(++r<s)(i=t[r]).callback.call(i.ctx,n);return;case 2:while(++r<s)(i=t[r]).callback.call(i.ctx,n,a);return;case 3:while(++r<s)(i=t[r]).callback.call(i.ctx,n,a,o);return;default:while(++r<s)(i=t[r]).callback.apply(i.ctx,e);return}};var d={listenTo:"on",listenToOnce:"once"};i.each(d,function(t,e){u[e]=function(e,r,s){var n=this._listeningTo||(this._listeningTo={});var a=e._listenId||(e._listenId=i.uniqueId("l"));n[a]=e;if(!s&&typeof r==="object")s=this;e[t](r,s,this);return this}});u.bind=u.on;u.unbind=u.off;i.extend(e,u);var p=e.Model=function(t,e){var r=t||{};e||(e={});this.cid=i.uniqueId("c");this.attributes={};if(e.collection)this.collection=e.collection;if(e.parse)r=this.parse(r,e)||{};r=i.defaults({},r,i.result(this,"defaults"));this.set(r,e);this.changed={};this.initialize.apply(this,arguments)};i.extend(p.prototype,u,{changed:null,validationError:null,idAttribute:"id",initialize:function(){},toJSON:function(t){return i.clone(this.attributes)},sync:function(){return e.sync.apply(this,arguments)},get:function(t){return this.attributes[t]},escape:function(t){return i.escape(this.get(t))},has:function(t){return this.get(t)!=null},set:function(t,e,r){var s,n,a,o,h,u,l,c;if(t==null)return this;if(typeof t==="object"){n=t;r=e}else{(n={})[t]=e}r||(r={});if(!this._validate(n,r))return false;a=r.unset;h=r.silent;o=[];u=this._changing;this._changing=true;if(!u){this._previousAttributes=i.clone(this.attributes);this.changed={}}c=this.attributes,l=this._previousAttributes;if(this.idAttribute in n)this.id=n[this.idAttribute];for(s in n){e=n[s];if(!i.isEqual(c[s],e))o.push(s);if(!i.isEqual(l[s],e)){this.changed[s]=e}else{delete this.changed[s]}a?delete c[s]:c[s]=e}if(!h){if(o.length)this._pending=r;for(var f=0,d=o.length;f<d;f++){this.trigger("change:"+o[f],this,c[o[f]],r)}}if(u)return this;if(!h){while(this._pending){r=this._pending;this._pending=false;this.trigger("change",this,r)}}this._pending=false;this._changing=false;return this},unset:function(t,e){return this.set(t,void 0,i.extend({},e,{unset:true}))},clear:function(t){var e={};for(var r in this.attributes)e[r]=void 0;return this.set(e,i.extend({},t,{unset:true}))},hasChanged:function(t){if(t==null)return!i.isEmpty(this.changed);return i.has(this.changed,t)},changedAttributes:function(t){if(!t)return this.hasChanged()?i.clone(this.changed):false;var e,r=false;var s=this._changing?this._previousAttributes:this.attributes;for(var n in t){if(i.isEqual(s[n],e=t[n]))continue;(r||(r={}))[n]=e}return r},previous:function(t){if(t==null||!this._previousAttributes)return null;return this._previousAttributes[t]},previousAttributes:function(){return i.clone(this._previousAttributes)},fetch:function(t){t=t?i.clone(t):{};if(t.parse===void 0)t.parse=true;var e=this;var r=t.success;t.success=function(i){if(!e.set(e.parse(i,t),t))return false;if(r)r(e,i,t);e.trigger("sync",e,i,t)};q(this,t);return this.sync("read",this,t)},save:function(t,e,r){var s,n,a,o=this.attributes;if(t==null||typeof t==="object"){s=t;r=e}else{(s={})[t]=e}r=i.extend({validate:true},r);if(s&&!r.wait){if(!this.set(s,r))return false}else{if(!this._validate(s,r))return false}if(s&&r.wait){this.attributes=i.extend({},o,s)}if(r.parse===void 0)r.parse=true;var h=this;var u=r.success;r.success=function(t){h.attributes=o;var e=h.parse(t,r);if(r.wait)e=i.extend(s||{},e);if(i.isObject(e)&&!h.set(e,r)){return false}if(u)u(h,t,r);h.trigger("sync",h,t,r)};q(this,r);n=this.isNew()?"create":r.patch?"patch":"update";if(n==="patch")r.attrs=s;a=this.sync(n,this,r);if(s&&r.wait)this.attributes=o;return a},destroy:function(t){t=t?i.clone(t):{};var e=this;var r=t.success;var s=function(){e.trigger("destroy",e,e.collection,t)};t.success=function(i){if(t.wait||e.isNew())s();if(r)r(e,i,t);if(!e.isNew())e.trigger("sync",e,i,t)};if(this.isNew()){t.success();return false}q(this,t);var n=this.sync("delete",this,t);if(!t.wait)s();return n},url:function(){var t=i.result(this,"urlRoot")||i.result(this.collection,"url")||M();if(this.isNew())return t;return t.replace(/([^\/])$/,"$1/")+encodeURIComponent(this.id)},parse:function(t,e){return t},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return!this.has(this.idAttribute)},isValid:function(t){return this._validate({},i.extend(t||{},{validate:true}))},_validate:function(t,e){if(!e.validate||!this.validate)return true;t=i.extend({},this.attributes,t);var r=this.validationError=this.validate(t,e)||null;if(!r)return true;this.trigger("invalid",this,r,i.extend(e,{validationError:r}));return false}});var v=["keys","values","pairs","invert","pick","omit"];i.each(v,function(t){p.prototype[t]=function(){var e=o.call(arguments);e.unshift(this.attributes);return i[t].apply(i,e)}});var g=e.Collection=function(t,e){e||(e={});if(e.model)this.model=e.model;if(e.comparator!==void 0)this.comparator=e.comparator;this._reset();this.initialize.apply(this,arguments);if(t)this.reset(t,i.extend({silent:true},e))};var m={add:true,remove:true,merge:true};var y={add:true,remove:false};i.extend(g.prototype,u,{model:p,initialize:function(){},toJSON:function(t){return this.map(function(e){return e.toJSON(t)})},sync:function(){return e.sync.apply(this,arguments)},add:function(t,e){return this.set(t,i.extend({merge:false},e,y))},remove:function(t,e){var r=!i.isArray(t);t=r?[t]:i.clone(t);e||(e={});var s,n,a,o;for(s=0,n=t.length;s<n;s++){o=t[s]=this.get(t[s]);if(!o)continue;delete this._byId[o.id];delete this._byId[o.cid];a=this.indexOf(o);this.models.splice(a,1);this.length--;if(!e.silent){e.index=a;o.trigger("remove",o,this,e)}this._removeReference(o,e)}return r?t[0]:t},set:function(t,e){e=i.defaults({},e,m);if(e.parse)t=this.parse(t,e);var r=!i.isArray(t);t=r?t?[t]:[]:i.clone(t);var s,n,a,o,h,u,l;var c=e.at;var f=this.model;var d=this.comparator&&c==null&&e.sort!==false;var v=i.isString(this.comparator)?this.comparator:null;var g=[],y=[],_={};var b=e.add,w=e.merge,x=e.remove;var E=!d&&b&&x?[]:false;for(s=0,n=t.length;s<n;s++){h=t[s]||{};if(h instanceof p){a=o=h}else{a=h[f.prototype.idAttribute||"id"]}if(u=this.get(a)){if(x)_[u.cid]=true;if(w){h=h===o?o.attributes:h;if(e.parse)h=u.parse(h,e);u.set(h,e);if(d&&!l&&u.hasChanged(v))l=true}t[s]=u}else if(b){o=t[s]=this._prepareModel(h,e);if(!o)continue;g.push(o);this._addReference(o,e)}o=u||o;if(E&&(o.isNew()||!_[o.id]))E.push(o);_[o.id]=true}if(x){for(s=0,n=this.length;s<n;++s){if(!_[(o=this.models[s]).cid])y.push(o)}if(y.length)this.remove(y,e)}if(g.length||E&&E.length){if(d)l=true;this.length+=g.length;if(c!=null){for(s=0,n=g.length;s<n;s++){this.models.splice(c+s,0,g[s])}}else{if(E)this.models.length=0;var k=E||g;for(s=0,n=k.length;s<n;s++){this.models.push(k[s])}}}if(l)this.sort({silent:true});if(!e.silent){for(s=0,n=g.length;s<n;s++){(o=g[s]).trigger("add",o,this,e)}if(l||E&&E.length)this.trigger("sort",this,e)}return r?t[0]:t},reset:function(t,e){e||(e={});for(var r=0,s=this.models.length;r<s;r++){this._removeReference(this.models[r],e)}e.previousModels=this.models;this._reset();t=this.add(t,i.extend({silent:true},e));if(!e.silent)this.trigger("reset",this,e);return t},push:function(t,e){return this.add(t,i.extend({at:this.length},e))},pop:function(t){var e=this.at(this.length-1);this.remove(e,t);return e},unshift:function(t,e){return this.add(t,i.extend({at:0},e))},shift:function(t){var e=this.at(0);this.remove(e,t);return e},slice:function(){return o.apply(this.models,arguments)},get:function(t){if(t==null)return void 0;return this._byId[t]||this._byId[t.id]||this._byId[t.cid]},at:function(t){return this.models[t]},where:function(t,e){if(i.isEmpty(t))return e?void 0:[];return this[e?"find":"filter"](function(e){for(var i in t){if(t[i]!==e.get(i))return false}return true})},findWhere:function(t){return this.where(t,true)},sort:function(t){if(!this.comparator)throw new Error("Cannot sort a set without a comparator");t||(t={});if(i.isString(this.comparator)||this.comparator.length===1){this.models=this.sortBy(this.comparator,this)}else{this.models.sort(i.bind(this.comparator,this))}if(!t.silent)this.trigger("sort",this,t);return this},pluck:function(t){return i.invoke(this.models,"get",t)},fetch:function(t){t=t?i.clone(t):{};if(t.parse===void 0)t.parse=true;var e=t.success;var r=this;t.success=function(i){var s=t.reset?"reset":"set";r[s](i,t);if(e)e(r,i,t);r.trigger("sync",r,i,t)};q(this,t);return this.sync("read",this,t)},create:function(t,e){e=e?i.clone(e):{};if(!(t=this._prepareModel(t,e)))return false;if(!e.wait)this.add(t,e);var r=this;var s=e.success;e.success=function(t,i){if(e.wait)r.add(t,e);if(s)s(t,i,e)};t.save(null,e);return t},parse:function(t,e){return t},clone:function(){return new this.constructor(this.models)},_reset:function(){this.length=0;this.models=[];this._byId={}},_prepareModel:function(t,e){if(t instanceof p)return t;e=e?i.clone(e):{};e.collection=this;var r=new this.model(t,e);if(!r.validationError)return r;this.trigger("invalid",this,r.validationError,e);return false},_addReference:function(t,e){this._byId[t.cid]=t;if(t.id!=null)this._byId[t.id]=t;if(!t.collection)t.collection=this;t.on("all",this._onModelEvent,this)},_removeReference:function(t,e){if(this===t.collection)delete t.collection;t.off("all",this._onModelEvent,this)},_onModelEvent:function(t,e,i,r){if((t==="add"||t==="remove")&&i!==this)return;if(t==="destroy")this.remove(e,r);if(e&&t==="change:"+e.idAttribute){delete this._byId[e.previous(e.idAttribute)];if(e.id!=null)this._byId[e.id]=e}this.trigger.apply(this,arguments)}});var _=["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"];i.each(_,function(t){g.prototype[t]=function(){var e=o.call(arguments);e.unshift(this.models);return i[t].apply(i,e)}});var b=["groupBy","countBy","sortBy","indexBy"];i.each(b,function(t){g.prototype[t]=function(e,r){var s=i.isFunction(e)?e:function(t){return t.get(e)};return i[t](this.models,s,r)}});var w=e.View=function(t){this.cid=i.uniqueId("view");t||(t={});i.extend(this,i.pick(t,E));this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()};var x=/^(\S+)\s*(.*)$/;var E=["model","collection","el","id","attributes","className","tagName","events"];i.extend(w.prototype,u,{tagName:"div",$:function(t){return this.$el.find(t)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();this.stopListening();return this},setElement:function(t,i){if(this.$el)this.undelegateEvents();this.$el=t instanceof e.$?t:e.$(t);this.el=this.$el[0];if(i!==false)this.delegateEvents();return this},delegateEvents:function(t){if(!(t||(t=i.result(this,"events"))))return this;this.undelegateEvents();for(var e in t){var r=t[e];if(!i.isFunction(r))r=this[t[e]];if(!r)continue;var s=e.match(x);var n=s[1],a=s[2];r=i.bind(r,this);n+=".delegateEvents"+this.cid;if(a===""){this.$el.on(n,r)}else{this.$el.on(n,a,r)}}return this},undelegateEvents:function(){this.$el.off(".delegateEvents"+this.cid);return this},_ensureElement:function(){if(!this.el){var t=i.extend({},i.result(this,"attributes"));if(this.id)t.id=i.result(this,"id");if(this.className)t["class"]=i.result(this,"className");var r=e.$("<"+i.result(this,"tagName")+">").attr(t);this.setElement(r,false)}else{this.setElement(i.result(this,"el"),false)}}});e.sync=function(t,r,s){var n=T[t];i.defaults(s||(s={}),{emulateHTTP:e.emulateHTTP,emulateJSON:e.emulateJSON});var a={type:n,dataType:"json"};if(!s.url){a.url=i.result(r,"url")||M()}if(s.data==null&&r&&(t==="create"||t==="update"||t==="patch")){a.contentType="application/json";a.data=JSON.stringify(s.attrs||r.toJSON(s))}if(s.emulateJSON){a.contentType="application/x-www-form-urlencoded";a.data=a.data?{model:a.data}:{}}if(s.emulateHTTP&&(n==="PUT"||n==="DELETE"||n==="PATCH")){a.type="POST";if(s.emulateJSON)a.data._method=n;var o=s.beforeSend;s.beforeSend=function(t){t.setRequestHeader("X-HTTP-Method-Override",n);if(o)return o.apply(this,arguments)}}if(a.type!=="GET"&&!s.emulateJSON){a.processData=false}if(a.type==="PATCH"&&k){a.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var h=s.xhr=e.ajax(i.extend(a,s));r.trigger("request",r,h,s);return h};var k=typeof window!=="undefined"&&!!window.ActiveXObject&&!(window.XMLHttpRequest&&(new XMLHttpRequest).dispatchEvent);var T={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};e.ajax=function(){return e.$.ajax.apply(e.$,arguments)};var $=e.Router=function(t){t||(t={});if(t.routes)this.routes=t.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var S=/\((.*?)\)/g;var H=/(\(\?)?:\w+/g;var A=/\*\w+/g;var I=/[\-{}\[\]+?.,\\\^$|#\s]/g;i.extend($.prototype,u,{initialize:function(){},route:function(t,r,s){if(!i.isRegExp(t))t=this._routeToRegExp(t);if(i.isFunction(r)){s=r;r=""}if(!s)s=this[r];var n=this;e.history.route(t,function(i){var a=n._extractParameters(t,i);n.execute(s,a);n.trigger.apply(n,["route:"+r].concat(a));n.trigger("route",r,a);e.history.trigger("route",n,r,a)});return this},execute:function(t,e){if(t)t.apply(this,e)},navigate:function(t,i){e.history.navigate(t,i);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=i.result(this,"routes");var t,e=i.keys(this.routes);while((t=e.pop())!=null){this.route(t,this.routes[t])}},_routeToRegExp:function(t){t=t.replace(I,"\\$&").replace(S,"(?:$1)?").replace(H,function(t,e){return e?t:"([^/?]+)"}).replace(A,"([^?]*?)");return new RegExp("^"+t+"(?:\\?([\\s\\S]*))?$")},_extractParameters:function(t,e){var r=t.exec(e).slice(1);return i.map(r,function(t,e){if(e===r.length-1)return t||null;return t?decodeURIComponent(t):null})}});var N=e.History=function(){this.handlers=[];i.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var R=/^[#\/]|\s+$/g;var O=/^\/+|\/+$/g;var P=/msie [\w.]+/;var C=/\/$/;var j=/#.*$/;N.started=false;i.extend(N.prototype,u,{interval:50,atRoot:function(){return this.location.pathname.replace(/[^\/]$/,"$&/")===this.root},getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(t==null){if(this._hasPushState||!this._wantsHashChange||e){t=decodeURI(this.location.pathname+this.location.search);var i=this.root.replace(C,"");if(!t.indexOf(i))t=t.slice(i.length)}else{t=this.getHash()}}return t.replace(R,"")},start:function(t){if(N.started)throw new Error("Backbone.history has already been started");N.started=true;this.options=i.extend({root:"/"},this.options,t);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 r=this.getFragment();var s=document.documentMode;var n=P.exec(navigator.userAgent.toLowerCase())&&(!s||s<=7);this.root=("/"+this.root+"/").replace(O,"/");if(n&&this._wantsHashChange){var a=e.$('<iframe src="javascript:0" tabindex="-1">');this.iframe=a.hide().appendTo("body")[0].contentWindow;this.navigate(r)}if(this._hasPushState){e.$(window).on("popstate",this.checkUrl)}else if(this._wantsHashChange&&"onhashchange"in window&&!n){e.$(window).on("hashchange",this.checkUrl)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}this.fragment=r;var o=this.location;if(this._wantsHashChange&&this._wantsPushState){if(!this._hasPushState&&!this.atRoot()){this.fragment=this.getFragment(null,true);this.location.replace(this.root+"#"+this.fragment);return true}else if(this._hasPushState&&this.atRoot()&&o.hash){this.fragment=this.getHash().replace(R,"");this.history.replaceState({},document.title,this.root+this.fragment)}}if(!this.options.silent)return this.loadUrl()},stop:function(){e.$(window).off("popstate",this.checkUrl).off("hashchange",this.checkUrl);if(this._checkUrlInterval)clearInterval(this._checkUrlInterval);N.started=false},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe){e=this.getFragment(this.getHash(this.iframe))}if(e===this.fragment)return false;if(this.iframe)this.navigate(e);this.loadUrl()},loadUrl:function(t){t=this.fragment=this.getFragment(t);return i.any(this.handlers,function(e){if(e.route.test(t)){e.callback(t);return true}})},navigate:function(t,e){if(!N.started)return false;if(!e||e===true)e={trigger:!!e};var i=this.root+(t=this.getFragment(t||""));t=t.replace(j,"");if(this.fragment===t)return;this.fragment=t;if(t===""&&i!=="/")i=i.slice(0,-1);if(this._hasPushState){this.history[e.replace?"replaceState":"pushState"]({},document.title,i)}else if(this._wantsHashChange){this._updateHash(this.location,t,e.replace);if(this.iframe&&t!==this.getFragment(this.getHash(this.iframe))){if(!e.replace)this.iframe.document.open().close();this._updateHash(this.iframe.location,t,e.replace)}}else{return this.location.assign(i)}if(e.trigger)return this.loadUrl(t)},_updateHash:function(t,e,i){if(i){var r=t.href.replace(/(javascript:|#).*$/,"");t.replace(r+"#"+e)}else{t.hash="#"+e}}});e.history=new N;var U=function(t,e){var r=this;var s;if(t&&i.has(t,"constructor")){s=t.constructor}else{s=function(){return r.apply(this,arguments)}}i.extend(s,r,e);var n=function(){this.constructor=s};n.prototype=r.prototype;s.prototype=new n;if(t)i.extend(s.prototype,t);s.__super__=r.prototype;return s};p.extend=g.extend=$.extend=w.extend=N.extend=U;var M=function(){throw new Error('A "url" property or function must be specified')};var q=function(t,e){var i=e.error;e.error=function(r){if(i)i(t,r,e);t.trigger("error",t,r,e)}};return e});
-//# sourceMappingURL=backbone-min.map
diff --git a/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.js b/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.js
new file mode 100644
index 0000000..d8ccc65
--- /dev/null
+++ b/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.js
@@ -0,0 +1,1325 @@
+/*
+ backbone.paginator 2.0.0
+ http://github.com/backbone-paginator/backbone.paginator
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ 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 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.<Object>} [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] : [];
+ models = models.slice();
+
+ 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(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.<Object|Backbone.Model>} 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});
+ });
+ }
+ else if (!pageCol.length && state.totalRecords) {
+ pageCol.reset(fullCol.models.slice(pageStart - pageSize, pageEnd - pageSize),
+ _extend({}, options, {parse: false}));
+ }
+ 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);
+ if (!pageCol.length && state.totalRecords) {
+ pageCol.reset(fullCol.models.slice(pageStart - pageSize, pageEnd - pageSize),
+ _extend({}, options, {parse: false}));
+ }
+ }
+ }
+ 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.currentPage / 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.
+ */
+ hasPreviousPage: 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.
+ */
+ hasNextPage: 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.<Object>} 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.<Object>} 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 = options.url || this.url || "";
+ if (_isFunction(url)) url = url.call(this);
+ 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) {
+ var o = _isFunction(queryParams.order) ?
+ queryParams.order.call(thisCopy) :
+ queryParams.order;
+ data[o] = 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/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.min.js b/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.min.js
deleted file mode 100644
index 687349c..0000000
--- a/htrace-core/src/web/lib/js/backbone.paginator-2.0.2.min.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/*
- backbone.paginator 2.0.0
- http://github.com/backbone-paginator/backbone.paginator
-
- Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
- Licensed under the MIT @license.
-*/
-!function(a){if("object"==typeof exports)module.exports=a(require("underscore"),require("backbone"));else if("function"==typeof define&&define.amd)define(["underscore","backbone"],a);else if("undefined"!=typeof _&&"undefined"!=typeof Backbone){var b=Backbone.PageableCollection,c=a(_,Backbone);Backbone.PageableCollection.noConflict=function(){return Backbone.PageableCollection=b,c}}}(function(a,b){"use strict";function c(b,c){if(!a.isNumber(b)||a.isNaN(b)||!a.isFinite(b)||~~b!==b)throw new TypeError("`"+c+"` must be a finite integer");return b}function d(a){for(var b,c,d,e,f={},g=decodeURIComponent,h=a.split("&"),i=0,j=h.length;j>i;i++){var k=h[i];b=k.split("="),c=b[0],d=b[1]||!0,c=g(c),d=g(d),e=f[c],o(e)?e.push(d):f[c]=e?[e,d]:d}return f}function e(a,b,c){var d=a._events[b];if(d&&d.length){var e=d[d.length-1],f=e.callback;e.callback=function(){try{f.apply(this,arguments),c()}catch(a){throw a}finally{e.callback=f}}}else c()}var f=a.extend,g=a.omit,h=a.clone,i=a.each,j=a.pick,k=a.contains,l=a.isEmpty,m=a.pairs,n=a.invert,o=a.isArray,p=a.isFunction,q=a.isObject,r=a.keys,s=a.isUndefined,t=Math.ceil,u=Math.floor,v=Math.max,w=b.Collection.prototype,x=/[\s'"]/g,y=/[<>\s'"]/g,z=b.PageableCollection=b.Collection.extend({state:{firstPage:1,lastPage:null,currentPage:null,pageSize:25,totalPages:null,totalRecords:null,sortKey:null,order:-1},mode:"server",queryParams:{currentPage:"page",pageSize:"per_page",totalPages:"total_pages",totalRecords:"total_entries",sortKey:"sort_by",order:"order",directions:{"-1":"asc",1:"desc"}},constructor:function(a,b){w.constructor.apply(this,arguments),b=b||{};var c=this.mode=b.mode||this.mode||A.mode,d=f({},A.queryParams,this.queryParams,b.queryParams||{});d.directions=f({},A.queryParams.directions,this.queryParams.directions,d.directions||{}),this.queryParams=d;var e=this.state=f({},A.state,this.state,b.state||{});e.currentPage=null==e.currentPage?e.firstPage:e.currentPage,o(a)||(a=a?[a]:[]),a=a.slice(),"server"==c||null!=e.totalRecords||l(a)||(e.totalRecords=a.length),this.switchMode(c,f({fetch:!1,resetState:!1,models:a},b));var g=b.comparator;if(e.sortKey&&!g&&this.setSorting(e.sortKey,e.order,b),"server"!=c){var i=this.fullCollection;g&&b.full&&(this.comparator=null,i.comparator=g),b.full&&i.sort(),a&&!l(a)&&(this.reset(a,f({silent:!0},b)),this.getPage(e.currentPage),a.splice.apply(a,[0,a.length].concat(this.models)))}this._initState=h(this.state)},_makeFullCollection:function(a,c){var d,e,f,g=["url","model","sync","comparator"],h=this.constructor.prototype,i={};for(d=0,e=g.length;e>d;d++)f=g[d],s(h[f])||(i[f]=h[f]);var j=new(b.Collection.extend(i))(a,c);for(d=0,e=g.length;e>d;d++)f=g[d],this[f]!==h[f]&&(j[f]=this[f]);return j},_makeCollectionEventHandler:function(a,b){return function(c,d,g,j){var k=a._handlers;i(r(k),function(c){var d=k[c];a.off(c,d),b.off(c,d)});var l=h(a.state),m=l.firstPage,n=0===m?l.currentPage:l.currentPage-1,o=l.pageSize,p=n*o,q=p+o;if("add"==c){var u,v,w,x,j=j||{};if(g==b)v=b.indexOf(d),v>=p&&q>v&&(x=a,u=w=v-p);else{u=a.indexOf(d),v=p+u,x=b;var w=s(j.at)?v:j.at+p}if(j.onRemove||(++l.totalRecords,delete j.onRemove),a.state=a._checkState(l),x){x.add(d,f({},j||{},{at:w}));var y=u>=o?d:!s(j.at)&&q>w&&a.length>o?a.at(o):null;y&&e(g,c,function(){a.remove(y,{onAdd:!0})})}}if("remove"==c)if(j.onAdd)delete j.onAdd;else{if(--l.totalRecords){var z=l.totalPages=t(l.totalRecords/o);l.lastPage=0===m?z-1:z||m,l.currentPage>z&&(l.currentPage=l.lastPage)}else l.totalRecords=null,l.totalPages=null;a.state=a._checkState(l);var A,B=j.index;g==a?((A=b.at(q))?e(a,c,function(){a.push(A,{onRemove:!0})}):!a.length&&l.totalRecords&&a.reset(b.models.slice(p-o,q-o),f({},j,{parse:!1})),b.remove(d)):B>=p&&q>B&&((A=b.at(q-1))&&e(a,c,function(){a.push(A,{onRemove:!0})}),a.remove(d),!a.length&&l.totalRecords&&a.reset(b.models.slice(p-o,q-o),f({},j,{parse:!1})))}if("reset"==c)if(j=g,g=d,g==a&&null==j.from&&null==j.to){var C=b.models.slice(0,p),D=b.models.slice(p+a.models.length);b.reset(C.concat(a.models).concat(D),j)}else g==b&&((l.totalRecords=b.models.length)||(l.totalRecords=null,l.totalPages=null),"client"==a.mode&&(l.lastPage=l.currentPage=l.firstPage),a.state=a._checkState(l),a.reset(b.models.slice(p,q),f({},j,{parse:!1})));"sort"==c&&(j=g,g=d,g===b&&a.reset(b.models.slice(p,q),f({},j,{parse:!1}))),i(r(k),function(c){var d=k[c];i([a,b],function(a){a.on(c,d);var b=a._events[c]||[];b.unshift(b.pop())})})}},_checkState:function(a){var b=this.mode,d=this.links,e=a.totalRecords,f=a.pageSize,g=a.currentPage,h=a.firstPage,i=a.totalPages;if(null!=e&&null!=f&&null!=g&&null!=h&&("infinite"==b?d:!0)){if(e=c(e,"totalRecords"),f=c(f,"pageSize"),g=c(g,"currentPage"),h=c(h,"firstPage"),1>f)throw new RangeError("`pageSize` must be >= 1");if(i=a.totalPages=t(e/f),0>h||h>1)throw new RangeError("`firstPage must be 0 or 1`");if(a.lastPage=0===h?v(0,i-1):i||h,"infinite"==b){if(!d[g+""])throw new RangeError("No link found for page "+g)}else if(h>g||i>0&&(h?g>i:g>=i))throw new RangeError("`currentPage` must be firstPage <= currentPage "+(h?">":">=")+" totalPages if "+h+"-based. Got "+g+".")}return a},setPageSize:function(a,b){a=c(a,"pageSize"),b=b||{first:!1};var d=this.state,e=t(d.totalRecords/a),h=e?v(d.firstPage,u(e*d.currentPage/d.totalPages)):d.firstPage;return d=this.state=this._checkState(f({},d,{pageSize:a,currentPage:b.first?d.firstPage:h,totalPages:e})),this.getPage(d.currentPage,g(b,["first"]))},switchMode:function(b,c){if(!k(["server","client","infinite"],b))throw new TypeError('`mode` must be one of "server", "client" or "infinite"');c=c||{fetch:!0,resetState:!0};var d=this.state=c.resetState?h(this._initState):this._checkState(f({},this.state));this.mode=b;var e,j=this,l=this.fullCollection,m=this._handlers=this._handlers||{};if("server"==b||l)"server"==b&&l&&(i(r(m),function(a){e=m[a],j.off(a,e),l.off(a,e)}),delete this._handlers,this._fullComparator=l.comparator,delete this.fullCollection);else{l=this._makeFullCollection(c.models||[],c),l.pageableCollection=this,this.fullCollection=l;var n=this._makeCollectionEventHandler(this,l);i(["add","remove","reset","sort"],function(b){m[b]=e=a.bind(n,{},b),j.on(b,e),l.on(b,e)}),l.comparator=this._fullComparator}if("infinite"==b)for(var o=this.links={},p=d.firstPage,q=t(d.totalRecords/d.pageSize),s=0===p?v(0,q-1):q||p,u=d.firstPage;s>=u;u++)o[u]=this.url;else this.links&&delete this.links;return c.fetch?this.fetch(g(c,"fetch","resetState")):this},hasPreviousPage:function(){var a=this.state,b=a.currentPage;return"infinite"!=this.mode?b>a.firstPage:!!this.links[b-1]},hasNextPage:function(){var a=this.state,b=this.state.currentPage;return"infinite"!=this.mode?b<a.lastPage:!!this.links[b+1]},getFirstPage:function(a){return this.getPage("first",a)},getPreviousPage:function(a){return this.getPage("prev",a)},getNextPage:function(a){return this.getPage("next",a)},getLastPage:function(a){return this.getPage("last",a)},getPage:function(a,b){var d=this.mode,e=this.fullCollection;b=b||{fetch:!1};var h=this.state,i=h.firstPage,j=h.currentPage,k=h.lastPage,m=h.pageSize,n=a;switch(a){case"first":n=i;break;case"prev":n=j-1;break;case"next":n=j+1;break;case"last":n=k;break;default:n=c(a,"index")}this.state=this._checkState(f({},h,{currentPage:n})),b.from=j,b.to=n;var o=(0===i?n:n-1)*m,p=e&&e.length?e.models.slice(o,o+m):[];return"client"!=d&&("infinite"!=d||l(p))||b.fetch?("infinite"==d&&(b.url=this.links[n]),this.fetch(g(b,"fetch"))):(this.reset(p,g(b,"fetch")),this)},getPageByOffset:function(a,b){if(0>a)throw new RangeError("`offset must be > 0`");a=c(a);var d=u(a/this.state.pageSize);return 0!==this.state.firstPage&&d++,d>this.state.lastPage&&(d=this.state.lastPage),this.getPage(d,b)},sync:function(a,c,d){var e=this;if("infinite"==e.mode){var g=d.success,h=e.state.currentPage;d.success=function(a,b,c){var i=e.links,j=e.parseLinks(a,f({xhr:c},d));j.first&&(i[e.state.firstPage]=j.first),j.prev&&(i[h-1]=j.prev),j.next&&(i[h+1]=j.next),g&&g(a,b,c)}}return(w.sync||b.sync).call(e,a,c,d)},parseLinks:function(a,b){var c={},d=b.xhr.getResponseHeader("Link");if(d){var e=["first","prev","next"];i(d.split(","),function(a){var b=a.split(";"),d=b[0].replace(y,""),f=b.slice(1);i(f,function(a){var b=a.split("="),f=b[0].replace(x,""),g=b[1].replace(x,"");"rel"==f&&k(e,g)&&(c[g]=d)})})}return c},parse:function(a,b){var c=this.parseState(a,h(this.queryParams),h(this.state),b);return c&&(this.state=this._checkState(f({},this.state,c))),this.parseRecords(a,b)},parseState:function(b,c,d){if(b&&2===b.length&&q(b[0])&&o(b[1])){var e=h(d),f=b[0];return i(m(g(c,"directions")),function(b){var c=b[0],d=b[1],g=f[d];s(g)||a.isNull(g)||(e[c]=f[d])}),f.order&&(e.order=1*n(c.directions)[f.order]),e}},parseRecords:function(a){return a&&2===a.length&&q(a[0])&&o(a[1])?a[1]:a},fetch:function(a){a=a||{};var b=this._checkState(this.state),c=this.mode;"infinite"!=c||a.url||(a.url=this.links[b.currentPage]);var e=a.data||{},i=a.url||this.url||"";p(i)&&(i=i.call(this));var k=i.indexOf("?");-1!=k&&(f(e,d(i.slice(k+1))),i=i.slice(0,k)),a.url=i,a.data=e;var l,n,o,q,t="client"==this.mode?j(this.queryParams,"sortKey","order"):g(j(this.queryParams,r(A.queryParams)),"directions"),u=m(t),v=h(this);for(l=0;l<u.length;l++)n=u[l],o=n[0],q=n[1],q=p(q)?q.call(v):q,null!=b[o]&&null!=q&&(e[q]=b[o]);if(b.sortKey&&b.order){var x=p(t.order)?t.order.call(v):t.order;e[x]=this.queryParams.directions[b.order+""]}else b.sortKey||delete e[t.order];var y=m(g(this.queryParams,r(A.queryParams)));for(l=0;l<y.length;l++)n=y[l],q=n[1],q=p(q)?q.call(v):q,null!=q&&(e[n[0]]=q);if("server"!=c){var z=this,B=this.fullCollection,C=a.success;return a.success=function(b,d,e){e=e||{},s(a.silent)?delete e.silent:e.silent=a.silent;var g=b.models;"client"==c?B.reset(g,e):(B.add(g,f({at:B.length},f(e,{parse:!1}))),z.trigger("reset",z,e)),C&&C(b,d,e)},w.fetch.call(this,f({},a,{silent:!0}))}return w.fetch.call(this,a)},_makeComparator:function(a,b,c){var d=this.state;return a=a||d.sortKey,b=b||d.order,a&&b?(c||(c=function(a,b){return a.get(b)}),function(d,e){var f,g=c(d,a),h=c(e,a);return 1===b&&(f=g,g=h,h=f),g===h?0:h>g?-1:1}):void 0},setSorting:function(a,b,c){var d=this.state;d.sortKey=a,d.order=b=b||d.order;var e=this.fullCollection,g=!1,h=!1;a||(g=h=!0);var i=this.mode;c=f({side:"client"==i?i:"server",full:!0},c);var j=this._makeComparator(a,b,c.sortValue),k=c.full,l=c.side;return"client"==l?k?(e&&(e.comparator=j),g=!0):(this.comparator=j,h=!0):"server"!=l||k||(this.comparator=j),g&&(this.comparator=null),h&&e&&(e.comparator=null),this}}),A=z.prototype;return z});
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/backgrid-0.3.5.js b/htrace-core/src/web/lib/js/backgrid-0.3.5.js
new file mode 100644
index 0000000..6f8da79
--- /dev/null
+++ b/htrace-core/src/web/lib/js/backgrid-0.3.5.js
@@ -0,0 +1,2883 @@
+/*!
+ backgrid
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2014 Jimmy Yuen Ho Wong and contributors <wyuenho@gmail.com>
+ Licensed under the MIT license.
+*/
+
+(function (factory) {
+
+ // CommonJS
+ if (typeof exports == "object") {
+ module.exports = factory(module.exports,
+ require("underscore"),
+ require("backbone"));
+ }
+ // Browser
+ else factory(this, this._, this.Backbone);
+}(function (root, _, Backbone) {
+
+ "use strict";
+
+/*
+ backgrid
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT license.
+*/
+
+// Copyright 2009, 2010 Kristopher Michael Kowal
+// https://github.com/kriskowal/es5-shim
+// ES5 15.5.4.20
+// http://es5.github.com/#x15.5.4.20
+var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" +
+ "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" +
+ "\u2029\uFEFF";
+if (!String.prototype.trim || ws.trim()) {
+ // http://blog.stevenlevithan.com/archives/faster-trim-javascript
+ // http://perfectionkills.com/whitespace-deviations/
+ ws = "[" + ws + "]";
+ var trimBeginRegexp = new RegExp("^" + ws + ws + "*"),
+ trimEndRegexp = new RegExp(ws + ws + "*$");
+ String.prototype.trim = function trim() {
+ if (this === undefined || this === null) {
+ throw new TypeError("can't convert " + this + " to object");
+ }
+ return String(this)
+ .replace(trimBeginRegexp, "")
+ .replace(trimEndRegexp, "");
+ };
+}
+
+function lpad(str, length, padstr) {
+ var paddingLen = length - (str + '').length;
+ paddingLen = paddingLen < 0 ? 0 : paddingLen;
+ var padding = '';
+ for (var i = 0; i < paddingLen; i++) {
+ padding = padding + padstr;
+ }
+ return padding + str;
+}
+
+var $ = Backbone.$;
+
+var Backgrid = root.Backgrid = {
+
+ Extension: {},
+
+ resolveNameToClass: function (name, suffix) {
+ if (_.isString(name)) {
+ var key = _.map(name.split('-'), function (e) {
+ return e.slice(0, 1).toUpperCase() + e.slice(1);
+ }).join('') + suffix;
+ var klass = Backgrid[key] || Backgrid.Extension[key];
+ if (_.isUndefined(klass)) {
+ throw new ReferenceError("Class '" + key + "' not found");
+ }
+ return klass;
+ }
+
+ return name;
+ },
+
+ callByNeed: function () {
+ var value = arguments[0];
+ if (!_.isFunction(value)) return value;
+
+ var context = arguments[1];
+ var args = [].slice.call(arguments, 2);
+ return value.apply(context, !!(args + '') ? args : []);
+ }
+
+};
+_.extend(Backgrid, Backbone.Events);
+
+/**
+ Command translates a DOM Event into commands that Backgrid
+ recognizes. Interested parties can listen on selected Backgrid events that
+ come with an instance of this class and act on the commands.
+
+ It is also possible to globally rebind the keyboard shortcuts by replacing
+ the methods in this class' prototype.
+
+ @class Backgrid.Command
+ @constructor
+ */
+var Command = Backgrid.Command = function (evt) {
+ _.extend(this, {
+ altKey: !!evt.altKey,
+ "char": evt["char"],
+ charCode: evt.charCode,
+ ctrlKey: !!evt.ctrlKey,
+ key: evt.key,
+ keyCode: evt.keyCode,
+ locale: evt.locale,
+ location: evt.location,
+ metaKey: !!evt.metaKey,
+ repeat: !!evt.repeat,
+ shiftKey: !!evt.shiftKey,
+ which: evt.which
+ });
+};
+_.extend(Command.prototype, {
+ /**
+ Up Arrow
+
+ @member Backgrid.Command
+ */
+ moveUp: function () { return this.keyCode == 38; },
+ /**
+ Down Arrow
+
+ @member Backgrid.Command
+ */
+ moveDown: function () { return this.keyCode === 40; },
+ /**
+ Shift Tab
+
+ @member Backgrid.Command
+ */
+ moveLeft: function () { return this.shiftKey && this.keyCode === 9; },
+ /**
+ Tab
+
+ @member Backgrid.Command
+ */
+ moveRight: function () { return !this.shiftKey && this.keyCode === 9; },
+ /**
+ Enter
+
+ @member Backgrid.Command
+ */
+ save: function () { return this.keyCode === 13; },
+ /**
+ Esc
+
+ @member Backgrid.Command
+ */
+ cancel: function () { return this.keyCode === 27; },
+ /**
+ None of the above.
+
+ @member Backgrid.Command
+ */
+ passThru: function () {
+ return !(this.moveUp() || this.moveDown() || this.moveLeft() ||
+ this.moveRight() || this.save() || this.cancel());
+ }
+});
+
+/*
+ backgrid
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT license.
+*/
+
+/**
+ Just a convenient class for interested parties to subclass.
+
+ The default Cell classes don't require the formatter to be a subclass of
+ Formatter as long as the fromRaw(rawData) and toRaw(formattedData) methods
+ are defined.
+
+ @abstract
+ @class Backgrid.CellFormatter
+ @constructor
+*/
+var CellFormatter = Backgrid.CellFormatter = function () {};
+_.extend(CellFormatter.prototype, {
+
+ /**
+ Takes a raw value from a model and returns an optionally formatted string
+ for display. The default implementation simply returns the supplied value
+ as is without any type conversion.
+
+ @member Backgrid.CellFormatter
+ @param {*} rawData
+ @param {Backbone.Model} model Used for more complicated formatting
+ @return {*}
+ */
+ fromRaw: function (rawData, model) {
+ return rawData;
+ },
+
+ /**
+ Takes a formatted string, usually from user input, and returns a
+ appropriately typed value for persistence in the model.
+
+ If the user input is invalid or unable to be converted to a raw value
+ suitable for persistence in the model, toRaw must return `undefined`.
+
+ @member Backgrid.CellFormatter
+ @param {string} formattedData
+ @param {Backbone.Model} model Used for more complicated formatting
+ @return {*|undefined}
+ */
+ toRaw: function (formattedData, model) {
+ return formattedData;
+ }
+
+});
+
+/**
+ A floating point number formatter. Doesn't understand scientific notation at
+ the moment.
+
+ @class Backgrid.NumberFormatter
+ @extends Backgrid.CellFormatter
+ @constructor
+ @throws {RangeError} If decimals < 0 or > 20.
+*/
+var NumberFormatter = Backgrid.NumberFormatter = function (options) {
+ _.extend(this, this.defaults, options || {});
+
+ if (this.decimals < 0 || this.decimals > 20) {
+ throw new RangeError("decimals must be between 0 and 20");
+ }
+};
+NumberFormatter.prototype = new CellFormatter();
+_.extend(NumberFormatter.prototype, {
+
+ /**
+ @member Backgrid.NumberFormatter
+ @cfg {Object} options
+
+ @cfg {number} [options.decimals=2] Number of decimals to display. Must be an integer.
+
+ @cfg {string} [options.decimalSeparator='.'] The separator to use when
+ displaying decimals.
+
+ @cfg {string} [options.orderSeparator=','] The separator to use to
+ separator thousands. May be an empty string.
+ */
+ defaults: {
+ decimals: 2,
+ decimalSeparator: '.',
+ orderSeparator: ','
+ },
+
+ HUMANIZED_NUM_RE: /(\d)(?=(?:\d{3})+$)/g,
+
+ /**
+ Takes a floating point number and convert it to a formatted string where
+ every thousand is separated by `orderSeparator`, with a `decimal` number of
+ decimals separated by `decimalSeparator`. The number returned is rounded
+ the usual way.
+
+ @member Backgrid.NumberFormatter
+ @param {number} number
+ @param {Backbone.Model} model Used for more complicated formatting
+ @return {string}
+ */
+ fromRaw: function (number, model) {
+ if (_.isNull(number) || _.isUndefined(number)) return '';
+
+ number = number.toFixed(~~this.decimals);
+
+ var parts = number.split('.');
+ var integerPart = parts[0];
+ var decimalPart = parts[1] ? (this.decimalSeparator || '.') + parts[1] : '';
+
+ return integerPart.replace(this.HUMANIZED_NUM_RE, '$1' + this.orderSeparator) + decimalPart;
+ },
+
+ /**
+ Takes a string, possibly formatted with `orderSeparator` and/or
+ `decimalSeparator`, and convert it back to a number.
+
+ @member Backgrid.NumberFormatter
+ @param {string} formattedData
+ @param {Backbone.Model} model Used for more complicated formatting
+ @return {number|undefined} Undefined if the string cannot be converted to
+ a number.
+ */
+ toRaw: function (formattedData, model) {
+ formattedData = formattedData.trim();
+
+ if (formattedData === '') return null;
+
+ var rawData = '';
+
+ var thousands = formattedData.split(this.orderSeparator);
+ for (var i = 0; i < thousands.length; i++) {
+ rawData += thousands[i];
+ }
+
+ var decimalParts = rawData.split(this.decimalSeparator);
+ rawData = '';
+ for (var i = 0; i < decimalParts.length; i++) {
+ rawData = rawData + decimalParts[i] + '.';
+ }
+
+ if (rawData[rawData.length - 1] === '.') {
+ rawData = rawData.slice(0, rawData.length - 1);
+ }
+
+ var result = (rawData * 1).toFixed(~~this.decimals) * 1;
+ if (_.isNumber(result) && !_.isNaN(result)) return result;
+ }
+
+});
+
+/**
+ A number formatter that converts a floating point number, optionally
+ multiplied by a multiplier, to a percentage string and vice versa.
+
+ @class Backgrid.PercentFormatter
+ @extends Backgrid.NumberFormatter
+ @constructor
+ @throws {RangeError} If decimals < 0 or > 20.
+ */
+var PercentFormatter = Backgrid.PercentFormatter = function () {
+ Backgrid.NumberFormatter.apply(this, arguments);
+};
+
+PercentFormatter.prototype = new Backgrid.NumberFormatter(),
+
+_.extend(PercentFormatter.prototype, {
+
+ /**
+ @member Backgrid.PercentFormatter
+ @cfg {Object} options
+
+ @cfg {number} [options.multiplier=1] The number used to multiply the model
+ value for display.
+
+ @cfg {string} [options.symbol='%'] The symbol to append to the percentage
+ string.
+ */
+ defaults: _.extend({}, NumberFormatter.prototype.defaults, {
+ multiplier: 1,
+ symbol: "%"
+ }),
+
+ /**
+ Takes a floating point number, where the number is first multiplied by
+ `multiplier`, then converted to a formatted string like
+ NumberFormatter#fromRaw, then finally append `symbol` to the end.
+
+ @member Backgrid.PercentFormatter
+ @param {number} rawValue
+ @param {Backbone.Model} model Used for more complicated formatting
+ @return {string}
+ */
+ fromRaw: function (number, model) {
+ var args = [].slice.call(arguments, 1);
+ args.unshift(number * this.multiplier);
+ return (NumberFormatter.prototype.fromRaw.apply(this, args) || "0") + this.symbol;
+ },
+
+ /**
+ Takes a string, possibly appended with `symbol` and/or `decimalSeparator`,
+ and convert it back to a number for the model like NumberFormatter#toRaw,
+ and then dividing it by `multiplier`.
+
+ @member Backgrid.PercentFormatter
+ @param {string} formattedData
+ @param {Backbone.Model} model Used for more complicated formatting
+ @return {number|undefined} Undefined if the string cannot be converted to
+ a number.
+ */
+ toRaw: function (formattedValue, model) {
+ var tokens = formattedValue.split(this.symbol);
+ if (tokens && tokens[0] && tokens[1] === "" || tokens[1] == null) {
+ var rawValue = NumberFormatter.prototype.toRaw.call(this, tokens[0]);
+ if (_.isUndefined(rawValue)) return rawValue;
+ return rawValue / this.multiplier;
+ }
+ }
+
+});
+
+/**
+ Formatter to converts between various datetime formats.
+
+ This class only understands ISO-8601 formatted datetime strings and UNIX
+ offset (number of milliseconds since UNIX Epoch). See
+ Backgrid.Extension.MomentFormatter if you need a much more flexible datetime
+ formatter.
+
+ @class Backgrid.DatetimeFormatter
+ @extends Backgrid.CellFormatter
+ @constructor
+ @throws {Error} If both `includeDate` and `includeTime` are false.
+*/
+var DatetimeFormatter = Backgrid.DatetimeFormatter = function (options) {
+ _.extend(this, this.defaults, options || {});
+
+ if (!this.includeDate && !this.includeTime) {
+ throw new Error("Either includeDate or includeTime must be true");
+ }
+};
+DatetimeFormatter.prototype = new CellFormatter();
+_.extend(DatetimeFormatter.prototype, {
+
+ /**
+ @member Backgrid.DatetimeFormatter
+
+ @cfg {Object} options
+
+ @cfg {boolean} [options.includeDate=true] Whether the values include the
+ date part.
+
+ @cfg {boolean} [options.includeTime=true] Whether the values include the
+ time part.
+
+ @cfg {boolean} [options.includeMilli=false] If `includeTime` is true,
+ whether to include the millisecond part, if it exists.
+ */
+ defaults: {
+ includeDate: true,
+ includeTime: true,
+ includeMilli: false
+ },
+
+ DATE_RE: /^([+\-]?\d{4})-(\d{2})-(\d{2})$/,
+ TIME_RE: /^(\d{2}):(\d{2}):(\d{2})(\.(\d{3}))?$/,
+ ISO_SPLITTER_RE: /T|Z| +/,
+
+ _convert: function (data, validate) {
+ if ((data + '').trim() === '') return null;
+
+ var date, time = null;
+ if (_.isNumber(data)) {
+ var jsDate = new Date(data);
+ date = lpad(jsDate.getUTCFullYear(), 4, 0) + '-' + lpad(jsDate.getUTCMonth() + 1, 2, 0) + '-' + lpad(jsDate.getUTCDate(), 2, 0);
+ time = lpad(jsDate.getUTCHours(), 2, 0) + ':' + lpad(jsDate.getUTCMinutes(), 2, 0) + ':' + lpad(jsDate.getUTCSeconds(), 2, 0);
+ }
+ else {
+ data = data.trim();
+ var parts = data.split(this.ISO_SPLITTER_RE) || [];
+ date = this.DATE_RE.test(parts[0]) ? parts[0] : '';
+ time = date && parts[1] ? parts[1] : this.TIME_RE.test(parts[0]) ? parts[0] : '';
+ }
+
+ var YYYYMMDD = this.DATE_RE.exec(date) || [];
+ var HHmmssSSS = this.TIME_RE.exec(time) || [];
+
+ if (validate) {
+ if (this.includeDate && _.isUndefined(YYYYMMDD[0])) return;
+ if (this.includeTime && _.isUndefined(HHmmssSSS[0])) return;
+ if (!this.includeDate && date) return;
+ if (!this.includeTime && time) return;
+ }
+
+ var jsDate = new Date(Date.UTC(YYYYMMDD[1] * 1 || 0,
+ YYYYMMDD[2] * 1 - 1 || 0,
+ YYYYMMDD[3] * 1 || 0,
+ HHmmssSSS[1] * 1 || null,
+ HHmmssSSS[2] * 1 || null,
+ HHmmssSSS[3] * 1 || null,
+ HHmmssSSS[5] * 1 || null));
+
+ var result = '';
+
+ if (this.includeDate) {
+ result = lpad(jsDate.getUTCFullYear(), 4, 0) + '-' + lpad(jsDate.getUTCMonth() + 1, 2, 0) + '-' + lpad(jsDate.getUTCDate(), 2, 0);
+ }
+
+ if (this.includeTime) {
+ result = result + (this.includeDate ? 'T' : '') + lpad(jsDate.getUTCHours(), 2, 0) + ':' + lpad(jsDate.getUTCMinutes(), 2, 0) + ':' + lpad(jsDate.getUTCSeconds(), 2, 0);
+
+ if (this.includeMilli) {
+ result = result + '.' + lpad(jsDate.getUTCMilliseconds(), 3, 0);
+ }
+ }
+
+ if (this.includeDate && this.includeTime) {
+ result += "Z";
+ }
+
+ return result;
+ },
+
+ /**
+ Converts an ISO-8601 formatted datetime string to a datetime string, date
+ string or a time string. The timezone is ignored if supplied.
+
+ @member Backgrid.DatetimeFormatter
+ @param {string} rawData
+ @param {Backbone.Model} model Used for more complicated formatting
+ @return {string|null|undefined} ISO-8601 string in UTC. Null and undefined
+ values are returned as is.
+ */
+ fromRaw: function (rawData, model) {
+ if (_.isNull(rawData) || _.isUndefined(rawData)) return '';
+ return this._convert(rawData);
+ },
+
+ /**
+ Converts an ISO-8601 formatted datetime string to a datetime string, date
+ string or a time string. The timezone is ignored if supplied. This method
+ parses the input values exactly the same way as
+ Backgrid.Extension.MomentFormatter#fromRaw(), in addition to doing some
+ sanity checks.
+
+ @member Backgrid.DatetimeFormatter
+ @param {string} formattedData
+ @param {Backbone.Model} model Used for more complicated formatting
+ @return {string|undefined} ISO-8601 string in UTC. Undefined if a date is
+ found when `includeDate` is false, or a time is found when `includeTime` is
+ false, or if `includeDate` is true and a date is not found, or if
+ `includeTime` is true and a time is not found.
+ */
+ toRaw: function (formattedData, model) {
+ return this._convert(formattedData, true);
+ }
+
+});
+
+/**
+ Formatter to convert any value to string.
+
+ @class Backgrid.StringFormatter
+ @extends Backgrid.CellFormatter
+ @constructor
+ */
+var StringFormatter = Backgrid.StringFormatter = function () {};
+StringFormatter.prototype = new CellFormatter();
+_.extend(StringFormatter.prototype, {
+ /**
+ Converts any value to a string using Ecmascript's implicit type
+ conversion. If the given value is `null` or `undefined`, an empty string is
+ returned instead.
+
+ @member Backgrid.StringFormatter
+ @param {*} rawValue
+ @param {Backbone.Model} model Used for more complicated formatting
+ @return {string}
+ */
+ fromRaw: function (rawValue, model) {
+ if (_.isUndefined(rawValue) || _.isNull(rawValue)) return '';
+ return rawValue + '';
+ }
+});
+
+/**
+ Simple email validation formatter.
+
+ @class Backgrid.EmailFormatter
+ @extends Backgrid.CellFormatter
+ @constructor
+ */
+var EmailFormatter = Backgrid.EmailFormatter = function () {};
+EmailFormatter.prototype = new CellFormatter();
+_.extend(EmailFormatter.prototype, {
+ /**
+ Return the input if it is a string that contains an '@' character and if
+ the strings before and after '@' are non-empty. If the input does not
+ validate, `undefined` is returned.
+
+ @member Backgrid.EmailFormatter
+ @param {*} formattedData
+ @param {Backbone.Model} model Used for more complicated formatting
+ @return {string|undefined}
+ */
+ toRaw: function (formattedData, model) {
+ var parts = formattedData.trim().split("@");
+ if (parts.length === 2 && _.all(parts)) {
+ return formattedData;
+ }
+ }
+});
+
+/**
+ Formatter for SelectCell.
+
+ If the type of a model value is not a string, it is expected that a subclass
+ of this formatter is provided to the SelectCell, with #toRaw overridden to
+ convert the string value returned from the DOM back to whatever value is
+ expected in the model.
+
+ @class Backgrid.SelectFormatter
+ @extends Backgrid.CellFormatter
+ @constructor
+*/
+var SelectFormatter = Backgrid.SelectFormatter = function () {};
+SelectFormatter.prototype = new CellFormatter();
+_.extend(SelectFormatter.prototype, {
+
+ /**
+ Normalizes raw scalar or array values to an array.
+
+ @member Backgrid.SelectFormatter
+ @param {*} rawValue
+ @param {Backbone.Model} model Used for more complicated formatting
+ @return {Array.<*>}
+ */
+ fromRaw: function (rawValue, model) {
+ return _.isArray(rawValue) ? rawValue : rawValue != null ? [rawValue] : [];
+ }
+});
+
+/*
+ backgrid
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT license.
+*/
+
+/**
+ Generic cell editor base class. Only defines an initializer for a number of
+ required parameters.
+
+ @abstract
+ @class Backgrid.CellEditor
+ @extends Backbone.View
+*/
+var CellEditor = Backgrid.CellEditor = Backbone.View.extend({
+
+ /**
+ Initializer.
+
+ @param {Object} options
+ @param {Backgrid.CellFormatter} options.formatter
+ @param {Backgrid.Column} options.column
+ @param {Backbone.Model} options.model
+
+ @throws {TypeError} If `formatter` is not a formatter instance, or when
+ `model` or `column` are undefined.
+ */
+ initialize: function (options) {
+ this.formatter = options.formatter;
+ this.column = options.column;
+ if (!(this.column instanceof Column)) {
+ this.column = new Column(this.column);
+ }
+
+ this.listenTo(this.model, "backgrid:editing", this.postRender);
+ },
+
+ /**
+ Post-rendering setup and initialization. Focuses the cell editor's `el` in
+ this default implementation. **Should** be called by Cell classes after
+ calling Backgrid.CellEditor#render.
+ */
+ postRender: function (model, column) {
+ if (column == null || column.get("name") == this.column.get("name")) {
+ this.$el.focus();
+ }
+ return this;
+ }
+
+});
+
+/**
+ InputCellEditor the cell editor type used by most core cell types. This cell
+ editor renders a text input box as its editor. The input will render a
+ placeholder if the value is empty on supported browsers.
+
+ @class Backgrid.InputCellEditor
+ @extends Backgrid.CellEditor
+*/
+var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({
+
+ /** @property */
+ tagName: "input",
+
+ /** @property */
+ attributes: {
+ type: "text"
+ },
+
+ /** @property */
+ events: {
+ "blur": "saveOrCancel",
+ "keydown": "saveOrCancel"
+ },
+
+ /**
+ Initializer. Removes this `el` from the DOM when a `done` event is
+ triggered.
+
+ @param {Object} options
+ @param {Backgrid.CellFormatter} options.formatter
+ @param {Backgrid.Column} options.column
+ @param {Backbone.Model} options.model
+ @param {string} [options.placeholder]
+ */
+ initialize: function (options) {
+ InputCellEditor.__super__.initialize.apply(this, arguments);
+
+ if (options.placeholder) {
+ this.$el.attr("placeholder", options.placeholder);
+ }
+ },
+
+ /**
+ Renders a text input with the cell value formatted for display, if it
+ exists.
+ */
+ render: function () {
+ var model = this.model;
+ this.$el.val(this.formatter.fromRaw(model.get(this.column.get("name")), model));
+ return this;
+ },
+
+ /**
+ If the key pressed is `enter`, `tab`, `up`, or `down`, converts the value
+ in the editor to a raw value for saving into the model using the formatter.
+
+ If the key pressed is `esc` the changes are undone.
+
+ If the editor goes out of focus (`blur`) but the value is invalid, the
+ event is intercepted and cancelled so the cell remains in focus pending for
+ further action. The changes are saved otherwise.
+
+ Triggers a Backbone `backgrid:edited` event from the model when successful,
+ and `backgrid:error` if the value cannot be converted. Classes listening to
+ the `error` event, usually the Cell classes, should respond appropriately,
+ usually by rendering some kind of error feedback.
+
+ @param {Event} e
+ */
+ saveOrCancel: function (e) {
+
+ var formatter = this.formatter;
+ var model = this.model;
+ var column = this.column;
+
+ var command = new Command(e);
+ var blurred = e.type === "blur";
+
+ if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() ||
+ command.save() || blurred) {
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ var val = this.$el.val();
+ var newValue = formatter.toRaw(val, model);
+ if (_.isUndefined(newValue)) {
+ model.trigger("backgrid:error", model, column, val);
+ }
+ else {
+ model.set(column.get("name"), newValue);
+ model.trigger("backgrid:edited", model, column, command);
+ }
+ }
+ // esc
+ else if (command.cancel()) {
+ // undo
+ e.stopPropagation();
+ model.trigger("backgrid:edited", model, column, command);
+ }
+ },
+
+ postRender: function (model, column) {
+ if (column == null || column.get("name") == this.column.get("name")) {
+ // move the cursor to the end on firefox if text is right aligned
+ if (this.$el.css("text-align") === "right") {
+ var val = this.$el.val();
+ this.$el.focus().val(null).val(val);
+ }
+ else this.$el.focus();
+ }
+ return this;
+ }
+
+});
+
+/**
+ The super-class for all Cell types. By default, this class renders a plain
+ table cell with the model value converted to a string using the
+ formatter. The table cell is clickable, upon which the cell will go into
+ editor mode, which is rendered by a Backgrid.InputCellEditor instance by
+ default. Upon encountering any formatting errors, this class will add an
+ `error` CSS class to the table cell.
+
+ @abstract
+ @class Backgrid.Cell
+ @extends Backbone.View
+*/
+var Cell = Backgrid.Cell = Backbone.View.extend({
+
+ /** @property */
+ tagName: "td",
+
+ /**
+ @property {Backgrid.CellFormatter|Object|string} [formatter=CellFormatter]
+ */
+ formatter: CellFormatter,
+
+ /**
+ @property {Backgrid.CellEditor} [editor=Backgrid.InputCellEditor] The
+ default editor for all cell instances of this class. This value must be a
+ class, it will be automatically instantiated upon entering edit mode.
+
+ See Backgrid.CellEditor
+ */
+ editor: InputCellEditor,
+
+ /** @property */
+ events: {
+ "click": "enterEditMode"
+ },
+
+ /**
+ Initializer.
+
+ @param {Object} options
+ @param {Backbone.Model} options.model
+ @param {Backgrid.Column} options.column
+
+ @throws {ReferenceError} If formatter is a string but a formatter class of
+ said name cannot be found in the Backgrid module.
+ */
+ initialize: function (options) {
+ this.column = options.column;
+ if (!(this.column instanceof Column)) {
+ this.column = new Column(this.column);
+ }
+
+ var column = this.column, model = this.model, $el = this.$el;
+
+ var formatter = Backgrid.resolveNameToClass(column.get("formatter") ||
+ this.formatter, "Formatter");
+
+ if (!_.isFunction(formatter.fromRaw) && !_.isFunction(formatter.toRaw)) {
+ formatter = new formatter();
+ }
+
+ this.formatter = formatter;
+
+ this.editor = Backgrid.resolveNameToClass(this.editor, "CellEditor");
+
+ this.listenTo(model, "change:" + column.get("name"), function () {
+ if (!$el.hasClass("editor")) this.render();
+ });
+
+ this.listenTo(model, "backgrid:error", this.renderError);
+
+ this.listenTo(column, "change:editable change:sortable change:renderable",
+ function (column) {
+ var changed = column.changedAttributes();
+ for (var key in changed) {
+ if (changed.hasOwnProperty(key)) {
+ $el.toggleClass(key, changed[key]);
+ }
+ }
+ });
+
+ if (Backgrid.callByNeed(column.editable(), column, model)) $el.addClass("editable");
+ if (Backgrid.callByNeed(column.sortable(), column, model)) $el.addClass("sortable");
+ if (Backgrid.callByNeed(column.renderable(), column, model)) $el.addClass("renderable");
+ },
+
+ /**
+ Render a text string in a table cell. The text is converted from the
+ model's raw value for this cell's column.
+ */
+ render: function () {
+ this.$el.empty();
+ var model = this.model;
+ this.$el.text(this.formatter.fromRaw(model.get(this.column.get("name")), model));
+ this.delegateEvents();
+ return this;
+ },
+
+ /**
+ If this column is editable, a new CellEditor instance is instantiated with
+ its required parameters. An `editor` CSS class is added to the cell upon
+ entering edit mode.
+
+ This method triggers a Backbone `backgrid:edit` event from the model when
+ the cell is entering edit mode and an editor instance has been constructed,
+ but before it is rendered and inserted into the DOM. The cell and the
+ constructed cell editor instance are sent as event parameters when this
+ event is triggered.
+
+ When this cell has finished switching to edit mode, a Backbone
+ `backgrid:editing` event is triggered from the model. The cell and the
+ constructed cell instance are also sent as parameters in the event.
+
+ When the model triggers a `backgrid:error` event, it means the editor is
+ unable to convert the current user input to an apprpriate value for the
+ model's column, and an `error` CSS class is added to the cell accordingly.
+ */
+ enterEditMode: function () {
+ var model = this.model;
+ var column = this.column;
+
+ var editable = Backgrid.callByNeed(column.editable(), column, model);
+ if (editable) {
+
+ this.currentEditor = new this.editor({
+ column: this.column,
+ model: this.model,
+ formatter: this.formatter
+ });
+
+ model.trigger("backgrid:edit", model, column, this, this.currentEditor);
+
+ // Need to redundantly undelegate events for Firefox
+ this.undelegateEvents();
+ this.$el.empty();
+ this.$el.append(this.currentEditor.$el);
+ this.currentEditor.render();
+ this.$el.addClass("editor");
+
+ model.trigger("backgrid:editing", model, column, this, this.currentEditor);
+ }
+ },
+
+ /**
+ Put an `error` CSS class on the table cell.
+ */
+ renderError: function (model, column) {
+ if (column == null || column.get("name") == this.column.get("name")) {
+ this.$el.addClass("error");
+ }
+ },
+
+ /**
+ Removes the editor and re-render in display mode.
+ */
+ exitEditMode: function () {
+ this.$el.removeClass("error");
+ this.currentEditor.remove();
+ this.stopListening(this.currentEditor);
+ delete this.currentEditor;
+ this.$el.removeClass("editor");
+ this.render();
+ },
+
+ /**
+ Clean up this cell.
+
+ @chainable
+ */
+ remove: function () {
+ if (this.currentEditor) {
+ this.currentEditor.remove.apply(this.currentEditor, arguments);
+ delete this.currentEditor;
+ }
+ return Cell.__super__.remove.apply(this, arguments);
+ }
+
+});
+
+/**
+ StringCell displays HTML escaped strings and accepts anything typed in.
+
+ @class Backgrid.StringCell
+ @extends Backgrid.Cell
+*/
+var StringCell = Backgrid.StringCell = Cell.extend({
+
+ /** @property */
+ className: "string-cell",
+
+ formatter: StringFormatter
+
+});
+
+/**
+ UriCell renders an HTML `<a>` anchor for the value and accepts URIs as user
+ input values. No type conversion or URL validation is done by the formatter
+ of this cell. Users who need URL validation are encourage to subclass UriCell
+ to take advantage of the parsing capabilities of the HTMLAnchorElement
+ available on HTML5-capable browsers or using a third-party library like
+ [URI.js](https://github.com/medialize/URI.js).
+
+ @class Backgrid.UriCell
+ @extends Backgrid.Cell
+*/
+var UriCell = Backgrid.UriCell = Cell.extend({
+
+ /** @property */
+ className: "uri-cell",
+
+ /**
+ @property {string} [title] The title attribute of the generated anchor. It
+ uses the display value formatted by the `formatter.fromRaw` by default.
+ */
+ title: null,
+
+ /**
+ @property {string} [target="_blank"] The target attribute of the generated
+ anchor.
+ */
+ target: "_blank",
+
+ initialize: function (options) {
+ UriCell.__super__.initialize.apply(this, arguments);
+ this.title = options.title || this.title;
+ this.target = options.target || this.target;
+ },
+
+ render: function () {
+ this.$el.empty();
+ var rawValue = this.model.get(this.column.get("name"));
+ var formattedValue = this.formatter.fromRaw(rawValue, this.model);
+ this.$el.append($("<a>", {
+ tabIndex: -1,
+ href: rawValue,
+ title: this.title || formattedValue,
+ target: this.target
+ }).text(formattedValue));
+ this.delegateEvents();
+ return this;
+ }
+
+});
+
+/**
+ Like Backgrid.UriCell, EmailCell renders an HTML `<a>` anchor for the
+ value. The `href` in the anchor is prefixed with `mailto:`. EmailCell will
+ complain if the user enters a string that doesn't contain the `@` sign.
+
+ @class Backgrid.EmailCell
+ @extends Backgrid.StringCell
+*/
+var EmailCell = Backgrid.EmailCell = StringCell.extend({
+
+ /** @property */
+ className: "email-cell",
+
+ formatter: EmailFormatter,
+
+ render: function () {
+ this.$el.empty();
+ var model = this.model;
+ var formattedValue = this.formatter.fromRaw(model.get(this.column.get("name")), model);
+ this.$el.append($("<a>", {
+ tabIndex: -1,
+ href: "mailto:" + formattedValue,
+ title: formattedValue
+ }).text(formattedValue));
+ this.delegateEvents();
+ return this;
+ }
+
+});
+
+/**
+ NumberCell is a generic cell that renders all numbers. Numbers are formatted
+ using a Backgrid.NumberFormatter.
+
+ @class Backgrid.NumberCell
+ @extends Backgrid.Cell
+*/
+var NumberCell = Backgrid.NumberCell = Cell.extend({
+
+ /** @property */
+ className: "number-cell",
+
+ /**
+ @property {number} [decimals=2] Must be an integer.
+ */
+ decimals: NumberFormatter.prototype.defaults.decimals,
+
+ /** @property {string} [decimalSeparator='.'] */
+ decimalSeparator: NumberFormatter.prototype.defaults.decimalSeparator,
+
+ /** @property {string} [orderSeparator=','] */
+ orderSeparator: NumberFormatter.prototype.defaults.orderSeparator,
+
+ /** @property {Backgrid.CellFormatter} [formatter=Backgrid.NumberFormatter] */
+ formatter: NumberFormatter,
+
+ /**
+ Initializes this cell and the number formatter.
+
+ @param {Object} options
+ @param {Backbone.Model} options.model
+ @param {Backgrid.Column} options.column
+ */
+ initialize: function (options) {
+ NumberCell.__super__.initialize.apply(this, arguments);
+ var formatter = this.formatter;
+ formatter.decimals = this.decimals;
+ formatter.decimalSeparator = this.decimalSeparator;
+ formatter.orderSeparator = this.orderSeparator;
+ }
+
+});
+
+/**
+ An IntegerCell is just a Backgrid.NumberCell with 0 decimals. If a floating
+ point number is supplied, the number is simply rounded the usual way when
+ displayed.
+
+ @class Backgrid.IntegerCell
+ @extends Backgrid.NumberCell
+*/
+var IntegerCell = Backgrid.IntegerCell = NumberCell.extend({
+
+ /** @property */
+ className: "integer-cell",
+
+ /**
+ @property {number} decimals Must be an integer.
+ */
+ decimals: 0
+});
+
+/**
+ A PercentCell is another Backgrid.NumberCell that takes a floating number,
+ optionally multiplied by a multiplier and display it as a percentage.
+
+ @class Backgrid.PercentCell
+ @extends Backgrid.NumberCell
+ */
+var PercentCell = Backgrid.PercentCell = NumberCell.extend({
+
+ /** @property */
+ className: "percent-cell",
+
+ /** @property {number} [multiplier=1] */
+ multiplier: PercentFormatter.prototype.defaults.multiplier,
+
+ /** @property {string} [symbol='%'] */
+ symbol: PercentFormatter.prototype.defaults.symbol,
+
+ /** @property {Backgrid.CellFormatter} [formatter=Backgrid.PercentFormatter] */
+ formatter: PercentFormatter,
+
+ /**
+ Initializes this cell and the percent formatter.
+
+ @param {Object} options
+ @param {Backbone.Model} options.model
+ @param {Backgrid.Column} options.column
+ */
+ initialize: function () {
+ PercentCell.__super__.initialize.apply(this, arguments);
+ var formatter = this.formatter;
+ formatter.multiplier = this.multiplier;
+ formatter.symbol = this.symbol;
+ }
+
+});
+
+/**
+ DatetimeCell is a basic cell that accepts datetime string values in RFC-2822
+ or W3C's subset of ISO-8601 and displays them in ISO-8601 format. For a much
+ more sophisticated date time cell with better datetime formatting, take a
+ look at the Backgrid.Extension.MomentCell extension.
+
+ @class Backgrid.DatetimeCell
+ @extends Backgrid.Cell
+
+ See:
+
+ - Backgrid.Extension.MomentCell
+ - Backgrid.DatetimeFormatter
+*/
+var DatetimeCell = Backgrid.DatetimeCell = Cell.extend({
+
+ /** @property */
+ className: "datetime-cell",
+
+ /**
+ @property {boolean} [includeDate=true]
+ */
+ includeDate: DatetimeFormatter.prototype.defaults.includeDate,
+
+ /**
+ @property {boolean} [includeTime=true]
+ */
+ includeTime: DatetimeFormatter.prototype.defaults.includeTime,
+
+ /**
+ @property {boolean} [includeMilli=false]
+ */
+ includeMilli: DatetimeFormatter.prototype.defaults.includeMilli,
+
+ /** @property {Backgrid.CellFormatter} [formatter=Backgrid.DatetimeFormatter] */
+ formatter: DatetimeFormatter,
+
+ /**
+ Initializes this cell and the datetime formatter.
+
+ @param {Object} options
+ @param {Backbone.Model} options.model
+ @param {Backgrid.Column} options.column
+ */
+ initialize: function (options) {
+ DatetimeCell.__super__.initialize.apply(this, arguments);
+ var formatter = this.formatter;
+ formatter.includeDate = this.includeDate;
+ formatter.includeTime = this.includeTime;
+ formatter.includeMilli = this.includeMilli;
+
+ var placeholder = this.includeDate ? "YYYY-MM-DD" : "";
+ placeholder += (this.includeDate && this.includeTime) ? "T" : "";
+ placeholder += this.includeTime ? "HH:mm:ss" : "";
+ placeholder += (this.includeTime && this.includeMilli) ? ".SSS" : "";
+
+ this.editor = this.editor.extend({
+ attributes: _.extend({}, this.editor.prototype.attributes, this.editor.attributes, {
+ placeholder: placeholder
+ })
+ });
+ }
+
+});
+
+/**
+ DateCell is a Backgrid.DatetimeCell without the time part.
+
+ @class Backgrid.DateCell
+ @extends Backgrid.DatetimeCell
+*/
+var DateCell = Backgrid.DateCell = DatetimeCell.extend({
+
+ /** @property */
+ className: "date-cell",
+
+ /** @property */
+ includeTime: false
+
+});
+
+/**
+ TimeCell is a Backgrid.DatetimeCell without the date part.
+
+ @class Backgrid.TimeCell
+ @extends Backgrid.DatetimeCell
+*/
+var TimeCell = Backgrid.TimeCell = DatetimeCell.extend({
+
+ /** @property */
+ className: "time-cell",
+
+ /** @property */
+ includeDate: false
+
+});
+
+/**
+ BooleanCellEditor renders a checkbox as its editor.
+
+ @class Backgrid.BooleanCellEditor
+ @extends Backgrid.CellEditor
+*/
+var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({
+
+ /** @property */
+ tagName: "input",
+
+ /** @property */
+ attributes: {
+ tabIndex: -1,
+ type: "checkbox"
+ },
+
+ /** @property */
+ events: {
+ "mousedown": function () {
+ this.mouseDown = true;
+ },
+ "blur": "enterOrExitEditMode",
+ "mouseup": function () {
+ this.mouseDown = false;
+ },
+ "change": "saveOrCancel",
+ "keydown": "saveOrCancel"
+ },
+
+ /**
+ Renders a checkbox and check it if the model value of this column is true,
+ uncheck otherwise.
+ */
+ render: function () {
+ var model = this.model;
+ var val = this.formatter.fromRaw(model.get(this.column.get("name")), model);
+ this.$el.prop("checked", val);
+ return this;
+ },
+
+ /**
+ Event handler. Hack to deal with the case where `blur` is fired before
+ `change` and `click` on a checkbox.
+ */
+ enterOrExitEditMode: function (e) {
+ if (!this.mouseDown) {
+ var model = this.model;
+ model.trigger("backgrid:edited", model, this.column, new Command(e));
+ }
+ },
+
+ /**
+ Event handler. Save the value into the model if the event is `change` or
+ one of the keyboard navigation key presses. Exit edit mode without saving
+ if `escape` was pressed.
+ */
+ saveOrCancel: function (e) {
+ var model = this.model;
+ var column = this.column;
+ var formatter = this.formatter;
+ var command = new Command(e);
+ // skip ahead to `change` when space is pressed
+ if (command.passThru() && e.type != "change") return true;
+ if (command.cancel()) {
+ e.stopPropagation();
+ model.trigger("backgrid:edited", model, column, command);
+ }
+
+ var $el = this.$el;
+ if (command.save() || command.moveLeft() || command.moveRight() || command.moveUp() ||
+ command.moveDown()) {
+ e.preventDefault();
+ e.stopPropagation();
+ var val = formatter.toRaw($el.prop("checked"), model);
+ model.set(column.get("name"), val);
+ model.trigger("backgrid:edited", model, column, command);
+ }
+ else if (e.type == "change") {
+ var val = formatter.toRaw($el.prop("checked"), model);
+ model.set(column.get("name"), val);
+ $el.focus();
+ }
+ }
+
+});
+
+/**
+ BooleanCell renders a checkbox both during display mode and edit mode. The
+ checkbox is checked if the model value is true, unchecked otherwise.
+
+ @class Backgrid.BooleanCell
+ @extends Backgrid.Cell
+*/
+var BooleanCell = Backgrid.BooleanCell = Cell.extend({
+
+ /** @property */
+ className: "boolean-cell",
+
+ /** @property */
+ editor: BooleanCellEditor,
+
+ /** @property */
+ events: {
+ "click": "enterEditMode"
+ },
+
+ /**
+ Renders a checkbox and check it if the model value of this column is true,
+ uncheck otherwise.
+ */
+ render: function () {
+ this.$el.empty();
+ var model = this.model, column = this.column;
+ var editable = Backgrid.callByNeed(column.editable(), column, model);
+ this.$el.append($("<input>", {
+ tabIndex: -1,
+ type: "checkbox",
+ checked: this.formatter.fromRaw(model.get(column.get("name")), model),
+ disabled: !editable
+ }));
+ this.delegateEvents();
+ return this;
+ }
+
+});
+
+/**
+ SelectCellEditor renders an HTML `<select>` fragment as the editor.
+
+ @class Backgrid.SelectCellEditor
+ @extends Backgrid.CellEditor
+*/
+var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({
+
+ /** @property */
+ tagName: "select",
+
+ /** @property */
+ events: {
+ "change": "save",
+ "blur": "close",
+ "keydown": "close"
+ },
+
+ /** @property {function(Object, ?Object=): string} template */
+ template: _.template('<option value="<%- value %>" <%= selected ? \'selected="selected"\' : "" %>><%- text %></option>', null, {variable: null}),
+
+ setOptionValues: function (optionValues) {
+ this.optionValues = optionValues;
+ this.optionValues = _.result(this, "optionValues");
+ },
+
+ setMultiple: function (multiple) {
+ this.multiple = multiple;
+ this.$el.prop("multiple", multiple);
+ },
+
+ _renderOptions: function (nvps, selectedValues) {
+ var options = '';
+ for (var i = 0; i < nvps.length; i++) {
+ options = options + this.template({
+ text: nvps[i][0],
+ value: nvps[i][1],
+ selected: _.indexOf(selectedValues, nvps[i][1]) > -1
+ });
+ }
+ return options;
+ },
+
+ /**
+ Renders the options if `optionValues` is a list of name-value pairs. The
+ options are contained inside option groups if `optionValues` is a list of
+ object hashes. The name is rendered at the option text and the value is the
+ option value. If `optionValues` is a function, it is called without a
+ parameter.
+ */
+ render: function () {
+ this.$el.empty();
+
+ var optionValues = _.result(this, "optionValues");
+ var model = this.model;
+ var selectedValues = this.formatter.fromRaw(model.get(this.column.get("name")), model);
+
+ if (!_.isArray(optionValues)) throw new TypeError("optionValues must be an array");
+
+ var optionValue = null;
+ var optionText = null;
+ var optionValue = null;
+ var optgroupName = null;
+ var optgroup = null;
+
+ for (var i = 0; i < optionValues.length; i++) {
+ var optionValue = optionValues[i];
+
+ if (_.isArray(optionValue)) {
+ optionText = optionValue[0];
+ optionValue = optionValue[1];
+
+ this.$el.append(this.template({
+ text: optionText,
+ value: optionValue,
+ selected: _.indexOf(selectedValues, optionValue) > -1
+ }));
+ }
+ else if (_.isObject(optionValue)) {
+ optgroupName = optionValue.name;
+ optgroup = $("<optgroup></optgroup>", { label: optgroupName });
+ optgroup.append(this._renderOptions.call(this, optionValue.values, selectedValues));
+ this.$el.append(optgroup);
+ }
+ else {
+ throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }");
+ }
+ }
+
+ this.delegateEvents();
+
+ return this;
+ },
+
+ /**
+ Saves the value of the selected option to the model attribute.
+ */
+ save: function (e) {
+ var model = this.model;
+ var column = this.column;
+ model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model));
+ },
+
+ /**
+ Triggers a `backgrid:edited` event from the model so the body can close
+ this editor.
+ */
+ close: function (e) {
+ var model = this.model;
+ var column = this.column;
+ var command = new Command(e);
+ if (command.cancel()) {
+ e.stopPropagation();
+ model.trigger("backgrid:edited", model, column, new Command(e));
+ }
+ else if (command.save() || command.moveLeft() || command.moveRight() ||
+ command.moveUp() || command.moveDown() || e.type == "blur") {
+ e.preventDefault();
+ e.stopPropagation();
+ this.save(e);
+ model.trigger("backgrid:edited", model, column, new Command(e));
+ }
+ }
+
+});
+
+/**
+ SelectCell is also a different kind of cell in that upon going into edit mode
+ the cell renders a list of options to pick from, as opposed to an input box.
+
+ SelectCell cannot be referenced by its string name when used in a column
+ definition because it requires an `optionValues` class attribute to be
+ defined. `optionValues` can either be a list of name-value pairs, to be
+ rendered as options, or a list of object hashes which consist of a key *name*
+ which is the option group name, and a key *values* which is a list of
+ name-value pairs to be rendered as options under that option group.
+
+ In addition, `optionValues` can also be a parameter-less function that
+ returns one of the above. If the options are static, it is recommended the
+ returned values to be memoized. `_.memoize()` is a good function to help with
+ that.
+
+ During display mode, the default formatter will normalize the raw model value
+ to an array of values whether the raw model value is a scalar or an
+ array. Each value is compared with the `optionValues` values using
+ Ecmascript's implicit type conversion rules. When exiting edit mode, no type
+ conversion is performed when saving into the model. This behavior is not
+ always desirable when the value type is anything other than string. To
+ control type conversion on the client-side, you should subclass SelectCell to
+ provide a custom formatter or provide the formatter to your column
+ definition.
+
+ See:
+ [$.fn.val()](http://api.jquery.com/val/)
+
+ @class Backgrid.SelectCell
+ @extends Backgrid.Cell
+*/
+var SelectCell = Backgrid.SelectCell = Cell.extend({
+
+ /** @property */
+ className: "select-cell",
+
+ /** @property */
+ editor: SelectCellEditor,
+
+ /** @property */
+ multiple: false,
+
+ /** @property */
+ formatter: SelectFormatter,
+
+ /**
+ @property {Array.<Array>|Array.<{name: string, values: Array.<Array>}>} optionValues
+ */
+ optionValues: undefined,
+
+ /** @property */
+ delimiter: ', ',
+
+ /**
+ Initializer.
+
+ @param {Object} options
+ @param {Backbone.Model} options.model
+ @param {Backgrid.Column} options.column
+
+ @throws {TypeError} If `optionsValues` is undefined.
+ */
+ initialize: function (options) {
+ SelectCell.__super__.initialize.apply(this, arguments);
+ this.listenTo(this.model, "backgrid:edit", function (model, column, cell, editor) {
+ if (column.get("name") == this.column.get("name")) {
+ editor.setOptionValues(this.optionValues);
+ editor.setMultiple(this.multiple);
+ }
+ });
+ },
+
+ /**
+ Renders the label using the raw value as key to look up from `optionValues`.
+
+ @throws {TypeError} If `optionValues` is malformed.
+ */
+ render: function () {
+ this.$el.empty();
+
+ var optionValues = _.result(this, "optionValues");
+ var model = this.model;
+ var rawData = this.formatter.fromRaw(model.get(this.column.get("name")), model);
+
+ var selectedText = [];
+
+ try {
+ if (!_.isArray(optionValues) || _.isEmpty(optionValues)) throw new TypeError;
+
+ for (var k = 0; k < rawData.length; k++) {
+ var rawDatum = rawData[k];
+
+ for (var i = 0; i < optionValues.length; i++) {
+ var optionValue = optionValues[i];
+
+ if (_.isArray(optionValue)) {
+ var optionText = optionValue[0];
+ var optionValue = optionValue[1];
+
+ if (optionValue == rawDatum) selectedText.push(optionText);
+ }
+ else if (_.isObject(optionValue)) {
+ var optionGroupValues = optionValue.values;
+
+ for (var j = 0; j < optionGroupValues.length; j++) {
+ var optionGroupValue = optionGroupValues[j];
+ if (optionGroupValue[1] == rawDatum) {
+ selectedText.push(optionGroupValue[0]);
+ }
+ }
+ }
+ else {
+ throw new TypeError;
+ }
+ }
+ }
+
+ this.$el.append(selectedText.join(this.delimiter));
+ }
+ catch (ex) {
+ if (ex instanceof TypeError) {
+ throw new TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}");
+ }
+ throw ex;
+ }
+
+ this.delegateEvents();
+
+ return this;
+ }
+
+});
+
+/*
+ backgrid
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT license.
+*/
+
+/**
+ A Column is a placeholder for column metadata.
+
+ You usually don't need to create an instance of this class yourself as a
+ collection of column instances will be created for you from a list of column
+ attributes in the Backgrid.js view class constructors.
+
+ @class Backgrid.Column
+ @extends Backbone.Model
+*/
+var Column = Backgrid.Column = Backbone.Model.extend({
+
+ /**
+ @cfg {Object} defaults Column defaults. To override any of these default
+ values, you can either change the prototype directly to override
+ Column.defaults globally or extend Column and supply the custom class to
+ Backgrid.Grid:
+
+ // Override Column defaults globally
+ Column.prototype.defaults.sortable = false;
+
+ // Override Column defaults locally
+ var MyColumn = Column.extend({
+ defaults: _.defaults({
+ editable: false
+ }, Column.prototype.defaults)
+ });
+
+ var grid = new Backgrid.Grid(columns: new Columns([{...}, {...}], {
+ model: MyColumn
+ }));
+
+ @cfg {string} [defaults.name] The default name of the model attribute.
+
+ @cfg {string} [defaults.label] The default label to show in the header.
+
+ @cfg {string|Backgrid.Cell} [defaults.cell] The default cell type. If this
+ is a string, the capitalized form will be used to look up a cell class in
+ Backbone, i.e.: string => StringCell. If a Cell subclass is supplied, it is
+ initialized with a hash of parameters. If a Cell instance is supplied, it
+ is used directly.
+
+ @cfg {string|Backgrid.HeaderCell} [defaults.headerCell] The default header
+ cell type.
+
+ @cfg {boolean|string|function(): boolean} [defaults.sortable=true] Whether
+ this column is sortable. If the value is a string, a method will the same
+ name will be looked up from the column instance to determine whether the
+ column should be sortable. The method's signature must be `function
+ (Backgrid.Column, Backbone.Model): boolean`.
+
+ @cfg {boolean|string|function(): boolean} [defaults.editable=true] Whether
+ this column is editable. If the value is a string, a method will the same
+ name will be looked up from the column instance to determine whether the
+ column should be editable. The method's signature must be `function
+ (Backgrid.Column, Backbone.Model): boolean`.
+
+ @cfg {boolean|string|function(): boolean} [defaults.renderable=true]
+ Whether this column is renderable. If the value is a string, a method will
+ the same name will be looked up from the column instance to determine
+ whether the column should be renderable. The method's signature must be
+ `function (Backrid.Column, Backbone.Model): boolean`.
+
+ @cfg {Backgrid.CellFormatter | Object | string} [defaults.formatter] The
+ formatter to use to convert between raw model values and user input.
+
+ @cfg {"toggle"|"cycle"} [defaults.sortType="cycle"] Whether sorting will
+ toggle between ascending and descending order, or cycle between insertion
+ order, ascending and descending order.
+
+ @cfg {(function(Backbone.Model, string): *) | string} [defaults.sortValue]
+ The function to use to extract a value from the model for comparison during
+ sorting. If this value is a string, a method with the same name will be
+ looked up from the column instance.
+
+ @cfg {"ascending"|"descending"|null} [defaults.direction=null] The initial
+ sorting direction for this column. The default is ordered by
+ Backbone.Model.cid, which usually means the collection is ordered by
+ insertion order.
+ */
+ defaults: {
+ name: undefined,
+ label: undefined,
+ sortable: true,
+ editable: true,
+ renderable: true,
+ formatter: undefined,
+ sortType: "cycle",
+ sortValue: undefined,
+ direction: null,
+ cell: undefined,
+ headerCell: undefined
+ },
+
+ /**
+ Initializes this Column instance.
+
+ @param {Object} attrs
+
+ @param {string} attrs.name The model attribute this column is responsible
+ for.
+
+ @param {string|Backgrid.Cell} attrs.cell The cell type to use to render
+ this column.
+
+ @param {string} [attrs.label]
+
+ @param {string|Backgrid.HeaderCell} [attrs.headerCell]
+
+ @param {boolean|string|function(): boolean} [attrs.sortable=true]
+
+ @param {boolean|string|function(): boolean} [attrs.editable=true]
+
+ @param {boolean|string|function(): boolean} [attrs.renderable=true]
+
+ @param {Backgrid.CellFormatter | Object | string} [attrs.formatter]
+
+ @param {"toggle"|"cycle"} [attrs.sortType="cycle"]
+
+ @param {(function(Backbone.Model, string): *) | string} [attrs.sortValue]
+
+ @throws {TypeError} If attrs.cell or attrs.options are not supplied.
+
+ @throws {ReferenceError} If formatter is a string but a formatter class of
+ said name cannot be found in the Backgrid module.
+
+ See:
+
+ - Backgrid.Column.defaults
+ - Backgrid.Cell
+ - Backgrid.CellFormatter
+ */
+ initialize: function () {
+ if (!this.has("label")) {
+ this.set({ label: this.get("name") }, { silent: true });
+ }
+
+ var headerCell = Backgrid.resolveNameToClass(this.get("headerCell"), "HeaderCell");
+
+ var cell = Backgrid.resolveNameToClass(this.get("cell"), "Cell");
+
+ this.set({cell: cell, headerCell: headerCell}, { silent: true });
+ },
+
+ /**
+ Returns an appropriate value extraction function from a model for sorting.
+
+ If the column model contains an attribute `sortValue`, if it is a string, a
+ method from the column instance identifified by the `sortValue` string is
+ returned. If it is a function, it it returned as is. If `sortValue` isn't
+ found from the column model's attributes, a default value extraction
+ function is returned which will compare according to the natural order of
+ the value's type.
+
+ @return {function(Backbone.Model, string): *}
+ */
+ sortValue: function () {
+ var sortValue = this.get("sortValue");
+ if (_.isString(sortValue)) return this[sortValue];
+ else if (_.isFunction(sortValue)) return sortValue;
+
+ return function (model, colName) {
+ return model.get(colName);
+ };
+ }
+
+ /**
+ @member Backgrid.Column
+ @protected
+ @method sortable
+ @return {function(Backgrid.Column, Backbone.Model): boolean | boolean}
+ */
+
+ /**
+ @member Backgrid.Column
+ @protected
+ @method editable
+ @return {function(Backgrid.Column, Backbone.Model): boolean | boolean}
+ */
+
+ /**
+ @member Backgrid.Column
+ @protected
+ @method renderable
+ @return {function(Backgrid.Column, Backbone.Model): boolean | boolean}
+ */
+});
+
+_.each(["sortable", "renderable", "editable"], function (key) {
+ Column.prototype[key] = function () {
+ var value = this.get(key);
+ if (_.isString(value)) return this[value];
+ else if (_.isFunction(value)) return value;
+
+ return !!value;
+ };
+});
+
+/**
+ A Backbone collection of Column instances.
+
+ @class Backgrid.Columns
+ @extends Backbone.Collection
+ */
+var Columns = Backgrid.Columns = Backbone.Collection.extend({
+
+ /**
+ @property {Backgrid.Column} model
+ */
+ model: Column
+});
+
+/*
+ backgrid
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT license.
+*/
+
+/**
+ Row is a simple container view that takes a model instance and a list of
+ column metadata describing how each of the model's attribute is to be
+ rendered, and apply the appropriate cell to each attribute.
+
+ @class Backgrid.Row
+ @extends Backbone.View
+*/
+var Row = Backgrid.Row = Backbone.View.extend({
+
+ /** @property */
+ tagName: "tr",
+
+ /**
+ Initializes a row view instance.
+
+ @param {Object} options
+ @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
+ @param {Backbone.Model} options.model The model instance to render.
+
+ @throws {TypeError} If options.columns or options.model is undefined.
+ */
+ initialize: function (options) {
+
+ var columns = this.columns = options.columns;
+ if (!(columns instanceof Backbone.Collection)) {
+ columns = this.columns = new Columns(columns);
+ }
+
+ var cells = this.cells = [];
+ for (var i = 0; i < columns.length; i++) {
+ cells.push(this.makeCell(columns.at(i), options));
+ }
+
+ this.listenTo(columns, "add", function (column, columns) {
+ var i = columns.indexOf(column);
+ var cell = this.makeCell(column, options);
+ cells.splice(i, 0, cell);
+
+ var $el = this.$el;
+ if (i === 0) {
+ $el.prepend(cell.render().$el);
+ }
+ else if (i === columns.length - 1) {
+ $el.append(cell.render().$el);
+ }
+ else {
+ $el.children().eq(i).before(cell.render().$el);
+ }
+ });
+
+ this.listenTo(columns, "remove", function (column, columns, opts) {
+ cells[opts.index].remove();
+ cells.splice(opts.index, 1);
+ });
+ },
+
+ /**
+ Factory method for making a cell. Used by #initialize internally. Override
+ this to provide an appropriate cell instance for a custom Row subclass.
+
+ @protected
+
+ @param {Backgrid.Column} column
+ @param {Object} options The options passed to #initialize.
+
+ @return {Backgrid.Cell}
+ */
+ makeCell: function (column) {
+ return new (column.get("cell"))({
+ column: column,
+ model: this.model
+ });
+ },
+
+ /**
+ Renders a row of cells for this row's model.
+ */
+ render: function () {
+ this.$el.empty();
+
+ var fragment = document.createDocumentFragment();
+ for (var i = 0; i < this.cells.length; i++) {
+ fragment.appendChild(this.cells[i].render().el);
+ }
+
+ this.el.appendChild(fragment);
+
+ this.delegateEvents();
+
+ return this;
+ },
+
+ /**
+ Clean up this row and its cells.
+
+ @chainable
+ */
+ remove: function () {
+ for (var i = 0; i < this.cells.length; i++) {
+ var cell = this.cells[i];
+ cell.remove.apply(cell, arguments);
+ }
+ return Backbone.View.prototype.remove.apply(this, arguments);
+ }
+
+});
+
+/**
+ EmptyRow is a simple container view that takes a list of column and render a
+ row with a single column.
+
+ @class Backgrid.EmptyRow
+ @extends Backbone.View
+*/
+var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({
+
+ /** @property */
+ tagName: "tr",
+
+ /** @property {string|function(): string} */
+ emptyText: null,
+
+ /**
+ Initializer.
+
+ @param {Object} options
+ @param {string|function(): string} options.emptyText
+ @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
+ */
+ initialize: function (options) {
+ this.emptyText = options.emptyText;
+ this.columns = options.columns;
+ },
+
+ /**
+ Renders an empty row.
+ */
+ render: function () {
+ this.$el.empty();
+
+ var td = document.createElement("td");
+ td.setAttribute("colspan", this.columns.length);
+ td.appendChild(document.createTextNode(_.result(this, "emptyText")));
+
+ this.el.className = "empty";
+ this.el.appendChild(td);
+
+ return this;
+ }
+});
+
+/*
+ backgrid
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT license.
+*/
+
+/**
+ HeaderCell is a special cell class that renders a column header cell. If the
+ column is sortable, a sorter is also rendered and will trigger a table
+ refresh after sorting.
+
+ @class Backgrid.HeaderCell
+ @extends Backbone.View
+ */
+var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
+
+ /** @property */
+ tagName: "th",
+
+ /** @property */
+ events: {
+ "click a": "onClick"
+ },
+
+ /**
+ Initializer.
+
+ @param {Object} options
+ @param {Backgrid.Column|Object} options.column
+
+ @throws {TypeError} If options.column or options.collection is undefined.
+ */
+ initialize: function (options) {
+ this.column = options.column;
+ if (!(this.column instanceof Column)) {
+ this.column = new Column(this.column);
+ }
+
+ var column = this.column, collection = this.collection, $el = this.$el;
+
+ this.listenTo(column, "change:editable change:sortable change:renderable",
+ function (column) {
+ var changed = column.changedAttributes();
+ for (var key in changed) {
+ if (changed.hasOwnProperty(key)) {
+ $el.toggleClass(key, changed[key]);
+ }
+ }
+ });
+ this.listenTo(column, "change:direction", this.setCellDirection);
+ this.listenTo(column, "change:name change:label", this.render);
+
+ if (Backgrid.callByNeed(column.editable(), column, collection)) $el.addClass("editable");
+ if (Backgrid.callByNeed(column.sortable(), column, collection)) $el.addClass("sortable");
+ if (Backgrid.callByNeed(column.renderable(), column, collection)) $el.addClass("renderable");
+
+ this.listenTo(collection.fullCollection || collection, "sort", this.removeCellDirection);
+ },
+
+ /**
+ Event handler for the collection's `sort` event. Removes all the CSS
+ direction classes.
+ */
+ removeCellDirection: function () {
+ this.$el.removeClass("ascending").removeClass("descending");
+ this.column.set("direction", null);
+ },
+
+ /**
+ Event handler for the column's `change:direction` event. If this
+ HeaderCell's column is being sorted on, it applies the direction given as a
+ CSS class to the header cell. Removes all the CSS direction classes
+ otherwise.
+ */
+ setCellDirection: function (column, direction) {
+ this.$el.removeClass("ascending").removeClass("descending");
+ if (column.cid == this.column.cid) this.$el.addClass(direction);
+ },
+
+ /**
+ Event handler for the `click` event on the cell's anchor. If the column is
+ sortable, clicking on the anchor will cycle through 3 sorting orderings -
+ `ascending`, `descending`, and default.
+ */
+ onClick: function (e) {
+ e.preventDefault();
+
+ var column = this.column;
+ var collection = this.collection;
+ var event = "backgrid:sort";
+
+ function cycleSort(header, col) {
+ if (column.get("direction") === "ascending") collection.trigger(event, col, "descending");
+ else if (column.get("direction") === "descending") collection.trigger(event, col, null);
+ else collection.trigger(event, col, "ascending");
+ }
+
+ function toggleSort(header, col) {
+ if (column.get("direction") === "ascending") collection.trigger(event, col, "descending");
+ else collection.trigger(event, col, "ascending");
+ }
+
+ var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection);
+ if (sortable) {
+ var sortType = column.get("sortType");
+ if (sortType === "toggle") toggleSort(this, column);
+ else cycleSort(this, column);
+ }
+ },
+
+ /**
+ Renders a header cell with a sorter, a label, and a class name for this
+ column.
+ */
+ render: function () {
+ this.$el.empty();
+ var column = this.column;
+ var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection);
+ var label;
+ if(sortable){
+ label = $("<a>").text(column.get("label")).append("<b class='sort-caret'></b>");
+ } else {
+ label = document.createTextNode(column.get("label"));
+ }
+
+ this.$el.append(label);
+ this.$el.addClass(column.get("name"));
+ this.$el.addClass(column.get("direction"));
+ this.delegateEvents();
+ return this;
+ }
+
+});
+
+/**
+ HeaderRow is a controller for a row of header cells.
+
+ @class Backgrid.HeaderRow
+ @extends Backgrid.Row
+ */
+var HeaderRow = Backgrid.HeaderRow = Backgrid.Row.extend({
+
+ requiredOptions: ["columns", "collection"],
+
+ /**
+ Initializer.
+
+ @param {Object} options
+ @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
+ @param {Backgrid.HeaderCell} [options.headerCell] Customized default
+ HeaderCell for all the columns. Supply a HeaderCell class or instance to a
+ the `headerCell` key in a column definition for column-specific header
+ rendering.
+
+ @throws {TypeError} If options.columns or options.collection is undefined.
+ */
+ initialize: function () {
+ Backgrid.Row.prototype.initialize.apply(this, arguments);
+ },
+
+ makeCell: function (column, options) {
+ var headerCell = column.get("headerCell") || options.headerCell || HeaderCell;
+ headerCell = new headerCell({
+ column: column,
+ collection: this.collection
+ });
+ return headerCell;
+ }
+
+});
+
+/**
+ Header is a special structural view class that renders a table head with a
+ single row of header cells.
+
+ @class Backgrid.Header
+ @extends Backbone.View
+ */
+var Header = Backgrid.Header = Backbone.View.extend({
+
+ /** @property */
+ tagName: "thead",
+
+ /**
+ Initializer. Initializes this table head view to contain a single header
+ row view.
+
+ @param {Object} options
+ @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
+ @param {Backbone.Model} options.model The model instance to render.
+
+ @throws {TypeError} If options.columns or options.model is undefined.
+ */
+ initialize: function (options) {
+ this.columns = options.columns;
+ if (!(this.columns instanceof Backbone.Collection)) {
+ this.columns = new Columns(this.columns);
+ }
+
+ this.row = new Backgrid.HeaderRow({
+ columns: this.columns,
+ collection: this.collection
+ });
+ },
+
+ /**
+ Renders this table head with a single row of header cells.
+ */
+ render: function () {
+ this.$el.append(this.row.render().$el);
+ this.delegateEvents();
+ return this;
+ },
+
+ /**
+ Clean up this header and its row.
+
+ @chainable
+ */
+ remove: function () {
+ this.row.remove.apply(this.row, arguments);
+ return Backbone.View.prototype.remove.apply(this, arguments);
+ }
+
+});
+
+/*
+ backgrid
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT license.
+*/
+
+/**
+ Body is the table body which contains the rows inside a table. Body is
+ responsible for refreshing the rows after sorting, insertion and removal.
+
+ @class Backgrid.Body
+ @extends Backbone.View
+*/
+var Body = Backgrid.Body = Backbone.View.extend({
+
+ /** @property */
+ tagName: "tbody",
+
+ /**
+ Initializer.
+
+ @param {Object} options
+ @param {Backbone.Collection} options.collection
+ @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
+ Column metadata.
+ @param {Backgrid.Row} [options.row=Backgrid.Row] The Row class to use.
+ @param {string|function(): string} [options.emptyText] The text to display in the empty row.
+
+ @throws {TypeError} If options.columns or options.collection is undefined.
+
+ See Backgrid.Row.
+ */
+ initialize: function (options) {
+
+ this.columns = options.columns;
+ if (!(this.columns instanceof Backbone.Collection)) {
+ this.columns = new Columns(this.columns);
+ }
+
+ this.row = options.row || Row;
+ this.rows = this.collection.map(function (model) {
+ var row = new this.row({
+ columns: this.columns,
+ model: model
+ });
+
+ return row;
+ }, this);
+
+ this.emptyText = options.emptyText;
+ this._unshiftEmptyRowMayBe();
+
+ var collection = this.collection;
+ this.listenTo(collection, "add", this.insertRow);
+ this.listenTo(collection, "remove", this.removeRow);
+ this.listenTo(collection, "sort", this.refresh);
+ this.listenTo(collection, "reset", this.refresh);
+ this.listenTo(collection, "backgrid:sort", this.sort);
+ this.listenTo(collection, "backgrid:edited", this.moveToNextCell);
+ },
+
+ _unshiftEmptyRowMayBe: function () {
+ if (this.rows.length === 0 && this.emptyText != null) {
+ this.rows.unshift(new EmptyRow({
+ emptyText: this.emptyText,
+ columns: this.columns
+ }));
+ }
+ },
+
+ /**
+ This method can be called either directly or as a callback to a
+ [Backbone.Collecton#add](http://backbonejs.org/#Collection-add) event.
+
+ When called directly, it accepts a model or an array of models and an
+ option hash just like
+ [Backbone.Collection#add](http://backbonejs.org/#Collection-add) and
+ delegates to it. Once the model is added, a new row is inserted into the
+ body and automatically rendered.
+
+ When called as a callback of an `add` event, splices a new row into the
+ body and renders it.
+
+ @param {Backbone.Model} model The model to render as a row.
+ @param {Backbone.Collection} collection When called directly, this
+ parameter is actually the options to
+ [Backbone.Collection#add](http://backbonejs.org/#Collection-add).
+ @param {Object} options When called directly, this must be null.
+
+ See:
+
+ - [Backbone.Collection#add](http://backbonejs.org/#Collection-add)
+ */
+ insertRow: function (model, collection, options) {
+
+ if (this.rows[0] instanceof EmptyRow) this.rows.pop().remove();
+
+ // insertRow() is called directly
+ if (!(collection instanceof Backbone.Collection) && !options) {
+ this.collection.add(model, (options = collection));
+ return;
+ }
+
+ var row = new this.row({
+ columns: this.columns,
+ model: model
+ });
+
+ var index = collection.indexOf(model);
+ this.rows.splice(index, 0, row);
+
+ var $el = this.$el;
+ var $children = $el.children();
+ var $rowEl = row.render().$el;
+
+ if (index >= $children.length) {
+ $el.append($rowEl);
+ }
+ else {
+ $children.eq(index).before($rowEl);
+ }
+
+ return this;
+ },
+
+ /**
+ The method can be called either directly or as a callback to a
+ [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
+ event.
+
+ When called directly, it accepts a model or an array of models and an
+ option hash just like
+ [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) and
+ delegates to it. Once the model is removed, a corresponding row is removed
+ from the body.
+
+ When called as a callback of a `remove` event, splices into the rows and
+ removes the row responsible for rendering the model.
+
+ @param {Backbone.Model} model The model to remove from the body.
+ @param {Backbone.Collection} collection When called directly, this
+ parameter is actually the options to
+ [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove).
+ @param {Object} options When called directly, this must be null.
+
+ See:
+
+ - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
+ */
+ removeRow: function (model, collection, options) {
+
+ // removeRow() is called directly
+ if (!options) {
+ this.collection.remove(model, (options = collection));
+ this._unshiftEmptyRowMayBe();
+ return;
+ }
+
+ if (_.isUndefined(options.render) || options.render) {
+ this.rows[options.index].remove();
+ }
+
+ this.rows.splice(options.index, 1);
+ this._unshiftEmptyRowMayBe();
+
+ return this;
+ },
+
+ /**
+ Reinitialize all the rows inside the body and re-render them. Triggers a
+ Backbone `backgrid:refresh` event from the collection along with the body
+ instance as its sole parameter when done.
+ */
+ refresh: function () {
+ for (var i = 0; i < this.rows.length; i++) {
+ this.rows[i].remove();
+ }
+
+ this.rows = this.collection.map(function (model) {
+ var row = new this.row({
+ columns: this.columns,
+ model: model
+ });
+
+ return row;
+ }, this);
+ this._unshiftEmptyRowMayBe();
+
+ this.render();
+
+ this.collection.trigger("backgrid:refresh", this);
+
+ return this;
+ },
+
+ /**
+ Renders all the rows inside this body. If the collection is empty and
+ `options.emptyText` is defined and not null in the constructor, an empty
+ row is rendered, otherwise no row is rendered.
+ */
+ render: function () {
+ this.$el.empty();
+
+ var fragment = document.createDocumentFragment();
+ for (var i = 0; i < this.rows.length; i++) {
+ var row = this.rows[i];
+ fragment.appendChild(row.render().el);
+ }
+
+ this.el.appendChild(fragment);
+
+ this.delegateEvents();
+
+ return this;
+ },
+
+ /**
+ Clean up this body and it's rows.
+
+ @chainable
+ */
+ remove: function () {
+ for (var i = 0; i < this.rows.length; i++) {
+ var row = this.rows[i];
+ row.remove.apply(row, arguments);
+ }
+ return Backbone.View.prototype.remove.apply(this, arguments);
+ },
+
+ /**
+ If the underlying collection is a Backbone.PageableCollection in
+ server-mode or infinite-mode, a page of models is fetched after sorting is
+ done on the server.
+
+ If the underlying collection is a Backbone.PageableCollection in
+ client-mode, or any
+ [Backbone.Collection](http://backbonejs.org/#Collection) instance, sorting
+ is done on the client side. If the collection is an instance of a
+ Backbone.PageableCollection, sorting will be done globally on all the pages
+ and the current page will then be returned.
+
+ Triggers a Backbone `backgrid:sorted` event from the collection when done
+ with the column, direction and a reference to the collection.
+
+ @param {Backgrid.Column} column
+ @param {null|"ascending"|"descending"} direction
+
+ See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator)
+ */
+ sort: function (column, direction) {
+
+ if (!_.contains(["ascending", "descending", null], direction)) {
+ throw new RangeError('direction must be one of "ascending", "descending" or `null`');
+ }
+
+ if (_.isString(column)) column = this.columns.findWhere({name: column});
+
+ var collection = this.collection;
+
+ var order;
+ if (direction === "ascending") order = -1;
+ else if (direction === "descending") order = 1;
+ else order = null;
+
+ var comparator = this.makeComparator(column.get("name"), order,
+ order ?
+ column.sortValue() :
+ function (model) {
+ return model.cid.replace('c', '') * 1;
+ });
+
+ if (Backbone.PageableCollection &&
+ collection instanceof Backbone.PageableCollection) {
+
+ collection.setSorting(order && column.get("name"), order,
+ {sortValue: column.sortValue()});
+
+ if (collection.fullCollection) {
+ // If order is null, pageable will remove the comparator on both sides,
+ // in this case the default insertion order comparator needs to be
+ // attached to get back to the order before sorting.
+ if (collection.fullCollection.comparator == null) {
+ collection.fullCollection.comparator = comparator;
+ }
+ collection.fullCollection.sort();
+ collection.trigger("backgrid:sorted", column, direction, collection);
+ }
+ else collection.fetch({reset: true, success: function () {
+ collection.trigger("backgrid:sorted", column, direction, collection);
+ }});
+ }
+ else {
+ collection.comparator = comparator;
+ collection.sort();
+ collection.trigger("backgrid:sorted", column, direction, collection);
+ }
+
+ column.set("direction", direction);
+
+ return this;
+ },
+
+ makeComparator: function (attr, order, func) {
+
+ return function (left, right) {
+ // extract the values from the models
+ var l = func(left, attr), r = func(right, attr), t;
+
+ // if descending order, swap left and right
+ if (order === 1) t = l, l = r, r = t;
+
+ // compare as usual
+ if (l === r) return 0;
+ else if (l < r) return -1;
+ return 1;
+ };
+ },
+
+ /**
+ Moves focus to the next renderable and editable cell and return the
+ currently editing cell to display mode.
+
+ Triggers a `backgrid:next` event on the model with the indices of the row
+ and column the user *intended* to move to, and whether the intended move
+ was going to go out of bounds. Note that *out of bound* always means an
+ attempt to go past the end of the last row.
+
+ @param {Backbone.Model} model The originating model
+ @param {Backgrid.Column} column The originating model column
+ @param {Backgrid.Command} command The Command object constructed from a DOM
+ event
+ */
+ moveToNextCell: function (model, column, command) {
+ var i = this.collection.indexOf(model);
+ var j = this.columns.indexOf(column);
+ var cell, renderable, editable, m, n;
+
+ this.rows[i].cells[j].exitEditMode();
+
+ if (command.moveUp() || command.moveDown() || command.moveLeft() ||
+ command.moveRight() || command.save()) {
+ var l = this.columns.length;
+ var maxOffset = l * this.collection.length;
+
+ if (command.moveUp() || command.moveDown()) {
+ m = i + (command.moveUp() ? -1 : 1);
+ var row = this.rows[m];
+ if (row) {
+ cell = row.cells[j];
+ if (Backgrid.callByNeed(cell.column.editable(), cell.column, model)) {
+ cell.enterEditMode();
+ model.trigger("backgrid:next", m, j, false);
+ }
+ }
+ else model.trigger("backgrid:next", m, j, true);
+ }
+ else if (command.moveLeft() || command.moveRight()) {
+ var right = command.moveRight();
+ for (var offset = i * l + j + (right ? 1 : -1);
+ offset >= 0 && offset < maxOffset;
+ right ? offset++ : offset--) {
+ m = ~~(offset / l);
+ n = offset - m * l;
+ cell = this.rows[m].cells[n];
+ renderable = Backgrid.callByNeed(cell.column.renderable(), cell.column, cell.model);
+ editable = Backgrid.callByNeed(cell.column.editable(), cell.column, model);
+ if (renderable && editable) {
+ cell.enterEditMode();
+ model.trigger("backgrid:next", m, n, false);
+ break;
+ }
+ }
+
+ if (offset == maxOffset) {
+ model.trigger("backgrid:next", ~~(offset / l), offset - m * l, true);
+ }
+ }
+ }
+
+ return this;
+ }
+});
+
+/*
+ backgrid
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT license.
+*/
+
+/**
+ A Footer is a generic class that only defines a default tag `tfoot` and
+ number of required parameters in the initializer.
+
+ @abstract
+ @class Backgrid.Footer
+ @extends Backbone.View
+ */
+var Footer = Backgrid.Footer = Backbone.View.extend({
+
+ /** @property */
+ tagName: "tfoot",
+
+ /**
+ Initializer.
+
+ @param {Object} options
+ @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
+ Column metadata.
+ @param {Backbone.Collection} options.collection
+
+ @throws {TypeError} If options.columns or options.collection is undefined.
+ */
+ initialize: function (options) {
+ this.columns = options.columns;
+ if (!(this.columns instanceof Backbone.Collection)) {
+ this.columns = new Backgrid.Columns(this.columns);
+ }
+ }
+
+});
+
+/*
+ backgrid
+ http://github.com/wyuenho/backgrid
+
+ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+ Licensed under the MIT license.
+*/
+
+/**
+ Grid represents a data grid that has a header, body and an optional footer.
+
+ By default, a Grid treats each model in a collection as a row, and each
+ attribute in a model as a column. To render a grid you must provide a list of
+ column metadata and a collection to the Grid constructor. Just like any
+ Backbone.View class, the grid is rendered as a DOM node fragment when you
+ call render().
+
+ var grid = Backgrid.Grid({
+ columns: [{ name: "id", label: "ID", type: "string" },
+ // ...
+ ],
+ collections: books
+ });
+
+ $("#table-container").append(grid.render().el);
+
+ Optionally, if you want to customize the rendering of the grid's header and
+ footer, you may choose to extend Backgrid.Header and Backgrid.Footer, and
+ then supply that class or an instance of that class to the Grid constructor.
+ See the documentation for Header and Footer for further details.
+
+ var grid = Backgrid.Grid({
+ columns: [{ name: "id", label: "ID", type: "string" }],
+ collections: books,
+ header: Backgrid.Header.extend({
+ //...
+ }),
+ footer: Backgrid.Paginator
+ });
+
+ Finally, if you want to override how the rows are rendered in the table body,
+ you can supply a Body subclass as the `body` attribute that uses a different
+ Row class.
+
+ @class Backgrid.Grid
+ @extends Backbone.View
+
+ See:
+
+ - Backgrid.Column
+ - Backgrid.Header
+ - Backgrid.Body
+ - Backgrid.Row
+ - Backgrid.Footer
+*/
+var Grid = Backgrid.Grid = Backbone.View.extend({
+
+ /** @property */
+ tagName: "table",
+
+ /** @property */
+ className: "backgrid",
+
+ /** @property */
+ header: Header,
+
+ /** @property */
+ body: Body,
+
+ /** @property */
+ footer: null,
+
+ /**
+ Initializes a Grid instance.
+
+ @param {Object} options
+ @param {Backbone.Collection.<Backgrid.Columns>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
+ @param {Backbone.Collection} options.collection The collection of tabular model data to display.
+ @param {Backgrid.Header} [options.header=Backgrid.Header] An optional Header class to override the default.
+ @param {Backgrid.Body} [options.body=Backgrid.Body] An optional Body class to override the default.
+ @param {Backgrid.Row} [options.row=Backgrid.Row] An optional Row class to override the default.
+ @param {Backgrid.Footer} [options.footer=Backgrid.Footer] An optional Footer class.
+ */
+ initialize: function (options) {
+ // Convert the list of column objects here first so the subviews don't have
+ // to.
+ if (!(options.columns instanceof Backbone.Collection)) {
+ options.columns = new Columns(options.columns);
+ }
+ this.columns = options.columns;
+
+ var filteredOptions = _.omit(options, ["el", "id", "attributes",
+ "className", "tagName", "events"]);
+
+ // must construct body first so it listens to backgrid:sort first
+ this.body = options.body || this.body;
+ this.body = new this.body(filteredOptions);
+
+ this.header = options.header || this.header;
+ if (this.header) {
+ this.header = new this.header(filteredOptions);
+ }
+
+ this.footer = options.footer || this.footer;
+ if (this.footer) {
+ this.footer = new this.footer(filteredOptions);
+ }
+
+ this.listenTo(this.columns, "reset", function () {
+ if (this.header) {
+ this.header = new (this.header.remove().constructor)(filteredOptions);
+ }
+ this.body = new (this.body.remove().constructor)(filteredOptions);
+ if (this.footer) {
+ this.footer = new (this.footer.remove().constructor)(filteredOptions);
+ }
+ this.render();
+ });
+ },
+
+ /**
+ Delegates to Backgrid.Body#insertRow.
+ */
+ insertRow: function () {
+ this.body.insertRow.apply(this.body, arguments);
+ return this;
+ },
+
+ /**
+ Delegates to Backgrid.Body#removeRow.
+ */
+ removeRow: function () {
+ this.body.removeRow.apply(this.body, arguments);
+ return this;
+ },
+
+ /**
+ Delegates to Backgrid.Columns#add for adding a column. Subviews can listen
+ to the `add` event from their internal `columns` if rerendering needs to
+ happen.
+
+ @param {Object} [options] Options for `Backgrid.Columns#add`.
+ */
+ insertColumn: function () {
+ this.columns.add.apply(this.columns, arguments);
+ return this;
+ },
+
+ /**
+ Delegates to Backgrid.Columns#remove for removing a column. Subviews can
+ listen to the `remove` event from the internal `columns` if rerendering
+ needs to happen.
+
+ @param {Object} [options] Options for `Backgrid.Columns#remove`.
+ */
+ removeColumn: function () {
+ this.columns.remove.apply(this.columns, arguments);
+ return this;
+ },
+
+ /**
+ Delegates to Backgrid.Body#sort.
+ */
+ sort: function () {
+ this.body.sort.apply(this.body, arguments);
+ return this;
+ },
+
+ /**
+ Renders the grid's header, then footer, then finally the body. Triggers a
+ Backbone `backgrid:rendered` event along with a reference to the grid when
+ the it has successfully been rendered.
+ */
+ render: function () {
+ this.$el.empty();
+
+ if (this.header) {
+ this.$el.append(this.header.render().$el);
+ }
+
+ if (this.footer) {
+ this.$el.append(this.footer.render().$el);
+ }
+
+ this.$el.append(this.body.render().$el);
+
+ this.delegateEvents();
+
+ this.trigger("backgrid:rendered", this);
+
+ return this;
+ },
+
+ /**
+ Clean up this grid and its subviews.
+
+ @chainable
+ */
+ remove: function () {
+ this.header && this.header.remove.apply(this.header, arguments);
+ this.body.remove.apply(this.body, arguments);
+ this.footer && this.footer.remove.apply(this.footer, arguments);
+ return Backbone.View.prototype.remove.apply(this, arguments);
+ }
+
+});
+return Backgrid;
+}));
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/backgrid-0.3.5.min.js b/htrace-core/src/web/lib/js/backgrid-0.3.5.min.js
deleted file mode 100644
index 4938964..0000000
--- a/htrace-core/src/web/lib/js/backgrid-0.3.5.min.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/*!
- backgrid
- http://github.com/wyuenho/backgrid
-
- Copyright (c) 2014 Jimmy Yuen Ho Wong and contributors <wyuenho@gmail.com>
- Licensed under the MIT license.
-*/
-!function(a){"object"==typeof exports?module.exports=a(module.exports,require("underscore"),require("backbone")):a(this,this._,this.Backbone)}(function(a,b,c){"use strict";function d(a,b,c){var d=b-(a+"").length;d=0>d?0:d;for(var e="",f=0;d>f;f++)e+=c;return e+a}var e=" \n\f\r \u2028\u2029";if(!String.prototype.trim||e.trim()){e="["+e+"]";var f=new RegExp("^"+e+e+"*"),g=new RegExp(e+e+"*$");String.prototype.trim=function(){if(void 0===this||null===this)throw new TypeError("can't convert "+this+" to object");return String(this).replace(f,"").replace(g,"")}}var h=c.$,i=a.Backgrid={Extension:{},resolveNameToClass:function(a,c){if(b.isString(a)){var d=b.map(a.split("-"),function(a){return a.slice(0,1).toUpperCase()+a.slice(1)}).join("")+c,e=i[d]||i.Extension[d];if(b.isUndefined(e))throw new ReferenceError("Class '"+d+"' not found");return e}return a},callByNeed:function(){var a=arguments[0];if(!b.isFunction(a))return a;var c=arguments[1],d=[].slice.call(arguments,2);return a.apply(c,d+""?d:[])}};b.extend(i,c.Events);var j=i.Command=function(a){b.extend(this,{altKey:!!a.altKey,"char":a["char"],charCode:a.charCode,ctrlKey:!!a.ctrlKey,key:a.key,keyCode:a.keyCode,locale:a.locale,location:a.location,metaKey:!!a.metaKey,repeat:!!a.repeat,shiftKey:!!a.shiftKey,which:a.which})};b.extend(j.prototype,{moveUp:function(){return 38==this.keyCode},moveDown:function(){return 40===this.keyCode},moveLeft:function(){return this.shiftKey&&9===this.keyCode},moveRight:function(){return!this.shiftKey&&9===this.keyCode},save:function(){return 13===this.keyCode},cancel:function(){return 27===this.keyCode},passThru:function(){return!(this.moveUp()||this.moveDown()||this.moveLeft()||this.moveRight()||this.save()||this.cancel())}});var k=i.CellFormatter=function(){};b.extend(k.prototype,{fromRaw:function(a){return a},toRaw:function(a){return a}});var l=i.NumberFormatter=function(a){if(b.extend(this,this.defaults,a||{}),this.decimals<0||this.decimals>20)throw new RangeError("decimals must be between 0 and 20")};l.prototype=new k,b.extend(l.prototype,{defaults:{decimals:2,decimalSeparator:".",orderSeparator:","},HUMANIZED_NUM_RE:/(\d)(?=(?:\d{3})+$)/g,fromRaw:function(a){if(b.isNull(a)||b.isUndefined(a))return"";a=a.toFixed(~~this.decimals);var c=a.split("."),d=c[0],e=c[1]?(this.decimalSeparator||".")+c[1]:"";return d.replace(this.HUMANIZED_NUM_RE,"$1"+this.orderSeparator)+e},toRaw:function(a){if(a=a.trim(),""===a)return null;for(var c="",d=a.split(this.orderSeparator),e=0;e<d.length;e++)c+=d[e];var f=c.split(this.decimalSeparator);c="";for(var e=0;e<f.length;e++)c=c+f[e]+".";"."===c[c.length-1]&&(c=c.slice(0,c.length-1));var g=1*(1*c).toFixed(~~this.decimals);return b.isNumber(g)&&!b.isNaN(g)?g:void 0}});var m=i.PercentFormatter=function(){i.NumberFormatter.apply(this,arguments)};m.prototype=new i.NumberFormatter,b.extend(m.prototype,{defaults:b.extend({},l.prototype.defaults,{multiplier:1,symbol:"%"}),fromRaw:function(a){var b=[].slice.call(arguments,1);return b.unshift(a*this.multiplier),(l.prototype.fromRaw.apply(this,b)||"0")+this.symbol},toRaw:function(a){var c=a.split(this.symbol);if(c&&c[0]&&""===c[1]||null==c[1]){var d=l.prototype.toRaw.call(this,c[0]);return b.isUndefined(d)?d:d/this.multiplier}}});var n=i.DatetimeFormatter=function(a){if(b.extend(this,this.defaults,a||{}),!this.includeDate&&!this.includeTime)throw new Error("Either includeDate or includeTime must be true")};n.prototype=new k,b.extend(n.prototype,{defaults:{includeDate:!0,includeTime:!0,includeMilli:!1},DATE_RE:/^([+\-]?\d{4})-(\d{2})-(\d{2})$/,TIME_RE:/^(\d{2}):(\d{2}):(\d{2})(\.(\d{3}))?$/,ISO_SPLITTER_RE:/T|Z| +/,_convert:function(a,c){if(""===(a+"").trim())return null;var e,f=null;if(b.isNumber(a)){var g=new Date(a);e=d(g.getUTCFullYear(),4,0)+"-"+d(g.getUTCMonth()+1,2,0)+"-"+d(g.getUTCDate(),2,0),f=d(g.getUTCHours(),2,0)+":"+d(g.getUTCMinutes(),2,0)+":"+d(g.getUTCSeconds(),2,0)}else{a=a.trim();var h=a.split(this.ISO_SPLITTER_RE)||[];e=this.DATE_RE.test(h[0])?h[0]:"",f=e&&h[1]?h[1]:this.TIME_RE.test(h[0])?h[0]:""}var i=this.DATE_RE.exec(e)||[],j=this.TIME_RE.exec(f)||[];if(c){if(this.includeDate&&b.isUndefined(i[0]))return;if(this.includeTime&&b.isUndefined(j[0]))return;if(!this.includeDate&&e)return;if(!this.includeTime&&f)return}var g=new Date(Date.UTC(1*i[1]||0,1*i[2]-1||0,1*i[3]||0,1*j[1]||null,1*j[2]||null,1*j[3]||null,1*j[5]||null)),k="";return this.includeDate&&(k=d(g.getUTCFullYear(),4,0)+"-"+d(g.getUTCMonth()+1,2,0)+"-"+d(g.getUTCDate(),2,0)),this.includeTime&&(k=k+(this.includeDate?"T":"")+d(g.getUTCHours(),2,0)+":"+d(g.getUTCMinutes(),2,0)+":"+d(g.getUTCSeconds(),2,0),this.includeMilli&&(k=k+"."+d(g.getUTCMilliseconds(),3,0))),this.includeDate&&this.includeTime&&(k+="Z"),k},fromRaw:function(a){return b.isNull(a)||b.isUndefined(a)?"":this._convert(a)},toRaw:function(a){return this._convert(a,!0)}});var o=i.StringFormatter=function(){};o.prototype=new k,b.extend(o.prototype,{fromRaw:function(a){return b.isUndefined(a)||b.isNull(a)?"":a+""}});var p=i.EmailFormatter=function(){};p.prototype=new k,b.extend(p.prototype,{toRaw:function(a){var c=a.trim().split("@");return 2===c.length&&b.all(c)?a:void 0}});var q=i.SelectFormatter=function(){};q.prototype=new k,b.extend(q.prototype,{fromRaw:function(a){return b.isArray(a)?a:null!=a?[a]:[]}});var r=i.CellEditor=c.View.extend({initialize:function(a){this.formatter=a.formatter,this.column=a.column,this.column instanceof C||(this.column=new C(this.column)),this.listenTo(this.model,"backgrid:editing",this.postRender)},postRender:function(a,b){return(null==b||b.get("name")==this.column.get("name"))&&this.$el.focus(),this}}),s=i.InputCellEditor=r.extend({tagName:"input",attributes:{type:"text"},events:{blur:"saveOrCancel",keydown:"saveOrCancel"},initialize:function(a){s.__super__.initialize.apply(this,arguments),a.placeholder&&this.$el.attr("placeholder",a.placeholder)},render:function(){var a=this.model;return this.$el.val(this.formatter.fromRaw(a.get(this.column.get("name")),a)),this},saveOrCancel:function(a){var c=this.formatter,d=this.model,e=this.column,f=new j(a),g="blur"===a.type;if(f.moveUp()||f.moveDown()||f.moveLeft()||f.moveRight()||f.save()||g){a.preventDefault(),a.stopPropagation();var h=this.$el.val(),i=c.toRaw(h,d);b.isUndefined(i)?d.trigger("backgrid:error",d,e,h):(d.set(e.get("name"),i),d.trigger("backgrid:edited",d,e,f))}else f.cancel()&&(a.stopPropagation(),d.trigger("backgrid:edited",d,e,f))},postRender:function(a,b){if(null==b||b.get("name")==this.column.get("name"))if("right"===this.$el.css("text-align")){var c=this.$el.val();this.$el.focus().val(null).val(c)}else this.$el.focus();return this}}),t=i.Cell=c.View.extend({tagName:"td",formatter:k,editor:s,events:{click:"enterEditMode"},initialize:function(a){this.column=a.column,this.column instanceof C||(this.column=new C(this.column));var c=this.column,d=this.model,e=this.$el,f=i.resolveNameToClass(c.get("formatter")||this.formatter,"Formatter");b.isFunction(f.fromRaw)||b.isFunction(f.toRaw)||(f=new f),this.formatter=f,this.editor=i.resolveNameToClass(this.editor,"CellEditor"),this.listenTo(d,"change:"+c.get("name"),function(){e.hasClass("editor")||this.render()}),this.listenTo(d,"backgrid:error",this.renderError),this.listenTo(c,"change:editable change:sortable change:renderable",function(a){var b=a.changedAttributes();for(var c in b)b.hasOwnProperty(c)&&e.toggleClass(c,b[c])}),i.callByNeed(c.editable(),c,d)&&e.addClass("editable"),i.callByNeed(c.sortable(),c,d)&&e.addClass("sortable"),i.callByNeed(c.renderable(),c,d)&&e.addClass("renderable")},render:function(){this.$el.empty();var a=this.model;return this.$el.text(this.formatter.fromRaw(a.get(this.column.get("name")),a)),this.delegateEvents(),this},enterEditMode:function(){var a=this.model,b=this.column,c=i.callByNeed(b.editable(),b,a);c&&(this.currentEditor=new this.editor({column:this.column,model:this.model,formatter:this.formatter}),a.trigger("backgrid:edit",a,b,this,this.currentEditor),this.undelegateEvents(),this.$el.empty(),this.$el.append(this.currentEditor.$el),this.currentEditor.render(),this.$el.addClass("editor"),a.trigger("backgrid:editing",a,b,this,this.currentEditor))},renderError:function(a,b){(null==b||b.get("name")==this.column.get("name"))&&this.$el.addClass("error")},exitEditMode:function(){this.$el.removeClass("error"),this.currentEditor.remove(),this.stopListening(this.currentEditor),delete this.currentEditor,this.$el.removeClass("editor"),this.render()},remove:function(){return this.currentEditor&&(this.currentEditor.remove.apply(this.currentEditor,arguments),delete this.currentEditor),t.__super__.remove.apply(this,arguments)}}),u=i.StringCell=t.extend({className:"string-cell",formatter:o}),v=i.UriCell=t.extend({className:"uri-cell",title:null,target:"_blank",initialize:function(a){v.__super__.initialize.apply(this,arguments),this.title=a.title||this.title,this.target=a.target||this.target},render:function(){this.$el.empty();var a=this.model.get(this.column.get("name")),b=this.formatter.fromRaw(a,this.model);return this.$el.append(h("<a>",{tabIndex:-1,href:a,title:this.title||b,target:this.target}).text(b)),this.delegateEvents(),this}}),w=(i.EmailCell=u.extend({className:"email-cell",formatter:p,render:function(){this.$el.empty();var a=this.model,b=this.formatter.fromRaw(a.get(this.column.get("name")),a);return this.$el.append(h("<a>",{tabIndex:-1,href:"mailto:"+b,title:b}).text(b)),this.delegateEvents(),this}}),i.NumberCell=t.extend({className:"number-cell",decimals:l.prototype.defaults.decimals,decimalSeparator:l.prototype.defaults.decimalSeparator,orderSeparator:l.prototype.defaults.orderSeparator,formatter:l,initialize:function(){w.__super__.initialize.apply(this,arguments);var a=this.formatter;a.decimals=this.decimals,a.decimalSeparator=this.decimalSeparator,a.orderSeparator=this.orderSeparator}})),x=(i.IntegerCell=w.extend({className:"integer-cell",decimals:0}),i.PercentCell=w.extend({className:"percent-cell",multiplier:m.prototype.defaults.multiplier,symbol:m.prototype.defaults.symbol,formatter:m,initialize:function(){x.__super__.initialize.apply(this,arguments);var a=this.formatter;a.multiplier=this.multiplier,a.symbol=this.symbol}})),y=i.DatetimeCell=t.extend({className:"datetime-cell",includeDate:n.prototype.defaults.includeDate,includeTime:n.prototype.defaults.includeTime,includeMilli:n.prototype.defaults.includeMilli,formatter:n,initialize:function(){y.__super__.initialize.apply(this,arguments);var a=this.formatter;a.includeDate=this.includeDate,a.includeTime=this.includeTime,a.includeMilli=this.includeMilli;var c=this.includeDate?"YYYY-MM-DD":"";c+=this.includeDate&&this.includeTime?"T":"",c+=this.includeTime?"HH:mm:ss":"",c+=this.includeTime&&this.includeMilli?".SSS":"",this.editor=this.editor.extend({attributes:b.extend({},this.editor.prototype.attributes,this.editor.attributes,{placeholder:c})})}}),z=(i.DateCell=y.extend({className:"date-cell",includeTime:!1}),i.TimeCell=y.extend({className:"time-cell",includeDate:!1}),i.BooleanCellEditor=r.extend({tagName:"input",attributes:{tabIndex:-1,type:"checkbox"},events:{mousedown:function(){this.mouseDown=!0},blur:"enterOrExitEditMode",mouseup:function(){this.mouseDown=!1},change:"saveOrCancel",keydown:"saveOrCancel"},render:function(){var a=this.model,b=this.formatter.fromRaw(a.get(this.column.get("name")),a);return this.$el.prop("checked",b),this},enterOrExitEditMode:function(a){if(!this.mouseDown){var b=this.model;b.trigger("backgrid:edited",b,this.column,new j(a))}},saveOrCancel:function(a){var b=this.model,c=this.column,d=this.formatter,e=new j(a);if(e.passThru()&&"change"!=a.type)return!0;e.cancel()&&(a.stopPropagation(),b.trigger("backgrid:edited",b,c,e));var f=this.$el;if(e.save()||e.moveLeft()||e.moveRight()||e.moveUp()||e.moveDown()){a.preventDefault(),a.stopPropagation();var g=d.toRaw(f.prop("checked"),b);b.set(c.get("name"),g),b.trigger("backgrid:edited",b,c,e)}else if("change"==a.type){var g=d.toRaw(f.prop("checked"),b);b.set(c.get("name"),g),f.focus()}}})),A=(i.BooleanCell=t.extend({className:"boolean-cell",editor:z,events:{click:"enterEditMode"},render:function(){this.$el.empty();var a=this.model,b=this.column,c=i.callByNeed(b.editable(),b,a);return this.$el.append(h("<input>",{tabIndex:-1,type:"checkbox",checked:this.formatter.fromRaw(a.get(b.get("name")),a),disabled:!c})),this.delegateEvents(),this}}),i.SelectCellEditor=r.extend({tagName:"select",events:{change:"save",blur:"close",keydown:"close"},template:b.template('<option value="<%- value %>" <%= selected ? \'selected="selected"\' : "" %>><%- text %></option>',null,{variable:null}),setOptionValues:function(a){this.optionValues=a,this.optionValues=b.result(this,"optionValues")},setMultiple:function(a){this.multiple=a,this.$el.prop("multiple",a)},_renderOptions:function(a,c){for(var d="",e=0;e<a.length;e++)d+=this.template({text:a[e][0],value:a[e][1],selected:b.indexOf(c,a[e][1])>-1});return d},render:function(){this.$el.empty();var a=b.result(this,"optionValues"),c=this.model,d=this.formatter.fromRaw(c.get(this.column.get("name")),c);if(!b.isArray(a))throw new TypeError("optionValues must be an array");for(var e=null,f=null,e=null,g=null,i=null,j=0;j<a.length;j++){var e=a[j];if(b.isArray(e))f=e[0],e=e[1],this.$el.append(this.template({text:f,value:e,selected:b.indexOf(d,e)>-1}));else{if(!b.isObject(e))throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }");g=e.name,i=h("<optgroup></optgroup>",{label:g}),i.append(this._renderOptions.call(this,e.values,d)),this.$el.append(i)}}return this.delegateEvents(),this},save:function(){var a=this.model,b=this.column;a.set(b.get("name"),this.formatter.toRaw(this.$el.val(),a))},close:function(a){var b=this.model,c=this.column,d=new j(a);d.cancel()?(a.stopPropagation(),b.trigger("backgrid:edited",b,c,new j(a))):(d.save()||d.moveLeft()||d.moveRight()||d.moveUp()||d.moveDown()||"blur"==a.type)&&(a.preventDefault(),a.stopPropagation(),this.save(a),b.trigger("backgrid:edited",b,c,new j(a)))}})),B=i.SelectCell=t.extend({className:"select-cell",editor:A,multiple:!1,formatter:q,optionValues:void 0,delimiter:", ",initialize:function(){B.__super__.initialize.apply(this,arguments),this.listenTo(this.model,"backgrid:edit",function(a,b,c,d){b.get("name")==this.column.get("name")&&(d.setOptionValues(this.optionValues),d.setMultiple(this.multiple))})},render:function(){this.$el.empty();var a=b.result(this,"optionValues"),c=this.model,d=this.formatter.fromRaw(c.get(this.column.get("name")),c),e=[];try{if(!b.isArray(a)||b.isEmpty(a))throw new TypeError;for(var f=0;f<d.length;f++)for(var g=d[f],h=0;h<a.length;h++){var i=a[h];if(b.isArray(i)){var j=i[0],i=i[1];i==g&&e.push(j)}else{if(!b.isObject(i))throw new TypeError;for(var k=i.values,l=0;l<k.length;l++){var m=k[l];m[1]==g&&e.push(m[0])}}}this.$el.append(e.join(this.delimiter))}catch(n){if(n instanceof TypeError)throw new TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}");throw n}return this.delegateEvents(),this}}),C=i.Column=c.Model.extend({defaults:{name:void 0,label:void 0,sortable:!0,editable:!0,renderable:!0,formatter:void 0,sortType:"cycle",sortValue:void 0,direction:null,cell:void 0,headerCell:void 0},initialize:function(){this.has("label")||this.set({label:this.get("name")},{silent:!0});var a=i.resolveNameToClass(this.get("headerCell"),"HeaderCell"),b=i.resolveNameToClass(this.get("cell"),"Cell");this.set({cell:b,headerCell:a},{silent:!0})},sortValue:function(){var a=this.get("sortValue");return b.isString(a)?this[a]:b.isFunction(a)?a:function(a,b){return a.get(b)}}});b.each(["sortable","renderable","editable"],function(a){C.prototype[a]=function(){var c=this.get(a);return b.isString(c)?this[c]:b.isFunction(c)?c:!!c}});{var D=i.Columns=c.Collection.extend({model:C}),E=i.Row=c.View.extend({tagName:"tr",initialize:function(a){var b=this.columns=a.columns;b instanceof c.Collection||(b=this.columns=new D(b));for(var d=this.cells=[],e=0;e<b.length;e++)d.push(this.makeCell(b.at(e),a));this.listenTo(b,"add",function(b,c){var e=c.indexOf(b),f=this.makeCell(b,a);d.splice(e,0,f);var g=this.$el;0===e?g.prepend(f.render().$el):e===c.length-1?g.append(f.render().$el):g.children().eq(e).before(f.render().$el)}),this.listenTo(b,"remove",function(a,b,c){d[c.index].remove(),d.splice(c.index,1)})},makeCell:function(a){return new(a.get("cell"))({column:a,model:this.model})},render:function(){this.$el.empty();for(var a=document.createDocumentFragment(),b=0;b<this.cells.length;b++)a.appendChild(this.cells[b].render().el);return this.el.appendChild(a),this.delegateEvents(),this},remove:function(){for(var a=0;a<this.cells.length;a++){var b=this.cells[a];b.remove.apply(b,arguments)}return c.View.prototype.remove.apply(this,arguments)}}),F=i.EmptyRow=c.View.extend({tagName:"tr",emptyText:null,initialize:function(a){this.emptyText=a.emptyText,this.columns=a.columns},render:function(){this.$el.empty();var a=document.createElement("td");return a.setAttribute("colspan",this.columns.length),a.appendChild(document.createTextNode(b.result(this,"emptyText"))),this.el.className="empty",this.el.appendChild(a),this}}),G=i.HeaderCell=c.View.extend({tagName:"th",events:{"click a":"onClick"},initialize:function(a){this.column=a.column,this.column instanceof C||(this.column=new C(this.column));var b=this.column,c=this.collection,d=this.$el;this.listenTo(b,"change:editable change:sortable change:renderable",function(a){var b=a.changedAttributes();for(var c in b)b.hasOwnProperty(c)&&d.toggleClass(c,b[c])}),this.listenTo(b,"change:direction",this.setCellDirection),this.listenTo(b,"change:name change:label",this.render),i.callByNeed(b.editable(),b,c)&&d.addClass("editable"),i.callByNeed(b.sortable(),b,c)&&d.addClass("sortable"),i.callByNeed(b.renderable(),b,c)&&d.addClass("renderable"),this.listenTo(c.fullCollection||c,"sort",this.removeCellDirection)},removeCellDirection:function(){this.$el.removeClass("ascending").removeClass("descending"),this.column.set("direction",null)},setCellDirection:function(a,b){this.$el.removeClass("ascending").removeClass("descending"),a.cid==this.column.cid&&this.$el.addClass(b)},onClick:function(a){function b(a,b){"ascending"===d.get("direction")?e.trigger(f,b,"descending"):"descending"===d.get("direction")?e.trigger(f,b,null):e.trigger(f,b,"ascending")}function c(a,b){"ascending"===d.get("direction")?e.trigger(f,b,"descending"):e.trigger(f,b,"ascending")}a.preventDefault();var d=this.column,e=this.collection,f="backgrid:sort",g=i.callByNeed(d.sortable(),d,this.collection);if(g){var h=d.get("sortType");"toggle"===h?c(this,d):b(this,d)}},render:function(){this.$el.empty();var a,b=this.column,c=i.callByNeed(b.sortable(),b,this.collection);return a=c?h("<a>").text(b.get("label")).append("<b class='sort-caret'></b>"):document.createTextNode(b.get("label")),this.$el.append(a),this.$el.addClass(b.get("name")),this.$el.addClass(b.get("direction")),this.delegateEvents(),this}}),H=(i.HeaderRow=i.Row.extend({requiredOptions:["columns","collection"],initialize:function(){i.Row.prototype.initialize.apply(this,arguments)},makeCell:function(a,b){var c=a.get("headerCell")||b.headerCell||G;return c=new c({column:a,collection:this.collection})}}),i.Header=c.View.extend({tagName:"thead",initialize:function(a){this.columns=a.columns,this.columns instanceof c.Collection||(this.columns=new D(this.columns)),this.row=new i.HeaderRow({columns:this.columns,collection:this.collection})},render:function(){return this.$el.append(this.row.render().$el),this.delegateEvents(),this},remove:function(){return this.row.remove.apply(this.row,arguments),c.View.prototype.remove.apply(this,arguments)}})),I=i.Body=c.View.extend({tagName:"tbody",initialize:function(a){this.columns=a.columns,this.columns instanceof c.Collection||(this.columns=new D(this.columns)),this.row=a.row||E,this.rows=this.collection.map(function(a){var b=new this.row({columns:this.columns,model:a});return b},this),this.emptyText=a.emptyText,this._unshiftEmptyRowMayBe();var b=this.collection;this.listenTo(b,"add",this.insertRow),this.listenTo(b,"remove",this.removeRow),this.listenTo(b,"sort",this.refresh),this.listenTo(b,"reset",this.refresh),this.listenTo(b,"backgrid:sort",this.sort),this.listenTo(b,"backgrid:edited",this.moveToNextCell)},_unshiftEmptyRowMayBe:function(){0===this.rows.length&&null!=this.emptyText&&this.rows.unshift(new F({emptyText:this.emptyText,columns:this.columns}))},insertRow:function(a,b,d){if(this.rows[0]instanceof F&&this.rows.pop().remove(),!(b instanceof c.Collection||d))return void this.collection.add(a,d=b);var e=new this.row({columns:this.columns,model:a}),f=b.indexOf(a);this.rows.splice(f,0,e);var g=this.$el,h=g.children(),i=e.render().$el;return f>=h.length?g.append(i):h.eq(f).before(i),this},removeRow:function(a,c,d){return d?((b.isUndefined(d.render)||d.render)&&this.rows[d.index].remove(),this.rows.splice(d.index,1),this._unshiftEmptyRowMayBe(),this):(this.collection.remove(a,d=c),void this._unshiftEmptyRowMayBe())},refresh:function(){for(var a=0;a<this.rows.length;a++)this.rows[a].remove();return this.rows=this.collection.map(function(a){var b=new this.row({columns:this.columns,model:a});return b},this),this._unshiftEmptyRowMayBe(),this.render(),this.collection.trigger("backgrid:refresh",this),this},render:function(){this.$el.empty();for(var a=document.createDocumentFragment(),b=0;b<this.rows.length;b++){var c=this.rows[b];a.appendChild(c.render().el)}return this.el.appendChild(a),this.delegateEvents(),this},remove:function(){for(var a=0;a<this.rows.length;a++){var b=this.rows[a];b.remove.apply(b,arguments)}return c.View.prototype.remove.apply(this,arguments)},sort:function(a,d){if(!b.contains(["ascending","descending",null],d))throw new RangeError('direction must be one of "ascending", "descending" or `null`');b.isString(a)&&(a=this.columns.findWhere({name:a}));var e,f=this.collection;e="ascending"===d?-1:"descending"===d?1:null;var g=this.makeComparator(a.get("name"),e,e?a.sortValue():function(a){return 1*a.cid.replace("c","")});return c.PageableCollection&&f instanceof c.PageableCollection?(f.setSorting(e&&a.get("name"),e,{sortValue:a.sortValue()}),f.fullCollection?(null==f.fullCollection.comparator&&(f.fullCollection.comparator=g),f.fullCollection.sort(),f.trigger("backgrid:sorted",a,d,f)):f.fetch({reset:!0,success:function(){f.trigger("backgrid:sorted",a,d,f)}})):(f.comparator=g,f.sort(),f.trigger("backgrid:sorted",a,d,f)),a.set("direction",d),this},makeComparator:function(a,b,c){return function(d,e){var f,g=c(d,a),h=c(e,a);return 1===b&&(f=g,g=h,h=f),g===h?0:h>g?-1:1}},moveToNextCell:function(a,b,c){var d,e,f,g,h,j=this.collection.indexOf(a),k=this.columns.indexOf(b);if(this.rows[j].cells[k].exitEditMode(),c.moveUp()||c.moveDown()||c.moveLeft()||c.moveRight()||c.save()){var l=this.columns.length,m=l*this.collection.length;if(c.moveUp()||c.moveDown()){g=j+(c.moveUp()?-1:1);var n=this.rows[g];n?(d=n.cells[k],i.callByNeed(d.column.editable(),d.column,a)&&(d.enterEditMode(),a.trigger("backgrid:next",g,k,!1))):a.trigger("backgrid:next",g,k,!0)}else if(c.moveLeft()||c.moveRight()){for(var o=c.moveRight(),p=j*l+k+(o?1:-1);p>=0&&m>p;o?p++:p--)if(g=~~(p/l),h=p-g*l,d=this.rows[g].cells[h],e=i.callByNeed(d.column.renderable(),d.column,d.model),f=i.callByNeed(d.column.editable(),d.column,a),e&&f){d.enterEditMode(),a.trigger("backgrid:next",g,h,!1);break}p==m&&a.trigger("backgrid:next",~~(p/l),p-g*l,!0)}}return this}});i.Footer=c.View.extend({tagName:"tfoot",initialize:function(a){this.columns=a.columns,this.columns instanceof c.Collection||(this.columns=new i.Columns(this.columns))}}),i.Grid=c.View.extend({tagName:"table",className:"backgrid",header:H,body:I,footer:null,initialize:function(a){a.columns instanceof c.Collection||(a.columns=new D(a.columns)),this.columns=a.columns;var d=b.omit(a,["el","id","attributes","className","tagName","events"]);this.body=a.body||this.body,this.body=new this.body(d),this.header=a.header||this.header,this.header&&(this.header=new this.header(d)),this.footer=a.footer||this.footer,this.footer&&(this.footer=new this.footer(d)),this.listenTo(this.columns,"reset",function(){this.header&&(this.header=new(this.header.remove().constructor)(d)),this.body=new(this.body.remove().constructor)(d),this.footer&&(this.footer=new(this.footer.remove().constructor)(d)),this.render()})},insertRow:function(){return this.body.insertRow.apply(this.body,arguments),this},removeRow:function(){return this.body.removeRow.apply(this.body,arguments),this},insertColumn:function(){return this.columns.add.apply(this.columns,arguments),this},removeColumn:function(){return this.columns.remove.apply(this.columns,arguments),this},sort:function(){return this.body.sort.apply(this.body,arguments),this},render:function(){return this.$el.empty(),this.header&&this.$el.append(this.header.render().$el),this.footer&&this.$el.append(this.footer.render().$el),this.$el.append(this.body.render().$el),this.delegateEvents(),this.trigger("backgrid:rendered",this),this},remove:function(){return this.header&&this.header.remove.apply(this.header,arguments),this.body.remove.apply(this.body,arguments),this.footer&&this.footer.remove.apply(this.footer,arguments),c.View.prototype.remove.apply(this,arguments)}})}return i});
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/backgrid-paginator.js b/htrace-core/src/web/lib/js/backgrid-paginator-0.3.5.js
similarity index 100%
rename from htrace-core/src/web/lib/js/backgrid-paginator.js
rename to htrace-core/src/web/lib/js/backgrid-paginator-0.3.5.js
diff --git a/htrace-core/src/web/lib/js/backgrid-paginator.min.js b/htrace-core/src/web/lib/js/backgrid-paginator.min.js
deleted file mode 100644
index 5d3bc0b..0000000
--- a/htrace-core/src/web/lib/js/backgrid-paginator.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- backgrid-paginator
- http://github.com/wyuenho/backgrid
-
- Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
- Licensed under the MIT @license.
- */
-// Trunk from Commit ID: 669bca9383f1cafa53dc0efefe96a83c5315684d.
-!function(a,b){"object"==typeof exports&&(module.exports=b(require("underscore"),require("backbone"),require("backgrid"),require("backbone.paginator"))),"function"==typeof define&&define.amd?define(["underscore","backbone","backgrid","backbone.paginator"],b):b(a._,a.Backbone,a.Backgrid)}(this,function(a,b,c){"use strict";var d=c.Extension.PageHandle=b.View.extend({tagName:"li",events:{"click a":"changePage"},title:function(a){return"Page "+a.label},isRewind:!1,isBack:!1,isForward:!1,isFastForward:!1,initialize:function(b){var c=this.collection,d=c.state,e=d.currentPage,f=d.firstPage,g=d.lastPage;a.extend(this,a.pick(b,["isRewind","isBack","isForward","isFastForward"]));var h;this.isRewind?h=f:this.isBack?h=Math.max(f,e-1):this.isForward?h=Math.min(g,e+1):this.isFastForward?h=g:(h=+b.pageIndex,h=f?h+1:h),this.pageIndex=h,this.label=(b.label||(f?h:h+1))+"";var i=b.title||this.title;this.title=a.isFunction(i)?i({label:this.label}):i},render:function(){this.$el.empty();var a=document.createElement("a");a.href="#",this.title&&(a.title=this.title),a.innerHTML=this.label,this.el.appendChild(a);var b=this.collection,c=b.state,d=c.currentPage,e=this.pageIndex;return this.isRewind&&d==c.firstPage||this.isBack&&!b.hasPreviousPage()||this.isForward&&!b.hasNextPage()||this.isFastForward&&(d==c.lastPage||c.totalPages<1)?this.$el.addClass("disabled"):this.isRewind||this.isBack||this.isForward||this.isFastForward||c.currentPage!=e||this.$el.addClass("active"),this.delegateEvents(),this},changePage:function(a){a.preventDefault();var b=this.$el,c=this.collection;return b.hasClass("active")||b.hasClass("disabled")||(this.isRewind?c.getFirstPage():this.isBack?c.getPreviousPage():this.isForward?c.getNextPage():this.isFastForward?c.getLastPage():c.getPage(this.pageIndex,{reset:!0})),this}}),e=c.Extension.Paginator=b.View.extend({className:"backgrid-paginator",windowSize:10,slideScale:.5,controls:{rewind:{label:"《",title:"First"},back:{label:"〈",title:"Previous"},forward:{label:"〉",title:"Next"},fastForward:{label:"》",title:"Last"}},renderIndexedPageHandles:!0,pageHandle:d,goBackFirstOnSort:!0,initialize:function(b){var c=this;c.controls=a.defaults(b.controls||{},c.controls,e.prototype.controls),a.extend(c,a.pick(b||{},"windowSize","pageHandle","slideScale","goBackFirstOnSort","renderIndexedPageHandles"));var d=c.collection;c.listenTo(d,"add",c.render),c.listenTo(d,"remove",c.render),c.listenTo(d,"reset",c.render),c.listenTo(d,"backgrid:sorted",function(){c.goBackFirstOnSort&&d.getFirstPage({reset:!0})})},slideMaybe:function(a,b,c,d){return Math.round(c%d/d)},slideThisMuch:function(a,b,c,d,e){return~~(d*e)},_calculateWindow:function(){var a=this.collection,b=a.state,c=b.firstPage,d=+b.lastPage;d=Math.max(0,c?d-1:d);var e=Math.max(b.currentPage,b.firstPage);e=c?e-1:e;var f=this.windowSize,g=this.slideScale,h=Math.floor(e/f)*f;e<=d-this.slideThisMuch()&&(h+=this.slideMaybe(c,d,e,f,g)*this.slideThisMuch(c,d,e,f,g));var i=Math.min(d+1,h+f);return[h,i]},makeHandles:function(){var b=[],c=this.collection,d=this._calculateWindow(),e=d[0],f=d[1];if(this.renderIndexedPageHandles)for(var g=e;f>g;g++)b.push(new this.pageHandle({collection:c,pageIndex:g}));var h=this.controls;return a.each(["back","rewind","forward","fastForward"],function(a){var d=h[a];if(d){var e={collection:c,title:d.title,label:d.label};e["is"+a.slice(0,1).toUpperCase()+a.slice(1)]=!0;var f=new this.pageHandle(e);"rewind"==a||"back"==a?b.unshift(f):b.push(f)}},this),b},render:function(){if(this.$el.empty(),this.handles)for(var a=0,b=this.handles.length;b>a;a++)this.handles[a].remove();for(var c=this.handles=this.makeHandles(),d=document.createElement("ul"),a=0;a<c.length;a++)d.appendChild(c[a].render().el);return this.el.appendChild(d),this}})});
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/underscore-1.7.0.js b/htrace-core/src/web/lib/js/underscore-1.7.0.js
new file mode 100644
index 0000000..d5b3375
--- /dev/null
+++ b/htrace-core/src/web/lib/js/underscore-1.7.0.js
@@ -0,0 +1,1416 @@
+// Underscore.js 1.7.0
+// http://underscorejs.org
+// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+// Underscore may be freely distributed under the MIT license.
+
+(function() {
+
+ // Baseline setup
+ // --------------
+
+ // Establish the root object, `window` in the browser, or `exports` on the server.
+ var root = this;
+
+ // Save the previous value of the `_` variable.
+ var previousUnderscore = root._;
+
+ // Save bytes in the minified (but not gzipped) version:
+ var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+ // Create quick reference variables for speed access to core prototypes.
+ var
+ push = ArrayProto.push,
+ slice = ArrayProto.slice,
+ concat = ArrayProto.concat,
+ toString = ObjProto.toString,
+ hasOwnProperty = ObjProto.hasOwnProperty;
+
+ // All **ECMAScript 5** native function implementations that we hope to use
+ // are declared here.
+ var
+ nativeIsArray = Array.isArray,
+ nativeKeys = Object.keys,
+ nativeBind = FuncProto.bind;
+
+ // Create a safe reference to the Underscore object for use below.
+ var _ = function(obj) {
+ if (obj instanceof _) return obj;
+ if (!(this instanceof _)) return new _(obj);
+ this._wrapped = obj;
+ };
+
+ // Export the Underscore object for **Node.js**, with
+ // backwards-compatibility for the old `require()` API. If we're in
+ // the browser, add `_` as a global object.
+ if (typeof exports !== 'undefined') {
+ if (typeof module !== 'undefined' && module.exports) {
+ exports = module.exports = _;
+ }
+ exports._ = _;
+ } else {
+ root._ = _;
+ }
+
+ // Current version.
+ _.VERSION = '1.7.0';
+
+ // Internal function that returns an efficient (for current engines) version
+ // of the passed-in callback, to be repeatedly applied in other Underscore
+ // functions.
+ var createCallback = function(func, context, argCount) {
+ if (context === void 0) return func;
+ switch (argCount == null ? 3 : argCount) {
+ case 1: return function(value) {
+ return func.call(context, value);
+ };
+ case 2: return function(value, other) {
+ return func.call(context, value, other);
+ };
+ case 3: return function(value, index, collection) {
+ return func.call(context, value, index, collection);
+ };
+ case 4: return function(accumulator, value, index, collection) {
+ return func.call(context, accumulator, value, index, collection);
+ };
+ }
+ return function() {
+ return func.apply(context, arguments);
+ };
+ };
+
+ // A mostly-internal function to generate callbacks that can be applied
+ // to each element in a collection, returning the desired result — either
+ // identity, an arbitrary callback, a property matcher, or a property accessor.
+ _.iteratee = function(value, context, argCount) {
+ if (value == null) return _.identity;
+ if (_.isFunction(value)) return createCallback(value, context, argCount);
+ if (_.isObject(value)) return _.matches(value);
+ return _.property(value);
+ };
+
+ // Collection Functions
+ // --------------------
+
+ // The cornerstone, an `each` implementation, aka `forEach`.
+ // Handles raw objects in addition to array-likes. Treats all
+ // sparse array-likes as if they were dense.
+ _.each = _.forEach = function(obj, iteratee, context) {
+ if (obj == null) return obj;
+ iteratee = createCallback(iteratee, context);
+ var i, length = obj.length;
+ if (length === +length) {
+ for (i = 0; i < length; i++) {
+ iteratee(obj[i], i, obj);
+ }
+ } else {
+ var keys = _.keys(obj);
+ for (i = 0, length = keys.length; i < length; i++) {
+ iteratee(obj[keys[i]], keys[i], obj);
+ }
+ }
+ return obj;
+ };
+
+ // Return the results of applying the iteratee to each element.
+ _.map = _.collect = function(obj, iteratee, context) {
+ if (obj == null) return [];
+ iteratee = _.iteratee(iteratee, context);
+ var keys = obj.length !== +obj.length && _.keys(obj),
+ length = (keys || obj).length,
+ results = Array(length),
+ currentKey;
+ for (var index = 0; index < length; index++) {
+ currentKey = keys ? keys[index] : index;
+ results[index] = iteratee(obj[currentKey], currentKey, obj);
+ }
+ return results;
+ };
+
+ var reduceError = 'Reduce of empty array with no initial value';
+
+ // **Reduce** builds up a single result from a list of values, aka `inject`,
+ // or `foldl`.
+ _.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) {
+ if (obj == null) obj = [];
+ iteratee = createCallback(iteratee, context, 4);
+ var keys = obj.length !== +obj.length && _.keys(obj),
+ length = (keys || obj).length,
+ index = 0, currentKey;
+ if (arguments.length < 3) {
+ if (!length) throw new TypeError(reduceError);
+ memo = obj[keys ? keys[index++] : index++];
+ }
+ for (; index < length; index++) {
+ currentKey = keys ? keys[index] : index;
+ memo = iteratee(memo, obj[currentKey], currentKey, obj);
+ }
+ return memo;
+ };
+
+ // The right-associative version of reduce, also known as `foldr`.
+ _.reduceRight = _.foldr = function(obj, iteratee, memo, context) {
+ if (obj == null) obj = [];
+ iteratee = createCallback(iteratee, context, 4);
+ var keys = obj.length !== + obj.length && _.keys(obj),
+ index = (keys || obj).length,
+ currentKey;
+ if (arguments.length < 3) {
+ if (!index) throw new TypeError(reduceError);
+ memo = obj[keys ? keys[--index] : --index];
+ }
+ while (index--) {
+ currentKey = keys ? keys[index] : index;
+ memo = iteratee(memo, obj[currentKey], currentKey, obj);
+ }
+ return memo;
+ };
+
+ // Return the first value which passes a truth test. Aliased as `detect`.
+ _.find = _.detect = function(obj, predicate, context) {
+ var result;
+ predicate = _.iteratee(predicate, context);
+ _.some(obj, function(value, index, list) {
+ if (predicate(value, index, list)) {
+ result = value;
+ return true;
+ }
+ });
+ return result;
+ };
+
+ // Return all the elements that pass a truth test.
+ // Aliased as `select`.
+ _.filter = _.select = function(obj, predicate, context) {
+ var results = [];
+ if (obj == null) return results;
+ predicate = _.iteratee(predicate, context);
+ _.each(obj, function(value, index, list) {
+ if (predicate(value, index, list)) results.push(value);
+ });
+ return results;
+ };
+
+ // Return all the elements for which a truth test fails.
+ _.reject = function(obj, predicate, context) {
+ return _.filter(obj, _.negate(_.iteratee(predicate)), context);
+ };
+
+ // Determine whether all of the elements match a truth test.
+ // Aliased as `all`.
+ _.every = _.all = function(obj, predicate, context) {
+ if (obj == null) return true;
+ predicate = _.iteratee(predicate, context);
+ var keys = obj.length !== +obj.length && _.keys(obj),
+ length = (keys || obj).length,
+ index, currentKey;
+ for (index = 0; index < length; index++) {
+ currentKey = keys ? keys[index] : index;
+ if (!predicate(obj[currentKey], currentKey, obj)) return false;
+ }
+ return true;
+ };
+
+ // Determine if at least one element in the object matches a truth test.
+ // Aliased as `any`.
+ _.some = _.any = function(obj, predicate, context) {
+ if (obj == null) return false;
+ predicate = _.iteratee(predicate, context);
+ var keys = obj.length !== +obj.length && _.keys(obj),
+ length = (keys || obj).length,
+ index, currentKey;
+ for (index = 0; index < length; index++) {
+ currentKey = keys ? keys[index] : index;
+ if (predicate(obj[currentKey], currentKey, obj)) return true;
+ }
+ return false;
+ };
+
+ // Determine if the array or object contains a given value (using `===`).
+ // Aliased as `include`.
+ _.contains = _.include = function(obj, target) {
+ if (obj == null) return false;
+ if (obj.length !== +obj.length) obj = _.values(obj);
+ return _.indexOf(obj, target) >= 0;
+ };
+
+ // Invoke a method (with arguments) on every item in a collection.
+ _.invoke = function(obj, method) {
+ var args = slice.call(arguments, 2);
+ var isFunc = _.isFunction(method);
+ return _.map(obj, function(value) {
+ return (isFunc ? method : value[method]).apply(value, args);
+ });
+ };
+
+ // Convenience version of a common use case of `map`: fetching a property.
+ _.pluck = function(obj, key) {
+ return _.map(obj, _.property(key));
+ };
+
+ // Convenience version of a common use case of `filter`: selecting only objects
+ // containing specific `key:value` pairs.
+ _.where = function(obj, attrs) {
+ return _.filter(obj, _.matches(attrs));
+ };
+
+ // Convenience version of a common use case of `find`: getting the first object
+ // containing specific `key:value` pairs.
+ _.findWhere = function(obj, attrs) {
+ return _.find(obj, _.matches(attrs));
+ };
+
+ // Return the maximum element (or element-based computation).
+ _.max = function(obj, iteratee, context) {
+ var result = -Infinity, lastComputed = -Infinity,
+ value, computed;
+ if (iteratee == null && obj != null) {
+ obj = obj.length === +obj.length ? obj : _.values(obj);
+ for (var i = 0, length = obj.length; i < length; i++) {
+ value = obj[i];
+ if (value > result) {
+ result = value;
+ }
+ }
+ } else {
+ iteratee = _.iteratee(iteratee, context);
+ _.each(obj, function(value, index, list) {
+ computed = iteratee(value, index, list);
+ if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
+ result = value;
+ lastComputed = computed;
+ }
+ });
+ }
+ return result;
+ };
+
+ // Return the minimum element (or element-based computation).
+ _.min = function(obj, iteratee, context) {
+ var result = Infinity, lastComputed = Infinity,
+ value, computed;
+ if (iteratee == null && obj != null) {
+ obj = obj.length === +obj.length ? obj : _.values(obj);
+ for (var i = 0, length = obj.length; i < length; i++) {
+ value = obj[i];
+ if (value < result) {
+ result = value;
+ }
+ }
+ } else {
+ iteratee = _.iteratee(iteratee, context);
+ _.each(obj, function(value, index, list) {
+ computed = iteratee(value, index, list);
+ if (computed < lastComputed || computed === Infinity && result === Infinity) {
+ result = value;
+ lastComputed = computed;
+ }
+ });
+ }
+ return result;
+ };
+
+ // Shuffle a collection, using the modern version of the
+ // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
+ _.shuffle = function(obj) {
+ var set = obj && obj.length === +obj.length ? obj : _.values(obj);
+ var length = set.length;
+ var shuffled = Array(length);
+ for (var index = 0, rand; index < length; index++) {
+ rand = _.random(0, index);
+ if (rand !== index) shuffled[index] = shuffled[rand];
+ shuffled[rand] = set[index];
+ }
+ return shuffled;
+ };
+
+ // Sample **n** random values from a collection.
+ // If **n** is not specified, returns a single random element.
+ // The internal `guard` argument allows it to work with `map`.
+ _.sample = function(obj, n, guard) {
+ if (n == null || guard) {
+ if (obj.length !== +obj.length) obj = _.values(obj);
+ return obj[_.random(obj.length - 1)];
+ }
+ return _.shuffle(obj).slice(0, Math.max(0, n));
+ };
+
+ // Sort the object's values by a criterion produced by an iteratee.
+ _.sortBy = function(obj, iteratee, context) {
+ iteratee = _.iteratee(iteratee, context);
+ return _.pluck(_.map(obj, function(value, index, list) {
+ return {
+ value: value,
+ index: index,
+ criteria: iteratee(value, index, list)
+ };
+ }).sort(function(left, right) {
+ var a = left.criteria;
+ var b = right.criteria;
+ if (a !== b) {
+ if (a > b || a === void 0) return 1;
+ if (a < b || b === void 0) return -1;
+ }
+ return left.index - right.index;
+ }), 'value');
+ };
+
+ // An internal function used for aggregate "group by" operations.
+ var group = function(behavior) {
+ return function(obj, iteratee, context) {
+ var result = {};
+ iteratee = _.iteratee(iteratee, context);
+ _.each(obj, function(value, index) {
+ var key = iteratee(value, index, obj);
+ behavior(result, value, key);
+ });
+ return result;
+ };
+ };
+
+ // Groups the object's values by a criterion. Pass either a string attribute
+ // to group by, or a function that returns the criterion.
+ _.groupBy = group(function(result, value, key) {
+ if (_.has(result, key)) result[key].push(value); else result[key] = [value];
+ });
+
+ // Indexes the object's values by a criterion, similar to `groupBy`, but for
+ // when you know that your index values will be unique.
+ _.indexBy = group(function(result, value, key) {
+ result[key] = value;
+ });
+
+ // Counts instances of an object that group by a certain criterion. Pass
+ // either a string attribute to count by, or a function that returns the
+ // criterion.
+ _.countBy = group(function(result, value, key) {
+ if (_.has(result, key)) result[key]++; else result[key] = 1;
+ });
+
+ // Use a comparator function to figure out the smallest index at which
+ // an object should be inserted so as to maintain order. Uses binary search.
+ _.sortedIndex = function(array, obj, iteratee, context) {
+ iteratee = _.iteratee(iteratee, context, 1);
+ var value = iteratee(obj);
+ var low = 0, high = array.length;
+ while (low < high) {
+ var mid = low + high >>> 1;
+ if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
+ }
+ return low;
+ };
+
+ // Safely create a real, live array from anything iterable.
+ _.toArray = function(obj) {
+ if (!obj) return [];
+ if (_.isArray(obj)) return slice.call(obj);
+ if (obj.length === +obj.length) return _.map(obj, _.identity);
+ return _.values(obj);
+ };
+
+ // Return the number of elements in an object.
+ _.size = function(obj) {
+ if (obj == null) return 0;
+ return obj.length === +obj.length ? obj.length : _.keys(obj).length;
+ };
+
+ // Split a collection into two arrays: one whose elements all satisfy the given
+ // predicate, and one whose elements all do not satisfy the predicate.
+ _.partition = function(obj, predicate, context) {
+ predicate = _.iteratee(predicate, context);
+ var pass = [], fail = [];
+ _.each(obj, function(value, key, obj) {
+ (predicate(value, key, obj) ? pass : fail).push(value);
+ });
+ return [pass, fail];
+ };
+
+ // Array Functions
+ // ---------------
+
+ // Get the first element of an array. Passing **n** will return the first N
+ // values in the array. Aliased as `head` and `take`. The **guard** check
+ // allows it to work with `_.map`.
+ _.first = _.head = _.take = function(array, n, guard) {
+ if (array == null) return void 0;
+ if (n == null || guard) return array[0];
+ if (n < 0) return [];
+ return slice.call(array, 0, n);
+ };
+
+ // Returns everything but the last entry of the array. Especially useful on
+ // the arguments object. Passing **n** will return all the values in
+ // the array, excluding the last N. The **guard** check allows it to work with
+ // `_.map`.
+ _.initial = function(array, n, guard) {
+ return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
+ };
+
+ // Get the last element of an array. Passing **n** will return the last N
+ // values in the array. The **guard** check allows it to work with `_.map`.
+ _.last = function(array, n, guard) {
+ if (array == null) return void 0;
+ if (n == null || guard) return array[array.length - 1];
+ return slice.call(array, Math.max(array.length - n, 0));
+ };
+
+ // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
+ // Especially useful on the arguments object. Passing an **n** will return
+ // the rest N values in the array. The **guard**
+ // check allows it to work with `_.map`.
+ _.rest = _.tail = _.drop = function(array, n, guard) {
+ return slice.call(array, n == null || guard ? 1 : n);
+ };
+
+ // Trim out all falsy values from an array.
+ _.compact = function(array) {
+ return _.filter(array, _.identity);
+ };
+
+ // Internal implementation of a recursive `flatten` function.
+ var flatten = function(input, shallow, strict, output) {
+ if (shallow && _.every(input, _.isArray)) {
+ return concat.apply(output, input);
+ }
+ for (var i = 0, length = input.length; i < length; i++) {
+ var value = input[i];
+ if (!_.isArray(value) && !_.isArguments(value)) {
+ if (!strict) output.push(value);
+ } else if (shallow) {
+ push.apply(output, value);
+ } else {
+ flatten(value, shallow, strict, output);
+ }
+ }
+ return output;
+ };
+
+ // Flatten out an array, either recursively (by default), or just one level.
+ _.flatten = function(array, shallow) {
+ return flatten(array, shallow, false, []);
+ };
+
+ // Return a version of the array that does not contain the specified value(s).
+ _.without = function(array) {
+ return _.difference(array, slice.call(arguments, 1));
+ };
+
+ // Produce a duplicate-free version of the array. If the array has already
+ // been sorted, you have the option of using a faster algorithm.
+ // Aliased as `unique`.
+ _.uniq = _.unique = function(array, isSorted, iteratee, context) {
+ if (array == null) return [];
+ if (!_.isBoolean(isSorted)) {
+ context = iteratee;
+ iteratee = isSorted;
+ isSorted = false;
+ }
+ if (iteratee != null) iteratee = _.iteratee(iteratee, context);
+ var result = [];
+ var seen = [];
+ for (var i = 0, length = array.length; i < length; i++) {
+ var value = array[i];
+ if (isSorted) {
+ if (!i || seen !== value) result.push(value);
+ seen = value;
+ } else if (iteratee) {
+ var computed = iteratee(value, i, array);
+ if (_.indexOf(seen, computed) < 0) {
+ seen.push(computed);
+ result.push(value);
+ }
+ } else if (_.indexOf(result, value) < 0) {
+ result.push(value);
+ }
+ }
+ return result;
+ };
+
+ // Produce an array that contains the union: each distinct element from all of
+ // the passed-in arrays.
+ _.union = function() {
+ return _.uniq(flatten(arguments, true, true, []));
+ };
+
+ // Produce an array that contains every item shared between all the
+ // passed-in arrays.
+ _.intersection = function(array) {
+ if (array == null) return [];
+ var result = [];
+ var argsLength = arguments.length;
+ for (var i = 0, length = array.length; i < length; i++) {
+ var item = array[i];
+ if (_.contains(result, item)) continue;
+ for (var j = 1; j < argsLength; j++) {
+ if (!_.contains(arguments[j], item)) break;
+ }
+ if (j === argsLength) result.push(item);
+ }
+ return result;
+ };
+
+ // Take the difference between one array and a number of other arrays.
+ // Only the elements present in just the first array will remain.
+ _.difference = function(array) {
+ var rest = flatten(slice.call(arguments, 1), true, true, []);
+ return _.filter(array, function(value){
+ return !_.contains(rest, value);
+ });
+ };
+
+ // Zip together multiple lists into a single array -- elements that share
+ // an index go together.
+ _.zip = function(array) {
+ if (array == null) return [];
+ var length = _.max(arguments, 'length').length;
+ var results = Array(length);
+ for (var i = 0; i < length; i++) {
+ results[i] = _.pluck(arguments, i);
+ }
+ return results;
+ };
+
+ // Converts lists into objects. Pass either a single array of `[key, value]`
+ // pairs, or two parallel arrays of the same length -- one of keys, and one of
+ // the corresponding values.
+ _.object = function(list, values) {
+ if (list == null) return {};
+ var result = {};
+ for (var i = 0, length = list.length; i < length; i++) {
+ if (values) {
+ result[list[i]] = values[i];
+ } else {
+ result[list[i][0]] = list[i][1];
+ }
+ }
+ return result;
+ };
+
+ // Return the position of the first occurrence of an item in an array,
+ // or -1 if the item is not included in the array.
+ // If the array is large and already in sort order, pass `true`
+ // for **isSorted** to use binary search.
+ _.indexOf = function(array, item, isSorted) {
+ if (array == null) return -1;
+ var i = 0, length = array.length;
+ if (isSorted) {
+ if (typeof isSorted == 'number') {
+ i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted;
+ } else {
+ i = _.sortedIndex(array, item);
+ return array[i] === item ? i : -1;
+ }
+ }
+ for (; i < length; i++) if (array[i] === item) return i;
+ return -1;
+ };
+
+ _.lastIndexOf = function(array, item, from) {
+ if (array == null) return -1;
+ var idx = array.length;
+ if (typeof from == 'number') {
+ idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1);
+ }
+ while (--idx >= 0) if (array[idx] === item) return idx;
+ return -1;
+ };
+
+ // Generate an integer Array containing an arithmetic progression. A port of
+ // the native Python `range()` function. See
+ // [the Python documentation](http://docs.python.org/library/functions.html#range).
+ _.range = function(start, stop, step) {
+ if (arguments.length <= 1) {
+ stop = start || 0;
+ start = 0;
+ }
+ step = step || 1;
+
+ var length = Math.max(Math.ceil((stop - start) / step), 0);
+ var range = Array(length);
+
+ for (var idx = 0; idx < length; idx++, start += step) {
+ range[idx] = start;
+ }
+
+ return range;
+ };
+
+ // Function (ahem) Functions
+ // ------------------
+
+ // Reusable constructor function for prototype setting.
+ var Ctor = function(){};
+
+ // Create a function bound to a given object (assigning `this`, and arguments,
+ // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
+ // available.
+ _.bind = function(func, context) {
+ var args, bound;
+ if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
+ if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
+ args = slice.call(arguments, 2);
+ bound = function() {
+ if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
+ Ctor.prototype = func.prototype;
+ var self = new Ctor;
+ Ctor.prototype = null;
+ var result = func.apply(self, args.concat(slice.call(arguments)));
+ if (_.isObject(result)) return result;
+ return self;
+ };
+ return bound;
+ };
+
+ // Partially apply a function by creating a version that has had some of its
+ // arguments pre-filled, without changing its dynamic `this` context. _ acts
+ // as a placeholder, allowing any combination of arguments to be pre-filled.
+ _.partial = function(func) {
+ var boundArgs = slice.call(arguments, 1);
+ return function() {
+ var position = 0;
+ var args = boundArgs.slice();
+ for (var i = 0, length = args.length; i < length; i++) {
+ if (args[i] === _) args[i] = arguments[position++];
+ }
+ while (position < arguments.length) args.push(arguments[position++]);
+ return func.apply(this, args);
+ };
+ };
+
+ // Bind a number of an object's methods to that object. Remaining arguments
+ // are the method names to be bound. Useful for ensuring that all callbacks
+ // defined on an object belong to it.
+ _.bindAll = function(obj) {
+ var i, length = arguments.length, key;
+ if (length <= 1) throw new Error('bindAll must be passed function names');
+ for (i = 1; i < length; i++) {
+ key = arguments[i];
+ obj[key] = _.bind(obj[key], obj);
+ }
+ return obj;
+ };
+
+ // Memoize an expensive function by storing its results.
+ _.memoize = function(func, hasher) {
+ var memoize = function(key) {
+ var cache = memoize.cache;
+ var address = hasher ? hasher.apply(this, arguments) : key;
+ if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
+ return cache[address];
+ };
+ memoize.cache = {};
+ return memoize;
+ };
+
+ // Delays a function for the given number of milliseconds, and then calls
+ // it with the arguments supplied.
+ _.delay = function(func, wait) {
+ var args = slice.call(arguments, 2);
+ return setTimeout(function(){
+ return func.apply(null, args);
+ }, wait);
+ };
+
+ // Defers a function, scheduling it to run after the current call stack has
+ // cleared.
+ _.defer = function(func) {
+ return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
+ };
+
+ // Returns a function, that, when invoked, will only be triggered at most once
+ // during a given window of time. Normally, the throttled function will run
+ // as much as it can, without ever going more than once per `wait` duration;
+ // but if you'd like to disable the execution on the leading edge, pass
+ // `{leading: false}`. To disable execution on the trailing edge, ditto.
+ _.throttle = function(func, wait, options) {
+ var context, args, result;
+ var timeout = null;
+ var previous = 0;
+ if (!options) options = {};
+ var later = function() {
+ previous = options.leading === false ? 0 : _.now();
+ timeout = null;
+ result = func.apply(context, args);
+ if (!timeout) context = args = null;
+ };
+ return function() {
+ var now = _.now();
+ if (!previous && options.leading === false) previous = now;
+ var remaining = wait - (now - previous);
+ context = this;
+ args = arguments;
+ if (remaining <= 0 || remaining > wait) {
+ clearTimeout(timeout);
+ timeout = null;
+ previous = now;
+ result = func.apply(context, args);
+ if (!timeout) context = args = null;
+ } else if (!timeout && options.trailing !== false) {
+ timeout = setTimeout(later, remaining);
+ }
+ return result;
+ };
+ };
+
+ // Returns a function, that, as long as it continues to be invoked, will not
+ // be triggered. The function will be called after it stops being called for
+ // N milliseconds. If `immediate` is passed, trigger the function on the
+ // leading edge, instead of the trailing.
+ _.debounce = function(func, wait, immediate) {
+ var timeout, args, context, timestamp, result;
+
+ var later = function() {
+ var last = _.now() - timestamp;
+
+ if (last < wait && last > 0) {
+ timeout = setTimeout(later, wait - last);
+ } else {
+ timeout = null;
+ if (!immediate) {
+ result = func.apply(context, args);
+ if (!timeout) context = args = null;
+ }
+ }
+ };
+
+ return function() {
+ context = this;
+ args = arguments;
+ timestamp = _.now();
+ var callNow = immediate && !timeout;
+ if (!timeout) timeout = setTimeout(later, wait);
+ if (callNow) {
+ result = func.apply(context, args);
+ context = args = null;
+ }
+
+ return result;
+ };
+ };
+
+ // Returns the first function passed as an argument to the second,
+ // allowing you to adjust arguments, run code before and after, and
+ // conditionally execute the original function.
+ _.wrap = function(func, wrapper) {
+ return _.partial(wrapper, func);
+ };
+
+ // Returns a negated version of the passed-in predicate.
+ _.negate = function(predicate) {
+ return function() {
+ return !predicate.apply(this, arguments);
+ };
+ };
+
+ // Returns a function that is the composition of a list of functions, each
+ // consuming the return value of the function that follows.
+ _.compose = function() {
+ var args = arguments;
+ var start = args.length - 1;
+ return function() {
+ var i = start;
+ var result = args[start].apply(this, arguments);
+ while (i--) result = args[i].call(this, result);
+ return result;
+ };
+ };
+
+ // Returns a function that will only be executed after being called N times.
+ _.after = function(times, func) {
+ return function() {
+ if (--times < 1) {
+ return func.apply(this, arguments);
+ }
+ };
+ };
+
+ // Returns a function that will only be executed before being called N times.
+ _.before = function(times, func) {
+ var memo;
+ return function() {
+ if (--times > 0) {
+ memo = func.apply(this, arguments);
+ } else {
+ func = null;
+ }
+ return memo;
+ };
+ };
+
+ // Returns a function that will be executed at most one time, no matter how
+ // often you call it. Useful for lazy initialization.
+ _.once = _.partial(_.before, 2);
+
+ // Object Functions
+ // ----------------
+
+ // Retrieve the names of an object's properties.
+ // Delegates to **ECMAScript 5**'s native `Object.keys`
+ _.keys = function(obj) {
+ if (!_.isObject(obj)) return [];
+ if (nativeKeys) return nativeKeys(obj);
+ var keys = [];
+ for (var key in obj) if (_.has(obj, key)) keys.push(key);
+ return keys;
+ };
+
+ // Retrieve the values of an object's properties.
+ _.values = function(obj) {
+ var keys = _.keys(obj);
+ var length = keys.length;
+ var values = Array(length);
+ for (var i = 0; i < length; i++) {
+ values[i] = obj[keys[i]];
+ }
+ return values;
+ };
+
+ // Convert an object into a list of `[key, value]` pairs.
+ _.pairs = function(obj) {
+ var keys = _.keys(obj);
+ var length = keys.length;
+ var pairs = Array(length);
+ for (var i = 0; i < length; i++) {
+ pairs[i] = [keys[i], obj[keys[i]]];
+ }
+ return pairs;
+ };
+
+ // Invert the keys and values of an object. The values must be serializable.
+ _.invert = function(obj) {
+ var result = {};
+ var keys = _.keys(obj);
+ for (var i = 0, length = keys.length; i < length; i++) {
+ result[obj[keys[i]]] = keys[i];
+ }
+ return result;
+ };
+
+ // Return a sorted list of the function names available on the object.
+ // Aliased as `methods`
+ _.functions = _.methods = function(obj) {
+ var names = [];
+ for (var key in obj) {
+ if (_.isFunction(obj[key])) names.push(key);
+ }
+ return names.sort();
+ };
+
+ // Extend a given object with all the properties in passed-in object(s).
+ _.extend = function(obj) {
+ if (!_.isObject(obj)) return obj;
+ var source, prop;
+ for (var i = 1, length = arguments.length; i < length; i++) {
+ source = arguments[i];
+ for (prop in source) {
+ if (hasOwnProperty.call(source, prop)) {
+ obj[prop] = source[prop];
+ }
+ }
+ }
+ return obj;
+ };
+
+ // Return a copy of the object only containing the whitelisted properties.
+ _.pick = function(obj, iteratee, context) {
+ var result = {}, key;
+ if (obj == null) return result;
+ if (_.isFunction(iteratee)) {
+ iteratee = createCallback(iteratee, context);
+ for (key in obj) {
+ var value = obj[key];
+ if (iteratee(value, key, obj)) result[key] = value;
+ }
+ } else {
+ var keys = concat.apply([], slice.call(arguments, 1));
+ obj = new Object(obj);
+ for (var i = 0, length = keys.length; i < length; i++) {
+ key = keys[i];
+ if (key in obj) result[key] = obj[key];
+ }
+ }
+ return result;
+ };
+
+ // Return a copy of the object without the blacklisted properties.
+ _.omit = function(obj, iteratee, context) {
+ if (_.isFunction(iteratee)) {
+ iteratee = _.negate(iteratee);
+ } else {
+ var keys = _.map(concat.apply([], slice.call(arguments, 1)), String);
+ iteratee = function(value, key) {
+ return !_.contains(keys, key);
+ };
+ }
+ return _.pick(obj, iteratee, context);
+ };
+
+ // Fill in a given object with default properties.
+ _.defaults = function(obj) {
+ if (!_.isObject(obj)) return obj;
+ for (var i = 1, length = arguments.length; i < length; i++) {
+ var source = arguments[i];
+ for (var prop in source) {
+ if (obj[prop] === void 0) obj[prop] = source[prop];
+ }
+ }
+ return obj;
+ };
+
+ // Create a (shallow-cloned) duplicate of an object.
+ _.clone = function(obj) {
+ if (!_.isObject(obj)) return obj;
+ return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+ };
+
+ // Invokes interceptor with the obj, and then returns obj.
+ // The primary purpose of this method is to "tap into" a method chain, in
+ // order to perform operations on intermediate results within the chain.
+ _.tap = function(obj, interceptor) {
+ interceptor(obj);
+ return obj;
+ };
+
+ // Internal recursive comparison function for `isEqual`.
+ var eq = function(a, b, aStack, bStack) {
+ // Identical objects are equal. `0 === -0`, but they aren't identical.
+ // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+ if (a === b) return a !== 0 || 1 / a === 1 / b;
+ // A strict comparison is necessary because `null == undefined`.
+ if (a == null || b == null) return a === b;
+ // Unwrap any wrapped objects.
+ if (a instanceof _) a = a._wrapped;
+ if (b instanceof _) b = b._wrapped;
+ // Compare `[[Class]]` names.
+ var className = toString.call(a);
+ if (className !== toString.call(b)) return false;
+ switch (className) {
+ // Strings, numbers, regular expressions, dates, and booleans are compared by value.
+ case '[object RegExp]':
+ // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
+ case '[object String]':
+ // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+ // equivalent to `new String("5")`.
+ return '' + a === '' + b;
+ case '[object Number]':
+ // `NaN`s are equivalent, but non-reflexive.
+ // Object(NaN) is equivalent to NaN
+ if (+a !== +a) return +b !== +b;
+ // An `egal` comparison is performed for other numeric values.
+ return +a === 0 ? 1 / +a === 1 / b : +a === +b;
+ case '[object Date]':
+ case '[object Boolean]':
+ // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+ // millisecond representations. Note that invalid dates with millisecond representations
+ // of `NaN` are not equivalent.
+ return +a === +b;
+ }
+ if (typeof a != 'object' || typeof b != 'object') return false;
+ // Assume equality for cyclic structures. The algorithm for detecting cyclic
+ // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+ var length = aStack.length;
+ while (length--) {
+ // Linear search. Performance is inversely proportional to the number of
+ // unique nested structures.
+ if (aStack[length] === a) return bStack[length] === b;
+ }
+ // Objects with different constructors are not equivalent, but `Object`s
+ // from different frames are.
+ var aCtor = a.constructor, bCtor = b.constructor;
+ if (
+ aCtor !== bCtor &&
+ // Handle Object.create(x) cases
+ 'constructor' in a && 'constructor' in b &&
+ !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
+ _.isFunction(bCtor) && bCtor instanceof bCtor)
+ ) {
+ return false;
+ }
+ // Add the first object to the stack of traversed objects.
+ aStack.push(a);
+ bStack.push(b);
+ var size, result;
+ // Recursively compare objects and arrays.
+ if (className === '[object Array]') {
+ // Compare array lengths to determine if a deep comparison is necessary.
+ size = a.length;
+ result = size === b.length;
+ if (result) {
+ // Deep compare the contents, ignoring non-numeric properties.
+ while (size--) {
+ if (!(result = eq(a[size], b[size], aStack, bStack))) break;
+ }
+ }
+ } else {
+ // Deep compare objects.
+ var keys = _.keys(a), key;
+ size = keys.length;
+ // Ensure that both objects contain the same number of properties before comparing deep equality.
+ result = _.keys(b).length === size;
+ if (result) {
+ while (size--) {
+ // Deep compare each member
+ key = keys[size];
+ if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
+ }
+ }
+ }
+ // Remove the first object from the stack of traversed objects.
+ aStack.pop();
+ bStack.pop();
+ return result;
+ };
+
+ // Perform a deep comparison to check if two objects are equal.
+ _.isEqual = function(a, b) {
+ return eq(a, b, [], []);
+ };
+
+ // Is a given array, string, or object empty?
+ // An "empty" object has no enumerable own-properties.
+ _.isEmpty = function(obj) {
+ if (obj == null) return true;
+ if (_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0;
+ for (var key in obj) if (_.has(obj, key)) return false;
+ return true;
+ };
+
+ // Is a given value a DOM element?
+ _.isElement = function(obj) {
+ return !!(obj && obj.nodeType === 1);
+ };
+
+ // Is a given value an array?
+ // Delegates to ECMA5's native Array.isArray
+ _.isArray = nativeIsArray || function(obj) {
+ return toString.call(obj) === '[object Array]';
+ };
+
+ // Is a given variable an object?
+ _.isObject = function(obj) {
+ var type = typeof obj;
+ return type === 'function' || type === 'object' && !!obj;
+ };
+
+ // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
+ _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
+ _['is' + name] = function(obj) {
+ return toString.call(obj) === '[object ' + name + ']';
+ };
+ });
+
+ // Define a fallback version of the method in browsers (ahem, IE), where
+ // there isn't any inspectable "Arguments" type.
+ if (!_.isArguments(arguments)) {
+ _.isArguments = function(obj) {
+ return _.has(obj, 'callee');
+ };
+ }
+
+ // Optimize `isFunction` if appropriate. Work around an IE 11 bug.
+ if (typeof /./ !== 'function') {
+ _.isFunction = function(obj) {
+ return typeof obj == 'function' || false;
+ };
+ }
+
+ // Is a given object a finite number?
+ _.isFinite = function(obj) {
+ return isFinite(obj) && !isNaN(parseFloat(obj));
+ };
+
+ // Is the given value `NaN`? (NaN is the only number which does not equal itself).
+ _.isNaN = function(obj) {
+ return _.isNumber(obj) && obj !== +obj;
+ };
+
+ // Is a given value a boolean?
+ _.isBoolean = function(obj) {
+ return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
+ };
+
+ // Is a given value equal to null?
+ _.isNull = function(obj) {
+ return obj === null;
+ };
+
+ // Is a given variable undefined?
+ _.isUndefined = function(obj) {
+ return obj === void 0;
+ };
+
+ // Shortcut function for checking if an object has a given property directly
+ // on itself (in other words, not on a prototype).
+ _.has = function(obj, key) {
+ return obj != null && hasOwnProperty.call(obj, key);
+ };
+
+ // Utility Functions
+ // -----------------
+
+ // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+ // previous owner. Returns a reference to the Underscore object.
+ _.noConflict = function() {
+ root._ = previousUnderscore;
+ return this;
+ };
+
+ // Keep the identity function around for default iteratees.
+ _.identity = function(value) {
+ return value;
+ };
+
+ // Predicate-generating functions. Often useful outside of Underscore.
+ _.constant = function(value) {
+ return function() {
+ return value;
+ };
+ };
+
+ _.noop = function(){};
+
+ _.property = function(key) {
+ return function(obj) {
+ return obj[key];
+ };
+ };
+
+ // Returns a predicate for checking whether an object has a given set of `key:value` pairs.
+ _.matches = function(attrs) {
+ var pairs = _.pairs(attrs), length = pairs.length;
+ return function(obj) {
+ if (obj == null) return !length;
+ obj = new Object(obj);
+ for (var i = 0; i < length; i++) {
+ var pair = pairs[i], key = pair[0];
+ if (pair[1] !== obj[key] || !(key in obj)) return false;
+ }
+ return true;
+ };
+ };
+
+ // Run a function **n** times.
+ _.times = function(n, iteratee, context) {
+ var accum = Array(Math.max(0, n));
+ iteratee = createCallback(iteratee, context, 1);
+ for (var i = 0; i < n; i++) accum[i] = iteratee(i);
+ return accum;
+ };
+
+ // Return a random integer between min and max (inclusive).
+ _.random = function(min, max) {
+ if (max == null) {
+ max = min;
+ min = 0;
+ }
+ return min + Math.floor(Math.random() * (max - min + 1));
+ };
+
+ // A (possibly faster) way to get the current timestamp as an integer.
+ _.now = Date.now || function() {
+ return new Date().getTime();
+ };
+
+ // List of HTML entities for escaping.
+ var escapeMap = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": ''',
+ '`': '`'
+ };
+ var unescapeMap = _.invert(escapeMap);
+
+ // Functions for escaping and unescaping strings to/from HTML interpolation.
+ var createEscaper = function(map) {
+ var escaper = function(match) {
+ return map[match];
+ };
+ // Regexes for identifying a key that needs to be escaped
+ var source = '(?:' + _.keys(map).join('|') + ')';
+ var testRegexp = RegExp(source);
+ var replaceRegexp = RegExp(source, 'g');
+ return function(string) {
+ string = string == null ? '' : '' + string;
+ return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
+ };
+ };
+ _.escape = createEscaper(escapeMap);
+ _.unescape = createEscaper(unescapeMap);
+
+ // If the value of the named `property` is a function then invoke it with the
+ // `object` as context; otherwise, return it.
+ _.result = function(object, property) {
+ if (object == null) return void 0;
+ var value = object[property];
+ return _.isFunction(value) ? object[property]() : value;
+ };
+
+ // Generate a unique integer id (unique within the entire client session).
+ // Useful for temporary DOM ids.
+ var idCounter = 0;
+ _.uniqueId = function(prefix) {
+ var id = ++idCounter + '';
+ return prefix ? prefix + id : id;
+ };
+
+ // By default, Underscore uses ERB-style template delimiters, change the
+ // following template settings to use alternative delimiters.
+ _.templateSettings = {
+ evaluate : /<%([\s\S]+?)%>/g,
+ interpolate : /<%=([\s\S]+?)%>/g,
+ escape : /<%-([\s\S]+?)%>/g
+ };
+
+ // When customizing `templateSettings`, if you don't want to define an
+ // interpolation, evaluation or escaping regex, we need one that is
+ // guaranteed not to match.
+ var noMatch = /(.)^/;
+
+ // Certain characters need to be escaped so that they can be put into a
+ // string literal.
+ var escapes = {
+ "'": "'",
+ '\\': '\\',
+ '\r': 'r',
+ '\n': 'n',
+ '\u2028': 'u2028',
+ '\u2029': 'u2029'
+ };
+
+ var escaper = /\\|'|\r|\n|\u2028|\u2029/g;
+
+ var escapeChar = function(match) {
+ return '\\' + escapes[match];
+ };
+
+ // JavaScript micro-templating, similar to John Resig's implementation.
+ // Underscore templating handles arbitrary delimiters, preserves whitespace,
+ // and correctly escapes quotes within interpolated code.
+ // NB: `oldSettings` only exists for backwards compatibility.
+ _.template = function(text, settings, oldSettings) {
+ if (!settings && oldSettings) settings = oldSettings;
+ settings = _.defaults({}, settings, _.templateSettings);
+
+ // Combine delimiters into one regular expression via alternation.
+ var matcher = RegExp([
+ (settings.escape || noMatch).source,
+ (settings.interpolate || noMatch).source,
+ (settings.evaluate || noMatch).source
+ ].join('|') + '|$', 'g');
+
+ // Compile the template source, escaping string literals appropriately.
+ var index = 0;
+ var source = "__p+='";
+ text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
+ source += text.slice(index, offset).replace(escaper, escapeChar);
+ index = offset + match.length;
+
+ if (escape) {
+ source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
+ } else if (interpolate) {
+ source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
+ } else if (evaluate) {
+ source += "';\n" + evaluate + "\n__p+='";
+ }
+
+ // Adobe VMs need the match returned to produce the correct offest.
+ return match;
+ });
+ source += "';\n";
+
+ // If a variable is not specified, place data values in local scope.
+ if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+ source = "var __t,__p='',__j=Array.prototype.join," +
+ "print=function(){__p+=__j.call(arguments,'');};\n" +
+ source + 'return __p;\n';
+
+ try {
+ var render = new Function(settings.variable || 'obj', '_', source);
+ } catch (e) {
+ e.source = source;
+ throw e;
+ }
+
+ var template = function(data) {
+ return render.call(this, data, _);
+ };
+
+ // Provide the compiled source as a convenience for precompilation.
+ var argument = settings.variable || 'obj';
+ template.source = 'function(' + argument + '){\n' + source + '}';
+
+ return template;
+ };
+
+ // Add a "chain" function. Start chaining a wrapped Underscore object.
+ _.chain = function(obj) {
+ var instance = _(obj);
+ instance._chain = true;
+ return instance;
+ };
+
+ // OOP
+ // ---------------
+ // If Underscore is called as a function, it returns a wrapped object that
+ // can be used OO-style. This wrapper holds altered versions of all the
+ // underscore functions. Wrapped objects may be chained.
+
+ // Helper function to continue chaining intermediate results.
+ var result = function(obj) {
+ return this._chain ? _(obj).chain() : obj;
+ };
+
+ // Add your own custom functions to the Underscore object.
+ _.mixin = function(obj) {
+ _.each(_.functions(obj), function(name) {
+ var func = _[name] = obj[name];
+ _.prototype[name] = function() {
+ var args = [this._wrapped];
+ push.apply(args, arguments);
+ return result.call(this, func.apply(_, args));
+ };
+ });
+ };
+
+ // Add all of the Underscore functions to the wrapper object.
+ _.mixin(_);
+
+ // Add all mutator Array functions to the wrapper.
+ _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+ var method = ArrayProto[name];
+ _.prototype[name] = function() {
+ var obj = this._wrapped;
+ method.apply(obj, arguments);
+ if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
+ return result.call(this, obj);
+ };
+ });
+
+ // Add all accessor Array functions to the wrapper.
+ _.each(['concat', 'join', 'slice'], function(name) {
+ var method = ArrayProto[name];
+ _.prototype[name] = function() {
+ return result.call(this, method.apply(this._wrapped, arguments));
+ };
+ });
+
+ // Extracts the result from a wrapped and chained object.
+ _.prototype.value = function() {
+ return this._wrapped;
+ };
+
+ // AMD registration happens at the end for compatibility with AMD loaders
+ // that may not enforce next-turn semantics on modules. Even though general
+ // practice for AMD registration is to be anonymous, underscore registers
+ // as a named module because, like jQuery, it is a base library that is
+ // popular enough to be bundled in a third party lib, but not be part of
+ // an AMD load request. Those cases could generate an error when an
+ // anonymous define() is called outside of a loader request.
+ if (typeof define === 'function' && define.amd) {
+ define('underscore', [], function() {
+ return _;
+ });
+ }
+}.call(this));
\ No newline at end of file
diff --git a/htrace-core/src/web/lib/js/underscore-1.7.0.min.js b/htrace-core/src/web/lib/js/underscore-1.7.0.min.js
deleted file mode 100644
index 9c71c0e..0000000
--- a/htrace-core/src/web/lib/js/underscore-1.7.0.min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// Underscore.js 1.7.0
-// http://underscorejs.org
-// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
-// Underscore may be freely distributed under the MIT license.
-(function(){var n=this,t=n._,r=Array.prototype,e=Object.prototype,u=Function.prototype,i=r.push,a=r.slice,o=r.concat,l=e.toString,c=e.hasOwnProperty,f=Array.isArray,s=Object.keys,p=u.bind,h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=h),exports._=h):n._=h,h.VERSION="1.7.0";var g=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}};h.iteratee=function(n,t,r){return null==n?h.identity:h.isFunction(n)?g(n,t,r):h.isObject(n)?h.matches(n):h.property(n)},h.each=h.forEach=function(n,t,r){if(null==n)return n;t=g(t,r);var e,u=n.length;if(u===+u)for(e=0;u>e;e++)t(n[e],e,n);else{var i=h.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},h.map=h.collect=function(n,t,r){if(null==n)return[];t=h.iteratee(t,r);for(var e,u=n.length!==+n.length&&h.keys(n),i=(u||n).length,a=Array(i),o=0;i>o;o++)e=u?u[o]:o,a[o]=t(n[e],e,n);return a};var v="Reduce of empty array with no initial value";h.reduce=h.foldl=h.inject=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length,o=0;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[o++]:o++]}for(;a>o;o++)u=i?i[o]:o,r=t(r,n[u],u,n);return r},h.reduceRight=h.foldr=function(n,t,r,e){null==n&&(n=[]),t=g(t,e,4);var u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;if(arguments.length<3){if(!a)throw new TypeError(v);r=n[i?i[--a]:--a]}for(;a--;)u=i?i[a]:a,r=t(r,n[u],u,n);return r},h.find=h.detect=function(n,t,r){var e;return t=h.iteratee(t,r),h.some(n,function(n,r,u){return t(n,r,u)?(e=n,!0):void 0}),e},h.filter=h.select=function(n,t,r){var e=[];return null==n?e:(t=h.iteratee(t,r),h.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e)},h.reject=function(n,t,r){return h.filter(n,h.negate(h.iteratee(t)),r)},h.every=h.all=function(n,t,r){if(null==n)return!0;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,!t(n[u],u,n))return!1;return!0},h.some=h.any=function(n,t,r){if(null==n)return!1;t=h.iteratee(t,r);var e,u,i=n.length!==+n.length&&h.keys(n),a=(i||n).length;for(e=0;a>e;e++)if(u=i?i[e]:e,t(n[u],u,n))return!0;return!1},h.contains=h.include=function(n,t){return null==n?!1:(n.length!==+n.length&&(n=h.values(n)),h.indexOf(n,t)>=0)},h.invoke=function(n,t){var r=a.call(arguments,2),e=h.isFunction(t);return h.map(n,function(n){return(e?t:n[t]).apply(n,r)})},h.pluck=function(n,t){return h.map(n,h.property(t))},h.where=function(n,t){return h.filter(n,h.matches(t))},h.findWhere=function(n,t){return h.find(n,h.matches(t))},h.max=function(n,t,r){var e,u,i=-1/0,a=-1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],e>i&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(u>a||u===-1/0&&i===-1/0)&&(i=n,a=u)});return i},h.min=function(n,t,r){var e,u,i=1/0,a=1/0;if(null==t&&null!=n){n=n.length===+n.length?n:h.values(n);for(var o=0,l=n.length;l>o;o++)e=n[o],i>e&&(i=e)}else t=h.iteratee(t,r),h.each(n,function(n,r,e){u=t(n,r,e),(a>u||1/0===u&&1/0===i)&&(i=n,a=u)});return i},h.shuffle=function(n){for(var t,r=n&&n.length===+n.length?n:h.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=h.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},h.sample=function(n,t,r){return null==t||r?(n.length!==+n.length&&(n=h.values(n)),n[h.random(n.length-1)]):h.shuffle(n).slice(0,Math.max(0,t))},h.sortBy=function(n,t,r){return t=h.iteratee(t,r),h.pluck(h.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var m=function(n){return function(t,r,e){var u={};return r=h.iteratee(r,e),h.each(t,function(e,i){var a=r(e,i,t);n(u,e,a)}),u}};h.groupBy=m(function(n,t,r){h.has(n,r)?n[r].push(t):n[r]=[t]}),h.indexBy=m(function(n,t,r){n[r]=t}),h.countBy=m(function(n,t,r){h.has(n,r)?n[r]++:n[r]=1}),h.sortedIndex=function(n,t,r,e){r=h.iteratee(r,e,1);for(var u=r(t),i=0,a=n.length;a>i;){var o=i+a>>>1;r(n[o])<u?i=o+1:a=o}return i},h.toArray=function(n){return n?h.isArray(n)?a.call(n):n.length===+n.length?h.map(n,h.identity):h.values(n):[]},h.size=function(n){return null==n?0:n.length===+n.length?n.length:h.keys(n).length},h.partition=function(n,t,r){t=h.iteratee(t,r);var e=[],u=[];return h.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},h.first=h.head=h.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:0>t?[]:a.call(n,0,t)},h.initial=function(n,t,r){return a.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},h.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:a.call(n,Math.max(n.length-t,0))},h.rest=h.tail=h.drop=function(n,t,r){return a.call(n,null==t||r?1:t)},h.compact=function(n){return h.filter(n,h.identity)};var y=function(n,t,r,e){if(t&&h.every(n,h.isArray))return o.apply(e,n);for(var u=0,a=n.length;a>u;u++){var l=n[u];h.isArray(l)||h.isArguments(l)?t?i.apply(e,l):y(l,t,r,e):r||e.push(l)}return e};h.flatten=function(n,t){return y(n,t,!1,[])},h.without=function(n){return h.difference(n,a.call(arguments,1))},h.uniq=h.unique=function(n,t,r,e){if(null==n)return[];h.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=h.iteratee(r,e));for(var u=[],i=[],a=0,o=n.length;o>a;a++){var l=n[a];if(t)a&&i===l||u.push(l),i=l;else if(r){var c=r(l,a,n);h.indexOf(i,c)<0&&(i.push(c),u.push(l))}else h.indexOf(u,l)<0&&u.push(l)}return u},h.union=function(){return h.uniq(y(arguments,!0,!0,[]))},h.intersection=function(n){if(null==n)return[];for(var t=[],r=arguments.length,e=0,u=n.length;u>e;e++){var i=n[e];if(!h.contains(t,i)){for(var a=1;r>a&&h.contains(arguments[a],i);a++);a===r&&t.push(i)}}return t},h.difference=function(n){var t=y(a.call(arguments,1),!0,!0,[]);return h.filter(n,function(n){return!h.contains(t,n)})},h.zip=function(n){if(null==n)return[];for(var t=h.max(arguments,"length").length,r=Array(t),e=0;t>e;e++)r[e]=h.pluck(arguments,e);return r},h.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},h.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=h.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}for(;u>e;e++)if(n[e]===t)return e;return-1},h.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=n.length;for("number"==typeof r&&(e=0>r?e+r+1:Math.min(e,r+1));--e>=0;)if(n[e]===t)return e;return-1},h.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var d=function(){};h.bind=function(n,t){var r,e;if(p&&n.bind===p)return p.apply(n,a.call(arguments,1));if(!h.isFunction(n))throw new TypeError("Bind must be called on a function");return r=a.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(a.call(arguments)));d.prototype=n.prototype;var u=new d;d.prototype=null;var i=n.apply(u,r.concat(a.call(arguments)));return h.isObject(i)?i:u}},h.partial=function(n){var t=a.call(arguments,1);return function(){for(var r=0,e=t.slice(),u=0,i=e.length;i>u;u++)e[u]===h&&(e[u]=arguments[r++]);for(;r<arguments.length;)e.push(arguments[r++]);return n.apply(this,e)}},h.bindAll=function(n){var t,r,e=arguments.length;if(1>=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=h.bind(n[r],n);return n},h.memoize=function(n,t){var r=function(e){var u=r.cache,i=t?t.apply(this,arguments):e;return h.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},h.delay=function(n,t){var r=a.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},h.defer=function(n){return h.delay.apply(h,[n,1].concat(a.call(arguments,1)))},h.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var l=function(){o=r.leading===!1?0:h.now(),a=null,i=n.apply(e,u),a||(e=u=null)};return function(){var c=h.now();o||r.leading!==!1||(o=c);var f=t-(c-o);return e=this,u=arguments,0>=f||f>t?(clearTimeout(a),a=null,o=c,i=n.apply(e,u),a||(e=u=null)):a||r.trailing===!1||(a=setTimeout(l,f)),i}},h.debounce=function(n,t,r){var e,u,i,a,o,l=function(){var c=h.now()-a;t>c&&c>0?e=setTimeout(l,t-c):(e=null,r||(o=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,a=h.now();var c=r&&!e;return e||(e=setTimeout(l,t)),c&&(o=n.apply(i,u),i=u=null),o}},h.wrap=function(n,t){return h.partial(t,n)},h.negate=function(n){return function(){return!n.apply(this,arguments)}},h.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},h.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},h.before=function(n,t){var r;return function(){return--n>0?r=t.apply(this,arguments):t=null,r}},h.once=h.partial(h.before,2),h.keys=function(n){if(!h.isObject(n))return[];if(s)return s(n);var t=[];for(var r in n)h.has(n,r)&&t.push(r);return t},h.values=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},h.pairs=function(n){for(var t=h.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},h.invert=function(n){for(var t={},r=h.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},h.functions=h.methods=function(n){var t=[];for(var r in n)h.isFunction(n[r])&&t.push(r);return t.sort()},h.extend=function(n){if(!h.isObject(n))return n;for(var t,r,e=1,u=arguments.length;u>e;e++){t=arguments[e];for(r in t)c.call(t,r)&&(n[r]=t[r])}return n},h.pick=function(n,t,r){var e,u={};if(null==n)return u;if(h.isFunction(t)){t=g(t,r);for(e in n){var i=n[e];t(i,e,n)&&(u[e]=i)}}else{var l=o.apply([],a.call(arguments,1));n=new Object(n);for(var c=0,f=l.length;f>c;c++)e=l[c],e in n&&(u[e]=n[e])}return u},h.omit=function(n,t,r){if(h.isFunction(t))t=h.negate(t);else{var e=h.map(o.apply([],a.call(arguments,1)),String);t=function(n,t){return!h.contains(e,t)}}return h.pick(n,t,r)},h.defaults=function(n){if(!h.isObject(n))return n;for(var t=1,r=arguments.length;r>t;t++){var e=arguments[t];for(var u in e)n[u]===void 0&&(n[u]=e[u])}return n},h.clone=function(n){return h.isObject(n)?h.isArray(n)?n.slice():h.extend({},n):n},h.tap=function(n,t){return t(n),n};var b=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof h&&(n=n._wrapped),t instanceof h&&(t=t._wrapped);var u=l.call(n);if(u!==l.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]===n)return e[i]===t;var a=n.constructor,o=t.constructor;if(a!==o&&"constructor"in n&&"constructor"in t&&!(h.isFunction(a)&&a instanceof a&&h.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c,f;if("[object Array]"===u){if(c=n.length,f=c===t.length)for(;c--&&(f=b(n[c],t[c],r,e)););}else{var s,p=h.keys(n);if(c=p.length,f=h.keys(t).length===c)for(;c--&&(s=p[c],f=h.has(t,s)&&b(n[s],t[s],r,e)););}return r.pop(),e.pop(),f};h.isEqual=function(n,t){return b(n,t,[],[])},h.isEmpty=function(n){if(null==n)return!0;if(h.isArray(n)||h.isString(n)||h.isArguments(n))return 0===n.length;for(var t in n)if(h.has(n,t))return!1;return!0},h.isElement=function(n){return!(!n||1!==n.nodeType)},h.isArray=f||function(n){return"[object Array]"===l.call(n)},h.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},h.each(["Arguments","Function","String","Number","Date","RegExp"],function(n){h["is"+n]=function(t){return l.call(t)==="[object "+n+"]"}}),h.isArguments(arguments)||(h.isArguments=function(n){return h.has(n,"callee")}),"function"!=typeof/./&&(h.isFunction=function(n){return"function"==typeof n||!1}),h.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},h.isNaN=function(n){return h.isNumber(n)&&n!==+n},h.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===l.call(n)},h.isNull=function(n){return null===n},h.isUndefined=function(n){return n===void 0},h.has=function(n,t){return null!=n&&c.call(n,t)},h.noConflict=function(){return n._=t,this},h.identity=function(n){return n},h.constant=function(n){return function(){return n}},h.noop=function(){},h.property=function(n){return function(t){return t[n]}},h.matches=function(n){var t=h.pairs(n),r=t.length;return function(n){if(null==n)return!r;n=new Object(n);for(var e=0;r>e;e++){var u=t[e],i=u[0];if(u[1]!==n[i]||!(i in n))return!1}return!0}},h.times=function(n,t,r){var e=Array(Math.max(0,n));t=g(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},h.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},h.now=Date.now||function(){return(new Date).getTime()};var _={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},w=h.invert(_),j=function(n){var t=function(t){return n[t]},r="(?:"+h.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=j(_),h.unescape=j(w),h.result=function(n,t){if(null==n)return void 0;var r=n[t];return h.isFunction(r)?n[t]():r};var x=0;h.uniqueId=function(n){var t=++x+"";return n?n+t:t},h.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var A=/(.)^/,k={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},O=/\\|'|\r|\n|\u2028|\u2029/g,F=function(n){return"\\"+k[n]};h.template=function(n,t,r){!t&&r&&(t=r),t=h.defaults({},t,h.templateSettings);var e=RegExp([(t.escape||A).source,(t.interpolate||A).source,(t.evaluate||A).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(O,F),u=o+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":a&&(i+="';\n"+a+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=new Function(t.variable||"obj","_",i)}catch(o){throw o.source=i,o}var l=function(n){return a.call(this,n,h)},c=t.variable||"obj";return l.source="function("+c+"){\n"+i+"}",l},h.chain=function(n){var t=h(n);return t._chain=!0,t};var E=function(n){return this._chain?h(n).chain():n};h.mixin=function(n){h.each(h.functions(n),function(t){var r=h[t]=n[t];h.prototype[t]=function(){var n=[this._wrapped];return i.apply(n,arguments),E.call(this,r.apply(h,n))}})},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=r[n];h.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],E.call(this,r)}}),h.each(["concat","join","slice"],function(n){var t=r[n];h.prototype[n]=function(){return E.call(this,t.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}).call(this);
-//# sourceMappingURL=underscore-min.map