decidim-core 0.27.0.rc1 → 0.27.0

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/announcement_cell.rb +1 -1
  3. data/app/cells/decidim/content_blocks/cta/show.erb +1 -1
  4. data/app/cells/decidim/content_blocks/cta_cell.rb +1 -1
  5. data/app/cells/decidim/content_blocks/hero/show.erb +2 -2
  6. data/app/cells/decidim/content_blocks/hero_cell.rb +1 -0
  7. data/app/cells/decidim/content_blocks/highlighted_content_banner/show.erb +2 -2
  8. data/app/cells/decidim/content_blocks/stats_cell.rb +1 -0
  9. data/app/cells/decidim/content_blocks/sub_hero_cell.rb +1 -1
  10. data/app/cells/decidim/data_consent/category.erb +19 -19
  11. data/app/cells/decidim/data_consent/dialog.erb +12 -12
  12. data/app/cells/decidim/data_consent/modal.erb +13 -13
  13. data/app/cells/decidim/data_consent_cell.rb +3 -3
  14. data/app/commands/decidim/update_account.rb +3 -1
  15. data/app/controllers/concerns/decidim/resource_versions_concern.rb +4 -0
  16. data/app/controllers/decidim/devise/registrations_controller.rb +5 -1
  17. data/app/controllers/decidim/last_activities_controller.rb +1 -0
  18. data/app/events/decidim/resource_endorsed_event.rb +2 -1
  19. data/app/forms/decidim/account_form.rb +1 -1
  20. data/app/forms/decidim/user_group_form.rb +1 -1
  21. data/app/helpers/decidim/filters_helper.rb +5 -1
  22. data/app/helpers/decidim/sanitize_helper.rb +12 -3
  23. data/app/models/decidim/action_log.rb +9 -9
  24. data/app/models/decidim/notification.rb +11 -2
  25. data/app/models/decidim/user_base_entity.rb +1 -0
  26. data/app/packs/entrypoints/decidim_core.js +1 -1
  27. data/app/packs/src/decidim/account_form.js +68 -18
  28. data/app/packs/src/decidim/{cookie_consent → data_consent}/consent_manager.js +27 -24
  29. data/app/packs/src/decidim/data_consent/consent_manager.test.js +280 -0
  30. data/app/packs/src/decidim/{cookie_consent/cookie_consent.js → data_consent/index.js} +12 -12
  31. data/app/packs/src/decidim/input_character_counter.js +57 -34
  32. data/app/packs/src/decidim/sw/a2hs.js +10 -1
  33. data/app/packs/stylesheets/decidim/modules/_cards.scss +2 -0
  34. data/app/packs/stylesheets/decidim/modules/_comments.scss +2 -0
  35. data/app/packs/stylesheets/decidim/modules/{_cookie-consent.scss → _data-consent.scss} +10 -10
  36. data/app/packs/stylesheets/decidim/modules/_forms.scss +5 -0
  37. data/app/packs/stylesheets/decidim/modules/_modules.scss +1 -1
  38. data/app/permissions/decidim/permissions.rb +4 -2
  39. data/app/presenters/decidim/push_notification_presenter.rb +2 -1
  40. data/app/presenters/decidim/user_presenter.rb +4 -1
  41. data/app/scrubbers/decidim/admin_input_scrubber.rb +25 -0
  42. data/app/scrubbers/decidim/user_input_scrubber.rb +3 -5
  43. data/app/services/decidim/iframe_disabler.rb +2 -3
  44. data/app/services/decidim/notifications_digest_sending_decider.rb +6 -2
  45. data/app/services/decidim/send_push_notification.rb +14 -12
  46. data/app/views/decidim/account/_password_fields.html.erb +2 -2
  47. data/app/views/decidim/download_your_data/show.html.erb +1 -1
  48. data/app/views/decidim/notifications_settings/show.html.erb +65 -67
  49. data/app/views/decidim/pages/_standalone.html.erb +1 -1
  50. data/app/views/decidim/pages/_tabbed.html.erb +1 -1
  51. data/app/views/decidim/user_interests/show.html.erb +11 -13
  52. data/app/views/layouts/decidim/_application.html.erb +1 -1
  53. data/app/views/layouts/decidim/_data_consent_warning.html.erb +8 -0
  54. data/app/views/layouts/decidim/_main_footer.html.erb +1 -1
  55. data/config/locales/am-ET.yml +0 -1
  56. data/config/locales/ar.yml +0 -11
  57. data/config/locales/bg.yml +1 -10
  58. data/config/locales/ca.yml +20 -25
  59. data/config/locales/cs.yml +26 -31
  60. data/config/locales/da.yml +0 -1
  61. data/config/locales/de.yml +205 -11
  62. data/config/locales/el.yml +0 -9
  63. data/config/locales/en.yml +17 -22
  64. data/config/locales/es-MX.yml +21 -26
  65. data/config/locales/es-PY.yml +20 -25
  66. data/config/locales/es.yml +22 -27
  67. data/config/locales/et.yml +0 -1
  68. data/config/locales/eu.yml +4 -13
  69. data/config/locales/fi-plain.yml +17 -22
  70. data/config/locales/fi.yml +17 -22
  71. data/config/locales/fr-CA.yml +30 -21
  72. data/config/locales/fr.yml +43 -34
  73. data/config/locales/ga-IE.yml +1 -3
  74. data/config/locales/gl.yml +0 -11
  75. data/config/locales/hr.yml +0 -1
  76. data/config/locales/hu.yml +271 -10
  77. data/config/locales/id-ID.yml +0 -11
  78. data/config/locales/is-IS.yml +2 -1
  79. data/config/locales/it.yml +3 -12
  80. data/config/locales/ja.yml +31 -25
  81. data/config/locales/ko.yml +0 -1
  82. data/config/locales/lb.yml +0 -9
  83. data/config/locales/lt.yml +1949 -0
  84. data/config/locales/lv.yml +0 -9
  85. data/config/locales/mt.yml +0 -1
  86. data/config/locales/nl.yml +18 -12
  87. data/config/locales/no.yml +1 -10
  88. data/config/locales/oc-FR.yml +1 -0
  89. data/config/locales/om-ET.yml +0 -1
  90. data/config/locales/pl.yml +1 -10
  91. data/config/locales/pt-BR.yml +4 -13
  92. data/config/locales/pt.yml +1 -10
  93. data/config/locales/ro-RO.yml +0 -9
  94. data/config/locales/ru.yml +0 -2
  95. data/config/locales/si-LK.yml +0 -1
  96. data/config/locales/sk.yml +0 -12
  97. data/config/locales/so-SO.yml +0 -1
  98. data/config/locales/sv.yml +9 -14
  99. data/config/locales/sw-KE.yml +0 -1
  100. data/config/locales/ti-ER.yml +0 -1
  101. data/config/locales/tr-TR.yml +2 -11
  102. data/config/locales/uk.yml +1 -2
  103. data/config/locales/val-ES.yml +0 -1
  104. data/config/locales/vi.yml +0 -1
  105. data/config/locales/zh-CN.yml +2 -11
  106. data/config/locales/zh-TW.yml +0 -1
  107. data/config/routes.rb +20 -2
  108. data/lib/decidim/attributes/model.rb +9 -1
  109. data/lib/decidim/content_parsers/hashtag_parser.rb +1 -1
  110. data/lib/decidim/core/engine.rb +1 -1
  111. data/lib/decidim/core/test/shared_examples/resource_endorsed_event_examples.rb +60 -0
  112. data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +40 -0
  113. data/lib/decidim/core/test/shared_examples/with_endorsable_permissions_examples.rb +1 -1
  114. data/lib/decidim/core/test.rb +2 -0
  115. data/lib/decidim/core/version.rb +1 -1
  116. data/lib/decidim/core.rb +52 -13
  117. data/lib/decidim/dependency_resolver.rb +272 -0
  118. data/lib/decidim/events/simple_event.rb +1 -0
  119. data/lib/decidim/has_resource_permission.rb +0 -2
  120. data/lib/decidim/map/provider/dynamic_map/here.rb +46 -1
  121. data/lib/decidim/nicknamizable.rb +1 -1
  122. data/lib/decidim/translatable_attributes.rb +8 -1
  123. data/lib/decidim/url_option_resolver.rb +1 -1
  124. metadata +18 -13
  125. data/app/views/decidim/devise/registrations/edit.html.erb +0 -41
  126. data/app/views/layouts/decidim/_cookie_warning.html.erb +0 -8
