decidim-core 0.32.0.rc3 → 0.32.0
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/author_cell.rb +0 -4
- data/app/cells/decidim/content_blocks/highlighted_content_banner_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/highlighted_participatory_spaces_cell.rb +1 -1
- data/app/commands/decidim/destroy_account.rb +12 -1
- data/app/commands/decidim/multiple_attachments_methods.rb +28 -27
- data/app/jobs/decidim/process_inactive_participant_job.rb +0 -7
- data/app/mailers/decidim/delete_user_mailer.rb +14 -0
- data/app/mailers/decidim/participants_account_mailer.rb +0 -16
- data/app/packs/src/decidim/controllers/main_menu/controller.js +33 -0
- data/app/packs/src/decidim/controllers/main_menu/main_menu.test.js +77 -0
- data/app/packs/src/decidim/controllers/mention/controller.js +296 -140
- data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +120 -457
- data/app/packs/src/decidim/controllers/multiple_mentions/controller.js +68 -32
- data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +30 -23
- data/app/packs/src/decidim/editor/common/suggestion.js +3 -1
- data/app/packs/src/decidim/editor/extensions/indent/index.js +9 -0
- data/app/packs/src/decidim/geocoding/reverse_geocoding.js +15 -5
- data/app/packs/src/decidim/geocoding/reverse_geocoding.test.js +197 -0
- data/app/packs/src/decidim/index.js +2 -2
- data/app/packs/stylesheets/decidim/_conversations.scss +14 -0
- data/app/packs/stylesheets/decidim/_dropdown.scss +1 -1
- data/app/packs/stylesheets/decidim/_editor_suggestions.scss +49 -0
- data/app/packs/stylesheets/decidim/_header.scss +12 -8
- data/app/packs/stylesheets/decidim/_tom_select.scss +23 -0
- data/app/packs/stylesheets/decidim/application.scss +2 -0
- data/app/packs/stylesheets/decidim/editor.scss +2 -33
- data/app/packs/stylesheets/decidim/geocoding_addons.scss +10 -2
- data/app/uploaders/decidim/image_uploader.rb +1 -1
- data/app/views/decidim/delete_user_mailer/delete.html.erb +6 -0
- data/app/views/decidim/messaging/conversations/_error_modal.html.erb +11 -19
- data/app/views/decidim/messaging/conversations/error.js.erb +12 -7
- data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile.html.erb +2 -1
- data/config/locales/ar.yml +0 -2
- data/config/locales/bg.yml +0 -2
- data/config/locales/ca-IT.yml +21 -10
- data/config/locales/ca.yml +21 -10
- data/config/locales/cs.yml +10 -9
- data/config/locales/de.yml +4 -13
- data/config/locales/el.yml +0 -1
- data/config/locales/en.yml +20 -9
- data/config/locales/es-MX.yml +20 -9
- data/config/locales/es-PY.yml +20 -9
- data/config/locales/es.yml +20 -9
- data/config/locales/eu.yml +36 -9
- data/config/locales/fi-plain.yml +19 -8
- data/config/locales/fi.yml +19 -8
- data/config/locales/fr-CA.yml +23 -9
- data/config/locales/fr.yml +23 -9
- data/config/locales/hu.yml +0 -2
- data/config/locales/it.yml +0 -2
- data/config/locales/ja.yml +59 -19
- data/config/locales/lb.yml +0 -2
- data/config/locales/lt.yml +0 -2
- data/config/locales/nl.yml +0 -2
- data/config/locales/no.yml +0 -2
- data/config/locales/pl.yml +2 -4
- data/config/locales/pt-BR.yml +2 -11
- data/config/locales/pt.yml +0 -2
- data/config/locales/ro-RO.yml +1 -10
- data/config/locales/sk.yml +1 -10
- data/config/locales/sv.yml +0 -9
- data/config/locales/tr-TR.yml +0 -2
- data/config/locales/zh-CN.yml +0 -2
- data/config/locales/zh-TW.yml +0 -2
- data/decidim-core.gemspec +1 -1
- data/lib/decidim/attachment_attributes.rb +58 -9
- data/lib/decidim/command.rb +1 -1
- data/lib/decidim/core/content_blocks/registry_manager.rb +4 -4
- data/lib/decidim/core/engine.rb +8 -0
- data/lib/decidim/core/test/factories.rb +3 -0
- data/lib/decidim/core/test/shared_examples/admin_resource_gallery_examples.rb +10 -10
- data/lib/decidim/core/test/shared_examples/comments_examples.rb +6 -6
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/map/autocomplete.rb +4 -3
- data/lib/decidim/searchable.rb +5 -0
- data/lib/decidim/view_model.rb +1 -1
- data/lib/tasks/decidim_mailers_tasks.rake +31 -9
- metadata +10 -9
- data/app/commands/decidim/gallery_methods.rb +0 -107
- data/app/packs/src/decidim/vendor/tribute.js +0 -1890
- data/app/packs/stylesheets/decidim/_tribute.scss +0 -36
- data/app/views/decidim/participants_account_mailer/removal_notification.html.erb +0 -11
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
import
|
|
2
|
+
import TomSelect from "tom-select/dist/cjs/tom-select.popular";
|
|
3
3
|
import icon from "src/decidim/refactor/moved/icon";
|
|
4
4
|
|
|
5
5
|
export default class extends Controller {
|
|
@@ -16,20 +16,6 @@ export default class extends Controller {
|
|
|
16
16
|
|
|
17
17
|
this.initializeEmptyFocusElement();
|
|
18
18
|
this.initializeAutoComplete();
|
|
19
|
-
this.setupSelectionListener();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Setup the selection event listener
|
|
24
|
-
* @returns {void}
|
|
25
|
-
*/
|
|
26
|
-
setupSelectionListener() {
|
|
27
|
-
this.selectionHandler = (event) => {
|
|
28
|
-
const feedback = event.detail;
|
|
29
|
-
const selection = feedback.selection;
|
|
30
|
-
this.handleSelection(selection);
|
|
31
|
-
};
|
|
32
|
-
this.searchInput.addEventListener("selection", this.selectionHandler);
|
|
33
19
|
}
|
|
34
20
|
|
|
35
21
|
/*
|
|
@@ -37,8 +23,8 @@ export default class extends Controller {
|
|
|
37
23
|
* @returns {void}
|
|
38
24
|
*/
|
|
39
25
|
disconnect() {
|
|
40
|
-
if (this.
|
|
41
|
-
this.
|
|
26
|
+
if (this.tomSelect) {
|
|
27
|
+
this.tomSelect.destroy();
|
|
42
28
|
}
|
|
43
29
|
}
|
|
44
30
|
|
|
@@ -80,11 +66,56 @@ export default class extends Controller {
|
|
|
80
66
|
* @returns {void}
|
|
81
67
|
*/
|
|
82
68
|
initializeAutoComplete() {
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
69
|
+
this.tomSelect = new TomSelect(this.searchInput, {
|
|
70
|
+
maxItems: 1,
|
|
71
|
+
valueField: "id",
|
|
72
|
+
labelField: "name",
|
|
73
|
+
searchField: ["name", "nickname"],
|
|
74
|
+
loadThrottle: 200,
|
|
75
|
+
loadingClass: "loading",
|
|
76
|
+
preload: false,
|
|
77
|
+
highlight: true,
|
|
78
|
+
load: (query, callback) => {
|
|
79
|
+
if (!query || query.length < 2) {
|
|
80
|
+
callback();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.getDataSource(query, (results) => {
|
|
84
|
+
const filtered = this.filterResults(results);
|
|
85
|
+
filtered.forEach((item) => {
|
|
86
|
+
if (item.directMessagesEnabled === "false") {
|
|
87
|
+
item.disabled = true;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
callback(filtered);
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
render: {
|
|
94
|
+
option: (data, escape) => {
|
|
95
|
+
const isDisabled = data.directMessagesEnabled === "false";
|
|
96
|
+
const className = isDisabled
|
|
97
|
+
? "disabled"
|
|
98
|
+
: "";
|
|
99
|
+
const disabledMsg = isDisabled
|
|
100
|
+
? `<small>${escape(this.searchInput.dataset.directMessagesDisabled)}</small>`
|
|
101
|
+
: "";
|
|
102
|
+
return `<div class="${className}">
|
|
103
|
+
<img src="${escape(data.avatarUrl)}" alt="${escape(data.name)}">
|
|
104
|
+
<span>${escape(data.nickname)}</span>
|
|
105
|
+
<small>${escape(data.name)}</small>
|
|
106
|
+
${disabledMsg}
|
|
107
|
+
</div>`;
|
|
108
|
+
},
|
|
109
|
+
"no_results": () => `<div class="no-results">${this.searchInput.dataset.noresults || ""}</div>`
|
|
110
|
+
},
|
|
111
|
+
onChange: (value) => {
|
|
112
|
+
if (value) {
|
|
113
|
+
const option = this.tomSelect.options[value];
|
|
114
|
+
this.handleSelection({ value: option });
|
|
115
|
+
this.tomSelect.clear();
|
|
116
|
+
this.tomSelect.clearOptions();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
88
119
|
});
|
|
89
120
|
}
|
|
90
121
|
|
|
@@ -129,7 +160,7 @@ export default class extends Controller {
|
|
|
129
160
|
*/
|
|
130
161
|
filterResults(list) {
|
|
131
162
|
return list.filter(
|
|
132
|
-
(item) => !this.selected.includes(item.
|
|
163
|
+
(item) => !this.selected.includes(item.id)
|
|
133
164
|
);
|
|
134
165
|
}
|
|
135
166
|
|
|
@@ -161,18 +192,23 @@ export default class extends Controller {
|
|
|
161
192
|
*/
|
|
162
193
|
handleSelection(selection) {
|
|
163
194
|
const id = selection.value.id;
|
|
164
|
-
// Check if we have reached the maximum limit or if direct messages are disabled
|
|
165
195
|
if (this.isMaxLimitReached() || selection.value.directMessagesEnabled === "false") {
|
|
166
196
|
return;
|
|
167
197
|
}
|
|
168
198
|
|
|
169
199
|
this.addSelectedUser(selection, id);
|
|
170
|
-
this.autoComplete.setInput("");
|
|
171
200
|
this.selected.push(id);
|
|
201
|
+
}
|
|
172
202
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Escape HTML characters in a string
|
|
205
|
+
* @param {string} str - The string to escape
|
|
206
|
+
* @returns {string} The escaped HTML string
|
|
207
|
+
*/
|
|
208
|
+
htmlEscape(str) {
|
|
209
|
+
const div = document.createElement("div");
|
|
210
|
+
div.appendChild(document.createTextNode(str));
|
|
211
|
+
return div.innerHTML;
|
|
176
212
|
}
|
|
177
213
|
|
|
178
214
|
/**
|
|
@@ -187,10 +223,10 @@ export default class extends Controller {
|
|
|
187
223
|
const listItem = document.createElement("li");
|
|
188
224
|
listItem.tabIndex = "-1";
|
|
189
225
|
listItem.innerHTML = `
|
|
190
|
-
<input type="hidden" name="${this.options.name}" value="${id}">
|
|
191
|
-
<img src="${selection.value.avatarUrl}" alt="${selection.value.name}">
|
|
192
|
-
<span>${selection.value.name}</span>
|
|
193
|
-
<button type="button" data-remove="${id}" tabindex="0" aria-controls="0" aria-label="${label}">${icon("delete-bin-line")}</button>
|
|
226
|
+
<input type="hidden" name="${this.htmlEscape(this.options.name)}" value="${this.htmlEscape(id)}">
|
|
227
|
+
<img src="${this.htmlEscape(selection.value.avatarUrl)}" alt="${this.htmlEscape(selection.value.name)}">
|
|
228
|
+
<span>${this.htmlEscape(selection.value.name)}</span>
|
|
229
|
+
<button type="button" data-remove="${this.htmlEscape(id)}" tabindex="0" aria-controls="0" aria-label="${this.htmlEscape(label)}">${icon("delete-bin-line")}</button>
|
|
194
230
|
`;
|
|
195
231
|
|
|
196
232
|
this.selectedItems.appendChild(listItem);
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
import { Application } from "@hotwired/stimulus"
|
|
8
8
|
import MultipleMentionsController from "src/decidim/controllers/multiple_mentions/controller"
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const TomSelect = require("tom-select/dist/cjs/tom-select.popular");
|
|
11
11
|
const iconMock = require("src/decidim/refactor/moved/icon");
|
|
12
12
|
|
|
13
13
|
// Mock the dependencies
|
|
14
|
-
jest.mock("
|
|
14
|
+
jest.mock("tom-select/dist/cjs/tom-select.popular", () => {
|
|
15
15
|
return jest.fn().mockImplementation(() => ({
|
|
16
|
-
|
|
16
|
+
destroy: jest.fn(),
|
|
17
|
+
clearOptions: jest.fn()
|
|
17
18
|
}));
|
|
18
19
|
});
|
|
19
20
|
|
|
@@ -114,12 +115,22 @@ describe("MultipleMentionsManager", () => {
|
|
|
114
115
|
expect(controller.emptyFocusElement).toBe(existingElement);
|
|
115
116
|
});
|
|
116
117
|
|
|
117
|
-
it("should initialize
|
|
118
|
-
expect(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
it("should initialize TomSelect with correct configuration", () => {
|
|
119
|
+
expect(TomSelect).toHaveBeenCalledWith(searchInput, {
|
|
120
|
+
maxItems: 1,
|
|
121
|
+
valueField: "id",
|
|
122
|
+
labelField: "name",
|
|
123
|
+
searchField: ["name", "nickname"],
|
|
124
|
+
loadThrottle: 200,
|
|
125
|
+
loadingClass: "loading",
|
|
126
|
+
preload: false,
|
|
127
|
+
highlight: true,
|
|
128
|
+
load: expect.any(Function),
|
|
129
|
+
render: {
|
|
130
|
+
option: expect.any(Function),
|
|
131
|
+
"no_results": expect.any(Function)
|
|
132
|
+
},
|
|
133
|
+
onChange: expect.any(Function)
|
|
123
134
|
});
|
|
124
135
|
});
|
|
125
136
|
});
|
|
@@ -226,24 +237,24 @@ describe("MultipleMentionsManager", () => {
|
|
|
226
237
|
controller.selected = ["1", "3"];
|
|
227
238
|
|
|
228
239
|
const list = [
|
|
229
|
-
{
|
|
230
|
-
{
|
|
231
|
-
{
|
|
232
|
-
{
|
|
240
|
+
{ id: "1" },
|
|
241
|
+
{ id: "2" },
|
|
242
|
+
{ id: "3" },
|
|
243
|
+
{ id: "4" }
|
|
233
244
|
];
|
|
234
245
|
|
|
235
246
|
const filtered = controller.filterResults(list);
|
|
236
247
|
|
|
237
248
|
expect(filtered).toEqual([
|
|
238
|
-
{
|
|
239
|
-
{
|
|
249
|
+
{ id: "2" },
|
|
250
|
+
{ id: "4" }
|
|
240
251
|
]);
|
|
241
252
|
});
|
|
242
253
|
|
|
243
254
|
it("should return all users when none are selected", () => {
|
|
244
255
|
const list = [
|
|
245
|
-
{
|
|
246
|
-
{
|
|
256
|
+
{ id: "1" },
|
|
257
|
+
{ id: "2" }
|
|
247
258
|
];
|
|
248
259
|
|
|
249
260
|
const filtered = controller.filterResults(list);
|
|
@@ -255,8 +266,8 @@ describe("MultipleMentionsManager", () => {
|
|
|
255
266
|
controller.selected = ["1", "2"];
|
|
256
267
|
|
|
257
268
|
const list = [
|
|
258
|
-
{
|
|
259
|
-
{
|
|
269
|
+
{ id: "1" },
|
|
270
|
+
{ id: "2" }
|
|
260
271
|
];
|
|
261
272
|
|
|
262
273
|
const filtered = controller.filterResults(list);
|
|
@@ -330,7 +341,6 @@ describe("MultipleMentionsManager", () => {
|
|
|
330
341
|
|
|
331
342
|
expect(controller.selected).toContain("1");
|
|
332
343
|
expect(selectedItems.children.length).toBe(1);
|
|
333
|
-
expect(controller.autoComplete.setInput).toHaveBeenCalledWith("");
|
|
334
344
|
});
|
|
335
345
|
|
|
336
346
|
it("should not add user when max limit is reached", () => {
|
|
@@ -350,7 +360,6 @@ describe("MultipleMentionsManager", () => {
|
|
|
350
360
|
|
|
351
361
|
expect(controller.selected).not.toContain("10");
|
|
352
362
|
expect(selectedItems.children.length).toBe(0);
|
|
353
|
-
expect(controller.autoComplete.setInput).not.toHaveBeenCalled();
|
|
354
363
|
});
|
|
355
364
|
|
|
356
365
|
it("should not add user when direct messages are disabled", () => {
|
|
@@ -368,7 +377,6 @@ describe("MultipleMentionsManager", () => {
|
|
|
368
377
|
|
|
369
378
|
expect(controller.selected).not.toContain("1");
|
|
370
379
|
expect(selectedItems.children.length).toBe(0);
|
|
371
|
-
expect(controller.autoComplete.setInput).not.toHaveBeenCalled();
|
|
372
380
|
});
|
|
373
381
|
|
|
374
382
|
it("should handle multiple valid selections", () => {
|
|
@@ -383,7 +391,6 @@ describe("MultipleMentionsManager", () => {
|
|
|
383
391
|
|
|
384
392
|
expect(controller.selected).toEqual(["1", "2"]);
|
|
385
393
|
expect(selectedItems.children.length).toBe(2);
|
|
386
|
-
expect(controller.autoComplete.setInput).toHaveBeenCalledTimes(2);
|
|
387
394
|
});
|
|
388
395
|
});
|
|
389
396
|
|
|
@@ -134,7 +134,9 @@ export const createSuggestionRenderer = (node, { itemConverter } = {}) => () =>
|
|
|
134
134
|
}
|
|
135
135
|
},
|
|
136
136
|
|
|
137
|
-
onUpdate({ clientRect, items }) {
|
|
137
|
+
onUpdate({ clientRect, items, command }) {
|
|
138
|
+
selectCommand = command;
|
|
139
|
+
|
|
138
140
|
if (!clientRect || !suggestion) {
|
|
139
141
|
return;
|
|
140
142
|
}
|
|
@@ -170,6 +170,15 @@ export default Extension.create({
|
|
|
170
170
|
"Shift-Tab": outdent,
|
|
171
171
|
Backspace: () => {
|
|
172
172
|
if (this.editor.isActive("listItem")) {
|
|
173
|
+
// When at the start of a list item, join the list items
|
|
174
|
+
// together using joinItemBackward which properly merges
|
|
175
|
+
// inline content. This ensures the items are merged
|
|
176
|
+
// correctly regardless of how the browser handles the
|
|
177
|
+
// event.
|
|
178
|
+
if (this.editor.state.selection.$head.parentOffset === 0) {
|
|
179
|
+
return this.editor.commands.joinItemBackward();
|
|
180
|
+
}
|
|
181
|
+
|
|
173
182
|
return false;
|
|
174
183
|
}
|
|
175
184
|
|
|
@@ -13,13 +13,24 @@ export const initializeReverseGeocoding = function() {
|
|
|
13
13
|
|
|
14
14
|
const setLocating = (button, enable) => {
|
|
15
15
|
if (enable) {
|
|
16
|
+
button.dataset.originalContent = button.innerHTML;
|
|
17
|
+
button.textContent = "";
|
|
18
|
+
const spinner = document.createElement("span");
|
|
19
|
+
spinner.className = "geocoding__spinner";
|
|
20
|
+
button.appendChild(spinner);
|
|
21
|
+
button.append(` ${button.dataset.locatingText || "Locating..."}`);
|
|
16
22
|
button.setAttribute("disabled", true);
|
|
23
|
+
button.classList.add("geocoding__button--locating");
|
|
17
24
|
} else {
|
|
25
|
+
if (button.dataset.originalContent) {
|
|
26
|
+
button.innerHTML = button.dataset.originalContent;
|
|
27
|
+
}
|
|
18
28
|
button.removeAttribute("disabled");
|
|
29
|
+
button.classList.remove("geocoding__button--locating");
|
|
19
30
|
}
|
|
20
31
|
};
|
|
21
32
|
|
|
22
|
-
document.querySelectorAll(".
|
|
33
|
+
document.querySelectorAll(".geocoding__button").forEach((button) => {
|
|
23
34
|
button.addEventListener("click", (event) => {
|
|
24
35
|
const target = event.target;
|
|
25
36
|
if (target.disabled) {
|
|
@@ -36,19 +47,18 @@ export const initializeReverseGeocoding = function() {
|
|
|
36
47
|
navigator.geolocation.getCurrentPosition((position) => {
|
|
37
48
|
const coordinates = [position.coords.latitude, position.coords.longitude];
|
|
38
49
|
|
|
39
|
-
// reverse geolocation
|
|
40
50
|
$.post(url, { latitude: coordinates[0], longitude: coordinates[1] }, (data) => {
|
|
41
51
|
input.value = data.address;
|
|
42
52
|
$(input).trigger("geocoder-suggest-coordinates.decidim", [coordinates]);
|
|
53
|
+
setLocating(target, false);
|
|
43
54
|
}).fail((xhr, status, error) => {
|
|
44
55
|
info(input, `${errorNoLocation} ${error}`);
|
|
56
|
+
setLocating(target, false);
|
|
45
57
|
});
|
|
46
58
|
|
|
47
|
-
setLocating(target, false);
|
|
48
|
-
|
|
49
59
|
}, (evt) => {
|
|
50
60
|
info(input, `${errorNoLocation} ${evt.message}`);
|
|
51
|
-
target
|
|
61
|
+
setLocating(target, false);
|
|
52
62
|
}, {
|
|
53
63
|
enableHighAccuracy: true
|
|
54
64
|
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/* global global, jest */
|
|
2
|
+
|
|
3
|
+
import { initializeReverseGeocoding } from "src/decidim/geocoding/reverse_geocoding";
|
|
4
|
+
|
|
5
|
+
describe("reverseGeocoding", () => {
|
|
6
|
+
let container = null;
|
|
7
|
+
let button = null;
|
|
8
|
+
let input = null;
|
|
9
|
+
let label = null;
|
|
10
|
+
let mockGeolocation = null;
|
|
11
|
+
let mockPost = null;
|
|
12
|
+
let originalGeolocation = null;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
document.body.innerHTML = "";
|
|
16
|
+
|
|
17
|
+
container = document.createElement("div");
|
|
18
|
+
container.innerHTML = `
|
|
19
|
+
<label>
|
|
20
|
+
<input id="test_address" type="text" />
|
|
21
|
+
<div class="geocoding__locate">
|
|
22
|
+
<button
|
|
23
|
+
class="geocoding__button"
|
|
24
|
+
type="button"
|
|
25
|
+
data-input="test_address"
|
|
26
|
+
data-error-no-location="Could not detect location."
|
|
27
|
+
data-error-unsupported="Device not supported."
|
|
28
|
+
data-locating-text="Locating..."
|
|
29
|
+
data-url="/locate">
|
|
30
|
+
Use my location
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</label>
|
|
34
|
+
`;
|
|
35
|
+
document.body.appendChild(container);
|
|
36
|
+
|
|
37
|
+
button = container.querySelector(".geocoding__button");
|
|
38
|
+
input = container.querySelector("#test_address");
|
|
39
|
+
label = container.querySelector("label");
|
|
40
|
+
|
|
41
|
+
originalGeolocation = navigator.geolocation;
|
|
42
|
+
mockGeolocation = {
|
|
43
|
+
getCurrentPosition: jest.fn()
|
|
44
|
+
};
|
|
45
|
+
Reflect.defineProperty(global.navigator, "geolocation", {
|
|
46
|
+
value: mockGeolocation,
|
|
47
|
+
configurable: true
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
mockPost = jest.fn();
|
|
51
|
+
global.jQuery = jest.fn(() => ({ trigger: jest.fn() }));
|
|
52
|
+
global.jQuery.post = mockPost;
|
|
53
|
+
// eslint-disable-next-line id-length
|
|
54
|
+
global.$ = global.jQuery;
|
|
55
|
+
|
|
56
|
+
initializeReverseGeocoding();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
Reflect.defineProperty(global.navigator, "geolocation", {
|
|
61
|
+
value: originalGeolocation,
|
|
62
|
+
configurable: true
|
|
63
|
+
});
|
|
64
|
+
jest.restoreAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("when clicking the button", () => {
|
|
68
|
+
it("shows the spinner and disables the button", () => {
|
|
69
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
70
|
+
|
|
71
|
+
button.click();
|
|
72
|
+
|
|
73
|
+
expect(button.disabled).toBe(true);
|
|
74
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(true);
|
|
75
|
+
expect(button.querySelector(".geocoding__spinner")).not.toBeNull();
|
|
76
|
+
expect(button.textContent).toContain("Locating...");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("uses custom locating text from data attribute", () => {
|
|
80
|
+
button.dataset.locatingText = "Finding you...";
|
|
81
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
82
|
+
|
|
83
|
+
button.click();
|
|
84
|
+
|
|
85
|
+
expect(button.textContent).toContain("Finding you...");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("stores original content for restoration", () => {
|
|
89
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
90
|
+
|
|
91
|
+
button.click();
|
|
92
|
+
|
|
93
|
+
expect(button.dataset.originalContent).toContain("Use my location");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("when geolocation succeeds", () => {
|
|
98
|
+
it("restores the button after successful reverse geocoding", () => {
|
|
99
|
+
const mockPosition = {
|
|
100
|
+
coords: { latitude: 40.7128, longitude: -74.006 }
|
|
101
|
+
};
|
|
102
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success) => {
|
|
103
|
+
success(mockPosition);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const mockDeferred = { fail: jest.fn() };
|
|
107
|
+
mockPost.mockImplementation((url, data, callback) => {
|
|
108
|
+
callback({ address: "New York, NY, USA" });
|
|
109
|
+
return mockDeferred;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
button.click();
|
|
113
|
+
|
|
114
|
+
expect(button.disabled).toBe(false);
|
|
115
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
116
|
+
expect(button.querySelector(".geocoding__spinner")).toBeNull();
|
|
117
|
+
expect(button.textContent).toContain("Use my location");
|
|
118
|
+
expect(input.value).toBe("New York, NY, USA");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("restores the button when reverse geocoding fails", () => {
|
|
122
|
+
const mockPosition = {
|
|
123
|
+
coords: { latitude: 40.7128, longitude: -74.006 }
|
|
124
|
+
};
|
|
125
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success) => {
|
|
126
|
+
success(mockPosition);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const mockDeferred = { fail: jest.fn((callback) => {
|
|
130
|
+
callback({}, "error", "Not Found");
|
|
131
|
+
}) };
|
|
132
|
+
mockPost.mockReturnValue(mockDeferred);
|
|
133
|
+
|
|
134
|
+
button.click();
|
|
135
|
+
|
|
136
|
+
expect(button.disabled).toBe(false);
|
|
137
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
138
|
+
expect(button.textContent).toContain("Use my location");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("when geolocation fails", () => {
|
|
143
|
+
it("restores the button when user denies permission", () => {
|
|
144
|
+
const mockError = { message: "User denied geolocation" };
|
|
145
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success, error) => {
|
|
146
|
+
error(mockError);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
button.click();
|
|
150
|
+
|
|
151
|
+
expect(button.disabled).toBe(false);
|
|
152
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
153
|
+
expect(button.textContent).toContain("Use my location");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("shows an error message", () => {
|
|
157
|
+
const mockError = { message: "User denied geolocation" };
|
|
158
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success, error) => {
|
|
159
|
+
error(mockError);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
button.click();
|
|
163
|
+
|
|
164
|
+
const errorElement = label.querySelector(".form-error");
|
|
165
|
+
expect(errorElement).not.toBeNull();
|
|
166
|
+
expect(errorElement.textContent).toContain("Could not detect location.");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("when geolocation is not supported", () => {
|
|
171
|
+
it("shows an unsupported error and does not show spinner", () => {
|
|
172
|
+
Reflect.defineProperty(global.navigator, "geolocation", {
|
|
173
|
+
value: null,
|
|
174
|
+
configurable: true
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
button.click();
|
|
178
|
+
|
|
179
|
+
expect(button.disabled).toBe(false);
|
|
180
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
181
|
+
const errorElement = label.querySelector(".form-error");
|
|
182
|
+
expect(errorElement).not.toBeNull();
|
|
183
|
+
expect(errorElement.textContent).toContain("Device not supported.");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("when button is already disabled", () => {
|
|
188
|
+
it("does not trigger geolocation again", () => {
|
|
189
|
+
button.disabled = true;
|
|
190
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
191
|
+
|
|
192
|
+
button.click();
|
|
193
|
+
|
|
194
|
+
expect(mockGeolocation.getCurrentPosition).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -60,6 +60,7 @@ window.Decidim = window.Decidim || {
|
|
|
60
60
|
announceForScreenReader
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
window.createDialog = createDialog;
|
|
63
64
|
window.morphdom = morphdom
|
|
64
65
|
|
|
65
66
|
// eslint-disable-next-line max-params
|
|
@@ -236,8 +237,7 @@ const initializer = (element = document) => {
|
|
|
236
237
|
document.dispatchEvent(new CustomEvent("decidim:loaded", { detail: { element } }));
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
//
|
|
240
|
-
// mentions stops working
|
|
240
|
+
// Keep this under jQuery ready to support components initialized on legacy templates
|
|
241
241
|
$(() => initializer());
|
|
242
242
|
|
|
243
243
|
// Run initializer action over the new DOM elements
|
|
@@ -88,6 +88,20 @@
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
&__modal {
|
|
91
|
+
&-error {
|
|
92
|
+
[data-dialog-closable] {
|
|
93
|
+
@apply top-6 right-6;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[data-dialog-container] {
|
|
97
|
+
@apply inline grid-cols-none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
[data-dialog-title] {
|
|
101
|
+
@apply col-span-2 md:col-span-1 md:col-start-2 grid grid-cols-[auto_1fr] items-start md:items-center gap-2 pb-4 text-left text-xl text-gray-2 md:border-b md:border-gray-3;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
&-results {
|
|
92
106
|
@apply mb-20 flex flex-wrap gap-x-8 gap-y-4;
|
|
93
107
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.editor-suggestions-props {
|
|
2
|
+
--editor-suggestions-border-color: #000;
|
|
3
|
+
--editor-suggestions-background-color: #eee;
|
|
4
|
+
--editor-suggestions-selected-highlight-color: #ccc;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.editor-suggestions {
|
|
8
|
+
@apply editor-suggestions-props border-0 bg-[var(--editor-suggestions-background-color)] max-w-sm drop-shadow-md rounded-md;
|
|
9
|
+
|
|
10
|
+
&:hover {
|
|
11
|
+
.editor-suggestions-item[data-selected]:not(:hover) {
|
|
12
|
+
@apply bg-transparent;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.editor-suggestions-item {
|
|
17
|
+
@apply flex items-center gap-2 w-full text-left py-[0.375rem] px-1.5 border-0 rounded-none text-sm;
|
|
18
|
+
|
|
19
|
+
&:first-child {
|
|
20
|
+
@apply rounded-t-md;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&:last-child {
|
|
24
|
+
@apply rounded-b-md;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&:hover,
|
|
28
|
+
&[data-selected] {
|
|
29
|
+
@apply bg-[var(--editor-suggestions-selected-highlight-color)];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.editor-suggestions-item-avatar {
|
|
33
|
+
@apply rounded-full w-6 h-6 object-cover flex-none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.editor-suggestions-item-label {
|
|
37
|
+
@apply truncate;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.editor-suggestions-item-disabled {
|
|
42
|
+
@apply cursor-default;
|
|
43
|
+
|
|
44
|
+
&:hover,
|
|
45
|
+
&[data-selected] {
|
|
46
|
+
@apply bg-transparent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|