mentionediting.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module mention/mentionediting
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import uid from '@ckeditor/ckeditor5-utils/src/uid';
  10. import MentionCommand from './mentioncommand';
  11. /**
  12. * The mention editing feature.
  13. *
  14. * It introduces the {@link module:mention/mentioncommand~MentionCommand command} and the `mention`
  15. * attribute in the {@link module:engine/model/model~Model model} which renders in the {@link module:engine/view/view view}
  16. * as a `<span class="mention" data-mention="name">`.
  17. *
  18. * @extends module:core/plugin~Plugin
  19. */
  20. export default class MentionEditing extends Plugin {
  21. /**
  22. * @inheritDoc
  23. */
  24. static get pluginName() {
  25. return 'MentionEditing';
  26. }
  27. /**
  28. * @inheritDoc
  29. */
  30. init() {
  31. const editor = this.editor;
  32. const model = editor.model;
  33. const doc = model.document;
  34. // Allow mention attribute on all text nodes.
  35. model.schema.extend( '$text', { allowAttributes: 'mention' } );
  36. editor.conversion.for( 'upcast' ).elementToAttribute( {
  37. view: {
  38. name: 'span',
  39. key: 'data-mention',
  40. classes: 'mention'
  41. },
  42. model: {
  43. key: 'mention',
  44. value: parseMentionViewItemAttributes
  45. }
  46. } );
  47. editor.conversion.for( 'downcast' ).attributeToElement( {
  48. model: 'mention',
  49. view: createViewMentionElement
  50. } );
  51. doc.registerPostFixer( writer => removePartialMentionPostFixer( writer, doc ) );
  52. doc.registerPostFixer( writer => selectionMentionAttributePostFixer( writer, doc ) );
  53. editor.commands.add( 'mention', new MentionCommand( editor ) );
  54. }
  55. }
  56. // Parses matched view element to mention attribute value.
  57. //
  58. // @param {module:engine/view/element} viewElement
  59. // @returns {Object} Mention attribute value
  60. function parseMentionViewItemAttributes( viewElement ) {
  61. const dataMention = viewElement.getAttribute( 'data-mention' );
  62. const textNode = viewElement.getChild( 0 );
  63. // Do not parse empty mentions.
  64. if ( !textNode || !textNode.is( 'text' ) ) {
  65. return;
  66. }
  67. const mentionString = textNode.data;
  68. // Assume that mention is set as marker + mention name.
  69. const marker = mentionString.slice( 0, 1 );
  70. const name = mentionString.slice( 1 );
  71. // Do not upcast partial mentions - might come from copy-paste of partially selected mention.
  72. if ( name != dataMention ) {
  73. return;
  74. }
  75. // Set UID for mention to not merge mentions in the same block that are next to each other.
  76. return { name: dataMention, _marker: marker, _id: uid() };
  77. }
  78. // Creates mention element from mention data.
  79. //
  80. // @param {Object} mention
  81. // @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
  82. // @returns {module:engine/view/attributeelement~AttributeElement}
  83. function createViewMentionElement( mention, viewWriter ) {
  84. if ( !mention ) {
  85. return;
  86. }
  87. const attributes = {
  88. class: 'mention',
  89. 'data-mention': mention.name
  90. };
  91. const options = {
  92. id: mention._id
  93. };
  94. return viewWriter.createAttributeElement( 'span', attributes, options );
  95. }
  96. // Model post-fixer that disallows typing with selection when selection is placed after the text node with mention attribute.
  97. //
  98. // @param {module:engine/model/writer~Writer} writer
  99. // @param {module:engine/model/document~Document} doc
  100. // @returns {Boolean} Returns true if selection was fixed.
  101. function selectionMentionAttributePostFixer( writer, doc ) {
  102. const selection = doc.selection;
  103. const focus = selection.focus;
  104. if ( selection.isCollapsed && selection.hasAttribute( 'mention' ) && isNodeBeforeAText( focus ) ) {
  105. writer.removeSelectionAttribute( 'mention' );
  106. return true;
  107. }
  108. function isNodeBeforeAText( position ) {
  109. return position.nodeBefore && position.nodeBefore.is( 'text' );
  110. }
  111. }
  112. // Model post-fixer that removes mention attribute from modified text node.
  113. //
  114. // @param {module:engine/model/writer~Writer} writer
  115. // @param {module:engine/model/document~Document} doc
  116. // @returns {Boolean} Returns true if selection was fixed.
  117. function removePartialMentionPostFixer( writer, doc ) {
  118. const changes = doc.differ.getChanges();
  119. let wasChanged = false;
  120. for ( const change of changes ) {
  121. // Check if user edited part of a mention.
  122. if ( change.type == 'insert' || change.type == 'remove' ) {
  123. const textNode = change.position.textNode;
  124. if ( change.name == '$text' && textNode && textNode.hasAttribute( 'mention' ) ) {
  125. writer.removeAttribute( 'mention', textNode );
  126. wasChanged = true;
  127. }
  128. }
  129. // Additional check for deleting last character of a text node.
  130. if ( change.type == 'remove' ) {
  131. const nodeBefore = change.position.nodeBefore;
  132. if ( nodeBefore && nodeBefore.hasAttribute( 'mention' ) ) {
  133. const text = nodeBefore.data;
  134. const mention = nodeBefore.getAttribute( 'mention' );
  135. const expectedText = mention._marker + mention.name;
  136. if ( text != expectedText ) {
  137. writer.removeAttribute( 'mention', nodeBefore );
  138. wasChanged = true;
  139. }
  140. }
  141. }
  142. }
  143. return wasChanged;
  144. }