/
mentioncommand.ts
203 lines (180 loc) · 6.04 KB
/
mentioncommand.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module mention/mentioncommand
*/
import { Command, type Editor } from 'ckeditor5/src/core.js';
import type { Range } from 'ckeditor5/src/engine.js';
import { CKEditorError, toMap } from 'ckeditor5/src/utils.js';
import { _addMentionAttributes } from './mentionediting.js';
const BRACKET_PAIRS = {
'(': ')',
'[': ']',
'{': '}'
} as const;
/**
* The mention command.
*
* The command is registered by {@link module:mention/mentionediting~MentionEditing} as `'mention'`.
*
* To insert a mention into a range, execute the command and specify a mention object with a range to replace:
*
* ```ts
* const focus = editor.model.document.selection.focus;
*
* // It will replace one character before the selection focus with the '#1234' text
* // with the mention attribute filled with passed attributes.
* editor.execute( 'mention', {
* marker: '#',
* mention: {
* id: '#1234',
* name: 'Foo',
* title: 'Big Foo'
* },
* range: editor.model.createRange( focus.getShiftedBy( -1 ), focus )
* } );
*
* // It will replace one character before the selection focus with the 'The "Big Foo"' text
* // with the mention attribute filled with passed attributes.
* editor.execute( 'mention', {
* marker: '#',
* mention: {
* id: '#1234',
* name: 'Foo',
* title: 'Big Foo'
* },
* text: 'The "Big Foo"',
* range: editor.model.createRange( focus.getShiftedBy( -1 ), focus )
* } );
* ```
*/
export default class MentionCommand extends Command {
/**
* @inheritDoc
*/
public constructor( editor: Editor ) {
super( editor );
// Since this command may pass range in execution parameters, it should be checked directly in execute block.
this._isEnabledBasedOnSelection = false;
}
/**
* @inheritDoc
*/
public override refresh(): void {
const model = this.editor.model;
const doc = model.document;
this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'mention' );
}
/**
* Executes the command.
*
* @param options Options for the executed command.
* @param options.mention The mention object to insert. When a string is passed, it will be used to create a plain
* object with the name attribute that equals the passed string.
* @param options.marker The marker character (e.g. `'@'`).
* @param options.text The text of the inserted mention. Defaults to the full mention string composed from `marker` and
* `mention` string or `mention.id` if an object is passed.
* @param options.range The range to replace.
* Note that the replaced range might be shorter than the inserted text with the mention attribute.
* @fires execute
*/
public override execute( options: {
mention: string | { id: string; [ key: string ]: unknown };
marker: string;
text?: string;
range?: Range;
} ): void {
const model = this.editor.model;
const document = model.document;
const selection = document.selection;
const mentionData = typeof options.mention == 'string' ? { id: options.mention } : options.mention;
const mentionID = mentionData.id;
const range = options.range || selection.getFirstRange();
// Don't execute command if range is in non-editable place.
if ( !model.canEditAt( range ) ) {
return;
}
const mentionText = options.text || mentionID;
const mention = _addMentionAttributes( { _text: mentionText, id: mentionID }, mentionData );
if ( options.marker.length != 1 ) {
/**
* The marker must be a single character.
*
* Correct markers: `'@'`, `'#'`.
*
* Incorrect markers: `'@@'`, `'[@'`.
*
* See {@link module:mention/mentionconfig~MentionConfig}.
*
* @error mentioncommand-incorrect-marker
*/
throw new CKEditorError(
'mentioncommand-incorrect-marker',
this
);
}
if ( mentionID.charAt( 0 ) != options.marker ) {
/**
* The feed item ID must start with the marker character.
*
* Correct mention feed setting:
*
* ```ts
* mentions: [
* {
* marker: '@',
* feed: [ '@Ann', '@Barney', ... ]
* }
* ]
* ```
*
* Incorrect mention feed setting:
*
* ```ts
* mentions: [
* {
* marker: '@',
* feed: [ 'Ann', 'Barney', ... ]
* }
* ]
* ```
*
* See {@link module:mention/mentionconfig~MentionConfig}.
*
* @error mentioncommand-incorrect-id
*/
throw new CKEditorError(
'mentioncommand-incorrect-id',
this
);
}
model.change( writer => {
const currentAttributes = toMap( selection.getAttributes() );
const attributesWithMention = new Map( currentAttributes.entries() );
attributesWithMention.set( 'mention', mention );
// Replace a range with the text with a mention.
const insertionRange = model.insertContent( writer.createText( mentionText, attributesWithMention ), range );
const nodeBefore = insertionRange.start.nodeBefore;
const nodeAfter = insertionRange.end.nodeAfter;
const isFollowedByWhiteSpace = nodeAfter && nodeAfter.is( '$text' ) && nodeAfter.data.startsWith( ' ' );
let isInsertedInBrackets = false;
if ( nodeBefore && nodeAfter && nodeBefore.is( '$text' ) && nodeAfter.is( '$text' ) ) {
const precedingCharacter = nodeBefore.data.slice( -1 );
const isPrecededByOpeningBracket = precedingCharacter in BRACKET_PAIRS;
const isFollowedByBracketClosure = isPrecededByOpeningBracket && nodeAfter.data.startsWith(
BRACKET_PAIRS[ precedingCharacter as keyof typeof BRACKET_PAIRS ]
);
isInsertedInBrackets = isPrecededByOpeningBracket && isFollowedByBracketClosure;
}
// Don't add a white space if either of the following is true:
// * there's already one after the mention;
// * the mention was inserted in the empty matching brackets.
// https://github.com/ckeditor/ckeditor5/issues/4651
if ( !isInsertedInBrackets && !isFollowedByWhiteSpace ) {
model.insertContent( writer.createText( ' ', currentAttributes ), range!.start.getShiftedBy( mentionText.length ) );
}
} );
}
}