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