decidim-core 0.31.2 → 0.31.4
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/content_blocks/highlighted_elements_with_cell_for_list_cell.rb +5 -1
- data/app/cells/decidim/nav_links/show.erb +3 -3
- data/app/cells/decidim/participatory_space_private_user/show.erb +6 -6
- data/app/cells/decidim/participatory_space_private_user_cell.rb +0 -4
- data/app/cells/decidim/report_button/already_reported_modal.erb +1 -1
- data/app/cells/decidim/report_button/flag_modal.erb +1 -1
- data/app/cells/decidim/report_user_button/already_reported_modal.erb +1 -1
- data/app/cells/decidim/report_user_button/flag_modal.erb +1 -1
- data/app/cells/decidim/share_text_widget/modal.erb +1 -1
- data/app/cells/decidim/upload_modal/files.erb +5 -1
- data/app/cells/decidim/upload_modal_cell.rb +10 -1
- data/app/commands/decidim/multiple_attachments_methods.rb +20 -3
- data/app/helpers/decidim/mailer_helper.rb +36 -0
- data/app/helpers/decidim/menu_helper.rb +2 -1
- data/app/helpers/decidim/newsletters_helper.rb +4 -22
- data/app/jobs/decidim/find_and_update_descendants_job.rb +8 -2
- data/app/jobs/decidim/update_search_indexes_job.rb +2 -2
- data/app/mailers/decidim/application_mailer.rb +4 -0
- data/app/packs/src/decidim/a11y.js +29 -0
- data/app/packs/src/decidim/a11y.test.js +81 -0
- data/app/packs/src/decidim/confirm.js +8 -1
- data/app/packs/src/decidim/confirm.test.js +225 -0
- data/app/packs/src/decidim/controllers/accordion/accordion.test.js +118 -0
- data/app/packs/src/decidim/controllers/accordion/controller.js +24 -0
- data/app/packs/src/decidim/controllers/dropdown/controller.js +26 -0
- data/app/packs/src/decidim/controllers/dropdown/dropdown.test.js +187 -0
- data/app/packs/src/decidim/controllers/form_validator/form_validator.js +3 -2
- data/app/packs/src/decidim/controllers/form_validator/form_validator.test.js +5 -0
- data/app/packs/src/decidim/controllers/language_change/controller.js +1 -0
- data/app/packs/src/decidim/controllers/language_change/language_change.test.js +13 -0
- data/app/packs/src/decidim/datepicker/datepicker_functions.js +26 -0
- data/app/packs/src/decidim/datepicker/generate_datepicker.js +2 -1
- data/app/packs/src/decidim/datepicker/generate_timepicker.js +3 -2
- data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +234 -0
- data/app/packs/src/decidim/editor/extensions/image/index.js +49 -11
- data/app/packs/src/decidim/editor/extensions/image/node_view.js +9 -1
- data/app/packs/src/decidim/editor/extensions/link/bubble_menu.js +34 -6
- data/app/packs/src/decidim/editor/extensions/link/index.js +45 -12
- data/app/packs/src/decidim/editor/test/extensions/image_links.test.js +161 -0
- data/app/packs/src/decidim/refactor/moved/focus_guard.js +4 -4
- data/app/packs/stylesheets/decidim/_cards.scss +12 -4
- data/app/packs/stylesheets/decidim/_flash.scss +1 -1
- data/app/packs/stylesheets/decidim/_rich_text.scss +17 -0
- data/app/packs/stylesheets/decidim/editor.scss +10 -0
- data/app/presenters/decidim/menu_item_presenter.rb +7 -1
- data/app/views/decidim/devise/invitations/edit.html.erb +3 -3
- data/app/views/decidim/devise/registrations/new.html.erb +1 -0
- data/app/views/decidim/devise/shared/_tos_fields.html.erb +3 -3
- data/app/views/decidim/notification_mailer/event_received.html.erb +3 -3
- data/app/views/decidim/pages/_tabbed.html.erb +3 -3
- data/app/views/decidim/shared/_filters.html.erb +5 -5
- data/app/views/decidim/shared/filters/_check_boxes_tree.html.erb +1 -1
- data/app/views/decidim/shared/filters/_collection.html.erb +1 -1
- data/config/initializers/devise.rb +6 -0
- data/config/locales/ar.yml +3 -3
- data/config/locales/bg.yml +0 -4
- data/config/locales/ca-IT.yml +7 -6
- data/config/locales/ca.yml +7 -6
- data/config/locales/cs.yml +5 -8
- data/config/locales/de.yml +31 -8
- data/config/locales/el.yml +0 -2
- data/config/locales/en.yml +5 -4
- data/config/locales/es-MX.yml +10 -9
- data/config/locales/es-PY.yml +10 -9
- data/config/locales/es.yml +12 -11
- data/config/locales/eu.yml +7 -5
- data/config/locales/fi-plain.yml +10 -4
- data/config/locales/fi.yml +11 -5
- data/config/locales/fr-CA.yml +7 -5
- data/config/locales/fr.yml +8 -7
- data/config/locales/gl.yml +0 -2
- data/config/locales/hu.yml +4 -8
- data/config/locales/id-ID.yml +0 -2
- data/config/locales/it.yml +1 -3
- data/config/locales/ja.yml +7 -8
- data/config/locales/lb.yml +0 -2
- data/config/locales/lt.yml +1 -3
- data/config/locales/lv.yml +0 -2
- data/config/locales/nl.yml +0 -2
- data/config/locales/no.yml +0 -2
- data/config/locales/pl.yml +0 -4
- data/config/locales/pt-BR.yml +4 -5
- data/config/locales/pt.yml +0 -2
- data/config/locales/ro-RO.yml +1 -5
- data/config/locales/ru.yml +0 -2
- data/config/locales/sk.yml +0 -4
- data/config/locales/sv.yml +8 -7
- data/config/locales/tr-TR.yml +17 -5
- data/config/locales/zh-CN.yml +0 -2
- data/config/locales/zh-TW.yml +1 -3
- data/lib/decidim/assets/tailwind/tailwind.config.js.erb +1 -1
- data/lib/decidim/content_parsers/blob_parser.rb +3 -3
- data/lib/decidim/content_renderers/blob_renderer.rb +2 -2
- data/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb +121 -0
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/form_builder.rb +58 -36
- data/lib/decidim/maintenance/taxonomy_importer.rb +1 -1
- data/lib/decidim/participatory_space_user.rb +1 -1
- data/lib/decidim/searchable.rb +4 -4
- metadata +14 -6
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
// Utility helper functions for the date and time picker functionality
|
|
2
2
|
|
|
3
|
+
export const adjustPickerPosition = (input, datePickerContainer, selector) => {
|
|
4
|
+
const parent = input.closest(selector);
|
|
5
|
+
|
|
6
|
+
if (getComputedStyle(parent).position === "static") {
|
|
7
|
+
parent.style.position = "relative";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const rect = input.getBoundingClientRect();
|
|
11
|
+
const calendarHeight = datePickerContainer.offsetHeight;
|
|
12
|
+
const spaceAbove = rect.top;
|
|
13
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
14
|
+
const openBelow = spaceBelow >= calendarHeight || spaceBelow >= spaceAbove;
|
|
15
|
+
|
|
16
|
+
if (openBelow) {
|
|
17
|
+
// Open below
|
|
18
|
+
datePickerContainer.style.top = `${input.offsetHeight}px`;
|
|
19
|
+
datePickerContainer.style.bottom = "";
|
|
20
|
+
} else {
|
|
21
|
+
// Open above
|
|
22
|
+
datePickerContainer.style.top = "";
|
|
23
|
+
datePickerContainer.style.bottom = `${input.offsetHeight}px`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
datePickerContainer.style.right = "0px";
|
|
27
|
+
};
|
|
28
|
+
|
|
3
29
|
export const setHour = (value, format) => {
|
|
4
30
|
const hour = value.split(":")[0];
|
|
5
31
|
if (format === 12) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable require-jsdoc */
|
|
2
2
|
import icon from "src/decidim/refactor/moved/icon"
|
|
3
|
-
import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos } from "src/decidim/datepicker/datepicker_functions"
|
|
3
|
+
import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos, adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions"
|
|
4
4
|
import { dateKeyDownListener, dateBeforeInputListener } from "src/decidim/datepicker/datepicker_listeners"
|
|
5
5
|
import { getDictionary } from "src/decidim/refactor/moved/i18n"
|
|
6
6
|
|
|
@@ -136,6 +136,7 @@ export default function generateDatePicker(input, row, formats) {
|
|
|
136
136
|
};
|
|
137
137
|
pickedDate = null;
|
|
138
138
|
datePickerContainer.style.display = "block";
|
|
139
|
+
adjustPickerPosition(date, datePickerContainer, ".datepicker__date-column");
|
|
139
140
|
|
|
140
141
|
document.addEventListener("click", datePickerDisplay);
|
|
141
142
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint max-lines: ["error", 310] */
|
|
3
3
|
|
|
4
4
|
import icon from "src/decidim/refactor/moved/icon"
|
|
5
|
-
import { changeHourDisplay, changeMinuteDisplay, formatDate, hourDisplay, minuteDisplay, formatTime, setHour, setMinute, updateTimeValue, updateInputValue } from "src/decidim/datepicker/datepicker_functions"
|
|
5
|
+
import { changeHourDisplay, changeMinuteDisplay, formatDate, hourDisplay, minuteDisplay, formatTime, setHour, setMinute, updateTimeValue, updateInputValue, adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions"
|
|
6
6
|
import { timeKeyDownListener, timeBeforeInputListener } from "src/decidim/datepicker/datepicker_listeners";
|
|
7
7
|
import { getDictionary } from "src/decidim/refactor/moved/i18n";
|
|
8
8
|
|
|
@@ -29,7 +29,6 @@ export default function generateTimePicker(input, row, formats) {
|
|
|
29
29
|
clock.setAttribute("disabled", input.attributes.disabled);
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
|
|
33
32
|
timeColumn.appendChild(time);
|
|
34
33
|
timeColumn.appendChild(clock);
|
|
35
34
|
|
|
@@ -279,6 +278,8 @@ export default function generateTimePicker(input, row, formats) {
|
|
|
279
278
|
event.preventDefault();
|
|
280
279
|
timePicker.style.display = "block";
|
|
281
280
|
document.addEventListener("click", timePickerDisplay);
|
|
281
|
+
adjustPickerPosition(time, timePicker, ".datepicker__time-column")
|
|
282
|
+
|
|
282
283
|
hours.value = hourDisplay(hour);
|
|
283
284
|
minutes.value = minuteDisplay(minute);
|
|
284
285
|
});
|
data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
|
|
3
|
+
import { adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions";
|
|
4
|
+
|
|
5
|
+
describe("adjustDatePickerPosition", () => {
|
|
6
|
+
let input = null;
|
|
7
|
+
let parent = null;
|
|
8
|
+
let datePickerContainer = null;
|
|
9
|
+
|
|
10
|
+
let originalInnerHeight = window.innerHeight;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Setup DOM structure
|
|
14
|
+
parent = document.createElement("div");
|
|
15
|
+
parent.className = "datepicker__date-column";
|
|
16
|
+
document.body.appendChild(parent);
|
|
17
|
+
|
|
18
|
+
input = document.createElement("input");
|
|
19
|
+
Reflect.defineProperty(input, "offsetHeight", {
|
|
20
|
+
configurable: true,
|
|
21
|
+
value: 40
|
|
22
|
+
});
|
|
23
|
+
parent.appendChild(input);
|
|
24
|
+
|
|
25
|
+
datePickerContainer = document.createElement("div");
|
|
26
|
+
datePickerContainer.className = "datepicker__container";
|
|
27
|
+
parent.appendChild(datePickerContainer);
|
|
28
|
+
|
|
29
|
+
// Mock offsetHeight for calendar
|
|
30
|
+
Reflect.defineProperty(datePickerContainer, "offsetHeight", {
|
|
31
|
+
configurable: true,
|
|
32
|
+
value: 300
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// store original viewport height
|
|
36
|
+
originalInnerHeight = window.innerHeight;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
document.body.removeChild(parent);
|
|
41
|
+
|
|
42
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
43
|
+
writable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
value: originalInnerHeight
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
jest.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("sets parent position to relative when static", () => {
|
|
52
|
+
parent.style.position = "static";
|
|
53
|
+
|
|
54
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
55
|
+
|
|
56
|
+
expect(parent.style.position).toBe("relative");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("does not change parent position when already positioned", () => {
|
|
60
|
+
parent.style.position = "absolute";
|
|
61
|
+
|
|
62
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
63
|
+
|
|
64
|
+
expect(parent.style.position).toBe("absolute");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("opens below when sufficient space below", () => {
|
|
68
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
69
|
+
top: 100,
|
|
70
|
+
bottom: 140
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
74
|
+
writable: true,
|
|
75
|
+
configurable: true,
|
|
76
|
+
value: 800
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
80
|
+
|
|
81
|
+
expect(datePickerContainer.style.top).toBe("40px");
|
|
82
|
+
expect(datePickerContainer.style.bottom).toBe("");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("opens above when insufficient space below", () => {
|
|
86
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
87
|
+
top: 400,
|
|
88
|
+
bottom: 440
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
92
|
+
writable: true,
|
|
93
|
+
configurable: true,
|
|
94
|
+
value: 500
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
98
|
+
|
|
99
|
+
expect(datePickerContainer.style.top).toBe("");
|
|
100
|
+
expect(datePickerContainer.style.bottom).toBe("40px");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("prefers opening below when space is equal above and below", () => {
|
|
104
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
105
|
+
top: 250,
|
|
106
|
+
bottom: 290
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
110
|
+
writable: true,
|
|
111
|
+
configurable: true,
|
|
112
|
+
value: 540
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
116
|
+
|
|
117
|
+
expect(datePickerContainer.style.top).toBe("40px");
|
|
118
|
+
expect(datePickerContainer.style.bottom).toBe("");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("always sets right position to 0px", () => {
|
|
122
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
123
|
+
top: 100,
|
|
124
|
+
bottom: 140
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column");
|
|
128
|
+
|
|
129
|
+
expect(datePickerContainer.style.right).toBe("0px");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
describe("adjustTimePickerPosition", () => {
|
|
135
|
+
let input = null;
|
|
136
|
+
let parent = null;
|
|
137
|
+
let timePicker = null;
|
|
138
|
+
|
|
139
|
+
let originalInnerHeight = window.innerHeight;
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
parent = document.createElement("div");
|
|
143
|
+
parent.className = "datepicker__time-column";
|
|
144
|
+
document.body.appendChild(parent);
|
|
145
|
+
|
|
146
|
+
input = document.createElement("input");
|
|
147
|
+
Reflect.defineProperty(input, "offsetHeight", {
|
|
148
|
+
configurable: true,
|
|
149
|
+
value: 30
|
|
150
|
+
});
|
|
151
|
+
parent.appendChild(input);
|
|
152
|
+
|
|
153
|
+
timePicker = document.createElement("div");
|
|
154
|
+
timePicker.className = "timepicker__container";
|
|
155
|
+
parent.appendChild(timePicker);
|
|
156
|
+
|
|
157
|
+
Reflect.defineProperty(timePicker, "offsetHeight", {
|
|
158
|
+
configurable: true,
|
|
159
|
+
value: 200
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// store original value before any test mutates it
|
|
163
|
+
originalInnerHeight = window.innerHeight;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
afterEach(() => {
|
|
167
|
+
// restore DOM
|
|
168
|
+
document.body.removeChild(parent);
|
|
169
|
+
|
|
170
|
+
// restore window.innerHeight (fix for CodeRabbit warning)
|
|
171
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
172
|
+
writable: true,
|
|
173
|
+
configurable: true,
|
|
174
|
+
value: originalInnerHeight
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
jest.restoreAllMocks();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("sets parent position to relative when static", () => {
|
|
181
|
+
parent.style.position = "static";
|
|
182
|
+
|
|
183
|
+
adjustPickerPosition(input, timePicker, ".datepicker__time-column");
|
|
184
|
+
|
|
185
|
+
expect(parent.style.position).toBe("relative");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("opens below when there is enough space", () => {
|
|
189
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
190
|
+
top: 100,
|
|
191
|
+
bottom: 130
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
195
|
+
writable: true,
|
|
196
|
+
configurable: true,
|
|
197
|
+
value: 700
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
adjustPickerPosition(input, timePicker, ".datepicker__time-column");
|
|
201
|
+
|
|
202
|
+
expect(timePicker.style.top).toBe("30px");
|
|
203
|
+
expect(timePicker.style.bottom).toBe("");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("opens above when there is not enough space below", () => {
|
|
207
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
208
|
+
top: 400,
|
|
209
|
+
bottom: 430
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
Reflect.defineProperty(window, "innerHeight", {
|
|
213
|
+
writable: true,
|
|
214
|
+
configurable: true,
|
|
215
|
+
value: 500
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
adjustPickerPosition(input, timePicker, ".datepicker__time-column");
|
|
219
|
+
|
|
220
|
+
expect(timePicker.style.top).toBe("");
|
|
221
|
+
expect(timePicker.style.bottom).toBe("30px");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("always aligns to the right", () => {
|
|
225
|
+
jest.spyOn(input, "getBoundingClientRect").mockReturnValue({
|
|
226
|
+
top: 100,
|
|
227
|
+
bottom: 130
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
adjustPickerPosition(input, timePicker, ".datepicker__time-column");
|
|
231
|
+
|
|
232
|
+
expect(timePicker.style.right).toBe("0px");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -80,7 +80,9 @@ export default Image.extend({
|
|
|
80
80
|
addAttributes() {
|
|
81
81
|
return {
|
|
82
82
|
...this.parent?.(),
|
|
83
|
-
width: { default: null }
|
|
83
|
+
width: { default: null },
|
|
84
|
+
href: { default: null },
|
|
85
|
+
target: { default: null }
|
|
84
86
|
};
|
|
85
87
|
},
|
|
86
88
|
|
|
@@ -92,13 +94,16 @@ export default Image.extend({
|
|
|
92
94
|
...this.parent?.(),
|
|
93
95
|
imageDialog: () => async ({ dispatch }) => {
|
|
94
96
|
if (dispatch) {
|
|
95
|
-
let { src, alt, width } = this.editor.getAttributes("image");
|
|
97
|
+
let { src, alt, width, href, target } = this.editor.getAttributes("image");
|
|
96
98
|
|
|
97
99
|
this.editor.commands.toggleDialog(true);
|
|
98
|
-
const dialogState = await uploadDialog.toggle(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const dialogState = await uploadDialog.toggle(
|
|
101
|
+
{ src, alt },
|
|
102
|
+
{
|
|
103
|
+
inputLabel: i18n.altLabel,
|
|
104
|
+
uploadHandler: async (file) => uploadImage(file, this.options.uploadImagesPath)
|
|
105
|
+
}
|
|
106
|
+
);
|
|
102
107
|
this.editor.commands.toggleDialog(false);
|
|
103
108
|
|
|
104
109
|
if (dialogState !== "save") {
|
|
@@ -113,8 +118,7 @@ export default Image.extend({
|
|
|
113
118
|
|
|
114
119
|
src = uploadDialog.getValue("src");
|
|
115
120
|
alt = uploadDialog.getValue("alt");
|
|
116
|
-
|
|
117
|
-
return this.editor.chain().setImage({ src, alt, width }).focus(null, { scrollIntoView: false }).run();
|
|
121
|
+
return this.editor.chain().setImage({ src, alt, width, href, target }).focus(null, { scrollIntoView: false }).run();
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
return true;
|
|
@@ -127,18 +131,52 @@ export default Image.extend({
|
|
|
127
131
|
},
|
|
128
132
|
|
|
129
133
|
parseHTML() {
|
|
130
|
-
return [
|
|
134
|
+
return [
|
|
135
|
+
{
|
|
136
|
+
tag: "div[data-image] img[src]:not([src^='data:'])",
|
|
137
|
+
getAttrs: (dom) => {
|
|
138
|
+
// Check if the image's parent div is wrapped in a link
|
|
139
|
+
const imageDiv = dom.closest("div[data-image]");
|
|
140
|
+
const link = imageDiv?.parentElement;
|
|
141
|
+
if (link && link.tagName === "A" && link.hasAttribute("href")) {
|
|
142
|
+
const attrs = { href: link.getAttribute("href") };
|
|
143
|
+
if (link.hasAttribute("target")) {
|
|
144
|
+
attrs.target = link.getAttribute("target");
|
|
145
|
+
}
|
|
146
|
+
return attrs;
|
|
147
|
+
}
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
];
|
|
131
152
|
},
|
|
132
153
|
|
|
133
154
|
renderHTML({ HTMLAttributes }) {
|
|
134
|
-
|
|
155
|
+
const { href, target, ...imgAttributes } = HTMLAttributes;
|
|
156
|
+
const imageContent = [
|
|
135
157
|
"div",
|
|
136
158
|
{ "class": "editor-content-image", "data-image": "" },
|
|
137
159
|
[
|
|
138
160
|
"img",
|
|
139
|
-
mergeAttributes(this.options.HTMLAttributes,
|
|
161
|
+
mergeAttributes(this.options.HTMLAttributes, imgAttributes)
|
|
140
162
|
]
|
|
141
163
|
];
|
|
164
|
+
|
|
165
|
+
if (href) {
|
|
166
|
+
const anchorAttributes = { href };
|
|
167
|
+
const anchorTarget = target;
|
|
168
|
+
if (anchorTarget) {
|
|
169
|
+
anchorAttributes.target = anchorTarget;
|
|
170
|
+
anchorAttributes.rel = "noopener noreferrer";
|
|
171
|
+
}
|
|
172
|
+
return [
|
|
173
|
+
"a",
|
|
174
|
+
anchorAttributes,
|
|
175
|
+
imageContent
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return imageContent;
|
|
142
180
|
},
|
|
143
181
|
|
|
144
182
|
addProseMirrorPlugins() {
|
|
@@ -62,7 +62,9 @@ export default (self) => {
|
|
|
62
62
|
const img = contentDOM.querySelector("img");
|
|
63
63
|
let activeResizeControl = null,
|
|
64
64
|
currentHeight = null,
|
|
65
|
+
currentHref = node.attrs.href,
|
|
65
66
|
currentSrc = node.attrs.src,
|
|
67
|
+
currentTarget = node.attrs.target,
|
|
66
68
|
currentWidth = null,
|
|
67
69
|
naturalHeight = img.naturalHeight,
|
|
68
70
|
naturalWidth = img.naturalWidth,
|
|
@@ -185,7 +187,13 @@ export default (self) => {
|
|
|
185
187
|
return false;
|
|
186
188
|
}
|
|
187
189
|
|
|
188
|
-
const { alt, src, title, width } = updatedNode.attrs;
|
|
190
|
+
const { alt, src, title, width, href, target } = updatedNode.attrs;
|
|
191
|
+
|
|
192
|
+
// If the href or target changed, we need to recreate the node because the
|
|
193
|
+
// structure changes (wrapped in <a> vs not wrapped)
|
|
194
|
+
if (href !== currentHref || target !== currentTarget) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
189
197
|
|
|
190
198
|
// We set the value through an attribute change here because otherwise
|
|
191
199
|
// we would trigger a mutation in the DOM which causes the update method
|
|
@@ -1,25 +1,53 @@
|
|
|
1
|
-
import { PluginKey } from "prosemirror-state";
|
|
1
|
+
import { NodeSelection, PluginKey } from "prosemirror-state";
|
|
2
2
|
|
|
3
3
|
import { getDictionary } from "src/decidim/refactor/moved/i18n";
|
|
4
4
|
import BubbleMenu from "src/decidim/editor/common/bubble_menu";
|
|
5
5
|
|
|
6
6
|
class LinkBubbleMenu extends BubbleMenu {
|
|
7
|
-
shouldDisplay() {
|
|
8
|
-
|
|
7
|
+
shouldDisplay(view) {
|
|
8
|
+
if (this.editor.isActive("link")) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const selection = view.state.selection;
|
|
13
|
+
return this.isImage(selection) && Boolean(selection.node.attrs.href);
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
display() {
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
display(view) {
|
|
17
|
+
if (this.editor.isActive("link")) {
|
|
18
|
+
const { href } = this.editor.getAttributes("link");
|
|
19
|
+
this.updateHref(href);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const selection = view.state.selection;
|
|
24
|
+
if (this.isImage(selection)) {
|
|
25
|
+
this.bubble.style.zIndex = "10";
|
|
26
|
+
this.updateHref(selection.node.attrs.href);
|
|
27
|
+
}
|
|
14
28
|
}
|
|
15
29
|
|
|
16
30
|
handleAction(action) {
|
|
17
31
|
if (action === "remove") {
|
|
32
|
+
const { selection } = this.editor.state;
|
|
33
|
+
if (this.isImage(selection)) {
|
|
34
|
+
this.editor.chain().focus(null, { scrollIntoView: false }).updateAttributes("image", { href: null, target: null }).run();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
this.editor.chain().focus(null, { scrollIntoView: false }).unsetLink().run();
|
|
19
39
|
} else {
|
|
20
40
|
this.editor.commands.linkDialog();
|
|
21
41
|
}
|
|
22
42
|
}
|
|
43
|
+
|
|
44
|
+
updateHref(href) {
|
|
45
|
+
this.element.querySelector("[data-linkbubble-value]").textContent = href;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
isImage(selection) {
|
|
49
|
+
return selection instanceof NodeSelection && selection.node.type.name === "image";
|
|
50
|
+
}
|
|
23
51
|
}
|
|
24
52
|
|
|
25
53
|
const createElement = () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Link from "@tiptap/extension-link";
|
|
2
|
-
import { Plugin } from "prosemirror-state";
|
|
2
|
+
import { Plugin, NodeSelection } from "prosemirror-state";
|
|
3
3
|
|
|
4
4
|
import { getDictionary } from "src/decidim/refactor/moved/i18n";
|
|
5
5
|
import InputDialog from "src/decidim/editor/common/input_dialog";
|
|
@@ -40,31 +40,55 @@ export default Link.extend({
|
|
|
40
40
|
...this.parent?.(),
|
|
41
41
|
|
|
42
42
|
toggleLinkBubble: () => ({ dispatch }) => {
|
|
43
|
+
const { selection } = this.editor.state;
|
|
44
|
+
const isImageSelection = selection instanceof NodeSelection && selection.node.type.name === "image";
|
|
45
|
+
const imageHasLink = isImageSelection && Boolean(selection.node.attrs.href);
|
|
46
|
+
|
|
43
47
|
if (dispatch) {
|
|
44
|
-
if (this.editor.isActive("link")) {
|
|
45
|
-
this.storage.bubbleMenu.
|
|
48
|
+
if (this.editor.isActive("link") || (isImageSelection && imageHasLink)) {
|
|
49
|
+
this.storage.bubbleMenu.handleSelectionChange(this.editor.view);
|
|
46
50
|
return true;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
this.storage.bubbleMenu.hide();
|
|
50
54
|
return false;
|
|
51
55
|
}
|
|
56
|
+
|
|
57
|
+
if (isImageSelection) {
|
|
58
|
+
return imageHasLink;
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
return this.editor.isActive("link");
|
|
53
62
|
},
|
|
54
63
|
|
|
55
64
|
linkDialog: () => async ({ dispatch, commands }) => {
|
|
56
65
|
if (dispatch) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
const { state } = this.editor;
|
|
67
|
+
const { selection } = state;
|
|
68
|
+
const isImageSelection = selection instanceof NodeSelection && selection.node.type.name === "image";
|
|
69
|
+
const nodeType = (() => {
|
|
70
|
+
if (isImageSelection) {
|
|
71
|
+
return "image";
|
|
72
|
+
}
|
|
73
|
+
return "link";
|
|
74
|
+
})();
|
|
75
|
+
|
|
76
|
+
if (!isImageSelection) {
|
|
77
|
+
// If the cursor is within the link but the link is not selected, the
|
|
78
|
+
// link would not be correctly updated. Also if only a part of the
|
|
79
|
+
// link is selected, the link would be split to separate links, only
|
|
80
|
+
// the current selection getting the updated link URL.
|
|
81
|
+
commands.extendMarkRange("link");
|
|
82
|
+
}
|
|
62
83
|
|
|
63
84
|
this.storage.bubbleMenu.hide();
|
|
64
85
|
|
|
65
86
|
const { allowTargetControl } = this.options;
|
|
66
87
|
|
|
67
|
-
let
|
|
88
|
+
let href = null;
|
|
89
|
+
let target = null;
|
|
90
|
+
|
|
91
|
+
({ href, target } = this.editor.getAttributes(nodeType));
|
|
68
92
|
|
|
69
93
|
const inputs = { href: { type: "text", label: i18n.hrefLabel } };
|
|
70
94
|
if (allowTargetControl) {
|
|
@@ -88,16 +112,25 @@ export default Link.extend({
|
|
|
88
112
|
target = null;
|
|
89
113
|
}
|
|
90
114
|
|
|
115
|
+
const buildChain = () => this.editor.chain().focus(null, { scrollIntoView: false });
|
|
116
|
+
|
|
91
117
|
if (dialogState !== "save") {
|
|
92
|
-
|
|
118
|
+
buildChain().toggleLinkBubble().run();
|
|
93
119
|
return false;
|
|
94
120
|
}
|
|
95
121
|
|
|
96
122
|
if (!href || href.trim().length < 1) {
|
|
97
|
-
|
|
123
|
+
if (isImageSelection) {
|
|
124
|
+
return buildChain().updateAttributes("image", { href: null, target: null }).run();
|
|
125
|
+
}
|
|
126
|
+
return buildChain().unsetLink().run();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isImageSelection) {
|
|
130
|
+
return buildChain().updateAttributes("image", { href: href, target }).toggleLinkBubble().run();
|
|
98
131
|
}
|
|
99
132
|
|
|
100
|
-
return
|
|
133
|
+
return buildChain().setLink({ href: href, target }).toggleLinkBubble().run();
|
|
101
134
|
}
|
|
102
135
|
|
|
103
136
|
return true;
|