liveposition.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/liveposition
  7. */
  8. import Position from './position';
  9. import Range from './range';
  10. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  11. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  12. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  13. /**
  14. * `LivePosition` is a type of {@link module:engine/model/position~Position Position}
  15. * that updates itself as {@link module:engine/model/document~Document document}
  16. * is changed through operations. It may be used as a bookmark.
  17. *
  18. * **Note:** Contrary to {@link module:engine/model/position~Position}, `LivePosition` works only in roots that are
  19. * {@link module:engine/model/rootelement~RootElement}.
  20. * If {@link module:engine/model/documentfragment~DocumentFragment} is passed, error will be thrown.
  21. *
  22. * **Note:** Be very careful when dealing with `LivePosition`. Each `LivePosition` instance bind events that might
  23. * have to be unbound.
  24. * Use {@link module:engine/model/liveposition~LivePosition#detach} whenever you don't need `LivePosition` anymore.
  25. *
  26. * @extends module:engine/model/position~Position
  27. */
  28. export default class LivePosition extends Position {
  29. /**
  30. * Creates a live position.
  31. *
  32. * @see module:engine/model/position~Position
  33. * @param {module:engine/model/rootelement~RootElement} root
  34. * @param {Array.<Number>} path
  35. * @param {module:engine/model/position~PositionStickiness} [stickiness] Defaults to `'sticksToNext'`.
  36. * See {@link module:engine/model/liveposition~LivePosition#stickiness}.
  37. */
  38. constructor( root, path, stickiness ) {
  39. super( root, path );
  40. if ( !this.root.is( 'rootElement' ) ) {
  41. /**
  42. * LivePosition's root has to be an instance of RootElement.
  43. *
  44. * @error liveposition-root-not-rootelement
  45. */
  46. throw new CKEditorError(
  47. 'model-liveposition-root-not-rootelement: LivePosition\'s root has to be an instance of RootElement.'
  48. );
  49. }
  50. /**
  51. * Flag representing `LivePosition` stickiness. `LivePosition` might be sticking to previous node or next node.
  52. * Whenever some nodes are inserted at the same position as `LivePosition`, `stickiness` is checked to decide if
  53. * LivePosition should be moved. Similar applies when a range of nodes is moved and one of it's boundary
  54. * position is same as `LivePosition`.
  55. *
  56. * Examples:
  57. *
  58. * Insert:
  59. * Position is at | and we insert at the same position, marked as ^:
  60. * - | sticks to previous node: `<p>f|^oo</p>` => `<p>f|baroo</p>`
  61. * - | sticks to next node: `<p>f^|oo</p>` => `<p>fbar|oo</p>`
  62. *
  63. * Move:
  64. * Position is at | and range [ ] is moved to position ^:
  65. * - | sticks to previous node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f|</p><p>booar</p>`
  66. * - | sticks to next node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f</p><p>b|ooar</p>`
  67. *
  68. * @member {module:engine/model/position~PositionStickiness} module:engine/model/liveposition~LivePosition#stickiness
  69. */
  70. this.stickiness = stickiness || 'sticksToNext';
  71. bindWithDocument.call( this );
  72. }
  73. /**
  74. * Unbinds all events previously bound by `LivePosition`. Use it whenever you don't need `LivePosition` instance
  75. * anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was
  76. * referring to it).
  77. */
  78. detach() {
  79. this.stopListening();
  80. }
  81. /**
  82. * @static
  83. * @method module:engine/model/liveposition~LivePosition.createAfter
  84. * @see module:engine/model/position~Position.createAfter
  85. * @param {module:engine/model/node~Node} node
  86. * @returns {module:engine/model/liveposition~LivePosition}
  87. */
  88. /**
  89. * @static
  90. * @method module:engine/model/liveposition~LivePosition.createBefore
  91. * @see module:engine/model/position~Position.createBefore
  92. * @param {module:engine/model/node~Node} node
  93. * @returns {module:engine/model/liveposition~LivePosition}
  94. */
  95. /**
  96. * @static
  97. * @method module:engine/model/liveposition~LivePosition.createFromParentAndOffset
  98. * @see module:engine/model/position~Position.createFromParentAndOffset
  99. * @param {module:engine/model/element~Element} parent
  100. * @param {Number} offset
  101. * @returns {module:engine/model/liveposition~LivePosition}
  102. */
  103. /**
  104. * @static
  105. * @method module:engine/model/liveposition~LivePosition.createFromPosition
  106. * @see module:engine/model/position~Position.createFromPosition
  107. * @param {module:engine/model/position~Position} position
  108. * @returns {module:engine/model/liveposition~LivePosition}
  109. */
  110. /**
  111. * Fired when `LivePosition` instance is changed due to changes on {@link module:engine/model/document~Document}.
  112. *
  113. * @event module:engine/model/liveposition~LivePosition#change
  114. * @param {module:engine/model/position~Position} oldPosition Position equal to this live position before it got changed.
  115. */
  116. }
  117. /**
  118. * Binds this `LivePosition` to the {@link module:engine/model/document~Document document} that owns
  119. * this position's {@link module:engine/model/position~Position#root root}.
  120. *
  121. * @ignore
  122. * @private
  123. * @method module:engine/model/liveposition~LivePosition.bindWithDocument
  124. */
  125. function bindWithDocument() {
  126. // Operation types handled by LivePosition (these are operations that change model tree structure).
  127. const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] );
  128. this.listenTo(
  129. this.root.document.model,
  130. 'applyOperation',
  131. ( event, args ) => {
  132. const operation = args[ 0 ];
  133. if ( !operation.isDocumentOperation ) {
  134. return;
  135. }
  136. if ( supportedTypes.has( operation.type ) ) {
  137. transform.call( this, operation );
  138. }
  139. },
  140. { priority: 'low' }
  141. );
  142. }
  143. /**
  144. * Updates this position accordingly to the updates applied to the model. Bases on change events.
  145. *
  146. * @ignore
  147. * @private
  148. * @method transform
  149. * @param {module:engine/model/operation/operation~Operation} operation Executed operation.
  150. */
  151. function transform( operation ) {
  152. /* eslint-disable no-case-declarations */
  153. let range;
  154. let position;
  155. if ( operation.type == 'insert' ) {
  156. range = Range.createFromPositionAndShift( operation.position, operation.nodes.maxOffset );
  157. } else {
  158. range = Range.createFromPositionAndShift( operation.getMovedRangeStart(), operation.howMany );
  159. position = operation.sourcePosition;
  160. }
  161. const howMany = range.end.offset - range.start.offset;
  162. let transformed;
  163. switch ( operation.type ) {
  164. case 'insert':
  165. const insertBefore = this.stickiness == 'sticksToNext';
  166. transformed = this._getTransformedByInsertion( range.start, howMany, insertBefore );
  167. break;
  168. case 'move':
  169. case 'remove':
  170. case 'reinsert':
  171. const originalRange = Range.createFromPositionAndShift( position, howMany );
  172. const gotMoved = originalRange.containsPosition( this ) ||
  173. ( originalRange.start.isEqual( this ) && this.stickiness == 'sticksToNext' ) ||
  174. ( originalRange.end.isEqual( this ) && this.stickiness == 'sticksToPrevious' );
  175. // We can't use ._getTransformedByMove() because we have a different if-condition.
  176. if ( gotMoved ) {
  177. transformed = this._getCombined( position, range.start );
  178. } else {
  179. const insertBefore = this.stickiness == 'sticksToNext';
  180. // `Position._getTransformedByMove` is expecting `targetPosition` to be "before" move
  181. // (before transformation). `range.start` is already after the move happened.
  182. // We have to revert `targetPosition` to the state before the move.
  183. const targetPosition = range.start._getTransformedByInsertion( position, howMany );
  184. transformed = this._getTransformedByMove( position, targetPosition, howMany, insertBefore );
  185. }
  186. break;
  187. }
  188. if ( !this.isEqual( transformed ) ) {
  189. const oldPosition = Position.createFromPosition( this );
  190. this.path = transformed.path;
  191. this.root = transformed.root;
  192. this.fire( 'change', oldPosition );
  193. }
  194. /* eslint-enable no-case-declarations */
  195. }
  196. mix( LivePosition, EmitterMixin );
  197. /**
  198. * Enum representing how position is "sticking" with their neighbour nodes.
  199. * Possible values: `'sticksToNext'`, `'sticksToPrevious'`.
  200. *
  201. * @typedef {String} module:engine/model/position~PositionStickiness
  202. */