@ckeditor/ckeditor5-clipboard 40.0.0 → 40.2.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/dragdrop.js CHANGED
@@ -1,577 +1,577 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
- */
5
- /**
6
- * @module clipboard/dragdrop
7
- */
8
- import { Plugin } from '@ckeditor/ckeditor5-core';
9
- import { LiveRange, MouseObserver } from '@ckeditor/ckeditor5-engine';
10
- import { Widget, isWidget } from '@ckeditor/ckeditor5-widget';
11
- import { env, uid, global, createElement, DomEmitterMixin, delay, Rect } from '@ckeditor/ckeditor5-utils';
12
- import ClipboardPipeline from './clipboardpipeline';
13
- import ClipboardObserver from './clipboardobserver';
14
- import DragDropTarget from './dragdroptarget';
15
- import DragDropBlockToolbar from './dragdropblocktoolbar';
16
- import '../theme/clipboard.css';
17
- // Drag and drop events overview:
18
- //
19
- // ┌──────────────────┐
20
- // │ mousedown │ Sets the draggable attribute.
21
- // └─────────┬────────┘
22
- // │
23
- // └─────────────────────┐
24
- // │ │
25
- // │ ┌─────────V────────┐
26
- // │ │ mouseup │ Dragging did not start, removes the draggable attribute.
27
- // │ └──────────────────┘
28
- // │
29
- // ┌─────────V────────┐ Retrieves the selected model.DocumentFragment
30
- // │ dragstart │ and converts it to view.DocumentFragment.
31
- // └─────────┬────────┘
32
- // │
33
- // ┌─────────V────────┐ Processes view.DocumentFragment to text/html and text/plain
34
- // │ clipboardOutput │ and stores the results in data.dataTransfer.
35
- // └─────────┬────────┘
36
- // │
37
- // │ DOM dragover
38
- // ┌────────────┐
39
- // │ │
40
- // ┌─────────V────────┐ │
41
- // │ dragging │ │ Updates the drop target marker.
42
- // └─────────┬────────┘ │
43
- // │ │
44
- // ┌─────────────└────────────┘
45
- // │ │ │
46
- // │ ┌─────────V────────┐ │
47
- // │ │ dragleave │ │ Removes the drop target marker.
48
- // │ └─────────┬────────┘ │
49
- // │ │ │
50
- // ┌───│─────────────┘ │
51
- // │ │ │ │
52
- // │ │ ┌─────────V────────┐ │
53
- // │ │ │ dragenter │ │ Focuses the editor view.
54
- // │ │ └─────────┬────────┘ │
55
- // │ │ │ │
56
- // │ │ └────────────┘
57
- // │ │
58
- // │ └─────────────┐
59
- // │ │ │
60
- // │ │ ┌─────────V────────┐
61
- // └───┐ │ drop │ (The default handler of the clipboard pipeline).
62
- // │ └─────────┬────────┘
63
- // │ │
64
- // │ ┌─────────V────────┐ Resolves the final data.targetRanges.
65
- // │ │ clipboardInput │ Aborts if dropping on dragged content.
66
- // │ └─────────┬────────┘
67
- // │ │
68
- // │ ┌─────────V────────┐
69
- // │ │ clipboardInput │ (The default handler of the clipboard pipeline).
70
- // │ └─────────┬────────┘
71
- // │ │
72
- // │ ┌───────────V───────────┐
73
- // │ │ inputTransformation │ (The default handler of the clipboard pipeline).
74
- // │ └───────────┬───────────┘
75
- // │ │
76
- // │ ┌──────────V──────────┐
77
- // │ │ contentInsertion │ Updates the document selection to drop range.
78
- // │ └──────────┬──────────┘
79
- // │ │
80
- // │ ┌──────────V──────────┐
81
- // │ │ contentInsertion │ (The default handler of the clipboard pipeline).
82
- // │ └──────────┬──────────┘
83
- // │ │
84
- // │ ┌──────────V──────────┐
85
- // │ │ contentInsertion │ Removes the content from the original range if the insertion was successful.
86
- // │ └──────────┬──────────┘
87
- // │ │
88
- // └─────────────┐
89
- // │
90
- // ┌─────────V────────┐
91
- // │ dragend │ Removes the drop marker and cleans the state.
92
- // └──────────────────┘
93
- //
94
- /**
95
- * The drag and drop feature. It works on top of the {@link module:clipboard/clipboardpipeline~ClipboardPipeline}.
96
- *
97
- * Read more about the clipboard integration in the {@glink framework/deep-dive/clipboard clipboard deep-dive} guide.
98
- *
99
- * @internal
100
- */
101
- export default class DragDrop extends Plugin {
102
- constructor() {
103
- super(...arguments);
104
- /**
105
- * A delayed callback removing draggable attributes.
106
- */
107
- this._clearDraggableAttributesDelayed = delay(() => this._clearDraggableAttributes(), 40);
108
- /**
109
- * Whether the dragged content can be dropped only in block context.
110
- */
111
- // TODO handle drag from other editor instance
112
- // TODO configure to use block, inline or both
113
- this._blockMode = false;
114
- /**
115
- * DOM Emitter.
116
- */
117
- this._domEmitter = new (DomEmitterMixin())();
118
- }
119
- /**
120
- * @inheritDoc
121
- */
122
- static get pluginName() {
123
- return 'DragDrop';
124
- }
125
- /**
126
- * @inheritDoc
127
- */
128
- static get requires() {
129
- return [ClipboardPipeline, Widget, DragDropTarget, DragDropBlockToolbar];
130
- }
131
- /**
132
- * @inheritDoc
133
- */
134
- init() {
135
- const editor = this.editor;
136
- const view = editor.editing.view;
137
- this._draggedRange = null;
138
- this._draggingUid = '';
139
- this._draggableElement = null;
140
- view.addObserver(ClipboardObserver);
141
- view.addObserver(MouseObserver);
142
- this._setupDragging();
143
- this._setupContentInsertionIntegration();
144
- this._setupClipboardInputIntegration();
145
- this._setupDraggableAttributeHandling();
146
- this.listenTo(editor, 'change:isReadOnly', (evt, name, isReadOnly) => {
147
- if (isReadOnly) {
148
- this.forceDisabled('readOnlyMode');
149
- }
150
- else {
151
- this.clearForceDisabled('readOnlyMode');
152
- }
153
- });
154
- this.on('change:isEnabled', (evt, name, isEnabled) => {
155
- if (!isEnabled) {
156
- this._finalizeDragging(false);
157
- }
158
- });
159
- if (env.isAndroid) {
160
- this.forceDisabled('noAndroidSupport');
161
- }
162
- }
163
- /**
164
- * @inheritDoc
165
- */
166
- destroy() {
167
- if (this._draggedRange) {
168
- this._draggedRange.detach();
169
- this._draggedRange = null;
170
- }
171
- if (this._previewContainer) {
172
- this._previewContainer.remove();
173
- }
174
- this._domEmitter.stopListening();
175
- this._clearDraggableAttributesDelayed.cancel();
176
- return super.destroy();
177
- }
178
- /**
179
- * Drag and drop events handling.
180
- */
181
- _setupDragging() {
182
- const editor = this.editor;
183
- const model = editor.model;
184
- const view = editor.editing.view;
185
- const viewDocument = view.document;
186
- const dragDropTarget = editor.plugins.get(DragDropTarget);
187
- // The handler for the drag start; it is responsible for setting data transfer object.
188
- this.listenTo(viewDocument, 'dragstart', (evt, data) => {
189
- // Don't drag the editable element itself.
190
- if (data.target && data.target.is('editableElement')) {
191
- data.preventDefault();
192
- return;
193
- }
194
- this._prepareDraggedRange(data.target);
195
- if (!this._draggedRange) {
196
- data.preventDefault();
197
- return;
198
- }
199
- this._draggingUid = uid();
200
- data.dataTransfer.effectAllowed = this.isEnabled ? 'copyMove' : 'copy';
201
- data.dataTransfer.setData('application/ckeditor5-dragging-uid', this._draggingUid);
202
- const draggedSelection = model.createSelection(this._draggedRange.toRange());
203
- const clipboardPipeline = this.editor.plugins.get('ClipboardPipeline');
204
- clipboardPipeline._fireOutputTransformationEvent(data.dataTransfer, draggedSelection, 'dragstart');
205
- const { dataTransfer, domTarget, domEvent } = data;
206
- const { clientX } = domEvent;
207
- this._updatePreview({ dataTransfer, domTarget, clientX });
208
- data.stopPropagation();
209
- if (!this.isEnabled) {
210
- this._draggedRange.detach();
211
- this._draggedRange = null;
212
- this._draggingUid = '';
213
- }
214
- }, { priority: 'low' });
215
- // The handler for finalizing drag and drop. It should always be triggered after dragging completes
216
- // even if it was completed in a different application.
217
- // Note: This is not fired if source text node got removed while downcasting a marker.
218
- this.listenTo(viewDocument, 'dragend', (evt, data) => {
219
- this._finalizeDragging(!data.dataTransfer.isCanceled && data.dataTransfer.dropEffect == 'move');
220
- }, { priority: 'low' });
221
- // Reset block dragging mode even if dropped outside the editable.
222
- this._domEmitter.listenTo(global.document, 'dragend', () => {
223
- this._blockMode = false;
224
- }, { useCapture: true });
225
- // Dragging over the editable.
226
- this.listenTo(viewDocument, 'dragenter', () => {
227
- if (!this.isEnabled) {
228
- return;
229
- }
230
- view.focus();
231
- });
232
- // Dragging out of the editable.
233
- this.listenTo(viewDocument, 'dragleave', () => {
234
- // We do not know if the mouse left the editor or just some element in it, so let us wait a few milliseconds
235
- // to check if 'dragover' is not fired.
236
- dragDropTarget.removeDropMarkerDelayed();
237
- });
238
- // Handler for moving dragged content over the target area.
239
- this.listenTo(viewDocument, 'dragging', (evt, data) => {
240
- if (!this.isEnabled) {
241
- data.dataTransfer.dropEffect = 'none';
242
- return;
243
- }
244
- const { clientX, clientY } = data.domEvent;
245
- dragDropTarget.updateDropMarker(data.target, data.targetRanges, clientX, clientY, this._blockMode);
246
- // If this is content being dragged from another editor, moving out of current editor instance
247
- // is not possible until 'dragend' event case will be fixed.
248
- if (!this._draggedRange) {
249
- data.dataTransfer.dropEffect = 'copy';
250
- }
251
- // In Firefox it is already set and effect allowed remains the same as originally set.
252
- if (!env.isGecko) {
253
- if (data.dataTransfer.effectAllowed == 'copy') {
254
- data.dataTransfer.dropEffect = 'copy';
255
- }
256
- else if (['all', 'copyMove'].includes(data.dataTransfer.effectAllowed)) {
257
- data.dataTransfer.dropEffect = 'move';
258
- }
259
- }
260
- evt.stop();
261
- }, { priority: 'low' });
262
- }
263
- /**
264
- * Integration with the `clipboardInput` event.
265
- */
266
- _setupClipboardInputIntegration() {
267
- const editor = this.editor;
268
- const view = editor.editing.view;
269
- const viewDocument = view.document;
270
- const dragDropTarget = editor.plugins.get(DragDropTarget);
271
- // Update the event target ranges and abort dropping if dropping over itself.
272
- this.listenTo(viewDocument, 'clipboardInput', (evt, data) => {
273
- if (data.method != 'drop') {
274
- return;
275
- }
276
- const { clientX, clientY } = data.domEvent;
277
- const targetRange = dragDropTarget.getFinalDropRange(data.target, data.targetRanges, clientX, clientY, this._blockMode);
278
- if (!targetRange) {
279
- this._finalizeDragging(false);
280
- evt.stop();
281
- return;
282
- }
283
- // Since we cannot rely on the drag end event, we must check if the local drag range is from the current drag and drop
284
- // or it is from some previous not cleared one.
285
- if (this._draggedRange && this._draggingUid != data.dataTransfer.getData('application/ckeditor5-dragging-uid')) {
286
- this._draggedRange.detach();
287
- this._draggedRange = null;
288
- this._draggingUid = '';
289
- }
290
- // Do not do anything if some content was dragged within the same document to the same position.
291
- const isMove = getFinalDropEffect(data.dataTransfer) == 'move';
292
- if (isMove && this._draggedRange && this._draggedRange.containsRange(targetRange, true)) {
293
- this._finalizeDragging(false);
294
- evt.stop();
295
- return;
296
- }
297
- // Override the target ranges with the one adjusted to the best one for a drop.
298
- data.targetRanges = [editor.editing.mapper.toViewRange(targetRange)];
299
- }, { priority: 'high' });
300
- }
301
- /**
302
- * Integration with the `contentInsertion` event of the clipboard pipeline.
303
- */
304
- _setupContentInsertionIntegration() {
305
- const clipboardPipeline = this.editor.plugins.get(ClipboardPipeline);
306
- clipboardPipeline.on('contentInsertion', (evt, data) => {
307
- if (!this.isEnabled || data.method !== 'drop') {
308
- return;
309
- }
310
- // Update the selection to the target range in the same change block to avoid selection post-fixing
311
- // and to be able to clone text attributes for plain text dropping.
312
- const ranges = data.targetRanges.map(viewRange => this.editor.editing.mapper.toModelRange(viewRange));
313
- this.editor.model.change(writer => writer.setSelection(ranges));
314
- }, { priority: 'high' });
315
- clipboardPipeline.on('contentInsertion', (evt, data) => {
316
- if (!this.isEnabled || data.method !== 'drop') {
317
- return;
318
- }
319
- // Remove dragged range content, remove markers, clean after dragging.
320
- const isMove = getFinalDropEffect(data.dataTransfer) == 'move';
321
- // Whether any content was inserted (insertion might fail if the schema is disallowing some elements
322
- // (for example an image caption allows only the content of a block but not blocks themselves.
323
- // Some integrations might not return valid range (i.e., table pasting).
324
- const isSuccess = !data.resultRange || !data.resultRange.isCollapsed;
325
- this._finalizeDragging(isSuccess && isMove);
326
- }, { priority: 'lowest' });
327
- }
328
- /**
329
- * Adds listeners that add the `draggable` attribute to the elements while the mouse button is down so the dragging could start.
330
- */
331
- _setupDraggableAttributeHandling() {
332
- const editor = this.editor;
333
- const view = editor.editing.view;
334
- const viewDocument = view.document;
335
- // Add the 'draggable' attribute to the widget while pressing the selection handle.
336
- // This is required for widgets to be draggable. In Chrome it will enable dragging text nodes.
337
- this.listenTo(viewDocument, 'mousedown', (evt, data) => {
338
- // The lack of data can be caused by editor tests firing fake mouse events. This should not occur
339
- // in real-life scenarios but this greatly simplifies editor tests that would otherwise fail a lot.
340
- if (env.isAndroid || !data) {
341
- return;
342
- }
343
- this._clearDraggableAttributesDelayed.cancel();
344
- // Check if this is a mousedown over the widget (but not a nested editable).
345
- let draggableElement = findDraggableWidget(data.target);
346
- // Note: There is a limitation that if more than a widget is selected (a widget and some text)
347
- // and dragging starts on the widget, then only the widget is dragged.
348
- // If this was not a widget then we should check if we need to drag some text content.
349
- // In Chrome set a 'draggable' attribute on closest editable to allow immediate dragging of the selected text range.
350
- // In Firefox this is not needed. In Safari it makes the whole editable draggable (not just textual content).
351
- // Disabled in read-only mode because draggable="true" + contenteditable="false" results
352
- // in not firing selectionchange event ever, which makes the selection stuck in read-only mode.
353
- if (env.isBlink && !editor.isReadOnly && !draggableElement && !viewDocument.selection.isCollapsed) {
354
- const selectedElement = viewDocument.selection.getSelectedElement();
355
- if (!selectedElement || !isWidget(selectedElement)) {
356
- draggableElement = viewDocument.selection.editableElement;
357
- }
358
- }
359
- if (draggableElement) {
360
- view.change(writer => {
361
- writer.setAttribute('draggable', 'true', draggableElement);
362
- });
363
- // Keep the reference to the model element in case the view element gets removed while dragging.
364
- this._draggableElement = editor.editing.mapper.toModelElement(draggableElement);
365
- }
366
- });
367
- // Remove the draggable attribute in case no dragging started (only mousedown + mouseup).
368
- this.listenTo(viewDocument, 'mouseup', () => {
369
- if (!env.isAndroid) {
370
- this._clearDraggableAttributesDelayed();
371
- }
372
- });
373
- }
374
- /**
375
- * Removes the `draggable` attribute from the element that was used for dragging.
376
- */
377
- _clearDraggableAttributes() {
378
- const editing = this.editor.editing;
379
- editing.view.change(writer => {
380
- // Remove 'draggable' attribute.
381
- if (this._draggableElement && this._draggableElement.root.rootName != '$graveyard') {
382
- writer.removeAttribute('draggable', editing.mapper.toViewElement(this._draggableElement));
383
- }
384
- this._draggableElement = null;
385
- });
386
- }
387
- /**
388
- * Deletes the dragged content from its original range and clears the dragging state.
389
- *
390
- * @param moved Whether the move succeeded.
391
- */
392
- _finalizeDragging(moved) {
393
- const editor = this.editor;
394
- const model = editor.model;
395
- const dragDropTarget = editor.plugins.get(DragDropTarget);
396
- dragDropTarget.removeDropMarker();
397
- this._clearDraggableAttributes();
398
- if (editor.plugins.has('WidgetToolbarRepository')) {
399
- const widgetToolbarRepository = editor.plugins.get('WidgetToolbarRepository');
400
- widgetToolbarRepository.clearForceDisabled('dragDrop');
401
- }
402
- this._draggingUid = '';
403
- if (this._previewContainer) {
404
- this._previewContainer.remove();
405
- this._previewContainer = undefined;
406
- }
407
- if (!this._draggedRange) {
408
- return;
409
- }
410
- // Delete moved content.
411
- if (moved && this.isEnabled) {
412
- model.change(writer => {
413
- const selection = model.createSelection(this._draggedRange);
414
- model.deleteContent(selection, { doNotAutoparagraph: true });
415
- // Check result selection if it does not require auto-paragraphing of empty container.
416
- const selectionParent = selection.getFirstPosition().parent;
417
- if (selectionParent.isEmpty &&
418
- !model.schema.checkChild(selectionParent, '$text') &&
419
- model.schema.checkChild(selectionParent, 'paragraph')) {
420
- writer.insertElement('paragraph', selectionParent, 0);
421
- }
422
- });
423
- }
424
- this._draggedRange.detach();
425
- this._draggedRange = null;
426
- }
427
- /**
428
- * Sets the dragged source range based on event target and document selection.
429
- */
430
- _prepareDraggedRange(target) {
431
- const editor = this.editor;
432
- const model = editor.model;
433
- const selection = model.document.selection;
434
- // Check if this is dragstart over the widget (but not a nested editable).
435
- const draggableWidget = target ? findDraggableWidget(target) : null;
436
- if (draggableWidget) {
437
- const modelElement = editor.editing.mapper.toModelElement(draggableWidget);
438
- this._draggedRange = LiveRange.fromRange(model.createRangeOn(modelElement));
439
- this._blockMode = model.schema.isBlock(modelElement);
440
- // Disable toolbars so they won't obscure the drop area.
441
- if (editor.plugins.has('WidgetToolbarRepository')) {
442
- const widgetToolbarRepository = editor.plugins.get('WidgetToolbarRepository');
443
- widgetToolbarRepository.forceDisabled('dragDrop');
444
- }
445
- return;
446
- }
447
- // If this was not a widget we should check if we need to drag some text content.
448
- if (selection.isCollapsed && !selection.getFirstPosition().parent.isEmpty) {
449
- return;
450
- }
451
- const blocks = Array.from(selection.getSelectedBlocks());
452
- const draggedRange = selection.getFirstRange();
453
- if (blocks.length == 0) {
454
- this._draggedRange = LiveRange.fromRange(draggedRange);
455
- return;
456
- }
457
- const blockRange = getRangeIncludingFullySelectedParents(model, blocks);
458
- if (blocks.length > 1) {
459
- this._draggedRange = LiveRange.fromRange(blockRange);
460
- this._blockMode = true;
461
- // TODO block mode for dragging from outside editor? or inline? or both?
462
- }
463
- else if (blocks.length == 1) {
464
- const touchesBlockEdges = draggedRange.start.isTouching(blockRange.start) &&
465
- draggedRange.end.isTouching(blockRange.end);
466
- this._draggedRange = LiveRange.fromRange(touchesBlockEdges ? blockRange : draggedRange);
467
- this._blockMode = touchesBlockEdges;
468
- }
469
- model.change(writer => writer.setSelection(this._draggedRange.toRange()));
470
- }
471
- /**
472
- * Updates the dragged preview image.
473
- */
474
- _updatePreview({ dataTransfer, domTarget, clientX }) {
475
- const view = this.editor.editing.view;
476
- const editable = view.document.selection.editableElement;
477
- const domEditable = view.domConverter.mapViewToDom(editable);
478
- const computedStyle = global.window.getComputedStyle(domEditable);
479
- if (!this._previewContainer) {
480
- this._previewContainer = createElement(global.document, 'div', {
481
- style: 'position: fixed; left: -999999px;'
482
- });
483
- global.document.body.appendChild(this._previewContainer);
484
- }
485
- else if (this._previewContainer.firstElementChild) {
486
- this._previewContainer.removeChild(this._previewContainer.firstElementChild);
487
- }
488
- const domRect = new Rect(domEditable);
489
- // If domTarget is inside the editable root, browsers will display the preview correctly by themselves.
490
- if (domEditable.contains(domTarget)) {
491
- return;
492
- }
493
- const domEditablePaddingLeft = parseFloat(computedStyle.paddingLeft);
494
- const preview = createElement(global.document, 'div');
495
- preview.className = 'ck ck-content';
496
- preview.style.width = computedStyle.width;
497
- preview.style.paddingLeft = `${domRect.left - clientX + domEditablePaddingLeft}px`;
498
- /**
499
- * Set white background in drag and drop preview if iOS.
500
- * Check: https://github.com/ckeditor/ckeditor5/issues/15085
501
- */
502
- if (env.isiOS) {
503
- preview.style.backgroundColor = 'white';
504
- }
505
- preview.innerHTML = dataTransfer.getData('text/html');
506
- dataTransfer.setDragImage(preview, 0, 0);
507
- this._previewContainer.appendChild(preview);
508
- }
509
- }
510
- /**
511
- * Returns the drop effect that should be a result of dragging the content.
512
- * This function is handling a quirk when checking the effect in the 'drop' DOM event.
513
- */
514
- function getFinalDropEffect(dataTransfer) {
515
- if (env.isGecko) {
516
- return dataTransfer.dropEffect;
517
- }
518
- return ['all', 'copyMove'].includes(dataTransfer.effectAllowed) ? 'move' : 'copy';
519
- }
520
- /**
521
- * Returns a widget element that should be dragged.
522
- */
523
- function findDraggableWidget(target) {
524
- // This is directly an editable so not a widget for sure.
525
- if (target.is('editableElement')) {
526
- return null;
527
- }
528
- // TODO: Let's have a isWidgetSelectionHandleDomElement() helper in ckeditor5-widget utils.
529
- if (target.hasClass('ck-widget__selection-handle')) {
530
- return target.findAncestor(isWidget);
531
- }
532
- // Direct hit on a widget.
533
- if (isWidget(target)) {
534
- return target;
535
- }
536
- // Find closest ancestor that is either a widget or an editable element...
537
- const ancestor = target.findAncestor(node => isWidget(node) || node.is('editableElement'));
538
- // ...and if closer was the widget then enable dragging it.
539
- if (isWidget(ancestor)) {
540
- return ancestor;
541
- }
542
- return null;
543
- }
544
- /**
545
- * Recursively checks if common parent of provided elements doesn't have any other children. If that's the case,
546
- * it returns range including this parent. Otherwise, it returns only the range from first to last element.
547
- *
548
- * Example:
549
- *
550
- * <blockQuote>
551
- * <paragraph>[Test 1</paragraph>
552
- * <paragraph>Test 2</paragraph>
553
- * <paragraph>Test 3]</paragraph>
554
- * <blockQuote>
555
- *
556
- * Because all elements inside the `blockQuote` are selected, the range is extended to include the `blockQuote` too.
557
- * If only first and second paragraphs would be selected, the range would not include it.
558
- */
559
- function getRangeIncludingFullySelectedParents(model, elements) {
560
- const firstElement = elements[0];
561
- const lastElement = elements[elements.length - 1];
562
- const parent = firstElement.getCommonAncestor(lastElement);
563
- const startPosition = model.createPositionBefore(firstElement);
564
- const endPosition = model.createPositionAfter(lastElement);
565
- if (parent &&
566
- parent.is('element') &&
567
- !model.schema.isLimit(parent)) {
568
- const parentRange = model.createRangeOn(parent);
569
- const touchesStart = startPosition.isTouching(parentRange.start);
570
- const touchesEnd = endPosition.isTouching(parentRange.end);
571
- if (touchesStart && touchesEnd) {
572
- // Selection includes all elements in the parent.
573
- return getRangeIncludingFullySelectedParents(model, [parent]);
574
- }
575
- }
576
- return model.createRange(startPosition, endPosition);
577
- }
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module clipboard/dragdrop
7
+ */
8
+ import { Plugin } from '@ckeditor/ckeditor5-core';
9
+ import { LiveRange, MouseObserver } from '@ckeditor/ckeditor5-engine';
10
+ import { Widget, isWidget } from '@ckeditor/ckeditor5-widget';
11
+ import { env, uid, global, createElement, DomEmitterMixin, delay, Rect } from '@ckeditor/ckeditor5-utils';
12
+ import ClipboardPipeline from './clipboardpipeline';
13
+ import ClipboardObserver from './clipboardobserver';
14
+ import DragDropTarget from './dragdroptarget';
15
+ import DragDropBlockToolbar from './dragdropblocktoolbar';
16
+ import '../theme/clipboard.css';
17
+ // Drag and drop events overview:
18
+ //
19
+ // ┌──────────────────┐
20
+ // │ mousedown │ Sets the draggable attribute.
21
+ // └─────────┬────────┘
22
+ // │
23
+ // └─────────────────────┐
24
+ // │ │
25
+ // │ ┌─────────V────────┐
26
+ // │ │ mouseup │ Dragging did not start, removes the draggable attribute.
27
+ // │ └──────────────────┘
28
+ // │
29
+ // ┌─────────V────────┐ Retrieves the selected model.DocumentFragment
30
+ // │ dragstart │ and converts it to view.DocumentFragment.
31
+ // └─────────┬────────┘
32
+ // │
33
+ // ┌─────────V────────┐ Processes view.DocumentFragment to text/html and text/plain
34
+ // │ clipboardOutput │ and stores the results in data.dataTransfer.
35
+ // └─────────┬────────┘
36
+ // │
37
+ // │ DOM dragover
38
+ // ┌────────────┐
39
+ // │ │
40
+ // ┌─────────V────────┐ │
41
+ // │ dragging │ │ Updates the drop target marker.
42
+ // └─────────┬────────┘ │
43
+ // │ │
44
+ // ┌─────────────└────────────┘
45
+ // │ │ │
46
+ // │ ┌─────────V────────┐ │
47
+ // │ │ dragleave │ │ Removes the drop target marker.
48
+ // │ └─────────┬────────┘ │
49
+ // │ │ │
50
+ // ┌───│─────────────┘ │
51
+ // │ │ │ │
52
+ // │ │ ┌─────────V────────┐ │
53
+ // │ │ │ dragenter │ │ Focuses the editor view.
54
+ // │ │ └─────────┬────────┘ │
55
+ // │ │ │ │
56
+ // │ │ └────────────┘
57
+ // │ │
58
+ // │ └─────────────┐
59
+ // │ │ │
60
+ // │ │ ┌─────────V────────┐
61
+ // └───┐ │ drop │ (The default handler of the clipboard pipeline).
62
+ // │ └─────────┬────────┘
63
+ // │ │
64
+ // │ ┌─────────V────────┐ Resolves the final data.targetRanges.
65
+ // │ │ clipboardInput │ Aborts if dropping on dragged content.
66
+ // │ └─────────┬────────┘
67
+ // │ │
68
+ // │ ┌─────────V────────┐
69
+ // │ │ clipboardInput │ (The default handler of the clipboard pipeline).
70
+ // │ └─────────┬────────┘
71
+ // │ │
72
+ // │ ┌───────────V───────────┐
73
+ // │ │ inputTransformation │ (The default handler of the clipboard pipeline).
74
+ // │ └───────────┬───────────┘
75
+ // │ │
76
+ // │ ┌──────────V──────────┐
77
+ // │ │ contentInsertion │ Updates the document selection to drop range.
78
+ // │ └──────────┬──────────┘
79
+ // │ │
80
+ // │ ┌──────────V──────────┐
81
+ // │ │ contentInsertion │ (The default handler of the clipboard pipeline).
82
+ // │ └──────────┬──────────┘
83
+ // │ │
84
+ // │ ┌──────────V──────────┐
85
+ // │ │ contentInsertion │ Removes the content from the original range if the insertion was successful.
86
+ // │ └──────────┬──────────┘
87
+ // │ │
88
+ // └─────────────┐
89
+ // │
90
+ // ┌─────────V────────┐
91
+ // │ dragend │ Removes the drop marker and cleans the state.
92
+ // └──────────────────┘
93
+ //
94
+ /**
95
+ * The drag and drop feature. It works on top of the {@link module:clipboard/clipboardpipeline~ClipboardPipeline}.
96
+ *
97
+ * Read more about the clipboard integration in the {@glink framework/deep-dive/clipboard clipboard deep-dive} guide.
98
+ *
99
+ * @internal
100
+ */
101
+ export default class DragDrop extends Plugin {
102
+ constructor() {
103
+ super(...arguments);
104
+ /**
105
+ * A delayed callback removing draggable attributes.
106
+ */
107
+ this._clearDraggableAttributesDelayed = delay(() => this._clearDraggableAttributes(), 40);
108
+ /**
109
+ * Whether the dragged content can be dropped only in block context.
110
+ */
111
+ // TODO handle drag from other editor instance
112
+ // TODO configure to use block, inline or both
113
+ this._blockMode = false;
114
+ /**
115
+ * DOM Emitter.
116
+ */
117
+ this._domEmitter = new (DomEmitterMixin())();
118
+ }
119
+ /**
120
+ * @inheritDoc
121
+ */
122
+ static get pluginName() {
123
+ return 'DragDrop';
124
+ }
125
+ /**
126
+ * @inheritDoc
127
+ */
128
+ static get requires() {
129
+ return [ClipboardPipeline, Widget, DragDropTarget, DragDropBlockToolbar];
130
+ }
131
+ /**
132
+ * @inheritDoc
133
+ */
134
+ init() {
135
+ const editor = this.editor;
136
+ const view = editor.editing.view;
137
+ this._draggedRange = null;
138
+ this._draggingUid = '';
139
+ this._draggableElement = null;
140
+ view.addObserver(ClipboardObserver);
141
+ view.addObserver(MouseObserver);
142
+ this._setupDragging();
143
+ this._setupContentInsertionIntegration();
144
+ this._setupClipboardInputIntegration();
145
+ this._setupDraggableAttributeHandling();
146
+ this.listenTo(editor, 'change:isReadOnly', (evt, name, isReadOnly) => {
147
+ if (isReadOnly) {
148
+ this.forceDisabled('readOnlyMode');
149
+ }
150
+ else {
151
+ this.clearForceDisabled('readOnlyMode');
152
+ }
153
+ });
154
+ this.on('change:isEnabled', (evt, name, isEnabled) => {
155
+ if (!isEnabled) {
156
+ this._finalizeDragging(false);
157
+ }
158
+ });
159
+ if (env.isAndroid) {
160
+ this.forceDisabled('noAndroidSupport');
161
+ }
162
+ }
163
+ /**
164
+ * @inheritDoc
165
+ */
166
+ destroy() {
167
+ if (this._draggedRange) {
168
+ this._draggedRange.detach();
169
+ this._draggedRange = null;
170
+ }
171
+ if (this._previewContainer) {
172
+ this._previewContainer.remove();
173
+ }
174
+ this._domEmitter.stopListening();
175
+ this._clearDraggableAttributesDelayed.cancel();
176
+ return super.destroy();
177
+ }
178
+ /**
179
+ * Drag and drop events handling.
180
+ */
181
+ _setupDragging() {
182
+ const editor = this.editor;
183
+ const model = editor.model;
184
+ const view = editor.editing.view;
185
+ const viewDocument = view.document;
186
+ const dragDropTarget = editor.plugins.get(DragDropTarget);
187
+ // The handler for the drag start; it is responsible for setting data transfer object.
188
+ this.listenTo(viewDocument, 'dragstart', (evt, data) => {
189
+ // Don't drag the editable element itself.
190
+ if (data.target && data.target.is('editableElement')) {
191
+ data.preventDefault();
192
+ return;
193
+ }
194
+ this._prepareDraggedRange(data.target);
195
+ if (!this._draggedRange) {
196
+ data.preventDefault();
197
+ return;
198
+ }
199
+ this._draggingUid = uid();
200
+ data.dataTransfer.effectAllowed = this.isEnabled ? 'copyMove' : 'copy';
201
+ data.dataTransfer.setData('application/ckeditor5-dragging-uid', this._draggingUid);
202
+ const draggedSelection = model.createSelection(this._draggedRange.toRange());
203
+ const clipboardPipeline = this.editor.plugins.get('ClipboardPipeline');
204
+ clipboardPipeline._fireOutputTransformationEvent(data.dataTransfer, draggedSelection, 'dragstart');
205
+ const { dataTransfer, domTarget, domEvent } = data;
206
+ const { clientX } = domEvent;
207
+ this._updatePreview({ dataTransfer, domTarget, clientX });
208
+ data.stopPropagation();
209
+ if (!this.isEnabled) {
210
+ this._draggedRange.detach();
211
+ this._draggedRange = null;
212
+ this._draggingUid = '';
213
+ }
214
+ }, { priority: 'low' });
215
+ // The handler for finalizing drag and drop. It should always be triggered after dragging completes
216
+ // even if it was completed in a different application.
217
+ // Note: This is not fired if source text node got removed while downcasting a marker.
218
+ this.listenTo(viewDocument, 'dragend', (evt, data) => {
219
+ this._finalizeDragging(!data.dataTransfer.isCanceled && data.dataTransfer.dropEffect == 'move');
220
+ }, { priority: 'low' });
221
+ // Reset block dragging mode even if dropped outside the editable.
222
+ this._domEmitter.listenTo(global.document, 'dragend', () => {
223
+ this._blockMode = false;
224
+ }, { useCapture: true });
225
+ // Dragging over the editable.
226
+ this.listenTo(viewDocument, 'dragenter', () => {
227
+ if (!this.isEnabled) {
228
+ return;
229
+ }
230
+ view.focus();
231
+ });
232
+ // Dragging out of the editable.
233
+ this.listenTo(viewDocument, 'dragleave', () => {
234
+ // We do not know if the mouse left the editor or just some element in it, so let us wait a few milliseconds
235
+ // to check if 'dragover' is not fired.
236
+ dragDropTarget.removeDropMarkerDelayed();
237
+ });
238
+ // Handler for moving dragged content over the target area.
239
+ this.listenTo(viewDocument, 'dragging', (evt, data) => {
240
+ if (!this.isEnabled) {
241
+ data.dataTransfer.dropEffect = 'none';
242
+ return;
243
+ }
244
+ const { clientX, clientY } = data.domEvent;
245
+ dragDropTarget.updateDropMarker(data.target, data.targetRanges, clientX, clientY, this._blockMode, this._draggedRange);
246
+ // If this is content being dragged from another editor, moving out of current editor instance
247
+ // is not possible until 'dragend' event case will be fixed.
248
+ if (!this._draggedRange) {
249
+ data.dataTransfer.dropEffect = 'copy';
250
+ }
251
+ // In Firefox it is already set and effect allowed remains the same as originally set.
252
+ if (!env.isGecko) {
253
+ if (data.dataTransfer.effectAllowed == 'copy') {
254
+ data.dataTransfer.dropEffect = 'copy';
255
+ }
256
+ else if (['all', 'copyMove'].includes(data.dataTransfer.effectAllowed)) {
257
+ data.dataTransfer.dropEffect = 'move';
258
+ }
259
+ }
260
+ evt.stop();
261
+ }, { priority: 'low' });
262
+ }
263
+ /**
264
+ * Integration with the `clipboardInput` event.
265
+ */
266
+ _setupClipboardInputIntegration() {
267
+ const editor = this.editor;
268
+ const view = editor.editing.view;
269
+ const viewDocument = view.document;
270
+ const dragDropTarget = editor.plugins.get(DragDropTarget);
271
+ // Update the event target ranges and abort dropping if dropping over itself.
272
+ this.listenTo(viewDocument, 'clipboardInput', (evt, data) => {
273
+ if (data.method != 'drop') {
274
+ return;
275
+ }
276
+ const { clientX, clientY } = data.domEvent;
277
+ const targetRange = dragDropTarget.getFinalDropRange(data.target, data.targetRanges, clientX, clientY, this._blockMode, this._draggedRange);
278
+ if (!targetRange) {
279
+ this._finalizeDragging(false);
280
+ evt.stop();
281
+ return;
282
+ }
283
+ // Since we cannot rely on the drag end event, we must check if the local drag range is from the current drag and drop
284
+ // or it is from some previous not cleared one.
285
+ if (this._draggedRange && this._draggingUid != data.dataTransfer.getData('application/ckeditor5-dragging-uid')) {
286
+ this._draggedRange.detach();
287
+ this._draggedRange = null;
288
+ this._draggingUid = '';
289
+ }
290
+ // Do not do anything if some content was dragged within the same document to the same position.
291
+ const isMove = getFinalDropEffect(data.dataTransfer) == 'move';
292
+ if (isMove && this._draggedRange && this._draggedRange.containsRange(targetRange, true)) {
293
+ this._finalizeDragging(false);
294
+ evt.stop();
295
+ return;
296
+ }
297
+ // Override the target ranges with the one adjusted to the best one for a drop.
298
+ data.targetRanges = [editor.editing.mapper.toViewRange(targetRange)];
299
+ }, { priority: 'high' });
300
+ }
301
+ /**
302
+ * Integration with the `contentInsertion` event of the clipboard pipeline.
303
+ */
304
+ _setupContentInsertionIntegration() {
305
+ const clipboardPipeline = this.editor.plugins.get(ClipboardPipeline);
306
+ clipboardPipeline.on('contentInsertion', (evt, data) => {
307
+ if (!this.isEnabled || data.method !== 'drop') {
308
+ return;
309
+ }
310
+ // Update the selection to the target range in the same change block to avoid selection post-fixing
311
+ // and to be able to clone text attributes for plain text dropping.
312
+ const ranges = data.targetRanges.map(viewRange => this.editor.editing.mapper.toModelRange(viewRange));
313
+ this.editor.model.change(writer => writer.setSelection(ranges));
314
+ }, { priority: 'high' });
315
+ clipboardPipeline.on('contentInsertion', (evt, data) => {
316
+ if (!this.isEnabled || data.method !== 'drop') {
317
+ return;
318
+ }
319
+ // Remove dragged range content, remove markers, clean after dragging.
320
+ const isMove = getFinalDropEffect(data.dataTransfer) == 'move';
321
+ // Whether any content was inserted (insertion might fail if the schema is disallowing some elements
322
+ // (for example an image caption allows only the content of a block but not blocks themselves.
323
+ // Some integrations might not return valid range (i.e., table pasting).
324
+ const isSuccess = !data.resultRange || !data.resultRange.isCollapsed;
325
+ this._finalizeDragging(isSuccess && isMove);
326
+ }, { priority: 'lowest' });
327
+ }
328
+ /**
329
+ * Adds listeners that add the `draggable` attribute to the elements while the mouse button is down so the dragging could start.
330
+ */
331
+ _setupDraggableAttributeHandling() {
332
+ const editor = this.editor;
333
+ const view = editor.editing.view;
334
+ const viewDocument = view.document;
335
+ // Add the 'draggable' attribute to the widget while pressing the selection handle.
336
+ // This is required for widgets to be draggable. In Chrome it will enable dragging text nodes.
337
+ this.listenTo(viewDocument, 'mousedown', (evt, data) => {
338
+ // The lack of data can be caused by editor tests firing fake mouse events. This should not occur
339
+ // in real-life scenarios but this greatly simplifies editor tests that would otherwise fail a lot.
340
+ if (env.isAndroid || !data) {
341
+ return;
342
+ }
343
+ this._clearDraggableAttributesDelayed.cancel();
344
+ // Check if this is a mousedown over the widget (but not a nested editable).
345
+ let draggableElement = findDraggableWidget(data.target);
346
+ // Note: There is a limitation that if more than a widget is selected (a widget and some text)
347
+ // and dragging starts on the widget, then only the widget is dragged.
348
+ // If this was not a widget then we should check if we need to drag some text content.
349
+ // In Chrome set a 'draggable' attribute on closest editable to allow immediate dragging of the selected text range.
350
+ // In Firefox this is not needed. In Safari it makes the whole editable draggable (not just textual content).
351
+ // Disabled in read-only mode because draggable="true" + contenteditable="false" results
352
+ // in not firing selectionchange event ever, which makes the selection stuck in read-only mode.
353
+ if (env.isBlink && !editor.isReadOnly && !draggableElement && !viewDocument.selection.isCollapsed) {
354
+ const selectedElement = viewDocument.selection.getSelectedElement();
355
+ if (!selectedElement || !isWidget(selectedElement)) {
356
+ draggableElement = viewDocument.selection.editableElement;
357
+ }
358
+ }
359
+ if (draggableElement) {
360
+ view.change(writer => {
361
+ writer.setAttribute('draggable', 'true', draggableElement);
362
+ });
363
+ // Keep the reference to the model element in case the view element gets removed while dragging.
364
+ this._draggableElement = editor.editing.mapper.toModelElement(draggableElement);
365
+ }
366
+ });
367
+ // Remove the draggable attribute in case no dragging started (only mousedown + mouseup).
368
+ this.listenTo(viewDocument, 'mouseup', () => {
369
+ if (!env.isAndroid) {
370
+ this._clearDraggableAttributesDelayed();
371
+ }
372
+ });
373
+ }
374
+ /**
375
+ * Removes the `draggable` attribute from the element that was used for dragging.
376
+ */
377
+ _clearDraggableAttributes() {
378
+ const editing = this.editor.editing;
379
+ editing.view.change(writer => {
380
+ // Remove 'draggable' attribute.
381
+ if (this._draggableElement && this._draggableElement.root.rootName != '$graveyard') {
382
+ writer.removeAttribute('draggable', editing.mapper.toViewElement(this._draggableElement));
383
+ }
384
+ this._draggableElement = null;
385
+ });
386
+ }
387
+ /**
388
+ * Deletes the dragged content from its original range and clears the dragging state.
389
+ *
390
+ * @param moved Whether the move succeeded.
391
+ */
392
+ _finalizeDragging(moved) {
393
+ const editor = this.editor;
394
+ const model = editor.model;
395
+ const dragDropTarget = editor.plugins.get(DragDropTarget);
396
+ dragDropTarget.removeDropMarker();
397
+ this._clearDraggableAttributes();
398
+ if (editor.plugins.has('WidgetToolbarRepository')) {
399
+ const widgetToolbarRepository = editor.plugins.get('WidgetToolbarRepository');
400
+ widgetToolbarRepository.clearForceDisabled('dragDrop');
401
+ }
402
+ this._draggingUid = '';
403
+ if (this._previewContainer) {
404
+ this._previewContainer.remove();
405
+ this._previewContainer = undefined;
406
+ }
407
+ if (!this._draggedRange) {
408
+ return;
409
+ }
410
+ // Delete moved content.
411
+ if (moved && this.isEnabled) {
412
+ model.change(writer => {
413
+ const selection = model.createSelection(this._draggedRange);
414
+ model.deleteContent(selection, { doNotAutoparagraph: true });
415
+ // Check result selection if it does not require auto-paragraphing of empty container.
416
+ const selectionParent = selection.getFirstPosition().parent;
417
+ if (selectionParent.isEmpty &&
418
+ !model.schema.checkChild(selectionParent, '$text') &&
419
+ model.schema.checkChild(selectionParent, 'paragraph')) {
420
+ writer.insertElement('paragraph', selectionParent, 0);
421
+ }
422
+ });
423
+ }
424
+ this._draggedRange.detach();
425
+ this._draggedRange = null;
426
+ }
427
+ /**
428
+ * Sets the dragged source range based on event target and document selection.
429
+ */
430
+ _prepareDraggedRange(target) {
431
+ const editor = this.editor;
432
+ const model = editor.model;
433
+ const selection = model.document.selection;
434
+ // Check if this is dragstart over the widget (but not a nested editable).
435
+ const draggableWidget = target ? findDraggableWidget(target) : null;
436
+ if (draggableWidget) {
437
+ const modelElement = editor.editing.mapper.toModelElement(draggableWidget);
438
+ this._draggedRange = LiveRange.fromRange(model.createRangeOn(modelElement));
439
+ this._blockMode = model.schema.isBlock(modelElement);
440
+ // Disable toolbars so they won't obscure the drop area.
441
+ if (editor.plugins.has('WidgetToolbarRepository')) {
442
+ const widgetToolbarRepository = editor.plugins.get('WidgetToolbarRepository');
443
+ widgetToolbarRepository.forceDisabled('dragDrop');
444
+ }
445
+ return;
446
+ }
447
+ // If this was not a widget we should check if we need to drag some text content.
448
+ if (selection.isCollapsed && !selection.getFirstPosition().parent.isEmpty) {
449
+ return;
450
+ }
451
+ const blocks = Array.from(selection.getSelectedBlocks());
452
+ const draggedRange = selection.getFirstRange();
453
+ if (blocks.length == 0) {
454
+ this._draggedRange = LiveRange.fromRange(draggedRange);
455
+ return;
456
+ }
457
+ const blockRange = getRangeIncludingFullySelectedParents(model, blocks);
458
+ if (blocks.length > 1) {
459
+ this._draggedRange = LiveRange.fromRange(blockRange);
460
+ this._blockMode = true;
461
+ // TODO block mode for dragging from outside editor? or inline? or both?
462
+ }
463
+ else if (blocks.length == 1) {
464
+ const touchesBlockEdges = draggedRange.start.isTouching(blockRange.start) &&
465
+ draggedRange.end.isTouching(blockRange.end);
466
+ this._draggedRange = LiveRange.fromRange(touchesBlockEdges ? blockRange : draggedRange);
467
+ this._blockMode = touchesBlockEdges;
468
+ }
469
+ model.change(writer => writer.setSelection(this._draggedRange.toRange()));
470
+ }
471
+ /**
472
+ * Updates the dragged preview image.
473
+ */
474
+ _updatePreview({ dataTransfer, domTarget, clientX }) {
475
+ const view = this.editor.editing.view;
476
+ const editable = view.document.selection.editableElement;
477
+ const domEditable = view.domConverter.mapViewToDom(editable);
478
+ const computedStyle = global.window.getComputedStyle(domEditable);
479
+ if (!this._previewContainer) {
480
+ this._previewContainer = createElement(global.document, 'div', {
481
+ style: 'position: fixed; left: -999999px;'
482
+ });
483
+ global.document.body.appendChild(this._previewContainer);
484
+ }
485
+ else if (this._previewContainer.firstElementChild) {
486
+ this._previewContainer.removeChild(this._previewContainer.firstElementChild);
487
+ }
488
+ const domRect = new Rect(domEditable);
489
+ // If domTarget is inside the editable root, browsers will display the preview correctly by themselves.
490
+ if (domEditable.contains(domTarget)) {
491
+ return;
492
+ }
493
+ const domEditablePaddingLeft = parseFloat(computedStyle.paddingLeft);
494
+ const preview = createElement(global.document, 'div');
495
+ preview.className = 'ck ck-content';
496
+ preview.style.width = computedStyle.width;
497
+ preview.style.paddingLeft = `${domRect.left - clientX + domEditablePaddingLeft}px`;
498
+ /**
499
+ * Set white background in drag and drop preview if iOS.
500
+ * Check: https://github.com/ckeditor/ckeditor5/issues/15085
501
+ */
502
+ if (env.isiOS) {
503
+ preview.style.backgroundColor = 'white';
504
+ }
505
+ preview.innerHTML = dataTransfer.getData('text/html');
506
+ dataTransfer.setDragImage(preview, 0, 0);
507
+ this._previewContainer.appendChild(preview);
508
+ }
509
+ }
510
+ /**
511
+ * Returns the drop effect that should be a result of dragging the content.
512
+ * This function is handling a quirk when checking the effect in the 'drop' DOM event.
513
+ */
514
+ function getFinalDropEffect(dataTransfer) {
515
+ if (env.isGecko) {
516
+ return dataTransfer.dropEffect;
517
+ }
518
+ return ['all', 'copyMove'].includes(dataTransfer.effectAllowed) ? 'move' : 'copy';
519
+ }
520
+ /**
521
+ * Returns a widget element that should be dragged.
522
+ */
523
+ function findDraggableWidget(target) {
524
+ // This is directly an editable so not a widget for sure.
525
+ if (target.is('editableElement')) {
526
+ return null;
527
+ }
528
+ // TODO: Let's have a isWidgetSelectionHandleDomElement() helper in ckeditor5-widget utils.
529
+ if (target.hasClass('ck-widget__selection-handle')) {
530
+ return target.findAncestor(isWidget);
531
+ }
532
+ // Direct hit on a widget.
533
+ if (isWidget(target)) {
534
+ return target;
535
+ }
536
+ // Find closest ancestor that is either a widget or an editable element...
537
+ const ancestor = target.findAncestor(node => isWidget(node) || node.is('editableElement'));
538
+ // ...and if closer was the widget then enable dragging it.
539
+ if (isWidget(ancestor)) {
540
+ return ancestor;
541
+ }
542
+ return null;
543
+ }
544
+ /**
545
+ * Recursively checks if common parent of provided elements doesn't have any other children. If that's the case,
546
+ * it returns range including this parent. Otherwise, it returns only the range from first to last element.
547
+ *
548
+ * Example:
549
+ *
550
+ * <blockQuote>
551
+ * <paragraph>[Test 1</paragraph>
552
+ * <paragraph>Test 2</paragraph>
553
+ * <paragraph>Test 3]</paragraph>
554
+ * <blockQuote>
555
+ *
556
+ * Because all elements inside the `blockQuote` are selected, the range is extended to include the `blockQuote` too.
557
+ * If only first and second paragraphs would be selected, the range would not include it.
558
+ */
559
+ function getRangeIncludingFullySelectedParents(model, elements) {
560
+ const firstElement = elements[0];
561
+ const lastElement = elements[elements.length - 1];
562
+ const parent = firstElement.getCommonAncestor(lastElement);
563
+ const startPosition = model.createPositionBefore(firstElement);
564
+ const endPosition = model.createPositionAfter(lastElement);
565
+ if (parent &&
566
+ parent.is('element') &&
567
+ !model.schema.isLimit(parent)) {
568
+ const parentRange = model.createRangeOn(parent);
569
+ const touchesStart = startPosition.isTouching(parentRange.start);
570
+ const touchesEnd = endPosition.isTouching(parentRange.end);
571
+ if (touchesStart && touchesEnd) {
572
+ // Selection includes all elements in the parent.
573
+ return getRangeIncludingFullySelectedParents(model, [parent]);
574
+ }
575
+ }
576
+ return model.createRange(startPosition, endPosition);
577
+ }