Post Cloaking:
* We now use a new custom view, {{cloaked-collection}} to display posts in a topic. * Posts are removed and inserted (cloaked/uncloaked) into the DOM dynamically based on whether they are visible in the current browser viewport. * There's been a lot of refactoring to ensure the relationship between the post views and the topic controller is sane. * Lots of fixes involving jumping to a post, including a new LockOn component to that tries to stay focused on an element even if stuff is loading before it in the DOM that would normally push it down.
This commit is contained in:
parent
8a9bef944f
commit
40f86829f7
|
@ -200,13 +200,13 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
},
|
||||
|
||||
replyAsNewTopic: function(post) {
|
||||
var composerController = this.get('controllers.composer');
|
||||
var promise = composerController.open({
|
||||
action: Discourse.Composer.CREATE_TOPIC,
|
||||
draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY
|
||||
});
|
||||
var postUrl = "" + location.protocol + "//" + location.host + (post.get('url'));
|
||||
var postLink = "[" + (this.get('title')) + "](" + postUrl + ")";
|
||||
var composerController = this.get('controllers.composer'),
|
||||
promise = composerController.open({
|
||||
action: Discourse.Composer.CREATE_TOPIC,
|
||||
draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY
|
||||
}),
|
||||
postUrl = "" + location.protocol + "//" + location.host + (post.get('url')),
|
||||
postLink = "[" + (this.get('title')) + "](" + postUrl + ")";
|
||||
|
||||
promise.then(function() {
|
||||
Discourse.Post.loadQuote(post.get('id')).then(function(q) {
|
||||
|
@ -459,6 +459,65 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
// If our current post is changed, notify the router
|
||||
_currentPostChanged: function() {
|
||||
var currentPost = this.get('currentPost');
|
||||
if (currentPost) {
|
||||
this.send('postChangedRoute', currentPost);
|
||||
}
|
||||
}.observes('currentPost'),
|
||||
|
||||
sawObjects: function(posts) {
|
||||
if (posts) {
|
||||
var self = this,
|
||||
lastReadPostNumber = this.get('last_read_post_number');
|
||||
|
||||
posts.forEach(function(post) {
|
||||
var postNumber = post.get('post_number');
|
||||
if (postNumber > lastReadPostNumber) {
|
||||
lastReadPostNumber = postNumber;
|
||||
}
|
||||
post.set('read', true);
|
||||
});
|
||||
self.set('last_read_post_number', lastReadPostNumber);
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
topVisibleChanged: function(post) {
|
||||
var postStream = this.get('postStream'),
|
||||
firstLoadedPost = postStream.get('firstLoadedPost');
|
||||
|
||||
this.set('currentPost', post.get('post_number'));
|
||||
|
||||
if (firstLoadedPost && firstLoadedPost === post) {
|
||||
// Note: jQuery shouldn't be done in a controller, but how else can we
|
||||
// trigger a scroll after a promise resolves in a controller? We need
|
||||
// to do this to preserve upwards infinte scrolling.
|
||||
var $body = $('body'),
|
||||
$elem = $('#post-cloak-' + post.get('post_number')),
|
||||
distToElement = $body.scrollTop() - $elem.position().top;
|
||||
|
||||
postStream.prependMore().then(function() {
|
||||
Em.run.next(function () {
|
||||
$elem = $('#post-cloak-' + post.get('post_number'));
|
||||
$('html, body').scrollTop($elem.position().top + distToElement);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
bottomVisibleChanged: function(post) {
|
||||
this.set('progressPosition', post.get('post_number'));
|
||||
|
||||
var postStream = this.get('postStream'),
|
||||
lastLoadedPost = postStream.get('lastLoadedPost');
|
||||
|
||||
if (lastLoadedPost && lastLoadedPost === post) {
|
||||
postStream.appendMore();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
/**
|
||||
Track visible elemnts on the screen.
|
||||
|
||||
You can register for triggers on:
|
||||
|
||||
`focusChanged` the top element we're focusing on
|
||||
|
||||
`seenElement` if we've seen the element
|
||||
|
||||
@class Eyeline
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
|
@ -16,107 +10,23 @@ 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
|
||||
|
||||
@method update
|
||||
**/
|
||||
Discourse.Eyeline.prototype.update = function() {
|
||||
var $elements, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight,
|
||||
_this = this;
|
||||
var docViewTop = $(window).scrollTop(),
|
||||
windowHeight = $(window).height(),
|
||||
docViewBottom = docViewTop + windowHeight,
|
||||
documentHeight = $(document).height(),
|
||||
$elements = $(this.selector),
|
||||
atBottom = false,
|
||||
foundElement = false,
|
||||
bottomOffset = $elements.last().offset(),
|
||||
self = this;
|
||||
|
||||
docViewTop = $(window).scrollTop();
|
||||
windowHeight = $(window).height();
|
||||
docViewBottom = docViewTop + windowHeight;
|
||||
documentHeight = $(document).height();
|
||||
$elements = $(this.selector);
|
||||
atBottom = false;
|
||||
|
||||
if (bottomOffset = $elements.last().offset()) {
|
||||
if (bottomOffset) {
|
||||
atBottom = (bottomOffset.top <= docViewBottom) && (bottomOffset.top >= docViewTop);
|
||||
}
|
||||
|
||||
|
@ -124,14 +34,12 @@ Discourse.Eyeline.prototype.update = function() {
|
|||
foundElement = false;
|
||||
|
||||
return $elements.each(function(i, elem) {
|
||||
var $elem, elemBottom, elemTop, markSeen;
|
||||
var $elem = $(elem),
|
||||
elemTop = $elem.offset().top,
|
||||
elemBottom = elemTop + $elem.height(),
|
||||
markSeen = false;
|
||||
|
||||
$elem = $(elem);
|
||||
elemTop = $elem.offset().top;
|
||||
elemBottom = elemTop + $elem.height();
|
||||
markSeen = false;
|
||||
// It's seen if...
|
||||
|
||||
// ...the element is vertically within the top and botom
|
||||
if ((elemTop <= docViewBottom) && (elemTop >= docViewTop)) markSeen = true;
|
||||
|
||||
|
@ -145,19 +53,17 @@ Discourse.Eyeline.prototype.update = function() {
|
|||
|
||||
// If you hit the bottom we mark all the elements as seen. Otherwise, just the first one
|
||||
if (!atBottom) {
|
||||
_this.trigger('saw', {
|
||||
detail: $elem
|
||||
});
|
||||
self.trigger('saw', { detail: $elem });
|
||||
if (i === 0) {
|
||||
_this.trigger('sawTop', { detail: $elem });
|
||||
self.trigger('sawTop', { detail: $elem });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (i === 0) {
|
||||
_this.trigger('sawTop', { detail: $elem });
|
||||
self.trigger('sawTop', { detail: $elem });
|
||||
}
|
||||
if (i === ($elements.length - 1)) {
|
||||
return _this.trigger('sawBottom', { detail: $elem });
|
||||
return self.trigger('sawBottom', { detail: $elem });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -169,10 +75,9 @@ Discourse.Eyeline.prototype.update = function() {
|
|||
@method flushRest
|
||||
**/
|
||||
Discourse.Eyeline.prototype.flushRest = function() {
|
||||
var eyeline = this;
|
||||
return $(this.selector).each(function(i, elem) {
|
||||
var $elem = $(elem);
|
||||
return eyeline.trigger('saw', { detail: $elem });
|
||||
var self = this;
|
||||
$(this.selector).each(function(i, elem) {
|
||||
return self.trigger('saw', { detail: $(elem) });
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -6,10 +6,13 @@
|
|||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
|
||||
var PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3,
|
||||
MAX_TRACKING_TIME = 1000 * 60 * 6;
|
||||
|
||||
Discourse.ScreenTrack = Ember.Object.extend({
|
||||
|
||||
init: function() {
|
||||
var screenTrack = this;
|
||||
this.reset();
|
||||
},
|
||||
|
||||
|
@ -24,9 +27,9 @@ Discourse.ScreenTrack = Ember.Object.extend({
|
|||
|
||||
// Create an interval timer if we don't have one.
|
||||
if (!this.get('interval')) {
|
||||
var screenTrack = this;
|
||||
var self = this;
|
||||
this.set('interval', setInterval(function () {
|
||||
screenTrack.tick();
|
||||
self.tick();
|
||||
}, 1000));
|
||||
}
|
||||
|
||||
|
@ -57,13 +60,15 @@ Discourse.ScreenTrack = Ember.Object.extend({
|
|||
|
||||
// Reset our timers
|
||||
reset: function() {
|
||||
this.set('lastTick', new Date().getTime());
|
||||
this.set('lastScrolled', new Date().getTime());
|
||||
this.set('lastFlush', 0);
|
||||
this.set('cancelled', false);
|
||||
this.set('timings', {});
|
||||
this.set('totalTimings', {});
|
||||
this.set('topicTime', 0);
|
||||
this.setProperties({
|
||||
lastTick: new Date().getTime(),
|
||||
lastScrolled: new Date().getTime(),
|
||||
lastFlush: 0,
|
||||
cancelled: false,
|
||||
timings: {},
|
||||
totalTimings: {},
|
||||
topicTime: 0
|
||||
});
|
||||
},
|
||||
|
||||
scrolled: function() {
|
||||
|
@ -76,24 +81,23 @@ Discourse.ScreenTrack = Ember.Object.extend({
|
|||
// We don't log anything unless we're logged in
|
||||
if (!Discourse.User.current()) return;
|
||||
|
||||
var newTimings = {};
|
||||
|
||||
// Update our total timings
|
||||
var totalTimings = this.get('totalTimings');
|
||||
var newTimings = {},
|
||||
totalTimings = this.get('totalTimings');
|
||||
|
||||
_.each(this.get('timings'), function(timing,key) {
|
||||
if (!totalTimings[timing.postNumber])
|
||||
totalTimings[timing.postNumber] = 0;
|
||||
|
||||
if (timing.time > 0 && totalTimings[timing.postNumber] < Discourse.ScreenTrack.MAX_TRACKING_TIME) {
|
||||
if (timing.time > 0 && totalTimings[timing.postNumber] < MAX_TRACKING_TIME) {
|
||||
totalTimings[timing.postNumber] += timing.time;
|
||||
newTimings[timing.postNumber] = timing.time;
|
||||
}
|
||||
timing.time = 0;
|
||||
});
|
||||
|
||||
var topicId = parseInt(this.get('topicId'), 10);
|
||||
var highestSeen = 0;
|
||||
var topicId = parseInt(this.get('topicId'), 10),
|
||||
highestSeen = 0;
|
||||
|
||||
_.each(newTimings, function(time,postNumber) {
|
||||
highestSeen = Math.max(highestSeen, parseInt(postNumber, 10));
|
||||
});
|
||||
|
@ -103,6 +107,7 @@ Discourse.ScreenTrack = Ember.Object.extend({
|
|||
highestSeenByTopic[topicId] = highestSeen;
|
||||
Discourse.TopicTrackingState.current().updateSeen(topicId, highestSeen);
|
||||
}
|
||||
|
||||
if (!$.isEmptyObject(newTimings)) {
|
||||
Discourse.ajax('/topics/timings', {
|
||||
data: {
|
||||
|
@ -126,7 +131,7 @@ Discourse.ScreenTrack = Ember.Object.extend({
|
|||
|
||||
// If the user hasn't scrolled the browser in a long time, stop tracking time read
|
||||
var sinceScrolled = new Date().getTime() - this.get('lastScrolled');
|
||||
if (sinceScrolled > Discourse.ScreenTrack.PAUSE_UNLESS_SCROLLED) {
|
||||
if (sinceScrolled > PAUSE_UNLESS_SCROLLED) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
@ -142,18 +147,16 @@ Discourse.ScreenTrack = Ember.Object.extend({
|
|||
if (!Discourse.get("hasFocus")) return;
|
||||
|
||||
this.set('topicTime', this.get('topicTime') + diff);
|
||||
var docViewTop = $(window).scrollTop() + $('header').height();
|
||||
var docViewBottom = docViewTop + $(window).height();
|
||||
var docViewTop = $(window).scrollTop() + $('header').height(),
|
||||
docViewBottom = docViewTop + $(window).height();
|
||||
|
||||
// TODO: Eyeline has a smarter more accurate function here. It's bad to do jQuery
|
||||
// in a model like component, so we should refactor this out later.
|
||||
var screenTrack = this;
|
||||
_.each(this.get('timings'),function(timing,id) {
|
||||
var $element, elemBottom, elemTop;
|
||||
$element = $(id);
|
||||
var $element = $(id);
|
||||
if ($element.length === 1) {
|
||||
elemTop = $element.offset().top;
|
||||
elemBottom = elemTop + $element.height();
|
||||
var elemTop = $element.offset().top,
|
||||
elemBottom = elemTop + $element.height();
|
||||
|
||||
// If part of the element is on the screen, increase the counter
|
||||
if (((docViewTop <= elemTop && elemTop <= docViewBottom)) || ((docViewTop <= elemBottom && elemBottom <= docViewBottom))) {
|
||||
|
@ -165,13 +168,5 @@ Discourse.ScreenTrack = Ember.Object.extend({
|
|||
});
|
||||
|
||||
|
||||
Discourse.ScreenTrack.reopenClass(Discourse.Singleton, {
|
||||
|
||||
// Don't send events if we haven't scrolled in a long time
|
||||
PAUSE_UNLESS_SCROLLED: 1000 * 60 * 3,
|
||||
|
||||
// After 6 minutes stop tracking read position on post
|
||||
MAX_TRACKING_TIME: 1000 * 60 * 6
|
||||
|
||||
});
|
||||
Discourse.ScreenTrack.reopenClass(Discourse.Singleton);
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ Discourse.URL = Em.Object.createWithMixins({
|
|||
// which triggers a replaceState even though the topic hasn't fully loaded yet!
|
||||
Em.run.next(function() {
|
||||
var location = Discourse.URL.get('router.location');
|
||||
if (location.replaceURL) { location.replaceURL(path); }
|
||||
if (location && location.replaceURL) { location.replaceURL(path); }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -133,15 +133,21 @@ Discourse.URL = Em.Object.createWithMixins({
|
|||
Discourse.URL.replaceState(path);
|
||||
|
||||
var topicController = Discourse.__container__.lookup('controller:topic'),
|
||||
opts = {};
|
||||
opts = {},
|
||||
postStream = topicController.get('postStream');
|
||||
|
||||
if (newMatches[3]) opts.nearPost = newMatches[3];
|
||||
var postStream = topicController.get('postStream');
|
||||
var closest = opts.nearPost || 1;
|
||||
|
||||
postStream.refresh(opts).then(function() {
|
||||
topicController.setProperties({
|
||||
currentPost: opts.nearPost || 1,
|
||||
progressPosition: opts.nearPost || 1
|
||||
currentPost: closest,
|
||||
progressPosition: closest,
|
||||
highlightOnInsert: closest,
|
||||
enteredAt: new Date().getTime().toString()
|
||||
});
|
||||
}).then(function() {
|
||||
Discourse.TopicView.jumpToPost(closest);
|
||||
});
|
||||
|
||||
// Abort routing, we have replaced our state.
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
|
||||
Discourse.Scrolling = Em.Mixin.create({
|
||||
|
||||
/**
|
||||
|
@ -18,20 +19,16 @@ Discourse.Scrolling = Em.Mixin.create({
|
|||
bindScrolling: function(opts) {
|
||||
opts = opts || {debounce: 100};
|
||||
|
||||
var scrollingMixin = this;
|
||||
var onScrollMethod;
|
||||
var self = this,
|
||||
onScrollMethod = function(e) {
|
||||
return Em.run.scheduleOnce('afterRender', self, 'scrolled');
|
||||
};
|
||||
|
||||
if (opts.debounce) {
|
||||
onScrollMethod = Discourse.debounce(function() {
|
||||
return scrollingMixin.scrolled();
|
||||
}, opts.debounce);
|
||||
} else {
|
||||
onScrollMethod = function() {
|
||||
return scrollingMixin.scrolled();
|
||||
};
|
||||
onScrollMethod = Discourse.debounce(onScrollMethod, opts.debounce);
|
||||
}
|
||||
|
||||
Discourse.ScrollingDOMMethods.bindOnScroll(onScrollMethod);
|
||||
Discourse.ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -39,8 +36,8 @@ Discourse.Scrolling = Em.Mixin.create({
|
|||
|
||||
@method unbindScrolling
|
||||
*/
|
||||
unbindScrolling: function() {
|
||||
Discourse.ScrollingDOMMethods.unbindOnScroll();
|
||||
unbindScrolling: function(name) {
|
||||
Discourse.ScrollingDOMMethods.unbindOnScroll(name);
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -56,14 +53,16 @@ Discourse.Scrolling = Em.Mixin.create({
|
|||
**/
|
||||
Discourse.ScrollingDOMMethods = {
|
||||
|
||||
bindOnScroll: function(onScrollMethod) {
|
||||
$(document).bind('touchmove.discourse', onScrollMethod);
|
||||
$(window).bind('scroll.discourse', onScrollMethod);
|
||||
bindOnScroll: function(onScrollMethod, name) {
|
||||
name = name || 'default';
|
||||
$(document).bind('touchmove.discourse-' + name, onScrollMethod);
|
||||
$(window).bind('scroll.discourse-' + name, onScrollMethod);
|
||||
},
|
||||
|
||||
unbindOnScroll: function() {
|
||||
$(window).unbind('scroll.discourse');
|
||||
$(document).unbind('touchmove.discourse');
|
||||
unbindOnScroll: function(name) {
|
||||
name = name || 'default';
|
||||
$(window).unbind('scroll.discourse-' + name);
|
||||
$(document).unbind('touchmove.discourse-' + name);
|
||||
}
|
||||
|
||||
};
|
|
@ -50,14 +50,32 @@ Discourse.PostStream = Em.Object.extend({
|
|||
/**
|
||||
Have we loaded the first post in the stream?
|
||||
|
||||
@property firstPostLoaded
|
||||
@property firstPostPresent
|
||||
**/
|
||||
firstPostLoaded: function() {
|
||||
firstPostPresent: function() {
|
||||
if (!this.get('hasLoadedData')) { return false; }
|
||||
return !!this.get('posts').findProperty('id', this.get('firstPostId'));
|
||||
}.property('hasLoadedData', 'posts.[]', 'firstPostId'),
|
||||
|
||||
firstPostNotLoaded: Em.computed.not('firstPostLoaded'),
|
||||
firstPostNotLoaded: Em.computed.not('firstPostPresent'),
|
||||
|
||||
/**
|
||||
The first post that we have loaded. Useful for checking to see if we should scroll upwards
|
||||
|
||||
@property firstLoadedPost
|
||||
**/
|
||||
firstLoadedPost: function() {
|
||||
return _.first(this.get('posts'));
|
||||
}.property('posts.@each'),
|
||||
|
||||
/**
|
||||
The last post we have loaded. Useful for checking to see if we should load more
|
||||
|
||||
@property lastLoadedPost
|
||||
**/
|
||||
lastLoadedPost: function() {
|
||||
return _.last(this.get('posts'));
|
||||
}.property('posts.@each'),
|
||||
|
||||
/**
|
||||
Returns the id of the first post in the set
|
||||
|
@ -80,14 +98,14 @@ Discourse.PostStream = Em.Object.extend({
|
|||
/**
|
||||
Have we loaded the last post in the stream?
|
||||
|
||||
@property lastPostLoaded
|
||||
@property loadedAllPosts
|
||||
**/
|
||||
lastPostLoaded: function() {
|
||||
loadedAllPosts: function() {
|
||||
if (!this.get('hasLoadedData')) { return false; }
|
||||
return !!this.get('posts').findProperty('id', this.get('lastPostId'));
|
||||
}.property('hasLoadedData', 'posts.@each.id', 'lastPostId'),
|
||||
|
||||
lastPostNotLoaded: Em.computed.not('lastPostLoaded'),
|
||||
lastPostNotLoaded: Em.computed.not('loadedAllPosts'),
|
||||
|
||||
/**
|
||||
Returns a JS Object of current stream filter options. It should match the query
|
||||
|
@ -163,18 +181,18 @@ Discourse.PostStream = Em.Object.extend({
|
|||
**/
|
||||
nextWindow: function() {
|
||||
// If we can't find the last post loaded, bail
|
||||
var lastPost = _.last(this.get('posts'));
|
||||
if (!lastPost) { return []; }
|
||||
var lastLoadedPost = this.get('lastLoadedPost');
|
||||
if (!lastLoadedPost) { return []; }
|
||||
|
||||
// Find the index of the last post loaded, if not found, bail
|
||||
var stream = this.get('stream');
|
||||
var lastIndex = this.indexOf(lastPost);
|
||||
var lastIndex = this.indexOf(lastLoadedPost);
|
||||
if (lastIndex === -1) { return []; }
|
||||
if ((lastIndex + 1) >= this.get('filteredPostsCount')) { return []; }
|
||||
|
||||
// find our window of posts
|
||||
return stream.slice(lastIndex+1, lastIndex+Discourse.SiteSettings.posts_per_page+1);
|
||||
}.property('posts.@each', 'stream.@each'),
|
||||
}.property('lastLoadedPost', 'stream.@each'),
|
||||
|
||||
|
||||
/**
|
||||
|
@ -197,7 +215,7 @@ Discourse.PostStream = Em.Object.extend({
|
|||
**/
|
||||
toggleSummary: function() {
|
||||
this.toggleProperty('summary');
|
||||
this.refresh();
|
||||
return this.refresh();
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -227,39 +245,31 @@ Discourse.PostStream = Em.Object.extend({
|
|||
@returns {Ember.Deferred} a promise that is resolved when the posts have been inserted into the stream.
|
||||
**/
|
||||
refresh: function(opts) {
|
||||
|
||||
opts = opts || {};
|
||||
opts.nearPost = parseInt(opts.nearPost, 10);
|
||||
|
||||
var topic = this.get('topic');
|
||||
var postStream = this;
|
||||
var topic = this.get('topic'),
|
||||
self = this;
|
||||
|
||||
// Do we already have the post in our list of posts? Jump there.
|
||||
var postWeWant = this.get('posts').findProperty('post_number', opts.nearPost);
|
||||
if (postWeWant) {
|
||||
Discourse.TopicView.jumpToPost(topic.get('id'), opts.nearPost);
|
||||
return Ember.RSVP.reject();
|
||||
}
|
||||
if (postWeWant) { return Ember.RSVP.resolve(); }
|
||||
|
||||
// TODO: if we have all the posts in the filter, don't go to the server for them.
|
||||
postStream.set('loadingFilter', true);
|
||||
self.set('loadingFilter', true);
|
||||
|
||||
opts = _.merge(opts, postStream.get('streamFilters'));
|
||||
opts = _.merge(opts, self.get('streamFilters'));
|
||||
|
||||
// Request a topicView
|
||||
return Discourse.PostStream.loadTopicView(topic.get('id'), opts).then(function (json) {
|
||||
topic.updateFromJson(json);
|
||||
postStream.updateFromJson(json.post_stream);
|
||||
postStream.setProperties({ loadingFilter: false, loaded: true });
|
||||
self.updateFromJson(json.post_stream);
|
||||
self.setProperties({ loadingFilter: false, loaded: true });
|
||||
|
||||
if (opts.nearPost) {
|
||||
Discourse.TopicView.jumpToPost(topic.get('id'), opts.nearPost);
|
||||
} else {
|
||||
Discourse.TopicView.jumpToPost(topic.get('id'), 1);
|
||||
}
|
||||
|
||||
Discourse.URL.set('queryParams', postStream.get('streamFilters'));
|
||||
}, function(result) {
|
||||
postStream.errorLoading(result);
|
||||
Discourse.URL.set('queryParams', self.get('streamFilters'));
|
||||
}).fail(function(result) {
|
||||
self.errorLoading(result);
|
||||
});
|
||||
},
|
||||
hasLoadedData: Em.computed.and('hasPosts', 'hasStream'),
|
||||
|
@ -271,23 +281,23 @@ Discourse.PostStream = Em.Object.extend({
|
|||
@returns {Ember.Deferred} a promise that's resolved when the posts have been added.
|
||||
**/
|
||||
appendMore: function() {
|
||||
var postStream = this;
|
||||
var self = this;
|
||||
|
||||
// Make sure we can append more posts
|
||||
if (!postStream.get('canAppendMore')) { return Ember.RSVP.reject(); }
|
||||
if (!self.get('canAppendMore')) { return Ember.RSVP.reject(); }
|
||||
|
||||
var postIds = postStream.get('nextWindow');
|
||||
var postIds = self.get('nextWindow');
|
||||
if (Ember.isEmpty(postIds)) { return Ember.RSVP.reject(); }
|
||||
|
||||
postStream.set('loadingBelow', true);
|
||||
self.set('loadingBelow', true);
|
||||
|
||||
var stopLoading = function() {
|
||||
postStream.set('loadingBelow', false);
|
||||
self.set('loadingBelow', false);
|
||||
};
|
||||
|
||||
return postStream.findPostsByIds(postIds).then(function(posts) {
|
||||
return self.findPostsByIds(postIds).then(function(posts) {
|
||||
posts.forEach(function(p) {
|
||||
postStream.appendPost(p);
|
||||
self.appendPost(p);
|
||||
});
|
||||
stopLoading();
|
||||
}, stopLoading);
|
||||
|
@ -349,7 +359,7 @@ Discourse.PostStream = Em.Object.extend({
|
|||
});
|
||||
|
||||
// If we're at the end of the stream, add the post
|
||||
if (this.get('lastPostLoaded')) {
|
||||
if (this.get('loadedAllPosts')) {
|
||||
this.appendPost(post);
|
||||
}
|
||||
|
||||
|
@ -452,11 +462,11 @@ Discourse.PostStream = Em.Object.extend({
|
|||
// We only trigger if there are no filters active
|
||||
if (!this.get('hasNoFilters')) { return; }
|
||||
|
||||
var lastPostLoaded = this.get('lastPostLoaded');
|
||||
var loadedAllPosts = this.get('loadedAllPosts');
|
||||
|
||||
if (this.get('stream').indexOf(postId) === -1) {
|
||||
this.get('stream').addObject(postId);
|
||||
if (lastPostLoaded) { this.appendMore(); }
|
||||
if (loadedAllPosts) { this.appendMore(); }
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -702,8 +712,8 @@ Discourse.PostStream.reopenClass({
|
|||
},
|
||||
|
||||
loadTopicView: function(topicId, args) {
|
||||
var opts = _.merge({}, args);
|
||||
var url = Discourse.getURL("/t/") + topicId;
|
||||
var opts = _.merge({}, args),
|
||||
url = Discourse.getURL("/t/") + topicId;
|
||||
if (opts.nearPost) {
|
||||
url += "/" + opts.nearPost;
|
||||
}
|
||||
|
|
|
@ -7,16 +7,16 @@
|
|||
@module Discourse
|
||||
**/
|
||||
Discourse.TopicFromParamsRoute = Discourse.Route.extend({
|
||||
abc: 'asdfasdf',
|
||||
|
||||
setupController: function(controller, params) {
|
||||
|
||||
params = params || {};
|
||||
params.track_visit = true;
|
||||
|
||||
var topic = this.modelFor('topic');
|
||||
var postStream = topic.get('postStream');
|
||||
var topic = this.modelFor('topic'),
|
||||
postStream = topic.get('postStream'),
|
||||
queryParams = Discourse.URL.get('queryParams');
|
||||
|
||||
var queryParams = Discourse.URL.get('queryParams');
|
||||
if (queryParams) {
|
||||
// Set summary on the postStream if present
|
||||
postStream.set('summary', Em.get(queryParams, 'filter') === 'summary');
|
||||
|
@ -41,9 +41,12 @@ Discourse.TopicFromParamsRoute = Discourse.Route.extend({
|
|||
topicController.setProperties({
|
||||
currentPost: closest,
|
||||
progressPosition: closest,
|
||||
enteredAt: new Date().getTime()
|
||||
enteredAt: new Date().getTime().toString(),
|
||||
highlightOnInsert: closest
|
||||
});
|
||||
|
||||
Discourse.TopicView.jumpToPost(closest);
|
||||
|
||||
if (topic.present('draft')) {
|
||||
composerController.open({
|
||||
draft: Discourse.Draft.getLocal(topic.get('draft_key'), topic.get('draft')),
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
@module Discourse
|
||||
**/
|
||||
Discourse.TopicRoute = Discourse.Route.extend({
|
||||
abc: 'def',
|
||||
|
||||
redirect: function() { Discourse.redirectIfLoginRequired(this); },
|
||||
|
||||
actions: {
|
||||
// Modals that can pop up within a topic
|
||||
|
||||
showPosterExpansion: function(post) {
|
||||
this.controllerFor('posterExpansion').show(post);
|
||||
},
|
||||
|
@ -61,7 +61,17 @@ Discourse.TopicRoute = Discourse.Route.extend({
|
|||
|
||||
splitTopic: function() {
|
||||
Discourse.Route.showModal(this, 'splitTopic', this.modelFor('topic'));
|
||||
}
|
||||
},
|
||||
|
||||
// Use replaceState to update the URL once it changes
|
||||
postChangedRoute: Discourse.debounce(function(currentPost) {
|
||||
var topic = this.modelFor('topic');
|
||||
if (topic && currentPost) {
|
||||
var postUrl = topic.get('url');
|
||||
if (currentPost > 1) { postUrl += "/" + currentPost; }
|
||||
Discourse.URL.replaceState(postUrl);
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
},
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{{#if postStream.loaded}}
|
||||
|
||||
{{#if postStream.firstPostLoaded}}
|
||||
{{#if postStream.firstPostPresent}}
|
||||
<div id='topic-title'>
|
||||
<div class='container'>
|
||||
<div class='inner'>
|
||||
|
@ -64,7 +64,7 @@
|
|||
{{/if}}
|
||||
|
||||
{{#unless postStream.loadingFilter}}
|
||||
{{collection itemViewClass="Discourse.PostView" contentBinding="postStream.posts" topicViewBinding="view"}}
|
||||
{{cloaked-collection cloakView="post" idProperty="post_number" defaultHeight="200" content=postStream.posts}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if postStream.loadingBelow}}
|
||||
|
@ -76,7 +76,7 @@
|
|||
{{#if postStream.loadingFilter}}
|
||||
<div class='spinner small'>{{i18n loading}}</div>
|
||||
{{else}}
|
||||
{{#if postStream.lastPostLoaded}}
|
||||
{{#if postStream.loadedAllPosts}}
|
||||
|
||||
{{view Discourse.TopicClosingView topicBinding="model"}}
|
||||
{{view Discourse.TopicFooterButtonsView topicBinding="model"}}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
Display a list of cloaked items
|
||||
|
||||
@class CloakedContainerView
|
||||
@extends Discourse.View
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
|
||||
var SLACK_RATIO = 0.75;
|
||||
|
||||
Discourse.CloakedCollectionView = Ember.CollectionView.extend(Discourse.Scrolling, {
|
||||
topVisible: null,
|
||||
bottomVisible: null,
|
||||
|
||||
init: function() {
|
||||
var cloakView = this.get('cloakView'),
|
||||
idProperty = this.get('idProperty') || 'id';
|
||||
|
||||
this.set('itemViewClass', Discourse.CloakedView.extend({
|
||||
classNames: [cloakView + '-cloak'],
|
||||
cloaks: Em.String.classify(cloakView) + 'View',
|
||||
defaultHeight: this.get('defaultHeight') || 100,
|
||||
|
||||
init: function() {
|
||||
this._super();
|
||||
this.set('elementId', cloakView + '-cloak-' + this.get('content.' + idProperty));
|
||||
}
|
||||
}));
|
||||
|
||||
this._super();
|
||||
Ember.run.next(this, 'scrolled');
|
||||
},
|
||||
|
||||
/**
|
||||
If the topmost visible view changed, we will notify the controller if it has an appropriate hook.
|
||||
|
||||
@method _topVisibleChanged
|
||||
@observes topVisible
|
||||
**/
|
||||
_topVisibleChanged: function() {
|
||||
var controller = this.get('controller');
|
||||
if (controller.topVisibleChanged) { controller.topVisibleChanged(this.get('topVisible')); }
|
||||
}.observes('topVisible'),
|
||||
|
||||
/**
|
||||
If the bottommost visible view changed, we will notify the controller if it has an appropriate hook.
|
||||
|
||||
@method _bottomVisible
|
||||
@observes bottomVisible
|
||||
**/
|
||||
_bottomVisible: function() {
|
||||
var controller = this.get('controller');
|
||||
if (controller.bottomVisibleChanged) { controller.bottomVisibleChanged(this.get('bottomVisible')); }
|
||||
}.observes('bottomVisible'),
|
||||
|
||||
/**
|
||||
Binary search for finding the topmost view on screen.
|
||||
|
||||
@method findTopView
|
||||
@param {Array} childViews the childViews to search through
|
||||
@param {Number} windowTop The top of the viewport to search against
|
||||
@param {Number} min The minimum index to search through of the child views
|
||||
@param {Number} max The max index to search through of the child views
|
||||
@returns {Number} the index into childViews of the topmost view
|
||||
**/
|
||||
findTopView: function(childViews, viewportTop, min, max) {
|
||||
if (max < min) { return min; }
|
||||
|
||||
var mid = Math.floor((min + max) / 2),
|
||||
$view = childViews[mid].$(),
|
||||
viewBottom = $view.offset().top + $view.height();
|
||||
|
||||
if (viewBottom > viewportTop) {
|
||||
return this.findTopView(childViews, viewportTop, min, mid-1);
|
||||
} else {
|
||||
return this.findTopView(childViews, viewportTop, mid+1, max);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
Determine what views are onscreen and cloak/uncloak them as necessary.
|
||||
|
||||
@method scrolled
|
||||
**/
|
||||
scrolled: function() {
|
||||
var childViews = this.get('childViews'),
|
||||
toUncloak = [],
|
||||
$w = $(window),
|
||||
windowHeight = $w.height(),
|
||||
windowTop = $w.scrollTop(),
|
||||
slack = Math.round(windowHeight * SLACK_RATIO),
|
||||
viewportTop = windowTop - slack,
|
||||
windowBottom = windowTop + windowHeight,
|
||||
viewportBottom = windowBottom + slack,
|
||||
topView = this.findTopView(childViews, viewportTop, 0, childViews.length-1),
|
||||
bodyHeight = $('body').height(),
|
||||
bottomView = topView,
|
||||
onscreen = [];
|
||||
|
||||
if (windowBottom > bodyHeight) { windowBottom = bodyHeight; }
|
||||
if (viewportBottom > bodyHeight) { viewportBottom = bodyHeight; }
|
||||
|
||||
// Find the bottom view and what's onscreen
|
||||
while (bottomView < childViews.length) {
|
||||
var view = childViews[bottomView],
|
||||
$view = view.$(),
|
||||
viewTop = $view.offset().top,
|
||||
viewBottom = viewTop + $view.height();
|
||||
|
||||
if (viewTop > viewportBottom) { break; }
|
||||
toUncloak.push(view);
|
||||
|
||||
if (viewBottom > windowTop && viewTop <= windowBottom) {
|
||||
onscreen.push(view.get('content'));
|
||||
}
|
||||
|
||||
bottomView++;
|
||||
}
|
||||
if (bottomView >= childViews.length) { bottomView = childViews.length - 1; }
|
||||
|
||||
// If our controller has a `sawObjects` method, pass the on screen objects to it.
|
||||
var controller = this.get('controller');
|
||||
if (onscreen.length) {
|
||||
this.setProperties({topVisible: onscreen[0], bottomVisible: onscreen[onscreen.length-1]});
|
||||
if (controller && controller.sawObjects) {
|
||||
Em.run.schedule('afterRender', function() {
|
||||
controller.sawObjects(onscreen);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.setProperties({topVisible: null, bottomVisible: null});
|
||||
}
|
||||
|
||||
var toCloak = childViews.slice(0, topView).concat(childViews.slice(bottomView+1));
|
||||
Em.run.schedule('afterRender', function() {
|
||||
toUncloak.forEach(function (v) { v.uncloak(); });
|
||||
toCloak.forEach(function (v) { v.cloak(); });
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
didInsertElement: function() {
|
||||
this.bindScrolling({debounce: 10});
|
||||
},
|
||||
|
||||
willDestroyElement: function() {
|
||||
this.unbindScrolling();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
Discourse.View.registerHelper('cloaked-collection', Discourse.CloakedCollectionView);
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
A cloaked view is one that removes its content when scrolled off the screen
|
||||
|
||||
@class CloakedView
|
||||
@extends Discourse.View
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.CloakedView = Discourse.View.extend({
|
||||
attributeBindings: ['style'],
|
||||
|
||||
init: function() {
|
||||
this._super();
|
||||
this.set('style', 'height: ' + this.get('defaultHeight') + 'px');
|
||||
},
|
||||
|
||||
/**
|
||||
Triggers the set up for rendering a view that is cloaked.
|
||||
|
||||
@method uncloak
|
||||
*/
|
||||
uncloak: function() {
|
||||
var containedView = this.get('containedView');
|
||||
if (!containedView) {
|
||||
this.setProperties({
|
||||
style: null,
|
||||
containedView: this.createChildView(Discourse[this.get('cloaks')], { content: this.get('content') })
|
||||
});
|
||||
|
||||
this.rerender();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
Removes the view from the DOM and tears down all observers.
|
||||
|
||||
@method cloak
|
||||
*/
|
||||
cloak: function() {
|
||||
var containedView = this.get('containedView'),
|
||||
self = this;
|
||||
|
||||
if (containedView && this.get('state') === 'inDOM') {
|
||||
var style = 'height: ' + this.$().height() + 'px;';
|
||||
this.set('style', style);
|
||||
this.$().prop('style', style);
|
||||
|
||||
// We need to remove the container after the height of the element has taken
|
||||
// effect.
|
||||
Ember.run.schedule('afterRender', function() {
|
||||
self.set('containedView', null);
|
||||
containedView.willDestroyElement();
|
||||
containedView.remove();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
Render the cloaked view if applicable.
|
||||
|
||||
@method render
|
||||
*/
|
||||
render: function(buffer) {
|
||||
var containedView = this.get('containedView');
|
||||
if (containedView && containedView.get('state') !== 'inDOM') {
|
||||
containedView.renderToBuffer(buffer);
|
||||
containedView.transitionTo('inDOM');
|
||||
Em.run.schedule('afterRender', function() {
|
||||
containedView.didInsertElement();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -10,8 +10,7 @@ Discourse.TextField = Ember.TextField.extend({
|
|||
attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus'],
|
||||
|
||||
placeholder: function() {
|
||||
|
||||
if( this.get('placeholderKey') ) {
|
||||
if (this.get('placeholderKey')) {
|
||||
return I18n.t(this.get('placeholderKey'));
|
||||
} else {
|
||||
return '';
|
||||
|
|
|
@ -172,18 +172,17 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, {
|
|||
|
||||
// Add the quote controls to a post
|
||||
insertQuoteControls: function() {
|
||||
var postView = this;
|
||||
|
||||
var self = this;
|
||||
return this.$('aside.quote').each(function(i, e) {
|
||||
var $aside = $(e);
|
||||
postView.updateQuoteElements($aside, 'chevron-down');
|
||||
self.updateQuoteElements($aside, 'chevron-down');
|
||||
var $title = $('.title', $aside);
|
||||
|
||||
// Unless it's a full quote, allow click to expand
|
||||
if (!($aside.data('full') || $title.data('has-quote-controls'))) {
|
||||
$title.on('click', function(e) {
|
||||
if ($(e.target).is('a')) return true;
|
||||
postView.toggleQuote($aside);
|
||||
self.toggleQuote($aside);
|
||||
});
|
||||
$title.data('has-quote-controls', true);
|
||||
}
|
||||
|
@ -191,17 +190,34 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, {
|
|||
},
|
||||
|
||||
willDestroyElement: function() {
|
||||
Discourse.ScreenTrack.current().stopTracking(this.$().prop('id'));
|
||||
Discourse.ScreenTrack.current().stopTracking(this.get('elementId'));
|
||||
},
|
||||
|
||||
didInsertElement: function() {
|
||||
var $post = this.$(),
|
||||
post = this.get('post');
|
||||
post = this.get('post'),
|
||||
postNumber = post.get('post_number'),
|
||||
highlightNumber = this.get('controller.highlightOnInsert');
|
||||
|
||||
// If we're meant to highlight a post
|
||||
if ((highlightNumber > 1) && (highlightNumber === postNumber)) {
|
||||
this.set('controller.highlightOnInsert', null);
|
||||
var $contents = $('.topic-body .contents', $post),
|
||||
origColor = $contents.data('orig-color') || $contents.css('backgroundColor');
|
||||
|
||||
$contents.data("orig-color", origColor);
|
||||
$contents
|
||||
.addClass('highlighted')
|
||||
.stop()
|
||||
.animate({ backgroundColor: origColor }, 2500, 'swing', function(){
|
||||
$contents.removeClass('highlighted');
|
||||
});
|
||||
}
|
||||
|
||||
this.showLinkCounts();
|
||||
|
||||
// Track this post
|
||||
Discourse.ScreenTrack.current().track(this.$().prop('id'), this.get('post.post_number'));
|
||||
Discourse.ScreenTrack.current().track(this.$().prop('id'), postNumber);
|
||||
|
||||
// Add syntax highlighting
|
||||
Discourse.SyntaxHighlighting.apply($post);
|
||||
|
@ -211,7 +227,5 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, {
|
|||
|
||||
// Find all the quotes
|
||||
this.insertQuoteControls();
|
||||
|
||||
$post.addClass('ready');
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/*global LockOn:true*/
|
||||
|
||||
/**
|
||||
This view is for rendering an icon representing the status of a topic
|
||||
|
||||
|
@ -21,10 +23,10 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
postStream: Em.computed.alias('controller.postStream'),
|
||||
|
||||
updateBar: function() {
|
||||
Em.run.scheduleOnce('afterRender', this, 'updateProgressBar');
|
||||
Em.run.scheduleOnce('afterRender', this, '_updateProgressBar');
|
||||
}.observes('controller.streamPercentage'),
|
||||
|
||||
updateProgressBar: function() {
|
||||
_updateProgressBar: function() {
|
||||
var $topicProgress = this._topicProgress;
|
||||
|
||||
// cache lookup
|
||||
|
@ -45,66 +47,36 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
.width(progressWidth);
|
||||
},
|
||||
|
||||
updateTitle: function() {
|
||||
_updateTitle: function() {
|
||||
var title = this.get('topic.title');
|
||||
if (title) return Discourse.set('title', title);
|
||||
}.observes('topic.loaded', 'topic.title'),
|
||||
|
||||
currentPostChanged: function() {
|
||||
var current = this.get('controller.currentPost');
|
||||
|
||||
var topic = this.get('topic');
|
||||
if (!(current && topic)) return;
|
||||
|
||||
if (current > (this.get('maxPost') || 0)) {
|
||||
this.set('maxPost', current);
|
||||
}
|
||||
|
||||
var postUrl = topic.get('url');
|
||||
if (current > 1) { postUrl += "/" + current; }
|
||||
// TODO: @Robin, this should all be integrated into the router,
|
||||
// the view should not be performing routing work
|
||||
//
|
||||
// This workaround ensures the router is aware the route changed,
|
||||
// without it, the up button was broken on long topics.
|
||||
// To repro, go to a topic with 50 posts, go to first post,
|
||||
// scroll to end, click up button ... nothing happens
|
||||
var handler =_.first(
|
||||
_.where(Discourse.URL.get("router.router.currentHandlerInfos"),
|
||||
function(o) {
|
||||
return o.name === "topic.fromParams";
|
||||
})
|
||||
);
|
||||
if(handler){
|
||||
handler.context = {nearPost: current};
|
||||
}
|
||||
Discourse.URL.replaceState(postUrl);
|
||||
}.observes('controller.currentPost', 'highest_post_number'),
|
||||
|
||||
composeChanged: function() {
|
||||
_composeChanged: function() {
|
||||
var composerController = Discourse.get('router.composerController');
|
||||
composerController.clearState();
|
||||
composerController.set('topic', this.get('topic'));
|
||||
}.observes('composer'),
|
||||
|
||||
enteredTopic: function() {
|
||||
_enteredTopic: function() {
|
||||
this._topicProgress = undefined;
|
||||
if (this.present('controller.enteredAt')) {
|
||||
var topicView = this;
|
||||
Em.run.schedule('afterRender', function() {
|
||||
topicView.updatePosition();
|
||||
});
|
||||
|
||||
// Ember is supposed to only call observers when values change but something
|
||||
// in our view set up is firing this observer with the same value. This check
|
||||
// prevents scrolled from being called twice.
|
||||
var enteredAt = this.get('controller.enteredAt');
|
||||
if (enteredAt && (this.get('lastEnteredAt') !== enteredAt)) {
|
||||
this.scrolled();
|
||||
this.set('lastEnteredAt', enteredAt);
|
||||
}
|
||||
}.observes('controller.enteredAt'),
|
||||
|
||||
didInsertElement: function(e) {
|
||||
this.bindScrolling({debounce: 0});
|
||||
this.bindScrolling({name: 'topic-view'});
|
||||
|
||||
var topicView = this;
|
||||
Em.run.schedule('afterRender', function () {
|
||||
$(window).resize('resize.discourse-on-scroll', function() {
|
||||
topicView.updatePosition();
|
||||
});
|
||||
$(window).resize('resize.discourse-on-scroll', function() {
|
||||
topicView.scrolled();
|
||||
});
|
||||
|
||||
// This get seems counter intuitive, but it's to trigger the observer on
|
||||
|
@ -120,8 +92,7 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
|
||||
// This view is being removed. Shut down operations
|
||||
willDestroyElement: function() {
|
||||
|
||||
this.unbindScrolling();
|
||||
this.unbindScrolling('topic-view');
|
||||
$(window).unbind('resize.discourse-on-scroll');
|
||||
|
||||
// Unbind link tracking
|
||||
|
@ -170,128 +141,16 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
}
|
||||
}.observes("Discourse.hasFocus"),
|
||||
|
||||
getPost: function($post){
|
||||
var post, postView;
|
||||
postView = Ember.View.views[$post.prop('id')];
|
||||
if (postView) {
|
||||
return postView.get('post');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// Called for every post seen, returns the post number
|
||||
postSeen: function($post) {
|
||||
var post = this.getPost($post);
|
||||
|
||||
if (post) {
|
||||
var postNumber = post.get('post_number');
|
||||
if (postNumber > (this.get('controller.last_read_post_number') || 0)) {
|
||||
this.set('controller.last_read_post_number', postNumber);
|
||||
}
|
||||
if (!post.get('read')) {
|
||||
post.set('read', true);
|
||||
}
|
||||
return post.get('post_number');
|
||||
}
|
||||
},
|
||||
|
||||
resetExamineDockCache: function() {
|
||||
this.set('docAt', false);
|
||||
},
|
||||
|
||||
updateDock: function(postView) {
|
||||
if (!postView) return;
|
||||
var post = postView.get('post');
|
||||
if (!post) return;
|
||||
|
||||
this.set('controller.progressPosition', this.get('postStream').indexOf(post) + 1);
|
||||
},
|
||||
|
||||
throttledPositionUpdate: Discourse.debounce(function() {
|
||||
Discourse.ScreenTrack.current().scrolled();
|
||||
var model = this.get('controller.model');
|
||||
if (model && this.get('nextPositionUpdate')) {
|
||||
this.set('controller.currentPost', this.get('nextPositionUpdate'));
|
||||
}
|
||||
},500),
|
||||
|
||||
scrolled: function(){
|
||||
this.updatePosition();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
Process the posts the current user has seen in the topic.
|
||||
|
||||
@private
|
||||
@method processSeenPosts
|
||||
**/
|
||||
processSeenPosts: function() {
|
||||
var rows = $('.topic-post.ready');
|
||||
if (!rows || rows.length === 0) { return; }
|
||||
|
||||
// if we have no rows
|
||||
var info = Discourse.Eyeline.analyze(rows);
|
||||
if(!info) { return; }
|
||||
|
||||
// We disable scrolling of the topic while performing initial positioning
|
||||
// This code needs to be refactored, the pipline for positioning posts is wack
|
||||
// Be sure to test on safari as well when playing with this
|
||||
if(!Discourse.TopicView.disableScroll) {
|
||||
|
||||
// are we scrolling upwards?
|
||||
if(info.top === 0 || info.onScreen[0] === 0 || info.bottom === 0) {
|
||||
var $body = $('body'),
|
||||
$elem = $(rows[0]),
|
||||
distToElement = $body.scrollTop() - $elem.position().top;
|
||||
this.get('postStream').prependMore().then(function() {
|
||||
Em.run.next(function () {
|
||||
$('html, body').scrollTop($elem.position().top + distToElement);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// are we scrolling down?
|
||||
var currentPost;
|
||||
if(info.bottom === rows.length-1) {
|
||||
currentPost = this.postSeen($(rows[info.bottom]));
|
||||
this.get('postStream').appendMore();
|
||||
}
|
||||
|
||||
|
||||
// update dock
|
||||
this.updateDock(Ember.View.views[rows[info.bottom].id]);
|
||||
|
||||
// mark everything on screen read
|
||||
var topicView = this;
|
||||
_.each(info.onScreen,function(item){
|
||||
var seen = topicView.postSeen($(rows[item]));
|
||||
currentPost = currentPost || seen;
|
||||
});
|
||||
|
||||
var currentForPositionUpdate = currentPost;
|
||||
if (!currentForPositionUpdate) {
|
||||
var postView = this.getPost($(rows[info.bottom]));
|
||||
if (postView) { currentForPositionUpdate = postView.get('post_number'); }
|
||||
}
|
||||
|
||||
if (currentForPositionUpdate) {
|
||||
this.set('nextPositionUpdate', currentPost || currentForPositionUpdate);
|
||||
this.throttledPositionUpdate();
|
||||
} else {
|
||||
console.error("can't update position ");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
The user has scrolled the window, or it is finished rendering and ready for processing.
|
||||
|
||||
@method updatePosition
|
||||
@method scrolled
|
||||
**/
|
||||
updatePosition: function() {
|
||||
this.processSeenPosts();
|
||||
scrolled: function(){
|
||||
|
||||
var offset = window.pageYOffset || $('html').scrollTop();
|
||||
if (!this.get('docAt')) {
|
||||
|
@ -324,11 +183,8 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
}.property(),
|
||||
|
||||
browseMoreMessage: function() {
|
||||
var opts = {
|
||||
latestLink: "<a href=\"/\">" + (I18n.t("topic.view_latest_topics")) + "</a>"
|
||||
};
|
||||
|
||||
var category = this.get('controller.content.category');
|
||||
var opts = { latestLink: "<a href=\"/\">" + (I18n.t("topic.view_latest_topics")) + "</a>" },
|
||||
category = this.get('controller.content.category');
|
||||
|
||||
if(Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id")) {
|
||||
category = null;
|
||||
|
@ -340,10 +196,9 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (I18n.t("topic.browse_all_categories")) + "</a>";
|
||||
}
|
||||
|
||||
var tracking = this.get('topicTrackingState');
|
||||
|
||||
var unreadTopics = tracking.countUnread();
|
||||
var newTopics = tracking.countNew();
|
||||
var tracking = this.get('topicTrackingState'),
|
||||
unreadTopics = tracking.countUnread(),
|
||||
newTopics = tracking.countNew();
|
||||
|
||||
if (newTopics + unreadTopics > 0) {
|
||||
var hasBoth = unreadTopics > 0 && newTopics > 0;
|
||||
|
@ -368,88 +223,23 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
|
|||
|
||||
Discourse.TopicView.reopenClass({
|
||||
|
||||
// Scroll to a given post, if in the DOM. Returns whether it was in the DOM or not.
|
||||
jumpToPost: function(topicId, postNumber, avoidScrollIfPossible) {
|
||||
this.disableScroll = true;
|
||||
Em.run.scheduleOnce('afterRender', function() {
|
||||
var rows = $('.topic-post.ready');
|
||||
jumpToPost: function(postNumber) {
|
||||
var holderId = '#post-cloak-' + postNumber;
|
||||
|
||||
// Make sure we're looking at the topic we want to scroll to
|
||||
if (topicId !== parseInt($('#topic').data('topic-id'), 10)) { return false; }
|
||||
|
||||
var $post = $("#post_" + postNumber);
|
||||
if ($post.length) {
|
||||
|
||||
var postTop = $post.offset().top;
|
||||
var highlight = true;
|
||||
|
||||
var header = $('header');
|
||||
var title = $('#topic-title');
|
||||
var expectedOffset = title.height() - header.find('.contents').height();
|
||||
|
||||
if (expectedOffset < 0) {
|
||||
expectedOffset = 0;
|
||||
}
|
||||
|
||||
var offset = (header.outerHeight(true) + expectedOffset);
|
||||
var windowScrollTop = $('html, body').scrollTop();
|
||||
|
||||
if (avoidScrollIfPossible && postTop > windowScrollTop + offset && postTop < windowScrollTop + $(window).height() + 100) {
|
||||
// in view
|
||||
} else {
|
||||
// not in view ... bring into view
|
||||
if (postNumber === 1) {
|
||||
$(window).scrollTop(0);
|
||||
highlight = false;
|
||||
} else {
|
||||
var desired = $post.offset().top - offset;
|
||||
$(window).scrollTop(desired);
|
||||
|
||||
// TODO @Robin, I am seeing multiple events in chrome issued after
|
||||
// jumpToPost if I refresh a page, sometimes I see 2, sometimes 3
|
||||
//
|
||||
// 1. Where are they coming from?
|
||||
// 2. On refresh we should only issue a single scrollTop
|
||||
// 3. If you are scrolled down in BoingBoing desired sometimes is wrong
|
||||
// due to vanishing header, we should not be rendering it imho until after
|
||||
// we render the posts
|
||||
|
||||
var first = true;
|
||||
var t = new Date();
|
||||
// console.log("DESIRED:" + desired);
|
||||
var enforceDesired = function(){
|
||||
if($(window).scrollTop() !== desired) {
|
||||
console.log("GOT EVENT " + $(window).scrollTop());
|
||||
console.log("Time " + (new Date() - t));
|
||||
console.trace();
|
||||
if(first) {
|
||||
$(window).scrollTop(desired);
|
||||
first = false;
|
||||
}
|
||||
// $(document).unbind("scroll", enforceDesired);
|
||||
}
|
||||
};
|
||||
|
||||
// uncomment this line to help debug this issue.
|
||||
// $(document).scroll(enforceDesired);
|
||||
}
|
||||
}
|
||||
|
||||
if(highlight) {
|
||||
var $contents = $('.topic-body .contents', $post);
|
||||
var origColor = $contents.data('orig-color') || $contents.css('backgroundColor');
|
||||
|
||||
$contents.data("orig-color", origColor);
|
||||
$contents
|
||||
.addClass('highlighted')
|
||||
.stop()
|
||||
.animate({ backgroundColor: origColor }, 2500, 'swing', function(){
|
||||
$contents.removeClass('highlighted');
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(function(){Discourse.TopicView.disableScroll = false;}, 500);
|
||||
Em.run.schedule('afterRender', function() {
|
||||
if (postNumber === 1) {
|
||||
$(window).scrollTop(0);
|
||||
return;
|
||||
}
|
||||
|
||||
new LockOn(holderId, {offsetCalculator: function() {
|
||||
var $header = $('header'),
|
||||
$title = $('#topic-title'),
|
||||
expectedOffset = $title.height() - $header.find('.contents').height();
|
||||
|
||||
return $header.outerHeight(true) + ((expectedOffset < 0) ? 0 : expectedOffset);
|
||||
}}).lock();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
//= require mousetrap.js
|
||||
//= require rsvp.js
|
||||
//= require show-html.js
|
||||
//= require lock-on.js
|
||||
|
||||
//= require ./discourse/helpers/i18n_helpers
|
||||
//= require ./discourse/mixins/ajax
|
||||
|
|
|
@ -17,7 +17,7 @@ h1 .topic-statuses .topic-status i {margin-right: 5px;}
|
|||
|
||||
.logo-small {margin-right: 8px;}
|
||||
|
||||
.topic-post {
|
||||
.post-cloak {
|
||||
padding: 0;
|
||||
|
||||
&:first-of-type {
|
||||
|
|
|
@ -5,6 +5,6 @@ test("Enter a Topic", function() {
|
|||
|
||||
visit("/t/internationalization-localization/280").then(function() {
|
||||
ok(exists("#topic"), "The was rendered");
|
||||
ok(exists("#topic .topic-post"), "The topic has posts");
|
||||
ok(exists("#topic .post-cloak"), "The topic has cloaked posts");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,17 +29,17 @@ test('appending posts', function() {
|
|||
equal(postStream.get('lastPostId'), 4, "the last post id is 4");
|
||||
|
||||
ok(!postStream.get('hasPosts'), "there are no posts by default");
|
||||
ok(!postStream.get('firstPostLoaded'), "the first post is not loaded");
|
||||
ok(!postStream.get('lastPostLoaded'), "the last post is not loaded");
|
||||
ok(!postStream.get('firstPostPresent'), "the first post is not loaded");
|
||||
ok(!postStream.get('loadedAllPosts'), "the last post is not loaded");
|
||||
equal(postStream.get('posts.length'), 0, "it has no posts initially");
|
||||
|
||||
postStream.appendPost(Discourse.Post.create({id: 2, post_number: 2}));
|
||||
ok(!postStream.get('firstPostLoaded'), "the first post is still not loaded");
|
||||
ok(!postStream.get('firstPostPresent'), "the first post is still not loaded");
|
||||
equal(postStream.get('posts.length'), 1, "it has one post in the stream");
|
||||
|
||||
postStream.appendPost(Discourse.Post.create({id: 4, post_number: 4}));
|
||||
ok(!postStream.get('firstPostLoaded'), "the first post is still loaded");
|
||||
ok(postStream.get('lastPostLoaded'), "the last post is now loaded");
|
||||
ok(!postStream.get('firstPostPresent'), "the first post is still loaded");
|
||||
ok(postStream.get('loadedAllPosts'), "the last post is now loaded");
|
||||
equal(postStream.get('posts.length'), 2, "it has two posts in the stream");
|
||||
|
||||
postStream.appendPost(Discourse.Post.create({id: 4, post_number: 4}));
|
||||
|
@ -54,8 +54,8 @@ test('appending posts', function() {
|
|||
|
||||
// change the stream
|
||||
postStream.set('stream', [1, 2, 4]);
|
||||
ok(!postStream.get('firstPostLoaded'), "the first post no longer loaded since the stream changed.");
|
||||
ok(postStream.get('lastPostLoaded'), "the last post is still the last post in the new stream");
|
||||
ok(!postStream.get('firstPostPresent'), "the first post no longer loaded since the stream changed.");
|
||||
ok(postStream.get('loadedAllPosts'), "the last post is still the last post in the new stream");
|
||||
});
|
||||
|
||||
test('closestPostNumberFor', function() {
|
||||
|
@ -383,18 +383,18 @@ test('triggerNewPostInStream', function() {
|
|||
});
|
||||
|
||||
|
||||
test("lastPostLoaded when the id changes", function() {
|
||||
test("loadedAllPosts when the id changes", function() {
|
||||
// This can happen in a race condition between staging a post and it coming through on the
|
||||
// message bus. If the id of a post changes we should reconsider the lastPostLoaded property.
|
||||
// message bus. If the id of a post changes we should reconsider the loadedAllPosts property.
|
||||
var postStream = buildStream(10101, [1, 2]);
|
||||
var postWithoutId = Discourse.Post.create({ raw: 'hello world this is my new post' });
|
||||
|
||||
postStream.appendPost(Discourse.Post.create({id: 1, post_number: 1}));
|
||||
postStream.appendPost(postWithoutId);
|
||||
ok(!postStream.get('lastPostLoaded'), 'the last post is not loaded');
|
||||
ok(!postStream.get('loadedAllPosts'), 'the last post is not loaded');
|
||||
|
||||
postWithoutId.set('id', 2);
|
||||
ok(postStream.get('lastPostLoaded'), 'the last post is loaded now that the post has an id');
|
||||
ok(postStream.get('loadedAllPosts'), 'the last post is loaded now that the post has an id');
|
||||
});
|
||||
|
||||
test("comitting and triggerNewPostInStream race condition", function() {
|
||||
|
|
|
@ -22075,7 +22075,7 @@ Ember.merge(inBuffer, {
|
|||
// when a view is rendered in a buffer, rerendering it simply
|
||||
// replaces the existing buffer with a new one
|
||||
rerender: function(view) {
|
||||
throw new Ember.Error("Something you did caused a view to re-render after it rendered but before it was inserted into the DOM.");
|
||||
throw new Ember.Error("Something you did caused a view to re-render after it rendered but before it was inserted into the DOM." + view.get('content.id'));
|
||||
},
|
||||
|
||||
// when a view is rendered in a buffer, appending a child
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
(function (exports) {
|
||||
|
||||
var scrollEvents = "scroll.lock-on touchmove.lock-on mousedown.lock-on wheel.lock-on DOMMouseScroll.lock-on mousewheel.lock-on keyup.lock-on";
|
||||
|
||||
var LockOn = function(selector, options) {
|
||||
this.selector = selector;
|
||||
this.options = options || {};
|
||||
};
|
||||
|
||||
LockOn.prototype.elementTop = function() {
|
||||
var offsetCalculator = this.options.offsetCalculator;
|
||||
return $(this.selector).offset().top - (offsetCalculator ? offsetCalculator() : 0);
|
||||
};
|
||||
|
||||
LockOn.prototype.lock = function() {
|
||||
var self = this,
|
||||
previousTop = this.elementTop(),
|
||||
startedAt = new Date().getTime()
|
||||
i = 0;
|
||||
|
||||
$(window).scrollTop(previousTop);
|
||||
|
||||
var interval = setInterval(function() {
|
||||
i = i + 1;
|
||||
|
||||
var top = self.elementTop(),
|
||||
scrollTop = $(window).scrollTop();
|
||||
|
||||
if ((top !== previousTop) || (scrollTop !== top)) {
|
||||
$(window).scrollTop(top);
|
||||
previousTop = top;
|
||||
}
|
||||
|
||||
// We commit suicide after 1s just to clean up
|
||||
var nowTime = new Date().getTime();
|
||||
if (nowTime - startedAt > 1000) {
|
||||
$('body,html').off(scrollEvents)
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
}, 50);
|
||||
|
||||
$('body,html').off(scrollEvents).on(scrollEvents, function(e){
|
||||
if ( e.which > 0 || e.type === "mousedown" || e.type === "mousewheel" || e.type === "touchmove") {
|
||||
$('body,html').off(scrollEvents);
|
||||
clearInterval(interval);
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
exports.LockOn = LockOn;
|
||||
|
||||
})(window);
|
Loading…
Reference in New Issue