decidim-core 0.26.0 → 0.26.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of decidim-core might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/app/cells/decidim/card_m_cell.rb +1 -1
- data/app/cells/decidim/diff/diff_mode_dropdown.erb +13 -8
- data/app/cells/decidim/diff/diff_mode_html.erb +13 -8
- data/app/cells/decidim/diff/show.erb +5 -3
- data/app/cells/decidim/endorsement_buttons_cell.rb.2 +211 -0
- data/app/cells/decidim/endorsers_list/show.erb +1 -1
- data/app/cells/decidim/fingerprint/show.erb +1 -1
- data/app/cells/decidim/followers/show.erb +1 -1
- data/app/cells/decidim/following/show.erb +2 -2
- data/app/cells/decidim/groups/show.erb +1 -1
- data/app/cells/decidim/members/show.erb +1 -1
- data/app/cells/decidim/profile_sidebar/show.erb +1 -1
- data/app/cells/decidim/user_conversation/messages.erb +1 -1
- data/app/cells/decidim/user_conversation_cell.rb +4 -0
- data/app/cells/decidim/user_conversations/add_conversation_users.erb +1 -1
- data/app/cells/decidim/version_cell.rb +1 -1
- data/app/cells/decidim/versions_list_cell.rb +1 -1
- data/app/cells/decidim/versions_list_item/show.erb +2 -2
- data/app/commands/decidim/messaging/reply_to_conversation.rb +4 -1
- data/app/commands/decidim/unendorse_resource.rb +5 -4
- data/app/controllers/decidim/application_controller.rb +1 -0
- data/app/controllers/decidim/components/base_controller.rb +0 -1
- data/app/events/decidim/amendable/amendment_base_event.rb +1 -1
- data/app/forms/decidim/messaging/message_form.rb +1 -1
- data/app/helpers/decidim/endorsable_helper.rb +7 -6
- data/app/helpers/decidim/social_share_button_helper.rb +26 -0
- data/app/helpers/decidim/twitter_search_helper.rb +14 -0
- data/app/packs/entrypoints/decidim_core.js +3 -0
- data/app/packs/src/decidim/back_to_list.js +26 -0
- data/app/packs/src/decidim/dialog_mode.js +11 -99
- data/app/packs/src/decidim/dialog_mode.test.js +17 -4
- data/app/packs/src/decidim/diff_mode_dropdown.js +3 -3
- data/app/packs/src/decidim/dropdowns_menus.js +1 -0
- data/app/packs/src/decidim/focus_guard.js +142 -0
- data/app/packs/src/decidim/form_filter.js +17 -1
- data/app/packs/src/decidim/form_remote.js +38 -0
- data/app/packs/src/decidim/index.js +15 -0
- data/app/packs/src/decidim/input_character_counter.js +4 -1
- data/app/packs/src/decidim/input_emoji.js +28 -5
- data/app/packs/src/decidim/input_multiple_mentions.js +19 -0
- data/app/packs/src/decidim/vendor/social-share-button.js +174 -0
- data/app/packs/stylesheets/decidim/modules/_buttons.scss +2 -1
- data/app/packs/stylesheets/decidim/modules/_forms.scss +6 -1
- data/app/packs/stylesheets/decidim/modules/_typography.scss +2 -0
- data/app/packs/stylesheets/decidim/utils/_settings.scss +1 -0
- data/app/packs/stylesheets/decidim/vendor/_social_share_button.scss +7 -1
- data/app/presenters/decidim/menu_item_presenter.rb +9 -1
- data/app/views/decidim/account/show.html.erb +1 -1
- data/app/views/decidim/application/_collection.html.erb +2 -2
- data/app/views/decidim/endorsements/identities.html.erb +1 -1
- data/app/views/decidim/groups/new.html.erb +2 -0
- data/app/views/decidim/messaging/conversations/_add_conversation_users.html.erb +1 -1
- data/app/views/decidim/messaging/conversations/_reply.html.erb +1 -1
- data/app/views/decidim/messaging/conversations/_start.html.erb +1 -1
- data/app/views/decidim/messaging/conversations/create.js.erb +1 -0
- data/app/views/layouts/decidim/_language_chooser.html.erb +9 -2
- data/app/views/layouts/decidim/_logo.html.erb +1 -1
- data/config/initializers/devise.rb +7 -19
- data/config/locales/ar.yml +55 -0
- data/config/locales/cs.yml +8 -0
- data/config/locales/de.yml +12 -1
- data/config/locales/en.yml +8 -0
- data/config/locales/fi-plain.yml +7 -0
- data/config/locales/fi.yml +8 -0
- data/config/locales/fr-CA.yml +8 -0
- data/config/locales/fr.yml +8 -0
- data/config/locales/gl.yml +10 -0
- data/config/locales/hu.yml +10 -0
- data/config/locales/it.yml +1 -0
- data/config/locales/ja.yml +13 -5
- data/db/seeds.rb +2 -2
- data/lib/decidim/content_renderers/link_renderer.rb +1 -1
- data/lib/decidim/core/engine.rb +43 -0
- data/lib/decidim/core/test/shared_examples/amendable/amendment_accepted_event_examples.rb +0 -1
- data/lib/decidim/core/test/shared_examples/amendable/amendment_created_event_examples.rb +0 -1
- data/lib/decidim/core/test/shared_examples/amendable/amendment_promoted_event_examples.rb +0 -1
- data/lib/decidim/core/test/shared_examples/amendable/amendment_rejected_event_examples.rb +0 -1
- data/lib/decidim/core/test/shared_examples/comments_examples.rb +27 -0
- data/lib/decidim/core/test/shared_examples/conversations_examples.rb +19 -0
- data/lib/decidim/core/test/shared_examples/endorsable.rb +69 -0
- data/lib/decidim/core/test.rb +2 -0
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/endorsable.rb +5 -1
- data/lib/decidim/middleware/rails_cookies.rb +23 -0
- data/lib/decidim/social_share/service.rb +33 -0
- data/lib/decidim/social_share/service_registry.rb +63 -0
- data/lib/decidim/social_share.rb +45 -0
- data/lib/decidim/view_model.rb +0 -1
- metadata +19 -8
- data/app/helpers/decidim/filter_params_helper.rb +0 -30
- data/config/initializers/mail_previews.rb +0 -5
@@ -1,70 +1,3 @@
|
|
1
|
-
const focusGuardClass = "focusguard";
|
2
|
-
const focusableNodes = ["A", "IFRAME", "OBJECT", "EMBED"];
|
3
|
-
const focusableDisableableNodes = ["BUTTON", "INPUT", "TEXTAREA", "SELECT"];
|
4
|
-
|
5
|
-
const isFocusGuard = (element) => {
|
6
|
-
return element.classList.contains(focusGuardClass);
|
7
|
-
}
|
8
|
-
|
9
|
-
const isFocusable = (element) => {
|
10
|
-
if (focusableNodes.indexOf(element.nodeName) > -1) {
|
11
|
-
return true;
|
12
|
-
}
|
13
|
-
if (focusableDisableableNodes.indexOf(element.nodeName) > -1 || element.getAttribute("contenteditable")) {
|
14
|
-
if (element.getAttribute("disabled")) {
|
15
|
-
return false;
|
16
|
-
}
|
17
|
-
return true;
|
18
|
-
}
|
19
|
-
|
20
|
-
const tabindex = parseInt(element.getAttribute("tabindex"), 10);
|
21
|
-
if (!isNaN(tabindex) && tabindex >= 0) {
|
22
|
-
return true;
|
23
|
-
}
|
24
|
-
|
25
|
-
return false;
|
26
|
-
}
|
27
|
-
|
28
|
-
const createFocusGuard = (position) => {
|
29
|
-
return $(`<div class="${focusGuardClass}" data-position="${position}" tabindex="0" aria-hidden="true"></div>`);
|
30
|
-
};
|
31
|
-
|
32
|
-
const handleContainerFocus = ($container, $guard) => {
|
33
|
-
const $reveal = $(".reveal:visible:last", $container);
|
34
|
-
if ($reveal.length > 0) {
|
35
|
-
handleContainerFocus($reveal, $guard);
|
36
|
-
return;
|
37
|
-
}
|
38
|
-
|
39
|
-
const $nodes = $("*:visible", $container);
|
40
|
-
let $target = null;
|
41
|
-
|
42
|
-
if ($guard.data("position") === "start") {
|
43
|
-
// Focus at the start guard, so focus the first focusable element after that
|
44
|
-
for (let ind = 0; ind < $nodes.length; ind += 1) {
|
45
|
-
if (!isFocusGuard($nodes[ind]) && isFocusable($nodes[ind])) {
|
46
|
-
$target = $($nodes[ind]);
|
47
|
-
break;
|
48
|
-
}
|
49
|
-
}
|
50
|
-
} else {
|
51
|
-
// Focus at the end guard, so focus the first focusable element after that
|
52
|
-
for (let ind = $nodes.length - 1; ind >= 0; ind -= 1) {
|
53
|
-
if (!isFocusGuard($nodes[ind]) && isFocusable($nodes[ind])) {
|
54
|
-
$target = $($nodes[ind]);
|
55
|
-
break;
|
56
|
-
}
|
57
|
-
}
|
58
|
-
}
|
59
|
-
|
60
|
-
if ($target) {
|
61
|
-
$target.trigger("focus");
|
62
|
-
} else {
|
63
|
-
// If no focusable element was found, blur the guard focus
|
64
|
-
$guard.blur();
|
65
|
-
}
|
66
|
-
};
|
67
|
-
|
68
1
|
/**
|
69
2
|
* A method to enable the dialog mode for the given dialog(s).
|
70
3
|
*
|
@@ -100,44 +33,23 @@ export default ($dialogs) => {
|
|
100
33
|
$title.trigger("focus");
|
101
34
|
}
|
102
35
|
|
103
|
-
// Once the final modal closes,
|
36
|
+
// Once the final modal closes, disable the focus guarding
|
104
37
|
$dialog.off("closed.zf.reveal.focusguard").on("closed.zf.reveal.focusguard", () => {
|
105
38
|
$dialog.off("closed.zf.reveal.focusguard");
|
106
39
|
|
107
40
|
// After the last dialog is closed, the tab guards should be removed.
|
108
|
-
//
|
109
|
-
// the
|
110
|
-
|
111
|
-
|
41
|
+
// This is done when the focus guard is disabled. If there is still a
|
42
|
+
// visible reveal item in the DOM, make that the currently "guarded"
|
43
|
+
// element. Note that there may be multiple dialogs open on top of each
|
44
|
+
// other at the same time.
|
45
|
+
const $visibleReveal = $(".reveal:visible:last", $container);
|
46
|
+
if ($visibleReveal.length > 0) {
|
47
|
+
window.focusGuard.trap($visibleReveal[0]);
|
48
|
+
} else {
|
49
|
+
window.focusGuard.disable();
|
112
50
|
}
|
113
51
|
});
|
114
52
|
|
115
|
-
|
116
|
-
const $guards = $(`> .${focusGuardClass}`, $container);
|
117
|
-
if ($guards.length > 0) {
|
118
|
-
// Make sure the guards are the first and last element as there have
|
119
|
-
// been changes in the DOM.
|
120
|
-
$guards.each((_j, guard) => {
|
121
|
-
const $guard = $(guard);
|
122
|
-
if ($guard.data("position") === "start") {
|
123
|
-
$container.prepend($guard);
|
124
|
-
} else {
|
125
|
-
$container.append($guard);
|
126
|
-
}
|
127
|
-
});
|
128
|
-
|
129
|
-
return;
|
130
|
-
}
|
131
|
-
|
132
|
-
// Add guards at the start and end of the document and attach their focus
|
133
|
-
// listeners
|
134
|
-
const $startGuard = createFocusGuard("start");
|
135
|
-
const $endGuard = createFocusGuard("end");
|
136
|
-
|
137
|
-
$container.prepend($startGuard);
|
138
|
-
$container.append($endGuard);
|
139
|
-
|
140
|
-
$startGuard.on("focus", () => handleContainerFocus($container, $startGuard));
|
141
|
-
$endGuard.on("focus", () => handleContainerFocus($container, $endGuard));
|
53
|
+
window.focusGuard.trap(dialog);
|
142
54
|
});
|
143
55
|
};
|
@@ -1,15 +1,21 @@
|
|
1
1
|
/* global jest, global */
|
2
2
|
|
3
|
+
// Custom visibility check for the tests because the of because the elements do
|
4
|
+
// not have offsetWidth or offsetHeight during the tests which are checked
|
5
|
+
// against to see whether the element is visible or not. This is also how jQuery
|
6
|
+
// behaves internally.
|
7
|
+
const isElementVisible = (element) => {
|
8
|
+
const display = global.window.getComputedStyle(element).display;
|
9
|
+
return ["inline", "block", "inline-block"].includes(display);
|
10
|
+
}
|
11
|
+
|
3
12
|
// Mock jQuery because the visibility indicator works differently within jest.
|
4
13
|
// This fixes jQuery reporting $(".element").is(":visible") correctly during the
|
5
14
|
// tests and within foundation, too.
|
6
15
|
jest.mock("jquery", () => {
|
7
16
|
const jq = jest.requireActual("jquery");
|
8
17
|
|
9
|
-
jq.expr.pseudos.visible =
|
10
|
-
const display = global.window.getComputedStyle(elem).display;
|
11
|
-
return ["inline", "block", "inline-block"].includes(display);
|
12
|
-
};
|
18
|
+
jq.expr.pseudos.visible = isElementVisible;
|
13
19
|
|
14
20
|
return jq;
|
15
21
|
});
|
@@ -17,6 +23,7 @@ jest.mock("jquery", () => {
|
|
17
23
|
import $ from "jquery"; // eslint-disable-line id-length
|
18
24
|
import "foundation-sites";
|
19
25
|
|
26
|
+
import FocusGuard from "./focus_guard.js";
|
20
27
|
import dialogMode from "./dialog_mode.js";
|
21
28
|
|
22
29
|
describe("dialogMode", () => {
|
@@ -54,6 +61,12 @@ describe("dialogMode", () => {
|
|
54
61
|
</div>
|
55
62
|
`;
|
56
63
|
|
64
|
+
window.focusGuard = new FocusGuard(document.body);
|
65
|
+
|
66
|
+
// Mock the isVisible element because the default visibility check does not
|
67
|
+
// work during the tests.
|
68
|
+
jest.spyOn(window.focusGuard, "isVisible").mockImplementation(isElementVisible);
|
69
|
+
|
57
70
|
beforeEach(() => {
|
58
71
|
$("body").html(content);
|
59
72
|
|
@@ -33,15 +33,15 @@ $(() => {
|
|
33
33
|
$(document).on("click", ".diff-view-by a.diff-view-html", (event) => {
|
34
34
|
event.preventDefault();
|
35
35
|
const $target = $(event.target);
|
36
|
-
$target.parents(".is-dropdown-submenu-parent").find("#diff-view-selected").text($target.text());
|
36
|
+
$target.parents(".is-dropdown-submenu-parent").find("#diff-view-html-selected").text($target.text());
|
37
37
|
const $visibleDiffViewsId = $allDiffViews.not(".hide").first().attr("id").split("_").slice(1, -1).join("_");
|
38
38
|
const $visibleDiffViews = $allDiffViews.filter(`[id*=${$visibleDiffViewsId}]`)
|
39
39
|
|
40
|
-
if ($target.attr("id") === "escaped-html") {
|
40
|
+
if ($target.attr("id") === "diff-view-escaped-html") {
|
41
41
|
$visibleDiffViews.filter("[id$=_unescaped]").addClass("hide");
|
42
42
|
$visibleDiffViews.filter("[id$=_escaped]").removeClass("hide");
|
43
43
|
}
|
44
|
-
if ($target.attr("id") === "unescaped-html") {
|
44
|
+
if ($target.attr("id") === "diff-view-unescaped-html") {
|
45
45
|
$visibleDiffViews.filter("[id$=_escaped]").addClass("hide");
|
46
46
|
$visibleDiffViews.filter("[id$=_unescaped]").removeClass("hide");
|
47
47
|
}
|
@@ -13,6 +13,7 @@ export default function fixDropdownMenus() {
|
|
13
13
|
// user to focus on the li element instead of the <a> element where we
|
14
14
|
// actually need the focus to be in.
|
15
15
|
$("li.is-dropdown-submenu-parent", element).removeAttr("aria-haspopup").removeAttr("aria-label");
|
16
|
+
$("li.is-dropdown-submenu-parent > a:first", element).removeAttr("aria-label");
|
16
17
|
// Foundation marks the wrong role for the submenu elements
|
17
18
|
$("ul.is-dropdown-submenu", element).attr("role", "menu");
|
18
19
|
})
|
@@ -0,0 +1,142 @@
|
|
1
|
+
import { Keyboard } from "foundation-sites"
|
2
|
+
|
3
|
+
const focusGuardClass = "focusguard";
|
4
|
+
const focusableNodes = ["A", "IFRAME", "OBJECT", "EMBED"];
|
5
|
+
const focusableDisableableNodes = ["BUTTON", "INPUT", "TEXTAREA", "SELECT"];
|
6
|
+
|
7
|
+
export default class FocusGuard {
|
8
|
+
constructor(container) {
|
9
|
+
this.container = container;
|
10
|
+
this.guardedElement = null;
|
11
|
+
}
|
12
|
+
|
13
|
+
trap(element) {
|
14
|
+
if (this.guardedElement) {
|
15
|
+
Keyboard.releaseFocus($(this.guardedElement));
|
16
|
+
}
|
17
|
+
|
18
|
+
this.enable();
|
19
|
+
this.guardedElement = element;
|
20
|
+
|
21
|
+
// Call the release focus first so that we don't accidentally add the
|
22
|
+
// keyboard trap twice. Note that the Foundation methods expect the elements
|
23
|
+
// to be jQuery elements which is why we pass them through jQuery.
|
24
|
+
Keyboard.releaseFocus($(element));
|
25
|
+
Keyboard.trapFocus($(element));
|
26
|
+
}
|
27
|
+
|
28
|
+
enable() {
|
29
|
+
// Check if the guards already exists due to some other dialog
|
30
|
+
const guards = this.container.querySelectorAll(`:scope > .${focusGuardClass}`);
|
31
|
+
if (guards.length > 0) {
|
32
|
+
// Make sure the guards are the first and last element as there have
|
33
|
+
// been changes in the DOM.
|
34
|
+
guards.forEach((guard) => {
|
35
|
+
if (guard.dataset.position === "start") {
|
36
|
+
this.container.prepend(guard);
|
37
|
+
} else {
|
38
|
+
this.container.append(guard);
|
39
|
+
}
|
40
|
+
})
|
41
|
+
|
42
|
+
return;
|
43
|
+
}
|
44
|
+
|
45
|
+
// Add guards at the start and end of the document and attach their focus
|
46
|
+
// listeners
|
47
|
+
const startGuard = this.createFocusGuard("start");
|
48
|
+
const endGuard = this.createFocusGuard("end");
|
49
|
+
|
50
|
+
this.container.prepend(startGuard);
|
51
|
+
this.container.append(endGuard);
|
52
|
+
|
53
|
+
startGuard.addEventListener("focus", () => this.handleContainerFocus(startGuard));
|
54
|
+
endGuard.addEventListener("focus", () => this.handleContainerFocus(endGuard));
|
55
|
+
}
|
56
|
+
|
57
|
+
disable() {
|
58
|
+
const guards = this.container.querySelectorAll(`:scope > .${focusGuardClass}`);
|
59
|
+
guards.forEach((guard) => guard.remove());
|
60
|
+
|
61
|
+
if (this.guardedElement) {
|
62
|
+
// Note that the Foundation methods expect the elements to be jQuery
|
63
|
+
// elements which is why we pass them through jQuery.
|
64
|
+
Keyboard.releaseFocus($(this.guardedElement));
|
65
|
+
this.guardedElement = null;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
createFocusGuard(position) {
|
70
|
+
const guard = document.createElement("div");
|
71
|
+
guard.className = focusGuardClass;
|
72
|
+
guard.dataset.position = position;
|
73
|
+
guard.tabIndex = 0;
|
74
|
+
guard.setAttribute("aria-hidden", "true");
|
75
|
+
|
76
|
+
return guard;
|
77
|
+
};
|
78
|
+
|
79
|
+
handleContainerFocus(guard) {
|
80
|
+
if (!this.guardedElement) {
|
81
|
+
guard.blur();
|
82
|
+
return;
|
83
|
+
}
|
84
|
+
|
85
|
+
const visibleNodes = Array.from(this.guardedElement.querySelectorAll("*")).filter((item) => {
|
86
|
+
return this.isVisible(item);
|
87
|
+
});
|
88
|
+
|
89
|
+
let target = null;
|
90
|
+
if (guard.dataset.position === "start") {
|
91
|
+
// Focus at the start guard, so focus the first focusable element after that
|
92
|
+
for (let ind = 0; ind < visibleNodes.length; ind += 1) {
|
93
|
+
if (!this.isFocusGuard(visibleNodes[ind]) && this.isFocusable(visibleNodes[ind])) {
|
94
|
+
target = visibleNodes[ind];
|
95
|
+
break;
|
96
|
+
}
|
97
|
+
}
|
98
|
+
} else {
|
99
|
+
// Focus at the end guard, so focus the first focusable element after that
|
100
|
+
for (let ind = visibleNodes.length - 1; ind >= 0; ind -= 1) {
|
101
|
+
if (!this.isFocusGuard(visibleNodes[ind]) && this.isFocusable(visibleNodes[ind])) {
|
102
|
+
target = visibleNodes[ind];
|
103
|
+
break;
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
if (target) {
|
109
|
+
target.focus();
|
110
|
+
} else {
|
111
|
+
// If no focusable element was found, blur the guard focus
|
112
|
+
guard.blur();
|
113
|
+
}
|
114
|
+
};
|
115
|
+
|
116
|
+
isVisible(element) {
|
117
|
+
return element.offsetWidth > 0 || element.offsetHeight > 0;
|
118
|
+
}
|
119
|
+
|
120
|
+
isFocusGuard(element) {
|
121
|
+
return element.classList.contains(focusGuardClass);
|
122
|
+
}
|
123
|
+
|
124
|
+
isFocusable(element) {
|
125
|
+
if (focusableNodes.indexOf(element.nodeName) > -1) {
|
126
|
+
return true;
|
127
|
+
}
|
128
|
+
if (focusableDisableableNodes.indexOf(element.nodeName) > -1 || element.getAttribute("contenteditable")) {
|
129
|
+
if (element.getAttribute("disabled")) {
|
130
|
+
return false;
|
131
|
+
}
|
132
|
+
return true;
|
133
|
+
}
|
134
|
+
|
135
|
+
const tabindex = parseInt(element.getAttribute("tabindex"), 10);
|
136
|
+
if (!isNaN(tabindex) && tabindex >= 0) {
|
137
|
+
return true;
|
138
|
+
}
|
139
|
+
|
140
|
+
return false;
|
141
|
+
}
|
142
|
+
}
|
@@ -279,6 +279,7 @@ export default class FormFilterComponent {
|
|
279
279
|
|
280
280
|
Rails.fire(this.$form[0], "submit");
|
281
281
|
pushState(newPath, newState);
|
282
|
+
this._saveFilters(newPath);
|
282
283
|
}
|
283
284
|
|
284
285
|
/**
|
@@ -315,5 +316,20 @@ export default class FormFilterComponent {
|
|
315
316
|
_getUID() {
|
316
317
|
return `filter-form-${new Date().setUTCMilliseconds()}-${Math.floor(Math.random() * 10000000)}`;
|
317
318
|
}
|
318
|
-
}
|
319
319
|
|
320
|
+
/**
|
321
|
+
* Saves the changed filters on sessionStorage API.
|
322
|
+
* @private
|
323
|
+
* @param {string} pathWithQueryStrings - path with all the query strings for filter. To be used with backToListLink().
|
324
|
+
* @returns {Void} - Returns nothing.
|
325
|
+
*/
|
326
|
+
_saveFilters(pathWithQueryStrings) {
|
327
|
+
if (!window.sessionStorage) {
|
328
|
+
return;
|
329
|
+
}
|
330
|
+
|
331
|
+
const pathName = this.$form.attr("action");
|
332
|
+
sessionStorage.setItem("filteredParams", JSON.stringify({[pathName]: pathWithQueryStrings}));
|
333
|
+
}
|
334
|
+
|
335
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import Rails from "@rails/ujs";
|
2
|
+
|
3
|
+
// Make the remote form submit buttons disabled when the form is being
|
4
|
+
// submitted to avoid multiple submits.
|
5
|
+
document.addEventListener("ajax:beforeSend", (ev) => {
|
6
|
+
if (!ev.target.matches("form[data-remote]")) {
|
7
|
+
return;
|
8
|
+
}
|
9
|
+
|
10
|
+
ev.target.querySelectorAll("[type=submit]").forEach((submit) => {
|
11
|
+
submit.disabled = true;
|
12
|
+
});
|
13
|
+
});
|
14
|
+
document.addEventListener("ajax:complete", (ev) => {
|
15
|
+
if (!ev.target.matches("form[data-remote]")) {
|
16
|
+
return;
|
17
|
+
}
|
18
|
+
|
19
|
+
ev.target.querySelectorAll("[type=submit]").forEach((submit) => {
|
20
|
+
submit.disabled = false;
|
21
|
+
});
|
22
|
+
});
|
23
|
+
|
24
|
+
// The forms that are attached to Foundation Abide do not work properly with
|
25
|
+
// Rails UJS Ajax forms that have the `data-remote` attribute attached to
|
26
|
+
// them. The reason is that in case Foundation Abide sees the form as valid,
|
27
|
+
// it will submit it normally bypassing the Rails UJS functionality.
|
28
|
+
// The submit events happens through jQuery in Foundation Abide which is why
|
29
|
+
// we need to bind the event with jQuery.
|
30
|
+
$(document).on("submit", "form[data-remote][data-abide]", (ev) => {
|
31
|
+
ev.preventDefault();
|
32
|
+
|
33
|
+
if (ev.target.querySelectorAll("[data-invalid]").length > 0) {
|
34
|
+
return;
|
35
|
+
}
|
36
|
+
|
37
|
+
Reflect.apply(Rails.handleRemote, ev.target, [ev]);
|
38
|
+
});
|
@@ -14,6 +14,8 @@ import DataPicker from "src/decidim/data_picker"
|
|
14
14
|
import FormFilterComponent from "src/decidim/form_filter"
|
15
15
|
import addInputEmoji from "src/decidim/input_emoji"
|
16
16
|
import dialogMode from "src/decidim/dialog_mode"
|
17
|
+
import FocusGuard from "src/decidim/focus_guard"
|
18
|
+
import backToListLink from "src/decidim/back_to_list"
|
17
19
|
|
18
20
|
window.Decidim = window.Decidim || {};
|
19
21
|
window.Decidim.config = new Configuration()
|
@@ -26,12 +28,23 @@ window.Decidim.addInputEmoji = addInputEmoji;
|
|
26
28
|
|
27
29
|
$(() => {
|
28
30
|
window.theDataPicker = new DataPicker($(".data-picker"));
|
31
|
+
window.focusGuard = new FocusGuard(document.querySelector("body"));
|
29
32
|
|
30
33
|
$(document).foundation();
|
31
34
|
$(document).on("open.zf.reveal", (ev) => {
|
32
35
|
dialogMode($(ev.target));
|
33
36
|
});
|
34
37
|
|
38
|
+
// Trap the focus within the mobile menu if the user enters it. This is an
|
39
|
+
// accessibility feature forcing the focus within the offcanvas container
|
40
|
+
// which holds the mobile menu.
|
41
|
+
$("#offCanvas").on("openedEnd.zf.offCanvas", (ev) => {
|
42
|
+
ev.target.querySelector(".main-nav a").focus();
|
43
|
+
window.focusGuard.trap(ev.target);
|
44
|
+
}).on("closed.zf.offCanvas", () => {
|
45
|
+
window.focusGuard.disable();
|
46
|
+
});
|
47
|
+
|
35
48
|
fixDropdownMenus();
|
36
49
|
|
37
50
|
svg4everybody();
|
@@ -70,4 +83,6 @@ $(() => {
|
|
70
83
|
updateExternalDomainLinks($("body"))
|
71
84
|
|
72
85
|
addInputEmoji()
|
86
|
+
|
87
|
+
backToListLink(document.querySelectorAll(".js-back-to-list"));
|
73
88
|
});
|
@@ -95,6 +95,9 @@ export default class InputCharacterCounter {
|
|
95
95
|
if (remaining === 1) {
|
96
96
|
message = MESSAGES.charactersLeft.one;
|
97
97
|
}
|
98
|
+
this.$input[0].dispatchEvent(
|
99
|
+
new CustomEvent("characterCounter", {detail: {remaining: remaining}})
|
100
|
+
);
|
98
101
|
showMessages.push(message.replace(COUNT_KEY, remaining));
|
99
102
|
}
|
100
103
|
|
@@ -118,4 +121,4 @@ $(() => {
|
|
118
121
|
});
|
119
122
|
});
|
120
123
|
|
121
|
-
export {
|
124
|
+
export {InputCharacterCounter, createCharacterCounter};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import {EmojiButton} from "@joeattardi/emoji-button";
|
2
2
|
|
3
3
|
// eslint-disable-next-line require-jsdoc
|
4
4
|
export default function addInputEmoji() {
|
@@ -23,15 +23,38 @@ export default function addInputEmoji() {
|
|
23
23
|
wrapper.className = "emoji__container"
|
24
24
|
const btnContainer = document.createElement("div");
|
25
25
|
btnContainer.className = "emoji__trigger"
|
26
|
-
|
26
|
+
const btn = document.createElement("div");
|
27
|
+
btn.className = "emoji__button"
|
28
|
+
btn.innerHTML = '<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="smile" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm4 72.6c-20.8 25-51.5 39.4-84 39.4s-63.2-14.3-84-39.4c-8.5-10.2-23.7-11.5-33.8-3.1-10.2 8.5-11.5 23.6-3.1 33.8 30 36 74.1 56.6 120.9 56.6s90.9-20.6 120.9-56.6c8.5-10.2 7.1-25.3-3.1-33.8-10.1-8.4-25.3-7.1-33.8 3.1z"></path></svg>'
|
27
29
|
|
28
|
-
elem.parentNode
|
30
|
+
const parent = elem.parentNode;
|
31
|
+
parent.insertBefore(wrapper, elem);
|
29
32
|
wrapper.appendChild(elem);
|
30
33
|
wrapper.appendChild(btnContainer);
|
34
|
+
btnContainer.appendChild(btn);
|
31
35
|
|
32
|
-
|
36
|
+
// The form errors need to be in the same container with the field they
|
37
|
+
// belong to for Foundation Abide to show them automatically.
|
38
|
+
parent.querySelectorAll(".form-error").forEach((el) => wrapper.appendChild(el));
|
33
39
|
|
34
|
-
|
40
|
+
let handlerPicker = function () {
|
41
|
+
picker.togglePicker(btnContainer);
|
42
|
+
}
|
43
|
+
|
44
|
+
btnContainer.addEventListener("click", handlerPicker);
|
45
|
+
|
46
|
+
elem.addEventListener("characterCounter", (event) => {
|
47
|
+
if (event.detail.remaining >= 4) {
|
48
|
+
btnContainer.addEventListener("click", handlerPicker);
|
49
|
+
btnContainer.removeAttribute("style");
|
50
|
+
} else {
|
51
|
+
btnContainer.removeEventListener("click", handlerPicker);
|
52
|
+
btnContainer.setAttribute("style", "color:lightgrey");
|
53
|
+
}
|
54
|
+
});
|
55
|
+
|
56
|
+
|
57
|
+
picker.on("emoji", ({emoji}) => {
|
35
58
|
elem.value += ` ${emoji} `
|
36
59
|
|
37
60
|
const event = new Event("emoji.added");
|
@@ -1,6 +1,20 @@
|
|
1
1
|
/* eslint no-unused-vars: 0 */
|
2
2
|
import Tribute from "src/decidim/vendor/tribute"
|
3
3
|
|
4
|
+
const updateSubmitButton = ($fieldContainer, $selectedItems) => {
|
5
|
+
const $form = $fieldContainer.closest("form");
|
6
|
+
if ($form.length < 1) {
|
7
|
+
return;
|
8
|
+
}
|
9
|
+
|
10
|
+
const $submitButton = $form.find("button[type='submit']");
|
11
|
+
if ($selectedItems.children().length === 0) {
|
12
|
+
$submitButton.prop("disabled", true);
|
13
|
+
} else {
|
14
|
+
$submitButton.prop("disabled", false);
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
4
18
|
$(() => {
|
5
19
|
const $multipleMentionContainer = $(".js-multiple-mentions");
|
6
20
|
const $multipleMentionRecipientsContainer = $(".js-multiple-mentions-recipients");
|
@@ -113,6 +127,7 @@ $(() => {
|
|
113
127
|
$(this).find("div").attr("tabIndex", 0).attr("aria-controls", 0).attr("aria-label", "Close").attr("role", "tab");
|
114
128
|
});
|
115
129
|
|
130
|
+
updateSubmitButton($multipleMentionContainer, $multipleMentionRecipientsContainer);
|
116
131
|
// Clean input
|
117
132
|
return "";
|
118
133
|
},
|
@@ -137,6 +152,8 @@ $(() => {
|
|
137
152
|
});
|
138
153
|
|
139
154
|
let setupEvents = function($element) {
|
155
|
+
updateSubmitButton($multipleMentionContainer, $multipleMentionRecipientsContainer);
|
156
|
+
|
140
157
|
// DOM manipulation
|
141
158
|
$element.on("focusin", (event) => {
|
142
159
|
// Set the parent container relative to the current element
|
@@ -172,6 +189,7 @@ $(() => {
|
|
172
189
|
let $target = event.target.parentNode;
|
173
190
|
if ($target.tagName === "LABEL") {
|
174
191
|
deleteRecipient($target);
|
192
|
+
updateSubmitButton($multipleMentionContainer, $multipleMentionRecipientsContainer);
|
175
193
|
}
|
176
194
|
});
|
177
195
|
// Allow delete with keypress on element in recipients list
|
@@ -179,6 +197,7 @@ $(() => {
|
|
179
197
|
let $target = event.target.parentNode;
|
180
198
|
if ($target.tagName === "LABEL") {
|
181
199
|
deleteRecipient($target);
|
200
|
+
updateSubmitButton($multipleMentionContainer, $multipleMentionRecipientsContainer);
|
182
201
|
}
|
183
202
|
});
|
184
203
|
};
|