@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/augmentation.d.ts +21 -21
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +1030 -1149
- package/dist/index.js.map +1 -1
- package/dist/mention.d.ts +73 -73
- package/dist/mentioncommand.d.ts +72 -72
- package/dist/mentionconfig.d.ts +249 -249
- package/dist/mentionediting.d.ts +37 -37
- package/dist/mentionui.d.ts +101 -101
- package/dist/ui/domwrapperview.d.ts +36 -36
- package/dist/ui/mentionlistitemview.d.ts +11 -11
- package/dist/ui/mentionsview.d.ts +55 -55
- package/package.json +6 -6
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
|
|
6
|
-
import { CKEditorError,
|
|
7
|
-
import {
|
|
8
|
-
import { TextWatcher } from
|
|
9
|
-
import { debounce } from
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// 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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// 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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1040
|
+
* Checks if string is a valid mention marker.
|
|
1041
|
+
*/
|
|
1042
|
+
function isValidMentionMarker(marker) {
|
|
1043
|
+
return !!marker;
|
|
1177
1044
|
}
|
|
1178
1045
|
/**
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|