From 76e06cfb021bee2ae195d10a0c2282d567274402 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Thu, 2 Jan 2014 13:46:53 +0000 Subject: [PATCH] Issue #1800022 by Wim Leers, JohnAlbin, herom: Update Backbone and Underscore. --- core/assets/vendor/backbone/backbone.js | 1533 +++++++++-------- core/assets/vendor/underscore/underscore.js | 379 ++-- core/modules/ckeditor/js/ckeditor.admin.js | 8 +- core/modules/contextual/js/contextual.js | 8 +- .../contextual/js/contextual.toolbar.js | 118 +- core/modules/edit/edit.module | 1 + core/modules/edit/js/edit.js | 18 +- core/modules/edit/js/models/BaseModel.js | 41 + core/modules/edit/js/models/EntityModel.js | 13 +- core/modules/edit/js/models/FieldModel.js | 7 +- core/modules/edit/js/views/AppView.js | 30 +- .../edit/js/views/ContextualLinkView.js | 2 +- core/modules/edit/js/views/EditorView.js | 3 +- .../edit/js/views/EntityDecorationView.js | 2 +- .../edit/js/views/EntityToolbarView.js | 8 +- .../edit/js/views/FieldDecorationView.js | 4 +- .../modules/edit/js/views/FieldToolbarView.js | 2 +- core/modules/system/system.module | 4 +- core/modules/toolbar/js/toolbar.js | 14 +- core/modules/tour/js/tour.js | 4 +- 20 files changed, 1243 insertions(+), 956 deletions(-) create mode 100644 core/modules/edit/js/models/BaseModel.js diff --git a/core/assets/vendor/backbone/backbone.js b/core/assets/vendor/backbone/backbone.js index 418fc7d207b4..3fec40351aac 100644 --- a/core/assets/vendor/backbone/backbone.js +++ b/core/assets/vendor/backbone/backbone.js @@ -1,6 +1,7 @@ -// Backbone.js 0.9.2 +// Backbone.js 1.1.0 -// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://backbonejs.org @@ -10,7 +11,7 @@ // Initial Setup // ------------- - // Save a reference to the global object (`window` in the browser, `global` + // Save a reference to the global object (`window` in the browser, `exports` // on the server). var root = this; @@ -18,14 +19,14 @@ // restored later on, if `noConflict` is used. var previousBackbone = root.Backbone; - // Create a local reference to array methods. - var ArrayProto = Array.prototype; - var push = ArrayProto.push; - var slice = ArrayProto.slice; - var splice = ArrayProto.splice; + // 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; // The top-level namespace. All public Backbone classes and modules will - // be attached to this. Exported for both CommonJS and the browser. + // be attached to this. Exported for both the browser and the server. var Backbone; if (typeof exports !== 'undefined') { Backbone = exports; @@ -34,14 +35,15 @@ } // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '0.9.2'; + Backbone.VERSION = '1.1.0'; // Require Underscore, if we're on the server, and it's not already present. var _ = root._; if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); - // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable. - Backbone.$ = root.jQuery || root.Zepto || root.ender; + // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable // to its previous owner. Returns a reference to this Backbone object. @@ -51,7 +53,7 @@ }; // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option - // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and // set a `X-Http-Method-Override` header. Backbone.emulateHTTP = false; @@ -62,14 +64,12 @@ Backbone.emulateJSON = false; // Backbone.Events - // ----------------- - - // Regular expression used to split event strings - var eventSplitter = /\s+/; + // --------------- // 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. + // 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); @@ -78,49 +78,55 @@ // var Events = Backbone.Events = { - // Bind one or more space separated events, `events`, to a `callback` - // function. Passing `"all"` will bind the callback to all events fired. - on: function(events, callback, context) { - var calls, event, list; - if (!callback) return this; - - events = events.split(eventSplitter); - calls = this._callbacks || (this._callbacks = {}); - - while (event = events.shift()) { - list = calls[event] || (calls[event] = []); - list.push(callback, context); - } - + // 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; }, - // 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 `events` is null, removes all bound callbacks for all events. - off: function(events, callback, context) { - var event, calls, list, i; - - // No events, or removing *all* events. - if (!(calls = this._callbacks)) return this; - if (!(events || callback || context)) { - delete this._callbacks; + // 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 = {}; return this; } - - events = events ? events.split(eventSplitter) : _.keys(calls); - - // Loop through the callback list, splicing where appropriate. - while (event = events.shift()) { - if (!(list = calls[event]) || !(callback || context)) { - delete calls[event]; - continue; - } - - for (i = list.length - 2; i >= 0; i -= 2) { - if (!(callback && list[i] !== callback || context && list[i + 1] !== context)) { - list.splice(i, 2); + 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]; } } @@ -131,76 +137,122 @@ // 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(events) { - var event, calls, list, i, length, args, all, rest; - if (!(calls = this._callbacks)) return this; - - rest = []; - events = events.split(eventSplitter); + 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; + }, - // Fill up `rest` with the callback arguments. Since we're only copying - // the tail of `arguments`, a loop is much faster than Array#slice. - for (i = 1, length = arguments.length; i < length; i++) { - rest[i - 1] = arguments[i]; + // 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; + } - // For each event, walk through the list of callbacks twice, first to - // trigger the event, then to trigger any `"all"` callbacks. - while (event = events.shift()) { - // Copy callback lists to prevent modification. - if (all = calls.all) all = all.slice(); - if (list = calls[event]) list = list.slice(); - - // Execute event callbacks. - if (list) { - for (i = 0, length = list.length; i < length; i += 2) { - list[i].apply(list[i + 1] || this, rest); - } - } + }; - // Execute "all" callbacks. - if (all) { - args = [event].concat(rest); - for (i = 0, length = all.length; i < length; i += 2) { - all[i].apply(all[i + 1] || this, args); - } - } + // 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; + } - return this; + // 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); + } }; + 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 // -------------- - // Create a new model, with defined attributes. A client id (`cid`) + // 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 defaults; - attributes || (attributes = {}); - if (options && options.collection) this.collection = options.collection; - if (options && options.parse) attributes = this.parse(attributes); - if (defaults = _.result(this, 'defaults')) { - attributes = _.extend({}, defaults, attributes); - } - this.attributes = {}; - this._escapedAttributes = {}; + 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._silent = {}; - this._pending = {}; - this.set(attributes, {silent: true}); - // Reset change tracking. - this.changed = {}; - this._silent = {}; - this._pending = {}; - this._previousAttributes = _.clone(this.attributes); this.initialize.apply(this, arguments); }; @@ -210,13 +262,8 @@ // A hash of attributes whose current and previous value differ. changed: null, - // A hash of attributes that have silently changed since the last time - // `change` was called. Will become pending attributes on the next call. - _silent: null, - - // A hash of attributes that have changed since the last `'change'` event - // began. - _pending: 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"`. @@ -231,7 +278,8 @@ return _.clone(this.attributes); }, - // Proxy `Backbone.sync` by default. + // 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); }, @@ -243,10 +291,7 @@ // Get the HTML-escaped value of an attribute. escape: function(attr) { - var html; - if (html = this._escapedAttributes[attr]) return html; - var val = this.get(attr); - return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val); + return _.escape(this.get(attr)); }, // Returns `true` if the attribute contains a value that is not null @@ -255,146 +300,197 @@ return this.get(attr) != null; }, - // Set a hash of model attributes on the object, firing `"change"` unless - // you choose to silence it. - set: function(key, value, options) { - var attrs, attr, val; + // 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 (_.isObject(key) || key == null) { + if (typeof key === 'object') { attrs = key; - options = value; + options = val; } else { - attrs = {}; - attrs[key] = value; + (attrs = {})[key] = val; } - // Extract attributes and options. options || (options = {}); - if (!attrs) return this; - if (attrs instanceof Model) attrs = attrs.attributes; - if (options.unset) for (attr in attrs) attrs[attr] = void 0; // 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]; - var changes = options.changes = {}; - var now = this.attributes; - var escaped = this._escapedAttributes; - var prev = this._previousAttributes || {}; - - // For each `set` attribute... + // For each `set` attribute, update or delete the current value. for (attr in attrs) { val = attrs[attr]; - - // If the new and current value differ, record the change. - if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) { - delete escaped[attr]; - (options.silent ? this._silent : changes)[attr] = true; - } - - // Update or delete the current value. - options.unset ? delete now[attr] : now[attr] = val; - - // If the new and previous value differ, record the change. If not, - // then remove changes for this attribute. - if (!_.isEqual(prev[attr], val) || (_.has(now, attr) !== _.has(prev, attr))) { + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { this.changed[attr] = val; - if (!options.silent) this._pending[attr] = true; } else { delete this.changed[attr]; - delete this._pending[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; + for (var i = 0, l = changes.length; i < l; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); } } - // Fire the `"change"` events. - if (!options.silent) this.change(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) { + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; return this; }, - // Remove an attribute from the model, firing `"change"` unless you choose - // to silence it. `unset` is a noop if the attribute doesn't exist. + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. unset: function(attr, options) { - options = _.extend({}, options, {unset: true}); - return this.set(attr, null, options); + return this.set(attr, void 0, _.extend({}, options, {unset: true})); }, - // Clear all attributes on the model, firing `"change"` unless you choose - // to silence it. + // Clear all attributes on the model, firing `"change"`. clear: function(options) { - options = _.extend({}, options, {unset: true}); - return this.set(_.clone(this.attributes), 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 overriden, + // 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, status, xhr) { - if (!model.set(model.parse(resp, xhr), options)) return false; + 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, value, options) { - var attrs, current, done; + save: function(key, val, options) { + var attrs, method, xhr, attributes = this.attributes; - // Handle both `("key", value)` and `({key: value})` -style calls. - if (_.isObject(key) || key == null) { + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { attrs = key; - options = value; + options = val; } else { - attrs = {}; - attrs[key] = value; + (attrs = {})[key] = val; } - options = options ? _.clone(options) : {}; - // If we're "wait"-ing to set changed attributes, validate early. - if (options.wait) { + 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; - current = _.clone(this.attributes); } - // Regular saves `set` attributes before persisting to the server. - var silentOptions = _.extend({}, options, {silent: true}); - if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) { - return false; + // Set temporary attributes if `{wait: true}`. + if (attrs && options.wait) { + this.attributes = _.extend({}, attributes, attrs); } - // Do not persist invalid models. - if (!attrs && !this.isValid()) return false; - // After a successful server-side save, the client is (optionally) // updated with the server-side state. + if (options.parse === void 0) options.parse = true; var model = this; var success = options.success; - options.success = function(resp, status, xhr) { - done = true; - var serverAttrs = model.parse(resp, xhr); + 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 (!model.set(serverAttrs, options)) return false; + if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { + return false; + } if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); }; + wrapError(this, options); - // Finish configuring and sending the Ajax request. - var xhr = this.sync(this.isNew() ? 'create' : 'update', this, options); + method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch') options.attrs = attrs; + xhr = this.sync(method, this, options); - // When using `wait`, reset attributes to original values unless - // `success` has been called already. - if (!done && options.wait) { - this.clear(silentOptions); - this.set(current, silentOptions); - } + // Restore attributes. + if (attrs && options.wait) this.attributes = attributes; return xhr; }, @@ -414,12 +510,14 @@ 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(); @@ -437,7 +535,7 @@ // **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, xhr) { + parse: function(resp, options) { return resp; }, @@ -451,103 +549,48 @@ return this.id == null; }, - // Call this method to manually fire a `"change"` event for this model and - // a `"change:attribute"` event for each changed attribute. - // Calling this will cause all objects observing the model to update. - change: function(options) { - options || (options = {}); - var changing = this._changing; - this._changing = true; - - // Silent changes become pending changes. - for (var attr in this._silent) this._pending[attr] = true; - - // Silent changes are triggered. - var changes = _.extend({}, options.changes, this._silent); - this._silent = {}; - for (var attr in changes) { - this.trigger('change:' + attr, this, this.get(attr), options); - } - if (changing) return this; - - // Continue firing `"change"` events while there are pending changes. - while (!_.isEmpty(this._pending)) { - this._pending = {}; - this.trigger('change', this, options); - // Pending and silent changes still remain. - for (var attr in this.changed) { - if (this._pending[attr] || this._silent[attr]) continue; - delete this.changed[attr]; - } - this._previousAttributes = _.clone(this.attributes); - } - - this._changing = false; - return this; - }, - - // 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, old = this._previousAttributes; - 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); - }, - - // Check if the model is currently in a valid state. It's only possible to - // get into an *invalid* state if you're using silent changes. + // Check if the model is currently in a valid state. isValid: function(options) { - return !this.validate || !this.validate(this.attributes, options); + return this._validate({}, _.extend(options || {}, { validate: true })); }, // Run validation against the next complete set of model attributes, - // returning `true` if all is well. If a specific `error` callback has - // been passed, call that instead of firing the general `"error"` event. + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. _validate: function(attrs, options) { - if (options.silent || !this.validate) return true; + if (!options.validate || !this.validate) return true; attrs = _.extend({}, this.attributes, attrs); - var error = this.validate(attrs, options); + var error = this.validationError = this.validate(attrs, options) || null; if (!error) return true; - if (options && options.error) options.error(this, error, options); - this.trigger('error', this, error, options); + 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 // ------------------- - // Provides a standard collection class for our sets of models, ordered - // or unordered. If a `comparator` is specified, the Collection will maintain + // 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 = {}); @@ -555,12 +598,13 @@ if (options.comparator !== void 0) this.comparator = options.comparator; this._reset(); this.initialize.apply(this, arguments); - if (models) { - if (options.parse) models = this.parse(models); - this.reset(models, {silent: true, parse: options.parse}); - } + 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, { @@ -583,71 +627,22 @@ return Backbone.sync.apply(this, arguments); }, - // Add a model, or list of models to the set. Pass **silent** to avoid - // firing the `add` event for every new model. + // Add a model, or list of models to the set. add: function(models, options) { - var i, args, length, model, existing; - var at = options && options.at; - models = _.isArray(models) ? models.slice() : [models]; - - // Begin by turning bare objects into model references, and preventing - // invalid models from being added. - for (i = 0, length = models.length; i < length; i++) { - if (models[i] = this._prepareModel(models[i], options)) continue; - throw new Error("Can't add an invalid model to a collection"); - } - - for (i = models.length - 1; i >= 0; i--) { - model = models[i]; - existing = model.id != null && this._byId[model.id]; - - // If a duplicate is found, splice it out and optionally merge it into - // the existing model. - if (existing || this._byCid[model.cid]) { - if (options && options.merge && existing) { - existing.set(model, options); - } - models.splice(i, 1); - continue; - } - - // Listen to added models' events, and index models for lookup by - // `id` and by `cid`. - model.on('all', this._onModelEvent, this); - this._byCid[model.cid] = model; - if (model.id != null) this._byId[model.id] = model; - } - - // Update `length` and splice in new models. - this.length += models.length; - args = [at != null ? at : this.models.length, 0]; - push.apply(args, models); - splice.apply(this.models, args); - - // Sort the collection if appropriate. - if (this.comparator && at == null) this.sort({silent: true}); - - if (options && options.silent) return this; - - // Trigger `add` events. - while (model = models.shift()) { - model.trigger('add', model, this, options); - } - - return this; + return this.set(models, _.extend({merge: false}, options, addOptions)); }, - // Remove a model, or a list of models from the set. Pass silent to avoid - // firing the `remove` event for every model removed. + // Remove a model, or a list of models from the set. remove: function(models, options) { - var i, l, index, model; + var singular = !_.isArray(models); + models = singular ? [models] : _.clone(models); options || (options = {}); - models = _.isArray(models) ? models.slice() : [models]; + var i, l, index, model; for (i = 0, l = models.length; i < l; i++) { - model = this.getByCid(models[i]) || this.get(models[i]); + model = models[i] = this.get(models[i]); if (!model) continue; delete this._byId[model.id]; - delete this._byCid[model.cid]; + delete this._byId[model.cid]; index = this.indexOf(model); this.models.splice(index, 1); this.length--; @@ -657,14 +652,123 @@ } this._removeReference(model); } - return this; + 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]; + } + + // 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); + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + model.on('all', this._onModelEvent, this); + this._byId[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + if (order) order.push(existing || model); + } + + // 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.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) { - model = this._prepareModel(model, options); - this.add(model, options); - return model; + return this.add(model, _.extend({at: this.length}, options)); }, // Remove a model from the end of the collection. @@ -676,9 +780,7 @@ // Add a model to the beginning of the collection. unshift: function(model, options) { - model = this._prepareModel(model, options); - this.add(model, _.extend({at: 0}, options)); - return model; + return this.add(model, _.extend({at: 0}, options)); }, // Remove a model from the beginning of the collection. @@ -689,19 +791,14 @@ }, // Slice out a sub-array of models from the collection. - slice: function(begin, end) { - return this.models.slice(begin, end); + slice: function() { + return slice.apply(this.models, arguments); }, // Get a model from the set by id. - get: function(id) { - if (id == null) return void 0; - return this._byId[id.id != null ? id.id : id]; - }, - - // Get a model from the set by client id. - getByCid: function(cid) { - return cid && this._byCid[cid.cid || cid]; + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj.id] || this._byId[obj.cid] || this._byId[obj]; }, // Get the model at the given index. @@ -709,10 +806,11 @@ return this.models[index]; }, - // Return models with matching attributes. Useful for simple cases of `filter`. - where: function(attrs) { - if (_.isEmpty(attrs)) return []; - return this.filter(function(model) { + // 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; } @@ -720,21 +818,27 @@ }); }, + // 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'); - } + 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 || !options.silent) this.trigger('reset', this, options); + if (!options.silent) this.trigger('sort', this, options); return this; }, @@ -743,31 +847,21 @@ return _.invoke(this.models, 'get', attr); }, - // 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 `add` or `remove` events. Fires `reset` when finished. - reset: function(models, options) { - for (var i = 0, l = this.models.length; i < l; i++) { - this._removeReference(this.models[i]); - } - this._reset(); - if (models) this.add(models, _.extend({silent: true}, options)); - if (!options || !options.silent) this.trigger('reset', this, options); - return this; - }, - // Fetch the default set of models for this collection, resetting the - // collection when they arrive. If `add: true` is passed, appends the - // models to the collection instead of resetting. + // 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 collection = this; var success = options.success; - options.success = function(resp, status, xhr) { - collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); + 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); }, @@ -775,11 +869,10 @@ // collection immediately, unless `wait: true` is passed, in which case we // wait for the server to agree. create: function(model, options) { - var collection = this; options = options ? _.clone(options) : {}; - model = this._prepareModel(model, options); - if (!model) return false; - if (!options.wait) collection.add(model, 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, options) { if (options.wait) collection.add(model, options); @@ -791,7 +884,7 @@ // **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, xhr) { + parse: function(resp, options) { return resp; }, @@ -800,35 +893,30 @@ return new this.constructor(this.models); }, - // Proxy to _'s chain. Can't be proxied the same way the rest of the - // underscore methods are proxied because it relies on the underscore - // constructor. - chain: function() { - return _(this.models).chain(); - }, - - // Reset all internal state. Called when the collection is reset. - _reset: function(options) { + // 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 = {}; - this._byCid = {}; }, - // Prepare a model or hash of attributes to be added to this collection. + // Prepare a hash of attributes (or other model) to be added to this + // collection. _prepareModel: function(attrs, options) { if (attrs instanceof Model) { if (!attrs.collection) attrs.collection = this; return attrs; } - options || (options = {}); + options = options ? _.clone(options) : {}; options.collection = this; var model = new this.model(attrs, options); - if (!model._validate(model.attributes, options)) return false; - return model; + if (!model.validationError) return model; + this.trigger('invalid', this, model.validationError, options); + return false; }, - // Internal method to remove a model's ties to a collection. + // Internal method to sever a model's ties to a collection. _removeReference: function(model) { if (this === model.collection) delete model.collection; model.off('all', this._onModelEvent, this); @@ -851,12 +939,14 @@ }); // 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', 'sortedIndex', 'toArray', 'size', 'first', 'head', 'take', - 'initial', 'rest', 'tail', 'last', 'without', 'indexOf', 'shuffle', - 'lastIndexOf', 'isEmpty']; + 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', + 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', + 'lastIndexOf', 'isEmpty', 'chain']; // Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function(method) { @@ -880,13 +970,240 @@ }; }); - // Backbone.Router - // ------------------- + // Backbone.View + // ------------- - // 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 = {}); + // 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); @@ -894,9 +1211,10 @@ // Cached regular expressions for matching named param parts and splatted // parts of route strings. - var namedParam = /:\w+/g; + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; var splatParam = /\*\w+/g; - var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; // Set up all inheritable **Backbone.Router** properties and methods. _.extend(Router.prototype, Events, { @@ -913,13 +1231,19 @@ // route: function(route, name, callback) { if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } if (!callback) callback = this[name]; - Backbone.history.route(route, _.bind(function(fragment) { - var args = this._extractParameters(route, fragment); - callback && callback.apply(this, args); - this.trigger.apply(this, ['route:' + name].concat(args)); - Backbone.history.trigger('route', this, name, args); - }, this)); + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + callback && callback.apply(router, args); + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + }); return this; }, @@ -934,12 +1258,10 @@ // routes can be defined at the bottom of the route map. _bindRoutes: function() { if (!this.routes) return; - var routes = []; - for (var route in this.routes) { - routes.unshift([route, this.routes[route]]); - } - for (var i = 0, l = routes.length; i < l; i++) { - this.route(routes[i][0], routes[i][1], this[routes[i][1]]); + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); } }, @@ -947,15 +1269,22 @@ // against the current location hash. _routeToRegExp: function(route) { route = route.replace(escapeRegExp, '\\$&') - .replace(namedParam, '([^\/]+)') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional) { + return optional ? match : '([^\/]+)'; + }) .replace(splatParam, '(.*?)'); return new RegExp('^' + route + '$'); }, // Given a route, and a URL fragment that it matches, return the array of - // extracted parameters. + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. _extractParameters: function(route, fragment) { - return route.exec(fragment).slice(1); + var params = route.exec(fragment).slice(1); + return _.map(params, function(param) { + return param ? decodeURIComponent(param) : null; + }); } }); @@ -963,21 +1292,24 @@ // Backbone.History // ---------------- - // Handles cross-browser history management, based on URL fragments. If the - // browser does not support `onhashchange`, falls back to polling. + // 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'); - // #1653 - Ensure that `History` can be used outside of the browser. + // 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 cleaning leading hashes and slashes. - var routeStripper = /^[#\/]/; + // 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; @@ -988,6 +1320,9 @@ // Cached regex for removing a trailing slash. var trailingSlash = /\/$/; + // Cached regex for stripping urls of hash and query. + var pathStripper = /[?#].*$/; + // Has the history handling already been started? History.started = false; @@ -1012,12 +1347,12 @@ if (this._hasPushState || !this._wantsHashChange || forcePushState) { fragment = this.location.pathname; var root = this.root.replace(trailingSlash, ''); - if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); + if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); } else { fragment = this.getHash(); } } - return decodeURIComponent(fragment.replace(routeStripper, '')); + return fragment.replace(routeStripper, ''); }, // Start the hash change handling, returning `true` if the current URL matches @@ -1028,7 +1363,7 @@ // Figure out the initial configuration. Do we need an iframe? // Is pushState desired ... is it available? - this.options = _.extend({}, {root: '/'}, this.options, options); + this.options = _.extend({root: '/'}, this.options, options); this.root = this.options.root; this._wantsHashChange = this.options.hashChange !== false; this._wantsPushState = !!this.options.pushState; @@ -1048,9 +1383,9 @@ // 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).bind('popstate', this.checkUrl); + Backbone.$(window).on('popstate', this.checkUrl); } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { - Backbone.$(window).bind('hashchange', this.checkUrl); + Backbone.$(window).on('hashchange', this.checkUrl); } else if (this._wantsHashChange) { this._checkUrlInterval = setInterval(this.checkUrl, this.interval); } @@ -1059,21 +1394,27 @@ // opened by a non-pushState browser. this.fragment = fragment; var loc = this.location; - var atRoot = (loc.pathname.replace(/[^/]$/, '$&/') === this.root) && !loc.search; - - // 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._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) { - this.fragment = this.getFragment(null, true); - this.location.replace(this.root + this.location.search + '#' + this.fragment); - // Return immediately as browser will do redirect to new url - return true; + var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root; + + // 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 && !atRoot) { + this.fragment = this.getFragment(null, true); + this.location.replace(this.root + this.location.search + '#' + 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 && atRoot && loc.hash) { + this.fragment = this.getHash().replace(routeStripper, ''); + this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); + } - // 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._wantsPushState && this._hasPushState && 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(); @@ -1082,7 +1423,7 @@ // Disable Backbone.history, perhaps temporarily. Not useful in a real app, // but possibly useful for unit testing Routers. stop: function() { - Backbone.$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl); + Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); clearInterval(this._checkUrlInterval); History.started = false; }, @@ -1102,21 +1443,20 @@ } if (current === this.fragment) return false; if (this.iframe) this.navigate(current); - this.loadUrl() || this.loadUrl(this.getHash()); + 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(fragmentOverride) { - var fragment = this.fragment = this.getFragment(fragmentOverride); - var matched = _.any(this.handlers, function(handler) { + 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; } }); - return matched; }, // Save a fragment into the hash history, or replace the URL state if the @@ -1128,11 +1468,18 @@ // 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}; - fragment = this.getFragment(fragment || ''); + if (!options || options === true) options = {trigger: !!options}; + + var url = this.root + (fragment = this.getFragment(fragment || '')); + + // Strip the fragment of the query and hash for matching. + fragment = fragment.replace(pathStripper, ''); + if (this.fragment === fragment) return; this.fragment = fragment; - var url = this.root + 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) { @@ -1155,7 +1502,7 @@ } else { return this.location.assign(url); } - if (options.trigger) this.loadUrl(fragment); + if (options.trigger) return this.loadUrl(fragment); }, // Update the hash location, either replacing the current entry, or adding @@ -1165,7 +1512,7 @@ var href = location.href.replace(/(javascript:|#).*$/, ''); location.replace(href + '#' + fragment); } else { - // #1649 - Some browsers require that `hash` contains a leading #. + // Some browsers require that `hash` contains a leading #. location.hash = '#' + fragment; } } @@ -1175,247 +1522,6 @@ // Create the default Backbone.history. Backbone.history = new History; - // Backbone.View - // ------------- - - // 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'); - this._configure(options || {}); - 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']; - - // 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 prefered 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; - }, - - // Clean up references to this view in order to prevent latent effects and - // memory leaks. - dispose: function() { - this.undelegateEvents(); - if (this.model) this.model.off(null, null, this); - if (this.collection) this.collection.off(null, null, this); - return this; - }, - - // Remove this view from the DOM. Note that the view isn't present in the - // DOM by default, so calling this method may be a no-op. - remove: function() { - this.dispose(); - this.$el.remove(); - return this; - }, - - // For small amounts of DOM Elements, where a full-blown template isn't - // needed, use **make** to manufacture elements, one at a time. - // - // var el = this.make('li', {'class': 'row'}, this.model.escape('title')); - // - make: function(tagName, attributes, content) { - var el = document.createElement(tagName); - if (attributes) Backbone.$(el).attr(attributes); - if (content != null) Backbone.$(el).html(content); - return el; - }, - - // 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.undelegateEvents(); - for (var key in events) { - var method = events[key]; - if (!_.isFunction(method)) method = this[events[key]]; - if (!method) throw new Error('Method "' + events[key] + '" does not exist'); - var match = key.match(delegateEventSplitter); - var eventName = match[1], selector = match[2]; - method = _.bind(method, this); - eventName += '.delegateEvents' + this.cid; - if (selector === '') { - this.$el.bind(eventName, method); - } else { - this.$el.delegate(selector, eventName, method); - } - } - }, - - // 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.unbind('.delegateEvents' + this.cid); - }, - - // Performs the initial configuration of a View with a set of options. - // Keys with special meaning *(model, collection, id, className)*, are - // attached directly to the view. - _configure: function(options) { - if (this.options) options = _.extend({}, this.options, options); - for (var i = 0, l = viewOptions.length; i < l; i++) { - var attr = viewOptions[i]; - if (options[attr]) this[attr] = options[attr]; - } - this.options = options; - }, - - // 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'); - this.setElement(this.make(_.result(this, 'tagName'), attrs), false); - } else { - this.setElement(this.el, false); - } - } - - }); - - // Backbone.sync - // ------------- - - // Map from CRUD to HTTP for our default `Backbone.sync` implementation. - var methodMap = { - 'create': 'POST', - 'update': 'PUT', - 'delete': 'DELETE', - 'read': 'GET' - }; - - // 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. - options || (options = {}); - - // 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 && model && (method === 'create' || method === 'update')) { - params.contentType = 'application/json'; - params.data = JSON.stringify(model); - } - - // For older servers, emulate JSON by encoding the request into an HTML-form. - if (Backbone.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 (Backbone.emulateHTTP) { - if (type === 'PUT' || type === 'DELETE') { - if (Backbone.emulateJSON) params.data._method = type; - params.type = 'POST'; - params.beforeSend = function(xhr) { - xhr.setRequestHeader('X-HTTP-Method-Override', type); - }; - } - } - - // Don't process data on a non-GET request. - if (params.type !== 'GET' && !Backbone.emulateJSON) { - params.processData = false; - } - - var success = options.success; - options.success = function(resp, status, xhr) { - if (success) success(resp, status, xhr); - model.trigger('sync', model, resp, options); - }; - - var error = options.error; - options.error = function(xhr, status, thrown) { - if (error) error(model, xhr, options); - model.trigger('error', model, xhr, options); - }; - - // Make the request, allowing the user to override any Ajax options. - return Backbone.ajax(_.extend(params, options)); - }; - - // Set the default implementation of `Backbone.ajax` to proxy through to `$`. - Backbone.ajax = function() { - return Backbone.$.ajax.apply(Backbone.$, arguments); - }; - // Helpers // ------- @@ -1432,12 +1538,15 @@ if (protoProps && _.has(protoProps, 'constructor')) { child = protoProps.constructor; } else { - child = function(){ parent.apply(this, arguments); }; + 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. - function Surrogate(){ this.constructor = child; }; + var Surrogate = function(){ this.constructor = child; }; Surrogate.prototype = parent.prototype; child.prototype = new Surrogate; @@ -1445,9 +1554,6 @@ // if supplied. if (protoProps) _.extend(child.prototype, protoProps); - // Add static properties to the constructor function, if supplied. - _.extend(child, parent, staticProps); - // Set a convenience property in case the parent's prototype is needed // later. child.__super__ = parent.prototype; @@ -1455,12 +1561,21 @@ return child; }; - // Set up inheritance for the model, collection, router, and view. - Model.extend = Collection.extend = Router.extend = View.extend = extend; + // 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); + }; + }; + }).call(this); diff --git a/core/assets/vendor/underscore/underscore.js b/core/assets/vendor/underscore/underscore.js index 013dcadbed05..8887e194bb14 100644 --- a/core/assets/vendor/underscore/underscore.js +++ b/core/assets/vendor/underscore/underscore.js @@ -1,6 +1,6 @@ -// Underscore.js 1.4.0 +// Underscore.js 1.5.2 // http://underscorejs.org -// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. (function() { @@ -8,7 +8,7 @@ // Baseline setup // -------------- - // Establish the root object, `window` in the browser, or `global` on the server. + // Establish the root object, `window` in the browser, or `exports` on the server. var root = this; // Save the previous value of the `_` variable. @@ -21,12 +21,12 @@ 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, - unshift = ArrayProto.unshift, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty; + 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. @@ -61,11 +61,11 @@ } exports._ = _; } else { - root['_'] = _; + root._ = _; } // Current version. - _.VERSION = '1.4.0'; + _.VERSION = '1.5.2'; // Collection Functions // -------------------- @@ -74,17 +74,17 @@ // Handles objects with the built-in `forEach`, arrays, and raw objects. // Delegates to **ECMAScript 5**'s native `forEach` if available. var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; if (nativeForEach && obj.forEach === nativeForEach) { obj.forEach(iterator, context); } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { + for (var i = 0, length = obj.length; i < length; i++) { if (iterator.call(context, obj[i], i, obj) === breaker) return; } } else { - for (var key in obj) { - if (_.has(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) return; - } + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; } } }; @@ -93,17 +93,21 @@ // Delegates to **ECMAScript 5**'s native `map` if available. _.map = _.collect = function(obj, iterator, context) { var results = []; + if (obj == null) return results; if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); each(obj, function(value, index, list) { - results[results.length] = iterator.call(context, value, index, list); + results.push(iterator.call(context, value, index, list)); }); 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`. Delegates to **ECMAScript 5**'s native `reduce` if available. _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { var initial = arguments.length > 2; + if (obj == null) obj = []; if (nativeReduce && obj.reduce === nativeReduce) { if (context) iterator = _.bind(iterator, context); return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); @@ -116,7 +120,7 @@ memo = iterator.call(context, memo, value, index, list); } }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + if (!initial) throw new TypeError(reduceError); return memo; }; @@ -124,9 +128,10 @@ // Delegates to **ECMAScript 5**'s native `reduceRight` if available. _.reduceRight = _.foldr = function(obj, iterator, memo, context) { var initial = arguments.length > 2; + if (obj == null) obj = []; if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { if (context) iterator = _.bind(iterator, context); - return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); } var length = obj.length; if (length !== +length) { @@ -142,7 +147,7 @@ memo = iterator.call(context, memo, obj[index], index, list); } }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + if (!initial) throw new TypeError(reduceError); return memo; }; @@ -163,20 +168,19 @@ // Aliased as `select`. _.filter = _.select = function(obj, iterator, context) { var results = []; + if (obj == null) return results; if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); each(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) results[results.length] = value; + if (iterator.call(context, value, index, list)) results.push(value); }); return results; }; // Return all the elements for which a truth test fails. _.reject = function(obj, iterator, context) { - var results = []; - each(obj, function(value, index, list) { - if (!iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; + return _.filter(obj, function(value, index, list) { + return !iterator.call(context, value, index, list); + }, context); }; // Determine whether all of the elements match a truth test. @@ -185,6 +189,7 @@ _.every = _.all = function(obj, iterator, context) { iterator || (iterator = _.identity); var result = true; + if (obj == null) return result; if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); each(obj, function(value, index, list) { if (!(result = result && iterator.call(context, value, index, list))) return breaker; @@ -198,6 +203,7 @@ var any = _.some = _.any = function(obj, iterator, context) { iterator || (iterator = _.identity); var result = false; + if (obj == null) return result; if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); each(obj, function(value, index, list) { if (result || (result = iterator.call(context, value, index, list))) return breaker; @@ -208,19 +214,19 @@ // Determine if the array or object contains a given value (using `===`). // Aliased as `include`. _.contains = _.include = function(obj, target) { - var found = false; + if (obj == null) return false; if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; - found = any(obj, function(value) { + return any(obj, function(value) { return value === target; }); - return found; }; // 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 (_.isFunction(method) ? method : value[method]).apply(value, args); + return (isFunc ? method : value[method]).apply(value, args); }); }; @@ -230,10 +236,10 @@ }; // Convenience version of a common use case of `filter`: selecting only objects - // with specific `key:value` pairs. - _.where = function(obj, attrs) { - if (_.isEmpty(attrs)) return []; - return _.filter(obj, function(value) { + // containing specific `key:value` pairs. + _.where = function(obj, attrs, first) { + if (_.isEmpty(attrs)) return first ? void 0 : []; + return _[first ? 'find' : 'filter'](obj, function(value) { for (var key in attrs) { if (attrs[key] !== value[key]) return false; } @@ -241,18 +247,24 @@ }); }; + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.where(obj, attrs, true); + }; + // Return the maximum element or (element-based computation). // Can't optimize arrays of integers longer than 65,535 elements. - // See: https://bugs.webkit.org/show_bug.cgi?id=80797 + // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) _.max = function(obj, iterator, context) { if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { return Math.max.apply(Math, obj); } if (!iterator && _.isEmpty(obj)) return -Infinity; - var result = {computed : -Infinity}; + var result = {computed : -Infinity, value: -Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; - computed >= result.computed && (result = {value : value, computed : computed}); + computed > result.computed && (result = {value : value, computed : computed}); }); return result.value; }; @@ -263,7 +275,7 @@ return Math.min.apply(Math, obj); } if (!iterator && _.isEmpty(obj)) return Infinity; - var result = {computed : Infinity}; + var result = {computed : Infinity, value: Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed < result.computed && (result = {value : value, computed : computed}); @@ -271,7 +283,8 @@ return result.value; }; - // Shuffle an array. + // Shuffle an array, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). _.shuffle = function(obj) { var rand; var index = 0; @@ -284,6 +297,16 @@ return shuffled; }; + // Sample **n** random values from an array. + // If **n** is not specified, returns a single random element from the array. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (arguments.length < 2 || guard) { + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + // An internal function to generate lookup iterators. var lookupIterator = function(value) { return _.isFunction(value) ? value : function(obj){ return obj[value]; }; @@ -294,9 +317,9 @@ var iterator = lookupIterator(value); return _.pluck(_.map(obj, function(value, index, list) { return { - value : value, - index : index, - criteria : iterator.call(context, value, index, list) + value: value, + index: index, + criteria: iterator.call(context, value, index, list) }; }).sort(function(left, right) { var a = left.criteria; @@ -305,38 +328,41 @@ if (a > b || a === void 0) return 1; if (a < b || b === void 0) return -1; } - return left.index < right.index ? -1 : 1; + return left.index - right.index; }), 'value'); }; // An internal function used for aggregate "group by" operations. - var group = function(obj, value, context, behavior) { - var result = {}; - var iterator = lookupIterator(value); - each(obj, function(value, index) { - var key = iterator.call(context, value, index, obj); - behavior(result, key, value); - }); - return result; + var group = function(behavior) { + return function(obj, value, context) { + var result = {}; + var iterator = value == null ? _.identity : lookupIterator(value); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + 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 = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { - (_.has(result, key) ? result[key] : (result[key] = [])).push(value); - }); - }; + _.groupBy = group(function(result, key, value) { + (_.has(result, key) ? result[key] : (result[key] = [])).push(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, key, value) { + 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 = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { - if (!_.has(result, key)) result[key] = 0; - result[key]++; - }); - }; + _.countBy = group(function(result, key) { + _.has(result, key) ? result[key]++ : 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. @@ -351,15 +377,17 @@ return low; }; - // Safely convert anything iterable into a real, live array. + // Safely create a real, live array from anything iterable. _.toArray = function(obj) { if (!obj) return []; - if (obj.length === +obj.length) return slice.call(obj); + 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; }; @@ -370,7 +398,8 @@ // 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) { - return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + if (array == null) return void 0; + return (n == null) || guard ? array[0] : slice.call(array, 0, n); }; // Returns everything but the last entry of the array. Especially useful on @@ -384,10 +413,11 @@ // 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 ((n != null) && !guard) { - return slice.call(array, Math.max(array.length - n, 0)); - } else { + if (array == null) return void 0; + if ((n == null) || guard) { return array[array.length - 1]; + } else { + return slice.call(array, Math.max(array.length - n, 0)); } }; @@ -401,13 +431,16 @@ // Trim out all falsy values from an array. _.compact = function(array) { - return _.filter(array, function(value){ return !!value; }); + return _.filter(array, _.identity); }; // Internal implementation of a recursive `flatten` function. var flatten = function(input, shallow, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } each(input, function(value) { - if (_.isArray(value)) { + if (_.isArray(value) || _.isArguments(value)) { shallow ? push.apply(output, value) : flatten(value, shallow, output); } else { output.push(value); @@ -416,7 +449,7 @@ return output; }; - // Return a completely flattened version of an array. + // Flatten out an array, either recursively (by default), or just one level. _.flatten = function(array, shallow) { return flatten(array, shallow, []); }; @@ -430,6 +463,11 @@ // been sorted, you have the option of using a faster algorithm. // Aliased as `unique`. _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } var initial = iterator ? _.map(array, iterator, context) : array; var results = []; var seen = []; @@ -445,7 +483,7 @@ // Produce an array that contains the union: each distinct element from all of // the passed-in arrays. _.union = function() { - return _.uniq(concat.apply(ArrayProto, arguments)); + return _.uniq(_.flatten(arguments, true)); }; // Produce an array that contains every item shared between all the @@ -469,11 +507,10 @@ // Zip together multiple lists into a single array -- elements that share // an index go together. _.zip = function() { - var args = slice.call(arguments); - var length = _.max(_.pluck(args, 'length')); + var length = _.max(_.pluck(arguments, "length").concat(0)); var results = new Array(length); for (var i = 0; i < length; i++) { - results[i] = _.pluck(args, "" + i); + results[i] = _.pluck(arguments, '' + i); } return results; }; @@ -482,8 +519,9 @@ // 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, l = list.length; i < l; i++) { + for (var i = 0, length = list.length; i < length; i++) { if (values) { result[list[i]] = values[i]; } else { @@ -500,22 +538,24 @@ // If the array is large and already in sort order, pass `true` // for **isSorted** to use binary search. _.indexOf = function(array, item, isSorted) { - var i = 0, l = array.length; + if (array == null) return -1; + var i = 0, length = array.length; if (isSorted) { if (typeof isSorted == 'number') { - i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); + i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); } else { i = _.sortedIndex(array, item); return array[i] === item ? i : -1; } } if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); - for (; i < l; i++) if (array[i] === item) return i; + for (; i < length; i++) if (array[i] === item) return i; return -1; }; // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; var hasIndex = from != null; if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); @@ -535,11 +575,11 @@ } step = arguments[2] || 1; - var len = Math.max(Math.ceil((stop - start) / step), 0); + var length = Math.max(Math.ceil((stop - start) / step), 0); var idx = 0; - var range = new Array(len); + var range = new Array(length); - while(idx < len) { + while(idx < length) { range[idx++] = start; start += step; } @@ -554,29 +594,38 @@ var ctor = function(){}; // Create a function bound to a given object (assigning `this`, and arguments, - // optionally). Binding with arguments is also known as `curry`. - // Delegates to **ECMAScript 5**'s native `Function.bind` if available. - // We check for `func.bind` first, to fail fast when `func` is undefined. - _.bind = function bind(func, context) { - var bound, args; - if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + // 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; args = slice.call(arguments, 2); return 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 (Object(result) === result) return result; return self; }; }; + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. + _.partial = function(func) { + var args = slice.call(arguments, 1); + return function() { + return func.apply(this, args.concat(slice.call(arguments))); + }; + }; + // Bind all of an object's methods to that object. Useful for ensuring that // all callbacks defined on an object belong to it. _.bindAll = function(obj) { var funcs = slice.call(arguments, 1); - if (funcs.length == 0) funcs = _.functions(obj); + if (funcs.length === 0) throw new Error("bindAll must be passed function names"); each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); return obj; }; @@ -605,27 +654,34 @@ }; // Returns a function, that, when invoked, will only be triggered at most once - // during a given window of time. - _.throttle = function(func, wait) { - var context, args, timeout, throttling, more, result; - var whenDone = _.debounce(function(){ more = throttling = false; }, wait); + // 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; + options || (options = {}); + var later = function() { + previous = options.leading === false ? 0 : new Date; + timeout = null; + result = func.apply(context, args); + }; return function() { - context = this; args = arguments; - var later = function() { + var now = new Date; + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); timeout = null; - if (more) { - result = func.apply(context, args); - } - whenDone(); - }; - if (!timeout) timeout = setTimeout(later, wait); - if (throttling) { - more = true; - } else { - throttling = true; + previous = now; result = func.apply(context, args); + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); } - whenDone(); return result; }; }; @@ -635,16 +691,24 @@ // N milliseconds. If `immediate` is passed, trigger the function on the // leading edge, instead of the trailing. _.debounce = function(func, wait, immediate) { - var timeout, result; + var timeout, args, context, timestamp, result; return function() { - var context = this, args = arguments; + context = this; + args = arguments; + timestamp = new Date(); var later = function() { - timeout = null; - if (!immediate) result = func.apply(context, args); + var last = (new Date()) - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) result = func.apply(context, args); + } }; var callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); + if (!timeout) { + timeout = setTimeout(later, wait); + } if (callNow) result = func.apply(context, args); return result; }; @@ -689,7 +753,6 @@ // Returns a function that will only be executed after being called N times. _.after = function(times, func) { - if (times <= 0) return func(); return function() { if (--times < 1) { return func.apply(this, arguments); @@ -705,28 +768,39 @@ _.keys = nativeKeys || function(obj) { if (obj !== Object(obj)) throw new TypeError('Invalid object'); var keys = []; - for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + 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 values = []; - for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); + var keys = _.keys(obj); + var length = keys.length; + var values = new 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 pairs = []; - for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); + var keys = _.keys(obj); + var length = keys.length; + var pairs = new 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 = {}; - for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } return result; }; @@ -743,8 +817,10 @@ // Extend a given object with all the properties in passed-in object(s). _.extend = function(obj) { each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - obj[prop] = source[prop]; + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } } }); return obj; @@ -773,8 +849,10 @@ // Fill in a given object with default properties. _.defaults = function(obj) { each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; + if (source) { + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } } }); return obj; @@ -797,7 +875,7 @@ // 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. + // 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; @@ -839,6 +917,13 @@ // 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 && !(_.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); @@ -855,13 +940,6 @@ } } } else { - // Objects with different constructors are not equivalent, but `Object`s - // from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && - _.isFunction(bCtor) && (bCtor instanceof bCtor))) { - return false; - } // Deep compare objects. for (var key in a) { if (_.has(a, key)) { @@ -939,7 +1017,7 @@ // Is a given object a finite number? _.isFinite = function(obj) { - return _.isNumber(obj) && isFinite(obj); + return isFinite(obj) && !isNaN(parseFloat(obj)); }; // Is the given value `NaN`? (NaN is the only number which does not equal itself). @@ -985,7 +1063,9 @@ // Run a function **n** times. _.times = function(n, iterator, context) { - for (var i = 0; i < n; i++) iterator.call(context, i); + var accum = Array(Math.max(0, n)); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; }; // Return a random integer between min and max (inclusive). @@ -994,7 +1074,7 @@ max = min; min = 0; } - return min + (0 | Math.random() * (max - min + 1)); + return min + Math.floor(Math.random() * (max - min + 1)); }; // List of HTML entities for escaping. @@ -1004,8 +1084,7 @@ '<': '<', '>': '>', '"': '"', - "'": ''', - '/': '/' + "'": ''' } }; entityMap.unescape = _.invert(entityMap.escape); @@ -1026,17 +1105,17 @@ }; }); - // If the value of the named property is a function then invoke it; - // otherwise, return it. + // 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 null; + if (object == null) return void 0; var value = object[property]; return _.isFunction(value) ? value.call(object) : value; }; // Add your own custom functions to the Underscore object. _.mixin = function(obj) { - each(_.functions(obj), function(name){ + each(_.functions(obj), function(name) { var func = _[name] = obj[name]; _.prototype[name] = function() { var args = [this._wrapped]; @@ -1050,7 +1129,7 @@ // Useful for temporary DOM ids. var idCounter = 0; _.uniqueId = function(prefix) { - var id = idCounter++; + var id = ++idCounter + ''; return prefix ? prefix + id : id; }; @@ -1085,6 +1164,7 @@ // Underscore templating handles arbitrary delimiters, preserves whitespace, // and correctly escapes quotes within interpolated code. _.template = function(text, data, settings) { + var render; settings = _.defaults({}, settings, _.templateSettings); // Combine delimiters into one regular expression via alternation. @@ -1100,11 +1180,18 @@ text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { source += text.slice(index, offset) .replace(escaper, function(match) { return '\\' + escapes[match]; }); - source += - escape ? "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'" : - interpolate ? "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" : - evaluate ? "';\n" + evaluate + "\n__p+='" : ''; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } index = offset + match.length; + return match; }); source += "';\n"; @@ -1116,7 +1203,7 @@ source + "return __p;\n"; try { - var render = new Function(settings.variable || 'obj', '_', source); + render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; diff --git a/core/modules/ckeditor/js/ckeditor.admin.js b/core/modules/ckeditor/js/ckeditor.admin.js index ccf2f715407f..d519d66f7c46 100644 --- a/core/modules/ckeditor/js/ckeditor.admin.js +++ b/core/modules/ckeditor/js/ckeditor.admin.js @@ -120,8 +120,8 @@ Drupal.ckeditor = { this.getCKEditorFeatures(this.model.get('hiddenEditorConfig'), this.disableFeaturesDisallowedByFilters.bind(this)); // Push the active editor configuration to the textarea. - this.model.on('change:activeEditorConfig', this.model.sync, this.model); - this.model.on('change:isDirty', this.parseEditorDOM, this); + this.model.listenTo(this.model, 'change:activeEditorConfig', this.model.sync); + this.listenTo(this.model, 'change:isDirty', this.parseEditorDOM); }, /** @@ -462,7 +462,7 @@ Drupal.ckeditor = { * {@inheritdoc} */ initialize: function () { - this.model.on('change:isDirty change:groupNamesVisible', this.render, this); + this.listenTo(this.model, 'change:isDirty change:groupNamesVisible', this.render); // Add a toggle for the button group names. $(Drupal.theme('ckeditorButtonGroupNamesToggle')) @@ -959,7 +959,7 @@ Drupal.ckeditor = { initialize: function () { // Announce the button and group positions when the model is no longer // dirty. - this.model.on('change:isDirty', this.announceMove, this); + this.listenTo(this.model, 'change:isDirty', this.announceMove); }, /** diff --git a/core/modules/contextual/js/contextual.js b/core/modules/contextual/js/contextual.js index ab404a23cc1a..46164113362d 100644 --- a/core/modules/contextual/js/contextual.js +++ b/core/modules/contextual/js/contextual.js @@ -269,7 +269,7 @@ Drupal.contextual = { * {@inheritdoc} */ initialize: function () { - this.model.on('change', this.render, this); + this.listenTo(this.model, 'change', this.render); }, /** @@ -308,7 +308,9 @@ Drupal.contextual = { * {@inheritdoc} */ initialize: function (options) { - this.model.on('change', this.render, this); + this.options = options; + + this.listenTo(this.model, 'change', this.render); // Use aria-role form so that the number of items in the list is spoken. this.$el.attr('role', 'form'); @@ -399,7 +401,7 @@ Drupal.contextual = { * {@inheritdoc} */ initialize: function () { - this.model.on('change:hasFocus', this.render, this); + this.listenTo(this.model, 'change:hasFocus', this.render); }, /** diff --git a/core/modules/contextual/js/contextual.toolbar.js b/core/modules/contextual/js/contextual.toolbar.js index e1a01dd0a8dc..0c6a8b8fccd0 100644 --- a/core/modules/contextual/js/contextual.toolbar.js +++ b/core/modules/contextual/js/contextual.toolbar.js @@ -20,8 +20,19 @@ var strings = { * A contextual links DOM element as rendered by the server. */ function initContextualToolbar (context) { + if (!Drupal.contextual || !Drupal.contextual.collection) { + return; + } + var contextualToolbar = Drupal.contextualToolbar; - var model = contextualToolbar.model = new contextualToolbar.Model(); + var model = contextualToolbar.model = new contextualToolbar.Model({ + // Checks whether localStorage indicates we should start in edit mode + // rather than view mode. + // @see Drupal.contextualToolbar.VisualView.persist() + isViewing: localStorage.getItem('Drupal.contextualToolbar.isViewing') !== 'false' + }, { + contextualCollection: Drupal.contextual.collection, + }); var viewOptions = { el: $('.toolbar .toolbar-bar .contextual-toolbar-tab'), @@ -30,36 +41,6 @@ function initContextualToolbar (context) { }; new contextualToolbar.VisualView(viewOptions); new contextualToolbar.AuralView(viewOptions); - - // Show the edit tab while there's >=1 contextual link. - if (Drupal.contextual && Drupal.contextual.collection) { - var contextualCollection = Drupal.contextual.collection; - var trackContextualCount = function () { - model.set('contextualCount', contextualCollection.length); - }; - contextualCollection.on('reset remove add', trackContextualCount); - trackContextualCount(); - - // Whenever edit mode is toggled, lock all contextual links. - model.on('change:isViewing', function() { - contextualCollection.each(function (contextualModel) { - contextualModel.set('isLocked', !model.get('isViewing')); - }); - }); - // When a new contextual link is added and edit mode is enabled, lock it. - contextualCollection.on('add', function (contextualModel) { - if (!model.get('isViewing')) { - contextualModel.set('isLocked', true); - } - }); - } - - // Checks whether localStorage indicates we should start in edit mode - // rather than view mode. - // @see Drupal.contextualToolbar.VisualView.persist() - if (localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false') { - model.set('isViewing', false); - } } /** @@ -96,10 +77,67 @@ Drupal.contextualToolbar = { // of tabbable elements when edit mode is enabled. tabbingContext: null }, - initialize: function () { - this.on('change:contextualCount', function (model) { - model.set('isVisible', model.get('contextualCount') > 0); + + /** + * {@inheritdoc} + * + * @param Object attrs + * @param Object options + * An object with the following option: + * - Backbone.collection contextualCollection: the collection of + * Drupal.contextual.Model models that represent the contextual links + * on the page. + */ + initialize: function (attrs, options) { + // Respond to new/removed contextual links. + this.listenTo(options.contextualCollection, { + 'reset remove add': this.countCountextualLinks, + 'add': this.lockNewContextualLinks + }); + + this.listenTo(this, { + // Automatically determine visibility. + 'change:contextualCount': this.updateVisibility, + // Whenever edit mode is toggled, lock all contextual links. + 'change:isViewing': function (model, isViewing) { + options.contextualCollection.each(function (contextualModel) { + contextualModel.set('isLocked', !isViewing); + }); + } }); + }, + + /** + * Tracks the number of contextual link models in the collection. + * + * @param Drupal.contextual.Model affectedModel + * The contextual links model that was added or removed. + * @param Backbone.Collection contextualCollection + * The collection of contextual link models. + */ + countCountextualLinks: function (contextualModel, contextualCollection) { + this.set('contextualCount', contextualCollection.length); + }, + + /** + * Lock newly added contextual links if edit mode is enabled. + * + * @param Drupal.contextual.Model addedContextualModel + * The contextual links model that was added. + * @param Backbone.Collection contextualCollection + * The collection of contextual link models. + */ + lockNewContextualLinks: function (contextualModel, contextualCollection) { + if (!this.get('isViewing')) { + contextualModel.set('isLocked', true); + } + }, + + /** + * Automatically updates visibility of the view/edit mode toggle. + */ + updateVisibility: function () { + this.set('isVisible', this.get('contextualCount') > 0); } }), @@ -128,8 +166,8 @@ Drupal.contextualToolbar = { * {@inheritdoc} */ initialize: function () { - this.model.on('change', this.render, this); - this.model.on('change:isViewing', this.persist, this); + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.model, 'change:isViewing', this.persist); }, /** @@ -175,9 +213,11 @@ Drupal.contextualToolbar = { /* * {@inheritdoc} */ - initialize: function () { - this.model.on('change', this.render, this); - this.model.on('change:isViewing', this.manageTabbing, this); + initialize: function (options) { + this.options = options; + + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.model, 'change:isViewing', this.manageTabbing); $(document).on('keyup', _.bind(this.onKeypress, this)); }, diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index f425a69d2329..3eaabccbba5c 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -61,6 +61,7 @@ function edit_library_info() { $path . '/js/edit.js' => $options, $path . '/js/util.js' => $options, // Models. + $path . '/js/models/BaseModel.js' => $options, $path . '/js/models/AppModel.js' => $options, $path . '/js/models/EntityModel.js' => $options, $path . '/js/models/FieldModel.js' => $options, diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index 429e7b2272a5..fe36fd1e123e 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -275,7 +275,7 @@ function processField (fieldElement) { // If an EntityModel for this field already exists (and hence also a "Quick // edit" contextual link), then initialize it immediately. - if (Drupal.edit.collections.entities.where({ entityID: entityID, entityInstanceID: entityInstanceID }).length > 0) { + if (Drupal.edit.collections.entities.findWhere({ entityID: entityID, entityInstanceID: entityInstanceID })) { initializeField(fieldElement, fieldID, entityID, entityInstanceID); } // Otherwise: queue the field. It is now available to be set up when its @@ -298,10 +298,10 @@ function processField (fieldElement) { * The field's entity's instance ID. */ function initializeField (fieldElement, fieldID, entityID, entityInstanceID) { - var entity = Drupal.edit.collections.entities.where({ + var entity = Drupal.edit.collections.entities.findWhere({ entityID: entityID, entityInstanceID: entityInstanceID - })[0]; + }); $(fieldElement).addClass('edit-field'); @@ -528,17 +528,15 @@ function initializeEntityContextualLink (contextualLink) { function deleteContainedModelsAndQueues($context) { $context.find('[data-edit-entity-id]').addBack('[data-edit-entity-id]').each(function (index, entityElement) { // Delete entity model. - // @todo change to findWhere() as soon as we have Backbone 1.0 in Drupal - // core. @see https://drupal.org/node/1800022 - var entityModels = Drupal.edit.collections.entities.where({el: entityElement}); - if (entityModels.length) { - var contextualLinkView = entityModels[0].get('contextualLinkView'); + var entityModel = Drupal.edit.collections.entities.findWhere({el: entityElement}); + if (entityModel) { + var contextualLinkView = entityModel.get('contextualLinkView'); contextualLinkView.undelegateEvents(); contextualLinkView.remove(); // Remove the EntityDecorationView. - entityModels[0].get('entityDecorationView').remove(); + entityModel.get('entityDecorationView').remove(); // Destroy the EntityModel; this will also destroy its FieldModels. - entityModels[0].destroy(); + entityModel.destroy(); } // Filter queue. diff --git a/core/modules/edit/js/models/BaseModel.js b/core/modules/edit/js/models/BaseModel.js new file mode 100644 index 000000000000..d7870b66834d --- /dev/null +++ b/core/modules/edit/js/models/BaseModel.js @@ -0,0 +1,41 @@ +/** + * @file + * A Backbone Model subclass that enforces validation when calling set(). + */ + +(function (Backbone) { + +"use strict"; + +Drupal.edit.BaseModel = Backbone.Model.extend({ + + /** + * {@inheritdoc} + */ + initialize: function (options) { + this.__initialized = true; + return Backbone.Model.prototype.initialize.call(this, options); + }, + + /** + * {@inheritdoc} + */ + set: function (key, val, options) { + if (this.__initialized) { + // Deal with both the "key", value and {key:value}-style arguments. + if (typeof key === 'object') { + key.validate = true; + } + else { + if (!options) { + options = {}; + } + options.validate = true; + } + } + return Backbone.Model.prototype.set.call(this, key, val, options); + } + +}); + +}(Backbone)); diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js index 7358bcbf50df..2e592e342841 100644 --- a/core/modules/edit/js/models/EntityModel.js +++ b/core/modules/edit/js/models/EntityModel.js @@ -7,7 +7,7 @@ "use strict"; -Drupal.edit.EntityModel = Backbone.Model.extend({ +Drupal.edit.EntityModel = Drupal.edit.BaseModel.extend({ defaults: { // The DOM element that represents this entity. It may seem bizarre to @@ -62,11 +62,14 @@ Drupal.edit.EntityModel = Backbone.Model.extend({ this.set('fields', new Drupal.edit.FieldCollection()); // Respond to entity state changes. - this.on('change:state', this.stateChange, this); + this.listenTo(this, 'change:state', this.stateChange); // The state of the entity is largely dependent on the state of its // fields. - this.get('fields').on('change:state', this.fieldStateChange, this); + this.listenTo(this.get('fields'), 'change:state', this.fieldStateChange); + + // Call Drupal.edit.BaseModel's initialize() method. + Drupal.edit.BaseModel.prototype.initialize.call(this); }, /** @@ -511,9 +514,9 @@ Drupal.edit.EntityModel = Backbone.Model.extend({ * @inheritdoc */ destroy: function (options) { - Backbone.Model.prototype.destroy.apply(this, options); + Drupal.edit.BaseModel.prototype.destroy.call(this, options); - this.off(null, null, this); + this.stopListening(); // Destroy all fields of this entity. this.get('fields').each(function (fieldModel) { diff --git a/core/modules/edit/js/models/FieldModel.js b/core/modules/edit/js/models/FieldModel.js index a9d3021c8daa..ea537a075201 100644 --- a/core/modules/edit/js/models/FieldModel.js +++ b/core/modules/edit/js/models/FieldModel.js @@ -10,7 +10,7 @@ /** * State of an in-place editable field in the DOM. */ -Drupal.edit.FieldModel = Backbone.Model.extend({ +Drupal.edit.FieldModel = Drupal.edit.BaseModel.extend({ defaults: { // The DOM element that represents this field. It may seem bizarre to have @@ -73,6 +73,9 @@ Drupal.edit.FieldModel = Backbone.Model.extend({ // Automatically generate the logical field ID. this.set('logicalFieldID', this.get('fieldID').split('/').slice(0, 4).join('/')); + + // Call Drupal.edit.BaseModel's initialize() method. + Drupal.edit.BaseModel.prototype.initialize.call(this, options); }, /** @@ -82,7 +85,7 @@ Drupal.edit.FieldModel = Backbone.Model.extend({ if (this.get('state') !== 'inactive') { throw new Error("FieldModel cannot be destroyed if it is not inactive state."); } - Backbone.Model.prototype.destroy.call(this, options); + Drupal.edit.BaseModel.prototype.destroy.call(this, options); }, /** diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js index 3c842f265256..93caf5c0c94e 100644 --- a/core/modules/edit/js/views/AppView.js +++ b/core/modules/edit/js/views/AppView.js @@ -36,21 +36,21 @@ Drupal.edit.AppView = Backbone.View.extend({ this.changedFieldStates = ['changed', 'saving', 'saved', 'invalid']; this.readyFieldStates = ['candidate', 'highlighted']; - options.entitiesCollection + this.listenTo(options.entitiesCollection, { // Track app state. - .on('change:state', this.appStateChange, this) - .on('change:isActive', this.enforceSingleActiveEntity, this); + 'change:state': this.appStateChange, + 'change:isActive': this.enforceSingleActiveEntity + }); - options.fieldsCollection - // Track app state. - .on('change:state', this.editorStateChange, this) - // Respond to field model HTML representation change events. - .on('change:html', this.propagateUpdatedField, this) - .on('change:html', this.renderUpdatedField, this) - // Respond to addition. - .on('add', this.rerenderedFieldToCandidate, this) - // Respond to destruction. - .on('destroy', this.teardownEditor, this); + // Track app state. + this.listenTo(options.fieldsCollection, 'change:state', this.editorStateChange); + // Respond to field model HTML representation change events. + this.listenTo(options.fieldsCollection, 'change:html', this.renderUpdatedField); + this.listenTo(options.fieldsCollection, 'change:html', this.propagateUpdatedField); + // Respond to addition. + this.listenTo(options.fieldsCollection, 'add', this.rerenderedFieldToCandidate); + // Respond to destruction. + this.listenTo(options.fieldsCollection, 'destroy', this.teardownEditor); }, /** @@ -540,10 +540,10 @@ Drupal.edit.AppView = Backbone.View.extend({ * A field that was just added to the collection of fields. */ rerenderedFieldToCandidate: function (fieldModel) { - var activeEntity = Drupal.edit.collections.entities.where({isActive: true})[0]; + var activeEntity = Drupal.edit.collections.entities.findWhere({isActive: true}); // Early-return if there is no active entity. - if (activeEntity === null) { + if (!activeEntity) { return; } diff --git a/core/modules/edit/js/views/ContextualLinkView.js b/core/modules/edit/js/views/ContextualLinkView.js index 3e9dee6c86fa..65a607774476 100644 --- a/core/modules/edit/js/views/ContextualLinkView.js +++ b/core/modules/edit/js/views/ContextualLinkView.js @@ -39,7 +39,7 @@ Drupal.edit.ContextualLinkView = Backbone.View.extend({ // Initial render. this.render(); // Re-render whenever this entity's isActive attribute changes. - this.model.on('change:isActive', this.render, this); + this.listenTo(this.model, 'change:isActive', this.render); }, /** diff --git a/core/modules/edit/js/views/EditorView.js b/core/modules/edit/js/views/EditorView.js index ce57d6f36169..a00432df4c97 100644 --- a/core/modules/edit/js/views/EditorView.js +++ b/core/modules/edit/js/views/EditorView.js @@ -40,7 +40,7 @@ Drupal.edit.EditorView = Backbone.View.extend({ */ initialize: function (options) { this.fieldModel = options.fieldModel; - this.fieldModel.on('change:state', this.stateChange, this); + this.listenTo(this.fieldModel, 'change:state', this.stateChange); }, /** @@ -50,7 +50,6 @@ Drupal.edit.EditorView = Backbone.View.extend({ // The el property is the field, which should not be removed. Remove the // pointer to it, then call Backbone.View.prototype.remove(). this.setElement(); - this.fieldModel.off(null, null, this); Backbone.View.prototype.remove.call(this); }, diff --git a/core/modules/edit/js/views/EntityDecorationView.js b/core/modules/edit/js/views/EntityDecorationView.js index f6fcb75911ff..ad107f9fdf3f 100644 --- a/core/modules/edit/js/views/EntityDecorationView.js +++ b/core/modules/edit/js/views/EntityDecorationView.js @@ -15,7 +15,7 @@ Drupal.edit.EntityDecorationView = Backbone.View.extend({ * Associated with the DOM root node of an editable entity. */ initialize: function () { - this.model.on('change', this.render, this); + this.listenTo(this.model, 'change', this.render); }, /** diff --git a/core/modules/edit/js/views/EntityToolbarView.js b/core/modules/edit/js/views/EntityToolbarView.js index a2906aa20057..889335a20ea2 100644 --- a/core/modules/edit/js/views/EntityToolbarView.js +++ b/core/modules/edit/js/views/EntityToolbarView.js @@ -29,11 +29,11 @@ Drupal.edit.EntityToolbarView = Backbone.View.extend({ this.$entity = $(this.model.get('el')); // Rerender whenever the entity state changes. - this.model.on('change:isActive change:isDirty change:state', this.render, this); + this.listenTo(this.model, 'change:isActive change:isDirty change:state', this.render); // Also rerender whenever a different field is highlighted or activated. - this.appModel.on('change:highlightedField change:activeField', this.render, this); + this.listenTo(this.appModel, 'change:highlightedField change:activeField', this.render); // Rerender when a field of the entity changes state. - this.model.get('fields').on('change:state', this.fieldStateChange, this); + this.listenTo(this.model.get('fields'), 'change:state', this.fieldStateChange); // Reposition the entity toolbar as the viewport and the position within the // viewport changes. @@ -121,8 +121,6 @@ Drupal.edit.EntityToolbarView = Backbone.View.extend({ this.$fence.remove(); // Stop listening to additional events. - this.appModel.off(null, null, this); - this.model.get('fields').off(null, null, this); $(window).off('resize.edit scroll.edit'); $(document).off('drupalViewportOffsetChange.edit'); diff --git a/core/modules/edit/js/views/FieldDecorationView.js b/core/modules/edit/js/views/FieldDecorationView.js index 4dfc403fb0c9..4172840f6926 100644 --- a/core/modules/edit/js/views/FieldDecorationView.js +++ b/core/modules/edit/js/views/FieldDecorationView.js @@ -29,8 +29,8 @@ Drupal.edit.FieldDecorationView = Backbone.View.extend({ initialize: function (options) { this.editorView = options.editorView; - this.model.on('change:state', this.stateChange, this); - this.model.on('change:isChanged change:inTempStore', this.renderChanged, this); + this.listenTo(this.model, 'change:state', this.stateChange); + this.listenTo(this.model, 'change:isChanged change:inTempStore', this.renderChanged); }, /** diff --git a/core/modules/edit/js/views/FieldToolbarView.js b/core/modules/edit/js/views/FieldToolbarView.js index 49e5b2794cb5..8aee09f43937 100644 --- a/core/modules/edit/js/views/FieldToolbarView.js +++ b/core/modules/edit/js/views/FieldToolbarView.js @@ -28,7 +28,7 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({ // Generate a DOM-compatible ID for the form container DOM element. this._id = 'edit-toolbar-for-' + this.model.id.replace(/[\/\[\]]/g, '_'); - this.model.on('change:state', this.stateChange, this); + this.listenTo(this.model, 'change:state', this.stateChange) }, /** diff --git a/core/modules/system/system.module b/core/modules/system/system.module index ad5f7f35c3de..d922cd97f859 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1743,7 +1743,7 @@ function system_library_info() { $libraries['underscore'] = array( 'title' => 'Underscore.js', 'website' => 'http://underscorejs.org/', - 'version' => '1.4.0', + 'version' => '1.5.2', 'js' => array( 'core/assets/vendor/underscore/underscore.js' => array('group' => JS_LIBRARY, 'weight' => -20), ), @@ -1753,7 +1753,7 @@ function system_library_info() { $libraries['backbone'] = array( 'title' => 'Backbone.js', 'website' => 'http://backbonejs.org/', - 'version' => '0.9.2', + 'version' => '1.1.0', 'js' => array( 'core/assets/vendor/backbone/backbone.js' => array('group' => JS_LIBRARY, 'weight' => -19), ), diff --git a/core/modules/toolbar/js/toolbar.js b/core/modules/toolbar/js/toolbar.js index 955ef593cfd1..fa7dffa3edd1 100644 --- a/core/modules/toolbar/js/toolbar.js +++ b/core/modules/toolbar/js/toolbar.js @@ -257,8 +257,8 @@ Drupal.toolbar = { initialize: function (options) { this.strings = options.strings; - this.model.on('change:orientation', this.onOrientationChange, this); - this.model.on('change:activeTray', this.onActiveTrayChange, this); + this.listenTo(this.model, 'change:orientation', this.onOrientationChange); + this.listenTo(this.model, 'change:activeTray', this.onActiveTrayChange); }, /** @@ -311,9 +311,9 @@ Drupal.toolbar = { initialize: function (options) { this.strings = options.strings; - this.model.on('change:activeTab change:orientation change:isOriented change:isTrayToggleVisible', this.render, this); - this.model.on('change:mqMatches', this.onMediaQueryChange, this); - this.model.on('change:offsets', this.adjustPlacement, this); + this.listenTo(this.model, 'change:activeTab change:orientation change:isOriented change:isTrayToggleVisible', this.render); + this.listenTo(this.model, 'change:mqMatches', this.onMediaQueryChange); + this.listenTo(this.model, 'change:offsets', this.adjustPlacement); // Add the tray orientation toggles. this.$el @@ -576,7 +576,7 @@ Drupal.toolbar = { * {@inheritdoc} */ initialize: function () { - this.model.on('change:subtrees', this.render, this); + this.listenTo(this.model, 'change:subtrees', this.render); }, /** @@ -611,7 +611,7 @@ Drupal.toolbar = { * {@inheritdoc} */ initialize: function () { - this.model.on('change:orientation change:offsets change:activeTray change:isOriented change:isFixed change:isViewportOverflowConstrained', this.render, this); + this.listenTo(this.model, 'change:orientation change:offsets change:activeTray change:isOriented change:isFixed change:isViewportOverflowConstrained', this.render); }, /** diff --git a/core/modules/tour/js/tour.js b/core/modules/tour/js/tour.js index 0438cdd826da..0f7a8fde35e7 100644 --- a/core/modules/tour/js/tour.js +++ b/core/modules/tour/js/tour.js @@ -74,8 +74,8 @@ Drupal.tour.views.ToggleTourView = Backbone.View.extend({ * Implements Backbone Views' initialize(). */ initialize: function () { - this.model.on('change:tour change:isActive', this.render, this); - this.model.on('change:isActive', this.toggleTour, this); + this.listenTo(this.model, 'change:tour change:isActive', this.render); + this.listenTo(this.model, 'change:isActive', this.toggleTour); }, /** -- GitLab