decidim-comments 0.29.2 → 0.30.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/comments/comment/actions.erb +0 -8
  3. data/app/cells/decidim/comments/comment/show.erb +41 -5
  4. data/app/cells/decidim/comments/comment_cell.rb +44 -0
  5. data/app/cells/decidim/comments/comment_form/comment_as.erb +24 -6
  6. data/app/cells/decidim/comments/comment_form/opinion.erb +0 -4
  7. data/app/cells/decidim/comments/comment_form/show.erb +31 -29
  8. data/app/cells/decidim/comments/comment_form_cell.rb +7 -2
  9. data/app/cells/decidim/comments/comment_thread/show.erb +1 -1
  10. data/app/cells/decidim/comments/comments/add_comment.erb +12 -14
  11. data/app/cells/decidim/comments/comments/comments_in_single_column.erb +6 -0
  12. data/app/cells/decidim/comments/comments/order_control.erb +32 -9
  13. data/app/cells/decidim/comments/comments/show.erb +7 -7
  14. data/app/cells/decidim/comments/comments_cell.rb +56 -12
  15. data/app/cells/decidim/comments/two_columns_comments/column.erb +20 -0
  16. data/app/cells/decidim/comments/two_columns_comments/show.erb +11 -0
  17. data/app/cells/decidim/comments/two_columns_comments_cell.rb +86 -0
  18. data/app/forms/decidim/comments/comment_form.rb +14 -0
  19. data/app/models/decidim/comments/comment.rb +9 -7
  20. data/app/packs/entrypoints/decidim_comments.js +1 -0
  21. data/app/packs/src/decidim/comments/comments.component.js +151 -24
  22. data/app/packs/src/decidim/comments/comments.component.test.js +2 -1
  23. data/app/packs/src/decidim/comments/comments.js +95 -12
  24. data/app/packs/src/decidim/comments/comments_dropdown.js +57 -0
  25. data/app/packs/src/decidim/comments/comments_mobile_modal.js +46 -0
  26. data/app/packs/stylesheets/comments.scss +203 -50
  27. data/app/queries/decidim/comments/metrics/comment_participants_metric_measure.rb +1 -1
  28. data/app/queries/decidim/comments/metrics/comments_metric_manage.rb +22 -17
  29. data/app/views/decidim/comments/comments/create.js.erb +9 -1
  30. data/config/locales/ar.yml +0 -1
  31. data/config/locales/bg.yml +0 -1
  32. data/config/locales/ca.yml +35 -2
  33. data/config/locales/cs.yml +37 -2
  34. data/config/locales/de.yml +35 -2
  35. data/config/locales/el.yml +0 -1
  36. data/config/locales/en.yml +35 -2
  37. data/config/locales/es-MX.yml +35 -2
  38. data/config/locales/es-PY.yml +35 -2
  39. data/config/locales/es.yml +35 -2
  40. data/config/locales/eu.yml +37 -4
  41. data/config/locales/fi-plain.yml +35 -2
  42. data/config/locales/fi.yml +35 -2
  43. data/config/locales/fr-CA.yml +8 -2
  44. data/config/locales/fr.yml +8 -2
  45. data/config/locales/gl.yml +0 -1
  46. data/config/locales/hu.yml +0 -1
  47. data/config/locales/id-ID.yml +0 -1
  48. data/config/locales/is-IS.yml +0 -1
  49. data/config/locales/it.yml +1 -1
  50. data/config/locales/ja.yml +34 -2
  51. data/config/locales/lb.yml +0 -1
  52. data/config/locales/lt.yml +0 -1
  53. data/config/locales/lv.yml +0 -1
  54. data/config/locales/nl.yml +0 -1
  55. data/config/locales/no.yml +0 -1
  56. data/config/locales/pl.yml +0 -2
  57. data/config/locales/pt-BR.yml +0 -1
  58. data/config/locales/pt.yml +0 -1
  59. data/config/locales/ro-RO.yml +1 -2
  60. data/config/locales/ru.yml +0 -1
  61. data/config/locales/sk.yml +0 -1
  62. data/config/locales/sv.yml +19 -2
  63. data/config/locales/tr-TR.yml +0 -1
  64. data/config/locales/uk.yml +0 -1
  65. data/config/locales/zh-CN.yml +0 -1
  66. data/config/locales/zh-TW.yml +0 -1
  67. data/decidim-comments.gemspec +1 -1
  68. data/lib/decidim/api/comment_mutation_type.rb +2 -2
  69. data/lib/decidim/api/comment_type.rb +12 -45
  70. data/lib/decidim/api/commentable_interface.rb +4 -16
  71. data/lib/decidim/api/commentable_mutation_type.rb +2 -3
  72. data/lib/decidim/comments/commentable.rb +11 -0
  73. data/lib/decidim/comments/engine.rb +7 -1
  74. data/lib/decidim/comments/test/factories.rb +8 -0
  75. data/lib/decidim/comments/test/shared_examples/comment_event.rb +1 -1
  76. data/lib/decidim/comments/test/shared_examples/comment_voted_event.rb +2 -2
  77. data/lib/decidim/comments/test/shared_examples/create_comment_context.rb +1 -1
  78. data/lib/decidim/comments/test/shared_examples/translatable_comment.rb +2 -2
  79. data/lib/decidim/comments/version.rb +1 -1
  80. metadata +16 -10
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Comments
5
+ # A cell to render comments in two columns layout.
6
+ class TwoColumnsCommentsCell < Decidim::Comments::CommentsCell
7
+ def call
8
+ initialize_comments
9
+ @interleaved_comments = interleave_comments(@sorted_comments_in_favor, @sorted_comments_against)
10
+ render :show
11
+ end
12
+
13
+ def render_column(top_comment, comments, icon_name, title)
14
+ set_column_variables(top_comment, comments, icon_name, title)
15
+ render :column
16
+ end
17
+
18
+ private
19
+
20
+ def initialize_comments
21
+ if model.closed?
22
+ load_closed_comments
23
+ else
24
+ @sorted_comments_in_favor = comments_in_favor
25
+ @sorted_comments_against = comments_against
26
+ end
27
+ end
28
+
29
+ def load_closed_comments
30
+ @top_comment_in_favor, @sorted_comments_in_favor = sorted_comments(comments_in_favor)
31
+ @top_comment_against, @sorted_comments_against = sorted_comments(comments_against)
32
+ end
33
+
34
+ def sorted_comments(comments)
35
+ top_comment = find_top_comment(comments)
36
+ sorted_comments = comments.where.not(id: top_comment&.id).order(created_at: :asc)
37
+ [top_comment, sorted_comments]
38
+ end
39
+
40
+ def find_top_comment(comments)
41
+ comments
42
+ .select("*, (up_votes_count - down_votes_count) AS vote_balance, up_votes_count AS upvotes, down_votes_count AS downvotes")
43
+ .where("up_votes_count > 0")
44
+ .reorder("vote_balance DESC, upvotes DESC, downvotes ASC, created_at ASC")
45
+ .first
46
+ end
47
+
48
+ def interleave_comments(comments_in_favor, comments_against)
49
+ interleave_top_comments + interleave_remaining_comments(comments_in_favor, comments_against)
50
+ end
51
+
52
+ def interleave_top_comments
53
+ return [] unless model.closed?
54
+
55
+ Array(@top_comment_in_favor) + Array(@top_comment_against)
56
+ end
57
+
58
+ def interleave_remaining_comments(comments_in_favor, comments_against)
59
+ interleaved = []
60
+ max_length = [comments_in_favor.size, comments_against.size].max
61
+
62
+ max_length.times do |i|
63
+ interleaved << comments_in_favor[i] if comments_in_favor[i]
64
+ interleaved << comments_against[i] if comments_against[i]
65
+ end
66
+
67
+ interleaved
68
+ end
69
+
70
+ def comments_in_favor
71
+ @comments_in_favor ||= model.comments.positive.order(:created_at)
72
+ end
73
+
74
+ def comments_against
75
+ @comments_against ||= model.comments.negative.order(:created_at)
76
+ end
77
+
78
+ def set_column_variables(top_comment, comments, icon_name, title)
79
+ @top_comment = top_comment
80
+ @comments = comments
81
+ @icon_name = icon_name
82
+ @title = title
83
+ end
84
+ end
85
+ end
86
+ end
@@ -5,6 +5,8 @@ module Decidim
5
5
  # A form object used to create comments from the graphql api.
