'use strict'; define('forum/topic/threadTools', [ 'components', 'translator', 'handleBack', 'forum/topic/posts', 'api', 'hooks', 'bootbox', 'alerts', 'bootstrap', ], function (components, translator, handleBack, posts, api, hooks, bootbox, alerts, bootstrap) { const ThreadTools = {}; ThreadTools.init = function (tid, topicContainer) { renderMenu(topicContainer); $('.topic-main-buttons [title]').tooltip({ container: '#content', animation: false, }); ThreadTools.observeTopicLabels($('[component="topic/labels"]')); // function topicCommand(method, path, command, onComplete) { topicContainer.on('click', '[component="topic/delete"]', function () { topicCommand('del', '/state', 'delete'); return false; }); topicContainer.on('click', '[component="topic/restore"]', function () { topicCommand('put', '/state', 'restore'); return false; }); topicContainer.on('click', '[component="topic/purge"]', function () { topicCommand('del', '', 'purge'); return false; }); topicContainer.on('click', '[component="topic/lock"]', function () { topicCommand('put', '/lock', 'lock'); return false; }); topicContainer.on('click', '[component="topic/unlock"]', function () { topicCommand('del', '/lock', 'unlock'); return false; }); topicContainer.on('click', '[component="topic/pin"]', function () { topicCommand('put', '/pin', 'pin'); return false; }); topicContainer.on('click', '[component="topic/unpin"]', function () { topicCommand('del', '/pin', 'unpin'); return false; }); topicContainer.on('click', '[component="topic/mark-unread"]', function () { topicCommand('del', '/read', undefined, () => { if (app.previousUrl && !app.previousUrl.match('^/topic')) { ajaxify.go(app.previousUrl, function () { handleBack.onBackClicked(true); }); } else if (ajaxify.data.category) { ajaxify.go('category/' + ajaxify.data.category.slug, handleBack.onBackClicked); } alerts.success('[[topic:mark_unread.success]]'); }); }); topicContainer.on('click', '[component="topic/mark-unread-for-all"]', function () { const btn = $(this); topicCommand('put', '/bump', undefined, () => { alerts.success('[[topic:markAsUnreadForAll.success]]'); btn.parents('.thread-tools.open').find('.dropdown-toggle').trigger('click'); }); }); topicContainer.on('click', '[component="topic/event/delete"]', function () { const eventId = $(this).attr('data-topic-event-id'); const eventEl = $(this).parents('[component="topic/event"]'); bootbox.confirm('[[topic:delete-event-confirm]]', (ok) => { if (ok) { api.del(`/topics/${tid}/events/${eventId}`, {}) .then(function () { eventEl.remove(); }) .catch(alerts.error); } }); }); topicContainer.on('click', '[component="topic/move"]', function () { require(['forum/topic/move'], function (move) { move.init([tid], ajaxify.data.cid); }); return false; }); topicContainer.on('click', '[component="topic/delete/posts"]', function () { require(['forum/topic/delete-posts'], function (deletePosts) { deletePosts.init(); }); }); topicContainer.on('click', '[component="topic/fork"]', function () { require(['forum/topic/fork'], function (fork) { fork.init(); }); }); topicContainer.on('click', '[component="topic/merge"]', function () { require(['forum/topic/merge'], function (merge) { merge.init(function () { merge.addTopic(ajaxify.data.tid); }); }); }); topicContainer.on('click', '[component="topic/tag"]', function () { require(['forum/topic/tag'], function (tag) { tag.init([ajaxify.data], ajaxify.data.tagWhitelist); }); }); topicContainer.on('click', '[component="topic/move-posts"]', function () { require(['forum/topic/move-post'], function (movePosts) { movePosts.init(); }); }); topicContainer.on('click', '[component="topic/following"]', function () { changeWatching('follow'); }); topicContainer.on('click', '[component="topic/not-following"]', function () { changeWatching('follow', 0); }); topicContainer.on('click', '[component="topic/ignoring"]', function () { changeWatching('ignore'); }); function changeWatching(type, state = 1) { const method = state ? 'put' : 'del'; api[method](`/topics/${tid}/${type}`, {}, () => { let message = ''; if (type === 'follow') { message = state ? '[[topic:following_topic.message]]' : '[[topic:not_following_topic.message]]'; } else if (type === 'ignore') { message = state ? '[[topic:ignoring_topic.message]]' : '[[topic:not_following_topic.message]]'; } // From here on out, type changes to 'unfollow' if state is falsy if (!state) { type = 'unfollow'; } setFollowState(type); alerts.alert({ alert_id: 'follow_thread', message: message, type: 'success', timeout: 5000, }); hooks.fire('action:topics.changeWatching', { tid: tid, type: type }); }, () => { alerts.alert({ type: 'danger', alert_id: 'topic_follow', title: '[[global:please_log_in]]', message: '[[topic:login_to_subscribe]]', timeout: 5000, }); }); return false; } }; ThreadTools.observeTopicLabels = function (labels) { // show or hide topic/labels container depending on children visibility const mut = new MutationObserver(function (mutations) { const first = mutations[0]; if (first && first.attributeName === 'class') { const visibleChildren = labels.children().filter((index, el) => !$(el).hasClass('hidden')); labels.toggleClass('hidden', !visibleChildren.length); } }); labels.children().each((index, el) => { mut.observe(el, { attributes: true }); }); }; function renderMenu(container) { container = container.get(0); if (!container) { return; } container.querySelectorAll('.thread-tools').forEach((toolsEl) => { toolsEl.addEventListener('show.bs.dropdown', (e) => { const dropdownMenu = e.target.nextElementSibling; if (!dropdownMenu) { return; } socket.emit('topics.loadTopicTools', { tid: ajaxify.data.tid, cid: ajaxify.data.cid }, function (err, data) { if (err) { return alerts.error(err); } app.parseAndTranslate('partials/topic/topic-menu-list', data, function (html) { $(dropdownMenu).html(html); hooks.fire('action:topic.tools.load', { element: $(dropdownMenu), }); }); }); }, { once: true, }); }); } function topicCommand(method, path, command, onComplete) { if (!onComplete) { onComplete = function () {}; } const tid = ajaxify.data.tid; const body = {}; const execute = function (ok) { if (ok) { api[method](`/topics/${tid}${path}`, body) .then(onComplete) .catch(alerts.error); } }; switch (command) { case 'delete': case 'restore': case 'purge': bootbox.confirm(`[[topic:thread_tools.${command}_confirm]]`, execute); break; case 'pin': ThreadTools.requestPinExpiry(body, execute.bind(null, true)); break; default: execute(true); break; } } ThreadTools.requestPinExpiry = function (body, onSuccess) { app.parseAndTranslate('modals/set-pin-expiry', {}, function (html) { const modal = bootbox.dialog({ title: '[[topic:thread_tools.pin]]', message: html, onEscape: true, size: 'small', buttons: { cancel: { label: '[[modules:bootbox.cancel]]', className: 'btn-link', }, save: { label: '[[global:save]]', className: 'btn-primary', callback: function () { const expiryEl = modal.get(0).querySelector('#expiry'); let expiry = expiryEl.value; // No expiry set if (expiry === '') { return onSuccess(); } // Expiration date set expiry = new Date(expiry); if (expiry && expiry.getTime() > Date.now()) { body.expiry = expiry.getTime(); onSuccess(); } else { alerts.error('[[error:invalid-date]]'); } }, }, }, }); }); }; ThreadTools.setLockedState = function (data) { const threadEl = components.get('topic'); if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) { return; } const isLocked = data.isLocked && !ajaxify.data.privileges.isAdminOrMod; components.get('topic/lock').toggleClass('hidden', data.isLocked).parent().attr('hidden', data.isLocked ? '' : null); components.get('topic/unlock').toggleClass('hidden', !data.isLocked).parent().attr('hidden', !data.isLocked ? '' : null); const hideReply = !!((data.isLocked || ajaxify.data.deleted) && !ajaxify.data.privileges.isAdminOrMod); components.get('topic/reply/container').toggleClass('hidden', hideReply); components.get('topic/reply/locked').toggleClass('hidden', ajaxify.data.privileges.isAdminOrMod || !data.isLocked || ajaxify.data.deleted); threadEl.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"]').toggleClass('hidden', hideReply); threadEl.find('[component="post/edit"], [component="post/delete"]').toggleClass('hidden', isLocked); threadEl.find('[component="post"][data-uid="' + app.user.uid + '"].deleted [component="post/tools"]').toggleClass('hidden', isLocked); $('[component="topic/labels"] [component="topic/locked"]').toggleClass('hidden', !data.isLocked); $('[component="post/tools"] .dropdown-menu').html(''); ajaxify.data.locked = data.isLocked; posts.addTopicEvents(data.events); }; ThreadTools.setDeleteState = function (data) { const threadEl = components.get('topic'); if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) { return; } components.get('topic/delete').toggleClass('hidden', data.isDelete).parent().attr('hidden', data.isDelete ? '' : null); components.get('topic/restore').toggleClass('hidden', !data.isDelete).parent().attr('hidden', !data.isDelete ? '' : null); components.get('topic/purge').toggleClass('hidden', !data.isDelete).parent().attr('hidden', !data.isDelete ? '' : null); components.get('topic/deleted/message').toggleClass('hidden', !data.isDelete); if (data.isDelete) { app.parseAndTranslate('partials/topic/deleted-message', { deleter: data.user, deleted: true, deletedTimestampISO: utils.toISOString(Date.now()), }, function (html) { components.get('topic/deleted/message').replaceWith(html); html.find('.timeago').timeago(); }); } const hideReply = data.isDelete && !ajaxify.data.privileges.isAdminOrMod; components.get('topic/reply/container').toggleClass('hidden', hideReply); components.get('topic/reply/locked').toggleClass('hidden', ajaxify.data.privileges.isAdminOrMod || !ajaxify.data.locked || data.isDelete); threadEl.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"]').toggleClass('hidden', hideReply); threadEl.toggleClass('deleted', data.isDelete); ajaxify.data.deleted = data.isDelete ? 1 : 0; posts.addTopicEvents(data.events); }; ThreadTools.setPinnedState = function (data) { const threadEl = components.get('topic'); if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) { return; } components.get('topic/pin').toggleClass('hidden', data.pinned).parent().attr('hidden', data.pinned ? '' : null); components.get('topic/unpin').toggleClass('hidden', !data.pinned).parent().attr('hidden', !data.pinned ? '' : null); const icon = $('[component="topic/labels"] [component="topic/pinned"]'); icon.toggleClass('hidden', !data.pinned); if (data.pinned) { icon.translateAttr('title', ( data.pinExpiry && data.pinExpiryISO ? '[[topic:pinned-with-expiry, ' + data.pinExpiryISO + ']]' : '[[topic:pinned]]' )); } ajaxify.data.pinned = data.pinned; posts.addTopicEvents(data.events); }; function setFollowState(state) { const titles = { follow: '[[topic:watching]]', unfollow: '[[topic:not-watching]]', ignore: '[[topic:ignoring]]', }; translator.translate(titles[state], function (translatedTitle) { const tooltip = bootstrap.Tooltip.getInstance('[component="topic/watch"]'); if (tooltip) { tooltip.setContent({ '.tooltip-inner': translatedTitle }); } }); let menu = components.get('topic/following/menu'); menu.toggleClass('hidden', state !== 'follow'); components.get('topic/following/check').toggleClass('fa-check', state === 'follow'); menu = components.get('topic/not-following/menu'); menu.toggleClass('hidden', state !== 'unfollow'); components.get('topic/not-following/check').toggleClass('fa-check', state === 'unfollow'); menu = components.get('topic/ignoring/menu'); menu.toggleClass('hidden', state !== 'ignore'); components.get('topic/ignoring/check').toggleClass('fa-check', state === 'ignore'); } return ThreadTools; });