attributeoperation.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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/operation/attributeoperation
  7. */
  8. import Operation from './operation';
  9. import Range from '../range';
  10. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  11. import { _setAttribute } from './utils';
  12. import { isEqual } from 'lodash-es';
  13. /**
  14. * Operation to change nodes' attribute.
  15. *
  16. * Using this class you can add, remove or change value of the attribute.
  17. *
  18. * @extends module:engine/model/operation/operation~Operation
  19. */
  20. export default class AttributeOperation extends Operation {
  21. /**
  22. * Creates an operation that changes, removes or adds attributes.
  23. *
  24. * If only `newValue` is set, attribute will be added on a node. Note that all nodes in operation's range must not
  25. * have an attribute with the same key as the added attribute.
  26. *
  27. * If only `oldValue` is set, then attribute with given key will be removed. Note that all nodes in operation's range
  28. * must have an attribute with that key added.
  29. *
  30. * If both `newValue` and `oldValue` are set, then the operation will change the attribute value. Note that all nodes in
  31. * operation's ranges must already have an attribute with given key and `oldValue` as value
  32. *
  33. * @param {module:engine/model/range~Range} range Range on which the operation should be applied. Must be a flat range.
  34. * @param {String} key Key of an attribute to change or remove.
  35. * @param {*} oldValue Old value of the attribute with given key or `null`, if attribute was not set before.
  36. * @param {*} newValue New value of the attribute with given key or `null`, if operation should remove attribute.
  37. * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
  38. * can be applied or `null` if the operation operates on detached (non-document) tree.
  39. */
  40. constructor( range, key, oldValue, newValue, baseVersion ) {
  41. super( baseVersion );
  42. /**
  43. * Range on which operation should be applied.
  44. *
  45. * @readonly
  46. * @member {module:engine/model/range~Range}
  47. */
  48. this.range = Range.createFromRange( range );
  49. /**
  50. * Key of an attribute to change or remove.
  51. *
  52. * @readonly
  53. * @member {String}
  54. */
  55. this.key = key;
  56. /**
  57. * Old value of the attribute with given key or `null`, if attribute was not set before.
  58. *
  59. * @readonly
  60. * @member {*}
  61. */
  62. this.oldValue = oldValue === undefined ? null : oldValue;
  63. /**
  64. * New value of the attribute with given key or `null`, if operation should remove attribute.
  65. *
  66. * @readonly
  67. * @member {*}
  68. */
  69. this.newValue = newValue === undefined ? null : newValue;
  70. }
  71. /**
  72. * @inheritDoc
  73. */
  74. get type() {
  75. if ( this.oldValue === null ) {
  76. return 'addAttribute';
  77. } else if ( this.newValue === null ) {
  78. return 'removeAttribute';
  79. } else {
  80. return 'changeAttribute';
  81. }
  82. }
  83. /**
  84. * Creates and returns an operation that has the same parameters as this operation.
  85. *
  86. * @returns {module:engine/model/operation/attributeoperation~AttributeOperation} Clone of this operation.
  87. */
  88. clone() {
  89. return new AttributeOperation( this.range, this.key, this.oldValue, this.newValue, this.baseVersion );
  90. }
  91. /**
  92. * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
  93. *
  94. * @returns {module:engine/model/operation/attributeoperation~AttributeOperation}
  95. */
  96. getReversed() {
  97. return new AttributeOperation( this.range, this.key, this.newValue, this.oldValue, this.baseVersion + 1 );
  98. }
  99. /**
  100. * @inheritDoc
  101. */
  102. toJSON() {
  103. const json = super.toJSON();
  104. json.range = this.range.toJSON();
  105. return json;
  106. }
  107. /**
  108. * @inheritDoc
  109. */
  110. _validate() {
  111. if ( !this.range.isFlat ) {
  112. /**
  113. * The range to change is not flat.
  114. *
  115. * @error attribute-operation-range-not-flat
  116. */
  117. throw new CKEditorError( 'attribute-operation-range-not-flat: The range to change is not flat.' );
  118. }
  119. for ( const item of this.range.getItems( { shallow: true } ) ) {
  120. if ( this.oldValue !== null && !isEqual( item.getAttribute( this.key ), this.oldValue ) ) {
  121. /**
  122. * Changed node has different attribute value than operation's old attribute value.
  123. *
  124. * @error attribute-operation-wrong-old-value
  125. * @param {module:engine/model/item~Item} item
  126. * @param {String} key
  127. * @param {*} value
  128. */
  129. throw new CKEditorError(
  130. 'attribute-operation-wrong-old-value: Changed node has different attribute value than operation\'s ' +
  131. 'old attribute value.',
  132. { item, key: this.key, value: this.oldValue }
  133. );
  134. }
  135. if ( this.oldValue === null && this.newValue !== null && item.hasAttribute( this.key ) ) {
  136. /**
  137. * The attribute with given key already exists for the given node.
  138. *
  139. * @error attribute-operation-attribute-exists
  140. * @param {module:engine/model/node~Node} node
  141. * @param {String} key
  142. */
  143. throw new CKEditorError(
  144. 'attribute-operation-attribute-exists: The attribute with given key already exists.',
  145. { node: item, key: this.key }
  146. );
  147. }
  148. }
  149. }
  150. /**
  151. * @inheritDoc
  152. */
  153. _execute() {
  154. // If value to set is same as old value, don't do anything.
  155. if ( !isEqual( this.oldValue, this.newValue ) ) {
  156. // Execution.
  157. _setAttribute( this.range, this.key, this.newValue );
  158. }
  159. }
  160. /**
  161. * @inheritDoc
  162. */
  163. static get className() {
  164. return 'engine.model.operation.AttributeOperation';
  165. }
  166. /**
  167. * Creates `AttributeOperation` object from deserilized object, i.e. from parsed JSON string.
  168. *
  169. * @param {Object} json Deserialized JSON object.
  170. * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
  171. * @returns {module:engine/model/operation/attributeoperation~AttributeOperation}
  172. */
  173. static fromJSON( json, document ) {
  174. return new AttributeOperation( Range.fromJSON( json.range, document ), json.key, json.oldValue, json.newValue, json.baseVersion );
  175. }
  176. }