attributedelta.js 7.8 KB

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