decidim-core 0.31.2 → 0.31.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/cells/decidim/content_blocks/highlighted_elements_with_cell_for_list_cell.rb +5 -1
- data/app/cells/decidim/report_button/already_reported_modal.erb +1 -1
- data/app/cells/decidim/report_button/flag_modal.erb +1 -1
- data/app/cells/decidim/report_user_button/already_reported_modal.erb +1 -1
- data/app/cells/decidim/report_user_button/flag_modal.erb +1 -1
- data/app/cells/decidim/share_text_widget/modal.erb +1 -1
- data/app/cells/decidim/upload_modal/files.erb +5 -1
- data/app/cells/decidim/upload_modal_cell.rb +10 -1
- data/app/commands/decidim/multiple_attachments_methods.rb +20 -3
- data/app/jobs/decidim/find_and_update_descendants_job.rb +8 -2
- data/app/jobs/decidim/update_search_indexes_job.rb +2 -2
- data/app/packs/src/decidim/a11y.js +29 -0
- data/app/packs/src/decidim/a11y.test.js +81 -0
- data/app/packs/src/decidim/confirm.js +8 -1
- data/app/packs/src/decidim/confirm.test.js +225 -0
- data/app/packs/src/decidim/controllers/language_change/controller.js +1 -0
- data/app/packs/src/decidim/controllers/language_change/language_change.test.js +13 -0
- data/app/packs/src/decidim/datepicker/datepicker_functions.js +26 -0
- data/app/packs/src/decidim/datepicker/generate_datepicker.js +2 -1
- data/app/packs/src/decidim/datepicker/generate_timepicker.js +3 -2
- data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +234 -0
- data/app/packs/src/decidim/refactor/moved/focus_guard.js +4 -4
- data/app/packs/stylesheets/decidim/_cards.scss +12 -4
- data/app/packs/stylesheets/decidim/_flash.scss +1 -1
- data/app/views/decidim/devise/invitations/edit.html.erb +3 -3
- data/config/initializers/devise.rb +6 -0
- data/config/locales/ar.yml +3 -3
- data/config/locales/bg.yml +0 -4
- data/config/locales/ca-IT.yml +7 -6
- data/config/locales/ca.yml +7 -6
- data/config/locales/cs.yml +5 -8
- data/config/locales/de.yml +4 -8
- data/config/locales/el.yml +0 -2
- data/config/locales/en.yml +5 -4
- data/config/locales/es-MX.yml +10 -9
- data/config/locales/es-PY.yml +10 -9
- data/config/locales/es.yml +12 -11
- data/config/locales/eu.yml +4 -5
- data/config/locales/fi-plain.yml +5 -4
- data/config/locales/fi.yml +6 -5
- data/config/locales/fr-CA.yml +7 -5
- data/config/locales/fr.yml +8 -7
- data/config/locales/gl.yml +0 -2
- data/config/locales/hu.yml +4 -8
- data/config/locales/id-ID.yml +0 -2
- data/config/locales/it.yml +1 -3
- data/config/locales/ja.yml +7 -8
- data/config/locales/lb.yml +0 -2
- data/config/locales/lt.yml +1 -3
- data/config/locales/lv.yml +0 -2
- data/config/locales/nl.yml +0 -2
- data/config/locales/no.yml +0 -2
- data/config/locales/pl.yml +0 -4
- data/config/locales/pt-BR.yml +4 -5
- data/config/locales/pt.yml +0 -2
- data/config/locales/ro-RO.yml +1 -5
- data/config/locales/ru.yml +0 -2
- data/config/locales/sk.yml +0 -4
- data/config/locales/sv.yml +8 -7
- data/config/locales/tr-TR.yml +17 -5
- data/config/locales/zh-CN.yml +0 -2
- data/config/locales/zh-TW.yml +1 -3
- data/lib/decidim/assets/tailwind/tailwind.config.js.erb +1 -1
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/form_builder.rb +58 -36
- data/lib/decidim/maintenance/taxonomy_importer.rb +1 -1
- data/lib/decidim/searchable.rb +4 -4
- metadata +9 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c44cbb210028e54b7a2bcc3a10fc79e041e355eac55e81b9a4b934f5fb5b42db
|
|
4
|
+
data.tar.gz: 0d87937801924b8d92ef31e2b136019090118d89b46ddbe5f2ce4052eef207b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 508ad856daf2345e66f3c6b0b01c1a0c851d225eca8d0e4e739d79f5584dacb6de8c0155b8a3f28ac80961f6346c31ef3d6fdc14f80299fd80184ea64697b4ef
|
|
7
|
+
data.tar.gz: 931b27cfcafe94a6f922d26c2c419dbf89386f64147234d5cf129641b52e2651e877cbfff63ec13f9844c5e6f91c12730df30f0fea2a5dffe2f6c61551bee522
|
|
@@ -19,10 +19,14 @@ module Decidim
|
|
|
19
19
|
@list_cell ||= cell(
|
|
20
20
|
list_cell_path,
|
|
21
21
|
published_components.one? ? published_components.first : published_components,
|
|
22
|
-
**model.settings.to_h, see_all_path:
|
|
22
|
+
**model.settings.to_h, **extra_list_cell_options, see_all_path:
|
|
23
23
|
)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def extra_list_cell_options
|
|
27
|
+
{}
|
|
28
|
+
end
|
|
29
|
+
|
|
26
30
|
def see_all_path; end
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%= decidim_modal id: modal_id, class: "flag-modal" do %>
|
|
2
2
|
<div data-dialog-container>
|
|
3
3
|
<%= icon "flag-line" %>
|
|
4
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
4
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
5
5
|
<div>
|
|
6
6
|
<div class="form__wrapper flag-modal__form">
|
|
7
7
|
<p class="flag-modal__form-description"><%= t("decidim.shared.flag_modal.already_reported") %></p>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<%= decidim_form_for report_form, builder:, url: report_path, method: :post, html: { id: nil, data: { controller: "report-form" } } do |f| %>
|
|
3
3
|
<div data-dialog-container>
|
|
4
4
|
<%= icon "flag-line" %>
|
|
5
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
5
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
6
6
|
<div>
|
|
7
7
|
<div class="form__wrapper flag-modal__form">
|
|
8
8
|
<p id="dialog-desc-<%= modal_id %>" class="flag-modal__form-description"><%= t("decidim.shared.flag_modal.description") %></p>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%= decidim_modal id: modal_id, class: "flag-user-modal" do %>
|
|
2
2
|
<div data-dialog-container>
|
|
3
3
|
<%= icon "flag-line" %>
|
|
4
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
4
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
5
5
|
<div>
|
|
6
6
|
<div class="form__wrapper flag-modal__form">
|
|
7
7
|
<p class="flag-modal__form-description"><%= t("decidim.shared.flag_user_modal.already_reported") %></p>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<%= decidim_form_for report_form, builder:, url: report_path, method: :post, html: { id: nil, data: { controller: "report-form" } } do |f| %>
|
|
3
3
|
<div data-dialog-container>
|
|
4
4
|
<%= icon "flag-line" %>
|
|
5
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
5
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
6
6
|
<div>
|
|
7
7
|
<div class="form__wrapper flag-modal__form">
|
|
8
8
|
<p class="flag-modal__form-description"><%= t("decidim.shared.flag_user_modal.description") %></p>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%= decidim_modal id: "socialShare", class: "share-modal" do %>
|
|
2
2
|
<div data-dialog-container>
|
|
3
|
-
<h2 tabindex="-1" data-dialog-title><%= t("share", scope: "decidim.shared.share_modal") %></h2>
|
|
3
|
+
<h2 id="dialog-title-socialShare" tabindex="-1" data-dialog-title><%= t("share", scope: "decidim.shared.share_modal") %></h2>
|
|
4
4
|
|
|
5
5
|
<div>
|
|
6
6
|
|
|
@@ -42,7 +42,11 @@
|
|
|
42
42
|
<% end %>
|
|
43
43
|
<% end %>
|
|
44
44
|
<% if attachment_blob.present? %>
|
|
45
|
-
|
|
45
|
+
<% if is_persisted_attachment %>
|
|
46
|
+
<%= form.hidden_field attribute, value: attachment.id, id: "hidden_#{attribute}_#{attachment.id}" %>
|
|
47
|
+
<% else %>
|
|
48
|
+
<%= form.hidden_field attribute, value: attachment_blob.signed_id, id: "hidden_#{attribute}_#{attachment_blob.id}" %>
|
|
49
|
+
<% end %>
|
|
46
50
|
<% end %>
|
|
47
51
|
</div>
|
|
48
52
|
<% end %>
|
|
@@ -128,7 +128,16 @@ module Decidim
|
|
|
128
128
|
@attachments = begin
|
|
129
129
|
attachments = options[:attachments] || form.object.send(attribute)
|
|
130
130
|
attachments = Array(attachments).compact_blank
|
|
131
|
-
attachments.map
|
|
131
|
+
attachments.map do |attachment|
|
|
132
|
+
case attachment
|
|
133
|
+
when String
|
|
134
|
+
ActiveStorage::Blob.find_signed(attachment)
|
|
135
|
+
when Integer
|
|
136
|
+
Decidim::Attachment.find_by(id: attachment)
|
|
137
|
+
else
|
|
138
|
+
attachment
|
|
139
|
+
end
|
|
140
|
+
end.compact
|
|
132
141
|
end
|
|
133
142
|
end
|
|
134
143
|
|
|
@@ -41,8 +41,9 @@ module Decidim
|
|
|
41
41
|
|
|
42
42
|
def create_attachments(first_weight: 0)
|
|
43
43
|
weight = first_weight
|
|
44
|
-
# Add the weights first to the old
|
|
45
|
-
|
|
44
|
+
# Add the weights first to the old documents
|
|
45
|
+
document_ids = keep_ids
|
|
46
|
+
Decidim::Attachment.where(id: document_ids).each do |document|
|
|
46
47
|
document.update!(weight:)
|
|
47
48
|
weight += 1
|
|
48
49
|
end
|
|
@@ -59,7 +60,7 @@ module Decidim
|
|
|
59
60
|
documents = include_all_attachments ? documents_attached_to.attachments.with_attached_file : documents_attached_to.documents
|
|
60
61
|
|
|
61
62
|
documents.each do |document|
|
|
62
|
-
document.destroy!
|
|
63
|
+
document.destroy! unless keep_ids.include?(document.id)
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
documents_attached_to.reload
|
|
@@ -98,5 +99,21 @@ module Decidim
|
|
|
98
99
|
def blob(signed_id)
|
|
99
100
|
ActiveStorage::Blob.find_signed(signed_id)
|
|
100
101
|
end
|
|
102
|
+
|
|
103
|
+
def keep_ids
|
|
104
|
+
documents_array = Array(@form.documents)
|
|
105
|
+
documents_array.map do |doc|
|
|
106
|
+
case doc
|
|
107
|
+
when Decidim::Attachment
|
|
108
|
+
doc.id
|
|
109
|
+
when Integer
|
|
110
|
+
doc
|
|
111
|
+
when String
|
|
112
|
+
doc.match?(/\A\d+\z/) ? doc.to_i : nil
|
|
113
|
+
when Hash
|
|
114
|
+
(doc[:id] || doc["id"]).to_i
|
|
115
|
+
end
|
|
116
|
+
end.compact
|
|
117
|
+
end
|
|
101
118
|
end
|
|
102
119
|
end
|
|
@@ -4,8 +4,14 @@ module Decidim
|
|
|
4
4
|
# Update search indexes for each descendants of a given element
|
|
5
5
|
class FindAndUpdateDescendantsJob < ApplicationJob
|
|
6
6
|
queue_as :default
|
|
7
|
+
MAX_DEPTH = 5
|
|
8
|
+
|
|
9
|
+
def perform(element, current_depth = 0)
|
|
10
|
+
if current_depth >= MAX_DEPTH
|
|
11
|
+
Rails.logger.warn "Max depth of #{MAX_DEPTH} reached for element #{element.class.name} with id #{element.id}. Stopping recursion."
|
|
12
|
+
return
|
|
13
|
+
end
|
|
7
14
|
|
|
8
|
-
def perform(element)
|
|
9
15
|
descendants_collector = components_for(element)
|
|
10
16
|
descendants_collector << element.comments.to_a if element.respond_to?(:comments)
|
|
11
17
|
|
|
@@ -14,7 +20,7 @@ module Decidim
|
|
|
14
20
|
descendants_collector.each do |descendants|
|
|
15
21
|
next if descendants.blank?
|
|
16
22
|
|
|
17
|
-
Decidim::UpdateSearchIndexesJob.perform_later(descendants)
|
|
23
|
+
Decidim::UpdateSearchIndexesJob.perform_later(descendants, current_depth + 1)
|
|
18
24
|
end
|
|
19
25
|
end
|
|
20
26
|
|
|
@@ -4,8 +4,8 @@ module Decidim
|
|
|
4
4
|
class UpdateSearchIndexesJob < ApplicationJob
|
|
5
5
|
queue_as :default
|
|
6
6
|
|
|
7
|
-
def perform(elements)
|
|
8
|
-
elements.each { |element| element.try(:try_update_index_for_search_resource) }
|
|
7
|
+
def perform(elements, current_depth = 0)
|
|
8
|
+
elements.each { |element| element.try(:try_update_index_for_search_resource, current_depth) }
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -8,6 +8,13 @@ import Dialogs from "a11y-dialog-component";
|
|
|
8
8
|
* @return {void}
|
|
9
9
|
*/
|
|
10
10
|
const createDialog = (component) => {
|
|
11
|
+
const getFocusableElements = (container) => {
|
|
12
|
+
const selectors = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex='-1'])";
|
|
13
|
+
return Array.from(container.querySelectorAll(selectors)).filter(
|
|
14
|
+
(el) => el.offsetParent !== null
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
11
18
|
const {
|
|
12
19
|
dataset: { dialog, ...attrs }
|
|
13
20
|
} = component;
|
|
@@ -29,11 +36,33 @@ const createDialog = (component) => {
|
|
|
29
36
|
backdropSelector: `[data-dialog="${dialog}"]`,
|
|
30
37
|
enableAutoFocus: false,
|
|
31
38
|
onOpen: (params, trigger) => {
|
|
39
|
+
const keyHandler = (event) => {
|
|
40
|
+
if (event.key !== "Tab") {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const focusable = getFocusableElements(params);
|
|
44
|
+
if (focusable.length === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (event.shiftKey && document.activeElement === focusable[0]) {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
focusable[focusable.length - 1].focus({ preventScroll: true });
|
|
50
|
+
} else if (!event.shiftKey && document.activeElement === focusable[focusable.length - 1]) {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
focusable[0].focus({ preventScroll: true });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
params._focusTrapHandler = keyHandler;
|
|
56
|
+
params.addEventListener("keydown", keyHandler);
|
|
32
57
|
setFocusOnTitle(params);
|
|
33
58
|
window.focusGuard.trap(params, trigger);
|
|
34
59
|
params.dispatchEvent(new CustomEvent("open.dialog"));
|
|
35
60
|
},
|
|
36
61
|
onClose: (params) => {
|
|
62
|
+
if (params._focusTrapHandler) {
|
|
63
|
+
params.removeEventListener("keydown", params._focusTrapHandler);
|
|
64
|
+
Reflect.deleteProperty(params, "_focusTrapHandler");
|
|
65
|
+
}
|
|
37
66
|
window.focusGuard.disable();
|
|
38
67
|
params.dispatchEvent(new CustomEvent("close.dialog"));
|
|
39
68
|
},
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
|
|
3
|
+
import { createDialog } from "src/decidim/a11y"
|
|
4
|
+
|
|
5
|
+
describe("a11y dialog focus trap", () => {
|
|
6
|
+
const dialogHtml = `
|
|
7
|
+
<button id="open-btn" data-dialog-open="testDialog">Open</button>
|
|
8
|
+
<div id="test-dialog" data-dialog="testDialog">
|
|
9
|
+
<div data-dialog-container>
|
|
10
|
+
<h2 id="dialog-title-testDialog" tabindex="-1" data-dialog-title>Test Dialog</h2>
|
|
11
|
+
<a href="#">Link 1</a>
|
|
12
|
+
<button>Button 1</button>
|
|
13
|
+
<input type="text" />
|
|
14
|
+
<button>Button 2</button>
|
|
15
|
+
<a href="#">Link 2</a>
|
|
16
|
+
</div>
|
|
17
|
+
<div data-dialog-actions>
|
|
18
|
+
<button data-dialog-close="testDialog">Close</button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
document.body.innerHTML = dialogHtml;
|
|
25
|
+
window.Decidim = {
|
|
26
|
+
currentDialogs: {}
|
|
27
|
+
};
|
|
28
|
+
window.focusGuard = {
|
|
29
|
+
trap: jest.fn(),
|
|
30
|
+
disable: jest.fn()
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("keydown handler", () => {
|
|
35
|
+
let dialogEl = null;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
const component = document.querySelector("[data-dialog]");
|
|
39
|
+
createDialog(component);
|
|
40
|
+
dialogEl = document.querySelector("[data-dialog='testDialog']");
|
|
41
|
+
// Get the dialog from Decidim.currentDialogs and open it
|
|
42
|
+
const dialog = window.Decidim.currentDialogs.testDialog;
|
|
43
|
+
dialog.open();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("adds keydown handler on open", () => {
|
|
47
|
+
expect(dialogEl._focusTrapHandler).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("handles Tab key", () => {
|
|
51
|
+
const selectors = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex='-1'])";
|
|
52
|
+
const tabbableElements = Array.from(dialogEl.querySelectorAll(selectors)).filter(
|
|
53
|
+
(el) => el.offsetParent || el.offsetParent === null
|
|
54
|
+
);
|
|
55
|
+
tabbableElements[tabbableElements.length - 1].focus();
|
|
56
|
+
|
|
57
|
+
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true });
|
|
58
|
+
const preventDefault = jest.fn();
|
|
59
|
+
event.preventDefault = preventDefault;
|
|
60
|
+
|
|
61
|
+
dialogEl.dispatchEvent(event);
|
|
62
|
+
|
|
63
|
+
expect(preventDefault).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("onClose cleanup", () => {
|
|
68
|
+
it("removes the keydown handler on close", () => {
|
|
69
|
+
const component = document.querySelector("[data-dialog]");
|
|
70
|
+
createDialog(component);
|
|
71
|
+
const dialogEl = document.querySelector("[data-dialog='testDialog']");
|
|
72
|
+
|
|
73
|
+
const dialog = window.Decidim.currentDialogs.testDialog;
|
|
74
|
+
dialog.open();
|
|
75
|
+
expect(dialogEl._focusTrapHandler).toBeDefined();
|
|
76
|
+
|
|
77
|
+
dialog.close();
|
|
78
|
+
expect(dialogEl._focusTrapHandler).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -169,7 +169,9 @@ export const initializeConfirm = () => {
|
|
|
169
169
|
return handleDocumentEvent(ev, [
|
|
170
170
|
Rails.linkClickSelector,
|
|
171
171
|
Rails.buttonClickSelector,
|
|
172
|
-
Rails.formInputClickSelector
|
|
172
|
+
Rails.formInputClickSelector,
|
|
173
|
+
'button[data-confirm][type="button"]',
|
|
174
|
+
"form button[data-confirm]"
|
|
173
175
|
]);
|
|
174
176
|
});
|
|
175
177
|
document.addEventListener("change", (ev) => {
|
|
@@ -187,6 +189,11 @@ export const initializeConfirm = () => {
|
|
|
187
189
|
$(Rails.formInputClickSelector).on("click.confirm", (ev) => {
|
|
188
190
|
handleConfirm(ev, getMatchingEventTarget(ev, Rails.formInputClickSelector));
|
|
189
191
|
});
|
|
192
|
+
|
|
193
|
+
// Handle button[type="button"] with data-confirm inside forms
|
|
194
|
+
$('button[data-confirm][type="button"]').on("click.confirm", (ev) => {
|
|
195
|
+
handleConfirm(ev, ev.currentTarget);
|
|
196
|
+
});
|
|
190
197
|
});
|
|
191
198
|
};
|
|
192
199
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
|
|
3
|
+
jest.mock("src/decidim/refactor/moved/icon", () => () => "<svg></svg>");
|
|
4
|
+
|
|
5
|
+
describe("Confirm dialog for button[type='button']", () => {
|
|
6
|
+
let mockRails = null;
|
|
7
|
+
let mockDecidim = null;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
document.body.innerHTML = `
|
|
12
|
+
<div id="confirm-modal" style="display: none;">
|
|
13
|
+
<div data-dialog-title></div>
|
|
14
|
+
<div class="confirm-modal-icon"></div>
|
|
15
|
+
<div data-confirm-modal-content></div>
|
|
16
|
+
<button data-confirm-ok>Confirm</button>
|
|
17
|
+
<button data-confirm-cancel>Cancel</button>
|
|
18
|
+
</div>
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
mockRails = {
|
|
22
|
+
linkClickSelector: "a[data-confirm]",
|
|
23
|
+
buttonClickSelector: "button[data-confirm]:not([form])",
|
|
24
|
+
formInputClickSelector: 'form button[type="submit"], form button:not([type])',
|
|
25
|
+
inputChangeSelector: "input[data-confirm], select[data-confirm]",
|
|
26
|
+
formSubmitSelector: "form[data-confirm]",
|
|
27
|
+
stopEverything: jest.fn(),
|
|
28
|
+
fire: jest.fn((el, event) => {
|
|
29
|
+
const evt = new CustomEvent(event);
|
|
30
|
+
el.dispatchEvent(evt);
|
|
31
|
+
return true;
|
|
32
|
+
}),
|
|
33
|
+
matches: function(element, selector) {
|
|
34
|
+
if (element instanceof Element) {
|
|
35
|
+
return element.matches(selector);
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
mockDecidim = {
|
|
42
|
+
currentDialogs: {
|
|
43
|
+
"confirm-modal": {
|
|
44
|
+
open: jest.fn(),
|
|
45
|
+
close: jest.fn()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
window.Rails = mockRails;
|
|
51
|
+
window.Decidim = mockDecidim;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
document.body.innerHTML = "";
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("selector matching for button[type='button'] with data-confirm", () => {
|
|
59
|
+
it("matches button[data-confirm][type='button'] selector", () => {
|
|
60
|
+
const button = document.createElement("button");
|
|
61
|
+
button.type = "button";
|
|
62
|
+
button.setAttribute("data-confirm", "Are you sure?");
|
|
63
|
+
|
|
64
|
+
expect(button.matches('button[data-confirm][type="button"]')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("matches form button[data-confirm] selector", () => {
|
|
68
|
+
const form = document.createElement("form");
|
|
69
|
+
const button = document.createElement("button");
|
|
70
|
+
button.setAttribute("data-confirm", "Are you sure?");
|
|
71
|
+
form.appendChild(button);
|
|
72
|
+
|
|
73
|
+
expect(button.matches("form button[data-confirm]")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not match regular button without data-confirm", () => {
|
|
77
|
+
const button = document.createElement("button");
|
|
78
|
+
button.type = "button";
|
|
79
|
+
|
|
80
|
+
expect(button.matches('button[data-confirm][type="button"]')).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not match button[type='submit'] with the type='button' selector", () => {
|
|
84
|
+
const button = document.createElement("button");
|
|
85
|
+
button.type = "submit";
|
|
86
|
+
button.setAttribute("data-confirm", "Are you sure?");
|
|
87
|
+
|
|
88
|
+
expect(button.matches('button[data-confirm][type="button"]')).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("matches button[type='button'] inside form", () => {
|
|
92
|
+
document.body.innerHTML = `
|
|
93
|
+
<form>
|
|
94
|
+
<button type="button" data-confirm="Test message">Click me</button>
|
|
95
|
+
</form>
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
const button = document.querySelector('button[type="button"]');
|
|
99
|
+
expect(button.matches('button[data-confirm][type="button"]')).toBe(true);
|
|
100
|
+
expect(button.matches("form button[data-confirm]")).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("does not match button[type='button'] without data-confirm inside form", () => {
|
|
104
|
+
document.body.innerHTML = `
|
|
105
|
+
<form>
|
|
106
|
+
<button type="button">Click me</button>
|
|
107
|
+
</form>
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const button = document.querySelector('button[type="button"]');
|
|
111
|
+
expect(button.matches('button[data-confirm][type="button"]')).toBe(false);
|
|
112
|
+
expect(button.matches("form button[data-confirm]")).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("initializeConfirm - selectors registration", () => {
|
|
117
|
+
it("adds click event listener with proper selectors including button[type='button'] support", async () => {
|
|
118
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
119
|
+
|
|
120
|
+
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
|
|
121
|
+
|
|
122
|
+
initializeConfirm();
|
|
123
|
+
|
|
124
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith("click", expect.any(Function));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("adds change event listener for input change selector", async () => {
|
|
128
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
129
|
+
|
|
130
|
+
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
|
|
131
|
+
|
|
132
|
+
initializeConfirm();
|
|
133
|
+
|
|
134
|
+
const changeHandlerCalls = addEventListenerSpy.mock.calls.filter(
|
|
135
|
+
(call) => call[0] === "change"
|
|
136
|
+
);
|
|
137
|
+
expect(changeHandlerCalls.length).toBeGreaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("adds submit event listener for form submit selector", async () => {
|
|
141
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
142
|
+
|
|
143
|
+
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
|
|
144
|
+
|
|
145
|
+
initializeConfirm();
|
|
146
|
+
|
|
147
|
+
const submitHandlerCalls = addEventListenerSpy.mock.calls.filter(
|
|
148
|
+
(call) => call[0] === "submit"
|
|
149
|
+
);
|
|
150
|
+
expect(submitHandlerCalls.length).toBeGreaterThan(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("adds turbo:load event listener for Foundation Abide compatibility", async () => {
|
|
154
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
155
|
+
|
|
156
|
+
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
|
|
157
|
+
|
|
158
|
+
initializeConfirm();
|
|
159
|
+
|
|
160
|
+
const turboLoadCalls = addEventListenerSpy.mock.calls.filter(
|
|
161
|
+
(call) => call[0] === "turbo:load"
|
|
162
|
+
);
|
|
163
|
+
expect(turboLoadCalls.length).toBeGreaterThan(0);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("handleDocumentEvent with button[type='button'] support", () => {
|
|
168
|
+
it("handles click on button[type='button'] with data-confirm and form attribute", async () => {
|
|
169
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
170
|
+
|
|
171
|
+
document.body.innerHTML = `
|
|
172
|
+
<form id="my-form">
|
|
173
|
+
<button type="button" form="my-form" data-confirm="Are you sure?">Click me</button>
|
|
174
|
+
</form>
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
const button = document.querySelector('button[type="button"]');
|
|
178
|
+
const openSpy = jest.spyOn(mockDecidim.currentDialogs["confirm-modal"], "open");
|
|
179
|
+
|
|
180
|
+
initializeConfirm();
|
|
181
|
+
|
|
182
|
+
button.click();
|
|
183
|
+
|
|
184
|
+
expect(openSpy).toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("handles click on button[type='button'] with data-confirm outside form", async () => {
|
|
188
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
189
|
+
|
|
190
|
+
document.body.innerHTML = `
|
|
191
|
+
<button type="button" data-confirm="Are you sure?">Click me</button>
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
const button = document.querySelector('button[type="button"]');
|
|
195
|
+
const openSpy = jest.spyOn(mockDecidim.currentDialogs["confirm-modal"], "open");
|
|
196
|
+
|
|
197
|
+
initializeConfirm();
|
|
198
|
+
|
|
199
|
+
button.click();
|
|
200
|
+
|
|
201
|
+
expect(openSpy).toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("does not trigger confirm for button without data-confirm attribute", async () => {
|
|
205
|
+
const { initializeConfirm } = await import("src/decidim/confirm.js");
|
|
206
|
+
|
|
207
|
+
document.body.innerHTML = `
|
|
208
|
+
<form>
|
|
209
|
+
<button type="button">Click me</button>
|
|
210
|
+
</form>
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
const button = document.querySelector('button[type="button"]');
|
|
214
|
+
const openSpy = jest.spyOn(mockDecidim.currentDialogs["confirm-modal"], "open");
|
|
215
|
+
|
|
216
|
+
initializeConfirm();
|
|
217
|
+
|
|
218
|
+
button.click();
|
|
219
|
+
|
|
220
|
+
expect(openSpy).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/* dummy end */
|
|
@@ -72,6 +72,19 @@ describe("LanguageChangeController", () => {
|
|
|
72
72
|
expect(removeSpy).toHaveBeenCalledWith("change", controller.handleChange);
|
|
73
73
|
removeSpy.mockRestore();
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
it("activates the selected option's panel on connect", () => {
|
|
77
|
+
const options = selectElement.querySelectorAll("option");
|
|
78
|
+
options[1].selected = true;
|
|
79
|
+
|
|
80
|
+
controller.disconnect();
|
|
81
|
+
controller.connect();
|
|
82
|
+
|
|
83
|
+
expect(panel0.classList.contains("is-active")).toBe(false);
|
|
84
|
+
expect(panel0.ariaHidden).toBe("true");
|
|
85
|
+
expect(panel1.classList.contains("is-active")).toBe(true);
|
|
86
|
+
expect(panel1.ariaHidden).toBe("false");
|
|
87
|
+
});
|
|
75
88
|
});
|
|
76
89
|
|
|
77
90
|
describe("handleChange", () => {
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
// Utility helper functions for the date and time picker functionality
|
|
2
2
|
|
|
3
|
+
export const adjustPickerPosition = (input, datePickerContainer, selector) => {
|
|
4
|
+
const parent = input.closest(selector);
|
|
5
|
+
|
|
6
|
+
if (getComputedStyle(parent).position === "static") {
|
|
7
|
+
parent.style.position = "relative";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const rect = input.getBoundingClientRect();
|
|
11
|
+
const calendarHeight = datePickerContainer.offsetHeight;
|
|
12
|
+
const spaceAbove = rect.top;
|
|
13
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
14
|
+
const openBelow = spaceBelow >= calendarHeight || spaceBelow >= spaceAbove;
|
|
15
|
+
|
|
16
|
+
if (openBelow) {
|
|
17
|
+
// Open below
|
|
18
|
+
datePickerContainer.style.top = `${input.offsetHeight}px`;
|
|
19
|
+
datePickerContainer.style.bottom = "";
|
|
20
|
+
} else {
|
|
21
|
+
// Open above
|
|
22
|
+
datePickerContainer.style.top = "";
|
|
23
|
+
datePickerContainer.style.bottom = `${input.offsetHeight}px`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
datePickerContainer.style.right = "0px";
|
|
27
|
+
};
|
|
28
|
+
|
|
3
29
|
export const setHour = (value, format) => {
|
|
4
30
|
const hour = value.split(":")[0];
|
|
5
31
|
if (format === 12) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable require-jsdoc */
|
|
2
2
|
import icon from "src/decidim/refactor/moved/icon"
|
|
3
|
-
import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos } from "src/decidim/datepicker/datepicker_functions"
|
|
3
|
+
import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos, adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions"
|
|
4
4
|
import { dateKeyDownListener, dateBeforeInputListener } from "src/decidim/datepicker/datepicker_listeners"
|
|
5
5
|
import { getDictionary } from "src/decidim/refactor/moved/i18n"
|
|
6
6
|
|
|
@@ -136,6 +136,7 @@ export default function generateDatePicker(input, row, formats) {
|
|
|
136
136
|
};
|
|
137
137
|
pickedDate = null;
|
|
138
138
|
datePickerContainer.style.display = "block";
|
|
139
|
+
adjustPickerPosition(date, datePickerContainer, ".datepicker__date-column");
|
|
139
140
|
|
|
140
141
|
document.addEventListener("click", datePickerDisplay);
|
|
141
142
|
|