@ckeditor/ckeditor5-mention
Version:
Mention feature for CKEditor 5.
153 lines (152 loc) • 6.12 kB
JavaScript
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module mention/mentioncommand
*/
import { Command } from 'ckeditor5/src/core.js';
import { CKEditorError, toMap } from 'ckeditor5/src/utils.js';
import { _addMentionAttributes } from './mentionediting.js';
const BRACKET_PAIRS = {
'(': ')',
'[': ']',
'{': '}'
};
/**
* 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
*/
constructor(editor) {
super(editor);
// Since this command may pass range in execution parameters, it should be checked directly in execute block.
this._isEnabledBasedOnSelection = false;
}
/**
* @inheritDoc
*/
refresh() {
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
*/
execute(options) {
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 (!mentionID.startsWith(options.marker)) {
/**
* The feed item ID must start with the marker character(s).
*
* 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]);
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));
}
});
}
}