6
6
  #
7
7
  class CommentForm < Form
8
+ include Decidim::UserRoleChecker
9
+
8
10
  attribute :body, Decidim::Attributes::CleanString
9
11
  attribute :alignment, Integer
10
12
  attribute :user_group_id, Integer
@@ -17,6 +19,7 @@ module Decidim
17
19
  validates :alignment, inclusion: { in: [0, 1, -1] }, if: ->(form) { form.alignment.present? }
18
20
 
19
21
  validate :max_depth
22
+ validate :commentable_can_have_comments
20
23
 
21
24
  def max_length
22
25
  if current_component.try(:settings).respond_to?(:comments_max_length)
@@ -33,6 +36,17 @@ module Decidim
33
36
 
34
37
  errors.add(:base, :invalid) if commentable.depth >= Comment::MAX_DEPTH
35
38
  end
39
+
40
+ private
41
+
42
+ # Private: Check if commentable can have comments and if not adds
43
+ # a validation error to the model
44
+ def commentable_can_have_comments
45
+ return unless current_component && current_component.participatory_space
46
+ return if user_has_any_role?(current_user, current_component.participatory_space)
47
+
48
+ errors.add(:commentable, :cannot_have_comments) unless commentable.accepts_new_comments?
49
+ end
36
50
  end
