@ckeditor/ckeditor5-mention 35.4.0 → 36.0.1

Sign up to get free protection for your applications and to get access to all the features.
package/src/mentionui.js CHANGED
@@ -1,849 +1,616 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
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 mention/mentionui
8
7
  */
9
-
10
8
  import { Plugin } from 'ckeditor5/src/core';
11
9
  import { ButtonView, ContextualBalloon, clickOutsideHandler } from 'ckeditor5/src/ui';
12
- import { Collection, keyCodes, env, Rect, CKEditorError, logWarning } from 'ckeditor5/src/utils';
10
+ import { CKEditorError, Collection, Rect, env, keyCodes, logWarning } from 'ckeditor5/src/utils';
13
11
  import { TextWatcher } from 'ckeditor5/src/typing';
14
-
15
12
  import { debounce } from 'lodash-es';
16
-
17
13
  import MentionsView from './ui/mentionsview';
18
14
  import DomWrapperView from './ui/domwrapperview';
19
15
  import MentionListItemView from './ui/mentionlistitemview';
20
-
21
16
  const VERTICAL_SPACING = 3;
22
-
23
17
  // The key codes that mention UI handles when it is open (without commit keys).
24
18
  const defaultHandledKeyCodes = [
25
- keyCodes.arrowup,
26
- keyCodes.arrowdown,
27
- keyCodes.esc
19
+ keyCodes.arrowup,
20
+ keyCodes.arrowdown,
21
+ keyCodes.esc
28
22
  ];
29
-
30
23
  // Dropdown commit key codes.
31
24
  const defaultCommitKeyCodes = [
32
- keyCodes.enter,
33
- keyCodes.tab
25
+ keyCodes.enter,
26
+ keyCodes.tab
34
27
  ];
35
-
36
28
  /**
37
29
  * The mention UI feature.
38
- *
39
- * @extends module:core/plugin~Plugin
40
30
  */
