diff --git a/ajax/libs/backbone.collectionView/0.11.4/backbone.collectionView.js b/ajax/libs/backbone.collectionView/0.11.4/backbone.collectionView.js new file mode 100644 index 000000000..03b91bda7 --- /dev/null +++ b/ajax/libs/backbone.collectionView/0.11.4/backbone.collectionView.js @@ -0,0 +1,1249 @@ +/*! +* Backbone.CollectionView, v0.11.4 +* 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 }, + { "reuseModelViews" : true }, + { "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.reuseModelViews && this.collection.get( thisModelView.model.cid ) ) + thisModelView.$el.detach(); + else + thisModelView.remove(); + }, this ); + + modelViewContainerEl.empty(); + var fragmentContainer; + + if( this.detachedRendering ) + fragmentContainer = document.createDocumentFragment(); + + this.collection.each( function( thisModel ) { + var thisModelView = oldViewManager.findByModelCid( thisModel.cid ); + if( ! this.reuseModelViews || _.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 < parentElOrDocumentFragment.children().length ) + // note this.collection.length might be greater than parentElOrDocumentFragment.children().length here + 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 && modelBeingSorted ) { + // 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 ); + } + + if( ! modelBeingSorted ) return; // something is wacky. we don't mess with this case, preferring to guarantee that we can always provide a reference to the model + + 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; +} ) ); diff --git a/ajax/libs/backbone.collectionView/0.11.4/backbone.collectionView.min.js b/ajax/libs/backbone.collectionView/0.11.4/backbone.collectionView.min.js new file mode 100644 index 000000000..7f6ad00dc --- /dev/null +++ b/ajax/libs/backbone.collectionView/0.11.4/backbone.collectionView.min.js @@ -0,0 +1,8 @@ +/*! +* Backbone.CollectionView, v0.11.4 +* Copyright (c)2013 Rotunda Software, LLC. +* Distributed under MIT license +* http://github.com/rotundasoftware/backbone-collection-view +*/ + +!function(a,b){"function"==typeof define&&define.amd?define(["underscore","backbone","jquery"],b):"undefined"!=typeof exports?module.exports=b(require("underscore"),require("backbone"),require("backbone").$):b(a._,a.Backbone,a.jQuery||a.Zepto||a.$)}(this,function(a,b,c){function d(b){var c=[];if(!a.isArray(b))throw new Error("Option declarations must be an array.");return a.each(b,function(b){var d,e,f;if(e=!1,f=void 0,a.isString(b))d=b;else{if(!a.isObject(b))throw new Error("Each element in the option declarations array must be either a string or an object.");d=a.first(a.keys(b)),f=a.clone(b[d])}"!"===d[d.length-1]&&(e=!0,d=d.slice(0,d.length-1)),c.push({name:d,required:e,defaultValue:f})}),c}var e=b.View,f="model",g=["collection","modelView","modelViewOptions","itemTemplate","selectableModelsFilter","sortableModelsFilter","visibleModelsFilter","itemTemplateFunction","detachedRendering","sortableOptions"],h={background:"transparent",border:"none","box-shadow":"none"};return b.CollectionView=b.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 b.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},{reuseModelViews:!0},{detachedRendering:!1},{emptyListCaption:null}],initialize:function(a){b.ViewOptions.add(this,"initializationOptions"),this.setOptions(a),this._hasBeenRendered=!1,this._isBackboneCourierAvailable()&&b.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(b,c){var d=!1,e=this;a.each(a.keys(b),function(f){var h=b[f],i=c[f];switch(f){case"collection":h!==i&&(e.stopListening(i),e._registerCollectionEvents());break;case"selectMultiple":!h&&e.selectedItems.length>1&&e.setSelectedModel(a.first(e.selectedItems),{by:"cid"});break;case"selectable":!h&&e.selectedItems.length>0&&e.setSelectedModels([]);break;case"selectableModelsFilter":h&&a.isFunction(h)&&e._validateSelection();break;case"itemTemplate":e._updateItemTemplate();break;case"processKeyEvents":h&&e.$el.attr("tabindex",0);break;case"modelView":e.viewManager.each(function(a){e.viewManager.remove(a),a.remove()})}a.contains(g,f)&&(d=!0)}),this._hasBeenRendered&&d&&this.render()},setOption:function(a,b){var c={};c[a]=b,this.setOptions(c)},getSelectedModel:function(b){return a.first(this.getSelectedModels(b))},getSelectedModels:function(b){var d=this;b=a.extend({},{by:f},b);var e=b.by,g=[];switch(e){case"id":a.each(this.selectedItems,function(a){g.push(d.collection.get(a).id)});break;case"cid":g=g.concat(this.selectedItems);break;case"offset":var h=0,i=this._getVisibleItemEls();i.each(function(){var a=c(this);a.is(".selected")&&g.push(h),h++});break;case"model":a.each(this.selectedItems,function(a){g.push(d.collection.get(a))});break;case"view":a.each(this.selectedItems,function(a){g.push(d.viewManager.findByModel(d.collection.get(a)))})}return g},setSelectedModels:function(b,d){if(!a.isArray(b))throw"Invalid parameter value";if(this.selectable||!(b.length>0)){d=a.extend({},{silent:!1,by:f},d);var e=d.by,g=[];switch(e){case"cid":g=b;break;case"id":this.collection.each(function(c){a.contains(b,c.id)&&g.push(c.cid)});break;case"model":g=a.pluck(b,"cid");break;case"view":a.each(b,function(a){g.push(a.model.cid)});break;case"offset":var h=0,i=this._getVisibleItemEls();i.each(function(){var d=c(this);a.contains(b,h)&&g.push(d.attr("data-model-cid")),h++})}var j=this.getSelectedModels(),k=a.clone(this.selectedItems);this.selectedItems=this._convertStringsToInts(g),this._validateSelection();var l=this.getSelectedModels();this._containSameElements(k,this.selectedItems)||(this._addSelectedClassToSelectedItems(k),d.silent||(this.trigger("selectionChanged",l,j),this._isBackboneCourierAvailable()&&this.spawn("selectionChanged",{selectedModels:l,oldSelectedModels:j})),this.updateDependentControls())}},setSelectedModel:function(a,b){a||0===a?this.setSelectedModels([a],b):this.setSelectedModels([],b)},render:function(){var b=this;this._hasBeenRendered=!0,this.selectable&&this._saveSelection();var c;c=this._getContainerEl();var d=this.viewManager;this.viewManager=new ChildViewContainer,d.each(function(a){this.reuseModelViews&&this.collection.get(a.model.cid)?a.$el.detach():a.remove()},this),c.empty();var e;if(this.detachedRendering&&(e=document.createDocumentFragment()),this.collection.each(function(b){var f=d.findByModelCid(b.cid);(!this.reuseModelViews||a.isUndefined(f))&&(f=this._createNewModelView(b,this._getModelViewOptions(b))),this._insertAndRenderModelView(f,e||c)},this),this.detachedRendering&&c.append(e),this.sortable){var f=a.extend({axis:"y",distance:10,forcePlaceholderSize:!0,start:a.bind(this._sortStart,this),change:a.bind(this._sortChange,this),stop:a.bind(this._sortStop,this),receive:a.bind(this._receive,this),over:a.bind(this._over,this)},a.result(this,"sortableOptions"));b._isRenderedAsTable()?f.items="> tbody > tr:not(.not-sortable)":b._isRenderedAsList()&&(f.items="> li:not(.not-sortable)"),this.$el=this.$el.sortable(f)}this._showEmptyListCaptionIfAppropriate(),this.trigger("render"),this._isBackboneCourierAvailable()&&this.spawn("render"),this.selectable&&(this._restoreSelection(),this.updateDependentControls()),a.isFunction(this.onAfterRender)&&this.onAfterRender()},_showEmptyListCaptionIfAppropriate:function(){if(this.emptyListCaption){var b=this._getVisibleItemEls();if(0===b.length){var d;d=a.isFunction(this.emptyListCaption)?this.emptyListCaption():this.emptyListCaption;var e=c(""+d+"");$emptyListCaptionEl=this._isRenderedAsList()?e.wrapAll("
  • ").parent().css(h):e.wrapAll("").parent().parent().css(h),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(b,c,d){var e=this._wrapModelView(b);11===c.nodeType?c.appendChild(e.get(0)):!a.isUndefined(d)&&d>=0&&d tbody");if(a.length>0)return a}return this.$el},_getClickedItemId:function(a){var b=null,d=c(a.currentTarget);if(d.closest(".collection-list").get(0)===this.$el.get(0)){var e=d.closest("[data-model-cid]");return e.length>0&&(b=e.attr("data-model-cid"),c.isNumeric(b)&&(b=parseInt(b,10))),b}},_updateItemTemplate:function(){var b;if(this.itemTemplate){if(0===c(this.itemTemplate).length)throw"Could not find item template from selector: "+this.itemTemplate;b=c(this.itemTemplate).html()}else b=this.$(".item-template").html();b&&(this.itemTemplateFunction=a.template(b))},_validateSelection:function(){var b=a.pluck(this.collection.models,"cid");this.selectedItems=a.intersection(b,this.selectedItems),a.isFunction(this.selectableModelsFilter)&&(this.selectedItems=a.filter(this.selectedItems,function(a){return this.selectableModelsFilter.call(this,this.collection.get(a))},this))},_saveSelection:function(){if(!this.selectable)throw"Attempt to save selection on non-selectable list";this.savedSelection={items:a.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(b){a.isUndefined(b)&&(b=[]);var c=b;c=a.without(c,this.selectedItems),a.each(c,function(a){this._getContainerEl().find("[data-model-cid="+a+"]").removeClass("selected")},this);var d=this.selectedItems;d=a.without(d,b),a.each(d,function(a){this._getContainerEl().find("[data-model-cid="+a+"]").addClass("selected")},this)},_reorderCollectionBasedOnHTML:function(){var a=this;this._getContainerEl().children().each(function(){var b=c(this).attr("data-model-cid");if(b){var d=a.collection.get(b);d&&(a.collection.remove(d,{silent:!0}),a.collection.add(d,{silent:!0,sort:!a.collection.comparator}))}}),this.collection.trigger("reorder"),this._isBackboneCourierAvailable()&&this.spawn("reorder"),this.collection.comparator&&this.collection.sort()},_getModelViewConstructor:function(){return this.modelView||e},_getModelViewOptions:function(b){return a.extend({model:b},this.modelViewOptions)},_createNewModelView:function(b,c){var d=this._getModelViewConstructor(b);if(a.isUndefined(d))throw"Could not find modelView constructor for model";var e=new d(c);return e.collectionListView=this,e},_wrapModelView:function(b){var c,d=this;return this._isRenderedAsTable()?c=b.$el.attr("data-model-cid",b.model.cid):this._isRenderedAsList()&&(c="li"===b.$el.prop("tagName").toLowerCase()?b.$el.attr("data-model-cid",b.model.cid):b.$el.wrapAll("
  • ").parent()),a.isFunction(this.sortableModelsFilter)&&(this.sortableModelsFilter.call(d,b.model)||c.addClass("not-sortable")),a.isFunction(this.selectableModelsFilter)&&(this.selectableModelsFilter.call(d,b.model)||c.addClass("not-selectable")),c},_convertStringsToInts:function(b){return a.map(b,function(b){if(!a.isString(b))return b;var c=parseInt(b,10);return c==b?c:b})},_containSameElements:function(b,c){if(b.length!=c.length)return!1;var d=a.intersection(b,c).length;return d==b.length},_isRenderedAsTable:function(){return"table"===this.$el.prop("tagName").toLowerCase()},_isRenderedAsList:function(){return!this._isRenderedAsTable()},_getVisibleItemEls:function(){var a=[];return a=this._getContainerEl().find("> [data-model-cid]:not(.not-visible)")},_charCodes:{upArrow:38,downArrow:40},_isBackboneCourierAvailable:function(){return!a.isUndefined(b.Courier)},_sortStart:function(a,b){var c=this.collection.get(b.item.attr("data-model-cid"));this.trigger("sortStart",c),this._isBackboneCourierAvailable()&&this.spawn("sortStart",{modelBeingSorted:c})},_sortChange:function(a,b){var c=this.collection.get(b.item.attr("data-model-cid"));this.trigger("sortChange",c),this._isBackboneCourierAvailable()&&this.spawn("sortChange",{modelBeingSorted:c})},_sortStop:function(a,b){var c=this.collection.get(b.item.attr("data-model-cid")),d=this._getContainerEl(),e=d.children().index(b.item);-1==e&&c&&this.collection.remove(c),c&&(this._reorderCollectionBasedOnHTML(),this.updateDependentControls(),this.trigger("sortStop",c,e),this._isBackboneCourierAvailable()&&this.spawn("sortStop",{modelBeingSorted:c,newIndex:e}))},_receive:function(a,b){var c=b.sender,d=c.data("view");if(d&&d.collection){var e=this._getContainerEl().children().index(b.item),f=d.collection.get(b.item.attr("data-model-cid"));d.collection.remove(f),this.collection.add(f,{at:e}),f.collection=this.collection,this.setSelectedModel(f)}},_over:function(){this._getContainerEl().find("> var.empty-list-caption").hide()},_onKeydown:function(a){if(!this.processKeyEvents)return!0;var b=!1;if(1==this.getSelectedModels({by:"offset"}).length){var c=this.getSelectedModel({by:"offset"});a.which===this._charCodes.upArrow&&0!==c?(this.setSelectedModel(c-1,{by:"offset"}),b=!0):a.which===this._charCodes.downArrow&&c!==this.collection.length-1&&(this.setSelectedModel(c+1,{by:"offset"}),b=!0)}return!b},_listItem_onMousedown:function(b){if(this.selectable&&this.clickToSelect){var c=this._getClickedItemId(b);if(c){if(a.isFunction(this.selectableModelsFilter)&&!this.selectableModelsFilter.call(this,this.collection.get(c)))return;if(this.selectMultiple&&b.shiftKey){var d=-1;this.selectedItems.length>0&&this.collection.find(function(b){return d++,a.contains(this.selectedItems,b.cid)},this);var e=-1;this.collection.find(function(a){return e++,a.cid==c},this);for(var f=-1==d?e:d,g=Math.min(e,f),h=Math.max(e,f),i=[],j=g;h>=j;j++)i.push(this.collection.at(j).cid);if(this.setSelectedModels(i,{by:"cid"}),document.selection&&document.selection.empty)document.selection.empty();else if(window.getSelection){var k=window.getSelection();k&&k.removeAllRanges&&k.removeAllRanges()}}else this.selectMultiple&&(this.clickToToggle||b.metaKey)?a.contains(this.selectedItems,c)?this.setSelectedModels(a.without(this.selectedItems,c),{by:"cid"}):this.setSelectedModels(a.union(this.selectedItems,c),{by:"cid"}):this.setSelectedModels([c],{by:"cid"})}else this.setSelectedModels([])}},_listItem_onDoubleClick:function(a){var b=this._getClickedItemId(a);if(b){var c=this.collection.get(b);this.trigger("doubleClick",c),this._isBackboneCourierAvailable()&&this.spawn("doubleClick",{clickedModel:c})}},_listBackground_onClick:function(a){this.selectable&&c(a.target).is(".collection-list")&&this.setSelectedModels([])}},{setDefaultModelViewConstructor:function(a){e=a}}),b.ViewOptions={},b.ViewOptions.add=function(b,c){a.isUndefined(c)&&(c="options"),b.setOptions=function(b){var e=this,f={},g={},h=a.result(this,c);if(!a.isUndefined(h)){var i=d(h);a.each(i,function(c){if(thisOptionName=c.name,thisOptionRequired=c.required,thisOptionDefaultValue=c.defaultValue,thisOptionRequired&&(!b||!a.contains(a.keys(b),thisOptionName)&&a.isUndefined(e[thisOptionName])||a.isUndefined(b[thisOptionName])))throw new Error('Required option "'+thisOptionName+'" was not supplied.');b&&thisOptionName in b?(a.isUndefined(e[thisOptionName])||(g[thisOptionName]=e[thisOptionName],f[thisOptionName]=b[thisOptionName]),e[thisOptionName]=b[thisOptionName]):!a.isUndefined(thisOptionDefaultValue)&&a.isUndefined(e[thisOptionName])&&(e[thisOptionName]=thisOptionDefaultValue)})}a.keys(f).length>0&&(a.isFunction(e.onOptionsChanged)?e.onOptionsChanged(f,g):a.isFunction(e._onOptionsChanged)&&e._onOptionsChanged(f,g))},b.getOptions=function(){var b=a.result(this,c);if(a.isUndefined(b))return[];var e=d(b),f=a.pluck(e,"name");return a.pick(this,f)}},ChildViewContainer=function(a,b){var c=function(a){this._views={},this._indexByModel={},this._indexByCustom={},this._updateLength(),b.each(a,this.add,this)};b.extend(c.prototype,{add:function(a,b){var c=a.cid;this._views[c]=a,a.model&&(this._indexByModel[a.model.cid]=c),b&&(this._indexByCustom[b]=c),this._updateLength()},findByModel:function(a){return this.findByModelCid(a.cid)},findByModelCid:function(a){var b=this._indexByModel[a];return this.findByCid(b)},findByCustom:function(a){var b=this._indexByCustom[a];return this.findByCid(b)},findByIndex:function(a){return b.values(this._views)[a]},findByCid:function(a){return this._views[a]},findIndexByCid:function(a){var c=-1,d=b.find(this._views,function(b){return c++,b.model.cid==a?b:void 0});return d?c:-1},remove:function(a){var c=a.cid;a.model&&delete this._indexByModel[a.model.cid],b.any(this._indexByCustom,function(a,b){return a===c?(delete this._indexByCustom[b],!0):void 0},this),delete this._views[c],this._updateLength()},call:function(a){this.apply(a,b.tail(arguments))},apply:function(a,c){b.each(this._views,function(d){b.isFunction(d[a])&&d[a].apply(d,c||[])})},_updateLength:function(){this.length=b.size(this._views)}});var d=["forEach","each","map","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","toArray","first","initial","rest","last","without","isEmpty","pluck"];return b.each(d,function(a){c.prototype[a]=function(){var c=b.values(this._views),d=[c].concat(b.toArray(arguments));return b[a].apply(b,d)}}),c}(b,a),b.CollectionView}); \ No newline at end of file diff --git a/ajax/libs/backbone.collectionView/package.json b/ajax/libs/backbone.collectionView/package.json index 3dc56d6e2..6c25a8940 100644 --- a/ajax/libs/backbone.collectionView/package.json +++ b/ajax/libs/backbone.collectionView/package.json @@ -1,7 +1,7 @@ { "name": "backbone.collectionView", "filename": "backbone.collectionView.min.js", - "version": "0.11.3", + "version": "0.11.4", "description": "Easily render backbone.js collections with support for automatic selection of models in response to clicks, reordering models via drag and drop, and more.", "main": "dist/backbone.collectionView.js", "devDependencies": {