@ckeditor/ckeditor5-clipboard 35.2.0 → 35.3.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/package.json +32 -24
- package/src/clipboard.js +12 -17
- package/src/clipboardobserver.js +59 -89
- package/src/clipboardpipeline.js +135 -248
- package/src/dragdrop.js +572 -715
- package/src/index.js +0 -2
- package/src/pasteplaintext.js +61 -80
- package/src/utils/normalizeclipboarddata.js +12 -15
- package/src/utils/plaintexttohtml.js +21 -26
- package/src/utils/viewtoplaintext.js +36 -42
- package/src/datatransfer.js +0 -112
package/src/dragdrop.js
CHANGED
|
@@ -2,13 +2,10 @@
|
|
|
2
2
|
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
5
|
/**
|
|
7
6
|
* @module clipboard/dragdrop
|
|
8
7
|
*/
|
|
9
|
-
|
|
10
8
|
/* globals setTimeout, clearTimeout */
|
|
11
|
-
|
|
12
9
|
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
|
13
10
|
import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
|
|
14
11
|
import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
|
|
@@ -16,14 +13,10 @@ import Widget from '@ckeditor/ckeditor5-widget/src/widget';
|
|
|
16
13
|
import uid from '@ckeditor/ckeditor5-utils/src/uid';
|
|
17
14
|
import env from '@ckeditor/ckeditor5-utils/src/env';
|
|
18
15
|
import { isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
|
|
19
|
-
|
|
20
16
|
import ClipboardPipeline from './clipboardpipeline';
|
|
21
17
|
import ClipboardObserver from './clipboardobserver';
|
|
22
|
-
|
|
23
18
|
import { throttle } from 'lodash-es';
|
|
24
|
-
|
|
25
19
|
import '../theme/clipboard.css';
|
|
26
|
-
|
|
27
20
|
// Drag and drop events overview:
|
|
28
21
|
//
|
|
29
22
|
// ┌──────────────────┐
|
|
@@ -101,7 +94,6 @@ import '../theme/clipboard.css';
|
|
|
101
94
|
// │ dragend │ Removes the drop marker and cleans the state.
|
|
102
95
|
// └──────────────────┘
|
|
103
96
|
//
|
|
104
|
-
|
|
105
97
|
/**
|
|
106
98
|
* The drag and drop feature. It works on top of the {@link module:clipboard/clipboardpipeline~ClipboardPipeline}.
|
|
107
99
|
*
|
|
@@ -110,764 +102,629 @@ import '../theme/clipboard.css';
|
|
|
110
102
|
* @extends module:core/plugin~Plugin
|
|
111
103
|
*/
|
|
112
104
|
export default class DragDrop extends Plugin {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return writer.createUIElement( 'span', { class: 'ck ck-clipboard-drop-target-position' }, function( domDocument ) {
|
|
551
|
-
const domElement = this.toDomElement( domDocument );
|
|
552
|
-
|
|
553
|
-
// Using word joiner to make this marker as high as text and also making text not break on marker.
|
|
554
|
-
domElement.append( '\u2060', domDocument.createElement( 'span' ), '\u2060' );
|
|
555
|
-
|
|
556
|
-
return domElement;
|
|
557
|
-
} );
|
|
558
|
-
}
|
|
559
|
-
} );
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Updates the drop target marker to the provided range.
|
|
564
|
-
*
|
|
565
|
-
* @private
|
|
566
|
-
* @param {module:engine/model/range~Range} targetRange The range to set the marker to.
|
|
567
|
-
*/
|
|
568
|
-
_updateDropMarker( targetRange ) {
|
|
569
|
-
const editor = this.editor;
|
|
570
|
-
const markers = editor.model.markers;
|
|
571
|
-
|
|
572
|
-
editor.model.change( writer => {
|
|
573
|
-
if ( markers.has( 'drop-target' ) ) {
|
|
574
|
-
if ( !markers.get( 'drop-target' ).getRange().isEqual( targetRange ) ) {
|
|
575
|
-
writer.updateMarker( 'drop-target', { range: targetRange } );
|
|
576
|
-
}
|
|
577
|
-
} else {
|
|
578
|
-
writer.addMarker( 'drop-target', {
|
|
579
|
-
range: targetRange,
|
|
580
|
-
usingOperation: false,
|
|
581
|
-
affectsData: false
|
|
582
|
-
} );
|
|
583
|
-
}
|
|
584
|
-
} );
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Removes the drop target marker.
|
|
589
|
-
*
|
|
590
|
-
* @private
|
|
591
|
-
*/
|
|
592
|
-
_removeDropMarker() {
|
|
593
|
-
const model = this.editor.model;
|
|
594
|
-
|
|
595
|
-
this._removeDropMarkerDelayed.cancel();
|
|
596
|
-
this._updateDropMarkerThrottled.cancel();
|
|
597
|
-
|
|
598
|
-
if ( model.markers.has( 'drop-target' ) ) {
|
|
599
|
-
model.change( writer => {
|
|
600
|
-
writer.removeMarker( 'drop-target' );
|
|
601
|
-
} );
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Deletes the dragged content from its original range and clears the dragging state.
|
|
607
|
-
*
|
|
608
|
-
* @private
|
|
609
|
-
* @param {Boolean} moved Whether the move succeeded.
|
|
610
|
-
*/
|
|
611
|
-
_finalizeDragging( moved ) {
|
|
612
|
-
const editor = this.editor;
|
|
613
|
-
const model = editor.model;
|
|
614
|
-
|
|
615
|
-
this._removeDropMarker();
|
|
616
|
-
this._clearDraggableAttributes();
|
|
617
|
-
|
|
618
|
-
if ( editor.plugins.has( 'WidgetToolbarRepository' ) ) {
|
|
619
|
-
editor.plugins.get( 'WidgetToolbarRepository' ).clearForceDisabled( 'dragDrop' );
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
this._draggingUid = '';
|
|
623
|
-
|
|
624
|
-
if ( !this._draggedRange ) {
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Delete moved content.
|
|
629
|
-
if ( moved && this.isEnabled ) {
|
|
630
|
-
model.deleteContent( model.createSelection( this._draggedRange ), { doNotAutoparagraph: true } );
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
this._draggedRange.detach();
|
|
634
|
-
this._draggedRange = null;
|
|
635
|
-
}
|
|
105
|
+
/**
|
|
106
|
+
* @inheritDoc
|
|
107
|
+
*/
|
|
108
|
+
static get pluginName() {
|
|
109
|
+
return 'DragDrop';
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* @inheritDoc
|
|
113
|
+
*/
|
|
114
|
+
static get requires() {
|
|
115
|
+
return [ClipboardPipeline, Widget];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* @inheritDoc
|
|
119
|
+
*/
|
|
120
|
+
init() {
|
|
121
|
+
const editor = this.editor;
|
|
122
|
+
const view = editor.editing.view;
|
|
123
|
+
/**
|
|
124
|
+
* The live range over the original content that is being dragged.
|
|
125
|
+
*
|
|
126
|
+
* @private
|
|
127
|
+
* @type {module:engine/model/liverange~LiveRange}
|
|
128
|
+
*/
|
|
129
|
+
this._draggedRange = null;
|
|
130
|
+
/**
|
|
131
|
+
* The UID of current dragging that is used to verify if the drop started in the same editor as the drag start.
|
|
132
|
+
*
|
|
133
|
+
* **Note**: This is a workaround for broken 'dragend' events (they are not fired if the source text node got removed).
|
|
134
|
+
*
|
|
135
|
+
* @private
|
|
136
|
+
* @type {String}
|
|
137
|
+
*/
|
|
138
|
+
this._draggingUid = '';
|
|
139
|
+
/**
|
|
140
|
+
* The reference to the model element that currently has a `draggable` attribute set (it is set while dragging).
|
|
141
|
+
*
|
|
142
|
+
* @private
|
|
143
|
+
* @type {module:engine/model/element~Element}
|
|
144
|
+
*/
|
|
145
|
+
this._draggableElement = null;
|
|
146
|
+
/**
|
|
147
|
+
* A throttled callback updating the drop marker.
|
|
148
|
+
*
|
|
149
|
+
* @private
|
|
150
|
+
* @type {Function}
|
|
151
|
+
*/
|
|
152
|
+
this._updateDropMarkerThrottled = throttle(targetRange => this._updateDropMarker(targetRange), 40);
|
|
153
|
+
/**
|
|
154
|
+
* A delayed callback removing the drop marker.
|
|
155
|
+
*
|
|
156
|
+
* @private
|
|
157
|
+
* @type {Function}
|
|
158
|
+
*/
|
|
159
|
+
this._removeDropMarkerDelayed = delay(() => this._removeDropMarker(), 40);
|
|
160
|
+
/**
|
|
161
|
+
* A delayed callback removing draggable attributes.
|
|
162
|
+
*
|
|
163
|
+
* @private
|
|
164
|
+
* @type {Function}
|
|
165
|
+
*/
|
|
166
|
+
this._clearDraggableAttributesDelayed = delay(() => this._clearDraggableAttributes(), 40);
|
|
167
|
+
view.addObserver(ClipboardObserver);
|
|
168
|
+
view.addObserver(MouseObserver);
|
|
169
|
+
this._setupDragging();
|
|
170
|
+
this._setupContentInsertionIntegration();
|
|
171
|
+
this._setupClipboardInputIntegration();
|
|
172
|
+
this._setupDropMarker();
|
|
173
|
+
this._setupDraggableAttributeHandling();
|
|
174
|
+
this.listenTo(editor, 'change:isReadOnly', (evt, name, isReadOnly) => {
|
|
175
|
+
if (isReadOnly) {
|
|
176
|
+
this.forceDisabled('readOnlyMode');
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
this.clearForceDisabled('readOnlyMode');
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
this.on('change:isEnabled', (evt, name, isEnabled) => {
|
|
183
|
+
if (!isEnabled) {
|
|
184
|
+
this._finalizeDragging(false);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
if (env.isAndroid) {
|
|
188
|
+
this.forceDisabled('noAndroidSupport');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* @inheritDoc
|
|
193
|
+
*/
|
|
194
|
+
destroy() {
|
|
195
|
+
if (this._draggedRange) {
|
|
196
|
+
this._draggedRange.detach();
|
|
197
|
+
this._draggedRange = null;
|
|
198
|
+
}
|
|
199
|
+
this._updateDropMarkerThrottled.cancel();
|
|
200
|
+
this._removeDropMarkerDelayed.cancel();
|
|
201
|
+
this._clearDraggableAttributesDelayed.cancel();
|
|
202
|
+
return super.destroy();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Drag and drop events handling.
|
|
206
|
+
*
|
|
207
|
+
* @private
|
|
208
|
+
*/
|
|
209
|
+
_setupDragging() {
|
|
210
|
+
const editor = this.editor;
|
|
211
|
+
const model = editor.model;
|
|
212
|
+
const modelDocument = model.document;
|
|
213
|
+
const view = editor.editing.view;
|
|
214
|
+
const viewDocument = view.document;
|
|
215
|
+
// The handler for the drag start; it is responsible for setting data transfer object.
|
|
216
|
+
this.listenTo(viewDocument, 'dragstart', (evt, data) => {
|
|
217
|
+
const selection = modelDocument.selection;
|
|
218
|
+
// Don't drag the editable element itself.
|
|
219
|
+
if (data.target && data.target.is('editableElement')) {
|
|
220
|
+
data.preventDefault();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// TODO we could clone this node somewhere and style it to match editing view but without handles,
|
|
224
|
+
// selection outline, WTA buttons, etc.
|
|
225
|
+
// data.dataTransfer._native.setDragImage( data.domTarget, 0, 0 );
|
|
226
|
+
// Check if this is dragstart over the widget (but not a nested editable).
|
|
227
|
+
const draggableWidget = data.target ? findDraggableWidget(data.target) : null;
|
|
228
|
+
if (draggableWidget) {
|
|
229
|
+
const modelElement = editor.editing.mapper.toModelElement(draggableWidget);
|
|
230
|
+
this._draggedRange = LiveRange.fromRange(model.createRangeOn(modelElement));
|
|
231
|
+
// Disable toolbars so they won't obscure the drop area.
|
|
232
|
+
if (editor.plugins.has('WidgetToolbarRepository')) {
|
|
233
|
+
editor.plugins.get('WidgetToolbarRepository').forceDisabled('dragDrop');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// If this was not a widget we should check if we need to drag some text content.
|
|
237
|
+
else if (!viewDocument.selection.isCollapsed) {
|
|
238
|
+
const selectedElement = viewDocument.selection.getSelectedElement();
|
|
239
|
+
if (!selectedElement || !isWidget(selectedElement)) {
|
|
240
|
+
this._draggedRange = LiveRange.fromRange(selection.getFirstRange());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!this._draggedRange) {
|
|
244
|
+
data.preventDefault();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this._draggingUid = uid();
|
|
248
|
+
data.dataTransfer.effectAllowed = this.isEnabled ? 'copyMove' : 'copy';
|
|
249
|
+
data.dataTransfer.setData('application/ckeditor5-dragging-uid', this._draggingUid);
|
|
250
|
+
const draggedSelection = model.createSelection(this._draggedRange.toRange());
|
|
251
|
+
const content = editor.data.toView(model.getSelectedContent(draggedSelection));
|
|
252
|
+
viewDocument.fire('clipboardOutput', {
|
|
253
|
+
dataTransfer: data.dataTransfer,
|
|
254
|
+
content,
|
|
255
|
+
method: 'dragstart'
|
|
256
|
+
});
|
|
257
|
+
if (!this.isEnabled) {
|
|
258
|
+
this._draggedRange.detach();
|
|
259
|
+
this._draggedRange = null;
|
|
260
|
+
this._draggingUid = '';
|
|
261
|
+
}
|
|
262
|
+
}, { priority: 'low' });
|
|
263
|
+
// The handler for finalizing drag and drop. It should always be triggered after dragging completes
|
|
264
|
+
// even if it was completed in a different application.
|
|
265
|
+
// Note: This is not fired if source text node got removed while downcasting a marker.
|
|
266
|
+
this.listenTo(viewDocument, 'dragend', (evt, data) => {
|
|
267
|
+
this._finalizeDragging(!data.dataTransfer.isCanceled && data.dataTransfer.dropEffect == 'move');
|
|
268
|
+
}, { priority: 'low' });
|
|
269
|
+
// Dragging over the editable.
|
|
270
|
+
this.listenTo(viewDocument, 'dragenter', () => {
|
|
271
|
+
if (!this.isEnabled) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
view.focus();
|
|
275
|
+
});
|
|
276
|
+
// Dragging out of the editable.
|
|
277
|
+
this.listenTo(viewDocument, 'dragleave', () => {
|
|
278
|
+
// We do not know if the mouse left the editor or just some element in it, so let us wait a few milliseconds
|
|
279
|
+
// to check if 'dragover' is not fired.
|
|
280
|
+
this._removeDropMarkerDelayed();
|
|
281
|
+
});
|
|
282
|
+
// Handler for moving dragged content over the target area.
|
|
283
|
+
this.listenTo(viewDocument, 'dragging', (evt, data) => {
|
|
284
|
+
if (!this.isEnabled) {
|
|
285
|
+
data.dataTransfer.dropEffect = 'none';
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
this._removeDropMarkerDelayed.cancel();
|
|
289
|
+
const targetRange = findDropTargetRange(editor, data.targetRanges, data.target);
|
|
290
|
+
// If this is content being dragged from another editor, moving out of current editor instance
|
|
291
|
+
// is not possible until 'dragend' event case will be fixed.
|
|
292
|
+
if (!this._draggedRange) {
|
|
293
|
+
data.dataTransfer.dropEffect = 'copy';
|
|
294
|
+
}
|
|
295
|
+
// In Firefox it is already set and effect allowed remains the same as originally set.
|
|
296
|
+
if (!env.isGecko) {
|
|
297
|
+
if (data.dataTransfer.effectAllowed == 'copy') {
|
|
298
|
+
data.dataTransfer.dropEffect = 'copy';
|
|
299
|
+
}
|
|
300
|
+
else if (['all', 'copyMove'].includes(data.dataTransfer.effectAllowed)) {
|
|
301
|
+
data.dataTransfer.dropEffect = 'move';
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/* istanbul ignore else */
|
|
305
|
+
if (targetRange) {
|
|
306
|
+
this._updateDropMarkerThrottled(targetRange);
|
|
307
|
+
}
|
|
308
|
+
}, { priority: 'low' });
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Integration with the `clipboardInput` event.
|
|
312
|
+
*
|
|
313
|
+
* @private
|
|
314
|
+
*/
|
|
315
|
+
_setupClipboardInputIntegration() {
|
|
316
|
+
const editor = this.editor;
|
|
317
|
+
const view = editor.editing.view;
|
|
318
|
+
const viewDocument = view.document;
|
|
319
|
+
// Update the event target ranges and abort dropping if dropping over itself.
|
|
320
|
+
this.listenTo(viewDocument, 'clipboardInput', (evt, data) => {
|
|
321
|
+
if (data.method != 'drop') {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const targetRange = findDropTargetRange(editor, data.targetRanges, data.target);
|
|
325
|
+
// The dragging markers must be removed after searching for the target range because sometimes
|
|
326
|
+
// the target lands on the marker itself.
|
|
327
|
+
this._removeDropMarker();
|
|
328
|
+
/* istanbul ignore if */
|
|
329
|
+
if (!targetRange) {
|
|
330
|
+
this._finalizeDragging(false);
|
|
331
|
+
evt.stop();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// Since we cannot rely on the drag end event, we must check if the local drag range is from the current drag and drop
|
|
335
|
+
// or it is from some previous not cleared one.
|
|
336
|
+
if (this._draggedRange && this._draggingUid != data.dataTransfer.getData('application/ckeditor5-dragging-uid')) {
|
|
337
|
+
this._draggedRange.detach();
|
|
338
|
+
this._draggedRange = null;
|
|
339
|
+
this._draggingUid = '';
|
|
340
|
+
}
|
|
341
|
+
// Do not do anything if some content was dragged within the same document to the same position.
|
|
342
|
+
const isMove = getFinalDropEffect(data.dataTransfer) == 'move';
|
|
343
|
+
if (isMove && this._draggedRange && this._draggedRange.containsRange(targetRange, true)) {
|
|
344
|
+
this._finalizeDragging(false);
|
|
345
|
+
evt.stop();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Override the target ranges with the one adjusted to the best one for a drop.
|
|
349
|
+
data.targetRanges = [editor.editing.mapper.toViewRange(targetRange)];
|
|
350
|
+
}, { priority: 'high' });
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Integration with the `contentInsertion` event of the clipboard pipeline.
|
|
354
|
+
*
|
|
355
|
+
* @private
|
|
356
|
+
*/
|
|
357
|
+
_setupContentInsertionIntegration() {
|
|
358
|
+
const clipboardPipeline = this.editor.plugins.get(ClipboardPipeline);
|
|
359
|
+
clipboardPipeline.on('contentInsertion', (evt, data) => {
|
|
360
|
+
if (!this.isEnabled || data.method !== 'drop') {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// Update the selection to the target range in the same change block to avoid selection post-fixing
|
|
364
|
+
// and to be able to clone text attributes for plain text dropping.
|
|
365
|
+
const ranges = data.targetRanges.map(viewRange => this.editor.editing.mapper.toModelRange(viewRange));
|
|
366
|
+
this.editor.model.change(writer => writer.setSelection(ranges));
|
|
367
|
+
}, { priority: 'high' });
|
|
368
|
+
clipboardPipeline.on('contentInsertion', (evt, data) => {
|
|
369
|
+
if (!this.isEnabled || data.method !== 'drop') {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// Remove dragged range content, remove markers, clean after dragging.
|
|
373
|
+
const isMove = getFinalDropEffect(data.dataTransfer) == 'move';
|
|
374
|
+
// Whether any content was inserted (insertion might fail if the schema is disallowing some elements
|
|
375
|
+
// (for example an image caption allows only the content of a block but not blocks themselves.
|
|
376
|
+
// Some integrations might not return valid range (i.e., table pasting).
|
|
377
|
+
const isSuccess = !data.resultRange || !data.resultRange.isCollapsed;
|
|
378
|
+
this._finalizeDragging(isSuccess && isMove);
|
|
379
|
+
}, { priority: 'lowest' });
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Adds listeners that add the `draggable` attribute to the elements while the mouse button is down so the dragging could start.
|
|
383
|
+
*
|
|
384
|
+
* @private
|
|
385
|
+
*/
|
|
386
|
+
_setupDraggableAttributeHandling() {
|
|
387
|
+
const editor = this.editor;
|
|
388
|
+
const view = editor.editing.view;
|
|
389
|
+
const viewDocument = view.document;
|
|
390
|
+
// Add the 'draggable' attribute to the widget while pressing the selection handle.
|
|
391
|
+
// This is required for widgets to be draggable. In Chrome it will enable dragging text nodes.
|
|
392
|
+
this.listenTo(viewDocument, 'mousedown', (evt, data) => {
|
|
393
|
+
// The lack of data can be caused by editor tests firing fake mouse events. This should not occur
|
|
394
|
+
// in real-life scenarios but this greatly simplifies editor tests that would otherwise fail a lot.
|
|
395
|
+
if (env.isAndroid || !data) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this._clearDraggableAttributesDelayed.cancel();
|
|
399
|
+
// Check if this is a mousedown over the widget (but not a nested editable).
|
|
400
|
+
let draggableElement = findDraggableWidget(data.target);
|
|
401
|
+
// Note: There is a limitation that if more than a widget is selected (a widget and some text)
|
|
402
|
+
// and dragging starts on the widget, then only the widget is dragged.
|
|
403
|
+
// If this was not a widget then we should check if we need to drag some text content.
|
|
404
|
+
// In Chrome set a 'draggable' attribute on closest editable to allow immediate dragging of the selected text range.
|
|
405
|
+
// In Firefox this is not needed. In Safari it makes the whole editable draggable (not just textual content).
|
|
406
|
+
// Disabled in read-only mode because draggable="true" + contenteditable="false" results
|
|
407
|
+
// in not firing selectionchange event ever, which makes the selection stuck in read-only mode.
|
|
408
|
+
if (env.isBlink && !editor.isReadOnly && !draggableElement && !viewDocument.selection.isCollapsed) {
|
|
409
|
+
const selectedElement = viewDocument.selection.getSelectedElement();
|
|
410
|
+
if (!selectedElement || !isWidget(selectedElement)) {
|
|
411
|
+
draggableElement = viewDocument.selection.editableElement;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (draggableElement) {
|
|
415
|
+
view.change(writer => {
|
|
416
|
+
writer.setAttribute('draggable', 'true', draggableElement);
|
|
417
|
+
});
|
|
418
|
+
// Keep the reference to the model element in case the view element gets removed while dragging.
|
|
419
|
+
this._draggableElement = editor.editing.mapper.toModelElement(draggableElement);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
// Remove the draggable attribute in case no dragging started (only mousedown + mouseup).
|
|
423
|
+
this.listenTo(viewDocument, 'mouseup', () => {
|
|
424
|
+
if (!env.isAndroid) {
|
|
425
|
+
this._clearDraggableAttributesDelayed();
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Removes the `draggable` attribute from the element that was used for dragging.
|
|
431
|
+
*
|
|
432
|
+
* @private
|
|
433
|
+
*/
|
|
434
|
+
_clearDraggableAttributes() {
|
|
435
|
+
const editing = this.editor.editing;
|
|
436
|
+
editing.view.change(writer => {
|
|
437
|
+
// Remove 'draggable' attribute.
|
|
438
|
+
if (this._draggableElement && this._draggableElement.root.rootName != '$graveyard') {
|
|
439
|
+
writer.removeAttribute('draggable', editing.mapper.toViewElement(this._draggableElement));
|
|
440
|
+
}
|
|
441
|
+
this._draggableElement = null;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Creates downcast conversion for the drop target marker.
|
|
446
|
+
*
|
|
447
|
+
* @private
|
|
448
|
+
*/
|
|
449
|
+
_setupDropMarker() {
|
|
450
|
+
const editor = this.editor;
|
|
451
|
+
// Drop marker conversion for hovering over widgets.
|
|
452
|
+
editor.conversion.for('editingDowncast').markerToHighlight({
|
|
453
|
+
model: 'drop-target',
|
|
454
|
+
view: {
|
|
455
|
+
classes: ['ck-clipboard-drop-target-range']
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
// Drop marker conversion for in text drop target.
|
|
459
|
+
editor.conversion.for('editingDowncast').markerToElement({
|
|
460
|
+
model: 'drop-target',
|
|
461
|
+
view: (data, { writer }) => {
|
|
462
|
+
const inText = editor.model.schema.checkChild(data.markerRange.start, '$text');
|
|
463
|
+
if (!inText) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
return writer.createUIElement('span', { class: 'ck ck-clipboard-drop-target-position' }, function (domDocument) {
|
|
467
|
+
const domElement = this.toDomElement(domDocument);
|
|
468
|
+
// Using word joiner to make this marker as high as text and also making text not break on marker.
|
|
469
|
+
domElement.append('\u2060', domDocument.createElement('span'), '\u2060');
|
|
470
|
+
return domElement;
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Updates the drop target marker to the provided range.
|
|
477
|
+
*
|
|
478
|
+
* @private
|
|
479
|
+
* @param {module:engine/model/range~Range} targetRange The range to set the marker to.
|
|
480
|
+
*/
|
|
481
|
+
_updateDropMarker(targetRange) {
|
|
482
|
+
const editor = this.editor;
|
|
483
|
+
const markers = editor.model.markers;
|
|
484
|
+
editor.model.change(writer => {
|
|
485
|
+
if (markers.has('drop-target')) {
|
|
486
|
+
if (!markers.get('drop-target').getRange().isEqual(targetRange)) {
|
|
487
|
+
writer.updateMarker('drop-target', { range: targetRange });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
writer.addMarker('drop-target', {
|
|
492
|
+
range: targetRange,
|
|
493
|
+
usingOperation: false,
|
|
494
|
+
affectsData: false
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Removes the drop target marker.
|
|
501
|
+
*
|
|
502
|
+
* @private
|
|
503
|
+
*/
|
|
504
|
+
_removeDropMarker() {
|
|
505
|
+
const model = this.editor.model;
|
|
506
|
+
this._removeDropMarkerDelayed.cancel();
|
|
507
|
+
this._updateDropMarkerThrottled.cancel();
|
|
508
|
+
if (model.markers.has('drop-target')) {
|
|
509
|
+
model.change(writer => {
|
|
510
|
+
writer.removeMarker('drop-target');
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Deletes the dragged content from its original range and clears the dragging state.
|
|
516
|
+
*
|
|
517
|
+
* @private
|
|
518
|
+
* @param {Boolean} moved Whether the move succeeded.
|
|
519
|
+
*/
|
|
520
|
+
_finalizeDragging(moved) {
|
|
521
|
+
const editor = this.editor;
|
|
522
|
+
const model = editor.model;
|
|
523
|
+
this._removeDropMarker();
|
|
524
|
+
this._clearDraggableAttributes();
|
|
525
|
+
if (editor.plugins.has('WidgetToolbarRepository')) {
|
|
526
|
+
editor.plugins.get('WidgetToolbarRepository').clearForceDisabled('dragDrop');
|
|
527
|
+
}
|
|
528
|
+
this._draggingUid = '';
|
|
529
|
+
if (!this._draggedRange) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
// Delete moved content.
|
|
533
|
+
if (moved && this.isEnabled) {
|
|
534
|
+
model.deleteContent(model.createSelection(this._draggedRange), { doNotAutoparagraph: true });
|
|
535
|
+
}
|
|
536
|
+
this._draggedRange.detach();
|
|
537
|
+
this._draggedRange = null;
|
|
538
|
+
}
|
|
636
539
|
}
|
|
637
|
-
|
|
638
540
|
// Returns fixed selection range for given position and target element.
|
|
639
541
|
//
|
|
640
542
|
// @param {module:core/editor/editor~Editor} editor
|
|
641
543
|
// @param {Array.<module:engine/view/range~Range>} targetViewRanges
|
|
642
544
|
// @param {module:engine/view/element~Element} targetViewElement
|
|
643
545
|
// @returns {module:engine/model/range~Range|null}
|
|
644
|
-
function findDropTargetRange(
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
// Try fixing selection position.
|
|
686
|
-
// In Firefox, the target position lands before widgets but in other browsers it tends to land after a widget.
|
|
687
|
-
range = model.schema.getNearestSelectionRange( targetModelPosition, env.isGecko ? 'forward' : 'backward' );
|
|
688
|
-
|
|
689
|
-
if ( range ) {
|
|
690
|
-
return range;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// There is no valid selection position inside the current limit element so find a closest object ancestor.
|
|
694
|
-
// This happens if the model position lands directly in the <table> element itself (view target element was a `<td>`
|
|
695
|
-
// so a nested editable, but view target position was directly in the `<figure>` element).
|
|
696
|
-
return findDropTargetRangeOnAncestorObject( editor, targetModelPosition.parent );
|
|
546
|
+
function findDropTargetRange(editor, targetViewRanges, targetViewElement) {
|
|
547
|
+
const model = editor.model;
|
|
548
|
+
const mapper = editor.editing.mapper;
|
|
549
|
+
let range = null;
|
|
550
|
+
const targetViewPosition = targetViewRanges ? targetViewRanges[0].start : null;
|
|
551
|
+
// A UIElement is not a valid drop element, use parent (this could be a drop marker or any other UIElement).
|
|
552
|
+
if (targetViewElement.is('uiElement')) {
|
|
553
|
+
targetViewElement = targetViewElement.parent;
|
|
554
|
+
}
|
|
555
|
+
// Quick win if the target is a widget (but not a nested editable).
|
|
556
|
+
range = findDropTargetRangeOnWidget(editor, targetViewElement);
|
|
557
|
+
if (range) {
|
|
558
|
+
return range;
|
|
559
|
+
}
|
|
560
|
+
// The easiest part is over, now we need to move to the model space.
|
|
561
|
+
// Find target model element and position.
|
|
562
|
+
const targetModelElement = getClosestMappedModelElement(editor, targetViewElement);
|
|
563
|
+
const targetModelPosition = targetViewPosition ? mapper.toModelPosition(targetViewPosition) : null;
|
|
564
|
+
// There is no target position while hovering over an empty table cell.
|
|
565
|
+
// In Safari, target position can be empty while hovering over a widget (e.g., a page-break).
|
|
566
|
+
// Find the drop position inside the element.
|
|
567
|
+
if (!targetModelPosition) {
|
|
568
|
+
return findDropTargetRangeInElement(editor, targetModelElement);
|
|
569
|
+
}
|
|
570
|
+
// Check if target position is between blocks and adjust drop position to the next object.
|
|
571
|
+
// This is because while hovering over a root element next to a widget the target position can jump in crazy places.
|
|
572
|
+
range = findDropTargetRangeBetweenBlocks(editor, targetModelPosition, targetModelElement);
|
|
573
|
+
if (range) {
|
|
574
|
+
return range;
|
|
575
|
+
}
|
|
576
|
+
// Try fixing selection position.
|
|
577
|
+
// In Firefox, the target position lands before widgets but in other browsers it tends to land after a widget.
|
|
578
|
+
range = model.schema.getNearestSelectionRange(targetModelPosition, env.isGecko ? 'forward' : 'backward');
|
|
579
|
+
if (range) {
|
|
580
|
+
return range;
|
|
581
|
+
}
|
|
582
|
+
// There is no valid selection position inside the current limit element so find a closest object ancestor.
|
|
583
|
+
// This happens if the model position lands directly in the <table> element itself (view target element was a `<td>`
|
|
584
|
+
// so a nested editable, but view target position was directly in the `<figure>` element).
|
|
585
|
+
return findDropTargetRangeOnAncestorObject(editor, targetModelPosition.parent);
|
|
697
586
|
}
|
|
698
|
-
|
|
699
587
|
// Returns fixed selection range for a given position and a target element if it is over the widget but not over its nested editable.
|
|
700
588
|
//
|
|
701
589
|
// @param {module:core/editor/editor~Editor} editor
|
|
702
590
|
// @param {module:engine/view/element~Element} targetViewElement
|
|
703
591
|
// @returns {module:engine/model/range~Range|null}
|
|
704
|
-
function findDropTargetRangeOnWidget(
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
return null;
|
|
592
|
+
function findDropTargetRangeOnWidget(editor, targetViewElement) {
|
|
593
|
+
const model = editor.model;
|
|
594
|
+
const mapper = editor.editing.mapper;
|
|
595
|
+
// Quick win if the target is a widget.
|
|
596
|
+
if (isWidget(targetViewElement)) {
|
|
597
|
+
return model.createRangeOn(mapper.toModelElement(targetViewElement));
|
|
598
|
+
}
|
|
599
|
+
// Check if we are deeper over a widget (but not over a nested editable).
|
|
600
|
+
if (!targetViewElement.is('editableElement')) {
|
|
601
|
+
// Find a closest ancestor that is either a widget or an editable element...
|
|
602
|
+
const ancestor = targetViewElement.findAncestor(node => isWidget(node) || node.is('editableElement'));
|
|
603
|
+
// ...and if the widget was closer then it is a drop target.
|
|
604
|
+
if (isWidget(ancestor)) {
|
|
605
|
+
return model.createRangeOn(mapper.toModelElement(ancestor));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
725
609
|
}
|
|
726
|
-
|
|
727
610
|
// Returns fixed selection range inside a model element.
|
|
728
611
|
//
|
|
729
612
|
// @param {module:core/editor/editor~Editor} editor
|
|
730
613
|
// @param {module:engine/model/element~Element} targetModelElement
|
|
731
614
|
// @returns {module:engine/model/range~Range}
|
|
732
|
-
function findDropTargetRangeInElement(
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
return schema.getNearestSelectionRange( positionAtElementStart, 'forward' );
|
|
615
|
+
function findDropTargetRangeInElement(editor, targetModelElement) {
|
|
616
|
+
const model = editor.model;
|
|
617
|
+
const schema = model.schema;
|
|
618
|
+
const positionAtElementStart = model.createPositionAt(targetModelElement, 0);
|
|
619
|
+
return schema.getNearestSelectionRange(positionAtElementStart, 'forward');
|
|
739
620
|
}
|
|
740
|
-
|
|
741
621
|
// Returns fixed selection range for a given position and a target element if the drop is between blocks.
|
|
742
622
|
//
|
|
743
623
|
// @param {module:core/editor/editor~Editor} editor
|
|
744
624
|
// @param {module:engine/model/position~Position} targetModelPosition
|
|
745
625
|
// @param {module:engine/model/element~Element} targetModelElement
|
|
746
626
|
// @returns {module:engine/model/range~Range|null}
|
|
747
|
-
function findDropTargetRangeBetweenBlocks(
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
// This is because while hovering over a root element next to a widget the target position can jump in crazy places.
|
|
767
|
-
if ( nodeAfter && model.schema.isObject( nodeAfter ) ) {
|
|
768
|
-
return model.createRangeOn( nodeAfter );
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
return null;
|
|
627
|
+
function findDropTargetRangeBetweenBlocks(editor, targetModelPosition, targetModelElement) {
|
|
628
|
+
const model = editor.model;
|
|
629
|
+
// Check if target is between blocks.
|
|
630
|
+
if (!model.schema.checkChild(targetModelElement, '$block')) {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
// Find position between blocks.
|
|
634
|
+
const positionAtElementStart = model.createPositionAt(targetModelElement, 0);
|
|
635
|
+
// Get the common part of the path (inside the target element and the target position).
|
|
636
|
+
const commonPath = targetModelPosition.path.slice(0, positionAtElementStart.path.length);
|
|
637
|
+
// Position between the blocks.
|
|
638
|
+
const betweenBlocksPosition = model.createPositionFromPath(targetModelPosition.root, commonPath);
|
|
639
|
+
const nodeAfter = betweenBlocksPosition.nodeAfter;
|
|
640
|
+
// Adjust drop position to the next object.
|
|
641
|
+
// This is because while hovering over a root element next to a widget the target position can jump in crazy places.
|
|
642
|
+
if (nodeAfter && model.schema.isObject(nodeAfter)) {
|
|
643
|
+
return model.createRangeOn(nodeAfter);
|
|
644
|
+
}
|
|
645
|
+
return null;
|
|
772
646
|
}
|
|
773
|
-
|
|
774
647
|
// Returns a selection range on the ancestor object.
|
|
775
648
|
//
|
|
776
649
|
// @param {module:core/editor/editor~Editor} editor
|
|
777
650
|
// @param {module:engine/model/element~Element} element
|
|
778
651
|
// @returns {module:engine/model/range~Range}
|
|
779
|
-
function findDropTargetRangeOnAncestorObject(
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
652
|
+
function findDropTargetRangeOnAncestorObject(editor, element) {
|
|
653
|
+
const model = editor.model;
|
|
654
|
+
let currentElement = element;
|
|
655
|
+
while (currentElement) {
|
|
656
|
+
if (model.schema.isObject(currentElement)) {
|
|
657
|
+
return model.createRangeOn(currentElement);
|
|
658
|
+
}
|
|
659
|
+
currentElement = currentElement.parent;
|
|
660
|
+
}
|
|
661
|
+
/* istanbul ignore next */
|
|
662
|
+
return null;
|
|
789
663
|
}
|
|
790
|
-
|
|
791
664
|
// Returns the closest model element for the specified view element.
|
|
792
665
|
//
|
|
793
666
|
// @param {module:core/editor/editor~Editor} editor
|
|
794
667
|
// @param {module:engine/view/element~Element} element
|
|
795
668
|
// @returns {module:engine/model/element~Element}
|
|
796
|
-
function getClosestMappedModelElement(
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const viewPosition = view.createPositionBefore( element );
|
|
808
|
-
const viewElement = mapper.findMappedViewAncestor( viewPosition );
|
|
809
|
-
|
|
810
|
-
return mapper.toModelElement( viewElement );
|
|
669
|
+
function getClosestMappedModelElement(editor, element) {
|
|
670
|
+
const mapper = editor.editing.mapper;
|
|
671
|
+
const view = editor.editing.view;
|
|
672
|
+
const targetModelElement = mapper.toModelElement(element);
|
|
673
|
+
if (targetModelElement) {
|
|
674
|
+
return targetModelElement;
|
|
675
|
+
}
|
|
676
|
+
// Find mapped ancestor if the target is inside not mapped element (for example inline code element).
|
|
677
|
+
const viewPosition = view.createPositionBefore(element);
|
|
678
|
+
const viewElement = mapper.findMappedViewAncestor(viewPosition);
|
|
679
|
+
return mapper.toModelElement(viewElement);
|
|
811
680
|
}
|
|
812
|
-
|
|
813
681
|
// Returns the drop effect that should be a result of dragging the content.
|
|
814
682
|
// This function is handling a quirk when checking the effect in the 'drop' DOM event.
|
|
815
|
-
function getFinalDropEffect(
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
return [ 'all', 'copyMove' ].includes( dataTransfer.effectAllowed ) ? 'move' : 'copy';
|
|
683
|
+
function getFinalDropEffect(dataTransfer) {
|
|
684
|
+
if (env.isGecko) {
|
|
685
|
+
return dataTransfer.dropEffect;
|
|
686
|
+
}
|
|
687
|
+
return ['all', 'copyMove'].includes(dataTransfer.effectAllowed) ? 'move' : 'copy';
|
|
821
688
|
}
|
|
822
|
-
|
|
823
689
|
// Returns a function wrapper that will trigger a function after a specified wait time.
|
|
824
690
|
// The timeout can be canceled by calling the cancel function on the returned wrapped function.
|
|
825
691
|
//
|
|
826
692
|
// @param {Function} func The function to wrap.
|
|
827
693
|
// @param {Number} wait The timeout in ms.
|
|
828
694
|
// @returns {Function}
|
|
829
|
-
function delay(
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
};
|
|
840
|
-
|
|
841
|
-
return delayed;
|
|
695
|
+
function delay(func, wait) {
|
|
696
|
+
let timer;
|
|
697
|
+
function delayed(...args) {
|
|
698
|
+
delayed.cancel();
|
|
699
|
+
timer = setTimeout(() => func(...args), wait);
|
|
700
|
+
}
|
|
701
|
+
delayed.cancel = () => {
|
|
702
|
+
clearTimeout(timer);
|
|
703
|
+
};
|
|
704
|
+
return delayed;
|
|
842
705
|
}
|
|
843
|
-
|
|
844
706
|
// Returns a widget element that should be dragged.
|
|
845
707
|
//
|
|
846
708
|
// @param {module:engine/view/element~Element} target
|
|
847
709
|
// @returns {module:engine/view/element~Element}
|
|
848
|
-
function findDraggableWidget(
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
if ( isWidget( ancestor ) ) {
|
|
869
|
-
return ancestor;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
return null;
|
|
710
|
+
function findDraggableWidget(target) {
|
|
711
|
+
// This is directly an editable so not a widget for sure.
|
|
712
|
+
if (target.is('editableElement')) {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
// TODO: Let's have a isWidgetSelectionHandleDomElement() helper in ckeditor5-widget utils.
|
|
716
|
+
if (target.hasClass('ck-widget__selection-handle')) {
|
|
717
|
+
return target.findAncestor(isWidget);
|
|
718
|
+
}
|
|
719
|
+
// Direct hit on a widget.
|
|
720
|
+
if (isWidget(target)) {
|
|
721
|
+
return target;
|
|
722
|
+
}
|
|
723
|
+
// Find closest ancestor that is either a widget or an editable element...
|
|
724
|
+
const ancestor = target.findAncestor(node => isWidget(node) || node.is('editableElement'));
|
|
725
|
+
// ...and if closer was the widget then enable dragging it.
|
|
726
|
+
if (isWidget(ancestor)) {
|
|
727
|
+
return ancestor;
|
|
728
|
+
}
|
|
729
|
+
return null;
|
|
873
730
|
}
|