basic-transformations.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import { addTransformationCase, defaultTransform } from './transform.js';
  7. import Range from '../range.js';
  8. import Position from '../position.js';
  9. import AttributeOperation from '../operation/attributeoperation.js';
  10. import Delta from './delta.js';
  11. import AttributeDelta from './attributedelta.js';
  12. import InsertDelta from './insertdelta.js';
  13. import MergeDelta from './mergedelta.js';
  14. import MoveDelta from './movedelta.js';
  15. import SplitDelta from './splitdelta.js';
  16. import WeakInsertDelta from './weakinsertdelta.js';
  17. import WrapDelta from './wrapdelta.js';
  18. import UnwrapDelta from './unwrapdelta.js';
  19. import compareArrays from '../../../utils/comparearrays.js';
  20. // Provide transformations for default deltas.
  21. // Add special case for AttributeDelta x WeakInsertDelta transformation.
  22. addTransformationCase( AttributeDelta, WeakInsertDelta, ( a, b, isStrong ) => {
  23. // If nodes are weak-inserted into attribute delta range, we need to apply changes from attribute delta on them.
  24. // So first we do the normal transformation and if this special cases happens, we will add an extra delta.
  25. const deltas = defaultTransform( a, b, isStrong );
  26. if ( a.range.containsPosition( b.position ) ) {
  27. deltas.push( _getComplementaryAttrDelta( b, a ) );
  28. }
  29. return deltas;
  30. } );
  31. // Add special case for InsertDelta x MergeDelta transformation.
  32. addTransformationCase( InsertDelta, MergeDelta, ( a, b, isStrong ) => {
  33. // If insert is applied at the same position where merge happened, we reverse the merge (we treat it like it
  34. // didn't happen) and then apply the original insert operation. This is "mirrored" in MergeDelta x InsertDelta
  35. // transformation below, where we simply do not apply MergeDelta.
  36. if ( a.position.isEqual( b.position ) ) {
  37. return [
  38. b.getReversed(),
  39. a.clone()
  40. ];
  41. }
  42. return defaultTransform( a, b, isStrong );
  43. } );
  44. // Add special case for MoveDelta x MergeDelta transformation.
  45. addTransformationCase( MoveDelta, MergeDelta, ( a, b, isStrong ) => {
  46. // If move delta is supposed to move a node that has been merged, we reverse the merge (we treat it like it
  47. // didn't happen) and then apply the original move operation. This is "mirrored" in MergeDelta x MoveDelta
  48. // transformation below, where we simply do not apply MergeDelta.
  49. const operateInSameParent = compareArrays( a.sourcePosition.getParentPath(), b.position.getParentPath() ) === 'SAME';
  50. const mergeInsideMoveRange = a.sourcePosition.offset <= b.position.offset && a.sourcePosition.offset + a.howMany > b.position.offset;
  51. if ( operateInSameParent && mergeInsideMoveRange ) {
  52. return [
  53. b.getReversed(),
  54. a.clone()
  55. ];
  56. }
  57. return defaultTransform( a, b, isStrong );
  58. } );
  59. // Add special case for MergeDelta x InsertDelta transformation.
  60. addTransformationCase( MergeDelta, InsertDelta, ( a, b, isStrong ) => {
  61. // If merge is applied at the same position where we inserted a range of nodes we cancel the merge as it's results
  62. // may be unexpected and very weird. Even if we do some "magic" we don't know what really are users' expectations.
  63. if ( a.position.isEqual( b.position ) ) {
  64. // This is "no-op" delta, it has no type and no operations, it basically does nothing.
  65. // It is used when we don't want to apply changes but still we need to return a delta.
  66. return [ new Delta() ];
  67. }
  68. return defaultTransform( a, b, isStrong );
  69. } );
  70. // Add special case for MergeDelta x MoveDelta transformation.
  71. addTransformationCase( MergeDelta, MoveDelta, ( a, b, isStrong ) => {
  72. // If merge is applied at the position between moved nodes we cancel the merge as it's results may be unexpected and
  73. // very weird. Even if we do some "magic" we don't know what really are users' expectations.
  74. const operateInSameParent = compareArrays( a.position.getParentPath(), b.sourcePosition.getParentPath() ) === 'SAME';
  75. const mergeInsideMoveRange = b.sourcePosition.offset <= a.position.offset && b.sourcePosition.offset + b.howMany > a.position.offset;
  76. if ( operateInSameParent && mergeInsideMoveRange ) {
  77. // This is "no-op" delta, it has no type and no operations, it basically does nothing.
  78. // It is used when we don't want to apply changes but still we need to return a delta.
  79. return [ new Delta() ];
  80. }
  81. return defaultTransform( a, b, isStrong );
  82. } );
  83. // Add special case for SplitDelta x SplitDelta transformation.
  84. addTransformationCase( SplitDelta, SplitDelta, ( a, b, isStrong ) => {
  85. const pathA = a.position.getParentPath();
  86. const pathB = b.position.getParentPath();
  87. // The special case is for splits inside the same parent.
  88. if ( compareArrays( pathA, pathB ) == 'SAME' ) {
  89. if ( a.position.offset == b.position.offset ) {
  90. // We are applying split at the position where split already happened. Additional split is not needed.
  91. return [ new Delta() ];
  92. } else if ( a.position.offset < b.position.offset ) {
  93. // Incoming split delta splits at closer offset. So we simply have to once again split the same node,
  94. // but since it was already split (at further offset) there are less child nodes in the split node.
  95. // This means that we have to update `howMany` parameter of `MoveOperation` for that delta.
  96. const delta = a.clone();
  97. delta._moveOperation.howMany = b.position.offset - a.position.offset;
  98. return [ delta ];
  99. } else {
  100. // Incoming split delta splits at further offset. We have to simulate that we are not splitting the
  101. // original split node but the node after it, which got created by the other split delta.
  102. // To do so, we increment offsets so it looks like the split delta was created in the next node.
  103. const delta = a.clone();
  104. delta._cloneOperation.position.offset++;
  105. delta._moveOperation.sourcePosition.path[ delta._moveOperation.sourcePosition.path.length - 2 ]++;
  106. delta._moveOperation.targetPosition.path[ delta._moveOperation.targetPosition.path.length - 2 ]++;
  107. delta._moveOperation.sourcePosition.offset = a.position.offset - b.position.offset;
  108. return [ delta ];
  109. }
  110. }
  111. return defaultTransform( a, b, isStrong );
  112. } );
  113. // Add special case for SplitDelta x UnwrapDelta transformation.
  114. addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, isStrong ) => {
  115. // If incoming split delta tries to split a node that just got unwrapped, there is actually nothing to split,
  116. // so we discard that delta.
  117. if ( compareArrays( b.position.path, a.position.getParentPath() ) === 'SAME' ) {
  118. // This is "no-op" delta, it has no type and no operations, it basically does nothing.
  119. // It is used when we don't want to apply changes but still we need to return a delta.
  120. return [ new Delta() ];
  121. }
  122. return defaultTransform( a, b, isStrong );
  123. } );
  124. // Add special case for SplitDelta x WrapDelta transformation.
  125. addTransformationCase( SplitDelta, WrapDelta, ( a, b, isStrong ) => {
  126. // If split is applied at the position between wrapped nodes, we cancel the split as it's results may be unexpected and
  127. // very weird. Even if we do some "magic" we don't know what really are users' expectations.
  128. const operateInSameParent = compareArrays( a.position.getParentPath(), b.range.start.getParentPath() ) === 'SAME';
  129. const splitInsideWrapRange = b.range.start.offset < a.position.offset && b.range.end.offset >= a.position.offset;
  130. if ( operateInSameParent && splitInsideWrapRange ) {
  131. // This is "no-op" delta, it has no type and no operations, it basically does nothing.
  132. // It is used when we don't want to apply changes but still we need to return a delta.
  133. return [ new Delta() ];
  134. } else if ( compareArrays( a.position.getParentPath(), b.range.end.getShiftedBy( -1 ).path ) === 'SAME' ) {
  135. // Split position is directly inside the last node from wrap range.
  136. // If that's the case, we manually change split delta so it will "target" inside the wrapping element.
  137. // By doing so we will be inserting split node right to the original node which feels natural and is a good UX.
  138. const delta = a.clone();
  139. // 1. Fix insert operation position.
  140. // Node to split is the last children of the wrapping element.
  141. // Wrapping element is the element inserted by WrapDelta (re)insert operation.
  142. // It is inserted after the wrapped range, but the wrapped range will be moved inside it.
  143. // Having this in mind, it is correct to use wrapped range start position as the position before wrapping element.
  144. const splitNodePos = Position.createFromPosition( b.range.start );
  145. // Now, `splitNodePos` points before wrapping element.
  146. // To get a position before last children of that element, we expand position's `path` member by proper offset.
  147. splitNodePos.path.push( b.howMany - 1 );
  148. // SplitDelta insert operation position should be right after the node we split.
  149. const insertPos = splitNodePos.getShiftedBy( 1 );
  150. delta._cloneOperation.position = insertPos;
  151. // 2. Fix move operation source position.
  152. // Nodes moved by SplitDelta will be moved from new position, modified by WrapDelta.
  153. // To obtain that new position, `splitNodePos` will be used, as this is the node we are extracting children from.
  154. const sourcePos = Position.createFromPosition( splitNodePos );
  155. // Nothing changed inside split node so it is correct to use the original split position offset.
  156. sourcePos.path.push( a.position.offset );
  157. delta._moveOperation.sourcePosition = sourcePos;
  158. // 3. Fix move operation target position.
  159. // SplitDelta move operation target position should be inside the node inserted by operation above.
  160. // Since the node is empty, we will insert at offset 0.
  161. const targetPos = Position.createFromPosition( insertPos );
  162. targetPos.path.push( 0 );
  163. delta._moveOperation.targetPosition = targetPos;
  164. return [ delta ];
  165. }
  166. return defaultTransform( a, b, isStrong );
  167. } );
  168. // Add special case for UnwrapDelta x SplitDelta transformation.
  169. addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, isStrong ) => {
  170. // If incoming unwrap delta tries to unwrap node that got split we should unwrap the original node and the split copy.
  171. // This can be achieved either by reverting split and applying unwrap to singular node, or creating additional unwrap delta.
  172. if ( compareArrays( a.position.path, b.position.getParentPath() ) === 'SAME' ) {
  173. return [
  174. b.getReversed(),
  175. a.clone()
  176. ];
  177. }
  178. return defaultTransform( a, b, isStrong );
  179. } );
  180. // Add special case for WeakInsertDelta x AttributeDelta transformation.
  181. addTransformationCase( WeakInsertDelta, AttributeDelta, ( a, b, isStrong ) => {
  182. // If nodes are weak-inserted into attribute delta range, we need to apply changes from attribute delta on them.
  183. // So first we do the normal transformation and if this special cases happens, we will add an extra delta.
  184. const deltas = defaultTransform( a, b, isStrong );
  185. if ( b.range.containsPosition( a.position ) ) {
  186. deltas.push( _getComplementaryAttrDelta( a, b ) );
  187. }
  188. return deltas;
  189. } );
  190. // Add special case for WrapDelta x SplitDelta transformation.
  191. addTransformationCase( WrapDelta, SplitDelta, ( a, b, isStrong ) => {
  192. // If incoming wrap delta tries to wrap range that contains split position, we have to cancel the split and apply
  193. // the wrap. Since split was already applied, we have to revert it.
  194. const operateInSameParent = compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'SAME';
  195. const splitInsideWrapRange = a.range.start.offset < b.position.offset && a.range.end.offset >= b.position.offset;
  196. if ( operateInSameParent && splitInsideWrapRange ) {
  197. return [
  198. b.getReversed(),
  199. a.clone()
  200. ];
  201. } else if ( compareArrays( b.position.getParentPath(), a.range.end.getShiftedBy( -1 ).path ) === 'SAME' ) {
  202. const delta = a.clone();
  203. // Move wrapping element insert position one node further so it is after the split node insertion.
  204. delta._insertOperation.position.offset++;
  205. // Include the split node copy.
  206. delta._moveOperation.howMany++;
  207. // Change the path to wrapping element in move operation.
  208. delta._moveOperation.targetPosition.path[ delta._moveOperation.targetPosition.path.length - 2 ]++;
  209. return [ delta ];
  210. }
  211. return defaultTransform( a, b, isStrong );
  212. } );
  213. // Helper function for `AttributeDelta` class transformations.
  214. // Creates an attribute delta that sets attribute from given `attributeDelta` on nodes from given `weakInsertDelta`.
  215. function _getComplementaryAttrDelta( weakInsertDelta, attributeDelta ) {
  216. const complementaryAttrDelta = new AttributeDelta();
  217. // At the beginning we store the attribute value from the first node on `weakInsertDelta` node list.
  218. let val = weakInsertDelta.nodeList.get( 0 ).getAttribute( attributeDelta.key );
  219. // This stores the last index of `weakInsertDelta` node list where the attribute value was different
  220. // than in the previous node. We need it to create separate `AttributeOperation`s for nodes with different attributes.
  221. let lastIndex = 0;
  222. for ( let i = 0; i < weakInsertDelta.nodeList.length; i++ ) {
  223. const node = weakInsertDelta.nodeList.get( i );
  224. const nodeAttrVal = node.getAttribute( attributeDelta.key );
  225. // If previous node has different attribute value, we will create an operation to the point before current node.
  226. // So all nodes with the same attributes up to this point will be included in one `AttributeOperation`.
  227. if ( nodeAttrVal != val ) {
  228. // New operation is created only when it is needed. If given node already has proper value for this
  229. // attribute we simply skip it without adding a new operation.
  230. if ( val != attributeDelta.value ) {
  231. const range = new Range( weakInsertDelta.position.getShiftedBy( lastIndex ), weakInsertDelta.position.getShiftedBy( i ) );
  232. // We don't care about base version because it will be updated after transformations anyway.
  233. const attrOperation = new AttributeOperation( range, attributeDelta.key, val, attributeDelta.value, 0 );
  234. complementaryAttrDelta.addOperation( attrOperation );
  235. }
  236. val = nodeAttrVal;
  237. lastIndex = i;
  238. }
  239. }
  240. // At the end we have to add additional `AttributeOperation` for the last part of node list. If all nodes on the
  241. // node list had same attributes, this will be the only operation added to the delta.
  242. const range = new Range(
  243. weakInsertDelta.position.getShiftedBy( lastIndex ),
  244. weakInsertDelta.position.getShiftedBy( weakInsertDelta.nodeList.length )
  245. );
  246. complementaryAttrDelta.addOperation( new AttributeOperation( range, attributeDelta.key, val, attributeDelta.value, 0 ) );
  247. return complementaryAttrDelta;
  248. }