diff --git a/app/assets/javascripts/discourse/components/eyeline.js b/app/assets/javascripts/discourse/components/eyeline.js index 393decd7d7d..81d21e372f2 100644 --- a/app/assets/javascripts/discourse/components/eyeline.js +++ b/app/assets/javascripts/discourse/components/eyeline.js @@ -14,7 +14,91 @@ **/ Discourse.Eyeline = function Eyeline(selector) { this.selector = selector; -} +}; + + +/** + Call this to analyze the positions of all the nodes in a set + + returns: a hash with top, bottom and onScreen items + {top: , bottom:, onScreen:} + **/ +Discourse.Eyeline.analyze = function(rows) { + var current, goingUp, i, increment, offset, + winHeight, winOffset, detected, onScreen, + bottom, top, outerHeight; + + if (rows.length === 0) return; + + i = parseInt(rows.length / 2, 10); + increment = parseInt(rows.length / 4, 10); + goingUp = undefined; + winOffset = window.pageYOffset || $('html').scrollTop(); + winHeight = window.innerHeight || $(window).height(); + + while (true) { + if (i === 0 || (i >= rows.length - 1)) { + break; + } + current = $(rows[i]); + offset = current.offset(); + + if (offset.top - winHeight < winOffset) { + if (offset.top + current.outerHeight() - window.innerHeight > winOffset) { + break; + } else { + i = i + increment; + if (goingUp !== undefined && increment === 1 && !goingUp) { + break; + } + goingUp = true; + } + } else { + i = i - increment; + if (goingUp !== undefined && increment === 1 && goingUp) { + break; + } + goingUp = false; + } + if (increment > 1) { + increment = parseInt(increment / 2, 10); + goingUp = undefined; + } + if (increment === 0) { + increment = 1; + goingUp = undefined; + } + } + + onScreen = []; + bottom = i; + // quick analysis of whats on screen + while(true) { + if(i < 0) { break;} + + current = $(rows[i]); + offset = current.offset(); + outerHeight = current.outerHeight(); + + // on screen + if(offset.top > winOffset && offset.top + outerHeight < winOffset + winHeight) { + onScreen.unshift(i); + } else { + + if(offset.top < winOffset) { + top = i; + break; + } else { + // bottom + } + } + i -=1; + } + + return({top: top, bottom: bottom, onScreen: onScreen}); + +}; + /** Call this whenever you want to consider what is being seen by the browser @@ -25,9 +109,6 @@ Discourse.Eyeline.prototype.update = function() { var $elements, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight, _this = this; - // before anything ... let us not do anything if we have no focus - if (!Discourse.get('hasFocus')) { return; } - docViewTop = $(window).scrollTop(); windowHeight = $(window).height(); docViewBottom = docViewTop + windowHeight; diff --git a/app/assets/javascripts/discourse/components/screen_track.js b/app/assets/javascripts/discourse/components/screen_track.js index 0eb9371f314..445dc29ef9f 100644 --- a/app/assets/javascripts/discourse/components/screen_track.js +++ b/app/assets/javascripts/discourse/components/screen_track.js @@ -138,6 +138,9 @@ Discourse.ScreenTrack = Ember.Object.extend({ this.topicTime += diff; docViewTop = $(window).scrollTop() + $('header').height(); docViewBottom = docViewTop + $(window).height(); + + // TODO: Eyeline has a smarter more accurate function here + return Object.keys(this.timings, function(id) { var $element, elemBottom, elemTop, timing; $element = $(id); diff --git a/app/assets/javascripts/discourse/mixins/scrolling.js b/app/assets/javascripts/discourse/mixins/scrolling.js index f07a5576119..bf731909d83 100644 --- a/app/assets/javascripts/discourse/mixins/scrolling.js +++ b/app/assets/javascripts/discourse/mixins/scrolling.js @@ -10,14 +10,23 @@ Discourse.Scrolling = Em.Mixin.create({ /** - Begin watching for scroll events. They will be called at max every 100ms. + Begin watching for scroll events. By default they will be called at max every 100ms. + call with {debounce: N} for a diff time @method bindScrolling */ - bindScrolling: function() { + bindScrolling: function(opts) { var onScroll, _this = this; - onScroll = Discourse.debounce(function() { return _this.scrolled(); }, 100); + + opts = opts || {debounce: 100}; + + if (opts.debounce) { + onScroll = Discourse.debounce(function() { return _this.scrolled(); }, 100); + } else { + onScroll = function(){ return _this.scrolled(); }; + } + $(document).bind('touchmove.discourse', onScroll); $(window).bind('scroll.discourse', onScroll); }, diff --git a/app/assets/javascripts/discourse/views/topic_view.js b/app/assets/javascripts/discourse/views/topic_view.js index 542015f2c9c..47fdad29592 100644 --- a/app/assets/javascripts/discourse/views/topic_view.js +++ b/app/assets/javascripts/discourse/views/topic_view.js @@ -36,7 +36,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { progressWidth = ratio * totalWidth; bg = $topicProgress.find('.bg'); bg.stop(true, true); - currentWidth = bg.width() + currentWidth = bg.width(); if (currentWidth === totalWidth) { bg.width(currentWidth - 1); @@ -61,13 +61,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { if (title) return Discourse.set('title', title); }).observes('topic.loaded', 'topic.title'), - newPostsPresent: (function() { - if (this.get('topic.highest_post_number')) { - this.updateBar(); - this.examineRead(); - } - }).observes('topic.highest_post_number'), - currentPostChanged: (function() { var current = this.get('controller.currentPost'); @@ -112,6 +105,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { willDestroyElement: function() { var screenTrack, controller; this.unbindScrolling(); + $(window).unbind('resize.discourse-on-scroll'); controller = this.get('controller'); controller.unsubscribe(); @@ -123,22 +117,13 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { } this.set('screenTrack', null); - - $(window).unbind('scroll.discourse-on-scroll'); - $(document).unbind('touchmove.discourse-on-scroll'); - $(window).unbind('resize.discourse-on-scroll'); - this.resetExamineDockCache(); }, didInsertElement: function(e) { var topicView = this; - var onScroll = Discourse.debounce(function() { return topicView.onScroll(); }, 10); - - $(window).bind('scroll.discourse-on-scroll', onScroll); - $(document).bind('touchmove.discourse-on-scroll', onScroll); - $(window).bind('resize.discourse-on-scroll', onScroll); - this.bindScrolling(); + this.bindScrolling({debounce: 0}); + $(window).bind('resize.discourse-on-scroll', function() { topicView.updatePosition(false); }); var controller = this.get('controller'); controller.subscribe(); @@ -151,44 +136,17 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { screenTrack.start(); this.set('screenTrack', screenTrack); - // Track the user's eyeline - var eyeline = new Discourse.Eyeline('.topic-post'); - eyeline.on('saw', function(e) { - topicView.postSeen(e.detail); - }); - - eyeline.on('sawBottom', function(e) { - topicView.postSeen(e.detail); - topicView.nextPage(e.detail); - }); - - eyeline.on('sawTop', function(e) { - topicView.postSeen(e.detail); - topicView.prevPage(e.detail); - }); - - this.set('eyeline', eyeline); this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) { return Discourse.ClickTrack.trackClick(e); }); - this.onScroll(); + this.updatePosition(true); }, // Triggered whenever any posts are rendered, debounced to save over calling postsRendered: Discourse.debounce(function() { - var $window = $(window); - var $lastPost = $('.row:last'); - - // we consider stuff at the end of the list as read, right away (if it is visible) - if ($window.height() + $window.scrollTop() >= $lastPost.offset().top + $lastPost.height()) { - this.examineRead(); - } else { - // last is not in view, so only examine in 2 seconds - var topicView = this; - Em.run.later(function() { topicView.examineRead(); }, 2000); - } - }, 100), + this.updatePosition(false); + }, 50), resetRead: function(e) { this.get('screenTrack').cancel(); @@ -204,18 +162,16 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { gotFocus: function(){ if (Discourse.get('hasFocus')){ - this.examineRead(); + this.scrolled(); } }.observes("Discourse.hasFocus"), - // Called for every post seen + // Called for every post seen, returns the post number postSeen: function($post) { var post, postView, _ref; - this.set('postNumberSeen', null); postView = Ember.View.views[$post.prop('id')]; if (postView) { post = postView.get('post'); - this.set('postNumberSeen', post.get('post_number')); if (post.get('post_number') > (this.get('topic.last_read_post_number') || 0)) { this.set('topic.last_read_post_number', post.get('post_number')); } @@ -224,6 +180,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { _ref = this.get('screenTrack'); if (_ref) { _ref.guessedSeen(post.get('post_number')); } } + return post.get('post_number'); } }, @@ -309,22 +266,12 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { }).observes('topic.highest_post_number'), loadMore: function(post) { - if (this.get('controller.loading') || this.get('controller.seenBottom')) return; + if (this.get('controller.loading')) { return; } // Don't load if we know we're at the bottom - if (this.get('topic.highest_post_number') === post.get('post_number')) { - var eyeline = this.get('eyeline'); - if (eyeline) { - eyeline.flushRest(); - } + if (this.get('topic.highest_post_number') === post.get('post_number')) { return; } - // Update our current post to the last number we saw - var postNumberSeen = this.get('postNumberSeen'); - if (postNumberSeen) { - this.set('controller.currentPost', postNumberSeen); - } - return; - } + if (this.get('controller.seenBottom')) { return; } // Don't double load ever if (this.topic.posts.last().post_number !== post.post_number) return; @@ -353,26 +300,6 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { }); }, - // Examine which posts are on the screen and mark them as read. Also figure out if we - // need to load more posts. - examineRead: function() { - // Track posts time on screen - var postNumberSeen, _ref, _ref1; - if (_ref = this.get('screenTrack')) { - _ref.scrolled(); - } - - // Update what we can see - if (_ref1 = this.get('eyeline')) { - _ref1.update(); - } - - // Update our current post to the last number we saw - if (postNumberSeen = this.get('postNumberSeen')) { - this.set('controller.currentPost', postNumberSeen); - } - }, - cancelEdit: function() { // close editing mode this.set('editingTopic', false); @@ -415,63 +342,56 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { this.dockedCounter = false; }, - detectDockPosition: function() { - var current, goingUp, i, increment, offset, post, postView, rows, winHeight, winOffset; - rows = $(".topic-post"); - if (rows.length === 0) return; - i = parseInt(rows.length / 2, 10); - increment = parseInt(rows.length / 4, 10); - goingUp = undefined; - winOffset = window.pageYOffset || $('html').scrollTop(); - winHeight = window.innerHeight || $(window).height(); - while (true) { - if (i === 0 || (i >= rows.length - 1)) { - break; - } - current = $(rows[i]); - offset = current.offset(); - if (offset.top - winHeight < winOffset) { - if (offset.top + current.outerHeight() - window.innerHeight > winOffset) { - break; - } else { - i = i + increment; - if (goingUp !== undefined && increment === 1 && !goingUp) { - break; - } - goingUp = true; - } - } else { - i = i - increment; - if (goingUp !== undefined && increment === 1 && goingUp) { - break; - } - goingUp = false; - } - if (increment > 1) { - increment = parseInt(increment / 2, 10); - goingUp = undefined; - } - if (increment === 0) { - increment = 1; - goingUp = undefined; - } - } - postView = Ember.View.views[rows[i].id]; + updateDock: function(postView) { + var post; if (!postView) return; post = postView.get('post'); if (!post) return; this.set('progressPosition', post.get('post_number')); }, - ensureDockIsTestedOnChange: (function() { - // this is subtle, firstPostLoaded will trigger ember to render the view containing #topic-title - // onScroll needs do know about it to be able to make a decision about the dock - Em.run.next(this, this.onScroll); - }).observes('firstPostLoaded'), + nonUrgentPositionUpdate: Discourse.debounce(function(opts){ + var screenTrack = this.get('screenTrack'); + if(opts.userActive && screenTrack) { + screenTrack.scrolled(); + } + this.set('controller.currentPost', opts.currentPost); + },500), - onScroll: function() { - var $lastPost, firstLoaded, lastPostOffset, offset, title; - this.detectDockPosition(); + scrolled: function(){ + this.updatePosition(true); + }, + + updatePosition: function(userActive) { + var $lastPost, firstLoaded, lastPostOffset, offset, + title, info, rows, screenTrack, _this, currentPost; + + _this = this; + rows = $('.topic-post'); + info = Discourse.Eyeline.analyze(rows); + + // top on screen + if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) { + this.prevPage($(rows[0])); + } + + // bottom of screen + if(info.bottom === rows.length-1) { + currentPost = _this.postSeen($(rows[info.bottom])); + this.nextPage($(rows[info.bottom])); + } + + // update dock + this.updateDock(Ember.View.views[rows[info.bottom].id]); + + // mark everything on screen read + $.each(info.onScreen,function(){ + var seen = _this.postSeen($(rows[this])); + currentPost = currentPost || seen; + }); + + this.nonUrgentPositionUpdate({userActive: userActive, currentPost: currentPost || info.bottom}); + offset = window.pageYOffset || $('html').scrollTop(); firstLoaded = this.get('firstPostLoaded'); if (!this.docAt) { @@ -508,6 +428,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { browseMoreMessage: (function() { var category, opts; + opts = { popularLink: "" + (Em.String.i18n("topic.view_popular_topics")) + "" }; @@ -518,12 +439,8 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, { opts.catLink = "" + (Em.String.i18n("topic.browse_all_categories")) + ""; return Ember.String.i18n("topic.read_more", opts); } - }).property(), + }).property() - // The window has been scrolled - scrolled: function(e) { - return this.examineRead(); - } }); Discourse.TopicView.reopenClass({