decidim-core 0.29.3 → 0.29.5

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.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/activity/show.erb +6 -6
  3. data/app/cells/decidim/address/show.erb +3 -3
  4. data/app/cells/decidim/author/show.erb +2 -4
  5. data/app/cells/decidim/content_blocks/participatory_space_extra_data/extra_data.erb +2 -2
  6. data/app/cells/decidim/content_blocks/participatory_space_main_data/title.erb +11 -2
  7. data/app/cells/decidim/footer_topics/show.erb +2 -2
  8. data/app/cells/decidim/group_admins/show.erb +3 -1
  9. data/app/cells/decidim/group_members/show.erb +6 -2
  10. data/app/cells/decidim/images_panel/show.erb +5 -2
  11. data/app/cells/decidim/participatory_space_dropdown_metadata/metadata.erb +4 -4
  12. data/app/cells/decidim/report_button/flag_modal.erb +11 -9
  13. data/app/cells/decidim/report_user_button/flag_modal.erb +11 -10
  14. data/app/cells/decidim/upload_modal/files.erb +4 -4
  15. data/app/cells/decidim/upload_modal_cell.rb +5 -3
  16. data/app/commands/decidim/amendable/accept.rb +2 -1
  17. data/app/commands/decidim/create_report.rb +5 -1
  18. data/app/commands/decidim/invite_user.rb +1 -1
  19. data/app/controllers/concerns/decidim/headers/browser_feature_permissions.rb +50 -0
  20. data/app/controllers/concerns/decidim/participatory_space_context.rb +4 -1
  21. data/app/controllers/decidim/amendments_controller.rb +3 -3
  22. data/app/controllers/decidim/application_controller.rb +1 -0
  23. data/app/controllers/decidim/reports_controller.rb +6 -1
  24. data/app/forms/decidim/omniauth_registration_form.rb +1 -1
  25. data/app/forms/decidim/registration_form.rb +1 -1
  26. data/app/helpers/decidim/amendments_helper.rb +2 -1
  27. data/app/helpers/decidim/filters_helper.rb +25 -0
  28. data/app/helpers/decidim/layout_helper.rb +6 -0
  29. data/app/helpers/decidim/menu_helper.rb +2 -2
  30. data/app/helpers/decidim/paginate_helper.rb +1 -1
  31. data/app/helpers/decidim/tooltip_helper.rb +4 -1
  32. data/app/mailers/decidim/notifications_digest_mailer.rb +7 -1
  33. data/app/mailers/decidim/reported_mailer.rb +17 -2
  34. data/app/packs/images/decidim/default-avatar.svg +1 -1
  35. data/app/packs/src/decidim/callout.js +13 -8
  36. data/app/packs/src/decidim/confirm.js +79 -59
  37. data/app/packs/src/decidim/datepicker/generate_datepicker.js +2 -0
  38. data/app/packs/src/decidim/datepicker/generate_timepicker.js +2 -0
  39. data/app/packs/src/decidim/direct_uploads/upload_field.js +3 -4
  40. data/app/packs/src/decidim/direct_uploads/upload_modal.js +8 -9
  41. data/app/packs/src/decidim/dropdown_menu.js +18 -0
  42. data/app/packs/src/decidim/editor/common/suggestion.js +11 -1
  43. data/app/packs/src/decidim/form_remote.js +1 -1
  44. data/app/packs/src/decidim/impersonation.js +1 -1
  45. data/app/packs/src/decidim/index.js +5 -1
  46. data/app/packs/src/decidim/input_character_counter.js +1 -1
  47. data/app/packs/src/decidim/session_timeouter.js +1 -1
  48. data/app/packs/src/decidim/utilities/dom.js +148 -0
  49. data/app/packs/stylesheets/decidim/_activity.scss +4 -4
  50. data/app/packs/stylesheets/decidim/_cards.scss +4 -0
  51. data/app/packs/stylesheets/decidim/_filters.scss +1 -1
  52. data/app/packs/stylesheets/decidim/_header.scss +64 -37
  53. data/app/packs/stylesheets/decidim/_layout.scss +2 -2
  54. data/app/packs/stylesheets/decidim/_modal.scss +1 -5
  55. data/app/packs/stylesheets/decidim/_modal_update.scss +5 -1
  56. data/app/permissions/decidim/default_permissions.rb +2 -0
  57. data/app/permissions/decidim/permissions.rb +13 -1
  58. data/app/presenters/decidim/notification_to_mailer_presenter.rb +7 -3
  59. data/app/queries/decidim/last_activity.rb +25 -0
  60. data/app/views/decidim/errors/internal_server_error.html.erb +1 -1
  61. data/app/views/decidim/errors/not_found.html.erb +1 -1
  62. data/app/views/decidim/messaging/conversations/_reply_form.html.erb +1 -2
  63. data/app/views/decidim/messaging/conversations/_start.html.erb +1 -1
  64. data/app/views/decidim/newsletters/unsubscribe.html.erb +16 -4
  65. data/app/views/decidim/reported_mailer/hidden_manually.html.erb +25 -0
  66. data/app/views/decidim/searches/_filters.html.erb +48 -13
  67. data/app/views/decidim/shared/_component_announcement.html.erb +1 -1
  68. data/app/views/decidim/shared/_confirm_modal.html.erb +3 -5
  69. data/app/views/decidim/shared/_filters.html.erb +6 -4
  70. data/app/views/decidim/shared/_results_per_page.html.erb +1 -1
  71. data/app/views/kaminari/decidim/_page.html.erb +1 -1
  72. data/app/views/kaminari/decidim/_paginator.html.erb +1 -1
  73. data/app/views/layouts/decidim/_js_configuration.html.erb +1 -0
  74. data/app/views/layouts/decidim/_logo.html.erb +2 -2
  75. data/app/views/layouts/decidim/footer/_main.html.erb +1 -1
  76. data/app/views/layouts/decidim/footer/_main_intro.html.erb +1 -1
  77. data/app/views/layouts/decidim/footer/_mini.html.erb +2 -2
  78. data/app/views/layouts/decidim/header/_main.html.erb +2 -2
  79. data/app/views/layouts/decidim/header/_main_links_desktop.html.erb +6 -0
  80. data/app/views/layouts/decidim/header/_main_links_dropdown.html.erb +2 -0
  81. data/app/views/layouts/decidim/header/_main_links_mobile_account.html.erb +1 -1
  82. data/app/views/layouts/decidim/header/_menu_breadcrumb_main_dropdown_desktop.html.erb +5 -11
  83. data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +5 -5
  84. data/app/views/layouts/decidim/header/_mobile_language_choose.html.erb +1 -1
  85. data/config/locales/ar.yml +0 -9
  86. data/config/locales/bg-BG.yml +2 -2
  87. data/config/locales/bg.yml +0 -11
  88. data/config/locales/ca-IT.yml +41 -6
  89. data/config/locales/ca.yml +41 -6
  90. data/config/locales/cs.yml +33 -10
  91. data/config/locales/de.yml +63 -25
  92. data/config/locales/el.yml +0 -10
  93. data/config/locales/en.yml +41 -6
  94. data/config/locales/es-MX.yml +42 -7
  95. data/config/locales/es-PY.yml +42 -7
  96. data/config/locales/es.yml +41 -6
  97. data/config/locales/eu.yml +79 -43
  98. data/config/locales/fi-plain.yml +55 -8
  99. data/config/locales/fi.yml +56 -9
  100. data/config/locales/fr-CA.yml +43 -9
  101. data/config/locales/fr-LU.yml +3 -3
  102. data/config/locales/fr.yml +43 -9
  103. data/config/locales/gl.yml +0 -8
  104. data/config/locales/hu.yml +0 -11
  105. data/config/locales/id-ID.yml +0 -8
  106. data/config/locales/is-IS.yml +0 -6
  107. data/config/locales/it.yml +124 -8
  108. data/config/locales/ja.yml +45 -11
  109. data/config/locales/lb-LU.yml +2 -2
  110. data/config/locales/lb.yml +0 -8
  111. data/config/locales/lt.yml +0 -11
  112. data/config/locales/lv.yml +0 -8
  113. data/config/locales/nl.yml +0 -8
  114. data/config/locales/no.yml +0 -8
  115. data/config/locales/pl.yml +0 -11
  116. data/config/locales/pt-BR.yml +1 -10
  117. data/config/locales/pt.yml +0 -8
  118. data/config/locales/ro-RO.yml +0 -14
  119. data/config/locales/ru.yml +0 -8
  120. data/config/locales/sk-SK.yml +3 -3
  121. data/config/locales/sk.yml +2 -8
  122. data/config/locales/sv.yml +56 -16
  123. data/config/locales/tr-TR.yml +1 -8
  124. data/config/locales/uk.yml +0 -7
  125. data/config/locales/zh-CN.yml +0 -8
  126. data/config/locales/zh-TW.yml +0 -11
  127. data/db/migrate/20171212103803_create_unique_nicknames.rb +1 -1
  128. data/db/migrate/20180221101934_fix_nickname_index.rb +1 -1
  129. data/db/migrate/20180706104107_add_nickname_to_managed_users.rb +1 -1
  130. data/db/migrate/20181001124950_move_users_groups_to_users_table.rb +1 -1
  131. data/db/migrate/20190412131728_fix_user_names.rb +1 -1
  132. data/lib/decidim/assets/tailwind/tailwind.config.js.erb +2 -1
  133. data/lib/decidim/core/test/factories.rb +2 -2
  134. data/lib/decidim/core/test/shared_examples/announcements_examples.rb +4 -0
  135. data/lib/decidim/core/version.rb +1 -1
  136. data/lib/decidim/form_builder.rb +14 -0
  137. data/lib/decidim/nicknamizable.rb +6 -9
  138. data/lib/tasks/upgrade/decidim_fix_nickname_uniqueness.rake +1 -1
  139. metadata +11 -7
  140. /data/app/views/decidim/reported_mailer/{hide.html.erb → hidden_automatically.html.erb} +0 -0
