Commits

Carl Eberhard  committed 7005608

HDA/Collections client-side refactoring for drill down view

  • Participants
  • Parent commits bdc4017

Comments (0)

Files changed (48)

File static/scripts/mvc/base-mvc.js

 
 //==============================================================================
 /** Backbone model that syncs to the browser's sessionStorage API.
+ *      This all largely happens behind the scenes and no special calls are required.
  */
 var SessionStorageModel = Backbone.Model.extend({
     initialize : function( initialAttrs ){
 
 
 //==============================================================================
+/** A mixin for models that allow T/F/Matching to their attributes - useful when
+ *      searching or filtering collections of models.
+ * @example:
+ *      see hda-model for searchAttribute and searchAliases definition examples.
+ *      see history-contents.matches for how collections are filtered
+ *      and see readonly-history-panel.searchHdas for how user input is connected to the filtering
+ */
+var SearchableModelMixin = {
+
+    /** what attributes of an HDA will be used in a text search */
+    searchAttributes : [
+        // override
+    ],
+
+    /** our attr keys don't often match the labels we display to the user - so, when using
+     *      attribute specifiers ('name="bler"') in a term, allow passing in aliases for the
+     *      following attr keys.
+     */
+    searchAliases : {
+        // override
+    },
+
+    /** search the attribute with key attrKey for the string searchFor; T/F if found */
+    searchAttribute : function( attrKey, searchFor ){
+        var attrVal = this.get( attrKey );
+        //this.debug( 'searchAttribute', attrKey, attrVal, searchFor );
+        // bail if empty searchFor or unsearchable values
+        if( !searchFor
+        ||  ( attrVal === undefined || attrVal === null ) ){
+            return false;
+        }
+        // pass to sep. fn for deep search of array attributes
+        if( _.isArray( attrVal ) ){ return this._searchArrayAttribute( attrVal, searchFor ); }
+        return ( attrVal.toString().toLowerCase().indexOf( searchFor.toLowerCase() ) !== -1 );
+    },
+
+    /** deep(er) search for array attributes; T/F if found */
+    _searchArrayAttribute : function( array, searchFor ){
+        //this.debug( '_searchArrayAttribute', array, searchFor );
+        searchFor = searchFor.toLowerCase();
+        //precondition: searchFor has already been validated as non-empty string
+        //precondition: assumes only 1 level array
+        //TODO: could possibly break up searchFor more (CSV...)
+        return _.any( array, function( elem ){
+            return ( elem.toString().toLowerCase().indexOf( searchFor.toLowerCase() ) !== -1 );
+        });
+    },
+
+    /** search all searchAttributes for the string searchFor,
+     *      returning a list of keys of attributes that contain searchFor
+     */
+    search : function( searchFor ){
+        var model = this;
+        return _.filter( this.searchAttributes, function( key ){
+            return model.searchAttribute( key, searchFor );
+        });
+    },
+
+    /** alias of search, but returns a boolean; accepts attribute specifiers where
+     *      the attributes searched can be narrowed to a single attribute using
+     *      the form: matches( 'genome_build=hg19' )
+     *      (the attribute keys allowed can also be aliases to the true attribute key;
+     *          see searchAliases above)
+     *  @param {String} term   plain text or ATTR_SPECIFIER sep. key=val pair
+     *  @returns {Boolean} was term found in (any) attribute(s)
+     */
+    matches : function( term ){
+        var ATTR_SPECIFIER = '=',
+            split = term.split( ATTR_SPECIFIER );
+        // attribute is specified - search only that
+        if( split.length >= 2 ){
+            var attrKey = split[0];
+            attrKey = this.searchAliases[ attrKey ] || attrKey;
+            return this.searchAttribute( attrKey, split[1] );
+        }
+        // no attribute is specified - search all attributes in searchAttributes
+        return !!this.search( term ).length;
+    },
+
+    /** an implicit AND search for all terms; IOW, a model must match all terms given
+     *      where terms is a whitespace separated value string.
+     *      e.g. given terms of: 'blah bler database=hg19'
+     *          an HDA would have to have attributes containing blah AND bler AND a genome_build == hg19
+     *      To include whitespace in terms: wrap the term in double quotations (name="blah bler").
+     */
+    matchesAll : function( terms ){
+        var model = this;
+        // break the terms up by whitespace and filter out the empty strings
+        terms = terms.match( /(".*"|\w*=".*"|\S*)/g ).filter( function( s ){ return !!s; });
+        return _.all( terms, function( term ){
+            term = term.replace( /"/g, '' );
+            return model.matches( term );
+        });
+    }
+};
+
+
+//==============================================================================
+/** A view that renders hidden and shows when some activator is clicked.
+ *      options:
+ *          showFn: the effect used to show/hide the View (defaults to jq.toggle)
+ *          $elementShown: some jqObject (defaults to this.$el) to be shown/hidden
+ *          onShowFirstTime: fn called the first time the view is shown
+ *          onshow: fn called every time the view is shown
+ *          onhide: fn called every time the view is hidden
+ *      events:
+ *          hiddenUntilActivated:shown (the view is passed as an arg)
+ *          hiddenUntilActivated:hidden (the view is passed as an arg)
+ *      instance vars:
+ *          view.hidden {boolean} is the view in the hidden state
+ */
 var HiddenUntilActivatedViewMixin = /** @lends hiddenUntilActivatedMixin# */{
 //TODO: since this is a mixin, consider moving toggle, hidden into HUAVOptions
 
-    /** */
+    /** call this in your initialize to set up the mixin
+     *  @param {jQuery} $activator the 'button' that's clicked to show/hide the view
+     *  @param {Object} hash with mixin options
+     */
     hiddenUntilActivated : function( $activator, options ){
         // call this in your view's initialize fn
         options = options || {};
+//TODO: flesh out options - show them all here
         this.HUAVOptions = {
             $elementShown   : this.$el,
             showFn          : jQuery.prototype.toggle,
         }
     },
 
+//TODO:?? remove? use .hidden?
+    /** returns T/F if the view is hidden */
     isHidden : function(){
         return ( this.HUAVOptions.$elementShown.is( ':hidden' ) );
     },
 
-    /** */
+    /** toggle the hidden state, show/hide $elementShown, call onshow/hide, trigger events */
     toggle : function(){
+//TODO: more specific name - toggle is too general
         // can be called manually as well with normal toggle arguments
         //TODO: better as a callback (when the show/hide is actually done)
         // show
     }
 };
 
+
 //==============================================================================
+/** Function that allows mixing of hashs into bbone MVC while showing the mixins first
+ *      (before the more local class overrides/hash).
+ *      Basically, a simple reversal of param order on _.defaults() - to show mixins in top of definition.
+ *  @example:
+ *      var NewModel = Something.extend( mixin( MyMixinA, MyMixinB, { ... myVars : ... }) );
+ *
+ *  NOTE: this does not combine any hashes (like events, etc.) and you're expected to handle that
+ */
 function mixin( mixinHash1, /* mixinHash2, etc: ... variadic */ propsHash ){
-    // usage: var NewModel = Something.extend( mixin( MyMixinA, MyMixinB, { ... }) );
-    //NOTE: this does not combine any hashes (like events, etc.) and you're expected to handle that
-
-    // simple reversal of param order on _.defaults() - to show mixins in top of definition
     var args = Array.prototype.slice.call( arguments, 0 ),
         lastArg = args.pop();
     args.unshift( lastArg );
     return _.defaults.apply( _, args );
 }
 
+//==============================================================================
+/** Return an underscore template fn from an array of strings.
+ *  @param {String[]} template      the template strings to compile into the underscore template fn
+ *  @param {String} jsonNamespace   an optional namespace for the json data passed in (defaults to 'model')
+ *  @returns {Function} the (wrapped) underscore template fn
+ *      The function accepts:
+ *
+ *  The template strings can access:
+ *      the json/model hash using model ("<%- model.myAttr %>) using the jsonNamespace above
+ *      _l: the localizer function
+ *      view (if passed): ostensibly, the view using the template (handy for view instance vars)
+ *      Because they're namespaced, undefined attributes will not throw an error.
+ *
+ *  @example:
+ *      templateBler : BASE_MVC.wrapTemplate([
+ *          '<div class="myclass <%- mynamespace.modelClass %>">',
+ *              '<span><% print( _l( mynamespace.message ) ); %>:<%= view.status %></span>'
+ *          '</div>'
+ *      ], 'mynamespace' )
+ *
+ *  Meant to be called in a View's definition in order to compile only once.
+ *
+ */
+function wrapTemplate( template, jsonNamespace ){
+    jsonNamespace = jsonNamespace || 'model';
+    var templateFn = _.template( template.join( '' ) );
+    return function( json, view ){
+        var templateVars = { view : view || {}, _l : _l };
+        templateVars[ jsonNamespace ] = json || {};
+        return templateFn( templateVars );
+    };
+}
 
 //==============================================================================
+/** A view which, when first rendered, shows only summary data/attributes, but
+ *      can be expanded to show further details (and optionally fetch those
+ *      details from the server).
+ */
 var ExpandableView = Backbone.View.extend( LoggableMixin ).extend({
 //TODO: Although the reasoning behind them is different, this shares a lot with HiddenUntilActivated above: combine them
     //PRECONDITION: model must have method hasDetails
+    //PRECONDITION: subclasses must have templates.el and templates.details
 
     initialize : function( attributes ){
         /** are the details of this view expanded/shown or not? */
         this.expanded   = attributes.expanded || false;
-        this.log( '\t expanded:', this.expanded );
+        //this.log( '\t expanded:', this.expanded );
     },
 
     // ........................................................................ render main
-//TODO: for lack of a better place, add rendering logic here
+    /** jq fx speed */
     fxSpeed : 'fast',
 
     /** Render this content, set up ui.
-     *  @param {Integer} speed   the speed of the render
-     *  @fires rendered when rendered
-     *  @fires rendered:ready when first rendered and NO running HDAs
-     *  @returns {Object} this HDABaseView
+     *  @param {Number or String} speed   the speed of the render
      */
     render : function( speed ){
         var $newRender = this._buildNewRender();
+        this._setUpBehaviors( $newRender );
         this._queueNewRender( $newRender, speed );
         return this;
     },
 
+    /** Build a temp div containing the new children for the view's $el.
+     *      If the view is already expanded, build the details as well.
+     */
     _buildNewRender : function(){
         // create a new render using a skeleton template, render title buttons, render body, and set up events, etc.
-        var $newRender = $( this.templates.skeleton( this.model.toJSON() ) );
+        var $newRender = $( this.templates.el( this.model.toJSON(), this ) );
         if( this.expanded ){
-            $newRender.children( '.details' ).replaceWith( this._renderDetails() );
+            this.$details( $newRender ).replaceWith( this._renderDetails().show() );
         }
-        this._setUpBehaviors( $newRender );
         return $newRender;
     },
 
-    /** Fade out the old el, replace with new dom, then fade in.
-     *  @param {Boolean} fade   whether or not to fade out/in when re-rendering
+    /** Fade out the old el, swap in the new contents, then fade in.
+     *  @param {Number or String} speed   jq speed to use for rendering effects
      *  @fires rendered when rendered
-     *  @fires rendered:ready when first rendered and NO running HDAs
      */
     _queueNewRender : function( $newRender, speed ) {
         speed = ( speed === undefined )?( this.fxSpeed ):( speed );
         ]);
     },
 
+    /** empty out the current el, move the $newRender's children in */
     _swapNewRender : function( $newRender ){
         return this.$el.empty().attr( 'class', this.className ).append( $newRender.children() );
     },
     /** set up js behaviors, event handlers for elements within the given container
      *  @param {jQuery} $container jq object that contains the elements to process (defaults to this.$el)
      */
-    _setUpBehaviors : function( $container ){
-        $container = $container || this.$el;
+    _setUpBehaviors : function( $where ){
+        $where = $where || this.$el;
         // set up canned behavior on children (bootstrap, popupmenus, editable_text, etc.)
-        make_popup_menus( $container );
-        $container.find( '[title]' ).tooltip({ placement : 'bottom' });
+        make_popup_menus( $where );
+        $where.find( '[title]' ).tooltip({ placement : 'bottom' });
     },
 
     // ......................................................................... details
+    /** shortcut to details DOM (as jQ) */
+    $details : function( $where ){
+        $where = $where || this.$el;
+        return $where.find( '.details' );
+    },
+
+    /** build the DOM for the details and set up behaviors on it */
     _renderDetails : function(){
-        // override this
-        return null;
+        var $newDetails = $( this.templates.details( this.model.toJSON(), this ) );
+        this._setUpBehaviors( $newDetails );
+        return $newDetails;
     },
 
     // ......................................................................... expansion/details
-    /** Show or hide the body/details of history content.
+    /** Show or hide the details
      *  @param {Boolean} expand if true, expand; if false, collapse
      */
     toggleExpanded : function( expand ){
      */
     expand : function(){
         var view = this;
-
-        function _renderDetailsAndExpand(){
-            view.$( '.details' ).replaceWith( view._renderDetails() );
-            // needs to be set after the above or the slide will not show
-            view.expanded = true;
-            view.$( '.details' ).slideDown( view.fxSpeed, function(){
+        return view._fetchModelDetails()
+            .always(function(){
+                var $newDetails = view._renderDetails();
+                view.$details().replaceWith( $newDetails );
+                // needs to be set after the above or the slide will not show
+                view.expanded = true;
+                $newDetails.slideDown( view.fxSpeed, function(){
                     view.trigger( 'expanded', view );
                 });
+            });
+    },
+
+    /** Check for model details and, if none, fetch them.
+     *  @returns {jQuery.promise} the model.fetch.xhr if details are being fetched, an empty promise if not
+     */
+    _fetchModelDetails : function(){
+        if( !this.model.hasDetails() ){
+            return this.model.fetch();
         }
-//TODO:?? remove
-        // fetch first if no details in the model
-        if( !view.model.hasDetails() ){
-            // we need the change event on HDCA's for the elements to be processed - so silent == false
-            view.model.fetch().always( function( model ){
-                _renderDetailsAndExpand();
-            });
-//TODO: no error handling
-        } else {
-            _renderDetailsAndExpand();
-        }
+        return jQuery.when();
     },
 
     /** Hide the body/details of an HDA.
     collapse : function(){
         var view = this;
         view.expanded = false;
-        this.$( '.details' ).slideUp( view.fxSpeed, function(){
+        this.$details().slideUp( view.fxSpeed, function(){
             view.trigger( 'collapsed', view );
         });
     }
 
 });
 
+
+//==============================================================================
+/** Mixin for views that can be dragged and dropped
+ *      Allows for the drag behavior to be turned on/off, setting/removing jQuery event
+ *          handlers each time.
+ *      dataTransfer data is set to the JSON string of the view's model.toJSON
+ *      Override '$dragHandle' to define the draggable DOM sub-element.
+ */
+var DraggableViewMixin = {
+
+    /** set up instance vars to track whether this view is currently draggable */
+    initialize : function( attributes ){
+        /** is the body of this hda view expanded/not? */
+        this.draggable  = attributes.draggable || false;
+    },
+
+    /** what part of the view's DOM triggers the dragging */
+    $dragHandle : function(){
+//TODO: make abstract/general - move this to listItem
+        // override to the element you want to be your view's handle
+        return this.$( '.title-bar' );
+    },
+
+    /** toggle whether this view is draggable */
+    toggleDraggable : function(){
+        if( this.draggable ){
+            this.draggableOff();
+        } else {
+            this.draggableOn();
+        }
+    },
+
+    /** allow the view to be dragged, set up event handlers */
+    draggableOn : function(){
+        this.draggable = true;
+        //TODO: I have no idea why this doesn't work with the events hash or jq.on()...
+        //this.$el.find( '.title-bar' )
+        //    .attr( 'draggable', true )
+        //    .bind( 'dragstart', this.dragStartHandler, false )
+        //    .bind( 'dragend',   this.dragEndHandler,   false );
+        this.dragStartHandler = _.bind( this._dragStartHandler, this );
+        this.dragEndHandler   = _.bind( this._dragEndHandler,   this );
+
+        var handle = this.$dragHandle().attr( 'draggable', true ).get(0);
+        handle.addEventListener( 'dragstart', this.dragStartHandler, false );
+        handle.addEventListener( 'dragend',   this.dragEndHandler,   false );
+    },
+
+    /** turn of view dragging and remove event listeners */
+    draggableOff : function(){
+        this.draggable = false;
+        var handle = this.$dragHandle().attr( 'draggable', false ).get(0);
+        handle.removeEventListener( 'dragstart', this.dragStartHandler, false );
+        handle.removeEventListener( 'dragend',   this.dragEndHandler,   false );
+    },
+
+    /** sets the dataTransfer data to the model's toJSON
+     *  @fires dragstart (bbone event) which is passed this view
+     */
+    _dragStartHandler : function( event ){
+        //this.debug( 'dragStartHandler:', this, event, arguments )
+        this.trigger( 'dragstart', this );
+        event.dataTransfer.effectAllowed = 'move';
+        //TODO: all except IE: should be 'application/json', IE: must be 'text'
+        event.dataTransfer.setData( 'text', JSON.stringify( this.model.toJSON() ) );
+        return false;
+    },
+
+    /** handle the dragend
+     *  @fires dragend (bbone event) which is passed this view
+     */
+    _dragEndHandler : function( event ){
+        this.trigger( 'dragend', this );
+        //this.debug( 'dragEndHandler:', event )
+        return false;
+    }
+};
+
+
+//==============================================================================
+/** Mixin that allows a view to be selected (gen. from a list).
+ *      Selection controls ($selector) may be hidden/shown/toggled.
+ *          The bbone event 'selectable' is fired when the controls are shown/hidden (passed T/F).
+ *      Default rendering is a font-awesome checkbox.
+ *      Default selector is '.selector' within the view's $el.
+ *      The bbone events 'selected' and 'de-selected' are fired when the $selector is clicked.
+ *          Both events are passed the view and the (jQuery) event.
+ */
+var SelectableViewMixin = {
+
+    /** Set up instance state vars for whether the selector is shown and whether the view has been selected */
+    initialize : function( attributes ){
+        /** is the view currently in selection mode? */
+        this.selectable = attributes.selectable || false;
+        /** is the view currently selected? */
+        this.selected   = attributes.selected || false;
+    },
+    
+    /** $el sub-element where the selector is rendered and what can be clicked to select. */
+    $selector : function(){
+        return this.$( '.selector' );
+    },
+
+    /** How the selector is rendered - defaults to font-awesome checkbox */
+    _renderSelected : function(){
+        // override
+        this.$selector().find( 'span' )
+            .toggleClass( 'fa-check-square-o', this.selected ).toggleClass( 'fa-square-o', !this.selected );
+    },
+
+    /** Toggle whether the selector is shown */
+    toggleSelector : function(){
+//TODO: use this.selectable
+        if( !this.$selector().is( ':visible' ) ){
+            this.showSelector();
+        } else {
+            this.hideSelector();
+        }
+    },
+
+    /** Display the selector control.
+     *  @param {Number} a jQuery fx speed
+     *  @fires: selectable which is passed true (IOW, the selector is shown) and the view
+     */
+    showSelector : function( speed ){
+        speed = speed !== undefined? speed : this.fxSpeed;
+        // make sure selected state is represented properly
+        this.selectable = true;
+        this.trigger( 'selectable', true, this );
+        this._renderSelected();
+        this.$selector().show( speed );
+    },
+
+    /** remove the selector control
+     *  @param {Number} a jQuery fx speed
+     *  @fires: selectable which is passed false (IOW, the selector is not shown) and the view
+     */
+    hideSelector : function( speed ){
+        speed = speed !== undefined? speed : this.fxSpeed;
+        // reverse the process from showSelect
+        this.selectable = false;
+        this.trigger( 'selectable', false, this );
+        this.$selector().hide( speed );
+    },
+
+    /** Toggle whether the view is selected */
+    toggleSelect : function( event ){
+        if( this.selected ){
+            this.deselect( event );
+        } else {
+            this.select( event );
+        }
+    },
+
+    /** Select this view and re-render the selector control to show it
+     *  @param {Event} a jQuery event that caused the selection
+     *  @fires: selected which is passed the view and the DOM event that triggered it (optionally)
+     */
+    select : function( event ){
+        // switch icon, set selected, and trigger event
+        if( !this.selected ){
+            this.trigger( 'selected', this, event );
+            this.selected = true;
+            this._renderSelected();
+        }
+        return false;
+    },
+
+    /** De-select this view and re-render the selector control to show it
+     *  @param {Event} a jQuery event that caused the selection
+     *  @fires: de-selected which is passed the view and the DOM event that triggered it (optionally)
+     */
+    deselect : function( event ){
+        // switch icon, set selected, and trigger event
+        if( this.selected ){
+            this.trigger( 'de-selected', this, event );
+            this.selected = false;
+            this._renderSelected();
+        }
+        return false;
+    }
+};
+
+
+//==============================================================================
+/** A view that is displayed in some larger list/grid/collection.
+ *      Inherits from Expandable, Selectable, Draggable.
+ *  The DOM contains warnings, a title bar, and a series of primary action controls.
+ *      Primary actions are meant to be easily accessible item functions (such as delete)
+ *      that are rendered in the title bar.
+ *
+ *  Details are rendered when the user clicks the title bar or presses enter/space when
+ *      the title bar is in focus.
+ *
+ *  Designed as a base class for history panel contents - but usable elsewhere (I hope).
+ */
+var ListItemView = ExpandableView.extend( mixin( SelectableViewMixin, DraggableViewMixin, {
+
+//TODO: that's a little contradictory
+    tagName     : 'div',
+    className   : 'list-item',
+
+    /** Set up the base class and all mixins */
+    initialize : function( attributes ){
+        ExpandableView.prototype.initialize.call( this, attributes );
+        SelectableViewMixin.initialize.call( this, attributes );
+        DraggableViewMixin.initialize.call( this, attributes );
+    },
+
+    // ........................................................................ rendering
+    /** In this override, call methods to build warnings, titlebar and primary actions */
+    _buildNewRender : function(){
+        var $newRender = ExpandableView.prototype._buildNewRender.call( this );
+        $newRender.find( '.warnings' ).replaceWith( this._renderWarnings() );
+        $newRender.find( '.title-bar' ).replaceWith( this._renderTitleBar() );
+        $newRender.find( '.primary-actions' ).append( this._renderPrimaryActions() );
+        $newRender.find( '.subtitle' ).replaceWith( this._renderSubtitle() );
+        return $newRender;
+    },
+
+    /** In this override, render the selector controls and set up dragging before the swap */
+    _swapNewRender : function( $newRender ){
+        ExpandableView.prototype._swapNewRender.call( this, $newRender );
+        if( this.selectable ){ this.showSelector( 0 ); }
+        if( this.draggable ){ this.draggableOn(); }
+        return this.$el;
+    },
+
+    /** Render any warnings the item may need to show (e.g. "I'm deleted") */
+    _renderWarnings : function(){
+        var view = this,
+            $warnings = $( '<div class="warnings"></div>' ),
+            json = view.model.toJSON();
+//TODO:! unordered (map)
+        _.each( view.templates.warnings, function( templateFn ){
+            $warnings.append( $( templateFn( json, view ) ) );
+        });
+        return $warnings;
+    },
+
+    /** Render the title bar (the main/exposed SUMMARY dom element) */
+    _renderTitleBar : function(){
+        return $( this.templates.titleBar( this.model.toJSON(), this ) );
+    },
+
+    /** Return an array of jQ objects containing common/easily-accessible item controls */
+    _renderPrimaryActions : function(){
+        // override this
+        return [];
+    },
+
+    /** Render the title bar (the main/exposed SUMMARY dom element) */
+    _renderSubtitle : function(){
+        return $( this.templates.subtitle( this.model.toJSON(), this ) );
+    },
+
+    // ......................................................................... events
+    /** event map */
+    events : {
+        // expand the body when the title is clicked or when in focus and space or enter is pressed
+        'click .title-bar'      : '_clickTitleBar',
+        'keydown .title-bar'    : '_keyDownTitleBar',
+
+        // dragging - don't work, originalEvent === null
+        //'dragstart .dataset-title-bar'  : 'dragStartHandler',
+        //'dragend .dataset-title-bar'    : 'dragEndHandler'
+
+        'click .selector'       : 'toggleSelect'
+    },
+
+    /** expand when the title bar is clicked */
+    _clickTitleBar : function( event ){
+        event.stopPropagation();
+        this.toggleExpanded();
+    },
+
+    /** expand when the title bar is in focus and enter or space is pressed */
+    _keyDownTitleBar : function( event ){
+        // bail (with propagation) if keydown and not space or enter
+        var KEYCODE_SPACE = 32, KEYCODE_RETURN = 13;
+        if( event && ( event.type === 'keydown' )
+        &&( event.keyCode === KEYCODE_SPACE || event.keyCode === KEYCODE_RETURN ) ){
+            this.toggleExpanded();
+            event.stopPropagation();
+            return false;
+        }
+        return true;
+    },
+
+    // ......................................................................... misc
+    /** String representation */
+    toString : function(){
+        var modelString = ( this.model )?( this.model + '' ):( '(no model)' );
+        return 'ListItemView(' + modelString + ')';
+    }
+}));
+
+// ............................................................................ TEMPLATES
+/** underscore templates */
+ListItemView.prototype.templates = (function(){
+//TODO: move to require text! plugin
+
+    var elTemplato = wrapTemplate([
+        '<div class="list-element">',
+            // errors, messages, etc.
+            '<div class="warnings"></div>',
+
+            // multi-select checkbox
+            '<div class="selector">',
+                '<span class="fa fa-2x fa-square-o"></span>',
+            '</div>',
+            // space for title bar buttons - gen. floated to the right
+            '<div class="primary-actions"></div>',
+            '<div class="title-bar"></div>',
+
+            // expandable area for more details
+            '<div class="details"></div>',
+        '</div>'
+    ]);
+
+    var warnings = {};
+
+    var titleBarTemplate = wrapTemplate([
+        // adding a tabindex here allows focusing the title bar and the use of keydown to expand the dataset display
+        '<div class="title-bar clear" tabindex="0">',
+//TODO: prob. belongs in dataset-list-item
+            '<span class="state-icon"></span>',
+            '<div class="title">',
+                '<span class="name"><%- element.name %></span>',
+            '</div>',
+            '<div class="subtitle"></div>',
+        '</div>'
+    ], 'element' );
+
+    var subtitleTemplate = wrapTemplate([
+        // override this
+        '<div class="subtitle"></div>'
+    ]);
+
+    var detailsTemplate = wrapTemplate([
+        // override this
+        '<div class="details"></div>'
+    ]);
+
+    return {
+        el          : elTemplato,
+        warnings    : warnings,
+        titleBar    : titleBarTemplate,
+        subtitle    : subtitleTemplate,
+        details     : detailsTemplate
+    };
+}());
+
+
 //==============================================================================
     return {
         LoggableMixin                   : LoggableMixin,
         SessionStorageModel             : SessionStorageModel,
+        SearchableModelMixin            : SearchableModelMixin,
         HiddenUntilActivatedViewMixin   : HiddenUntilActivatedViewMixin,
         mixin                           : mixin,
-        ExpandableView                  : ExpandableView
+        wrapTemplate                    : wrapTemplate,
+        ExpandableView                  : ExpandableView,
+        DraggableViewMixin              : DraggableViewMixin,
+        SelectableViewMixin             : SelectableViewMixin,
+        ListItemView                    : ListItemView
     };
 });

File static/scripts/mvc/collection/collection-model.js

 define([
-    "mvc/dataset/hda-model",
+    "mvc/dataset/dataset-model",
     "mvc/base-mvc",
     "utils/localization"
-], function( HDA_MODEL, BASE_MVC, _l ){
+], function( DATASET, BASE_MVC, _l ){
 //==============================================================================
-/** @class Backbone model for Dataset collection elements.
- *      DC Elements contain a sub-model named 'object'. This class moves that
- *      'object' from the JSON in the attributes list to a full, instantiated
- *      sub-model found in this.object. This is done on intialization and
- *      everytime the 'change:object' event is fired.
- *
- *  @borrows LoggableMixin#logger as #logger
- *  @borrows LoggableMixin#log as #log
- *  @constructs
+/*
+Notes:
+
+Terminology:
+    DatasetCollection/DC : a container of datasets or nested DatasetCollections
+    Element/DatasetCollectionElement/DCE : an item contained in a DatasetCollection
+    HistoryDatasetCollectionAssociation/HDCA: a DatasetCollection contained in a history
+
+
+This all seems too complex unfortunately:
+
+- Terminology collision between DatasetCollections (DCs) and Backbone Collections.
+- In the DatasetCollections API JSON, DC Elements use a 'Has A' stucture to *contain*
+    either a dataset or a nested DC. This would make the hierarchy much taller. I've
+    decided to merge the contained JSON with the DC element json - making the 'has a'
+    relation into an 'is a' relation. This seems simpler to me and allowed a lot of
+    DRY in both models and views, but may make tracking or tracing within these models
+    more difficult (since DatasetCollectionElements are now *also* DatasetAssociations
+    or DatasetCollections (nested)). This also violates the rule of thumb about
+    favoring aggregation over inheritance.
+- Currently, there are three DatasetCollection subclasses: List, Pair, and ListPaired.
+    These each should a) be usable on their own, b) be usable in the context of
+    nesting within a collection model (at least in the case of ListPaired), and
+    c) be usable within the context of other container models (like History or
+    LibraryFolder, etc.). I've tried to separate/extract classes in order to
+    handle those three situations, but it's proven difficult to do in a simple,
+    readable manner.
+- Ideally, histories and libraries would inherit from the same server models as
+    dataset collections do since they are (in essence) dataset collections themselves -
+    making the whole nested structure simpler. This would be a large, error-prone
+    refactoring and migration.
+
+Many of the classes and heirarchy are meant as extension points so, while the
+relations and flow may be difficult to understand initially, they'll allow us to
+handle the growth or flux dataset collection in the future (w/o actually implementing
+any YAGNI).
+
+*/
+//_________________________________________________________________________________________________ ELEMENTS
+/** @class mixin for Dataset collection elements.
+ *      When collection elements are passed from the API, the underlying element is
+ *          in a sub-object 'object' (IOW, a DCE representing an HDA will have HDA json in element.object).
+ *      This mixin uses the constructor and parse methods to merge that JSON with the DCE attribtues
+ *          effectively changing a DCE from a container to a subclass.
  */
-var DatasetCollectionElement = Backbone.Model.extend( BASE_MVC.LoggableMixin ).extend(
-/** @lends DatasetCollectionElement.prototype */{
+var DatasetCollectionElementMixin = {
 
-    //TODO:?? this model may be unneccessary - it reflects the api structure, but...
-    //  if we munge the element with the element.object at parse, we can flatten the entire hierarchy
-
-    /** logger used to record this.log messages, commonly set to console */
-    // comment this out to suppress log output
-    //logger              : console,
-
+    /** default attributes used by elements in a dataset collection */
     defaults : {
         model_class         : 'DatasetCollectionElement',
         element_identifier  : null,
         element_type        : null
     },
 
-    /** Set up.
-     *  @see Backbone.Collection#initialize
-     */
-    initialize : function( model, options ){
-        this.info( this + '.initialize:', model, options );
-        options = options || {};
-        //this._setUpListeners();
-
-        this.object = this._createObjectModel();
-        this.on( 'change:object', function(){
-            //this.log( 'change:object' );
-//TODO: prob. better to update the sub-model instead of re-creating it
-            this.object = this._createObjectModel();
-        });
+    /** merge the attributes of the sub-object 'object' into this model */
+    _mergeObject : function( attributes ){
+        _.extend( attributes, attributes.object );
+        delete attributes.object;
+        return attributes;
     },
 
-    _createObjectModel : function(){
-        //this.log( '_createObjectModel', this.get( 'object' ), this.object );
-        //TODO: same patterns as HDCA _createElementsModel - refactor to BASE_MVC.hasSubModel?
-        if( _.isUndefined( this.object ) ){ this.object = null; }
-        if( !this.get( 'object' ) ){ return this.object; }
-
-        var object = this.get( 'object' ),
-            ObjectClass = this._getObjectClass();
-        this.unset( 'object', { silent: true });
-        this.object = new ObjectClass( object );
-
-        return this.object;
+    /** override to merge this.object into this */
+    constructor : function( attributes, options ){
+        this.debug( '\t DatasetCollectionElement.constructor:', attributes, options );
+        attributes = this._mergeObject( attributes );
+        Backbone.Model.apply( this, arguments );
     },
 
-    _getObjectClass : function(){
-        this.debug( 'DCE, element_type:', this.get( 'element_type' ) );
-        switch( this.get( 'element_type' ) ){
-            case 'dataset_collection':
-                return DatasetCollection;
-            case 'hda':
-                return HDA_MODEL.HistoryDatasetAssociation;
-        }
-        throw new TypeError( 'Unknown element_type: ' + this.get( 'element_type' ) );
-    },
+    /** when the model is fetched, merge this.object into this */
+    parse : function( response, options ){
+        var attributes = response;
+        attributes = this._mergeObject( attributes );
+        return attributes;
+    }
+};
 
-    toJSON : function(){
-        var json = Backbone.Model.prototype.toJSON.call( this );
-        if( this.object ){
-            json.object = this.object.toJSON();
-        }
-        return json;
-    },
+//TODO: unused?
+/** Concrete class of Generic DatasetCollectionElement */
+var DatasetCollectionElement = Backbone.Model
+    .extend( BASE_MVC.LoggableMixin )
+    .extend( DatasetCollectionElementMixin );
 
-    hasDetails : function(){
-        return ( this.object !== null
-            &&   this.object.hasDetails() );
-    },
-
-    /** String representation. */
-    toString : function(){
-        var objStr = ( this.object )?( '' + this.object ):( this.get( 'element_identifier' ) );
-        return ([ 'DatasetCollectionElement(', objStr, ')' ].join( '' ));
-    }
-});
-
-
+    
 //==============================================================================
-/** @class Backbone model for
- *  @borrows LoggableMixin#logger as #logger
- *  @borrows LoggableMixin#log as #log
- *  @constructs
- */
-var HDADCE = DatasetCollectionElement.extend(
-/** @lends DatasetCollectionElement.prototype */{
-
-    _getObjectClass : function(){
-        return HDA_MODEL.HistoryDatasetAssociation;
-    },
-
-    /** String representation. */
-    toString : function(){
-        var objStr = ( this.object )?( '' + this.object ):( this.get( 'element_identifier' ) );
-        return ([ 'HDADCE(', objStr, ')' ].join( '' ));
-    }
-});
-
-
-//==============================================================================
-/** @class Backbone model for
- *  @borrows LoggableMixin#logger as #logger
- *  @borrows LoggableMixin#log as #log
- *  @constructs
- */
-var DCDCE = DatasetCollectionElement.extend(
-/** @lends DatasetCollectionElement.prototype */{
-
-    _getObjectClass : function(){
-        return DatasetCollection;
-    },
-
-    getVisibleContents : function(){
-        return this.object? this.object.getVisibleContents(): [];
-    },
-
-    /** String representation. */
-    toString : function(){
-        var objStr = ( this.object )?( '' + this.object ):( this.get( 'element_identifier' ) );
-        return ([ 'DCDCE(', objStr, ')' ].join( '' ));
-    }
-});
-
-
-//==============================================================================
-/** @class Backbone collection for DCEs.
- *      NOTE: used *only* in second level of list:paired collections (a
- *      collection that contains collections)
- *
- *  @borrows LoggableMixin#logger as #logger
- *  @borrows LoggableMixin#log as #log
- *  @constructs
- */
+/** @class Base/Abstract Backbone collection for Generic DCEs. */
 var DCECollection = Backbone.Collection.extend( BASE_MVC.LoggableMixin ).extend(
-/** @lends DatasetCollectionElementCollection.prototype */{
+/** @lends DCECollection.prototype */{
     model: DatasetCollectionElement,
 
     // comment this out to suppress log output
     /** logger used to record this.log messages, commonly set to console */
     //logger              : console,
 
+//TODO: unused?
     /** Set up.
      *  @see Backbone.Collection#initialize
      */
-    initialize : function( models, options ){
+    initialize : function( attributes, options ){
+        this.debug( this + '(DCECollection).initialize:', attributes, options );
         options = options || {};
-        this.info( this + '.initialize:', models, options );
         //this._setUpListeners();
     },
 
 
 
 //==============================================================================
-/** @class Backbone collection for
- *  @borrows LoggableMixin#logger as #logger
- *  @borrows LoggableMixin#log as #log
- *  @constructs
+/** @class Backbone model for a dataset collection element that is a dataset (HDA).
  */
-var HDADCECollection = DCECollection.extend(
-/** @lends DatasetCollectionElementCollection.prototype */{
-    model: HDADCE,
+var DatasetDCE = DATASET.DatasetAssociation.extend( BASE_MVC.mixin( DatasetCollectionElementMixin,
+/** @lends DatasetDCE.prototype */{
+
+    /** logger used to record this.log messages, commonly set to console */
+    //logger              : console,
+
+    defaults : _.extend( {}, DATASET.DatasetAssociation.prototype.defaults, DatasetCollectionElementMixin.defaults ),
+
+    // because all objects have constructors (as this hashmap would even if this next line wasn't present)
+    //  the constructor in hcontentMixin won't be attached by BASE_MVC.mixin to this model
+    //  - re-apply manually it now
+    /** call the mixin constructor */
+    constructor : function( attributes, options ){
+        this.debug( '\t DatasetDCE.constructor:', attributes, options );
+        //DATASET.DatasetAssociation.prototype.constructor.call( this, attributes, options );
+        DatasetCollectionElementMixin.constructor.call( this, attributes, options );
+    },
+
+//TODO: unused?
+    /** set up */
+    initialize : function( attributes, options ){
+        this.debug( this + '(DatasetDCE).initialize:', attributes, options );
+        DATASET.DatasetAssociation.prototype.initialize.call( this, attributes, options );
+    },
 
     /** String representation. */
     toString : function(){
-         return ([ 'HDADCECollection(', this.length, ')' ].join( '' ));
+        var objStr = this.get( 'element_identifier' );
+        return ([ 'DatasetDCE(', objStr, ')' ].join( '' ));
+    }
+}));
+
+
+//==============================================================================
+/** @class DCECollection of DatasetDCE's (a list of datasets, a pair of datasets).
+ */
+var DatasetDCECollection = DCECollection.extend(
+/** @lends DatasetDCECollection.prototype */{
+    model: DatasetDCE,
+
+    /** logger used to record this.log messages, commonly set to console */
+    //logger              : console,
+
+//TODO: unused?
+    /**  */
+    initialize : function( attributes, options ){
+        this.debug( this + '(DatasetDCECollection).initialize:', attributes, options );
+        DCECollection.prototype.initialize.call( this, attributes, options );
+    },
+
+    /** String representation. */
+    toString : function(){
+         return ([ 'DatasetDCECollection(', this.length, ')' ].join( '' ));
     }
 });
 
 
-//==============================================================================
-/** @class Backbone collection for
- *  @borrows LoggableMixin#logger as #logger
- *  @borrows LoggableMixin#log as #log
- *  @constructs
+//_________________________________________________________________________________________________ COLLECTIONS
+/** @class Backbone model for Dataset Collections.
+ *      The DC API returns an array of JSON objects under the attribute elements.
+ *      This model:
+ *          - removes that array/attribute ('elements') from the model,
+ *          - creates a bbone collection (of the class defined in the 'collectionClass' attribute),
+ *          - passes that json onto the bbone collection
+ *          - caches the bbone collection in this.elements
  */
-var DCDCECollection = DCECollection.extend(
-/** @lends DatasetCollectionElementCollection.prototype */{
-    model: DCDCE,
+var DatasetCollection = Backbone.Model
+        .extend( BASE_MVC.LoggableMixin )
+        .extend( BASE_MVC.SearchableModelMixin )
+.extend(/** @lends DatasetCollection.prototype */{
 
-    /** String representation. */
-    toString : function(){
-         return ([ 'DCDCECollection(', this.length, ')' ].join( '' ));
-    }
-});
-
-
-//==============================================================================
-/** @class Backbone model for Dataset Collections.
- *      DCs contain a bbone collection named 'elements' using the class found in
- *      this.collectionClass (gen. DatasetCollectionElementCollection). DCs move
- *      that 'object' from the JSON in the attributes list to a full, instantiated
- *      collection found in this.elements. This is done on intialization and
- *      everytime the 'change:elements' event is fired.
- *
- *  @borrows LoggableMixin#logger as #logger
- *  @borrows LoggableMixin#log as #log
- *  @constructs
- */
-var DatasetCollection = Backbone.Model.extend( BASE_MVC.LoggableMixin ).extend(
-/** @lends ListDatasetCollection.prototype */{
-
-    //logger : console,
+    /** logger used to record this.log messages, commonly set to console */
+    //logger              : console,
 
     /** default attributes for a model */
     defaults : {
-        collection_type     : 'list'
+        /* 'list', 'paired', or 'list:paired' */
+        collection_type     : null,
+        //??
+        deleted             : false
     },
 
+    /** Which class to use for elements */
     collectionClass : DCECollection,
 
     /**  */
     initialize : function( model, options ){
-        this.info( 'DatasetCollection.initialize:', model, options );
+        this.debug( this + '(DatasetCollection).initialize:', model, options, this );
         //historyContent.HistoryContent.prototype.initialize.call( this, attrs, options );
         this.elements = this._createElementsModel();
-//TODO:?? no way to use parse here?
         this.on( 'change:elements', function(){
             this.log( 'change:elements' );
 //TODO: prob. better to update the collection instead of re-creating it
 
     /** move elements model attribute to full collection */
     _createElementsModel : function(){
-        this.log( '_createElementsModel', this.get( 'elements' ), this.elements );
+        this.debug( this + '._createElementsModel', this.collectionClass, this.get( 'elements' ), this.elements );
 //TODO: same patterns as DatasetCollectionElement _createObjectModel - refactor to BASE_MVC.hasSubModel?
         var elements = this.get( 'elements' ) || [];
-        this.info( 'elements:', elements );
         this.unset( 'elements', { silent: true });
         this.elements = new this.collectionClass( elements );
+        //this.debug( 'collectionClass:', this.collectionClass + '', this.elements );
         return this.elements;
     },
 
+    // ........................................................................ common queries
+    /** pass the elements back within the model json when this is serialized */
     toJSON : function(){
         var json = Backbone.Model.prototype.toJSON.call( this );
         if( this.elements ){
         return json;
     },
 
+    /** is the collection done with updates and ready to be used? (finished running, etc.) */
+    inReadyState : function(){
+//TODO: state currenly unimplemented for collections
+        return true;
+    },
+
+    //TODO:?? the following are the same interface as DatasetAssociation - can we combine?
+    /** Does the DC contain any elements yet? Is a fetch() required? */
     hasDetails : function(){
 //TODO: this is incorrect for (accidentally) empty collections
         this.debug( 'hasDetails:', this.elements.length );
         return this.elements.length !== 0;
     },
 
+    /** Given the filters, what models in this.elements would be returned? */
     getVisibleContents : function( filters ){
-        //TODO: filters unused for now
+        // filters unused for now
         return this.elements;
     },
 
+    // ........................................................................ ajax
+    /** save this dataset, _Mark_ing it as deleted (just a flag) */
+    'delete' : function( options ){
+        if( this.get( 'deleted' ) ){ return jQuery.when(); }
+        return this.save( { deleted: true }, options );
+    },
+    /** save this dataset, _Mark_ing it as undeleted */
+    undelete : function( options ){
+        if( !this.get( 'deleted' ) || this.get( 'purged' ) ){ return jQuery.when(); }
+        return this.save( { deleted: false }, options );
+    },
+
+    // ........................................................................ searchable
+    searchAttributes : [
+        'name'
+    ],
+
     // ........................................................................ misc
     /** String representation */
     toString : function(){
 
 
 //==============================================================================
+/** Model for a DatasetCollection containing datasets (non-nested).
+ */
 var ListDatasetCollection = DatasetCollection.extend(
 /** @lends ListDatasetCollection.prototype */{
 
-    collectionClass : HDADCECollection,
+    /** logger used to record this.log messages, commonly set to console */
+    //logger              : console,
+
+    collectionClass : DatasetDCECollection,
+
+//TODO: unused?
+    initialize : function( attrs, options ){
+        this.debug( this + '(ListDatasetCollection).initialize:', attrs, options );
+        DatasetCollection.prototype.initialize.call( this, attrs, options );
+    },
 
     /** String representation. */
     toString : function(){
 
 
 //==============================================================================
+/** Model for a DatasetCollection containing fwd/rev datasets (a list of 2).
+ */
 var PairDatasetCollection = ListDatasetCollection.extend(
-/** @lends ListDatasetCollection.prototype */{
+/** @lends PairDatasetCollection.prototype */{
+
+    /** logger used to record this.log messages, commonly set to console */
+    //logger              : console,
+
+//TODO: unused?
+    /**  */
+    initialize : function( attrs, options ){
+        this.debug( this + '(PairDatasetCollection).initialize:', attrs, options );
+        ListDatasetCollection.prototype.initialize.call( this, attrs, options );
+    },
 
     /** String representation. */
     toString : function(){
 });
 
 
+//_________________________________________________________________________________________________ NESTED COLLECTIONS
+// this is where things get weird, man. Weird.
+//TODO: it might be possible to compact all the following...I think.
 //==============================================================================
+/** @class Backbone model for a Generic DatasetCollectionElement that is also a DatasetCollection
+ *      (a nested collection). Currently only list:paired.
+ */
+var NestedDCDCE = DatasetCollection.extend( BASE_MVC.mixin( DatasetCollectionElementMixin,
+/** @lends NestedDCDCE.prototype */{
+
+    /** logger used to record this.log messages, commonly set to console */
+    //logger              : console,
+
+    // because all objects have constructors (as this hashmap would even if this next line wasn't present)
+    //  the constructor in hcontentMixin won't be attached by BASE_MVC.mixin to this model
+    //  - re-apply manually it now
+    /** call the mixin constructor */
+    constructor : function( attributes, options ){
+        this.debug( '\t NestedDCDCE.constructor:', attributes, options );
+        DatasetCollectionElementMixin.constructor.call( this, attributes, options );
+    },
+    
+    /** String representation. */
+    toString : function(){
+        var objStr = ( this.object )?( '' + this.object ):( this.get( 'element_identifier' ) );
+        return ([ 'NestedDCDCE(', objStr, ')' ].join( '' ));
+    }
+}));
+
+
+//==============================================================================
+/** @class Backbone collection containing Generic NestedDCDCE's (nested dataset collections).
+ */
+var NestedDCDCECollection = DCECollection.extend(
+/** @lends NestedDCDCECollection.prototype */{
+
+    /** logger used to record this.log messages, commonly set to console */
+    //logger              : console,
+    
+    model: NestedDCDCE,
+
+//TODO: unused?
+    /**  */
+    initialize : function( attrs, options ){
+        this.debug( this + '(NestedDCDCECollection).initialize:', attrs, options );
+        DCECollection.prototype.initialize.call( this, attrs, options );
+    },
+
+    /** String representation. */
+    toString : function(){
+         return ([ 'NestedDCDCECollection(', this.length, ')' ].join( '' ));
+    }
+});
+
+
+//==============================================================================
+/** @class Backbone model for a paired dataset collection within a list:paired dataset collection.
+ */
+var NestedPairDCDCE = PairDatasetCollection.extend( BASE_MVC.mixin( DatasetCollectionElementMixin,
+/** @lends NestedPairDCDCE.prototype */{
+//TODO:?? possibly rename to NestedDatasetCollection?
+
+    /** logger used to record this.log messages, commonly set to console */
+    //logger              : console,
+
+    /**  */
+    constructor : function( attributes, options ){
+        this.debug( '\t NestedPairDCDCE.constructor:', attributes, options );
+        //DatasetCollection.constructor.call( this, attributes, options );
+        DatasetCollectionElementMixin.constructor.call( this, attributes, options );
+    },
+
+    /** String representation. */
+    toString : function(){
+        var objStr = ( this.object )?( '' + this.object ):( this.get( 'element_identifier' ) );
+        return ([ 'NestedPairDCDCE(', objStr, ')' ].join( '' ));
+    }
+}));
+
+
+//==============================================================================
+/** @class Backbone collection for a backbone collection containing paired dataset collections.
+ */
+var NestedPairDCDCECollection = NestedDCDCECollection.extend(
+/** @lends PairDCDCECollection.prototype */{
+
+    /** logger used to record this.log messages, commonly set to console */
+    //logger              : console,
+
+    model: NestedPairDCDCE,
+
+//TODO: unused?
+    /**  */
+    initialize : function( attrs, options ){
+        this.debug( this + '(NestedPairDCDCECollection).initialize:', attrs, options );
+        NestedDCDCECollection.prototype.initialize.call( this, attrs, options );
+    },
+
+    /** String representation. */
+    toString : function(){
+         return ([ 'NestedPairDCDCECollection(', this.length, ')' ].join( '' ));
+    }
+});
+
+
+//==============================================================================
+/** @class Backbone Model for a DatasetCollection (list) that contains DatasetCollections (pairs).
+ */
 var ListPairedDatasetCollection = DatasetCollection.extend(
-/** @lends ListDatasetCollection.prototype */{
+/** @lends ListPairedDatasetCollection.prototype */{
 
-    collectionClass : DCDCECollection,
+    /** logger used to record this.log messages, commonly set to console */
+    //logger              : console,
 
     // list:paired is the only collection that itself contains collections
-    //collectionClass : DatasetCollectionCollection,
+    collectionClass : NestedPairDCDCECollection,
+
+//TODO: unused?
+    /**  */
+    initialize : function( attributes, options ){
+        this.debug( this + '(ListPairedDatasetCollection).initialize:', attributes, options );
+        DatasetCollection.prototype.initialize.call( this, attributes, options );
+    },
 
     /** String representation. */
     toString : function(){
 
 //==============================================================================
     return {
-        //DatasetCollection                   : DatasetCollection,
         ListDatasetCollection               : ListDatasetCollection,
         PairDatasetCollection               : PairDatasetCollection,
         ListPairedDatasetCollection         : ListPairedDatasetCollection

File static/scripts/mvc/collection/collection-panel.js

     //MODEL is either a DatasetCollection (or subclass) or a DatasetCollectionElement (list of pairs)
 
     /** logger used to record this.log messages, commonly set to console */
-    // comment this out to suppress log output
     //logger              : console,
 
     tagName             : 'div',
     /** (in ms) that jquery effects will use */
     fxSpeed             : 'fast',
 
-    DCEViewClass        : DC_BASE.DCEBaseView,
+    DatasetDCEViewClass : DC_BASE.DatasetDCEBaseView,
+    NestedDCEViewClass  : DC_BASE.NestedDCEBaseView,
 
     // ......................................................................... SET UP
     /** Set up the view, set up storage, bind listeners to HistoryContents events
         this.hasUser = attributes.hasUser;
         this.panelStack = [];
         this.parentName = attributes.parentName;
-
-        window.collectionPanel = this;
     },
 
     /** create any event listeners for the panel
         //this.debug( 'content json:', JSON.stringify( content, null, '  ' ) );
         var contentView = null,
             ContentClass = this._getContentClass( content );
-        //this.debug( 'content.object json:', JSON.stringify( content.object, null, '  ' ) );
         this.debug( 'ContentClass:', ContentClass );
-        //this.debug( 'content:', content );
-        this.debug( 'content.object:', content.object );
+        this.debug( 'content:', content );
         contentView = new ContentClass({
             model           : content,
             linkTarget      : this.linkTarget,
     /**  */
     _getContentClass : function( content ){
         this.debug( this + '._getContentClass:', content );
+        this.debug( 'DCEViewClass:', this.DCEViewClass );
         switch( content.get( 'element_type' ) ){
             case 'hda':
                 return this.DCEViewClass;
 // =============================================================================
 /** @class non-editable, read-only View/Controller for a dataset collection. */
 var ListCollectionPanel = CollectionPanel.extend({
-    DCEViewClass        : DC_BASE.HDADCEBaseView,
+
+    DCEViewClass        : DC_BASE.DatasetDCEBaseView,
+
     // ........................................................................ misc
     /** string rep */
     toString    : function(){
 // =============================================================================
 /** @class non-editable, read-only View/Controller for a dataset collection. */
 var PairCollectionPanel = ListCollectionPanel.extend({
+
     // ........................................................................ misc
     /** string rep */
     toString    : function(){
 // =============================================================================
 /** @class non-editable, read-only View/Controller for a dataset collection. */
 var ListOfPairsCollectionPanel = CollectionPanel.extend({
-    DCEViewClass        : DC_BASE.DCDCEBaseView,
+
+    DCEViewClass        : DC_BASE.NestedDCDCEBaseView,
+
     // ........................................................................ misc
     /** string rep */
     toString    : function(){

File static/scripts/mvc/collection/dataset-collection-base.js

 define([
+    "mvc/dataset/dataset-list-element",
     "mvc/base-mvc",
     "utils/localization"
-], function( BASE_MVC, _l ){
+], function( DATASET_LI, BASE_MVC, _l ){
 /* global Backbone, LoggableMixin */
 //==============================================================================
+var ListItemView = BASE_MVC.ListItemView;
 /** @class Read only view for DatasetCollection.
- *  @name DCBaseView
- *
- *  @augments Backbone.View
- *  @borrows LoggableMixin#logger as #logger
- *  @borrows LoggableMixin#log as #log
- *  @constructs
  */
-var DCBaseView = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({
+var DCBaseView = ListItemView.extend({
+//TODO: may not be needed
 
     /** logger used to record this.log messages, commonly set to console */
-    // comment this out to suppress log output
     //logger              : console,
 
-    /**  */
-    className   : "dataset-collection",
+    className   : ListItemView.prototype.className + " dataset-collection",
     /**  */
     fxSpeed     : 'fast',
 
-    /**  */
+//TODO: ununsed
+    /** set up */
     initialize  : function( attributes ){
         if( attributes.logger ){ this.logger = this.model.logger = attributes.logger; }
         this.log( 'DCBaseView.initialize:', attributes );
+        ListItemView.prototype.initialize.call( this, attributes );
     },
 
-    // ........................................................................ render main
-    /** Render this content, set up ui.
-     *  @param {Boolean} fade   whether or not to fade out/in when re-rendering
-     *  @fires rendered when rendered
-     *  @fires rendered:ready when first rendered and NO running HDAs
-     *  @returns {Object} this HDABaseView
+    /** In this override, don't show`or render any details (no need to do anything here)
+     *      - currently the parent control will load a panel for this collection over itself
+     *  @fires expanded when a body has been expanded
      */
-    render : function( fade ){
-        var $newRender = this._buildNewRender();
-        this._queueNewRender( $newRender, fade );
-        return this;
+    expand : function(){
+        var view = this;
+        return view._fetchModelDetails()
+            .always(function(){
+                view.trigger( 'expanded', view );
+            });
     },
 
-    _buildNewRender : function(){
-        // create a new render using a skeleton template, render title buttons, render body, and set up events, etc.
-        var $newRender = $( this.templates.skeleton( this.model.toJSON() ) );
-        $newRender.find( '.primary-actions' ).append( this._render_primaryActions() );
-        this._setUpBehaviors( $newRender );
-        //this._renderSelectable( $newRender );
-        return $newRender;
-    },
-
-    /** Fade out the old el, replace with new dom, then fade in.
-     *  @param {Boolean} fade   whether or not to fade out/in when re-rendering
-     *  @fires rendered when rendered
-     *  @fires rendered:ready when first rendered and NO running HDAs
-     */
-    _queueNewRender : function( $newRender, fade ) {
-        fade = ( fade === undefined )?( true ):( fade );
-        var view = this;
-
-        // fade the old render out (if desired)
-        if( fade ){
-            $( view ).queue( function( next ){ this.$el.fadeOut( view.fxSpeed, next ); });
+    // ......................................................................... rendering
+    //TODO:?? possibly move to listItem
+    /** render a subtitle to show the user what sort of collection this is */
+    _renderSubtitle : function(){
+        var $subtitle = $( '<div class="subtitle"></div>' );
+        //TODO: would be good to get this in the subtitle
+        //var len = this.model.elements.length;
+        switch( this.model.get( 'collection_type' ) ){
+            case 'list':
+                return $subtitle.text( _l( 'a list of datasets' ) );
+            case 'paired':
+                return $subtitle.text( _l( 'a pair of datasets' ) );
+            case 'list:paired':
+                return $subtitle.text( _l( 'a list of paired datasets' ) );
         }
-        // empty the old render, swap in the new render contents
-        $( view ).queue( function( next ){
-//TODO:?? change to replaceWith pattern?
-            this.$el.empty().attr( 'class', view.className ).append( $newRender.children() );
-            next();
-        });
-        // fade the new in
-        if( fade ){
-            $( view ).queue( function( next ){ this.$el.fadeIn( view.fxSpeed, next ); });
-        }
-        // trigger an event to know we're ready
-        $( view ).queue( function( next ){
-            this.trigger( 'rendered', view );
-            next();
-        });
-    },
-
-    /** set up js behaviors, event handlers for elements within the given container
-     *  @param {jQuery} $container jq object that contains the elements to process (defaults to this.$el)
-     */
-    _setUpBehaviors : function( $container ){
-        $container = $container || this.$el;
-        make_popup_menus( $container );
-        $container.find( '[title]' ).tooltip({ placement : 'bottom' });
-    },
-
-    // ........................................................................ titlebar buttons
-    /** Render icon-button group for the common, most easily accessed actions.
-     *  @returns {jQuery} rendered DOM or null
-     */
-    _render_primaryActions : function(){
-        // override
-        return [];
-    },
-
-    // ......................................................................... misc
-    events : {
-        'click .title-bar' : function( event ){
-            this.trigger( 'expanded', this );
-        }
+        return $subtitle;
     },
 
     // ......................................................................... misc
     }
 });
 
-/** templates for DCBaseViews (skeleton and body) */
-DCBaseView.templates = DCBaseView.prototype.templates = (function(){
-// use closure to run underscore template fn only once at module load
-    var skeletonTemplate = _.template([
-        '<div class="dataset-collection">',
-            '<div class="primary-actions"></div>',
-            '<div class="title-bar clear" tabindex="0">',
-                '<div class="title">',
-                    '<span class="name"><%- collection.element_identifier %></span>',
-                '</div>',
+// ............................................................................ TEMPLATES
+/** underscore templates */
+DCBaseView.prototype.templates = (function(){
+
+    // use element identifier
+    var titleBarTemplate = BASE_MVC.wrapTemplate([
+        '<div class="title-bar clear" tabindex="0">',
+            '<div class="title">',
+                '<span class="name"><%- collection.element_identifier || collection.name %></span>',
             '</div>',
-            '<div class="details"></div>',
+            '<div class="subtitle"></div>',
         '</div>'
-    ].join( '' ));
+    ], 'collection' );
 
-    var bodyTemplate = _.template([
-        '<div class="details">',
-            '<div class="summary">', _l( 'A dataset collection.' ), '</div>',
-        '</div>'
-    ].join( '' ));
-
-    // we override here in order to pass the localizer (_L) into the template scope - since we use it as a fn within
-    return {
-        skeleton : function( collectionJSON ){
-            return skeletonTemplate({ _l: _l, collection: collectionJSON });
-        },
-        body : function( collectionJSON ){
-            return bodyTemplate({ _l: _l, collection: collectionJSON });
-        }
-    };
+    return _.extend( {}, ListItemView.prototype.templates, {
+        titleBar : titleBarTemplate
+    });
 }());
 
 
 //==============================================================================
 /** @class Read only view for DatasetCollectionElement.
- *  @name DCBaseView
- *
- *  @augments Backbone.View
- *  @borrows LoggableMixin#logger as #logger
- *  @borrows LoggableMixin#log as #log
- *  @constructs
  */
-var DCEBaseView = BASE_MVC.ExpandableView.extend({
+var DCEBaseView = ListItemView.extend({
+//TODO: this might be expendable - compacted with HDADCEBaseView
 
     /** logger used to record this.log messages, commonly set to console */
     // comment this out to suppress log output
-    //logger              : console,
+    logger              : console,
 
-    /**  */
-    className   : "dataset-collection-element collection-dataset dataset",
-    /**  */
+    /** add the DCE class to the list item */
+    className   : ListItemView.prototype.className + " dataset-collection-element",
+    /** jq fx speed for this view */
     fxSpeed     : 'fast',
 
     /**  */
     initialize  : function( attributes ){
         if( attributes.logger ){ this.logger = this.model.logger = attributes.logger; }
         this.log( 'DCEBaseView.initialize:', attributes );
-        BASE_MVC.ExpandableView.prototype.initialize.call( this, attributes );
-    },
-
-    // ......................................................................... renderers
-    /** Render the enclosing div of the hda body and, if expanded, the html in the body
-     *  @returns {jQuery} rendered DOM
-     */
-    _renderDetails : function(){
-        var $details = $( this.templates.details( this.model.toJSON() ) );
-        this._setUpBehaviors( $details );
-        // only render the body html if it's being shown
-        if( this.expanded ){
-            $details.show();
-        }
-        return $details;
-    },
-
-    // ......................................................................... events
-    events : {
-        // expand the body when the title is clicked or when in focus and space or enter is pressed
-        'click .title-bar'      : '_clickTitleBar',
-        'keydown .title-bar'    : '_keyDownTitleBar'
-    },
-
-    _clickTitleBar : function( event ){
-        event.stopPropagation();
-        this.toggleExpanded();
-    },
-
-    _keyDownTitleBar : function( event ){
-        // bail (with propagation) if keydown and not space or enter
-        var KEYCODE_SPACE = 32, KEYCODE_RETURN = 13;
-        if( event && ( event.type === 'keydown' )
-        &&( event.keyCode === KEYCODE_SPACE || event.keyCode === KEYCODE_RETURN ) ){
-            this.toggleExpanded();
-            event.stopPropagation();
-            return false;
-        }
-        return true;
+        ListItemView.prototype.initialize.call( this, attributes );
     },
 
     // ......................................................................... misc
     }
 });
 
-/** templates for DCBaseViews (skeleton and body) */
-DCEBaseView.templates = DCEBaseView.prototype.templates = (function(){
-// use closure to run underscore template fn only once at module load
-    var skeletonTemplate = _.template([
-        '<div class="dataset-collection-element collection-dataset dataset">',
-            '<div class="primary-actions"></div>',
-            '<div class="title-bar clear" tabindex="0">',
-                '<span class="state-icon"></span>',
-                '<div class="title">',
-                    '<span class="name"><%- collection.element_identifier %></span>',
-                '</div>',
+// ............................................................................ TEMPLATES
+/** underscore templates */
+DCEBaseView.prototype.templates = (function(){
+
+    // use the element identifier here - since that will persist and the user will need it
+    var titleBarTemplate = BASE_MVC.wrapTemplate([
+        '<div class="title-bar clear" tabindex="0">',
+            '<div class="title">',
+                '<span class="name"><%- element.element_identifier %></span>',
             '</div>',
-            '<div class="details"></div>',
+            '<div class="subtitle"></div>',
         '</div>'
-    ].join( '' ));
+    ], 'element' );
 
-    var bodyTemplate = _.template([
-        '<div class="details">',
-            '<div class="summary">',
-                _l( 'A dataset collection element.' ),
-            '</div>',
-        '</div>'
-    ].join( '' ));
-
-    // we override here in order to pass the localizer (_L) into the template scope - since we use it as a fn within
-    return {
-        skeleton : function( collectionJSON ){
-            return skeletonTemplate({ _l: _l, collection: collectionJSON });
-        },
-        body : function( collectionJSON ){
-            return bodyTemplate({ _l: _l, collection: collectionJSON });
-        }
-    };
+    return _.extend( {}, ListItemView.prototype.templates, {
+        titleBar : titleBarTemplate
+    });
 }());
 
 
 //==============================================================================
-/** @class Read only view for DatasetCollectionElement.
- *  @name DCBaseView
- *
- *  @augments Backbone.View
- *  @borrows LoggableMixin#logger as #logger
- *  @borrows LoggableMixin#log as #log
- *  @constructs
+/** @class Read only view for a DatasetCollectionElement that is also an HDA.
  */
-var HDADCEBaseView = DCEBaseView.extend({
+var DatasetDCEBaseView = DATASET_LI.DatasetListItemView.extend({
+
+    className   : DATASET_LI.DatasetListItemView.prototype.className + " dataset-collection-element",
 
     /** logger used to record this.log messages, commonly set to console */
     // comment this out to suppress log output
     //logger              : console,
 
-    // ......................................................................... misc
-    /** String representation */
-    toString : function(){
-        var modelString = ( this.model )?( this.model + '' ):( '(no model)' );
-        return 'HDADCEBaseView(' + modelString + ')';
-    }
-});
-
-/** templates for DCBaseViews (skeleton and body) */
-HDADCEBaseView.templates = HDADCEBaseView.prototype.templates = (function(){
-// use closure to run underscore template fn only once at module load
-    var skeletonTemplate = _.template([
-        '<div class="dataset-collection-element dataset">',
-            '<div class="primary-actions"></div>',
-            '<div class="title-bar clear" tabindex="0">',
-                '<span class="state-icon"></span>',
-                '<div class="title">',
-//TODO:?? re-check this: in pairs the name and identifier are different - but not otherwise
-                    '<span class="name"><%- element.element_identifier %></span>',
-                '</div>',
-//                '<% if( element.element_identifier !== hda.name ){ %>',
-//                    '<div class="subtitle"><%- element.element_identifier %></div>',
-//                '<% } %>',
-            '</div>',
-            '<div class="details"></div>',
-        '</div>'
-    ].join( '' ));
-
-    var detailsTemplate = _.template([
-        '<div class="details">',
-            '<div class="summary">',
-                '<% if( hda.misc_blurb ){ %>',
-                    '<div class="blurb">',
-                        '<span class="value"><%- hda.misc_blurb %></span>',
-                    '</div>',
-                '<% } %>',
-
-                '<% if( hda.data_type ){ %>',
-                    '<div class="datatype">',
-                        '<label class="prompt">', _l( 'format' ), '</label>',
-                        '<span class="value"><%- hda.data_type %></span>',
-                    '</div>',
-                '<% } %>',
-
-                '<% if( hda.metadata_dbkey ){ %>',
-                    '<div class="dbkey">',
-                        '<label class="prompt">', _l( 'database' ), '</label>',
-                        '<span class="value">',
-                            '<%- hda.metadata_dbkey %>',
-                        '</span>',
-                    '</div>',
-                '<% } %>',
-
-                '<% if( hda.misc_info ){ %>',
-                    '<div class="info">',
-                        '<span class="value"><%- hda.misc_info %></span>',
-                    '</div>',
-                '<% } %>',
-            '</div>',
-            // end dataset-summary
-
-            '<div class="actions clear">',
-                '<div class="left"></div>',
-                '<div class="right"></div>',
-            '</div>',
-
-            '<% if( !hda.deleted ){ %>',
-                '<div class="tags-display"></div>',
-                '<div class="annotation-display"></div>',
-
-                '<div class="display-applications">',
-                    //TODO: the following two should be compacted
-                    '<% _.each( hda.display_apps, function( app ){ %>',