1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-12-29 02:57:15 +02:00
Sonarr/src/UI/JsLibraries/backbone.collectionview.js

1072 lines
55 KiB
JavaScript

/*!
* Backbone.CollectionView, v0.8.1
* 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 kAllowedOptions = [
"collection", "modelView", "modelViewOptions", "itemTemplate", "emptyListCaption",
"selectable", "clickToSelect", "selectableModelsFilter", "visibleModelsFilter",
"selectMultiple", "clickToToggle", "processKeyEvents", "sortable", "sortableOptions", "sortableModelsFilter", "itemTemplateFunction", "detachedRendering"
];
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 : { "*" : "." },
initialize : function( options ) {
var _this = this;
this._hasBeenRendered = false;
// default options
options = _.extend( {}, {
collection : null,
modelView : this.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
}, options );
// add each of the white-listed options to the CollectionView object itself
_.each( kAllowedOptions, function( option ) {
_this[ option ] = options[option];
} );
if( ! this.collection ) this.collection = new Backbone.Collection();
if( this._isBackboneCourierAvailable() ) {
Backbone.Courier.add( this );
}
this.$el.data( "view", this ); // needed for connected sortable lists
this.$el.addClass( "collection-list" );
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();
//this.listenTo( this.collection, "change", function() { this.render(); this.spawn( "change" ); } ); // don't want changes to models bubbling up and triggering the list's render() function
// note we do NOT call render here anymore, because if we inherit from this class we will likely call this
// function using __super__ before the rest of the initialization logic for the decedent class. however, we may
// override the render() function in that decedent class as well, and that will certainly expect all the initialization
// to be done already. so we have to make sure to not jump the gun and start rending at this point.
// this.render();
},
setOption : function( name, value ) {
var _this = this;
if( name === "collection" ) {
this._setCollection( value );
}
else {
if( _.contains( kAllowedOptions, name ) ) {
switch( name ) {
case "selectMultiple" :
this[ name ] = value;
if( !value && this.selectedItems.length > 1 )
this.setSelectedModel( _.first( this.selectedItems ), { by : "cid" } );
break;
case "selectable" :
if( !value && this.selectedItems.length > 0 )
this.setSelectedModels( [] );
this[ name ] = value;
break;
case "selectableModelsFilter" :
this[ name ] = value;
if( value && _.isFunction( value ) )
this._validateSelection();
break;
case "itemTemplate" :
this[ name ] = value;
this._updateItemTemplate();
break;
case "processKeyEvents" :
this[ name ] = value;
if( value ) this.$el.attr( "tabindex", 0 ); // so we get keyboard events
break;
case "modelView" :
this[ name ] = value;
//need to remove all old view instances
this.viewManager.each( function( view ) {
_this.viewManager.remove( view );
// destroy the View itself
view.remove();
} );
break;
default :
this[ name ] = value;
}
if( _.contains( kOptionsRequiringRerendering, name ) ) this.render();
}
else throw name + " is not an allowed option";
}
},
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;
if( this._isRenderedAsTable() )
itemElements = this.$el.find( "> tbody > [data-model-cid]:not(.not-visible)" );
else if( this._isRenderedAsList() )
itemElements = this.$el.find( "> [data-model-cid]:not(.not-visible)" );
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( ! this.selectable ) 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
if( ! _.isArray( newSelectedItems ) ) throw "Invalid parameter value";
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;
if( this._isRenderedAsTable() )
itemElements = this.$el.find( "> tbody > [data-model-cid]:not(.not-visible)" );
else if( this._isRenderedAsList() )
itemElements = this.$el.find( "> [data-model-cid]:not(.not-visible)" );
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
if( this._isRenderedAsTable() ) {
var tbodyChild = this.$el.find( "> tbody" );
if( tbodyChild.length > 0 )
modelViewContainerEl = tbodyChild;
}
if( _.isUndefined( modelViewContainerEl ) )
modelViewContainerEl = this.$el;
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;
thisModelView = oldViewManager.findByModelCid( thisModel.cid );
if( _.isUndefined( thisModelView ) ) {
// if the model view was not already created on previous render,
// then create and initialize it now.
var modelViewOptions = this._getModelViewOptions( thisModel );
thisModelView = this._createNewModelView( thisModel, modelViewOptions );
thisModelView.collectionListView = _this;
}
var thisModelViewWrapped = this._wrapModelView( thisModelView );
if( this.detachedRendering )
fragmentContainer.appendChild( thisModelViewWrapped[0] );
else
modelViewContainerEl.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 = thisModelView.render();
// return false from the view's render function to hide this item
if( renderResult === false ) {
thisModelViewWrapped.hide();
thisModelViewWrapped.addClass( "not-visible" );
}
if( _.isFunction( this.visibleModelsFilter ) ) {
if( ! this.visibleModelsFilter( thisModel ) ) {
if( thisModelViewWrapped.children().length === 1 )
thisModelViewWrapped.hide();
else thisModelView.$el.hide();
thisModelViewWrapped.addClass( "not-visible" );
}
}
this.viewManager.add( thisModelView );
}, 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 );
}
if( this.emptyListCaption ) {
var visibleView = this.viewManager.find( function( view ) {
return ! view.$el.hasClass( "not-visible" );
} );
if( _.isUndefined( visibleView ) ) {
var emptyListString;
if( _.isFunction( this.emptyListCaption ) )
emptyListString = this.emptyListCaption();
else
emptyListString = this.emptyListCaption;
var $emptyCaptionEl;
var $varEl = $( "<var class='empty-list-caption'>" + emptyListString + "</var>" );
//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( "<li class='not-sortable'></li>" ).parent().css( kStylesForEmptyListCaption );
else
$emptyListCaptionEl = $varEl.wrapAll( "<tr class='not-sortable'><td></td></tr>" ).parent().parent().css( kStylesForEmptyListCaption );
this.$el.append( $emptyListCaptionEl );
}
}
this.trigger( "render" );
if( this._isBackboneCourierAvailable() )
this.spawn( "render" );
if( this.selectable ) {
this._restoreSelection();
this.updateDependentControls();
}
if( _.isFunction( this.onAfterRender ) )
this.onAfterRender();
},
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 );
},
_validateSelectionAndRender : function() {
this._validateSelection();
this.render();
},
_registerCollectionEvents : function() {
this.listenTo( this.collection, "add", function() {
if( this._hasBeenRendered ) this.render();
if( this._isBackboneCourierAvailable() )
this.spawn( "add" );
} );
this.listenTo( this.collection, "remove", function() {
if( this._hasBeenRendered ) this.render();
if( this._isBackboneCourierAvailable() )
this.spawn( "remove" );
} );
this.listenTo( this.collection, "reset", function() {
if( this._hasBeenRendered ) this.render();
if( this._isBackboneCourierAvailable() )
this.spawn( "reset" );
} );
// It should be up to the model to rerender itself when it 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() {
if( this._hasBeenRendered ) this.render();
if( this._isBackboneCourierAvailable() )
this.spawn( "sort" );
} );
},
_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;
},
_setCollection : function( newCollection ) {
if( newCollection !== this.collection )
{
this.stopListening( this.collection );
this.collection = newCollection;
this._registerCollectionEvents();
}
if( this._hasBeenRendered ) this.render();
},
_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" ais 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 : 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.$el.find( "[data-model-cid=" + thisItemId + "]" ).removeClass( "selected" );
}, this );
var itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems;
itemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass );
_.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) {
this.$el.find( "[data-model-cid=" + thisItemId + "]" ).addClass( "selected" );
}, this );
},
_reorderCollectionBasedOnHTML : function() {
var _this = this;
this.$el.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";
return new ( modelViewConstructor )( modelViewOptions );
},
_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 <li></li> (if its not already an <li>)
// 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( "<li data-model-cid='" + modelView.model.cid + "'></li>" ).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();
},
_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._isRenderedAsTable()) ? this.$el.find( "> tbody" ) : this.$el;
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.$el.children().index( ui.item );
var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) );
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.$el.find( ".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.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];
},
// 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, _);
})();