decidim-core 0.32.0.rc2 → 0.32.0.rc3
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/amend_button_card/show.erb +2 -2
- data/app/cells/decidim/card_metadata_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/cta_settings_form/show.erb +1 -1
- data/app/cells/decidim/content_blocks/hero_settings_form/show.erb +1 -1
- data/app/cells/decidim/content_blocks/highlighted_content_banner_settings_form/show.erb +1 -1
- data/app/cells/decidim/content_blocks/participatory_space_hero_settings_form/show.erb +1 -1
- data/app/cells/decidim/content_blocks/static_page/section_settings_form/show.erb +3 -1
- data/app/cells/decidim/content_blocks/static_page/summary_settings_form/show.erb +3 -1
- data/app/cells/decidim/content_blocks/static_page/two_pane_section_settings_form/show.erb +4 -2
- data/app/cells/decidim/resource_types_filter/show.erb +2 -2
- data/app/controllers/concerns/decidim/devise_controllers.rb +9 -0
- data/app/events/decidim/amendable/amendment_base_event.rb +7 -1
- data/app/models/decidim/moderation.rb +1 -1
- data/app/models/decidim/participatory_space/member.rb +1 -1
- data/app/models/decidim/user_base_entity.rb +17 -2
- data/app/models/decidim/user_moderation.rb +1 -1
- data/app/packs/src/decidim/controllers/breadcrumb_truncate/breadcrumb_truncate.test.js +230 -0
- data/app/packs/src/decidim/controllers/breadcrumb_truncate/controller.js +172 -0
- data/app/packs/src/decidim/controllers/form_validator/form_validator.js +6 -6
- data/app/packs/src/decidim/controllers/form_validator/form_validator.test.js +23 -1
- data/app/packs/src/decidim/direct_uploads/upload_field.js +4 -4
- data/app/packs/src/decidim/direct_uploads/upload_modal.js +11 -7
- data/app/packs/src/decidim/index.js +2 -1
- data/app/packs/src/decidim/sw/sw.js +1 -1
- data/app/packs/src/decidim/utilities/text.js +6 -6
- data/app/packs/stylesheets/decidim/_floating_help.scss +1 -1
- data/app/packs/stylesheets/decidim/_header.scss +36 -7
- data/app/presenters/decidim/menu_item_presenter.rb +9 -3
- data/app/presenters/decidim/stats_presenter.rb +1 -1
- data/app/views/decidim/gamification/badges/index.html.erb +1 -1
- data/app/views/decidim/homepage/show.html.erb +1 -1
- data/app/views/decidim/last_activities/index.html.erb +1 -1
- data/app/views/decidim/newsletters/unsubscribe.html.erb +1 -1
- data/app/views/decidim/offline/show.html.erb +1 -1
- data/app/views/decidim/pages/index.html.erb +1 -1
- data/app/views/decidim/profiles/show.html.erb +1 -1
- data/app/views/decidim/shared/_resource_actions.html.erb +4 -4
- data/app/views/decidim/user_activities/index.html.erb +1 -1
- data/app/views/layouts/decidim/_wrapper.html.erb +2 -2
- data/app/views/layouts/decidim/header/_menu.html.erb +1 -1
- data/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb +31 -4
- data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile.html.erb +17 -13
- data/app/views/layouts/decidim/shared/_layout_center.html.erb +1 -1
- data/app/views/layouts/decidim/shared/_layout_item.html.erb +1 -1
- data/app/views/layouts/decidim/shared/_layout_two_col.html.erb +1 -1
- data/config/initializers/decidim_locale_aware_named_route_helper.rb +16 -0
- data/config/locales/ar.yml +0 -23
- data/config/locales/bg.yml +0 -23
- data/config/locales/ca-IT.yml +25 -25
- data/config/locales/ca.yml +25 -25
- data/config/locales/cs.yml +2 -25
- data/config/locales/de.yml +1 -33
- data/config/locales/el.yml +0 -21
- data/config/locales/en.yml +25 -25
- data/config/locales/es-MX.yml +25 -25
- data/config/locales/es-PY.yml +25 -25
- data/config/locales/es.yml +25 -25
- data/config/locales/eu.yml +51 -67
- data/config/locales/fi-plain.yml +27 -26
- data/config/locales/fi.yml +27 -26
- data/config/locales/fr-CA.yml +2 -22
- data/config/locales/fr.yml +2 -22
- data/config/locales/gl.yml +0 -22
- data/config/locales/hu.yml +0 -20
- data/config/locales/id-ID.yml +0 -21
- data/config/locales/is-IS.yml +0 -6
- data/config/locales/it.yml +1 -18
- data/config/locales/ja.yml +0 -29
- data/config/locales/lb.yml +0 -21
- data/config/locales/lt.yml +0 -22
- data/config/locales/lv.yml +0 -21
- data/config/locales/nl.yml +0 -21
- data/config/locales/no.yml +0 -21
- data/config/locales/pl.yml +0 -23
- data/config/locales/pt-BR.yml +0 -29
- data/config/locales/pt.yml +0 -21
- data/config/locales/ro-RO.yml +0 -22
- data/config/locales/ru.yml +0 -8
- data/config/locales/sk.yml +0 -33
- data/config/locales/sv.yml +6 -31
- data/config/locales/tr-TR.yml +0 -22
- data/config/locales/uk.yml +0 -6
- data/config/locales/zh-CN.yml +0 -19
- data/config/locales/zh-TW.yml +0 -22
- data/decidim-core.gemspec +1 -1
- data/lib/decidim/api/functions/user_entity_list.rb +2 -0
- data/lib/decidim/content_renderers/base_renderer.rb +112 -0
- data/lib/decidim/content_renderers/blob_renderer.rb +4 -7
- data/lib/decidim/content_renderers/mention_resource_renderer.rb +10 -6
- data/lib/decidim/content_renderers/resource_renderer.rb +16 -7
- data/lib/decidim/content_renderers/user_renderer.rb +11 -9
- data/lib/decidim/core/test/shared_examples/amendable/amendment_event_examples.rb +2 -2
- data/lib/decidim/core/test/shared_examples/amendable/amendment_promoted_event_examples.rb +3 -3
- data/lib/decidim/core/test/shared_examples/has_space_in_mcell_examples.rb +2 -1
- data/lib/decidim/core/test/shared_examples/resource_liked_event_examples.rb +3 -3
- data/lib/decidim/core/version.rb +1 -1
- metadata +8 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c8c0eaf7af52859f34c0b152525144104d544b91f8a5ba49173b2f25530709c
|
|
4
|
+
data.tar.gz: c1d8019a4006489b276e3d2ca8c5c6f194890035369ab69a7c7e358376f16fae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 88f9cdca14f469d2fe61ca88c3ba56f8e44586a951be4de8a3faa3f9de1d75b982f8b0db9023052470118f8f47c4d327eecf4687a568f636a18a6c85cfacd874
|
|
7
|
+
data.tar.gz: de42f147191cad92bd9b25c908a4526aaa317313e44603828b59ec0f40e9360f1a9cc8e046b64ed226b9c1585cdca4b213f271380e003d43936b813ebef6160f
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<li role="
|
|
2
|
-
<%= action_authorized_link_to :amend, new_amend_path, resource: model, data: { "redirect_url" => new_amend_path }, id: "amend-button", class: "dropdown__button" do %>
|
|
1
|
+
<li role="presentation" class="dropdown__item">
|
|
2
|
+
<%= action_authorized_link_to :amend, new_amend_path, resource: model, data: { "redirect_url" => new_amend_path }, id: "amend-button", class: "dropdown__button", role: "menuitem" do %>
|
|
3
3
|
<span><%= t("button", scope: "decidim.amendments.amendable", model_name: nil) %></span>
|
|
4
4
|
<%= icon "pencil-line" %>
|
|
5
5
|
<% end %>
|
|
@@ -29,7 +29,7 @@ module Decidim
|
|
|
29
29
|
return unless show_space?
|
|
30
30
|
|
|
31
31
|
{
|
|
32
|
-
text:
|
|
32
|
+
text: translated_attribute(participatory_space.title),
|
|
33
33
|
icon: resource_type_icon_key(participatory_space.class),
|
|
34
34
|
url: Decidim::ResourceLocatorPresenter.new(participatory_space).path
|
|
35
35
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
<%= settings_fields.translated :text_field, :description, label: t("decidim.content_blocks.cta_settings_form.description") %>
|
|
5
5
|
<% end %>
|
|
6
6
|
|
|
7
|
-
<% form.fields_for :images,
|
|
7
|
+
<% form.fields_for :images, content_block.images_container do |images_fields| %>
|
|
8
8
|
<%= images_fields.upload :background_image, label: t("decidim.content_blocks.cta_settings_form.background_image") %>
|
|
9
9
|
<% end %>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
</div>
|
|
15
15
|
<% end %>
|
|
16
16
|
|
|
17
|
-
<% form.fields_for :images,
|
|
17
|
+
<% form.fields_for :images, content_block.images_container do |images_fields| %>
|
|
18
18
|
<div class="row column">
|
|
19
19
|
<%= images_fields.upload :background_image, label: t(".background_image"), button_class: "button button__sm button__transparent-secondary" %>
|
|
20
20
|
</div>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
</div>
|
|
21
21
|
<% end %>
|
|
22
22
|
|
|
23
|
-
<% form.fields_for :images,
|
|
23
|
+
<% form.fields_for :images, content_block.images_container do |images_fields| %>
|
|
24
24
|
<div class="row column">
|
|
25
25
|
<%= images_fields.upload :background_image, label: t(".background_image"), button_class: "button button__sm button__transparent-secondary" %>
|
|
26
26
|
</div>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<% end %>
|
|
10
10
|
|
|
11
|
-
<% form.fields_for :images,
|
|
11
|
+
<% form.fields_for :images, content_block.images_container do |images_fields| %>
|
|
12
12
|
<div class="row column">
|
|
13
13
|
<%= images_fields.upload :background_image, label: t("decidim.content_blocks.cta_settings_form.background_image"), button_class: "button button__sm button__transparent-secondary" %>
|
|
14
14
|
</div>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<% form.fields_for :settings, form.object.settings do |settings_fields| %>
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
<div class="row column">
|
|
3
|
+
<%= settings_fields.translated :editor, :left_column, label: label_left_column %>
|
|
4
|
+
<%= settings_fields.translated :editor, :right_column, label: label_right_column %>
|
|
5
|
+
</div>
|
|
4
6
|
<% end %>
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
</button>
|
|
11
11
|
<ul id="dropdown-menu-resource">
|
|
12
12
|
<% resource_types.each do |resource_type| %>
|
|
13
|
-
<li role="
|
|
14
|
-
<%= link_to filter_url(resource_type[0]), class: "filter#{" is-active" if filter_param == resource_type[0]}" do %>
|
|
13
|
+
<li role="presentation">
|
|
14
|
+
<%= link_to filter_url(resource_type[0]), class: "filter#{" is-active" if filter_param == resource_type[0]}", role: "menuitem" do %>
|
|
15
15
|
<span class="sr-only"><%= resource_type[1] %></span>
|
|
16
16
|
<%= text_with_resource_icon(*resource_type) %>
|
|
17
17
|
<% end %>
|
|
@@ -41,6 +41,10 @@ module Decidim
|
|
|
41
41
|
|
|
42
42
|
layout "layouts/decidim/application"
|
|
43
43
|
|
|
44
|
+
# Ensure locale is set before Devise's own prepended callbacks and reset
|
|
45
|
+
# after the request finishes.
|
|
46
|
+
prepend_around_action :set_current_locale
|
|
47
|
+
|
|
44
48
|
# Saves the location before loading each page so we can return to the
|
|
45
49
|
# right page.
|
|
46
50
|
before_action :store_current_location
|
|
@@ -58,6 +62,11 @@ module Decidim
|
|
|
58
62
|
|
|
59
63
|
store_location_for(:user, redirect_url)
|
|
60
64
|
end
|
|
65
|
+
|
|
66
|
+
def set_current_locale(&)
|
|
67
|
+
locale = Decidim::LocaleRouterDetector.new(request, params).locale
|
|
68
|
+
I18n.with_locale(locale, &)
|
|
69
|
+
end
|
|
61
70
|
end
|
|
62
71
|
end
|
|
63
72
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Decidim::Amendable
|
|
4
4
|
class AmendmentBaseEvent < Decidim::Events::SimpleEvent
|
|
5
5
|
i18n_attributes :amendable_path, :amendable_type, :amendable_title,
|
|
6
|
-
:emendation_path, :emendation_author_nickname, :emendation_author_path
|
|
6
|
+
:emendation_path, :emendation_author_nickname, :emendation_author_name, :emendation_author_path
|
|
7
7
|
|
|
8
8
|
def amendable_title
|
|
9
9
|
@amendable_title ||= translated_attribute(amendable_resource.title)
|
|
@@ -33,6 +33,12 @@ module Decidim::Amendable
|
|
|
33
33
|
@emendation_author_nickname ||= emendation_author.nickname
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
def emendation_author_name
|
|
37
|
+
return unless emendation_resource
|
|
38
|
+
|
|
39
|
+
@emendation_author_name ||= emendation_author.name
|
|
40
|
+
end
|
|
41
|
+
|
|
36
42
|
def emendation_author_path
|
|
37
43
|
return unless emendation_resource
|
|
38
44
|
|
|
@@ -32,7 +32,7 @@ module Decidim
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def self.ransackable_attributes(_auth_object = nil)
|
|
35
|
-
%w(reported_id_string reported_content created_at)
|
|
35
|
+
%w(reported_id_string reported_content created_at report_count)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def self.ransackable_associations(_auth_object = nil)
|
|
@@ -42,7 +42,7 @@ module Decidim
|
|
|
42
42
|
def self.ransackable_attributes(auth_object = nil)
|
|
43
43
|
return [] unless auth_object&.admin?
|
|
44
44
|
|
|
45
|
-
%w(name nickname email invitation_accepted_at last_sign_in_at invitation_sent_at role)
|
|
45
|
+
%w(name nickname email invitation_accepted_at last_sign_in_at invitation_sent_at role published)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def self.ransackable_associations(_auth_object = nil)
|
|
@@ -69,12 +69,27 @@ module Decidim
|
|
|
69
69
|
Decidim::UserBaseEntity.joins(:follows).where(decidim_follows: { user: self }).blocked.exists?
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
ransacker :role do
|
|
73
|
+
Arel.sql(%{CASE WHEN "decidim_users"."admin" = true THEN 'admin' ELSE cast("decidim_users"."roles" as text) END})
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
ransacker :user_moderation_report_count do
|
|
77
|
+
query = <<~SQL.squish
|
|
78
|
+
(
|
|
79
|
+
SELECT COALESCE(MAX(decidim_user_moderations.report_count), 0)
|
|
80
|
+
FROM decidim_user_moderations
|
|
81
|
+
WHERE decidim_user_moderations.decidim_user_id = decidim_users.id
|
|
82
|
+
)
|
|
83
|
+
SQL
|
|
84
|
+
Arel.sql(query)
|
|
85
|
+
end
|
|
86
|
+
|
|
72
87
|
def self.ransackable_attributes(auth_object = nil)
|
|
73
|
-
base = %w(name email nickname last_sign_in_at)
|
|
88
|
+
base = %w(name email nickname last_sign_in_at created_at)
|
|
74
89
|
|
|
75
90
|
return base unless auth_object&.admin?
|
|
76
91
|
|
|
77
|
-
base + %w(invitation_sent_at invitation_accepted_at officialized_at)
|
|
92
|
+
base + %w(invitation_sent_at invitation_accepted_at officialized_at role user_moderation_report_count)
|
|
78
93
|
end
|
|
79
94
|
|
|
80
95
|
def self.ransackable_associations(_auth_object = nil)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
import { Application } from "@hotwired/stimulus"
|
|
3
|
+
import BreadcrumbTruncateController from "src/decidim/controllers/breadcrumb_truncate/controller"
|
|
4
|
+
|
|
5
|
+
describe("BreadcrumbTruncateController", () => {
|
|
6
|
+
let application = null
|
|
7
|
+
let controller = null
|
|
8
|
+
let element = null
|
|
9
|
+
let textElement = null
|
|
10
|
+
let resizeObserverMock = null
|
|
11
|
+
let originalResizeObserver = null
|
|
12
|
+
|
|
13
|
+
const startController = () => new Promise((resolve) => {
|
|
14
|
+
setTimeout(() => {
|
|
15
|
+
controller = application.getControllerForElementAndIdentifier(element, "breadcrumb-truncate")
|
|
16
|
+
resolve()
|
|
17
|
+
}, 0)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
originalResizeObserver = window.ResizeObserver
|
|
22
|
+
resizeObserverMock = {
|
|
23
|
+
observe: jest.fn(),
|
|
24
|
+
disconnect: jest.fn()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
window.ResizeObserver = jest.fn().mockImplementation(() => resizeObserverMock)
|
|
28
|
+
|
|
29
|
+
document.body.innerHTML = `
|
|
30
|
+
<span data-controller="breadcrumb-truncate" class="truncate">
|
|
31
|
+
<span data-breadcrumb-truncate-target="text">Very long participatory space title example</span>
|
|
32
|
+
</span>
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
application = Application.start()
|
|
36
|
+
application.register("breadcrumb-truncate", BreadcrumbTruncateController)
|
|
37
|
+
|
|
38
|
+
element = document.querySelector('[data-controller="breadcrumb-truncate"]')
|
|
39
|
+
textElement = element.querySelector('[data-breadcrumb-truncate-target="text"]')
|
|
40
|
+
|
|
41
|
+
return startController()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
application.stop()
|
|
46
|
+
document.body.innerHTML = ""
|
|
47
|
+
window.ResizeObserver = originalResizeObserver
|
|
48
|
+
jest.restoreAllMocks()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("keeps the full text when it fits", () => {
|
|
52
|
+
Reflect.defineProperty(element, "clientWidth", { configurable: true, value: 100 })
|
|
53
|
+
Reflect.defineProperty(element, "scrollWidth", { configurable: true, get: () => 80 })
|
|
54
|
+
|
|
55
|
+
controller.refresh()
|
|
56
|
+
|
|
57
|
+
expect(textElement.textContent).toBe("Very long participatory space title example")
|
|
58
|
+
expect(element.hasAttribute("title")).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("truncates at the last fitting word and adds an ellipsis", () => {
|
|
62
|
+
Reflect.defineProperty(element, "clientWidth", { configurable: true, value: 100 })
|
|
63
|
+
Reflect.defineProperty(element, "scrollWidth", {
|
|
64
|
+
configurable: true,
|
|
65
|
+
get: () => (textElement.textContent.length > 16
|
|
66
|
+
? 120
|
|
67
|
+
: 80)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
controller.refresh()
|
|
71
|
+
|
|
72
|
+
expect(textElement.textContent).toBe("Very long...")
|
|
73
|
+
expect(element.getAttribute("title")).toBe("Very long participatory space title example")
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("falls back to character truncation for a single long word", () => {
|
|
77
|
+
textElement.textContent = "veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery"
|
|
78
|
+
controller.originalTexts = [textElement.textContent]
|
|
79
|
+
|
|
80
|
+
Reflect.defineProperty(element, "clientWidth", { configurable: true, value: 100 })
|
|
81
|
+
Reflect.defineProperty(element, "scrollWidth", {
|
|
82
|
+
configurable: true,
|
|
83
|
+
get: () => (textElement.textContent.length > 9
|
|
84
|
+
? 120
|
|
85
|
+
: 80)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
controller.refresh()
|
|
89
|
+
|
|
90
|
+
expect(textElement.textContent.endsWith("...")).toBe(true)
|
|
91
|
+
expect(textElement.textContent.includes(" ")).toBe(false)
|
|
92
|
+
expect(textElement.textContent.length).toBeGreaterThan(3)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("disconnects the resize observer", () => {
|
|
96
|
+
controller.disconnect()
|
|
97
|
+
|
|
98
|
+
expect(resizeObserverMock.disconnect).toHaveBeenCalled()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("does nothing when no text target exists", async () => {
|
|
102
|
+
document.body.innerHTML = "<nav data-controller=\"breadcrumb-truncate\"></nav>"
|
|
103
|
+
|
|
104
|
+
application.stop()
|
|
105
|
+
application = Application.start()
|
|
106
|
+
application.register("breadcrumb-truncate", BreadcrumbTruncateController)
|
|
107
|
+
|
|
108
|
+
element = document.querySelector('[data-controller="breadcrumb-truncate"]')
|
|
109
|
+
|
|
110
|
+
await startController()
|
|
111
|
+
|
|
112
|
+
expect(() => controller.refresh()).not.toThrow()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("prioritizes truncating the widest item in group mode", async () => {
|
|
116
|
+
document.body.innerHTML = `
|
|
117
|
+
<nav data-controller="breadcrumb-truncate">
|
|
118
|
+
<span data-breadcrumb-truncate-target="item">
|
|
119
|
+
<span data-breadcrumb-truncate-target="text">Very long participatory space title example</span>
|
|
120
|
+
</span>
|
|
121
|
+
<span data-breadcrumb-truncate-target="item">
|
|
122
|
+
<span data-breadcrumb-truncate-target="text">Proposals</span>
|
|
123
|
+
</span>
|
|
124
|
+
</nav>
|
|
125
|
+
`
|
|
126
|
+
|
|
127
|
+
application.stop()
|
|
128
|
+
application = Application.start()
|
|
129
|
+
application.register("breadcrumb-truncate", BreadcrumbTruncateController)
|
|
130
|
+
|
|
131
|
+
element = document.querySelector('[data-controller="breadcrumb-truncate"]')
|
|
132
|
+
const itemElements = element.querySelectorAll('[data-breadcrumb-truncate-target="item"]')
|
|
133
|
+
const textElements = element.querySelectorAll('[data-breadcrumb-truncate-target="text"]')
|
|
134
|
+
const initialWidths = [150, 50]
|
|
135
|
+
|
|
136
|
+
Reflect.defineProperty(element, "clientWidth", { configurable: true, value: 100 })
|
|
137
|
+
Reflect.defineProperty(itemElements[0], "clientWidth", {
|
|
138
|
+
configurable: true,
|
|
139
|
+
get: () => Number.parseInt(itemElements[0].style.maxWidth || `${initialWidths[0]}`, 10)
|
|
140
|
+
})
|
|
141
|
+
Reflect.defineProperty(itemElements[1], "clientWidth", {
|
|
142
|
+
configurable: true,
|
|
143
|
+
get: () => Number.parseInt(itemElements[1].style.maxWidth || `${initialWidths[1]}`, 10)
|
|
144
|
+
})
|
|
145
|
+
Reflect.defineProperty(element, "scrollWidth", {
|
|
146
|
+
configurable: true,
|
|
147
|
+
get: () => Array.from(itemElements).reduce((sum, item) => sum + item.clientWidth, 0)
|
|
148
|
+
})
|
|
149
|
+
Reflect.defineProperty(itemElements[0], "scrollWidth", {
|
|
150
|
+
configurable: true,
|
|
151
|
+
get: () => (textElements[0].textContent.length > 16
|
|
152
|
+
? 120
|
|
153
|
+
: 80)
|
|
154
|
+
})
|
|
155
|
+
Reflect.defineProperty(itemElements[1], "scrollWidth", {
|
|
156
|
+
configurable: true,
|
|
157
|
+
get: () => 50
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
await startController()
|
|
161
|
+
controller.refresh()
|
|
162
|
+
|
|
163
|
+
expect(textElements[0].textContent).not.toBe("Very long participatory space title example")
|
|
164
|
+
expect(textElements[0].textContent.endsWith("...")).toBe(true)
|
|
165
|
+
expect(textElements[1].textContent).toBe("Proposals")
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it("preserves shorter deep breadcrumb labels while shrinking the longest one first", async () => {
|
|
169
|
+
document.body.innerHTML = `
|
|
170
|
+
<nav data-controller="breadcrumb-truncate">
|
|
171
|
+
<span data-breadcrumb-truncate-target="item">
|
|
172
|
+
<span data-breadcrumb-truncate-target="text">This is a very long title for a participatory process for a test</span>
|
|
173
|
+
</span>
|
|
174
|
+
<span data-breadcrumb-truncate-target="item">
|
|
175
|
+
<span data-breadcrumb-truncate-target="text">Debates</span>
|
|
176
|
+
</span>
|
|
177
|
+
<span data-breadcrumb-truncate-target="item">
|
|
178
|
+
<span data-breadcrumb-truncate-target="text">Debate made by a participant</span>
|
|
179
|
+
</span>
|
|
180
|
+
</nav>
|
|
181
|
+
`
|
|
182
|
+
|
|
183
|
+
application.stop()
|
|
184
|
+
application = Application.start()
|
|
185
|
+
application.register("breadcrumb-truncate", BreadcrumbTruncateController)
|
|
186
|
+
|
|
187
|
+
element = document.querySelector('[data-controller="breadcrumb-truncate"]')
|
|
188
|
+
const itemElements = element.querySelectorAll('[data-breadcrumb-truncate-target="item"]')
|
|
189
|
+
const textElements = element.querySelectorAll('[data-breadcrumb-truncate-target="text"]')
|
|
190
|
+
|
|
191
|
+
Reflect.defineProperty(element, "clientWidth", { configurable: true, value: 220 })
|
|
192
|
+
|
|
193
|
+
const initialWidths = [180, 56, 104]
|
|
194
|
+
|
|
195
|
+
itemElements.forEach((item, index) => {
|
|
196
|
+
Reflect.defineProperty(item, "clientWidth", {
|
|
197
|
+
configurable: true,
|
|
198
|
+
get: () => Number.parseInt(item.style.maxWidth || `${initialWidths[index]}`, 10)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
Reflect.defineProperty(element, "scrollWidth", {
|
|
202
|
+
configurable: true,
|
|
203
|
+
get: () => Array.from(itemElements).reduce((sum, item) => sum + item.clientWidth, 0)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
Reflect.defineProperty(itemElements[0], "scrollWidth", {
|
|
207
|
+
configurable: true,
|
|
208
|
+
get: () => (textElements[0].textContent.length > 18
|
|
209
|
+
? 160
|
|
210
|
+
: 80)
|
|
211
|
+
})
|
|
212
|
+
Reflect.defineProperty(itemElements[1], "scrollWidth", {
|
|
213
|
+
configurable: true,
|
|
214
|
+
get: () => 56
|
|
215
|
+
})
|
|
216
|
+
Reflect.defineProperty(itemElements[2], "scrollWidth", {
|
|
217
|
+
configurable: true,
|
|
218
|
+
get: () => (textElements[2].textContent.length > 20
|
|
219
|
+
? 120
|
|
220
|
+
: 96)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
await startController()
|
|
224
|
+
controller.refresh()
|
|
225
|
+
|
|
226
|
+
expect(textElements[0].textContent.endsWith("...")).toBe(true)
|
|
227
|
+
expect(textElements[1].textContent).toBe("Debates")
|
|
228
|
+
expect(textElements[2].textContent).toBe("Debate made by a participant")
|
|
229
|
+
})
|
|
230
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
const ELLIPSIS = "..."
|
|
4
|
+
const PREFERRED_MIN_WIDTH = 96
|
|
5
|
+
const ABSOLUTE_MIN_WIDTH = 24
|
|
6
|
+
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static get targets() {
|
|
9
|
+
return ["item", "text"]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
connect() {
|
|
13
|
+
this.originalTexts = this.textTargets.map((target) => target.textContent.trim())
|
|
14
|
+
this.refresh = this.refresh.bind(this)
|
|
15
|
+
|
|
16
|
+
this.resizeObserver = new ResizeObserver(() => this.refresh())
|
|
17
|
+
this.resizeObserver.observe(this.element)
|
|
18
|
+
window.addEventListener("resize", this.refresh)
|
|
19
|
+
|
|
20
|
+
this.refresh()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
disconnect() {
|
|
24
|
+
this.resizeObserver?.disconnect()
|
|
25
|
+
window.removeEventListener("resize", this.refresh)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
refresh() {
|
|
29
|
+
if (!this.hasTextTarget) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (this.hasItemTarget) {
|
|
34
|
+
this.refreshGroup()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.truncateItem(this.element, this.textTarget, this.originalTexts[0])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
refreshGroup() {
|
|
42
|
+
this.itemTargets.forEach((item, index) => this.resetItem(item, this.textTargets[index], this.originalTexts[index]))
|
|
43
|
+
|
|
44
|
+
if (this.element.clientWidth <= 0) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!this.isOverflowing(this.element)) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const candidates = this.itemTargets.map((item, index) => ({
|
|
53
|
+
item,
|
|
54
|
+
text: this.textTargets[index],
|
|
55
|
+
originalText: this.originalTexts[index],
|
|
56
|
+
width: item.scrollWidth
|
|
57
|
+
})).sort((left, right) => right.width - left.width)
|
|
58
|
+
|
|
59
|
+
candidates.forEach((candidate) => {
|
|
60
|
+
if (!this.isOverflowing(this.element)) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.shrinkCandidate(candidate, PREFERRED_MIN_WIDTH)
|
|
65
|
+
|
|
66
|
+
if (this.isOverflowing(this.element)) {
|
|
67
|
+
this.shrinkCandidate(candidate, ABSOLUTE_MIN_WIDTH)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
shrinkCandidate(candidate, minimumWidth) {
|
|
73
|
+
const overflow = this.element.scrollWidth - this.element.clientWidth
|
|
74
|
+
|
|
75
|
+
if (overflow <= 0) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const minWidth = Math.min(candidate.width, minimumWidth)
|
|
80
|
+
const shrinkableWidth = candidate.width - minWidth
|
|
81
|
+
|
|
82
|
+
if (shrinkableWidth <= 0) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const reduction = Math.min(overflow, shrinkableWidth)
|
|
87
|
+
const nextWidth = candidate.width - reduction
|
|
88
|
+
|
|
89
|
+
candidate.item.style.flex = `0 1 ${nextWidth}px`
|
|
90
|
+
candidate.item.style.maxWidth = `${nextWidth}px`
|
|
91
|
+
candidate.item.style.minWidth = "0"
|
|
92
|
+
|
|
93
|
+
this.truncateItem(candidate.item, candidate.text, candidate.originalText)
|
|
94
|
+
candidate.width = nextWidth
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
resetItem(item, text, originalText) {
|
|
98
|
+
item.style.removeProperty("flex")
|
|
99
|
+
item.style.removeProperty("max-width")
|
|
100
|
+
item.style.removeProperty("min-width")
|
|
101
|
+
item.removeAttribute("title")
|
|
102
|
+
text.textContent = originalText
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
truncateItem(item, text, originalText) {
|
|
106
|
+
if (item.clientWidth <= 0) {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
text.textContent = originalText
|
|
111
|
+
|
|
112
|
+
if (!this.isOverflowing(item)) {
|
|
113
|
+
item.removeAttribute("title")
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const words = originalText.split(/\s+/).filter(Boolean)
|
|
118
|
+
let visibleText = ""
|
|
119
|
+
|
|
120
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
121
|
+
let nextText = words[index]
|
|
122
|
+
|
|
123
|
+
if (visibleText) {
|
|
124
|
+
nextText = `${visibleText} ${words[index]}`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
text.textContent = `${nextText}${ELLIPSIS}`
|
|
128
|
+
|
|
129
|
+
if (this.isOverflowing(item)) {
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
visibleText = nextText
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!visibleText) {
|
|
137
|
+
visibleText = this.truncateSingleWord(item, text, words[0] || "")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (visibleText === originalText) {
|
|
141
|
+
text.textContent = visibleText
|
|
142
|
+
} else {
|
|
143
|
+
text.textContent = `${visibleText}${ELLIPSIS}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
item.setAttribute("title", originalText)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
truncateSingleWord(item, text, word) {
|
|
150
|
+
let visibleText = ""
|
|
151
|
+
|
|
152
|
+
for (let index = 0; index < word.length; index += 1) {
|
|
153
|
+
text.textContent = `${visibleText}${word[index]}${ELLIPSIS}`
|
|
154
|
+
|
|
155
|
+
if (this.isOverflowing(item)) {
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
visibleText += word[index]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!visibleText && word) {
|
|
163
|
+
return word[0]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return visibleText
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
isOverflowing(element) {
|
|
170
|
+
return element.scrollWidth > element.clientWidth
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -419,13 +419,13 @@ class FormValidator {
|
|
|
419
419
|
return true;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
422
|
+
if (isValid) {
|
|
423
|
+
this.removeCheckboxGroupErrorClasses(groupName);
|
|
424
|
+
} else {
|
|
425
|
+
checkboxGroup.forEach((checkboxElement) => {
|
|
426
426
|
this.addInputErrorClasses(checkboxElement, ["required"]);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
429
|
|
|
430
430
|
return isValid;
|
|
431
431
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* eslint max-lines: ["error",
|
|
1
|
+
/* eslint max-lines: ["error", 712] */
|
|
2
2
|
/* global jest */
|
|
3
3
|
/**
|
|
4
4
|
* @jest-environment jsdom
|
|
@@ -253,6 +253,28 @@ describe("FormValidator", () => {
|
|
|
253
253
|
const isValidTwo = validatorInstance.validateCheckboxGroup("multiCheckboxes");
|
|
254
254
|
expect(isValidTwo).toBe(true);
|
|
255
255
|
});
|
|
256
|
+
|
|
257
|
+
it("should remove error classes once for the group, not per checkbox", () => {
|
|
258
|
+
formElement.innerHTML = `
|
|
259
|
+
<fieldset>
|
|
260
|
+
<input type="checkbox" id="cb-1" name="largeGroup" value="1" required>
|
|
261
|
+
<input type="checkbox" id="cb-2" name="largeGroup" value="2" required>
|
|
262
|
+
<input type="checkbox" id="cb-3" name="largeGroup" value="3" required>
|
|
263
|
+
</fieldset>
|
|
264
|
+
<button type="submit">Submit</button>
|
|
265
|
+
`;
|
|
266
|
+
validatorInstance = new FormValidator(formElement);
|
|
267
|
+
formElement.querySelector("#cb-1").checked = true;
|
|
268
|
+
|
|
269
|
+
const removeGroupSpy = jest.spyOn(validatorInstance, "removeCheckboxGroupErrorClasses");
|
|
270
|
+
const removeInputSpy = jest.spyOn(validatorInstance, "removeInputErrorClasses");
|
|
271
|
+
|
|
272
|
+
const isValid = validatorInstance.validateCheckboxGroup("largeGroup");
|
|
273
|
+
|
|
274
|
+
expect(isValid).toBe(true);
|
|
275
|
+
expect(removeGroupSpy).toHaveBeenCalledTimes(1);
|
|
276
|
+
expect(removeInputSpy).not.toHaveBeenCalled();
|
|
277
|
+
});
|
|
256
278
|
});
|
|
257
279
|
|
|
258
280
|
describe("Custom Validators", () => {
|