decidim-core 0.26.2 → 0.27.0.rc1

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 (543) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/cells/decidim/amendable/announcement_cell.rb +3 -8
  4. data/app/cells/decidim/announcement_cell.rb +1 -1
  5. data/app/cells/decidim/author_cell.rb +1 -1
  6. data/app/cells/decidim/content_blocks/footer_sub_hero/show.erb +1 -1
  7. data/app/cells/decidim/content_blocks/hero/show.erb +1 -1
  8. data/app/cells/decidim/content_blocks/highlighted_content_banner/show.erb +3 -3
  9. data/app/cells/decidim/content_blocks/highlighted_elements_cell.rb +1 -1
  10. data/app/cells/decidim/content_blocks/how_to_participate/show.erb +1 -1
  11. data/app/cells/decidim/content_blocks/last_activity/show.erb +1 -1
  12. data/app/cells/decidim/content_blocks/last_activity_cell.rb +3 -3
  13. data/app/cells/decidim/content_blocks/stats_cell.rb +4 -4
  14. data/app/cells/decidim/content_blocks/sub_hero/show.erb +1 -1
  15. data/app/cells/decidim/data_consent/category.erb +64 -0
  16. data/app/cells/decidim/data_consent/dialog.erb +29 -0
  17. data/app/cells/decidim/data_consent/modal.erb +39 -0
  18. data/app/cells/decidim/data_consent/show.erb +3 -0
  19. data/app/cells/decidim/data_consent_cell.rb +21 -0
  20. data/app/cells/decidim/follow_button_cell.rb +5 -7
  21. data/app/cells/decidim/profile_sidebar_cell.rb +1 -1
  22. data/app/cells/decidim/search_results_cell.rb +1 -1
  23. data/app/cells/decidim/tos_page/refuse_btn_modal.erb +1 -1
  24. data/app/cells/decidim/upload_modal/files.erb +52 -0
  25. data/app/cells/decidim/upload_modal/modal.erb +49 -0
  26. data/app/cells/decidim/upload_modal/show.erb +3 -0
  27. data/app/cells/decidim/upload_modal_cell.rb +205 -0
  28. data/app/cells/decidim/user_conversation/reply.erb +1 -1
  29. data/app/cells/decidim/user_conversations/add_conversation_users.erb +5 -3
  30. data/app/cells/decidim/version_cell.rb +2 -13
  31. data/app/cells/decidim/versions_list_cell.rb +2 -13
  32. data/app/cells/decidim/versions_list_item_cell.rb +2 -13
  33. data/app/commands/decidim/accept_group_invitation.rb +1 -1
  34. data/app/commands/decidim/accept_user_group_join_request.rb +1 -1
  35. data/app/commands/decidim/amendable/accept.rb +1 -1
  36. data/app/commands/decidim/amendable/create_draft.rb +1 -1
  37. data/app/commands/decidim/amendable/destroy_draft.rb +1 -1
  38. data/app/commands/decidim/amendable/promote.rb +1 -1
  39. data/app/commands/decidim/amendable/publish_draft.rb +1 -1
  40. data/app/commands/decidim/amendable/reject.rb +1 -1
  41. data/app/commands/decidim/amendable/update_draft.rb +1 -1
  42. data/app/commands/decidim/amendable/withdraw.rb +1 -1
  43. data/app/commands/decidim/attachment_methods.rb +3 -2
  44. data/app/commands/decidim/create_editor_image.rb +1 -1
  45. data/app/commands/decidim/create_follow.rb +1 -1
  46. data/app/commands/decidim/create_omniauth_registration.rb +2 -3
  47. data/app/commands/decidim/create_registration.rb +1 -2
  48. data/app/commands/decidim/create_report.rb +1 -1
  49. data/app/commands/decidim/create_user_group.rb +2 -2
  50. data/app/commands/decidim/create_user_report.rb +5 -3
  51. data/app/commands/decidim/delete_follow.rb +1 -1
  52. data/app/commands/decidim/demote_membership.rb +1 -1
  53. data/app/commands/decidim/destroy_account.rb +1 -1
  54. data/app/commands/decidim/endorse_resource.rb +1 -1
  55. data/app/commands/decidim/gallery_methods.rb +45 -10
  56. data/app/commands/decidim/invite_user.rb +1 -3
  57. data/app/commands/decidim/invite_user_again.rb +1 -1
  58. data/app/commands/decidim/invite_user_to_group.rb +1 -1
  59. data/app/commands/decidim/join_user_group.rb +1 -1
  60. data/app/commands/decidim/leave_user_group.rb +8 -2
  61. data/app/commands/decidim/messaging/reply_to_conversation.rb +2 -2
  62. data/app/commands/decidim/messaging/start_conversation.rb +2 -2
  63. data/app/commands/decidim/multiple_attachments_methods.rb +44 -5
  64. data/app/commands/decidim/promote_membership.rb +1 -1
  65. data/app/commands/decidim/reject_group_invitation.rb +1 -1
  66. data/app/commands/decidim/reject_user_group_join_request.rb +1 -1
  67. data/app/commands/decidim/remove_user_from_group.rb +1 -1
  68. data/app/commands/decidim/resend_confirmation_instructions.rb +18 -0
  69. data/app/commands/decidim/search.rb +10 -9
  70. data/app/commands/decidim/unendorse_resource.rb +1 -1
  71. data/app/commands/decidim/unsubscribe_settings.rb +1 -1
  72. data/app/commands/decidim/update_account.rb +2 -2
  73. data/app/commands/decidim/update_notifications_settings.rb +3 -2
  74. data/app/commands/decidim/update_password.rb +31 -0
  75. data/app/commands/decidim/update_user_group.rb +2 -2
  76. data/app/commands/decidim/update_user_interests.rb +1 -1
  77. data/app/commands/decidim/validate_upload.rb +15 -0
  78. data/app/controllers/concerns/decidim/devise_controllers.rb +0 -1
  79. data/app/controllers/concerns/decidim/filter_resource.rb +23 -24
  80. data/app/controllers/concerns/decidim/force_authentication.rb +1 -2
  81. data/app/controllers/concerns/decidim/http_caching_disabler.rb +1 -1
  82. data/app/controllers/concerns/decidim/needs_password_change.rb +43 -0
  83. data/app/controllers/concerns/decidim/needs_tos_accepted.rb +3 -3
  84. data/app/controllers/concerns/decidim/orderable.rb +1 -1
  85. data/app/controllers/concerns/decidim/participatory_space_context.rb +2 -2
  86. data/app/controllers/decidim/account_controller.rb +45 -0
  87. data/app/controllers/decidim/application_controller.rb +1 -1
  88. data/app/controllers/decidim/devise/invitations_controller.rb +2 -0
  89. data/app/controllers/decidim/devise/passwords_controller.rb +33 -0
  90. data/app/controllers/decidim/devise/registrations_controller.rb +2 -0
  91. data/app/controllers/decidim/devise/sessions_controller.rb +2 -0
  92. data/app/controllers/decidim/{data_portability_controller.rb → download_your_data_controller.rb} +8 -8
  93. data/app/controllers/decidim/groups_controller.rb +5 -0
  94. data/app/controllers/decidim/last_activities_controller.rb +7 -9
  95. data/app/controllers/decidim/manifests_controller.rb +11 -0
  96. data/app/controllers/decidim/newsletters_controller.rb +1 -1
  97. data/app/controllers/decidim/notifications_subscriptions_controller.rb +16 -0
  98. data/app/controllers/decidim/offline_controller.rb +7 -0
  99. data/app/controllers/decidim/profiles_controller.rb +1 -1
  100. data/app/controllers/decidim/searches_controller.rb +4 -3
  101. data/app/controllers/decidim/short_links_controller.rb +35 -0
  102. data/app/controllers/decidim/upload_validations_controller.rb +44 -0
  103. data/app/controllers/decidim/user_activities_controller.rb +32 -16
  104. data/app/controllers/decidim/user_interests_controller.rb +0 -2
  105. data/app/controllers/decidim/user_timeline_controller.rb +6 -8
  106. data/app/events/decidim/change_nickname_event.rb +21 -0
  107. data/app/forms/decidim/account_form.rb +7 -6
  108. data/app/forms/decidim/form.rb +1 -1
  109. data/app/forms/decidim/messaging/conversation_form.rb +1 -1
  110. data/app/forms/decidim/notifications_settings_form.rb +8 -2
  111. data/app/forms/decidim/password_form.rb +10 -0
  112. data/app/forms/decidim/registration_form.rb +10 -4
  113. data/app/forms/decidim/upload_validation_form.rb +54 -0
  114. data/app/forms/decidim/user_group_form.rb +1 -1
  115. data/app/forms/decidim/user_interest_scope_form.rb +1 -1
  116. data/app/forms/decidim/user_interests_form.rb +1 -1
  117. data/app/forms/translatable_presence_validator.rb +2 -2
  118. data/app/forms/url_validator.rb +1 -1
  119. data/app/helpers/decidim/application_helper.rb +2 -2
  120. data/app/helpers/decidim/attachments_helper.rb +1 -1
  121. data/app/helpers/decidim/cells_paginate_helper.rb +1 -1
  122. data/app/helpers/decidim/datalist_select_helper.rb +40 -0
  123. data/app/helpers/decidim/decidim_form_helper.rb +1 -1
  124. data/app/helpers/decidim/followable_helper.rb +1 -1
  125. data/app/helpers/decidim/icon_helper.rb +1 -1
  126. data/app/helpers/decidim/layout_helper.rb +15 -1
  127. data/app/helpers/decidim/map_helper.rb +12 -8
  128. data/app/helpers/decidim/meta_tags_helper.rb +0 -2
  129. data/app/helpers/decidim/notifications_settings_helper.rb +15 -0
  130. data/app/helpers/decidim/passwords_helper.rb +41 -0
  131. data/app/helpers/decidim/sanitize_helper.rb +1 -0
  132. data/app/helpers/decidim/searches_helper.rb +6 -6
  133. data/app/helpers/decidim/short_link_helper.rb +28 -0
  134. data/app/helpers/decidim/translations_helper.rb +2 -2
  135. data/app/jobs/decidim/{data_portability_export_job.rb → download_your_data_export_job.rb} +5 -5
  136. data/app/jobs/decidim/email_notifications_digest_generator_job.rb +21 -0
  137. data/app/jobs/decidim/notification_generator_for_recipient_job.rb +5 -3
  138. data/app/jobs/decidim/reminder_generator_job.rb +12 -0
  139. data/app/jobs/decidim/resend_confirmation_instructions_job.rb +11 -0
  140. data/app/jobs/decidim/user_report_job.rb +2 -2
  141. data/app/mailers/concerns/decidim/multitenant_asset_host.rb +11 -3
  142. data/app/mailers/decidim/application_mailer.rb +1 -1
  143. data/app/mailers/decidim/block_user_mailer.rb +8 -7
  144. data/app/mailers/decidim/export_mailer.rb +2 -2
  145. data/app/mailers/decidim/newsletter_mailer.rb +2 -2
  146. data/app/mailers/decidim/newsletters_opt_in_mailer.rb +7 -5
  147. data/app/mailers/decidim/notifications_digest_mailer.rb +23 -0
  148. data/app/mailers/decidim/user_report_mailer.rb +10 -11
  149. data/app/models/decidim/action_log.rb +86 -2
  150. data/app/models/decidim/amendment.rb +0 -1
  151. data/app/models/decidim/area.rb +1 -2
  152. data/app/models/decidim/area_type.rb +5 -0
  153. data/app/models/decidim/attachment.rb +6 -0
  154. data/app/models/decidim/attachment_collection.rb +5 -0
  155. data/app/models/decidim/category.rb +9 -4
  156. data/app/models/decidim/coauthorship.rb +0 -2
  157. data/app/models/decidim/contextual_help_section.rb +5 -1
  158. data/app/models/decidim/follow.rb +2 -2
  159. data/app/models/decidim/gamification/badge_score.rb +0 -1
  160. data/app/models/decidim/identity.rb +2 -2
  161. data/app/models/decidim/messaging/conversation.rb +4 -4
  162. data/app/models/decidim/messaging/message.rb +1 -2
  163. data/app/models/decidim/messaging/participation.rb +0 -2
  164. data/app/models/decidim/messaging/receipt.rb +0 -2
  165. data/app/models/decidim/notification.rb +5 -2
  166. data/app/models/decidim/oauth_application.rb +12 -1
  167. data/app/models/decidim/organization.rb +7 -7
  168. data/app/models/decidim/participatory_space_private_user.rb +4 -2
  169. data/app/models/decidim/permission_action.rb +1 -0
  170. data/app/models/decidim/reminder.rb +10 -0
  171. data/app/models/decidim/reminder_delivery.rb +7 -0
  172. data/app/models/decidim/reminder_record.rb +29 -0
  173. data/app/models/decidim/report.rb +2 -2
  174. data/app/models/decidim/scope.rb +3 -4
  175. data/app/models/decidim/scope_type.rb +5 -0
  176. data/app/models/decidim/share_token.rb +0 -2
  177. data/app/models/decidim/short_link.rb +162 -0
  178. data/app/models/decidim/user.rb +34 -6
  179. data/app/models/decidim/user_base_entity.rb +1 -1
  180. data/app/models/decidim/user_group.rb +3 -3
  181. data/app/models/decidim/user_moderation.rb +0 -2
  182. data/app/models/decidim/user_report.rb +2 -2
  183. data/app/packs/entrypoints/decidim_core.js +2 -2
  184. data/app/packs/entrypoints/decidim_sw.js +1 -0
  185. data/app/packs/src/decidim/autocomplete.js +298 -0
  186. data/app/packs/src/decidim/cookie_consent/consent_manager.js +133 -0
  187. data/app/packs/src/decidim/cookie_consent/cookie_consent.js +103 -0
  188. data/app/packs/src/decidim/datalist_select.js +36 -0
  189. data/app/packs/src/decidim/direct_uploads/upload_field.js +117 -0
  190. data/app/packs/src/decidim/direct_uploads/upload_modal.js +264 -0
  191. data/app/packs/src/decidim/direct_uploads/upload_utility.js +49 -0
  192. data/app/packs/src/decidim/direct_uploads/uploader.js +89 -0
  193. data/app/packs/src/decidim/external_domain_warning.js +1 -1
  194. data/app/packs/src/decidim/external_link.js +14 -3
  195. data/app/packs/src/decidim/external_link.test.js +64 -0
  196. data/app/packs/src/decidim/geocoding.js +10 -35
  197. data/app/packs/src/decidim/identity_selector_dialog.js +1 -1
  198. data/app/packs/src/decidim/impersonation.js +3 -3
  199. data/app/packs/src/decidim/input_character_counter.js +177 -7
  200. data/app/packs/src/decidim/input_multiple_mentions.js +87 -187
  201. data/app/packs/src/decidim/map/factory.js +3 -1
  202. data/app/packs/src/decidim/map/icon.js +2 -1
  203. data/app/packs/src/decidim/map/legacy.js +2 -2
  204. data/app/packs/src/decidim/map/svg-icon.js +248 -0
  205. data/app/packs/src/decidim/map.js +2 -2
  206. data/app/packs/src/decidim/security/selfxss_warning.js +5 -1
  207. data/app/packs/src/decidim/session_timeouter.js +15 -15
  208. data/app/packs/src/decidim/sw/a2hs.js +28 -0
  209. data/app/packs/src/decidim/sw/index.js +3 -0
  210. data/app/packs/src/decidim/sw/loader.js +18 -0
  211. data/app/packs/src/decidim/sw/push-permissions.js +87 -0
  212. data/app/packs/src/decidim/sw/sw.js +80 -0
  213. data/app/packs/src/decidim/vizzs/areachart.js +33 -28
  214. data/app/packs/src/decidim/vizzs/{metrics.js → index.js} +0 -0
  215. data/app/packs/src/decidim/vizzs/linechart.js +23 -18
  216. data/app/packs/src/decidim/vizzs/rowchart.js +15 -11
  217. data/app/packs/stylesheets/decidim/email.scss +66 -0
  218. data/app/packs/stylesheets/decidim/layouts/_user.scss +8 -0
  219. data/app/packs/stylesheets/decidim/modules/_autocomplete.scss +99 -0
  220. data/app/packs/stylesheets/decidim/modules/_cookie-consent.scss +115 -0
  221. data/app/packs/stylesheets/decidim/modules/_footer.scss +4 -0
  222. data/app/packs/stylesheets/decidim/modules/_input-multiple-mentions.scss +29 -2
  223. data/app/packs/stylesheets/decidim/modules/_modules.scss +3 -1
  224. data/app/packs/stylesheets/decidim/modules/_reveal.scss +15 -0
  225. data/app/packs/stylesheets/decidim/modules/_upload_modal.scss +143 -0
  226. data/app/packs/stylesheets/decidim/utils/_fontface.scss +3 -0
  227. data/app/permissions/decidim/permissions.rb +2 -4
  228. data/app/presenters/decidim/admin_log/area_type_presenter.rb +34 -0
  229. data/app/presenters/decidim/admin_log/attachment_collection_presenter.rb +35 -0
  230. data/app/presenters/decidim/admin_log/attachment_presenter.rb +27 -0
  231. data/app/presenters/decidim/admin_log/category_presenter.rb +36 -0
  232. data/app/presenters/decidim/admin_log/component_presenter.rb +35 -3
  233. data/app/presenters/decidim/admin_log/contextual_help_section_presenter.rb +33 -0
  234. data/app/presenters/decidim/admin_log/organization_presenter.rb +9 -4
  235. data/app/presenters/decidim/admin_log/scope_type_presenter.rb +34 -0
  236. data/app/presenters/decidim/home_stats_presenter.rb +4 -2
  237. data/app/presenters/decidim/log/base_presenter.rb +1 -1
  238. data/app/presenters/decidim/menu_item_presenter.rb +1 -1
  239. data/app/presenters/decidim/metric_charts_presenter.rb +7 -1
  240. data/app/presenters/decidim/nil_presenter.rb +3 -1
  241. data/app/presenters/decidim/notification_to_mailer_presenter.rb +39 -0
  242. data/app/presenters/decidim/notifications_digest_presenter.rb +42 -0
  243. data/app/presenters/decidim/official_author_presenter.rb +4 -0
  244. data/app/presenters/decidim/organization_presenter.rb +22 -0
  245. data/app/presenters/decidim/push_notification_presenter.rb +24 -0
  246. data/app/presenters/decidim/stats_presenter.rb +1 -1
  247. data/app/presenters/decidim/validation_errors_presenter.rb +1 -1
  248. data/app/queries/decidim/messaging/user_conversations.rb +1 -1
  249. data/app/queries/decidim/metric_manage.rb +1 -1
  250. data/app/queries/decidim/own_activities.rb +11 -0
  251. data/app/queries/decidim/participatory_processes_with_user_role.rb +1 -1
  252. data/app/{services/decidim/activity_search.rb → queries/decidim/public_activities.rb} +35 -77
  253. data/app/queries/decidim/public_components.rb +1 -1
  254. data/app/queries/decidim/similar_emendations.rb +1 -1
  255. data/app/queries/decidim/stats_users_count.rb +1 -1
  256. data/app/queries/decidim/user_groups/accepted_memberships.rb +1 -1
  257. data/app/queries/decidim/user_groups/accepted_user_groups.rb +1 -1
  258. data/app/queries/decidim/user_groups/accepted_users.rb +1 -1
  259. data/app/queries/decidim/user_groups/admin_memberships.rb +1 -1
  260. data/app/queries/decidim/user_groups/invited_memberships.rb +1 -1
  261. data/app/queries/decidim/user_groups/manageable_user_groups.rb +1 -1
  262. data/app/queries/decidim/user_groups/member_memberships.rb +1 -1
  263. data/app/services/decidim/{data_portability_exporter.rb → download_your_data_exporter.rb} +6 -6
  264. data/app/services/decidim/email_notification_generator.rb +2 -2
  265. data/app/services/decidim/engine_resolver.rb +66 -0
  266. data/app/services/decidim/events_manager.rb +1 -1
  267. data/app/services/decidim/iframe_disabler.rb +31 -0
  268. data/app/services/decidim/notification_generator_for_recipient.rb +1 -1
  269. data/app/services/decidim/notifications_digest_sending_decider.rb +18 -0
  270. data/app/services/decidim/notifications_subscriptions_persistor.rb +37 -0
  271. data/app/services/decidim/open_data_exporter.rb +1 -1
  272. data/app/services/decidim/resource_search.rb +17 -164
  273. data/app/services/decidim/send_push_notification.rb +59 -0
  274. data/app/services/decidim/traceability.rb +1 -1
  275. data/app/uploaders/decidim/application_uploader.rb +2 -12
  276. data/app/uploaders/decidim/cw/attachment_uploader.rb +1 -1
  277. data/app/uploaders/decidim/cw/{data_portability_uploader.rb → download_your_data_uploader.rb} +3 -3
  278. data/app/uploaders/decidim/cw/image_uploader.rb +2 -2
  279. data/app/uploaders/decidim/organization_favicon_uploader.rb +3 -2
  280. data/app/validators/passthru_validator.rb +29 -21
  281. data/app/validators/password_validator.rb +25 -4
  282. data/app/validators/uploader_image_dimensions_validator.rb +1 -1
  283. data/app/views/decidim/account/_account_notification.js.erb +20 -0
  284. data/app/views/decidim/account/_password_fields.html.erb +2 -2
  285. data/app/views/decidim/account/cancel_email_change.js.erb +6 -0
  286. data/app/views/decidim/account/resend_confirmation_instructions.erb +1 -0
  287. data/app/views/decidim/account/show.html.erb +19 -6
  288. data/app/views/decidim/application/{_attachments.html.erb → _attachments.erb} +2 -2
  289. data/app/views/decidim/application/_collection.html.erb +1 -1
  290. data/app/views/decidim/application/{_document.html.erb → _document.erb} +0 -0
  291. data/app/views/decidim/application/{_documents.html.erb → _documents.erb} +2 -2
  292. data/app/views/decidim/application/{_photos.html.erb → _photos.erb} +0 -0
  293. data/app/views/decidim/devise/confirmations/new.html.erb +1 -1
  294. data/app/views/decidim/devise/invitations/edit.html.erb +3 -4
  295. data/app/views/decidim/devise/omniauth_registrations/new.html.erb +3 -3
  296. data/app/views/decidim/devise/passwords/edit.html.erb +7 -5
  297. data/app/views/decidim/devise/passwords/new.html.erb +1 -1
  298. data/app/views/decidim/devise/registrations/edit.html.erb +5 -5
  299. data/app/views/decidim/devise/registrations/new.html.erb +5 -5
  300. data/app/views/decidim/devise/sessions/new.html.erb +2 -2
  301. data/app/views/decidim/{data_portability → download_your_data}/export.html.erb +0 -0
  302. data/app/views/decidim/{data_portability → download_your_data}/show.html.erb +2 -2
  303. data/app/views/decidim/export_mailer/download_your_data_export.html.erb +7 -0
  304. data/app/views/decidim/last_activities/index.html.erb +1 -1
  305. data/app/views/decidim/manifests/show.json.erb +31 -0
  306. data/app/views/decidim/messaging/conversations/_add_conversation_users.html.erb +5 -3
  307. data/app/views/decidim/messaging/conversations/_reply.html.erb +1 -1
  308. data/app/views/decidim/messaging/conversations/_start.html.erb +1 -3
  309. data/app/views/decidim/messaging/conversations/create.js.erb +1 -0
  310. data/app/views/decidim/notifications_digest_mailer/_email_content.html.erb +11 -0
  311. data/app/views/decidim/notifications_digest_mailer/digest_mail.erb +27 -0
  312. data/app/views/decidim/notifications_settings/show.html.erb +34 -9
  313. data/app/views/decidim/offline/show.html.erb +9 -0
  314. data/app/views/decidim/pages/_standalone.html.erb +1 -1
  315. data/app/views/decidim/reported_mailer/hide.html.erb +1 -1
  316. data/app/views/decidim/reported_mailer/report.html.erb +1 -1
  317. data/app/views/decidim/scopes/picker.html.erb +1 -1
  318. data/app/views/decidim/searches/_filters.html.erb +2 -2
  319. data/app/views/decidim/shared/participatory_space_filters/_filters.html.erb +3 -3
  320. data/app/views/decidim/user_report_mailer/notify.html.erb +10 -3
  321. data/app/views/devise/mailer/confirmation_instructions.html.erb +3 -1
  322. data/app/views/layouts/decidim/_application.html.erb +6 -3
  323. data/app/views/layouts/decidim/_cookie_warning.html.erb +8 -8
  324. data/app/views/layouts/decidim/_decidim_javascript.html.erb +12 -0
  325. data/app/views/layouts/decidim/_head.html.erb +4 -2
  326. data/app/views/layouts/decidim/_js_configuration.html.erb +5 -1
  327. data/app/views/layouts/decidim/_main_footer.html.erb +1 -0
  328. data/app/views/layouts/decidim/_offline_banner.html.erb +11 -0
  329. data/app/views/layouts/decidim/widget.html.erb +2 -2
  330. data/config/assets.rb +1 -0
  331. data/config/locales/am-ET.yml +1 -0
  332. data/config/locales/ar.yml +48 -36
  333. data/config/locales/bg.yml +15 -30
  334. data/config/locales/ca.yml +186 -35
  335. data/config/locales/cs.yml +186 -33
  336. data/config/locales/da.yml +1 -0
  337. data/config/locales/de.yml +25 -36
  338. data/config/locales/el.yml +15 -30
  339. data/config/locales/en.yml +183 -32
  340. data/config/locales/eo.yml +4 -6
  341. data/config/locales/es-MX.yml +185 -34
  342. data/config/locales/es-PY.yml +185 -34
  343. data/config/locales/es.yml +185 -34
  344. data/config/locales/et.yml +1 -0
  345. data/config/locales/eu.yml +15 -36
  346. data/config/locales/fi-plain.yml +184 -33
  347. data/config/locales/fi.yml +184 -33
  348. data/config/locales/fr-CA.yml +170 -33
  349. data/config/locales/fr.yml +170 -33
  350. data/config/locales/ga-IE.yml +5 -7
  351. data/config/locales/gl.yml +47 -30
  352. data/config/locales/hr.yml +1 -0
  353. data/config/locales/hu.yml +65 -35
  354. data/config/locales/id-ID.yml +15 -29
  355. data/config/locales/is-IS.yml +6 -15
  356. data/config/locales/it.yml +22 -38
  357. data/config/locales/ja.yml +173 -34
  358. data/config/locales/ko.yml +1 -0
  359. data/config/locales/lb.yml +16 -32
  360. data/config/locales/lt.yml +1 -0
  361. data/config/locales/lv.yml +15 -28
  362. data/config/locales/mt.yml +1 -0
  363. data/config/locales/nl.yml +25 -33
  364. data/config/locales/no.yml +24 -35
  365. data/config/locales/om-ET.yml +1 -0
  366. data/config/locales/pl.yml +113 -32
  367. data/config/locales/pt-BR.yml +17 -32
  368. data/config/locales/pt.yml +16 -32
  369. data/config/locales/ro-RO.yml +20 -36
  370. data/config/locales/ru.yml +15 -27
  371. data/config/locales/si-LK.yml +1 -0
  372. data/config/locales/sk.yml +15 -30
  373. data/config/locales/sl.yml +1 -0
  374. data/config/locales/so-SO.yml +1 -0
  375. data/config/locales/sr-CS.yml +1 -0
  376. data/config/locales/sv.yml +69 -34
  377. data/config/locales/sw-KE.yml +1 -0
  378. data/config/locales/ti-ER.yml +1 -0
  379. data/config/locales/tr-TR.yml +15 -29
  380. data/config/locales/uk.yml +15 -26
  381. data/config/locales/val-ES.yml +1 -0
  382. data/config/locales/vi.yml +1 -0
  383. data/config/locales/zh-CN.yml +15 -28
  384. data/config/locales/zh-TW.yml +1 -0
  385. data/config/routes.rb +17 -2
  386. data/db/migrate/20180508111710_add_accepted_tos_version_field_to_users.rb +1 -0
  387. data/db/migrate/20181030090144_destroy_deleted_users_follows.rb +1 -0
  388. data/db/migrate/20190412131728_fix_user_names.rb +4 -4
  389. data/db/migrate/20210831181634_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
  390. data/db/migrate/20210831181635_create_active_storage_variant_records.active_storage.rb +14 -0
  391. data/db/migrate/20211208155453_create_decidim_reminders.rb +11 -0
  392. data/db/migrate/20211209121025_create_decidim_reminder_records.rb +11 -0
  393. data/db/migrate/20211209121040_create_decidim_reminder_deliveries.rb +10 -0
  394. data/db/migrate/20220118121921_change_required_description_categories.rb +7 -0
  395. data/db/migrate/20220127113419_add_notification_settings_to_users.rb +7 -0
  396. data/db/migrate/20220203121137_add_notifications_sending_frequency_to_users.rb +7 -0
  397. data/db/migrate/20220215172439_add_digest_sent_at_to_users.rb +7 -0
  398. data/db/migrate/20220323195258_add_index_to_decidim_users_notifications_sending_frequency.rb +7 -0
  399. data/db/migrate/20220427142214_drop_emails_on_notifications_flag_from_user.rb +15 -0
  400. data/db/migrate/20220518094535_add_previous_passwords_to_users.rb +20 -0
  401. data/db/migrate/20220524195530_create_decidim_short_links.rb +23 -0
  402. data/db/seeds.rb +17 -18
  403. data/lib/decidim/acts_as_tree.rb +47 -0
  404. data/lib/decidim/api/input_sorts/component_input_sort.rb +2 -1
  405. data/lib/decidim/api/interfaces/authorable_interface.rb +6 -5
  406. data/lib/decidim/asset_router.rb +80 -0
  407. data/lib/decidim/attachment_attributes.rb +8 -8
  408. data/lib/decidim/attribute_object/form.rb +174 -0
  409. data/lib/decidim/attribute_object/model.rb +148 -0
  410. data/lib/decidim/attribute_object/nested_validator.rb +22 -0
  411. data/lib/decidim/attribute_object/type_map.rb +14 -0
  412. data/lib/decidim/attribute_object/type_resolver.rb +91 -0
  413. data/lib/decidim/attribute_object.rb +26 -0
  414. data/lib/decidim/attributes/array.rb +62 -0
  415. data/lib/decidim/attributes/blob.rb +20 -0
  416. data/lib/decidim/attributes/clean_string.rb +12 -6
  417. data/lib/decidim/attributes/hash.rb +36 -0
  418. data/lib/decidim/attributes/integer.rb +17 -0
  419. data/lib/decidim/attributes/localized_date.rb +9 -7
  420. data/lib/decidim/attributes/model.rb +24 -0
  421. data/lib/decidim/attributes/object.rb +28 -0
  422. data/lib/decidim/attributes/symbol.rb +23 -0
  423. data/lib/decidim/attributes/time_with_zone.rb +9 -7
  424. data/lib/decidim/attributes.rb +29 -0
  425. data/lib/decidim/authorable.rb +21 -6
  426. data/lib/decidim/authorization_form_builder.rb +9 -11
  427. data/lib/decidim/carrier_wave_migrator_service.rb +15 -15
  428. data/lib/decidim/coauthorable.rb +22 -4
  429. data/lib/decidim/command.rb +44 -0
  430. data/lib/decidim/common_passwords.rb +1 -1
  431. data/lib/decidim/component_manifest.rb +5 -5
  432. data/lib/decidim/component_validator.rb +2 -2
  433. data/lib/decidim/content_block_manifest.rb +1 -1
  434. data/lib/decidim/content_parsers/hashtag_parser.rb +2 -2
  435. data/lib/decidim/content_parsers/resource_parser.rb +1 -1
  436. data/lib/decidim/content_parsers/user_group_parser.rb +1 -1
  437. data/lib/decidim/content_parsers/user_parser.rb +4 -4
  438. data/lib/decidim/content_renderers/hashtag_renderer.rb +1 -1
  439. data/lib/decidim/content_renderers/link_renderer.rb +2 -2
  440. data/lib/decidim/content_renderers/user_group_renderer.rb +1 -1
  441. data/lib/decidim/content_renderers/user_renderer.rb +1 -1
  442. data/lib/decidim/controller_helpers.rb +41 -0
  443. data/lib/decidim/core/engine.rb +29 -18
  444. data/lib/decidim/core/test/factories.rb +48 -8
  445. data/lib/decidim/core/test/shared_examples/admin_resource_gallery_examples.rb +16 -4
  446. data/lib/decidim/core/test/shared_examples/amendable/amendment_form_examples.rb +1 -1
  447. data/lib/decidim/core/test/shared_examples/authorable_interface_examples.rb +15 -2
  448. data/lib/decidim/core/test/shared_examples/coauthorable_interface_examples.rb +2 -2
  449. data/lib/decidim/core/test/shared_examples/comments_examples.rb +203 -0
  450. data/lib/decidim/core/test/shared_examples/localised_email.rb +1 -1
  451. data/lib/decidim/core/test/shared_examples/logo_email.rb +1 -1
  452. data/lib/decidim/core/test/shared_examples/map_examples.rb +2 -2
  453. data/lib/decidim/core/test/shared_examples/preview_component_with_share_token_examples.rb +0 -6
  454. data/lib/decidim/core/test/shared_examples/resource_search_examples.rb +120 -40
  455. data/lib/decidim/core/test/shared_examples/resourceable.rb +0 -6
  456. data/lib/decidim/core/test/shared_examples/searchable_participatory_space_examples.rb +1 -1
  457. data/lib/decidim/core/test/shared_examples/searchable_resources_shared_context.rb +1 -1
  458. data/lib/decidim/core/test/shared_examples/system_endorse_resource_examples.rb +3 -3
  459. data/lib/decidim/core/test/shared_examples/translated_event_examples.rb +2 -2
  460. data/lib/decidim/core/version.rb +1 -1
  461. data/lib/decidim/core.rb +108 -14
  462. data/lib/decidim/{data_portability.rb → download_your_data.rb} +3 -3
  463. data/lib/decidim/{data_portability_serializers/data_portability_conversation_serializer.rb → download_your_data_serializers/download_your_data_conversation_serializer.rb} +2 -2
  464. data/lib/decidim/{data_portability_serializers/data_portability_follow_serializer.rb → download_your_data_serializers/download_your_data_follow_serializer.rb} +2 -2
  465. data/lib/decidim/{data_portability_serializers/data_portability_identity_serializer.rb → download_your_data_serializers/download_your_data_identity_serializer.rb} +2 -2
  466. data/lib/decidim/{data_portability_serializers/data_portability_notification_serializer.rb → download_your_data_serializers/download_your_data_notification_serializer.rb} +2 -2
  467. data/lib/decidim/{data_portability_serializers/data_portability_participatory_space_private_user_serializer.rb → download_your_data_serializers/download_your_data_participatory_space_private_user_serializer.rb} +2 -2
  468. data/lib/decidim/{data_portability_serializers/data_portability_report_serializer.rb → download_your_data_serializers/download_your_data_report_serializer.rb} +2 -2
  469. data/lib/decidim/{data_portability_serializers/data_portability_user_group_serializer.rb → download_your_data_serializers/download_your_data_user_group_serializer.rb} +2 -2
  470. data/lib/decidim/{data_portability_serializers/data_portability_user_serializer.rb → download_your_data_serializers/download_your_data_user_serializer.rb} +3 -3
  471. data/lib/decidim/download_your_data_serializers.rb +23 -0
  472. data/lib/decidim/engine_router.rb +7 -1
  473. data/lib/decidim/env.rb +56 -0
  474. data/lib/decidim/event_recorder.rb +25 -0
  475. data/lib/decidim/events/base_event.rb +10 -14
  476. data/lib/decidim/events/simple_event.rb +12 -4
  477. data/lib/decidim/exporters/export_data.rb +1 -1
  478. data/lib/decidim/exporters/export_manifest.rb +2 -1
  479. data/lib/decidim/file_validator_humanizer.rb +24 -1
  480. data/lib/decidim/filterable_resource.rb +55 -0
  481. data/lib/decidim/fingerprint_calculator.rb +2 -4
  482. data/lib/decidim/form_builder.rb +158 -130
  483. data/lib/decidim/gamification/badge.rb +2 -3
  484. data/lib/decidim/gamification/badge_scorer.rb +1 -0
  485. data/lib/decidim/has_area.rb +27 -0
  486. data/lib/decidim/has_attachments.rb +2 -2
  487. data/lib/decidim/has_category.rb +36 -0
  488. data/lib/decidim/importers/import_manifest.rb +3 -21
  489. data/lib/decidim/jsonb_attributes.rb +6 -4
  490. data/lib/decidim/manifest_messages.rb +23 -0
  491. data/lib/decidim/map/autocomplete.rb +6 -4
  492. data/lib/decidim/map/frontend.rb +2 -2
  493. data/lib/decidim/map/provider/geocoding/here.rb +5 -5
  494. data/lib/decidim/map/provider/here.rb +3 -0
  495. data/lib/decidim/map/provider/osm.rb +3 -0
  496. data/lib/decidim/map.rb +4 -4
  497. data/lib/decidim/menu.rb +1 -1
  498. data/lib/decidim/metric_manifest.rb +2 -2
  499. data/lib/decidim/nicknamizable.rb +1 -1
  500. data/lib/decidim/notification_setting_manifest.rb +19 -0
  501. data/lib/decidim/organization_settings.rb +8 -7
  502. data/lib/decidim/participatory_space_context_manifest.rb +2 -2
  503. data/lib/decidim/participatory_space_manifest.rb +2 -2
  504. data/lib/decidim/processes_file_locally.rb +31 -0
  505. data/lib/decidim/query.rb +84 -0
  506. data/lib/decidim/record_encryptor.rb +29 -8
  507. data/lib/decidim/reminder_manifest.rb +75 -0
  508. data/lib/decidim/reminder_registry.rb +67 -0
  509. data/lib/decidim/resource_manifest.rb +1 -1
  510. data/lib/decidim/resourceable.rb +11 -0
  511. data/lib/decidim/scopable.rb +15 -0
  512. data/lib/decidim/settings_manifest.rb +4 -4
  513. data/lib/decidim/spring.rb +16 -0
  514. data/lib/decidim/translatable_attributes.rb +12 -8
  515. data/lib/decidim/url_option_resolver.rb +50 -0
  516. data/lib/decidim/view_hooks.rb +1 -1
  517. data/lib/decidim/view_model.rb +5 -1
  518. data/lib/decidim/webpacker/thread_safe_compiler.rb +30 -0
  519. data/lib/decidim/webpacker/webpack/custom.js +15 -3
  520. data/lib/decidim/webpacker/webpacker.yml +1 -1
  521. data/lib/decidim/webpacker.rb +2 -0
  522. data/lib/devise/models/decidim_validatable.rb +1 -1
  523. data/lib/tasks/decidim_active_storage_migration_tasks.rake +1 -3
  524. data/lib/tasks/{decidim_data_portability_tasks.rake → decidim_download_your_data_tasks.rake} +9 -9
  525. data/lib/tasks/decidim_mailers_tasks.rake +23 -0
  526. data/lib/tasks/decidim_metrics_tasks.rake +1 -1
  527. data/lib/tasks/decidim_pwa_tasks.rake +18 -0
  528. data/lib/tasks/decidim_reminders_tasks.rake +17 -0
  529. data/lib/tasks/decidim_tasks.rake +6 -1
  530. data/lib/tasks/decidim_webpacker_tasks.rake +1 -1
  531. data/lib/tasks/upgrade/decidim_fix_nickname_uniqueness.rake +51 -0
  532. metadata +184 -83
  533. data/app/controllers/decidim/cookie_policy_controller.rb +0 -24
  534. data/app/helpers/decidim/cookies_helper.rb +0 -11
  535. data/app/packs/src/decidim/vizzs/renders.js +0 -9
  536. data/app/packs/src/decidim/vizzs.js +0 -1
  537. data/app/packs/stylesheets/decidim/modules/_cookie-bar.scss +0 -26
  538. data/app/services/decidim/home_activity_search.rb +0 -80
  539. data/app/services/decidim/participatory_space_search.rb +0 -54
  540. data/app/views/decidim/cookie_policy/accept.js.erb +0 -3
  541. data/app/views/decidim/export_mailer/data_portability_export.html.erb +0 -7
  542. data/lib/decidim/data_portability_serializers.rb +0 -23
  543. data/lib/decidim/middleware/rails_cookies.rb +0 -23