37
51
  end
38
52
  end
@@ -17,6 +17,7 @@ module Decidim
17
17
  include Decidim::TranslatableResource
18
18
  include Decidim::TranslatableAttributes
19
19
  include Decidim::ActsAsTree
20
+ include ActionView::Helpers::TextHelper
20
21
 
21
22
  # Limit the max depth of a comment tree. If C is a comment and R is a reply:
22
23
  # C (depth 0)
@@ -55,7 +56,6 @@ module Decidim
55
56
  validates :depth, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: MAX_DEPTH }
56
57
  validates :alignment, inclusion: { in: [0, 1, -1] }
57
58
  validate :body_length
58
- validate :commentable_can_have_comments
59
59
 
60
60
  scope :not_deleted, -> { where(deleted_at: nil) }
61
61
 
@@ -80,6 +80,10 @@ module Decidim
80
80
  where(alignment: -1)
81
81
  end
82
82
 
83
+ def reported_title
84
+ truncate(translated_attribute(body))
85
+ end
86
+
83
87
  def organization
84
88
  commentable&.organization || participatory_space&.organization
85
89
  end
@@ -207,6 +211,10 @@ module Decidim
207
211
  Decidim::ActionLog.where(resource: self).exists?(["extra @> ?", Arel.sql("{\"edit\":true}")])
208
212
  end
209
213
 
214
+ def extra_actions_for(current_user)
215
+ root_commentable.try(:actions_for_comment, self, current_user)
216
+ end
217
+
210
218
  private
211
219
 
212
220
  def body_length
@@ -228,12 +236,6 @@ module Decidim
228
236
  component.settings.comments_max_length.positive?
229
237
  end
230
238
 
231
- # Private: Check if commentable can have comments and if not adds
232
- # a validation error to the model
233
- def commentable_can_have_comments
234
- errors.add(:commentable, :cannot_have_comments) unless root_commentable.accepts_new_comments?
235
- end
236
-
237
239
  # Private: Compute comment depth inside the current comment tree
238
240
  def compute_depth
239
241
  self.depth = commentable.depth + 1 if commentable.respond_to?(:depth)
@@ -3,3 +3,4 @@ import "stylesheets/comments.scss"
3
3
 
4
4
  // JavaScript
5
5
  import "src/decidim/comments/comments"
6
+ import "src/decidim/comments/comments_mobile_modal"
@@ -1,5 +1,6 @@
1
1
  /* eslint id-length: ["error", { "exceptions": ["$"] }] */
2
2
  /* eslint max-lines: ["error", {"max": 350, "skipBlankLines": true}] */
3
+ /* eslint-disable max-lines */
3
4
 
