refactor: navigator so it works with multiple thumbs/navigators

This commit is contained in:
Barış Soner Uşaklı
2023-02-10 23:48:26 -05:00
parent 65886c580f
commit fc260aeab3
2 changed files with 179 additions and 101 deletions

View File

@@ -12,18 +12,14 @@ define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], funct
let renderPostIndex;
let isNavigating = false;
let firstMove = true;
let bsEnv = '';
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;
let thumbs;
$(window).on('action:ajaxify.start', function () {
$(window).off('keydown', onKeyDown);
@@ -41,11 +37,8 @@ define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], funct
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');
thumbs = $('.scroller-thumb');
bsEnv = utils.findBootstrapEnvironment();
$(window).off('scroll', navigator.delayedUpdate).on('scroll', navigator.delayedUpdate);
@@ -54,9 +47,10 @@ define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], funct
});
paginationBlockEl.off('shown.bs.dropdown', '.wrapper').on('shown.bs.dropdown', '.wrapper', function () {
const el = $(this);
setTimeout(async function () {
if (utils.findBootstrapEnvironment() === 'lg') {
$('.pagination-block input').focus();
if (['lg', 'xl', 'xxl'].includes(utils.findBootstrapEnvironment())) {
el.find('input').trigger('focus');
}
const postCountInTopic = await socket.emit('topics.getPostCountInTopic', ajaxify.data.tid);
if (postCountInTopic > 0) {
@@ -131,9 +125,11 @@ define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], funct
}
}
function clampTop(newTop) {
function clampTop(thumb, newTop) {
const parent = thumb.parent();
const parentOffset = parent.offset();
const thumbIcon = thumb.find('.scroller-thumb-icon');
const thumbIconHeight = thumbIcon.height();
if (newTop < parentOffset.top) {
newTop = parentOffset.top;
} else if (newTop > parentOffset.top + parent.height() - thumbIconHeight) {
@@ -143,55 +139,128 @@ define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], funct
}
function setThumbToIndex(index) {
if (!thumb || !thumb.length || thumb.is(':hidden')) {
if (!thumbs || !thumbs.length || !thumbs.is(':visible')) {
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);
thumbs.each((i, el) => {
const thumb = $(el);
if (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 thumbIcon = thumb.find('.scroller-thumb-icon');
const thumbIconHeight = thumbIcon.height();
const newTop = clampTop(thumb, parentOffset.top + ((parent.height() - thumbIconHeight) * percent));
const offset = { top: newTop, left: thumb.offset().left };
thumb.offset(offset);
updateThumbTextToIndex(thumb, index);
updateThumbTimestampToIndex(thumb, index);
});
renderPost(index);
}
function updateThumbTextToIndex(thumb, index) {
if (bsEnv === 'xs' || bsEnv === 'sm' || bsEnv === 'md') {
thumb.find('.thumb-text').text(`${index}/${ajaxify.data.postcount}`);
} else {
thumb.find('.thumb-text').translateText(`[[topic:navigator.index, ${index}, ${ajaxify.data.postcount}]]`);
}
}
async function updateThumbTimestampToIndex(thumb, index) {
const el = thumb.find('.thumb-timestamp');
if (el.length) {
const timestamp = await getPostTimestampByIndex(index);
el.attr('title', utils.toISOString(timestamp)).timeago();
}
}
async function getPostTimestampByIndex(index) {
// load timestamp of post from DOM if it exists
// if not load from server
const postEl = $(`[component="post"][data-index=${index - 1}]`);
if (postEl.length) {
return parseInt(postEl.attr('data-timestamp'), 10);
}
return await socket.emit('posts.getPostTimestampByIndex', {
tid: ajaxify.data.tid,
index: index - 1,
});
}
function handleScrollNav() {
if (!thumb.length) {
if (!thumbs.length) {
return;
}
const parent = thumb.parent();
parent.on('click', function (ev) {
const parents = thumbs.parent();
parents.off('click').on('click', function (ev) {
if ($(ev.target).hasClass('scroller-container')) {
const index = calculateIndexFromY(ev.pageY);
const thumb = $(ev.target).find('.scroller-thumb');
const index = calculateIndexFromY(thumb, ev.pageY);
navigator.scrollToIndex(index - 1, true, 0);
return false;
}
});
function calculateIndexFromY(y) {
const newTop = clampTop(y - thumbIconHalfHeight);
function calculateIndexFromY(thumb, y) {
const parent = thumb.parent();
const thumbIcon = thumb.find('.scroller-thumb-icon');
const thumbIconHeight = thumbIcon.height();
const newTop = clampTop(thumb, y - (thumbIconHeight / 2));
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;
return index > ajaxify.data.postcount ? ajaxify.data.postcount : index;
}
let mouseDragging = false;
hooks.on('action:ajaxify.end', function () {
renderPostIndex = null;
});
$('.pagination-block .dropdown-menu').parent().on('shown.bs.dropdown', function () {
setThumbToIndex(index);
});
paginationBlockEl.find('.dropdown-menu').parent()
.off('shown.bs.dropdown')
.on('shown.bs.dropdown', function () {
setThumbToIndex(index);
});
thumb.on('mousedown', function () {
// the thumb that's being dragged, there can be more than on on the DOM
let dragThumb = null;
const debounceUpdateThumbTimestamp = utils.debounce(updateThumbTimestampToIndex, 50);
function mousemove(ev) {
if (!dragThumb || !dragThumb.length) {
return;
}
const thumbIcon = dragThumb.find('.scroller-thumb-icon');
const thumbIconHeight = thumbIcon.height();
const newTop = clampTop(dragThumb, ev.pageY - (thumbIconHeight / 2));
dragThumb.offset({ top: newTop, left: dragThumb.offset().left });
const index = calculateIndexFromY(dragThumb, ev.pageY);
navigator.updateTextAndProgressBar();
updateThumbTextToIndex(dragThumb, index);
debounceUpdateThumbTimestamp(dragThumb, index);
if (firstMove) {
delayedRenderPost();
}
firstMove = false;
ev.stopPropagation();
return false;
}
thumbs.off('mousedown').on('mousedown', function () {
mouseDragging = true;
dragThumb = $(this);
dragThumb.addClass('active');
$(window).on('mousemove', mousemove);
firstMove = true;
});
@@ -205,20 +274,10 @@ define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], funct
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();
if (dragThumb && dragThumb.length) {
dragThumb.removeClass('active');
}
firstMove = false;
ev.stopPropagation();
return false;
dragThumb = null;
}
function delayedRenderPost() {
@@ -231,48 +290,55 @@ define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], funct
$(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;
});
thumbs.each((i, el) => {
const thumb = $(el);
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) {
thumb.off('touchstart').on('touchstart', function (ev) {
isNavigating = true;
delayedRenderPost();
}
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;
});
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);
thumb.off('touchmove').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();
}
}
firstMove = false;
});
thumb.on('touchend', function () {
clearRenderInterval();
if (isNavigating) {
navigator.scrollToIndex(index - 1, true, 0);
isNavigating = false;
paginationBlockEl.find('.dropdown-menu.show').removeClass('show');
}
if (isNavigating && ev.cancelable) {
ev.preventDefault();
ev.stopPropagation();
const thumbIcon = thumb.find('.scroller-thumb-icon');
const thumbIconHeight = thumbIcon.height();
const newTop = clampTop(thumb, touchY + $(window).scrollTop() - (thumbIconHeight / 2));
thumb.offset({ top: newTop, left: thumb.offset().left });
const index = calculateIndexFromY(thumb, touchY + $(window).scrollTop());
navigator.updateTextAndProgressBar();
updateThumbTextToIndex(thumb, index);
debounceUpdateThumbTimestamp(thumb, index);
if (firstMove) {
renderPost(index);
}
}
firstMove = false;
});
thumb.off('touchend').on('touchend', function () {
clearRenderInterval();
if (isNavigating) {
navigator.scrollToIndex(index - 1, true, 0);
isNavigating = false;
paginationBlockEl.find('.dropdown-menu.show').removeClass('show');
}
});
});
}
@@ -283,26 +349,19 @@ define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], funct
}
}
function renderPost(index, callback) {
callback = callback || function () {};
if (renderPostIndex === index || paginationBlockEl.find('.post-content').is(':hidden')) {
async function renderPost(index) {
if (!index || renderPostIndex === index || !paginationBlockEl.find('.post-content').is(':visible')) {
return;
}
renderPostIndex = index;
socket.emit('posts.getPostSummaryByIndex', { tid: ajaxify.data.tid, index: index - 1 }, function (err, postData) {
if (err) {
return alerts.error(err);
}
app.parseAndTranslate('partials/topic/navigation-post', { post: postData }, function (html) {
paginationBlockEl
.find('.post-content')
.html(html)
.find('.timeago').timeago();
});
const postData = await socket.emit('posts.getPostSummaryByIndex', { tid: ajaxify.data.tid, index: index - 1 });
callback();
});
const html = await app.parseAndTranslate('partials/topic/navigation-post', { post: postData });
paginationBlockEl
.find('.post-content')
.html(html)
.find('.timeago').timeago();
}
function handleKeys() {

View File

@@ -58,6 +58,25 @@ SocketPosts.getPostSummaryByIndex = async function (socket, data) {
return postsData[0];
};
SocketPosts.getPostTimestampByIndex = async function (socket, data) {
if (data.index < 0) {
data.index = 0;
}
let pid;
if (data.index === 0) {
pid = await topics.getTopicField(data.tid, 'mainPid');
} else {
pid = await db.getSortedSetRange(`tid:${data.tid}:posts`, data.index - 1, data.index - 1);
}
pid = Array.isArray(pid) ? pid[0] : pid;
const topicPrivileges = await privileges.topics.get(data.tid, socket.uid);
if (!topicPrivileges['topics:read']) {
throw new Error('[[error:no-privileges]]');
}
return await posts.getPostField(pid, 'timestamp');
};
SocketPosts.getPostSummaryByPid = async function (socket, data) {
if (!data || !data.pid) {
throw new Error('[[error:invalid-data]]');