Files
NodeBB/public/src/client/topic/postTools.js
Barış Soner Uşaklı 7ba70d1561 Bootstrap5 (#10894)
* chore: up deps

* chore: up composer

* fix(deps): bump 2factor to v7

* chore: up harmony

* chore: up harmony

* fix: missing await

* feat: allow middlewares to pass in template values via res.locals

* feat: buildAccountData middleware automatically added ot all account routes

* fix: properly allow values in res.locals.templateValues to be added to the template data

* refactor: user/blocks

* refactor(accounts): categories and consent

* feat: automatically 404 if exposeUid or exposeGroupName come up empty

* refactor: remove calls to getUserDataByUserSlug for most account routes, since it is populated via middleware now

* fix: allow exposeUid and exposeGroupName to work with slugs with mixed capitalization

* fix: move reputation removal check to accountHelpers method

* test: skip i18n tests if ref branch when present is not develop

* fix(deps): bump theme versions

* fix(deps): bump ntfy and 2factor

* chore: up harmony

* fix: add missing return

* fix: #11191, only focus on search input on md environments and up

* feat: allow file uploads on mobile chat

closes https://github.com/NodeBB/NodeBB/issues/11217

* chore: up themes

* chore: add lang string

* fix(deps): bump ntfy to 1.0.15

* refactor: use new if/each syntax

* chore: up composer

* fix: regression from user helper refactor

* chore: up harmony

* chore: up composer

* chore: up harmony

* chore: up harmony

* chore: up harmony

* chore: fix composer version

* feat: add increment helper

* chore: up harmony

* fix: #11228 no timestamps in future 

* chore: up harmony

* check config.theme as well

fire action:posts.loaded after processing dom

* chore: up harmony

* chore: up harmony

* chore: up harmony

* chore: up themes

* chore: up harmony

* remove extra class

* refactor: move these to core from harmony

* chore: up widgets

* chore: up widgets

* height auto

* fix: closes #11238

* dont focus inputs, annoying on mobile

* fix: dont focus twice, only focus on chat input on desktop

dont wrap widget footer in row

* chore: up harmony

* chore: up harmony

* update chat window

* chore: up themes

* fix cache buster for skins

* chat fixes

* chore: up harmony

* chore: up composer

* refactor: change hook logs to debug

* fix: scroll to post right after adding to dom

* fix: hash scrolling and highlighting correct post

* test: re-enable read API schema tests

* fix: add back schema changes for 179faa2270 and c3920ccb10

* fix: schema changes from 488f0978a4

* fix: schema changes for f4cf482a87

* fix: schema update for be6bbabd0e

* fix: schema changes for 69c96078ea

* fix: schema changes for d1364c3130

* fix: schema changes for 84ff1152f7

* fix: schema changes for b860c2605c

* fix: schema changes for 23cb67a112

* fix: schema changes for b916e42f40

* fix: schema change for a9bbb586fc

* fix: schema changes for 4b738c8cd3

* fix: schema changes for 58b5781cea

* fix: schema changes for 794bf01b21

* fix: schema changes for 80ea12c1c1, e368feef51, and 52ead114be

* fix: composer-default object in config?

* fix: schema changes for 9acdc6808c and 0930934200

* fix: schema changes for c0a52924f1

* fix: schema change for aba420a3f3, move loggedInUser to optional props

* fix: schema changes for 8c67031609

* fix: schema changes for 27e53b42f3

* fix: schema changes for 2835966518

* fix: breaking test for email confirmation API call

* fix: schema changes for refactored search page

* fix: schema changes for user object

* fix: schema changes for 9f531f957e

* fix: schema changes for c4042c70de and 23175110a2

* fix: schema changes for 9b3616b103

* fix: schema changes for 5afd5de07d

* fix: schema change for 1d7baf1217

* fix: schema changes for 57bfb37c55 and be6bbabd0e

* fix: schema changes for 6e86b4afa2 and 3efad2e13b and 68f66223e7

* fix: allowing optional qs prop in pagination keys (not sure why this didn't break before)

* fix: re-login on email change

* fix: schema changes for c926358d73

* fix: schema changes for 388a8270c9

* fix: schema change for 2658bcc821

* fix: no need to call account middlewares for chats routes

* fix: schema changes for 71743affc3

* fix: final schema changes

* test: support for anyOf and oneOf

* fix: check thumb

* dont scroll to top on back press

* remove group log

* fix: add top margin to merged and deleted alerts

* chore: up widgets

* fix: improve fix-lists mixin

* chore: up harmony/composer

* feat: allow hiding quicksearch results during search

* dont record searches made by composer

* chore: up 54

* chore: up spam be gone

* feat: add prev/next page and page count into mobile paginator

* chore: up harmony

* chore: up harmony

* use old style for IS

* fix: hide entire toolbar row if no posts or not singlePost

* fix: updated messaging for post-queue template, #11206

* fix: btn-sm on post queue back button

* fix: bump harmony, closes #11206

* fix: remove unused alert module import

* fix: bump harmony

* fix: bump harmony

* chore: up harmony

* refactor: IS scrolltop

* fix: update users:search-user-for-chat source string

* feat: support for mark-read toggle on chats dropdown and recent chats list

* feat: api v3 calls to mark chat read/unread

* feat: send event:chats.mark socket event on mark read or unread

* refactor: allow frontend to mark chats as unread, use new API v3 routes instead of socket calls, better frontend event handling

* docs: openapi schema updates for chat marking

* fix: allow unread state toggling in chats dropdown too

* fix: issue where repeated openings of the chats dropdown would continually add events for mark-read/unread

* fix: debug log

* refactor: move userSearch filter to a module

* feat(routes): allow remounting /categories (#11230)

* feat: send flags count to frontend on flags list page

* refactor: filter form client-side js to extract out some logic

* fix: applyFilters to not take any arguments, update selectedCids in updateButton instead of onHidden

* fix: use userFilter module for assignee, reporterId, targetUid

* fix(openapi): schema changes for updated flags page

* fix: dont allow adding duplicates to userFilter

* use same var

* remove log

* fix: closes #11282

* feat: lang key for x-topics

* chore: up harmony

* chore: up emoji

* chore: up harmony

* fix: update userFilter to allow new option `selectedBlock`

* fix: wrong block name passed to userFilter

* fix: https://github.com/NodeBB/NodeBB/issues/11283

* fix: chats, allow multiple dropdowns like in harmony

* chore: up harmony

* refactor: flag note adding/editing, closes #11285

* fix: remove old prepareEdit logic

* chore: add caveat about hacky code block in userFilter module

* fix: placeholders for userFilter module

* refactor: navigator so it works with multiple thumbs/navigators

* chore: up harmony

* fix: closes #11287, destroy quick reply autocomplete

on navigation

* fix: filter disabled categories on user categories page count

* chore: up harmony

* docs: update openapi spec to include info about passing in timestamps for topic creation, removing timestamp as valid request param for topic replying

* fix: send back null values on ACP search dashboard for startDate and endDate if not expicitly passed in, fix tests

* fix: tweak table order in ACP dash searches

* fix: only invoke navigator click drag on left mouse button

* feat: add back unread indicator to navigator

* clear bookmark on mark unread

* fix: navigator crash on ajaxify

* better thumb top calculation

* fix: reset user bookmark when topic is marked unread

* Revert "fix: reset user bookmark when topic is marked unread"

This reverts commit 9bcd85c2c6.

* fix: update unread indicator on scroll, add unread count

* chore: bump harmony

* fix: crash on navigator unread update when backing out of a topic

* fix: closes #11183

* fix: update topics:recent zset when rescheduling a topic

* fix: dupe quote button, increase delay, hide immediately on empty selection

* fix: navigator not showing up on first load

* refactor: remove glance

assorted fixes to navigator
dont reduce remaning count if user scrolls down and up quickly
only call topic.navigatorCallback when index changes

* more sanity checks for bookmark

dont allow setting bookmark higher than topic postcount

* closes #11218, 🚋

* Revert "fix: update topics:recent zset when rescheduling a topic"

This reverts commit 737973cca9.

* fix: #11306, show proper error if queued post doesn't exist

was showing no-privileges if someone else accepted the post

* https://github.com/NodeBB/NodeBB/issues/11307

dont use li

* chore: up harmony

* chore: bump version string

* fix: copy paste fail

* feat: closes #7382, tag filtering

add client side support for filtering by tags on /category, /recent and /unread

* chore: up harmony

* chore: up harmony

* Revert "fix: add back req.query fallback for backwards compatibility" [breaking]

This reverts commit cf6cc2c454.
This commit is no longer required as passing in a CSRF token via query parameter is no longer supported as of NodeBB v3.x

This is a breaking change.

* fix: pass csrf token in form data, re: NodeBB/NodeBB#11309

* chore: up deps

* fix: tests, use x-csrf-token query param removed

* test: fix csrf_token

* lint: remove unused

* feat: add itemprop="image" to avatar helper

* fix: get chat upload button in chat modal

* breaking: remove deprecated socket.io methods

* test: update messaging tests to not use sockets

* fix: parent post links

* fix: prevent post tooltip if mouse leaves before data/tpl is loaded

* chore: up harmony

* chore: up harmony

* chore: up harmony

* chore: up harmony

* fix: nested replies indices

* fix(deps): bump 2factor

* feat: add loggedIn user to all api routes

* chore: up themes

* refactor: audit admin v3 write api routes as per #11321

* refactor: audit category v3 write api routes as per #11321 [breaking]

docs: fix open api spec for #11321

* refactor: audit chat v3 write api routes as per #11321

* refactor: audit files v3 write api routes as per #11321

* refactor: audit flags v3 write api routes as per #11321

* refactor: audit posts v3 write api routes as per #11321

* refactor: audit topics v3 write api routes as per #11321

* refactor: audit users v3 write api routes as per #11321

* fix: lang string

* remove min height

* fix: empty topic/labels taking up space

* fix: tag filtering when changing filter to watched topics

or changing popular time limit to month

* chore: up harmony

* fix: closes #11354, show no post error if queued post already accepted/rejected

* test: #11354

* test: #11354

* fix(deps): bump 2factor

* fix: #11357 clear cache on thumb remove

* fix: thumb remove on windows, closes #11357

* test: openapi for thumbs

* test: fix openapi

---------

Co-authored-by: Julian Lam <julian@nodebb.org>
Co-authored-by: Opliko <opliko.reg@protonmail.com>
2023-03-17 11:58:31 -04:00

544 lines
16 KiB
JavaScript

'use strict';
define('forum/topic/postTools', [
'share',
'navigator',
'components',
'translator',
'forum/topic/votes',
'api',
'bootbox',
'alerts',
'hooks',
], function (share, navigator, components, translator, votes, api, bootbox, alerts, hooks) {
const PostTools = {};
let staleReplyAnyway = false;
PostTools.init = function (tid) {
staleReplyAnyway = false;
renderMenu();
addPostHandlers(tid);
share.addShareHandlers(ajaxify.data.titleRaw);
votes.addVoteHandler();
PostTools.updatePostCount(ajaxify.data.postcount);
};
function renderMenu() {
const container = document.querySelector('[component="topic"]');
if (!container) {
return;
}
$('[component="topic"]').on('show.bs.dropdown', '.moderator-tools', function () {
const $this = $(this);
const dropdownMenu = $this.find('.dropdown-menu');
if (dropdownMenu.attr('data-loaded')) {
return;
}
const postEl = $this.parents('[data-pid]');
const pid = postEl.attr('data-pid');
const index = parseInt(postEl.attr('data-index'), 10);
socket.emit('posts.loadPostTools', { pid: pid, cid: ajaxify.data.cid }, async (err, data) => {
if (err) {
return alerts.error(err);
}
data.posts.display_move_tools = data.posts.display_move_tools && index !== 0;
const html = await app.parseAndTranslate('partials/topic/post-menu-list', data);
const clipboard = require('clipboard');
dropdownMenu.attr('data-loaded', 'true').html(html);
new clipboard('[data-clipboard-text]');
hooks.fire('action:post.tools.load', {
element: dropdownMenu,
});
});
});
}
PostTools.toggle = function (pid, isDeleted) {
const postEl = components.get('post', 'pid', pid);
postEl.find('[component="post/quote"], [component="post/bookmark"], [component="post/reply"], [component="post/flag"], [component="user/chat"]')
.toggleClass('hidden', isDeleted);
postEl.find('[component="post/delete"]').toggleClass('hidden', isDeleted).parent().attr('hidden', isDeleted ? '' : null);
postEl.find('[component="post/restore"]').toggleClass('hidden', !isDeleted).parent().attr('hidden', !isDeleted ? '' : null);
postEl.find('[component="post/purge"]').toggleClass('hidden', !isDeleted).parent().attr('hidden', !isDeleted ? '' : null);
PostTools.removeMenu(postEl);
};
PostTools.removeMenu = function (postEl) {
postEl.find('[component="post/tools"] .dropdown-menu')
.removeAttr('data-loaded').html('');
};
PostTools.updatePostCount = function (postCount) {
const postCountEl = components.get('topic/post-count');
postCountEl.html(postCount).attr('title', postCount);
utils.makeNumbersHumanReadable(postCountEl);
navigator.setCount(postCount);
};
function addPostHandlers(tid) {
const postContainer = components.get('topic');
handleSelectionTooltip();
postContainer.on('click', '[component="post/quote"]', function () {
onQuoteClicked($(this), tid);
});
postContainer.on('click', '[component="post/reply"]', function () {
onReplyClicked($(this), tid);
});
$('.topic').on('click', '[component="topic/reply"]', function (e) {
e.preventDefault();
onReplyClicked($(this), tid);
});
$('.topic').on('click', '[component="topic/reply-as-topic"]', function () {
translator.translate('[[topic:link_back, ' + ajaxify.data.titleRaw + ', ' + config.relative_path + '/topic/' + ajaxify.data.slug + ']]', function (body) {
hooks.fire('action:composer.topic.new', {
cid: ajaxify.data.cid,
body: body,
});
});
});
postContainer.on('click', '[component="post/bookmark"]', function () {
return bookmarkPost($(this), getData($(this), 'data-pid'));
});
postContainer.on('click', '[component="post/upvote"]', function () {
return votes.toggleVote($(this), '.upvoted', 1);
});
postContainer.on('click', '[component="post/downvote"]', function () {
return votes.toggleVote($(this), '.downvoted', -1);
});
postContainer.on('click', '[component="post/vote-count"]', function () {
votes.showVotes(getData($(this), 'data-pid'));
});
postContainer.on('click', '[component="post/flag"]', function () {
const pid = getData($(this), 'data-pid');
require(['flags'], function (flags) {
flags.showFlagModal({
type: 'post',
id: pid,
});
});
});
postContainer.on('click', '[component="post/flagUser"]', function () {
const uid = getData($(this), 'data-uid');
require(['flags'], function (flags) {
flags.showFlagModal({
type: 'user',
id: uid,
});
});
});
postContainer.on('click', '[component="post/flagResolve"]', function () {
const flagId = $(this).attr('data-flagId');
require(['flags'], function (flags) {
flags.resolve(flagId);
});
});
postContainer.on('click', '[component="post/edit"]', function () {
const btn = $(this);
const timestamp = parseInt(getData(btn, 'data-timestamp'), 10);
const postEditDuration = parseInt(ajaxify.data.postEditDuration, 10);
if (checkDuration(postEditDuration, timestamp, 'post-edit-duration-expired')) {
hooks.fire('action:composer.post.edit', {
pid: getData(btn, 'data-pid'),
});
}
});
if (config.enablePostHistory && ajaxify.data.privileges['posts:history']) {
postContainer.on('click', '[component="post/view-history"], [component="post/edit-indicator"]', function () {
const btn = $(this);
require(['forum/topic/diffs'], function (diffs) {
diffs.open(getData(btn, 'data-pid'));
});
});
}
postContainer.on('click', '[component="post/delete"]', function () {
const btn = $(this);
const timestamp = parseInt(getData(btn, 'data-timestamp'), 10);
const postDeleteDuration = parseInt(ajaxify.data.postDeleteDuration, 10);
if (checkDuration(postDeleteDuration, timestamp, 'post-delete-duration-expired')) {
togglePostDelete($(this));
}
});
function checkDuration(duration, postTimestamp, languageKey) {
if (!ajaxify.data.privileges.isAdminOrMod && duration && Date.now() - postTimestamp > duration * 1000) {
const numDays = Math.floor(duration / 86400);
const numHours = Math.floor((duration % 86400) / 3600);
const numMinutes = Math.floor(((duration % 86400) % 3600) / 60);
const numSeconds = ((duration % 86400) % 3600) % 60;
let msg = '[[error:' + languageKey + ', ' + duration + ']]';
if (numDays) {
if (numHours) {
msg = '[[error:' + languageKey + '-days-hours, ' + numDays + ', ' + numHours + ']]';
} else {
msg = '[[error:' + languageKey + '-days, ' + numDays + ']]';
}
} else if (numHours) {
if (numMinutes) {
msg = '[[error:' + languageKey + '-hours-minutes, ' + numHours + ', ' + numMinutes + ']]';
} else {
msg = '[[error:' + languageKey + '-hours, ' + numHours + ']]';
}
} else if (numMinutes) {
if (numSeconds) {
msg = '[[error:' + languageKey + '-minutes-seconds, ' + numMinutes + ', ' + numSeconds + ']]';
} else {
msg = '[[error:' + languageKey + '-minutes, ' + numMinutes + ']]';
}
}
alerts.error(msg);
return false;
}
return true;
}
postContainer.on('click', '[component="post/restore"]', function () {
togglePostDelete($(this));
});
postContainer.on('click', '[component="post/purge"]', function () {
purgePost($(this));
});
postContainer.on('click', '[component="post/move"]', function () {
const btn = $(this);
require(['forum/topic/move-post'], function (movePost) {
movePost.init(btn.parents('[data-pid]'));
});
});
postContainer.on('click', '[component="post/change-owner"]', function () {
const btn = $(this);
require(['forum/topic/change-owner'], function (changeOwner) {
changeOwner.init(btn.parents('[data-pid]'));
});
});
postContainer.on('click', '[component="post/ban-ip"]', function () {
const ip = $(this).attr('data-ip');
socket.emit('blacklist.addRule', ip, function (err) {
if (err) {
return alerts.error(err);
}
alerts.success('[[admin/manage/blacklist:ban-ip]]');
});
});
postContainer.on('click', '[component="post/chat"]', function () {
openChat($(this));
});
}
async function onReplyClicked(button, tid) {
const selectedNode = await getSelectedNode();
showStaleWarning(async function () {
let username = await getUserSlug(button);
if (getData(button, 'data-uid') === '0' || !getData(button, 'data-userslug')) {
username = '';
}
const toPid = button.is('[component="post/reply"]') ? getData(button, 'data-pid') : null;
const isQuoteToPid = !toPid || !selectedNode.pid || toPid === selectedNode.pid;
if (selectedNode.text && isQuoteToPid) {
username = username || selectedNode.username;
hooks.fire('action:composer.addQuote', {
tid: tid,
pid: toPid,
topicName: ajaxify.data.titleRaw,
username: username,
text: selectedNode.text,
selectedPid: selectedNode.pid,
});
} else {
hooks.fire('action:composer.post.new', {
tid: tid,
pid: toPid,
topicName: ajaxify.data.titleRaw,
text: username ? username + ' ' : ($('[component="topic/quickreply/text"]').val() || ''),
});
}
});
}
async function onQuoteClicked(button, tid) {
const selectedNode = await getSelectedNode();
showStaleWarning(async function () {
const username = await getUserSlug(button);
const toPid = getData(button, 'data-pid');
function quote(text) {
hooks.fire('action:composer.addQuote', {
tid: tid,
pid: toPid,
username: username,
topicName: ajaxify.data.titleRaw,
text: text,
});
}
if (selectedNode.text && toPid && toPid === selectedNode.pid) {
return quote(selectedNode.text);
}
socket.emit('posts.getRawPost', toPid, function (err, post) {
if (err) {
return alerts.error(err);
}
quote(post);
});
});
}
async function getSelectedNode() {
let selectedText = '';
let selectedPid;
let username = '';
const selection = window.getSelection ? window.getSelection() : document.selection.createRange();
const postContents = $('[component="post"] [component="post/content"]');
let content;
postContents.each(function (index, el) {
if (selection && selection.containsNode && el && selection.containsNode(el, true)) {
content = el;
}
});
if (content && selection) {
selectedText = selection.toString();
const postEl = $(content).parents('[component="post"]');
selectedPid = postEl.attr('data-pid');
username = await getUserSlug($(content));
}
return { text: selectedText, pid: selectedPid, username: username };
}
function bookmarkPost(button, pid) {
const method = button.attr('data-bookmarked') === 'false' ? 'put' : 'del';
api[method](`/posts/${pid}/bookmark`, undefined, function (err) {
if (err) {
return alerts.error(err);
}
const type = method === 'put' ? 'bookmark' : 'unbookmark';
hooks.fire(`action:post.${type}`, { pid: pid });
});
return false;
}
function getData(button, data) {
return button.parents('[data-pid]').attr(data);
}
function getUserSlug(button) {
return new Promise((resolve) => {
let slug = '';
if (button.attr('component') === 'topic/reply') {
resolve(slug);
return;
}
const post = button.parents('[data-pid]');
if (post.length) {
require(['slugify'], function (slugify) {
slug = slugify(post.attr('data-username'), true);
if (!slug) {
if (post.attr('data-uid') !== '0') {
slug = '[[global:former_user]]';
} else {
slug = '[[global:guest]]';
}
}
if (slug && slug !== '[[global:former_user]]' && slug !== '[[global:guest]]') {
slug = '@' + slug;
}
resolve(slug);
});
return;
}
resolve(slug);
});
}
function togglePostDelete(button) {
const pid = getData(button, 'data-pid');
const postEl = components.get('post', 'pid', pid);
const action = !postEl.hasClass('deleted') ? 'delete' : 'restore';
postAction(action, pid);
}
function purgePost(button) {
postAction('purge', getData(button, 'data-pid'));
}
async function postAction(action, pid) {
({ action } = await hooks.fire(`static:post.${action}`, { action, pid }));
if (!action) {
return;
}
bootbox.confirm('[[topic:post_' + action + '_confirm]]', function (confirm) {
if (!confirm) {
return;
}
const route = action === 'purge' ? '' : '/state';
const method = action === 'restore' ? 'put' : 'del';
api[method](`/posts/${pid}${route}`).catch(alerts.error);
});
}
function openChat(button) {
const post = button.parents('[data-pid]');
require(['chat'], function (chat) {
chat.newChat(post.attr('data-uid'));
});
button.parents('.btn-group').find('.dropdown-toggle').click();
return false;
}
function showStaleWarning(callback) {
const staleThreshold = Math.min(Date.now() - (1000 * 60 * 60 * 24 * ajaxify.data.topicStaleDays), 8640000000000000);
if (staleReplyAnyway || ajaxify.data.lastposttime >= staleThreshold) {
return callback();
}
const warning = bootbox.dialog({
title: '[[topic:stale.title]]',
message: '[[topic:stale.warning]]',
buttons: {
reply: {
label: '[[topic:stale.reply_anyway]]',
className: 'btn-link',
callback: function () {
staleReplyAnyway = true;
callback();
},
},
create: {
label: '[[topic:stale.create]]',
className: 'btn-primary',
callback: function () {
translator.translate('[[topic:link_back, ' + ajaxify.data.title + ', ' + config.relative_path + '/topic/' + ajaxify.data.slug + ']]', function (body) {
hooks.fire('action:composer.topic.new', {
cid: ajaxify.data.cid,
body: body,
fromStaleTopic: true,
});
});
},
},
},
});
warning.modal();
}
const selectionChangeFn = utils.debounce(selectionChange, 250);
function handleSelectionTooltip() {
if (!ajaxify.data.privileges['topics:reply']) {
return;
}
hooks.onPage('action:posts.loaded', delayedTooltip);
$(document).off('selectionchange');
$(document).on('selectionchange', function () {
const selectionEmpty = window.getSelection().toString() === '';
if (selectionEmpty) {
$('[component="selection/tooltip"]').addClass('hidden');
}
});
$(document).on('selectionchange', selectionChangeFn);
}
function selectionChange() {
const selectionEmpty = window.getSelection().toString() === '';
if (!selectionEmpty) {
delayedTooltip();
}
}
async function delayedTooltip() {
let selectionTooltip = $('[component="selection/tooltip"]');
selectionTooltip.addClass('hidden');
if (selectionTooltip.attr('data-ajaxify') === '1') {
selectionTooltip.remove();
return;
}
const selection = window.getSelection();
if (selection.focusNode && selection.type === 'Range' && ajaxify.data.template.topic) {
const focusNode = $(selection.focusNode);
const anchorNode = $(selection.anchorNode);
const firstPid = anchorNode.parents('[data-pid]').attr('data-pid');
const lastPid = focusNode.parents('[data-pid]').attr('data-pid');
if (firstPid !== lastPid || !focusNode.parents('[component="post/content"]').length || !anchorNode.parents('[component="post/content"]').length) {
return;
}
const postEl = focusNode.parents('[data-pid]');
const selectionRange = selection.getRangeAt(0);
if (!postEl.length || selectionRange.collapsed) {
return;
}
const rects = selectionRange.getClientRects();
const lastRect = rects[rects.length - 1];
if (!selectionTooltip.length) {
selectionTooltip = await app.parseAndTranslate('partials/topic/selection-tooltip', ajaxify.data);
$('[component="selection/tooltip"]').remove();
selectionTooltip.addClass('hidden').appendTo('body');
}
selectionTooltip.off('click').on('click', '[component="selection/tooltip/quote"]', function () {
selectionTooltip.addClass('hidden');
onQuoteClicked(postEl.find('[component="post/quote"]'), ajaxify.data.tid);
});
selectionTooltip.removeClass('hidden');
$(window).one('action:ajaxify.start', function () {
selectionTooltip.attr('data-ajaxify', 1).addClass('hidden');
$(document).off('selectionchange', selectionChangeFn);
});
const tooltipWidth = selectionTooltip.outerWidth(true);
selectionTooltip.css({
top: lastRect.bottom + $(window).scrollTop(),
left: tooltipWidth > lastRect.width ? lastRect.left : lastRect.left + lastRect.width - tooltipWidth,
});
}
}
return PostTools;
});