4
5
  /**
5
6
  * A plain JavaScript component that handles the comments.
@@ -11,7 +12,8 @@
11
12
  // This is necessary for testing purposes
12
13
  const $ = window.$;
13
14
 
14
- import changeReportFormBehavior from "src/decidim/change_report_form_behavior"
15
+ import changeReportFormBehavior from "src/decidim/change_report_form_behavior";
16
+ import { initializeCommentsDropdown } from "../../decidim/comments/comments_dropdown";
15
17
 
16
18
  export default class CommentsComponent {
17
19
  constructor($element, config) {
@@ -26,6 +28,9 @@ export default class CommentsComponent {
26
28
  this.toggleTranslations = config.toggleTranslations;
27
29
  this.id = this.$element.attr("id") || this._getUID();
28
30
  this.mounted = false;
31
+
32
+ this._onTextInput = this._onTextInput.bind(this);
33
+ this._onToggleOpinion = this._onToggleOpinion.bind(this);
29
34
  }
30
35
 
31
36
  /**
@@ -43,6 +48,7 @@ export default class CommentsComponent {
43
48
  $(".add-comment textarea", this.$element).prop("disabled", false);
44
49
  });
45
50
  }
51
+ this._initializeSortDropdown();
46
52
  }
47
53
  }
48
54
 
@@ -65,17 +71,45 @@ export default class CommentsComponent {
65
71
 
66
72
  /**
67
73
  * Adds a new thread to the comments section.
74
+ * If the layout is a two-column layout, the comment is added to either
75
+ * the "in favor" or "against" column based on the alignment provided.
76
+ * If the layout is a single column or on a mobile screen,
77
+ * the comment is added to the general comment thread with interleaved ordering.
78
+ *
68
79
  * @public
69
- * @param {String} threadHtml - The HTML content for the thread.
80
+ * @param {String} threadHtml - The HTML content for the thread to be added.
81
+ * @param {Number|null} alignment - Specifies the alignment of the comment.
82
+ * If -1, the comment is added to the "against" column.
83
+ * If 1, the comment is added to the "in favor" column.
84
+ * If null or if on a mobile screen, the comment is added to the general thread.
70
85
  * @param {Boolean} fromCurrentUser - A boolean indicating whether the user
71
- * herself was the author of the new thread. Defaults to false.
72
- * @returns {Void} - Returns nothing
86
+ * is the author of the new thread. Defaults to false.
87
+ * @returns {Void} - Does not return a value.
73
88
  */
74
- addThread(threadHtml, fromCurrentUser = false) {
75
- const $parent = $(".comments:first", this.$element);
89
+ addThread(threadHtml, alignment = null, fromCurrentUser = false) {
76
90
  const $comment = $(threadHtml);
77
- const $threads = $(".comment-threads", this.$element);
78
- this._addComment($threads, $comment);
91
+ let $parent = null;
92
+
93
+ const $commentsContainer = $(".comments-two-columns", this.$element);
94
+ const isTwoColumnsLayout = $commentsContainer.length > 0;
95
+ const isMobileScreen = window.innerWidth < 768;
96
+
97
+ if (isTwoColumnsLayout && !isMobileScreen) {
98
+ const $inFavorColumn = $(".comments-section__in-favor", this.$element);
99
+ const $againstColumn = $(".comments-section__against", this.$element);
100
+
101
+ if (alignment === 1 && $inFavorColumn.length > 0) {
102
+ $parent = $inFavorColumn;
103
+ } else if (alignment === -1 && $againstColumn.length > 0) {
104
+ $parent = $againstColumn;
105
+ } else {
106
+ $parent = $(".comment-threads", this.$element);
107
+ }
108
+ } else {
109
+ $parent = $(".comment-threads", this.$element);
110
+ }
111
+
112
+ this._addComment($parent, $comment);
79
113
  this._finalizeCommentCreation($parent, fromCurrentUser);
80
114
  }
81
115
 
@@ -132,6 +166,13 @@ export default class CommentsComponent {
132
166
  this._stopPolling();
133
167
  });
134
168
 
169
+ document.querySelectorAll(".new_report").forEach((container) => changeReportFormBehavior(container));
170
+
171
+ const $dropdown = $add.find("[data-comments-dropdown]");
172
+ if ($dropdown.length > 0) {
173
+ initializeCommentsDropdown($dropdown[0]);
174
+ }
175
+
135
176
  document.querySelectorAll(".new_report").forEach((container) => changeReportFormBehavior(container))
