8
0

position.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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 Text from './text.js';
  7. import compareArrays from '../../utils/comparearrays.js';
  8. import CKEditorError from '../../utils/ckeditorerror.js';
  9. /**
  10. * Position in the tree. Position is always located before or after a node.
  11. *
  12. * @memberOf engine.treeView
  13. */
  14. export default class Position {
  15. /**
  16. * Creates a position.
  17. *
  18. * @param {engine.treeView.Node} parent Position parent node.
  19. * @param {Number} offset Position offset.
  20. */
  21. constructor( parent, offset ) {
  22. /**
  23. * Position parent node.
  24. *
  25. * @member {engine.treeView.Node} engine.treeView.Position#parent
  26. */
  27. this.parent = parent;
  28. /**
  29. * Position offset.
  30. *
  31. * @member {Number} engine.treeView.Position#offset
  32. */
  33. this.offset = offset;
  34. }
  35. /**
  36. * Node directly after the position. Equals `null` when there is no node after position or position is located
  37. * inside text node.
  38. *
  39. * @readonly
  40. * @type {engine.treeView.Node|null}
  41. */
  42. get nodeAfter() {
  43. if ( this.parent instanceof Text ) {
  44. return null;
  45. }
  46. return this.parent.getChild( this.offset ) || null;
  47. }
  48. /**
  49. * Node directly before the position. Equals `null` when there is no node before position or position is located
  50. * inside text node.
  51. *
  52. * @readonly
  53. * @type {engine.treeView.Node|null}
  54. */
  55. get nodeBefore() {
  56. if ( this.parent instanceof Text ) {
  57. return null;
  58. }
  59. return this.parent.getChild( this.offset - 1 ) || null;
  60. }
  61. /**
  62. * Returns a new instance of Position with offset incremented by `shift` value.
  63. *
  64. * @param {Number} shift How position offset should get changed. Accepts negative values.
  65. * @returns {engine.treeView.Position} Shifted position.
  66. */
  67. getShiftedBy( shift ) {
  68. let shifted = Position.createFromPosition( this );
  69. let offset = shifted.offset + shift;
  70. shifted.offset = offset < 0 ? 0 : offset;
  71. return shifted;
  72. }
  73. /**
  74. * Checks whether this position equals given position.
  75. *
  76. * @param {engine.treeView.Position} otherPosition Position to compare with.
  77. * @returns {Boolean} True if positions are same.
  78. */
  79. isEqual( otherPosition ) {
  80. return this == otherPosition || ( this.parent == otherPosition.parent && this.offset == otherPosition.offset );
  81. }
  82. /**
  83. * Checks whether this position is located before given position. When method returns `false` it does not mean that
  84. * this position is after give one. Two positions may be located inside separate roots and in that situation this
  85. * method will still return `false`.
  86. *
  87. * @see engine.treeView.Position#isAfter
  88. * @see engine.treeView.Position#compareWith
  89. * @param {engine.treeView.Position} otherPosition Position to compare with.
  90. * @returns {Boolean} Returns `true` if this position is before given position.
  91. */
  92. isBefore( otherPosition ) {
  93. return this.compareWith( otherPosition ) == 'BEFORE';
  94. }
  95. /**
  96. * Checks whether this position is located after given position. When method returns `false` it does not mean that
  97. * this position is before give one. Two positions may be located inside separate roots and in that situation this
  98. * method will still return `false`.
  99. *
  100. * @see engine.treeView.Position#isBefore
  101. * @see engine.treeView.Position#compareWith
  102. * @param {engine.treeView.Position} otherPosition Position to compare with.
  103. * @returns {Boolean} Returns `true` if this position is after given position.
  104. */
  105. isAfter( otherPosition ) {
  106. return this.compareWith( otherPosition ) == 'AFTER';
  107. }
  108. /**
  109. * Checks whether this position is before, after or in same position that other position. Two positions may be also
  110. * different when they are located in separate roots.
  111. *
  112. * @param {engine.treeView.Position} otherPosition Position to compare with.
  113. * @returns {engine.treeView.PositionRelation}
  114. */
  115. compareWith( otherPosition ) {
  116. if ( this.isEqual( otherPosition ) ) {
  117. return 'SAME';
  118. }
  119. // If positions have same parent.
  120. if ( this.parent === otherPosition.parent ) {
  121. return this.offset - otherPosition.offset < 0 ? 'BEFORE' : 'AFTER';
  122. }
  123. // Get path from root to position's parent element.
  124. const path = this.parent.getAncestors( { includeNode: true } );
  125. const otherPath = otherPosition.parent.getAncestors( { includeNode: true } );
  126. // Compare both path arrays to find common ancestor.
  127. const result = compareArrays( path, otherPath );
  128. let commonAncestorIndex;
  129. switch ( result ) {
  130. case 0:
  131. // No common ancestors found.
  132. return 'DIFFERENT';
  133. case 'PREFIX':
  134. commonAncestorIndex = path.length - 1;
  135. break;
  136. case 'EXTENSION':
  137. commonAncestorIndex = otherPath.length - 1;
  138. break;
  139. default:
  140. commonAncestorIndex = result - 1;
  141. }
  142. // Common ancestor of two positions.
  143. const commonAncestor = path[ commonAncestorIndex ];
  144. const nextAncestor1 = path[ commonAncestorIndex + 1 ];
  145. const nextAncestor2 = otherPath[ commonAncestorIndex + 1 ];
  146. // Check if common ancestor is not one of the parents.
  147. if ( commonAncestor === this.parent ) {
  148. const index = this.offset - nextAncestor2.getIndex();
  149. return index <= 0 ? 'BEFORE' : 'AFTER';
  150. } else if ( commonAncestor === otherPosition.parent ) {
  151. const index = nextAncestor1.getIndex() - otherPosition.offset;
  152. return index < 0 ? 'BEFORE' : 'AFTER';
  153. }
  154. const index = nextAncestor1.getIndex() - nextAncestor2.getIndex();
  155. // Compare indexes of next ancestors inside common one.
  156. return index < 0 ? 'BEFORE' : 'AFTER';
  157. }
  158. /**
  159. * Creates a new position after given node.
  160. *
  161. * @param {engine.treeView.Node} node Node the position should be directly after.
  162. * @returns {engine.treeView.Position}
  163. */
  164. static createAfter( node ) {
  165. if ( !node.parent ) {
  166. /**
  167. * You can not make position after root.
  168. *
  169. * @error position-after-root
  170. * @param {engine.treeView.Node} root
  171. */
  172. throw new CKEditorError( 'position-after-root: You can not make position after root.', { root: node } );
  173. }
  174. return new Position( node.parent, node.getIndex() + 1 );
  175. }
  176. /**
  177. * Creates a new position before the given node.
  178. *
  179. * @param {engine.treeView.node} node Node the position should be directly before.
  180. * @returns {engine.treeView.Position}
  181. */
  182. static createBefore( node ) {
  183. if ( !node.parent ) {
  184. /**
  185. * You can not make position before root.
  186. *
  187. * @error position-before-root
  188. * @param {engine.treeView.Node} root
  189. */
  190. throw new CKEditorError( 'position-before-root: You can not make position before root.', { root: node } );
  191. }
  192. return new Position( node.parent, node.getIndex() );
  193. }
  194. /**
  195. * Creates and returns a new instance of Position, which is equal to passed position.
  196. *
  197. * @param {engine.treeView.Position} position Position to be cloned.
  198. * @returns {engine.treeView.Position}
  199. */
  200. static createFromPosition( position ) {
  201. return new this( position.parent, position.offset );
  202. }
  203. }
  204. /**
  205. * A flag indicating whether this position is `'BEFORE'` or `'AFTER'` or `'SAME'` as given position.
  206. * If positions are in different roots `'DIFFERENT'` flag is returned.
  207. *
  208. * @typedef {String} engine.treeView.PositionRelation
  209. */