|
@@ -8,12 +8,17 @@
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
|
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
|
|
-import MentionCommand from './mentioncommand';
|
|
|
|
|
import uid from '@ckeditor/ckeditor5-utils/src/uid';
|
|
import uid from '@ckeditor/ckeditor5-utils/src/uid';
|
|
|
|
|
|
|
|
|
|
+import MentionCommand from './mentioncommand';
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* The mention editing feature.
|
|
* 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="name">`.
|
|
|
|
|
+ *
|
|
|
* @extends module:core/plugin~Plugin
|
|
* @extends module:core/plugin~Plugin
|
|
|
*/
|
|
*/
|
|
|
export default class MentionEditing extends Plugin {
|
|
export default class MentionEditing extends Plugin {
|
|
@@ -29,9 +34,11 @@ export default class MentionEditing extends Plugin {
|
|
|
*/
|
|
*/
|
|
|
init() {
|
|
init() {
|
|
|
const editor = this.editor;
|
|
const editor = this.editor;
|
|
|
|
|
+ const model = editor.model;
|
|
|
|
|
+ const doc = model.document;
|
|
|
|
|
|
|
|
- // Allow mention attribute on text nodes.
|
|
|
|
|
- editor.model.schema.extend( '$text', { allowAttributes: 'mention' } );
|
|
|
|
|
|
|
+ // Allow mention attribute on all text nodes.
|
|
|
|
|
+ model.schema.extend( '$text', { allowAttributes: 'mention' } );
|
|
|
|
|
|
|
|
editor.conversion.for( 'upcast' ).elementToAttribute( {
|
|
editor.conversion.for( 'upcast' ).elementToAttribute( {
|
|
|
view: {
|
|
view: {
|
|
@@ -41,99 +48,131 @@ export default class MentionEditing extends Plugin {
|
|
|
},
|
|
},
|
|
|
model: {
|
|
model: {
|
|
|
key: 'mention',
|
|
key: 'mention',
|
|
|
- value: viewItem => {
|
|
|
|
|
- const dataMention = viewItem.getAttribute( 'data-mention' );
|
|
|
|
|
|
|
+ value: parseMentionViewItemAttributes
|
|
|
|
|
+ }
|
|
|
|
|
+ } );
|
|
|
|
|
|
|
|
- const textNode = viewItem.getChild( 0 );
|
|
|
|
|
|
|
+ editor.conversion.for( 'downcast' ).attributeToElement( {
|
|
|
|
|
+ model: 'mention',
|
|
|
|
|
+ view: createViewMentionElement
|
|
|
|
|
+ } );
|
|
|
|
|
|
|
|
- if ( !textNode || !textNode.is( 'text' ) ) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ doc.registerPostFixer( writer => removePartialMentionPostFixer( writer, doc ) );
|
|
|
|
|
+ doc.registerPostFixer( writer => selectionMentionAttributePostFixer( writer, doc ) );
|
|
|
|
|
|
|
|
- const mentionString = textNode.data;
|
|
|
|
|
|
|
+ editor.commands.add( 'mention', new MentionCommand( editor ) );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // const marker = mentionString.slice( 0, 1 );
|
|
|
|
|
- const name = mentionString.slice( 1 );
|
|
|
|
|
|
|
+// Parses matched view element to mention attribute value.
|
|
|
|
|
+//
|
|
|
|
|
+// @param {module:engine/view/element} viewElement
|
|
|
|
|
+// @returns {Object} Mention attribute value
|
|
|
|
|
+function parseMentionViewItemAttributes( viewElement ) {
|
|
|
|
|
+ const dataMention = viewElement.getAttribute( 'data-mention' );
|
|
|
|
|
|
|
|
- if ( name != dataMention ) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const textNode = viewElement.getChild( 0 );
|
|
|
|
|
|
|
|
- return { name: dataMention, _id: uid() };
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } );
|
|
|
|
|
|
|
+ // Do not parse empty mentions.
|
|
|
|
|
+ if ( !textNode || !textNode.is( 'text' ) ) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- editor.conversion.for( 'downcast' ).attributeToElement( {
|
|
|
|
|
- model: 'mention',
|
|
|
|
|
- view: ( mention, viewWriter ) => {
|
|
|
|
|
- if ( !mention ) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const mentionString = textNode.data;
|
|
|
|
|
|
|
|
- const attributes = {
|
|
|
|
|
- class: 'mention',
|
|
|
|
|
- 'data-mention': mention.name
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ // Assume that mention is set as marker + mention name.
|
|
|
|
|
+ const marker = mentionString.slice( 0, 1 );
|
|
|
|
|
+ const name = mentionString.slice( 1 );
|
|
|
|
|
|
|
|
- const options = {
|
|
|
|
|
- id: mention._id
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ // Do not upcast partial mentions - might come from copy-paste of partially selected mention.
|
|
|
|
|
+ if ( name != dataMention ) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return viewWriter.createAttributeElement( 'span', attributes, options );
|
|
|
|
|
- }
|
|
|
|
|
- } );
|
|
|
|
|
|
|
+ // Set UID for mention to not merge mentions in the same block that are next to each other.
|
|
|
|
|
+ return { name: dataMention, _marker: marker, _id: uid() };
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // Remove mention attribute if text was edited.
|
|
|
|
|
- editor.model.document.registerPostFixer( writer => {
|
|
|
|
|
- const changes = editor.model.document.differ.getChanges();
|
|
|
|
|
|
|
+// Creates mention element from mention data.
|
|
|
|
|
+//
|
|
|
|
|
+// @param {Object} mention
|
|
|
|
|
+// @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
|
|
|
|
|
+// @returns {module:engine/view/attributeelement~AttributeElement}
|
|
|
|
|
+function createViewMentionElement( mention, viewWriter ) {
|
|
|
|
|
+ if ( !mention ) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- let wasChanged = false;
|
|
|
|
|
|
|
+ const attributes = {
|
|
|
|
|
+ class: 'mention',
|
|
|
|
|
+ 'data-mention': mention.name
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- for ( const change of changes ) {
|
|
|
|
|
- if ( change.type == 'insert' || change.type == 'remove' ) {
|
|
|
|
|
- const textNode = change.position.textNode;
|
|
|
|
|
|
|
+ const options = {
|
|
|
|
|
+ id: mention._id
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- if ( change.name == '$text' && textNode && textNode.hasAttribute( 'mention' ) ) {
|
|
|
|
|
- writer.removeAttribute( 'mention', textNode );
|
|
|
|
|
- wasChanged = true;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return viewWriter.createAttributeElement( 'span', attributes, options );
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- if ( change.type == 'remove' ) {
|
|
|
|
|
- const nodeBefore = change.position.nodeBefore;
|
|
|
|
|
|
|
+// Model post-fixer that disallows typing with selection when selection is placed after the text node with mention attribute.
|
|
|
|
|
+//
|
|
|
|
|
+// @param {module:engine/model/writer~Writer} writer
|
|
|
|
|
+// @param {module:engine/model/document~Document} doc
|
|
|
|
|
+// @returns {Boolean} Returns true if selection was fixed.
|
|
|
|
|
+function selectionMentionAttributePostFixer( writer, doc ) {
|
|
|
|
|
+ const selection = doc.selection;
|
|
|
|
|
+ const focus = selection.focus;
|
|
|
|
|
|
|
|
- if ( nodeBefore && nodeBefore.hasAttribute( 'mention' ) ) {
|
|
|
|
|
- const text = nodeBefore.data;
|
|
|
|
|
|
|
+ if ( selection.isCollapsed && selection.hasAttribute( 'mention' ) && isNodeBeforeAText( focus ) ) {
|
|
|
|
|
+ writer.removeSelectionAttribute( 'mention' );
|
|
|
|
|
|
|
|
- const mention = nodeBefore.getAttribute( 'mention' );
|
|
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const name = mention.name;
|
|
|
|
|
|
|
+ function isNodeBeforeAText( position ) {
|
|
|
|
|
+ return position.nodeBefore && position.nodeBefore.is( 'text' );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- const textName = text.slice( 1 );
|
|
|
|
|
|
|
+// Model post-fixer that removes mention attribute from modified text node.
|
|
|
|
|
+//
|
|
|
|
|
+// @param {module:engine/model/writer~Writer} writer
|
|
|
|
|
+// @param {module:engine/model/document~Document} doc
|
|
|
|
|
+// @returns {Boolean} Returns true if selection was fixed.
|
|
|
|
|
+function removePartialMentionPostFixer( writer, doc ) {
|
|
|
|
|
+ const changes = doc.differ.getChanges();
|
|
|
|
|
|
|
|
- if ( textName != name ) {
|
|
|
|
|
- writer.removeAttribute( 'mention', nodeBefore );
|
|
|
|
|
- wasChanged = true;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ let wasChanged = false;
|
|
|
|
|
+
|
|
|
|
|
+ for ( const change of changes ) {
|
|
|
|
|
+ // Check if user edited part of a mention.
|
|
|
|
|
+ if ( change.type == 'insert' || change.type == 'remove' ) {
|
|
|
|
|
+ const textNode = change.position.textNode;
|
|
|
|
|
+
|
|
|
|
|
+ if ( change.name == '$text' && textNode && textNode.hasAttribute( 'mention' ) ) {
|
|
|
|
|
+ writer.removeAttribute( 'mention', textNode );
|
|
|
|
|
+ wasChanged = true;
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return wasChanged;
|
|
|
|
|
- } );
|
|
|
|
|
|
|
+ // Additional check for deleting last character of a text node.
|
|
|
|
|
+ if ( change.type == 'remove' ) {
|
|
|
|
|
+ const nodeBefore = change.position.nodeBefore;
|
|
|
|
|
|
|
|
- editor.model.document.registerPostFixer( writer => {
|
|
|
|
|
- const selection = editor.model.document.selection;
|
|
|
|
|
- const focus = selection.focus;
|
|
|
|
|
|
|
+ if ( nodeBefore && nodeBefore.hasAttribute( 'mention' ) ) {
|
|
|
|
|
+ const text = nodeBefore.data;
|
|
|
|
|
+ const mention = nodeBefore.getAttribute( 'mention' );
|
|
|
|
|
|
|
|
- if ( selection.hasAttribute( 'mention' ) && selection.isCollapsed && focus.nodeBefore && focus.nodeBefore.is( 'text' ) ) {
|
|
|
|
|
- writer.removeSelectionAttribute( 'mention' );
|
|
|
|
|
|
|
+ const expectedText = mention._marker + mention.name;
|
|
|
|
|
|
|
|
- return true;
|
|
|
|
|
|
|
+ if ( text != expectedText ) {
|
|
|
|
|
+ writer.removeAttribute( 'mention', nodeBefore );
|
|
|
|
|
+ wasChanged = true;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- } );
|
|
|
|
|
-
|
|
|
|
|
- editor.commands.add( 'mention', new MentionCommand( editor ) );
|
|
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ return wasChanged;
|
|
|
}
|
|
}
|