attributeoperation.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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/operation/attributeoperation
  7. */
  8. import Operation from './operation';
  9. import Range from '../range';
  10. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  11. import writer from '../writer';
  12. import isEqual from '@ckeditor/ckeditor5-utils/src/lib/lodash/isEqual';
  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.
  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} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
  38. */
  39. constructor( range, key, oldValue, newValue, baseVersion ) {
  40. super( baseVersion );
  41. /**
  42. * Range on which operation should be applied.
  43. *
  44. * @readonly
  45. * @member {module:engine/model/range~Range}
  46. */
  47. this.range = Range.createFromRange( range );
  48. /**
  49. * Key of an attribute to change or remove.
  50. *
  51. * @readonly
  52. * @member {String}
  53. */
  54. this.key = key;
  55. /**
  56. * Old value of the attribute with given key or `null`, if attribute was not set before.
  57. *
  58. * @readonly
  59. * @member {*}
  60. */
  61. this.oldValue = oldValue === undefined ? null : oldValue;
  62. /**
  63. * New value of the attribute with given key or `null`, if operation should remove attribute.
  64. *
  65. * @readonly
  66. * @member {*}
  67. */
  68. this.newValue = newValue === undefined ? null : newValue;
  69. /**
  70. * @inheritDoc
  71. */
  72. this.isDocumentOperation = !!this.range.root.document;
  73. }
  74. /**
  75. * @inheritDoc
  76. */
  77. get type() {
  78. if ( this.oldValue === null ) {
  79. return 'addAttribute';
  80. } else if ( this.newValue === null ) {
  81. return 'removeAttribute';
  82. } else {
  83. return 'changeAttribute';
  84. }
  85. }
  86. /**
  87. * Creates and returns an operation that has the same parameters as this operation.
  88. *
  89. * @returns {module:engine/model/operation/attributeoperation~AttributeOperation} Clone of this operation.
  90. */
  91. clone() {
  92. return new AttributeOperation( this.range, this.key, this.oldValue, this.newValue, this.baseVersion );
  93. }
  94. /**
  95. * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
  96. *
  97. * @returns {module:engine/model/operation/attributeoperation~AttributeOperation}
  98. */
  99. getReversed() {
  100. return new AttributeOperation( this.range, this.key, this.newValue, this.oldValue, this.baseVersion + 1 );
  101. }
  102. /**
  103. * @inheritDoc
  104. */
  105. _execute() {
  106. // Validation.
  107. for ( const item of this.range.getItems() ) {
  108. if ( this.oldValue !== null && !isEqual( item.getAttribute( this.key ), this.oldValue ) ) {
  109. /**
  110. * Changed node has different attribute value than operation's old attribute value.
  111. *
  112. * @error attribute-operation-wrong-old-value
  113. * @param {module:engine/model/item~Item} item
  114. * @param {String} key
  115. * @param {*} value
  116. */
  117. throw new CKEditorError(
  118. 'attribute-operation-wrong-old-value: Changed node has different attribute value than operation\'s ' +
  119. 'old attribute value.',
  120. { item, key: this.key, value: this.oldValue }
  121. );
  122. }
  123. if ( this.oldValue === null && this.newValue !== null && item.hasAttribute( this.key ) ) {
  124. /**
  125. * The attribute with given key already exists for the given node.
  126. *
  127. * @error attribute-operation-attribute-exists
  128. * @param {module:engine/model/node~Node} node
  129. * @param {String} key
  130. */
  131. throw new CKEditorError(
  132. 'attribute-operation-attribute-exists: The attribute with given key already exists.',
  133. { node: item, key: this.key }
  134. );
  135. }
  136. }
  137. // If value to set is same as old value, don't do anything.
  138. if ( !isEqual( this.oldValue, this.newValue ) ) {
  139. // Execution.
  140. writer.setAttribute( this.range, this.key, this.newValue );
  141. }
  142. return { range: this.range, key: this.key, oldValue: this.oldValue, newValue: this.newValue };
  143. }
  144. /**
  145. * @inheritDoc
  146. */
  147. static get className() {
  148. return 'engine.model.operation.AttributeOperation';
  149. }
  150. /**
  151. * Creates `AttributeOperation` object from deserilized object, i.e. from parsed JSON string.
  152. *
  153. * @param {Object} json Deserialized JSON object.
  154. * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
  155. * @returns {module:engine/model/operation/attributeoperation~AttributeOperation}
  156. */
  157. static fromJSON( json, document ) {
  158. return new AttributeOperation( Range.fromJSON( json.range, document ), json.key, json.oldValue, json.newValue, json.baseVersion );
  159. }
  160. }