You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nodebb/public/src/modules/navigator.js

587 lines
16 KiB
JavaScript

'use strict';
define('navigator', ['forum/pagination', 'components', 'hooks'], function (pagination, components, hooks) {
const navigator = {};
let index = 0;
let count = 0;
let navigatorUpdateTimeoutId;
let renderPostIntervalId;
let touchX;
let touchY;
let renderPostIndex;
let isNavigating = false;
let firstMove = true;
navigator.scrollActive = false;
let paginationBlockEl = $('.pagination-block');
let paginationTextEl = paginationBlockEl.find('.pagination-text');
let paginationBlockMeterEl = paginationBlockEl.find('meter');
let paginationBlockProgressEl = paginationBlockEl.find('.progress-bar');
let thumb;
let thumbText;
let thumbIcon;
let thumbIconHeight;
let thumbIconHalfHeight;
$(window).on('action:ajaxify.start', function () {
$(window).off('keydown', onKeyDown);
});
navigator.init = function (selector, count, toTop, toBottom, callback) {
index = 0;
navigator.selector = selector;
navigator.callback = callback;
navigator.toTop = toTop || function () {};
navigator.toBottom = toBottom || function () {};
paginationBlockEl = $('.pagination-block');
paginationTextEl = paginationBlockEl.find('.pagination-text');
paginationBlockMeterEl = paginationBlockEl.find('meter');
paginationBlockProgressEl = paginationBlockEl.find('.progress-bar');
thumbIcon = $('.scroller-thumb-icon');
thumbIconHeight = thumbIcon.height();
thumbIconHalfHeight = thumbIconHeight / 2;
thumb = $('.scroller-thumb');
thumbText = thumb.find('.thumb-text');
$(window).off('scroll', navigator.delayedUpdate).on('scroll', navigator.delayedUpdate);
paginationBlockEl.find('.dropdown-menu').off('click').on('click', function (e) {
e.stopPropagation();
});
paginationBlockEl.off('shown.bs.dropdown', '.dropdown').on('shown.bs.dropdown', '.dropdown', function () {
setTimeout(function () {
$('.pagination-block input').focus();
}, 100);
});
paginationBlockEl.find('.pageup').off('click').on('click', navigator.scrollUp);
paginationBlockEl.find('.pagedown').off('click').on('click', navigator.scrollDown);
paginationBlockEl.find('.pagetop').off('click').on('click', navigator.toTop);
paginationBlockEl.find('.pagebottom').off('click').on('click', navigator.toBottom);
paginationBlockEl.find('input').on('keydown', function (e) {
if (e.which === 13) {
const input = $(this);
if (!utils.isNumber(input.val())) {
input.val('');
return;
}
const index = parseInt(input.val(), 10);
const url = generateUrl(index);
input.val('');
$('.pagination-block .dropdown-toggle').trigger('click');
ajaxify.go(url);
}
});
if (ajaxify.data.template.topic) {
handleScrollNav();
}
handleKeys();
navigator.setCount(count);
navigator.update(0);
};
function clampTop(newTop) {
const parent = thumb.parent();
const parentOffset = parent.offset();
if (newTop < parentOffset.top) {
newTop = parentOffset.top;
} else if (newTop > parentOffset.top + parent.height() - thumbIconHeight) {
newTop = parentOffset.top + parent.height() - thumbIconHeight;
}
return newTop;
}
function setThumbToIndex(index) {
if (!thumb.length || thumb.is(':hidden')) {
return;
}
const parent = thumb.parent();
const parentOffset = parent.offset();
let percent = (index - 1) / ajaxify.data.postcount;
if (index === count) {
percent = 1;
}
const newTop = clampTop(parentOffset.top + ((parent.height() - thumbIconHeight) * percent));
const offset = { top: newTop, left: thumb.offset().left };
thumb.offset(offset);
thumbText.text(index + '/' + ajaxify.data.postcount);
renderPost(index);
}
function handleScrollNav() {
if (!thumb.length) {
return;
}
const parent = thumb.parent();
parent.on('click', function (ev) {
if ($(ev.target).hasClass('scroller-container')) {
const index = calculateIndexFromY(ev.pageY);
navigator.scrollToIndex(index - 1, true, 0);
return false;
}
});
function calculateIndexFromY(y) {
const newTop = clampTop(y - thumbIconHalfHeight);
const parentOffset = parent.offset();
const percent = (newTop - parentOffset.top) / (parent.height() - thumbIconHeight);
index = Math.max(1, Math.ceil(ajaxify.data.postcount * percent));
return index > ajaxify.data.postcount ? ajaxify.data.count : index;
}
let mouseDragging = false;
hooks.on('action:ajaxify.end', function () {
renderPostIndex = null;
});
$('.pagination-block .dropdown-menu').parent().on('shown.bs.dropdown', function () {
setThumbToIndex(index);
});
thumb.on('mousedown', function () {
mouseDragging = true;
$(window).on('mousemove', mousemove);
firstMove = true;
});
function mouseup() {
$(window).off('mousemove', mousemove);
if (mouseDragging) {
navigator.scrollToIndex(index - 1, true, 0);
paginationBlockEl.find('[data-toggle="dropdown"]').trigger('click');
}
clearRenderInterval();
mouseDragging = false;
firstMove = false;
}
function mousemove(ev) {
const newTop = clampTop(ev.pageY - thumbIconHalfHeight);
thumb.offset({ top: newTop, left: thumb.offset().left });
const index = calculateIndexFromY(ev.pageY);
navigator.updateTextAndProgressBar();
thumbText.text(index + '/' + ajaxify.data.postcount);
if (firstMove) {
delayedRenderPost();
}
firstMove = false;
ev.stopPropagation();
return false;
}
function delayedRenderPost() {
clearRenderInterval();
renderPostIntervalId = setInterval(function () {
renderPost(index);
}, 250);
}
$(window).off('mousemove', mousemove);
$(window).off('mouseup', mouseup).on('mouseup', mouseup);
thumb.on('touchstart', function (ev) {
isNavigating = true;
touchX = Math.min($(window).width(), Math.max(0, ev.touches[0].clientX));
touchY = Math.min($(window).height(), Math.max(0, ev.touches[0].clientY));
firstMove = true;
});
thumb.on('touchmove', function (ev) {
const windowWidth = $(window).width();
const windowHeight = $(window).height();
const deltaX = Math.abs(touchX - Math.min(windowWidth, Math.max(0, ev.touches[0].clientX)));
const deltaY = Math.abs(touchY - Math.min(windowHeight, Math.max(0, ev.touches[0].clientY)));
touchX = Math.min(windowWidth, Math.max(0, ev.touches[0].clientX));
touchY = Math.min(windowHeight, Math.max(0, ev.touches[0].clientY));
if (deltaY >= deltaX && firstMove) {
isNavigating = true;
delayedRenderPost();
}
if (isNavigating && ev.cancelable) {
ev.preventDefault();
ev.stopPropagation();
const newTop = clampTop(touchY + $(window).scrollTop() - thumbIconHalfHeight);
thumb.offset({ top: newTop, left: thumb.offset().left });
const index = calculateIndexFromY(touchY + $(window).scrollTop());
navigator.updateTextAndProgressBar();
thumbText.text(index + '/' + ajaxify.data.postcount);
if (firstMove) {
renderPost(index);
}
}
firstMove = false;
});
thumb.on('touchend', function () {
clearRenderInterval();
if (isNavigating) {
navigator.scrollToIndex(index - 1, true, 0);
isNavigating = false;
paginationBlockEl.find('[data-toggle="dropdown"]').trigger('click');
}
});
}
function clearRenderInterval() {
if (renderPostIntervalId) {
clearInterval(renderPostIntervalId);
renderPostIntervalId = 0;
}
}
function renderPost(index, callback) {
callback = callback || function () {};
if (renderPostIndex === index || paginationBlockEl.find('.post-content').is(':hidden')) {
return;
}
renderPostIndex = index;
socket.emit('posts.getPostSummaryByIndex', { tid: ajaxify.data.tid, index: index - 1 }, function (err, postData) {
if (err) {
return app.alertError(err.message);
}
app.parseAndTranslate('partials/topic/navigation-post', { post: postData }, function (html) {
paginationBlockEl
.find('.post-content')
.html(html)
.find('.timeago').timeago();
});
callback();
});
}
function handleKeys() {
if (!config.usePagination) {
$(window).off('keydown', onKeyDown).on('keydown', onKeyDown);
}
}
function onKeyDown(ev) {
if (ev.target.nodeName === 'BODY') {
if (ev.shiftKey || ev.ctrlKey || ev.altKey) {
return;
}
if (ev.which === 36 && navigator.toTop) { // home key
navigator.toTop();
return false;
} else if (ev.which === 35 && navigator.toBottom) { // end key
navigator.toBottom();
return false;
}
}
}
function generateUrl(index) {
const pathname = window.location.pathname.replace(config.relative_path, '');
const parts = pathname.split('/');
return parts[1] + '/' + parts[2] + '/' + parts[3] + (index ? '/' + index : '');
}
navigator.setCount = function (value) {
value = parseInt(value, 10);
if (value === count) {
return;
}
count = value;
navigator.updateTextAndProgressBar();
};
navigator.show = function () {
toggle(true);
};
navigator.disable = function () {
count = 0;
index = 1;
navigator.callback = null;
navigator.selector = null;
$(window).off('scroll', navigator.delayedUpdate);
toggle(false);
};
function toggle(flag) {
const path = ajaxify.removeRelativePath(window.location.pathname.slice(1));
if (flag && (!path.startsWith('topic') && !path.startsWith('category'))) {
return;
}
paginationBlockEl.toggleClass('ready', flag);
}
navigator.delayedUpdate = function () {
if (!navigatorUpdateTimeoutId) {
navigatorUpdateTimeoutId = setTimeout(function () {
navigator.update();
navigatorUpdateTimeoutId = undefined;
}, 100);
}
};
navigator.update = function (threshold) {
/*
The "threshold" is defined as the distance from the top of the page to
a spot where a user is expecting to begin reading.
*/
threshold = typeof threshold === 'number' ? threshold : undefined;
let newIndex = index;
const els = $(navigator.selector);
if (els.length) {
newIndex = parseInt(els.first().attr('data-index'), 10) + 1;
}
const scrollTop = $(window).scrollTop();
const windowHeight = $(window).height();
const documentHeight = $(document).height();
const middleOfViewport = scrollTop + (windowHeight / 2);
let previousDistance = Number.MAX_VALUE;
els.each(function () {
const $this = $(this);
const elIndex = parseInt($this.attr('data-index'), 10);
if (elIndex >= 0) {
const distanceToMiddle = Math.abs(middleOfViewport - ($this.offset().top + ($this.outerHeight(true) / 2)));
if (distanceToMiddle > previousDistance) {
return false;
}
if (distanceToMiddle < previousDistance) {
newIndex = elIndex + 1;
previousDistance = distanceToMiddle;
}
}
});
const atTop = scrollTop === 0 && parseInt(els.first().attr('data-index'), 10) === 0;
const nearBottom = scrollTop + windowHeight > documentHeight - 100 && parseInt(els.last().attr('data-index'), 10) === count - 1;
if (atTop) {
newIndex = 1;
} else if (nearBottom) {
newIndex = count;
}
// If a threshold is undefined, try to determine one based on new index
if (threshold === undefined && ajaxify.data.template.topic) {
if (atTop) {
threshold = 0;
} else {
const anchorEl = components.get('post/anchor', index - 1);
if (anchorEl.length) {
const anchorRect = anchorEl.get(0).getBoundingClientRect();
threshold = anchorRect.top;
}
}
}
if (typeof navigator.callback === 'function') {
navigator.callback(newIndex, count, threshold);
}
if (newIndex !== index) {
index = newIndex;
navigator.updateTextAndProgressBar();
setThumbToIndex(index);
}
toggle(!!count);
};
navigator.updateTextAndProgressBar = function () {
if (!utils.isNumber(index)) {
return;
}
index = index > count ? count : index;
paginationTextEl.translateHtml('[[global:pagination.out_of, ' + index + ', ' + count + ']]');
const fraction = (index - 1) / (count - 1 || 1);
paginationBlockMeterEl.val(fraction);
paginationBlockProgressEl.width((fraction * 100) + '%');
};
navigator.scrollUp = function () {
const $window = $(window);
if (config.usePagination) {
const atTop = $window.scrollTop() <= 0;
if (atTop) {
return pagination.previousPage(function () {
$('body,html').scrollTop($(document).height() - $window.height());
});
}
}
$('body,html').animate({
scrollTop: $window.scrollTop() - $window.height(),
});
};
navigator.scrollDown = function () {
const $window = $(window);
if (config.usePagination) {
const atBottom = $window.scrollTop() >= $(document).height() - $window.height();
if (atBottom) {
return pagination.nextPage();
}
}
$('body,html').animate({
scrollTop: $window.scrollTop() + $window.height(),
});
};
navigator.scrollTop = function (index) {
if ($(navigator.selector + '[data-index="' + index + '"]').length) {
navigator.scrollToIndex(index, true);
} else {
ajaxify.go(generateUrl());
}
};
navigator.scrollBottom = function (index) {
if (parseInt(index, 10) < 0) {
return;
}
if ($(navigator.selector + '[data-index="' + index + '"]').length) {
navigator.scrollToIndex(index, true);
} else {
index = parseInt(index, 10) + 1;
ajaxify.go(generateUrl(index));
}
};
navigator.scrollToIndex = function (index, highlight, duration) {
const inTopic = !!components.get('topic').length;
const inCategory = !!components.get('category').length;
if (!utils.isNumber(index) || (!inTopic && !inCategory)) {
return;
}
duration = duration !== undefined ? duration : 400;
navigator.scrollActive = true;
// if in topic and item already on page
if (inTopic && components.get('post/anchor', index).length) {
return navigator.scrollToPostIndex(index, highlight, duration);
}
// if in category and item alreay on page
if (inCategory && $('[component="category/topic"][data-index="' + index + '"]').length) {
return navigator.scrollToTopicIndex(index, highlight, duration);
}
if (!config.usePagination) {
navigator.scrollActive = false;
index = parseInt(index, 10) + 1;
ajaxify.go(generateUrl(index));
return;
}
const scrollMethod = inTopic ? navigator.scrollToPostIndex : navigator.scrollToTopicIndex;
const page = 1 + Math.floor(index / config.postsPerPage);
if (parseInt(page, 10) !== ajaxify.data.pagination.currentPage) {
pagination.loadPage(page, function () {
scrollMethod(index, highlight, duration);
});
} else {
scrollMethod(index, highlight, duration);
}
};
navigator.scrollToPostIndex = function (postIndex, highlight, duration) {
const scrollTo = components.get('post', 'index', postIndex);
navigator.scrollToElement(scrollTo, highlight, duration);
};
navigator.scrollToTopicIndex = function (topicIndex, highlight, duration) {
const scrollTo = $('[component="category/topic"][data-index="' + topicIndex + '"]');
navigator.scrollToElement(scrollTo, highlight, duration);
};
navigator.scrollToElement = function (scrollTo, highlight, duration) {
if (!scrollTo.length) {
navigator.scrollActive = false;
return;
}
const postHeight = scrollTo.outerHeight(true);
const navbarHeight = components.get('navbar').outerHeight(true);
const topicHeaderHeight = $('.topic-header').outerHeight(true) || 0;
const viewportHeight = $(window).height();
// Temporarily disable navigator update on scroll
$(window).off('scroll', navigator.delayedUpdate);
duration = duration !== undefined ? duration : 400;
navigator.scrollActive = true;
let done = false;
function animateScroll() {
function reenableScroll() {
// Re-enable onScroll behaviour
$(window).on('scroll', navigator.delayedUpdate);
const scrollToRect = scrollTo.get(0).getBoundingClientRect();
navigator.update(scrollToRect.top);
}
function onAnimateComplete() {
if (done) {
reenableScroll();
return;
}
done = true;
navigator.scrollActive = false;
highlightPost();
$('body').scrollTop($('body').scrollTop() - 1);
$('html').scrollTop($('html').scrollTop() - 1);
}
let scrollTop = 0;
if (postHeight < viewportHeight - navbarHeight - topicHeaderHeight) {
scrollTop = scrollTo.offset().top - (viewportHeight / 2) + (postHeight / 2);
} else {
scrollTop = scrollTo.offset().top - navbarHeight - topicHeaderHeight;
}
if (duration === 0) {
$(window).scrollTop(scrollTop);
onAnimateComplete();
reenableScroll();
return;
}
$('html, body').animate({
scrollTop: scrollTop + 'px',
}, duration, onAnimateComplete);
}
function highlightPost() {
if (highlight) {
$('[component="post"],[component="category/topic"]').removeClass('highlight');
scrollTo.addClass('highlight');
setTimeout(function () {
scrollTo.removeClass('highlight');
}, 10000);
}
}
animateScroll();
};
return navigator;
});