@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/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 { throttle } from 'lodash-es';
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._updateDropMarkerThrottled.cancel();
160
- this._removeDropMarkerDelayed.cancel();
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
- // TODO we could clone this node somewhere and style it to match editing view but without handles,
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
- const canEditAtDraggedRange = this.isEnabled && editor.model.canEditAt(this._draggedRange);
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 content = editor.data.toView(model.getSelectedContent(draggedSelection));
212
- viewDocument.fire('clipboardOutput', {
213
- dataTransfer: data.dataTransfer,
214
- content,
215
- method: 'dragstart'
216
- });
217
- if (!canEditAtDraggedRange) {
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
- this._removeDropMarkerDelayed();
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
- this._removeDropMarkerDelayed.cancel();
249
- const targetRange = findDropTargetRange(editor, data.targetRanges, data.target);
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
- /* istanbul ignore else -- @preserve */
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 targetRange = findDropTargetRange(editor, data.targetRanges, data.target);
288
- // The dragging markers must be removed after searching for the target range because sometimes
289
- // the target lands on the marker itself.
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
- const editableElement = viewDocument.selection.editableElement;
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
- this._removeDropMarker();
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.deleteContent(model.createSelection(this._draggedRange), { doNotAutoparagraph: true });
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
- * Returns fixed selection range for given position and target element.
497
- */
498
- function findDropTargetRange(editor, targetViewRanges, targetViewElement) {
499
- const model = editor.model;
500
- const mapper = editor.editing.mapper;
501
- let range = null;
502
- const targetViewPosition = targetViewRanges ? targetViewRanges[0].start : null;
503
- // A UIElement is not a valid drop element, use parent (this could be a drop marker or any other UIElement).
504
- if (targetViewElement.is('uiElement')) {
505
- targetViewElement = targetViewElement.parent;
506
- }
507
- // Quick win if the target is a widget (but not a nested editable).
508
- range = findDropTargetRangeOnWidget(editor, targetViewElement);
509
- if (range) {
510
- return range;
511
- }
512
- // The easiest part is over, now we need to move to the model space.
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
- return null;
559
- }
560
- /**
561
- * Returns fixed selection range inside a model element.
562
- */
563
- function findDropTargetRangeInElement(editor, targetModelElement) {
564
- const model = editor.model;
565
- const schema = model.schema;
566
- const positionAtElementStart = model.createPositionAt(targetModelElement, 0);
567
- return schema.getNearestSelectionRange(positionAtElementStart, 'forward');
568
- }
569
- /**
570
- * Returns fixed selection range for a given position and a target element if the drop is between blocks.
571
- */
572
- function findDropTargetRangeBetweenBlocks(editor, targetModelPosition, targetModelElement) {
573
- const model = editor.model;
574
- // Check if target is between blocks.
575
- if (!model.schema.checkChild(targetModelElement, '$block')) {
576
- return null;
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
- currentElement = currentElement.parent;
469
+ model.change(writer => writer.setSelection(this._draggedRange.toRange()));
603
470
  }
604
- /* istanbul ignore next -- @preserve */
605
- return null;
606
- }
607
- /**
608
- * Returns the closest model element for the specified view element.
609
- */
610
- function getClosestMappedModelElement(editor, element) {
611
- const mapper = editor.editing.mapper;
612
- const view = editor.editing.view;
613
- const targetModelElement = mapper.toModelElement(element);
614
- if (targetModelElement) {
615
- return targetModelElement;
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
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { Plugin } from '@ckeditor/ckeditor5-core';
9
9
  /**
10
- * Integration of an experimental block Drag and drop support with the block toolbar.
10
+ * Integration of a block Drag and Drop support with the block toolbar.
11
11
  *
12
12
  * @internal
13
13
  */
@@ -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 an experimental block Drag and drop support with the block toolbar.
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
- this.editor.editing.view.getObserver(ClipboardObserver).onDomEvent(domEvent);
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
- this.editor.editing.view.getObserver(ClipboardObserver).onDomEvent({
104
+ view.getObserver(ClipboardObserver).onDomEvent({
98
105
  ...domEvent,
99
106
  type: domEvent.type,
100
107
  dataTransfer: domEvent.dataTransfer,