textwatcher.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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 typing/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:typing/textwatcher~TextWatcher#event:matched:data `matched:data`},
  14. * {@link module:typing/textwatcher~TextWatcher#event:matched:selection `matched:selection`} and
  15. * {@link module:typing/textwatcher~TextWatcher#event:unmatched `unmatched`} events on typing or selection changes.
  16. *
  17. * @private
  18. */
  19. export default class TextWatcher {
  20. /**
  21. * Creates a text watcher instance.
  22. * @param {module:engine/model/model~Model} model
  23. * @param {Function} testCallback The function used to match the text.
  24. */
  25. constructor( model, testCallback ) {
  26. this.model = model;
  27. this.testCallback = testCallback;
  28. this.hasMatch = false;
  29. this._startListening();
  30. }
  31. /**
  32. * Starts listening to the editor for typing and selection events.
  33. *
  34. * @private
  35. */
  36. _startListening() {
  37. const model = this.model;
  38. const document = model.document;
  39. document.selection.on( 'change:range', ( evt, { directChange } ) => {
  40. // Indirect changes (i.e. when the user types or external changes are applied) are handled in the document's change event.
  41. if ( !directChange ) {
  42. return;
  43. }
  44. // Act only on collapsed selection.
  45. if ( !document.selection.isCollapsed ) {
  46. if ( this.hasMatch ) {
  47. this.fire( 'unmatched' );
  48. this.hasMatch = false;
  49. }
  50. return;
  51. }
  52. this._evaluateTextBeforeSelection( 'selection' );
  53. } );
  54. document.on( 'change:data', ( evt, batch ) => {
  55. if ( batch.type == 'transparent' ) {
  56. return;
  57. }
  58. this._evaluateTextBeforeSelection( 'data' );
  59. } );
  60. }
  61. /**
  62. * Checks the editor content for matched text.
  63. *
  64. * @fires matched:data
  65. * @fires matched:selection
  66. * @fires unmatched
  67. *
  68. * @private
  69. */
  70. _evaluateTextBeforeSelection( suffix ) {
  71. const text = this._getText();
  72. const textHasMatch = this.testCallback( text );
  73. if ( !textHasMatch && this.hasMatch ) {
  74. /**
  75. * Fired whenever the text does not match anymore. Fired only when the text watcher found a match.
  76. *
  77. * @event unmatched
  78. */
  79. this.fire( 'unmatched' );
  80. }
  81. this.hasMatch = textHasMatch;
  82. if ( textHasMatch ) {
  83. /**
  84. * Fired whenever the text watcher found a match for data changes.
  85. *
  86. * @event matched:data
  87. */
  88. /**
  89. * Fired whenever the text watcher found a match for selection changes.
  90. *
  91. * @event matched:selection
  92. */
  93. this.fire( `matched:${ suffix }`, { text } );
  94. }
  95. }
  96. /**
  97. * Returns the text before the caret from the current selection block.
  98. *
  99. * @returns {String|undefined} The text from the block or undefined if the selection is not collapsed.
  100. * @private
  101. */
  102. _getText() {
  103. const model = this.model;
  104. const document = model.document;
  105. const selection = document.selection;
  106. const rangeBeforeSelection = model.createRange( model.createPositionAt( selection.focus.parent, 0 ), selection.focus );
  107. return _getText( rangeBeforeSelection );
  108. }
  109. }
  110. // Returns the whole text from a given range by adding all data from the text nodes together.
  111. //
  112. // @param {module:engine/model/range~Range} range
  113. // @returns {String}
  114. function _getText( range ) {
  115. return Array.from( range.getItems() ).reduce( ( rangeText, node ) => {
  116. if ( node.is( 'softBreak' ) ) {
  117. // Trim text to a softBreak.
  118. return '';
  119. }
  120. return rangeText + node.data;
  121. }, '' );
  122. }
  123. mix( TextWatcher, EmitterMixin );