decidim-core 0.32.0 → 0.32.2
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/comments_button_cell.rb +9 -1
- data/app/cells/decidim/content_blocks/participatory_space_main_data/title.erb +1 -14
- data/app/cells/decidim/data_consent/category.erb +20 -33
- data/app/cells/decidim/following_cell.rb +1 -1
- data/app/cells/decidim/footer_topics/show.erb +5 -8
- data/app/cells/decidim/member/show.erb +1 -1
- data/app/cells/decidim/nav_links/show.erb +3 -1
- data/app/cells/decidim/share_text_widget/modal.erb +1 -0
- data/app/cells/decidim/share_widget/modal.erb +1 -0
- data/app/cells/decidim/share_widget/qr_code_modal.erb +2 -4
- data/app/commands/decidim/create_omniauth_registration.rb +17 -2
- data/app/controllers/concerns/decidim/devise_authentication_methods.rb +4 -0
- data/app/controllers/concerns/decidim/devise_controllers.rb +2 -0
- data/app/controllers/concerns/decidim/disable_redirection_to_external_host.rb +2 -0
- data/app/controllers/concerns/decidim/filter_resource.rb +2 -0
- data/app/controllers/concerns/decidim/impersonate_users.rb +2 -2
- data/app/controllers/concerns/decidim/locale_switcher.rb +12 -10
- data/app/controllers/concerns/decidim/needs_permission.rb +22 -10
- data/app/controllers/concerns/decidim/onboarding_action_methods.rb +2 -0
- data/app/controllers/concerns/decidim/paginable.rb +7 -3
- data/app/controllers/concerns/decidim/payload_info.rb +2 -0
- data/app/controllers/concerns/decidim/resource_versions_concern.rb +14 -0
- data/app/controllers/decidim/application_controller.rb +0 -6
- data/app/controllers/decidim/authorization_modals_controller.rb +2 -0
- data/app/controllers/decidim/components/base_controller.rb +2 -0
- data/app/controllers/decidim/devise/passwords_controller.rb +8 -0
- data/app/controllers/decidim/favicon_controller.rb +9 -0
- data/app/controllers/decidim/free_resource_authorization_modals_controller.rb +2 -0
- data/app/controllers/decidim/gamification/badges_controller.rb +2 -0
- data/app/controllers/decidim/homepage_controller.rb +4 -1
- data/app/controllers/decidim/likes_controller.rb +1 -1
- data/app/controllers/decidim/links_controller.rb +2 -0
- data/app/controllers/decidim/manifests_controller.rb +2 -0
- data/app/controllers/decidim/messaging/conversations_controller.rb +10 -4
- data/app/controllers/decidim/newsletters_controller.rb +4 -0
- data/app/controllers/decidim/notifications_subscriptions_controller.rb +4 -0
- data/app/controllers/decidim/offline_controller.rb +5 -1
- data/app/controllers/decidim/open_data_controller.rb +5 -1
- data/app/controllers/decidim/pages_controller.rb +31 -3
- data/app/controllers/decidim/private_downloads_controller.rb +2 -0
- data/app/controllers/decidim/profiles_controller.rb +19 -1
- data/app/controllers/decidim/qr_controller.rb +3 -1
- data/app/controllers/decidim/resource_autocomplete_controller.rb +2 -0
- data/app/controllers/decidim/searches_controller.rb +2 -0
- data/app/controllers/decidim/short_links_controller.rb +4 -0
- data/app/controllers/decidim/static_map_controller.rb +2 -0
- data/app/controllers/decidim/timeouts_controller.rb +4 -0
- data/app/controllers/decidim/tos_controller.rb +2 -0
- data/app/controllers/decidim/upload_validations_controller.rb +4 -2
- data/app/controllers/decidim/user_activities_controller.rb +3 -0
- data/app/forms/decidim/upload_validation_form.rb +16 -10
- data/app/helpers/concerns/decidim/user_role_checker.rb +35 -10
- data/app/helpers/decidim/decidim_form_helper.rb +1 -1
- data/app/helpers/decidim/menu_helper.rb +0 -2
- data/app/helpers/decidim/sanitize_helper.rb +11 -1
- data/app/jobs/decidim/application_job.rb +4 -0
- data/app/models/decidim/authorization.rb +24 -0
- data/app/models/decidim/oauth_application.rb +1 -0
- data/app/models/decidim/user.rb +3 -2
- data/app/models/decidim/user_base_entity.rb +1 -1
- data/app/packs/src/decidim/callout.js +10 -0
- data/app/packs/src/decidim/callout.test.js +32 -0
- data/app/packs/src/decidim/controllers/accordion/accordion.test.js +260 -42
- data/app/packs/src/decidim/controllers/accordion/controller.js +136 -17
- data/app/packs/src/decidim/controllers/autofocus/controller.js +30 -0
- data/app/packs/src/decidim/controllers/dropdown/controller.js +6 -0
- data/app/packs/src/decidim/controllers/dropdown/dropdown.test.js +28 -0
- data/app/packs/src/decidim/controllers/language_change/controller.js +21 -8
- data/app/packs/src/decidim/controllers/language_change/language_change.test.js +0 -1
- data/app/packs/src/decidim/controllers/main_menu/controller.js +4 -0
- data/app/packs/src/decidim/controllers/main_menu/main_menu.test.js +23 -0
- data/app/packs/src/decidim/controllers/mention/controller.js +32 -3
- data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +8 -5
- data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +1 -1
- data/app/packs/src/decidim/controllers/password_toggler/controller.js +22 -1
- data/app/packs/src/decidim/controllers/password_toggler/password_toggler.test.js +11 -20
- data/app/packs/src/decidim/datepicker/datepicker_functions.js +41 -25
- data/app/packs/src/decidim/datepicker/generate_datepicker.js +4 -4
- data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +64 -0
- data/app/packs/src/decidim/datepicker/test/time.test.js +39 -24
- data/app/packs/src/decidim/direct_uploads/upload_modal.js +2 -1
- data/app/packs/src/decidim/editor/common/suggestion.js +32 -22
- data/app/packs/src/decidim/editor/extensions/mention/index.js +14 -3
- data/app/packs/src/decidim/editor/index.js +4 -0
- data/app/packs/src/decidim/editor/test/extensions/mention.test.js +32 -9
- data/app/packs/src/decidim/map/controller.js +10 -1
- data/app/packs/src/decidim/refactor/implementation/autofocus.js +22 -0
- data/app/packs/src/decidim/refactor/implementation/autofocus.test.js +171 -0
- data/app/packs/src/decidim/refactor/moved/focus_guard.js +12 -12
- data/app/packs/src/decidim/refactor/moved/focus_guard.test.js +136 -0
- data/app/packs/src/decidim/utilities/dom.js +2 -2
- data/app/packs/stylesheets/decidim/_cards.scss +1 -1
- data/app/packs/stylesheets/decidim/_content_blocks.scss +4 -10
- data/app/packs/stylesheets/decidim/_conversations.scss +0 -8
- data/app/packs/stylesheets/decidim/_cookies.scss +13 -5
- data/app/packs/stylesheets/decidim/_documents.scss +1 -1
- data/app/packs/stylesheets/decidim/_footer.scss +33 -1
- data/app/packs/stylesheets/decidim/_modal.scss +16 -8
- data/app/packs/stylesheets/decidim/_modal_share.scss +0 -22
- data/app/packs/stylesheets/decidim/_participatory_spaces.scss +12 -2
- data/app/packs/stylesheets/decidim/_rich_text.scss +12 -9
- data/app/packs/stylesheets/decidim/_typography.scss +5 -2
- data/app/packs/stylesheets/decidim/application.scss +1 -0
- data/app/packs/stylesheets/decidim/legacy/email.scss +0 -5
- data/app/permissions/decidim/permissions.rb +20 -0
- data/app/presenters/decidim/admin_log/component_presenter.rb +1 -1
- data/app/presenters/decidim/footer_menu_presenter.rb +2 -4
- data/app/presenters/decidim/log/base_presenter.rb +10 -1
- data/app/presenters/decidim/log/user_presenter.rb +2 -2
- data/app/presenters/decidim/menu_item_presenter.rb +1 -5
- data/app/presenters/decidim/resource_presenter.rb +11 -0
- data/app/resolvers/decidim/meta_image_url_resolver.rb +14 -1
- data/app/serializers/decidim/exporters/open_data_blocked_user_serializer.rb +11 -2
- data/app/serializers/decidim/exporters/open_data_moderation_serializer.rb +25 -1
- data/app/services/decidim/svg_content_validator.rb +554 -0
- data/app/uploaders/decidim/application_uploader.rb +2 -2
- data/app/validators/uploader_image_content_validator.rb +276 -0
- data/app/views/decidim/application/_radio_accordion.html.erb +1 -5
- data/app/views/decidim/messaging/conversations/_add_conversation_users.html.erb +1 -1
- data/app/views/decidim/messaging/conversations/create.js.erb +1 -3
- data/app/views/decidim/searches/_filters.html.erb +1 -1
- data/app/views/decidim/shared/_login_modal.html.erb +12 -14
- data/app/views/layouts/decidim/_mailer_logo.html.erb +1 -1
- data/app/views/layouts/decidim/footer/_main.html.erb +1 -1
- data/app/views/layouts/decidim/footer/_main_links.html.erb +56 -28
- data/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb +3 -2
- data/config/initializers/rack_attack.rb +10 -0
- data/config/locales/bg.yml +0 -1
- data/config/locales/ca-IT.yml +9 -1
- data/config/locales/ca.yml +16 -6
- data/config/locales/cs.yml +31 -1
- data/config/locales/de.yml +99 -3
- data/config/locales/en.yml +11 -1
- data/config/locales/es-MX.yml +11 -1
- data/config/locales/es-PY.yml +11 -1
- data/config/locales/es.yml +15 -5
- data/config/locales/eu.yml +166 -161
- data/config/locales/fi-plain.yml +9 -1
- data/config/locales/fi.yml +9 -1
- data/config/locales/fr-CA.yml +12 -1
- data/config/locales/fr.yml +12 -1
- data/config/locales/gl.yml +1485 -33
- data/config/locales/hu.yml +0 -1
- data/config/locales/ja.yml +28 -1
- data/config/locales/pl.yml +2 -1
- data/config/locales/pt-BR.yml +0 -1
- data/config/locales/ro-RO.yml +0 -1
- data/config/locales/sk.yml +2 -1
- data/config/locales/sv.yml +68 -12
- data/config/locales/tr-TR.yml +0 -2
- data/config/routes.rb +11 -2
- data/db/migrate/20250217192438_convert_user_groups_into_users.rb +3 -2
- data/db/migrate/20260909000000_add_failed_attempts_and_locked_at_to_decidim_authorizations.rb +8 -0
- data/decidim-core.gemspec +4 -1
- data/lib/decidim/api/functions/user_entity_list.rb +1 -0
- data/lib/decidim/api/types/user_moderation_type.rb +2 -2
- data/lib/decidim/content_parsers/user_parser.rb +1 -1
- data/lib/decidim/content_parsers.rb +0 -1
- data/lib/decidim/content_renderers/mention_resource_renderer.rb +18 -4
- data/lib/decidim/content_renderers/user_renderer.rb +15 -4
- data/lib/decidim/core/test/shared_examples/access_mode_restricted_participatory_spaces.rb +8 -5
- data/lib/decidim/core/test/shared_examples/access_mode_transparent_participatory_spaces.rb +14 -11
- data/lib/decidim/core/test/shared_examples/comments_examples.rb +26 -0
- data/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb +77 -0
- data/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb +58 -0
- data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +23 -1
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/core.rb +10 -1
- data/lib/decidim/form_builder.rb +9 -1
- data/lib/decidim/has_upload_validations.rb +1 -0
- data/lib/decidim/legacy_form_builder.rb +4 -0
- data/lib/decidim/map/dynamic_map.rb +3 -1
- metadata +33 -7
- data/lib/decidim/content_parsers/inline_images_parser.rb +0 -68
- data/lib/tasks/upgrade/decidim_active_storage_migration_tasks.rake +0 -79
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* global jest */
|
|
2
|
-
|
|
3
1
|
export const createSuggestionRenderer = (node, { itemConverter } = {}) => () => {
|
|
4
2
|
let suggestion = null;
|
|
5
3
|
let suggestionItems = null;
|
|
@@ -55,25 +53,25 @@ export const createSuggestionRenderer = (node, { itemConverter } = {}) => () =>
|
|
|
55
53
|
return;
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
const
|
|
59
|
-
if (currentRange
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return true;
|
|
74
|
-
}).setTextSelection({ from: currentRange.from, to: currentRange.from }).run();
|
|
56
|
+
const item = convertItem(items[idx]);
|
|
57
|
+
if (currentRange) {
|
|
58
|
+
const docSize = currentEditor.state.doc.content.size;
|
|
59
|
+
const isValidRange = currentRange.from >= 0 && currentRange.to <= docSize && currentRange.from <= currentRange.to;
|
|
60
|
+
|
|
61
|
+
if (isValidRange) {
|
|
62
|
+
const nodeType = currentEditor.schema.nodes[items[idx].__typename === "User"
|
|
63
|
+
? "mention"
|
|
64
|
+
: "mentionResource"];
|
|
65
|
+
currentEditor.chain().focus().setTextSelection(currentRange).insertContent({
|
|
66
|
+
type: nodeType.name,
|
|
67
|
+
attrs: item
|
|
68
|
+
}).insertContent(" ").run();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
75
71
|
}
|
|
76
|
-
|
|
72
|
+
|
|
73
|
+
const command = selectCommand;
|
|
74
|
+
command(item);
|
|
77
75
|
};
|
|
78
76
|
|
|
79
77
|
const showSuggestions = ({ items, clientRect }) => {
|
|
@@ -87,7 +85,7 @@ export const createSuggestionRenderer = (node, { itemConverter } = {}) => () =>
|
|
|
87
85
|
suggestion.classList.remove("hidden", "hide");
|
|
88
86
|
suggestion.innerHTML = "";
|
|
89
87
|
items.forEach((rawItem, idx) => {
|
|
90
|
-
const { label, id, help } = convertItem(rawItem);
|
|
88
|
+
const { label, id, help, avatarUrl, displayLabel } = convertItem(rawItem);
|
|
91
89
|
const suggestionItem = document.createElement("button");
|
|
92
90
|
suggestionItem.type = "button";
|
|
93
91
|
suggestionItem.classList.add("editor-suggestions-item");
|
|
@@ -103,7 +101,19 @@ export const createSuggestionRenderer = (node, { itemConverter } = {}) => () =>
|
|
|
103
101
|
if (help) {
|
|
104
102
|
suggestionItem.disabled = true;
|
|
105
103
|
}
|
|
106
|
-
|
|
104
|
+
|
|
105
|
+
if (avatarUrl) {
|
|
106
|
+
const avatar = document.createElement("img");
|
|
107
|
+
avatar.classList.add("editor-suggestions-item-avatar");
|
|
108
|
+
avatar.src = avatarUrl;
|
|
109
|
+
avatar.alt = label;
|
|
110
|
+
suggestionItem.append(avatar);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const labelElement = document.createElement("span");
|
|
114
|
+
labelElement.classList.add("editor-suggestions-item-label");
|
|
115
|
+
labelElement.textContent = displayLabel || label;
|
|
116
|
+
suggestionItem.append(labelElement);
|
|
107
117
|
suggestion.append(suggestionItem);
|
|
108
118
|
|
|
109
119
|
suggestionItem.addEventListener("click", (ev) => {
|
|
@@ -29,9 +29,11 @@ const searchUsers = async (queryText) => {
|
|
|
29
29
|
export default Mention.extend({
|
|
30
30
|
addOptions() {
|
|
31
31
|
const parentOptions = this.parent?.();
|
|
32
|
+
const searchPromptMessage = parentOptions?.searchPromptMessage || "Type to search participants";
|
|
32
33
|
|
|
33
34
|
return {
|
|
34
35
|
...parentOptions,
|
|
36
|
+
searchPromptMessage,
|
|
35
37
|
renderText({ node }) {
|
|
36
38
|
// renderText is used to create the DOM representation
|
|
37
39
|
const label = node.attrs.label ?? node.attrs.id;
|
|
@@ -42,16 +44,25 @@ export default Mention.extend({
|
|
|
42
44
|
allowSpaces: true,
|
|
43
45
|
items: async ({ query }) => {
|
|
44
46
|
if (query.length < 2) {
|
|
45
|
-
return [];
|
|
47
|
+
return [{ label: searchPromptMessage, help: true }];
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
const data = await searchUsers(query);
|
|
49
|
-
const sorted = data.sort((
|
|
51
|
+
const sorted = data.sort((first, second) => first.nickname.localeCompare(second.nickname));
|
|
50
52
|
return sorted.slice(0, 5);
|
|
51
53
|
},
|
|
52
54
|
render: createSuggestionRenderer(this, {
|
|
53
55
|
itemConverter: (user) => {
|
|
54
|
-
|
|
56
|
+
if (user.help) {
|
|
57
|
+
return { label: user.label, help: true }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
id: user.nickname,
|
|
62
|
+
label: user.nickname,
|
|
63
|
+
displayLabel: `${user.nickname} (${user.name})`,
|
|
64
|
+
avatarUrl: user.avatarUrl
|
|
65
|
+
}
|
|
55
66
|
}
|
|
56
67
|
})
|
|
57
68
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* global jest, global */
|
|
2
2
|
|
|
3
|
-
import { createBasicEditor, updateContent } from "src/decidim/editor/test/helpers";
|
|
3
|
+
import { createBasicEditor, updateContent, sleep } from "src/decidim/editor/test/helpers";
|
|
4
4
|
|
|
5
5
|
import Mention from "src/decidim/editor/extensions/mention";
|
|
6
6
|
|
|
@@ -87,8 +87,11 @@ describe("Mention", () => {
|
|
|
87
87
|
const items = suggestions.querySelectorAll(".editor-suggestions-item");
|
|
88
88
|
expect(items.length).toEqual(expectedTags.length);
|
|
89
89
|
for (const item of items) {
|
|
90
|
-
expect(expectedTags.includes(item.textContent)).toBe(true);
|
|
90
|
+
expect(expectedTags.includes(item.querySelector(".editor-suggestions-item-label").textContent)).toBe(true);
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
expect(items[0].querySelector(".editor-suggestions-item-avatar")).toBeInstanceOf(HTMLImageElement);
|
|
94
|
+
expect(items[0].querySelector(".editor-suggestions-item-avatar").getAttribute("src")).toBe("/avatars/joannadoe.jpg");
|
|
92
95
|
});
|
|
93
96
|
|
|
94
97
|
it("does not display the suggestions when less than two characters are entered", async () => {
|
|
@@ -97,9 +100,12 @@ describe("Mention", () => {
|
|
|
97
100
|
|
|
98
101
|
const suggestions = document.querySelector(".editor-suggestions");
|
|
99
102
|
expect(suggestions).toBeInstanceOf(HTMLDivElement);
|
|
100
|
-
|
|
101
|
-
expect(
|
|
102
|
-
expect(
|
|
103
|
+
const items = suggestions.querySelectorAll(".editor-suggestions-item");
|
|
104
|
+
expect(items.length).toBe(1);
|
|
105
|
+
expect(items[0].textContent).toBe("Type to search participants");
|
|
106
|
+
expect(items[0].disabled).toBe(true);
|
|
107
|
+
expect(suggestions.classList.contains("hidden")).toBe(false);
|
|
108
|
+
expect(suggestions.classList.contains("hide")).toBe(false);
|
|
103
109
|
});
|
|
104
110
|
|
|
105
111
|
it("allows selecting a mention from the list by clicking it", async () => {
|
|
@@ -110,10 +116,10 @@ describe("Mention", () => {
|
|
|
110
116
|
suggestions.querySelector(".editor-suggestions-item").click();
|
|
111
117
|
|
|
112
118
|
expect(editorElement.innerHTML).toEqual(
|
|
113
|
-
'<p><span data-suggestion="mention" data-id="@johndoe" data-label="@johndoe
|
|
119
|
+
'<p><span data-suggestion="mention" data-id="@johndoe" data-label="@johndoe">@johndoe</span> </p>'
|
|
114
120
|
);
|
|
115
121
|
expect(normalizeHTML(editor.getHTML())).toEqual(
|
|
116
|
-
'<p><span data-type="mention" data-id="@johndoe" data-label="@johndoe
|
|
122
|
+
'<p><span data-type="mention" data-id="@johndoe" data-label="@johndoe">@johndoe</span> </p>'
|
|
117
123
|
);
|
|
118
124
|
});
|
|
119
125
|
|
|
@@ -124,10 +130,27 @@ describe("Mention", () => {
|
|
|
124
130
|
editorElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
|
|
125
131
|
|
|
126
132
|
expect(editorElement.innerHTML).toEqual(
|
|
127
|
-
'<p><span data-suggestion="mention" data-id="@johndoe" data-label="@johndoe
|
|
133
|
+
'<p><span data-suggestion="mention" data-id="@johndoe" data-label="@johndoe">@johndoe</span> </p>'
|
|
128
134
|
);
|
|
129
135
|
expect(normalizeHTML(editor.getHTML())).toEqual(
|
|
130
|
-
'<p><span data-type="mention" data-id="@johndoe" data-label="@johndoe
|
|
136
|
+
'<p><span data-type="mention" data-id="@johndoe" data-label="@johndoe">@johndoe</span> </p>'
|
|
131
137
|
);
|
|
132
138
|
});
|
|
139
|
+
|
|
140
|
+
it("allows selecting a mention when content changes after triggering", async () => {
|
|
141
|
+
editorElement.focus();
|
|
142
|
+
await updateContent(editorElement, "@joh", editor);
|
|
143
|
+
|
|
144
|
+
const suggestions = document.querySelector(".editor-suggestions");
|
|
145
|
+
expect(suggestions).toBeInstanceOf(HTMLDivElement);
|
|
146
|
+
|
|
147
|
+
editor.commands.setContent("<p>Some new content @joh</p>");
|
|
148
|
+
await sleep(50);
|
|
149
|
+
|
|
150
|
+
expect(() => {
|
|
151
|
+
suggestions.querySelector(".editor-suggestions-item").click();
|
|
152
|
+
}).not.toThrow();
|
|
153
|
+
|
|
154
|
+
expect(normalizeHTML(editor.getHTML())).toContain('data-type="mention"');
|
|
155
|
+
});
|
|
133
156
|
});
|
|
@@ -29,7 +29,16 @@ export default class MapController {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
load() {
|
|
32
|
-
|
|
32
|
+
const zoomControl = L.control.zoom({
|
|
33
|
+
zoomInTitle: this.config.zoomInText || "Zoom in",
|
|
34
|
+
zoomOutTitle: this.config.zoomOutText || "Zoom out"
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.map = L.map(this.mapId, {
|
|
38
|
+
zoomControl: false
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.map.addControl(zoomControl);
|
|
33
42
|
|
|
34
43
|
this.map.scrollWheelZoom.disable();
|
|
35
44
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This focuses an element
|
|
3
|
+
* @param {HTMLElement} element The container element to search for a focusable input or editor.
|
|
4
|
+
* @returns {void} Nothing
|
|
5
|
+
*/
|
|
6
|
+
export default function autofocus(element) {
|
|
7
|
+
let node = element.querySelector(".editor .ProseMirror, input[type='text'], textarea")
|
|
8
|
+
|
|
9
|
+
if (!node) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
node.dispatchEvent(new CustomEvent("move-cursor-to-end", { bubbles: true }));
|
|
14
|
+
|
|
15
|
+
if (typeof node.focus === "function") {
|
|
16
|
+
node.focus();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let value = node.value;
|
|
20
|
+
node.value = "";
|
|
21
|
+
node.value = value;
|
|
22
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
|
|
3
|
+
import autofocus from "src/decidim/refactor/implementation/autofocus"
|
|
4
|
+
|
|
5
|
+
describe("autofocus", () => {
|
|
6
|
+
describe("when ProseMirror editor is present", () => {
|
|
7
|
+
it("dispatches move-cursor-to-end event on ProseMirror", () => {
|
|
8
|
+
const container = document.createElement("div");
|
|
9
|
+
const editor = document.createElement("div");
|
|
10
|
+
editor.className = "editor";
|
|
11
|
+
const prosemirror = document.createElement("div");
|
|
12
|
+
prosemirror.className = "ProseMirror";
|
|
13
|
+
editor.appendChild(prosemirror);
|
|
14
|
+
container.appendChild(editor);
|
|
15
|
+
document.body.appendChild(container);
|
|
16
|
+
|
|
17
|
+
const dispatchSpy = jest.spyOn(prosemirror, "dispatchEvent");
|
|
18
|
+
|
|
19
|
+
autofocus(container);
|
|
20
|
+
|
|
21
|
+
expect(dispatchSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
22
|
+
type: "move-cursor-to-end",
|
|
23
|
+
bubbles: true
|
|
24
|
+
}));
|
|
25
|
+
dispatchSpy.mockRestore();
|
|
26
|
+
container.remove();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("calls focus on ProseMirror element", () => {
|
|
30
|
+
const container = document.createElement("div");
|
|
31
|
+
const editor = document.createElement("div");
|
|
32
|
+
editor.className = "editor";
|
|
33
|
+
const prosemirror = document.createElement("div");
|
|
34
|
+
prosemirror.className = "ProseMirror";
|
|
35
|
+
prosemirror.contentEditable = true;
|
|
36
|
+
const focusSpy = jest.spyOn(prosemirror, "focus");
|
|
37
|
+
editor.appendChild(prosemirror);
|
|
38
|
+
container.appendChild(editor);
|
|
39
|
+
document.body.appendChild(container);
|
|
40
|
+
|
|
41
|
+
autofocus(container);
|
|
42
|
+
|
|
43
|
+
expect(focusSpy).toHaveBeenCalled();
|
|
44
|
+
focusSpy.mockRestore();
|
|
45
|
+
container.remove();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("when input[type='text'] is present", () => {
|
|
50
|
+
it("focuses on the input element", () => {
|
|
51
|
+
const container = document.createElement("div");
|
|
52
|
+
const input = document.createElement("input");
|
|
53
|
+
input.type = "text";
|
|
54
|
+
input.value = "test value";
|
|
55
|
+
const focusSpy = jest.spyOn(input, "focus");
|
|
56
|
+
container.appendChild(input);
|
|
57
|
+
document.body.appendChild(container);
|
|
58
|
+
|
|
59
|
+
autofocus(container);
|
|
60
|
+
|
|
61
|
+
expect(focusSpy).toHaveBeenCalled();
|
|
62
|
+
expect(input.value).toBe("test value");
|
|
63
|
+
focusSpy.mockRestore();
|
|
64
|
+
container.remove();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("clears and restores the input value", () => {
|
|
68
|
+
const container = document.createElement("div");
|
|
69
|
+
const input = document.createElement("input");
|
|
70
|
+
input.type = "text";
|
|
71
|
+
input.value = "original value";
|
|
72
|
+
container.appendChild(input);
|
|
73
|
+
document.body.appendChild(container);
|
|
74
|
+
|
|
75
|
+
autofocus(container);
|
|
76
|
+
|
|
77
|
+
expect(input.value).toBe("original value");
|
|
78
|
+
container.remove();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("when textarea is present", () => {
|
|
83
|
+
it("focuses on the textarea element", () => {
|
|
84
|
+
const container = document.createElement("div");
|
|
85
|
+
const textarea = document.createElement("textarea");
|
|
86
|
+
textarea.value = "textarea content";
|
|
87
|
+
const focusSpy = jest.spyOn(textarea, "focus");
|
|
88
|
+
container.appendChild(textarea);
|
|
89
|
+
document.body.appendChild(container);
|
|
90
|
+
|
|
91
|
+
autofocus(container);
|
|
92
|
+
|
|
93
|
+
expect(focusSpy).toHaveBeenCalled();
|
|
94
|
+
expect(textarea.value).toBe("textarea content");
|
|
95
|
+
focusSpy.mockRestore();
|
|
96
|
+
container.remove();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("clears and restores the textarea value", () => {
|
|
100
|
+
const container = document.createElement("div");
|
|
101
|
+
const textarea = document.createElement("textarea");
|
|
102
|
+
textarea.value = "original content";
|
|
103
|
+
container.appendChild(textarea);
|
|
104
|
+
document.body.appendChild(container);
|
|
105
|
+
|
|
106
|
+
autofocus(container);
|
|
107
|
+
|
|
108
|
+
expect(textarea.value).toBe("original content");
|
|
109
|
+
container.remove();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("when no focusable element is present", () => {
|
|
114
|
+
it("does nothing and does not throw", () => {
|
|
115
|
+
const container = document.createElement("div");
|
|
116
|
+
container.innerHTML = "<p>No focusable element here</p>";
|
|
117
|
+
document.body.appendChild(container);
|
|
118
|
+
|
|
119
|
+
expect(() => autofocus(container)).not.toThrow();
|
|
120
|
+
container.remove();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("priority of elements", () => {
|
|
125
|
+
it("prefers ProseMirror over input", () => {
|
|
126
|
+
const container = document.createElement("div");
|
|
127
|
+
const editor = document.createElement("div");
|
|
128
|
+
editor.className = "editor";
|
|
129
|
+
const prosemirror = document.createElement("div");
|
|
130
|
+
prosemirror.className = "ProseMirror";
|
|
131
|
+
editor.appendChild(prosemirror);
|
|
132
|
+
const input = document.createElement("input");
|
|
133
|
+
input.type = "text";
|
|
134
|
+
editor.appendChild(input);
|
|
135
|
+
container.appendChild(editor);
|
|
136
|
+
document.body.appendChild(container);
|
|
137
|
+
|
|
138
|
+
const prosemirrorDispatchSpy = jest.spyOn(prosemirror, "dispatchEvent");
|
|
139
|
+
const inputFocusSpy = jest.spyOn(input, "focus");
|
|
140
|
+
|
|
141
|
+
autofocus(container);
|
|
142
|
+
|
|
143
|
+
expect(prosemirrorDispatchSpy).toHaveBeenCalled();
|
|
144
|
+
expect(inputFocusSpy).not.toHaveBeenCalled();
|
|
145
|
+
prosemirrorDispatchSpy.mockRestore();
|
|
146
|
+
inputFocusSpy.mockRestore();
|
|
147
|
+
container.remove();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("prefers input over textarea", () => {
|
|
151
|
+
const container = document.createElement("div");
|
|
152
|
+
const input = document.createElement("input");
|
|
153
|
+
input.type = "text";
|
|
154
|
+
const textarea = document.createElement("textarea");
|
|
155
|
+
container.appendChild(input);
|
|
156
|
+
container.appendChild(textarea);
|
|
157
|
+
document.body.appendChild(container);
|
|
158
|
+
|
|
159
|
+
const inputFocusSpy = jest.spyOn(input, "focus");
|
|
160
|
+
const textareaFocusSpy = jest.spyOn(textarea, "focus");
|
|
161
|
+
|
|
162
|
+
autofocus(container);
|
|
163
|
+
|
|
164
|
+
expect(inputFocusSpy).toHaveBeenCalled();
|
|
165
|
+
expect(textareaFocusSpy).not.toHaveBeenCalled();
|
|
166
|
+
inputFocusSpy.mockRestore();
|
|
167
|
+
textareaFocusSpy.mockRestore();
|
|
168
|
+
container.remove();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -5,14 +5,12 @@ const focusableDisableableNodes = ["BUTTON", "INPUT", "TEXTAREA", "SELECT"];
|
|
|
5
5
|
export default class FocusGuard {
|
|
6
6
|
constructor(container) {
|
|
7
7
|
this.container = container;
|
|
8
|
-
this.
|
|
9
|
-
this.triggerElement = null;
|
|
8
|
+
this.stack = [];
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
trap(element, trigger) {
|
|
13
12
|
this.enable();
|
|
14
|
-
this.
|
|
15
|
-
this.triggerElement = trigger;
|
|
13
|
+
this.stack.push({ element, trigger });
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
enable() {
|
|
@@ -45,14 +43,15 @@ export default class FocusGuard {
|
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
disable() {
|
|
48
|
-
const
|
|
49
|
-
guards.forEach((guard) => guard.remove());
|
|
46
|
+
const current = this.stack.pop();
|
|
50
47
|
|
|
51
|
-
this.
|
|
48
|
+
if (this.stack.length === 0) {
|
|
49
|
+
const guards = this.container.querySelectorAll(`:scope > .${focusGuardClass}`);
|
|
50
|
+
guards.forEach((guard) => guard.remove());
|
|
51
|
+
}
|
|
52
52
|
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
this.triggerElement = null;
|
|
53
|
+
if (current && current.trigger) {
|
|
54
|
+
current.trigger.focus();
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -67,12 +66,13 @@ export default class FocusGuard {
|
|
|
67
66
|
};
|
|
68
67
|
|
|
69
68
|
handleContainerFocus(guard) {
|
|
70
|
-
|
|
69
|
+
const current = this.stack[this.stack.length - 1];
|
|
70
|
+
if (!current || !current.element) {
|
|
71
71
|
guard.blur();
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const visibleNodes = Array.from(
|
|
75
|
+
const visibleNodes = Array.from(current.element.querySelectorAll("*")).filter((item) => {
|
|
76
76
|
return this.isVisible(item);
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
|
|
3
|
+
import FocusGuard from "src/decidim/refactor/moved/focus_guard"
|
|
4
|
+
|
|
5
|
+
describe("FocusGuard", () => {
|
|
6
|
+
let focusGuard = null;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
document.body.innerHTML = "";
|
|
10
|
+
focusGuard = new FocusGuard(document.body);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
document.body.innerHTML = "";
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("single dialog", () => {
|
|
18
|
+
it("creates focus guards on trap", () => {
|
|
19
|
+
const dialog = document.createElement("div");
|
|
20
|
+
const trigger = document.createElement("button");
|
|
21
|
+
document.body.appendChild(dialog);
|
|
22
|
+
document.body.appendChild(trigger);
|
|
23
|
+
|
|
24
|
+
focusGuard.trap(dialog, trigger);
|
|
25
|
+
const guards = document.body.querySelectorAll(".focusguard");
|
|
26
|
+
expect(guards.length).toBe(2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("removes focus guards and returns focus on disable", () => {
|
|
30
|
+
const dialog = document.createElement("div");
|
|
31
|
+
const trigger = document.createElement("button");
|
|
32
|
+
document.body.appendChild(dialog);
|
|
33
|
+
document.body.appendChild(trigger);
|
|
34
|
+
|
|
35
|
+
focusGuard.trap(dialog, trigger);
|
|
36
|
+
focusGuard.disable();
|
|
37
|
+
|
|
38
|
+
const guards = document.body.querySelectorAll(".focusguard");
|
|
39
|
+
expect(guards.length).toBe(0);
|
|
40
|
+
expect(document.activeElement).toBe(trigger);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("nested dialogs", () => {
|
|
45
|
+
it("keeps guards when inner dialog is disabled", () => {
|
|
46
|
+
const outerDialog = document.createElement("div");
|
|
47
|
+
const outerTrigger = document.createElement("button");
|
|
48
|
+
const innerDialog = document.createElement("div");
|
|
49
|
+
const innerTrigger = document.createElement("button");
|
|
50
|
+
|
|
51
|
+
document.body.appendChild(outerDialog);
|
|
52
|
+
document.body.appendChild(outerTrigger);
|
|
53
|
+
document.body.appendChild(innerDialog);
|
|
54
|
+
document.body.appendChild(innerTrigger);
|
|
55
|
+
|
|
56
|
+
focusGuard.trap(outerDialog, outerTrigger);
|
|
57
|
+
focusGuard.trap(innerDialog, innerTrigger);
|
|
58
|
+
|
|
59
|
+
expect(document.body.querySelectorAll(".focusguard").length).toBe(2);
|
|
60
|
+
|
|
61
|
+
focusGuard.disable();
|
|
62
|
+
|
|
63
|
+
expect(document.body.querySelectorAll(".focusguard").length).toBe(2);
|
|
64
|
+
expect(document.activeElement).toBe(innerTrigger);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("removes guards when outer dialog is disabled last", () => {
|
|
68
|
+
const outerDialog = document.createElement("div");
|
|
69
|
+
const outerTrigger = document.createElement("button");
|
|
70
|
+
const innerDialog = document.createElement("div");
|
|
71
|
+
const innerTrigger = document.createElement("button");
|
|
72
|
+
|
|
73
|
+
document.body.appendChild(outerDialog);
|
|
74
|
+
document.body.appendChild(outerTrigger);
|
|
75
|
+
document.body.appendChild(innerDialog);
|
|
76
|
+
document.body.appendChild(innerTrigger);
|
|
77
|
+
|
|
78
|
+
focusGuard.trap(outerDialog, outerTrigger);
|
|
79
|
+
focusGuard.trap(innerDialog, innerTrigger);
|
|
80
|
+
|
|
81
|
+
focusGuard.disable();
|
|
82
|
+
focusGuard.disable();
|
|
83
|
+
|
|
84
|
+
expect(document.body.querySelectorAll(".focusguard").length).toBe(0);
|
|
85
|
+
expect(document.activeElement).toBe(outerTrigger);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("cycles focus to the current top dialog when a guard receives focus", () => {
|
|
89
|
+
jest.spyOn(focusGuard, "isVisible").mockReturnValue(true);
|
|
90
|
+
|
|
91
|
+
const outerDialog = document.createElement("div");
|
|
92
|
+
const outerInput = document.createElement("input");
|
|
93
|
+
outerDialog.appendChild(outerInput);
|
|
94
|
+
|
|
95
|
+
const innerDialog = document.createElement("div");
|
|
96
|
+
const innerInput = document.createElement("input");
|
|
97
|
+
innerDialog.appendChild(innerInput);
|
|
98
|
+
|
|
99
|
+
document.body.appendChild(outerDialog);
|
|
100
|
+
document.body.appendChild(innerDialog);
|
|
101
|
+
|
|
102
|
+
focusGuard.trap(outerDialog, null);
|
|
103
|
+
focusGuard.trap(innerDialog, null);
|
|
104
|
+
|
|
105
|
+
const endGuard = document.body.querySelector('.focusguard[data-position="end"]');
|
|
106
|
+
endGuard.focus();
|
|
107
|
+
|
|
108
|
+
expect(document.activeElement).toBe(innerInput);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("cycles focus backward to the current top dialog when the start guard receives focus", () => {
|
|
112
|
+
jest.spyOn(focusGuard, "isVisible").mockReturnValue(true);
|
|
113
|
+
|
|
114
|
+
const outerDialog = document.createElement("div");
|
|
115
|
+
const outerInput = document.createElement("input");
|
|
116
|
+
outerDialog.appendChild(outerInput);
|
|
117
|
+
|
|
118
|
+
const innerDialog = document.createElement("div");
|
|
119
|
+
const innerInput = document.createElement("input");
|
|
120
|
+
const innerButton = document.createElement("button");
|
|
121
|
+
innerDialog.appendChild(innerInput);
|
|
122
|
+
innerDialog.appendChild(innerButton);
|
|
123
|
+
|
|
124
|
+
document.body.appendChild(outerDialog);
|
|
125
|
+
document.body.appendChild(innerDialog);
|
|
126
|
+
|
|
127
|
+
focusGuard.trap(outerDialog, null);
|
|
128
|
+
focusGuard.trap(innerDialog, null);
|
|
129
|
+
|
|
130
|
+
const startGuard = document.body.querySelector('.focusguard[data-position="start"]');
|
|
131
|
+
startGuard.focus();
|
|
132
|
+
|
|
133
|
+
expect(document.activeElement).toBe(innerButton);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -77,7 +77,7 @@ const createUnloadPreventer = () => {
|
|
|
77
77
|
ev.preventDefault();
|
|
78
78
|
ev.stopPropagation();
|
|
79
79
|
|
|
80
|
-
confirmAction(confirmMessage, link).then((answer) => {
|
|
80
|
+
confirmAction(confirmMessage, link, { iconName: "alert-line" }).then((answer) => {
|
|
81
81
|
if (!answer) {
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
@@ -109,7 +109,7 @@ const createUnloadPreventer = () => {
|
|
|
109
109
|
ev.stopImmediatePropagation();
|
|
110
110
|
ev.stopPropagation();
|
|
111
111
|
|
|
112
|
-
confirmAction(confirmMessage, button).then((answer) => {
|
|
112
|
+
confirmAction(confirmMessage, button, { iconName: "alert-line" }).then((answer) => {
|
|
113
113
|
if (!answer) {
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
@@ -13,18 +13,12 @@
|
|
|
13
13
|
@apply block max-h-40 overflow-hidden relative first:[&_*]:mt-0 before:content-[''] before:absolute before:inset-0 before:h-full before:w-full before:bg-gradient-to-b before:from-transparent before:to-white after:content-[''] after:absolute after:inset-0 after:h-full after:w-full after:bg-gradient-to-b after:from-transparent after:to-white;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
[aria-expanded
|
|
17
|
-
|
|
18
|
-
[aria-expanded="true"] > span:first-of-type,
|
|
19
|
-
[aria-expanded="true"] > svg:first-of-type {
|
|
20
|
-
@apply hidden;
|
|
16
|
+
[aria-expanded] > svg {
|
|
17
|
+
@apply transition-transform;
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
[aria-expanded="true"] > svg
|
|
24
|
-
|
|
25
|
-
[aria-expanded="false"] > span:first-of-type,
|
|
26
|
-
[aria-expanded="false"] > svg:first-of-type {
|
|
27
|
-
@apply block;
|
|
20
|
+
[aria-expanded="true"] > svg {
|
|
21
|
+
@apply rotate-180;
|
|
28
22
|
}
|
|
29
23
|
}
|
|
30
24
|
|