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.

Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/card_m_cell.rb +1 -1
  3. data/app/cells/decidim/diff/diff_mode_dropdown.erb +13 -8
  4. data/app/cells/decidim/diff/diff_mode_html.erb +13 -8
  5. data/app/cells/decidim/diff/show.erb +5 -3
  6. data/app/cells/decidim/endorsement_buttons_cell.rb.2 +211 -0
  7. data/app/cells/decidim/endorsers_list/show.erb +1 -1
  8. data/app/cells/decidim/fingerprint/show.erb +1 -1
  9. data/app/cells/decidim/followers/show.erb +1 -1
  10. data/app/cells/decidim/following/show.erb +2 -2
  11. data/app/cells/decidim/groups/show.erb +1 -1
  12. data/app/cells/decidim/members/show.erb +1 -1
  13. data/app/cells/decidim/profile_sidebar/show.erb +1 -1
  14. data/app/cells/decidim/user_conversation/messages.erb +1 -1
  15. data/app/cells/decidim/user_conversation_cell.rb +4 -0
  16. data/app/cells/decidim/user_conversations/add_conversation_users.erb +1 -1
  17. data/app/cells/decidim/version_cell.rb +1 -1
  18. data/app/cells/decidim/versions_list_cell.rb +1 -1
  19. data/app/cells/decidim/versions_list_item/show.erb +2 -2
  20. data/app/commands/decidim/messaging/reply_to_conversation.rb +4 -1
  21. data/app/commands/decidim/unendorse_resource.rb +5 -4
  22. data/app/controllers/decidim/application_controller.rb +1 -0
  23. data/app/controllers/decidim/components/base_controller.rb +0 -1
  24. data/app/events/decidim/amendable/amendment_base_event.rb +1 -1
  25. data/app/forms/decidim/messaging/message_form.rb +1 -1
  26. data/app/helpers/decidim/endorsable_helper.rb +7 -6
  27. data/app/helpers/decidim/social_share_button_helper.rb +26 -0
  28. data/app/helpers/decidim/twitter_search_helper.rb +14 -0
  29. data/app/packs/entrypoints/decidim_core.js +3 -0
  30. data/app/packs/src/decidim/back_to_list.js +26 -0
  31. data/app/packs/src/decidim/dialog_mode.js +11 -99
  32. data/app/packs/src/decidim/dialog_mode.test.js +17 -4
  33. data/app/packs/src/decidim/diff_mode_dropdown.js +3 -3
  34. data/app/packs/src/decidim/dropdowns_menus.js +1 -0
  35. data/app/packs/src/decidim/focus_guard.js +142 -0
  36. data/app/packs/src/decidim/form_filter.js +17 -1
  37. data/app/packs/src/decidim/form_remote.js +38 -0
  38. data/app/packs/src/decidim/index.js +15 -0
  39. data/app/packs/src/decidim/input_character_counter.js +4 -1
  40. data/app/packs/src/decidim/input_emoji.js +28 -5
  41. data/app/packs/src/decidim/input_multiple_mentions.js +19 -0
  42. data/app/packs/src/decidim/vendor/social-share-button.js +174 -0
  43. data/app/packs/stylesheets/decidim/modules/_buttons.scss +2 -1
  44. data/app/packs/stylesheets/decidim/modules/_forms.scss +6 -1
  45. data/app/packs/stylesheets/decidim/modules/_typography.scss +2 -0
  46. data/app/packs/stylesheets/decidim/utils/_settings.scss +1 -0
  47. data/app/packs/stylesheets/decidim/vendor/_social_share_button.scss +7 -1
  48. data/app/presenters/decidim/menu_item_presenter.rb +9 -1
  49. data/app/views/decidim/account/show.html.erb +1 -1
  50. data/app/views/decidim/application/_collection.html.erb +2 -2
  51. data/app/views/decidim/endorsements/identities.html.erb +1 -1
  52. data/app/views/decidim/groups/new.html.erb +2 -0
  53. data/app/views/decidim/messaging/conversations/_add_conversation_users.html.erb +1 -1
  54. data/app/views/decidim/messaging/conversations/_reply.html.erb +1 -1
  55. data/app/views/decidim/messaging/conversations/_start.html.erb +1 -1
  56. data/app/views/decidim/messaging/conversations/create.js.erb +1 -0
  57. data/app/views/layouts/decidim/_language_chooser.html.erb +9 -2
  58. data/app/views/layouts/decidim/_logo.html.erb +1 -1
  59. data/config/initializers/devise.rb +7 -19
  60. data/config/locales/ar.yml +55 -0
  61. data/config/locales/cs.yml +8 -0
  62. data/config/locales/de.yml +12 -1
  63. data/config/locales/en.yml +8 -0
  64. data/config/locales/fi-plain.yml +7 -0
  65. data/config/locales/fi.yml +8 -0
  66. data/config/locales/fr-CA.yml +8 -0
  67. data/config/locales/fr.yml +8 -0
  68. data/config/locales/gl.yml +10 -0
  69. data/config/locales/hu.yml +10 -0
  70. data/config/locales/it.yml +1 -0
  71. data/config/locales/ja.yml +13 -5
  72. data/db/seeds.rb +2 -2
  73. data/lib/decidim/content_renderers/link_renderer.rb +1 -1
  74. data/lib/decidim/core/engine.rb +43 -0
  75. data/lib/decidim/core/test/shared_examples/amendable/amendment_accepted_event_examples.rb +0 -1
  76. data/lib/decidim/core/test/shared_examples/amendable/amendment_created_event_examples.rb +0 -1
  77. data/lib/decidim/core/test/shared_examples/amendable/amendment_promoted_event_examples.rb +0 -1
  78. data/lib/decidim/core/test/shared_examples/amendable/amendment_rejected_event_examples.rb +0 -1
  79. data/lib/decidim/core/test/shared_examples/comments_examples.rb +27 -0
  80. data/lib/decidim/core/test/shared_examples/conversations_examples.rb +19 -0
  81. data/lib/decidim/core/test/shared_examples/endorsable.rb +69 -0
  82. data/lib/decidim/core/test.rb +2 -0
  83. data/lib/decidim/core/version.rb +1 -1
  84. data/lib/decidim/endorsable.rb +5 -1
  85. data/lib/decidim/middleware/rails_cookies.rb +23 -0
  86. data/lib/decidim/social_share/service.rb +33 -0
  87. data/lib/decidim/social_share/service_registry.rb +63 -0
  88. data/lib/decidim/social_share.rb +45 -0
  89. data/lib/decidim/view_model.rb +0 -1
  90. metadata +19 -8
  91. data/app/helpers/decidim/filter_params_helper.rb +0 -30
  92. 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, remove the focus guards from the container
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
- // Note that there may be multiple dialogs open on top of each other at
109
- // the same time.
110
- if ($(".reveal:visible", $container).length < 1) {
111
- $(`> .${focusGuardClass}`, $container).remove();
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
- // Check if the guards already exists due to some other dialog
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 = (elem) => {
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 { InputCharacterCounter, createCharacterCounter };
124
+ export {InputCharacterCounter, createCharacterCounter};
@@ -1,4 +1,4 @@
1
- import { EmojiButton } from "@joeattardi/emoji-button";
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
- btnContainer.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>'
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.insertBefore(wrapper, elem);
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
- btnContainer.addEventListener("click", () => picker.togglePicker(btnContainer))
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
- picker.on("emoji", ({ emoji }) => {
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
  };