mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
UI, history: auto-expand sections
- track scrolling and auto expand sections when scroll 'cursor' is over them for more than a certain time - improve rendering speed by using previously existing renders
This commit is contained in:
@@ -145,7 +145,8 @@ var HistoryContents = _super.extend( BASE_MVC.LoggableMixin ).extend({
|
||||
* @returns {HistoryDatasetAssociation} the model with the given hid or undefined if not found
|
||||
*/
|
||||
getByHid : function( hid ){
|
||||
return _.first( this.filter( function( c ){ return c.get( 'hid' ) === hid; }) );
|
||||
// note: there *can* be more than one content with a given hid, this finds the first based on order
|
||||
return this.findWhere({ hid: hid });
|
||||
},
|
||||
|
||||
/** return true if all contents have details */
|
||||
@@ -335,7 +336,7 @@ var HistoryContents = _super.extend( BASE_MVC.LoggableMixin ).extend({
|
||||
// TODO: remove the need to maintain allFetched/lastFetched here
|
||||
// by using fetchFirst/More here
|
||||
function _notifyAndContinue( response, offset ){
|
||||
// console.log( 'rcvd:', response.length );
|
||||
console.log( 'rcvd:', response.length );
|
||||
deferred.notify( response, limit, offset );
|
||||
if( self.allFetched ){
|
||||
deferred.resolve( response, limit, offset );
|
||||
@@ -358,6 +359,7 @@ var HistoryContents = _super.extend( BASE_MVC.LoggableMixin ).extend({
|
||||
// console.log( 'fetching:', _options.limit, _options.offset );
|
||||
_.defer( function(){
|
||||
fetchFn.call( self, _options )
|
||||
.always( function(){ console.log( 'always:', arguments ); })
|
||||
.fail( deferred.reject )
|
||||
.done( function( r ){ _notifyAndContinue( r, offset ); });
|
||||
});
|
||||
@@ -628,6 +630,11 @@ var HidSectionedHistoryContents = HistoryContents.extend({
|
||||
return this._filterSectionCollection( this.currentSection, filterFn );
|
||||
},
|
||||
|
||||
/** */
|
||||
setCurrentSection : function( section ){
|
||||
this.currentSection = section;
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------ sectioned fetching
|
||||
/** @type {Integer} number of contents/hid entries per section/page displayed */
|
||||
hidsPerSection : 100,
|
||||
@@ -698,6 +705,48 @@ silent: true,
|
||||
return self.fetchSection( section, options )
|
||||
.always( function(){ self.trigger( 'fetching-deleted-done', self ); });
|
||||
},
|
||||
|
||||
/** fetch contents' details in batches of limitPerCall - note: only get searchable details here */
|
||||
progressivelyFetchDetails : function( options ){
|
||||
// console.log( 'progressivelyFetchDetails:', options );
|
||||
options = options || {};
|
||||
var deferred = jQuery.Deferred();
|
||||
var self = this;
|
||||
var startingSection = this._countSections();
|
||||
var searchAttributes = HDA_MODEL.HistoryDatasetAssociation.prototype.searchAttributes;
|
||||
var detailKeys = searchAttributes.join( ',' );
|
||||
|
||||
// TODO: remove the need to maintain allFetched/lastFetched here
|
||||
// by using fetchFirst/More here
|
||||
function _notifyAndContinue( response, section ){
|
||||
deferred.notify( response, section );
|
||||
if( section === 0 ){
|
||||
deferred.resolve( response );
|
||||
return;
|
||||
}
|
||||
_recursivelyFetch( section - 1 );
|
||||
}
|
||||
|
||||
function _recursivelyFetch( section ){
|
||||
// console.log( '_recursivelyFetch:', offset );
|
||||
var _options = _.extend( _.clone( options ), {
|
||||
view : 'summary',
|
||||
keys : detailKeys,
|
||||
reset : section === 0,
|
||||
bypassCache : true
|
||||
});
|
||||
_.defer( function(){
|
||||
return self.fetchSection( section, _options )
|
||||
.fail( deferred.reject )
|
||||
.done( function( r ){
|
||||
_notifyAndContinue( r, section );
|
||||
});
|
||||
});
|
||||
}
|
||||
_recursivelyFetch( startingSection );
|
||||
return deferred;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -200,13 +200,16 @@ var HistoryView = _super.extend(
|
||||
// ------------------------------------------------------------------------ client-side pagination
|
||||
/** */
|
||||
renderItems : function( $whereTo ){
|
||||
$( '.tooltip' ).remove();
|
||||
|
||||
// console.log( 'renderItems -----------------------------------------------------------' );
|
||||
$whereTo = $whereTo || this.$el;
|
||||
var self = this;
|
||||
var contents = self.model.contents;
|
||||
var hidsPerSection = contents.hidsPerSection;
|
||||
|
||||
self.freeViews();
|
||||
// self.freeViews();
|
||||
self.views = [];
|
||||
// render sections
|
||||
// console.log( 'renderItems:', self.$list( $whereTo ) );
|
||||
self.$list( $whereTo ).html( contents._mapSectionRanges( function( section ){
|
||||
@@ -223,16 +226,28 @@ var HistoryView = _super.extend(
|
||||
},
|
||||
|
||||
_renderSection : function( section, $whereTo ){
|
||||
// // console.debug( this + '._renderSection', section, $whereTo );
|
||||
// var self = this;
|
||||
// // render views from collection for the current section, replacing that section marker with them
|
||||
// // note: shows only one section's worth of views at a time
|
||||
// var views = [];
|
||||
// var sectionModels = self.model.contents._filterSectionCollection( section, _.bind( this._filterItem, this ) );
|
||||
// self.$section( section, $whereTo ).append( sectionModels.map( function( itemModel ){
|
||||
// var view = self._createItemView( itemModel );
|
||||
// views.push( view );
|
||||
// return self._renderItemView$el( view );
|
||||
// }));
|
||||
// return views;
|
||||
|
||||
// console.debug( this + '._renderSection', section, $whereTo );
|
||||
var self = this;
|
||||
// render views from collection for the current section, replacing that section marker with them
|
||||
// note: shows only one section's worth of views at a time
|
||||
var views = [];
|
||||
var sectionModels = self.model.contents._filterSectionCollection( section, _.bind( this._filterItem, this ) );
|
||||
self.$section( section, $whereTo ).append( sectionModels.map( function( itemModel ){
|
||||
var view = self._createItemView( itemModel );
|
||||
views.push( view );
|
||||
return self._renderItemView$el( view );
|
||||
var views = self._modelsToViews( sectionModels );
|
||||
self.$section( section, $whereTo ).append( views.map( function( view ){
|
||||
// console.log( 'view.$el.children()', view.el.children.length );
|
||||
return view.delegateEvents().el.children.length? view.$el : self._renderItemView$el( view );
|
||||
}));
|
||||
return views;
|
||||
},
|
||||
@@ -317,7 +332,7 @@ var HistoryView = _super.extend(
|
||||
if( isNotLastSection ){
|
||||
// we need to set this now (instead of after the fetch) so that the
|
||||
// if statement above isn't triggered more than once
|
||||
self.model.contents.currentSection = lastSection;
|
||||
self.model.contents.setCurrentSection( lastSection );
|
||||
self.model.contents.fetchSection( lastSection, { silent: true })
|
||||
.done( function(){
|
||||
self.renderItems();
|
||||
@@ -453,14 +468,25 @@ var HistoryView = _super.extend(
|
||||
},
|
||||
|
||||
/** loads a section and re-renders items */
|
||||
openSection : function( section ){
|
||||
console.log( this + '.openSection:', section );
|
||||
openSection : function( section, options ){
|
||||
options = options || {};
|
||||
var self = this;
|
||||
return self.model.contents.fetchSection( section, { silent: true })
|
||||
var contents = self.model.contents;
|
||||
var isLastSection = section === contents._lastSection();
|
||||
return contents.fetchSection( section, { silent: true })
|
||||
.done( function(){
|
||||
self.model.contents.currentSection = section;
|
||||
contents.setCurrentSection( section );
|
||||
self.renderItems();
|
||||
self.scrollTo( self.$section( section ).get(0).offsetTop );
|
||||
|
||||
var sectionElement = self.$section( section ).get(0);
|
||||
var sectionTop = sectionElement.offsetTop;
|
||||
var sectionBottom = sectionElement.offsetTop + sectionElement.offsetHeight;
|
||||
if( options.startAtBottom ){
|
||||
// place bottom of scroll container at bottom of section
|
||||
self.scrollTo( sectionBottom - self.$scrollContainer().height() );
|
||||
} else {
|
||||
self.scrollTo( isLastSection? 0 : sectionElement.offsetTop );
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -581,6 +607,100 @@ var HistoryView = _super.extend(
|
||||
return this.scrollToItem( _.first( this.viewsWhereModel({ hid: hid }) ) );
|
||||
},
|
||||
|
||||
/** @type {Number} ms to debounce scroll handler (some browsers fire *much* more often than others) */
|
||||
SCROLL_HANDLER_DEBOUNCE_MS : 10,
|
||||
|
||||
/** override to track the scroll container for this view */
|
||||
_setUpBehaviors : function( $where ){
|
||||
var self = this,
|
||||
$newRender = _super.prototype._setUpBehaviors.call( this, $where );
|
||||
// this needs to be handled outside the events hash since we're accessing the scollContainer
|
||||
// (rebind and debounce the method so we can cache for any later removal)
|
||||
|
||||
// window.cursor = $( '<div/>' ).attr( 'id', 'cursor' ).css({
|
||||
// position: 'absolute',
|
||||
// height: '1px',
|
||||
// width: '100%',
|
||||
// background: 'red',
|
||||
// }).prependTo( $where );
|
||||
|
||||
self.scrollHandler = _.debounce( _.bind( this.scrollHandler, self ), self.SCROLL_HANDLER_DEBOUNCE_MS );
|
||||
self.$scrollContainer( $where ).on( 'scroll', self.scrollHandler );
|
||||
return self;
|
||||
},
|
||||
|
||||
FOCUSED_SECTION_OPEN_DELAY : 700,
|
||||
|
||||
/** */
|
||||
scrollHandler : function( ev ){
|
||||
var self = this;
|
||||
if( !self.model.contents._countSections() ){ return; }
|
||||
var FOCUS_CLASS = 'fisheye-focus';
|
||||
var prevCursor = self._getScrollCursor();
|
||||
var $section = self._getScrollFocusedSection( prevCursor );
|
||||
var sectionNumber = $section.data( 'section' );
|
||||
// if we're still on the old section: do nothing and bail early
|
||||
if( sectionNumber === self._previouslyFocusedSection ){ return; }
|
||||
|
||||
// if we've moved to a new section, clear the previous opening timer, set the focused class
|
||||
clearTimeout( self._focusedSectionOpenTimeoutId );
|
||||
self.$( '.' + FOCUS_CLASS ).removeClass( FOCUS_CLASS );
|
||||
$section.addClass( FOCUS_CLASS );
|
||||
self._previouslyFocusedSection = sectionNumber;
|
||||
|
||||
// if this section hasn't been opened, delay a bit - then open it
|
||||
if( sectionNumber === self.model.contents.currentSection ){ return; }
|
||||
self._focusedSectionOpenTimeoutId = _.delay( function(){
|
||||
var newCursor = self._getScrollCursor();
|
||||
var currentSection = self._getScrollFocusedSection( newCursor ).data( 'section' );
|
||||
var stillThere = currentSection === self._previouslyFocusedSection;
|
||||
if( stillThere ){
|
||||
// start at the bottom or top of the newly opened section based on the previous cursor position
|
||||
// if moving down or if cursor is inside header: start at top; moving up: start at bottom
|
||||
var topOfList = Galaxy.currHistoryPanel.$list().get(0).offsetTop;
|
||||
var startAtBottom = newCursor > topOfList && newCursor < prevCursor;
|
||||
self.openSection( currentSection, { startAtBottom: startAtBottom });
|
||||
}
|
||||
}, this.FOCUSED_SECTION_OPEN_DELAY );
|
||||
},
|
||||
|
||||
_getScrollFocusedSection : function( cursor ){
|
||||
cursor = cursor || this._getScrollCursor();
|
||||
// console.log( cursor );
|
||||
// this.$( '#cursor' ).css( 'top', cursor );
|
||||
|
||||
var $sections = this.$list().children( '.list-items-section' );
|
||||
var scrollTop = this.$scrollContainer().scrollTop();
|
||||
// return the first section if the cursor is there *or* in the header
|
||||
var topOfFirst = scrollTop + $sections.eq(0).position().top;
|
||||
var bottomOfFirst = topOfFirst + $sections.eq(0).get(0).offsetHeight;
|
||||
if( cursor < bottomOfFirst ){
|
||||
return $sections.eq(0);
|
||||
}
|
||||
// cycle remaining sections checking cursor v. their dimensions
|
||||
for( var i=1; i<$sections.size(); i++ ){
|
||||
// TODO:?? tops are actually static* (*for the most part)
|
||||
var $section = $sections.eq(i);
|
||||
var sectionTop = scrollTop + $section.position().top;
|
||||
var sectionBottom = sectionTop + $section.get(0).offsetHeight;
|
||||
if( cursor > sectionTop && cursor < sectionBottom ){
|
||||
return $section;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getScrollCursor : function(){
|
||||
var UNOPENED_SECTION_HEIGHT = 24;
|
||||
var $scrolled = this.$scrollContainer();
|
||||
var viewportHeight = $scrolled.height();
|
||||
var scrollTop = $scrolled.scrollTop();
|
||||
var percScrolled = scrollTop / ( this.el.scrollHeight - viewportHeight );
|
||||
// this bumps the cursor at the two ends a bit closer to the middle (at the cost of some math)
|
||||
var fromMid = ( percScrolled / 0.5 ) - 1;
|
||||
var adj = ( Math.pow( fromMid, 3 ) * -UNOPENED_SECTION_HEIGHT ) - 1;
|
||||
return scrollTop + ( $scrolled.height() * percScrolled ) + adj;
|
||||
},
|
||||
|
||||
// ........................................................................ misc
|
||||
/** Return a string rep of the history */
|
||||
toString : function(){
|
||||
|
||||
@@ -288,6 +288,7 @@ var ListItemView = ExpandableView.extend(
|
||||
|
||||
/** expand when the title bar is clicked */
|
||||
_clickTitleBar : function( event ){
|
||||
console.log( '_clickTitleBar' );
|
||||
event.stopPropagation();
|
||||
if( event.altKey ){
|
||||
this.toggleSelect( event );
|
||||
|
||||
@@ -105,17 +105,6 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(/** @lends
|
||||
this._setUpListeners();
|
||||
},
|
||||
|
||||
/** free any sub-views the list has */
|
||||
freeViews : function(){
|
||||
var self = this;
|
||||
_.each( self.views, function( view ){
|
||||
self.stopListening( view );
|
||||
view.off();
|
||||
});
|
||||
self.views = [];
|
||||
return self;
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------ listeners
|
||||
/** create any event listeners for the list */
|
||||
_setUpListeners : function(){
|
||||
@@ -414,9 +403,32 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(/** @lends
|
||||
});
|
||||
var view = new ViewClass( options );
|
||||
this._setUpItemViewListeners( view );
|
||||
this.viewMap[ model.id ] = view;
|
||||
return view;
|
||||
},
|
||||
|
||||
/** Free a view for a model. Note: does not remove it from the DOM */
|
||||
_destroyItemView : function( view ){
|
||||
this.stopListening( view );
|
||||
this.views = _.without( this.views, view );
|
||||
delete this.viewMap[ view.model.id ];
|
||||
},
|
||||
|
||||
_destroyItemViews : function( view ){
|
||||
var self = this;
|
||||
self.views.forEach( function( v ){
|
||||
self.stopListening( v );
|
||||
});
|
||||
self.views = [];
|
||||
self.viewMap = {};
|
||||
return self;
|
||||
},
|
||||
|
||||
/** free any sub-views the list has */
|
||||
freeViews : function(){
|
||||
return this._destroyItemViews();
|
||||
},
|
||||
|
||||
/** Get the bbone view class based on the model */
|
||||
_getItemViewClass : function( model ){
|
||||
// override this
|
||||
@@ -502,12 +514,16 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(/** @lends
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------ collection/views syncing
|
||||
_resortViews : function( models ){
|
||||
_modelsToViews : function( models ){
|
||||
var self = this;
|
||||
var viewMap = _.indexBy( self.views, function( v ){ return v.model.id; });
|
||||
return models.map( function( m ){
|
||||
return viewMap[ m.id ] || self._createItemView( m );
|
||||
var reused = 0;
|
||||
// var viewMap = _.indexBy( self.views, function( v ){ return v.model.id; });
|
||||
var views = _.map( models, function( m ){
|
||||
reused += self.viewMap[ m.id ]? 1 : 0;
|
||||
return self.viewMap[ m.id ] || self._createItemView( m );
|
||||
});
|
||||
// console.log( reused, 'reused', views.length - reused, 'created' );
|
||||
return views;
|
||||
},
|
||||
|
||||
/** Add a view (if the model should be viewable) to the panel */
|
||||
|
||||
@@ -482,11 +482,15 @@
|
||||
text-align: right;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
// cursor: pointer;
|
||||
background: white;
|
||||
// a {
|
||||
text-decoration: underline;
|
||||
// }
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
&.fisheye-focus {
|
||||
background: @brand-primary;
|
||||
& > a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user