liverange.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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/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. * Creates a {@link module:engine/model/range~Range range instance} that is equal to this live range.
  39. *
  40. * @returns {module:engine/model/range~Range}
  41. */
  42. toRange() {
  43. return new Range( this.start, this.end );
  44. }
  45. /**
  46. * Creates a `LiveRange` instance that is equal to the given range.
  47. *
  48. * @param {module:engine/model/range~Range} range
  49. * @returns {module:engine/model/liverange~LiveRange}
  50. */
  51. static fromRange( range ) {
  52. return new LiveRange( range.start, range.end );
  53. }
  54. /**
  55. * @see module:engine/model/range~Range._createIn
  56. * @static
  57. * @protected
  58. * @method module:engine/model/liverange~LiveRange._createIn
  59. * @param {module:engine/model/element~Element} element
  60. * @returns {module:engine/model/liverange~LiveRange}
  61. */
  62. /**
  63. * @see module:engine/model/range~Range._createOn
  64. * @static
  65. * @protected
  66. * @method module:engine/model/liverange~LiveRange._createOn
  67. * @param {module:engine/model/element~Element} element
  68. * @returns {module:engine/model/liverange~LiveRange}
  69. */
  70. /**
  71. * @see module:engine/model/range~Range._createFromPositionAndShift
  72. * @static
  73. * @protected
  74. * @method module:engine/model/liverange~LiveRange._createFromPositionAndShift
  75. * @param {module:engine/model/position~Position} position
  76. * @param {Number} shift
  77. * @returns {module:engine/model/liverange~LiveRange}
  78. */
  79. /**
  80. * Fired when `LiveRange` instance boundaries have changed due to changes in the
  81. * {@link module:engine/model/document~Document document}.
  82. *
  83. * @event change:range
  84. * @param {module:engine/model/range~Range} oldRange Range with start and end position equal to start and end position of this live
  85. * range before it got changed.
  86. * @param {Object} data Object with additional information about the change.
  87. * @param {module:engine/model/position~Position|null} data.deletionPosition Source position for remove and merge changes.
  88. * Available if the range was moved to the graveyard root, `null` otherwise.
  89. */
  90. /**
  91. * Fired when `LiveRange` instance boundaries have not changed after a change in {@link module:engine/model/document~Document document}
  92. * but the change took place inside the range, effectively changing its content.
  93. *
  94. * @event change:content
  95. * @param {module:engine/model/range~Range} range Range with start and end position equal to start and end position of
  96. * change range.
  97. * @param {Object} data Object with additional information about the change.
  98. * @param {null} data.deletionPosition Due to the nature of this event, this property is always set to `null`. It is passed
  99. * for compatibility with the {@link module:engine/model/liverange~LiveRange#event:change:range} event.
  100. */
  101. }
  102. // Binds this `LiveRange` to the {@link module:engine/model/document~Document document}
  103. // that owns this range's {@link module:engine/model/range~Range#root root}.
  104. //
  105. // @private
  106. function bindWithDocument() {
  107. this.listenTo(
  108. this.root.document.model,
  109. 'applyOperation',
  110. ( event, args ) => {
  111. const operation = args[ 0 ];
  112. if ( !operation.isDocumentOperation ) {
  113. return;
  114. }
  115. transform.call( this, operation );
  116. },
  117. { priority: 'low' }
  118. );
  119. }
  120. // Updates this range accordingly to the updates applied to the model. Bases on change events.
  121. //
  122. // @private
  123. // @param {module:engine/model/operation/operation~Operation} operation Executed operation.
  124. function transform( operation ) {
  125. // Transform the range by the operation. Join the result ranges if needed.
  126. const ranges = this.getTransformedByOperation( operation );
  127. const result = Range._createFromRanges( ranges );
  128. const boundariesChanged = !result.isEqual( this );
  129. const contentChanged = doesOperationChangeRangeContent( this, operation );
  130. let deletionPosition = null;
  131. if ( boundariesChanged ) {
  132. // If range boundaries have changed, fire `change:range` event.
  133. //
  134. if ( result.root.rootName == '$graveyard' ) {
  135. // If the range was moved to the graveyard root, set `deletionPosition`.
  136. if ( operation.type == 'remove' ) {
  137. deletionPosition = operation.sourcePosition;
  138. } else {
  139. // Merge operation.
  140. deletionPosition = operation.deletionPosition;
  141. }
  142. }
  143. const oldRange = this.toRange();
  144. this.start = result.start;
  145. this.end = result.end;
  146. this.fire( 'change:range', oldRange, { deletionPosition } );
  147. } else if ( contentChanged ) {
  148. // If range boundaries have not changed, but there was change inside the range, fire `change:content` event.
  149. this.fire( 'change:content', this.toRange(), { deletionPosition } );
  150. }
  151. }
  152. // Checks whether given operation changes something inside the range (even if it does not change boundaries).
  153. //
  154. // @private
  155. // @param {module:engine/model/range~Range} range Range to check.
  156. // @param {module:engine/model/operation/operation~Operation} operation Executed operation.
  157. // @returns {Boolean}
  158. function doesOperationChangeRangeContent( range, operation ) {
  159. switch ( operation.type ) {
  160. case 'insert':
  161. return range.containsPosition( operation.position );
  162. case 'move':
  163. case 'remove':
  164. case 'reinsert':
  165. case 'merge':
  166. return range.containsPosition( operation.sourcePosition ) ||
  167. range.start.isEqual( operation.sourcePosition ) ||
  168. range.containsPosition( operation.targetPosition );
  169. case 'split':
  170. return range.containsPosition( operation.splitPosition ) || range.containsPosition( operation.insertionPosition );
  171. }
  172. return false;
  173. }
  174. mix( LiveRange, EmitterMixin );