@ckeditor/ckeditor5-clipboard 48.2.0 → 48.3.0-alpha.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.
- package/dist/augmentation.d.ts +14 -14
- package/dist/clipboard.d.ts +35 -35
- package/dist/clipboardmarkersutils.d.ts +194 -194
- package/dist/clipboardobserver.d.ts +274 -274
- package/dist/clipboardpipeline.d.ts +252 -252
- package/dist/dragdrop.d.ts +101 -101
- package/dist/dragdropblocktoolbar.d.ts +46 -46
- package/dist/dragdroptarget.d.ts +95 -94
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +21 -21
- package/dist/index.js +1706 -2182
- package/dist/index.js.map +1 -1
- package/dist/lineview.d.ts +42 -42
- package/dist/pasteplaintext.d.ts +27 -27
- package/dist/utils/normalizeclipboarddata.d.ts +12 -12
- package/dist/utils/plaintexttohtml.d.ts +10 -10
- package/dist/utils/viewtoplaintext.d.ts +12 -12
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -2,2238 +2,1762 @@
|
|
|
2
2
|
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
4
|
*/
|
|
5
|
-
import { Plugin } from
|
|
6
|
-
import { EventInfo,
|
|
7
|
-
import { DomEventObserver,
|
|
8
|
-
import { mapValues, throttle } from
|
|
9
|
-
import { Widget, isWidget } from
|
|
10
|
-
import { View } from
|
|
5
|
+
import { Plugin } from "@ckeditor/ckeditor5-core";
|
|
6
|
+
import { DomEmitterMixin, EventInfo, Rect, ResizeObserver, createElement, delay, env, getRangeFromMouseEvent, global, toUnit, uid } from "@ckeditor/ckeditor5-utils";
|
|
7
|
+
import { DomEventObserver, ModelLiveRange, ModelRange, PointerObserver, ViewDataTransfer } from "@ckeditor/ckeditor5-engine";
|
|
8
|
+
import { mapValues, throttle } from "es-toolkit/compat";
|
|
9
|
+
import { Widget, isWidget } from "@ckeditor/ckeditor5-widget";
|
|
10
|
+
import { View } from "@ckeditor/ckeditor5-ui";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.replace(/^\s/, ' ').replace(/\s$/, ' ')// Preserve other subsequent spaces now.
|
|
30
|
-
.replace(/\s\s/g, ' ');
|
|
31
|
-
if (text.includes('</p><p>') || text.includes('<br>')) {
|
|
32
|
-
// If we created paragraphs above, add the trailing ones.
|
|
33
|
-
text = `<p>${text}</p>`;
|
|
34
|
-
}
|
|
35
|
-
// TODO:
|
|
36
|
-
// * What about '\nfoo' vs ' foo'?
|
|
37
|
-
return text;
|
|
13
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
14
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @module clipboard/utils/plaintexttohtml
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Converts plain text to its HTML-ized version.
|
|
21
|
+
*
|
|
22
|
+
* @param text The plain text to convert.
|
|
23
|
+
* @returns HTML generated from the plain text.
|
|
24
|
+
*/
|
|
25
|
+
function plainTextToHtml(text) {
|
|
26
|
+
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r?\n\r?\n/g, "</p><p>").replace(/\r?\n/g, "<br>").replace(/\t/g, " ").replace(/^\s/, " ").replace(/\s$/, " ").replace(/\s\s/g, " ");
|
|
27
|
+
if (text.includes("</p><p>") || text.includes("<br>")) text = `<p>${text}</p>`;
|
|
28
|
+
return text;
|
|
38
29
|
}
|
|
39
30
|
|
|
40
31
|
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* {@link module:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const nativeDataTransfer = 'clipboardData' in domEvent ? domEvent.clipboardData : domEvent.dataTransfer;
|
|
118
|
-
const cacheFiles = domEvent.type == 'drop' || domEvent.type == 'paste';
|
|
119
|
-
const evtData = {
|
|
120
|
-
dataTransfer: new ViewDataTransfer(nativeDataTransfer, {
|
|
121
|
-
cacheFiles
|
|
122
|
-
})
|
|
123
|
-
};
|
|
124
|
-
if (domEvent.type == 'drop' || domEvent.type == 'dragover') {
|
|
125
|
-
const domRange = getRangeFromMouseEvent(domEvent);
|
|
126
|
-
evtData.dropRange = domRange && this.view.domConverter.domRangeToView(domRange);
|
|
127
|
-
}
|
|
128
|
-
this.fire(domEvent.type, domEvent, evtData);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
32
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
33
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* @module clipboard/clipboardobserver
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Clipboard events observer.
|
|
40
|
+
*
|
|
41
|
+
* Fires the following events:
|
|
42
|
+
*
|
|
43
|
+
* * {@link module:engine/view/document~ViewDocument#event:clipboardInput},
|
|
44
|
+
* * {@link module:engine/view/document~ViewDocument#event:paste},
|
|
45
|
+
* * {@link module:engine/view/document~ViewDocument#event:copy},
|
|
46
|
+
* * {@link module:engine/view/document~ViewDocument#event:cut},
|
|
47
|
+
* * {@link module:engine/view/document~ViewDocument#event:drop},
|
|
48
|
+
* * {@link module:engine/view/document~ViewDocument#event:dragover},
|
|
49
|
+
* * {@link module:engine/view/document~ViewDocument#event:dragging},
|
|
50
|
+
* * {@link module:engine/view/document~ViewDocument#event:dragstart},
|
|
51
|
+
* * {@link module:engine/view/document~ViewDocument#event:dragend},
|
|
52
|
+
* * {@link module:engine/view/document~ViewDocument#event:dragenter},
|
|
53
|
+
* * {@link module:engine/view/document~ViewDocument#event:dragleave}.
|
|
54
|
+
*
|
|
55
|
+
* **Note**: This observer is not available by default (ckeditor5-engine does not add it on its own).
|
|
56
|
+
* To make it available, it needs to be added to {@link module:engine/view/document~ViewDocument} by using
|
|
57
|
+
* the {@link module:engine/view/view~EditingView#addObserver `View#addObserver()`} method. Alternatively, you can load the
|
|
58
|
+
* {@link module:clipboard/clipboard~Clipboard} plugin which adds this observer automatically (because it uses it).
|
|
59
|
+
*/
|
|
60
|
+
var ClipboardObserver = class extends DomEventObserver {
|
|
61
|
+
domEventType = [
|
|
62
|
+
"paste",
|
|
63
|
+
"copy",
|
|
64
|
+
"cut",
|
|
65
|
+
"drop",
|
|
66
|
+
"dragover",
|
|
67
|
+
"dragstart",
|
|
68
|
+
"dragend",
|
|
69
|
+
"dragenter",
|
|
70
|
+
"dragleave"
|
|
71
|
+
];
|
|
72
|
+
constructor(view) {
|
|
73
|
+
super(view);
|
|
74
|
+
const viewDocument = this.document;
|
|
75
|
+
this.listenTo(viewDocument, "paste", handleInput("clipboardInput"), { priority: "low" });
|
|
76
|
+
this.listenTo(viewDocument, "drop", handleInput("clipboardInput"), { priority: "low" });
|
|
77
|
+
this.listenTo(viewDocument, "dragover", handleInput("dragging"), { priority: "low" });
|
|
78
|
+
function handleInput(type) {
|
|
79
|
+
return (evt, data) => {
|
|
80
|
+
data.preventDefault();
|
|
81
|
+
const targetRanges = data.dropRange ? [data.dropRange] : null;
|
|
82
|
+
const dataTransfer = data.dataTransfer;
|
|
83
|
+
const eventInfo = new EventInfo(viewDocument, type);
|
|
84
|
+
let content = "";
|
|
85
|
+
if (dataTransfer.getData("text/html")) content = dataTransfer.getData("text/html");
|
|
86
|
+
else if (dataTransfer.getData("text/plain")) content = plainTextToHtml(dataTransfer.getData("text/plain"));
|
|
87
|
+
viewDocument.fire(eventInfo, {
|
|
88
|
+
dataTransfer,
|
|
89
|
+
content,
|
|
90
|
+
method: evt.name,
|
|
91
|
+
targetRanges,
|
|
92
|
+
target: data.target,
|
|
93
|
+
domEvent: data.domEvent
|
|
94
|
+
});
|
|
95
|
+
if (eventInfo.stop.called) data.stopPropagation();
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
onDomEvent(domEvent) {
|
|
100
|
+
const evtData = { dataTransfer: new ViewDataTransfer("clipboardData" in domEvent ? domEvent.clipboardData : domEvent.dataTransfer, { cacheFiles: domEvent.type == "drop" || domEvent.type == "paste" }) };
|
|
101
|
+
if (domEvent.type == "drop" || domEvent.type == "dragover") {
|
|
102
|
+
const domRange = getRangeFromMouseEvent(domEvent);
|
|
103
|
+
evtData.dropRange = domRange && this.view.domConverter.domRangeToView(domRange);
|
|
104
|
+
}
|
|
105
|
+
this.fire(domEvent.type, domEvent, evtData);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
131
108
|
|
|
132
109
|
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
})// Remove all HTML comments.
|
|
153
|
-
.replace(/<!--[\s\S]*?-->/g, '');
|
|
110
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
111
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
112
|
+
*/
|
|
113
|
+
/**
|
|
114
|
+
* @module clipboard/utils/normalizeclipboarddata
|
|
115
|
+
*/
|
|
116
|
+
/**
|
|
117
|
+
* Removes some popular browser quirks out of the clipboard data (HTML).
|
|
118
|
+
* Removes all HTML comments. These are considered an internal thing and it makes little sense if they leak into the editor data.
|
|
119
|
+
*
|
|
120
|
+
* @param data The HTML data to normalize.
|
|
121
|
+
* @returns Normalized HTML.
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
function normalizeClipboardData(data) {
|
|
125
|
+
return data.replace(/<span(?: class="Apple-converted-space"|)>(\s+)<\/span>/g, (fullMatch, spaces) => {
|
|
126
|
+
if (spaces.length == 1) return " ";
|
|
127
|
+
return spaces;
|
|
128
|
+
}).replace(/<!--[\s\S]*?-->/g, "");
|
|
154
129
|
}
|
|
155
130
|
|
|
156
131
|
/**
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// Most `view.ContainerElement` want to be separate by new-line, but some are creating one structure
|
|
163
|
-
// together (like `<li>`) so it is better to separate them by only one "\n".
|
|
164
|
-
const smallPaddingElements = [
|
|
165
|
-
'figcaption',
|
|
166
|
-
'li'
|
|
167
|
-
];
|
|
168
|
-
const listElements = [
|
|
169
|
-
'ol',
|
|
170
|
-
'ul'
|
|
171
|
-
];
|
|
132
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
133
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
134
|
+
*/
|
|
135
|
+
const smallPaddingElements = ["figcaption", "li"];
|
|
136
|
+
const listElements = ["ol", "ul"];
|
|
172
137
|
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const doc = document.implementation.createHTMLDocument('');
|
|
200
|
-
const tempElement = doc.createElement('div');
|
|
201
|
-
viewItem.render(tempElement, converter);
|
|
202
|
-
text += domElementToPlainText(tempElement);
|
|
203
|
-
}
|
|
204
|
-
return text;
|
|
138
|
+
* Converts {@link module:engine/view/item~ViewItem view item} and all of its children to plain text.
|
|
139
|
+
*
|
|
140
|
+
* @param converter The converter instance.
|
|
141
|
+
* @param viewItem View item to convert.
|
|
142
|
+
* @returns Plain text representation of `viewItem`.
|
|
143
|
+
*/
|
|
144
|
+
function viewToPlainText(converter, viewItem) {
|
|
145
|
+
if (viewItem.is("$text") || viewItem.is("$textProxy")) return viewItem.data;
|
|
146
|
+
if (viewItem.is("element", "img") && viewItem.hasAttribute("alt")) return viewItem.getAttribute("alt");
|
|
147
|
+
if (viewItem.is("element", "br")) return "\n";
|
|
148
|
+
/**
|
|
149
|
+
* Item is a document fragment, attribute element or container element. It doesn't
|
|
150
|
+
* have it's own text value, so we need to convert its children elements.
|
|
151
|
+
*/
|
|
152
|
+
let text = "";
|
|
153
|
+
let prev = null;
|
|
154
|
+
for (const child of viewItem.getChildren()) {
|
|
155
|
+
text += newLinePadding(child, prev) + viewToPlainText(converter, child);
|
|
156
|
+
prev = child;
|
|
157
|
+
}
|
|
158
|
+
if (viewItem.is("rawElement")) {
|
|
159
|
+
const tempElement = document.implementation.createHTMLDocument("").createElement("div");
|
|
160
|
+
viewItem.render(tempElement, converter);
|
|
161
|
+
text += domElementToPlainText(tempElement);
|
|
162
|
+
}
|
|
163
|
+
return text;
|
|
205
164
|
}
|
|
206
165
|
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
for (const child of element.childNodes){
|
|
216
|
-
text += domElementToPlainText(child);
|
|
217
|
-
}
|
|
218
|
-
return text;
|
|
166
|
+
* Recursively converts DOM element and all of its children to plain text.
|
|
167
|
+
*/
|
|
168
|
+
function domElementToPlainText(element) {
|
|
169
|
+
let text = "";
|
|
170
|
+
if (element.nodeType === Node.TEXT_NODE) return element.textContent;
|
|
171
|
+
else if (element.tagName === "BR") return "\n";
|
|
172
|
+
for (const child of element.childNodes) text += domElementToPlainText(child);
|
|
173
|
+
return text;
|
|
219
174
|
}
|
|
220
175
|
/**
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
if (!element.is('containerElement') && !previous.is('containerElement')) {
|
|
238
|
-
// Don't add padding between non-container elements.
|
|
239
|
-
return '';
|
|
240
|
-
}
|
|
241
|
-
if (smallPaddingElements.includes(element.name) || smallPaddingElements.includes(previous.name)) {
|
|
242
|
-
// Add small padding between selected container elements.
|
|
243
|
-
return '\n';
|
|
244
|
-
}
|
|
245
|
-
// Do not add padding around the elements that won't be rendered.
|
|
246
|
-
if (element.is('element') && element.getCustomProperty('dataPipeline:transparentRendering') || previous.is('element') && previous.getCustomProperty('dataPipeline:transparentRendering')) {
|
|
247
|
-
return '';
|
|
248
|
-
}
|
|
249
|
-
// Add empty lines between container elements.
|
|
250
|
-
return '\n\n';
|
|
176
|
+
* Returns new line padding to prefix the given elements with.
|
|
177
|
+
*/
|
|
178
|
+
function newLinePadding(element, previous) {
|
|
179
|
+
if (!previous) return "";
|
|
180
|
+
if (element.is("element", "li") && !element.isEmpty && element.getChild(0).is("containerElement")) return "\n\n";
|
|
181
|
+
if (listElements.includes(element.name) && listElements.includes(previous.name))
|
|
182
|
+
/**
|
|
183
|
+
* Because `<ul>` and `<ol>` are ViewAttributeElements, two consecutive lists will not have any padding between
|
|
184
|
+
* them (see the `if` statement below). To fix this, we need to make an exception for this case.
|
|
185
|
+
*/
|
|
186
|
+
return "\n\n";
|
|
187
|
+
if (!element.is("containerElement") && !previous.is("containerElement")) return "";
|
|
188
|
+
if (smallPaddingElements.includes(element.name) || smallPaddingElements.includes(previous.name)) return "\n";
|
|
189
|
+
if (element.is("element") && element.getCustomProperty("dataPipeline:transparentRendering") || previous.is("element") && previous.getCustomProperty("dataPipeline:transparentRendering")) return "";
|
|
190
|
+
return "\n\n";
|
|
251
191
|
}
|
|
252
192
|
|
|
253
193
|
/**
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
acc[fakeMarker.name] = {
|
|
620
|
-
...prevFakeMarker,
|
|
621
|
-
[fakeMarker.type]: position
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
if (fakeMarker.markerElement) {
|
|
625
|
-
writer.remove(fakeMarker.markerElement);
|
|
626
|
-
}
|
|
627
|
-
return acc;
|
|
628
|
-
}, {});
|
|
629
|
-
// We cannot construct ranges directly in previous reduce because element ranges can overlap.
|
|
630
|
-
// In other words lets assume we have such scenario:
|
|
631
|
-
// <fake-marker-start /> <paragraph /> <fake-marker-2-start /> <fake-marker-end /> <fake-marker-2-end />
|
|
632
|
-
//
|
|
633
|
-
// We have to remove `fake-marker-start` firstly and then remove `fake-marker-2-start`.
|
|
634
|
-
// Removal of `fake-marker-2-start` affects `fake-marker-end` position so we cannot create
|
|
635
|
-
// connection between `fake-marker-start` and `fake-marker-end` without iterating whole set firstly.
|
|
636
|
-
return mapValues(fakeMarkersRanges, (range)=>new ModelRange(range.start || writer.createPositionFromPath(rootElement, [
|
|
637
|
-
0
|
|
638
|
-
]), range.end || writer.createPositionAt(rootElement, 'end')));
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* Returns array that contains list of fake markers with corresponding `$marker` elements.
|
|
642
|
-
*
|
|
643
|
-
* For each marker, there can be two `$marker` elements or only one (if the document fragment contained
|
|
644
|
-
* only the beginning or only the end of a marker).
|
|
645
|
-
*
|
|
646
|
-
* @param writer An instance of the model writer.
|
|
647
|
-
* @param rootElement The element to be checked.
|
|
648
|
-
*/ _getAllFakeMarkersFromElement(writer, rootElement) {
|
|
649
|
-
const foundFakeMarkers = Array.from(writer.createRangeIn(rootElement)).flatMap(({ item })=>{
|
|
650
|
-
if (!item.is('element', '$marker')) {
|
|
651
|
-
return [];
|
|
652
|
-
}
|
|
653
|
-
const name = item.getAttribute('data-name');
|
|
654
|
-
const type = item.getAttribute('data-type');
|
|
655
|
-
return [
|
|
656
|
-
{
|
|
657
|
-
markerElement: item,
|
|
658
|
-
name,
|
|
659
|
-
type
|
|
660
|
-
}
|
|
661
|
-
];
|
|
662
|
-
});
|
|
663
|
-
const prependFakeMarkers = [];
|
|
664
|
-
const appendFakeMarkers = [];
|
|
665
|
-
for (const fakeMarker of foundFakeMarkers){
|
|
666
|
-
if (fakeMarker.type === 'end') {
|
|
667
|
-
// <fake-marker> [ phrase</fake-marker> phrase ]
|
|
668
|
-
// ^
|
|
669
|
-
// Handle case when marker is just before start of selection.
|
|
670
|
-
// Only end marker is inside selection.
|
|
671
|
-
const hasMatchingStartMarker = foundFakeMarkers.some((otherFakeMarker)=>otherFakeMarker.name === fakeMarker.name && otherFakeMarker.type === 'start');
|
|
672
|
-
if (!hasMatchingStartMarker) {
|
|
673
|
-
prependFakeMarkers.push({
|
|
674
|
-
markerElement: null,
|
|
675
|
-
name: fakeMarker.name,
|
|
676
|
-
type: 'start'
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (fakeMarker.type === 'start') {
|
|
681
|
-
// [<fake-marker>phrase]</fake-marker>
|
|
682
|
-
// ^
|
|
683
|
-
// Handle case when fake marker is after selection.
|
|
684
|
-
// Only start marker is inside selection.
|
|
685
|
-
const hasMatchingEndMarker = foundFakeMarkers.some((otherFakeMarker)=>otherFakeMarker.name === fakeMarker.name && otherFakeMarker.type === 'end');
|
|
686
|
-
if (!hasMatchingEndMarker) {
|
|
687
|
-
appendFakeMarkers.unshift({
|
|
688
|
-
markerElement: null,
|
|
689
|
-
name: fakeMarker.name,
|
|
690
|
-
type: 'end'
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
return [
|
|
696
|
-
...prependFakeMarkers,
|
|
697
|
-
...foundFakeMarkers,
|
|
698
|
-
...appendFakeMarkers
|
|
699
|
-
];
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* When copy of markers occurs we have to make sure that pasted markers have different names
|
|
703
|
-
* than source markers. This functions helps with assigning unique part to marker name to
|
|
704
|
-
* prevent duplicated markers error.
|
|
705
|
-
*
|
|
706
|
-
* @param name Name of marker
|
|
707
|
-
*/ _getUniqueMarkerName(name) {
|
|
708
|
-
const parts = name.split(':');
|
|
709
|
-
const newId = uid().substring(1, 6);
|
|
710
|
-
// It looks like the marker already is UID marker so in this scenario just swap
|
|
711
|
-
// last part of marker name and assign new UID.
|
|
712
|
-
//
|
|
713
|
-
// example: comment:{ threadId }:{ id } => comment:{ threadId }:{ newId }
|
|
714
|
-
if (parts.length === 3) {
|
|
715
|
-
return `${parts.slice(0, 2).join(':')}:${newId}`;
|
|
716
|
-
}
|
|
717
|
-
// Assign new segment to marker name with id.
|
|
718
|
-
//
|
|
719
|
-
// example: comment => comment:{ newId }
|
|
720
|
-
return `${parts.join(':')}:${newId}`;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
194
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
195
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
196
|
+
*/
|
|
197
|
+
/**
|
|
198
|
+
* @module clipboard/clipboardmarkersutils
|
|
199
|
+
*/
|
|
200
|
+
/**
|
|
201
|
+
* Part of the clipboard logic. Responsible for collecting markers from selected fragments
|
|
202
|
+
* and restoring them with proper positions in pasted elements.
|
|
203
|
+
*
|
|
204
|
+
* @internal
|
|
205
|
+
*/
|
|
206
|
+
var ClipboardMarkersUtils = class extends Plugin {
|
|
207
|
+
/**
|
|
208
|
+
* Map of marker names that can be copied.
|
|
209
|
+
*
|
|
210
|
+
* @internal
|
|
211
|
+
*/
|
|
212
|
+
_markersToCopy = /* @__PURE__ */ new Map();
|
|
213
|
+
/**
|
|
214
|
+
* @inheritDoc
|
|
215
|
+
*/
|
|
216
|
+
static get pluginName() {
|
|
217
|
+
return "ClipboardMarkersUtils";
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* @inheritDoc
|
|
221
|
+
*/
|
|
222
|
+
static get isOfficialPlugin() {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Registers marker name as copyable in clipboard pipeline.
|
|
227
|
+
*
|
|
228
|
+
* @param markerName Name of marker that can be copied.
|
|
229
|
+
* @param config Configuration that describes what can be performed on specified marker.
|
|
230
|
+
* @internal
|
|
231
|
+
*/
|
|
232
|
+
_registerMarkerToCopy(markerName, config) {
|
|
233
|
+
this._markersToCopy.set(markerName, config);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Performs copy markers on provided selection and paste it to fragment returned from `getCopiedFragment`.
|
|
237
|
+
*
|
|
238
|
+
* 1. Picks all markers in provided selection.
|
|
239
|
+
* 2. Inserts fake markers to document.
|
|
240
|
+
* 3. Gets copied selection fragment from document.
|
|
241
|
+
* 4. Removes fake elements from fragment and document.
|
|
242
|
+
* 5. Inserts markers in the place of removed fake markers.
|
|
243
|
+
*
|
|
244
|
+
* Due to selection modification, when inserting items, `getCopiedFragment` must *always* operate on `writer.model.document.selection'.
|
|
245
|
+
* Do not use any other custom selection object within callback, as this will lead to out-of-bounds exceptions in rare scenarios.
|
|
246
|
+
*
|
|
247
|
+
* @param action Type of clipboard action.
|
|
248
|
+
* @param selection Selection to be checked.
|
|
249
|
+
* @param getCopiedFragment Callback that performs copy of selection and returns it as fragment.
|
|
250
|
+
* @internal
|
|
251
|
+
*/
|
|
252
|
+
_copySelectedFragmentWithMarkers(action, selection, getCopiedFragment = (writer) => writer.model.getSelectedContent(writer.model.document.selection)) {
|
|
253
|
+
return this.editor.model.change((writer) => {
|
|
254
|
+
const oldSelection = writer.model.document.selection;
|
|
255
|
+
writer.setSelection(selection);
|
|
256
|
+
const sourceSelectionInsertedMarkers = this._insertFakeMarkersIntoSelection(writer, writer.model.document.selection, action);
|
|
257
|
+
const fragment = getCopiedFragment(writer);
|
|
258
|
+
const fakeMarkersRangesInsideRange = this._removeFakeMarkersInsideElement(writer, fragment);
|
|
259
|
+
for (const [markerName, elements] of Object.entries(sourceSelectionInsertedMarkers)) {
|
|
260
|
+
fakeMarkersRangesInsideRange[markerName] ||= writer.createRangeIn(fragment);
|
|
261
|
+
for (const element of elements) writer.remove(element);
|
|
262
|
+
}
|
|
263
|
+
fragment.markers.clear();
|
|
264
|
+
for (const [markerName, range] of Object.entries(fakeMarkersRangesInsideRange)) fragment.markers.set(markerName, range);
|
|
265
|
+
writer.setSelection(oldSelection);
|
|
266
|
+
return fragment;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Performs paste of markers on already pasted element.
|
|
271
|
+
*
|
|
272
|
+
* 1. Inserts fake markers that are present in fragment element (such fragment will be processed in `getPastedDocumentElement`).
|
|
273
|
+
* 2. Calls `getPastedDocumentElement` and gets element that is inserted into root model.
|
|
274
|
+
* 3. Removes all fake markers present in transformed element.
|
|
275
|
+
* 4. Inserts new markers with removed fake markers ranges into pasted fragment.
|
|
276
|
+
*
|
|
277
|
+
* There are multiple edge cases that have to be considered before calling this function:
|
|
278
|
+
*
|
|
279
|
+
* * `markers` are inserted into the same element that must be later transformed inside `getPastedDocumentElement`.
|
|
280
|
+
* * Fake marker elements inside `getPastedDocumentElement` can be cloned, but their ranges cannot overlap.
|
|
281
|
+
* * If `duplicateOnPaste` is `true` in marker config then associated marker ID is regenerated before pasting.
|
|
282
|
+
*
|
|
283
|
+
* @param markers Object that maps marker name to corresponding range.
|
|
284
|
+
* @param getPastedDocumentElement Getter used to get target markers element.
|
|
285
|
+
* @internal
|
|
286
|
+
*/
|
|
287
|
+
_pasteMarkersIntoTransformedElement(markers, getPastedDocumentElement) {
|
|
288
|
+
const pasteMarkers = this._getPasteMarkersFromRangeMap(markers);
|
|
289
|
+
return this.editor.model.change((writer) => {
|
|
290
|
+
const sourceFragmentFakeMarkers = this._insertFakeMarkersElements(writer, pasteMarkers);
|
|
291
|
+
const transformedElement = getPastedDocumentElement(writer);
|
|
292
|
+
const removedFakeMarkers = this._removeFakeMarkersInsideElement(writer, transformedElement);
|
|
293
|
+
for (const element of Object.values(sourceFragmentFakeMarkers).flat()) writer.remove(element);
|
|
294
|
+
for (const [markerName, range] of Object.entries(removedFakeMarkers)) if (!writer.model.markers.has(markerName)) writer.addMarker(markerName, {
|
|
295
|
+
usingOperation: true,
|
|
296
|
+
affectsData: true,
|
|
297
|
+
range
|
|
298
|
+
});
|
|
299
|
+
return transformedElement;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Pastes document fragment with markers to document.
|
|
304
|
+
* If `duplicateOnPaste` is `true` in marker config then associated markers IDs
|
|
305
|
+
* are regenerated before pasting to avoid markers duplications in content.
|
|
306
|
+
*
|
|
307
|
+
* @param fragment Document fragment that should contain already processed by pipeline markers.
|
|
308
|
+
* @internal
|
|
309
|
+
*/
|
|
310
|
+
_pasteFragmentWithMarkers(fragment) {
|
|
311
|
+
const pasteMarkers = this._getPasteMarkersFromRangeMap(fragment.markers);
|
|
312
|
+
fragment.markers.clear();
|
|
313
|
+
for (const copyableMarker of pasteMarkers) fragment.markers.set(copyableMarker.name, copyableMarker.range);
|
|
314
|
+
return this.editor.model.insertContent(fragment);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* In some situations we have to perform copy on selected fragment with certain markers. This function allows to temporarily bypass
|
|
318
|
+
* restrictions on markers that we want to copy.
|
|
319
|
+
*
|
|
320
|
+
* This function executes `executor()` callback. For the duration of the callback, if the clipboard pipeline is used to copy
|
|
321
|
+
* content, markers with the specified name will be copied to the clipboard as well.
|
|
322
|
+
*
|
|
323
|
+
* @param markerName Which markers should be copied.
|
|
324
|
+
* @param executor Callback executed.
|
|
325
|
+
* @param config Optional configuration flags used to copy (such like partial copy flag).
|
|
326
|
+
* @internal
|
|
327
|
+
*/
|
|
328
|
+
_forceMarkersCopy(markerName, executor, config = {
|
|
329
|
+
allowedActions: "all",
|
|
330
|
+
copyPartiallySelected: true,
|
|
331
|
+
duplicateOnPaste: true
|
|
332
|
+
}) {
|
|
333
|
+
const before = this._markersToCopy.get(markerName);
|
|
334
|
+
this._markersToCopy.set(markerName, config);
|
|
335
|
+
executor();
|
|
336
|
+
if (before) this._markersToCopy.set(markerName, before);
|
|
337
|
+
else this._markersToCopy.delete(markerName);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Checks if marker can be copied.
|
|
341
|
+
*
|
|
342
|
+
* @param markerName Name of checked marker.
|
|
343
|
+
* @param action Type of clipboard action. If null then checks only if marker is registered as copyable.
|
|
344
|
+
* @internal
|
|
345
|
+
*/
|
|
346
|
+
_isMarkerCopyable(markerName, action) {
|
|
347
|
+
const config = this._getMarkerClipboardConfig(markerName);
|
|
348
|
+
if (!config) return false;
|
|
349
|
+
if (!action) return true;
|
|
350
|
+
const { allowedActions } = config;
|
|
351
|
+
return allowedActions === "all" || allowedActions.includes(action);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Checks if marker has any clipboard copy behavior configuration.
|
|
355
|
+
*
|
|
356
|
+
* @param markerName Name of checked marker.
|
|
357
|
+
*/
|
|
358
|
+
_hasMarkerConfiguration(markerName) {
|
|
359
|
+
return !!this._getMarkerClipboardConfig(markerName);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Returns marker's configuration flags passed during registration.
|
|
363
|
+
*
|
|
364
|
+
* @param markerName Name of marker that should be returned.
|
|
365
|
+
* @internal
|
|
366
|
+
*/
|
|
367
|
+
_getMarkerClipboardConfig(markerName) {
|
|
368
|
+
const [markerNamePrefix] = markerName.split(":");
|
|
369
|
+
return this._markersToCopy.get(markerNamePrefix) || null;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* First step of copying markers. It looks for markers intersecting with given selection and inserts `$marker` elements
|
|
373
|
+
* at positions where document markers start or end. This way `$marker` elements can be easily copied together with
|
|
374
|
+
* the rest of the content of the selection.
|
|
375
|
+
*
|
|
376
|
+
* @param writer An instance of the model writer.
|
|
377
|
+
* @param selection Selection to be checked.
|
|
378
|
+
* @param action Type of clipboard action.
|
|
379
|
+
*/
|
|
380
|
+
_insertFakeMarkersIntoSelection(writer, selection, action) {
|
|
381
|
+
const copyableMarkers = this._getCopyableMarkersFromSelection(writer, selection, action);
|
|
382
|
+
return this._insertFakeMarkersElements(writer, copyableMarkers);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Returns array of markers that can be copied in specified selection.
|
|
386
|
+
*
|
|
387
|
+
* If marker cannot be copied partially (according to `copyPartiallySelected` configuration flag) and
|
|
388
|
+
* is not present entirely in any selection range then it will be skipped.
|
|
389
|
+
*
|
|
390
|
+
* @param writer An instance of the model writer.
|
|
391
|
+
* @param selection Selection which will be checked.
|
|
392
|
+
* @param action Type of clipboard action. If null then checks only if marker is registered as copyable.
|
|
393
|
+
*/
|
|
394
|
+
_getCopyableMarkersFromSelection(writer, selection, action) {
|
|
395
|
+
const selectionRanges = Array.from(selection.getRanges());
|
|
396
|
+
const markersInRanges = new Set(selectionRanges.flatMap((selectionRange) => Array.from(writer.model.markers.getMarkersIntersectingRange(selectionRange))));
|
|
397
|
+
const isSelectionMarkerCopyable = (marker) => {
|
|
398
|
+
if (!this._isMarkerCopyable(marker.name, action)) return false;
|
|
399
|
+
const { copyPartiallySelected } = this._getMarkerClipboardConfig(marker.name);
|
|
400
|
+
if (!copyPartiallySelected) {
|
|
401
|
+
const markerRange = marker.getRange();
|
|
402
|
+
return selectionRanges.some((selectionRange) => selectionRange.containsRange(markerRange, true));
|
|
403
|
+
}
|
|
404
|
+
return true;
|
|
405
|
+
};
|
|
406
|
+
return Array.from(markersInRanges).filter(isSelectionMarkerCopyable).map((copyableMarker) => {
|
|
407
|
+
return {
|
|
408
|
+
name: action === "dragstart" ? this._getUniqueMarkerName(copyableMarker.name) : copyableMarker.name,
|
|
409
|
+
range: copyableMarker.getRange()
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Picks all markers from markers map that can be pasted.
|
|
415
|
+
* If `duplicateOnPaste` is `true`, it regenerates their IDs to ensure uniqueness.
|
|
416
|
+
* If marker is not registered, it will be kept in the array anyway.
|
|
417
|
+
*
|
|
418
|
+
* @param markers Object that maps marker name to corresponding range.
|
|
419
|
+
* @param action Type of clipboard action. If null then checks only if marker is registered as copyable.
|
|
420
|
+
*/
|
|
421
|
+
_getPasteMarkersFromRangeMap(markers, action = null) {
|
|
422
|
+
const { model } = this.editor;
|
|
423
|
+
return (markers instanceof Map ? Array.from(markers.entries()) : Object.entries(markers)).flatMap(([markerName, range]) => {
|
|
424
|
+
if (!this._hasMarkerConfiguration(markerName)) return [{
|
|
425
|
+
name: markerName,
|
|
426
|
+
range
|
|
427
|
+
}];
|
|
428
|
+
if (this._isMarkerCopyable(markerName, action)) {
|
|
429
|
+
const copyMarkerConfig = this._getMarkerClipboardConfig(markerName);
|
|
430
|
+
const isInGraveyard = model.markers.has(markerName) && model.markers.get(markerName).getRange().root.rootName === "$graveyard";
|
|
431
|
+
if (copyMarkerConfig.duplicateOnPaste || isInGraveyard) markerName = this._getUniqueMarkerName(markerName);
|
|
432
|
+
return [{
|
|
433
|
+
name: markerName,
|
|
434
|
+
range
|
|
435
|
+
}];
|
|
436
|
+
}
|
|
437
|
+
return [];
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Inserts specified array of fake markers elements to document and assigns them `type` and `name` attributes.
|
|
442
|
+
* Fake markers elements are used to calculate position of markers on pasted fragment that were transformed during
|
|
443
|
+
* steps between copy and paste.
|
|
444
|
+
*
|
|
445
|
+
* @param writer An instance of the model writer.
|
|
446
|
+
* @param markers Array of markers that will be inserted.
|
|
447
|
+
*/
|
|
448
|
+
_insertFakeMarkersElements(writer, markers) {
|
|
449
|
+
const mappedMarkers = {};
|
|
450
|
+
const sortedMarkers = markers.flatMap((marker) => {
|
|
451
|
+
const { start, end } = marker.range;
|
|
452
|
+
return [{
|
|
453
|
+
position: start,
|
|
454
|
+
marker,
|
|
455
|
+
type: "start"
|
|
456
|
+
}, {
|
|
457
|
+
position: end,
|
|
458
|
+
marker,
|
|
459
|
+
type: "end"
|
|
460
|
+
}];
|
|
461
|
+
}).sort(({ position: posA }, { position: posB }) => posA.isBefore(posB) ? 1 : -1);
|
|
462
|
+
for (const { position, marker, type } of sortedMarkers) {
|
|
463
|
+
const fakeMarker = writer.createElement("$marker", {
|
|
464
|
+
"data-name": marker.name,
|
|
465
|
+
"data-type": type
|
|
466
|
+
});
|
|
467
|
+
if (!mappedMarkers[marker.name]) mappedMarkers[marker.name] = [];
|
|
468
|
+
mappedMarkers[marker.name].push(fakeMarker);
|
|
469
|
+
writer.insert(fakeMarker, position);
|
|
470
|
+
}
|
|
471
|
+
return mappedMarkers;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Removes all `$marker` elements from the given document fragment.
|
|
475
|
+
*
|
|
476
|
+
* Returns an object where keys are marker names, and values are ranges corresponding to positions
|
|
477
|
+
* where `$marker` elements were inserted.
|
|
478
|
+
*
|
|
479
|
+
* If the document fragment had only one `$marker` element for given marker (start or end) the other boundary is set automatically
|
|
480
|
+
* (to the end or start of the document fragment, respectively).
|
|
481
|
+
*
|
|
482
|
+
* @param writer An instance of the model writer.
|
|
483
|
+
* @param rootElement The element to be checked.
|
|
484
|
+
*/
|
|
485
|
+
_removeFakeMarkersInsideElement(writer, rootElement) {
|
|
486
|
+
return mapValues(this._getAllFakeMarkersFromElement(writer, rootElement).reduce((acc, fakeMarker) => {
|
|
487
|
+
const position = fakeMarker.markerElement && writer.createPositionBefore(fakeMarker.markerElement);
|
|
488
|
+
let prevFakeMarker = acc[fakeMarker.name];
|
|
489
|
+
let skipAssign = false;
|
|
490
|
+
if (prevFakeMarker?.start && prevFakeMarker?.end) {
|
|
491
|
+
if (this._getMarkerClipboardConfig(fakeMarker.name).duplicateOnPaste) acc[this._getUniqueMarkerName(fakeMarker.name)] = acc[fakeMarker.name];
|
|
492
|
+
else skipAssign = true;
|
|
493
|
+
prevFakeMarker = null;
|
|
494
|
+
}
|
|
495
|
+
if (!skipAssign) acc[fakeMarker.name] = {
|
|
496
|
+
...prevFakeMarker,
|
|
497
|
+
[fakeMarker.type]: position
|
|
498
|
+
};
|
|
499
|
+
if (fakeMarker.markerElement) writer.remove(fakeMarker.markerElement);
|
|
500
|
+
return acc;
|
|
501
|
+
}, {}), (range) => new ModelRange(range.start || writer.createPositionFromPath(rootElement, [0]), range.end || writer.createPositionAt(rootElement, "end")));
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Returns array that contains list of fake markers with corresponding `$marker` elements.
|
|
505
|
+
*
|
|
506
|
+
* For each marker, there can be two `$marker` elements or only one (if the document fragment contained
|
|
507
|
+
* only the beginning or only the end of a marker).
|
|
508
|
+
*
|
|
509
|
+
* @param writer An instance of the model writer.
|
|
510
|
+
* @param rootElement The element to be checked.
|
|
511
|
+
*/
|
|
512
|
+
_getAllFakeMarkersFromElement(writer, rootElement) {
|
|
513
|
+
const foundFakeMarkers = Array.from(writer.createRangeIn(rootElement)).flatMap(({ item }) => {
|
|
514
|
+
if (!item.is("element", "$marker")) return [];
|
|
515
|
+
return [{
|
|
516
|
+
markerElement: item,
|
|
517
|
+
name: item.getAttribute("data-name"),
|
|
518
|
+
type: item.getAttribute("data-type")
|
|
519
|
+
}];
|
|
520
|
+
});
|
|
521
|
+
const prependFakeMarkers = [];
|
|
522
|
+
const appendFakeMarkers = [];
|
|
523
|
+
for (const fakeMarker of foundFakeMarkers) {
|
|
524
|
+
if (fakeMarker.type === "end") {
|
|
525
|
+
if (!foundFakeMarkers.some((otherFakeMarker) => otherFakeMarker.name === fakeMarker.name && otherFakeMarker.type === "start")) prependFakeMarkers.push({
|
|
526
|
+
markerElement: null,
|
|
527
|
+
name: fakeMarker.name,
|
|
528
|
+
type: "start"
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (fakeMarker.type === "start") {
|
|
532
|
+
if (!foundFakeMarkers.some((otherFakeMarker) => otherFakeMarker.name === fakeMarker.name && otherFakeMarker.type === "end")) appendFakeMarkers.unshift({
|
|
533
|
+
markerElement: null,
|
|
534
|
+
name: fakeMarker.name,
|
|
535
|
+
type: "end"
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return [
|
|
540
|
+
...prependFakeMarkers,
|
|
541
|
+
...foundFakeMarkers,
|
|
542
|
+
...appendFakeMarkers
|
|
543
|
+
];
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* When copy of markers occurs we have to make sure that pasted markers have different names
|
|
547
|
+
* than source markers. This functions helps with assigning unique part to marker name to
|
|
548
|
+
* prevent duplicated markers error.
|
|
549
|
+
*
|
|
550
|
+
* @param name Name of marker
|
|
551
|
+
*/
|
|
552
|
+
_getUniqueMarkerName(name) {
|
|
553
|
+
const parts = name.split(":");
|
|
554
|
+
const newId = uid().substring(1, 6);
|
|
555
|
+
if (parts.length === 3) return `${parts.slice(0, 2).join(":")}:${newId}`;
|
|
556
|
+
return `${parts.join(":")}:${newId}`;
|
|
557
|
+
}
|
|
558
|
+
};
|
|
723
559
|
|
|
724
|
-
// Input pipeline events overview:
|
|
725
|
-
//
|
|
726
|
-
// ┌──────────────────────┐ ┌──────────────────────┐
|
|
727
|
-
// │ view.Document │ │ view.Document │
|
|
728
|
-
// │ paste │ │ drop │
|
|
729
|
-
// └───────────┬──────────┘ └───────────┬──────────┘
|
|
730
|
-
// │ │
|
|
731
|
-
// └────────────────┌────────────────┘
|
|
732
|
-
// │
|
|
733
|
-
// ┌─────────V────────┐
|
|
734
|
-
// │ view.Document │ Retrieves text/html or text/plain from data.dataTransfer
|
|
735
|
-
// │ clipboardInput │ and processes it to view.DocumentFragment.
|
|
736
|
-
// └─────────┬────────┘
|
|
737
|
-
// │
|
|
738
|
-
// ┌───────────V───────────┐
|
|
739
|
-
// │ ClipboardPipeline │ Converts view.DocumentFragment to model.DocumentFragment.
|
|
740
|
-
// │ inputTransformation │
|
|
741
|
-
// └───────────┬───────────┘
|
|
742
|
-
// │
|
|
743
|
-
// ┌──────────V──────────┐
|
|
744
|
-
// │ ClipboardPipeline │ Calls model.insertContent().
|
|
745
|
-
// │ contentInsertion │
|
|
746
|
-
// └─────────────────────┘
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
// Output pipeline events overview:
|
|
750
|
-
//
|
|
751
|
-
// ┌──────────────────────┐ ┌──────────────────────┐
|
|
752
|
-
// │ view.Document │ │ view.Document │ Retrieves the selected model.DocumentFragment
|
|
753
|
-
// │ copy │ │ cut │ and fires the `outputTransformation` event.
|
|
754
|
-
// └───────────┬──────────┘ └───────────┬──────────┘
|
|
755
|
-
// │ │
|
|
756
|
-
// └────────────────┌────────────────┘
|
|
757
|
-
// │
|
|
758
|
-
// ┌───────────V───────────┐
|
|
759
|
-
// │ ClipboardPipeline │ Processes model.DocumentFragment and converts it to
|
|
760
|
-
// │ outputTransformation │ view.DocumentFragment.
|
|
761
|
-
// └───────────┬───────────┘
|
|
762
|
-
// │
|
|
763
|
-
// ┌─────────V────────┐
|
|
764
|
-
// │ view.Document │ Processes view.DocumentFragment to text/html and text/plain
|
|
765
|
-
// │ clipboardOutput │ and stores the results in data.dataTransfer.
|
|
766
|
-
// └──────────────────┘
|
|
767
|
-
//
|
|
768
560
|
/**
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
this.listenTo(viewDocument, 'cut', (evt, data)=>{
|
|
962
|
-
// Cutting is disabled when selection is in non-editable place.
|
|
963
|
-
// See: https://github.com/ckeditor/ckeditor5-clipboard/issues/26.
|
|
964
|
-
if (!editor.model.canEditAt(editor.model.document.selection)) {
|
|
965
|
-
data.preventDefault();
|
|
966
|
-
} else {
|
|
967
|
-
onCopyCut(evt, data);
|
|
968
|
-
}
|
|
969
|
-
}, {
|
|
970
|
-
priority: 'low'
|
|
971
|
-
});
|
|
972
|
-
this.listenTo(this, 'outputTransformation', (evt, data)=>{
|
|
973
|
-
const content = editor.data.toView(data.content, {
|
|
974
|
-
isClipboardPipeline: true
|
|
975
|
-
});
|
|
976
|
-
viewDocument.fire('clipboardOutput', {
|
|
977
|
-
dataTransfer: data.dataTransfer,
|
|
978
|
-
content,
|
|
979
|
-
method: data.method
|
|
980
|
-
});
|
|
981
|
-
}, {
|
|
982
|
-
priority: 'low'
|
|
983
|
-
});
|
|
984
|
-
this.listenTo(viewDocument, 'clipboardOutput', (evt, data)=>{
|
|
985
|
-
if (!data.content.isEmpty) {
|
|
986
|
-
data.dataTransfer.setData('text/html', this.editor.data.htmlProcessor.toData(data.content));
|
|
987
|
-
data.dataTransfer.setData('text/plain', viewToPlainText(editor.data.htmlProcessor.domConverter, data.content));
|
|
988
|
-
data.dataTransfer.setData('application/ckeditor5-editor-id', this.editor.id);
|
|
989
|
-
}
|
|
990
|
-
if (data.method == 'cut') {
|
|
991
|
-
editor.model.deleteContent(modelDocument.selection);
|
|
992
|
-
}
|
|
993
|
-
}, {
|
|
994
|
-
priority: 'low'
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
}
|
|
561
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
562
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
563
|
+
*/
|
|
564
|
+
/**
|
|
565
|
+
* @module clipboard/clipboardpipeline
|
|
566
|
+
*/
|
|
567
|
+
/**
|
|
568
|
+
* The clipboard pipeline feature. It is responsible for intercepting the `paste` and `drop` events and
|
|
569
|
+
* passing the pasted content through a series of events in order to insert it into the editor's content.
|
|
570
|
+
* It also handles the `cut` and `copy` events to fill the native clipboard with the serialized editor's data.
|
|
571
|
+
*
|
|
572
|
+
* # Input pipeline
|
|
573
|
+
*
|
|
574
|
+
* The behavior of the default handlers (all at a `low` priority):
|
|
575
|
+
*
|
|
576
|
+
* ## Event: `paste` or `drop`
|
|
577
|
+
*
|
|
578
|
+
* 1. Translates the event data.
|
|
579
|
+
* 2. Fires the {@link module:engine/view/document~ViewDocument#event:clipboardInput `view.Document#clipboardInput`} event.
|
|
580
|
+
*
|
|
581
|
+
* ## Event: `view.Document#clipboardInput`
|
|
582
|
+
*
|
|
583
|
+
* 1. If the `data.content` event field is already set (by some listener on a higher priority), it takes this content and fires the event
|
|
584
|
+
* from the last point.
|
|
585
|
+
* 2. Otherwise, it retrieves `text/html` or `text/plain` from `data.dataTransfer`.
|
|
586
|
+
* 3. Normalizes the raw data by applying simple filters on string data.
|
|
587
|
+
* 4. Processes the raw data to {@link module:engine/view/documentfragment~ViewDocumentFragment `view.DocumentFragment`} with the
|
|
588
|
+
* {@link module:engine/controller/datacontroller~DataController#htmlProcessor `DataController#htmlProcessor`}.
|
|
589
|
+
* 5. Fires the {@link module:clipboard/clipboardpipeline~ClipboardPipeline#event:inputTransformation
|
|
590
|
+
* `ClipboardPipeline#inputTransformation`} event with the view document fragment in the `data.content` event field.
|
|
591
|
+
*
|
|
592
|
+
* ## Event: `ClipboardPipeline#inputTransformation`
|
|
593
|
+
*
|
|
594
|
+
* 1. Converts {@link module:engine/view/documentfragment~ViewDocumentFragment `view.DocumentFragment`} from the `data.content` field to
|
|
595
|
+
* {@link module:engine/model/documentfragment~ModelDocumentFragment `model.DocumentFragment`}.
|
|
596
|
+
* 2. Fires the {@link module:clipboard/clipboardpipeline~ClipboardPipeline#event:contentInsertion `ClipboardPipeline#contentInsertion`}
|
|
597
|
+
* event with the model document fragment in the `data.content` event field.
|
|
598
|
+
* **Note**: The `ClipboardPipeline#contentInsertion` event is fired within a model change block to allow other handlers
|
|
599
|
+
* to run in the same block without post-fixers called in between (i.e., the selection post-fixer).
|
|
600
|
+
*
|
|
601
|
+
* ## Event: `ClipboardPipeline#contentInsertion`
|
|
602
|
+
*
|
|
603
|
+
* 1. Calls {@link module:engine/model/model~Model#insertContent `model.insertContent()`} to insert `data.content`
|
|
604
|
+
* at the current selection position.
|
|
605
|
+
*
|
|
606
|
+
* # Output pipeline
|
|
607
|
+
*
|
|
608
|
+
* The behavior of the default handlers (all at a `low` priority):
|
|
609
|
+
*
|
|
610
|
+
* ## Event: `copy`, `cut` or `dragstart`
|
|
611
|
+
*
|
|
612
|
+
* 1. Retrieves the selected {@link module:engine/model/documentfragment~ModelDocumentFragment `model.DocumentFragment`} by calling
|
|
613
|
+
* {@link module:engine/model/model~Model#getSelectedContent `model#getSelectedContent()`}.
|
|
614
|
+
* 2. Converts the model document fragment to {@link module:engine/view/documentfragment~ViewDocumentFragment `view.DocumentFragment`}.
|
|
615
|
+
* 3. Fires the {@link module:engine/view/document~ViewDocument#event:clipboardOutput `view.Document#clipboardOutput`} event
|
|
616
|
+
* with the view document fragment in the `data.content` event field.
|
|
617
|
+
*
|
|
618
|
+
* ## Event: `view.Document#clipboardOutput`
|
|
619
|
+
*
|
|
620
|
+
* 1. Processes `data.content` to HTML and plain text with the
|
|
621
|
+
* {@link module:engine/controller/datacontroller~DataController#htmlProcessor `DataController#htmlProcessor`}.
|
|
622
|
+
* 2. Updates the `data.dataTransfer` data for `text/html` and `text/plain` with the processed data.
|
|
623
|
+
* 3. For the `cut` method, calls {@link module:engine/model/model~Model#deleteContent `model.deleteContent()`}
|
|
624
|
+
* on the current selection.
|
|
625
|
+
*
|
|
626
|
+
* Read more about the clipboard integration in the {@glink framework/deep-dive/clipboard clipboard deep-dive} guide.
|
|
627
|
+
*/
|
|
628
|
+
var ClipboardPipeline = class extends Plugin {
|
|
629
|
+
/**
|
|
630
|
+
* @inheritDoc
|
|
631
|
+
*/
|
|
632
|
+
static get pluginName() {
|
|
633
|
+
return "ClipboardPipeline";
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* @inheritDoc
|
|
637
|
+
*/
|
|
638
|
+
static get isOfficialPlugin() {
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* @inheritDoc
|
|
643
|
+
*/
|
|
644
|
+
static get requires() {
|
|
645
|
+
return [ClipboardMarkersUtils];
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* @inheritDoc
|
|
649
|
+
*/
|
|
650
|
+
init() {
|
|
651
|
+
this.editor.editing.view.addObserver(ClipboardObserver);
|
|
652
|
+
this._setupPasteDrop();
|
|
653
|
+
this._setupCopyCut();
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Fires Clipboard `'outputTransformation'` event for given parameters.
|
|
657
|
+
*
|
|
658
|
+
* @internal
|
|
659
|
+
*/
|
|
660
|
+
_fireOutputTransformationEvent(dataTransfer, selection, method) {
|
|
661
|
+
const clipboardMarkersUtils = this.editor.plugins.get("ClipboardMarkersUtils");
|
|
662
|
+
this.editor.model.enqueueChange({ isUndoable: method === "cut" }, () => {
|
|
663
|
+
const documentFragment = clipboardMarkersUtils._copySelectedFragmentWithMarkers(method, selection);
|
|
664
|
+
this.fire("outputTransformation", {
|
|
665
|
+
dataTransfer,
|
|
666
|
+
content: documentFragment,
|
|
667
|
+
method
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* The clipboard paste pipeline.
|
|
673
|
+
*/
|
|
674
|
+
_setupPasteDrop() {
|
|
675
|
+
const editor = this.editor;
|
|
676
|
+
const model = editor.model;
|
|
677
|
+
const view = editor.editing.view;
|
|
678
|
+
const viewDocument = view.document;
|
|
679
|
+
const clipboardMarkersUtils = this.editor.plugins.get("ClipboardMarkersUtils");
|
|
680
|
+
this.listenTo(viewDocument, "clipboardInput", (evt, data) => {
|
|
681
|
+
if (data.method == "paste" && !editor.model.canEditAt(editor.model.document.selection)) evt.stop();
|
|
682
|
+
}, { priority: "highest" });
|
|
683
|
+
this.listenTo(viewDocument, "clipboardInput", (evt, data) => {
|
|
684
|
+
const dataTransfer = data.dataTransfer;
|
|
685
|
+
const eventInfo = new EventInfo(this, "inputTransformation");
|
|
686
|
+
const sourceEditorId = dataTransfer.getData("application/ckeditor5-editor-id") || null;
|
|
687
|
+
const content = typeof data.content == "string" ? this.editor.data.htmlProcessor.toView(normalizeClipboardData(data.content)) : data.content;
|
|
688
|
+
this.fire(eventInfo, {
|
|
689
|
+
content,
|
|
690
|
+
dataTransfer,
|
|
691
|
+
sourceEditorId,
|
|
692
|
+
extraContent: data.extraContent,
|
|
693
|
+
targetRanges: data.targetRanges,
|
|
694
|
+
method: data.method
|
|
695
|
+
});
|
|
696
|
+
if (eventInfo.stop.called) evt.stop();
|
|
697
|
+
view.scrollToTheSelection();
|
|
698
|
+
}, { priority: "low" });
|
|
699
|
+
this.listenTo(this, "inputTransformation", (evt, data) => {
|
|
700
|
+
if (data.content.isEmpty) return;
|
|
701
|
+
const modelFragment = this.editor.data.toModel(data.content, "$clipboardHolder");
|
|
702
|
+
if (modelFragment.childCount == 0) return;
|
|
703
|
+
evt.stop();
|
|
704
|
+
model.change(() => {
|
|
705
|
+
this.fire("contentInsertion", {
|
|
706
|
+
content: modelFragment,
|
|
707
|
+
method: data.method,
|
|
708
|
+
sourceEditorId: data.sourceEditorId,
|
|
709
|
+
dataTransfer: data.dataTransfer,
|
|
710
|
+
targetRanges: data.targetRanges
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
}, { priority: "low" });
|
|
714
|
+
this.listenTo(this, "contentInsertion", (evt, data) => {
|
|
715
|
+
data.resultRange = clipboardMarkersUtils._pasteFragmentWithMarkers(data.content);
|
|
716
|
+
}, { priority: "low" });
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* The clipboard copy/cut pipeline.
|
|
720
|
+
*/
|
|
721
|
+
_setupCopyCut() {
|
|
722
|
+
const editor = this.editor;
|
|
723
|
+
const modelDocument = editor.model.document;
|
|
724
|
+
const viewDocument = editor.editing.view.document;
|
|
725
|
+
const onCopyCut = (evt, data) => {
|
|
726
|
+
const dataTransfer = data.dataTransfer;
|
|
727
|
+
data.preventDefault();
|
|
728
|
+
this._fireOutputTransformationEvent(dataTransfer, modelDocument.selection, evt.name);
|
|
729
|
+
};
|
|
730
|
+
this.listenTo(viewDocument, "copy", onCopyCut, { priority: "low" });
|
|
731
|
+
this.listenTo(viewDocument, "cut", (evt, data) => {
|
|
732
|
+
if (!editor.model.canEditAt(editor.model.document.selection)) data.preventDefault();
|
|
733
|
+
else onCopyCut(evt, data);
|
|
734
|
+
}, { priority: "low" });
|
|
735
|
+
this.listenTo(this, "outputTransformation", (evt, data) => {
|
|
736
|
+
const content = editor.data.toView(data.content, { isClipboardPipeline: true });
|
|
737
|
+
viewDocument.fire("clipboardOutput", {
|
|
738
|
+
dataTransfer: data.dataTransfer,
|
|
739
|
+
content,
|
|
740
|
+
method: data.method
|
|
741
|
+
});
|
|
742
|
+
}, { priority: "low" });
|
|
743
|
+
this.listenTo(viewDocument, "clipboardOutput", (evt, data) => {
|
|
744
|
+
if (!data.content.isEmpty) {
|
|
745
|
+
data.dataTransfer.setData("text/html", this.editor.data.htmlProcessor.toData(data.content));
|
|
746
|
+
data.dataTransfer.setData("text/plain", viewToPlainText(editor.data.htmlProcessor.domConverter, data.content));
|
|
747
|
+
data.dataTransfer.setData("application/ckeditor5-editor-id", this.editor.id);
|
|
748
|
+
}
|
|
749
|
+
if (data.method == "cut") editor.model.deleteContent(modelDocument.selection);
|
|
750
|
+
}, { priority: "low" });
|
|
751
|
+
}
|
|
752
|
+
};
|
|
998
753
|
|
|
999
|
-
const toPx = /* #__PURE__ */ toUnit('px');
|
|
1000
754
|
/**
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
755
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
756
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
757
|
+
*/
|
|
758
|
+
/**
|
|
759
|
+
* @module clipboard/lineview
|
|
760
|
+
*/
|
|
761
|
+
/* istanbul ignore file -- @preserve */
|
|
762
|
+
const toPx = /* #__PURE__ */ toUnit("px");
|
|
763
|
+
/**
|
|
764
|
+
* The horizontal drop target line view.
|
|
765
|
+
*
|
|
766
|
+
* @internal
|
|
767
|
+
*/
|
|
768
|
+
var LineView = class extends View {
|
|
769
|
+
/**
|
|
770
|
+
* @inheritDoc
|
|
771
|
+
*/
|
|
772
|
+
constructor() {
|
|
773
|
+
super();
|
|
774
|
+
const bind = this.bindTemplate;
|
|
775
|
+
this.set({
|
|
776
|
+
isVisible: false,
|
|
777
|
+
left: null,
|
|
778
|
+
top: null,
|
|
779
|
+
width: null
|
|
780
|
+
});
|
|
781
|
+
this.setTemplate({
|
|
782
|
+
tag: "div",
|
|
783
|
+
attributes: {
|
|
784
|
+
class: [
|
|
785
|
+
"ck",
|
|
786
|
+
"ck-clipboard-drop-target-line",
|
|
787
|
+
bind.if("isVisible", "ck-hidden", (value) => !value)
|
|
788
|
+
],
|
|
789
|
+
style: {
|
|
790
|
+
left: bind.to("left", (left) => toPx(left)),
|
|
791
|
+
top: bind.to("top", (top) => toPx(top)),
|
|
792
|
+
width: bind.to("width", (width) => toPx(width))
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
};
|
|
1033
798
|
|
|
1034
799
|
/**
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
const domElement = this.editor.editing.view.domConverter.mapViewToDom(viewElement);
|
|
1256
|
-
domScrollable = findScrollableElement(domElement);
|
|
1257
|
-
this._domEmitter.listenTo(domScrollable, 'scroll', this._reconvertMarkerThrottled, {
|
|
1258
|
-
usePassive: true
|
|
1259
|
-
});
|
|
1260
|
-
const resizeObserver = new ResizeObserver(domScrollable, this._reconvertMarkerThrottled);
|
|
1261
|
-
this._scrollables.set(rootName, {
|
|
1262
|
-
domElement: domScrollable,
|
|
1263
|
-
resizeObserver
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
|
-
return new Rect(domScrollable).excludeScrollbarsAndBorders();
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
800
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
801
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
802
|
+
*/
|
|
803
|
+
/**
|
|
804
|
+
* @module clipboard/dragdroptarget
|
|
805
|
+
*/
|
|
806
|
+
/**
|
|
807
|
+
* Part of the Drag and Drop handling. Responsible for finding and displaying the drop target.
|
|
808
|
+
*
|
|
809
|
+
* @internal
|
|
810
|
+
*/
|
|
811
|
+
var DragDropTarget = class extends Plugin {
|
|
812
|
+
/**
|
|
813
|
+
* A delayed callback removing the drop marker.
|
|
814
|
+
*
|
|
815
|
+
* @internal
|
|
816
|
+
*/
|
|
817
|
+
removeDropMarkerDelayed = delay(() => this.removeDropMarker(), 40);
|
|
818
|
+
/**
|
|
819
|
+
* A throttled callback updating the drop marker.
|
|
820
|
+
*/
|
|
821
|
+
_updateDropMarkerThrottled = throttle((targetRange) => this._updateDropMarker(targetRange), 40);
|
|
822
|
+
/**
|
|
823
|
+
* A throttled callback reconverting the drop parker.
|
|
824
|
+
*/
|
|
825
|
+
_reconvertMarkerThrottled = throttle(() => {
|
|
826
|
+
if (this.editor.model.markers.has("drop-target")) this.editor.editing.reconvertMarker("drop-target");
|
|
827
|
+
}, 0);
|
|
828
|
+
/**
|
|
829
|
+
* The horizontal drop target line view.
|
|
830
|
+
*/
|
|
831
|
+
_dropTargetLineView = new LineView();
|
|
832
|
+
/**
|
|
833
|
+
* DOM Emitter.
|
|
834
|
+
*/
|
|
835
|
+
_domEmitter = new (DomEmitterMixin())();
|
|
836
|
+
/**
|
|
837
|
+
* Map of document scrollable elements.
|
|
838
|
+
*/
|
|
839
|
+
_scrollables = /* @__PURE__ */ new Map();
|
|
840
|
+
/**
|
|
841
|
+
* @inheritDoc
|
|
842
|
+
*/
|
|
843
|
+
static get pluginName() {
|
|
844
|
+
return "DragDropTarget";
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* @inheritDoc
|
|
848
|
+
*/
|
|
849
|
+
static get isOfficialPlugin() {
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* @inheritDoc
|
|
854
|
+
*/
|
|
855
|
+
init() {
|
|
856
|
+
this._setupDropMarker();
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* @inheritDoc
|
|
860
|
+
*/
|
|
861
|
+
destroy() {
|
|
862
|
+
this._domEmitter.stopListening();
|
|
863
|
+
for (const { resizeObserver } of this._scrollables.values()) resizeObserver.destroy();
|
|
864
|
+
this._updateDropMarkerThrottled.cancel();
|
|
865
|
+
this.removeDropMarkerDelayed.cancel();
|
|
866
|
+
this._reconvertMarkerThrottled.cancel();
|
|
867
|
+
return super.destroy();
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Finds the drop target range and updates the drop marker.
|
|
871
|
+
*
|
|
872
|
+
* @return The updated drop target range or null if no valid range was found.
|
|
873
|
+
* @internal
|
|
874
|
+
*/
|
|
875
|
+
updateDropMarker(targetViewElement, targetViewRanges, clientX, clientY, blockMode, draggedRange) {
|
|
876
|
+
this.removeDropMarkerDelayed.cancel();
|
|
877
|
+
const targetRange = findDropTargetRange(this.editor, targetViewElement, targetViewRanges, clientX, clientY, blockMode, draggedRange);
|
|
878
|
+
/* istanbul ignore next -- @preserve */
|
|
879
|
+
if (!targetRange) return null;
|
|
880
|
+
if (draggedRange && draggedRange.containsRange(targetRange)) {
|
|
881
|
+
this.removeDropMarker();
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
if (targetRange && !this.editor.model.canEditAt(targetRange)) {
|
|
885
|
+
this.removeDropMarker();
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
this._updateDropMarkerThrottled(targetRange);
|
|
889
|
+
return targetRange;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Finds the final drop target range.
|
|
893
|
+
*
|
|
894
|
+
* @internal
|
|
895
|
+
*/
|
|
896
|
+
getFinalDropRange(targetViewElement, targetViewRanges, clientX, clientY, blockMode, draggedRange) {
|
|
897
|
+
const targetRange = findDropTargetRange(this.editor, targetViewElement, targetViewRanges, clientX, clientY, blockMode, draggedRange);
|
|
898
|
+
this.removeDropMarker();
|
|
899
|
+
return targetRange;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Removes the drop target marker.
|
|
903
|
+
*
|
|
904
|
+
* @internal
|
|
905
|
+
*/
|
|
906
|
+
removeDropMarker() {
|
|
907
|
+
const model = this.editor.model;
|
|
908
|
+
this.removeDropMarkerDelayed.cancel();
|
|
909
|
+
this._updateDropMarkerThrottled.cancel();
|
|
910
|
+
this._dropTargetLineView.isVisible = false;
|
|
911
|
+
if (model.markers.has("drop-target")) model.change((writer) => {
|
|
912
|
+
writer.removeMarker("drop-target");
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Creates downcast conversion for the drop target marker.
|
|
917
|
+
*/
|
|
918
|
+
_setupDropMarker() {
|
|
919
|
+
const editor = this.editor;
|
|
920
|
+
editor.ui.view.body.add(this._dropTargetLineView);
|
|
921
|
+
editor.conversion.for("editingDowncast").markerToHighlight({
|
|
922
|
+
model: "drop-target",
|
|
923
|
+
view: { classes: ["ck-clipboard-drop-target-range"] }
|
|
924
|
+
});
|
|
925
|
+
editor.conversion.for("editingDowncast").markerToElement({
|
|
926
|
+
model: "drop-target",
|
|
927
|
+
view: (data, { writer }) => {
|
|
928
|
+
if (editor.model.schema.checkChild(data.markerRange.start, "$text")) {
|
|
929
|
+
this._dropTargetLineView.isVisible = false;
|
|
930
|
+
return this._createDropTargetPosition(writer);
|
|
931
|
+
} else if (data.markerRange.isCollapsed) this._updateDropTargetLine(data.markerRange);
|
|
932
|
+
else this._dropTargetLineView.isVisible = false;
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Updates the drop target marker to the provided range.
|
|
938
|
+
*
|
|
939
|
+
* @param targetRange The range to set the marker to.
|
|
940
|
+
*/
|
|
941
|
+
_updateDropMarker(targetRange) {
|
|
942
|
+
const editor = this.editor;
|
|
943
|
+
const markers = editor.model.markers;
|
|
944
|
+
editor.model.change((writer) => {
|
|
945
|
+
if (markers.has("drop-target")) {
|
|
946
|
+
if (!markers.get("drop-target").getRange().isEqual(targetRange)) writer.updateMarker("drop-target", { range: targetRange });
|
|
947
|
+
} else writer.addMarker("drop-target", {
|
|
948
|
+
range: targetRange,
|
|
949
|
+
usingOperation: false,
|
|
950
|
+
affectsData: false
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Creates the UI element for vertical (in-line) drop target.
|
|
956
|
+
*/
|
|
957
|
+
_createDropTargetPosition(writer) {
|
|
958
|
+
return writer.createUIElement("span", { class: "ck ck-clipboard-drop-target-position" }, function(domDocument) {
|
|
959
|
+
const domElement = this.toDomElement(domDocument);
|
|
960
|
+
domElement.append("", domDocument.createElement("span"), "");
|
|
961
|
+
return domElement;
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Updates the horizontal drop target line.
|
|
966
|
+
*/
|
|
967
|
+
_updateDropTargetLine(range) {
|
|
968
|
+
const editing = this.editor.editing;
|
|
969
|
+
const nodeBefore = range.start.nodeBefore;
|
|
970
|
+
const nodeAfter = range.start.nodeAfter;
|
|
971
|
+
const nodeParent = range.start.parent;
|
|
972
|
+
const viewElementBefore = nodeBefore ? editing.mapper.toViewElement(nodeBefore) : null;
|
|
973
|
+
const domElementBefore = viewElementBefore ? editing.view.domConverter.mapViewToDom(viewElementBefore) : null;
|
|
974
|
+
const viewElementAfter = nodeAfter ? editing.mapper.toViewElement(nodeAfter) : null;
|
|
975
|
+
const domElementAfter = viewElementAfter ? editing.view.domConverter.mapViewToDom(viewElementAfter) : null;
|
|
976
|
+
const viewElementParent = editing.mapper.toViewElement(nodeParent);
|
|
977
|
+
if (!viewElementParent) return;
|
|
978
|
+
const domElementParent = editing.view.domConverter.mapViewToDom(viewElementParent);
|
|
979
|
+
const domScrollableRect = this._getScrollableRect(viewElementParent);
|
|
980
|
+
const { scrollX, scrollY } = global.window;
|
|
981
|
+
const rectBefore = domElementBefore ? new Rect(domElementBefore) : null;
|
|
982
|
+
const rectAfter = domElementAfter ? new Rect(domElementAfter) : null;
|
|
983
|
+
const rectParent = new Rect(domElementParent).excludeScrollbarsAndBorders();
|
|
984
|
+
const above = rectBefore ? rectBefore.bottom : rectParent.top;
|
|
985
|
+
const below = rectAfter ? rectAfter.top : rectParent.bottom;
|
|
986
|
+
const parentStyle = global.window.getComputedStyle(domElementParent);
|
|
987
|
+
const top = above <= below ? (above + below) / 2 : below;
|
|
988
|
+
if (domScrollableRect.top < top && top < domScrollableRect.bottom) {
|
|
989
|
+
const left = rectParent.left + parseFloat(parentStyle.paddingLeft);
|
|
990
|
+
const right = rectParent.right - parseFloat(parentStyle.paddingRight);
|
|
991
|
+
const leftClamped = Math.max(left + scrollX, domScrollableRect.left);
|
|
992
|
+
const rightClamped = Math.min(right + scrollX, domScrollableRect.right);
|
|
993
|
+
this._dropTargetLineView.set({
|
|
994
|
+
isVisible: true,
|
|
995
|
+
left: leftClamped,
|
|
996
|
+
top: top + scrollY,
|
|
997
|
+
width: rightClamped - leftClamped
|
|
998
|
+
});
|
|
999
|
+
} else this._dropTargetLineView.isVisible = false;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Finds the closest scrollable element rect for the given view element.
|
|
1003
|
+
*/
|
|
1004
|
+
_getScrollableRect(viewElement) {
|
|
1005
|
+
const rootName = viewElement.root.rootName;
|
|
1006
|
+
let domScrollable;
|
|
1007
|
+
if (this._scrollables.has(rootName)) domScrollable = this._scrollables.get(rootName).domElement;
|
|
1008
|
+
else {
|
|
1009
|
+
domScrollable = findScrollableElement(this.editor.editing.view.domConverter.mapViewToDom(viewElement));
|
|
1010
|
+
this._domEmitter.listenTo(domScrollable, "scroll", this._reconvertMarkerThrottled, { usePassive: true });
|
|
1011
|
+
const resizeObserver = new ResizeObserver(domScrollable, this._reconvertMarkerThrottled);
|
|
1012
|
+
this._scrollables.set(rootName, {
|
|
1013
|
+
domElement: domScrollable,
|
|
1014
|
+
resizeObserver
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
return new Rect(domScrollable).excludeScrollbarsAndBorders();
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1269
1020
|
/**
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
return model.createRange(model.createPositionAt(modelElement, 'end'));
|
|
1306
|
-
}
|
|
1307
|
-
while(startIndex < endIndex - 1){
|
|
1308
|
-
const middleIndex = Math.floor((startIndex + endIndex) / 2);
|
|
1309
|
-
const side = findElementSide(editor, childNodes[middleIndex], clientX, clientY);
|
|
1310
|
-
if (side == 'before') {
|
|
1311
|
-
endIndex = middleIndex;
|
|
1312
|
-
} else {
|
|
1313
|
-
startIndex = middleIndex;
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
return findDropTargetRangeForElement(editor, childNodes[startIndex], clientX, clientY);
|
|
1317
|
-
}
|
|
1318
|
-
modelElement = modelElement.parent;
|
|
1319
|
-
}
|
|
1320
|
-
return null;
|
|
1021
|
+
* Returns fixed selection range for given position and target element.
|
|
1022
|
+
*/
|
|
1023
|
+
function findDropTargetRange(editor, targetViewElement, targetViewRanges, clientX, clientY, blockMode, draggedRange) {
|
|
1024
|
+
const model = editor.model;
|
|
1025
|
+
const mapper = editor.editing.mapper;
|
|
1026
|
+
let modelElement = getClosestMappedModelElement(editor, targetViewElement);
|
|
1027
|
+
while (modelElement) {
|
|
1028
|
+
if (!blockMode) {
|
|
1029
|
+
if (model.schema.checkChild(modelElement, "$text")) {
|
|
1030
|
+
if (targetViewRanges) {
|
|
1031
|
+
const targetViewPosition = targetViewRanges[0].start;
|
|
1032
|
+
const targetModelPosition = mapper.toModelPosition(targetViewPosition);
|
|
1033
|
+
if (!draggedRange || Array.from(draggedRange.getItems({ shallow: true })).some((item) => model.schema.checkChild(targetModelPosition, item))) {
|
|
1034
|
+
if (model.schema.checkChild(targetModelPosition, "$text")) return model.createRange(targetModelPosition);
|
|
1035
|
+
else if (targetViewPosition) return findDropTargetRangeForElement(editor, getClosestMappedModelElement(editor, targetViewPosition.parent), clientX, clientY);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
} else if (model.schema.isInline(modelElement)) return findDropTargetRangeForElement(editor, modelElement, clientX, clientY);
|
|
1039
|
+
}
|
|
1040
|
+
if (model.schema.isBlock(modelElement)) return findDropTargetRangeForElement(editor, modelElement, clientX, clientY);
|
|
1041
|
+
else if (model.schema.checkChild(modelElement, "$block")) {
|
|
1042
|
+
const childNodes = Array.from(modelElement.getChildren()).filter((node) => node.is("element") && !shouldIgnoreElement(editor, node));
|
|
1043
|
+
let startIndex = 0;
|
|
1044
|
+
let endIndex = childNodes.length;
|
|
1045
|
+
if (endIndex == 0) return model.createRange(model.createPositionAt(modelElement, "end"));
|
|
1046
|
+
while (startIndex < endIndex - 1) {
|
|
1047
|
+
const middleIndex = Math.floor((startIndex + endIndex) / 2);
|
|
1048
|
+
if (findElementSide(editor, childNodes[middleIndex], clientX, clientY) == "before") endIndex = middleIndex;
|
|
1049
|
+
else startIndex = middleIndex;
|
|
1050
|
+
}
|
|
1051
|
+
return findDropTargetRangeForElement(editor, childNodes[startIndex], clientX, clientY);
|
|
1052
|
+
}
|
|
1053
|
+
modelElement = modelElement.parent;
|
|
1054
|
+
}
|
|
1055
|
+
return null;
|
|
1321
1056
|
}
|
|
1322
1057
|
/**
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
return global.window.getComputedStyle(domElement).float != 'none';
|
|
1058
|
+
* Returns true for elements which should be ignored.
|
|
1059
|
+
*/
|
|
1060
|
+
function shouldIgnoreElement(editor, modelElement) {
|
|
1061
|
+
const mapper = editor.editing.mapper;
|
|
1062
|
+
const domConverter = editor.editing.view.domConverter;
|
|
1063
|
+
const viewElement = mapper.toViewElement(modelElement);
|
|
1064
|
+
if (!viewElement) return true;
|
|
1065
|
+
const domElement = domConverter.mapViewToDom(viewElement);
|
|
1066
|
+
return global.window.getComputedStyle(domElement).float != "none";
|
|
1333
1067
|
}
|
|
1334
1068
|
/**
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1069
|
+
* Returns target range relative to the given element.
|
|
1070
|
+
*/
|
|
1071
|
+
function findDropTargetRangeForElement(editor, modelElement, clientX, clientY) {
|
|
1072
|
+
const model = editor.model;
|
|
1073
|
+
return model.createRange(model.createPositionAt(modelElement, findElementSide(editor, modelElement, clientX, clientY)));
|
|
1339
1074
|
}
|
|
1340
1075
|
/**
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
} else {
|
|
1351
|
-
return clientY < (rect.top + rect.bottom) / 2 ? 'before' : 'after';
|
|
1352
|
-
}
|
|
1076
|
+
* Resolves whether drop marker should be before or after the given element.
|
|
1077
|
+
*/
|
|
1078
|
+
function findElementSide(editor, modelElement, clientX, clientY) {
|
|
1079
|
+
const mapper = editor.editing.mapper;
|
|
1080
|
+
const domConverter = editor.editing.view.domConverter;
|
|
1081
|
+
const viewElement = mapper.toViewElement(modelElement);
|
|
1082
|
+
const rect = new Rect(domConverter.mapViewToDom(viewElement));
|
|
1083
|
+
if (editor.model.schema.isInline(modelElement)) return clientX < (rect.left + rect.right) / 2 ? "before" : "after";
|
|
1084
|
+
else return clientY < (rect.top + rect.bottom) / 2 ? "before" : "after";
|
|
1353
1085
|
}
|
|
1354
1086
|
/**
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const viewElement = mapper.findMappedViewAncestor(viewPosition);
|
|
1366
|
-
return mapper.toModelElement(viewElement);
|
|
1087
|
+
* Returns the closest model element for the specified view element.
|
|
1088
|
+
*/
|
|
1089
|
+
function getClosestMappedModelElement(editor, element) {
|
|
1090
|
+
const mapper = editor.editing.mapper;
|
|
1091
|
+
const view = editor.editing.view;
|
|
1092
|
+
const targetModelElement = mapper.toModelElement(element);
|
|
1093
|
+
if (targetModelElement) return targetModelElement;
|
|
1094
|
+
const viewPosition = view.createPositionBefore(element);
|
|
1095
|
+
const viewElement = mapper.findMappedViewAncestor(viewPosition);
|
|
1096
|
+
return mapper.toModelElement(viewElement);
|
|
1367
1097
|
}
|
|
1368
1098
|
/**
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
return domElement;
|
|
1099
|
+
* Returns the closest scrollable ancestor DOM element.
|
|
1100
|
+
*
|
|
1101
|
+
* It is assumed that `domNode` is attached to the document.
|
|
1102
|
+
*/
|
|
1103
|
+
function findScrollableElement(domNode) {
|
|
1104
|
+
let domElement = domNode;
|
|
1105
|
+
do {
|
|
1106
|
+
domElement = domElement.parentElement;
|
|
1107
|
+
const overflow = global.window.getComputedStyle(domElement).overflowY;
|
|
1108
|
+
if (overflow == "auto" || overflow == "scroll") break;
|
|
1109
|
+
} while (domElement.tagName != "BODY");
|
|
1110
|
+
return domElement;
|
|
1382
1111
|
}
|
|
1383
1112
|
|
|
1384
1113
|
/**
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1114
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1115
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1116
|
+
*/
|
|
1117
|
+
/**
|
|
1118
|
+
* @module clipboard/dragdropblocktoolbar
|
|
1119
|
+
*/
|
|
1120
|
+
/**
|
|
1121
|
+
* Integration of a block Drag and Drop support with the block toolbar.
|
|
1122
|
+
*
|
|
1123
|
+
* @internal
|
|
1124
|
+
*/
|
|
1125
|
+
var DragDropBlockToolbar = class extends Plugin {
|
|
1126
|
+
/**
|
|
1127
|
+
* Whether current dragging is started by block toolbar button dragging.
|
|
1128
|
+
*/
|
|
1129
|
+
_isBlockDragging = false;
|
|
1130
|
+
/**
|
|
1131
|
+
* DOM Emitter.
|
|
1132
|
+
*/
|
|
1133
|
+
_domEmitter = new (DomEmitterMixin())();
|
|
1134
|
+
/**
|
|
1135
|
+
* @inheritDoc
|
|
1136
|
+
*/
|
|
1137
|
+
static get pluginName() {
|
|
1138
|
+
return "DragDropBlockToolbar";
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* @inheritDoc
|
|
1142
|
+
*/
|
|
1143
|
+
static get isOfficialPlugin() {
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* @inheritDoc
|
|
1148
|
+
*/
|
|
1149
|
+
init() {
|
|
1150
|
+
const editor = this.editor;
|
|
1151
|
+
this.listenTo(editor, "change:isReadOnly", (evt, name, isReadOnly) => {
|
|
1152
|
+
if (isReadOnly) {
|
|
1153
|
+
this.forceDisabled("readOnlyMode");
|
|
1154
|
+
this._isBlockDragging = false;
|
|
1155
|
+
} else this.clearForceDisabled("readOnlyMode");
|
|
1156
|
+
});
|
|
1157
|
+
if (env.isAndroid) this.forceDisabled("noAndroidSupport");
|
|
1158
|
+
if (editor.plugins.has("BlockToolbar")) {
|
|
1159
|
+
const element = editor.plugins.get("BlockToolbar").buttonView.element;
|
|
1160
|
+
this._domEmitter.listenTo(element, "dragstart", (evt, data) => this._handleBlockDragStart(data));
|
|
1161
|
+
this._domEmitter.listenTo(global.document, "dragover", (evt, data) => this._handleBlockDragging(data));
|
|
1162
|
+
this._domEmitter.listenTo(global.document, "drop", (evt, data) => this._handleBlockDragging(data));
|
|
1163
|
+
this._domEmitter.listenTo(global.document, "dragend", () => this._handleBlockDragEnd(), { useCapture: true });
|
|
1164
|
+
if (this.isEnabled) element.setAttribute("draggable", "true");
|
|
1165
|
+
this.on("change:isEnabled", (evt, name, isEnabled) => {
|
|
1166
|
+
element.setAttribute("draggable", isEnabled ? "true" : "false");
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* @inheritDoc
|
|
1172
|
+
*/
|
|
1173
|
+
destroy() {
|
|
1174
|
+
this._domEmitter.stopListening();
|
|
1175
|
+
return super.destroy();
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* The `dragstart` event handler.
|
|
1179
|
+
*/
|
|
1180
|
+
_handleBlockDragStart(domEvent) {
|
|
1181
|
+
if (!this.isEnabled) return;
|
|
1182
|
+
const model = this.editor.model;
|
|
1183
|
+
const selection = model.document.selection;
|
|
1184
|
+
const view = this.editor.editing.view;
|
|
1185
|
+
const blocks = Array.from(selection.getSelectedBlocks());
|
|
1186
|
+
const draggedRange = model.createRange(model.createPositionBefore(blocks[0]), model.createPositionAfter(blocks[blocks.length - 1]));
|
|
1187
|
+
model.change((writer) => writer.setSelection(draggedRange));
|
|
1188
|
+
this._isBlockDragging = true;
|
|
1189
|
+
view.focus();
|
|
1190
|
+
view.getObserver(ClipboardObserver).onDomEvent(domEvent);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* The `dragover` and `drop` event handler.
|
|
1194
|
+
*/
|
|
1195
|
+
_handleBlockDragging(domEvent) {
|
|
1196
|
+
if (!this.isEnabled || !this._isBlockDragging) return;
|
|
1197
|
+
const clientX = domEvent.clientX + (this.editor.locale.contentLanguageDirection == "ltr" ? 100 : -100);
|
|
1198
|
+
const clientY = domEvent.clientY;
|
|
1199
|
+
const target = document.elementFromPoint(clientX, clientY);
|
|
1200
|
+
const view = this.editor.editing.view;
|
|
1201
|
+
if (!target || !target.closest(".ck-editor__editable")) return;
|
|
1202
|
+
view.getObserver(ClipboardObserver).onDomEvent({
|
|
1203
|
+
...domEvent,
|
|
1204
|
+
type: domEvent.type,
|
|
1205
|
+
dataTransfer: domEvent.dataTransfer,
|
|
1206
|
+
target,
|
|
1207
|
+
clientX,
|
|
1208
|
+
clientY,
|
|
1209
|
+
preventDefault: () => domEvent.preventDefault(),
|
|
1210
|
+
stopPropagation: () => domEvent.stopPropagation()
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* The `dragend` event handler.
|
|
1215
|
+
*/
|
|
1216
|
+
_handleBlockDragEnd() {
|
|
1217
|
+
this._isBlockDragging = false;
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1489
1220
|
|
|
1490
|
-
// Drag and drop events overview:
|
|
1491
|
-
//
|
|
1492
|
-
// ┌──────────────────┐
|
|
1493
|
-
// │ mousedown │ Sets the draggable attribute.
|
|
1494
|
-
// └─────────┬────────┘
|
|
1495
|
-
// │
|
|
1496
|
-
// └─────────────────────┐
|
|
1497
|
-
// │ │
|
|
1498
|
-
// │ ┌─────────V────────┐
|
|
1499
|
-
// │ │ mouseup │ Dragging did not start, removes the draggable attribute.
|
|
1500
|
-
// │ └──────────────────┘
|
|
1501
|
-
// │
|
|
1502
|
-
// ┌─────────V────────┐ Retrieves the selected model.DocumentFragment
|
|
1503
|
-
// │ dragstart │ and converts it to view.DocumentFragment.
|
|
1504
|
-
// └─────────┬────────┘
|
|
1505
|
-
// │
|
|
1506
|
-
// ┌─────────V────────┐ Processes view.DocumentFragment to text/html and text/plain
|
|
1507
|
-
// │ clipboardOutput │ and stores the results in data.dataTransfer.
|
|
1508
|
-
// └─────────┬────────┘
|
|
1509
|
-
// │
|
|
1510
|
-
// │ DOM dragover
|
|
1511
|
-
// ┌────────────┐
|
|
1512
|
-
// │ │
|
|
1513
|
-
// ┌─────────V────────┐ │
|
|
1514
|
-
// │ dragging │ │ Updates the drop target marker.
|
|
1515
|
-
// └─────────┬────────┘ │
|
|
1516
|
-
// │ │
|
|
1517
|
-
// ┌─────────────└────────────┘
|
|
1518
|
-
// │ │ │
|
|
1519
|
-
// │ ┌─────────V────────┐ │
|
|
1520
|
-
// │ │ dragleave │ │ Removes the drop target marker.
|
|
1521
|
-
// │ └─────────┬────────┘ │
|
|
1522
|
-
// │ │ │
|
|
1523
|
-
// ┌───│─────────────┘ │
|
|
1524
|
-
// │ │ │ │
|
|
1525
|
-
// │ │ ┌─────────V────────┐ │
|
|
1526
|
-
// │ │ │ dragenter │ │ Focuses the editor view.
|
|
1527
|
-
// │ │ └─────────┬────────┘ │
|
|
1528
|
-
// │ │ │ │
|
|
1529
|
-
// │ │ └────────────┘
|
|
1530
|
-
// │ │
|
|
1531
|
-
// │ └─────────────┐
|
|
1532
|
-
// │ │ │
|
|
1533
|
-
// │ │ ┌─────────V────────┐
|
|
1534
|
-
// └───┐ │ drop │ (The default handler of the clipboard pipeline).
|
|
1535
|
-
// │ └─────────┬────────┘
|
|
1536
|
-
// │ │
|
|
1537
|
-
// │ ┌─────────V────────┐ Resolves the final data.targetRanges.
|
|
1538
|
-
// │ │ clipboardInput │ Aborts if dropping on dragged content.
|
|
1539
|
-
// │ └─────────┬────────┘
|
|
1540
|
-
// │ │
|
|
1541
|
-
// │ ┌─────────V────────┐
|
|
1542
|
-
// │ │ clipboardInput │ (The default handler of the clipboard pipeline).
|
|
1543
|
-
// │ └─────────┬────────┘
|
|
1544
|
-
// │ │
|
|
1545
|
-
// │ ┌───────────V───────────┐
|
|
1546
|
-
// │ │ inputTransformation │ (The default handler of the clipboard pipeline).
|
|
1547
|
-
// │ └───────────┬───────────┘
|
|
1548
|
-
// │ │
|
|
1549
|
-
// │ ┌──────────V──────────┐
|
|
1550
|
-
// │ │ contentInsertion │ Updates the document selection to drop range.
|
|
1551
|
-
// │ └──────────┬──────────┘
|
|
1552
|
-
// │ │
|
|
1553
|
-
// │ ┌──────────V──────────┐
|
|
1554
|
-
// │ │ contentInsertion │ (The default handler of the clipboard pipeline).
|
|
1555
|
-
// │ └──────────┬──────────┘
|
|
1556
|
-
// │ │
|
|
1557
|
-
// │ ┌──────────V──────────┐
|
|
1558
|
-
// │ │ contentInsertion │ Removes the content from the original range if the insertion was successful.
|
|
1559
|
-
// │ └──────────┬──────────┘
|
|
1560
|
-
// │ │
|
|
1561
|
-
// └─────────────┐
|
|
1562
|
-
// │
|
|
1563
|
-
// ┌─────────V────────┐
|
|
1564
|
-
// │ dragend │ Removes the drop marker and cleans the state.
|
|
1565
|
-
// └──────────────────┘
|
|
1566
|
-
//
|
|
1567
1221
|
/**
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
/**
|
|
1575
|
-
* The live range over the original content that is being dragged.
|
|
1576
|
-
*/ _draggedRange;
|
|
1577
|
-
/**
|
|
1578
|
-
* The UID of current dragging that is used to verify if the drop started in the same editor as the drag start.
|
|
1579
|
-
*
|
|
1580
|
-
* **Note**: This is a workaround for broken 'dragend' events (they are not fired if the source text node got removed).
|
|
1581
|
-
*/ _draggingUid;
|
|
1582
|
-
/**
|
|
1583
|
-
* The reference to the model element that currently has a `draggable` attribute set (it is set while dragging).
|
|
1584
|
-
*/ _draggableElement;
|
|
1585
|
-
/**
|
|
1586
|
-
* A delayed callback removing draggable attributes.
|
|
1587
|
-
*/ _clearDraggableAttributesDelayed = delay(()=>this._clearDraggableAttributes(), 40);
|
|
1588
|
-
/**
|
|
1589
|
-
* Whether the dragged content can be dropped only in block context.
|
|
1590
|
-
*/ // TODO handle drag from other editor instance
|
|
1591
|
-
// TODO configure to use block, inline or both
|
|
1592
|
-
_blockMode = false;
|
|
1593
|
-
/**
|
|
1594
|
-
* DOM Emitter.
|
|
1595
|
-
*/ _domEmitter = new (DomEmitterMixin())();
|
|
1596
|
-
/**
|
|
1597
|
-
* The DOM element used to generate dragged preview image.
|
|
1598
|
-
*/ _previewContainer;
|
|
1599
|
-
/**
|
|
1600
|
-
* @inheritDoc
|
|
1601
|
-
*/ static get pluginName() {
|
|
1602
|
-
return 'DragDrop';
|
|
1603
|
-
}
|
|
1604
|
-
/**
|
|
1605
|
-
* @inheritDoc
|
|
1606
|
-
*/ static get isOfficialPlugin() {
|
|
1607
|
-
return true;
|
|
1608
|
-
}
|
|
1609
|
-
/**
|
|
1610
|
-
* @inheritDoc
|
|
1611
|
-
*/ static get requires() {
|
|
1612
|
-
return [
|
|
1613
|
-
ClipboardPipeline,
|
|
1614
|
-
Widget,
|
|
1615
|
-
DragDropTarget,
|
|
1616
|
-
DragDropBlockToolbar
|
|
1617
|
-
];
|
|
1618
|
-
}
|
|
1619
|
-
/**
|
|
1620
|
-
* @inheritDoc
|
|
1621
|
-
*/ init() {
|
|
1622
|
-
const editor = this.editor;
|
|
1623
|
-
const view = editor.editing.view;
|
|
1624
|
-
this._draggedRange = null;
|
|
1625
|
-
this._draggingUid = '';
|
|
1626
|
-
this._draggableElement = null;
|
|
1627
|
-
view.addObserver(ClipboardObserver);
|
|
1628
|
-
view.addObserver(PointerObserver);
|
|
1629
|
-
this._setupDragging();
|
|
1630
|
-
this._setupContentInsertionIntegration();
|
|
1631
|
-
this._setupClipboardInputIntegration();
|
|
1632
|
-
this._setupDraggableAttributeHandling();
|
|
1633
|
-
this.listenTo(editor, 'change:isReadOnly', (evt, name, isReadOnly)=>{
|
|
1634
|
-
if (isReadOnly) {
|
|
1635
|
-
this.forceDisabled('readOnlyMode');
|
|
1636
|
-
} else {
|
|
1637
|
-
this.clearForceDisabled('readOnlyMode');
|
|
1638
|
-
}
|
|
1639
|
-
});
|
|
1640
|
-
this.on('change:isEnabled', (evt, name, isEnabled)=>{
|
|
1641
|
-
if (!isEnabled) {
|
|
1642
|
-
this._finalizeDragging(false);
|
|
1643
|
-
}
|
|
1644
|
-
});
|
|
1645
|
-
if (env.isAndroid) {
|
|
1646
|
-
this.forceDisabled('noAndroidSupport');
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
/**
|
|
1650
|
-
* @inheritDoc
|
|
1651
|
-
*/ destroy() {
|
|
1652
|
-
if (this._draggedRange) {
|
|
1653
|
-
this._draggedRange.detach();
|
|
1654
|
-
this._draggedRange = null;
|
|
1655
|
-
}
|
|
1656
|
-
if (this._previewContainer) {
|
|
1657
|
-
this._previewContainer.remove();
|
|
1658
|
-
}
|
|
1659
|
-
this._domEmitter.stopListening();
|
|
1660
|
-
this._clearDraggableAttributesDelayed.cancel();
|
|
1661
|
-
return super.destroy();
|
|
1662
|
-
}
|
|
1663
|
-
/**
|
|
1664
|
-
* Drag and drop events handling.
|
|
1665
|
-
*/ _setupDragging() {
|
|
1666
|
-
const editor = this.editor;
|
|
1667
|
-
const model = editor.model;
|
|
1668
|
-
const view = editor.editing.view;
|
|
1669
|
-
const viewDocument = view.document;
|
|
1670
|
-
const dragDropTarget = editor.plugins.get(DragDropTarget);
|
|
1671
|
-
// The handler for the drag start; it is responsible for setting data transfer object.
|
|
1672
|
-
this.listenTo(viewDocument, 'dragstart', (evt, data)=>{
|
|
1673
|
-
// Don't drag the editable element itself.
|
|
1674
|
-
if (data.target?.is('editableElement')) {
|
|
1675
|
-
data.preventDefault();
|
|
1676
|
-
return;
|
|
1677
|
-
}
|
|
1678
|
-
this._prepareDraggedRange(data.target);
|
|
1679
|
-
if (!this._draggedRange) {
|
|
1680
|
-
data.preventDefault();
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
|
-
this._draggingUid = uid();
|
|
1684
|
-
const canEditAtDraggedRange = this.isEnabled && editor.model.canEditAt(this._draggedRange);
|
|
1685
|
-
data.dataTransfer.effectAllowed = canEditAtDraggedRange ? 'copyMove' : 'copy';
|
|
1686
|
-
data.dataTransfer.setData('application/ckeditor5-dragging-uid', this._draggingUid);
|
|
1687
|
-
const draggedSelection = model.createSelection(this._draggedRange.toRange());
|
|
1688
|
-
const clipboardPipeline = this.editor.plugins.get('ClipboardPipeline');
|
|
1689
|
-
clipboardPipeline._fireOutputTransformationEvent(data.dataTransfer, draggedSelection, 'dragstart');
|
|
1690
|
-
const { dataTransfer, domTarget, domEvent } = data;
|
|
1691
|
-
const { clientX } = domEvent;
|
|
1692
|
-
this._updatePreview({
|
|
1693
|
-
dataTransfer,
|
|
1694
|
-
domTarget,
|
|
1695
|
-
clientX
|
|
1696
|
-
});
|
|
1697
|
-
data.stopPropagation();
|
|
1698
|
-
if (!canEditAtDraggedRange) {
|
|
1699
|
-
this._draggedRange.detach();
|
|
1700
|
-
this._draggedRange = null;
|
|
1701
|
-
this._draggingUid = '';
|
|
1702
|
-
}
|
|
1703
|
-
}, {
|
|
1704
|
-
priority: 'low'
|
|
1705
|
-
});
|
|
1706
|
-
// The handler for finalizing drag and drop. It should always be triggered after dragging completes
|
|
1707
|
-
// even if it was completed in a different application.
|
|
1708
|
-
// Note: This is not fired if source text node got removed while downcasting a marker.
|
|
1709
|
-
this.listenTo(viewDocument, 'dragend', (evt, data)=>{
|
|
1710
|
-
this._finalizeDragging(!data.dataTransfer.isCanceled && data.dataTransfer.dropEffect == 'move');
|
|
1711
|
-
}, {
|
|
1712
|
-
priority: 'low'
|
|
1713
|
-
});
|
|
1714
|
-
// Reset block dragging mode even if dropped outside the editable.
|
|
1715
|
-
this._domEmitter.listenTo(global.document, 'dragend', ()=>{
|
|
1716
|
-
this._blockMode = false;
|
|
1717
|
-
}, {
|
|
1718
|
-
useCapture: true
|
|
1719
|
-
});
|
|
1720
|
-
// Dragging over the editable.
|
|
1721
|
-
this.listenTo(viewDocument, 'dragenter', ()=>{
|
|
1722
|
-
if (!this.isEnabled) {
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
view.focus();
|
|
1726
|
-
});
|
|
1727
|
-
// Dragging out of the editable.
|
|
1728
|
-
this.listenTo(viewDocument, 'dragleave', ()=>{
|
|
1729
|
-
// We do not know if the mouse left the editor or just some element in it, so let us wait a few milliseconds
|
|
1730
|
-
// to check if 'dragover' is not fired.
|
|
1731
|
-
dragDropTarget.removeDropMarkerDelayed();
|
|
1732
|
-
});
|
|
1733
|
-
// Handler for moving dragged content over the target area.
|
|
1734
|
-
this.listenTo(viewDocument, 'dragging', (evt, data)=>{
|
|
1735
|
-
if (!this.isEnabled) {
|
|
1736
|
-
data.dataTransfer.dropEffect = 'none';
|
|
1737
|
-
return;
|
|
1738
|
-
}
|
|
1739
|
-
const { clientX, clientY } = data.domEvent;
|
|
1740
|
-
const targetRange = dragDropTarget.updateDropMarker(data.target, data.targetRanges, clientX, clientY, this._blockMode, this._draggedRange);
|
|
1741
|
-
// Do not allow dropping if there is no valid target range (e.g., dropping on non-editable place).
|
|
1742
|
-
if (!targetRange) {
|
|
1743
|
-
data.dataTransfer.dropEffect = 'none';
|
|
1744
|
-
return;
|
|
1745
|
-
}
|
|
1746
|
-
// If this is content being dragged from another editor, moving out of current editor instance
|
|
1747
|
-
// is not possible until 'dragend' event case will be fixed.
|
|
1748
|
-
if (!this._draggedRange) {
|
|
1749
|
-
data.dataTransfer.dropEffect = 'copy';
|
|
1750
|
-
}
|
|
1751
|
-
// In Firefox it is already set and effect allowed remains the same as originally set.
|
|
1752
|
-
if (!env.isGecko) {
|
|
1753
|
-
if (data.dataTransfer.effectAllowed == 'copy') {
|
|
1754
|
-
data.dataTransfer.dropEffect = 'copy';
|
|
1755
|
-
} else if ([
|
|
1756
|
-
'all',
|
|
1757
|
-
'copyMove'
|
|
1758
|
-
].includes(data.dataTransfer.effectAllowed)) {
|
|
1759
|
-
data.dataTransfer.dropEffect = 'move';
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
evt.stop();
|
|
1763
|
-
}, {
|
|
1764
|
-
priority: 'low'
|
|
1765
|
-
});
|
|
1766
|
-
}
|
|
1767
|
-
/**
|
|
1768
|
-
* Integration with the `clipboardInput` event.
|
|
1769
|
-
*/ _setupClipboardInputIntegration() {
|
|
1770
|
-
const editor = this.editor;
|
|
1771
|
-
const view = editor.editing.view;
|
|
1772
|
-
const viewDocument = view.document;
|
|
1773
|
-
const dragDropTarget = editor.plugins.get(DragDropTarget);
|
|
1774
|
-
// Update the event target ranges and abort dropping if dropping over itself.
|
|
1775
|
-
this.listenTo(viewDocument, 'clipboardInput', (evt, data)=>{
|
|
1776
|
-
if (data.method != 'drop') {
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
|
-
const { clientX, clientY } = data.domEvent;
|
|
1780
|
-
const targetRange = dragDropTarget.getFinalDropRange(data.target, data.targetRanges, clientX, clientY, this._blockMode, this._draggedRange);
|
|
1781
|
-
if (!targetRange) {
|
|
1782
|
-
this._finalizeDragging(false);
|
|
1783
|
-
evt.stop();
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
1786
|
-
// Since we cannot rely on the drag end event, we must check if the local drag range is from the current drag and drop
|
|
1787
|
-
// or it is from some previous not cleared one.
|
|
1788
|
-
if (this._draggedRange && this._draggingUid != data.dataTransfer.getData('application/ckeditor5-dragging-uid')) {
|
|
1789
|
-
this._draggedRange.detach();
|
|
1790
|
-
this._draggedRange = null;
|
|
1791
|
-
this._draggingUid = '';
|
|
1792
|
-
}
|
|
1793
|
-
// Do not do anything if some content was dragged within the same document to the same position.
|
|
1794
|
-
const isMove = getFinalDropEffect(data.dataTransfer) == 'move';
|
|
1795
|
-
if (isMove && this._draggedRange && this._draggedRange.containsRange(targetRange, true)) {
|
|
1796
|
-
this._finalizeDragging(false);
|
|
1797
|
-
evt.stop();
|
|
1798
|
-
return;
|
|
1799
|
-
}
|
|
1800
|
-
// Override the target ranges with the one adjusted to the best one for a drop.
|
|
1801
|
-
data.targetRanges = [
|
|
1802
|
-
editor.editing.mapper.toViewRange(targetRange)
|
|
1803
|
-
];
|
|
1804
|
-
}, {
|
|
1805
|
-
priority: 'high'
|
|
1806
|
-
});
|
|
1807
|
-
}
|
|
1808
|
-
/**
|
|
1809
|
-
* Integration with the `contentInsertion` event of the clipboard pipeline.
|
|
1810
|
-
*/ _setupContentInsertionIntegration() {
|
|
1811
|
-
const clipboardPipeline = this.editor.plugins.get(ClipboardPipeline);
|
|
1812
|
-
clipboardPipeline.on('contentInsertion', (evt, data)=>{
|
|
1813
|
-
if (!this.isEnabled || data.method !== 'drop') {
|
|
1814
|
-
return;
|
|
1815
|
-
}
|
|
1816
|
-
// Update the selection to the target range in the same change block to avoid selection post-fixing
|
|
1817
|
-
// and to be able to clone text attributes for plain text dropping.
|
|
1818
|
-
const ranges = data.targetRanges.map((viewRange)=>this.editor.editing.mapper.toModelRange(viewRange));
|
|
1819
|
-
this.editor.model.change((writer)=>writer.setSelection(ranges));
|
|
1820
|
-
}, {
|
|
1821
|
-
priority: 'high'
|
|
1822
|
-
});
|
|
1823
|
-
clipboardPipeline.on('contentInsertion', (evt, data)=>{
|
|
1824
|
-
if (!this.isEnabled || data.method !== 'drop') {
|
|
1825
|
-
return;
|
|
1826
|
-
}
|
|
1827
|
-
// Remove dragged range content, remove markers, clean after dragging.
|
|
1828
|
-
const isMove = getFinalDropEffect(data.dataTransfer) == 'move';
|
|
1829
|
-
// Whether any content was inserted (insertion might fail if the schema is disallowing some elements
|
|
1830
|
-
// (for example an image caption allows only the content of a block but not blocks themselves.
|
|
1831
|
-
// Some integrations might not return valid range (i.e., table pasting).
|
|
1832
|
-
const isSuccess = !data.resultRange || !data.resultRange.isCollapsed;
|
|
1833
|
-
this._finalizeDragging(isSuccess && isMove);
|
|
1834
|
-
}, {
|
|
1835
|
-
priority: 'lowest'
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
/**
|
|
1839
|
-
* Adds listeners that add the `draggable` attribute to the elements while the mouse button is down so the dragging could start.
|
|
1840
|
-
*/ _setupDraggableAttributeHandling() {
|
|
1841
|
-
const editor = this.editor;
|
|
1842
|
-
const view = editor.editing.view;
|
|
1843
|
-
const viewDocument = view.document;
|
|
1844
|
-
// Add the 'draggable' attribute to the widget while pressing the selection handle.
|
|
1845
|
-
// This is required for widgets to be draggable. In Chrome it will enable dragging text nodes.
|
|
1846
|
-
this.listenTo(viewDocument, 'pointerdown', (evt, data)=>{
|
|
1847
|
-
// The lack of data can be caused by editor tests firing fake mouse events. This should not occur
|
|
1848
|
-
// in real-life scenarios but this greatly simplifies editor tests that would otherwise fail a lot.
|
|
1849
|
-
if (env.isAndroid || !data) {
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
1852
|
-
this._clearDraggableAttributesDelayed.cancel();
|
|
1853
|
-
// Check if this is a mousedown over the widget (but not a nested editable).
|
|
1854
|
-
let draggableElement = findDraggableWidget(data.target);
|
|
1855
|
-
// Note: There is a limitation that if more than a widget is selected (a widget and some text)
|
|
1856
|
-
// and dragging starts on the widget, then only the widget is dragged.
|
|
1857
|
-
// If this was not a widget then we should check if we need to drag some text content.
|
|
1858
|
-
// In Chrome set a 'draggable' attribute on closest editable to allow immediate dragging of the selected text range.
|
|
1859
|
-
// In Firefox this is not needed. In Safari it makes the whole editable draggable (not just textual content).
|
|
1860
|
-
// Disabled in read-only mode because draggable="true" + contenteditable="false" results
|
|
1861
|
-
// in not firing selectionchange event ever, which makes the selection stuck in read-only mode.
|
|
1862
|
-
if (env.isBlink && !editor.isReadOnly && !draggableElement && !viewDocument.selection.isCollapsed) {
|
|
1863
|
-
const selectedElement = viewDocument.selection.getSelectedElement();
|
|
1864
|
-
if (!selectedElement || !isWidget(selectedElement)) {
|
|
1865
|
-
draggableElement = viewDocument.selection.editableElement;
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
if (draggableElement) {
|
|
1869
|
-
view.change((writer)=>{
|
|
1870
|
-
writer.setAttribute('draggable', 'true', draggableElement);
|
|
1871
|
-
});
|
|
1872
|
-
// Keep the reference to the model element in case the view element gets removed while dragging.
|
|
1873
|
-
this._draggableElement = editor.editing.mapper.toModelElement(draggableElement);
|
|
1874
|
-
}
|
|
1875
|
-
});
|
|
1876
|
-
// Remove the draggable attribute in case no dragging started (only mousedown + mouseup).
|
|
1877
|
-
this.listenTo(viewDocument, 'pointerup', ()=>{
|
|
1878
|
-
if (!env.isAndroid) {
|
|
1879
|
-
this._clearDraggableAttributesDelayed();
|
|
1880
|
-
}
|
|
1881
|
-
});
|
|
1882
|
-
}
|
|
1883
|
-
/**
|
|
1884
|
-
* Removes the `draggable` attribute from the element that was used for dragging.
|
|
1885
|
-
*/ _clearDraggableAttributes() {
|
|
1886
|
-
const editing = this.editor.editing;
|
|
1887
|
-
editing.view.change((writer)=>{
|
|
1888
|
-
// Remove 'draggable' attribute.
|
|
1889
|
-
if (this._draggableElement && this._draggableElement.root.rootName != '$graveyard') {
|
|
1890
|
-
writer.removeAttribute('draggable', editing.mapper.toViewElement(this._draggableElement));
|
|
1891
|
-
}
|
|
1892
|
-
this._draggableElement = null;
|
|
1893
|
-
});
|
|
1894
|
-
}
|
|
1895
|
-
/**
|
|
1896
|
-
* Deletes the dragged content from its original range and clears the dragging state.
|
|
1897
|
-
*
|
|
1898
|
-
* @param moved Whether the move succeeded.
|
|
1899
|
-
*/ _finalizeDragging(moved) {
|
|
1900
|
-
const editor = this.editor;
|
|
1901
|
-
const model = editor.model;
|
|
1902
|
-
const dragDropTarget = editor.plugins.get(DragDropTarget);
|
|
1903
|
-
dragDropTarget.removeDropMarker();
|
|
1904
|
-
this._clearDraggableAttributes();
|
|
1905
|
-
if (editor.plugins.has('WidgetToolbarRepository')) {
|
|
1906
|
-
const widgetToolbarRepository = editor.plugins.get('WidgetToolbarRepository');
|
|
1907
|
-
widgetToolbarRepository.clearForceDisabled('dragDrop');
|
|
1908
|
-
}
|
|
1909
|
-
this._draggingUid = '';
|
|
1910
|
-
if (this._previewContainer) {
|
|
1911
|
-
this._previewContainer.remove();
|
|
1912
|
-
this._previewContainer = undefined;
|
|
1913
|
-
}
|
|
1914
|
-
if (!this._draggedRange) {
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
// Delete moved content.
|
|
1918
|
-
if (moved && this.isEnabled) {
|
|
1919
|
-
model.change((writer)=>{
|
|
1920
|
-
const selection = model.createSelection(this._draggedRange);
|
|
1921
|
-
model.deleteContent(selection, {
|
|
1922
|
-
doNotAutoparagraph: true
|
|
1923
|
-
});
|
|
1924
|
-
// Check result selection if it does not require auto-paragraphing of empty container.
|
|
1925
|
-
const selectionParent = selection.getFirstPosition().parent;
|
|
1926
|
-
if (selectionParent.isEmpty && !model.schema.checkChild(selectionParent, '$text') && model.schema.checkChild(selectionParent, 'paragraph')) {
|
|
1927
|
-
writer.insertElement('paragraph', selectionParent, 0);
|
|
1928
|
-
}
|
|
1929
|
-
});
|
|
1930
|
-
}
|
|
1931
|
-
this._draggedRange.detach();
|
|
1932
|
-
this._draggedRange = null;
|
|
1933
|
-
}
|
|
1934
|
-
/**
|
|
1935
|
-
* Sets the dragged source range based on event target and document selection.
|
|
1936
|
-
*/ _prepareDraggedRange(target) {
|
|
1937
|
-
const editor = this.editor;
|
|
1938
|
-
const model = editor.model;
|
|
1939
|
-
const selection = model.document.selection;
|
|
1940
|
-
// Check if this is dragstart over the widget (but not a nested editable).
|
|
1941
|
-
const draggableWidget = target ? findDraggableWidget(target) : null;
|
|
1942
|
-
if (draggableWidget) {
|
|
1943
|
-
const modelElement = editor.editing.mapper.toModelElement(draggableWidget);
|
|
1944
|
-
this._draggedRange = ModelLiveRange.fromRange(model.createRangeOn(modelElement));
|
|
1945
|
-
this._blockMode = model.schema.isBlock(modelElement);
|
|
1946
|
-
// Disable toolbars so they won't obscure the drop area.
|
|
1947
|
-
if (editor.plugins.has('WidgetToolbarRepository')) {
|
|
1948
|
-
const widgetToolbarRepository = editor.plugins.get('WidgetToolbarRepository');
|
|
1949
|
-
widgetToolbarRepository.forceDisabled('dragDrop');
|
|
1950
|
-
}
|
|
1951
|
-
return;
|
|
1952
|
-
}
|
|
1953
|
-
// If this was not a widget we should check if we need to drag some text content.
|
|
1954
|
-
if (selection.isCollapsed && !selection.getFirstPosition().parent.isEmpty) {
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
const blocks = Array.from(selection.getSelectedBlocks());
|
|
1958
|
-
const draggedRange = selection.getFirstRange();
|
|
1959
|
-
if (blocks.length == 0) {
|
|
1960
|
-
this._draggedRange = ModelLiveRange.fromRange(draggedRange);
|
|
1961
|
-
return;
|
|
1962
|
-
}
|
|
1963
|
-
const blockRange = getRangeIncludingFullySelectedParents(model, blocks);
|
|
1964
|
-
if (blocks.length > 1) {
|
|
1965
|
-
this._draggedRange = ModelLiveRange.fromRange(blockRange);
|
|
1966
|
-
this._blockMode = true;
|
|
1967
|
-
// TODO block mode for dragging from outside editor? or inline? or both?
|
|
1968
|
-
} else if (blocks.length == 1) {
|
|
1969
|
-
const touchesBlockEdges = draggedRange.start.isTouching(blockRange.start) && draggedRange.end.isTouching(blockRange.end);
|
|
1970
|
-
this._draggedRange = ModelLiveRange.fromRange(touchesBlockEdges ? blockRange : draggedRange);
|
|
1971
|
-
this._blockMode = touchesBlockEdges;
|
|
1972
|
-
}
|
|
1973
|
-
model.change((writer)=>writer.setSelection(this._draggedRange.toRange()));
|
|
1974
|
-
}
|
|
1975
|
-
/**
|
|
1976
|
-
* Updates the dragged preview image.
|
|
1977
|
-
*/ _updatePreview({ dataTransfer, domTarget, clientX }) {
|
|
1978
|
-
const view = this.editor.editing.view;
|
|
1979
|
-
const editable = view.document.selection.editableElement;
|
|
1980
|
-
const domEditable = view.domConverter.mapViewToDom(editable);
|
|
1981
|
-
const computedStyle = global.window.getComputedStyle(domEditable);
|
|
1982
|
-
if (!this._previewContainer) {
|
|
1983
|
-
this._previewContainer = createElement(global.document, 'div', {
|
|
1984
|
-
style: 'position: fixed; left: -999999px;'
|
|
1985
|
-
});
|
|
1986
|
-
global.document.body.appendChild(this._previewContainer);
|
|
1987
|
-
} else if (this._previewContainer.firstElementChild) {
|
|
1988
|
-
this._previewContainer.removeChild(this._previewContainer.firstElementChild);
|
|
1989
|
-
}
|
|
1990
|
-
const preview = createElement(global.document, 'div');
|
|
1991
|
-
preview.className = 'ck ck-content ck-clipboard-preview';
|
|
1992
|
-
const domRect = new Rect(domEditable);
|
|
1993
|
-
const domEditablePaddingLeft = parseFloat(computedStyle.paddingLeft);
|
|
1994
|
-
const domEditablePaddingRight = parseFloat(computedStyle.paddingRight);
|
|
1995
|
-
const editableWidth = parseFloat(computedStyle.width) - domEditablePaddingLeft - domEditablePaddingRight;
|
|
1996
|
-
// Dragging by the drag handle outside editable element.
|
|
1997
|
-
if (!domEditable.contains(domTarget)) {
|
|
1998
|
-
if (!env.isiOS) {
|
|
1999
|
-
const offsetLeft = domRect.left - clientX + domEditablePaddingLeft;
|
|
2000
|
-
preview.style.width = `${editableWidth + offsetLeft}px`;
|
|
2001
|
-
preview.style.paddingLeft = `${offsetLeft}px`;
|
|
2002
|
-
} else {
|
|
2003
|
-
// Without a padding as on iOS preview have a background and padding would be visible.
|
|
2004
|
-
preview.style.width = `${editableWidth}px`;
|
|
2005
|
-
preview.style.backgroundColor = 'var(--ck-color-base-background)';
|
|
2006
|
-
}
|
|
2007
|
-
} else if (env.isiOS) {
|
|
2008
|
-
// Custom preview for iOS. Note that it must have some dimensions for iOS to start dragging element.
|
|
2009
|
-
preview.style.maxWidth = `${editableWidth}px`;
|
|
2010
|
-
preview.style.padding = '10px';
|
|
2011
|
-
preview.style.minWidth = '200px';
|
|
2012
|
-
preview.style.minHeight = '20px';
|
|
2013
|
-
preview.style.boxSizing = 'border-box';
|
|
2014
|
-
preview.style.backgroundColor = 'var(--ck-color-base-background)';
|
|
2015
|
-
} else {
|
|
2016
|
-
// If domTarget is inside the editable root, browsers will display the preview correctly by themselves.
|
|
2017
|
-
return;
|
|
2018
|
-
}
|
|
2019
|
-
view.domConverter.setContentOf(preview, dataTransfer.getData('text/html'));
|
|
2020
|
-
dataTransfer.setDragImage(preview, 0, 0);
|
|
2021
|
-
this._previewContainer.appendChild(preview);
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
1222
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1223
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1224
|
+
*/
|
|
1225
|
+
/**
|
|
1226
|
+
* @module clipboard/dragdrop
|
|
1227
|
+
*/
|
|
2024
1228
|
/**
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
1229
|
+
* The drag and drop feature. It works on top of the {@link module:clipboard/clipboardpipeline~ClipboardPipeline}.
|
|
1230
|
+
*
|
|
1231
|
+
* Read more about the clipboard integration in the {@glink framework/deep-dive/clipboard clipboard deep-dive} guide.
|
|
1232
|
+
*
|
|
1233
|
+
* @internal
|
|
1234
|
+
*/
|
|
1235
|
+
var DragDrop = class extends Plugin {
|
|
1236
|
+
/**
|
|
1237
|
+
* The live range over the original content that is being dragged.
|
|
1238
|
+
*/
|
|
1239
|
+
_draggedRange;
|
|
1240
|
+
/**
|
|
1241
|
+
* The UID of current dragging that is used to verify if the drop started in the same editor as the drag start.
|
|
1242
|
+
*
|
|
1243
|
+
* **Note**: This is a workaround for broken 'dragend' events (they are not fired if the source text node got removed).
|
|
1244
|
+
*/
|
|
1245
|
+
_draggingUid;
|
|
1246
|
+
/**
|
|
1247
|
+
* The reference to the model element that currently has a `draggable` attribute set (it is set while dragging).
|
|
1248
|
+
*/
|
|
1249
|
+
_draggableElement;
|
|
1250
|
+
/**
|
|
1251
|
+
* A delayed callback removing draggable attributes.
|
|
1252
|
+
*/
|
|
1253
|
+
_clearDraggableAttributesDelayed = delay(() => this._clearDraggableAttributes(), 40);
|
|
1254
|
+
/**
|
|
1255
|
+
* Whether the dragged content can be dropped only in block context.
|
|
1256
|
+
*/
|
|
1257
|
+
_blockMode = false;
|
|
1258
|
+
/**
|
|
1259
|
+
* DOM Emitter.
|
|
1260
|
+
*/
|
|
1261
|
+
_domEmitter = new (DomEmitterMixin())();
|
|
1262
|
+
/**
|
|
1263
|
+
* The DOM element used to generate dragged preview image.
|
|
1264
|
+
*/
|
|
1265
|
+
_previewContainer;
|
|
1266
|
+
/**
|
|
1267
|
+
* @inheritDoc
|
|
1268
|
+
*/
|
|
1269
|
+
static get pluginName() {
|
|
1270
|
+
return "DragDrop";
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* @inheritDoc
|
|
1274
|
+
*/
|
|
1275
|
+
static get isOfficialPlugin() {
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* @inheritDoc
|
|
1280
|
+
*/
|
|
1281
|
+
static get requires() {
|
|
1282
|
+
return [
|
|
1283
|
+
ClipboardPipeline,
|
|
1284
|
+
Widget,
|
|
1285
|
+
DragDropTarget,
|
|
1286
|
+
DragDropBlockToolbar
|
|
1287
|
+
];
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* @inheritDoc
|
|
1291
|
+
*/
|
|
1292
|
+
init() {
|
|
1293
|
+
const editor = this.editor;
|
|
1294
|
+
const view = editor.editing.view;
|
|
1295
|
+
this._draggedRange = null;
|
|
1296
|
+
this._draggingUid = "";
|
|
1297
|
+
this._draggableElement = null;
|
|
1298
|
+
view.addObserver(ClipboardObserver);
|
|
1299
|
+
view.addObserver(PointerObserver);
|
|
1300
|
+
this._setupDragging();
|
|
1301
|
+
this._setupContentInsertionIntegration();
|
|
1302
|
+
this._setupClipboardInputIntegration();
|
|
1303
|
+
this._setupDraggableAttributeHandling();
|
|
1304
|
+
this.listenTo(editor, "change:isReadOnly", (evt, name, isReadOnly) => {
|
|
1305
|
+
if (isReadOnly) this.forceDisabled("readOnlyMode");
|
|
1306
|
+
else this.clearForceDisabled("readOnlyMode");
|
|
1307
|
+
});
|
|
1308
|
+
this.on("change:isEnabled", (evt, name, isEnabled) => {
|
|
1309
|
+
if (!isEnabled) this._finalizeDragging(false);
|
|
1310
|
+
});
|
|
1311
|
+
if (env.isAndroid) this.forceDisabled("noAndroidSupport");
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* @inheritDoc
|
|
1315
|
+
*/
|
|
1316
|
+
destroy() {
|
|
1317
|
+
if (this._draggedRange) {
|
|
1318
|
+
this._draggedRange.detach();
|
|
1319
|
+
this._draggedRange = null;
|
|
1320
|
+
}
|
|
1321
|
+
if (this._previewContainer) this._previewContainer.remove();
|
|
1322
|
+
this._domEmitter.stopListening();
|
|
1323
|
+
this._clearDraggableAttributesDelayed.cancel();
|
|
1324
|
+
return super.destroy();
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Drag and drop events handling.
|
|
1328
|
+
*/
|
|
1329
|
+
_setupDragging() {
|
|
1330
|
+
const editor = this.editor;
|
|
1331
|
+
const model = editor.model;
|
|
1332
|
+
const view = editor.editing.view;
|
|
1333
|
+
const viewDocument = view.document;
|
|
1334
|
+
const dragDropTarget = editor.plugins.get(DragDropTarget);
|
|
1335
|
+
this.listenTo(viewDocument, "dragstart", (evt, data) => {
|
|
1336
|
+
if (data.target?.is("editableElement")) {
|
|
1337
|
+
data.preventDefault();
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
this._prepareDraggedRange(data.target);
|
|
1341
|
+
if (!this._draggedRange) {
|
|
1342
|
+
data.preventDefault();
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
this._draggingUid = uid();
|
|
1346
|
+
const canEditAtDraggedRange = this.isEnabled && editor.model.canEditAt(this._draggedRange);
|
|
1347
|
+
data.dataTransfer.effectAllowed = canEditAtDraggedRange ? "copyMove" : "copy";
|
|
1348
|
+
data.dataTransfer.setData("application/ckeditor5-dragging-uid", this._draggingUid);
|
|
1349
|
+
const draggedSelection = model.createSelection(this._draggedRange.toRange());
|
|
1350
|
+
this.editor.plugins.get("ClipboardPipeline")._fireOutputTransformationEvent(data.dataTransfer, draggedSelection, "dragstart");
|
|
1351
|
+
const { dataTransfer, domTarget, domEvent } = data;
|
|
1352
|
+
const { clientX } = domEvent;
|
|
1353
|
+
this._updatePreview({
|
|
1354
|
+
dataTransfer,
|
|
1355
|
+
domTarget,
|
|
1356
|
+
clientX
|
|
1357
|
+
});
|
|
1358
|
+
data.stopPropagation();
|
|
1359
|
+
if (!canEditAtDraggedRange) {
|
|
1360
|
+
this._draggedRange.detach();
|
|
1361
|
+
this._draggedRange = null;
|
|
1362
|
+
this._draggingUid = "";
|
|
1363
|
+
}
|
|
1364
|
+
}, { priority: "low" });
|
|
1365
|
+
this.listenTo(viewDocument, "dragend", (evt, data) => {
|
|
1366
|
+
this._finalizeDragging(!data.dataTransfer.isCanceled && data.dataTransfer.dropEffect == "move");
|
|
1367
|
+
}, { priority: "low" });
|
|
1368
|
+
this._domEmitter.listenTo(global.document, "dragend", () => {
|
|
1369
|
+
this._blockMode = false;
|
|
1370
|
+
}, { useCapture: true });
|
|
1371
|
+
this.listenTo(viewDocument, "dragenter", () => {
|
|
1372
|
+
if (!this.isEnabled) return;
|
|
1373
|
+
view.focus();
|
|
1374
|
+
});
|
|
1375
|
+
this.listenTo(viewDocument, "dragleave", () => {
|
|
1376
|
+
dragDropTarget.removeDropMarkerDelayed();
|
|
1377
|
+
});
|
|
1378
|
+
this.listenTo(viewDocument, "dragging", (evt, data) => {
|
|
1379
|
+
if (!this.isEnabled) {
|
|
1380
|
+
data.dataTransfer.dropEffect = "none";
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
const { clientX, clientY } = data.domEvent;
|
|
1384
|
+
if (!dragDropTarget.updateDropMarker(data.target, data.targetRanges, clientX, clientY, this._blockMode, this._draggedRange)) {
|
|
1385
|
+
data.dataTransfer.dropEffect = "none";
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (!this._draggedRange) data.dataTransfer.dropEffect = "copy";
|
|
1389
|
+
if (!env.isGecko) {
|
|
1390
|
+
if (data.dataTransfer.effectAllowed == "copy") data.dataTransfer.dropEffect = "copy";
|
|
1391
|
+
else if (["all", "copyMove"].includes(data.dataTransfer.effectAllowed)) data.dataTransfer.dropEffect = "move";
|
|
1392
|
+
}
|
|
1393
|
+
evt.stop();
|
|
1394
|
+
}, { priority: "low" });
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Integration with the `clipboardInput` event.
|
|
1398
|
+
*/
|
|
1399
|
+
_setupClipboardInputIntegration() {
|
|
1400
|
+
const editor = this.editor;
|
|
1401
|
+
const viewDocument = editor.editing.view.document;
|
|
1402
|
+
const dragDropTarget = editor.plugins.get(DragDropTarget);
|
|
1403
|
+
this.listenTo(viewDocument, "clipboardInput", (evt, data) => {
|
|
1404
|
+
if (data.method != "drop") return;
|
|
1405
|
+
const { clientX, clientY } = data.domEvent;
|
|
1406
|
+
const targetRange = dragDropTarget.getFinalDropRange(data.target, data.targetRanges, clientX, clientY, this._blockMode, this._draggedRange);
|
|
1407
|
+
if (!targetRange) {
|
|
1408
|
+
this._finalizeDragging(false);
|
|
1409
|
+
evt.stop();
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
if (this._draggedRange && this._draggingUid != data.dataTransfer.getData("application/ckeditor5-dragging-uid")) {
|
|
1413
|
+
this._draggedRange.detach();
|
|
1414
|
+
this._draggedRange = null;
|
|
1415
|
+
this._draggingUid = "";
|
|
1416
|
+
}
|
|
1417
|
+
if (getFinalDropEffect(data.dataTransfer) == "move" && this._draggedRange && this._draggedRange.containsRange(targetRange, true)) {
|
|
1418
|
+
this._finalizeDragging(false);
|
|
1419
|
+
evt.stop();
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
data.targetRanges = [editor.editing.mapper.toViewRange(targetRange)];
|
|
1423
|
+
}, { priority: "high" });
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Integration with the `contentInsertion` event of the clipboard pipeline.
|
|
1427
|
+
*/
|
|
1428
|
+
_setupContentInsertionIntegration() {
|
|
1429
|
+
const clipboardPipeline = this.editor.plugins.get(ClipboardPipeline);
|
|
1430
|
+
clipboardPipeline.on("contentInsertion", (evt, data) => {
|
|
1431
|
+
if (!this.isEnabled || data.method !== "drop") return;
|
|
1432
|
+
const ranges = data.targetRanges.map((viewRange) => this.editor.editing.mapper.toModelRange(viewRange));
|
|
1433
|
+
this.editor.model.change((writer) => writer.setSelection(ranges));
|
|
1434
|
+
}, { priority: "high" });
|
|
1435
|
+
clipboardPipeline.on("contentInsertion", (evt, data) => {
|
|
1436
|
+
if (!this.isEnabled || data.method !== "drop") return;
|
|
1437
|
+
const isMove = getFinalDropEffect(data.dataTransfer) == "move";
|
|
1438
|
+
const isSuccess = !data.resultRange || !data.resultRange.isCollapsed;
|
|
1439
|
+
this._finalizeDragging(isSuccess && isMove);
|
|
1440
|
+
}, { priority: "lowest" });
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Adds listeners that add the `draggable` attribute to the elements while the mouse button is down so the dragging could start.
|
|
1444
|
+
*/
|
|
1445
|
+
_setupDraggableAttributeHandling() {
|
|
1446
|
+
const editor = this.editor;
|
|
1447
|
+
const view = editor.editing.view;
|
|
1448
|
+
const viewDocument = view.document;
|
|
1449
|
+
this.listenTo(viewDocument, "pointerdown", (evt, data) => {
|
|
1450
|
+
if (env.isAndroid || !data) return;
|
|
1451
|
+
this._clearDraggableAttributesDelayed.cancel();
|
|
1452
|
+
let draggableElement = findDraggableWidget(data.target);
|
|
1453
|
+
if (env.isBlink && !editor.isReadOnly && !draggableElement && !viewDocument.selection.isCollapsed) {
|
|
1454
|
+
const selectedElement = viewDocument.selection.getSelectedElement();
|
|
1455
|
+
if (!selectedElement || !isWidget(selectedElement)) draggableElement = viewDocument.selection.editableElement;
|
|
1456
|
+
}
|
|
1457
|
+
if (draggableElement) {
|
|
1458
|
+
view.change((writer) => {
|
|
1459
|
+
writer.setAttribute("draggable", "true", draggableElement);
|
|
1460
|
+
});
|
|
1461
|
+
this._draggableElement = editor.editing.mapper.toModelElement(draggableElement);
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
this.listenTo(viewDocument, "pointerup", () => {
|
|
1465
|
+
if (!env.isAndroid) this._clearDraggableAttributesDelayed();
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Removes the `draggable` attribute from the element that was used for dragging.
|
|
1470
|
+
*/
|
|
1471
|
+
_clearDraggableAttributes() {
|
|
1472
|
+
const editing = this.editor.editing;
|
|
1473
|
+
editing.view.change((writer) => {
|
|
1474
|
+
if (this._draggableElement && this._draggableElement.root.rootName != "$graveyard") writer.removeAttribute("draggable", editing.mapper.toViewElement(this._draggableElement));
|
|
1475
|
+
this._draggableElement = null;
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Deletes the dragged content from its original range and clears the dragging state.
|
|
1480
|
+
*
|
|
1481
|
+
* @param moved Whether the move succeeded.
|
|
1482
|
+
*/
|
|
1483
|
+
_finalizeDragging(moved) {
|
|
1484
|
+
const editor = this.editor;
|
|
1485
|
+
const model = editor.model;
|
|
1486
|
+
editor.plugins.get(DragDropTarget).removeDropMarker();
|
|
1487
|
+
this._clearDraggableAttributes();
|
|
1488
|
+
if (editor.plugins.has("WidgetToolbarRepository")) editor.plugins.get("WidgetToolbarRepository").clearForceDisabled("dragDrop");
|
|
1489
|
+
this._draggingUid = "";
|
|
1490
|
+
if (this._previewContainer) {
|
|
1491
|
+
this._previewContainer.remove();
|
|
1492
|
+
this._previewContainer = void 0;
|
|
1493
|
+
}
|
|
1494
|
+
if (!this._draggedRange) return;
|
|
1495
|
+
if (moved && this.isEnabled) model.change((writer) => {
|
|
1496
|
+
const selection = model.createSelection(this._draggedRange);
|
|
1497
|
+
model.deleteContent(selection, { doNotAutoparagraph: true });
|
|
1498
|
+
const selectionParent = selection.getFirstPosition().parent;
|
|
1499
|
+
if (selectionParent.isEmpty && !model.schema.checkChild(selectionParent, "$text") && model.schema.checkChild(selectionParent, "paragraph")) writer.insertElement("paragraph", selectionParent, 0);
|
|
1500
|
+
});
|
|
1501
|
+
this._draggedRange.detach();
|
|
1502
|
+
this._draggedRange = null;
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Sets the dragged source range based on event target and document selection.
|
|
1506
|
+
*/
|
|
1507
|
+
_prepareDraggedRange(target) {
|
|
1508
|
+
const editor = this.editor;
|
|
1509
|
+
const model = editor.model;
|
|
1510
|
+
const selection = model.document.selection;
|
|
1511
|
+
const draggableWidget = target ? findDraggableWidget(target) : null;
|
|
1512
|
+
if (draggableWidget) {
|
|
1513
|
+
const modelElement = editor.editing.mapper.toModelElement(draggableWidget);
|
|
1514
|
+
this._draggedRange = ModelLiveRange.fromRange(model.createRangeOn(modelElement));
|
|
1515
|
+
this._blockMode = model.schema.isBlock(modelElement);
|
|
1516
|
+
if (editor.plugins.has("WidgetToolbarRepository")) editor.plugins.get("WidgetToolbarRepository").forceDisabled("dragDrop");
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
if (selection.isCollapsed && !selection.getFirstPosition().parent.isEmpty) return;
|
|
1520
|
+
const blocks = Array.from(selection.getSelectedBlocks());
|
|
1521
|
+
const draggedRange = selection.getFirstRange();
|
|
1522
|
+
if (blocks.length == 0) {
|
|
1523
|
+
this._draggedRange = ModelLiveRange.fromRange(draggedRange);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
const blockRange = getRangeIncludingFullySelectedParents(model, blocks);
|
|
1527
|
+
if (blocks.length > 1) {
|
|
1528
|
+
this._draggedRange = ModelLiveRange.fromRange(blockRange);
|
|
1529
|
+
this._blockMode = true;
|
|
1530
|
+
} else if (blocks.length == 1) {
|
|
1531
|
+
const touchesBlockEdges = draggedRange.start.isTouching(blockRange.start) && draggedRange.end.isTouching(blockRange.end);
|
|
1532
|
+
this._draggedRange = ModelLiveRange.fromRange(touchesBlockEdges ? blockRange : draggedRange);
|
|
1533
|
+
this._blockMode = touchesBlockEdges;
|
|
1534
|
+
}
|
|
1535
|
+
model.change((writer) => writer.setSelection(this._draggedRange.toRange()));
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Updates the dragged preview image.
|
|
1539
|
+
*/
|
|
1540
|
+
_updatePreview({ dataTransfer, domTarget, clientX }) {
|
|
1541
|
+
const view = this.editor.editing.view;
|
|
1542
|
+
const editable = view.document.selection.editableElement;
|
|
1543
|
+
const domEditable = view.domConverter.mapViewToDom(editable);
|
|
1544
|
+
const computedStyle = global.window.getComputedStyle(domEditable);
|
|
1545
|
+
if (!this._previewContainer) {
|
|
1546
|
+
this._previewContainer = createElement(global.document, "div", { style: "position: fixed; left: -999999px;" });
|
|
1547
|
+
global.document.body.appendChild(this._previewContainer);
|
|
1548
|
+
} else if (this._previewContainer.firstElementChild) this._previewContainer.removeChild(this._previewContainer.firstElementChild);
|
|
1549
|
+
const preview = createElement(global.document, "div");
|
|
1550
|
+
preview.className = "ck ck-content ck-clipboard-preview";
|
|
1551
|
+
const domRect = new Rect(domEditable);
|
|
1552
|
+
const domEditablePaddingLeft = parseFloat(computedStyle.paddingLeft);
|
|
1553
|
+
const domEditablePaddingRight = parseFloat(computedStyle.paddingRight);
|
|
1554
|
+
const editableWidth = parseFloat(computedStyle.width) - domEditablePaddingLeft - domEditablePaddingRight;
|
|
1555
|
+
if (!domEditable.contains(domTarget)) if (!env.isiOS) {
|
|
1556
|
+
const offsetLeft = domRect.left - clientX + domEditablePaddingLeft;
|
|
1557
|
+
preview.style.width = `${editableWidth + offsetLeft}px`;
|
|
1558
|
+
preview.style.paddingLeft = `${offsetLeft}px`;
|
|
1559
|
+
} else {
|
|
1560
|
+
preview.style.width = `${editableWidth}px`;
|
|
1561
|
+
preview.style.backgroundColor = "var(--ck-color-base-background)";
|
|
1562
|
+
}
|
|
1563
|
+
else if (env.isiOS) {
|
|
1564
|
+
preview.style.maxWidth = `${editableWidth}px`;
|
|
1565
|
+
preview.style.padding = "10px";
|
|
1566
|
+
preview.style.minWidth = "200px";
|
|
1567
|
+
preview.style.minHeight = "20px";
|
|
1568
|
+
preview.style.boxSizing = "border-box";
|
|
1569
|
+
preview.style.backgroundColor = "var(--ck-color-base-background)";
|
|
1570
|
+
} else return;
|
|
1571
|
+
view.domConverter.setContentOf(preview, dataTransfer.getData("text/html"));
|
|
1572
|
+
dataTransfer.setDragImage(preview, 0, 0);
|
|
1573
|
+
this._previewContainer.appendChild(preview);
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
/**
|
|
1577
|
+
* Returns the drop effect that should be a result of dragging the content.
|
|
1578
|
+
* This function is handling a quirk when checking the effect in the 'drop' DOM event.
|
|
1579
|
+
*/
|
|
1580
|
+
function getFinalDropEffect(dataTransfer) {
|
|
1581
|
+
if (env.isGecko) return dataTransfer.dropEffect;
|
|
1582
|
+
return ["all", "copyMove"].includes(dataTransfer.effectAllowed) ? "move" : "copy";
|
|
2035
1583
|
}
|
|
2036
1584
|
/**
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
}
|
|
2047
|
-
// Direct hit on a widget.
|
|
2048
|
-
if (isWidget(target)) {
|
|
2049
|
-
return target;
|
|
2050
|
-
}
|
|
2051
|
-
// Find closest ancestor that is either a widget or an editable element...
|
|
2052
|
-
const ancestor = target.findAncestor((node)=>isWidget(node) || node.is('editableElement'));
|
|
2053
|
-
// ...and if closer was the widget then enable dragging it.
|
|
2054
|
-
if (isWidget(ancestor)) {
|
|
2055
|
-
return ancestor;
|
|
2056
|
-
}
|
|
2057
|
-
return null;
|
|
1585
|
+
* Returns a widget element that should be dragged.
|
|
1586
|
+
*/
|
|
1587
|
+
function findDraggableWidget(target) {
|
|
1588
|
+
if (target.is("editableElement")) return null;
|
|
1589
|
+
if (target.hasClass("ck-widget__selection-handle")) return target.findAncestor(isWidget);
|
|
1590
|
+
if (isWidget(target)) return target;
|
|
1591
|
+
const ancestor = target.findAncestor((node) => isWidget(node) || node.is("editableElement"));
|
|
1592
|
+
if (isWidget(ancestor)) return ancestor;
|
|
1593
|
+
return null;
|
|
2058
1594
|
}
|
|
2059
1595
|
/**
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
]);
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
return model.createRange(startPosition, endPosition);
|
|
1596
|
+
* Recursively checks if common parent of provided elements doesn't have any other children. If that's the case,
|
|
1597
|
+
* it returns range including this parent. Otherwise, it returns only the range from first to last element.
|
|
1598
|
+
*
|
|
1599
|
+
* Example:
|
|
1600
|
+
*
|
|
1601
|
+
* <blockQuote>
|
|
1602
|
+
* <paragraph>[Test 1</paragraph>
|
|
1603
|
+
* <paragraph>Test 2</paragraph>
|
|
1604
|
+
* <paragraph>Test 3]</paragraph>
|
|
1605
|
+
* <blockQuote>
|
|
1606
|
+
*
|
|
1607
|
+
* Because all elements inside the `blockQuote` are selected, the range is extended to include the `blockQuote` too.
|
|
1608
|
+
* If only first and second paragraphs would be selected, the range would not include it.
|
|
1609
|
+
*/
|
|
1610
|
+
function getRangeIncludingFullySelectedParents(model, elements) {
|
|
1611
|
+
const firstElement = elements[0];
|
|
1612
|
+
const lastElement = elements[elements.length - 1];
|
|
1613
|
+
const parent = firstElement.getCommonAncestor(lastElement);
|
|
1614
|
+
const startPosition = model.createPositionBefore(firstElement);
|
|
1615
|
+
const endPosition = model.createPositionAfter(lastElement);
|
|
1616
|
+
if (parent && parent.is("element") && !model.schema.isLimit(parent)) {
|
|
1617
|
+
const parentRange = model.createRangeOn(parent);
|
|
1618
|
+
const touchesStart = startPosition.isTouching(parentRange.start);
|
|
1619
|
+
const touchesEnd = endPosition.isTouching(parentRange.end);
|
|
1620
|
+
if (touchesStart && touchesEnd) return getRangeIncludingFullySelectedParents(model, [parent]);
|
|
1621
|
+
}
|
|
1622
|
+
return model.createRange(startPosition, endPosition);
|
|
2091
1623
|
}
|
|
2092
1624
|
|
|
2093
1625
|
/**
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
*/ static get pluginName() {
|
|
2101
|
-
return 'PastePlainText';
|
|
2102
|
-
}
|
|
2103
|
-
/**
|
|
2104
|
-
* @inheritDoc
|
|
2105
|
-
*/ static get isOfficialPlugin() {
|
|
2106
|
-
return true;
|
|
2107
|
-
}
|
|
2108
|
-
/**
|
|
2109
|
-
* @inheritDoc
|
|
2110
|
-
*/ static get requires() {
|
|
2111
|
-
return [
|
|
2112
|
-
ClipboardPipeline
|
|
2113
|
-
];
|
|
2114
|
-
}
|
|
2115
|
-
/**
|
|
2116
|
-
* @inheritDoc
|
|
2117
|
-
*/ init() {
|
|
2118
|
-
const editor = this.editor;
|
|
2119
|
-
const model = editor.model;
|
|
2120
|
-
const view = editor.editing.view;
|
|
2121
|
-
const selection = model.document.selection;
|
|
2122
|
-
view.addObserver(ClipboardObserver);
|
|
2123
|
-
editor.plugins.get(ClipboardPipeline).on('contentInsertion', (evt, data)=>{
|
|
2124
|
-
if (!isUnformattedInlineContent(data.content, model)) {
|
|
2125
|
-
return;
|
|
2126
|
-
}
|
|
2127
|
-
model.change((writer)=>{
|
|
2128
|
-
// Formatting attributes should be preserved.
|
|
2129
|
-
const textAttributes = Array.from(selection.getAttributes()).filter(([key])=>model.schema.getAttributeProperties(key).isFormatting);
|
|
2130
|
-
if (!selection.isCollapsed) {
|
|
2131
|
-
model.deleteContent(selection, {
|
|
2132
|
-
doNotAutoparagraph: true
|
|
2133
|
-
});
|
|
2134
|
-
}
|
|
2135
|
-
// Also preserve other attributes if they survived the content deletion (because they were not fully selected).
|
|
2136
|
-
// For example linkHref is not a formatting attribute but it should be preserved if pasted text was in the middle
|
|
2137
|
-
// of a link.
|
|
2138
|
-
textAttributes.push(...selection.getAttributes());
|
|
2139
|
-
const range = writer.createRangeIn(data.content);
|
|
2140
|
-
for (const item of range.getItems()){
|
|
2141
|
-
for (const attribute of textAttributes){
|
|
2142
|
-
if (model.schema.checkAttribute(item, attribute[0])) {
|
|
2143
|
-
writer.setAttribute(attribute[0], attribute[1], item);
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
});
|
|
2148
|
-
});
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
1626
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1627
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1628
|
+
*/
|
|
1629
|
+
/**
|
|
1630
|
+
* @module clipboard/pasteplaintext
|
|
1631
|
+
*/
|
|
2151
1632
|
/**
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
1633
|
+
* The plugin detects the user's intention to paste plain text.
|
|
1634
|
+
*
|
|
1635
|
+
* For example, it detects the <kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd> keystroke.
|
|
1636
|
+
*/
|
|
1637
|
+
var PastePlainText = class extends Plugin {
|
|
1638
|
+
/**
|
|
1639
|
+
* @inheritDoc
|
|
1640
|
+
*/
|
|
1641
|
+
static get pluginName() {
|
|
1642
|
+
return "PastePlainText";
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* @inheritDoc
|
|
1646
|
+
*/
|
|
1647
|
+
static get isOfficialPlugin() {
|
|
1648
|
+
return true;
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* @inheritDoc
|
|
1652
|
+
*/
|
|
1653
|
+
static get requires() {
|
|
1654
|
+
return [ClipboardPipeline];
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* @inheritDoc
|
|
1658
|
+
*/
|
|
1659
|
+
init() {
|
|
1660
|
+
const editor = this.editor;
|
|
1661
|
+
const model = editor.model;
|
|
1662
|
+
const view = editor.editing.view;
|
|
1663
|
+
const selection = model.document.selection;
|
|
1664
|
+
view.addObserver(ClipboardObserver);
|
|
1665
|
+
editor.plugins.get(ClipboardPipeline).on("contentInsertion", (evt, data) => {
|
|
1666
|
+
if (!isUnformattedInlineContent(data.content, model)) return;
|
|
1667
|
+
model.change((writer) => {
|
|
1668
|
+
const textAttributes = Array.from(selection.getAttributes()).filter(([key]) => model.schema.getAttributeProperties(key).isFormatting);
|
|
1669
|
+
if (!selection.isCollapsed) model.deleteContent(selection, { doNotAutoparagraph: true });
|
|
1670
|
+
textAttributes.push(...selection.getAttributes());
|
|
1671
|
+
const range = writer.createRangeIn(data.content);
|
|
1672
|
+
for (const item of range.getItems()) for (const attribute of textAttributes) if (model.schema.checkAttribute(item, attribute[0])) writer.setAttribute(attribute[0], attribute[1], item);
|
|
1673
|
+
});
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
/**
|
|
1678
|
+
* Returns true if specified `documentFragment` represents the unformatted inline content.
|
|
1679
|
+
*/
|
|
1680
|
+
function isUnformattedInlineContent(documentFragment, model) {
|
|
1681
|
+
let range = model.createRangeIn(documentFragment);
|
|
1682
|
+
if (documentFragment.childCount == 1) {
|
|
1683
|
+
const child = documentFragment.getChild(0);
|
|
1684
|
+
if (child.is("element") && model.schema.isBlock(child) && !model.schema.isObject(child) && !model.schema.isLimit(child)) range = model.createRangeIn(child);
|
|
1685
|
+
}
|
|
1686
|
+
for (const child of range.getItems()) {
|
|
1687
|
+
if (!model.schema.isInline(child)) return false;
|
|
1688
|
+
if (Array.from(child.getAttributeKeys()).find((key) => model.schema.getAttributeProperties(key).isFormatting)) return false;
|
|
1689
|
+
}
|
|
1690
|
+
return true;
|
|
2181
1691
|
}
|
|
2182
1692
|
|
|
2183
1693
|
/**
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
1694
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1695
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1696
|
+
*/
|
|
1697
|
+
/**
|
|
1698
|
+
* @module clipboard/clipboard
|
|
1699
|
+
*/
|
|
1700
|
+
/**
|
|
1701
|
+
* The clipboard feature.
|
|
1702
|
+
*
|
|
1703
|
+
* Read more about the clipboard integration in the {@glink framework/deep-dive/clipboard clipboard deep-dive} guide.
|
|
1704
|
+
*
|
|
1705
|
+
* This is a "glue" plugin which loads the following plugins:
|
|
1706
|
+
* * {@link module:clipboard/clipboardpipeline~ClipboardPipeline}
|
|
1707
|
+
* * {@link module:clipboard/dragdrop~DragDrop}
|
|
1708
|
+
* * {@link module:clipboard/pasteplaintext~PastePlainText}
|
|
1709
|
+
*/
|
|
1710
|
+
var Clipboard = class extends Plugin {
|
|
1711
|
+
/**
|
|
1712
|
+
* @inheritDoc
|
|
1713
|
+
*/
|
|
1714
|
+
static get pluginName() {
|
|
1715
|
+
return "Clipboard";
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* @inheritDoc
|
|
1719
|
+
*/
|
|
1720
|
+
static get isOfficialPlugin() {
|
|
1721
|
+
return true;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* @inheritDoc
|
|
1725
|
+
*/
|
|
1726
|
+
static get requires() {
|
|
1727
|
+
return [
|
|
1728
|
+
ClipboardMarkersUtils,
|
|
1729
|
+
ClipboardPipeline,
|
|
1730
|
+
DragDrop,
|
|
1731
|
+
PastePlainText
|
|
1732
|
+
];
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* @inheritDoc
|
|
1736
|
+
*/
|
|
1737
|
+
init() {
|
|
1738
|
+
const editor = this.editor;
|
|
1739
|
+
const t = this.editor.t;
|
|
1740
|
+
editor.accessibility.addKeystrokeInfos({ keystrokes: [
|
|
1741
|
+
{
|
|
1742
|
+
label: t("Copy selected content"),
|
|
1743
|
+
keystroke: "CTRL+C"
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
label: t("Paste content"),
|
|
1747
|
+
keystroke: "CTRL+V"
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
label: t("Paste content as plain text"),
|
|
1751
|
+
keystroke: "CTRL+SHIFT+V"
|
|
1752
|
+
}
|
|
1753
|
+
] });
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1759
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1760
|
+
*/
|
|
2237
1761
|
|
|
2238
|
-
export { Clipboard, ClipboardMarkersUtils, ClipboardObserver, ClipboardPipeline, DragDrop,
|
|
2239
|
-
//# sourceMappingURL=index.js.map
|
|
1762
|
+
export { Clipboard, ClipboardMarkersUtils, ClipboardObserver, ClipboardPipeline, DragDrop, DragDrop as _DragDrop, DragDropBlockToolbar, DragDropBlockToolbar as _DragDropBlockToolbar, DragDropTarget, DragDropTarget as _DragDropTarget, PastePlainText, LineView as _ClipboardLineView, normalizeClipboardData as _normalizeClipboardData, plainTextToHtml, viewToPlainText };
|
|
1763
|
+
//# sourceMappingURL=index.js.map
|