nodebb-plugin-poll 3.2.9 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,203 +1,240 @@
1
1
  'use strict';
2
2
 
3
3
  (function (Poll) {
4
- var Creator = {};
4
+ const Creator = {};
5
5
 
6
6
  function init() {
7
7
  $(window).on('action:composer.enhanced', initComposer);
8
+ }
8
9
 
9
- $(window).on('action:redactor.load', initRedactor);
10
+ function initComposer(ev, { postContainer, postData }) {
11
+ require(['composer', 'composer/formatting'], function (composer, formatting) {
12
+ if (formatting) {
13
+ formatting.addButtonDispatch('poll', function (/* textarea */) {
14
+ onPollButtonClicked(composer);
15
+ });
10
16
 
11
- $(window).on('action:composer.loaded', function (ev, data) {
12
- if ($.Redactor) {
13
- if (data.composerData.isMain && $.Redactor.opts.plugins.indexOf('poll') === -1) {
14
- $.Redactor.opts.plugins.push('poll');
15
- } else if (!data.composerData.isMain && $.Redactor.opts.plugins.indexOf('poll') !== -1) {
16
- $.Redactor.opts.plugins.splice($.Redactor.opts.plugins.indexOf('poll'), 1);
17
- }
17
+ updateComposerBadgeCount(postContainer, postData.polls ? postData.polls.length : 0);
18
18
  }
19
19
  });
20
20
  }
21
21
 
22
- function initComposer() {
23
- require(['composer', 'composer/formatting', 'composer/controls'], function (composer, formatting, controls) {
24
- if (formatting && controls) {
25
- formatting.addButtonDispatch('poll', function (textarea) {
26
- composerBtnHandle(composer, textarea);
27
- });
28
- }
22
+ async function onPollButtonClicked(composer) {
23
+ const postData = composer.posts[composer.active];
24
+ if (!postData) {
25
+ return showAlert('error', '[[poll:error.invalid-post]]');
26
+ }
27
+ if (parseInt(postData.cid, 10) === 0) {
28
+ return showAlert('error', '[[error:category-not-selected]]');
29
+ }
30
+
31
+ const canCreate = await socket.emit('plugins.poll.canCreate', {
32
+ cid: postData.cid,
33
+ pid: postData.pid,
34
+ tid: postData.tid,
29
35
  });
36
+ if (!canCreate) {
37
+ return showAlert('error', '[[error:no-privileges]]');
38
+ }
39
+ postData.polls = postData.polls || [];
40
+ await openManageModal({ postData, uuid: composer.active });
30
41
  }
31
42
 
32
- function initRedactor() {
33
- $.Redactor.prototype.poll = function () {
34
- return {
35
- init: function () {
36
- var self = this;
37
-
38
- // require translator as such because it was undefined without it
39
- require(['translator'], function (translator) {
40
- translator.translate('[[poll:creator_title]]', function (translated) {
41
- var button = self.button.add('poll', translated);
42
- self.button.setIcon(button, '<i class="fa fa-bar-chart-o"></i>');
43
- self.button.addCallback(button, self.poll.onClick);
44
- });
45
- });
46
- },
47
- onClick: function () {
48
- var self = this;
49
- var code = this.code.get();
50
- require(['composer'], function (composer) {
51
- composerBtnHandle(composer, {
52
- value: code,
53
- redactor: function (code) {
54
- self.code.set(code);
55
- },
56
- });
57
- });
43
+ async function openManageModal(payload) {
44
+ const bootbox = await app.require('bootbox');
45
+ const { postData, modal, uuid } = payload;
46
+ const html = await app.parseAndTranslate('poll/manage', { post: postData });
47
+
48
+ if (modal) {
49
+ modal.find('.bootbox-body').html(html[0].outerHTML);
50
+ handleSort({ modal, polls: postData.polls });
51
+ } else {
52
+ const modal = bootbox.dialog({
53
+ title: '[[poll:manage-polls]]',
54
+ message: html,
55
+ className: 'poll-manage',
56
+ buttons: {
57
+ add: {
58
+ label: '<i class="fa fa-plus"></i> [[poll:add-poll]]',
59
+ className: 'btn-success',
60
+ callback: () => {
61
+ Creator.show('[[poll:creator_title]]', {
62
+ // temp id, will be replaced in the backend before saving
63
+ // used to identify the poll in the manage modal after creation
64
+ // so we can edit/delete
65
+ pollId: Date.now(),
66
+ maximumVotesPerUser: config.poll.maximumVotesPerUser,
67
+ }, config.poll).then((newPoll) => {
68
+ if (newPoll) {
69
+ postData.polls.push(newPoll);
70
+ }
71
+ openManageModal({ ...payload, modal });
72
+ const postContainer = $(`[component="composer"][data-uuid="${uuid}"]`);
73
+ updateComposerBadgeCount(postContainer, postData.polls.length);
74
+ });
75
+ return false;
76
+ },
77
+ },
78
+ close: {
79
+ label: '[[global:close]]',
80
+ className: 'btn-primary',
81
+ },
58
82
  },
59
- };
60
- };
83
+ });
84
+ handleDelete({ ...payload, modal });
85
+ handleEdit({ ...payload, modal });
86
+ handleSort({ modal, polls: postData.polls });
87
+ }
61
88
  }
62
89
 
63
- function composerBtnHandle(composer, textarea) {
64
- require(['composer/controls', 'alerts'], function (controls, alerts) {
65
- var post = composer.posts[composer.active];
66
- if (!post || !post.isMain || (isNaN(parseInt(post.cid, 10)) && isNaN(parseInt(post.pid, 10)))) {
67
- return alerts.error('[[poll:error.not_main]]');
68
- }
69
- if (parseInt(post.cid, 10) === 0) {
70
- return alerts.error('[[error:category-not-selected]]');
71
- }
72
-
73
- socket.emit('plugins.poll.canCreate', {
74
- cid: post.cid,
75
- pid: post.pid,
76
- }, function (err, canCreate) {
77
- if (err) {
78
- return alerts.error(err);
90
+ async function handleDelete(payload) {
91
+ const bootbox = await app.require('bootbox');
92
+ const { uuid, modal, postData } = payload;
93
+ modal.on('click', 'button[data-action="remove"]', function () {
94
+ const clickedPollId = $(this).attr('data-poll-id');
95
+ bootbox.confirm('[[poll:confirm-remove]]', (ok) => {
96
+ if (!ok) {
97
+ return;
79
98
  }
80
- if (!canCreate) {
81
- return alerts.error('[[error:no-privileges]]');
99
+ const poll = postData?.polls.find(poll => String(poll.pollId) === clickedPollId);
100
+ if (poll) {
101
+ postData.polls = postData.polls.filter(poll => String(poll.pollId) !== clickedPollId);
102
+ openManageModal(payload);
103
+ const postContainer = $(`[component="composer"][data-uuid="${uuid}"]`);
104
+ updateComposerBadgeCount(postContainer, postData.polls.length);
82
105
  }
83
- socket.emit('plugins.poll.getConfig', null, function (err, config) {
84
- if (err) {
85
- return alerts.error(err);
86
- }
87
-
88
- var poll = {};
106
+ });
107
+ });
108
+ };
89
109
 
90
- // If there's already a poll in the post, serialize it for editing
91
- if (Poll.serializer.canSerialize(textarea.value)) {
92
- poll = Poll.serializer.serialize(textarea.value, config);
110
+ function handleEdit(payload) {
111
+ const { modal, postData } = payload;
112
+ modal.on('click', 'button[data-action="edit"]', async function () {
113
+ const clickedPollId = $(this).attr('data-poll-id');
114
+ const poll = postData?.polls.find(poll => String(poll.pollId) === clickedPollId);
115
+ if (poll) {
116
+ const editedPoll = await Creator.show(
117
+ '[[poll:edit-a-poll-title]]',
118
+ poll,
119
+ config.poll,
120
+ );
121
+ if (editedPoll) {
122
+ postData.polls = postData.polls.map(
123
+ p => String(p.pollId) === String(editedPoll.pollId) ? editedPoll : p
124
+ );
125
+ openManageModal(payload);
126
+ }
127
+ }
128
+ });
129
+ }
93
130
 
94
- if (poll.settings.end === 0) {
95
- delete poll.settings.end;
96
- } else {
97
- poll.settings.end = parseInt(poll.settings.end, 10);
98
- }
131
+ function handleSort({ modal, polls }) {
132
+ if (Array.isArray(polls) && polls.length > 1) {
133
+ const selectorEl = modal.find('.topic-poll-modal');
134
+ selectorEl.sortable({
135
+ handle: '[component="sort/handle"]',
136
+ axis: 'y',
137
+ zIndex: 9999,
138
+ items: '[component="post/poll/item"]',
139
+ });
140
+ selectorEl.on('sortupdate', function () {
141
+ if (!polls) return;
142
+ const newOrder = [];
143
+ selectorEl.find('[component="post/poll/item"]').each(function () {
144
+ const pollId = $(this).attr('data-poll-id');
145
+ const poll = polls.find(p => String(p.pollId) === pollId);
146
+ if (poll) {
147
+ newOrder.push(poll);
99
148
  }
100
-
101
- Creator.show(poll, config, function (data) {
102
- // Anything invalid will be discarded by the serializer
103
- var markup = Poll.serializer.deserialize(data, config);
104
-
105
- // Remove any existing poll markup
106
- textarea.value = Poll.serializer.removeMarkup(textarea.value);
107
-
108
- // Insert the poll markup at the bottom
109
- if (textarea.value.charAt(textarea.value.length - 1) !== '\n') {
110
- markup = '\n' + markup;
111
- }
112
-
113
- if ($.Redactor) {
114
- textarea.redactor(textarea.value + '<p>' + markup + '</p>');
115
- } else {
116
- controls.insertIntoTextarea(textarea, markup);
117
- }
118
- });
119
149
  });
150
+ // Mutate polls array in place
151
+ polls.length = 0;
152
+ Array.prototype.push.apply(polls, newOrder);
120
153
  });
121
- });
122
- }
123
-
124
- Creator.show = function (poll, config, callback) {
125
- if (poll.hasOwnProperty('info')) {
126
- return Poll.alertError('Editing not implemented');
127
154
  }
155
+ };
128
156
 
129
- require(['bootbox'], function (bootbox) {
130
- app.parseAndTranslate('poll/creator', { poll: poll, config: config, isRedactor: !!$.Redactor }, function (html) {
131
- // Initialise modal
132
- var modal = bootbox.dialog({
133
- title: '[[poll:creator_title]]',
134
- message: html,
135
- className: 'poll-creator',
136
- buttons: {
137
- cancel: {
138
- label: '[[modules:bootbox.cancel]]',
139
- callback: function () {
140
- return true;
157
+ Creator.show = function (modalTitle, poll, config) {
158
+ return new Promise((resolve) => {
159
+ require(['bootbox'], function (bootbox) {
160
+ app.parseAndTranslate('poll/creator', { poll, config }, function (html) {
161
+ const modal = bootbox.dialog({
162
+ title: modalTitle,
163
+ message: html,
164
+ className: 'poll-creator',
165
+ buttons: {
166
+ cancel: {
167
+ label: '[[global:close]]',
168
+ callback: function () {
169
+ resolve(null);
170
+ },
141
171
  },
142
- },
143
- save: {
144
- label: '[[modules:bootbox.confirm]]',
145
- callback: function (e) {
146
- clearErrors();
147
- var form = $(e.currentTarget).parents('.bootbox').find('#pollCreator');
148
- var obj = serializeObjectFromForm(form);
149
-
150
- // Let's be nice and at least show an error if there are no options
151
- obj.options.filter(function (obj) {
152
- return obj.length;
153
- });
154
-
155
- if (obj.options.length === 0) {
156
- return error('[[poll:error.no_options]]');
157
- }
158
-
159
- if (obj.settings.end) {
160
- obj.settings.end = new Date(obj.settings.end).getTime();
161
- }
162
- callback(obj);
163
- return true;
172
+ save: {
173
+ label: '[[modules:bootbox.confirm]]',
174
+ callback: function () {
175
+ clearErrors();
176
+ const form = modal.find('#pollCreator');
177
+ const newPoll = serializeObjectFromForm(form);
178
+
179
+ // Let's be nice and at least show an error if there are no options
180
+ if (newPoll.options.length === 0) {
181
+ error('[[poll:error.no_options]]');
182
+ return false;
183
+ }
184
+
185
+ resolve(newPoll);
186
+ return true;
187
+ },
164
188
  },
165
189
  },
166
- },
167
- });
190
+ });
168
191
 
169
- // Add option adder
170
- modal.find('#pollAddOption')
171
- .off('click')
172
- .on('click', function (e) {
173
- var el = $(e.currentTarget);
174
- var prevOption = el.prev();
175
-
176
- if (config.limits.maxOptions <= el.prevAll('input').length) {
177
- clearErrors();
178
- require(['translator'], function (translator) {
179
- translator.translate('[[poll:error.max_options]]', function (text) {
180
- error(text.replace('%d', config.limits.maxOptions));
181
- });
192
+ handleOptionSort({ modal });
193
+
194
+ // Add option adder
195
+ modal.find('#pollAddOption')
196
+ .off('click')
197
+ .on('click', async function (e) {
198
+ const el = $(e.currentTarget);
199
+ if (config.maxOptions <= el.prevAll('input').length) {
200
+ clearErrors();
201
+ error(`[[poll:error.max_options, ${config.maxOptions}]]`);
202
+ return false;
203
+ }
204
+ const id = Date.now();
205
+ const newOptionInput = await app.parseAndTranslate('poll/option-input', {
206
+ id: id,
207
+ title: '',
182
208
  });
183
- return false;
184
- }
209
+ const container = modal.find('#poll-options-container');
210
+ container.append(newOptionInput);
211
+ modal.find(`[data-option-id="${id}"] input`).focus();
212
+ });
185
213
 
186
- if (prevOption.val().length !== 0) {
187
- prevOption.clone().val('').insertBefore(el).focus();
188
- }
214
+ modal.find('#poll-options-container').on('click', '[data-action="remove-option"]', function () {
215
+ $(this).parents('[component="post/poll/option/item"]').remove();
189
216
  });
217
+ });
190
218
  });
191
219
  });
192
220
  };
193
221
 
194
- function error(message) {
195
- var errorBox = $('#pollErrorBox');
222
+ function handleOptionSort({ modal }) {
223
+ const selectorEl = modal.find('#poll-options-container');
224
+ selectorEl.sortable({
225
+ handle: '[component="sort/handle"]',
226
+ axis: 'y',
227
+ zIndex: 9999,
228
+ items: '[component="post/poll/option/item"]',
229
+ });
230
+ }
196
231
 
232
+ async function error(message) {
233
+ const errorBox = $('#pollErrorBox');
234
+ const translator = await app.require('translator');
235
+ const msgTranslated = await translator.translate(message);
197
236
  errorBox.removeClass('hidden');
198
- errorBox.append(message + '<br>');
199
-
200
- return false;
237
+ errorBox.append(msgTranslated + '<br>');
201
238
  }
202
239
 
203
240
  function clearErrors() {
@@ -205,21 +242,44 @@
205
242
  }
206
243
 
207
244
  function serializeObjectFromForm(form) {
208
- var obj = form.serializeObject();
209
- var result = {
210
- options: obj.options,
211
- settings: {
212
- title: obj['settings.title'],
213
- maxvotes: obj['settings.maxvotes'],
214
- disallowVoteUpdate: obj['settings.disallowVoteUpdate'] === 'on' ? 'true' : 'false',
215
- allowAnonVoting: obj['settings.allowAnonVoting'] === 'on' ? 'true' : 'false',
216
- end: obj['settings.end'],
217
- },
245
+ const obj = form.serializeObject();
246
+ obj.options = obj.options || [];
247
+ obj.id = obj.id || [];
248
+
249
+ const options = obj.options.map((opt, index) => ({
250
+ id: obj.id[index],
251
+ title: opt,
252
+ })).filter(option => option.title?.length);
253
+
254
+ const result = {
255
+ pollId: obj.pollId,
256
+ options: options,
257
+ title: obj.title,
258
+ maximumVotesPerUser: obj.maximumVotesPerUser,
259
+ disallowVoteUpdate: obj.disallowVoteUpdate === 'on' ? 1 : 0,
260
+ allowAnonVoting: obj.allowAnonVoting === 'on' ? 1 : 0,
261
+ end: obj.end ? new Date(obj.end).getTime() : 0,
218
262
  };
219
263
 
220
264
  return result;
221
265
  }
222
266
 
267
+ function showAlert(type, message) {
268
+ require(['alerts'], function (alerts) {
269
+ alerts[type](message);
270
+ });
271
+ }
272
+
273
+ function updateComposerBadgeCount(postContainer, count) {
274
+ require(['composer'], (composer) => {
275
+ composer.updateFormattingBtnBadgeCount(
276
+ postContainer,
277
+ 'poll',
278
+ count
279
+ );
280
+ });
281
+ }
282
+
223
283
  Poll.creator = Creator;
224
284
 
225
285
  init();
@@ -4,38 +4,65 @@
4
4
 
5
5
  window.Poll = {};
6
6
 
7
- (function () {
7
+ $(document).ready(function () {
8
8
  window.Poll.alertError = function (message) {
9
9
  require(['alerts'], function (alerts) {
10
10
  alerts.error(message);
11
11
  });
12
12
  };
13
13
 
14
- require('poll/serializer')(window.utils);
14
+ require(['hooks'], function (hooks) {
15
+ // add polls to data that is sent to server on composer submit
16
+ hooks.on('filter:composer.submit', function (hookData) {
17
+ hookData.composerData.polls = hookData.postData.polls || [];
18
+ });
19
+
20
+ hooks.on('filter:composer.drafts.save', function (hookData) {
21
+ const { draft, postData } = hookData;
22
+ if (Object.hasOwn(postData, 'polls')) {
23
+ draft.polls = postData.polls || [];
24
+ }
25
+ return hookData;
26
+ });
27
+
28
+ hooks.on('filter:composer.drafts.open', function (hookData) {
29
+ const { draft } = hookData;
30
+ if (Object.hasOwn(draft, 'polls')) {
31
+ hookData.composerData.polls = draft.polls || [];
32
+ }
33
+ return hookData;
34
+ });
35
+
36
+ hooks.on('filter:composer.topic.push', function (hookData) {
37
+ hookData.pushData.polls = hookData.data.polls || [];
38
+ return hookData;
39
+ });
40
+ hooks.on('filter:composer.reply.push', function (hookData) {
41
+ hookData.pushData.polls = hookData.data.polls || [];
42
+ return hookData;
43
+ });
44
+ });
15
45
 
16
46
  $(window).on('action:topic.loading', function () {
17
- if (ajaxify.data.posts.length > 0 && ajaxify.data.posts[0].hasOwnProperty('pollId')) {
18
- getPollByPost(ajaxify.data.posts[0]);
19
- }
47
+ ajaxify.data.posts.forEach((post) => {
48
+ getPollByPost(post);
49
+ });
20
50
  });
21
51
 
22
52
  $(window).on('action:posts.loaded', function (ev, data) {
23
53
  data.posts.forEach((post) => {
24
- if (post.hasOwnProperty('pollId')) {
25
- getPollByPost(post);
26
- }
54
+ getPollByPost(post);
27
55
  });
28
56
  });
29
57
 
30
58
  $(window).on('action:posts.edited', function (ev, data) {
31
- if (data.post.hasOwnProperty('pollId')) {
32
- getPollByPost(data.post);
33
- }
59
+ getPollByPost(data.post);
34
60
  });
35
61
 
36
62
  $(window).on('action:ajaxify.end', function () {
37
63
  $('[data-widget-poll-id]').each(function () {
38
- getPoll($(this).attr('data-widget-poll-id'), $(this));
64
+ const $this = $(this);
65
+ getPolls([$this.attr('data-widget-poll-id')], $this);
39
66
  });
40
67
  });
41
68
 
@@ -44,25 +71,34 @@ window.Poll = {};
44
71
  });
45
72
 
46
73
  function getPollByPost(post) {
47
- const postEl = $('[component="post"][data-pid="' + post.pid + '"]');
48
- if (postEl.length && post.pollId) {
49
- getPoll(post.pollId, postEl.find('[component="post/content"]'));
74
+ if (!post || !post.hasOwnProperty('pollIds') || !post.pollIds) return;
75
+ let pollIds;
76
+ try {
77
+ pollIds = JSON.parse(post.pollIds || '[]');
78
+ } catch (err) {
79
+ pollIds = [];
80
+ }
81
+ if (pollIds.length) {
82
+ const postEl = $(`[component="post"][data-pid="${post.pid}"]`);
83
+ getPolls(pollIds, postEl.find('[component="post/content"]'));
50
84
  }
51
85
  }
52
86
 
53
- function getPoll(pollId, container) {
54
- pollId = parseInt(pollId, 10);
55
- if (!isNaN(pollId)) {
56
- if (!socket.connected) {
57
- socket.connect();
87
+ function getPolls(pollIds, container) {
88
+ const validPollIds = pollIds.map(id => parseInt(id, 10)).filter(id => !isNaN(id));
89
+ socket.emit('plugins.poll.get', { pollIds: validPollIds }, async function (err, pollDataArr) {
90
+ if (err) {
91
+ return Poll.alertError(err.message);
58
92
  }
59
- socket.emit('plugins.poll.get', { pollId }, function (err, pollData) {
60
- if (err) {
61
- return Poll.alertError(err.message);
62
- }
93
+ pollDataArr.reverse();
94
+ for (const pollData of pollDataArr) {
63
95
  pollData.container = container;
64
- Poll.view.load(pollData);
65
- });
66
- }
96
+ if (container.attr('data-widget-poll-id')) {
97
+ pollData.isWidget = true;
98
+ }
99
+ // eslint-disable-next-line no-await-in-loop
100
+ await Poll.view.load(pollData);
101
+ }
102
+ });
67
103
  }
68
- }());
104
+ });