@@ -2,10 +2,10 @@ import Cookies from "js-cookie";
2
2
 
3
3
  class ConsentManager {
4
4
  // Options should contain the following keys:
5
- // - modal - HTML element of the cookie consent modal (e.g. "<div id="cc-modal">Foo bar</div>")
6
- // - categories - Available cookie categories (e.g. ["essential", "preferences", "analytics", "marketing"])
5
+ // - modal - HTML element of the data consent modal (e.g. "<div id="dc-modal">Foo bar</div>")
6
+ // - categories - Available data consent categories (e.g. ["essential", "preferences", "analytics", "marketing"])
7
7
  // - cookieName - Name of the cookie saved in the browser (e.g. "decidim-consent")
8
- // - warningElement - HTML element to be shown when user hasn't accepted necessary cookie(s) to display the content.
8
+ // - warningElement - HTML element to be shown when user has not given the necessary data consent to display the content.
9
9
  constructor(options) {
10
10
  this.modal = options.modal;
11
11
  this.categories = options.categories;
@@ -21,7 +21,12 @@ class ConsentManager {
21
21
 
22
22
  updateState(newState) {
23
23
  this.state = newState;
24
- Cookies.set(this.cookieName, JSON.stringify(this.state));
24
+ Cookies.set(this.cookieName, JSON.stringify(this.state), {
25
+ domain: document.location.host.split(":")[0],
26
+ sameSite: "Lax",
27
+ expires: 365,
28
+ secure: window.location.protocol === "https:"
29
+ });
25
30
  this.updateModalSelections();
26
31
  this.triggerState();
27
32
  }
@@ -46,33 +51,31 @@ class ConsentManager {
46
51
  triggerIframes() {
47
52
  if (this.allAccepted()) {
48
53
  document.querySelectorAll(".disabled-iframe").forEach((original) => {
49
- let newElement = this.transformElement(original, "iframe");
50
- newElement.className = original.classList.toString().replace("disabled-iframe", "");
51
- original.parentElement.appendChild(newElement);
52
- original.remove();
53
- })
54
+ if (original.childNodes && original.childNodes.length) {
55
+ const content = Array.from(original.childNodes).find((childNode) => {
56
+ return childNode.nodeType === Node.COMMENT_NODE;
57
+ });
58
+ if (!content) {
59
+ return;
60
+ }
61
+ const newElement = document.createElement("div");
62
+ newElement.innerHTML = content.nodeValue;
63
+ original.parentNode.replaceChild(newElement.firstElementChild, original);
64
+ }
65
+ });
54
66
  } else {
55
67
  document.querySelectorAll("iframe").forEach((original) => {
56
- const newElement = this.transformElement(original, "div");
57
- newElement.className = `disabled-iframe ${original.classList.toString()}`;
58
- original.parentElement.appendChild(newElement);
59
- original.remove();
60
- })
68
+ const newElement = document.createElement("div");
69
+ newElement.className = "disabled-iframe";
70
+ newElement.appendChild(document.createComment(`${original.outerHTML}`));
71
+ original.parentNode.replaceChild(newElement, original);
72
+ });
61
73
  }
62
74
  }
