diff --git a/Gruntfile.js b/Gruntfile.js
index e91992882..2a052e3b3 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -27,7 +27,6 @@ module.exports = function (grunt) {
'UI/JsLibraries/require.js' : 'http://raw.github.com/jrburke/requirejs/master/require.js',
'UI/JsLibraries/sugar.js' : 'http://raw.github.com/andrewplummer/Sugar/master/release/sugar-full.development.js',
'UI/JsLibraries/lodash.underscore.js' : 'http://raw.github.com/bestiejs/lodash/master/dist/lodash.underscore.js',
- 'UI/JsLibraries/lunr.js' : 'http://raw.github.com/olivernn/lunr.js/master/lunr.js',
'UI/JsLibraries/messenger.js' : 'http://raw.github.com/HubSpot/messenger/master/build/js/messenger.js',
'UI/Content/Messenger/messenger.css' : 'http://raw.github.com/HubSpot/messenger/master/build/css/messenger.css',
diff --git a/UI/.idea/libraries/backbone_backgrid_filter_js.xml b/UI/.idea/libraries/backbone_backgrid_filter_js.xml
deleted file mode 100644
index f7bf089af..000000000
--- a/UI/.idea/libraries/backbone_backgrid_filter_js.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/UI/.idea/libraries/libraries.xml b/UI/.idea/libraries/libraries.xml
deleted file mode 100644
index 621295489..000000000
--- a/UI/.idea/libraries/libraries.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/UI/JsLibraries/backbone.collectionbinder.js b/UI/JsLibraries/backbone.collectionbinder.js
deleted file mode 100644
index 12f081bc9..000000000
--- a/UI/JsLibraries/backbone.collectionbinder.js
+++ /dev/null
@@ -1,284 +0,0 @@
-// Backbone.CollectionBinder v1.0.0
-// (c) 2013 Bart Wood
-// Distributed Under MIT License
-
-(function(){
-
- if(!Backbone){
- throw 'Please include Backbone.js before Backbone.ModelBinder.js';
- }
-
- if(!Backbone.ModelBinder){
- throw 'Please include Backbone.ModelBinder.js before Backbone.CollectionBinder.js';
- }
-
- Backbone.CollectionBinder = function(elManagerFactory, options){
- _.bindAll(this);
- this._elManagers = {};
-
- this._elManagerFactory = elManagerFactory;
- if(!this._elManagerFactory) throw 'elManagerFactory must be defined.';
-
- // Let the factory just use the trigger function on the view binder
- this._elManagerFactory.trigger = this.trigger;
-
- this._options = options || {};
- };
-
- Backbone.CollectionBinder.VERSION = '1.0.0';
-
- _.extend(Backbone.CollectionBinder.prototype, Backbone.Events, {
- bind: function(collection, parentEl){
- this.unbind();
-
- if(!collection) throw 'collection must be defined';
- if(!parentEl) throw 'parentEl must be defined';
-
- this._collection = collection;
- this._elManagerFactory.setParentEl(parentEl);
-
- this._onCollectionReset();
-
- this._collection.on('add', this._onCollectionAdd, this);
- this._collection.on('remove', this._onCollectionRemove, this);
- this._collection.on('reset', this._onCollectionReset, this);
-
- },
-
- unbind: function(){
- if(this._collection !== undefined){
- this._collection.off('add', this._onCollectionAdd);
- this._collection.off('remove', this._onCollectionRemove);
- this._collection.off('reset', this._onCollectionReset);
- }
-
- this._removeAllElManagers();
- },
-
- getManagerForEl: function(el){
- var i, elManager, elManagers = _.values(this._elManagers);
-
- for(i = 0; i < elManagers.length; i++){
- elManager = elManagers[i];
-
- if(elManager.isElContained(el)){
- return elManager;
- }
- }
-
- return undefined;
- },
-
- getManagerForModel: function(model){
- var i, elManager, elManagers = _.values(this._elManagers);
-
- for(i = 0; i < elManagers.length; i++){
- elManager = elManagers[i];
-
- if(elManager.getModel() === model){
- return elManager;
- }
- }
-
- return undefined;
- },
-
- _onCollectionAdd: function(model){
- this._elManagers[model.cid] = this._elManagerFactory.makeElManager(model);
- this._elManagers[model.cid].createEl();
-
- if(this._options['autoSort']){
- this.sortRootEls();
- }
- },
-
- _onCollectionRemove: function(model){
- this._removeElManager(model);
- },
-
- _onCollectionReset: function(){
- this._removeAllElManagers();
-
- this._collection.each(function(model){
- this._onCollectionAdd(model);
- }, this);
-
- this.trigger('elsReset', this._collection);
- },
-
- _removeAllElManagers: function(){
- _.each(this._elManagers, function(elManager){
- elManager.removeEl();
- delete this._elManagers[elManager._model.cid];
- }, this);
-
- delete this._elManagers;
- this._elManagers = {};
- },
-
- _removeElManager: function(model){
- if(this._elManagers[model.cid] !== undefined){
- this._elManagers[model.cid].removeEl();
- delete this._elManagers[model.cid];
- }
- },
-
- sortRootEls: function(){
- this._collection.each(function(model, modelIndex){
- var modelElManager = this.getManagerForModel(model);
- if(modelElManager){
- var modelEl = modelElManager.getEl();
- var currentRootEls = $(this._elManagerFactory.getParentEl()).children();
-
- if(currentRootEls[modelIndex] !== modelEl[0]){
- modelEl.detach();
- modelEl.insertBefore(currentRootEls[modelIndex]);
- }
- }
- }, this);
- }
- });
-
- // The ElManagerFactory is used for els that are just html templates
- // elHtml - how the model's html will be rendered. Must have a single root element (div,span).
- // bindings (optional) - either a string which is the binding attribute (name, id, data-name, etc.) or a normal bindings hash
- Backbone.CollectionBinder.ElManagerFactory = function(elHtml, bindings){
- _.bindAll(this);
-
- this._elHtml = elHtml;
- this._bindings = bindings;
-
- if(! _.isString(this._elHtml)) throw 'elHtml must be a valid html string';
- };
-
- _.extend(Backbone.CollectionBinder.ElManagerFactory.prototype, {
- setParentEl: function(parentEl){
- this._parentEl = parentEl;
- },
-
- getParentEl: function(){
- return this._parentEl;
- },
-
- makeElManager: function(model){
-
- var elManager = {
- _model: model,
-
- createEl: function(){
-
- this._el = $(this._elHtml);
- $(this._parentEl).append(this._el);
-
- if(this._bindings){
- if(_.isString(this._bindings)){
- this._modelBinder = new Backbone.ModelBinder();
- this._modelBinder.bind(this._model, this._el, Backbone.ModelBinder.createDefaultBindings(this._el, this._bindings));
- }
- else if(_.isObject(this._bindings)){
- this._modelBinder = new Backbone.ModelBinder();
- this._modelBinder.bind(this._model, this._el, this._bindings);
- }
- else {
- throw 'Unsupported bindings type, please use a boolean or a bindings hash';
- }
- }
-
- this.trigger('elCreated', this._model, this._el);
- },
-
- removeEl: function(){
- if(this._modelBinder !== undefined){
- this._modelBinder.unbind();
- }
-
- this._el.remove();
- this.trigger('elRemoved', this._model, this._el);
- },
-
- isElContained: function(findEl){
- return this._el === findEl || $(this._el).has(findEl).length > 0;
- },
-
- getModel: function(){
- return this._model;
- },
-
- getEl: function(){
- return this._el;
- }
- };
-
- _.extend(elManager, this);
- return elManager;
- }
- });
-
-
- // The ViewManagerFactory is used for els that are created and owned by backbone views.
- // There is no bindings option because the view made by the viewCreator should take care of any binding
- // viewCreator - a callback that will create backbone view instances for a model passed to the callback
- Backbone.CollectionBinder.ViewManagerFactory = function(viewCreator){
- _.bindAll(this);
- this._viewCreator = viewCreator;
-
- if(!_.isFunction(this._viewCreator)) throw 'viewCreator must be a valid function that accepts a model and returns a backbone view';
- };
-
- _.extend(Backbone.CollectionBinder.ViewManagerFactory.prototype, {
- setParentEl: function(parentEl){
- this._parentEl = parentEl;
- },
-
- getParentEl: function(){
- return this._parentEl;
- },
-
- makeElManager: function(model){
- var elManager = {
-
- _model: model,
-
- createEl: function(){
- this._view = this._viewCreator(model);
- $(this._parentEl).append(this._view.render(this._model).el);
-
- this.trigger('elCreated', this._model, this._view);
- },
-
- removeEl: function(){
- if(this._view.close !== undefined){
- this._view.close();
- }
- else {
- this._view.$el.remove();
- console.log('warning, you should implement a close() function for your view, you might end up with zombies');
- }
-
- this.trigger('elRemoved', this._model, this._view);
- },
-
- isElContained: function(findEl){
- return this._view.el === findEl || this._view.$el.has(findEl).length > 0;
- },
-
- getModel: function(){
- return this._model;
- },
-
- getView: function(){
- return this._view;
- },
-
- getEl: function(){
- return this._view.$el;
- }
- };
-
- _.extend(elManager, this);
-
- return elManager;
- }
- });
-
-}).call(this);
diff --git a/UI/JsLibraries/backbone.debug.js b/UI/JsLibraries/backbone.debug.js
deleted file mode 100644
index 59d200245..000000000
--- a/UI/JsLibraries/backbone.debug.js
+++ /dev/null
@@ -1,131 +0,0 @@
-(function () {
- var __bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; };
-
- window.Backbone.Debug = (function () {
-
- function Debug() {
- this._hookPrototype = __bind(this._hookPrototype, this);
- this._hookMethod = __bind(this._hookMethod, this);
- this._logSync = __bind(this._logSync, this);
- this._logEvent = __bind(this._logEvent, this);
- this._saveObjects = __bind(this._saveObjects, this);
- this._hookSync = __bind(this._hookSync, this);
- this._hookEvents = __bind(this._hookEvents, this);
- this._trackObjects = __bind(this._trackObjects, this);
- this.off = __bind(this.off, this);
- this.on = __bind(this.on, this);
- this.routers = __bind(this.routers, this);
- this.views = __bind(this.views, this);
- this.models = __bind(this.models, this);
- this.collections = __bind(this.collections, this); this._options = {
- 'log:events': true,
- 'log:sync': true
- };
- this._objects = {
- Collection: {},
- Model: {},
- View: {},
- Router: {}
- };
- this._trackObjects();
- this._hookEvents();
- this._hookSync();
- }
-
- Debug.prototype.collections = function () {
- return this._objects.Collection;
- };
-
- Debug.prototype.models = function () {
- return this._objects.Model;
- };
-
- Debug.prototype.views = function () {
- return this._objects.View;
- };
-
- Debug.prototype.routers = function () {
- return this._objects.Router;
- };
-
- Debug.prototype.on = function (option) {
- if (option != null) {
- return this._options[option] = true;
- } else {
- this._options['log:events'] = true;
- return this._options['log:sync'] = true;
- }
- };
-
- Debug.prototype.off = function (option) {
- if (option != null) {
- return this._options[option] = false;
- } else {
- this._options['log:events'] = false;
- return this._options['log:sync'] = false;
- }
- };
-
- Debug.prototype._trackObjects = function () {
- this._hookPrototype('Collection', 'constructor', this._saveObjects);
- this._hookPrototype('Model', 'constructor', this._saveObjects);
- this._hookPrototype('View', 'constructor', this._saveObjects);
- return this._hookPrototype('Router', 'constructor', this._saveObjects);
- };
-
- Debug.prototype._hookEvents = function () {
- this._hookPrototype('Collection', 'trigger', this._logEvent);
- this._hookPrototype('Model', 'trigger', this._logEvent);
- this._hookPrototype('View', 'trigger', this._logEvent);
- return this._hookPrototype('Router', 'trigger', this._logEvent);
- };
-
- Debug.prototype._hookSync = function () {
- return this._hookMethod('sync', this._logSync);
- };
-
- Debug.prototype._saveObjects = function (type, method, object) {
- return this._objects[type][object.constructor.name + ':' + object.cid] = object;
- };
-
- Debug.prototype._logEvent = function (parent_object, method, object, args) {
- if (this._options['log:events']) {
- return console.log("" + args[0] + " - ", object);
- }
- };
-
- Debug.prototype._logSync = function (method, object, args) {
- if (this._options['log:sync'] === true) {
- return console.log("sync - " + args[0], args[1]);
- }
- };
-
- Debug.prototype._hookMethod = function (method, action) {
- var original;
- original = window.Backbone[method];
- return window.Backbone[method] = function () {
- var ret;
- ret = original.apply(this, arguments);
- action(method, this, arguments);
- return ret;
- };
- };
-
- Debug.prototype._hookPrototype = function (object, method, action) {
- var original;
- original = window.Backbone[object].prototype[method];
- return window.Backbone[object].prototype[method] = function () {
- var ret;
- ret = original.apply(this, arguments);
- action(object, method, this, arguments);
- return ret;
- };
- };
-
- return Debug;
-
- })();
-
- window.Backbone.debug = new Backbone.Debug();
-
-}).call(this);
\ No newline at end of file
diff --git a/UI/JsLibraries/backbone.marionette.viewswapper.js b/UI/JsLibraries/backbone.marionette.viewswapper.js
deleted file mode 100644
index bd19cbb80..000000000
--- a/UI/JsLibraries/backbone.marionette.viewswapper.js
+++ /dev/null
@@ -1,212 +0,0 @@
-https://github.com/marionettejs/backbone.marionette/blob/viewswap/docs/marionette.viewswapper.md
-
-// View Swapper
-// ------------
-//
-// Switch out views based on events that are triggered
-// by the currently displayed view. Enables easy "edit in
-// place" features, "loading" screens, and more.
-
- Marionette.ViewSwapper = Marionette.View.extend({
- constructor: function (options) {
- this._swapperViews = {};
- this._swapperBindings = new Marionette.EventBinder();
- this._currentViewBindings = new Marionette.EventBinder();
-
- Marionette.View.prototype.constructor.apply(this, arguments);
-
- this.views = Marionette.getOption(this, "views");
- this.swapOn = Marionette.getOption(this, "swapOn");
- this.initialView = Marionette.getOption(this, "initialView");
-
- this._setupViewEvents("swapper", this, this._swapperBindings);
- },
-
- // Render the current view. If no current view is set, it
- // will render the `initialView` that was configured.
- render: function () {
- // set up the initial view to display, on first render
- if (!this.currentView) {
- var initialViewName = Marionette.getOption(this, "initialView");
- this._swapView(initialViewName);
- }
-
- // render and show the new view
- this.currentView.render();
- this.$el.append(this.currentView.$el);
-
- // setup a callback for the showView call to recieve
- var done = _.bind(function () {
- // trigger show/onShow on the previous view
- if (this.currentView) {
- Marionette.triggerMethod.call(this.currentView, "show");
- Marionette.triggerMethod.call(this, "swap:in", this.currentView);
- }
- }, this);
-
- // show the view, passing it the done callback
- this.showView(this.currentView, done);
- },
-
- // Show a view that is being swapped in. Override this method to
- // set up your own custom fade in / show method
- showView: function (view, done) {
- view.$el.show();
- done();
- },
-
- // Hide a view that is being swapped out. Override this method to
- // set up your own custom fade out / hide method
- hideView: function (view, done) {
- view.$el.hide();
- done();
- },
-
- // Ensure the views that were configured for this view swapper get closed
- close: function () {
-
- // Close all of the configured views that we are swapping between
- _.each(this.views, function (view, name) {
- view.close();
- });
-
- // unbind all the events, and clean up any decorator views
- this._swapperViews = {};
- this._currentViewBindings.unbindAll();
- this._swapperBindings.unbindAll();
-
- // Close the base view that we extended from
- Marionette.View.prototype.close.apply(this, arguments);
- },
-
- // Get a view by name, throwing an exception if the view instance
- // is not found.
- _getView: function (viewName) {
- var originalView, error, views;
- var swapperView = this._swapperViews[viewName];
-
- // Do not allow the name "swapper" to be used as a target view
- // or initial view. This is reserved for the ViewSwapper instance,
- // when configuring `swapOn` events
- if (viewName === "swapper") {
- error = new Error("Cannot display 'swapper' as a view.");
- error.name = "InvalidViewName";
- throw error;
- }
-
- // Do we have a view with the specified name?
- if (!swapperView) {
- originalView = this.views[viewName];
-
- // No view, so throw an exception
- if (!originalView) {
- error = new Error("Cannot show view in ViewSwapper. View '" + viewName + "' not found.");
- error.name = "ViewNotFoundError";
- throw error;
- }
-
- // Found the view, so build a Decorator around it
- swapperView = this._buildSwapperView(originalView, viewName);
- this._swapperViews[viewName] = swapperView;
- }
-
- return swapperView;
- },
-
- // Decorate the configured view with information that the view swapper
- // needs, to keep track of the view's current state.
- _buildSwapperView: function (originalView, viewName) {
- var swapperView = Marionette.createObject(originalView);
- _.extend(swapperView, {
-
- viewName: viewName,
- originalView: originalView,
-
- // Prevent the underlying view from being rendered more than once
- render: function () {
- var value;
-
- if (this._hasBeenRendered) {
- return this;
- } else {
-
- // prevent any more rendering
- this._hasBeenRendered = true;
-
- // do the render
- value = originalView.render.apply(originalView, arguments);
-
- // trigger render/onRender
- Marionette.triggerMethod.call(this, "render");
-
- // return whatever was sent back to us
- return value;
- }
- }
-
- });
-
- return swapperView;
- },
-
- // Set up the event handlers for the individual views, so that the
- // swapping can happen when a view event is triggered
- _setupViewEvents: function (viewName, view, bindings) {
- if (!this.swapOn || !this.swapOn[viewName]) { return; }
- var that = this;
-
- // default to current view bindings, unless otherwise specified
- if (!bindings) {
- bindings = this._currentViewBindings;
- }
-
- // close the previous event bindings
- bindings.unbindAll();
-
- // set up the new view's event bindings
- _.each(this.swapOn[viewName], function (targetViewName, eventName) {
-
- bindings.bindTo(view, eventName, function () {
- that._swapView(targetViewName);
- });
-
- });
- },
-
- // Do the swapping of the views to the new view, by name
- _swapView: function (viewName) {
-
- // only swap views if the target view is not the same
- // as the current view
- var view = this._getView(viewName);
- if (view === this.currentView) {
- return;
- }
-
- // Provide a callback function that will switch over to
- // the new view, when called
- var done = _.bind(function () {
-
- // trigger hide/onHide on the previous view
- if (this.currentView) {
- Marionette.triggerMethod.call(this.currentView, "hide");
- Marionette.triggerMethod.call(this, "swap:out", this.currentView);
- }
-
- // get the next view, configure it's events and render it
- this._setupViewEvents(viewName, view);
- this.currentView = view;
- this.render();
-
- }, this);
-
- if (this.currentView) {
- // if we have a current view, hide it so that the new
- // view can be show in it's place
- this.hideView(this.currentView, done);
- } else {
- // no current view, so just switch to the new view
- done();
- }
- }
- });
\ No newline at end of file
diff --git a/UI/JsLibraries/backbone.relational.js b/UI/JsLibraries/backbone.relational.js
deleted file mode 100644
index 16fd296a8..000000000
--- a/UI/JsLibraries/backbone.relational.js
+++ /dev/null
@@ -1,1860 +0,0 @@
-/* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */
-/**
- * Backbone-relational.js 0.8.5
- * (c) 2011-2013 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors)
- *
- * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt.
- * For details and documentation: https://github.com/PaulUithol/Backbone-relational.
- * Depends on Backbone (and thus on Underscore as well): https://github.com/documentcloud/backbone.
- */
-( function( undefined ) {
- "use strict";
-
- /**
- * CommonJS shim
- **/
- var _, Backbone, exports;
- if ( typeof window === 'undefined' ) {
- _ = require( 'underscore' );
- Backbone = require( 'backbone' );
- exports = module.exports = Backbone;
- }
- else {
- _ = window._;
- Backbone = window.Backbone;
- exports = window;
- }
-
- Backbone.Relational = {
- showWarnings: true
- };
-
- /**
- * Semaphore mixin; can be used as both binary and counting.
- **/
- Backbone.Semaphore = {
- _permitsAvailable: null,
- _permitsUsed: 0,
-
- acquire: function() {
- if ( this._permitsAvailable && this._permitsUsed >= this._permitsAvailable ) {
- throw new Error( 'Max permits acquired' );
- }
- else {
- this._permitsUsed++;
- }
- },
-
- release: function() {
- if ( this._permitsUsed === 0 ) {
- throw new Error( 'All permits released' );
- }
- else {
- this._permitsUsed--;
- }
- },
-
- isLocked: function() {
- return this._permitsUsed > 0;
- },
-
- setAvailablePermits: function( amount ) {
- if ( this._permitsUsed > amount ) {
- throw new Error( 'Available permits cannot be less than used permits' );
- }
- this._permitsAvailable = amount;
- }
- };
-
- /**
- * A BlockingQueue that accumulates items while blocked (via 'block'),
- * and processes them when unblocked (via 'unblock').
- * Process can also be called manually (via 'process').
- */
- Backbone.BlockingQueue = function() {
- this._queue = [];
- };
- _.extend( Backbone.BlockingQueue.prototype, Backbone.Semaphore, {
- _queue: null,
-
- add: function( func ) {
- if ( this.isBlocked() ) {
- this._queue.push( func );
- }
- else {
- func();
- }
- },
-
- process: function() {
- while ( this._queue && this._queue.length ) {
- this._queue.shift()();
- }
- },
-
- block: function() {
- this.acquire();
- },
-
- unblock: function() {
- this.release();
- if ( !this.isBlocked() ) {
- this.process();
- }
- },
-
- isBlocked: function() {
- return this.isLocked();
- }
- });
- /**
- * Global event queue. Accumulates external events ('add:', 'remove:' and 'change:')
- * until the top-level object is fully initialized (see 'Backbone.RelationalModel').
- */
- Backbone.Relational.eventQueue = new Backbone.BlockingQueue();
-
- /**
- * Backbone.Store keeps track of all created (and destruction of) Backbone.RelationalModel.
- * Handles lookup for relations.
- */
- Backbone.Store = function() {
- this._collections = [];
- this._reverseRelations = [];
- this._orphanRelations = [];
- this._subModels = [];
- this._modelScopes = [ exports ];
- };
- _.extend( Backbone.Store.prototype, Backbone.Events, {
- /**
- * Create a new `Relation`.
- * @param {Backbone.RelationalModel} [model]
- * @param {Object} relation
- * @param {Object} [options]
- */
- initializeRelation: function( model, relation, options ) {
- var type = !_.isString( relation.type ) ? relation.type : Backbone[ relation.type ] || this.getObjectByName( relation.type );
- if ( type && type.prototype instanceof Backbone.Relation ) {
- new type( model, relation, options ); // Also pushes the new Relation into `model._relations`
- }
- else {
- Backbone.Relational.showWarnings && typeof console !== 'undefined' && console.warn( 'Relation=%o; missing or invalid relation type!', relation );
- }
- },
-
- /**
- * Add a scope for `getObjectByName` to look for model types by name.
- * @param {Object} scope
- */
- addModelScope: function( scope ) {
- this._modelScopes.push( scope );
- },
-
- /**
- * Remove a scope.
- * @param {Object} scope
- */
- removeModelScope: function( scope ) {
- this._modelScopes = _.without( this._modelScopes, scope );
- },
-
- /**
- * Add a set of subModelTypes to the store, that can be used to resolve the '_superModel'
- * for a model later in 'setupSuperModel'.
- *
- * @param {Backbone.RelationalModel} subModelTypes
- * @param {Backbone.RelationalModel} superModelType
- */
- addSubModels: function( subModelTypes, superModelType ) {
- this._subModels.push({
- 'superModelType': superModelType,
- 'subModels': subModelTypes
- });
- },
-
- /**
- * Check if the given modelType is registered as another model's subModel. If so, add it to the super model's
- * '_subModels', and set the modelType's '_superModel', '_subModelTypeName', and '_subModelTypeAttribute'.
- *
- * @param {Backbone.RelationalModel} modelType
- */
- setupSuperModel: function( modelType ) {
- _.find( this._subModels, function( subModelDef ) {
- return _.find( subModelDef.subModels || [], function( subModelTypeName, typeValue ) {
- var subModelType = this.getObjectByName( subModelTypeName );
-
- if ( modelType === subModelType ) {
- // Set 'modelType' as a child of the found superModel
- subModelDef.superModelType._subModels[ typeValue ] = modelType;
-
- // Set '_superModel', '_subModelTypeValue', and '_subModelTypeAttribute' on 'modelType'.
- modelType._superModel = subModelDef.superModelType;
- modelType._subModelTypeValue = typeValue;
- modelType._subModelTypeAttribute = subModelDef.superModelType.prototype.subModelTypeAttribute;
- return true;
- }
- }, this );
- }, this );
- },
-
- /**
- * Add a reverse relation. Is added to the 'relations' property on model's prototype, and to
- * existing instances of 'model' in the store as well.
- * @param {Object} relation
- * @param {Backbone.RelationalModel} relation.model
- * @param {String} relation.type
- * @param {String} relation.key
- * @param {String|Object} relation.relatedModel
- */
- addReverseRelation: function( relation ) {
- var exists = _.any( this._reverseRelations, function( rel ) {
- return _.all( relation || [], function( val, key ) {
- return val === rel[ key ];
- });
- });
-
- if ( !exists && relation.model && relation.type ) {
- this._reverseRelations.push( relation );
- this._addRelation( relation.model, relation );
- this.retroFitRelation( relation );
- }
- },
-
- /**
- * Deposit a `relation` for which the `relatedModel` can't be resolved at the moment.
- *
- * @param {Object} relation
- */
- addOrphanRelation: function( relation ) {
- var exists = _.any( this._orphanRelations, function( rel ) {
- return _.all( relation || [], function( val, key ) {
- return val === rel[ key ];
- });
- });
-
- if ( !exists && relation.model && relation.type ) {
- this._orphanRelations.push( relation );
- }
- },
-
- /**
- * Try to initialize any `_orphanRelation`s
- */
- processOrphanRelations: function() {
- // Make sure to operate on a copy since we're removing while iterating
- _.each( this._orphanRelations.slice( 0 ), function( rel ) {
- var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
- if ( relatedModel ) {
- this.initializeRelation( null, rel );
- this._orphanRelations = _.without( this._orphanRelations, rel );
- }
- }, this );
- },
-
- /**
- *
- * @param {Backbone.RelationalModel.constructor} type
- * @param {Object} relation
- * @private
- */
- _addRelation: function( type, relation ) {
- if ( !type.prototype.relations ) {
- type.prototype.relations = [];
- }
- type.prototype.relations.push( relation );
-
- _.each( type._subModels || [], function( subModel ) {
- this._addRelation( subModel, relation );
- }, this );
- },
-
- /**
- * Add a 'relation' to all existing instances of 'relation.model' in the store
- * @param {Object} relation
- */
- retroFitRelation: function( relation ) {
- var coll = this.getCollection( relation.model, false );
- coll && coll.each( function( model ) {
- if ( !( model instanceof relation.model ) ) {
- return;
- }
-
- new relation.type( model, relation );
- }, this );
- },
-
- /**
- * Find the Store's collection for a certain type of model.
- * @param {Backbone.RelationalModel} type
- * @param {Boolean} [create=true] Should a collection be created if none is found?
- * @return {Backbone.Collection} A collection if found (or applicable for 'model'), or null
- */
- getCollection: function( type, create ) {
- if ( type instanceof Backbone.RelationalModel ) {
- type = type.constructor;
- }
-
- var rootModel = type;
- while ( rootModel._superModel ) {
- rootModel = rootModel._superModel;
- }
-
- var coll = _.findWhere( this._collections, { model: rootModel } );
-
- if ( !coll && create !== false ) {
- coll = this._createCollection( rootModel );
- }
-
- return coll;
- },
-
- /**
- * Find a model type on one of the modelScopes by name. Names are split on dots.
- * @param {String} name
- * @return {Object}
- */
- getObjectByName: function( name ) {
- var parts = name.split( '.' ),
- type = null;
-
- _.find( this._modelScopes, function( scope ) {
- type = _.reduce( parts || [], function( memo, val ) {
- return memo ? memo[ val ] : undefined;
- }, scope );
-
- if ( type && type !== scope ) {
- return true;
- }
- }, this );
-
- return type;
- },
-
- _createCollection: function( type ) {
- var coll;
-
- // If 'type' is an instance, take its constructor
- if ( type instanceof Backbone.RelationalModel ) {
- type = type.constructor;
- }
-
- // Type should inherit from Backbone.RelationalModel.
- if ( type.prototype instanceof Backbone.RelationalModel ) {
- coll = new Backbone.Collection();
- coll.model = type;
-
- this._collections.push( coll );
- }
-
- return coll;
- },
-
- /**
- * Find the attribute that is to be used as the `id` on a given object
- * @param type
- * @param {String|Number|Object|Backbone.RelationalModel} item
- * @return {String|Number}
- */
- resolveIdForItem: function( type, item ) {
- var id = _.isString( item ) || _.isNumber( item ) ? item : null;
-
- if ( id === null ) {
- if ( item instanceof Backbone.RelationalModel ) {
- id = item.id;
- }
- else if ( _.isObject( item ) ) {
- id = item[ type.prototype.idAttribute ];
- }
- }
-
- // Make all falsy values `null` (except for 0, which could be an id.. see '/issues/179')
- if ( !id && id !== 0 ) {
- id = null;
- }
-
- return id;
- },
-
- /**
- * Find a specific model of a certain `type` in the store
- * @param type
- * @param {String|Number|Object|Backbone.RelationalModel} item
- */
- find: function( type, item ) {
- var id = this.resolveIdForItem( type, item );
- var coll = this.getCollection( type );
-
- // Because the found object could be of any of the type's superModel
- // types, only return it if it's actually of the type asked for.
- if ( coll ) {
- var obj = coll.get( id );
-
- if ( obj instanceof type ) {
- return obj;
- }
- }
-
- return null;
- },
-
- /**
- * Add a 'model' to its appropriate collection. Retain the original contents of 'model.collection'.
- * @param {Backbone.RelationalModel} model
- */
- register: function( model ) {
- var coll = this.getCollection( model );
-
- if ( coll ) {
- var modelColl = model.collection;
- coll.add( model );
- this.listenTo( model, 'destroy', this.unregister, this );
- model.collection = modelColl;
- }
- },
-
- /**
- * Check if the given model may use the given `id`
- * @param model
- * @param [id]
- */
- checkId: function( model, id ) {
- var coll = this.getCollection( model ),
- duplicate = coll && coll.get( id );
-
- if ( duplicate && model !== duplicate ) {
- if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
- console.warn( 'Duplicate id! Old RelationalModel=%o, new RelationalModel=%o', duplicate, model );
- }
-
- throw new Error( "Cannot instantiate more than one Backbone.RelationalModel with the same id per type!" );
- }
- },
-
- /**
- * Explicitly update a model's id in its store collection
- * @param {Backbone.RelationalModel} model
- */
- update: function( model ) {
- var coll = this.getCollection( model );
- // This triggers updating the lookup indices kept in a collection
- coll._onModelEvent( 'change:' + model.idAttribute, model, coll );
-
- // Trigger an event on model so related models (having the model's new id in their keyContents) can add it.
- model.trigger( 'relational:change:id', model, coll );
- },
-
- /**
- * Remove a 'model' from the store.
- * @param {Backbone.RelationalModel} model
- */
- unregister: function( model ) {
- this.stopListening( model, 'destroy', this.unregister );
- var coll = this.getCollection( model );
- coll && coll.remove( model );
- },
-
- /**
- * Reset the `store` to it's original state. The `reverseRelations` are kept though, since attempting to
- * re-initialize these on models would lead to a large amount of warnings.
- */
- reset: function() {
- this.stopListening();
- this._collections = [];
- this._subModels = [];
- this._modelScopes = [ exports ];
- }
- });
- Backbone.Relational.store = new Backbone.Store();
-
- /**
- * The main Relation class, from which 'HasOne' and 'HasMany' inherit. Internally, 'relational:' events
- * are used to regulate addition and removal of models from relations.
- *
- * @param {Backbone.RelationalModel} [instance] Model that this relation is created for. If no model is supplied,
- * Relation just tries to instantiate it's `reverseRelation` if specified, and bails out after that.
- * @param {Object} options
- * @param {string} options.key
- * @param {Backbone.RelationalModel.constructor} options.relatedModel
- * @param {Boolean|String} [options.includeInJSON=true] Serialize the given attribute for related model(s)' in toJSON, or just their ids.
- * @param {Boolean} [options.createModels=true] Create objects from the contents of keys if the object is not found in Backbone.store.
- * @param {Object} [options.reverseRelation] Specify a bi-directional relation. If provided, Relation will reciprocate
- * the relation to the 'relatedModel'. Required and optional properties match 'options', except that it also needs
- * {Backbone.Relation|String} type ('HasOne' or 'HasMany').
- * @param {Object} opts
- */
- Backbone.Relation = function( instance, options, opts ) {
- this.instance = instance;
- // Make sure 'options' is sane, and fill with defaults from subclasses and this object's prototype
- options = _.isObject( options ) ? options : {};
- this.reverseRelation = _.defaults( options.reverseRelation || {}, this.options.reverseRelation );
- this.options = _.defaults( options, this.options, Backbone.Relation.prototype.options );
-
- this.reverseRelation.type = !_.isString( this.reverseRelation.type ) ? this.reverseRelation.type :
- Backbone[ this.reverseRelation.type ] || Backbone.Relational.store.getObjectByName( this.reverseRelation.type );
-
- this.key = this.options.key;
- this.keySource = this.options.keySource || this.key;
- this.keyDestination = this.options.keyDestination || this.keySource || this.key;
-
- this.model = this.options.model || this.instance.constructor;
- this.relatedModel = this.options.relatedModel;
- if ( _.isString( this.relatedModel ) ) {
- this.relatedModel = Backbone.Relational.store.getObjectByName( this.relatedModel );
- }
-
- if ( !this.checkPreconditions() ) {
- return;
- }
-
- // Add the reverse relation on 'relatedModel' to the store's reverseRelations
- if ( !this.options.isAutoRelation && this.reverseRelation.type && this.reverseRelation.key ) {
- Backbone.Relational.store.addReverseRelation( _.defaults( {
- isAutoRelation: true,
- model: this.relatedModel,
- relatedModel: this.model,
- reverseRelation: this.options // current relation is the 'reverseRelation' for its own reverseRelation
- },
- this.reverseRelation // Take further properties from this.reverseRelation (type, key, etc.)
- ) );
- }
-
- if ( instance ) {
- var contentKey = this.keySource;
- if ( contentKey !== this.key && typeof this.instance.get( this.key ) === 'object' ) {
- contentKey = this.key;
- }
-
- this.setKeyContents( this.instance.get( contentKey ) );
- this.relatedCollection = Backbone.Relational.store.getCollection( this.relatedModel );
-
- // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'.
- if ( this.keySource !== this.key ) {
- this.instance.unset( this.keySource, { silent: true } );
- }
-
- // Add this Relation to instance._relations
- this.instance._relations[ this.key ] = this;
-
- this.initialize( opts );
-
- if ( this.options.autoFetch ) {
- this.instance.fetchRelated( this.key, _.isObject( this.options.autoFetch ) ? this.options.autoFetch : {} );
- }
-
- // When 'relatedModel' are created or destroyed, check if it affects this relation.
- this.listenTo( this.instance, 'destroy', this.destroy )
- .listenTo( this.relatedCollection, 'relational:add relational:change:id', this.tryAddRelated )
- .listenTo( this.relatedCollection, 'relational:remove', this.removeRelated )
- }
- };
- // Fix inheritance :\
- Backbone.Relation.extend = Backbone.Model.extend;
- // Set up all inheritable **Backbone.Relation** properties and methods.
- _.extend( Backbone.Relation.prototype, Backbone.Events, Backbone.Semaphore, {
- options: {
- createModels: true,
- includeInJSON: true,
- isAutoRelation: false,
- autoFetch: false,
- parse: false
- },
-
- instance: null,
- key: null,
- keyContents: null,
- relatedModel: null,
- relatedCollection: null,
- reverseRelation: null,
- related: null,
-
- /**
- * Check several pre-conditions.
- * @return {Boolean} True if pre-conditions are satisfied, false if they're not.
- */
- checkPreconditions: function() {
- var i = this.instance,
- k = this.key,
- m = this.model,
- rm = this.relatedModel,
- warn = Backbone.Relational.showWarnings && typeof console !== 'undefined';
-
- if ( !m || !k || !rm ) {
- warn && console.warn( 'Relation=%o: missing model, key or relatedModel (%o, %o, %o).', this, m, k, rm );
- return false;
- }
- // Check if the type in 'model' inherits from Backbone.RelationalModel
- if ( !( m.prototype instanceof Backbone.RelationalModel ) ) {
- warn && console.warn( 'Relation=%o: model does not inherit from Backbone.RelationalModel (%o).', this, i );
- return false;
- }
- // Check if the type in 'relatedModel' inherits from Backbone.RelationalModel
- if ( !( rm.prototype instanceof Backbone.RelationalModel ) ) {
- warn && console.warn( 'Relation=%o: relatedModel does not inherit from Backbone.RelationalModel (%o).', this, rm );
- return false;
- }
- // Check if this is not a HasMany, and the reverse relation is HasMany as well
- if ( this instanceof Backbone.HasMany && this.reverseRelation.type === Backbone.HasMany ) {
- warn && console.warn( 'Relation=%o: relation is a HasMany, and the reverseRelation is HasMany as well.', this );
- return false;
- }
- // Check if we're not attempting to create a relationship on a `key` that's already used.
- if ( i && _.keys( i._relations ).length ) {
- var existing = _.find( i._relations, function( rel ) {
- return rel.key === k;
- }, this );
-
- if ( existing ) {
- warn && console.warn( 'Cannot create relation=%o on %o for model=%o: already taken by relation=%o.',
- this, k, i, existing );
- return false;
- }
- }
-
- return true;
- },
-
- /**
- * Set the related model(s) for this relation
- * @param {Backbone.Model|Backbone.Collection} related
- */
- setRelated: function( related ) {
- this.related = related;
-
- this.instance.acquire();
- this.instance.attributes[ this.key ] = related;
- this.instance.release();
- },
-
- /**
- * Determine if a relation (on a different RelationalModel) is the reverse
- * relation of the current one.
- * @param {Backbone.Relation} relation
- * @return {Boolean}
- */
- _isReverseRelation: function( relation ) {
- return relation.instance instanceof this.relatedModel && this.reverseRelation.key === relation.key &&
- this.key === relation.reverseRelation.key;
- },
-
- /**
- * Get the reverse relations (pointing back to 'this.key' on 'this.instance') for the currently related model(s).
- * @param {Backbone.RelationalModel} [model] Get the reverse relations for a specific model.
- * If not specified, 'this.related' is used.
- * @return {Backbone.Relation[]}
- */
- getReverseRelations: function( model ) {
- var reverseRelations = [];
- // Iterate over 'model', 'this.related.models' (if this.related is a Backbone.Collection), or wrap 'this.related' in an array.
- var models = !_.isUndefined( model ) ? [ model ] : this.related && ( this.related.models || [ this.related ] );
- _.each( models || [], function( related ) {
- _.each( related.getRelations() || [], function( relation ) {
- if ( this._isReverseRelation( relation ) ) {
- reverseRelations.push( relation );
- }
- }, this );
- }, this );
-
- return reverseRelations;
- },
-
- /**
- * When `this.instance` is destroyed, cleanup our relations.
- * Get reverse relation, call removeRelated on each.
- */
- destroy: function() {
- this.stopListening();
-
- if ( this instanceof Backbone.HasOne ) {
- this.setRelated( null );
- }
- else if ( this instanceof Backbone.HasMany ) {
- this.setRelated( this._prepareCollection() );
- }
-
- _.each( this.getReverseRelations(), function( relation ) {
- relation.removeRelated( this.instance );
- }, this );
- }
- });
-
- Backbone.HasOne = Backbone.Relation.extend({
- options: {
- reverseRelation: { type: 'HasMany' }
- },
-
- initialize: function( opts ) {
- this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange );
-
- var related = this.findRelated( opts );
- this.setRelated( related );
-
- // Notify new 'related' object of the new relation.
- _.each( this.getReverseRelations(), function( relation ) {
- relation.addRelated( this.instance, opts );
- }, this );
- },
-
- /**
- * Find related Models.
- * @param {Object} [options]
- * @return {Backbone.Model}
- */
- findRelated: function( options ) {
- var related = null;
-
- options = _.defaults( { parse: this.options.parse }, options );
-
- if ( this.keyContents instanceof this.relatedModel ) {
- related = this.keyContents;
- }
- else if ( this.keyContents || this.keyContents === 0 ) { // since 0 can be a valid `id` as well
- var opts = _.defaults( { create: this.options.createModels }, options );
- related = this.relatedModel.findOrCreate( this.keyContents, opts );
- }
-
- // Nullify `keyId` if we have a related model; in case it was already part of the relation
- if ( this.related ) {
- this.keyId = null;
- }
-
- return related;
- },
-
- /**
- * Normalize and reduce `keyContents` to an `id`, for easier comparison
- * @param {String|Number|Backbone.Model} keyContents
- */
- setKeyContents: function( keyContents ) {
- this.keyContents = keyContents;
- this.keyId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, this.keyContents );
- },
-
- /**
- * Event handler for `change:`.
- * If the key is changed, notify old & new reverse relations and initialize the new relation.
- */
- onChange: function( model, attr, options ) {
- // Don't accept recursive calls to onChange (like onChange->findRelated->findOrCreate->initializeRelations->addRelated->onChange)
- if ( this.isLocked() ) {
- return;
- }
- this.acquire();
- options = options ? _.clone( options ) : {};
-
- // 'options.__related' is set by 'addRelated'/'removeRelated'. If it is set, the change
- // is the result of a call from a relation. If it's not, the change is the result of
- // a 'set' call on this.instance.
- var changed = _.isUndefined( options.__related ),
- oldRelated = changed ? this.related : options.__related;
-
- if ( changed ) {
- this.setKeyContents( attr );
- var related = this.findRelated( options );
- this.setRelated( related );
- }
-
- // Notify old 'related' object of the terminated relation
- if ( oldRelated && this.related !== oldRelated ) {
- _.each( this.getReverseRelations( oldRelated ), function( relation ) {
- relation.removeRelated( this.instance, null, options );
- }, this );
- }
-
- // Notify new 'related' object of the new relation. Note we do re-apply even if this.related is oldRelated;
- // that can be necessary for bi-directional relations if 'this.instance' was created after 'this.related'.
- // In that case, 'this.instance' will already know 'this.related', but the reverse might not exist yet.
- _.each( this.getReverseRelations(), function( relation ) {
- relation.addRelated( this.instance, options );
- }, this );
-
- // Fire the 'change:' event if 'related' was updated
- if ( !options.silent && this.related !== oldRelated ) {
- var dit = this;
- this.changed = true;
- Backbone.Relational.eventQueue.add( function() {
- dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true );
- dit.changed = false;
- });
- }
- this.release();
- },
-
- /**
- * If a new 'this.relatedModel' appears in the 'store', try to match it to the last set 'keyContents'
- */
- tryAddRelated: function( model, coll, options ) {
- if ( ( this.keyId || this.keyId === 0 ) && model.id === this.keyId ) { // since 0 can be a valid `id` as well
- this.addRelated( model, options );
- this.keyId = null;
- }
- },
-
- addRelated: function( model, options ) {
- // Allow 'model' to set up its relations before proceeding.
- // (which can result in a call to 'addRelated' from a relation of 'model')
- var dit = this;
- model.queue( function() {
- if ( model !== dit.related ) {
- var oldRelated = dit.related || null;
- dit.setRelated( model );
- dit.onChange( dit.instance, model, _.defaults( { __related: oldRelated }, options ) );
- }
- });
- },
-
- removeRelated: function( model, coll, options ) {
- if ( !this.related ) {
- return;
- }
-
- if ( model === this.related ) {
- var oldRelated = this.related || null;
- this.setRelated( null );
- this.onChange( this.instance, model, _.defaults( { __related: oldRelated }, options ) );
- }
- }
- });
-
- Backbone.HasMany = Backbone.Relation.extend({
- collectionType: null,
-
- options: {
- reverseRelation: { type: 'HasOne' },
- collectionType: Backbone.Collection,
- collectionKey: true,
- collectionOptions: {}
- },
-
- initialize: function( opts ) {
- this.listenTo( this.instance, 'relational:change:' + this.key, this.onChange );
-
- // Handle a custom 'collectionType'
- this.collectionType = this.options.collectionType;
- if ( _.isString( this.collectionType ) ) {
- this.collectionType = Backbone.Relational.store.getObjectByName( this.collectionType );
- }
- if ( this.collectionType !== Backbone.Collection && !( this.collectionType.prototype instanceof Backbone.Collection ) ) {
- throw new Error( '`collectionType` must inherit from Backbone.Collection' );
- }
-
- var related = this.findRelated( opts );
- this.setRelated( related );
- },
-
- /**
- * Bind events and setup collectionKeys for a collection that is to be used as the backing store for a HasMany.
- * If no 'collection' is supplied, a new collection will be created of the specified 'collectionType' option.
- * @param {Backbone.Collection} [collection]
- * @return {Backbone.Collection}
- */
- _prepareCollection: function( collection ) {
- if ( this.related ) {
- this.stopListening( this.related );
- }
-
- if ( !collection || !( collection instanceof Backbone.Collection ) ) {
- var options = _.isFunction( this.options.collectionOptions ) ?
- this.options.collectionOptions( this.instance ) : this.options.collectionOptions;
-
- collection = new this.collectionType( null, options );
- }
-
- collection.model = this.relatedModel;
-
- if ( this.options.collectionKey ) {
- var key = this.options.collectionKey === true ? this.options.reverseRelation.key : this.options.collectionKey;
-
- if ( collection[ key ] && collection[ key ] !== this.instance ) {
- if ( Backbone.Relational.showWarnings && typeof console !== 'undefined' ) {
- console.warn( 'Relation=%o; collectionKey=%s already exists on collection=%o', this, key, this.options.collectionKey );
- }
- }
- else if ( key ) {
- collection[ key ] = this.instance;
- }
- }
-
- this.listenTo( collection, 'relational:add', this.handleAddition )
- .listenTo( collection, 'relational:remove', this.handleRemoval )
- .listenTo( collection, 'relational:reset', this.handleReset );
-
- return collection;
- },
-
- /**
- * Find related Models.
- * @param {Object} [options]
- * @return {Backbone.Collection}
- */
- findRelated: function( options ) {
- var related = null;
-
- options = _.defaults( { parse: this.options.parse }, options );
-
- // Replace 'this.related' by 'this.keyContents' if it is a Backbone.Collection
- if ( this.keyContents instanceof Backbone.Collection ) {
- this._prepareCollection( this.keyContents );
- related = this.keyContents;
- }
- // Otherwise, 'this.keyContents' should be an array of related object ids.
- // Re-use the current 'this.related' if it is a Backbone.Collection; otherwise, create a new collection.
- else {
- var toAdd = [];
-
- _.each( this.keyContents, function( attributes ) {
- if ( attributes instanceof this.relatedModel ) {
- var model = attributes;
- }
- else {
- // If `merge` is true, update models here, instead of during update.
- model = this.relatedModel.findOrCreate( attributes,
- _.extend( { merge: true }, options, { create: this.options.createModels } )
- );
- }
-
- model && toAdd.push( model );
- }, this );
-
- if ( this.related instanceof Backbone.Collection ) {
- related = this.related;
- }
- else {
- related = this._prepareCollection();
- }
-
- // By now, both `merge` and `parse` will already have been executed for models if they were specified.
- // Disable them to prevent additional calls.
- related.set( toAdd, _.defaults( { merge: false, parse: false }, options ) );
- }
-
- // Remove entries from `keyIds` that were already part of the relation (and are thus 'unchanged')
- this.keyIds = _.difference( this.keyIds, _.pluck( related.models, 'id' ) );
-
- return related;
- },
-
- /**
- * Normalize and reduce `keyContents` to a list of `ids`, for easier comparison
- * @param {String|Number|String[]|Number[]|Backbone.Collection} keyContents
- */
- setKeyContents: function( keyContents ) {
- this.keyContents = keyContents instanceof Backbone.Collection ? keyContents : null;
- this.keyIds = [];
-
- if ( !this.keyContents && ( keyContents || keyContents === 0 ) ) { // since 0 can be a valid `id` as well
- // Handle cases the an API/user supplies just an Object/id instead of an Array
- this.keyContents = _.isArray( keyContents ) ? keyContents : [ keyContents ];
-
- _.each( this.keyContents, function( item ) {
- var itemId = Backbone.Relational.store.resolveIdForItem( this.relatedModel, item );
- if ( itemId || itemId === 0 ) {
- this.keyIds.push( itemId );
- }
- }, this );
- }
- },
-
- /**
- * Event handler for `change:`.
- * If the contents of the key are changed, notify old & new reverse relations and initialize the new relation.
- */
- onChange: function( model, attr, options ) {
- options = options ? _.clone( options ) : {};
- this.setKeyContents( attr );
- this.changed = false;
-
- var related = this.findRelated( options );
- this.setRelated( related );
-
- if ( !options.silent ) {
- var dit = this;
- Backbone.Relational.eventQueue.add( function() {
- // The `changed` flag can be set in `handleAddition` or `handleRemoval`
- if ( dit.changed ) {
- dit.instance.trigger( 'change:' + dit.key, dit.instance, dit.related, options, true );
- dit.changed = false;
- }
- });
- }
- },
-
- /**
- * When a model is added to a 'HasMany', trigger 'add' on 'this.instance' and notify reverse relations.
- * (should be 'HasOne', must set 'this.instance' as their related).
- */
- handleAddition: function( model, coll, options ) {
- //console.debug('handleAddition called; args=%o', arguments);
- options = options ? _.clone( options ) : {};
- this.changed = true;
-
- _.each( this.getReverseRelations( model ), function( relation ) {
- relation.addRelated( this.instance, options );
- }, this );
-
- // Only trigger 'add' once the newly added model is initialized (so, has its relations set up)
- var dit = this;
- !options.silent && Backbone.Relational.eventQueue.add( function() {
- dit.instance.trigger( 'add:' + dit.key, model, dit.related, options );
- });
- },
-
- /**
- * When a model is removed from a 'HasMany', trigger 'remove' on 'this.instance' and notify reverse relations.
- * (should be 'HasOne', which should be nullified)
- */
- handleRemoval: function( model, coll, options ) {
- //console.debug('handleRemoval called; args=%o', arguments);
- options = options ? _.clone( options ) : {};
- this.changed = true;
-
- _.each( this.getReverseRelations( model ), function( relation ) {
- relation.removeRelated( this.instance, null, options );
- }, this );
-
- var dit = this;
- !options.silent && Backbone.Relational.eventQueue.add( function() {
- dit.instance.trigger( 'remove:' + dit.key, model, dit.related, options );
- });
- },
-
- handleReset: function( coll, options ) {
- var dit = this;
- options = options ? _.clone( options ) : {};
- !options.silent && Backbone.Relational.eventQueue.add( function() {
- dit.instance.trigger( 'reset:' + dit.key, dit.related, options );
- });
- },
-
- tryAddRelated: function( model, coll, options ) {
- var item = _.contains( this.keyIds, model.id );
-
- if ( item ) {
- this.addRelated( model, options );
- this.keyIds = _.without( this.keyIds, model.id );
- }
- },
-
- addRelated: function( model, options ) {
- // Allow 'model' to set up its relations before proceeding.
- // (which can result in a call to 'addRelated' from a relation of 'model')
- var dit = this;
- model.queue( function() {
- if ( dit.related && !dit.related.get( model ) ) {
- dit.related.add( model, _.defaults( { parse: false }, options ) );
- }
- });
- },
-
- removeRelated: function( model, coll, options ) {
- if ( this.related.get( model ) ) {
- this.related.remove( model, options );
- }
- }
- });
-
- /**
- * A type of Backbone.Model that also maintains relations to other models and collections.
- * New events when compared to the original:
- * - 'add:' (model, related collection, options)
- * - 'remove:' (model, related collection, options)
- * - 'change:' (model, related model or collection, options)
- */
- Backbone.RelationalModel = Backbone.Model.extend({
- relations: null, // Relation descriptions on the prototype
- _relations: null, // Relation instances
- _isInitialized: false,
- _deferProcessing: false,
- _queue: null,
-
- subModelTypeAttribute: 'type',
- subModelTypes: null,
-
- constructor: function( attributes, options ) {
- // Nasty hack, for cases like 'model.get( ).add( item )'.
- // Defer 'processQueue', so that when 'Relation.createModels' is used we trigger 'HasMany'
- // collection events only after the model is really fully set up.
- // Example: event for "p.on( 'add:jobs' )" -> "p.get('jobs').add( { company: c.id, person: p.id } )".
- if ( options && options.collection ) {
- var dit = this,
- collection = this.collection = options.collection;
-
- // Prevent `collection` from cascading down to nested models; they shouldn't go into this `if` clause.
- delete options.collection;
-
- this._deferProcessing = true;
-
- var processQueue = function( model ) {
- if ( model === dit ) {
- dit._deferProcessing = false;
- dit.processQueue();
- collection.off( 'relational:add', processQueue );
- }
- };
- collection.on( 'relational:add', processQueue );
-
- // So we do process the queue eventually, regardless of whether this model actually gets added to 'options.collection'.
- _.defer( function() {
- processQueue( dit );
- });
- }
-
- Backbone.Relational.store.processOrphanRelations();
-
- this._queue = new Backbone.BlockingQueue();
- this._queue.block();
- Backbone.Relational.eventQueue.block();
-
- try {
- Backbone.Model.apply( this, arguments );
- }
- finally {
- // Try to run the global queue holding external events
- Backbone.Relational.eventQueue.unblock();
- }
- },
-
- /**
- * Override 'trigger' to queue 'change' and 'change:*' events
- */
- trigger: function( eventName ) {
- if ( eventName.length > 5 && eventName.indexOf( 'change' ) === 0 ) {
- var dit = this,
- args = arguments;
-
- Backbone.Relational.eventQueue.add( function() {
- if ( !dit._isInitialized ) {
- return;
- }
-
- // Determine if the `change` event is still valid, now that all relations are populated
- var changed = true;
- if ( eventName === 'change' ) {
- changed = dit.hasChanged();
- }
- else {
- var attr = eventName.slice( 7 ),
- rel = dit.getRelation( attr );
-
- if ( rel ) {
- // If `attr` is a relation, `change:attr` get triggered from `Relation.onChange`.
- // These take precedence over `change:attr` events triggered by `Model.set`.
- // The relation set a fourth attribute to `true`. If this attribute is present,
- // continue triggering this event; otherwise, it's from `Model.set` and should be stopped.
- changed = ( args[ 4 ] === true );
-
- // If this event was triggered by a relation, set the right value in `this.changed`
- // (a Collection or Model instead of raw data).
- if ( changed ) {
- dit.changed[ attr ] = args[ 2 ];
- }
- // Otherwise, this event is from `Model.set`. If the relation doesn't report a change,
- // remove attr from `dit.changed` so `hasChanged` doesn't take it into account.
- else if ( !rel.changed ) {
- delete dit.changed[ attr ];
- }
- }
- }
-
- changed && Backbone.Model.prototype.trigger.apply( dit, args );
- });
- }
- else {
- Backbone.Model.prototype.trigger.apply( this, arguments );
- }
-
- return this;
- },
-
- /**
- * Initialize Relations present in this.relations; determine the type (HasOne/HasMany), then creates a new instance.
- * Invoked in the first call so 'set' (which is made from the Backbone.Model constructor).
- */
- initializeRelations: function( options ) {
- this.acquire(); // Setting up relations often also involve calls to 'set', and we only want to enter this function once
- this._relations = {};
-
- _.each( this.relations || [], function( rel ) {
- Backbone.Relational.store.initializeRelation( this, rel, options );
- }, this );
-
- this._isInitialized = true;
- this.release();
- this.processQueue();
- },
-
- /**
- * When new values are set, notify this model's relations (also if options.silent is set).
- * (Relation.setRelated locks this model before calling 'set' on it to prevent loops)
- */
- updateRelations: function( options ) {
- if ( this._isInitialized && !this.isLocked() ) {
- _.each( this._relations, function( rel ) {
- // Update from data in `rel.keySource` if set, or `rel.key` otherwise
- var val = this.attributes[ rel.keySource ] || this.attributes[ rel.key ];
- if ( rel.related !== val ) {
- this.trigger( 'relational:change:' + rel.key, this, val, options || {} );
- }
- }, this );
- }
- },
-
- /**
- * Either add to the queue (if we're not initialized yet), or execute right away.
- */
- queue: function( func ) {
- this._queue.add( func );
- },
-
- /**
- * Process _queue
- */
- processQueue: function() {
- if ( this._isInitialized && !this._deferProcessing && this._queue.isBlocked() ) {
- this._queue.unblock();
- }
- },
-
- /**
- * Get a specific relation.
- * @param key {string} The relation key to look for.
- * @return {Backbone.Relation} An instance of 'Backbone.Relation', if a relation was found for 'key', or null.
- */
- getRelation: function( key ) {
- return this._relations[ key ];
- },
-
- /**
- * Get all of the created relations.
- * @return {Backbone.Relation[]}
- */
- getRelations: function() {
- return _.values( this._relations );
- },
-
- /**
- * Retrieve related objects.
- * @param key {string} The relation key to fetch models for.
- * @param [options] {Object} Options for 'Backbone.Model.fetch' and 'Backbone.sync'.
- * @param [refresh=false] {boolean} Fetch existing models from the server as well (in order to update them).
- * @return {jQuery.when[]} An array of request objects
- */
- fetchRelated: function( key, options, refresh ) {
- // Set default `options` for fetch
- options = _.extend( { update: true, remove: false }, options );
-
- var setUrl,
- requests = [],
- rel = this.getRelation( key ),
- idsToFetch = rel && ( rel.keyIds || ( ( rel.keyId || rel.keyId === 0 ) ? [ rel.keyId ] : [] ) );
-
- // On `refresh`, add the ids for current models in the relation to `idsToFetch`
- if ( refresh ) {
- var models = rel.related instanceof Backbone.Collection ? rel.related.models : [ rel.related ];
- _.each( models, function( model ) {
- if ( model.id || model.id === 0 ) {
- idsToFetch.push( model.id );
- }
- });
- }
-
- if ( idsToFetch && idsToFetch.length ) {
- // Find (or create) a model for each one that is to be fetched
- var created = [],
- models = _.map( idsToFetch, function( id ) {
- var model = Backbone.Relational.store.find( rel.relatedModel, id );
-
- if ( !model ) {
- var attrs = {};
- attrs[ rel.relatedModel.prototype.idAttribute ] = id;
- model = rel.relatedModel.findOrCreate( attrs, options );
- created.push( model );
- }
-
- return model;
- }, this );
-
- // Try if the 'collection' can provide a url to fetch a set of models in one request.
- if ( rel.related instanceof Backbone.Collection && _.isFunction( rel.related.url ) ) {
- setUrl = rel.related.url( models );
- }
-
- // An assumption is that when 'Backbone.Collection.url' is a function, it can handle building of set urls.
- // To make sure it can, test if the url we got by supplying a list of models to fetch is different from
- // the one supplied for the default fetch action (without args to 'url').
- if ( setUrl && setUrl !== rel.related.url() ) {
- var opts = _.defaults(
- {
- error: function() {
- var args = arguments;
- _.each( created, function( model ) {
- model.trigger( 'destroy', model, model.collection, options );
- options.error && options.error.apply( model, args );
- });
- },
- url: setUrl
- },
- options
- );
-
- requests = [ rel.related.fetch( opts ) ];
- }
- else {
- requests = _.map( models, function( model ) {
- var opts = _.defaults(
- {
- error: function() {
- if ( _.contains( created, model ) ) {
- model.trigger( 'destroy', model, model.collection, options );
- options.error && options.error.apply( model, arguments );
- }
- }
- },
- options
- );
- return model.fetch( opts );
- }, this );
- }
- }
-
- return requests;
- },
-
- get: function( attr ) {
- var originalResult = Backbone.Model.prototype.get.call( this, attr );
-
- // Use `originalResult` get if dotNotation not enabled or not required because no dot is in `attr`
- if ( !this.dotNotation || attr.indexOf( '.' ) === -1 ) {
- return originalResult;
- }
-
- // Go through all splits and return the final result
- var splits = attr.split( '.' );
- var result = _.reduce(splits, function( model, split ) {
- if ( !( model instanceof Backbone.Model ) ) {
- throw new Error( 'Attribute must be an instanceof Backbone.Model. Is: ' + model + ', currentSplit: ' + split );
- }
-
- return Backbone.Model.prototype.get.call( model, split );
- }, this );
-
- if ( originalResult !== undefined && result !== undefined ) {
- throw new Error( "Ambiguous result for '" + attr + "'. direct result: " + originalResult + ", dotNotation: " + result );
- }
-
- return originalResult || result;
- },
-
- set: function( key, value, options ) {
- Backbone.Relational.eventQueue.block();
-
- // Duplicate backbone's behavior to allow separate key/value parameters, instead of a single 'attributes' object
- var attributes;
- if ( _.isObject( key ) || key == null ) {
- attributes = key;
- options = value;
- }
- else {
- attributes = {};
- attributes[ key ] = value;
- }
-
- try {
- var id = this.id,
- newId = attributes && this.idAttribute in attributes && attributes[ this.idAttribute ];
-
- // Check if we're not setting a duplicate id before actually calling `set`.
- Backbone.Relational.store.checkId( this, newId );
-
- var result = Backbone.Model.prototype.set.apply( this, arguments );
-
- // Ideal place to set up relations, if this is the first time we're here for this model
- if ( !this._isInitialized && !this.isLocked() ) {
- this.constructor.initializeModelHierarchy();
- Backbone.Relational.store.register( this );
- this.initializeRelations( options );
- }
- // The store should know about an `id` update asap
- else if ( newId && newId !== id ) {
- Backbone.Relational.store.update( this );
- }
-
- if ( attributes ) {
- this.updateRelations( options );
- }
- }
- finally {
- // Try to run the global queue holding external events
- Backbone.Relational.eventQueue.unblock();
- }
-
- return result;
- },
-
- unset: function( attribute, options ) {
- Backbone.Relational.eventQueue.block();
-
- var result = Backbone.Model.prototype.unset.apply( this, arguments );
- this.updateRelations( options );
-
- // Try to run the global queue holding external events
- Backbone.Relational.eventQueue.unblock();
-
- return result;
- },
-
- clear: function( options ) {
- Backbone.Relational.eventQueue.block();
-
- var result = Backbone.Model.prototype.clear.apply( this, arguments );
- this.updateRelations( options );
-
- // Try to run the global queue holding external events
- Backbone.Relational.eventQueue.unblock();
-
- return result;
- },
-
- clone: function() {
- var attributes = _.clone( this.attributes );
- if ( !_.isUndefined( attributes[ this.idAttribute ] ) ) {
- attributes[ this.idAttribute ] = null;
- }
-
- _.each( this.getRelations(), function( rel ) {
- delete attributes[ rel.key ];
- });
-
- return new this.constructor( attributes );
- },
-
- /**
- * Convert relations to JSON, omits them when required
- */
- toJSON: function( options ) {
- // If this Model has already been fully serialized in this branch once, return to avoid loops
- if ( this.isLocked() ) {
- return this.id;
- }
-
- this.acquire();
- var json = Backbone.Model.prototype.toJSON.call( this, options );
-
- if ( this.constructor._superModel && !( this.constructor._subModelTypeAttribute in json ) ) {
- json[ this.constructor._subModelTypeAttribute ] = this.constructor._subModelTypeValue;
- }
-
- _.each( this._relations, function( rel ) {
- var related = json[ rel.key ],
- includeInJSON = rel.options.includeInJSON,
- value = null;
-
- if ( includeInJSON === true ) {
- if ( related && _.isFunction( related.toJSON ) ) {
- value = related.toJSON( options );
- }
- }
- else if ( _.isString( includeInJSON ) ) {
- if ( related instanceof Backbone.Collection ) {
- value = related.pluck( includeInJSON );
- }
- else if ( related instanceof Backbone.Model ) {
- value = related.get( includeInJSON );
- }
-
- // Add ids for 'unfound' models if includeInJSON is equal to (only) the relatedModel's `idAttribute`
- if ( includeInJSON === rel.relatedModel.prototype.idAttribute ) {
- if ( rel instanceof Backbone.HasMany ) {
- value = value.concat( rel.keyIds );
- }
- else if ( rel instanceof Backbone.HasOne ) {
- value = value || rel.keyId;
- }
- }
- }
- else if ( _.isArray( includeInJSON ) ) {
- if ( related instanceof Backbone.Collection ) {
- value = [];
- related.each( function( model ) {
- var curJson = {};
- _.each( includeInJSON, function( key ) {
- curJson[ key ] = model.get( key );
- });
- value.push( curJson );
- });
- }
- else if ( related instanceof Backbone.Model ) {
- value = {};
- _.each( includeInJSON, function( key ) {
- value[ key ] = related.get( key );
- });
- }
- }
- else {
- delete json[ rel.key ];
- }
-
- if ( includeInJSON ) {
- json[ rel.keyDestination ] = value;
- }
-
- if ( rel.keyDestination !== rel.key ) {
- delete json[ rel.key ];
- }
- });
-
- this.release();
- return json;
- }
- },
- {
- /**
- *
- * @param superModel
- * @returns {Backbone.RelationalModel.constructor}
- */
- setup: function( superModel ) {
- // We don't want to share a relations array with a parent, as this will cause problems with
- // reverse relations.
- this.prototype.relations = ( this.prototype.relations || [] ).slice( 0 );
-
- this._subModels = {};
- this._superModel = null;
-
- // If this model has 'subModelTypes' itself, remember them in the store
- if ( this.prototype.hasOwnProperty( 'subModelTypes' ) ) {
- Backbone.Relational.store.addSubModels( this.prototype.subModelTypes, this );
- }
- // The 'subModelTypes' property should not be inherited, so reset it.
- else {
- this.prototype.subModelTypes = null;
- }
-
- // Initialize all reverseRelations that belong to this new model.
- _.each( this.prototype.relations || [], function( rel ) {
- if ( !rel.model ) {
- rel.model = this;
- }
-
- if ( rel.reverseRelation && rel.model === this ) {
- var preInitialize = true;
- if ( _.isString( rel.relatedModel ) ) {
- /**
- * The related model might not be defined for two reasons
- * 1. it is related to itself
- * 2. it never gets defined, e.g. a typo
- * 3. the model hasn't been defined yet, but will be later
- * In neither of these cases do we need to pre-initialize reverse relations.
- * However, for 3. (which is, to us, indistinguishable from 2.), we do need to attempt
- * setting up this relation again later, in case the related model is defined later.
- */
- var relatedModel = Backbone.Relational.store.getObjectByName( rel.relatedModel );
- preInitialize = relatedModel && ( relatedModel.prototype instanceof Backbone.RelationalModel );
- }
-
- if ( preInitialize ) {
- Backbone.Relational.store.initializeRelation( null, rel );
- }
- else if ( _.isString( rel.relatedModel ) ) {
- Backbone.Relational.store.addOrphanRelation( rel );
- }
- }
- }, this );
-
- return this;
- },
-
- /**
- * Create a 'Backbone.Model' instance based on 'attributes'.
- * @param {Object} attributes
- * @param {Object} [options]
- * @return {Backbone.Model}
- */
- build: function( attributes, options ) {
- var model = this;
-
- // 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet.
- this.initializeModelHierarchy();
-
- // Determine what type of (sub)model should be built if applicable.
- // Lookup the proper subModelType in 'this._subModels'.
- if ( this._subModels && this.prototype.subModelTypeAttribute in attributes ) {
- var subModelTypeAttribute = attributes[ this.prototype.subModelTypeAttribute ];
- var subModelType = this._subModels[ subModelTypeAttribute ];
- if ( subModelType ) {
- model = subModelType;
- }
- }
-
- return new model( attributes, options );
- },
-
- /**
- *
- */
- initializeModelHierarchy: function() {
- // If we're here for the first time, try to determine if this modelType has a 'superModel'.
- if ( _.isUndefined( this._superModel ) || _.isNull( this._superModel ) ) {
- Backbone.Relational.store.setupSuperModel( this );
-
- // If a superModel has been found, copy relations from the _superModel if they haven't been
- // inherited automatically (due to a redefinition of 'relations').
- // Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail
- // the isUndefined/isNull check next time.
- if ( this._superModel && this._superModel.prototype.relations ) {
- // Find relations that exist on the `_superModel`, but not yet on this model.
- var inheritedRelations = _.select( this._superModel.prototype.relations || [], function( superRel ) {
- return !_.any( this.prototype.relations || [], function( rel ) {
- return superRel.relatedModel === rel.relatedModel && superRel.key === rel.key;
- }, this );
- }, this );
-
- this.prototype.relations = inheritedRelations.concat( this.prototype.relations );
- }
- else {
- this._superModel = false;
- }
- }
-
- // If we came here through 'build' for a model that has 'subModelTypes', and not all of them have been resolved yet, try to resolve each.
- if ( this.prototype.subModelTypes && _.keys( this.prototype.subModelTypes ).length !== _.keys( this._subModels ).length ) {
- _.each( this.prototype.subModelTypes || [], function( subModelTypeName ) {
- var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName );
- subModelType && subModelType.initializeModelHierarchy();
- });
- }
- },
-
- /**
- * Find an instance of `this` type in 'Backbone.Relational.store'.
- * - If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found.
- * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`.
- * Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`).
- * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model.
- * @param {Object} [options]
- * @param {Boolean} [options.create=true]
- * @param {Boolean} [options.merge=true]
- * @param {Boolean} [options.parse=false]
- * @return {Backbone.RelationalModel}
- */
- findOrCreate: function( attributes, options ) {
- options || ( options = {} );
- var parsedAttributes = ( _.isObject( attributes ) && options.parse && this.prototype.parse ) ?
- this.prototype.parse( attributes ) : attributes;
-
- // Try to find an instance of 'this' model type in the store
- var model = Backbone.Relational.store.find( this, parsedAttributes );
-
- // If we found an instance, update it with the data in 'item' (unless 'options.merge' is false).
- // If not, create an instance (unless 'options.create' is false).
- if ( _.isObject( attributes ) ) {
- if ( model && options.merge !== false ) {
- // Make sure `options.collection` doesn't cascade to nested models
- delete options.collection;
-
- model.set( parsedAttributes, options );
- }
- else if ( !model && options.create !== false ) {
- model = this.build( attributes, options );
- }
- }
-
- return model;
- }
- });
- _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore );
-
- /**
- * Override Backbone.Collection._prepareModel, so objects will be built using the correct type
- * if the collection.model has subModels.
- * Attempts to find a model for `attrs` in Backbone.store through `findOrCreate`
- * (which sets the new properties on it if found), or instantiates a new model.
- */
- Backbone.Collection.prototype.__prepareModel = Backbone.Collection.prototype._prepareModel;
- Backbone.Collection.prototype._prepareModel = function ( attrs, options ) {
- var model;
-
- if ( attrs instanceof Backbone.Model ) {
- if ( !attrs.collection ) {
- attrs.collection = this;
- }
- model = attrs;
- }
- else {
- options || ( options = {} );
- options.collection = this;
-
- if ( typeof this.model.findOrCreate !== 'undefined' ) {
- model = this.model.findOrCreate( attrs, options );
- }
- else {
- model = new this.model( attrs, options );
- }
-
- if ( model && model.isNew() && !model._validate( attrs, options ) ) {
- this.trigger( 'invalid', this, attrs, options );
- model = false;
- }
- }
-
- return model;
- };
-
-
- /**
- * Override Backbone.Collection.set, so we'll create objects from attributes where required,
- * and update the existing models. Also, trigger 'relational:add'.
- */
- var set = Backbone.Collection.prototype.__set = Backbone.Collection.prototype.set;
- Backbone.Collection.prototype.set = function( models, options ) {
- // Short-circuit if this Collection doesn't hold RelationalModels
- if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
- return set.apply( this, arguments );
- }
-
- if ( options && options.parse ) {
- models = this.parse( models, options );
- }
-
- if ( !_.isArray( models ) ) {
- models = models ? [ models ] : [];
- }
-
- var newModels = [],
- toAdd = [];
-
- //console.debug( 'calling add on coll=%o; model=%o, options=%o', this, models, options );
- _.each( models, function( model ) {
- if ( !( model instanceof Backbone.Model ) ) {
- model = Backbone.Collection.prototype._prepareModel.call( this, model, options );
- }
-
- if ( model ) {
- toAdd.push( model );
-
- if ( !( this.get( model ) || this.get( model.cid ) ) ) {
- newModels.push( model );
- }
- // If we arrive in `add` while performing a `set` (after a create, so the model gains an `id`),
- // we may get here before `_onModelEvent` has had the chance to update `_byId`.
- else if ( model.id != null ) {
- this._byId[ model.id ] = model;
- }
- }
- }, this );
-
- // Add 'models' in a single batch, so the original add will only be called once (and thus 'sort', etc).
- // If `parse` was specified, the collection and contained models have been parsed now.
- set.call( this, toAdd, _.defaults( { parse: false }, options ) );
-
- _.each( newModels, function( model ) {
- // Fire a `relational:add` event for any model in `newModels` that has actually been added to the collection.
- if ( this.get( model ) || this.get( model.cid ) ) {
- this.trigger( 'relational:add', model, this, options );
- }
- }, this );
-
- return this;
- };
-
- /**
- * Override 'Backbone.Collection.remove' to trigger 'relational:remove'.
- */
- var remove = Backbone.Collection.prototype.__remove = Backbone.Collection.prototype.remove;
- Backbone.Collection.prototype.remove = function( models, options ) {
- // Short-circuit if this Collection doesn't hold RelationalModels
- if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
- return remove.apply( this, arguments );
- }
-
- models = _.isArray( models ) ? models.slice() : [ models ];
- options || ( options = {} );
-
- var toRemove = [];
-
- //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options );
- _.each( models, function( model ) {
- model = this.get( model ) || this.get( model.cid );
- model && toRemove.push( model );
- }, this );
-
- if ( toRemove.length ) {
- remove.call( this, toRemove, options );
-
- _.each( toRemove, function( model ) {
- this.trigger('relational:remove', model, this, options);
- }, this );
- }
-
- return this;
- };
-
- /**
- * Override 'Backbone.Collection.reset' to trigger 'relational:reset'.
- */
- var reset = Backbone.Collection.prototype.__reset = Backbone.Collection.prototype.reset;
- Backbone.Collection.prototype.reset = function( models, options ) {
- options = _.extend( { merge: true }, options );
- reset.call( this, models, options );
-
- if ( this.model.prototype instanceof Backbone.RelationalModel ) {
- this.trigger( 'relational:reset', this, options );
- }
-
- return this;
- };
-
- /**
- * Override 'Backbone.Collection.sort' to trigger 'relational:reset'.
- */
- var sort = Backbone.Collection.prototype.__sort = Backbone.Collection.prototype.sort;
- Backbone.Collection.prototype.sort = function( options ) {
- sort.call( this, options );
-
- if ( this.model.prototype instanceof Backbone.RelationalModel ) {
- this.trigger( 'relational:reset', this, options );
- }
-
- return this;
- };
-
- /**
- * Override 'Backbone.Collection.trigger' so 'add', 'remove' and 'reset' events are queued until relations
- * are ready.
- */
- var trigger = Backbone.Collection.prototype.__trigger = Backbone.Collection.prototype.trigger;
- Backbone.Collection.prototype.trigger = function( eventName ) {
- // Short-circuit if this Collection doesn't hold RelationalModels
- if ( !( this.model.prototype instanceof Backbone.RelationalModel ) ) {
- return trigger.apply( this, arguments );
- }
-
- if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' ) {
- var dit = this,
- args = arguments;
-
- if ( _.isObject( args[ 3 ] ) ) {
- args = _.toArray( args );
- // the fourth argument is the option object.
- // we need to clone it, as it could be modified while we wait on the eventQueue to be unblocked
- args[ 3 ] = _.clone( args[ 3 ] );
- }
-
- Backbone.Relational.eventQueue.add( function() {
- trigger.apply( dit, args );
- });
- }
- else {
- trigger.apply( this, arguments );
- }
-
- return this;
- };
-
- // Override .extend() to automatically call .setup()
- Backbone.RelationalModel.extend = function( protoProps, classProps ) {
- var child = Backbone.Model.extend.apply( this, arguments );
-
- child.setup( this );
-
- return child;
- };
-})();
diff --git a/UI/JsLibraries/lunr.js b/UI/JsLibraries/lunr.js
deleted file mode 100644
index 346f4f271..000000000
--- a/UI/JsLibraries/lunr.js
+++ /dev/null
@@ -1,1587 +0,0 @@
-/**
- * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 0.3.3
- * Copyright (C) 2013 Oliver Nightingale
- * MIT Licensed
- * @license
- */
-
-/**
- * Convenience function for instantiating a new lunr index and configuring it
- * with the default pipeline functions and the passed config function.
- *
- * When using this convenience function a new index will be created with the
- * following functions already in the pipeline:
- *
- * lunr.StopWordFilter - filters out any stop words before they enter the
- * index
- *
- * lunr.stemmer - stems the tokens before entering the index.
- *
- * Example:
- *
- * var idx = lunr(function () {
- * this.field('title', 10)
- * this.field('tags', 100)
- * this.field('body')
- *
- * this.ref('cid')
- *
- * this.pipeline.add(function () {
- * // some custom pipeline function
- * })
- *
- * })
- *
- * @param {Function} config A function that will be called with the new instance
- * of the lunr.Index as both its context and first parameter. It can be used to
- * customize the instance of new lunr.Index.
- * @namespace
- * @module
- * @returns {lunr.Index}
- *
- */
-var lunr = function (config) {
- var idx = new lunr.Index
-
- idx.pipeline.add(lunr.stopWordFilter, lunr.stemmer)
-
- if (config) config.call(idx, idx)
-
- return idx
-}
-
-lunr.version = "0.3.3"
-
-if (typeof module !== 'undefined') {
- module.exports = lunr
-}
-/*!
- * lunr.utils
- * Copyright (C) 2013 Oliver Nightingale
- */
-
-/**
- * A namespace containing utils for the rest of the lunr library
- */
-lunr.utils = {}
-
-/**
- * Print a warning message to the console.
- *
- * @param {String} message The message to be printed.
- * @memberOf Utils
- */
-lunr.utils.warn = (function (global) {
- return function (message) {
- if (global.console && console.warn) {
- console.warn(message)
- }
- }
-})(this)
-/*!
- * lunr.tokenizer
- * Copyright (C) 2013 Oliver Nightingale
- */
-
-/**
- * A function for splitting a string into tokens ready to be inserted into
- * the search index.
- *
- * @module
- * @param {String} str The string to convert into tokens
- * @returns {Array}
- */
-lunr.tokenizer = function (str) {
- if (!str) return []
- if (Array.isArray(str)) return str
-
- var str = str.replace(/^\s+/, '')
-
- for (var i = str.length - 1; i >= 0; i--) {
- if (/\S/.test(str.charAt(i))) {
- str = str.substring(0, i + 1)
- break
- }
- }
-
- return str
- .split(/\s+/)
- .map(function (token) {
- return token.replace(/^\W+/, '').replace(/\W+$/, '').toLowerCase()
- })
-}
-/*!
- * lunr.Pipeline
- * Copyright (C) 2013 Oliver Nightingale
- */
-
-/**
- * lunr.Pipelines maintain an ordered list of functions to be applied to all
- * tokens in documents entering the search index and queries being ran against
- * the index.
- *
- * An instance of lunr.Index created with the lunr shortcut will contain a
- * pipeline with a stop word filter and an English language stemmer. Extra
- * functions can be added before or after either of these functions or these
- * default functions can be removed.
- *
- * When run the pipeline will call each function in turn, passing a token, the
- * index of that token in the original list of all tokens and finally a list of
- * all the original tokens.
- *
- * The output of functions in the pipeline will be passed to the next function
- * in the pipeline. To exclude a token from entering the index the function
- * should return undefined, the rest of the pipeline will not be called with
- * this token.
- *
- * For serialisation of pipelines to work, all functions used in an instance of
- * a pipeline should be registered with lunr.Pipeline. Registered functions can
- * then be loaded. If trying to load a serialised pipeline that uses functions
- * that are not registered an error will be thrown.
- *
- * If not planning on serialising the pipeline then registering pipeline functions
- * is not necessary.
- *
- * @constructor
- */
-lunr.Pipeline = function () {
- this._stack = []
-}
-
-lunr.Pipeline.registeredFunctions = {}
-
-/**
- * Register a function with the pipeline.
- *
- * Functions that are used in the pipeline should be registered if the pipeline
- * needs to be serialised, or a serialised pipeline needs to be loaded.
- *
- * Registering a function does not add it to a pipeline, functions must still be
- * added to instances of the pipeline for them to be used when running a pipeline.
- *
- * @param {Function} fn The function to check for.
- * @param {String} label The label to register this function with
- * @memberOf Pipeline
- */
-lunr.Pipeline.registerFunction = function (fn, label) {
- if (label in this.registeredFunctions) {
- lunr.utils.warn('Overwriting existing registered function: ' + label)
- }
-
- fn.label = label
- lunr.Pipeline.registeredFunctions[fn.label] = fn
-}
-
-/**
- * Warns if the function is not registered as a Pipeline function.
- *
- * @param {Function} fn The function to check for.
- * @private
- * @memberOf Pipeline
- */
-lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) {
- var isRegistered = fn.label && (fn.label in this.registeredFunctions)
-
- if (!isRegistered) {
- lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn)
- }
-}
-
-/**
- * Loads a previously serialised pipeline.
- *
- * All functions to be loaded must already be registered with lunr.Pipeline.
- * If any function from the serialised data has not been registered then an
- * error will be thrown.
- *
- * @param {Object} serialised The serialised pipeline to load.
- * @returns {lunr.Pipeline}
- * @memberOf Pipeline
- */
-lunr.Pipeline.load = function (serialised) {
- var pipeline = new lunr.Pipeline
-
- serialised.forEach(function (fnName) {
- var fn = lunr.Pipeline.registeredFunctions[fnName]
-
- if (fn) {
- pipeline.add(fn)
- } else {
- throw new Error ('Cannot load un-registered function: ' + fnName)
- }
- })
-
- return pipeline
-}
-
-/**
- * Adds new functions to the end of the pipeline.
- *
- * Logs a warning if the function has not been registered.
- *
- * @param {Function} functions Any number of functions to add to the pipeline.
- * @memberOf Pipeline
- */
-lunr.Pipeline.prototype.add = function () {
- var fns = Array.prototype.slice.call(arguments)
-
- fns.forEach(function (fn) {
- lunr.Pipeline.warnIfFunctionNotRegistered(fn)
- this._stack.push(fn)
- }, this)
-}
-
-/**
- * Adds a single function after a function that already exists in the
- * pipeline.
- *
- * Logs a warning if the function has not been registered.
- *
- * @param {Function} existingFn A function that already exists in the pipeline.
- * @param {Function} newFn The new function to add to the pipeline.
- * @memberOf Pipeline
- */
-lunr.Pipeline.prototype.after = function (existingFn, newFn) {
- lunr.Pipeline.warnIfFunctionNotRegistered(newFn)
-
- var pos = this._stack.indexOf(existingFn) + 1
- this._stack.splice(pos, 0, newFn)
-}
-
-/**
- * Adds a single function before a function that already exists in the
- * pipeline.
- *
- * Logs a warning if the function has not been registered.
- *
- * @param {Function} existingFn A function that already exists in the pipeline.
- * @param {Function} newFn The new function to add to the pipeline.
- * @memberOf Pipeline
- */
-lunr.Pipeline.prototype.before = function (existingFn, newFn) {
- lunr.Pipeline.warnIfFunctionNotRegistered(newFn)
-
- var pos = this._stack.indexOf(existingFn)
- this._stack.splice(pos, 0, newFn)
-}
-
-/**
- * Removes a function from the pipeline.
- *
- * @param {Function} fn The function to remove from the pipeline.
- * @memberOf Pipeline
- */
-lunr.Pipeline.prototype.remove = function (fn) {
- var pos = this._stack.indexOf(fn)
- this._stack.splice(pos, 1)
-}
-
-/**
- * Runs the current list of functions that make up the pipeline against the
- * passed tokens.
- *
- * @param {Array} tokens The tokens to run through the pipeline.
- * @returns {Array}
- * @memberOf Pipeline
- */
-lunr.Pipeline.prototype.run = function (tokens) {
- var out = [],
- tokenLength = tokens.length,
- stackLength = this._stack.length
-
- for (var i = 0; i < tokenLength; i++) {
- var token = tokens[i]
-
- for (var j = 0; j < stackLength; j++) {
- token = this._stack[j](token, i, tokens)
- if (token === void 0) break
- };
-
- if (token !== void 0) out.push(token)
- };
-
- return out
-}
-
-/**
- * Returns a representation of the pipeline ready for serialisation.
- *
- * Logs a warning if the function has not been registered.
- *
- * @returns {Array}
- * @memberOf Pipeline
- */
-lunr.Pipeline.prototype.toJSON = function () {
- return this._stack.map(function (fn) {
- lunr.Pipeline.warnIfFunctionNotRegistered(fn)
-
- return fn.label
- })
-}
-/*!
- * lunr.Vector
- * Copyright (C) 2013 Oliver Nightingale
- */
-
-/**
- * lunr.Vectors wrap arrays and add vector related operations for the array
- * elements.
- *
- * @constructor
- * @param {Array} elements Elements that make up the vector.
- */
-lunr.Vector = function (elements) {
- this.elements = elements
-
- for (var i = 0; i < elements.length; i++) {
- if (!(i in this.elements)) this.elements[i] = 0
- }
-}
-
-/**
- * Calculates the magnitude of this vector.
- *
- * @returns {Number}
- * @memberOf Vector
- */
-lunr.Vector.prototype.magnitude = function () {
- if (this._magnitude) return this._magnitude
-
- var sumOfSquares = 0,
- elems = this.elements,
- len = elems.length,
- el
-
- for (var i = 0; i < len; i++) {
- el = elems[i]
- sumOfSquares += el * el
- };
-
- return this._magnitude = Math.sqrt(sumOfSquares)
-}
-
-/**
- * Calculates the dot product of this vector and another vector.
- *
- * @param {lunr.Vector} otherVector The vector to compute the dot product with.
- * @returns {Number}
- * @memberOf Vector
- */
-lunr.Vector.prototype.dot = function (otherVector) {
- var elem1 = this.elements,
- elem2 = otherVector.elements,
- length = elem1.length,
- dotProduct = 0
-
- for (var i = 0; i < length; i++) {
- dotProduct += elem1[i] * elem2[i]
- };
-
- return dotProduct
-}
-
-/**
- * Calculates the cosine similarity between this vector and another
- * vector.
- *
- * @param {lunr.Vector} otherVector The other vector to calculate the
- * similarity with.
- * @returns {Number}
- * @memberOf Vector
- */
-lunr.Vector.prototype.similarity = function (otherVector) {
- return this.dot(otherVector) / (this.magnitude() * otherVector.magnitude())
-}
-
-/**
- * Converts this vector back into an array.
- *
- * @returns {Array}
- * @memberOf Vector
- */
-lunr.Vector.prototype.toArray = function () {
- return this.elements
-}
-/*!
- * lunr.SortedSet
- * Copyright (C) 2013 Oliver Nightingale
- */
-
-/**
- * lunr.SortedSets are used to maintain an array of uniq values in a sorted
- * order.
- *
- * @constructor
- */
-lunr.SortedSet = function () {
- this.length = 0
- this.elements = []
-}
-
-/**
- * Loads a previously serialised sorted set.
- *
- * @param {Array} serialisedData The serialised set to load.
- * @returns {lunr.SortedSet}
- * @memberOf SortedSet
- */
-lunr.SortedSet.load = function (serialisedData) {
- var set = new this
-
- set.elements = serialisedData
- set.length = serialisedData.length
-
- return set
-}
-
-/**
- * Inserts new items into the set in the correct position to maintain the
- * order.
- *
- * @param {Object} The objects to add to this set.
- * @memberOf SortedSet
- */
-lunr.SortedSet.prototype.add = function () {
- Array.prototype.slice.call(arguments).forEach(function (element) {
- if (~this.indexOf(element)) return
- this.elements.splice(this.locationFor(element), 0, element)
- }, this)
-
- this.length = this.elements.length
-}
-
-/**
- * Converts this sorted set into an array.
- *
- * @returns {Array}
- * @memberOf SortedSet
- */
-lunr.SortedSet.prototype.toArray = function () {
- return this.elements.slice()
-}
-
-/**
- * Creates a new array with the results of calling a provided function on every
- * element in this sorted set.
- *
- * Delegates to Array.prototype.map and has the same signature.
- *
- * @param {Function} fn The function that is called on each element of the
- * set.
- * @param {Object} ctx An optional object that can be used as the context
- * for the function fn.
- * @returns {Array}
- * @memberOf SortedSet
- */
-lunr.SortedSet.prototype.map = function (fn, ctx) {
- return this.elements.map(fn, ctx)
-}
-
-/**
- * Executes a provided function once per sorted set element.
- *
- * Delegates to Array.prototype.forEach and has the same signature.
- *
- * @param {Function} fn The function that is called on each element of the
- * set.
- * @param {Object} ctx An optional object that can be used as the context
- * @memberOf SortedSet
- * for the function fn.
- */
-lunr.SortedSet.prototype.forEach = function (fn, ctx) {
- return this.elements.forEach(fn, ctx)
-}
-
-/**
- * Returns the index at which a given element can be found in the
- * sorted set, or -1 if it is not present.
- *
- * @param {Object} elem The object to locate in the sorted set.
- * @param {Number} start An optional index at which to start searching from
- * within the set.
- * @param {Number} end An optional index at which to stop search from within
- * the set.
- * @returns {Number}
- * @memberOf SortedSet
- */
-lunr.SortedSet.prototype.indexOf = function (elem, start, end) {
- var start = start || 0,
- end = end || this.elements.length,
- sectionLength = end - start,
- pivot = start + Math.floor(sectionLength / 2),
- pivotElem = this.elements[pivot]
-
- if (sectionLength <= 1) {
- if (pivotElem === elem) {
- return pivot
- } else {
- return -1
- }
- }
-
- if (pivotElem < elem) return this.indexOf(elem, pivot, end)
- if (pivotElem > elem) return this.indexOf(elem, start, pivot)
- if (pivotElem === elem) return pivot
-}
-
-/**
- * Returns the position within the sorted set that an element should be
- * inserted at to maintain the current order of the set.
- *
- * This function assumes that the element to search for does not already exist
- * in the sorted set.
- *
- * @param {Object} elem The elem to find the position for in the set
- * @param {Number} start An optional index at which to start searching from
- * within the set.
- * @param {Number} end An optional index at which to stop search from within
- * the set.
- * @returns {Number}
- * @memberOf SortedSet
- */
-lunr.SortedSet.prototype.locationFor = function (elem, start, end) {
- var start = start || 0,
- end = end || this.elements.length,
- sectionLength = end - start,
- pivot = start + Math.floor(sectionLength / 2),
- pivotElem = this.elements[pivot]
-
- if (sectionLength <= 1) {
- if (pivotElem > elem) return pivot
- if (pivotElem < elem) return pivot + 1
- }
-
- if (pivotElem < elem) return this.locationFor(elem, pivot, end)
- if (pivotElem > elem) return this.locationFor(elem, start, pivot)
-}
-
-/**
- * Creates a new lunr.SortedSet that contains the elements in the intersection
- * of this set and the passed set.
- *
- * @param {lunr.SortedSet} otherSet The set to intersect with this set.
- * @returns {lunr.SortedSet}
- * @memberOf SortedSet
- */
-lunr.SortedSet.prototype.intersect = function (otherSet) {
- var intersectSet = new lunr.SortedSet,
- i = 0, j = 0,
- a_len = this.length, b_len = otherSet.length,
- a = this.elements, b = otherSet.elements
-
- while (true) {
- if (i > a_len - 1 || j > b_len - 1) break
-
- if (a[i] === b[j]) {
- intersectSet.add(a[i])
- i++, j++
- continue
- }
-
- if (a[i] < b[j]) {
- i++
- continue
- }
-
- if (a[i] > b[j]) {
- j++
- continue
- }
- };
-
- return intersectSet
-}
-
-/**
- * Makes a copy of this set
- *
- * @returns {lunr.SortedSet}
- * @memberOf SortedSet
- */
-lunr.SortedSet.prototype.clone = function () {
- var clone = new lunr.SortedSet
-
- clone.elements = this.toArray()
- clone.length = clone.elements.length
-
- return clone
-}
-
-/**
- * Creates a new lunr.SortedSet that contains the elements in the union
- * of this set and the passed set.
- *
- * @param {lunr.SortedSet} otherSet The set to union with this set.
- * @returns {lunr.SortedSet}
- * @memberOf SortedSet
- */
-lunr.SortedSet.prototype.union = function (otherSet) {
- var longSet, shortSet, unionSet
-
- if (this.length >= otherSet.length) {
- longSet = this, shortSet = otherSet
- } else {
- longSet = otherSet, shortSet = this
- }
-
- unionSet = longSet.clone()
-
- unionSet.add.apply(unionSet, shortSet.toArray())
-
- return unionSet
-}
-
-/**
- * Returns a representation of the sorted set ready for serialisation.
- *
- * @returns {Array}
- * @memberOf SortedSet
- */
-lunr.SortedSet.prototype.toJSON = function () {
- return this.toArray()
-}
-/*!
- * lunr.Index
- * Copyright (C) 2013 Oliver Nightingale
- */
-
-/**
- * lunr.Index is object that manages a search index. It contains the indexes
- * and stores all the tokens and document lookups. It also provides the main
- * user facing API for the library.
- *
- * @constructor
- */
-lunr.Index = function () {
- this._fields = []
- this._ref = 'id'
- this.pipeline = new lunr.Pipeline
- this.documentStore = new lunr.Store
- this.tokenStore = new lunr.TokenStore
- this.corpusTokens = new lunr.SortedSet
-}
-
-
-/**
- * Loads a previously serialised index.
- *
- * Issues a warning if the index being imported was serialised
- * by a different version of lunr.
- *
- * @param {Object} serialisedData The serialised set to load.
- * @returns {lunr.Index}
- * @memberOf Index
- */
-lunr.Index.load = function (serialisedData) {
- if (serialisedData.version !== lunr.version) {
- lunr.utils.warn('version mismatch: current ' + lunr.version + ' importing ' + serialisedData.version)
- }
-
- var idx = new this
-
- idx._fields = serialisedData.fields
- idx._ref = serialisedData.ref
-
- idx.documentStore = lunr.Store.load(serialisedData.documentStore)
- idx.tokenStore = lunr.TokenStore.load(serialisedData.tokenStore)
- idx.corpusTokens = lunr.SortedSet.load(serialisedData.corpusTokens)
- idx.pipeline = lunr.Pipeline.load(serialisedData.pipeline)
-
- return idx
-}
-
-/**
- * Adds a field to the list of fields that will be searchable within documents
- * in the index.
- *
- * An optional boost param can be passed to affect how much tokens in this field
- * rank in search results, by default the boost value is 1.
- *
- * Fields should be added before any documents are added to the index, fields
- * that are added after documents are added to the index will only apply to new
- * documents added to the index.
- *
- * @param {String} fieldName The name of the field within the document that
- * should be indexed
- * @param {Number} boost An optional boost that can be applied to terms in this
- * field.
- * @returns {lunr.Index}
- * @memberOf Index
- */
-lunr.Index.prototype.field = function (fieldName, opts) {
- var opts = opts || {},
- field = { name: fieldName, boost: opts.boost || 1 }
-
- this._fields.push(field)
- return this
-}
-
-/**
- * Sets the property used to uniquely identify documents added to the index,
- * by default this property is 'id'.
- *
- * This should only be changed before adding documents to the index, changing
- * the ref property without resetting the index can lead to unexpected results.
- *
- * @param {String} refName The property to use to uniquely identify the
- * documents in the index.
- * @returns {lunr.Index}
- * @memberOf Index
- */
-lunr.Index.prototype.ref = function (refName) {
- this._ref = refName
- return this
-}
-
-/**
- * Add a document to the index.
- *
- * This is the way new documents enter the index, this function will run the
- * fields from the document through the index's pipeline and then add it to
- * the index, it will then show up in search results.
- *
- * @param {Object} doc The document to add to the index.
- * @memberOf Index
- */
-lunr.Index.prototype.add = function (doc) {
- var docTokens = {},
- allDocumentTokens = new lunr.SortedSet,
- docRef = doc[this._ref]
-
- this._fields.forEach(function (field) {
- var fieldTokens = this.pipeline.run(lunr.tokenizer(doc[field.name]))
-
- docTokens[field.name] = fieldTokens
- lunr.SortedSet.prototype.add.apply(allDocumentTokens, fieldTokens)
- }, this)
-
- this.documentStore.set(docRef, allDocumentTokens)
- lunr.SortedSet.prototype.add.apply(this.corpusTokens, allDocumentTokens.toArray())
-
- for (var i = 0; i < allDocumentTokens.length; i++) {
- var token = allDocumentTokens.elements[i]
- var tf = this._fields.reduce(function (memo, field) {
- var fieldLength = docTokens[field.name].length
-
- if (!fieldLength) return memo
-
- var tokenCount = docTokens[field.name].filter(function (t) { return t === token }).length
-
- return memo + (tokenCount / fieldLength * field.boost)
- }, 0)
-
- this.tokenStore.add(token, { ref: docRef, tf: tf })
- };
-}
-
-/**
- * Removes a document from the index.
- *
- * To make sure documents no longer show up in search results they can be
- * removed from the index using this method.
- *
- * The document passed only needs to have the same ref property value as the
- * document that was added to the index, they could be completely different
- * objects.
- *
- * @param {Object} doc The document to remove from the index.
- * @memberOf Index
- */
-lunr.Index.prototype.remove = function (doc) {
- var docRef = doc[this._ref]
-
- if (!this.documentStore.has(docRef)) return
-
- var docTokens = this.documentStore.get(docRef)
-
- this.documentStore.remove(docRef)
-
- docTokens.forEach(function (token) {
- this.tokenStore.remove(token, docRef)
- }, this)
-}
-
-/**
- * Updates a document in the index.
- *
- * When a document contained within the index gets updated, fields changed,
- * added or removed, to make sure it correctly matched against search queries,
- * it should be updated in the index.
- *
- * This method is just a wrapper around `remove` and `add`
- *
- * @param {Object} doc The document to update in the index.
- * @see Index.prototype.remove
- * @see Index.prototype.add
- * @memberOf Index
- */
-lunr.Index.prototype.update = function (doc) {
- this.remove(doc)
- this.add(doc)
-}
-
-/**
- * Calculates the inverse document frequency for a token within the index.
- *
- * @param {String} token The token to calculate the idf of.
- * @see Index.prototype.idf
- * @private
- * @memberOf Index
- */
-lunr.Index.prototype.idf = function (term) {
- var documentFrequency = Object.keys(this.tokenStore.get(term)).length
-
- if (documentFrequency === 0) {
- return 1
- } else {
- return 1 + Math.log(this.tokenStore.length / documentFrequency)
- }
-}
-
-/**
- * Searches the index using the passed query.
- *
- * Queries should be a string, multiple words are allowed and will lead to an
- * AND based query, e.g. `idx.search('foo bar')` will run a search for
- * documents containing both 'foo' and 'bar'.
- *
- * All query tokens are passed through the same pipeline that document tokens
- * are passed through, so any language processing involved will be run on every
- * query term.
- *
- * Each query term is expanded, so that the term 'he' might be expanded to
- * 'hello' and 'help' if those terms were already included in the index.
- *
- * Matching documents are returned as an array of objects, each object contains
- * the matching document ref, as set for this index, and the similarity score
- * for this document against the query.
- *
- * @param {String} query The query to search the index with.
- * @returns {Object}
- * @see Index.prototype.idf
- * @see Index.prototype.documentVector
- * @memberOf Index
- */
-lunr.Index.prototype.search = function (query) {
- var queryTokens = this.pipeline.run(lunr.tokenizer(query)),
- queryArr = new Array (this.corpusTokens.length),
- documentSets = [],
- fieldBoosts = this._fields.reduce(function (memo, f) { return memo + f.boost }, 0)
-
- var hasSomeToken = queryTokens.some(function (token) {
- return this.tokenStore.has(token)
- }, this)
-
- if (!hasSomeToken) return []
-
- queryTokens
- .forEach(function (token, i, tokens) {
- var tf = 1 / tokens.length * this._fields.length * fieldBoosts,
- self = this
-
- var set = this.tokenStore.expand(token).reduce(function (memo, key) {
- var pos = self.corpusTokens.indexOf(key),
- idf = self.idf(key),
- exactMatchBoost = (key === token ? 10 : 1),
- set = new lunr.SortedSet
-
- // calculate the query tf-idf score for this token
- // applying an exactMatchBoost to ensure these rank
- // higher than expanded terms
- if (pos > -1) queryArr[pos] = tf * idf * exactMatchBoost
-
- // add all the documents that have this key into a set
- Object.keys(self.tokenStore.get(key)).forEach(function (ref) { set.add(ref) })
-
- return memo.union(set)
- }, new lunr.SortedSet)
-
- documentSets.push(set)
- }, this)
-
- var documentSet = documentSets.reduce(function (memo, set) {
- return memo.intersect(set)
- })
-
- var queryVector = new lunr.Vector (queryArr)
-
- return documentSet
- .map(function (ref) {
- return { ref: ref, score: queryVector.similarity(this.documentVector(ref)) }
- }, this)
- .sort(function (a, b) {
- return b.score - a.score
- })
-}
-
-/**
- * Generates a vector containing all the tokens in the document matching the
- * passed documentRef.
- *
- * The vector contains the tf-idf score for each token contained in the
- * document with the passed documentRef. The vector will contain an element
- * for every token in the indexes corpus, if the document does not contain that
- * token the element will be 0.
- *
- * @param {Object} documentRef The ref to find the document with.
- * @returns {lunr.Vector}
- * @private
- * @memberOf Index
- */
-lunr.Index.prototype.documentVector = function (documentRef) {
- var documentTokens = this.documentStore.get(documentRef),
- documentTokensLength = documentTokens.length,
- documentArr = new Array (this.corpusTokens.length)
-
- for (var i = 0; i < documentTokensLength; i++) {
- var token = documentTokens.elements[i],
- tf = this.tokenStore.get(token)[documentRef].tf,
- idf = this.idf(token)
-
- documentArr[this.corpusTokens.indexOf(token)] = tf * idf
- };
-
- return new lunr.Vector (documentArr)
-}
-
-/**
- * Returns a representation of the index ready for serialisation.
- *
- * @returns {Object}
- * @memberOf Index
- */
-lunr.Index.prototype.toJSON = function () {
- return {
- version: lunr.version,
- fields: this._fields,
- ref: this._ref,
- documentStore: this.documentStore.toJSON(),
- tokenStore: this.tokenStore.toJSON(),
- corpusTokens: this.corpusTokens.toJSON(),
- pipeline: this.pipeline.toJSON()
- }
-}
-/*!
- * lunr.Store
- * Copyright (C) 2013 Oliver Nightingale
- */
-
-/**
- * lunr.Store is a simple key-value store used for storing sets of tokens for
- * documents stored in index.
- *
- * @constructor
- * @module
- */
-lunr.Store = function () {
- this.store = {}
- this.length = 0
-}
-
-/**
- * Loads a previously serialised store
- *
- * @param {Object} serialisedData The serialised store to load.
- * @returns {lunr.Store}
- * @memberOf Store
- */
-lunr.Store.load = function (serialisedData) {
- var store = new this
-
- store.length = serialisedData.length
- store.store = Object.keys(serialisedData.store).reduce(function (memo, key) {
- memo[key] = lunr.SortedSet.load(serialisedData.store[key])
- return memo
- }, {})
-
- return store
-}
-
-/**
- * Stores the given tokens in the store against the given id.
- *
- * @param {Object} id The key used to store the tokens against.
- * @param {Object} tokens The tokens to store against the key.
- * @memberOf Store
- */
-lunr.Store.prototype.set = function (id, tokens) {
- this.store[id] = tokens
- this.length = Object.keys(this.store).length
-}
-
-/**
- * Retrieves the tokens from the store for a given key.
- *
- * @param {Object} id The key to lookup and retrieve from the store.
- * @returns {Object}
- * @memberOf Store
- */
-lunr.Store.prototype.get = function (id) {
- return this.store[id]
-}
-
-/**
- * Checks whether the store contains a key.
- *
- * @param {Object} id The id to look up in the store.
- * @returns {Boolean}
- * @memberOf Store
- */
-lunr.Store.prototype.has = function (id) {
- return id in this.store
-}
-
-/**
- * Removes the value for a key in the store.
- *
- * @param {Object} id The id to remove from the store.
- * @memberOf Store
- */
-lunr.Store.prototype.remove = function (id) {
- if (!this.has(id)) return
-
- delete this.store[id]
- this.length--
-}
-
-/**
- * Returns a representation of the store ready for serialisation.
- *
- * @returns {Object}
- * @memberOf Store
- */
-lunr.Store.prototype.toJSON = function () {
- return {
- store: this.store,
- length: this.length
- }
-}
-
-/*!
- * lunr.stemmer
- * Copyright (C) 2013 Oliver Nightingale
- * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt
- */
-
-/**
- * lunr.stemmer is an english language stemmer, this is a JavaScript
- * implementation of the PorterStemmer taken from http://tartaurs.org/~martin
- *
- * @module
- * @param {String} str The string to stem
- * @returns {String}
- * @see lunr.Pipeline
- */
-lunr.stemmer = (function(){
- var step2list = {
- "ational" : "ate",
- "tional" : "tion",
- "enci" : "ence",
- "anci" : "ance",
- "izer" : "ize",
- "bli" : "ble",
- "alli" : "al",
- "entli" : "ent",
- "eli" : "e",
- "ousli" : "ous",
- "ization" : "ize",
- "ation" : "ate",
- "ator" : "ate",
- "alism" : "al",
- "iveness" : "ive",
- "fulness" : "ful",
- "ousness" : "ous",
- "aliti" : "al",
- "iviti" : "ive",
- "biliti" : "ble",
- "logi" : "log"
- },
-
- step3list = {
- "icate" : "ic",
- "ative" : "",
- "alize" : "al",
- "iciti" : "ic",
- "ical" : "ic",
- "ful" : "",
- "ness" : ""
- },
-
- c = "[^aeiou]", // consonant
- v = "[aeiouy]", // vowel
- C = c + "[^aeiouy]*", // consonant sequence
- V = v + "[aeiou]*", // vowel sequence
-
- mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0
- meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1
- mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1
- s_v = "^(" + C + ")?" + v; // vowel in stem
-
- return function (w) {
- var stem,
- suffix,
- firstch,
- re,
- re2,
- re3,
- re4;
-
- if (w.length < 3) { return w; }
-
- firstch = w.substr(0,1);
- if (firstch == "y") {
- w = firstch.toUpperCase() + w.substr(1);
- }
-
- // Step 1a
- re = /^(.+?)(ss|i)es$/;
- re2 = /^(.+?)([^s])s$/;
-
- if (re.test(w)) { w = w.replace(re,"$1$2"); }
- else if (re2.test(w)) { w = w.replace(re2,"$1$2"); }
-
- // Step 1b
- re = /^(.+?)eed$/;
- re2 = /^(.+?)(ed|ing)$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- re = new RegExp(mgr0);
- if (re.test(fp[1])) {
- re = /.$/;
- w = w.replace(re,"");
- }
- } else if (re2.test(w)) {
- var fp = re2.exec(w);
- stem = fp[1];
- re2 = new RegExp(s_v);
- if (re2.test(stem)) {
- w = stem;
- re2 = /(at|bl|iz)$/;
- re3 = new RegExp("([^aeiouylsz])\\1$");
- re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
- if (re2.test(w)) { w = w + "e"; }
- else if (re3.test(w)) { re = /.$/; w = w.replace(re,""); }
- else if (re4.test(w)) { w = w + "e"; }
- }
- }
-
- // Step 1c
- re = /^(.+?)y$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- stem = fp[1];
- re = new RegExp(s_v);
- if (re.test(stem)) { w = stem + "i"; }
- }
-
- // Step 2
- re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- stem = fp[1];
- suffix = fp[2];
- re = new RegExp(mgr0);
- if (re.test(stem)) {
- w = stem + step2list[suffix];
- }
- }
-
- // Step 3
- re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- stem = fp[1];
- suffix = fp[2];
- re = new RegExp(mgr0);
- if (re.test(stem)) {
- w = stem + step3list[suffix];
- }
- }
-
- // Step 4
- re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
- re2 = /^(.+?)(s|t)(ion)$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- stem = fp[1];
- re = new RegExp(mgr1);
- if (re.test(stem)) {
- w = stem;
- }
- } else if (re2.test(w)) {
- var fp = re2.exec(w);
- stem = fp[1] + fp[2];
- re2 = new RegExp(mgr1);
- if (re2.test(stem)) {
- w = stem;
- }
- }
-
- // Step 5
- re = /^(.+?)e$/;
- if (re.test(w)) {
- var fp = re.exec(w);
- stem = fp[1];
- re = new RegExp(mgr1);
- re2 = new RegExp(meq1);
- re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
- if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) {
- w = stem;
- }
- }
-
- re = /ll$/;
- re2 = new RegExp(mgr1);
- if (re.test(w) && re2.test(w)) {
- re = /.$/;
- w = w.replace(re,"");
- }
-
- // and turn initial Y back to y
-
- if (firstch == "y") {
- w = firstch.toLowerCase() + w.substr(1);
- }
-
- return w;
- }
-})();
-
-lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer')
-/*!
- * lunr.stopWordFilter
- * Copyright (C) 2013 Oliver Nightingale
- */
-
-/**
- * lunr.stopWordFilter is an English language stop word list filter, any words
- * contained in the list will not be passed through the filter.
- *
- * This is intended to be used in the Pipeline. If the token does not pass the
- * filter then undefined will be returned.
- *
- * @module
- * @param {String} token The token to pass through the filter
- * @returns {String}
- * @see lunr.Pipeline
- */
-lunr.stopWordFilter = function (token) {
- if (lunr.stopWordFilter.stopWords.indexOf(token) === -1) return token
-}
-
-lunr.stopWordFilter.stopWords = new lunr.SortedSet
-lunr.stopWordFilter.stopWords.length = 119
-lunr.stopWordFilter.stopWords.elements = [
- "a",
- "able",
- "about",
- "across",
- "after",
- "all",
- "almost",
- "also",
- "am",
- "among",
- "an",
- "and",
- "any",
- "are",
- "as",
- "at",
- "be",
- "because",
- "been",
- "but",
- "by",
- "can",
- "cannot",
- "could",
- "dear",
- "did",
- "do",
- "does",
- "either",
- "else",
- "ever",
- "every",
- "for",
- "from",
- "get",
- "got",
- "had",
- "has",
- "have",
- "he",
- "her",
- "hers",
- "him",
- "his",
- "how",
- "however",
- "i",
- "if",
- "in",
- "into",
- "is",
- "it",
- "its",
- "just",
- "least",
- "let",
- "like",
- "likely",
- "may",
- "me",
- "might",
- "most",
- "must",
- "my",
- "neither",
- "no",
- "nor",
- "not",
- "of",
- "off",
- "often",
- "on",
- "only",
- "or",
- "other",
- "our",
- "own",
- "rather",
- "said",
- "say",
- "says",
- "she",
- "should",
- "since",
- "so",
- "some",
- "than",
- "that",
- "the",
- "their",
- "them",
- "then",
- "there",
- "these",
- "they",
- "this",
- "tis",
- "to",
- "too",
- "twas",
- "us",
- "wants",
- "was",
- "we",
- "were",
- "what",
- "when",
- "where",
- "which",
- "while",
- "who",
- "whom",
- "why",
- "will",
- "with",
- "would",
- "yet",
- "you",
- "your"
-]
-
-lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter')
-/*!
- * lunr.stemmer
- * Copyright (C) 2013 Oliver Nightingale
- * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt
- */
-
-/**
- * lunr.TokenStore is used for efficient storing and lookup of the reverse
- * index of token to document ref.
- *
- * @constructor
- */
-lunr.TokenStore = function () {
- this.root = { docs: {} }
- this.length = 0
-}
-
-/**
- * Loads a previously serialised token store
- *
- * @param {Object} serialisedData The serialised token store to load.
- * @returns {lunr.TokenStore}
- * @memberOf TokenStore
- */
-lunr.TokenStore.load = function (serialisedData) {
- var store = new this
-
- store.root = serialisedData.root
- store.length = serialisedData.length
-
- return store
-}
-
-/**
- * Adds a new token doc pair to the store.
- *
- * By default this function starts at the root of the current store, however
- * it can start at any node of any token store if required.
- *
- * @param {String} token The token to store the doc under
- * @param {Object} doc The doc to store against the token
- * @param {Object} root An optional node at which to start looking for the
- * correct place to enter the doc, by default the root of this lunr.TokenStore
- * is used.
- * @memberOf TokenStore
- */
-lunr.TokenStore.prototype.add = function (token, doc, root) {
- var root = root || this.root,
- key = token[0],
- rest = token.slice(1)
-
- if (!(key in root)) root[key] = {docs: {}}
-
- if (rest.length === 0) {
- root[key].docs[doc.ref] = doc
- this.length += 1
- return
- } else {
- return this.add(rest, doc, root[key])
- }
-}
-
-/**
- * Checks whether this key is contained within this lunr.TokenStore.
- *
- * By default this function starts at the root of the current store, however
- * it can start at any node of any token store if required.
- *
- * @param {String} token The token to check for
- * @param {Object} root An optional node at which to start
- * @memberOf TokenStore
- */
-lunr.TokenStore.prototype.has = function (token, root) {
- var root = root || this.root,
- key = token[0],
- rest = token.slice(1)
-
- if (!(key in root)) return false
-
- if (rest.length === 0) {
- return true
- } else {
- return this.has(rest, root[key])
- }
-}
-
-/**
- * Retrieve a node from the token store for a given token.
- *
- * By default this function starts at the root of the current store, however
- * it can start at any node of any token store if required.
- *
- * @param {String} token The token to get the node for.
- * @param {Object} root An optional node at which to start.
- * @returns {Object}
- * @see TokenStore.prototype.get
- * @memberOf TokenStore
- */
-lunr.TokenStore.prototype.getNode = function (token, root) {
- var root = root || this.root,
- key = token[0],
- rest = token.slice(1)
-
- if (!(key in root)) return {}
-
- if (rest.length === 0) {
- return root[key]
- } else {
- return this.getNode(rest, root[key])
- }
-}
-
-/**
- * Retrieve the documents for a node for the given token.
- *
- * By default this function starts at the root of the current store, however
- * it can start at any node of any token store if required.
- *
- * @param {String} token The token to get the documents for.
- * @param {Object} root An optional node at which to start.
- * @returns {Object}
- * @memberOf TokenStore
- */
-lunr.TokenStore.prototype.get = function (token, root) {
- return this.getNode(token, root).docs || {}
-}
-
-/**
- * Remove the document identified by ref from the token in the store.
- *
- * By default this function starts at the root of the current store, however
- * it can start at any node of any token store if required.
- *
- * @param {String} token The token to get the documents for.
- * @param {String} ref The ref of the document to remove from this token.
- * @param {Object} root An optional node at which to start.
- * @returns {Object}
- * @memberOf TokenStore
- */
-lunr.TokenStore.prototype.remove = function (token, ref, root) {
- var root = root || this.root,
- key = token[0],
- rest = token.slice(1)
-
- if (!(key in root)) return
-
- if (rest.length === 0) {
- delete root[key].docs[ref]
- } else {
- return this.remove(rest, ref, root[key])
- }
-}
-
-/**
- * Find all the possible suffixes of the passed token using tokens
- * currently in the store.
- *
- * @param {String} token The token to expand.
- * @returns {Array}
- * @memberOf TokenStore
- */
-lunr.TokenStore.prototype.expand = function (token, memo) {
- var root = this.getNode(token),
- docs = root.docs || {},
- memo = memo || []
-
- if (Object.keys(docs).length) memo.push(token)
-
- Object.keys(root)
- .forEach(function (key) {
- if (key === 'docs') return
-
- memo.concat(this.expand(token + key, memo))
- }, this)
-
- return memo
-}
-
-/**
- * Returns a representation of the token store ready for serialisation.
- *
- * @returns {Object}
- * @memberOf TokenStore
- */
-lunr.TokenStore.prototype.toJSON = function () {
- return {
- root: this.root,
- length: this.length
- }
-}
-