41
31
  export default class MentionUI extends Plugin {
42
- /**
43
- * @inheritDoc
44
- */
45
- static get pluginName() {
46
- return 'MentionUI';
47
- }
48
-
49
- /**
50
- * @inheritDoc
51
- */
52
- static get requires() {
53
- return [ ContextualBalloon ];
54
- }
55
-
56
- /**
57
- * @inheritDoc
58
- */
59
- constructor( editor ) {
60
- super( editor );
61
-
62
- /**
63
- * The mention view.
64
- *
65
- * @type {module:mention/ui/mentionsview~MentionsView}
66
- * @private
67
- */
68
- this._mentionsView = this._createMentionView();
69
-
70
- /**
71
- * Stores mention feeds configurations.
72
- *
73
- * @type {Map<String, Object>}
74
- * @private
75
- */
76
- this._mentionsConfigurations = new Map();
77
-
78
- /**
79
- * Debounced feed requester. It uses `lodash#debounce` method to delay function call.
80
- *
81
- * @private
82
- * @param {String} marker
83
- * @param {String} feedText
84
- * @method
85
- */
86
- this._requestFeedDebounced = debounce( this._requestFeed, 100 );
87
-
88
- editor.config.define( 'mention', { feeds: [] } );
89
- }
90
-
91
- /**
92
- * @inheritDoc
93
- */
94
- init() {
95
- const editor = this.editor;
96
-
97
- const commitKeys = editor.config.get( 'mention.commitKeys' ) || defaultCommitKeyCodes;
98
- const handledKeyCodes = defaultHandledKeyCodes.concat( commitKeys );
99
-
100
- /**
101
- * The contextual balloon plugin instance.
102
- *
103
- * @private
104
- * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
105
- */
106
- this._balloon = editor.plugins.get( ContextualBalloon );
107
-
108
- // Key listener that handles navigation in mention view.
109
- editor.editing.view.document.on( 'keydown', ( evt, data ) => {
110
- if ( isHandledKey( data.keyCode ) && this._isUIVisible ) {
111
- data.preventDefault();
112
- evt.stop(); // Required for Enter key overriding.
113
-
114
- if ( data.keyCode == keyCodes.arrowdown ) {
115
- this._mentionsView.selectNext();
116
- }
117
-
118
- if ( data.keyCode == keyCodes.arrowup ) {
119
- this._mentionsView.selectPrevious();
120
- }
121
-
122
- if ( commitKeys.includes( data.keyCode ) ) {
123
- this._mentionsView.executeSelected();
124
- }
125
-
126
- if ( data.keyCode == keyCodes.esc ) {
127
- this._hideUIAndRemoveMarker();
128
- }
129
- }
130
- }, { priority: 'highest' } ); // Required to override the Enter key.
131
-
132
- // Close the dropdown upon clicking outside of the plugin UI.
133
- clickOutsideHandler( {
134
- emitter: this._mentionsView,
135
- activator: () => this._isUIVisible,
136
- contextElements: [ this._balloon.view.element ],
137
- callback: () => this._hideUIAndRemoveMarker()
138
- } );
139
-
140
- const feeds = editor.config.get( 'mention.feeds' );
141
-
142
- for ( const mentionDescription of feeds ) {
143
- const feed = mentionDescription.feed;
144
-
145
- const marker = mentionDescription.marker;
146
-
147
- if ( !isValidMentionMarker( marker ) ) {
148
- /**
149
- * The marker must be a single character.
150
- *
151
- * Correct markers: `'@'`, `'#'`.
152
- *
153
- * Incorrect markers: `'$$'`, `'[@'`.
154
- *
155
- * See {@link module:mention/mention~MentionConfig}.
156
- *
157
- * @error mentionconfig-incorrect-marker
158
- * @param {String} marker Configured marker
159
- */
160
- throw new CKEditorError( 'mentionconfig-incorrect-marker', null, { marker } );
161
- }
162
-
163
- const feedCallback = typeof feed == 'function' ? feed.bind( this.editor ) : createFeedCallback( feed );
164
- const itemRenderer = mentionDescription.itemRenderer;
165
- const definition = { marker, feedCallback, itemRenderer };
166
-
167
- this._mentionsConfigurations.set( marker, definition );
168
- }
169
-
170
- this._setupTextWatcher( feeds );
171
- this.listenTo( editor, 'change:isReadOnly', () => {
172
- this._hideUIAndRemoveMarker();
173
- } );
174
- this.on( 'requestFeed:response', ( evt, data ) => this._handleFeedResponse( data ) );
175
- this.on( 'requestFeed:error', () => this._hideUIAndRemoveMarker() );
176
-
177
- // Checks if a given key code is handled by the mention UI.
178
- //
179
- // @param {Number}
180
- // @returns {Boolean}
181
- function isHandledKey( keyCode ) {
182
- return handledKeyCodes.includes( keyCode );
183
- }
184
- }
185
-
186
- /**
187
- * @inheritDoc
188
- */
189
- destroy() {
190
- super.destroy();
191
-
192
- // Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
193
- this._mentionsView.destroy();
194
- }
195
-
196
- /**
197
- * Returns true when {@link #_mentionsView} is in the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon} and it is
198
- * currently visible.
199
- *
200
- * @readonly
201
- * @protected
202
- * @type {Boolean}
203
- */
204
- get _isUIVisible() {
205
- return this._balloon.visibleView === this._mentionsView;
206
- }
207
-
208
- /**
209
- * Creates the {@link #_mentionsView}.
210
- *
211
- * @private
212
- * @returns {module:mention/ui/mentionsview~MentionsView}
213
- */
214
- _createMentionView() {
215
- const locale = this.editor.locale;
216
-
217
- const mentionsView = new MentionsView( locale );
218
-
219
- this._items = new Collection();
220
-
221
- mentionsView.items.bindTo( this._items ).using( data => {
222
- const { item, marker } = data;
223
-
224
- // Set to 10 by default for backwards compatibility. See: #10479
225
- const dropdownLimit = this.editor.config.get( 'mention.dropdownLimit' ) || 10;
226
-
227
- if ( mentionsView.items.length >= dropdownLimit ) {
228
- return;
229
- }
230
-
231
- const listItemView = new MentionListItemView( locale );
232
-
233
- const view = this._renderItem( item, marker );
234
- view.delegate( 'execute' ).to( listItemView );
235
-
236
- listItemView.children.add( view );
237
- listItemView.item = item;
238
- listItemView.marker = marker;
239
-
240
- listItemView.on( 'execute', () => {
241
- mentionsView.fire( 'execute', {
242
- item,
243
- marker
244
- } );
245
- } );
246
-
247
- return listItemView;
248
- } );
249
-
250
- mentionsView.on( 'execute', ( evt, data ) => {
251
- const editor = this.editor;
252
- const model = editor.model;
253
-
254
- const item = data.item;
255
- const marker = data.marker;
256
-
257
- const mentionMarker = editor.model.markers.get( 'mention' );
258
-
259
- // Create a range on matched text.
260
- const end = model.createPositionAt( model.document.selection.focus );
261
- const start = model.createPositionAt( mentionMarker.getStart() );
262
- const range = model.createRange( start, end );
263
-
264
- this._hideUIAndRemoveMarker();
265
-
266
- editor.execute( 'mention', {
267
- mention: item,
268
- text: item.text,
269
- marker,
270
- range
271
- } );
272
-
273
- editor.editing.view.focus();
274
- } );
275
-
276
- return mentionsView;
277
- }
278
-
279
- /**
280
- * Returns item renderer for the marker.
281
- *
282
- * @private
283
- * @param {String} marker
284
- * @returns {Function|null}
285
- */
286
- _getItemRenderer( marker ) {
287
- const { itemRenderer } = this._mentionsConfigurations.get( marker );
288
-
289
- return itemRenderer;
290
- }
291
-
292
- /**
293
- * Requests a feed from a configured callbacks.
294
- *
295
- * @private
296
- * @fires module:mention/mentionui~MentionUI#event:requestFeed:response
297
- * @fires module:mention/mentionui~MentionUI#event:requestFeed:discarded
298
- * @fires module:mention/mentionui~MentionUI#event:requestFeed:error
299
- * @param {String} marker
300
- * @param {String} feedText
301
- */
302
- _requestFeed( marker, feedText ) {
303
- // @if CK_DEBUG_MENTION // console.log( '%c[Feed]%c Requesting for', 'color: blue', 'color: black', `"${ feedText }"` );
304
-
305
- // Store the last requested feed - it is used to discard any out-of order requests.
306
- this._lastRequested = feedText;
307
-
308
- const { feedCallback } = this._mentionsConfigurations.get( marker );
309
- const feedResponse = feedCallback( feedText );
310
-
311
- const isAsynchronous = feedResponse instanceof Promise;
312
-
313
- // For synchronous feeds (e.g. callbacks, arrays) fire the response event immediately.
314
- if ( !isAsynchronous ) {
315
- /**
316
- * Fired whenever requested feed has a response.
317
- *
318
- * @event requestFeed:response
319
- * @param {Object} data Event data.
320
- * @param {Array.<module:mention/mention~MentionFeedItem>} data.feed Autocomplete items.
321
- * @param {String} data.marker The character which triggers autocompletion for mention.
322
- * @param {String} data.feedText The text for which feed items were requested.
323
- */
324
- this.fire( 'requestFeed:response', { feed: feedResponse, marker, feedText } );
325
-
326
- return;
327
- }
328
-
329
- // Handle the asynchronous responses.
330
- feedResponse
331
- .then( response => {
332
- // Check the feed text of this response with the last requested one so either:
333
- if ( this._lastRequested == feedText ) {
334
- // It is the same and fire the response event.
335
- this.fire( 'requestFeed:response', { feed: response, marker, feedText } );
336
- } else {
337
- // It is different - most probably out-of-order one, so fire the discarded event.
338
- /**
339
- * Fired whenever the requested feed was discarded. This happens when the response was delayed and
340
- * other feed was already requested.
341
- *
342
- * @event requestFeed:discarded
343
- * @param {Object} data Event data.
344
- * @param {Array.<module:mention/mention~MentionFeedItem>} data.feed Autocomplete items.
345
- * @param {String} data.marker The character which triggers autocompletion for mention.
346
- * @param {String} data.feedText The text for which feed items were requested.
347
- */
348
- this.fire( 'requestFeed:discarded', { feed: response, marker, feedText } );
349
- }
350
- } )
351
- .catch( error => {
352
- /**
353
- * Fired whenever the requested {@link module:mention/mention~MentionFeed#feed} promise fails with error.
354
- *
355
- * @event requestFeed:error
356
- * @param {Object} data Event data.
357
- * @param {Error} data.error The error that was caught.
358
- */
359
- this.fire( 'requestFeed:error', { error } );
360
-
361
- /**
362
- * The callback used for obtaining mention autocomplete feed thrown and error and the mention UI was hidden or
363
- * not displayed at all.
364
- *
365
- * @error mention-feed-callback-error
366
- * @param {String} marker Configured marker for which error occurred.
367
- */
368
- logWarning( 'mention-feed-callback-error', { marker } );
369
- } );
370
- }
371
-
372
- /**
373
- * Registers a text watcher for the marker.
374
- *
375
- * @private
376
- * @param {Array.<Object>} feeds Feeds of mention plugin configured in editor
377
- * @returns {module:typing/textwatcher~TextWatcher}
378
- */
379
- _setupTextWatcher( feeds ) {
380
- const editor = this.editor;
381
-
382
- const feedsWithPattern = feeds.map( feed => ( {
383
- ...feed,
384
- pattern: createRegExp( feed.marker, feed.minimumCharacters || 0 )
385
- } ) );
386
-
387
- const watcher = new TextWatcher( editor.model, createTestCallback( feedsWithPattern ) );
388
-
389
- watcher.on( 'matched', ( evt, data ) => {
390
- const markerDefinition = getLastValidMarkerInText( feedsWithPattern, data.text );
391
- const selection = editor.model.document.selection;
392
- const focus = selection.focus;
393
- const markerPosition = editor.model.createPositionAt( focus.parent, markerDefinition.position );
394
-
395
- if ( isPositionInExistingMention( focus ) || isMarkerInExistingMention( markerPosition ) ) {
396
- this._hideUIAndRemoveMarker();
397
-
398
- return;
399
- }
400
-
401
- const feedText = requestFeedText( markerDefinition, data.text );
402
- const matchedTextLength = markerDefinition.marker.length + feedText.length;
403
-
404
- // Create a marker range.
405
- const start = focus.getShiftedBy( -matchedTextLength );
406
- const end = focus.getShiftedBy( -feedText.length );
407
-
408
- const markerRange = editor.model.createRange( start, end );
409
-
410
- // @if CK_DEBUG_MENTION // console.group( '%c[TextWatcher]%c matched', 'color: red', 'color: black', `"${ feedText }"` );
411
- // @if CK_DEBUG_MENTION // console.log( 'data#text', `"${ data.text }"` );
412
- // @if CK_DEBUG_MENTION // console.log( 'data#range', data.range.start.path, data.range.end.path );
413
- // @if CK_DEBUG_MENTION // console.log( 'marker definition', markerDefinition );
414
- // @if CK_DEBUG_MENTION // console.log( 'marker range', markerRange.start.path, markerRange.end.path );
415
-
416
- if ( checkIfStillInCompletionMode( editor ) ) {
417
- const mentionMarker = editor.model.markers.get( 'mention' );
418
-
419
- // Update the marker - user might've moved the selection to other mention trigger.
420
- editor.model.change( writer => {
421
- // @if CK_DEBUG_MENTION // console.log( '%c[Editing]%c Updating the marker.', 'color: purple', 'color: black' );
422
-
423
- writer.updateMarker( mentionMarker, { range: markerRange } );
424
- } );
425
- } else {
426
- editor.model.change( writer => {
427
- // @if CK_DEBUG_MENTION // console.log( '%c[Editing]%c Adding the marker.', 'color: purple', 'color: black' );
428
-
429
- writer.addMarker( 'mention', { range: markerRange, usingOperation: false, affectsData: false } );
430
- } );
431
- }
432
-
433
- this._requestFeedDebounced( markerDefinition.marker, feedText );
434
-
435
- // @if CK_DEBUG_MENTION // console.groupEnd( '[TextWatcher] matched' );
436
- } );
437
-
438
- watcher.on( 'unmatched', () => {
439
- this._hideUIAndRemoveMarker();
440
- } );
441
-
442
- const mentionCommand = editor.commands.get( 'mention' );
443
- watcher.bind( 'isEnabled' ).to( mentionCommand );
444
-
445
- return watcher;
446
- }
447
-
448
- /**
449
- * Handles the feed response event data.
450
- *
451
- * @param data
452
- * @private
453
- */
454
- _handleFeedResponse( data ) {
455
- const { feed, marker } = data;
456
-
457
- // eslint-disable-next-line max-len
458
- // @if CK_DEBUG_MENTION // console.log( `%c[Feed]%c Response for "${ data.feedText }" (${ feed.length })`, 'color: blue', 'color: black', feed );
459
-
460
- // If the marker is not in the document happens when the selection had changed and the 'mention' marker was removed.
461
- if ( !checkIfStillInCompletionMode( this.editor ) ) {
462
- return;
463
- }
464
-
465
- // Reset the view.
466
- this._items.clear();
467
-
468
- for ( const feedItem of feed ) {
469
- const item = typeof feedItem != 'object' ? { id: feedItem, text: feedItem } : feedItem;
470
-
471
- this._items.add( { item, marker } );
472
- }
473
-
474
- const mentionMarker = this.editor.model.markers.get( 'mention' );
475
-
476
- if ( this._items.length ) {
477
- this._showOrUpdateUI( mentionMarker );
478
- } else {
479
- // Do not show empty mention UI.
480
- this._hideUIAndRemoveMarker();
481
- }
482
- }
483
-
484
- /**
485
- * Shows the mentions balloon. If the panel is already visible, it will reposition it.
486
- *
487
- * @private
488
- */
489
- _showOrUpdateUI( markerMarker ) {
490
- if ( this._isUIVisible ) {
491
- // @if CK_DEBUG_MENTION // console.log( '%c[UI]%c Updating position.', 'color: green', 'color: black' );
492
-
493
- // Update balloon position as the mention list view may change its size.
494
- this._balloon.updatePosition( this._getBalloonPanelPositionData( markerMarker, this._mentionsView.position ) );
495
- } else {
496
- // @if CK_DEBUG_MENTION // console.log( '%c[UI]%c Showing the UI.', 'color: green', 'color: black' );
497
-
498
- this._balloon.add( {
499
- view: this._mentionsView,
500
- position: this._getBalloonPanelPositionData( markerMarker, this._mentionsView.position ),
501
- singleViewMode: true
502
- } );
503
- }
504
-
505
- this._mentionsView.position = this._balloon.view.position;
506
- this._mentionsView.selectFirst();
507
- }
508
-
509
- /**
510
- * Hides the mentions balloon and removes the 'mention' marker from the markers collection.
511
- *
512
- * @private
513
- */
514
- _hideUIAndRemoveMarker() {
515
- // Remove the mention view from balloon before removing marker - it is used by balloon position target().
516
- if ( this._balloon.hasView( this._mentionsView ) ) {
517
- // @if CK_DEBUG_MENTION // console.log( '%c[UI]%c Hiding the UI.', 'color: green', 'color: black' );
518
-
519
- this._balloon.remove( this._mentionsView );
520
- }
521
-
522
- if ( checkIfStillInCompletionMode( this.editor ) ) {
523
- // @if CK_DEBUG_MENTION // console.log( '%c[Editing]%c Removing marker.', 'color: purple', 'color: black' );
524
-
525
- this.editor.model.change( writer => writer.removeMarker( 'mention' ) );
526
- }
527
-
528
- // Make the last matched position on panel view undefined so the #_getBalloonPanelPositionData() method will return all positions
529
- // on the next call.
530
- this._mentionsView.position = undefined;
531
- }
532
-
533
- /**
534
- * Renders a single item in the autocomplete list.
535
- *
536
- * @private
537
- * @param {module:mention/mention~MentionFeedItem} item
538
- * @param {String} marker
539
- * @returns {module:ui/button/buttonview~ButtonView|module:mention/ui/domwrapperview~DomWrapperView}
540
- */
541
- _renderItem( item, marker ) {
542
- const editor = this.editor;
543
-
544
- let view;
545
- let label = item.id;
546
-
547
- const renderer = this._getItemRenderer( marker );
548
-
549
- if ( renderer ) {
550
- const renderResult = renderer( item );
551
-
552
- if ( typeof renderResult != 'string' ) {
553
- view = new DomWrapperView( editor.locale, renderResult );
554
- } else {
555
- label = renderResult;
556
- }
557
- }
558
-
559
- if ( !view ) {
560
- const buttonView = new ButtonView( editor.locale );
561
-
562
- buttonView.label = label;
563
- buttonView.withText = true;
564
-
565
- view = buttonView;
566
- }
567
-
568
- return view;
569
- }
570
-
571
- /**
572
- * Creates a position options object used to position the balloon panel.
573
- *
574
- * @param {module:engine/model/markercollection~Marker} mentionMarker
575
- * @param {String|undefined} preferredPosition The name of the last matched position name.
576
- * @returns {module:utils/dom/position~Options}
577
- * @private
578
- */
579
- _getBalloonPanelPositionData( mentionMarker, preferredPosition ) {
580
- const editor = this.editor;
581
- const editing = editor.editing;
582
- const domConverter = editing.view.domConverter;
583
- const mapper = editing.mapper;
584
-
585
- return {
586
- target: () => {
587
- let modelRange = mentionMarker.getRange();
588
-
589
- // Target the UI to the model selection range - the marker has been removed so probably the UI will not be shown anyway.
590
- // The logic is used by ContextualBalloon to display another panel in the same place.
591
- if ( modelRange.start.root.rootName == '$graveyard' ) {
592
- modelRange = editor.model.document.selection.getFirstRange();
593
- }
594
-
595
- const viewRange = mapper.toViewRange( modelRange );
596
- const rangeRects = Rect.getDomRangeRects( domConverter.viewRangeToDom( viewRange ) );
597
-
598
- return rangeRects.pop();
599
- },
600
- limiter: () => {
601
- const view = this.editor.editing.view;
602
- const viewDocument = view.document;
603
- const editableElement = viewDocument.selection.editableElement;
604
-
605
- if ( editableElement ) {
606
- return view.domConverter.mapViewToDom( editableElement.root );
607
- }
608
-
609
- return null;
610
- },
611
- positions: getBalloonPanelPositions( preferredPosition )
612
- };
613
- }
32
+ /**
33
+ * @inheritDoc
34
+ */
35
+ static get pluginName() {
36
+ return 'MentionUI';
37
+ }
38
+ /**
39
+ * @inheritDoc
40
+ */
41
+ static get requires() {
42
+ return [ContextualBalloon];
43
+ }
44
+ /**
45
+ * @inheritDoc
46
+ */
47
+ constructor(editor) {
48
+ super(editor);
49
+ this._items = new Collection();
50
+ this._mentionsView = this._createMentionView();
51
+ this._mentionsConfigurations = new Map();
52
+ this._requestFeedDebounced = debounce(this._requestFeed, 100);
53
+ editor.config.define('mention', { feeds: [] });
54
+ }
55
+ /**
56
+ * @inheritDoc
57
+ */
58
+ init() {
59
+ const editor = this.editor;
60
+ const commitKeys = editor.config.get('mention.commitKeys') || defaultCommitKeyCodes;
61
+ const handledKeyCodes = defaultHandledKeyCodes.concat(commitKeys);
62
+ this._balloon = editor.plugins.get(ContextualBalloon);
63
+ // Key listener that handles navigation in mention view.
64
+ editor.editing.view.document.on('keydown', (evt, data) => {
65
+ if (isHandledKey(data.keyCode) && this._isUIVisible) {
66
+ data.preventDefault();
67
+ evt.stop(); // Required for Enter key overriding.
68
+ if (data.keyCode == keyCodes.arrowdown) {
69
+ this._mentionsView.selectNext();
70
+ }
71
+ if (data.keyCode == keyCodes.arrowup) {
72
+ this._mentionsView.selectPrevious();
73
+ }
74
+ if (commitKeys.includes(data.keyCode)) {
75
+ this._mentionsView.executeSelected();
76
+ }
77
+ if (data.keyCode == keyCodes.esc) {
78
+ this._hideUIAndRemoveMarker();
79
+ }
80
+ }
81
+ }, { priority: 'highest' }); // Required to override the Enter key.
82
+ // Close the dropdown upon clicking outside of the plugin UI.
83
+ clickOutsideHandler({
84
+ emitter: this._mentionsView,
85
+ activator: () => this._isUIVisible,
86
+ contextElements: () => [this._balloon.view.element],
87
+ callback: () => this._hideUIAndRemoveMarker()
88
+ });
89
+ const feeds = editor.config.get('mention.feeds');
90
+ for (const mentionDescription of feeds) {
91
+ const feed = mentionDescription.feed;
92
+ const marker = mentionDescription.marker;
93
+ if (!isValidMentionMarker(marker)) {
94
+ /**
95
+ * The marker must be a single character.
96
+ *
97
+ * Correct markers: `'@'`, `'#'`.
98
+ *
99
+ * Incorrect markers: `'$$'`, `'[@'`.
100
+ *
101
+ * See {@link module:mention/mention~MentionConfig}.
102
+ *
103
+ * @error mentionconfig-incorrect-marker
104
+ * @param marker Configured marker
105
+ */
106
+ throw new CKEditorError('mentionconfig-incorrect-marker', null, { marker });
107
+ }
108
+ const feedCallback = typeof feed == 'function' ? feed.bind(this.editor) : createFeedCallback(feed);
109
+ const itemRenderer = mentionDescription.itemRenderer;
110
+ const definition = { marker, feedCallback, itemRenderer };
111
+ this._mentionsConfigurations.set(marker, definition);
112
+ }
113
+ this._setupTextWatcher(feeds);
114
+ this.listenTo(editor, 'change:isReadOnly', () => {
115
+ this._hideUIAndRemoveMarker();
116
+ });
117
+ this.on('requestFeed:response', (evt, data) => this._handleFeedResponse(data));
118
+ this.on('requestFeed:error', () => this._hideUIAndRemoveMarker());
119
+ /**
120
+ * Checks if a given key code is handled by the mention UI.
121
+ */
122
+ function isHandledKey(keyCode) {
123
+ return handledKeyCodes.includes(keyCode);
124
+ }
125
+ }
126
+ /**
127
+ * @inheritDoc
128
+ */
129
+ destroy() {
130
+ super.destroy();
131
+ // Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
132
+ this._mentionsView.destroy();
133
+ }
134
+ /**
135
+ * Returns true when {@link #_mentionsView} is in the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon} and it is
136
+ * currently visible.
137
+ */
138
+ get _isUIVisible() {
139
+ return this._balloon.visibleView === this._mentionsView;
140
+ }
141
+ /**
142
+ * Creates the {@link #_mentionsView}.
143
+ */
144
+ _createMentionView() {
145
+ const locale = this.editor.locale;
146
+ const mentionsView = new MentionsView(locale);
147
+ mentionsView.items.bindTo(this._items).using(data => {
148
+ const { item, marker } = data;
149
+ // Set to 10 by default for backwards compatibility. See: #10479
150
+ const dropdownLimit = this.editor.config.get('mention.dropdownLimit') || 10;
151
+ if (mentionsView.items.length >= dropdownLimit) {
152
+ return null;
153
+ }
154
+ const listItemView = new MentionListItemView(locale);
155
+ const view = this._renderItem(item, marker);
156
+ view.delegate('execute').to(listItemView);
157
+ listItemView.children.add(view);
158
+ listItemView.item = item;
159
+ listItemView.marker = marker;
160
+ listItemView.on('execute', () => {
161
+ mentionsView.fire('execute', {
162
+ item,
163
+ marker
164
+ });
165
+ });
166
+ return listItemView;
167
+ });
168
+ mentionsView.on('execute', (evt, data) => {
169
+ const editor = this.editor;
170
+ const model = editor.model;
171
+ const item = data.item;
172
+ const marker = data.marker;
173
+ const mentionMarker = editor.model.markers.get('mention');
174
+ // Create a range on matched text.
175
+ const end = model.createPositionAt(model.document.selection.focus);
176
+ const start = model.createPositionAt(mentionMarker.getStart());
177
+ const range = model.createRange(start, end);
178
+ this._hideUIAndRemoveMarker();
179
+ editor.execute('mention', {
180
+ mention: item,
181
+ text: item.text,
182
+ marker,
183
+ range
184
+ });
185
+ editor.editing.view.focus();
186
+ });
187
+ return mentionsView;
188
+ }
189
+ /**
190
+ * Returns item renderer for the marker.
191
+ */
192
+ _getItemRenderer(marker) {
193
+ const { itemRenderer } = this._mentionsConfigurations.get(marker);
194
+ return itemRenderer;
195
+ }
196
+ /**
197
+ * Requests a feed from a configured callbacks.
198
+ *
199
+ * @fires response
200
+ * @fires discarded
201
+ * @fires error
202
+ */
203
+ _requestFeed(marker, feedText) {
204
+ // @if CK_DEBUG_MENTION // console.log( '%c[Feed]%c Requesting for', 'color: blue', 'color: black', `"${ feedText }"` );
205
+ // Store the last requested feed - it is used to discard any out-of order requests.
206
+ this._lastRequested = feedText;
207
+ const { feedCallback } = this._mentionsConfigurations.get(marker);
208
+ const feedResponse = feedCallback(feedText);
209
+ const isAsynchronous = feedResponse instanceof Promise;
210
+ // For synchronous feeds (e.g. callbacks, arrays) fire the response event immediately.
211
+ if (!isAsynchronous) {
212
+ this.fire('requestFeed:response', { feed: feedResponse, marker, feedText });
213
+ return;
214
+ }
215
+ // Handle the asynchronous responses.
216
+ feedResponse
217
+ .then(response => {
218
+ // Check the feed text of this response with the last requested one so either:
219
+ if (this._lastRequested == feedText) {
220
+ // It is the same and fire the response event.
221
+ this.fire('requestFeed:response', { feed: response, marker, feedText });
222
+ }
223
+ else {
224
+ // It is different - most probably out-of-order one, so fire the discarded event.
225
+ this.fire('requestFeed:discarded', { feed: response, marker, feedText });
226
+ }
227
+ })
228
+ .catch(error => {
229
+ this.fire('requestFeed:error', { error });
230
+ /**
231
+ * The callback used for obtaining mention autocomplete feed thrown and error and the mention UI was hidden or
232
+ * not displayed at all.
233
+ *
234
+ * @error mention-feed-callback-error
235
+ */
236
+ logWarning('mention-feed-callback-error', { marker });
237
+ });
238
+ }
239
+ /**
240
+ * Registers a text watcher for the marker.
241
+ */
242
+ _setupTextWatcher(feeds) {
243
+ const editor = this.editor;
244
+ const feedsWithPattern = feeds.map(feed => ({
245
+ ...feed,
246
+ pattern: createRegExp(feed.marker, feed.minimumCharacters || 0)
247
+ }));
248
+ const watcher = new TextWatcher(editor.model, createTestCallback(feedsWithPattern));
249
+ watcher.on('matched', (evt, data) => {
250
+ const markerDefinition = getLastValidMarkerInText(feedsWithPattern, data.text);
251
+ const selection = editor.model.document.selection;
252
+ const focus = selection.focus;
253
+ const markerPosition = editor.model.createPositionAt(focus.parent, markerDefinition.position);
254
+ if (isPositionInExistingMention(focus) || isMarkerInExistingMention(markerPosition)) {
255
+ this._hideUIAndRemoveMarker();
256
+ return;
257
+ }
258
+ const feedText = requestFeedText(markerDefinition, data.text);
259
+ const matchedTextLength = markerDefinition.marker.length + feedText.length;
260
+ // Create a marker range.
261
+ const start = focus.getShiftedBy(-matchedTextLength);
262
+ const end = focus.getShiftedBy(-feedText.length);
263
+ const markerRange = editor.model.createRange(start, end);
264
+ // @if CK_DEBUG_MENTION // console.group( '%c[TextWatcher]%c matched', 'color: red', 'color: black', `"${ feedText }"` );
265
+ // @if CK_DEBUG_MENTION // console.log( 'data#text', `"${ data.text }"` );
266
+ // @if CK_DEBUG_MENTION // console.log( 'data#range', data.range.start.path, data.range.end.path );
267
+ // @if CK_DEBUG_MENTION // console.log( 'marker definition', markerDefinition );
268
+ // @if CK_DEBUG_MENTION // console.log( 'marker range', markerRange.start.path, markerRange.end.path );
269
+ if (checkIfStillInCompletionMode(editor)) {
270
+ const mentionMarker = editor.model.markers.get('mention');
271
+ // Update the marker - user might've moved the selection to other mention trigger.
272
+ editor.model.change(writer => {
273
+ // @if CK_DEBUG_MENTION // console.log( '%c[Editing]%c Updating the marker.', 'color: purple', 'color: black' );
274
+ writer.updateMarker(mentionMarker, { range: markerRange });
275
+ });
276
+ }
277
+ else {
278
+ editor.model.change(writer => {
279
+ // @if CK_DEBUG_MENTION // console.log( '%c[Editing]%c Adding the marker.', 'color: purple', 'color: black' );
280
+ writer.addMarker('mention', { range: markerRange, usingOperation: false, affectsData: false });
281
+ });
282
+ }
283
+ this._requestFeedDebounced(markerDefinition.marker, feedText);
284
+ // @if CK_DEBUG_MENTION // console.groupEnd( '[TextWatcher] matched' );
285
+ });
286
+ watcher.on('unmatched', () => {
287
+ this._hideUIAndRemoveMarker();
288
+ });
289
+ const mentionCommand = editor.commands.get('mention');
290
+ watcher.bind('isEnabled').to(mentionCommand);
291
+ return watcher;
292
+ }
293
+ /**
294
+ * Handles the feed response event data.
295
+ */
296
+ _handleFeedResponse(data) {
297
+ const { feed, marker } = data;
298
+ // eslint-disable-next-line max-len
299
+ // @if CK_DEBUG_MENTION // console.log( `%c[Feed]%c Response for "${ data.feedText }" (${ feed.length })`, 'color: blue', 'color: black', feed );
300
+ // If the marker is not in the document happens when the selection had changed and the 'mention' marker was removed.
301
+ if (!checkIfStillInCompletionMode(this.editor)) {
302
+ return;
303
+ }
304
+ // Reset the view.
305
+ this._items.clear();
306
+ for (const feedItem of feed) {
307
+ const item = typeof feedItem != 'object' ? { id: feedItem, text: feedItem } : feedItem;
308
+ this._items.add({ item, marker });
309
+ }
310
+ const mentionMarker = this.editor.model.markers.get('mention');
311
+ if (this._items.length) {
312
+ this._showOrUpdateUI(mentionMarker);
313
+ }
314
+ else {
315
+ // Do not show empty mention UI.
316
+ this._hideUIAndRemoveMarker();
317
+ }
318
+ }
319
+ /**
320
+ * Shows the mentions balloon. If the panel is already visible, it will reposition it.
321
+ */
322
+ _showOrUpdateUI(markerMarker) {
323
+ if (this._isUIVisible) {
324
+ // @if CK_DEBUG_MENTION // console.log( '%c[UI]%c Updating position.', 'color: green', 'color: black' );
325
+ // Update balloon position as the mention list view may change its size.
326
+ this._balloon.updatePosition(this._getBalloonPanelPositionData(markerMarker, this._mentionsView.position));
327
+ }
328
+ else {
329
+ // @if CK_DEBUG_MENTION // console.log( '%c[UI]%c Showing the UI.', 'color: green', 'color: black' );
330
+ this._balloon.add({
331
+ view: this._mentionsView,
332
+ position: this._getBalloonPanelPositionData(markerMarker, this._mentionsView.position),
333
+ singleViewMode: true
334
+ });
335
+ }
336
+ this._mentionsView.position = this._balloon.view.position;
337
+ this._mentionsView.selectFirst();
338
+ }
339
+ /**
340
+ * Hides the mentions balloon and removes the 'mention' marker from the markers collection.
341
+ */
342
+ _hideUIAndRemoveMarker() {
343
+ // Remove the mention view from balloon before removing marker - it is used by balloon position target().
344
+ if (this._balloon.hasView(this._mentionsView)) {
345
+ // @if CK_DEBUG_MENTION // console.log( '%c[UI]%c Hiding the UI.', 'color: green', 'color: black' );
346
+ this._balloon.remove(this._mentionsView);
347
+ }
348
+ if (checkIfStillInCompletionMode(this.editor)) {
349
+ // @if CK_DEBUG_MENTION // console.log( '%c[Editing]%c Removing marker.', 'color: purple', 'color: black' );
350
+ this.editor.model.change(writer => writer.removeMarker('mention'));
351
+ }
352
+ // Make the last matched position on panel view undefined so the #_getBalloonPanelPositionData() method will return all positions
353
+ // on the next call.
354
+ this._mentionsView.position = undefined;
355
+ }
356
+ /**
357
+ * Renders a single item in the autocomplete list.
358
+ */
359
+ _renderItem(item, marker) {
360
+ const editor = this.editor;
361
+ let view;
362
+ let label = item.id;
363
+ const renderer = this._getItemRenderer(marker);
364
+ if (renderer) {
365
+ const renderResult = renderer(item);
366
+ if (typeof renderResult != 'string') {
367
+ view = new DomWrapperView(editor.locale, renderResult);
368
+ }
369
+ else {
370
+ label = renderResult;
371
+ }
372
+ }
373
+ if (!view) {
374
+ const buttonView = new ButtonView(editor.locale);
375
+ buttonView.label = label;
376
+ buttonView.withText = true;
377
+ view = buttonView;
378
+ }
379
+ return view;
380
+ }
381
+ /**
382
+ * Creates a position options object used to position the balloon panel.
383
+ *
384
+ * @param mentionMarker
385
+ * @param preferredPosition The name of the last matched position name.
386
+ */
387
+ _getBalloonPanelPositionData(mentionMarker, preferredPosition) {
388
+ const editor = this.editor;
389
+ const editing = editor.editing;
390
+ const domConverter = editing.view.domConverter;
391
+ const mapper = editing.mapper;
392
+ return {
393
+ target: () => {
394
+ let modelRange = mentionMarker.getRange();
395
+ // Target the UI to the model selection range - the marker has been removed so probably the UI will not be shown anyway.
396
+ // The logic is used by ContextualBalloon to display another panel in the same place.
397
+ if (modelRange.start.root.rootName == '$graveyard') {
398
+ modelRange = editor.model.document.selection.getFirstRange();
399
+ }
400
+ const viewRange = mapper.toViewRange(modelRange);
401
+ const rangeRects = Rect.getDomRangeRects(domConverter.viewRangeToDom(viewRange));
402
+ return rangeRects.pop();
403
+ },
404
+ limiter: () => {
405
+ const view = this.editor.editing.view;
406
+ const viewDocument = view.document;
407
+ const editableElement = viewDocument.selection.editableElement;
408
+ if (editableElement) {
409
+ return view.domConverter.mapViewToDom(editableElement.root);
410
+ }
411
+ return null;
412
+ },
413
+ positions: getBalloonPanelPositions(preferredPosition)
414
+ };
415
+ }
614
416
  }
