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:
carlfeberhard
2016-05-06 14:31:32 -04:00
parent 7a6794e104
commit 2d9ec54fe2
17 changed files with 241 additions and 51 deletions
@@ -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;
},
});
+132 -12
View File
@@ -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 );
+31 -15
View File
@@ -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 */
+7 -3
View File
@@ -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