8
0

mapper.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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 ModelPosition from '../treemodel/position.js';
  7. import ViewPosition from '../treeview/position.js';
  8. import ViewText from '../treeview/text.js';
  9. /**
  10. * Maps elements and positions between {@link core.treeView.TreeView TreeView} and {@link core.treeModel TreeModel}.
  11. *
  12. * Mapper use binded elements to find corresponding elements and positions, so, to get proper results,
  13. * all Tree Model elements should be {@link core.treeController.Mapper#bindElements binded}.
  14. *
  15. * @memberOf core.treeController
  16. */
  17. export default class Mapper {
  18. /**
  19. * Creates an instance of the mapper.
  20. */
  21. constructor() {
  22. /**
  23. * Model element to View element mapping.
  24. *
  25. * @private
  26. * @member {WeakMap} core.treeController.Mapper#_modelToViewMapping
  27. */
  28. this._modelToViewMapping = new WeakMap();
  29. /**
  30. * View element to Model element mapping.
  31. *
  32. * @private
  33. * @member {WeakMap} core.treeController.Mapper#_viewToModelMapping
  34. */
  35. this._viewToModelMapping = new WeakMap();
  36. }
  37. /**
  38. * Marks model and view elements as corresponding. Corresponding elements can be retrieved by using
  39. * the {@link core.treeController.Mapper#toModelElement toModelElement} and {@link core.treeController.Mapper#toViewElement toViewElement}
  40. * methods. The information that elements are bound is also used to translate positions.
  41. *
  42. * @param {core.treeModel.Element} modelElement Model element.
  43. * @param {core.treeView.Element} viewElement View element.
  44. */
  45. bindElements( modelElement, viewElement ) {
  46. this._modelToViewMapping.set( modelElement, viewElement );
  47. this._viewToModelMapping.set( viewElement, modelElement );
  48. }
  49. /**
  50. * Gets the corresponding model element.
  51. *
  52. * @param {core.treeView.Element} viewElement View element.
  53. * @returns {core.treeModel.Element|null} Corresponding model element or `null` if not found.
  54. */
  55. toModelElement( viewElement ) {
  56. return this._viewToModelMapping.get( viewElement );
  57. }
  58. /**
  59. * Gets the corresponding view element.
  60. *
  61. * @param {core.treeModel.Element} modelElement Model element.
  62. * @returns {core.treeView.Element|null} Corresponding view element or `null` if not found.
  63. */
  64. toViewElement( modelElement ) {
  65. return this._modelToViewMapping.get( modelElement );
  66. }
  67. /**
  68. * Gets the corresponding model position.
  69. *
  70. * @param {core.treeView.Position} viewPosition View position.
  71. * @returns {core.treeModel.Position} Corresponding model position.
  72. */
  73. toModelPosition( viewPosition ) {
  74. let viewBlock = viewPosition.parent;
  75. let modelParent = this._viewToModelMapping.get( viewBlock );
  76. while ( !modelParent ) {
  77. viewBlock = viewBlock.parent;
  78. modelParent = this._viewToModelMapping.get( viewBlock );
  79. }
  80. let modelOffset = this._toModelOffset( viewPosition.parent, viewPosition.offset, viewBlock );
  81. return ModelPosition.createFromParentAndOffset( modelParent, modelOffset );
  82. }
  83. /**
  84. * Gets the corresponding view position.
  85. *
  86. * @param {core.treeModel.Position} modelPosition Model position.
  87. * @returns {core.treeView.Position} Corresponding view position.
  88. */
  89. toViewPosition( modelPosition ) {
  90. let viewContainer = this._modelToViewMapping.get( modelPosition.parent );
  91. return this._findPositionIn( viewContainer, modelPosition.offset );
  92. }
  93. /**
  94. * Calculates model offset based on the view position and the block element.
  95. *
  96. * Example:
  97. *
  98. * <p>foo<b>ba|r</b></p> // _toModelOffset( b, 2, p ) -> 5
  99. *
  100. * Is a sum of:
  101. *
  102. * <p>foo|<b>bar</b></p> // _toModelOffset( p, 3, p ) -> 3
  103. * <p>foo<b>ba|r</b></p> // _toModelOffset( b, 2, b ) -> 2
  104. *
  105. * @private
  106. * @param {core.treeView.Element} viewParent Position parent.
  107. * @param {Number} viewOffset Position offset.
  108. * @param {core.treeView.Element} viewBlock Block used as a base to calculate offset.
  109. * @returns {Number} Offset in the model.
  110. */
  111. _toModelOffset( viewParent, viewOffset, viewBlock ) {
  112. if ( viewBlock != viewParent ) {
  113. // See example.
  114. const offsetToParentBegging = this._toModelOffset( viewParent.parent, viewParent.getIndex(), viewBlock );
  115. const offsetInParent = this._toModelOffset( viewParent, viewOffset, viewParent );
  116. return offsetToParentBegging + offsetInParent;
  117. }
  118. // viewBlock == viewParent, so we need to calculate the offset in the parent element.
  119. // If the position is a text it is simple ("ba|r" -> 2).
  120. if ( viewParent instanceof ViewText ) {
  121. return viewOffset;
  122. }
  123. // If the position is in an element we need to sum lengths of siblings ( <b> bar </b> foo | -> 3 + 3 = 6 ).
  124. let modelOffset = 0;
  125. for ( let i = 0; i < viewOffset; i++ ) {
  126. modelOffset += this._getModelLength( viewParent.getChild( i ) );
  127. }
  128. return modelOffset;
  129. }
  130. /**
  131. * Gets the length of the view element in the model.
  132. *
  133. * Examples:
  134. *
  135. * foo -> 3 // Length of the text is the length of data.
  136. * <b>foo</b> -> 3 // Length the element which has no corresponding model element is a length of its children.
  137. * <p>foo</p> -> 1 // Length the element which has corresponding model element is always 1.
  138. *
  139. * @private
  140. * @param {core.treeView.Element} viewNode View node.
  141. * @returns {Number} Length of the node in the tree model.
  142. */
  143. _getModelLength( viewNode ) {
  144. // If we need mapping to be more flexible this method may fire event, so every feature may define the relation
  145. // between length in the model to the length in the view, for example if one element in the model creates two
  146. // elements in the view. Now I can not find any example where such feature would be useful.
  147. if ( this._viewToModelMapping.has( viewNode ) ) {
  148. return 1;
  149. } else if ( viewNode instanceof ViewText ) {
  150. return viewNode.data.length;
  151. } else {
  152. let len = 0;
  153. for ( let child of viewNode.getChildren() ) {
  154. len += this._getModelLength( child );
  155. }
  156. return len;
  157. }
  158. }
  159. /**
  160. * Finds the position in the view node (or its children) with the expected model offset.
  161. *
  162. * Example:
  163. *
  164. * <p>fo<b>bar</b>bom</p> -> expected offset: 4
  165. *
  166. * _findPositionIn( p, 4 ):
  167. * <p>|fo<b>bar</b>bom</p> -> expected offset: 4, actual offset: 0
  168. * <p>fo|<b>bar</b>bom</p> -> expected offset: 4, actual offset: 2
  169. * <p>fo<b>bar</b>|bom</p> -> expected offset: 4, actual offset: 5 -> we are too far
  170. *
  171. * _findPositionIn( b, 4 - ( 5 - 3 ) ):
  172. * <p>fo<b>|bar</b>bom</p> -> expected offset: 2, actual offset: 0
  173. * <p>fo<b>bar|</b>bom</p> -> expected offset: 2, actual offset: 3 -> we are too far
  174. *
  175. * _findPositionIn( bar, 2 - ( 3 - 3 ) ):
  176. * We are in the text node so we can simple find the offset.
  177. * <p>fo<b>ba|r</b>bom</p> -> expected offset: 2, actual offset: 2 -> position found
  178. *
  179. * @private
  180. * @param {core.treeView.Element} viewParent Tree view element in which we are looking for the position.
  181. * @param {Number} expectedOffset Expected offset.
  182. * @returns {core.treeView.Position} Found position.
  183. */
  184. _findPositionIn( viewParent, expectedOffset ) {
  185. // Last scanned view node.
  186. let viewNode;
  187. // Length of the last scanned view node.
  188. let lastLength = 0;
  189. let modelOffset = 0;
  190. let viewOffset = 0;
  191. // In the text node it is simple: offset in the model equals offset in the text.
  192. if ( viewParent instanceof ViewText ) {
  193. return new ViewPosition( viewParent, expectedOffset );
  194. }
  195. // In other cases we add lengths of child nodes to find the proper offset.
  196. // If it is smaller we add the length.
  197. while ( modelOffset < expectedOffset ) {
  198. viewNode = viewParent.getChild( viewOffset );
  199. lastLength = this._getModelLength( viewNode );
  200. modelOffset += lastLength;
  201. viewOffset++;
  202. }
  203. // If it equals we found the position.
  204. if ( modelOffset == expectedOffset ) {
  205. return new ViewPosition( viewParent, viewOffset );
  206. }
  207. // If it is higher we need to enter last child.
  208. else {
  209. // ( modelOffset - lastLength ) is the offset to the child we enter,
  210. // so we subtract it from the expected offset to fine the offset in the child.
  211. return this._findPositionIn( viewNode, expectedOffset - ( modelOffset - lastLength ) );
  212. }
  213. }
  214. }