@@ -4,13 +4,18 @@
4
4
  * reload without this.
5
5
  */
6
6
 
7
- $(() => {
8
- const $callout = $('.callout[role="alert"]:first');
9
- if ($callout.length > 0) {
10
- setTimeout(() => {
11
- // The content insertion is to try to hint some of the screen readers
12
- // that the alert content has changed and needs to be announced.
13
- $callout.attr("tabindex", "0").focus().html(`${$callout.html()} `);
14
- }, 500);
7
+ document.addEventListener("turbo:load", () => {
8
+ const callout = document.querySelector(".flash[role='alert']");
9
+ if (!callout) {
10
+ return;
15
11
  }
12
+
13
+ setTimeout(() => {
14
+ callout.setAttribute("tabindex", "0");
15
+ callout.focus();
16
+
17
+ // The content insertion is to try to hint some of the screen readers
18
+ // that the alert content has changed and needs to be announced.
19
+ callout.innerHTML += " ";
20
+ }, 500);
16
21
  });
@@ -5,12 +5,14 @@
5
5
  * it to gain control over the confirm events BEFORE rails-ujs is loaded.
6
6
  */
7
7
 
8
- import Rails from "@rails/ujs"
8
+ const { Rails } = window;
9
9
 
10
10
  class ConfirmDialog {
11
11
  constructor(sourceElement) {
12
12
  this.$modal = $("#confirm-modal");
13
- this.$source = sourceElement;
13
+ if (sourceElement) {
14
+ this.$source = $(sourceElement);
15
+ }
14
16
  this.$content = $("[data-confirm-modal-content]", this.$modal);
15
17
  this.$buttonConfirm = $("[data-confirm-ok]", this.$modal);
16
18
  this.$buttonCancel = $("[data-confirm-cancel]", this.$modal);
@@ -29,22 +31,37 @@ class ConfirmDialog {
29
31
  this.$buttonConfirm.on("click", (ev) => {
30
32
  ev.preventDefault();
31
33
 
32
- window.Decidim.currentDialogs["confirm-modal"].close()
33
- resolve(true);
34
- this.$source.focus();
34
+ this.close(() => resolve(true));
35
35
  });
36
36
 
37
37
  this.$buttonCancel.on("click", (ev) => {
38
38
  ev.preventDefault();
39
39
 
40
- window.Decidim.currentDialogs["confirm-modal"].close()
41
- resolve(false);
42
- this.$source.focus();
40
+ this.close(() => resolve(false));
43
41
  });
44
42
  });
45
43
  }
44
+
45
+ close(afterClose) {
46
+ window.Decidim.currentDialogs["confirm-modal"].close()
47
+ afterClose();
48
+ if (this.$source) {
49
+ this.$source.focus();
50
+ }
51
+ }
46
52
  }
47
53
 
54
+ const runConfirm = (message, sourceElement = null) => new Promise((resolve) => {
55
+ const dialog = new ConfirmDialog(sourceElement);
56
+ dialog.confirm(message).then((answer) => {
57
+ let completed = true;
58
+ if (sourceElement) {
59
+ completed = Rails.fire(sourceElement, "confirm:complete", [answer]);
60
+ }
61
+ resolve(answer && completed);
62
+ });
63
+ });
64
+
48
65
  // Override the default confirm dialog by Rails
49
66
  // See:
50
67
  // https://github.com/rails/rails/blob/fba1064153d8e2f4654df7762a7d3664b93e9fc8/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee
@@ -64,37 +81,35 @@ const allowAction = (ev, element) => {
64
81
  return false;
65
82
  }
66
83
 
67
- const dialog = new ConfirmDialog(
68
- $(element)
69
- );
70
- dialog.confirm(message).then((answer) => {
71
- const completed = Rails.fire(element, "confirm:complete", [answer]);
72
- if (answer && completed) {
73
- // Allow the event to propagate normally and re-dispatch it without
74
- // the confirm data attribute which the Rails internal method is
75
- // checking.
76
- $(element).data("confirm", null);
77
- $(element).removeAttr("data-confirm");
78
-
79
- // The submit button click events will not do anything if they are
80
- // dispatched as is. In these cases, just submit the underlying form.
81
- if (ev.type === "click" &&
82
- (
83
- $(element).is('button[type="submit"]') ||
84
- $(element).is('input[type="submit"]')
85
- )
86
- ) {
87
- $(element).parents("form").submit();
88
- } else {
89
- let origEv = ev.originalEvent || ev;
90
- let newEv = origEv;
91
- if (typeof Event === "function") {
92
- // Clone the event because otherwise some click events may not
93
- // work properly when re-dispatched.
94
- newEv = new origEv.constructor(origEv.type, origEv);
95
- }
96
- ev.target.dispatchEvent(newEv);
84
+ runConfirm(message, element).then((answer) => {
85
+ if (!answer) {
86
+ return;
87
+ }
88
+
89
+ // Allow the event to propagate normally and re-dispatch it without
90
+ // the confirm data attribute which the Rails internal method is
91
+ // checking.
92
+ $(element).data("confirm", null);
93
+ $(element).removeAttr("data-confirm");
94
+
95
+ // The submit button click events will not do anything if they are
96
+ // dispatched as is. In these cases, just submit the underlying form.
97
+ if (ev.type === "click" &&
98
+ (
99
+ $(element).is('button[type="submit"]') ||
100
+ $(element).is('input[type="submit"]')
101
+ )
102
+ ) {
103
+ $(element).parents("form").submit();
104
+ } else {
105
+ let origEv = ev.originalEvent || ev;
106
+ let newEv = origEv;
107
+ if (typeof Event === "function") {
108
+ // Clone the event because otherwise some click events may not
109
+ // work properly when re-dispatched.
110
+ newEv = new origEv.constructor(origEv.type, origEv);
97
111
  }
112
+ ev.target.dispatchEvent(newEv);
98
113
  }
99
114
  });
100
115
 
@@ -129,26 +144,31 @@ const handleDocumentEvent = (ev, matchSelectors) => {
129
144
  });
130
145
  };
131
146
 
132
- document.addEventListener("click", (ev) => {
133
- return handleDocumentEvent(ev, [
134
- Rails.linkClickSelector,
135
- Rails.buttonClickSelector,
136
- Rails.formInputClickSelector
137
- ]);
138
- });
139
- document.addEventListener("change", (ev) => {
140
- return handleDocumentEvent(ev, [Rails.inputChangeSelector]);
141
- });
142
- document.addEventListener("submit", (ev) => {
143
- return handleDocumentEvent(ev, [Rails.formSubmitSelector]);
144
- });
147
+ // Note that this needs to be run **before** Rails.start()
148
+ export const initializeConfirm = () => {
149
+ document.addEventListener("click", (ev) => {
150
+ return handleDocumentEvent(ev, [
151
+ Rails.linkClickSelector,
152
+ Rails.buttonClickSelector,
153
+ Rails.formInputClickSelector
154
+ ]);
155
+ });
156
+ document.addEventListener("change", (ev) => {
157
+ return handleDocumentEvent(ev, [Rails.inputChangeSelector]);
158
+ });
159
+ document.addEventListener("submit", (ev) => {
160
+ return handleDocumentEvent(ev, [Rails.formSubmitSelector]);
161
+ });
145
162
 
146
- // This is needed for the confirm dialog to work with Foundation Abide.
147
- // Abide registers its own submit click listeners since Foundation 5.6.x
148
- // which will be handled before the document listeners above. This would
149
- // break the custom confirm functionality when used with Foundation Abide.
150
- document.addEventListener("DOMContentLoaded", function() {
151
- $(Rails.formInputClickSelector).on("click.confirm", (ev) => {
152
- handleConfirm(ev, getMatchingEventTarget(ev, Rails.formInputClickSelector));
163
+ // This is needed for the confirm dialog to work with Foundation Abide.
164
+ // Abide registers its own submit click listeners since Foundation 5.6.x
165
+ // which will be handled before the document listeners above. This would
166
+ // break the custom confirm functionality when used with Foundation Abide.
167
+ document.addEventListener("DOMContentLoaded", function() {
168
+ $(Rails.formInputClickSelector).on("click.confirm", (ev) => {
169
+ handleConfirm(ev, getMatchingEventTarget(ev, Rails.formInputClickSelector));
170
+ });
153
171
  });
154
- });
172
+ };
173
+
174
+ export default runConfirm;
@@ -13,11 +13,13 @@ export default function generateDatePicker(input, row, formats) {
13
13
  const date = document.createElement("input");
14
14
  date.setAttribute("id", `${input.id}_date`);
15
15
  date.setAttribute("type", "text");
16
+ date.setAttribute("aria-label", input.dataset.dateLabel);
16
17
 
17
18
  const calendar = document.createElement("button");
18
19
  calendar.innerHTML = icon("calendar-line");
19
20
  calendar.setAttribute("class", "datepicker__calendar-button");
20
21
  calendar.setAttribute("type", "button");
22
+ calendar.setAttribute("aria-label", input.dataset.buttonDateLabel);
21
23
 
22
24
  dateColumn.appendChild(date);
23
25
  dateColumn.appendChild(calendar);
@@ -13,11 +13,13 @@ export default function generateTimePicker(input, row, formats) {
13
13
  const time = document.createElement("input");
14
14
  time.setAttribute("id", `${input.id}_time`);
15
15
  time.setAttribute("type", "text");
16
+ time.setAttribute("aria-label", input.dataset.timeLabel);
16
17
 
17
18
  const clock = document.createElement("button");
18
19
  clock.innerHTML = icon("time-line")
19
20
  clock.setAttribute("class", "datepicker__clock-button");
20
21
  clock.setAttribute("type", "button");
22
+ clock.setAttribute("aria-label", input.dataset.buttonTimeLabel);
21
23
 
22
24
  timeColumn.appendChild(time);
23
25
  timeColumn.appendChild(clock);
@@ -1,5 +1,4 @@
1
1
  import UploadModal from "src/decidim/direct_uploads/upload_modal";
2
- import { truncateFilename } from "src/decidim/direct_uploads/upload_utility";
3
2
  import { escapeHtml, escapeQuotes } from "src/decidim/utilities/text";
4
3
 
5
4
  const updateModalTitle = (modal) => {
@@ -30,7 +29,7 @@ const updateActiveUploads = (modal) => {
30
29
  const [removeFiles, addFiles] = [modal.items.filter(({ removable }) => removable), modal.items.filter(({ removable }) => !removable)]
31
30
 
32
31
  addFiles.forEach((file, ix) => {
33
- let title = truncateFilename(file.name, 19)
32
+ let title = file.name
34
33
 
35
34
  let hidden = ""
36
35
  if (file.hiddenField) {
@@ -79,8 +78,8 @@ const updateActiveUploads = (modal) => {
79
78
 
80
79
  const template = `
81
80
  <div ${attachmentIdOrHiddenField} data-filename="${escapeQuotes(file.name)}" data-title="${escapeQuotes(title)}">
82
- ${(/image/).test(file.type) && `<div><img src="data:," alt="${escapeQuotes(file.name)}" /></div>` || ""}
83
- <span>${escapeHtml(title)} (${escapeHtml(truncateFilename(file.name))})</span>
81
+ ${(/image/).test(file.type) && "<div><img src=\"data:,\" role=\"presentation\" /></div>" || ""}
82
+ <span>${escapeHtml(title)}</span>
84
83
  ${hidden}
85
84
  </div>
86
85
  `
@@ -1,6 +1,5 @@
1
1
  import { Uploader } from "src/decidim/direct_uploads/uploader";
2
2
  import icon from "src/decidim/icon";
3
- import { truncateFilename } from "src/decidim/direct_uploads/upload_utility";
4
3
  import { escapeHtml, escapeQuotes } from "src/decidim/utilities/text";
5
4
 
6
5
  const STATUS = {
@@ -42,7 +41,7 @@ export default class UploadModal {
42
41
 
43
42
  this.emptyItems = this.modal.querySelector("[data-dropzone-no-items]");
44
43
  this.uploadItems = this.modal.querySelector("[data-dropzone-items]");
45
- this.input = this.dropZone.querySelector("input");
44
+ this.input = this.dropZone.querySelector("input[type=file]");
46
45
  this.items = []
47
46
 
48
47
  this.attachmentCounter = 0;
@@ -171,29 +170,29 @@ export default class UploadModal {
171
170
 
172
171
  createUploadItem(file, errors, opts = {}) {
173
172
  const okTemplate = `
174
- <img src="data:," alt="${escapeQuotes(file.name)}" />
175
- <span>${escapeHtml(truncateFilename(file.name))}</span>
173
+ <img src="data:,", role="presentation" />
174
+ <span class="upload-modal__span">${escapeHtml(file.name)}</span>
176
175
  `
177
176
 
178
177
  const errorTemplate = `
179
178
  <div>${icon("error-warning-line")}</div>
180
179
  <div>
181
- <span>${escapeHtml(truncateFilename(file.name))}</span>
180
+ <span class="upload-modal__span">${escapeHtml(file.name)}</span>
182
181
  <span>${this.locales.validation_error}</span>
183
182
  <ul>${errors.map((error) => `<li>${error}</li>`).join("\n")}</ul>
184
183
  </div>
185
184
  `
186
185
 
187
186
  const titleTemplate = `
188
- <img src="data:," alt="${escapeQuotes(file.name)}" />
187
+ <img src="data:," role="presentation" />
189
188
  <div>
190
189
  <div>
191
190
  <label>${this.locales.filename}</label>
192
- <span>${escapeHtml(truncateFilename(file.name))}</span>
191
+ <span class="upload-modal__span">${escapeHtml(file.name)}</span>
193
192
  </div>
194
193
  <div>
195
- <label>${this.locales.title}</label>
196
- <input class="sm" type="text" value="${escapeQuotes(opts.title || truncateFilename(file.name))}" />
194
+ <label for="${file.name}">${this.locales.title}</label>
195
+ <input class="sm" type="text" value="${escapeQuotes(opts.title || file.name)}" id="${file.name}" />
197
196
  </div>
198
197
  </div>
199
198
  `
@@ -0,0 +1,18 @@
1
+ // changes the value "menu" of role attribute set by a11y on div dropdown-menu-account and
2
+ // dropdown-menu-account-mobile which are inappropriate for accessibility
3
+ document.addEventListener("DOMContentLoaded", () => {
4
+ const dropdownDiv = document.querySelector("#dropdown-menu-account");
5
+ const dropdownMobileDiv = document.querySelector("#dropdown-menu-account-mobile");
6
+ if (dropdownDiv) {
7
+ setTimeout(() => {
8
+ dropdownDiv.setAttribute("role", "dialog")
9
+ dropdownMobileDiv.setAttribute("role", "dialog")
10
+ }, 300)
11
+ }
12
+ const triggerButtonMobile = document.querySelector("#dropdown-trigger-links-mobile");
13
+ if (triggerButtonMobile) {
14
+ triggerButtonMobile.addEventListener("click", () => {
15
+ dropdownMobileDiv.setAttribute("aria-modal", "true")
16
+ })
17
+ }
18
+ });
@@ -98,7 +98,17 @@ export const createSuggestionRenderer = (node, { itemConverter } = {}) => () =>
98
98
  suggestionItem.textContent = label;
99
99
  suggestion.append(suggestionItem);
100
100
 
101
- suggestionItem.addEventListener("click", () => selectItem(idx));
101
+ suggestionItem.addEventListener("click", (ev) => {
102
+ ev.preventDefault();
103
+
104
+ if (currentRange) {
105
+ // Increase the current range by 1 since the ENTER key would do the
106
+ // same when doing the selection through keyboard.
107
+ currentRange.to += 1;
108
+ }
109
+
110
+ selectItem(idx);
111
+ });
102
112
  });
103
113
  }
104
114
 
@@ -1,4 +1,4 @@
1
- import Rails from "@rails/ujs";
1
+ const { Rails } = window;
2
2
 
3
3
  // Make the remote form submit buttons disabled when the form is being
4
4
  // submitted to avoid multiple submits.
@@ -15,7 +15,7 @@ $(() => {
15
15
  }, 1000);
16
16
 
17
17
  // Prevent reload when page is already unloading, otherwise it may cause infinite reloads.
18
- window.addEventListener("beforeunload", () => {
18
+ window.addEventListener("pagehide", () => {
19
19
  clearInterval(exitInterval);
20
20
  return;
21
21
  });
@@ -41,7 +41,6 @@ import "src/decidim/vizzs"
41
41
  import "src/decidim/responsive_horizontal_tabs"
42
42
  import "src/decidim/security/selfxss_warning"
43
43
  import "src/decidim/session_timeouter"
44
- import "src/decidim/confirm"
45
44
  import "src/decidim/results_listing"
46
45
  import "src/decidim/impersonation"
47
46
  import "src/decidim/gallery"
@@ -51,8 +50,10 @@ import "src/decidim/abide_form_validator_fixer"
51
50
  import "src/decidim/sw"
52
51
  import "src/decidim/sticky_header"
53
52
  import "src/decidim/attachments"
53
+ import "src/decidim/dropdown_menu"
54
54
 
55
55
  // local deps that require initialization
56
+ import ConfirmDialog, { initializeConfirm } from "src/decidim/confirm"
56
57
  import formDatePicker from "src/decidim/datepicker/form_datepicker"
57
58
  import Configuration from "src/decidim/configuration"
58
59
  import ExternalLink from "src/decidim/external_link"
@@ -91,6 +92,7 @@ window.Decidim = window.Decidim || {
91
92
  addInputEmoji,
92
93
  EmojiButton,
93
94
  Dialogs,
95
+ ConfirmDialog,
94
96
  announceForScreenReader
95
97
  };
96
98
 
@@ -123,6 +125,8 @@ window.initFoundation = (element) => {
123
125
  });
124
126
  };
125
127
 
128
+ // Confirm initialization needs to happen before Rails.start()
129
+ initializeConfirm();
126
130
  Rails.start()
127
131
 
128
132
  /**
@@ -93,7 +93,7 @@ export default class InputCharacterCounter {
93
93
  this.$srTarget = $(`#${screenReaderId}`);
94
94
  if (!this.$srTarget.length) {
95
95
  this.$srTarget = $(
96
- `<span role="status" id="${screenReaderId}" class="sr-only remaining-character-count-sr" />`
96
+ `<span role="status" id="${screenReaderId}" class="sr-only remaining-character-count-sr" aria-hidden="true"/>`
97
97
  );
98
98
  this.$target.before(this.$srTarget);
99
99
  }
@@ -127,7 +127,7 @@ $(() => {
127
127
  setTimer(timeoutInSeconds);
128
128
  });
129
129
 
130
- window.addEventListener("beforeunload", () => {
130
+ window.addEventListener("pagehide", () => {
131
131
  clearInterval(exitInterval);
132
132
  return;
133
133
  });
@@ -0,0 +1,148 @@
1
+ import confirmAction from "src/decidim/confirm"
2
+ import { getMessages } from "src/decidim/i18n"
3
+
4
+ const { Rails } = window;
5
+
6
+ const createUnloadPreventer = () => {
7
+ const preventUnloadConditions = [];
8
+
9
+ const confirmMessage = getMessages("confirmUnload") || "Are you sure you want to leave this page?";
10
+
11
+ const canUnload = (event) => !preventUnloadConditions.some((condition) => condition(event));
12
+
13
+ // TLDR:
14
+ // The beforeunload event does not work during tests due to the deprecation of
15
+ // the unload event and ChromeDriver automatically accepting these dialogs.
16
+ // ---
17
+ //
18
+ // Even when there are custom listeners on links and forms, the beforeunload
19
+ // event is to ensure that the user does not accidentally reload the page or
20
+ // close the browser or the tab. Note that this does not work during the tests
21
+ // with ChromeDriver due to the deprecation of the unload event and
22
+ // ChromeDriver automatically accepting these dialogs. For the time being,
23
+ // this should work when a real user interacts with the browser along with the
24
+ // "Permissions-Policy" header set by the backend. For more information about
25
+ // the header, see Decidim::Headers::BrowserFeaturePermissions).
26
+ const unloadListener = (event) => {
27
+ if (canUnload(event)) {
28
+ return;
29
+ }
30
+
31
+ // According to:
32
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
33
+ //
34
+ // > [...] best practice is to trigger the dialog by invoking
35
+ // > preventDefault() on the event object, while also setting returnValue to
36
+ // > support legacy cases.
37
+ event.preventDefault();
38
+ event.returnValue = true;
39
+ };
40
+
41
+ // The beforeunload event listener has to be registered AFTER a user
42
+ // interaction which is why it is wrapped around the next click event that
43
+ // happens after the first unload listener was registered. Otherwise it might
44
+ // not work due to the deprecation of the unload APIs in Chromium based
45
+ // browsers and possibly in the web standards in the future.
46
+ //
47
+ // According to:
48
+ // https://developer.chrome.com/docs/web-platform/page-lifecycle-api#the_beforeunload_event
49
+ //
50
+ // > Never add a beforeunload listener unconditionally or use it as an
51
+ // > end-of-session signal. Only add it when a user has unsaved work, and
52
+ // > remove it as soon as that work has been saved.
53
+ const registerBeforeUnload = () => {
54
+ window.removeEventListener("click", registerBeforeUnload);
55
+ window.addEventListener("beforeunload", unloadListener);
56
+ };
57
+
58
+ const disableBeforeUnload = () => {
59
+ window.removeEventListener("click", registerBeforeUnload);
60
+ window.removeEventListener("beforeunload", unloadListener);
61
+ };
62
+
63
+ const linkClickListener = (ev) => {
64
+ const link = ev.target?.closest("a");
65
+ if (!link) {
66
+ return;
67
+ }
68
+
69
+ if (canUnload(ev)) {
70
+ disableBeforeUnload();
71
+ document.removeEventListener("click", linkClickListener);
72
+ return;
73
+ }
74
+
75
+ window.exitUrl = link.href;
76
+
77
+ ev.preventDefault();
78
+ ev.stopPropagation();
79
+
80
+ confirmAction(confirmMessage, link).then((answer) => {
81
+ if (!answer) {
82
+ return;
83
+ }
84
+
85
+ disableBeforeUnload();
86
+ document.removeEventListener("click", linkClickListener);
87
+ link.click();
88
+ });
89
+ };
90
+
91
+ const formSubmitListener = (ev) => {
92
+ const source = ev.target?.closest("form");
93
+ if (!source) {
94
+ return;
95
+ }
96
+
97
+ if (canUnload(ev)) {
98
+ disableBeforeUnload();
99
+ document.removeEventListener("submit", formSubmitListener);
100
+ return;
101
+ }
102
+
103
+ const button = source.closest(Rails.formSubmitSelector);
104
+ if (!button) {
105
+ return;
106
+ }
107
+
108
+ ev.preventDefault();
109
+ ev.stopImmediatePropagation();
110
+ ev.stopPropagation();
111
+
112
+ confirmAction(confirmMessage, button).then((answer) => {
113
+ if (!answer) {
114
+ return;
115
+ }
116
+
117
+ disableBeforeUnload();
118
+ document.removeEventListener("submit", formSubmitListener);
119
+ source.submit();
120
+ });
121
+ };
122
+
123
+ const registerPreventUnloadListeners = () => {
124
+ window.addEventListener("click", registerBeforeUnload);
125
+ document.addEventListener("click", linkClickListener);
126
+ document.addEventListener("submit", formSubmitListener);
127
+ };
128
+
129
+ return {
130
+ addPreventCondition: (condition) => {
131
+ if (typeof condition !== "function") {
132
+ return;
133
+ }
134
+
135
+ if (preventUnloadConditions.length < 1) {
136
+ // The unload listeners are global, so only the first call to this
137
+ // function should result to registering these listeners.
138
+ registerPreventUnloadListeners();
139
+ }
140
+
141
+ preventUnloadConditions.push(condition);
142
+ }
143
+ };
144
+ };
145
+
146
+ const unloadPreventer = createUnloadPreventer();
147
+
148
+ export const preventUnload = (condition) => unloadPreventer.addPreventCondition(condition);
@@ -12,12 +12,12 @@
12
12
  &__content {
13
13
  @apply col-span-2 md:col-span-1 order-first md:order-none space-y-2;
14
14
 
15
- > span:first-child {
16
- span {
17
- @apply text-gray-2 text-sm;
15
+ > div:first-child {
16
+ p {
17
+ @apply text-gray-2 text-sm inline-block;
18
18
 
19
19
  svg {
20
- @apply text-gray fill-current;
20
+ @apply text-gray fill-current inline;
21
21
  }
22
22
  }
23
23
 
@@ -164,6 +164,10 @@
164
164
  svg {
165
165
  @apply flex-none text-gray fill-current;
166
166
  }
167
+
168
+ [data-author] {
169
+ @apply flex first:[&>*]:max-w-40 -mr-8;
170
+ }
167
171
  }
168
172
 
169
173
  /* shared styles */
@@ -89,7 +89,7 @@
89
89
  }
90
90
 
91
91
  span {
92
- @apply text-sm text-gray-2 truncate whitespace-nowrap first-letter:uppercase;
92
+ @apply text-sm text-gray-2 first-letter:uppercase;
93
93
  }
94
94
 
95
95
  [data-controls*="panel"] {