615
-
616
- // Returns the balloon positions data callbacks.
617
- //
618
- // @param {String} preferredPosition
619
- // @returns {Array.<module:utils/dom/position~Position>}
620
- function getBalloonPanelPositions( preferredPosition ) {
621
- const positions = {
622
- // Positions the panel to the southeast of the caret rectangle.
623
- 'caret_se': targetRect => {
624
- return {
625
- top: targetRect.bottom + VERTICAL_SPACING,
626
- left: targetRect.right,
627
- name: 'caret_se',
628
- config: {
629
- withArrow: false
630
- }
631
- };
632
- },
633
-
634
- // Positions the panel to the northeast of the caret rectangle.
635
- 'caret_ne': ( targetRect, balloonRect ) => {
636
- return {
637
- top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
638
- left: targetRect.right,
639
- name: 'caret_ne',
640
- config: {
641
- withArrow: false
642
- }
643
- };
644
- },
645
-
646
- // Positions the panel to the southwest of the caret rectangle.
647
- 'caret_sw': ( targetRect, balloonRect ) => {
648
- return {
649
- top: targetRect.bottom + VERTICAL_SPACING,
650
- left: targetRect.right - balloonRect.width,
651
- name: 'caret_sw',
652
- config: {
653
- withArrow: false
654
- }
655
- };
656
- },
657
-
658
- // Positions the panel to the northwest of the caret rect.
659
- 'caret_nw': ( targetRect, balloonRect ) => {
660
- return {
661
- top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
662
- left: targetRect.right - balloonRect.width,
663
- name: 'caret_nw',
664
- config: {
665
- withArrow: false
666
- }
667
- };
668
- }
669
- };
670
-
671
- // Returns only the last position if it was matched to prevent the panel from jumping after the first match.
672
- if ( Object.prototype.hasOwnProperty.call( positions, preferredPosition ) ) {
673
- return [
674
- positions[ preferredPosition ]
675
- ];
676
- }
677
-
678
- // By default return all position callbacks.
679
- return [
680
- positions.caret_se,
681
- positions.caret_sw,
682
- positions.caret_ne,
683
- positions.caret_nw
684
- ];
417
+ /**
418
+ * Returns the balloon positions data callbacks.
419
+ */
420
+ function getBalloonPanelPositions(preferredPosition) {
421
+ const positions = {
422
+ // Positions the panel to the southeast of the caret rectangle.
423
+ 'caret_se': (targetRect) => {
424
+ return {
425
+ top: targetRect.bottom + VERTICAL_SPACING,
426
+ left: targetRect.right,
427
+ name: 'caret_se',
428
+ config: {
429
+ withArrow: false
430
+ }
431
+ };
432
+ },
433
+ // Positions the panel to the northeast of the caret rectangle.
434
+ 'caret_ne': (targetRect, balloonRect) => {
435
+ return {
436
+ top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
437
+ left: targetRect.right,
438
+ name: 'caret_ne',
439
+ config: {
440
+ withArrow: false
441
+ }
442
+ };
443
+ },
444
+ // Positions the panel to the southwest of the caret rectangle.
445
+ 'caret_sw': (targetRect, balloonRect) => {
446
+ return {
447
+ top: targetRect.bottom + VERTICAL_SPACING,
448
+ left: targetRect.right - balloonRect.width,
449
+ name: 'caret_sw',
450
+ config: {
451
+ withArrow: false
452
+ }
453
+ };
454
+ },
455
+ // Positions the panel to the northwest of the caret rect.
456
+ 'caret_nw': (targetRect, balloonRect) => {
457
+ return {
458
+ top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
459
+ left: targetRect.right - balloonRect.width,
460
+ name: 'caret_nw',
461
+ config: {
462
+ withArrow: false
463
+ }
464
+ };
465
+ }
466
+ };
467
+ // Returns only the last position if it was matched to prevent the panel from jumping after the first match.
468
+ if (Object.prototype.hasOwnProperty.call(positions, preferredPosition)) {
469
+ return [
470
+ positions[preferredPosition]
471
+ ];
472
+ }
473
+ // By default return all position callbacks.
474
+ return [
475
+ positions.caret_se,
476
+ positions.caret_sw,
477
+ positions.caret_ne,
478
+ positions.caret_nw
479
+ ];
685
480
  }
686
-
687
- // Returns a marker definition of the last valid occurring marker in a given string.
688
- // If there is no valid marker in a string, it returns undefined.
689
- //
690
- // Example of returned object:
691
- //
692
- // {
693
- // marker: '@',
694
- // position: 4,
695
- // minimumCharacters: 0
696
- // }
697
- //
698
- // @param {Array.<Object>} feedsWithPattern Registered feeds in editor for mention plugin with created RegExp for matching marker.
699
- // @param {String} text String to find the marker in
700
- // @returns {Object} Matched marker's definition
701
- function getLastValidMarkerInText( feedsWithPattern, text ) {
702
- let lastValidMarker;
703
-
704
- for ( const feed of feedsWithPattern ) {
705
- const currentMarkerLastIndex = text.lastIndexOf( feed.marker );
706
-
707
- if ( currentMarkerLastIndex > 0 && !text.substring( currentMarkerLastIndex - 1 ).match( feed.pattern ) ) {
708
- continue;
709
- }
710
-
711
- if ( !lastValidMarker || currentMarkerLastIndex >= lastValidMarker.position ) {
712
- lastValidMarker = {
713
- marker: feed.marker,
714
- position: currentMarkerLastIndex,
715
- minimumCharacters: feed.minimumCharacters,
716
- pattern: feed.pattern
717
- };
718
- }
719
- }
720
-
721
- return lastValidMarker;
481
+ /**
482
+ * Returns a marker definition of the last valid occurring marker in a given string.
483
+ * If there is no valid marker in a string, it returns undefined.
484
+ *
485
+ * Example of returned object:
486
+ *
487
+ * ```ts
488
+ * {
489
+ * marker: '@',
490
+ * position: 4,
491
+ * minimumCharacters: 0
492
+ * }
493
+ * ````
494
+ *
495
+ * @param feedsWithPattern Registered feeds in editor for mention plugin with created RegExp for matching marker.
496
+ * @param text String to find the marker in
497
+ * @returns Matched marker's definition
498
+ */
499
+ function getLastValidMarkerInText(feedsWithPattern, text) {
500
+ let lastValidMarker;
501
+ for (const feed of feedsWithPattern) {
502
+ const currentMarkerLastIndex = text.lastIndexOf(feed.marker);
503
+ if (currentMarkerLastIndex > 0 && !text.substring(currentMarkerLastIndex - 1).match(feed.pattern)) {
504
+ continue;
505
+ }
506
+ if (!lastValidMarker || currentMarkerLastIndex >= lastValidMarker.position) {
507
+ lastValidMarker = {
508
+ marker: feed.marker,
509
+ position: currentMarkerLastIndex,
510
+ minimumCharacters: feed.minimumCharacters,
511
+ pattern: feed.pattern
512
+ };
513
+ }
514
+ }
515
+ return lastValidMarker;
722
516
  }
723
-
724
- // Creates a RegExp pattern for the marker.
725
- //
726
- // Function has to be exported to achieve 100% code coverage.
727
- //
728
- // @param {String} marker
729
- // @param {Number} minimumCharacters
730
- // @returns {RegExp}
731
- export function createRegExp( marker, minimumCharacters ) {
732
- const numberOfCharacters = minimumCharacters == 0 ? '*' : `{${ minimumCharacters },}`;
733
-
734
- const openAfterCharacters = env.features.isRegExpUnicodePropertySupported ? '\\p{Ps}\\p{Pi}"\'' : '\\(\\[{"\'';
735
- const mentionCharacters = '.';
736
-
737
- // The pattern consists of 3 groups:
738
- // - 0 (non-capturing): Opening sequence - start of the line, space or an opening punctuation character like "(" or "\"",
739
- // - 1: The marker character,
740
- // - 2: Mention input (taking the minimal length into consideration to trigger the UI),
741
- //
742
- // The pattern matches up to the caret (end of string switch - $).
743
- // (0: opening sequence )(1: marker )(2: typed mention )$
744
- const pattern = `(?:^|[ ${ openAfterCharacters }])([${ marker }])(${ mentionCharacters }${ numberOfCharacters })$`;
745
- return new RegExp( pattern, 'u' );
517
+ /**
518
+ * Creates a RegExp pattern for the marker.
519
+ *
520
+ * Function has to be exported to achieve 100% code coverage.
521
+ */
522
+ export function createRegExp(marker, minimumCharacters) {
523
+ const numberOfCharacters = minimumCharacters == 0 ? '*' : `{${minimumCharacters},}`;
524
+ const openAfterCharacters = env.features.isRegExpUnicodePropertySupported ? '\\p{Ps}\\p{Pi}"\'' : '\\(\\[{"\'';
525
+ const mentionCharacters = '.';
526
+ // The pattern consists of 3 groups:
527
+ // - 0 (non-capturing): Opening sequence - start of the line, space or an opening punctuation character like "(" or "\"",
528
+ // - 1: The marker character,
529
+ // - 2: Mention input (taking the minimal length into consideration to trigger the UI),
530
+ //
531
+ // The pattern matches up to the caret (end of string switch - $).
532
+ // (0: opening sequence )(1: marker )(2: typed mention )$
533
+ const pattern = `(?:^|[ ${openAfterCharacters}])([${marker}])(${mentionCharacters}${numberOfCharacters})$`;
534
+ return new RegExp(pattern, 'u');
746
535
  }
747
-
748
- // Creates a test callback for the marker to be used in the text watcher instance.
749
- //
750
- // @param {Array.<Object>} feedsWithPattern Feeds of mention plugin configured in editor with RegExp to match marker in text
751
- // @returns {Function}
752
- function createTestCallback( feedsWithPattern ) {
753
- const textMatcher = text => {
754
- const markerDefinition = getLastValidMarkerInText( feedsWithPattern, text );
755
-
756
- if ( !markerDefinition ) {
757
- return false;
758
- }
759
-
760
- let splitStringFrom = 0;
761
-
762
- if ( markerDefinition.position !== 0 ) {
763
- splitStringFrom = markerDefinition.position - 1;
764
- }
765
-
766
- const textToTest = text.substring( splitStringFrom );
767
-
768
- return markerDefinition.pattern.test( textToTest );
769
- };
770
-
771
- return textMatcher;
536
+ /**
537
+ * Creates a test callback for the marker to be used in the text watcher instance.
538
+ *
539
+ * @param feedsWithPattern Feeds of mention plugin configured in editor with RegExp to match marker in text
540
+ */
541
+ function createTestCallback(feedsWithPattern) {
542
+ const textMatcher = (text) => {
543
+ const markerDefinition = getLastValidMarkerInText(feedsWithPattern, text);
544
+ if (!markerDefinition) {
545
+ return false;
546
+ }
547
+ let splitStringFrom = 0;
548
+ if (markerDefinition.position !== 0) {
549
+ splitStringFrom = markerDefinition.position - 1;
550
+ }
551
+ const textToTest = text.substring(splitStringFrom);
552
+ return markerDefinition.pattern.test(textToTest);
553
+ };
554
+ return textMatcher;
772
555
  }
773
-
774
- // Creates a text matcher from the marker.
775
- //
776
- // @param {Object} markerDefinition
777
- // @param {String} text
778
- // @returns {Function}
779
- function requestFeedText( markerDefinition, text ) {
780
- let splitStringFrom = 0;
781
-
782
- if ( markerDefinition.position !== 0 ) {
783
- splitStringFrom = markerDefinition.position - 1;
784
- }
785
-
786
- const regExp = createRegExp( markerDefinition.marker, 0 );
787
- const textToMatch = text.substring( splitStringFrom );
788
- const match = textToMatch.match( regExp );
789
-
790
- return match[ 2 ];
556
+ /**
557
+ * Creates a text matcher from the marker.
558
+ */
559
+ function requestFeedText(markerDefinition, text) {
560
+ let splitStringFrom = 0;
561
+ if (markerDefinition.position !== 0) {
562
+ splitStringFrom = markerDefinition.position - 1;
563
+ }
564
+ const regExp = createRegExp(markerDefinition.marker, 0);
565
+ const textToMatch = text.substring(splitStringFrom);
566
+ const match = textToMatch.match(regExp);
567
+ return match[2];
791
568
  }
792
-
793
- // The default feed callback.
794
- function createFeedCallback( feedItems ) {
795
- return feedText => {
796
- const filteredItems = feedItems
797
- // Make the default mention feed case-insensitive.
798
- .filter( item => {
799
- // Item might be defined as object.
800
- const itemId = typeof item == 'string' ? item : String( item.id );
801
-
802
- // The default feed is case insensitive.
803
- return itemId.toLowerCase().includes( feedText.toLowerCase() );
804
- } );
805
- return filteredItems;
806
- };
569
+ /**
570
+ * The default feed callback.
571
+ */
572
+ function createFeedCallback(feedItems) {
573
+ return (feedText) => {
574
+ const filteredItems = feedItems
575
+ // Make the default mention feed case-insensitive.
576
+ .filter(item => {
577
+ // Item might be defined as object.
578
+ const itemId = typeof item == 'string' ? item : String(item.id);
579
+ // The default feed is case insensitive.
580
+ return itemId.toLowerCase().includes(feedText.toLowerCase());
581
+ });
582
+ return filteredItems;
583
+ };
807
584
  }
808
-
809
- // Checks if position in inside or right after a text with a mention.
810
- //
811
- // @param {module:engine/model/position~Position} position.
812
- // @returns {Boolean}
813
- function isPositionInExistingMention( position ) {
814
- // The text watcher listens only to changed range in selection - so the selection attributes are not yet available
815
- // and you cannot use selection.hasAttribute( 'mention' ) just yet.
816
- // See https://github.com/ckeditor/ckeditor5-engine/issues/1723.
817
- const hasMention = position.textNode && position.textNode.hasAttribute( 'mention' );
818
-
819
- const nodeBefore = position.nodeBefore;
820
-
821
- return hasMention || nodeBefore && nodeBefore.is( '$text' ) && nodeBefore.hasAttribute( 'mention' );
585
+ /**
586
+ * Checks if position in inside or right after a text with a mention.
587
+ */
588
+ function isPositionInExistingMention(position) {
589
+ // The text watcher listens only to changed range in selection - so the selection attributes are not yet available
590
+ // and you cannot use selection.hasAttribute( 'mention' ) just yet.
591
+ // See https://github.com/ckeditor/ckeditor5-engine/issues/1723.
592
+ const hasMention = position.textNode && position.textNode.hasAttribute('mention');
593
+ const nodeBefore = position.nodeBefore;
594
+ return hasMention || nodeBefore && nodeBefore.is('$text') && nodeBefore.hasAttribute('mention');
822
595
  }
823
-
824
- // Checks if the closest marker offset is at the beginning of a mention.
825
- //
826
- // See https://github.com/ckeditor/ckeditor5/issues/11400.
827
- //
828
- // @param {module:engine/model/position~Position} markerPosition
829
- // @returns {Boolean}
830
- function isMarkerInExistingMention( markerPosition ) {
831
- const nodeAfter = markerPosition.nodeAfter;
832
-
833
- return nodeAfter && nodeAfter.is( '$text' ) && nodeAfter.hasAttribute( 'mention' );
596
+ /**
597
+ * Checks if the closest marker offset is at the beginning of a mention.
598
+ *
599
+ * See https://github.com/ckeditor/ckeditor5/issues/11400.
600
+ */
601
+ function isMarkerInExistingMention(markerPosition) {
602
+ const nodeAfter = markerPosition.nodeAfter;
603
+ return nodeAfter && nodeAfter.is('$text') && nodeAfter.hasAttribute('mention');
834
604
  }
835
-
836
- // Checks if string is a valid mention marker.
837
- //
838
- // @param {String} marker
839
- // @returns {Boolean}
840
- function isValidMentionMarker( marker ) {
841
- return marker && marker.length == 1;
605
+ /**
606
+ * Checks if string is a valid mention marker.
607
+ */
608
+ function isValidMentionMarker(marker) {
609
+ return marker && marker.length == 1;
842
610
  }
843
-
844
- // Checks the mention plugins is in completion mode (e.g. when typing is after a valid mention string like @foo).
845
- //
846
- // @returns {Boolean}
847
- function checkIfStillInCompletionMode( editor ) {
848
- return editor.model.markers.has( 'mention' );
611
+ /**
612
+ * Checks the mention plugins is in completion mode (e.g. when typing is after a valid mention string like @foo).
613
+ */
614
+ function checkIfStillInCompletionMode(editor) {
615
+ return editor.model.markers.has('mention');
849
616
  }