liverange.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module engine/model/liverange
  7. */
  8. import Range from './range';
  9. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  10. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  11. /**
  12. * `LiveRange` is a type of {@link module:engine/model/range~Range Range}
  13. * that updates itself as {@link module:engine/model/document~Document document}
  14. * is changed through operations. It may be used as a bookmark.
  15. *
  16. * **Note:** Be very careful when dealing with `LiveRange`. Each `LiveRange` instance bind events that might
  17. * have to be unbound. Use {@link module:engine/model/liverange~LiveRange#detach detach} whenever you don't need `LiveRange` anymore.
  18. */
  19. export default class LiveRange extends Range {
  20. /**
  21. * Creates a live range.
  22. *
  23. * @see module:engine/model/range~Range
  24. */
  25. constructor( start, end ) {
  26. super( start, end );
  27. bindWithDocument.call( this );
  28. }
  29. /**
  30. * Unbinds all events previously bound by `LiveRange`. Use it whenever you don't need `LiveRange` instance
  31. * anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was
  32. * referring to it).
  33. */
  34. detach() {
  35. this.stopListening();
  36. }
  37. /**
  38. * Checks whether this object is of the given.
  39. *
  40. * liveRange.is( 'range' ); // -> true
  41. * liveRange.is( 'model:range' ); // -> true
  42. * liveRange.is( 'liveRange' ); // -> true
  43. * liveRange.is( 'model:liveRange' ); // -> true
  44. *
  45. * liveRange.is( 'view:range' ); // -> false
  46. * liveRange.is( 'documentSelection' ); // -> false
  47. *
  48. * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
  49. *
  50. * @param {String} type
  51. * @returns {Boolean}
  52. */
  53. is( type ) {
  54. return type === 'liveRange' || type === 'model:liveRange' ||
  55. // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
  56. type == 'range' || type === 'model:range';
  57. }
  58. /**
  59. * Creates a {@link module:engine/model/range~Range range instance} that is equal to this live range.
  60. *
  61. * @returns {module:engine/model/range~Range}
  62. */
  63. toRange() {
  64. return new Range( this.start, this.end );
  65. }
  66. /**
  67. * Creates a `LiveRange` instance that is equal to the given range.
  68. *
  69. * @param {module:engine/model/range~Range} range
  70. * @returns {module:engine/model/liverange~LiveRange}
  71. */
  72. static fromRange( range ) {
  73. return new LiveRange( range.start, range.end );
  74. }
  75. /**
  76. * @see module:engine/model/range~Range._createIn
  77. * @static
  78. * @protected
  79. * @method module:engine/model/liverange~LiveRange._createIn
  80. * @param {module:engine/model/element~Element} element
  81. * @returns {module:engine/model/liverange~LiveRange}
  82. */
  83. /**
  84. * @see module:engine/model/range~Range._createOn
  85. * @static
  86. * @protected
  87. * @method module:engine/model/liverange~LiveRange._createOn
  88. * @param {module:engine/model/element~Element} element
  89. * @returns {module:engine/model/liverange~LiveRange}
  90. */
  91. /**
  92. * @see module:engine/model/range~Range._createFromPositionAndShift
  93. * @static
  94. * @protected
  95. * @method module:engine/model/liverange~LiveRange._createFromPositionAndShift
  96. * @param {module:engine/model/position~Position} position
  97. * @param {Number} shift
  98. * @returns {module:engine/model/liverange~LiveRange}
  99. */
  100. /**
  101. * Fired when `LiveRange` instance boundaries have changed due to changes in the
  102. * {@link module:engine/model/document~Document document}.
  103. *
  104. * @event change:range
  105. * @param {module:engine/model/range~Range} oldRange Range with start and end position equal to start and end position of this live
  106. * range before it got changed.
  107. * @param {Object} data Object with additional information about the change.
  108. * @param {module:engine/model/position~Position|null} data.deletionPosition Source position for remove and merge changes.
  109. * Available if the range was moved to the graveyard root, `null` otherwise.
  110. */
  111. /**
  112. * Fired when `LiveRange` instance boundaries have not changed after a change in {@link module:engine/model/document~Document document}
  113. * but the change took place inside the range, effectively changing its content.
  114. *
  115. * @event change:content
  116. * @param {module:engine/model/range~Range} range Range with start and end position equal to start and end position of
  117. * change range.
  118. * @param {Object} data Object with additional information about the change.
  119. * @param {null} data.deletionPosition Due to the nature of this event, this property is always set to `null`. It is passed
  120. * for compatibility with the {@link module:engine/model/liverange~LiveRange#event:change:range} event.
  121. */
  122. }
  123. // Binds this `LiveRange` to the {@link module:engine/model/document~Document document}
  124. // that owns this range's {@link module:engine/model/range~Range#root root}.
  125. //
  126. // @private
  127. function bindWithDocument() {
  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. transform.call( this, operation );
  137. },
  138. { priority: 'low' }
  139. );
  140. }
  141. // Updates this range accordingly to the updates applied to the model. Bases on change events.
  142. //
  143. // @private
  144. // @param {module:engine/model/operation/operation~Operation} operation Executed operation.
  145. function transform( operation ) {
  146. // Transform the range by the operation. Join the result ranges if needed.
  147. const ranges = this.getTransformedByOperation( operation );
  148. const result = Range._createFromRanges( ranges );
  149. const boundariesChanged = !result.isEqual( this );
  150. const contentChanged = doesOperationChangeRangeContent( this, operation );
  151. let deletionPosition = null;
  152. if ( boundariesChanged ) {
  153. // If range boundaries have changed, fire `change:range` event.
  154. //
  155. if ( result.root.rootName == '$graveyard' ) {
  156. // If the range was moved to the graveyard root, set `deletionPosition`.
  157. if ( operation.type == 'remove' ) {
  158. deletionPosition = operation.sourcePosition;
  159. } else {
  160. // Merge operation.
  161. deletionPosition = operation.deletionPosition;
  162. }
  163. }
  164. const oldRange = this.toRange();
  165. this.start = result.start;
  166. this.end = result.end;
  167. this.fire( 'change:range', oldRange, { deletionPosition } );
  168. } else if ( contentChanged ) {
  169. // If range boundaries have not changed, but there was change inside the range, fire `change:content` event.
  170. this.fire( 'change:content', this.toRange(), { deletionPosition } );
  171. }
  172. }
  173. // Checks whether given operation changes something inside the range (even if it does not change boundaries).
  174. //
  175. // @private
  176. // @param {module:engine/model/range~Range} range Range to check.
  177. // @param {module:engine/model/operation/operation~Operation} operation Executed operation.
  178. // @returns {Boolean}
  179. function doesOperationChangeRangeContent( range, operation ) {
  180. switch ( operation.type ) {
  181. case 'insert':
  182. return range.containsPosition( operation.position );
  183. case 'move':
  184. case 'remove':
  185. case 'reinsert':
  186. case 'merge':
  187. return range.containsPosition( operation.sourcePosition ) ||
  188. range.start.isEqual( operation.sourcePosition ) ||
  189. range.containsPosition( operation.targetPosition );
  190. case 'split':
  191. return range.containsPosition( operation.splitPosition ) || range.containsPosition( operation.insertionPosition );
  192. }
  193. return false;
  194. }
  195. mix( LiveRange, EmitterMixin );