decidim-core 0.31.4 → 0.31.6
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/amendable/amend_button_card/show.erb +2 -2
- data/app/cells/decidim/author_cell.rb +0 -4
- data/app/cells/decidim/content_blocks/cta_settings_form/show.erb +1 -1
- data/app/cells/decidim/content_blocks/hero_settings_form/show.erb +2 -2
- data/app/cells/decidim/content_blocks/highlighted_content_banner_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/highlighted_content_banner_settings_form/show.erb +1 -1
- data/app/cells/decidim/content_blocks/highlighted_participatory_spaces_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/html_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/participatory_space_hero_settings_form/show.erb +1 -1
- data/app/cells/decidim/content_blocks/static_page/section_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/static_page/section_settings_form/show.erb +3 -1
- data/app/cells/decidim/content_blocks/static_page/summary_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/static_page/summary_settings_form/show.erb +3 -1
- data/app/cells/decidim/content_blocks/static_page/two_pane_section_cell.rb +2 -2
- data/app/cells/decidim/content_blocks/static_page/two_pane_section_settings_form/show.erb +4 -2
- data/app/cells/decidim/data_consent/category.erb +5 -5
- data/app/cells/decidim/resource_types_filter/show.erb +2 -2
- data/app/cells/decidim/upload_modal_cell.rb +5 -0
- data/app/commands/decidim/destroy_account.rb +12 -1
- data/app/commands/decidim/multiple_attachments_methods.rb +28 -27
- data/app/controllers/concerns/decidim/devise_controllers.rb +10 -0
- data/app/controllers/decidim/download_your_data_controller.rb +1 -1
- data/app/controllers/decidim/notifications_subscriptions_controller.rb +8 -0
- data/app/controllers/decidim/private_downloads_controller.rb +29 -0
- data/app/jobs/decidim/process_inactive_participant_job.rb +0 -7
- data/app/mailers/decidim/delete_user_mailer.rb +14 -0
- data/app/mailers/decidim/participants_account_mailer.rb +0 -16
- data/app/models/decidim/attachment.rb +20 -2
- data/app/models/decidim/authorization.rb +7 -0
- data/app/models/decidim/moderation.rb +1 -1
- data/app/models/decidim/participatory_space_private_user.rb +1 -1
- data/app/models/decidim/private_download.rb +61 -0
- data/app/models/decidim/private_export.rb +6 -0
- data/app/models/decidim/user_base_entity.rb +17 -2
- data/app/models/decidim/user_moderation.rb +1 -1
- data/app/packs/src/decidim/controllers/form_validator/form_validator.js +6 -6
- data/app/packs/src/decidim/controllers/form_validator/form_validator.test.js +23 -1
- data/app/packs/src/decidim/controllers/mention/controller.js +296 -140
- data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +120 -457
- data/app/packs/src/decidim/controllers/multiple_mentions/controller.js +68 -32
- data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +30 -23
- data/app/packs/src/decidim/direct_uploads/upload_field.js +4 -4
- data/app/packs/src/decidim/direct_uploads/upload_modal.js +11 -7
- data/app/packs/src/decidim/geocoding/reverse_geocoding.js +15 -5
- data/app/packs/src/decidim/geocoding/reverse_geocoding.test.js +197 -0
- data/app/packs/src/decidim/index.js +4 -3
- data/app/packs/src/decidim/sw/push-permissions.js +48 -13
- data/app/packs/src/decidim/sw/sw.js +1 -1
- data/app/packs/src/decidim/utilities/text.js +6 -6
- data/app/packs/stylesheets/decidim/_conversations.scss +14 -0
- data/app/packs/stylesheets/decidim/_editor_suggestions.scss +49 -0
- data/app/packs/stylesheets/decidim/_floating_help.scss +1 -1
- data/app/packs/stylesheets/decidim/_tom_select.scss +23 -0
- data/app/packs/stylesheets/decidim/application.scss +2 -0
- data/app/packs/stylesheets/decidim/editor.scss +2 -33
- data/app/packs/stylesheets/decidim/geocoding_addons.scss +10 -2
- data/app/presenters/decidim/menu_item_presenter.rb +9 -3
- data/app/presenters/decidim/stats_presenter.rb +1 -1
- data/app/services/decidim/notifications_subscriptions_persistor.rb +6 -0
- data/app/services/decidim/push_subscription_endpoint_validator.rb +34 -0
- data/app/services/decidim/send_push_notification.rb +5 -1
- data/app/uploaders/decidim/image_uploader.rb +1 -1
- data/app/views/decidim/delete_user_mailer/delete.html.erb +6 -0
- data/app/views/decidim/gamification/badges/index.html.erb +1 -1
- data/app/views/decidim/homepage/show.html.erb +1 -1
- data/app/views/decidim/last_activities/index.html.erb +1 -1
- data/app/views/decidim/messaging/conversations/_error_modal.html.erb +11 -19
- data/app/views/decidim/messaging/conversations/error.js.erb +12 -7
- data/app/views/decidim/newsletters/unsubscribe.html.erb +1 -1
- data/app/views/decidim/notifications_settings/show.html.erb +5 -5
- data/app/views/decidim/offline/show.html.erb +1 -1
- data/app/views/decidim/pages/index.html.erb +1 -1
- data/app/views/decidim/profiles/show.html.erb +1 -1
- data/app/views/decidim/shared/_resource_actions.html.erb +4 -4
- data/app/views/decidim/user_activities/index.html.erb +1 -1
- data/app/views/layouts/decidim/_wrapper.html.erb +2 -2
- data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
- data/app/views/layouts/decidim/shared/_layout_center.html.erb +1 -1
- data/app/views/layouts/decidim/shared/_layout_item.html.erb +1 -1
- data/app/views/layouts/decidim/shared/_layout_two_col.html.erb +1 -1
- data/config/locales/ar.yml +0 -5
- data/config/locales/bg.yml +0 -6
- data/config/locales/ca-IT.yml +10 -10
- data/config/locales/ca.yml +10 -10
- data/config/locales/cs.yml +4 -13
- data/config/locales/de.yml +5 -17
- data/config/locales/el.yml +0 -4
- data/config/locales/en.yml +9 -9
- data/config/locales/es-MX.yml +9 -9
- data/config/locales/es-PY.yml +9 -9
- data/config/locales/es.yml +9 -9
- data/config/locales/eu.yml +49 -49
- data/config/locales/fi-plain.yml +9 -9
- data/config/locales/fi.yml +11 -11
- data/config/locales/fr-CA.yml +10 -9
- data/config/locales/fr.yml +10 -9
- data/config/locales/gl.yml +0 -3
- data/config/locales/hu.yml +0 -5
- data/config/locales/id-ID.yml +0 -3
- data/config/locales/it.yml +11 -3
- data/config/locales/ja.yml +24 -21
- data/config/locales/lb.yml +0 -5
- data/config/locales/lt.yml +0 -5
- data/config/locales/lv.yml +0 -3
- data/config/locales/nl.yml +0 -5
- data/config/locales/no.yml +0 -5
- data/config/locales/pl.yml +2 -8
- data/config/locales/pt-BR.yml +3 -16
- data/config/locales/pt.yml +0 -5
- data/config/locales/ro-RO.yml +1 -14
- data/config/locales/sk.yml +1408 -4
- data/config/locales/sv.yml +3 -11
- data/config/locales/tr-TR.yml +0 -6
- data/config/locales/zh-CN.yml +0 -5
- data/config/locales/zh-TW.yml +0 -5
- data/config/routes.rb +1 -0
- data/lib/decidim/api/functions/user_entity_list.rb +2 -0
- data/lib/decidim/attachment_attributes.rb +58 -9
- data/lib/decidim/command.rb +1 -1
- data/lib/decidim/content_renderers/base_renderer.rb +112 -0
- data/lib/decidim/content_renderers/blob_renderer.rb +4 -7
- data/lib/decidim/content_renderers/mention_resource_renderer.rb +10 -6
- data/lib/decidim/content_renderers/resource_renderer.rb +16 -7
- data/lib/decidim/content_renderers/user_renderer.rb +11 -9
- data/lib/decidim/core/content_blocks/registry_manager.rb +4 -4
- data/lib/decidim/core/engine.rb +8 -0
- data/lib/decidim/core/test/factories.rb +3 -0
- data/lib/decidim/core/test/shared_examples/admin_resource_gallery_examples.rb +10 -10
- data/lib/decidim/core/test/shared_examples/comments_examples.rb +6 -6
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/map/autocomplete.rb +4 -3
- data/lib/decidim/searchable.rb +5 -0
- data/lib/decidim/view_model.rb +1 -1
- data/lib/tasks/decidim_mailers_tasks.rake +31 -9
- metadata +14 -10
- data/app/commands/decidim/gallery_methods.rb +0 -107
- data/app/packs/src/decidim/vendor/tribute.js +0 -1890
- data/app/packs/stylesheets/decidim/_tribute.scss +0 -36
- data/app/views/decidim/participants_account_mailer/removal_notification.html.erb +0 -11
|
@@ -69,12 +69,27 @@ module Decidim
|
|
|
69
69
|
Decidim::UserBaseEntity.joins(:follows).where(decidim_follows: { user: self }).blocked.exists?
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
ransacker :role do
|
|
73
|
+
Arel.sql(%{CASE WHEN "decidim_users"."admin" = true THEN 'admin' ELSE cast("decidim_users"."roles" as text) END})
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
ransacker :user_moderation_report_count do
|
|
77
|
+
query = <<~SQL.squish
|
|
78
|
+
(
|
|
79
|
+
SELECT COALESCE(MAX(decidim_user_moderations.report_count), 0)
|
|
80
|
+
FROM decidim_user_moderations
|
|
81
|
+
WHERE decidim_user_moderations.decidim_user_id = decidim_users.id
|
|
82
|
+
)
|
|
83
|
+
SQL
|
|
84
|
+
Arel.sql(query)
|
|
85
|
+
end
|
|
86
|
+
|
|
72
87
|
def self.ransackable_attributes(auth_object = nil)
|
|
73
|
-
base = %w(name email nickname last_sign_in_at)
|
|
88
|
+
base = %w(name email nickname last_sign_in_at created_at)
|
|
74
89
|
|
|
75
90
|
return base unless auth_object&.admin?
|
|
76
91
|
|
|
77
|
-
base + %w(invitation_sent_at invitation_accepted_at officialized_at)
|
|
92
|
+
base + %w(invitation_sent_at invitation_accepted_at officialized_at role user_moderation_report_count)
|
|
78
93
|
end
|
|
79
94
|
|
|
80
95
|
def self.ransackable_associations(_auth_object = nil)
|
|
@@ -419,13 +419,13 @@ class FormValidator {
|
|
|
419
419
|
return true;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
422
|
+
if (isValid) {
|
|
423
|
+
this.removeCheckboxGroupErrorClasses(groupName);
|
|
424
|
+
} else {
|
|
425
|
+
checkboxGroup.forEach((checkboxElement) => {
|
|
426
426
|
this.addInputErrorClasses(checkboxElement, ["required"]);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
429
|
|
|
430
430
|
return isValid;
|
|
431
431
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* eslint max-lines: ["error",
|
|
1
|
+
/* eslint max-lines: ["error", 712] */
|
|
2
2
|
/* global jest */
|
|
3
3
|
/**
|
|
4
4
|
* @jest-environment jsdom
|
|
@@ -253,6 +253,28 @@ describe("FormValidator", () => {
|
|
|
253
253
|
const isValidTwo = validatorInstance.validateCheckboxGroup("multiCheckboxes");
|
|
254
254
|
expect(isValidTwo).toBe(true);
|
|
255
255
|
});
|
|
256
|
+
|
|
257
|
+
it("should remove error classes once for the group, not per checkbox", () => {
|
|
258
|
+
formElement.innerHTML = `
|
|
259
|
+
<fieldset>
|
|
260
|
+
<input type="checkbox" id="cb-1" name="largeGroup" value="1" required>
|
|
261
|
+
<input type="checkbox" id="cb-2" name="largeGroup" value="2" required>
|
|
262
|
+
<input type="checkbox" id="cb-3" name="largeGroup" value="3" required>
|
|
263
|
+
</fieldset>
|
|
264
|
+
<button type="submit">Submit</button>
|
|
265
|
+
`;
|
|
266
|
+
validatorInstance = new FormValidator(formElement);
|
|
267
|
+
formElement.querySelector("#cb-1").checked = true;
|
|
268
|
+
|
|
269
|
+
const removeGroupSpy = jest.spyOn(validatorInstance, "removeCheckboxGroupErrorClasses");
|
|
270
|
+
const removeInputSpy = jest.spyOn(validatorInstance, "removeInputErrorClasses");
|
|
271
|
+
|
|
272
|
+
const isValid = validatorInstance.validateCheckboxGroup("largeGroup");
|
|
273
|
+
|
|
274
|
+
expect(isValid).toBe(true);
|
|
275
|
+
expect(removeGroupSpy).toHaveBeenCalledTimes(1);
|
|
276
|
+
expect(removeInputSpy).not.toHaveBeenCalled();
|
|
277
|
+
});
|
|
256
278
|
});
|
|
257
279
|
|
|
258
280
|
describe("Custom Validators", () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
/* eslint max-lines: ["error", 400] */
|
|
2
|
+
|
|
1
3
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
import Tribute from "src/decidim/vendor/tribute";
|
|
3
4
|
|
|
4
5
|
export default class extends Controller {
|
|
5
6
|
connect() {
|
|
@@ -9,7 +10,12 @@ export default class extends Controller {
|
|
|
9
10
|
menuItemLimit: 5
|
|
10
11
|
};
|
|
11
12
|
|
|
12
|
-
this.
|
|
13
|
+
this.suggestion = null;
|
|
14
|
+
this.suggestions = [];
|
|
15
|
+
this.selectedIndex = -1;
|
|
16
|
+
this.isActive = false;
|
|
17
|
+
this.currentMentionStart = null;
|
|
18
|
+
this.requestId = 0;
|
|
13
19
|
this.isInitialized = false;
|
|
14
20
|
|
|
15
21
|
// Prevent initialization inside editor components
|
|
@@ -17,100 +23,62 @@ export default class extends Controller {
|
|
|
17
23
|
return;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
this.element.removeAttribute("data-tribute");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
this.createTribute();
|
|
26
|
+
this.createSuggestionContainer();
|
|
25
27
|
this.setupEventListeners();
|
|
26
28
|
this.isInitialized = true;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
disconnect() {
|
|
30
|
-
if (this.
|
|
31
|
-
this.
|
|
32
|
+
if (this.suggestion) {
|
|
33
|
+
this.suggestion.remove();
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
this.element.removeEventListener("focusin", this.handleFocusIn);
|
|
35
37
|
this.element.removeEventListener("focusout", this.handleFocusOut);
|
|
36
38
|
this.element.removeEventListener("input", this.handleInput);
|
|
39
|
+
this.element.removeEventListener("keydown", this.handleKeyDown);
|
|
40
|
+
document.removeEventListener("click", this.handleDocumentClick);
|
|
37
41
|
|
|
38
|
-
this.
|
|
42
|
+
this.suggestion = null;
|
|
43
|
+
this.suggestions = [];
|
|
39
44
|
this.isInitialized = false;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
createTribute() {
|
|
48
|
-
const noMatchTemplate = this.options.noDataFoundMessage
|
|
49
|
-
? () => `<li>${this.options.noDataFoundMessage}</li>`
|
|
50
|
-
: null;
|
|
51
|
-
|
|
52
|
-
this.tribute = new Tribute({
|
|
53
|
-
trigger: "@",
|
|
54
|
-
values: this.debounce((text, callback) => {
|
|
55
|
-
this.performRemoteSearch(text, callback);
|
|
56
|
-
}, this.options.debounceDelay),
|
|
57
|
-
positionMenu: true,
|
|
58
|
-
menuContainer: null,
|
|
59
|
-
allowSpaces: true,
|
|
60
|
-
menuItemLimit: this.options.menuItemLimit,
|
|
61
|
-
fillAttr: "nickname",
|
|
62
|
-
selectClass: "highlight",
|
|
63
|
-
noMatchTemplate: noMatchTemplate,
|
|
64
|
-
lookup: (item) => item.nickname + item.name,
|
|
65
|
-
selectTemplate: (item) => {
|
|
66
|
-
if (typeof item === "undefined") {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
return item.original.nickname;
|
|
70
|
-
},
|
|
71
|
-
menuItemTemplate: (item) => {
|
|
72
|
-
return `
|
|
73
|
-
<img src="${item.original.avatarUrl}" alt="author-avatar">
|
|
74
|
-
<strong>${item.original.nickname}</strong>
|
|
75
|
-
<small>${item.original.name}</small>
|
|
76
|
-
`;
|
|
77
|
-
}
|
|
78
|
-
});
|
|
47
|
+
createSuggestionContainer() {
|
|
48
|
+
this.suggestion = document.createElement("div");
|
|
49
|
+
this.suggestion.classList.add("editor-suggestions", "hidden", "hide");
|
|
50
|
+
document.body.append(this.suggestion);
|
|
51
|
+
this.suggestion.addEventListener("mousedown", (event) => event.preventDefault());
|
|
79
52
|
|
|
80
|
-
this.
|
|
53
|
+
this.performRemoteSearch = this.debounce(this.performRemoteSearch.bind(this), this.options.debounceDelay);
|
|
81
54
|
}
|
|
82
55
|
|
|
83
|
-
/**
|
|
84
|
-
* Set up event listeners for the element
|
|
85
|
-
* @returns {void}
|
|
86
|
-
* @private
|
|
87
|
-
*/
|
|
88
56
|
setupEventListeners() {
|
|
89
|
-
|
|
90
|
-
this.
|
|
91
|
-
this.
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
* Handle focus out event
|
|
109
|
-
* @param {Event} event - The focus out event
|
|
110
|
-
* @returns {void}
|
|
111
|
-
* @private
|
|
112
|
-
*/
|
|
57
|
+
this.handleFocusIn = this.handleFocusIn.bind(this);
|
|
58
|
+
this.handleFocusOut = this.handleFocusOut.bind(this);
|
|
59
|
+
this.handleInput = this.handleInput.bind(this);
|
|
60
|
+
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
61
|
+
this.handleDocumentClick = this.handleDocumentClick.bind(this);
|
|
62
|
+
|
|
63
|
+
this.element.addEventListener("focusin", this.handleFocusIn);
|
|
64
|
+
this.element.addEventListener("focusout", this.handleFocusOut);
|
|
65
|
+
this.element.addEventListener("input", this.handleInput);
|
|
66
|
+
this.element.addEventListener("keydown", this.handleKeyDown);
|
|
67
|
+
document.addEventListener("click", this.handleDocumentClick);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
handleFocusIn() {
|
|
71
|
+
if (this.element.parentNode) {
|
|
72
|
+
this.element.parentNode.classList.add("is-active");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
113
76
|
handleFocusOut(event) {
|
|
77
|
+
if (this.suggestion && this.suggestion.contains(event.relatedTarget)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.hideSuggestions();
|
|
114
82
|
const parent = event.target.parentNode;
|
|
115
83
|
|
|
116
84
|
if (parent && parent.classList.contains("is-active")) {
|
|
@@ -118,63 +86,99 @@ export default class extends Controller {
|
|
|
118
86
|
}
|
|
119
87
|
}
|
|
120
88
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
89
|
+
handleInput() {
|
|
90
|
+
const trigger = this.mentionTriggerAtCursor();
|
|
91
|
+
if (!trigger) {
|
|
92
|
+
this.hideSuggestions();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.currentMentionStart = trigger.start;
|
|
97
|
+
this.performRemoteSearch(trigger.query);
|
|
98
|
+
}
|
|
129
99
|
|
|
130
|
-
|
|
100
|
+
handleKeyDown(event) {
|
|
101
|
+
if (!this.isActive) {
|
|
131
102
|
return;
|
|
132
103
|
}
|
|
133
104
|
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
105
|
+
if (event.key === "Escape") {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
this.hideSuggestions();
|
|
108
|
+
} else if (event.key === "ArrowDown") {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
this.updateSelectedIndex(1);
|
|
111
|
+
this.renderSuggestions();
|
|
112
|
+
} else if (event.key === "ArrowUp") {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
this.updateSelectedIndex(-1);
|
|
115
|
+
this.renderSuggestions();
|
|
116
|
+
} else if (event.key === "Enter") {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
this.selectSuggestion(this.selectedIndex);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
140
121
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
122
|
+
handleDocumentClick(event) {
|
|
123
|
+
if (this.element === event.target || this.element.contains(event.target) || this.suggestion?.contains(event.target)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.hideSuggestions();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
mentionTriggerAtCursor() {
|
|
131
|
+
const value = this.element.value || "";
|
|
132
|
+
const caretPosition = this.element.selectionStart;
|
|
133
|
+
|
|
134
|
+
if (typeof caretPosition !== "number") {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const textBeforeCursor = value.slice(0, caretPosition);
|
|
139
|
+
const mentionMatch = textBeforeCursor.match(/(?:^|\s)@([\w.-]{2,})$/);
|
|
140
|
+
if (!mentionMatch) {
|
|
141
|
+
return null;
|
|
144
142
|
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
query: mentionMatch[1],
|
|
146
|
+
start: caretPosition - mentionMatch[1].length - 1
|
|
147
|
+
};
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* @returns {void}
|
|
152
|
-
* @private
|
|
153
|
-
*/
|
|
154
|
-
performRemoteSearch(text, callback) {
|
|
150
|
+
performRemoteSearch(text) {
|
|
151
|
+
const currentRequestId = this.requestId + 1;
|
|
152
|
+
this.requestId = currentRequestId;
|
|
153
|
+
|
|
155
154
|
const query = `{users(filter:{wildcard:"${text}"}){nickname,name,avatarUrl,__typename}}`;
|
|
156
155
|
const apiPath = window.Decidim.config.get("api_path");
|
|
157
156
|
|
|
158
157
|
this.makeRequest(apiPath, { query }).
|
|
159
158
|
then((response) => {
|
|
159
|
+
if (this.requestId !== currentRequestId) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
160
163
|
const data = response.data.users || [];
|
|
161
|
-
|
|
164
|
+
const sortedData = data.sort((first, second) => first.nickname.localeCompare(second.nickname));
|
|
165
|
+
this.suggestions = sortedData.slice(0, this.options.menuItemLimit);
|
|
166
|
+
this.selectedIndex = this.suggestions.length > 0
|
|
167
|
+
? 0
|
|
168
|
+
: -1;
|
|
169
|
+
this.renderSuggestions({ showNoResults: true });
|
|
162
170
|
}).
|
|
163
171
|
catch(() => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
if (this.requestId !== currentRequestId) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.suggestions = [];
|
|
177
|
+
this.selectedIndex = -1;
|
|
178
|
+
this.renderSuggestions({ showNoResults: true });
|
|
168
179
|
});
|
|
169
180
|
}
|
|
170
181
|
|
|
171
|
-
/**
|
|
172
|
-
* Make an HTTP POST request
|
|
173
|
-
* @param {string} url - The request URL
|
|
174
|
-
* @param {Object} data - The request data
|
|
175
|
-
* @returns {Promise} The request promise
|
|
176
|
-
* @private
|
|
177
|
-
*/
|
|
178
182
|
makeRequest(url, data) {
|
|
179
183
|
return fetch(url, {
|
|
180
184
|
method: "POST",
|
|
@@ -191,35 +195,191 @@ export default class extends Controller {
|
|
|
191
195
|
});
|
|
192
196
|
}
|
|
193
197
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
renderSuggestions({ showNoResults = false } = {}) {
|
|
199
|
+
if (!this.suggestion) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.suggestion.innerHTML = "";
|
|
204
|
+
|
|
205
|
+
if (this.suggestions.length < 1) {
|
|
206
|
+
this.isActive = false;
|
|
207
|
+
|
|
208
|
+
if (showNoResults && this.options.noDataFoundMessage) {
|
|
209
|
+
const noResultsItem = document.createElement("button");
|
|
210
|
+
noResultsItem.type = "button";
|
|
211
|
+
noResultsItem.disabled = true;
|
|
212
|
+
noResultsItem.classList.add("editor-suggestions-item", "editor-suggestions-item-disabled");
|
|
213
|
+
noResultsItem.textContent = this.options.noDataFoundMessage;
|
|
214
|
+
this.suggestion.append(noResultsItem);
|
|
215
|
+
this.positionSuggestionMenu();
|
|
216
|
+
this.suggestion.classList.remove("hidden", "hide");
|
|
217
|
+
} else {
|
|
218
|
+
this.suggestion.classList.add("hidden", "hide");
|
|
219
|
+
}
|
|
220
|
+
|
|
201
221
|
return;
|
|
202
222
|
}
|
|
203
223
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
this.suggestions.forEach((item, index) => {
|
|
225
|
+
const suggestionItem = document.createElement("button");
|
|
226
|
+
suggestionItem.type = "button";
|
|
227
|
+
suggestionItem.classList.add("editor-suggestions-item");
|
|
228
|
+
suggestionItem.dataset.index = index;
|
|
229
|
+
|
|
230
|
+
if (item.avatarUrl) {
|
|
231
|
+
const avatar = document.createElement("img");
|
|
232
|
+
avatar.classList.add("editor-suggestions-item-avatar");
|
|
233
|
+
avatar.src = item.avatarUrl;
|
|
234
|
+
avatar.alt = item.name || item.nickname;
|
|
235
|
+
suggestionItem.append(avatar);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const label = document.createElement("span");
|
|
239
|
+
label.classList.add("editor-suggestions-item-label");
|
|
240
|
+
label.textContent = `${item.nickname} (${item.name})`;
|
|
241
|
+
suggestionItem.append(label);
|
|
207
242
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// Remove inline styles for absolute positioning
|
|
211
|
-
tributeContainer.removeAttribute("style");
|
|
243
|
+
if (index === this.selectedIndex) {
|
|
244
|
+
suggestionItem.dataset.selected = "true";
|
|
212
245
|
}
|
|
246
|
+
|
|
247
|
+
suggestionItem.addEventListener("click", (event) => {
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
this.selectSuggestion(index);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
this.suggestion.append(suggestionItem);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
this.positionSuggestionMenu();
|
|
256
|
+
this.isActive = true;
|
|
257
|
+
this.suggestion.classList.remove("hidden", "hide");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
positionSuggestionMenu() {
|
|
261
|
+
if (!this.suggestion || this.currentMentionStart === null) {
|
|
262
|
+
return;
|
|
213
263
|
}
|
|
264
|
+
|
|
265
|
+
const coordinates = this.coordinatesForTextIndex(this.currentMentionStart);
|
|
266
|
+
if (!coordinates) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Object.assign(this.suggestion.style, {
|
|
271
|
+
position: "absolute",
|
|
272
|
+
top: `${coordinates.top}px`,
|
|
273
|
+
left: `${coordinates.left}px`
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
coordinatesForTextIndex(index) {
|
|
278
|
+
const element = this.element;
|
|
279
|
+
const styles = window.getComputedStyle(element);
|
|
280
|
+
const elementRect = element.getBoundingClientRect();
|
|
281
|
+
const borderTopWidth = parseFloat(styles.borderTopWidth) || 0;
|
|
282
|
+
const borderLeftWidth = parseFloat(styles.borderLeftWidth) || 0;
|
|
283
|
+
const lineHeight = parseFloat(styles.lineHeight) || 16;
|
|
284
|
+
|
|
285
|
+
const mirror = document.createElement("div");
|
|
286
|
+
const mirrorStyles = [
|
|
287
|
+
"fontFamily",
|
|
288
|
+
"fontSize",
|
|
289
|
+
"fontWeight",
|
|
290
|
+
"fontStyle",
|
|
291
|
+
"letterSpacing",
|
|
292
|
+
"textTransform",
|
|
293
|
+
"wordSpacing",
|
|
294
|
+
"textIndent",
|
|
295
|
+
"boxSizing",
|
|
296
|
+
"width",
|
|
297
|
+
"paddingTop",
|
|
298
|
+
"paddingRight",
|
|
299
|
+
"paddingBottom",
|
|
300
|
+
"paddingLeft",
|
|
301
|
+
"borderTopWidth",
|
|
302
|
+
"borderRightWidth",
|
|
303
|
+
"borderBottomWidth",
|
|
304
|
+
"borderLeftWidth",
|
|
305
|
+
"borderStyle",
|
|
306
|
+
"lineHeight"
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
mirrorStyles.forEach((property) => {
|
|
310
|
+
mirror.style[property] = styles[property];
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
mirror.style.position = "absolute";
|
|
314
|
+
mirror.style.visibility = "hidden";
|
|
315
|
+
mirror.style.whiteSpace = element.nodeName === "TEXTAREA"
|
|
316
|
+
? "pre-wrap"
|
|
317
|
+
: "pre";
|
|
318
|
+
mirror.style.overflowWrap = "break-word";
|
|
319
|
+
mirror.style.top = "0";
|
|
320
|
+
mirror.style.left = "-9999px";
|
|
321
|
+
|
|
322
|
+
const before = document.createTextNode((element.value || "").slice(0, index));
|
|
323
|
+
const marker = document.createElement("span");
|
|
324
|
+
marker.textContent = "@";
|
|
325
|
+
|
|
326
|
+
mirror.append(before);
|
|
327
|
+
mirror.append(marker);
|
|
328
|
+
document.body.append(mirror);
|
|
329
|
+
|
|
330
|
+
const top = elementRect.top + window.scrollY + marker.offsetTop - element.scrollTop + borderTopWidth + lineHeight;
|
|
331
|
+
const left = elementRect.left + window.scrollX + marker.offsetLeft - element.scrollLeft + borderLeftWidth;
|
|
332
|
+
|
|
333
|
+
mirror.remove();
|
|
334
|
+
|
|
335
|
+
return { top, left };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
updateSelectedIndex(direction) {
|
|
339
|
+
if (this.suggestions.length < 1) {
|
|
340
|
+
this.selectedIndex = -1;
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const maxIndex = this.suggestions.length - 1;
|
|
345
|
+
const nextIndex = this.selectedIndex + direction;
|
|
346
|
+
|
|
347
|
+
this.selectedIndex = Math.max(0, Math.min(nextIndex, maxIndex));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
selectSuggestion(index) {
|
|
351
|
+
const selectedItem = this.suggestions[index];
|
|
352
|
+
if (!selectedItem || this.currentMentionStart === null) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const cursorPosition = this.element.selectionStart;
|
|
357
|
+
const value = this.element.value || "";
|
|
358
|
+
const mentionValue = `${selectedItem.nickname} `;
|
|
359
|
+
const newValue = `${value.slice(0, this.currentMentionStart)}${mentionValue}${value.slice(cursorPosition)}`;
|
|
360
|
+
|
|
361
|
+
this.element.value = newValue;
|
|
362
|
+
|
|
363
|
+
const newPosition = this.currentMentionStart + mentionValue.length;
|
|
364
|
+
this.element.setSelectionRange(newPosition, newPosition);
|
|
365
|
+
|
|
366
|
+
this.element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
367
|
+
this.hideSuggestions();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
hideSuggestions() {
|
|
371
|
+
if (!this.suggestion) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.isActive = false;
|
|
376
|
+
this.currentMentionStart = null;
|
|
377
|
+
this.selectedIndex = -1;
|
|
378
|
+
this.suggestions = [];
|
|
379
|
+
this.suggestion.classList.add("hidden", "hide");
|
|
380
|
+
this.suggestion.innerHTML = "";
|
|
214
381
|
}
|
|
215
382
|
|
|
216
|
-
/**
|
|
217
|
-
* Create a debounced version of a function
|
|
218
|
-
* @param {Function} callback - The function to debounce
|
|
219
|
-
* @param {number} wait - The debounce delay in milliseconds
|
|
220
|
-
* @returns {Function} The debounced function
|
|
221
|
-
* @private
|
|
222
|
-
*/
|
|
223
383
|
debounce(callback, wait) {
|
|
224
384
|
let timeout = null;
|
|
225
385
|
return (...args) => {
|
|
@@ -233,10 +393,6 @@ export default class extends Controller {
|
|
|
233
393
|
};
|
|
234
394
|
}
|
|
235
395
|
|
|
236
|
-
/**
|
|
237
|
-
* Check if the component is initialized
|
|
238
|
-
* @returns {boolean} True if initialized, false otherwise
|
|
239
|
-
*/
|
|
240
396
|
get initialized() {
|
|
241
397
|
return this.isInitialized;
|
|
242
398
|
}
|