@ckeditor/ckeditor5-clipboard 39.0.2 → 40.1.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/LICENSE.md +3 -3
- package/package.json +6 -6
- package/src/augmentation.d.ts +1 -2
- package/src/clipboardpipeline.d.ts +40 -1
- package/src/clipboardpipeline.js +28 -7
- package/src/dragdrop.d.ts +22 -20
- package/src/dragdrop.js +180 -258
- package/src/dragdropblocktoolbar.d.ts +1 -1
- package/src/dragdropblocktoolbar.js +13 -6
- package/src/dragdroptarget.d.ts +3 -3
- package/src/dragdroptarget.js +30 -19
- package/src/index.d.ts +1 -2
- package/src/index.js +0 -1
- package/src/utils/plaintexttohtml.js +2 -0
- package/src/utils/viewtoplaintext.js +48 -31
- package/src/dragdropexperimental.d.ts +0 -101
- package/src/dragdropexperimental.js +0 -522
package/src/dragdrop.js
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
9
9
|
import { LiveRange, MouseObserver } from '@ckeditor/ckeditor5-engine';
|
|
10
10
|
import { Widget, isWidget } from '@ckeditor/ckeditor5-widget';
|
|
11
|
-
import { env, uid, delay } from '@ckeditor/ckeditor5-utils';
|
|
11
|
+
import { env, uid, global, createElement, DomEmitterMixin, delay, Rect } from '@ckeditor/ckeditor5-utils';
|
|
12
12
|
import ClipboardPipeline from './clipboardpipeline';
|
|
13
13
|
import ClipboardObserver from './clipboardobserver';
|
|
14
|
-
import
|
|
14
|
+
import DragDropTarget from './dragdroptarget';
|
|
15
|
+
import DragDropBlockToolbar from './dragdropblocktoolbar';
|
|
15
16
|
import '../theme/clipboard.css';
|
|
16
17
|
// Drag and drop events overview:
|
|
17
18
|
//
|
|
@@ -94,8 +95,27 @@ import '../theme/clipboard.css';
|
|
|
94
95
|
* The drag and drop feature. It works on top of the {@link module:clipboard/clipboardpipeline~ClipboardPipeline}.
|
|
95
96
|
*
|
|
96
97
|
* Read more about the clipboard integration in the {@glink framework/deep-dive/clipboard clipboard deep-dive} guide.
|
|
98
|
+
*
|
|
99
|
+
* @internal
|
|
97
100
|
*/
|
|
98
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
|
+
}
|
|
99
119
|
/**
|
|
100
120
|
* @inheritDoc
|
|
101
121
|
*/
|
|
@@ -106,7 +126,7 @@ export default class DragDrop extends Plugin {
|
|
|
106
126
|
* @inheritDoc
|
|
107
127
|
*/
|
|
108
128
|
static get requires() {
|
|
109
|
-
return [ClipboardPipeline, Widget];
|
|
129
|
+
return [ClipboardPipeline, Widget, DragDropTarget, DragDropBlockToolbar];
|
|
110
130
|
}
|
|
111
131
|
/**
|
|
112
132
|
* @inheritDoc
|
|
@@ -117,19 +137,11 @@ export default class DragDrop extends Plugin {
|
|
|
117
137
|
this._draggedRange = null;
|
|
118
138
|
this._draggingUid = '';
|
|
119
139
|
this._draggableElement = null;
|
|
120
|
-
this._updateDropMarkerThrottled = throttle(targetRange => this._updateDropMarker(targetRange), 40);
|
|
121
|
-
this._removeDropMarkerDelayed = delay(() => this._removeDropMarker(), 40);
|
|
122
|
-
this._clearDraggableAttributesDelayed = delay(() => this._clearDraggableAttributes(), 40);
|
|
123
|
-
if (editor.plugins.has('DragDropExperimental')) {
|
|
124
|
-
this.forceDisabled('DragDropExperimental');
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
140
|
view.addObserver(ClipboardObserver);
|
|
128
141
|
view.addObserver(MouseObserver);
|
|
129
142
|
this._setupDragging();
|
|
130
143
|
this._setupContentInsertionIntegration();
|
|
131
144
|
this._setupClipboardInputIntegration();
|
|
132
|
-
this._setupDropMarker();
|
|
133
145
|
this._setupDraggableAttributeHandling();
|
|
134
146
|
this.listenTo(editor, 'change:isReadOnly', (evt, name, isReadOnly) => {
|
|
135
147
|
if (isReadOnly) {
|
|
@@ -156,8 +168,10 @@ export default class DragDrop extends Plugin {
|
|
|
156
168
|
this._draggedRange.detach();
|
|
157
169
|
this._draggedRange = null;
|
|
158
170
|
}
|
|
159
|
-
this.
|
|
160
|
-
|
|
171
|
+
if (this._previewContainer) {
|
|
172
|
+
this._previewContainer.remove();
|
|
173
|
+
}
|
|
174
|
+
this._domEmitter.stopListening();
|
|
161
175
|
this._clearDraggableAttributesDelayed.cancel();
|
|
162
176
|
return super.destroy();
|
|
163
177
|
}
|
|
@@ -167,54 +181,32 @@ export default class DragDrop extends Plugin {
|
|
|
167
181
|
_setupDragging() {
|
|
168
182
|
const editor = this.editor;
|
|
169
183
|
const model = editor.model;
|
|
170
|
-
const modelDocument = model.document;
|
|
171
184
|
const view = editor.editing.view;
|
|
172
185
|
const viewDocument = view.document;
|
|
186
|
+
const dragDropTarget = editor.plugins.get(DragDropTarget);
|
|
173
187
|
// The handler for the drag start; it is responsible for setting data transfer object.
|
|
174
188
|
this.listenTo(viewDocument, 'dragstart', (evt, data) => {
|
|
175
|
-
const selection = modelDocument.selection;
|
|
176
189
|
// Don't drag the editable element itself.
|
|
177
190
|
if (data.target && data.target.is('editableElement')) {
|
|
178
191
|
data.preventDefault();
|
|
179
192
|
return;
|
|
180
193
|
}
|
|
181
|
-
|
|
182
|
-
// selection outline, WTA buttons, etc.
|
|
183
|
-
// data.dataTransfer._native.setDragImage( data.domTarget, 0, 0 );
|
|
184
|
-
// Check if this is dragstart over the widget (but not a nested editable).
|
|
185
|
-
const draggableWidget = data.target ? findDraggableWidget(data.target) : null;
|
|
186
|
-
if (draggableWidget) {
|
|
187
|
-
const modelElement = editor.editing.mapper.toModelElement(draggableWidget);
|
|
188
|
-
this._draggedRange = LiveRange.fromRange(model.createRangeOn(modelElement));
|
|
189
|
-
// Disable toolbars so they won't obscure the drop area.
|
|
190
|
-
if (editor.plugins.has('WidgetToolbarRepository')) {
|
|
191
|
-
const widgetToolbarRepository = editor.plugins.get('WidgetToolbarRepository');
|
|
192
|
-
widgetToolbarRepository.forceDisabled('dragDrop');
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
// If this was not a widget we should check if we need to drag some text content.
|
|
196
|
-
else if (!viewDocument.selection.isCollapsed) {
|
|
197
|
-
const selectedElement = viewDocument.selection.getSelectedElement();
|
|
198
|
-
if (!selectedElement || !isWidget(selectedElement)) {
|
|
199
|
-
this._draggedRange = LiveRange.fromRange(selection.getFirstRange());
|
|
200
|
-
}
|
|
201
|
-
}
|
|
194
|
+
this._prepareDraggedRange(data.target);
|
|
202
195
|
if (!this._draggedRange) {
|
|
203
196
|
data.preventDefault();
|
|
204
197
|
return;
|
|
205
198
|
}
|
|
206
199
|
this._draggingUid = uid();
|
|
207
|
-
|
|
208
|
-
data.dataTransfer.effectAllowed = canEditAtDraggedRange ? 'copyMove' : 'copy';
|
|
200
|
+
data.dataTransfer.effectAllowed = this.isEnabled ? 'copyMove' : 'copy';
|
|
209
201
|
data.dataTransfer.setData('application/ckeditor5-dragging-uid', this._draggingUid);
|
|
210
202
|
const draggedSelection = model.createSelection(this._draggedRange.toRange());
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (!
|
|
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) {
|
|
218
210
|
this._draggedRange.detach();
|
|
219
211
|
this._draggedRange = null;
|
|
220
212
|
this._draggingUid = '';
|
|
@@ -226,6 +218,10 @@ export default class DragDrop extends Plugin {
|
|
|
226
218
|
this.listenTo(viewDocument, 'dragend', (evt, data) => {
|
|
227
219
|
this._finalizeDragging(!data.dataTransfer.isCanceled && data.dataTransfer.dropEffect == 'move');
|
|
228
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 });
|
|
229
225
|
// Dragging over the editable.
|
|
230
226
|
this.listenTo(viewDocument, 'dragenter', () => {
|
|
231
227
|
if (!this.isEnabled) {
|
|
@@ -237,7 +233,7 @@ export default class DragDrop extends Plugin {
|
|
|
237
233
|
this.listenTo(viewDocument, 'dragleave', () => {
|
|
238
234
|
// We do not know if the mouse left the editor or just some element in it, so let us wait a few milliseconds
|
|
239
235
|
// to check if 'dragover' is not fired.
|
|
240
|
-
|
|
236
|
+
dragDropTarget.removeDropMarkerDelayed();
|
|
241
237
|
});
|
|
242
238
|
// Handler for moving dragged content over the target area.
|
|
243
239
|
this.listenTo(viewDocument, 'dragging', (evt, data) => {
|
|
@@ -245,13 +241,8 @@ export default class DragDrop extends Plugin {
|
|
|
245
241
|
data.dataTransfer.dropEffect = 'none';
|
|
246
242
|
return;
|
|
247
243
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// Do not drop if target place is not editable.
|
|
251
|
-
if (!editor.model.canEditAt(targetRange)) {
|
|
252
|
-
data.dataTransfer.dropEffect = 'none';
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
244
|
+
const { clientX, clientY } = data.domEvent;
|
|
245
|
+
dragDropTarget.updateDropMarker(data.target, data.targetRanges, clientX, clientY, this._blockMode, this._draggedRange);
|
|
255
246
|
// If this is content being dragged from another editor, moving out of current editor instance
|
|
256
247
|
// is not possible until 'dragend' event case will be fixed.
|
|
257
248
|
if (!this._draggedRange) {
|
|
@@ -266,10 +257,7 @@ export default class DragDrop extends Plugin {
|
|
|
266
257
|
data.dataTransfer.dropEffect = 'move';
|
|
267
258
|
}
|
|
268
259
|
}
|
|
269
|
-
|
|
270
|
-
if (targetRange) {
|
|
271
|
-
this._updateDropMarkerThrottled(targetRange);
|
|
272
|
-
}
|
|
260
|
+
evt.stop();
|
|
273
261
|
}, { priority: 'low' });
|
|
274
262
|
}
|
|
275
263
|
/**
|
|
@@ -279,17 +267,15 @@ export default class DragDrop extends Plugin {
|
|
|
279
267
|
const editor = this.editor;
|
|
280
268
|
const view = editor.editing.view;
|
|
281
269
|
const viewDocument = view.document;
|
|
270
|
+
const dragDropTarget = editor.plugins.get(DragDropTarget);
|
|
282
271
|
// Update the event target ranges and abort dropping if dropping over itself.
|
|
283
272
|
this.listenTo(viewDocument, 'clipboardInput', (evt, data) => {
|
|
284
273
|
if (data.method != 'drop') {
|
|
285
274
|
return;
|
|
286
275
|
}
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
this._removeDropMarker();
|
|
291
|
-
/* istanbul ignore if -- @preserve */
|
|
292
|
-
if (!targetRange || !editor.model.canEditAt(targetRange)) {
|
|
276
|
+
const { clientX, clientY } = data.domEvent;
|
|
277
|
+
const targetRange = dragDropTarget.getFinalDropRange(data.target, data.targetRanges, clientX, clientY, this._blockMode, this._draggedRange);
|
|
278
|
+
if (!targetRange) {
|
|
293
279
|
this._finalizeDragging(false);
|
|
294
280
|
evt.stop();
|
|
295
281
|
return;
|
|
@@ -364,13 +350,10 @@ export default class DragDrop extends Plugin {
|
|
|
364
350
|
// In Firefox this is not needed. In Safari it makes the whole editable draggable (not just textual content).
|
|
365
351
|
// Disabled in read-only mode because draggable="true" + contenteditable="false" results
|
|
366
352
|
// in not firing selectionchange event ever, which makes the selection stuck in read-only mode.
|
|
367
|
-
if (env.isBlink && !draggableElement && !viewDocument.selection.isCollapsed) {
|
|
353
|
+
if (env.isBlink && !editor.isReadOnly && !draggableElement && !viewDocument.selection.isCollapsed) {
|
|
368
354
|
const selectedElement = viewDocument.selection.getSelectedElement();
|
|
369
355
|
if (!selectedElement || !isWidget(selectedElement)) {
|
|
370
|
-
|
|
371
|
-
if (editableElement && !editableElement.isReadOnly) {
|
|
372
|
-
draggableElement = editableElement;
|
|
373
|
-
}
|
|
356
|
+
draggableElement = viewDocument.selection.editableElement;
|
|
374
357
|
}
|
|
375
358
|
}
|
|
376
359
|
if (draggableElement) {
|
|
@@ -401,71 +384,6 @@ export default class DragDrop extends Plugin {
|
|
|
401
384
|
this._draggableElement = null;
|
|
402
385
|
});
|
|
403
386
|
}
|
|
404
|
-
/**
|
|
405
|
-
* Creates downcast conversion for the drop target marker.
|
|
406
|
-
*/
|
|
407
|
-
_setupDropMarker() {
|
|
408
|
-
const editor = this.editor;
|
|
409
|
-
// Drop marker conversion for hovering over widgets.
|
|
410
|
-
editor.conversion.for('editingDowncast').markerToHighlight({
|
|
411
|
-
model: 'drop-target',
|
|
412
|
-
view: {
|
|
413
|
-
classes: ['ck-clipboard-drop-target-range']
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
// Drop marker conversion for in text drop target.
|
|
417
|
-
editor.conversion.for('editingDowncast').markerToElement({
|
|
418
|
-
model: 'drop-target',
|
|
419
|
-
view: (data, { writer }) => {
|
|
420
|
-
const inText = editor.model.schema.checkChild(data.markerRange.start, '$text');
|
|
421
|
-
if (!inText) {
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
return writer.createUIElement('span', { class: 'ck ck-clipboard-drop-target-position' }, function (domDocument) {
|
|
425
|
-
const domElement = this.toDomElement(domDocument);
|
|
426
|
-
// Using word joiner to make this marker as high as text and also making text not break on marker.
|
|
427
|
-
domElement.append('\u2060', domDocument.createElement('span'), '\u2060');
|
|
428
|
-
return domElement;
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Updates the drop target marker to the provided range.
|
|
435
|
-
*
|
|
436
|
-
* @param targetRange The range to set the marker to.
|
|
437
|
-
*/
|
|
438
|
-
_updateDropMarker(targetRange) {
|
|
439
|
-
const editor = this.editor;
|
|
440
|
-
const markers = editor.model.markers;
|
|
441
|
-
editor.model.change(writer => {
|
|
442
|
-
if (markers.has('drop-target')) {
|
|
443
|
-
if (!markers.get('drop-target').getRange().isEqual(targetRange)) {
|
|
444
|
-
writer.updateMarker('drop-target', { range: targetRange });
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
writer.addMarker('drop-target', {
|
|
449
|
-
range: targetRange,
|
|
450
|
-
usingOperation: false,
|
|
451
|
-
affectsData: false
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Removes the drop target marker.
|
|
458
|
-
*/
|
|
459
|
-
_removeDropMarker() {
|
|
460
|
-
const model = this.editor.model;
|
|
461
|
-
this._removeDropMarkerDelayed.cancel();
|
|
462
|
-
this._updateDropMarkerThrottled.cancel();
|
|
463
|
-
if (model.markers.has('drop-target')) {
|
|
464
|
-
model.change(writer => {
|
|
465
|
-
writer.removeMarker('drop-target');
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
387
|
/**
|
|
470
388
|
* Deletes the dragged content from its original range and clears the dragging state.
|
|
471
389
|
*
|
|
@@ -474,150 +392,120 @@ export default class DragDrop extends Plugin {
|
|
|
474
392
|
_finalizeDragging(moved) {
|
|
475
393
|
const editor = this.editor;
|
|
476
394
|
const model = editor.model;
|
|
477
|
-
|
|
395
|
+
const dragDropTarget = editor.plugins.get(DragDropTarget);
|
|
396
|
+
dragDropTarget.removeDropMarker();
|
|
478
397
|
this._clearDraggableAttributes();
|
|
479
398
|
if (editor.plugins.has('WidgetToolbarRepository')) {
|
|
480
399
|
const widgetToolbarRepository = editor.plugins.get('WidgetToolbarRepository');
|
|
481
400
|
widgetToolbarRepository.clearForceDisabled('dragDrop');
|
|
482
401
|
}
|
|
483
402
|
this._draggingUid = '';
|
|
403
|
+
if (this._previewContainer) {
|
|
404
|
+
this._previewContainer.remove();
|
|
405
|
+
this._previewContainer = undefined;
|
|
406
|
+
}
|
|
484
407
|
if (!this._draggedRange) {
|
|
485
408
|
return;
|
|
486
409
|
}
|
|
487
410
|
// Delete moved content.
|
|
488
411
|
if (moved && this.isEnabled) {
|
|
489
|
-
model.
|
|
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
|
+
});
|
|
490
423
|
}
|
|
491
424
|
this._draggedRange.detach();
|
|
492
425
|
this._draggedRange = null;
|
|
493
426
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
// Find target model element and position.
|
|
514
|
-
const targetModelElement = getClosestMappedModelElement(editor, targetViewElement);
|
|
515
|
-
const targetModelPosition = targetViewPosition ? mapper.toModelPosition(targetViewPosition) : null;
|
|
516
|
-
// There is no target position while hovering over an empty table cell.
|
|
517
|
-
// In Safari, target position can be empty while hovering over a widget (e.g., a page-break).
|
|
518
|
-
// Find the drop position inside the element.
|
|
519
|
-
if (!targetModelPosition) {
|
|
520
|
-
return findDropTargetRangeInElement(editor, targetModelElement);
|
|
521
|
-
}
|
|
522
|
-
// Check if target position is between blocks and adjust drop position to the next object.
|
|
523
|
-
// This is because while hovering over a root element next to a widget the target position can jump in crazy places.
|
|
524
|
-
range = findDropTargetRangeBetweenBlocks(editor, targetModelPosition, targetModelElement);
|
|
525
|
-
if (range) {
|
|
526
|
-
return range;
|
|
527
|
-
}
|
|
528
|
-
// Try fixing selection position.
|
|
529
|
-
// In Firefox, the target position lands before widgets but in other browsers it tends to land after a widget.
|
|
530
|
-
range = model.schema.getNearestSelectionRange(targetModelPosition, env.isGecko ? 'forward' : 'backward');
|
|
531
|
-
if (range) {
|
|
532
|
-
return range;
|
|
533
|
-
}
|
|
534
|
-
// There is no valid selection position inside the current limit element so find a closest object ancestor.
|
|
535
|
-
// This happens if the model position lands directly in the <table> element itself (view target element was a `<td>`
|
|
536
|
-
// so a nested editable, but view target position was directly in the `<figure>` element).
|
|
537
|
-
return findDropTargetRangeOnAncestorObject(editor, targetModelPosition.parent);
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Returns fixed selection range for a given position and a target element if it is over the widget but not over its nested editable.
|
|
541
|
-
*/
|
|
542
|
-
function findDropTargetRangeOnWidget(editor, targetViewElement) {
|
|
543
|
-
const model = editor.model;
|
|
544
|
-
const mapper = editor.editing.mapper;
|
|
545
|
-
// Quick win if the target is a widget.
|
|
546
|
-
if (isWidget(targetViewElement)) {
|
|
547
|
-
return model.createRangeOn(mapper.toModelElement(targetViewElement));
|
|
548
|
-
}
|
|
549
|
-
// Check if we are deeper over a widget (but not over a nested editable).
|
|
550
|
-
if (!targetViewElement.is('editableElement')) {
|
|
551
|
-
// Find a closest ancestor that is either a widget or an editable element...
|
|
552
|
-
const ancestor = targetViewElement.findAncestor(node => isWidget(node) || node.is('editableElement'));
|
|
553
|
-
// ...and if the widget was closer then it is a drop target.
|
|
554
|
-
if (isWidget(ancestor)) {
|
|
555
|
-
return model.createRangeOn(mapper.toModelElement(ancestor));
|
|
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;
|
|
556
446
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
// Find position between blocks.
|
|
579
|
-
const positionAtElementStart = model.createPositionAt(targetModelElement, 0);
|
|
580
|
-
// Get the common part of the path (inside the target element and the target position).
|
|
581
|
-
const commonPath = targetModelPosition.path.slice(0, positionAtElementStart.path.length);
|
|
582
|
-
// Position between the blocks.
|
|
583
|
-
const betweenBlocksPosition = model.createPositionFromPath(targetModelPosition.root, commonPath);
|
|
584
|
-
const nodeAfter = betweenBlocksPosition.nodeAfter;
|
|
585
|
-
// Adjust drop position to the next object.
|
|
586
|
-
// This is because while hovering over a root element next to a widget the target position can jump in crazy places.
|
|
587
|
-
if (nodeAfter && model.schema.isObject(nodeAfter)) {
|
|
588
|
-
return model.createRangeOn(nodeAfter);
|
|
589
|
-
}
|
|
590
|
-
return null;
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Returns a selection range on the ancestor object.
|
|
594
|
-
*/
|
|
595
|
-
function findDropTargetRangeOnAncestorObject(editor, element) {
|
|
596
|
-
const model = editor.model;
|
|
597
|
-
let currentElement = element;
|
|
598
|
-
while (currentElement) {
|
|
599
|
-
if (model.schema.isObject(currentElement)) {
|
|
600
|
-
return model.createRangeOn(currentElement);
|
|
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;
|
|
601
468
|
}
|
|
602
|
-
|
|
469
|
+
model.change(writer => writer.setSelection(this._draggedRange.toRange()));
|
|
603
470
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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);
|
|
616
508
|
}
|
|
617
|
-
// Find mapped ancestor if the target is inside not mapped element (for example inline code element).
|
|
618
|
-
const viewPosition = view.createPositionBefore(element);
|
|
619
|
-
const viewElement = mapper.findMappedViewAncestor(viewPosition);
|
|
620
|
-
return mapper.toModelElement(viewElement);
|
|
621
509
|
}
|
|
622
510
|
/**
|
|
623
511
|
* Returns the drop effect that should be a result of dragging the content.
|
|
@@ -653,3 +541,37 @@ function findDraggableWidget(target) {
|
|
|
653
541
|
}
|
|
654
542
|
return null;
|
|
655
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
|
+
}
|
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @module clipboard/dragdropblocktoolbar
|
|
7
7
|
*/
|
|
8
|
-
/* istanbul ignore file -- @preserve */
|
|
9
8
|
import { Plugin } from '@ckeditor/ckeditor5-core';
|
|
10
9
|
import { env, global, DomEmitterMixin } from '@ckeditor/ckeditor5-utils';
|
|
11
10
|
import ClipboardObserver from './clipboardobserver';
|
|
12
11
|
/**
|
|
13
|
-
* Integration of
|
|
12
|
+
* Integration of a block Drag and Drop support with the block toolbar.
|
|
14
13
|
*
|
|
15
14
|
* @internal
|
|
16
15
|
*/
|
|
@@ -52,11 +51,16 @@ export default class DragDropBlockToolbar extends Plugin {
|
|
|
52
51
|
if (editor.plugins.has('BlockToolbar')) {
|
|
53
52
|
const blockToolbar = editor.plugins.get('BlockToolbar');
|
|
54
53
|
const element = blockToolbar.buttonView.element;
|
|
55
|
-
element.setAttribute('draggable', 'true');
|
|
56
54
|
this._domEmitter.listenTo(element, 'dragstart', (evt, data) => this._handleBlockDragStart(data));
|
|
57
55
|
this._domEmitter.listenTo(global.document, 'dragover', (evt, data) => this._handleBlockDragging(data));
|
|
58
56
|
this._domEmitter.listenTo(global.document, 'drop', (evt, data) => this._handleBlockDragging(data));
|
|
59
57
|
this._domEmitter.listenTo(global.document, 'dragend', () => this._handleBlockDragEnd(), { useCapture: true });
|
|
58
|
+
if (this.isEnabled) {
|
|
59
|
+
element.setAttribute('draggable', 'true');
|
|
60
|
+
}
|
|
61
|
+
this.on('change:isEnabled', (evt, name, isEnabled) => {
|
|
62
|
+
element.setAttribute('draggable', isEnabled ? 'true' : 'false');
|
|
63
|
+
});
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
@@ -75,11 +79,13 @@ export default class DragDropBlockToolbar extends Plugin {
|
|
|
75
79
|
}
|
|
76
80
|
const model = this.editor.model;
|
|
77
81
|
const selection = model.document.selection;
|
|
82
|
+
const view = this.editor.editing.view;
|
|
78
83
|
const blocks = Array.from(selection.getSelectedBlocks());
|
|
79
84
|
const draggedRange = model.createRange(model.createPositionBefore(blocks[0]), model.createPositionAfter(blocks[blocks.length - 1]));
|
|
80
85
|
model.change(writer => writer.setSelection(draggedRange));
|
|
81
86
|
this._isBlockDragging = true;
|
|
82
|
-
|
|
87
|
+
view.focus();
|
|
88
|
+
view.getObserver(ClipboardObserver).onDomEvent(domEvent);
|
|
83
89
|
}
|
|
84
90
|
/**
|
|
85
91
|
* The `dragover` and `drop` event handler.
|
|
@@ -88,13 +94,14 @@ export default class DragDropBlockToolbar extends Plugin {
|
|
|
88
94
|
if (!this.isEnabled || !this._isBlockDragging) {
|
|
89
95
|
return;
|
|
90
96
|
}
|
|
91
|
-
const clientX = domEvent.clientX + 100;
|
|
97
|
+
const clientX = domEvent.clientX + (this.editor.locale.contentLanguageDirection == 'ltr' ? 100 : -100);
|
|
92
98
|
const clientY = domEvent.clientY;
|
|
93
99
|
const target = document.elementFromPoint(clientX, clientY);
|
|
100
|
+
const view = this.editor.editing.view;
|
|
94
101
|
if (!target || !target.closest('.ck-editor__editable')) {
|
|
95
102
|
return;
|
|
96
103
|
}
|
|
97
|
-
|
|
104
|
+
view.getObserver(ClipboardObserver).onDomEvent({
|
|
98
105
|
...domEvent,
|
|
99
106
|
type: domEvent.type,
|
|
100
107
|
dataTransfer: domEvent.dataTransfer,
|