attributedelta.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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/delta/attributedelta
  7. */
  8. import Delta from './delta';
  9. import DeltaFactory from './deltafactory';
  10. import { register } from '../batch';
  11. import AttributeOperation from '../operation/attributeoperation';
  12. import RootAttributeOperation from '../operation/rootattributeoperation';
  13. import NoOperation from '../operation/nooperation';
  14. import Position from '../position';
  15. import Range from '../range';
  16. /**
  17. * To provide specific OT behavior and better collisions solving, methods to change attributes
  18. * ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute})
  19. * use `AttributeDelta` class which inherits from the `Delta` class and may overwrite some methods.
  20. * @extends module:engine/model/delta/delta~Delta
  21. */
  22. export default class AttributeDelta extends Delta {
  23. /**
  24. * @inheritDoc
  25. */
  26. get type() {
  27. return 'attribute';
  28. }
  29. /**
  30. * The attribute key that is changed by the delta or `null` if the delta has no operations.
  31. *
  32. * @readonly
  33. * @type {String|null}
  34. */
  35. get key() {
  36. return this.operations[ 0 ] ? this.operations[ 0 ].key : null;
  37. }
  38. /**
  39. * The attribute value that is set by the delta or `null` if the delta has no operations.
  40. *
  41. * @readonly
  42. * @type {*|null}
  43. */
  44. get value() {
  45. return this.operations[ 0 ] ? this.operations[ 0 ].newValue : null;
  46. }
  47. /**
  48. * The range on which delta operates or `null` if the delta has no operations.
  49. *
  50. * @readonly
  51. * @type {module:engine/model/range~Range|null}
  52. */
  53. get range() {
  54. // Check if it is cached.
  55. if ( this._range ) {
  56. return this._range;
  57. }
  58. let start = null;
  59. let end = null;
  60. for ( const operation of this.operations ) {
  61. if ( operation instanceof NoOperation ) {
  62. continue;
  63. }
  64. if ( start === null || start.isAfter( operation.range.start ) ) {
  65. start = operation.range.start;
  66. }
  67. if ( end === null || end.isBefore( operation.range.end ) ) {
  68. end = operation.range.end;
  69. }
  70. }
  71. if ( start && end ) {
  72. this._range = new Range( start, end );
  73. return this._range;
  74. }
  75. return null;
  76. }
  77. get _reverseDeltaClass() {
  78. return AttributeDelta;
  79. }
  80. /**
  81. * @inheritDoc
  82. */
  83. toJSON() {
  84. const json = super.toJSON();
  85. delete json._range;
  86. return json;
  87. }
  88. /**
  89. * @inheritDoc
  90. */
  91. static get className() {
  92. return 'engine.model.delta.AttributeDelta';
  93. }
  94. }
  95. /**
  96. * To provide specific OT behavior and better collisions solving, methods to change attributes
  97. * ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute})
  98. * use `RootAttributeDelta` class which inherits from the `Delta` class and may
  99. * overwrite some methods.
  100. *
  101. * @extends module:engine/model/delta/delta~Delta
  102. */
  103. export class RootAttributeDelta extends Delta {
  104. /**
  105. * @inheritDoc
  106. */
  107. static get className() {
  108. return 'engine.model.delta.RootAttributeDelta';
  109. }
  110. }
  111. /**
  112. * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
  113. * or on a {@link module:engine/model/range~Range range}.
  114. *
  115. * @chainable
  116. * @method module:engine/model/batch~Batch#setAttribute
  117. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  118. * Model item or range on which the attribute will be set.
  119. * @param {String} key Attribute key.
  120. * @param {*} value Attribute new value.
  121. */
  122. register( 'setAttribute', function( itemOrRange, key, value ) {
  123. attribute( this, key, value, itemOrRange );
  124. return this;
  125. } );
  126. /**
  127. * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
  128. * or from a {@link module:engine/model/range~Range range}.
  129. *
  130. * @chainable
  131. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  132. * Model item or range from which the attribute will be removed.
  133. * @method module:engine/model/batch~Batch#removeAttribute
  134. * @param {String} key Attribute key.
  135. */
  136. register( 'removeAttribute', function( itemOrRange, key ) {
  137. attribute( this, key, null, itemOrRange );
  138. return this;
  139. } );
  140. function attribute( batch, key, value, itemOrRange ) {
  141. if ( itemOrRange instanceof Range ) {
  142. changeRange( batch, batch.document, key, value, itemOrRange );
  143. } else {
  144. changeItem( batch, batch.document, key, value, itemOrRange );
  145. }
  146. }
  147. function changeItem( batch, doc, key, value, item ) {
  148. const previousValue = item.getAttribute( key );
  149. let range, operation;
  150. const delta = item.is( 'rootElement' ) ? new RootAttributeDelta() : new AttributeDelta();
  151. if ( previousValue != value ) {
  152. batch.addDelta( delta );
  153. if ( item.is( 'rootElement' ) ) {
  154. // If we change attributes of root element, we have to use `RootAttributeOperation`.
  155. operation = new RootAttributeOperation( item, key, previousValue, value, doc.version );
  156. } else {
  157. if ( item.is( 'element' ) ) {
  158. // If we change the attribute of the element, we do not want to change attributes of its children, so
  159. // the end of the range cannot be after the closing tag, it should be inside that element, before any of
  160. // it's children, so the range will contain only the opening tag.
  161. range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
  162. } else {
  163. // If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
  164. // all characters represented by it.
  165. range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
  166. }
  167. operation = new AttributeOperation( range, key, previousValue, value, doc.version );
  168. }
  169. delta.addOperation( operation );
  170. doc.applyOperation( operation );
  171. }
  172. }
  173. // Because attribute operation needs to have the same attribute value on the whole range, this function splits the range
  174. // into smaller parts.
  175. function changeRange( batch, doc, attributeKey, attributeValue, range ) {
  176. const delta = new AttributeDelta();
  177. // Position of the last split, the beginning of the new range.
  178. let lastSplitPosition = range.start;
  179. // Currently position in the scanning range. Because we need value after the position, it is not a current
  180. // position of the iterator but the previous one (we need to iterate one more time to get the value after).
  181. let position,
  182. // Value before the currently position.
  183. attributeValueBefore,
  184. // Value after the currently position.
  185. attributeValueAfter;
  186. for ( const value of range ) {
  187. attributeValueAfter = value.item.getAttribute( attributeKey );
  188. // At the first run of the iterator the position in undefined. We also do not have a attributeValueBefore, but
  189. // because attributeValueAfter may be null, attributeValueBefore may be equal attributeValueAfter ( undefined == null ).
  190. if ( position && attributeValueBefore != attributeValueAfter ) {
  191. // if attributeValueBefore == attributeValue there is nothing to change, so we add operation only if these values are different.
  192. if ( attributeValueBefore != attributeValue ) {
  193. addOperation();
  194. }
  195. lastSplitPosition = position;
  196. }
  197. position = value.nextPosition;
  198. attributeValueBefore = attributeValueAfter;
  199. }
  200. // Because position in the loop is not the iterator position (see let position comment), the last position in
  201. // the while loop will be last but one position in the range. We need to check the last position manually.
  202. if ( position instanceof Position && position != lastSplitPosition && attributeValueBefore != attributeValue ) {
  203. addOperation();
  204. }
  205. function addOperation() {
  206. // Add delta to the batch only if there is at least operation in the delta. Add delta only once.
  207. if ( delta.operations.length === 0 ) {
  208. batch.addDelta( delta );
  209. }
  210. const range = new Range( lastSplitPosition, position );
  211. const operation = new AttributeOperation( range, attributeKey, attributeValueBefore, attributeValue, doc.version );
  212. delta.addOperation( operation );
  213. doc.applyOperation( operation );
  214. }
  215. }
  216. DeltaFactory.register( AttributeDelta );
  217. DeltaFactory.register( RootAttributeDelta );