diff --git a/node_modules/nodebb-plugin-composer-default/static/lib/composer.js b/node_modules/nodebb-plugin-composer-default/static/lib/composer.js index 1965622926..70e381b8b4 100644 --- a/node_modules/nodebb-plugin-composer-default/static/lib/composer.js +++ b/node_modules/nodebb-plugin-composer-default/static/lib/composer.js @@ -1,919 +1,886 @@ 'use strict'; define('composer', [ - 'taskbar', - 'translator', - 'composer/uploads', - 'composer/formatting', - 'composer/drafts', - 'composer/tags', - 'composer/categoryList', - 'composer/preview', - 'composer/resize', - 'composer/autocomplete', - 'composer/scheduler', - 'composer/post-queue', - 'scrollStop', - 'topicThumbs', - 'api', - 'bootbox', - 'alerts', - 'hooks', - 'messages', - 'search', - 'screenfull', + 'taskbar', + 'translator', + 'composer/uploads', + 'composer/formatting', + 'composer/drafts', + 'composer/tags', + 'composer/categoryList', + 'composer/preview', + 'composer/resize', + 'composer/autocomplete', + 'composer/scheduler', + 'composer/post-queue', + 'scrollStop', + 'topicThumbs', + 'api', + 'bootbox', + 'alerts', + 'hooks', + 'messages', + 'search', + 'screenfull', ], function (taskbar, translator, uploads, formatting, drafts, tags, - categoryList, preview, resize, autocomplete, scheduler, postQueue, scrollStop, - topicThumbs, api, bootbox, alerts, hooks, messagesModule, search, screenfull) { - var composer = { - active: undefined, - posts: {}, - bsEnvironment: undefined, - formatting: undefined, - }; - - var isAnonymous = false; - - $(window).off('resize', onWindowResize).on('resize', onWindowResize); - onWindowResize(); - - $(window).on('action:composer.topics.post', function (ev, data) { - localStorage.removeItem('category:' + data.data.cid + ':bookmark'); - localStorage.removeItem('category:' + data.data.cid + ':bookmark:clicked'); - }); - - $(window).on('popstate', function () { - var env = utils.findBootstrapEnvironment(); - if (composer.active && (env === 'xs' || env === 'sm')) { - if (!composer.posts[composer.active].modified) { - composer.discard(composer.active); - if (composer.discardConfirm && composer.discardConfirm.length) { - composer.discardConfirm.modal('hide'); - delete composer.discardConfirm; - } - return; - } - - translator.translate('[[modules:composer.discard]]', function (translated) { - composer.discardConfirm = bootbox.confirm(translated, function (confirm) { - if (confirm) { - composer.discard(composer.active); - } else { - composer.posts[composer.active].modified = true; - } - }); - composer.posts[composer.active].modified = false; - }); - } - }); - - function removeComposerHistory() { - var env = composer.bsEnvironment; - if (ajaxify.data.template.compose === true || env === 'xs' || env === 'sm') { - history.back(); - } - } - - function onWindowResize() { - var env = utils.findBootstrapEnvironment(); - var isMobile = env === 'xs' || env === 'sm'; - - if (preview.toggle) { - if (preview.env !== env && isMobile) { - preview.env = env; - preview.toggle(false); - } - preview.env = env; - } - - if (composer.active !== undefined) { - resize.reposition($('.composer[data-uuid="' + composer.active + '"]')); - - if (!isMobile && window.location.pathname.startsWith(config.relative_path + '/compose')) { - /* - * If this conditional is met, we're no longer in mobile/tablet - * resolution but we've somehow managed to have a mobile - * composer load, so let's go back to the topic - */ - history.back(); - } else if (isMobile && !window.location.pathname.startsWith(config.relative_path + '/compose')) { - /* - * In this case, we're in mobile/tablet resolution but the composer - * that loaded was a regular composer, so let's fix the address bar - */ - mobileHistoryAppend(); - } - } - composer.bsEnvironment = env; - } - - function alreadyOpen(post) { - // If a composer for the same cid/tid/pid is already open, return the uuid, else return bool false - var type; - var id; - - if (post.hasOwnProperty('cid')) { - type = 'cid'; - } else if (post.hasOwnProperty('tid')) { - type = 'tid'; - } else if (post.hasOwnProperty('pid')) { - type = 'pid'; - } - - id = post[type]; - - // Find a match - for (var uuid in composer.posts) { - if (composer.posts[uuid].hasOwnProperty(type) && id === composer.posts[uuid][type]) { - return uuid; - } - } - - // No matches... - return false; - } - - function push(post) { - if (!post) { - return; - } - - var uuid = utils.generateUUID(); - var existingUUID = alreadyOpen(post); - - if (existingUUID) { - taskbar.updateActive(existingUUID); - return composer.load(existingUUID); - } - - var actionText = '[[topic:composer.new-topic]]'; - if (post.action === 'posts.reply') { - actionText = '[[topic:composer.replying-to]]'; - } else if (post.action === 'posts.edit') { - actionText = '[[topic:composer.editing-in]]'; - } - - translator.translate(actionText, function (translatedAction) { - taskbar.push('composer', uuid, { - title: translatedAction.replace('%1', '"' + post.title + '"'), - }); - }); - - composer.posts[uuid] = post; - composer.load(uuid); - } - - async function composerAlert(post_uuid, message) { - $('.composer[data-uuid="' + post_uuid + '"]').find('.composer-submit').removeAttr('disabled'); - - const { showAlert } = await hooks.fire('filter:composer.error', { post_uuid, message, showAlert: true }); - - if (showAlert) { - alerts.alert({ - type: 'danger', - timeout: 10000, - title: '', - message: message, - alert_id: 'post_error', - }); - } - } - - composer.findByTid = function (tid) { - // Iterates through the initialised composers and returns the uuid of the matching composer - for (var uuid in composer.posts) { - if (composer.posts.hasOwnProperty(uuid) && composer.posts[uuid].hasOwnProperty('tid') && parseInt(composer.posts[uuid].tid, 10) === parseInt(tid, 10)) { - return uuid; - } - } - - return null; - }; - - composer.addButton = function (iconClass, onClick, title) { - formatting.addButton(iconClass, onClick, title); - }; - - composer.newTopic = async (data) => { - let pushData = { - save_id: data.save_id, - action: 'topics.post', - cid: data.cid, - handle: data.handle, - title: data.title || '', - body: data.body || '', - tags: data.tags || [], - modified: !!((data.title && data.title.length) || (data.body && data.body.length)), - isMain: true, - }; - - ({ pushData } = await hooks.fire('filter:composer.topic.push', { - data: data, - pushData: pushData, - })); - - push(pushData); - }; - - composer.addQuote = function (data) { - // tid, toPid, selectedPid, title, username, text, uuid - data.uuid = data.uuid || composer.active; - - var escapedTitle = (data.title || '') - .replace(/([\\`*_{}[\]()#+\-.!])/g, '\\$1') - .replace(/\[/g, '[') - .replace(/\]/g, ']') - .replace(/%/g, '%') - .replace(/,/g, ','); - - if (data.body) { - data.body = '> ' + data.body.replace(/\n/g, '\n> ') + '\n\n'; - } - var link = '[' + escapedTitle + '](' + config.relative_path + '/post/' + encodeURIComponent(data.selectedPid || data.toPid) + ')'; - if (data.uuid === undefined) { - if (data.title && (data.selectedPid || data.toPid)) { - composer.newReply({ - tid: data.tid, - toPid: data.toPid, - title: data.title, - body: '[[modules:composer.user-said-in, ' + data.username + ', ' + link + ']]\n' + data.body, - }); - } else { - composer.newReply({ - tid: data.tid, - toPid: data.toPid, - title: data.title, - body: '[[modules:composer.user-said, ' + data.username + ']]\n' + data.body, - }); - } - return; - } else if (data.uuid !== composer.active) { - // If the composer is not currently active, activate it - composer.load(data.uuid); - } - - var postContainer = $('.composer[data-uuid="' + data.uuid + '"]'); - var bodyEl = postContainer.find('textarea'); - var prevText = bodyEl.val(); - if (data.title && (data.selectedPid || data.toPid)) { - translator.translate('[[modules:composer.user-said-in, ' + data.username + ', ' + link + ']]\n', config.defaultLang, onTranslated); - } else { - translator.translate('[[modules:composer.user-said, ' + data.username + ']]\n', config.defaultLang, onTranslated); - } - - function onTranslated(translated) { - composer.posts[data.uuid].body = (prevText.length ? prevText + '\n\n' : '') + translated + data.body; - bodyEl.val(composer.posts[data.uuid].body); - focusElements(postContainer); - preview.render(postContainer); - } - }; - - composer.newReply = function (data) { - translator.translate(data.body, config.defaultLang, function (translated) { - push({ - save_id: data.save_id, - action: 'posts.reply', - tid: data.tid, - toPid: data.toPid, - title: data.title, - body: translated, - modified: !!(translated && translated.length), - isMain: false, - }); - }); - }; - - composer.editPost = function (data) { - // pid, text - socket.emit('plugins.composer.push', data.pid, function (err, postData) { - if (err) { - return alerts.error(err); - } - postData.save_id = data.save_id; - postData.action = 'posts.edit'; - postData.pid = data.pid; - postData.modified = false; - if (data.body) { - postData.body = data.body; - postData.modified = true; - } - if (data.title) { - postData.title = data.title; - postData.modified = true; - } - push(postData); - }); - }; - - composer.load = function (post_uuid) { - var postContainer = $('.composer[data-uuid="' + post_uuid + '"]'); - if (postContainer.length) { - activate(post_uuid); - resize.reposition(postContainer); - focusElements(postContainer); - onShow(); - } else if (composer.formatting) { - createNewComposer(post_uuid); - } else { - socket.emit('plugins.composer.getFormattingOptions', function (err, options) { - if (err) { - return alerts.error(err); - } - composer.formatting = options; - createNewComposer(post_uuid); - }); - } - }; - - composer.enhance = function (postContainer, post_uuid, postData) { - /* - This method enhances a composer container with client-side sugar (preview, etc) - Everything in here also applies to the /compose route - */ - - if (!post_uuid && !postData) { - post_uuid = utils.generateUUID(); - composer.posts[post_uuid] = ajaxify.data; - postData = ajaxify.data; - postContainer.attr('data-uuid', post_uuid); - } - - categoryList.init(postContainer, composer.posts[post_uuid]); - scheduler.init(postContainer, composer.posts); - - formatting.addHandler(postContainer); - formatting.addComposerButtons(); - preview.handleToggler(postContainer); - postQueue.showAlert(postContainer, postData); - uploads.initialize(post_uuid); - tags.init(postContainer, composer.posts[post_uuid]); - autocomplete.init(postContainer, post_uuid); - - postContainer.on('change', 'input, textarea', function () { - composer.posts[post_uuid].modified = true; - }); - - postContainer.on('click', '.composer-submit', function (e) { - e.preventDefault(); - e.stopPropagation(); // Other click events bring composer back to active state which is undesired on submit - - $(this).attr('disabled', true); - post(post_uuid); - }); - - // event listener for the anonymous checkbox - // Added after adding the front end check box for anonymous posting - postContainer.on('change', '#anonymous-checkbox', function () { - isAnonymous = $(this).is(':checked'); - }); - - require(['mousetrap'], function (mousetrap) { - mousetrap(postContainer.get(0)).bind('mod+enter', function () { - postContainer.find('.composer-submit').attr('disabled', true); - post(post_uuid); - }); - }); - - postContainer.find('.composer-discard').on('click', function (e) { - e.preventDefault(); - - if (!composer.posts[post_uuid].modified) { - composer.discard(post_uuid); - return removeComposerHistory(); - } - - formatting.exitFullscreen(); - - var btn = $(this).prop('disabled', true); - translator.translate('[[modules:composer.discard]]', function (translated) { - bootbox.confirm(translated, function (confirm) { - if (confirm) { - composer.discard(post_uuid); - removeComposerHistory(); - } - btn.prop('disabled', false); - }); - }); - }); - - - postContainer.on('click', '.suggested-response', function () { - var responseText = $(this).data('response'); - - // Find the composer textarea within the current composer instance - var textarea = postContainer.find('textarea.write'); - var currentText = textarea.val(); - - // Insert or append the response text - if (!currentText.trim()) { - textarea.val(responseText); - } else { - textarea.val(currentText + '\n' + responseText); - } - - // Move cursor to the end - textarea[0].selectionStart = textarea[0].selectionEnd = textarea.val().length; - - // Trigger input event to update previews or autosave features - textarea.trigger('input'); - }); - - postContainer.find('.composer-minimize, .minimize .trigger').on('click', function (e) { - e.preventDefault(); - e.stopPropagation(); - composer.minimize(post_uuid); - }); - - const textareaEl = postContainer.find('textarea'); - textareaEl.on('input propertychange', utils.debounce(function () { - preview.render(postContainer); - }, 250)); - - textareaEl.on('scroll', function () { - preview.matchScroll(postContainer); - }); - - drafts.init(postContainer, postData); - const draft = drafts.get(postData.save_id); - - preview.render(postContainer, function () { - preview.matchScroll(postContainer); - }); - - handleHelp(postContainer); - handleSearch(postContainer); - focusElements(postContainer); - if (postData.action === 'posts.edit') { - composer.updateThumbCount(post_uuid, postContainer); - } - - // Hide "zen mode" if fullscreen API is not enabled/available (ahem, iOS...) - if (!screenfull.isEnabled) { - $('[data-format="zen"]').parent().addClass('hidden'); - } - - hooks.fire('action:composer.enhanced', { postContainer, postData, draft }); - }; - - async function getSelectedCategory(postData) { - if (ajaxify.data.template.category && parseInt(postData.cid, 10) === parseInt(ajaxify.data.cid, 10)) { - // no need to load data if we are already on the category page - return ajaxify.data; - } else if (parseInt(postData.cid, 10)) { - return await api.get(`/api/category/${postData.cid}`, {}); - } - return null; - } - - async function createNewComposer(post_uuid) { - var postData = composer.posts[post_uuid]; - - var isTopic = postData ? postData.hasOwnProperty('cid') : false; - var isMain = postData ? !!postData.isMain : false; - var isEditing = postData ? !!postData.pid : false; - var isGuestPost = postData ? parseInt(postData.uid, 10) === 0 : false; - const isScheduled = postData.timestamp > Date.now(); - - // see - // https://github.com/NodeBB/NodeBB/issues/2994 and - // https://github.com/NodeBB/NodeBB/issues/1951 - // remove when 1951 is resolved - - var title = postData.title.replace(/%/g, '%').replace(/,/g, ','); - postData.category = await getSelectedCategory(postData); - const privileges = postData.category ? postData.category.privileges : ajaxify.data.privileges; - var data = { - topicTitle: title, - titleLength: title.length, - body: translator.escape(utils.escapeHTML(postData.body)), - mobile: composer.bsEnvironment === 'xs' || composer.bsEnvironment === 'sm', - resizable: true, - thumb: postData.thumb, - isTopicOrMain: isTopic || isMain, - maximumTitleLength: config.maximumTitleLength, - maximumPostLength: config.maximumPostLength, - minimumTagLength: config.minimumTagLength, - maximumTagLength: config.maximumTagLength, - 'composer:showHelpTab': config['composer:showHelpTab'], - isTopic: isTopic, - isEditing: isEditing, - canSchedule: !!(isMain && privileges && - ((privileges['topics:schedule'] && !isEditing) || (isScheduled && privileges.view_scheduled))), - showHandleInput: config.allowGuestHandles && - (app.user.uid === 0 || (isEditing && isGuestPost && app.user.isAdmin)), - handle: postData ? postData.handle || '' : undefined, - formatting: composer.formatting, - tagWhitelist: postData.category ? postData.category.tagWhitelist : ajaxify.data.tagWhitelist, - privileges: app.user.privileges, - selectedCategory: postData.category, - submitOptions: [ - // Add items using `filter:composer.create`, or just add them to the