@ckeditor/ckeditor5-clipboard 0.0.0-nightly-next-20260105.0 → 0.0.0-nightly-next-20260106.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/{src → dist}/augmentation.d.ts +4 -0
- package/{src → dist}/clipboard.d.ts +4 -0
- package/{src → dist}/clipboardmarkersutils.d.ts +4 -0
- package/{src → dist}/clipboardobserver.d.ts +4 -0
- package/{src → dist}/clipboardpipeline.d.ts +4 -0
- package/{src → dist}/dragdrop.d.ts +4 -0
- package/{src → dist}/dragdropblocktoolbar.d.ts +4 -0
- package/{src → dist}/dragdroptarget.d.ts +4 -0
- package/{src → dist}/index.d.ts +4 -0
- package/{src → dist}/lineview.d.ts +4 -0
- package/{src → dist}/pasteplaintext.d.ts +4 -0
- package/{src → dist}/utils/normalizeclipboarddata.d.ts +4 -0
- package/{src → dist}/utils/plaintexttohtml.d.ts +4 -0
- package/{src → dist}/utils/viewtoplaintext.d.ts +4 -0
- package/package.json +22 -30
- package/src/augmentation.js +0 -5
- package/src/clipboard.js +0 -66
- package/src/clipboardmarkersutils.js +0 -500
- package/src/clipboardobserver.js +0 -75
- package/src/clipboardpipeline.js +0 -287
- package/src/dragdrop.js +0 -619
- package/src/dragdropblocktoolbar.js +0 -124
- package/src/dragdroptarget.js +0 -390
- package/src/index.js +0 -23
- package/src/lineview.js +0 -46
- package/src/pasteplaintext.js +0 -102
- package/src/utils/normalizeclipboarddata.js +0 -28
- package/src/utils/plaintexttohtml.js +0 -39
- package/src/utils/viewtoplaintext.js +0 -96
|
@@ -1,500 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @module clipboard/clipboardmarkersutils
|
|
7
|
-
*/
|
|
8
|
-
import { mapValues } from 'es-toolkit/compat';
|
|
9
|
-
import { uid } from '@ckeditor/ckeditor5-utils';
|
|
10
|
-
import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
11
|
-
import { ModelRange } from '@ckeditor/ckeditor5-engine';
|
|
12
|
-
/**
|
|
13
|
-
* Part of the clipboard logic. Responsible for collecting markers from selected fragments
|
|
14
|
-
* and restoring them with proper positions in pasted elements.
|
|
15
|
-
*
|
|
16
|
-
* @internal
|
|
17
|
-
*/
|
|
18
|
-
export class ClipboardMarkersUtils extends Plugin {
|
|
19
|
-
/**
|
|
20
|
-
* Map of marker names that can be copied.
|
|
21
|
-
*
|
|
22
|
-
* @internal
|
|
23
|
-
*/
|
|
24
|
-
_markersToCopy = new Map();
|
|
25
|
-
/**
|
|
26
|
-
* @inheritDoc
|
|
27
|
-
*/
|
|
28
|
-
static get pluginName() {
|
|
29
|
-
return 'ClipboardMarkersUtils';
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* @inheritDoc
|
|
33
|
-
*/
|
|
34
|
-
static get isOfficialPlugin() {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Registers marker name as copyable in clipboard pipeline.
|
|
39
|
-
*
|
|
40
|
-
* @param markerName Name of marker that can be copied.
|
|
41
|
-
* @param config Configuration that describes what can be performed on specified marker.
|
|
42
|
-
* @internal
|
|
43
|
-
*/
|
|
44
|
-
_registerMarkerToCopy(markerName, config) {
|
|
45
|
-
this._markersToCopy.set(markerName, config);
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Performs copy markers on provided selection and paste it to fragment returned from `getCopiedFragment`.
|
|
49
|
-
*
|
|
50
|
-
* 1. Picks all markers in provided selection.
|
|
51
|
-
* 2. Inserts fake markers to document.
|
|
52
|
-
* 3. Gets copied selection fragment from document.
|
|
53
|
-
* 4. Removes fake elements from fragment and document.
|
|
54
|
-
* 5. Inserts markers in the place of removed fake markers.
|
|
55
|
-
*
|
|
56
|
-
* Due to selection modification, when inserting items, `getCopiedFragment` must *always* operate on `writer.model.document.selection'.
|
|
57
|
-
* Do not use any other custom selection object within callback, as this will lead to out-of-bounds exceptions in rare scenarios.
|
|
58
|
-
*
|
|
59
|
-
* @param action Type of clipboard action.
|
|
60
|
-
* @param selection Selection to be checked.
|
|
61
|
-
* @param getCopiedFragment Callback that performs copy of selection and returns it as fragment.
|
|
62
|
-
* @internal
|
|
63
|
-
*/
|
|
64
|
-
_copySelectedFragmentWithMarkers(action, selection, getCopiedFragment = writer => writer.model.getSelectedContent(writer.model.document.selection)) {
|
|
65
|
-
return this.editor.model.change(writer => {
|
|
66
|
-
const oldSelection = writer.model.document.selection;
|
|
67
|
-
// In some scenarios, such like in drag & drop, passed `selection` parameter is not actually
|
|
68
|
-
// the same `selection` as the `writer.model.document.selection` which means that `_insertFakeMarkersToSelection`
|
|
69
|
-
// is not affecting passed `selection` `start` and `end` positions but rather modifies `writer.model.document.selection`.
|
|
70
|
-
//
|
|
71
|
-
// It is critical due to fact that when we have selection that starts [ 0, 0 ] and ends at [ 1, 0 ]
|
|
72
|
-
// and after inserting fake marker it will point to such marker instead of new widget position at start: [ 1, 0 ] end: [2, 0 ].
|
|
73
|
-
// `writer.insert` modifies only original `writer.model.document.selection`.
|
|
74
|
-
writer.setSelection(selection);
|
|
75
|
-
const sourceSelectionInsertedMarkers = this._insertFakeMarkersIntoSelection(writer, writer.model.document.selection, action);
|
|
76
|
-
const fragment = getCopiedFragment(writer);
|
|
77
|
-
const fakeMarkersRangesInsideRange = this._removeFakeMarkersInsideElement(writer, fragment);
|
|
78
|
-
// <fake-marker> [Foo] Bar</fake-marker>
|
|
79
|
-
// ^ ^
|
|
80
|
-
// In `_insertFakeMarkersIntoSelection` call we inserted fake marker just before first element.
|
|
81
|
-
// The problem is that the first element can be start position of selection so insertion fake-marker
|
|
82
|
-
// before such element shifts selection (so selection that was at [0, 0] now is at [0, 1]).
|
|
83
|
-
// It means that inserted fake-marker is no longer present inside such selection and is orphaned.
|
|
84
|
-
// This function checks special case of such problem. Markers that are orphaned at the start position
|
|
85
|
-
// and end position in the same time. Basically it means that they overlaps whole element.
|
|
86
|
-
for (const [markerName, elements] of Object.entries(sourceSelectionInsertedMarkers)) {
|
|
87
|
-
fakeMarkersRangesInsideRange[markerName] ||= writer.createRangeIn(fragment);
|
|
88
|
-
for (const element of elements) {
|
|
89
|
-
writer.remove(element);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
fragment.markers.clear();
|
|
93
|
-
for (const [markerName, range] of Object.entries(fakeMarkersRangesInsideRange)) {
|
|
94
|
-
fragment.markers.set(markerName, range);
|
|
95
|
-
}
|
|
96
|
-
// Revert back selection to previous one.
|
|
97
|
-
writer.setSelection(oldSelection);
|
|
98
|
-
return fragment;
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Performs paste of markers on already pasted element.
|
|
103
|
-
*
|
|
104
|
-
* 1. Inserts fake markers that are present in fragment element (such fragment will be processed in `getPastedDocumentElement`).
|
|
105
|
-
* 2. Calls `getPastedDocumentElement` and gets element that is inserted into root model.
|
|
106
|
-
* 3. Removes all fake markers present in transformed element.
|
|
107
|
-
* 4. Inserts new markers with removed fake markers ranges into pasted fragment.
|
|
108
|
-
*
|
|
109
|
-
* There are multiple edge cases that have to be considered before calling this function:
|
|
110
|
-
*
|
|
111
|
-
* * `markers` are inserted into the same element that must be later transformed inside `getPastedDocumentElement`.
|
|
112
|
-
* * Fake marker elements inside `getPastedDocumentElement` can be cloned, but their ranges cannot overlap.
|
|
113
|
-
* * If `duplicateOnPaste` is `true` in marker config then associated marker ID is regenerated before pasting.
|
|
114
|
-
*
|
|
115
|
-
* @param markers Object that maps marker name to corresponding range.
|
|
116
|
-
* @param getPastedDocumentElement Getter used to get target markers element.
|
|
117
|
-
* @internal
|
|
118
|
-
*/
|
|
119
|
-
_pasteMarkersIntoTransformedElement(markers, getPastedDocumentElement) {
|
|
120
|
-
const pasteMarkers = this._getPasteMarkersFromRangeMap(markers);
|
|
121
|
-
return this.editor.model.change(writer => {
|
|
122
|
-
// Inserts fake markers into source fragment / element that is later transformed inside `getPastedDocumentElement`.
|
|
123
|
-
const sourceFragmentFakeMarkers = this._insertFakeMarkersElements(writer, pasteMarkers);
|
|
124
|
-
// Modifies document fragment (for example, cloning table cells) and then inserts it into the document.
|
|
125
|
-
const transformedElement = getPastedDocumentElement(writer);
|
|
126
|
-
// Removes markers in pasted and transformed fragment in root document.
|
|
127
|
-
const removedFakeMarkers = this._removeFakeMarkersInsideElement(writer, transformedElement);
|
|
128
|
-
// Cleans up fake markers inserted into source fragment (that one before transformation which is not pasted).
|
|
129
|
-
for (const element of Object.values(sourceFragmentFakeMarkers).flat()) {
|
|
130
|
-
writer.remove(element);
|
|
131
|
-
}
|
|
132
|
-
// Inserts to root document fake markers.
|
|
133
|
-
for (const [markerName, range] of Object.entries(removedFakeMarkers)) {
|
|
134
|
-
if (!writer.model.markers.has(markerName)) {
|
|
135
|
-
writer.addMarker(markerName, {
|
|
136
|
-
usingOperation: true,
|
|
137
|
-
affectsData: true,
|
|
138
|
-
range
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return transformedElement;
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Pastes document fragment with markers to document.
|
|
147
|
-
* If `duplicateOnPaste` is `true` in marker config then associated markers IDs
|
|
148
|
-
* are regenerated before pasting to avoid markers duplications in content.
|
|
149
|
-
*
|
|
150
|
-
* @param fragment Document fragment that should contain already processed by pipeline markers.
|
|
151
|
-
* @internal
|
|
152
|
-
*/
|
|
153
|
-
_pasteFragmentWithMarkers(fragment) {
|
|
154
|
-
const pasteMarkers = this._getPasteMarkersFromRangeMap(fragment.markers);
|
|
155
|
-
fragment.markers.clear();
|
|
156
|
-
for (const copyableMarker of pasteMarkers) {
|
|
157
|
-
fragment.markers.set(copyableMarker.name, copyableMarker.range);
|
|
158
|
-
}
|
|
159
|
-
return this.editor.model.insertContent(fragment);
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* In some situations we have to perform copy on selected fragment with certain markers. This function allows to temporarily bypass
|
|
163
|
-
* restrictions on markers that we want to copy.
|
|
164
|
-
*
|
|
165
|
-
* This function executes `executor()` callback. For the duration of the callback, if the clipboard pipeline is used to copy
|
|
166
|
-
* content, markers with the specified name will be copied to the clipboard as well.
|
|
167
|
-
*
|
|
168
|
-
* @param markerName Which markers should be copied.
|
|
169
|
-
* @param executor Callback executed.
|
|
170
|
-
* @param config Optional configuration flags used to copy (such like partial copy flag).
|
|
171
|
-
* @internal
|
|
172
|
-
*/
|
|
173
|
-
_forceMarkersCopy(markerName, executor, config = {
|
|
174
|
-
allowedActions: 'all',
|
|
175
|
-
copyPartiallySelected: true,
|
|
176
|
-
duplicateOnPaste: true
|
|
177
|
-
}) {
|
|
178
|
-
const before = this._markersToCopy.get(markerName);
|
|
179
|
-
this._markersToCopy.set(markerName, config);
|
|
180
|
-
executor();
|
|
181
|
-
if (before) {
|
|
182
|
-
this._markersToCopy.set(markerName, before);
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
this._markersToCopy.delete(markerName);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Checks if marker can be copied.
|
|
190
|
-
*
|
|
191
|
-
* @param markerName Name of checked marker.
|
|
192
|
-
* @param action Type of clipboard action. If null then checks only if marker is registered as copyable.
|
|
193
|
-
* @internal
|
|
194
|
-
*/
|
|
195
|
-
_isMarkerCopyable(markerName, action) {
|
|
196
|
-
const config = this._getMarkerClipboardConfig(markerName);
|
|
197
|
-
if (!config) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
// If there is no action provided then only presence of marker is checked.
|
|
201
|
-
if (!action) {
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
const { allowedActions } = config;
|
|
205
|
-
return allowedActions === 'all' || allowedActions.includes(action);
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Checks if marker has any clipboard copy behavior configuration.
|
|
209
|
-
*
|
|
210
|
-
* @param markerName Name of checked marker.
|
|
211
|
-
*/
|
|
212
|
-
_hasMarkerConfiguration(markerName) {
|
|
213
|
-
return !!this._getMarkerClipboardConfig(markerName);
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Returns marker's configuration flags passed during registration.
|
|
217
|
-
*
|
|
218
|
-
* @param markerName Name of marker that should be returned.
|
|
219
|
-
* @internal
|
|
220
|
-
*/
|
|
221
|
-
_getMarkerClipboardConfig(markerName) {
|
|
222
|
-
const [markerNamePrefix] = markerName.split(':');
|
|
223
|
-
return this._markersToCopy.get(markerNamePrefix) || null;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* First step of copying markers. It looks for markers intersecting with given selection and inserts `$marker` elements
|
|
227
|
-
* at positions where document markers start or end. This way `$marker` elements can be easily copied together with
|
|
228
|
-
* the rest of the content of the selection.
|
|
229
|
-
*
|
|
230
|
-
* @param writer An instance of the model writer.
|
|
231
|
-
* @param selection Selection to be checked.
|
|
232
|
-
* @param action Type of clipboard action.
|
|
233
|
-
*/
|
|
234
|
-
_insertFakeMarkersIntoSelection(writer, selection, action) {
|
|
235
|
-
const copyableMarkers = this._getCopyableMarkersFromSelection(writer, selection, action);
|
|
236
|
-
return this._insertFakeMarkersElements(writer, copyableMarkers);
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Returns array of markers that can be copied in specified selection.
|
|
240
|
-
*
|
|
241
|
-
* If marker cannot be copied partially (according to `copyPartiallySelected` configuration flag) and
|
|
242
|
-
* is not present entirely in any selection range then it will be skipped.
|
|
243
|
-
*
|
|
244
|
-
* @param writer An instance of the model writer.
|
|
245
|
-
* @param selection Selection which will be checked.
|
|
246
|
-
* @param action Type of clipboard action. If null then checks only if marker is registered as copyable.
|
|
247
|
-
*/
|
|
248
|
-
_getCopyableMarkersFromSelection(writer, selection, action) {
|
|
249
|
-
const selectionRanges = Array.from(selection.getRanges());
|
|
250
|
-
// Picks all markers in provided ranges. Ensures that there are no duplications if
|
|
251
|
-
// there are multiple ranges that intersects with the same marker.
|
|
252
|
-
const markersInRanges = new Set(selectionRanges.flatMap(selectionRange => Array.from(writer.model.markers.getMarkersIntersectingRange(selectionRange))));
|
|
253
|
-
const isSelectionMarkerCopyable = (marker) => {
|
|
254
|
-
// Check if marker exists in configuration and provided action can be performed on it.
|
|
255
|
-
const isCopyable = this._isMarkerCopyable(marker.name, action);
|
|
256
|
-
if (!isCopyable) {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
// Checks if configuration disallows to copy marker only if part of its content is selected.
|
|
260
|
-
//
|
|
261
|
-
// Example:
|
|
262
|
-
// <marker-a> Hello [ World ] </marker-a>
|
|
263
|
-
// ^ selection
|
|
264
|
-
//
|
|
265
|
-
// In this scenario `marker-a` won't be copied because selection doesn't overlap its content entirely.
|
|
266
|
-
const { copyPartiallySelected } = this._getMarkerClipboardConfig(marker.name);
|
|
267
|
-
if (!copyPartiallySelected) {
|
|
268
|
-
const markerRange = marker.getRange();
|
|
269
|
-
return selectionRanges.some(selectionRange => selectionRange.containsRange(markerRange, true));
|
|
270
|
-
}
|
|
271
|
-
return true;
|
|
272
|
-
};
|
|
273
|
-
return Array
|
|
274
|
-
.from(markersInRanges)
|
|
275
|
-
.filter(isSelectionMarkerCopyable)
|
|
276
|
-
.map((copyableMarker) => {
|
|
277
|
-
// During `dragstart` event original marker is still present in tree.
|
|
278
|
-
// It is removed after the clipboard drop event, so none of the copied markers are inserted at the end.
|
|
279
|
-
// It happens because there already markers with specified `marker.name` when clipboard is trying to insert data
|
|
280
|
-
// and it aborts inserting.
|
|
281
|
-
const name = action === 'dragstart' ? this._getUniqueMarkerName(copyableMarker.name) : copyableMarker.name;
|
|
282
|
-
return {
|
|
283
|
-
name,
|
|
284
|
-
range: copyableMarker.getRange()
|
|
285
|
-
};
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Picks all markers from markers map that can be pasted.
|
|
290
|
-
* If `duplicateOnPaste` is `true`, it regenerates their IDs to ensure uniqueness.
|
|
291
|
-
* If marker is not registered, it will be kept in the array anyway.
|
|
292
|
-
*
|
|
293
|
-
* @param markers Object that maps marker name to corresponding range.
|
|
294
|
-
* @param action Type of clipboard action. If null then checks only if marker is registered as copyable.
|
|
295
|
-
*/
|
|
296
|
-
_getPasteMarkersFromRangeMap(markers, action = null) {
|
|
297
|
-
const { model } = this.editor;
|
|
298
|
-
const entries = markers instanceof Map ? Array.from(markers.entries()) : Object.entries(markers);
|
|
299
|
-
return entries.flatMap(([markerName, range]) => {
|
|
300
|
-
if (!this._hasMarkerConfiguration(markerName)) {
|
|
301
|
-
return [
|
|
302
|
-
{
|
|
303
|
-
name: markerName,
|
|
304
|
-
range
|
|
305
|
-
}
|
|
306
|
-
];
|
|
307
|
-
}
|
|
308
|
-
if (this._isMarkerCopyable(markerName, action)) {
|
|
309
|
-
const copyMarkerConfig = this._getMarkerClipboardConfig(markerName);
|
|
310
|
-
const isInGraveyard = model.markers.has(markerName) &&
|
|
311
|
-
model.markers.get(markerName).getRange().root.rootName === '$graveyard';
|
|
312
|
-
if (copyMarkerConfig.duplicateOnPaste || isInGraveyard) {
|
|
313
|
-
markerName = this._getUniqueMarkerName(markerName);
|
|
314
|
-
}
|
|
315
|
-
return [
|
|
316
|
-
{
|
|
317
|
-
name: markerName,
|
|
318
|
-
range
|
|
319
|
-
}
|
|
320
|
-
];
|
|
321
|
-
}
|
|
322
|
-
return [];
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Inserts specified array of fake markers elements to document and assigns them `type` and `name` attributes.
|
|
327
|
-
* Fake markers elements are used to calculate position of markers on pasted fragment that were transformed during
|
|
328
|
-
* steps between copy and paste.
|
|
329
|
-
*
|
|
330
|
-
* @param writer An instance of the model writer.
|
|
331
|
-
* @param markers Array of markers that will be inserted.
|
|
332
|
-
*/
|
|
333
|
-
_insertFakeMarkersElements(writer, markers) {
|
|
334
|
-
const mappedMarkers = {};
|
|
335
|
-
const sortedMarkers = markers
|
|
336
|
-
.flatMap(marker => {
|
|
337
|
-
const { start, end } = marker.range;
|
|
338
|
-
return [
|
|
339
|
-
{ position: start, marker, type: 'start' },
|
|
340
|
-
{ position: end, marker, type: 'end' }
|
|
341
|
-
];
|
|
342
|
-
})
|
|
343
|
-
// Markers position is sorted backwards to ensure that the insertion of fake markers will not change
|
|
344
|
-
// the position of the next markers.
|
|
345
|
-
.sort(({ position: posA }, { position: posB }) => posA.isBefore(posB) ? 1 : -1);
|
|
346
|
-
for (const { position, marker, type } of sortedMarkers) {
|
|
347
|
-
const fakeMarker = writer.createElement('$marker', {
|
|
348
|
-
'data-name': marker.name,
|
|
349
|
-
'data-type': type
|
|
350
|
-
});
|
|
351
|
-
if (!mappedMarkers[marker.name]) {
|
|
352
|
-
mappedMarkers[marker.name] = [];
|
|
353
|
-
}
|
|
354
|
-
mappedMarkers[marker.name].push(fakeMarker);
|
|
355
|
-
writer.insert(fakeMarker, position);
|
|
356
|
-
}
|
|
357
|
-
return mappedMarkers;
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Removes all `$marker` elements from the given document fragment.
|
|
361
|
-
*
|
|
362
|
-
* Returns an object where keys are marker names, and values are ranges corresponding to positions
|
|
363
|
-
* where `$marker` elements were inserted.
|
|
364
|
-
*
|
|
365
|
-
* If the document fragment had only one `$marker` element for given marker (start or end) the other boundary is set automatically
|
|
366
|
-
* (to the end or start of the document fragment, respectively).
|
|
367
|
-
*
|
|
368
|
-
* @param writer An instance of the model writer.
|
|
369
|
-
* @param rootElement The element to be checked.
|
|
370
|
-
*/
|
|
371
|
-
_removeFakeMarkersInsideElement(writer, rootElement) {
|
|
372
|
-
const fakeMarkersElements = this._getAllFakeMarkersFromElement(writer, rootElement);
|
|
373
|
-
const fakeMarkersRanges = fakeMarkersElements.reduce((acc, fakeMarker) => {
|
|
374
|
-
const position = fakeMarker.markerElement && writer.createPositionBefore(fakeMarker.markerElement);
|
|
375
|
-
let prevFakeMarker = acc[fakeMarker.name];
|
|
376
|
-
// Handle scenario when tables clone cells with the same fake node. Example:
|
|
377
|
-
//
|
|
378
|
-
// <cell><fake-marker-a></cell> <cell><fake-marker-a></cell> <cell><fake-marker-a></cell>
|
|
379
|
-
// ^ cloned ^ cloned
|
|
380
|
-
//
|
|
381
|
-
// The easiest way to bypass this issue is to rename already existing in map nodes and
|
|
382
|
-
// set them new unique name.
|
|
383
|
-
let skipAssign = false;
|
|
384
|
-
if (prevFakeMarker?.start && prevFakeMarker?.end) {
|
|
385
|
-
const config = this._getMarkerClipboardConfig(fakeMarker.name);
|
|
386
|
-
if (config.duplicateOnPaste) {
|
|
387
|
-
acc[this._getUniqueMarkerName(fakeMarker.name)] = acc[fakeMarker.name];
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
skipAssign = true;
|
|
391
|
-
}
|
|
392
|
-
prevFakeMarker = null;
|
|
393
|
-
}
|
|
394
|
-
if (!skipAssign) {
|
|
395
|
-
acc[fakeMarker.name] = {
|
|
396
|
-
...prevFakeMarker,
|
|
397
|
-
[fakeMarker.type]: position
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
if (fakeMarker.markerElement) {
|
|
401
|
-
writer.remove(fakeMarker.markerElement);
|
|
402
|
-
}
|
|
403
|
-
return acc;
|
|
404
|
-
}, {});
|
|
405
|
-
// We cannot construct ranges directly in previous reduce because element ranges can overlap.
|
|
406
|
-
// In other words lets assume we have such scenario:
|
|
407
|
-
// <fake-marker-start /> <paragraph /> <fake-marker-2-start /> <fake-marker-end /> <fake-marker-2-end />
|
|
408
|
-
//
|
|
409
|
-
// We have to remove `fake-marker-start` firstly and then remove `fake-marker-2-start`.
|
|
410
|
-
// Removal of `fake-marker-2-start` affects `fake-marker-end` position so we cannot create
|
|
411
|
-
// connection between `fake-marker-start` and `fake-marker-end` without iterating whole set firstly.
|
|
412
|
-
return mapValues(fakeMarkersRanges, range => new ModelRange(range.start || writer.createPositionFromPath(rootElement, [0]), range.end || writer.createPositionAt(rootElement, 'end')));
|
|
413
|
-
}
|
|
414
|
-
/**
|
|
415
|
-
* Returns array that contains list of fake markers with corresponding `$marker` elements.
|
|
416
|
-
*
|
|
417
|
-
* For each marker, there can be two `$marker` elements or only one (if the document fragment contained
|
|
418
|
-
* only the beginning or only the end of a marker).
|
|
419
|
-
*
|
|
420
|
-
* @param writer An instance of the model writer.
|
|
421
|
-
* @param rootElement The element to be checked.
|
|
422
|
-
*/
|
|
423
|
-
_getAllFakeMarkersFromElement(writer, rootElement) {
|
|
424
|
-
const foundFakeMarkers = Array
|
|
425
|
-
.from(writer.createRangeIn(rootElement))
|
|
426
|
-
.flatMap(({ item }) => {
|
|
427
|
-
if (!item.is('element', '$marker')) {
|
|
428
|
-
return [];
|
|
429
|
-
}
|
|
430
|
-
const name = item.getAttribute('data-name');
|
|
431
|
-
const type = item.getAttribute('data-type');
|
|
432
|
-
return [
|
|
433
|
-
{
|
|
434
|
-
markerElement: item,
|
|
435
|
-
name,
|
|
436
|
-
type
|
|
437
|
-
}
|
|
438
|
-
];
|
|
439
|
-
});
|
|
440
|
-
const prependFakeMarkers = [];
|
|
441
|
-
const appendFakeMarkers = [];
|
|
442
|
-
for (const fakeMarker of foundFakeMarkers) {
|
|
443
|
-
if (fakeMarker.type === 'end') {
|
|
444
|
-
// <fake-marker> [ phrase</fake-marker> phrase ]
|
|
445
|
-
// ^
|
|
446
|
-
// Handle case when marker is just before start of selection.
|
|
447
|
-
// Only end marker is inside selection.
|
|
448
|
-
const hasMatchingStartMarker = foundFakeMarkers.some(otherFakeMarker => otherFakeMarker.name === fakeMarker.name && otherFakeMarker.type === 'start');
|
|
449
|
-
if (!hasMatchingStartMarker) {
|
|
450
|
-
prependFakeMarkers.push({
|
|
451
|
-
markerElement: null,
|
|
452
|
-
name: fakeMarker.name,
|
|
453
|
-
type: 'start'
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
if (fakeMarker.type === 'start') {
|
|
458
|
-
// [<fake-marker>phrase]</fake-marker>
|
|
459
|
-
// ^
|
|
460
|
-
// Handle case when fake marker is after selection.
|
|
461
|
-
// Only start marker is inside selection.
|
|
462
|
-
const hasMatchingEndMarker = foundFakeMarkers.some(otherFakeMarker => otherFakeMarker.name === fakeMarker.name && otherFakeMarker.type === 'end');
|
|
463
|
-
if (!hasMatchingEndMarker) {
|
|
464
|
-
appendFakeMarkers.unshift({
|
|
465
|
-
markerElement: null,
|
|
466
|
-
name: fakeMarker.name,
|
|
467
|
-
type: 'end'
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return [
|
|
473
|
-
...prependFakeMarkers,
|
|
474
|
-
...foundFakeMarkers,
|
|
475
|
-
...appendFakeMarkers
|
|
476
|
-
];
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* When copy of markers occurs we have to make sure that pasted markers have different names
|
|
480
|
-
* than source markers. This functions helps with assigning unique part to marker name to
|
|
481
|
-
* prevent duplicated markers error.
|
|
482
|
-
*
|
|
483
|
-
* @param name Name of marker
|
|
484
|
-
*/
|
|
485
|
-
_getUniqueMarkerName(name) {
|
|
486
|
-
const parts = name.split(':');
|
|
487
|
-
const newId = uid().substring(1, 6);
|
|
488
|
-
// It looks like the marker already is UID marker so in this scenario just swap
|
|
489
|
-
// last part of marker name and assign new UID.
|
|
490
|
-
//
|
|
491
|
-
// example: comment:{ threadId }:{ id } => comment:{ threadId }:{ newId }
|
|
492
|
-
if (parts.length === 3) {
|
|
493
|
-
return `${parts.slice(0, 2).join(':')}:${newId}`;
|
|
494
|
-
}
|
|
495
|
-
// Assign new segment to marker name with id.
|
|
496
|
-
//
|
|
497
|
-
// example: comment => comment:{ newId }
|
|
498
|
-
return `${parts.join(':')}:${newId}`;
|
|
499
|
-
}
|
|
500
|
-
}
|
package/src/clipboardobserver.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @module clipboard/clipboardobserver
|
|
7
|
-
*/
|
|
8
|
-
import { EventInfo, getRangeFromMouseEvent } from '@ckeditor/ckeditor5-utils';
|
|
9
|
-
import { ViewDataTransfer, DomEventObserver } from '@ckeditor/ckeditor5-engine';
|
|
10
|
-
/**
|
|
11
|
-
* Clipboard events observer.
|
|
12
|
-
*
|
|
13
|
-
* Fires the following events:
|
|
14
|
-
*
|
|
15
|
-
* * {@link module:engine/view/document~ViewDocument#event:clipboardInput},
|
|
16
|
-
* * {@link module:engine/view/document~ViewDocument#event:paste},
|
|
17
|
-
* * {@link module:engine/view/document~ViewDocument#event:copy},
|
|
18
|
-
* * {@link module:engine/view/document~ViewDocument#event:cut},
|
|
19
|
-
* * {@link module:engine/view/document~ViewDocument#event:drop},
|
|
20
|
-
* * {@link module:engine/view/document~ViewDocument#event:dragover},
|
|
21
|
-
* * {@link module:engine/view/document~ViewDocument#event:dragging},
|
|
22
|
-
* * {@link module:engine/view/document~ViewDocument#event:dragstart},
|
|
23
|
-
* * {@link module:engine/view/document~ViewDocument#event:dragend},
|
|
24
|
-
* * {@link module:engine/view/document~ViewDocument#event:dragenter},
|
|
25
|
-
* * {@link module:engine/view/document~ViewDocument#event:dragleave}.
|
|
26
|
-
*
|
|
27
|
-
* **Note**: This observer is not available by default (ckeditor5-engine does not add it on its own).
|
|
28
|
-
* To make it available, it needs to be added to {@link module:engine/view/document~ViewDocument} by using
|
|
29
|
-
* the {@link module:engine/view/view~EditingView#addObserver `View#addObserver()`} method. Alternatively, you can load the
|
|
30
|
-
* {@link module:clipboard/clipboard~Clipboard} plugin which adds this observer automatically (because it uses it).
|
|
31
|
-
*/
|
|
32
|
-
export class ClipboardObserver extends DomEventObserver {
|
|
33
|
-
domEventType = [
|
|
34
|
-
'paste', 'copy', 'cut', 'drop', 'dragover', 'dragstart', 'dragend', 'dragenter', 'dragleave'
|
|
35
|
-
];
|
|
36
|
-
constructor(view) {
|
|
37
|
-
super(view);
|
|
38
|
-
const viewDocument = this.document;
|
|
39
|
-
this.listenTo(viewDocument, 'paste', handleInput('clipboardInput'), { priority: 'low' });
|
|
40
|
-
this.listenTo(viewDocument, 'drop', handleInput('clipboardInput'), { priority: 'low' });
|
|
41
|
-
this.listenTo(viewDocument, 'dragover', handleInput('dragging'), { priority: 'low' });
|
|
42
|
-
function handleInput(type) {
|
|
43
|
-
return (evt, data) => {
|
|
44
|
-
data.preventDefault();
|
|
45
|
-
const targetRanges = data.dropRange ? [data.dropRange] : null;
|
|
46
|
-
const eventInfo = new EventInfo(viewDocument, type);
|
|
47
|
-
viewDocument.fire(eventInfo, {
|
|
48
|
-
dataTransfer: data.dataTransfer,
|
|
49
|
-
method: evt.name,
|
|
50
|
-
targetRanges,
|
|
51
|
-
target: data.target,
|
|
52
|
-
domEvent: data.domEvent
|
|
53
|
-
});
|
|
54
|
-
// If CKEditor handled the input, do not bubble the original event any further.
|
|
55
|
-
// This helps external integrations recognize that fact and act accordingly.
|
|
56
|
-
// https://github.com/ckeditor/ckeditor5-upload/issues/92
|
|
57
|
-
if (eventInfo.stop.called) {
|
|
58
|
-
data.stopPropagation();
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
onDomEvent(domEvent) {
|
|
64
|
-
const nativeDataTransfer = 'clipboardData' in domEvent ? domEvent.clipboardData : domEvent.dataTransfer;
|
|
65
|
-
const cacheFiles = domEvent.type == 'drop' || domEvent.type == 'paste';
|
|
66
|
-
const evtData = {
|
|
67
|
-
dataTransfer: new ViewDataTransfer(nativeDataTransfer, { cacheFiles })
|
|
68
|
-
};
|
|
69
|
-
if (domEvent.type == 'drop' || domEvent.type == 'dragover') {
|
|
70
|
-
const domRange = getRangeFromMouseEvent(domEvent);
|
|
71
|
-
evtData.dropRange = domRange && this.view.domConverter.domRangeToView(domRange);
|
|
72
|
-
}
|
|
73
|
-
this.fire(domEvent.type, domEvent, evtData);
|
|
74
|
-
}
|
|
75
|
-
}
|