8
0

renderer.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import diff from '../utils-diff.js';
  7. import CKEditorError from '../ckeditorerror.js';
  8. /**
  9. * Renderer updates DOM tree, to make it a reflection of the view tree. Changed nodes need to be
  10. * {@link core.treeView.Renderer#markToSync marked} to be rendered. Then, on {@link core.treeView.Renderer#render render}, renderer
  11. * ensure they need to be refreshed and creates DOM nodes from view nodes,
  12. * {@link core.treeView.Converter#bindElements bind} them and insert into DOM tree. Renderer use {@link core.treeView.Converter}
  13. * to transform and bind nodes.
  14. *
  15. * @memberOf core.treeView
  16. */
  17. export default class Renderer {
  18. /**
  19. * Creates a renderer instance.
  20. *
  21. * @param {core.treeView.Converter} converter Converter instance.
  22. */
  23. constructor( converter ) {
  24. /**
  25. * Converter instance.
  26. *
  27. * @readonly
  28. * @member {core.treeView.Converter} core.treeView.Renderer#converter
  29. */
  30. this.converter = converter;
  31. /**
  32. * Set of nodes which attributes changed and may need to be rendered.
  33. *
  34. * @readonly
  35. * @member {Set.<core.treeView.Node>} core.treeView.Renderer#markedAttributes
  36. */
  37. this.markedAttributes = new Set();
  38. /**
  39. * Set of elements which child lists changed and may need to be rendered.
  40. *
  41. * @readonly
  42. * @member {Set.<core.treeView.Node>} core.treeView.Renderer#markedChildren
  43. */
  44. this.markedChildren = new Set();
  45. /**
  46. * Set of text nodes which text data changed and may need to be rendered.
  47. *
  48. * @readonly
  49. * @member {Set.<core.treeView.Node>} core.treeView.Renderer#markedTexts
  50. */
  51. this.markedTexts = new Set();
  52. }
  53. /**
  54. * Mark node to be synchronized.
  55. *
  56. * Note that only view nodes which parents have corresponding DOM elements need to be marked to be synchronized.
  57. *
  58. * @see core.treeView.Renderer#markedAttributes
  59. * @see core.treeView.Renderer#markedChildren
  60. * @see core.treeView.Renderer#markedTexts
  61. *
  62. * @param {core.treeView.ChangeType} type Type of the change.
  63. * @param {core.treeView.Node} node Node to be marked.
  64. */
  65. markToSync( type, node ) {
  66. if ( type === 'TEXT' ) {
  67. if ( this.converter.getCorrespondingDom( node.parent ) ) {
  68. this.markedTexts.add( node );
  69. }
  70. } else {
  71. // If the node has no DOM element it is not rendered yet,
  72. // its children/attributes do not need to be marked to be sync.
  73. if ( !this.converter.getCorrespondingDom( node ) ) {
  74. return;
  75. }
  76. if ( type === 'ATTRIBUTES' ) {
  77. this.markedAttributes.add( node );
  78. } else if ( type === 'CHILDREN' ) {
  79. this.markedChildren.add( node );
  80. } else {
  81. /**
  82. * Unknown type passed to Renderer.markToSync.
  83. *
  84. * @error renderer-unknown-type
  85. */
  86. throw new CKEditorError( 'renderer-unknown-type: Unknown type passed to Renderer.markToSync.' );
  87. }
  88. }
  89. }
  90. /**
  91. * Render method check {@link core.treeView.Renderer#markedAttributes}, {@link core.treeView.Renderer#markedChildren} and
  92. * {@link core.treeView.Renderer#markedTexts} and updated all nodes which needs to be updated. Then it clear all three
  93. * sets.
  94. *
  95. * Renderer try not to bread IME, so it do as little as it is possible to update DOM.
  96. *
  97. * For attributes it adds new attributes to DOM elements, update attributes with different values and remove
  98. * attributes which does not exists in the view element.
  99. *
  100. * For text nodes it update the text string if it is different. Note that if parent element is marked as an element
  101. * which changed child list, text node update will not be done, because it may not be possible do find a
  102. * {@link core.treeView.Converter#getCorrespondingDomText corresponding DOM text}. The change will be handled in the
  103. * parent element.
  104. *
  105. * For nodes which changed child list it calculates a {@link diff} using {@link core.treeView.Converter#compareNodes}
  106. * and add or removed nodes which changed.
  107. */
  108. render() {
  109. const converter = this.converter;
  110. for ( let node of this.markedTexts ) {
  111. if ( !this.markedChildren.has( node.parent ) && converter.getCorrespondingDom( node.parent ) ) {
  112. updateText( node );
  113. }
  114. }
  115. for ( let element of this.markedAttributes ) {
  116. updateAttrs( element );
  117. }
  118. for ( let element of this.markedChildren ) {
  119. updateChildren( element );
  120. }
  121. this.markedTexts.clear();
  122. this.markedAttributes.clear();
  123. this.markedChildren.clear();
  124. function updateText( viewText ) {
  125. const domText = converter.getCorrespondingDom( viewText );
  126. if ( domText.data != viewText.data ) {
  127. domText.data = viewText.data;
  128. }
  129. }
  130. function updateAttrs( viewElement ) {
  131. const domElement = converter.getCorrespondingDom( viewElement );
  132. const domAttrKeys = Array.from( domElement.attributes ).map( attr => attr.name );
  133. const viewAttrKeys = viewElement.getAttributeKeys();
  134. // Add or overwrite attributes.
  135. for ( let key of viewAttrKeys ) {
  136. domElement.setAttribute( key, viewElement.getAttribute( key ) );
  137. }
  138. // Remove from DOM attributes which do not exists in the view.
  139. for ( let key of domAttrKeys ) {
  140. if ( !viewElement.hasAttribute( key ) ) {
  141. domElement.removeAttribute( key );
  142. }
  143. }
  144. }
  145. function updateChildren( viewElement ) {
  146. const domElement = converter.getCorrespondingDom( viewElement );
  147. const domChildren = domElement.childNodes;
  148. const viewChildren = Array.from( viewElement.getChildren() );
  149. const domDocument = domElement.ownerDocument;
  150. const actions = diff( domChildren, viewChildren,
  151. ( domNode, viewNode ) => converter.compareNodes( domNode, viewNode ) );
  152. let i = 0;
  153. for ( let action of actions ) {
  154. if ( action === 'INSERT' ) {
  155. let domChildToInsert = converter.viewToDom( viewChildren[ i ], domDocument, { bind: true } );
  156. domElement.insertBefore( domChildToInsert, domChildren[ i ] || null );
  157. i++;
  158. } else if ( action === 'DELETE' ) {
  159. domElement.removeChild( domChildren[ i ] );
  160. } else { // 'EQUAL'
  161. i++;
  162. }
  163. }
  164. }
  165. }
  166. }