nodebb-plugin-poll 3.2.8 → 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,210 +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
- className: 'btn-default',
140
- callback: function () {
141
- 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
+ },
142
171
  },
143
- },
144
- save: {
145
- label: '[[modules:bootbox.confirm]]',
146
- className: 'btn-primary',
147
- callback: function (e) {
148
- clearErrors();
149
- var form = $(e.currentTarget).parents('.bootbox').find('#pollCreator');
150
- var obj = serializeObjectFromForm(form);
151
-
152
- // Let's be nice and at least show an error if there are no options
153
- obj.options.filter(function (obj) {
154
- return obj.length;
155
- });
156
-
157
- if (obj.options.length === 0) {
158
- return error('[[poll:error.no_options]]');
159
- }
160
-
161
- if (obj.settings.end) {
162
- obj.settings.end = new Date(obj.settings.end).getTime();
163
- }
164
- callback(obj);
165
- 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
+ },
166
188
  },
167
189
  },
168
- },
169
- });
190
+ });
191
+
192
+ handleOptionSort({ modal });
170
193
 
171
- // Add option adder
172
- modal.find('#pollAddOption')
173
- .off('click')
174
- .on('click', function (e) {
175
- var el = $(e.currentTarget);
176
- var prevOption = el.prev();
177
-
178
- if (config.limits.maxOptions <= el.prevAll('input').length) {
179
- clearErrors();
180
- require(['translator'], function (translator) {
181
- translator.translate('[[poll:error.max_options]]', function (text) {
182
- error(text.replace('%d', config.limits.maxOptions));
183
- });
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: '',
184
208
  });
185
- return false;
186
- }
209
+ const container = modal.find('#poll-options-container');
210
+ container.append(newOptionInput);
211
+ modal.find(`[data-option-id="${id}"] input`).focus();
212
+ });
187
213
 
188
- if (prevOption.val().length !== 0) {
189
- prevOption.clone().val('').insertBefore(el).focus();
190
- }
214
+ modal.find('#poll-options-container').on('click', '[data-action="remove-option"]', function () {
215
+ $(this).parents('[component="post/poll/option/item"]').remove();
191
216
  });
192
-
193
- if (poll.settings && poll.settings.end) {
194
-
195
- // flatpickrInstance.setDate(poll.settings.end);
196
- }
217
+ });
197
218
  });
198
219
  });
199
220
  };
200
221
 
201
- function error(message) {
202
- 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
+ }
203
231
 
232
+ async function error(message) {
233
+ const errorBox = $('#pollErrorBox');
234
+ const translator = await app.require('translator');
235
+ const msgTranslated = await translator.translate(message);
204
236
  errorBox.removeClass('hidden');
205
- errorBox.append(message + '<br>');
206
-
207
- return false;
237
+ errorBox.append(msgTranslated + '<br>');
208
238
  }
209
239
 
210
240
  function clearErrors() {
@@ -212,21 +242,44 @@
212
242
  }
213
243
 
214
244
  function serializeObjectFromForm(form) {
215
- var obj = form.serializeObject();
216
- var result = {
217
- options: obj.options,
218
- settings: {
219
- title: obj['settings.title'],
220
- maxvotes: obj['settings.maxvotes'],
221
- disallowVoteUpdate: obj['settings.disallowVoteUpdate'] === 'on' ? 'true' : 'false',
222
- allowAnonVoting: obj['settings.allowAnonVoting'] === 'on' ? 'true' : 'false',
223
- end: obj['settings.end'],
224
- },
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,
225
262
  };
226
263
 
227
264
  return result;
228
265
  }
229
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
+
230
283
  Poll.creator = Creator;
231
284
 
232
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
+ });