decidim-comments 0.29.1 → 0.30.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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/app/views/decidim/comments/comments/update.js.erb +6 -0
- data/config/locales/ar.yml +0 -1
- data/config/locales/bg.yml +0 -1
- data/config/locales/bn-BD.yml +1 -0
- data/config/locales/bs-BA.yml +15 -0
- data/config/locales/ca.yml +36 -3
- data/config/locales/cs.yml +37 -2
- data/config/locales/de.yml +36 -3
- 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 +36 -3
- data/config/locales/eu.yml +55 -19
- data/config/locales/fi-plain.yml +35 -2
- data/config/locales/fi.yml +39 -6
- 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 +1 -2
- data/config/locales/ru.yml +0 -1
- data/config/locales/sk.yml +0 -1
- data/config/locales/sv.yml +19 -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 +2 -2
- data/lib/decidim/api/comment_mutation_type.rb +2 -2
- data/lib/decidim/api/comment_type.rb +25 -45
- data/lib/decidim/api/commentable_interface.rb +10 -16
- data/lib/decidim/api/commentable_mutation_type.rb +2 -3
- data/lib/decidim/comments/commentable.rb +11 -0
- data/lib/decidim/comments/commentable_with_component.rb +3 -1
- data/lib/decidim/comments/engine.rb +7 -1
- data/lib/decidim/comments/query_extensions.rb +1 -1
- data/lib/decidim/comments/test/factories.rb +9 -1
- 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 +19 -11
@@ -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
|
+
});
|