mapper.js 8.7 KB

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