textwatcher.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module mention/textwatcher
  7. */
  8. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  9. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  10. /**
  11. * The text watcher feature.
  12. *
  13. * Fires {@link module:mention/textwatcher~TextWatcher#event:matched `matched`} and
  14. * {@link module:mention/textwatcher~TextWatcher#event:unmatched `unmatched`} events on typing or selection changes.
  15. *
  16. * @private
  17. */
  18. export default class TextWatcher {
  19. /**
  20. * Creates a text watcher instance.
  21. * @param {module:core/editor/editor~Editor} editor
  22. * @param {Function} testCallback The function used to match the text.
  23. * @param {Function} textMatcherCallback The function used to process matched text.
  24. */
  25. constructor( editor, testCallback, textMatcherCallback ) {
  26. this.editor = editor;
  27. this.testCallback = testCallback;
  28. this.textMatcher = textMatcherCallback;
  29. this.hasMatch = false;
  30. this._startListening();
  31. }
  32. /**
  33. * The last matched text.
  34. *
  35. * @property {String}
  36. */
  37. get last() {
  38. return this._getText();
  39. }
  40. /**
  41. * Starts listening to the editor for typing and selection events.
  42. *
  43. * @private
  44. */
  45. _startListening() {
  46. const editor = this.editor;
  47. editor.model.document.selection.on( 'change:range', ( evt, { directChange } ) => {
  48. // Indirect changes (i.e. when the user types or external changes are applied) are handled in the document's change event.
  49. if ( !directChange ) {
  50. return;
  51. }
  52. this._evaluateTextBeforeSelection();
  53. } );
  54. editor.model.document.on( 'change:data', ( evt, batch ) => {
  55. if ( batch.type == 'transparent' ) {
  56. return false;
  57. }
  58. this._evaluateTextBeforeSelection();
  59. } );
  60. }
  61. /**
  62. * Checks the editor content for matched text.
  63. *
  64. * @fires matched
  65. * @fires unmatched
  66. *
  67. * @private
  68. */
  69. _evaluateTextBeforeSelection() {
  70. const text = this._getText();
  71. const textHasMatch = this.testCallback( text );
  72. if ( !textHasMatch && this.hasMatch ) {
  73. /**
  74. * Fired whenever the text does not match anymore. Fired only when the text watcher found a match.
  75. *
  76. * @event unmatched
  77. */
  78. this.fire( 'unmatched' );
  79. }
  80. this.hasMatch = textHasMatch;
  81. if ( textHasMatch ) {
  82. const matched = this.textMatcher( text );
  83. /**
  84. * Fired whenever the text watcher found a match.
  85. *
  86. * @event matched
  87. */
  88. this.fire( 'matched', { text, matched } );
  89. }
  90. }
  91. /**
  92. * Returns the text before the caret from the current selection block.
  93. *
  94. * @returns {String|undefined} The text from the block or undefined if the selection is not collapsed.
  95. * @private
  96. */
  97. _getText() {
  98. const editor = this.editor;
  99. const model = editor.model;
  100. const selection = model.document.selection;
  101. // Do nothing if the selection is not collapsed.
  102. if ( !selection.isCollapsed ) {
  103. return;
  104. }
  105. const rangeBeforeSelection = model.createRange( model.createPositionAt( selection.focus.parent, 0 ), selection.focus );
  106. return _getText( rangeBeforeSelection );
  107. }
  108. }
  109. /**
  110. * Returns the whole text from a given range by adding all data from the text nodes together.
  111. *
  112. * @protected
  113. * @param {module:engine/model/range~Range} range
  114. * @returns {String}
  115. */
  116. export function _getText( range ) {
  117. return Array.from( range.getItems() ).reduce( ( rangeText, node ) => {
  118. if ( node.is( 'softBreak' ) ) {
  119. // Trim text to softBreak
  120. return '';
  121. }
  122. return rangeText + node.data;
  123. }, '' );
  124. }
  125. mix( TextWatcher, EmitterMixin );