@@ -0,0 +1,49 @@
1
+ export const checkTitles = (parentElement, saveButton) => {
2
+ let everyItemHasTitle = true;
3
+
4
+ Array.from(parentElement.children).forEach((item) => {
5
+ const title = item.querySelector("input[type='text']").value;
6
+ const noTitleErrorSpan = item.querySelector(".no-title-error");
7
+ if (!title || title.length === 0) {
8
+ everyItemHasTitle = false;
9
+ saveButton.disabled = true;
10
+ noTitleErrorSpan.classList.add("is-visible");
11
+ item.appendChild(noTitleErrorSpan);
12
+ } else {
13
+ noTitleErrorSpan.classList.remove("is-visible");
14
+ }
15
+ });
16
+
17
+ if (everyItemHasTitle) {
18
+ saveButton.disabled = false;
19
+ }
20
+ }
21
+
22
+ export const truncateFilename = (filename, maxLength = 31) => {
23
+ if (filename.length <= maxLength) {
24
+ return filename;
25
+ }
26
+
27
+ const charactersFromBegin = Math.floor(maxLength / 2) - 3;
28
+ const charactersFromEnd = maxLength - charactersFromBegin - 3;
29
+ return `${filename.slice(0, charactersFromBegin)}...${filename.slice(-charactersFromEnd)}`;
30
+ }
31
+
32
+ export const createHiddenInput = (elClasses, elName, elValue) => {
33
+ const el = document.createElement("input");
34
+ el.type = "hidden";
35
+ if (elClasses) {
36
+ if (typeof (elClasses) === "string") {
37
+ el.className = elClasses;
38
+ } else {
39
+ el.className = elClasses.join(" ")
40
+ }
41
+ }
42
+ if (elName) {
43
+ el.name = elName;
44
+ }
45
+ if (elValue) {
46
+ el.value = elValue;
47
+ }
48
+ return el;
49
+ }
@@ -0,0 +1,89 @@
1
+ import { DirectUpload } from "@rails/activestorage";
2
+
3
+ export class Uploader {
4
+ constructor(modal, uploadItem, options) {
5
+ this.modal = modal;
6
+ this.uploadItem = uploadItem;
7
+ this.progressBar = uploadItem.querySelector(".progress-bar");
8
+ this.validationSent = false;
9
+ this.fileTooBig = false;
10
+ if (modal.options.maxFileSize && options.file.size > modal.options.maxFileSize) {
11
+ this.fileTooBig = true;
12
+ this.showError([modal.locales.file_size_too_large]);
13
+ } else {
14
+ this.upload = new DirectUpload(options.file, options.url, this);
15
+ }
16
+ }
17
+
18
+ showError(errors) {
19
+ this.progressBar.classList.add("filled");
20
+ this.progressBar.innerHTML = this.modal.locales.validation_error;
21
+ this.uploadItem.dataset.state = "error";
22
+ const errorList = this.uploadItem.querySelector(".upload-errors");
23
+ errors.forEach((error) => {
24
+ const errorItem = document.createElement("li");
25
+ errorItem.classList.add("form-error", "is-visible");
26
+ errorItem.innerHTML = error;
27
+ errorList.appendChild(errorItem);
28
+ })
29
+ }
30
+
31
+ validate(blobId) {
32
+ const callback = (data) => {
33
+ let errors = []
34
+ for (const [, value] of Object.entries(data)) {
35
+ errors = errors.concat(value);
36
+ }
37
+
38
+ this.progressBar.style.justifyContent = "center";
39
+ if (errors.length === 0) {
40
+ this.progressBar.innerHTML = this.modal.locales.uploaded;
41
+ this.uploadItem.dataset.state = "validated";
42
+ } else {
43
+ this.showError(errors);
44
+ }
45
+ this.progressBar.classList.add("filled");
46
+ }
47
+
48
+ if (!this.validationSent) {
49
+ let property = this.modal.options.addAttribute;
50
+ if (this.modal.options.titled) {
51
+ property = "file"
52
+ }
53
+ const params = new URLSearchParams({
54
+ resourceClass: this.modal.options.resourceClass,
55
+ property: property,
56
+ blob: blobId,
57
+ formClass: this.modal.options.formObjectClass
58
+ });
59
+
60
+ fetch(`/upload_validations?${params.toString()}`, {
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ "X-CSRF-Token": $("meta[name=csrf-token]").attr("content")
65
+ }
66
+ }).then((response) => response.json()).then((data) => {
67
+ callback(data);
68
+ });
69
+ this.validationSent = true;
70
+ }
71
+ }
72
+
73
+ directUploadWillStoreFileWithXHR(request) {
74
+ request.upload.addEventListener("progress", (event) => {
75
+ const progress = Math.floor(event.loaded / event.total * 100);
76
+ let width = "15%";
77
+ if (progress > 15) {
78
+ width = `${progress}%`;
79
+ }
80
+ this.progressBar.style.width = width;
81
+
82
+ if (progress === 100) {
83
+ this.progressBar.innerHTML = this.modal.locales.validating;
84
+ return;
85
+ }
86
+ this.progressBar.innerHTML = `${progress}%`;
87
+ });
88
+ }
89
+ }
@@ -9,7 +9,7 @@ export default function updateExternalDomainLinks($target) {
9
9
 
10
10
  $("a", $target).filter((_i, link) => {
11
11
  const $link = $(link);
12
- if (!$link[0].hasAttribute("href") || $link.parents(".ql-editor").length > 0) {
12
+ if (!$link[0].hasAttribute("href") || $link.parents(".editor-container").length > 0) {
13
13
  return false;
14
14
  }
15
15
 
@@ -42,13 +42,24 @@ export default class ExternalLink {
42
42
  return;
43
43
  }
44
44
 
45
- this.$link.addClass("external-link-container");
45
+ let $target = this.$link;
46
+ if (this.$link[0].hasAttribute("data-external-link-target")) {
47
+ $target = $(this.$link.data("external-link-target"), this.$link);
48
+ if ($target.length < 1) {
49
+ $target = this.$link;
50
+ }
51
+ }
52
+
53
+ $target.addClass("external-link-container");
46
54
  let spacer = "&nbsp;";
47
- if (this.$link.text().trim().length < 1) {
55
+ if (this.$link[0].hasAttribute("data-external-link-spacer")) {
56
+ spacer = this.$link.data("external-link-spacer");
57
+ } else if ($target.text().trim().length < 1) {
48
58
  // Fixes image links extra space
49
59
  spacer = "";
50
60
  }
51
- this.$link.append(`${spacer}${this.generateElement()}`);
61
+
62
+ $target.append(`${spacer}${this.generateElement()}`);
52
63
  }
53
64
 
54
65
  generateElement() {
@@ -0,0 +1,64 @@
1
+ import $ from "jquery"; // eslint-disable-line id-length
2
+
3
+ import ExternalLink from "./external_link";
4
+
5
+ describe("ExternalLink", () => {
6
+ const content = `
7
+ <div id="links">
8
+ <a href="https://decidim.org/" target="_blank">This is an external link</a>
9
+ <a href="https://decidim.org/" target="_blank"><img src="/path/to/image.png" alt=""></a>
10
+ <a href="https://decidim.org/" target="_blank" data-external-link-spacer="%%%">Custom spacer link</a>
11
+ <a href="https://decidim.org/" target="_blank" data-external-link-target=".external-wrapper"><span class="external-wrapper"></span>This is the link</a>
12
+ </div>
13
+ `;
14
+
15
+ const config = {
16
+ "icons_path": "/path/to/icons.svg"
17
+ };
18
+ window.Decidim = {
19
+ config: {
20
+ get: (key) => config[key]
21
+ }
22
+ }
23
+ const expectedIcon = '<svg class="icon icon--external-link" role="img" aria-hidden="true"><title>external-link</title><use href="/path/to/icons.svg#icon-external-link"></use></svg>';
24
+
25
+ beforeEach(() => {
26
+ $("body").html(content);
27
+ $('a[target="_blank"]').each((_i, elem) => {
28
+ const $link = $(elem);
29
+ $link.data("external-link", new ExternalLink($link));
30
+ });
31
+ });
32
+
33
+ it("adds the external link indicator to the normal external link", () => {
34
+ const $link = $("#links a")[0];
35
+
36
+ expect($link.outerHTML).toEqual(
37
+ `<a href="https://decidim.org/" target="_blank" class="external-link-container">This is an external link&nbsp;<span class="external-link-indicator">${expectedIcon}<span class="show-for-sr">(External link)</span></span></a>`
38
+ );
39
+ });
40
+
41
+ it("adds the external link indicator without a spacer to the image link", () => {
42
+ const $link = $("#links a")[1];
43
+
44
+ expect($link.outerHTML).toEqual(
45
+ `<a href="https://decidim.org/" target="_blank" class="external-link-container"><img src="/path/to/image.png" alt=""><span class="external-link-indicator">${expectedIcon}<span class="show-for-sr">(External link)</span></span></a>`
46
+ );
47
+ });
48
+
49
+ it("adds the external link indicator with a custom spacer to the link with the custom spacer configuration", () => {
50
+ const $link = $("#links a")[2];
51
+
52
+ expect($link.outerHTML).toEqual(
53
+ `<a href="https://decidim.org/" target="_blank" data-external-link-spacer="%%%" class="external-link-container">Custom spacer link%%%<span class="external-link-indicator">${expectedIcon}<span class="show-for-sr">(External link)</span></span></a>`
54
+ );
55
+ });
56
+
57
+ it("adds the external link indicator with a custom container to the link with the custom container configuration", () => {
58
+ const $link = $("#links a")[3];
59
+
60
+ expect($link.outerHTML).toEqual(
61
+ `<a href="https://decidim.org/" target="_blank" data-external-link-target=".external-wrapper"><span class="external-wrapper external-link-container"><span class="external-link-indicator">${expectedIcon}<span class="show-for-sr">(External link)</span></span></span>This is the link</a>`
62
+ );
63
+ });
64
+ });
@@ -1,43 +1,20 @@
1
- import Tribute from "src/decidim/vendor/tribute"
1
+ import AutoComplete from "src/decidim/autocomplete";
2
2
 
3
3
  $(() => {
4
4
  $("[data-decidim-geocoding]").each((_i, el) => {
5
5
  const $input = $(el);
6
- const $fieldContainer = $input.parent();
7
6
 
8
- $fieldContainer.addClass("has-tribute");
9
-
10
- const tribute = new Tribute(
11
- {
12
- autocompleteMode: true,
13
- // autocompleteSeparator: / \+ /, // See below, requires Tribute update
14
- allowSpaces: true,
15
- positionMenu: false,
16
- replaceTextSuffix: "",
17
- menuContainer: $fieldContainer.get(0),
18
- noMatchTemplate: null,
19
- values: (text, cb) => {
20
- $input.trigger("geocoder-suggest.decidim", [text, cb]);
21
- }
7
+ const autoComplete = new AutoComplete(el, {
8
+ mode: "single",
9
+ dataMatchKeys: ["value"],
10
+ dataSource: (query, callback) => {
11
+ $input.trigger("geocoder-suggest.decidim", [query, callback]);
22
12
  }
23
- );
24
-
25
- // Port https://github.com/zurb/tribute/pull/406
26
- // This changes the autocomplete separator from space to " + " so that
27
- // we can do searches such as "streetname 4" including a space. Otherwise
28
- // this would do two separate searches for "streetname" and "4".
29
- tribute.range.getLastWordInText = (text) => {
30
- const final = text.replace(/\u00A0/g, " ");
31
- const wordsArray = final.split(/ \+ /);
32
- const worldsCount = wordsArray.length - 1;
33
-
34
- return wordsArray[worldsCount].trim();
35
- };
36
-
37
- tribute.attach($input.get(0));
13
+ });
14
+ el.addEventListener("selection", autoComplete);
38
15
 
39
- $input.on("tribute-replaced", function(ev) {
40
- const selectedItem = ev.detail.item.original;
16
+ $input.on("selection", (event) => {
17
+ const selectedItem = event.detail.selection.value;
41
18
  $input.trigger("geocoder-suggest-select.decidim", [selectedItem]);
42
19
 
43
20
  // Not all geocoding autocomplete APIs include the coordinates in the
@@ -48,7 +25,5 @@ $(() => {
48
25
  $input.trigger("geocoder-suggest-coordinates.decidim", [selectedItem.coordinates]);
49
26
  }
50
27
  });
51
-
52
- $input.data("geocoder-tribute", tribute);
53
28
  });
54
29
  });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Makes the #select-identity-button to open a popup for the user to
3
- * select with which identity he wants to perform an action.
3
+ * select with which identity they want to perform an action.
4
4
  *
5
5
  */
6
6
  $(() => {
@@ -1,11 +1,11 @@
1
- import moment from "moment"
1
+ import dayjs from "dayjs"
2
2
 
3
3
  $(() => {
4
4
  const $impersonationWarning = $(".impersonation-warning");
5
5
  if ($impersonationWarning.length) {
6
- const endsAt = moment($impersonationWarning.data("session-ends-at"));
6
+ const endsAt = dayjs($impersonationWarning.data("session-ends-at"));
7
7
  const exitInterval = setInterval(() => {
8
- const diff = (endsAt - moment()) / 60000;
8
+ const diff = (endsAt - dayjs()) / 60000;
9
9
  const diffInMinutes = Math.round(diff);
10
10
  $impersonationWarning.find(".minutes").html(diffInMinutes);
11
11
 
@@ -1,4 +1,14 @@
1
1
  const COUNT_KEY = "%count%";
2
+ // How often SR announces the message in relation to maximum characters. E.g.
3
+ // if max characters is 1000, screen reader announces the remaining characters
4
+ // every 100 (= 0.1 * 1000) characters. This will be "floored" to the closest
5
+ // 100 if the maximum characters > 100. E.g. if max characters is 5500, the
6
+ // threshold is 500 (= Math.floor(550 / 100) * 100). With 100 or less
7
+ // characters, this ratio is omitted and the announce threshold is always set to
8
+ // 10.
9
+ const SR_ANNOUNCE_THRESHOLD_RATIO = 0.1;
10
+ // The number of characters left after which every keystroke will be announced.
11
+ const SR_ANNOUNCE_EVERY_THRESHOLD = 10;
2
12
  const DEFAULT_MESSAGES = {
3
13
  charactersAtLeast: {
4
14
  one: `at least ${COUNT_KEY} character`,
@@ -21,15 +31,35 @@ export default class InputCharacterCounter {
21
31
  this.$target = $(this.$input.data("remaining-characters"));
22
32
  this.minCharacters = parseInt(this.$input.attr("minlength"), 10);
23
33
  this.maxCharacters = parseInt(this.$input.attr("maxlength"), 10);
34
+ this.describeByCounter = typeof this.$input.attr("aria-describedby") === "undefined";
24
35
 
25
- if (this.$target.length < 1) {
26
- let targetId = null;
36
+ // Define the closest length for the input "gaps" defined by the threshold.
37
+ if (this.maxCharacters > 10) {
38
+ if (this.maxCharacters > 100) {
39
+ this.announceThreshold = Math.floor(this.maxCharacters * SR_ANNOUNCE_THRESHOLD_RATIO / 100) * 100;
40
+ } else {
41
+ this.announceThreshold = 10;
42
+ }
43
+
44
+ // The number of characters left after which every keystroke will be announced.
45
+ this.announceEveryThreshold = SR_ANNOUNCE_EVERY_THRESHOLD;
46
+ } else {
47
+ this.announceThreshold = 1;
48
+ this.announceEveryThreshold = 1;
49
+ }
50
+
51
+ let targetId = this.$target.attr("id");
52
+ if (typeof targetId === "undefined") {
27
53
  if (this.$input.attr("id") && this.$input.attr("id").length > 0) {
28
54
  targetId = `${this.$input.attr("id")}_characters`;
29
55
  } else {
30
56
  targetId = `characters_${Math.random().toString(36).substr(2, 9)}`;
31
57
  }
58
+ }
32
59
 
60
+ if (this.$target.length > 0) {
61
+ this.$target.attr("id", targetId)
62
+ } else {
33
63
  this.$target = $(`<span id="${targetId}" class="form-input-extra-before" />`)
34
64
 
35
65
  // If input is a hidden for WYSIWYG editor add it at the end
@@ -48,11 +78,35 @@ export default class InputCharacterCounter {
48
78
  }
49
79
  }
50
80
 
81
+ this.updateInputLength();
82
+ this.previousInputLength = this.inputLength;
83
+
51
84
  if (this.$target.length > 0 && (this.maxCharacters > 0 || this.minCharacters > 0)) {
85
+ // Create the screen reader target element. We don't want to constantly
86
+ // announce every change to screen reader, only occasionally.
87
+ this.$srTarget = $(
88
+ `<span role="status" id="${targetId}_sr" class="show-for-sr remaining-character-count-sr" />`
89
+ );
90
+ this.$target.before(this.$srTarget);
91
+ this.$target.attr("aria-hidden", "true");
92
+ this.setDescribedBy(true);
93
+
52
94
  this.bindEvents();
53
95
  }
54
96
  }
55
97
 
98
+ setDescribedBy(active) {
99
+ if (!this.describeByCounter) {
100
+ return;
101
+ }
102
+
103
+ if (active) {
104
+ this.$input.attr("aria-describedby", this.$srTarget.attr("id"));
105
+ } else {
106
+ this.$input.removeAttr("aria-describedby");
107
+ }
108
+ }
109
+
56
110
  bindEvents() {
57
111
  // In WYSIWYG editors (Quill) we need to find the active editor from the
58
112
  // DOM node. Quill has the experimental "find" method that should work
@@ -69,17 +123,109 @@ export default class InputCharacterCounter {
69
123
  this.$input.on("keyup", () => {
70
124
  this.updateStatus();
71
125
  });
126
+ this.$input.on("input", () => {
127
+ this.updateInputLength();
128
+ this.checkScreenReaderUpdate();
129
+ // If the input is "described by" the character counter, some screen
130
+ // readers (NVDA) announce the status twice when it is updated. By
131
+ // removing the aria-describedby attribute while the user is typing makes
132
+ // the screen reader announce the status only once.
133
+ this.setDescribedBy(false);
134
+ });
135
+ this.$input.on("focus", () => {
136
+ this.updateScreenReaderStatus();
137
+ });
138
+ this.$input.on("blur", () => {
139
+ this.updateScreenReaderStatus();
140
+ this.setDescribedBy(true);
141
+ });
72
142
  if (this.$input.get(0) !== null) {
73
143
  this.$input.get(0).addEventListener("emoji.added", () => {
74
144
  this.updateStatus();
75
145
  });
76
146
  }
77
147
  this.updateStatus();
148
+ this.updateScreenReaderStatus();
78
149
  }
79
150
 
80
- updateStatus() {
81
- const numCharacters = this.$input.val().length;
151
+ getInputLength() {
152
+ return this.inputLength;
153
+ }
154
+
155
+ updateInputLength() {
156
+ this.previousInputLength = this.inputLength;
157
+ this.inputLength = this.$input.val().length;
158
+ }
159
+
160
+ /**
161
+ * This compares the current inputLength to the previous value and decides
162
+ * whether the user is currently adding or deleting characters from the view.
163
+ *
164
+ * @returns {String} The input direction either "ins" for insert or "del" for
165
+ * delete.
166
+ */
167
+ getInputDirection() {
168
+ if (this.inputLength < this.previousInputLength) {
169
+ return "del";
170
+ }
171
+
172
+ return "ins";
173
+ }
174
+
175
+ getScreenReaderLength() {
176
+ const currentLength = this.getInputLength();
177
+ if (this.maxCharacters < 10) {
178
+ return currentLength;
179
+ } else if (this.maxCharacters - currentLength <= this.announceEveryThreshold) {
180
+ return currentLength;
181
+ }
182
+
183
+ const srLength = currentLength - currentLength % this.announceThreshold;
184
+
185
+ // Prevent the screen reader telling too many characters left if the user
186
+ // deletes a characters. This can cause confusing experience e.g. when the
187
+ // user is closing the maximum amount of characters, so if the previous
188
+ // announcement was "10 characters left" and the user removes one character,
189
+ // the screen reader would announce "100 characters left" next time (when
190
+ // they actually have only 11 characters left). Similar when they are
191
+ // deleting a character at 900 characters, the screen reader would announce
192
+ // "1000 characters left" even when they only have 901 characters left.
193
+ if (this.getInputDirection() === "del") {
194
+ // The first branch makes sure that if the SR length matches the actual
195
+ // length, it will be always announced.
196
+ if (srLength === currentLength) {
197
+ return srLength;
198
+ // The second branch checks that if we are at the final threshold, we
199
+ // should not announce "0 characters left" when the user deletes more than
200
+ // the "announce after every stroke" limit (this.announceEveryThreshold).
201
+ } else if (this.maxCharacters - srLength === this.announceThreshold) {
202
+ return this.announcedAt || currentLength;
203
+ // The third branch checks that when deleting characters, we should
204
+ // announce the next threshold to get accurate annoucement. E.g. when we
205
+ // have 750 characters left and the user deletes 100 characters at once,
206
+ // we should announce "700 characters left" after that deletion.
207
+ } else if (srLength < currentLength) {
208
+ return srLength + this.announceThreshold;
209
+ }
210
+ // This fixes an issue in the following situation:
211
+ // 1. 750 characters left
212
+ // 2. Delete 100 characters in a row
213
+ // 3. SR: "800 characters left" (actual 850)
214
+ // 4. Type one additional character
215
+ // 5. Without this, SR would announce "900 characters left" = confusing
216
+ } else if (srLength < this.announcedAt) {
217
+ return this.announcedAt;
218
+ }
219
+
220
+ return srLength;
221
+ }
222
+
223
+ getMessages(currentLength = null) {
82
224
  const showMessages = [];
225
+ let inputLength = currentLength;
226
+ if (inputLength === null) {
227
+ inputLength = this.getInputLength()
228
+ }
83
229
 
84
230
  if (this.minCharacters > 0) {
85
231
  let message = MESSAGES.charactersAtLeast.other;
@@ -90,7 +236,7 @@ export default class InputCharacterCounter {
90
236
  }
91
237
 
92
238
  if (this.maxCharacters > 0) {
93
- const remaining = this.maxCharacters - numCharacters;
239
+ const remaining = this.maxCharacters - inputLength;
94
240
  let message = MESSAGES.charactersLeft.other;
95
241
  if (remaining === 1) {
96
242
  message = MESSAGES.charactersLeft.one;
@@ -101,12 +247,36 @@ export default class InputCharacterCounter {
101
247
  showMessages.push(message.replace(COUNT_KEY, remaining));
102
248
  }
103
249
 
104
- this.$target.text(showMessages.join(", "));
250
+ return showMessages;
251
+ }
252
+
253
+ updateStatus() {
254
+ this.$target.text(this.getMessages().join(", "));
255
+ }
256
+
257
+ checkScreenReaderUpdate() {
258
+ if (this.maxCharacters < 1) {
259
+ return;
260
+ }
261
+
262
+ const currentLength = this.getScreenReaderLength();
263
+ if (currentLength === this.announcedAt) {
264
+ return;
265
+ }
266
+
267
+ this.announcedAt = currentLength;
268
+ this.updateScreenReaderStatus(currentLength);
269
+ }
270
+
271
+ updateScreenReaderStatus(currentLength = null) {
272
+ this.$srTarget.text(this.getMessages(currentLength).join(", "));
105
273
  }
106
274
  }
107
275
 
108
276
  const createCharacterCounter = ($input) => {
109
- $input.data("remaining-characters-counter", new InputCharacterCounter($input));
277
+ if (typeof $input !== "undefined" && $input.length) {
278
+ $input.data("remaining-characters-counter", new InputCharacterCounter($input));
279
+ }
110
280
  }
111
281
 
112
282
  $(() => {