selectionobserver.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. /**
  2. * @license Copyright (c) 2003-2015, 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.js';
  10. import MutationObserver from './mutationobserver.js';
  11. import log from '../../../utils/log.js';
  12. /**
  13. * Selection observer class observes selection changes in the document. If selection changes on the document this
  14. * observer checks if there are any mutations and if DOM selection is different than the
  15. * {@link module:engine/view/document~Document#selection view selection}. Selection observer fires
  16. * {@link module:engine/view/document~Document#event:selectionChange} event only if selection change was the only change in the document
  17. * and DOM selection is different then the view selection.
  18. *
  19. * Note that this observer is attached by the {@link module:engine/view/document~Document} and is available by default.
  20. *
  21. * @see module:engine/view/mutationobserver~MutationObserver
  22. * @extends module:engine/view/observer/observer~Observer.Observer
  23. */
  24. export default class SelectionObserver extends Observer {
  25. constructor( document ) {
  26. super( document );
  27. /**
  28. * Instance of the mutation observer. Selection observer calls
  29. * {@link module:engine/view/observer/mutationobserver~MutationObserver#flush} to ensure that the mutations will be handled before the
  30. * {@link module:engine/view/document~Document#event:selectionChange} event is fired.
  31. *
  32. * @readonly
  33. * @member {module:engine/view/observer/mutationobserver~MutationObserver}
  34. * module:engine/view/observer/selectionobserver~SelectionObserver#mutationObserver
  35. */
  36. this.mutationObserver = document.getObserver( MutationObserver );
  37. /**
  38. * Reference to the {@link module:engine/view/document~Document} object.
  39. *
  40. * @readonly
  41. * @member {module:engine/view/document~Document} module:engine/view/observer/selectionobserver~SelectionObserver#document
  42. */
  43. this.document = document;
  44. /**
  45. * Reference to the view {@link module:engine/view/selection~Selection} object used to compare new selection with it.
  46. *
  47. * @readonly
  48. * @member {module:engine/view/selection~Selection} module:engine/view/observer/selectionobserver~SelectionObserver#selection
  49. */
  50. this.selection = document.selection;
  51. /**
  52. * Reference to the {@link module:engine/view/document~Document#domConverter}.
  53. *
  54. * @readonly
  55. * @member {module:engine/view/domconverter~DomConverter} module:engine/view/observer/selectionobserver~SelectionObserver#domConverter
  56. */
  57. this.domConverter = document.domConverter;
  58. /**
  59. * Set of documents which have added "selectionchange" listener to avoid adding listener twice to the same
  60. * document.
  61. *
  62. * @private
  63. * @member {WeakSet.<Document>} module:engine/view/observer/selectionobserver~SelectionObserver#_documents
  64. */
  65. this._documents = new WeakSet();
  66. this._clearInfiniteLoopInterval = setInterval( () => this._clearInfiniteLoop(), 2000 );
  67. /**
  68. * Private property to store the last selection, to check if the code does not enter infinite loop.
  69. *
  70. * @private
  71. * @member {module:engine/view/selection~Selection} module:engine/view/observer/selectionobserver~SelectionObserver#_lastSelection
  72. */
  73. /**
  74. * Private property to store the last but one selection, to check if the code does not enter infinite loop.
  75. *
  76. * @private
  77. * @member {module:engine/view/selection~Selection} module:engine/view/observer/selectionobserver~SelectionObserver#_lastButOneSelection
  78. */
  79. /**
  80. * Private property to check if the code does not enter infinite loop.
  81. *
  82. * @private
  83. * @member {Number} module:engine/view/observer/selectionobserver~SelectionObserver#_loopbackCounter
  84. */
  85. }
  86. /**
  87. * @inheritDoc
  88. */
  89. observe( domElement ) {
  90. const domDocument = domElement.ownerDocument;
  91. // Add listener once per each document.
  92. if ( this._documents.has( domDocument ) ) {
  93. return;
  94. }
  95. this.listenTo( domDocument, 'selectionchange', () => {
  96. this._handleSelectionChange( domDocument );
  97. } );
  98. this._documents.add( domDocument );
  99. }
  100. /**
  101. * @inheritDoc
  102. */
  103. destroy() {
  104. super.destroy();
  105. clearInterval( this._clearInfiniteLoopInterval );
  106. }
  107. /**
  108. * Selection change listener. {@link module:engine/view/observer/mutationobserver~MutationObserver#flush Flush} mutations, check if
  109. * selection changes and fires {@link module:engine/view/document~Document#event:selectionChange} event.
  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. if ( this._isInfiniteLoop( newViewSelection ) ) {
  129. /**
  130. * Selection change observer detected an infinite rendering loop.
  131. * Most probably you try to put the selection in the position which is not allowed
  132. * by the browser and browser fixes it automatically what causes `selectionchange` event on
  133. * which a loopback through a model tries to re-render the wrong selection and again.
  134. *
  135. * @error selectionchange-infinite-loop
  136. */
  137. log.warn( 'selectionchange-infinite-loop: Selection change observer detected an infinite rendering loop.' );
  138. return;
  139. }
  140. // Should be fired only when selection change was the only document change.
  141. this.document.fire( 'selectionChange', {
  142. oldSelection: this.selection,
  143. newSelection: newViewSelection,
  144. domSelection: domSelection
  145. } );
  146. }
  147. /**
  148. * Checks if selection rendering entered an infinite loop.
  149. *
  150. * See https://github.com/ckeditor/ckeditor5-engine/issues/400.
  151. *
  152. * @private
  153. * @param {module:engine/view/selection~Selection} newSelection DOM selection converted to view.
  154. * @returns {Boolean} True is the same selection repeat more then 10 times.
  155. */
  156. _isInfiniteLoop( newSelection ) {
  157. // If the position is the same a the last one or the last but one we increment the counter.
  158. // We need to check last two selections because the browser will first fire a selectionchange event
  159. // for an incorrect selection and then for a corrected one.
  160. if ( this._lastSelection && this._lastButOneSelection &&
  161. ( newSelection.isEqual( this._lastSelection ) || newSelection.isEqual( this._lastButOneSelection ) ) ) {
  162. this._loopbackCounter++;
  163. } else {
  164. this._lastButOneSelection = this._lastSelection;
  165. this._lastSelection = newSelection;
  166. this._loopbackCounter = 0;
  167. }
  168. // This counter is reset every 2 seconds. 50 selection changes in 2 seconds is enough high number
  169. // to be very difficult (impossible) to achieve using just keyboard keys (during normal editor use).
  170. if ( this._loopbackCounter > 50 ) {
  171. return true;
  172. }
  173. return false;
  174. }
  175. /**
  176. * Clears `SelectionObserver` internal properties connected with preventing infinite loop.
  177. *
  178. * @protected
  179. */
  180. _clearInfiniteLoop() {
  181. this._lastSelection = null;
  182. this._lastButOneSelection = null;
  183. this._loopbackCounter = 0;
  184. }
  185. }
  186. /**
  187. * Fired when selection has changed. This event is fired only when the selection change was the only change that happened
  188. * in the document, and old selection is different then the new selection.
  189. *
  190. * Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
  191. *
  192. * Note that because {@link module:engine/view/observer/selectionobserver~SelectionObserver} is attached by the
  193. * {@link module:engine/view/document~Document}
  194. * this event is available by default.
  195. *
  196. * @see module:engine/view/observer/selectionobserver~SelectionObserver
  197. * @event module:engine/view/document~Document#event:selectionChange
  198. * @param {Object} data
  199. * @param {module:engine/view/selection~Selection} data.oldSelection Old View selection which is
  200. * {@link module:engine/view/document~Document#selection}.
  201. * @param {module:engine/view/selection~Selection} data.newSelection New View selection which is converted DOM selection.
  202. * @param {Selection} data.domSelection Native DOM selection.
  203. */