136
177
 
137
178
  if ($text.length && $text.get(0) !== null) {
@@ -160,32 +201,35 @@ export default class CommentsComponent {
160
201
  $target.append($container);
161
202
 
162
203
  this._initializeComments($container);
163
- document.dispatchEvent(new CustomEvent("comments:loaded", { detail: {commentsIds: [this.lastCommentId] }}));
204
+ document.dispatchEvent(new CustomEvent("comments:loaded", { detail: { commentsIds: [this.lastCommentId] } }));
164
205
  }
165
206
 
166
207
  /**
167
208
  * Finalizes the new comment creation after the comment adding finishes
168
209
  * successfully.
169
210
  * @private
170
- * @param {jQuery} $parent - The parent comment element to finalize.
211
+ * @param {jQuery} $parent - The parent element representing where the comment
212
+ * was added.
171
213
  * @param {Boolean} fromCurrentUser - A boolean indicating whether the user
172
214
  * herself was the author of the new comment.
173
215
  * @returns {Void} - Returns nothing
174
216
  */
175
217
  _finalizeCommentCreation($parent, fromCurrentUser) {
176
218
  if (fromCurrentUser) {
177
- const $add = $(".add-comment", $parent);
178
- $("textarea", $add).each((_i, text) => {
179
- const $text = $(text);
180
- // Reset textarea content
181
- $text.val("")
182
- // Update characterCounter component
183
- const characterCounter = $text.data("remaining-characters-counter");
219
+ const $addCommentForms = $(".add-comment", this.$element);
220
+
221
+ $addCommentForms.each((_i, form) => {
222
+ const $form = $(form);
223
+ const $textarea = $form.find("textarea");
224
+
225
+ $textarea.val("");
226
+
227
+ const characterCounter = $textarea.data("remaining-characters-counter");
184
228
  if (characterCounter) {
185
229
  characterCounter.handleInput();
186
230
  characterCounter.updateStatus();
187
231
  }
188
- })
232
+ });
189
233
  }
190
234
 
191
235
  // Restart the polling
@@ -205,6 +249,11 @@ export default class CommentsComponent {
205
249
  }, this.pollingInterval);
206
250
  }
207
251
 
