Fixing viewport shuffling due to image load

Introduced new method ".loadImages()" in posts client side lib
to handle viewport height changes when loading images. Requires
nodebb-plugin-markdown@5.0.0

@BenLubar @boomzillawtf
This commit is contained in:
Julian Lam
2016-03-27 15:52:26 -04:00
parent 5d4f61ec96
commit 3e2231d2cb
7 changed files with 126 additions and 13 deletions

View File

@@ -47,7 +47,7 @@
"nodebb-plugin-composer-default": "3.0.18",
"nodebb-plugin-dbsearch": "1.0.1",
"nodebb-plugin-emoji-extended": "1.0.3",
"nodebb-plugin-markdown": "4.0.17",
"nodebb-plugin-markdown": "5.0.0",
"nodebb-plugin-mentions": "1.0.21",
"nodebb-plugin-soundpack-default": "0.1.6",
"nodebb-plugin-spam-be-gone": "0.4.6",

21
public/less/global.less Normal file
View File

@@ -0,0 +1,21 @@
/*
This stylesheet is applied to all themes and all pages.
They can be overridden by themes, though their presence (or initial settings) may be depended upon by
client-side logic in core.
==========
*/
/* Prevent viewport shuffling on image load by restricting image dimensions until viewed by the browser */
[component="post/content"] img:not(.not-responsive) {
height: 1rem;
opacity: 0;
transition: width 500ms ease;
transition: height 500ms ease;
transition: opacity 500ms ease;
&[data-state="loaded"] {
height: auto;
opacity: 1;
}
}

View File

