@ckeditor/ckeditor5-mention 48.2.0 → 48.3.0-alpha.1

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/dist/index.js CHANGED
@@ -2,1212 +2,1093 @@
2
2
  * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
- import { Command, Plugin } from '@ckeditor/ckeditor5-core/dist/index.js';
6
- import { CKEditorError, toMap, uid, Rect, keyCodes, Collection, logWarning, env } from '@ckeditor/ckeditor5-utils/dist/index.js';
7
- import { ListView, View, ListItemView, ContextualBalloon, clickOutsideHandler, ButtonView } from '@ckeditor/ckeditor5-ui/dist/index.js';
8
- import { TextWatcher } from '@ckeditor/ckeditor5-typing/dist/index.js';
9
- import { debounce } from 'es-toolkit/compat';
5
+ import { Command, Plugin } from "@ckeditor/ckeditor5-core";
6
+ import { CKEditorError, Collection, Rect, env, keyCodes, logWarning, toMap, uid } from "@ckeditor/ckeditor5-utils";
7
+ import { ButtonView, ContextualBalloon, ListItemView, ListView, View, clickOutsideHandler } from "@ckeditor/ckeditor5-ui";
8
+ import { TextWatcher } from "@ckeditor/ckeditor5-typing";
9
+ import { debounce } from "es-toolkit/compat";
10
10
 
11
+ /**
12
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
13
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
14
+ */
15
+ /**
16
+ * @module mention/mentioncommand
17
+ */
11
18
  const BRACKET_PAIRS = {
12
- '(': ')',
13
- '[': ']',
14
- '{': '}'
19
+ "(": ")",
20
+ "[": "]",
21
+ "{": "}"
15
22
  };
16
23
  /**
17
- * The mention command.
18
- *
19
- * The command is registered by {@link module:mention/mentionediting~MentionEditing} as `'mention'`.
20
- *
21
- * To insert a mention into a range, execute the command and specify a mention object with a range to replace:
22
- *
23
- * ```ts
24
- * const focus = editor.model.document.selection.focus;
25
- *
26
- * // It will replace one character before the selection focus with the '#1234' text
27
- * // with the mention attribute filled with passed attributes.
28
- * editor.execute( 'mention', {
29
- * marker: '#',
30
- * mention: {
31
- * id: '#1234',
32
- * name: 'Foo',
33
- * title: 'Big Foo'
34
- * },
35
- * range: editor.model.createRange( focus.getShiftedBy( -1 ), focus )
36
- * } );
37
- *
38
- * // It will replace one character before the selection focus with the 'The "Big Foo"' text
39
- * // with the mention attribute filled with passed attributes.
40
- * editor.execute( 'mention', {
41
- * marker: '#',
42
- * mention: {
43
- * id: '#1234',
44
- * name: 'Foo',
45
- * title: 'Big Foo'
46
- * },
47
- * text: 'The "Big Foo"',
48
- * range: editor.model.createRange( focus.getShiftedBy( -1 ), focus )
49
- * } );
50
- * ```
51
- */ class MentionCommand extends Command {
52
- /**
53
- * @inheritDoc
54
- */ constructor(editor){
55
- super(editor);
56
- // Since this command may pass range in execution parameters, it should be checked directly in execute block.
57
- this._isEnabledBasedOnSelection = false;
58
- }
59
- /**
60
- * @inheritDoc
61
- */ refresh() {
62
- const model = this.editor.model;
63
- const doc = model.document;
64
- this.isEnabled = model.schema.checkAttributeInSelection(doc.selection, 'mention');
65
- }
66
- /**
67
- * Executes the command.
68
- *
69
- * @param options Options for the executed command.
70
- * @param options.mention The mention object to insert. When a string is passed, it will be used to create a plain
71
- * object with the name attribute that equals the passed string.
72
- * @param options.marker The marker character (e.g. `'@'`).
73
- * @param options.text The text of the inserted mention. Defaults to the full mention string composed from `marker` and
74
- * `mention` string or `mention.id` if an object is passed.
75
- * @param options.range The range to replace.
76
- * Note that the replaced range might be shorter than the inserted text with the mention attribute.
77
- * @fires execute
78
- */ execute(options) {
79
- const model = this.editor.model;
80
- const document = model.document;
81
- const selection = document.selection;
82
- const mentionData = typeof options.mention == 'string' ? {
83
- id: options.mention
84
- } : options.mention;
85
- const mentionID = mentionData.id;
86
- const range = options.range || selection.getFirstRange();
87
- // Don't execute command if range is in non-editable place.
88
- if (!model.canEditAt(range)) {
89
- return;
90
- }
91
- const mentionText = options.text || mentionID;
92
- const mention = _addMentionAttributes({
93
- _text: mentionText,
94
- id: mentionID
95
- }, mentionData);
96
- if (!mentionID.startsWith(options.marker)) {
97
- /**
98
- * The feed item ID must start with the marker character(s).
99
- *
100
- * Correct mention feed setting:
101
- *
102
- * ```ts
103
- * mentions: [
104
- * {
105
- * marker: '@',
106
- * feed: [ '@Ann', '@Barney', ... ]
107
- * }
108
- * ]
109
- * ```
110
- *
111
- * Incorrect mention feed setting:
112
- *
113
- * ```ts
114
- * mentions: [
115
- * {
116
- * marker: '@',
117
- * feed: [ 'Ann', 'Barney', ... ]
118
- * }
119
- * ]
120
- * ```
121
- *
122
- * See {@link module:mention/mentionconfig~MentionConfig}.
123
- *
124
- * @error mentioncommand-incorrect-id
125
- */ throw new CKEditorError('mentioncommand-incorrect-id', this);
126
- }
127
- model.change((writer)=>{
128
- const currentAttributes = toMap(selection.getAttributes());
129
- const attributesWithMention = new Map(currentAttributes.entries());
130
- attributesWithMention.set('mention', mention);
131
- // Replace a range with the text with a mention.
132
- const insertionRange = model.insertContent(writer.createText(mentionText, attributesWithMention), range);
133
- const nodeBefore = insertionRange.start.nodeBefore;
134
- const nodeAfter = insertionRange.end.nodeAfter;
135
- const isFollowedByWhiteSpace = nodeAfter && nodeAfter.is('$text') && nodeAfter.data.startsWith(' ');
136
- let isInsertedInBrackets = false;
137
- if (nodeBefore && nodeAfter && nodeBefore.is('$text') && nodeAfter.is('$text')) {
138
- const precedingCharacter = nodeBefore.data.slice(-1);
139
- const isPrecededByOpeningBracket = precedingCharacter in BRACKET_PAIRS;
140
- const isFollowedByBracketClosure = isPrecededByOpeningBracket && nodeAfter.data.startsWith(BRACKET_PAIRS[precedingCharacter]);
141
- isInsertedInBrackets = isPrecededByOpeningBracket && isFollowedByBracketClosure;
142
- }
143
- // Don't add a white space if either of the following is true:
144
- // * there's already one after the mention;
145
- // * the mention was inserted in the empty matching brackets.
146
- // https://github.com/ckeditor/ckeditor5/issues/4651
147
- if (!isInsertedInBrackets && !isFollowedByWhiteSpace) {
148
- model.insertContent(writer.createText(' ', currentAttributes), range.start.getShiftedBy(mentionText.length));
149
- }
150
- });
151
- }
152
- }
24
+ * The mention command.
25
+ *
26
+ * The command is registered by {@link module:mention/mentionediting~MentionEditing} as `'mention'`.
27
+ *
28
+ * To insert a mention into a range, execute the command and specify a mention object with a range to replace:
29
+ *
30
+ * ```ts
31
+ * const focus = editor.model.document.selection.focus;
32
+ *
33
+ * // It will replace one character before the selection focus with the '#1234' text
34
+ * // with the mention attribute filled with passed attributes.
35
+ * editor.execute( 'mention', {
36
+ * marker: '#',
37
+ * mention: {
38
+ * id: '#1234',
39
+ * name: 'Foo',
40
+ * title: 'Big Foo'
41
+ * },
42
+ * range: editor.model.createRange( focus.getShiftedBy( -1 ), focus )
43
+ * } );
44
+ *
45
+ * // It will replace one character before the selection focus with the 'The "Big Foo"' text
46
+ * // with the mention attribute filled with passed attributes.
47
+ * editor.execute( 'mention', {
48
+ * marker: '#',
49
+ * mention: {
50
+ * id: '#1234',
51
+ * name: 'Foo',
52
+ * title: 'Big Foo'
53
+ * },
54
+ * text: 'The "Big Foo"',
55
+ * range: editor.model.createRange( focus.getShiftedBy( -1 ), focus )
56
+ * } );
57
+ * ```
58
+ */
59
+ var MentionCommand = class extends Command {
60
+ /**
61
+ * @inheritDoc
62
+ */
63
+ constructor(editor) {
64
+ super(editor);
65
+ this._isEnabledBasedOnSelection = false;
66
+ }
67
+ /**
68
+ * @inheritDoc
69
+ */
70
+ refresh() {
71
+ const model = this.editor.model;
72
+ const doc = model.document;
73
+ this.isEnabled = model.schema.checkAttributeInSelection(doc.selection, "mention");
74
+ }
75
+ /**
76
+ * Executes the command.
77
+ *
78
+ * @param options Options for the executed command.
79
+ * @param options.mention The mention object to insert. When a string is passed, it will be used to create a plain
80
+ * object with the name attribute that equals the passed string.
81
+ * @param options.marker The marker character (e.g. `'@'`).
82
+ * @param options.text The text of the inserted mention. Defaults to the full mention string composed from `marker` and
83
+ * `mention` string or `mention.id` if an object is passed.
84
+ * @param options.range The range to replace.
85
+ * Note that the replaced range might be shorter than the inserted text with the mention attribute.
86
+ * @fires execute
87
+ */
88
+ execute(options) {
89
+ const model = this.editor.model;
90
+ const selection = model.document.selection;
91
+ const mentionData = typeof options.mention == "string" ? { id: options.mention } : options.mention;
92
+ const mentionID = mentionData.id;
93
+ const range = options.range || selection.getFirstRange();
94
+ if (!model.canEditAt(range)) return;
95
+ const mentionText = options.text || mentionID;
96
+ const mention = _addMentionAttributes({
97
+ _text: mentionText,
98
+ id: mentionID
99
+ }, mentionData);
100
+ if (!mentionID.startsWith(options.marker))
101
+ /**
102
+ * The feed item ID must start with the marker character(s).
103
+ *
104
+ * Correct mention feed setting:
105
+ *
106
+ * ```ts
107
+ * mentions: [
108
+ * {
109
+ * marker: '@',
110
+ * feed: [ '@Ann', '@Barney', ... ]
111
+ * }
112
+ * ]
113
+ * ```
114
+ *
115
+ * Incorrect mention feed setting:
116
+ *
117
+ * ```ts
118
+ * mentions: [
119
+ * {
120
+ * marker: '@',
121
+ * feed: [ 'Ann', 'Barney', ... ]
122
+ * }
123
+ * ]
124
+ * ```
125
+ *
126
+ * See {@link module:mention/mentionconfig~MentionConfig}.
127
+ *
128
+ * @error mentioncommand-incorrect-id
129
+ */
130
+ throw new CKEditorError("mentioncommand-incorrect-id", this);
131
+ model.change((writer) => {
132
+ const currentAttributes = toMap(selection.getAttributes());
133
+ const attributesWithMention = new Map(currentAttributes.entries());
134
+ attributesWithMention.set("mention", mention);
135
+ const insertionRange = model.insertContent(writer.createText(mentionText, attributesWithMention), range);
136
+ const nodeBefore = insertionRange.start.nodeBefore;
137
+ const nodeAfter = insertionRange.end.nodeAfter;
138
+ const isFollowedByWhiteSpace = nodeAfter && nodeAfter.is("$text") && nodeAfter.data.startsWith(" ");
139
+ let isInsertedInBrackets = false;
140
+ if (nodeBefore && nodeAfter && nodeBefore.is("$text") && nodeAfter.is("$text")) {
141
+ const precedingCharacter = nodeBefore.data.slice(-1);
142
+ const isPrecededByOpeningBracket = precedingCharacter in BRACKET_PAIRS;
143
+ const isFollowedByBracketClosure = isPrecededByOpeningBracket && nodeAfter.data.startsWith(BRACKET_PAIRS[precedingCharacter]);
144
+ isInsertedInBrackets = isPrecededByOpeningBracket && isFollowedByBracketClosure;
145
+ }
146
+ if (!isInsertedInBrackets && !isFollowedByWhiteSpace) model.insertContent(writer.createText(" ", currentAttributes), range.start.getShiftedBy(mentionText.length));
147
+ });
148
+ }
149
+ };
153
150
 
