decidim-core 0.30.6 → 0.30.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/cells/decidim/content_blocks/highlighted_elements_with_cell_for_list_cell.rb +5 -1
- data/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb +2 -2
- 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/statistic/show.erb +4 -4
- data/app/cells/decidim/upload_modal/files.erb +9 -5
- data/app/cells/decidim/upload_modal_cell.rb +14 -1
- data/app/commands/decidim/multiple_attachments_methods.rb +20 -3
- data/app/helpers/decidim/mailer_helper.rb +36 -0
- data/app/helpers/decidim/menu_helper.rb +2 -1
- data/app/helpers/decidim/newsletters_helper.rb +4 -22
- 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/mailers/decidim/application_mailer.rb +4 -0
- 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/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 +9 -1
- data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +234 -0
- data/app/packs/src/decidim/direct_uploads/upload_field.js +1 -1
- data/app/packs/src/decidim/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/packs/stylesheets/decidim/_modal_update.scss +1 -1
- data/app/presenters/decidim/menu_item_presenter.rb +7 -1
- data/app/views/decidim/devise/invitations/edit.html.erb +3 -3
- data/app/views/decidim/devise/registrations/new.html.erb +1 -0
- data/app/views/decidim/devise/shared/_tos_fields.html.erb +3 -3
- data/app/views/decidim/notification_mailer/event_received.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 +8 -8
- data/config/locales/de.yml +17 -8
- data/config/locales/el.yml +0 -2
- data/config/locales/en.yml +5 -4
- data/config/locales/es-MX.yml +8 -7
- data/config/locales/es-PY.yml +8 -7
- data/config/locales/es.yml +10 -9
- data/config/locales/eu.yml +8 -6
- data/config/locales/fi-plain.yml +10 -4
- data/config/locales/fi.yml +11 -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 +5 -9
- 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/content_parsers/blob_parser.rb +2 -2
- data/lib/decidim/content_renderers/blob_renderer.rb +2 -2
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/form_builder.rb +25 -1
- data/lib/decidim/maintenance/taxonomy_importer.rb +1 -1
- data/lib/decidim/searchable.rb +4 -4
- metadata +10 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b31c19b0f1263634559541d974f68dba477328f79a493ab4188eb3d8cb42a0eb
|
|
4
|
+
data.tar.gz: a9d06e9ea81dfe61b62c9f99961a4284af380a5343a12ce5c7ca7b055f4041b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c821103cefa1cfc8c3688fe69dce51bd2e6c94a448bee4fdc05ef5abd08e522cc4785e0827217b464b9b4a601cb52daa8539b21b0bf98f39e69fdff1b5824a5
|
|
7
|
+
data.tar.gz: 9c27f4b8ce372ac3295e719a3985b7276ec91d90303b4e627545d4571012936d3d4f1694c6e81e4df95fac154bbb2aaa3889130b9ffcfc609a1b2a40a699a99f
|
|
@@ -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
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
<% metadata_valued_items.each do |item| %>
|
|
3
3
|
<div class="participatory-space__metadata-item">
|
|
4
4
|
<div class="participatory-space__metadata-item-title">
|
|
5
|
-
<
|
|
5
|
+
<p><%= item[:title] %></p>
|
|
6
6
|
</div>
|
|
7
|
-
<
|
|
7
|
+
<p><%= item[:value] %></p>
|
|
8
8
|
</div>
|
|
9
9
|
<% end %>
|
|
10
10
|
</div>
|
|
@@ -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
|
<%= form_for report_form, builder:, url: report_path, method: :post, html: { id: nil } 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
|
<%= form_for report_form, builder:, url: report_path, method: :post, html: { id: nil } 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,8 +1,8 @@
|
|
|
1
1
|
<div class="statistic <%= stat_dom_class %>" data-statistic>
|
|
2
|
-
<
|
|
2
|
+
<p class="statistic__title" title="<%= stat_title %>">
|
|
3
3
|
<%= stat_title %>
|
|
4
|
-
</
|
|
5
|
-
<
|
|
4
|
+
</p>
|
|
5
|
+
<p class="statistic__number">
|
|
6
6
|
<%= stat_number %>
|
|
7
|
-
</
|
|
7
|
+
</p>
|
|
8
8
|
</div>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<div class="upload-modal__files-container upload-container-for-<%= attribute %> <%= with_title %>">
|
|
2
2
|
<div>
|
|
3
|
-
<%= label %>
|
|
3
|
+
<%= options[:paragraph] == true ? paragraph : label %>
|
|
4
4
|
|
|
5
5
|
<% if options[:help_text].present? %>
|
|
6
|
-
<
|
|
6
|
+
<p class="help-text"><%= options[:help_text] %></p>
|
|
7
7
|
<% end %>
|
|
8
8
|
|
|
9
9
|
<%# NOTE: this block is about wrapping a default image for the avatar with the new styles,
|
|
@@ -33,16 +33,20 @@
|
|
|
33
33
|
<% end %>
|
|
34
34
|
|
|
35
35
|
<% if has_title? %>
|
|
36
|
-
<
|
|
36
|
+
<p><%= title_for(attachment) %></p>
|
|
37
37
|
<% else %>
|
|
38
38
|
<% if attachment_blob&.image? %>
|
|
39
|
-
<
|
|
39
|
+
<p><%= title_for(attachment) %></p>
|
|
40
40
|
<% else %>
|
|
41
41
|
<%= link_to title_for(attachment), file_attachment_path(attachment), class: "w-full break-all mb-2" %>
|
|
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 %>
|
|
@@ -30,6 +30,10 @@ module Decidim
|
|
|
30
30
|
form.send(:custom_label, attribute, options[:label], { required: required?, for: nil })
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def paragraph
|
|
34
|
+
form.send(:custom_paragraph, attribute, options[:label], { required: required? })
|
|
35
|
+
end
|
|
36
|
+
|
|
33
37
|
def button_label
|
|
34
38
|
return button_edit_label if attachments.count.positive?
|
|
35
39
|
|
|
@@ -124,7 +128,16 @@ module Decidim
|
|
|
124
128
|
@attachments = begin
|
|
125
129
|
attachments = options[:attachments] || form.object.send(attribute)
|
|
126
130
|
attachments = Array(attachments).compact_blank
|
|
127
|
-
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
|
|
128
141
|
end
|
|
129
142
|
end
|
|
130
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
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
# Helper that provides methods to render order selector and links
|
|
5
|
+
module MailerHelper
|
|
6
|
+
# Transforms relative image URLs in HTML content to absolute URLs using the provided host.
|
|
7
|
+
# This is used in emails (newsletters and notifications) to ensure images display correctly
|
|
8
|
+
# in email clients.
|
|
9
|
+
#
|
|
10
|
+
# @param content [String] - HTML content with img tags
|
|
11
|
+
# @param host [String] - the Decidim::Organization host to use for the root URL
|
|
12
|
+
#
|
|
13
|
+
# @return [String] - the content with transformed image URLs
|
|
14
|
+
def decidim_transform_image_urls(content, host)
|
|
15
|
+
return content if host.blank? || content.blank?
|
|
16
|
+
|
|
17
|
+
root_url = if Rails.application.secrets.dig(:storage, :cdn_host).present?
|
|
18
|
+
Rails.application.secrets.dig(:storage, :cdn_host).chomp("/")
|
|
19
|
+
else
|
|
20
|
+
Decidim::EngineRouter.new("decidim", {}).root_url(host:).chomp("/")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
content.gsub(/src\s*=\s*(['"])([^'"]*)\1/) do
|
|
24
|
+
quote = Regexp.last_match(1)
|
|
25
|
+
src_value = Regexp.last_match(2)
|
|
26
|
+
|
|
27
|
+
if src_value.blank? || src_value.start_with?("http://", "https://", "data:", "//", "cid:")
|
|
28
|
+
%(src=#{quote}#{src_value}#{quote})
|
|
29
|
+
else
|
|
30
|
+
normalized_src = src_value.start_with?("/") ? src_value : "/#{src_value}"
|
|
31
|
+
%(src=#{quote}#{root_url}#{normalized_src}#{quote})
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -57,7 +57,8 @@ module Decidim
|
|
|
57
57
|
self,
|
|
58
58
|
element_class: "font-semibold underline",
|
|
59
59
|
active_class: "is-active",
|
|
60
|
-
|
|
60
|
+
role: false,
|
|
61
|
+
container_options: { class: "space-y-4 break-inside-avoid" },
|
|
61
62
|
label: t("layouts.decidim.footer.decidim_title")
|
|
62
63
|
)
|
|
63
64
|
end
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
module Decidim
|
|
4
4
|
# Helper that provides methods to render links with utm codes, and replaced name
|
|
5
5
|
module NewslettersHelper
|
|
6
|
+
include Decidim::SanitizeHelper
|
|
7
|
+
include Decidim::MailerHelper
|
|
8
|
+
|
|
6
9
|
# If the newsletter body there are some links and the Decidim.track_newsletter_links = true
|
|
7
10
|
# it will be replaced with the utm_codes method described below.
|
|
8
11
|
# for example transform "https://es.lipsum.com/" to "https://es.lipsum.com/?utm_source=localhost&utm_campaign=newsletter_11"
|
|
@@ -19,7 +22,7 @@ module Decidim
|
|
|
19
22
|
|
|
20
23
|
content = interpret_name(content, user)
|
|
21
24
|
content = track_newsletter_links(content, id, host)
|
|
22
|
-
|
|
25
|
+
decidim_transform_image_urls(content, host)
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
# this method is used to generate the root link on mail with the utm_codes
|
|
@@ -67,27 +70,6 @@ module Decidim
|
|
|
67
70
|
content.gsub("%{name}", user.name)
|
|
68
71
|
end
|
|
69
72
|
|
|
70
|
-
# Find each img HTML tag with relative path in src attribute
|
|
71
|
-
# For each URL, prepends the decidim.root_url
|
|
72
|
-
# If host is not defined it returns full content
|
|
73
|
-
#
|
|
74
|
-
# @param content [String] - the string to convert
|
|
75
|
-
# @param host [String] - the Decidim::Organization host to replace
|
|
76
|
-
#
|
|
77
|
-
# @return [String] - the content converted
|
|
78
|
-
#
|
|
79
|
-
def transform_image_urls(content, host)
|
|
80
|
-
return content if host.blank?
|
|
81
|
-
|
|
82
|
-
content.scan(/src\s*=\s*"([^"]*)"/).each do |src|
|
|
83
|
-
root_url = decidim.root_url(host:)[0..-2]
|
|
84
|
-
src_replaced = "#{root_url}#{src.first}"
|
|
85
|
-
content = content.gsub(/src\s*=\s*"([^"]*#{src.first})"/, %(src="#{src_replaced}"))
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
content
|
|
89
|
-
end
|
|
90
|
-
|
|
91
73
|
# Add tracking query params to each links
|
|
92
74
|
#
|
|
93
75
|
# @param content [String] - the string to convert
|
|
@@ -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
|
|
@@ -7,9 +7,13 @@ module Decidim
|
|
|
7
7
|
include LocalisedMailer
|
|
8
8
|
include MultitenantAssetHost
|
|
9
9
|
include Decidim::SanitizeHelper
|
|
10
|
+
include Decidim::MailerHelper
|
|
10
11
|
include Decidim::OrganizationHelper
|
|
11
12
|
helper_method :organization_name, :decidim_escape_translated, :decidim_sanitize_translated, :translated_attribute, :decidim_sanitize, :decidim_sanitize_newsletter
|
|
12
13
|
|
|
14
|
+
helper Decidim::SanitizeHelper
|
|
15
|
+
helper Decidim::MailerHelper
|
|
16
|
+
|
|
13
17
|
after_action :set_smtp
|
|
14
18
|
after_action :set_from
|
|
15
19
|
|
|
@@ -124,6 +124,13 @@ const createDropdown = (component) => {
|
|
|
124
124
|
* @return {void}
|
|
125
125
|
*/
|
|
126
126
|
const createDialog = (component) => {
|
|
127
|
+
const getFocusableElements = (container) => {
|
|
128
|
+
const selectors = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex='-1'])";
|
|
129
|
+
return Array.from(container.querySelectorAll(selectors)).filter(
|
|
130
|
+
(el) => el.offsetParent !== null
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
127
134
|
const {
|
|
128
135
|
dataset: { dialog, ...attrs }
|
|
129
136
|
} = component;
|
|
@@ -145,11 +152,33 @@ const createDialog = (component) => {
|
|
|
145
152
|
backdropSelector: `[data-dialog="${dialog}"]`,
|
|
146
153
|
enableAutoFocus: false,
|
|
147
154
|
onOpen: (params, trigger) => {
|
|
155
|
+
const keyHandler = (event) => {
|
|
156
|
+
if (event.key !== "Tab") {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const focusable = getFocusableElements(params);
|
|
160
|
+
if (focusable.length === 0) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (event.shiftKey && document.activeElement === focusable[0]) {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
focusable[focusable.length - 1].focus({ preventScroll: true });
|
|
166
|
+
} else if (!event.shiftKey && document.activeElement === focusable[focusable.length - 1]) {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
focusable[0].focus({ preventScroll: true });
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
params._focusTrapHandler = keyHandler;
|
|
172
|
+
params.addEventListener("keydown", keyHandler);
|
|
148
173
|
setFocusOnTitle(params);
|
|
149
174
|
window.focusGuard.trap(params, trigger);
|
|
150
175
|
params.dispatchEvent(new CustomEvent("open.dialog"));
|
|
151
176
|
},
|
|
152
177
|
onClose: (params) => {
|
|
178
|
+
if (params._focusTrapHandler) {
|
|
179
|
+
params.removeEventListener("keydown", params._focusTrapHandler);
|
|
180
|
+
Reflect.deleteProperty(params, "_focusTrapHandler");
|
|
181
|
+
}
|
|
153
182
|
window.focusGuard.disable();
|
|
154
183
|
params.dispatchEvent(new CustomEvent("close.dialog"));
|
|
155
184
|
},
|
|
@@ -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
|
+
});
|
|
@@ -162,7 +162,9 @@ export const initializeConfirm = () => {
|
|
|
162
162
|
return handleDocumentEvent(ev, [
|
|
163
163
|
Rails.linkClickSelector,
|
|
164
164
|
Rails.buttonClickSelector,
|
|
165
|
-
Rails.formInputClickSelector
|
|
165
|
+
Rails.formInputClickSelector,
|
|
166
|
+
'button[data-confirm][type="button"]',
|
|
167
|
+
"form button[data-confirm]"
|
|
166
168
|
]);
|
|
167
169
|
});
|
|
168
170
|
document.addEventListener("change", (ev) => {
|
|
@@ -180,6 +182,11 @@ export const initializeConfirm = () => {
|
|
|
180
182
|
$(Rails.formInputClickSelector).on("click.confirm", (ev) => {
|
|
181
183
|
handleConfirm(ev, getMatchingEventTarget(ev, Rails.formInputClickSelector));
|
|
182
184
|
});
|
|
185
|
+
|
|
186
|
+
// Handle button[type="button"] with data-confirm inside forms
|
|
187
|
+
$('button[data-confirm][type="button"]').on("click.confirm", (ev) => {
|
|
188
|
+
handleConfirm(ev, ev.currentTarget);
|
|
189
|
+
});
|
|
183
190
|
});
|
|
184
191
|
};
|
|
185
192
|
|