63
75
 
64
- transformElement(original, targetType) {
65
- const newElement = document.createElement(targetType);
66
- ["src", "allow", "frameborder", "style", "loading"].forEach((attribute) => {
67
- newElement.setAttribute(attribute, original.getAttribute(attribute));
68
- })
69
-
70
- return newElement;
71
- }
72
-
73
76
  triggerWarnings() {
74
77
  document.querySelectorAll(".disabled-iframe").forEach((original) => {
75
- if (original.querySelector(".cookie-warning")) {
78
+ if (original.querySelector(".dataconsent-warning")) {
76
79
  return;
77
80
  }
78
81
 
@@ -0,0 +1,280 @@
1
+ /* global global, jest */
2
+
3
+ import Cookies from "js-cookie";
4
+ import ConsentManager from "./consent_manager";
5
+
6
+ // Mock js-cookie so that we can store the return values from the "set" method
7
+ // in order to inspect their flags.
8
+ jest.mock("js-cookie", () => {
9
+ const originalModule = jest.requireActual("js-cookie");
10
+
11
+ return {
12
+ // ...originalModule,
13
+ cookieStorage: {},
14
+ get: originalModule.get,
15
+ set: function (name, value, options) {
16
+ this.cookieStorage[name] = originalModule.set(name, value, options);
17
+ }
18
+ };
19
+ });
20
+
21
+ Reflect.deleteProperty(global.window, "location");
22
+ global.window = Object.create(window);
23
+ global.window.location = {
24
+ protocol: "https:",
25
+ hostname: "decidim.dev"
26
+ };
27
+
28
+ describe("ConsentManager", () => {
29
+ const dialogContent = `
30
+ <div id="dc-dialog-wrapper" class="flex-center" role="region">
31
+ <div class="dc-dialog padding-vertical-1">
32
+ <div class="row expanded">
33
+ <div class="columns medium-12 large-8">
34
+ <div class="dc-content">
35
+ <div class="h5">Information about the cookies used on the website</div>
36
+ <div>
37
+ We use cookies on our website to improve the performance and content of the site. The cookies enable us to provide a more individual user experience and social media channels.
38
+ </div>
39
+ </div>
40
+ </div>
41
+ <div class="columns medium-12 large-4">
42
+ <div class="dc-button-wrapper flex-center">
43
+ <button id="dc-dialog-accept" class="button">
44
+ Accept all
45
+ </button>
46
+ <button id="dc-dialog-reject" class="button hollow">
47
+ Accept only essential
48
+ </button>
49
+ <button id="dc-dialog-settings" class="button clear" data-open="dc-modal" aria-controls="dc-modal" aria-haspopup="dialog" tabindex="0">
50
+ Settings
51
+ </button>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ `;
58
+ const modalContent = `
59
+ <div class="reveal dc-modal" id="dc-modal" role="dialog" data-close-on-click="false" data-close-on-esc="false" aria-modal="true" aria-hidden="true">
60
+ <div class="reveal__header">
61
+ <h3 class="reveal__title">Cookie settings</h3>
62
+ <p>
63
+ We use cookies to ensure the basic functionalities of the website and to enhance your online experience. You can choose for each category to opt-in/out whenever you want.
64
+ </p>
65
+ </div>
66
+
67
+ <div class="dc-categories">
68
+ <div class="category-wrapper margin-vertical-1" data-id="essential">
69
+ <div class="category-row flex-center">
70
+ <button class="dc-title padding-left-3">
71
+ <span class="h5 dc-category-title">
72
+ <strong>Essential</strong>
73
+ </span>
74
+ </button>
75
+ <div class="dc-switch">
76
+ <input class="switch-input" checked="checked" id="dc-essential" type="checkbox" name="essential" disabled="">
77
+ <label class="switch-paddle" for="dc-essential">
78
+ <span class="show-for-sr">Toggle Essential</span>
79
+ </label>
80
+ </div>
81
+ </div>
82
+ <div class="dc-description hide">
83
+ <div class="description-text">
84
+ <p>These cookies are essential for the proper functioning of my website. Without these cookies, the website would not work properly.</p>
85
+ </div>
86
+ <div class="dataconsent-details-wrapper">
87
+ <div class="row detail-titles">
88
+ <div class="columns small-2">Type</div>
89
+ <div class="columns small-2">Name</div>
90
+ <div class="columns small-2">Service</div>
91
+ <div class="columns small-6">Description</div>
92
+ </div>
93
+ <div class="row dataconsent-detail-row">
94
+ <div class="columns small-2">Cookie</div>
95
+ <div class="columns small-2">_session_id</div>
96
+ <div class="columns small-2">This website</div>
97
+ <div class="columns small-6">
98
+ Allows websites to remember user within a website when they move between web pages.
99
+ </div>
100
+ </div>
101
+ <div class="row dataconsent-detail-row">
102
+ <div class="columns small-2">Cookie</div>
103
+ <div class="columns small-2">decidim-consent</div>
104
+ <div class="columns small-2">This website</div>
105
+ <div class="columns small-6">
106
+ Stores information about the cookies allowed by the user on this website.
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ <div class="category-wrapper margin-vertical-1" data-id="preferences">
113
+ <div class="category-row flex-center">
114
+ <button class="dc-title padding-left-3">
115
+ <span class="h5 dc-category-title">
116
+ <strong>Preferences</strong>
117
+ </span>
118
+ </button>
119
+ <div class="dc-switch">
120
+ <input class="switch-input" id="dc-preferences" type="checkbox" name="preferences">
121
+ <label class="switch-paddle" for="dc-preferences">
122
+ <span class="show-for-sr">Toggle Preferences</span>
123
+ </label>
124
+ </div>
125
+ </div>
126
+ <div class="dc-description hide">
127
+ <div class="description-text">
128
+ <p>These cookies allow the website to remember the choices you have made in the past</p>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ <div class="category-wrapper margin-vertical-1" data-id="analytics">
133
+ <div class="category-row flex-center">
134
+ <button class="dc-title padding-left-3">
135
+ <span class="h5 dc-category-title">
136
+ <strong>Analytics and statistics</strong>
137
+ </span>
138
+ </button>
139
+ <div class="dc-switch">
140
+ <input class="switch-input" id="dc-analytics" type="checkbox" name="analytics">
141
+ <label class="switch-paddle" for="dc-analytics">
142
+ <span class="show-for-sr">Toggle Analytics and statistics</span>
143
+ </label>
144
+ </div>
145
+ </div>
146
+ <div class="dc-description hide">
147
+ <div class="description-text">
148
+ <p>Analytics cookies are cookies that track how users navigate and interact with a website. The information collected is used to help the website owner improve the website.</p>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ <div class="category-wrapper margin-vertical-1" data-id="marketing">
153
+ <div class="category-row flex-center">
154
+ <button class="dc-title padding-left-3">
155
+ <span class="h5 dc-category-title">
156
+ <strong>Marketing</strong>
157
+ </span>
158
+ </button>
159
+ <div class="dc-switch">
160
+ <input class="switch-input" id="dc-marketing" type="checkbox" name="marketing">
161
+ <label class="switch-paddle" for="dc-marketing">
162
+ <span class="show-for-sr">Toggle Marketing</span>
163
+ </label>
164
+ </div>
165
+ </div>
166
+ <div class="dc-description hide">
167
+ <div class="description-text">
168
+ <p>These cookies collect information about how you use the website, which pages you visited and which links you clicked on.</p>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ <div class="dc-buttons-wrapper flex-center">
174
+ <div class="dc-buttons-left">
175
+ <button id="dc-modal-accept" class="button" data-close="">Accept all</button>
176
+ <button id="dc-modal-reject" class="button hollow" data-close="">Accept only essential</button>
177
+ </div>
178
+ <div class="dc-buttons-right">
179
+ <button id="dc-modal-save" class="button clear" data-close="">Save settings</button>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ `;
184
+ const cookieWarningContent = `
185
+ <div class="dataconsent-warning flex-center padding-1 hide">
186
+ <p>You need to enable all cookies in order to see this content.</p>
187
+ <a href="#" class="button margin-vertical-2" data-open="dc-modal">
188
+ Change cookie settings
189
+ </a>
190
+ </div>
191
+ `;
192
+
193
+ const getCookie = (name) => {
194
+ for (const [key, value] of Object.entries(Cookies.cookieStorage)) {
195
+ if (key === name) {
196
+ const cookie = {};
197
+ value.split(";").forEach((cookieString) => {
198
+ const kv = cookieString.trim().split("=");
199
+ const val = kv[1] || true;
200
+
201
+ if (kv[0] === key) {
202
+ cookie.value = val;
203
+ } else {
204
+ cookie[kv[0]] = val;
205
+ }
206
+ });
207
+
208
+ return cookie;
209
+ }
210
+ }
211
+
212
+ return null;
213
+ };
214
+
215
+ let manager = null;
216
+ const originalLocation = window.location;
217
+
218
+ beforeEach(() => {
219
+ Cookies.cookieStorage = {};
220
+
221
+ document.body.innerHTML = dialogContent + modalContent + cookieWarningContent;
222
+
223
+ const modal = document.querySelector("#dc-modal");
224
+ const categories = [...modal.querySelectorAll(".category-wrapper")].map((el) => el.dataset.id);
225
+ manager = new ConsentManager({
226
+ modal: modal,
227
+ categories: categories,
228
+ cookieName: "decidim-consent",
229
+ warningElement: document.querySelector(".dataconsent-warning")
230
+ });
231
+ });
232
+
233
+ afterEach(() => {
234
+ window.location = originalLocation;
235
+ });
236
+
237
+ describe("updateState", () => {
238
+ let cookie = null;
239
+
240
+ describe("when the current URL is secure", () => {
241
+ beforeEach(() => {
242
+ manager.updateState({ foo: "bar" });
243
+ cookie = getCookie("decidim-consent");
244
+ });
245
+
246
+ it("sets the cookie with the correct value", () => {
247
+ expect(cookie.value).toEqual("{%22foo%22:%22bar%22}");
248
+ });
249
+
250
+ it("sets the cookie with the correct expiry", () => {
251
+ const expiry = Date.parse(cookie.expires);
252
+ const oneYearFromNow = Date.now() + 3600 * 24 * 1000 * 365;
253
+
254
+ // Expect it to be within a second of the expected expiry time because
255
+ // it would be pretty hard to try to guess the exact same millisecond.
256
+ expect(oneYearFromNow - 1000).toBeLessThan(expiry);
257
+ expect(oneYearFromNow + 1000).toBeGreaterThan(expiry);
258
+ });
259
+
260
+ it("sets the cookie with the correct flags", () => {
261
+ expect(cookie.domain).toEqual("decidim.dev");
262
+ expect(cookie.sameSite).toEqual("Lax");
263
+ expect(cookie.secure).toBe(true);
264
+ });
265
+ });
266
+
267
+ describe("when the current URL is insecure", () => {
268
+ beforeEach(() => {
269
+ Reflect.defineProperty(window.location, "protocol", { value: "http:" })
270
+
271
+ manager.updateState({ foo: "bar" });
272
+ cookie = getCookie("decidim-consent");
273
+ });
274
+
275
+ it("does not set the secure flag", () => {
276
+ expect(cookie.secure).toBeUndefined();
277
+ });
278
+ });
279
+ });
280
+ });
@@ -1,15 +1,15 @@
1
- import ConsentManager from "src/decidim/cookie_consent/consent_manager";
1
+ import ConsentManager from "src/decidim/data_consent/consent_manager";
2
2
 
3
3
  const initDialog = (manager) => {
4
4
  if (Object.keys(manager.state).length > 0) {
5
5
  return;
6
6
  }
7
- const dialogWrapper = document.querySelector("#cc-dialog-wrapper");
7
+ const dialogWrapper = document.querySelector("#dc-dialog-wrapper");
8
8
  dialogWrapper.classList.remove("hide");
9
9
 
10
- const acceptAllButton = dialogWrapper.querySelector("#cc-dialog-accept");
11
- const rejectAllButton = dialogWrapper.querySelector("#cc-dialog-reject");
12
- const settingsButton = dialogWrapper.querySelector("#cc-dialog-settings");
10
+ const acceptAllButton = dialogWrapper.querySelector("#dc-dialog-accept");
11
+ const rejectAllButton = dialogWrapper.querySelector("#dc-dialog-reject");
12
+ const settingsButton = dialogWrapper.querySelector("#dc-dialog-settings");
13
13
 
14
14
  acceptAllButton.addEventListener("click", () => {
15
15
  manager.acceptAll();
@@ -30,8 +30,8 @@ const initModal = (manager) => {
30
30
  const categoryElements = manager.modal.querySelectorAll(".category-wrapper");
31
31
 
32
32
  categoryElements.forEach((categoryEl) => {
33
- const categoryButton = categoryEl.querySelector(".cc-title");
34
- const categoryDescription = categoryEl.querySelector(".cc-description");
33
+ const categoryButton = categoryEl.querySelector(".dc-title");
34
+ const categoryDescription = categoryEl.querySelector(".dc-description");
35
35
  categoryButton.addEventListener("click", () => {
36
36
  const hidden = categoryDescription.classList.contains("hide");
37
37
  if (hidden) {
@@ -44,9 +44,9 @@ const initModal = (manager) => {
44
44
  })
45
45
  })
46
46
 
47
- const acceptAllButton = manager.modal.querySelector("#cc-modal-accept");
48
- const rejectAllButton = manager.modal.querySelector("#cc-modal-reject");
49
- const saveSettingsButton = manager.modal.querySelector("#cc-modal-save");
47
+ const acceptAllButton = manager.modal.querySelector("#dc-modal-accept");
48
+ const rejectAllButton = manager.modal.querySelector("#dc-modal-reject");
49
+ const saveSettingsButton = manager.modal.querySelector("#dc-modal-save");
50
50
 
51
51
  acceptAllButton.addEventListener("click", () => {
52
52
  manager.acceptAll();
@@ -84,7 +84,7 @@ const initDisabledIframes = (manager) => {
84
84
  }
85
85
 
86
86
  document.addEventListener("DOMContentLoaded", () => {
87
- const modal = document.querySelector("#cc-modal");
87
+ const modal = document.querySelector("#dc-modal");
88
88
  if (!modal) {
89
89
  return;
90
90
  }
@@ -94,7 +94,7 @@ document.addEventListener("DOMContentLoaded", () => {
94
94
  modal: modal,
95
95
  categories: categories,
96
96
  cookieName: window.Decidim.config.get("consent_cookie_name"),
97
- warningElement: document.querySelector(".cookie-warning")
97
+ warningElement: document.querySelector(".dataconsent-warning")
98
98
  });
99
99
 
100
100
  initDisabledIframes(manager);
@@ -1,3 +1,5 @@
1
+ /* eslint max-lines: ["error", {"max": 350, "skipBlankLines": true}] */
2
+
1
3
  const COUNT_KEY = "%count%";
2
4
  // How often SR announces the message in relation to maximum characters. E.g.
3
5
  // if max characters is 1000, screen reader announces the remaining characters
@@ -36,7 +38,7 @@ export default class InputCharacterCounter {
36
38
  // Define the closest length for the input "gaps" defined by the threshold.
37
39
  if (this.maxCharacters > 10) {
38
40
  if (this.maxCharacters > 100) {
39
- this.announceThreshold = Math.floor(this.maxCharacters * SR_ANNOUNCE_THRESHOLD_RATIO / 100) * 100;
41
+ this.announceThreshold = Math.floor(this.maxCharacters * SR_ANNOUNCE_THRESHOLD_RATIO);
40
42
  } else {
41
43
  this.announceThreshold = 10;
42
44
  }
@@ -78,9 +80,6 @@ export default class InputCharacterCounter {
78
80
  }
79
81
  }
80
82
 
81
- this.updateInputLength();
82
- this.previousInputLength = this.inputLength;
83
-
84
83
  if (this.$target.length > 0 && (this.maxCharacters > 0 || this.minCharacters > 0)) {
85
84
  // Create the screen reader target element. We don't want to constantly
86
85
  // announce every change to screen reader, only occasionally.
@@ -89,58 +88,68 @@ export default class InputCharacterCounter {
89
88
  );
90
89
  this.$target.before(this.$srTarget);
91
90
  this.$target.attr("aria-hidden", "true");
92
- this.setDescribedBy(true);
93
91
 
94
- this.bindEvents();
92
+ this.$userInput = this.$input;
93
+
94
+ // In WYSIWYG editors (Quill) we need to find the active editor from the
95
+ // DOM node. Quill has the experimental "find" method that should work
96
+ // fine in this case
97
+ if (Quill && this.$input.parent().is(".editor")) {
98
+ // Wait until the next javascript loop so Quill editors are created
99
+ setTimeout(() => {
100
+ this.editor = Quill.find(this.$input.siblings(".editor-container")[0]);
101
+ this.$userInput = $(this.editor.root);
102
+ this.initialize();
103
+ });
104
+ } else {
105
+ this.initialize();
106
+ }
95
107
  }
96
108
  }
97
109
 
110
+ initialize() {
111
+ this.updateInputLength();
112
+ this.previousInputLength = this.inputLength;
113
+
114
+ this.bindEvents();
115
+ this.setDescribedBy(true);
116
+ }
117
+
98
118
  setDescribedBy(active) {
99
119
  if (!this.describeByCounter) {
100
120
  return;
101
121
  }
102
122
 
103
123
  if (active) {
104
- this.$input.attr("aria-describedby", this.$srTarget.attr("id"));
124
+ this.$userInput.attr("aria-describedby", this.$srTarget.attr("id"));
105
125
  } else {
106
- this.$input.removeAttr("aria-describedby");
126
+ this.$userInput.removeAttr("aria-describedby");
107
127
  }
108
128
  }
109
129
 
110
130
  bindEvents() {
111
- // In WYSIWYG editors (Quill) we need to find the active editor from the
112
- // DOM node. Quill has the experimental "find" method that should work
113
- // fine in this case
114
- if (Quill && this.$input.parent().is(".editor")) {
115
- // Wait until the next javascript loop so Quill editors are created
116
- setTimeout(() => {
117
- const editor = Quill.find(this.$input.siblings(".editor-container")[0]);
118
- editor.on("text-change", () => {
119
- this.updateStatus();
120
- });
121
- })
131
+ if (this.editor) {
132
+ this.editor.on("text-change", () => {
133
+ this.handleInput();
134
+ });
135
+ } else {
136
+ this.$userInput.on("input", () => {
137
+ this.handleInput();
138
+ });
122
139
  }
123
- this.$input.on("keyup", () => {
140
+
141
+ this.$userInput.on("keyup", () => {
124
142
  this.updateStatus();
125
143
  });
126
- this.$input.on("input", () => {
127
- this.updateInputLength();
128
- this.checkScreenReaderUpdate();
129
- // If the input is "described by" the character counter, some screen
130
- // readers (NVDA) announce the status twice when it is updated. By
131
- // removing the aria-describedby attribute while the user is typing makes
132
- // the screen reader announce the status only once.
133
- this.setDescribedBy(false);
134
- });
135
- this.$input.on("focus", () => {
144
+ this.$userInput.on("focus", () => {
136
145
  this.updateScreenReaderStatus();
137
146
  });
138
- this.$input.on("blur", () => {
147
+ this.$userInput.on("blur", () => {
139
148
  this.updateScreenReaderStatus();
140
149
  this.setDescribedBy(true);
141
150
  });
142
- if (this.$input.get(0) !== null) {
143
- this.$input.get(0).addEventListener("emoji.added", () => {
151
+ if (this.$userInput.get(0) !== null) {
152
+ this.$userInput.get(0).addEventListener("emoji.added", () => {
144
153
  this.updateStatus();
145
154
  });
146
155
  }
@@ -154,7 +163,21 @@ export default class InputCharacterCounter {
154
163
 
155
164
  updateInputLength() {
156
165
  this.previousInputLength = this.inputLength;
157
- this.inputLength = this.$input.val().length;
166
+ if (this.editor) {
167
+ this.inputLength = this.editor.getLength();
168
+ } else {
169
+ this.inputLength = this.$input.val().length;
170
+ }
171
+ }
172
+
173
+ handleInput() {
174
+ this.updateInputLength();
175
+ this.checkScreenReaderUpdate();
176
+ // If the input is "described by" the character counter, some screen
177
+ // readers (NVDA) announce the status twice when it is updated. By
178
+ // removing the aria-describedby attribute while the user is typing makes
179
+ // the screen reader announce the status only once.
180
+ this.setDescribedBy(false);
158
181
  }
159
182
 
160
183
  /**
@@ -3,9 +3,17 @@ const DELAYED_VISITS = 2
3
3
  let deferredPrompt = null
4
4
 
5
5
  const shouldCountVisitedPages = () => sessionStorage.getItem("userChoice") !== "dismissed" && visitedPages.length < DELAYED_VISITS && !visitedPages.includes(location.pathname)
6
- const shouldPrompt = () => deferredPrompt && sessionStorage.getItem("userChoice") !== "dismissed" && visitedPages.length >= DELAYED_VISITS
6
+ const shouldPrompt = () => {
7
+ // Disable the application install prompt showing constantly.
8
+ if (localStorage.getItem("pwaInstallPromptSeen")) {
9
+ return false
10
+ }
11
+
12
+ return deferredPrompt && sessionStorage.getItem("userChoice") !== "dismissed" && visitedPages.length >= DELAYED_VISITS
13
+ }
7
14
 
8
15
  window.addEventListener("beforeinstallprompt", (event) => {
16
+ event.preventDefault()
9
17
  deferredPrompt = event
10
18
 
11
19
  // allow the user browse through different locations before prompt them anything
@@ -24,5 +32,6 @@ window.addEventListener("click", async (event) => {
24
32
  // store the user choice to avoid asking again in the current session
25
33
  sessionStorage.setItem("userChoice", outcome)
26
34
  sessionStorage.removeItem("visitedPages")
35
+ localStorage.setItem("pwaInstallPromptSeen", true)
27
36
  }
28
37
  });
@@ -30,6 +30,8 @@ $datetime-bg: var(--primary);
30
30
  border-radius: $card-border-radius;
31
31
  // Keep visible for accessibility (active/focused card as a link)
32
32
  overflow: visible;
33
+ overflow-wrap: break-word;
34
+ hyphens: auto;
33
35
 
34
36
  @include modifiers(
35
37
  border-top-color,
@@ -80,6 +80,8 @@ $comment-form-bg: $light-gray;
80
80
 
81
81
  .comment__content{
82
82
  padding: 0 $comment-padding;
83
+ overflow-wrap: break-word;
84
+ hyphens: auto;
83
85
 
84
86
  > :last-child{
85
87
  margin-bottom: 0;