decidim-core 0.31.6 → 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/card_metadata_cell.rb +1 -1
- data/app/cells/decidim/comments_button_cell.rb +9 -1
- 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/share_widget/qr_code_modal.erb +1 -1
- data/app/events/decidim/amendable/amendment_base_event.rb +7 -1
- data/app/helpers/decidim/menu_helper.rb +0 -2
- data/app/models/decidim/user.rb +2 -2
- 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/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_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/stylesheets/decidim/_cards.scss +1 -1
- 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/_participatory_spaces.scss +1 -1
- data/app/presenters/decidim/footer_menu_presenter.rb +2 -4
- data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -0
- data/app/views/decidim/searches/_filters.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/locales/ar.yml +0 -20
- data/config/locales/bg.yml +0 -19
- data/config/locales/ca-IT.yml +28 -23
- data/config/locales/ca.yml +28 -23
- data/config/locales/cs.yml +13 -21
- data/config/locales/de.yml +0 -25
- data/config/locales/el.yml +0 -18
- data/config/locales/en.yml +28 -23
- data/config/locales/es-MX.yml +28 -23
- data/config/locales/es-PY.yml +28 -23
- data/config/locales/es.yml +28 -23
- data/config/locales/eu.yml +27 -24
- data/config/locales/fi-plain.yml +28 -23
- data/config/locales/fi.yml +28 -23
- data/config/locales/fr-CA.yml +7 -19
- data/config/locales/fr.yml +7 -19
- data/config/locales/gl.yml +0 -19
- data/config/locales/hu.yml +0 -17
- 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 +28 -23
- 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 -19
- data/config/locales/pt-BR.yml +0 -25
- data/config/locales/pt.yml +0 -18
- data/config/locales/ro-RO.yml +0 -18
- data/config/locales/ru.yml +0 -8
- data/config/locales/sk.yml +0 -25
- data/config/locales/sv.yml +4 -25
- data/config/locales/tr-TR.yml +0 -18
- 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/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/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/dynamic_map.rb +3 -1
- metadata +27 -6
|
@@ -142,6 +142,34 @@ describe("DropdownController", () => {
|
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
describe("aria-expanded initialization", () => {
|
|
146
|
+
it("sets aria-expanded to false when missing and isOpen is false", () => {
|
|
147
|
+
const testController = createController(element);
|
|
148
|
+
|
|
149
|
+
testController.connect();
|
|
150
|
+
|
|
151
|
+
expect(element.getAttribute("aria-expanded")).toBe("false");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("sets aria-expanded to true when missing and data-open is true", () => {
|
|
155
|
+
element.setAttribute("data-open", "true");
|
|
156
|
+
const testController = createController(element);
|
|
157
|
+
|
|
158
|
+
testController.connect();
|
|
159
|
+
|
|
160
|
+
expect(element.getAttribute("aria-expanded")).toBe("true");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("does not override existing aria-expanded attribute", () => {
|
|
164
|
+
element.setAttribute("aria-expanded", "true");
|
|
165
|
+
const testController = createController(element);
|
|
166
|
+
|
|
167
|
+
testController.connect();
|
|
168
|
+
|
|
169
|
+
expect(element.getAttribute("aria-expanded")).toBe("true");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
145
173
|
describe("data-add-aria-roles option", () => {
|
|
146
174
|
it("keeps role menu when data-add-aria-roles is true", () => {
|
|
147
175
|
element.setAttribute("data-add-aria-roles", "true");
|
|
@@ -55,6 +55,18 @@ export default class extends Controller {
|
|
|
55
55
|
hide: icon("eye-off-line")
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
const existingWrapper = this.input.closest(".input-group__password");
|
|
59
|
+
if (existingWrapper) {
|
|
60
|
+
const existingButton = existingWrapper.querySelector('button[type="button"]');
|
|
61
|
+
const existingStatusText = existingWrapper.querySelector('span.sr-only[aria-live="polite"]');
|
|
62
|
+
if (existingButton && existingStatusText) {
|
|
63
|
+
this.button = existingButton;
|
|
64
|
+
this.statusText = existingStatusText;
|
|
65
|
+
this.bindListeners();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
this.init();
|
|
59
71
|
}
|
|
60
72
|
|
|
@@ -81,8 +93,17 @@ export default class extends Controller {
|
|
|
81
93
|
*/
|
|
82
94
|
init() {
|
|
83
95
|
this.createControls();
|
|
96
|
+
this.bindListeners();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
bindListeners() {
|
|
100
|
+
if (this.button && this.boundToggleVisibility) {
|
|
101
|
+
this.button.removeEventListener("click", this.boundToggleVisibility);
|
|
102
|
+
}
|
|
103
|
+
if (this.form && this.boundHidePassword) {
|
|
104
|
+
this.form.removeEventListener("submit", this.boundHidePassword);
|
|
105
|
+
}
|
|
84
106
|
|
|
85
|
-
// Bind methods to maintain correct context
|
|
86
107
|
this.boundToggleVisibility = this.toggleVisibility.bind(this);
|
|
87
108
|
this.boundHidePassword = this.hidePassword.bind(this);
|
|
88
109
|
|
|
@@ -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");
|
|
@@ -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
|
+
});
|
|
@@ -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 {
|
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
module Decidim
|
|
4
4
|
# A presenter to render footer menu
|
|
5
5
|
class FooterMenuPresenter < MenuPresenter
|
|
6
|
-
def
|
|
7
|
-
|
|
8
|
-
safe_join([content_tag(:h2, @options[:label], class: "h5 mb-4"), render_menu])
|
|
9
|
-
end
|
|
6
|
+
def render_menu_items
|
|
7
|
+
render_menu
|
|
10
8
|
end
|
|
11
9
|
end
|
|
12
10
|
end
|
|
@@ -79,6 +79,9 @@ module Decidim
|
|
|
79
79
|
image_url = image_element["src"]
|
|
80
80
|
next if image_url.blank?
|
|
81
81
|
|
|
82
|
+
blob = find_blob_by_id(image_url.split("/").last)
|
|
83
|
+
return blob if blob.present?
|
|
84
|
+
|
|
82
85
|
blob = find_blob_by_key(image_url.split("/").second_to_last)
|
|
83
86
|
return blob if blob.present?
|
|
84
87
|
end
|
|
@@ -120,6 +123,10 @@ module Decidim
|
|
|
120
123
|
ActiveStorage::Blob.find_signed(blob_key) if blob_key.present?
|
|
121
124
|
end
|
|
122
125
|
|
|
126
|
+
def find_blob_by_id(blob_id)
|
|
127
|
+
ActiveStorage::Blob.find_by(id: blob_id) if blob_id.present?
|
|
128
|
+
end
|
|
129
|
+
|
|
123
130
|
def blob_from_attached_file(file)
|
|
124
131
|
return unless file.respond_to?(:attached?) && file.attached?
|
|
125
132
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</button>
|
|
17
17
|
<% end %>
|
|
18
18
|
</div>
|
|
19
|
-
<button id="dropdown-trigger-search" data-controller="dropdown" data-target="dropdown-menu-search" data-auto-close="true"
|
|
19
|
+
<button id="dropdown-trigger-search" data-controller="dropdown" data-target="dropdown-menu-search" data-auto-close="true">
|
|
20
20
|
<%= content_tag :span, t("decidim.searches.filters_small_view.filter_by"), class: "#{"is-active" if params.dig(:filter, :with_resource_type) == nil}" %>
|
|
21
21
|
<% @blocks.each do |elements| %>
|
|
22
22
|
<% elements.each do |type, results| %>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<div class="lg:w-1/3">
|
|
4
4
|
<%= render partial: "layouts/decidim/footer/main_intro" %>
|
|
5
5
|
</div>
|
|
6
|
-
<div class="lg:w-2/3 grid grid-cols-1 gap-x-4
|
|
6
|
+
<div class="lg:w-2/3 grid grid-cols-1 gap-x-4 lg:grid-cols-4 text-md text-white">
|
|
7
7
|
<%= render partial: "layouts/decidim/footer/main_links" %>
|
|
8
8
|
</div>
|
|
9
9
|
</div>
|
|
@@ -1,31 +1,59 @@
|
|
|
1
|
-
|
|
1
|
+
<div data-controller="accordion" data-multiselectable="true" class="contents">
|
|
2
|
+
<nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.decidim_title") %>">
|
|
3
|
+
<button type="button" id="panel-footer-menu-trigger" data-controls="panel-footer-menu" data-open-lg="true">
|
|
4
|
+
<span class="h4"><%= t("layouts.decidim.footer.decidim_title") %></span>
|
|
5
|
+
<%= icon "arrow-down-s-line" %>
|
|
6
|
+
</button>
|
|
7
|
+
<div id="panel-footer-menu" aria-hidden="true">
|
|
8
|
+
<%= footer_menu.render_menu_items %>
|
|
9
|
+
</div>
|
|
10
|
+
</nav>
|
|
2
11
|
|
|
3
|
-
<nav role="navigation" aria-label="<%= t("layouts.decidim.user_menu.profile") %>">
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
<nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.user_menu.profile") %>">
|
|
13
|
+
<button type="button" id="panel-profile-trigger" data-controls="panel-profile" data-open-lg="true">
|
|
14
|
+
<span class="h4"><%= t("layouts.decidim.user_menu.profile") %></span>
|
|
15
|
+
<%= icon "arrow-down-s-line" %>
|
|
16
|
+
</button>
|
|
17
|
+
<div id="panel-profile" aria-hidden="true">
|
|
18
|
+
<ul>
|
|
19
|
+
<% if current_user %>
|
|
20
|
+
<li><%= link_to t("layouts.decidim.user_menu.configuration"), decidim.account_path %></li>
|
|
21
|
+
<li><%= link_to t("layouts.decidim.user_menu.public_profile"), decidim.profile_path(current_user.nickname) %></li>
|
|
22
|
+
<li><%= link_to t("layouts.decidim.user_menu.notifications"), decidim.notifications_path %></li>
|
|
23
|
+
<li><%= link_to t("layouts.decidim.user_menu.conversations"), decidim.conversations_path %></li>
|
|
24
|
+
<% else %>
|
|
25
|
+
<% if current_organization.sign_up_enabled? %>
|
|
26
|
+
<li><%= link_to t("layouts.decidim.footer.sign_up"), decidim.new_user_registration_path %></li>
|
|
27
|
+
<% end %>
|
|
28
|
+
<li><%= link_to t("layouts.decidim.footer.log_in"), decidim.new_user_session_path %></li>
|
|
29
|
+
<% end %>
|
|
30
|
+
</ul>
|
|
31
|
+
</div>
|
|
32
|
+
</nav>
|
|
19
33
|
|
|
20
|
-
<nav role="navigation" aria-label="<%= t("layouts.decidim.footer.resources") %>">
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
<nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.resources") %>">
|
|
35
|
+
<button type="button" id="panel-resources-trigger" data-controls="panel-resources" data-open-lg="true">
|
|
36
|
+
<span class="h4"><%= t("layouts.decidim.footer.resources") %></span>
|
|
37
|
+
<%= icon "arrow-down-s-line" %>
|
|
38
|
+
</button>
|
|
39
|
+
<div id="panel-resources" aria-hidden="true">
|
|
40
|
+
<ul>
|
|
41
|
+
<li><%= link_to t("decidim.profiles.show.activity"), decidim.last_activities_path %></li>
|
|
42
|
+
<% if Decidim.module_installed?(:meetings) %>
|
|
43
|
+
<li><%= link_to t("decidim.pages.home.extended.meetings"), Decidim::Meetings::DirectoryEngine.routes.url_helpers.root_path %></li>
|
|
44
|
+
<% end %>
|
|
45
|
+
<li><%= link_to t("layouts.decidim.footer.open_data"), decidim.open_data_path %></li>
|
|
46
|
+
</ul>
|
|
47
|
+
</div>
|
|
48
|
+
</nav>
|
|
30
49
|
|
|
31
|
-
<%=
|
|
50
|
+
<nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.help") %>">
|
|
51
|
+
<button type="button" id="panel-help-trigger" data-controls="panel-help" data-open-lg="true">
|
|
52
|
+
<span class="h4"><%= t("layouts.decidim.footer.help") %></span>
|
|
53
|
+
<%= icon "arrow-down-s-line" %>
|
|
54
|
+
</button>
|
|
55
|
+
<div id="panel-help" aria-hidden="true">
|
|
56
|
+
<%= cell("decidim/footer_topics", nil) %>
|
|
57
|
+
</div>
|
|
58
|
+
</nav>
|
|
59
|
+
</div>
|