Files
NodeBB/public/src/client/topic.js

337 lines
9.8 KiB
JavaScript
Raw Normal View History

'use strict';
define('forum/topic', [
'forum/infinitescroll',
'forum/topic/threadTools',
'forum/topic/postTools',
'forum/topic/events',
'forum/topic/posts',
2015-01-08 13:47:15 -05:00
'navigator',
'sort',
2017-02-17 19:31:21 -07:00
'components',
2017-04-19 20:33:03 -06:00
'storage',
'hooks',
'api',
2021-12-06 12:45:35 -05:00
'alerts',
], function (
infinitescroll, threadTools, postTools,
2021-10-28 12:00:51 -04:00
events, posts, navigator, sort,
2021-12-06 12:45:35 -05:00
components, storage, hooks, api, alerts
) {
2021-10-28 12:00:51 -04:00
const Topic = {};
let tid = 0;
let currentUrl = '';
$(window).on('action:ajaxify.start', function (ev, data) {
events.removeListeners();
2015-10-28 15:06:19 -04:00
2017-07-14 17:29:31 -04:00
if (!String(data.url).startsWith('topic/')) {
2015-10-19 11:28:18 -04:00
navigator.disable();
2015-03-17 16:12:06 -04:00
components.get('navbar/title').find('span').text('').hide();
2021-12-06 14:31:35 -05:00
alerts.remove('bookmark');
2015-06-10 14:16:35 -04:00
}
});
Topic.init = function () {
const tidChanged = !tid || parseInt(tid, 10) !== parseInt(ajaxify.data.tid, 10);
tid = ajaxify.data.tid;
currentUrl = ajaxify.currentPage;
hooks.fire('action:topic.loading');
app.enterRoom('topic_' + tid);
if (tidChanged) {
posts.signaturesShown = {};
}
posts.onTopicPageLoad(components.get('post'));
2021-10-28 12:00:51 -04:00
navigator.init('[component="post"]', ajaxify.data.postcount, Topic.toTop, Topic.toBottom, utils.debounce(Topic.navigatorCallback, 500));
postTools.init(tid);
threadTools.init(tid, $('.topic'));
events.init();
sort.handleSort('topicPostSort', 'topic/' + ajaxify.data.slug);
2016-09-15 19:16:52 +03:00
if (!config.usePagination) {
infinitescroll.init($('[component="topic"]'), posts.loadMorePosts);
}
addBlockQuoteHandler();
addParentHandler();
2018-10-11 14:26:53 -04:00
addDropupHandler();
2018-11-17 20:50:07 -05:00
addRepliesHandler();
addPostsPreviewHandler();
handleBookmark(tid);
2021-10-28 12:00:51 -04:00
$(window).on('scroll', utils.debounce(updateTopicTitle, 250));
handleTopicSearch();
2015-09-25 18:21:25 -04:00
hooks.fire('action:topic.loaded', ajaxify.data);
};
function handleTopicSearch() {
if (config.topicSearchEnabled) {
require(['mousetrap', 'search'], function (mousetrap, search) {
2020-05-06 12:55:54 -04:00
mousetrap.bind(['command+f', 'ctrl+f'], function (e) {
if (ajaxify.data.template.topic) {
e.preventDefault();
$('#search-fields input').val('in:topic-' + ajaxify.data.tid + ' ');
search.showAndFocusInput();
}
});
});
}
}
Topic.toTop = function () {
2015-09-25 19:01:15 -04:00
navigator.scrollTop(0);
};
Topic.toBottom = function () {
socket.emit('topics.postcount', ajaxify.data.tid, function (err, postCount) {
2016-08-16 19:46:59 +02:00
if (err) {
2021-12-06 14:31:35 -05:00
return alerts.error(err);
2016-08-16 19:46:59 +02:00
}
2015-02-25 17:59:59 -05:00
navigator.scrollBottom(postCount - 1);
});
};
function handleBookmark(tid) {
2020-06-05 18:24:11 -04:00
if (window.location.hash) {
const el = $(utils.escapeHTML(window.location.hash));
2020-06-05 18:24:11 -04:00
if (el.length) {
return navigator.scrollToElement(el, true, 0);
}
}
const bookmark = ajaxify.data.bookmark || storage.getItem('topic:' + tid + ':bookmark');
const postIndex = ajaxify.data.postIndex;
2020-06-05 18:24:11 -04:00
if (postIndex > 1) {
2017-06-13 20:15:48 -04:00
if (components.get('post/anchor', postIndex - 1).length) {
return navigator.scrollToPostIndex(postIndex - 1, true, 0);
}
2021-02-04 02:07:29 -07:00
} else if (bookmark && (
!config.usePagination ||
(config.usePagination && ajaxify.data.pagination.currentPage === 1)
) && ajaxify.data.postcount > ajaxify.data.bookmarkThreshold) {
2021-12-06 12:45:35 -05:00
alerts.alert({
alert_id: 'bookmark',
message: '[[topic:bookmark_instructions]]',
timeout: 0,
type: 'info',
2017-02-18 01:27:46 -07:00
clickfn: function () {
navigator.scrollToIndex(parseInt(bookmark, 10), true);
},
2017-02-18 01:27:46 -07:00
closefn: function () {
2017-04-19 20:33:03 -06:00
storage.removeItem('topic:' + tid + ':bookmark');
2017-02-17 19:31:21 -07:00
},
});
setTimeout(function () {
2021-12-06 12:45:35 -05:00
alerts.remove('bookmark');
}, 10000);
}
}
function addBlockQuoteHandler() {
components.get('topic').on('click', 'blockquote .toggle', function () {
const blockQuote = $(this).parent('blockquote');
const toggle = $(this);
blockQuote.toggleClass('uncollapsed');
const collapsed = !blockQuote.hasClass('uncollapsed');
toggle.toggleClass('fa-angle-down', collapsed).toggleClass('fa-angle-up', !collapsed);
});
}
function addParentHandler() {
components.get('topic').on('click', '[component="post/parent"]', function (e) {
const toPid = $(this).attr('data-topid');
2015-09-29 15:46:11 -04:00
const toPost = $('[component="topic"]>[component="post"][data-pid="' + toPid + '"]');
2015-09-29 15:46:11 -04:00
if (toPost.length) {
2016-07-14 16:28:11 -05:00
e.preventDefault();
2017-04-21 13:48:43 -04:00
navigator.scrollToIndex(toPost.attr('data-index'), true);
return false;
2015-09-29 14:35:28 -04:00
}
});
}
Webpack5 (#10311) * feat: webpack 5 part 1 * fix: gruntfile fixes * fix: fix taskbar warning add app.importScript copy public/src/modules to build folder * refactor: remove commented old code * feat: reenable admin * fix: acp settings pages, fix sortable on manage categories embedded require in html not allowed * fix: bundle serialize/deserizeli so plugins dont break * test: fixe util tests * test: fix require path * test: more test fixes * test: require correct utils module * test: require correct utils * test: log stack * test: fix db require blowing up tests * test: move and disable bundle test * refactor: add aliases * test: disable testing route * fix: move webpack modules necessary for build, into `dependencies` * test: fix one more test remove 500-embed.tpl * fix: restore use of assets/nodebb.min.js, at least for now * fix: remove unnecessary line break * fix: point to proper ACP bundle * test: maybe fix build test * test: composer * refactor: dont need dist * refactor: more cleanup use everything from build/public folder * get rid of conditional import in app.js * fix: ace * refactor: cropper alias * test: lint and test fixes * lint: fix * refactor: rename function to app.require * refactor: go back to using app.require * chore: use github branch * chore: use webpack branch * feat: webpack webinstaller * feat: add chunkFile name with contenthash * refactor: move hooks to top * refactor: get rid of template500Function * fix(deps): use webpack5 branch of 2factor plugin * chore: tagging v2.0.0-beta.0 pre-release version :boom: :shipit: :tada: :rocket: * refactor: disable cache on templates loadTemplate is called once by benchpress and the result is cache internally * refactor: add server side helpers.js * feat: deprecate /plugins shorthand route, closes #10343 * refactor: use build/public for webpack * test: fix filename * fix: more specific selector * lint: ignore * refactor: fix comments * test: add debug for random failing test * refactor: cleanup remove test page, remove dupe functions in utils.common * lint: use relative path for now * chore: bump prerelease version * feat: add translateKeys * fix: optional params * fix: get rid of extra timeago files * refactor: cleanup, require timeago locale earlier remove translator.prepareDOM, it is in header.tpl html tag * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378) * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels - Existing hooks are preserved (to be deprecated at a later date, possibly) - New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks * docs: fix typo in comment * test: spec changes * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378) * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels - Existing hooks are preserved (to be deprecated at a later date, possibly) - New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks * docs: fix typo in comment * test: spec changes * feat: allow app.require('bootbox'/'benchpressjs') * refactor: require server side utils * test: jquery ready * change istaller to use build/public * test: use document.addEventListener * refactor: closes #10301 * refactor: generateTopicClass * fix: column counts for other privileges * fix: #10443, regression where sorted-list items did not render into the DOM in the predicted order [breaking] * fix: typo in hook name * refactor: introduce a generic autocomplete.init() method that can be called to add nodebb-style autocompletion but using different data sources (e.g. not user/groups/tags) * fix: crash if `delay` not passed in (as it cannot be destructured) * refactor: replace substr * feat: set --panel-offset style in html element based on stored value in localStorage * refactor: addDropupHandler() logic to be less naive - Take into account height of the menu - Don't apply dropUp logic if there's nothing in the dropdown - Remove 'hidden' class (added by default in Persona for post tools) when menu items are added closes #10423 * refactor: simplify utils.params [breaking] Retrospective analysis of the usage of this method suggests that the options passed in are superfluous, and that only `url` is required. Using a browser built-in makes more sense to accomplish what this method sets out to do. * feat: add support for returning full URLSearchParams for utils.params * fix: utils.params() fallback handling * fix: default empty obj for params() * fix: remove \'loggedin\' and \'register\' qs parameters once they have been used, delay invocation of messages until ajaxify.end * fix: utils.params() not allowing relative paths to be passed in * refactor(DRY): new assertPasswordValidity utils method * fix: incorrect error message returned on insufficient privilege on flag edit * fix: read/update/delete access to flags API should be limited for moderators to only post flags in categories they moderate - added failing tests and patched up middleware.assert.flags to fix * refactor: flag api v3 tests to create new post and flags on every round * fix: missing error:no-flag language key * refactor: flags.canView to check flag existence, simplify middleware.assert.flag * feat: flag deletion API endpoint, #10426 * feat: UI for flag deletion, closes #10426 * chore: update plugin versions * chore: up emoji * chore: update markdown * chore: up emoji-android * fix: regression caused by utils.params() refactor, supports arrays and pipes all values through utils.toType, adjusts tests to type check Co-authored-by: Julian Lam <julian@nodebb.org>
2022-04-29 21:39:33 -04:00
Topic.applyDropup = function () {
const containerRect = this.getBoundingClientRect();
const dropdownEl = this.querySelector('.dropdown-menu');
const dropdownStyle = window.getComputedStyle(dropdownEl);
const dropdownHeight = dropdownStyle.getPropertyValue('height').slice(0, -2);
const offset = document.documentElement.style.getPropertyValue('--panel-offset').slice(0, -2);
// Toggler position (including its height, since the menu spawns above it),
// minus the dropdown's height and navbar offset
const dropUp = (containerRect.top + containerRect.height - dropdownHeight - offset) > 0;
this.classList.toggle('dropup', dropUp);
};
2018-10-11 14:26:53 -04:00
function addDropupHandler() {
// Locate all dropdowns
const target = $('#content .dropdown-menu').parent();
Webpack5 (#10311) * feat: webpack 5 part 1 * fix: gruntfile fixes * fix: fix taskbar warning add app.importScript copy public/src/modules to build folder * refactor: remove commented old code * feat: reenable admin * fix: acp settings pages, fix sortable on manage categories embedded require in html not allowed * fix: bundle serialize/deserizeli so plugins dont break * test: fixe util tests * test: fix require path * test: more test fixes * test: require correct utils module * test: require correct utils * test: log stack * test: fix db require blowing up tests * test: move and disable bundle test * refactor: add aliases * test: disable testing route * fix: move webpack modules necessary for build, into `dependencies` * test: fix one more test remove 500-embed.tpl * fix: restore use of assets/nodebb.min.js, at least for now * fix: remove unnecessary line break * fix: point to proper ACP bundle * test: maybe fix build test * test: composer * refactor: dont need dist * refactor: more cleanup use everything from build/public folder * get rid of conditional import in app.js * fix: ace * refactor: cropper alias * test: lint and test fixes * lint: fix * refactor: rename function to app.require * refactor: go back to using app.require * chore: use github branch * chore: use webpack branch * feat: webpack webinstaller * feat: add chunkFile name with contenthash * refactor: move hooks to top * refactor: get rid of template500Function * fix(deps): use webpack5 branch of 2factor plugin * chore: tagging v2.0.0-beta.0 pre-release version :boom: :shipit: :tada: :rocket: * refactor: disable cache on templates loadTemplate is called once by benchpress and the result is cache internally * refactor: add server side helpers.js * feat: deprecate /plugins shorthand route, closes #10343 * refactor: use build/public for webpack * test: fix filename * fix: more specific selector * lint: ignore * refactor: fix comments * test: add debug for random failing test * refactor: cleanup remove test page, remove dupe functions in utils.common * lint: use relative path for now * chore: bump prerelease version * feat: add translateKeys * fix: optional params * fix: get rid of extra timeago files * refactor: cleanup, require timeago locale earlier remove translator.prepareDOM, it is in header.tpl html tag * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378) * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels - Existing hooks are preserved (to be deprecated at a later date, possibly) - New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks * docs: fix typo in comment * test: spec changes * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378) * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels - Existing hooks are preserved (to be deprecated at a later date, possibly) - New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks * docs: fix typo in comment * test: spec changes * feat: allow app.require('bootbox'/'benchpressjs') * refactor: require server side utils * test: jquery ready * change istaller to use build/public * test: use document.addEventListener * refactor: closes #10301 * refactor: generateTopicClass * fix: column counts for other privileges * fix: #10443, regression where sorted-list items did not render into the DOM in the predicted order [breaking] * fix: typo in hook name * refactor: introduce a generic autocomplete.init() method that can be called to add nodebb-style autocompletion but using different data sources (e.g. not user/groups/tags) * fix: crash if `delay` not passed in (as it cannot be destructured) * refactor: replace substr * feat: set --panel-offset style in html element based on stored value in localStorage * refactor: addDropupHandler() logic to be less naive - Take into account height of the menu - Don't apply dropUp logic if there's nothing in the dropdown - Remove 'hidden' class (added by default in Persona for post tools) when menu items are added closes #10423 * refactor: simplify utils.params [breaking] Retrospective analysis of the usage of this method suggests that the options passed in are superfluous, and that only `url` is required. Using a browser built-in makes more sense to accomplish what this method sets out to do. * feat: add support for returning full URLSearchParams for utils.params * fix: utils.params() fallback handling * fix: default empty obj for params() * fix: remove \'loggedin\' and \'register\' qs parameters once they have been used, delay invocation of messages until ajaxify.end * fix: utils.params() not allowing relative paths to be passed in * refactor(DRY): new assertPasswordValidity utils method * fix: incorrect error message returned on insufficient privilege on flag edit * fix: read/update/delete access to flags API should be limited for moderators to only post flags in categories they moderate - added failing tests and patched up middleware.assert.flags to fix * refactor: flag api v3 tests to create new post and flags on every round * fix: missing error:no-flag language key * refactor: flags.canView to check flag existence, simplify middleware.assert.flag * feat: flag deletion API endpoint, #10426 * feat: UI for flag deletion, closes #10426 * chore: update plugin versions * chore: up emoji * chore: update markdown * chore: up emoji-android * fix: regression caused by utils.params() refactor, supports arrays and pipes all values through utils.toType, adjusts tests to type check Co-authored-by: Julian Lam <julian@nodebb.org>
2022-04-29 21:39:33 -04:00
$(target).on('shown.bs.dropdown', function () {
const dropdownEl = this.querySelector('.dropdown-menu');
if (dropdownEl.innerHTML) {
Topic.applyDropup.call(this);
}
2018-10-11 14:26:53 -04:00
});
}
2018-11-17 20:50:07 -05:00
function addRepliesHandler() {
$('[component="topic"]').on('click', '[component="post/reply-count"]', function () {
const btn = $(this);
2018-11-17 20:50:07 -05:00
require(['forum/topic/replies'], function (replies) {
replies.init(btn);
});
});
}
function addPostsPreviewHandler() {
2021-11-05 13:20:13 -04:00
if (!ajaxify.data.showPostPreviewsOnHover || utils.isMobile()) {
return;
}
let timeoutId = 0;
2021-11-01 18:22:39 -04:00
const postCache = {};
2021-11-03 10:52:03 -04:00
$(window).one('action:ajaxify.start', function () {
clearTimeout(timeoutId);
$('#post-tooltip').remove();
});
$('[component="topic"]').on('mouseenter', '[component="post"] a, [component="topic/event"] a', async function () {
const link = $(this);
async function renderPost(pid) {
2021-11-01 18:22:39 -04:00
const postData = postCache[pid] || await socket.emit('posts.getPostSummaryByPid', { pid: pid });
2021-12-21 11:52:16 -05:00
$('#post-tooltip').remove();
if (postData && ajaxify.data.template.topic) {
2021-11-01 18:22:39 -04:00
postCache[pid] = postData;
const tooltip = await app.parseAndTranslate('partials/topic/post-preview', { post: postData });
tooltip.hide().find('.timeago').timeago();
tooltip.appendTo($('body')).fadeIn(300);
const postContent = link.parents('[component="topic"]').find('[component="post/content"]').first();
const postRect = postContent.offset();
const postWidth = postContent.width();
const linkRect = link.offset();
tooltip.css({
top: linkRect.top + 30,
left: postRect.left,
width: postWidth,
});
}
}
const href = link.attr('href');
const location = utils.urlToLocation(href);
const pathname = location.pathname;
const validHref = href && href !== '#' && window.location.hostname === location.hostname;
$('#post-tooltip').remove();
2021-11-01 15:30:36 -04:00
const postMatch = validHref && pathname && pathname.match(/\/post\/([\d]+)/);
const topicMatch = validHref && pathname && pathname.match(/\/topic\/([\d]+)/);
if (postMatch) {
const pid = postMatch[1];
if (parseInt(link.parents('[component="post"]').attr('data-pid'), 10) === parseInt(pid, 10)) {
return; // dont render self post
}
timeoutId = setTimeout(async () => {
renderPost(pid);
}, 300);
} else if (topicMatch) {
timeoutId = setTimeout(async () => {
const tid = topicMatch[1];
const topicData = await api.get('/topics/' + tid, {});
renderPost(topicData.mainPid);
}, 300);
}
}).on('mouseleave', '[component="post"] a, [component="topic/event"] a', function () {
clearTimeout(timeoutId);
$('#post-tooltip').remove();
});
}
function updateTopicTitle() {
const span = components.get('navbar/title').find('span');
2016-03-29 12:39:41 +03:00
if ($(window).scrollTop() > 50 && span.hasClass('hidden')) {
span.html(ajaxify.data.title).removeClass('hidden');
} else if ($(window).scrollTop() <= 50 && !span.hasClass('hidden')) {
span.html('').addClass('hidden');
}
if ($(window).scrollTop() > 300) {
2021-12-06 16:24:09 -05:00
alerts.remove('bookmark');
}
}
Topic.navigatorCallback = function (index, elementCount) {
2021-10-28 12:00:51 -04:00
if (!ajaxify.data.template.topic || navigator.scrollActive) {
2016-06-21 14:43:38 +03:00
return;
}
const newUrl = 'topic/' + ajaxify.data.slug + (index > 1 ? ('/' + index) : '');
2016-06-21 14:43:38 +03:00
if (newUrl !== currentUrl) {
currentUrl = newUrl;
2016-06-21 14:43:38 +03:00
2021-10-28 12:00:51 -04:00
if (index >= elementCount && app.user.uid) {
socket.emit('topics.markAsRead', [ajaxify.data.tid]);
}
2016-06-21 14:43:38 +03:00
2021-10-28 12:00:51 -04:00
updateUserBookmark(index);
2016-09-15 19:16:52 +03:00
2021-10-28 12:00:51 -04:00
Topic.replaceURLTimeout = 0;
if (ajaxify.data.updateUrlWithPostIndex && history.replaceState) {
let search = window.location.search || '';
if (!config.usePagination) {
search = (search && !/^\?page=\d+$/.test(search) ? search : '');
2016-06-21 14:43:38 +03:00
}
2021-10-28 12:00:51 -04:00
history.replaceState({
url: newUrl + search,
}, null, window.location.protocol + '//' + window.location.host + config.relative_path + '/' + newUrl + search);
}
}
};
function updateUserBookmark(index) {
const bookmarkKey = 'topic:' + ajaxify.data.tid + ':bookmark';
const currentBookmark = ajaxify.data.bookmark || storage.getItem(bookmarkKey);
if (config.topicPostSort === 'newest_to_oldest') {
index = Math.max(1, ajaxify.data.postcount - index + 2);
}
2021-02-04 02:07:29 -07:00
if (
ajaxify.data.postcount > ajaxify.data.bookmarkThreshold &&
(
!currentBookmark ||
parseInt(index, 10) > parseInt(currentBookmark, 10) ||
ajaxify.data.postcount < parseInt(currentBookmark, 10)
)
) {
if (app.user.uid) {
socket.emit('topics.bookmark', {
2017-02-18 01:19:20 -07:00
tid: ajaxify.data.tid,
index: index,
}, function (err) {
if (err) {
2021-12-06 14:31:35 -05:00
return alerts.error(err);
}
ajaxify.data.bookmark = index + 1;
});
} else {
2017-04-19 20:33:03 -06:00
storage.setItem(bookmarkKey, index);
}
}
// removes the bookmark alert when we get to / past the bookmark
if (!currentBookmark || parseInt(index, 10) >= parseInt(currentBookmark, 10)) {
2021-12-06 14:31:35 -05:00
alerts.remove('bookmark');
}
}
return Topic;
});