154
151
  /**
155
- * The mention editing feature.
156
- *
157
- * It introduces the {@link module:mention/mentioncommand~MentionCommand command} and the `mention`
158
- * attribute in the {@link module:engine/model/model~Model model} which renders in the {@link module:engine/view/view view}
159
- * as a `<span class="mention" data-mention="@mention">`.
160
- */ class MentionEditing extends Plugin {
161
- /**
162
- * @inheritDoc
163
- */ static get pluginName() {
164
- return 'MentionEditing';
165
- }
166
- /**
167
- * @inheritDoc
168
- */ static get isOfficialPlugin() {
169
- return true;
170
- }
171
- /**
172
- * @inheritDoc
173
- */ init() {
174
- const editor = this.editor;
175
- const model = editor.model;
176
- const doc = model.document;
177
- // Allow the mention attribute on all text nodes.
178
- model.schema.extend('$text', {
179
- allowAttributes: 'mention'
180
- });
181
- // Disallow the mention attribute on text nodes inside code blocks.
182
- // This prevents mention-based features (slash commands, emoji autocomplete)
183
- // from triggering inside code blocks.
184
- model.schema.addAttributeCheck((context)=>{
185
- if (context.endsWith('codeBlock $text')) {
186
- return false;
187
- }
188
- }, 'mention');
189
- // Upcast conversion.
190
- editor.conversion.for('upcast').elementToAttribute({
191
- view: {
192
- name: 'span',
193
- attributes: 'data-mention',
194
- classes: 'mention'
195
- },
196
- model: {
197
- key: 'mention',
198
- value: (viewElement)=>_toMentionAttribute(viewElement)
199
- }
200
- });
201
- // Downcast conversion.
202
- editor.conversion.for('downcast').attributeToElement({
203
- model: 'mention',
204
- view: createViewMentionElement
205
- });
206
- editor.conversion.for('downcast').add(preventPartialMentionDowncast);
207
- doc.registerPostFixer((writer)=>removePartialMentionPostFixer(writer, doc, model.schema));
208
- doc.registerPostFixer((writer)=>extendAttributeOnMentionPostFixer(writer, doc));
209
- doc.registerPostFixer((writer)=>selectionMentionAttributePostFixer(writer, doc));
210
- editor.commands.add('mention', new MentionCommand(editor));
211
- }
212
- }
152
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
153
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
154
+ */
155
+ /**
156
+ * @module mention/mentionediting
157
+ */
158
+ /**
159
+ * The mention editing feature.
160
+ *
161
+ * It introduces the {@link module:mention/mentioncommand~MentionCommand command} and the `mention`
162
+ * attribute in the {@link module:engine/model/model~Model model} which renders in the {@link module:engine/view/view view}
163
+ * as a `<span class="mention" data-mention="@mention">`.
164
+ */
165
+ var MentionEditing = class extends Plugin {
166
+ /**
167
+ * @inheritDoc
168
+ */
169
+ static get pluginName() {
170
+ return "MentionEditing";
171
+ }
172
+ /**
173
+ * @inheritDoc
174
+ */
175
+ static get isOfficialPlugin() {
176
+ return true;
177
+ }
178
+ /**
179
+ * @inheritDoc
180
+ */
181
+ init() {
182
+ const editor = this.editor;
183
+ const model = editor.model;
184
+ const doc = model.document;
185
+ model.schema.extend("$text", { allowAttributes: "mention" });
186
+ model.schema.addAttributeCheck((context) => {
187
+ if (context.endsWith("codeBlock $text")) return false;
188
+ }, "mention");
189
+ editor.conversion.for("upcast").elementToAttribute({
190
+ view: {
191
+ name: "span",
192
+ attributes: "data-mention",
193
+ classes: "mention"
194
+ },
195
+ model: {
196
+ key: "mention",
197
+ value: (viewElement) => _toMentionAttribute(viewElement)
198
+ }
199
+ });
200
+ editor.conversion.for("downcast").attributeToElement({
201
+ model: "mention",
202
+ view: createViewMentionElement
203
+ });
204
+ editor.conversion.for("downcast").add(preventPartialMentionDowncast);
205
+ doc.registerPostFixer((writer) => removePartialMentionPostFixer(writer, doc, model.schema));
206
+ doc.registerPostFixer((writer) => extendAttributeOnMentionPostFixer(writer, doc));
207
+ doc.registerPostFixer((writer) => selectionMentionAttributePostFixer(writer, doc));
208
+ editor.commands.add("mention", new MentionCommand(editor));
209
+ }
210
+ };
213
211
  /**
214
- * @internal
215
- */ function _addMentionAttributes(baseMentionData, data) {
216
- return Object.assign({
217
- uid: uid().slice(0, 8)
218
- }, baseMentionData, data || {});
212
+ * @internal
213
+ */
214
+ function _addMentionAttributes(baseMentionData, data) {
215
+ return Object.assign({ uid: uid().slice(0, 8) }, baseMentionData, data || {});
219
216
  }
220
217
  /**
221
- * Creates a mention attribute value from the provided view element and optional data.
222
- *
223
- * This function is exposed as
224
- * {@link module:mention/mention~Mention#toMentionAttribute `editor.plugins.get( 'Mention' ).toMentionAttribute()`}.
225
- *
226
- * @internal
227
- */ function _toMentionAttribute(viewElementOrMention, data) {
228
- const dataMention = viewElementOrMention.getAttribute('data-mention');
229
- const textNode = viewElementOrMention.getChild(0);
230
- // Do not convert empty mentions.
231
- if (!textNode) {
232
- return;
233
- }
234
- const dataUid = viewElementOrMention.getAttribute('data-mention-uid');
235
- const baseMentionData = {
236
- id: dataMention,
237
- _text: textNode.data
238
- };
239
- return _addMentionAttributes(baseMentionData, dataUid ? {
240
- uid: dataUid,
241
- ...data
242
- } : data);
218
+ * Creates a mention attribute value from the provided view element and optional data.
219
+ *
220
+ * This function is exposed as
221
+ * {@link module:mention/mention~Mention#toMentionAttribute `editor.plugins.get( 'Mention' ).toMentionAttribute()`}.
222
+ *
223
+ * @internal
224
+ */
225
+ function _toMentionAttribute(viewElementOrMention, data) {
226
+ const dataMention = viewElementOrMention.getAttribute("data-mention");
227
+ const textNode = viewElementOrMention.getChild(0);
228
+ if (!textNode) return;
229
+ const dataUid = viewElementOrMention.getAttribute("data-mention-uid");
230
+ return _addMentionAttributes({
231
+ id: dataMention,
232
+ _text: textNode.data
233
+ }, dataUid ? {
234
+ uid: dataUid,
235
+ ...data
236
+ } : data);
243
237
  }
244
238
  /**
245
- * A converter that blocks partial mention from being converted.
246
- *
247
- * This converter is registered with 'highest' priority in order to consume mention attribute before it is converted by
248
- * any other converters. This converter only consumes partial mention - those whose `_text` attribute is not equal to text with mention
249
- * attribute. This may happen when copying part of mention text.
250
- */ function preventPartialMentionDowncast(dispatcher) {
251
- dispatcher.on('attribute:mention', (evt, data, conversionApi)=>{
252
- const mention = data.attributeNewValue;
253
- if (!data.item.is('$textProxy') || !mention) {
254
- return;
255
- }
256
- const start = data.range.start;
257
- const textNode = start.textNode || start.nodeAfter;
258
- if (textNode.data != mention._text) {
259
- // Consume item to prevent partial mention conversion.
260
- conversionApi.consumable.consume(data.item, evt.name);
261
- }
262
- }, {
263
- priority: 'highest'
264
- });
239
+ * A converter that blocks partial mention from being converted.
240
+ *
241
+ * This converter is registered with 'highest' priority in order to consume mention attribute before it is converted by
242
+ * any other converters. This converter only consumes partial mention - those whose `_text` attribute is not equal to text with mention
243
+ * attribute. This may happen when copying part of mention text.
244
+ */
245
+ function preventPartialMentionDowncast(dispatcher) {
246
+ dispatcher.on("attribute:mention", (evt, data, conversionApi) => {
247
+ const mention = data.attributeNewValue;
248
+ if (!data.item.is("$textProxy") || !mention) return;
249
+ const start = data.range.start;
250
+ if ((start.textNode || start.nodeAfter).data != mention._text) conversionApi.consumable.consume(data.item, evt.name);
251
+ }, { priority: "highest" });
265
252
  }
266
253
  /**
267
- * Creates a mention element from the mention data.
268
- */ function createViewMentionElement(mention, { writer, options }) {
269
- if (!mention) {
270
- return;
271
- }
272
- return writer.createAttributeElement('span', {
273
- class: 'mention',
274
- 'data-mention': mention.id,
275
- // Omit `data-mention-uid` in clipboard (copy/cut) to prevent UIDs duplication.
276
- ...!options.isClipboardPipeline && {
277
- 'data-mention-uid': mention.uid
278
- }
279
- }, {
280
- id: mention.uid,
281
- priority: 20
282
- });
254
+ * Creates a mention element from the mention data.
255
+ */
256
+ function createViewMentionElement(mention, { writer, options }) {
257
+ if (!mention) return;
258
+ return writer.createAttributeElement("span", {
259
+ class: "mention",
260
+ "data-mention": mention.id,
261
+ ...!options.isClipboardPipeline && { "data-mention-uid": mention.uid }
262
+ }, {
263
+ id: mention.uid,
264
+ priority: 20
265
+ });
283
266
  }
284
267
  /**
285
- * Model post-fixer that disallows typing with selection when the selection is placed after the text node with the mention attribute or
286
- * before a text node with mention attribute.
287
- */ function selectionMentionAttributePostFixer(writer, doc) {
288
- const selection = doc.selection;
289
- const focus = selection.focus;
290
- if (selection.isCollapsed && selection.hasAttribute('mention') && shouldNotTypeWithMentionAt(focus)) {
291
- writer.removeSelectionAttribute('mention');
292
- return true;
293
- }
294
- return false;
268
+ * Model post-fixer that disallows typing with selection when the selection is placed after the text node with the mention attribute or
269
+ * before a text node with mention attribute.
270
+ */
271
+ function selectionMentionAttributePostFixer(writer, doc) {
272
+ const selection = doc.selection;
273
+ const focus = selection.focus;
274
+ if (selection.isCollapsed && selection.hasAttribute("mention") && shouldNotTypeWithMentionAt(focus)) {
275
+ writer.removeSelectionAttribute("mention");
276
+ return true;
277
+ }
278
+ return false;
295
279
  }
296
280
  /**
297
- * Helper function to detect if mention attribute should be removed from selection.
298
- * This check makes only sense if the selection has mention attribute.
299
- *
300
- * The mention attribute should be removed from a selection when selection focus is placed:
301
- * a) after a text node
302
- * b) the position is at parents start - the selection will set attributes from node after.
303
- */ function shouldNotTypeWithMentionAt(position) {
304
- const isAtStart = position.isAtStart;
305
- const isAfterAMention = position.nodeBefore && position.nodeBefore.is('$text');
306
- return isAfterAMention || isAtStart;
281
+ * Helper function to detect if mention attribute should be removed from selection.
282
+ * This check makes only sense if the selection has mention attribute.
283
+ *
284
+ * The mention attribute should be removed from a selection when selection focus is placed:
285
+ * a) after a text node
286
+ * b) the position is at parents start - the selection will set attributes from node after.
287
+ */
288
+ function shouldNotTypeWithMentionAt(position) {
289
+ const isAtStart = position.isAtStart;
290
+ return position.nodeBefore && position.nodeBefore.is("$text") || isAtStart;
307
291
  }
308
292
  /**
309
- * Model post-fixer that removes the mention attribute from the modified text node.
310
- */ function removePartialMentionPostFixer(writer, doc, schema) {
311
- const changes = doc.differ.getChanges();
312
- let wasChanged = false;
313
- for (const change of changes){
314
- if (change.type == 'attribute') {
315
- continue;
316
- }
317
- // Checks the text node on the current position.
318
- const position = change.position;
319
- if (change.name == '$text') {
320
- const nodeAfterInsertedTextNode = position.textNode && position.textNode.nextSibling;
321
- // Checks the text node where the change occurred.
322
- wasChanged = checkAndFix(position.textNode, writer) || wasChanged;
323
- // Occurs on paste inside a text node with mention.
324
- wasChanged = checkAndFix(nodeAfterInsertedTextNode, writer) || wasChanged;
325
- wasChanged = checkAndFix(position.nodeBefore, writer) || wasChanged;
326
- wasChanged = checkAndFix(position.nodeAfter, writer) || wasChanged;
327
- }
328
- // Checks text nodes in inserted elements (might occur when splitting a paragraph or pasting content inside text with mention).
329
- if (change.name != '$text' && change.type == 'insert') {
330
- const insertedNode = position.nodeAfter;
331
- for (const item of writer.createRangeIn(insertedNode).getItems()){
332
- wasChanged = checkAndFix(item, writer) || wasChanged;
333
- }
334
- }
335
- // Inserted inline elements might break mention.
336
- if (change.type == 'insert' && schema.isInline(change.name)) {
337
- const nodeAfterInserted = position.nodeAfter && position.nodeAfter.nextSibling;
338
- wasChanged = checkAndFix(position.nodeBefore, writer) || wasChanged;
339
- wasChanged = checkAndFix(nodeAfterInserted, writer) || wasChanged;
340
- }
341
- }
342
- return wasChanged;
293
+ * Model post-fixer that removes the mention attribute from the modified text node.
294
+ */
295
+ function removePartialMentionPostFixer(writer, doc, schema) {
296
+ const changes = doc.differ.getChanges();
297
+ let wasChanged = false;
298
+ for (const change of changes) {
299
+ if (change.type == "attribute") continue;
300
+ const position = change.position;
301
+ if (change.name == "$text") {
302
+ const nodeAfterInsertedTextNode = position.textNode && position.textNode.nextSibling;
303
+ wasChanged = checkAndFix(position.textNode, writer) || wasChanged;
304
+ wasChanged = checkAndFix(nodeAfterInsertedTextNode, writer) || wasChanged;
305
+ wasChanged = checkAndFix(position.nodeBefore, writer) || wasChanged;
306
+ wasChanged = checkAndFix(position.nodeAfter, writer) || wasChanged;
307
+ }
308
+ if (change.name != "$text" && change.type == "insert") {
309
+ const insertedNode = position.nodeAfter;
310
+ for (const item of writer.createRangeIn(insertedNode).getItems()) wasChanged = checkAndFix(item, writer) || wasChanged;
311
+ }
312
+ if (change.type == "insert" && schema.isInline(change.name)) {
313
+ const nodeAfterInserted = position.nodeAfter && position.nodeAfter.nextSibling;
314
+ wasChanged = checkAndFix(position.nodeBefore, writer) || wasChanged;
315
+ wasChanged = checkAndFix(nodeAfterInserted, writer) || wasChanged;
316
+ }
317
+ }
318
+ return wasChanged;
343
319
  }
344
320
  /**
345
- * This post-fixer will extend the attribute applied on the part of the mention so the whole text node of the mention will have
346
- * the added attribute.
347
- */ function extendAttributeOnMentionPostFixer(writer, doc) {
348
- const changes = doc.differ.getChanges();
349
- let wasChanged = false;
350
- for (const change of changes){
351
- if (change.type === 'attribute' && change.attributeKey != 'mention') {
352
- // Checks the node on the left side of the range...
353
- const nodeBefore = change.range.start.nodeBefore;
354
- // ... and on the right side of the range.
355
- const nodeAfter = change.range.end.nodeAfter;
356
- for (const node of [
357
- nodeBefore,
358
- nodeAfter
359
- ]){
360
- if (isBrokenMentionNode(node) && node.getAttribute(change.attributeKey) != change.attributeNewValue) {
361
- writer.setAttribute(change.attributeKey, change.attributeNewValue, node);
362
- wasChanged = true;
363
- }
364
- }
365
- }
366
- }
367
- return wasChanged;
321
+ * This post-fixer will extend the attribute applied on the part of the mention so the whole text node of the mention will have
322
+ * the added attribute.
323
+ */
324
+ function extendAttributeOnMentionPostFixer(writer, doc) {
325
+ const changes = doc.differ.getChanges();
326
+ let wasChanged = false;
327
+ for (const change of changes) if (change.type === "attribute" && change.attributeKey != "mention") {
328
+ const nodeBefore = change.range.start.nodeBefore;
329
+ const nodeAfter = change.range.end.nodeAfter;
330
+ for (const node of [nodeBefore, nodeAfter]) if (isBrokenMentionNode(node) && node.getAttribute(change.attributeKey) != change.attributeNewValue) {
331
+ writer.setAttribute(change.attributeKey, change.attributeNewValue, node);
332
+ wasChanged = true;
333
+ }
334
+ }
335
+ return wasChanged;
368
336
  }
369
337
  /**
370
- * Checks if a node has a correct mention attribute if present.
371
- * Returns `true` if the node is text and has a mention attribute whose text does not match the expected mention text.
372
- */ function isBrokenMentionNode(node) {
373
- if (!node || !(node.is('$text') || node.is('$textProxy')) || !node.hasAttribute('mention')) {
374
- return false;
375
- }
376
- const text = node.data;
377
- const mention = node.getAttribute('mention');
378
- const expectedText = mention._text;
379
- return text != expectedText;
338
+ * Checks if a node has a correct mention attribute if present.
339
+ * Returns `true` if the node is text and has a mention attribute whose text does not match the expected mention text.
340
+ */
341
+ function isBrokenMentionNode(node) {
342
+ if (!node || !(node.is("$text") || node.is("$textProxy")) || !node.hasAttribute("mention")) return false;
343
+ return node.data != node.getAttribute("mention")._text;
380
344
  }
381
345
  /**
382
- * Fixes a mention on a text node if it needs a fix.
383
- */ function checkAndFix(textNode, writer) {
384
- if (isBrokenMentionNode(textNode)) {
385
- writer.removeAttribute('mention', textNode);
386
- return true;
387
- }
388
- return false;
346
+ * Fixes a mention on a text node if it needs a fix.
347
+ */
348
+ function checkAndFix(textNode, writer) {
349
+ if (isBrokenMentionNode(textNode)) {
350
+ writer.removeAttribute("mention", textNode);
351
+ return true;
352
+ }
353
+ return false;
389
354
  }
390
355
 
391
356
  /**
392
- * The mention ui view.
393
- */ class MentionsView extends ListView {
394
- selected;
395
- position;
396
- /**
397
- * @inheritDoc
398
- */ constructor(locale){
399
- super(locale);
400
- this.extendTemplate({
401
- attributes: {
402
- class: [
403
- 'ck-mentions'
404
- ],
405
- tabindex: '-1'
406
- }
407
- });
408
- }
409
- /**
410
- * {@link #select Selects} the first item.
411
- */ selectFirst() {
412
- this.select(0);
413
- }
414
- /**
415
- * Selects next item to the currently {@link #select selected}.
416
- *
417
- * If the last item is already selected, it will select the first item.
418
- */ selectNext() {
419
- const item = this.selected;
420
- const index = this.items.getIndex(item);
421
- this.select(index + 1);
422
- }
423
- /**
424
- * Selects previous item to the currently {@link #select selected}.
425
- *
426
- * If the first item is already selected, it will select the last item.
427
- */ selectPrevious() {
428
- const item = this.selected;
429
- const index = this.items.getIndex(item);
430
- this.select(index - 1);
431
- }
432
- /**
433
- * Marks item at a given index as selected.
434
- *
435
- * Handles selection cycling when passed index is out of bounds:
436
- * - if the index is lower than 0, it will select the last item,
437
- * - if the index is higher than the last item index, it will select the first item.
438
- *
439
- * @param index Index of an item to be marked as selected.
440
- */ select(index) {
441
- let indexToGet = 0;
442
- if (index > 0 && index < this.items.length) {
443
- indexToGet = index;
444
- } else if (index < 0) {
445
- indexToGet = this.items.length - 1;
446
- }
447
- const item = this.items.get(indexToGet);
448
- // Return early if item is already selected.
449
- if (this.selected === item) {
450
- return;
451
- }
452
- // Remove highlight of previously selected item.
453
- if (this.selected) {
454
- this.selected.removeHighlight();
455
- }
456
- item.highlight();
457
- this.selected = item;
458
- // Scroll the mentions view to the selected element.
459
- if (!this._isItemVisibleInScrolledArea(item)) {
460
- this.element.scrollTop = item.element.offsetTop;
461
- }
462
- }
463
- /**
464
- * Triggers the `execute` event on the {@link #select selected} item.
465
- */ executeSelected() {
466
- this.selected.fire('execute');
467
- }
468
- /**
469
- * Checks if an item is visible in the scrollable area.
470
- *
471
- * The item is considered visible when:
472
- * - its top boundary is inside the scrollable rect
473
- * - its bottom boundary is inside the scrollable rect (the whole item must be visible)
474
- */ _isItemVisibleInScrolledArea(item) {
475
- return new Rect(this.element).contains(new Rect(item.element));
476
- }
477
- }
357
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
358
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
359
+ */
360
+ /**
361
+ * @module mention/ui/mentionsview
362
+ */
363
+ /**
364
+ * The mention ui view.
365
+ */
366
+ var MentionsView = class extends ListView {
367
+ selected;
368
+ position;
369
+ /**
370
+ * @inheritDoc
371
+ */
372
+ constructor(locale) {
373
+ super(locale);
374
+ this.extendTemplate({ attributes: {
375
+ class: ["ck-mentions"],
376
+ tabindex: "-1"
377
+ } });
378
+ }
379
+ /**
380
+ * {@link #select Selects} the first item.
381
+ */
382
+ selectFirst() {
383
+ this.select(0);
384
+ }
385
+ /**
386
+ * Selects next item to the currently {@link #select selected}.
387
+ *
388
+ * If the last item is already selected, it will select the first item.
389
+ */
390
+ selectNext() {
391
+ const item = this.selected;
392
+ const index = this.items.getIndex(item);
393
+ this.select(index + 1);
394
+ }
395
+ /**
396
+ * Selects previous item to the currently {@link #select selected}.
397
+ *
398
+ * If the first item is already selected, it will select the last item.
399
+ */
400
+ selectPrevious() {
401
+ const item = this.selected;
402
+ const index = this.items.getIndex(item);
403
+ this.select(index - 1);
404
+ }
405
+ /**
406
+ * Marks item at a given index as selected.
407
+ *
408
+ * Handles selection cycling when passed index is out of bounds:
409
+ * - if the index is lower than 0, it will select the last item,
410
+ * - if the index is higher than the last item index, it will select the first item.
411
+ *
412
+ * @param index Index of an item to be marked as selected.
413
+ */
414
+ select(index) {
415
+ let indexToGet = 0;
416
+ if (index > 0 && index < this.items.length) indexToGet = index;
417
+ else if (index < 0) indexToGet = this.items.length - 1;
418
+ const item = this.items.get(indexToGet);
419
+ if (this.selected === item) return;
420
+ if (this.selected) this.selected.removeHighlight();
421
+ item.highlight();
422
+ this.selected = item;
423
+ if (!this._isItemVisibleInScrolledArea(item)) this.element.scrollTop = item.element.offsetTop;
424
+ }
425
+ /**
426
+ * Triggers the `execute` event on the {@link #select selected} item.
427
+ */
428
+ executeSelected() {
429
+ this.selected.fire("execute");
430
+ }
431
+ /**
432
+ * Checks if an item is visible in the scrollable area.
433
+ *
434
+ * The item is considered visible when:
435
+ * - its top boundary is inside the scrollable rect
436
+ * - its bottom boundary is inside the scrollable rect (the whole item must be visible)
437
+ */
438
+ _isItemVisibleInScrolledArea(item) {
439
+ return new Rect(this.element).contains(new Rect(item.element));
440
+ }
441
+ };
478
442
 
479
443
  /**
480
- * This class wraps DOM element as a CKEditor5 UI View.
481
- *
482
- * It allows to render any DOM element and use it in mentions list.
483
- */ class MentionDomWrapperView extends View {
484
- /**
485
- * The DOM element for which wrapper was created.
486
- */ domElement;
487
- /**
488
- * Creates an instance of {@link module:mention/ui/domwrapperview~MentionDomWrapperView} class.
489
- *
490
- * Also see {@link #render}.
491
- */ constructor(locale, domElement){
492
- super(locale);
493
- // Disable template rendering on this view.
494
- this.template = undefined;
495
- this.domElement = domElement;
496
- // Render dom wrapper as a button.
497
- this.domElement.classList.add('ck-button');
498
- this.set('isOn', false);
499
- // Handle isOn state as in buttons.
500
- this.on('change:isOn', (evt, name, isOn)=>{
501
- if (isOn) {
502
- this.domElement.classList.add('ck-on');
503
- this.domElement.classList.remove('ck-off');
504
- } else {
505
- this.domElement.classList.add('ck-off');
506
- this.domElement.classList.remove('ck-on');
507
- }
508
- });
509
- // Pass click event as execute event.
510
- this.listenTo(this.domElement, 'click', ()=>{
511
- this.fire('execute');
512
- });
513
- }
514
- /**
515
- * @inheritDoc
516
- */ render() {
517
- super.render();
518
- this.element = this.domElement;
519
- }
520
- /**
521
- * Focuses the DOM element.
522
- */ focus() {
523
- this.domElement.focus();
524
- }
525
- }
444
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
445
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
446
+ */
447
+ /**
448
+ * @module mention/ui/domwrapperview
449
+ */
450
+ /**
451
+ * This class wraps DOM element as a CKEditor5 UI View.
452
+ *
453
+ * It allows to render any DOM element and use it in mentions list.
454
+ */
455
+ var MentionDomWrapperView = class extends View {
456
+ /**
457
+ * The DOM element for which wrapper was created.
458
+ */
459
+ domElement;
460
+ /**
461
+ * Creates an instance of {@link module:mention/ui/domwrapperview~MentionDomWrapperView} class.
462
+ *
463
+ * Also see {@link #render}.
464
+ */
465
+ constructor(locale, domElement) {
466
+ super(locale);
467
+ this.template = void 0;
468
+ this.domElement = domElement;
469
+ this.domElement.classList.add("ck-button");
470
+ this.set("isOn", false);
471
+ this.on("change:isOn", (evt, name, isOn) => {
472
+ if (isOn) {
473
+ this.domElement.classList.add("ck-on");
474
+ this.domElement.classList.remove("ck-off");
475
+ } else {
476
+ this.domElement.classList.add("ck-off");
477
+ this.domElement.classList.remove("ck-on");
478
+ }
479
+ });
480
+ this.listenTo(this.domElement, "click", () => {
481
+ this.fire("execute");
482
+ });
483
+ }
484
+ /**
485
+ * @inheritDoc
486
+ */
487
+ render() {
488
+ super.render();
489
+ this.element = this.domElement;
490
+ }
491
+ /**
492
+ * Focuses the DOM element.
493
+ */
494
+ focus() {
495
+ this.domElement.focus();
496
+ }
497
+ };
526
498
 
527
- class MentionListItemView extends ListItemView {
528
- item;
529
- marker;
530
- highlight() {
531
- const child = this.children.first;
532
- child.isOn = true;
533
- }
534
- removeHighlight() {
535
- const child = this.children.first;
536
- child.isOn = false;
537
- }
538
- }
499
+ /**
500
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
501
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
502
+ */
503
+ /**
504
+ * @module mention/ui/mentionlistitemview
505
+ */
506
+ var MentionListItemView = class extends ListItemView {
507
+ item;
508
+ marker;
509
+ highlight() {
510
+ const child = this.children.first;
511
+ child.isOn = true;
512
+ }
513
+ removeHighlight() {
514
+ const child = this.children.first;
515
+ child.isOn = false;
516
+ }
517
+ };
539
518
 
519
+ /**
520
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
521
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
522
+ */
523
+ /**
524
+ * @module mention/mentionui
525
+ */
540
526
  const VERTICAL_SPACING = 3;
541
- // The key codes that mention UI handles when it is open (without commit keys).
542
527
  const defaultHandledKeyCodes = [
543
- keyCodes.arrowup,
544
- keyCodes.arrowdown,
545
- keyCodes.esc
546
- ];
547
- // Dropdown commit key codes.
548
- const defaultCommitKeyCodes = [
549
- keyCodes.enter,
550
- keyCodes.tab
528
+ keyCodes.arrowup,
529
+ keyCodes.arrowdown,
530
+ keyCodes.esc
551
531
  ];
532
+ const defaultCommitKeyCodes = [keyCodes.enter, keyCodes.tab];
552
533
  /**
553
- * The mention UI feature.
554
- */ class MentionUI extends Plugin {
555
- /**
556
- * The mention view.
557
- */ _mentionsView;
558
- /**
559
- * Stores mention feeds configurations.
560
- */ _mentionsConfigurations;
561
- /**
562
- * The contextual balloon plugin instance.
563
- */ _balloon;
564
- _items = new Collection();
565
- _lastRequested;
566
- /**
567
- * Debounced feed requester. It uses `es-toolkit#debounce` method to delay function call.
568
- */ _requestFeedDebounced;
569
- /**
570
- * @inheritDoc
571
- */ static get pluginName() {
572
- return 'MentionUI';
573
- }
574
- /**
575
- * @inheritDoc
576
- */ static get isOfficialPlugin() {
577
- return true;
578
- }
579
- /**
580
- * @inheritDoc
581
- */ static get requires() {
582
- return [
583
- ContextualBalloon
584
- ];
585
- }
586
- /**
587
- * @inheritDoc
588
- */ constructor(editor){
589
- super(editor);
590
- this._mentionsView = this._createMentionView();
591
- this._mentionsConfigurations = new Map();
592
- this._requestFeedDebounced = debounce(this._requestFeed, 100);
593
- editor.config.define('mention', {
594
- feeds: []
595
- });
596
- }
597
- /**
598
- * @inheritDoc
599
- */ init() {
600
- const editor = this.editor;
601
- const commitKeys = editor.config.get('mention.commitKeys') || defaultCommitKeyCodes;
602
- const handledKeyCodes = defaultHandledKeyCodes.concat(commitKeys);
603
- this._balloon = editor.plugins.get(ContextualBalloon);
604
- // Key listener that handles navigation in mention view.
605
- editor.editing.view.document.on('keydown', (evt, data)=>{
606
- if (isHandledKey(data.keyCode) && this._isUIVisible) {
607
- data.preventDefault();
608
- evt.stop(); // Required for Enter key overriding.
609
- if (data.keyCode == keyCodes.arrowdown) {
610
- this._mentionsView.selectNext();
611
- }
612
- if (data.keyCode == keyCodes.arrowup) {
613
- this._mentionsView.selectPrevious();
614
- }
615
- if (commitKeys.includes(data.keyCode)) {
616
- this._mentionsView.executeSelected();
617
- }
618
- if (data.keyCode == keyCodes.esc) {
619
- this._hideUIAndRemoveMarker();
620
- }
621
- }
622
- }, {
623
- priority: 'highest'
624
- }); // Required to override the Enter key.
625
- // Close the dropdown upon clicking outside of the plugin UI.
626
- clickOutsideHandler({
627
- emitter: this._mentionsView,
628
- activator: ()=>this._isUIVisible,
629
- contextElements: ()=>[
630
- this._balloon.view.element
631
- ],
632
- callback: ()=>this._hideUIAndRemoveMarker()
633
- });
634
- const feeds = editor.config.get('mention.feeds');
635
- for (const mentionDescription of feeds){
636
- const { feed, marker, dropdownLimit } = mentionDescription;
637
- if (!isValidMentionMarker(marker)) {
638
- /**
639
- * The marker must be a single character.
640
- *
641
- * Correct markers: `'@'`, `'#'`.
642
- *
643
- * Incorrect markers: `'$$'`, `'[@'`.
644
- *
645
- * See {@link module:mention/mentionconfig~MentionConfig}.
646
- *
647
- * @error mentionconfig-incorrect-marker
648
- * @param {string} marker Configured marker
649
- */ throw new CKEditorError('mentionconfig-incorrect-marker', null, {
650
- marker
651
- });
652
- }
653
- const feedCallback = typeof feed == 'function' ? feed.bind(this.editor) : createFeedCallback(feed);
654
- const itemRenderer = mentionDescription.itemRenderer;
655
- const definition = {
656
- marker,
657
- feedCallback,
658
- itemRenderer,
659
- dropdownLimit
660
- };
661
- this._mentionsConfigurations.set(marker, definition);
662
- }
663
- this._setupTextWatcher(feeds);
664
- this.listenTo(editor, 'change:isReadOnly', ()=>{
665
- this._hideUIAndRemoveMarker();
666
- });
667
- this.on('requestFeed:response', (evt, data)=>this._handleFeedResponse(data));
668
- this.on('requestFeed:error', ()=>this._hideUIAndRemoveMarker());
669
- /**
670
- * Checks if a given key code is handled by the mention UI.
671
- */ function isHandledKey(keyCode) {
672
- return handledKeyCodes.includes(keyCode);
673
- }
674
- }
675
- /**
676
- * @inheritDoc
677
- */ destroy() {
678
- super.destroy();
679
- // Destroy created UI components as they are not automatically destroyed (see https://github.com/ckeditor/ckeditor5/issues/1341).
680
- this._mentionsView.destroy();
681
- }
682
- /**
683
- * Returns true when {@link #_mentionsView} is in the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon} and it is
684
- * currently visible.
685
- */ get _isUIVisible() {
686
- return this._balloon.visibleView === this._mentionsView;
687
- }
688
- /**
689
- * Creates the {@link #_mentionsView}.
690
- */ _createMentionView() {
691
- const locale = this.editor.locale;
692
- const mentionsView = new MentionsView(locale);
693
- mentionsView.items.bindTo(this._items).using((data)=>{
694
- const { item, marker } = data;
695
- const { dropdownLimit: markerDropdownLimit } = this._mentionsConfigurations.get(marker);
696
- // Set to 10 by default for backwards compatibility. See: https://github.com/ckeditor/ckeditor5/issues/10479
697
- const dropdownLimit = markerDropdownLimit || this.editor.config.get('mention.dropdownLimit') || 10;
698
- if (mentionsView.items.length >= dropdownLimit) {
699
- return null;
700
- }
701
- const listItemView = new MentionListItemView(locale);
702
- const view = this._renderItem(item, marker);
703
- view.delegate('execute').to(listItemView);
704
- listItemView.children.add(view);
705
- listItemView.item = item;
706
- listItemView.marker = marker;
707
- listItemView.on('execute', ()=>{
708
- mentionsView.fire('execute', {
709
- item,
710
- marker
711
- });
712
- });
713
- return listItemView;
714
- });
715
- mentionsView.on('execute', (evt, data)=>{
716
- const editor = this.editor;
717
- const model = editor.model;
718
- const item = data.item;
719
- const marker = data.marker;
720
- const mentionMarker = editor.model.markers.get('mention');
721
- // Create a range on matched text.
722
- const end = model.createPositionAt(model.document.selection.focus);
723
- const start = model.createPositionAt(mentionMarker.getStart());
724
- const range = model.createRange(start, end);
725
- this._hideUIAndRemoveMarker();
726
- editor.execute('mention', {
727
- mention: item,
728
- text: item.text,
729
- marker,
730
- range
731
- });
732
- editor.editing.view.focus();
733
- });
734
- return mentionsView;
735
- }
736
- /**
737
- * Returns item renderer for the marker.
738
- */ _getItemRenderer(marker) {
739
- const { itemRenderer } = this._mentionsConfigurations.get(marker);
740
- return itemRenderer;
741
- }
742
- /**
743
- * Requests a feed from a configured callbacks.
744
- */ _requestFeed(marker, feedText) {
745
- // @if CK_DEBUG_MENTION // console.log( '%c[Feed]%c Requesting for', 'color: blue', 'color: black', `"${ feedText }"` );
746
- // Store the last requested feed - it is used to discard any out-of order requests.
747
- this._lastRequested = feedText;
748
- const { feedCallback } = this._mentionsConfigurations.get(marker);
749
- const feedResponse = feedCallback(feedText);
750
- const isAsynchronous = feedResponse instanceof Promise;
751
- // For synchronous feeds (e.g. callbacks, arrays) fire the response event immediately.
752
- if (!isAsynchronous) {
753
- this.fire('requestFeed:response', {
754
- feed: feedResponse,
755
- marker,
756
- feedText
757
- });
758
- return;
759
- }
760
- // Handle the asynchronous responses.
761
- feedResponse.then((response)=>{
762
- // Check the feed text of this response with the last requested one so either:
763
- if (this._lastRequested == feedText) {
764
- // It is the same and fire the response event.
765
- this.fire('requestFeed:response', {
766
- feed: response,
767
- marker,
768
- feedText
769
- });
770
- } else {
771
- // It is different - most probably out-of-order one, so fire the discarded event.
772
- this.fire('requestFeed:discarded', {
773
- feed: response,
774
- marker,
775
- feedText
776
- });
777
- }
778
- }).catch((error)=>{
779
- this.fire('requestFeed:error', {
780
- error
781
- });
782
- /**
783
- * The callback used for obtaining mention autocomplete feed thrown and error and the mention UI was hidden or
784
- * not displayed at all.
785
- *
786
- * @error mention-feed-callback-error
787
- */ logWarning('mention-feed-callback-error', {
788
- marker
789
- });
790
- });
791
- }
792
- /**
793
- * Registers a text watcher for the marker.
794
- */ _setupTextWatcher(feeds) {
795
- const editor = this.editor;
796
- const feedsWithPattern = feeds.map((feed)=>({
797
- ...feed,
798
- pattern: createRegExp(feed.marker, feed.minimumCharacters || 0)
799
- }));
800
- const watcher = new TextWatcher(editor.model, createTestCallback(feedsWithPattern));
801
- watcher.on('matched', (evt, data)=>{
802
- const markerDefinition = getLastValidMarkerInText(feedsWithPattern, data.text);
803
- const selection = editor.model.document.selection;
804
- const focus = selection.focus;
805
- const markerPosition = editor.model.createPositionAt(focus.parent, markerDefinition.position);
806
- if (isPositionInExistingMention(focus) || isMarkerInExistingMention(markerPosition)) {
807
- this._hideUIAndRemoveMarker();
808
- return;
809
- }
810
- const feedText = requestFeedText(markerDefinition, data.text);
811
- const matchedTextLength = markerDefinition.marker.length + feedText.length;
812
- // Create a marker range.
813
- const start = focus.getShiftedBy(-matchedTextLength);
814
- const end = focus.getShiftedBy(-feedText.length);
815
- const markerRange = editor.model.createRange(start, end);
816
- // @if CK_DEBUG_MENTION // console.group( '%c[TextWatcher]%c matched', 'color: red', 'color: black', `"${ feedText }"` );
817
- // @if CK_DEBUG_MENTION // console.log( 'data#text', `"${ data.text }"` );
818
- // @if CK_DEBUG_MENTION // console.log( 'data#range', data.range.start.path, data.range.end.path );
819
- // @if CK_DEBUG_MENTION // console.log( 'marker definition', markerDefinition );
820
- // @if CK_DEBUG_MENTION // console.log( 'marker range', markerRange.start.path, markerRange.end.path );
821
- if (checkIfStillInCompletionMode(editor)) {
822
- const mentionMarker = editor.model.markers.get('mention');
823
- // Update the marker - user might've moved the selection to other mention trigger.
824
- editor.model.change((writer)=>{
825
- // @if CK_DEBUG_MENTION // console.log( '%c[Editing]%c Updating the marker.', 'color: purple', 'color: black' );
826
- writer.updateMarker(mentionMarker, {
827
- range: markerRange
828
- });
829
- });
830
- } else {
831
- editor.model.change((writer)=>{
832
- // @if CK_DEBUG_MENTION // console.log( '%c[Editing]%c Adding the marker.', 'color: purple', 'color: black' );
833
- writer.addMarker('mention', {
834
- range: markerRange,
835
- usingOperation: false,
836
- affectsData: false
837
- });
838
- });
839
- }
840
- this._requestFeedDebounced(markerDefinition.marker, feedText);
841
- // @if CK_DEBUG_MENTION // console.groupEnd();
842
- });
843
- watcher.on('unmatched', ()=>{
844
- this._hideUIAndRemoveMarker();
845
- });
846
- const mentionCommand = editor.commands.get('mention');
847
- watcher.bind('isEnabled').to(mentionCommand);
848
- return watcher;
849
- }
850
- /**
851
- * Handles the feed response event data.
852
- */ _handleFeedResponse(data) {
853
- const { feed, marker } = data;
854
- // eslint-disable-next-line @stylistic/max-len
855
- // @if CK_DEBUG_MENTION // console.log( `%c[Feed]%c Response for "${ data.feedText }" (${ feed.length })`, 'color: blue', 'color: black', feed );
856
- // If the marker is not in the document happens when the selection had changed and the 'mention' marker was removed.
857
- if (!checkIfStillInCompletionMode(this.editor)) {
858
- return;
859
- }
860
- // Do not show the mention UI if the mention command is disabled
861
- // (e.g. the cursor was moved into a code block after the feed was requested).
862
- const mentionCommand = this.editor.commands.get('mention');
863
- if (!mentionCommand.isEnabled) {
864
- this._hideUIAndRemoveMarker();
865
- return;
866
- }
867
- // Reset the view.
868
- this._items.clear();
869
- for (const feedItem of feed){
870
- const item = typeof feedItem != 'object' ? {
871
- id: feedItem,
872
- text: feedItem
873
- } : feedItem;
874
- this._items.add({
875
- item,
876
- marker
877
- });
878
- }
879
- const mentionMarker = this.editor.model.markers.get('mention');
880
- if (this._items.length) {
881
- this._showOrUpdateUI(mentionMarker);
882
- } else {
883
- // Do not show empty mention UI.
884
- this._hideUIAndRemoveMarker();
885
- }
886
- }
887
- /**
888
- * Shows the mentions balloon. If the panel is already visible, it will reposition it.
889
- */ _showOrUpdateUI(markerMarker) {
890
- if (this._isUIVisible) {
891
- // @if CK_DEBUG_MENTION // console.log( '%c[UI]%c Updating position.', 'color: green', 'color: black' );
892
- // Update balloon position as the mention list view may change its size.
893
- this._balloon.updatePosition(this._getBalloonPanelPositionData(markerMarker, this._mentionsView.position));
894
- } else {
895
- // @if CK_DEBUG_MENTION // console.log( '%c[UI]%c Showing the UI.', 'color: green', 'color: black' );
896
- this._balloon.add({
897
- view: this._mentionsView,
898
- position: this._getBalloonPanelPositionData(markerMarker, this._mentionsView.position),
899
- singleViewMode: true,
900
- balloonClassName: 'ck-mention-balloon'
901
- });
902
- }
903
- this._mentionsView.position = this._balloon.view.position;
904
- this._mentionsView.selectFirst();
905
- }
906
- /**
907
- * Hides the mentions balloon and removes the 'mention' marker from the markers collection.
908
- */ _hideUIAndRemoveMarker() {
909
- // Remove the mention view from balloon before removing marker - it is used by balloon position target().
910
- if (this._balloon.hasView(this._mentionsView)) {
911
- // @if CK_DEBUG_MENTION // console.log( '%c[UI]%c Hiding the UI.', 'color: green', 'color: black' );
912
- this._balloon.remove(this._mentionsView);
913
- }
914
- if (checkIfStillInCompletionMode(this.editor)) {
915
- // @if CK_DEBUG_MENTION // console.log( '%c[Editing]%c Removing marker.', 'color: purple', 'color: black' );
916
- this.editor.model.change((writer)=>writer.removeMarker('mention'));
917
- }
918
- // Make the last matched position on panel view undefined so the #_getBalloonPanelPositionData() method will return all positions
919
- // on the next call.
920
- this._mentionsView.position = undefined;
921
- }
922
- /**
923
- * Renders a single item in the autocomplete list.
924
- */ _renderItem(item, marker) {
925
- const editor = this.editor;
926
- let view;
927
- let label = item.id;
928
- const renderer = this._getItemRenderer(marker);
929
- if (renderer) {
930
- const renderResult = renderer(item);
931
- if (typeof renderResult != 'string') {
932
- view = new MentionDomWrapperView(editor.locale, renderResult);
933
- } else {
934
- label = renderResult;
935
- }
936
- }
937
- if (!view) {
938
- const buttonView = new ButtonView(editor.locale);
939
- buttonView.label = label;
940
- buttonView.withText = true;
941
- view = buttonView;
942
- }
943
- return view;
944
- }
945
- /**
946
- * Creates a position options object used to position the balloon panel.
947
- *
948
- * @param mentionMarker
949
- * @param preferredPosition The name of the last matched position name.
950
- */ _getBalloonPanelPositionData(mentionMarker, preferredPosition) {
951
- const editor = this.editor;
952
- const editing = editor.editing;
953
- const domConverter = editing.view.domConverter;
954
- const mapper = editing.mapper;
955
- const uiLanguageDirection = editor.locale.uiLanguageDirection;
956
- return {
957
- target: ()=>{
958
- let modelRange = mentionMarker.getRange();
959
- // Target the UI to the model selection range - the marker has been removed so probably the UI will not be shown anyway.
960
- // The logic is used by ContextualBalloon to display another panel in the same place.
961
- if (modelRange.start.root.rootName == '$graveyard') {
962
- modelRange = editor.model.document.selection.getFirstRange();
963
- }
964
- const viewRange = mapper.toViewRange(modelRange);
965
- const rangeRects = Rect.getDomRangeRects(domConverter.viewRangeToDom(viewRange));
966
- return rangeRects.pop();
967
- },
968
- limiter: ()=>{
969
- const view = this.editor.editing.view;
970
- const viewDocument = view.document;
971
- const editableElement = viewDocument.selection.editableElement;
972
- if (editableElement) {
973
- return view.domConverter.mapViewToDom(editableElement.root);
974
- }
975
- return null;
976
- },
977
- positions: getBalloonPanelPositions(preferredPosition, uiLanguageDirection)
978
- };
979
- }
980
- }
534
+ * The mention UI feature.
535
+ */
536
+ var MentionUI = class extends Plugin {
537
+ /**
538
+ * The mention view.
539
+ */
540
+ _mentionsView;
541
+ /**
542
+ * Stores mention feeds configurations.
543
+ */
544
+ _mentionsConfigurations;
545
+ /**
546
+ * The contextual balloon plugin instance.
547
+ */
548
+ _balloon;
549
+ _items = new Collection();
550
+ _lastRequested;
551
+ /**
552
+ * Debounced feed requester. It uses `es-toolkit#debounce` method to delay function call.
553
+ */
554
+ _requestFeedDebounced;
555
+ /**
556
+ * @inheritDoc
557
+ */
558
+ static get pluginName() {
559
+ return "MentionUI";
560
+ }
561
+ /**
562
+ * @inheritDoc
563
+ */
564
+ static get isOfficialPlugin() {
565
+ return true;
566
+ }
567
+ /**
568
+ * @inheritDoc
569
+ */
570
+ static get requires() {
571
+ return [ContextualBalloon];
572
+ }
573
+ /**
574
+ * @inheritDoc
575
+ */
576
+ constructor(editor) {
577
+ super(editor);
578
+ this._mentionsView = this._createMentionView();
579
+ this._mentionsConfigurations = /* @__PURE__ */ new Map();
580
+ this._requestFeedDebounced = debounce(this._requestFeed, 100);
581
+ editor.config.define("mention", { feeds: [] });
582
+ }
583
+ /**
584
+ * @inheritDoc
585
+ */
586
+ init() {
587
+ const editor = this.editor;
588
+ const commitKeys = editor.config.get("mention.commitKeys") || defaultCommitKeyCodes;
589
+ const handledKeyCodes = defaultHandledKeyCodes.concat(commitKeys);
590
+ this._balloon = editor.plugins.get(ContextualBalloon);
591
+ editor.editing.view.document.on("keydown", (evt, data) => {
592
+ if (isHandledKey(data.keyCode) && this._isUIVisible) {
593
+ data.preventDefault();
594
+ evt.stop();
595
+ if (data.keyCode == keyCodes.arrowdown) this._mentionsView.selectNext();
596
+ if (data.keyCode == keyCodes.arrowup) this._mentionsView.selectPrevious();
597
+ if (commitKeys.includes(data.keyCode)) this._mentionsView.executeSelected();
598
+ if (data.keyCode == keyCodes.esc) this._hideUIAndRemoveMarker();
599
+ }
600
+ }, { priority: "highest" });
601
+ clickOutsideHandler({
602
+ emitter: this._mentionsView,
603
+ activator: () => this._isUIVisible,
604
+ contextElements: () => [this._balloon.view.element],
605
+ callback: () => this._hideUIAndRemoveMarker()
606
+ });
607
+ const feeds = editor.config.get("mention.feeds");
608
+ for (const mentionDescription of feeds) {
609
+ const { feed, marker, dropdownLimit } = mentionDescription;
610
+ if (!isValidMentionMarker(marker))
611
+ /**
612
+ * The marker must be a single character.
613
+ *
614
+ * Correct markers: `'@'`, `'#'`.
615
+ *
616
+ * Incorrect markers: `'$$'`, `'[@'`.
617
+ *
618
+ * See {@link module:mention/mentionconfig~MentionConfig}.
619
+ *
620
+ * @error mentionconfig-incorrect-marker
621
+ * @param {string} marker Configured marker
622
+ */
623
+ throw new CKEditorError("mentionconfig-incorrect-marker", null, { marker });
624
+ const definition = {
625
+ marker,
626
+ feedCallback: typeof feed == "function" ? feed.bind(this.editor) : createFeedCallback(feed),
627
+ itemRenderer: mentionDescription.itemRenderer,
628
+ dropdownLimit
629
+ };
630
+ this._mentionsConfigurations.set(marker, definition);
631
+ }
632
+ this._setupTextWatcher(feeds);
633
+ this.listenTo(editor, "change:isReadOnly", () => {
634
+ this._hideUIAndRemoveMarker();
635
+ });
636
+ this.on("requestFeed:response", (evt, data) => this._handleFeedResponse(data));
637
+ this.on("requestFeed:error", () => this._hideUIAndRemoveMarker());
638
+ /**
639
+ * Checks if a given key code is handled by the mention UI.
640
+ */
641
+ function isHandledKey(keyCode) {
642
+ return handledKeyCodes.includes(keyCode);
643
+ }
644
+ }
645
+ /**
646
+ * @inheritDoc
647
+ */
648
+ destroy() {
649
+ super.destroy();
650
+ this._mentionsView.destroy();
651
+ }
652
+ /**
653
+ * Returns true when {@link #_mentionsView} is in the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon} and it is
654
+ * currently visible.
655
+ */
656
+ get _isUIVisible() {
657
+ return this._balloon.visibleView === this._mentionsView;
658
+ }
659
+ /**
660
+ * Creates the {@link #_mentionsView}.
661
+ */
662
+ _createMentionView() {
663
+ const locale = this.editor.locale;
664
+ const mentionsView = new MentionsView(locale);
665
+ mentionsView.items.bindTo(this._items).using((data) => {
666
+ const { item, marker } = data;
667
+ const { dropdownLimit: markerDropdownLimit } = this._mentionsConfigurations.get(marker);
668
+ const dropdownLimit = markerDropdownLimit || this.editor.config.get("mention.dropdownLimit") || 10;
669
+ if (mentionsView.items.length >= dropdownLimit) return null;
670
+ const listItemView = new MentionListItemView(locale);
671
+ const view = this._renderItem(item, marker);
672
+ view.delegate("execute").to(listItemView);
673
+ listItemView.children.add(view);
674
+ listItemView.item = item;
675
+ listItemView.marker = marker;
676
+ listItemView.on("execute", () => {
677
+ mentionsView.fire("execute", {
678
+ item,
679
+ marker
680
+ });
681
+ });
682
+ return listItemView;
683
+ });
684
+ mentionsView.on("execute", (evt, data) => {
685
+ const editor = this.editor;
686
+ const model = editor.model;
687
+ const item = data.item;
688
+ const marker = data.marker;
689
+ const mentionMarker = editor.model.markers.get("mention");
690
+ const end = model.createPositionAt(model.document.selection.focus);
691
+ const start = model.createPositionAt(mentionMarker.getStart());
692
+ const range = model.createRange(start, end);
693
+ this._hideUIAndRemoveMarker();
694
+ editor.execute("mention", {
695
+ mention: item,
696
+ text: item.text,
697
+ marker,
698
+ range
699
+ });
700
+ editor.editing.view.focus();
701
+ });
702
+ return mentionsView;
703
+ }
704
+ /**
705
+ * Returns item renderer for the marker.
706
+ */
707
+ _getItemRenderer(marker) {
708
+ const { itemRenderer } = this._mentionsConfigurations.get(marker);
709
+ return itemRenderer;
710
+ }
711
+ /**
712
+ * Requests a feed from a configured callbacks.
713
+ */
714
+ _requestFeed(marker, feedText) {
715
+ this._lastRequested = feedText;
716
+ const { feedCallback } = this._mentionsConfigurations.get(marker);
717
+ const feedResponse = feedCallback(feedText);
718
+ if (!(feedResponse instanceof Promise)) {
719
+ this.fire("requestFeed:response", {
720
+ feed: feedResponse,
721
+ marker,
722
+ feedText
723
+ });
724
+ return;
725
+ }
726
+ feedResponse.then((response) => {
727
+ if (this._lastRequested == feedText) this.fire("requestFeed:response", {
728
+ feed: response,
729
+ marker,
730
+ feedText
731
+ });
732
+ else this.fire("requestFeed:discarded", {
733
+ feed: response,
734
+ marker,
735
+ feedText
736
+ });
737
+ }).catch((error) => {
738
+ this.fire("requestFeed:error", { error });
739
+ /**
740
+ * The callback used for obtaining mention autocomplete feed thrown and error and the mention UI was hidden or
741
+ * not displayed at all.
742
+ *
743
+ * @error mention-feed-callback-error
744
+ */
745
+ logWarning("mention-feed-callback-error", { marker });
746
+ });
747
+ }
748
+ /**
749
+ * Registers a text watcher for the marker.
750
+ */
751
+ _setupTextWatcher(feeds) {
752
+ const editor = this.editor;
753
+ const feedsWithPattern = feeds.map((feed) => ({
754
+ ...feed,
755
+ pattern: createRegExp(feed.marker, feed.minimumCharacters || 0)
756
+ }));
757
+ const watcher = new TextWatcher(editor.model, createTestCallback(feedsWithPattern));
758
+ watcher.on("matched", (evt, data) => {
759
+ const markerDefinition = getLastValidMarkerInText(feedsWithPattern, data.text);
760
+ const focus = editor.model.document.selection.focus;
761
+ const markerPosition = editor.model.createPositionAt(focus.parent, markerDefinition.position);
762
+ if (isPositionInExistingMention(focus) || isMarkerInExistingMention(markerPosition)) {
763
+ this._hideUIAndRemoveMarker();
764
+ return;
765
+ }
766
+ const feedText = requestFeedText(markerDefinition, data.text);
767
+ const matchedTextLength = markerDefinition.marker.length + feedText.length;
768
+ const start = focus.getShiftedBy(-matchedTextLength);
769
+ const end = focus.getShiftedBy(-feedText.length);
770
+ const markerRange = editor.model.createRange(start, end);
771
+ if (checkIfStillInCompletionMode(editor)) {
772
+ const mentionMarker = editor.model.markers.get("mention");
773
+ editor.model.change((writer) => {
774
+ writer.updateMarker(mentionMarker, { range: markerRange });
775
+ });
776
+ } else editor.model.change((writer) => {
777
+ writer.addMarker("mention", {
778
+ range: markerRange,
779
+ usingOperation: false,
780
+ affectsData: false
781
+ });
782
+ });
783
+ this._requestFeedDebounced(markerDefinition.marker, feedText);
784
+ });
785
+ watcher.on("unmatched", () => {
786
+ this._hideUIAndRemoveMarker();
787
+ });
788
+ const mentionCommand = editor.commands.get("mention");
789
+ watcher.bind("isEnabled").to(mentionCommand);
790
+ return watcher;
791
+ }
792
+ /**
793
+ * Handles the feed response event data.
794
+ */
795
+ _handleFeedResponse(data) {
796
+ const { feed, marker } = data;
797
+ if (!checkIfStillInCompletionMode(this.editor)) return;
798
+ if (!this.editor.commands.get("mention").isEnabled) {
799
+ this._hideUIAndRemoveMarker();
800
+ return;
801
+ }
802
+ this._items.clear();
803
+ for (const feedItem of feed) {
804
+ const item = typeof feedItem != "object" ? {
805
+ id: feedItem,
806
+ text: feedItem
807
+ } : feedItem;
808
+ this._items.add({
809
+ item,
810
+ marker
811
+ });
812
+ }
813
+ const mentionMarker = this.editor.model.markers.get("mention");
814
+ if (this._items.length) this._showOrUpdateUI(mentionMarker);
815
+ else this._hideUIAndRemoveMarker();
816
+ }
817
+ /**
818
+ * Shows the mentions balloon. If the panel is already visible, it will reposition it.
819
+ */
820
+ _showOrUpdateUI(markerMarker) {
821
+ if (this._isUIVisible) this._balloon.updatePosition(this._getBalloonPanelPositionData(markerMarker, this._mentionsView.position));
822
+ else this._balloon.add({
823
+ view: this._mentionsView,
824
+ position: this._getBalloonPanelPositionData(markerMarker, this._mentionsView.position),
825
+ singleViewMode: true,
826
+ balloonClassName: "ck-mention-balloon"
827
+ });
828
+ this._mentionsView.position = this._balloon.view.position;
829
+ this._mentionsView.selectFirst();
830
+ }
831
+ /**
832
+ * Hides the mentions balloon and removes the 'mention' marker from the markers collection.
833
+ */
834
+ _hideUIAndRemoveMarker() {
835
+ if (this._balloon.hasView(this._mentionsView)) this._balloon.remove(this._mentionsView);
836
+ if (checkIfStillInCompletionMode(this.editor)) this.editor.model.change((writer) => writer.removeMarker("mention"));
837
+ this._mentionsView.position = void 0;
838
+ }
839
+ /**
840
+ * Renders a single item in the autocomplete list.
841
+ */
842
+ _renderItem(item, marker) {
843
+ const editor = this.editor;
844
+ let view;
845
+ let label = item.id;
846
+ const renderer = this._getItemRenderer(marker);
847
+ if (renderer) {
848
+ const renderResult = renderer(item);
849
+ if (typeof renderResult != "string") view = new MentionDomWrapperView(editor.locale, renderResult);
850
+ else label = renderResult;
851
+ }
852
+ if (!view) {
853
+ const buttonView = new ButtonView(editor.locale);
854
+ buttonView.label = label;
855
+ buttonView.withText = true;
856
+ view = buttonView;
857
+ }
858
+ return view;
859
+ }
860
+ /**
861
+ * Creates a position options object used to position the balloon panel.
862
+ *
863
+ * @param mentionMarker
864
+ * @param preferredPosition The name of the last matched position name.
865
+ */
866
+ _getBalloonPanelPositionData(mentionMarker, preferredPosition) {
867
+ const editor = this.editor;
868
+ const editing = editor.editing;
869
+ const domConverter = editing.view.domConverter;
870
+ const mapper = editing.mapper;
871
+ const uiLanguageDirection = editor.locale.uiLanguageDirection;
872
+ return {
873
+ target: () => {
874
+ let modelRange = mentionMarker.getRange();
875
+ if (modelRange.start.root.rootName == "$graveyard") modelRange = editor.model.document.selection.getFirstRange();
876
+ const viewRange = mapper.toViewRange(modelRange);
877
+ return Rect.getDomRangeRects(domConverter.viewRangeToDom(viewRange)).pop();
878
+ },
879
+ limiter: () => {
880
+ const view = this.editor.editing.view;
881
+ const editableElement = view.document.selection.editableElement;
882
+ if (editableElement) return view.domConverter.mapViewToDom(editableElement.root);
883
+ return null;
884
+ },
885
+ positions: getBalloonPanelPositions(preferredPosition, uiLanguageDirection)
886
+ };
887
+ }
888
+ };
981
889
  /**
982
- * Returns the balloon positions data callbacks.
983
- */ function getBalloonPanelPositions(preferredPosition, uiLanguageDirection) {
984
- const positions = {
985
- // Positions the panel to the southeast of the caret rectangle.
986
- 'caret_se': (targetRect)=>{
987
- return {
988
- top: targetRect.bottom + VERTICAL_SPACING,
989
- left: targetRect.right,
990
- name: 'caret_se',
991
- config: {
992
- withArrow: false
993
- }
994
- };
995
- },
996
- // Positions the panel to the northeast of the caret rectangle.
997
- 'caret_ne': (targetRect, balloonRect)=>{
998
- return {
999
- top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
1000
- left: targetRect.right,
1001
- name: 'caret_ne',
1002
- config: {
1003
- withArrow: false
1004
- }
1005
- };
1006
- },
1007
- // Positions the panel to the southwest of the caret rectangle.
1008
- 'caret_sw': (targetRect, balloonRect)=>{
1009
- return {
1010
- top: targetRect.bottom + VERTICAL_SPACING,
1011
- left: targetRect.right - balloonRect.width,
1012
- name: 'caret_sw',
1013
- config: {
1014
- withArrow: false
1015
- }
1016
- };
1017
- },
1018
- // Positions the panel to the northwest of the caret rect.
1019
- 'caret_nw': (targetRect, balloonRect)=>{
1020
- return {
1021
- top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
1022
- left: targetRect.right - balloonRect.width,
1023
- name: 'caret_nw',
1024
- config: {
1025
- withArrow: false
1026
- }
1027
- };
1028
- }
1029
- };
1030
- // Returns only the last position if it was matched to prevent the panel from jumping after the first match.
1031
- if (Object.prototype.hasOwnProperty.call(positions, preferredPosition)) {
1032
- return [
1033
- positions[preferredPosition]
1034
- ];
1035
- }
1036
- // By default, return all position callbacks ordered depending on the UI language direction.
1037
- return uiLanguageDirection !== 'rtl' ? [
1038
- positions.caret_se,
1039
- positions.caret_sw,
1040
- positions.caret_ne,
1041
- positions.caret_nw
1042
- ] : [
1043
- positions.caret_sw,
1044
- positions.caret_se,
1045
- positions.caret_nw,
1046
- positions.caret_ne
1047
- ];
890
+ * Returns the balloon positions data callbacks.
891
+ */
892
+ function getBalloonPanelPositions(preferredPosition, uiLanguageDirection) {
893
+ const positions = {
894
+ "caret_se": (targetRect) => {
895
+ return {
896
+ top: targetRect.bottom + VERTICAL_SPACING,
897
+ left: targetRect.right,
898
+ name: "caret_se",
899
+ config: { withArrow: false }
900
+ };
901
+ },
902
+ "caret_ne": (targetRect, balloonRect) => {
903
+ return {
904
+ top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
905
+ left: targetRect.right,
906
+ name: "caret_ne",
907
+ config: { withArrow: false }
908
+ };
909
+ },
910
+ "caret_sw": (targetRect, balloonRect) => {
911
+ return {
912
+ top: targetRect.bottom + VERTICAL_SPACING,
913
+ left: targetRect.right - balloonRect.width,
914
+ name: "caret_sw",
915
+ config: { withArrow: false }
916
+ };
917
+ },
918
+ "caret_nw": (targetRect, balloonRect) => {
919
+ return {
920
+ top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
921
+ left: targetRect.right - balloonRect.width,
922
+ name: "caret_nw",
923
+ config: { withArrow: false }
924
+ };
925
+ }
926
+ };
927
+ if (Object.prototype.hasOwnProperty.call(positions, preferredPosition)) return [positions[preferredPosition]];
928
+ return uiLanguageDirection !== "rtl" ? [
929
+ positions.caret_se,
930
+ positions.caret_sw,
931
+ positions.caret_ne,
932
+ positions.caret_nw
933
+ ] : [
934
+ positions.caret_sw,
935
+ positions.caret_se,
936
+ positions.caret_nw,
937
+ positions.caret_ne
938
+ ];
1048
939
  }
1049
940
  /**
1050
- * Returns a marker definition of the last valid occurring marker in a given string.
1051
- * If there is no valid marker in a string, it returns undefined.
1052
- *
1053
- * Example of returned object:
1054
- *
1055
- * ```ts
1056
- * {
1057
- * marker: '@',
1058
- * position: 4,
1059
- * minimumCharacters: 0
1060
- * }
1061
- * ````
1062
- *
1063
- * @param feedsWithPattern Registered feeds in editor for mention plugin with created RegExp for matching marker.
1064
- * @param text String to find the marker in
1065
- * @returns Matched marker's definition
1066
- */ function getLastValidMarkerInText(feedsWithPattern, text) {
1067
- let lastValidMarker;
1068
- for (const feed of feedsWithPattern){
1069
- const currentMarkerLastIndex = text.lastIndexOf(feed.marker);
1070
- if (currentMarkerLastIndex > 0 && !text.substring(currentMarkerLastIndex - 1).match(feed.pattern)) {
1071
- continue;
1072
- }
1073
- if (!lastValidMarker || currentMarkerLastIndex >= lastValidMarker.position) {
1074
- lastValidMarker = {
1075
- marker: feed.marker,
1076
- position: currentMarkerLastIndex,
1077
- minimumCharacters: feed.minimumCharacters,
1078
- pattern: feed.pattern
1079
- };
1080
- }
1081
- }
1082
- return lastValidMarker;
941
+ * Returns a marker definition of the last valid occurring marker in a given string.
942
+ * If there is no valid marker in a string, it returns undefined.
943
+ *
944
+ * Example of returned object:
945
+ *
946
+ * ```ts
947
+ * {
948
+ * marker: '@',
949
+ * position: 4,
950
+ * minimumCharacters: 0
951
+ * }
952
+ * ````
953
+ *
954
+ * @param feedsWithPattern Registered feeds in editor for mention plugin with created RegExp for matching marker.
955
+ * @param text String to find the marker in
956
+ * @returns Matched marker's definition
957
+ */
958
+ function getLastValidMarkerInText(feedsWithPattern, text) {
959
+ let lastValidMarker;
960
+ for (const feed of feedsWithPattern) {
961
+ const currentMarkerLastIndex = text.lastIndexOf(feed.marker);
962
+ if (currentMarkerLastIndex > 0 && !text.substring(currentMarkerLastIndex - 1).match(feed.pattern)) continue;
963
+ if (!lastValidMarker || currentMarkerLastIndex >= lastValidMarker.position) lastValidMarker = {
964
+ marker: feed.marker,
965
+ position: currentMarkerLastIndex,
966
+ minimumCharacters: feed.minimumCharacters,
967
+ pattern: feed.pattern
968
+ };
969
+ }
970
+ return lastValidMarker;
1083
971
  }
1084
972
  /**
1085
- * Creates a RegExp pattern for the marker.
1086
- *
1087
- * Function has to be exported to achieve 100% code coverage.
1088
- *
1089
- * @internal
1090
- */ function createRegExp(marker, minimumCharacters) {
1091
- const numberOfCharacters = minimumCharacters == 0 ? '*' : `{${minimumCharacters},}`;
1092
- const openAfterCharacters = env.features.isRegExpUnicodePropertySupported ? '\\p{Ps}\\p{Pi}"\'' : '\\(\\[{"\'';
1093
- const mentionCharacters = '.';
1094
- // I wanted to make an util out of it, but since this regexp uses "u" flag, it became difficult.
1095
- // When "u" flag is used, the regexp has "strict" escaping rules, i.e. if you try to escape a character that does not need
1096
- // to be escaped, RegExp() will throw. It made it difficult to write a generic util, because different characters are
1097
- // allowed in different context. For example, escaping "-" sometimes was correct, but sometimes it threw an error.
1098
- marker = marker.replace(/[.*+?^${}()\-|[\]\\]/g, '\\$&');
1099
- // The pattern consists of 3 groups:
1100
- //
1101
- // - 0 (non-capturing): Opening sequence - start of the line, space or an opening punctuation character like "(" or "\"",
1102
- // - 1: The marker character(s),
1103
- // - 2: Mention input (taking the minimal length into consideration to trigger the UI),
1104
- //
1105
- // The pattern matches up to the caret (end of string switch - $).
1106
- // (0: opening sequence )(1: marker )(2: typed mention )$
1107
- const pattern = `(?:^|[ ${openAfterCharacters}])(${marker})(${mentionCharacters}${numberOfCharacters})$`;
1108
- return new RegExp(pattern, 'u');
973
+ * Creates a RegExp pattern for the marker.
974
+ *
975
+ * Function has to be exported to achieve 100% code coverage.
976
+ *
977
+ * @internal
978
+ */
979
+ function createRegExp(marker, minimumCharacters) {
980
+ const numberOfCharacters = minimumCharacters == 0 ? "*" : `{${minimumCharacters},}`;
981
+ const openAfterCharacters = env.features.isRegExpUnicodePropertySupported ? "\\p{Ps}\\p{Pi}\"'" : "\\(\\[{\"'";
982
+ const mentionCharacters = ".";
983
+ marker = marker.replace(/[.*+?^${}()\-|[\]\\]/g, "\\$&");
984
+ const pattern = `(?:^|[ ${openAfterCharacters}])(${marker})(${mentionCharacters}${numberOfCharacters})$`;
985
+ return new RegExp(pattern, "u");
1109
986
  }
1110
987
  /**
1111
- * Creates a test callback for the marker to be used in the text watcher instance.
1112
- *
1113
- * @param feedsWithPattern Feeds of mention plugin configured in editor with RegExp to match marker in text
1114
- */ function createTestCallback(feedsWithPattern) {
1115
- const textMatcher = (text)=>{
1116
- const markerDefinition = getLastValidMarkerInText(feedsWithPattern, text);
1117
- if (!markerDefinition) {
1118
- return false;
1119
- }
1120
- let splitStringFrom = 0;
1121
- if (markerDefinition.position !== 0) {
1122
- splitStringFrom = markerDefinition.position - 1;
1123
- }
1124
- const textToTest = text.substring(splitStringFrom);
1125
- return markerDefinition.pattern.test(textToTest);
1126
- };
1127
- return textMatcher;
988
+ * Creates a test callback for the marker to be used in the text watcher instance.
989
+ *
990
+ * @param feedsWithPattern Feeds of mention plugin configured in editor with RegExp to match marker in text
991
+ */
992
+ function createTestCallback(feedsWithPattern) {
993
+ const textMatcher = (text) => {
994
+ const markerDefinition = getLastValidMarkerInText(feedsWithPattern, text);
995
+ if (!markerDefinition) return false;
996
+ let splitStringFrom = 0;
997
+ if (markerDefinition.position !== 0) splitStringFrom = markerDefinition.position - 1;
998
+ const textToTest = text.substring(splitStringFrom);
999
+ return markerDefinition.pattern.test(textToTest);
1000
+ };
1001
+ return textMatcher;
1128
1002
  }
1129
1003
  /**
1130
- * Creates a text matcher from the marker.
1131
- */ function requestFeedText(markerDefinition, text) {
1132
- let splitStringFrom = 0;
1133
- if (markerDefinition.position !== 0) {
1134
- splitStringFrom = markerDefinition.position - 1;
1135
- }
1136
- const regExp = createRegExp(markerDefinition.marker, 0);
1137
- const textToMatch = text.substring(splitStringFrom);
1138
- const match = textToMatch.match(regExp);
1139
- return match[2];
1004
+ * Creates a text matcher from the marker.
1005
+ */
1006
+ function requestFeedText(markerDefinition, text) {
1007
+ let splitStringFrom = 0;
1008
+ if (markerDefinition.position !== 0) splitStringFrom = markerDefinition.position - 1;
1009
+ const regExp = createRegExp(markerDefinition.marker, 0);
1010
+ return text.substring(splitStringFrom).match(regExp)[2];
1140
1011
  }
1141
1012
  /**
1142
- * The default feed callback.
1143
- */ function createFeedCallback(feedItems) {
1144
- return (feedText)=>{
1145
- const filteredItems = feedItems// Make the default mention feed case-insensitive.
1146
- .filter((item)=>{
1147
- // Item might be defined as object.
1148
- const itemId = typeof item == 'string' ? item : String(item.id);
1149
- // The default feed is case insensitive.
1150
- return itemId.toLowerCase().includes(feedText.toLowerCase());
1151
- });
1152
- return filteredItems;
1153
- };
1013
+ * The default feed callback.
1014
+ */
1015
+ function createFeedCallback(feedItems) {
1016
+ return (feedText) => {
1017
+ return feedItems.filter((item) => {
1018
+ return (typeof item == "string" ? item : String(item.id)).toLowerCase().includes(feedText.toLowerCase());
1019
+ });
1020
+ };
1154
1021
  }
1155
1022
  /**
1156
- * Checks if position in inside or right after a text with a mention.
1157
- */ function isPositionInExistingMention(position) {
1158
- // The text watcher listens only to changed range in selection - so the selection attributes are not yet available
1159
- // and you cannot use selection.hasAttribute( 'mention' ) just yet.
1160
- // See https://github.com/ckeditor/ckeditor5-engine/issues/1723.
1161
- const hasMention = position.textNode && position.textNode.hasAttribute('mention');
1162
- const nodeBefore = position.nodeBefore;
1163
- return hasMention || nodeBefore && nodeBefore.is('$text') && nodeBefore.hasAttribute('mention');
1023
+ * Checks if position in inside or right after a text with a mention.
1024
+ */
1025
+ function isPositionInExistingMention(position) {
1026
+ const hasMention = position.textNode && position.textNode.hasAttribute("mention");
1027
+ const nodeBefore = position.nodeBefore;
1028
+ return hasMention || nodeBefore && nodeBefore.is("$text") && nodeBefore.hasAttribute("mention");
1164
1029
  }
1165
1030
  /**
1166
- * Checks if the closest marker offset is at the beginning of a mention.
1167
- *
1168
- * See https://github.com/ckeditor/ckeditor5/issues/11400.
1169
- */ function isMarkerInExistingMention(markerPosition) {
1170
- const nodeAfter = markerPosition.nodeAfter;
1171
- return nodeAfter && nodeAfter.is('$text') && nodeAfter.hasAttribute('mention');
1031
+ * Checks if the closest marker offset is at the beginning of a mention.
1032
+ *
1033
+ * See https://github.com/ckeditor/ckeditor5/issues/11400.
1034
+ */
1035
+ function isMarkerInExistingMention(markerPosition) {
1036
+ const nodeAfter = markerPosition.nodeAfter;
1037
+ return nodeAfter && nodeAfter.is("$text") && nodeAfter.hasAttribute("mention");
1172
1038
  }
1173
1039
  /**
1174
- * Checks if string is a valid mention marker.
1175
- */ function isValidMentionMarker(marker) {
1176
- return !!marker;
1040
+ * Checks if string is a valid mention marker.
1041
+ */
1042
+ function isValidMentionMarker(marker) {
1043
+ return !!marker;
1177
1044
  }
1178
1045
  /**
1179
- * Checks the mention plugins is in completion mode (e.g. when typing is after a valid mention string like @foo).
1180
- */ function checkIfStillInCompletionMode(editor) {
1181
- return editor.model.markers.has('mention');
1046
+ * Checks the mention plugins is in completion mode (e.g. when typing is after a valid mention string like @foo).
1047
+ */
1048
+ function checkIfStillInCompletionMode(editor) {
1049
+ return editor.model.markers.has("mention");
1182
1050
  }
1183
1051
 
1184
1052
  /**
1185
- * The mention plugin.
1186
- *
1187
- * For a detailed overview, check the {@glink features/mentions Mention feature} guide.
1188
- */ class Mention extends Plugin {
1189
- toMentionAttribute(viewElement, data) {
1190
- return _toMentionAttribute(viewElement, data);
1191
- }
1192
- /**
1193
- * @inheritDoc
1194
- */ static get pluginName() {
1195
- return 'Mention';
1196
- }
1197
- /**
1198
- * @inheritDoc
1199
- */ static get isOfficialPlugin() {
1200
- return true;
1201
- }
1202
- /**
1203
- * @inheritDoc
1204
- */ static get requires() {
1205
- return [
1206
- MentionEditing,
1207
- MentionUI
1208
- ];
1209
- }
1210
- }
1053
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1054
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1055
+ */
1056
+ /**
1057
+ * @module mention/mention
1058
+ */
1059
+ /**
1060
+ * The mention plugin.
1061
+ *
1062
+ * For a detailed overview, check the {@glink features/mentions Mention feature} guide.
1063
+ */
1064
+ var Mention = class extends Plugin {
1065
+ toMentionAttribute(viewElement, data) {
1066
+ return _toMentionAttribute(viewElement, data);
1067
+ }
1068
+ /**
1069
+ * @inheritDoc
1070
+ */
1071
+ static get pluginName() {
1072
+ return "Mention";
1073
+ }
1074
+ /**
1075
+ * @inheritDoc
1076
+ */
1077
+ static get isOfficialPlugin() {
1078
+ return true;
1079
+ }
1080
+ /**
1081
+ * @inheritDoc
1082
+ */
1083
+ static get requires() {
1084
+ return [MentionEditing, MentionUI];
1085
+ }
1086
+ };
1087
+
1088
+ /**
1089
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
1090
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
1091
+ */
1211
1092
 
1212
1093
  export { Mention, MentionCommand, MentionDomWrapperView, MentionEditing, MentionListItemView, MentionUI, MentionsView, _addMentionAttributes, createRegExp as _createMentionMarkerRegExp, _toMentionAttribute };
1213
- //# sourceMappingURL=index.js.map
1094
+ //# sourceMappingURL=index.js.map