', { component: 'post/replies' }).html(html).hide();
- if (button.attr('data-target-component')) {
- post.find('[component="' + button.attr('data-target-component') + '"]').html(repliesEl);
- } else {
- repliesEl.insertAfter(button);
- }
-
- repliesEl.slideDown('fast');
- posts.onNewPostsAddedToDom(html);
- hooks.fire('action:posts.loaded', { posts: data });
- });
- });
- } else if (close.is(':not(.hidden)')) {
- close.addClass('hidden');
- open.removeClass('hidden');
- loading.addClass('hidden');
- post.find('[component="post/replies"]').slideUp('fast', function () {
- $(this).remove();
- });
- }
- };
-
- Replies.onNewPost = function (data) {
- const post = data.posts[0];
- if (!post) {
- return;
- }
- incrementCount(post, 1);
- data.hideReplies = config.hasOwnProperty('showNestedReplies') ? !config.showNestedReplies : true;
- app.parseAndTranslate('topic', 'posts', data, function (html) {
- const replies = $('[component="post"][data-pid="' + post.toPid + '"] [component="post/replies"]').first();
- if (replies.length) {
- if (config.topicPostSort === 'newest_to_oldest') {
- replies.prepend(html);
- } else {
- replies.append(html);
- }
- posts.onNewPostsAddedToDom(html);
- }
- });
- };
-
- Replies.onPostPurged = function (post) {
- incrementCount(post, -1);
- };
-
- function incrementCount(post, inc) {
- const replyCount = $('[component="post"][data-pid="' + post.toPid + '"]').find('[component="post/reply-count"]').first();
- const countEl = replyCount.find('[component="post/reply-count/text"]');
- const avatars = replyCount.find('[component="post/reply-count/avatars"]');
- const count = Math.max(0, parseInt(countEl.attr('data-replies'), 10) + inc);
- const timestamp = replyCount.find('.timeago').attr('title', post.timestampISO);
-
- countEl.attr('data-replies', count);
- replyCount.toggleClass('hidden', count <= 0);
- if (count > 1) {
- countEl.translateText('[[topic:replies_to_this_post, ' + count + ']]');
- } else {
- countEl.translateText('[[topic:one_reply_to_this_post]]');
- }
-
- if (!avatars.find('[data-uid="' + post.uid + '"]').length && count < 7) {
- app.parseAndTranslate('topic', 'posts', { posts: [{ replies: { users: [post.user] } }] }, function (html) {
- avatars.prepend(html.find('[component="post/reply-count/avatars"] [component="avatar/picture"]'));
- });
- }
-
- avatars.addClass('hasMore');
-
- timestamp.data('timeago', null).timeago();
- }
-
- return Replies;
+define('forum/topic/replies', ['forum/topic/posts', 'hooks', 'alerts'], (posts, hooks, alerts) => {
+ const Replies = {};
+
+ Replies.init = function (button) {
+ const post = button.closest('[data-pid]');
+ const pid = post.data('pid');
+ const open = button.find('[component="post/replies/open"]');
+ const loading = button.find('[component="post/replies/loading"]');
+ const close = button.find('[component="post/replies/close"]');
+
+ if (open.is(':not(.hidden)') && loading.is('.hidden')) {
+ open.addClass('hidden');
+ loading.removeClass('hidden');
+
+ socket.emit('posts.getReplies', pid, (error, data) => {
+ loading.addClass('hidden');
+ if (error) {
+ open.removeClass('hidden');
+ return alerts.error(error);
+ }
+
+ close.removeClass('hidden');
+
+ posts.modifyPostsByPrivileges(data);
+ const tplData = {
+ posts: data,
+ privileges: ajaxify.data.privileges,
+ 'downvote:disabled': ajaxify.data['downvote:disabled'],
+ 'reputation:disabled': ajaxify.data['reputation:disabled'],
+ loggedIn: Boolean(app.user.uid),
+ hideReplies: config.hasOwnProperty('showNestedReplies') ? !config.showNestedReplies : true,
+ };
+ app.parseAndTranslate('topic', 'posts', tplData, html => {
+ const repliesElement = $('
', {component: 'post/replies'}).html(html).hide();
+ if (button.attr('data-target-component')) {
+ post.find('[component="' + button.attr('data-target-component') + '"]').html(repliesElement);
+ } else {
+ repliesElement.insertAfter(button);
+ }
+
+ repliesElement.slideDown('fast');
+ posts.onNewPostsAddedToDom(html);
+ hooks.fire('action:posts.loaded', {posts: data});
+ });
+ });
+ } else if (close.is(':not(.hidden)')) {
+ close.addClass('hidden');
+ open.removeClass('hidden');
+ loading.addClass('hidden');
+ post.find('[component="post/replies"]').slideUp('fast', function () {
+ $(this).remove();
+ });
+ }
+ };
+
+ Replies.onNewPost = function (data) {
+ const post = data.posts[0];
+ if (!post) {
+ return;
+ }
+
+ incrementCount(post, 1);
+ data.hideReplies = config.hasOwnProperty('showNestedReplies') ? !config.showNestedReplies : true;
+ app.parseAndTranslate('topic', 'posts', data, html => {
+ const replies = $('[component="post"][data-pid="' + post.toPid + '"] [component="post/replies"]').first();
+ if (replies.length > 0) {
+ if (config.topicPostSort === 'newest_to_oldest') {
+ replies.prepend(html);
+ } else {
+ replies.append(html);
+ }
+
+ posts.onNewPostsAddedToDom(html);
+ }
+ });
+ };
+
+ Replies.onPostPurged = function (post) {
+ incrementCount(post, -1);
+ };
+
+ function incrementCount(post, increment) {
+ const replyCount = $('[component="post"][data-pid="' + post.toPid + '"]').find('[component="post/reply-count"]').first();
+ const countElement = replyCount.find('[component="post/reply-count/text"]');
+ const avatars = replyCount.find('[component="post/reply-count/avatars"]');
+ const count = Math.max(0, Number.parseInt(countElement.attr('data-replies'), 10) + increment);
+ const timestamp = replyCount.find('.timeago').attr('title', post.timestampISO);
+
+ countElement.attr('data-replies', count);
+ replyCount.toggleClass('hidden', count <= 0);
+ if (count > 1) {
+ countElement.translateText('[[topic:replies_to_this_post, ' + count + ']]');
+ } else {
+ countElement.translateText('[[topic:one_reply_to_this_post]]');
+ }
+
+ if (avatars.find('[data-uid="' + post.uid + '"]').length === 0 && count < 7) {
+ app.parseAndTranslate('topic', 'posts', {posts: [{replies: {users: [post.user]}}]}, html => {
+ avatars.prepend(html.find('[component="post/reply-count/avatars"] [component="avatar/picture"]'));
+ });
+ }
+
+ avatars.addClass('hasMore');
+
+ timestamp.data('timeago', null).timeago();
+ }
+
+ return Replies;
});
diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js
index 3a72d47..62b3b68 100644
--- a/public/src/client/topic/threadTools.js
+++ b/public/src/client/topic/threadTools.js
@@ -1,323 +1,326 @@
'use strict';
-const { utils } = require('sortablejs');
-
+const {utils} = require('sortablejs');
define('forum/topic/threadTools', [
- 'components',
- 'translator',
- 'handleBack',
- 'forum/topic/posts',
- 'api',
- 'hooks',
- 'bootbox',
- 'alerts',
-], function (components, translator, handleBack, posts, api, hooks, bootbox, alerts) {
- const ThreadTools = {};
-
- ThreadTools.init = function (tid, topicContainer) {
- renderMenu(topicContainer);
-
- // 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/private"]', function () {
- topicCommand('put', '/private', 'private');
- return false;
- });
-
- topicContainer.on('click', '[component="topic/public"]', function () {
- topicCommand('del', '/private', 'public');
- return false;
- });
-
- 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);
- }
- });
- });
-
- // todo: should also use topicCommand, but no write api call exists for this yet
- topicContainer.on('click', '[component="topic/mark-unread"]', function () {
- socket.emit('topics.markUnread', tid, function (err) {
- if (err) {
- return alerts.error(err);
- }
-
- 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]]');
- });
- return false;
- });
-
- topicContainer.on('click', '[component="topic/mark-unread-for-all"]', function () {
- const btn = $(this);
- socket.emit('topics.markAsUnreadForAll', [tid], function (err) {
- if (err) {
- return alerts.error(err);
- }
- alerts.success('[[topic:markAsUnreadForAll.success]]');
- btn.parents('.thread-tools.open').find('.dropdown-toggle').trigger('click');
- });
- return false;
- });
-
- 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/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;
- }
- };
-
- function renderMenu(container) {
- container.on('show.bs.dropdown', '.thread-tools', function () {
- const $this = $(this);
- const dropdownMenu = $this.find('.dropdown-menu');
- if (dropdownMenu.html()) {
- return;
- }
-
- dropdownMenu.toggleClass('hidden', true);
- 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);
- dropdownMenu.toggleClass('hidden', false);
-
- hooks.fire('action:topic.tools.load', {
- element: dropdownMenu,
- });
- });
- });
- });
- }
-
- 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.setPrivateState = function (data) {
- const threadEl = components.get('topic');
- if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) {
- return;
- }
-
- components.get('topic/private').toggleClass('hidden', data.isPrivate).parent().attr('hidden', data.isPrivate ? '' : null);
- components.get('topic/public').toggleClass('hidden', !data.isPrivate).parent().attr('hidden', !data.isPrivate ? '' : null);
-
- /* if (data.isPrivate) {
+ 'components',
+ 'translator',
+ 'handleBack',
+ 'forum/topic/posts',
+ 'api',
+ 'hooks',
+ 'bootbox',
+ 'alerts',
+], (components, translator, handleBack, posts, api, hooks, bootbox, alerts) => {
+ const ThreadTools = {};
+
+ ThreadTools.init = function (tid, topicContainer) {
+ renderMenu(topicContainer);
+
+ // Function topicCommand(method, path, command, onComplete) {
+ topicContainer.on('click', '[component="topic/delete"]', () => {
+ topicCommand('del', '/state', 'delete');
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/restore"]', () => {
+ topicCommand('put', '/state', 'restore');
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/purge"]', () => {
+ topicCommand('del', '', 'purge');
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/lock"]', () => {
+ topicCommand('put', '/lock', 'lock');
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/unlock"]', () => {
+ topicCommand('del', '/lock', 'unlock');
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/pin"]', () => {
+ topicCommand('put', '/pin', 'pin');
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/unpin"]', () => {
+ topicCommand('del', '/pin', 'unpin');
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/private"]', () => {
+ topicCommand('put', '/private', 'private');
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/public"]', () => {
+ topicCommand('del', '/private', 'public');
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/event/delete"]', function () {
+ const eventId = $(this).attr('data-topic-event-id');
+ const eventElement = $(this).parents('[component="topic/event"]');
+ bootbox.confirm('[[topic:delete-event-confirm]]', ok => {
+ if (ok) {
+ api.del(`/topics/${tid}/events/${eventId}`, {})
+ .then(() => {
+ eventElement.remove();
+ })
+ .catch(alerts.error);
+ }
+ });
+ });
+
+ // Todo: should also use topicCommand, but no write api call exists for this yet
+ topicContainer.on('click', '[component="topic/mark-unread"]', () => {
+ socket.emit('topics.markUnread', tid, error => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ if (app.previousUrl && !app.previousUrl.match('^/topic')) {
+ ajaxify.go(app.previousUrl, () => {
+ handleBack.onBackClicked(true);
+ });
+ } else if (ajaxify.data.category) {
+ ajaxify.go('category/' + ajaxify.data.category.slug, handleBack.onBackClicked);
+ }
+
+ alerts.success('[[topic:mark_unread.success]]');
+ });
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/mark-unread-for-all"]', function () {
+ const button = $(this);
+ socket.emit('topics.markAsUnreadForAll', [tid], error => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ alerts.success('[[topic:markAsUnreadForAll.success]]');
+ button.parents('.thread-tools.open').find('.dropdown-toggle').trigger('click');
+ });
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/move"]', () => {
+ require(['forum/topic/move'], move => {
+ move.init([tid], ajaxify.data.cid);
+ });
+ return false;
+ });
+
+ topicContainer.on('click', '[component="topic/delete/posts"]', () => {
+ require(['forum/topic/delete-posts'], deletePosts => {
+ deletePosts.init();
+ });
+ });
+
+ topicContainer.on('click', '[component="topic/fork"]', () => {
+ require(['forum/topic/fork'], fork => {
+ fork.init();
+ });
+ });
+
+ topicContainer.on('click', '[component="topic/move-posts"]', () => {
+ require(['forum/topic/move-post'], movePosts => {
+ movePosts.init();
+ });
+ });
+
+ topicContainer.on('click', '[component="topic/following"]', () => {
+ changeWatching('follow');
+ });
+ topicContainer.on('click', '[component="topic/not-following"]', () => {
+ changeWatching('follow', 0);
+ });
+ topicContainer.on('click', '[component="topic/ignoring"]', () => {
+ 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,
+ type: 'success',
+ timeout: 5000,
+ });
+
+ hooks.fire('action:topics.changeWatching', {tid, type});
+ }, () => {
+ alerts.alert({
+ type: 'danger',
+ alert_id: 'topic_follow',
+ title: '[[global:please_log_in]]',
+ message: '[[topic:login_to_subscribe]]',
+ timeout: 5000,
+ });
+ });
+
+ return false;
+ }
+ };
+
+ function renderMenu(container) {
+ container.on('show.bs.dropdown', '.thread-tools', function () {
+ const $this = $(this);
+ const dropdownMenu = $this.find('.dropdown-menu');
+ if (dropdownMenu.html()) {
+ return;
+ }
+
+ dropdownMenu.toggleClass('hidden', true);
+ socket.emit('topics.loadTopicTools', {tid: ajaxify.data.tid, cid: ajaxify.data.cid}, (error, data) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ app.parseAndTranslate('partials/topic/topic-menu-list', data, html => {
+ dropdownMenu.html(html);
+ dropdownMenu.toggleClass('hidden', false);
+
+ hooks.fire('action:topic.tools.load', {
+ element: dropdownMenu,
+ });
+ });
+ });
+ });
+ }
+
+ function topicCommand(method, path, command, 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', {}, 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() {
+ const expiryElement = modal.get(0).querySelector('#expiry');
+ let expiry = expiryElement.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 threadElement = components.get('topic');
+ if (Number.parseInt(data.tid, 10) !== Number.parseInt(threadElement.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 = Boolean((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);
+
+ threadElement.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"]').toggleClass('hidden', hideReply);
+ threadElement.find('[component="post/edit"], [component="post/delete"]').toggleClass('hidden', isLocked);
+
+ threadElement.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.setPrivateState = function (data) {
+ const threadElement = components.get('topic');
+ if (Number.parseInt(data.tid, 10) !== Number.parseInt(threadElement.attr('data-tid'), 10)) {
+ return;
+ }
+
+ components.get('topic/private').toggleClass('hidden', data.isPrivate).parent().attr('hidden', data.isPrivate ? '' : null);
+ components.get('topic/public').toggleClass('hidden', !data.isPrivate).parent().attr('hidden', data.isPrivate ? null : '');
+
+ /* If (data.isPrivate) {
app.parseAndTranslate('partials/topic/privated-message', {
privater: data.user,
private: true,
@@ -328,93 +331,93 @@ define('forum/topic/threadTools', [
});
} */
- threadEl.toggleClass('private', data.isPrivate);
- ajaxify.data.private = data.isPrivate ? 1 : 0;
-
- posts.addTopicEvents(data.event);
- };
-
- 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) {
- $('[component="topic/watch"] button')
- .attr('title', translatedTitle)
- .tooltip('fixTitle');
- });
-
- 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;
+ threadElement.toggleClass('private', data.isPrivate);
+ ajaxify.data.private = data.isPrivate ? 1 : 0;
+
+ posts.addTopicEvents(data.event);
+ };
+
+ ThreadTools.setDeleteState = function (data) {
+ const threadElement = components.get('topic');
+ if (Number.parseInt(data.tid, 10) !== Number.parseInt(threadElement.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()),
+ }, 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);
+ threadElement.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"]').toggleClass('hidden', hideReply);
+
+ threadElement.toggleClass('deleted', data.isDelete);
+ ajaxify.data.deleted = data.isDelete ? 1 : 0;
+
+ posts.addTopicEvents(data.events);
+ };
+
+ ThreadTools.setPinnedState = function (data) {
+ const threadElement = components.get('topic');
+ if (Number.parseInt(data.tid, 10) !== Number.parseInt(threadElement.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], translatedTitle => {
+ $('[component="topic/watch"] button')
+ .attr('title', translatedTitle)
+ .tooltip('fixTitle');
+ });
+
+ 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;
});
diff --git a/public/src/client/topic/votes.js b/public/src/client/topic/votes.js
index 05632f2..a202164 100644
--- a/public/src/client/topic/votes.js
+++ b/public/src/client/topic/votes.js
@@ -1,110 +1,111 @@
'use strict';
-
define('forum/topic/votes', [
- 'components', 'translator', 'api', 'hooks', 'bootbox', 'alerts',
-], function (components, translator, api, hooks, bootbox, alerts) {
- const Votes = {};
-
- Votes.addVoteHandler = function () {
- components.get('topic').on('mouseenter', '[data-pid] [component="post/vote-count"]', loadDataAndCreateTooltip);
- };
-
- function loadDataAndCreateTooltip(e) {
- e.stopPropagation();
-
- const $this = $(this);
- const el = $this.parent();
- el.find('.tooltip').css('display', 'none');
- const pid = el.parents('[data-pid]').attr('data-pid');
-
- socket.emit('posts.getUpvoters', [pid], function (err, data) {
- if (err) {
- return alerts.error(err);
- }
-
- if (data.length) {
- createTooltip($this, data[0]);
- }
- });
- return false;
- }
-
- function createTooltip(el, data) {
- function doCreateTooltip(title) {
- el.attr('title', title).tooltip('fixTitle').tooltip('show');
- el.parent().find('.tooltip').css('display', '');
- }
- let usernames = data.usernames
- .filter(name => name !== '[[global:former_user]]');
- if (!usernames.length) {
- return;
- }
- if (usernames.length + data.otherCount > 6) {
- usernames = usernames.join(', ').replace(/,/g, '|');
- translator.translate('[[topic:users_and_others, ' + usernames + ', ' + data.otherCount + ']]', function (translated) {
- translated = translated.replace(/\|/g, ',');
- doCreateTooltip(translated);
- });
- } else {
- usernames = usernames.join(', ');
- doCreateTooltip(usernames);
- }
- }
-
-
- Votes.toggleVote = function (button, className, delta) {
- const post = button.closest('[data-pid]');
- const currentState = post.find(className).length;
-
- const method = currentState ? 'del' : 'put';
- const pid = post.attr('data-pid');
- api[method](`/posts/${pid}/vote`, {
- delta: delta,
- }, function (err) {
- if (err) {
- if (!app.user.uid) {
- ajaxify.go('login');
- return;
- }
- return alerts.error(err);
- }
- hooks.fire('action:post.toggleVote', {
- pid: pid,
- delta: delta,
- unvote: method === 'del',
- });
- });
-
- return false;
- };
-
- Votes.showVotes = function (pid) {
- socket.emit('posts.getVoters', { pid: pid, cid: ajaxify.data.cid }, function (err, data) {
- if (err) {
- if (err.message === '[[error:no-privileges]]') {
- return;
- }
-
- // Only show error if it's an unexpected error.
- return alerts.error(err);
- }
-
- app.parseAndTranslate('partials/modals/votes_modal', data, function (html) {
- const dialog = bootbox.dialog({
- title: '[[global:voters]]',
- message: html,
- className: 'vote-modal',
- show: true,
- });
-
- dialog.on('click', function () {
- dialog.modal('hide');
- });
- });
- });
- };
-
-
- return Votes;
+ 'components', 'translator', 'api', 'hooks', 'bootbox', 'alerts',
+], (components, translator, api, hooks, bootbox, alerts) => {
+ const Votes = {};
+
+ Votes.addVoteHandler = function () {
+ components.get('topic').on('mouseenter', '[data-pid] [component="post/vote-count"]', loadDataAndCreateTooltip);
+ };
+
+ function loadDataAndCreateTooltip(e) {
+ e.stopPropagation();
+
+ const $this = $(this);
+ const element = $this.parent();
+ element.find('.tooltip').css('display', 'none');
+ const pid = element.parents('[data-pid]').attr('data-pid');
+
+ socket.emit('posts.getUpvoters', [pid], (error, data) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ if (data.length > 0) {
+ createTooltip($this, data[0]);
+ }
+ });
+ return false;
+ }
+
+ function createTooltip(element, data) {
+ function doCreateTooltip(title) {
+ element.attr('title', title).tooltip('fixTitle').tooltip('show');
+ element.parent().find('.tooltip').css('display', '');
+ }
+
+ let usernames = data.usernames
+ .filter(name => name !== '[[global:former_user]]');
+ if (usernames.length === 0) {
+ return;
+ }
+
+ if (usernames.length + data.otherCount > 6) {
+ usernames = usernames.join(', ').replaceAll(',', '|');
+ translator.translate('[[topic:users_and_others, ' + usernames + ', ' + data.otherCount + ']]', translated => {
+ translated = translated.replaceAll('|', ',');
+ doCreateTooltip(translated);
+ });
+ } else {
+ usernames = usernames.join(', ');
+ doCreateTooltip(usernames);
+ }
+ }
+
+ Votes.toggleVote = function (button, className, delta) {
+ const post = button.closest('[data-pid]');
+ const currentState = post.find(className).length;
+
+ const method = currentState ? 'del' : 'put';
+ const pid = post.attr('data-pid');
+ api[method](`/posts/${pid}/vote`, {
+ delta,
+ }, error => {
+ if (error) {
+ if (!app.user.uid) {
+ ajaxify.go('login');
+ return;
+ }
+
+ return alerts.error(error);
+ }
+
+ hooks.fire('action:post.toggleVote', {
+ pid,
+ delta,
+ unvote: method === 'del',
+ });
+ });
+
+ return false;
+ };
+
+ Votes.showVotes = function (pid) {
+ socket.emit('posts.getVoters', {pid, cid: ajaxify.data.cid}, (error, data) => {
+ if (error) {
+ if (error.message === '[[error:no-privileges]]') {
+ return;
+ }
+
+ // Only show error if it's an unexpected error.
+ return alerts.error(error);
+ }
+
+ app.parseAndTranslate('partials/modals/votes_modal', data, html => {
+ const dialog = bootbox.dialog({
+ title: '[[global:voters]]',
+ message: html,
+ className: 'vote-modal',
+ show: true,
+ });
+
+ dialog.on('click', () => {
+ dialog.modal('hide');
+ });
+ });
+ });
+ };
+
+ return Votes;
});
diff --git a/public/src/client/unread.js b/public/src/client/unread.js
index 8d7b529..78d1136 100644
--- a/public/src/client/unread.js
+++ b/public/src/client/unread.js
@@ -1,112 +1,114 @@
'use strict';
-
define('forum/unread', [
- 'forum/header/unread', 'topicSelect', 'components', 'topicList', 'categorySelector', 'alerts',
-], function (headerUnread, topicSelect, components, topicList, categorySelector, alerts) {
- const Unread = {};
-
- Unread.init = function () {
- app.enterRoom('unread_topics');
-
- handleMarkRead();
-
- topicList.init('unread');
-
- headerUnread.updateUnreadTopicCount('/' + ajaxify.data.selectedFilter.url, ajaxify.data.topicCount);
- };
-
- function handleMarkRead() {
- function markAllRead() {
- socket.emit('topics.markAllRead', function (err) {
- if (err) {
- return alerts.error(err);
- }
-
- alerts.success('[[unread:topics_marked_as_read.success]]');
-
- $('[component="category"]').empty();
- $('[component="pagination"]').addClass('hidden');
- $('#category-no-topics').removeClass('hidden');
- $('.markread').addClass('hidden');
- });
- }
-
- function markSelectedRead() {
- const tids = topicSelect.getSelectedTids();
- if (!tids.length) {
- return;
- }
- socket.emit('topics.markAsRead', tids, function (err) {
- if (err) {
- return alerts.error(err);
- }
-
- doneRemovingTids(tids);
- });
- }
-
- function markCategoryRead(cid) {
- function getCategoryTids(cid) {
- const tids = [];
- components.get('category/topic', 'cid', cid).each(function () {
- tids.push($(this).attr('data-tid'));
- });
- return tids;
- }
- const tids = getCategoryTids(cid);
-
- socket.emit('topics.markCategoryTopicsRead', cid, function (err) {
- if (err) {
- return alerts.error(err);
- }
-
- doneRemovingTids(tids);
- });
- }
- const selector = categorySelector.init($('[component="category-selector"]'), {
- onSelect: function (category) {
- selector.selectCategory(0);
- if (category.cid === 'all') {
- markAllRead();
- } else if (category.cid === 'selected') {
- markSelectedRead();
- } else if (parseInt(category.cid, 10) > 0) {
- markCategoryRead(category.cid);
- }
- },
- selectCategoryLabel: ajaxify.data.selectCategoryLabel || '[[unread:mark_as_read]]',
- localCategories: [
- {
- cid: 'selected',
- name: '[[unread:selected]]',
- icon: '',
- },
- {
- cid: 'all',
- name: '[[unread:all]]',
- icon: '',
- },
- ],
- });
- }
-
- function doneRemovingTids(tids) {
- removeTids(tids);
-
- alerts.success('[[unread:topics_marked_as_read.success]]');
-
- if (!$('[component="category"]').children().length) {
- $('#category-no-topics').removeClass('hidden');
- $('.markread').addClass('hidden');
- }
- }
-
- function removeTids(tids) {
- for (let i = 0; i < tids.length; i += 1) {
- components.get('category/topic', 'tid', tids[i]).remove();
- }
- }
-
- return Unread;
+ 'forum/header/unread', 'topicSelect', 'components', 'topicList', 'categorySelector', 'alerts',
+], (headerUnread, topicSelect, components, topicList, categorySelector, alerts) => {
+ const Unread = {};
+
+ Unread.init = function () {
+ app.enterRoom('unread_topics');
+
+ handleMarkRead();
+
+ topicList.init('unread');
+
+ headerUnread.updateUnreadTopicCount('/' + ajaxify.data.selectedFilter.url, ajaxify.data.topicCount);
+ };
+
+ function handleMarkRead() {
+ function markAllRead() {
+ socket.emit('topics.markAllRead', error => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ alerts.success('[[unread:topics_marked_as_read.success]]');
+
+ $('[component="category"]').empty();
+ $('[component="pagination"]').addClass('hidden');
+ $('#category-no-topics').removeClass('hidden');
+ $('.markread').addClass('hidden');
+ });
+ }
+
+ function markSelectedRead() {
+ const tids = topicSelect.getSelectedTids();
+ if (tids.length === 0) {
+ return;
+ }
+
+ socket.emit('topics.markAsRead', tids, error => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ doneRemovingTids(tids);
+ });
+ }
+
+ function markCategoryRead(cid) {
+ function getCategoryTids(cid) {
+ const tids = [];
+ components.get('category/topic', 'cid', cid).each(function () {
+ tids.push($(this).attr('data-tid'));
+ });
+ return tids;
+ }
+
+ const tids = getCategoryTids(cid);
+
+ socket.emit('topics.markCategoryTopicsRead', cid, error => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ doneRemovingTids(tids);
+ });
+ }
+
+ const selector = categorySelector.init($('[component="category-selector"]'), {
+ onSelect(category) {
+ selector.selectCategory(0);
+ if (category.cid === 'all') {
+ markAllRead();
+ } else if (category.cid === 'selected') {
+ markSelectedRead();
+ } else if (Number.parseInt(category.cid, 10) > 0) {
+ markCategoryRead(category.cid);
+ }
+ },
+ selectCategoryLabel: ajaxify.data.selectCategoryLabel || '[[unread:mark_as_read]]',
+ localCategories: [
+ {
+ cid: 'selected',
+ name: '[[unread:selected]]',
+ icon: '',
+ },
+ {
+ cid: 'all',
+ name: '[[unread:all]]',
+ icon: '',
+ },
+ ],
+ });
+ }
+
+ function doneRemovingTids(tids) {
+ removeTids(tids);
+
+ alerts.success('[[unread:topics_marked_as_read.success]]');
+
+ if ($('[component="category"]').children().length === 0) {
+ $('#category-no-topics').removeClass('hidden');
+ $('.markread').addClass('hidden');
+ }
+ }
+
+ function removeTids(tids) {
+ for (const tid of tids) {
+ components.get('category/topic', 'tid', tid).remove();
+ }
+ }
+
+ return Unread;
});
diff --git a/public/src/client/users.js b/public/src/client/users.js
index 3e36b1f..d8df77f 100644
--- a/public/src/client/users.js
+++ b/public/src/client/users.js
@@ -1,122 +1,138 @@
'use strict';
-
define('forum/users', [
- 'translator', 'benchpress', 'api', 'alerts', 'accounts/invite',
-], function (translator, Benchpress, api, alerts, AccountInvite) {
- const Users = {};
-
- let searchResultCount = 0;
-
- Users.init = function () {
- app.enterRoom('user_list');
-
- const section = utils.param('section') ? ('?section=' + utils.param('section')) : '';
- $('.nav-pills li').removeClass('active').find('a[href="' + window.location.pathname + section + '"]').parent()
- .addClass('active');
-
- Users.handleSearch();
-
- AccountInvite.handle();
-
- socket.removeListener('event:user_status_change', onUserStatusChange);
- socket.on('event:user_status_change', onUserStatusChange);
- };
-
- Users.handleSearch = function (params) {
- searchResultCount = params && params.resultCount;
- $('#search-user').on('keyup', utils.debounce(doSearch, 250));
- $('.search select, .search input[type="checkbox"]').on('change', doSearch);
- };
-
- function doSearch() {
- if (!ajaxify.data.template.users) {
- return;
- }
- $('[component="user/search/icon"]').removeClass('fa-search').addClass('fa-spinner fa-spin');
- const username = $('#search-user').val();
- const activeSection = getActiveSection();
-
- const query = {
- section: activeSection,
- page: 1,
- };
-
- if (!username) {
- return loadPage(query);
- }
-
- query.query = username;
- query.sortBy = getSortBy();
- const filters = [];
- if ($('.search .online-only').is(':checked') || (activeSection === 'online')) {
- filters.push('online');
- }
- if (activeSection === 'banned') {
- filters.push('banned');
- }
- if (activeSection === 'flagged') {
- filters.push('flagged');
- }
- if (filters.length) {
- query.filters = filters;
- }
-
- loadPage(query);
- }
-
- function getSortBy() {
- let sortBy;
- const activeSection = getActiveSection();
- if (activeSection === 'sort-posts') {
- sortBy = 'postcount';
- } else if (activeSection === 'sort-reputation') {
- sortBy = 'reputation';
- } else if (activeSection === 'users') {
- sortBy = 'joindate';
- }
- return sortBy;
- }
-
-
- function loadPage(query) {
- api.get('/api/users', query)
- .then(renderSearchResults)
- .catch(alerts.error);
- }
-
- function renderSearchResults(data) {
- Benchpress.render('partials/paginator', { pagination: data.pagination }).then(function (html) {
- $('.pagination-container').replaceWith(html);
- });
-
- if (searchResultCount) {
- data.users = data.users.slice(0, searchResultCount);
- }
-
- data.isAdminOrGlobalMod = app.user.isAdmin || app.user.isGlobalMod;
- app.parseAndTranslate('users', 'users', data, function (html) {
- $('#users-container').html(html);
- html.find('span.timeago').timeago();
- $('[component="user/search/icon"]').addClass('fa-search').removeClass('fa-spinner fa-spin');
- });
- }
-
- function onUserStatusChange(data) {
- const section = getActiveSection();
-
- if ((section.startsWith('online') || section.startsWith('users'))) {
- updateUser(data);
- }
- }
-
- function updateUser(data) {
- app.updateUserStatus($('#users-container [data-uid="' + data.uid + '"] [component="user/status"]'), data.status);
- }
-
- function getActiveSection() {
- return utils.param('section') || '';
- }
-
- return Users;
+ 'translator', 'benchpress', 'api', 'alerts', 'accounts/invite',
+], (translator, Benchpress, api, alerts, AccountInvite) => {
+ const Users = {};
+
+ let searchResultCount = 0;
+
+ Users.init = function () {
+ app.enterRoom('user_list');
+
+ const section = utils.param('section') ? ('?section=' + utils.param('section')) : '';
+ $('.nav-pills li').removeClass('active').find('a[href="' + window.location.pathname + section + '"]').parent()
+ .addClass('active');
+
+ Users.handleSearch();
+
+ AccountInvite.handle();
+
+ socket.removeListener('event:user_status_change', onUserStatusChange);
+ socket.on('event:user_status_change', onUserStatusChange);
+ };
+
+ Users.handleSearch = function (parameters) {
+ searchResultCount = parameters && parameters.resultCount;
+ $('#search-user').on('keyup', utils.debounce(doSearch, 250));
+ $('.search select, .search input[type="checkbox"]').on('change', doSearch);
+ };
+
+ function doSearch() {
+ if (!ajaxify.data.template.users) {
+ return;
+ }
+
+ $('[component="user/search/icon"]').removeClass('fa-search').addClass('fa-spinner fa-spin');
+ const username = $('#search-user').val();
+ const activeSection = getActiveSection();
+
+ const query = {
+ section: activeSection,
+ page: 1,
+ };
+
+ if (!username) {
+ return loadPage(query);
+ }
+
+ query.query = username;
+ query.sortBy = getSortBy();
+ const filters = [];
+ if ($('.search .online-only').is(':checked') || (activeSection === 'online')) {
+ filters.push('online');
+ }
+
+ if (activeSection === 'banned') {
+ filters.push('banned');
+ }
+
+ if (activeSection === 'flagged') {
+ filters.push('flagged');
+ }
+
+ if (filters.length > 0) {
+ query.filters = filters;
+ }
+
+ loadPage(query);
+ }
+
+ function getSortBy() {
+ let sortBy;
+ const activeSection = getActiveSection();
+ switch (activeSection) {
+ case 'sort-posts': {
+ sortBy = 'postcount';
+
+ break;
+ }
+
+ case 'sort-reputation': {
+ sortBy = 'reputation';
+
+ break;
+ }
+
+ case 'users': {
+ sortBy = 'joindate';
+
+ break;
+ }
+ // No default
+ }
+
+ return sortBy;
+ }
+
+ function loadPage(query) {
+ api.get('/api/users', query)
+ .then(renderSearchResults)
+ .catch(alerts.error);
+ }
+
+ function renderSearchResults(data) {
+ Benchpress.render('partials/paginator', {pagination: data.pagination}).then(html => {
+ $('.pagination-container').replaceWith(html);
+ });
+
+ if (searchResultCount) {
+ data.users = data.users.slice(0, searchResultCount);
+ }
+
+ data.isAdminOrGlobalMod = app.user.isAdmin || app.user.isGlobalMod;
+ app.parseAndTranslate('users', 'users', data, html => {
+ $('#users-container').html(html);
+ html.find('span.timeago').timeago();
+ $('[component="user/search/icon"]').addClass('fa-search').removeClass('fa-spinner fa-spin');
+ });
+ }
+
+ function onUserStatusChange(data) {
+ const section = getActiveSection();
+
+ if ((section.startsWith('online') || section.startsWith('users'))) {
+ updateUser(data);
+ }
+ }
+
+ function updateUser(data) {
+ app.updateUserStatus($('#users-container [data-uid="' + data.uid + '"] [component="user/status"]'), data.status);
+ }
+
+ function getActiveSection() {
+ return utils.param('section') || '';
+ }
+
+ return Users;
});
diff --git a/public/src/installer/install.js b/public/src/installer/install.js
index bf6b560..0526c1f 100644
--- a/public/src/installer/install.js
+++ b/public/src/installer/install.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-redeclare */
'use strict';
@@ -7,138 +6,148 @@ const zxcvbn = require('zxcvbn');
const utils = require('../utils');
const slugify = require('../modules/slugify');
-$('document').ready(function () {
- setupInputs();
- $('[name="username"]').focus();
-
- activate('database', $('[name="database"]'));
-
- if ($('#database-error').length) {
- $('[name="database"]').parents('.input-row').addClass('error');
- $('html, body').animate({
- scrollTop: ($('#database-error').offset().top + 100) + 'px',
- }, 400);
- }
-
- $('#launch').on('click', launchForum);
-
- if ($('#installing').length) {
- setTimeout(function () {
- window.location.reload(true);
- }, 5000);
- }
-
- function setupInputs() {
- $('form').on('focus', '.form-control', function () {
- const parent = $(this).parents('.input-row');
-
- $('.input-row.active').removeClass('active');
- parent.addClass('active').removeClass('error');
-
- const help = parent.find('.help-text');
- help.html(help.attr('data-help'));
- });
-
- $('form').on('blur change', '[name]', function () {
- activate($(this).attr('name'), $(this));
- });
-
- $('form').submit(validateAll);
- }
-
- function validateAll(ev) {
- $('form .admin [name]').each(function () {
- activate($(this).attr('name'), $(this));
- });
-
- if ($('form .admin .error').length) {
- ev.preventDefault();
- $('html, body').animate({ scrollTop: '0px' }, 400);
-
- return false;
- }
- $('#submit .working').removeClass('hide');
- }
-
- function activate(type, el) {
- const field = el.val();
- const parent = el.parents('.input-row');
- const help = parent.children('.help-text');
-
- function validateUsername(field) {
- if (!utils.isUserNameValid(field) || !slugify(field)) {
- parent.addClass('error');
- help.html('Invalid Username.');
- } else {
- parent.removeClass('error');
- }
- }
-
- function validatePassword(field) {
- if (!utils.isPasswordValid(field)) {
- parent.addClass('error');
- help.html('Invalid Password.');
- } else if (field.length < $('[name="admin:password"]').attr('data-minimum-length')) {
- parent.addClass('error');
- help.html('Password is too short.');
- } else if (zxcvbn(field).score < parseInt($('[name="admin:password"]').attr('data-minimum-strength'), 10)) {
- parent.addClass('error');
- help.html('Password is too weak.');
- } else {
- parent.removeClass('error');
- }
- }
-
- function validateConfirmPassword() {
- if ($('[name="admin:password"]').val() !== $('[name="admin:passwordConfirm"]').val()) {
- parent.addClass('error');
- help.html('Passwords do not match.');
- } else {
- parent.removeClass('error');
- }
- }
-
- function validateEmail(field) {
- if (!utils.isEmailValid(field)) {
- parent.addClass('error');
- help.html('Invalid Email Address.');
- } else {
- parent.removeClass('error');
- }
- }
-
- function switchDatabase(field) {
- $('#database-config').html($('[data-database="' + field + '"]').html());
- }
-
- switch (type) {
- case 'admin:username':
- return validateUsername(field);
- case 'admin:password':
- return validatePassword(field);
- case 'admin:passwordConfirm':
- return validateConfirmPassword(field);
- case 'admin:email':
- return validateEmail(field);
- case 'database':
- return switchDatabase(field);
- }
- }
-
- function launchForum() {
- $('#launch .working').removeClass('hide');
- $.post('/launch', function () {
- let successCount = 0;
- const url = $('#launch').attr('data-url');
- setInterval(function () {
- $.get(url + '/admin').done(function () {
- if (successCount >= 5) {
- window.location = 'admin';
- } else {
- successCount += 1;
- }
- });
- }, 750);
- });
- }
+$('document').ready(() => {
+ setupInputs();
+ $('[name="username"]').focus();
+
+ activate('database', $('[name="database"]'));
+
+ if ($('#database-error').length > 0) {
+ $('[name="database"]').parents('.input-row').addClass('error');
+ $('html, body').animate({
+ scrollTop: ($('#database-error').offset().top + 100) + 'px',
+ }, 400);
+ }
+
+ $('#launch').on('click', launchForum);
+
+ if ($('#installing').length > 0) {
+ setTimeout(() => {
+ window.location.reload(true);
+ }, 5000);
+ }
+
+ function setupInputs() {
+ $('form').on('focus', '.form-control', function () {
+ const parent = $(this).parents('.input-row');
+
+ $('.input-row.active').removeClass('active');
+ parent.addClass('active').removeClass('error');
+
+ const help = parent.find('.help-text');
+ help.html(help.attr('data-help'));
+ });
+
+ $('form').on('blur change', '[name]', function () {
+ activate($(this).attr('name'), $(this));
+ });
+
+ $('form').submit(validateAll);
+ }
+
+ function validateAll(event) {
+ $('form .admin [name]').each(function () {
+ activate($(this).attr('name'), $(this));
+ });
+
+ if ($('form .admin .error').length > 0) {
+ event.preventDefault();
+ $('html, body').animate({scrollTop: '0px'}, 400);
+
+ return false;
+ }
+
+ $('#submit .working').removeClass('hide');
+ }
+
+ function activate(type, element) {
+ const field = element.val();
+ const parent = element.parents('.input-row');
+ const help = parent.children('.help-text');
+
+ function validateUsername(field) {
+ if (!utils.isUserNameValid(field) || !slugify(field)) {
+ parent.addClass('error');
+ help.html('Invalid Username.');
+ } else {
+ parent.removeClass('error');
+ }
+ }
+
+ function validatePassword(field) {
+ if (!utils.isPasswordValid(field)) {
+ parent.addClass('error');
+ help.html('Invalid Password.');
+ } else if (field.length < $('[name="admin:password"]').attr('data-minimum-length')) {
+ parent.addClass('error');
+ help.html('Password is too short.');
+ } else if (zxcvbn(field).score < Number.parseInt($('[name="admin:password"]').attr('data-minimum-strength'), 10)) {
+ parent.addClass('error');
+ help.html('Password is too weak.');
+ } else {
+ parent.removeClass('error');
+ }
+ }
+
+ function validateConfirmPassword() {
+ if ($('[name="admin:password"]').val() === $('[name="admin:passwordConfirm"]').val()) {
+ parent.removeClass('error');
+ } else {
+ parent.addClass('error');
+ help.html('Passwords do not match.');
+ }
+ }
+
+ function validateEmail(field) {
+ if (utils.isEmailValid(field)) {
+ parent.removeClass('error');
+ } else {
+ parent.addClass('error');
+ help.html('Invalid Email Address.');
+ }
+ }
+
+ function switchDatabase(field) {
+ $('#database-config').html($('[data-database="' + field + '"]').html());
+ }
+
+ switch (type) {
+ case 'admin:username': {
+ return validateUsername(field);
+ }
+
+ case 'admin:password': {
+ return validatePassword(field);
+ }
+
+ case 'admin:passwordConfirm': {
+ return validateConfirmPassword(field);
+ }
+
+ case 'admin:email': {
+ return validateEmail(field);
+ }
+
+ case 'database': {
+ return switchDatabase(field);
+ }
+ }
+ }
+
+ function launchForum() {
+ $('#launch .working').removeClass('hide');
+ $.post('/launch', () => {
+ let successCount = 0;
+ const url = $('#launch').attr('data-url');
+ setInterval(() => {
+ $.get(url + '/admin').done(() => {
+ if (successCount >= 5) {
+ window.location = 'admin';
+ } else {
+ successCount += 1;
+ }
+ });
+ }, 750);
+ });
+ }
});
diff --git a/public/src/modules/accounts/delete.js b/public/src/modules/accounts/delete.js
index e8f16de..0eb1b83 100644
--- a/public/src/modules/accounts/delete.js
+++ b/public/src/modules/accounts/delete.js
@@ -1,53 +1,53 @@
'use strict';
-define('accounts/delete', ['api', 'bootbox', 'alerts'], function (api, bootbox, alerts) {
- const Delete = {};
-
- Delete.account = function (uid, callback) {
- executeAction(
- uid,
- '[[user:delete_this_account_confirm]]',
- '/account',
- '[[user:account-deleted]]',
- callback
- );
- };
-
- Delete.content = function (uid, callback) {
- executeAction(
- uid,
- '[[user:delete_account_content_confirm]]',
- '/content',
- '[[user:account-content-deleted]]',
- callback
- );
- };
-
- Delete.purge = function (uid, callback) {
- executeAction(
- uid,
- '[[user:delete_all_confirm]]',
- '',
- '[[user:account-deleted]]',
- callback
- );
- };
-
- function executeAction(uid, confirmText, path, successText, callback) {
- bootbox.confirm(confirmText, function (confirm) {
- if (!confirm) {
- return;
- }
-
- api.del(`/users/${uid}${path}`, {}).then(() => {
- alerts.success(successText);
-
- if (typeof callback === 'function') {
- return callback();
- }
- }).catch(alerts.error);
- });
- }
-
- return Delete;
+define('accounts/delete', ['api', 'bootbox', 'alerts'], (api, bootbox, alerts) => {
+ const Delete = {};
+
+ Delete.account = function (uid, callback) {
+ executeAction(
+ uid,
+ '[[user:delete_this_account_confirm]]',
+ '/account',
+ '[[user:account-deleted]]',
+ callback,
+ );
+ };
+
+ Delete.content = function (uid, callback) {
+ executeAction(
+ uid,
+ '[[user:delete_account_content_confirm]]',
+ '/content',
+ '[[user:account-content-deleted]]',
+ callback,
+ );
+ };
+
+ Delete.purge = function (uid, callback) {
+ executeAction(
+ uid,
+ '[[user:delete_all_confirm]]',
+ '',
+ '[[user:account-deleted]]',
+ callback,
+ );
+ };
+
+ function executeAction(uid, confirmText, path, successText, callback) {
+ bootbox.confirm(confirmText, confirm => {
+ if (!confirm) {
+ return;
+ }
+
+ api.del(`/users/${uid}${path}`, {}).then(() => {
+ alerts.success(successText);
+
+ if (typeof callback === 'function') {
+ return callback();
+ }
+ }).catch(alerts.error);
+ });
+ }
+
+ return Delete;
});
diff --git a/public/src/modules/accounts/invite.js b/public/src/modules/accounts/invite.js
index 95f6177..867f7fa 100644
--- a/public/src/modules/accounts/invite.js
+++ b/public/src/modules/accounts/invite.js
@@ -1,60 +1,60 @@
'use strict';
-define('accounts/invite', ['api', 'benchpress', 'bootbox', 'alerts'], function (api, Benchpress, bootbox, alerts) {
- const Invite = {};
-
- function isACP() {
- return ajaxify.data.template.name.startsWith('admin/');
- }
-
- Invite.handle = function () {
- $('[component="user/invite"]').on('click', function (e) {
- e.preventDefault();
- api.get(`/api/v3/users/${app.user.uid}/invites/groups`, {}).then((groups) => {
- Benchpress.parse('modals/invite', { groups: groups }, function (html) {
- bootbox.dialog({
- message: html,
- title: `[[${isACP() ? 'admin/manage/users:invite' : 'users:invite'}]]`,
- onEscape: true,
- buttons: {
- cancel: {
- label: `[[${isACP() ? 'admin/manage/users:alerts.button-cancel' : 'modules:bootbox.cancel'}]]`,
- className: 'btn-default',
- },
- invite: {
- label: `[[${isACP() ? 'admin/manage/users:invite' : 'users:invite'}]]`,
- className: 'btn-primary',
- callback: Invite.send,
- },
- },
- });
- });
- }).catch(alerts.error);
- });
- };
-
- Invite.send = function () {
- const $emails = $('#invite-modal-emails');
- const $groups = $('#invite-modal-groups');
-
- const data = {
- emails: $emails.val()
- .split(',')
- .map(m => m.trim())
- .filter(Boolean)
- .filter((m, i, arr) => i === arr.indexOf(m))
- .join(','),
- groupsToJoin: $groups.val(),
- };
-
- if (!data.emails) {
- return;
- }
-
- api.post(`/users/${app.user.uid}/invites`, data).then(() => {
- alerts.success(`[[${isACP() ? 'admin/manage/users:alerts.email-sent-to' : 'users:invitation-email-sent'}, ${data.emails.replace(/,/g, ', ')}]]`);
- }).catch(alerts.error);
- };
-
- return Invite;
+define('accounts/invite', ['api', 'benchpress', 'bootbox', 'alerts'], (api, Benchpress, bootbox, alerts) => {
+ const Invite = {};
+
+ function isACP() {
+ return ajaxify.data.template.name.startsWith('admin/');
+ }
+
+ Invite.handle = function () {
+ $('[component="user/invite"]').on('click', e => {
+ e.preventDefault();
+ api.get(`/api/v3/users/${app.user.uid}/invites/groups`, {}).then(groups => {
+ Benchpress.parse('modals/invite', {groups}, html => {
+ bootbox.dialog({
+ message: html,
+ title: `[[${isACP() ? 'admin/manage/users:invite' : 'users:invite'}]]`,
+ onEscape: true,
+ buttons: {
+ cancel: {
+ label: `[[${isACP() ? 'admin/manage/users:alerts.button-cancel' : 'modules:bootbox.cancel'}]]`,
+ className: 'btn-default',
+ },
+ invite: {
+ label: `[[${isACP() ? 'admin/manage/users:invite' : 'users:invite'}]]`,
+ className: 'btn-primary',
+ callback: Invite.send,
+ },
+ },
+ });
+ });
+ }).catch(alerts.error);
+ });
+ };
+
+ Invite.send = function () {
+ const $emails = $('#invite-modal-emails');
+ const $groups = $('#invite-modal-groups');
+
+ const data = {
+ emails: $emails.val()
+ .split(',')
+ .map(m => m.trim())
+ .filter(Boolean)
+ .filter((m, i, array) => i === array.indexOf(m))
+ .join(','),
+ groupsToJoin: $groups.val(),
+ };
+
+ if (!data.emails) {
+ return;
+ }
+
+ api.post(`/users/${app.user.uid}/invites`, data).then(() => {
+ alerts.success(`[[${isACP() ? 'admin/manage/users:alerts.email-sent-to' : 'users:invitation-email-sent'}, ${data.emails.replaceAll(',', ', ')}]]`);
+ }).catch(alerts.error);
+ };
+
+ return Invite;
});
diff --git a/public/src/modules/accounts/picture.js b/public/src/modules/accounts/picture.js
index 0d905c1..aaad5c0 100644
--- a/public/src/modules/accounts/picture.js
+++ b/public/src/modules/accounts/picture.js
@@ -1,219 +1,220 @@
'use strict';
define('accounts/picture', [
- 'pictureCropper',
- 'api',
- 'bootbox',
- 'alerts',
+ 'pictureCropper',
+ 'api',
+ 'bootbox',
+ 'alerts',
], (pictureCropper, api, bootbox, alerts) => {
- const Picture = {};
-
- Picture.openChangeModal = () => {
- socket.emit('user.getProfilePictures', {
- uid: ajaxify.data.uid,
- }, function (err, pictures) {
- if (err) {
- return alerts.error(err);
- }
-
- // boolean to signify whether an uploaded picture is present in the pictures list
- const uploaded = pictures.reduce(function (memo, cur) {
- return memo || cur.type === 'uploaded';
- }, false);
-
- app.parseAndTranslate('partials/modals/change_picture_modal', {
- pictures: pictures,
- uploaded: uploaded,
- icon: { text: ajaxify.data['icon:text'], bgColor: ajaxify.data['icon:bgColor'] },
- defaultAvatar: ajaxify.data.defaultAvatar,
- allowProfileImageUploads: ajaxify.data.allowProfileImageUploads,
- iconBackgrounds: config.iconBackgrounds,
- user: {
- uid: ajaxify.data.uid,
- username: ajaxify.data.username,
- picture: ajaxify.data.picture,
- 'icon:text': ajaxify.data['icon:text'],
- 'icon:bgColor': ajaxify.data['icon:bgColor'],
- },
- }, function (html) {
- const modal = bootbox.dialog({
- className: 'picture-switcher',
- title: '[[user:change_picture]]',
- message: html,
- show: true,
- buttons: {
- close: {
- label: '[[global:close]]',
- callback: onCloseModal,
- className: 'btn-link',
- },
- update: {
- label: '[[global:save_changes]]',
- callback: saveSelection,
- },
- },
- });
-
- modal.on('shown.bs.modal', updateImages);
- modal.on('click', '.list-group-item', function selectImageType() {
- modal.find('.list-group-item').removeClass('active');
- $(this).addClass('active');
- });
- modal.on('change', 'input[type="radio"][name="icon:bgColor"]', (e) => {
- const value = e.target.value;
- modal.find('.user-icon').css('background-color', value);
- });
-
- handleImageUpload(modal);
-
- function updateImages() {
- // Check to see which one is the active picture
- if (!ajaxify.data.picture) {
- modal.find('.list-group-item .user-icon').parents('.list-group-item').addClass('active');
- } else {
- modal.find('.list-group-item img').each(function () {
- if (this.getAttribute('src') === ajaxify.data.picture) {
- $(this).parents('.list-group-item').addClass('active');
- }
- });
- }
-
- // Update avatar background colour
- const radioEl = document.querySelector(`.modal input[type="radio"][value="${ajaxify.data['icon:bgColor']}"]`);
- if (radioEl) {
- radioEl.checked = true;
- } else {
- // Check the first one
- document.querySelector('.modal input[type="radio"]').checked = true;
- }
- }
-
- function saveSelection() {
- const type = modal.find('.list-group-item.active').attr('data-type');
- const iconBgColor = document.querySelector('.modal.picture-switcher input[type="radio"]:checked').value || 'transparent';
-
- changeUserPicture(type, iconBgColor).then(() => {
- Picture.updateHeader(type === 'default' ? '' : modal.find('.list-group-item.active img').attr('src'), iconBgColor);
- ajaxify.refresh();
- }).catch(alerts.error);
- }
-
- function onCloseModal() {
- modal.modal('hide');
- }
- });
- });
- };
-
- Picture.updateHeader = (picture, iconBgColor) => {
- if (parseInt(ajaxify.data.theirid, 10) !== parseInt(ajaxify.data.yourid, 10)) {
- return;
- }
- if (!picture && ajaxify.data.defaultAvatar) {
- picture = ajaxify.data.defaultAvatar;
- }
- $('#header [component="avatar/picture"]')[picture ? 'show' : 'hide']();
- $('#header [component="avatar/icon"]')[!picture ? 'show' : 'hide']();
- if (picture) {
- $('#header [component="avatar/picture"]').attr('src', picture);
- }
-
- if (iconBgColor) {
- document.querySelectorAll('[component="navbar"] [component="avatar/icon"]').forEach((el) => {
- el.style['background-color'] = iconBgColor;
- });
- }
- };
-
- function handleImageUpload(modal) {
- function onUploadComplete(urlOnServer) {
- urlOnServer = (!urlOnServer.startsWith('http') ? config.relative_path : '') + urlOnServer + '?' + Date.now();
-
- Picture.updateHeader(urlOnServer);
-
- if (ajaxify.data.picture && ajaxify.data.picture.length) {
- $('#user-current-picture, img.avatar').attr('src', urlOnServer);
- ajaxify.data.uploadedpicture = urlOnServer;
- } else {
- ajaxify.refresh(function () {
- $('#user-current-picture, img.avatar').attr('src', urlOnServer);
- });
- }
- }
-
- function onRemoveComplete() {
- if (ajaxify.data.uploadedpicture === ajaxify.data.picture) {
- ajaxify.refresh();
- Picture.updateHeader();
- }
- }
-
- modal.find('[data-action="upload"]').on('click', function () {
- modal.modal('hide');
-
- pictureCropper.show({
- socketMethod: 'user.uploadCroppedPicture',
- route: config.relative_path + '/api/user/' + ajaxify.data.userslug + '/uploadpicture',
- aspectRatio: 1 / 1,
- paramName: 'uid',
- paramValue: ajaxify.data.theirid,
- fileSize: ajaxify.data.maximumProfileImageSize,
- allowSkippingCrop: false,
- title: '[[user:upload_picture]]',
- description: '[[user:upload_a_picture]]',
- accept: ajaxify.data.allowedProfileImageExtensions,
- }, function (url) {
- onUploadComplete(url);
- });
-
- return false;
- });
-
- modal.find('[data-action="upload-url"]').on('click', function () {
- modal.modal('hide');
- app.parseAndTranslate('partials/modals/upload_picture_from_url_modal', {}, function (uploadModal) {
- uploadModal.modal('show');
-
- uploadModal.find('.upload-btn').on('click', function () {
- const url = uploadModal.find('#uploadFromUrl').val();
- if (!url) {
- return false;
- }
-
- uploadModal.modal('hide');
-
- pictureCropper.handleImageCrop({
- url: url,
- socketMethod: 'user.uploadCroppedPicture',
- aspectRatio: 1,
- allowSkippingCrop: false,
- paramName: 'uid',
- paramValue: ajaxify.data.theirid,
- }, onUploadComplete);
-
- return false;
- });
- });
-
- return false;
- });
-
- modal.find('[data-action="remove-uploaded"]').on('click', function () {
- socket.emit('user.removeUploadedPicture', {
- uid: ajaxify.data.theirid,
- }, function (err) {
- modal.modal('hide');
- if (err) {
- return alerts.error(err);
- }
- onRemoveComplete();
- });
- });
- }
-
- function changeUserPicture(type, bgColor) {
- return api.put(`/users/${ajaxify.data.theirid}/picture`, { type, bgColor });
- }
-
- return Picture;
+ const Picture = {};
+
+ Picture.openChangeModal = () => {
+ socket.emit('user.getProfilePictures', {
+ uid: ajaxify.data.uid,
+ }, (error, pictures) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ // Boolean to signify whether an uploaded picture is present in the pictures list
+ const uploaded = pictures.reduce((memo, current) => memo || current.type === 'uploaded', false);
+
+ app.parseAndTranslate('partials/modals/change_picture_modal', {
+ pictures,
+ uploaded,
+ icon: {text: ajaxify.data['icon:text'], bgColor: ajaxify.data['icon:bgColor']},
+ defaultAvatar: ajaxify.data.defaultAvatar,
+ allowProfileImageUploads: ajaxify.data.allowProfileImageUploads,
+ iconBackgrounds: config.iconBackgrounds,
+ user: {
+ uid: ajaxify.data.uid,
+ username: ajaxify.data.username,
+ picture: ajaxify.data.picture,
+ 'icon:text': ajaxify.data['icon:text'],
+ 'icon:bgColor': ajaxify.data['icon:bgColor'],
+ },
+ }, html => {
+ const modal = bootbox.dialog({
+ className: 'picture-switcher',
+ title: '[[user:change_picture]]',
+ message: html,
+ show: true,
+ buttons: {
+ close: {
+ label: '[[global:close]]',
+ callback: onCloseModal,
+ className: 'btn-link',
+ },
+ update: {
+ label: '[[global:save_changes]]',
+ callback: saveSelection,
+ },
+ },
+ });
+
+ modal.on('shown.bs.modal', updateImages);
+ modal.on('click', '.list-group-item', function selectImageType() {
+ modal.find('.list-group-item').removeClass('active');
+ $(this).addClass('active');
+ });
+ modal.on('change', 'input[type="radio"][name="icon:bgColor"]', e => {
+ const value = e.target.value;
+ modal.find('.user-icon').css('background-color', value);
+ });
+
+ handleImageUpload(modal);
+
+ function updateImages() {
+ // Check to see which one is the active picture
+ if (ajaxify.data.picture) {
+ modal.find('.list-group-item img').each(function () {
+ if (this.getAttribute('src') === ajaxify.data.picture) {
+ $(this).parents('.list-group-item').addClass('active');
+ }
+ });
+ } else {
+ modal.find('.list-group-item .user-icon').parents('.list-group-item').addClass('active');
+ }
+
+ // Update avatar background colour
+ const radioElement = document.querySelector(`.modal input[type="radio"][value="${ajaxify.data['icon:bgColor']}"]`);
+ if (radioElement) {
+ radioElement.checked = true;
+ } else {
+ // Check the first one
+ document.querySelector('.modal input[type="radio"]').checked = true;
+ }
+ }
+
+ function saveSelection() {
+ const type = modal.find('.list-group-item.active').attr('data-type');
+ const iconBgColor = document.querySelector('.modal.picture-switcher input[type="radio"]:checked').value || 'transparent';
+
+ changeUserPicture(type, iconBgColor).then(() => {
+ Picture.updateHeader(type === 'default' ? '' : modal.find('.list-group-item.active img').attr('src'), iconBgColor);
+ ajaxify.refresh();
+ }).catch(alerts.error);
+ }
+
+ function onCloseModal() {
+ modal.modal('hide');
+ }
+ });
+ });
+ };
+
+ Picture.updateHeader = (picture, iconBgColor) => {
+ if (Number.parseInt(ajaxify.data.theirid, 10) !== Number.parseInt(ajaxify.data.yourid, 10)) {
+ return;
+ }
+
+ if (!picture && ajaxify.data.defaultAvatar) {
+ picture = ajaxify.data.defaultAvatar;
+ }
+
+ $('#header [component="avatar/picture"]')[picture ? 'show' : 'hide']();
+ $('#header [component="avatar/icon"]')[picture ? 'hide' : 'show']();
+ if (picture) {
+ $('#header [component="avatar/picture"]').attr('src', picture);
+ }
+
+ if (iconBgColor) {
+ for (const element of document.querySelectorAll('[component="navbar"] [component="avatar/icon"]')) {
+ element.style['background-color'] = iconBgColor;
+ }
+ }
+ };
+
+ function handleImageUpload(modal) {
+ function onUploadComplete(urlOnServer) {
+ urlOnServer = (urlOnServer.startsWith('http') ? '' : config.relative_path) + urlOnServer + '?' + Date.now();
+
+ Picture.updateHeader(urlOnServer);
+
+ if (ajaxify.data.picture && ajaxify.data.picture.length > 0) {
+ $('#user-current-picture, img.avatar').attr('src', urlOnServer);
+ ajaxify.data.uploadedpicture = urlOnServer;
+ } else {
+ ajaxify.refresh(() => {
+ $('#user-current-picture, img.avatar').attr('src', urlOnServer);
+ });
+ }
+ }
+
+ function onRemoveComplete() {
+ if (ajaxify.data.uploadedpicture === ajaxify.data.picture) {
+ ajaxify.refresh();
+ Picture.updateHeader();
+ }
+ }
+
+ modal.find('[data-action="upload"]').on('click', () => {
+ modal.modal('hide');
+
+ pictureCropper.show({
+ socketMethod: 'user.uploadCroppedPicture',
+ route: config.relative_path + '/api/user/' + ajaxify.data.userslug + '/uploadpicture',
+ aspectRatio: 1 / 1,
+ paramName: 'uid',
+ paramValue: ajaxify.data.theirid,
+ fileSize: ajaxify.data.maximumProfileImageSize,
+ allowSkippingCrop: false,
+ title: '[[user:upload_picture]]',
+ description: '[[user:upload_a_picture]]',
+ accept: ajaxify.data.allowedProfileImageExtensions,
+ }, url => {
+ onUploadComplete(url);
+ });
+
+ return false;
+ });
+
+ modal.find('[data-action="upload-url"]').on('click', () => {
+ modal.modal('hide');
+ app.parseAndTranslate('partials/modals/upload_picture_from_url_modal', {}, uploadModal => {
+ uploadModal.modal('show');
+
+ uploadModal.find('.upload-btn').on('click', () => {
+ const url = uploadModal.find('#uploadFromUrl').val();
+ if (!url) {
+ return false;
+ }
+
+ uploadModal.modal('hide');
+
+ pictureCropper.handleImageCrop({
+ url,
+ socketMethod: 'user.uploadCroppedPicture',
+ aspectRatio: 1,
+ allowSkippingCrop: false,
+ paramName: 'uid',
+ paramValue: ajaxify.data.theirid,
+ }, onUploadComplete);
+
+ return false;
+ });
+ });
+
+ return false;
+ });
+
+ modal.find('[data-action="remove-uploaded"]').on('click', () => {
+ socket.emit('user.removeUploadedPicture', {
+ uid: ajaxify.data.theirid,
+ }, error => {
+ modal.modal('hide');
+ if (error) {
+ return alerts.error(error);
+ }
+
+ onRemoveComplete();
+ });
+ });
+ }
+
+ function changeUserPicture(type, bgColor) {
+ return api.put(`/users/${ajaxify.data.theirid}/picture`, {type, bgColor});
+ }
+
+ return Picture;
});
diff --git a/public/src/modules/alerts.js b/public/src/modules/alerts.js
index 682101f..f882b5f 100644
--- a/public/src/modules/alerts.js
+++ b/public/src/modules/alerts.js
@@ -1,155 +1,157 @@
'use strict';
-
-define('alerts', ['translator', 'components', 'hooks'], function (translator, components, hooks) {
- const module = {};
-
- module.alert = function (params) {
- params.alert_id = 'alert_button_' + (params.alert_id ? params.alert_id : new Date().getTime());
- params.title = params.title ? params.title.trim() || '' : '';
- params.message = params.message ? params.message.trim() : '';
- params.type = params.type || 'info';
-
- const alert = $('#' + params.alert_id);
- if (alert.length) {
- updateAlert(alert, params);
- } else {
- createNew(params);
- }
- };
-
- module.success = function (message, timeout) {
- module.alert({
- alert_id: utils.generateUUID(),
- title: '[[global:alert.success]]',
- message: message,
- type: 'success',
- timeout: timeout || 5000,
- });
- };
-
- module.error = function (message, timeout) {
- message = (message && message.message) || message;
-
- if (message === '[[error:revalidate-failure]]') {
- socket.disconnect();
- app.reconnect();
- return;
- }
-
- module.alert({
- alert_id: utils.generateUUID(),
- title: '[[global:alert.error]]',
- message: message,
- type: 'danger',
- timeout: timeout || 10000,
- });
- };
-
- module.remove = function (id) {
- $('#alert_button_' + id).remove();
- };
-
- function createNew(params) {
- app.parseAndTranslate('alert', params, function (html) {
- let alert = $('#' + params.alert_id);
- if (alert.length) {
- return updateAlert(alert, params);
- }
- alert = html;
- alert.fadeIn(200);
-
- components.get('toaster/tray').prepend(alert);
-
- if (typeof params.closefn === 'function') {
- alert.find('button').on('click', function () {
- params.closefn();
- fadeOut(alert);
- return false;
- });
- }
-
- if (params.timeout) {
- startTimeout(alert, params);
- }
-
- if (typeof params.clickfn === 'function') {
- alert
- .addClass('pointer')
- .on('click', function (e) {
- if (!$(e.target).is('.close')) {
- params.clickfn(alert, params);
- }
- fadeOut(alert);
- });
- }
-
- hooks.fire('action:alert.new', { alert, params });
- });
- }
-
- function updateAlert(alert, params) {
- alert.find('strong').translateHtml(params.title);
- alert.find('p').translateHtml(params.message);
- alert.attr('class', 'alert alert-dismissable alert-' + params.type + ' clearfix');
-
- clearTimeout(parseInt(alert.attr('timeoutId'), 10));
- if (params.timeout) {
- startTimeout(alert, params);
- }
-
- hooks.fire('action:alert.update', { alert, params });
-
- // Handle changes in the clickfn
- alert.off('click').removeClass('pointer');
- if (typeof params.clickfn === 'function') {
- alert
- .addClass('pointer')
- .on('click', function (e) {
- if (!$(e.target).is('.close')) {
- params.clickfn();
- }
- fadeOut(alert);
- });
- }
- }
-
- function fadeOut(alert) {
- alert.fadeOut(500, function () {
- $(this).remove();
- });
- }
-
- function startTimeout(alert, params) {
- const timeout = params.timeout;
-
- const timeoutId = setTimeout(function () {
- fadeOut(alert);
-
- if (typeof params.timeoutfn === 'function') {
- params.timeoutfn(alert, params);
- }
- }, timeout);
-
- alert.attr('timeoutId', timeoutId);
-
- // Reset and start animation
- alert.css('transition-property', 'none');
- alert.removeClass('animate');
-
- setTimeout(function () {
- alert.css('transition-property', '');
- alert.css('transition', 'width ' + (timeout + 450) + 'ms linear, background-color ' + (timeout + 450) + 'ms ease-in');
- alert.addClass('animate');
- hooks.fire('action:alert.animate', { alert, params });
- }, 50);
-
- // Handle mouseenter/mouseleave
- alert
- .on('mouseenter', function () {
- $(this).css('transition-duration', 0);
- });
- }
-
- return module;
+define('alerts', ['translator', 'components', 'hooks'], (translator, components, hooks) => {
+ const module = {};
+
+ module.alert = function (parameters) {
+ parameters.alert_id = 'alert_button_' + (parameters.alert_id ? parameters.alert_id : Date.now());
+ parameters.title = parameters.title ? parameters.title.trim() || '' : '';
+ parameters.message = parameters.message ? parameters.message.trim() : '';
+ parameters.type = parameters.type || 'info';
+
+ const alert = $('#' + parameters.alert_id);
+ if (alert.length > 0) {
+ updateAlert(alert, parameters);
+ } else {
+ createNew(parameters);
+ }
+ };
+
+ module.success = function (message, timeout) {
+ module.alert({
+ alert_id: utils.generateUUID(),
+ title: '[[global:alert.success]]',
+ message,
+ type: 'success',
+ timeout: timeout || 5000,
+ });
+ };
+
+ module.error = function (message, timeout) {
+ message = (message && message.message) || message;
+
+ if (message === '[[error:revalidate-failure]]') {
+ socket.disconnect();
+ app.reconnect();
+ return;
+ }
+
+ module.alert({
+ alert_id: utils.generateUUID(),
+ title: '[[global:alert.error]]',
+ message,
+ type: 'danger',
+ timeout: timeout || 10_000,
+ });
+ };
+
+ module.remove = function (id) {
+ $('#alert_button_' + id).remove();
+ };
+
+ function createNew(parameters) {
+ app.parseAndTranslate('alert', parameters, html => {
+ let alert = $('#' + parameters.alert_id);
+ if (alert.length > 0) {
+ return updateAlert(alert, parameters);
+ }
+
+ alert = html;
+ alert.fadeIn(200);
+
+ components.get('toaster/tray').prepend(alert);
+
+ if (typeof parameters.closefn === 'function') {
+ alert.find('button').on('click', () => {
+ parameters.closefn();
+ fadeOut(alert);
+ return false;
+ });
+ }
+
+ if (parameters.timeout) {
+ startTimeout(alert, parameters);
+ }
+
+ if (typeof parameters.clickfn === 'function') {
+ alert
+ .addClass('pointer')
+ .on('click', e => {
+ if (!$(e.target).is('.close')) {
+ parameters.clickfn(alert, parameters);
+ }
+
+ fadeOut(alert);
+ });
+ }
+
+ hooks.fire('action:alert.new', {alert, params: parameters});
+ });
+ }
+
+ function updateAlert(alert, parameters) {
+ alert.find('strong').translateHtml(parameters.title);
+ alert.find('p').translateHtml(parameters.message);
+ alert.attr('class', 'alert alert-dismissable alert-' + parameters.type + ' clearfix');
+
+ clearTimeout(Number.parseInt(alert.attr('timeoutId'), 10));
+ if (parameters.timeout) {
+ startTimeout(alert, parameters);
+ }
+
+ hooks.fire('action:alert.update', {alert, params: parameters});
+
+ // Handle changes in the clickfn
+ alert.off('click').removeClass('pointer');
+ if (typeof parameters.clickfn === 'function') {
+ alert
+ .addClass('pointer')
+ .on('click', e => {
+ if (!$(e.target).is('.close')) {
+ parameters.clickfn();
+ }
+
+ fadeOut(alert);
+ });
+ }
+ }
+
+ function fadeOut(alert) {
+ alert.fadeOut(500, function () {
+ $(this).remove();
+ });
+ }
+
+ function startTimeout(alert, parameters) {
+ const timeout = parameters.timeout;
+
+ const timeoutId = setTimeout(() => {
+ fadeOut(alert);
+
+ if (typeof parameters.timeoutfn === 'function') {
+ parameters.timeoutfn(alert, parameters);
+ }
+ }, timeout);
+
+ alert.attr('timeoutId', timeoutId);
+
+ // Reset and start animation
+ alert.css('transition-property', 'none');
+ alert.removeClass('animate');
+
+ setTimeout(() => {
+ alert.css('transition-property', '');
+ alert.css('transition', 'width ' + (timeout + 450) + 'ms linear, background-color ' + (timeout + 450) + 'ms ease-in');
+ alert.addClass('animate');
+ hooks.fire('action:alert.animate', {alert, params: parameters});
+ }, 50);
+
+ // Handle mouseenter/mouseleave
+ alert
+ .on('mouseenter', function () {
+ $(this).css('transition-duration', 0);
+ });
+ }
+
+ return module;
});
diff --git a/public/src/modules/api.js b/public/src/modules/api.js
index 32b1e8e..1ba45de 100644
--- a/public/src/modules/api.js
+++ b/public/src/modules/api.js
@@ -1,100 +1,103 @@
'use strict';
-define('api', ['hooks'], (hooks) => {
- const api = {};
- const baseUrl = config.relative_path + '/api/v3';
+define('api', ['hooks'], hooks => {
+ const api = {};
+ const baseUrl = config.relative_path + '/api/v3';
- function call(options, callback) {
- options.url = options.url.startsWith('/api') ?
- config.relative_path + options.url :
- baseUrl + options.url;
+ function call(options, callback) {
+ options.url = options.url.startsWith('/api')
+ ? config.relative_path + options.url
+ : baseUrl + options.url;
- async function doAjax(cb) {
- // Allow options to be modified by plugins, etc.
- ({ options } = await hooks.fire('filter:api.options', { options }));
+ async function doAjax(callback_) {
+ // Allow options to be modified by plugins, etc.
+ ({options} = await hooks.fire('filter:api.options', {options}));
- $.ajax(options)
- .done((res) => {
- cb(null, (
- res &&
- res.hasOwnProperty('status') &&
- res.hasOwnProperty('response') ? res.response : (res || {})
- ));
- })
- .fail((ev) => {
- let errMessage;
- if (ev.responseJSON) {
- errMessage = ev.responseJSON.status && ev.responseJSON.status.message ?
- ev.responseJSON.status.message :
- ev.responseJSON.error;
- }
+ $.ajax(options)
+ .done(res => {
+ callback_(null, (
+ res
+ && res.hasOwnProperty('status')
+ && res.hasOwnProperty('response') ? res.response : (res || {})
+ ));
+ })
+ .fail(event => {
+ let errorMessage;
+ if (event.responseJSON) {
+ errorMessage = event.responseJSON.status && event.responseJSON.status.message
+ ? event.responseJSON.status.message
+ : event.responseJSON.error;
+ }
- cb(new Error(errMessage || ev.statusText));
- });
- }
+ callback_(new Error(errorMessage || event.statusText));
+ });
+ }
- if (typeof callback === 'function') {
- doAjax(callback);
- return;
- }
+ if (typeof callback === 'function') {
+ doAjax(callback);
+ return;
+ }
- return new Promise((resolve, reject) => {
- doAjax(function (err, data) {
- if (err) reject(err);
- else resolve(data);
- });
- });
- }
+ return new Promise((resolve, reject) => {
+ doAjax((error, data) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(data);
+ }
+ });
+ });
+ }
- api.get = (route, payload, onSuccess) => call({
- url: route + (payload && Object.keys(payload).length ? ('?' + $.param(payload)) : ''),
- }, onSuccess);
+ api.get = (route, payload, onSuccess) => call({
+ url: route + (payload && Object.keys(payload).length > 0 ? ('?' + $.param(payload)) : ''),
+ }, onSuccess);
- api.head = (route, payload, onSuccess) => call({
- url: route + (payload && Object.keys(payload).length ? ('?' + $.param(payload)) : ''),
- method: 'head',
- }, onSuccess);
+ api.head = (route, payload, onSuccess) => call({
+ url: route + (payload && Object.keys(payload).length > 0 ? ('?' + $.param(payload)) : ''),
+ method: 'head',
+ }, onSuccess);
- api.post = (route, payload, onSuccess) => call({
- url: route,
- method: 'post',
- data: JSON.stringify(payload || {}),
- contentType: 'application/json; charset=utf-8',
- headers: {
- 'x-csrf-token': config.csrf_token,
- },
- }, onSuccess);
+ api.post = (route, payload, onSuccess) => call({
+ url: route,
+ method: 'post',
+ data: JSON.stringify(payload || {}),
+ contentType: 'application/json; charset=utf-8',
+ headers: {
+ 'x-csrf-token': config.csrf_token,
+ },
+ }, onSuccess);
- api.patch = (route, payload, onSuccess) => call({
- url: route,
- method: 'patch',
- data: JSON.stringify(payload || {}),
- contentType: 'application/json; charset=utf-8',
- headers: {
- 'x-csrf-token': config.csrf_token,
- },
- }, onSuccess);
+ api.patch = (route, payload, onSuccess) => call({
+ url: route,
+ method: 'patch',
+ data: JSON.stringify(payload || {}),
+ contentType: 'application/json; charset=utf-8',
+ headers: {
+ 'x-csrf-token': config.csrf_token,
+ },
+ }, onSuccess);
- api.put = (route, payload, onSuccess) => call({
- url: route,
- method: 'put',
- data: JSON.stringify(payload || {}),
- contentType: 'application/json; charset=utf-8',
- headers: {
- 'x-csrf-token': config.csrf_token,
- },
- }, onSuccess);
+ api.put = (route, payload, onSuccess) => call({
+ url: route,
+ method: 'put',
+ data: JSON.stringify(payload || {}),
+ contentType: 'application/json; charset=utf-8',
+ headers: {
+ 'x-csrf-token': config.csrf_token,
+ },
+ }, onSuccess);
- api.del = (route, payload, onSuccess) => call({
- url: route,
- method: 'delete',
- data: JSON.stringify(payload),
- contentType: 'application/json; charset=utf-8',
- headers: {
- 'x-csrf-token': config.csrf_token,
- },
- }, onSuccess);
- api.delete = api.del;
+ api.del = (route, payload, onSuccess) => call({
+ url: route,
+ method: 'delete',
+ data: JSON.stringify(payload),
+ contentType: 'application/json; charset=utf-8',
+ headers: {
+ 'x-csrf-token': config.csrf_token,
+ },
+ }, onSuccess);
+ api.delete = api.del;
- return api;
+ return api;
});
diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js
index 77aaece..30c30d5 100644
--- a/public/src/modules/autocomplete.js
+++ b/public/src/modules/autocomplete.js
@@ -1,133 +1,136 @@
'use strict';
-define('autocomplete', ['api', 'alerts'], function (api, alerts) {
- const module = {};
- const _default = {
- delay: 200,
- };
-
- module.init = (params) => {
- const { input, source, onSelect, delay } = { ..._default, ...params };
-
- app.loadJQueryUI(function () {
- input.autocomplete({
- delay,
- open: function () {
- $(this).autocomplete('widget').css('z-index', 100005);
- },
- select: function (event, ui) {
- handleOnSelect(input, onSelect, event, ui);
- },
- source,
- });
- });
- };
-
- module.user = function (input, params, onSelect) {
- if (typeof params === 'function') {
- onSelect = params;
- params = {};
- }
- params = params || {};
-
- module.init({
- input,
- onSelect,
- source: (request, response) => {
- params.query = request.term;
-
- api.get('/api/users', params, function (err, result) {
- if (err) {
- return alerts.error(err);
- }
-
- if (result && result.users) {
- const names = result.users.map(function (user) {
- const username = $('
').html(user.username).text();
- return user && {
- label: username,
- value: username,
- user: {
- uid: user.uid,
- name: user.username,
- slug: user.userslug,
- username: user.username,
- userslug: user.userslug,
- picture: user.picture,
- banned: user.banned,
- 'icon:text': user['icon:text'],
- 'icon:bgColor': user['icon:bgColor'],
- },
- };
- });
- response(names);
- }
-
- $('.ui-autocomplete a').attr('data-ajaxify', 'false');
- });
- },
- });
- };
-
- module.group = function (input, onSelect) {
- module.init({
- input,
- onSelect,
- source: (request, response) => {
- socket.emit('groups.search', {
- query: request.term,
- }, function (err, results) {
- if (err) {
- return alerts.error(err);
- }
- if (results && results.length) {
- const names = results.map(function (group) {
- return group && {
- label: group.name,
- value: group.name,
- group: group,
- };
- });
- response(names);
- }
- $('.ui-autocomplete a').attr('data-ajaxify', 'false');
- });
- },
- });
- };
-
- module.tag = function (input, onSelect) {
- module.init({
- input,
- onSelect,
- delay: 100,
- source: (request, response) => {
- socket.emit('topics.autocompleteTags', {
- query: request.term,
- cid: ajaxify.data.cid || 0,
- }, function (err, tags) {
- if (err) {
- return alerts.error(err);
- }
- if (tags) {
- response(tags);
- }
- $('.ui-autocomplete a').attr('data-ajaxify', 'false');
- });
- },
- });
- };
-
- function handleOnSelect(input, onselect, event, ui) {
- onselect = onselect || function () { };
- const e = jQuery.Event('keypress');
- e.which = 13;
- e.keyCode = 13;
- setTimeout(function () {
- input.trigger(e);
- }, 100);
- onselect(event, ui);
- }
-
- return module;
+define('autocomplete', ['api', 'alerts'], (api, alerts) => {
+ const module = {};
+ const _default = {
+ delay: 200,
+ };
+
+ module.init = parameters => {
+ const {input, source, onSelect, delay} = {..._default, ...parameters};
+
+ app.loadJQueryUI(() => {
+ input.autocomplete({
+ delay,
+ open() {
+ $(this).autocomplete('widget').css('z-index', 100_005);
+ },
+ select(event, ui) {
+ handleOnSelect(input, onSelect, event, ui);
+ },
+ source,
+ });
+ });
+ };
+
+ module.user = function (input, parameters, onSelect) {
+ if (typeof parameters === 'function') {
+ onSelect = parameters;
+ parameters = {};
+ }
+
+ parameters ||= {};
+
+ module.init({
+ input,
+ onSelect,
+ source(request, response) {
+ parameters.query = request.term;
+
+ api.get('/api/users', parameters, (error, result) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ if (result && result.users) {
+ const names = result.users.map(user => {
+ const username = $('
').html(user.username).text();
+ return user && {
+ label: username,
+ value: username,
+ user: {
+ uid: user.uid,
+ name: user.username,
+ slug: user.userslug,
+ username: user.username,
+ userslug: user.userslug,
+ picture: user.picture,
+ banned: user.banned,
+ 'icon:text': user['icon:text'],
+ 'icon:bgColor': user['icon:bgColor'],
+ },
+ };
+ });
+ response(names);
+ }
+
+ $('.ui-autocomplete a').attr('data-ajaxify', 'false');
+ });
+ },
+ });
+ };
+
+ module.group = function (input, onSelect) {
+ module.init({
+ input,
+ onSelect,
+ source(request, response) {
+ socket.emit('groups.search', {
+ query: request.term,
+ }, (error, results) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ if (results && results.length > 0) {
+ const names = results.map(group => group && {
+ label: group.name,
+ value: group.name,
+ group,
+ });
+ response(names);
+ }
+
+ $('.ui-autocomplete a').attr('data-ajaxify', 'false');
+ });
+ },
+ });
+ };
+
+ module.tag = function (input, onSelect) {
+ module.init({
+ input,
+ onSelect,
+ delay: 100,
+ source(request, response) {
+ socket.emit('topics.autocompleteTags', {
+ query: request.term,
+ cid: ajaxify.data.cid || 0,
+ }, (error, tags) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ if (tags) {
+ response(tags);
+ }
+
+ $('.ui-autocomplete a').attr('data-ajaxify', 'false');
+ });
+ },
+ });
+ };
+
+ function handleOnSelect(input, onselect, event, ui) {
+ onselect ||= function () {};
+ const e = jQuery.Event('keypress');
+ e.which = 13;
+ e.keyCode = 13;
+ setTimeout(() => {
+ input.trigger(e);
+ }, 100);
+ onselect(event, ui);
+ }
+
+ return module;
});
diff --git a/public/src/modules/categoryFilter.js b/public/src/modules/categoryFilter.js
index fef03a8..793855d 100644
--- a/public/src/modules/categoryFilter.js
+++ b/public/src/modules/categoryFilter.js
@@ -1,103 +1,111 @@
'use strict';
-define('categoryFilter', ['categorySearch', 'api', 'hooks'], function (categorySearch, api, hooks) {
- const categoryFilter = {};
-
- categoryFilter.init = function (el, options) {
- if (!el || !el.length) {
- return;
- }
- options = options || {};
- options.states = options.states || ['watching', 'notwatching', 'ignoring'];
- options.template = 'partials/category-filter';
-
- hooks.fire('action:category.filter.options', { el: el, options: options });
-
- categorySearch.init(el, options);
-
- let selectedCids = [];
- let initialCids = [];
- if (Array.isArray(options.selectedCids)) {
- selectedCids = options.selectedCids.map(cid => parseInt(cid, 10));
- } else if (Array.isArray(ajaxify.data.selectedCids)) {
- selectedCids = ajaxify.data.selectedCids.map(cid => parseInt(cid, 10));
- }
- initialCids = selectedCids.slice();
-
- el.on('hidden.bs.dropdown', function () {
- let changed = initialCids.length !== selectedCids.length;
- initialCids.forEach(function (cid, index) {
- if (cid !== selectedCids[index]) {
- changed = true;
- }
- });
- if (changed) {
- updateFilterButton(el, selectedCids);
- }
- if (options.onHidden) {
- options.onHidden({ changed: changed, selectedCids: selectedCids.slice() });
- return;
- }
- if (changed) {
- let url = window.location.pathname;
- const currentParams = utils.params();
- if (selectedCids.length) {
- currentParams.cid = selectedCids;
- url += '?' + decodeURIComponent($.param(currentParams));
- }
- ajaxify.go(url);
- }
- });
-
- el.on('click', '[component="category/list"] [data-cid]', function () {
- const listEl = el.find('[component="category/list"]');
- const categoryEl = $(this);
- const link = categoryEl.find('a').attr('href');
- if (link && link !== '#' && link.length) {
- return;
- }
- const cid = parseInt(categoryEl.attr('data-cid'), 10);
- const icon = categoryEl.find('[component="category/select/icon"]');
-
- if (selectedCids.includes(cid)) {
- selectedCids.splice(selectedCids.indexOf(cid), 1);
- } else {
- selectedCids.push(cid);
- }
- selectedCids.sort(function (a, b) {
- return a - b;
- });
- options.selectedCids = selectedCids;
-
- icon.toggleClass('invisible');
- listEl.find('li[data-all="all"] i').toggleClass('invisible', !!selectedCids.length);
- if (options.onSelect) {
- options.onSelect({ cid: cid, selectedCids: selectedCids.slice() });
- }
- return false;
- });
- };
-
- function updateFilterButton(el, selectedCids) {
- if (selectedCids.length > 1) {
- renderButton({
- icon: 'fa-plus',
- name: '[[unread:multiple-categories-selected]]',
- bgColor: '#ddd',
- });
- } else if (selectedCids.length === 1) {
- api.get(`/categories/${selectedCids[0]}`, {}).then(renderButton);
- } else {
- renderButton();
- }
- function renderButton(category) {
- app.parseAndTranslate('partials/category-filter-content', {
- selectedCategory: category,
- }, function (html) {
- el.find('button').replaceWith($('
').html(html).find('button'));
- });
- }
- }
-
- return categoryFilter;
+define('categoryFilter', ['categorySearch', 'api', 'hooks'], (categorySearch, api, hooks) => {
+ const categoryFilter = {};
+
+ categoryFilter.init = function (element, options) {
+ if (!element || element.length === 0) {
+ return;
+ }
+
+ options ||= {};
+ options.states = options.states || ['watching', 'notwatching', 'ignoring'];
+ options.template = 'partials/category-filter';
+
+ hooks.fire('action:category.filter.options', {el: element, options});
+
+ categorySearch.init(element, options);
+
+ let selectedCids = [];
+ let initialCids = [];
+ if (Array.isArray(options.selectedCids)) {
+ selectedCids = options.selectedCids.map(cid => Number.parseInt(cid, 10));
+ } else if (Array.isArray(ajaxify.data.selectedCids)) {
+ selectedCids = ajaxify.data.selectedCids.map(cid => Number.parseInt(cid, 10));
+ }
+
+ initialCids = selectedCids.slice();
+
+ element.on('hidden.bs.dropdown', () => {
+ let changed = initialCids.length !== selectedCids.length;
+ for (const [index, cid] of initialCids.entries()) {
+ if (cid !== selectedCids[index]) {
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ updateFilterButton(element, selectedCids);
+ }
+
+ if (options.onHidden) {
+ options.onHidden({changed, selectedCids: selectedCids.slice()});
+ return;
+ }
+
+ if (changed) {
+ let url = window.location.pathname;
+ const currentParameters = utils.params();
+ if (selectedCids.length > 0) {
+ currentParameters.cid = selectedCids;
+ url += '?' + decodeURIComponent($.param(currentParameters));
+ }
+
+ ajaxify.go(url);
+ }
+ });
+
+ element.on('click', '[component="category/list"] [data-cid]', function () {
+ const listElement = element.find('[component="category/list"]');
+ const categoryElement = $(this);
+ const link = categoryElement.find('a').attr('href');
+ if (link && link !== '#' && link.length > 0) {
+ return;
+ }
+
+ const cid = Number.parseInt(categoryElement.attr('data-cid'), 10);
+ const icon = categoryElement.find('[component="category/select/icon"]');
+
+ if (selectedCids.includes(cid)) {
+ selectedCids.splice(selectedCids.indexOf(cid), 1);
+ } else {
+ selectedCids.push(cid);
+ }
+
+ selectedCids.sort((a, b) => a - b);
+ options.selectedCids = selectedCids;
+
+ icon.toggleClass('invisible');
+ listElement.find('li[data-all="all"] i').toggleClass('invisible', selectedCids.length > 0);
+ if (options.onSelect) {
+ options.onSelect({cid, selectedCids: selectedCids.slice()});
+ }
+
+ return false;
+ });
+ };
+
+ function updateFilterButton(element, selectedCids) {
+ if (selectedCids.length > 1) {
+ renderButton({
+ icon: 'fa-plus',
+ name: '[[unread:multiple-categories-selected]]',
+ bgColor: '#ddd',
+ });
+ } else if (selectedCids.length === 1) {
+ api.get(`/categories/${selectedCids[0]}`, {}).then(renderButton);
+ } else {
+ renderButton();
+ }
+
+ function renderButton(category) {
+ app.parseAndTranslate('partials/category-filter-content', {
+ selectedCategory: category,
+ }, html => {
+ element.find('button').replaceWith($('
').html(html).find('button'));
+ });
+ }
+ }
+
+ return categoryFilter;
});
diff --git a/public/src/modules/categorySearch.js b/public/src/modules/categorySearch.js
index 3db9432..a6a6b3a 100644
--- a/public/src/modules/categorySearch.js
+++ b/public/src/modules/categorySearch.js
@@ -1,101 +1,104 @@
'use strict';
-define('categorySearch', ['alerts'], function (alerts) {
- const categorySearch = {};
-
- categorySearch.init = function (el, options) {
- let categoriesList = null;
- options = options || {};
- options.privilege = options.privilege || 'topics:read';
- options.states = options.states || ['watching', 'notwatching', 'ignoring'];
-
- let localCategories = [];
- if (Array.isArray(options.localCategories)) {
- localCategories = options.localCategories.map(c => ({ ...c }));
- }
- options.selectedCids = options.selectedCids || ajaxify.data.selectedCids || [];
-
- const searchEl = el.find('[component="category-selector-search"]');
- if (!searchEl.length) {
- return;
- }
-
- const toggleVisibility = searchEl.parent('[component="category/dropdown"]').length > 0 ||
- searchEl.parent('[component="category-selector"]').length > 0;
-
- el.on('show.bs.dropdown', function () {
- if (toggleVisibility) {
- el.find('.dropdown-toggle').addClass('hidden');
- searchEl.removeClass('hidden');
- }
-
- function doSearch() {
- const val = searchEl.find('input').val();
- if (val.length > 1 || (!val && !categoriesList)) {
- loadList(val, function (categories) {
- categoriesList = categoriesList || categories;
- renderList(categories);
- });
- } else if (!val && categoriesList) {
- categoriesList.forEach(function (c) {
- c.selected = options.selectedCids.includes(c.cid);
- });
- renderList(categoriesList);
- }
- }
-
- searchEl.on('click', function (ev) {
- ev.preventDefault();
- ev.stopPropagation();
- });
- searchEl.find('input').val('').on('keyup', utils.debounce(doSearch, 300));
- doSearch();
- });
-
- el.on('shown.bs.dropdown', function () {
- searchEl.find('input').focus();
- });
-
- el.on('hide.bs.dropdown', function () {
- if (toggleVisibility) {
- el.find('.dropdown-toggle').removeClass('hidden');
- searchEl.addClass('hidden');
- }
-
- searchEl.off('click');
- searchEl.find('input').off('keyup');
- });
-
- function loadList(search, callback) {
- socket.emit('categories.categorySearch', {
- search: search,
- query: utils.params(),
- parentCid: options.parentCid || 0,
- selectedCids: options.selectedCids,
- privilege: options.privilege,
- states: options.states,
- showLinks: options.showLinks,
- }, function (err, categories) {
- if (err) {
- return alerts.error(err);
- }
- callback(localCategories.concat(categories));
- });
- }
-
- function renderList(categories) {
- app.parseAndTranslate(options.template, {
- categoryItems: categories.slice(0, 200),
- selectedCategory: ajaxify.data.selectedCategory,
- allCategoriesUrl: ajaxify.data.allCategoriesUrl,
- }, function (html) {
- el.find('[component="category/list"]')
- .replaceWith(html.find('[component="category/list"]'));
- el.find('[component="category/list"] [component="category/no-matches"]')
- .toggleClass('hidden', !!categories.length);
- });
- }
- };
-
- return categorySearch;
+define('categorySearch', ['alerts'], alerts => {
+ const categorySearch = {};
+
+ categorySearch.init = function (element, options) {
+ let categoriesList = null;
+ options ||= {};
+ options.privilege = options.privilege || 'topics:read';
+ options.states = options.states || ['watching', 'notwatching', 'ignoring'];
+
+ let localCategories = [];
+ if (Array.isArray(options.localCategories)) {
+ localCategories = options.localCategories.map(c => ({...c}));
+ }
+
+ options.selectedCids = options.selectedCids || ajaxify.data.selectedCids || [];
+
+ const searchElement = element.find('[component="category-selector-search"]');
+ if (searchElement.length === 0) {
+ return;
+ }
+
+ const toggleVisibility = searchElement.parent('[component="category/dropdown"]').length > 0
+ || searchElement.parent('[component="category-selector"]').length > 0;
+
+ element.on('show.bs.dropdown', () => {
+ if (toggleVisibility) {
+ element.find('.dropdown-toggle').addClass('hidden');
+ searchElement.removeClass('hidden');
+ }
+
+ function doSearch() {
+ const value = searchElement.find('input').val();
+ if (value.length > 1 || (!value && !categoriesList)) {
+ loadList(value, categories => {
+ categoriesList ||= categories;
+ renderList(categories);
+ });
+ } else if (!value && categoriesList) {
+ for (const c of categoriesList) {
+ c.selected = options.selectedCids.includes(c.cid);
+ }
+
+ renderList(categoriesList);
+ }
+ }
+
+ searchElement.on('click', event => {
+ event.preventDefault();
+ event.stopPropagation();
+ });
+ searchElement.find('input').val('').on('keyup', utils.debounce(doSearch, 300));
+ doSearch();
+ });
+
+ element.on('shown.bs.dropdown', () => {
+ searchElement.find('input').focus();
+ });
+
+ element.on('hide.bs.dropdown', () => {
+ if (toggleVisibility) {
+ element.find('.dropdown-toggle').removeClass('hidden');
+ searchElement.addClass('hidden');
+ }
+
+ searchElement.off('click');
+ searchElement.find('input').off('keyup');
+ });
+
+ function loadList(search, callback) {
+ socket.emit('categories.categorySearch', {
+ search,
+ query: utils.params(),
+ parentCid: options.parentCid || 0,
+ selectedCids: options.selectedCids,
+ privilege: options.privilege,
+ states: options.states,
+ showLinks: options.showLinks,
+ }, (error, categories) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ callback(localCategories.concat(categories));
+ });
+ }
+
+ function renderList(categories) {
+ app.parseAndTranslate(options.template, {
+ categoryItems: categories.slice(0, 200),
+ selectedCategory: ajaxify.data.selectedCategory,
+ allCategoriesUrl: ajaxify.data.allCategoriesUrl,
+ }, html => {
+ element.find('[component="category/list"]')
+ .replaceWith(html.find('[component="category/list"]'));
+ element.find('[component="category/list"] [component="category/no-matches"]')
+ .toggleClass('hidden', categories.length > 0);
+ });
+ }
+ };
+
+ return categorySearch;
});
diff --git a/public/src/modules/categorySelector.js b/public/src/modules/categorySelector.js
index 8ac3295..4e40397 100644
--- a/public/src/modules/categorySelector.js
+++ b/public/src/modules/categorySelector.js
@@ -1,96 +1,104 @@
'use strict';
define('categorySelector', [
- 'categorySearch', 'bootbox', 'hooks',
-], function (categorySearch, bootbox, hooks) {
- const categorySelector = {};
-
- categorySelector.init = function (el, options) {
- if (!el || !el.length) {
- return;
- }
- options = options || {};
- const onSelect = options.onSelect || function () {};
-
- options.states = options.states || ['watching', 'notwatching', 'ignoring'];
- options.template = 'partials/category-selector';
- hooks.fire('action:category.selector.options', { el: el, options: options });
-
- categorySearch.init(el, options);
-
- const selector = {
- el: el,
- selectedCategory: null,
- };
- el.on('click', '[data-cid]', function () {
- const categoryEl = $(this);
- if (categoryEl.hasClass('disabled')) {
- return false;
- }
- selector.selectCategory(categoryEl.attr('data-cid'));
- onSelect(selector.selectedCategory);
- });
- const defaultSelectHtml = selector.el.find('[component="category-selector-selected"]').html();
- selector.selectCategory = function (cid) {
- const categoryEl = selector.el.find('[data-cid="' + cid + '"]');
- selector.selectedCategory = {
- cid: cid,
- name: categoryEl.attr('data-name'),
- };
-
- if (categoryEl.length) {
- selector.el.find('[component="category-selector-selected"]').html(
- categoryEl.find('[component="category-markup"]').html()
- );
- } else {
- selector.el.find('[component="category-selector-selected"]').html(
- defaultSelectHtml
- );
- }
- };
- selector.getSelectedCategory = function () {
- return selector.selectedCategory;
- };
- selector.getSelectedCid = function () {
- return selector.selectedCategory ? selector.selectedCategory.cid : 0;
- };
- return selector;
- };
-
- categorySelector.modal = function (options) {
- options = options || {};
- options.onSelect = options.onSelect || function () {};
- options.onSubmit = options.onSubmit || function () {};
- app.parseAndTranslate('admin/partials/categories/select-category', { message: options.message }, function (html) {
- const modal = bootbox.dialog({
- title: options.title || '[[modules:composer.select_category]]',
- message: html,
- buttons: {
- save: {
- label: '[[global:select]]',
- className: 'btn-primary',
- callback: submit,
- },
- },
- });
-
- const selector = categorySelector.init(modal.find('[component="category-selector"]'), options);
- function submit(ev) {
- ev.preventDefault();
- if (selector.selectedCategory) {
- options.onSubmit(selector.selectedCategory);
- modal.modal('hide');
- }
- return false;
- }
- if (options.openOnLoad) {
- modal.on('shown.bs.modal', function () {
- modal.find('.dropdown-toggle').dropdown('toggle');
- });
- }
- modal.find('form').on('submit', submit);
- });
- };
-
- return categorySelector;
+ 'categorySearch', 'bootbox', 'hooks',
+], (categorySearch, bootbox, hooks) => {
+ const categorySelector = {};
+
+ categorySelector.init = function (element, options) {
+ if (!element || element.length === 0) {
+ return;
+ }
+
+ options ||= {};
+ const onSelect = options.onSelect || function () {};
+
+ options.states = options.states || ['watching', 'notwatching', 'ignoring'];
+ options.template = 'partials/category-selector';
+ hooks.fire('action:category.selector.options', {el: element, options});
+
+ categorySearch.init(element, options);
+
+ const selector = {
+ el: element,
+ selectedCategory: null,
+ };
+ element.on('click', '[data-cid]', function () {
+ const categoryElement = $(this);
+ if (categoryElement.hasClass('disabled')) {
+ return false;
+ }
+
+ selector.selectCategory(categoryElement.attr('data-cid'));
+ onSelect(selector.selectedCategory);
+ });
+ const defaultSelectHtml = selector.el.find('[component="category-selector-selected"]').html();
+ selector.selectCategory = function (cid) {
+ const categoryElement = selector.el.find('[data-cid="' + cid + '"]');
+ selector.selectedCategory = {
+ cid,
+ name: categoryElement.attr('data-name'),
+ };
+
+ if (categoryElement.length > 0) {
+ selector.el.find('[component="category-selector-selected"]').html(
+ categoryElement.find('[component="category-markup"]').html(),
+ );
+ } else {
+ selector.el.find('[component="category-selector-selected"]').html(
+ defaultSelectHtml,
+ );
+ }
+ };
+
+ selector.getSelectedCategory = function () {
+ return selector.selectedCategory;
+ };
+
+ selector.getSelectedCid = function () {
+ return selector.selectedCategory ? selector.selectedCategory.cid : 0;
+ };
+
+ return selector;
+ };
+
+ categorySelector.modal = function (options) {
+ options ||= {};
+ options.onSelect = options.onSelect || function () {};
+ options.onSubmit = options.onSubmit || function () {};
+ app.parseAndTranslate('admin/partials/categories/select-category', {message: options.message}, html => {
+ const modal = bootbox.dialog({
+ title: options.title || '[[modules:composer.select_category]]',
+ message: html,
+ buttons: {
+ save: {
+ label: '[[global:select]]',
+ className: 'btn-primary',
+ callback: submit,
+ },
+ },
+ });
+
+ const selector = categorySelector.init(modal.find('[component="category-selector"]'), options);
+ function submit(event) {
+ event.preventDefault();
+ if (selector.selectedCategory) {
+ options.onSubmit(selector.selectedCategory);
+ modal.modal('hide');
+ }
+
+ return false;
+ }
+
+ if (options.openOnLoad) {
+ modal.on('shown.bs.modal', () => {
+ modal.find('.dropdown-toggle').dropdown('toggle');
+ });
+ }
+
+ modal.find('form').on('submit', submit);
+ });
+ };
+
+ return categorySelector;
});
diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js
index 0657e6b..2077eec 100644
--- a/public/src/modules/chat.js
+++ b/public/src/modules/chat.js
@@ -1,434 +1,432 @@
'use strict';
define('chat', [
- 'components', 'taskbar', 'translator', 'hooks', 'bootbox', 'alerts', 'api',
-], function (components, taskbar, translator, hooks, bootbox, alerts, api) {
- const module = {};
- let newMessage = false;
-
- module.openChat = function (roomId, uid) {
- if (!app.user.uid) {
- return alerts.error('[[error:not-logged-in]]');
- }
-
- function loadAndCenter(chatModal) {
- module.load(chatModal.attr('data-uuid'));
- module.center(chatModal);
- module.focusInput(chatModal);
- }
-
- if (module.modalExists(roomId)) {
- loadAndCenter(module.getModal(roomId));
- } else {
- api.get(`/chats/${roomId}`, {
- uid: uid || app.user.uid,
- }).then((roomData) => {
- roomData.users = roomData.users.filter(function (user) {
- return user && parseInt(user.uid, 10) !== parseInt(app.user.uid, 10);
- });
- roomData.uid = uid || app.user.uid;
- roomData.isSelf = true;
- module.createModal(roomData, loadAndCenter);
- }).catch(alerts.error);
- }
- };
-
- module.newChat = function (touid, callback) {
- function createChat() {
- api.post(`/chats`, {
- uids: [touid],
- }).then(({ roomId }) => {
- if (!ajaxify.data.template.chats) {
- module.openChat(roomId);
- } else {
- ajaxify.go('chats/' + roomId);
- }
-
- callback(null, roomId);
- }).catch(alerts.error);
- }
-
- callback = callback || function () { };
- if (!app.user.uid) {
- return alerts.error('[[error:not-logged-in]]');
- }
-
- if (parseInt(touid, 10) === parseInt(app.user.uid, 10)) {
- return alerts.error('[[error:cant-chat-with-yourself]]');
- }
- socket.emit('modules.chats.isDnD', touid, function (err, isDnD) {
- if (err) {
- return alerts.error(err);
- }
- if (!isDnD) {
- return createChat();
- }
-
- bootbox.confirm('[[modules:chat.confirm-chat-with-dnd-user]]', function (ok) {
- if (ok) {
- createChat();
- }
- });
- });
- };
-
- module.loadChatsDropdown = function (chatsListEl) {
- socket.emit('modules.chats.getRecentChats', {
- uid: app.user.uid,
- after: 0,
- }, function (err, data) {
- if (err) {
- return alerts.error(err);
- }
-
- const rooms = data.rooms.filter(function (room) {
- return room.teaser;
- });
-
- translator.toggleTimeagoShorthand(function () {
- for (let i = 0; i < rooms.length; i += 1) {
- rooms[i].teaser.timeago = $.timeago(new Date(parseInt(rooms[i].teaser.timestamp, 10)));
- }
- translator.toggleTimeagoShorthand();
- app.parseAndTranslate('partials/chats/dropdown', { rooms: rooms }, function (html) {
- chatsListEl.find('*').not('.navigation-link').remove();
- chatsListEl.prepend(html);
- app.createUserTooltips(chatsListEl, 'right');
- chatsListEl.off('click').on('click', '[data-roomid]', function (ev) {
- if ($(ev.target).parents('.user-link').length) {
- return;
- }
- const roomId = $(this).attr('data-roomid');
- if (!ajaxify.currentPage.match(/^chats\//)) {
- module.openChat(roomId);
- } else {
- ajaxify.go('user/' + app.user.userslug + '/chats/' + roomId);
- }
- });
-
- $('[component="chats/mark-all-read"]').off('click').on('click', function () {
- socket.emit('modules.chats.markAllRead', function (err) {
- if (err) {
- return alerts.error(err);
- }
- });
- });
- });
- });
- });
- };
-
-
- module.onChatMessageReceived = function (data) {
- const isSelf = data.self === 1;
- data.message.self = data.self;
-
- newMessage = data.self === 0;
- if (module.modalExists(data.roomId)) {
- addMessageToModal(data);
- } else if (!ajaxify.data.template.chats) {
- api.get(`/chats/${data.roomId}`, {}).then((roomData) => {
- roomData.users = roomData.users.filter(function (user) {
- return user && parseInt(user.uid, 10) !== parseInt(app.user.uid, 10);
- });
- roomData.silent = true;
- roomData.uid = app.user.uid;
- roomData.isSelf = isSelf;
- module.createModal(roomData);
- }).catch(alerts.error);
- }
- };
-
- function addMessageToModal(data) {
- const modal = module.getModal(data.roomId);
- const username = data.message.fromUser.username;
- const isSelf = data.self === 1;
- require(['forum/chats/messages'], function (ChatsMessages) {
- // don't add if already added
- if (!modal.find('[data-mid="' + data.message.messageId + '"]').length) {
- ChatsMessages.appendChatMessage(modal.find('.chat-content'), data.message);
- }
-
- if (modal.is(':visible')) {
- taskbar.updateActive(modal.attr('data-uuid'));
- if (ChatsMessages.isAtBottom(modal.find('.chat-content'))) {
- ChatsMessages.scrollToBottom(modal.find('.chat-content'));
- }
- } else if (!ajaxify.data.template.chats) {
- module.toggleNew(modal.attr('data-uuid'), true, true);
- }
-
- if (!isSelf && (!modal.is(':visible') || !app.isFocused)) {
- taskbar.push('chat', modal.attr('data-uuid'), {
- title: '[[modules:chat.chatting_with]] ' + (data.roomName || username),
- touid: data.message.fromUser.uid,
- roomId: data.roomId,
- isSelf: false,
- });
- }
- });
- }
-
- module.onUserStatusChange = function (data) {
- const modal = module.getModal(data.uid);
- app.updateUserStatus(modal.find('[component="user/status"]'), data.status);
- };
-
- module.onRoomRename = function (data) {
- const newTitle = $('
').html(data.newName).text();
- const modal = module.getModal(data.roomId);
- modal.find('[component="chat/room/name"]').text(newTitle);
- taskbar.update('chat', modal.attr('data-uuid'), {
- title: newTitle,
- });
- hooks.fire('action:chat.renamed', Object.assign(data, {
- modal: modal,
- }));
- };
-
- module.getModal = function (roomId) {
- return $('#chat-modal-' + roomId);
- };
-
- module.modalExists = function (roomId) {
- return $('#chat-modal-' + roomId).length !== 0;
- };
-
- module.createModal = function (data, callback) {
- callback = callback || function () {};
- require([
- 'scrollStop', 'forum/chats', 'forum/chats/messages',
- ], function (scrollStop, Chats, ChatsMessages) {
- app.parseAndTranslate('chat', data, function (chatModal) {
- if (module.modalExists(data.roomId)) {
- return callback(module.getModal(data.roomId));
- }
- const uuid = utils.generateUUID();
- let dragged = false;
-
- chatModal.attr('id', 'chat-modal-' + data.roomId);
- chatModal.attr('data-roomid', data.roomId);
- chatModal.attr('intervalId', 0);
- chatModal.attr('data-uuid', uuid);
- chatModal.css('position', 'fixed');
- chatModal.appendTo($('body'));
- chatModal.find('.timeago').timeago();
- module.center(chatModal);
-
- app.loadJQueryUI(function () {
- chatModal.find('.modal-content').resizable({
- handles: 'n, e, s, w, se',
- minHeight: 250,
- minWidth: 400,
- });
-
- chatModal.find('.modal-content').on('resize', function (event, ui) {
- if (ui.originalSize.height === ui.size.height) {
- return;
- }
-
- chatModal.find('.modal-body').css('height', module.calculateChatListHeight(chatModal));
- });
-
- chatModal.draggable({
- start: function () {
- taskbar.updateActive(uuid);
- },
- stop: function () {
- module.focusInput(chatModal);
- },
- distance: 10,
- handle: '.modal-header',
- });
- });
-
- scrollStop.apply(chatModal.find('[component="chat/messages"]'));
-
- chatModal.find('#chat-close-btn').on('click', function () {
- module.close(chatModal);
- });
-
- function gotoChats() {
- const text = components.get('chat/input').val();
- $(window).one('action:ajaxify.end', function () {
- components.get('chat/input').val(text);
- });
-
- ajaxify.go('user/' + app.user.userslug + '/chats/' + chatModal.attr('data-roomid'));
- module.close(chatModal);
- }
-
- chatModal.find('.modal-header').on('dblclick', gotoChats);
- chatModal.find('button[data-action="maximize"]').on('click', gotoChats);
- chatModal.find('button[data-action="minimize"]').on('click', function () {
- const uuid = chatModal.attr('data-uuid');
- module.minimize(uuid);
- });
-
- chatModal.on('mouseup', function () {
- taskbar.updateActive(chatModal.attr('data-uuid'));
-
- if (dragged) {
- dragged = false;
- }
- });
-
- chatModal.on('mousemove', function (e) {
- if (e.which === 1) {
- dragged = true;
- }
- });
-
- chatModal.on('mousemove keypress click', function () {
- if (newMessage) {
- socket.emit('modules.chats.markRead', data.roomId);
- newMessage = false;
- }
- });
-
- Chats.addActionHandlers(chatModal.find('[component="chat/messages"]'), data.roomId);
- Chats.addRenameHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="rename"]'), data.roomName);
- Chats.addLeaveHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="leave"]'));
- Chats.addSendHandlers(chatModal.attr('data-roomid'), chatModal.find('.chat-input'), chatModal.find('[data-action="send"]'));
- Chats.addMemberHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="members"]'));
-
- Chats.createAutoComplete(chatModal.find('[component="chat/input"]'));
-
- Chats.addScrollHandler(chatModal.attr('data-roomid'), data.uid, chatModal.find('.chat-content'));
- Chats.addScrollBottomHandler(chatModal.find('.chat-content'));
-
- Chats.addCharactersLeftHandler(chatModal);
- Chats.addIPHandler(chatModal);
-
- Chats.addUploadHandler({
- dragDropAreaEl: chatModal.find('.modal-content'),
- pasteEl: chatModal,
- uploadFormEl: chatModal.find('[component="chat/upload"]'),
- inputEl: chatModal.find('[component="chat/input"]'),
- });
-
- ChatsMessages.addSocketListeners();
-
- taskbar.push('chat', chatModal.attr('data-uuid'), {
- title: '[[modules:chat.chatting_with]] ' + (data.roomName || (data.users.length ? data.users[0].username : '')),
- roomId: data.roomId,
- icon: 'fa-comment',
- state: '',
- isSelf: data.isSelf,
- }, function () {
- taskbar.toggleNew(chatModal.attr('data-uuid'), !data.isSelf);
- hooks.fire('action:chat.loaded', chatModal);
-
- if (typeof callback === 'function') {
- callback(chatModal);
- }
- });
- });
- });
- };
-
- module.focusInput = function (chatModal) {
- setTimeout(function () {
- chatModal.find('[component="chat/input"]').focus();
- }, 20);
- };
-
- module.close = function (chatModal) {
- const uuid = chatModal.attr('data-uuid');
- clearInterval(chatModal.attr('intervalId'));
- chatModal.attr('intervalId', 0);
- chatModal.remove();
- chatModal.data('modal', null);
- taskbar.discard('chat', uuid);
-
- if (chatModal.attr('data-mobile')) {
- module.disableMobileBehaviour(chatModal);
- }
-
- hooks.fire('action:chat.closed', {
- uuid: uuid,
- modal: chatModal,
- });
- };
-
- // TODO: see taskbar.js:44
- module.closeByUUID = function (uuid) {
- const chatModal = $('.chat-modal[data-uuid="' + uuid + '"]');
- module.close(chatModal);
- };
-
- module.center = function (chatModal) {
- let hideAfter = false;
- if (chatModal.hasClass('hide')) {
- chatModal.removeClass('hide');
- hideAfter = true;
- }
- chatModal.css('left', Math.max(0, (($(window).width() - $(chatModal).outerWidth()) / 2) + $(window).scrollLeft()) + 'px');
- chatModal.css('top', Math.max(0, ($(window).height() / 2) - ($(chatModal).outerHeight() / 2)) + 'px');
-
- if (hideAfter) {
- chatModal.addClass('hide');
- }
- return chatModal;
- };
-
- module.load = function (uuid) {
- require(['forum/chats/messages'], function (ChatsMessages) {
- const chatModal = $('.chat-modal[data-uuid="' + uuid + '"]');
- chatModal.removeClass('hide');
- taskbar.updateActive(uuid);
- ChatsMessages.scrollToBottom(chatModal.find('.chat-content'));
- module.focusInput(chatModal);
- socket.emit('modules.chats.markRead', chatModal.attr('data-roomid'));
-
- const env = utils.findBootstrapEnvironment();
- if (env === 'xs' || env === 'sm') {
- module.enableMobileBehaviour(chatModal);
- }
- });
- };
-
- module.enableMobileBehaviour = function (modalEl) {
- app.toggleNavbar(false);
- modalEl.attr('data-mobile', '1');
- const messagesEl = modalEl.find('.modal-body');
- messagesEl.css('height', module.calculateChatListHeight(modalEl));
- function resize() {
- messagesEl.css('height', module.calculateChatListHeight(modalEl));
- require(['forum/chats/messages'], function (ChatsMessages) {
- ChatsMessages.scrollToBottom(modalEl.find('.chat-content'));
- });
- }
-
- $(window).on('resize', resize);
- $(window).one('action:ajaxify.start', function () {
- module.close(modalEl);
- $(window).off('resize', resize);
- });
- };
-
- module.disableMobileBehaviour = function () {
- app.toggleNavbar(true);
- };
-
- module.calculateChatListHeight = function (modalEl) {
- // Formula: modal height minus header height. Simple(tm).
- return modalEl.find('.modal-content').outerHeight() - modalEl.find('.modal-header').outerHeight();
- };
-
- module.minimize = function (uuid) {
- const chatModal = $('.chat-modal[data-uuid="' + uuid + '"]');
- chatModal.addClass('hide');
- taskbar.minimize('chat', uuid);
- clearInterval(chatModal.attr('intervalId'));
- chatModal.attr('intervalId', 0);
- hooks.fire('action:chat.minimized', {
- uuid: uuid,
- modal: chatModal,
- });
- };
-
- module.toggleNew = taskbar.toggleNew;
-
- return module;
+ 'components', 'taskbar', 'translator', 'hooks', 'bootbox', 'alerts', 'api',
+], (components, taskbar, translator, hooks, bootbox, alerts, api) => {
+ const module = {};
+ let newMessage = false;
+
+ module.openChat = function (roomId, uid) {
+ if (!app.user.uid) {
+ return alerts.error('[[error:not-logged-in]]');
+ }
+
+ function loadAndCenter(chatModal) {
+ module.load(chatModal.attr('data-uuid'));
+ module.center(chatModal);
+ module.focusInput(chatModal);
+ }
+
+ if (module.modalExists(roomId)) {
+ loadAndCenter(module.getModal(roomId));
+ } else {
+ api.get(`/chats/${roomId}`, {
+ uid: uid || app.user.uid,
+ }).then(roomData => {
+ roomData.users = roomData.users.filter(user => user && Number.parseInt(user.uid, 10) !== Number.parseInt(app.user.uid, 10));
+ roomData.uid = uid || app.user.uid;
+ roomData.isSelf = true;
+ module.createModal(roomData, loadAndCenter);
+ }).catch(alerts.error);
+ }
+ };
+
+ module.newChat = function (touid, callback) {
+ function createChat() {
+ api.post('/chats', {
+ uids: [touid],
+ }).then(({roomId}) => {
+ if (ajaxify.data.template.chats) {
+ ajaxify.go('chats/' + roomId);
+ } else {
+ module.openChat(roomId);
+ }
+
+ callback(null, roomId);
+ }).catch(alerts.error);
+ }
+
+ callback ||= function () {};
+ if (!app.user.uid) {
+ return alerts.error('[[error:not-logged-in]]');
+ }
+
+ if (Number.parseInt(touid, 10) === Number.parseInt(app.user.uid, 10)) {
+ return alerts.error('[[error:cant-chat-with-yourself]]');
+ }
+
+ socket.emit('modules.chats.isDnD', touid, (error, isDnD) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ if (!isDnD) {
+ return createChat();
+ }
+
+ bootbox.confirm('[[modules:chat.confirm-chat-with-dnd-user]]', ok => {
+ if (ok) {
+ createChat();
+ }
+ });
+ });
+ };
+
+ module.loadChatsDropdown = function (chatsListElement) {
+ socket.emit('modules.chats.getRecentChats', {
+ uid: app.user.uid,
+ after: 0,
+ }, (error, data) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ const rooms = data.rooms.filter(room => room.teaser);
+
+ translator.toggleTimeagoShorthand(() => {
+ for (const room of rooms) {
+ room.teaser.timeago = $.timeago(new Date(Number.parseInt(room.teaser.timestamp, 10)));
+ }
+
+ translator.toggleTimeagoShorthand();
+ app.parseAndTranslate('partials/chats/dropdown', {rooms}, html => {
+ chatsListElement.find('*').not('.navigation-link').remove();
+ chatsListElement.prepend(html);
+ app.createUserTooltips(chatsListElement, 'right');
+ chatsListElement.off('click').on('click', '[data-roomid]', function (event) {
+ if ($(event.target).parents('.user-link').length > 0) {
+ return;
+ }
+
+ const roomId = $(this).attr('data-roomid');
+ if (/^chats\//.test(ajaxify.currentPage)) {
+ ajaxify.go('user/' + app.user.userslug + '/chats/' + roomId);
+ } else {
+ module.openChat(roomId);
+ }
+ });
+
+ $('[component="chats/mark-all-read"]').off('click').on('click', () => {
+ socket.emit('modules.chats.markAllRead', error => {
+ if (error) {
+ return alerts.error(error);
+ }
+ });
+ });
+ });
+ });
+ });
+ };
+
+ module.onChatMessageReceived = function (data) {
+ const isSelf = data.self === 1;
+ data.message.self = data.self;
+
+ newMessage = data.self === 0;
+ if (module.modalExists(data.roomId)) {
+ addMessageToModal(data);
+ } else if (!ajaxify.data.template.chats) {
+ api.get(`/chats/${data.roomId}`, {}).then(roomData => {
+ roomData.users = roomData.users.filter(user => user && Number.parseInt(user.uid, 10) !== Number.parseInt(app.user.uid, 10));
+ roomData.silent = true;
+ roomData.uid = app.user.uid;
+ roomData.isSelf = isSelf;
+ module.createModal(roomData);
+ }).catch(alerts.error);
+ }
+ };
+
+ function addMessageToModal(data) {
+ const modal = module.getModal(data.roomId);
+ const username = data.message.fromUser.username;
+ const isSelf = data.self === 1;
+ require(['forum/chats/messages'], ChatsMessages => {
+ // Don't add if already added
+ if (modal.find('[data-mid="' + data.message.messageId + '"]').length === 0) {
+ ChatsMessages.appendChatMessage(modal.find('.chat-content'), data.message);
+ }
+
+ if (modal.is(':visible')) {
+ taskbar.updateActive(modal.attr('data-uuid'));
+ if (ChatsMessages.isAtBottom(modal.find('.chat-content'))) {
+ ChatsMessages.scrollToBottom(modal.find('.chat-content'));
+ }
+ } else if (!ajaxify.data.template.chats) {
+ module.toggleNew(modal.attr('data-uuid'), true, true);
+ }
+
+ if (!isSelf && (!modal.is(':visible') || !app.isFocused)) {
+ taskbar.push('chat', modal.attr('data-uuid'), {
+ title: '[[modules:chat.chatting_with]] ' + (data.roomName || username),
+ touid: data.message.fromUser.uid,
+ roomId: data.roomId,
+ isSelf: false,
+ });
+ }
+ });
+ }
+
+ module.onUserStatusChange = function (data) {
+ const modal = module.getModal(data.uid);
+ app.updateUserStatus(modal.find('[component="user/status"]'), data.status);
+ };
+
+ module.onRoomRename = function (data) {
+ const newTitle = $('
').html(data.newName).text();
+ const modal = module.getModal(data.roomId);
+ modal.find('[component="chat/room/name"]').text(newTitle);
+ taskbar.update('chat', modal.attr('data-uuid'), {
+ title: newTitle,
+ });
+ hooks.fire('action:chat.renamed', Object.assign(data, {
+ modal,
+ }));
+ };
+
+ module.getModal = function (roomId) {
+ return $('#chat-modal-' + roomId);
+ };
+
+ module.modalExists = function (roomId) {
+ return $('#chat-modal-' + roomId).length > 0;
+ };
+
+ module.createModal = function (data, callback) {
+ callback ||= function () {};
+ require([
+ 'scrollStop', 'forum/chats', 'forum/chats/messages',
+ ], (scrollStop, Chats, ChatsMessages) => {
+ app.parseAndTranslate('chat', data, chatModal => {
+ if (module.modalExists(data.roomId)) {
+ return callback(module.getModal(data.roomId));
+ }
+
+ const uuid = utils.generateUUID();
+ let dragged = false;
+
+ chatModal.attr('id', 'chat-modal-' + data.roomId);
+ chatModal.attr('data-roomid', data.roomId);
+ chatModal.attr('intervalId', 0);
+ chatModal.attr('data-uuid', uuid);
+ chatModal.css('position', 'fixed');
+ chatModal.appendTo($('body'));
+ chatModal.find('.timeago').timeago();
+ module.center(chatModal);
+
+ app.loadJQueryUI(() => {
+ chatModal.find('.modal-content').resizable({
+ handles: 'n, e, s, w, se',
+ minHeight: 250,
+ minWidth: 400,
+ });
+
+ chatModal.find('.modal-content').on('resize', (event, ui) => {
+ if (ui.originalSize.height === ui.size.height) {
+ return;
+ }
+
+ chatModal.find('.modal-body').css('height', module.calculateChatListHeight(chatModal));
+ });
+
+ chatModal.draggable({
+ start() {
+ taskbar.updateActive(uuid);
+ },
+ stop() {
+ module.focusInput(chatModal);
+ },
+ distance: 10,
+ handle: '.modal-header',
+ });
+ });
+
+ scrollStop.apply(chatModal.find('[component="chat/messages"]'));
+
+ chatModal.find('#chat-close-btn').on('click', () => {
+ module.close(chatModal);
+ });
+
+ function gotoChats() {
+ const text = components.get('chat/input').val();
+ $(window).one('action:ajaxify.end', () => {
+ components.get('chat/input').val(text);
+ });
+
+ ajaxify.go('user/' + app.user.userslug + '/chats/' + chatModal.attr('data-roomid'));
+ module.close(chatModal);
+ }
+
+ chatModal.find('.modal-header').on('dblclick', gotoChats);
+ chatModal.find('button[data-action="maximize"]').on('click', gotoChats);
+ chatModal.find('button[data-action="minimize"]').on('click', () => {
+ const uuid = chatModal.attr('data-uuid');
+ module.minimize(uuid);
+ });
+
+ chatModal.on('mouseup', () => {
+ taskbar.updateActive(chatModal.attr('data-uuid'));
+
+ dragged &&= false;
+ });
+
+ chatModal.on('mousemove', e => {
+ if (e.which === 1) {
+ dragged = true;
+ }
+ });
+
+ chatModal.on('mousemove keypress click', () => {
+ if (newMessage) {
+ socket.emit('modules.chats.markRead', data.roomId);
+ newMessage = false;
+ }
+ });
+
+ Chats.addActionHandlers(chatModal.find('[component="chat/messages"]'), data.roomId);
+ Chats.addRenameHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="rename"]'), data.roomName);
+ Chats.addLeaveHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="leave"]'));
+ Chats.addSendHandlers(chatModal.attr('data-roomid'), chatModal.find('.chat-input'), chatModal.find('[data-action="send"]'));
+ Chats.addMemberHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="members"]'));
+
+ Chats.createAutoComplete(chatModal.find('[component="chat/input"]'));
+
+ Chats.addScrollHandler(chatModal.attr('data-roomid'), data.uid, chatModal.find('.chat-content'));
+ Chats.addScrollBottomHandler(chatModal.find('.chat-content'));
+
+ Chats.addCharactersLeftHandler(chatModal);
+ Chats.addIPHandler(chatModal);
+
+ Chats.addUploadHandler({
+ dragDropAreaEl: chatModal.find('.modal-content'),
+ pasteEl: chatModal,
+ uploadFormEl: chatModal.find('[component="chat/upload"]'),
+ inputEl: chatModal.find('[component="chat/input"]'),
+ });
+
+ ChatsMessages.addSocketListeners();
+
+ taskbar.push('chat', chatModal.attr('data-uuid'), {
+ title: '[[modules:chat.chatting_with]] ' + (data.roomName || (data.users.length > 0 ? data.users[0].username : '')),
+ roomId: data.roomId,
+ icon: 'fa-comment',
+ state: '',
+ isSelf: data.isSelf,
+ }, () => {
+ taskbar.toggleNew(chatModal.attr('data-uuid'), !data.isSelf);
+ hooks.fire('action:chat.loaded', chatModal);
+
+ if (typeof callback === 'function') {
+ callback(chatModal);
+ }
+ });
+ });
+ });
+ };
+
+ module.focusInput = function (chatModal) {
+ setTimeout(() => {
+ chatModal.find('[component="chat/input"]').focus();
+ }, 20);
+ };
+
+ module.close = function (chatModal) {
+ const uuid = chatModal.attr('data-uuid');
+ clearInterval(chatModal.attr('intervalId'));
+ chatModal.attr('intervalId', 0);
+ chatModal.remove();
+ chatModal.data('modal', null);
+ taskbar.discard('chat', uuid);
+
+ if (chatModal.attr('data-mobile')) {
+ module.disableMobileBehaviour(chatModal);
+ }
+
+ hooks.fire('action:chat.closed', {
+ uuid,
+ modal: chatModal,
+ });
+ };
+
+ // TODO: see taskbar.js:44
+ module.closeByUUID = function (uuid) {
+ const chatModal = $('.chat-modal[data-uuid="' + uuid + '"]');
+ module.close(chatModal);
+ };
+
+ module.center = function (chatModal) {
+ let hideAfter = false;
+ if (chatModal.hasClass('hide')) {
+ chatModal.removeClass('hide');
+ hideAfter = true;
+ }
+
+ chatModal.css('left', Math.max(0, (($(window).width() - $(chatModal).outerWidth()) / 2) + $(window).scrollLeft()) + 'px');
+ chatModal.css('top', Math.max(0, ($(window).height() / 2) - ($(chatModal).outerHeight() / 2)) + 'px');
+
+ if (hideAfter) {
+ chatModal.addClass('hide');
+ }
+
+ return chatModal;
+ };
+
+ module.load = function (uuid) {
+ require(['forum/chats/messages'], ChatsMessages => {
+ const chatModal = $('.chat-modal[data-uuid="' + uuid + '"]');
+ chatModal.removeClass('hide');
+ taskbar.updateActive(uuid);
+ ChatsMessages.scrollToBottom(chatModal.find('.chat-content'));
+ module.focusInput(chatModal);
+ socket.emit('modules.chats.markRead', chatModal.attr('data-roomid'));
+
+ const env = utils.findBootstrapEnvironment();
+ if (env === 'xs' || env === 'sm') {
+ module.enableMobileBehaviour(chatModal);
+ }
+ });
+ };
+
+ module.enableMobileBehaviour = function (modalElement) {
+ app.toggleNavbar(false);
+ modalElement.attr('data-mobile', '1');
+ const messagesElement = modalElement.find('.modal-body');
+ messagesElement.css('height', module.calculateChatListHeight(modalElement));
+ function resize() {
+ messagesElement.css('height', module.calculateChatListHeight(modalElement));
+ require(['forum/chats/messages'], ChatsMessages => {
+ ChatsMessages.scrollToBottom(modalElement.find('.chat-content'));
+ });
+ }
+
+ $(window).on('resize', resize);
+ $(window).one('action:ajaxify.start', () => {
+ module.close(modalElement);
+ $(window).off('resize', resize);
+ });
+ };
+
+ module.disableMobileBehaviour = function () {
+ app.toggleNavbar(true);
+ };
+
+ module.calculateChatListHeight = function (modalElement) {
+ // Formula: modal height minus header height. Simple(tm).
+ return modalElement.find('.modal-content').outerHeight() - modalElement.find('.modal-header').outerHeight();
+ };
+
+ module.minimize = function (uuid) {
+ const chatModal = $('.chat-modal[data-uuid="' + uuid + '"]');
+ chatModal.addClass('hide');
+ taskbar.minimize('chat', uuid);
+ clearInterval(chatModal.attr('intervalId'));
+ chatModal.attr('intervalId', 0);
+ hooks.fire('action:chat.minimized', {
+ uuid,
+ modal: chatModal,
+ });
+ };
+
+ module.toggleNew = taskbar.toggleNew;
+
+ return module;
});
diff --git a/public/src/modules/components.js b/public/src/modules/components.js
index 305aa3c..db56d32 100644
--- a/public/src/modules/components.js
+++ b/public/src/modules/components.js
@@ -1,73 +1,75 @@
'use strict';
-define('components', function () {
- const components = {};
+define('components', () => {
+ const components = {};
- components.core = {
- 'topic/teaser': function (tid) {
- if (tid) {
- return $('[component="category/topic"][data-tid="' + tid + '"] [component="topic/teaser"]');
- }
- return $('[component="topic/teaser"]');
- },
- topic: function (name, value) {
- return $('[component="topic"][data-' + name + '="' + value + '"]');
- },
- post: function (name, value) {
- return $('[component="post"][data-' + name + '="' + value + '"]');
- },
- 'post/content': function (pid) {
- return $('[component="post"][data-pid="' + pid + '"] [component="post/content"]');
- },
- 'post/header': function (pid) {
- return $('[component="post"][data-pid="' + pid + '"] [component="post/header"]');
- },
- 'post/anchor': function (index) {
- return $('[component="post"][data-index="' + index + '"] [component="post/anchor"]');
- },
- 'post/vote-count': function (pid) {
- return $('[component="post"][data-pid="' + pid + '"] [component="post/vote-count"]');
- },
- 'post/bookmark-count': function (pid) {
- return $('[component="post"][data-pid="' + pid + '"] [component="post/bookmark-count"]');
- },
+ components.core = {
+ 'topic/teaser'(tid) {
+ if (tid) {
+ return $('[component="category/topic"][data-tid="' + tid + '"] [component="topic/teaser"]');
+ }
- 'user/postcount': function (uid) {
- return $('[component="user/postcount"][data-uid="' + uid + '"]');
- },
- 'user/reputation': function (uid) {
- return $('[component="user/reputation"][data-uid="' + uid + '"]');
- },
+ return $('[component="topic/teaser"]');
+ },
+ topic(name, value) {
+ return $('[component="topic"][data-' + name + '="' + value + '"]');
+ },
+ post(name, value) {
+ return $('[component="post"][data-' + name + '="' + value + '"]');
+ },
+ 'post/content'(pid) {
+ return $('[component="post"][data-pid="' + pid + '"] [component="post/content"]');
+ },
+ 'post/header'(pid) {
+ return $('[component="post"][data-pid="' + pid + '"] [component="post/header"]');
+ },
+ 'post/anchor'(index) {
+ return $('[component="post"][data-index="' + index + '"] [component="post/anchor"]');
+ },
+ 'post/vote-count'(pid) {
+ return $('[component="post"][data-pid="' + pid + '"] [component="post/vote-count"]');
+ },
+ 'post/bookmark-count'(pid) {
+ return $('[component="post"][data-pid="' + pid + '"] [component="post/bookmark-count"]');
+ },
- 'category/topic': function (name, value) {
- return $('[component="category/topic"][data-' + name + '="' + value + '"]');
- },
+ 'user/postcount'(uid) {
+ return $('[component="user/postcount"][data-uid="' + uid + '"]');
+ },
+ 'user/reputation'(uid) {
+ return $('[component="user/reputation"][data-uid="' + uid + '"]');
+ },
- 'categories/category': function (name, value) {
- return $('[component="categories/category"][data-' + name + '="' + value + '"]');
- },
+ 'category/topic'(name, value) {
+ return $('[component="category/topic"][data-' + name + '="' + value + '"]');
+ },
- 'chat/message': function (messageId) {
- return $('[component="chat/message"][data-mid="' + messageId + '"]');
- },
+ 'categories/category'(name, value) {
+ return $('[component="categories/category"][data-' + name + '="' + value + '"]');
+ },
- 'chat/message/body': function (messageId) {
- return $('[component="chat/message"][data-mid="' + messageId + '"] [component="chat/message/body"]');
- },
+ 'chat/message'(messageId) {
+ return $('[component="chat/message"][data-mid="' + messageId + '"]');
+ },
- 'chat/recent/room': function (roomid) {
- return $('[component="chat/recent/room"][data-roomid="' + roomid + '"]');
- },
- };
+ 'chat/message/body'(messageId) {
+ return $('[component="chat/message"][data-mid="' + messageId + '"] [component="chat/message/body"]');
+ },
- components.get = function () {
- const args = Array.prototype.slice.call(arguments, 1);
+ 'chat/recent/room'(roomid) {
+ return $('[component="chat/recent/room"][data-roomid="' + roomid + '"]');
+ },
+ };
- if (components.core[arguments[0]] && args.length) {
- return components.core[arguments[0]].apply(this, args);
- }
- return $('[component="' + arguments[0] + '"]');
- };
+ components.get = function () {
+ const arguments_ = Array.prototype.slice.call(arguments, 1);
- return components;
+ if (components.core[arguments[0]] && arguments_.length > 0) {
+ return components.core[arguments[0]].apply(this, arguments_);
+ }
+
+ return $('[component="' + arguments[0] + '"]');
+ };
+
+ return components;
});
diff --git a/public/src/modules/coverPhoto.js b/public/src/modules/coverPhoto.js
index aa62779..0c7d795 100644
--- a/public/src/modules/coverPhoto.js
+++ b/public/src/modules/coverPhoto.js
@@ -1,89 +1,88 @@
'use strict';
-
define('coverPhoto', [
- 'alerts',
- 'vendor/jquery/draggable-background/backgroundDraggable',
-], function (alerts) {
- const coverPhoto = {
- coverEl: null,
- saveFn: null,
- };
-
- coverPhoto.init = function (coverEl, saveFn, uploadFn, removeFn) {
- coverPhoto.coverEl = coverEl;
- coverPhoto.saveFn = saveFn;
-
- coverEl.find('.upload').on('click', uploadFn);
- coverEl.find('.resize').on('click', function () {
- enableDragging(coverEl);
- });
- coverEl.find('.remove').on('click', removeFn);
-
- coverEl
- .on('dragover', coverPhoto.onDragOver)
- .on('drop', coverPhoto.onDrop);
-
- coverEl.find('.save').on('click', coverPhoto.save);
- coverEl.addClass('initialised');
- };
-
- coverPhoto.onDragOver = function (e) {
- e.stopPropagation();
- e.preventDefault();
- e.originalEvent.dataTransfer.dropEffect = 'copy';
- };
-
- coverPhoto.onDrop = function (e) {
- e.stopPropagation();
- e.preventDefault();
-
- const files = e.originalEvent.dataTransfer.files;
- const reader = new FileReader();
-
- if (files.length && files[0].type.match('image.*')) {
- reader.onload = function (e) {
- coverPhoto.coverEl.css('background-image', 'url(' + e.target.result + ')');
- coverPhoto.newCover = e.target.result;
- };
-
- reader.readAsDataURL(files[0]);
- enableDragging(coverPhoto.coverEl);
- }
- };
-
- function enableDragging(coverEl) {
- coverEl.toggleClass('active', 1)
- .backgroundDraggable({
- axis: 'y',
- units: 'percent',
- });
-
- alerts.alert({
- alert_id: 'drag_start',
- title: '[[modules:cover.dragging_title]]',
- message: '[[modules:cover.dragging_message]]',
- timeout: 5000,
- });
- }
-
- coverPhoto.save = function () {
- coverPhoto.coverEl.addClass('saving');
-
- coverPhoto.saveFn(coverPhoto.newCover || undefined, coverPhoto.coverEl.css('background-position'), function (err) {
- if (!err) {
- coverPhoto.coverEl.toggleClass('active', 0);
- coverPhoto.coverEl.backgroundDraggable('disable');
- coverPhoto.coverEl.off('dragover', coverPhoto.onDragOver);
- coverPhoto.coverEl.off('drop', coverPhoto.onDrop);
- alerts.success('[[modules:cover.saved]]');
- } else {
- alerts.error(err);
- }
-
- coverPhoto.coverEl.removeClass('saving');
- });
- };
-
- return coverPhoto;
+ 'alerts',
+ 'vendor/jquery/draggable-background/backgroundDraggable',
+], alerts => {
+ const coverPhoto = {
+ coverEl: null,
+ saveFn: null,
+ };
+
+ coverPhoto.init = function (coverElement, saveFunction, uploadFunction, removeFunction) {
+ coverPhoto.coverEl = coverElement;
+ coverPhoto.saveFn = saveFunction;
+
+ coverElement.find('.upload').on('click', uploadFunction);
+ coverElement.find('.resize').on('click', () => {
+ enableDragging(coverElement);
+ });
+ coverElement.find('.remove').on('click', removeFunction);
+
+ coverElement
+ .on('dragover', coverPhoto.onDragOver)
+ .on('drop', coverPhoto.onDrop);
+
+ coverElement.find('.save').on('click', coverPhoto.save);
+ coverElement.addClass('initialised');
+ };
+
+ coverPhoto.onDragOver = function (e) {
+ e.stopPropagation();
+ e.preventDefault();
+ e.originalEvent.dataTransfer.dropEffect = 'copy';
+ };
+
+ coverPhoto.onDrop = function (e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ const files = e.originalEvent.dataTransfer.files;
+ const reader = new FileReader();
+
+ if (files.length > 0 && files[0].type.match('image.*')) {
+ reader.addEventListener('load', e => {
+ coverPhoto.coverEl.css('background-image', 'url(' + e.target.result + ')');
+ coverPhoto.newCover = e.target.result;
+ });
+
+ reader.readAsDataURL(files[0]);
+ enableDragging(coverPhoto.coverEl);
+ }
+ };
+
+ function enableDragging(coverElement) {
+ coverElement.toggleClass('active', 1)
+ .backgroundDraggable({
+ axis: 'y',
+ units: 'percent',
+ });
+
+ alerts.alert({
+ alert_id: 'drag_start',
+ title: '[[modules:cover.dragging_title]]',
+ message: '[[modules:cover.dragging_message]]',
+ timeout: 5000,
+ });
+ }
+
+ coverPhoto.save = function () {
+ coverPhoto.coverEl.addClass('saving');
+
+ coverPhoto.saveFn(coverPhoto.newCover || undefined, coverPhoto.coverEl.css('background-position'), error => {
+ if (error) {
+ alerts.error(error);
+ } else {
+ coverPhoto.coverEl.toggleClass('active', 0);
+ coverPhoto.coverEl.backgroundDraggable('disable');
+ coverPhoto.coverEl.off('dragover', coverPhoto.onDragOver);
+ coverPhoto.coverEl.off('drop', coverPhoto.onDrop);
+ alerts.success('[[modules:cover.saved]]');
+ }
+
+ coverPhoto.coverEl.removeClass('saving');
+ });
+ };
+
+ return coverPhoto;
});
diff --git a/public/src/modules/flags.js b/public/src/modules/flags.js
index a1473cc..4b80efb 100644
--- a/public/src/modules/flags.js
+++ b/public/src/modules/flags.js
@@ -1,95 +1,97 @@
'use strict';
+define('flags', ['hooks', 'components', 'api', 'alerts'], (hooks, components, api, alerts) => {
+ const Flag = {};
+ let flagModal;
+ let flagCommit;
+ let flagReason;
-define('flags', ['hooks', 'components', 'api', 'alerts'], function (hooks, components, api, alerts) {
- const Flag = {};
- let flagModal;
- let flagCommit;
- let flagReason;
-
- Flag.showFlagModal = function (data) {
- app.parseAndTranslate('partials/modals/flag_modal', data, function (html) {
- flagModal = html;
- flagModal.on('hidden.bs.modal', function () {
- flagModal.remove();
- });
-
- flagCommit = flagModal.find('#flag-post-commit');
- flagReason = flagModal.find('#flag-reason-custom');
-
- flagModal.on('click', 'input[name="flag-reason"]', function () {
- if ($(this).attr('id') === 'flag-reason-other') {
- flagReason.removeAttr('disabled');
- if (!flagReason.val().length) {
- flagCommit.attr('disabled', true);
- }
- } else {
- flagReason.attr('disabled', true);
- flagCommit.removeAttr('disabled');
- }
- });
-
- flagCommit.on('click', function () {
- const selected = $('input[name="flag-reason"]:checked');
- let reason = selected.val();
- if (selected.attr('id') === 'flag-reason-other') {
- reason = flagReason.val();
- }
- createFlag(data.type, data.id, reason);
- });
-
- flagModal.on('click', '#flag-reason-other', function () {
- flagReason.focus();
- });
-
- flagModal.modal('show');
- hooks.fire('action:flag.showModal', {
- modalEl: flagModal,
- type: data.type,
- id: data.id,
- });
-
- flagModal.find('#flag-reason-custom').on('keyup blur change', checkFlagButtonEnable);
- });
- };
-
- Flag.resolve = function (flagId) {
- api.put(`/flags/${flagId}`, {
- state: 'resolved',
- }).then(() => {
- alerts.success('[[flags:resolved]]');
- hooks.fire('action:flag.resolved', { flagId: flagId });
- }).catch(alerts.error);
- };
-
- function createFlag(type, id, reason) {
- if (!type || !id || !reason) {
- return;
- }
- const data = { type: type, id: id, reason: reason };
- api.post('/flags', data, function (err, flagId) {
- if (err) {
- return alerts.error(err);
- }
-
- flagModal.modal('hide');
- alerts.success('[[flags:modal-submit-success]]');
- if (type === 'post') {
- const postEl = components.get('post', 'pid', id);
- postEl.find('[component="post/flag"]').addClass('hidden').parent().attr('hidden', '');
- postEl.find('[component="post/already-flagged"]').removeClass('hidden').parent().attr('hidden', null);
- }
- hooks.fire('action:flag.create', { flagId: flagId, data: data });
- });
- }
-
- function checkFlagButtonEnable() {
- if (flagModal.find('#flag-reason-custom').val()) {
- flagCommit.removeAttr('disabled');
- } else {
- flagCommit.attr('disabled', true);
- }
- }
-
- return Flag;
+ Flag.showFlagModal = function (data) {
+ app.parseAndTranslate('partials/modals/flag_modal', data, html => {
+ flagModal = html;
+ flagModal.on('hidden.bs.modal', () => {
+ flagModal.remove();
+ });
+
+ flagCommit = flagModal.find('#flag-post-commit');
+ flagReason = flagModal.find('#flag-reason-custom');
+
+ flagModal.on('click', 'input[name="flag-reason"]', function () {
+ if ($(this).attr('id') === 'flag-reason-other') {
+ flagReason.removeAttr('disabled');
+ if (flagReason.val().length === 0) {
+ flagCommit.attr('disabled', true);
+ }
+ } else {
+ flagReason.attr('disabled', true);
+ flagCommit.removeAttr('disabled');
+ }
+ });
+
+ flagCommit.on('click', () => {
+ const selected = $('input[name="flag-reason"]:checked');
+ let reason = selected.val();
+ if (selected.attr('id') === 'flag-reason-other') {
+ reason = flagReason.val();
+ }
+
+ createFlag(data.type, data.id, reason);
+ });
+
+ flagModal.on('click', '#flag-reason-other', () => {
+ flagReason.focus();
+ });
+
+ flagModal.modal('show');
+ hooks.fire('action:flag.showModal', {
+ modalEl: flagModal,
+ type: data.type,
+ id: data.id,
+ });
+
+ flagModal.find('#flag-reason-custom').on('keyup blur change', checkFlagButtonEnable);
+ });
+ };
+
+ Flag.resolve = function (flagId) {
+ api.put(`/flags/${flagId}`, {
+ state: 'resolved',
+ }).then(() => {
+ alerts.success('[[flags:resolved]]');
+ hooks.fire('action:flag.resolved', {flagId});
+ }).catch(alerts.error);
+ };
+
+ function createFlag(type, id, reason) {
+ if (!type || !id || !reason) {
+ return;
+ }
+
+ const data = {type, id, reason};
+ api.post('/flags', data, (error, flagId) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ flagModal.modal('hide');
+ alerts.success('[[flags:modal-submit-success]]');
+ if (type === 'post') {
+ const postElement = components.get('post', 'pid', id);
+ postElement.find('[component="post/flag"]').addClass('hidden').parent().attr('hidden', '');
+ postElement.find('[component="post/already-flagged"]').removeClass('hidden').parent().attr('hidden', null);
+ }
+
+ hooks.fire('action:flag.create', {flagId, data});
+ });
+ }
+
+ function checkFlagButtonEnable() {
+ if (flagModal.find('#flag-reason-custom').val()) {
+ flagCommit.removeAttr('disabled');
+ } else {
+ flagCommit.attr('disabled', true);
+ }
+ }
+
+ return Flag;
});
diff --git a/public/src/modules/groupSearch.js b/public/src/modules/groupSearch.js
index 016173e..3c9dc8f 100644
--- a/public/src/modules/groupSearch.js
+++ b/public/src/modules/groupSearch.js
@@ -1,60 +1,64 @@
'use strict';
-define('groupSearch', function () {
- const groupSearch = {};
-
- groupSearch.init = function (el) {
- if (utils.isTouchDevice()) {
- return;
- }
- const searchEl = el.find('[component="group-selector-search"]');
- if (!searchEl.length) {
- return;
- }
- const toggleVisibility = searchEl.parent('[component="group-selector"]').length > 0;
-
- const groupEls = el.find('[component="group-list"] [data-name]');
- el.on('show.bs.dropdown', function () {
- function updateList() {
- const val = searchEl.find('input').val().toLowerCase();
- let noMatch = true;
- groupEls.each(function () {
- const liEl = $(this);
- const isMatch = liEl.attr('data-name').toLowerCase().indexOf(val) !== -1;
- if (noMatch && isMatch) {
- noMatch = false;
- }
-
- liEl.toggleClass('hidden', !isMatch);
- });
-
- el.find('[component="group-list"] [component="group-no-matches"]').toggleClass('hidden', !noMatch);
- }
- if (toggleVisibility) {
- el.find('.dropdown-toggle').addClass('hidden');
- searchEl.removeClass('hidden');
- }
-
- searchEl.on('click', function (ev) {
- ev.preventDefault();
- ev.stopPropagation();
- });
- searchEl.find('input').val('').on('keyup', updateList);
- updateList();
- });
-
- el.on('shown.bs.dropdown', function () {
- searchEl.find('input').focus();
- });
-
- el.on('hide.bs.dropdown', function () {
- if (toggleVisibility) {
- el.find('.dropdown-toggle').removeClass('hidden');
- searchEl.addClass('hidden');
- }
- searchEl.off('click').find('input').off('keyup');
- });
- };
-
- return groupSearch;
+define('groupSearch', () => {
+ const groupSearch = {};
+
+ groupSearch.init = function (element) {
+ if (utils.isTouchDevice()) {
+ return;
+ }
+
+ const searchElement = element.find('[component="group-selector-search"]');
+ if (searchElement.length === 0) {
+ return;
+ }
+
+ const toggleVisibility = searchElement.parent('[component="group-selector"]').length > 0;
+
+ const groupEls = element.find('[component="group-list"] [data-name]');
+ element.on('show.bs.dropdown', () => {
+ function updateList() {
+ const value = searchElement.find('input').val().toLowerCase();
+ let noMatch = true;
+ groupEls.each(function () {
+ const liElement = $(this);
+ const isMatch = liElement.attr('data-name').toLowerCase().includes(value);
+ if (noMatch && isMatch) {
+ noMatch = false;
+ }
+
+ liElement.toggleClass('hidden', !isMatch);
+ });
+
+ element.find('[component="group-list"] [component="group-no-matches"]').toggleClass('hidden', !noMatch);
+ }
+
+ if (toggleVisibility) {
+ element.find('.dropdown-toggle').addClass('hidden');
+ searchElement.removeClass('hidden');
+ }
+
+ searchElement.on('click', event => {
+ event.preventDefault();
+ event.stopPropagation();
+ });
+ searchElement.find('input').val('').on('keyup', updateList);
+ updateList();
+ });
+
+ element.on('shown.bs.dropdown', () => {
+ searchElement.find('input').focus();
+ });
+
+ element.on('hide.bs.dropdown', () => {
+ if (toggleVisibility) {
+ element.find('.dropdown-toggle').removeClass('hidden');
+ searchElement.addClass('hidden');
+ }
+
+ searchElement.off('click').find('input').off('keyup');
+ });
+ };
+
+ return groupSearch;
});
diff --git a/public/src/modules/handleBack.js b/public/src/modules/handleBack.js
index 389add5..b8792b8 100644
--- a/public/src/modules/handleBack.js
+++ b/public/src/modules/handleBack.js
@@ -1,106 +1,106 @@
'use strict';
define('handleBack', [
- 'components',
- 'storage',
- 'navigator',
- 'forum/pagination',
-], function (components, storage, navigator, pagination) {
- const handleBack = {};
- let loadTopicsMethod;
+ 'components',
+ 'storage',
+ 'navigator',
+ 'forum/pagination',
+], (components, storage, navigator, pagination) => {
+ const handleBack = {};
+ let loadTopicsMethod;
- handleBack.init = function (_loadTopicsMethod) {
- loadTopicsMethod = _loadTopicsMethod;
- saveClickedIndex();
- $(window).off('action:popstate', onBackClicked).on('action:popstate', onBackClicked);
- };
+ handleBack.init = function (_loadTopicsMethod) {
+ loadTopicsMethod = _loadTopicsMethod;
+ saveClickedIndex();
+ $(window).off('action:popstate', onBackClicked).on('action:popstate', onBackClicked);
+ };
- handleBack.onBackClicked = onBackClicked;
+ handleBack.onBackClicked = onBackClicked;
- function saveClickedIndex() {
- $('[component="category"]').on('click', '[component="topic/header"]', function () {
- const clickedIndex = $(this).parents('[data-index]').attr('data-index');
- const windowScrollTop = $(window).scrollTop();
- $('[component="category/topic"]').each(function (index, el) {
- if ($(el).offset().top - windowScrollTop > 0) {
- storage.setItem('category:bookmark', $(el).attr('data-index'));
- storage.setItem('category:bookmark:clicked', clickedIndex);
- storage.setItem('category:bookmark:offset', $(el).offset().top - windowScrollTop);
- return false;
- }
- });
- });
- }
+ function saveClickedIndex() {
+ $('[component="category"]').on('click', '[component="topic/header"]', function () {
+ const clickedIndex = $(this).parents('[data-index]').attr('data-index');
+ const windowScrollTop = $(window).scrollTop();
+ $('[component="category/topic"]').each((index, element) => {
+ if ($(element).offset().top - windowScrollTop > 0) {
+ storage.setItem('category:bookmark', $(element).attr('data-index'));
+ storage.setItem('category:bookmark:clicked', clickedIndex);
+ storage.setItem('category:bookmark:offset', $(element).offset().top - windowScrollTop);
+ return false;
+ }
+ });
+ });
+ }
- function onBackClicked(isMarkedUnread) {
- const highlightUnread = isMarkedUnread && ajaxify.data.template.unread;
- if (
- ajaxify.data.template.category ||
- ajaxify.data.template.recent ||
- ajaxify.data.template.popular ||
- highlightUnread
- ) {
- let bookmarkIndex = storage.getItem('category:bookmark');
- let clickedIndex = storage.getItem('category:bookmark:clicked');
+ function onBackClicked(isMarkedUnread) {
+ const highlightUnread = isMarkedUnread && ajaxify.data.template.unread;
+ if (
+ ajaxify.data.template.category
+ || ajaxify.data.template.recent
+ || ajaxify.data.template.popular
+ || highlightUnread
+ ) {
+ let bookmarkIndex = storage.getItem('category:bookmark');
+ let clickedIndex = storage.getItem('category:bookmark:clicked');
- storage.removeItem('category:bookmark');
- storage.removeItem('category:bookmark:clicked');
- if (!utils.isNumber(bookmarkIndex)) {
- return;
- }
+ storage.removeItem('category:bookmark');
+ storage.removeItem('category:bookmark:clicked');
+ if (!utils.isNumber(bookmarkIndex)) {
+ return;
+ }
- bookmarkIndex = Math.max(0, parseInt(bookmarkIndex, 10) || 0);
- clickedIndex = Math.max(0, parseInt(clickedIndex, 10) || 0);
+ bookmarkIndex = Math.max(0, Number.parseInt(bookmarkIndex, 10) || 0);
+ clickedIndex = Math.max(0, Number.parseInt(clickedIndex, 10) || 0);
- if (config.usePagination) {
- const page = Math.ceil((parseInt(bookmarkIndex, 10) + 1) / config.topicsPerPage);
- if (parseInt(page, 10) !== ajaxify.data.pagination.currentPage) {
- pagination.loadPage(page, function () {
- handleBack.scrollToTopic(bookmarkIndex, clickedIndex);
- });
- } else {
- handleBack.scrollToTopic(bookmarkIndex, clickedIndex);
- }
- } else {
- if (bookmarkIndex === 0) {
- handleBack.scrollToTopic(bookmarkIndex, clickedIndex);
- return;
- }
+ if (config.usePagination) {
+ const page = Math.ceil((Number.parseInt(bookmarkIndex, 10) + 1) / config.topicsPerPage);
+ if (Number.parseInt(page, 10) === ajaxify.data.pagination.currentPage) {
+ handleBack.scrollToTopic(bookmarkIndex, clickedIndex);
+ } else {
+ pagination.loadPage(page, () => {
+ handleBack.scrollToTopic(bookmarkIndex, clickedIndex);
+ });
+ }
+ } else {
+ if (bookmarkIndex === 0) {
+ handleBack.scrollToTopic(bookmarkIndex, clickedIndex);
+ return;
+ }
- $('[component="category"]').empty();
- loadTopicsMethod(Math.max(0, bookmarkIndex - 1) + 1, function () {
- handleBack.scrollToTopic(bookmarkIndex, clickedIndex);
- });
- }
- }
- }
+ $('[component="category"]').empty();
+ loadTopicsMethod(Math.max(0, bookmarkIndex - 1) + 1, () => {
+ handleBack.scrollToTopic(bookmarkIndex, clickedIndex);
+ });
+ }
+ }
+ }
- handleBack.highlightTopic = function (topicIndex) {
- const highlight = components.get('category/topic', 'index', topicIndex);
+ handleBack.highlightTopic = function (topicIndex) {
+ const highlight = components.get('category/topic', 'index', topicIndex);
- if (highlight.length && !highlight.hasClass('highlight')) {
- highlight.addClass('highlight');
- setTimeout(function () {
- highlight.removeClass('highlight');
- }, 5000);
- }
- };
+ if (highlight.length > 0 && !highlight.hasClass('highlight')) {
+ highlight.addClass('highlight');
+ setTimeout(() => {
+ highlight.removeClass('highlight');
+ }, 5000);
+ }
+ };
- handleBack.scrollToTopic = function (bookmarkIndex, clickedIndex) {
- if (!utils.isNumber(bookmarkIndex)) {
- return;
- }
+ handleBack.scrollToTopic = function (bookmarkIndex, clickedIndex) {
+ if (!utils.isNumber(bookmarkIndex)) {
+ return;
+ }
- const scrollTo = components.get('category/topic', 'index', bookmarkIndex);
+ const scrollTo = components.get('category/topic', 'index', bookmarkIndex);
- if (scrollTo.length) {
- const offset = storage.getItem('category:bookmark:offset');
- storage.removeItem('category:bookmark:offset');
- $(window).scrollTop(scrollTo.offset().top - offset);
- handleBack.highlightTopic(clickedIndex);
- navigator.update();
- }
- };
+ if (scrollTo.length > 0) {
+ const offset = storage.getItem('category:bookmark:offset');
+ storage.removeItem('category:bookmark:offset');
+ $(window).scrollTop(scrollTo.offset().top - offset);
+ handleBack.highlightTopic(clickedIndex);
+ navigator.update();
+ }
+ };
- return handleBack;
+ return handleBack;
});
diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js
index b85fb5f..09c3847 100644
--- a/public/src/modules/helpers.common.js
+++ b/public/src/modules/helpers.common.js
@@ -1,202 +1,213 @@
'use strict';
module.exports = function (utils, Benchpress, relative_path) {
- Benchpress.setGlobal('true', true);
- Benchpress.setGlobal('false', false);
-
- const helpers = {
- displayMenuItem,
- buildMetaTag,
- buildLinkTag,
- stringify,
- escape,
- stripTags,
- generateCategoryBackground,
- generateChildrenCategories,
- generateTopicClass,
- membershipBtn,
- spawnPrivilegeStates,
- localeToHTML,
- renderTopicImage,
- renderTopicEvents,
- renderEvents,
- renderDigestAvatar,
- userAgentIcons,
- buildAvatar,
- register,
- __escape: identity,
- };
-
- function identity(str) {
- return str;
- }
-
- function displayMenuItem(data, index) {
- const item = data.navigation[index];
- if (!item) {
- return false;
- }
-
- if (item.route.match('/users') && data.user && !data.user.privileges['view:users']) {
- return false;
- }
-
- if (item.route.match('/tags') && data.user && !data.user.privileges['view:tags']) {
- return false;
- }
-
- if (item.route.match('/groups') && data.user && !data.user.privileges['view:groups']) {
- return false;
- }
-
- return true;
- }
-
- function buildMetaTag(tag) {
- const name = tag.name ? 'name="' + tag.name + '" ' : '';
- const property = tag.property ? 'property="' + tag.property + '" ' : '';
- const content = tag.content ? 'content="' + tag.content.replace(/\n/g, ' ') + '" ' : '';
-
- return '
\n\t';
- }
-
- function buildLinkTag(tag) {
- const attributes = ['link', 'rel', 'as', 'type', 'href', 'sizes', 'title', 'crossorigin'];
- const [link, rel, as, type, href, sizes, title, crossorigin] = attributes.map(attr => (tag[attr] ? `${attr}="${tag[attr]}" ` : ''));
-
- return '
\n\t';
- }
-
- function stringify(obj) {
- // Turns the incoming object into a JSON string
- return JSON.stringify(obj).replace(/&/gm, '&').replace(//gm, '>')
- .replace(/"/g, '"');
- }
-
- function escape(str) {
- return utils.escapeHTML(str);
- }
-
- function stripTags(str) {
- return utils.stripHTMLTags(str);
- }
-
- function generateCategoryBackground(category) {
- if (!category) {
- return '';
- }
- const style = [];
-
- if (category.bgColor) {
- style.push('background-color: ' + category.bgColor);
- }
-
- if (category.color) {
- style.push('color: ' + category.color);
- }
-
- if (category.backgroundImage) {
- style.push('background-image: url(' + category.backgroundImage + ')');
- if (category.imageClass) {
- style.push('background-size: ' + category.imageClass);
- }
- }
-
- return style.join('; ') + ';';
- }
-
- function generateChildrenCategories(category) {
- let html = '';
- if (!category || !category.children || !category.children.length) {
- return html;
- }
- category.children.forEach(function (child) {
- if (child && !child.isSection) {
- const link = child.link ? child.link : (relative_path + '/category/' + child.slug);
- html += '
' +
- '' +
- '' +
- '
' +
- '' + child.name + '';
- }
- });
- html = html ? ('
' + html + '') : html;
- return html;
- }
-
- function generateTopicClass(topic) {
- const fields = ['locked', 'pinned', 'deleted', 'unread', 'scheduled'];
- return fields.filter(field => !!topic[field]).join(' ');
- }
-
- // Groups helpers
- function membershipBtn(groupObj) {
- if (groupObj.isMember && groupObj.name !== 'administrators') {
- return '
';
- }
-
- if (groupObj.isPending && groupObj.name !== 'administrators') {
- return '
';
- } else if (groupObj.isInvited) {
- return '
';
- } else if (!groupObj.disableJoinRequests && groupObj.name !== 'administrators') {
- return '
';
- }
- return '';
- }
-
- function spawnPrivilegeStates(member, privileges) {
- const states = [];
- for (const priv in privileges) {
- if (privileges.hasOwnProperty(priv)) {
- states.push({
- name: priv,
- state: privileges[priv],
- });
- }
- }
- return states.map(function (priv) {
- const guestDisabled = ['groups:moderate', 'groups:posts:upvote', 'groups:posts:downvote', 'groups:local:login', 'groups:group:create'];
- const spidersEnabled = ['groups:find', 'groups:read', 'groups:topics:read', 'groups:view:users', 'groups:view:tags', 'groups:view:groups'];
- const globalModDisabled = ['groups:moderate'];
- const disabled =
- (member === 'guests' && (guestDisabled.includes(priv.name) || priv.name.startsWith('groups:admin:'))) ||
- (member === 'spiders' && !spidersEnabled.includes(priv.name)) ||
- (member === 'Global Moderators' && globalModDisabled.includes(priv.name));
-
- return '
| ';
- }).join('');
- }
-
- function localeToHTML(locale, fallback) {
- locale = locale || fallback || 'en-GB';
- return locale.replace('_', '-');
- }
-
- function renderTopicImage(topicObj) {
- if (topicObj.thumb) {
- return '

';
- }
- return '

';
- }
-
- function renderTopicEvents(index, sort) {
- if (sort === 'most_votes') {
- return '';
- }
- const start = this.posts[index].eventStart;
- const end = this.posts[index].eventEnd;
- const events = this.events.filter(event => event.timestamp >= start && event.timestamp < end);
- if (!events.length) {
- return '';
- }
-
- return renderEvents.call(this, events);
- }
-
- function renderEvents(events) {
- return events.reduce((html, event) => {
- html += `
+ Benchpress.setGlobal('true', true);
+ Benchpress.setGlobal('false', false);
+
+ const helpers = {
+ displayMenuItem,
+ buildMetaTag,
+ buildLinkTag,
+ stringify,
+ escape,
+ stripTags,
+ generateCategoryBackground,
+ generateChildrenCategories,
+ generateTopicClass,
+ membershipBtn: membershipButton,
+ spawnPrivilegeStates,
+ localeToHTML,
+ renderTopicImage,
+ renderTopicEvents,
+ renderEvents,
+ renderDigestAvatar,
+ userAgentIcons,
+ buildAvatar,
+ register,
+ __escape: identity,
+ };
+
+ function identity(string_) {
+ return string_;
+ }
+
+ function displayMenuItem(data, index) {
+ const item = data.navigation[index];
+ if (!item) {
+ return false;
+ }
+
+ if (item.route.match('/users') && data.user && !data.user.privileges['view:users']) {
+ return false;
+ }
+
+ if (item.route.match('/tags') && data.user && !data.user.privileges['view:tags']) {
+ return false;
+ }
+
+ if (item.route.match('/groups') && data.user && !data.user.privileges['view:groups']) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function buildMetaTag(tag) {
+ const name = tag.name ? 'name="' + tag.name + '" ' : '';
+ const property = tag.property ? 'property="' + tag.property + '" ' : '';
+ const content = tag.content ? 'content="' + tag.content.replaceAll('\n', ' ') + '" ' : '';
+
+ return '\n\t';
+ }
+
+ function buildLinkTag(tag) {
+ const attributes = ['link', 'rel', 'as', 'type', 'href', 'sizes', 'title', 'crossorigin'];
+ const [link, rel, as, type, href, sizes, title, crossorigin] = attributes.map(attribute => (tag[attribute] ? `${attribute}="${tag[attribute]}" ` : ''));
+
+ return '\n\t';
+ }
+
+ function stringify(object) {
+ // Turns the incoming object into a JSON string
+ return JSON.stringify(object).replaceAll(/&/gm, '&').replaceAll(//gm, '>')
+ .replaceAll('"', '"');
+ }
+
+ function escape(string_) {
+ return utils.escapeHTML(string_);
+ }
+
+ function stripTags(string_) {
+ return utils.stripHTMLTags(string_);
+ }
+
+ function generateCategoryBackground(category) {
+ if (!category) {
+ return '';
+ }
+
+ const style = [];
+
+ if (category.bgColor) {
+ style.push('background-color: ' + category.bgColor);
+ }
+
+ if (category.color) {
+ style.push('color: ' + category.color);
+ }
+
+ if (category.backgroundImage) {
+ style.push('background-image: url(' + category.backgroundImage + ')');
+ if (category.imageClass) {
+ style.push('background-size: ' + category.imageClass);
+ }
+ }
+
+ return style.join('; ') + ';';
+ }
+
+ function generateChildrenCategories(category) {
+ let html = '';
+ if (!category || !category.children || category.children.length === 0) {
+ return html;
+ }
+
+ for (const child of category.children) {
+ if (child && !child.isSection) {
+ const link = child.link ? child.link : (relative_path + '/category/' + child.slug);
+ html += ''
+ + ''
+ + ''
+ + '
'
+ + '' + child.name + '';
+ }
+ }
+
+ html = html ? ('' + html + '') : html;
+ return html;
+ }
+
+ function generateTopicClass(topic) {
+ const fields = ['locked', 'pinned', 'deleted', 'unread', 'scheduled'];
+ return fields.filter(field => Boolean(topic[field])).join(' ');
+ }
+
+ // Groups helpers
+ function membershipButton(groupObject) {
+ if (groupObject.isMember && groupObject.name !== 'administrators') {
+ return '';
+ }
+
+ if (groupObject.isPending && groupObject.name !== 'administrators') {
+ return '';
+ }
+
+ if (groupObject.isInvited) {
+ return '';
+ }
+
+ if (!groupObject.disableJoinRequests && groupObject.name !== 'administrators') {
+ return '';
+ }
+
+ return '';
+ }
+
+ function spawnPrivilegeStates(member, privileges) {
+ const states = [];
+ for (const priv in privileges) {
+ if (privileges.hasOwnProperty(priv)) {
+ states.push({
+ name: priv,
+ state: privileges[priv],
+ });
+ }
+ }
+
+ return states.map(priv => {
+ const guestDisabled = ['groups:moderate', 'groups:posts:upvote', 'groups:posts:downvote', 'groups:local:login', 'groups:group:create'];
+ const spidersEnabled = ['groups:find', 'groups:read', 'groups:topics:read', 'groups:view:users', 'groups:view:tags', 'groups:view:groups'];
+ const globalModuleDisabled = ['groups:moderate'];
+ const disabled
+ = (member === 'guests' && (guestDisabled.includes(priv.name) || priv.name.startsWith('groups:admin:')))
+ || (member === 'spiders' && !spidersEnabled.includes(priv.name))
+ || (member === 'Global Moderators' && globalModuleDisabled.includes(priv.name));
+
+ return ' | ';
+ }).join('');
+ }
+
+ function localeToHTML(locale, fallback) {
+ locale ||= fallback || 'en-GB';
+ return locale.replace('_', '-');
+ }
+
+ function renderTopicImage(topicObject) {
+ if (topicObject.thumb) {
+ return '
';
+ }
+
+ return '
';
+ }
+
+ function renderTopicEvents(index, sort) {
+ if (sort === 'most_votes') {
+ return '';
+ }
+
+ const start = this.posts[index].eventStart;
+ const end = this.posts[index].eventEnd;
+ const events = this.events.filter(event => event.timestamp >= start && event.timestamp < end);
+ if (events.length === 0) {
+ return '';
+ }
+
+ return renderEvents.call(this, events);
+ }
+
+ function renderEvents(events) {
+ return events.reduce((html, event) => {
+ html += `
@@ -205,92 +216,115 @@ module.exports = function (utils, Benchpress, relative_path) {
`;
- if (event.user) {
- if (!event.user.system) {
- html += `${buildAvatar(event.user, 'xs', true)} ${event.user.username} `;
- } else {
- html += `[[global:system-user]] `;
- }
- }
-
- html += ``;
-
- if (this.privileges.isAdminOrMod) {
- html += ` `;
- }
-
- return html;
- }, '');
- }
-
- function renderDigestAvatar(block) {
- if (block.teaser) {
- if (block.teaser.user.picture) {
- return '
';
- }
- return '' + block.teaser.user['icon:text'] + '
';
- }
- if (block.user.picture) {
- return '
';
- }
- return '' + block.user['icon:text'] + '
';
- }
-
- function userAgentIcons(data) {
- let icons = '';
-
- switch (data.platform) {
- case 'Linux':
- icons += '';
- break;
- case 'Microsoft Windows':
- icons += '';
- break;
- case 'Apple Mac':
- icons += '';
- break;
- case 'Android':
- icons += '';
- break;
- case 'iPad':
- icons += '';
- break;
- case 'iPod': // intentional fall-through
- case 'iPhone':
- icons += '';
- break;
- default:
- icons += '';
- break;
- }
-
- switch (data.browser) {
- case 'Chrome':
- icons += '';
- break;
- case 'Firefox':
- icons += '';
- break;
- case 'Safari':
- icons += '';
- break;
- case 'IE':
- icons += '';
- break;
- case 'Edge':
- icons += '';
- break;
- default:
- icons += '';
- break;
- }
-
- return icons;
- }
-
- function buildAvatar(userObj, size, rounded, classNames, component) {
- /**
- * userObj requires:
+ if (event.user) {
+ html += event.user.system ? '[[global:system-user]] ' : `${buildAvatar(event.user, 'xs', true)} ${event.user.username} `;
+ }
+
+ html += ``;
+
+ if (this.privileges.isAdminOrMod) {
+ html += ` `;
+ }
+
+ return html;
+ }, '');
+ }
+
+ function renderDigestAvatar(block) {
+ if (block.teaser) {
+ if (block.teaser.user.picture) {
+ return '
';
+ }
+
+ return '' + block.teaser.user['icon:text'] + '
';
+ }
+
+ if (block.user.picture) {
+ return '
';
+ }
+
+ return '' + block.user['icon:text'] + '
';
+ }
+
+ function userAgentIcons(data) {
+ let icons = '';
+
+ switch (data.platform) {
+ case 'Linux': {
+ icons += '';
+ break;
+ }
+
+ case 'Microsoft Windows': {
+ icons += '';
+ break;
+ }
+
+ case 'Apple Mac': {
+ icons += '';
+ break;
+ }
+
+ case 'Android': {
+ icons += '';
+ break;
+ }
+
+ case 'iPad': {
+ icons += '';
+ break;
+ }
+
+ case 'iPod': // Intentional fall-through
+ case 'iPhone': {
+ icons += '';
+ break;
+ }
+
+ default: {
+ icons += '';
+ break;
+ }
+ }
+
+ switch (data.browser) {
+ case 'Chrome': {
+ icons += '';
+ break;
+ }
+
+ case 'Firefox': {
+ icons += '';
+ break;
+ }
+
+ case 'Safari': {
+ icons += '';
+ break;
+ }
+
+ case 'IE': {
+ icons += '';
+ break;
+ }
+
+ case 'Edge': {
+ icons += '';
+ break;
+ }
+
+ default: {
+ icons += '';
+ break;
+ }
+ }
+
+ return icons;
+ }
+
+ function buildAvatar(userObject, size, rounded, classNames, component) {
+ /**
+ * UserObj requires:
* - uid, picture, icon:bgColor, icon:text (getUserField w/ "picture" should return all 4), username
* size: one of "xs", "sm", "md", "lg", or "xl" (required), or an integer
* rounded: true or false (optional, default false)
@@ -298,50 +332,49 @@ module.exports = function (utils, Benchpress, relative_path) {
* component: overrides the default component (optional, default none)
*/
- // Try to use root context if passed-in userObj is undefined
- if (!userObj) {
- userObj = this;
- }
-
- const attributes = [
- 'alt="' + userObj.username + '"',
- 'title="' + userObj.username + '"',
- 'data-uid="' + userObj.uid + '"',
- 'loading="lazy"',
- ];
- const styles = [];
- classNames = classNames || '';
-
- // Validate sizes, handle integers, otherwise fall back to `avatar-sm`
- if (['xs', 'sm', 'sm2x', 'md', 'lg', 'xl'].includes(size)) {
- classNames += ' avatar-' + size;
- } else if (!isNaN(parseInt(size, 10))) {
- styles.push('width: ' + size + 'px;', 'height: ' + size + 'px;', 'line-height: ' + size + 'px;', 'font-size: ' + (parseInt(size, 10) / 16) + 'rem;');
- } else {
- classNames += ' avatar-sm';
- }
- attributes.unshift('class="avatar ' + classNames + (rounded ? ' avatar-rounded' : '') + '"');
-
- // Component override
- if (component) {
- attributes.push('component="' + component + '"');
- } else {
- attributes.push('component="avatar/' + (userObj.picture ? 'picture' : 'icon') + '"');
- }
-
- if (userObj.picture) {
- return '
';
- }
-
- styles.push('background-color: ' + userObj['icon:bgColor'] + ';');
- return '' + userObj['icon:text'] + '';
- }
-
- function register() {
- Object.keys(helpers).forEach(function (helperName) {
- Benchpress.registerHelper(helperName, helpers[helperName]);
- });
- }
-
- return helpers;
+ // Try to use root context if passed-in userObj is undefined
+ userObject ||= this;
+
+ const attributes = [
+ 'alt="' + userObject.username + '"',
+ 'title="' + userObject.username + '"',
+ 'data-uid="' + userObject.uid + '"',
+ 'loading="lazy"',
+ ];
+ const styles = [];
+ classNames ||= '';
+
+ // Validate sizes, handle integers, otherwise fall back to `avatar-sm`
+ if (['xs', 'sm', 'sm2x', 'md', 'lg', 'xl'].includes(size)) {
+ classNames += ' avatar-' + size;
+ } else if (isNaN(Number.parseInt(size, 10))) {
+ classNames += ' avatar-sm';
+ } else {
+ styles.push('width: ' + size + 'px;', 'height: ' + size + 'px;', 'line-height: ' + size + 'px;', 'font-size: ' + (Number.parseInt(size, 10) / 16) + 'rem;');
+ }
+
+ attributes.unshift('class="avatar ' + classNames + (rounded ? ' avatar-rounded' : '') + '"');
+
+ // Component override
+ if (component) {
+ attributes.push('component="' + component + '"');
+ } else {
+ attributes.push('component="avatar/' + (userObject.picture ? 'picture' : 'icon') + '"');
+ }
+
+ if (userObject.picture) {
+ return '
';
+ }
+
+ styles.push('background-color: ' + userObject['icon:bgColor'] + ';');
+ return '' + userObject['icon:text'] + '';
+ }
+
+ function register() {
+ for (const helperName of Object.keys(helpers)) {
+ Benchpress.registerHelper(helperName, helpers[helperName]);
+ }
+ }
+
+ return helpers;
};
diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js
index b5651c7..4ca7ba2 100644
--- a/public/src/modules/helpers.js
+++ b/public/src/modules/helpers.js
@@ -2,6 +2,4 @@
const factory = require('./helpers.common');
-define('helpers', ['utils', 'benchpressjs'], function (utils, Benchpressjs) {
- return factory(utils, Benchpressjs, config.relative_path);
-});
+define('helpers', ['utils', 'benchpressjs'], (utils, Benchpressjs) => factory(utils, Benchpressjs, config.relative_path));
diff --git a/public/src/modules/hooks.js b/public/src/modules/hooks.js
index 0f84c96..a7b8c84 100644
--- a/public/src/modules/hooks.js
+++ b/public/src/modules/hooks.js
@@ -1,173 +1,182 @@
'use strict';
define('hooks', [], () => {
- const Hooks = {
- loaded: {},
- temporary: new Set(),
- runOnce: new Set(),
- deprecated: {
-
- },
- logs: {
- _collection: new Set(),
- },
- };
-
- Hooks.logs.collect = () => {
- if (Hooks.logs._collection) {
- return;
- }
-
- Hooks.logs._collection = new Set();
- };
-
- Hooks.logs.log = (...args) => {
- if (Hooks.logs._collection) {
- Hooks.logs._collection.add(args);
- } else {
- console.log.apply(console, args);
- }
- };
-
- Hooks.logs.flush = () => {
- if (Hooks.logs._collection && Hooks.logs._collection.size) {
- console.groupCollapsed('[hooks] Changes to hooks on this page …');
- Hooks.logs._collection.forEach((args) => {
- console.log.apply(console, args);
- });
- console.groupEnd();
- }
-
- delete Hooks.logs._collection;
- };
-
- Hooks.register = (hookName, method) => {
- Hooks.loaded[hookName] = Hooks.loaded[hookName] || new Set();
- Hooks.loaded[hookName].add(method);
-
- if (Hooks.deprecated.hasOwnProperty(hookName)) {
- const deprecated = Hooks.deprecated[hookName];
-
- if (deprecated) {
- console.groupCollapsed(`[hooks] Hook "${hookName}" is deprecated, please use "${deprecated}" instead.`);
- } else {
- console.groupCollapsed(`[hooks] Hook "${hookName}" is deprecated, there is no alternative.`);
- }
-
- console.info(method);
- console.groupEnd();
- }
-
- Hooks.logs.log(`[hooks] Registered ${hookName}`, method);
- return Hooks;
- };
- Hooks.on = Hooks.register;
- Hooks.one = (hookName, method) => {
- Hooks.runOnce.add({ hookName, method });
- return Hooks.register(hookName, method);
- };
-
- // registerPage/onPage takes care of unregistering the listener on ajaxify
- Hooks.registerPage = (hookName, method) => {
- Hooks.temporary.add({ hookName, method });
- return Hooks.register(hookName, method);
- };
- Hooks.onPage = Hooks.registerPage;
- Hooks.register('action:ajaxify.start', () => {
- Hooks.temporary.forEach((pair) => {
- Hooks.unregister(pair.hookName, pair.method);
- Hooks.temporary.delete(pair);
- });
- });
-
- Hooks.unregister = (hookName, method) => {
- if (Hooks.loaded[hookName] && Hooks.loaded[hookName].has(method)) {
- Hooks.loaded[hookName].delete(method);
- Hooks.logs.log(`[hooks] Unregistered ${hookName}`, method);
- } else {
- Hooks.logs.log(`[hooks] Unregistration of ${hookName} failed, passed-in method is not a registered listener or the hook itself has no listeners, currently.`);
- }
-
- return Hooks;
- };
- Hooks.off = Hooks.unregister;
-
- Hooks.hasListeners = hookName => Hooks.loaded[hookName] && Hooks.loaded[hookName].size > 0;
-
- const _onHookError = (e, listener, data) => {
- console.warn(`[hooks] Exception encountered in ${listener.name ? listener.name : 'anonymous function'}, stack trace follows.`);
- console.error(e);
- return Promise.resolve(data);
- };
-
- const _fireFilterHook = (hookName, data) => {
- if (!Hooks.hasListeners(hookName)) {
- return Promise.resolve(data);
- }
-
- const listeners = Array.from(Hooks.loaded[hookName]);
- return listeners.reduce((promise, listener) => promise.then((data) => {
- try {
- const result = listener(data);
- return utils.isPromise(result) ?
- result.then(data => Promise.resolve(data)).catch(e => _onHookError(e, listener, data)) :
- result;
- } catch (e) {
- return _onHookError(e, listener, data);
- }
- }), Promise.resolve(data));
- };
-
- const _fireActionHook = (hookName, data) => {
- if (Hooks.hasListeners(hookName)) {
- Hooks.loaded[hookName].forEach(listener => listener(data));
- }
-
- // Backwards compatibility (remove this when we eventually remove jQuery from NodeBB core)
- $(window).trigger(hookName, data);
- };
-
- const _fireStaticHook = async (hookName, data) => {
- if (!Hooks.hasListeners(hookName)) {
- return Promise.resolve(data);
- }
-
- const listeners = Array.from(Hooks.loaded[hookName]);
- await Promise.allSettled(listeners.map((listener) => {
- try {
- return listener(data);
- } catch (e) {
- return _onHookError(e, listener);
- }
- }));
-
- return await Promise.resolve(data);
- };
-
- Hooks.fire = (hookName, data) => {
- const type = hookName.split(':').shift();
- let result;
- switch (type) {
- case 'filter':
- result = _fireFilterHook(hookName, data);
- break;
-
- case 'action':
- result = _fireActionHook(hookName, data);
- break;
-
- case 'static':
- result = _fireStaticHook(hookName, data);
- break;
- }
- Hooks.runOnce.forEach((pair) => {
- if (pair.hookName === hookName) {
- Hooks.unregister(hookName, pair.method);
- Hooks.runOnce.delete(pair);
- }
- });
- return result;
- };
-
- return Hooks;
+ const Hooks = {
+ loaded: {},
+ temporary: new Set(),
+ runOnce: new Set(),
+ deprecated: {},
+ logs: {
+ _collection: new Set(),
+ },
+ };
+
+ Hooks.logs.collect = () => {
+ if (Hooks.logs._collection) {
+ return;
+ }
+
+ Hooks.logs._collection = new Set();
+ };
+
+ Hooks.logs.log = (...arguments_) => {
+ if (Hooks.logs._collection) {
+ Hooks.logs._collection.add(arguments_);
+ } else {
+ console.log.apply(console, arguments_);
+ }
+ };
+
+ Hooks.logs.flush = () => {
+ if (Hooks.logs._collection && Hooks.logs._collection.size > 0) {
+ console.groupCollapsed('[hooks] Changes to hooks on this page …');
+ for (const arguments_ of Hooks.logs._collection) {
+ console.log.apply(console, arguments_);
+ }
+
+ console.groupEnd();
+ }
+
+ delete Hooks.logs._collection;
+ };
+
+ Hooks.register = (hookName, method) => {
+ Hooks.loaded[hookName] = Hooks.loaded[hookName] || new Set();
+ Hooks.loaded[hookName].add(method);
+
+ if (Hooks.deprecated.hasOwnProperty(hookName)) {
+ const deprecated = Hooks.deprecated[hookName];
+
+ if (deprecated) {
+ console.groupCollapsed(`[hooks] Hook "${hookName}" is deprecated, please use "${deprecated}" instead.`);
+ } else {
+ console.groupCollapsed(`[hooks] Hook "${hookName}" is deprecated, there is no alternative.`);
+ }
+
+ console.info(method);
+ console.groupEnd();
+ }
+
+ Hooks.logs.log(`[hooks] Registered ${hookName}`, method);
+ return Hooks;
+ };
+
+ Hooks.on = Hooks.register;
+ Hooks.one = (hookName, method) => {
+ Hooks.runOnce.add({hookName, method});
+ return Hooks.register(hookName, method);
+ };
+
+ // RegisterPage/onPage takes care of unregistering the listener on ajaxify
+ Hooks.registerPage = (hookName, method) => {
+ Hooks.temporary.add({hookName, method});
+ return Hooks.register(hookName, method);
+ };
+
+ Hooks.onPage = Hooks.registerPage;
+ Hooks.register('action:ajaxify.start', () => {
+ for (const pair of Hooks.temporary) {
+ Hooks.unregister(pair.hookName, pair.method);
+ Hooks.temporary.delete(pair);
+ }
+ });
+
+ Hooks.unregister = (hookName, method) => {
+ if (Hooks.loaded[hookName] && Hooks.loaded[hookName].has(method)) {
+ Hooks.loaded[hookName].delete(method);
+ Hooks.logs.log(`[hooks] Unregistered ${hookName}`, method);
+ } else {
+ Hooks.logs.log(`[hooks] Unregistration of ${hookName} failed, passed-in method is not a registered listener or the hook itself has no listeners, currently.`);
+ }
+
+ return Hooks;
+ };
+
+ Hooks.off = Hooks.unregister;
+
+ Hooks.hasListeners = hookName => Hooks.loaded[hookName] && Hooks.loaded[hookName].size > 0;
+
+ const _onHookError = (e, listener, data) => {
+ console.warn(`[hooks] Exception encountered in ${listener.name ? listener.name : 'anonymous function'}, stack trace follows.`);
+ console.error(e);
+ return Promise.resolve(data);
+ };
+
+ const _fireFilterHook = (hookName, data) => {
+ if (!Hooks.hasListeners(hookName)) {
+ return Promise.resolve(data);
+ }
+
+ const listeners = Array.from(Hooks.loaded[hookName]);
+ return listeners.reduce((promise, listener) => promise.then(data => {
+ try {
+ const result = listener(data);
+ return utils.isPromise(result)
+ ? result.then(data => data).catch(error => _onHookError(error, listener, data))
+ : result;
+ } catch (error) {
+ return _onHookError(error, listener, data);
+ }
+ }), Promise.resolve(data));
+ };
+
+ const _fireActionHook = (hookName, data) => {
+ if (Hooks.hasListeners(hookName)) {
+ for (const listener of Hooks.loaded[hookName]) {
+ listener(data);
+ }
+ }
+
+ // Backwards compatibility (remove this when we eventually remove jQuery from NodeBB core)
+ $(window).trigger(hookName, data);
+ };
+
+ const _fireStaticHook = async (hookName, data) => {
+ if (!Hooks.hasListeners(hookName)) {
+ return data;
+ }
+
+ const listeners = Array.from(Hooks.loaded[hookName]);
+ await Promise.allSettled(listeners.map(listener => {
+ try {
+ return listener(data);
+ } catch (error) {
+ return _onHookError(error, listener);
+ }
+ }));
+
+ return await Promise.resolve(data);
+ };
+
+ Hooks.fire = (hookName, data) => {
+ const type = hookName.split(':').shift();
+ let result;
+ switch (type) {
+ case 'filter': {
+ result = _fireFilterHook(hookName, data);
+ break;
+ }
+
+ case 'action': {
+ result = _fireActionHook(hookName, data);
+ break;
+ }
+
+ case 'static': {
+ result = _fireStaticHook(hookName, data);
+ break;
+ }
+ }
+
+ for (const pair of Hooks.runOnce) {
+ if (pair.hookName === hookName) {
+ Hooks.unregister(hookName, pair.method);
+ Hooks.runOnce.delete(pair);
+ }
+ }
+
+ return result;
+ };
+
+ return Hooks;
});
diff --git a/public/src/modules/iconSelect.js b/public/src/modules/iconSelect.js
index b1cb443..cf8bd96 100644
--- a/public/src/modules/iconSelect.js
+++ b/public/src/modules/iconSelect.js
@@ -1,125 +1,124 @@
'use strict';
-
-define('iconSelect', ['benchpress', 'bootbox'], function (Benchpress, bootbox) {
- const iconSelect = {};
-
- iconSelect.init = function (el, onModified) {
- onModified = onModified || function () {};
- const doubleSize = el.hasClass('fa-2x');
- let selected = el.attr('class').replace('fa-2x', '').replace('fa', '').replace(/\s+/g, '');
-
- $('#icons .selected').removeClass('selected');
-
- if (selected) {
- try {
- $('#icons .fa-icons .fa.' + selected).addClass('selected');
- } catch (err) {
- selected = '';
- }
- }
-
- Benchpress.render('partials/fontawesome', {}).then(function (html) {
- html = $(html);
- html.find('.fa-icons').prepend($(''));
-
- const picker = bootbox.dialog({
- onEscape: true,
- backdrop: true,
- show: false,
- message: html,
- title: 'Select an Icon',
- buttons: {
- noIcon: {
- label: 'No Icon',
- className: 'btn-default',
- callback: function () {
- el.attr('class', 'fa ' + (doubleSize ? 'fa-2x ' : ''));
- el.val('');
- el.attr('value', '');
-
- onModified(el);
- },
- },
- success: {
- label: 'Select',
- className: 'btn-primary',
- callback: function () {
- const iconClass = $('.bootbox .selected').attr('class') || `fa fa-${$('.bootbox #fa-filter').val()}`;
- const categoryIconClass = $('').addClass(iconClass).removeClass('fa').removeClass('selected')
- .attr('class');
- const searchElVal = picker.find('input').val();
-
- if (categoryIconClass) {
- el.attr('class', 'fa ' + (doubleSize ? 'fa-2x ' : '') + categoryIconClass);
- el.val(categoryIconClass);
- el.attr('value', categoryIconClass);
- } else if (searchElVal) {
- el.attr('class', searchElVal);
- el.val(searchElVal);
- el.attr('value', searchElVal);
- }
-
- onModified(el);
- },
- },
- },
- });
-
- picker.on('show.bs.modal', function () {
- const modalEl = $(this);
- const searchEl = modalEl.find('input');
-
- if (selected) {
- modalEl.find('.' + selected).addClass('selected');
- searchEl.val(selected.replace('fa-', ''));
- }
- }).modal('show');
-
- picker.on('shown.bs.modal', function () {
- const modalEl = $(this);
- const searchEl = modalEl.find('input');
- const icons = modalEl.find('.fa-icons i');
- const submitEl = modalEl.find('button.btn-primary');
-
- function changeSelection(newSelection) {
- modalEl.find('i.selected').removeClass('selected');
- if (newSelection) {
- newSelection.addClass('selected');
- } else if (searchEl.val().length === 0) {
- if (selected) {
- modalEl.find('.' + selected).addClass('selected');
- }
- } else {
- modalEl.find('i:visible').first().addClass('selected');
- }
- }
-
- // Focus on the input box
- searchEl.selectRange(0, searchEl.val().length);
-
- modalEl.find('.icon-container').on('click', 'i', function () {
- searchEl.val($(this).attr('class').replace('fa fa-', '').replace('selected', ''));
- changeSelection($(this));
- });
-
- searchEl.on('keyup', function (e) {
- if (e.keyCode !== 13) {
- // Filter
- icons.show();
- icons.each(function (idx, el) {
- if (!el.className.match(new RegExp('^fa fa-.*' + searchEl.val() + '.*$'))) {
- $(el).hide();
- }
- });
- changeSelection();
- } else {
- submitEl.click();
- }
- });
- });
- });
- };
-
- return iconSelect;
+define('iconSelect', ['benchpress', 'bootbox'], (Benchpress, bootbox) => {
+ const iconSelect = {};
+
+ iconSelect.init = function (element, onModified) {
+ onModified ||= function () {};
+ const doubleSize = element.hasClass('fa-2x');
+ let selected = element.attr('class').replace('fa-2x', '').replace('fa', '').replaceAll(/\s+/g, '');
+
+ $('#icons .selected').removeClass('selected');
+
+ if (selected) {
+ try {
+ $('#icons .fa-icons .fa.' + selected).addClass('selected');
+ } catch {
+ selected = '';
+ }
+ }
+
+ Benchpress.render('partials/fontawesome', {}).then(html => {
+ html = $(html);
+ html.find('.fa-icons').prepend($(''));
+
+ const picker = bootbox.dialog({
+ onEscape: true,
+ backdrop: true,
+ show: false,
+ message: html,
+ title: 'Select an Icon',
+ buttons: {
+ noIcon: {
+ label: 'No Icon',
+ className: 'btn-default',
+ callback() {
+ element.attr('class', 'fa ' + (doubleSize ? 'fa-2x ' : ''));
+ element.val('');
+ element.attr('value', '');
+
+ onModified(element);
+ },
+ },
+ success: {
+ label: 'Select',
+ className: 'btn-primary',
+ callback() {
+ const iconClass = $('.bootbox .selected').attr('class') || `fa fa-${$('.bootbox #fa-filter').val()}`;
+ const categoryIconClass = $('').addClass(iconClass).removeClass('fa').removeClass('selected')
+ .attr('class');
+ const searchElementValue = picker.find('input').val();
+
+ if (categoryIconClass) {
+ element.attr('class', 'fa ' + (doubleSize ? 'fa-2x ' : '') + categoryIconClass);
+ element.val(categoryIconClass);
+ element.attr('value', categoryIconClass);
+ } else if (searchElementValue) {
+ element.attr('class', searchElementValue);
+ element.val(searchElementValue);
+ element.attr('value', searchElementValue);
+ }
+
+ onModified(element);
+ },
+ },
+ },
+ });
+
+ picker.on('show.bs.modal', function () {
+ const modalElement = $(this);
+ const searchElement = modalElement.find('input');
+
+ if (selected) {
+ modalElement.find('.' + selected).addClass('selected');
+ searchElement.val(selected.replace('fa-', ''));
+ }
+ }).modal('show');
+
+ picker.on('shown.bs.modal', function () {
+ const modalElement = $(this);
+ const searchElement = modalElement.find('input');
+ const icons = modalElement.find('.fa-icons i');
+ const submitElement = modalElement.find('button.btn-primary');
+
+ function changeSelection(newSelection) {
+ modalElement.find('i.selected').removeClass('selected');
+ if (newSelection) {
+ newSelection.addClass('selected');
+ } else if (searchElement.val().length === 0) {
+ if (selected) {
+ modalElement.find('.' + selected).addClass('selected');
+ }
+ } else {
+ modalElement.find('i:visible').first().addClass('selected');
+ }
+ }
+
+ // Focus on the input box
+ searchElement.selectRange(0, searchElement.val().length);
+
+ modalElement.find('.icon-container').on('click', 'i', function () {
+ searchElement.val($(this).attr('class').replace('fa fa-', '').replace('selected', ''));
+ changeSelection($(this));
+ });
+
+ searchElement.on('keyup', e => {
+ if (e.keyCode === 13) {
+ submitElement.click();
+ } else {
+ // Filter
+ icons.show();
+ icons.each((index, element_) => {
+ if (!new RegExp('^fa fa-.*' + searchElement.val() + '.*$').test(element_.className)) {
+ $(element_).hide();
+ }
+ });
+ changeSelection();
+ }
+ });
+ });
+ });
+ };
+
+ return iconSelect;
});
diff --git a/public/src/modules/logout.js b/public/src/modules/logout.js
index 70bfcd9..135dd35 100644
--- a/public/src/modules/logout.js
+++ b/public/src/modules/logout.js
@@ -1,28 +1,26 @@
'use strict';
-define('logout', ['hooks'], function (hooks) {
- return function logout(redirect) {
- redirect = redirect === undefined ? true : redirect;
- hooks.fire('action:app.logout');
+define('logout', ['hooks'], hooks => function logout(redirect) {
+ redirect = redirect === undefined ? true : redirect;
+ hooks.fire('action:app.logout');
- $.ajax(config.relative_path + '/logout', {
- type: 'POST',
- headers: {
- 'x-csrf-token': config.csrf_token,
- },
- beforeSend: function () {
- app.flags._logout = true;
- },
- success: function (data) {
- hooks.fire('action:app.loggedOut', data);
- if (redirect) {
- if (data.next) {
- window.location.href = data.next;
- } else {
- window.location.reload();
- }
- }
- },
- });
- };
+ $.ajax(config.relative_path + '/logout', {
+ type: 'POST',
+ headers: {
+ 'x-csrf-token': config.csrf_token,
+ },
+ beforeSend() {
+ app.flags._logout = true;
+ },
+ success(data) {
+ hooks.fire('action:app.loggedOut', data);
+ if (redirect) {
+ if (data.next) {
+ window.location.href = data.next;
+ } else {
+ window.location.reload();
+ }
+ }
+ },
+ });
});
diff --git a/public/src/modules/messages.js b/public/src/modules/messages.js
index e30d2a4..01dfa35 100644
--- a/public/src/modules/messages.js
+++ b/public/src/modules/messages.js
@@ -1,131 +1,134 @@
'use strict';
-define('messages', ['bootbox', 'translator', 'storage', 'alerts', 'hooks'], function (bootbox, translator, storage, alerts, hooks) {
- const messages = {};
-
- let showWelcomeMessage;
- let registerMessage;
-
- messages.show = function () {
- hooks.one('action:ajaxify.end', () => {
- showQueryStringMessages();
- showCookieWarning();
- messages.showEmailConfirmWarning();
- });
- };
-
- messages.showEmailConfirmWarning = function (message) {
- if (!config.emailPrompt || !app.user.uid || parseInt(storage.getItem('email-confirm-dismiss'), 10) === 1) {
- return;
- }
- const msg = {
- alert_id: 'email_confirm',
- type: 'warning',
- timeout: 0,
- closefn: () => {
- storage.setItem('email-confirm-dismiss', 1);
- },
- };
-
- if (!app.user.email) {
- msg.message = '[[error:no-email-to-confirm]]';
- msg.clickfn = function () {
- alerts.remove('email_confirm');
- ajaxify.go('user/' + app.user.userslug + '/edit/email');
- };
- alerts.alert(msg);
- } else if (!app.user['email:confirmed'] && !app.user.isEmailConfirmSent) {
- msg.message = message || '[[error:email-not-confirmed]]';
- msg.clickfn = function () {
- alerts.remove('email_confirm');
- ajaxify.go('/me/edit/email');
- };
- alerts.alert(msg);
- } else if (!app.user['email:confirmed'] && app.user.isEmailConfirmSent) {
- msg.message = '[[error:email-not-confirmed-email-sent]]';
- alerts.alert(msg);
- }
- };
-
- function showCookieWarning() {
- if (!config.cookies.enabled || !navigator.cookieEnabled || app.inAdmin || storage.getItem('cookieconsent') === '1') {
- return;
- }
-
- config.cookies.message = translator.unescape(config.cookies.message);
- config.cookies.dismiss = translator.unescape(config.cookies.dismiss);
- config.cookies.link = translator.unescape(config.cookies.link);
- config.cookies.link_url = translator.unescape(config.cookies.link_url);
-
- app.parseAndTranslate('partials/cookie-consent', config.cookies, function (html) {
- $(document.body).append(html);
- $(document.body).addClass('cookie-consent-open');
-
- const warningEl = $('.cookie-consent');
- const dismissEl = warningEl.find('button');
- dismissEl.on('click', function () {
- // Save consent cookie and remove warning element
- storage.setItem('cookieconsent', '1');
- warningEl.remove();
- $(document.body).removeClass('cookie-consent-open');
- });
- });
- }
-
- function showQueryStringMessages() {
- const params = utils.params({ full: true });
- showWelcomeMessage = params.has('loggedin');
- registerMessage = params.get('register');
-
- if (showWelcomeMessage) {
- alerts.alert({
- type: 'success',
- title: '[[global:welcome_back]] ' + app.user.username + '!',
- message: '[[global:you_have_successfully_logged_in]]',
- timeout: 5000,
- });
-
- params.delete('loggedin');
- }
-
- if (registerMessage) {
- bootbox.alert({
- message: utils.escapeHTML(decodeURIComponent(registerMessage)),
- });
-
- params.delete('register');
- }
-
- if (params.has('lang') && params.get('lang') === config.defaultLang) {
- console.info(`The "lang" parameter was passed in to set the language to "${params.get('lang')}", but that is already the forum default language.`);
- params.delete('lang');
- }
-
- const qs = params.toString();
- ajaxify.updateHistory(ajaxify.currentPage + (qs ? `?${qs}` : '') + document.location.hash, true);
- }
-
- messages.showInvalidSession = function () {
- bootbox.alert({
- title: '[[error:invalid-session]]',
- message: '[[error:invalid-session-text]]',
- closeButton: false,
- callback: function () {
- window.location.reload();
- },
- });
- };
-
- messages.showSessionMismatch = function () {
- bootbox.alert({
- title: '[[error:session-mismatch]]',
- message: '[[error:session-mismatch-text]]',
- closeButton: false,
- callback: function () {
- window.location.reload();
- },
- });
- };
-
- return messages;
+define('messages', ['bootbox', 'translator', 'storage', 'alerts', 'hooks'], (bootbox, translator, storage, alerts, hooks) => {
+ const messages = {};
+
+ let showWelcomeMessage;
+ let registerMessage;
+
+ messages.show = function () {
+ hooks.one('action:ajaxify.end', () => {
+ showQueryStringMessages();
+ showCookieWarning();
+ messages.showEmailConfirmWarning();
+ });
+ };
+
+ messages.showEmailConfirmWarning = function (message) {
+ if (!config.emailPrompt || !app.user.uid || Number.parseInt(storage.getItem('email-confirm-dismiss'), 10) === 1) {
+ return;
+ }
+
+ const message_ = {
+ alert_id: 'email_confirm',
+ type: 'warning',
+ timeout: 0,
+ closefn() {
+ storage.setItem('email-confirm-dismiss', 1);
+ },
+ };
+
+ if (!app.user.email) {
+ message_.message = '[[error:no-email-to-confirm]]';
+ message_.clickfn = function () {
+ alerts.remove('email_confirm');
+ ajaxify.go('user/' + app.user.userslug + '/edit/email');
+ };
+
+ alerts.alert(message_);
+ } else if (!app.user['email:confirmed'] && !app.user.isEmailConfirmSent) {
+ message_.message = message || '[[error:email-not-confirmed]]';
+ message_.clickfn = function () {
+ alerts.remove('email_confirm');
+ ajaxify.go('/me/edit/email');
+ };
+
+ alerts.alert(message_);
+ } else if (!app.user['email:confirmed'] && app.user.isEmailConfirmSent) {
+ message_.message = '[[error:email-not-confirmed-email-sent]]';
+ alerts.alert(message_);
+ }
+ };
+
+ function showCookieWarning() {
+ if (!config.cookies.enabled || !navigator.cookieEnabled || app.inAdmin || storage.getItem('cookieconsent') === '1') {
+ return;
+ }
+
+ config.cookies.message = translator.unescape(config.cookies.message);
+ config.cookies.dismiss = translator.unescape(config.cookies.dismiss);
+ config.cookies.link = translator.unescape(config.cookies.link);
+ config.cookies.link_url = translator.unescape(config.cookies.link_url);
+
+ app.parseAndTranslate('partials/cookie-consent', config.cookies, html => {
+ $(document.body).append(html);
+ $(document.body).addClass('cookie-consent-open');
+
+ const warningElement = $('.cookie-consent');
+ const dismissElement = warningElement.find('button');
+ dismissElement.on('click', () => {
+ // Save consent cookie and remove warning element
+ storage.setItem('cookieconsent', '1');
+ warningElement.remove();
+ $(document.body).removeClass('cookie-consent-open');
+ });
+ });
+ }
+
+ function showQueryStringMessages() {
+ const parameters = utils.params({full: true});
+ showWelcomeMessage = parameters.has('loggedin');
+ registerMessage = parameters.get('register');
+
+ if (showWelcomeMessage) {
+ alerts.alert({
+ type: 'success',
+ title: '[[global:welcome_back]] ' + app.user.username + '!',
+ message: '[[global:you_have_successfully_logged_in]]',
+ timeout: 5000,
+ });
+
+ parameters.delete('loggedin');
+ }
+
+ if (registerMessage) {
+ bootbox.alert({
+ message: utils.escapeHTML(decodeURIComponent(registerMessage)),
+ });
+
+ parameters.delete('register');
+ }
+
+ if (parameters.has('lang') && parameters.get('lang') === config.defaultLang) {
+ console.info(`The "lang" parameter was passed in to set the language to "${parameters.get('lang')}", but that is already the forum default language.`);
+ parameters.delete('lang');
+ }
+
+ const qs = parameters.toString();
+ ajaxify.updateHistory(ajaxify.currentPage + (qs ? `?${qs}` : '') + document.location.hash, true);
+ }
+
+ messages.showInvalidSession = function () {
+ bootbox.alert({
+ title: '[[error:invalid-session]]',
+ message: '[[error:invalid-session-text]]',
+ closeButton: false,
+ callback() {
+ window.location.reload();
+ },
+ });
+ };
+
+ messages.showSessionMismatch = function () {
+ bootbox.alert({
+ title: '[[error:session-mismatch]]',
+ message: '[[error:session-mismatch-text]]',
+ closeButton: false,
+ callback() {
+ window.location.reload();
+ },
+ });
+ };
+
+ return messages;
});
diff --git a/public/src/modules/navigator.js b/public/src/modules/navigator.js
index 1a84c4c..de22a11 100644
--- a/public/src/modules/navigator.js
+++ b/public/src/modules/navigator.js
@@ -1,646 +1,665 @@
'use strict';
-define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], function (pagination, components, hooks, alerts) {
- const navigator = {};
- let index = 0;
- let count = 0;
- let navigatorUpdateTimeoutId;
-
- let renderPostIntervalId;
- let touchX;
- let touchY;
- let renderPostIndex;
- let isNavigating = false;
- let firstMove = true;
-
- 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;
-
- $(window).on('action:ajaxify.start', function () {
- $(window).off('keydown', onKeyDown);
- });
-
- navigator.init = function (selector, count, toTop, toBottom, callback) {
- index = 0;
- navigator.selector = selector;
- navigator.callback = callback;
- navigator.toTop = toTop || function () {};
- navigator.toBottom = toBottom || function () {};
-
- paginationBlockEl = $('.pagination-block');
- paginationTextEl = paginationBlockEl.find('.pagination-text');
- 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');
-
- $(window).off('scroll', navigator.delayedUpdate).on('scroll', navigator.delayedUpdate);
-
- paginationBlockEl.find('.dropdown-menu').off('click').on('click', function (e) {
- e.stopPropagation();
- });
-
- paginationBlockEl.off('shown.bs.dropdown', '.wrapper').on('shown.bs.dropdown', '.wrapper', function () {
- setTimeout(async function () {
- if (utils.findBootstrapEnvironment() === 'lg') {
- $('.pagination-block input').focus();
- }
- const postCountInTopic = await socket.emit('topics.getPostCountInTopic', ajaxify.data.tid);
- if (postCountInTopic > 0) {
- paginationBlockEl.find('#myNextPostBtn').removeAttr('disabled');
- }
- }, 100);
- });
- paginationBlockEl.find('.pageup').off('click').on('click', navigator.scrollUp);
- paginationBlockEl.find('.pagedown').off('click').on('click', navigator.scrollDown);
- paginationBlockEl.find('.pagetop').off('click').on('click', navigator.toTop);
- paginationBlockEl.find('.pagebottom').off('click').on('click', navigator.toBottom);
- paginationBlockEl.find('#myNextPostBtn').off('click').on('click', gotoMyNextPost);
-
- paginationBlockEl.find('input').on('keydown', function (e) {
- if (e.which === 13) {
- const input = $(this);
- if (!utils.isNumber(input.val())) {
- input.val('');
- return;
- }
-
- const index = parseInt(input.val(), 10);
- const url = generateUrl(index);
- input.val('');
- $('.pagination-block .dropdown-toggle').trigger('click');
- ajaxify.go(url);
- }
- });
-
- if (ajaxify.data.template.topic) {
- handleScrollNav();
- }
-
- handleKeys();
-
- navigator.setCount(count);
- navigator.update(0);
- };
-
- let lastNextIndex = 0;
- async function gotoMyNextPost() {
- async function getNext(startIndex) {
- return await socket.emit('topics.getMyNextPostIndex', {
- tid: ajaxify.data.tid,
- index: Math.max(1, startIndex),
- sort: config.topicPostSort,
- });
- }
- if (ajaxify.data.template.topic) {
- let nextIndex = await getNext(index);
- if (lastNextIndex === nextIndex) { // handles last post in pagination
- nextIndex = await getNext(nextIndex);
- }
- if (nextIndex && index !== nextIndex + 1) {
- lastNextIndex = nextIndex;
- $(window).one('action:ajaxify.end', function () {
- if (paginationBlockEl.find('.dropdown-menu').is(':hidden')) {
- paginationBlockEl.find('.dropdown-toggle').dropdown('toggle');
- }
- });
- navigator.scrollToIndex(nextIndex, true, 0);
- } else {
- alerts.alert({
- message: '[[topic:no-more-next-post]]',
- type: 'info',
- });
-
- lastNextIndex = 1;
- }
- }
- }
-
- function clampTop(newTop) {
- const parent = thumb.parent();
- const parentOffset = parent.offset();
- if (newTop < parentOffset.top) {
- newTop = parentOffset.top;
- } else if (newTop > parentOffset.top + parent.height() - thumbIconHeight) {
- newTop = parentOffset.top + parent.height() - thumbIconHeight;
- }
- return newTop;
- }
-
- function setThumbToIndex(index) {
- if (!thumb.length || 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 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);
- renderPost(index);
- }
-
- function handleScrollNav() {
- if (!thumb.length) {
- return;
- }
-
- const parent = thumb.parent();
- parent.on('click', function (ev) {
- if ($(ev.target).hasClass('scroller-container')) {
- const index = calculateIndexFromY(ev.pageY);
- navigator.scrollToIndex(index - 1, true, 0);
- return false;
- }
- });
-
- function calculateIndexFromY(y) {
- const newTop = clampTop(y - thumbIconHalfHeight);
- 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;
- }
-
- let mouseDragging = false;
- hooks.on('action:ajaxify.end', function () {
- renderPostIndex = null;
- });
- $('.pagination-block .dropdown-menu').parent().on('shown.bs.dropdown', function () {
- setThumbToIndex(index);
- });
-
- thumb.on('mousedown', function () {
- mouseDragging = true;
- $(window).on('mousemove', mousemove);
- firstMove = true;
- });
-
- function mouseup() {
- $(window).off('mousemove', mousemove);
- if (mouseDragging) {
- navigator.scrollToIndex(index - 1, true, 0);
- paginationBlockEl.find('[data-toggle="dropdown"]').trigger('click');
- }
- 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();
- }
- firstMove = false;
- ev.stopPropagation();
- return false;
- }
-
- function delayedRenderPost() {
- clearRenderInterval();
- renderPostIntervalId = setInterval(function () {
- renderPost(index);
- }, 250);
- }
-
- $(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;
- });
-
- 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) {
- isNavigating = true;
- delayedRenderPost();
- }
-
- 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);
- }
- }
- firstMove = false;
- });
-
- thumb.on('touchend', function () {
- clearRenderInterval();
- if (isNavigating) {
- navigator.scrollToIndex(index - 1, true, 0);
- isNavigating = false;
- paginationBlockEl.find('[data-toggle="dropdown"]').trigger('click');
- }
- });
- }
-
- function clearRenderInterval() {
- if (renderPostIntervalId) {
- clearInterval(renderPostIntervalId);
- renderPostIntervalId = 0;
- }
- }
-
- function renderPost(index, callback) {
- callback = callback || function () {};
- if (renderPostIndex === index || paginationBlockEl.find('.post-content').is(':hidden')) {
- 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();
- });
-
- callback();
- });
- }
-
- function handleKeys() {
- if (!config.usePagination) {
- $(window).off('keydown', onKeyDown).on('keydown', onKeyDown);
- }
- }
-
- function onKeyDown(ev) {
- if (ev.target.nodeName === 'BODY') {
- if (ev.shiftKey || ev.ctrlKey || ev.altKey) {
- return;
- }
- if (ev.which === 36 && navigator.toTop) { // home key
- navigator.toTop();
- return false;
- } else if (ev.which === 35 && navigator.toBottom) { // end key
- navigator.toBottom();
- return false;
- }
- }
- }
-
- function generateUrl(index) {
- const pathname = window.location.pathname.replace(config.relative_path, '');
- const parts = pathname.split('/');
- return parts[1] + '/' + parts[2] + '/' + parts[3] + (index ? '/' + index : '');
- }
-
- navigator.getCount = () => count;
-
- navigator.setCount = function (value) {
- value = parseInt(value, 10);
- if (value === count) {
- return;
- }
- count = value;
- navigator.updateTextAndProgressBar();
- };
-
- navigator.show = function () {
- toggle(true);
- };
-
- navigator.disable = function () {
- count = 0;
- index = 1;
- navigator.callback = null;
- navigator.selector = null;
- $(window).off('scroll', navigator.delayedUpdate);
-
- toggle(false);
- };
-
- function toggle(flag) {
- const path = ajaxify.removeRelativePath(window.location.pathname.slice(1));
- if (flag && (!path.startsWith('topic') && !path.startsWith('category'))) {
- return;
- }
-
- paginationBlockEl.toggleClass('ready', flag);
- }
-
- navigator.delayedUpdate = function () {
- if (!navigatorUpdateTimeoutId) {
- navigatorUpdateTimeoutId = setTimeout(function () {
- navigator.update();
- navigatorUpdateTimeoutId = undefined;
- }, 100);
- }
- };
-
- navigator.update = function (threshold) {
- /*
+define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], (pagination, components, hooks, alerts) => {
+ const navigator = {};
+ let index = 0;
+ let count = 0;
+ let navigatorUpdateTimeoutId;
+
+ let renderPostIntervalId;
+ let touchX;
+ let touchY;
+ let renderPostIndex;
+ let isNavigating = false;
+ let firstMove = true;
+
+ navigator.scrollActive = false;
+
+ let paginationBlockElement = $('.pagination-block');
+ let paginationTextElement = paginationBlockElement.find('.pagination-text');
+ let paginationBlockMeterElement = paginationBlockElement.find('meter');
+ let paginationBlockProgressElement = paginationBlockElement.find('.progress-bar');
+ let thumb;
+ let thumbText;
+ let thumbIcon;
+ let thumbIconHeight;
+ let thumbIconHalfHeight;
+
+ $(window).on('action:ajaxify.start', () => {
+ $(window).off('keydown', onKeyDown);
+ });
+
+ navigator.init = function (selector, count, toTop, toBottom, callback) {
+ index = 0;
+ navigator.selector = selector;
+ navigator.callback = callback;
+ navigator.toTop = toTop || function () {};
+ navigator.toBottom = toBottom || function () {};
+
+ paginationBlockElement = $('.pagination-block');
+ paginationTextElement = paginationBlockElement.find('.pagination-text');
+ paginationBlockMeterElement = paginationBlockElement.find('meter');
+ paginationBlockProgressElement = paginationBlockElement.find('.progress-bar');
+
+ thumbIcon = $('.scroller-thumb-icon');
+ thumbIconHeight = thumbIcon.height();
+ thumbIconHalfHeight = thumbIconHeight / 2;
+ thumb = $('.scroller-thumb');
+ thumbText = thumb.find('.thumb-text');
+
+ $(window).off('scroll', navigator.delayedUpdate).on('scroll', navigator.delayedUpdate);
+
+ paginationBlockElement.find('.dropdown-menu').off('click').on('click', e => {
+ e.stopPropagation();
+ });
+
+ paginationBlockElement.off('shown.bs.dropdown', '.wrapper').on('shown.bs.dropdown', '.wrapper', () => {
+ setTimeout(async () => {
+ if (utils.findBootstrapEnvironment() === 'lg') {
+ $('.pagination-block input').focus();
+ }
+
+ const postCountInTopic = await socket.emit('topics.getPostCountInTopic', ajaxify.data.tid);
+ if (postCountInTopic > 0) {
+ paginationBlockElement.find('#myNextPostBtn').removeAttr('disabled');
+ }
+ }, 100);
+ });
+ paginationBlockElement.find('.pageup').off('click').on('click', navigator.scrollUp);
+ paginationBlockElement.find('.pagedown').off('click').on('click', navigator.scrollDown);
+ paginationBlockElement.find('.pagetop').off('click').on('click', navigator.toTop);
+ paginationBlockElement.find('.pagebottom').off('click').on('click', navigator.toBottom);
+ paginationBlockElement.find('#myNextPostBtn').off('click').on('click', gotoMyNextPost);
+
+ paginationBlockElement.find('input').on('keydown', function (e) {
+ if (e.which === 13) {
+ const input = $(this);
+ if (!utils.isNumber(input.val())) {
+ input.val('');
+ return;
+ }
+
+ const index = Number.parseInt(input.val(), 10);
+ const url = generateUrl(index);
+ input.val('');
+ $('.pagination-block .dropdown-toggle').trigger('click');
+ ajaxify.go(url);
+ }
+ });
+
+ if (ajaxify.data.template.topic) {
+ handleScrollNav();
+ }
+
+ handleKeys();
+
+ navigator.setCount(count);
+ navigator.update(0);
+ };
+
+ let lastNextIndex = 0;
+ async function gotoMyNextPost() {
+ async function getNext(startIndex) {
+ return await socket.emit('topics.getMyNextPostIndex', {
+ tid: ajaxify.data.tid,
+ index: Math.max(1, startIndex),
+ sort: config.topicPostSort,
+ });
+ }
+
+ if (ajaxify.data.template.topic) {
+ let nextIndex = await getNext(index);
+ if (lastNextIndex === nextIndex) { // Handles last post in pagination
+ nextIndex = await getNext(nextIndex);
+ }
+
+ if (nextIndex && index !== nextIndex + 1) {
+ lastNextIndex = nextIndex;
+ $(window).one('action:ajaxify.end', () => {
+ if (paginationBlockElement.find('.dropdown-menu').is(':hidden')) {
+ paginationBlockElement.find('.dropdown-toggle').dropdown('toggle');
+ }
+ });
+ navigator.scrollToIndex(nextIndex, true, 0);
+ } else {
+ alerts.alert({
+ message: '[[topic:no-more-next-post]]',
+ type: 'info',
+ });
+
+ lastNextIndex = 1;
+ }
+ }
+ }
+
+ function clampTop(newTop) {
+ const parent = thumb.parent();
+ const parentOffset = parent.offset();
+ if (newTop < parentOffset.top) {
+ newTop = parentOffset.top;
+ } else if (newTop > parentOffset.top + parent.height() - thumbIconHeight) {
+ newTop = parentOffset.top + parent.height() - thumbIconHeight;
+ }
+
+ return newTop;
+ }
+
+ function setThumbToIndex(index) {
+ if (thumb.length === 0 || 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 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);
+ renderPost(index);
+ }
+
+ function handleScrollNav() {
+ if (thumb.length === 0) {
+ return;
+ }
+
+ const parent = thumb.parent();
+ parent.on('click', event => {
+ if ($(event.target).hasClass('scroller-container')) {
+ const index = calculateIndexFromY(event.pageY);
+ navigator.scrollToIndex(index - 1, true, 0);
+ return false;
+ }
+ });
+
+ function calculateIndexFromY(y) {
+ const newTop = clampTop(y - thumbIconHalfHeight);
+ 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;
+ }
+
+ let mouseDragging = false;
+ hooks.on('action:ajaxify.end', () => {
+ renderPostIndex = null;
+ });
+ $('.pagination-block .dropdown-menu').parent().on('shown.bs.dropdown', () => {
+ setThumbToIndex(index);
+ });
+
+ thumb.on('mousedown', () => {
+ mouseDragging = true;
+ $(window).on('mousemove', mousemove);
+ firstMove = true;
+ });
+
+ function mouseup() {
+ $(window).off('mousemove', mousemove);
+ if (mouseDragging) {
+ navigator.scrollToIndex(index - 1, true, 0);
+ paginationBlockElement.find('[data-toggle="dropdown"]').trigger('click');
+ }
+
+ clearRenderInterval();
+ mouseDragging = false;
+ firstMove = false;
+ }
+
+ function mousemove(event) {
+ const newTop = clampTop(event.pageY - thumbIconHalfHeight);
+ thumb.offset({top: newTop, left: thumb.offset().left});
+ const index = calculateIndexFromY(event.pageY);
+ navigator.updateTextAndProgressBar();
+ thumbText.text(index + '/' + ajaxify.data.postcount);
+ if (firstMove) {
+ delayedRenderPost();
+ }
+
+ firstMove = false;
+ event.stopPropagation();
+ return false;
+ }
+
+ function delayedRenderPost() {
+ clearRenderInterval();
+ renderPostIntervalId = setInterval(() => {
+ renderPost(index);
+ }, 250);
+ }
+
+ $(window).off('mousemove', mousemove);
+ $(window).off('mouseup', mouseup).on('mouseup', mouseup);
+
+ thumb.on('touchstart', event => {
+ isNavigating = true;
+ touchX = Math.min($(window).width(), Math.max(0, event.touches[0].clientX));
+ touchY = Math.min($(window).height(), Math.max(0, event.touches[0].clientY));
+ firstMove = true;
+ });
+
+ thumb.on('touchmove', event => {
+ const windowWidth = $(window).width();
+ const windowHeight = $(window).height();
+ const deltaX = Math.abs(touchX - Math.min(windowWidth, Math.max(0, event.touches[0].clientX)));
+ const deltaY = Math.abs(touchY - Math.min(windowHeight, Math.max(0, event.touches[0].clientY)));
+ touchX = Math.min(windowWidth, Math.max(0, event.touches[0].clientX));
+ touchY = Math.min(windowHeight, Math.max(0, event.touches[0].clientY));
+
+ if (deltaY >= deltaX && firstMove) {
+ isNavigating = true;
+ delayedRenderPost();
+ }
+
+ if (isNavigating && event.cancelable) {
+ event.preventDefault();
+ event.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);
+ }
+ }
+
+ firstMove = false;
+ });
+
+ thumb.on('touchend', () => {
+ clearRenderInterval();
+ if (isNavigating) {
+ navigator.scrollToIndex(index - 1, true, 0);
+ isNavigating = false;
+ paginationBlockElement.find('[data-toggle="dropdown"]').trigger('click');
+ }
+ });
+ }
+
+ function clearRenderInterval() {
+ if (renderPostIntervalId) {
+ clearInterval(renderPostIntervalId);
+ renderPostIntervalId = 0;
+ }
+ }
+
+ function renderPost(index, callback) {
+ callback ||= function () {};
+ if (renderPostIndex === index || paginationBlockElement.find('.post-content').is(':hidden')) {
+ return;
+ }
+
+ renderPostIndex = index;
+
+ socket.emit('posts.getPostSummaryByIndex', {tid: ajaxify.data.tid, index: index - 1}, (error, postData) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ app.parseAndTranslate('partials/topic/navigation-post', {post: postData}, html => {
+ paginationBlockElement
+ .find('.post-content')
+ .html(html)
+ .find('.timeago').timeago();
+ });
+
+ callback();
+ });
+ }
+
+ function handleKeys() {
+ if (!config.usePagination) {
+ $(window).off('keydown', onKeyDown).on('keydown', onKeyDown);
+ }
+ }
+
+ function onKeyDown(event) {
+ if (event.target.nodeName === 'BODY') {
+ if (event.shiftKey || event.ctrlKey || event.altKey) {
+ return;
+ }
+
+ if (event.which === 36 && navigator.toTop) { // Home key
+ navigator.toTop();
+ return false;
+ }
+
+ if (event.which === 35 && navigator.toBottom) { // End key
+ navigator.toBottom();
+ return false;
+ }
+ }
+ }
+
+ function generateUrl(index) {
+ const pathname = window.location.pathname.replace(config.relative_path, '');
+ const parts = pathname.split('/');
+ return parts[1] + '/' + parts[2] + '/' + parts[3] + (index ? '/' + index : '');
+ }
+
+ navigator.getCount = () => count;
+
+ navigator.setCount = function (value) {
+ value = Number.parseInt(value, 10);
+ if (value === count) {
+ return;
+ }
+
+ count = value;
+ navigator.updateTextAndProgressBar();
+ };
+
+ navigator.show = function () {
+ toggle(true);
+ };
+
+ navigator.disable = function () {
+ count = 0;
+ index = 1;
+ navigator.callback = null;
+ navigator.selector = null;
+ $(window).off('scroll', navigator.delayedUpdate);
+
+ toggle(false);
+ };
+
+ function toggle(flag) {
+ const path = ajaxify.removeRelativePath(window.location.pathname.slice(1));
+ if (flag && (!path.startsWith('topic') && !path.startsWith('category'))) {
+ return;
+ }
+
+ paginationBlockElement.toggleClass('ready', flag);
+ }
+
+ navigator.delayedUpdate = function () {
+ navigatorUpdateTimeoutId ||= setTimeout(() => {
+ navigator.update();
+ navigatorUpdateTimeoutId = undefined;
+ }, 100);
+ };
+
+ navigator.update = function (threshold) {
+ /*
The "threshold" is defined as the distance from the top of the page to
a spot where a user is expecting to begin reading.
*/
- threshold = typeof threshold === 'number' ? threshold : undefined;
- let newIndex = index;
- const els = $(navigator.selector);
- if (els.length) {
- newIndex = parseInt(els.first().attr('data-index'), 10) + 1;
- }
-
- const scrollTop = $(window).scrollTop();
- const windowHeight = $(window).height();
- const documentHeight = $(document).height();
- const middleOfViewport = scrollTop + (windowHeight / 2);
- let previousDistance = Number.MAX_VALUE;
- els.each(function () {
- const $this = $(this);
- const elIndex = parseInt($this.attr('data-index'), 10);
- if (elIndex >= 0) {
- const distanceToMiddle =
- Math.abs(middleOfViewport - ($this.offset().top + ($this.outerHeight(true) / 2)));
-
- if (distanceToMiddle > previousDistance) {
- return false;
- }
-
- if (distanceToMiddle < previousDistance) {
- newIndex = elIndex + 1;
- previousDistance = distanceToMiddle;
- }
- }
- });
-
- const atTop = scrollTop === 0 && parseInt(els.first().attr('data-index'), 10) === 0;
- const nearBottom = scrollTop + windowHeight > documentHeight - 100 && parseInt(els.last().attr('data-index'), 10) === count - 1;
-
- if (atTop) {
- newIndex = 1;
- } else if (nearBottom) {
- newIndex = count;
- }
-
- // If a threshold is undefined, try to determine one based on new index
- if (threshold === undefined && ajaxify.data.template.topic) {
- if (atTop) {
- threshold = 0;
- } else {
- const anchorEl = components.get('post/anchor', index - 1);
- if (anchorEl.length) {
- const anchorRect = anchorEl.get(0).getBoundingClientRect();
- threshold = anchorRect.top;
- }
- }
- }
-
- if (typeof navigator.callback === 'function') {
- navigator.callback(newIndex, count, threshold);
- }
-
- if (newIndex !== index) {
- index = newIndex;
- navigator.updateTextAndProgressBar();
- setThumbToIndex(index);
- }
-
- toggle(!!count);
- };
-
- navigator.getIndex = () => index;
-
- navigator.setIndex = (newIndex) => {
- index = newIndex + 1;
- navigator.updateTextAndProgressBar();
- setThumbToIndex(index);
- };
-
- navigator.updateTextAndProgressBar = function () {
- if (!utils.isNumber(index)) {
- return;
- }
- index = index > count ? count : index;
- paginationTextEl.translateHtml('[[global:pagination.out_of, ' + index + ', ' + count + ']]');
- const fraction = (index - 1) / (count - 1 || 1);
- paginationBlockMeterEl.val(fraction);
- paginationBlockProgressEl.width((fraction * 100) + '%');
- };
-
- navigator.scrollUp = function () {
- const $window = $(window);
-
- if (config.usePagination) {
- const atTop = $window.scrollTop() <= 0;
- if (atTop) {
- return pagination.previousPage(function () {
- $('body,html').scrollTop($(document).height() - $window.height());
- });
- }
- }
- $('body,html').animate({
- scrollTop: $window.scrollTop() - $window.height(),
- });
- };
-
- navigator.scrollDown = function () {
- const $window = $(window);
-
- if (config.usePagination) {
- const atBottom = $window.scrollTop() >= $(document).height() - $window.height();
- if (atBottom) {
- return pagination.nextPage();
- }
- }
- $('body,html').animate({
- scrollTop: $window.scrollTop() + $window.height(),
- });
- };
-
- navigator.scrollTop = function (index) {
- if ($(navigator.selector + '[data-index="' + index + '"]').length) {
- navigator.scrollToIndex(index, true);
- } else {
- ajaxify.go(generateUrl());
- }
- };
-
- navigator.scrollBottom = function (index) {
- if (parseInt(index, 10) < 0) {
- return;
- }
-
- if ($(navigator.selector + '[data-index="' + index + '"]').length) {
- navigator.scrollToIndex(index, true);
- } else {
- index = parseInt(index, 10) + 1;
- ajaxify.go(generateUrl(index));
- }
- };
-
- navigator.scrollToIndex = function (index, highlight, duration) {
- const inTopic = !!components.get('topic').length;
- const inCategory = !!components.get('category').length;
-
- if (!utils.isNumber(index) || (!inTopic && !inCategory)) {
- return;
- }
-
- duration = duration !== undefined ? duration : 400;
- navigator.scrollActive = true;
-
- // if in topic and item already on page
- if (inTopic && components.get('post/anchor', index).length) {
- return navigator.scrollToPostIndex(index, highlight, duration);
- }
-
- // if in category and item alreay on page
- if (inCategory && $('[component="category/topic"][data-index="' + index + '"]').length) {
- return navigator.scrollToTopicIndex(index, highlight, duration);
- }
-
- if (!config.usePagination) {
- navigator.scrollActive = false;
- index = parseInt(index, 10) + 1;
- ajaxify.go(generateUrl(index));
- return;
- }
-
- const scrollMethod = inTopic ? navigator.scrollToPostIndex : navigator.scrollToTopicIndex;
-
- const page = 1 + Math.floor(index / config.postsPerPage);
- if (parseInt(page, 10) !== ajaxify.data.pagination.currentPage) {
- pagination.loadPage(page, function () {
- scrollMethod(index, highlight, duration);
- });
- } else {
- scrollMethod(index, highlight, duration);
- }
- };
-
- navigator.scrollToPostIndex = function (postIndex, highlight, duration) {
- const scrollTo = components.get('post', 'index', postIndex);
- navigator.scrollToElement(scrollTo, highlight, duration, postIndex);
- };
-
- navigator.scrollToTopicIndex = function (topicIndex, highlight, duration) {
- const scrollTo = $('[component="category/topic"][data-index="' + topicIndex + '"]');
- navigator.scrollToElement(scrollTo, highlight, duration, topicIndex);
- };
-
- navigator.scrollToElement = async (scrollTo, highlight, duration, newIndex = null) => {
- if (!scrollTo.length) {
- navigator.scrollActive = false;
- return;
- }
-
- await hooks.fire('filter:navigator.scroll', { scrollTo, highlight, duration, newIndex });
-
- const postHeight = scrollTo.outerHeight(true);
- const navbarHeight = components.get('navbar').outerHeight(true) || 0;
- const topicHeaderHeight = $('.topic-header').outerHeight(true) || 0;
- const viewportHeight = $(window).height();
-
- // Temporarily disable navigator update on scroll
- $(window).off('scroll', navigator.delayedUpdate);
-
- duration = duration !== undefined ? duration : 400;
- navigator.scrollActive = true;
- let done = false;
-
- function animateScroll() {
- function reenableScroll() {
- // Re-enable onScroll behaviour
- setTimeout(() => { // fixes race condition from jQuery — onAnimateComplete called too quickly
- $(window).on('scroll', navigator.delayedUpdate);
-
- hooks.fire('action:navigator.scrolled', { scrollTo, highlight, duration, newIndex });
- }, 50);
- }
- function onAnimateComplete() {
- if (done) {
- reenableScroll();
- return;
- }
- done = true;
-
- navigator.scrollActive = false;
- highlightPost();
-
- const scrollToRect = scrollTo.get(0).getBoundingClientRect();
- if (!newIndex) {
- navigator.update(scrollToRect.top);
- } else {
- navigator.setIndex(newIndex);
- }
- }
-
- let scrollTop = 0;
- if (postHeight < viewportHeight - navbarHeight - topicHeaderHeight) {
- scrollTop = scrollTo.offset().top - (viewportHeight / 2) + (postHeight / 2);
- } else {
- scrollTop = scrollTo.offset().top - navbarHeight - topicHeaderHeight;
- }
-
- if (duration === 0) {
- $(window).scrollTop(scrollTop);
- onAnimateComplete();
- reenableScroll();
- return;
- }
- $('html, body').animate({
- scrollTop: scrollTop + 'px',
- }, duration, onAnimateComplete);
- }
-
- function highlightPost() {
- if (highlight) {
- $('[component="post"],[component="category/topic"]').removeClass('highlight');
- scrollTo.addClass('highlight');
- setTimeout(function () {
- scrollTo.removeClass('highlight');
- }, 10000);
- }
- }
-
- animateScroll();
- };
-
- return navigator;
+ threshold = typeof threshold === 'number' ? threshold : undefined;
+ let newIndex = index;
+ const els = $(navigator.selector);
+ if (els.length > 0) {
+ newIndex = Number.parseInt(els.first().attr('data-index'), 10) + 1;
+ }
+
+ const scrollTop = $(window).scrollTop();
+ const windowHeight = $(window).height();
+ const documentHeight = $(document).height();
+ const middleOfViewport = scrollTop + (windowHeight / 2);
+ let previousDistance = Number.MAX_VALUE;
+ els.each(function () {
+ const $this = $(this);
+ const elementIndex = Number.parseInt($this.attr('data-index'), 10);
+ if (elementIndex >= 0) {
+ const distanceToMiddle
+ = Math.abs(middleOfViewport - ($this.offset().top + ($this.outerHeight(true) / 2)));
+
+ if (distanceToMiddle > previousDistance) {
+ return false;
+ }
+
+ if (distanceToMiddle < previousDistance) {
+ newIndex = elementIndex + 1;
+ previousDistance = distanceToMiddle;
+ }
+ }
+ });
+
+ const atTop = scrollTop === 0 && Number.parseInt(els.first().attr('data-index'), 10) === 0;
+ const nearBottom = scrollTop + windowHeight > documentHeight - 100 && Number.parseInt(els.last().attr('data-index'), 10) === count - 1;
+
+ if (atTop) {
+ newIndex = 1;
+ } else if (nearBottom) {
+ newIndex = count;
+ }
+
+ // If a threshold is undefined, try to determine one based on new index
+ if (threshold === undefined && ajaxify.data.template.topic) {
+ if (atTop) {
+ threshold = 0;
+ } else {
+ const anchorElement = components.get('post/anchor', index - 1);
+ if (anchorElement.length > 0) {
+ const anchorRect = anchorElement.get(0).getBoundingClientRect();
+ threshold = anchorRect.top;
+ }
+ }
+ }
+
+ if (typeof navigator.callback === 'function') {
+ navigator.callback(newIndex, count, threshold);
+ }
+
+ if (newIndex !== index) {
+ index = newIndex;
+ navigator.updateTextAndProgressBar();
+ setThumbToIndex(index);
+ }
+
+ toggle(Boolean(count));
+ };
+
+ navigator.getIndex = () => index;
+
+ navigator.setIndex = newIndex => {
+ index = newIndex + 1;
+ navigator.updateTextAndProgressBar();
+ setThumbToIndex(index);
+ };
+
+ navigator.updateTextAndProgressBar = function () {
+ if (!utils.isNumber(index)) {
+ return;
+ }
+
+ index = index > count ? count : index;
+ paginationTextElement.translateHtml('[[global:pagination.out_of, ' + index + ', ' + count + ']]');
+ const fraction = (index - 1) / (count - 1 || 1);
+ paginationBlockMeterElement.val(fraction);
+ paginationBlockProgressElement.width((fraction * 100) + '%');
+ };
+
+ navigator.scrollUp = function () {
+ const $window = $(window);
+
+ if (config.usePagination) {
+ const atTop = $window.scrollTop() <= 0;
+ if (atTop) {
+ return pagination.previousPage(() => {
+ $('body,html').scrollTop($(document).height() - $window.height());
+ });
+ }
+ }
+
+ $('body,html').animate({
+ scrollTop: $window.scrollTop() - $window.height(),
+ });
+ };
+
+ navigator.scrollDown = function () {
+ const $window = $(window);
+
+ if (config.usePagination) {
+ const atBottom = $window.scrollTop() >= $(document).height() - $window.height();
+ if (atBottom) {
+ return pagination.nextPage();
+ }
+ }
+
+ $('body,html').animate({
+ scrollTop: $window.scrollTop() + $window.height(),
+ });
+ };
+
+ navigator.scrollTop = function (index) {
+ if ($(navigator.selector + '[data-index="' + index + '"]').length > 0) {
+ navigator.scrollToIndex(index, true);
+ } else {
+ ajaxify.go(generateUrl());
+ }
+ };
+
+ navigator.scrollBottom = function (index) {
+ if (Number.parseInt(index, 10) < 0) {
+ return;
+ }
+
+ if ($(navigator.selector + '[data-index="' + index + '"]').length > 0) {
+ navigator.scrollToIndex(index, true);
+ } else {
+ index = Number.parseInt(index, 10) + 1;
+ ajaxify.go(generateUrl(index));
+ }
+ };
+
+ navigator.scrollToIndex = function (index, highlight, duration) {
+ const inTopic = components.get('topic').length > 0;
+ const inCategory = components.get('category').length > 0;
+
+ if (!utils.isNumber(index) || (!inTopic && !inCategory)) {
+ return;
+ }
+
+ duration = duration === undefined ? 400 : duration;
+ navigator.scrollActive = true;
+
+ // If in topic and item already on page
+ if (inTopic && components.get('post/anchor', index).length > 0) {
+ return navigator.scrollToPostIndex(index, highlight, duration);
+ }
+
+ // If in category and item alreay on page
+ if (inCategory && $('[component="category/topic"][data-index="' + index + '"]').length > 0) {
+ return navigator.scrollToTopicIndex(index, highlight, duration);
+ }
+
+ if (!config.usePagination) {
+ navigator.scrollActive = false;
+ index = Number.parseInt(index, 10) + 1;
+ ajaxify.go(generateUrl(index));
+ return;
+ }
+
+ const scrollMethod = inTopic ? navigator.scrollToPostIndex : navigator.scrollToTopicIndex;
+
+ const page = 1 + Math.floor(index / config.postsPerPage);
+ if (Number.parseInt(page, 10) === ajaxify.data.pagination.currentPage) {
+ scrollMethod(index, highlight, duration);
+ } else {
+ pagination.loadPage(page, () => {
+ scrollMethod(index, highlight, duration);
+ });
+ }
+ };
+
+ navigator.scrollToPostIndex = function (postIndex, highlight, duration) {
+ const scrollTo = components.get('post', 'index', postIndex);
+ navigator.scrollToElement(scrollTo, highlight, duration, postIndex);
+ };
+
+ navigator.scrollToTopicIndex = function (topicIndex, highlight, duration) {
+ const scrollTo = $('[component="category/topic"][data-index="' + topicIndex + '"]');
+ navigator.scrollToElement(scrollTo, highlight, duration, topicIndex);
+ };
+
+ navigator.scrollToElement = async (scrollTo, highlight, duration, newIndex = null) => {
+ if (scrollTo.length === 0) {
+ navigator.scrollActive = false;
+ return;
+ }
+
+ await hooks.fire('filter:navigator.scroll', {
+ scrollTo, highlight, duration, newIndex,
+ });
+
+ const postHeight = scrollTo.outerHeight(true);
+ const navbarHeight = components.get('navbar').outerHeight(true) || 0;
+ const topicHeaderHeight = $('.topic-header').outerHeight(true) || 0;
+ const viewportHeight = $(window).height();
+
+ // Temporarily disable navigator update on scroll
+ $(window).off('scroll', navigator.delayedUpdate);
+
+ duration = duration === undefined ? 400 : duration;
+ navigator.scrollActive = true;
+ let done = false;
+
+ function animateScroll() {
+ function reenableScroll() {
+ // Re-enable onScroll behaviour
+ setTimeout(() => { // Fixes race condition from jQuery — onAnimateComplete called too quickly
+ $(window).on('scroll', navigator.delayedUpdate);
+
+ hooks.fire('action:navigator.scrolled', {
+ scrollTo, highlight, duration, newIndex,
+ });
+ }, 50);
+ }
+
+ function onAnimateComplete() {
+ if (done) {
+ reenableScroll();
+ return;
+ }
+
+ done = true;
+
+ navigator.scrollActive = false;
+ highlightPost();
+
+ const scrollToRect = scrollTo.get(0).getBoundingClientRect();
+ if (newIndex) {
+ navigator.setIndex(newIndex);
+ } else {
+ navigator.update(scrollToRect.top);
+ }
+ }
+
+ let scrollTop = 0;
+ scrollTop = postHeight < viewportHeight - navbarHeight - topicHeaderHeight ? scrollTo.offset().top - (viewportHeight / 2) + (postHeight / 2) : scrollTo.offset().top - navbarHeight - topicHeaderHeight;
+
+ if (duration === 0) {
+ $(window).scrollTop(scrollTop);
+ onAnimateComplete();
+ reenableScroll();
+ return;
+ }
+
+ $('html, body').animate({
+ scrollTop: scrollTop + 'px',
+ }, duration, onAnimateComplete);
+ }
+
+ function highlightPost() {
+ if (highlight) {
+ $('[component="post"],[component="category/topic"]').removeClass('highlight');
+ scrollTo.addClass('highlight');
+ setTimeout(() => {
+ scrollTo.removeClass('highlight');
+ }, 10_000);
+ }
+ }
+
+ animateScroll();
+ };
+
+ return navigator;
});
diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js
index da512fd..1f5703b 100644
--- a/public/src/modules/notifications.js
+++ b/public/src/modules/notifications.js
@@ -1,160 +1,162 @@
'use strict';
-
define('notifications', [
- 'translator',
- 'components',
- 'navigator',
- 'tinycon',
- 'hooks',
- 'alerts',
-], function (translator, components, navigator, Tinycon, hooks, alerts) {
- const Notifications = {};
-
- let unreadNotifs = {};
-
- const _addShortTimeagoString = ({ notifications: notifs }) => new Promise((resolve) => {
- translator.toggleTimeagoShorthand(function () {
- for (let i = 0; i < notifs.length; i += 1) {
- notifs[i].timeago = $.timeago(new Date(parseInt(notifs[i].datetime, 10)));
- }
- translator.toggleTimeagoShorthand();
- resolve({ notifications: notifs });
- });
- });
- hooks.on('filter:notifications.load', _addShortTimeagoString);
-
- Notifications.loadNotifications = function (notifList, callback) {
- callback = callback || function () {};
- socket.emit('notifications.get', null, function (err, data) {
- if (err) {
- return alerts.error(err);
- }
-
- const notifs = data.unread.concat(data.read).sort(function (a, b) {
- return parseInt(a.datetime, 10) > parseInt(b.datetime, 10) ? -1 : 1;
- });
-
- hooks.fire('filter:notifications.load', { notifications: notifs }).then(({ notifications }) => {
- app.parseAndTranslate('partials/notifications_list', { notifications }, function (html) {
- notifList.html(html);
- notifList.off('click').on('click', '[data-nid]', function (ev) {
- const notifEl = $(this);
- if (scrollToPostIndexIfOnPage(notifEl)) {
- ev.stopPropagation();
- ev.preventDefault();
- components.get('notifications/list').dropdown('toggle');
- }
-
- const unread = notifEl.hasClass('unread');
- if (!unread) {
- return;
- }
- const nid = notifEl.attr('data-nid');
- markNotification(nid, true);
- });
- components.get('notifications').on('click', '.mark-all-read', Notifications.markAllRead);
-
- notifList.on('click', '.mark-read', function () {
- const liEl = $(this).parents('li');
- const unread = liEl.hasClass('unread');
- const nid = liEl.attr('data-nid');
- markNotification(nid, unread, function () {
- liEl.toggleClass('unread');
- });
- return false;
- });
-
- hooks.fire('action:notifications.loaded', {
- notifications: notifs,
- list: notifList,
- });
- callback();
- });
- });
- });
- };
-
- Notifications.onNewNotification = function (notifData) {
- if (ajaxify.currentPage === 'notifications') {
- ajaxify.refresh();
- }
-
- socket.emit('notifications.getCount', function (err, count) {
- if (err) {
- return alerts.error(err);
- }
-
- Notifications.updateNotifCount(count);
- });
-
- if (!unreadNotifs[notifData.nid]) {
- unreadNotifs[notifData.nid] = true;
- }
- };
-
- function markNotification(nid, read, callback) {
- socket.emit('notifications.mark' + (read ? 'Read' : 'Unread'), nid, function (err) {
- if (err) {
- return alerts.error(err);
- }
-
- if (read && unreadNotifs[nid]) {
- delete unreadNotifs[nid];
- }
- if (callback) {
- callback();
- }
- });
- }
-
- function scrollToPostIndexIfOnPage(notifEl) {
- // Scroll to index if already in topic (gh#5873)
- const pid = notifEl.attr('data-pid');
- const path = notifEl.attr('data-path');
- const postEl = components.get('post', 'pid', pid);
- if (path.startsWith(config.relative_path + '/post/') && pid && postEl.length && ajaxify.data.template.topic) {
- navigator.scrollToIndex(postEl.attr('data-index'), true);
- return true;
- }
- return false;
- }
-
- Notifications.updateNotifCount = function (count) {
- const notifIcon = components.get('notifications/icon');
- count = Math.max(0, count);
- if (count > 0) {
- notifIcon.removeClass('fa-bell-o').addClass('fa-bell');
- } else {
- notifIcon.removeClass('fa-bell').addClass('fa-bell-o');
- }
-
- notifIcon.toggleClass('unread-count', count > 0);
- notifIcon.attr('data-content', count > 99 ? '99+' : count);
-
- const payload = {
- count: count,
- updateFavicon: true,
- };
- hooks.fire('action:notification.updateCount', payload);
-
- if (payload.updateFavicon) {
- Tinycon.setBubble(count > 99 ? '99+' : count);
- }
-
- if (navigator.setAppBadge) { // feature detection
- navigator.setAppBadge(count);
- }
- };
-
- Notifications.markAllRead = function () {
- socket.emit('notifications.markAllRead', function (err) {
- if (err) {
- alerts.error(err);
- }
- unreadNotifs = {};
- });
- };
-
- return Notifications;
+ 'translator',
+ 'components',
+ 'navigator',
+ 'tinycon',
+ 'hooks',
+ 'alerts',
+], (translator, components, navigator, Tinycon, hooks, alerts) => {
+ const Notifications = {};
+
+ let unreadNotifs = {};
+
+ const _addShortTimeagoString = ({notifications: notifs}) => new Promise(resolve => {
+ translator.toggleTimeagoShorthand(() => {
+ for (const notification of notifs) {
+ notification.timeago = $.timeago(new Date(Number.parseInt(notification.datetime, 10)));
+ }
+
+ translator.toggleTimeagoShorthand();
+ resolve({notifications: notifs});
+ });
+ });
+ hooks.on('filter:notifications.load', _addShortTimeagoString);
+
+ Notifications.loadNotifications = function (notificationList, callback) {
+ callback ||= function () {};
+ socket.emit('notifications.get', null, (error, data) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ const notifs = data.unread.concat(data.read).sort((a, b) => Number.parseInt(a.datetime, 10) > Number.parseInt(b.datetime, 10) ? -1 : 1);
+
+ hooks.fire('filter:notifications.load', {notifications: notifs}).then(({notifications}) => {
+ app.parseAndTranslate('partials/notifications_list', {notifications}, html => {
+ notificationList.html(html);
+ notificationList.off('click').on('click', '[data-nid]', function (event) {
+ const notificationElement = $(this);
+ if (scrollToPostIndexIfOnPage(notificationElement)) {
+ event.stopPropagation();
+ event.preventDefault();
+ components.get('notifications/list').dropdown('toggle');
+ }
+
+ const unread = notificationElement.hasClass('unread');
+ if (!unread) {
+ return;
+ }
+
+ const nid = notificationElement.attr('data-nid');
+ markNotification(nid, true);
+ });
+ components.get('notifications').on('click', '.mark-all-read', Notifications.markAllRead);
+
+ notificationList.on('click', '.mark-read', function () {
+ const liElement = $(this).parents('li');
+ const unread = liElement.hasClass('unread');
+ const nid = liElement.attr('data-nid');
+ markNotification(nid, unread, () => {
+ liElement.toggleClass('unread');
+ });
+ return false;
+ });
+
+ hooks.fire('action:notifications.loaded', {
+ notifications: notifs,
+ list: notificationList,
+ });
+ callback();
+ });
+ });
+ });
+ };
+
+ Notifications.onNewNotification = function (notificationData) {
+ if (ajaxify.currentPage === 'notifications') {
+ ajaxify.refresh();
+ }
+
+ socket.emit('notifications.getCount', (error, count) => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ Notifications.updateNotifCount(count);
+ });
+
+ if (!unreadNotifs[notificationData.nid]) {
+ unreadNotifs[notificationData.nid] = true;
+ }
+ };
+
+ function markNotification(nid, read, callback) {
+ socket.emit('notifications.mark' + (read ? 'Read' : 'Unread'), nid, error => {
+ if (error) {
+ return alerts.error(error);
+ }
+
+ if (read && unreadNotifs[nid]) {
+ delete unreadNotifs[nid];
+ }
+
+ if (callback) {
+ callback();
+ }
+ });
+ }
+
+ function scrollToPostIndexIfOnPage(notificationElement) {
+ // Scroll to index if already in topic (gh#5873)
+ const pid = notificationElement.attr('data-pid');
+ const path = notificationElement.attr('data-path');
+ const postElement = components.get('post', 'pid', pid);
+ if (path.startsWith(config.relative_path + '/post/') && pid && postElement.length > 0 && ajaxify.data.template.topic) {
+ navigator.scrollToIndex(postElement.attr('data-index'), true);
+ return true;
+ }
+
+ return false;
+ }
+
+ Notifications.updateNotifCount = function (count) {
+ const notificationIcon = components.get('notifications/icon');
+ count = Math.max(0, count);
+ if (count > 0) {
+ notificationIcon.removeClass('fa-bell-o').addClass('fa-bell');
+ } else {
+ notificationIcon.removeClass('fa-bell').addClass('fa-bell-o');
+ }
+
+ notificationIcon.toggleClass('unread-count', count > 0);
+ notificationIcon.attr('data-content', count > 99 ? '99+' : count);
+
+ const payload = {
+ count,
+ updateFavicon: true,
+ };
+ hooks.fire('action:notification.updateCount', payload);
+
+ if (payload.updateFavicon) {
+ Tinycon.setBubble(count > 99 ? '99+' : count);
+ }
+
+ if (navigator.setAppBadge) { // Feature detection
+ navigator.setAppBadge(count);
+ }
+ };
+
+ Notifications.markAllRead = function () {
+ socket.emit('notifications.markAllRead', error => {
+ if (error) {
+ alerts.error(error);
+ }
+
+ unreadNotifs = {};
+ });
+ };
+
+ return Notifications;
});
diff --git a/public/src/modules/postSelect.js b/public/src/modules/postSelect.js
index e1f266d..193e594 100644
--- a/public/src/modules/postSelect.js
+++ b/public/src/modules/postSelect.js
@@ -1,73 +1,74 @@
'use strict';
+define('postSelect', ['components'], components => {
+ const PostSelect = {};
+ let onSelect;
-define('postSelect', ['components'], function (components) {
- const PostSelect = {};
- let onSelect;
+ PostSelect.pids = [];
- PostSelect.pids = [];
+ let allowMainPostSelect = false;
- let allowMainPostSelect = false;
+ PostSelect.init = function (_onSelect, options) {
+ PostSelect.pids.length = 0;
+ onSelect = _onSelect;
+ options ||= {};
+ allowMainPostSelect = options.allowMainPostSelect || false;
+ $('#content').on('click', '[component="topic"] [component="post"]', onPostClicked);
+ disableClicksOnPosts();
+ };
- PostSelect.init = function (_onSelect, options) {
- PostSelect.pids.length = 0;
- onSelect = _onSelect;
- options = options || {};
- allowMainPostSelect = options.allowMainPostSelect || false;
- $('#content').on('click', '[component="topic"] [component="post"]', onPostClicked);
- disableClicksOnPosts();
- };
+ function onPostClicked(event) {
+ event.stopPropagation();
+ const pidClicked = $(this).attr('data-pid');
+ const postEls = $('[component="topic"] [data-pid="' + pidClicked + '"]');
+ if (!allowMainPostSelect && Number.parseInt($(this).attr('data-index'), 10) === 0) {
+ return;
+ }
- function onPostClicked(ev) {
- ev.stopPropagation();
- const pidClicked = $(this).attr('data-pid');
- const postEls = $('[component="topic"] [data-pid="' + pidClicked + '"]');
- if (!allowMainPostSelect && parseInt($(this).attr('data-index'), 10) === 0) {
- return;
- }
- PostSelect.togglePostSelection(postEls, pidClicked);
- }
+ PostSelect.togglePostSelection(postEls, pidClicked);
+ }
- PostSelect.disable = function () {
- PostSelect.pids.forEach(function (pid) {
- components.get('post', 'pid', pid).toggleClass('bg-success', false);
- });
+ PostSelect.disable = function () {
+ for (const pid of PostSelect.pids) {
+ components.get('post', 'pid', pid).toggleClass('bg-success', false);
+ }
- $('#content').off('click', '[component="topic"] [component="post"]', onPostClicked);
- enableClicksOnPosts();
- };
+ $('#content').off('click', '[component="topic"] [component="post"]', onPostClicked);
+ enableClicksOnPosts();
+ };
- PostSelect.togglePostSelection = function (postEls, pid) {
- if (pid) {
- const index = PostSelect.pids.indexOf(pid);
- if (index === -1) {
- PostSelect.pids.push(pid);
- postEls.toggleClass('bg-success', true);
- } else {
- PostSelect.pids.splice(index, 1);
- postEls.toggleClass('bg-success', false);
- }
+ PostSelect.togglePostSelection = function (postEls, pid) {
+ if (pid) {
+ const index = PostSelect.pids.indexOf(pid);
+ if (index === -1) {
+ PostSelect.pids.push(pid);
+ postEls.toggleClass('bg-success', true);
+ } else {
+ PostSelect.pids.splice(index, 1);
+ postEls.toggleClass('bg-success', false);
+ }
- if (PostSelect.pids.length) {
- PostSelect.pids.sort(function (a, b) { return a - b; });
- }
- if (typeof onSelect === 'function') {
- onSelect();
- }
- }
- };
+ if (PostSelect.pids.length > 0) {
+ PostSelect.pids.sort((a, b) => a - b);
+ }
- function disableClicks() {
- return false;
- }
+ if (typeof onSelect === 'function') {
+ onSelect();
+ }
+ }
+ };
- function disableClicksOnPosts() {
- $('#content').on('click', '[component="post"] button, [component="post"] a', disableClicks);
- }
+ function disableClicks() {
+ return false;
+ }
- function enableClicksOnPosts() {
- $('#content').off('click', '[component="post"] button, [component="post"] a', disableClicks);
- }
+ function disableClicksOnPosts() {
+ $('#content').on('click', '[component="post"] button, [component="post"] a', disableClicks);
+ }
- return PostSelect;
+ function enableClicksOnPosts() {
+ $('#content').off('click', '[component="post"] button, [component="post"] a', disableClicks);
+ }
+
+ return PostSelect;
});
diff --git a/public/src/modules/scrollStop.js b/public/src/modules/scrollStop.js
index 12f2562..98c4645 100644
--- a/public/src/modules/scrollStop.js
+++ b/public/src/modules/scrollStop.js
@@ -1,6 +1,5 @@
'use strict';
-
/*
The point of this library is to enhance(tm) a textarea so that if scrolled,
you can only scroll to the top of it and the event doesn't bubble up to
@@ -9,23 +8,23 @@
While I'm here, might I say this is a solved issue on Linux?
*/
-define('scrollStop', function () {
- const Module = {};
+define('scrollStop', () => {
+ const Module = {};
- Module.apply = function (element) {
- $(element).on('mousewheel', function (e) {
- const scrollTop = this.scrollTop;
- const scrollHeight = this.scrollHeight;
- const elementHeight = Math.round(this.getBoundingClientRect().height);
+ Module.apply = function (element) {
+ $(element).on('mousewheel', function (e) {
+ const scrollTop = this.scrollTop;
+ const scrollHeight = this.scrollHeight;
+ const elementHeight = Math.round(this.getBoundingClientRect().height);
- if (
- (e.originalEvent.deltaY < 0 && scrollTop === 0) || // scroll up
- (e.originalEvent.deltaY > 0 && (elementHeight + scrollTop) >= scrollHeight) // scroll down
- ) {
- return false;
- }
- });
- };
+ if (
+ (e.originalEvent.deltaY < 0 && scrollTop === 0) // Scroll up
+ || (e.originalEvent.deltaY > 0 && (elementHeight + scrollTop) >= scrollHeight) // Scroll down
+ ) {
+ return false;
+ }
+ });
+ };
- return Module;
+ return Module;
});
diff --git a/public/src/modules/search.js b/public/src/modules/search.js
index b055336..6993cf5 100644
--- a/public/src/modules/search.js
+++ b/public/src/modules/search.js
@@ -1,345 +1,349 @@
'use strict';
-define('search', ['translator', 'storage', 'hooks', 'alerts'], function (translator, storage, hooks, alerts) {
- const Search = {
- current: {},
- };
-
- Search.init = function (searchOptions) {
- if (!config.searchEnabled) {
- return;
- }
-
- searchOptions = searchOptions || { in: config.searchDefaultInQuick || 'titles' };
- const searchButton = $('#search-button');
- const searchFields = $('#search-fields');
- const searchInput = $('#search-fields input');
- const quickSearchContainer = $('#quick-search-container');
-
- $('#search-form .advanced-search-link').off('mousedown').on('mousedown', function () {
- ajaxify.go('/search');
- });
-
- $('#search-form').off('submit').on('submit', function () {
- searchInput.blur();
- });
- searchInput.off('blur').on('blur', function dismissSearch() {
- setTimeout(function () {
- if (!searchInput.is(':focus')) {
- searchFields.addClass('hidden');
- searchButton.removeClass('hidden');
- }
- }, 200);
- });
- searchInput.off('focus');
-
- const searchElements = {
- inputEl: searchInput,
- resultEl: quickSearchContainer,
- };
-
- Search.enableQuickSearch({
- searchOptions: searchOptions,
- searchElements: searchElements,
- });
-
- searchButton.off('click').on('click', function (e) {
- if (!config.loggedIn && !app.user.privileges['search:content']) {
- alerts.alert({
- message: '[[error:search-requires-login]]',
- timeout: 3000,
- });
- ajaxify.go('login');
- return false;
- }
- e.stopPropagation();
-
- Search.showAndFocusInput();
- return false;
- });
-
- $('#search-form').off('submit').on('submit', function () {
- const input = $(this).find('input');
- const data = Search.getSearchPreferences();
- data.term = input.val();
- data.in = searchOptions.in;
- hooks.fire('action:search.submit', {
- searchOptions: data,
- searchElements: searchElements,
- });
- Search.query(data, function () {
- input.val('');
- });
-
- return false;
- });
- };
-
- Search.enableQuickSearch = function (options) {
- if (!config.searchEnabled || !app.user.privileges['search:content']) {
- return;
- }
-
- const searchOptions = Object.assign({ in: config.searchDefaultInQuick || 'titles' }, options.searchOptions);
- const quickSearchResults = options.searchElements.resultEl;
- const inputEl = options.searchElements.inputEl;
- let oldValue = inputEl.val();
- const filterCategoryEl = quickSearchResults.find('.filter-category');
-
- function updateCategoryFilterName() {
- if (ajaxify.data.template.category && ajaxify.data.cid) {
- translator.translate('[[search:search-in-category, ' + ajaxify.data.name + ']]', function (translated) {
- const name = $('').html(translated).text();
- filterCategoryEl.find('.name').text(name);
- });
- }
- filterCategoryEl.toggleClass('hidden', !(ajaxify.data.template.category && ajaxify.data.cid));
- }
-
- function doSearch() {
- options.searchOptions = Object.assign({}, searchOptions);
- options.searchOptions.term = inputEl.val();
- updateCategoryFilterName();
-
- if (ajaxify.data.template.category && ajaxify.data.cid) {
- if (filterCategoryEl.find('input[type="checkbox"]').is(':checked')) {
- options.searchOptions.categories = [ajaxify.data.cid];
- options.searchOptions.searchChildren = true;
- }
- }
-
- quickSearchResults.removeClass('hidden').find('.quick-search-results-container').html('');
- quickSearchResults.find('.loading-indicator').removeClass('hidden');
- hooks.fire('action:search.quick.start', options);
- options.searchOptions.searchOnly = 1;
- Search.api(options.searchOptions, function (data) {
- quickSearchResults.find('.loading-indicator').addClass('hidden');
- if (!data.posts || (options.hideOnNoMatches && !data.posts.length)) {
- return quickSearchResults.addClass('hidden').find('.quick-search-results-container').html('');
- }
- data.posts.forEach(function (p) {
- const text = $('' + p.content + '
').text();
- const query = inputEl.val().toLowerCase().replace(/^in:topic-\d+/, '');
- const start = Math.max(0, text.toLowerCase().indexOf(query) - 40);
- p.snippet = utils.escapeHTML((start > 0 ? '...' : '') +
- text.slice(start, start + 80) +
- (text.length - start > 80 ? '...' : ''));
- });
- app.parseAndTranslate('partials/quick-search-results', data, function (html) {
- if (html.length) {
- html.find('.timeago').timeago();
- }
- quickSearchResults.toggleClass('hidden', !html.length || !inputEl.is(':focus'))
- .find('.quick-search-results-container')
- .html(html.length ? html : '');
- const highlightEls = quickSearchResults.find(
- '.quick-search-results .quick-search-title, .quick-search-results .snippet'
- );
- Search.highlightMatches(options.searchOptions.term, highlightEls);
- hooks.fire('action:search.quick.complete', {
- data: data,
- options: options,
- });
- });
- });
- }
-
- quickSearchResults.find('.filter-category input[type="checkbox"]').on('change', function () {
- inputEl.focus();
- doSearch();
- });
-
- inputEl.off('keyup').on('keyup', utils.debounce(function () {
- if (inputEl.val().length < 3) {
- quickSearchResults.addClass('hidden');
- oldValue = inputEl.val();
- return;
- }
- if (inputEl.val() === oldValue) {
- return;
- }
- oldValue = inputEl.val();
- if (!inputEl.is(':focus')) {
- return quickSearchResults.addClass('hidden');
- }
- doSearch();
- }, 500));
-
- let mousedownOnResults = false;
- quickSearchResults.on('mousedown', function () {
- $(window).one('mouseup', function () {
- quickSearchResults.addClass('hidden');
- });
- mousedownOnResults = true;
- });
- inputEl.on('blur', function () {
- if (!inputEl.is(':focus') && !mousedownOnResults && !quickSearchResults.hasClass('hidden')) {
- quickSearchResults.addClass('hidden');
- }
- });
-
- let ajaxified = false;
- hooks.on('action:ajaxify.end', function () {
- if (!ajaxify.isCold()) {
- ajaxified = true;
- }
- });
-
- inputEl.on('focus', function () {
- mousedownOnResults = false;
- const query = inputEl.val();
- oldValue = query;
- if (query && quickSearchResults.find('#quick-search-results').children().length) {
- updateCategoryFilterName();
- if (ajaxified) {
- doSearch();
- ajaxified = false;
- } else {
- quickSearchResults.removeClass('hidden');
- }
- inputEl[0].setSelectionRange(
- query.startsWith('in:topic') ? query.indexOf(' ') + 1 : 0,
- query.length
- );
- }
- });
-
- inputEl.off('refresh').on('refresh', function () {
- doSearch();
- });
- };
-
- Search.showAndFocusInput = function () {
- $('#search-fields').removeClass('hidden');
- $('#search-button').addClass('hidden');
- $('#search-fields input').focus();
- };
-
- Search.query = function (data, callback) {
- callback = callback || function () {};
- ajaxify.go('search?' + createQueryString(data));
- callback();
- };
-
- Search.api = function (data, callback) {
- const apiURL = config.relative_path + '/api/search?' + createQueryString(data);
- data.searchOnly = undefined;
- const searchURL = config.relative_path + '/search?' + createQueryString(data);
- $.get(apiURL, function (result) {
- result.url = searchURL;
- callback(result);
- });
- };
-
- function createQueryString(data) {
- const searchIn = data.in || 'titles';
- const postedBy = data.by || '';
- let term = data.term.replace(/^[ ?#]*/, '');
- try {
- term = encodeURIComponent(term);
- } catch (e) {
- return alerts.error('[[error:invalid-search-term]]');
- }
-
- const query = {
- term: term,
- in: searchIn,
- };
-
- if (data.matchWords) {
- query.matchWords = data.matchWords;
- }
-
- if (postedBy && postedBy.length && (searchIn === 'posts' || searchIn === 'titles' || searchIn === 'titlesposts')) {
- query.by = postedBy;
- }
-
- if (data.topicName) {
- query.topicName = data.topicName;
- }
-
- if (data.categories && data.categories.length) {
- query.categories = data.categories;
- if (data.searchChildren) {
- query.searchChildren = data.searchChildren;
- }
- }
-
- if (data.hasTags && data.hasTags.length) {
- query.hasTags = data.hasTags;
- }
-
- if (parseInt(data.replies, 10) > 0) {
- query.replies = data.replies;
- query.repliesFilter = data.repliesFilter || 'atleast';
- }
-
- if (data.timeRange) {
- query.timeRange = data.timeRange;
- query.timeFilter = data.timeFilter || 'newer';
- }
-
- if (data.sortBy) {
- query.sortBy = data.sortBy;
- query.sortDirection = data.sortDirection;
- }
-
- if (data.showAs) {
- query.showAs = data.showAs;
- }
-
- if (data.searchOnly) {
- query.searchOnly = data.searchOnly;
- }
-
- hooks.fire('action:search.createQueryString', {
- query: query,
- data: data,
- });
-
- return decodeURIComponent($.param(query));
- }
-
- Search.getSearchPreferences = function () {
- try {
- return JSON.parse(storage.getItem('search-preferences') || '{}');
- } catch (e) {
- return {};
- }
- };
-
- Search.highlightMatches = function (searchQuery, els) {
- if (!searchQuery || !els.length) {
- return;
- }
- searchQuery = utils.escapeHTML(searchQuery.replace(/^"/, '').replace(/"$/, '').trim());
- const regexStr = searchQuery.split(' ')
- .map(function (word) { return utils.escapeRegexChars(word); })
- .join('|');
- const regex = new RegExp('(' + regexStr + ')', 'gi');
-
- els.each(function () {
- const result = $(this);
- const nested = [];
-
- result.find('*').each(function () {
- $(this).after('');
- nested.push($('').append($(this)));
- });
-
- result.html(result.html().replace(regex, function (match, p1) {
- return '' + p1 + '';
- }));
-
- nested.forEach(function (nestedEl, i) {
- result.html(result.html().replace('', function () {
- return nestedEl.html();
- }));
- });
- });
-
- $('.search-result-text').find('img:not(.not-responsive)').addClass('img-responsive');
- };
-
- return Search;
+define('search', ['translator', 'storage', 'hooks', 'alerts'], (translator, storage, hooks, alerts) => {
+ const Search = {
+ current: {},
+ };
+
+ Search.init = function (searchOptions) {
+ if (!config.searchEnabled) {
+ return;
+ }
+
+ searchOptions ||= {in: config.searchDefaultInQuick || 'titles'};
+ const searchButton = $('#search-button');
+ const searchFields = $('#search-fields');
+ const searchInput = $('#search-fields input');
+ const quickSearchContainer = $('#quick-search-container');
+
+ $('#search-form .advanced-search-link').off('mousedown').on('mousedown', () => {
+ ajaxify.go('/search');
+ });
+
+ $('#search-form').off('submit').on('submit', () => {
+ searchInput.blur();
+ });
+ searchInput.off('blur').on('blur', function dismissSearch() {
+ setTimeout(() => {
+ if (!searchInput.is(':focus')) {
+ searchFields.addClass('hidden');
+ searchButton.removeClass('hidden');
+ }
+ }, 200);
+ });
+ searchInput.off('focus');
+
+ const searchElements = {
+ inputEl: searchInput,
+ resultEl: quickSearchContainer,
+ };
+
+ Search.enableQuickSearch({
+ searchOptions,
+ searchElements,
+ });
+
+ searchButton.off('click').on('click', e => {
+ if (!config.loggedIn && !app.user.privileges['search:content']) {
+ alerts.alert({
+ message: '[[error:search-requires-login]]',
+ timeout: 3000,
+ });
+ ajaxify.go('login');
+ return false;
+ }
+
+ e.stopPropagation();
+
+ Search.showAndFocusInput();
+ return false;
+ });
+
+ $('#search-form').off('submit').on('submit', function () {
+ const input = $(this).find('input');
+ const data = Search.getSearchPreferences();
+ data.term = input.val();
+ data.in = searchOptions.in;
+ hooks.fire('action:search.submit', {
+ searchOptions: data,
+ searchElements,
+ });
+ Search.query(data, () => {
+ input.val('');
+ });
+
+ return false;
+ });
+ };
+
+ Search.enableQuickSearch = function (options) {
+ if (!config.searchEnabled || !app.user.privileges['search:content']) {
+ return;
+ }
+
+ const searchOptions = Object.assign({in: config.searchDefaultInQuick || 'titles'}, options.searchOptions);
+ const quickSearchResults = options.searchElements.resultEl;
+ const inputElement = options.searchElements.inputEl;
+ let oldValue = inputElement.val();
+ const filterCategoryElement = quickSearchResults.find('.filter-category');
+
+ function updateCategoryFilterName() {
+ if (ajaxify.data.template.category && ajaxify.data.cid) {
+ translator.translate('[[search:search-in-category, ' + ajaxify.data.name + ']]', translated => {
+ const name = $('').html(translated).text();
+ filterCategoryElement.find('.name').text(name);
+ });
+ }
+
+ filterCategoryElement.toggleClass('hidden', !(ajaxify.data.template.category && ajaxify.data.cid));
+ }
+
+ function doSearch() {
+ options.searchOptions = Object.assign({}, searchOptions);
+ options.searchOptions.term = inputElement.val();
+ updateCategoryFilterName();
+
+ if (ajaxify.data.template.category && ajaxify.data.cid && filterCategoryElement.find('input[type="checkbox"]').is(':checked')) {
+ options.searchOptions.categories = [ajaxify.data.cid];
+ options.searchOptions.searchChildren = true;
+ }
+
+ quickSearchResults.removeClass('hidden').find('.quick-search-results-container').html('');
+ quickSearchResults.find('.loading-indicator').removeClass('hidden');
+ hooks.fire('action:search.quick.start', options);
+ options.searchOptions.searchOnly = 1;
+ Search.api(options.searchOptions, data => {
+ quickSearchResults.find('.loading-indicator').addClass('hidden');
+ if (!data.posts || (options.hideOnNoMatches && data.posts.length === 0)) {
+ return quickSearchResults.addClass('hidden').find('.quick-search-results-container').html('');
+ }
+
+ for (const p of data.posts) {
+ const text = $('' + p.content + '
').text();
+ const query = inputElement.val().toLowerCase().replace(/^in:topic-\d+/, '');
+ const start = Math.max(0, text.toLowerCase().indexOf(query) - 40);
+ p.snippet = utils.escapeHTML((start > 0 ? '...' : '')
+ + text.slice(start, start + 80)
+ + (text.length - start > 80 ? '...' : ''));
+ }
+
+ app.parseAndTranslate('partials/quick-search-results', data, html => {
+ if (html.length > 0) {
+ html.find('.timeago').timeago();
+ }
+
+ quickSearchResults.toggleClass('hidden', html.length === 0 || !inputElement.is(':focus'))
+ .find('.quick-search-results-container')
+ .html(html.length > 0 ? html : '');
+ const highlightEls = quickSearchResults.find(
+ '.quick-search-results .quick-search-title, .quick-search-results .snippet',
+ );
+ Search.highlightMatches(options.searchOptions.term, highlightEls);
+ hooks.fire('action:search.quick.complete', {
+ data,
+ options,
+ });
+ });
+ });
+ }
+
+ quickSearchResults.find('.filter-category input[type="checkbox"]').on('change', () => {
+ inputElement.focus();
+ doSearch();
+ });
+
+ inputElement.off('keyup').on('keyup', utils.debounce(() => {
+ if (inputElement.val().length < 3) {
+ quickSearchResults.addClass('hidden');
+ oldValue = inputElement.val();
+ return;
+ }
+
+ if (inputElement.val() === oldValue) {
+ return;
+ }
+
+ oldValue = inputElement.val();
+ if (!inputElement.is(':focus')) {
+ return quickSearchResults.addClass('hidden');
+ }
+
+ doSearch();
+ }, 500));
+
+ let mousedownOnResults = false;
+ quickSearchResults.on('mousedown', () => {
+ $(window).one('mouseup', () => {
+ quickSearchResults.addClass('hidden');
+ });
+ mousedownOnResults = true;
+ });
+ inputElement.on('blur', () => {
+ if (!inputElement.is(':focus') && !mousedownOnResults && !quickSearchResults.hasClass('hidden')) {
+ quickSearchResults.addClass('hidden');
+ }
+ });
+
+ let ajaxified = false;
+ hooks.on('action:ajaxify.end', () => {
+ if (!ajaxify.isCold()) {
+ ajaxified = true;
+ }
+ });
+
+ inputElement.on('focus', () => {
+ mousedownOnResults = false;
+ const query = inputElement.val();
+ oldValue = query;
+ if (query && quickSearchResults.find('#quick-search-results').children().length > 0) {
+ updateCategoryFilterName();
+ if (ajaxified) {
+ doSearch();
+ ajaxified = false;
+ } else {
+ quickSearchResults.removeClass('hidden');
+ }
+
+ inputElement[0].setSelectionRange(
+ query.startsWith('in:topic') ? query.indexOf(' ') + 1 : 0,
+ query.length,
+ );
+ }
+ });
+
+ inputElement.off('refresh').on('refresh', () => {
+ doSearch();
+ });
+ };
+
+ Search.showAndFocusInput = function () {
+ $('#search-fields').removeClass('hidden');
+ $('#search-button').addClass('hidden');
+ $('#search-fields input').focus();
+ };
+
+ Search.query = function (data, callback) {
+ callback ||= function () {};
+ ajaxify.go('search?' + createQueryString(data));
+ callback();
+ };
+
+ Search.api = function (data, callback) {
+ const apiURL = config.relative_path + '/api/search?' + createQueryString(data);
+ data.searchOnly = undefined;
+ const searchURL = config.relative_path + '/search?' + createQueryString(data);
+ $.get(apiURL, result => {
+ result.url = searchURL;
+ callback(result);
+ });
+ };
+
+ function createQueryString(data) {
+ const searchIn = data.in || 'titles';
+ const postedBy = data.by || '';
+ let term = data.term.replace(/^[ ?#]*/, '');
+ try {
+ term = encodeURIComponent(term);
+ } catch {
+ return alerts.error('[[error:invalid-search-term]]');
+ }
+
+ const query = {
+ term,
+ in: searchIn,
+ };
+
+ if (data.matchWords) {
+ query.matchWords = data.matchWords;
+ }
+
+ if (postedBy && postedBy.length > 0 && (searchIn === 'posts' || searchIn === 'titles' || searchIn === 'titlesposts')) {
+ query.by = postedBy;
+ }
+
+ if (data.topicName) {
+ query.topicName = data.topicName;
+ }
+
+ if (data.categories && data.categories.length > 0) {
+ query.categories = data.categories;
+ if (data.searchChildren) {
+ query.searchChildren = data.searchChildren;
+ }
+ }
+
+ if (data.hasTags && data.hasTags.length > 0) {
+ query.hasTags = data.hasTags;
+ }
+
+ if (Number.parseInt(data.replies, 10) > 0) {
+ query.replies = data.replies;
+ query.repliesFilter = data.repliesFilter || 'atleast';
+ }
+
+ if (data.timeRange) {
+ query.timeRange = data.timeRange;
+ query.timeFilter = data.timeFilter || 'newer';
+ }
+
+ if (data.sortBy) {
+ query.sortBy = data.sortBy;
+ query.sortDirection = data.sortDirection;
+ }
+
+ if (data.showAs) {
+ query.showAs = data.showAs;
+ }
+
+ if (data.searchOnly) {
+ query.searchOnly = data.searchOnly;
+ }
+
+ hooks.fire('action:search.createQueryString', {
+ query,
+ data,
+ });
+
+ return decodeURIComponent($.param(query));
+ }
+
+ Search.getSearchPreferences = function () {
+ try {
+ return JSON.parse(storage.getItem('search-preferences') || '{}');
+ } catch {
+ return {};
+ }
+ };
+
+ Search.highlightMatches = function (searchQuery, els) {
+ if (!searchQuery || els.length === 0) {
+ return;
+ }
+
+ searchQuery = utils.escapeHTML(searchQuery.replace(/^"/, '').replace(/"$/, '').trim());
+ const regexString = searchQuery.split(' ')
+ .map(word => utils.escapeRegexChars(word))
+ .join('|');
+ const regex = new RegExp('(' + regexString + ')', 'gi');
+
+ els.each(function () {
+ const result = $(this);
+ const nested = [];
+
+ result.find('*').each(function () {
+ $(this).after('');
+ nested.push($('').append($(this)));
+ });
+
+ result.html(result.html().replace(regex, (match, p1) => '' + p1 + ''));
+
+ for (const [i, nestedElement] of nested.entries()) {
+ result.html(result.html().replace('', () => nestedElement.html()));
+ }
+ });
+
+ $('.search-result-text').find('img:not(.not-responsive)').addClass('img-responsive');
+ };
+
+ return Search;
});
diff --git a/public/src/modules/settings.js b/public/src/modules/settings.js
index 166ab5c..77f2cde 100644
--- a/public/src/modules/settings.js
+++ b/public/src/modules/settings.js
@@ -1,374 +1,395 @@
'use strict';
+define('settings', ['hooks', 'alerts'], (hooks, alerts) => {
+ // eslint-disable-next-line prefer-const
+ let Settings;
+ let onReady = [];
+ let waitingJobs = 0;
-define('settings', ['hooks', 'alerts'], function (hooks, alerts) {
- // eslint-disable-next-line prefer-const
- let Settings;
- let onReady = [];
- let waitingJobs = 0;
- // eslint-disable-next-line prefer-const
- let helper;
+ let helper;
- /**
+ /**
Returns the hook of given name that matches the given type or element.
@param type The type of the element to get the matching hook for, or the element itself.
@param name The name of the hook.
*/
- function getHook(type, name) {
- if (typeof type !== 'string') {
- type = $(type);
- type = type.data('type') || type.attr('type') || type.prop('tagName');
- }
- const plugin = Settings.plugins[type.toLowerCase()];
- if (plugin == null) {
- return;
- }
- const hook = plugin[name];
- if (typeof hook === 'function') {
- return hook;
- }
- return null;
- }
-
- // eslint-disable-next-line prefer-const
- helper = {
- /**
+ function getHook(type, name) {
+ if (typeof type !== 'string') {
+ type = $(type);
+ type = type.data('type') || type.attr('type') || type.prop('tagName');
+ }
+
+ const plugin = Settings.plugins[type.toLowerCase()];
+ if (plugin == null) {
+ return;
+ }
+
+ const hook = plugin[name];
+ if (typeof hook === 'function') {
+ return hook;
+ }
+
+ return null;
+ }
+
+ // eslint-disable-next-line prefer-const
+ helper = {
+ /**
@returns Object A deep clone of the given object.
*/
- deepClone: function (obj) {
- if (typeof obj === 'object') {
- return JSON.parse(JSON.stringify(obj));
- }
- return obj;
- },
- /**
+ deepClone(object) {
+ if (typeof object === 'object') {
+ return JSON.parse(JSON.stringify(object));
+ }
+
+ return object;
+ },
+ /**
Creates a new Element with given data.
@param tagName The tag-name of the element to create.
@param data The attributes to set.
@param text The text to add into the element.
@returns HTMLElement The created element.
*/
- createElement: function (tagName, data, text) {
- const element = document.createElement(tagName);
- for (const k in data) {
- if (data.hasOwnProperty(k)) {
- element.setAttribute(k, data[k]);
- }
- }
- if (text) {
- element.appendChild(document.createTextNode(text));
- }
- return element;
- },
- /**
+ createElement(tagName, data, text) {
+ const element = document.createElement(tagName);
+ for (const k in data) {
+ if (data.hasOwnProperty(k)) {
+ element.setAttribute(k, data[k]);
+ }
+ }
+
+ if (text) {
+ element.append(document.createTextNode(text));
+ }
+
+ return element;
+ },
+ /**
Calls the init-hook of the given element.
@param element The element to initialize.
*/
- initElement: function (element) {
- const hook = getHook(element, 'init');
- if (hook != null) {
- hook.call(Settings, $(element));
- }
- },
- /**
+ initElement(element) {
+ const hook = getHook(element, 'init');
+ if (hook != null) {
+ hook.call(Settings, $(element));
+ }
+ },
+ /**
Calls the destruct-hook of the given element.
@param element The element to destruct.
*/
- destructElement: function (element) {
- const hook = getHook(element, 'destruct');
- if (hook != null) {
- hook.call(Settings, $(element));
- }
- },
- /**
+ destructElement(element) {
+ const hook = getHook(element, 'destruct');
+ if (hook != null) {
+ hook.call(Settings, $(element));
+ }
+ },
+ /**
Creates and initializes a new element.
@param type The type of the new element.
@param tagName The tag-name of the new element.
@param data The data to forward to create-hook or use as attributes.
@returns JQuery The created element.
*/
- createElementOfType: function (type, tagName, data) {
- let element;
- const hook = getHook(type, 'create');
- if (hook != null) {
- element = $(hook.call(Settings, type, tagName, data));
- } else {
- if (data == null) {
- data = {};
- }
- if (type != null) {
- data.type = type;
- }
- element = $(helper.createElement(tagName || 'input', data));
- }
- element.data('type', type);
- helper.initElement(element);
- return element;
- },
- /**
+ createElementOfType(type, tagName, data) {
+ let element;
+ const hook = getHook(type, 'create');
+ if (hook == null) {
+ data ??= {};
+
+ if (type != null) {
+ data.type = type;
+ }
+
+ element = $(helper.createElement(tagName || 'input', data));
+ } else {
+ element = $(hook.call(Settings, type, tagName, data));
+ }
+
+ element.data('type', type);
+ helper.initElement(element);
+ return element;
+ },
+ /**
Creates a new Array that contains values of given Array depending on trim and empty.
@param array The array to clean.
@param trim Whether to trim each value if it has a trim-function.
@param empty Whether empty values should get added.
@returns Array The filtered and/or modified Array.
*/
- cleanArray: function (array, trim, empty) {
- const cleaned = [];
- if (!trim && empty) {
- return array;
- }
- for (let i = 0; i < array.length; i += 1) {
- let value = array[i];
- if (trim) {
- if (value === !!value) {
- value = +value;
- } else if (value && typeof value.trim === 'function') {
- value = value.trim();
- }
- }
- if (empty || (value != null && value.length)) {
- cleaned.push(value);
- }
- }
- return cleaned;
- },
- isTrue: function (value) {
- return value === 'true' || +value === 1;
- },
- isFalse: function (value) {
- return value === 'false' || +value === 0;
- },
- /**
+ cleanArray(array, trim, empty) {
+ const cleaned = [];
+ if (!trim && empty) {
+ return array;
+ }
+
+ for (let value of array) {
+ if (trim) {
+ if (value === Boolean(value)) {
+ value = Number(value);
+ } else if (value && typeof value.trim === 'function') {
+ value = value.trim();
+ }
+ }
+
+ if (empty || (value != null && value.length > 0)) {
+ cleaned.push(value);
+ }
+ }
+
+ return cleaned;
+ },
+ isTrue(value) {
+ return value === 'true' || Number(value) === 1;
+ },
+ isFalse(value) {
+ return value === 'false' || Number(value) === 0;
+ },
+ /**
Calls the get-hook of the given element and returns its result.
If no hook is specified it gets treated as input-field.
@param element The element of that the value should get read.
@returns Object The value of the element.
*/
- readValue: function (element) {
- let empty = !helper.isFalse(element.data('empty'));
- const trim = !helper.isFalse(element.data('trim'));
- const split = element.data('split');
- const hook = getHook(element, 'get');
- let value;
- if (hook != null) {
- return hook.call(Settings, element, trim, empty);
- }
- if (split != null) {
- empty = helper.isTrue(element.data('empty')); // default empty-value is false for arrays
- value = element.val();
- const array = (value != null && value.split(split || ',')) || [];
- return helper.cleanArray(array, trim, empty);
- }
- value = element.val();
- if (trim && value != null && typeof value.trim === 'function') {
- value = value.trim();
- }
- if (empty || (value !== undefined && (value == null || value.length !== 0))) {
- return value;
- }
- },
- /**
+ readValue(element) {
+ let empty = !helper.isFalse(element.data('empty'));
+ const trim = !helper.isFalse(element.data('trim'));
+ const split = element.data('split');
+ const hook = getHook(element, 'get');
+ let value;
+ if (hook != null) {
+ return hook.call(Settings, element, trim, empty);
+ }
+
+ if (split != null) {
+ empty = helper.isTrue(element.data('empty')); // Default empty-value is false for arrays
+ value = element.val();
+ const array = (value != null && value.split(split || ',')) || [];
+ return helper.cleanArray(array, trim, empty);
+ }
+
+ value = element.val();
+ if (trim && value != null && typeof value.trim === 'function') {
+ value = value.trim();
+ }
+
+ if (empty || (value !== undefined && (value == null || value.length > 0))) {
+ return value;
+ }
+ },
+ /**
Calls the set-hook of the given element.
If no hook is specified it gets treated as input-field.
@param element The JQuery-Object of the element to fill.
@param value The value to set.
*/
- fillField: function (element, value) {
- const hook = getHook(element, 'set');
- let trim = element.data('trim');
- trim = trim !== 'false' && +trim !== 0;
- if (hook != null) {
- return hook.call(Settings, element, value, trim);
- }
- if (value instanceof Array) {
- value = value.join(element.data('split') || (trim ? ', ' : ','));
- }
- if (trim && value && typeof value.trim === 'function') {
- value = value.trim();
- if (typeof value.toString === 'function') {
- value = value.toString();
- }
- } else if (value != null) {
- if (typeof value.toString === 'function') {
- value = value.toString();
- }
- if (trim) {
- value = value.trim();
- }
- } else {
- value = '';
- }
- if (value !== undefined) {
- element.val(value);
- }
- },
- /**
+ fillField(element, value) {
+ const hook = getHook(element, 'set');
+ let trim = element.data('trim');
+ trim = trim !== 'false' && Number(trim) !== 0;
+ if (hook != null) {
+ return hook.call(Settings, element, value, trim);
+ }
+
+ if (Array.isArray(value)) {
+ value = value.join(element.data('split') || (trim ? ', ' : ','));
+ }
+
+ if (trim && value && typeof value.trim === 'function') {
+ value = value.trim();
+ if (typeof value.toString === 'function') {
+ value = value.toString();
+ }
+ } else if (value == null) {
+ value = '';
+ } else {
+ if (typeof value.toString === 'function') {
+ value = value.toString();
+ }
+
+ if (trim) {
+ value = value.trim();
+ }
+ }
+
+ if (value !== undefined) {
+ element.val(value);
+ }
+ },
+ /**
Calls the init-hook and {@link helper.fillField} on each field within wrapper-object.
@param wrapper The wrapper-element to set settings within.
*/
- initFields: function (wrapper) {
- $('[data-key]', wrapper).each(function (ignored, field) {
- field = $(field);
- const hook = getHook(field, 'init');
- const keyParts = field.data('key').split('.');
- let value = Settings.get();
- if (hook != null) {
- hook.call(Settings, field);
- }
- for (let i = 0; i < keyParts.length; i += 1) {
- const part = keyParts[i];
- if (part && value != null) {
- value = value[part];
- }
- }
- helper.fillField(field, value);
- });
- },
- /**
+ initFields(wrapper) {
+ $('[data-key]', wrapper).each((ignored, field) => {
+ field = $(field);
+ const hook = getHook(field, 'init');
+ const keyParts = field.data('key').split('.');
+ let value = Settings.get();
+ if (hook != null) {
+ hook.call(Settings, field);
+ }
+
+ for (const part of keyParts) {
+ if (part && value != null) {
+ value = value[part];
+ }
+ }
+
+ helper.fillField(field, value);
+ });
+ },
+ /**
Increases the amount of jobs before settings are ready by given amount.
@param amount The amount of jobs to register.
*/
- registerReadyJobs: function (amount) {
- waitingJobs += amount;
- return waitingJobs;
- },
- /**
+ registerReadyJobs(amount) {
+ waitingJobs += amount;
+ return waitingJobs;
+ },
+ /**
Decreases the amount of jobs before settings are ready by given amount or 1.
If the amount is less or equal 0 all callbacks registered by {@link helper.whenReady} get called.
@param amount The amount of jobs that finished.
*/
- beforeReadyJobsDecreased: function (amount) {
- if (amount == null) {
- amount = 1;
- }
- if (waitingJobs > 0) {
- waitingJobs -= amount;
- if (waitingJobs <= 0) {
- for (let i = 0; i < onReady.length; i += 1) {
- onReady[i]();
- }
- onReady = [];
- }
- }
- },
- /**
+ beforeReadyJobsDecreased(amount) {
+ amount ??= 1;
+
+ if (waitingJobs > 0) {
+ waitingJobs -= amount;
+ if (waitingJobs <= 0) {
+ for (const element of onReady) {
+ element();
+ }
+
+ onReady = [];
+ }
+ }
+ },
+ /**
Calls the given callback when the settings are ready.
@param callback The callback.
*/
- whenReady: function (callback) {
- if (waitingJobs <= 0) {
- callback();
- } else {
- onReady.push(callback);
- }
- },
- serializeForm: function (formEl) {
- const values = formEl.serializeObject();
-
- // "Fix" checkbox values, so that unchecked options are not omitted
- formEl.find('input[type="checkbox"]').each(function (idx, inputEl) {
- inputEl = $(inputEl);
- if (!inputEl.is(':checked')) {
- values[inputEl.attr('name')] = 'off';
- }
- });
-
- // save multiple selects as json arrays
- formEl.find('select[multiple]').each(function (idx, selectEl) {
- selectEl = $(selectEl);
- values[selectEl.attr('name')] = JSON.stringify(selectEl.val());
- });
-
- return values;
- },
- /**
+ whenReady(callback) {
+ if (waitingJobs <= 0) {
+ callback();
+ } else {
+ onReady.push(callback);
+ }
+ },
+ serializeForm(formElement) {
+ const values = formElement.serializeObject();
+
+ // "Fix" checkbox values, so that unchecked options are not omitted
+ formElement.find('input[type="checkbox"]').each((index, inputElement) => {
+ inputElement = $(inputElement);
+ if (!inputElement.is(':checked')) {
+ values[inputElement.attr('name')] = 'off';
+ }
+ });
+
+ // Save multiple selects as json arrays
+ formElement.find('select[multiple]').each((index, selectElement) => {
+ selectElement = $(selectElement);
+ values[selectElement.attr('name')] = JSON.stringify(selectElement.val());
+ });
+
+ return values;
+ },
+ /**
Persists the given settings with given hash.
@param hash The hash to use as settings-id.
@param settings The settings-object to persist.
@param notify Whether to send notification when settings got saved.
@param callback The callback to call when done.
*/
- persistSettings: function (hash, settings, notify, callback) {
- if (settings != null && settings._ != null && typeof settings._ !== 'string') {
- settings = helper.deepClone(settings);
- settings._ = JSON.stringify(settings._);
- }
- socket.emit('admin.settings.set', {
- hash: hash,
- values: settings,
- }, function (err) {
- if (notify) {
- if (err) {
- alerts.alert({
- title: '[[admin/admin:changes-not-saved]]',
- type: 'danger',
- message: `[[admin/admin/changes-not-saved-message, ${err.message}]]`,
- timeout: 5000,
- });
- } else {
- alerts.alert({
- title: '[[admin/admin:changes-saved]]',
- type: 'success',
- message: '[[admin/admin:changes-saved-message]]',
- timeout: 2500,
- });
- }
- }
- if (typeof callback === 'function') {
- callback(err);
- }
- });
- },
- /**
+ persistSettings(hash, settings, notify, callback) {
+ if (settings != null && settings._ != null && typeof settings._ !== 'string') {
+ settings = helper.deepClone(settings);
+ settings._ = JSON.stringify(settings._);
+ }
+
+ socket.emit('admin.settings.set', {
+ hash,
+ values: settings,
+ }, error => {
+ if (notify) {
+ if (error) {
+ alerts.alert({
+ title: '[[admin/admin:changes-not-saved]]',
+ type: 'danger',
+ message: `[[admin/admin/changes-not-saved-message, ${error.message}]]`,
+ timeout: 5000,
+ });
+ } else {
+ alerts.alert({
+ title: '[[admin/admin:changes-saved]]',
+ type: 'success',
+ message: '[[admin/admin:changes-saved-message]]',
+ timeout: 2500,
+ });
+ }
+ }
+
+ if (typeof callback === 'function') {
+ callback(error);
+ }
+ });
+ },
+ /**
Sets the settings to use to given settings.
@param settings The settings to use.
*/
- use: function (settings) {
- try {
- settings._ = JSON.parse(settings._);
- } catch (_error) {}
- Settings.cfg = settings;
- },
- };
-
- // eslint-disable-next-line prefer-const
- Settings = {
- helper: helper,
- plugins: {},
- cfg: {},
-
- /**
+ use(settings) {
+ try {
+ settings._ = JSON.parse(settings._);
+ } catch {}
+
+ Settings.cfg = settings;
+ },
+ };
+
+ Settings = {
+ helper,
+ plugins: {},
+ cfg: {},
+
+ /**
Returns the saved settings.
@returns Object The settings.
*/
- get: function () {
- if (Settings.cfg != null && Settings.cfg._ !== undefined) {
- return Settings.cfg._;
- }
- return Settings.cfg;
- },
- /**
+ get() {
+ if (Settings.cfg != null && Settings.cfg._ !== undefined) {
+ return Settings.cfg._;
+ }
+
+ return Settings.cfg;
+ },
+ /**
Registers a new plugin and calls its use-hook.
@param service The plugin to register.
@param types The types to bind the plugin to.
*/
- registerPlugin: function (service, types) {
- if (types == null) {
- types = service.types;
- } else {
- service.types = types;
- }
- if (typeof service.use === 'function') {
- service.use.call(Settings);
- }
- for (let i = 0; i < types.length; i += 1) {
- const type = types[i].toLowerCase();
- if (Settings.plugins[type] == null) {
- Settings.plugins[type] = service;
- }
- }
- },
- /**
+ registerPlugin(service, types) {
+ if (types == null) {
+ types = service.types;
+ } else {
+ service.types = types;
+ }
+
+ if (typeof service.use === 'function') {
+ service.use.call(Settings);
+ }
+
+ for (const type_ of types) {
+ const type = type_.toLowerCase();
+ if (Settings.plugins[type] == null) {
+ Settings.plugins[type] = service;
+ }
+ }
+ },
+ /**
Sets the settings to given ones, resets the fields within given wrapper and saves the settings server-side.
@param hash The hash to use as settings-id.
@param settings The settings to set.
@@ -376,234 +397,234 @@ define('settings', ['hooks', 'alerts'], function (hooks, alerts) {
@param callback The callback to call when done.
@param notify Whether to send notification when settings got saved.
*/
- set: function (hash, settings, wrapper, callback, notify) {
- if (notify == null) {
- notify = true;
- }
- helper.whenReady(function () {
- helper.use(settings);
- helper.initFields(wrapper || 'form');
- helper.persistSettings(hash, settings, notify, callback);
- });
- },
- /**
+ set(hash, settings, wrapper, callback, notify) {
+ notify ??= true;
+
+ helper.whenReady(() => {
+ helper.use(settings);
+ helper.initFields(wrapper || 'form');
+ helper.persistSettings(hash, settings, notify, callback);
+ });
+ },
+ /**
Fetches the settings from server and calls {@link Settings.helper.initFields} once the settings are ready.
@param hash The hash to use as settings-id.
@param wrapper The wrapper-element to set settings within.
@param callback The callback to call when done.
*/
- sync: function (hash, wrapper, callback) {
- socket.emit('admin.settings.get', {
- hash: hash,
- }, function (err, values) {
- if (err) {
- if (typeof callback === 'function') {
- callback(err);
- }
- } else {
- helper.whenReady(function () {
- helper.use(values);
- helper.initFields(wrapper || 'form');
- if (typeof callback === 'function') {
- callback();
- }
- });
- }
- });
- },
- /**
+ sync(hash, wrapper, callback) {
+ socket.emit('admin.settings.get', {
+ hash,
+ }, (error, values) => {
+ if (error) {
+ if (typeof callback === 'function') {
+ callback(error);
+ }
+ } else {
+ helper.whenReady(() => {
+ helper.use(values);
+ helper.initFields(wrapper || 'form');
+ if (typeof callback === 'function') {
+ callback();
+ }
+ });
+ }
+ });
+ },
+ /**
Reads the settings from fields and saves them server-side.
@param hash The hash to use as settings-id.
@param wrapper The wrapper-element to find settings within.
@param callback The callback to call when done.
@param notify Whether to send notification when settings got saved.
*/
- persist: function (hash, wrapper, callback, notify) {
- const notSaved = [];
- const fields = $('[data-key]', wrapper || 'form').toArray();
- if (notify == null) {
- notify = true;
- }
- for (let i = 0; i < fields.length; i += 1) {
- const field = $(fields[i]);
- const value = helper.readValue(field);
- let parentCfg = Settings.get();
- const keyParts = field.data('key').split('.');
- const lastKey = keyParts[keyParts.length - 1];
- if (keyParts.length > 1) {
- for (let j = 0; j < keyParts.length - 1; j += 1) {
- const part = keyParts[j];
- if (part && parentCfg != null) {
- parentCfg = parentCfg[part];
- }
- }
- }
- if (parentCfg != null) {
- if (value != null) {
- parentCfg[lastKey] = value;
- } else {
- delete parentCfg[lastKey];
- }
- } else {
- notSaved.push(field.data('key'));
- }
- }
- if (notSaved.length) {
- alerts.alert({
- title: 'Attributes Not Saved',
- message: "'" + (notSaved.join(', ')) + "' could not be saved. Please contact the plugin-author!",
- type: 'danger',
- timeout: 5000,
- });
- }
- helper.persistSettings(hash, Settings.cfg, notify, callback);
- },
- load: function (hash, formEl, callback) {
- callback = callback || function () {};
- const call = formEl.attr('data-socket-get');
-
- socket.emit(call || 'admin.settings.get', {
- hash: hash,
- }, function (err, values) {
- if (err) {
- return callback(err);
- }
- // multipe selects are saved as json arrays, parse them here
- $(formEl).find('select[multiple]').each(function (idx, selectEl) {
- const key = $(selectEl).attr('name');
- if (key && values.hasOwnProperty(key)) {
- try {
- values[key] = JSON.parse(values[key]);
- } catch (e) {
- // Leave the value as is
- }
- }
- });
-
- // Save loaded settings into ajaxify.data for use client-side
- ajaxify.data[call ? hash : 'settings'] = values;
-
- helper.whenReady(function () {
- $(formEl).find('[data-sorted-list]').each(function (idx, el) {
- getHook(el, 'get').call(Settings, $(el), hash);
- });
- });
-
- $(formEl).deserialize(values);
- $(formEl).find('input[type="checkbox"]').each(function () {
- $(this).parents('.mdl-switch').toggleClass('is-checked', $(this).is(':checked'));
- });
- hooks.fire('action:admin.settingsLoaded');
-
- // Handle unsaved changes
- $(formEl).on('change', 'input, select, textarea', function () {
- app.flags = app.flags || {};
- app.flags._unsaved = true;
- });
-
- const saveEl = document.getElementById('save');
- if (saveEl) {
- require(['mousetrap'], function (mousetrap) {
- mousetrap.bind('ctrl+s', function (ev) {
- saveEl.click();
- ev.preventDefault();
- });
- });
- }
-
- callback(null, values);
- });
- },
- save: function (hash, formEl, callback) {
- formEl = $(formEl);
-
- const controls = formEl.get(0).elements;
- const ok = Settings.check(controls);
- if (!ok) {
- return;
- }
-
- if (formEl.length) {
- const values = helper.serializeForm(formEl);
-
- helper.whenReady(function () {
- const list = formEl.find('[data-sorted-list]');
- if (list.length) {
- list.each((idx, item) => {
- getHook(item, 'set').call(Settings, $(item), values);
- });
- }
- });
-
- const call = formEl.attr('data-socket-set');
- socket.emit(call || 'admin.settings.set', {
- hash: hash,
- values: values,
- }, function (err) {
- // Remove unsaved flag to re-enable ajaxify
- app.flags._unsaved = false;
-
- // Also save to local ajaxify.data
- ajaxify.data[call ? hash : 'settings'] = values;
-
- if (typeof callback === 'function') {
- callback(err);
- } else if (err) {
- alerts.alert({
- title: '[[admin/admin:changes-not-saved]]',
- message: `[[admin/admin:changes-not-saved-message, ${err.message}]]`,
- type: 'error',
- timeout: 2500,
- });
- } else {
- alerts.alert({
- title: '[[admin/admin:changes-saved]]',
- type: 'success',
- timeout: 2500,
- });
- }
- });
- }
- },
- check: function (controls) {
- const onTrigger = (e) => {
- const wrapper = e.target.closest('.form-group');
- if (wrapper) {
- wrapper.classList.add('has-error');
- }
-
- e.target.removeEventListener('invalid', onTrigger);
- };
-
- return Array.prototype.map.call(controls, (controlEl) => {
- const wrapper = controlEl.closest('.form-group');
- if (wrapper) {
- wrapper.classList.remove('has-error');
- }
-
- controlEl.addEventListener('invalid', onTrigger);
- return controlEl.reportValidity();
- }).every(Boolean);
- },
- };
-
-
- helper.registerReadyJobs(1);
- require([
- 'settings/checkbox',
- 'settings/number',
- 'settings/textarea',
- 'settings/select',
- 'settings/array',
- 'settings/key',
- 'settings/object',
- 'settings/sorted-list',
- ], function () {
- for (let i = 0; i < arguments.length; i += 1) {
- Settings.registerPlugin(arguments[i]);
- }
- helper.beforeReadyJobsDecreased();
- });
-
- return Settings;
+ persist(hash, wrapper, callback, notify) {
+ const notSaved = [];
+ const fields = $('[data-key]', wrapper || 'form').toArray();
+ notify ??= true;
+
+ for (const field_ of fields) {
+ const field = $(field_);
+ const value = helper.readValue(field);
+ let parentCfg = Settings.get();
+ const keyParts = field.data('key').split('.');
+ const lastKey = keyParts.at(-1);
+ if (keyParts.length > 1) {
+ for (let index = 0; index < keyParts.length - 1; index += 1) {
+ const part = keyParts[index];
+ if (part && parentCfg != null) {
+ parentCfg = parentCfg[part];
+ }
+ }
+ }
+
+ if (parentCfg == null) {
+ notSaved.push(field.data('key'));
+ } else if (value == null) {
+ delete parentCfg[lastKey];
+ } else {
+ parentCfg[lastKey] = value;
+ }
+ }
+
+ if (notSaved.length > 0) {
+ alerts.alert({
+ title: 'Attributes Not Saved',
+ message: '\'' + (notSaved.join(', ')) + '\' could not be saved. Please contact the plugin-author!',
+ type: 'danger',
+ timeout: 5000,
+ });
+ }
+
+ helper.persistSettings(hash, Settings.cfg, notify, callback);
+ },
+ load(hash, formElement, callback) {
+ callback ||= function () {};
+ const call = formElement.attr('data-socket-get');
+
+ socket.emit(call || 'admin.settings.get', {
+ hash,
+ }, (error, values) => {
+ if (error) {
+ return callback(error);
+ }
+
+ // Multipe selects are saved as json arrays, parse them here
+ $(formElement).find('select[multiple]').each((index, selectElement) => {
+ const key = $(selectElement).attr('name');
+ if (key && values.hasOwnProperty(key)) {
+ try {
+ values[key] = JSON.parse(values[key]);
+ } catch {
+ // Leave the value as is
+ }
+ }
+ });
+
+ // Save loaded settings into ajaxify.data for use client-side
+ ajaxify.data[call ? hash : 'settings'] = values;
+
+ helper.whenReady(() => {
+ $(formElement).find('[data-sorted-list]').each((index, element) => {
+ getHook(element, 'get').call(Settings, $(element), hash);
+ });
+ });
+
+ $(formElement).deserialize(values);
+ $(formElement).find('input[type="checkbox"]').each(function () {
+ $(this).parents('.mdl-switch').toggleClass('is-checked', $(this).is(':checked'));
+ });
+ hooks.fire('action:admin.settingsLoaded');
+
+ // Handle unsaved changes
+ $(formElement).on('change', 'input, select, textarea', () => {
+ app.flags = app.flags || {};
+ app.flags._unsaved = true;
+ });
+
+ const saveElement = document.querySelector('#save');
+ if (saveElement) {
+ require(['mousetrap'], mousetrap => {
+ mousetrap.bind('ctrl+s', event => {
+ saveElement.click();
+ event.preventDefault();
+ });
+ });
+ }
+
+ callback(null, values);
+ });
+ },
+ save(hash, formElement, callback) {
+ formElement = $(formElement);
+
+ const controls = formElement.get(0).elements;
+ const ok = Settings.check(controls);
+ if (!ok) {
+ return;
+ }
+
+ if (formElement.length > 0) {
+ const values = helper.serializeForm(formElement);
+
+ helper.whenReady(() => {
+ const list = formElement.find('[data-sorted-list]');
+ if (list.length > 0) {
+ list.each((index, item) => {
+ getHook(item, 'set').call(Settings, $(item), values);
+ });
+ }
+ });
+
+ const call = formElement.attr('data-socket-set');
+ socket.emit(call || 'admin.settings.set', {
+ hash,
+ values,
+ }, error => {
+ // Remove unsaved flag to re-enable ajaxify
+ app.flags._unsaved = false;
+
+ // Also save to local ajaxify.data
+ ajaxify.data[call ? hash : 'settings'] = values;
+
+ if (typeof callback === 'function') {
+ callback(error);
+ } else if (error) {
+ alerts.alert({
+ title: '[[admin/admin:changes-not-saved]]',
+ message: `[[admin/admin:changes-not-saved-message, ${error.message}]]`,
+ type: 'error',
+ timeout: 2500,
+ });
+ } else {
+ alerts.alert({
+ title: '[[admin/admin:changes-saved]]',
+ type: 'success',
+ timeout: 2500,
+ });
+ }
+ });
+ }
+ },
+ check(controls) {
+ const onTrigger = e => {
+ const wrapper = e.target.closest('.form-group');
+ if (wrapper) {
+ wrapper.classList.add('has-error');
+ }
+
+ e.target.removeEventListener('invalid', onTrigger);
+ };
+
+ return Array.prototype.map.call(controls, controlElement => {
+ const wrapper = controlElement.closest('.form-group');
+ if (wrapper) {
+ wrapper.classList.remove('has-error');
+ }
+
+ controlElement.addEventListener('invalid', onTrigger);
+ return controlElement.reportValidity();
+ }).every(Boolean);
+ },
+ };
+
+ helper.registerReadyJobs(1);
+ require([
+ 'settings/checkbox',
+ 'settings/number',
+ 'settings/textarea',
+ 'settings/select',
+ 'settings/array',
+ 'settings/key',
+ 'settings/object',
+ 'settings/sorted-list',
+ ], function () {
+ for (const argument of arguments) {
+ Settings.registerPlugin(argument);
+ }
+
+ helper.beforeReadyJobsDecreased();
+ });
+
+ return Settings;
});
diff --git a/public/src/modules/settings/array.js b/public/src/modules/settings/array.js
index 64fa0ce..6b6f2e5 100644
--- a/public/src/modules/settings/array.js
+++ b/public/src/modules/settings/array.js
@@ -1,34 +1,34 @@
'use strict';
-define('settings/array', function () {
- let helper = null;
+define('settings/array', () => {
+ let helper = null;
- /**
+ /**
Creates a new button that removes itself and the given elements on click.
Calls {@link Settings.helper.destructElement} for each given field.
@param elements The elements to remove on click.
@returns JQuery The created remove-button.
*/
- function createRemoveButton(elements) {
- const rm = $(helper.createElement('button', {
- class: 'btn btn-xs btn-primary remove',
- title: 'Remove Item',
- }, '-'));
- rm.click(function (event) {
- event.preventDefault();
- elements.remove();
- rm.remove();
- elements.each(function (i, element) {
- element = $(element);
- if (element.is('[data-key]')) {
- helper.destructElement(element);
- }
- });
- });
- return rm;
- }
+ function createRemoveButton(elements) {
+ const rm = $(helper.createElement('button', {
+ class: 'btn btn-xs btn-primary remove',
+ title: 'Remove Item',
+ }, '-'));
+ rm.click(event => {
+ event.preventDefault();
+ elements.remove();
+ rm.remove();
+ elements.each((i, element) => {
+ element = $(element);
+ if (element.is('[data-key]')) {
+ helper.destructElement(element);
+ }
+ });
+ });
+ return rm;
+ }
- /**
+ /**
Creates a new child-element of given field with given data and calls given callback with elements to add.
@param field Any wrapper that contains all fields of the array.
@param key The key of the array.
@@ -38,108 +38,112 @@ define('settings/array', function () {
@param separator The separator to use.
@param insertCb The callback to insert the elements.
*/
- function addArrayChildElement(field, key, attributes, value, separator, insertCb) {
- attributes = helper.deepClone(attributes);
- const type = attributes['data-type'] || attributes.type || 'text';
- const element = $(helper.createElementOfType(type, attributes.tagName, attributes));
- element.attr('data-parent', '_' + key);
- delete attributes['data-type'];
- delete attributes.tagName;
- for (const name in attributes) {
- if (attributes.hasOwnProperty(name)) {
- const val = attributes[name];
- if (name.search('data-') === 0) {
- element.data(name.substring(5), val);
- } else if (name.search('prop-') === 0) {
- element.prop(name.substring(5), val);
- } else {
- element.attr(name, val);
- }
- }
- }
- helper.fillField(element, value);
- if ($('[data-parent="_' + key + '"]', field).length) {
- insertCb(separator);
- }
- insertCb(element);
- insertCb(createRemoveButton(element.add(separator)));
- }
+ function addArrayChildElement(field, key, attributes, value, separator, insertCallback) {
+ attributes = helper.deepClone(attributes);
+ const type = attributes['data-type'] || attributes.type || 'text';
+ const element = $(helper.createElementOfType(type, attributes.tagName, attributes));
+ element.attr('data-parent', '_' + key);
+ delete attributes['data-type'];
+ delete attributes.tagName;
+ for (const name in attributes) {
+ if (attributes.hasOwnProperty(name)) {
+ const value_ = attributes[name];
+ if (name.search('data-') === 0) {
+ element.data(name.slice(5), value_);
+ } else if (name.search('prop-') === 0) {
+ element.prop(name.slice(5), value_);
+ } else {
+ element.attr(name, value_);
+ }
+ }
+ }
- /**
+ helper.fillField(element, value);
+ if ($('[data-parent="_' + key + '"]', field).length > 0) {
+ insertCallback(separator);
+ }
+
+ insertCallback(element);
+ insertCallback(createRemoveButton(element.add(separator)));
+ }
+
+ /**
Adds a new button that adds a new child-element to given element on click.
@param element The element to insert the button.
@param key The key to forward to {@link addArrayChildElement}.
@param attributes The attributes to forward to {@link addArrayChildElement}.
@param separator The separator to forward to {@link addArrayChildElement}.
*/
- function addAddButton(element, key, attributes, separator) {
- const addSpace = $(document.createTextNode(' '));
- const newValue = element.data('new') || '';
- const add = $(helper.createElement('button', {
- class: 'btn btn-sm btn-primary add',
- title: 'Expand Array',
- }, '+'));
- add.click(function (event) {
- event.preventDefault();
- addArrayChildElement(element, key, attributes, newValue, separator.clone(), function (el) {
- addSpace.before(el);
- });
- });
- element.append(addSpace);
- element.append(add);
- }
+ function addAddButton(element, key, attributes, separator) {
+ const addSpace = $(document.createTextNode(' '));
+ const newValue = element.data('new') || '';
+ const add = $(helper.createElement('button', {
+ class: 'btn btn-sm btn-primary add',
+ title: 'Expand Array',
+ }, '+'));
+ add.click(event => {
+ event.preventDefault();
+ addArrayChildElement(element, key, attributes, newValue, separator.clone(), element_ => {
+ addSpace.before(element_);
+ });
+ });
+ element.append(addSpace);
+ element.append(add);
+ }
+
+ const SettingsArray = {
+ types: ['array', 'div'],
+ use() {
+ helper = this.helper;
+ },
+ create(ignored, tagName) {
+ return helper.createElement(tagName || 'div');
+ },
+ set(element, value) {
+ let attributes = element.data('attributes');
+ const key = element.data('key') || element.data('parent');
+ let separator = element.data('split') || ', ';
+ separator = (function () {
+ try {
+ return $(separator);
+ } catch {
+ return $(document.createTextNode(separator));
+ }
+ })();
+ if (typeof attributes !== 'object') {
+ attributes = {};
+ }
+
+ element.empty();
+ if (!(Array.isArray(value))) {
+ value = [];
+ }
+ for (const element_ of value) {
+ addArrayChildElement(element, key, attributes, element_, separator.clone(), element__ => {
+ element.append(element__);
+ });
+ }
- const SettingsArray = {
- types: ['array', 'div'],
- use: function () {
- helper = this.helper;
- },
- create: function (ignored, tagName) {
- return helper.createElement(tagName || 'div');
- },
- set: function (element, value) {
- let attributes = element.data('attributes');
- const key = element.data('key') || element.data('parent');
- let separator = element.data('split') || ', ';
- separator = (function () {
- try {
- return $(separator);
- } catch (_error) {
- return $(document.createTextNode(separator));
- }
- }());
- if (typeof attributes !== 'object') {
- attributes = {};
- }
- element.empty();
- if (!(value instanceof Array)) {
- value = [];
- }
- for (let i = 0; i < value.length; i += 1) {
- addArrayChildElement(element, key, attributes, value[i], separator.clone(), function (el) {
- element.append(el);
- });
- }
- addAddButton(element, key, attributes, separator);
- },
- get: function (element, trim, empty) {
- const key = element.data('key') || element.data('parent');
- const children = $('[data-parent="_' + key + '"]', element);
- const values = [];
- children.each(function (i, child) {
- child = $(child);
- const val = helper.readValue(child);
- const empty = helper.isTrue(child.data('empty'));
- if (empty || (val !== undefined && (val == null || val.length !== 0))) {
- return values.push(val);
- }
- });
- if (empty || values.length) {
- return values;
- }
- },
- };
+ addAddButton(element, key, attributes, separator);
+ },
+ get(element, trim, empty) {
+ const key = element.data('key') || element.data('parent');
+ const children = $('[data-parent="_' + key + '"]', element);
+ const values = [];
+ children.each((i, child) => {
+ child = $(child);
+ const value = helper.readValue(child);
+ const empty = helper.isTrue(child.data('empty'));
+ if (empty || (value !== undefined && (value == null || value.length > 0))) {
+ return values.push(value);
+ }
+ });
+ if (empty || values.length > 0) {
+ return values;
+ }
+ },
+ };
- return SettingsArray;
+ return SettingsArray;
});
diff --git a/public/src/modules/settings/checkbox.js b/public/src/modules/settings/checkbox.js
index c21b3de..1e9ea2d 100644
--- a/public/src/modules/settings/checkbox.js
+++ b/public/src/modules/settings/checkbox.js
@@ -1,39 +1,43 @@
'use strict';
-define('settings/checkbox', function () {
- let Settings = null;
+define('settings/checkbox', () => {
+ let Settings = null;
- const SettingsCheckbox = {
- types: ['checkbox'],
- use: function () {
- Settings = this;
- },
- create: function () {
- return Settings.helper.createElement('input', {
- type: 'checkbox',
- });
- },
- set: function (element, value) {
- element.prop('checked', value);
- element.closest('.mdl-switch').toggleClass('is-checked', element.is(':checked'));
- },
- get: function (element, trim, empty) {
- const value = element.prop('checked');
- if (value == null) {
- return;
- }
- if (!empty) {
- if (value) {
- return value;
- }
- return;
- }
- if (trim) {
- return value ? 1 : 0;
- }
- return value;
- },
- };
+ const SettingsCheckbox = {
+ types: ['checkbox'],
+ use() {
+ Settings = this;
+ },
+ create() {
+ return Settings.helper.createElement('input', {
+ type: 'checkbox',
+ });
+ },
+ set(element, value) {
+ element.prop('checked', value);
+ element.closest('.mdl-switch').toggleClass('is-checked', element.is(':checked'));
+ },
+ get(element, trim, empty) {
+ const value = element.prop('checked');
+ if (value == null) {
+ return;
+ }
- return SettingsCheckbox;
+ if (!empty) {
+ if (value) {
+ return value;
+ }
+
+ return;
+ }
+
+ if (trim) {
+ return value ? 1 : 0;
+ }
+
+ return value;
+ },
+ };
+
+ return SettingsCheckbox;
});
diff --git a/public/src/modules/settings/key.js b/public/src/modules/settings/key.js
index 5d25a1b..d0a69e7 100644
--- a/public/src/modules/settings/key.js
+++ b/public/src/modules/settings/key.js
@@ -1,100 +1,107 @@
'use strict';
-define('settings/key', function () {
- let helper = null;
- let lastKey = null;
- let oldKey = null;
- const keyMap = Object.freeze({
- 0: '',
- 8: 'Backspace',
- 9: 'Tab',
- 13: 'Enter',
- 27: 'Escape',
- 32: 'Space',
- 37: 'Left',
- 38: 'Up',
- 39: 'Right',
- 40: 'Down',
- 45: 'Insert',
- 46: 'Delete',
- 187: '=',
- 189: '-',
- 190: '.',
- 191: '/',
- 219: '[',
- 220: '\\',
- 221: ']',
- });
-
- function Key() {
- this.c = false;
- this.a = false;
- this.s = false;
- this.m = false;
- this.code = 0;
- this.char = '';
- }
-
- /**
+define('settings/key', () => {
+ let helper = null;
+ let lastKey = null;
+ let oldKey = null;
+ const keyMap = Object.freeze({
+ 0: '',
+ 8: 'Backspace',
+ 9: 'Tab',
+ 13: 'Enter',
+ 27: 'Escape',
+ 32: 'Space',
+ 37: 'Left',
+ 38: 'Up',
+ 39: 'Right',
+ 40: 'Down',
+ 45: 'Insert',
+ 46: 'Delete',
+ 187: '=',
+ 189: '-',
+ 190: '.',
+ 191: '/',
+ 219: '[',
+ 220: '\\',
+ 221: ']',
+ });
+
+ function Key() {
+ this.c = false;
+ this.a = false;
+ this.s = false;
+ this.m = false;
+ this.code = 0;
+ this.char = '';
+ }
+
+ /**
Returns either a Key-Object representing the given event or null if only modification-keys got released.
@param event The event to inspect.
@returns Key | null The Key-Object the focused element should be set to.
*/
- function getKey(event) {
- const anyModChange = (
- event.ctrlKey !== lastKey.c ||
- event.altKey !== lastKey.a ||
- event.shiftKey !== lastKey.s ||
- event.metaKey !== lastKey.m
- );
- const modChange = (
- event.ctrlKey +
- event.altKey +
- event.shiftKey +
- event.metaKey -
- lastKey.c -
- lastKey.a -
- lastKey.s -
- lastKey.m
- );
- const key = new Key();
- key.c = event.ctrlKey;
- key.a = event.altKey;
- key.s = event.shiftKey;
- key.m = event.metaKey;
- lastKey = key;
- if (anyModChange) {
- if (modChange < 0) {
- return null;
- }
- key.code = oldKey.code;
- key.char = oldKey.char;
- } else {
- key.code = event.which;
- key.char = convertKeyCodeToChar(key.code);
- }
- oldKey = key;
- return key;
- }
-
- /**
+ function getKey(event) {
+ const anyModuleChange = (
+ event.ctrlKey !== lastKey.c
+ || event.altKey !== lastKey.a
+ || event.shiftKey !== lastKey.s
+ || event.metaKey !== lastKey.m
+ );
+ const moduleChange = (
+ event.ctrlKey
+ + event.altKey
+ + event.shiftKey
+ + event.metaKey
+ - lastKey.c
+ - lastKey.a
+ - lastKey.s
+ - lastKey.m
+ );
+ const key = new Key();
+ key.c = event.ctrlKey;
+ key.a = event.altKey;
+ key.s = event.shiftKey;
+ key.m = event.metaKey;
+ lastKey = key;
+ if (anyModuleChange) {
+ if (moduleChange < 0) {
+ return null;
+ }
+
+ key.code = oldKey.code;
+ key.char = oldKey.char;
+ } else {
+ key.code = event.which;
+ key.char = convertKeyCodeToChar(key.code);
+ }
+
+ oldKey = key;
+ return key;
+ }
+
+ /**
Returns the string that represents the given key-code.
@param code The key-code.
@returns String Representation of the given key-code.
*/
- function convertKeyCodeToChar(code) {
- code = +code;
- if (code === 0) {
- return '';
- } else if (code >= 48 && code <= 90) {
- return String.fromCharCode(code).toUpperCase();
- } else if (code >= 112 && code <= 123) {
- return 'F' + (code - 111);
- }
- return keyMap[code] || ('#' + code);
- }
-
- /**
+ function convertKeyCodeToChar(code) {
+ code = Number(code);
+ if (code === 0) {
+ return '';
+ }
+
+ if (code >= 48 && code <= 90) {
+ return String.fromCharCode(code).toUpperCase();
+ }
+
+ if (code >= 112 && code <= 123) {
+ return 'F' + (code - 111);
+ }
+
+ return keyMap[code] || ('#' + code);
+ }
+
+ /**
Returns a string to identify a Key-Object.
@param key The Key-Object that should get identified.
@param human Whether to show 'Enter a key' when key-char is empty.
@@ -102,136 +109,153 @@ define('settings/key', function () {
@param separator The separator between modification-names and key-char.
@returns String The string to identify the given key-object the given way.
*/
- function getKeyString(key, human, short, separator) {
- let str = '';
- if (!(key instanceof Key)) {
- return str;
- }
- if (!key.char) {
- if (human) {
- return 'Enter a key';
- }
- return '';
- }
- if (!separator || /CtrlAShifMea#/.test(separator)) {
- separator = human ? ' + ' : '+';
- }
- if (key.c) {
- str += (short ? 'C' : 'Ctrl') + separator;
- }
- if (key.a) {
- str += (short ? 'A' : 'Alt') + separator;
- }
- if (key.s) {
- str += (short ? 'S' : 'Shift') + separator;
- }
- if (key.m) {
- str += (short ? 'M' : 'Meta') + separator;
- }
-
- let out;
- if (human) {
- out = key.char;
- } else if (key.code) {
- out = '#' + key.code || '';
- }
-
- return str + out;
- }
-
- /**
+ function getKeyString(key, human, short, separator) {
+ let string_ = '';
+ if (!(key instanceof Key)) {
+ return string_;
+ }
+
+ if (!key.char) {
+ if (human) {
+ return 'Enter a key';
+ }
+
+ return '';
+ }
+
+ if (!separator || /CtrlAShifMea#/.test(separator)) {
+ separator = human ? ' + ' : '+';
+ }
+
+ if (key.c) {
+ string_ += (short ? 'C' : 'Ctrl') + separator;
+ }
+
+ if (key.a) {
+ string_ += (short ? 'A' : 'Alt') + separator;
+ }
+
+ if (key.s) {
+ string_ += (short ? 'S' : 'Shift') + separator;
+ }
+
+ if (key.m) {
+ string_ += (short ? 'M' : 'Meta') + separator;
+ }
+
+ let out;
+ if (human) {
+ out = key.char;
+ } else if (key.code) {
+ out = '#' + key.code || '';
+ }
+
+ return string_ + out;
+ }
+
+ /**
Parses the given string into a Key-Object.
@param str The string to parse.
@returns Key The Key-Object that got identified by the given string.
*/
- function getKeyFromString(str) {
- if (str instanceof Key) {
- return str;
- }
- const key = new Key();
- const sep = /([^CtrlAShifMea#\d]+)(?:#|\d)/.exec(str);
- const parts = sep != null ? str.split(sep[1]) : [str];
- for (let i = 0; i < parts.length; i += 1) {
- const part = parts[i];
- switch (part) {
- case 'C':
- case 'Ctrl':
- key.c = true;
- break;
- case 'A':
- case 'Alt':
- key.a = true;
- break;
- case 'S':
- case 'Shift':
- key.s = true;
- break;
- case 'M':
- case 'Meta':
- key.m = true;
- break;
- default: {
- const num = /\d+/.exec(part);
- if (num != null) {
- key.code = num[0];
- }
- key.char = convertKeyCodeToChar(key.code);
- }
- }
- }
- return key;
- }
-
-
- const SettingsKey = {
- types: ['key'],
- use: function () {
- helper = this.helper;
- },
- init: function (element) {
- element.focus(function () {
- oldKey = element.data('keyData') || new Key();
- lastKey = new Key();
- }).keydown(function (event) {
- event.preventDefault();
- handleEvent(element, event);
- }).keyup(function (event) {
- handleEvent(element, event);
- });
- return element;
- },
- set: function (element, value) {
- const key = getKeyFromString(value || '');
- element.data('keyData', key);
- if (key.code) {
- element.removeClass('alert-danger');
- } else {
- element.addClass('alert-danger');
- }
- element.val(getKeyString(key, true, false, ' + '));
- },
- get: function (element, trim, empty) {
- const key = element.data('keyData');
- const separator = element.data('split') || element.data('separator') || '+';
- const short = !helper.isFalse(element.data('short'));
- if (trim) {
- if (empty || (key != null && key.char)) {
- return getKeyString(key, false, short, separator);
- }
- } else if (empty || (key != null && key.code)) {
- return key;
- }
- },
- };
-
- function handleEvent(element, event) {
- event = event || window.event;
- event.which = event.which || event.keyCode || event.key;
- const key = getKey(event);
- if (key != null) {
- SettingsKey.set(element, key);
- }
- }
-
- return SettingsKey;
+ function getKeyFromString(string_) {
+ if (string_ instanceof Key) {
+ return string_;
+ }
+
+ const key = new Key();
+ const separator = /([^CtrlAShifMea#\d]+)[#\d]/.exec(string_);
+ const parts = separator == null ? [string_] : string_.split(separator[1]);
+ for (const part of parts) {
+ switch (part) {
+ case 'C':
+ case 'Ctrl': {
+ key.c = true;
+ break;
+ }
+
+ case 'A':
+ case 'Alt': {
+ key.a = true;
+ break;
+ }
+
+ case 'S':
+ case 'Shift': {
+ key.s = true;
+ break;
+ }
+
+ case 'M':
+ case 'Meta': {
+ key.m = true;
+ break;
+ }
+
+ default: {
+ const number_ = /\d+/.exec(part);
+ if (number_ != null) {
+ key.code = number_[0];
+ }
+
+ key.char = convertKeyCodeToChar(key.code);
+ }
+ }
+ }
+
+ return key;
+ }
+
+ const SettingsKey = {
+ types: ['key'],
+ use() {
+ helper = this.helper;
+ },
+ init(element) {
+ element.focus(() => {
+ oldKey = element.data('keyData') || new Key();
+ lastKey = new Key();
+ }).keydown(event => {
+ event.preventDefault();
+ handleEvent(element, event);
+ }).keyup(event => {
+ handleEvent(element, event);
+ });
+ return element;
+ },
+ set(element, value) {
+ const key = getKeyFromString(value || '');
+ element.data('keyData', key);
+ if (key.code) {
+ element.removeClass('alert-danger');
+ } else {
+ element.addClass('alert-danger');
+ }
+
+ element.val(getKeyString(key, true, false, ' + '));
+ },
+ get(element, trim, empty) {
+ const key = element.data('keyData');
+ const separator = element.data('split') || element.data('separator') || '+';
+ const short = !helper.isFalse(element.data('short'));
+ if (trim) {
+ if (empty || (key != null && key.char)) {
+ return getKeyString(key, false, short, separator);
+ }
+ } else if (empty || (key != null && key.code)) {
+ return key;
+ }
+ },
+ };
+
+ function handleEvent(element, event) {
+ event ||= window.event;
+ event.which = event.which || event.keyCode || event.key;
+ const key = getKey(event);
+ if (key != null) {
+ SettingsKey.set(element, key);
+ }
+ }
+
+ return SettingsKey;
});
diff --git a/public/src/modules/settings/number.js b/public/src/modules/settings/number.js
index 4cf2884..e87bd6e 100644
--- a/public/src/modules/settings/number.js
+++ b/public/src/modules/settings/number.js
@@ -1,17 +1,17 @@
'use strict';
-define('settings/number', function () {
- return {
- types: ['number'],
- get: function (element, trim, empty) {
- const value = element.val();
- if (!empty) {
- if (value) {
- return +value;
- }
- return;
- }
- return value ? +value : 0;
- },
- };
-});
+define('settings/number', () => ({
+ types: ['number'],
+ get(element, trim, empty) {
+ const value = element.val();
+ if (!empty) {
+ if (value) {
+ return Number(value);
+ }
+
+ return;
+ }
+
+ return value ? Number(value) : 0;
+ },
+}));
diff --git a/public/src/modules/settings/object.js b/public/src/modules/settings/object.js
index 1c4bd82..98e552a 100644
--- a/public/src/modules/settings/object.js
+++ b/public/src/modules/settings/object.js
@@ -1,9 +1,9 @@
'use strict';
-define('settings/object', function () {
- let helper = null;
+define('settings/object', () => {
+ let helper = null;
- /**
+ /**
Creates a new child-element of given property with given data and calls given callback with elements to add.
@param field Any wrapper that contains all properties of the object.
@param key The key of the object.
@@ -13,112 +13,120 @@ define('settings/object', function () {
@param separator The separator to use.
@param insertCb The callback to insert the elements.
*/
- function addObjectPropertyElement(field, key, attributes, prop, value, separator, insertCb) {
- const prepend = attributes['data-prepend'];
- const append = attributes['data-append'];
- delete attributes['data-prepend'];
- delete attributes['data-append'];
- attributes = helper.deepClone(attributes);
- const type = attributes['data-type'] || attributes.type || 'text';
- const element = $(helper.createElementOfType(type, attributes.tagName, attributes));
- element.attr('data-parent', '_' + key);
- element.attr('data-prop', prop);
- delete attributes['data-type'];
- delete attributes.tagName;
- for (const name in attributes) {
- if (attributes.hasOwnProperty(name)) {
- const val = attributes[name];
- if (name.search('data-') === 0) {
- element.data(name.substring(5), val);
- } else if (name.search('prop-') === 0) {
- element.prop(name.substring(5), val);
- } else {
- element.attr(name, val);
- }
- }
- }
- helper.fillField(element, value);
- if ($('[data-parent="_' + key + '"]', field).length) {
- insertCb(separator);
- }
- if (prepend) {
- insertCb(prepend);
- }
- insertCb(element);
- if (append) {
- insertCb(append);
- }
- }
+ function addObjectPropertyElement(field, key, attributes, property, value, separator, insertCallback) {
+ const prepend = attributes['data-prepend'];
+ const append = attributes['data-append'];
+ delete attributes['data-prepend'];
+ delete attributes['data-append'];
+ attributes = helper.deepClone(attributes);
+ const type = attributes['data-type'] || attributes.type || 'text';
+ const element = $(helper.createElementOfType(type, attributes.tagName, attributes));
+ element.attr('data-parent', '_' + key);
+ element.attr('data-prop', property);
+ delete attributes['data-type'];
+ delete attributes.tagName;
+ for (const name in attributes) {
+ if (attributes.hasOwnProperty(name)) {
+ const value_ = attributes[name];
+ if (name.search('data-') === 0) {
+ element.data(name.slice(5), value_);
+ } else if (name.search('prop-') === 0) {
+ element.prop(name.slice(5), value_);
+ } else {
+ element.attr(name, value_);
+ }
+ }
+ }
- const SettingsObject = {
- types: ['object'],
- use: function () {
- helper = this.helper;
- },
- create: function (ignored, tagName) {
- return helper.createElement(tagName || 'div');
- },
- set: function (element, value) {
- const properties = element.data('attributes') || element.data('properties');
- const key = element.data('key') || element.data('parent');
- let separator = element.data('split') || ', ';
- let propertyIndex;
- let propertyName;
- let attributes;
- separator = (function () {
- try {
- return $(separator);
- } catch (_error) {
- return $(document.createTextNode(separator));
- }
- }());
- element.empty();
- if (typeof value !== 'object') {
- value = {};
- }
- if (Array.isArray(properties)) {
- for (propertyIndex in properties) {
- if (properties.hasOwnProperty(propertyIndex)) {
- attributes = properties[propertyIndex];
- if (typeof attributes !== 'object') {
- attributes = {};
- }
- propertyName = attributes['data-prop'] || attributes['data-property'] || propertyIndex;
- if (value[propertyName] === undefined && attributes['data-new'] !== undefined) {
- value[propertyName] = attributes['data-new'];
- }
- addObjectPropertyElement(
- element,
- key,
- attributes,
- propertyName,
- value[propertyName],
- separator.clone(),
- function (el) { element.append(el); }
- );
- }
- }
- }
- },
- get: function (element, trim, empty) {
- const key = element.data('key') || element.data('parent');
- const properties = $('[data-parent="_' + key + '"]', element);
- const value = {};
- properties.each(function (i, property) {
- property = $(property);
- const val = helper.readValue(property);
- const prop = property.data('prop');
- const empty = helper.isTrue(property.data('empty'));
- if (empty || (val !== undefined && (val == null || val.length !== 0))) {
- value[prop] = val;
- return val;
- }
- });
- if (empty || Object.keys(value).length) {
- return value;
- }
- },
- };
+ helper.fillField(element, value);
+ if ($('[data-parent="_' + key + '"]', field).length > 0) {
+ insertCallback(separator);
+ }
- return SettingsObject;
+ if (prepend) {
+ insertCallback(prepend);
+ }
+
+ insertCallback(element);
+ if (append) {
+ insertCallback(append);
+ }
+ }
+
+ const SettingsObject = {
+ types: ['object'],
+ use() {
+ helper = this.helper;
+ },
+ create(ignored, tagName) {
+ return helper.createElement(tagName || 'div');
+ },
+ set(element, value) {
+ const properties = element.data('attributes') || element.data('properties');
+ const key = element.data('key') || element.data('parent');
+ let separator = element.data('split') || ', ';
+ let propertyIndex;
+ let propertyName;
+ let attributes;
+ separator = (function () {
+ try {
+ return $(separator);
+ } catch {
+ return $(document.createTextNode(separator));
+ }
+ })();
+ element.empty();
+ if (typeof value !== 'object') {
+ value = {};
+ }
+
+ if (Array.isArray(properties)) {
+ for (propertyIndex in properties) {
+ if (properties.hasOwnProperty(propertyIndex)) {
+ attributes = properties[propertyIndex];
+ if (typeof attributes !== 'object') {
+ attributes = {};
+ }
+
+ propertyName = attributes['data-prop'] || attributes['data-property'] || propertyIndex;
+ if (value[propertyName] === undefined && attributes['data-new'] !== undefined) {
+ value[propertyName] = attributes['data-new'];
+ }
+
+ addObjectPropertyElement(
+ element,
+ key,
+ attributes,
+ propertyName,
+ value[propertyName],
+ separator.clone(),
+ element_ => {
+ element.append(element_);
+ },
+ );
+ }
+ }
+ }
+ },
+ get(element, trim, empty) {
+ const key = element.data('key') || element.data('parent');
+ const properties = $('[data-parent="_' + key + '"]', element);
+ const value = {};
+ properties.each((i, property) => {
+ property = $(property);
+ const value_ = helper.readValue(property);
+ const property_ = property.data('prop');
+ const empty = helper.isTrue(property.data('empty'));
+ if (empty || (value_ !== undefined && (value_ == null || value_.length > 0))) {
+ value[property_] = value_;
+ return value_;
+ }
+ });
+ if (empty || Object.keys(value).length > 0) {
+ return value;
+ }
+ },
+ };
+
+ return SettingsObject;
});
diff --git a/public/src/modules/settings/select.js b/public/src/modules/settings/select.js
index c149235..32ab601 100644
--- a/public/src/modules/settings/select.js
+++ b/public/src/modules/settings/select.js
@@ -1,46 +1,44 @@
'use strict';
-define('settings/select', function () {
- let Settings = null;
+define('settings/select', () => {
+ let Settings = null;
- function addOptions(element, options) {
- for (let i = 0; i < options.length; i += 1) {
- const optionData = options[i];
- const value = optionData.text || optionData.value;
- delete optionData.text;
- element.append($(Settings.helper.createElement('option', optionData)).text(value));
- }
- }
+ function addOptions(element, options) {
+ for (const optionData of options) {
+ const value = optionData.text || optionData.value;
+ delete optionData.text;
+ element.append($(Settings.helper.createElement('option', optionData)).text(value));
+ }
+ }
+ const SettingsSelect = {
+ types: ['select'],
+ use() {
+ Settings = this;
+ },
+ create(ignore, ignored, data) {
+ const element = $(Settings.helper.createElement('select'));
+ // Prevent data-options from being attached to DOM
+ addOptions(element, data['data-options']);
+ delete data['data-options'];
+ return element;
+ },
+ init(element) {
+ const options = element.data('options');
+ if (options != null) {
+ addOptions(element, options);
+ }
+ },
+ set(element, value) {
+ element.val(value || '');
+ },
+ get(element, ignored, empty) {
+ const value = element.val();
+ if (empty || value) {
+ return value;
+ }
+ },
+ };
- const SettingsSelect = {
- types: ['select'],
- use: function () {
- Settings = this;
- },
- create: function (ignore, ignored, data) {
- const element = $(Settings.helper.createElement('select'));
- // prevent data-options from being attached to DOM
- addOptions(element, data['data-options']);
- delete data['data-options'];
- return element;
- },
- init: function (element) {
- const options = element.data('options');
- if (options != null) {
- addOptions(element, options);
- }
- },
- set: function (element, value) {
- element.val(value || '');
- },
- get: function (element, ignored, empty) {
- const value = element.val();
- if (empty || value) {
- return value;
- }
- },
- };
-
- return SettingsSelect;
+ return SettingsSelect;
});
diff --git a/public/src/modules/settings/sorted-list.js b/public/src/modules/settings/sorted-list.js
index d565e4e..34b7f93 100644
--- a/public/src/modules/settings/sorted-list.js
+++ b/public/src/modules/settings/sorted-list.js
@@ -1,172 +1,171 @@
'use strict';
define('settings/sorted-list', [
- 'benchpress',
- 'bootbox',
- 'hooks',
- 'jquery-ui/widgets/sortable',
-], function (benchpress, bootbox, hooks) {
- let Settings;
-
-
- const SortedList = {
- types: ['sorted-list'],
- use: function () {
- Settings = this;
- },
- set: function ($container, values) {
- const key = $container.attr('data-sorted-list');
-
- values[key] = [];
- $container.find('[data-type="item"]').each(function (idx, item) {
- const itemUUID = $(item).attr('data-sorted-list-uuid');
-
- const formData = Settings.helper.serializeForm($('[data-sorted-list-object="' + key + '"][data-sorted-list-uuid="' + itemUUID + '"]'));
- stripTags(formData);
- values[key].push(formData);
- });
- },
- get: async ($container, hash) => {
- const { listEl, key, formTpl, formValues } = await hooks.fire('filter:settings.sorted-list.load', {
- listEl: $container.find('[data-type="list"]'),
- key: $container.attr('data-sorted-list'),
- formTpl: $container.attr('data-form-template'),
- formValues: {},
- });
-
- const formHtml = await benchpress.render(formTpl, formValues);
-
- const addBtn = $('[data-sorted-list="' + key + '"] [data-type="add"]');
-
- addBtn.on('click', function () {
- const modal = bootbox.confirm(formHtml, function (save) {
- if (save) {
- SortedList.addItem(modal.find('form').children(), $container);
- }
- });
- hooks.fire('action:settings.sorted-list.modal', { modal });
- });
-
- const call = $container.parents('form').attr('data-socket-get');
- const list = ajaxify.data[call ? hash : 'settings'][key];
-
- if (Array.isArray(list) && typeof list[0] !== 'string') {
- const items = await Promise.all(list.map(async (item) => {
- ({ item } = await hooks.fire('filter:settings.sorted-list.loadItem', { item }));
-
- const itemUUID = utils.generateUUID();
- const form = $(formHtml).deserialize(item);
- form.attr('data-sorted-list-uuid', itemUUID);
- form.attr('data-sorted-list-object', key);
- $('#content').append(form.hide());
-
- return { itemUUID, item };
- }));
-
- // todo: parse() needs to be refactored to return the html, so multiple calls can be parallelized
- // eslint-disable-next-line no-restricted-syntax
- for (const { itemUUID, item } of items) {
- // eslint-disable-next-line no-await-in-loop
- await parse($container, itemUUID, item);
- hooks.fire('action:settings.sorted-list.itemLoaded', { element: listEl.get(0) });
- }
-
- hooks.fire('action:settings.sorted-list.loaded', {
- containerEl: $container.get(0),
- listEl: listEl.get(0),
- hash,
- key,
- });
- }
-
- listEl.sortable().addClass('pointer');
- },
- addItem: async ($formElements, $target) => {
- const key = $target.attr('data-sorted-list');
- const itemUUID = utils.generateUUID();
- const form = $('');
- form.append($formElements);
-
- $('#content').append(form.hide());
-
- let data = Settings.helper.serializeForm(form);
- ({ item: data } = await hooks.fire('filter:settings.sorted-list.loadItem', { item: data }));
- parse($target, itemUUID, data);
- },
- };
-
- function setupRemoveButton($container, itemUUID) {
- const removeBtn = $container.find('[data-sorted-list-uuid="' + itemUUID + '"] [data-type="remove"]');
- removeBtn.on('click', function () {
- $('[data-sorted-list-uuid="' + itemUUID + '"]').remove();
- });
- }
-
- function setupEditButton($container, itemUUID) {
- const $list = $container.find('[data-type="list"]');
- const key = $container.attr('data-sorted-list');
- const editBtn = $('[data-sorted-list-uuid="' + itemUUID + '"] [data-type="edit"]');
-
- editBtn.on('click', function () {
- const form = $('[data-sorted-list-uuid="' + itemUUID + '"][data-sorted-list-object="' + key + '"]');
- const clone = form.clone(true).show();
-
- // .clone() doesn't preserve the state of `select` elements, fixing after the fact
- clone.find('select').each((idx, el) => {
- el.value = form.find(`select#${el.id}`).val();
- });
-
- const modal = bootbox.confirm(clone, async (save) => {
- if (save) {
- const form = $('');
- form.append(modal.find('form').children());
-
- $('#content').find('[data-sorted-list-uuid="' + itemUUID + '"][data-sorted-list-object="' + key + '"]').remove();
- $('#content').append(form.hide());
-
-
- let data = Settings.helper.serializeForm(form);
- ({ item: data } = await hooks.fire('filter:settings.sorted-list.loadItem', { item: data }));
- stripTags(data);
-
- const oldItem = $list.find('[data-sorted-list-uuid="' + itemUUID + '"]');
- parse($container, itemUUID, data, oldItem);
- }
- });
- hooks.fire('action:settings.sorted-list.modal', { modal });
- });
- }
-
- function parse($container, itemUUID, data, replaceEl) {
- // replaceEl is optional
- const $list = $container.find('[data-type="list"]');
- const itemTpl = $container.attr('data-item-template');
-
- stripTags(data);
-
- return new Promise((resolve) => {
- app.parseAndTranslate(itemTpl, data, function (itemHtml) {
- itemHtml = $(itemHtml);
- if (replaceEl) {
- replaceEl.replaceWith(itemHtml);
- } else {
- $list.append(itemHtml);
- }
- itemHtml.attr('data-sorted-list-uuid', itemUUID);
-
- setupRemoveButton($container, itemUUID);
- setupEditButton($container, itemUUID);
- hooks.fire('action:settings.sorted-list.parse', { itemHtml });
- resolve();
- });
- });
- }
-
- function stripTags(data) {
- return Object.entries(data || {}).forEach(([field, value]) => {
- data[field] = typeof value === 'string' ? utils.stripHTMLTags(value, utils.stripTags) : value;
- });
- }
-
- return SortedList;
+ 'benchpress',
+ 'bootbox',
+ 'hooks',
+ 'jquery-ui/widgets/sortable',
+], (benchpress, bootbox, hooks) => {
+ let Settings;
+
+ const SortedList = {
+ types: ['sorted-list'],
+ use() {
+ Settings = this;
+ },
+ set($container, values) {
+ const key = $container.attr('data-sorted-list');
+
+ values[key] = [];
+ $container.find('[data-type="item"]').each((index, item) => {
+ const itemUUID = $(item).attr('data-sorted-list-uuid');
+
+ const formData = Settings.helper.serializeForm($('[data-sorted-list-object="' + key + '"][data-sorted-list-uuid="' + itemUUID + '"]'));
+ stripTags(formData);
+ values[key].push(formData);
+ });
+ },
+ async get($container, hash) {
+ const {listEl, key, formTpl, formValues} = await hooks.fire('filter:settings.sorted-list.load', {
+ listEl: $container.find('[data-type="list"]'),
+ key: $container.attr('data-sorted-list'),
+ formTpl: $container.attr('data-form-template'),
+ formValues: {},
+ });
+
+ const formHtml = await benchpress.render(formTpl, formValues);
+
+ const addButton = $('[data-sorted-list="' + key + '"] [data-type="add"]');
+
+ addButton.on('click', () => {
+ const modal = bootbox.confirm(formHtml, save => {
+ if (save) {
+ SortedList.addItem(modal.find('form').children(), $container);
+ }
+ });
+ hooks.fire('action:settings.sorted-list.modal', {modal});
+ });
+
+ const call = $container.parents('form').attr('data-socket-get');
+ const list = ajaxify.data[call ? hash : 'settings'][key];
+
+ if (Array.isArray(list) && typeof list[0] !== 'string') {
+ const items = await Promise.all(list.map(async item => {
+ ({item} = await hooks.fire('filter:settings.sorted-list.loadItem', {item}));
+
+ const itemUUID = utils.generateUUID();
+ const form = $(formHtml).deserialize(item);
+ form.attr('data-sorted-list-uuid', itemUUID);
+ form.attr('data-sorted-list-object', key);
+ $('#content').append(form.hide());
+
+ return {itemUUID, item};
+ }));
+
+ // Todo: parse() needs to be refactored to return the html, so multiple calls can be parallelized
+
+ for (const {itemUUID, item} of items) {
+ // eslint-disable-next-line no-await-in-loop
+ await parse($container, itemUUID, item);
+ hooks.fire('action:settings.sorted-list.itemLoaded', {element: listEl.get(0)});
+ }
+
+ hooks.fire('action:settings.sorted-list.loaded', {
+ containerEl: $container.get(0),
+ listEl: listEl.get(0),
+ hash,
+ key,
+ });
+ }
+
+ listEl.sortable().addClass('pointer');
+ },
+ async addItem($formElements, $target) {
+ const key = $target.attr('data-sorted-list');
+ const itemUUID = utils.generateUUID();
+ const form = $('');
+ form.append($formElements);
+
+ $('#content').append(form.hide());
+
+ let data = Settings.helper.serializeForm(form);
+ ({item: data} = await hooks.fire('filter:settings.sorted-list.loadItem', {item: data}));
+ parse($target, itemUUID, data);
+ },
+ };
+
+ function setupRemoveButton($container, itemUUID) {
+ const removeButton = $container.find('[data-sorted-list-uuid="' + itemUUID + '"] [data-type="remove"]');
+ removeButton.on('click', () => {
+ $('[data-sorted-list-uuid="' + itemUUID + '"]').remove();
+ });
+ }
+
+ function setupEditButton($container, itemUUID) {
+ const $list = $container.find('[data-type="list"]');
+ const key = $container.attr('data-sorted-list');
+ const editButton = $('[data-sorted-list-uuid="' + itemUUID + '"] [data-type="edit"]');
+
+ editButton.on('click', () => {
+ const form = $('[data-sorted-list-uuid="' + itemUUID + '"][data-sorted-list-object="' + key + '"]');
+ const clone = form.clone(true).show();
+
+ // .clone() doesn't preserve the state of `select` elements, fixing after the fact
+ clone.find('select').each((index, element) => {
+ element.value = form.find(`select#${element.id}`).val();
+ });
+
+ const modal = bootbox.confirm(clone, async save => {
+ if (save) {
+ const form = $('');
+ form.append(modal.find('form').children());
+
+ $('#content').find('[data-sorted-list-uuid="' + itemUUID + '"][data-sorted-list-object="' + key + '"]').remove();
+ $('#content').append(form.hide());
+
+ let data = Settings.helper.serializeForm(form);
+ ({item: data} = await hooks.fire('filter:settings.sorted-list.loadItem', {item: data}));
+ stripTags(data);
+
+ const oldItem = $list.find('[data-sorted-list-uuid="' + itemUUID + '"]');
+ parse($container, itemUUID, data, oldItem);
+ }
+ });
+ hooks.fire('action:settings.sorted-list.modal', {modal});
+ });
+ }
+
+ function parse($container, itemUUID, data, replaceElement) {
+ // ReplaceEl is optional
+ const $list = $container.find('[data-type="list"]');
+ const itemTpl = $container.attr('data-item-template');
+
+ stripTags(data);
+
+ return new Promise(resolve => {
+ app.parseAndTranslate(itemTpl, data, itemHtml => {
+ itemHtml = $(itemHtml);
+ if (replaceElement) {
+ replaceElement.replaceWith(itemHtml);
+ } else {
+ $list.append(itemHtml);
+ }
+
+ itemHtml.attr('data-sorted-list-uuid', itemUUID);
+
+ setupRemoveButton($container, itemUUID);
+ setupEditButton($container, itemUUID);
+ hooks.fire('action:settings.sorted-list.parse', {itemHtml});
+ resolve();
+ });
+ });
+ }
+
+ function stripTags(data) {
+ return Object.entries(data || {}).forEach(([field, value]) => {
+ data[field] = typeof value === 'string' ? utils.stripHTMLTags(value, utils.stripTags) : value;
+ });
+ }
+
+ return SortedList;
});
diff --git a/public/src/modules/settings/textarea.js b/public/src/modules/settings/textarea.js
index 1ef9513..86d78d7 100644
--- a/public/src/modules/settings/textarea.js
+++ b/public/src/modules/settings/textarea.js
@@ -1,36 +1,34 @@
'use strict';
-define('settings/textarea', function () {
- let Settings = null;
+define('settings/textarea', () => {
+ let Settings = null;
- const SettingsArea = {
- types: ['textarea'],
- use: function () {
- Settings = this;
- },
- create: function () {
- return Settings.helper.createElement('textarea');
- },
- set: function (element, value, trim) {
- if (trim && value != null && typeof value.trim === 'function') {
- value = value.trim();
- }
- element.val(value || '');
- },
- get: function (element, trim, empty) {
- let value = element.val();
- if (trim) {
- if (value == null) {
- value = undefined;
- } else {
- value = value.trim();
- }
- }
- if (empty || value) {
- return value;
- }
- },
- };
+ const SettingsArea = {
+ types: ['textarea'],
+ use() {
+ Settings = this;
+ },
+ create() {
+ return Settings.helper.createElement('textarea');
+ },
+ set(element, value, trim) {
+ if (trim && value != null && typeof value.trim === 'function') {
+ value = value.trim();
+ }
- return SettingsArea;
+ element.val(value || '');
+ },
+ get(element, trim, empty) {
+ let value = element.val();
+ if (trim) {
+ value = value == null ? undefined : value.trim();
+ }
+
+ if (empty || value) {
+ return value;
+ }
+ },
+ };
+
+ return SettingsArea;
});
diff --git a/public/src/modules/share.js b/public/src/modules/share.js
index ea2f131..0961f14 100644
--- a/public/src/modules/share.js
+++ b/public/src/modules/share.js
@@ -1,55 +1,54 @@
'use strict';
-
-define('share', ['hooks'], function (hooks) {
- const module = {};
-
- module.addShareHandlers = function (name) {
- const baseUrl = window.location.protocol + '//' + window.location.host;
-
- function openShare(url, urlToPost, width, height) {
- window.open(url + encodeURIComponent(baseUrl + config.relative_path + urlToPost), '_blank', 'width=' + width + ',height=' + height + ',scrollbars=no,status=no');
- hooks.fire('action:share.open', {
- url: url,
- urlToPost: urlToPost,
- });
- return false;
- }
-
- $('#content').off('shown.bs.dropdown', '.share-dropdown').on('shown.bs.dropdown', '.share-dropdown', function () {
- const postLink = $(this).find('.post-link');
- postLink.val(baseUrl + getPostUrl($(this)));
-
- // without the setTimeout can't select the text in the input
- setTimeout(function () {
- postLink.putCursorAtEnd().select();
- }, 50);
- });
-
- addHandler('.post-link', function (e) {
- e.preventDefault();
- return false;
- });
-
- addHandler('[component="share/twitter"]', function () {
- return openShare('https://twitter.com/intent/tweet?text=' + encodeURIComponent(name) + '&url=', getPostUrl($(this)), 550, 420);
- });
-
- addHandler('[component="share/facebook"]', function () {
- return openShare('https://www.facebook.com/sharer/sharer.php?u=', getPostUrl($(this)), 626, 436);
- });
-
- hooks.fire('action:share.addHandlers', { openShare: openShare });
- };
-
- function addHandler(selector, callback) {
- $('#content').off('click', selector).on('click', selector, callback);
- }
-
- function getPostUrl(clickedElement) {
- const pid = parseInt(clickedElement.parents('[data-pid]').attr('data-pid'), 10);
- return '/post' + (pid ? '/' + (pid) : '');
- }
-
- return module;
+define('share', ['hooks'], hooks => {
+ const module = {};
+
+ module.addShareHandlers = function (name) {
+ const baseUrl = window.location.protocol + '//' + window.location.host;
+
+ function openShare(url, urlToPost, width, height) {
+ window.open(url + encodeURIComponent(baseUrl + config.relative_path + urlToPost), '_blank', 'width=' + width + ',height=' + height + ',scrollbars=no,status=no');
+ hooks.fire('action:share.open', {
+ url,
+ urlToPost,
+ });
+ return false;
+ }
+
+ $('#content').off('shown.bs.dropdown', '.share-dropdown').on('shown.bs.dropdown', '.share-dropdown', function () {
+ const postLink = $(this).find('.post-link');
+ postLink.val(baseUrl + getPostUrl($(this)));
+
+ // Without the setTimeout can't select the text in the input
+ setTimeout(() => {
+ postLink.putCursorAtEnd().select();
+ }, 50);
+ });
+
+ addHandler('.post-link', e => {
+ e.preventDefault();
+ return false;
+ });
+
+ addHandler('[component="share/twitter"]', function () {
+ return openShare('https://twitter.com/intent/tweet?text=' + encodeURIComponent(name) + '&url=', getPostUrl($(this)), 550, 420);
+ });
+
+ addHandler('[component="share/facebook"]', function () {
+ return openShare('https://www.facebook.com/sharer/sharer.php?u=', getPostUrl($(this)), 626, 436);
+ });
+
+ hooks.fire('action:share.addHandlers', {openShare});
+ };
+
+ function addHandler(selector, callback) {
+ $('#content').off('click', selector).on('click', selector, callback);
+ }
+
+ function getPostUrl(clickedElement) {
+ const pid = Number.parseInt(clickedElement.parents('[data-pid]').attr('data-pid'), 10);
+ return '/post' + (pid ? '/' + (pid) : '');
+ }
+
+ return module;
});
diff --git a/public/src/modules/slugify.js b/public/src/modules/slugify.js
index b189b2c..0df7157 100644
--- a/public/src/modules/slugify.js
+++ b/public/src/modules/slugify.js
@@ -2,39 +2,37 @@
/* global XRegExp */
(function (factory) {
- if (typeof define === 'function' && define.amd) {
- define('slugify', ['xregexp'], factory);
- } else if (typeof exports === 'object') {
- module.exports = factory(require('xregexp'));
- } else {
- window.slugify = factory(XRegExp);
- }
-}(function (XRegExp) {
- const invalidUnicodeChars = XRegExp('[^\\p{L}\\s\\d\\-_]', 'g');
- const invalidLatinChars = /[^\w\s\d\-_]/g;
- const trimRegex = /^\s+|\s+$/g;
- const collapseWhitespace = /\s+/g;
- const collapseDash = /-+/g;
- const trimTrailingDash = /-$/g;
- const trimLeadingDash = /^-/g;
- const isLatin = /^[\w\d\s.,\-@]+$/;
+ if (typeof define === 'function' && define.amd) {
+ define('slugify', ['xregexp'], factory);
+ } else if (typeof exports === 'object') {
+ module.exports = factory(require('xregexp'));
+ } else {
+ window.slugify = factory(XRegExp);
+ }
+})(XRegExp => {
+ const invalidUnicodeChars = XRegExp('[^\\p{L}\\s\\d\\-_]', 'g');
+ const invalidLatinChars = /[^\w\s\d\-_]/g;
+ const trimRegex = /^\s+|\s+$/g;
+ const collapseWhitespace = /\s+/g;
+ const collapseDash = /-+/g;
+ const trimTrailingDash = /-$/g;
+ const trimLeadingDash = /^-/g;
+ const isLatin = /^[\w\d\s.,\-@]+$/;
- // http://dense13.com/blog/2009/05/03/converting-string-to-slug-javascript/
- return function slugify(str, preserveCase) {
- if (!str) {
- return '';
- }
- str = String(str).replace(trimRegex, '');
- if (isLatin.test(str)) {
- str = str.replace(invalidLatinChars, '-');
- } else {
- str = XRegExp.replace(str, invalidUnicodeChars, '-');
- }
- str = !preserveCase ? str.toLocaleLowerCase() : str;
- str = str.replace(collapseWhitespace, '-');
- str = str.replace(collapseDash, '-');
- str = str.replace(trimTrailingDash, '');
- str = str.replace(trimLeadingDash, '');
- return str;
- };
-}));
+ // http://dense13.com/blog/2009/05/03/converting-string-to-slug-javascript/
+ return function slugify(string_, preserveCase) {
+ if (!string_) {
+ return '';
+ }
+
+ string_ = String(string_).replaceAll(trimRegex, '');
+ string_ = isLatin.test(string_) ? string_.replaceAll(invalidLatinChars, '-') : XRegExp.replace(string_, invalidUnicodeChars, '-');
+
+ string_ = preserveCase ? string_ : string_.toLocaleLowerCase();
+ string_ = string_.replaceAll(collapseWhitespace, '-');
+ string_ = string_.replaceAll(collapseDash, '-');
+ string_ = string_.replaceAll(trimTrailingDash, '');
+ string_ = string_.replaceAll(trimLeadingDash, '');
+ return string_;
+ };
+});
diff --git a/public/src/modules/sort.js b/public/src/modules/sort.js
index 85c4dfd..8ee5aa3 100644
--- a/public/src/modules/sort.js
+++ b/public/src/modules/sort.js
@@ -1,39 +1,39 @@
'use strict';
+define('sort', ['components', 'api'], (components, api) => {
+ const module = {};
-define('sort', ['components', 'api'], function (components, api) {
- const module = {};
+ module.handleSort = function (field, gotoOnSave) {
+ const threadSort = components.get('thread/sort');
+ threadSort.find('i').removeClass('fa-check');
+ const currentSetting = threadSort.find('a[data-sort="' + config[field] + '"]');
+ currentSetting.find('i').addClass('fa-check');
- module.handleSort = function (field, gotoOnSave) {
- const threadSort = components.get('thread/sort');
- threadSort.find('i').removeClass('fa-check');
- const currentSetting = threadSort.find('a[data-sort="' + config[field] + '"]');
- currentSetting.find('i').addClass('fa-check');
+ $('body')
+ .off('click', '[component="thread/sort"] a')
+ .on('click', '[component="thread/sort"] a', function () {
+ function refresh(newSetting, parameters) {
+ config[field] = newSetting;
+ const qs = decodeURIComponent($.param(parameters));
+ ajaxify.go(gotoOnSave + (qs ? '?' + qs : ''));
+ }
- $('body')
- .off('click', '[component="thread/sort"] a')
- .on('click', '[component="thread/sort"] a', function () {
- function refresh(newSetting, params) {
- config[field] = newSetting;
- const qs = decodeURIComponent($.param(params));
- ajaxify.go(gotoOnSave + (qs ? '?' + qs : ''));
- }
- const newSetting = $(this).attr('data-sort');
- if (app.user.uid) {
- const payload = { settings: {} };
- payload.settings[field] = newSetting;
- api.put(`/users/${app.user.uid}/settings`, payload).then(() => {
- // Yes, this is normal. If you are logged in, sort is not
- // added to qs since it's saved to user settings
- refresh(newSetting, utils.params());
- });
- } else {
- const urlParams = utils.params();
- urlParams.sort = newSetting;
- refresh(newSetting, urlParams);
- }
- });
- };
+ const newSetting = $(this).attr('data-sort');
+ if (app.user.uid) {
+ const payload = {settings: {}};
+ payload.settings[field] = newSetting;
+ api.put(`/users/${app.user.uid}/settings`, payload).then(() => {
+ // Yes, this is normal. If you are logged in, sort is not
+ // added to qs since it's saved to user settings
+ refresh(newSetting, utils.params());
+ });
+ } else {
+ const urlParameters = utils.params();
+ urlParameters.sort = newSetting;
+ refresh(newSetting, urlParameters);
+ }
+ });
+ };
- return module;
+ return module;
});
diff --git a/public/src/modules/storage.js b/public/src/modules/storage.js
index b7d4c9a..a8813bd 100644
--- a/public/src/modules/storage.js
+++ b/public/src/modules/storage.js
@@ -3,82 +3,89 @@
/**
* Checks localStorage and provides a fallback if it doesn't exist or is disabled
*/
-define('storage', function () {
- function Storage() {
- this._store = {};
- this._keys = [];
- }
- Storage.prototype.isMock = true;
- Storage.prototype.setItem = function (key, val) {
- key = String(key);
- if (this._keys.indexOf(key) === -1) {
- this._keys.push(key);
- }
- this._store[key] = val;
- };
- Storage.prototype.getItem = function (key) {
- key = String(key);
- if (this._keys.indexOf(key) === -1) {
- return null;
- }
-
- return this._store[key];
- };
- Storage.prototype.removeItem = function (key) {
- key = String(key);
- this._keys = this._keys.filter(function (x) {
- return x !== key;
- });
- this._store[key] = null;
- };
- Storage.prototype.clear = function () {
- this._keys = [];
- this._store = {};
- };
- Storage.prototype.key = function (n) {
- n = parseInt(n, 10) || 0;
- return this._keys[n];
- };
- if (Object.defineProperty) {
- Object.defineProperty(Storage.prototype, 'length', {
- get: function () {
- return this._keys.length;
- },
- });
- }
-
- let storage;
- const item = Date.now().toString();
-
- try {
- storage = window.localStorage;
- storage.setItem(item, item);
- if (storage.getItem(item) !== item) {
- throw Error('localStorage behaved unexpectedly');
- }
- storage.removeItem(item);
-
- return storage;
- } catch (e) {
- console.warn(e);
- console.warn('localStorage failed, falling back on sessionStorage');
-
- // see if sessionStorage works, and if so, return that
- try {
- storage = window.sessionStorage;
- storage.setItem(item, item);
- if (storage.getItem(item) !== item) {
- throw Error('sessionStorage behaved unexpectedly');
- }
- storage.removeItem(item);
-
- return storage;
- } catch (e) {
- console.warn(e);
- console.warn('sessionStorage failed, falling back on memory storage');
-
- // return an object implementing mock methods
- return new Storage();
- }
- }
+define('storage', () => {
+ function Storage() {
+ this._store = {};
+ this._keys = [];
+ }
+
+ Storage.prototype.isMock = true;
+ Storage.prototype.setItem = function (key, value) {
+ key = String(key);
+ if (!this._keys.includes(key)) {
+ this._keys.push(key);
+ }
+
+ this._store[key] = value;
+ };
+
+ Storage.prototype.getItem = function (key) {
+ key = String(key);
+ if (!this._keys.includes(key)) {
+ return null;
+ }
+
+ return this._store[key];
+ };
+
+ Storage.prototype.removeItem = function (key) {
+ key = String(key);
+ this._keys = this._keys.filter(x => x !== key);
+ this._store[key] = null;
+ };
+
+ Storage.prototype.clear = function () {
+ this._keys = [];
+ this._store = {};
+ };
+
+ Storage.prototype.key = function (n) {
+ n = Number.parseInt(n, 10) || 0;
+ return this._keys[n];
+ };
+
+ if (Object.defineProperty) {
+ Object.defineProperty(Storage.prototype, 'length', {
+ get() {
+ return this._keys.length;
+ },
+ });
+ }
+
+ let storage;
+ const item = Date.now().toString();
+
+ try {
+ storage = window.localStorage;
+ storage.setItem(item, item);
+ if (storage.getItem(item) !== item) {
+ throw new Error('localStorage behaved unexpectedly');
+ }
+
+ storage.removeItem(item);
+
+ return storage;
+ } catch (error) {
+ console.warn(error);
+ console.warn('localStorage failed, falling back on sessionStorage');
+
+ // See if sessionStorage works, and if so, return that
+ try {
+ storage = window.sessionStorage;
+ storage.setItem(item, item);
+ if (storage.getItem(item) !== item) {
+ throw new Error('sessionStorage behaved unexpectedly');
+ }
+
+ storage.removeItem(item);
+
+ return storage;
+ } catch (error) {
+ console.warn(error);
+ console.warn('sessionStorage failed, falling back on memory storage');
+
+ // Return an object implementing mock methods
+ return new Storage();
+ }
+ }
});
diff --git a/public/src/modules/taskbar.js b/public/src/modules/taskbar.js
index 4d1ddcb..17c829a 100644
--- a/public/src/modules/taskbar.js
+++ b/public/src/modules/taskbar.js
@@ -1,213 +1,219 @@
'use strict';
-
-define('taskbar', ['benchpress', 'translator', 'hooks'], function (Benchpress, translator, hooks) {
- const taskbar = {};
-
- taskbar.init = function () {
- const self = this;
-
- Benchpress.render('modules/taskbar', {}).then(function (html) {
- self.taskbar = $(html);
- self.tasklist = self.taskbar.find('ul');
- $(document.body).append(self.taskbar);
-
- self.taskbar.on('click', 'li', async function () {
- const $btn = $(this);
- const moduleName = $btn.attr('data-module');
- const uuid = $btn.attr('data-uuid');
-
- const module = await app.require(moduleName);
- if (!$btn.hasClass('active')) {
- minimizeAll();
- module.load(uuid);
- taskbar.toggleNew(uuid, false);
-
- taskbar.tasklist.removeClass('active');
- $btn.addClass('active');
- } else {
- module.minimize(uuid);
- }
- return false;
- });
- });
-
- $(window).on('action:app.loggedOut', function () {
- taskbar.closeAll();
- });
- };
-
- taskbar.close = async function (moduleName, uuid) {
- // Sends signal to the appropriate module's .close() fn (if present)
- const btnEl = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]');
- let fnName = 'close';
-
- // TODO: Refactor chat module to not take uuid in close instead of by jQuery element
- if (moduleName === 'chat') {
- fnName = 'closeByUUID';
- }
-
- if (btnEl.length) {
- const module = await app.require(moduleName);
- if (module && typeof module[fnName] === 'function') {
- module[fnName](uuid);
- }
- }
- };
-
- taskbar.closeAll = function (module) {
- // module is optional
- let selector = '[data-uuid]';
-
- if (module) {
- selector = '[data-module="' + module + '"]' + selector;
- }
-
- taskbar.tasklist.find(selector).each(function (idx, el) {
- taskbar.close(module || el.getAttribute('data-module'), el.getAttribute('data-uuid'));
- });
- };
-
- taskbar.discard = function (module, uuid) {
- const btnEl = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]');
- btnEl.remove();
-
- update();
- };
-
- taskbar.push = function (module, uuid, options, callback) {
- callback = callback || function () {};
- const element = taskbar.tasklist.find('li[data-uuid="' + uuid + '"]');
-
- const data = {
- module: module,
- uuid: uuid,
- options: options,
- element: element,
- };
-
- hooks.fire('filter:taskbar.push', data);
-
- if (!element.length && data.module) {
- createTaskbarItem(data, callback);
- } else {
- callback(element);
- }
- };
-
- taskbar.get = function (module) {
- const items = $('[data-module="' + module + '"]').map(function (idx, el) {
- return $(el).data();
- });
-
- return items;
- };
-
- taskbar.minimize = function (module, uuid) {
- const btnEl = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]');
- btnEl.toggleClass('active', false);
- };
-
- taskbar.toggleNew = function (uuid, state, silent) {
- const btnEl = taskbar.tasklist.find('[data-uuid="' + uuid + '"]');
- btnEl.toggleClass('new', state);
-
- if (!silent) {
- hooks.fire('action:taskbar.toggleNew', uuid);
- }
- };
-
- taskbar.updateActive = function (uuid) {
- const tasks = taskbar.tasklist.find('li');
- tasks.removeClass('active');
- tasks.filter('[data-uuid="' + uuid + '"]').addClass('active');
-
- $('[data-uuid]:not([data-module])').toggleClass('modal-unfocused', true);
- $('[data-uuid="' + uuid + '"]:not([data-module])').toggleClass('modal-unfocused', false);
- };
-
- taskbar.isActive = function (uuid) {
- const taskBtn = taskbar.tasklist.find('li[data-uuid="' + uuid + '"]');
- return taskBtn.hasClass('active');
- };
-
- function update() {
- const tasks = taskbar.tasklist.find('li');
-
- if (tasks.length > 0) {
- taskbar.taskbar.attr('data-active', '1');
- } else {
- taskbar.taskbar.removeAttr('data-active');
- }
- }
-
- function minimizeAll() {
- taskbar.tasklist.find('.active').removeClass('active');
- }
-
- function createTaskbarItem(data, callback) {
- translator.translate(data.options.title, function (taskTitle) {
- const title = $('').text(taskTitle || 'NodeBB Task').html();
-
- const taskbarEl = $('')
- .addClass(data.options.className)
- .html('
' +
- (data.options.icon ? ' ' : '') +
- '' + title + '' +
- '')
- .attr({
- title: title,
- 'data-module': data.module,
- 'data-uuid': data.uuid,
- })
- .addClass(data.options.state !== undefined ? data.options.state : 'active');
-
- if (!data.options.state || data.options.state === 'active') {
- minimizeAll();
- }
-
- taskbar.tasklist.append(taskbarEl);
- update();
-
- data.element = taskbarEl;
-
- taskbarEl.data(data);
- hooks.fire('action:taskbar.pushed', data);
- callback(taskbarEl);
- });
- }
-
- const processUpdate = function (element, key, value) {
- switch (key) {
- case 'title':
- element.find('[component="taskbar/title"]').text(value);
- break;
- case 'icon':
- element.find('i').attr('class', 'fa fa-' + value);
- break;
- case 'image':
- element.find('a').css('background-image', value ? 'url("' + value.replace(///g, '/') + '")' : '');
- break;
- case 'background-color':
- element.find('a').css('background-color', value);
- break;
- }
- };
-
- taskbar.update = function (module, uuid, options) {
- const element = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]');
- if (!element.length) {
- return;
- }
- const data = element.data();
-
- Object.keys(options).forEach(function (key) {
- data[key] = options[key];
- processUpdate(element, key, options[key]);
- });
-
- element.data(data);
- };
-
- return taskbar;
+define('taskbar', ['benchpress', 'translator', 'hooks'], (Benchpress, translator, hooks) => {
+ const taskbar = {};
+
+ taskbar.init = function () {
+ const self = this;
+
+ Benchpress.render('modules/taskbar', {}).then(html => {
+ self.taskbar = $(html);
+ self.tasklist = self.taskbar.find('ul');
+ $(document.body).append(self.taskbar);
+
+ self.taskbar.on('click', 'li', async function () {
+ const $button = $(this);
+ const moduleName = $button.attr('data-module');
+ const uuid = $button.attr('data-uuid');
+
+ const module = await app.require(moduleName);
+ if ($button.hasClass('active')) {
+ module.minimize(uuid);
+ } else {
+ minimizeAll();
+ module.load(uuid);
+ taskbar.toggleNew(uuid, false);
+
+ taskbar.tasklist.removeClass('active');
+ $button.addClass('active');
+ }
+
+ return false;
+ });
+ });
+
+ $(window).on('action:app.loggedOut', () => {
+ taskbar.closeAll();
+ });
+ };
+
+ taskbar.close = async function (moduleName, uuid) {
+ // Sends signal to the appropriate module's .close() fn (if present)
+ const buttonElement = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]');
+ let functionName = 'close';
+
+ // TODO: Refactor chat module to not take uuid in close instead of by jQuery element
+ if (moduleName === 'chat') {
+ functionName = 'closeByUUID';
+ }
+
+ if (buttonElement.length > 0) {
+ const module = await app.require(moduleName);
+ if (module && typeof module[functionName] === 'function') {
+ module[functionName](uuid);
+ }
+ }
+ };
+
+ taskbar.closeAll = function (module) {
+ // Module is optional
+ let selector = '[data-uuid]';
+
+ if (module) {
+ selector = '[data-module="' + module + '"]' + selector;
+ }
+
+ taskbar.tasklist.find(selector).each((index, element) => {
+ taskbar.close(module || element.dataset.module, element.dataset.uuid);
+ });
+ };
+
+ taskbar.discard = function (module, uuid) {
+ const buttonElement = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]');
+ buttonElement.remove();
+
+ update();
+ };
+
+ taskbar.push = function (module, uuid, options, callback) {
+ callback ||= function () {};
+ const element = taskbar.tasklist.find('li[data-uuid="' + uuid + '"]');
+
+ const data = {
+ module,
+ uuid,
+ options,
+ element,
+ };
+
+ hooks.fire('filter:taskbar.push', data);
+
+ if (element.length === 0 && data.module) {
+ createTaskbarItem(data, callback);
+ } else {
+ callback(element);
+ }
+ };
+
+ taskbar.get = function (module) {
+ const items = $('[data-module="' + module + '"]').map((index, element) => $(element).data());
+
+ return items;
+ };
+
+ taskbar.minimize = function (module, uuid) {
+ const buttonElement = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]');
+ buttonElement.toggleClass('active', false);
+ };
+
+ taskbar.toggleNew = function (uuid, state, silent) {
+ const buttonElement = taskbar.tasklist.find('[data-uuid="' + uuid + '"]');
+ buttonElement.toggleClass('new', state);
+
+ if (!silent) {
+ hooks.fire('action:taskbar.toggleNew', uuid);
+ }
+ };
+
+ taskbar.updateActive = function (uuid) {
+ const tasks = taskbar.tasklist.find('li');
+ tasks.removeClass('active');
+ tasks.filter('[data-uuid="' + uuid + '"]').addClass('active');
+
+ $('[data-uuid]:not([data-module])').toggleClass('modal-unfocused', true);
+ $('[data-uuid="' + uuid + '"]:not([data-module])').toggleClass('modal-unfocused', false);
+ };
+
+ taskbar.isActive = function (uuid) {
+ const taskButton = taskbar.tasklist.find('li[data-uuid="' + uuid + '"]');
+ return taskButton.hasClass('active');
+ };
+
+ function update() {
+ const tasks = taskbar.tasklist.find('li');
+
+ if (tasks.length > 0) {
+ taskbar.taskbar.attr('data-active', '1');
+ } else {
+ taskbar.taskbar.removeAttr('data-active');
+ }
+ }
+
+ function minimizeAll() {
+ taskbar.tasklist.find('.active').removeClass('active');
+ }
+
+ function createTaskbarItem(data, callback) {
+ translator.translate(data.options.title, taskTitle => {
+ const title = $('
').text(taskTitle || 'NodeBB Task').html();
+
+ const taskbarElement = $('
')
+ .addClass(data.options.className)
+ .html('
'
+ + (data.options.icon ? ' ' : '')
+ + '' + title + ''
+ + '')
+ .attr({
+ title,
+ 'data-module': data.module,
+ 'data-uuid': data.uuid,
+ })
+ .addClass(data.options.state === undefined ? 'active' : data.options.state);
+
+ if (!data.options.state || data.options.state === 'active') {
+ minimizeAll();
+ }
+
+ taskbar.tasklist.append(taskbarElement);
+ update();
+
+ data.element = taskbarElement;
+
+ taskbarElement.data(data);
+ hooks.fire('action:taskbar.pushed', data);
+ callback(taskbarElement);
+ });
+ }
+
+ const processUpdate = function (element, key, value) {
+ switch (key) {
+ case 'title': {
+ element.find('[component="taskbar/title"]').text(value);
+ break;
+ }
+
+ case 'icon': {
+ element.find('i').attr('class', 'fa fa-' + value);
+ break;
+ }
+
+ case 'image': {
+ element.find('a').css('background-image', value ? 'url("' + value.replaceAll('/', '/') + '")' : '');
+ break;
+ }
+
+ case 'background-color': {
+ element.find('a').css('background-color', value);
+ break;
+ }
+ }
+ };
+
+ taskbar.update = function (module, uuid, options) {
+ const element = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]');
+ if (element.length === 0) {
+ return;
+ }
+
+ const data = element.data();
+
+ for (const key of Object.keys(options)) {
+ data[key] = options[key];
+ processUpdate(element, key, options[key]);
+ }
+
+ element.data(data);
+ };
+
+ return taskbar;
});
diff --git a/public/src/modules/topicList.js b/public/src/modules/topicList.js
index 6342969..4d05de5 100644
--- a/public/src/modules/topicList.js
+++ b/public/src/modules/topicList.js
@@ -1,279 +1,278 @@
'use strict';
define('topicList', [
- 'forum/infinitescroll',
- 'handleBack',
- 'topicSelect',
- 'categoryFilter',
- 'forum/category/tools',
- 'hooks',
-], function (infinitescroll, handleBack, topicSelect, categoryFilter, categoryTools, hooks) {
- const TopicList = {};
- let templateName = '';
-
- let newTopicCount = 0;
- let newPostCount = 0;
-
- let loadTopicsCallback;
- let topicListEl;
-
- const scheduledTopics = [];
-
- $(window).on('action:ajaxify.start', function () {
- TopicList.removeListeners();
- categoryTools.removeListeners();
- });
-
- TopicList.init = function (template, cb) {
- topicListEl = findTopicListElement();
-
- templateName = template;
- loadTopicsCallback = cb || loadTopicsAfter;
-
- categoryTools.init();
-
- TopicList.watchForNewPosts();
- const states = ['watching'];
- if (ajaxify.data.selectedFilter && ajaxify.data.selectedFilter.filter === 'watched') {
- states.push('notwatching', 'ignoring');
- } else if (template !== 'unread') {
- states.push('notwatching');
- }
-
- categoryFilter.init($('[component="category/dropdown"]'), {
- states: states,
- });
-
- if (!config.usePagination) {
- infinitescroll.init(TopicList.loadMoreTopics);
- }
-
- handleBack.init(function (after, handleBackCallback) {
- loadTopicsCallback(after, 1, function (data, loadCallback) {
- onTopicsLoaded(templateName, data.topics, ajaxify.data.showSelect, 1, function () {
- handleBackCallback();
- loadCallback();
- });
- });
- });
-
- if ($('body').height() <= $(window).height() && topicListEl.children().length >= 20) {
- $('#load-more-btn').show();
- }
-
- $('#load-more-btn').on('click', function () {
- TopicList.loadMoreTopics(1);
- });
-
- hooks.fire('action:topics.loaded', { topics: ajaxify.data.topics });
- };
-
- function findTopicListElement() {
- return $('[component="category"]').filter(function (i, e) {
- return !$(e).parents('[widget-area],[data-widget-area]').length;
- });
- }
-
- TopicList.watchForNewPosts = function () {
- $('#new-topics-alert').on('click', function () {
- $(this).addClass('hide');
- });
- newPostCount = 0;
- newTopicCount = 0;
- TopicList.removeListeners();
- socket.on('event:new_topic', onNewTopic);
- socket.on('event:new_post', onNewPost);
- };
-
- TopicList.removeListeners = function () {
- socket.removeListener('event:new_topic', onNewTopic);
- socket.removeListener('event:new_post', onNewPost);
- };
-
- function onNewTopic(data) {
- const d = ajaxify.data;
-
- const categories = d.selectedCids &&
- d.selectedCids.length &&
- d.selectedCids.indexOf(parseInt(data.cid, 10)) === -1;
- const filterWatched = d.selectedFilter &&
- d.selectedFilter.filter === 'watched';
- const category = d.template.category &&
- parseInt(d.cid, 10) !== parseInt(data.cid, 10);
-
- const preventAlert = !!(categories || filterWatched || category || scheduledTopics.includes(data.tid));
- hooks.fire('filter:topicList.onNewTopic', { topic: data, preventAlert }).then((result) => {
- if (result.preventAlert) {
- return;
- }
-
- if (data.scheduled && data.tid) {
- scheduledTopics.push(data.tid);
- }
- newTopicCount += 1;
- updateAlertText();
- });
- }
-
- function onNewPost(data) {
- const post = data.posts[0];
- if (!post || !post.topic || post.topic.isFollowing) {
- return;
- }
-
- const d = ajaxify.data;
-
- const isMain = parseInt(post.topic.mainPid, 10) === parseInt(post.pid, 10);
- const categories = d.selectedCids &&
- d.selectedCids.length &&
- d.selectedCids.indexOf(parseInt(post.topic.cid, 10)) === -1;
- const filterNew = d.selectedFilter &&
- d.selectedFilter.filter === 'new';
- const filterWatched = d.selectedFilter &&
- d.selectedFilter.filter === 'watched' &&
- !post.topic.isFollowing;
- const category = d.template.category &&
- parseInt(d.cid, 10) !== parseInt(post.topic.cid, 10);
-
- const preventAlert = !!(isMain || categories || filterNew || filterWatched || category);
- hooks.fire('filter:topicList.onNewPost', { post, preventAlert }).then((result) => {
- if (result.preventAlert) {
- return;
- }
-
- newPostCount += 1;
- updateAlertText();
- });
- }
-
- function updateAlertText() {
- let text = '';
-
- if (newTopicCount === 0) {
- if (newPostCount === 1) {
- text = '[[recent:there-is-a-new-post]]';
- } else if (newPostCount > 1) {
- text = '[[recent:there-are-new-posts, ' + newPostCount + ']]';
- }
- } else if (newTopicCount === 1) {
- if (newPostCount === 0) {
- text = '[[recent:there-is-a-new-topic]]';
- } else if (newPostCount === 1) {
- text = '[[recent:there-is-a-new-topic-and-a-new-post]]';
- } else if (newPostCount > 1) {
- text = '[[recent:there-is-a-new-topic-and-new-posts, ' + newPostCount + ']]';
- }
- } else if (newTopicCount > 1) {
- if (newPostCount === 0) {
- text = '[[recent:there-are-new-topics, ' + newTopicCount + ']]';
- } else if (newPostCount === 1) {
- text = '[[recent:there-are-new-topics-and-a-new-post, ' + newTopicCount + ']]';
- } else if (newPostCount > 1) {
- text = '[[recent:there-are-new-topics-and-new-posts, ' + newTopicCount + ', ' + newPostCount + ']]';
- }
- }
-
- text += ' [[recent:click-here-to-reload]]';
-
- $('#new-topics-alert').translateText(text).removeClass('hide').fadeIn('slow');
- $('#category-no-topics').addClass('hide');
- }
-
- TopicList.loadMoreTopics = function (direction) {
- if (!topicListEl.length || !topicListEl.children().length) {
- return;
- }
- const topics = topicListEl.find('[component="category/topic"]');
- const afterEl = direction > 0 ? topics.last() : topics.first();
- const after = (parseInt(afterEl.attr('data-index'), 10) || 0) + (direction > 0 ? 1 : 0);
-
- if (!utils.isNumber(after) || (after === 0 && topicListEl.find('[component="category/topic"][data-index="0"]').length)) {
- return;
- }
-
- loadTopicsCallback(after, direction, function (data, done) {
- onTopicsLoaded(templateName, data.topics, ajaxify.data.showSelect, direction, done);
- });
- };
-
- function calculateNextPage(after, direction) {
- return Math.floor(after / config.topicsPerPage) + (direction > 0 ? 1 : 0);
- }
-
- function loadTopicsAfter(after, direction, callback) {
- callback = callback || function () {};
- const query = utils.params();
- query.page = calculateNextPage(after, direction);
- infinitescroll.loadMoreXhr(query, callback);
- }
-
- function filterTopicsOnDom(topics) {
- return topics.filter(function (topic) {
- return !topicListEl.find('[component="category/topic"][data-tid="' + topic.tid + '"]').length;
- });
- }
-
- function onTopicsLoaded(templateName, topics, showSelect, direction, callback) {
- if (!topics || !topics.length) {
- $('#load-more-btn').hide();
- return callback();
- }
- topics = filterTopicsOnDom(topics);
-
- if (!topics.length) {
- $('#load-more-btn').hide();
- return callback();
- }
-
- let after;
- let before;
- const topicEls = topicListEl.find('[component="category/topic"]');
-
- if (direction > 0 && topics.length) {
- after = topicEls.last();
- } else if (direction < 0 && topics.length) {
- before = topicEls.first();
- }
-
- const tplData = {
- topics: topics,
- showSelect: showSelect,
- template: {
- name: templateName,
- },
- };
- tplData.template[templateName] = true;
-
- hooks.fire('action:topics.loading', { topics: topics, after: after, before: before });
-
- app.parseAndTranslate(templateName, 'topics', tplData, function (html) {
- topicListEl.removeClass('hidden');
- $('#category-no-topics').remove();
-
- if (after && after.length) {
- html.insertAfter(after);
- } else if (before && before.length) {
- const height = $(document).height();
- const scrollTop = $(window).scrollTop();
-
- html.insertBefore(before);
-
- $(window).scrollTop(scrollTop + ($(document).height() - height));
- } else {
- topicListEl.append(html);
- }
-
- if (!topicSelect.getSelectedTids().length) {
- infinitescroll.removeExtra(topicListEl.find('[component="category/topic"]'), direction, Math.max(60, config.topicsPerPage * 3));
- }
-
- html.find('.timeago').timeago();
- app.createUserTooltips(html);
- utils.makeNumbersHumanReadable(html.find('.human-readable-number'));
- hooks.fire('action:topics.loaded', { topics: topics, template: templateName });
- callback();
- });
- }
-
- return TopicList;
+ 'forum/infinitescroll',
+ 'handleBack',
+ 'topicSelect',
+ 'categoryFilter',
+ 'forum/category/tools',
+ 'hooks',
+], (infinitescroll, handleBack, topicSelect, categoryFilter, categoryTools, hooks) => {
+ const TopicList = {};
+ let templateName = '';
+
+ let newTopicCount = 0;
+ let newPostCount = 0;
+
+ let loadTopicsCallback;
+ let topicListElement;
+
+ const scheduledTopics = [];
+
+ $(window).on('action:ajaxify.start', () => {
+ TopicList.removeListeners();
+ categoryTools.removeListeners();
+ });
+
+ TopicList.init = function (template, callback) {
+ topicListElement = findTopicListElement();
+
+ templateName = template;
+ loadTopicsCallback = callback || loadTopicsAfter;
+
+ categoryTools.init();
+
+ TopicList.watchForNewPosts();
+ const states = ['watching'];
+ if (ajaxify.data.selectedFilter && ajaxify.data.selectedFilter.filter === 'watched') {
+ states.push('notwatching', 'ignoring');
+ } else if (template !== 'unread') {
+ states.push('notwatching');
+ }
+
+ categoryFilter.init($('[component="category/dropdown"]'), {
+ states,
+ });
+
+ if (!config.usePagination) {
+ infinitescroll.init(TopicList.loadMoreTopics);
+ }
+
+ handleBack.init((after, handleBackCallback) => {
+ loadTopicsCallback(after, 1, (data, loadCallback) => {
+ onTopicsLoaded(templateName, data.topics, ajaxify.data.showSelect, 1, () => {
+ handleBackCallback();
+ loadCallback();
+ });
+ });
+ });
+
+ if ($('body').height() <= $(window).height() && topicListElement.children().length >= 20) {
+ $('#load-more-btn').show();
+ }
+
+ $('#load-more-btn').on('click', () => {
+ TopicList.loadMoreTopics(1);
+ });
+
+ hooks.fire('action:topics.loaded', {topics: ajaxify.data.topics});
+ };
+
+ function findTopicListElement() {
+ return $('[component="category"]').filter((i, e) => $(e).parents('[widget-area],[data-widget-area]').length === 0);
+ }
+
+ TopicList.watchForNewPosts = function () {
+ $('#new-topics-alert').on('click', function () {
+ $(this).addClass('hide');
+ });
+ newPostCount = 0;
+ newTopicCount = 0;
+ TopicList.removeListeners();
+ socket.on('event:new_topic', onNewTopic);
+ socket.on('event:new_post', onNewPost);
+ };
+
+ TopicList.removeListeners = function () {
+ socket.removeListener('event:new_topic', onNewTopic);
+ socket.removeListener('event:new_post', onNewPost);
+ };
+
+ function onNewTopic(data) {
+ const d = ajaxify.data;
+
+ const categories = d.selectedCids
+ && d.selectedCids.length
+ && !d.selectedCids.includes(Number.parseInt(data.cid, 10));
+ const filterWatched = d.selectedFilter
+ && d.selectedFilter.filter === 'watched';
+ const category = d.template.category
+ && Number.parseInt(d.cid, 10) !== Number.parseInt(data.cid, 10);
+
+ const preventAlert = Boolean(categories || filterWatched || category || scheduledTopics.includes(data.tid));
+ hooks.fire('filter:topicList.onNewTopic', {topic: data, preventAlert}).then(result => {
+ if (result.preventAlert) {
+ return;
+ }
+
+ if (data.scheduled && data.tid) {
+ scheduledTopics.push(data.tid);
+ }
+
+ newTopicCount += 1;
+ updateAlertText();
+ });
+ }
+
+ function onNewPost(data) {
+ const post = data.posts[0];
+ if (!post || !post.topic || post.topic.isFollowing) {
+ return;
+ }
+
+ const d = ajaxify.data;
+
+ const isMain = Number.parseInt(post.topic.mainPid, 10) === Number.parseInt(post.pid, 10);
+ const categories = d.selectedCids
+ && d.selectedCids.length
+ && !d.selectedCids.includes(Number.parseInt(post.topic.cid, 10));
+ const filterNew = d.selectedFilter
+ && d.selectedFilter.filter === 'new';
+ const filterWatched = d.selectedFilter
+ && d.selectedFilter.filter === 'watched'
+ && !post.topic.isFollowing;
+ const category = d.template.category
+ && Number.parseInt(d.cid, 10) !== Number.parseInt(post.topic.cid, 10);
+
+ const preventAlert = Boolean(isMain || categories || filterNew || filterWatched || category);
+ hooks.fire('filter:topicList.onNewPost', {post, preventAlert}).then(result => {
+ if (result.preventAlert) {
+ return;
+ }
+
+ newPostCount += 1;
+ updateAlertText();
+ });
+ }
+
+ function updateAlertText() {
+ let text = '';
+
+ if (newTopicCount === 0) {
+ if (newPostCount === 1) {
+ text = '[[recent:there-is-a-new-post]]';
+ } else if (newPostCount > 1) {
+ text = '[[recent:there-are-new-posts, ' + newPostCount + ']]';
+ }
+ } else if (newTopicCount === 1) {
+ if (newPostCount === 0) {
+ text = '[[recent:there-is-a-new-topic]]';
+ } else if (newPostCount === 1) {
+ text = '[[recent:there-is-a-new-topic-and-a-new-post]]';
+ } else if (newPostCount > 1) {
+ text = '[[recent:there-is-a-new-topic-and-new-posts, ' + newPostCount + ']]';
+ }
+ } else if (newTopicCount > 1) {
+ if (newPostCount === 0) {
+ text = '[[recent:there-are-new-topics, ' + newTopicCount + ']]';
+ } else if (newPostCount === 1) {
+ text = '[[recent:there-are-new-topics-and-a-new-post, ' + newTopicCount + ']]';
+ } else if (newPostCount > 1) {
+ text = '[[recent:there-are-new-topics-and-new-posts, ' + newTopicCount + ', ' + newPostCount + ']]';
+ }
+ }
+
+ text += ' [[recent:click-here-to-reload]]';
+
+ $('#new-topics-alert').translateText(text).removeClass('hide').fadeIn('slow');
+ $('#category-no-topics').addClass('hide');
+ }
+
+ TopicList.loadMoreTopics = function (direction) {
+ if (topicListElement.length === 0 || topicListElement.children().length === 0) {
+ return;
+ }
+
+ const topics = topicListElement.find('[component="category/topic"]');
+ const afterElement = direction > 0 ? topics.last() : topics.first();
+ const after = (Number.parseInt(afterElement.attr('data-index'), 10) || 0) + (direction > 0 ? 1 : 0);
+
+ if (!utils.isNumber(after) || (after === 0 && topicListElement.find('[component="category/topic"][data-index="0"]').length > 0)) {
+ return;
+ }
+
+ loadTopicsCallback(after, direction, (data, done) => {
+ onTopicsLoaded(templateName, data.topics, ajaxify.data.showSelect, direction, done);
+ });
+ };
+
+ function calculateNextPage(after, direction) {
+ return Math.floor(after / config.topicsPerPage) + (direction > 0 ? 1 : 0);
+ }
+
+ function loadTopicsAfter(after, direction, callback) {
+ callback ||= function () {};
+ const query = utils.params();
+ query.page = calculateNextPage(after, direction);
+ infinitescroll.loadMoreXhr(query, callback);
+ }
+
+ function filterTopicsOnDom(topics) {
+ return topics.filter(topic => topicListElement.find('[component="category/topic"][data-tid="' + topic.tid + '"]').length === 0);
+ }
+
+ function onTopicsLoaded(templateName, topics, showSelect, direction, callback) {
+ if (!topics || topics.length === 0) {
+ $('#load-more-btn').hide();
+ return callback();
+ }
+
+ topics = filterTopicsOnDom(topics);
+
+ if (topics.length === 0) {
+ $('#load-more-btn').hide();
+ return callback();
+ }
+
+ let after;
+ let before;
+ const topicEls = topicListElement.find('[component="category/topic"]');
+
+ if (direction > 0 && topics.length > 0) {
+ after = topicEls.last();
+ } else if (direction < 0 && topics.length > 0) {
+ before = topicEls.first();
+ }
+
+ const tplData = {
+ topics,
+ showSelect,
+ template: {
+ name: templateName,
+ },
+ };
+ tplData.template[templateName] = true;
+
+ hooks.fire('action:topics.loading', {topics, after, before});
+
+ app.parseAndTranslate(templateName, 'topics', tplData, html => {
+ topicListElement.removeClass('hidden');
+ $('#category-no-topics').remove();
+
+ if (after && after.length > 0) {
+ html.insertAfter(after);
+ } else if (before && before.length > 0) {
+ const height = $(document).height();
+ const scrollTop = $(window).scrollTop();
+
+ html.insertBefore(before);
+
+ $(window).scrollTop(scrollTop + ($(document).height() - height));
+ } else {
+ topicListElement.append(html);
+ }
+
+ if (topicSelect.getSelectedTids().length === 0) {
+ infinitescroll.removeExtra(topicListElement.find('[component="category/topic"]'), direction, Math.max(60, config.topicsPerPage * 3));
+ }
+
+ html.find('.timeago').timeago();
+ app.createUserTooltips(html);
+ utils.makeNumbersHumanReadable(html.find('.human-readable-number'));
+ hooks.fire('action:topics.loaded', {topics, template: templateName});
+ callback();
+ });
+ }
+
+ return TopicList;
});
diff --git a/public/src/modules/topicSelect.js b/public/src/modules/topicSelect.js
index 2767fea..45cdde1 100644
--- a/public/src/modules/topicSelect.js
+++ b/public/src/modules/topicSelect.js
@@ -1,88 +1,86 @@
'use strict';
-
-define('topicSelect', ['components'], function (components) {
- const TopicSelect = {};
- let lastSelected;
-
- let topicsContainer;
-
- TopicSelect.init = function (onSelect) {
- topicsContainer = $('[component="category"]');
- topicsContainer.on('selectstart', '[component="topic/select"]', function (ev) {
- ev.preventDefault();
- });
-
- topicsContainer.on('click', '[component="topic/select"]', function (ev) {
- const select = $(this);
-
- if (ev.shiftKey) {
- selectRange($(this).parents('[component="category/topic"]').attr('data-tid'));
- lastSelected = select;
- return false;
- }
-
- const isSelected = select.parents('[data-tid]').hasClass('selected');
- toggleSelect(select, !isSelected);
- lastSelected = select;
- if (typeof onSelect === 'function') {
- onSelect();
- }
- });
- };
-
- function toggleSelect(select, isSelected) {
- select.toggleClass('fa-check-square-o', isSelected);
- select.toggleClass('fa-square-o', !isSelected);
- select.parents('[component="category/topic"]').toggleClass('selected', isSelected);
- }
-
- TopicSelect.getSelectedTids = function () {
- const tids = [];
- if (!topicsContainer) {
- return tids;
- }
- topicsContainer.find('[component="category/topic"].selected').each(function () {
- tids.push($(this).attr('data-tid'));
- });
- return tids;
- };
-
- TopicSelect.unselectAll = function () {
- if (topicsContainer) {
- topicsContainer.find('[component="category/topic"].selected').removeClass('selected');
- topicsContainer.find('[component="topic/select"]').toggleClass('fa-check-square-o', false).toggleClass('fa-square-o', true);
- }
- };
-
- function selectRange(clickedTid) {
- if (!lastSelected) {
- lastSelected = $('[component="category/topic"]').first().find('[component="topic/select"]');
- }
-
- const isClickedSelected = components.get('category/topic', 'tid', clickedTid).hasClass('selected');
-
- const clickedIndex = getIndex(clickedTid);
- const lastIndex = getIndex(lastSelected.parents('[component="category/topic"]').attr('data-tid'));
- selectIndexRange(clickedIndex, lastIndex, !isClickedSelected);
- }
-
- function selectIndexRange(start, end, isSelected) {
- if (start > end) {
- const tmp = start;
- start = end;
- end = tmp;
- }
-
- for (let i = start; i <= end; i += 1) {
- const topic = $('[component="category/topic"]').eq(i);
- toggleSelect(topic.find('[component="topic/select"]'), isSelected);
- }
- }
-
- function getIndex(tid) {
- return components.get('category/topic', 'tid', tid).index('[component="category/topic"]');
- }
-
- return TopicSelect;
+define('topicSelect', ['components'], components => {
+ const TopicSelect = {};
+ let lastSelected;
+
+ let topicsContainer;
+
+ TopicSelect.init = function (onSelect) {
+ topicsContainer = $('[component="category"]');
+ topicsContainer.on('selectstart', '[component="topic/select"]', event => {
+ event.preventDefault();
+ });
+
+ topicsContainer.on('click', '[component="topic/select"]', function (event) {
+ const select = $(this);
+
+ if (event.shiftKey) {
+ selectRange($(this).parents('[component="category/topic"]').attr('data-tid'));
+ lastSelected = select;
+ return false;
+ }
+
+ const isSelected = select.parents('[data-tid]').hasClass('selected');
+ toggleSelect(select, !isSelected);
+ lastSelected = select;
+ if (typeof onSelect === 'function') {
+ onSelect();
+ }
+ });
+ };
+
+ function toggleSelect(select, isSelected) {
+ select.toggleClass('fa-check-square-o', isSelected);
+ select.toggleClass('fa-square-o', !isSelected);
+ select.parents('[component="category/topic"]').toggleClass('selected', isSelected);
+ }
+
+ TopicSelect.getSelectedTids = function () {
+ const tids = [];
+ if (!topicsContainer) {
+ return tids;
+ }
+
+ topicsContainer.find('[component="category/topic"].selected').each(function () {
+ tids.push($(this).attr('data-tid'));
+ });
+ return tids;
+ };
+
+ TopicSelect.unselectAll = function () {
+ if (topicsContainer) {
+ topicsContainer.find('[component="category/topic"].selected').removeClass('selected');
+ topicsContainer.find('[component="topic/select"]').toggleClass('fa-check-square-o', false).toggleClass('fa-square-o', true);
+ }
+ };
+
+ function selectRange(clickedTid) {
+ lastSelected ||= $('[component="category/topic"]').first().find('[component="topic/select"]');
+
+ const isClickedSelected = components.get('category/topic', 'tid', clickedTid).hasClass('selected');
+
+ const clickedIndex = getIndex(clickedTid);
+ const lastIndex = getIndex(lastSelected.parents('[component="category/topic"]').attr('data-tid'));
+ selectIndexRange(clickedIndex, lastIndex, !isClickedSelected);
+ }
+
+ function selectIndexRange(start, end, isSelected) {
+ if (start > end) {
+ const temporary = start;
+ start = end;
+ end = temporary;
+ }
+
+ for (let i = start; i <= end; i += 1) {
+ const topic = $('[component="category/topic"]').eq(i);
+ toggleSelect(topic.find('[component="topic/select"]'), isSelected);
+ }
+ }
+
+ function getIndex(tid) {
+ return components.get('category/topic', 'tid', tid).index('[component="category/topic"]');
+ }
+
+ return TopicSelect;
});
diff --git a/public/src/modules/topicThumbs.js b/public/src/modules/topicThumbs.js
index 761bf5d..ab78786 100644
--- a/public/src/modules/topicThumbs.js
+++ b/public/src/modules/topicThumbs.js
@@ -1,130 +1,130 @@
'use strict';
define('topicThumbs', [
- 'api', 'bootbox', 'alerts', 'uploader', 'benchpress', 'translator', 'jquery-ui/widgets/sortable',
-], function (api, bootbox, alerts, uploader, Benchpress, translator) {
- const Thumbs = {};
-
- Thumbs.get = id => api.get(`/topics/${id}/thumbs`, {});
-
- Thumbs.getByPid = pid => api.get(`/posts/${pid}`, {}).then(post => Thumbs.get(post.tid));
-
- Thumbs.delete = (id, path) => api.del(`/topics/${id}/thumbs`, {
- path: path,
- });
-
- Thumbs.deleteAll = (id) => {
- Thumbs.get(id).then((thumbs) => {
- Promise.all(thumbs.map(thumb => Thumbs.delete(id, thumb.url)));
- });
- };
-
- Thumbs.upload = id => new Promise((resolve) => {
- uploader.show({
- title: '[[topic:composer.thumb_title]]',
- method: 'put',
- route: config.relative_path + `/api/v3/topics/${id}/thumbs`,
- }, function (url) {
- resolve(url);
- });
- });
-
- Thumbs.modal = {};
-
- Thumbs.modal.open = function (payload) {
- const { id, pid } = payload;
- let { modal } = payload;
- let numThumbs;
-
- return new Promise((resolve) => {
- Promise.all([
- Thumbs.get(id),
- pid ? Thumbs.getByPid(pid) : [],
- ]).then(results => new Promise((resolve) => {
- const thumbs = results.reduce((memo, cur) => memo.concat(cur));
- numThumbs = thumbs.length;
-
- resolve(thumbs);
- })).then(thumbs => Benchpress.render('modals/topic-thumbs', { thumbs })).then((html) => {
- if (modal) {
- translator.translate(html, function (translated) {
- modal.find('.bootbox-body').html(translated);
- Thumbs.modal.handleSort({ modal, numThumbs });
- });
- } else {
- modal = bootbox.dialog({
- title: '[[modules:thumbs.modal.title]]',
- message: html,
- buttons: {
- add: {
- label: '
[[modules:thumbs.modal.add]]',
- className: 'btn-success',
- callback: () => {
- Thumbs.upload(id).then(() => {
- Thumbs.modal.open({ ...payload, modal });
- require(['composer'], (composer) => {
- composer.updateThumbCount(id, $(`[component="composer"][data-uuid="${id}"]`));
- resolve();
- });
- });
- return false;
- },
- },
- close: {
- label: '[[global:close]]',
- className: 'btn-primary',
- },
- },
- });
- Thumbs.modal.handleDelete({ ...payload, modal });
- Thumbs.modal.handleSort({ modal, numThumbs });
- }
- });
- });
- };
-
- Thumbs.modal.handleDelete = (payload) => {
- const modalEl = payload.modal.get(0);
-
- modalEl.addEventListener('click', (ev) => {
- if (ev.target.closest('button[data-action="remove"]')) {
- bootbox.confirm('[[modules:thumbs.modal.confirm-remove]]', (ok) => {
- if (!ok) {
- return;
- }
-
- const id = ev.target.closest('.media[data-id]').getAttribute('data-id');
- const path = ev.target.closest('.media[data-path]').getAttribute('data-path');
- api.del(`/topics/${id}/thumbs`, {
- path: path,
- }).then(() => {
- Thumbs.modal.open(payload);
- }).catch(alerts.error);
- });
- }
- });
- };
-
- Thumbs.modal.handleSort = ({ modal, numThumbs }) => {
- if (numThumbs > 1) {
- const selectorEl = modal.find('.topic-thumbs-modal');
- selectorEl.sortable({
- items: '[data-id]',
- });
- selectorEl.on('sortupdate', Thumbs.modal.handleSortChange);
- }
- };
-
- Thumbs.modal.handleSortChange = (ev, ui) => {
- const items = ui.item.get(0).parentNode.querySelectorAll('[data-id]');
- Array.from(items).forEach((el, order) => {
- const id = el.getAttribute('data-id');
- let path = el.getAttribute('data-path');
- path = path.replace(new RegExp(`^${config.upload_url}`), '');
-
- api.put(`/topics/${id}/thumbs/order`, { path, order }).catch(alerts.error);
- });
- };
-
- return Thumbs;
+ 'api', 'bootbox', 'alerts', 'uploader', 'benchpress', 'translator', 'jquery-ui/widgets/sortable',
+], (api, bootbox, alerts, uploader, Benchpress, translator) => {
+ const Thumbs = {};
+
+ Thumbs.get = id => api.get(`/topics/${id}/thumbs`, {});
+
+ Thumbs.getByPid = pid => api.get(`/posts/${pid}`, {}).then(post => Thumbs.get(post.tid));
+
+ Thumbs.delete = (id, path) => api.del(`/topics/${id}/thumbs`, {
+ path,
+ });
+
+ Thumbs.deleteAll = id => {
+ Thumbs.get(id).then(thumbs => {
+ Promise.all(thumbs.map(thumb => Thumbs.delete(id, thumb.url)));
+ });
+ };
+
+ Thumbs.upload = id => new Promise(resolve => {
+ uploader.show({
+ title: '[[topic:composer.thumb_title]]',
+ method: 'put',
+ route: config.relative_path + `/api/v3/topics/${id}/thumbs`,
+ }, url => {
+ resolve(url);
+ });
+ });
+
+ Thumbs.modal = {};
+
+ Thumbs.modal.open = function (payload) {
+ const {id, pid} = payload;
+ let {modal} = payload;
+ let numberThumbs;
+
+ return new Promise(resolve => {
+ Promise.all([
+ Thumbs.get(id),
+ pid ? Thumbs.getByPid(pid) : [],
+ ]).then(results => new Promise(resolve => {
+ const thumbs = results.reduce((memo, current) => memo.concat(current));
+ numberThumbs = thumbs.length;
+
+ resolve(thumbs);
+ })).then(thumbs => Benchpress.render('modals/topic-thumbs', {thumbs})).then(html => {
+ if (modal) {
+ translator.translate(html, translated => {
+ modal.find('.bootbox-body').html(translated);
+ Thumbs.modal.handleSort({modal, numThumbs: numberThumbs});
+ });
+ } else {
+ modal = bootbox.dialog({
+ title: '[[modules:thumbs.modal.title]]',
+ message: html,
+ buttons: {
+ add: {
+ label: '
[[modules:thumbs.modal.add]]',
+ className: 'btn-success',
+ callback() {
+ Thumbs.upload(id).then(() => {
+ Thumbs.modal.open({...payload, modal});
+ require(['composer'], composer => {
+ composer.updateThumbCount(id, $(`[component="composer"][data-uuid="${id}"]`));
+ resolve();
+ });
+ });
+ return false;
+ },
+ },
+ close: {
+ label: '[[global:close]]',
+ className: 'btn-primary',
+ },
+ },
+ });
+ Thumbs.modal.handleDelete({...payload, modal});
+ Thumbs.modal.handleSort({modal, numThumbs: numberThumbs});
+ }
+ });
+ });
+ };
+
+ Thumbs.modal.handleDelete = payload => {
+ const modalElement = payload.modal.get(0);
+
+ modalElement.addEventListener('click', event => {
+ if (event.target.closest('button[data-action="remove"]')) {
+ bootbox.confirm('[[modules:thumbs.modal.confirm-remove]]', ok => {
+ if (!ok) {
+ return;
+ }
+
+ const id = event.target.closest('.media[data-id]').dataset.id;
+ const path = event.target.closest('.media[data-path]').dataset.path;
+ api.del(`/topics/${id}/thumbs`, {
+ path,
+ }).then(() => {
+ Thumbs.modal.open(payload);
+ }).catch(alerts.error);
+ });
+ }
+ });
+ };
+
+ Thumbs.modal.handleSort = ({modal, numThumbs}) => {
+ if (numThumbs > 1) {
+ const selectorElement = modal.find('.topic-thumbs-modal');
+ selectorElement.sortable({
+ items: '[data-id]',
+ });
+ selectorElement.on('sortupdate', Thumbs.modal.handleSortChange);
+ }
+ };
+
+ Thumbs.modal.handleSortChange = (event, ui) => {
+ const items = ui.item.get(0).parentNode.querySelectorAll('[data-id]');
+ for (const [order, element] of Array.from(items).entries()) {
+ const id = element.dataset.id;
+ let path = element.dataset.path;
+ path = path.replace(new RegExp(`^${config.upload_url}`), '');
+
+ api.put(`/topics/${id}/thumbs/order`, {path, order}).catch(alerts.error);
+ }
+ };
+
+ return Thumbs;
});
diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js
index c5e48c8..55462a5 100644
--- a/public/src/modules/translator.js
+++ b/public/src/modules/translator.js
@@ -2,24 +2,28 @@
const factory = require('./translator.common');
-define('translator', ['jquery', 'utils'], function (jQuery, utils) {
- function loadClient(language, namespace) {
- return new Promise(function (resolve, reject) {
- jQuery.getJSON([config.asset_base_url, 'language', language, namespace].join('/') + '.json?' + config['cache-buster'], function (data) {
- const payload = {
- language: language,
- namespace: namespace,
- data: data,
- };
- require(['hooks'], function (hooks) {
- hooks.fire('action:translator.loadClient', payload);
- resolve(payload.promise ? Promise.resolve(payload.promise) : data);
- });
- }).fail(function (jqxhr, textStatus, error) {
- reject(new Error(textStatus + ', ' + error));
- });
- });
- }
- const warn = function () { console.warn.apply(console, arguments); };
- return factory(utils, loadClient, warn);
+define('translator', ['jquery', 'utils'], (indexQuery, utils) => {
+ function loadClient(language, namespace) {
+ return new Promise((resolve, reject) => {
+ indexQuery.getJSON([config.asset_base_url, 'language', language, namespace].join('/') + '.json?' + config['cache-buster'], data => {
+ const payload = {
+ language,
+ namespace,
+ data,
+ };
+ require(['hooks'], hooks => {
+ hooks.fire('action:translator.loadClient', payload);
+ resolve(payload.promise ? Promise.resolve(payload.promise) : data);
+ });
+ }).fail((jqxhr, textStatus, error) => {
+ reject(new Error(textStatus + ', ' + error));
+ });
+ });
+ }
+
+ const warn = function () {
+ console.warn.apply(console, arguments);
+ };
+
+ return factory(utils, loadClient, warn);
});
diff --git a/public/src/modules/uploadHelpers.js b/public/src/modules/uploadHelpers.js
index e56f589..97fd6e1 100644
--- a/public/src/modules/uploadHelpers.js
+++ b/public/src/modules/uploadHelpers.js
@@ -1,199 +1,207 @@
'use strict';
-
-define('uploadHelpers', ['alerts'], function (alerts) {
- const uploadHelpers = {};
-
- uploadHelpers.init = function (options) {
- const formEl = options.uploadFormEl;
- if (!formEl.length) {
- return;
- }
- formEl.attr('action', config.relative_path + options.route);
-
- if (options.dragDropAreaEl) {
- uploadHelpers.handleDragDrop({
- container: options.dragDropAreaEl,
- callback: function (upload) {
- uploadHelpers.ajaxSubmit({
- uploadForm: formEl,
- upload: upload,
- callback: options.callback,
- });
- },
- });
- }
-
- if (options.pasteEl) {
- uploadHelpers.handlePaste({
- container: options.pasteEl,
- callback: function (upload) {
- uploadHelpers.ajaxSubmit({
- uploadForm: formEl,
- upload: upload,
- callback: options.callback,
- });
- },
- });
- }
- };
-
- uploadHelpers.handleDragDrop = function (options) {
- let draggingDocument = false;
- const postContainer = options.container;
- const drop = options.container.find('.imagedrop');
-
- postContainer.on('dragenter', function onDragEnter() {
- if (draggingDocument) {
- return;
- }
- drop.css('top', '0px');
- drop.css('height', postContainer.height() + 'px');
- drop.css('line-height', postContainer.height() + 'px');
- drop.show();
-
- drop.on('dragleave', function () {
- drop.hide();
- drop.off('dragleave');
- });
- });
-
- drop.on('drop', function onDragDrop(e) {
- e.preventDefault();
- const files = e.originalEvent.dataTransfer.files;
-
- if (files.length) {
- let formData;
- if (window.FormData) {
- formData = new FormData();
- for (var i = 0; i < files.length; ++i) {
- formData.append('files[]', files[i], files[i].name);
- }
- }
- options.callback({
- files: files,
- formData: formData,
- });
- }
-
- drop.hide();
- return false;
- });
-
- function cancel(e) {
- e.preventDefault();
- return false;
- }
-
- $(document)
- .off('dragstart')
- .on('dragstart', function () {
- draggingDocument = true;
- })
- .off('dragend')
- .on('dragend, mouseup', function () {
- draggingDocument = false;
- });
-
- drop.on('dragover', cancel);
- drop.on('dragenter', cancel);
- };
-
- uploadHelpers.handlePaste = function (options) {
- const container = options.container;
- container.on('paste', function (event) {
- const items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
- const files = [];
- const fileNames = [];
- let formData = null;
- if (window.FormData) {
- formData = new FormData();
- }
- [].forEach.call(items, function (item) {
- const file = item.getAsFile();
- if (file) {
- const fileName = utils.generateUUID() + '-' + file.name;
- if (formData) {
- formData.append('files[]', file, fileName);
- }
- files.push(file);
- fileNames.push(fileName);
- }
- });
-
- if (files.length) {
- options.callback({
- files: files,
- fileNames: fileNames,
- formData: formData,
- });
- }
- });
- };
-
- uploadHelpers.ajaxSubmit = function (options) {
- const files = [...options.upload.files];
-
- for (let i = 0; i < files.length; ++i) {
- const isImage = files[i].type.match(/image./);
- if ((isImage && !app.user.privileges['upload:post:image']) || (!isImage && !app.user.privileges['upload:post:file'])) {
- return alerts.error('[[error:no-privileges]]');
- }
- if (files[i].size > parseInt(config.maximumFileSize, 10) * 1024) {
- options.uploadForm[0].reset();
- return alerts.error('[[error:file-too-big, ' + config.maximumFileSize + ']]');
- }
- }
- const alert_id = Date.now();
- options.uploadForm.off('submit').on('submit', function () {
- $(this).ajaxSubmit({
- headers: {
- 'x-csrf-token': config.csrf_token,
- },
- resetForm: true,
- clearForm: true,
- formData: options.upload.formData,
- error: function (xhr) {
- let errorMsg = (xhr.responseJSON &&
- (xhr.responseJSON.error || (xhr.responseJSON.status && xhr.responseJSON.status.message))) ||
- '[[error:parse-error]]';
-
- if (xhr && xhr.status === 413) {
- errorMsg = xhr.statusText || 'Request Entity Too Large';
- }
- alerts.error(errorMsg);
- alerts.remove(alert_id);
- },
-
- uploadProgress: function (event, position, total, percent) {
- alerts.alert({
- alert_id: alert_id,
- message: '[[modules:composer.uploading, ' + percent + '%]]',
- });
- },
-
- success: function (res) {
- const uploads = res.response.images;
- if (uploads && uploads.length) {
- for (var i = 0; i < uploads.length; ++i) {
- uploads[i].filename = files[i].name;
- uploads[i].isImage = /image./.test(files[i].type);
- }
- }
- options.callback(uploads);
- },
-
- complete: function () {
- options.uploadForm[0].reset();
- setTimeout(alerts.remove, 100, alert_id);
- },
- });
-
- return false;
- });
-
- options.uploadForm.submit();
- };
-
- return uploadHelpers;
+define('uploadHelpers', ['alerts'], alerts => {
+ const uploadHelpers = {};
+
+ uploadHelpers.init = function (options) {
+ const formElement = options.uploadFormEl;
+ if (formElement.length === 0) {
+ return;
+ }
+
+ formElement.attr('action', config.relative_path + options.route);
+
+ if (options.dragDropAreaEl) {
+ uploadHelpers.handleDragDrop({
+ container: options.dragDropAreaEl,
+ callback(upload) {
+ uploadHelpers.ajaxSubmit({
+ uploadForm: formElement,
+ upload,
+ callback: options.callback,
+ });
+ },
+ });
+ }
+
+ if (options.pasteEl) {
+ uploadHelpers.handlePaste({
+ container: options.pasteEl,
+ callback(upload) {
+ uploadHelpers.ajaxSubmit({
+ uploadForm: formElement,
+ upload,
+ callback: options.callback,
+ });
+ },
+ });
+ }
+ };
+
+ uploadHelpers.handleDragDrop = function (options) {
+ let draggingDocument = false;
+ const postContainer = options.container;
+ const drop = options.container.find('.imagedrop');
+
+ postContainer.on('dragenter', function onDragEnter() {
+ if (draggingDocument) {
+ return;
+ }
+
+ drop.css('top', '0px');
+ drop.css('height', postContainer.height() + 'px');
+ drop.css('line-height', postContainer.height() + 'px');
+ drop.show();
+
+ drop.on('dragleave', () => {
+ drop.hide();
+ drop.off('dragleave');
+ });
+ });
+
+ drop.on('drop', function onDragDrop(e) {
+ e.preventDefault();
+ const files = e.originalEvent.dataTransfer.files;
+
+ if (files.length > 0) {
+ let formData;
+ if (window.FormData) {
+ formData = new FormData();
+ for (const file of files) {
+ formData.append('files[]', file, file.name);
+ }
+ }
+
+ options.callback({
+ files,
+ formData,
+ });
+ }
+
+ drop.hide();
+ return false;
+ });
+
+ function cancel(e) {
+ e.preventDefault();
+ return false;
+ }
+
+ $(document)
+ .off('dragstart')
+ .on('dragstart', () => {
+ draggingDocument = true;
+ })
+ .off('dragend')
+ .on('dragend, mouseup', () => {
+ draggingDocument = false;
+ });
+
+ drop.on('dragover', cancel);
+ drop.on('dragenter', cancel);
+ };
+
+ uploadHelpers.handlePaste = function (options) {
+ const container = options.container;
+ container.on('paste', event => {
+ const items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
+ const files = [];
+ const fileNames = [];
+ let formData = null;
+ if (window.FormData) {
+ formData = new FormData();
+ }
+
+ Array.prototype.forEach.call(items, item => {
+ const file = item.getAsFile();
+ if (file) {
+ const fileName = utils.generateUUID() + '-' + file.name;
+ if (formData) {
+ formData.append('files[]', file, fileName);
+ }
+
+ files.push(file);
+ fileNames.push(fileName);
+ }
+ });
+
+ if (files.length > 0) {
+ options.callback({
+ files,
+ fileNames,
+ formData,
+ });
+ }
+ });
+ };
+
+ uploadHelpers.ajaxSubmit = function (options) {
+ const files = [...options.upload.files];
+
+ for (const file of files) {
+ const isImage = file.type.match(/image./);
+ if ((isImage && !app.user.privileges['upload:post:image']) || (!isImage && !app.user.privileges['upload:post:file'])) {
+ return alerts.error('[[error:no-privileges]]');
+ }
+
+ if (file.size > Number.parseInt(config.maximumFileSize, 10) * 1024) {
+ options.uploadForm[0].reset();
+ return alerts.error('[[error:file-too-big, ' + config.maximumFileSize + ']]');
+ }
+ }
+
+ const alert_id = Date.now();
+ options.uploadForm.off('submit').on('submit', function () {
+ $(this).ajaxSubmit({
+ headers: {
+ 'x-csrf-token': config.csrf_token,
+ },
+ resetForm: true,
+ clearForm: true,
+ formData: options.upload.formData,
+ error(xhr) {
+ let errorMessage = (xhr.responseJSON
+ && (xhr.responseJSON.error || (xhr.responseJSON.status && xhr.responseJSON.status.message)))
+ || '[[error:parse-error]]';
+
+ if (xhr && xhr.status === 413) {
+ errorMessage = xhr.statusText || 'Request Entity Too Large';
+ }
+
+ alerts.error(errorMessage);
+ alerts.remove(alert_id);
+ },
+
+ uploadProgress(event, position, total, percent) {
+ alerts.alert({
+ alert_id,
+ message: '[[modules:composer.uploading, ' + percent + '%]]',
+ });
+ },
+
+ success(res) {
+ const uploads = res.response.images;
+ if (uploads && uploads.length > 0) {
+ for (const [i, upload] of uploads.entries()) {
+ upload.filename = files[i].name;
+ upload.isImage = /image./.test(files[i].type);
+ }
+ }
+
+ options.callback(uploads);
+ },
+
+ complete() {
+ options.uploadForm[0].reset();
+ setTimeout(alerts.remove, 100, alert_id);
+ },
+ });
+
+ return false;
+ });
+
+ options.uploadForm.submit();
+ };
+
+ return uploadHelpers;
});
diff --git a/public/src/modules/uploader.js b/public/src/modules/uploader.js
index 62210a2..9393443 100644
--- a/public/src/modules/uploader.js
+++ b/public/src/modules/uploader.js
@@ -1,118 +1,121 @@
'use strict';
-
-define('uploader', ['jquery-form'], function () {
- const module = {};
-
- module.show = function (data, callback) {
- const fileSize = data.hasOwnProperty('fileSize') && data.fileSize !== undefined ? parseInt(data.fileSize, 10) : false;
- app.parseAndTranslate('partials/modals/upload_file_modal', {
- showHelp: data.hasOwnProperty('showHelp') && data.showHelp !== undefined ? data.showHelp : true,
- fileSize: fileSize,
- title: data.title || '[[global:upload_file]]',
- description: data.description || '',
- button: data.button || '[[global:upload]]',
- accept: data.accept ? data.accept.replace(/,/g, ', ') : '',
- }, function (uploadModal) {
- uploadModal.modal('show');
- uploadModal.on('hidden.bs.modal', function () {
- uploadModal.remove();
- });
-
- const uploadForm = uploadModal.find('#uploadForm');
- uploadForm.attr('action', data.route);
- uploadForm.find('#params').val(JSON.stringify(data.params));
-
- uploadModal.find('#fileUploadSubmitBtn').on('click', function () {
- $(this).addClass('disabled');
- uploadForm.submit();
- });
-
- uploadForm.submit(function () {
- onSubmit(uploadModal, fileSize, callback);
- return false;
- });
- });
- };
-
- module.hideAlerts = function (modal) {
- $(modal).find('#alert-status, #alert-success, #alert-error, #upload-progress-box').addClass('hide');
- };
-
- function onSubmit(uploadModal, fileSize, callback) {
- showAlert(uploadModal, 'status', '[[uploads:uploading-file]]');
-
- uploadModal.find('#upload-progress-bar').css('width', '0%');
- uploadModal.find('#upload-progress-box').show().removeClass('hide');
-
- const fileInput = uploadModal.find('#fileInput');
- if (!fileInput.val()) {
- return showAlert(uploadModal, 'error', '[[uploads:select-file-to-upload]]');
- }
- if (!hasValidFileSize(fileInput[0], fileSize)) {
- return showAlert(uploadModal, 'error', '[[error:file-too-big, ' + fileSize + ']]');
- }
-
- module.ajaxSubmit(uploadModal, callback);
- }
-
- function showAlert(uploadModal, type, message) {
- module.hideAlerts(uploadModal);
- if (type === 'error') {
- uploadModal.find('#fileUploadSubmitBtn').removeClass('disabled');
- }
- uploadModal.find('#alert-' + type).translateText(message).removeClass('hide');
- }
-
- module.ajaxSubmit = function (uploadModal, callback) {
- const uploadForm = uploadModal.find('#uploadForm');
- uploadForm.ajaxSubmit({
- headers: {
- 'x-csrf-token': config.csrf_token,
- },
- error: function (xhr) {
- xhr = maybeParse(xhr);
- showAlert(uploadModal, 'error', xhr.responseJSON ? (xhr.responseJSON.error || xhr.statusText) : 'error uploading, code : ' + xhr.status);
- },
- uploadProgress: function (event, position, total, percent) {
- uploadModal.find('#upload-progress-bar').css('width', percent + '%');
- },
- success: function (response) {
- let images = maybeParse(response);
-
- // Appropriately handle v3 API responses
- if (response.hasOwnProperty('response') && response.hasOwnProperty('status') && response.status.code === 'ok') {
- images = response.response.images;
- }
-
- callback(images[0].url);
-
- showAlert(uploadModal, 'success', '[[uploads:upload-success]]');
- setTimeout(function () {
- module.hideAlerts(uploadModal);
- uploadModal.modal('hide');
- }, 750);
- },
- });
- };
-
- function maybeParse(response) {
- if (typeof response === 'string') {
- try {
- return $.parseJSON(response);
- } catch (e) {
- return { error: '[[error:parse-error]]' };
- }
- }
- return response;
- }
-
- function hasValidFileSize(fileElement, maxSize) {
- if (window.FileReader && maxSize) {
- return fileElement.files[0].size <= maxSize * 1000;
- }
- return true;
- }
-
- return module;
+define('uploader', ['jquery-form'], () => {
+ const module = {};
+
+ module.show = function (data, callback) {
+ const fileSize = data.hasOwnProperty('fileSize') && data.fileSize !== undefined ? Number.parseInt(data.fileSize, 10) : false;
+ app.parseAndTranslate('partials/modals/upload_file_modal', {
+ showHelp: data.hasOwnProperty('showHelp') && data.showHelp !== undefined ? data.showHelp : true,
+ fileSize,
+ title: data.title || '[[global:upload_file]]',
+ description: data.description || '',
+ button: data.button || '[[global:upload]]',
+ accept: data.accept ? data.accept.replaceAll(',', ', ') : '',
+ }, uploadModal => {
+ uploadModal.modal('show');
+ uploadModal.on('hidden.bs.modal', () => {
+ uploadModal.remove();
+ });
+
+ const uploadForm = uploadModal.find('#uploadForm');
+ uploadForm.attr('action', data.route);
+ uploadForm.find('#params').val(JSON.stringify(data.params));
+
+ uploadModal.find('#fileUploadSubmitBtn').on('click', function () {
+ $(this).addClass('disabled');
+ uploadForm.submit();
+ });
+
+ uploadForm.submit(() => {
+ onSubmit(uploadModal, fileSize, callback);
+ return false;
+ });
+ });
+ };
+
+ module.hideAlerts = function (modal) {
+ $(modal).find('#alert-status, #alert-success, #alert-error, #upload-progress-box').addClass('hide');
+ };
+
+ function onSubmit(uploadModal, fileSize, callback) {
+ showAlert(uploadModal, 'status', '[[uploads:uploading-file]]');
+
+ uploadModal.find('#upload-progress-bar').css('width', '0%');
+ uploadModal.find('#upload-progress-box').show().removeClass('hide');
+
+ const fileInput = uploadModal.find('#fileInput');
+ if (!fileInput.val()) {
+ return showAlert(uploadModal, 'error', '[[uploads:select-file-to-upload]]');
+ }
+
+ if (!hasValidFileSize(fileInput[0], fileSize)) {
+ return showAlert(uploadModal, 'error', '[[error:file-too-big, ' + fileSize + ']]');
+ }
+
+ module.ajaxSubmit(uploadModal, callback);
+ }
+
+ function showAlert(uploadModal, type, message) {
+ module.hideAlerts(uploadModal);
+ if (type === 'error') {
+ uploadModal.find('#fileUploadSubmitBtn').removeClass('disabled');
+ }
+
+ uploadModal.find('#alert-' + type).translateText(message).removeClass('hide');
+ }
+
+ module.ajaxSubmit = function (uploadModal, callback) {
+ const uploadForm = uploadModal.find('#uploadForm');
+ uploadForm.ajaxSubmit({
+ headers: {
+ 'x-csrf-token': config.csrf_token,
+ },
+ error(xhr) {
+ xhr = maybeParse(xhr);
+ showAlert(uploadModal, 'error', xhr.responseJSON ? (xhr.responseJSON.error || xhr.statusText) : 'error uploading, code : ' + xhr.status);
+ },
+ uploadProgress(event, position, total, percent) {
+ uploadModal.find('#upload-progress-bar').css('width', percent + '%');
+ },
+ success(response) {
+ let images = maybeParse(response);
+
+ // Appropriately handle v3 API responses
+ if (response.hasOwnProperty('response') && response.hasOwnProperty('status') && response.status.code === 'ok') {
+ images = response.response.images;
+ }
+
+ callback(images[0].url);
+
+ showAlert(uploadModal, 'success', '[[uploads:upload-success]]');
+ setTimeout(() => {
+ module.hideAlerts(uploadModal);
+ uploadModal.modal('hide');
+ }, 750);
+ },
+ });
+ };
+
+ function maybeParse(response) {
+ if (typeof response === 'string') {
+ try {
+ return $.parseJSON(response);
+ } catch {
+ return {error: '[[error:parse-error]]'};
+ }
+ }
+
+ return response;
+ }
+
+ function hasValidFileSize(fileElement, maxSize) {
+ if (window.FileReader && maxSize) {
+ return fileElement.files[0].size <= maxSize * 1000;
+ }
+
+ return true;
+ }
+
+ return module;
});
diff --git a/public/src/overrides.js b/public/src/overrides.js
index 3cae763..2cd8bfa 100644
--- a/public/src/overrides.js
+++ b/public/src/overrides.js
@@ -4,159 +4,161 @@ const translator = require('./modules/translator');
window.overrides = window.overrides || {};
-function translate(elements, type, str) {
- return elements.each(function () {
- var el = $(this);
- translator.translate(str, function (translated) {
- el[type](translated);
- });
- });
+function translate(elements, type, string_) {
+ return elements.each(function () {
+ const element = $(this);
+ translator.translate(string_, translated => {
+ element[type](translated);
+ });
+ });
}
if (typeof window !== 'undefined') {
- (function ($) {
- $.fn.getCursorPosition = function () {
- const el = $(this).get(0);
- let pos = 0;
- if ('selectionStart' in el) {
- pos = el.selectionStart;
- } else if ('selection' in document) {
- el.focus();
- const Sel = document.selection.createRange();
- const SelLength = document.selection.createRange().text.length;
- Sel.moveStart('character', -el.value.length);
- pos = Sel.text.length - SelLength;
- }
- return pos;
- };
-
- $.fn.selectRange = function (start, end) {
- if (!end) {
- end = start;
- }
- return this.each(function () {
- if (this.setSelectionRange) {
- this.focus();
- this.setSelectionRange(start, end);
- } else if (this.createTextRange) {
- const range = this.createTextRange();
- range.collapse(true);
- range.moveEnd('character', end);
- range.moveStart('character', start);
- range.select();
- }
- });
- };
-
- // http://stackoverflow.com/questions/511088/use-javascript-to-place-cursor-at-end-of-text-in-text-input-element
- $.fn.putCursorAtEnd = function () {
- return this.each(function () {
- $(this).focus();
-
- if (this.setSelectionRange) {
- const len = $(this).val().length * 2;
- this.setSelectionRange(len, len);
- } else {
- $(this).val($(this).val());
- }
- this.scrollTop = 999999;
- });
- };
-
- $.fn.translateHtml = function (str) {
- return translate(this, 'html', str);
- };
-
- $.fn.translateText = function (str) {
- return translate(this, 'text', str);
- };
-
- $.fn.translateVal = function (str) {
- return translate(this, 'val', str);
- };
-
- $.fn.translateAttr = function (attr, str) {
- return this.each(function () {
- const el = $(this);
- translator.translate(str, function (translated) {
- el.attr(attr, translated);
- });
- });
- };
- }(jQuery || { fn: {} }));
-
- (function () {
- // FIX FOR #1245 - https://github.com/NodeBB/NodeBB/issues/1245
- // from http://stackoverflow.com/questions/15931962/bootstrap-dropdown-disappear-with-right-click-on-firefox
- // obtain a reference to the original handler
- let _clearMenus = $._data(document, 'events').click.filter(function (el) {
- return el.namespace === 'bs.data-api.dropdown' && el.selector === undefined;
- });
-
- if (_clearMenus.length) {
- _clearMenus = _clearMenus[0].handler;
- }
-
- // disable the old listener
- $(document)
- .off('click.data-api.dropdown', _clearMenus)
- .on('click.data-api.dropdown', function (e) {
- // call the handler only when not right-click
- if (e.button !== 2) {
- _clearMenus();
- }
- });
- }());
- let timeagoFn;
- overrides.overrideTimeagoCutoff = function () {
- const cutoff = parseInt(ajaxify.data.timeagoCutoff || config.timeagoCutoff, 10);
- if (cutoff === 0) {
- $.timeago.settings.cutoff = 1;
- } else if (cutoff > 0) {
- $.timeago.settings.cutoff = 1000 * 60 * 60 * 24 * cutoff;
- }
- };
-
- overrides.overrideTimeago = function () {
- if (!timeagoFn) {
- timeagoFn = $.fn.timeago;
- }
-
- overrides.overrideTimeagoCutoff();
-
- $.timeago.settings.allowFuture = true;
- const userLang = config.userLang.replace('_', '-');
- const options = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' };
- let formatFn = function (date) {
- return date.toLocaleString(userLang, options);
- };
- try {
- if (typeof Intl !== 'undefined') {
- const dtFormat = new Intl.DateTimeFormat(userLang, options);
- formatFn = dtFormat.format;
- }
- } catch (err) {
- console.error(err);
- }
-
- let iso;
- let date;
- $.fn.timeago = function () {
- const els = $(this);
- // Convert "old" format to new format (#5108)
- els.each(function () {
- iso = this.getAttribute('title');
- if (!iso) {
- return;
- }
- this.setAttribute('datetime', iso);
- date = new Date(iso);
- if (!isNaN(date)) {
- this.textContent = formatFn(date);
- }
- });
-
- timeagoFn.apply(this, arguments);
- };
- };
+ (function ($) {
+ $.fn.getCursorPosition = function () {
+ const element = $(this).get(0);
+ let pos = 0;
+ if ('selectionStart' in element) {
+ pos = element.selectionStart;
+ } else if ('selection' in document) {
+ element.focus();
+ const Sel = document.selection.createRange();
+ const SelLength = document.selection.createRange().text.length;
+ Sel.moveStart('character', -element.value.length);
+ pos = Sel.text.length - SelLength;
+ }
+
+ return pos;
+ };
+
+ $.fn.selectRange = function (start, end) {
+ end ||= start;
+
+ return this.each(function () {
+ if (this.setSelectionRange) {
+ this.focus();
+ this.setSelectionRange(start, end);
+ } else if (this.createTextRange) {
+ const range = this.createTextRange();
+ range.collapse(true);
+ range.moveEnd('character', end);
+ range.moveStart('character', start);
+ range.select();
+ }
+ });
+ };
+
+ // http://stackoverflow.com/questions/511088/use-javascript-to-place-cursor-at-end-of-text-in-text-input-element
+ $.fn.putCursorAtEnd = function () {
+ return this.each(function () {
+ $(this).focus();
+
+ if (this.setSelectionRange) {
+ const length = $(this).val().length * 2;
+ this.setSelectionRange(length, length);
+ } else {
+ $(this).val($(this).val());
+ }
+
+ this.scrollTop = 999_999;
+ });
+ };
+
+ $.fn.translateHtml = function (string_) {
+ return translate(this, 'html', string_);
+ };
+
+ $.fn.translateText = function (string_) {
+ return translate(this, 'text', string_);
+ };
+
+ $.fn.translateVal = function (string_) {
+ return translate(this, 'val', string_);
+ };
+
+ $.fn.translateAttr = function (attribute, string_) {
+ return this.each(function () {
+ const element = $(this);
+ translator.translate(string_, translated => {
+ element.attr(attribute, translated);
+ });
+ });
+ };
+ })(jQuery || {fn: {}});
+
+ (function () {
+ // FIX FOR #1245 - https://github.com/NodeBB/NodeBB/issues/1245
+ // from http://stackoverflow.com/questions/15931962/bootstrap-dropdown-disappear-with-right-click-on-firefox
+ // obtain a reference to the original handler
+ let _clearMenus = $._data(document, 'events').click.filter(element => element.namespace === 'bs.data-api.dropdown' && element.selector === undefined);
+
+ if (_clearMenus.length > 0) {
+ _clearMenus = _clearMenus[0].handler;
+ }
+
+ // Disable the old listener
+ $(document)
+ .off('click.data-api.dropdown', _clearMenus)
+ .on('click.data-api.dropdown', e => {
+ // Call the handler only when not right-click
+ if (e.button !== 2) {
+ _clearMenus();
+ }
+ });
+ })();
+
+ let timeagoFunction;
+ overrides.overrideTimeagoCutoff = function () {
+ const cutoff = Number.parseInt(ajaxify.data.timeagoCutoff || config.timeagoCutoff, 10);
+ if (cutoff === 0) {
+ $.timeago.settings.cutoff = 1;
+ } else if (cutoff > 0) {
+ $.timeago.settings.cutoff = 1000 * 60 * 60 * 24 * cutoff;
+ }
+ };
+
+ overrides.overrideTimeago = function () {
+ timeagoFunction ||= $.fn.timeago;
+
+ overrides.overrideTimeagoCutoff();
+
+ $.timeago.settings.allowFuture = true;
+ const userLang = config.userLang.replace('_', '-');
+ const options = {
+ year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric',
+ };
+ let formatFunction = function (date) {
+ return date.toLocaleString(userLang, options);
+ };
+
+ try {
+ if (typeof Intl !== 'undefined') {
+ const dtFormat = new Intl.DateTimeFormat(userLang, options);
+ formatFunction = dtFormat.format;
+ }
+ } catch (error) {
+ console.error(error);
+ }
+
+ let iso;
+ let date;
+ $.fn.timeago = function () {
+ const els = $(this);
+ // Convert "old" format to new format (#5108)
+ els.each(function () {
+ iso = this.getAttribute('title');
+ if (!iso) {
+ return;
+ }
+
+ this.setAttribute('datetime', iso);
+ date = new Date(iso);
+ if (!isNaN(date)) {
+ this.textContent = formatFunction(date);
+ }
+ });
+
+ Reflect.apply(timeagoFunction, this, arguments);
+ };
+ };
}
diff --git a/public/src/service-worker.js b/public/src/service-worker.js
index 883a7dc..80714ad 100644
--- a/public/src/service-worker.js
+++ b/public/src/service-worker.js
@@ -1,19 +1,19 @@
'use strict';
-self.addEventListener('fetch', function (event) {
- // This is the code that ignores post requests
- // https://github.com/NodeBB/NodeBB/issues/9151
- // https://github.com/w3c/ServiceWorker/issues/1141
- // https://stackoverflow.com/questions/54448367/ajax-xmlhttprequest-progress-monitoring-doesnt-work-with-service-workers
- if (event.request.method === 'POST') {
- return;
- }
+self.addEventListener('fetch', event => {
+ // This is the code that ignores post requests
+ // https://github.com/NodeBB/NodeBB/issues/9151
+ // https://github.com/w3c/ServiceWorker/issues/1141
+ // https://stackoverflow.com/questions/54448367/ajax-xmlhttprequest-progress-monitoring-doesnt-work-with-service-workers
+ if (event.request.method === 'POST') {
+ return;
+ }
- event.respondWith(caches.match(event.request).then(function (response) {
- if (!response) {
- return fetch(event.request);
- }
+ event.respondWith(caches.match(event.request).then(response => {
+ if (!response) {
+ return fetch(event.request);
+ }
- return response;
- }));
+ return response;
+ }));
});
diff --git a/public/src/sockets.js b/public/src/sockets.js
index 1665726..b913742 100644
--- a/public/src/sockets.js
+++ b/public/src/sockets.js
@@ -1,257 +1,259 @@
'use strict';
-// eslint-disable-next-line no-redeclare
const io = require('socket.io-client');
-// eslint-disable-next-line no-redeclare
const $ = require('jquery');
app = window.app || {};
(function () {
- let reconnecting = false;
-
- const ioParams = {
- reconnectionAttempts: config.maxReconnectionAttempts,
- reconnectionDelay: config.reconnectionDelay,
- transports: config.socketioTransports,
- path: config.relative_path + '/socket.io',
- };
-
- window.socket = io(config.websocketAddress, ioParams);
-
- const oEmit = socket.emit;
- socket.emit = function (event, data, callback) {
- if (typeof data === 'function') {
- callback = data;
- data = null;
- }
- if (typeof callback === 'function') {
- oEmit.apply(socket, [event, data, callback]);
- return;
- }
-
- return new Promise(function (resolve, reject) {
- oEmit.apply(socket, [event, data, function (err, result) {
- if (err) reject(err);
- else resolve(result);
- }]);
- });
- };
-
- let hooks;
- require(['hooks'], function (_hooks) {
- hooks = _hooks;
- if (parseInt(app.user.uid, 10) >= 0) {
- addHandlers();
- }
- });
-
- window.app.reconnect = () => {
- if (socket.connected) {
- return;
- }
-
- const reconnectEl = $('#reconnect');
- $('#reconnect-alert')
- .removeClass('alert-danger pointer')
- .addClass('alert-warning')
- .find('p')
- .translateText(`[[global:reconnecting-message, ${config.siteTitle}]]`);
-
- reconnectEl.html('
');
- socket.connect();
- };
-
- function addHandlers() {
- socket.on('connect', onConnect);
-
- socket.on('disconnect', onDisconnect);
-
- socket.io.on('reconnect_failed', function () {
- const reconnectEl = $('#reconnect');
- reconnectEl.html('
');
-
- $('#reconnect-alert')
- .removeClass('alert-warning')
- .addClass('alert-danger pointer')
- .find('p')
- .translateText('[[error:socket-reconnect-failed]]')
- .one('click', app.reconnect);
-
- $(window).one('focus', app.reconnect);
- });
-
- socket.on('checkSession', function (uid) {
- if (parseInt(uid, 10) !== parseInt(app.user.uid, 10)) {
- handleSessionMismatch();
- }
- });
- socket.on('event:invalid_session', () => {
- handleInvalidSession();
- });
-
- socket.on('setHostname', function (hostname) {
- app.upstreamHost = hostname;
- });
-
- socket.on('event:banned', onEventBanned);
- socket.on('event:unbanned', onEventUnbanned);
- socket.on('event:logout', function () {
- require(['logout'], function (logout) {
- logout();
- });
- });
- socket.on('event:alert', function (params) {
- require(['alerts'], function (alerts) {
- alerts.alert(params);
- });
- });
- socket.on('event:deprecated_call', function (data) {
- console.warn('[socket.io] ', data.eventName, 'is now deprecated in favour of', data.replacement);
- });
-
- socket.removeAllListeners('event:nodebb.ready');
- socket.on('event:nodebb.ready', function (data) {
- if ((data.hostname === app.upstreamHost) && (!app.cacheBuster || app.cacheBuster !== data['cache-buster'])) {
- app.cacheBuster = data['cache-buster'];
- require(['alerts'], function (alerts) {
- alerts.alert({
- alert_id: 'forum_updated',
- title: '[[global:updated.title]]',
- message: '[[global:updated.message]]',
- clickfn: function () {
- window.location.reload();
- },
- type: 'warning',
- });
- });
- }
- });
- socket.on('event:livereload', function () {
- if (app.user.isAdmin && !ajaxify.currentPage.match(/admin/)) {
- window.location.reload();
- }
- });
- }
-
- function handleInvalidSession() {
- socket.disconnect();
- require(['messages', 'logout'], function (messages, logout) {
- logout(false);
- messages.showInvalidSession();
- });
- }
-
- function handleSessionMismatch() {
- if (app.flags._login || app.flags._logout) {
- return;
- }
-
- socket.disconnect();
- require(['messages'], function (messages) {
- messages.showSessionMismatch();
- });
- }
-
- function onConnect() {
- if (!reconnecting) {
- hooks.fire('action:connected');
- }
-
- if (reconnecting) {
- const reconnectEl = $('#reconnect');
- const reconnectAlert = $('#reconnect-alert');
-
- reconnectEl.tooltip('destroy');
- reconnectEl.html('
');
- reconnectAlert.addClass('hide');
- reconnecting = false;
-
- reJoinCurrentRoom();
-
- socket.emit('meta.reconnected');
-
- hooks.fire('action:reconnected');
-
- setTimeout(function () {
- reconnectEl.removeClass('active').addClass('hide');
- }, 3000);
- }
- }
-
- function reJoinCurrentRoom() {
- if (app.currentRoom) {
- const current = app.currentRoom;
- app.currentRoom = '';
- app.enterRoom(current);
- }
- }
-
- function onReconnecting() {
- reconnecting = true;
- const reconnectEl = $('#reconnect');
- const reconnectAlert = $('#reconnect-alert');
-
- if (!reconnectEl.hasClass('active')) {
- reconnectEl.html('
');
- reconnectAlert.removeClass('hide');
- }
-
- reconnectEl.addClass('active').removeClass('hide').tooltip({
- placement: 'bottom',
- });
- }
-
- function onDisconnect() {
- setTimeout(function () {
- if (socket.disconnected) {
- onReconnecting();
- }
- }, 2000);
-
- hooks.fire('action:disconnected');
- }
-
- function onEventBanned(data) {
- require(['bootbox', 'translator'], function (bootbox, translator) {
- const message = data.until ?
- translator.compile('error:user-banned-reason-until', (new Date(data.until).toLocaleString()), data.reason) :
- '[[error:user-banned-reason, ' + data.reason + ']]';
- translator.translate(message, function (message) {
- bootbox.alert({
- title: '[[error:user-banned]]',
- message: message,
- closeButton: false,
- callback: function () {
- window.location.href = config.relative_path + '/';
- },
- });
- });
- });
- }
-
- function onEventUnbanned() {
- require(['bootbox'], function (bootbox) {
- bootbox.alert({
- title: '[[global:alert.unbanned]]',
- message: '[[global:alert.unbanned.message]]',
- closeButton: false,
- callback: function () {
- window.location.href = config.relative_path + '/';
- },
- });
- });
- }
-
- if (
- config.socketioOrigins &&
- config.socketioOrigins !== '*:*' &&
- config.socketioOrigins.indexOf(location.hostname) === -1
- ) {
- console.error(
- 'You are accessing the forum from an unknown origin. This will likely result in websockets failing to connect. \n' +
- 'To fix this, set the `"url"` value in `config.json` to the URL at which you access the site. \n' +
- 'For more information, see this FAQ topic: https://community.nodebb.org/topic/13388'
- );
- }
-}());
+ let reconnecting = false;
+
+ const ioParameters = {
+ reconnectionAttempts: config.maxReconnectionAttempts,
+ reconnectionDelay: config.reconnectionDelay,
+ transports: config.socketioTransports,
+ path: config.relative_path + '/socket.io',
+ };
+
+ window.socket = io(config.websocketAddress, ioParameters);
+
+ const oEmit = socket.emit;
+ socket.emit = function (event, data, callback) {
+ if (typeof data === 'function') {
+ callback = data;
+ data = null;
+ }
+
+ if (typeof callback === 'function') {
+ oEmit.apply(socket, [event, data, callback]);
+ return;
+ }
+
+ return new Promise((resolve, reject) => {
+ oEmit.apply(socket, [event, data, function (error, result) {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(result);
+ }
+ }]);
+ });
+ };
+
+ let hooks;
+ require(['hooks'], _hooks => {
+ hooks = _hooks;
+ if (Number.parseInt(app.user.uid, 10) >= 0) {
+ addHandlers();
+ }
+ });
+
+ window.app.reconnect = () => {
+ if (socket.connected) {
+ return;
+ }
+
+ const reconnectElement = $('#reconnect');
+ $('#reconnect-alert')
+ .removeClass('alert-danger pointer')
+ .addClass('alert-warning')
+ .find('p')
+ .translateText(`[[global:reconnecting-message, ${config.siteTitle}]]`);
+
+ reconnectElement.html('
');
+ socket.connect();
+ };
+
+ function addHandlers() {
+ socket.on('connect', onConnect);
+
+ socket.on('disconnect', onDisconnect);
+
+ socket.io.on('reconnect_failed', () => {
+ const reconnectElement = $('#reconnect');
+ reconnectElement.html('
');
+
+ $('#reconnect-alert')
+ .removeClass('alert-warning')
+ .addClass('alert-danger pointer')
+ .find('p')
+ .translateText('[[error:socket-reconnect-failed]]')
+ .one('click', app.reconnect);
+
+ $(window).one('focus', app.reconnect);
+ });
+
+ socket.on('checkSession', uid => {
+ if (Number.parseInt(uid, 10) !== Number.parseInt(app.user.uid, 10)) {
+ handleSessionMismatch();
+ }
+ });
+ socket.on('event:invalid_session', () => {
+ handleInvalidSession();
+ });
+
+ socket.on('setHostname', hostname => {
+ app.upstreamHost = hostname;
+ });
+
+ socket.on('event:banned', onEventBanned);
+ socket.on('event:unbanned', onEventUnbanned);
+ socket.on('event:logout', () => {
+ require(['logout'], logout => {
+ logout();
+ });
+ });
+ socket.on('event:alert', parameters => {
+ require(['alerts'], alerts => {
+ alerts.alert(parameters);
+ });
+ });
+ socket.on('event:deprecated_call', data => {
+ console.warn('[socket.io]', data.eventName, 'is now deprecated in favour of', data.replacement);
+ });
+
+ socket.removeAllListeners('event:nodebb.ready');
+ socket.on('event:nodebb.ready', data => {
+ if ((data.hostname === app.upstreamHost) && (!app.cacheBuster || app.cacheBuster !== data['cache-buster'])) {
+ app.cacheBuster = data['cache-buster'];
+ require(['alerts'], alerts => {
+ alerts.alert({
+ alert_id: 'forum_updated',
+ title: '[[global:updated.title]]',
+ message: '[[global:updated.message]]',
+ clickfn() {
+ window.location.reload();
+ },
+ type: 'warning',
+ });
+ });
+ }
+ });
+ socket.on('event:livereload', () => {
+ if (app.user.isAdmin && !/admin/.test(ajaxify.currentPage)) {
+ window.location.reload();
+ }
+ });
+ }
+
+ function handleInvalidSession() {
+ socket.disconnect();
+ require(['messages', 'logout'], (messages, logout) => {
+ logout(false);
+ messages.showInvalidSession();
+ });
+ }
+
+ function handleSessionMismatch() {
+ if (app.flags._login || app.flags._logout) {
+ return;
+ }
+
+ socket.disconnect();
+ require(['messages'], messages => {
+ messages.showSessionMismatch();
+ });
+ }
+
+ function onConnect() {
+ if (!reconnecting) {
+ hooks.fire('action:connected');
+ }
+
+ if (reconnecting) {
+ const reconnectElement = $('#reconnect');
+ const reconnectAlert = $('#reconnect-alert');
+
+ reconnectElement.tooltip('destroy');
+ reconnectElement.html('
');
+ reconnectAlert.addClass('hide');
+ reconnecting = false;
+
+ reJoinCurrentRoom();
+
+ socket.emit('meta.reconnected');
+
+ hooks.fire('action:reconnected');
+
+ setTimeout(() => {
+ reconnectElement.removeClass('active').addClass('hide');
+ }, 3000);
+ }
+ }
+
+ function reJoinCurrentRoom() {
+ if (app.currentRoom) {
+ const current = app.currentRoom;
+ app.currentRoom = '';
+ app.enterRoom(current);
+ }
+ }
+
+ function onReconnecting() {
+ reconnecting = true;
+ const reconnectElement = $('#reconnect');
+ const reconnectAlert = $('#reconnect-alert');
+
+ if (!reconnectElement.hasClass('active')) {
+ reconnectElement.html('
');
+ reconnectAlert.removeClass('hide');
+ }
+
+ reconnectElement.addClass('active').removeClass('hide').tooltip({
+ placement: 'bottom',
+ });
+ }
+
+ function onDisconnect() {
+ setTimeout(() => {
+ if (socket.disconnected) {
+ onReconnecting();
+ }
+ }, 2000);
+
+ hooks.fire('action:disconnected');
+ }
+
+ function onEventBanned(data) {
+ require(['bootbox', 'translator'], (bootbox, translator) => {
+ const message = data.until
+ ? translator.compile('error:user-banned-reason-until', (new Date(data.until).toLocaleString()), data.reason)
+ : '[[error:user-banned-reason, ' + data.reason + ']]';
+ translator.translate(message, message => {
+ bootbox.alert({
+ title: '[[error:user-banned]]',
+ message,
+ closeButton: false,
+ callback() {
+ window.location.href = config.relative_path + '/';
+ },
+ });
+ });
+ });
+ }
+
+ function onEventUnbanned() {
+ require(['bootbox'], bootbox => {
+ bootbox.alert({
+ title: '[[global:alert.unbanned]]',
+ message: '[[global:alert.unbanned.message]]',
+ closeButton: false,
+ callback() {
+ window.location.href = config.relative_path + '/';
+ },
+ });
+ });
+ }
+
+ if (
+ config.socketioOrigins
+ && config.socketioOrigins !== '*:*'
+ && !config.socketioOrigins.includes(location.hostname)
+ ) {
+ console.error(
+ 'You are accessing the forum from an unknown origin. This will likely result in websockets failing to connect. \n'
+ + 'To fix this, set the `"url"` value in `config.json` to the URL at which you access the site. \n'
+ + 'For more information, see this FAQ topic: https://community.nodebb.org/topic/13388',
+ );
+ }
+})();
diff --git a/public/src/utils.common.js b/public/src/utils.common.js
index 67af6c6..5658433 100644
--- a/public/src/utils.common.js
+++ b/public/src/utils.common.js
@@ -1,750 +1,979 @@
'use strict';
-
-// add default escape function for escaping HTML entities
+// Add default escape function for escaping HTML entities
const escapeCharMap = Object.freeze({
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": ''',
- '`': '`',
- '=': '=',
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ '\'': ''',
+ '`': '`',
+ '=': '=',
});
function replaceChar(c) {
- return escapeCharMap[c];
+ return escapeCharMap[c];
}
+
const escapeChars = /[&<>"'`=]/g;
const HTMLEntities = Object.freeze({
- amp: '&',
- gt: '>',
- lt: '<',
- quot: '"',
- apos: "'",
- AElig: 198,
- Aacute: 193,
- Acirc: 194,
- Agrave: 192,
- Aring: 197,
- Atilde: 195,
- Auml: 196,
- Ccedil: 199,
- ETH: 208,
- Eacute: 201,
- Ecirc: 202,
- Egrave: 200,
- Euml: 203,
- Iacute: 205,
- Icirc: 206,
- Igrave: 204,
- Iuml: 207,
- Ntilde: 209,
- Oacute: 211,
- Ocirc: 212,
- Ograve: 210,
- Oslash: 216,
- Otilde: 213,
- Ouml: 214,
- THORN: 222,
- Uacute: 218,
- Ucirc: 219,
- Ugrave: 217,
- Uuml: 220,
- Yacute: 221,
- aacute: 225,
- acirc: 226,
- aelig: 230,
- agrave: 224,
- aring: 229,
- atilde: 227,
- auml: 228,
- ccedil: 231,
- eacute: 233,
- ecirc: 234,
- egrave: 232,
- eth: 240,
- euml: 235,
- iacute: 237,
- icirc: 238,
- igrave: 236,
- iuml: 239,
- ntilde: 241,
- oacute: 243,
- ocirc: 244,
- ograve: 242,
- oslash: 248,
- otilde: 245,
- ouml: 246,
- szlig: 223,
- thorn: 254,
- uacute: 250,
- ucirc: 251,
- ugrave: 249,
- uuml: 252,
- yacute: 253,
- yuml: 255,
- copy: 169,
- reg: 174,
- nbsp: 160,
- iexcl: 161,
- cent: 162,
- pound: 163,
- curren: 164,
- yen: 165,
- brvbar: 166,
- sect: 167,
- uml: 168,
- ordf: 170,
- laquo: 171,
- not: 172,
- shy: 173,
- macr: 175,
- deg: 176,
- plusmn: 177,
- sup1: 185,
- sup2: 178,
- sup3: 179,
- acute: 180,
- micro: 181,
- para: 182,
- middot: 183,
- cedil: 184,
- ordm: 186,
- raquo: 187,
- frac14: 188,
- frac12: 189,
- frac34: 190,
- iquest: 191,
- times: 215,
- divide: 247,
- 'OElig;': 338,
- 'oelig;': 339,
- 'Scaron;': 352,
- 'scaron;': 353,
- 'Yuml;': 376,
- 'fnof;': 402,
- 'circ;': 710,
- 'tilde;': 732,
- 'Alpha;': 913,
- 'Beta;': 914,
- 'Gamma;': 915,
- 'Delta;': 916,
- 'Epsilon;': 917,
- 'Zeta;': 918,
- 'Eta;': 919,
- 'Theta;': 920,
- 'Iota;': 921,
- 'Kappa;': 922,
- 'Lambda;': 923,
- 'Mu;': 924,
- 'Nu;': 925,
- 'Xi;': 926,
- 'Omicron;': 927,
- 'Pi;': 928,
- 'Rho;': 929,
- 'Sigma;': 931,
- 'Tau;': 932,
- 'Upsilon;': 933,
- 'Phi;': 934,
- 'Chi;': 935,
- 'Psi;': 936,
- 'Omega;': 937,
- 'alpha;': 945,
- 'beta;': 946,
- 'gamma;': 947,
- 'delta;': 948,
- 'epsilon;': 949,
- 'zeta;': 950,
- 'eta;': 951,
- 'theta;': 952,
- 'iota;': 953,
- 'kappa;': 954,
- 'lambda;': 955,
- 'mu;': 956,
- 'nu;': 957,
- 'xi;': 958,
- 'omicron;': 959,
- 'pi;': 960,
- 'rho;': 961,
- 'sigmaf;': 962,
- 'sigma;': 963,
- 'tau;': 964,
- 'upsilon;': 965,
- 'phi;': 966,
- 'chi;': 967,
- 'psi;': 968,
- 'omega;': 969,
- 'thetasym;': 977,
- 'upsih;': 978,
- 'piv;': 982,
- 'ensp;': 8194,
- 'emsp;': 8195,
- 'thinsp;': 8201,
- 'zwnj;': 8204,
- 'zwj;': 8205,
- 'lrm;': 8206,
- 'rlm;': 8207,
- 'ndash;': 8211,
- 'mdash;': 8212,
- 'lsquo;': 8216,
- 'rsquo;': 8217,
- 'sbquo;': 8218,
- 'ldquo;': 8220,
- 'rdquo;': 8221,
- 'bdquo;': 8222,
- 'dagger;': 8224,
- 'Dagger;': 8225,
- 'bull;': 8226,
- 'hellip;': 8230,
- 'permil;': 8240,
- 'prime;': 8242,
- 'Prime;': 8243,
- 'lsaquo;': 8249,
- 'rsaquo;': 8250,
- 'oline;': 8254,
- 'frasl;': 8260,
- 'euro;': 8364,
- 'image;': 8465,
- 'weierp;': 8472,
- 'real;': 8476,
- 'trade;': 8482,
- 'alefsym;': 8501,
- 'larr;': 8592,
- 'uarr;': 8593,
- 'rarr;': 8594,
- 'darr;': 8595,
- 'harr;': 8596,
- 'crarr;': 8629,
- 'lArr;': 8656,
- 'uArr;': 8657,
- 'rArr;': 8658,
- 'dArr;': 8659,
- 'hArr;': 8660,
- 'forall;': 8704,
- 'part;': 8706,
- 'exist;': 8707,
- 'empty;': 8709,
- 'nabla;': 8711,
- 'isin;': 8712,
- 'notin;': 8713,
- 'ni;': 8715,
- 'prod;': 8719,
- 'sum;': 8721,
- 'minus;': 8722,
- 'lowast;': 8727,
- 'radic;': 8730,
- 'prop;': 8733,
- 'infin;': 8734,
- 'ang;': 8736,
- 'and;': 8743,
- 'or;': 8744,
- 'cap;': 8745,
- 'cup;': 8746,
- 'int;': 8747,
- 'there4;': 8756,
- 'sim;': 8764,
- 'cong;': 8773,
- 'asymp;': 8776,
- 'ne;': 8800,
- 'equiv;': 8801,
- 'le;': 8804,
- 'ge;': 8805,
- 'sub;': 8834,
- 'sup;': 8835,
- 'nsub;': 8836,
- 'sube;': 8838,
- 'supe;': 8839,
- 'oplus;': 8853,
- 'otimes;': 8855,
- 'perp;': 8869,
- 'sdot;': 8901,
- 'lceil;': 8968,
- 'rceil;': 8969,
- 'lfloor;': 8970,
- 'rfloor;': 8971,
- 'lang;': 9001,
- 'rang;': 9002,
- 'loz;': 9674,
- 'spades;': 9824,
- 'clubs;': 9827,
- 'hearts;': 9829,
- 'diams;': 9830,
+ amp: '&',
+ gt: '>',
+ lt: '<',
+ quot: '"',
+ apos: '\'',
+ AElig: 198,
+ Aacute: 193,
+ Acirc: 194,
+ Agrave: 192,
+ Aring: 197,
+ Atilde: 195,
+ Auml: 196,
+ Ccedil: 199,
+ ETH: 208,
+ Eacute: 201,
+ Ecirc: 202,
+ Egrave: 200,
+ Euml: 203,
+ Iacute: 205,
+ Icirc: 206,
+ Igrave: 204,
+ Iuml: 207,
+ Ntilde: 209,
+ Oacute: 211,
+ Ocirc: 212,
+ Ograve: 210,
+ Oslash: 216,
+ Otilde: 213,
+ Ouml: 214,
+ THORN: 222,
+ Uacute: 218,
+ Ucirc: 219,
+ Ugrave: 217,
+ Uuml: 220,
+ Yacute: 221,
+ aacute: 225,
+ acirc: 226,
+ aelig: 230,
+ agrave: 224,
+ aring: 229,
+ atilde: 227,
+ auml: 228,
+ ccedil: 231,
+ eacute: 233,
+ ecirc: 234,
+ egrave: 232,
+ eth: 240,
+ euml: 235,
+ iacute: 237,
+ icirc: 238,
+ igrave: 236,
+ iuml: 239,
+ ntilde: 241,
+ oacute: 243,
+ ocirc: 244,
+ ograve: 242,
+ oslash: 248,
+ otilde: 245,
+ ouml: 246,
+ szlig: 223,
+ thorn: 254,
+ uacute: 250,
+ ucirc: 251,
+ ugrave: 249,
+ uuml: 252,
+ yacute: 253,
+ yuml: 255,
+ copy: 169,
+ reg: 174,
+ nbsp: 160,
+ iexcl: 161,
+ cent: 162,
+ pound: 163,
+ curren: 164,
+ yen: 165,
+ brvbar: 166,
+ sect: 167,
+ uml: 168,
+ ordf: 170,
+ laquo: 171,
+ not: 172,
+ shy: 173,
+ macr: 175,
+ deg: 176,
+ plusmn: 177,
+ sup1: 185,
+ sup2: 178,
+ sup3: 179,
+ acute: 180,
+ micro: 181,
+ para: 182,
+ middot: 183,
+ cedil: 184,
+ ordm: 186,
+ raquo: 187,
+ frac14: 188,
+ frac12: 189,
+ frac34: 190,
+ iquest: 191,
+ times: 215,
+ divide: 247,
+ 'OElig;': 338,
+ 'oelig;': 339,
+ 'Scaron;': 352,
+ 'scaron;': 353,
+ 'Yuml;': 376,
+ 'fnof;': 402,
+ 'circ;': 710,
+ 'tilde;': 732,
+ 'Alpha;': 913,
+ 'Beta;': 914,
+ 'Gamma;': 915,
+ 'Delta;': 916,
+ 'Epsilon;': 917,
+ 'Zeta;': 918,
+ 'Eta;': 919,
+ 'Theta;': 920,
+ 'Iota;': 921,
+ 'Kappa;': 922,
+ 'Lambda;': 923,
+ 'Mu;': 924,
+ 'Nu;': 925,
+ 'Xi;': 926,
+ 'Omicron;': 927,
+ 'Pi;': 928,
+ 'Rho;': 929,
+ 'Sigma;': 931,
+ 'Tau;': 932,
+ 'Upsilon;': 933,
+ 'Phi;': 934,
+ 'Chi;': 935,
+ 'Psi;': 936,
+ 'Omega;': 937,
+ 'alpha;': 945,
+ 'beta;': 946,
+ 'gamma;': 947,
+ 'delta;': 948,
+ 'epsilon;': 949,
+ 'zeta;': 950,
+ 'eta;': 951,
+ 'theta;': 952,
+ 'iota;': 953,
+ 'kappa;': 954,
+ 'lambda;': 955,
+ 'mu;': 956,
+ 'nu;': 957,
+ 'xi;': 958,
+ 'omicron;': 959,
+ 'pi;': 960,
+ 'rho;': 961,
+ 'sigmaf;': 962,
+ 'sigma;': 963,
+ 'tau;': 964,
+ 'upsilon;': 965,
+ 'phi;': 966,
+ 'chi;': 967,
+ 'psi;': 968,
+ 'omega;': 969,
+ 'thetasym;': 977,
+ 'upsih;': 978,
+ 'piv;': 982,
+ 'ensp;': 8194,
+ 'emsp;': 8195,
+ 'thinsp;': 8201,
+ 'zwnj;': 8204,
+ 'zwj;': 8205,
+ 'lrm;': 8206,
+ 'rlm;': 8207,
+ 'ndash;': 8211,
+ 'mdash;': 8212,
+ 'lsquo;': 8216,
+ 'rsquo;': 8217,
+ 'sbquo;': 8218,
+ 'ldquo;': 8220,
+ 'rdquo;': 8221,
+ 'bdquo;': 8222,
+ 'dagger;': 8224,
+ 'Dagger;': 8225,
+ 'bull;': 8226,
+ 'hellip;': 8230,
+ 'permil;': 8240,
+ 'prime;': 8242,
+ 'Prime;': 8243,
+ 'lsaquo;': 8249,
+ 'rsaquo;': 8250,
+ 'oline;': 8254,
+ 'frasl;': 8260,
+ 'euro;': 8364,
+ 'image;': 8465,
+ 'weierp;': 8472,
+ 'real;': 8476,
+ 'trade;': 8482,
+ 'alefsym;': 8501,
+ 'larr;': 8592,
+ 'uarr;': 8593,
+ 'rarr;': 8594,
+ 'darr;': 8595,
+ 'harr;': 8596,
+ 'crarr;': 8629,
+ 'lArr;': 8656,
+ 'uArr;': 8657,
+ 'rArr;': 8658,
+ 'dArr;': 8659,
+ 'hArr;': 8660,
+ 'forall;': 8704,
+ 'part;': 8706,
+ 'exist;': 8707,
+ 'empty;': 8709,
+ 'nabla;': 8711,
+ 'isin;': 8712,
+ 'notin;': 8713,
+ 'ni;': 8715,
+ 'prod;': 8719,
+ 'sum;': 8721,
+ 'minus;': 8722,
+ 'lowast;': 8727,
+ 'radic;': 8730,
+ 'prop;': 8733,
+ 'infin;': 8734,
+ 'ang;': 8736,
+ 'and;': 8743,
+ 'or;': 8744,
+ 'cap;': 8745,
+ 'cup;': 8746,
+ 'int;': 8747,
+ 'there4;': 8756,
+ 'sim;': 8764,
+ 'cong;': 8773,
+ 'asymp;': 8776,
+ 'ne;': 8800,
+ 'equiv;': 8801,
+ 'le;': 8804,
+ 'ge;': 8805,
+ 'sub;': 8834,
+ 'sup;': 8835,
+ 'nsub;': 8836,
+ 'sube;': 8838,
+ 'supe;': 8839,
+ 'oplus;': 8853,
+ 'otimes;': 8855,
+ 'perp;': 8869,
+ 'sdot;': 8901,
+ 'lceil;': 8968,
+ 'rceil;': 8969,
+ 'lfloor;': 8970,
+ 'rfloor;': 8971,
+ 'lang;': 9001,
+ 'rang;': 9002,
+ 'loz;': 9674,
+ 'spades;': 9824,
+ 'clubs;': 9827,
+ 'hearts;': 9829,
+ 'diams;': 9830,
});
-/* eslint-disable no-redeclare */
const utils = {
- // https://github.com/substack/node-ent/blob/master/index.js
- decodeHTMLEntities: function (html) {
- return String(html)
- .replace(/(\d+);?/g, function (_, code) {
- return String.fromCharCode(code);
- })
- .replace(/[xX]([A-Fa-f0-9]+);?/g, function (_, hex) {
- return String.fromCharCode(parseInt(hex, 16));
- })
- .replace(/&([^;\W]+;?)/g, function (m, e) {
- const ee = e.replace(/;$/, '');
- const target = HTMLEntities[e] || (e.match(/;$/) && HTMLEntities[ee]);
-
- if (typeof target === 'number') {
- return String.fromCharCode(target);
- } else if (typeof target === 'string') {
- return target;
- }
-
- return m;
- });
- },
- // https://github.com/jprichardson/string.js/blob/master/lib/string.js
- stripHTMLTags: function (str, tags) {
- const pattern = (tags || ['']).join('|');
- return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), '');
- },
-
- cleanUpTag: function (tag, maxLength) {
- if (typeof tag !== 'string' || !tag.length) {
- return '';
- }
-
- tag = tag.trim().toLowerCase();
- // see https://github.com/NodeBB/NodeBB/issues/4378
- tag = tag.replace(/\u202E/gi, '');
- tag = tag.replace(/[,/#!$^*;:{}=_`<>'"~()?|]/g, '');
- tag = tag.slice(0, maxLength || 15).trim();
- const matches = tag.match(/^[.-]*(.+?)[.-]*$/);
- if (matches && matches.length > 1) {
- tag = matches[1];
- }
- return tag;
- },
-
- removePunctuation: function (str) {
- return str.replace(/[.,-/#!$%^&*;:{}=\-_`<>'"~()?]/g, '');
- },
-
- isEmailValid: function (email) {
- return typeof email === 'string' && email.length && email.indexOf('@') !== -1 && email.indexOf(',') === -1 && email.indexOf(';') === -1;
- },
-
- isUserNameValid: function (name) {
- return (name && name !== '' && (/^['" \-+.*[\]0-9\u00BF-\u1FFF\u2C00-\uD7FF\w]+$/.test(name)));
- },
-
- isPasswordValid: function (password) {
- return typeof password === 'string' && password.length;
- },
-
- isNumber: function (n) {
- // `isFinite('') === true` so isNan parseFloat check is necessary
- return !isNaN(parseFloat(n)) && isFinite(n);
- },
-
- languageKeyRegex: /\[\[[\w]+:.+\]\]/,
- hasLanguageKey: function (input) {
- return utils.languageKeyRegex.test(input);
- },
- userLangToTimeagoCode: function (userLang) {
- const mapping = {
- 'en-GB': 'en',
- 'en-US': 'en',
- 'fa-IR': 'fa',
- 'pt-BR': 'pt-br',
- nb: 'no',
- };
- return mapping.hasOwnProperty(userLang) ? mapping[userLang] : userLang;
- },
- // shallow objects merge
- merge: function () {
- const result = {};
- let obj;
- let keys;
- for (let i = 0; i < arguments.length; i += 1) {
- obj = arguments[i] || {};
- keys = Object.keys(obj);
- for (let j = 0; j < keys.length; j += 1) {
- result[keys[j]] = obj[keys[j]];
- }
- }
- return result;
- },
-
- fileExtension: function (path) {
- return ('' + path).split('.').pop();
- },
-
- extensionMimeTypeMap: {
- bmp: 'image/bmp',
- cmx: 'image/x-cmx',
- cod: 'image/cis-cod',
- gif: 'image/gif',
- ico: 'image/x-icon',
- ief: 'image/ief',
- jfif: 'image/pipeg',
- jpe: 'image/jpeg',
- jpeg: 'image/jpeg',
- jpg: 'image/jpeg',
- png: 'image/png',
- pbm: 'image/x-portable-bitmap',
- pgm: 'image/x-portable-graymap',
- pnm: 'image/x-portable-anymap',
- ppm: 'image/x-portable-pixmap',
- ras: 'image/x-cmu-raster',
- rgb: 'image/x-rgb',
- svg: 'image/svg+xml',
- tif: 'image/tiff',
- tiff: 'image/tiff',
- xbm: 'image/x-xbitmap',
- xpm: 'image/x-xpixmap',
- xwd: 'image/x-xwindowdump',
- },
-
- fileMimeType: function (path) {
- return utils.extensionToMimeType(utils.fileExtension(path));
- },
-
- extensionToMimeType: function (extension) {
- return utils.extensionMimeTypeMap.hasOwnProperty(extension) ? utils.extensionMimeTypeMap[extension] : '*';
- },
-
- isPromise: function (object) {
- // https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise#comment97339131_27746324
- return object && typeof object.then === 'function';
- },
-
- promiseParallel: function (obj) {
- const keys = Object.keys(obj);
- return Promise.all(
- keys.map(function (k) { return obj[k]; })
- ).then(function (results) {
- const data = {};
- keys.forEach(function (k, i) {
- data[k] = results[i];
- });
- return data;
- });
- },
-
- // https://github.com/sindresorhus/is-absolute-url
- isAbsoluteUrlRE: /^[a-zA-Z][a-zA-Z\d+\-.]*:/,
- isWinPathRE: /^[a-zA-Z]:\\/,
- isAbsoluteUrl: function (url) {
- if (utils.isWinPathRE.test(url)) {
- return false;
- }
- return utils.isAbsoluteUrlRE.test(url);
- },
-
- isRelativeUrl: function (url) {
- return !utils.isAbsoluteUrl(url);
- },
-
- makeNumberHumanReadable: function (num) {
- const n = parseInt(num, 10);
- if (!n) {
- return num;
- }
- if (n > 999999) {
- return (n / 1000000).toFixed(1) + 'm';
- } else if (n > 999) {
- return (n / 1000).toFixed(1) + 'k';
- }
- return n;
- },
-
- // takes a string like 1000 and returns 1,000
- addCommas: function (text) {
- return String(text).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
- },
-
- toISOString: function (timestamp) {
- if (!timestamp || !Date.prototype.toISOString) {
- return '';
- }
-
- // Prevent too-high values to be passed to Date object
- timestamp = Math.min(timestamp, 8640000000000000);
-
- try {
- return new Date(parseInt(timestamp, 10)).toISOString();
- } catch (e) {
- return timestamp;
- }
- },
-
- tags: ['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont',
- 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
- 'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
- 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
- 'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
- 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
- 'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
- 'small', 'source', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
- 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'],
-
- stripTags: ['abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'base', 'basefont',
- 'bdi', 'bdo', 'big', 'blink', 'body', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
- 'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
- 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
- 'head', 'header', 'hr', 'html', 'iframe', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
- 'map', 'mark', 'marquee', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
- 'output', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
- 'source', 'span', 'strike', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
- 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'],
-
- escapeRegexChars: function (text) {
- return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
- },
-
- escapeHTML: function (str) {
- if (str == null) {
- return '';
- }
- if (!str) {
- return String(str);
- }
-
- return str.toString().replace(escapeChars, replaceChar);
- },
-
- isAndroidBrowser: function () {
- // http://stackoverflow.com/questions/9286355/how-to-detect-only-the-native-android-browser
- const nua = navigator.userAgent;
- return ((nua.indexOf('Mozilla/5.0') > -1 && nua.indexOf('Android ') > -1 && nua.indexOf('AppleWebKit') > -1) && !(nua.indexOf('Chrome') > -1));
- },
-
- isTouchDevice: function () {
- return 'ontouchstart' in document.documentElement;
- },
-
- findBootstrapEnvironment: function () {
- // http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api
- const envs = ['xs', 'sm', 'md', 'lg'];
- const $el = $('
');
-
- $el.appendTo($('body'));
-
- for (let i = envs.length - 1; i >= 0; i -= 1) {
- const env = envs[i];
-
- $el.addClass('hidden-' + env);
- if ($el.is(':hidden')) {
- $el.remove();
- return env;
- }
- }
- },
-
- isMobile: function () {
- const env = utils.findBootstrapEnvironment();
- return ['xs', 'sm'].some(function (targetEnv) {
- return targetEnv === env;
- });
- },
-
- getHoursArray: function () {
- const currentHour = new Date().getHours();
- const labels = [];
-
- for (let i = currentHour, ii = currentHour - 24; i > ii; i -= 1) {
- const hour = i < 0 ? 24 + i : i;
- labels.push(hour + ':00');
- }
-
- return labels.reverse();
- },
-
- getDaysArray: function (from, amount) {
- const currentDay = new Date(parseInt(from, 10) || Date.now()).getTime();
- const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
- const labels = [];
- let tmpDate;
-
- for (let x = (amount || 30) - 1; x >= 0; x -= 1) {
- tmpDate = new Date(currentDay - (1000 * 60 * 60 * 24 * x));
- labels.push(months[tmpDate.getMonth()] + ' ' + tmpDate.getDate());
- }
-
- return labels;
- },
-
- /* Retrieved from http://stackoverflow.com/a/7557433 @ 27 Mar 2016 */
- isElementInViewport: function (el) {
- // special bonus for those using jQuery
- if (typeof jQuery === 'function' && el instanceof jQuery) {
- el = el[0];
- }
-
- const rect = el.getBoundingClientRect();
-
- return (
- rect.top >= 0 &&
- rect.left >= 0 &&
- rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */
- rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */
- );
- },
-
- // get all the url params in a single key/value hash
- params: function (options = {}) {
- let url;
- if (options.url && !options.url.startsWith('http')) {
- // relative path passed in
- options.url = options.url.replace(new RegExp(`/?${config.relative_path.slice(1)}/`, 'g'), '');
- url = new URL(document.location);
- url.pathname = options.url;
- } else {
- url = new URL(options.url || document.location);
- }
- let params = url.searchParams;
-
- if (options.full) { // return URLSearchParams object
- return params;
- }
-
- // Handle arrays passed in query string (Object.fromEntries does not)
- const arrays = {};
- params.forEach((value, key) => {
- if (!key.endsWith('[]')) {
- return;
- }
-
- key = key.slice(0, -2);
- arrays[key] = arrays[key] || [];
- arrays[key].push(utils.toType(value));
- });
- Object.keys(arrays).forEach((key) => {
- params.delete(`${key}[]`);
- });
-
- // Backwards compatibility with v1.x -- all values passed through utils.toType()
- params = Object.fromEntries(params);
- Object.keys(params).forEach((key) => {
- params[key] = utils.toType(params[key]);
- });
-
- return { ...params, ...arrays };
- },
-
- param: function (key) {
- return this.params()[key];
- },
-
- urlToLocation: function (url) {
- const a = document.createElement('a');
- a.href = url;
- return a;
- },
-
- // return boolean if string 'true' or string 'false', or if a parsable string which is a number
- // also supports JSON object and/or arrays parsing
- toType: function (str) {
- const type = typeof str;
- if (type !== 'string') {
- return str;
- }
- const nb = parseFloat(str);
- if (!isNaN(nb) && isFinite(str)) {
- return nb;
- }
- if (str === 'false') {
- return false;
- }
- if (str === 'true') {
- return true;
- }
-
- try {
- str = JSON.parse(str);
- } catch (e) {}
-
- return str;
- },
-
- // Safely get/set chained properties on an object
- // set example: utils.props(A, 'a.b.c.d', 10) // sets A to {a: {b: {c: {d: 10}}}}, and returns 10
- // get example: utils.props(A, 'a.b.c') // returns {d: 10}
- // get example: utils.props(A, 'a.b.c.foo.bar') // returns undefined without throwing a TypeError
- // credits to github.com/gkindel
- props: function (obj, props, value) {
- if (obj === undefined) {
- obj = window;
- }
- if (props == null) {
- return undefined;
- }
- const i = props.indexOf('.');
- if (i === -1) {
- if (value !== undefined) {
- obj[props] = value;
- }
- return obj[props];
- }
- const prop = props.slice(0, i);
- const newProps = props.slice(i + 1);
-
- if (props !== undefined && !(obj[prop] instanceof Object)) {
- obj[prop] = {};
- }
-
- return utils.props(obj[prop], newProps, value);
- },
-
- isInternalURI: function (targetLocation, referenceLocation, relative_path) {
- return targetLocation.host === '' || // Relative paths are always internal links
- (
- targetLocation.host === referenceLocation.host &&
+ // https://github.com/substack/node-ent/blob/master/index.js
+ decodeHTMLEntities(html) {
+ return String(html)
+ .replaceAll(/(\d+);?/g, (_, code) => String.fromCharCode(code))
+ .replaceAll(/[xX]([A-Fa-f\d]+);?/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
+ .replaceAll(/&([^;\W]+;?)/g, (m, e) => {
+ const ee = e.replace(/;$/, '');
+ const target = HTMLEntities[e] || (e.match(/;$/) && HTMLEntities[ee]);
+
+ if (typeof target === 'number') {
+ return String.fromCharCode(target);
+ }
+
+ if (typeof target === 'string') {
+ return target;
+ }
+
+ return m;
+ });
+ },
+ // https://github.com/jprichardson/string.js/blob/master/lib/string.js
+ stripHTMLTags(string_, tags) {
+ const pattern = (tags || ['']).join('|');
+ return String(string_).replaceAll(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), '');
+ },
+
+ cleanUpTag(tag, maxLength) {
+ if (typeof tag !== 'string' || tag.length === 0) {
+ return '';
+ }
+
+ tag = tag.trim().toLowerCase();
+ // See https://github.com/NodeBB/NodeBB/issues/4378
+ tag = tag.replaceAll(/\u202E/gi, '');
+ tag = tag.replaceAll(/[,/#!$^*;:{}=_`<>'"~()?|]/g, '');
+ tag = tag.slice(0, maxLength || 15).trim();
+ const matches = tag.match(/^[.-]*(.+?)[.-]*$/);
+ if (matches && matches.length > 1) {
+ tag = matches[1];
+ }
+
+ return tag;
+ },
+
+ removePunctuation(string_) {
+ return string_.replaceAll(/[.,-/#!$%^&*;:{}=\-_`<>'"~()?]/g, '');
+ },
+
+ isEmailValid(email) {
+ return typeof email === 'string' && email.length && email.includes('@') && !email.includes(',') && !email.includes(';');
+ },
+
+ isUserNameValid(name) {
+ return (name && name !== '' && (/^['" \-+.*[\]\d\u00BF-\u1FFF\u2C00-\uD7FF\w]+$/.test(name)));
+ },
+
+ isPasswordValid(password) {
+ return typeof password === 'string' && password.length;
+ },
+
+ isNumber(n) {
+ // `isFinite('') === true` so isNan parseFloat check is necessary
+ return !isNaN(Number.parseFloat(n)) && isFinite(n);
+ },
+
+ languageKeyRegex: /\[\[\w+:.+]]/,
+ hasLanguageKey(input) {
+ return utils.languageKeyRegex.test(input);
+ },
+ userLangToTimeagoCode(userLang) {
+ const mapping = {
+ 'en-GB': 'en',
+ 'en-US': 'en',
+ 'fa-IR': 'fa',
+ 'pt-BR': 'pt-br',
+ nb: 'no',
+ };
+ return mapping.hasOwnProperty(userLang) ? mapping[userLang] : userLang;
+ },
+ // Shallow objects merge
+ merge() {
+ const result = {};
+ let object;
+ let keys;
+ for (const argument of arguments) {
+ object = argument || {};
+ keys = Object.keys(object);
+ for (const key of keys) {
+ result[key] = object[key];
+ }
+ }
+
+ return result;
+ },
+
+ fileExtension(path) {
+ return (String(path)).split('.').pop();
+ },
+
+ extensionMimeTypeMap: {
+ bmp: 'image/bmp',
+ cmx: 'image/x-cmx',
+ cod: 'image/cis-cod',
+ gif: 'image/gif',
+ ico: 'image/x-icon',
+ ief: 'image/ief',
+ jfif: 'image/pipeg',
+ jpe: 'image/jpeg',
+ jpeg: 'image/jpeg',
+ jpg: 'image/jpeg',
+ png: 'image/png',
+ pbm: 'image/x-portable-bitmap',
+ pgm: 'image/x-portable-graymap',
+ pnm: 'image/x-portable-anymap',
+ ppm: 'image/x-portable-pixmap',
+ ras: 'image/x-cmu-raster',
+ rgb: 'image/x-rgb',
+ svg: 'image/svg+xml',
+ tif: 'image/tiff',
+ tiff: 'image/tiff',
+ xbm: 'image/x-xbitmap',
+ xpm: 'image/x-xpixmap',
+ xwd: 'image/x-xwindowdump',
+ },
+
+ fileMimeType(path) {
+ return utils.extensionToMimeType(utils.fileExtension(path));
+ },
+
+ extensionToMimeType(extension) {
+ return utils.extensionMimeTypeMap.hasOwnProperty(extension) ? utils.extensionMimeTypeMap[extension] : '*';
+ },
+
+ isPromise(object) {
+ // https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise#comment97339131_27746324
+ return object && typeof object.then === 'function';
+ },
+
+ promiseParallel(object) {
+ const keys = Object.keys(object);
+ return Promise.all(
+ keys.map(k => object[k]),
+ ).then(results => {
+ const data = {};
+ for (const [i, k] of keys.entries()) {
+ data[k] = results[i];
+ }
+
+ return data;
+ });
+ },
+
+ // https://github.com/sindresorhus/is-absolute-url
+ isAbsoluteUrlRE: /^[a-zA-Z][a-zA-Z\d+\-.]*:/,
+ isWinPathRE: /^[a-zA-Z]:\\/,
+ isAbsoluteUrl(url) {
+ if (utils.isWinPathRE.test(url)) {
+ return false;
+ }
+
+ return utils.isAbsoluteUrlRE.test(url);
+ },
+
+ isRelativeUrl(url) {
+ return !utils.isAbsoluteUrl(url);
+ },
+
+ makeNumberHumanReadable(number_) {
+ const n = Number.parseInt(number_, 10);
+ if (!n) {
+ return number_;
+ }
+
+ if (n > 999_999) {
+ return (n / 1_000_000).toFixed(1) + 'm';
+ }
+
+ if (n > 999) {
+ return (n / 1000).toFixed(1) + 'k';
+ }
+
+ return n;
+ },
+
+ // Takes a string like 1000 and returns 1,000
+ addCommas(text) {
+ return String(text).replaceAll(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
+ },
+
+ toISOString(timestamp) {
+ if (!timestamp || !Date.prototype.toISOString) {
+ return '';
+ }
+
+ // Prevent too-high values to be passed to Date object
+ timestamp = Math.min(timestamp, 8_640_000_000_000_000);
+
+ try {
+ return new Date(Number.parseInt(timestamp, 10)).toISOString();
+ } catch {
+ return timestamp;
+ }
+ },
+
+ tags: ['a',
+ 'abbr',
+ 'acronym',
+ 'address',
+ 'applet',
+ 'area',
+ 'article',
+ 'aside',
+ 'audio',
+ 'b',
+ 'base',
+ 'basefont',
+ 'bdi',
+ 'bdo',
+ 'big',
+ 'blockquote',
+ 'body',
+ 'br',
+ 'button',
+ 'canvas',
+ 'caption',
+ 'center',
+ 'cite',
+ 'code',
+ 'col',
+ 'colgroup',
+ 'command',
+ 'datalist',
+ 'dd',
+ 'del',
+ 'details',
+ 'dfn',
+ 'dialog',
+ 'dir',
+ 'div',
+ 'dl',
+ 'dt',
+ 'em',
+ 'embed',
+ 'fieldset',
+ 'figcaption',
+ 'figure',
+ 'font',
+ 'footer',
+ 'form',
+ 'frame',
+ 'frameset',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'head',
+ 'header',
+ 'hr',
+ 'html',
+ 'i',
+ 'iframe',
+ 'img',
+ 'input',
+ 'ins',
+ 'kbd',
+ 'keygen',
+ 'label',
+ 'legend',
+ 'li',
+ 'link',
+ 'map',
+ 'mark',
+ 'menu',
+ 'meta',
+ 'meter',
+ 'nav',
+ 'noframes',
+ 'noscript',
+ 'object',
+ 'ol',
+ 'optgroup',
+ 'option',
+ 'output',
+ 'p',
+ 'param',
+ 'pre',
+ 'progress',
+ 'q',
+ 'rp',
+ 'rt',
+ 'ruby',
+ 's',
+ 'samp',
+ 'script',
+ 'section',
+ 'select',
+ 'small',
+ 'source',
+ 'span',
+ 'strike',
+ 'strong',
+ 'style',
+ 'sub',
+ 'summary',
+ 'sup',
+ 'table',
+ 'tbody',
+ 'td',
+ 'textarea',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'time',
+ 'title',
+ 'tr',
+ 'track',
+ 'tt',
+ 'u',
+ 'ul',
+ 'const',
+ 'video',
+ 'wbr'],
+
+ stripTags: ['abbr',
+ 'acronym',
+ 'address',
+ 'applet',
+ 'area',
+ 'article',
+ 'aside',
+ 'audio',
+ 'base',
+ 'basefont',
+ 'bdi',
+ 'bdo',
+ 'big',
+ 'blink',
+ 'body',
+ 'button',
+ 'canvas',
+ 'caption',
+ 'center',
+ 'cite',
+ 'code',
+ 'col',
+ 'colgroup',
+ 'command',
+ 'datalist',
+ 'dd',
+ 'del',
+ 'details',
+ 'dfn',
+ 'dialog',
+ 'dir',
+ 'div',
+ 'dl',
+ 'dt',
+ 'em',
+ 'embed',
+ 'fieldset',
+ 'figcaption',
+ 'figure',
+ 'font',
+ 'footer',
+ 'form',
+ 'frame',
+ 'frameset',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'head',
+ 'header',
+ 'hr',
+ 'html',
+ 'iframe',
+ 'input',
+ 'ins',
+ 'kbd',
+ 'keygen',
+ 'label',
+ 'legend',
+ 'li',
+ 'link',
+ 'map',
+ 'mark',
+ 'marquee',
+ 'menu',
+ 'meta',
+ 'meter',
+ 'nav',
+ 'noframes',
+ 'noscript',
+ 'object',
+ 'ol',
+ 'optgroup',
+ 'option',
+ 'output',
+ 'param',
+ 'pre',
+ 'progress',
+ 'q',
+ 'rp',
+ 'rt',
+ 'ruby',
+ 's',
+ 'samp',
+ 'script',
+ 'section',
+ 'select',
+ 'source',
+ 'span',
+ 'strike',
+ 'style',
+ 'sub',
+ 'summary',
+ 'sup',
+ 'table',
+ 'tbody',
+ 'td',
+ 'textarea',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'time',
+ 'title',
+ 'tr',
+ 'track',
+ 'tt',
+ 'u',
+ 'ul',
+ 'const',
+ 'video',
+ 'wbr'],
+
+ escapeRegexChars(text) {
+ return text.replaceAll(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+ },
+
+ escapeHTML(string_) {
+ if (string_ == null) {
+ return '';
+ }
+
+ if (!string_) {
+ return String(string_);
+ }
+
+ return string_.toString().replaceAll(escapeChars, replaceChar);
+ },
+
+ isAndroidBrowser() {
+ // http://stackoverflow.com/questions/9286355/how-to-detect-only-the-native-android-browser
+ const nua = navigator.userAgent;
+ return ((nua.includes('Mozilla/5.0') && nua.includes('Android ') && nua.includes('AppleWebKit')) && !(nua.includes('Chrome')));
+ },
+
+ isTouchDevice() {
+ return 'ontouchstart' in document.documentElement;
+ },
+
+ findBootstrapEnvironment() {
+ // http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api
+ const environments = ['xs', 'sm', 'md', 'lg'];
+ const $element = $('
');
+
+ $element.appendTo($('body'));
+
+ for (let i = environments.length - 1; i >= 0; i -= 1) {
+ const env = environments[i];
+
+ $element.addClass('hidden-' + env);
+ if ($element.is(':hidden')) {
+ $element.remove();
+ return env;
+ }
+ }
+ },
+
+ isMobile() {
+ const env = utils.findBootstrapEnvironment();
+ return ['xs', 'sm'].includes(env);
+ },
+
+ getHoursArray() {
+ const currentHour = new Date().getHours();
+ const labels = [];
+
+ for (let i = currentHour, ii = currentHour - 24; i > ii; i -= 1) {
+ const hour = i < 0 ? 24 + i : i;
+ labels.push(hour + ':00');
+ }
+
+ return labels.reverse();
+ },
+
+ getDaysArray(from, amount) {
+ const currentDay = new Date(Number.parseInt(from, 10) || Date.now()).getTime();
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ const labels = [];
+ let temporaryDate;
+
+ for (let x = (amount || 30) - 1; x >= 0; x -= 1) {
+ temporaryDate = new Date(currentDay - (1000 * 60 * 60 * 24 * x));
+ labels.push(months[temporaryDate.getMonth()] + ' ' + temporaryDate.getDate());
+ }
+
+ return labels;
+ },
+
+ /* Retrieved from http://stackoverflow.com/a/7557433 @ 27 Mar 2016 */
+ isElementInViewport(element) {
+ // Special bonus for those using jQuery
+ if (typeof jQuery === 'function' && element instanceof jQuery) {
+ element = element[0];
+ }
+
+ const rect = element.getBoundingClientRect();
+
+ return (
+ rect.top >= 0
+ && rect.left >= 0
+ && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) /* Or $(window).height() */
+ && rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* Or $(window).width() */
+ );
+ },
+
+ // Get all the url params in a single key/value hash
+ params(options = {}) {
+ let url;
+ if (options.url && !options.url.startsWith('http')) {
+ // Relative path passed in
+ options.url = options.url.replaceAll(new RegExp(`/?${config.relative_path.slice(1)}/`, 'g'), '');
+ url = new URL(document.location);
+ url.pathname = options.url;
+ } else {
+ url = new URL(options.url || document.location);
+ }
+
+ let parameters = url.searchParams;
+
+ if (options.full) { // Return URLSearchParams object
+ return parameters;
+ }
+
+ // Handle arrays passed in query string (Object.fromEntries does not)
+ const arrays = {};
+ for (let [key, value] of parameters.entries()) {
+ if (!key.endsWith('[]')) {
+ continue;
+ }
+
+ key = key.slice(0, -2);
+ arrays[key] = arrays[key] || [];
+ arrays[key].push(utils.toType(value));
+ }
+
+ for (const key of Object.keys(arrays)) {
+ parameters.delete(`${key}[]`);
+ }
+
+ // Backwards compatibility with v1.x -- all values passed through utils.toType()
+ parameters = Object.fromEntries(parameters);
+ for (const key of Object.keys(parameters)) {
+ parameters[key] = utils.toType(parameters[key]);
+ }
+
+ return {...parameters, ...arrays};
+ },
+
+ param(key) {
+ return this.params()[key];
+ },
+
+ urlToLocation(url) {
+ const a = document.createElement('a');
+ a.href = url;
+ return a;
+ },
+
+ // Return boolean if string 'true' or string 'false', or if a parsable string which is a number
+ // also supports JSON object and/or arrays parsing
+ toType(string_) {
+ const type = typeof string_;
+ if (type !== 'string') {
+ return string_;
+ }
+
+ const nb = Number.parseFloat(string_);
+ if (!isNaN(nb) && isFinite(string_)) {
+ return nb;
+ }
+
+ if (string_ === 'false') {
+ return false;
+ }
+
+ if (string_ === 'true') {
+ return true;
+ }
+
+ try {
+ string_ = JSON.parse(string_);
+ } catch {}
+
+ return string_;
+ },
+
+ // Safely get/set chained properties on an object
+ // set example: utils.props(A, 'a.b.c.d', 10) // sets A to {a: {b: {c: {d: 10}}}}, and returns 10
+ // get example: utils.props(A, 'a.b.c') // returns {d: 10}
+ // get example: utils.props(A, 'a.b.c.foo.bar') // returns undefined without throwing a TypeError
+ // credits to github.com/gkindel
+ props(object, properties, value) {
+ if (object === undefined) {
+ object = window;
+ }
+
+ if (properties == null) {
+ return undefined;
+ }
+
+ const i = properties.indexOf('.');
+ if (i === -1) {
+ if (value !== undefined) {
+ object[properties] = value;
+ }
+
+ return object[properties];
+ }
+
+ const property = properties.slice(0, i);
+ const newProperties = properties.slice(i + 1);
+
+ if (properties !== undefined && !(object[property] instanceof Object)) {
+ object[property] = {};
+ }
+
+ return utils.props(object[property], newProperties, value);
+ },
+
+ isInternalURI(targetLocation, referenceLocation, relative_path) {
+ return targetLocation.host === '' // Relative paths are always internal links
+ || (
+ targetLocation.host === referenceLocation.host
// Otherwise need to check if protocol and host match
- targetLocation.protocol === referenceLocation.protocol &&
+ && targetLocation.protocol === referenceLocation.protocol
// Subfolder installs need this additional check
- (relative_path.length > 0 ? targetLocation.pathname.indexOf(relative_path) === 0 : true)
+ && (relative_path.length > 0 ? targetLocation.pathname.indexOf(relative_path) === 0 : true)
);
- },
-
- rtrim: function (str) {
- return str.replace(/\s+$/g, '');
- },
-
- debounce: function (func, wait, immediate) {
- // modified from https://davidwalsh.name/javascript-debounce-function
- let timeout;
- return function () {
- const context = this;
- const args = arguments;
- const later = function () {
- timeout = null;
- if (!immediate) {
- func.apply(context, args);
- }
- };
- const callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) {
- func.apply(context, args);
- }
- };
- },
- throttle: function (func, wait, immediate) {
- let timeout;
- return function () {
- const context = this;
- const args = arguments;
- const later = function () {
- timeout = null;
- if (!immediate) {
- func.apply(context, args);
- }
- };
- const callNow = immediate && !timeout;
- if (!timeout) {
- timeout = setTimeout(later, wait);
- }
- if (callNow) {
- func.apply(context, args);
- }
- };
- },
+ },
+
+ rtrim(string_) {
+ return string_.replaceAll(/\s+$/g, '');
+ },
+
+ debounce(function_, wait, immediate) {
+ // Modified from https://davidwalsh.name/javascript-debounce-function
+ let timeout;
+ return function () {
+ const context = this;
+ const arguments_ = arguments;
+ const later = function () {
+ timeout = null;
+ if (!immediate) {
+ function_.apply(context, arguments_);
+ }
+ };
+
+ const callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ function_.apply(context, arguments_);
+ }
+ };
+ },
+ throttle(function_, wait, immediate) {
+ let timeout;
+ return function () {
+ const context = this;
+ const arguments_ = arguments;
+ const later = function () {
+ timeout = null;
+ if (!immediate) {
+ function_.apply(context, arguments_);
+ }
+ };
+
+ const callNow = immediate && !timeout;
+ timeout ||= setTimeout(later, wait);
+
+ if (callNow) {
+ function_.apply(context, arguments_);
+ }
+ };
+ },
};
module.exports = utils;
diff --git a/public/src/utils.js b/public/src/utils.js
index fbb1695..ae856ec 100644
--- a/public/src/utils.js
+++ b/public/src/utils.js
@@ -1,83 +1,80 @@
-/* eslint-disable no-redeclare */
'use strict';
const $ = require('jquery');
-const utils = { ...require('./utils.common') };
+const utils = {...require('./utils.common')};
utils.getLanguage = function () {
- let lang = 'en-GB';
- if (typeof window === 'object' && window.config && window.utils) {
- lang = utils.params().lang || config.userLang || config.defaultLang || 'en-GB';
- }
- return lang;
-};
+ let lang = 'en-GB';
+ if (typeof window === 'object' && window.config && window.utils) {
+ lang = utils.params().lang || config.userLang || config.defaultLang || 'en-GB';
+ }
+ return lang;
+};
utils.makeNumbersHumanReadable = function (elements) {
- elements.each(function () {
- $(this)
- .html(utils.makeNumberHumanReadable($(this).attr('title')))
- .removeClass('hidden');
- });
+ elements.each(function () {
+ $(this)
+ .html(utils.makeNumberHumanReadable($(this).attr('title')))
+ .removeClass('hidden');
+ });
};
utils.addCommasToNumbers = function (elements) {
- elements.each(function (index, element) {
- $(element)
- .html(utils.addCommas($(element).html()))
- .removeClass('hidden');
- });
+ elements.each((index, element) => {
+ $(element)
+ .html(utils.addCommas($(element).html()))
+ .removeClass('hidden');
+ });
};
utils.findBootstrapEnvironment = function () {
- // http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api
- const envs = ['xs', 'sm', 'md', 'lg'];
- const $el = $('
');
+ // http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api
+ const environments = ['xs', 'sm', 'md', 'lg'];
+ const $element = $('
');
- $el.appendTo($('body'));
+ $element.appendTo($('body'));
- for (let i = envs.length - 1; i >= 0; i -= 1) {
- const env = envs[i];
+ for (let i = environments.length - 1; i >= 0; i -= 1) {
+ const env = environments[i];
- $el.addClass('hidden-' + env);
- if ($el.is(':hidden')) {
- $el.remove();
- return env;
- }
- }
+ $element.addClass('hidden-' + env);
+ if ($element.is(':hidden')) {
+ $element.remove();
+ return env;
+ }
+ }
};
utils.isMobile = function () {
- const env = utils.findBootstrapEnvironment();
- return ['xs', 'sm'].some(function (targetEnv) {
- return targetEnv === env;
- });
+ const env = utils.findBootstrapEnvironment();
+ return ['xs', 'sm'].includes(env);
};
utils.assertPasswordValidity = (password, zxcvbn) => {
- // More checks on top of basic utils.isPasswordValid()
- if (!utils.isPasswordValid(password)) {
- throw new Error('[[user:change_password_error]]');
- } else if (password.length < ajaxify.data.minimumPasswordLength) {
- throw new Error('[[reset_password:password_too_short]]');
- } else if (password.length > 512) {
- throw new Error('[[error:password-too-long]]');
- }
-
- const passwordStrength = zxcvbn(password);
- if (passwordStrength.score < ajaxify.data.minimumPasswordStrength) {
- throw new Error('[[user:weak_password]]');
- }
+ // More checks on top of basic utils.isPasswordValid()
+ if (!utils.isPasswordValid(password)) {
+ throw new Error('[[user:change_password_error]]');
+ } else if (password.length < ajaxify.data.minimumPasswordLength) {
+ throw new Error('[[reset_password:password_too_short]]');
+ } else if (password.length > 512) {
+ throw new Error('[[error:password-too-long]]');
+ }
+
+ const passwordStrength = zxcvbn(password);
+ if (passwordStrength.score < ajaxify.data.minimumPasswordStrength) {
+ throw new Error('[[user:weak_password]]');
+ }
};
utils.generateUUID = function () {
- // from https://github.com/tracker1/node-uuid4/blob/master/browser.js
- const temp_url = URL.createObjectURL(new Blob());
- const uuid = temp_url.toString();
- URL.revokeObjectURL(temp_url);
- return uuid.split(/[:/]/g).pop().toLowerCase(); // remove prefixes
+ // From https://github.com/tracker1/node-uuid4/blob/master/browser.js
+ const temporary_url = URL.createObjectURL(new Blob());
+ const uuid = temporary_url.toString();
+ URL.revokeObjectURL(temporary_url);
+ return uuid.split(/[:/]/g).pop().toLowerCase(); // Remove prefixes
};
module.exports = utils;
diff --git a/public/src/widgets.js b/public/src/widgets.js
index 7ad6522..6c9bcbf 100644
--- a/public/src/widgets.js
+++ b/public/src/widgets.js
@@ -1,52 +1,52 @@
'use strict';
module.exports.render = function (template) {
- if (template.match(/^admin/)) {
- return;
- }
-
- const locations = Object.keys(ajaxify.data.widgets);
-
- locations.forEach(function (location) {
- let area = $('#content [widget-area="' + location + '"],#content [data-widget-area="' + location + '"]').eq(0);
- if (area.length) {
- return;
- }
-
- const widgetsAtLocation = ajaxify.data.widgets[location] || [];
- let html = '';
-
- widgetsAtLocation.forEach(function (widget) {
- html += widget.html;
- });
-
- if (location === 'footer' && !$('#content [widget-area="footer"],#content [data-widget-area="footer"]').length) {
- $('#content').append($('
'));
- } else if (location === 'sidebar' && !$('#content [widget-area="sidebar"],#content [data-widget-area="sidebar"]').length) {
- if ($('[component="account/cover"]').length) {
- $('[component="account/cover"]').nextAll().wrapAll($('
'));
- } else if ($('[component="groups/cover"]').length) {
- $('[component="groups/cover"]').nextAll().wrapAll($('
'));
- } else {
- $('#content > *').wrapAll($('
'));
- }
- } else if (location === 'header' && !$('#content [widget-area="header"],#content [data-widget-area="header"]').length) {
- $('#content').prepend($('
'));
- }
-
- area = $('#content [widget-area="' + location + '"],#content [data-widget-area="' + location + '"]').eq(0);
- if (html && area.length) {
- area.html(html);
- area.find('img:not(.not-responsive)').addClass('img-responsive');
- }
-
- if (widgetsAtLocation.length) {
- area.removeClass('hidden');
- }
- });
-
- require(['hooks'], function (hooks) {
- hooks.fire('action:widgets.loaded', {});
- });
+ if (template.startsWith('admin')) {
+ return;
+ }
+
+ const locations = Object.keys(ajaxify.data.widgets);
+
+ for (const location of locations) {
+ let area = $('#content [widget-area="' + location + '"],#content [data-widget-area="' + location + '"]').eq(0);
+ if (area.length > 0) {
+ continue;
+ }
+
+ const widgetsAtLocation = ajaxify.data.widgets[location] || [];
+ let html = '';
+
+ for (const widget of widgetsAtLocation) {
+ html += widget.html;
+ }
+
+ if (location === 'footer' && $('#content [widget-area="footer"],#content [data-widget-area="footer"]').length === 0) {
+ $('#content').append($('
'));
+ } else if (location === 'sidebar' && $('#content [widget-area="sidebar"],#content [data-widget-area="sidebar"]').length === 0) {
+ if ($('[component="account/cover"]').length > 0) {
+ $('[component="account/cover"]').nextAll().wrapAll($('
'));
+ } else if ($('[component="groups/cover"]').length > 0) {
+ $('[component="groups/cover"]').nextAll().wrapAll($('