decidim-core 0.32.0 → 0.32.1
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/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/share_widget/qr_code_modal.erb +1 -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/main_menu/controller.js +4 -0
- data/app/packs/src/decidim/controllers/main_menu/main_menu.test.js +23 -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/ca-IT.yml +6 -0
- data/config/locales/ca.yml +6 -0
- data/config/locales/cs.yml +16 -0
- data/config/locales/en.yml +6 -0
- data/config/locales/es-MX.yml +6 -0
- data/config/locales/es-PY.yml +6 -0
- data/config/locales/es.yml +6 -0
- data/config/locales/eu.yml +5 -1
- data/config/locales/fi-plain.yml +6 -0
- data/config/locales/fi.yml +6 -0
- data/config/locales/fr-CA.yml +7 -0
- data/config/locales/fr.yml +7 -0
- data/config/locales/ja.yml +25 -0
- data/config/locales/pl.yml +2 -0
- data/db/migrate/20250217192438_convert_user_groups_into_users.rb +3 -2
- data/decidim-core.gemspec +4 -1
- data/lib/decidim/core/test/shared_examples/access_mode_restricted_participatory_spaces.rb +8 -5
- data/lib/decidim/core/test/shared_examples/access_mode_transparent_participatory_spaces.rb +14 -11
- data/lib/decidim/core/test/shared_examples/comments_examples.rb +26 -0
- 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 +26 -5
|
@@ -70,6 +70,29 @@ describe("MainMenuController", () => {
|
|
|
70
70
|
expect(closeSpy).toHaveBeenCalledWith("click", controller.handleCloseButtonClick)
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
it("sets aria-expanded to false when missing", async () => {
|
|
74
|
+
document.body.innerHTML = `
|
|
75
|
+
<button data-controller="main-menu" data-target="main-menu-container" data-close-button="main-menu-close">
|
|
76
|
+
Menu
|
|
77
|
+
</button>
|
|
78
|
+
<div id="main-menu-container" aria-hidden="true"></div>
|
|
79
|
+
<button id="main-menu-close">Close</button>
|
|
80
|
+
`
|
|
81
|
+
|
|
82
|
+
application.stop()
|
|
83
|
+
application = Application.start()
|
|
84
|
+
application.register("main-menu", MainMenuController)
|
|
85
|
+
|
|
86
|
+
const newButton = document.querySelector('[data-controller="main-menu"]')
|
|
87
|
+
await new Promise((resolve) => {
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
resolve(application.getControllerForElementAndIdentifier(newButton, "main-menu"))
|
|
90
|
+
}, 0)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
expect(newButton.getAttribute("aria-expanded")).toBe("false")
|
|
94
|
+
})
|
|
95
|
+
|
|
73
96
|
it("returns early when menu container is missing", async () => {
|
|
74
97
|
document.body.innerHTML = `
|
|
75
98
|
<button data-controller="main-menu" data-target="missing-menu"></button>
|
|
@@ -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>
|
data/config/locales/ca-IT.yml
CHANGED
|
@@ -1205,6 +1205,8 @@ ca-IT:
|
|
|
1205
1205
|
dynamic:
|
|
1206
1206
|
screen_reader_explanation: El següent element és un mapa que presenta els components d'aquesta pàgina com a punts al mapa. L'element es pot fer servir amb un lector de pantalla però pot ser difícil d'entendre.
|
|
1207
1207
|
skip_button: Saltar el mapa
|
|
1208
|
+
zoom_in: Ampliar
|
|
1209
|
+
zoom_out: Allunyar
|
|
1208
1210
|
static:
|
|
1209
1211
|
latlng_text: 'latitud: %{latitude}, longitud: %{longitude}'
|
|
1210
1212
|
map_service_brand: OpenStreetMap
|
|
@@ -1546,6 +1548,10 @@ ca-IT:
|
|
|
1546
1548
|
never_logged_in: mai ha iniciat sessió
|
|
1547
1549
|
subject: Avís de compte inactiu
|
|
1548
1550
|
participatory_space:
|
|
1551
|
+
labels:
|
|
1552
|
+
reference: Referència de l'espai de participació
|
|
1553
|
+
restricted: Espai de participació restringit
|
|
1554
|
+
transparent: Espai de participació transparent
|
|
1549
1555
|
pages:
|
|
1550
1556
|
user_profile:
|
|
1551
1557
|
member_of:
|
data/config/locales/ca.yml
CHANGED
|
@@ -1205,6 +1205,8 @@ ca:
|
|
|
1205
1205
|
dynamic:
|
|
1206
1206
|
screen_reader_explanation: El següent element és un mapa que presenta els components d'aquesta pàgina com a punts al mapa. L'element es pot fer servir amb un lector de pantalla però pot ser difícil d'entendre.
|
|
1207
1207
|
skip_button: Saltar el mapa
|
|
1208
|
+
zoom_in: Ampliar
|
|
1209
|
+
zoom_out: Allunyar
|
|
1208
1210
|
static:
|
|
1209
1211
|
latlng_text: 'latitud: %{latitude}, longitud: %{longitude}'
|
|
1210
1212
|
map_service_brand: OpenStreetMap
|
|
@@ -1546,6 +1548,10 @@ ca:
|
|
|
1546
1548
|
never_logged_in: mai ha iniciat sessió
|
|
1547
1549
|
subject: Avís de compte inactiu
|
|
1548
1550
|
participatory_space:
|
|
1551
|
+
labels:
|
|
1552
|
+
reference: Referència de l'espai de participació
|
|
1553
|
+
restricted: Espai de participació restringit
|
|
1554
|
+
transparent: Espai de participació transparent
|
|
1549
1555
|
pages:
|
|
1550
1556
|
user_profile:
|
|
1551
1557
|
member_of:
|