@ckeditor/ckeditor5-mention 35.4.0 → 36.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,291 +1,231 @@
1
1
  /**
2
- * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module mention/mentionediting
8
7
  */
9
-
10
8
  import { Plugin } from 'ckeditor5/src/core';
11
9
  import { uid } from 'ckeditor5/src/utils';
12
-
13
10
  import MentionCommand from './mentioncommand';
14
-
15
11
  /**
16
12
  * The mention editing feature.
17
13
  *
18
14
  * It introduces the {@link module:mention/mentioncommand~MentionCommand command} and the `mention`
19
15
  * attribute in the {@link module:engine/model/model~Model model} which renders in the {@link module:engine/view/view view}
20
16
  * as a `<span class="mention" data-mention="@mention">`.
21
- *
22
- * @extends module:core/plugin~Plugin
23
17
  */
24
18
  export default class MentionEditing extends Plugin {
25
- /**
26
- * @inheritDoc
27
- */
28
- static get pluginName() {
29
- return 'MentionEditing';
30
- }
31
-
32
- /**
33
- * @inheritDoc
34
- */
35
- init() {
36
- const editor = this.editor;
37
- const model = editor.model;
38
- const doc = model.document;
39
-
40
- // Allow the mention attribute on all text nodes.
41
- model.schema.extend( '$text', { allowAttributes: 'mention' } );
42
-
43
- // Upcast conversion.
44
- editor.conversion.for( 'upcast' ).elementToAttribute( {
45
- view: {
46
- name: 'span',
47
- key: 'data-mention',
48
- classes: 'mention'
49
- },
50
- model: {
51
- key: 'mention',
52
- value: viewElement => _toMentionAttribute( viewElement )
53
- }
54
- } );
55
-
56
- // Downcast conversion.
57
- editor.conversion.for( 'downcast' ).attributeToElement( {
58
- model: 'mention',
59
- view: createViewMentionElement
60
- } );
61
- editor.conversion.for( 'downcast' ).add( preventPartialMentionDowncast );
62
-
63
- doc.registerPostFixer( writer => removePartialMentionPostFixer( writer, doc, model.schema ) );
64
- doc.registerPostFixer( writer => extendAttributeOnMentionPostFixer( writer, doc ) );
65
- doc.registerPostFixer( writer => selectionMentionAttributePostFixer( writer, doc ) );
66
-
67
- editor.commands.add( 'mention', new MentionCommand( editor ) );
68
- }
19
+ /**
20
+ * @inheritDoc
21
+ */
22
+ static get pluginName() {
23
+ return 'MentionEditing';
24
+ }
25
+ /**
26
+ * @inheritDoc
27
+ */
28
+ init() {
29
+ const editor = this.editor;
30
+ const model = editor.model;
31
+ const doc = model.document;
32
+ // Allow the mention attribute on all text nodes.
33
+ model.schema.extend('$text', { allowAttributes: 'mention' });
34
+ // Upcast conversion.
35
+ editor.conversion.for('upcast').elementToAttribute({
36
+ view: {
37
+ name: 'span',
38
+ key: 'data-mention',
39
+ classes: 'mention'
40
+ },
41
+ model: {
42
+ key: 'mention',
43
+ value: (viewElement) => _toMentionAttribute(viewElement)
44
+ }
45
+ });
46
+ // Downcast conversion.
47
+ editor.conversion.for('downcast').attributeToElement({
48
+ model: 'mention',
49
+ view: createViewMentionElement
50
+ });
51
+ editor.conversion.for('downcast').add(preventPartialMentionDowncast);
52
+ doc.registerPostFixer(writer => removePartialMentionPostFixer(writer, doc, model.schema));
53
+ doc.registerPostFixer(writer => extendAttributeOnMentionPostFixer(writer, doc));
54
+ doc.registerPostFixer(writer => selectionMentionAttributePostFixer(writer, doc));
55
+ editor.commands.add('mention', new MentionCommand(editor));
56
+ }
69
57
  }