@@ -61,12 +61,12 @@ define('forum/topic', [
addParentHandler();
handleBookmark(tid);
handleKeys();
navigator.init('[component="post/anchor"]', ajaxify.data.postcount, Topic.toTop, Topic.toBottom, Topic.navigatorCallback, Topic.calculateIndex);
handleBookmark(tid);
$(window).on('scroll', updateTopicTitle);
handleTopicSearch();
@@ -141,6 +141,7 @@ define('forum/topic', [
return navigator.scrollToPostIndex(postIndex, true);
}
} else if (bookmark && (!config.usePagination || (config.usePagination && ajaxify.data.pagination.currentPage === 1)) && ajaxify.data.postcount > 5) {
navigator.update();
app.alert({
alert_id: 'bookmark',
message: '[[topic:bookmark_instructions]]',
@@ -156,6 +157,8 @@ define('forum/topic', [
setTimeout(function() {
app.removeAlert('bookmark');
}, 10000);
} else {
navigator.update();
}
}
@@ -233,7 +236,7 @@ define('forum/topic', [
return index;
};
Topic.navigatorCallback = function(index, elementCount) {
Topic.navigatorCallback = function(index, elementCount, threshold) {
var path = ajaxify.removeRelativePath(window.location.pathname.slice(1));
if (!path.startsWith('topic')) {
return 1;
@@ -248,13 +251,13 @@ define('forum/topic', [
newUrl += '/' + index;
}
posts.loadImages(threshold);
if (newUrl !== currentUrl) {
if (Topic.replaceURLTimeout) {
clearTimeout(Topic.replaceURLTimeout);
}
Topic.replaceURLTimeout = setTimeout(function() {
updateUserBookmark(index);
Topic.replaceURLTimeout = 0;

View File

@@ -231,6 +231,66 @@ define('forum/topic/posts', [
hidePostToolsForDeletedPosts(posts);
};
Posts.loadImages = function(threshold) {
/*
If threshold is defined, images loaded above this threshold will modify
the user's scroll position so they are not scrolled away from content
they were reading. Images loaded below this threshold will push down content.
If no threshold is defined, loaded images will push down content, as per
default
*/
var images = components.get('post/content').find('img[data-state="unloaded"]'),
visible = images.filter(function() {
return utils.isElementInViewport(this);
}),
scrollTop = $(window).scrollTop(),
adjusting = false,
adjustQueue = [],
adjustPosition = function() {
adjusting = true;
oldHeight = document.body.clientHeight;
// Display the image
$(this).attr('data-state', 'loaded');
newHeight = document.body.clientHeight;
var imageRect = this.getBoundingClientRect();
if (imageRect.top < threshold) {
scrollTop = scrollTop + (newHeight - oldHeight);
$(window).scrollTop(scrollTop);
}
if (adjustQueue.length) {
adjustQueue.pop()();
} else {
adjusting = false;
}
},
oldHeight, newHeight;
// For each image, reset the source and adjust scrollTop when loaded
visible.attr('data-state', 'loading');
visible.each(function(index, image) {
image = $(image);
image.on('load', function() {
if (!adjusting) {
adjustPosition.call(this);
} else {
adjustQueue.push(adjustPosition.bind(this));
}
});
image.attr('src', image.attr('data-src'));
if (image.parent().attr('href')) {
image.parent().attr('href', image.attr('data-src'));
}
image.removeAttr('data-src');
});
};
Posts.wrapImagesInLinks = function(posts) {
posts.find('[component="post/content"] img:not(.emoji)').each(function() {
var $this = $(this);

View File

@@ -1,7 +1,7 @@
'use strict';
/* globals app, define, ajaxify, utils, config */
/* globals define, ajaxify, utils, config */
define('navigator', ['forum/pagination', 'components'], function(pagination, components) {
@@ -56,7 +56,6 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com
});
navigator.setCount(count);
navigator.update();
};
function generateUrl(index) {
@@ -92,7 +91,13 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com
$('.pagination-block').toggleClass('ready', flag);
}
navigator.update = function() {
navigator.update = function(threshold) {
threshold = typeof threshold === 'number' ? threshold : undefined;
/*
The "threshold" is defined as the distance from the top of the page to
a spot where a user is expecting to begin reading.
*/
var els = $(navigator.selector);
if (els.length) {
index = parseInt(els.first().attr('data-index'), 10) + 1;
@@ -114,7 +119,7 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com
});
if (typeof navigator.callback === 'function') {
navigator.callback(index, count);
navigator.callback(index, count, threshold);
}
navigator.updateTextAndProgressBar();
@@ -202,6 +207,9 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com
return;
}
// Temporarily disable navigator update on scroll
$(window).off('scroll', navigator.update);
duration = duration !== undefined ? duration : 400;
navigator.scrollActive = true;
var done = false;
@@ -209,21 +217,24 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com
function animateScroll() {
var scrollTop = 0;
if (postHeight < viewportHeight) {
scrollTop = (scrollTo.offset().top - (viewportHeight / 2) + (postHeight / 2)) + 'px';
scrollTop = (scrollTo.offset().top - (viewportHeight / 2) + (postHeight / 2));
} else {
scrollTop = scrollTo.offset().top - navbarHeight;
}
$('html, body').animate({
scrollTop: scrollTop
scrollTop: scrollTop + 'px'
}, duration, function() {
if (done) {
// Re-enable onScroll behaviour
$(window).on('scroll', navigator.update);
var scrollToRect = scrollTo.get(0).getBoundingClientRect();
navigator.update(scrollToRect.top);
return;
}
done = true;
navigator.scrollActive = false;
navigator.update();
highlightPost();
$('body').scrollTop($('body').scrollTop() - 1);
$('html').scrollTop($('html').scrollTop() - 1);

View File

@@ -309,6 +309,23 @@
return labels;
},
/* Retrieved from http://stackoverflow.com/a/7557433 @ 27 Mar 2016 */
isElementInViewport: function(el) {
//special bonus for those using jQuery
if (typeof jQuery === "function" && el instanceof jQuery) {
el = el[0];
}
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
);
},
// get all the url params in a single key/value hash
params: function(options) {
var a, hash = {}, params;

View File

@@ -72,6 +72,7 @@ module.exports = function(Meta) {
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/blacklist.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/global.less";';
var acpSource = '\n@import "..' + path.sep + 'public/less/admin/admin";\n' + source;
acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";';