decidim-comments 0.29.2 → 0.30.0.rc2
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.
- checksums.yaml +4 -4
- data/app/cells/decidim/comments/comment/actions.erb +0 -8
- data/app/cells/decidim/comments/comment/show.erb +41 -5
- data/app/cells/decidim/comments/comment_cell.rb +44 -0
- data/app/cells/decidim/comments/comment_form/comment_as.erb +24 -6
- data/app/cells/decidim/comments/comment_form/opinion.erb +0 -4
- data/app/cells/decidim/comments/comment_form/show.erb +31 -29
- data/app/cells/decidim/comments/comment_form_cell.rb +7 -2
- data/app/cells/decidim/comments/comment_thread/show.erb +1 -1
- data/app/cells/decidim/comments/comments/add_comment.erb +12 -14
- data/app/cells/decidim/comments/comments/comments_in_single_column.erb +6 -0
- data/app/cells/decidim/comments/comments/order_control.erb +32 -9
- data/app/cells/decidim/comments/comments/show.erb +7 -7
- data/app/cells/decidim/comments/comments_cell.rb +56 -12
- data/app/cells/decidim/comments/two_columns_comments/column.erb +20 -0
- data/app/cells/decidim/comments/two_columns_comments/show.erb +11 -0
- data/app/cells/decidim/comments/two_columns_comments_cell.rb +86 -0
- data/app/forms/decidim/comments/comment_form.rb +14 -0
- data/app/models/decidim/comments/comment.rb +9 -7
- data/app/packs/entrypoints/decidim_comments.js +1 -0
- data/app/packs/src/decidim/comments/comments.component.js +151 -24
- data/app/packs/src/decidim/comments/comments.component.test.js +2 -1
- data/app/packs/src/decidim/comments/comments.js +95 -12
- data/app/packs/src/decidim/comments/comments_dropdown.js +57 -0
- data/app/packs/src/decidim/comments/comments_mobile_modal.js +46 -0
- data/app/packs/stylesheets/comments.scss +203 -50
- data/app/queries/decidim/comments/metrics/comment_participants_metric_measure.rb +1 -1
- data/app/queries/decidim/comments/metrics/comments_metric_manage.rb +22 -17
- data/app/views/decidim/comments/comments/create.js.erb +9 -1
- data/config/locales/ar.yml +0 -1
- data/config/locales/bg.yml +0 -1
- data/config/locales/ca.yml +35 -2
- data/config/locales/cs.yml +37 -2
- data/config/locales/de.yml +35 -2
- data/config/locales/el.yml +0 -1
- data/config/locales/en.yml +35 -2
- data/config/locales/es-MX.yml +35 -2
- data/config/locales/es-PY.yml +35 -2
- data/config/locales/es.yml +35 -2
- data/config/locales/eu.yml +46 -13
- data/config/locales/fi-plain.yml +35 -2
- data/config/locales/fi.yml +35 -2
- data/config/locales/fr-CA.yml +8 -2
- data/config/locales/fr.yml +8 -2
- data/config/locales/gl.yml +0 -1
- data/config/locales/hu.yml +0 -1
- data/config/locales/id-ID.yml +0 -1
- data/config/locales/is-IS.yml +0 -1
- data/config/locales/it.yml +1 -1
- data/config/locales/ja.yml +34 -2
- data/config/locales/lb.yml +0 -1
- data/config/locales/lt.yml +0 -1
- data/config/locales/lv.yml +0 -1
- data/config/locales/nl.yml +0 -1
- data/config/locales/no.yml +0 -1
- data/config/locales/pl.yml +0 -2
- data/config/locales/pt-BR.yml +0 -1
- data/config/locales/pt.yml +0 -1
- data/config/locales/ro-RO.yml +92 -58
- data/config/locales/ru.yml +0 -1
- data/config/locales/sk.yml +0 -1
- data/config/locales/sv.yml +35 -2
- data/config/locales/tr-TR.yml +0 -1
- data/config/locales/uk.yml +0 -1
- data/config/locales/zh-CN.yml +0 -1
- data/config/locales/zh-TW.yml +0 -1
- data/decidim-comments.gemspec +1 -1
- data/lib/decidim/api/comment_mutation_type.rb +2 -2
- data/lib/decidim/api/comment_type.rb +12 -45
- data/lib/decidim/api/commentable_interface.rb +4 -16
- data/lib/decidim/api/commentable_mutation_type.rb +2 -3
- data/lib/decidim/comments/commentable.rb +11 -0
- data/lib/decidim/comments/engine.rb +7 -1
- data/lib/decidim/comments/test/factories.rb +8 -0
- data/lib/decidim/comments/test/shared_examples/comment_event.rb +1 -1
- data/lib/decidim/comments/test/shared_examples/comment_voted_event.rb +2 -2
- data/lib/decidim/comments/test/shared_examples/create_comment_context.rb +1 -1
- data/lib/decidim/comments/test/shared_examples/translatable_comment.rb +2 -2
- data/lib/decidim/comments/version.rb +1 -1
- 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)
|
@@ -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
|
-
*
|
72
|
-
* @returns {Void} -
|
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
|
-
|
78
|
-
|
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
|
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 $
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
$
|
182
|
-
|
183
|
-
|
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 {
|
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
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
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
|
-
|
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
|
-
|
7
|
-
|
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
|
-
|
10
|
-
|
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
|
-
|
19
|
-
|
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
|
+
});
|