8
0

selectionobserver.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/view/observer/selectionobserver
  7. */
  8. /* global setInterval, clearInterval */
  9. import Observer from './observer';
  10. import MutationObserver from './mutationobserver';
  11. import log from '@ckeditor/ckeditor5-utils/src/log';
  12. import debounce from '@ckeditor/ckeditor5-utils/src/lib/lodash/debounce';
  13. /**
  14. * Selection observer class observes selection changes in the document. If selection changes on the document this
  15. * observer checks if there are any mutations and if DOM selection is different than the
  16. * {@link module:engine/view/document~Document#selection view selection}. Selection observer fires
  17. * {@link module:engine/view/document~Document#event:selectionChange} event only if selection change was the only change in the document
  18. * and DOM selection is different then the view selection.
  19. *
  20. * Note that this observer is attached by the {@link module:engine/view/document~Document} and is available by default.
  21. *
  22. * @see module:engine/view/observer/mutationobserver~MutationObserver
  23. * @extends module:engine/view/observer/observer~Observer
  24. */
  25. export default class SelectionObserver extends Observer {
  26. constructor( document ) {
  27. super( document );
  28. /**
  29. * Instance of the mutation observer. Selection observer calls
  30. * {@link module:engine/view/observer/mutationobserver~MutationObserver#flush} to ensure that the mutations will be handled before the
  31. * {@link module:engine/view/document~Document#event:selectionChange} event is fired.
  32. *
  33. * @readonly
  34. * @member {module:engine/view/observer/mutationobserver~MutationObserver}
  35. * module:engine/view/observer/selectionobserver~SelectionObserver#mutationObserver
  36. */
  37. this.mutationObserver = document.getObserver( MutationObserver );
  38. /**
  39. * Reference to the {@link module:engine/view/document~Document} object.
  40. *
  41. * @readonly
  42. * @member {module:engine/view/document~Document} module:engine/view/observer/selectionobserver~SelectionObserver#document
  43. */
  44. this.document = document;
  45. /**
  46. * Reference to the view {@link module:engine/view/selection~Selection} object used to compare new selection with it.
  47. *
  48. * @readonly
  49. * @member {module:engine/view/selection~Selection} module:engine/view/observer/selectionobserver~SelectionObserver#selection
  50. */
  51. this.selection = document.selection;
  52. /**
  53. * Reference to the {@link module:engine/view/document~Document#domConverter}.
  54. *
  55. * @readonly
  56. * @member {module:engine/view/domconverter~DomConverter} module:engine/view/observer/selectionobserver~SelectionObserver#domConverter
  57. */
  58. this.domConverter = document.domConverter;
  59. /**
  60. * Set of documents which have added "selectionchange" listener to avoid adding listener twice to the same
  61. * document.
  62. *
  63. * @private
  64. * @member {WeakSet.<Document>} module:engine/view/observer/selectionobserver~SelectionObserver#_documents
  65. */
  66. this._documents = new WeakSet();
  67. /**
  68. * Fires debounced event `selectionChangeDone`. It uses `lodash#debounce` method to delay function call.
  69. *
  70. * @private
  71. * @param {Object} data Selection change data.
  72. * @method #_fireSelectionChangeDoneDebounced
  73. */
  74. this._fireSelectionChangeDoneDebounced = debounce( data => this.document.fire( 'selectionChangeDone', data ), 200 );
  75. this._clearInfiniteLoopInterval = setInterval( () => this._clearInfiniteLoop(), 1000 );
  76. /**
  77. * Private property to check if the code does not enter infinite loop.
  78. *
  79. * @private
  80. * @member {Number} module:engine/view/observer/selectionobserver~SelectionObserver#_loopbackCounter
  81. */
  82. this._loopbackCounter = 0;
  83. }
  84. /**
  85. * @inheritDoc
  86. */
  87. observe( domElement ) {
  88. const domDocument = domElement.ownerDocument;
  89. // Add listener once per each document.
  90. if ( this._documents.has( domDocument ) ) {
  91. return;
  92. }
  93. this.listenTo( domDocument, 'selectionchange', () => {
  94. this._handleSelectionChange( domDocument );
  95. } );
  96. this._documents.add( domDocument );
  97. }
  98. /**
  99. * @inheritDoc
  100. */
  101. destroy() {
  102. super.destroy();
  103. clearInterval( this._clearInfiniteLoopInterval );
  104. this._fireSelectionChangeDoneDebounced.cancel();
  105. }
  106. /**
  107. * Selection change listener. {@link module:engine/view/observer/mutationobserver~MutationObserver#flush Flush} mutations, check if
  108. * selection changes and fires {@link module:engine/view/document~Document#event:selectionChange} event on every change
  109. * and {@link module:engine/view/document~Document#event:selectionChangeDone} when selection stop changing.
  110. *
  111. * @private
  112. * @param {Document} domDocument DOM document.
  113. */
  114. _handleSelectionChange( domDocument ) {
  115. if ( !this.isEnabled || !this.document.isFocused ) {
  116. return;
  117. }
  118. // Ensure the mutation event will be before selection event on all browsers.
  119. this.mutationObserver.flush();
  120. // If there were mutations then the view will be re-rendered by the mutation observer and selection
  121. // will be updated, so selections will equal and event will not be fired, as expected.
  122. const domSelection = domDocument.defaultView.getSelection();
  123. const newViewSelection = this.domConverter.domSelectionToView( domSelection );
  124. if ( this.selection.isEqual( newViewSelection ) ) {
  125. return;
  126. }
  127. // Ensure we are not in the infinite loop (#400).
  128. // This counter is reset each second. 60 selection changes in 1 second is enough high number
  129. // to be very difficult (impossible) to achieve using just keyboard keys (during normal editor use).
  130. if ( ++this._loopbackCounter > 60 ) {
  131. /**
  132. * Selection change observer detected an infinite rendering loop.
  133. * Most probably you try to put the selection in the position which is not allowed
  134. * by the browser and browser fixes it automatically what causes `selectionchange` event on
  135. * which a loopback through a model tries to re-render the wrong selection and again.
  136. *
  137. * @error selectionchange-infinite-loop
  138. */
  139. log.warn( 'selectionchange-infinite-loop: Selection change observer detected an infinite rendering loop.' );
  140. return;
  141. }
  142. const data = {
  143. oldSelection: this.selection,
  144. newSelection: newViewSelection,
  145. domSelection: domSelection
  146. };
  147. // Should be fired only when selection change was the only document change.
  148. this.document.fire( 'selectionChange', data );
  149. // Call` #_fireSelectionChangeDoneDebounced` every time when `selectionChange` event is fired.
  150. // This function is debounced what means that `selectionChangeDone` event will be fired only when
  151. // defined int the function time will elapse since the last time the function was called.
  152. // So `selectionChangeDone` will be fired when selection will stop changing.
  153. this._fireSelectionChangeDoneDebounced( data );
  154. }
  155. /**
  156. * Clears `SelectionObserver` internal properties connected with preventing infinite loop.
  157. *
  158. * @protected
  159. */
  160. _clearInfiniteLoop() {
  161. this._loopbackCounter = 0;
  162. }
  163. }
  164. /**
  165. * Fired when selection has changed. This event is fired only when the selection change was the only change that happened
  166. * in the document, and old selection is different then the new selection.
  167. *
  168. * Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
  169. *
  170. * Note that because {@link module:engine/view/observer/selectionobserver~SelectionObserver} is attached by the
  171. * {@link module:engine/view/document~Document}
  172. * this event is available by default.
  173. *
  174. * @see module:engine/view/observer/selectionobserver~SelectionObserver
  175. * @event module:engine/view/document~Document#event:selectionChange
  176. * @param {Object} data
  177. * @param {module:engine/view/selection~Selection} data.oldSelection Old View selection which is
  178. * {@link module:engine/view/document~Document#selection}.
  179. * @param {module:engine/view/selection~Selection} data.newSelection New View selection which is converted DOM selection.
  180. * @param {Selection} data.domSelection Native DOM selection.
  181. */
  182. /**
  183. * Fired when selection stops changing.
  184. *
  185. * Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
  186. *
  187. * Note that because {@link module:engine/view/observer/selectionobserver~SelectionObserver} is attached by the
  188. * {@link module:engine/view/document~Document}
  189. * this event is available by default.
  190. *
  191. * @see module:engine/view/observer/selectionobserver~SelectionObserver
  192. * @event module:engine/view/document~Document#event:selectionChangeDone
  193. * @param {Object} data
  194. * @param {module:engine/view/selection~Selection} data.oldSelection Old View selection which is
  195. * {@link module:engine/view/document~Document#selection}.
  196. * @param {module:engine/view/selection~Selection} data.newSelection New View selection which is converted DOM selection.
  197. * @param {Selection} data.domSelection Native DOM selection.
  198. */