diff --git a/ajax/libs/backbone.collectionView/0.9.0/backbone.collectionView.js b/ajax/libs/backbone.collectionView/0.9.0/backbone.collectionView.js
new file mode 100644
index 000000000..1024b6c27
--- /dev/null
+++ b/ajax/libs/backbone.collectionView/0.9.0/backbone.collectionView.js
@@ -0,0 +1,1228 @@
+/*!
+* Backbone.CollectionView, v0.9.0
+* Copyright (c)2013 Rotunda Software, LLC.
+* Distributed under MIT license
+* http://github.com/rotundasoftware/backbone-collection-view
+*/
+
+(function() {
+ var mDefaultModelViewConstructor = Backbone.View;
+
+ var kDefaultReferenceBy = "model";
+
+ var kOptionsRequiringRerendering = [ "collection", "modelView", "modelViewOptions", "itemTemplate", "selectableModelsFilter", "sortableModelsFilter", "visibleModelsFilter", "itemTemplateFunction", "detachedRendering", "sortableOptions" ];
+
+ var kStylesForEmptyListCaption = {
+ "background" : "transparent",
+ "border" : "none",
+ "box-shadow" : "none"
+ };
+
+ Backbone.CollectionView = Backbone.View.extend( {
+
+ tagName : "ul",
+
+ events : {
+ "mousedown li, td" : "_listItem_onMousedown",
+ "dblclick li, td" : "_listItem_onDoubleClick",
+ "click" : "_listBackground_onClick",
+ "click ul.collection-list, table.collection-list" : "_listBackground_onClick",
+ "keydown" : "_onKeydown"
+ },
+
+ // only used if Backbone.Courier is available
+ spawnMessages : {
+ "focus" : "focus"
+ },
+
+ //only used if Backbone.Courier is available
+ passMessages : { "*" : "." },
+
+ // viewOption definitions with default values.
+ initializationOptions : [ { "collection" : new Backbone.Collection() },
+ { "modelView" : null },
+ { "modelViewOptions" : {} },
+ { "itemTemplate" : null },
+ { "itemTemplateFunction" : null },
+ { "selectable" : true },
+ { "clickToSelect" : true },
+ { "selectableModelsFilter" : null },
+ { "visibleModelsFilter" : null },
+ { "sortableModelsFilter" : null },
+ { "selectMultiple" : false },
+ { "clickToToggle" : false },
+ { "processKeyEvents" : true },
+ { "sortable" : false },
+ { "sortableOptions" : null },
+ { "detachedRendering" : false },
+ { "emptyListCaption" : null }
+ ],
+
+ initialize : function( options ) {
+ Backbone.ViewOptions.add( this, "initializationOptions" ); // setup the ViewOptions functionality.
+ this.setOptions( options ); // and make use of any provided options
+
+ this._hasBeenRendered = false;
+
+ if( this._isBackboneCourierAvailable() ) {
+ Backbone.Courier.add( this );
+ }
+
+ this.$el.data( "view", this ); // needed for connected sortable lists
+ this.$el.addClass( "collection-list" );
+ if( this.selectable ) this.$el.addClass( "selectable" );
+
+ if( this.processKeyEvents )
+ this.$el.attr( "tabindex", 0 ); // so we get keyboard events
+
+ this.selectedItems = [];
+
+ this._updateItemTemplate();
+
+ if( this.collection )
+ this._registerCollectionEvents();
+
+ this.viewManager = new ChildViewContainer();
+ },
+
+ onOptionsChanged : function( changedOptions, originalOptions ) {
+ var rerender = false;
+ var _this = this;
+ _.each( _.keys( changedOptions ), function( changedOptionKey ) {
+ var newVal = changedOptions[ changedOptionKey ];
+ var oldVal = originalOptions[ changedOptionKey ];
+ switch( changedOptionKey ) {
+ case "collection" :
+ if ( newVal !== oldVal ) {
+ _this.stopListening( oldVal );
+ _this._registerCollectionEvents();
+ }
+ break;
+ case "selectMultiple":
+ if( ! newVal && _this.selectedItems.length > 1 )
+ _this.setSelectedModel( _.first( _this.selectedItems ), { by : "cid" } );
+ break;
+ case "selectable" :
+ if( ! newVal && _this.selectedItems.length > 0 )
+ _this.setSelectedModels( [] );
+ break;
+ case "selectableModelsFilter" :
+ if( newVal && _.isFunction( newVal ) )
+ _this._validateSelection();
+ break;
+ case "itemTemplate" :
+ _this._updateItemTemplate();
+ break;
+ case "processKeyEvents" :
+ if( newVal ) _this.$el.attr( "tabindex", 0 ); // so we get keyboard events
+ break;
+ case "modelView" :
+ //need to remove all old view instances
+ _this.viewManager.each( function( view ) {
+ _this.viewManager.remove( view );
+ // destroy the View itself
+ view.remove();
+ } );
+ break;
+ }
+ if( _.contains( kOptionsRequiringRerendering, changedOptionKey ) ) rerender = true;
+ });
+ if( this._hasBeenRendered && rerender ) {
+ this.render(); // Rerender the view if the rerender flag has been set.
+ }
+ },
+
+ setOption : function( optionName, optionValue ) { // now is mearly a wrapper around backbone.viewOptions' setOptions()
+ var optionHash = {};
+ optionHash[ optionName ] = optionValue;
+ this.setOptions( optionHash );
+ },
+
+ getSelectedModel : function( options ) {
+ return _.first( this.getSelectedModels( options ) );
+ },
+
+ getSelectedModels : function ( options ) {
+ var _this = this;
+
+ options = _.extend( {}, {
+ by : kDefaultReferenceBy
+ }, options );
+
+ var referenceBy = options.by;
+ var items = [];
+
+ switch( referenceBy ) {
+ case "id" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.collection.get( item ).id );
+ } );
+ break;
+ case "cid" :
+ items = items.concat( this.selectedItems );
+ break;
+ case "offset" :
+ var curLineNumber = 0;
+
+ var itemElements = this._getVisibleItemEls();
+
+ itemElements.each( function() {
+ var thisItemEl = $( this );
+ if( thisItemEl.is( ".selected" ) )
+ items.push( curLineNumber );
+ curLineNumber++;
+ } );
+ break;
+ case "model" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.collection.get( item ) );
+ } );
+ break;
+ case "view" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.viewManager.findByModel( _this.collection.get( item ) ) );
+ } );
+ break;
+ }
+
+ return items;
+
+ },
+
+ setSelectedModels : function( newSelectedItems, options ) {
+ if( ! _.isArray( newSelectedItems ) ) throw "Invalid parameter value";
+ if( ! this.selectable && newSelectedItems.length > 0 ) return; // used to throw error, but there are some circumstances in which a list can be selectable at times and not at others, don't want to have to worry about catching errors
+
+ options = _.extend( {}, {
+ silent : false,
+ by : kDefaultReferenceBy
+ }, options );
+
+ var referenceBy = options.by;
+ var newSelectedCids = [];
+
+ switch( referenceBy ) {
+ case "cid" :
+ newSelectedCids = newSelectedItems;
+ break;
+ case "id" :
+ this.collection.each( function( thisModel ) {
+ if( _.contains( newSelectedItems, thisModel.id ) ) newSelectedCids.push( thisModel.cid );
+ } );
+ break;
+ case "model" :
+ newSelectedCids = _.pluck( newSelectedItems, "cid" );
+ break;
+ case "view" :
+ _.each( newSelectedItems, function( item ) {
+ newSelectedCids.push( item.model.cid );
+ } );
+ break;
+ case "offset" :
+ var curLineNumber = 0;
+ var selectedItems = [];
+
+ var itemElements = this._getVisibleItemEls();
+ itemElements.each( function() {
+ var thisItemEl = $( this );
+ if( _.contains( newSelectedItems, curLineNumber ) )
+ newSelectedCids.push( thisItemEl.attr( "data-model-cid" ) );
+ curLineNumber++;
+ } );
+ break;
+ }
+
+ var oldSelectedModels = this.getSelectedModels();
+ var oldSelectedCids = _.clone( this.selectedItems );
+
+ this.selectedItems = this._convertStringsToInts( newSelectedCids );
+ this._validateSelection();
+
+ var newSelectedModels = this.getSelectedModels();
+
+ if( ! this._containSameElements( oldSelectedCids, this.selectedItems ) )
+ {
+ this._addSelectedClassToSelectedItems( oldSelectedCids );
+
+ if( ! options.silent )
+ {
+ this.trigger( "selectionChanged", newSelectedModels, oldSelectedModels );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "selectionChanged", {
+ selectedModels : newSelectedModels,
+ oldSelectedModels : oldSelectedModels
+ } );
+ }
+ }
+
+ this.updateDependentControls();
+ }
+ },
+
+ setSelectedModel : function( newSelectedItem, options ) {
+ if( ! newSelectedItem && newSelectedItem !== 0 )
+ this.setSelectedModels( [], options );
+ else
+ this.setSelectedModels( [ newSelectedItem ], options );
+ },
+
+ render : function(){
+ var _this = this;
+
+ this._hasBeenRendered = true;
+
+ if( this.selectable ) this._saveSelection();
+
+ var modelViewContainerEl;
+
+ // If collection view element is a table and it has a tbody
+ // within it, render the model views inside of the tbody
+ modelViewContainerEl = this._getContainerEl();
+
+ var oldViewManager = this.viewManager;
+ this.viewManager = new ChildViewContainer();
+
+ // detach each of our subviews that we have already created to represent models
+ // in the collection. We are going to re-use the ones that represent models that
+ // are still here, instead of creating new ones, so that we don't loose state
+ // information in the views.
+ oldViewManager.each( function( thisModelView ) {
+ // to boost performance, only detach those views that will be sticking around.
+ // we won't need the other ones later, so no need to detach them individually.
+ if( _this.collection.get( thisModelView.model.cid ) )
+ thisModelView.$el.detach();
+ else
+ thisModelView.remove();
+ } );
+
+ modelViewContainerEl.empty();
+ var fragmentContainer;
+
+ if( this.detachedRendering )
+ fragmentContainer = document.createDocumentFragment();
+
+ this.collection.each( function( thisModel ) {
+ var thisModelView = oldViewManager.findByModelCid( thisModel.cid );
+ if( _.isUndefined( thisModelView ) ) {
+ // if the model view has not already been created on a
+ // previous render then create and initialize it now.
+ thisModelView = this._createNewModelView( thisModel, this._getModelViewOptions( thisModel ) );
+ }
+
+ this._insertAndRenderModelView( thisModelView, fragmentContainer || modelViewContainerEl );
+ }, this );
+
+ if( this.detachedRendering )
+ modelViewContainerEl.append( fragmentContainer );
+
+ if( this.sortable )
+ {
+ var sortableOptions = _.extend( {
+ axis: "y",
+ distance: 10,
+ forcePlaceholderSize : true,
+ start : _.bind( this._sortStart, this ),
+ change : _.bind( this._sortChange, this ),
+ stop : _.bind( this._sortStop, this ),
+ receive : _.bind( this._receive, this ),
+ over : _.bind( this._over, this )
+ }, _.result( this, "sortableOptions" ) );
+
+ if( _this._isRenderedAsTable() ) {
+ sortableOptions.items = "> tbody > tr:not(.not-sortable)";
+ }
+ else if( _this._isRenderedAsList() ) {
+ sortableOptions.items = "> li:not(.not-sortable)";
+ }
+
+ this.$el = this.$el.sortable( sortableOptions );
+ }
+
+ this._showEmptyListCaptionIfAppropriate();
+
+ this.trigger( "render" );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "render" );
+
+ if( this.selectable ) {
+ this._restoreSelection();
+ this.updateDependentControls();
+ }
+
+ if( _.isFunction( this.onAfterRender ) )
+ this.onAfterRender();
+ },
+
+ _showEmptyListCaptionIfAppropriate : function ( ) {
+ if( this.emptyListCaption ) {
+ var visibleEls = this._getVisibleItemEls();
+
+ if( visibleEls.length === 0 ) {
+ var emptyListString;
+
+ if( _.isFunction( this.emptyListCaption ) )
+ emptyListString = this.emptyListCaption();
+ else
+ emptyListString = this.emptyListCaption;
+
+ var $emptyCaptionEl;
+ var $varEl = $( "" + emptyListString + "" );
+
+ //need to wrap the empty caption to make it fit the rendered list structure (either with an li or a tr td)
+ if( this._isRenderedAsList() )
+ $emptyListCaptionEl = $varEl.wrapAll( "
" ).parent().css( kStylesForEmptyListCaption );
+ else
+ $emptyListCaptionEl = $varEl.wrapAll( " |
" ).parent().parent().css( kStylesForEmptyListCaption );
+
+ this._getContainerEl().append( $emptyListCaptionEl );
+ }
+ }
+ },
+
+ _removeEmptyListCaption : function( ) {
+ if( this._isRenderedAsList() )
+ this._getContainerEl().find( "> li > var.empty-list-caption" ).parent().remove();
+ else
+ this._getContainerEl().find( "> tr > td > var.empty-list-caption" ).parent().parent().remove();
+ },
+
+ // Render a single model view in container object "parentElOrDocumentFragment", which is either
+ // a documentFragment or a jquery object. optional arg atIndex is not support for document fragments.
+ _insertAndRenderModelView : function( modelView, parentElOrDocumentFragment, atIndex ) {
+ var thisModelViewWrapped = this._wrapModelView( modelView );
+
+ if( parentElOrDocumentFragment.nodeType === 11 ) // if we are inserting into a document fragment, we need to use the DOM appendChild method
+ parentElOrDocumentFragment.appendChild( thisModelViewWrapped.get( 0 ) );
+ else if( ! _.isUndefined( atIndex ) && atIndex > 0 && atIndex < this.collection.length - 1 )
+ parentElOrDocumentFragment.children().eq( atIndex ).before( thisModelViewWrapped );
+ else
+ parentElOrDocumentFragment.append( thisModelViewWrapped );
+
+ // we have to render the modelView after it has been put in context, as opposed to in the
+ // initialize function of the modelView, because some rendering might be dependent on
+ // the modelView's context in the DOM tree. For example, if the modelView stretch()'s itself,
+ // it must be in full context in the DOM tree or else the stretch will not behave as intended.
+ var renderResult = modelView.render();
+
+ // return false from the view's render function to hide this item
+ if( renderResult === false ) {
+ thisModelViewWrapped.hide();
+ thisModelViewWrapped.addClass( "not-visible" );
+ }
+
+ var hideThisModelView = false;
+ if( _.isFunction( this.visibleModelsFilter ) ) {
+ hideThisModelView = ! this.visibleModelsFilter( modelView.model );
+ if( hideThisModelView ) {
+ if( thisModelViewWrapped.children().length === 1 )
+ thisModelViewWrapped.hide();
+ else modelView.$el.hide();
+
+ thisModelViewWrapped.addClass( "not-visible" );
+ }
+ }
+
+ if( ! hideThisModelView && this.emptyListCaption ) this._removeEmptyListCaption();
+
+ this.viewManager.add( modelView );
+ },
+
+ updateDependentControls : function() {
+ this.trigger( "updateDependentControls", this.getSelectedModels() );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "updateDependentControls", {
+ selectedModels : this.getSelectedModels()
+ } );
+ }
+ },
+
+ // Override `Backbone.View.remove` to also destroy all Views in `viewManager`
+ remove : function() {
+ this.viewManager.each( function( view ) {
+ view.remove();
+ } );
+
+ Backbone.View.prototype.remove.apply( this, arguments );
+ },
+
+ // A method to remove the view relating to model.
+ _removeModelView : function( model ) {
+ var viewManager = this.viewManager;
+ var view = viewManager.findByModelCid( model.cid );
+
+ if ( this.selectable ) this._saveSelection();
+
+ viewManager.remove( view ); // Remove the view from the viewManager
+ view.remove(); // Remove the view from the DOM
+ this._getContainerEl().children( "[data-model-cid=" + model.cid + "]" ).remove(); // Remove the wrapper from the DOM
+
+ if ( this.selectable ) this._restoreSelection();
+
+ this._showEmptyListCaptionIfAppropriate();
+ },
+
+ _validateSelectionAndRender : function() {
+ this._validateSelection();
+ this.render();
+ },
+
+ _registerCollectionEvents : function() {
+ this.listenTo( this.collection, "add", function( model ) {
+ if( this._hasBeenRendered ) {
+ var modelView = this._createNewModelView( model, this._getModelViewOptions( model ) );
+ this._insertAndRenderModelView( modelView, this._getContainerEl(), this.collection.indexOf( model ) );
+ }
+
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "add" );
+ } );
+
+ this.listenTo( this.collection, "remove", function( model ) {
+ if( this._hasBeenRendered )
+ this._removeModelView( model );
+
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "remove" );
+ } );
+
+ this.listenTo( this.collection, "reset", function() {
+ if( this._hasBeenRendered ) this.render();
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "reset" );
+ } );
+
+ // we should not be listening to change events on the model as a default behavior. the models
+ // should be responsible for re-rendering themselves if necessary, and if the collection does
+ // also need to re-render as a result of a model change, this should be handled by overriding
+ // this method. by default the collection view should not re-render in response to model changes
+ // this.listenTo( this.collection, "change", function( model ) {
+ // if( this._hasBeenRendered ) this.viewManager.findByModel( model ).render();
+ // if( this._isBackboneCourierAvailable() )
+ // this.spawn( "change", { model : model } );
+ // } );
+
+ this.listenTo( this.collection, "sort", function( collection, options ) {
+ if( this._hasBeenRendered && options.add !== true ) this.render();
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sort" );
+ } );
+ },
+
+ _getContainerEl : function() {
+ if ( this._isRenderedAsTable() ) {
+ // not all tables have a tbody, so we test
+ var tbody = this.$el.find( "> tbody" );
+ if ( tbody.length > 0 )
+ return tbody;
+ }
+ return this.$el;
+ },
+
+ _getClickedItemId : function( theEvent ) {
+ var clickedItemId = null;
+
+ // important to use currentTarget as opposed to target, since we could be bubbling
+ // an event that took place within another collectionList
+ var clickedItemEl = $( theEvent.currentTarget );
+ if( clickedItemEl.closest( ".collection-list" ).get(0) !== this.$el.get(0) ) return;
+
+ // determine which list item was clicked. If we clicked in the blank area
+ // underneath all the elements, we want to know that too, since in this
+ // case we will want to deselect all elements. so check to see if the clicked
+ // DOM element is the list itself to find that out.
+ var clickedItem = clickedItemEl.closest( "[data-model-cid]" );
+ if( clickedItem.length > 0 )
+ {
+ clickedItemId = clickedItem.attr( "data-model-cid" );
+ if( $.isNumeric( clickedItemId ) ) clickedItemId = parseInt( clickedItemId, 10 );
+ }
+
+ return clickedItemId;
+ },
+
+ _updateItemTemplate : function() {
+ var itemTemplateHtml;
+ if( this.itemTemplate )
+ {
+ if( $( this.itemTemplate ).length === 0 )
+ throw "Could not find item template from selector: " + this.itemTemplate;
+
+ itemTemplateHtml = $( this.itemTemplate ).html();
+ }
+ else
+ itemTemplateHtml = this.$( ".item-template" ).html();
+
+ if( itemTemplateHtml ) this.itemTemplateFunction = _.template( itemTemplateHtml );
+
+ },
+
+ _validateSelection : function() {
+ // note can't use the collection's proxy to underscore because "cid" is not an attribute,
+ // but an element of the model object itself.
+ var modelReferenceIds = _.pluck( this.collection.models, "cid" );
+ this.selectedItems = _.intersection( modelReferenceIds, this.selectedItems );
+
+ if( _.isFunction( this.selectableModelsFilter ) )
+ {
+ this.selectedItems = _.filter( this.selectedItems, function( thisItemId ) {
+ return this.selectableModelsFilter.call( this, this.collection.get( thisItemId ) );
+ }, this );
+ }
+ },
+
+ _saveSelection : function() {
+ // save the current selection. use restoreSelection() to restore the selection to the state it was in the last time saveSelection() was called.
+ if( ! this.selectable ) throw "Attempt to save selection on non-selectable list";
+ this.savedSelection = {
+ items : _.clone( this.selectedItems ),
+ offset : this.getSelectedModel( { by : "offset" } )
+ };
+ },
+
+ _restoreSelection : function() {
+ if( ! this.savedSelection ) throw "Attempt to restore selection but no selection has been saved!";
+
+ // reset selectedItems to empty so that we "redraw" all "selected" classes
+ // when we set our new selection. We do this because it is likely that our
+ // contents have been refreshed, and we have thus lost all old "selected" classes.
+ this.setSelectedModels( [], { silent : true } );
+
+ if( this.savedSelection.items.length > 0 )
+ {
+ // first try to restore the old selected items using their reference ids.
+ this.setSelectedModels( this.savedSelection.items, { by : "cid", silent : true } );
+
+ // all the items with the saved reference ids have been removed from the list.
+ // ok. try to restore the selection based on the offset that used to be selected.
+ // this is the expected behavior after a item is deleted from a list (i.e. select
+ // the line that immediately follows the deleted line).
+ if( this.selectedItems.length === 0 )
+ this.setSelectedModel( this.savedSelection.offset, { by : "offset" } );
+
+ // Trigger a selection changed if the previously selected items were not all found
+ if (this.selectedItems.length !== this.savedSelection.items.length)
+ {
+ this.trigger( "selectionChanged", this.getSelectedModels(), [] );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "selectionChanged", {
+ selectedModels : this.getSelectedModels(),
+ oldSelectedModels : []
+ } );
+ }
+ }
+ }
+
+ delete this.savedSelection;
+ },
+
+ _addSelectedClassToSelectedItems : function( oldItemsIdsWithSelectedClass ) {
+ if( _.isUndefined( oldItemsIdsWithSelectedClass ) ) oldItemsIdsWithSelectedClass = [];
+
+ // oldItemsIdsWithSelectedClass is used for optimization purposes only. If this info is supplied then we
+ // only have to add / remove the "selected" class from those items that "selected" state has changed.
+
+ var itemsIdsFromWhichSelectedClassNeedsToBeRemoved = oldItemsIdsWithSelectedClass;
+ itemsIdsFromWhichSelectedClassNeedsToBeRemoved = _.without( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, this.selectedItems );
+
+ _.each( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, function( thisItemId ) {
+ this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).removeClass( "selected" );
+ }, this );
+
+ var itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems;
+ itemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass );
+
+ _.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) {
+ this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).addClass( "selected" );
+ }, this );
+ },
+
+ _reorderCollectionBasedOnHTML : function() {
+ var _this = this;
+
+ this._getContainerEl().children().each( function() {
+ var thisModelCid = $( this ).attr( "data-model-cid" );
+
+ if( thisModelCid )
+ {
+ // remove the current model and then add it back (at the end of the collection).
+ // When we are done looping through all models, they will be in the correct order.
+ var thisModel = _this.collection.get( thisModelCid );
+ if( thisModel )
+ {
+ _this.collection.remove( thisModel, { silent : true } );
+ _this.collection.add( thisModel, { silent : true, sort : ! _this.collection.comparator } );
+ }
+ }
+ } );
+
+ this.collection.trigger( "reorder" );
+
+ if( this._isBackboneCourierAvailable() ) this.spawn( "reorder" );
+
+ if( this.collection.comparator ) this.collection.sort();
+
+ },
+
+ _getModelViewConstructor : function( thisModel ) {
+ return this.modelView || mDefaultModelViewConstructor;
+ },
+
+ _getModelViewOptions : function( thisModel ) {
+ return _.extend( { model : thisModel }, this.modelViewOptions );
+ },
+
+ _createNewModelView : function( model, modelViewOptions ) {
+ var modelViewConstructor = this._getModelViewConstructor( model );
+ if( _.isUndefined( modelViewConstructor ) ) throw "Could not find modelView constructor for model";
+
+ var newModelView = new( modelViewConstructor )( modelViewOptions );
+ newModelView.collectionListView = this;
+
+ return newModelView;
+ },
+
+ _wrapModelView : function( modelView ) {
+ var _this = this;
+
+ // we use items client ids as opposed to real ids, since we may not have a representation
+ // of these models on the server
+ var wrappedModelView;
+
+ if( this._isRenderedAsTable() ) {
+ // if we are rendering the collection in a table, the template $el is a tr so we just need to set the data-model-cid
+ wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid );
+ }
+ else if( this._isRenderedAsList() ) {
+ // if we are rendering the collection in a list, we need wrap each item in an (if its not already an )
+ // and set the data-model-cid
+ if( modelView.$el.prop( "tagName" ).toLowerCase() === "li" ) {
+ wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid );
+ } else {
+ wrappedModelView = modelView.$el.wrapAll( "" ).parent();
+ }
+ }
+
+ if( _.isFunction( this.sortableModelsFilter ) )
+ if( ! this.sortableModelsFilter.call( _this, modelView.model ) )
+ wrappedModelView.addClass( "not-sortable" );
+
+ if( _.isFunction( this.selectableModelsFilter ) )
+ if( ! this.selectableModelsFilter.call( _this, modelView.model ) )
+ wrappedModelView.addClass( "not-selectable" );
+
+ return wrappedModelView;
+ },
+
+ _convertStringsToInts : function( theArray ) {
+ return _.map( theArray, function( thisEl ) {
+ if( ! _.isString( thisEl ) ) return thisEl;
+ var thisElAsNumber = parseInt( thisEl, 10 );
+ return( thisElAsNumber == thisEl ? thisElAsNumber : thisEl );
+ } );
+ },
+
+ _containSameElements : function( arrayA, arrayB ) {
+ if( arrayA.length != arrayB.length ) return false;
+ var intersectionSize = _.intersection( arrayA, arrayB ).length;
+ return intersectionSize == arrayA.length; // and must also equal arrayB.length, since arrayA.length == arrayB.length
+ },
+
+ _isRenderedAsTable : function() {
+ return this.$el.prop( "tagName" ).toLowerCase() === "table";
+ },
+
+ _isRenderedAsList : function() {
+ return ! this._isRenderedAsTable();
+ },
+
+ // Returns the wrapper HTML element for each visible modelView.
+ // When rendering in a table context, the returned elements are the $el of each modelView.
+ // When rendering in a list context,
+ // If the $el of the modelView is an , the returned elements are the $el of each modelView.
+ // Otherwise, the returned elements are the 's the collectionView wrapped around each modelView $el.
+ _getVisibleItemEls : function() {
+ var itemElements = [];
+ itemElements = this._getContainerEl().find( "> [data-model-cid]:not(.not-visible)" );
+
+ return itemElements;
+ },
+
+ _charCodes : {
+ upArrow : 38,
+ downArrow : 40
+ },
+
+ _isBackboneCourierAvailable : function() {
+ return !_.isUndefined( Backbone.Courier );
+ },
+
+ _sortStart : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ this.trigger( "sortStart", modelBeingSorted );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortStart", { modelBeingSorted : modelBeingSorted } );
+ },
+
+ _sortChange : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ this.trigger( "sortChange", modelBeingSorted );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortChange", { modelBeingSorted : modelBeingSorted } );
+ },
+
+ _sortStop : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ var modelViewContainerEl = this._getContainerEl();
+ var newIndex = modelViewContainerEl.children().index( ui.item );
+
+ if( newIndex == -1 ) {
+ // the element was removed from this list. can happen if this sortable is connected
+ // to another sortable, and the item was dropped into the other sortable.
+ this.collection.remove( modelBeingSorted );
+ }
+
+ this._reorderCollectionBasedOnHTML();
+ this.updateDependentControls();
+ this.trigger( "sortStop", modelBeingSorted, newIndex );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortStop", { modelBeingSorted : modelBeingSorted, newIndex : newIndex } );
+ },
+
+ _receive : function( event, ui ) {
+ var senderListEl = ui.sender;
+ var senderCollectionListView = senderListEl.data( "view" );
+ if( ! senderCollectionListView || ! senderCollectionListView.collection ) return;
+
+ var newIndex = this._getContainerEl().children().index( ui.item );
+ var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) );
+ senderCollectionListView.collection.remove( modelReceived );
+ this.collection.add( modelReceived, { at : newIndex } );
+ modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value.
+ this.setSelectedModel( modelReceived );
+ },
+
+ _over : function( event, ui ) {
+ // when an item is being dragged into the sortable,
+ // hide the empty list caption if it exists
+ this._getContainerEl().find( "> var.empty-list-caption" ).hide();
+ },
+
+ _onKeydown : function( event ) {
+ if( ! this.processKeyEvents ) return true;
+
+ var trap = false;
+
+ if( this.getSelectedModels( { by : "offset" } ).length == 1 )
+ {
+ // need to trap down and up arrows or else the browser
+ // will end up scrolling a autoscroll div.
+
+ var currentOffset = this.getSelectedModel( { by : "offset" } );
+ if( event.which === this._charCodes.upArrow && currentOffset !== 0 )
+ {
+ this.setSelectedModel( currentOffset - 1, { by : "offset" } );
+ trap = true;
+ }
+ else if( event.which === this._charCodes.downArrow && currentOffset !== this.collection.length - 1 )
+ {
+ this.setSelectedModel( currentOffset + 1, { by : "offset" } );
+ trap = true;
+ }
+ }
+
+ return ! trap;
+ },
+
+ _listItem_onMousedown : function( theEvent ) {
+ if( ! this.selectable || ! this.clickToSelect ) return;
+
+ var clickedItemId = this._getClickedItemId( theEvent );
+
+ if( clickedItemId )
+ {
+ // Exit if an unselectable item was clicked
+ if( _.isFunction( this.selectableModelsFilter ) &&
+ ! this.selectableModelsFilter.call( this, this.collection.get( clickedItemId ) ) )
+ {
+ return;
+ }
+
+ // a selectable list item was clicked
+ if( this.selectMultiple && theEvent.shiftKey )
+ {
+ var firstSelectedItemIndex = -1;
+
+ if( this.selectedItems.length > 0 )
+ {
+ this.collection.find( function( thisItemModel ) {
+ firstSelectedItemIndex++;
+
+ // exit when we find our first selected element
+ return _.contains( this.selectedItems, thisItemModel.cid );
+ }, this );
+ }
+
+ var clickedItemIndex = -1;
+ this.collection.find( function( thisItemModel ) {
+ clickedItemIndex++;
+
+ // exit when we find the clicked element
+ return thisItemModel.cid == clickedItemId;
+ }, this );
+
+ var shiftKeyRootSelectedItemIndex = firstSelectedItemIndex == -1 ? clickedItemIndex : firstSelectedItemIndex;
+ var minSelectedItemIndex = Math.min( clickedItemIndex, shiftKeyRootSelectedItemIndex );
+ var maxSelectedItemIndex = Math.max( clickedItemIndex, shiftKeyRootSelectedItemIndex );
+
+ var newSelectedItems = [];
+ for( var thisIndex = minSelectedItemIndex; thisIndex <= maxSelectedItemIndex; thisIndex ++ )
+ newSelectedItems.push( this.collection.at( thisIndex ).cid );
+ this.setSelectedModels( newSelectedItems, { by : "cid" } );
+
+ // shift clicking will usually highlight selectable text, which we do not want.
+ // this is a cross browser (hopefully) snippet that deselects all text selection.
+ if( document.selection && document.selection.empty )
+ document.selection.empty();
+ else if(window.getSelection) {
+ var sel = window.getSelection();
+ if( sel && sel.removeAllRanges )
+ sel.removeAllRanges();
+ }
+ }
+ else if( this.selectMultiple && ( this.clickToToggle || theEvent.metaKey ) )
+ {
+ if( _.contains( this.selectedItems, clickedItemId ) )
+ this.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : "cid" } );
+ else this.setSelectedModels( _.union( this.selectedItems, clickedItemId ), { by : "cid" } );
+ }
+ else
+ this.setSelectedModels( [ clickedItemId ], { by : "cid" } );
+ }
+ else
+ // the blank area of the list was clicked
+ this.setSelectedModels( [] );
+
+ },
+
+ _listItem_onDoubleClick : function( theEvent ) {
+ var clickedItemId = this._getClickedItemId( theEvent );
+
+ if( clickedItemId )
+ {
+ var clickedModel = this.collection.get( clickedItemId );
+ this.trigger( "doubleClick", clickedModel );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "doubleClick", { clickedModel : clickedModel } );
+ }
+ },
+
+ _listBackground_onClick : function( theEvent ) {
+ if( ! this.selectable ) return;
+ if( ! $( theEvent.target ).is( ".collection-list" ) ) return;
+
+ this.setSelectedModels( [] );
+ }
+
+ }, {
+ setDefaultModelViewConstructor : function( theConstructor ) {
+ mDefaultModelViewConstructor = theConstructor;
+ }
+ });
+
+ // Backbone.ViewOptions
+ // --------------------
+ // v0.2.0
+ //
+ // Copyright (c)2014 Rotunda Software
+ //
+ // https://github.com/rotundasoftware/backbone.viewOptions
+
+ // Backbone.ViewOptions
+ // --------------------
+ //
+ // An plugin to declare and get/set options on views.
+
+ /*
+ * Backbone.ViewOptions, v0.2
+ * Copyright (c)2014 Rotunda Software, LLC.
+ * Distributed under MIT license
+ * http://github.com/rotundasoftware/backbone.viewOptions
+ */
+
+ Backbone.ViewOptions = {};
+
+ Backbone.ViewOptions.add = function( view, optionsDeclarationsProperty ) {
+ if( _.isUndefined( optionsDeclarationsProperty ) ) optionsDeclarationsProperty = "options";
+
+ // ****************** Public methods added to view ******************
+
+ view.setOptions = function( options ) {
+ var _this = this;
+ var optionsThatWereChanged = {};
+ var optionsThatWereChangedOriginalValues = {};
+
+ var optionDeclarations = _.result( this, optionsDeclarationsProperty );
+
+ if( ! _.isUndefined( optionDeclarations ) ) {
+ var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );
+
+ _.each( normalizedOptionDeclarations, function( thisOptionDeclaration ) {
+ thisOptionName = thisOptionDeclaration.name;
+ thisOptionRequired = thisOptionDeclaration.required;
+ thisOptionDefaultValue = thisOptionDeclaration.defaultValue;
+
+ if( thisOptionRequired ) {
+ // note we do not throw an error if a required option is not supplied, but it is
+ // found on the object itself (due to a prior call of view.setOptions, most likely)
+ if( ! options ||
+ ( ( ! _.contains( _.keys( options ), thisOptionName ) && _.isUndefined( _this[ thisOptionName ] ) ) ) ||
+ _.isUndefined( options[ thisOptionName ] ) )
+ throw new Error( "Required option \"" + thisOptionName + "\" was not supplied." );
+ }
+
+ // attach the supplied value of this option, or the appropriate default value, to the view object
+ if( options && thisOptionName in options ) {
+ // if this option already exists on the view, make a note that we will be changing it
+ if( ! _.isUndefined( _this[ thisOptionName ] ) ) {
+ optionsThatWereChangedOriginalValues[ thisOptionName ] = _this[ thisOptionName ];
+ optionsThatWereChanged[ thisOptionName ] = options[ thisOptionName ];
+ }
+ _this[ thisOptionName ] = options[ thisOptionName ];
+ // note we do NOT delete the option off the options object here so that
+ // multiple views can be passed the same options object without issue.
+ }
+ else if( ! _.isUndefined( thisOptionDefaultValue ) && _.isUndefined( _this[ thisOptionName ] ) ) {
+ // note defaults do not write over any existing properties on the view itself.
+ _this[ thisOptionName ] = thisOptionDefaultValue;
+ }
+ } );
+ }
+
+ if( _.keys( optionsThatWereChanged ).length > 0 ) {
+ if( _.isFunction( _this.onOptionsChanged ) )
+ _this.onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedOriginalValues );
+ else if( _.isFunction( _this._onOptionsChanged ) )
+ _this._onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedOriginalValues );
+ }
+ };
+
+ view.getOptions = function() {
+ var optionDeclarations = _.result( this, optionsDeclarationsProperty );
+ if( _.isUndefined( optionDeclarations ) ) return [];
+
+ var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );
+ var optionsNames = _.pluck( normalizedOptionDeclarations, "name" );
+
+ return _.pick( this, optionsNames );
+ };
+ };
+
+ // ****************** Private Utility Functions ******************
+
+ function _normalizeOptionDeclarations( optionDeclarations ) {
+ // convert our short-hand option syntax (with exclamation marks, etc.)
+ // to a simple array of standard option declaration objects.
+ var normalizedOptionDeclarations = [];
+
+ if( ! _.isArray( optionDeclarations ) ) {
+ throw new Error( "Option declarations must be an array." );
+ }
+
+ _.each( optionDeclarations, function( thisOptionDeclaration ) {
+ var thisOptionName, thisOptionRequired, thisOptionDefaultValue;
+
+ thisOptionRequired = false;
+ thisOptionDefaultValue = undefined;
+
+ if( _.isString( thisOptionDeclaration ) )
+ thisOptionName = thisOptionDeclaration;
+ else if( _.isObject( thisOptionDeclaration ) ) {
+ thisOptionName = _.first( _.keys( thisOptionDeclaration ) );
+ thisOptionDefaultValue = _.clone( thisOptionDeclaration[ thisOptionName ] );
+ }
+ else throw new Error( "Each element in the option declarations array must be either a string or an object." );
+
+ if( thisOptionName[ thisOptionName.length - 1 ] === "!" ) {
+ thisOptionRequired = true;
+ thisOptionName = thisOptionName.slice( 0, thisOptionName.length - 1 );
+ }
+
+ normalizedOptionDeclarations.push( {
+ name : thisOptionName,
+ required : thisOptionRequired,
+ defaultValue : thisOptionDefaultValue
+ } );
+ } );
+
+ return normalizedOptionDeclarations;
+ };
+
+
+ // Backbone.BabySitter
+ // -------------------
+ // v0.0.6
+ //
+ // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
+ // Distributed under MIT license
+ //
+ // http://github.com/babysitterjs/backbone.babysitter
+
+ // Backbone.ChildViewContainer
+ // ---------------------------
+ //
+ // Provide a container to store, retrieve and
+ // shut down child views.
+
+ ChildViewContainer = (function(Backbone, _){
+
+ // Container Constructor
+ // ---------------------
+
+ var Container = function(views){
+ this._views = {};
+ this._indexByModel = {};
+ this._indexByCustom = {};
+ this._updateLength();
+
+ _.each(views, this.add, this);
+ };
+
+ // Container Methods
+ // -----------------
+
+ _.extend(Container.prototype, {
+
+ // Add a view to this container. Stores the view
+ // by `cid` and makes it searchable by the model
+ // cid (and model itself). Optionally specify
+ // a custom key to store an retrieve the view.
+ add: function(view, customIndex){
+ var viewCid = view.cid;
+
+ // store the view
+ this._views[viewCid] = view;
+
+ // index it by model
+ if (view.model){
+ this._indexByModel[view.model.cid] = viewCid;
+ }
+
+ // index by custom
+ if (customIndex){
+ this._indexByCustom[customIndex] = viewCid;
+ }
+
+ this._updateLength();
+ },
+
+ // Find a view by the model that was attached to
+ // it. Uses the model's `cid` to find it.
+ findByModel: function(model){
+ return this.findByModelCid(model.cid);
+ },
+
+ // Find a view by the `cid` of the model that was attached to
+ // it. Uses the model's `cid` to find the view `cid` and
+ // retrieve the view using it.
+ findByModelCid: function(modelCid){
+ var viewCid = this._indexByModel[modelCid];
+ return this.findByCid(viewCid);
+ },
+
+ // Find a view by a custom indexer.
+ findByCustom: function(index){
+ var viewCid = this._indexByCustom[index];
+ return this.findByCid(viewCid);
+ },
+
+ // Find by index. This is not guaranteed to be a
+ // stable index.
+ findByIndex: function(index){
+ return _.values(this._views)[index];
+ },
+
+ // retrieve a view by it's `cid` directly
+ findByCid: function(cid){
+ return this._views[cid];
+ },
+
+ findIndexByCid : function( cid ) {
+ var index = -1;
+ var view = _.find( this._views, function ( view ) {
+ index++;
+ if( view.model.cid == cid )
+ return view;
+ } );
+ return ( view ) ? index : -1;
+ },
+
+ // Remove a view
+ remove: function(view){
+ var viewCid = view.cid;
+
+ // delete model index
+ if (view.model){
+ delete this._indexByModel[view.model.cid];
+ }
+
+ // delete custom index
+ _.any(this._indexByCustom, function(cid, key) {
+ if (cid === viewCid) {
+ delete this._indexByCustom[key];
+ return true;
+ }
+ }, this);
+
+ // remove the view from the container
+ delete this._views[viewCid];
+
+ // update the length
+ this._updateLength();
+ },
+
+ // Call a method on every view in the container,
+ // passing parameters to the call method one at a
+ // time, like `function.call`.
+ call: function(method){
+ this.apply(method, _.tail(arguments));
+ },
+
+ // Apply a method on every view in the container,
+ // passing parameters to the call method one at a
+ // time, like `function.apply`.
+ apply: function(method, args){
+ _.each(this._views, function(view){
+ if (_.isFunction(view[method])){
+ view[method].apply(view, args || []);
+ }
+ });
+ },
+
+ // Update the `.length` attribute on this container
+ _updateLength: function(){
+ this.length = _.size(this._views);
+ }
+ });
+
+ // Borrowing this code from Backbone.Collection:
+ // http://backbonejs.org/docs/backbone.html#section-106
+ //
+ // Mix in methods from Underscore, for iteration, and other
+ // collection related features.
+ var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
+ 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
+ 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
+ 'last', 'without', 'isEmpty', 'pluck'];
+
+ _.each(methods, function(method) {
+ Container.prototype[method] = function() {
+ var views = _.values(this._views);
+ var args = [views].concat(_.toArray(arguments));
+ return _[method].apply(_, args);
+ };
+ });
+
+ // return the public API
+ return Container;
+ })(Backbone, _);
+})();
diff --git a/ajax/libs/backbone.collectionView/0.9.0/backbone.collectionView.min.js b/ajax/libs/backbone.collectionView/0.9.0/backbone.collectionView.min.js
new file mode 100644
index 000000000..2fb9d8a51
--- /dev/null
+++ b/ajax/libs/backbone.collectionView/0.9.0/backbone.collectionView.min.js
@@ -0,0 +1,8 @@
+/*!
+* Backbone.CollectionView, v0.9.0
+* Copyright (c)2013 Rotunda Software, LLC.
+* Distributed under MIT license
+* http://github.com/rotundasoftware/backbone-collection-view
+*/
+
+(function(){function e(e){var t=[];if(!_.isArray(e))throw Error("Option declarations must be an array.");return _.each(e,function(e){var i,s,n;if(s=!1,n=void 0,_.isString(e))i=e;else{if(!_.isObject(e))throw Error("Each element in the option declarations array must be either a string or an object.");i=_.first(_.keys(e)),n=_.clone(e[i])}"!"===i[i.length-1]&&(s=!0,i=i.slice(0,i.length-1)),t.push({name:i,required:s,defaultValue:n})}),t}var t=Backbone.View,i="model",s=["collection","modelView","modelViewOptions","itemTemplate","selectableModelsFilter","sortableModelsFilter","visibleModelsFilter","itemTemplateFunction","detachedRendering","sortableOptions"],n={background:"transparent",border:"none","box-shadow":"none"};Backbone.CollectionView=Backbone.View.extend({tagName:"ul",events:{"mousedown li, td":"_listItem_onMousedown","dblclick li, td":"_listItem_onDoubleClick",click:"_listBackground_onClick","click ul.collection-list, table.collection-list":"_listBackground_onClick",keydown:"_onKeydown"},spawnMessages:{focus:"focus"},passMessages:{"*":"."},initializationOptions:[{collection:new Backbone.Collection},{modelView:null},{modelViewOptions:{}},{itemTemplate:null},{itemTemplateFunction:null},{selectable:!0},{clickToSelect:!0},{selectableModelsFilter:null},{visibleModelsFilter:null},{sortableModelsFilter:null},{selectMultiple:!1},{clickToToggle:!1},{processKeyEvents:!0},{sortable:!1},{sortableOptions:null},{detachedRendering:!1},{emptyListCaption:null}],initialize:function(e){Backbone.ViewOptions.add(this,"initializationOptions"),this.setOptions(e),this._hasBeenRendered=!1,this._isBackboneCourierAvailable()&&Backbone.Courier.add(this),this.$el.data("view",this),this.$el.addClass("collection-list"),this.selectable&&this.$el.addClass("selectable"),this.processKeyEvents&&this.$el.attr("tabindex",0),this.selectedItems=[],this._updateItemTemplate(),this.collection&&this._registerCollectionEvents(),this.viewManager=new ChildViewContainer},onOptionsChanged:function(e,t){var i=!1,n=this;_.each(_.keys(e),function(o){var l=e[o],a=t[o];switch(o){case"collection":l!==a&&(n.stopListening(a),n._registerCollectionEvents());break;case"selectMultiple":!l&&n.selectedItems.length>1&&n.setSelectedModel(_.first(n.selectedItems),{by:"cid"});break;case"selectable":!l&&n.selectedItems.length>0&&n.setSelectedModels([]);break;case"selectableModelsFilter":l&&_.isFunction(l)&&n._validateSelection();break;case"itemTemplate":n._updateItemTemplate();break;case"processKeyEvents":l&&n.$el.attr("tabindex",0);break;case"modelView":n.viewManager.each(function(e){n.viewManager.remove(e),e.remove()})}_.contains(s,o)&&(i=!0)}),this._hasBeenRendered&&i&&this.render()},setOption:function(e,t){var i={};i[e]=t,this.setOptions(i)},getSelectedModel:function(e){return _.first(this.getSelectedModels(e))},getSelectedModels:function(e){var t=this;e=_.extend({},{by:i},e);var s=e.by,n=[];switch(s){case"id":_.each(this.selectedItems,function(e){n.push(t.collection.get(e).id)});break;case"cid":n=n.concat(this.selectedItems);break;case"offset":var o=0,l=this._getVisibleItemEls();l.each(function(){var e=$(this);e.is(".selected")&&n.push(o),o++});break;case"model":_.each(this.selectedItems,function(e){n.push(t.collection.get(e))});break;case"view":_.each(this.selectedItems,function(e){n.push(t.viewManager.findByModel(t.collection.get(e)))})}return n},setSelectedModels:function(e,t){if(!_.isArray(e))throw"Invalid parameter value";if(this.selectable||!(e.length>0)){t=_.extend({},{silent:!1,by:i},t);var s=t.by,n=[];switch(s){case"cid":n=e;break;case"id":this.collection.each(function(t){_.contains(e,t.id)&&n.push(t.cid)});break;case"model":n=_.pluck(e,"cid");break;case"view":_.each(e,function(e){n.push(e.model.cid)});break;case"offset":var o=0,l=this._getVisibleItemEls();l.each(function(){var t=$(this);_.contains(e,o)&&n.push(t.attr("data-model-cid")),o++})}var a=this.getSelectedModels(),d=_.clone(this.selectedItems);this.selectedItems=this._convertStringsToInts(n),this._validateSelection();var r=this.getSelectedModels();this._containSameElements(d,this.selectedItems)||(this._addSelectedClassToSelectedItems(d),t.silent||(this.trigger("selectionChanged",r,a),this._isBackboneCourierAvailable()&&this.spawn("selectionChanged",{selectedModels:r,oldSelectedModels:a})),this.updateDependentControls())}},setSelectedModel:function(e,t){e||0===e?this.setSelectedModels([e],t):this.setSelectedModels([],t)},render:function(){var e=this;this._hasBeenRendered=!0,this.selectable&&this._saveSelection();var t;t=this._getContainerEl();var i=this.viewManager;this.viewManager=new ChildViewContainer,i.each(function(t){e.collection.get(t.model.cid)?t.$el.detach():t.remove()}),t.empty();var s;if(this.detachedRendering&&(s=document.createDocumentFragment()),this.collection.each(function(e){var n=i.findByModelCid(e.cid);_.isUndefined(n)&&(n=this._createNewModelView(e,this._getModelViewOptions(e))),this._insertAndRenderModelView(n,s||t)},this),this.detachedRendering&&t.append(s),this.sortable){var n=_.extend({axis:"y",distance:10,forcePlaceholderSize:!0,start:_.bind(this._sortStart,this),change:_.bind(this._sortChange,this),stop:_.bind(this._sortStop,this),receive:_.bind(this._receive,this),over:_.bind(this._over,this)},_.result(this,"sortableOptions"));e._isRenderedAsTable()?n.items="> tbody > tr:not(.not-sortable)":e._isRenderedAsList()&&(n.items="> li:not(.not-sortable)"),this.$el=this.$el.sortable(n)}this._showEmptyListCaptionIfAppropriate(),this.trigger("render"),this._isBackboneCourierAvailable()&&this.spawn("render"),this.selectable&&(this._restoreSelection(),this.updateDependentControls()),_.isFunction(this.onAfterRender)&&this.onAfterRender()},_showEmptyListCaptionIfAppropriate:function(){if(this.emptyListCaption){var e=this._getVisibleItemEls();if(0===e.length){var t;t=_.isFunction(this.emptyListCaption)?this.emptyListCaption():this.emptyListCaption;var i=$(""+t+"");$emptyListCaptionEl=this._isRenderedAsList()?i.wrapAll("").parent().css(n):i.wrapAll(" |
").parent().parent().css(n),this._getContainerEl().append($emptyListCaptionEl)}}},_removeEmptyListCaption:function(){this._isRenderedAsList()?this._getContainerEl().find("> li > var.empty-list-caption").parent().remove():this._getContainerEl().find("> tr > td > var.empty-list-caption").parent().parent().remove()},_insertAndRenderModelView:function(e,t,i){var s=this._wrapModelView(e);11===t.nodeType?t.appendChild(s.get(0)):!_.isUndefined(i)&&i>0&&this.collection.length-1>i?t.children().eq(i).before(s):t.append(s);var n=e.render();n===!1&&(s.hide(),s.addClass("not-visible"));var o=!1;_.isFunction(this.visibleModelsFilter)&&(o=!this.visibleModelsFilter(e.model),o&&(1===s.children().length?s.hide():e.$el.hide(),s.addClass("not-visible"))),!o&&this.emptyListCaption&&this._removeEmptyListCaption(),this.viewManager.add(e)},updateDependentControls:function(){this.trigger("updateDependentControls",this.getSelectedModels()),this._isBackboneCourierAvailable()&&this.spawn("updateDependentControls",{selectedModels:this.getSelectedModels()})},remove:function(){this.viewManager.each(function(e){e.remove()}),Backbone.View.prototype.remove.apply(this,arguments)},_removeModelView:function(e){var t=this.viewManager,i=t.findByModelCid(e.cid);this.selectable&&this._saveSelection(),t.remove(i),i.remove(),this._getContainerEl().children("[data-model-cid="+e.cid+"]").remove(),this.selectable&&this._restoreSelection(),this._showEmptyListCaptionIfAppropriate()},_validateSelectionAndRender:function(){this._validateSelection(),this.render()},_registerCollectionEvents:function(){this.listenTo(this.collection,"add",function(e){if(this._hasBeenRendered){var t=this._createNewModelView(e,this._getModelViewOptions(e));this._insertAndRenderModelView(t,this._getContainerEl(),this.collection.indexOf(e))}this._isBackboneCourierAvailable()&&this.spawn("add")}),this.listenTo(this.collection,"remove",function(e){this._hasBeenRendered&&this._removeModelView(e),this._isBackboneCourierAvailable()&&this.spawn("remove")}),this.listenTo(this.collection,"reset",function(){this._hasBeenRendered&&this.render(),this._isBackboneCourierAvailable()&&this.spawn("reset")}),this.listenTo(this.collection,"sort",function(e,t){this._hasBeenRendered&&t.add!==!0&&this.render(),this._isBackboneCourierAvailable()&&this.spawn("sort")})},_getContainerEl:function(){if(this._isRenderedAsTable()){var e=this.$el.find("> tbody");if(e.length>0)return e}return this.$el},_getClickedItemId:function(e){var t=null,i=$(e.currentTarget);if(i.closest(".collection-list").get(0)===this.$el.get(0)){var s=i.closest("[data-model-cid]");return s.length>0&&(t=s.attr("data-model-cid"),$.isNumeric(t)&&(t=parseInt(t,10))),t}},_updateItemTemplate:function(){var e;if(this.itemTemplate){if(0===$(this.itemTemplate).length)throw"Could not find item template from selector: "+this.itemTemplate;e=$(this.itemTemplate).html()}else e=this.$(".item-template").html();e&&(this.itemTemplateFunction=_.template(e))},_validateSelection:function(){var e=_.pluck(this.collection.models,"cid");this.selectedItems=_.intersection(e,this.selectedItems),_.isFunction(this.selectableModelsFilter)&&(this.selectedItems=_.filter(this.selectedItems,function(e){return this.selectableModelsFilter.call(this,this.collection.get(e))},this))},_saveSelection:function(){if(!this.selectable)throw"Attempt to save selection on non-selectable list";this.savedSelection={items:_.clone(this.selectedItems),offset:this.getSelectedModel({by:"offset"})}},_restoreSelection:function(){if(!this.savedSelection)throw"Attempt to restore selection but no selection has been saved!";this.setSelectedModels([],{silent:!0}),this.savedSelection.items.length>0&&(this.setSelectedModels(this.savedSelection.items,{by:"cid",silent:!0}),0===this.selectedItems.length&&this.setSelectedModel(this.savedSelection.offset,{by:"offset"}),this.selectedItems.length!==this.savedSelection.items.length&&(this.trigger("selectionChanged",this.getSelectedModels(),[]),this._isBackboneCourierAvailable()&&this.spawn("selectionChanged",{selectedModels:this.getSelectedModels(),oldSelectedModels:[]}))),delete this.savedSelection},_addSelectedClassToSelectedItems:function(e){_.isUndefined(e)&&(e=[]);var t=e;t=_.without(t,this.selectedItems),_.each(t,function(e){this._getContainerEl().find("[data-model-cid="+e+"]").removeClass("selected")},this);var i=this.selectedItems;i=_.without(i,e),_.each(i,function(e){this._getContainerEl().find("[data-model-cid="+e+"]").addClass("selected")},this)},_reorderCollectionBasedOnHTML:function(){var e=this;this._getContainerEl().children().each(function(){var t=$(this).attr("data-model-cid");if(t){var i=e.collection.get(t);i&&(e.collection.remove(i,{silent:!0}),e.collection.add(i,{silent:!0,sort:!e.collection.comparator}))}}),this.collection.trigger("reorder"),this._isBackboneCourierAvailable()&&this.spawn("reorder"),this.collection.comparator&&this.collection.sort()},_getModelViewConstructor:function(){return this.modelView||t},_getModelViewOptions:function(e){return _.extend({model:e},this.modelViewOptions)},_createNewModelView:function(e,t){var i=this._getModelViewConstructor(e);if(_.isUndefined(i))throw"Could not find modelView constructor for model";var s=new i(t);return s.collectionListView=this,s},_wrapModelView:function(e){var t,i=this;return this._isRenderedAsTable()?t=e.$el.attr("data-model-cid",e.model.cid):this._isRenderedAsList()&&(t="li"===e.$el.prop("tagName").toLowerCase()?e.$el.attr("data-model-cid",e.model.cid):e.$el.wrapAll("").parent()),_.isFunction(this.sortableModelsFilter)&&(this.sortableModelsFilter.call(i,e.model)||t.addClass("not-sortable")),_.isFunction(this.selectableModelsFilter)&&(this.selectableModelsFilter.call(i,e.model)||t.addClass("not-selectable")),t},_convertStringsToInts:function(e){return _.map(e,function(e){if(!_.isString(e))return e;var t=parseInt(e,10);return t==e?t:e})},_containSameElements:function(e,t){if(e.length!=t.length)return!1;var i=_.intersection(e,t).length;return i==e.length},_isRenderedAsTable:function(){return"table"===this.$el.prop("tagName").toLowerCase()},_isRenderedAsList:function(){return!this._isRenderedAsTable()},_getVisibleItemEls:function(){var e=[];return e=this._getContainerEl().find("> [data-model-cid]:not(.not-visible)")},_charCodes:{upArrow:38,downArrow:40},_isBackboneCourierAvailable:function(){return!_.isUndefined(Backbone.Courier)},_sortStart:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid"));this.trigger("sortStart",i),this._isBackboneCourierAvailable()&&this.spawn("sortStart",{modelBeingSorted:i})},_sortChange:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid"));this.trigger("sortChange",i),this._isBackboneCourierAvailable()&&this.spawn("sortChange",{modelBeingSorted:i})},_sortStop:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid")),s=this._getContainerEl(),n=s.children().index(t.item);-1==n&&this.collection.remove(i),this._reorderCollectionBasedOnHTML(),this.updateDependentControls(),this.trigger("sortStop",i,n),this._isBackboneCourierAvailable()&&this.spawn("sortStop",{modelBeingSorted:i,newIndex:n})},_receive:function(e,t){var i=t.sender,s=i.data("view");if(s&&s.collection){var n=this._getContainerEl().children().index(t.item),o=s.collection.get(t.item.attr("data-model-cid"));s.collection.remove(o),this.collection.add(o,{at:n}),o.collection=this.collection,this.setSelectedModel(o)}},_over:function(){this._getContainerEl().find("> var.empty-list-caption").hide()},_onKeydown:function(e){if(!this.processKeyEvents)return!0;var t=!1;if(1==this.getSelectedModels({by:"offset"}).length){var i=this.getSelectedModel({by:"offset"});e.which===this._charCodes.upArrow&&0!==i?(this.setSelectedModel(i-1,{by:"offset"}),t=!0):e.which===this._charCodes.downArrow&&i!==this.collection.length-1&&(this.setSelectedModel(i+1,{by:"offset"}),t=!0)}return!t},_listItem_onMousedown:function(e){if(this.selectable&&this.clickToSelect){var t=this._getClickedItemId(e);if(t){if(_.isFunction(this.selectableModelsFilter)&&!this.selectableModelsFilter.call(this,this.collection.get(t)))return;if(this.selectMultiple&&e.shiftKey){var i=-1;this.selectedItems.length>0&&this.collection.find(function(e){return i++,_.contains(this.selectedItems,e.cid)},this);var s=-1;this.collection.find(function(e){return s++,e.cid==t},this);for(var n=-1==i?s:i,o=Math.min(s,n),l=Math.max(s,n),a=[],d=o;l>=d;d++)a.push(this.collection.at(d).cid);if(this.setSelectedModels(a,{by:"cid"}),document.selection&&document.selection.empty)document.selection.empty();else if(window.getSelection){var r=window.getSelection();r&&r.removeAllRanges&&r.removeAllRanges()}}else this.selectMultiple&&(this.clickToToggle||e.metaKey)?_.contains(this.selectedItems,t)?this.setSelectedModels(_.without(this.selectedItems,t),{by:"cid"}):this.setSelectedModels(_.union(this.selectedItems,t),{by:"cid"}):this.setSelectedModels([t],{by:"cid"})}else this.setSelectedModels([])}},_listItem_onDoubleClick:function(e){var t=this._getClickedItemId(e);if(t){var i=this.collection.get(t);this.trigger("doubleClick",i),this._isBackboneCourierAvailable()&&this.spawn("doubleClick",{clickedModel:i})}},_listBackground_onClick:function(e){this.selectable&&$(e.target).is(".collection-list")&&this.setSelectedModels([])}},{setDefaultModelViewConstructor:function(e){t=e}}),Backbone.ViewOptions={},Backbone.ViewOptions.add=function(t,i){_.isUndefined(i)&&(i="options"),t.setOptions=function(t){var s=this,n={},o={},l=_.result(this,i);if(!_.isUndefined(l)){var a=e(l);_.each(a,function(e){if(thisOptionName=e.name,thisOptionRequired=e.required,thisOptionDefaultValue=e.defaultValue,thisOptionRequired&&(!t||!_.contains(_.keys(t),thisOptionName)&&_.isUndefined(s[thisOptionName])||_.isUndefined(t[thisOptionName])))throw Error('Required option "'+thisOptionName+'" was not supplied.');t&&thisOptionName in t?(_.isUndefined(s[thisOptionName])||(o[thisOptionName]=s[thisOptionName],n[thisOptionName]=t[thisOptionName]),s[thisOptionName]=t[thisOptionName]):!_.isUndefined(thisOptionDefaultValue)&&_.isUndefined(s[thisOptionName])&&(s[thisOptionName]=thisOptionDefaultValue)})}_.keys(n).length>0&&(_.isFunction(s.onOptionsChanged)?s.onOptionsChanged(n,o):_.isFunction(s._onOptionsChanged)&&s._onOptionsChanged(n,o))},t.getOptions=function(){var t=_.result(this,i);if(_.isUndefined(t))return[];var s=e(t),n=_.pluck(s,"name");return _.pick(this,n)}},ChildViewContainer=function(e,t){var i=function(e){this._views={},this._indexByModel={},this._indexByCustom={},this._updateLength(),t.each(e,this.add,this)};t.extend(i.prototype,{add:function(e,t){var i=e.cid;this._views[i]=e,e.model&&(this._indexByModel[e.model.cid]=i),t&&(this._indexByCustom[t]=i),this._updateLength()},findByModel:function(e){return this.findByModelCid(e.cid)},findByModelCid:function(e){var t=this._indexByModel[e];return this.findByCid(t)},findByCustom:function(e){var t=this._indexByCustom[e];return this.findByCid(t)},findByIndex:function(e){return t.values(this._views)[e]},findByCid:function(e){return this._views[e]},findIndexByCid:function(e){var i=-1,s=t.find(this._views,function(t){return i++,t.model.cid==e?t:void 0});return s?i:-1},remove:function(e){var i=e.cid;e.model&&delete this._indexByModel[e.model.cid],t.any(this._indexByCustom,function(e,t){return e===i?(delete this._indexByCustom[t],!0):void 0},this),delete this._views[i],this._updateLength()},call:function(e){this.apply(e,t.tail(arguments))},apply:function(e,i){t.each(this._views,function(s){t.isFunction(s[e])&&s[e].apply(s,i||[])})},_updateLength:function(){this.length=t.size(this._views)}});var s=["forEach","each","map","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","toArray","first","initial","rest","last","without","isEmpty","pluck"];return t.each(s,function(e){i.prototype[e]=function(){var i=t.values(this._views),s=[i].concat(t.toArray(arguments));return t[e].apply(t,s)}}),i}(Backbone,_)})();
\ No newline at end of file
diff --git a/ajax/libs/backbone.collectionView/0.9.1/backbone.collectionView.js b/ajax/libs/backbone.collectionView/0.9.1/backbone.collectionView.js
new file mode 100644
index 000000000..1024b6c27
--- /dev/null
+++ b/ajax/libs/backbone.collectionView/0.9.1/backbone.collectionView.js
@@ -0,0 +1,1228 @@
+/*!
+* Backbone.CollectionView, v0.9.0
+* Copyright (c)2013 Rotunda Software, LLC.
+* Distributed under MIT license
+* http://github.com/rotundasoftware/backbone-collection-view
+*/
+
+(function() {
+ var mDefaultModelViewConstructor = Backbone.View;
+
+ var kDefaultReferenceBy = "model";
+
+ var kOptionsRequiringRerendering = [ "collection", "modelView", "modelViewOptions", "itemTemplate", "selectableModelsFilter", "sortableModelsFilter", "visibleModelsFilter", "itemTemplateFunction", "detachedRendering", "sortableOptions" ];
+
+ var kStylesForEmptyListCaption = {
+ "background" : "transparent",
+ "border" : "none",
+ "box-shadow" : "none"
+ };
+
+ Backbone.CollectionView = Backbone.View.extend( {
+
+ tagName : "ul",
+
+ events : {
+ "mousedown li, td" : "_listItem_onMousedown",
+ "dblclick li, td" : "_listItem_onDoubleClick",
+ "click" : "_listBackground_onClick",
+ "click ul.collection-list, table.collection-list" : "_listBackground_onClick",
+ "keydown" : "_onKeydown"
+ },
+
+ // only used if Backbone.Courier is available
+ spawnMessages : {
+ "focus" : "focus"
+ },
+
+ //only used if Backbone.Courier is available
+ passMessages : { "*" : "." },
+
+ // viewOption definitions with default values.
+ initializationOptions : [ { "collection" : new Backbone.Collection() },
+ { "modelView" : null },
+ { "modelViewOptions" : {} },
+ { "itemTemplate" : null },
+ { "itemTemplateFunction" : null },
+ { "selectable" : true },
+ { "clickToSelect" : true },
+ { "selectableModelsFilter" : null },
+ { "visibleModelsFilter" : null },
+ { "sortableModelsFilter" : null },
+ { "selectMultiple" : false },
+ { "clickToToggle" : false },
+ { "processKeyEvents" : true },
+ { "sortable" : false },
+ { "sortableOptions" : null },
+ { "detachedRendering" : false },
+ { "emptyListCaption" : null }
+ ],
+
+ initialize : function( options ) {
+ Backbone.ViewOptions.add( this, "initializationOptions" ); // setup the ViewOptions functionality.
+ this.setOptions( options ); // and make use of any provided options
+
+ this._hasBeenRendered = false;
+
+ if( this._isBackboneCourierAvailable() ) {
+ Backbone.Courier.add( this );
+ }
+
+ this.$el.data( "view", this ); // needed for connected sortable lists
+ this.$el.addClass( "collection-list" );
+ if( this.selectable ) this.$el.addClass( "selectable" );
+
+ if( this.processKeyEvents )
+ this.$el.attr( "tabindex", 0 ); // so we get keyboard events
+
+ this.selectedItems = [];
+
+ this._updateItemTemplate();
+
+ if( this.collection )
+ this._registerCollectionEvents();
+
+ this.viewManager = new ChildViewContainer();
+ },
+
+ onOptionsChanged : function( changedOptions, originalOptions ) {
+ var rerender = false;
+ var _this = this;
+ _.each( _.keys( changedOptions ), function( changedOptionKey ) {
+ var newVal = changedOptions[ changedOptionKey ];
+ var oldVal = originalOptions[ changedOptionKey ];
+ switch( changedOptionKey ) {
+ case "collection" :
+ if ( newVal !== oldVal ) {
+ _this.stopListening( oldVal );
+ _this._registerCollectionEvents();
+ }
+ break;
+ case "selectMultiple":
+ if( ! newVal && _this.selectedItems.length > 1 )
+ _this.setSelectedModel( _.first( _this.selectedItems ), { by : "cid" } );
+ break;
+ case "selectable" :
+ if( ! newVal && _this.selectedItems.length > 0 )
+ _this.setSelectedModels( [] );
+ break;
+ case "selectableModelsFilter" :
+ if( newVal && _.isFunction( newVal ) )
+ _this._validateSelection();
+ break;
+ case "itemTemplate" :
+ _this._updateItemTemplate();
+ break;
+ case "processKeyEvents" :
+ if( newVal ) _this.$el.attr( "tabindex", 0 ); // so we get keyboard events
+ break;
+ case "modelView" :
+ //need to remove all old view instances
+ _this.viewManager.each( function( view ) {
+ _this.viewManager.remove( view );
+ // destroy the View itself
+ view.remove();
+ } );
+ break;
+ }
+ if( _.contains( kOptionsRequiringRerendering, changedOptionKey ) ) rerender = true;
+ });
+ if( this._hasBeenRendered && rerender ) {
+ this.render(); // Rerender the view if the rerender flag has been set.
+ }
+ },
+
+ setOption : function( optionName, optionValue ) { // now is mearly a wrapper around backbone.viewOptions' setOptions()
+ var optionHash = {};
+ optionHash[ optionName ] = optionValue;
+ this.setOptions( optionHash );
+ },
+
+ getSelectedModel : function( options ) {
+ return _.first( this.getSelectedModels( options ) );
+ },
+
+ getSelectedModels : function ( options ) {
+ var _this = this;
+
+ options = _.extend( {}, {
+ by : kDefaultReferenceBy
+ }, options );
+
+ var referenceBy = options.by;
+ var items = [];
+
+ switch( referenceBy ) {
+ case "id" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.collection.get( item ).id );
+ } );
+ break;
+ case "cid" :
+ items = items.concat( this.selectedItems );
+ break;
+ case "offset" :
+ var curLineNumber = 0;
+
+ var itemElements = this._getVisibleItemEls();
+
+ itemElements.each( function() {
+ var thisItemEl = $( this );
+ if( thisItemEl.is( ".selected" ) )
+ items.push( curLineNumber );
+ curLineNumber++;
+ } );
+ break;
+ case "model" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.collection.get( item ) );
+ } );
+ break;
+ case "view" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.viewManager.findByModel( _this.collection.get( item ) ) );
+ } );
+ break;
+ }
+
+ return items;
+
+ },
+
+ setSelectedModels : function( newSelectedItems, options ) {
+ if( ! _.isArray( newSelectedItems ) ) throw "Invalid parameter value";
+ if( ! this.selectable && newSelectedItems.length > 0 ) return; // used to throw error, but there are some circumstances in which a list can be selectable at times and not at others, don't want to have to worry about catching errors
+
+ options = _.extend( {}, {
+ silent : false,
+ by : kDefaultReferenceBy
+ }, options );
+
+ var referenceBy = options.by;
+ var newSelectedCids = [];
+
+ switch( referenceBy ) {
+ case "cid" :
+ newSelectedCids = newSelectedItems;
+ break;
+ case "id" :
+ this.collection.each( function( thisModel ) {
+ if( _.contains( newSelectedItems, thisModel.id ) ) newSelectedCids.push( thisModel.cid );
+ } );
+ break;
+ case "model" :
+ newSelectedCids = _.pluck( newSelectedItems, "cid" );
+ break;
+ case "view" :
+ _.each( newSelectedItems, function( item ) {
+ newSelectedCids.push( item.model.cid );
+ } );
+ break;
+ case "offset" :
+ var curLineNumber = 0;
+ var selectedItems = [];
+
+ var itemElements = this._getVisibleItemEls();
+ itemElements.each( function() {
+ var thisItemEl = $( this );
+ if( _.contains( newSelectedItems, curLineNumber ) )
+ newSelectedCids.push( thisItemEl.attr( "data-model-cid" ) );
+ curLineNumber++;
+ } );
+ break;
+ }
+
+ var oldSelectedModels = this.getSelectedModels();
+ var oldSelectedCids = _.clone( this.selectedItems );
+
+ this.selectedItems = this._convertStringsToInts( newSelectedCids );
+ this._validateSelection();
+
+ var newSelectedModels = this.getSelectedModels();
+
+ if( ! this._containSameElements( oldSelectedCids, this.selectedItems ) )
+ {
+ this._addSelectedClassToSelectedItems( oldSelectedCids );
+
+ if( ! options.silent )
+ {
+ this.trigger( "selectionChanged", newSelectedModels, oldSelectedModels );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "selectionChanged", {
+ selectedModels : newSelectedModels,
+ oldSelectedModels : oldSelectedModels
+ } );
+ }
+ }
+
+ this.updateDependentControls();
+ }
+ },
+
+ setSelectedModel : function( newSelectedItem, options ) {
+ if( ! newSelectedItem && newSelectedItem !== 0 )
+ this.setSelectedModels( [], options );
+ else
+ this.setSelectedModels( [ newSelectedItem ], options );
+ },
+
+ render : function(){
+ var _this = this;
+
+ this._hasBeenRendered = true;
+
+ if( this.selectable ) this._saveSelection();
+
+ var modelViewContainerEl;
+
+ // If collection view element is a table and it has a tbody
+ // within it, render the model views inside of the tbody
+ modelViewContainerEl = this._getContainerEl();
+
+ var oldViewManager = this.viewManager;
+ this.viewManager = new ChildViewContainer();
+
+ // detach each of our subviews that we have already created to represent models
+ // in the collection. We are going to re-use the ones that represent models that
+ // are still here, instead of creating new ones, so that we don't loose state
+ // information in the views.
+ oldViewManager.each( function( thisModelView ) {
+ // to boost performance, only detach those views that will be sticking around.
+ // we won't need the other ones later, so no need to detach them individually.
+ if( _this.collection.get( thisModelView.model.cid ) )
+ thisModelView.$el.detach();
+ else
+ thisModelView.remove();
+ } );
+
+ modelViewContainerEl.empty();
+ var fragmentContainer;
+
+ if( this.detachedRendering )
+ fragmentContainer = document.createDocumentFragment();
+
+ this.collection.each( function( thisModel ) {
+ var thisModelView = oldViewManager.findByModelCid( thisModel.cid );
+ if( _.isUndefined( thisModelView ) ) {
+ // if the model view has not already been created on a
+ // previous render then create and initialize it now.
+ thisModelView = this._createNewModelView( thisModel, this._getModelViewOptions( thisModel ) );
+ }
+
+ this._insertAndRenderModelView( thisModelView, fragmentContainer || modelViewContainerEl );
+ }, this );
+
+ if( this.detachedRendering )
+ modelViewContainerEl.append( fragmentContainer );
+
+ if( this.sortable )
+ {
+ var sortableOptions = _.extend( {
+ axis: "y",
+ distance: 10,
+ forcePlaceholderSize : true,
+ start : _.bind( this._sortStart, this ),
+ change : _.bind( this._sortChange, this ),
+ stop : _.bind( this._sortStop, this ),
+ receive : _.bind( this._receive, this ),
+ over : _.bind( this._over, this )
+ }, _.result( this, "sortableOptions" ) );
+
+ if( _this._isRenderedAsTable() ) {
+ sortableOptions.items = "> tbody > tr:not(.not-sortable)";
+ }
+ else if( _this._isRenderedAsList() ) {
+ sortableOptions.items = "> li:not(.not-sortable)";
+ }
+
+ this.$el = this.$el.sortable( sortableOptions );
+ }
+
+ this._showEmptyListCaptionIfAppropriate();
+
+ this.trigger( "render" );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "render" );
+
+ if( this.selectable ) {
+ this._restoreSelection();
+ this.updateDependentControls();
+ }
+
+ if( _.isFunction( this.onAfterRender ) )
+ this.onAfterRender();
+ },
+
+ _showEmptyListCaptionIfAppropriate : function ( ) {
+ if( this.emptyListCaption ) {
+ var visibleEls = this._getVisibleItemEls();
+
+ if( visibleEls.length === 0 ) {
+ var emptyListString;
+
+ if( _.isFunction( this.emptyListCaption ) )
+ emptyListString = this.emptyListCaption();
+ else
+ emptyListString = this.emptyListCaption;
+
+ var $emptyCaptionEl;
+ var $varEl = $( "" + emptyListString + "" );
+
+ //need to wrap the empty caption to make it fit the rendered list structure (either with an li or a tr td)
+ if( this._isRenderedAsList() )
+ $emptyListCaptionEl = $varEl.wrapAll( "" ).parent().css( kStylesForEmptyListCaption );
+ else
+ $emptyListCaptionEl = $varEl.wrapAll( " |
" ).parent().parent().css( kStylesForEmptyListCaption );
+
+ this._getContainerEl().append( $emptyListCaptionEl );
+ }
+ }
+ },
+
+ _removeEmptyListCaption : function( ) {
+ if( this._isRenderedAsList() )
+ this._getContainerEl().find( "> li > var.empty-list-caption" ).parent().remove();
+ else
+ this._getContainerEl().find( "> tr > td > var.empty-list-caption" ).parent().parent().remove();
+ },
+
+ // Render a single model view in container object "parentElOrDocumentFragment", which is either
+ // a documentFragment or a jquery object. optional arg atIndex is not support for document fragments.
+ _insertAndRenderModelView : function( modelView, parentElOrDocumentFragment, atIndex ) {
+ var thisModelViewWrapped = this._wrapModelView( modelView );
+
+ if( parentElOrDocumentFragment.nodeType === 11 ) // if we are inserting into a document fragment, we need to use the DOM appendChild method
+ parentElOrDocumentFragment.appendChild( thisModelViewWrapped.get( 0 ) );
+ else if( ! _.isUndefined( atIndex ) && atIndex > 0 && atIndex < this.collection.length - 1 )
+ parentElOrDocumentFragment.children().eq( atIndex ).before( thisModelViewWrapped );
+ else
+ parentElOrDocumentFragment.append( thisModelViewWrapped );
+
+ // we have to render the modelView after it has been put in context, as opposed to in the
+ // initialize function of the modelView, because some rendering might be dependent on
+ // the modelView's context in the DOM tree. For example, if the modelView stretch()'s itself,
+ // it must be in full context in the DOM tree or else the stretch will not behave as intended.
+ var renderResult = modelView.render();
+
+ // return false from the view's render function to hide this item
+ if( renderResult === false ) {
+ thisModelViewWrapped.hide();
+ thisModelViewWrapped.addClass( "not-visible" );
+ }
+
+ var hideThisModelView = false;
+ if( _.isFunction( this.visibleModelsFilter ) ) {
+ hideThisModelView = ! this.visibleModelsFilter( modelView.model );
+ if( hideThisModelView ) {
+ if( thisModelViewWrapped.children().length === 1 )
+ thisModelViewWrapped.hide();
+ else modelView.$el.hide();
+
+ thisModelViewWrapped.addClass( "not-visible" );
+ }
+ }
+
+ if( ! hideThisModelView && this.emptyListCaption ) this._removeEmptyListCaption();
+
+ this.viewManager.add( modelView );
+ },
+
+ updateDependentControls : function() {
+ this.trigger( "updateDependentControls", this.getSelectedModels() );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "updateDependentControls", {
+ selectedModels : this.getSelectedModels()
+ } );
+ }
+ },
+
+ // Override `Backbone.View.remove` to also destroy all Views in `viewManager`
+ remove : function() {
+ this.viewManager.each( function( view ) {
+ view.remove();
+ } );
+
+ Backbone.View.prototype.remove.apply( this, arguments );
+ },
+
+ // A method to remove the view relating to model.
+ _removeModelView : function( model ) {
+ var viewManager = this.viewManager;
+ var view = viewManager.findByModelCid( model.cid );
+
+ if ( this.selectable ) this._saveSelection();
+
+ viewManager.remove( view ); // Remove the view from the viewManager
+ view.remove(); // Remove the view from the DOM
+ this._getContainerEl().children( "[data-model-cid=" + model.cid + "]" ).remove(); // Remove the wrapper from the DOM
+
+ if ( this.selectable ) this._restoreSelection();
+
+ this._showEmptyListCaptionIfAppropriate();
+ },
+
+ _validateSelectionAndRender : function() {
+ this._validateSelection();
+ this.render();
+ },
+
+ _registerCollectionEvents : function() {
+ this.listenTo( this.collection, "add", function( model ) {
+ if( this._hasBeenRendered ) {
+ var modelView = this._createNewModelView( model, this._getModelViewOptions( model ) );
+ this._insertAndRenderModelView( modelView, this._getContainerEl(), this.collection.indexOf( model ) );
+ }
+
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "add" );
+ } );
+
+ this.listenTo( this.collection, "remove", function( model ) {
+ if( this._hasBeenRendered )
+ this._removeModelView( model );
+
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "remove" );
+ } );
+
+ this.listenTo( this.collection, "reset", function() {
+ if( this._hasBeenRendered ) this.render();
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "reset" );
+ } );
+
+ // we should not be listening to change events on the model as a default behavior. the models
+ // should be responsible for re-rendering themselves if necessary, and if the collection does
+ // also need to re-render as a result of a model change, this should be handled by overriding
+ // this method. by default the collection view should not re-render in response to model changes
+ // this.listenTo( this.collection, "change", function( model ) {
+ // if( this._hasBeenRendered ) this.viewManager.findByModel( model ).render();
+ // if( this._isBackboneCourierAvailable() )
+ // this.spawn( "change", { model : model } );
+ // } );
+
+ this.listenTo( this.collection, "sort", function( collection, options ) {
+ if( this._hasBeenRendered && options.add !== true ) this.render();
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sort" );
+ } );
+ },
+
+ _getContainerEl : function() {
+ if ( this._isRenderedAsTable() ) {
+ // not all tables have a tbody, so we test
+ var tbody = this.$el.find( "> tbody" );
+ if ( tbody.length > 0 )
+ return tbody;
+ }
+ return this.$el;
+ },
+
+ _getClickedItemId : function( theEvent ) {
+ var clickedItemId = null;
+
+ // important to use currentTarget as opposed to target, since we could be bubbling
+ // an event that took place within another collectionList
+ var clickedItemEl = $( theEvent.currentTarget );
+ if( clickedItemEl.closest( ".collection-list" ).get(0) !== this.$el.get(0) ) return;
+
+ // determine which list item was clicked. If we clicked in the blank area
+ // underneath all the elements, we want to know that too, since in this
+ // case we will want to deselect all elements. so check to see if the clicked
+ // DOM element is the list itself to find that out.
+ var clickedItem = clickedItemEl.closest( "[data-model-cid]" );
+ if( clickedItem.length > 0 )
+ {
+ clickedItemId = clickedItem.attr( "data-model-cid" );
+ if( $.isNumeric( clickedItemId ) ) clickedItemId = parseInt( clickedItemId, 10 );
+ }
+
+ return clickedItemId;
+ },
+
+ _updateItemTemplate : function() {
+ var itemTemplateHtml;
+ if( this.itemTemplate )
+ {
+ if( $( this.itemTemplate ).length === 0 )
+ throw "Could not find item template from selector: " + this.itemTemplate;
+
+ itemTemplateHtml = $( this.itemTemplate ).html();
+ }
+ else
+ itemTemplateHtml = this.$( ".item-template" ).html();
+
+ if( itemTemplateHtml ) this.itemTemplateFunction = _.template( itemTemplateHtml );
+
+ },
+
+ _validateSelection : function() {
+ // note can't use the collection's proxy to underscore because "cid" is not an attribute,
+ // but an element of the model object itself.
+ var modelReferenceIds = _.pluck( this.collection.models, "cid" );
+ this.selectedItems = _.intersection( modelReferenceIds, this.selectedItems );
+
+ if( _.isFunction( this.selectableModelsFilter ) )
+ {
+ this.selectedItems = _.filter( this.selectedItems, function( thisItemId ) {
+ return this.selectableModelsFilter.call( this, this.collection.get( thisItemId ) );
+ }, this );
+ }
+ },
+
+ _saveSelection : function() {
+ // save the current selection. use restoreSelection() to restore the selection to the state it was in the last time saveSelection() was called.
+ if( ! this.selectable ) throw "Attempt to save selection on non-selectable list";
+ this.savedSelection = {
+ items : _.clone( this.selectedItems ),
+ offset : this.getSelectedModel( { by : "offset" } )
+ };
+ },
+
+ _restoreSelection : function() {
+ if( ! this.savedSelection ) throw "Attempt to restore selection but no selection has been saved!";
+
+ // reset selectedItems to empty so that we "redraw" all "selected" classes
+ // when we set our new selection. We do this because it is likely that our
+ // contents have been refreshed, and we have thus lost all old "selected" classes.
+ this.setSelectedModels( [], { silent : true } );
+
+ if( this.savedSelection.items.length > 0 )
+ {
+ // first try to restore the old selected items using their reference ids.
+ this.setSelectedModels( this.savedSelection.items, { by : "cid", silent : true } );
+
+ // all the items with the saved reference ids have been removed from the list.
+ // ok. try to restore the selection based on the offset that used to be selected.
+ // this is the expected behavior after a item is deleted from a list (i.e. select
+ // the line that immediately follows the deleted line).
+ if( this.selectedItems.length === 0 )
+ this.setSelectedModel( this.savedSelection.offset, { by : "offset" } );
+
+ // Trigger a selection changed if the previously selected items were not all found
+ if (this.selectedItems.length !== this.savedSelection.items.length)
+ {
+ this.trigger( "selectionChanged", this.getSelectedModels(), [] );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "selectionChanged", {
+ selectedModels : this.getSelectedModels(),
+ oldSelectedModels : []
+ } );
+ }
+ }
+ }
+
+ delete this.savedSelection;
+ },
+
+ _addSelectedClassToSelectedItems : function( oldItemsIdsWithSelectedClass ) {
+ if( _.isUndefined( oldItemsIdsWithSelectedClass ) ) oldItemsIdsWithSelectedClass = [];
+
+ // oldItemsIdsWithSelectedClass is used for optimization purposes only. If this info is supplied then we
+ // only have to add / remove the "selected" class from those items that "selected" state has changed.
+
+ var itemsIdsFromWhichSelectedClassNeedsToBeRemoved = oldItemsIdsWithSelectedClass;
+ itemsIdsFromWhichSelectedClassNeedsToBeRemoved = _.without( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, this.selectedItems );
+
+ _.each( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, function( thisItemId ) {
+ this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).removeClass( "selected" );
+ }, this );
+
+ var itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems;
+ itemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass );
+
+ _.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) {
+ this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).addClass( "selected" );
+ }, this );
+ },
+
+ _reorderCollectionBasedOnHTML : function() {
+ var _this = this;
+
+ this._getContainerEl().children().each( function() {
+ var thisModelCid = $( this ).attr( "data-model-cid" );
+
+ if( thisModelCid )
+ {
+ // remove the current model and then add it back (at the end of the collection).
+ // When we are done looping through all models, they will be in the correct order.
+ var thisModel = _this.collection.get( thisModelCid );
+ if( thisModel )
+ {
+ _this.collection.remove( thisModel, { silent : true } );
+ _this.collection.add( thisModel, { silent : true, sort : ! _this.collection.comparator } );
+ }
+ }
+ } );
+
+ this.collection.trigger( "reorder" );
+
+ if( this._isBackboneCourierAvailable() ) this.spawn( "reorder" );
+
+ if( this.collection.comparator ) this.collection.sort();
+
+ },
+
+ _getModelViewConstructor : function( thisModel ) {
+ return this.modelView || mDefaultModelViewConstructor;
+ },
+
+ _getModelViewOptions : function( thisModel ) {
+ return _.extend( { model : thisModel }, this.modelViewOptions );
+ },
+
+ _createNewModelView : function( model, modelViewOptions ) {
+ var modelViewConstructor = this._getModelViewConstructor( model );
+ if( _.isUndefined( modelViewConstructor ) ) throw "Could not find modelView constructor for model";
+
+ var newModelView = new( modelViewConstructor )( modelViewOptions );
+ newModelView.collectionListView = this;
+
+ return newModelView;
+ },
+
+ _wrapModelView : function( modelView ) {
+ var _this = this;
+
+ // we use items client ids as opposed to real ids, since we may not have a representation
+ // of these models on the server
+ var wrappedModelView;
+
+ if( this._isRenderedAsTable() ) {
+ // if we are rendering the collection in a table, the template $el is a tr so we just need to set the data-model-cid
+ wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid );
+ }
+ else if( this._isRenderedAsList() ) {
+ // if we are rendering the collection in a list, we need wrap each item in an (if its not already an )
+ // and set the data-model-cid
+ if( modelView.$el.prop( "tagName" ).toLowerCase() === "li" ) {
+ wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid );
+ } else {
+ wrappedModelView = modelView.$el.wrapAll( "" ).parent();
+ }
+ }
+
+ if( _.isFunction( this.sortableModelsFilter ) )
+ if( ! this.sortableModelsFilter.call( _this, modelView.model ) )
+ wrappedModelView.addClass( "not-sortable" );
+
+ if( _.isFunction( this.selectableModelsFilter ) )
+ if( ! this.selectableModelsFilter.call( _this, modelView.model ) )
+ wrappedModelView.addClass( "not-selectable" );
+
+ return wrappedModelView;
+ },
+
+ _convertStringsToInts : function( theArray ) {
+ return _.map( theArray, function( thisEl ) {
+ if( ! _.isString( thisEl ) ) return thisEl;
+ var thisElAsNumber = parseInt( thisEl, 10 );
+ return( thisElAsNumber == thisEl ? thisElAsNumber : thisEl );
+ } );
+ },
+
+ _containSameElements : function( arrayA, arrayB ) {
+ if( arrayA.length != arrayB.length ) return false;
+ var intersectionSize = _.intersection( arrayA, arrayB ).length;
+ return intersectionSize == arrayA.length; // and must also equal arrayB.length, since arrayA.length == arrayB.length
+ },
+
+ _isRenderedAsTable : function() {
+ return this.$el.prop( "tagName" ).toLowerCase() === "table";
+ },
+
+ _isRenderedAsList : function() {
+ return ! this._isRenderedAsTable();
+ },
+
+ // Returns the wrapper HTML element for each visible modelView.
+ // When rendering in a table context, the returned elements are the $el of each modelView.
+ // When rendering in a list context,
+ // If the $el of the modelView is an , the returned elements are the $el of each modelView.
+ // Otherwise, the returned elements are the 's the collectionView wrapped around each modelView $el.
+ _getVisibleItemEls : function() {
+ var itemElements = [];
+ itemElements = this._getContainerEl().find( "> [data-model-cid]:not(.not-visible)" );
+
+ return itemElements;
+ },
+
+ _charCodes : {
+ upArrow : 38,
+ downArrow : 40
+ },
+
+ _isBackboneCourierAvailable : function() {
+ return !_.isUndefined( Backbone.Courier );
+ },
+
+ _sortStart : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ this.trigger( "sortStart", modelBeingSorted );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortStart", { modelBeingSorted : modelBeingSorted } );
+ },
+
+ _sortChange : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ this.trigger( "sortChange", modelBeingSorted );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortChange", { modelBeingSorted : modelBeingSorted } );
+ },
+
+ _sortStop : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ var modelViewContainerEl = this._getContainerEl();
+ var newIndex = modelViewContainerEl.children().index( ui.item );
+
+ if( newIndex == -1 ) {
+ // the element was removed from this list. can happen if this sortable is connected
+ // to another sortable, and the item was dropped into the other sortable.
+ this.collection.remove( modelBeingSorted );
+ }
+
+ this._reorderCollectionBasedOnHTML();
+ this.updateDependentControls();
+ this.trigger( "sortStop", modelBeingSorted, newIndex );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortStop", { modelBeingSorted : modelBeingSorted, newIndex : newIndex } );
+ },
+
+ _receive : function( event, ui ) {
+ var senderListEl = ui.sender;
+ var senderCollectionListView = senderListEl.data( "view" );
+ if( ! senderCollectionListView || ! senderCollectionListView.collection ) return;
+
+ var newIndex = this._getContainerEl().children().index( ui.item );
+ var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) );
+ senderCollectionListView.collection.remove( modelReceived );
+ this.collection.add( modelReceived, { at : newIndex } );
+ modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value.
+ this.setSelectedModel( modelReceived );
+ },
+
+ _over : function( event, ui ) {
+ // when an item is being dragged into the sortable,
+ // hide the empty list caption if it exists
+ this._getContainerEl().find( "> var.empty-list-caption" ).hide();
+ },
+
+ _onKeydown : function( event ) {
+ if( ! this.processKeyEvents ) return true;
+
+ var trap = false;
+
+ if( this.getSelectedModels( { by : "offset" } ).length == 1 )
+ {
+ // need to trap down and up arrows or else the browser
+ // will end up scrolling a autoscroll div.
+
+ var currentOffset = this.getSelectedModel( { by : "offset" } );
+ if( event.which === this._charCodes.upArrow && currentOffset !== 0 )
+ {
+ this.setSelectedModel( currentOffset - 1, { by : "offset" } );
+ trap = true;
+ }
+ else if( event.which === this._charCodes.downArrow && currentOffset !== this.collection.length - 1 )
+ {
+ this.setSelectedModel( currentOffset + 1, { by : "offset" } );
+ trap = true;
+ }
+ }
+
+ return ! trap;
+ },
+
+ _listItem_onMousedown : function( theEvent ) {
+ if( ! this.selectable || ! this.clickToSelect ) return;
+
+ var clickedItemId = this._getClickedItemId( theEvent );
+
+ if( clickedItemId )
+ {
+ // Exit if an unselectable item was clicked
+ if( _.isFunction( this.selectableModelsFilter ) &&
+ ! this.selectableModelsFilter.call( this, this.collection.get( clickedItemId ) ) )
+ {
+ return;
+ }
+
+ // a selectable list item was clicked
+ if( this.selectMultiple && theEvent.shiftKey )
+ {
+ var firstSelectedItemIndex = -1;
+
+ if( this.selectedItems.length > 0 )
+ {
+ this.collection.find( function( thisItemModel ) {
+ firstSelectedItemIndex++;
+
+ // exit when we find our first selected element
+ return _.contains( this.selectedItems, thisItemModel.cid );
+ }, this );
+ }
+
+ var clickedItemIndex = -1;
+ this.collection.find( function( thisItemModel ) {
+ clickedItemIndex++;
+
+ // exit when we find the clicked element
+ return thisItemModel.cid == clickedItemId;
+ }, this );
+
+ var shiftKeyRootSelectedItemIndex = firstSelectedItemIndex == -1 ? clickedItemIndex : firstSelectedItemIndex;
+ var minSelectedItemIndex = Math.min( clickedItemIndex, shiftKeyRootSelectedItemIndex );
+ var maxSelectedItemIndex = Math.max( clickedItemIndex, shiftKeyRootSelectedItemIndex );
+
+ var newSelectedItems = [];
+ for( var thisIndex = minSelectedItemIndex; thisIndex <= maxSelectedItemIndex; thisIndex ++ )
+ newSelectedItems.push( this.collection.at( thisIndex ).cid );
+ this.setSelectedModels( newSelectedItems, { by : "cid" } );
+
+ // shift clicking will usually highlight selectable text, which we do not want.
+ // this is a cross browser (hopefully) snippet that deselects all text selection.
+ if( document.selection && document.selection.empty )
+ document.selection.empty();
+ else if(window.getSelection) {
+ var sel = window.getSelection();
+ if( sel && sel.removeAllRanges )
+ sel.removeAllRanges();
+ }
+ }
+ else if( this.selectMultiple && ( this.clickToToggle || theEvent.metaKey ) )
+ {
+ if( _.contains( this.selectedItems, clickedItemId ) )
+ this.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : "cid" } );
+ else this.setSelectedModels( _.union( this.selectedItems, clickedItemId ), { by : "cid" } );
+ }
+ else
+ this.setSelectedModels( [ clickedItemId ], { by : "cid" } );
+ }
+ else
+ // the blank area of the list was clicked
+ this.setSelectedModels( [] );
+
+ },
+
+ _listItem_onDoubleClick : function( theEvent ) {
+ var clickedItemId = this._getClickedItemId( theEvent );
+
+ if( clickedItemId )
+ {
+ var clickedModel = this.collection.get( clickedItemId );
+ this.trigger( "doubleClick", clickedModel );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "doubleClick", { clickedModel : clickedModel } );
+ }
+ },
+
+ _listBackground_onClick : function( theEvent ) {
+ if( ! this.selectable ) return;
+ if( ! $( theEvent.target ).is( ".collection-list" ) ) return;
+
+ this.setSelectedModels( [] );
+ }
+
+ }, {
+ setDefaultModelViewConstructor : function( theConstructor ) {
+ mDefaultModelViewConstructor = theConstructor;
+ }
+ });
+
+ // Backbone.ViewOptions
+ // --------------------
+ // v0.2.0
+ //
+ // Copyright (c)2014 Rotunda Software
+ //
+ // https://github.com/rotundasoftware/backbone.viewOptions
+
+ // Backbone.ViewOptions
+ // --------------------
+ //
+ // An plugin to declare and get/set options on views.
+
+ /*
+ * Backbone.ViewOptions, v0.2
+ * Copyright (c)2014 Rotunda Software, LLC.
+ * Distributed under MIT license
+ * http://github.com/rotundasoftware/backbone.viewOptions
+ */
+
+ Backbone.ViewOptions = {};
+
+ Backbone.ViewOptions.add = function( view, optionsDeclarationsProperty ) {
+ if( _.isUndefined( optionsDeclarationsProperty ) ) optionsDeclarationsProperty = "options";
+
+ // ****************** Public methods added to view ******************
+
+ view.setOptions = function( options ) {
+ var _this = this;
+ var optionsThatWereChanged = {};
+ var optionsThatWereChangedOriginalValues = {};
+
+ var optionDeclarations = _.result( this, optionsDeclarationsProperty );
+
+ if( ! _.isUndefined( optionDeclarations ) ) {
+ var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );
+
+ _.each( normalizedOptionDeclarations, function( thisOptionDeclaration ) {
+ thisOptionName = thisOptionDeclaration.name;
+ thisOptionRequired = thisOptionDeclaration.required;
+ thisOptionDefaultValue = thisOptionDeclaration.defaultValue;
+
+ if( thisOptionRequired ) {
+ // note we do not throw an error if a required option is not supplied, but it is
+ // found on the object itself (due to a prior call of view.setOptions, most likely)
+ if( ! options ||
+ ( ( ! _.contains( _.keys( options ), thisOptionName ) && _.isUndefined( _this[ thisOptionName ] ) ) ) ||
+ _.isUndefined( options[ thisOptionName ] ) )
+ throw new Error( "Required option \"" + thisOptionName + "\" was not supplied." );
+ }
+
+ // attach the supplied value of this option, or the appropriate default value, to the view object
+ if( options && thisOptionName in options ) {
+ // if this option already exists on the view, make a note that we will be changing it
+ if( ! _.isUndefined( _this[ thisOptionName ] ) ) {
+ optionsThatWereChangedOriginalValues[ thisOptionName ] = _this[ thisOptionName ];
+ optionsThatWereChanged[ thisOptionName ] = options[ thisOptionName ];
+ }
+ _this[ thisOptionName ] = options[ thisOptionName ];
+ // note we do NOT delete the option off the options object here so that
+ // multiple views can be passed the same options object without issue.
+ }
+ else if( ! _.isUndefined( thisOptionDefaultValue ) && _.isUndefined( _this[ thisOptionName ] ) ) {
+ // note defaults do not write over any existing properties on the view itself.
+ _this[ thisOptionName ] = thisOptionDefaultValue;
+ }
+ } );
+ }
+
+ if( _.keys( optionsThatWereChanged ).length > 0 ) {
+ if( _.isFunction( _this.onOptionsChanged ) )
+ _this.onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedOriginalValues );
+ else if( _.isFunction( _this._onOptionsChanged ) )
+ _this._onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedOriginalValues );
+ }
+ };
+
+ view.getOptions = function() {
+ var optionDeclarations = _.result( this, optionsDeclarationsProperty );
+ if( _.isUndefined( optionDeclarations ) ) return [];
+
+ var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );
+ var optionsNames = _.pluck( normalizedOptionDeclarations, "name" );
+
+ return _.pick( this, optionsNames );
+ };
+ };
+
+ // ****************** Private Utility Functions ******************
+
+ function _normalizeOptionDeclarations( optionDeclarations ) {
+ // convert our short-hand option syntax (with exclamation marks, etc.)
+ // to a simple array of standard option declaration objects.
+ var normalizedOptionDeclarations = [];
+
+ if( ! _.isArray( optionDeclarations ) ) {
+ throw new Error( "Option declarations must be an array." );
+ }
+
+ _.each( optionDeclarations, function( thisOptionDeclaration ) {
+ var thisOptionName, thisOptionRequired, thisOptionDefaultValue;
+
+ thisOptionRequired = false;
+ thisOptionDefaultValue = undefined;
+
+ if( _.isString( thisOptionDeclaration ) )
+ thisOptionName = thisOptionDeclaration;
+ else if( _.isObject( thisOptionDeclaration ) ) {
+ thisOptionName = _.first( _.keys( thisOptionDeclaration ) );
+ thisOptionDefaultValue = _.clone( thisOptionDeclaration[ thisOptionName ] );
+ }
+ else throw new Error( "Each element in the option declarations array must be either a string or an object." );
+
+ if( thisOptionName[ thisOptionName.length - 1 ] === "!" ) {
+ thisOptionRequired = true;
+ thisOptionName = thisOptionName.slice( 0, thisOptionName.length - 1 );
+ }
+
+ normalizedOptionDeclarations.push( {
+ name : thisOptionName,
+ required : thisOptionRequired,
+ defaultValue : thisOptionDefaultValue
+ } );
+ } );
+
+ return normalizedOptionDeclarations;
+ };
+
+
+ // Backbone.BabySitter
+ // -------------------
+ // v0.0.6
+ //
+ // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
+ // Distributed under MIT license
+ //
+ // http://github.com/babysitterjs/backbone.babysitter
+
+ // Backbone.ChildViewContainer
+ // ---------------------------
+ //
+ // Provide a container to store, retrieve and
+ // shut down child views.
+
+ ChildViewContainer = (function(Backbone, _){
+
+ // Container Constructor
+ // ---------------------
+
+ var Container = function(views){
+ this._views = {};
+ this._indexByModel = {};
+ this._indexByCustom = {};
+ this._updateLength();
+
+ _.each(views, this.add, this);
+ };
+
+ // Container Methods
+ // -----------------
+
+ _.extend(Container.prototype, {
+
+ // Add a view to this container. Stores the view
+ // by `cid` and makes it searchable by the model
+ // cid (and model itself). Optionally specify
+ // a custom key to store an retrieve the view.
+ add: function(view, customIndex){
+ var viewCid = view.cid;
+
+ // store the view
+ this._views[viewCid] = view;
+
+ // index it by model
+ if (view.model){
+ this._indexByModel[view.model.cid] = viewCid;
+ }
+
+ // index by custom
+ if (customIndex){
+ this._indexByCustom[customIndex] = viewCid;
+ }
+
+ this._updateLength();
+ },
+
+ // Find a view by the model that was attached to
+ // it. Uses the model's `cid` to find it.
+ findByModel: function(model){
+ return this.findByModelCid(model.cid);
+ },
+
+ // Find a view by the `cid` of the model that was attached to
+ // it. Uses the model's `cid` to find the view `cid` and
+ // retrieve the view using it.
+ findByModelCid: function(modelCid){
+ var viewCid = this._indexByModel[modelCid];
+ return this.findByCid(viewCid);
+ },
+
+ // Find a view by a custom indexer.
+ findByCustom: function(index){
+ var viewCid = this._indexByCustom[index];
+ return this.findByCid(viewCid);
+ },
+
+ // Find by index. This is not guaranteed to be a
+ // stable index.
+ findByIndex: function(index){
+ return _.values(this._views)[index];
+ },
+
+ // retrieve a view by it's `cid` directly
+ findByCid: function(cid){
+ return this._views[cid];
+ },
+
+ findIndexByCid : function( cid ) {
+ var index = -1;
+ var view = _.find( this._views, function ( view ) {
+ index++;
+ if( view.model.cid == cid )
+ return view;
+ } );
+ return ( view ) ? index : -1;
+ },
+
+ // Remove a view
+ remove: function(view){
+ var viewCid = view.cid;
+
+ // delete model index
+ if (view.model){
+ delete this._indexByModel[view.model.cid];
+ }
+
+ // delete custom index
+ _.any(this._indexByCustom, function(cid, key) {
+ if (cid === viewCid) {
+ delete this._indexByCustom[key];
+ return true;
+ }
+ }, this);
+
+ // remove the view from the container
+ delete this._views[viewCid];
+
+ // update the length
+ this._updateLength();
+ },
+
+ // Call a method on every view in the container,
+ // passing parameters to the call method one at a
+ // time, like `function.call`.
+ call: function(method){
+ this.apply(method, _.tail(arguments));
+ },
+
+ // Apply a method on every view in the container,
+ // passing parameters to the call method one at a
+ // time, like `function.apply`.
+ apply: function(method, args){
+ _.each(this._views, function(view){
+ if (_.isFunction(view[method])){
+ view[method].apply(view, args || []);
+ }
+ });
+ },
+
+ // Update the `.length` attribute on this container
+ _updateLength: function(){
+ this.length = _.size(this._views);
+ }
+ });
+
+ // Borrowing this code from Backbone.Collection:
+ // http://backbonejs.org/docs/backbone.html#section-106
+ //
+ // Mix in methods from Underscore, for iteration, and other
+ // collection related features.
+ var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
+ 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
+ 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
+ 'last', 'without', 'isEmpty', 'pluck'];
+
+ _.each(methods, function(method) {
+ Container.prototype[method] = function() {
+ var views = _.values(this._views);
+ var args = [views].concat(_.toArray(arguments));
+ return _[method].apply(_, args);
+ };
+ });
+
+ // return the public API
+ return Container;
+ })(Backbone, _);
+})();
diff --git a/ajax/libs/backbone.collectionView/0.9.1/backbone.collectionView.min.js b/ajax/libs/backbone.collectionView/0.9.1/backbone.collectionView.min.js
new file mode 100644
index 000000000..2fb9d8a51
--- /dev/null
+++ b/ajax/libs/backbone.collectionView/0.9.1/backbone.collectionView.min.js
@@ -0,0 +1,8 @@
+/*!
+* Backbone.CollectionView, v0.9.0
+* Copyright (c)2013 Rotunda Software, LLC.
+* Distributed under MIT license
+* http://github.com/rotundasoftware/backbone-collection-view
+*/
+
+(function(){function e(e){var t=[];if(!_.isArray(e))throw Error("Option declarations must be an array.");return _.each(e,function(e){var i,s,n;if(s=!1,n=void 0,_.isString(e))i=e;else{if(!_.isObject(e))throw Error("Each element in the option declarations array must be either a string or an object.");i=_.first(_.keys(e)),n=_.clone(e[i])}"!"===i[i.length-1]&&(s=!0,i=i.slice(0,i.length-1)),t.push({name:i,required:s,defaultValue:n})}),t}var t=Backbone.View,i="model",s=["collection","modelView","modelViewOptions","itemTemplate","selectableModelsFilter","sortableModelsFilter","visibleModelsFilter","itemTemplateFunction","detachedRendering","sortableOptions"],n={background:"transparent",border:"none","box-shadow":"none"};Backbone.CollectionView=Backbone.View.extend({tagName:"ul",events:{"mousedown li, td":"_listItem_onMousedown","dblclick li, td":"_listItem_onDoubleClick",click:"_listBackground_onClick","click ul.collection-list, table.collection-list":"_listBackground_onClick",keydown:"_onKeydown"},spawnMessages:{focus:"focus"},passMessages:{"*":"."},initializationOptions:[{collection:new Backbone.Collection},{modelView:null},{modelViewOptions:{}},{itemTemplate:null},{itemTemplateFunction:null},{selectable:!0},{clickToSelect:!0},{selectableModelsFilter:null},{visibleModelsFilter:null},{sortableModelsFilter:null},{selectMultiple:!1},{clickToToggle:!1},{processKeyEvents:!0},{sortable:!1},{sortableOptions:null},{detachedRendering:!1},{emptyListCaption:null}],initialize:function(e){Backbone.ViewOptions.add(this,"initializationOptions"),this.setOptions(e),this._hasBeenRendered=!1,this._isBackboneCourierAvailable()&&Backbone.Courier.add(this),this.$el.data("view",this),this.$el.addClass("collection-list"),this.selectable&&this.$el.addClass("selectable"),this.processKeyEvents&&this.$el.attr("tabindex",0),this.selectedItems=[],this._updateItemTemplate(),this.collection&&this._registerCollectionEvents(),this.viewManager=new ChildViewContainer},onOptionsChanged:function(e,t){var i=!1,n=this;_.each(_.keys(e),function(o){var l=e[o],a=t[o];switch(o){case"collection":l!==a&&(n.stopListening(a),n._registerCollectionEvents());break;case"selectMultiple":!l&&n.selectedItems.length>1&&n.setSelectedModel(_.first(n.selectedItems),{by:"cid"});break;case"selectable":!l&&n.selectedItems.length>0&&n.setSelectedModels([]);break;case"selectableModelsFilter":l&&_.isFunction(l)&&n._validateSelection();break;case"itemTemplate":n._updateItemTemplate();break;case"processKeyEvents":l&&n.$el.attr("tabindex",0);break;case"modelView":n.viewManager.each(function(e){n.viewManager.remove(e),e.remove()})}_.contains(s,o)&&(i=!0)}),this._hasBeenRendered&&i&&this.render()},setOption:function(e,t){var i={};i[e]=t,this.setOptions(i)},getSelectedModel:function(e){return _.first(this.getSelectedModels(e))},getSelectedModels:function(e){var t=this;e=_.extend({},{by:i},e);var s=e.by,n=[];switch(s){case"id":_.each(this.selectedItems,function(e){n.push(t.collection.get(e).id)});break;case"cid":n=n.concat(this.selectedItems);break;case"offset":var o=0,l=this._getVisibleItemEls();l.each(function(){var e=$(this);e.is(".selected")&&n.push(o),o++});break;case"model":_.each(this.selectedItems,function(e){n.push(t.collection.get(e))});break;case"view":_.each(this.selectedItems,function(e){n.push(t.viewManager.findByModel(t.collection.get(e)))})}return n},setSelectedModels:function(e,t){if(!_.isArray(e))throw"Invalid parameter value";if(this.selectable||!(e.length>0)){t=_.extend({},{silent:!1,by:i},t);var s=t.by,n=[];switch(s){case"cid":n=e;break;case"id":this.collection.each(function(t){_.contains(e,t.id)&&n.push(t.cid)});break;case"model":n=_.pluck(e,"cid");break;case"view":_.each(e,function(e){n.push(e.model.cid)});break;case"offset":var o=0,l=this._getVisibleItemEls();l.each(function(){var t=$(this);_.contains(e,o)&&n.push(t.attr("data-model-cid")),o++})}var a=this.getSelectedModels(),d=_.clone(this.selectedItems);this.selectedItems=this._convertStringsToInts(n),this._validateSelection();var r=this.getSelectedModels();this._containSameElements(d,this.selectedItems)||(this._addSelectedClassToSelectedItems(d),t.silent||(this.trigger("selectionChanged",r,a),this._isBackboneCourierAvailable()&&this.spawn("selectionChanged",{selectedModels:r,oldSelectedModels:a})),this.updateDependentControls())}},setSelectedModel:function(e,t){e||0===e?this.setSelectedModels([e],t):this.setSelectedModels([],t)},render:function(){var e=this;this._hasBeenRendered=!0,this.selectable&&this._saveSelection();var t;t=this._getContainerEl();var i=this.viewManager;this.viewManager=new ChildViewContainer,i.each(function(t){e.collection.get(t.model.cid)?t.$el.detach():t.remove()}),t.empty();var s;if(this.detachedRendering&&(s=document.createDocumentFragment()),this.collection.each(function(e){var n=i.findByModelCid(e.cid);_.isUndefined(n)&&(n=this._createNewModelView(e,this._getModelViewOptions(e))),this._insertAndRenderModelView(n,s||t)},this),this.detachedRendering&&t.append(s),this.sortable){var n=_.extend({axis:"y",distance:10,forcePlaceholderSize:!0,start:_.bind(this._sortStart,this),change:_.bind(this._sortChange,this),stop:_.bind(this._sortStop,this),receive:_.bind(this._receive,this),over:_.bind(this._over,this)},_.result(this,"sortableOptions"));e._isRenderedAsTable()?n.items="> tbody > tr:not(.not-sortable)":e._isRenderedAsList()&&(n.items="> li:not(.not-sortable)"),this.$el=this.$el.sortable(n)}this._showEmptyListCaptionIfAppropriate(),this.trigger("render"),this._isBackboneCourierAvailable()&&this.spawn("render"),this.selectable&&(this._restoreSelection(),this.updateDependentControls()),_.isFunction(this.onAfterRender)&&this.onAfterRender()},_showEmptyListCaptionIfAppropriate:function(){if(this.emptyListCaption){var e=this._getVisibleItemEls();if(0===e.length){var t;t=_.isFunction(this.emptyListCaption)?this.emptyListCaption():this.emptyListCaption;var i=$(""+t+"");$emptyListCaptionEl=this._isRenderedAsList()?i.wrapAll("").parent().css(n):i.wrapAll(" |
").parent().parent().css(n),this._getContainerEl().append($emptyListCaptionEl)}}},_removeEmptyListCaption:function(){this._isRenderedAsList()?this._getContainerEl().find("> li > var.empty-list-caption").parent().remove():this._getContainerEl().find("> tr > td > var.empty-list-caption").parent().parent().remove()},_insertAndRenderModelView:function(e,t,i){var s=this._wrapModelView(e);11===t.nodeType?t.appendChild(s.get(0)):!_.isUndefined(i)&&i>0&&this.collection.length-1>i?t.children().eq(i).before(s):t.append(s);var n=e.render();n===!1&&(s.hide(),s.addClass("not-visible"));var o=!1;_.isFunction(this.visibleModelsFilter)&&(o=!this.visibleModelsFilter(e.model),o&&(1===s.children().length?s.hide():e.$el.hide(),s.addClass("not-visible"))),!o&&this.emptyListCaption&&this._removeEmptyListCaption(),this.viewManager.add(e)},updateDependentControls:function(){this.trigger("updateDependentControls",this.getSelectedModels()),this._isBackboneCourierAvailable()&&this.spawn("updateDependentControls",{selectedModels:this.getSelectedModels()})},remove:function(){this.viewManager.each(function(e){e.remove()}),Backbone.View.prototype.remove.apply(this,arguments)},_removeModelView:function(e){var t=this.viewManager,i=t.findByModelCid(e.cid);this.selectable&&this._saveSelection(),t.remove(i),i.remove(),this._getContainerEl().children("[data-model-cid="+e.cid+"]").remove(),this.selectable&&this._restoreSelection(),this._showEmptyListCaptionIfAppropriate()},_validateSelectionAndRender:function(){this._validateSelection(),this.render()},_registerCollectionEvents:function(){this.listenTo(this.collection,"add",function(e){if(this._hasBeenRendered){var t=this._createNewModelView(e,this._getModelViewOptions(e));this._insertAndRenderModelView(t,this._getContainerEl(),this.collection.indexOf(e))}this._isBackboneCourierAvailable()&&this.spawn("add")}),this.listenTo(this.collection,"remove",function(e){this._hasBeenRendered&&this._removeModelView(e),this._isBackboneCourierAvailable()&&this.spawn("remove")}),this.listenTo(this.collection,"reset",function(){this._hasBeenRendered&&this.render(),this._isBackboneCourierAvailable()&&this.spawn("reset")}),this.listenTo(this.collection,"sort",function(e,t){this._hasBeenRendered&&t.add!==!0&&this.render(),this._isBackboneCourierAvailable()&&this.spawn("sort")})},_getContainerEl:function(){if(this._isRenderedAsTable()){var e=this.$el.find("> tbody");if(e.length>0)return e}return this.$el},_getClickedItemId:function(e){var t=null,i=$(e.currentTarget);if(i.closest(".collection-list").get(0)===this.$el.get(0)){var s=i.closest("[data-model-cid]");return s.length>0&&(t=s.attr("data-model-cid"),$.isNumeric(t)&&(t=parseInt(t,10))),t}},_updateItemTemplate:function(){var e;if(this.itemTemplate){if(0===$(this.itemTemplate).length)throw"Could not find item template from selector: "+this.itemTemplate;e=$(this.itemTemplate).html()}else e=this.$(".item-template").html();e&&(this.itemTemplateFunction=_.template(e))},_validateSelection:function(){var e=_.pluck(this.collection.models,"cid");this.selectedItems=_.intersection(e,this.selectedItems),_.isFunction(this.selectableModelsFilter)&&(this.selectedItems=_.filter(this.selectedItems,function(e){return this.selectableModelsFilter.call(this,this.collection.get(e))},this))},_saveSelection:function(){if(!this.selectable)throw"Attempt to save selection on non-selectable list";this.savedSelection={items:_.clone(this.selectedItems),offset:this.getSelectedModel({by:"offset"})}},_restoreSelection:function(){if(!this.savedSelection)throw"Attempt to restore selection but no selection has been saved!";this.setSelectedModels([],{silent:!0}),this.savedSelection.items.length>0&&(this.setSelectedModels(this.savedSelection.items,{by:"cid",silent:!0}),0===this.selectedItems.length&&this.setSelectedModel(this.savedSelection.offset,{by:"offset"}),this.selectedItems.length!==this.savedSelection.items.length&&(this.trigger("selectionChanged",this.getSelectedModels(),[]),this._isBackboneCourierAvailable()&&this.spawn("selectionChanged",{selectedModels:this.getSelectedModels(),oldSelectedModels:[]}))),delete this.savedSelection},_addSelectedClassToSelectedItems:function(e){_.isUndefined(e)&&(e=[]);var t=e;t=_.without(t,this.selectedItems),_.each(t,function(e){this._getContainerEl().find("[data-model-cid="+e+"]").removeClass("selected")},this);var i=this.selectedItems;i=_.without(i,e),_.each(i,function(e){this._getContainerEl().find("[data-model-cid="+e+"]").addClass("selected")},this)},_reorderCollectionBasedOnHTML:function(){var e=this;this._getContainerEl().children().each(function(){var t=$(this).attr("data-model-cid");if(t){var i=e.collection.get(t);i&&(e.collection.remove(i,{silent:!0}),e.collection.add(i,{silent:!0,sort:!e.collection.comparator}))}}),this.collection.trigger("reorder"),this._isBackboneCourierAvailable()&&this.spawn("reorder"),this.collection.comparator&&this.collection.sort()},_getModelViewConstructor:function(){return this.modelView||t},_getModelViewOptions:function(e){return _.extend({model:e},this.modelViewOptions)},_createNewModelView:function(e,t){var i=this._getModelViewConstructor(e);if(_.isUndefined(i))throw"Could not find modelView constructor for model";var s=new i(t);return s.collectionListView=this,s},_wrapModelView:function(e){var t,i=this;return this._isRenderedAsTable()?t=e.$el.attr("data-model-cid",e.model.cid):this._isRenderedAsList()&&(t="li"===e.$el.prop("tagName").toLowerCase()?e.$el.attr("data-model-cid",e.model.cid):e.$el.wrapAll("").parent()),_.isFunction(this.sortableModelsFilter)&&(this.sortableModelsFilter.call(i,e.model)||t.addClass("not-sortable")),_.isFunction(this.selectableModelsFilter)&&(this.selectableModelsFilter.call(i,e.model)||t.addClass("not-selectable")),t},_convertStringsToInts:function(e){return _.map(e,function(e){if(!_.isString(e))return e;var t=parseInt(e,10);return t==e?t:e})},_containSameElements:function(e,t){if(e.length!=t.length)return!1;var i=_.intersection(e,t).length;return i==e.length},_isRenderedAsTable:function(){return"table"===this.$el.prop("tagName").toLowerCase()},_isRenderedAsList:function(){return!this._isRenderedAsTable()},_getVisibleItemEls:function(){var e=[];return e=this._getContainerEl().find("> [data-model-cid]:not(.not-visible)")},_charCodes:{upArrow:38,downArrow:40},_isBackboneCourierAvailable:function(){return!_.isUndefined(Backbone.Courier)},_sortStart:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid"));this.trigger("sortStart",i),this._isBackboneCourierAvailable()&&this.spawn("sortStart",{modelBeingSorted:i})},_sortChange:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid"));this.trigger("sortChange",i),this._isBackboneCourierAvailable()&&this.spawn("sortChange",{modelBeingSorted:i})},_sortStop:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid")),s=this._getContainerEl(),n=s.children().index(t.item);-1==n&&this.collection.remove(i),this._reorderCollectionBasedOnHTML(),this.updateDependentControls(),this.trigger("sortStop",i,n),this._isBackboneCourierAvailable()&&this.spawn("sortStop",{modelBeingSorted:i,newIndex:n})},_receive:function(e,t){var i=t.sender,s=i.data("view");if(s&&s.collection){var n=this._getContainerEl().children().index(t.item),o=s.collection.get(t.item.attr("data-model-cid"));s.collection.remove(o),this.collection.add(o,{at:n}),o.collection=this.collection,this.setSelectedModel(o)}},_over:function(){this._getContainerEl().find("> var.empty-list-caption").hide()},_onKeydown:function(e){if(!this.processKeyEvents)return!0;var t=!1;if(1==this.getSelectedModels({by:"offset"}).length){var i=this.getSelectedModel({by:"offset"});e.which===this._charCodes.upArrow&&0!==i?(this.setSelectedModel(i-1,{by:"offset"}),t=!0):e.which===this._charCodes.downArrow&&i!==this.collection.length-1&&(this.setSelectedModel(i+1,{by:"offset"}),t=!0)}return!t},_listItem_onMousedown:function(e){if(this.selectable&&this.clickToSelect){var t=this._getClickedItemId(e);if(t){if(_.isFunction(this.selectableModelsFilter)&&!this.selectableModelsFilter.call(this,this.collection.get(t)))return;if(this.selectMultiple&&e.shiftKey){var i=-1;this.selectedItems.length>0&&this.collection.find(function(e){return i++,_.contains(this.selectedItems,e.cid)},this);var s=-1;this.collection.find(function(e){return s++,e.cid==t},this);for(var n=-1==i?s:i,o=Math.min(s,n),l=Math.max(s,n),a=[],d=o;l>=d;d++)a.push(this.collection.at(d).cid);if(this.setSelectedModels(a,{by:"cid"}),document.selection&&document.selection.empty)document.selection.empty();else if(window.getSelection){var r=window.getSelection();r&&r.removeAllRanges&&r.removeAllRanges()}}else this.selectMultiple&&(this.clickToToggle||e.metaKey)?_.contains(this.selectedItems,t)?this.setSelectedModels(_.without(this.selectedItems,t),{by:"cid"}):this.setSelectedModels(_.union(this.selectedItems,t),{by:"cid"}):this.setSelectedModels([t],{by:"cid"})}else this.setSelectedModels([])}},_listItem_onDoubleClick:function(e){var t=this._getClickedItemId(e);if(t){var i=this.collection.get(t);this.trigger("doubleClick",i),this._isBackboneCourierAvailable()&&this.spawn("doubleClick",{clickedModel:i})}},_listBackground_onClick:function(e){this.selectable&&$(e.target).is(".collection-list")&&this.setSelectedModels([])}},{setDefaultModelViewConstructor:function(e){t=e}}),Backbone.ViewOptions={},Backbone.ViewOptions.add=function(t,i){_.isUndefined(i)&&(i="options"),t.setOptions=function(t){var s=this,n={},o={},l=_.result(this,i);if(!_.isUndefined(l)){var a=e(l);_.each(a,function(e){if(thisOptionName=e.name,thisOptionRequired=e.required,thisOptionDefaultValue=e.defaultValue,thisOptionRequired&&(!t||!_.contains(_.keys(t),thisOptionName)&&_.isUndefined(s[thisOptionName])||_.isUndefined(t[thisOptionName])))throw Error('Required option "'+thisOptionName+'" was not supplied.');t&&thisOptionName in t?(_.isUndefined(s[thisOptionName])||(o[thisOptionName]=s[thisOptionName],n[thisOptionName]=t[thisOptionName]),s[thisOptionName]=t[thisOptionName]):!_.isUndefined(thisOptionDefaultValue)&&_.isUndefined(s[thisOptionName])&&(s[thisOptionName]=thisOptionDefaultValue)})}_.keys(n).length>0&&(_.isFunction(s.onOptionsChanged)?s.onOptionsChanged(n,o):_.isFunction(s._onOptionsChanged)&&s._onOptionsChanged(n,o))},t.getOptions=function(){var t=_.result(this,i);if(_.isUndefined(t))return[];var s=e(t),n=_.pluck(s,"name");return _.pick(this,n)}},ChildViewContainer=function(e,t){var i=function(e){this._views={},this._indexByModel={},this._indexByCustom={},this._updateLength(),t.each(e,this.add,this)};t.extend(i.prototype,{add:function(e,t){var i=e.cid;this._views[i]=e,e.model&&(this._indexByModel[e.model.cid]=i),t&&(this._indexByCustom[t]=i),this._updateLength()},findByModel:function(e){return this.findByModelCid(e.cid)},findByModelCid:function(e){var t=this._indexByModel[e];return this.findByCid(t)},findByCustom:function(e){var t=this._indexByCustom[e];return this.findByCid(t)},findByIndex:function(e){return t.values(this._views)[e]},findByCid:function(e){return this._views[e]},findIndexByCid:function(e){var i=-1,s=t.find(this._views,function(t){return i++,t.model.cid==e?t:void 0});return s?i:-1},remove:function(e){var i=e.cid;e.model&&delete this._indexByModel[e.model.cid],t.any(this._indexByCustom,function(e,t){return e===i?(delete this._indexByCustom[t],!0):void 0},this),delete this._views[i],this._updateLength()},call:function(e){this.apply(e,t.tail(arguments))},apply:function(e,i){t.each(this._views,function(s){t.isFunction(s[e])&&s[e].apply(s,i||[])})},_updateLength:function(){this.length=t.size(this._views)}});var s=["forEach","each","map","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","toArray","first","initial","rest","last","without","isEmpty","pluck"];return t.each(s,function(e){i.prototype[e]=function(){var i=t.values(this._views),s=[i].concat(t.toArray(arguments));return t[e].apply(t,s)}}),i}(Backbone,_)})();
\ No newline at end of file
diff --git a/ajax/libs/backbone.collectionView/0.9.2/backbone.collectionView.js b/ajax/libs/backbone.collectionView/0.9.2/backbone.collectionView.js
new file mode 100644
index 000000000..27338719d
--- /dev/null
+++ b/ajax/libs/backbone.collectionView/0.9.2/backbone.collectionView.js
@@ -0,0 +1,1242 @@
+/*!
+* Backbone.CollectionView, v0.9.1
+* Copyright (c)2013 Rotunda Software, LLC.
+* Distributed under MIT license
+* http://github.com/rotundasoftware/backbone-collection-view
+*/
+
+( function( root, factory ) {
+ // UMD wrapper
+ if ( typeof define === 'function' && define.amd ) {
+ // AMD
+ define( [ 'underscore', 'backbone', 'jquery' ], factory );
+ } else if ( typeof exports !== 'undefined' ) {
+ // Node/CommonJS
+ module.exports = factory( require('underscore' ), require( 'backbone' ), require( 'backbone' ).$ );
+ } else {
+ // Browser globals
+ factory( root._, root.Backbone, ( root.jQuery || root.Zepto || root.$ ) );
+ }
+
+}( this, function( _, Backbone, $ ) {
+ var mDefaultModelViewConstructor = Backbone.View;
+
+ var kDefaultReferenceBy = "model";
+
+ var kOptionsRequiringRerendering = [ "collection", "modelView", "modelViewOptions", "itemTemplate", "selectableModelsFilter", "sortableModelsFilter", "visibleModelsFilter", "itemTemplateFunction", "detachedRendering", "sortableOptions" ];
+
+ var kStylesForEmptyListCaption = {
+ "background" : "transparent",
+ "border" : "none",
+ "box-shadow" : "none"
+ };
+
+ Backbone.CollectionView = Backbone.View.extend( {
+
+ tagName : "ul",
+
+ events : {
+ "mousedown li, td" : "_listItem_onMousedown",
+ "dblclick li, td" : "_listItem_onDoubleClick",
+ "click" : "_listBackground_onClick",
+ "click ul.collection-list, table.collection-list" : "_listBackground_onClick",
+ "keydown" : "_onKeydown"
+ },
+
+ // only used if Backbone.Courier is available
+ spawnMessages : {
+ "focus" : "focus"
+ },
+
+ //only used if Backbone.Courier is available
+ passMessages : { "*" : "." },
+
+ // viewOption definitions with default values.
+ initializationOptions : [
+ { "collection" : new Backbone.Collection() },
+ { "modelView" : null },
+ { "modelViewOptions" : {} },
+ { "itemTemplate" : null },
+ { "itemTemplateFunction" : null },
+ { "selectable" : true },
+ { "clickToSelect" : true },
+ { "selectableModelsFilter" : null },
+ { "visibleModelsFilter" : null },
+ { "sortableModelsFilter" : null },
+ { "selectMultiple" : false },
+ { "clickToToggle" : false },
+ { "processKeyEvents" : true },
+ { "sortable" : false },
+ { "sortableOptions" : null },
+ { "detachedRendering" : false },
+ { "emptyListCaption" : null }
+ ],
+
+ initialize : function( options ) {
+ Backbone.ViewOptions.add( this, "initializationOptions" ); // setup the ViewOptions functionality.
+ this.setOptions( options ); // and make use of any provided options
+
+ this._hasBeenRendered = false;
+
+ if( this._isBackboneCourierAvailable() ) {
+ Backbone.Courier.add( this );
+ }
+
+ this.$el.data( "view", this ); // needed for connected sortable lists
+ this.$el.addClass( "collection-list" );
+ if( this.selectable ) this.$el.addClass( "selectable" );
+
+ if( this.processKeyEvents )
+ this.$el.attr( "tabindex", 0 ); // so we get keyboard events
+
+ this.selectedItems = [];
+
+ this._updateItemTemplate();
+
+ if( this.collection )
+ this._registerCollectionEvents();
+
+ this.viewManager = new ChildViewContainer();
+ },
+
+ onOptionsChanged : function( changedOptions, originalOptions ) {
+ var rerender = false;
+ var _this = this;
+ _.each( _.keys( changedOptions ), function( changedOptionKey ) {
+ var newVal = changedOptions[ changedOptionKey ];
+ var oldVal = originalOptions[ changedOptionKey ];
+ switch( changedOptionKey ) {
+ case "collection" :
+ if ( newVal !== oldVal ) {
+ _this.stopListening( oldVal );
+ _this._registerCollectionEvents();
+ }
+ break;
+ case "selectMultiple":
+ if( ! newVal && _this.selectedItems.length > 1 )
+ _this.setSelectedModel( _.first( _this.selectedItems ), { by : "cid" } );
+ break;
+ case "selectable" :
+ if( ! newVal && _this.selectedItems.length > 0 )
+ _this.setSelectedModels( [] );
+ break;
+ case "selectableModelsFilter" :
+ if( newVal && _.isFunction( newVal ) )
+ _this._validateSelection();
+ break;
+ case "itemTemplate" :
+ _this._updateItemTemplate();
+ break;
+ case "processKeyEvents" :
+ if( newVal ) _this.$el.attr( "tabindex", 0 ); // so we get keyboard events
+ break;
+ case "modelView" :
+ //need to remove all old view instances
+ _this.viewManager.each( function( view ) {
+ _this.viewManager.remove( view );
+ // destroy the View itself
+ view.remove();
+ } );
+ break;
+ }
+ if( _.contains( kOptionsRequiringRerendering, changedOptionKey ) ) rerender = true;
+ });
+ if( this._hasBeenRendered && rerender ) {
+ this.render(); // Rerender the view if the rerender flag has been set.
+ }
+ },
+
+ setOption : function( optionName, optionValue ) { // now is mearly a wrapper around backbone.viewOptions' setOptions()
+ var optionHash = {};
+ optionHash[ optionName ] = optionValue;
+ this.setOptions( optionHash );
+ },
+
+ getSelectedModel : function( options ) {
+ return _.first( this.getSelectedModels( options ) );
+ },
+
+ getSelectedModels : function ( options ) {
+ var _this = this;
+
+ options = _.extend( {}, {
+ by : kDefaultReferenceBy
+ }, options );
+
+ var referenceBy = options.by;
+ var items = [];
+
+ switch( referenceBy ) {
+ case "id" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.collection.get( item ).id );
+ } );
+ break;
+ case "cid" :
+ items = items.concat( this.selectedItems );
+ break;
+ case "offset" :
+ var curLineNumber = 0;
+
+ var itemElements = this._getVisibleItemEls();
+
+ itemElements.each( function() {
+ var thisItemEl = $( this );
+ if( thisItemEl.is( ".selected" ) )
+ items.push( curLineNumber );
+ curLineNumber++;
+ } );
+ break;
+ case "model" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.collection.get( item ) );
+ } );
+ break;
+ case "view" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.viewManager.findByModel( _this.collection.get( item ) ) );
+ } );
+ break;
+ }
+
+ return items;
+
+ },
+
+ setSelectedModels : function( newSelectedItems, options ) {
+ if( ! _.isArray( newSelectedItems ) ) throw "Invalid parameter value";
+ if( ! this.selectable && newSelectedItems.length > 0 ) return; // used to throw error, but there are some circumstances in which a list can be selectable at times and not at others, don't want to have to worry about catching errors
+
+ options = _.extend( {}, {
+ silent : false,
+ by : kDefaultReferenceBy
+ }, options );
+
+ var referenceBy = options.by;
+ var newSelectedCids = [];
+
+ switch( referenceBy ) {
+ case "cid" :
+ newSelectedCids = newSelectedItems;
+ break;
+ case "id" :
+ this.collection.each( function( thisModel ) {
+ if( _.contains( newSelectedItems, thisModel.id ) ) newSelectedCids.push( thisModel.cid );
+ } );
+ break;
+ case "model" :
+ newSelectedCids = _.pluck( newSelectedItems, "cid" );
+ break;
+ case "view" :
+ _.each( newSelectedItems, function( item ) {
+ newSelectedCids.push( item.model.cid );
+ } );
+ break;
+ case "offset" :
+ var curLineNumber = 0;
+ var selectedItems = [];
+
+ var itemElements = this._getVisibleItemEls();
+ itemElements.each( function() {
+ var thisItemEl = $( this );
+ if( _.contains( newSelectedItems, curLineNumber ) )
+ newSelectedCids.push( thisItemEl.attr( "data-model-cid" ) );
+ curLineNumber++;
+ } );
+ break;
+ }
+
+ var oldSelectedModels = this.getSelectedModels();
+ var oldSelectedCids = _.clone( this.selectedItems );
+
+ this.selectedItems = this._convertStringsToInts( newSelectedCids );
+ this._validateSelection();
+
+ var newSelectedModels = this.getSelectedModels();
+
+ if( ! this._containSameElements( oldSelectedCids, this.selectedItems ) )
+ {
+ this._addSelectedClassToSelectedItems( oldSelectedCids );
+
+ if( ! options.silent )
+ {
+ this.trigger( "selectionChanged", newSelectedModels, oldSelectedModels );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "selectionChanged", {
+ selectedModels : newSelectedModels,
+ oldSelectedModels : oldSelectedModels
+ } );
+ }
+ }
+
+ this.updateDependentControls();
+ }
+ },
+
+ setSelectedModel : function( newSelectedItem, options ) {
+ if( ! newSelectedItem && newSelectedItem !== 0 )
+ this.setSelectedModels( [], options );
+ else
+ this.setSelectedModels( [ newSelectedItem ], options );
+ },
+
+ render : function(){
+ var _this = this;
+
+ this._hasBeenRendered = true;
+
+ if( this.selectable ) this._saveSelection();
+
+ var modelViewContainerEl;
+
+ // If collection view element is a table and it has a tbody
+ // within it, render the model views inside of the tbody
+ modelViewContainerEl = this._getContainerEl();
+
+ var oldViewManager = this.viewManager;
+ this.viewManager = new ChildViewContainer();
+
+ // detach each of our subviews that we have already created to represent models
+ // in the collection. We are going to re-use the ones that represent models that
+ // are still here, instead of creating new ones, so that we don't loose state
+ // information in the views.
+ oldViewManager.each( function( thisModelView ) {
+ // to boost performance, only detach those views that will be sticking around.
+ // we won't need the other ones later, so no need to detach them individually.
+ if( _this.collection.get( thisModelView.model.cid ) )
+ thisModelView.$el.detach();
+ else
+ thisModelView.remove();
+ } );
+
+ modelViewContainerEl.empty();
+ var fragmentContainer;
+
+ if( this.detachedRendering )
+ fragmentContainer = document.createDocumentFragment();
+
+ this.collection.each( function( thisModel ) {
+ var thisModelView = oldViewManager.findByModelCid( thisModel.cid );
+ if( _.isUndefined( thisModelView ) ) {
+ // if the model view has not already been created on a
+ // previous render then create and initialize it now.
+ thisModelView = this._createNewModelView( thisModel, this._getModelViewOptions( thisModel ) );
+ }
+
+ this._insertAndRenderModelView( thisModelView, fragmentContainer || modelViewContainerEl );
+ }, this );
+
+ if( this.detachedRendering )
+ modelViewContainerEl.append( fragmentContainer );
+
+ if( this.sortable )
+ {
+ var sortableOptions = _.extend( {
+ axis: "y",
+ distance: 10,
+ forcePlaceholderSize : true,
+ start : _.bind( this._sortStart, this ),
+ change : _.bind( this._sortChange, this ),
+ stop : _.bind( this._sortStop, this ),
+ receive : _.bind( this._receive, this ),
+ over : _.bind( this._over, this )
+ }, _.result( this, "sortableOptions" ) );
+
+ if( _this._isRenderedAsTable() ) {
+ sortableOptions.items = "> tbody > tr:not(.not-sortable)";
+ }
+ else if( _this._isRenderedAsList() ) {
+ sortableOptions.items = "> li:not(.not-sortable)";
+ }
+
+ this.$el = this.$el.sortable( sortableOptions );
+ }
+
+ this._showEmptyListCaptionIfAppropriate();
+
+ this.trigger( "render" );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "render" );
+
+ if( this.selectable ) {
+ this._restoreSelection();
+ this.updateDependentControls();
+ }
+
+ if( _.isFunction( this.onAfterRender ) )
+ this.onAfterRender();
+ },
+
+ _showEmptyListCaptionIfAppropriate : function ( ) {
+ if( this.emptyListCaption ) {
+ var visibleEls = this._getVisibleItemEls();
+
+ if( visibleEls.length === 0 ) {
+ var emptyListString;
+
+ if( _.isFunction( this.emptyListCaption ) )
+ emptyListString = this.emptyListCaption();
+ else
+ emptyListString = this.emptyListCaption;
+
+ var $emptyCaptionEl;
+ var $varEl = $( "" + emptyListString + "" );
+
+ //need to wrap the empty caption to make it fit the rendered list structure (either with an li or a tr td)
+ if( this._isRenderedAsList() )
+ $emptyListCaptionEl = $varEl.wrapAll( "" ).parent().css( kStylesForEmptyListCaption );
+ else
+ $emptyListCaptionEl = $varEl.wrapAll( " |
" ).parent().parent().css( kStylesForEmptyListCaption );
+
+ this._getContainerEl().append( $emptyListCaptionEl );
+ }
+ }
+ },
+
+ _removeEmptyListCaption : function( ) {
+ if( this._isRenderedAsList() )
+ this._getContainerEl().find( "> li > var.empty-list-caption" ).parent().remove();
+ else
+ this._getContainerEl().find( "> tr > td > var.empty-list-caption" ).parent().parent().remove();
+ },
+
+ // Render a single model view in container object "parentElOrDocumentFragment", which is either
+ // a documentFragment or a jquery object. optional arg atIndex is not support for document fragments.
+ _insertAndRenderModelView : function( modelView, parentElOrDocumentFragment, atIndex ) {
+ var thisModelViewWrapped = this._wrapModelView( modelView );
+
+ if( parentElOrDocumentFragment.nodeType === 11 ) // if we are inserting into a document fragment, we need to use the DOM appendChild method
+ parentElOrDocumentFragment.appendChild( thisModelViewWrapped.get( 0 ) );
+ else if( ! _.isUndefined( atIndex ) && atIndex > 0 && atIndex < this.collection.length - 1 )
+ parentElOrDocumentFragment.children().eq( atIndex ).before( thisModelViewWrapped );
+ else
+ parentElOrDocumentFragment.append( thisModelViewWrapped );
+
+ // we have to render the modelView after it has been put in context, as opposed to in the
+ // initialize function of the modelView, because some rendering might be dependent on
+ // the modelView's context in the DOM tree. For example, if the modelView stretch()'s itself,
+ // it must be in full context in the DOM tree or else the stretch will not behave as intended.
+ var renderResult = modelView.render();
+
+ // return false from the view's render function to hide this item
+ if( renderResult === false ) {
+ thisModelViewWrapped.hide();
+ thisModelViewWrapped.addClass( "not-visible" );
+ }
+
+ var hideThisModelView = false;
+ if( _.isFunction( this.visibleModelsFilter ) ) {
+ hideThisModelView = ! this.visibleModelsFilter( modelView.model );
+ if( hideThisModelView ) {
+ if( thisModelViewWrapped.children().length === 1 )
+ thisModelViewWrapped.hide();
+ else modelView.$el.hide();
+
+ thisModelViewWrapped.addClass( "not-visible" );
+ }
+ }
+
+ if( ! hideThisModelView && this.emptyListCaption ) this._removeEmptyListCaption();
+
+ this.viewManager.add( modelView );
+ },
+
+ updateDependentControls : function() {
+ this.trigger( "updateDependentControls", this.getSelectedModels() );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "updateDependentControls", {
+ selectedModels : this.getSelectedModels()
+ } );
+ }
+ },
+
+ // Override `Backbone.View.remove` to also destroy all Views in `viewManager`
+ remove : function() {
+ this.viewManager.each( function( view ) {
+ view.remove();
+ } );
+
+ Backbone.View.prototype.remove.apply( this, arguments );
+ },
+
+ // A method to remove the view relating to model.
+ _removeModelView : function( model ) {
+ var viewManager = this.viewManager;
+ var view = viewManager.findByModelCid( model.cid );
+
+ if ( this.selectable ) this._saveSelection();
+
+ viewManager.remove( view ); // Remove the view from the viewManager
+ view.remove(); // Remove the view from the DOM
+ this._getContainerEl().children( "[data-model-cid=" + model.cid + "]" ).remove(); // Remove the wrapper from the DOM
+
+ if ( this.selectable ) this._restoreSelection();
+
+ this._showEmptyListCaptionIfAppropriate();
+ },
+
+ _validateSelectionAndRender : function() {
+ this._validateSelection();
+ this.render();
+ },
+
+ _registerCollectionEvents : function() {
+ this.listenTo( this.collection, "add", function( model ) {
+ if( this._hasBeenRendered ) {
+ var modelView = this._createNewModelView( model, this._getModelViewOptions( model ) );
+ this._insertAndRenderModelView( modelView, this._getContainerEl(), this.collection.indexOf( model ) );
+ }
+
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "add" );
+ } );
+
+ this.listenTo( this.collection, "remove", function( model ) {
+ if( this._hasBeenRendered )
+ this._removeModelView( model );
+
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "remove" );
+ } );
+
+ this.listenTo( this.collection, "reset", function() {
+ if( this._hasBeenRendered ) this.render();
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "reset" );
+ } );
+
+ // we should not be listening to change events on the model as a default behavior. the models
+ // should be responsible for re-rendering themselves if necessary, and if the collection does
+ // also need to re-render as a result of a model change, this should be handled by overriding
+ // this method. by default the collection view should not re-render in response to model changes
+ // this.listenTo( this.collection, "change", function( model ) {
+ // if( this._hasBeenRendered ) this.viewManager.findByModel( model ).render();
+ // if( this._isBackboneCourierAvailable() )
+ // this.spawn( "change", { model : model } );
+ // } );
+
+ this.listenTo( this.collection, "sort", function( collection, options ) {
+ if( this._hasBeenRendered && options.add !== true ) this.render();
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sort" );
+ } );
+ },
+
+ _getContainerEl : function() {
+ if ( this._isRenderedAsTable() ) {
+ // not all tables have a tbody, so we test
+ var tbody = this.$el.find( "> tbody" );
+ if ( tbody.length > 0 )
+ return tbody;
+ }
+ return this.$el;
+ },
+
+ _getClickedItemId : function( theEvent ) {
+ var clickedItemId = null;
+
+ // important to use currentTarget as opposed to target, since we could be bubbling
+ // an event that took place within another collectionList
+ var clickedItemEl = $( theEvent.currentTarget );
+ if( clickedItemEl.closest( ".collection-list" ).get(0) !== this.$el.get(0) ) return;
+
+ // determine which list item was clicked. If we clicked in the blank area
+ // underneath all the elements, we want to know that too, since in this
+ // case we will want to deselect all elements. so check to see if the clicked
+ // DOM element is the list itself to find that out.
+ var clickedItem = clickedItemEl.closest( "[data-model-cid]" );
+ if( clickedItem.length > 0 )
+ {
+ clickedItemId = clickedItem.attr( "data-model-cid" );
+ if( $.isNumeric( clickedItemId ) ) clickedItemId = parseInt( clickedItemId, 10 );
+ }
+
+ return clickedItemId;
+ },
+
+ _updateItemTemplate : function() {
+ var itemTemplateHtml;
+ if( this.itemTemplate )
+ {
+ if( $( this.itemTemplate ).length === 0 )
+ throw "Could not find item template from selector: " + this.itemTemplate;
+
+ itemTemplateHtml = $( this.itemTemplate ).html();
+ }
+ else
+ itemTemplateHtml = this.$( ".item-template" ).html();
+
+ if( itemTemplateHtml ) this.itemTemplateFunction = _.template( itemTemplateHtml );
+
+ },
+
+ _validateSelection : function() {
+ // note can't use the collection's proxy to underscore because "cid" is not an attribute,
+ // but an element of the model object itself.
+ var modelReferenceIds = _.pluck( this.collection.models, "cid" );
+ this.selectedItems = _.intersection( modelReferenceIds, this.selectedItems );
+
+ if( _.isFunction( this.selectableModelsFilter ) )
+ {
+ this.selectedItems = _.filter( this.selectedItems, function( thisItemId ) {
+ return this.selectableModelsFilter.call( this, this.collection.get( thisItemId ) );
+ }, this );
+ }
+ },
+
+ _saveSelection : function() {
+ // save the current selection. use restoreSelection() to restore the selection to the state it was in the last time saveSelection() was called.
+ if( ! this.selectable ) throw "Attempt to save selection on non-selectable list";
+ this.savedSelection = {
+ items : _.clone( this.selectedItems ),
+ offset : this.getSelectedModel( { by : "offset" } )
+ };
+ },
+
+ _restoreSelection : function() {
+ if( ! this.savedSelection ) throw "Attempt to restore selection but no selection has been saved!";
+
+ // reset selectedItems to empty so that we "redraw" all "selected" classes
+ // when we set our new selection. We do this because it is likely that our
+ // contents have been refreshed, and we have thus lost all old "selected" classes.
+ this.setSelectedModels( [], { silent : true } );
+
+ if( this.savedSelection.items.length > 0 )
+ {
+ // first try to restore the old selected items using their reference ids.
+ this.setSelectedModels( this.savedSelection.items, { by : "cid", silent : true } );
+
+ // all the items with the saved reference ids have been removed from the list.
+ // ok. try to restore the selection based on the offset that used to be selected.
+ // this is the expected behavior after a item is deleted from a list (i.e. select
+ // the line that immediately follows the deleted line).
+ if( this.selectedItems.length === 0 )
+ this.setSelectedModel( this.savedSelection.offset, { by : "offset" } );
+
+ // Trigger a selection changed if the previously selected items were not all found
+ if (this.selectedItems.length !== this.savedSelection.items.length)
+ {
+ this.trigger( "selectionChanged", this.getSelectedModels(), [] );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "selectionChanged", {
+ selectedModels : this.getSelectedModels(),
+ oldSelectedModels : []
+ } );
+ }
+ }
+ }
+
+ delete this.savedSelection;
+ },
+
+ _addSelectedClassToSelectedItems : function( oldItemsIdsWithSelectedClass ) {
+ if( _.isUndefined( oldItemsIdsWithSelectedClass ) ) oldItemsIdsWithSelectedClass = [];
+
+ // oldItemsIdsWithSelectedClass is used for optimization purposes only. If this info is supplied then we
+ // only have to add / remove the "selected" class from those items that "selected" state has changed.
+
+ var itemsIdsFromWhichSelectedClassNeedsToBeRemoved = oldItemsIdsWithSelectedClass;
+ itemsIdsFromWhichSelectedClassNeedsToBeRemoved = _.without( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, this.selectedItems );
+
+ _.each( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, function( thisItemId ) {
+ this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).removeClass( "selected" );
+ }, this );
+
+ var itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems;
+ itemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass );
+
+ _.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) {
+ this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).addClass( "selected" );
+ }, this );
+ },
+
+ _reorderCollectionBasedOnHTML : function() {
+ var _this = this;
+
+ this._getContainerEl().children().each( function() {
+ var thisModelCid = $( this ).attr( "data-model-cid" );
+
+ if( thisModelCid )
+ {
+ // remove the current model and then add it back (at the end of the collection).
+ // When we are done looping through all models, they will be in the correct order.
+ var thisModel = _this.collection.get( thisModelCid );
+ if( thisModel )
+ {
+ _this.collection.remove( thisModel, { silent : true } );
+ _this.collection.add( thisModel, { silent : true, sort : ! _this.collection.comparator } );
+ }
+ }
+ } );
+
+ this.collection.trigger( "reorder" );
+
+ if( this._isBackboneCourierAvailable() ) this.spawn( "reorder" );
+
+ if( this.collection.comparator ) this.collection.sort();
+
+ },
+
+ _getModelViewConstructor : function( thisModel ) {
+ return this.modelView || mDefaultModelViewConstructor;
+ },
+
+ _getModelViewOptions : function( thisModel ) {
+ return _.extend( { model : thisModel }, this.modelViewOptions );
+ },
+
+ _createNewModelView : function( model, modelViewOptions ) {
+ var modelViewConstructor = this._getModelViewConstructor( model );
+ if( _.isUndefined( modelViewConstructor ) ) throw "Could not find modelView constructor for model";
+
+ var newModelView = new( modelViewConstructor )( modelViewOptions );
+ newModelView.collectionListView = this;
+
+ return newModelView;
+ },
+
+ _wrapModelView : function( modelView ) {
+ var _this = this;
+
+ // we use items client ids as opposed to real ids, since we may not have a representation
+ // of these models on the server
+ var wrappedModelView;
+
+ if( this._isRenderedAsTable() ) {
+ // if we are rendering the collection in a table, the template $el is a tr so we just need to set the data-model-cid
+ wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid );
+ }
+ else if( this._isRenderedAsList() ) {
+ // if we are rendering the collection in a list, we need wrap each item in an (if its not already an )
+ // and set the data-model-cid
+ if( modelView.$el.prop( "tagName" ).toLowerCase() === "li" ) {
+ wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid );
+ } else {
+ wrappedModelView = modelView.$el.wrapAll( "" ).parent();
+ }
+ }
+
+ if( _.isFunction( this.sortableModelsFilter ) )
+ if( ! this.sortableModelsFilter.call( _this, modelView.model ) )
+ wrappedModelView.addClass( "not-sortable" );
+
+ if( _.isFunction( this.selectableModelsFilter ) )
+ if( ! this.selectableModelsFilter.call( _this, modelView.model ) )
+ wrappedModelView.addClass( "not-selectable" );
+
+ return wrappedModelView;
+ },
+
+ _convertStringsToInts : function( theArray ) {
+ return _.map( theArray, function( thisEl ) {
+ if( ! _.isString( thisEl ) ) return thisEl;
+ var thisElAsNumber = parseInt( thisEl, 10 );
+ return( thisElAsNumber == thisEl ? thisElAsNumber : thisEl );
+ } );
+ },
+
+ _containSameElements : function( arrayA, arrayB ) {
+ if( arrayA.length != arrayB.length ) return false;
+ var intersectionSize = _.intersection( arrayA, arrayB ).length;
+ return intersectionSize == arrayA.length; // and must also equal arrayB.length, since arrayA.length == arrayB.length
+ },
+
+ _isRenderedAsTable : function() {
+ return this.$el.prop( "tagName" ).toLowerCase() === "table";
+ },
+
+ _isRenderedAsList : function() {
+ return ! this._isRenderedAsTable();
+ },
+
+ // Returns the wrapper HTML element for each visible modelView.
+ // When rendering in a table context, the returned elements are the $el of each modelView.
+ // When rendering in a list context,
+ // If the $el of the modelView is an , the returned elements are the $el of each modelView.
+ // Otherwise, the returned elements are the 's the collectionView wrapped around each modelView $el.
+ _getVisibleItemEls : function() {
+ var itemElements = [];
+ itemElements = this._getContainerEl().find( "> [data-model-cid]:not(.not-visible)" );
+
+ return itemElements;
+ },
+
+ _charCodes : {
+ upArrow : 38,
+ downArrow : 40
+ },
+
+ _isBackboneCourierAvailable : function() {
+ return !_.isUndefined( Backbone.Courier );
+ },
+
+ _sortStart : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ this.trigger( "sortStart", modelBeingSorted );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortStart", { modelBeingSorted : modelBeingSorted } );
+ },
+
+ _sortChange : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ this.trigger( "sortChange", modelBeingSorted );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortChange", { modelBeingSorted : modelBeingSorted } );
+ },
+
+ _sortStop : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ var modelViewContainerEl = this._getContainerEl();
+ var newIndex = modelViewContainerEl.children().index( ui.item );
+
+ if( newIndex == -1 ) {
+ // the element was removed from this list. can happen if this sortable is connected
+ // to another sortable, and the item was dropped into the other sortable.
+ this.collection.remove( modelBeingSorted );
+ }
+
+ this._reorderCollectionBasedOnHTML();
+ this.updateDependentControls();
+ this.trigger( "sortStop", modelBeingSorted, newIndex );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortStop", { modelBeingSorted : modelBeingSorted, newIndex : newIndex } );
+ },
+
+ _receive : function( event, ui ) {
+ var senderListEl = ui.sender;
+ var senderCollectionListView = senderListEl.data( "view" );
+ if( ! senderCollectionListView || ! senderCollectionListView.collection ) return;
+
+ var newIndex = this._getContainerEl().children().index( ui.item );
+ var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) );
+ senderCollectionListView.collection.remove( modelReceived );
+ this.collection.add( modelReceived, { at : newIndex } );
+ modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value.
+ this.setSelectedModel( modelReceived );
+ },
+
+ _over : function( event, ui ) {
+ // when an item is being dragged into the sortable,
+ // hide the empty list caption if it exists
+ this._getContainerEl().find( "> var.empty-list-caption" ).hide();
+ },
+
+ _onKeydown : function( event ) {
+ if( ! this.processKeyEvents ) return true;
+
+ var trap = false;
+
+ if( this.getSelectedModels( { by : "offset" } ).length == 1 )
+ {
+ // need to trap down and up arrows or else the browser
+ // will end up scrolling a autoscroll div.
+
+ var currentOffset = this.getSelectedModel( { by : "offset" } );
+ if( event.which === this._charCodes.upArrow && currentOffset !== 0 )
+ {
+ this.setSelectedModel( currentOffset - 1, { by : "offset" } );
+ trap = true;
+ }
+ else if( event.which === this._charCodes.downArrow && currentOffset !== this.collection.length - 1 )
+ {
+ this.setSelectedModel( currentOffset + 1, { by : "offset" } );
+ trap = true;
+ }
+ }
+
+ return ! trap;
+ },
+
+ _listItem_onMousedown : function( theEvent ) {
+ if( ! this.selectable || ! this.clickToSelect ) return;
+
+ var clickedItemId = this._getClickedItemId( theEvent );
+
+ if( clickedItemId )
+ {
+ // Exit if an unselectable item was clicked
+ if( _.isFunction( this.selectableModelsFilter ) &&
+ ! this.selectableModelsFilter.call( this, this.collection.get( clickedItemId ) ) )
+ {
+ return;
+ }
+
+ // a selectable list item was clicked
+ if( this.selectMultiple && theEvent.shiftKey )
+ {
+ var firstSelectedItemIndex = -1;
+
+ if( this.selectedItems.length > 0 )
+ {
+ this.collection.find( function( thisItemModel ) {
+ firstSelectedItemIndex++;
+
+ // exit when we find our first selected element
+ return _.contains( this.selectedItems, thisItemModel.cid );
+ }, this );
+ }
+
+ var clickedItemIndex = -1;
+ this.collection.find( function( thisItemModel ) {
+ clickedItemIndex++;
+
+ // exit when we find the clicked element
+ return thisItemModel.cid == clickedItemId;
+ }, this );
+
+ var shiftKeyRootSelectedItemIndex = firstSelectedItemIndex == -1 ? clickedItemIndex : firstSelectedItemIndex;
+ var minSelectedItemIndex = Math.min( clickedItemIndex, shiftKeyRootSelectedItemIndex );
+ var maxSelectedItemIndex = Math.max( clickedItemIndex, shiftKeyRootSelectedItemIndex );
+
+ var newSelectedItems = [];
+ for( var thisIndex = minSelectedItemIndex; thisIndex <= maxSelectedItemIndex; thisIndex ++ )
+ newSelectedItems.push( this.collection.at( thisIndex ).cid );
+ this.setSelectedModels( newSelectedItems, { by : "cid" } );
+
+ // shift clicking will usually highlight selectable text, which we do not want.
+ // this is a cross browser (hopefully) snippet that deselects all text selection.
+ if( document.selection && document.selection.empty )
+ document.selection.empty();
+ else if(window.getSelection) {
+ var sel = window.getSelection();
+ if( sel && sel.removeAllRanges )
+ sel.removeAllRanges();
+ }
+ }
+ else if( this.selectMultiple && ( this.clickToToggle || theEvent.metaKey ) )
+ {
+ if( _.contains( this.selectedItems, clickedItemId ) )
+ this.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : "cid" } );
+ else this.setSelectedModels( _.union( this.selectedItems, clickedItemId ), { by : "cid" } );
+ }
+ else
+ this.setSelectedModels( [ clickedItemId ], { by : "cid" } );
+ }
+ else
+ // the blank area of the list was clicked
+ this.setSelectedModels( [] );
+
+ },
+
+ _listItem_onDoubleClick : function( theEvent ) {
+ var clickedItemId = this._getClickedItemId( theEvent );
+
+ if( clickedItemId )
+ {
+ var clickedModel = this.collection.get( clickedItemId );
+ this.trigger( "doubleClick", clickedModel );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "doubleClick", { clickedModel : clickedModel } );
+ }
+ },
+
+ _listBackground_onClick : function( theEvent ) {
+ if( ! this.selectable ) return;
+ if( ! $( theEvent.target ).is( ".collection-list" ) ) return;
+
+ this.setSelectedModels( [] );
+ }
+
+ }, {
+ setDefaultModelViewConstructor : function( theConstructor ) {
+ mDefaultModelViewConstructor = theConstructor;
+ }
+ });
+
+ // Backbone.ViewOptions
+ // --------------------
+ // v0.2.0
+ //
+ // Copyright (c)2014 Rotunda Software
+ //
+ // https://github.com/rotundasoftware/backbone.viewOptions
+
+ // Backbone.ViewOptions
+ // --------------------
+ //
+ // An plugin to declare and get/set options on views.
+
+ /*
+ * Backbone.ViewOptions, v0.2
+ * Copyright (c)2014 Rotunda Software, LLC.
+ * Distributed under MIT license
+ * http://github.com/rotundasoftware/backbone.viewOptions
+ */
+
+ Backbone.ViewOptions = {};
+
+ Backbone.ViewOptions.add = function( view, optionsDeclarationsProperty ) {
+ if( _.isUndefined( optionsDeclarationsProperty ) ) optionsDeclarationsProperty = "options";
+
+ // ****************** Public methods added to view ******************
+
+ view.setOptions = function( options ) {
+ var _this = this;
+ var optionsThatWereChanged = {};
+ var optionsThatWereChangedOriginalValues = {};
+
+ var optionDeclarations = _.result( this, optionsDeclarationsProperty );
+
+ if( ! _.isUndefined( optionDeclarations ) ) {
+ var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );
+
+ _.each( normalizedOptionDeclarations, function( thisOptionDeclaration ) {
+ thisOptionName = thisOptionDeclaration.name;
+ thisOptionRequired = thisOptionDeclaration.required;
+ thisOptionDefaultValue = thisOptionDeclaration.defaultValue;
+
+ if( thisOptionRequired ) {
+ // note we do not throw an error if a required option is not supplied, but it is
+ // found on the object itself (due to a prior call of view.setOptions, most likely)
+ if( ! options ||
+ ( ( ! _.contains( _.keys( options ), thisOptionName ) && _.isUndefined( _this[ thisOptionName ] ) ) ) ||
+ _.isUndefined( options[ thisOptionName ] ) )
+ throw new Error( "Required option \"" + thisOptionName + "\" was not supplied." );
+ }
+
+ // attach the supplied value of this option, or the appropriate default value, to the view object
+ if( options && thisOptionName in options ) {
+ // if this option already exists on the view, make a note that we will be changing it
+ if( ! _.isUndefined( _this[ thisOptionName ] ) ) {
+ optionsThatWereChangedOriginalValues[ thisOptionName ] = _this[ thisOptionName ];
+ optionsThatWereChanged[ thisOptionName ] = options[ thisOptionName ];
+ }
+ _this[ thisOptionName ] = options[ thisOptionName ];
+ // note we do NOT delete the option off the options object here so that
+ // multiple views can be passed the same options object without issue.
+ }
+ else if( ! _.isUndefined( thisOptionDefaultValue ) && _.isUndefined( _this[ thisOptionName ] ) ) {
+ // note defaults do not write over any existing properties on the view itself.
+ _this[ thisOptionName ] = thisOptionDefaultValue;
+ }
+ } );
+ }
+
+ if( _.keys( optionsThatWereChanged ).length > 0 ) {
+ if( _.isFunction( _this.onOptionsChanged ) )
+ _this.onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedOriginalValues );
+ else if( _.isFunction( _this._onOptionsChanged ) )
+ _this._onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedOriginalValues );
+ }
+ };
+
+ view.getOptions = function() {
+ var optionDeclarations = _.result( this, optionsDeclarationsProperty );
+ if( _.isUndefined( optionDeclarations ) ) return [];
+
+ var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );
+ var optionsNames = _.pluck( normalizedOptionDeclarations, "name" );
+
+ return _.pick( this, optionsNames );
+ };
+ };
+
+ // ****************** Private Utility Functions ******************
+
+ function _normalizeOptionDeclarations( optionDeclarations ) {
+ // convert our short-hand option syntax (with exclamation marks, etc.)
+ // to a simple array of standard option declaration objects.
+ var normalizedOptionDeclarations = [];
+
+ if( ! _.isArray( optionDeclarations ) ) {
+ throw new Error( "Option declarations must be an array." );
+ }
+
+ _.each( optionDeclarations, function( thisOptionDeclaration ) {
+ var thisOptionName, thisOptionRequired, thisOptionDefaultValue;
+
+ thisOptionRequired = false;
+ thisOptionDefaultValue = undefined;
+
+ if( _.isString( thisOptionDeclaration ) )
+ thisOptionName = thisOptionDeclaration;
+ else if( _.isObject( thisOptionDeclaration ) ) {
+ thisOptionName = _.first( _.keys( thisOptionDeclaration ) );
+ thisOptionDefaultValue = _.clone( thisOptionDeclaration[ thisOptionName ] );
+ }
+ else throw new Error( "Each element in the option declarations array must be either a string or an object." );
+
+ if( thisOptionName[ thisOptionName.length - 1 ] === "!" ) {
+ thisOptionRequired = true;
+ thisOptionName = thisOptionName.slice( 0, thisOptionName.length - 1 );
+ }
+
+ normalizedOptionDeclarations.push( {
+ name : thisOptionName,
+ required : thisOptionRequired,
+ defaultValue : thisOptionDefaultValue
+ } );
+ } );
+
+ return normalizedOptionDeclarations;
+ };
+
+
+ // Backbone.BabySitter
+ // -------------------
+ // v0.0.6
+ //
+ // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
+ // Distributed under MIT license
+ //
+ // http://github.com/babysitterjs/backbone.babysitter
+
+ // Backbone.ChildViewContainer
+ // ---------------------------
+ //
+ // Provide a container to store, retrieve and
+ // shut down child views.
+
+ ChildViewContainer = (function(Backbone, _){
+
+ // Container Constructor
+ // ---------------------
+
+ var Container = function(views){
+ this._views = {};
+ this._indexByModel = {};
+ this._indexByCustom = {};
+ this._updateLength();
+
+ _.each(views, this.add, this);
+ };
+
+ // Container Methods
+ // -----------------
+
+ _.extend(Container.prototype, {
+
+ // Add a view to this container. Stores the view
+ // by `cid` and makes it searchable by the model
+ // cid (and model itself). Optionally specify
+ // a custom key to store an retrieve the view.
+ add: function(view, customIndex){
+ var viewCid = view.cid;
+
+ // store the view
+ this._views[viewCid] = view;
+
+ // index it by model
+ if (view.model){
+ this._indexByModel[view.model.cid] = viewCid;
+ }
+
+ // index by custom
+ if (customIndex){
+ this._indexByCustom[customIndex] = viewCid;
+ }
+
+ this._updateLength();
+ },
+
+ // Find a view by the model that was attached to
+ // it. Uses the model's `cid` to find it.
+ findByModel: function(model){
+ return this.findByModelCid(model.cid);
+ },
+
+ // Find a view by the `cid` of the model that was attached to
+ // it. Uses the model's `cid` to find the view `cid` and
+ // retrieve the view using it.
+ findByModelCid: function(modelCid){
+ var viewCid = this._indexByModel[modelCid];
+ return this.findByCid(viewCid);
+ },
+
+ // Find a view by a custom indexer.
+ findByCustom: function(index){
+ var viewCid = this._indexByCustom[index];
+ return this.findByCid(viewCid);
+ },
+
+ // Find by index. This is not guaranteed to be a
+ // stable index.
+ findByIndex: function(index){
+ return _.values(this._views)[index];
+ },
+
+ // retrieve a view by it's `cid` directly
+ findByCid: function(cid){
+ return this._views[cid];
+ },
+
+ findIndexByCid : function( cid ) {
+ var index = -1;
+ var view = _.find( this._views, function ( view ) {
+ index++;
+ if( view.model.cid == cid )
+ return view;
+ } );
+ return ( view ) ? index : -1;
+ },
+
+ // Remove a view
+ remove: function(view){
+ var viewCid = view.cid;
+
+ // delete model index
+ if (view.model){
+ delete this._indexByModel[view.model.cid];
+ }
+
+ // delete custom index
+ _.any(this._indexByCustom, function(cid, key) {
+ if (cid === viewCid) {
+ delete this._indexByCustom[key];
+ return true;
+ }
+ }, this);
+
+ // remove the view from the container
+ delete this._views[viewCid];
+
+ // update the length
+ this._updateLength();
+ },
+
+ // Call a method on every view in the container,
+ // passing parameters to the call method one at a
+ // time, like `function.call`.
+ call: function(method){
+ this.apply(method, _.tail(arguments));
+ },
+
+ // Apply a method on every view in the container,
+ // passing parameters to the call method one at a
+ // time, like `function.apply`.
+ apply: function(method, args){
+ _.each(this._views, function(view){
+ if (_.isFunction(view[method])){
+ view[method].apply(view, args || []);
+ }
+ });
+ },
+
+ // Update the `.length` attribute on this container
+ _updateLength: function(){
+ this.length = _.size(this._views);
+ }
+ });
+
+ // Borrowing this code from Backbone.Collection:
+ // http://backbonejs.org/docs/backbone.html#section-106
+ //
+ // Mix in methods from Underscore, for iteration, and other
+ // collection related features.
+ var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
+ 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
+ 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
+ 'last', 'without', 'isEmpty', 'pluck'];
+
+ _.each(methods, function(method) {
+ Container.prototype[method] = function() {
+ var views = _.values(this._views);
+ var args = [views].concat(_.toArray(arguments));
+ return _[method].apply(_, args);
+ };
+ });
+
+ // return the public API
+ return Container;
+ })(Backbone, _);
+} ) );
\ No newline at end of file
diff --git a/ajax/libs/backbone.collectionView/0.9.2/backbone.collectionView.min.js b/ajax/libs/backbone.collectionView/0.9.2/backbone.collectionView.min.js
new file mode 100644
index 000000000..456c9605b
--- /dev/null
+++ b/ajax/libs/backbone.collectionView/0.9.2/backbone.collectionView.min.js
@@ -0,0 +1,8 @@
+/*!
+* Backbone.CollectionView, v0.9.1
+* Copyright (c)2013 Rotunda Software, LLC.
+* Distributed under MIT license
+* http://github.com/rotundasoftware/backbone-collection-view
+*/
+
+(function(e,t){"function"==typeof define&&define.amd?define(["underscore","backbone","jquery"],t):"undefined"!=typeof exports?module.exports=t(require("underscore"),require("backbone"),require("backbone").$):t(e._,e.Backbone,e.jQuery||e.Zepto||e.$)})(this,function(e,t,i){function s(t){var i=[];if(!e.isArray(t))throw Error("Option declarations must be an array.");return e.each(t,function(t){var s,n,o;if(n=!1,o=void 0,e.isString(t))s=t;else{if(!e.isObject(t))throw Error("Each element in the option declarations array must be either a string or an object.");s=e.first(e.keys(t)),o=e.clone(t[s])}"!"===s[s.length-1]&&(n=!0,s=s.slice(0,s.length-1)),i.push({name:s,required:n,defaultValue:o})}),i}var n=t.View,o="model",l=["collection","modelView","modelViewOptions","itemTemplate","selectableModelsFilter","sortableModelsFilter","visibleModelsFilter","itemTemplateFunction","detachedRendering","sortableOptions"],a={background:"transparent",border:"none","box-shadow":"none"};t.CollectionView=t.View.extend({tagName:"ul",events:{"mousedown li, td":"_listItem_onMousedown","dblclick li, td":"_listItem_onDoubleClick",click:"_listBackground_onClick","click ul.collection-list, table.collection-list":"_listBackground_onClick",keydown:"_onKeydown"},spawnMessages:{focus:"focus"},passMessages:{"*":"."},initializationOptions:[{collection:new t.Collection},{modelView:null},{modelViewOptions:{}},{itemTemplate:null},{itemTemplateFunction:null},{selectable:!0},{clickToSelect:!0},{selectableModelsFilter:null},{visibleModelsFilter:null},{sortableModelsFilter:null},{selectMultiple:!1},{clickToToggle:!1},{processKeyEvents:!0},{sortable:!1},{sortableOptions:null},{detachedRendering:!1},{emptyListCaption:null}],initialize:function(e){t.ViewOptions.add(this,"initializationOptions"),this.setOptions(e),this._hasBeenRendered=!1,this._isBackboneCourierAvailable()&&t.Courier.add(this),this.$el.data("view",this),this.$el.addClass("collection-list"),this.selectable&&this.$el.addClass("selectable"),this.processKeyEvents&&this.$el.attr("tabindex",0),this.selectedItems=[],this._updateItemTemplate(),this.collection&&this._registerCollectionEvents(),this.viewManager=new ChildViewContainer},onOptionsChanged:function(t,i){var s=!1,n=this;e.each(e.keys(t),function(o){var a=t[o],d=i[o];switch(o){case"collection":a!==d&&(n.stopListening(d),n._registerCollectionEvents());break;case"selectMultiple":!a&&n.selectedItems.length>1&&n.setSelectedModel(e.first(n.selectedItems),{by:"cid"});break;case"selectable":!a&&n.selectedItems.length>0&&n.setSelectedModels([]);break;case"selectableModelsFilter":a&&e.isFunction(a)&&n._validateSelection();break;case"itemTemplate":n._updateItemTemplate();break;case"processKeyEvents":a&&n.$el.attr("tabindex",0);break;case"modelView":n.viewManager.each(function(e){n.viewManager.remove(e),e.remove()})}e.contains(l,o)&&(s=!0)}),this._hasBeenRendered&&s&&this.render()},setOption:function(e,t){var i={};i[e]=t,this.setOptions(i)},getSelectedModel:function(t){return e.first(this.getSelectedModels(t))},getSelectedModels:function(t){var s=this;t=e.extend({},{by:o},t);var n=t.by,l=[];switch(n){case"id":e.each(this.selectedItems,function(e){l.push(s.collection.get(e).id)});break;case"cid":l=l.concat(this.selectedItems);break;case"offset":var a=0,d=this._getVisibleItemEls();d.each(function(){var e=i(this);e.is(".selected")&&l.push(a),a++});break;case"model":e.each(this.selectedItems,function(e){l.push(s.collection.get(e))});break;case"view":e.each(this.selectedItems,function(e){l.push(s.viewManager.findByModel(s.collection.get(e)))})}return l},setSelectedModels:function(t,s){if(!e.isArray(t))throw"Invalid parameter value";if(this.selectable||!(t.length>0)){s=e.extend({},{silent:!1,by:o},s);var n=s.by,l=[];switch(n){case"cid":l=t;break;case"id":this.collection.each(function(i){e.contains(t,i.id)&&l.push(i.cid)});break;case"model":l=e.pluck(t,"cid");break;case"view":e.each(t,function(e){l.push(e.model.cid)});break;case"offset":var a=0,d=this._getVisibleItemEls();d.each(function(){var s=i(this);e.contains(t,a)&&l.push(s.attr("data-model-cid")),a++})}var r=this.getSelectedModels(),c=e.clone(this.selectedItems);this.selectedItems=this._convertStringsToInts(l),this._validateSelection();var h=this.getSelectedModels();this._containSameElements(c,this.selectedItems)||(this._addSelectedClassToSelectedItems(c),s.silent||(this.trigger("selectionChanged",h,r),this._isBackboneCourierAvailable()&&this.spawn("selectionChanged",{selectedModels:h,oldSelectedModels:r})),this.updateDependentControls())}},setSelectedModel:function(e,t){e||0===e?this.setSelectedModels([e],t):this.setSelectedModels([],t)},render:function(){var t=this;this._hasBeenRendered=!0,this.selectable&&this._saveSelection();var i;i=this._getContainerEl();var s=this.viewManager;this.viewManager=new ChildViewContainer,s.each(function(e){t.collection.get(e.model.cid)?e.$el.detach():e.remove()}),i.empty();var n;if(this.detachedRendering&&(n=document.createDocumentFragment()),this.collection.each(function(t){var o=s.findByModelCid(t.cid);e.isUndefined(o)&&(o=this._createNewModelView(t,this._getModelViewOptions(t))),this._insertAndRenderModelView(o,n||i)},this),this.detachedRendering&&i.append(n),this.sortable){var o=e.extend({axis:"y",distance:10,forcePlaceholderSize:!0,start:e.bind(this._sortStart,this),change:e.bind(this._sortChange,this),stop:e.bind(this._sortStop,this),receive:e.bind(this._receive,this),over:e.bind(this._over,this)},e.result(this,"sortableOptions"));t._isRenderedAsTable()?o.items="> tbody > tr:not(.not-sortable)":t._isRenderedAsList()&&(o.items="> li:not(.not-sortable)"),this.$el=this.$el.sortable(o)}this._showEmptyListCaptionIfAppropriate(),this.trigger("render"),this._isBackboneCourierAvailable()&&this.spawn("render"),this.selectable&&(this._restoreSelection(),this.updateDependentControls()),e.isFunction(this.onAfterRender)&&this.onAfterRender()},_showEmptyListCaptionIfAppropriate:function(){if(this.emptyListCaption){var t=this._getVisibleItemEls();if(0===t.length){var s;s=e.isFunction(this.emptyListCaption)?this.emptyListCaption():this.emptyListCaption;var n=i(""+s+"");$emptyListCaptionEl=this._isRenderedAsList()?n.wrapAll("").parent().css(a):n.wrapAll(" |
").parent().parent().css(a),this._getContainerEl().append($emptyListCaptionEl)}}},_removeEmptyListCaption:function(){this._isRenderedAsList()?this._getContainerEl().find("> li > var.empty-list-caption").parent().remove():this._getContainerEl().find("> tr > td > var.empty-list-caption").parent().parent().remove()},_insertAndRenderModelView:function(t,i,s){var n=this._wrapModelView(t);11===i.nodeType?i.appendChild(n.get(0)):!e.isUndefined(s)&&s>0&&this.collection.length-1>s?i.children().eq(s).before(n):i.append(n);var o=t.render();o===!1&&(n.hide(),n.addClass("not-visible"));var l=!1;e.isFunction(this.visibleModelsFilter)&&(l=!this.visibleModelsFilter(t.model),l&&(1===n.children().length?n.hide():t.$el.hide(),n.addClass("not-visible"))),!l&&this.emptyListCaption&&this._removeEmptyListCaption(),this.viewManager.add(t)},updateDependentControls:function(){this.trigger("updateDependentControls",this.getSelectedModels()),this._isBackboneCourierAvailable()&&this.spawn("updateDependentControls",{selectedModels:this.getSelectedModels()})},remove:function(){this.viewManager.each(function(e){e.remove()}),t.View.prototype.remove.apply(this,arguments)},_removeModelView:function(e){var t=this.viewManager,i=t.findByModelCid(e.cid);this.selectable&&this._saveSelection(),t.remove(i),i.remove(),this._getContainerEl().children("[data-model-cid="+e.cid+"]").remove(),this.selectable&&this._restoreSelection(),this._showEmptyListCaptionIfAppropriate()},_validateSelectionAndRender:function(){this._validateSelection(),this.render()},_registerCollectionEvents:function(){this.listenTo(this.collection,"add",function(e){if(this._hasBeenRendered){var t=this._createNewModelView(e,this._getModelViewOptions(e));this._insertAndRenderModelView(t,this._getContainerEl(),this.collection.indexOf(e))}this._isBackboneCourierAvailable()&&this.spawn("add")}),this.listenTo(this.collection,"remove",function(e){this._hasBeenRendered&&this._removeModelView(e),this._isBackboneCourierAvailable()&&this.spawn("remove")}),this.listenTo(this.collection,"reset",function(){this._hasBeenRendered&&this.render(),this._isBackboneCourierAvailable()&&this.spawn("reset")}),this.listenTo(this.collection,"sort",function(e,t){this._hasBeenRendered&&t.add!==!0&&this.render(),this._isBackboneCourierAvailable()&&this.spawn("sort")})},_getContainerEl:function(){if(this._isRenderedAsTable()){var e=this.$el.find("> tbody");if(e.length>0)return e}return this.$el},_getClickedItemId:function(e){var t=null,s=i(e.currentTarget);if(s.closest(".collection-list").get(0)===this.$el.get(0)){var n=s.closest("[data-model-cid]");return n.length>0&&(t=n.attr("data-model-cid"),i.isNumeric(t)&&(t=parseInt(t,10))),t}},_updateItemTemplate:function(){var t;if(this.itemTemplate){if(0===i(this.itemTemplate).length)throw"Could not find item template from selector: "+this.itemTemplate;t=i(this.itemTemplate).html()}else t=this.$(".item-template").html();t&&(this.itemTemplateFunction=e.template(t))},_validateSelection:function(){var t=e.pluck(this.collection.models,"cid");this.selectedItems=e.intersection(t,this.selectedItems),e.isFunction(this.selectableModelsFilter)&&(this.selectedItems=e.filter(this.selectedItems,function(e){return this.selectableModelsFilter.call(this,this.collection.get(e))},this))},_saveSelection:function(){if(!this.selectable)throw"Attempt to save selection on non-selectable list";this.savedSelection={items:e.clone(this.selectedItems),offset:this.getSelectedModel({by:"offset"})}},_restoreSelection:function(){if(!this.savedSelection)throw"Attempt to restore selection but no selection has been saved!";this.setSelectedModels([],{silent:!0}),this.savedSelection.items.length>0&&(this.setSelectedModels(this.savedSelection.items,{by:"cid",silent:!0}),0===this.selectedItems.length&&this.setSelectedModel(this.savedSelection.offset,{by:"offset"}),this.selectedItems.length!==this.savedSelection.items.length&&(this.trigger("selectionChanged",this.getSelectedModels(),[]),this._isBackboneCourierAvailable()&&this.spawn("selectionChanged",{selectedModels:this.getSelectedModels(),oldSelectedModels:[]}))),delete this.savedSelection},_addSelectedClassToSelectedItems:function(t){e.isUndefined(t)&&(t=[]);var i=t;i=e.without(i,this.selectedItems),e.each(i,function(e){this._getContainerEl().find("[data-model-cid="+e+"]").removeClass("selected")},this);var s=this.selectedItems;s=e.without(s,t),e.each(s,function(e){this._getContainerEl().find("[data-model-cid="+e+"]").addClass("selected")},this)},_reorderCollectionBasedOnHTML:function(){var e=this;this._getContainerEl().children().each(function(){var t=i(this).attr("data-model-cid");if(t){var s=e.collection.get(t);s&&(e.collection.remove(s,{silent:!0}),e.collection.add(s,{silent:!0,sort:!e.collection.comparator}))}}),this.collection.trigger("reorder"),this._isBackboneCourierAvailable()&&this.spawn("reorder"),this.collection.comparator&&this.collection.sort()},_getModelViewConstructor:function(){return this.modelView||n},_getModelViewOptions:function(t){return e.extend({model:t},this.modelViewOptions)},_createNewModelView:function(t,i){var s=this._getModelViewConstructor(t);if(e.isUndefined(s))throw"Could not find modelView constructor for model";var n=new s(i);return n.collectionListView=this,n},_wrapModelView:function(t){var i,s=this;return this._isRenderedAsTable()?i=t.$el.attr("data-model-cid",t.model.cid):this._isRenderedAsList()&&(i="li"===t.$el.prop("tagName").toLowerCase()?t.$el.attr("data-model-cid",t.model.cid):t.$el.wrapAll("").parent()),e.isFunction(this.sortableModelsFilter)&&(this.sortableModelsFilter.call(s,t.model)||i.addClass("not-sortable")),e.isFunction(this.selectableModelsFilter)&&(this.selectableModelsFilter.call(s,t.model)||i.addClass("not-selectable")),i},_convertStringsToInts:function(t){return e.map(t,function(t){if(!e.isString(t))return t;var i=parseInt(t,10);return i==t?i:t})},_containSameElements:function(t,i){if(t.length!=i.length)return!1;var s=e.intersection(t,i).length;return s==t.length},_isRenderedAsTable:function(){return"table"===this.$el.prop("tagName").toLowerCase()},_isRenderedAsList:function(){return!this._isRenderedAsTable()},_getVisibleItemEls:function(){var e=[];return e=this._getContainerEl().find("> [data-model-cid]:not(.not-visible)")},_charCodes:{upArrow:38,downArrow:40},_isBackboneCourierAvailable:function(){return!e.isUndefined(t.Courier)},_sortStart:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid"));this.trigger("sortStart",i),this._isBackboneCourierAvailable()&&this.spawn("sortStart",{modelBeingSorted:i})},_sortChange:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid"));this.trigger("sortChange",i),this._isBackboneCourierAvailable()&&this.spawn("sortChange",{modelBeingSorted:i})},_sortStop:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid")),s=this._getContainerEl(),n=s.children().index(t.item);-1==n&&this.collection.remove(i),this._reorderCollectionBasedOnHTML(),this.updateDependentControls(),this.trigger("sortStop",i,n),this._isBackboneCourierAvailable()&&this.spawn("sortStop",{modelBeingSorted:i,newIndex:n})},_receive:function(e,t){var i=t.sender,s=i.data("view");if(s&&s.collection){var n=this._getContainerEl().children().index(t.item),o=s.collection.get(t.item.attr("data-model-cid"));s.collection.remove(o),this.collection.add(o,{at:n}),o.collection=this.collection,this.setSelectedModel(o)}},_over:function(){this._getContainerEl().find("> var.empty-list-caption").hide()},_onKeydown:function(e){if(!this.processKeyEvents)return!0;var t=!1;if(1==this.getSelectedModels({by:"offset"}).length){var i=this.getSelectedModel({by:"offset"});e.which===this._charCodes.upArrow&&0!==i?(this.setSelectedModel(i-1,{by:"offset"}),t=!0):e.which===this._charCodes.downArrow&&i!==this.collection.length-1&&(this.setSelectedModel(i+1,{by:"offset"}),t=!0)}return!t},_listItem_onMousedown:function(t){if(this.selectable&&this.clickToSelect){var i=this._getClickedItemId(t);if(i){if(e.isFunction(this.selectableModelsFilter)&&!this.selectableModelsFilter.call(this,this.collection.get(i)))return;if(this.selectMultiple&&t.shiftKey){var s=-1;this.selectedItems.length>0&&this.collection.find(function(t){return s++,e.contains(this.selectedItems,t.cid)},this);var n=-1;this.collection.find(function(e){return n++,e.cid==i},this);for(var o=-1==s?n:s,l=Math.min(n,o),a=Math.max(n,o),d=[],r=l;a>=r;r++)d.push(this.collection.at(r).cid);if(this.setSelectedModels(d,{by:"cid"}),document.selection&&document.selection.empty)document.selection.empty();else if(window.getSelection){var c=window.getSelection();c&&c.removeAllRanges&&c.removeAllRanges()}}else this.selectMultiple&&(this.clickToToggle||t.metaKey)?e.contains(this.selectedItems,i)?this.setSelectedModels(e.without(this.selectedItems,i),{by:"cid"}):this.setSelectedModels(e.union(this.selectedItems,i),{by:"cid"}):this.setSelectedModels([i],{by:"cid"})}else this.setSelectedModels([])}},_listItem_onDoubleClick:function(e){var t=this._getClickedItemId(e);if(t){var i=this.collection.get(t);this.trigger("doubleClick",i),this._isBackboneCourierAvailable()&&this.spawn("doubleClick",{clickedModel:i})}},_listBackground_onClick:function(e){this.selectable&&i(e.target).is(".collection-list")&&this.setSelectedModels([])}},{setDefaultModelViewConstructor:function(e){n=e}}),t.ViewOptions={},t.ViewOptions.add=function(t,i){e.isUndefined(i)&&(i="options"),t.setOptions=function(t){var n=this,o={},l={},a=e.result(this,i);if(!e.isUndefined(a)){var d=s(a);e.each(d,function(i){if(thisOptionName=i.name,thisOptionRequired=i.required,thisOptionDefaultValue=i.defaultValue,thisOptionRequired&&(!t||!e.contains(e.keys(t),thisOptionName)&&e.isUndefined(n[thisOptionName])||e.isUndefined(t[thisOptionName])))throw Error('Required option "'+thisOptionName+'" was not supplied.');t&&thisOptionName in t?(e.isUndefined(n[thisOptionName])||(l[thisOptionName]=n[thisOptionName],o[thisOptionName]=t[thisOptionName]),n[thisOptionName]=t[thisOptionName]):!e.isUndefined(thisOptionDefaultValue)&&e.isUndefined(n[thisOptionName])&&(n[thisOptionName]=thisOptionDefaultValue)})}e.keys(o).length>0&&(e.isFunction(n.onOptionsChanged)?n.onOptionsChanged(o,l):e.isFunction(n._onOptionsChanged)&&n._onOptionsChanged(o,l))},t.getOptions=function(){var t=e.result(this,i);if(e.isUndefined(t))return[];var n=s(t),o=e.pluck(n,"name");return e.pick(this,o)}},ChildViewContainer=function(e,t){var i=function(e){this._views={},this._indexByModel={},this._indexByCustom={},this._updateLength(),t.each(e,this.add,this)};t.extend(i.prototype,{add:function(e,t){var i=e.cid;this._views[i]=e,e.model&&(this._indexByModel[e.model.cid]=i),t&&(this._indexByCustom[t]=i),this._updateLength()},findByModel:function(e){return this.findByModelCid(e.cid)},findByModelCid:function(e){var t=this._indexByModel[e];return this.findByCid(t)},findByCustom:function(e){var t=this._indexByCustom[e];return this.findByCid(t)},findByIndex:function(e){return t.values(this._views)[e]},findByCid:function(e){return this._views[e]},findIndexByCid:function(e){var i=-1,s=t.find(this._views,function(t){return i++,t.model.cid==e?t:void 0});return s?i:-1},remove:function(e){var i=e.cid;e.model&&delete this._indexByModel[e.model.cid],t.any(this._indexByCustom,function(e,t){return e===i?(delete this._indexByCustom[t],!0):void 0},this),delete this._views[i],this._updateLength()},call:function(e){this.apply(e,t.tail(arguments))},apply:function(e,i){t.each(this._views,function(s){t.isFunction(s[e])&&s[e].apply(s,i||[])})},_updateLength:function(){this.length=t.size(this._views)}});var s=["forEach","each","map","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","toArray","first","initial","rest","last","without","isEmpty","pluck"];return t.each(s,function(e){i.prototype[e]=function(){var i=t.values(this._views),s=[i].concat(t.toArray(arguments));return t[e].apply(t,s)}}),i}(t,e)});
\ No newline at end of file
diff --git a/ajax/libs/backbone.collectionView/0.9.3/backbone.collectionView.js b/ajax/libs/backbone.collectionView/0.9.3/backbone.collectionView.js
new file mode 100644
index 000000000..28fea7a0e
--- /dev/null
+++ b/ajax/libs/backbone.collectionView/0.9.3/backbone.collectionView.js
@@ -0,0 +1,1243 @@
+/*!
+* Backbone.CollectionView, v0.9.2
+* Copyright (c)2013 Rotunda Software, LLC.
+* Distributed under MIT license
+* http://github.com/rotundasoftware/backbone-collection-view
+*/
+
+( function( root, factory ) {
+ // UMD wrapper
+ if ( typeof define === 'function' && define.amd ) {
+ // AMD
+ define( [ 'underscore', 'backbone', 'jquery' ], factory );
+ } else if ( typeof exports !== 'undefined' ) {
+ // Node/CommonJS
+ module.exports = factory( require('underscore' ), require( 'backbone' ), require( 'backbone' ).$ );
+ } else {
+ // Browser globals
+ factory( root._, root.Backbone, ( root.jQuery || root.Zepto || root.$ ) );
+ }
+}( this, function( _, Backbone, $ ) {
+ var mDefaultModelViewConstructor = Backbone.View;
+
+ var kDefaultReferenceBy = "model";
+
+ var kOptionsRequiringRerendering = [ "collection", "modelView", "modelViewOptions", "itemTemplate", "selectableModelsFilter", "sortableModelsFilter", "visibleModelsFilter", "itemTemplateFunction", "detachedRendering", "sortableOptions" ];
+
+ var kStylesForEmptyListCaption = {
+ "background" : "transparent",
+ "border" : "none",
+ "box-shadow" : "none"
+ };
+
+ Backbone.CollectionView = Backbone.View.extend( {
+
+ tagName : "ul",
+
+ events : {
+ "mousedown li, td" : "_listItem_onMousedown",
+ "dblclick li, td" : "_listItem_onDoubleClick",
+ "click" : "_listBackground_onClick",
+ "click ul.collection-list, table.collection-list" : "_listBackground_onClick",
+ "keydown" : "_onKeydown"
+ },
+
+ // only used if Backbone.Courier is available
+ spawnMessages : {
+ "focus" : "focus"
+ },
+
+ //only used if Backbone.Courier is available
+ passMessages : { "*" : "." },
+
+ // viewOption definitions with default values.
+ initializationOptions : [
+ { "collection" : new Backbone.Collection() },
+ { "modelView" : null },
+ { "modelViewOptions" : {} },
+ { "itemTemplate" : null },
+ { "itemTemplateFunction" : null },
+ { "selectable" : true },
+ { "clickToSelect" : true },
+ { "selectableModelsFilter" : null },
+ { "visibleModelsFilter" : null },
+ { "sortableModelsFilter" : null },
+ { "selectMultiple" : false },
+ { "clickToToggle" : false },
+ { "processKeyEvents" : true },
+ { "sortable" : false },
+ { "sortableOptions" : null },
+ { "detachedRendering" : false },
+ { "emptyListCaption" : null }
+ ],
+
+ initialize : function( options ) {
+ Backbone.ViewOptions.add( this, "initializationOptions" ); // setup the ViewOptions functionality.
+ this.setOptions( options ); // and make use of any provided options
+
+ this._hasBeenRendered = false;
+
+ if( this._isBackboneCourierAvailable() ) {
+ Backbone.Courier.add( this );
+ }
+
+ this.$el.data( "view", this ); // needed for connected sortable lists
+ this.$el.addClass( "collection-list" );
+ if( this.selectable ) this.$el.addClass( "selectable" );
+
+ if( this.processKeyEvents )
+ this.$el.attr( "tabindex", 0 ); // so we get keyboard events
+
+ this.selectedItems = [];
+
+ this._updateItemTemplate();
+
+ if( this.collection )
+ this._registerCollectionEvents();
+
+ this.viewManager = new ChildViewContainer();
+ },
+
+ onOptionsChanged : function( changedOptions, originalOptions ) {
+ var rerender = false;
+ var _this = this;
+ _.each( _.keys( changedOptions ), function( changedOptionKey ) {
+ var newVal = changedOptions[ changedOptionKey ];
+ var oldVal = originalOptions[ changedOptionKey ];
+ switch( changedOptionKey ) {
+ case "collection" :
+ if ( newVal !== oldVal ) {
+ _this.stopListening( oldVal );
+ _this._registerCollectionEvents();
+ }
+ break;
+ case "selectMultiple":
+ if( ! newVal && _this.selectedItems.length > 1 )
+ _this.setSelectedModel( _.first( _this.selectedItems ), { by : "cid" } );
+ break;
+ case "selectable" :
+ if( ! newVal && _this.selectedItems.length > 0 )
+ _this.setSelectedModels( [] );
+ break;
+ case "selectableModelsFilter" :
+ if( newVal && _.isFunction( newVal ) )
+ _this._validateSelection();
+ break;
+ case "itemTemplate" :
+ _this._updateItemTemplate();
+ break;
+ case "processKeyEvents" :
+ if( newVal ) _this.$el.attr( "tabindex", 0 ); // so we get keyboard events
+ break;
+ case "modelView" :
+ //need to remove all old view instances
+ _this.viewManager.each( function( view ) {
+ _this.viewManager.remove( view );
+ // destroy the View itself
+ view.remove();
+ } );
+ break;
+ }
+ if( _.contains( kOptionsRequiringRerendering, changedOptionKey ) ) rerender = true;
+ });
+ if( this._hasBeenRendered && rerender ) {
+ this.render(); // Rerender the view if the rerender flag has been set.
+ }
+ },
+
+ setOption : function( optionName, optionValue ) { // now is mearly a wrapper around backbone.viewOptions' setOptions()
+ var optionHash = {};
+ optionHash[ optionName ] = optionValue;
+ this.setOptions( optionHash );
+ },
+
+ getSelectedModel : function( options ) {
+ return _.first( this.getSelectedModels( options ) );
+ },
+
+ getSelectedModels : function ( options ) {
+ var _this = this;
+
+ options = _.extend( {}, {
+ by : kDefaultReferenceBy
+ }, options );
+
+ var referenceBy = options.by;
+ var items = [];
+
+ switch( referenceBy ) {
+ case "id" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.collection.get( item ).id );
+ } );
+ break;
+ case "cid" :
+ items = items.concat( this.selectedItems );
+ break;
+ case "offset" :
+ var curLineNumber = 0;
+
+ var itemElements = this._getVisibleItemEls();
+
+ itemElements.each( function() {
+ var thisItemEl = $( this );
+ if( thisItemEl.is( ".selected" ) )
+ items.push( curLineNumber );
+ curLineNumber++;
+ } );
+ break;
+ case "model" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.collection.get( item ) );
+ } );
+ break;
+ case "view" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.viewManager.findByModel( _this.collection.get( item ) ) );
+ } );
+ break;
+ }
+
+ return items;
+
+ },
+
+ setSelectedModels : function( newSelectedItems, options ) {
+ if( ! _.isArray( newSelectedItems ) ) throw "Invalid parameter value";
+ if( ! this.selectable && newSelectedItems.length > 0 ) return; // used to throw error, but there are some circumstances in which a list can be selectable at times and not at others, don't want to have to worry about catching errors
+
+ options = _.extend( {}, {
+ silent : false,
+ by : kDefaultReferenceBy
+ }, options );
+
+ var referenceBy = options.by;
+ var newSelectedCids = [];
+
+ switch( referenceBy ) {
+ case "cid" :
+ newSelectedCids = newSelectedItems;
+ break;
+ case "id" :
+ this.collection.each( function( thisModel ) {
+ if( _.contains( newSelectedItems, thisModel.id ) ) newSelectedCids.push( thisModel.cid );
+ } );
+ break;
+ case "model" :
+ newSelectedCids = _.pluck( newSelectedItems, "cid" );
+ break;
+ case "view" :
+ _.each( newSelectedItems, function( item ) {
+ newSelectedCids.push( item.model.cid );
+ } );
+ break;
+ case "offset" :
+ var curLineNumber = 0;
+ var selectedItems = [];
+
+ var itemElements = this._getVisibleItemEls();
+ itemElements.each( function() {
+ var thisItemEl = $( this );
+ if( _.contains( newSelectedItems, curLineNumber ) )
+ newSelectedCids.push( thisItemEl.attr( "data-model-cid" ) );
+ curLineNumber++;
+ } );
+ break;
+ }
+
+ var oldSelectedModels = this.getSelectedModels();
+ var oldSelectedCids = _.clone( this.selectedItems );
+
+ this.selectedItems = this._convertStringsToInts( newSelectedCids );
+ this._validateSelection();
+
+ var newSelectedModels = this.getSelectedModels();
+
+ if( ! this._containSameElements( oldSelectedCids, this.selectedItems ) )
+ {
+ this._addSelectedClassToSelectedItems( oldSelectedCids );
+
+ if( ! options.silent )
+ {
+ this.trigger( "selectionChanged", newSelectedModels, oldSelectedModels );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "selectionChanged", {
+ selectedModels : newSelectedModels,
+ oldSelectedModels : oldSelectedModels
+ } );
+ }
+ }
+
+ this.updateDependentControls();
+ }
+ },
+
+ setSelectedModel : function( newSelectedItem, options ) {
+ if( ! newSelectedItem && newSelectedItem !== 0 )
+ this.setSelectedModels( [], options );
+ else
+ this.setSelectedModels( [ newSelectedItem ], options );
+ },
+
+ render : function(){
+ var _this = this;
+
+ this._hasBeenRendered = true;
+
+ if( this.selectable ) this._saveSelection();
+
+ var modelViewContainerEl;
+
+ // If collection view element is a table and it has a tbody
+ // within it, render the model views inside of the tbody
+ modelViewContainerEl = this._getContainerEl();
+
+ var oldViewManager = this.viewManager;
+ this.viewManager = new ChildViewContainer();
+
+ // detach each of our subviews that we have already created to represent models
+ // in the collection. We are going to re-use the ones that represent models that
+ // are still here, instead of creating new ones, so that we don't loose state
+ // information in the views.
+ oldViewManager.each( function( thisModelView ) {
+ // to boost performance, only detach those views that will be sticking around.
+ // we won't need the other ones later, so no need to detach them individually.
+ if( _this.collection.get( thisModelView.model.cid ) )
+ thisModelView.$el.detach();
+ else
+ thisModelView.remove();
+ } );
+
+ modelViewContainerEl.empty();
+ var fragmentContainer;
+
+ if( this.detachedRendering )
+ fragmentContainer = document.createDocumentFragment();
+
+ this.collection.each( function( thisModel ) {
+ var thisModelView = oldViewManager.findByModelCid( thisModel.cid );
+ if( _.isUndefined( thisModelView ) ) {
+ // if the model view has not already been created on a
+ // previous render then create and initialize it now.
+ thisModelView = this._createNewModelView( thisModel, this._getModelViewOptions( thisModel ) );
+ }
+
+ this._insertAndRenderModelView( thisModelView, fragmentContainer || modelViewContainerEl );
+ }, this );
+
+ if( this.detachedRendering )
+ modelViewContainerEl.append( fragmentContainer );
+
+ if( this.sortable )
+ {
+ var sortableOptions = _.extend( {
+ axis: "y",
+ distance: 10,
+ forcePlaceholderSize : true,
+ start : _.bind( this._sortStart, this ),
+ change : _.bind( this._sortChange, this ),
+ stop : _.bind( this._sortStop, this ),
+ receive : _.bind( this._receive, this ),
+ over : _.bind( this._over, this )
+ }, _.result( this, "sortableOptions" ) );
+
+ if( _this._isRenderedAsTable() ) {
+ sortableOptions.items = "> tbody > tr:not(.not-sortable)";
+ }
+ else if( _this._isRenderedAsList() ) {
+ sortableOptions.items = "> li:not(.not-sortable)";
+ }
+
+ this.$el = this.$el.sortable( sortableOptions );
+ }
+
+ this._showEmptyListCaptionIfAppropriate();
+
+ this.trigger( "render" );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "render" );
+
+ if( this.selectable ) {
+ this._restoreSelection();
+ this.updateDependentControls();
+ }
+
+ if( _.isFunction( this.onAfterRender ) )
+ this.onAfterRender();
+ },
+
+ _showEmptyListCaptionIfAppropriate : function ( ) {
+ if( this.emptyListCaption ) {
+ var visibleEls = this._getVisibleItemEls();
+
+ if( visibleEls.length === 0 ) {
+ var emptyListString;
+
+ if( _.isFunction( this.emptyListCaption ) )
+ emptyListString = this.emptyListCaption();
+ else
+ emptyListString = this.emptyListCaption;
+
+ var $emptyCaptionEl;
+ var $varEl = $( "" + emptyListString + "" );
+
+ //need to wrap the empty caption to make it fit the rendered list structure (either with an li or a tr td)
+ if( this._isRenderedAsList() )
+ $emptyListCaptionEl = $varEl.wrapAll( "" ).parent().css( kStylesForEmptyListCaption );
+ else
+ $emptyListCaptionEl = $varEl.wrapAll( " |
" ).parent().parent().css( kStylesForEmptyListCaption );
+
+ this._getContainerEl().append( $emptyListCaptionEl );
+ }
+ }
+ },
+
+ _removeEmptyListCaption : function( ) {
+ if( this._isRenderedAsList() )
+ this._getContainerEl().find( "> li > var.empty-list-caption" ).parent().remove();
+ else
+ this._getContainerEl().find( "> tr > td > var.empty-list-caption" ).parent().parent().remove();
+ },
+
+ // Render a single model view in container object "parentElOrDocumentFragment", which is either
+ // a documentFragment or a jquery object. optional arg atIndex is not support for document fragments.
+ _insertAndRenderModelView : function( modelView, parentElOrDocumentFragment, atIndex ) {
+ var thisModelViewWrapped = this._wrapModelView( modelView );
+
+ if( parentElOrDocumentFragment.nodeType === 11 ) // if we are inserting into a document fragment, we need to use the DOM appendChild method
+ parentElOrDocumentFragment.appendChild( thisModelViewWrapped.get( 0 ) );
+ else if( ! _.isUndefined( atIndex ) && atIndex > 0 && atIndex < this.collection.length - 1 )
+ parentElOrDocumentFragment.children().eq( atIndex ).before( thisModelViewWrapped );
+ else
+ parentElOrDocumentFragment.append( thisModelViewWrapped );
+
+ // we have to render the modelView after it has been put in context, as opposed to in the
+ // initialize function of the modelView, because some rendering might be dependent on
+ // the modelView's context in the DOM tree. For example, if the modelView stretch()'s itself,
+ // it must be in full context in the DOM tree or else the stretch will not behave as intended.
+ var renderResult = modelView.render();
+
+ // return false from the view's render function to hide this item
+ if( renderResult === false ) {
+ thisModelViewWrapped.hide();
+ thisModelViewWrapped.addClass( "not-visible" );
+ }
+
+ var hideThisModelView = false;
+ if( _.isFunction( this.visibleModelsFilter ) ) {
+ hideThisModelView = ! this.visibleModelsFilter( modelView.model );
+ if( hideThisModelView ) {
+ if( thisModelViewWrapped.children().length === 1 )
+ thisModelViewWrapped.hide();
+ else modelView.$el.hide();
+
+ thisModelViewWrapped.addClass( "not-visible" );
+ }
+ }
+
+ if( ! hideThisModelView && this.emptyListCaption ) this._removeEmptyListCaption();
+
+ this.viewManager.add( modelView );
+ },
+
+ updateDependentControls : function() {
+ this.trigger( "updateDependentControls", this.getSelectedModels() );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "updateDependentControls", {
+ selectedModels : this.getSelectedModels()
+ } );
+ }
+ },
+
+ // Override `Backbone.View.remove` to also destroy all Views in `viewManager`
+ remove : function() {
+ this.viewManager.each( function( view ) {
+ view.remove();
+ } );
+
+ Backbone.View.prototype.remove.apply( this, arguments );
+ },
+
+ // A method to remove the view relating to model.
+ _removeModelView : function( model ) {
+ var viewManager = this.viewManager;
+ var view = viewManager.findByModelCid( model.cid );
+
+ if ( this.selectable ) this._saveSelection();
+
+ viewManager.remove( view ); // Remove the view from the viewManager
+ view.remove(); // Remove the view from the DOM
+ this._getContainerEl().children( "[data-model-cid=" + model.cid + "]" ).remove(); // Remove the wrapper from the DOM
+
+ if ( this.selectable ) this._restoreSelection();
+
+ this._showEmptyListCaptionIfAppropriate();
+ },
+
+ _validateSelectionAndRender : function() {
+ this._validateSelection();
+ this.render();
+ },
+
+ _registerCollectionEvents : function() {
+ this.listenTo( this.collection, "add", function( model ) {
+ if( this._hasBeenRendered ) {
+ var modelView = this._createNewModelView( model, this._getModelViewOptions( model ) );
+ this._insertAndRenderModelView( modelView, this._getContainerEl(), this.collection.indexOf( model ) );
+ }
+
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "add" );
+ } );
+
+ this.listenTo( this.collection, "remove", function( model ) {
+ if( this._hasBeenRendered )
+ this._removeModelView( model );
+
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "remove" );
+ } );
+
+ this.listenTo( this.collection, "reset", function() {
+ if( this._hasBeenRendered ) this.render();
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "reset" );
+ } );
+
+ // we should not be listening to change events on the model as a default behavior. the models
+ // should be responsible for re-rendering themselves if necessary, and if the collection does
+ // also need to re-render as a result of a model change, this should be handled by overriding
+ // this method. by default the collection view should not re-render in response to model changes
+ // this.listenTo( this.collection, "change", function( model ) {
+ // if( this._hasBeenRendered ) this.viewManager.findByModel( model ).render();
+ // if( this._isBackboneCourierAvailable() )
+ // this.spawn( "change", { model : model } );
+ // } );
+
+ this.listenTo( this.collection, "sort", function( collection, options ) {
+ if( this._hasBeenRendered && options.add !== true ) this.render();
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sort" );
+ } );
+ },
+
+ _getContainerEl : function() {
+ if ( this._isRenderedAsTable() ) {
+ // not all tables have a tbody, so we test
+ var tbody = this.$el.find( "> tbody" );
+ if ( tbody.length > 0 )
+ return tbody;
+ }
+ return this.$el;
+ },
+
+ _getClickedItemId : function( theEvent ) {
+ var clickedItemId = null;
+
+ // important to use currentTarget as opposed to target, since we could be bubbling
+ // an event that took place within another collectionList
+ var clickedItemEl = $( theEvent.currentTarget );
+ if( clickedItemEl.closest( ".collection-list" ).get(0) !== this.$el.get(0) ) return;
+
+ // determine which list item was clicked. If we clicked in the blank area
+ // underneath all the elements, we want to know that too, since in this
+ // case we will want to deselect all elements. so check to see if the clicked
+ // DOM element is the list itself to find that out.
+ var clickedItem = clickedItemEl.closest( "[data-model-cid]" );
+ if( clickedItem.length > 0 )
+ {
+ clickedItemId = clickedItem.attr( "data-model-cid" );
+ if( $.isNumeric( clickedItemId ) ) clickedItemId = parseInt( clickedItemId, 10 );
+ }
+
+ return clickedItemId;
+ },
+
+ _updateItemTemplate : function() {
+ var itemTemplateHtml;
+ if( this.itemTemplate )
+ {
+ if( $( this.itemTemplate ).length === 0 )
+ throw "Could not find item template from selector: " + this.itemTemplate;
+
+ itemTemplateHtml = $( this.itemTemplate ).html();
+ }
+ else
+ itemTemplateHtml = this.$( ".item-template" ).html();
+
+ if( itemTemplateHtml ) this.itemTemplateFunction = _.template( itemTemplateHtml );
+
+ },
+
+ _validateSelection : function() {
+ // note can't use the collection's proxy to underscore because "cid" is not an attribute,
+ // but an element of the model object itself.
+ var modelReferenceIds = _.pluck( this.collection.models, "cid" );
+ this.selectedItems = _.intersection( modelReferenceIds, this.selectedItems );
+
+ if( _.isFunction( this.selectableModelsFilter ) )
+ {
+ this.selectedItems = _.filter( this.selectedItems, function( thisItemId ) {
+ return this.selectableModelsFilter.call( this, this.collection.get( thisItemId ) );
+ }, this );
+ }
+ },
+
+ _saveSelection : function() {
+ // save the current selection. use restoreSelection() to restore the selection to the state it was in the last time saveSelection() was called.
+ if( ! this.selectable ) throw "Attempt to save selection on non-selectable list";
+ this.savedSelection = {
+ items : _.clone( this.selectedItems ),
+ offset : this.getSelectedModel( { by : "offset" } )
+ };
+ },
+
+ _restoreSelection : function() {
+ if( ! this.savedSelection ) throw "Attempt to restore selection but no selection has been saved!";
+
+ // reset selectedItems to empty so that we "redraw" all "selected" classes
+ // when we set our new selection. We do this because it is likely that our
+ // contents have been refreshed, and we have thus lost all old "selected" classes.
+ this.setSelectedModels( [], { silent : true } );
+
+ if( this.savedSelection.items.length > 0 )
+ {
+ // first try to restore the old selected items using their reference ids.
+ this.setSelectedModels( this.savedSelection.items, { by : "cid", silent : true } );
+
+ // all the items with the saved reference ids have been removed from the list.
+ // ok. try to restore the selection based on the offset that used to be selected.
+ // this is the expected behavior after a item is deleted from a list (i.e. select
+ // the line that immediately follows the deleted line).
+ if( this.selectedItems.length === 0 )
+ this.setSelectedModel( this.savedSelection.offset, { by : "offset" } );
+
+ // Trigger a selection changed if the previously selected items were not all found
+ if (this.selectedItems.length !== this.savedSelection.items.length)
+ {
+ this.trigger( "selectionChanged", this.getSelectedModels(), [] );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "selectionChanged", {
+ selectedModels : this.getSelectedModels(),
+ oldSelectedModels : []
+ } );
+ }
+ }
+ }
+
+ delete this.savedSelection;
+ },
+
+ _addSelectedClassToSelectedItems : function( oldItemsIdsWithSelectedClass ) {
+ if( _.isUndefined( oldItemsIdsWithSelectedClass ) ) oldItemsIdsWithSelectedClass = [];
+
+ // oldItemsIdsWithSelectedClass is used for optimization purposes only. If this info is supplied then we
+ // only have to add / remove the "selected" class from those items that "selected" state has changed.
+
+ var itemsIdsFromWhichSelectedClassNeedsToBeRemoved = oldItemsIdsWithSelectedClass;
+ itemsIdsFromWhichSelectedClassNeedsToBeRemoved = _.without( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, this.selectedItems );
+
+ _.each( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, function( thisItemId ) {
+ this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).removeClass( "selected" );
+ }, this );
+
+ var itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems;
+ itemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass );
+
+ _.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) {
+ this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).addClass( "selected" );
+ }, this );
+ },
+
+ _reorderCollectionBasedOnHTML : function() {
+ var _this = this;
+
+ this._getContainerEl().children().each( function() {
+ var thisModelCid = $( this ).attr( "data-model-cid" );
+
+ if( thisModelCid )
+ {
+ // remove the current model and then add it back (at the end of the collection).
+ // When we are done looping through all models, they will be in the correct order.
+ var thisModel = _this.collection.get( thisModelCid );
+ if( thisModel )
+ {
+ _this.collection.remove( thisModel, { silent : true } );
+ _this.collection.add( thisModel, { silent : true, sort : ! _this.collection.comparator } );
+ }
+ }
+ } );
+
+ this.collection.trigger( "reorder" );
+
+ if( this._isBackboneCourierAvailable() ) this.spawn( "reorder" );
+
+ if( this.collection.comparator ) this.collection.sort();
+
+ },
+
+ _getModelViewConstructor : function( thisModel ) {
+ return this.modelView || mDefaultModelViewConstructor;
+ },
+
+ _getModelViewOptions : function( thisModel ) {
+ return _.extend( { model : thisModel }, this.modelViewOptions );
+ },
+
+ _createNewModelView : function( model, modelViewOptions ) {
+ var modelViewConstructor = this._getModelViewConstructor( model );
+ if( _.isUndefined( modelViewConstructor ) ) throw "Could not find modelView constructor for model";
+
+ var newModelView = new( modelViewConstructor )( modelViewOptions );
+ newModelView.collectionListView = this;
+
+ return newModelView;
+ },
+
+ _wrapModelView : function( modelView ) {
+ var _this = this;
+
+ // we use items client ids as opposed to real ids, since we may not have a representation
+ // of these models on the server
+ var wrappedModelView;
+
+ if( this._isRenderedAsTable() ) {
+ // if we are rendering the collection in a table, the template $el is a tr so we just need to set the data-model-cid
+ wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid );
+ }
+ else if( this._isRenderedAsList() ) {
+ // if we are rendering the collection in a list, we need wrap each item in an (if its not already an )
+ // and set the data-model-cid
+ if( modelView.$el.prop( "tagName" ).toLowerCase() === "li" ) {
+ wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid );
+ } else {
+ wrappedModelView = modelView.$el.wrapAll( "" ).parent();
+ }
+ }
+
+ if( _.isFunction( this.sortableModelsFilter ) )
+ if( ! this.sortableModelsFilter.call( _this, modelView.model ) )
+ wrappedModelView.addClass( "not-sortable" );
+
+ if( _.isFunction( this.selectableModelsFilter ) )
+ if( ! this.selectableModelsFilter.call( _this, modelView.model ) )
+ wrappedModelView.addClass( "not-selectable" );
+
+ return wrappedModelView;
+ },
+
+ _convertStringsToInts : function( theArray ) {
+ return _.map( theArray, function( thisEl ) {
+ if( ! _.isString( thisEl ) ) return thisEl;
+ var thisElAsNumber = parseInt( thisEl, 10 );
+ return( thisElAsNumber == thisEl ? thisElAsNumber : thisEl );
+ } );
+ },
+
+ _containSameElements : function( arrayA, arrayB ) {
+ if( arrayA.length != arrayB.length ) return false;
+ var intersectionSize = _.intersection( arrayA, arrayB ).length;
+ return intersectionSize == arrayA.length; // and must also equal arrayB.length, since arrayA.length == arrayB.length
+ },
+
+ _isRenderedAsTable : function() {
+ return this.$el.prop( "tagName" ).toLowerCase() === "table";
+ },
+
+ _isRenderedAsList : function() {
+ return ! this._isRenderedAsTable();
+ },
+
+ // Returns the wrapper HTML element for each visible modelView.
+ // When rendering in a table context, the returned elements are the $el of each modelView.
+ // When rendering in a list context,
+ // If the $el of the modelView is an , the returned elements are the $el of each modelView.
+ // Otherwise, the returned elements are the 's the collectionView wrapped around each modelView $el.
+ _getVisibleItemEls : function() {
+ var itemElements = [];
+ itemElements = this._getContainerEl().find( "> [data-model-cid]:not(.not-visible)" );
+
+ return itemElements;
+ },
+
+ _charCodes : {
+ upArrow : 38,
+ downArrow : 40
+ },
+
+ _isBackboneCourierAvailable : function() {
+ return !_.isUndefined( Backbone.Courier );
+ },
+
+ _sortStart : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ this.trigger( "sortStart", modelBeingSorted );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortStart", { modelBeingSorted : modelBeingSorted } );
+ },
+
+ _sortChange : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ this.trigger( "sortChange", modelBeingSorted );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortChange", { modelBeingSorted : modelBeingSorted } );
+ },
+
+ _sortStop : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ var modelViewContainerEl = this._getContainerEl();
+ var newIndex = modelViewContainerEl.children().index( ui.item );
+
+ if( newIndex == -1 ) {
+ // the element was removed from this list. can happen if this sortable is connected
+ // to another sortable, and the item was dropped into the other sortable.
+ this.collection.remove( modelBeingSorted );
+ }
+
+ this._reorderCollectionBasedOnHTML();
+ this.updateDependentControls();
+ this.trigger( "sortStop", modelBeingSorted, newIndex );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortStop", { modelBeingSorted : modelBeingSorted, newIndex : newIndex } );
+ },
+
+ _receive : function( event, ui ) {
+ var senderListEl = ui.sender;
+ var senderCollectionListView = senderListEl.data( "view" );
+ if( ! senderCollectionListView || ! senderCollectionListView.collection ) return;
+
+ var newIndex = this._getContainerEl().children().index( ui.item );
+ var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) );
+ senderCollectionListView.collection.remove( modelReceived );
+ this.collection.add( modelReceived, { at : newIndex } );
+ modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value.
+ this.setSelectedModel( modelReceived );
+ },
+
+ _over : function( event, ui ) {
+ // when an item is being dragged into the sortable,
+ // hide the empty list caption if it exists
+ this._getContainerEl().find( "> var.empty-list-caption" ).hide();
+ },
+
+ _onKeydown : function( event ) {
+ if( ! this.processKeyEvents ) return true;
+
+ var trap = false;
+
+ if( this.getSelectedModels( { by : "offset" } ).length == 1 )
+ {
+ // need to trap down and up arrows or else the browser
+ // will end up scrolling a autoscroll div.
+
+ var currentOffset = this.getSelectedModel( { by : "offset" } );
+ if( event.which === this._charCodes.upArrow && currentOffset !== 0 )
+ {
+ this.setSelectedModel( currentOffset - 1, { by : "offset" } );
+ trap = true;
+ }
+ else if( event.which === this._charCodes.downArrow && currentOffset !== this.collection.length - 1 )
+ {
+ this.setSelectedModel( currentOffset + 1, { by : "offset" } );
+ trap = true;
+ }
+ }
+
+ return ! trap;
+ },
+
+ _listItem_onMousedown : function( theEvent ) {
+ if( ! this.selectable || ! this.clickToSelect ) return;
+
+ var clickedItemId = this._getClickedItemId( theEvent );
+
+ if( clickedItemId )
+ {
+ // Exit if an unselectable item was clicked
+ if( _.isFunction( this.selectableModelsFilter ) &&
+ ! this.selectableModelsFilter.call( this, this.collection.get( clickedItemId ) ) )
+ {
+ return;
+ }
+
+ // a selectable list item was clicked
+ if( this.selectMultiple && theEvent.shiftKey )
+ {
+ var firstSelectedItemIndex = -1;
+
+ if( this.selectedItems.length > 0 )
+ {
+ this.collection.find( function( thisItemModel ) {
+ firstSelectedItemIndex++;
+
+ // exit when we find our first selected element
+ return _.contains( this.selectedItems, thisItemModel.cid );
+ }, this );
+ }
+
+ var clickedItemIndex = -1;
+ this.collection.find( function( thisItemModel ) {
+ clickedItemIndex++;
+
+ // exit when we find the clicked element
+ return thisItemModel.cid == clickedItemId;
+ }, this );
+
+ var shiftKeyRootSelectedItemIndex = firstSelectedItemIndex == -1 ? clickedItemIndex : firstSelectedItemIndex;
+ var minSelectedItemIndex = Math.min( clickedItemIndex, shiftKeyRootSelectedItemIndex );
+ var maxSelectedItemIndex = Math.max( clickedItemIndex, shiftKeyRootSelectedItemIndex );
+
+ var newSelectedItems = [];
+ for( var thisIndex = minSelectedItemIndex; thisIndex <= maxSelectedItemIndex; thisIndex ++ )
+ newSelectedItems.push( this.collection.at( thisIndex ).cid );
+ this.setSelectedModels( newSelectedItems, { by : "cid" } );
+
+ // shift clicking will usually highlight selectable text, which we do not want.
+ // this is a cross browser (hopefully) snippet that deselects all text selection.
+ if( document.selection && document.selection.empty )
+ document.selection.empty();
+ else if(window.getSelection) {
+ var sel = window.getSelection();
+ if( sel && sel.removeAllRanges )
+ sel.removeAllRanges();
+ }
+ }
+ else if( this.selectMultiple && ( this.clickToToggle || theEvent.metaKey ) )
+ {
+ if( _.contains( this.selectedItems, clickedItemId ) )
+ this.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : "cid" } );
+ else this.setSelectedModels( _.union( this.selectedItems, clickedItemId ), { by : "cid" } );
+ }
+ else
+ this.setSelectedModels( [ clickedItemId ], { by : "cid" } );
+ }
+ else
+ // the blank area of the list was clicked
+ this.setSelectedModels( [] );
+
+ },
+
+ _listItem_onDoubleClick : function( theEvent ) {
+ var clickedItemId = this._getClickedItemId( theEvent );
+
+ if( clickedItemId )
+ {
+ var clickedModel = this.collection.get( clickedItemId );
+ this.trigger( "doubleClick", clickedModel );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "doubleClick", { clickedModel : clickedModel } );
+ }
+ },
+
+ _listBackground_onClick : function( theEvent ) {
+ if( ! this.selectable ) return;
+ if( ! $( theEvent.target ).is( ".collection-list" ) ) return;
+
+ this.setSelectedModels( [] );
+ }
+
+ }, {
+ setDefaultModelViewConstructor : function( theConstructor ) {
+ mDefaultModelViewConstructor = theConstructor;
+ }
+ });
+
+ // Backbone.ViewOptions
+ // --------------------
+ // v0.2.0
+ //
+ // Copyright (c)2014 Rotunda Software
+ //
+ // https://github.com/rotundasoftware/backbone.viewOptions
+
+ // Backbone.ViewOptions
+ // --------------------
+ //
+ // An plugin to declare and get/set options on views.
+
+ /*
+ * Backbone.ViewOptions, v0.2
+ * Copyright (c)2014 Rotunda Software, LLC.
+ * Distributed under MIT license
+ * http://github.com/rotundasoftware/backbone.viewOptions
+ */
+
+ Backbone.ViewOptions = {};
+
+ Backbone.ViewOptions.add = function( view, optionsDeclarationsProperty ) {
+ if( _.isUndefined( optionsDeclarationsProperty ) ) optionsDeclarationsProperty = "options";
+
+ // ****************** Public methods added to view ******************
+
+ view.setOptions = function( options ) {
+ var _this = this;
+ var optionsThatWereChanged = {};
+ var optionsThatWereChangedOriginalValues = {};
+
+ var optionDeclarations = _.result( this, optionsDeclarationsProperty );
+
+ if( ! _.isUndefined( optionDeclarations ) ) {
+ var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );
+
+ _.each( normalizedOptionDeclarations, function( thisOptionDeclaration ) {
+ thisOptionName = thisOptionDeclaration.name;
+ thisOptionRequired = thisOptionDeclaration.required;
+ thisOptionDefaultValue = thisOptionDeclaration.defaultValue;
+
+ if( thisOptionRequired ) {
+ // note we do not throw an error if a required option is not supplied, but it is
+ // found on the object itself (due to a prior call of view.setOptions, most likely)
+ if( ! options ||
+ ( ( ! _.contains( _.keys( options ), thisOptionName ) && _.isUndefined( _this[ thisOptionName ] ) ) ) ||
+ _.isUndefined( options[ thisOptionName ] ) )
+ throw new Error( "Required option \"" + thisOptionName + "\" was not supplied." );
+ }
+
+ // attach the supplied value of this option, or the appropriate default value, to the view object
+ if( options && thisOptionName in options ) {
+ // if this option already exists on the view, make a note that we will be changing it
+ if( ! _.isUndefined( _this[ thisOptionName ] ) ) {
+ optionsThatWereChangedOriginalValues[ thisOptionName ] = _this[ thisOptionName ];
+ optionsThatWereChanged[ thisOptionName ] = options[ thisOptionName ];
+ }
+ _this[ thisOptionName ] = options[ thisOptionName ];
+ // note we do NOT delete the option off the options object here so that
+ // multiple views can be passed the same options object without issue.
+ }
+ else if( ! _.isUndefined( thisOptionDefaultValue ) && _.isUndefined( _this[ thisOptionName ] ) ) {
+ // note defaults do not write over any existing properties on the view itself.
+ _this[ thisOptionName ] = thisOptionDefaultValue;
+ }
+ } );
+ }
+
+ if( _.keys( optionsThatWereChanged ).length > 0 ) {
+ if( _.isFunction( _this.onOptionsChanged ) )
+ _this.onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedOriginalValues );
+ else if( _.isFunction( _this._onOptionsChanged ) )
+ _this._onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedOriginalValues );
+ }
+ };
+
+ view.getOptions = function() {
+ var optionDeclarations = _.result( this, optionsDeclarationsProperty );
+ if( _.isUndefined( optionDeclarations ) ) return [];
+
+ var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );
+ var optionsNames = _.pluck( normalizedOptionDeclarations, "name" );
+
+ return _.pick( this, optionsNames );
+ };
+ };
+
+ // ****************** Private Utility Functions ******************
+
+ function _normalizeOptionDeclarations( optionDeclarations ) {
+ // convert our short-hand option syntax (with exclamation marks, etc.)
+ // to a simple array of standard option declaration objects.
+ var normalizedOptionDeclarations = [];
+
+ if( ! _.isArray( optionDeclarations ) ) {
+ throw new Error( "Option declarations must be an array." );
+ }
+
+ _.each( optionDeclarations, function( thisOptionDeclaration ) {
+ var thisOptionName, thisOptionRequired, thisOptionDefaultValue;
+
+ thisOptionRequired = false;
+ thisOptionDefaultValue = undefined;
+
+ if( _.isString( thisOptionDeclaration ) )
+ thisOptionName = thisOptionDeclaration;
+ else if( _.isObject( thisOptionDeclaration ) ) {
+ thisOptionName = _.first( _.keys( thisOptionDeclaration ) );
+ thisOptionDefaultValue = _.clone( thisOptionDeclaration[ thisOptionName ] );
+ }
+ else throw new Error( "Each element in the option declarations array must be either a string or an object." );
+
+ if( thisOptionName[ thisOptionName.length - 1 ] === "!" ) {
+ thisOptionRequired = true;
+ thisOptionName = thisOptionName.slice( 0, thisOptionName.length - 1 );
+ }
+
+ normalizedOptionDeclarations.push( {
+ name : thisOptionName,
+ required : thisOptionRequired,
+ defaultValue : thisOptionDefaultValue
+ } );
+ } );
+
+ return normalizedOptionDeclarations;
+ };
+
+
+ // Backbone.BabySitter
+ // -------------------
+ // v0.0.6
+ //
+ // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
+ // Distributed under MIT license
+ //
+ // http://github.com/babysitterjs/backbone.babysitter
+
+ // Backbone.ChildViewContainer
+ // ---------------------------
+ //
+ // Provide a container to store, retrieve and
+ // shut down child views.
+
+ ChildViewContainer = (function(Backbone, _){
+
+ // Container Constructor
+ // ---------------------
+
+ var Container = function(views){
+ this._views = {};
+ this._indexByModel = {};
+ this._indexByCustom = {};
+ this._updateLength();
+
+ _.each(views, this.add, this);
+ };
+
+ // Container Methods
+ // -----------------
+
+ _.extend(Container.prototype, {
+
+ // Add a view to this container. Stores the view
+ // by `cid` and makes it searchable by the model
+ // cid (and model itself). Optionally specify
+ // a custom key to store an retrieve the view.
+ add: function(view, customIndex){
+ var viewCid = view.cid;
+
+ // store the view
+ this._views[viewCid] = view;
+
+ // index it by model
+ if (view.model){
+ this._indexByModel[view.model.cid] = viewCid;
+ }
+
+ // index by custom
+ if (customIndex){
+ this._indexByCustom[customIndex] = viewCid;
+ }
+
+ this._updateLength();
+ },
+
+ // Find a view by the model that was attached to
+ // it. Uses the model's `cid` to find it.
+ findByModel: function(model){
+ return this.findByModelCid(model.cid);
+ },
+
+ // Find a view by the `cid` of the model that was attached to
+ // it. Uses the model's `cid` to find the view `cid` and
+ // retrieve the view using it.
+ findByModelCid: function(modelCid){
+ var viewCid = this._indexByModel[modelCid];
+ return this.findByCid(viewCid);
+ },
+
+ // Find a view by a custom indexer.
+ findByCustom: function(index){
+ var viewCid = this._indexByCustom[index];
+ return this.findByCid(viewCid);
+ },
+
+ // Find by index. This is not guaranteed to be a
+ // stable index.
+ findByIndex: function(index){
+ return _.values(this._views)[index];
+ },
+
+ // retrieve a view by it's `cid` directly
+ findByCid: function(cid){
+ return this._views[cid];
+ },
+
+ findIndexByCid : function( cid ) {
+ var index = -1;
+ var view = _.find( this._views, function ( view ) {
+ index++;
+ if( view.model.cid == cid )
+ return view;
+ } );
+ return ( view ) ? index : -1;
+ },
+
+ // Remove a view
+ remove: function(view){
+ var viewCid = view.cid;
+
+ // delete model index
+ if (view.model){
+ delete this._indexByModel[view.model.cid];
+ }
+
+ // delete custom index
+ _.any(this._indexByCustom, function(cid, key) {
+ if (cid === viewCid) {
+ delete this._indexByCustom[key];
+ return true;
+ }
+ }, this);
+
+ // remove the view from the container
+ delete this._views[viewCid];
+
+ // update the length
+ this._updateLength();
+ },
+
+ // Call a method on every view in the container,
+ // passing parameters to the call method one at a
+ // time, like `function.call`.
+ call: function(method){
+ this.apply(method, _.tail(arguments));
+ },
+
+ // Apply a method on every view in the container,
+ // passing parameters to the call method one at a
+ // time, like `function.apply`.
+ apply: function(method, args){
+ _.each(this._views, function(view){
+ if (_.isFunction(view[method])){
+ view[method].apply(view, args || []);
+ }
+ });
+ },
+
+ // Update the `.length` attribute on this container
+ _updateLength: function(){
+ this.length = _.size(this._views);
+ }
+ });
+
+ // Borrowing this code from Backbone.Collection:
+ // http://backbonejs.org/docs/backbone.html#section-106
+ //
+ // Mix in methods from Underscore, for iteration, and other
+ // collection related features.
+ var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
+ 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
+ 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
+ 'last', 'without', 'isEmpty', 'pluck'];
+
+ _.each(methods, function(method) {
+ Container.prototype[method] = function() {
+ var views = _.values(this._views);
+ var args = [views].concat(_.toArray(arguments));
+ return _[method].apply(_, args);
+ };
+ });
+
+ // return the public API
+ return Container;
+ })(Backbone, _);
+
+ return Backbone.CollectionView;
+} ) );
\ No newline at end of file
diff --git a/ajax/libs/backbone.collectionView/0.9.3/backbone.collectionView.min.js b/ajax/libs/backbone.collectionView/0.9.3/backbone.collectionView.min.js
new file mode 100644
index 000000000..fa2a0a90a
--- /dev/null
+++ b/ajax/libs/backbone.collectionView/0.9.3/backbone.collectionView.min.js
@@ -0,0 +1,8 @@
+/*!
+* Backbone.CollectionView, v0.9.2
+* Copyright (c)2013 Rotunda Software, LLC.
+* Distributed under MIT license
+* http://github.com/rotundasoftware/backbone-collection-view
+*/
+
+(function(e,t){"function"==typeof define&&define.amd?define(["underscore","backbone","jquery"],t):"undefined"!=typeof exports?module.exports=t(require("underscore"),require("backbone"),require("backbone").$):t(e._,e.Backbone,e.jQuery||e.Zepto||e.$)})(this,function(e,t,i){function s(t){var i=[];if(!e.isArray(t))throw Error("Option declarations must be an array.");return e.each(t,function(t){var s,n,o;if(n=!1,o=void 0,e.isString(t))s=t;else{if(!e.isObject(t))throw Error("Each element in the option declarations array must be either a string or an object.");s=e.first(e.keys(t)),o=e.clone(t[s])}"!"===s[s.length-1]&&(n=!0,s=s.slice(0,s.length-1)),i.push({name:s,required:n,defaultValue:o})}),i}var n=t.View,o="model",l=["collection","modelView","modelViewOptions","itemTemplate","selectableModelsFilter","sortableModelsFilter","visibleModelsFilter","itemTemplateFunction","detachedRendering","sortableOptions"],a={background:"transparent",border:"none","box-shadow":"none"};return t.CollectionView=t.View.extend({tagName:"ul",events:{"mousedown li, td":"_listItem_onMousedown","dblclick li, td":"_listItem_onDoubleClick",click:"_listBackground_onClick","click ul.collection-list, table.collection-list":"_listBackground_onClick",keydown:"_onKeydown"},spawnMessages:{focus:"focus"},passMessages:{"*":"."},initializationOptions:[{collection:new t.Collection},{modelView:null},{modelViewOptions:{}},{itemTemplate:null},{itemTemplateFunction:null},{selectable:!0},{clickToSelect:!0},{selectableModelsFilter:null},{visibleModelsFilter:null},{sortableModelsFilter:null},{selectMultiple:!1},{clickToToggle:!1},{processKeyEvents:!0},{sortable:!1},{sortableOptions:null},{detachedRendering:!1},{emptyListCaption:null}],initialize:function(e){t.ViewOptions.add(this,"initializationOptions"),this.setOptions(e),this._hasBeenRendered=!1,this._isBackboneCourierAvailable()&&t.Courier.add(this),this.$el.data("view",this),this.$el.addClass("collection-list"),this.selectable&&this.$el.addClass("selectable"),this.processKeyEvents&&this.$el.attr("tabindex",0),this.selectedItems=[],this._updateItemTemplate(),this.collection&&this._registerCollectionEvents(),this.viewManager=new ChildViewContainer},onOptionsChanged:function(t,i){var s=!1,n=this;e.each(e.keys(t),function(o){var a=t[o],r=i[o];switch(o){case"collection":a!==r&&(n.stopListening(r),n._registerCollectionEvents());break;case"selectMultiple":!a&&n.selectedItems.length>1&&n.setSelectedModel(e.first(n.selectedItems),{by:"cid"});break;case"selectable":!a&&n.selectedItems.length>0&&n.setSelectedModels([]);break;case"selectableModelsFilter":a&&e.isFunction(a)&&n._validateSelection();break;case"itemTemplate":n._updateItemTemplate();break;case"processKeyEvents":a&&n.$el.attr("tabindex",0);break;case"modelView":n.viewManager.each(function(e){n.viewManager.remove(e),e.remove()})}e.contains(l,o)&&(s=!0)}),this._hasBeenRendered&&s&&this.render()},setOption:function(e,t){var i={};i[e]=t,this.setOptions(i)},getSelectedModel:function(t){return e.first(this.getSelectedModels(t))},getSelectedModels:function(t){var s=this;t=e.extend({},{by:o},t);var n=t.by,l=[];switch(n){case"id":e.each(this.selectedItems,function(e){l.push(s.collection.get(e).id)});break;case"cid":l=l.concat(this.selectedItems);break;case"offset":var a=0,r=this._getVisibleItemEls();r.each(function(){var e=i(this);e.is(".selected")&&l.push(a),a++});break;case"model":e.each(this.selectedItems,function(e){l.push(s.collection.get(e))});break;case"view":e.each(this.selectedItems,function(e){l.push(s.viewManager.findByModel(s.collection.get(e)))})}return l},setSelectedModels:function(t,s){if(!e.isArray(t))throw"Invalid parameter value";if(this.selectable||!(t.length>0)){s=e.extend({},{silent:!1,by:o},s);var n=s.by,l=[];switch(n){case"cid":l=t;break;case"id":this.collection.each(function(i){e.contains(t,i.id)&&l.push(i.cid)});break;case"model":l=e.pluck(t,"cid");break;case"view":e.each(t,function(e){l.push(e.model.cid)});break;case"offset":var a=0,r=this._getVisibleItemEls();r.each(function(){var s=i(this);e.contains(t,a)&&l.push(s.attr("data-model-cid")),a++})}var d=this.getSelectedModels(),c=e.clone(this.selectedItems);this.selectedItems=this._convertStringsToInts(l),this._validateSelection();var h=this.getSelectedModels();this._containSameElements(c,this.selectedItems)||(this._addSelectedClassToSelectedItems(c),s.silent||(this.trigger("selectionChanged",h,d),this._isBackboneCourierAvailable()&&this.spawn("selectionChanged",{selectedModels:h,oldSelectedModels:d})),this.updateDependentControls())}},setSelectedModel:function(e,t){e||0===e?this.setSelectedModels([e],t):this.setSelectedModels([],t)},render:function(){var t=this;this._hasBeenRendered=!0,this.selectable&&this._saveSelection();var i;i=this._getContainerEl();var s=this.viewManager;this.viewManager=new ChildViewContainer,s.each(function(e){t.collection.get(e.model.cid)?e.$el.detach():e.remove()}),i.empty();var n;if(this.detachedRendering&&(n=document.createDocumentFragment()),this.collection.each(function(t){var o=s.findByModelCid(t.cid);e.isUndefined(o)&&(o=this._createNewModelView(t,this._getModelViewOptions(t))),this._insertAndRenderModelView(o,n||i)},this),this.detachedRendering&&i.append(n),this.sortable){var o=e.extend({axis:"y",distance:10,forcePlaceholderSize:!0,start:e.bind(this._sortStart,this),change:e.bind(this._sortChange,this),stop:e.bind(this._sortStop,this),receive:e.bind(this._receive,this),over:e.bind(this._over,this)},e.result(this,"sortableOptions"));t._isRenderedAsTable()?o.items="> tbody > tr:not(.not-sortable)":t._isRenderedAsList()&&(o.items="> li:not(.not-sortable)"),this.$el=this.$el.sortable(o)}this._showEmptyListCaptionIfAppropriate(),this.trigger("render"),this._isBackboneCourierAvailable()&&this.spawn("render"),this.selectable&&(this._restoreSelection(),this.updateDependentControls()),e.isFunction(this.onAfterRender)&&this.onAfterRender()},_showEmptyListCaptionIfAppropriate:function(){if(this.emptyListCaption){var t=this._getVisibleItemEls();if(0===t.length){var s;s=e.isFunction(this.emptyListCaption)?this.emptyListCaption():this.emptyListCaption;var n=i(""+s+"");$emptyListCaptionEl=this._isRenderedAsList()?n.wrapAll("").parent().css(a):n.wrapAll(" |
").parent().parent().css(a),this._getContainerEl().append($emptyListCaptionEl)}}},_removeEmptyListCaption:function(){this._isRenderedAsList()?this._getContainerEl().find("> li > var.empty-list-caption").parent().remove():this._getContainerEl().find("> tr > td > var.empty-list-caption").parent().parent().remove()},_insertAndRenderModelView:function(t,i,s){var n=this._wrapModelView(t);11===i.nodeType?i.appendChild(n.get(0)):!e.isUndefined(s)&&s>0&&this.collection.length-1>s?i.children().eq(s).before(n):i.append(n);var o=t.render();o===!1&&(n.hide(),n.addClass("not-visible"));var l=!1;e.isFunction(this.visibleModelsFilter)&&(l=!this.visibleModelsFilter(t.model),l&&(1===n.children().length?n.hide():t.$el.hide(),n.addClass("not-visible"))),!l&&this.emptyListCaption&&this._removeEmptyListCaption(),this.viewManager.add(t)},updateDependentControls:function(){this.trigger("updateDependentControls",this.getSelectedModels()),this._isBackboneCourierAvailable()&&this.spawn("updateDependentControls",{selectedModels:this.getSelectedModels()})},remove:function(){this.viewManager.each(function(e){e.remove()}),t.View.prototype.remove.apply(this,arguments)},_removeModelView:function(e){var t=this.viewManager,i=t.findByModelCid(e.cid);this.selectable&&this._saveSelection(),t.remove(i),i.remove(),this._getContainerEl().children("[data-model-cid="+e.cid+"]").remove(),this.selectable&&this._restoreSelection(),this._showEmptyListCaptionIfAppropriate()},_validateSelectionAndRender:function(){this._validateSelection(),this.render()},_registerCollectionEvents:function(){this.listenTo(this.collection,"add",function(e){if(this._hasBeenRendered){var t=this._createNewModelView(e,this._getModelViewOptions(e));this._insertAndRenderModelView(t,this._getContainerEl(),this.collection.indexOf(e))}this._isBackboneCourierAvailable()&&this.spawn("add")}),this.listenTo(this.collection,"remove",function(e){this._hasBeenRendered&&this._removeModelView(e),this._isBackboneCourierAvailable()&&this.spawn("remove")}),this.listenTo(this.collection,"reset",function(){this._hasBeenRendered&&this.render(),this._isBackboneCourierAvailable()&&this.spawn("reset")}),this.listenTo(this.collection,"sort",function(e,t){this._hasBeenRendered&&t.add!==!0&&this.render(),this._isBackboneCourierAvailable()&&this.spawn("sort")})},_getContainerEl:function(){if(this._isRenderedAsTable()){var e=this.$el.find("> tbody");if(e.length>0)return e}return this.$el},_getClickedItemId:function(e){var t=null,s=i(e.currentTarget);if(s.closest(".collection-list").get(0)===this.$el.get(0)){var n=s.closest("[data-model-cid]");return n.length>0&&(t=n.attr("data-model-cid"),i.isNumeric(t)&&(t=parseInt(t,10))),t}},_updateItemTemplate:function(){var t;if(this.itemTemplate){if(0===i(this.itemTemplate).length)throw"Could not find item template from selector: "+this.itemTemplate;t=i(this.itemTemplate).html()}else t=this.$(".item-template").html();t&&(this.itemTemplateFunction=e.template(t))},_validateSelection:function(){var t=e.pluck(this.collection.models,"cid");this.selectedItems=e.intersection(t,this.selectedItems),e.isFunction(this.selectableModelsFilter)&&(this.selectedItems=e.filter(this.selectedItems,function(e){return this.selectableModelsFilter.call(this,this.collection.get(e))},this))},_saveSelection:function(){if(!this.selectable)throw"Attempt to save selection on non-selectable list";this.savedSelection={items:e.clone(this.selectedItems),offset:this.getSelectedModel({by:"offset"})}},_restoreSelection:function(){if(!this.savedSelection)throw"Attempt to restore selection but no selection has been saved!";this.setSelectedModels([],{silent:!0}),this.savedSelection.items.length>0&&(this.setSelectedModels(this.savedSelection.items,{by:"cid",silent:!0}),0===this.selectedItems.length&&this.setSelectedModel(this.savedSelection.offset,{by:"offset"}),this.selectedItems.length!==this.savedSelection.items.length&&(this.trigger("selectionChanged",this.getSelectedModels(),[]),this._isBackboneCourierAvailable()&&this.spawn("selectionChanged",{selectedModels:this.getSelectedModels(),oldSelectedModels:[]}))),delete this.savedSelection},_addSelectedClassToSelectedItems:function(t){e.isUndefined(t)&&(t=[]);var i=t;i=e.without(i,this.selectedItems),e.each(i,function(e){this._getContainerEl().find("[data-model-cid="+e+"]").removeClass("selected")},this);var s=this.selectedItems;s=e.without(s,t),e.each(s,function(e){this._getContainerEl().find("[data-model-cid="+e+"]").addClass("selected")},this)},_reorderCollectionBasedOnHTML:function(){var e=this;this._getContainerEl().children().each(function(){var t=i(this).attr("data-model-cid");if(t){var s=e.collection.get(t);s&&(e.collection.remove(s,{silent:!0}),e.collection.add(s,{silent:!0,sort:!e.collection.comparator}))}}),this.collection.trigger("reorder"),this._isBackboneCourierAvailable()&&this.spawn("reorder"),this.collection.comparator&&this.collection.sort()},_getModelViewConstructor:function(){return this.modelView||n},_getModelViewOptions:function(t){return e.extend({model:t},this.modelViewOptions)},_createNewModelView:function(t,i){var s=this._getModelViewConstructor(t);if(e.isUndefined(s))throw"Could not find modelView constructor for model";var n=new s(i);return n.collectionListView=this,n},_wrapModelView:function(t){var i,s=this;return this._isRenderedAsTable()?i=t.$el.attr("data-model-cid",t.model.cid):this._isRenderedAsList()&&(i="li"===t.$el.prop("tagName").toLowerCase()?t.$el.attr("data-model-cid",t.model.cid):t.$el.wrapAll("").parent()),e.isFunction(this.sortableModelsFilter)&&(this.sortableModelsFilter.call(s,t.model)||i.addClass("not-sortable")),e.isFunction(this.selectableModelsFilter)&&(this.selectableModelsFilter.call(s,t.model)||i.addClass("not-selectable")),i},_convertStringsToInts:function(t){return e.map(t,function(t){if(!e.isString(t))return t;var i=parseInt(t,10);return i==t?i:t})},_containSameElements:function(t,i){if(t.length!=i.length)return!1;var s=e.intersection(t,i).length;return s==t.length},_isRenderedAsTable:function(){return"table"===this.$el.prop("tagName").toLowerCase()},_isRenderedAsList:function(){return!this._isRenderedAsTable()},_getVisibleItemEls:function(){var e=[];return e=this._getContainerEl().find("> [data-model-cid]:not(.not-visible)")},_charCodes:{upArrow:38,downArrow:40},_isBackboneCourierAvailable:function(){return!e.isUndefined(t.Courier)},_sortStart:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid"));this.trigger("sortStart",i),this._isBackboneCourierAvailable()&&this.spawn("sortStart",{modelBeingSorted:i})},_sortChange:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid"));this.trigger("sortChange",i),this._isBackboneCourierAvailable()&&this.spawn("sortChange",{modelBeingSorted:i})},_sortStop:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid")),s=this._getContainerEl(),n=s.children().index(t.item);-1==n&&this.collection.remove(i),this._reorderCollectionBasedOnHTML(),this.updateDependentControls(),this.trigger("sortStop",i,n),this._isBackboneCourierAvailable()&&this.spawn("sortStop",{modelBeingSorted:i,newIndex:n})},_receive:function(e,t){var i=t.sender,s=i.data("view");if(s&&s.collection){var n=this._getContainerEl().children().index(t.item),o=s.collection.get(t.item.attr("data-model-cid"));s.collection.remove(o),this.collection.add(o,{at:n}),o.collection=this.collection,this.setSelectedModel(o)}},_over:function(){this._getContainerEl().find("> var.empty-list-caption").hide()},_onKeydown:function(e){if(!this.processKeyEvents)return!0;var t=!1;if(1==this.getSelectedModels({by:"offset"}).length){var i=this.getSelectedModel({by:"offset"});e.which===this._charCodes.upArrow&&0!==i?(this.setSelectedModel(i-1,{by:"offset"}),t=!0):e.which===this._charCodes.downArrow&&i!==this.collection.length-1&&(this.setSelectedModel(i+1,{by:"offset"}),t=!0)}return!t},_listItem_onMousedown:function(t){if(this.selectable&&this.clickToSelect){var i=this._getClickedItemId(t);if(i){if(e.isFunction(this.selectableModelsFilter)&&!this.selectableModelsFilter.call(this,this.collection.get(i)))return;if(this.selectMultiple&&t.shiftKey){var s=-1;this.selectedItems.length>0&&this.collection.find(function(t){return s++,e.contains(this.selectedItems,t.cid)},this);var n=-1;this.collection.find(function(e){return n++,e.cid==i},this);for(var o=-1==s?n:s,l=Math.min(n,o),a=Math.max(n,o),r=[],d=l;a>=d;d++)r.push(this.collection.at(d).cid);if(this.setSelectedModels(r,{by:"cid"}),document.selection&&document.selection.empty)document.selection.empty();else if(window.getSelection){var c=window.getSelection();c&&c.removeAllRanges&&c.removeAllRanges()}}else this.selectMultiple&&(this.clickToToggle||t.metaKey)?e.contains(this.selectedItems,i)?this.setSelectedModels(e.without(this.selectedItems,i),{by:"cid"}):this.setSelectedModels(e.union(this.selectedItems,i),{by:"cid"}):this.setSelectedModels([i],{by:"cid"})}else this.setSelectedModels([])}},_listItem_onDoubleClick:function(e){var t=this._getClickedItemId(e);if(t){var i=this.collection.get(t);this.trigger("doubleClick",i),this._isBackboneCourierAvailable()&&this.spawn("doubleClick",{clickedModel:i})}},_listBackground_onClick:function(e){this.selectable&&i(e.target).is(".collection-list")&&this.setSelectedModels([])}},{setDefaultModelViewConstructor:function(e){n=e}}),t.ViewOptions={},t.ViewOptions.add=function(t,i){e.isUndefined(i)&&(i="options"),t.setOptions=function(t){var n=this,o={},l={},a=e.result(this,i);if(!e.isUndefined(a)){var r=s(a);e.each(r,function(i){if(thisOptionName=i.name,thisOptionRequired=i.required,thisOptionDefaultValue=i.defaultValue,thisOptionRequired&&(!t||!e.contains(e.keys(t),thisOptionName)&&e.isUndefined(n[thisOptionName])||e.isUndefined(t[thisOptionName])))throw Error('Required option "'+thisOptionName+'" was not supplied.');t&&thisOptionName in t?(e.isUndefined(n[thisOptionName])||(l[thisOptionName]=n[thisOptionName],o[thisOptionName]=t[thisOptionName]),n[thisOptionName]=t[thisOptionName]):!e.isUndefined(thisOptionDefaultValue)&&e.isUndefined(n[thisOptionName])&&(n[thisOptionName]=thisOptionDefaultValue)})}e.keys(o).length>0&&(e.isFunction(n.onOptionsChanged)?n.onOptionsChanged(o,l):e.isFunction(n._onOptionsChanged)&&n._onOptionsChanged(o,l))},t.getOptions=function(){var t=e.result(this,i);if(e.isUndefined(t))return[];var n=s(t),o=e.pluck(n,"name");return e.pick(this,o)}},ChildViewContainer=function(e,t){var i=function(e){this._views={},this._indexByModel={},this._indexByCustom={},this._updateLength(),t.each(e,this.add,this)};t.extend(i.prototype,{add:function(e,t){var i=e.cid;this._views[i]=e,e.model&&(this._indexByModel[e.model.cid]=i),t&&(this._indexByCustom[t]=i),this._updateLength()},findByModel:function(e){return this.findByModelCid(e.cid)},findByModelCid:function(e){var t=this._indexByModel[e];return this.findByCid(t)},findByCustom:function(e){var t=this._indexByCustom[e];return this.findByCid(t)},findByIndex:function(e){return t.values(this._views)[e]},findByCid:function(e){return this._views[e]},findIndexByCid:function(e){var i=-1,s=t.find(this._views,function(t){return i++,t.model.cid==e?t:void 0});return s?i:-1},remove:function(e){var i=e.cid;e.model&&delete this._indexByModel[e.model.cid],t.any(this._indexByCustom,function(e,t){return e===i?(delete this._indexByCustom[t],!0):void 0},this),delete this._views[i],this._updateLength()},call:function(e){this.apply(e,t.tail(arguments))},apply:function(e,i){t.each(this._views,function(s){t.isFunction(s[e])&&s[e].apply(s,i||[])})},_updateLength:function(){this.length=t.size(this._views)}});var s=["forEach","each","map","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","toArray","first","initial","rest","last","without","isEmpty","pluck"];return t.each(s,function(e){i.prototype[e]=function(){var i=t.values(this._views),s=[i].concat(t.toArray(arguments));return t[e].apply(t,s)}}),i}(t,e),t.CollectionView});
\ No newline at end of file
diff --git a/ajax/libs/backbone.collectionView/0.9.4/backbone.collectionView.js b/ajax/libs/backbone.collectionView/0.9.4/backbone.collectionView.js
new file mode 100644
index 000000000..98444e26a
--- /dev/null
+++ b/ajax/libs/backbone.collectionView/0.9.4/backbone.collectionView.js
@@ -0,0 +1,1243 @@
+/*!
+* Backbone.CollectionView, v0.9.3
+* Copyright (c)2013 Rotunda Software, LLC.
+* Distributed under MIT license
+* http://github.com/rotundasoftware/backbone-collection-view
+*/
+
+( function( root, factory ) {
+ // UMD wrapper
+ if ( typeof define === 'function' && define.amd ) {
+ // AMD
+ define( [ 'underscore', 'backbone', 'jquery' ], factory );
+ } else if ( typeof exports !== 'undefined' ) {
+ // Node/CommonJS
+ module.exports = factory( require('underscore' ), require( 'backbone' ), require( 'backbone' ).$ );
+ } else {
+ // Browser globals
+ factory( root._, root.Backbone, ( root.jQuery || root.Zepto || root.$ ) );
+ }
+}( this, function( _, Backbone, $ ) {
+ var mDefaultModelViewConstructor = Backbone.View;
+
+ var kDefaultReferenceBy = "model";
+
+ var kOptionsRequiringRerendering = [ "collection", "modelView", "modelViewOptions", "itemTemplate", "selectableModelsFilter", "sortableModelsFilter", "visibleModelsFilter", "itemTemplateFunction", "detachedRendering", "sortableOptions" ];
+
+ var kStylesForEmptyListCaption = {
+ "background" : "transparent",
+ "border" : "none",
+ "box-shadow" : "none"
+ };
+
+ Backbone.CollectionView = Backbone.View.extend( {
+
+ tagName : "ul",
+
+ events : {
+ "mousedown li, td" : "_listItem_onMousedown",
+ "dblclick li, td" : "_listItem_onDoubleClick",
+ "click" : "_listBackground_onClick",
+ "click ul.collection-list, table.collection-list" : "_listBackground_onClick",
+ "keydown" : "_onKeydown"
+ },
+
+ // only used if Backbone.Courier is available
+ spawnMessages : {
+ "focus" : "focus"
+ },
+
+ //only used if Backbone.Courier is available
+ passMessages : { "*" : "." },
+
+ // viewOption definitions with default values.
+ initializationOptions : [
+ { "collection" : new Backbone.Collection() },
+ { "modelView" : null },
+ { "modelViewOptions" : {} },
+ { "itemTemplate" : null },
+ { "itemTemplateFunction" : null },
+ { "selectable" : true },
+ { "clickToSelect" : true },
+ { "selectableModelsFilter" : null },
+ { "visibleModelsFilter" : null },
+ { "sortableModelsFilter" : null },
+ { "selectMultiple" : false },
+ { "clickToToggle" : false },
+ { "processKeyEvents" : true },
+ { "sortable" : false },
+ { "sortableOptions" : null },
+ { "detachedRendering" : false },
+ { "emptyListCaption" : null }
+ ],
+
+ initialize : function( options ) {
+ Backbone.ViewOptions.add( this, "initializationOptions" ); // setup the ViewOptions functionality.
+ this.setOptions( options ); // and make use of any provided options
+
+ this._hasBeenRendered = false;
+
+ if( this._isBackboneCourierAvailable() ) {
+ Backbone.Courier.add( this );
+ }
+
+ this.$el.data( "view", this ); // needed for connected sortable lists
+ this.$el.addClass( "collection-list" );
+ if( this.selectable ) this.$el.addClass( "selectable" );
+
+ if( this.processKeyEvents )
+ this.$el.attr( "tabindex", 0 ); // so we get keyboard events
+
+ this.selectedItems = [];
+
+ this._updateItemTemplate();
+
+ if( this.collection )
+ this._registerCollectionEvents();
+
+ this.viewManager = new ChildViewContainer();
+ },
+
+ onOptionsChanged : function( changedOptions, originalOptions ) {
+ var rerender = false;
+ var _this = this;
+ _.each( _.keys( changedOptions ), function( changedOptionKey ) {
+ var newVal = changedOptions[ changedOptionKey ];
+ var oldVal = originalOptions[ changedOptionKey ];
+ switch( changedOptionKey ) {
+ case "collection" :
+ if ( newVal !== oldVal ) {
+ _this.stopListening( oldVal );
+ _this._registerCollectionEvents();
+ }
+ break;
+ case "selectMultiple":
+ if( ! newVal && _this.selectedItems.length > 1 )
+ _this.setSelectedModel( _.first( _this.selectedItems ), { by : "cid" } );
+ break;
+ case "selectable" :
+ if( ! newVal && _this.selectedItems.length > 0 )
+ _this.setSelectedModels( [] );
+ break;
+ case "selectableModelsFilter" :
+ if( newVal && _.isFunction( newVal ) )
+ _this._validateSelection();
+ break;
+ case "itemTemplate" :
+ _this._updateItemTemplate();
+ break;
+ case "processKeyEvents" :
+ if( newVal ) _this.$el.attr( "tabindex", 0 ); // so we get keyboard events
+ break;
+ case "modelView" :
+ //need to remove all old view instances
+ _this.viewManager.each( function( view ) {
+ _this.viewManager.remove( view );
+ // destroy the View itself
+ view.remove();
+ } );
+ break;
+ }
+ if( _.contains( kOptionsRequiringRerendering, changedOptionKey ) ) rerender = true;
+ });
+ if( this._hasBeenRendered && rerender ) {
+ this.render(); // Rerender the view if the rerender flag has been set.
+ }
+ },
+
+ setOption : function( optionName, optionValue ) { // now is mearly a wrapper around backbone.viewOptions' setOptions()
+ var optionHash = {};
+ optionHash[ optionName ] = optionValue;
+ this.setOptions( optionHash );
+ },
+
+ getSelectedModel : function( options ) {
+ return _.first( this.getSelectedModels( options ) );
+ },
+
+ getSelectedModels : function ( options ) {
+ var _this = this;
+
+ options = _.extend( {}, {
+ by : kDefaultReferenceBy
+ }, options );
+
+ var referenceBy = options.by;
+ var items = [];
+
+ switch( referenceBy ) {
+ case "id" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.collection.get( item ).id );
+ } );
+ break;
+ case "cid" :
+ items = items.concat( this.selectedItems );
+ break;
+ case "offset" :
+ var curLineNumber = 0;
+
+ var itemElements = this._getVisibleItemEls();
+
+ itemElements.each( function() {
+ var thisItemEl = $( this );
+ if( thisItemEl.is( ".selected" ) )
+ items.push( curLineNumber );
+ curLineNumber++;
+ } );
+ break;
+ case "model" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.collection.get( item ) );
+ } );
+ break;
+ case "view" :
+ _.each( this.selectedItems, function ( item ) {
+ items.push( _this.viewManager.findByModel( _this.collection.get( item ) ) );
+ } );
+ break;
+ }
+
+ return items;
+
+ },
+
+ setSelectedModels : function( newSelectedItems, options ) {
+ if( ! _.isArray( newSelectedItems ) ) throw "Invalid parameter value";
+ if( ! this.selectable && newSelectedItems.length > 0 ) return; // used to throw error, but there are some circumstances in which a list can be selectable at times and not at others, don't want to have to worry about catching errors
+
+ options = _.extend( {}, {
+ silent : false,
+ by : kDefaultReferenceBy
+ }, options );
+
+ var referenceBy = options.by;
+ var newSelectedCids = [];
+
+ switch( referenceBy ) {
+ case "cid" :
+ newSelectedCids = newSelectedItems;
+ break;
+ case "id" :
+ this.collection.each( function( thisModel ) {
+ if( _.contains( newSelectedItems, thisModel.id ) ) newSelectedCids.push( thisModel.cid );
+ } );
+ break;
+ case "model" :
+ newSelectedCids = _.pluck( newSelectedItems, "cid" );
+ break;
+ case "view" :
+ _.each( newSelectedItems, function( item ) {
+ newSelectedCids.push( item.model.cid );
+ } );
+ break;
+ case "offset" :
+ var curLineNumber = 0;
+ var selectedItems = [];
+
+ var itemElements = this._getVisibleItemEls();
+ itemElements.each( function() {
+ var thisItemEl = $( this );
+ if( _.contains( newSelectedItems, curLineNumber ) )
+ newSelectedCids.push( thisItemEl.attr( "data-model-cid" ) );
+ curLineNumber++;
+ } );
+ break;
+ }
+
+ var oldSelectedModels = this.getSelectedModels();
+ var oldSelectedCids = _.clone( this.selectedItems );
+
+ this.selectedItems = this._convertStringsToInts( newSelectedCids );
+ this._validateSelection();
+
+ var newSelectedModels = this.getSelectedModels();
+
+ if( ! this._containSameElements( oldSelectedCids, this.selectedItems ) )
+ {
+ this._addSelectedClassToSelectedItems( oldSelectedCids );
+
+ if( ! options.silent )
+ {
+ this.trigger( "selectionChanged", newSelectedModels, oldSelectedModels );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "selectionChanged", {
+ selectedModels : newSelectedModels,
+ oldSelectedModels : oldSelectedModels
+ } );
+ }
+ }
+
+ this.updateDependentControls();
+ }
+ },
+
+ setSelectedModel : function( newSelectedItem, options ) {
+ if( ! newSelectedItem && newSelectedItem !== 0 )
+ this.setSelectedModels( [], options );
+ else
+ this.setSelectedModels( [ newSelectedItem ], options );
+ },
+
+ render : function(){
+ var _this = this;
+
+ this._hasBeenRendered = true;
+
+ if( this.selectable ) this._saveSelection();
+
+ var modelViewContainerEl;
+
+ // If collection view element is a table and it has a tbody
+ // within it, render the model views inside of the tbody
+ modelViewContainerEl = this._getContainerEl();
+
+ var oldViewManager = this.viewManager;
+ this.viewManager = new ChildViewContainer();
+
+ // detach each of our subviews that we have already created to represent models
+ // in the collection. We are going to re-use the ones that represent models that
+ // are still here, instead of creating new ones, so that we don't loose state
+ // information in the views.
+ oldViewManager.each( function( thisModelView ) {
+ // to boost performance, only detach those views that will be sticking around.
+ // we won't need the other ones later, so no need to detach them individually.
+ if( _this.collection.get( thisModelView.model.cid ) )
+ thisModelView.$el.detach();
+ else
+ thisModelView.remove();
+ } );
+
+ modelViewContainerEl.empty();
+ var fragmentContainer;
+
+ if( this.detachedRendering )
+ fragmentContainer = document.createDocumentFragment();
+
+ this.collection.each( function( thisModel ) {
+ var thisModelView = oldViewManager.findByModelCid( thisModel.cid );
+ if( _.isUndefined( thisModelView ) ) {
+ // if the model view has not already been created on a
+ // previous render then create and initialize it now.
+ thisModelView = this._createNewModelView( thisModel, this._getModelViewOptions( thisModel ) );
+ }
+
+ this._insertAndRenderModelView( thisModelView, fragmentContainer || modelViewContainerEl );
+ }, this );
+
+ if( this.detachedRendering )
+ modelViewContainerEl.append( fragmentContainer );
+
+ if( this.sortable )
+ {
+ var sortableOptions = _.extend( {
+ axis: "y",
+ distance: 10,
+ forcePlaceholderSize : true,
+ start : _.bind( this._sortStart, this ),
+ change : _.bind( this._sortChange, this ),
+ stop : _.bind( this._sortStop, this ),
+ receive : _.bind( this._receive, this ),
+ over : _.bind( this._over, this )
+ }, _.result( this, "sortableOptions" ) );
+
+ if( _this._isRenderedAsTable() ) {
+ sortableOptions.items = "> tbody > tr:not(.not-sortable)";
+ }
+ else if( _this._isRenderedAsList() ) {
+ sortableOptions.items = "> li:not(.not-sortable)";
+ }
+
+ this.$el = this.$el.sortable( sortableOptions );
+ }
+
+ this._showEmptyListCaptionIfAppropriate();
+
+ this.trigger( "render" );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "render" );
+
+ if( this.selectable ) {
+ this._restoreSelection();
+ this.updateDependentControls();
+ }
+
+ if( _.isFunction( this.onAfterRender ) )
+ this.onAfterRender();
+ },
+
+ _showEmptyListCaptionIfAppropriate : function ( ) {
+ if( this.emptyListCaption ) {
+ var visibleEls = this._getVisibleItemEls();
+
+ if( visibleEls.length === 0 ) {
+ var emptyListString;
+
+ if( _.isFunction( this.emptyListCaption ) )
+ emptyListString = this.emptyListCaption();
+ else
+ emptyListString = this.emptyListCaption;
+
+ var $emptyCaptionEl;
+ var $varEl = $( "" + emptyListString + "" );
+
+ //need to wrap the empty caption to make it fit the rendered list structure (either with an li or a tr td)
+ if( this._isRenderedAsList() )
+ $emptyListCaptionEl = $varEl.wrapAll( "" ).parent().css( kStylesForEmptyListCaption );
+ else
+ $emptyListCaptionEl = $varEl.wrapAll( " |
" ).parent().parent().css( kStylesForEmptyListCaption );
+
+ this._getContainerEl().append( $emptyListCaptionEl );
+ }
+ }
+ },
+
+ _removeEmptyListCaption : function( ) {
+ if( this._isRenderedAsList() )
+ this._getContainerEl().find( "> li > var.empty-list-caption" ).parent().remove();
+ else
+ this._getContainerEl().find( "> tr > td > var.empty-list-caption" ).parent().parent().remove();
+ },
+
+ // Render a single model view in container object "parentElOrDocumentFragment", which is either
+ // a documentFragment or a jquery object. optional arg atIndex is not support for document fragments.
+ _insertAndRenderModelView : function( modelView, parentElOrDocumentFragment, atIndex ) {
+ var thisModelViewWrapped = this._wrapModelView( modelView );
+
+ if( parentElOrDocumentFragment.nodeType === 11 ) // if we are inserting into a document fragment, we need to use the DOM appendChild method
+ parentElOrDocumentFragment.appendChild( thisModelViewWrapped.get( 0 ) );
+ else if( ! _.isUndefined( atIndex ) && atIndex > 0 && atIndex < this.collection.length - 1 )
+ parentElOrDocumentFragment.children().eq( atIndex ).before( thisModelViewWrapped );
+ else
+ parentElOrDocumentFragment.append( thisModelViewWrapped );
+
+ // we have to render the modelView after it has been put in context, as opposed to in the
+ // initialize function of the modelView, because some rendering might be dependent on
+ // the modelView's context in the DOM tree. For example, if the modelView stretch()'s itself,
+ // it must be in full context in the DOM tree or else the stretch will not behave as intended.
+ var renderResult = modelView.render();
+
+ // return false from the view's render function to hide this item
+ if( renderResult === false ) {
+ thisModelViewWrapped.hide();
+ thisModelViewWrapped.addClass( "not-visible" );
+ }
+
+ var hideThisModelView = false;
+ if( _.isFunction( this.visibleModelsFilter ) ) {
+ hideThisModelView = ! this.visibleModelsFilter( modelView.model );
+ if( hideThisModelView ) {
+ if( thisModelViewWrapped.children().length === 1 )
+ thisModelViewWrapped.hide();
+ else modelView.$el.hide();
+
+ thisModelViewWrapped.addClass( "not-visible" );
+ }
+ }
+
+ if( ! hideThisModelView && this.emptyListCaption ) this._removeEmptyListCaption();
+
+ this.viewManager.add( modelView );
+ },
+
+ updateDependentControls : function() {
+ this.trigger( "updateDependentControls", this.getSelectedModels() );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "updateDependentControls", {
+ selectedModels : this.getSelectedModels()
+ } );
+ }
+ },
+
+ // Override `Backbone.View.remove` to also destroy all Views in `viewManager`
+ remove : function() {
+ this.viewManager.each( function( view ) {
+ view.remove();
+ } );
+
+ Backbone.View.prototype.remove.apply( this, arguments );
+ },
+
+ // A method to remove the view relating to model.
+ _removeModelView : function( model ) {
+ var viewManager = this.viewManager;
+ var view = viewManager.findByModelCid( model.cid );
+
+ if ( this.selectable ) this._saveSelection();
+
+ viewManager.remove( view ); // Remove the view from the viewManager
+ view.remove(); // Remove the view from the DOM
+ this._getContainerEl().children( "[data-model-cid=" + model.cid + "]" ).remove(); // Remove the wrapper from the DOM
+
+ if ( this.selectable ) this._restoreSelection();
+
+ this._showEmptyListCaptionIfAppropriate();
+ },
+
+ _validateSelectionAndRender : function() {
+ this._validateSelection();
+ this.render();
+ },
+
+ _registerCollectionEvents : function() {
+ this.listenTo( this.collection, "add", function( model ) {
+ if( this._hasBeenRendered ) {
+ var modelView = this._createNewModelView( model, this._getModelViewOptions( model ) );
+ this._insertAndRenderModelView( modelView, this._getContainerEl(), this.collection.indexOf( model ) );
+ }
+
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "add" );
+ } );
+
+ this.listenTo( this.collection, "remove", function( model ) {
+ if( this._hasBeenRendered )
+ this._removeModelView( model );
+
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "remove" );
+ } );
+
+ this.listenTo( this.collection, "reset", function() {
+ if( this._hasBeenRendered ) this.render();
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "reset" );
+ } );
+
+ // we should not be listening to change events on the model as a default behavior. the models
+ // should be responsible for re-rendering themselves if necessary, and if the collection does
+ // also need to re-render as a result of a model change, this should be handled by overriding
+ // this method. by default the collection view should not re-render in response to model changes
+ // this.listenTo( this.collection, "change", function( model ) {
+ // if( this._hasBeenRendered ) this.viewManager.findByModel( model ).render();
+ // if( this._isBackboneCourierAvailable() )
+ // this.spawn( "change", { model : model } );
+ // } );
+
+ this.listenTo( this.collection, "sort", function( collection, options ) {
+ if( this._hasBeenRendered && options.add !== true ) this.render();
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sort" );
+ } );
+ },
+
+ _getContainerEl : function() {
+ if ( this._isRenderedAsTable() ) {
+ // not all tables have a tbody, so we test
+ var tbody = this.$el.find( "> tbody" );
+ if ( tbody.length > 0 )
+ return tbody;
+ }
+ return this.$el;
+ },
+
+ _getClickedItemId : function( theEvent ) {
+ var clickedItemId = null;
+
+ // important to use currentTarget as opposed to target, since we could be bubbling
+ // an event that took place within another collectionList
+ var clickedItemEl = $( theEvent.currentTarget );
+ if( clickedItemEl.closest( ".collection-list" ).get(0) !== this.$el.get(0) ) return;
+
+ // determine which list item was clicked. If we clicked in the blank area
+ // underneath all the elements, we want to know that too, since in this
+ // case we will want to deselect all elements. so check to see if the clicked
+ // DOM element is the list itself to find that out.
+ var clickedItem = clickedItemEl.closest( "[data-model-cid]" );
+ if( clickedItem.length > 0 )
+ {
+ clickedItemId = clickedItem.attr( "data-model-cid" );
+ if( $.isNumeric( clickedItemId ) ) clickedItemId = parseInt( clickedItemId, 10 );
+ }
+
+ return clickedItemId;
+ },
+
+ _updateItemTemplate : function() {
+ var itemTemplateHtml;
+ if( this.itemTemplate )
+ {
+ if( $( this.itemTemplate ).length === 0 )
+ throw "Could not find item template from selector: " + this.itemTemplate;
+
+ itemTemplateHtml = $( this.itemTemplate ).html();
+ }
+ else
+ itemTemplateHtml = this.$( ".item-template" ).html();
+
+ if( itemTemplateHtml ) this.itemTemplateFunction = _.template( itemTemplateHtml );
+
+ },
+
+ _validateSelection : function() {
+ // note can't use the collection's proxy to underscore because "cid" is not an attribute,
+ // but an element of the model object itself.
+ var modelReferenceIds = _.pluck( this.collection.models, "cid" );
+ this.selectedItems = _.intersection( modelReferenceIds, this.selectedItems );
+
+ if( _.isFunction( this.selectableModelsFilter ) )
+ {
+ this.selectedItems = _.filter( this.selectedItems, function( thisItemId ) {
+ return this.selectableModelsFilter.call( this, this.collection.get( thisItemId ) );
+ }, this );
+ }
+ },
+
+ _saveSelection : function() {
+ // save the current selection. use restoreSelection() to restore the selection to the state it was in the last time saveSelection() was called.
+ if( ! this.selectable ) throw "Attempt to save selection on non-selectable list";
+ this.savedSelection = {
+ items : _.clone( this.selectedItems ),
+ offset : this.getSelectedModel( { by : "offset" } )
+ };
+ },
+
+ _restoreSelection : function() {
+ if( ! this.savedSelection ) throw "Attempt to restore selection but no selection has been saved!";
+
+ // reset selectedItems to empty so that we "redraw" all "selected" classes
+ // when we set our new selection. We do this because it is likely that our
+ // contents have been refreshed, and we have thus lost all old "selected" classes.
+ this.setSelectedModels( [], { silent : true } );
+
+ if( this.savedSelection.items.length > 0 )
+ {
+ // first try to restore the old selected items using their reference ids.
+ this.setSelectedModels( this.savedSelection.items, { by : "cid", silent : true } );
+
+ // all the items with the saved reference ids have been removed from the list.
+ // ok. try to restore the selection based on the offset that used to be selected.
+ // this is the expected behavior after a item is deleted from a list (i.e. select
+ // the line that immediately follows the deleted line).
+ if( this.selectedItems.length === 0 )
+ this.setSelectedModel( this.savedSelection.offset, { by : "offset" } );
+
+ // Trigger a selection changed if the previously selected items were not all found
+ if (this.selectedItems.length !== this.savedSelection.items.length)
+ {
+ this.trigger( "selectionChanged", this.getSelectedModels(), [] );
+ if( this._isBackboneCourierAvailable() ) {
+ this.spawn( "selectionChanged", {
+ selectedModels : this.getSelectedModels(),
+ oldSelectedModels : []
+ } );
+ }
+ }
+ }
+
+ delete this.savedSelection;
+ },
+
+ _addSelectedClassToSelectedItems : function( oldItemsIdsWithSelectedClass ) {
+ if( _.isUndefined( oldItemsIdsWithSelectedClass ) ) oldItemsIdsWithSelectedClass = [];
+
+ // oldItemsIdsWithSelectedClass is used for optimization purposes only. If this info is supplied then we
+ // only have to add / remove the "selected" class from those items that "selected" state has changed.
+
+ var itemsIdsFromWhichSelectedClassNeedsToBeRemoved = oldItemsIdsWithSelectedClass;
+ itemsIdsFromWhichSelectedClassNeedsToBeRemoved = _.without( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, this.selectedItems );
+
+ _.each( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, function( thisItemId ) {
+ this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).removeClass( "selected" );
+ }, this );
+
+ var itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems;
+ itemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass );
+
+ _.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) {
+ this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).addClass( "selected" );
+ }, this );
+ },
+
+ _reorderCollectionBasedOnHTML : function() {
+ var _this = this;
+
+ this._getContainerEl().children().each( function() {
+ var thisModelCid = $( this ).attr( "data-model-cid" );
+
+ if( thisModelCid )
+ {
+ // remove the current model and then add it back (at the end of the collection).
+ // When we are done looping through all models, they will be in the correct order.
+ var thisModel = _this.collection.get( thisModelCid );
+ if( thisModel )
+ {
+ _this.collection.remove( thisModel, { silent : true } );
+ _this.collection.add( thisModel, { silent : true, sort : ! _this.collection.comparator } );
+ }
+ }
+ } );
+
+ this.collection.trigger( "reorder" );
+
+ if( this._isBackboneCourierAvailable() ) this.spawn( "reorder" );
+
+ if( this.collection.comparator ) this.collection.sort();
+
+ },
+
+ _getModelViewConstructor : function( thisModel ) {
+ return this.modelView || mDefaultModelViewConstructor;
+ },
+
+ _getModelViewOptions : function( thisModel ) {
+ return _.extend( { model : thisModel }, this.modelViewOptions );
+ },
+
+ _createNewModelView : function( model, modelViewOptions ) {
+ var modelViewConstructor = this._getModelViewConstructor( model );
+ if( _.isUndefined( modelViewConstructor ) ) throw "Could not find modelView constructor for model";
+
+ var newModelView = new( modelViewConstructor )( modelViewOptions );
+ newModelView.collectionListView = this;
+
+ return newModelView;
+ },
+
+ _wrapModelView : function( modelView ) {
+ var _this = this;
+
+ // we use items client ids as opposed to real ids, since we may not have a representation
+ // of these models on the server
+ var wrappedModelView;
+
+ if( this._isRenderedAsTable() ) {
+ // if we are rendering the collection in a table, the template $el is a tr so we just need to set the data-model-cid
+ wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid );
+ }
+ else if( this._isRenderedAsList() ) {
+ // if we are rendering the collection in a list, we need wrap each item in an (if its not already an )
+ // and set the data-model-cid
+ if( modelView.$el.prop( "tagName" ).toLowerCase() === "li" ) {
+ wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid );
+ } else {
+ wrappedModelView = modelView.$el.wrapAll( "" ).parent();
+ }
+ }
+
+ if( _.isFunction( this.sortableModelsFilter ) )
+ if( ! this.sortableModelsFilter.call( _this, modelView.model ) )
+ wrappedModelView.addClass( "not-sortable" );
+
+ if( _.isFunction( this.selectableModelsFilter ) )
+ if( ! this.selectableModelsFilter.call( _this, modelView.model ) )
+ wrappedModelView.addClass( "not-selectable" );
+
+ return wrappedModelView;
+ },
+
+ _convertStringsToInts : function( theArray ) {
+ return _.map( theArray, function( thisEl ) {
+ if( ! _.isString( thisEl ) ) return thisEl;
+ var thisElAsNumber = parseInt( thisEl, 10 );
+ return( thisElAsNumber == thisEl ? thisElAsNumber : thisEl );
+ } );
+ },
+
+ _containSameElements : function( arrayA, arrayB ) {
+ if( arrayA.length != arrayB.length ) return false;
+ var intersectionSize = _.intersection( arrayA, arrayB ).length;
+ return intersectionSize == arrayA.length; // and must also equal arrayB.length, since arrayA.length == arrayB.length
+ },
+
+ _isRenderedAsTable : function() {
+ return this.$el.prop( "tagName" ).toLowerCase() === "table";
+ },
+
+ _isRenderedAsList : function() {
+ return ! this._isRenderedAsTable();
+ },
+
+ // Returns the wrapper HTML element for each visible modelView.
+ // When rendering in a table context, the returned elements are the $el of each modelView.
+ // When rendering in a list context,
+ // If the $el of the modelView is an , the returned elements are the $el of each modelView.
+ // Otherwise, the returned elements are the 's the collectionView wrapped around each modelView $el.
+ _getVisibleItemEls : function() {
+ var itemElements = [];
+ itemElements = this._getContainerEl().find( "> [data-model-cid]:not(.not-visible)" );
+
+ return itemElements;
+ },
+
+ _charCodes : {
+ upArrow : 38,
+ downArrow : 40
+ },
+
+ _isBackboneCourierAvailable : function() {
+ return !_.isUndefined( Backbone.Courier );
+ },
+
+ _sortStart : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ this.trigger( "sortStart", modelBeingSorted );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortStart", { modelBeingSorted : modelBeingSorted } );
+ },
+
+ _sortChange : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ this.trigger( "sortChange", modelBeingSorted );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortChange", { modelBeingSorted : modelBeingSorted } );
+ },
+
+ _sortStop : function( event, ui ) {
+ var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
+ var modelViewContainerEl = this._getContainerEl();
+ var newIndex = modelViewContainerEl.children().index( ui.item );
+
+ if( newIndex == -1 ) {
+ // the element was removed from this list. can happen if this sortable is connected
+ // to another sortable, and the item was dropped into the other sortable.
+ this.collection.remove( modelBeingSorted );
+ }
+
+ this._reorderCollectionBasedOnHTML();
+ this.updateDependentControls();
+ this.trigger( "sortStop", modelBeingSorted, newIndex );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "sortStop", { modelBeingSorted : modelBeingSorted, newIndex : newIndex } );
+ },
+
+ _receive : function( event, ui ) {
+ var senderListEl = ui.sender;
+ var senderCollectionListView = senderListEl.data( "view" );
+ if( ! senderCollectionListView || ! senderCollectionListView.collection ) return;
+
+ var newIndex = this._getContainerEl().children().index( ui.item );
+ var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) );
+ senderCollectionListView.collection.remove( modelReceived );
+ this.collection.add( modelReceived, { at : newIndex } );
+ modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value.
+ this.setSelectedModel( modelReceived );
+ },
+
+ _over : function( event, ui ) {
+ // when an item is being dragged into the sortable,
+ // hide the empty list caption if it exists
+ this._getContainerEl().find( "> var.empty-list-caption" ).hide();
+ },
+
+ _onKeydown : function( event ) {
+ if( ! this.processKeyEvents ) return true;
+
+ var trap = false;
+
+ if( this.getSelectedModels( { by : "offset" } ).length == 1 )
+ {
+ // need to trap down and up arrows or else the browser
+ // will end up scrolling a autoscroll div.
+
+ var currentOffset = this.getSelectedModel( { by : "offset" } );
+ if( event.which === this._charCodes.upArrow && currentOffset !== 0 )
+ {
+ this.setSelectedModel( currentOffset - 1, { by : "offset" } );
+ trap = true;
+ }
+ else if( event.which === this._charCodes.downArrow && currentOffset !== this.collection.length - 1 )
+ {
+ this.setSelectedModel( currentOffset + 1, { by : "offset" } );
+ trap = true;
+ }
+ }
+
+ return ! trap;
+ },
+
+ _listItem_onMousedown : function( theEvent ) {
+ if( ! this.selectable || ! this.clickToSelect ) return;
+
+ var clickedItemId = this._getClickedItemId( theEvent );
+
+ if( clickedItemId )
+ {
+ // Exit if an unselectable item was clicked
+ if( _.isFunction( this.selectableModelsFilter ) &&
+ ! this.selectableModelsFilter.call( this, this.collection.get( clickedItemId ) ) )
+ {
+ return;
+ }
+
+ // a selectable list item was clicked
+ if( this.selectMultiple && theEvent.shiftKey )
+ {
+ var firstSelectedItemIndex = -1;
+
+ if( this.selectedItems.length > 0 )
+ {
+ this.collection.find( function( thisItemModel ) {
+ firstSelectedItemIndex++;
+
+ // exit when we find our first selected element
+ return _.contains( this.selectedItems, thisItemModel.cid );
+ }, this );
+ }
+
+ var clickedItemIndex = -1;
+ this.collection.find( function( thisItemModel ) {
+ clickedItemIndex++;
+
+ // exit when we find the clicked element
+ return thisItemModel.cid == clickedItemId;
+ }, this );
+
+ var shiftKeyRootSelectedItemIndex = firstSelectedItemIndex == -1 ? clickedItemIndex : firstSelectedItemIndex;
+ var minSelectedItemIndex = Math.min( clickedItemIndex, shiftKeyRootSelectedItemIndex );
+ var maxSelectedItemIndex = Math.max( clickedItemIndex, shiftKeyRootSelectedItemIndex );
+
+ var newSelectedItems = [];
+ for( var thisIndex = minSelectedItemIndex; thisIndex <= maxSelectedItemIndex; thisIndex ++ )
+ newSelectedItems.push( this.collection.at( thisIndex ).cid );
+ this.setSelectedModels( newSelectedItems, { by : "cid" } );
+
+ // shift clicking will usually highlight selectable text, which we do not want.
+ // this is a cross browser (hopefully) snippet that deselects all text selection.
+ if( document.selection && document.selection.empty )
+ document.selection.empty();
+ else if(window.getSelection) {
+ var sel = window.getSelection();
+ if( sel && sel.removeAllRanges )
+ sel.removeAllRanges();
+ }
+ }
+ else if( this.selectMultiple && ( this.clickToToggle || theEvent.metaKey ) )
+ {
+ if( _.contains( this.selectedItems, clickedItemId ) )
+ this.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : "cid" } );
+ else this.setSelectedModels( _.union( this.selectedItems, clickedItemId ), { by : "cid" } );
+ }
+ else
+ this.setSelectedModels( [ clickedItemId ], { by : "cid" } );
+ }
+ else
+ // the blank area of the list was clicked
+ this.setSelectedModels( [] );
+
+ },
+
+ _listItem_onDoubleClick : function( theEvent ) {
+ var clickedItemId = this._getClickedItemId( theEvent );
+
+ if( clickedItemId )
+ {
+ var clickedModel = this.collection.get( clickedItemId );
+ this.trigger( "doubleClick", clickedModel );
+ if( this._isBackboneCourierAvailable() )
+ this.spawn( "doubleClick", { clickedModel : clickedModel } );
+ }
+ },
+
+ _listBackground_onClick : function( theEvent ) {
+ if( ! this.selectable ) return;
+ if( ! $( theEvent.target ).is( ".collection-list" ) ) return;
+
+ this.setSelectedModels( [] );
+ }
+
+ }, {
+ setDefaultModelViewConstructor : function( theConstructor ) {
+ mDefaultModelViewConstructor = theConstructor;
+ }
+ });
+
+ // Backbone.ViewOptions
+ // --------------------
+ // v0.2.0
+ //
+ // Copyright (c)2014 Rotunda Software
+ //
+ // https://github.com/rotundasoftware/backbone.viewOptions
+
+ // Backbone.ViewOptions
+ // --------------------
+ //
+ // An plugin to declare and get/set options on views.
+
+ /*
+ * Backbone.ViewOptions, v0.2
+ * Copyright (c)2014 Rotunda Software, LLC.
+ * Distributed under MIT license
+ * http://github.com/rotundasoftware/backbone.viewOptions
+ */
+
+ Backbone.ViewOptions = {};
+
+ Backbone.ViewOptions.add = function( view, optionsDeclarationsProperty ) {
+ if( _.isUndefined( optionsDeclarationsProperty ) ) optionsDeclarationsProperty = "options";
+
+ // ****************** Public methods added to view ******************
+
+ view.setOptions = function( options ) {
+ var _this = this;
+ var optionsThatWereChanged = {};
+ var optionsThatWereChangedOriginalValues = {};
+
+ var optionDeclarations = _.result( this, optionsDeclarationsProperty );
+
+ if( ! _.isUndefined( optionDeclarations ) ) {
+ var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );
+
+ _.each( normalizedOptionDeclarations, function( thisOptionDeclaration ) {
+ thisOptionName = thisOptionDeclaration.name;
+ thisOptionRequired = thisOptionDeclaration.required;
+ thisOptionDefaultValue = thisOptionDeclaration.defaultValue;
+
+ if( thisOptionRequired ) {
+ // note we do not throw an error if a required option is not supplied, but it is
+ // found on the object itself (due to a prior call of view.setOptions, most likely)
+ if( ! options ||
+ ( ( ! _.contains( _.keys( options ), thisOptionName ) && _.isUndefined( _this[ thisOptionName ] ) ) ) ||
+ _.isUndefined( options[ thisOptionName ] ) )
+ throw new Error( "Required option \"" + thisOptionName + "\" was not supplied." );
+ }
+
+ // attach the supplied value of this option, or the appropriate default value, to the view object
+ if( options && thisOptionName in options ) {
+ // if this option already exists on the view, make a note that we will be changing it
+ if( ! _.isUndefined( _this[ thisOptionName ] ) ) {
+ optionsThatWereChangedOriginalValues[ thisOptionName ] = _this[ thisOptionName ];
+ optionsThatWereChanged[ thisOptionName ] = options[ thisOptionName ];
+ }
+ _this[ thisOptionName ] = options[ thisOptionName ];
+ // note we do NOT delete the option off the options object here so that
+ // multiple views can be passed the same options object without issue.
+ }
+ else if( ! _.isUndefined( thisOptionDefaultValue ) && _.isUndefined( _this[ thisOptionName ] ) ) {
+ // note defaults do not write over any existing properties on the view itself.
+ _this[ thisOptionName ] = thisOptionDefaultValue;
+ }
+ } );
+ }
+
+ if( _.keys( optionsThatWereChanged ).length > 0 ) {
+ if( _.isFunction( _this.onOptionsChanged ) )
+ _this.onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedOriginalValues );
+ else if( _.isFunction( _this._onOptionsChanged ) )
+ _this._onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedOriginalValues );
+ }
+ };
+
+ view.getOptions = function() {
+ var optionDeclarations = _.result( this, optionsDeclarationsProperty );
+ if( _.isUndefined( optionDeclarations ) ) return [];
+
+ var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );
+ var optionsNames = _.pluck( normalizedOptionDeclarations, "name" );
+
+ return _.pick( this, optionsNames );
+ };
+ };
+
+ // ****************** Private Utility Functions ******************
+
+ function _normalizeOptionDeclarations( optionDeclarations ) {
+ // convert our short-hand option syntax (with exclamation marks, etc.)
+ // to a simple array of standard option declaration objects.
+ var normalizedOptionDeclarations = [];
+
+ if( ! _.isArray( optionDeclarations ) ) {
+ throw new Error( "Option declarations must be an array." );
+ }
+
+ _.each( optionDeclarations, function( thisOptionDeclaration ) {
+ var thisOptionName, thisOptionRequired, thisOptionDefaultValue;
+
+ thisOptionRequired = false;
+ thisOptionDefaultValue = undefined;
+
+ if( _.isString( thisOptionDeclaration ) )
+ thisOptionName = thisOptionDeclaration;
+ else if( _.isObject( thisOptionDeclaration ) ) {
+ thisOptionName = _.first( _.keys( thisOptionDeclaration ) );
+ thisOptionDefaultValue = _.clone( thisOptionDeclaration[ thisOptionName ] );
+ }
+ else throw new Error( "Each element in the option declarations array must be either a string or an object." );
+
+ if( thisOptionName[ thisOptionName.length - 1 ] === "!" ) {
+ thisOptionRequired = true;
+ thisOptionName = thisOptionName.slice( 0, thisOptionName.length - 1 );
+ }
+
+ normalizedOptionDeclarations.push( {
+ name : thisOptionName,
+ required : thisOptionRequired,
+ defaultValue : thisOptionDefaultValue
+ } );
+ } );
+
+ return normalizedOptionDeclarations;
+ };
+
+
+ // Backbone.BabySitter
+ // -------------------
+ // v0.0.6
+ //
+ // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
+ // Distributed under MIT license
+ //
+ // http://github.com/babysitterjs/backbone.babysitter
+
+ // Backbone.ChildViewContainer
+ // ---------------------------
+ //
+ // Provide a container to store, retrieve and
+ // shut down child views.
+
+ ChildViewContainer = (function(Backbone, _){
+
+ // Container Constructor
+ // ---------------------
+
+ var Container = function(views){
+ this._views = {};
+ this._indexByModel = {};
+ this._indexByCustom = {};
+ this._updateLength();
+
+ _.each(views, this.add, this);
+ };
+
+ // Container Methods
+ // -----------------
+
+ _.extend(Container.prototype, {
+
+ // Add a view to this container. Stores the view
+ // by `cid` and makes it searchable by the model
+ // cid (and model itself). Optionally specify
+ // a custom key to store an retrieve the view.
+ add: function(view, customIndex){
+ var viewCid = view.cid;
+
+ // store the view
+ this._views[viewCid] = view;
+
+ // index it by model
+ if (view.model){
+ this._indexByModel[view.model.cid] = viewCid;
+ }
+
+ // index by custom
+ if (customIndex){
+ this._indexByCustom[customIndex] = viewCid;
+ }
+
+ this._updateLength();
+ },
+
+ // Find a view by the model that was attached to
+ // it. Uses the model's `cid` to find it.
+ findByModel: function(model){
+ return this.findByModelCid(model.cid);
+ },
+
+ // Find a view by the `cid` of the model that was attached to
+ // it. Uses the model's `cid` to find the view `cid` and
+ // retrieve the view using it.
+ findByModelCid: function(modelCid){
+ var viewCid = this._indexByModel[modelCid];
+ return this.findByCid(viewCid);
+ },
+
+ // Find a view by a custom indexer.
+ findByCustom: function(index){
+ var viewCid = this._indexByCustom[index];
+ return this.findByCid(viewCid);
+ },
+
+ // Find by index. This is not guaranteed to be a
+ // stable index.
+ findByIndex: function(index){
+ return _.values(this._views)[index];
+ },
+
+ // retrieve a view by it's `cid` directly
+ findByCid: function(cid){
+ return this._views[cid];
+ },
+
+ findIndexByCid : function( cid ) {
+ var index = -1;
+ var view = _.find( this._views, function ( view ) {
+ index++;
+ if( view.model.cid == cid )
+ return view;
+ } );
+ return ( view ) ? index : -1;
+ },
+
+ // Remove a view
+ remove: function(view){
+ var viewCid = view.cid;
+
+ // delete model index
+ if (view.model){
+ delete this._indexByModel[view.model.cid];
+ }
+
+ // delete custom index
+ _.any(this._indexByCustom, function(cid, key) {
+ if (cid === viewCid) {
+ delete this._indexByCustom[key];
+ return true;
+ }
+ }, this);
+
+ // remove the view from the container
+ delete this._views[viewCid];
+
+ // update the length
+ this._updateLength();
+ },
+
+ // Call a method on every view in the container,
+ // passing parameters to the call method one at a
+ // time, like `function.call`.
+ call: function(method){
+ this.apply(method, _.tail(arguments));
+ },
+
+ // Apply a method on every view in the container,
+ // passing parameters to the call method one at a
+ // time, like `function.apply`.
+ apply: function(method, args){
+ _.each(this._views, function(view){
+ if (_.isFunction(view[method])){
+ view[method].apply(view, args || []);
+ }
+ });
+ },
+
+ // Update the `.length` attribute on this container
+ _updateLength: function(){
+ this.length = _.size(this._views);
+ }
+ });
+
+ // Borrowing this code from Backbone.Collection:
+ // http://backbonejs.org/docs/backbone.html#section-106
+ //
+ // Mix in methods from Underscore, for iteration, and other
+ // collection related features.
+ var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
+ 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
+ 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
+ 'last', 'without', 'isEmpty', 'pluck'];
+
+ _.each(methods, function(method) {
+ Container.prototype[method] = function() {
+ var views = _.values(this._views);
+ var args = [views].concat(_.toArray(arguments));
+ return _[method].apply(_, args);
+ };
+ });
+
+ // return the public API
+ return Container;
+ })(Backbone, _);
+
+ return Backbone.CollectionView;
+} ) );
\ No newline at end of file
diff --git a/ajax/libs/backbone.collectionView/0.9.4/backbone.collectionView.min.js b/ajax/libs/backbone.collectionView/0.9.4/backbone.collectionView.min.js
new file mode 100644
index 000000000..7c0dfdba5
--- /dev/null
+++ b/ajax/libs/backbone.collectionView/0.9.4/backbone.collectionView.min.js
@@ -0,0 +1,8 @@
+/*!
+* Backbone.CollectionView, v0.9.3
+* Copyright (c)2013 Rotunda Software, LLC.
+* Distributed under MIT license
+* http://github.com/rotundasoftware/backbone-collection-view
+*/
+
+(function(e,t){"function"==typeof define&&define.amd?define(["underscore","backbone","jquery"],t):"undefined"!=typeof exports?module.exports=t(require("underscore"),require("backbone"),require("backbone").$):t(e._,e.Backbone,e.jQuery||e.Zepto||e.$)})(this,function(e,t,i){function s(t){var i=[];if(!e.isArray(t))throw Error("Option declarations must be an array.");return e.each(t,function(t){var s,n,o;if(n=!1,o=void 0,e.isString(t))s=t;else{if(!e.isObject(t))throw Error("Each element in the option declarations array must be either a string or an object.");s=e.first(e.keys(t)),o=e.clone(t[s])}"!"===s[s.length-1]&&(n=!0,s=s.slice(0,s.length-1)),i.push({name:s,required:n,defaultValue:o})}),i}var n=t.View,o="model",l=["collection","modelView","modelViewOptions","itemTemplate","selectableModelsFilter","sortableModelsFilter","visibleModelsFilter","itemTemplateFunction","detachedRendering","sortableOptions"],a={background:"transparent",border:"none","box-shadow":"none"};return t.CollectionView=t.View.extend({tagName:"ul",events:{"mousedown li, td":"_listItem_onMousedown","dblclick li, td":"_listItem_onDoubleClick",click:"_listBackground_onClick","click ul.collection-list, table.collection-list":"_listBackground_onClick",keydown:"_onKeydown"},spawnMessages:{focus:"focus"},passMessages:{"*":"."},initializationOptions:[{collection:new t.Collection},{modelView:null},{modelViewOptions:{}},{itemTemplate:null},{itemTemplateFunction:null},{selectable:!0},{clickToSelect:!0},{selectableModelsFilter:null},{visibleModelsFilter:null},{sortableModelsFilter:null},{selectMultiple:!1},{clickToToggle:!1},{processKeyEvents:!0},{sortable:!1},{sortableOptions:null},{detachedRendering:!1},{emptyListCaption:null}],initialize:function(e){t.ViewOptions.add(this,"initializationOptions"),this.setOptions(e),this._hasBeenRendered=!1,this._isBackboneCourierAvailable()&&t.Courier.add(this),this.$el.data("view",this),this.$el.addClass("collection-list"),this.selectable&&this.$el.addClass("selectable"),this.processKeyEvents&&this.$el.attr("tabindex",0),this.selectedItems=[],this._updateItemTemplate(),this.collection&&this._registerCollectionEvents(),this.viewManager=new ChildViewContainer},onOptionsChanged:function(t,i){var s=!1,n=this;e.each(e.keys(t),function(o){var a=t[o],r=i[o];switch(o){case"collection":a!==r&&(n.stopListening(r),n._registerCollectionEvents());break;case"selectMultiple":!a&&n.selectedItems.length>1&&n.setSelectedModel(e.first(n.selectedItems),{by:"cid"});break;case"selectable":!a&&n.selectedItems.length>0&&n.setSelectedModels([]);break;case"selectableModelsFilter":a&&e.isFunction(a)&&n._validateSelection();break;case"itemTemplate":n._updateItemTemplate();break;case"processKeyEvents":a&&n.$el.attr("tabindex",0);break;case"modelView":n.viewManager.each(function(e){n.viewManager.remove(e),e.remove()})}e.contains(l,o)&&(s=!0)}),this._hasBeenRendered&&s&&this.render()},setOption:function(e,t){var i={};i[e]=t,this.setOptions(i)},getSelectedModel:function(t){return e.first(this.getSelectedModels(t))},getSelectedModels:function(t){var s=this;t=e.extend({},{by:o},t);var n=t.by,l=[];switch(n){case"id":e.each(this.selectedItems,function(e){l.push(s.collection.get(e).id)});break;case"cid":l=l.concat(this.selectedItems);break;case"offset":var a=0,r=this._getVisibleItemEls();r.each(function(){var e=i(this);e.is(".selected")&&l.push(a),a++});break;case"model":e.each(this.selectedItems,function(e){l.push(s.collection.get(e))});break;case"view":e.each(this.selectedItems,function(e){l.push(s.viewManager.findByModel(s.collection.get(e)))})}return l},setSelectedModels:function(t,s){if(!e.isArray(t))throw"Invalid parameter value";if(this.selectable||!(t.length>0)){s=e.extend({},{silent:!1,by:o},s);var n=s.by,l=[];switch(n){case"cid":l=t;break;case"id":this.collection.each(function(i){e.contains(t,i.id)&&l.push(i.cid)});break;case"model":l=e.pluck(t,"cid");break;case"view":e.each(t,function(e){l.push(e.model.cid)});break;case"offset":var a=0,r=this._getVisibleItemEls();r.each(function(){var s=i(this);e.contains(t,a)&&l.push(s.attr("data-model-cid")),a++})}var d=this.getSelectedModels(),c=e.clone(this.selectedItems);this.selectedItems=this._convertStringsToInts(l),this._validateSelection();var h=this.getSelectedModels();this._containSameElements(c,this.selectedItems)||(this._addSelectedClassToSelectedItems(c),s.silent||(this.trigger("selectionChanged",h,d),this._isBackboneCourierAvailable()&&this.spawn("selectionChanged",{selectedModels:h,oldSelectedModels:d})),this.updateDependentControls())}},setSelectedModel:function(e,t){e||0===e?this.setSelectedModels([e],t):this.setSelectedModels([],t)},render:function(){var t=this;this._hasBeenRendered=!0,this.selectable&&this._saveSelection();var i;i=this._getContainerEl();var s=this.viewManager;this.viewManager=new ChildViewContainer,s.each(function(e){t.collection.get(e.model.cid)?e.$el.detach():e.remove()}),i.empty();var n;if(this.detachedRendering&&(n=document.createDocumentFragment()),this.collection.each(function(t){var o=s.findByModelCid(t.cid);e.isUndefined(o)&&(o=this._createNewModelView(t,this._getModelViewOptions(t))),this._insertAndRenderModelView(o,n||i)},this),this.detachedRendering&&i.append(n),this.sortable){var o=e.extend({axis:"y",distance:10,forcePlaceholderSize:!0,start:e.bind(this._sortStart,this),change:e.bind(this._sortChange,this),stop:e.bind(this._sortStop,this),receive:e.bind(this._receive,this),over:e.bind(this._over,this)},e.result(this,"sortableOptions"));t._isRenderedAsTable()?o.items="> tbody > tr:not(.not-sortable)":t._isRenderedAsList()&&(o.items="> li:not(.not-sortable)"),this.$el=this.$el.sortable(o)}this._showEmptyListCaptionIfAppropriate(),this.trigger("render"),this._isBackboneCourierAvailable()&&this.spawn("render"),this.selectable&&(this._restoreSelection(),this.updateDependentControls()),e.isFunction(this.onAfterRender)&&this.onAfterRender()},_showEmptyListCaptionIfAppropriate:function(){if(this.emptyListCaption){var t=this._getVisibleItemEls();if(0===t.length){var s;s=e.isFunction(this.emptyListCaption)?this.emptyListCaption():this.emptyListCaption;var n=i(""+s+"");$emptyListCaptionEl=this._isRenderedAsList()?n.wrapAll("").parent().css(a):n.wrapAll(" |
").parent().parent().css(a),this._getContainerEl().append($emptyListCaptionEl)}}},_removeEmptyListCaption:function(){this._isRenderedAsList()?this._getContainerEl().find("> li > var.empty-list-caption").parent().remove():this._getContainerEl().find("> tr > td > var.empty-list-caption").parent().parent().remove()},_insertAndRenderModelView:function(t,i,s){var n=this._wrapModelView(t);11===i.nodeType?i.appendChild(n.get(0)):!e.isUndefined(s)&&s>0&&this.collection.length-1>s?i.children().eq(s).before(n):i.append(n);var o=t.render();o===!1&&(n.hide(),n.addClass("not-visible"));var l=!1;e.isFunction(this.visibleModelsFilter)&&(l=!this.visibleModelsFilter(t.model),l&&(1===n.children().length?n.hide():t.$el.hide(),n.addClass("not-visible"))),!l&&this.emptyListCaption&&this._removeEmptyListCaption(),this.viewManager.add(t)},updateDependentControls:function(){this.trigger("updateDependentControls",this.getSelectedModels()),this._isBackboneCourierAvailable()&&this.spawn("updateDependentControls",{selectedModels:this.getSelectedModels()})},remove:function(){this.viewManager.each(function(e){e.remove()}),t.View.prototype.remove.apply(this,arguments)},_removeModelView:function(e){var t=this.viewManager,i=t.findByModelCid(e.cid);this.selectable&&this._saveSelection(),t.remove(i),i.remove(),this._getContainerEl().children("[data-model-cid="+e.cid+"]").remove(),this.selectable&&this._restoreSelection(),this._showEmptyListCaptionIfAppropriate()},_validateSelectionAndRender:function(){this._validateSelection(),this.render()},_registerCollectionEvents:function(){this.listenTo(this.collection,"add",function(e){if(this._hasBeenRendered){var t=this._createNewModelView(e,this._getModelViewOptions(e));this._insertAndRenderModelView(t,this._getContainerEl(),this.collection.indexOf(e))}this._isBackboneCourierAvailable()&&this.spawn("add")}),this.listenTo(this.collection,"remove",function(e){this._hasBeenRendered&&this._removeModelView(e),this._isBackboneCourierAvailable()&&this.spawn("remove")}),this.listenTo(this.collection,"reset",function(){this._hasBeenRendered&&this.render(),this._isBackboneCourierAvailable()&&this.spawn("reset")}),this.listenTo(this.collection,"sort",function(e,t){this._hasBeenRendered&&t.add!==!0&&this.render(),this._isBackboneCourierAvailable()&&this.spawn("sort")})},_getContainerEl:function(){if(this._isRenderedAsTable()){var e=this.$el.find("> tbody");if(e.length>0)return e}return this.$el},_getClickedItemId:function(e){var t=null,s=i(e.currentTarget);if(s.closest(".collection-list").get(0)===this.$el.get(0)){var n=s.closest("[data-model-cid]");return n.length>0&&(t=n.attr("data-model-cid"),i.isNumeric(t)&&(t=parseInt(t,10))),t}},_updateItemTemplate:function(){var t;if(this.itemTemplate){if(0===i(this.itemTemplate).length)throw"Could not find item template from selector: "+this.itemTemplate;t=i(this.itemTemplate).html()}else t=this.$(".item-template").html();t&&(this.itemTemplateFunction=e.template(t))},_validateSelection:function(){var t=e.pluck(this.collection.models,"cid");this.selectedItems=e.intersection(t,this.selectedItems),e.isFunction(this.selectableModelsFilter)&&(this.selectedItems=e.filter(this.selectedItems,function(e){return this.selectableModelsFilter.call(this,this.collection.get(e))},this))},_saveSelection:function(){if(!this.selectable)throw"Attempt to save selection on non-selectable list";this.savedSelection={items:e.clone(this.selectedItems),offset:this.getSelectedModel({by:"offset"})}},_restoreSelection:function(){if(!this.savedSelection)throw"Attempt to restore selection but no selection has been saved!";this.setSelectedModels([],{silent:!0}),this.savedSelection.items.length>0&&(this.setSelectedModels(this.savedSelection.items,{by:"cid",silent:!0}),0===this.selectedItems.length&&this.setSelectedModel(this.savedSelection.offset,{by:"offset"}),this.selectedItems.length!==this.savedSelection.items.length&&(this.trigger("selectionChanged",this.getSelectedModels(),[]),this._isBackboneCourierAvailable()&&this.spawn("selectionChanged",{selectedModels:this.getSelectedModels(),oldSelectedModels:[]}))),delete this.savedSelection},_addSelectedClassToSelectedItems:function(t){e.isUndefined(t)&&(t=[]);var i=t;i=e.without(i,this.selectedItems),e.each(i,function(e){this._getContainerEl().find("[data-model-cid="+e+"]").removeClass("selected")},this);var s=this.selectedItems;s=e.without(s,t),e.each(s,function(e){this._getContainerEl().find("[data-model-cid="+e+"]").addClass("selected")},this)},_reorderCollectionBasedOnHTML:function(){var e=this;this._getContainerEl().children().each(function(){var t=i(this).attr("data-model-cid");if(t){var s=e.collection.get(t);s&&(e.collection.remove(s,{silent:!0}),e.collection.add(s,{silent:!0,sort:!e.collection.comparator}))}}),this.collection.trigger("reorder"),this._isBackboneCourierAvailable()&&this.spawn("reorder"),this.collection.comparator&&this.collection.sort()},_getModelViewConstructor:function(){return this.modelView||n},_getModelViewOptions:function(t){return e.extend({model:t},this.modelViewOptions)},_createNewModelView:function(t,i){var s=this._getModelViewConstructor(t);if(e.isUndefined(s))throw"Could not find modelView constructor for model";var n=new s(i);return n.collectionListView=this,n},_wrapModelView:function(t){var i,s=this;return this._isRenderedAsTable()?i=t.$el.attr("data-model-cid",t.model.cid):this._isRenderedAsList()&&(i="li"===t.$el.prop("tagName").toLowerCase()?t.$el.attr("data-model-cid",t.model.cid):t.$el.wrapAll("").parent()),e.isFunction(this.sortableModelsFilter)&&(this.sortableModelsFilter.call(s,t.model)||i.addClass("not-sortable")),e.isFunction(this.selectableModelsFilter)&&(this.selectableModelsFilter.call(s,t.model)||i.addClass("not-selectable")),i},_convertStringsToInts:function(t){return e.map(t,function(t){if(!e.isString(t))return t;var i=parseInt(t,10);return i==t?i:t})},_containSameElements:function(t,i){if(t.length!=i.length)return!1;var s=e.intersection(t,i).length;return s==t.length},_isRenderedAsTable:function(){return"table"===this.$el.prop("tagName").toLowerCase()},_isRenderedAsList:function(){return!this._isRenderedAsTable()},_getVisibleItemEls:function(){var e=[];return e=this._getContainerEl().find("> [data-model-cid]:not(.not-visible)")},_charCodes:{upArrow:38,downArrow:40},_isBackboneCourierAvailable:function(){return!e.isUndefined(t.Courier)},_sortStart:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid"));this.trigger("sortStart",i),this._isBackboneCourierAvailable()&&this.spawn("sortStart",{modelBeingSorted:i})},_sortChange:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid"));this.trigger("sortChange",i),this._isBackboneCourierAvailable()&&this.spawn("sortChange",{modelBeingSorted:i})},_sortStop:function(e,t){var i=this.collection.get(t.item.attr("data-model-cid")),s=this._getContainerEl(),n=s.children().index(t.item);-1==n&&this.collection.remove(i),this._reorderCollectionBasedOnHTML(),this.updateDependentControls(),this.trigger("sortStop",i,n),this._isBackboneCourierAvailable()&&this.spawn("sortStop",{modelBeingSorted:i,newIndex:n})},_receive:function(e,t){var i=t.sender,s=i.data("view");if(s&&s.collection){var n=this._getContainerEl().children().index(t.item),o=s.collection.get(t.item.attr("data-model-cid"));s.collection.remove(o),this.collection.add(o,{at:n}),o.collection=this.collection,this.setSelectedModel(o)}},_over:function(){this._getContainerEl().find("> var.empty-list-caption").hide()},_onKeydown:function(e){if(!this.processKeyEvents)return!0;var t=!1;if(1==this.getSelectedModels({by:"offset"}).length){var i=this.getSelectedModel({by:"offset"});e.which===this._charCodes.upArrow&&0!==i?(this.setSelectedModel(i-1,{by:"offset"}),t=!0):e.which===this._charCodes.downArrow&&i!==this.collection.length-1&&(this.setSelectedModel(i+1,{by:"offset"}),t=!0)}return!t},_listItem_onMousedown:function(t){if(this.selectable&&this.clickToSelect){var i=this._getClickedItemId(t);if(i){if(e.isFunction(this.selectableModelsFilter)&&!this.selectableModelsFilter.call(this,this.collection.get(i)))return;if(this.selectMultiple&&t.shiftKey){var s=-1;this.selectedItems.length>0&&this.collection.find(function(t){return s++,e.contains(this.selectedItems,t.cid)},this);var n=-1;this.collection.find(function(e){return n++,e.cid==i},this);for(var o=-1==s?n:s,l=Math.min(n,o),a=Math.max(n,o),r=[],d=l;a>=d;d++)r.push(this.collection.at(d).cid);if(this.setSelectedModels(r,{by:"cid"}),document.selection&&document.selection.empty)document.selection.empty();else if(window.getSelection){var c=window.getSelection();c&&c.removeAllRanges&&c.removeAllRanges()}}else this.selectMultiple&&(this.clickToToggle||t.metaKey)?e.contains(this.selectedItems,i)?this.setSelectedModels(e.without(this.selectedItems,i),{by:"cid"}):this.setSelectedModels(e.union(this.selectedItems,i),{by:"cid"}):this.setSelectedModels([i],{by:"cid"})}else this.setSelectedModels([])}},_listItem_onDoubleClick:function(e){var t=this._getClickedItemId(e);if(t){var i=this.collection.get(t);this.trigger("doubleClick",i),this._isBackboneCourierAvailable()&&this.spawn("doubleClick",{clickedModel:i})}},_listBackground_onClick:function(e){this.selectable&&i(e.target).is(".collection-list")&&this.setSelectedModels([])}},{setDefaultModelViewConstructor:function(e){n=e}}),t.ViewOptions={},t.ViewOptions.add=function(t,i){e.isUndefined(i)&&(i="options"),t.setOptions=function(t){var n=this,o={},l={},a=e.result(this,i);if(!e.isUndefined(a)){var r=s(a);e.each(r,function(i){if(thisOptionName=i.name,thisOptionRequired=i.required,thisOptionDefaultValue=i.defaultValue,thisOptionRequired&&(!t||!e.contains(e.keys(t),thisOptionName)&&e.isUndefined(n[thisOptionName])||e.isUndefined(t[thisOptionName])))throw Error('Required option "'+thisOptionName+'" was not supplied.');t&&thisOptionName in t?(e.isUndefined(n[thisOptionName])||(l[thisOptionName]=n[thisOptionName],o[thisOptionName]=t[thisOptionName]),n[thisOptionName]=t[thisOptionName]):!e.isUndefined(thisOptionDefaultValue)&&e.isUndefined(n[thisOptionName])&&(n[thisOptionName]=thisOptionDefaultValue)})}e.keys(o).length>0&&(e.isFunction(n.onOptionsChanged)?n.onOptionsChanged(o,l):e.isFunction(n._onOptionsChanged)&&n._onOptionsChanged(o,l))},t.getOptions=function(){var t=e.result(this,i);if(e.isUndefined(t))return[];var n=s(t),o=e.pluck(n,"name");return e.pick(this,o)}},ChildViewContainer=function(e,t){var i=function(e){this._views={},this._indexByModel={},this._indexByCustom={},this._updateLength(),t.each(e,this.add,this)};t.extend(i.prototype,{add:function(e,t){var i=e.cid;this._views[i]=e,e.model&&(this._indexByModel[e.model.cid]=i),t&&(this._indexByCustom[t]=i),this._updateLength()},findByModel:function(e){return this.findByModelCid(e.cid)},findByModelCid:function(e){var t=this._indexByModel[e];return this.findByCid(t)},findByCustom:function(e){var t=this._indexByCustom[e];return this.findByCid(t)},findByIndex:function(e){return t.values(this._views)[e]},findByCid:function(e){return this._views[e]},findIndexByCid:function(e){var i=-1,s=t.find(this._views,function(t){return i++,t.model.cid==e?t:void 0});return s?i:-1},remove:function(e){var i=e.cid;e.model&&delete this._indexByModel[e.model.cid],t.any(this._indexByCustom,function(e,t){return e===i?(delete this._indexByCustom[t],!0):void 0},this),delete this._views[i],this._updateLength()},call:function(e){this.apply(e,t.tail(arguments))},apply:function(e,i){t.each(this._views,function(s){t.isFunction(s[e])&&s[e].apply(s,i||[])})},_updateLength:function(){this.length=t.size(this._views)}});var s=["forEach","each","map","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","toArray","first","initial","rest","last","without","isEmpty","pluck"];return t.each(s,function(e){i.prototype[e]=function(){var i=t.values(this._views),s=[i].concat(t.toArray(arguments));return t[e].apply(t,s)}}),i}(t,e),t.CollectionView});
\ No newline at end of file
diff --git a/ajax/libs/backbone.collectionView/package.json b/ajax/libs/backbone.collectionView/package.json
index 08b64a66f..cc43ea2a4 100644
--- a/ajax/libs/backbone.collectionView/package.json
+++ b/ajax/libs/backbone.collectionView/package.json
@@ -30,15 +30,17 @@
"license": "MIT",
"readmeFilename": "README.md",
"dependencies": {
- "backbone" : "~1.0",
- "jquery" : "~2.1.1",
- "jquery-ui" : "~1.10.1"
+ "backbone": "~1.0",
+ "jquery": "~2.1.1",
+ "jquery-ui": "~1.10.1"
},
"npmName": "bb-collection-view",
- "npmFileMap": [{
- "basePath": "/dist/",
- "files": [
- "*.js"
- ]
- }]
-}
+ "npmFileMap": [
+ {
+ "basePath": "/dist/",
+ "files": [
+ "*.js"
+ ]
+ }
+ ]
+}
\ No newline at end of file