252
+ reloadAllComments() {
253
+ this._setLoading();
254
+ this._fetchComments();
255
+ }
256
+
208
257
  /**
209
258
  * Sends an ajax request based on current
210
259
  * params to get comments for the component
@@ -266,6 +315,47 @@ export default class CommentsComponent {
266
315
  this._setLoading();
267
316
  }
268
317
 
318
+ /**
319
+ * Updates the state of the submit button based on input text and opinion selection.
320
+ *
321
+ * @param {Object} params - The parameters for updating the submit button state.
322
+ * @param {jQuery} params.$form - The form element.
323
+ * @param {boolean} params.isTextNotEmpty - Whether the text input is not empty.
324
+ * @param {boolean} params.isTwoColumnsLayout - Whether the layout is two-column.
325
+ * @param {boolean} params.isOpinionSelected - Whether an opinion (for/against) has been selected.
326
+ * @returns {void} - Does not return a value.
327
+ * @private
328
+ */
329
+ _updateSubmitButtonState({ $form, isTextNotEmpty, isTwoColumnsLayout, isOpinionSelected }) {
330
+ const $submit = $("button[type='submit']", $form);
331
+ if (isTextNotEmpty && (!isTwoColumnsLayout || isOpinionSelected)) {
332
+ $submit.removeAttr("disabled");
333
+ } else {
334
+ $submit.attr("disabled", "disabled");
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Prepares parameters for updating the submit button state.
340
+ *
341
+ * @param {jQuery} $form - The form element.
342
+ * @returns {Object} - Returns an object with necessary parameters.
343
+ * @private
344
+ */
345
+ _prepareSubmitButtonStateParams($form) {
346
+ const $opinionButtons = $("[data-opinion-toggle] button", $form.closest(".add-comment"));
347
+ const isTwoColumnsLayout = $(".comments-two-columns", this.$element).length > 0;
348
+ const isOpinionSelected = $opinionButtons.filter("[aria-pressed='true']").length > 0;
349
+ const isTextNotEmpty = $("textarea", $form).val().length > 0;
350
+
351
+ return {
352
+ $form,
353
+ isTextNotEmpty,
354
+ isTwoColumnsLayout,
355
+ isOpinionSelected
356
+ };
357
+ }
358
+
269
359
  /**
270
360
  * Event listener for the opinion toggle buttons.
271
361
  * @private
@@ -297,24 +387,61 @@ export default class CommentsComponent {
297
387
 
298
388
  // Announce the selected state for the screen reader
299
389
  $selectedState.text($btn.data("selected-label"));
390
+
391
+ this._updateSubmitButtonState(this._prepareSubmitButtonStateParams($form));
300
392
  }
301
393
 
302
394
  /**
303
395
  * Event listener for the comment field text input.
304
396
  * @private
305
- * @param {Event} ev - The event object.
397
+ * @param {{target: (*|jQuery|HTMLElement)}} ev - The event object.
306
398
  * @returns {Void} - Returns nothing
307
399
  */
308
400
  _onTextInput(ev) {
309
401
  const $text = $(ev.target);
310
402
  const $add = $text.closest(".add-comment");
311
403
  const $form = $("form", $add);
312
- const $submit = $("button[type='submit']", $form);
313
404
 
314
- if ($text.val().length > 0) {
315
- $submit.removeAttr("disabled");
316
- } else {
317
- $submit.attr("disabled", "disabled");
405
+ this._updateSubmitButtonState(this._prepareSubmitButtonStateParams($form));
406
+ }
407
+
408
+ /**
409
+ * Adds the behaviour for the drop down order section within comments.
410
+ * @private
411
+ * @returns {Void} - Returns nothing
412
+ */
413
+ _initializeSortDropdown() {
414
+ const desktopOrderSelect = document.querySelector("[data-desktop-order-comment-select]");
415
+ const mobileOrderSelect = document.querySelector("[data-mobile-order-comment-select]");
416
+
417
+ if (!desktopOrderSelect && !mobileOrderSelect) {
418
+ return;
318
419
  }
420
+
421
+ desktopOrderSelect.style.borderColor = "black";
422
+ mobileOrderSelect.style.borderColor = "black";
423
+
424
+ desktopOrderSelect.addEventListener("change", function(event) {
425
+ const selectedOption = desktopOrderSelect.querySelector(`[value=${event.target.value}]`);
426
+ const orderUrl = selectedOption.dataset.orderCommentUrl;
427
+
428
+ Rails.ajax({
429
+ url: orderUrl,
430
+ type: "GET",
431
+ error: (data) => (console.error(data))
432
+ });
433
+ });
434
+
435
+ mobileOrderSelect.addEventListener("change", function(event) {
436
+ const selectedOption = mobileOrderSelect.querySelector(`[value=${event.target.value}]`);
437
+ const orderUrl = selectedOption.dataset.orderCommentUrl;
438
+
439
+ Rails.ajax({
440
+ url: orderUrl,
441
+ type: "GET",
442
+ error: (data) => (console.error(data))
443
+ });
444
+ });
319
445
  }
320
446
  }
447
+ /* eslint-enable max-lines */
@@ -616,7 +616,8 @@ describe("CommentsComponent", () => {
616
616
  textArea.val("I am writing a new comment...");
617
617
 
618
618
  const newThread = generateCommentThread(999, "This is a dynamically added comment");
619
- subject.addThread(newThread, true);
619
+ const alignment = null;
620
+ subject.addThread(newThread, alignment, true);
620
621
 
621
622
  expect(textArea.val()).toEqual("");
622
623
  });
@@ -1,20 +1,103 @@
1
- import CommentsComponent from "src/decidim/comments/comments.component"
1
+ import CommentsComponent from "src/decidim/comments/comments.component";
2
+ import { screens } from "tailwindcss/defaultTheme";
2
3
 
3
4
  window.Decidim.CommentsComponent = CommentsComponent;
4
5
 
6
+ /**
7
+ * Debounce function to limit the rate at which a function is called.
8
+ * It ensures that the function is not called more than once within the specified delay.
9
+ *
10
+ * @param {Function} func - The function to debounce.
11
+ * @param {number} delay - The delay (in milliseconds) to wait before calling the function.
12
+ * @returns {Function} - A debounced version of the original function.
13
+ */
14
+ const debounce = (func, delay) => {
15
+ let timeout = null;
16
+ return (...args) => {
17
+ clearTimeout(timeout);
18
+ timeout = setTimeout(() => func(...args), delay);
19
+ };
20
+ };
21
+
22
+ /**
23
+ * Initializes the CommentsComponent for a specific element.
24
+ * If the component is not yet created for the given element, it creates and mounts it.
25
+ *
26
+ * @param {jQuery} $el - The jQuery-wrapped element to initialize the CommentsComponent for.
27
+ * @returns {CommentsComponent} - The initialized CommentsComponent instance.
28
+ */
29
+ const initializeCommentsComponent = ($el) => {
30
+ const commentsData = $el.data("decidim-comments");
31
+ let comments = $el.data("comments");
32
+
33
+ if (!comments) {
34
+ comments = new CommentsComponent($el, commentsData);
35
+ comments.mountComponent();
36
+ $el.data("comments", comments);
37
+ }
38
+
39
+ return comments;
40
+ };
41
+
42
+ /**
43
+ * Updates the CommentsComponent for a specific element.
44
+ * It unmounts any existing component and then re-initializes it.
45
+ *
46
+ * @param {jQuery} $el - The jQuery-wrapped element to update the CommentsComponent for.
47
+ * @returns {void}
48
+ */
49
+ const updateCommentsComponent = ($el) => {
50
+ const existingComments = $el.data("comments");
51
+
52
+ if (existingComments && typeof existingComments.unmountComponent === "function") {
53
+ existingComments.unmountComponent();
54
+ }
55
+
56
+ const newComments = new CommentsComponent($el, $el.data("decidim-comments"));
57
+ newComments.mountComponent();
58
+ $el.data("comments", newComments);
59
+ };
60
+
61
+ /**
62
+ * Main initializer for all comment elements on the page.
63
+ * It sets up all CommentsComponent instances and listens for screen resizes to update components if necessary.
64
+ *
65
+ * @returns {void}
66
+ */
5
67
  const commentsInitializer = () => {
6
- // Mount comments component
7
- $("[data-decidim-comments]").each((_i, el) => {
68
+ const smBreakpoint = parseInt(screens.md.replace("px", ""), 10);
69
+ const isMobileScreen = () => window.matchMedia(`(max-width: ${smBreakpoint}px)`).matches;
70
+ let wasMobileScreen = isMobileScreen();
71
+ const commentElements = $("[data-decidim-comments]");
72
+ const commentsMap = new Map();
73
+
74
+ // Initialize a CommentsComponent for each comment element
75
+ commentElements.each((_i, el) => {
8
76
  const $el = $(el);
9
- let comments = $(el).data("comments");
10
- if (!comments) {
11
- comments = new CommentsComponent($el, $el.data("decidim-comments"));
12
- }
13
- comments.mountComponent();
14
- $(el).data("comments", comments);
77
+ const comments = initializeCommentsComponent($el);
78
+ commentsMap.set($el, comments);
15
79
  });
16
- }
17
80
 
18
- // If no jQuery is used the Tribute feature used in comments to autocomplete
19
- // mentions stops working
81
+ /**
82
+ * Handles the window resize event.
83
+ * It re-initializes the CommentsComponent for each comment element if the screen size has changed from mobile to desktop or vice versa.
84
+ *
85
+ * @returns {void}
86
+ */
87
+ const handleResize = debounce(() => {
88
+ const isNowMobileScreen = isMobileScreen();
89
+ if (wasMobileScreen !== isNowMobileScreen) {
90
+ commentElements.each((_i, el) => {
91
+ const $el = $(el);
92
+ updateCommentsComponent($el);
93
+ });
94
+ wasMobileScreen = isNowMobileScreen;
95
+ }
96
+ }, 200);
97
+
98
+ // Listen for window resize events and trigger the handler
99
+ window.addEventListener("resize", handleResize);
100
+ };
101
+
102
+ // If no jQuery is used the Tribute feature used in comments to autocomplete mentions stops working
20
103
  $(() => commentsInitializer());
@@ -0,0 +1,57 @@
1
+ // Dropdown menu for user_group
2
+ export const initializeCommentsDropdown = function (elements) {
3
+ let dropdownButtons = document;
4
+
5
+ if (elements === document) {
6
+ dropdownButtons = document.querySelectorAll("[data-comments-dropdown]");
7
+ } else if (elements instanceof NodeList || Array.isArray(elements)) {
8
+ dropdownButtons = elements;
9
+ } else {
10
+ dropdownButtons = [elements];
11
+ }
12
+
13
+ dropdownButtons.forEach((button) => {
14
+ const dropdownId = button.getAttribute("data-target");
15
+ const dropdownMenu = document.getElementById(dropdownId);
16
+
17
+ if (dropdownMenu) {
18
+ const firstLi = dropdownMenu.querySelector("li");
19
+ const firstAuthorInfo = firstLi?.querySelector(
20
+ ".comment__as-author-info"
21
+ );
22
+
23
+ if (firstAuthorInfo) {
24
+ button.querySelector("span").innerHTML = firstAuthorInfo.innerHTML;
25
+ firstLi.style.display = "none";
26
+ }
27
+
28
+ dropdownMenu.querySelectorAll("li").forEach((li) => {
29
+ li.addEventListener("click", () => {
30
+ const input = li.querySelector("input[type='radio']");
31
+
32
+ if (input) {
33
+ input.checked = true;
34
+ input.dispatchEvent(new Event("click"));
35
+
36
+ const authorInfo = li.querySelector(".comment__as-author-info");
37
+
38
+ if (authorInfo) {
39
+ const authorContent = authorInfo.innerHTML;
40
+
41
+ setTimeout(() => {
42
+ button.querySelector("span").innerHTML = authorContent;
43
+
44
+ li.style.display = "none";
45
+ dropdownMenu.querySelectorAll("li").forEach((otherLi) => {
46
+ if (otherLi !== li) {
47
+ otherLi.style.display = "";
48
+ }
49
+ });
50
+ }, 500);
51
+ }
52
+ }
53
+ });
54
+ });
55
+ }
56
+ });
57
+ };
@@ -0,0 +1,46 @@
1
+ // The code: 1. Manages a responsive "Add Comment" modal: Opens fullscreen on mobile (<= sm) and closes it via a "close" button.
2
+ // 2. Handles dropdown menus: Dynamically updates button content based on user selection, hides selected items, and manages dropdown visibility.
3
+ // This creates a responsive, interactive comment interface with mobile-friendly design and dynamic user group selection.
4
+
5
+ import { screens } from "tailwindcss/defaultTheme"
6
+ import { initializeCommentsDropdown } from "../../decidim/comments/comments_dropdown";
7
+
8
+ // Add comment card for mobile
9
+ const addCommentMobile = function (addCommentCard) {
10
+ const smBreakpoint = parseInt(screens.sm.replace("px", ""), 10);
11
+ if (window.matchMedia(`(max-width: ${smBreakpoint}px)`).matches) {
12
+ addCommentCard.classList.remove("hidden");
13
+ addCommentCard.classList.add("fullscreen");
14
+ }
15
+ };
16
+
17
+ const closeAddComment = function (addCommentCard) {
18
+ addCommentCard.classList.add("hidden");
19
+ addCommentCard.classList.remove("fullscreen");
20
+ }
21
+
22
+ document.addEventListener("DOMContentLoaded", () => {
23
+ // Add comment card for mobile
24
+ const addCommentCard = document.getElementById("add-comment-anchor");
25
+ if (addCommentCard) {
26
+ document.querySelectorAll(".add-comment-mobile").forEach((addButtonMobile) => {
27
+ addButtonMobile.addEventListener("click", () => {
28
+ addCommentMobile(addCommentCard);
29
+ });
30
+ });
31
+ }
32
+
33
+ // Close comment modal
34
+ const closeButton = document.querySelector(
35
+ "#add-comment-anchor .close-add-comment-fullscreen"
36
+ );
37
+ if (closeButton) {
38
+ closeButton.addEventListener("click", () =>
39
+ closeAddComment(addCommentCard)
40
+ );
41
+ }
42
+
43
+
44
+ // Initialize dropdown menu
45
+ initializeCommentsDropdown(document);
46
+ });