decidim-core 0.31.5 → 0.31.7
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/card_metadata_cell.rb +1 -1
- data/app/cells/decidim/comments_button_cell.rb +9 -1
- 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/participatory_space_hero_settings_form/show.erb +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_settings_form/show.erb +3 -1
- 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 +20 -33
- data/app/cells/decidim/footer_topics/show.erb +5 -8
- data/app/cells/decidim/participatory_space_dropdown_metadata/links.erb +1 -1
- data/app/cells/decidim/resource_types_filter/show.erb +2 -2
- data/app/cells/decidim/share_widget/qr_code_modal.erb +1 -1
- 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/events/decidim/amendable/amendment_base_event.rb +7 -1
- data/app/helpers/decidim/menu_helper.rb +0 -2
- 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/moderation.rb +1 -1
- data/app/models/decidim/participatory_space_private_user.rb +1 -1
- data/app/models/decidim/user.rb +2 -2
- 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/accordion/accordion.test.js +196 -42
- data/app/packs/src/decidim/controllers/accordion/controller.js +93 -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/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/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/direct_uploads/upload_field.js +4 -4
- data/app/packs/src/decidim/direct_uploads/upload_modal.js +12 -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/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/sw/sw.js +1 -1
- data/app/packs/src/decidim/utilities/text.js +6 -6
- data/app/packs/stylesheets/decidim/_cards.scss +1 -1
- data/app/packs/stylesheets/decidim/_conversations.scss +14 -0
- data/app/packs/stylesheets/decidim/_cookies.scss +13 -5
- data/app/packs/stylesheets/decidim/_documents.scss +1 -1
- 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/_footer.scss +33 -1
- data/app/packs/stylesheets/decidim/_participatory_spaces.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/footer_menu_presenter.rb +2 -4
- data/app/presenters/decidim/menu_item_presenter.rb +9 -3
- data/app/presenters/decidim/stats_presenter.rb +1 -1
- data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -0
- 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/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/searches/_filters.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/footer/_main.html.erb +1 -1
- data/app/views/layouts/decidim/footer/_main_links.html.erb +56 -28
- 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 -25
- data/config/locales/bg.yml +0 -25
- data/config/locales/ca-IT.yml +37 -33
- data/config/locales/ca.yml +37 -33
- data/config/locales/cs.yml +12 -31
- data/config/locales/de.yml +5 -42
- data/config/locales/el.yml +0 -22
- data/config/locales/en.yml +36 -32
- data/config/locales/es-MX.yml +36 -32
- data/config/locales/es-PY.yml +36 -32
- data/config/locales/es.yml +36 -32
- data/config/locales/eu.yml +72 -70
- data/config/locales/fi-plain.yml +37 -32
- data/config/locales/fi.yml +37 -32
- data/config/locales/fr-CA.yml +16 -28
- data/config/locales/fr.yml +16 -28
- data/config/locales/gl.yml +0 -22
- data/config/locales/hu.yml +0 -22
- data/config/locales/id-ID.yml +0 -21
- data/config/locales/is-IS.yml +0 -6
- data/config/locales/it.yml +1 -20
- data/config/locales/ja.yml +52 -44
- data/config/locales/lb.yml +0 -23
- data/config/locales/lt.yml +0 -24
- data/config/locales/lv.yml +0 -21
- data/config/locales/nl.yml +0 -23
- data/config/locales/no.yml +0 -23
- data/config/locales/pl.yml +4 -27
- data/config/locales/pt-BR.yml +2 -40
- data/config/locales/pt.yml +0 -23
- data/config/locales/ro-RO.yml +1 -32
- data/config/locales/ru.yml +0 -8
- data/config/locales/sk.yml +1 -39
- data/config/locales/sv.yml +6 -36
- data/config/locales/tr-TR.yml +0 -24
- data/config/locales/uk.yml +0 -6
- data/config/locales/zh-CN.yml +0 -21
- data/config/locales/zh-TW.yml +0 -24
- data/db/migrate/20250217192438_convert_user_groups_into_users.rb +3 -2
- data/decidim-core.gemspec +3 -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/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 +32 -6
- data/lib/decidim/core/test/shared_examples/has_space_in_mcell_examples.rb +2 -1
- data/lib/decidim/core/test/shared_examples/resource_liked_event_examples.rb +3 -3
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/core.rb +1 -1
- data/lib/decidim/map/autocomplete.rb +4 -3
- data/lib/decidim/map/dynamic_map.rb +3 -1
- 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 +32 -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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/* global global, jest */
|
|
2
|
+
|
|
3
|
+
import { initializeReverseGeocoding } from "src/decidim/geocoding/reverse_geocoding";
|
|
4
|
+
|
|
5
|
+
describe("reverseGeocoding", () => {
|
|
6
|
+
let container = null;
|
|
7
|
+
let button = null;
|
|
8
|
+
let input = null;
|
|
9
|
+
let label = null;
|
|
10
|
+
let mockGeolocation = null;
|
|
11
|
+
let mockPost = null;
|
|
12
|
+
let originalGeolocation = null;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
document.body.innerHTML = "";
|
|
16
|
+
|
|
17
|
+
container = document.createElement("div");
|
|
18
|
+
container.innerHTML = `
|
|
19
|
+
<label>
|
|
20
|
+
<input id="test_address" type="text" />
|
|
21
|
+
<div class="geocoding__locate">
|
|
22
|
+
<button
|
|
23
|
+
class="geocoding__button"
|
|
24
|
+
type="button"
|
|
25
|
+
data-input="test_address"
|
|
26
|
+
data-error-no-location="Could not detect location."
|
|
27
|
+
data-error-unsupported="Device not supported."
|
|
28
|
+
data-locating-text="Locating..."
|
|
29
|
+
data-url="/locate">
|
|
30
|
+
Use my location
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</label>
|
|
34
|
+
`;
|
|
35
|
+
document.body.appendChild(container);
|
|
36
|
+
|
|
37
|
+
button = container.querySelector(".geocoding__button");
|
|
38
|
+
input = container.querySelector("#test_address");
|
|
39
|
+
label = container.querySelector("label");
|
|
40
|
+
|
|
41
|
+
originalGeolocation = navigator.geolocation;
|
|
42
|
+
mockGeolocation = {
|
|
43
|
+
getCurrentPosition: jest.fn()
|
|
44
|
+
};
|
|
45
|
+
Reflect.defineProperty(global.navigator, "geolocation", {
|
|
46
|
+
value: mockGeolocation,
|
|
47
|
+
configurable: true
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
mockPost = jest.fn();
|
|
51
|
+
global.jQuery = jest.fn(() => ({ trigger: jest.fn() }));
|
|
52
|
+
global.jQuery.post = mockPost;
|
|
53
|
+
// eslint-disable-next-line id-length
|
|
54
|
+
global.$ = global.jQuery;
|
|
55
|
+
|
|
56
|
+
initializeReverseGeocoding();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
Reflect.defineProperty(global.navigator, "geolocation", {
|
|
61
|
+
value: originalGeolocation,
|
|
62
|
+
configurable: true
|
|
63
|
+
});
|
|
64
|
+
jest.restoreAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("when clicking the button", () => {
|
|
68
|
+
it("shows the spinner and disables the button", () => {
|
|
69
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
70
|
+
|
|
71
|
+
button.click();
|
|
72
|
+
|
|
73
|
+
expect(button.disabled).toBe(true);
|
|
74
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(true);
|
|
75
|
+
expect(button.querySelector(".geocoding__spinner")).not.toBeNull();
|
|
76
|
+
expect(button.textContent).toContain("Locating...");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("uses custom locating text from data attribute", () => {
|
|
80
|
+
button.dataset.locatingText = "Finding you...";
|
|
81
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
82
|
+
|
|
83
|
+
button.click();
|
|
84
|
+
|
|
85
|
+
expect(button.textContent).toContain("Finding you...");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("stores original content for restoration", () => {
|
|
89
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
90
|
+
|
|
91
|
+
button.click();
|
|
92
|
+
|
|
93
|
+
expect(button.dataset.originalContent).toContain("Use my location");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("when geolocation succeeds", () => {
|
|
98
|
+
it("restores the button after successful reverse geocoding", () => {
|
|
99
|
+
const mockPosition = {
|
|
100
|
+
coords: { latitude: 40.7128, longitude: -74.006 }
|
|
101
|
+
};
|
|
102
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success) => {
|
|
103
|
+
success(mockPosition);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const mockDeferred = { fail: jest.fn() };
|
|
107
|
+
mockPost.mockImplementation((url, data, callback) => {
|
|
108
|
+
callback({ address: "New York, NY, USA" });
|
|
109
|
+
return mockDeferred;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
button.click();
|
|
113
|
+
|
|
114
|
+
expect(button.disabled).toBe(false);
|
|
115
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
116
|
+
expect(button.querySelector(".geocoding__spinner")).toBeNull();
|
|
117
|
+
expect(button.textContent).toContain("Use my location");
|
|
118
|
+
expect(input.value).toBe("New York, NY, USA");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("restores the button when reverse geocoding fails", () => {
|
|
122
|
+
const mockPosition = {
|
|
123
|
+
coords: { latitude: 40.7128, longitude: -74.006 }
|
|
124
|
+
};
|
|
125
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success) => {
|
|
126
|
+
success(mockPosition);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const mockDeferred = { fail: jest.fn((callback) => {
|
|
130
|
+
callback({}, "error", "Not Found");
|
|
131
|
+
}) };
|
|
132
|
+
mockPost.mockReturnValue(mockDeferred);
|
|
133
|
+
|
|
134
|
+
button.click();
|
|
135
|
+
|
|
136
|
+
expect(button.disabled).toBe(false);
|
|
137
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
138
|
+
expect(button.textContent).toContain("Use my location");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("when geolocation fails", () => {
|
|
143
|
+
it("restores the button when user denies permission", () => {
|
|
144
|
+
const mockError = { message: "User denied geolocation" };
|
|
145
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success, error) => {
|
|
146
|
+
error(mockError);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
button.click();
|
|
150
|
+
|
|
151
|
+
expect(button.disabled).toBe(false);
|
|
152
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
153
|
+
expect(button.textContent).toContain("Use my location");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("shows an error message", () => {
|
|
157
|
+
const mockError = { message: "User denied geolocation" };
|
|
158
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success, error) => {
|
|
159
|
+
error(mockError);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
button.click();
|
|
163
|
+
|
|
164
|
+
const errorElement = label.querySelector(".form-error");
|
|
165
|
+
expect(errorElement).not.toBeNull();
|
|
166
|
+
expect(errorElement.textContent).toContain("Could not detect location.");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("when geolocation is not supported", () => {
|
|
171
|
+
it("shows an unsupported error and does not show spinner", () => {
|
|
172
|
+
Reflect.defineProperty(global.navigator, "geolocation", {
|
|
173
|
+
value: null,
|
|
174
|
+
configurable: true
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
button.click();
|
|
178
|
+
|
|
179
|
+
expect(button.disabled).toBe(false);
|
|
180
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
181
|
+
const errorElement = label.querySelector(".form-error");
|
|
182
|
+
expect(errorElement).not.toBeNull();
|
|
183
|
+
expect(errorElement.textContent).toContain("Device not supported.");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("when button is already disabled", () => {
|
|
188
|
+
it("does not trigger geolocation again", () => {
|
|
189
|
+
button.disabled = true;
|
|
190
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
191
|
+
|
|
192
|
+
button.click();
|
|
193
|
+
|
|
194
|
+
expect(mockGeolocation.getCurrentPosition).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -34,7 +34,6 @@ import "src/decidim/security/selfxss_warning"
|
|
|
34
34
|
import "src/decidim/session_timeouter"
|
|
35
35
|
import "src/decidim/results_listing"
|
|
36
36
|
import "src/decidim/data_consent"
|
|
37
|
-
import "src/decidim/sw"
|
|
38
37
|
import "src/decidim/attachments"
|
|
39
38
|
import "src/decidim/dropdown_menu"
|
|
40
39
|
import "src/decidim/callout"
|
|
@@ -62,6 +61,7 @@ window.Decidim = window.Decidim || {
|
|
|
62
61
|
announceForScreenReader
|
|
63
62
|
};
|
|
64
63
|
|
|
64
|
+
window.createDialog = createDialog;
|
|
65
65
|
window.morphdom = morphdom
|
|
66
66
|
|
|
67
67
|
// eslint-disable-next-line max-params
|
|
@@ -156,6 +156,8 @@ document.addEventListener("turbo:load", () => {
|
|
|
156
156
|
deprecationMessage(container, '.callout[role="alert"]', '.flash[role="alert"]'));
|
|
157
157
|
document.querySelectorAll(".js-back-to-list").forEach((container) =>
|
|
158
158
|
deprecationMessage(container, ".js-back-to-list", "NEEDS TO BE REMOVED"));
|
|
159
|
+
document.querySelectorAll("[data-toggler]").forEach((container) =>
|
|
160
|
+
deprecationMessage(container, "[data-toggler]", "Use the Stimulus toggle controller with hidden targets"));
|
|
159
161
|
})
|
|
160
162
|
|
|
161
163
|
// REDESIGN_PENDING: deprecated
|
|
@@ -236,8 +238,7 @@ const initializer = (element = document) => {
|
|
|
236
238
|
document.dispatchEvent(new CustomEvent("decidim:loaded", { detail: { element } }));
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
//
|
|
240
|
-
// mentions stops working
|
|
241
|
+
// Keep this under jQuery ready to support components initialized on legacy templates
|
|
241
242
|
$(() => initializer());
|
|
242
243
|
|
|
243
244
|
// Run initializer action over the new DOM elements
|
|
@@ -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
|
+
});
|
|
@@ -53,7 +53,7 @@ self.addEventListener("notificationclick", (event) => {
|
|
|
53
53
|
// avoid caching admin or users paths
|
|
54
54
|
registerRoute(
|
|
55
55
|
({ url }) =>
|
|
56
|
-
["
|
|
56
|
+
["admin", "users"].some((pathSegment) => url.pathname.split("/").includes(pathSegment)),
|
|
57
57
|
new NetworkOnly()
|
|
58
58
|
);
|
|
59
59
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
const escapeQuotes = (text) => {
|
|
2
2
|
if (!text) {
|
|
3
3
|
return "";
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
el.appendChild(document.createTextNode(text));
|
|
8
|
-
return el.innerHTML;
|
|
6
|
+
return text.replace(/"/g, """);
|
|
9
7
|
}
|
|
10
8
|
|
|
11
|
-
export const
|
|
9
|
+
export const escapeHtml = (text) => {
|
|
12
10
|
if (!text) {
|
|
13
11
|
return "";
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
const el = document.createElement("div");
|
|
15
|
+
el.appendChild(document.createTextNode(text));
|
|
16
|
+
return escapeQuotes(el.innerHTML);
|
|
17
17
|
}
|
|
@@ -88,6 +88,20 @@
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
&__modal {
|
|
91
|
+
&-error {
|
|
92
|
+
[data-dialog-closable] {
|
|
93
|
+
@apply top-6 right-6;
|
|
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
|
+
}
|
|
104
|
+
|
|
91
105
|
&-results {
|
|
92
106
|
@apply mb-20 flex flex-wrap gap-x-8 gap-y-4;
|
|
93
107
|
}
|
|
@@ -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 {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.editor-suggestions-props {
|
|
2
|
+
--editor-suggestions-border-color: #000;
|
|
3
|
+
--editor-suggestions-background-color: #eee;
|
|
4
|
+
--editor-suggestions-selected-highlight-color: #ccc;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.editor-suggestions {
|
|
8
|
+
@apply editor-suggestions-props border-0 bg-[var(--editor-suggestions-background-color)] max-w-sm drop-shadow-md rounded-md;
|
|
9
|
+
|
|
10
|
+
&:hover {
|
|
11
|
+
.editor-suggestions-item[data-selected]:not(:hover) {
|
|
12
|
+
@apply bg-transparent;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.editor-suggestions-item {
|
|
17
|
+
@apply flex items-center gap-2 w-full text-left py-[0.375rem] px-1.5 border-0 rounded-none text-sm;
|
|
18
|
+
|
|
19
|
+
&:first-child {
|
|
20
|
+
@apply rounded-t-md;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&:last-child {
|
|
24
|
+
@apply rounded-b-md;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&:hover,
|
|
28
|
+
&[data-selected] {
|
|
29
|
+
@apply bg-[var(--editor-suggestions-selected-highlight-color)];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.editor-suggestions-item-avatar {
|
|
33
|
+
@apply rounded-full w-6 h-6 object-cover flex-none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.editor-suggestions-item-label {
|
|
37
|
+
@apply truncate;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.editor-suggestions-item-disabled {
|
|
42
|
+
@apply cursor-default;
|
|
43
|
+
|
|
44
|
+
&:hover,
|
|
45
|
+
&[data-selected] {
|
|
46
|
+
@apply bg-transparent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[data-floating-help] {
|
|
2
|
-
@apply absolute z-10 top-
|
|
2
|
+
@apply absolute z-10 top-20 md:top-10 right-0 [&_button]:bg-background [&_button]:px-2 [&_button]:rounded-r-none [&_svg]:text-gray [&_svg]:fill-current;
|
|
3
3
|
|
|
4
4
|
&:hover {
|
|
5
5
|
@apply [&_svg]:text-white [&_button]:text-white [&_button]:bg-secondary [&_button]:no-underline;
|
|
@@ -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 {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@use "tom-select/dist/scss/tom-select";
|
|
2
|
+
|
|
3
|
+
.ts {
|
|
4
|
+
&-control {
|
|
5
|
+
@apply border-gray text-md min-h-[40px];
|
|
6
|
+
|
|
7
|
+
input {
|
|
8
|
+
@apply font-normal text-black text-md;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
&-dropdown {
|
|
13
|
+
@apply text-md text-gray-2 font-normal;
|
|
14
|
+
|
|
15
|
+
.option img {
|
|
16
|
+
@apply inline;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.active {
|
|
20
|
+
@apply bg-gray-3;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|