/client/galaxy/scripts/mvc/list/list-item.js
JavaScript | 507 lines | 301 code | 62 blank | 144 comment | 25 complexity | 93a9462c8251d4e2230e7c05a82457b3 MD5 | raw file
Possible License(s): CC-BY-3.0
- define([
- 'mvc/base-mvc',
- 'utils/localization'
- ], function( BASE_MVC, _l ){
- //==============================================================================
- /** 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( BASE_MVC.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.fxSpeed = attributes.fxSpeed !== undefined? attributes.fxSpeed : this.fxSpeed;
- },
- // ........................................................................ render main
- /** jq fx speed */
- fxSpeed : 'fast',
- /** Render this content, set up ui.
- * @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.el( this.model.toJSON(), this ) );
- if( this.expanded ){
- this.$details( $newRender ).replaceWith( this._renderDetails().show() );
- }
- return $newRender;
- },
- /** 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
- */
- _queueNewRender : function( $newRender, speed ) {
- speed = ( speed === undefined )?( this.fxSpeed ):( speed );
- var view = this;
- $( view ).queue( 'fx', [
- function( next ){ this.$el.fadeOut( speed, next ); },
- function( next ){
- view._swapNewRender( $newRender );
- next();
- },
- function( next ){ this.$el.fadeIn( speed, next ); },
- function( next ){
- this.trigger( 'rendered', view );
- next();
- }
- ]);
- },
- /** empty out the current el, move the $newRender's children in */
- _swapNewRender : function( $newRender ){
- return this.$el.empty()
- .attr( 'class', _.isFunction( this.className )? this.className(): 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( $where ){
- $where = $where || this.$el;
- // set up canned behavior on children (bootstrap, popupmenus, editable_text, etc.)
- //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(){
- var $newDetails = $( this.templates.details( this.model.toJSON(), this ) );
- this._setUpBehaviors( $newDetails );
- return $newDetails;
- },
- // ......................................................................... expansion/details
- /** Show or hide the details
- * @param {Boolean} expand if true, expand; if false, collapse
- */
- toggleExpanded : function( expand ){
- expand = ( expand === undefined )?( !this.expanded ):( expand );
- if( expand ){
- this.expand();
- } else {
- this.collapse();
- }
- return this;
- },
- /** Render and show the full, detailed body of this view including extra data and controls.
- * note: if the model does not have detailed data, fetch that data before showing the body
- * @fires expanded when a body has been expanded
- */
- expand : function(){
- var view = this;
- return view._fetchModelDetails().always( function(){
- view._expand();
- });
- },
- /** 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();
- }
- return jQuery.when();
- },
- /** Inner fn called when expand (public) has fetched the details */
- _expand : function(){
- var view = this,
- $newDetails = view._renderDetails();
- view.$details().replaceWith( $newDetails );
- // needs to be set after the above or the slide will not show
- view.expanded = true;
- view.$details().slideDown({
- duration : view.fxSpeed,
- step: function(){
- view.trigger( 'expanding', view );
- },
- complete: function(){
- view.trigger( 'expanded', view );
- }
- });
- },
- /** Hide the body/details of an HDA.
- * @fires collapsed when a body has been collapsed
- */
- collapse : function(){
- this.debug( this + '(ExpandableView).collapse' );
- var view = this;
- view.expanded = false;
- this.$details().slideUp({
- duration : view.fxSpeed,
- step: function(){
- view.trigger( 'collapsing', view );
- },
- complete: function(){
- view.trigger( 'collapsed', view );
- }
- });
- }
- });
- //==============================================================================
- /** 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(
- BASE_MVC.mixin( BASE_MVC.SelectableViewMixin, BASE_MVC.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 );
- BASE_MVC.SelectableViewMixin.initialize.call( this, attributes );
- BASE_MVC.DraggableViewMixin.initialize.call( this, attributes );
- this._setUpListeners();
- },
- /** event listeners */
- _setUpListeners : function(){
- // hide the primary actions in the title bar when selectable and narrow
- this.on( 'selectable', function( isSelectable ){
- if( isSelectable ){
- this.$( '.primary-actions' ).hide();
- } else {
- this.$( '.primary-actions' ).show();
- }
- }, this );
- //this.on( 'all', function( event ){
- // this.log( event );
- //}, this );
- return this;
- },
- // ........................................................................ rendering
- /** In this override, call methods to build warnings, titlebar and primary actions */
- _buildNewRender : function(){
- var $newRender = ExpandableView.prototype._buildNewRender.call( this );
- $newRender.children( '.warnings' ).replaceWith( this._renderWarnings() );
- $newRender.children( '.title-bar' ).replaceWith( this._renderTitleBar() );
- $newRender.children( '.primary-actions' ).append( this._renderPrimaryActions() );
- $newRender.find( '> .title-bar .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();
- if( event.altKey ){
- this.toggleSelect( event );
- if( !this.selectable ){
- this.showSelector();
- }
- } else {
- 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 = BASE_MVC.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 = BASE_MVC.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 = BASE_MVC.wrapTemplate([
- // override this
- '<div class="subtitle"></div>'
- ]);
- var detailsTemplate = BASE_MVC.wrapTemplate([
- // override this
- '<div class="details"></div>'
- ]);
- return {
- el : elTemplato,
- warnings : warnings,
- titleBar : titleBarTemplate,
- subtitle : subtitleTemplate,
- details : detailsTemplate
- };
- }());
- //==============================================================================
- /** A view that is displayed in some larger list/grid/collection.
- * *AND* can display some sub-list of it's own when expanded (e.g. dataset collections).
- * This list will 'foldout' when the item is expanded depending on this.foldoutStyle:
- * If 'foldout': will expand vertically to show the nested list
- * If 'drilldown': will overlay the parent list
- *
- * Inherits from ListItemView.
- *
- * _renderDetails does the work of creating this.details: a sub-view that shows the nested list
- */
- var FoldoutListItemView = ListItemView.extend({
-
- /** If 'foldout': show the sub-panel inside the expanded item
- * If 'drilldown': only fire events and handle by pub-sub
- * (allow the panel containing this item to attach it, hide itself, etc.)
- */
- foldoutStyle : 'foldout',
- /** Panel view class to instantiate for the sub-panel */
- foldoutPanelClass : null,
- /** override to:
- * add attributes foldoutStyle and foldoutPanelClass for config poly
- * disrespect attributes.expanded if drilldown
- */
- initialize : function( attributes ){
- //TODO: hackish
- if( this.foldoutStyle === 'drilldown' ){ this.expanded = false; }
- this.foldoutStyle = attributes.foldoutStyle || this.foldoutStyle;
- this.foldoutPanelClass = attributes.foldoutPanelClass || this.foldoutPanelClass;
- ListItemView.prototype.initialize.call( this, attributes );
- this.foldout = this._createFoldoutPanel();
- },
- //TODO:?? override to exclude foldout scope?
- //$ : function( selector ){
- // var $found = ListItemView.prototype.$.call( this, selector );
- // return $found;
- //},
- /** in this override, attach the foldout panel when rendering details */
- _renderDetails : function(){
- //TODO: hackish
- if( this.foldoutStyle === 'drilldown' ){ return $(); }
- var $newDetails = ListItemView.prototype._renderDetails.call( this );
- return this._attachFoldout( this.foldout, $newDetails );
- },
- /** In this override, handle collection expansion. */
- _createFoldoutPanel : function(){
- var model = this.model;
- var FoldoutClass = this._getFoldoutPanelClass( model ),
- options = this._getFoldoutPanelOptions( model ),
- foldout = new FoldoutClass( _.extend( options, {
- model : model
- }));
- return foldout;
- },
- /** Stub to return proper foldout panel class */
- _getFoldoutPanelClass : function(){
- // override
- return this.foldoutPanelClass;
- },
- /** Stub to return proper foldout panel options */
- _getFoldoutPanelOptions : function(){
- return {
- // propagate foldout style down
- foldoutStyle : this.foldoutStyle,
- fxSpeed : this.fxSpeed
- };
- },
- /** Render the foldout panel inside the view, hiding controls */
- _attachFoldout : function( foldout, $whereTo ){
- $whereTo = $whereTo || this.$( '> .details' );
- this.foldout = foldout.render( 0 );
- //TODO: hack
- foldout.$( '> .controls' ).hide();
- return $whereTo.append( foldout.$el );
- },
- /** In this override, branch on foldoutStyle to show expanded */
- expand : function(){
- var view = this;
- return view._fetchModelDetails()
- .always(function(){
- if( view.foldoutStyle === 'foldout' ){
- view._expand();
- } else if( view.foldoutStyle === 'drilldown' ){
- view._expandByDrilldown();
- }
- });
- },
- /** For drilldown, set up close handler and fire expanded:drilldown
- * containing views can listen to this and handle other things
- * (like hiding themselves) by listening for expanded/collapsed:drilldown
- */
- _expandByDrilldown : function(){
- var view = this;
- // attachment and rendering done by listener
- view.foldout.on( 'close', function(){
- view.trigger( 'collapsed:drilldown', view, view.foldout );
- });
- view.trigger( 'expanded:drilldown', view, view.foldout );
- }
- });
- // ............................................................................ TEMPLATES
- /** underscore templates */
- FoldoutListItemView.prototype.templates = (function(){
- //TODO:?? unnecessary?
- // use element identifier
- var detailsTemplate = BASE_MVC.wrapTemplate([
- '<div class="details">',
- // override with more info (that goes above the panel)
- '</div>'
- ], 'collection' );
- return _.extend( {}, ListItemView.prototype.templates, {
- details : detailsTemplate
- });
- }());
- //==============================================================================
- return {
- ExpandableView : ExpandableView,
- ListItemView : ListItemView,
- FoldoutListItemView : FoldoutListItemView
- };
- });