8
0

liveposition.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. /**
  2. * @license Copyright (c) 2003-2017, 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,
  130. 'change',
  131. ( event, type, changes ) => {
  132. if ( supportedTypes.has( type ) ) {
  133. transform.call( this, type, changes.range, changes.sourcePosition );
  134. }
  135. },
  136. { priority: 'high' }
  137. );
  138. }
  139. /**
  140. * Updates this position accordingly to the updates applied to the model. Bases on change events.
  141. *
  142. * @ignore
  143. * @private
  144. * @method transform
  145. * @param {String} type Type of changes applied to the Tree Model.
  146. * @param {module:engine/model/range~Range} range Range containing the result of applied change.
  147. * @param {module:engine/model/position~Position} [position] Additional position parameter provided by some change events.
  148. */
  149. function transform( type, range, position ) {
  150. /* eslint-disable no-case-declarations */
  151. const howMany = range.end.offset - range.start.offset;
  152. let transformed;
  153. switch ( type ) {
  154. case 'insert':
  155. const insertBefore = this.stickiness == 'sticksToNext';
  156. transformed = this._getTransformedByInsertion( range.start, howMany, insertBefore );
  157. break;
  158. case 'move':
  159. case 'remove':
  160. case 'reinsert':
  161. const originalRange = Range.createFromPositionAndShift( position, howMany );
  162. const gotMoved = originalRange.containsPosition( this ) ||
  163. ( originalRange.start.isEqual( this ) && this.stickiness == 'sticksToNext' ) ||
  164. ( originalRange.end.isEqual( this ) && this.stickiness == 'sticksToPrevious' );
  165. // We can't use ._getTransformedByMove() because we have a different if-condition.
  166. if ( gotMoved ) {
  167. transformed = this._getCombined( position, range.start );
  168. } else {
  169. const insertBefore = this.stickiness == 'sticksToNext';
  170. transformed = this._getTransformedByMove( position, range.start, howMany, insertBefore );
  171. }
  172. break;
  173. }
  174. if ( !this.isEqual( transformed ) ) {
  175. const oldPosition = Position.createFromPosition( this );
  176. this.path = transformed.path;
  177. this.root = transformed.root;
  178. this.fire( 'change', oldPosition );
  179. }
  180. /* eslint-enable no-case-declarations */
  181. }
  182. mix( LivePosition, EmitterMixin );
  183. /**
  184. * Enum representing how position is "sticking" with their neighbour nodes.
  185. * Possible values: `'sticksToNext'`, `'sticksToPrevious'`.
  186. *
  187. * @typedef {String} module:engine/model/position~PositionStickiness
  188. */