decidim-core 0.31.6 → 0.31.8
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/card_metadata_cell.rb +1 -1
- data/app/cells/decidim/comments_button_cell.rb +9 -1
- data/app/cells/decidim/content_blocks/participatory_space_main_data/title.erb +1 -14
- data/app/cells/decidim/data_consent/category.erb +20 -33
- data/app/cells/decidim/following_cell.rb +1 -1
- data/app/cells/decidim/footer_topics/show.erb +5 -8
- data/app/cells/decidim/nav_links/show.erb +3 -1
- data/app/cells/decidim/participatory_space_dropdown_metadata/links.erb +1 -1
- data/app/cells/decidim/participatory_space_private_user/show.erb +1 -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 +62 -11
- 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 +10 -0
- 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 +2 -0
- data/app/events/decidim/amendable/amendment_base_event.rb +7 -1
- 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/menu_helper.rb +0 -2
- 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 +3 -2
- 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 +260 -42
- data/app/packs/src/decidim/controllers/accordion/controller.js +136 -17
- data/app/packs/src/decidim/controllers/dropdown/controller.js +6 -0
- data/app/packs/src/decidim/controllers/dropdown/dropdown.test.js +28 -0
- data/app/packs/src/decidim/controllers/password_toggler/controller.js +22 -1
- data/app/packs/src/decidim/controllers/password_toggler/password_toggler.test.js +11 -20
- data/app/packs/src/decidim/datepicker/datepicker_functions.js +34 -23
- data/app/packs/src/decidim/datepicker/generate_datepicker.js +4 -4
- data/app/packs/src/decidim/datepicker/test/time.test.js +39 -24
- data/app/packs/src/decidim/direct_uploads/upload_modal.js +2 -1
- data/app/packs/src/decidim/map/controller.js +10 -1
- data/app/packs/src/decidim/refactor/moved/focus_guard.js +12 -12
- data/app/packs/src/decidim/refactor/moved/focus_guard.test.js +136 -0
- data/app/packs/src/decidim/utilities/dom.js +2 -2
- data/app/packs/stylesheets/decidim/_cards.scss +1 -1
- data/app/packs/stylesheets/decidim/_content_blocks.scss +4 -10
- data/app/packs/stylesheets/decidim/_conversations.scss +0 -8
- data/app/packs/stylesheets/decidim/_cookies.scss +13 -5
- data/app/packs/stylesheets/decidim/_documents.scss +1 -1
- data/app/packs/stylesheets/decidim/_footer.scss +33 -1
- 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 +12 -2
- data/app/packs/stylesheets/decidim/_rich_text.scss +12 -9
- data/app/packs/stylesheets/decidim/_typography.scss +5 -2
- 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/footer_menu_presenter.rb +2 -4
- data/app/presenters/decidim/log/base_presenter.rb +10 -1
- data/app/presenters/decidim/log/user_presenter.rb +2 -2
- data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -0
- 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 +278 -0
- data/app/views/decidim/application/_radio_accordion.html.erb +1 -5
- data/app/views/decidim/messaging/conversations/create.js.erb +1 -3
- data/app/views/decidim/searches/_filters.html.erb +1 -1
- 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/footer/_main.html.erb +1 -1
- data/app/views/layouts/decidim/footer/_main_links.html.erb +56 -28
- data/config/initializers/rack_attack.rb +10 -0
- data/config/locales/ar.yml +0 -20
- data/config/locales/bg.yml +0 -20
- data/config/locales/ca-IT.yml +31 -24
- data/config/locales/ca.yml +31 -24
- data/config/locales/cs.yml +20 -22
- data/config/locales/de.yml +59 -26
- data/config/locales/el.yml +0 -18
- data/config/locales/en.yml +31 -24
- data/config/locales/es-MX.yml +31 -24
- data/config/locales/es-PY.yml +31 -24
- data/config/locales/es.yml +31 -24
- data/config/locales/eu.yml +178 -176
- data/config/locales/fi-plain.yml +29 -24
- data/config/locales/fi.yml +29 -24
- data/config/locales/fr-CA.yml +10 -20
- data/config/locales/fr.yml +10 -20
- data/config/locales/gl.yml +1440 -51
- data/config/locales/hu.yml +0 -18
- data/config/locales/id-ID.yml +0 -18
- data/config/locales/is-IS.yml +0 -6
- data/config/locales/it.yml +0 -17
- data/config/locales/ja.yml +29 -24
- data/config/locales/lb.yml +0 -18
- data/config/locales/lt.yml +0 -19
- data/config/locales/lv.yml +0 -18
- data/config/locales/nl.yml +0 -18
- data/config/locales/no.yml +0 -18
- data/config/locales/pl.yml +2 -20
- data/config/locales/pt-BR.yml +0 -26
- data/config/locales/pt.yml +0 -18
- data/config/locales/ro-RO.yml +0 -19
- data/config/locales/ru.yml +0 -8
- data/config/locales/sk.yml +2 -26
- data/config/locales/sv.yml +45 -29
- data/config/locales/tr-TR.yml +0 -20
- data/config/locales/uk.yml +0 -6
- data/config/locales/zh-CN.yml +0 -16
- data/config/locales/zh-TW.yml +0 -19
- data/db/migrate/20250217192438_convert_user_groups_into_users.rb +3 -2
- data/db/migrate/20260909000000_add_failed_attempts_and_locked_at_to_decidim_authorizations.rb +8 -0
- data/decidim-core.gemspec +3 -0
- data/lib/decidim/core/test/shared_examples/amendable/amendment_event_examples.rb +2 -2
- data/lib/decidim/core/test/shared_examples/amendable/amendment_promoted_event_examples.rb +3 -3
- data/lib/decidim/core/test/shared_examples/comments_examples.rb +26 -0
- data/lib/decidim/core/test/shared_examples/has_space_in_mcell_examples.rb +2 -1
- 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 +85 -6
- data/lib/decidim/core/test/shared_examples/resource_liked_event_examples.rb +3 -3
- 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 +10 -1
- data/lib/decidim/has_upload_validations.rb +1 -0
- data/lib/decidim/legacy_form_builder.rb +4 -0
- data/lib/decidim/map/dynamic_map.rb +3 -1
- data/lib/decidim/query_extensions.rb +2 -0
- metadata +31 -6
|
@@ -22,17 +22,7 @@ describe("PasswordToggler", () => {
|
|
|
22
22
|
container = document.createElement("div");
|
|
23
23
|
container.innerHTML = `
|
|
24
24
|
<div data-controller="password-toggler" class="flex flex-row items-center gap-x-2" data-show-password="Show secret" data-hide-password="Hide Secret" data-hidden-password="Secret is hidden" data-shown-password="Secret is shown">
|
|
25
|
-
<
|
|
26
|
-
<div class="input-group__password">
|
|
27
|
-
<button type="button" aria-controls="token_162" aria-label="Show password">
|
|
28
|
-
<svg width="0.75em" height="0.75em" role="img" aria-hidden="true">
|
|
29
|
-
<title>eye-line</title>
|
|
30
|
-
<use href="/decidim-packs/media/images/remixicon.symbol-e643e553623ffcd49f94.svg#ri-eye-line"></use>
|
|
31
|
-
</svg>
|
|
32
|
-
</button>
|
|
33
|
-
<input type="password" id="token_162" value="AXXXXXXXX" class="w-full" autocomplete="off">
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
25
|
+
<input type="password" id="token_162" value="AXXXXXXXX" class="w-full" autocomplete="off">
|
|
36
26
|
<div class="basis-1/4">
|
|
37
27
|
<button type="button" class="button button__sm button__text-primary" data-clipboard-copy="#token_162">
|
|
38
28
|
<span>Copy secret</span>
|
|
@@ -204,22 +194,23 @@ describe("PasswordToggler", () => {
|
|
|
204
194
|
});
|
|
205
195
|
});
|
|
206
196
|
|
|
207
|
-
describe("
|
|
208
|
-
it("
|
|
209
|
-
|
|
210
|
-
|
|
197
|
+
describe("idempotency", () => {
|
|
198
|
+
it("does not duplicate controls on reconnect", () => {
|
|
199
|
+
controller.disconnect();
|
|
200
|
+
controller.connect();
|
|
211
201
|
|
|
212
|
-
|
|
202
|
+
const wrappers = passwordElement.querySelectorAll(".input-group__password");
|
|
203
|
+
const buttons = passwordElement.querySelectorAll('button[aria-controls="token_162"]');
|
|
213
204
|
|
|
214
|
-
|
|
215
|
-
expect(
|
|
216
|
-
expect(controller.input.getAttribute("type")).toBe("password");
|
|
205
|
+
expect(wrappers.length).toBe(1);
|
|
206
|
+
expect(buttons.length).toBe(1);
|
|
217
207
|
});
|
|
208
|
+
});
|
|
218
209
|
|
|
210
|
+
describe("integration with existing markup", () => {
|
|
219
211
|
it("handles password input with existing value", () => {
|
|
220
212
|
expect(controller.input.value).toBe("AXXXXXXXX");
|
|
221
213
|
|
|
222
|
-
controller.init();
|
|
223
214
|
controller.showPassword();
|
|
224
215
|
|
|
225
216
|
expect(controller.input.getAttribute("type")).toBe("text");
|
|
@@ -50,10 +50,7 @@ export const setMinute = (value) => {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
export const formatInputDate = (date, formats) => {
|
|
53
|
-
const
|
|
54
|
-
const year = dateList[0];
|
|
55
|
-
const month = dateList[1];
|
|
56
|
-
const day = dateList[2];
|
|
53
|
+
const [year, month, day] = date.split("-");
|
|
57
54
|
|
|
58
55
|
if (formats.order === "m-d-y") {
|
|
59
56
|
return `${month}${formats.separator}${day}${formats.separator}${year}`;
|
|
@@ -63,10 +60,34 @@ export const formatInputDate = (date, formats) => {
|
|
|
63
60
|
return `${day}${formats.separator}${month}${formats.separator}${year}`;
|
|
64
61
|
};
|
|
65
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Splits a time string into two elements: hours and minutes. Ensures a returned
|
|
65
|
+
* array of size 2 for any kind of strings or `null` values.
|
|
66
|
+
*
|
|
67
|
+
* Examples:
|
|
68
|
+
* splitTime("") => ["00", "00"]
|
|
69
|
+
* splitTime(null) => ["00", "00"]
|
|
70
|
+
* splitTime("12") => ["12", "00"]
|
|
71
|
+
* splitTime("12:30") => ["12", "30"]
|
|
72
|
+
* splitTime("2:4") => ["02", "04"]
|
|
73
|
+
* splitTime("12:30:45") => ["12", "30"]
|
|
74
|
+
*
|
|
75
|
+
* @param {String} value The original time string, typically hh:ss.
|
|
76
|
+
* @returns {Array} An array with two elements where the first element is the
|
|
77
|
+
* hour part and the second element is the minute part.
|
|
78
|
+
*/
|
|
79
|
+
const splitTime = (value) => {
|
|
80
|
+
return [...(value || "").split(":"), "00"].splice(0, 2).map((part) => part.padStart(2, "0"));
|
|
81
|
+
};
|
|
82
|
+
|
|
66
83
|
export const formatInputTime = (time, format, input) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
if (!time) {
|
|
85
|
+
return "";
|
|
86
|
+
} else if (time.length < 1) {
|
|
87
|
+
return time;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let [hour, minute] = splitTime(time);
|
|
70
91
|
|
|
71
92
|
if (format === 12) {
|
|
72
93
|
if (Number(hour) === 12) {
|
|
@@ -80,11 +101,9 @@ export const formatInputTime = (time, format, input) => {
|
|
|
80
101
|
} else if (Number(hour) === 0) {
|
|
81
102
|
hour = "12";
|
|
82
103
|
}
|
|
83
|
-
|
|
84
|
-
return `${hour}:${minute}`;
|
|
85
104
|
};
|
|
86
105
|
|
|
87
|
-
return
|
|
106
|
+
return `${hour}:${minute}`;
|
|
88
107
|
};
|
|
89
108
|
|
|
90
109
|
export const changeHourDisplay = (change, hour, format) => {
|
|
@@ -199,29 +218,21 @@ export const formatDate = (value, formats) => {
|
|
|
199
218
|
};
|
|
200
219
|
|
|
201
220
|
export const formatTime = (value, format, inputname) => {
|
|
221
|
+
let [hour, minute] = splitTime(value);
|
|
222
|
+
|
|
202
223
|
if (format === 12) {
|
|
203
|
-
const splitValue = value.split(":");
|
|
204
|
-
let hour = splitValue[0];
|
|
205
|
-
const minute = splitValue[1];
|
|
206
224
|
if (document.getElementById(`period_am_${inputname}`).checked) {
|
|
207
|
-
|
|
208
|
-
case "12":
|
|
225
|
+
if (hour === "12") {
|
|
209
226
|
hour = "00";
|
|
210
|
-
|
|
211
|
-
return `${hour}:${minute}`;
|
|
212
|
-
default:
|
|
213
|
-
return value;
|
|
214
|
-
};
|
|
227
|
+
}
|
|
215
228
|
} else if (document.getElementById(`period_pm_${inputname}`).checked) {
|
|
216
229
|
if (Number(hour) > 0 && Number(hour) < 12) {
|
|
217
230
|
hour = `${Number(hour) + 12}`;
|
|
218
231
|
};
|
|
219
|
-
|
|
220
|
-
return `${hour}:${minute}`
|
|
221
232
|
};
|
|
222
233
|
};
|
|
223
234
|
|
|
224
|
-
return
|
|
235
|
+
return `${hour}:${minute}`;
|
|
225
236
|
};
|
|
226
237
|
|
|
227
238
|
export const updateInputValue = (input, formats, time) => {
|
|
@@ -58,7 +58,7 @@ export default function generateDatePicker(input, row, formats) {
|
|
|
58
58
|
dateColumn.appendChild(datePickerContainer);
|
|
59
59
|
|
|
60
60
|
let prevDate = null;
|
|
61
|
-
let defaultTime = input.
|
|
61
|
+
let defaultTime = input.dataset.defaultTime || "00:00"
|
|
62
62
|
|
|
63
63
|
const datePickerDisplay = (event) => {
|
|
64
64
|
if (!dateColumn.contains(event.target)) {
|
|
@@ -79,7 +79,7 @@ export default function generateDatePicker(input, row, formats) {
|
|
|
79
79
|
if (input.type === "date") {
|
|
80
80
|
input.value = `${formatDate(date.value, formats)}`;
|
|
81
81
|
} else if (input.type === "datetime-local") {
|
|
82
|
-
input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value, formats.time, input.id)
|
|
82
|
+
input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value || defaultTime, formats.time, input.id)}`;
|
|
83
83
|
};
|
|
84
84
|
};
|
|
85
85
|
});
|
|
@@ -95,7 +95,7 @@ export default function generateDatePicker(input, row, formats) {
|
|
|
95
95
|
if (input.type === "date") {
|
|
96
96
|
input.value = `${formatDate(date.value, formats)}`;
|
|
97
97
|
} else if (input.type === "datetime-local") {
|
|
98
|
-
input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value, formats.time, input.id)
|
|
98
|
+
input.value = `${formatDate(date.value, formats)}T${formatTime(document.querySelector(`#${input.id}_time`).value || defaultTime, formats.time, input.id)}`;
|
|
99
99
|
};
|
|
100
100
|
};
|
|
101
101
|
});
|
|
@@ -115,7 +115,7 @@ export default function generateDatePicker(input, row, formats) {
|
|
|
115
115
|
if (input.type === "date") {
|
|
116
116
|
input.value = `${pickedDate}`;
|
|
117
117
|
} else if (input.type === "datetime-local") {
|
|
118
|
-
input.value = `${pickedDate}T${formatTime(document.querySelector(`#${input.id}_time`).value, formats.time, input.id)
|
|
118
|
+
input.value = `${pickedDate}T${formatTime(document.querySelector(`#${input.id}_time`).value || defaultTime, formats.time, input.id)}`;
|
|
119
119
|
};
|
|
120
120
|
datePickerContainer.style.display = "none";
|
|
121
121
|
});
|
|
@@ -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
|
|
|
@@ -253,7 +253,8 @@ export default class UploadModal {
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
setProgressBar(name, value) {
|
|
256
|
-
const
|
|
256
|
+
const items = Array.from(this.uploadItems.querySelectorAll("[data-filename]"));
|
|
257
|
+
const item = items.filter((el) => el.dataset.filename === name).pop();
|
|
257
258
|
if (item) {
|
|
258
259
|
item.querySelector("progress").value = value;
|
|
259
260
|
}
|
|
@@ -29,7 +29,16 @@ export default class MapController {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
load() {
|
|
32
|
-
|
|
32
|
+
const zoomControl = L.control.zoom({
|
|
33
|
+
zoomInTitle: this.config.zoomInText || "Zoom in",
|
|
34
|
+
zoomOutTitle: this.config.zoomOutText || "Zoom out"
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.map = L.map(this.mapId, {
|
|
38
|
+
zoomControl: false
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.map.addControl(zoomControl);
|
|
33
42
|
|
|
34
43
|
this.map.scrollWheelZoom.disable();
|
|
35
44
|
|
|
@@ -5,14 +5,12 @@ const focusableDisableableNodes = ["BUTTON", "INPUT", "TEXTAREA", "SELECT"];
|
|
|
5
5
|
export default class FocusGuard {
|
|
6
6
|
constructor(container) {
|
|
7
7
|
this.container = container;
|
|
8
|
-
this.
|
|
9
|
-
this.triggerElement = null;
|
|
8
|
+
this.stack = [];
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
trap(element, trigger) {
|
|
13
12
|
this.enable();
|
|
14
|
-
this.
|
|
15
|
-
this.triggerElement = trigger;
|
|
13
|
+
this.stack.push({ element, trigger });
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
enable() {
|
|
@@ -45,14 +43,15 @@ export default class FocusGuard {
|
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
disable() {
|
|
48
|
-
const
|
|
49
|
-
guards.forEach((guard) => guard.remove());
|
|
46
|
+
const current = this.stack.pop();
|
|
50
47
|
|
|
51
|
-
this.
|
|
48
|
+
if (this.stack.length === 0) {
|
|
49
|
+
const guards = this.container.querySelectorAll(`:scope > .${focusGuardClass}`);
|
|
50
|
+
guards.forEach((guard) => guard.remove());
|
|
51
|
+
}
|
|
52
52
|
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
this.triggerElement = null;
|
|
53
|
+
if (current && current.trigger) {
|
|
54
|
+
current.trigger.focus();
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -67,12 +66,13 @@ export default class FocusGuard {
|
|
|
67
66
|
};
|
|
68
67
|
|
|
69
68
|
handleContainerFocus(guard) {
|
|
70
|
-
|
|
69
|
+
const current = this.stack[this.stack.length - 1];
|
|
70
|
+
if (!current || !current.element) {
|
|
71
71
|
guard.blur();
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const visibleNodes = Array.from(
|
|
75
|
+
const visibleNodes = Array.from(current.element.querySelectorAll("*")).filter((item) => {
|
|
76
76
|
return this.isVisible(item);
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
|
|
3
|
+
import FocusGuard from "src/decidim/refactor/moved/focus_guard"
|
|
4
|
+
|
|
5
|
+
describe("FocusGuard", () => {
|
|
6
|
+
let focusGuard = null;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
document.body.innerHTML = "";
|
|
10
|
+
focusGuard = new FocusGuard(document.body);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
document.body.innerHTML = "";
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("single dialog", () => {
|
|
18
|
+
it("creates focus guards on trap", () => {
|
|
19
|
+
const dialog = document.createElement("div");
|
|
20
|
+
const trigger = document.createElement("button");
|
|
21
|
+
document.body.appendChild(dialog);
|
|
22
|
+
document.body.appendChild(trigger);
|
|
23
|
+
|
|
24
|
+
focusGuard.trap(dialog, trigger);
|
|
25
|
+
const guards = document.body.querySelectorAll(".focusguard");
|
|
26
|
+
expect(guards.length).toBe(2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("removes focus guards and returns focus on disable", () => {
|
|
30
|
+
const dialog = document.createElement("div");
|
|
31
|
+
const trigger = document.createElement("button");
|
|
32
|
+
document.body.appendChild(dialog);
|
|
33
|
+
document.body.appendChild(trigger);
|
|
34
|
+
|
|
35
|
+
focusGuard.trap(dialog, trigger);
|
|
36
|
+
focusGuard.disable();
|
|
37
|
+
|
|
38
|
+
const guards = document.body.querySelectorAll(".focusguard");
|
|
39
|
+
expect(guards.length).toBe(0);
|
|
40
|
+
expect(document.activeElement).toBe(trigger);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("nested dialogs", () => {
|
|
45
|
+
it("keeps guards when inner dialog is disabled", () => {
|
|
46
|
+
const outerDialog = document.createElement("div");
|
|
47
|
+
const outerTrigger = document.createElement("button");
|
|
48
|
+
const innerDialog = document.createElement("div");
|
|
49
|
+
const innerTrigger = document.createElement("button");
|
|
50
|
+
|
|
51
|
+
document.body.appendChild(outerDialog);
|
|
52
|
+
document.body.appendChild(outerTrigger);
|
|
53
|
+
document.body.appendChild(innerDialog);
|
|
54
|
+
document.body.appendChild(innerTrigger);
|
|
55
|
+
|
|
56
|
+
focusGuard.trap(outerDialog, outerTrigger);
|
|
57
|
+
focusGuard.trap(innerDialog, innerTrigger);
|
|
58
|
+
|
|
59
|
+
expect(document.body.querySelectorAll(".focusguard").length).toBe(2);
|
|
60
|
+
|
|
61
|
+
focusGuard.disable();
|
|
62
|
+
|
|
63
|
+
expect(document.body.querySelectorAll(".focusguard").length).toBe(2);
|
|
64
|
+
expect(document.activeElement).toBe(innerTrigger);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("removes guards when outer dialog is disabled last", () => {
|
|
68
|
+
const outerDialog = document.createElement("div");
|
|
69
|
+
const outerTrigger = document.createElement("button");
|
|
70
|
+
const innerDialog = document.createElement("div");
|
|
71
|
+
const innerTrigger = document.createElement("button");
|
|
72
|
+
|
|
73
|
+
document.body.appendChild(outerDialog);
|
|
74
|
+
document.body.appendChild(outerTrigger);
|
|
75
|
+
document.body.appendChild(innerDialog);
|
|
76
|
+
document.body.appendChild(innerTrigger);
|
|
77
|
+
|
|
78
|
+
focusGuard.trap(outerDialog, outerTrigger);
|
|
79
|
+
focusGuard.trap(innerDialog, innerTrigger);
|
|
80
|
+
|
|
81
|
+
focusGuard.disable();
|
|
82
|
+
focusGuard.disable();
|
|
83
|
+
|
|
84
|
+
expect(document.body.querySelectorAll(".focusguard").length).toBe(0);
|
|
85
|
+
expect(document.activeElement).toBe(outerTrigger);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("cycles focus to the current top dialog when a guard receives focus", () => {
|
|
89
|
+
jest.spyOn(focusGuard, "isVisible").mockReturnValue(true);
|
|
90
|
+
|
|
91
|
+
const outerDialog = document.createElement("div");
|
|
92
|
+
const outerInput = document.createElement("input");
|
|
93
|
+
outerDialog.appendChild(outerInput);
|
|
94
|
+
|
|
95
|
+
const innerDialog = document.createElement("div");
|
|
96
|
+
const innerInput = document.createElement("input");
|
|
97
|
+
innerDialog.appendChild(innerInput);
|
|
98
|
+
|
|
99
|
+
document.body.appendChild(outerDialog);
|
|
100
|
+
document.body.appendChild(innerDialog);
|
|
101
|
+
|
|
102
|
+
focusGuard.trap(outerDialog, null);
|
|
103
|
+
focusGuard.trap(innerDialog, null);
|
|
104
|
+
|
|
105
|
+
const endGuard = document.body.querySelector('.focusguard[data-position="end"]');
|
|
106
|
+
endGuard.focus();
|
|
107
|
+
|
|
108
|
+
expect(document.activeElement).toBe(innerInput);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("cycles focus backward to the current top dialog when the start guard receives focus", () => {
|
|
112
|
+
jest.spyOn(focusGuard, "isVisible").mockReturnValue(true);
|
|
113
|
+
|
|
114
|
+
const outerDialog = document.createElement("div");
|
|
115
|
+
const outerInput = document.createElement("input");
|
|
116
|
+
outerDialog.appendChild(outerInput);
|
|
117
|
+
|
|
118
|
+
const innerDialog = document.createElement("div");
|
|
119
|
+
const innerInput = document.createElement("input");
|
|
120
|
+
const innerButton = document.createElement("button");
|
|
121
|
+
innerDialog.appendChild(innerInput);
|
|
122
|
+
innerDialog.appendChild(innerButton);
|
|
123
|
+
|
|
124
|
+
document.body.appendChild(outerDialog);
|
|
125
|
+
document.body.appendChild(innerDialog);
|
|
126
|
+
|
|
127
|
+
focusGuard.trap(outerDialog, null);
|
|
128
|
+
focusGuard.trap(innerDialog, null);
|
|
129
|
+
|
|
130
|
+
const startGuard = document.body.querySelector('.focusguard[data-position="start"]');
|
|
131
|
+
startGuard.focus();
|
|
132
|
+
|
|
133
|
+
expect(document.activeElement).toBe(innerButton);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -77,7 +77,7 @@ const createUnloadPreventer = () => {
|
|
|
77
77
|
ev.preventDefault();
|
|
78
78
|
ev.stopPropagation();
|
|
79
79
|
|
|
80
|
-
confirmAction(confirmMessage, link).then((answer) => {
|
|
80
|
+
confirmAction(confirmMessage, link, { iconName: "alert-line" }).then((answer) => {
|
|
81
81
|
if (!answer) {
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
@@ -109,7 +109,7 @@ const createUnloadPreventer = () => {
|
|
|
109
109
|
ev.stopImmediatePropagation();
|
|
110
110
|
ev.stopPropagation();
|
|
111
111
|
|
|
112
|
-
confirmAction(confirmMessage, button).then((answer) => {
|
|
112
|
+
confirmAction(confirmMessage, button, { iconName: "alert-line" }).then((answer) => {
|
|
113
113
|
if (!answer) {
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
@@ -13,18 +13,12 @@
|
|
|
13
13
|
@apply block max-h-40 overflow-hidden relative first:[&_*]:mt-0 before:content-[''] before:absolute before:inset-0 before:h-full before:w-full before:bg-gradient-to-b before:from-transparent before:to-white after:content-[''] after:absolute after:inset-0 after:h-full after:w-full after:bg-gradient-to-b after:from-transparent after:to-white;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
[aria-expanded
|
|
17
|
-
|
|
18
|
-
[aria-expanded="true"] > span:first-of-type,
|
|
19
|
-
[aria-expanded="true"] > svg:first-of-type {
|
|
20
|
-
@apply hidden;
|
|
16
|
+
[aria-expanded] > svg {
|
|
17
|
+
@apply transition-transform;
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
[aria-expanded="true"] > svg
|
|
24
|
-
|
|
25
|
-
[aria-expanded="false"] > span:first-of-type,
|
|
26
|
-
[aria-expanded="false"] > svg:first-of-type {
|
|
27
|
-
@apply block;
|
|
20
|
+
[aria-expanded="true"] > svg {
|
|
21
|
+
@apply rotate-180;
|
|
28
22
|
}
|
|
29
23
|
}
|
|
30
24
|
|
|
@@ -92,14 +92,6 @@
|
|
|
92
92
|
[data-dialog-closable] {
|
|
93
93
|
@apply top-6 right-6;
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
[data-dialog-container] {
|
|
97
|
-
@apply inline grid-cols-none;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
[data-dialog-title] {
|
|
101
|
-
@apply col-span-2 md:col-span-1 md:col-start-2 grid grid-cols-[auto_1fr] items-start md:items-center gap-2 pb-4 text-left text-xl text-gray-2 md:border-b md:border-gray-3;
|
|
102
|
-
}
|
|
103
95
|
}
|
|
104
96
|
|
|
105
97
|
&-results {
|
|
@@ -65,17 +65,25 @@
|
|
|
65
65
|
&-panel {
|
|
66
66
|
@apply divide-y divide-gray-3 [&>*]:py-4 [&>p]:text-gray-2;
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
@apply
|
|
68
|
+
table {
|
|
69
|
+
@apply w-full border-collapse;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
@apply
|
|
72
|
+
thead tr {
|
|
73
|
+
@apply bg-background-4;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
th {
|
|
77
|
+
@apply w-1/4 p-2 text-gray-2 font-bold text-sm text-left;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
td {
|
|
77
81
|
@apply w-1/4 p-2 text-gray-2 text-xs;
|
|
78
82
|
}
|
|
83
|
+
|
|
84
|
+
tbody tr {
|
|
85
|
+
@apply border-b border-gray-3;
|
|
86
|
+
}
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
&-toggle {
|
|
@@ -3,12 +3,44 @@ footer {
|
|
|
3
3
|
@apply bg-gray-4;
|
|
4
4
|
|
|
5
5
|
&__top {
|
|
6
|
-
@apply
|
|
6
|
+
@apply flex flex-col lg:flex-row gap-0 lg:gap-8 container py-10;
|
|
7
7
|
|
|
8
8
|
.prose a,
|
|
9
9
|
.prose strong {
|
|
10
10
|
@apply text-white;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
.footer__nav {
|
|
14
|
+
@apply border-b border-white/20 lg:border-0;
|
|
15
|
+
|
|
16
|
+
ul {
|
|
17
|
+
@apply space-y-4 break-inside-avoid mb-4;
|
|
18
|
+
|
|
19
|
+
li a {
|
|
20
|
+
@apply font-semibold underline decoration-1 underline-offset-1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
> [data-controls] {
|
|
25
|
+
@apply flex items-center justify-between w-full py-4 lg:pointer-events-none lg:cursor-default text-left text-white font-semibold;
|
|
26
|
+
|
|
27
|
+
svg {
|
|
28
|
+
@apply w-4 h-4 fill-current transition-transform lg:hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&[aria-expanded="true"] svg {
|
|
32
|
+
@apply rotate-180;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[id*="panel"][aria-hidden="true"] {
|
|
37
|
+
@apply hidden lg:block;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[id*="panel"][aria-hidden="false"] {
|
|
41
|
+
@apply block;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
12
44
|
}
|
|
13
45
|
|
|
14
46
|
&__down {
|