@ckeditor/ckeditor5-link 36.0.0 → 37.0.0-alpha.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/build/link.js +1 -1
- package/package.json +30 -25
- package/src/autolink.d.ts +64 -0
- package/src/autolink.js +192 -261
- package/src/index.d.ts +14 -0
- package/src/index.js +0 -2
- package/src/link.d.ts +30 -0
- package/src/link.js +13 -241
- package/src/linkcommand.d.ts +127 -0
- package/src/linkcommand.js +224 -273
- package/src/linkconfig.d.ts +261 -0
- package/src/linkconfig.js +5 -0
- package/src/linkediting.d.ts +110 -0
- package/src/linkediting.js +516 -664
- package/src/linkimage.d.ts +30 -0
- package/src/linkimage.js +12 -19
- package/src/linkimageediting.d.ts +43 -0
- package/src/linkimageediting.js +222 -275
- package/src/linkimageui.d.ts +43 -0
- package/src/linkimageui.js +75 -101
- package/src/linkui.d.ts +170 -0
- package/src/linkui.js +557 -729
- package/src/ui/linkactionsview.d.ts +101 -0
- package/src/ui/linkactionsview.js +134 -227
- package/src/ui/linkformview.d.ts +141 -0
- package/src/ui/linkformview.js +212 -342
- package/src/unlinkcommand.d.ts +36 -0
- package/src/unlinkcommand.js +51 -65
- package/src/utils/automaticdecorators.d.ts +44 -0
- package/src/utils/automaticdecorators.js +126 -148
- package/src/utils/manualdecorator.d.ts +72 -0
- package/src/utils/manualdecorator.js +35 -89
- package/src/utils.d.ts +80 -0
- package/src/utils.js +55 -121
package/src/linkui.js
CHANGED
|
@@ -2,752 +2,580 @@
|
|
|
2
2
|
* @license Copyright (c) 2003-2023, 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 link/linkui
|
|
8
7
|
*/
|
|
9
|
-
|
|
10
8
|
import { Plugin } from 'ckeditor5/src/core';
|
|
11
9
|
import { ClickObserver } from 'ckeditor5/src/engine';
|
|
12
|
-
import { ButtonView, ContextualBalloon, clickOutsideHandler } from 'ckeditor5/src/ui';
|
|
10
|
+
import { ButtonView, ContextualBalloon, clickOutsideHandler, CssTransitionDisablerMixin } from 'ckeditor5/src/ui';
|
|
13
11
|
import { isWidget } from 'ckeditor5/src/widget';
|
|
14
12
|
import LinkFormView from './ui/linkformview';
|
|
15
13
|
import LinkActionsView from './ui/linkactionsview';
|
|
16
14
|
import { addLinkProtocolIfApplicable, isLinkElement, LINK_KEYSTROKE } from './utils';
|
|
17
|
-
|
|
18
15
|
import linkIcon from '../theme/icons/link.svg';
|
|
19
|
-
|
|
20
16
|
const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
|
|
21
|
-
|
|
22
17
|
/**
|
|
23
18
|
* The link UI plugin. It introduces the `'link'` and `'unlink'` buttons and support for the <kbd>Ctrl+K</kbd> keystroke.
|
|
24
19
|
*
|
|
25
20
|
* It uses the
|
|
26
21
|
* {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
|
|
27
|
-
*
|
|
28
|
-
* @extends module:core/plugin~Plugin
|
|
29
22
|
*/
|
|
30
23
|
export default class LinkUI extends Plugin {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
get _areActionsVisible() {
|
|
580
|
-
return this._balloon.visibleView === this.actionsView;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon}.
|
|
585
|
-
*
|
|
586
|
-
* @readonly
|
|
587
|
-
* @protected
|
|
588
|
-
* @type {Boolean}
|
|
589
|
-
*/
|
|
590
|
-
get _isUIInPanel() {
|
|
591
|
-
return this._isFormInPanel || this._areActionsInPanel;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon} and it is
|
|
596
|
-
* currently visible.
|
|
597
|
-
*
|
|
598
|
-
* @readonly
|
|
599
|
-
* @protected
|
|
600
|
-
* @type {Boolean}
|
|
601
|
-
*/
|
|
602
|
-
get _isUIVisible() {
|
|
603
|
-
const visibleView = this._balloon.visibleView;
|
|
604
|
-
|
|
605
|
-
return visibleView == this.formView || this._areActionsVisible;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
|
|
610
|
-
* to the target element or selection.
|
|
611
|
-
*
|
|
612
|
-
* If the selection is collapsed and inside a link element, the panel will be attached to the
|
|
613
|
-
* entire link element. Otherwise, it will be attached to the selection.
|
|
614
|
-
*
|
|
615
|
-
* @private
|
|
616
|
-
* @returns {module:utils/dom/position~Options}
|
|
617
|
-
*/
|
|
618
|
-
_getBalloonPositionData() {
|
|
619
|
-
const view = this.editor.editing.view;
|
|
620
|
-
const model = this.editor.model;
|
|
621
|
-
const viewDocument = view.document;
|
|
622
|
-
let target = null;
|
|
623
|
-
|
|
624
|
-
if ( model.markers.has( VISUAL_SELECTION_MARKER_NAME ) ) {
|
|
625
|
-
// There are cases when we highlight selection using a marker (#7705, #4721).
|
|
626
|
-
const markerViewElements = Array.from( this.editor.editing.mapper.markerNameToElements( VISUAL_SELECTION_MARKER_NAME ) );
|
|
627
|
-
const newRange = view.createRange(
|
|
628
|
-
view.createPositionBefore( markerViewElements[ 0 ] ),
|
|
629
|
-
view.createPositionAfter( markerViewElements[ markerViewElements.length - 1 ] )
|
|
630
|
-
);
|
|
631
|
-
|
|
632
|
-
target = view.domConverter.viewRangeToDom( newRange );
|
|
633
|
-
} else {
|
|
634
|
-
// Make sure the target is calculated on demand at the last moment because a cached DOM range
|
|
635
|
-
// (which is very fragile) can desynchronize with the state of the editing view if there was
|
|
636
|
-
// any rendering done in the meantime. This can happen, for instance, when an inline widget
|
|
637
|
-
// gets unlinked.
|
|
638
|
-
target = () => {
|
|
639
|
-
const targetLink = this._getSelectedLinkElement();
|
|
640
|
-
|
|
641
|
-
return targetLink ?
|
|
642
|
-
// When selection is inside link element, then attach panel to this element.
|
|
643
|
-
view.domConverter.mapViewToDom( targetLink ) :
|
|
644
|
-
// Otherwise attach panel to the selection.
|
|
645
|
-
view.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() );
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
return { target };
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Returns the link {@link module:engine/view/attributeelement~AttributeElement} under
|
|
654
|
-
* the {@link module:engine/view/document~Document editing view's} selection or `null`
|
|
655
|
-
* if there is none.
|
|
656
|
-
*
|
|
657
|
-
* **Note**: For a non–collapsed selection, the link element is returned when **fully**
|
|
658
|
-
* selected and the **only** element within the selection boundaries, or when
|
|
659
|
-
* a linked widget is selected.
|
|
660
|
-
*
|
|
661
|
-
* @private
|
|
662
|
-
* @returns {module:engine/view/attributeelement~AttributeElement|null}
|
|
663
|
-
*/
|
|
664
|
-
_getSelectedLinkElement() {
|
|
665
|
-
const view = this.editor.editing.view;
|
|
666
|
-
const selection = view.document.selection;
|
|
667
|
-
const selectedElement = selection.getSelectedElement();
|
|
668
|
-
|
|
669
|
-
// The selection is collapsed or some widget is selected (especially inline widget).
|
|
670
|
-
if ( selection.isCollapsed || selectedElement && isWidget( selectedElement ) ) {
|
|
671
|
-
return findLinkElementAncestor( selection.getFirstPosition() );
|
|
672
|
-
} else {
|
|
673
|
-
// The range for fully selected link is usually anchored in adjacent text nodes.
|
|
674
|
-
// Trim it to get closer to the actual link element.
|
|
675
|
-
const range = selection.getFirstRange().getTrimmed();
|
|
676
|
-
const startLink = findLinkElementAncestor( range.start );
|
|
677
|
-
const endLink = findLinkElementAncestor( range.end );
|
|
678
|
-
|
|
679
|
-
if ( !startLink || startLink != endLink ) {
|
|
680
|
-
return null;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Check if the link element is fully selected.
|
|
684
|
-
if ( view.createRangeIn( startLink ).getTrimmed().isEqual( range ) ) {
|
|
685
|
-
return startLink;
|
|
686
|
-
} else {
|
|
687
|
-
return null;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Displays a fake visual selection when the contextual balloon is displayed.
|
|
694
|
-
*
|
|
695
|
-
* This adds a 'link-ui' marker into the document that is rendered as a highlight on selected text fragment.
|
|
696
|
-
*
|
|
697
|
-
* @private
|
|
698
|
-
*/
|
|
699
|
-
_showFakeVisualSelection() {
|
|
700
|
-
const model = this.editor.model;
|
|
701
|
-
|
|
702
|
-
model.change( writer => {
|
|
703
|
-
const range = model.document.selection.getFirstRange();
|
|
704
|
-
|
|
705
|
-
if ( model.markers.has( VISUAL_SELECTION_MARKER_NAME ) ) {
|
|
706
|
-
writer.updateMarker( VISUAL_SELECTION_MARKER_NAME, { range } );
|
|
707
|
-
} else {
|
|
708
|
-
if ( range.start.isAtEnd ) {
|
|
709
|
-
const startPosition = range.start.getLastMatchingPosition(
|
|
710
|
-
( { item } ) => !model.schema.isContent( item ),
|
|
711
|
-
{ boundaries: range }
|
|
712
|
-
);
|
|
713
|
-
|
|
714
|
-
writer.addMarker( VISUAL_SELECTION_MARKER_NAME, {
|
|
715
|
-
usingOperation: false,
|
|
716
|
-
affectsData: false,
|
|
717
|
-
range: writer.createRange( startPosition, range.end )
|
|
718
|
-
} );
|
|
719
|
-
} else {
|
|
720
|
-
writer.addMarker( VISUAL_SELECTION_MARKER_NAME, {
|
|
721
|
-
usingOperation: false,
|
|
722
|
-
affectsData: false,
|
|
723
|
-
range
|
|
724
|
-
} );
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
} );
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Hides the fake visual selection created in {@link #_showFakeVisualSelection}.
|
|
732
|
-
*
|
|
733
|
-
* @private
|
|
734
|
-
*/
|
|
735
|
-
_hideFakeVisualSelection() {
|
|
736
|
-
const model = this.editor.model;
|
|
737
|
-
|
|
738
|
-
if ( model.markers.has( VISUAL_SELECTION_MARKER_NAME ) ) {
|
|
739
|
-
model.change( writer => {
|
|
740
|
-
writer.removeMarker( VISUAL_SELECTION_MARKER_NAME );
|
|
741
|
-
} );
|
|
742
|
-
}
|
|
743
|
-
}
|
|
24
|
+
constructor() {
|
|
25
|
+
super(...arguments);
|
|
26
|
+
/**
|
|
27
|
+
* The actions view displayed inside of the balloon.
|
|
28
|
+
*/
|
|
29
|
+
this.actionsView = null;
|
|
30
|
+
/**
|
|
31
|
+
* The form view displayed inside the balloon.
|
|
32
|
+
*/
|
|
33
|
+
this.formView = null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @inheritDoc
|
|
37
|
+
*/
|
|
38
|
+
static get requires() {
|
|
39
|
+
return [ContextualBalloon];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @inheritDoc
|
|
43
|
+
*/
|
|
44
|
+
static get pluginName() {
|
|
45
|
+
return 'LinkUI';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @inheritDoc
|
|
49
|
+
*/
|
|
50
|
+
init() {
|
|
51
|
+
const editor = this.editor;
|
|
52
|
+
editor.editing.view.addObserver(ClickObserver);
|
|
53
|
+
this._balloon = editor.plugins.get(ContextualBalloon);
|
|
54
|
+
// Create toolbar buttons.
|
|
55
|
+
this._createToolbarLinkButton();
|
|
56
|
+
this._enableBalloonActivators();
|
|
57
|
+
// Renders a fake visual selection marker on an expanded selection.
|
|
58
|
+
editor.conversion.for('editingDowncast').markerToHighlight({
|
|
59
|
+
model: VISUAL_SELECTION_MARKER_NAME,
|
|
60
|
+
view: {
|
|
61
|
+
classes: ['ck-fake-link-selection']
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// Renders a fake visual selection marker on a collapsed selection.
|
|
65
|
+
editor.conversion.for('editingDowncast').markerToElement({
|
|
66
|
+
model: VISUAL_SELECTION_MARKER_NAME,
|
|
67
|
+
view: {
|
|
68
|
+
name: 'span',
|
|
69
|
+
classes: ['ck-fake-link-selection', 'ck-fake-link-selection_collapsed']
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* @inheritDoc
|
|
75
|
+
*/
|
|
76
|
+
destroy() {
|
|
77
|
+
super.destroy();
|
|
78
|
+
// Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
|
|
79
|
+
if (this.formView) {
|
|
80
|
+
this.formView.destroy();
|
|
81
|
+
}
|
|
82
|
+
if (this.actionsView) {
|
|
83
|
+
this.actionsView.destroy();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Creates views.
|
|
88
|
+
*/
|
|
89
|
+
_createViews() {
|
|
90
|
+
this.actionsView = this._createActionsView();
|
|
91
|
+
this.formView = this._createFormView();
|
|
92
|
+
// Attach lifecycle actions to the the balloon.
|
|
93
|
+
this._enableUserBalloonInteractions();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Creates the {@link module:link/ui/linkactionsview~LinkActionsView} instance.
|
|
97
|
+
*/
|
|
98
|
+
_createActionsView() {
|
|
99
|
+
const editor = this.editor;
|
|
100
|
+
const actionsView = new LinkActionsView(editor.locale);
|
|
101
|
+
const linkCommand = editor.commands.get('link');
|
|
102
|
+
const unlinkCommand = editor.commands.get('unlink');
|
|
103
|
+
actionsView.bind('href').to(linkCommand, 'value');
|
|
104
|
+
actionsView.editButtonView.bind('isEnabled').to(linkCommand);
|
|
105
|
+
actionsView.unlinkButtonView.bind('isEnabled').to(unlinkCommand);
|
|
106
|
+
// Execute unlink command after clicking on the "Edit" button.
|
|
107
|
+
this.listenTo(actionsView, 'edit', () => {
|
|
108
|
+
this._addFormView();
|
|
109
|
+
});
|
|
110
|
+
// Execute unlink command after clicking on the "Unlink" button.
|
|
111
|
+
this.listenTo(actionsView, 'unlink', () => {
|
|
112
|
+
editor.execute('unlink');
|
|
113
|
+
this._hideUI();
|
|
114
|
+
});
|
|
115
|
+
// Close the panel on esc key press when the **actions have focus**.
|
|
116
|
+
actionsView.keystrokes.set('Esc', (data, cancel) => {
|
|
117
|
+
this._hideUI();
|
|
118
|
+
cancel();
|
|
119
|
+
});
|
|
120
|
+
// Open the form view on Ctrl+K when the **actions have focus**..
|
|
121
|
+
actionsView.keystrokes.set(LINK_KEYSTROKE, (data, cancel) => {
|
|
122
|
+
this._addFormView();
|
|
123
|
+
cancel();
|
|
124
|
+
});
|
|
125
|
+
return actionsView;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Creates the {@link module:link/ui/linkformview~LinkFormView} instance.
|
|
129
|
+
*/
|
|
130
|
+
_createFormView() {
|
|
131
|
+
const editor = this.editor;
|
|
132
|
+
const linkCommand = editor.commands.get('link');
|
|
133
|
+
const defaultProtocol = editor.config.get('link.defaultProtocol');
|
|
134
|
+
const formView = new (CssTransitionDisablerMixin(LinkFormView))(editor.locale, linkCommand);
|
|
135
|
+
formView.urlInputView.fieldView.bind('value').to(linkCommand, 'value');
|
|
136
|
+
// Form elements should be read-only when corresponding commands are disabled.
|
|
137
|
+
formView.urlInputView.bind('isEnabled').to(linkCommand, 'isEnabled');
|
|
138
|
+
formView.saveButtonView.bind('isEnabled').to(linkCommand);
|
|
139
|
+
// Execute link command after clicking the "Save" button.
|
|
140
|
+
this.listenTo(formView, 'submit', () => {
|
|
141
|
+
const { value } = formView.urlInputView.fieldView.element;
|
|
142
|
+
const parsedUrl = addLinkProtocolIfApplicable(value, defaultProtocol);
|
|
143
|
+
editor.execute('link', parsedUrl, formView.getDecoratorSwitchesState());
|
|
144
|
+
this._closeFormView();
|
|
145
|
+
});
|
|
146
|
+
// Hide the panel after clicking the "Cancel" button.
|
|
147
|
+
this.listenTo(formView, 'cancel', () => {
|
|
148
|
+
this._closeFormView();
|
|
149
|
+
});
|
|
150
|
+
// Close the panel on esc key press when the **form has focus**.
|
|
151
|
+
formView.keystrokes.set('Esc', (data, cancel) => {
|
|
152
|
+
this._closeFormView();
|
|
153
|
+
cancel();
|
|
154
|
+
});
|
|
155
|
+
return formView;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Creates a toolbar Link button. Clicking this button will show
|
|
159
|
+
* a {@link #_balloon} attached to the selection.
|
|
160
|
+
*/
|
|
161
|
+
_createToolbarLinkButton() {
|
|
162
|
+
const editor = this.editor;
|
|
163
|
+
const linkCommand = editor.commands.get('link');
|
|
164
|
+
const t = editor.t;
|
|
165
|
+
editor.ui.componentFactory.add('link', locale => {
|
|
166
|
+
const button = new ButtonView(locale);
|
|
167
|
+
button.isEnabled = true;
|
|
168
|
+
button.label = t('Link');
|
|
169
|
+
button.icon = linkIcon;
|
|
170
|
+
button.keystroke = LINK_KEYSTROKE;
|
|
171
|
+
button.tooltip = true;
|
|
172
|
+
button.isToggleable = true;
|
|
173
|
+
// Bind button to the command.
|
|
174
|
+
button.bind('isEnabled').to(linkCommand, 'isEnabled');
|
|
175
|
+
button.bind('isOn').to(linkCommand, 'value', value => !!value);
|
|
176
|
+
// Show the panel on button click.
|
|
177
|
+
this.listenTo(button, 'execute', () => this._showUI(true));
|
|
178
|
+
return button;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Attaches actions that control whether the balloon panel containing the
|
|
183
|
+
* {@link #formView} should be displayed.
|
|
184
|
+
*/
|
|
185
|
+
_enableBalloonActivators() {
|
|
186
|
+
const editor = this.editor;
|
|
187
|
+
const viewDocument = editor.editing.view.document;
|
|
188
|
+
// Handle click on view document and show panel when selection is placed inside the link element.
|
|
189
|
+
// Keep panel open until selection will be inside the same link element.
|
|
190
|
+
this.listenTo(viewDocument, 'click', () => {
|
|
191
|
+
const parentLink = this._getSelectedLinkElement();
|
|
192
|
+
if (parentLink) {
|
|
193
|
+
// Then show panel but keep focus inside editor editable.
|
|
194
|
+
this._showUI();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
// Handle the `Ctrl+K` keystroke and show the panel.
|
|
198
|
+
editor.keystrokes.set(LINK_KEYSTROKE, (keyEvtData, cancel) => {
|
|
199
|
+
// Prevent focusing the search bar in FF, Chrome and Edge. See https://github.com/ckeditor/ckeditor5/issues/4811.
|
|
200
|
+
cancel();
|
|
201
|
+
if (editor.commands.get('link').isEnabled) {
|
|
202
|
+
this._showUI(true);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Attaches actions that control whether the balloon panel containing the
|
|
208
|
+
* {@link #formView} is visible or not.
|
|
209
|
+
*/
|
|
210
|
+
_enableUserBalloonInteractions() {
|
|
211
|
+
// Focus the form if the balloon is visible and the Tab key has been pressed.
|
|
212
|
+
this.editor.keystrokes.set('Tab', (data, cancel) => {
|
|
213
|
+
if (this._areActionsVisible && !this.actionsView.focusTracker.isFocused) {
|
|
214
|
+
this.actionsView.focus();
|
|
215
|
+
cancel();
|
|
216
|
+
}
|
|
217
|
+
}, {
|
|
218
|
+
// Use the high priority because the link UI navigation is more important
|
|
219
|
+
// than other feature's actions, e.g. list indentation.
|
|
220
|
+
// https://github.com/ckeditor/ckeditor5-link/issues/146
|
|
221
|
+
priority: 'high'
|
|
222
|
+
});
|
|
223
|
+
// Close the panel on the Esc key press when the editable has focus and the balloon is visible.
|
|
224
|
+
this.editor.keystrokes.set('Esc', (data, cancel) => {
|
|
225
|
+
if (this._isUIVisible) {
|
|
226
|
+
this._hideUI();
|
|
227
|
+
cancel();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
// Close on click outside of balloon panel element.
|
|
231
|
+
clickOutsideHandler({
|
|
232
|
+
emitter: this.formView,
|
|
233
|
+
activator: () => this._isUIInPanel,
|
|
234
|
+
contextElements: () => [this._balloon.view.element],
|
|
235
|
+
callback: () => this._hideUI()
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Adds the {@link #actionsView} to the {@link #_balloon}.
|
|
240
|
+
*
|
|
241
|
+
* @internal
|
|
242
|
+
*/
|
|
243
|
+
_addActionsView() {
|
|
244
|
+
if (!this.actionsView) {
|
|
245
|
+
this._createViews();
|
|
246
|
+
}
|
|
247
|
+
if (this._areActionsInPanel) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this._balloon.add({
|
|
251
|
+
view: this.actionsView,
|
|
252
|
+
position: this._getBalloonPositionData()
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Adds the {@link #formView} to the {@link #_balloon}.
|
|
257
|
+
*/
|
|
258
|
+
_addFormView() {
|
|
259
|
+
if (!this.formView) {
|
|
260
|
+
this._createViews();
|
|
261
|
+
}
|
|
262
|
+
if (this._isFormInPanel) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const editor = this.editor;
|
|
266
|
+
const linkCommand = editor.commands.get('link');
|
|
267
|
+
this.formView.disableCssTransitions();
|
|
268
|
+
this._balloon.add({
|
|
269
|
+
view: this.formView,
|
|
270
|
+
position: this._getBalloonPositionData()
|
|
271
|
+
});
|
|
272
|
+
// Select input when form view is currently visible.
|
|
273
|
+
if (this._balloon.visibleView === this.formView) {
|
|
274
|
+
this.formView.urlInputView.fieldView.select();
|
|
275
|
+
}
|
|
276
|
+
this.formView.enableCssTransitions();
|
|
277
|
+
// Make sure that each time the panel shows up, the URL field remains in sync with the value of
|
|
278
|
+
// the command. If the user typed in the input, then canceled the balloon (`urlInputView.fieldView#value` stays
|
|
279
|
+
// unaltered) and re-opened it without changing the value of the link command (e.g. because they
|
|
280
|
+
// clicked the same link), they would see the old value instead of the actual value of the command.
|
|
281
|
+
// https://github.com/ckeditor/ckeditor5-link/issues/78
|
|
282
|
+
// https://github.com/ckeditor/ckeditor5-link/issues/123
|
|
283
|
+
this.formView.urlInputView.fieldView.element.value = linkCommand.value || '';
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Closes the form view. Decides whether the balloon should be hidden completely or if the action view should be shown. This is
|
|
287
|
+
* decided upon the link command value (which has a value if the document selection is in the link).
|
|
288
|
+
*
|
|
289
|
+
* Additionally, if any {@link module:link/link~LinkConfig#decorators} are defined in the editor configuration, the state of
|
|
290
|
+
* switch buttons responsible for manual decorator handling is restored.
|
|
291
|
+
*/
|
|
292
|
+
_closeFormView() {
|
|
293
|
+
const linkCommand = this.editor.commands.get('link');
|
|
294
|
+
// Restore manual decorator states to represent the current model state. This case is important to reset the switch buttons
|
|
295
|
+
// when the user cancels the editing form.
|
|
296
|
+
linkCommand.restoreManualDecoratorStates();
|
|
297
|
+
if (linkCommand.value !== undefined) {
|
|
298
|
+
this._removeFormView();
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this._hideUI();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Removes the {@link #formView} from the {@link #_balloon}.
|
|
306
|
+
*/
|
|
307
|
+
_removeFormView() {
|
|
308
|
+
if (this._isFormInPanel) {
|
|
309
|
+
// Blur the input element before removing it from DOM to prevent issues in some browsers.
|
|
310
|
+
// See https://github.com/ckeditor/ckeditor5/issues/1501.
|
|
311
|
+
this.formView.saveButtonView.focus();
|
|
312
|
+
this._balloon.remove(this.formView);
|
|
313
|
+
// Because the form has an input which has focus, the focus must be brought back
|
|
314
|
+
// to the editor. Otherwise, it would be lost.
|
|
315
|
+
this.editor.editing.view.focus();
|
|
316
|
+
this._hideFakeVisualSelection();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Shows the correct UI type. It is either {@link #formView} or {@link #actionsView}.
|
|
321
|
+
*
|
|
322
|
+
* @internal
|
|
323
|
+
*/
|
|
324
|
+
_showUI(forceVisible = false) {
|
|
325
|
+
if (!this.formView) {
|
|
326
|
+
this._createViews();
|
|
327
|
+
}
|
|
328
|
+
// When there's no link under the selection, go straight to the editing UI.
|
|
329
|
+
if (!this._getSelectedLinkElement()) {
|
|
330
|
+
// Show visual selection on a text without a link when the contextual balloon is displayed.
|
|
331
|
+
// See https://github.com/ckeditor/ckeditor5/issues/4721.
|
|
332
|
+
this._showFakeVisualSelection();
|
|
333
|
+
this._addActionsView();
|
|
334
|
+
// Be sure panel with link is visible.
|
|
335
|
+
if (forceVisible) {
|
|
336
|
+
this._balloon.showStack('main');
|
|
337
|
+
}
|
|
338
|
+
this._addFormView();
|
|
339
|
+
}
|
|
340
|
+
// If there's a link under the selection...
|
|
341
|
+
else {
|
|
342
|
+
// Go to the editing UI if actions are already visible.
|
|
343
|
+
if (this._areActionsVisible) {
|
|
344
|
+
this._addFormView();
|
|
345
|
+
}
|
|
346
|
+
// Otherwise display just the actions UI.
|
|
347
|
+
else {
|
|
348
|
+
this._addActionsView();
|
|
349
|
+
}
|
|
350
|
+
// Be sure panel with link is visible.
|
|
351
|
+
if (forceVisible) {
|
|
352
|
+
this._balloon.showStack('main');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Begin responding to ui#update once the UI is added.
|
|
356
|
+
this._startUpdatingUI();
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Removes the {@link #formView} from the {@link #_balloon}.
|
|
360
|
+
*
|
|
361
|
+
* See {@link #_addFormView}, {@link #_addActionsView}.
|
|
362
|
+
*/
|
|
363
|
+
_hideUI() {
|
|
364
|
+
if (!this._isUIInPanel) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const editor = this.editor;
|
|
368
|
+
this.stopListening(editor.ui, 'update');
|
|
369
|
+
this.stopListening(this._balloon, 'change:visibleView');
|
|
370
|
+
// Make sure the focus always gets back to the editable _before_ removing the focused form view.
|
|
371
|
+
// Doing otherwise causes issues in some browsers. See https://github.com/ckeditor/ckeditor5-link/issues/193.
|
|
372
|
+
editor.editing.view.focus();
|
|
373
|
+
// Remove form first because it's on top of the stack.
|
|
374
|
+
this._removeFormView();
|
|
375
|
+
// Then remove the actions view because it's beneath the form.
|
|
376
|
+
this._balloon.remove(this.actionsView);
|
|
377
|
+
this._hideFakeVisualSelection();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Makes the UI react to the {@link module:core/editor/editorui~EditorUI#event:update} event to
|
|
381
|
+
* reposition itself when the editor UI should be refreshed.
|
|
382
|
+
*
|
|
383
|
+
* See: {@link #_hideUI} to learn when the UI stops reacting to the `update` event.
|
|
384
|
+
*/
|
|
385
|
+
_startUpdatingUI() {
|
|
386
|
+
const editor = this.editor;
|
|
387
|
+
const viewDocument = editor.editing.view.document;
|
|
388
|
+
let prevSelectedLink = this._getSelectedLinkElement();
|
|
389
|
+
let prevSelectionParent = getSelectionParent();
|
|
390
|
+
const update = () => {
|
|
391
|
+
const selectedLink = this._getSelectedLinkElement();
|
|
392
|
+
const selectionParent = getSelectionParent();
|
|
393
|
+
// Hide the panel if:
|
|
394
|
+
//
|
|
395
|
+
// * the selection went out of the EXISTING link element. E.g. user moved the caret out
|
|
396
|
+
// of the link,
|
|
397
|
+
// * the selection went to a different parent when creating a NEW link. E.g. someone
|
|
398
|
+
// else modified the document.
|
|
399
|
+
// * the selection has expanded (e.g. displaying link actions then pressing SHIFT+Right arrow).
|
|
400
|
+
//
|
|
401
|
+
// Note: #_getSelectedLinkElement will return a link for a non-collapsed selection only
|
|
402
|
+
// when fully selected.
|
|
403
|
+
if ((prevSelectedLink && !selectedLink) ||
|
|
404
|
+
(!prevSelectedLink && selectionParent !== prevSelectionParent)) {
|
|
405
|
+
this._hideUI();
|
|
406
|
+
}
|
|
407
|
+
// Update the position of the panel when:
|
|
408
|
+
// * link panel is in the visible stack
|
|
409
|
+
// * the selection remains in the original link element,
|
|
410
|
+
// * there was no link element in the first place, i.e. creating a new link
|
|
411
|
+
else if (this._isUIVisible) {
|
|
412
|
+
// If still in a link element, simply update the position of the balloon.
|
|
413
|
+
// If there was no link (e.g. inserting one), the balloon must be moved
|
|
414
|
+
// to the new position in the editing view (a new native DOM range).
|
|
415
|
+
this._balloon.updatePosition(this._getBalloonPositionData());
|
|
416
|
+
}
|
|
417
|
+
prevSelectedLink = selectedLink;
|
|
418
|
+
prevSelectionParent = selectionParent;
|
|
419
|
+
};
|
|
420
|
+
function getSelectionParent() {
|
|
421
|
+
return viewDocument.selection.focus.getAncestors()
|
|
422
|
+
.reverse()
|
|
423
|
+
.find((node) => node.is('element'));
|
|
424
|
+
}
|
|
425
|
+
this.listenTo(editor.ui, 'update', update);
|
|
426
|
+
this.listenTo(this._balloon, 'change:visibleView', update);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Returns `true` when {@link #formView} is in the {@link #_balloon}.
|
|
430
|
+
*/
|
|
431
|
+
get _isFormInPanel() {
|
|
432
|
+
return !!this.formView && this._balloon.hasView(this.formView);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Returns `true` when {@link #actionsView} is in the {@link #_balloon}.
|
|
436
|
+
*/
|
|
437
|
+
get _areActionsInPanel() {
|
|
438
|
+
return !!this.actionsView && this._balloon.hasView(this.actionsView);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Returns `true` when {@link #actionsView} is in the {@link #_balloon} and it is
|
|
442
|
+
* currently visible.
|
|
443
|
+
*/
|
|
444
|
+
get _areActionsVisible() {
|
|
445
|
+
return !!this.actionsView && this._balloon.visibleView === this.actionsView;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon}.
|
|
449
|
+
*/
|
|
450
|
+
get _isUIInPanel() {
|
|
451
|
+
return this._isFormInPanel || this._areActionsInPanel;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon} and it is
|
|
455
|
+
* currently visible.
|
|
456
|
+
*/
|
|
457
|
+
get _isUIVisible() {
|
|
458
|
+
const visibleView = this._balloon.visibleView;
|
|
459
|
+
return !!this.formView && visibleView == this.formView || this._areActionsVisible;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
|
|
463
|
+
* to the target element or selection.
|
|
464
|
+
*
|
|
465
|
+
* If the selection is collapsed and inside a link element, the panel will be attached to the
|
|
466
|
+
* entire link element. Otherwise, it will be attached to the selection.
|
|
467
|
+
*/
|
|
468
|
+
_getBalloonPositionData() {
|
|
469
|
+
const view = this.editor.editing.view;
|
|
470
|
+
const model = this.editor.model;
|
|
471
|
+
const viewDocument = view.document;
|
|
472
|
+
let target;
|
|
473
|
+
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
474
|
+
// There are cases when we highlight selection using a marker (#7705, #4721).
|
|
475
|
+
const markerViewElements = Array.from(this.editor.editing.mapper.markerNameToElements(VISUAL_SELECTION_MARKER_NAME));
|
|
476
|
+
const newRange = view.createRange(view.createPositionBefore(markerViewElements[0]), view.createPositionAfter(markerViewElements[markerViewElements.length - 1]));
|
|
477
|
+
target = view.domConverter.viewRangeToDom(newRange);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
// Make sure the target is calculated on demand at the last moment because a cached DOM range
|
|
481
|
+
// (which is very fragile) can desynchronize with the state of the editing view if there was
|
|
482
|
+
// any rendering done in the meantime. This can happen, for instance, when an inline widget
|
|
483
|
+
// gets unlinked.
|
|
484
|
+
target = () => {
|
|
485
|
+
const targetLink = this._getSelectedLinkElement();
|
|
486
|
+
return targetLink ?
|
|
487
|
+
// When selection is inside link element, then attach panel to this element.
|
|
488
|
+
view.domConverter.mapViewToDom(targetLink) :
|
|
489
|
+
// Otherwise attach panel to the selection.
|
|
490
|
+
view.domConverter.viewRangeToDom(viewDocument.selection.getFirstRange());
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return { target };
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Returns the link {@link module:engine/view/attributeelement~AttributeElement} under
|
|
497
|
+
* the {@link module:engine/view/document~Document editing view's} selection or `null`
|
|
498
|
+
* if there is none.
|
|
499
|
+
*
|
|
500
|
+
* **Note**: For a non–collapsed selection, the link element is returned when **fully**
|
|
501
|
+
* selected and the **only** element within the selection boundaries, or when
|
|
502
|
+
* a linked widget is selected.
|
|
503
|
+
*/
|
|
504
|
+
_getSelectedLinkElement() {
|
|
505
|
+
const view = this.editor.editing.view;
|
|
506
|
+
const selection = view.document.selection;
|
|
507
|
+
const selectedElement = selection.getSelectedElement();
|
|
508
|
+
// The selection is collapsed or some widget is selected (especially inline widget).
|
|
509
|
+
if (selection.isCollapsed || selectedElement && isWidget(selectedElement)) {
|
|
510
|
+
return findLinkElementAncestor(selection.getFirstPosition());
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
// The range for fully selected link is usually anchored in adjacent text nodes.
|
|
514
|
+
// Trim it to get closer to the actual link element.
|
|
515
|
+
const range = selection.getFirstRange().getTrimmed();
|
|
516
|
+
const startLink = findLinkElementAncestor(range.start);
|
|
517
|
+
const endLink = findLinkElementAncestor(range.end);
|
|
518
|
+
if (!startLink || startLink != endLink) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
// Check if the link element is fully selected.
|
|
522
|
+
if (view.createRangeIn(startLink).getTrimmed().isEqual(range)) {
|
|
523
|
+
return startLink;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Displays a fake visual selection when the contextual balloon is displayed.
|
|
532
|
+
*
|
|
533
|
+
* This adds a 'link-ui' marker into the document that is rendered as a highlight on selected text fragment.
|
|
534
|
+
*/
|
|
535
|
+
_showFakeVisualSelection() {
|
|
536
|
+
const model = this.editor.model;
|
|
537
|
+
model.change(writer => {
|
|
538
|
+
const range = model.document.selection.getFirstRange();
|
|
539
|
+
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
540
|
+
writer.updateMarker(VISUAL_SELECTION_MARKER_NAME, { range });
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
if (range.start.isAtEnd) {
|
|
544
|
+
const startPosition = range.start.getLastMatchingPosition(({ item }) => !model.schema.isContent(item), { boundaries: range });
|
|
545
|
+
writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
|
546
|
+
usingOperation: false,
|
|
547
|
+
affectsData: false,
|
|
548
|
+
range: writer.createRange(startPosition, range.end)
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
|
553
|
+
usingOperation: false,
|
|
554
|
+
affectsData: false,
|
|
555
|
+
range
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Hides the fake visual selection created in {@link #_showFakeVisualSelection}.
|
|
563
|
+
*/
|
|
564
|
+
_hideFakeVisualSelection() {
|
|
565
|
+
const model = this.editor.model;
|
|
566
|
+
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
567
|
+
model.change(writer => {
|
|
568
|
+
writer.removeMarker(VISUAL_SELECTION_MARKER_NAME);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
744
572
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
function findLinkElementAncestor(
|
|
752
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Returns a link element if there's one among the ancestors of the provided `Position`.
|
|
575
|
+
*
|
|
576
|
+
* @param View position to analyze.
|
|
577
|
+
* @returns Link element at the position or null.
|
|
578
|
+
*/
|
|
579
|
+
function findLinkElementAncestor(position) {
|
|
580
|
+
return position.getAncestors().find((ancestor) => isLinkElement(ancestor)) || null;
|
|
753
581
|
}
|