decidim-core 0.26.2 → 0.26.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of decidim-core might be problematic. Click here for more details.

Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/amendable/announcement_cell.rb +1 -1
  3. data/app/cells/decidim/card_m_cell.rb +1 -1
  4. data/app/cells/decidim/content_blocks/cta/show.erb +1 -1
  5. data/app/cells/decidim/content_blocks/hero/show.erb +1 -1
  6. data/app/cells/decidim/content_blocks/highlighted_content_banner/show.erb +1 -1
  7. data/app/cells/decidim/content_blocks/stats_cell.rb +1 -0
  8. data/app/controllers/concerns/decidim/resource_versions_concern.rb +4 -0
  9. data/app/controllers/decidim/devise/invitations_controller.rb +9 -2
  10. data/app/controllers/decidim/devise/registrations_controller.rb +5 -1
  11. data/app/events/decidim/resource_endorsed_event.rb +2 -1
  12. data/app/forms/decidim/account_form.rb +8 -7
  13. data/app/forms/decidim/amendable/form.rb +2 -1
  14. data/app/forms/decidim/registration_form.rb +11 -5
  15. data/app/helpers/decidim/filters_helper.rb +5 -1
  16. data/app/mailers/decidim/notification_mailer.rb +1 -0
  17. data/app/models/decidim/action_log.rb +9 -9
  18. data/app/models/decidim/user_base_entity.rb +1 -0
  19. data/app/packs/src/decidim/editor/clipboard_override.js +143 -0
  20. data/app/packs/src/decidim/editor/clipboard_utilities.js +119 -0
  21. data/app/packs/src/decidim/editor/linebreak_module.js +0 -8
  22. data/app/packs/src/decidim/editor.js +9 -2
  23. data/app/packs/src/decidim/map/factory.js +3 -1
  24. data/app/packs/src/decidim/map/legacy.js +2 -2
  25. data/app/packs/src/decidim/map.js +2 -2
  26. data/app/packs/stylesheets/decidim/modules/_cards.scss +2 -0
  27. data/app/packs/stylesheets/decidim/modules/_comments.scss +2 -0
  28. data/app/packs/stylesheets/decidim/modules/_forms.scss +5 -0
  29. data/app/permissions/decidim/permissions.rb +4 -2
  30. data/app/presenters/decidim/home_stats_presenter.rb +11 -4
  31. data/app/presenters/decidim/stats_presenter.rb +7 -8
  32. data/app/presenters/decidim/user_presenter.rb +12 -4
  33. data/app/services/decidim/activity_search.rb +1 -0
  34. data/app/validators/etiquette_validator.rb +7 -3
  35. data/app/views/decidim/data_portability/show.html.erb +1 -1
  36. data/app/views/decidim/notification_mailer/event_received.html.erb +1 -1
  37. data/app/views/decidim/notifications_settings/show.html.erb +49 -51
  38. data/app/views/decidim/user_interests/show.html.erb +11 -13
  39. data/config/locales/ar.yml +0 -24
  40. data/config/locales/bg.yml +1 -23
  41. data/config/locales/ca.yml +5 -25
  42. data/config/locales/cs.yml +13 -33
  43. data/config/locales/de.yml +64 -25
  44. data/config/locales/el.yml +0 -22
  45. data/config/locales/en.yml +2 -22
  46. data/config/locales/es-MX.yml +5 -25
  47. data/config/locales/es-PY.yml +5 -25
  48. data/config/locales/es.yml +10 -30
  49. data/config/locales/eu.yml +5 -26
  50. data/config/locales/fi-plain.yml +2 -22
  51. data/config/locales/fi.yml +2 -22
  52. data/config/locales/fr-CA.yml +4 -24
  53. data/config/locales/fr.yml +11 -31
  54. data/config/locales/ga-IE.yml +1 -5
  55. data/config/locales/gl.yml +0 -24
  56. data/config/locales/gn-PY.yml +1 -0
  57. data/config/locales/hu.yml +173 -23
  58. data/config/locales/id-ID.yml +0 -24
  59. data/config/locales/is-IS.yml +2 -1
  60. data/config/locales/it.yml +3 -25
  61. data/config/locales/ja.yml +4 -24
  62. data/config/locales/lb.yml +1 -23
  63. data/config/locales/lo-LA.yml +1 -0
  64. data/config/locales/lt.yml +1780 -0
  65. data/config/locales/lv.yml +0 -22
  66. data/config/locales/nl.yml +3 -25
  67. data/config/locales/no.yml +1 -23
  68. data/config/locales/oc-FR.yml +1 -0
  69. data/config/locales/pl.yml +50 -23
  70. data/config/locales/pt-BR.yml +3 -25
  71. data/config/locales/pt.yml +2 -24
  72. data/config/locales/ro-RO.yml +1 -23
  73. data/config/locales/ru.yml +0 -5
  74. data/config/locales/sk.yml +1 -26
  75. data/config/locales/sv.yml +2 -23
  76. data/config/locales/tr-TR.yml +2 -24
  77. data/config/locales/uk.yml +1 -2
  78. data/config/locales/zh-CN.yml +2 -24
  79. data/config/routes.rb +20 -2
  80. data/lib/decidim/attributes/localized_date.rb +9 -1
  81. data/lib/decidim/attributes/time_with_zone.rb +13 -1
  82. data/lib/decidim/content_parsers/hashtag_parser.rb +1 -1
  83. data/lib/decidim/core/engine.rb +1 -6
  84. data/lib/decidim/core/test/shared_examples/mcell_examples.rb +17 -0
  85. data/lib/decidim/core/test/shared_examples/resource_endorsed_event_examples.rb +60 -0
  86. data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +40 -0
  87. data/lib/decidim/core/test/shared_examples/with_endorsable_permissions_examples.rb +1 -1
  88. data/lib/decidim/core/test.rb +3 -0
  89. data/lib/decidim/core/version.rb +1 -1
  90. data/lib/decidim/events/simple_event.rb +8 -1
  91. data/lib/decidim/form_builder.rb +8 -1
  92. data/lib/decidim/has_resource_permission.rb +0 -2
  93. data/lib/decidim/map/provider/dynamic_map/here.rb +46 -1
  94. data/lib/decidim/nicknamizable.rb +1 -1
  95. data/lib/decidim/resourceable.rb +5 -4
  96. data/lib/decidim/settings_manifest.rb +1 -1
  97. data/lib/decidim/translatable_attributes.rb +8 -1
  98. metadata +14 -8
  99. data/app/packs/images/decidim/gamification/badges/decidim_gamification_badges_invitations.svg +0 -1
  100. data/app/views/decidim/devise/registrations/edit.html.erb +0 -41
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18193603bd469c0ea5937875b577f4cc5439727b2ad1759e60ea725d029e18f1
4
- data.tar.gz: 91c95bec2cdc7af5ad17a5d5ccd165817516fbaf0ed46051c198d3b8b5b1321b
3
+ metadata.gz: 1863adec9ae8ce9d4d528f247a2f18561de3618eacb84e50c03fae6a89fb2d6e
4
+ data.tar.gz: 032b9cce96757af5e5d87d687f64231a1630fdc003973b3b16cc4044f7f667bb
5
5
  SHA512:
