ckeditor5-image-upload-base64
Version:
The development environment of CKEditor 5 – the best browser-based rich text editor.
292 lines (238 loc) • 9.54 kB
JavaScript
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module mention/mentionediting
*/
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import uid from '@ckeditor/ckeditor5-utils/src/uid';
import MentionCommand from './mentioncommand';
/**
* The mention editing feature.
*
* It introduces the {@link module:mention/mentioncommand~MentionCommand command} and the `mention`
* attribute in the {@link module:engine/model/model~Model model} which renders in the {@link module:engine/view/view view}
* as a `<span class="mention" data-mention="@mention">`.
*
* @extends module:core/plugin~Plugin
*/
export default class MentionEditing extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'MentionEditing';
}
/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const model = editor.model;
const doc = model.document;
// Allow the mention attribute on all text nodes.
model.schema.extend( '$text', { allowAttributes: 'mention' } );
// Upcast conversion.
editor.conversion.for( 'upcast' ).elementToAttribute( {
view: {
name: 'span',
key: 'data-mention',
classes: 'mention'
},
model: {
key: 'mention',
value: _toMentionAttribute
}
} );
// Downcast conversion.
editor.conversion.for( 'downcast' ).attributeToElement( {
model: 'mention',
view: createViewMentionElement
} );
editor.conversion.for( 'downcast' ).add( preventPartialMentionDowncast );
doc.registerPostFixer( writer => removePartialMentionPostFixer( writer, doc, model.schema ) );
doc.registerPostFixer( writer => extendAttributeOnMentionPostFixer( writer, doc ) );
doc.registerPostFixer( writer => selectionMentionAttributePostFixer( writer, doc ) );
editor.commands.add( 'mention', new MentionCommand( editor ) );
}
}
export function _addMentionAttributes( baseMentionData, data ) {
return Object.assign( { uid: uid() }, baseMentionData, data || {} );
}
/**
* Creates a mention attribute value from the provided view element and optional data.
*
* This function is exposed as
* {@link module:mention/mention~Mention#toMentionAttribute `editor.plugins.get( 'Mention' ).toMentionAttribute()`}.
*
* @protected
* @param {module:engine/view/element~Element} viewElementOrMention
* @param {String|Object} [data] Mention data to be extended.
* @returns {module:mention/mention~MentionAttribute}
*/
export function _toMentionAttribute( viewElementOrMention, data ) {
const dataMention = viewElementOrMention.getAttribute( 'data-mention' );
const textNode = viewElementOrMention.getChild( 0 );
// Do not convert empty mentions.
if ( !textNode ) {
return;
}
const baseMentionData = {
id: dataMention,
_text: textNode.data
};
return _addMentionAttributes( baseMentionData, data );
}
// A converter that blocks partial mention from being converted.
//
// This converter is registered with 'highest' priority in order to consume mention attribute before it is converted by
// any other converters. This converter only consumes partial mention - those whose `_text` attribute is not equal to text with mention
// attribute. This may happen when copying part of mention text.
//
// @param {module:engine/conversion/dwoncastdispatcher~DowncastDispatcher}
function preventPartialMentionDowncast( dispatcher ) {
dispatcher.on( 'attribute:mention', ( evt, data, conversionApi ) => {
const mention = data.attributeNewValue;
if ( !data.item.is( '$textProxy' ) || !mention ) {
return;
}
const start = data.range.start;
const textNode = start.textNode || start.nodeAfter;
if ( textNode.data != mention._text ) {
// Consume item to prevent partial mention conversion.
conversionApi.consumable.consume( data.item, evt.name );
}
}, { priority: 'highest' } );
}
// Creates a mention element from the mention data.
//
// @param {Object} mention
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
// @returns {module:engine/view/attributeelement~AttributeElement}
function createViewMentionElement( mention, { writer } ) {
if ( !mention ) {
return;
}
const attributes = {
class: 'mention',
'data-mention': mention.id
};
const options = {
id: mention.uid,
priority: 20
};
return writer.createAttributeElement( 'span', attributes, options );
}
// Model post-fixer that disallows typing with selection when the selection is placed after the text node with the mention attribute or
// before a text node with mention attribute.
//
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/document~Document} doc
// @returns {Boolean} Returns `true` if the selection was fixed.
function selectionMentionAttributePostFixer( writer, doc ) {
const selection = doc.selection;
const focus = selection.focus;
if ( selection.isCollapsed && selection.hasAttribute( 'mention' ) && shouldNotTypeWithMentionAt( focus ) ) {
writer.removeSelectionAttribute( 'mention' );
return true;
}
}
// Helper function to detect if mention attribute should be removed from selection.
// This check makes only sense if the selection has mention attribute.
//
// The mention attribute should be removed from a selection when selection focus is placed:
// a) after a text node
// b) the position is at parents start - the selection will set attributes from node after.
function shouldNotTypeWithMentionAt( position ) {
const isAtStart = position.isAtStart;
const isAfterAMention = position.nodeBefore && position.nodeBefore.is( '$text' );
return isAfterAMention || isAtStart;
}
// Model post-fixer that removes the mention attribute from the modified text node.
//
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/document~Document} doc
// @returns {Boolean} Returns `true` if the selection was fixed.
function removePartialMentionPostFixer( writer, doc, schema ) {
const changes = doc.differ.getChanges();
let wasChanged = false;
for ( const change of changes ) {
// Checks the text node on the current position.
const position = change.position;
if ( change.name == '$text' ) {
const nodeAfterInsertedTextNode = position.textNode && position.textNode.nextSibling;
// Checks the text node where the change occurred.
wasChanged = checkAndFix( position.textNode, writer ) || wasChanged;
// Occurs on paste inside a text node with mention.
wasChanged = checkAndFix( nodeAfterInsertedTextNode, writer ) || wasChanged;
wasChanged = checkAndFix( position.nodeBefore, writer ) || wasChanged;
wasChanged = checkAndFix( position.nodeAfter, writer ) || wasChanged;
}
// Checks text nodes in inserted elements (might occur when splitting a paragraph or pasting content inside text with mention).
if ( change.name != '$text' && change.type == 'insert' ) {
const insertedNode = position.nodeAfter;
for ( const item of writer.createRangeIn( insertedNode ).getItems() ) {
wasChanged = checkAndFix( item, writer ) || wasChanged;
}
}
// Inserted inline elements might break mention.
if ( change.type == 'insert' && schema.isInline( change.name ) ) {
const nodeAfterInserted = position.nodeAfter && position.nodeAfter.nextSibling;
wasChanged = checkAndFix( position.nodeBefore, writer ) || wasChanged;
wasChanged = checkAndFix( nodeAfterInserted, writer ) || wasChanged;
}
}
return wasChanged;
}
// This post-fixer will extend the attribute applied on the part of the mention so the whole text node of the mention will have
// the added attribute.
//
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/document~Document} doc
// @returns {Boolean} Returns `true` if the selection was fixed.
function extendAttributeOnMentionPostFixer( writer, doc ) {
const changes = doc.differ.getChanges();
let wasChanged = false;
for ( const change of changes ) {
if ( change.type === 'attribute' && change.attributeKey != 'mention' ) {
// Checks the node on the left side of the range...
const nodeBefore = change.range.start.nodeBefore;
// ... and on the right side of the range.
const nodeAfter = change.range.end.nodeAfter;
for ( const node of [ nodeBefore, nodeAfter ] ) {
if ( isBrokenMentionNode( node ) && node.getAttribute( change.attributeKey ) != change.attributeNewValue ) {
writer.setAttribute( change.attributeKey, change.attributeNewValue, node );
wasChanged = true;
}
}
}
}
return wasChanged;
}
// Checks if a node has a correct mention attribute if present.
// Returns `true` if the node is text and has a mention attribute whose text does not match the expected mention text.
//
// @param {module:engine/model/node~Node} node The node to check.
// @returns {Boolean}
function isBrokenMentionNode( node ) {
if ( !node || !( node.is( '$text' ) || node.is( '$textProxy' ) ) || !node.hasAttribute( 'mention' ) ) {
return false;
}
const text = node.data;
const mention = node.getAttribute( 'mention' );
const expectedText = mention._text;
return text != expectedText;
}
// Fixes a mention on a text node if it needs a fix.
//
// @param {module:engine/model/text~Text} textNode
// @param {module:engine/model/writer~Writer} writer
// @returns {Boolean}
function checkAndFix( textNode, writer ) {
if ( isBrokenMentionNode( textNode ) ) {
writer.removeAttribute( 'mention', textNode );
return true;
}
return false;
}