decidim-core 0.32.1 → 0.32.2
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/content_blocks/participatory_space_main_data/title.erb +1 -14
- data/app/cells/decidim/following_cell.rb +1 -1
- data/app/cells/decidim/member/show.erb +1 -1
- data/app/cells/decidim/nav_links/show.erb +3 -1
- data/app/cells/decidim/share_text_widget/modal.erb +1 -0
- data/app/cells/decidim/share_widget/modal.erb +1 -0
- data/app/cells/decidim/share_widget/qr_code_modal.erb +2 -4
- data/app/commands/decidim/create_omniauth_registration.rb +17 -2
- data/app/controllers/concerns/decidim/devise_authentication_methods.rb +4 -0
- data/app/controllers/concerns/decidim/devise_controllers.rb +2 -0
- data/app/controllers/concerns/decidim/disable_redirection_to_external_host.rb +2 -0
- data/app/controllers/concerns/decidim/filter_resource.rb +2 -0
- data/app/controllers/concerns/decidim/impersonate_users.rb +2 -2
- data/app/controllers/concerns/decidim/locale_switcher.rb +12 -10
- data/app/controllers/concerns/decidim/needs_permission.rb +22 -10
- data/app/controllers/concerns/decidim/onboarding_action_methods.rb +2 -0
- data/app/controllers/concerns/decidim/paginable.rb +7 -3
- data/app/controllers/concerns/decidim/payload_info.rb +2 -0
- data/app/controllers/concerns/decidim/resource_versions_concern.rb +14 -0
- data/app/controllers/decidim/application_controller.rb +0 -6
- data/app/controllers/decidim/authorization_modals_controller.rb +2 -0
- data/app/controllers/decidim/components/base_controller.rb +2 -0
- data/app/controllers/decidim/devise/passwords_controller.rb +8 -0
- data/app/controllers/decidim/favicon_controller.rb +9 -0
- data/app/controllers/decidim/free_resource_authorization_modals_controller.rb +2 -0
- data/app/controllers/decidim/gamification/badges_controller.rb +2 -0
- data/app/controllers/decidim/homepage_controller.rb +4 -1
- data/app/controllers/decidim/likes_controller.rb +1 -1
- data/app/controllers/decidim/links_controller.rb +2 -0
- data/app/controllers/decidim/manifests_controller.rb +2 -0
- data/app/controllers/decidim/messaging/conversations_controller.rb +10 -4
- data/app/controllers/decidim/newsletters_controller.rb +4 -0
- data/app/controllers/decidim/notifications_subscriptions_controller.rb +4 -0
- data/app/controllers/decidim/offline_controller.rb +5 -1
- data/app/controllers/decidim/open_data_controller.rb +5 -1
- data/app/controllers/decidim/pages_controller.rb +31 -3
- data/app/controllers/decidim/private_downloads_controller.rb +2 -0
- data/app/controllers/decidim/profiles_controller.rb +19 -1
- data/app/controllers/decidim/qr_controller.rb +3 -1
- data/app/controllers/decidim/resource_autocomplete_controller.rb +2 -0
- data/app/controllers/decidim/searches_controller.rb +2 -0
- data/app/controllers/decidim/short_links_controller.rb +4 -0
- data/app/controllers/decidim/static_map_controller.rb +2 -0
- data/app/controllers/decidim/timeouts_controller.rb +4 -0
- data/app/controllers/decidim/tos_controller.rb +2 -0
- data/app/controllers/decidim/upload_validations_controller.rb +4 -2
- data/app/controllers/decidim/user_activities_controller.rb +3 -0
- data/app/forms/decidim/upload_validation_form.rb +16 -10
- data/app/helpers/concerns/decidim/user_role_checker.rb +35 -10
- data/app/helpers/decidim/decidim_form_helper.rb +1 -1
- data/app/helpers/decidim/sanitize_helper.rb +11 -1
- data/app/jobs/decidim/application_job.rb +4 -0
- data/app/models/decidim/authorization.rb +24 -0
- data/app/models/decidim/oauth_application.rb +1 -0
- data/app/models/decidim/user.rb +1 -0
- data/app/models/decidim/user_base_entity.rb +1 -1
- data/app/packs/src/decidim/callout.js +10 -0
- data/app/packs/src/decidim/callout.test.js +32 -0
- data/app/packs/src/decidim/controllers/accordion/accordion.test.js +64 -0
- data/app/packs/src/decidim/controllers/accordion/controller.js +43 -0
- data/app/packs/src/decidim/controllers/autofocus/controller.js +30 -0
- data/app/packs/src/decidim/controllers/language_change/controller.js +21 -8
- data/app/packs/src/decidim/controllers/language_change/language_change.test.js +0 -1
- data/app/packs/src/decidim/controllers/mention/controller.js +32 -3
- data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +8 -5
- data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +1 -1
- data/app/packs/src/decidim/datepicker/datepicker_functions.js +41 -25
- data/app/packs/src/decidim/datepicker/generate_datepicker.js +4 -4
- data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +64 -0
- data/app/packs/src/decidim/datepicker/test/time.test.js +39 -24
- data/app/packs/src/decidim/editor/common/suggestion.js +32 -22
- data/app/packs/src/decidim/editor/extensions/mention/index.js +14 -3
- data/app/packs/src/decidim/editor/index.js +4 -0
- data/app/packs/src/decidim/editor/test/extensions/mention.test.js +32 -9
- data/app/packs/src/decidim/refactor/implementation/autofocus.js +22 -0
- data/app/packs/src/decidim/refactor/implementation/autofocus.test.js +171 -0
- data/app/packs/src/decidim/utilities/dom.js +2 -2
- data/app/packs/stylesheets/decidim/_content_blocks.scss +4 -10
- data/app/packs/stylesheets/decidim/_conversations.scss +0 -8
- data/app/packs/stylesheets/decidim/_modal.scss +16 -8
- data/app/packs/stylesheets/decidim/_modal_share.scss +0 -22
- data/app/packs/stylesheets/decidim/_participatory_spaces.scss +11 -1
- data/app/packs/stylesheets/decidim/_rich_text.scss +12 -9
- data/app/packs/stylesheets/decidim/_typography.scss +5 -2
- data/app/packs/stylesheets/decidim/application.scss +1 -0
- data/app/packs/stylesheets/decidim/legacy/email.scss +0 -5
- data/app/permissions/decidim/permissions.rb +20 -0
- data/app/presenters/decidim/admin_log/component_presenter.rb +1 -1
- data/app/presenters/decidim/log/base_presenter.rb +10 -1
- data/app/presenters/decidim/log/user_presenter.rb +2 -2
- data/app/presenters/decidim/menu_item_presenter.rb +1 -5
- data/app/presenters/decidim/resource_presenter.rb +11 -0
- data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -1
- data/app/serializers/decidim/exporters/open_data_blocked_user_serializer.rb +11 -2
- data/app/serializers/decidim/exporters/open_data_moderation_serializer.rb +25 -1
- data/app/services/decidim/svg_content_validator.rb +554 -0
- data/app/uploaders/decidim/application_uploader.rb +2 -2
- data/app/validators/uploader_image_content_validator.rb +276 -0
- data/app/views/decidim/application/_radio_accordion.html.erb +1 -5
- data/app/views/decidim/messaging/conversations/_add_conversation_users.html.erb +1 -1
- data/app/views/decidim/messaging/conversations/create.js.erb +1 -3
- data/app/views/decidim/shared/_login_modal.html.erb +12 -14
- data/app/views/layouts/decidim/_mailer_logo.html.erb +1 -1
- data/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb +3 -2
- data/config/initializers/rack_attack.rb +10 -0
- data/config/locales/bg.yml +0 -1
- data/config/locales/ca-IT.yml +3 -1
- data/config/locales/ca.yml +10 -6
- data/config/locales/cs.yml +15 -1
- data/config/locales/de.yml +99 -3
- data/config/locales/en.yml +5 -1
- data/config/locales/es-MX.yml +5 -1
- data/config/locales/es-PY.yml +5 -1
- data/config/locales/es.yml +9 -5
- data/config/locales/eu.yml +161 -160
- data/config/locales/fi-plain.yml +3 -1
- data/config/locales/fi.yml +3 -1
- data/config/locales/fr-CA.yml +5 -1
- data/config/locales/fr.yml +5 -1
- data/config/locales/gl.yml +1485 -33
- data/config/locales/hu.yml +0 -1
- data/config/locales/ja.yml +3 -1
- data/config/locales/pl.yml +0 -1
- data/config/locales/pt-BR.yml +0 -1
- data/config/locales/ro-RO.yml +0 -1
- data/config/locales/sk.yml +2 -1
- data/config/locales/sv.yml +68 -12
- data/config/locales/tr-TR.yml +0 -2
- data/config/routes.rb +11 -2
- data/db/migrate/20260909000000_add_failed_attempts_and_locked_at_to_decidim_authorizations.rb +8 -0
- data/decidim-core.gemspec +1 -1
- data/lib/decidim/api/functions/user_entity_list.rb +1 -0
- data/lib/decidim/api/types/user_moderation_type.rb +2 -2
- data/lib/decidim/content_parsers/user_parser.rb +1 -1
- data/lib/decidim/content_parsers.rb +0 -1
- data/lib/decidim/content_renderers/mention_resource_renderer.rb +18 -4
- data/lib/decidim/content_renderers/user_renderer.rb +15 -4
- data/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb +77 -0
- data/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb +58 -0
- data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +23 -1
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/core.rb +9 -0
- data/lib/decidim/form_builder.rb +9 -1
- data/lib/decidim/has_upload_validations.rb +1 -0
- data/lib/decidim/legacy_form_builder.rb +4 -0
- metadata +12 -7
- data/lib/decidim/content_parsers/inline_images_parser.rb +0 -68
- data/lib/tasks/upgrade/decidim_active_storage_migration_tasks.rake +0 -79
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import autofocus from "src/decidim/refactor/implementation/autofocus"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* This controller is used to change the active tab when the language is changed in the admin or system panel.
|
|
@@ -10,30 +11,42 @@ export default class extends Controller {
|
|
|
10
11
|
connect() {
|
|
11
12
|
this.handleChange = this.handleChange.bind(this);
|
|
12
13
|
this.element.addEventListener("change", this.handleChange);
|
|
13
|
-
this.element.
|
|
14
|
+
this.tabsContent = this.element.parentElement.parentElement.nextElementSibling;
|
|
15
|
+
|
|
16
|
+
this.setActiveTab(this.element.value);
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
disconnect() {
|
|
17
20
|
this.element.removeEventListener("change", this.handleChange)
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
let tabsContent = event.target.parentElement.parentElement.nextElementSibling;
|
|
23
|
-
|
|
24
|
-
if (!tabsContent) {
|
|
23
|
+
setActiveTab(targetTabPaneSelector) {
|
|
24
|
+
if (!this.tabsContent) {
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
let activeTabContent = tabsContent.querySelector(".is-active");
|
|
28
|
+
let activeTabContent = this.tabsContent.querySelector(".is-active");
|
|
29
29
|
if (activeTabContent) {
|
|
30
30
|
activeTabContent.ariaHidden = "true";
|
|
31
31
|
activeTabContent.classList.remove("is-active");
|
|
32
32
|
}
|
|
33
|
-
let activePane = tabsContent.querySelector(targetTabPaneSelector);
|
|
33
|
+
let activePane = this.tabsContent.querySelector(targetTabPaneSelector);
|
|
34
34
|
if (activePane) {
|
|
35
35
|
activePane.ariaHidden = "false";
|
|
36
36
|
activePane.classList.add("is-active");
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
handleChange(event) {
|
|
41
|
+
this.setActiveTab(event.target.value);
|
|
42
|
+
|
|
43
|
+
if (!this.tabsContent) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const activePane = this.tabsContent.querySelector(event.target.value);
|
|
48
|
+
if (activePane) {
|
|
49
|
+
autofocus(activePane);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
39
52
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
/* eslint max-lines: ["error",
|
|
1
|
+
/* eslint max-lines: ["error", 450] */
|
|
2
2
|
|
|
3
3
|
import { Controller } from "@hotwired/stimulus"
|
|
4
4
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
connect() {
|
|
7
7
|
this.options = {
|
|
8
|
-
noDataFoundMessage: this.element.getAttribute("data-
|
|
8
|
+
noDataFoundMessage: this.element.getAttribute("data-no_results") || "No results found",
|
|
9
|
+
searchPromptMessage: this.element.getAttribute("data-search_prompt") || "Type to search participants",
|
|
9
10
|
debounceDelay: 250,
|
|
10
11
|
menuItemLimit: 5
|
|
11
12
|
};
|
|
@@ -94,6 +95,14 @@ export default class extends Controller {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
this.currentMentionStart = trigger.start;
|
|
98
|
+
|
|
99
|
+
if (trigger.query.length < 2) {
|
|
100
|
+
// Invalidate any pending debounced searches
|
|
101
|
+
this.requestId += 1;
|
|
102
|
+
this.showMessage(this.options.searchPromptMessage);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
97
106
|
this.performRemoteSearch(trigger.query);
|
|
98
107
|
}
|
|
99
108
|
|
|
@@ -136,7 +145,7 @@ export default class extends Controller {
|
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
const textBeforeCursor = value.slice(0, caretPosition);
|
|
139
|
-
const mentionMatch = textBeforeCursor.match(/(?:^|\s)@([\w.-]{
|
|
148
|
+
const mentionMatch = textBeforeCursor.match(/(?:^|\s)@([\w.-]{1,})$/);
|
|
140
149
|
if (!mentionMatch) {
|
|
141
150
|
return null;
|
|
142
151
|
}
|
|
@@ -257,6 +266,26 @@ export default class extends Controller {
|
|
|
257
266
|
this.suggestion.classList.remove("hidden", "hide");
|
|
258
267
|
}
|
|
259
268
|
|
|
269
|
+
showMessage(message) {
|
|
270
|
+
if (!this.suggestion || !message) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.suggestion.innerHTML = "";
|
|
275
|
+
this.isActive = false;
|
|
276
|
+
this.selectedIndex = -1;
|
|
277
|
+
|
|
278
|
+
const item = document.createElement("button");
|
|
279
|
+
item.type = "button";
|
|
280
|
+
item.disabled = true;
|
|
281
|
+
item.classList.add("editor-suggestions-item", "editor-suggestions-item-disabled");
|
|
282
|
+
item.textContent = message;
|
|
283
|
+
|
|
284
|
+
this.suggestion.append(item);
|
|
285
|
+
this.positionSuggestionMenu();
|
|
286
|
+
this.suggestion.classList.remove("hidden", "hide");
|
|
287
|
+
}
|
|
288
|
+
|
|
260
289
|
positionSuggestionMenu() {
|
|
261
290
|
if (!this.suggestion || this.currentMentionStart === null) {
|
|
262
291
|
return;
|
|
@@ -26,7 +26,7 @@ describe("MentionController", () => {
|
|
|
26
26
|
|
|
27
27
|
document.body.innerHTML = `
|
|
28
28
|
<div class="mention-container">
|
|
29
|
-
<input data-
|
|
29
|
+
<input data-no_results="No users found" data-search_prompt="Type to search participants" data-controller="mention" />
|
|
30
30
|
</div>
|
|
31
31
|
`;
|
|
32
32
|
|
|
@@ -111,17 +111,20 @@ describe("MentionController", () => {
|
|
|
111
111
|
expect(suggestions.classList.contains("hidden")).toBe(false);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
it("
|
|
114
|
+
it("shows search prompt when first mention character is entered", async () => {
|
|
115
115
|
await connectController();
|
|
116
116
|
|
|
117
117
|
mockElement.value = "Hello @d";
|
|
118
118
|
mockElement.setSelectionRange(mockElement.value.length, mockElement.value.length);
|
|
119
119
|
mockElement.dispatchEvent(new Event("input", { bubbles: true }));
|
|
120
120
|
|
|
121
|
-
await waitForDebounce();
|
|
122
|
-
|
|
123
121
|
const suggestions = document.querySelector(".editor-suggestions");
|
|
124
|
-
|
|
122
|
+
const promptItem = suggestions.querySelector(".editor-suggestions-item");
|
|
123
|
+
|
|
124
|
+
expect(promptItem).toBeInstanceOf(HTMLButtonElement);
|
|
125
|
+
expect(promptItem.textContent).toBe("Type to search participants");
|
|
126
|
+
expect(promptItem.disabled).toBe(true);
|
|
127
|
+
expect(suggestions.classList.contains("hidden")).toBe(false);
|
|
125
128
|
});
|
|
126
129
|
|
|
127
130
|
it("inserts selected mention on click", async () => {
|
|
@@ -68,7 +68,7 @@ describe("MultipleMentionsManager", () => {
|
|
|
68
68
|
spellcheck="false"
|
|
69
69
|
autocomplete="off"
|
|
70
70
|
placeholder="Search..."
|
|
71
|
-
data-
|
|
71
|
+
data-no_results="No results"
|
|
72
72
|
data-selected= "selected-users"
|
|
73
73
|
contenteditable="true"
|
|
74
74
|
data-direct-messages-disabled="Direct messages disabled">
|
|
@@ -6,11 +6,16 @@ export const adjustPickerPosition = (input, datePickerContainer, selector) => {
|
|
|
6
6
|
if (getComputedStyle(parent).position === "static") {
|
|
7
7
|
parent.style.position = "relative";
|
|
8
8
|
}
|
|
9
|
+
const stickyFooterItem = document.querySelector(".item__edit-sticky");
|
|
10
|
+
|
|
11
|
+
const availableBottom = stickyFooterItem
|
|
12
|
+
? stickyFooterItem.getBoundingClientRect().top
|
|
13
|
+
: window.innerHeight;
|
|
9
14
|
|
|
10
|
-
const rect = input.getBoundingClientRect();
|
|
11
15
|
const calendarHeight = datePickerContainer.offsetHeight;
|
|
16
|
+
const rect = input.getBoundingClientRect();
|
|
17
|
+
const spaceBelow = availableBottom - rect.bottom;
|
|
12
18
|
const spaceAbove = rect.top;
|
|
13
|
-
const spaceBelow = window.innerHeight - rect.bottom;
|
|
14
19
|
const openBelow = spaceBelow >= calendarHeight || spaceBelow >= spaceAbove;
|
|
15
20
|
|
|
16
21
|
if (openBelow) {
|
|
@@ -50,10 +55,7 @@ export const setMinute = (value) => {
|
|
|
50
55
|
};
|
|
51
56
|
|
|
52
57
|
export const formatInputDate = (date, formats) => {
|
|
53
|
-
const
|
|
54
|
-
const year = dateList[0];
|
|
55
|
-
const month = dateList[1];
|
|
56
|
-
const day = dateList[2];
|
|
58
|
+
const [year, month, day] = date.split("-");
|
|
57
59
|
|
|
58
60
|
if (formats.order === "m-d-y") {
|
|
59
61
|
return `${month}${formats.separator}${day}${formats.separator}${year}`;
|
|
@@ -63,10 +65,34 @@ export const formatInputDate = (date, formats) => {
|
|
|
63
65
|
return `${day}${formats.separator}${month}${formats.separator}${year}`;
|
|
64
66
|
};
|
|
65
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Splits a time string into two elements: hours and minutes. Ensures a returned
|
|
70
|
+
* array of size 2 for any kind of strings or `null` values.
|
|
71
|
+
*
|
|
72
|
+
* Examples:
|
|
73
|
+
* splitTime("") => ["00", "00"]
|
|
74
|
+
* splitTime(null) => ["00", "00"]
|
|
75
|
+
* splitTime("12") => ["12", "00"]
|
|
76
|
+
* splitTime("12:30") => ["12", "30"]
|
|
77
|
+
* splitTime("2:4") => ["02", "04"]
|
|
78
|
+
* splitTime("12:30:45") => ["12", "30"]
|
|
79
|
+
*
|
|
80
|
+
* @param {String} value The original time string, typically hh:ss.
|
|
81
|
+
* @returns {Array} An array with two elements where the first element is the
|
|
82
|
+
* hour part and the second element is the minute part.
|
|
83
|
+
*/
|
|
84
|
+
const splitTime = (value) => {
|
|
85
|
+
return [...(value || "").split(":"), "00"].splice(0, 2).map((part) => part.padStart(2, "0"));
|
|
86
|
+
};
|
|
87
|
+
|
|
66
88
|
export const formatInputTime = (time, format, input) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
if (!time) {
|
|
90
|
+
return "";
|
|
91
|
+
} else if (time.length < 1) {
|
|
92
|
+
return time;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let [hour, minute] = splitTime(time);
|
|
70
96
|
|
|
71
97
|
if (format === 12) {
|
|
72
98
|
if (Number(hour) === 12) {
|
|
@@ -80,11 +106,9 @@ export const formatInputTime = (time, format, input) => {
|
|
|
80
106
|
} else if (Number(hour) === 0) {
|
|
81
107
|
hour = "12";
|
|
82
108
|
}
|
|
83
|
-
|
|
84
|
-
return `${hour}:${minute}`;
|
|
85
109
|
};
|
|
86
110
|
|
|
87
|
-
return
|
|
111
|
+
return `${hour}:${minute}`;
|
|
88
112
|
};
|
|
89
113
|
|
|
90
114
|
export const changeHourDisplay = (change, hour, format) => {
|
|
@@ -199,29 +223,21 @@ export const formatDate = (value, formats) => {
|
|
|
199
223
|
};
|
|
200
224
|
|
|
201
225
|
export const formatTime = (value, format, inputname) => {
|
|
226
|
+
let [hour, minute] = splitTime(value);
|
|
227
|
+
|
|
202
228
|
if (format === 12) {
|
|
203
|
-
const splitValue = value.split(":");
|
|
204
|
-
let hour = splitValue[0];
|
|
205
|
-
const minute = splitValue[1];
|
|
206
229
|
if (document.getElementById(`period_am_${inputname}`).checked) {
|
|
207
|
-
|
|
208
|
-
case "12":
|
|
230
|
+
if (hour === "12") {
|
|
209
231
|
hour = "00";
|
|
210
|
-
|
|
211
|
-
return `${hour}:${minute}`;
|
|
212
|
-
default:
|
|
213
|
-
return value;
|
|
214
|
-
};
|
|
232
|
+
}
|
|
215
233
|
} else if (document.getElementById(`period_pm_${inputname}`).checked) {
|
|
216
234
|
if (Number(hour) > 0 && Number(hour) < 12) {
|
|
217
235
|
hour = `${Number(hour) + 12}`;
|
|
218
236
|
};
|
|
219
|
-
|
|
220
|
-
return `${hour}:${minute}`
|
|
221
237
|
};
|
|
222
238
|
};
|
|
223
239
|
|
|
224
|
-
return
|
|
240
|
+
return `${hour}:${minute}`;
|
|
225
241
|
};
|
|
226
242
|
|
|
227
243
|
export const updateInputValue = (input, formats, time) => {
|
|
@@ -51,7 +51,7 @@ export default function generateDatePicker(input, row, formats) {
|
|
|
51
51
|
dateColumn.appendChild(datePickerContainer);
|
|
52
52
|
|
|
53
53
|
let prevDate = null;
|
|
54
|
-
let defaultTime = input.
|
|
54
|
+
let defaultTime = input.dataset.defaultTime || "00:00"
|
|
55
55
|
|
|
56
56
|
const datePickerDisplay = (event) => {
|
|
57
57
|
if (!dateColumn.contains(event.target)) {
|
|
@@ -72,7 +72,7 @@ export default function generateDatePicker(input, row, formats) {
|
|
|
72
72
|
if (input.type === "date") {
|
|
73
73
|
input.value = `${formatDate(date.value, formats)}`;
|
|
74
74
|
} else if (input.type === "datetime-local") {
|
|
75
|
-
input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value, formats.time, input.id)
|
|
75
|
+
input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value || defaultTime, formats.time, input.id)}`;
|
|
76
76
|
};
|
|
77
77
|
};
|
|
78
78
|
});
|
|
@@ -88,7 +88,7 @@ export default function generateDatePicker(input, row, formats) {
|
|
|
88
88
|
if (input.type === "date") {
|
|
89
89
|
input.value = `${formatDate(date.value, formats)}`;
|
|
90
90
|
} else if (input.type === "datetime-local") {
|
|
91
|
-
input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value, formats.time, input.id)
|
|
91
|
+
input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value || defaultTime, formats.time, input.id)}`;
|
|
92
92
|
};
|
|
93
93
|
};
|
|
94
94
|
});
|
|
@@ -103,7 +103,7 @@ export default function generateDatePicker(input, row, formats) {
|
|
|
103
103
|
if (input.type === "date") {
|
|
104
104
|
input.value = `${pickedDate}`;
|
|
105
105
|
} else if (input.type === "datetime-local") {
|
|
106
|
-
input.value = `${pickedDate}T${formatTime(document.querySelector(`#${input.id}_time`).value, formats.time, input.id)
|
|
106
|
+
input.value = `${pickedDate}T${formatTime(document.querySelector(`#${input.id}_time`).value || defaultTime, formats.time, input.id)}`;
|
|
107
107
|
};
|
|
108
108
|
datePickerContainer.style.display = "none";
|
|
109
109
|
});
|
data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js
CHANGED
|
@@ -128,6 +128,37 @@ describe("adjustDatePickerPosition", () => {
|
|
|
128
128
|
|
|
129
129
|
expect(datePickerContainer.style.right).toBe("0px");
|
|
130
130
|
});
|
|
131
|
+
|
|
132
|
+
it("opens below when there is enough space above the sticky action area", () => {
|
|
133
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
134
|
+
top: 100,
|
|
135
|
+
bottom: 140
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
139
|
+
writable: true,
|
|
140
|
+
configurable: true,
|
|
141
|
+
value: 800
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const stickyContainer = document.createElement("div");
|
|
145
|
+
stickyContainer.className = "item__edit-sticky";
|
|
146
|
+
|
|
147
|
+
jest.spyOn(stickyContainer, "getBoundingClientRect").mockReturnValue({
|
|
148
|
+
top: 500
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
document.body.appendChild(stickyContainer);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
155
|
+
|
|
156
|
+
expect(datePickerContainer.style.top).toBe("40px");
|
|
157
|
+
expect(datePickerContainer.style.bottom).toBe("");
|
|
158
|
+
} finally {
|
|
159
|
+
document.body.removeChild(stickyContainer);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
131
162
|
});
|
|
132
163
|
|
|
133
164
|
|
|
@@ -231,4 +262,37 @@ describe("adjustTimePickerPosition", () => {
|
|
|
231
262
|
|
|
232
263
|
expect(timePicker.style.right).toBe("0px");
|
|
233
264
|
});
|
|
265
|
+
|
|
266
|
+
it("opens above when sticky action area reduces the available space below", () => {
|
|
267
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
268
|
+
top: 300,
|
|
269
|
+
bottom: 340
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
273
|
+
writable: true,
|
|
274
|
+
configurable: true,
|
|
275
|
+
value: 600
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const stickyContainer = document.createElement("div");
|
|
279
|
+
stickyContainer.className = "item__edit-sticky";
|
|
280
|
+
|
|
281
|
+
jest.spyOn(stickyContainer, "getBoundingClientRect").mockReturnValue({
|
|
282
|
+
top: 500
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
document.body.appendChild(stickyContainer);
|
|
286
|
+
|
|
287
|
+
adjustPickerPosition(
|
|
288
|
+
input,
|
|
289
|
+
timePicker,
|
|
290
|
+
".datepicker__time-column"
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(timePicker.style.top).toBe("");
|
|
294
|
+
expect(timePicker.style.bottom).toBe("30px");
|
|
295
|
+
|
|
296
|
+
document.body.removeChild(stickyContainer);
|
|
297
|
+
});
|
|
234
298
|
});
|
|
@@ -49,34 +49,42 @@ describe("setMinute", () => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
describe("formatInputTime", () => {
|
|
52
|
-
const setupDOM = () => {
|
|
53
|
-
document.body.innerHTML = `
|
|
54
|
-
<input type="radio" id="period_am_exampleInput" name="period">
|
|
55
|
-
<input type="radio" id="period_pm_exampleInput" name="period">
|
|
56
|
-
`;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
52
|
describe("24-hour format", () => {
|
|
53
|
+
const format = 24;
|
|
54
|
+
const input = { id: "exampleInput" };
|
|
55
|
+
|
|
60
56
|
it("returns the value in correct format", () => {
|
|
61
|
-
|
|
62
|
-
const time = "15:30";
|
|
63
|
-
const format = 24;
|
|
64
|
-
const input = { id: "exampleInput" };
|
|
57
|
+
const result = formatInputTime("15:30", format, input);
|
|
65
58
|
|
|
66
|
-
|
|
59
|
+
expect(result).toBe("15:30");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns the value in correct format if seconds are provided", () => {
|
|
63
|
+
const result = formatInputTime("15:30:04", format, input);
|
|
67
64
|
|
|
68
65
|
expect(result).toBe("15:30");
|
|
69
66
|
});
|
|
67
|
+
|
|
68
|
+
it("returns an empty string if an empty string is provided", () => {
|
|
69
|
+
const result = formatInputTime("", 24, { id: "exampleInput" });
|
|
70
|
+
|
|
71
|
+
expect(result).toBe("");
|
|
72
|
+
});
|
|
70
73
|
})
|
|
71
74
|
|
|
72
75
|
describe("12-hour format", () => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
document.body.innerHTML = `
|
|
78
|
+
<input type="radio" id="period_am_exampleInput" name="period">
|
|
79
|
+
<input type="radio" id="period_pm_exampleInput" name="period">
|
|
80
|
+
`;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const format = 12;
|
|
84
|
+
const input = { id: "exampleInput" };
|
|
78
85
|
|
|
79
|
-
|
|
86
|
+
it("returns the value in correct format and checks the PM radio-button", () => {
|
|
87
|
+
const result = formatInputTime("12:45", format, input);
|
|
80
88
|
|
|
81
89
|
expect(result).toBe("12:45");
|
|
82
90
|
expect(document.getElementById("period_pm_exampleInput").checked).toBeTruthy();
|
|
@@ -84,17 +92,24 @@ describe("formatInputTime", () => {
|
|
|
84
92
|
});
|
|
85
93
|
|
|
86
94
|
it("returns the value in correct format", () => {
|
|
87
|
-
|
|
88
|
-
const time = "00:25";
|
|
89
|
-
const format = 12;
|
|
90
|
-
const input = { id: "exampleInput" };
|
|
91
|
-
|
|
92
|
-
const result = formatInputTime(time, format, input);
|
|
95
|
+
const result = formatInputTime("00:25", format, input);
|
|
93
96
|
|
|
94
97
|
expect(result).toBe("12:25");
|
|
95
98
|
// We are not expecting a radio button to be selected here since in a real environment the selected button is not
|
|
96
99
|
// handled by this method for values < 12
|
|
97
100
|
});
|
|
101
|
+
|
|
102
|
+
it("returns the value in correct format if seconds are provided", () => {
|
|
103
|
+
const result = formatInputTime("00:25:04", format, input);
|
|
104
|
+
|
|
105
|
+
expect(result).toBe("12:25");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns an empty string if an empty string is provided", () => {
|
|
109
|
+
const result = formatInputTime("", 24, { id: "exampleInput" });
|
|
110
|
+
|
|
111
|
+
expect(result).toBe("");
|
|
112
|
+
});
|
|
98
113
|
})
|
|
99
114
|
});
|
|
100
115
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* global jest */
|
|
2
|
-
|
|
3
1
|
export const createSuggestionRenderer = (node, { itemConverter } = {}) => () => {
|
|
4
2
|
let suggestion = null;
|
|
5
3
|
let suggestionItems = null;
|
|
@@ -55,25 +53,25 @@ export const createSuggestionRenderer = (node, { itemConverter } = {}) => () =>
|
|
|
55
53
|
return;
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
const
|
|
59
|
-
if (currentRange
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return true;
|
|
74
|
-
}).setTextSelection({ from: currentRange.from, to: currentRange.from }).run();
|
|
56
|
+
const item = convertItem(items[idx]);
|
|
57
|
+
if (currentRange) {
|
|
58
|
+
const docSize = currentEditor.state.doc.content.size;
|
|
59
|
+
const isValidRange = currentRange.from >= 0 && currentRange.to <= docSize && currentRange.from <= currentRange.to;
|
|
60
|
+
|
|
61
|
+
if (isValidRange) {
|
|
62
|
+
const nodeType = currentEditor.schema.nodes[items[idx].__typename === "User"
|
|
63
|
+
? "mention"
|
|
64
|
+
: "mentionResource"];
|
|
65
|
+
currentEditor.chain().focus().setTextSelection(currentRange).insertContent({
|
|
66
|
+
type: nodeType.name,
|
|
67
|
+
attrs: item
|
|
68
|
+
}).insertContent(" ").run();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
75
71
|
}
|
|
76
|
-
|
|
72
|
+
|
|
73
|
+
const command = selectCommand;
|
|
74
|
+
command(item);
|
|
77
75
|
};
|
|
78
76
|
|
|
79
77
|
const showSuggestions = ({ items, clientRect }) => {
|
|
@@ -87,7 +85,7 @@ export const createSuggestionRenderer = (node, { itemConverter } = {}) => () =>
|
|
|
87
85
|
suggestion.classList.remove("hidden", "hide");
|
|
88
86
|
suggestion.innerHTML = "";
|
|
89
87
|
items.forEach((rawItem, idx) => {
|
|
90
|
-
const { label, id, help } = convertItem(rawItem);
|
|
88
|
+
const { label, id, help, avatarUrl, displayLabel } = convertItem(rawItem);
|
|
91
89
|
const suggestionItem = document.createElement("button");
|
|
92
90
|
suggestionItem.type = "button";
|
|
93
91
|
suggestionItem.classList.add("editor-suggestions-item");
|
|
@@ -103,7 +101,19 @@ export const createSuggestionRenderer = (node, { itemConverter } = {}) => () =>
|
|
|
103
101
|
if (help) {
|
|
104
102
|
suggestionItem.disabled = true;
|
|
105
103
|
}
|
|
106
|
-
|
|
104
|
+
|
|
105
|
+
if (avatarUrl) {
|
|
106
|
+
const avatar = document.createElement("img");
|
|
107
|
+
avatar.classList.add("editor-suggestions-item-avatar");
|
|
108
|
+
avatar.src = avatarUrl;
|
|
109
|
+
avatar.alt = label;
|
|
110
|
+
suggestionItem.append(avatar);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const labelElement = document.createElement("span");
|
|
114
|
+
labelElement.classList.add("editor-suggestions-item-label");
|
|
115
|
+
labelElement.textContent = displayLabel || label;
|
|
116
|
+
suggestionItem.append(labelElement);
|
|
107
117
|
suggestion.append(suggestionItem);
|
|
108
118
|
|
|
109
119
|
suggestionItem.addEventListener("click", (ev) => {
|
|
@@ -29,9 +29,11 @@ const searchUsers = async (queryText) => {
|
|
|
29
29
|
export default Mention.extend({
|
|
30
30
|
addOptions() {
|
|
31
31
|
const parentOptions = this.parent?.();
|
|
32
|
+
const searchPromptMessage = parentOptions?.searchPromptMessage || "Type to search participants";
|
|
32
33
|
|
|
33
34
|
return {
|
|
34
35
|
...parentOptions,
|
|
36
|
+
searchPromptMessage,
|
|
35
37
|
renderText({ node }) {
|
|
36
38
|
// renderText is used to create the DOM representation
|
|
37
39
|
const label = node.attrs.label ?? node.attrs.id;
|
|
@@ -42,16 +44,25 @@ export default Mention.extend({
|
|
|
42
44
|
allowSpaces: true,
|
|
43
45
|
items: async ({ query }) => {
|
|
44
46
|
if (query.length < 2) {
|
|
45
|
-
return [];
|
|
47
|
+
return [{ label: searchPromptMessage, help: true }];
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
const data = await searchUsers(query);
|
|
49
|
-
const sorted = data.sort((
|
|
51
|
+
const sorted = data.sort((first, second) => first.nickname.localeCompare(second.nickname));
|
|
50
52
|
return sorted.slice(0, 5);
|
|
51
53
|
},
|
|
52
54
|
render: createSuggestionRenderer(this, {
|
|
53
55
|
itemConverter: (user) => {
|
|
54
|
-
|
|
56
|
+
if (user.help) {
|
|
57
|
+
return { label: user.label, help: true }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
id: user.nickname,
|
|
62
|
+
label: user.nickname,
|
|
63
|
+
displayLabel: `${user.nickname} (${user.name})`,
|
|
64
|
+
avatarUrl: user.avatarUrl
|
|
65
|
+
}
|
|
55
66
|
}
|
|
56
67
|
})
|
|
57
68
|
}
|