6
- metadata.gz: 71b39ec5cae4751ab21048ef686cd6b3704e8d5469d9ba608fcf163944ebf83fc100d1a47c59d4eae4b4c01af4d5074918113a8cf533151e954eb97defa14bf9
7
- data.tar.gz: 72011b7df988f022988d64892177f9f4b28338019152d0a2ce46f489696256acdcd095d0da83346e41d70cdeb72c72ef7e21c0e701b50a1d4b35a6de8c51840d
6
+ metadata.gz: 44579a2195e6d79ca6e5d78a1695815c4db2d2dd850b9a94e819d5219cda61cad771aa172ad99628bb9460e79a99b02fa4cd72fb377649b565b22ad010eb6924
7
+ data.tar.gz: 959d4d1552af3fd6b25fb1fb27292d8d107c260bedddd5a49dce829f6271772fd8a2c043d401cdc5061a615e1de71157d0b084eeab3b794e9aa3373451ad7ef8
@@ -39,7 +39,7 @@ module Decidim::Amendable
39
39
  end
40
40
 
41
41
  def proposal_link(resource = model.amendable, text = nil)
42
- text ||= %(<strong>#{present(model.amendable).title}</strong>)
42
+ text ||= %(<strong>#{decidim_sanitize(present(model.amendable).title, strip_tags: true)}</strong>)
43
43
  link_to resource_locator(resource).path do
44
44
  text
45
45
  end
@@ -57,7 +57,7 @@ module Decidim
57
57
  end
58
58
 
59
59
  def title
60
- translated_attribute model.title
60
+ decidim_html_escape(translated_attribute(model.title))
61
61
  end
62
62
 
63
63
  def description
@@ -1,5 +1,5 @@
1
1
  <section class="section">
2
- <div class="expanded hero" style="background-image:url(<%= background_image %>);">
2
+ <div class="expanded hero" style="background-image:url('<%= background_image %>');">
3
3
  <div class="hero__container">
4
4
  <div class="row">
5
5
  <div class="columns small-centered medium-6 text-center">
@@ -1,4 +1,4 @@
1
- <section class="extended hero home-section" style="background-image:url(<%= background_image %>);">
1
+ <section class="extended hero home-section" style="background-image:url('<%= background_image %>');">
2
2
  <div class="hero__container">
3
3
  <div class="row">
4
4
  <div class="columns small-centered large-10">
@@ -1,5 +1,5 @@
1
1
  <section class="extended highligted-content-banner home-section"
2
- style="background-image:url(<%= current_organization.attached_uploader(:highlighted_content_banner_image).path %>);">
2
+ style="background-image:url('<%= current_organization.attached_uploader(:highlighted_content_banner_image).path %>');">
3
3
  <div class="highligted-content-banner__container">
4
4
  <div class="row">
5
5
  <div class="columns large-10">
@@ -16,6 +16,7 @@ module Decidim
16
16
  def cache_hash
17
17
  hash = []
18
18
  hash.push(I18n.locale)
19
+ hash.push(current_organization.cache_key)
19
20
  hash.join(Decidim.cache_key_separator)
20
21
  end
21
22
  end
@@ -10,6 +10,10 @@ module Decidim
10
10
  helper Decidim::TraceabilityHelper
11
11
  helper_method :current_version, :versioned_resource
12
12
 
13
+ def show
14
+ raise ActionController::RoutingError, "Not found" unless current_version
15
+ end
16
+
13
17
  private
14
18
 
15
19
  # Overwrite this method in your controller to define how to find the
@@ -19,7 +19,7 @@ module Decidim
19
19
  # invitation. Using the param `invite_redirect` we can redirect the user
20
20
  # to a custom path after it has accepted the invitation.
21
21
  def after_accept_path_for(resource)
22
- params[:invite_redirect] || after_sign_in_path_for(resource)
22
+ invite_redirect_path || after_sign_in_path_for(resource)
23
23
  end
24
24
 
25
25
  # When a managed user accepts the invitation is promoted to non-managed user.
@@ -30,7 +30,6 @@ module Decidim
30
30
  resource.update!(newsletter_notifications_at: Time.current) if update_resource_params[:newsletter_notifications]
31
31
  resource.update!(managed: false) if resource.managed?
32
32
  resource.update!(accepted_tos_version: resource.organization.tos_version)
33
- Decidim::Gamification.increment_score(resource.invited_by, :invitations) if resource.invited_by
34
33
  end
35
34
 
36
35
  resource
@@ -38,6 +37,14 @@ module Decidim
38
37
 
39
38
  protected
40
39
 
40
+ def invite_redirect_path
41
+ path = params[:invite_redirect]
42
+ return unless path
43
+ return unless path.starts_with?(%r{^/[a-z0-9]+})
44
+
45
+ path
46
+ end
47
+
41
48
  def configure_permitted_parameters
42
49
  devise_parameter_sanitizer.permit(:accept_invitation, keys: [:nickname, :tos_agreement, :newsletter_notifications])
43
50
  end
@@ -37,7 +37,7 @@ module Decidim
37
37
  end
38
38
 
39
39
  on(:invalid) do
40
- flash.now[:alert] = @form.errors[:base].join(", ") if @form.errors[:base].any?
40
+ flash.now[:alert] = @form.errors.full_messages.join(", ") if @form.errors.full_messages.any?
41
41
  render :new
42
42
  end
43
43
  end
@@ -58,6 +58,10 @@ module Decidim
58
58
  super(hash)
59
59
  resource.organization = current_organization
60
60
  end
61
+
62
+ def devise_mapping
63
+ ::Devise.mappings[:user]
64
+ end
61
65
  end
62
66
  end
63
67
  end
@@ -15,7 +15,8 @@ module Decidim
15
15
  end
16
16
 
17
17
  def resource_text
18
- resource.body
18
+ return resource.body if resource.respond_to? :body
19
+ return resource.description if resource.respond_to? :description
19
20
  end
20
21
 
21
22
  def resource_type
@@ -19,9 +19,9 @@ module Decidim
19
19
  attribute :personal_url
20
20
  attribute :about
21
21
 
22
- validates :name, presence: true
23
- validates :email, presence: true, 'valid_email_2/email': { disposable: true }
24
- validates :nickname, presence: true, format: Decidim::User::REGEXP_NICKNAME
22
+ validates :name, presence: true, format: { with: Decidim::User::REGEXP_NAME }
23
+ validates :email, presence: true, "valid_email_2/email": { disposable: true }
24
+ validates :nickname, presence: true, format: { with: Decidim::User::REGEXP_NICKNAME }
25
25
 
26
26
  validates :nickname, length: { maximum: Decidim::User.nickname_max_length, allow_blank: true }
27
27
  validates :password, confirmation: true
@@ -50,7 +50,7 @@ module Decidim
50
50
  end
51
51
 
52
52
  def unique_email
53
- return true if Decidim::User.where(
53
+ return true if Decidim::UserBaseEntity.where(
54
54
  organization: context.current_organization,
55
55
  email: email
56
56
  ).where.not(id: context.current_user.id).empty?
@@ -60,9 +60,10 @@ module Decidim
60
60
  end
61
61
 
62
62
  def unique_nickname
63
- return true if Decidim::User.where(
64
- organization: context.current_organization,
65
- nickname: nickname
63
+ return true if Decidim::UserBaseEntity.where(
64
+ "decidim_organization_id = ? AND LOWER(nickname) = ? ",
65
+ context.current_organization.id,
66
+ nickname.downcase
66
67
  ).where.not(id: context.current_user.id).empty?
67
68
 
68
69
  errors.add :nickname, :taken
@@ -66,7 +66,8 @@ module Decidim
66
66
  errors = amendable_form_errors.details[key] - @original_form.errors.details[key]
67
67
 
68
68
  errors.map do |hash|
69
- @amendable_form.errors.add(key, hash[:error]) unless @amendable_form.errors.details[key].include? error: hash[:error]
69
+ error = hash.delete(:error)
70
+ @amendable_form.errors.add(key, error, **hash) unless @amendable_form.errors.details[key].include?(error: error)
70
71
  end
71
72
  end
72
73
  end
@@ -14,9 +14,9 @@ module Decidim
14
14
  attribute :tos_agreement, Boolean
15
15
  attribute :current_locale, String
16
16
 
17
- validates :name, presence: true
18
- validates :nickname, presence: true, format: /\A[\w\-]+\z/, length: { maximum: Decidim::User.nickname_max_length }
19
- validates :email, presence: true, 'valid_email_2/email': { disposable: true }
17
+ validates :name, presence: true, format: { with: Decidim::User::REGEXP_NAME }
18
+ validates :nickname, presence: true, format: { with: Decidim::User::REGEXP_NICKNAME }, length: { maximum: Decidim::User.nickname_max_length }
19
+ validates :email, presence: true, "valid_email_2/email": { disposable: true }
20
20
  validates :password, confirmation: true
21
21
  validates :password, password: { name: :name, email: :email, username: :nickname }
22
22
  validates :password_confirmation, presence: true
@@ -35,11 +35,17 @@ module Decidim
35
35
  private
36
36
 
37
37
  def email_unique_in_organization
38
- errors.add :email, :taken if User.no_active_invitation.find_by(email: email, organization: current_organization).present?
38
+ errors.add :email, :taken if valid_users.find_by(email: email, organization: current_organization).present?
39
39
  end
40
40
 
41
41
  def nickname_unique_in_organization
42
- errors.add :nickname, :taken if User.no_active_invitation.find_by(nickname: nickname, organization: current_organization).present?
42
+ return false unless nickname
43
+
44
+ errors.add :nickname, :taken if valid_users.find_by("LOWER(nickname)= ? AND decidim_organization_id = ?", nickname.downcase, current_organization.id).present?
45
+ end
46
+
47
+ def valid_users
48
+ UserBaseEntity.where(invitation_token: nil)
43
49
  end
44
50
 
45
51
  def no_pending_invitations_exist
@@ -24,7 +24,11 @@ module Decidim
24
24
  remote: true,
25
25
  html: { id: nil }.merge(html_options)
26
26
  ) do |form|
27
- yield form
27
+ # Cannot use `concat()` here because it's not available in cells
28
+ inner = []
29
+ inner << hidden_field_tag("per_page", params[:per_page], id: nil) if params[:per_page]
30
+ inner << capture { yield form }
31
+ inner.join.html_safe
28
32
  end
29
33
  end
30
34
  end
@@ -5,6 +5,7 @@ module Decidim
5
5
  # a events are received.
6
6
  class NotificationMailer < Decidim::ApplicationMailer
7
7
  helper Decidim::ResourceHelper
8
+ helper Decidim::SanitizeHelper
8
9
 
9
10
  def event_received(event, event_class_name, resource, user, user_role, extra) # rubocop:disable Metrics/ParameterLists
10
11
  with_user(user) do
@@ -130,16 +130,16 @@ module Decidim
130
130
  end
131
131
 
132
132
  # Whether this activity or log is visible for a given user (can also be nil)
133
- #
134
- # Returns a True/False.
135
133
  def visible_for?(user)
136
- return false if resource_lazy.blank?
137
- return false if participatory_space_lazy.blank?
138
- return false if resource_lazy.respond_to?(:deleted?) && resource_lazy.deleted?
139
- return false if resource_lazy.respond_to?(:hidden?) && resource_lazy.hidden?
140
- return false if resource_lazy.respond_to?(:can_participate?) && !resource_lazy.can_participate?(user)
141
-
142
- true
134
+ resource_lazy.present? &&
135
+ participatory_space_lazy.present? &&
136
+ !resource_lazy.try(:deleted?) &&
137
+ !resource_lazy.try(:hidden?) &&
138
+ (!resource_lazy.respond_to?(:can_participate?) || resource_lazy.try(:can_participate?, user))
139
+ rescue NameError => e
140
+ Rails.logger.warn "Failed resource for #{self.class.name}(id=#{id}): #{e.message}"
141
+
142
+ false
143
143
  end
144
144
  end
145
145
  end
@@ -30,6 +30,7 @@ module Decidim
30
30
 
31
31
  scope :blocked, -> { where(blocked: true) }
32
32
  scope :not_blocked, -> { where(blocked: false) }
33
+ scope :available, -> { where(deleted_at: nil, blocked: false, managed: false) }
33
34
 
34
35
  # Public: Returns a collection with all the public entities this user is following.
35
36
  #
@@ -0,0 +1,143 @@
1
+ /* eslint max-lines: ["error", 350] */
2
+
3
+ /**
4
+ * Quill clipboard utilities
5
+ *
6
+ * Copyright (c) 2017, Slab
7
+ * Copyright (c) 2014, Jason Chen
8
+ * Copyright (c) 2013, salesforce.com
9
+ * BSD 3-Clause "New" or "Revised" License
10
+ *
11
+ * Extends the original version from https://github.com/quilljs/quill
12
+ * Relevant parts converted from TypeScript to JavaScript
13
+ */
14
+
15
+ import CodeBlock from "quill/formats/code";
16
+ import { matchNewline, matchBreak, deltaEndsWith, traverse } from "src/decidim/editor/clipboard_utilities";
17
+
18
+ const Delta = Quill.import("delta");
19
+ const Clipboard = Quill.import("modules/clipboard");
20
+
21
+ /**
22
+ * Pasting bold text is broken in Quill as described at:
23
+ * https://github.com/quilljs/quill/issues/306
24
+ *
25
+ * The reason is that the `<strong>` nodes are not recognized as bold types.
26
+ * This override fixes the issue by introducing parts of the newer Quill code
27
+ * at GitHub and defining the `<strong>` tags as bold tags.
28
+ */
29
+ export default class ClipboardOverride extends Clipboard {
30
+ constructor(quill, options) {
31
+ super(quill, options);
32
+ this.overrideMatcher("b", "b, strong");
33
+ this.overrideMatcher("br", "br", matchBreak);
34
+
35
+ // Change the matchNewLine matchers to the newer version
36
+ this.matchers[1][1] = matchNewline;
37
+ this.matchers[3][1] = matchNewline;
38
+
39
+ // Remove `matchSpacing` as that is also removed in the newer versions.
40
+ this.removeMatcher(Node.ELEMENT_NODE, "matchSpacing");
41
+ }
42
+
43
+ overrideMatcher(originalSelector, newSelector, newMatcher = null) {
44
+ const idx = this.matchers.findIndex((item) => item[0] === originalSelector);
45
+ if (idx >= 0) {
46
+ this.matchers[idx][0] = newSelector;
47
+ if (newMatcher) {
48
+ this.matchers[idx][1] = newMatcher;
49
+ }
50
+ }
51
+ }
52
+
53
+ removeMatcher(selector, matcherName) {
54
+ const idx = this.matchers.findIndex((item) => item[0] === selector && item[1].name === matcherName);
55
+ if (idx >= 0) {
56
+ this.matchers.splice(idx, 1);
57
+ }
58
+ }
59
+
60
+ onPaste(ev) {
61
+ if (ev.defaultPrevented || !this.quill.isEnabled()) {
62
+ return;
63
+ }
64
+ ev.preventDefault();
65
+ const range = this.quill.getSelection(true);
66
+ if (range === null) {
67
+ return;
68
+ }
69
+ const html = ev.clipboardData.getData("text/html");
70
+ const text = ev.clipboardData.getData("text/plain");
71
+ const files = Array.from(ev.clipboardData.files || []);
72
+ if (!html && files.length > 0) {
73
+ this.quill.uploader.upload(range, files);
74
+ return;
75
+ }
76
+ if (html && files.length > 0) {
77
+ const doc = new DOMParser().parseFromString(html, "text/html");
78
+ if (
79
+ doc.body.childElementCount === 1 &&
80
+ doc.body.firstElementChild.tagName === "IMG"
81
+ ) {
82
+ this.quill.uploader.upload(range, files);
83
+ return;
84
+ }
85
+ }
86
+ this.onPasteRange(range, { html, text });
87
+ }
88
+
89
+ onPasteRange(range, { text, html }) {
90
+ const formats = this.quill.getFormat(range.index);
91
+ const pastedDelta = this.convertPaste({ text, html }, formats);
92
+ // debug.log('onPaste", pastedDelta, { text, html });
93
+ const delta = new Delta().retain(range.index).delete(range.length).concat(pastedDelta);
94
+ this.quill.updateContents(delta, Quill.sources.USER);
95
+ // range.length contributes to delta.length()
96
+ this.quill.setSelection(
97
+ delta.length() - range.length,
98
+ Quill.sources.SILENT,
99
+ );
100
+ this.quill.scrollIntoView();
101
+ }
102
+
103
+ convertPaste({ html, text }, formats = {}) {
104
+ if (formats[CodeBlock.blotName]) {
105
+ return new Delta().insert(text, {
106
+ [CodeBlock.blotName]: formats[CodeBlock.blotName]
107
+ });
108
+ }
109
+ if (!html) {
110
+ return new Delta().insert(text || "");
111
+ }
112
+ const delta = this.convertPasteHTML(html);
113
+ // Remove trailing newline
114
+ if (
115
+ deltaEndsWith(delta, "\n") &&
116
+ (delta.ops[delta.ops.length - 1].attributes === null || formats.table)
117
+ ) {
118
+ return delta.compose(new Delta().retain(delta.length() - 1).delete(1));
119
+ }
120
+ return delta;
121
+ }
122
+
123
+ convertPasteHTML(html) {
124
+ const doc = new DOMParser().parseFromString(html, "text/html");
125
+ const container = doc.body;
126
+ const nodeMatches = new WeakMap();
127
+ const [elementMatchers, textMatchers] = this.prepareMatching(
128
+ container,
129
+ nodeMatches
130
+ );
131
+ return traverse(
132
+ this.quill.scroll,
133
+ container,
134
+ elementMatchers,
135
+ textMatchers,
136
+ nodeMatches
137
+ );
138
+ }
139
+ }
140
+
141
+ // Disable warning messages from overwritting modules
142
+ Quill.debug("error");
143
+ Quill.register({"modules/clipboard": ClipboardOverride}, true);
@@ -0,0 +1,119 @@
1
+ import { BlockEmbed } from "quill/blots/block";
2
+
3
+ const Delta = Quill.import("delta");
4
+ const Parchment = Quill.import("parchment");
5
+
6
+ // Newer version used only for the pasting, not compatible with the version of
7
+ // Quill in use.
8
+ const traverse = (scroll, node, elementMatchers, textMatchers, nodeMatches) => { // eslint-disable-line max-params
9
+ // Post-order
10
+ if (node.nodeType === node.TEXT_NODE) {
11
+ return textMatchers.reduce((delta, matcher) => {
12
+ return matcher(node, delta, scroll);
13
+ }, new Delta());
14
+ }
15
+ if (node.nodeType === node.ELEMENT_NODE) {
16
+ return Array.from(node.childNodes || []).reduce((delta, childNode) => {
17
+ let childrenDelta = traverse(
18
+ scroll,
19
+ childNode,
20
+ elementMatchers,
21
+ textMatchers,
22
+ nodeMatches,
23
+ );
24
+ if (childNode.nodeType === node.ELEMENT_NODE) {
25
+ childrenDelta = elementMatchers.reduce((reducedDelta, matcher) => {
26
+ return matcher(childNode, reducedDelta, scroll);
27
+ }, childrenDelta);
28
+ childrenDelta = (nodeMatches.get(childNode) || []).reduce(
29
+ (reducedDelta, matcher) => {
30
+ return matcher(childNode, reducedDelta, scroll);
31
+ },
32
+ childrenDelta,
33
+ );
34
+ }
35
+ return delta.concat(childrenDelta);
36
+ }, new Delta());
37
+ }
38
+ return new Delta();
39
+ }
40
+
41
+ const deltaEndsWith = (delta, text) => {
42
+ let endText = "";
43
+ for (let idx = delta.ops.length - 1; idx >= 0 && endText.length < text.length; idx -= 1) {
44
+ const op = delta.ops[idx];
45
+ if (typeof op.insert !== "string") {
46
+ break;
47
+ }
48
+ endText = op.insert + endText;
49
+ }
50
+ return endText.slice(-1 * text.length) === text;
51
+ }
52
+
53
+ const isLine = (node) => {
54
+ if (node.childNodes.length === 0) {
55
+ // Exclude embed blocks
56
+ return false;
57
+ }
58
+ return [
59
+ "address", "article", "blockquote", "canvas", "dd", "div", "dl", "dt",
60
+ "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3",
61
+ "h4", "h5", "h6", "header", "iframe", "li", "main", "nav", "ol", "output",
62
+ "p", "pre", "section", "table", "td", "tr", "ul", "video"
63
+ ].includes(node.tagName.toLowerCase());
64
+ }
65
+
66
+ const matchNewLineScroll = (nextSibling, delta, scroll) => {
67
+ if (!scroll) {
68
+ return null;
69
+ }
70
+
71
+ const match = Parchment.query(nextSibling)
72
+ if (match && match.prototype instanceof BlockEmbed) {
73
+ return delta.insert("\n");
74
+ }
75
+ return null;
76
+ }
77
+
78
+ const matchNewline = (node, delta, scroll) => {
79
+ if (!deltaEndsWith(delta, "\n")) {
80
+ // When scroll is defined, it was initiated from the paste event. Otherwise
81
+ // it is a normal Quill initiated traversal which handles adding the line
82
+ // breaks already.
83
+ if (scroll && node.nodeType === node.ELEMENT_NODE && node.tagName === "BR") {
84
+ return delta.insert({"break": ""});
85
+ }
86
+ if (isLine(node)) {
87
+ return delta.insert("\n");
88
+ }
89
+ if (delta.length() > 0 && node.nextSibling) {
90
+ let { nextSibling } = node;
91
+ while (nextSibling !== null) {
92
+ if (isLine(nextSibling)) {
93
+ return delta.insert("\n");
94
+ }
95
+ const scrollMatch = matchNewLineScroll(nextSibling, delta, scroll);
96
+ if (scrollMatch) {
97
+ return scrollMatch;
98
+ }
99
+ nextSibling = nextSibling.firstChild;
100
+ }
101
+ }
102
+ }
103
+ return delta;
104
+ }
105
+
106
+ const matchBreak = (node, delta) => {
107
+ if (!deltaEndsWith(delta, "\n")) {
108
+ delta.insert({"break": ""});
109
+ }
110
+ return delta;
111
+ }
112
+
113
+ export {
114
+ traverse,
115
+ deltaEndsWith,
116
+ isLine,
117
+ matchNewline,
118
+ matchBreak
119
+ }
@@ -129,7 +129,6 @@ class ScrollOvderride extends Scroll {
129
129
  Quill.register("blots/scroll", ScrollOvderride, true);
130
130
  Parchment.register(ScrollOvderride);
131
131
 
132
-
133
132
  export default function lineBreakButtonHandler(quill) {
134
133
  let range = quill.selection.getRange()[0];
135
134
  let currentLeaf = quill.getLeaf(range.index)[0];
@@ -167,13 +166,6 @@ Quill.register("modules/linebreak", (quill) => {
167
166
  }
168
167
  });
169
168
 
170
- quill.clipboard.addMatcher("BR", (node) => {
171
- if (node?.parentNode?.tagName === "A") {
172
- return new Delta().insert("\n");
173
- }
174
- return new Delta().insert({"break": ""});
175
- });
176
-
177
169
  addEnterBindings(quill);
178
170
  backspaceBindingsRangeAny(quill);
179
171
  backspaceBindings(quill);
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable require-jsdoc */
2
2
 
3
3
  import lineBreakButtonHandler from "src/decidim/editor/linebreak_module"
4
+ import "src/decidim/editor/clipboard_override"
4
5
  import "src/decidim/vendor/image-resize.min"
5
6
  import "src/decidim/vendor/image-upload.min"
6
7
 
@@ -10,6 +11,7 @@ export default function createQuillEditor(container) {
10
11
  const toolbar = $(container).data("toolbar");
11
12
  const disabled = $(container).data("disabled");
12
13
 
14
+ const allowedEmptyContentSelector = "iframe";
13
15
  let quillToolbar = [
14
16
  ["bold", "italic", "underline", "linebreak"],
15
17
  [{ list: "ordered" }, { list: "bullet" }],
@@ -93,10 +95,15 @@ export default function createQuillEditor(container) {
93
95
  });
94
96
  container.dispatchEvent(event);
95
97
 
96
- if (text === "\n" || text === "\n\n") {
98
+ if ((text === "\n" || text === "\n\n") && quill.root.querySelectorAll(allowedEmptyContentSelector).length === 0) {
97
99
  $input.val("");
98
100
  } else {
99
- $input.val(quill.root.innerHTML);
101
+ const emptyParagraph = "<p><br></p>";
102
+ const cleanHTML = quill.root.innerHTML.replace(
103
+ new RegExp(`^${emptyParagraph}|${emptyParagraph}$`, "g"),
104
+ ""
105
+ );
106
+ $input.val(cleanHTML);
100
107
  }
101
108
  });
102
109
  // After editor is ready, linebreak_module deletes two extraneous new lines
@@ -33,7 +33,7 @@ import MapDragMarkerController from "src/decidim/map/controller/drag_marker"
33
33
  * @param {Object} config The map configuration object.
34
34
  * @returns {MapController} The controller for the map.
35
35
  */
36
- export default function createMapController(mapId, config) {
36
+ const createMapController = function(mapId, config) {
37
37
  if (config.type === "static") {
38
38
  return new MapStaticController(mapId, config);
39
39
  } else if (config.type === "drag-marker") {
@@ -42,3 +42,5 @@ export default function createMapController(mapId, config) {
42
42
 
43
43
  return new MapMarkersController(mapId, config);
44
44
  }
45
+
46
+ window.Decidim.createMapController = createMapController;
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable require-jsdoc */
2
2
 
3
3
  import * as L from "leaflet";
4
- import createMapController from "src/decidim/map/factory"
4
+ import "src/decidim/map/factory"
5
5
 
6
6
  /**
7
7
  * @deprecated
@@ -60,7 +60,7 @@ const loadMap = (mapId, markersData) => {
60
60
  legacyMapSupport($map);
61
61
 
62
62
  const mapData = $map.data("decidim-map");
63
- const ctrl = createMapController(mapId, mapData);
63
+ const ctrl = window.Decidim.createMapController(mapId, mapData);
64
64
  const map = ctrl.load();
65
65
 
66
66
  L.tileLayer.here(mapData.tileLayer).addTo(map);
@@ -1,4 +1,4 @@
1
- import createMapController from "src/decidim/map/factory"
1
+ import "src/decidim/map/factory"
2
2
 
3
3
  $(() => {
4
4
  // Load the map controller factory method in the document.ready handler to
@@ -20,7 +20,7 @@ $(() => {
20
20
  }
21
21
 
22
22
  const mapConfig = $map.data("decidim-map");
23
- const ctrl = createMapController(mapId, mapConfig);
23
+ const ctrl = window.Decidim.createMapController(mapId, mapConfig);
24
24
  const map = ctrl.load();
25
25
 
26
26
  $map.data("map", map);
@@ -30,6 +30,8 @@ $datetime-bg: var(--primary);
30
30
  border-radius: $card-border-radius;
31
31
  // Keep visible for accessibility (active/focused card as a link)
32
32
  overflow: visible;
33
+ overflow-wrap: break-word;
34
+ hyphens: auto;
33
35
 
34
36
  @include modifiers(
35
37
  border-top-color,