8
0

mutationobserver.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Observer from './observer.js';
  6. import { startsWithFiller, getDataWithoutFiller } from '../filler.js';
  7. /**
  8. * Mutation observer class observes changes in the DOM, fires {@link engine.view.Document#mutations} event, mark view elements
  9. * as changed and call {@link engine.view.render}. Because all mutated nodes are marked as "to be rendered" and the
  10. * {@link engine.view.render} is called, all changes will be reverted, unless the mutation will be handled by the
  11. * {@link engine.view.Document#mutations} event listener. It means user will see only handled changes, and the editor will
  12. * block all changes which are not handled.
  13. *
  14. * Mutation Observer also take care of reducing number of mutations which are fired. It removes duplicates and
  15. * mutations on elements which do not have corresponding view elements. Also
  16. * {@link engine.view.Document.MutatatedText text mutation} is fired only if parent element do not change child list.
  17. *
  18. * Note that this observer is attached by the {@link engine.view.Document} and is available by default.
  19. *
  20. * @memberOf engine.view.observer
  21. * @extends engine.view.observer.Observer
  22. */
  23. export default class MutationObserver extends Observer {
  24. constructor( document ) {
  25. super( document );
  26. /**
  27. * Native mutation observer config.
  28. *
  29. * @private
  30. * @member {Object} engine.view.observer.MutationObserver#_config
  31. */
  32. this._config = {
  33. childList: true,
  34. characterData: true,
  35. characterDataOldValue: true,
  36. subtree: true
  37. };
  38. /**
  39. * Reference to the {@link engine.view.Document#domConverter}.
  40. *
  41. * @member {engine.view.DomConverter} engine.view.observer.MutationObserver#domConverter
  42. */
  43. this.domConverter = document.domConverter;
  44. /**
  45. * Reference to the {@link engine.view.Document#renderer}.
  46. *
  47. * @member {engine.view.Renderer} engine.view.observer.MutationObserver#renderer
  48. */
  49. this.renderer = document.renderer;
  50. /**
  51. * Observed DOM elements.
  52. *
  53. * @private
  54. * @member {Array.<HTMLElement>} engine.view.observer.MutationObserver#_domElements
  55. */
  56. this._domElements = [];
  57. /**
  58. * Native mutation observer.
  59. *
  60. * @private
  61. * @member {MutationObserver} engine.view.observer.MutationObserver#_mutationObserver
  62. */
  63. this._mutationObserver = new window.MutationObserver( this._onMutations.bind( this ) );
  64. }
  65. /**
  66. * Synchronously fires {@link engine.view.Document#mutations} event with all mutations in record queue.
  67. * At the same time empties the queue so mutations will not be fired twice.
  68. */
  69. flush() {
  70. this._onMutations( this._mutationObserver.takeRecords() );
  71. }
  72. /**
  73. * @inheritDoc
  74. */
  75. observe( domElement ) {
  76. this._domElements.push( domElement );
  77. if ( this.isEnabled ) {
  78. this._mutationObserver.observe( domElement, this._config );
  79. }
  80. }
  81. /**
  82. * @inheritDoc
  83. */
  84. enable() {
  85. super.enable();
  86. for ( let domElement of this._domElements ) {
  87. this._mutationObserver.observe( domElement, this._config );
  88. }
  89. }
  90. /**
  91. * @inheritDoc
  92. */
  93. disable() {
  94. super.disable();
  95. this._mutationObserver.disconnect();
  96. }
  97. /**
  98. * Handles mutations. Deduplicates, mark view elements to sync, fire event and call render.
  99. *
  100. * @private
  101. * @method engine.view.observer.MutationObserver#_onMutations
  102. * @param {Array.<Object>} domMutations Array of native mutations.
  103. */
  104. _onMutations( domMutations ) {
  105. // As a result of this.flush() we can have an empty collection.
  106. if ( domMutations.length === 0 ) {
  107. return;
  108. }
  109. const domConverter = this.domConverter;
  110. // Use map and set for deduplication.
  111. const mutatedTexts = new Map();
  112. const mutatedElements = new Set();
  113. // Handle `childList` mutations first, so we will be able to check if the `characterData` mutation is in the
  114. // element with changed structure anyway.
  115. for ( let mutation of domMutations ) {
  116. if ( mutation.type === 'childList' ) {
  117. const element = domConverter.getCorrespondingViewElement( mutation.target );
  118. if ( element ) {
  119. mutatedElements.add( element );
  120. }
  121. }
  122. }
  123. // Handle `characterData` mutations later, when we have the full list of nodes which changed structure.
  124. for ( let mutation of domMutations ) {
  125. if ( mutation.type === 'characterData' ) {
  126. const text = domConverter.getCorrespondingViewText( mutation.target );
  127. if ( text && !mutatedElements.has( text.parent ) ) {
  128. // Use text as a key, for deduplication. If there will be another mutation on the same text element
  129. // we will have only one in the map.
  130. mutatedTexts.set( text, {
  131. type: 'text',
  132. oldText: text.data,
  133. newText: getDataWithoutFiller( mutation.target ),
  134. node: text
  135. } );
  136. }
  137. // When we added first letter to the text node which had only inline filler, for the DOM it is mutation
  138. // on text, but for the view, where filler text node did not existed, new text node was created, so we
  139. // need to fire 'children' mutation instead of 'text'.
  140. else if ( !text && startsWithFiller( mutation.target ) ) {
  141. mutatedElements.add( domConverter.getCorrespondingViewElement( mutation.target.parentNode ) );
  142. }
  143. }
  144. }
  145. // Now we build the list of mutations to fire and mark elements. We did not do it earlier to avoid marking the
  146. // same node multiple times in case of duplication.
  147. // List of mutations we will fire.
  148. const viewMutations = [];
  149. for ( let mutatedText of mutatedTexts.values() ) {
  150. this.renderer.markToSync( 'text', mutatedText.node );
  151. viewMutations.push( mutatedText );
  152. }
  153. for ( let viewElement of mutatedElements ) {
  154. const domElement = domConverter.getCorrespondingDomElement( viewElement );
  155. const viewChildren = viewElement.getChildren();
  156. const newViewChildren = domConverter.domChildrenToView( domElement );
  157. this.renderer.markToSync( 'children', viewElement );
  158. viewMutations.push( {
  159. type: 'children',
  160. oldChildren: Array.from( viewChildren ),
  161. newChildren: Array.from( newViewChildren ),
  162. node: viewElement
  163. } );
  164. }
  165. this.document.fire( 'mutations', viewMutations );
  166. this.document.render();
  167. }
  168. }
  169. /**
  170. * Fired when mutation occurred. If tree view is not changed on this event, DOM will be reverter to the state before
  171. * mutation, so all changes which should be applied, should be handled on this event.
  172. *
  173. * Introduced by {@link engine.view.observer.MutationObserver}.
  174. *
  175. * Note that because {@link engine.view.observer.MutationObserver} is attached by the {@link engine.view.Document}
  176. * this event is available by default.
  177. *
  178. * @see engine.view.observer.MutationObserver
  179. * @event engine.view.Document#mutations
  180. * @param {Array.<engine.view.Document~MutatatedText|engine.view.Document~MutatatedChildren>} viewMutations
  181. * Array of mutations.
  182. * For mutated texts it will be {@link engine.view.Document~MutatatedText} and for mutated elements it will be
  183. * {@link engine.view.Document~MutatatedElement}. You can recognize the type based on the `type` property.
  184. */
  185. /**
  186. * Mutation item for text.
  187. *
  188. * @see engine.view.Document#mutations
  189. * @see engine.view.MutatatedChildren
  190. *
  191. * @typedef {Object} engine.view.MutatatedText
  192. *
  193. * @property {String} type For text mutations it is always 'text'.
  194. * @property {engine.view.Text} node Mutated text node.
  195. * @property {String} oldText Old text.
  196. * @property {String} newText New text.
  197. */
  198. /**
  199. * Mutation item for child nodes.
  200. *
  201. * @see engine.view.Document#mutations
  202. * @see engine.view.MutatatedText
  203. *
  204. * @typedef {Object} engine.view.MutatatedChildren
  205. *
  206. * @property {String} type For child nodes mutations it is always 'children'.
  207. * @property {engine.view.Element} node Parent of the mutated children.
  208. * @property {Array.<engine.view.Node>} oldChildren Old child nodes.
  209. * @property {Array.<engine.view.Node>} newChildren New child nodes.
  210. */