liverange.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Range from './range.js';
  6. import EmitterMixin from '../../utils/emittermixin.js';
  7. import mix from '../../utils/mix.js';
  8. /**
  9. * `LiveRange` is a type of {@link engine.model.Range Range} that updates itself as {@link engine.model.Document document}
  10. * is changed through operations. It may be used as a bookmark.
  11. *
  12. * **Note:** Be very careful when dealing with `LiveRange`. Each `LiveRange` instance bind events that might
  13. * have to be unbound. Use {@link engine.model.LiveRange#detach detach} whenever you don't need `LiveRange` anymore.
  14. *
  15. * @memberOf engine.model
  16. */
  17. export default class LiveRange extends Range {
  18. /**
  19. * Creates a live range.
  20. *
  21. * @see engine.model.Range
  22. */
  23. constructor( start, end ) {
  24. super( start, end );
  25. bindWithDocument.call( this );
  26. }
  27. /**
  28. * Unbinds all events previously bound by `LiveRange`. Use it whenever you don't need `LiveRange` instance
  29. * anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was
  30. * referring to it).
  31. */
  32. detach() {
  33. this.stopListening();
  34. }
  35. /**
  36. * @see engine.model.Range.createIn
  37. * @static
  38. * @method engine.model.LiveRange.createIn
  39. * @param {engine.model.Element} element
  40. * @returns {engine.model.LiveRange}
  41. */
  42. /**
  43. * @see engine.model.Range.createFromPositionAndShift
  44. * @static
  45. * @method engine.model.LiveRange.createFromPositionAndShift
  46. * @param {engine.model.Position} position
  47. * @param {Number} shift
  48. * @returns {engine.model.LiveRange}
  49. */
  50. /**
  51. * @see engine.model.Range.createFromParentsAndOffsets
  52. * @static
  53. * @method engine.model.LiveRange.createFromParentsAndOffsets
  54. * @param {engine.model.Element} startElement
  55. * @param {Number} startOffset
  56. * @param {engine.model.Element} endElement
  57. * @param {Number} endOffset
  58. * @returns {engine.model.LiveRange}
  59. */
  60. /**
  61. * @see engine.model.Range.createFromRange
  62. * @static
  63. * @method engine.model.LiveRange.createFromRange
  64. * @param {engine.model.Range} range
  65. * @returns {engine.model.LiveRange}
  66. */
  67. /**
  68. * Fired when `LiveRange` instance is changed due to changes on {@link engine.model.Document}.
  69. *
  70. * @event engine.model.LiveRange#change
  71. */
  72. }
  73. /**
  74. * Binds this `LiveRange` to the {@link engine.model.Document document} that owns this range's {@link engine.model.Range#root root}.
  75. *
  76. * @ignore
  77. * @private
  78. * @method engine.model.LiveRange#bindWithDocument
  79. */
  80. function bindWithDocument() {
  81. /*jshint validthis: true */
  82. // Operation types handled by LiveRange (these are operations that change model tree structure).
  83. const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] );
  84. this.listenTo(
  85. this.root.document,
  86. 'change',
  87. ( event, type, changes ) => {
  88. if ( supportedTypes.has( type ) ) {
  89. transform.call( this, type, changes.range, changes.sourcePosition );
  90. }
  91. }
  92. );
  93. }
  94. /**
  95. * Updates this range accordingly to the updates applied to the model. Bases on change events.
  96. *
  97. * @ignore
  98. * @private
  99. * @method transform
  100. * @param {String} type Type of changes applied to the Tree Model.
  101. * @param {engine.model.Range} range Range containing the result of applied change.
  102. * @param {engine.model.Position} [position] Additional position parameter provided by some change events.
  103. */
  104. function transform( type, range, position ) {
  105. /* jshint validthis: true */
  106. let updated;
  107. const howMany = range.end.offset - range.start.offset;
  108. switch ( type ) {
  109. case 'insert':
  110. updated = this._getTransformedByInsertion( range.start, howMany, false, true )[ 0 ];
  111. break;
  112. case 'move':
  113. case 'remove':
  114. case 'reinsert':
  115. const sourcePosition = position;
  116. // Range._getTransformedByMove is expecting `targetPosition` to be "before" move
  117. // (before transformation). `range.start` is already after the move happened.
  118. // We have to revert `range.start` to the state before the move.
  119. const targetPosition = range.start._getTransformedByInsertion( sourcePosition, howMany );
  120. const result = this._getTransformedByMove( sourcePosition, targetPosition, howMany, false, true );
  121. // First item in the array is the "difference" part, so a part of the range
  122. // that did not get moved. We use it as reference range and expand if possible.
  123. updated = result[ 0 ];
  124. // We will check if there is other range and if it is touching the reference range.
  125. // If it does, we will expand the reference range (at the beginning or at the end).
  126. // Keep in mind that without settings `spread` flag, `_getTransformedByMove` may
  127. // return maximum two ranges.
  128. if ( result.length > 1 ) {
  129. let otherRange = result[ 1 ];
  130. if ( updated.start.isTouching( otherRange.end ) ) {
  131. updated.start = otherRange.start;
  132. } else if ( updated.end.isTouching( otherRange.start ) ) {
  133. updated.end = otherRange.end;
  134. }
  135. }
  136. break;
  137. }
  138. // If anything changed, update the range and fire an event.
  139. if ( !updated.start.isEqual( this.start ) || !updated.end.isEqual( this.end ) ) {
  140. this.start = updated.start;
  141. this.end = updated.end;
  142. this.fire( 'change' );
  143. }
  144. }
  145. mix( LiveRange, EmitterMixin );