70
-
71
- export function _addMentionAttributes( baseMentionData, data ) {
72
- return Object.assign( { uid: uid() }, baseMentionData, data || {} );
58
+ /**
59
+ * @internal
60
+ */
61
+ export function _addMentionAttributes(baseMentionData, data) {
62
+ return Object.assign({ uid: uid() }, baseMentionData, data || {});
73
63
  }
74
-
75
64
  /**
76
65
  * Creates a mention attribute value from the provided view element and optional data.
77
66
  *
78
67
  * This function is exposed as
79
68
  * {@link module:mention/mention~Mention#toMentionAttribute `editor.plugins.get( 'Mention' ).toMentionAttribute()`}.
80
69
  *
81
- * @protected
82
- * @param {module:engine/view/element~Element} viewElementOrMention
83
- * @param {String|Object} [data] Mention data to be extended.
84
- * @returns {module:mention/mention~MentionAttribute}
70
+ * @internal
85
71
  */
86
- export function _toMentionAttribute( viewElementOrMention, data ) {
87
- const dataMention = viewElementOrMention.getAttribute( 'data-mention' );
88
-
89
- const textNode = viewElementOrMention.getChild( 0 );
90
-
91
- // Do not convert empty mentions.
92
- if ( !textNode ) {
93
- return;
94
- }
95
-
96
- const baseMentionData = {
97
- id: dataMention,
98
- _text: textNode.data
99
- };
100
-
101
- return _addMentionAttributes( baseMentionData, data );
72
+ export function _toMentionAttribute(viewElementOrMention, data) {
73
+ const dataMention = viewElementOrMention.getAttribute('data-mention');
74
+ const textNode = viewElementOrMention.getChild(0);
75
+ // Do not convert empty mentions.
76
+ if (!textNode) {
77
+ return;
78
+ }
79
+ const baseMentionData = {
80
+ id: dataMention,
81
+ _text: textNode.data
82
+ };
83
+ return _addMentionAttributes(baseMentionData, data);
102
84
  }
103
-
104
- // A converter that blocks partial mention from being converted.
105
- //
106
- // This converter is registered with 'highest' priority in order to consume mention attribute before it is converted by
107
- // any other converters. This converter only consumes partial mention - those whose `_text` attribute is not equal to text with mention
108
- // attribute. This may happen when copying part of mention text.
109
- //
110
- // @param {module:engine/conversion/dwoncastdispatcher~DowncastDispatcher}
111
- function preventPartialMentionDowncast( dispatcher ) {
112
- dispatcher.on( 'attribute:mention', ( evt, data, conversionApi ) => {
113
- const mention = data.attributeNewValue;
114
-
115
- if ( !data.item.is( '$textProxy' ) || !mention ) {
116
- return;
117
- }
118
-
119
- const start = data.range.start;
120
- const textNode = start.textNode || start.nodeAfter;
121
-
122
- if ( textNode.data != mention._text ) {
123
- // Consume item to prevent partial mention conversion.
124
- conversionApi.consumable.consume( data.item, evt.name );
125
- }
126
- }, { priority: 'highest' } );
85
+ /**
86
+ * A converter that blocks partial mention from being converted.
87
+ *
88
+ * This converter is registered with 'highest' priority in order to consume mention attribute before it is converted by
89
+ * any other converters. This converter only consumes partial mention - those whose `_text` attribute is not equal to text with mention
90
+ * attribute. This may happen when copying part of mention text.
91
+ */
92
+ function preventPartialMentionDowncast(dispatcher) {
93
+ dispatcher.on('attribute:mention', (evt, data, conversionApi) => {
94
+ const mention = data.attributeNewValue;
95
+ if (!data.item.is('$textProxy') || !mention) {
96
+ return;
97
+ }
98
+ const start = data.range.start;
99
+ const textNode = start.textNode || start.nodeAfter;
100
+ if (textNode.data != mention._text) {
101
+ // Consume item to prevent partial mention conversion.
102
+ conversionApi.consumable.consume(data.item, evt.name);
103
+ }
104
+ }, { priority: 'highest' });
127
105
  }
128
-
129
- // Creates a mention element from the mention data.
130
- //
131
- // @param {Object} mention
132
- // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
133
- // @returns {module:engine/view/attributeelement~AttributeElement}
134
- function createViewMentionElement( mention, { writer } ) {
135
- if ( !mention ) {
136
- return;
137
- }
138
-
139
- const attributes = {
140
- class: 'mention',
141
- 'data-mention': mention.id
142
- };
143
-
144
- const options = {
145
- id: mention.uid,
146
- priority: 20
147
- };
148
-
149
- return writer.createAttributeElement( 'span', attributes, options );
106
+ /**
107
+ * Creates a mention element from the mention data.
108
+ */
109
+ function createViewMentionElement(mention, { writer }) {
110
+ if (!mention) {
111
+ return;
112
+ }
113
+ const attributes = {
114
+ class: 'mention',
115
+ 'data-mention': mention.id
116
+ };
117
+ const options = {
118
+ id: mention.uid,
119
+ priority: 20
120
+ };
121
+ return writer.createAttributeElement('span', attributes, options);
150
122
  }
151
-
152
- // Model post-fixer that disallows typing with selection when the selection is placed after the text node with the mention attribute or
153
- // before a text node with mention attribute.
154
- //
155
- // @param {module:engine/model/writer~Writer} writer
156
- // @param {module:engine/model/document~Document} doc
157
- // @returns {Boolean} Returns `true` if the selection was fixed.
158
- function selectionMentionAttributePostFixer( writer, doc ) {
159
- const selection = doc.selection;
160
- const focus = selection.focus;
161
-
162
- if ( selection.isCollapsed && selection.hasAttribute( 'mention' ) && shouldNotTypeWithMentionAt( focus ) ) {
163
- writer.removeSelectionAttribute( 'mention' );
164
-
165
- return true;
166
- }
123
+ /**
124
+ * Model post-fixer that disallows typing with selection when the selection is placed after the text node with the mention attribute or
125
+ * before a text node with mention attribute.
126
+ */
127
+ function selectionMentionAttributePostFixer(writer, doc) {
128
+ const selection = doc.selection;
129
+ const focus = selection.focus;
130
+ if (selection.isCollapsed && selection.hasAttribute('mention') && shouldNotTypeWithMentionAt(focus)) {
131
+ writer.removeSelectionAttribute('mention');
132
+ return true;
133
+ }
134
+ return false;
167
135
  }
168
-
169
- // Helper function to detect if mention attribute should be removed from selection.
170
- // This check makes only sense if the selection has mention attribute.
171
- //
172
- // The mention attribute should be removed from a selection when selection focus is placed:
173
- // a) after a text node
174
- // b) the position is at parents start - the selection will set attributes from node after.
175
- function shouldNotTypeWithMentionAt( position ) {
176
- const isAtStart = position.isAtStart;
177
- const isAfterAMention = position.nodeBefore && position.nodeBefore.is( '$text' );
178
-
179
- return isAfterAMention || isAtStart;
136
+ /**
137
+ * Helper function to detect if mention attribute should be removed from selection.
138
+ * This check makes only sense if the selection has mention attribute.
139
+ *
140
+ * The mention attribute should be removed from a selection when selection focus is placed:
141
+ * a) after a text node
142
+ * b) the position is at parents start - the selection will set attributes from node after.
143
+ */
144
+ function shouldNotTypeWithMentionAt(position) {
145
+ const isAtStart = position.isAtStart;
146
+ const isAfterAMention = position.nodeBefore && position.nodeBefore.is('$text');
147
+ return isAfterAMention || isAtStart;
180
148
  }
181
-
182
- // Model post-fixer that removes the mention attribute from the modified text node.
183
- //
184
- // @param {module:engine/model/writer~Writer} writer
185
- // @param {module:engine/model/document~Document} doc
186
- // @returns {Boolean} Returns `true` if the selection was fixed.
187
- function removePartialMentionPostFixer( writer, doc, schema ) {
188
- const changes = doc.differ.getChanges();
189
-
190
- let wasChanged = false;
191
-
192
- for ( const change of changes ) {
193
- // Checks the text node on the current position.
194
- const position = change.position;
195
-
196
- if ( change.name == '$text' ) {
197
- const nodeAfterInsertedTextNode = position.textNode && position.textNode.nextSibling;
198
-
199
- // Checks the text node where the change occurred.
200
- wasChanged = checkAndFix( position.textNode, writer ) || wasChanged;
201
-
202
- // Occurs on paste inside a text node with mention.
203
- wasChanged = checkAndFix( nodeAfterInsertedTextNode, writer ) || wasChanged;
204
- wasChanged = checkAndFix( position.nodeBefore, writer ) || wasChanged;
205
- wasChanged = checkAndFix( position.nodeAfter, writer ) || wasChanged;
206
- }
207
-
208
- // Checks text nodes in inserted elements (might occur when splitting a paragraph or pasting content inside text with mention).
209
- if ( change.name != '$text' && change.type == 'insert' ) {
210
- const insertedNode = position.nodeAfter;
211
-
212
- for ( const item of writer.createRangeIn( insertedNode ).getItems() ) {
213
- wasChanged = checkAndFix( item, writer ) || wasChanged;
214
- }
215
- }
216
-
217
- // Inserted inline elements might break mention.
218
- if ( change.type == 'insert' && schema.isInline( change.name ) ) {
219
- const nodeAfterInserted = position.nodeAfter && position.nodeAfter.nextSibling;
220
-
221
- wasChanged = checkAndFix( position.nodeBefore, writer ) || wasChanged;
222
- wasChanged = checkAndFix( nodeAfterInserted, writer ) || wasChanged;
223
- }
224
- }
225
-
226
- return wasChanged;
149
+ /**
150
+ * Model post-fixer that removes the mention attribute from the modified text node.
151
+ */
152
+ function removePartialMentionPostFixer(writer, doc, schema) {
153
+ const changes = doc.differ.getChanges();
154
+ let wasChanged = false;
155
+ for (const change of changes) {
156
+ if (change.type == 'attribute') {
157
+ continue;
158
+ }
159
+ // Checks the text node on the current position.
160
+ const position = change.position;
161
+ if (change.name == '$text') {
162
+ const nodeAfterInsertedTextNode = position.textNode && position.textNode.nextSibling;
163
+ // Checks the text node where the change occurred.
164
+ wasChanged = checkAndFix(position.textNode, writer) || wasChanged;
165
+ // Occurs on paste inside a text node with mention.
166
+ wasChanged = checkAndFix(nodeAfterInsertedTextNode, writer) || wasChanged;
167
+ wasChanged = checkAndFix(position.nodeBefore, writer) || wasChanged;
168
+ wasChanged = checkAndFix(position.nodeAfter, writer) || wasChanged;
169
+ }
170
+ // Checks text nodes in inserted elements (might occur when splitting a paragraph or pasting content inside text with mention).
171
+ if (change.name != '$text' && change.type == 'insert') {
172
+ const insertedNode = position.nodeAfter;
173
+ for (const item of writer.createRangeIn(insertedNode).getItems()) {
174
+ wasChanged = checkAndFix(item, writer) || wasChanged;
175
+ }
176
+ }
177
+ // Inserted inline elements might break mention.
178
+ if (change.type == 'insert' && schema.isInline(change.name)) {
179
+ const nodeAfterInserted = position.nodeAfter && position.nodeAfter.nextSibling;
180
+ wasChanged = checkAndFix(position.nodeBefore, writer) || wasChanged;
181
+ wasChanged = checkAndFix(nodeAfterInserted, writer) || wasChanged;
182
+ }
183
+ }
184
+ return wasChanged;
227
185
  }
228
-
229
- // This post-fixer will extend the attribute applied on the part of the mention so the whole text node of the mention will have
230
- // the added attribute.
231
- //
232
- // @param {module:engine/model/writer~Writer} writer
233
- // @param {module:engine/model/document~Document} doc
234
- // @returns {Boolean} Returns `true` if the selection was fixed.
235
- function extendAttributeOnMentionPostFixer( writer, doc ) {
236
- const changes = doc.differ.getChanges();
237
-
238
- let wasChanged = false;
239
-
240
- for ( const change of changes ) {
241
- if ( change.type === 'attribute' && change.attributeKey != 'mention' ) {
242
- // Checks the node on the left side of the range...
243
- const nodeBefore = change.range.start.nodeBefore;
244
- // ... and on the right side of the range.
245
- const nodeAfter = change.range.end.nodeAfter;
246
-
247
- for ( const node of [ nodeBefore, nodeAfter ] ) {
248
- if ( isBrokenMentionNode( node ) && node.getAttribute( change.attributeKey ) != change.attributeNewValue ) {
249
- writer.setAttribute( change.attributeKey, change.attributeNewValue, node );
250
-
251
- wasChanged = true;
252
- }
253
- }
254
- }
255
- }
256
-
257
- return wasChanged;
186
+ /**
187
+ * This post-fixer will extend the attribute applied on the part of the mention so the whole text node of the mention will have
188
+ * the added attribute.
189
+ */
190
+ function extendAttributeOnMentionPostFixer(writer, doc) {
191
+ const changes = doc.differ.getChanges();
192
+ let wasChanged = false;
193
+ for (const change of changes) {
194
+ if (change.type === 'attribute' && change.attributeKey != 'mention') {
195
+ // Checks the node on the left side of the range...
196
+ const nodeBefore = change.range.start.nodeBefore;
197
+ // ... and on the right side of the range.
198
+ const nodeAfter = change.range.end.nodeAfter;
199
+ for (const node of [nodeBefore, nodeAfter]) {
200
+ if (isBrokenMentionNode(node) && node.getAttribute(change.attributeKey) != change.attributeNewValue) {
201
+ writer.setAttribute(change.attributeKey, change.attributeNewValue, node);
202
+ wasChanged = true;
203
+ }
204
+ }
205
+ }
206
+ }
207
+ return wasChanged;
258
208
  }
259
-
260
- // Checks if a node has a correct mention attribute if present.
261
- // Returns `true` if the node is text and has a mention attribute whose text does not match the expected mention text.
262
- //
263
- // @param {module:engine/model/node~Node} node The node to check.
264
- // @returns {Boolean}
265
- function isBrokenMentionNode( node ) {
266
- if ( !node || !( node.is( '$text' ) || node.is( '$textProxy' ) ) || !node.hasAttribute( 'mention' ) ) {
267
- return false;
268
- }
269
-
270
- const text = node.data;
271
- const mention = node.getAttribute( 'mention' );
272
-
273
- const expectedText = mention._text;
274
-
275
- return text != expectedText;
209
+ /**
210
+ * Checks if a node has a correct mention attribute if present.
211
+ * Returns `true` if the node is text and has a mention attribute whose text does not match the expected mention text.
212
+ */
213
+ function isBrokenMentionNode(node) {
214
+ if (!node || !(node.is('$text') || node.is('$textProxy')) || !node.hasAttribute('mention')) {
215
+ return false;
216
+ }
217
+ const text = node.data;
218
+ const mention = node.getAttribute('mention');
219
+ const expectedText = mention._text;
220
+ return text != expectedText;
276
221
  }
277
-
278
- // Fixes a mention on a text node if it needs a fix.
279
- //
280
- // @param {module:engine/model/text~Text} textNode
281
- // @param {module:engine/model/writer~Writer} writer
282
- // @returns {Boolean}
283
- function checkAndFix( textNode, writer ) {
284
- if ( isBrokenMentionNode( textNode ) ) {
285
- writer.removeAttribute( 'mention', textNode );
286
-
287
- return true;
288
- }
289
-
290
- return false;
222
+ /**
223
+ * Fixes a mention on a text node if it needs a fix.
224
+ */
225
+ function checkAndFix(textNode, writer) {
226
+ if (isBrokenMentionNode(textNode)) {
227
+ writer.removeAttribute('mention', textNode);
228
+ return true;
229
+ }
230
+ return false;
291
231
  }