decidim-core 0.27.0 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/cells/decidim/amendable/announcement_cell.rb +1 -1
- data/app/cells/decidim/card_m_cell.rb +1 -1
- data/app/controllers/decidim/devise/invitations_controller.rb +9 -2
- data/app/controllers/decidim/last_activities_controller.rb +5 -2
- data/app/forms/decidim/account_form.rb +2 -2
- data/app/forms/decidim/amendable/form.rb +2 -1
- data/app/forms/decidim/registration_form.rb +2 -2
- data/app/forms/decidim/upload_validation_form.rb +51 -7
- data/app/helpers/decidim/layout_helper.rb +12 -4
- data/app/helpers/decidim/sanitize_helper.rb +1 -1
- data/app/mailers/decidim/notification_mailer.rb +1 -0
- data/app/mailers/decidim/notifications_digest_mailer.rb +1 -0
- data/app/packs/src/decidim/editor/clipboard_override.js +143 -0
- data/app/packs/src/decidim/editor/clipboard_utilities.js +119 -0
- data/app/packs/src/decidim/editor/linebreak_module.js +0 -8
- data/app/packs/src/decidim/editor.js +9 -2
- data/app/packs/stylesheets/decidim/_editor.scss +129 -0
- data/app/packs/stylesheets/decidim/extras/_quill.scss +0 -6
- data/app/presenters/decidim/home_stats_presenter.rb +11 -4
- data/app/presenters/decidim/stats_presenter.rb +7 -8
- data/app/presenters/decidim/user_presenter.rb +9 -4
- data/app/queries/decidim/public_activities.rb +1 -0
- data/app/validators/etiquette_validator.rb +7 -3
- data/app/validators/file_content_type_validator.rb +103 -0
- data/app/validators/passthru_validator.rb +11 -0
- data/app/validators/uploader_content_type_validator.rb +22 -0
- data/app/views/decidim/notification_mailer/event_received.html.erb +1 -1
- data/app/views/decidim/notifications_digest_mailer/_email_content.html.erb +1 -1
- data/config/locales/ar.yml +0 -13
- data/config/locales/bg.yml +0 -13
- data/config/locales/ca.yml +5 -13
- data/config/locales/cs.yml +6 -14
- data/config/locales/de.yml +0 -13
- data/config/locales/el.yml +0 -13
- data/config/locales/en.yml +5 -13
- data/config/locales/es-MX.yml +5 -13
- data/config/locales/es-PY.yml +5 -13
- data/config/locales/es.yml +9 -17
- data/config/locales/eu.yml +4 -13
- data/config/locales/fi-plain.yml +5 -13
- data/config/locales/fi.yml +5 -13
- data/config/locales/fr-CA.yml +5 -13
- data/config/locales/fr.yml +5 -13
- data/config/locales/ga-IE.yml +0 -2
- data/config/locales/gl.yml +0 -13
- data/config/locales/gn-PY.yml +1 -0
- data/config/locales/hu.yml +0 -13
- data/config/locales/id-ID.yml +0 -13
- data/config/locales/it.yml +0 -13
- data/config/locales/ja.yml +5 -13
- data/config/locales/lb.yml +0 -13
- data/config/locales/lo-LA.yml +1 -0
- data/config/locales/lt.yml +0 -13
- data/config/locales/lv.yml +0 -13
- data/config/locales/nl.yml +0 -13
- data/config/locales/no.yml +0 -13
- data/config/locales/pl.yml +0 -13
- data/config/locales/pt-BR.yml +0 -13
- data/config/locales/pt.yml +0 -13
- data/config/locales/ro-RO.yml +0 -13
- data/config/locales/ru.yml +0 -2
- data/config/locales/sk.yml +0 -13
- data/config/locales/sv.yml +0 -13
- data/config/locales/tr-TR.yml +0 -13
- data/config/locales/zh-CN.yml +0 -13
- data/lib/decidim/attributes/localized_date.rb +1 -1
- data/lib/decidim/attributes/time_with_zone.rb +5 -2
- data/lib/decidim/core/engine.rb +7 -5
- data/lib/decidim/core/test/shared_examples/comments_examples.rb +1 -1
- data/lib/decidim/core/test/shared_examples/editor_shared_examples.rb +30 -0
- data/lib/decidim/core/test/shared_examples/mcell_examples.rb +17 -0
- data/lib/decidim/core/test.rb +2 -0
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/file_validator_humanizer.rb +1 -1
- data/lib/decidim/form_builder.rb +10 -3
- data/lib/decidim/resourceable.rb +5 -4
- data/lib/decidim/settings_manifest.rb +1 -1
- metadata +13 -7
- data/app/packs/images/decidim/gamification/badges/decidim_gamification_badges_invitations.svg +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9cfa315481be2ecd36504616728a76d1c445b99a233f36e50db71a7351ac04b
|
|
4
|
+
data.tar.gz: d67d9216824b975d528f0e64a8f605eacc4849fb042836d63e5c2fe5bc4da218
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 776eba5812d48f147afa41d5b0a2eab52d2c46ce2db9616bd784c2f784be2851eaa1e341439ba1ac13f9b8152e1a3fb09acdbe08e93a9e2e6721e7c9afb09982
|
|
7
|
+
data.tar.gz: eb00d8fe971338da9056eb5e6268934614f5803d1d102d643a14c7602e8323266195948a282220c1013c1f0e39511f8c5adc95aa9e843244be01904589eae57e
|
|
@@ -36,7 +36,7 @@ module Decidim::Amendable
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def proposal_link(resource = model.amendable, text = nil)
|
|
39
|
-
text ||= %(<strong>#{present(model.amendable).title}</strong>)
|
|
39
|
+
text ||= %(<strong>#{decidim_sanitize(present(model.amendable).title, strip_tags: true)}</strong>)
|
|
40
40
|
link_to resource_locator(resource).path do
|
|
41
41
|
text
|
|
42
42
|
end
|
|
@@ -21,7 +21,7 @@ module Decidim
|
|
|
21
21
|
# invitation. Using the param `invite_redirect` we can redirect the user
|
|
22
22
|
# to a custom path after it has accepted the invitation.
|
|
23
23
|
def after_accept_path_for(resource)
|
|
24
|
-
|
|
24
|
+
invite_redirect_path || after_sign_in_path_for(resource)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# When a managed user accepts the invitation is promoted to non-managed user.
|
|
@@ -32,7 +32,6 @@ module Decidim
|
|
|
32
32
|
resource.update!(newsletter_notifications_at: Time.current) if update_resource_params[:newsletter_notifications]
|
|
33
33
|
resource.update!(managed: false) if resource.managed?
|
|
34
34
|
resource.update!(accepted_tos_version: resource.organization.tos_version)
|
|
35
|
-
Decidim::Gamification.increment_score(resource.invited_by, :invitations) if resource.invited_by
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
resource
|
|
@@ -40,6 +39,14 @@ module Decidim
|
|
|
40
39
|
|
|
41
40
|
protected
|
|
42
41
|
|
|
42
|
+
def invite_redirect_path
|
|
43
|
+
path = params[:invite_redirect]
|
|
44
|
+
return unless path
|
|
45
|
+
return unless path.starts_with?(%r{^/[a-z0-9]+})
|
|
46
|
+
|
|
47
|
+
path
|
|
48
|
+
end
|
|
49
|
+
|
|
43
50
|
def configure_permitted_parameters
|
|
44
51
|
devise_parameter_sanitizer.permit(:accept_invitation, keys: [:nickname, :tos_agreement, :newsletter_notifications])
|
|
45
52
|
end
|
|
@@ -33,8 +33,11 @@ module Decidim
|
|
|
33
33
|
|
|
34
34
|
def search_collection
|
|
35
35
|
ActionLog
|
|
36
|
-
.where(
|
|
37
|
-
|
|
36
|
+
.where(
|
|
37
|
+
organization: current_organization,
|
|
38
|
+
visibility: %w(public-only all)
|
|
39
|
+
)
|
|
40
|
+
.with_new_resource_type("all")
|
|
38
41
|
.order(created_at: :desc)
|
|
39
42
|
end
|
|
40
43
|
|
|
@@ -19,9 +19,9 @@ module Decidim
|
|
|
19
19
|
attribute :personal_url
|
|
20
20
|
attribute :about
|
|
21
21
|
|
|
22
|
-
validates :name, presence: true
|
|
22
|
+
validates :name, presence: true, format: { with: Decidim::User::REGEXP_NAME }
|
|
23
23
|
validates :email, presence: true, "valid_email_2/email": { disposable: true }
|
|
24
|
-
validates :nickname, presence: true, format: Decidim::User::REGEXP_NICKNAME
|
|
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
|
|
@@ -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
|
-
|
|
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,8 +14,8 @@ 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: Decidim::User::REGEXP_NICKNAME, length: { maximum: Decidim::User.nickname_max_length }
|
|
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
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 }
|
|
@@ -7,8 +7,8 @@ module Decidim
|
|
|
7
7
|
include Decidim::HasUploadValidations
|
|
8
8
|
|
|
9
9
|
attribute :resource_class, String
|
|
10
|
-
# Property is named as attribute in upload modal and passthru validator, but
|
|
11
|
-
# cannot be named as attribute here.
|
|
10
|
+
# Property is named as attribute in upload modal and passthru validator, but
|
|
11
|
+
# it cannot be named as attribute here.
|
|
12
12
|
attribute :property, String
|
|
13
13
|
attribute :blob, String
|
|
14
14
|
attribute :form_class, String
|
|
@@ -16,9 +16,22 @@ module Decidim
|
|
|
16
16
|
validates :resource_class, presence: true
|
|
17
17
|
validates :property, presence: true
|
|
18
18
|
validates :blob, presence: true
|
|
19
|
-
validate :
|
|
19
|
+
validate :file_validators, if: ->(form) { form.resource_class.present? && form.property.present? && form.blob.present? }
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
alias organization current_organization
|
|
22
|
+
|
|
23
|
+
# This is a "trick" to provide the attachment context (i.e. admin or
|
|
24
|
+
# participant) to the attachment records being validated. This is to show
|
|
25
|
+
# the invalid content type / file extension errors with the correct file
|
|
26
|
+
# extensions that may be shown in the help text next to the upload
|
|
27
|
+
# drag'n'drop field.
|
|
28
|
+
def attached_to
|
|
29
|
+
@attached_to ||= AttachmentContextProxy.new(organization, attachment_context)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def file_validators
|
|
22
35
|
org = organization
|
|
23
36
|
PassthruValidator.new(
|
|
24
37
|
attributes: [property],
|
|
@@ -31,8 +44,6 @@ module Decidim
|
|
|
31
44
|
).validate_each(self, property.to_sym, blob)
|
|
32
45
|
end
|
|
33
46
|
|
|
34
|
-
private
|
|
35
|
-
|
|
36
47
|
def validate_with
|
|
37
48
|
if form_object_class && form_object_class._validators[property.to_sym].is_a?(Array) && form_object_class._validators[property.to_sym].size.positive?
|
|
38
49
|
passthru = form_object_class._validators[property.to_sym].find { |v| v.is_a?(PassthruValidator) }
|
|
@@ -49,6 +60,39 @@ module Decidim
|
|
|
49
60
|
end
|
|
50
61
|
end
|
|
51
62
|
|
|
52
|
-
|
|
63
|
+
# The attachment context (i.e. admin or participant) is determined using the
|
|
64
|
+
# form class name and checking if it contains the `Admin` namespace in it.
|
|
65
|
+
# And example use case is the attachment forms in the admin panel.
|
|
66
|
+
def attachment_context
|
|
67
|
+
return :participant unless form_object_class
|
|
68
|
+
return :admin if form_object_class.name.include? "::Admin::"
|
|
69
|
+
|
|
70
|
+
:participant
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# This class provides ability to interpret the attachment context based on
|
|
74
|
+
# the details available within the context of this class. Normally the
|
|
75
|
+
# attachment context would be defined by the record to which the attachment
|
|
76
|
+
# are added to, e.g. proposals (participant contenxt) or participatory
|
|
77
|
+
# processes (admin context). Unfortunately this information is not available
|
|
78
|
+
# when the parameters are passed to the upload validation.
|
|
79
|
+
class AttachmentContextProxy
|
|
80
|
+
attr_reader :organization, :attachment_context
|
|
81
|
+
|
|
82
|
+
delegate :id, :_read_attribute, to: :organization
|
|
83
|
+
|
|
84
|
+
def initialize(organization, attachment_context)
|
|
85
|
+
@organization = organization
|
|
86
|
+
@attachment_context = attachment_context
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.primary_key
|
|
90
|
+
:id
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.polymorphic_name
|
|
94
|
+
"Decidim::Organization"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
53
97
|
end
|
|
54
98
|
end
|
|
@@ -88,8 +88,11 @@ module Decidim
|
|
|
88
88
|
classes = _icon_classes(options) + ["external-icon"]
|
|
89
89
|
|
|
90
90
|
if path.split(".").last == "svg"
|
|
91
|
+
icon_path = application_path(path)
|
|
92
|
+
return unless icon_path
|
|
93
|
+
|
|
91
94
|
attributes = { class: classes.join(" ") }.merge(options)
|
|
92
|
-
asset = File.read(
|
|
95
|
+
asset = File.read(icon_path)
|
|
93
96
|
asset.gsub("<svg ", "<svg#{tag_builder.tag_options(attributes)} ").html_safe
|
|
94
97
|
else
|
|
95
98
|
image_pack_tag(path, class: classes.join(" "), style: "display: none")
|
|
@@ -97,9 +100,14 @@ module Decidim
|
|
|
97
100
|
end
|
|
98
101
|
|
|
99
102
|
def application_path(path)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
# Force the path to be returned without the protocol and host even when a
|
|
104
|
+
# custom asset host has been defined. The host parameter needs to be a
|
|
105
|
+
# non-nil because otherwise it will be set to the asset host at
|
|
106
|
+
# ActionView::Helpers::AssetUrlHelper#compute_asset_host.
|
|
107
|
+
img_path = asset_pack_path(path, host: "", protocol: :relative)
|
|
108
|
+
Rails.public_path.join(img_path.sub(%r{^/}, ""))
|
|
109
|
+
rescue ::Webpacker::Manifest::MissingEntryError
|
|
110
|
+
nil
|
|
103
111
|
end
|
|
104
112
|
|
|
105
113
|
# Allows to create role attribute according to accessibility rules
|
|
@@ -37,7 +37,7 @@ module Decidim
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def decidim_sanitize_editor(html, options = {})
|
|
40
|
-
content_tag(:div, decidim_sanitize(html, options), class: %w(ql-editor
|
|
40
|
+
content_tag(:div, decidim_sanitize(html, options), class: %w(ql-editor-display))
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def decidim_sanitize_editor_admin(html, options = {})
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|