@ckeditor/ckeditor5-engine 47.6.1 → 48.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/LICENSE.md +1 -1
  2. package/{src → dist}/engineconfig.d.ts +6 -15
  3. package/dist/index-editor.css +38 -15
  4. package/dist/index.css +37 -37
  5. package/dist/index.css.map +1 -1
  6. package/{src → dist}/index.d.ts +0 -1
  7. package/dist/index.js +588 -94
  8. package/dist/index.js.map +1 -1
  9. package/{src → dist}/model/model.d.ts +10 -4
  10. package/{src → dist}/model/selection.d.ts +1 -1
  11. package/{src → dist}/view/downcastwriter.d.ts +3 -2
  12. package/{src → dist}/view/element.d.ts +2 -2
  13. package/{src → dist}/view/matcher.d.ts +4 -2
  14. package/dist/view/styles/background.d.ts +18 -0
  15. package/{src → dist}/view/styles/border.d.ts +0 -12
  16. package/{src → dist}/view/styles/margin.d.ts +0 -13
  17. package/{src → dist}/view/styles/padding.d.ts +0 -13
  18. package/{src → dist}/view/styles/utils.d.ts +12 -0
  19. package/package.json +20 -39
  20. package/src/controller/datacontroller.js +0 -522
  21. package/src/controller/editingcontroller.js +0 -181
  22. package/src/conversion/conversion.js +0 -606
  23. package/src/conversion/conversionhelpers.js +0 -33
  24. package/src/conversion/downcastdispatcher.js +0 -563
  25. package/src/conversion/downcasthelpers.js +0 -2160
  26. package/src/conversion/mapper.js +0 -1050
  27. package/src/conversion/modelconsumable.js +0 -331
  28. package/src/conversion/upcastdispatcher.js +0 -470
  29. package/src/conversion/upcasthelpers.js +0 -952
  30. package/src/conversion/viewconsumable.js +0 -541
  31. package/src/dataprocessor/basichtmlwriter.js +0 -22
  32. package/src/dataprocessor/dataprocessor.js +0 -5
  33. package/src/dataprocessor/htmldataprocessor.js +0 -107
  34. package/src/dataprocessor/htmlwriter.js +0 -5
  35. package/src/dataprocessor/xmldataprocessor.js +0 -127
  36. package/src/dev-utils/model.js +0 -396
  37. package/src/dev-utils/operationreplayer.js +0 -116
  38. package/src/dev-utils/utils.js +0 -122
  39. package/src/dev-utils/view.js +0 -990
  40. package/src/engineconfig.js +0 -5
  41. package/src/index.js +0 -134
  42. package/src/legacyerrors.js +0 -17
  43. package/src/model/batch.js +0 -98
  44. package/src/model/differ.js +0 -1288
  45. package/src/model/document.js +0 -398
  46. package/src/model/documentfragment.js +0 -332
  47. package/src/model/documentselection.js +0 -1026
  48. package/src/model/element.js +0 -323
  49. package/src/model/history.js +0 -206
  50. package/src/model/item.js +0 -5
  51. package/src/model/liveposition.js +0 -93
  52. package/src/model/liverange.js +0 -121
  53. package/src/model/markercollection.js +0 -436
  54. package/src/model/model.js +0 -866
  55. package/src/model/node.js +0 -371
  56. package/src/model/nodelist.js +0 -244
  57. package/src/model/operation/attributeoperation.js +0 -172
  58. package/src/model/operation/detachoperation.js +0 -87
  59. package/src/model/operation/insertoperation.js +0 -153
  60. package/src/model/operation/markeroperation.js +0 -136
  61. package/src/model/operation/mergeoperation.js +0 -184
  62. package/src/model/operation/moveoperation.js +0 -179
  63. package/src/model/operation/nooperation.js +0 -48
  64. package/src/model/operation/operation.js +0 -78
  65. package/src/model/operation/operationfactory.js +0 -44
  66. package/src/model/operation/renameoperation.js +0 -128
  67. package/src/model/operation/rootattributeoperation.js +0 -173
  68. package/src/model/operation/rootoperation.js +0 -106
  69. package/src/model/operation/splitoperation.js +0 -214
  70. package/src/model/operation/transform.js +0 -2211
  71. package/src/model/operation/utils.js +0 -217
  72. package/src/model/position.js +0 -1041
  73. package/src/model/range.js +0 -880
  74. package/src/model/rootelement.js +0 -82
  75. package/src/model/schema.js +0 -1542
  76. package/src/model/selection.js +0 -814
  77. package/src/model/text.js +0 -92
  78. package/src/model/textproxy.js +0 -202
  79. package/src/model/treewalker.js +0 -313
  80. package/src/model/typecheckable.js +0 -16
  81. package/src/model/utils/autoparagraphing.js +0 -63
  82. package/src/model/utils/deletecontent.js +0 -509
  83. package/src/model/utils/getselectedcontent.js +0 -126
  84. package/src/model/utils/insertcontent.js +0 -750
  85. package/src/model/utils/insertobject.js +0 -135
  86. package/src/model/utils/modifyselection.js +0 -187
  87. package/src/model/utils/selection-post-fixer.js +0 -264
  88. package/src/model/writer.js +0 -1318
  89. package/src/view/attributeelement.js +0 -220
  90. package/src/view/containerelement.js +0 -91
  91. package/src/view/datatransfer.js +0 -106
  92. package/src/view/document.js +0 -139
  93. package/src/view/documentfragment.js +0 -251
  94. package/src/view/documentselection.js +0 -270
  95. package/src/view/domconverter.js +0 -1661
  96. package/src/view/downcastwriter.js +0 -1589
  97. package/src/view/editableelement.js +0 -74
  98. package/src/view/element.js +0 -1053
  99. package/src/view/elementdefinition.js +0 -5
  100. package/src/view/emptyelement.js +0 -83
  101. package/src/view/filler.js +0 -161
  102. package/src/view/item.js +0 -5
  103. package/src/view/matcher.js +0 -437
  104. package/src/view/node.js +0 -238
  105. package/src/view/observer/arrowkeysobserver.js +0 -40
  106. package/src/view/observer/bubblingemittermixin.js +0 -215
  107. package/src/view/observer/bubblingeventinfo.js +0 -49
  108. package/src/view/observer/clickobserver.js +0 -26
  109. package/src/view/observer/compositionobserver.js +0 -64
  110. package/src/view/observer/domeventdata.js +0 -63
  111. package/src/view/observer/domeventobserver.js +0 -81
  112. package/src/view/observer/fakeselectionobserver.js +0 -95
  113. package/src/view/observer/focusobserver.js +0 -166
  114. package/src/view/observer/inputobserver.js +0 -236
  115. package/src/view/observer/keyobserver.js +0 -36
  116. package/src/view/observer/mouseobserver.js +0 -26
  117. package/src/view/observer/mutationobserver.js +0 -219
  118. package/src/view/observer/observer.js +0 -92
  119. package/src/view/observer/pointerobserver.js +0 -26
  120. package/src/view/observer/selectionobserver.js +0 -318
  121. package/src/view/observer/tabobserver.js +0 -42
  122. package/src/view/observer/touchobserver.js +0 -26
  123. package/src/view/placeholder.js +0 -285
  124. package/src/view/position.js +0 -341
  125. package/src/view/range.js +0 -451
  126. package/src/view/rawelement.js +0 -115
  127. package/src/view/renderer.js +0 -1148
  128. package/src/view/rooteditableelement.js +0 -78
  129. package/src/view/selection.js +0 -594
  130. package/src/view/styles/background.d.ts +0 -33
  131. package/src/view/styles/background.js +0 -74
  132. package/src/view/styles/border.js +0 -316
  133. package/src/view/styles/margin.js +0 -34
  134. package/src/view/styles/padding.js +0 -34
  135. package/src/view/styles/utils.js +0 -219
  136. package/src/view/stylesmap.js +0 -941
  137. package/src/view/text.js +0 -110
  138. package/src/view/textproxy.js +0 -136
  139. package/src/view/tokenlist.js +0 -194
  140. package/src/view/treewalker.js +0 -389
  141. package/src/view/typecheckable.js +0 -19
  142. package/src/view/uielement.js +0 -194
  143. package/src/view/upcastwriter.js +0 -363
  144. package/src/view/view.js +0 -579
  145. package/theme/placeholder.css +0 -36
  146. package/theme/renderer.css +0 -9
  147. /package/{src → dist}/controller/datacontroller.d.ts +0 -0
  148. /package/{src → dist}/controller/editingcontroller.d.ts +0 -0
  149. /package/{src → dist}/conversion/conversion.d.ts +0 -0
  150. /package/{src → dist}/conversion/conversionhelpers.d.ts +0 -0
  151. /package/{src → dist}/conversion/downcastdispatcher.d.ts +0 -0
  152. /package/{src → dist}/conversion/downcasthelpers.d.ts +0 -0
  153. /package/{src → dist}/conversion/mapper.d.ts +0 -0
  154. /package/{src → dist}/conversion/modelconsumable.d.ts +0 -0
  155. /package/{src → dist}/conversion/upcastdispatcher.d.ts +0 -0
  156. /package/{src → dist}/conversion/upcasthelpers.d.ts +0 -0
  157. /package/{src → dist}/conversion/viewconsumable.d.ts +0 -0
  158. /package/{src → dist}/dataprocessor/basichtmlwriter.d.ts +0 -0
  159. /package/{src → dist}/dataprocessor/dataprocessor.d.ts +0 -0
  160. /package/{src → dist}/dataprocessor/htmldataprocessor.d.ts +0 -0
  161. /package/{src → dist}/dataprocessor/htmlwriter.d.ts +0 -0
  162. /package/{src → dist}/dataprocessor/xmldataprocessor.d.ts +0 -0
  163. /package/{src → dist}/dev-utils/model.d.ts +0 -0
  164. /package/{src → dist}/dev-utils/operationreplayer.d.ts +0 -0
  165. /package/{src → dist}/dev-utils/utils.d.ts +0 -0
  166. /package/{src → dist}/dev-utils/view.d.ts +0 -0
  167. /package/{src → dist}/legacyerrors.d.ts +0 -0
  168. /package/{src → dist}/model/batch.d.ts +0 -0
  169. /package/{src → dist}/model/differ.d.ts +0 -0
  170. /package/{src → dist}/model/document.d.ts +0 -0
  171. /package/{src → dist}/model/documentfragment.d.ts +0 -0
  172. /package/{src → dist}/model/documentselection.d.ts +0 -0
  173. /package/{src → dist}/model/element.d.ts +0 -0
  174. /package/{src → dist}/model/history.d.ts +0 -0
  175. /package/{src → dist}/model/item.d.ts +0 -0
  176. /package/{src → dist}/model/liveposition.d.ts +0 -0
  177. /package/{src → dist}/model/liverange.d.ts +0 -0
  178. /package/{src → dist}/model/markercollection.d.ts +0 -0
  179. /package/{src → dist}/model/node.d.ts +0 -0
  180. /package/{src → dist}/model/nodelist.d.ts +0 -0
  181. /package/{src → dist}/model/operation/attributeoperation.d.ts +0 -0
  182. /package/{src → dist}/model/operation/detachoperation.d.ts +0 -0
  183. /package/{src → dist}/model/operation/insertoperation.d.ts +0 -0
  184. /package/{src → dist}/model/operation/markeroperation.d.ts +0 -0
  185. /package/{src → dist}/model/operation/mergeoperation.d.ts +0 -0
  186. /package/{src → dist}/model/operation/moveoperation.d.ts +0 -0
  187. /package/{src → dist}/model/operation/nooperation.d.ts +0 -0
  188. /package/{src → dist}/model/operation/operation.d.ts +0 -0
  189. /package/{src → dist}/model/operation/operationfactory.d.ts +0 -0
  190. /package/{src → dist}/model/operation/renameoperation.d.ts +0 -0
  191. /package/{src → dist}/model/operation/rootattributeoperation.d.ts +0 -0
  192. /package/{src → dist}/model/operation/rootoperation.d.ts +0 -0
  193. /package/{src → dist}/model/operation/splitoperation.d.ts +0 -0
  194. /package/{src → dist}/model/operation/transform.d.ts +0 -0
  195. /package/{src → dist}/model/operation/utils.d.ts +0 -0
  196. /package/{src → dist}/model/position.d.ts +0 -0
  197. /package/{src → dist}/model/range.d.ts +0 -0
  198. /package/{src → dist}/model/rootelement.d.ts +0 -0
  199. /package/{src → dist}/model/schema.d.ts +0 -0
  200. /package/{src → dist}/model/text.d.ts +0 -0
  201. /package/{src → dist}/model/textproxy.d.ts +0 -0
  202. /package/{src → dist}/model/treewalker.d.ts +0 -0
  203. /package/{src → dist}/model/typecheckable.d.ts +0 -0
  204. /package/{src → dist}/model/utils/autoparagraphing.d.ts +0 -0
  205. /package/{src → dist}/model/utils/deletecontent.d.ts +0 -0
  206. /package/{src → dist}/model/utils/getselectedcontent.d.ts +0 -0
  207. /package/{src → dist}/model/utils/insertcontent.d.ts +0 -0
  208. /package/{src → dist}/model/utils/insertobject.d.ts +0 -0
  209. /package/{src → dist}/model/utils/modifyselection.d.ts +0 -0
  210. /package/{src → dist}/model/utils/selection-post-fixer.d.ts +0 -0
  211. /package/{src → dist}/model/writer.d.ts +0 -0
  212. /package/{src → dist}/view/attributeelement.d.ts +0 -0
  213. /package/{src → dist}/view/containerelement.d.ts +0 -0
  214. /package/{src → dist}/view/datatransfer.d.ts +0 -0
  215. /package/{src → dist}/view/document.d.ts +0 -0
  216. /package/{src → dist}/view/documentfragment.d.ts +0 -0
  217. /package/{src → dist}/view/documentselection.d.ts +0 -0
  218. /package/{src → dist}/view/domconverter.d.ts +0 -0
  219. /package/{src → dist}/view/editableelement.d.ts +0 -0
  220. /package/{src → dist}/view/elementdefinition.d.ts +0 -0
  221. /package/{src → dist}/view/emptyelement.d.ts +0 -0
  222. /package/{src → dist}/view/filler.d.ts +0 -0
  223. /package/{src → dist}/view/item.d.ts +0 -0
  224. /package/{src → dist}/view/node.d.ts +0 -0
  225. /package/{src → dist}/view/observer/arrowkeysobserver.d.ts +0 -0
  226. /package/{src → dist}/view/observer/bubblingemittermixin.d.ts +0 -0
  227. /package/{src → dist}/view/observer/bubblingeventinfo.d.ts +0 -0
  228. /package/{src → dist}/view/observer/clickobserver.d.ts +0 -0
  229. /package/{src → dist}/view/observer/compositionobserver.d.ts +0 -0
  230. /package/{src → dist}/view/observer/domeventdata.d.ts +0 -0
  231. /package/{src → dist}/view/observer/domeventobserver.d.ts +0 -0
  232. /package/{src → dist}/view/observer/fakeselectionobserver.d.ts +0 -0
  233. /package/{src → dist}/view/observer/focusobserver.d.ts +0 -0
  234. /package/{src → dist}/view/observer/inputobserver.d.ts +0 -0
  235. /package/{src → dist}/view/observer/keyobserver.d.ts +0 -0
  236. /package/{src → dist}/view/observer/mouseobserver.d.ts +0 -0
  237. /package/{src → dist}/view/observer/mutationobserver.d.ts +0 -0
  238. /package/{src → dist}/view/observer/observer.d.ts +0 -0
  239. /package/{src → dist}/view/observer/pointerobserver.d.ts +0 -0
  240. /package/{src → dist}/view/observer/selectionobserver.d.ts +0 -0
  241. /package/{src → dist}/view/observer/tabobserver.d.ts +0 -0
  242. /package/{src → dist}/view/observer/touchobserver.d.ts +0 -0
  243. /package/{src → dist}/view/placeholder.d.ts +0 -0
  244. /package/{src → dist}/view/position.d.ts +0 -0
  245. /package/{src → dist}/view/range.d.ts +0 -0
  246. /package/{src → dist}/view/rawelement.d.ts +0 -0
  247. /package/{src → dist}/view/renderer.d.ts +0 -0
  248. /package/{src → dist}/view/rooteditableelement.d.ts +0 -0
  249. /package/{src → dist}/view/selection.d.ts +0 -0
  250. /package/{src → dist}/view/stylesmap.d.ts +0 -0
  251. /package/{src → dist}/view/text.d.ts +0 -0
  252. /package/{src → dist}/view/textproxy.d.ts +0 -0
  253. /package/{src → dist}/view/tokenlist.d.ts +0 -0
  254. /package/{src → dist}/view/treewalker.d.ts +0 -0
  255. /package/{src → dist}/view/typecheckable.d.ts +0 -0
  256. /package/{src → dist}/view/uielement.d.ts +0 -0
  257. /package/{src → dist}/view/upcastwriter.d.ts +0 -0
  258. /package/{src → dist}/view/view.d.ts +0 -0
@@ -1,2211 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
- */
5
- /**
6
- * @module engine/model/operation/transform
7
- */
8
- import { InsertOperation } from './insertoperation.js';
9
- import { AttributeOperation } from './attributeoperation.js';
10
- import { RenameOperation } from './renameoperation.js';
11
- import { MarkerOperation } from './markeroperation.js';
12
- import { MoveOperation } from './moveoperation.js';
13
- import { RootAttributeOperation } from './rootattributeoperation.js';
14
- import { RootOperation } from './rootoperation.js';
15
- import { MergeOperation } from './mergeoperation.js';
16
- import { SplitOperation } from './splitoperation.js';
17
- import { NoOperation } from './nooperation.js';
18
- import { ModelRange } from '../range.js';
19
- import { ModelPosition } from '../position.js';
20
- import { compareArrays } from '@ckeditor/ckeditor5-utils';
21
- const transformations = new Map();
22
- /**
23
- * Sets a transformation function to be be used to transform instances of class `OperationA` by instances of class `OperationB`.
24
- *
25
- * The `transformationFunction` is passed three parameters:
26
- *
27
- * * `a` - operation to be transformed, an instance of `OperationA`,
28
- * * `b` - operation to be transformed by, an instance of `OperationB`,
29
- * * {@link module:engine/model/operation/transform~TransformationContext `context`} - object with additional information about
30
- * transformation context.
31
- *
32
- * The `transformationFunction` should return transformation result, which is an array with one or multiple
33
- * {@link module:engine/model/operation/operation~Operation operation} instances.
34
- *
35
- * @param transformationFunction Function to use for transforming.
36
- */
37
- function setTransformation(OperationA, OperationB, transformationFunction) {
38
- let aGroup = transformations.get(OperationA);
39
- if (!aGroup) {
40
- aGroup = new Map();
41
- transformations.set(OperationA, aGroup);
42
- }
43
- aGroup.set(OperationB, transformationFunction);
44
- }
45
- /**
46
- * Returns a previously set transformation function for transforming an instance of `OperationA` by an instance of `OperationB`.
47
- *
48
- * If no transformation was set for given pair of operations, {@link module:engine/model/operation/transform~noUpdateTransformation}
49
- * is returned. This means that if no transformation was set, the `OperationA` instance will not change when transformed
50
- * by the `OperationB` instance.
51
- *
52
- * @returns Function set to transform an instance of `OperationA` by an instance of `OperationB`.
53
- */
54
- function getTransformation(OperationA, OperationB) {
55
- const aGroup = transformations.get(OperationA);
56
- if (aGroup && aGroup.has(OperationB)) {
57
- return aGroup.get(OperationB);
58
- }
59
- return noUpdateTransformation;
60
- }
61
- /**
62
- * A transformation function that only clones operation to transform, without changing it.
63
- */
64
- function noUpdateTransformation(a) {
65
- return [a];
66
- }
67
- /**
68
- * Transforms operation `a` by operation `b`.
69
- *
70
- * @param a Operation to be transformed.
71
- * @param b Operation to transform by.
72
- * @param context Transformation context for this transformation.
73
- * @returns Transformation result.
74
- * @internal
75
- */
76
- export function transform(a, b, context = {}) {
77
- const transformationFunction = getTransformation(a.constructor, b.constructor);
78
- /* eslint-disable no-useless-catch */
79
- try {
80
- a = a.clone();
81
- return transformationFunction(a, b, context);
82
- }
83
- catch (e) {
84
- // @if CK_DEBUG // console.warn( 'Error during operation transformation!', e.message );
85
- // @if CK_DEBUG // console.warn( 'Transformed operation', a );
86
- // @if CK_DEBUG // console.warn( 'Operation transformed by', b );
87
- // @if CK_DEBUG // console.warn( 'context.aIsStrong', context.aIsStrong );
88
- // @if CK_DEBUG // console.warn( 'context.aWasUndone', context.aWasUndone );
89
- // @if CK_DEBUG // console.warn( 'context.bWasUndone', context.bWasUndone );
90
- // @if CK_DEBUG // console.warn( 'context.abRelation', context.abRelation );
91
- // @if CK_DEBUG // console.warn( 'context.baRelation', context.baRelation );
92
- throw e;
93
- }
94
- /* eslint-enable no-useless-catch */
95
- }
96
- /**
97
- * Performs a transformation of two sets of operations - `operationsA` and `operationsB`. The transformation is two-way -
98
- * both transformed `operationsA` and transformed `operationsB` are returned.
99
- *
100
- * Note, that the first operation in each set should base on the same document state (
101
- * {@link module:engine/model/document~ModelDocument#version document version}).
102
- *
103
- * It is assumed that `operationsA` are "more important" during conflict resolution between two operations.
104
- *
105
- * New copies of both passed arrays and operations inside them are returned. Passed arguments are not altered.
106
- *
107
- * Base versions of the transformed operations sets are updated accordingly. For example, assume that base versions are `4`
108
- * and there are `3` operations in `operationsA` and `5` operations in `operationsB`. Then:
109
- *
110
- * * transformed `operationsA` will start from base version `9` (`4` base version + `5` operations B),
111
- * * transformed `operationsB` will start from base version `7` (`4` base version + `3` operations A).
112
- *
113
- * If no operation was broken into two during transformation, then both sets will end up with an operation that bases on version `11`:
114
- *
115
- * * transformed `operationsA` start from `9` and there are `3` of them, so the last will have `baseVersion` equal to `11`,
116
- * * transformed `operationsB` start from `7` and there are `5` of them, so the last will have `baseVersion` equal to `11`.
117
- *
118
- * @param operationsA
119
- * @param operationsB
120
- * @param options Additional transformation options.
121
- * @param options.document Document which the operations change.
122
- * @param options.useRelations Whether during transformation relations should be used (used during undo for better conflict resolution).
123
- * @param options.padWithNoOps Whether additional {@link module:engine/model/operation/nooperation~NoOperation}s
124
- * should be added to the transformation results to force the same last base version for both transformed sets (in case
125
- * if some operations got broken into multiple operations during transformation).
126
- * @param options.forceWeakRemove If set to `false`, remove operation will be always stronger than move operation,
127
- * so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
128
- * @returns Transformation result.
129
- */
130
- export function transformOperationSets(operationsA, operationsB, options) {
131
- // Create new arrays so the originally passed arguments are not changed.
132
- // No need to clone operations, they are cloned as they are transformed.
133
- operationsA = operationsA.slice();
134
- operationsB = operationsB.slice();
135
- const contextFactory = new ContextFactory(options.document, options.useRelations, options.forceWeakRemove);
136
- contextFactory.setOriginalOperations(operationsA);
137
- contextFactory.setOriginalOperations(operationsB);
138
- const originalOperations = contextFactory.originalOperations;
139
- // If one of sets is empty there is simply nothing to transform, so return sets as they are.
140
- if (operationsA.length == 0 || operationsB.length == 0) {
141
- return { operationsA, operationsB, originalOperations };
142
- }
143
- //
144
- // Following is a description of transformation process:
145
- //
146
- // There are `operationsA` and `operationsB` to be transformed, both by both.
147
- //
148
- // So, suppose we have sets of two operations each: `operationsA` = `[ a1, a2 ]`, `operationsB` = `[ b1, b2 ]`.
149
- //
150
- // Remember, that we can only transform operations that base on the same context. We assert that `a1` and `b1` base on
151
- // the same context and we transform them. Then, we get `a1'` and `b1'`. `a2` bases on a context with `a1` -- `a2`
152
- // is an operation that followed `a1`. Similarly, `b2` bases on a context with `b1`.
153
- //
154
- // However, since `a1'` is a result of transformation by `b1`, `a1'` now also has a context with `b1`. This means that
155
- // we can safely transform `a1'` by `b2`. As we finish transforming `a1`, we also transformed all `operationsB`.
156
- // All `operationsB` also have context including `a1`. Now, we can properly transform `a2` by those operations.
157
- //
158
- // The transformation process can be visualized on a transformation diagram ("diamond diagram"):
159
- //
160
- // [the initial state]
161
- // [common for a1 and b1]
162
- //
163
- // *
164
- // / \
165
- // / \
166
- // b1 a1
167
- // / \
168
- // / \
169
- // * *
170
- // / \ / \
171
- // / \ / \
172
- // b2 a1' b1' a2
173
- // / \ / \
174
- // / \ / \
175
- // * * *
176
- // \ / \ /
177
- // \ / \ /
178
- // a1'' b2' a2' b1''
179
- // \ / \ /
180
- // \ / \ /
181
- // * *
182
- // \ /
183
- // \ /
184
- // a2'' b2''
185
- // \ /
186
- // \ /
187
- // *
188
- //
189
- // [the final state]
190
- //
191
- // The final state can be reached from the initial state by applying `a1`, `a2`, `b1''` and `b2''`, as well as by
192
- // applying `b1`, `b2`, `a1''`, `a2''`. Note how the operations get to a proper common state before each pair is
193
- // transformed.
194
- //
195
- // Another thing to consider is that an operation during transformation can be broken into multiple operations.
196
- // Suppose that `a1` * `b1` = `[ a11', a12' ]` (instead of `a1'` that we considered previously).
197
- //
198
- // In that case, we leave `a12'` for later and we continue transforming `a11'` until it is transformed by all `operationsB`
199
- // (in our case it is just `b2`). At this point, `b1` is transformed by "whole" `a1`, while `b2` is only transformed
200
- // by `a11'`. Similarly, `a12'` is only transformed by `b1`. This leads to a conclusion that we need to start transforming `a12'`
201
- // from the moment just after it was broken. So, `a12'` is transformed by `b2`. Now, "the whole" `a1` is transformed
202
- // by `operationsB`, while all `operationsB` are transformed by "the whole" `a1`. This means that we can continue with
203
- // following `operationsA` (in our case it is just `a2`).
204
- //
205
- // Of course, also `operationsB` can be broken. However, since we focus on transforming operation `a` to the end,
206
- // the only thing to do is to store both pieces of operation `b`, so that the next transformed operation `a` will
207
- // be transformed by both of them.
208
- //
209
- // *
210
- // / \
211
- // / \
212
- // / \
213
- // b1 a1
214
- // / \
215
- // / \
216
- // / \
217
- // * *
218
- // / \ / \
219
- // / a11' / \
220
- // / \ / \
221
- // b2 * b1' a2
222
- // / / \ / \
223
- // / / a12' / \
224
- // / / \ / \
225
- // * b2' * *
226
- // \ / / \ /
227
- // a11'' / b21'' \ /
228
- // \ / / \ /
229
- // * * a2' b1''
230
- // \ / \ \ /
231
- // a12'' b22''\ \ /
232
- // \ / \ \ /
233
- // * a2'' *
234
- // \ \ /
235
- // \ \ b21'''
236
- // \ \ /
237
- // a2''' *
238
- // \ /
239
- // \ b22'''
240
- // \ /
241
- // *
242
- //
243
- // Note, how `a1` is broken and transformed into `a11'` and `a12'`, while `b2'` got broken and transformed into `b21''` and `b22''`.
244
- //
245
- // Having all that on mind, here is an outline for the transformation process algorithm:
246
- //
247
- // 1. We have `operationsA` and `operationsB` array, which we dynamically update as the transformation process goes.
248
- //
249
- // 2. We take next (or first) operation from `operationsA` and check from which operation `b` we need to start transforming it.
250
- // All original `operationsA` are set to be transformed starting from the first operation `b`.
251
- //
252
- // 3. We take operations from `operationsB`, one by one, starting from the correct one, and transform operation `a`
253
- // by operation `b` (and vice versa). We update `operationsA` and `operationsB` by replacing the original operations
254
- // with the transformation results.
255
- //
256
- // 4. If operation is broken into multiple operations, we save all the new operations in the place of the
257
- // original operation.
258
- //
259
- // 5. Additionally, if operation `a` was broken, for the "new" operation, we remember from which operation `b` it should
260
- // be transformed by.
261
- //
262
- // 6. We continue transforming "current" operation `a` until it is transformed by all `operationsB`. Then, go to 2.
263
- // unless the last operation `a` was transformed.
264
- //
265
- // The actual implementation of the above algorithm is slightly different, as only one loop (while) is used.
266
- // The difference is that we have "current" `a` operation to transform and we store the index of the next `b` operation
267
- // to transform by. Each loop operates on two indexes then: index pointing to currently processed `a` operation and
268
- // index pointing to next `b` operation. Each loop is just one `a * b` + `b * a` transformation. After each loop
269
- // operation `b` index is updated. If all `b` operations were visited for the current `a` operation, we change
270
- // current `a` operation index to the next one.
271
- //
272
- // For each operation `a`, keeps information what is the index in `operationsB` from which the transformation should start.
273
- const nextTransformIndex = new WeakMap();
274
- // For all the original `operationsA`, set that they should be transformed starting from the first of `operationsB`.
275
- for (const op of operationsA) {
276
- nextTransformIndex.set(op, 0);
277
- }
278
- // Additional data that is used for some postprocessing after the main transformation process is done.
279
- const data = {
280
- nextBaseVersionA: operationsA[operationsA.length - 1].baseVersion + 1,
281
- nextBaseVersionB: operationsB[operationsB.length - 1].baseVersion + 1,
282
- originalOperationsACount: operationsA.length,
283
- originalOperationsBCount: operationsB.length
284
- };
285
- // Index of currently transformed operation `a`.
286
- let i = 0;
287
- // While not all `operationsA` are transformed...
288
- while (i < operationsA.length) {
289
- // Get "current" operation `a`.
290
- const opA = operationsA[i];
291
- // For the "current" operation `a`, get the index of the next operation `b` to transform by.
292
- const indexB = nextTransformIndex.get(opA);
293
- // If operation `a` was already transformed by every operation `b`, change "current" operation `a` to the next one.
294
- if (indexB == operationsB.length) {
295
- i++;
296
- continue;
297
- }
298
- const opB = operationsB[indexB];
299
- // Transform `a` by `b` and `b` by `a`.
300
- const newOpsA = transform(opA, opB, contextFactory.getContext(opA, opB, true));
301
- const newOpsB = transform(opB, opA, contextFactory.getContext(opB, opA, false));
302
- // As a result we get one or more `newOpsA` and one or more `newOpsB` operations.
303
- // Update contextual information about operations.
304
- contextFactory.updateRelation(opA, opB);
305
- contextFactory.setOriginalOperations(newOpsA, opA);
306
- contextFactory.setOriginalOperations(newOpsB, opB);
307
- // For new `a` operations, update their index of the next operation `b` to transform them by.
308
- //
309
- // This is needed even if there was only one result (`a` was not broken) because that information is used
310
- // at the beginning of this loop every time.
311
- for (const newOpA of newOpsA) {
312
- // Acknowledge, that operation `b` also might be broken into multiple operations.
313
- //
314
- // This is why we raise `indexB` not just by 1. If `newOpsB` are multiple operations, they will be
315
- // spliced in the place of `opB`. So we need to change `transformBy` accordingly, so that an operation won't
316
- // be transformed by the same operation (part of it) again.
317
- nextTransformIndex.set(newOpA, indexB + newOpsB.length);
318
- }
319
- // Update `operationsA` and `operationsB` with the transformed versions.
320
- operationsA.splice(i, 1, ...newOpsA);
321
- operationsB.splice(indexB, 1, ...newOpsB);
322
- }
323
- handlePartialMarkerOperations(operationsA);
324
- handlePartialMarkerOperations(operationsB);
325
- if (options.padWithNoOps) {
326
- // If no-operations padding is enabled, count how many extra `a` and `b` operations were generated.
327
- const brokenOperationsACount = operationsA.length - data.originalOperationsACount;
328
- const brokenOperationsBCount = operationsB.length - data.originalOperationsBCount;
329
- // Then, if that number is not the same, pad `operationsA` or `operationsB` with correct number of no-ops so
330
- // that the base versions are equalled.
331
- //
332
- // Note that only one array will be updated, as only one of those subtractions can be greater than zero.
333
- padWithNoOps(operationsA, brokenOperationsBCount - brokenOperationsACount);
334
- padWithNoOps(operationsB, brokenOperationsACount - brokenOperationsBCount);
335
- }
336
- // Finally, update base versions of transformed operations.
337
- updateBaseVersions(operationsA, data.nextBaseVersionB);
338
- updateBaseVersions(operationsB, data.nextBaseVersionA);
339
- return { operationsA, operationsB, originalOperations };
340
- }
341
- /**
342
- * Gathers additional data about operations processed during transformation. Can be used to obtain contextual information
343
- * about two operations that are about to be transformed. This contextual information can be used for better conflict resolution.
344
- */
345
- class ContextFactory {
346
- originalOperations;
347
- _history;
348
- _useRelations;
349
- _forceWeakRemove;
350
- _relations;
351
- /**
352
- * Creates `ContextFactory` instance.
353
- *
354
- * @param document Document which the operations change.
355
- * @param useRelations Whether during transformation relations should be used (used during undo for
356
- * better conflict resolution).
357
- * @param forceWeakRemove If set to `false`, remove operation will be always stronger than move operation,
358
- * so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
359
- */
360
- constructor(document, useRelations, forceWeakRemove = false) {
361
- // For each operation that is created during transformation process, we keep a reference to the original operation
362
- // which it comes from. The original operation works as a kind of "identifier". Every contextual information
363
- // gathered during transformation that we want to save for given operation, is actually saved for the original operation.
364
- // This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
365
- // gathered data through original operation reference.
366
- this.originalOperations = new Map();
367
- // `model.History` instance which information about undone operations will be taken from.
368
- this._history = document.history;
369
- // Whether additional context should be used.
370
- this._useRelations = useRelations;
371
- this._forceWeakRemove = !!forceWeakRemove;
372
- // Relations is a double-map structure (maps in map) where for two operations we store how those operations were related
373
- // to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
374
- // we keep relations between them.
375
- this._relations = new Map();
376
- }
377
- /**
378
- * Sets "original operation" for given operations.
379
- *
380
- * During transformation process, operations are cloned, then changed, then processed again, sometimes broken into two
381
- * or multiple operations. When gathering additional data it is important that all operations can be somehow linked
382
- * so a cloned and transformed "version" still kept track of the data assigned earlier to it.
383
- *
384
- * The original operation object will be used as such an universal linking id. Throughout the transformation process
385
- * all cloned operations will refer to "the original operation" when storing and reading additional data.
386
- *
387
- * If `takeFrom` is not set, each operation from `operations` array will be assigned itself as "the original operation".
388
- * This should be used as an initialization step.
389
- *
390
- * If `takeFrom` is set, each operation from `operations` will be assigned the same original operation as assigned
391
- * for `takeFrom` operation. This should be used to update original operations. It should be used in a way that
392
- * `operations` are the result of `takeFrom` transformation to ensure proper "original operation propagation".
393
- */
394
- setOriginalOperations(operations, takeFrom = null) {
395
- const originalOperation = takeFrom ? this.originalOperations.get(takeFrom) : null;
396
- for (const operation of operations) {
397
- this.originalOperations.set(operation, originalOperation || operation);
398
- }
399
- }
400
- /**
401
- * Saves a relation between operations `opA` and `opB`.
402
- *
403
- * Relations are then later used to help solve conflicts when operations are transformed.
404
- */
405
- updateRelation(opA, opB) {
406
- // The use of relations is described in a bigger detail in transformation functions.
407
- //
408
- // In brief, this function, for specified pairs of operation types, checks how positions defined in those operations relate.
409
- // Then those relations are saved. For example, for two move operations, it is saved if one of those operations target
410
- // position is before the other operation source position. This kind of information gives contextual information when
411
- // transformation is used during undo. Similar checks are done for other pairs of operations.
412
- //
413
- if (opA instanceof MoveOperation) {
414
- if (opB instanceof MergeOperation) {
415
- if (opA.targetPosition.isEqual(opB.sourcePosition) || opB.movedRange.containsPosition(opA.targetPosition)) {
416
- this._setRelation(opA, opB, 'insertAtSource');
417
- }
418
- else if (opA.targetPosition.isEqual(opB.deletionPosition)) {
419
- this._setRelation(opA, opB, 'insertBetween');
420
- }
421
- else if (opA.targetPosition.isAfter(opB.sourcePosition)) {
422
- this._setRelation(opA, opB, 'moveTargetAfter');
423
- }
424
- else if (opA.howMany > 1 && opA.sourcePosition.isEqual(opB.deletionPosition)) {
425
- this._setRelation(opA, opB, 'firstToMoveMerged');
426
- }
427
- else if (opA.howMany > 1 && opA.sourcePosition.getShiftedBy(opA.howMany - 1).isEqual(opB.deletionPosition)) {
428
- this._setRelation(opA, opB, 'lastToMoveMerged');
429
- }
430
- }
431
- else if (opB instanceof MoveOperation) {
432
- if (opA.targetPosition.isEqual(opB.sourcePosition) || opA.targetPosition.isBefore(opB.sourcePosition)) {
433
- this._setRelation(opA, opB, 'insertBefore');
434
- }
435
- else {
436
- this._setRelation(opA, opB, 'insertAfter');
437
- }
438
- }
439
- }
440
- else if (opA instanceof SplitOperation) {
441
- if (opB instanceof MergeOperation) {
442
- if (opA.splitPosition.isBefore(opB.sourcePosition)) {
443
- this._setRelation(opA, opB, 'splitBefore');
444
- }
445
- }
446
- else if (opB instanceof MoveOperation) {
447
- if (opA.splitPosition.isEqual(opB.sourcePosition) || opA.splitPosition.isBefore(opB.sourcePosition)) {
448
- this._setRelation(opA, opB, 'splitBefore');
449
- }
450
- else {
451
- const range = ModelRange._createFromPositionAndShift(opB.sourcePosition, opB.howMany);
452
- if (opA.splitPosition.hasSameParentAs(opB.sourcePosition) && range.containsPosition(opA.splitPosition)) {
453
- // TODO: Potential bug -- we are saving offset value directly and it is not later updated during OT.
454
- // TODO: This may cause a bug it here was an non-undone operation that may have impacted this offset.
455
- // TODO: Similar error was with MarkerOperation relations, where full path was saved and never updated.
456
- const howMany = range.end.offset - opA.splitPosition.offset;
457
- const offset = opA.splitPosition.offset - range.start.offset;
458
- this._setRelation(opA, opB, { howMany, offset });
459
- }
460
- }
461
- }
462
- }
463
- else if (opA instanceof MergeOperation) {
464
- if (opB instanceof MergeOperation) {
465
- if (!opA.targetPosition.isEqual(opB.sourcePosition)) {
466
- this._setRelation(opA, opB, 'mergeTargetNotMoved');
467
- }
468
- if (opA.sourcePosition.isEqual(opB.targetPosition)) {
469
- this._setRelation(opA, opB, 'mergeSourceNotMoved');
470
- }
471
- if (opA.sourcePosition.isEqual(opB.sourcePosition)) {
472
- this._setRelation(opA, opB, 'mergeSameElement');
473
- }
474
- }
475
- else if (opB instanceof SplitOperation) {
476
- if (opA.sourcePosition.isEqual(opB.splitPosition)) {
477
- this._setRelation(opA, opB, 'splitAtSource');
478
- }
479
- }
480
- else if (opB instanceof MoveOperation && opB.howMany > 0) {
481
- if (opA.sourcePosition.isEqual(opB.sourcePosition.getShiftedBy(opB.howMany))) {
482
- this._setRelation(opA, opB, 'mergeSourceAffected');
483
- }
484
- if (opA.targetPosition.isEqual(opB.sourcePosition)) {
485
- this._setRelation(opA, opB, 'mergeTargetWasBefore');
486
- }
487
- }
488
- }
489
- else if (opA instanceof MarkerOperation) {
490
- const markerRange = opA.newRange;
491
- if (!markerRange) {
492
- return;
493
- }
494
- if (opB instanceof MergeOperation) {
495
- const wasInLeftElement = markerRange.start.isEqual(opB.targetPosition);
496
- const wasStartBeforeMergedElement = markerRange.start.isEqual(opB.deletionPosition);
497
- const wasEndBeforeMergedElement = markerRange.end.isEqual(opB.deletionPosition);
498
- const wasInRightElement = markerRange.end.isEqual(opB.sourcePosition);
499
- if (wasInLeftElement || wasStartBeforeMergedElement || wasEndBeforeMergedElement || wasInRightElement) {
500
- this._setRelation(opA, opB, {
501
- wasInLeftElement,
502
- wasStartBeforeMergedElement,
503
- wasEndBeforeMergedElement,
504
- wasInRightElement
505
- });
506
- }
507
- }
508
- }
509
- }
510
- /**
511
- * Evaluates and returns contextual information about two given operations `opA` and `opB` which are about to be transformed.
512
- */
513
- getContext(opA, opB, aIsStrong) {
514
- return {
515
- aIsStrong,
516
- aWasUndone: this._wasUndone(opA),
517
- bWasUndone: this._wasUndone(opB),
518
- abRelation: this._useRelations ? this._getRelation(opA, opB) : null,
519
- baRelation: this._useRelations ? this._getRelation(opB, opA) : null,
520
- forceWeakRemove: this._forceWeakRemove
521
- };
522
- }
523
- /**
524
- * Returns whether given operation `op` has already been undone.
525
- *
526
- * Information whether an operation was undone gives more context when making a decision when two operations are in conflict.
527
- */
528
- _wasUndone(op) {
529
- // For `op`, get its original operation. After all, if `op` is a clone (or even transformed clone) of another
530
- // operation, literally `op` couldn't be undone. It was just generated. If anything, it was the operation it origins
531
- // from which was undone. So get that original operation.
532
- const originalOp = this.originalOperations.get(op);
533
- // And check with the document if the original operation was undone.
534
- return originalOp.wasUndone || this._history.isUndoneOperation(originalOp);
535
- }
536
- /**
537
- * Returns a relation between `opA` and an operation which is undone by `opB`. This can be `String` value if a relation
538
- * was set earlier or `null` if there was no relation between those operations.
539
- *
540
- * This is a little tricky to understand, so let's compare it to `ContextFactory#_wasUndone`.
541
- *
542
- * When `wasUndone( opB )` is used, we check if the `opB` has already been undone. It is obvious, that the
543
- * undoing operation must happen after the undone operation. So, essentially, we have `opB`, we take document history,
544
- * we look forward in the future and ask if in that future `opB` was undone.
545
- *
546
- * Relations is a backward process to `wasUndone()`.
547
- *
548
- * Long story short - using relations is asking what happened in the past. Looking back. This time we have an undoing
549
- * operation `opB` which has undone some other operation. When there is a transformation `opA` x `opB` and there is
550
- * a conflict to solve and `opB` is an undoing operation, we can look back in the history and see what was a relation
551
- * between `opA` and the operation which `opB` undone. Basing on that relation from the past, we can now make
552
- * a better decision when resolving a conflict between two operations, because we know more about the context of
553
- * those two operations.
554
- *
555
- * This is why this function does not return a relation directly between `opA` and `opB` because we need to look
556
- * back to search for a meaningful contextual information.
557
- */
558
- _getRelation(opA, opB) {
559
- // Get the original operation. Similarly as in `wasUndone()` it is used as an universal identifier for stored data.
560
- const origB = this.originalOperations.get(opB);
561
- const undoneB = this._history.getUndoneOperation(origB);
562
- // If `opB` is not undoing any operation, there is no relation.
563
- if (!undoneB) {
564
- return null;
565
- }
566
- const origA = this.originalOperations.get(opA);
567
- const relationsA = this._relations.get(origA);
568
- // Get all relations for `opA`, and check if there is a relation with `opB`-undone-counterpart. If so, return it.
569
- if (relationsA) {
570
- return relationsA.get(undoneB) || null;
571
- }
572
- return null;
573
- }
574
- /**
575
- * Helper function for `ContextFactory#updateRelations`.
576
- */
577
- _setRelation(opA, opB, relation) {
578
- // As always, setting is for original operations, not the clones/transformed operations.
579
- const origA = this.originalOperations.get(opA);
580
- const origB = this.originalOperations.get(opB);
581
- let relationsA = this._relations.get(origA);
582
- if (!relationsA) {
583
- relationsA = new Map();
584
- this._relations.set(origA, relationsA);
585
- }
586
- relationsA.set(origB, relation);
587
- }
588
- }
589
- /**
590
- * An utility function that updates {@link module:engine/model/operation/operation~Operation#baseVersion base versions}
591
- * of passed operations.
592
- *
593
- * The function simply sets `baseVersion` as a base version of the first passed operation and then increments it for
594
- * each following operation in `operations`.
595
- *
596
- * @param operations Operations to update.
597
- * @param baseVersion Base version to set for the first operation in `operations`.
598
- */
599
- function updateBaseVersions(operations, baseVersion) {
600
- for (const operation of operations) {
601
- operation.baseVersion = baseVersion++;
602
- }
603
- }
604
- /**
605
- * Adds `howMany` instances of {@link module:engine/model/operation/nooperation~NoOperation} to `operations` set.
606
- */
607
- function padWithNoOps(operations, howMany) {
608
- for (let i = 0; i < howMany; i++) {
609
- operations.push(new NoOperation(0));
610
- }
611
- }
612
- /**
613
- * Transformed operations set may include marker operations which were broken into multiple marker operations during transformation.
614
- * It represents marker range being broken into multiple pieces as the transformation was processed. Each partial marker operation is
615
- * a piece of the original marker range.
616
- *
617
- * These partial marker operations ("marker range pieces") should be "glued" together if, after transformations, the ranges ended up
618
- * next to each other.
619
- *
620
- * If the ranges did not end up next to each other, then partial marker operations should be discarded, as the marker range cannot
621
- * be broken into two pieces.
622
- *
623
- * There is always one "reference" marker operation (the original operation) and there may be some partial marker operations. Partial
624
- * marker operations have base version set to `-1`. If the `operations` set includes partial marker operations, then they are always
625
- * after the original marker operation.
626
- *
627
- * See also `MarkerOperation` x `MoveOperation` transformation.
628
- * See also https://github.com/ckeditor/ckeditor5/pull/17071.
629
- */
630
- function handlePartialMarkerOperations(operations) {
631
- const markerOps = new Map();
632
- for (let i = 0; i < operations.length; i++) {
633
- const op = operations[i];
634
- if (!(op instanceof MarkerOperation)) {
635
- continue;
636
- }
637
- if (op.baseVersion !== -1) {
638
- markerOps.set(op.name, {
639
- op,
640
- ranges: op.newRange ? [op.newRange] : []
641
- });
642
- }
643
- else {
644
- if (op.newRange) {
645
- // `markerOps.get( op.name )` must exist because original marker operation is always before partial marker operations.
646
- // If the original marker operation was changed to `NoOperation`, then the partial marker operations would be changed
647
- // to `NoOperation` as well, so this is not a case.
648
- const partialRanges = markerOps.get(op.name).ranges;
649
- // `refRange` is the range coming from the original operation.
650
- const refRange = partialRanges[0];
651
- // Filter out ranges that are inside the reference range.
652
- //
653
- // We don't need to combine them, as the reference range already includes `op.newRange`. At the same time, the method
654
- // `ModelRange._createFromRanges()` (which we use later on) prohibits passing intersecting ranges and works incorrectly when
655
- // such array of ranges is passed.
656
- //
657
- // Note, that there cannot be a situation where these ranges intersect but are not contained.
658
- //
659
- if (!refRange.containsRange(op.newRange, true)) {
660
- partialRanges.push(op.newRange);
661
- }
662
- }
663
- operations.splice(i, 1);
664
- i--;
665
- }
666
- }
667
- for (const { op, ranges } of markerOps.values()) {
668
- if (ranges.length) {
669
- op.newRange = ModelRange._createFromRanges(ranges);
670
- }
671
- else {
672
- op.newRange = null;
673
- }
674
- }
675
- }
676
- // -----------------------
677
- setTransformation(AttributeOperation, AttributeOperation, (a, b, context) => {
678
- // If operations in conflict, check if their ranges intersect and manage them properly.
679
- //
680
- // Operations can be in conflict only if:
681
- //
682
- // * their key is the same (they change the same attribute), and
683
- // * they are in the same parent (operations for ranges [ 1 ] - [ 3 ] and [ 2, 0 ] - [ 2, 5 ] change different
684
- // elements and can't be in conflict).
685
- if (a.key === b.key && a.range.start.hasSameParentAs(b.range.start)) {
686
- // First, we want to apply change to the part of a range that has not been changed by the other operation.
687
- const operations = a.range.getDifference(b.range).map(range => {
688
- return new AttributeOperation(range, a.key, a.oldValue, a.newValue, 0);
689
- });
690
- // Then we take care of the common part of ranges.
691
- const common = a.range.getIntersection(b.range);
692
- if (common) {
693
- // If this operation is more important, we also want to apply change to the part of the
694
- // original range that has already been changed by the other operation. Since that range
695
- // got changed we also have to update `oldValue`.
696
- if (context.aIsStrong) {
697
- operations.push(new AttributeOperation(common, b.key, b.newValue, a.newValue, 0));
698
- }
699
- }
700
- if (operations.length == 0) {
701
- return [new NoOperation(0)];
702
- }
703
- return operations;
704
- }
705
- else {
706
- // If operations don't conflict, simply return an array containing just a clone of this operation.
707
- return [a];
708
- }
709
- });
710
- setTransformation(AttributeOperation, InsertOperation, (a, b) => {
711
- // Case 1:
712
- //
713
- // The attribute operation range includes the position where nodes were inserted.
714
- // There are two possible scenarios: the inserted nodes were text and they should receive attributes or
715
- // the inserted nodes were elements and they should not receive attributes.
716
- //
717
- if (a.range.start.hasSameParentAs(b.position) && a.range.containsPosition(b.position)) {
718
- // If new nodes should not receive attributes, two separated ranges will be returned.
719
- // Otherwise, one expanded range will be returned.
720
- const range = a.range._getTransformedByInsertion(b.position, b.howMany, !b.shouldReceiveAttributes);
721
- const result = range.map(r => {
722
- return new AttributeOperation(r, a.key, a.oldValue, a.newValue, a.baseVersion);
723
- });
724
- if (b.shouldReceiveAttributes) {
725
- // `AttributeOperation#range` includes some newly inserted text.
726
- // The operation should also change the attribute of that text. An example:
727
- //
728
- // Bold should be applied on the following range:
729
- // <p>Fo[zb]ar</p>
730
- //
731
- // In meantime, new text is typed:
732
- // <p>Fozxxbar</p>
733
- //
734
- // Bold should be applied also on the new text:
735
- // <p>Fo[zxxb]ar</p>
736
- // <p>Fo<$text bold="true">zxxb</$text>ar</p>
737
- //
738
- // There is a special case to consider here to consider.
739
- //
740
- // Consider setting an attribute with multiple possible values, for example `highlight`. The inserted text might
741
- // have already an attribute value applied and the `oldValue` property of the attribute operation might be wrong:
742
- //
743
- // Attribute `highlight="yellow"` should be applied on the following range:
744
- // <p>Fo[zb]ar<p>
745
- //
746
- // In meantime, character `x` with `highlight="red"` is typed:
747
- // <p>Fo[z<$text highlight="red">x</$text>b]ar</p>
748
- //
749
- // In this case we cannot simply apply operation changing the attribute value from `null` to `"yellow"` for the whole range
750
- // because that would lead to an exception (`oldValue` is incorrect for `x`).
751
- //
752
- // We also cannot break the original range as this would mess up a scenario when there are multiple following
753
- // insert operations, because then only the first inserted character is included in those ranges:
754
- // <p>Fo[z][x][b]ar</p> --> <p>Fo[z][x]x[b]ar</p> --> <p>Fo[z][x]xx[b]ar</p>
755
- //
756
- // So, the attribute range needs be expanded, no matter what attributes are set on the inserted nodes:
757
- //
758
- // <p>Fo[z<$text highlight="red">x</$text>b]ar</p> <--- Change from `null` to `yellow`, throwing an exception.
759
- //
760
- // But before that operation would be applied, we will add an additional attribute operation that will change
761
- // attributes on the inserted nodes in a way which would make the original operation correct:
762
- //
763
- // <p>Fo[z{<$text highlight="red">}x</$text>b]ar</p> <--- Change range `{}` from `red` to `null`.
764
- // <p>Fo[zxb]ar</p> <--- Now change from `null` to `yellow` is completely fine.
765
- //
766
- // Generate complementary attribute operation. Be sure to add it before the original operation.
767
- const op = _getComplementaryAttributeOperations(b, a.key, a.oldValue);
768
- if (op) {
769
- result.unshift(op);
770
- }
771
- }
772
- // If nodes should not receive new attribute, we are done here.
773
- return result;
774
- }
775
- // If insert operation is not expanding the attribute operation range, simply transform the range.
776
- a.range = a.range._getTransformedByInsertion(b.position, b.howMany, false)[0];
777
- return [a];
778
- });
779
- /**
780
- * Helper function for `AttributeOperation` x `InsertOperation` (and reverse) transformation.
781
- *
782
- * For given `insertOperation` it checks the inserted node if it has an attribute `key` set to a value different
783
- * than `newValue`. If so, it generates an `AttributeOperation` which changes the value of `key` attribute to `newValue`.
784
- */
785
- function _getComplementaryAttributeOperations(insertOperation, key, newValue) {
786
- const nodes = insertOperation.nodes;
787
- // At the beginning we store the attribute value from the first node.
788
- const insertValue = nodes.getNode(0).getAttribute(key);
789
- if (insertValue == newValue) {
790
- return null;
791
- }
792
- const range = new ModelRange(insertOperation.position, insertOperation.position.getShiftedBy(insertOperation.howMany));
793
- return new AttributeOperation(range, key, insertValue, newValue, 0);
794
- }
795
- setTransformation(AttributeOperation, MergeOperation, (a, b) => {
796
- const ranges = [];
797
- // Case 1:
798
- //
799
- // Attribute change on the merged element. In this case, the merged element was moved to the graveyard.
800
- // An additional attribute operation that will change the (re)moved element needs to be generated.
801
- //
802
- if (a.range.start.hasSameParentAs(b.deletionPosition)) {
803
- if (a.range.containsPosition(b.deletionPosition) || a.range.start.isEqual(b.deletionPosition)) {
804
- ranges.push(ModelRange._createFromPositionAndShift(b.graveyardPosition, 1));
805
- }
806
- }
807
- const range = a.range._getTransformedByMergeOperation(b);
808
- // Do not add empty (collapsed) ranges to the result. `range` may be collapsed if it contained only the merged element.
809
- if (!range.isCollapsed) {
810
- ranges.push(range);
811
- }
812
- // Create `AttributeOperation`s out of the ranges.
813
- return ranges.map(range => {
814
- return new AttributeOperation(range, a.key, a.oldValue, a.newValue, a.baseVersion);
815
- });
816
- });
817
- setTransformation(AttributeOperation, MoveOperation, (a, b) => {
818
- const ranges = _breakRangeByMoveOperation(a.range, b);
819
- // Create `AttributeOperation`s out of the ranges.
820
- return ranges.map(range => new AttributeOperation(range, a.key, a.oldValue, a.newValue, a.baseVersion));
821
- });
822
- /**
823
- * Helper function for `AttributeOperation` x `MoveOperation` transformation.
824
- *
825
- * Takes the passed `range` and transforms it by move operation `moveOp` in a specific way. Only top-level nodes of `range`
826
- * are considered to be in the range. If move operation moves nodes deep from inside of the range, those nodes won't
827
- * be included in the result. In other words, top-level nodes of the ranges from the result are exactly the same as
828
- * top-level nodes of the original `range`.
829
- *
830
- * This is important for `AttributeOperation` because, for its range, it changes only the top-level nodes. So we need to
831
- * track only how those nodes have been affected by `MoveOperation`.
832
- */
833
- function _breakRangeByMoveOperation(range, moveOp) {
834
- const moveRange = ModelRange._createFromPositionAndShift(moveOp.sourcePosition, moveOp.howMany);
835
- // We are transforming `range` (original range) by `moveRange` (range moved by move operation). As usual when it comes to
836
- // transforming a ranges, we may have a common part of the ranges and we may have a difference part (zero to two ranges).
837
- let common = null;
838
- let difference = [];
839
- // Let's compare the ranges.
840
- if (moveRange.containsRange(range, true)) {
841
- // If the whole original range is moved, treat it whole as a common part. There's also no difference part.
842
- common = range;
843
- }
844
- else if (range.start.hasSameParentAs(moveRange.start)) {
845
- // If the ranges are "on the same level" (in the same parent) then move operation may move exactly those nodes
846
- // that are changed by the attribute operation. In this case we get common part and difference part in the usual way.
847
- difference = range.getDifference(moveRange);
848
- common = range.getIntersection(moveRange);
849
- }
850
- else {
851
- // In any other situation we assume that original range is different than move range, that is that move operation
852
- // moves other nodes that attribute operation change. Even if the moved range is deep inside in the original range.
853
- //
854
- // Note that this is different than in `.getIntersection` (we would get a common part in that case) and different
855
- // than `.getDifference` (we would get two ranges).
856
- difference = [range];
857
- }
858
- const result = [];
859
- // The default behaviour of `_getTransformedByMove` might get wrong results for difference part, though, so
860
- // we do it by hand.
861
- for (let diff of difference) {
862
- // First, transform the range by removing moved nodes. Since this is a difference, this is safe, `null` won't be returned
863
- // as the range is different than the moved range.
864
- diff = diff._getTransformedByDeletion(moveOp.sourcePosition, moveOp.howMany);
865
- // Transform also `targetPosition`.
866
- const targetPosition = moveOp.getMovedRangeStart();
867
- // Spread the range only if moved nodes are inserted only between the top-level nodes of the `diff` range.
868
- const spread = diff.start.hasSameParentAs(targetPosition);
869
- // Transform by insertion of moved nodes.
870
- const diffs = diff._getTransformedByInsertion(targetPosition, moveOp.howMany, spread);
871
- result.push(...diffs);
872
- }
873
- // Common part can be simply transformed by the move operation. This is because move operation will not target to
874
- // that common part (the operation would have to target inside its own moved range).
875
- if (common) {
876
- result.push(common._getTransformedByMove(moveOp.sourcePosition, moveOp.targetPosition, moveOp.howMany, false)[0]);
877
- }
878
- return result;
879
- }
880
- setTransformation(AttributeOperation, SplitOperation, (a, b) => {
881
- // Case 1:
882
- //
883
- // Split node is the last node in `AttributeOperation#range`.
884
- // `AttributeOperation#range` needs to be expanded to include the new (split) node.
885
- //
886
- // Attribute `type` to be changed to `numbered` but the `listItem` is split.
887
- // <listItem type="bulleted">foobar</listItem>
888
- //
889
- // After split:
890
- // <listItem type="bulleted">foo</listItem><listItem type="bulleted">bar</listItem>
891
- //
892
- // After attribute change:
893
- // <listItem type="numbered">foo</listItem><listItem type="numbered">foo</listItem>
894
- //
895
- if (a.range.end.isEqual(b.insertionPosition)) {
896
- if (!b.graveyardPosition) {
897
- a.range.end.offset++;
898
- }
899
- return [a];
900
- }
901
- // Case 2:
902
- //
903
- // Split position is inside `AttributeOperation#range`, at the same level, so the nodes to change are
904
- // not going to make a flat range.
905
- //
906
- // Content with range-to-change and split position:
907
- // <p>Fo[zb^a]r</p>
908
- //
909
- // After split:
910
- // <p>Fozb</p><p>ar</p>
911
- //
912
- // Make two separate ranges containing all nodes to change:
913
- // <p>Fo[zb]</p><p>[a]r</p>
914
- //
915
- if (a.range.start.hasSameParentAs(b.splitPosition) && a.range.containsPosition(b.splitPosition)) {
916
- const secondPart = a.clone();
917
- secondPart.range = new ModelRange(b.moveTargetPosition.clone(), a.range.end._getCombined(b.splitPosition, b.moveTargetPosition));
918
- a.range.end = b.splitPosition.clone();
919
- a.range.end.stickiness = 'toPrevious';
920
- return [a, secondPart];
921
- }
922
- // The default case.
923
- //
924
- a.range = a.range._getTransformedBySplitOperation(b);
925
- return [a];
926
- });
927
- setTransformation(InsertOperation, AttributeOperation, (a, b) => {
928
- const result = [a];
929
- // Case 1:
930
- //
931
- // The attribute operation range includes the position where nodes were inserted.
932
- // There are two possible scenarios: the inserted nodes were text and they should receive attributes or
933
- // the inserted nodes were elements and they should not receive attributes.
934
- //
935
- // This is a mirror scenario to the one described in `AttributeOperation` x `InsertOperation` transformation,
936
- // although this case is a little less complicated. In this case we simply need to change attributes of the
937
- // inserted nodes and that's it.
938
- //
939
- if (a.shouldReceiveAttributes && a.position.hasSameParentAs(b.range.start) && b.range.containsPosition(a.position)) {
940
- const op = _getComplementaryAttributeOperations(a, b.key, b.newValue);
941
- if (op) {
942
- result.push(op);
943
- }
944
- }
945
- // The default case is: do nothing.
946
- // `AttributeOperation` does not change the model tree structure so `InsertOperation` does not need to be changed.
947
- //
948
- return result;
949
- });
950
- setTransformation(InsertOperation, InsertOperation, (a, b, context) => {
951
- // Case 1:
952
- //
953
- // Two insert operations insert nodes at the same position. Since they are the same, it needs to be decided
954
- // what will be the order of inserted nodes. However, there is no additional information to help in that
955
- // decision. Also, when `b` will be transformed by `a`, the same order must be maintained.
956
- //
957
- // To achieve that, we will check if the operation is strong.
958
- // If it is, it won't get transformed. If it is not, it will be moved.
959
- //
960
- if (a.position.isEqual(b.position) && context.aIsStrong) {
961
- return [a];
962
- }
963
- // The default case.
964
- //
965
- a.position = a.position._getTransformedByInsertOperation(b);
966
- return [a];
967
- });
968
- setTransformation(InsertOperation, MoveOperation, (a, b) => {
969
- // The default case.
970
- //
971
- a.position = a.position._getTransformedByMoveOperation(b);
972
- return [a];
973
- });
974
- setTransformation(InsertOperation, SplitOperation, (a, b) => {
975
- // The default case.
976
- //
977
- a.position = a.position._getTransformedBySplitOperation(b);
978
- return [a];
979
- });
980
- setTransformation(InsertOperation, MergeOperation, (a, b) => {
981
- a.position = a.position._getTransformedByMergeOperation(b);
982
- return [a];
983
- });
984
- // -----------------------
985
- setTransformation(MarkerOperation, InsertOperation, (a, b) => {
986
- if (a.oldRange) {
987
- a.oldRange = a.oldRange._getTransformedByInsertOperation(b)[0];
988
- }
989
- if (a.newRange) {
990
- a.newRange = a.newRange._getTransformedByInsertOperation(b)[0];
991
- }
992
- return [a];
993
- });
994
- setTransformation(MarkerOperation, MarkerOperation, (a, b, context) => {
995
- if (a.name == b.name) {
996
- if (context.aIsStrong) {
997
- a.oldRange = b.newRange ? b.newRange.clone() : null;
998
- }
999
- else {
1000
- return [new NoOperation(0)];
1001
- }
1002
- }
1003
- return [a];
1004
- });
1005
- setTransformation(MarkerOperation, MergeOperation, (a, b) => {
1006
- if (a.oldRange) {
1007
- a.oldRange = a.oldRange._getTransformedByMergeOperation(b);
1008
- }
1009
- if (a.newRange) {
1010
- a.newRange = a.newRange._getTransformedByMergeOperation(b);
1011
- }
1012
- return [a];
1013
- });
1014
- setTransformation(MarkerOperation, MoveOperation, (a, b) => {
1015
- const result = [a];
1016
- if (a.oldRange) {
1017
- a.oldRange = ModelRange._createFromRanges(a.oldRange._getTransformedByMoveOperation(b));
1018
- }
1019
- if (a.newRange) {
1020
- // In many simple cases the marker range will be kept integral after the transformation. For example, if some nodes
1021
- // were inserted before the range, or into the range, then the marker range is not broken into two.
1022
- //
1023
- // However, if some nodes are taken out of the range and moved somewhere else, or are moved into the range, then the marker
1024
- // range is "broken" into two or three pieces, and these pieces must be transformed and updated separately.
1025
- //
1026
- // When the marker range is transformed by move operation, as a result we get an array with one (simple case) or multiple
1027
- // ("broken range" case) ranges.
1028
- const ranges = a.newRange._getTransformedByMoveOperation(b);
1029
- a.newRange = ranges[0];
1030
- // If there are multiple ranges, we will create separate marker operations for each piece of the original marker range.
1031
- // Since they will be marker operations, they will be processed through the transformation process.
1032
- //
1033
- // However, we cannot create multiple ranges for the same marker (for the same marker name). A marker has only one range.
1034
- // So, we cannot really have multiple marker operations for the same marker. We will keep the track of the separate marker
1035
- // operations to see, if after all transformations, the marker pieces are next to each other or not. If so, we will glue
1036
- // them together to the original marker operation (`a`). If not, we will discard them. These extra operations will never
1037
- // be executed, as they will only exist temporarily during the transformation process.
1038
- //
1039
- // We will call these additional marker operations "partial marker operations" and we will mark them with negative base version.
1040
- //
1041
- // See also `handlePartialMarkerOperations()`.
1042
- // See also https://github.com/ckeditor/ckeditor5/pull/17071.
1043
- //
1044
- for (let i = 1; i < ranges.length; i++) {
1045
- const op = a.clone();
1046
- op.oldRange = null;
1047
- op.newRange = ranges[i];
1048
- op.baseVersion = -1;
1049
- result.push(op);
1050
- }
1051
- }
1052
- return result;
1053
- });
1054
- setTransformation(MarkerOperation, SplitOperation, (a, b, context) => {
1055
- if (a.oldRange) {
1056
- a.oldRange = a.oldRange._getTransformedBySplitOperation(b);
1057
- }
1058
- if (a.newRange) {
1059
- if (context.abRelation) {
1060
- // If we have context, it means that there was previously a merge operation which transformed this marker operation, and that
1061
- // merge operation was then undone, and this split operation should reverse the marker to state before the merge operation.
1062
- const aNewRange = a.newRange._getTransformedBySplitOperation(b);
1063
- if (a.newRange.start.isEqual(b.splitPosition)) {
1064
- // If marker range start is same as split position, we need to decide where marker start should be placed, as there
1065
- // are three possible options.
1066
- //
1067
- if (context.abRelation.wasStartBeforeMergedElement) {
1068
- // If the marker start was initially before the merged element, move it back to that place.
1069
- //
1070
- // <p>Foo</p>[<p>Bar]</p> -- merge -> <p>Foo[Bar]</p> -- default split -> <p>Foo</p><p>[Bar]</p>
1071
- // <p>Foo</p>[<p>Bar]</p> -- merge -> <p>Foo[Bar]</p> --- fixed split --> <p>Foo</p>[<p>Bar]</p>
1072
- //
1073
- a.newRange.start = ModelPosition._createAt(b.insertionPosition);
1074
- }
1075
- else if (context.abRelation.wasInLeftElement) {
1076
- // If the marker start was initially in the "left" element, keep the start position there.
1077
- //
1078
- // <p>Foo[</p><p>Bar]</p> -- merge -> <p>Foo[Bar]</p> -- default split -> <p>Foo</p><p>[Bar]</p>
1079
- // <p>Foo[</p><p>Bar]</p> -- merge -> <p>Foo[Bar]</p> --- fixed split --> <p>Foo[</p><p>Bar]</p>
1080
- //
1081
- a.newRange.start = ModelPosition._createAt(a.newRange.start);
1082
- }
1083
- else {
1084
- // Finally, the start position must have been at the beginning of the "right" (merged) element.
1085
- // In this case, move it back to the "right" element.
1086
- //
1087
- // Note, that this what the default transformation (`_getTransformedBySplitOperation()`) does, BUT ONLY for
1088
- // a non-collapsed marker. For a collapsed marker, by default, the whole marker is kept at the split position,
1089
- // which would be incorrect. This is why this case is needed, and why we don't use `aNewRange.start` here.
1090
- //
1091
- // <p>Foo</p><p>[]Bar</p> -- merge -> <p>Foo[]Bar</p> -- default split -> <p>Foo[]</p><p>Bar</p>
1092
- // <p>Foo</p><p>[]Bar</p> -- merge -> <p>Foo[]Bar</p> --- fixed split --> <p>Foo</p><p>[]Bar</p>
1093
- //
1094
- a.newRange.start = ModelPosition._createAt(b.moveTargetPosition);
1095
- }
1096
- }
1097
- else {
1098
- // If marker range start is not the same as split position, simply use the default transformation, as there is no
1099
- // ambiguity in this case.
1100
- a.newRange.start = aNewRange.start;
1101
- }
1102
- if (a.newRange.end.isEqual(b.splitPosition)) {
1103
- // If marker range end is same as split position, we need to decide where marker end should be placed, as there
1104
- // are three possible options.
1105
- //
1106
- if (a.newRange.end.isEqual(b.splitPosition) && context.abRelation.wasEndBeforeMergedElement) {
1107
- // If the marker end was initially before the merged element, move it back to that place.
1108
- //
1109
- // <p>[Foo</p>]<p>Bar</p> -- merge -> <p>[Foo]Bar</p> -- default split -> <p>[Foo]</p><p>Bar</p>
1110
- // <p>[Foo</p>]<p>Bar</p> -- merge -> <p>[Foo]Bar</p> --- fixed split --> <p>[Foo</p>]<p>Bar</p>
1111
- //
1112
- a.newRange.end = ModelPosition._createAt(b.insertionPosition);
1113
- }
1114
- else if (context.abRelation.wasInRightElement) {
1115
- // If the marker was initially in the "right" element, keep the end position there.
1116
- //
1117
- // <p>[Foo</p><p>]Bar</p> -- merge -> <p>[Foo]Bar</p> -- default split -> <p>[Foo]</p><p>Bar</p>
1118
- // <p>[Foo</p><p>]Bar</p> -- merge -> <p>[Foo]Bar</p> --- fixed split --> <p>[Foo</p><p>]Bar</p>
1119
- //
1120
- a.newRange.end = ModelPosition._createAt(b.moveTargetPosition);
1121
- }
1122
- else {
1123
- // Finally, the end position must have been at the end of the "left" (merged) element.
1124
- // In this case, keep it where it is.
1125
- //
1126
- // Note, that this is what the default transformation does, so we could use `aNewRange.end`, but this is more clear.
1127
- //
1128
- a.newRange.end = ModelPosition._createAt(a.newRange.end);
1129
- }
1130
- }
1131
- else {
1132
- // If marker range end is not the same as split position, simply use the default transformation, as there is no
1133
- // ambiguity in this case.
1134
- a.newRange.end = aNewRange.end;
1135
- }
1136
- return [a];
1137
- }
1138
- a.newRange = a.newRange._getTransformedBySplitOperation(b);
1139
- }
1140
- return [a];
1141
- });
1142
- // -----------------------
1143
- setTransformation(MergeOperation, InsertOperation, (a, b) => {
1144
- if (a.sourcePosition.hasSameParentAs(b.position)) {
1145
- a.howMany += b.howMany;
1146
- }
1147
- a.sourcePosition = a.sourcePosition._getTransformedByInsertOperation(b);
1148
- a.targetPosition = a.targetPosition._getTransformedByInsertOperation(b);
1149
- return [a];
1150
- });
1151
- setTransformation(MergeOperation, MergeOperation, (a, b, context) => {
1152
- // Case 1:
1153
- //
1154
- // Same merge operations.
1155
- //
1156
- // Both operations have same source and target positions. So the element already got merged and there is
1157
- // theoretically nothing to do.
1158
- //
1159
- if (a.sourcePosition.isEqual(b.sourcePosition) && a.targetPosition.isEqual(b.targetPosition)) {
1160
- // There are two ways that we can provide a do-nothing operation.
1161
- //
1162
- // First is simply a NoOperation instance. We will use it if `b` operation was not undone.
1163
- //
1164
- // Second is a merge operation that has the source operation in the merged element - in the graveyard -
1165
- // same target position and `howMany` equal to `0`. So it is basically merging an empty element from graveyard
1166
- // which is almost the same as NoOperation.
1167
- //
1168
- // This way the merge operation can be later transformed by split operation
1169
- // to provide correct undo. This will be used if `b` operation was undone (only then it is correct).
1170
- //
1171
- if (!context.bWasUndone) {
1172
- return [new NoOperation(0)];
1173
- }
1174
- else {
1175
- const path = b.graveyardPosition.path.slice();
1176
- path.push(0);
1177
- a.sourcePosition = new ModelPosition(b.graveyardPosition.root, path);
1178
- a.howMany = 0;
1179
- return [a];
1180
- }
1181
- }
1182
- // Case 2:
1183
- //
1184
- // Same merge source position but different target position.
1185
- //
1186
- // This can happen during collaboration. For example, if one client merged a paragraph to the previous paragraph
1187
- // and the other person removed that paragraph and merged the same paragraph to something before:
1188
- //
1189
- // Client A:
1190
- // <p>Foo</p><p>Bar</p><p>[]Xyz</p>
1191
- // <p>Foo</p><p>BarXyz</p>
1192
- //
1193
- // Client B:
1194
- // <p>Foo</p>[<p>Bar</p>]<p>Xyz</p>
1195
- // <p>Foo</p><p>[]Xyz</p>
1196
- // <p>FooXyz</p>
1197
- //
1198
- // In this case we need to decide where finally "Xyz" will land:
1199
- //
1200
- // <p>FooXyz</p> graveyard: <p>Bar</p>
1201
- // <p>Foo</p> graveyard: <p>BarXyz</p>
1202
- //
1203
- // Let's move it in a way so that a merge operation that does not target to graveyard is more important so that
1204
- // nodes does not end up in the graveyard. It makes sense. Both for Client A and for Client B "Xyz" finally did not
1205
- // end up in the graveyard (see above).
1206
- //
1207
- // If neither or both operations point to graveyard, then let `aIsStrong` decide.
1208
- //
1209
- if (a.sourcePosition.isEqual(b.sourcePosition) && !a.targetPosition.isEqual(b.targetPosition) &&
1210
- !context.bWasUndone && context.abRelation != 'splitAtSource') {
1211
- const aToGraveyard = a.targetPosition.root.rootName == '$graveyard';
1212
- const bToGraveyard = b.targetPosition.root.rootName == '$graveyard';
1213
- // If `aIsWeak` it means that `a` points to graveyard while `b` doesn't. Don't move nodes then.
1214
- const aIsWeak = aToGraveyard && !bToGraveyard;
1215
- // If `bIsWeak` it means that `b` points to graveyard while `a` doesn't. Force moving nodes then.
1216
- const bIsWeak = bToGraveyard && !aToGraveyard;
1217
- // Force move if `b` is weak or neither operation is weak but `a` is stronger through `context.aIsStrong`.
1218
- const forceMove = bIsWeak || (!aIsWeak && context.aIsStrong);
1219
- if (forceMove) {
1220
- const sourcePosition = b.targetPosition._getTransformedByMergeOperation(b);
1221
- const targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
1222
- return [new MoveOperation(sourcePosition, a.howMany, targetPosition, 0)];
1223
- }
1224
- else {
1225
- return [new NoOperation(0)];
1226
- }
1227
- }
1228
- // The default case.
1229
- // TODO: Possibly, there's a missing case for same `targetPosition` but different `sourcePosition`.
1230
- //
1231
- if (a.sourcePosition.hasSameParentAs(b.targetPosition)) {
1232
- a.howMany += b.howMany;
1233
- }
1234
- a.sourcePosition = a.sourcePosition._getTransformedByMergeOperation(b);
1235
- a.targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
1236
- // Handle positions in graveyard.
1237
- // If graveyard positions are same and `a` operation is strong - do not transform.
1238
- if (!a.graveyardPosition.isEqual(b.graveyardPosition) || !context.aIsStrong) {
1239
- a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation(b);
1240
- }
1241
- return [a];
1242
- });
1243
- setTransformation(MergeOperation, MoveOperation, (a, b, context) => {
1244
- // Case 1:
1245
- //
1246
- // The element to merge got removed.
1247
- //
1248
- // Merge operation does support merging elements which are not siblings. So it would not be a problem
1249
- // from technical point of view. However, if the element was removed, the intention of the user deleting it
1250
- // was to have it all deleted, together with its children. From user experience point of view, moving back the
1251
- // removed nodes might be unexpected. This means that in this scenario we will block the merging.
1252
- //
1253
- // The exception to this rule would be if the remove operation was later undone.
1254
- //
1255
- const removedRange = ModelRange._createFromPositionAndShift(b.sourcePosition, b.howMany);
1256
- if (b.type == 'remove' && !context.bWasUndone) {
1257
- if (a.deletionPosition.hasSameParentAs(b.sourcePosition) && removedRange.containsPosition(a.sourcePosition)) {
1258
- return [new NoOperation(0)];
1259
- }
1260
- }
1261
- // In most cases we want `sourcePosition` to stick to previous and `targetPosition` to stick to next.
1262
- // Usually, `sourcePosition` is at the beginning of the merged element and `targetPosition` is at the end of the merge-target element.
1263
- //
1264
- // However, `sourcePosition` and `targetPosition` may end up in the middle of an element due to some OT magic that happens during undo.
1265
- // It is expected and used in `MergeOperation` x `SplitOperation` transformation.
1266
- //
1267
- // But when these positions are in the middle, it messes up the regular `MergeOperation` x `MoveOperation` transformation because
1268
- // these positions may "follow" some moved elements. And we want them stick in the original elements.
1269
- //
1270
- // This is why we add two extra cases: (1) and (2).
1271
- //
1272
- // But after this `MergeOperation` is transformed by "this" move (which is undone), we also need to define extra cases for
1273
- // the operation undoing previous move. These are (3) and (4).
1274
- //
1275
- // (1). Note that this case is also added to `updateRelations()` and sets `mergeSourceAffected` relation.
1276
- //
1277
- // [] is move operation, } is merge source position (sticks to previous by default):
1278
- // <p>A[b]}c</p> -> <p>A}c</p>
1279
- //
1280
- if (b.sourcePosition.getShiftedBy(b.howMany).isEqual(a.sourcePosition)) {
1281
- a.sourcePosition.stickiness = 'toNone';
1282
- }
1283
- // (3). This is the transformation for undoing operation of the above case.
1284
- //
1285
- // [] is move operation, } is merge source position (sticks to previous by default):
1286
- // <p>A}c</p> -> <p>A[b]}c</p> (instead of <p>A}[b]c</p>)
1287
- //
1288
- else if (b.targetPosition.isEqual(a.sourcePosition) && context.abRelation == 'mergeSourceAffected') {
1289
- a.sourcePosition.stickiness = 'toNext';
1290
- }
1291
- // (2). Note that this case is also added to `updateRelations()` and sets `mergeTargetWasBefore` relation.
1292
- //
1293
- // [] is move operation, { is merge target position (sticks to next by default):
1294
- // <p>A{[b]c</p> -> <p>A{c</p>
1295
- //
1296
- else if (b.sourcePosition.isEqual(a.targetPosition)) {
1297
- a.targetPosition.stickiness = 'toNone';
1298
- a.howMany -= b.howMany;
1299
- }
1300
- // (4). This is the transformation for undoing operation of the above case.
1301
- //
1302
- // [] is move operation, { is merge target position (sticks to next by default):
1303
- // <p>A{c</p> -> <p>A{[b]c</p> (instead of <p>A[b]{c</p>)
1304
- //
1305
- else if (b.targetPosition.isEqual(a.targetPosition) && context.abRelation == 'mergeTargetWasBefore') {
1306
- a.targetPosition.stickiness = 'toPrevious';
1307
- a.howMany += b.howMany;
1308
- }
1309
- // The default case.
1310
- else {
1311
- if (a.sourcePosition.hasSameParentAs(b.targetPosition)) {
1312
- a.howMany += b.howMany;
1313
- }
1314
- if (a.sourcePosition.hasSameParentAs(b.sourcePosition)) {
1315
- a.howMany -= b.howMany;
1316
- }
1317
- }
1318
- a.sourcePosition = a.sourcePosition._getTransformedByMoveOperation(b);
1319
- a.targetPosition = a.targetPosition._getTransformedByMoveOperation(b);
1320
- // After transformations are done, make sure to revert stickiness in case if (1) - (4) scenario happened.
1321
- a.sourcePosition.stickiness = 'toPrevious';
1322
- a.targetPosition.stickiness = 'toNext';
1323
- // `MergeOperation` graveyard position is like `MoveOperation` target position. It is a position where element(s) will
1324
- // be moved. Like in other similar cases, we need to consider the scenario when those positions are same.
1325
- // Here, we will treat `MergeOperation` like it is always strong (see `InsertOperation` x `InsertOperation` for comparison).
1326
- // This means that we won't transform graveyard position if it is equal to move operation target position.
1327
- if (!a.graveyardPosition.isEqual(b.targetPosition)) {
1328
- a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation(b);
1329
- }
1330
- return [a];
1331
- });
1332
- setTransformation(MergeOperation, SplitOperation, (a, b, context) => {
1333
- if (b.graveyardPosition) {
1334
- // If `b` operation defines graveyard position, a node from graveyard will be moved. This means that we need to
1335
- // transform `a.graveyardPosition` accordingly.
1336
- a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion(b.graveyardPosition, 1);
1337
- // This is a scenario foreseen in `MergeOperation` x `MergeOperation`, with two identical merge operations.
1338
- //
1339
- // So, there was `MergeOperation` x `MergeOperation` transformation earlier. Now, `a` is a merge operation which
1340
- // source position is in graveyard. Interestingly, split operation wants to use the node to be merged by `a`. This
1341
- // means that `b` is undoing that merge operation from earlier, which caused `a` to be in graveyard.
1342
- //
1343
- // If that's the case, at this point, we will only "fix" `a.howMany`. It was earlier set to `0` in
1344
- // `MergeOperation` x `MergeOperation` transformation. Later transformations in this function will change other
1345
- // properties.
1346
- //
1347
- if (a.deletionPosition.isEqual(b.graveyardPosition)) {
1348
- a.howMany = b.howMany;
1349
- }
1350
- }
1351
- // Case 1:
1352
- //
1353
- // Merge operation moves nodes to the place where split happens.
1354
- //
1355
- // This is a classic situation when there are two paragraphs, and there is a split (enter) at the end of the first
1356
- // paragraph and there is a merge (delete) at the beginning of the second paragraph:
1357
- //
1358
- // <p>Foo{}</p><p>[]Bar</p>.
1359
- //
1360
- // User A presses enter after `Foo`, while User B presses backspace before `Bar`. It is intuitive that after both operations, the
1361
- // editor state should stay the same.
1362
- //
1363
- // State after split:
1364
- // <p>Foo</p><p></p><p>[]Bar</p>
1365
- //
1366
- // When this happens, `Bar` should be merged to the newly created paragraph, to maintain the editor state:
1367
- // <p>Foo</p><p>Bar</p>
1368
- //
1369
- // Another option is to merge into the original paragraph `Foo`, according to the `targetPosition`. This results in an incorrect state:
1370
- // <p>FooBar</p><p></p>
1371
- //
1372
- // Also, consider an example where User A also writes something in the new paragraph:
1373
- // <p>Foo</p><p>Xyz</p><p>[]Bar</p>
1374
- //
1375
- // In this case it is clear that merge should happen into `[ 1, 3 ]` not into `[ 0, 3 ]`. It first has to be transformed to `[ 1, 0 ]`,
1376
- // and then transformed be insertion into `[ 1, 3 ]`.
1377
- //
1378
- // So, usually we want to move `targetPosition` to the new paragraph when it is same as split position. This is how it is handled
1379
- // in the default transformation (`_getTransformedBySplitOperation()`). We don't need a special case for this.
1380
- //
1381
- // However, there are two exceptions, when we **do not** want to transform `targetPosition`, and we need a special case then.
1382
- //
1383
- // These exceptions happen only if undo is involved. During OT, above presented case (`<p>Foo{}</p><p>[]Bar</p>`) is the only way
1384
- // how `SplitOperation#splitPosition` and `MergeOperation#targetPosition` can be the same.
1385
- //
1386
- // First exception is when the element to merge is in the graveyard and split operation uses it. In that case
1387
- // if target position would be transformed, the merge operation would target at the source position:
1388
- //
1389
- // root: <p>Foo[]</p> graveyard: <p></p>
1390
- //
1391
- // SplitOperation: root [ 0, 3 ] using graveyard [ 0 ] (howMany = 0)
1392
- // MergeOperation: graveyard [ 0, 0 ] -> root [ 0, 3 ] (howMany = 0)
1393
- //
1394
- // Since split operation moves the graveyard element back to the root (to path `[ 1 ]`), the merge operation `sourcePosition` changes.
1395
- // After split we have: `<p>Foo</p><p></p>`, so `sourcePosition` is `[ 1, 0 ]`. But if `targetPosition` is transformed, then it
1396
- // also becomes `[ 1, 0 ]`. In this case, we want to keep the `targetPosition` as it was.
1397
- //
1398
- // Second exception is connected strictly with undo relations. If this `MergeOperation` was earlier transformed by
1399
- // `MergeOperation` and we stored an information that earlier the target position was not affected, then here, when transforming by
1400
- // `SplitOperation` we are not going to change it as well.
1401
- //
1402
- // For these two cases we will only transform `sourcePosition` and return early.
1403
- //
1404
- // Note, that earlier there was also third special case here. `targetPosition` was not transformed, if it pointed into the middle of
1405
- // target element, not into its end (as usual). This can also happen only with undo involved. However, it wasn't always a correct
1406
- // solution, as in some cases we actually wanted to transform `targetPosition`. Also, this case usually happens together with the second
1407
- // case described above. There is only one scenario that we have in our unit tests, where this third case happened without second case.
1408
- // However, this scenario went fine no matter if we transformed `targetPosition` or not. That's because this happened in the middle
1409
- // of transformation process and the operation was correctly transformed later on.
1410
- //
1411
- if (a.targetPosition.isEqual(b.splitPosition)) {
1412
- const mergeSplittingElement = b.graveyardPosition && a.deletionPosition.isEqual(b.graveyardPosition);
1413
- if (mergeSplittingElement || context.abRelation == 'mergeTargetNotMoved') {
1414
- a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation(b);
1415
- return [a];
1416
- }
1417
- }
1418
- // Case 2:
1419
- //
1420
- // Merge source is at the same position as split position. This sometimes happen, mostly during undo.
1421
- // The decision here is mostly to choose whether merge source position should stay where it is (so it will be at the end of the
1422
- // split element) or should be move to the beginning of the new element.
1423
- //
1424
- if (a.sourcePosition.isEqual(b.splitPosition)) {
1425
- // Use context to check if `SplitOperation` is not undoing a merge operation, that didn't change the `a` operation.
1426
- // This scenario happens the undone merge operation moved nodes at the source position of `a` operation.
1427
- // In that case `a` operation source position should stay where it is.
1428
- if (context.abRelation == 'mergeSourceNotMoved') {
1429
- a.howMany = 0;
1430
- a.targetPosition = a.targetPosition._getTransformedBySplitOperation(b);
1431
- return [a];
1432
- }
1433
- // This merge operation might have been earlier transformed by a merge operation which both merged the same element.
1434
- // See that case in `MergeOperation` x `MergeOperation` transformation. In that scenario, if the merge operation has been undone,
1435
- // the special case is not applied.
1436
- //
1437
- // Now, the merge operation is transformed by the split which has undone that previous merge operation.
1438
- // So now we are fixing situation which was skipped in `MergeOperation` x `MergeOperation` case.
1439
- //
1440
- if (context.abRelation == 'mergeSameElement' || a.sourcePosition.offset > 0) {
1441
- a.sourcePosition = b.moveTargetPosition.clone();
1442
- a.targetPosition = a.targetPosition._getTransformedBySplitOperation(b);
1443
- return [a];
1444
- }
1445
- }
1446
- // The default case.
1447
- //
1448
- if (a.sourcePosition.hasSameParentAs(b.splitPosition)) {
1449
- a.howMany = b.splitPosition.offset;
1450
- }
1451
- a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation(b);
1452
- a.targetPosition = a.targetPosition._getTransformedBySplitOperation(b);
1453
- return [a];
1454
- });
1455
- // -----------------------
1456
- setTransformation(MoveOperation, InsertOperation, (a, b) => {
1457
- const moveRange = ModelRange._createFromPositionAndShift(a.sourcePosition, a.howMany);
1458
- const transformed = moveRange._getTransformedByInsertOperation(b, false)[0];
1459
- a.sourcePosition = transformed.start;
1460
- a.howMany = transformed.end.offset - transformed.start.offset;
1461
- // See `InsertOperation` x `MoveOperation` transformation for details on this case.
1462
- //
1463
- // In summary, both operations point to the same place, so the order of nodes needs to be decided.
1464
- // `MoveOperation` is considered weaker, so it is always transformed, unless there was a certain relation
1465
- // between operations.
1466
- //
1467
- if (!a.targetPosition.isEqual(b.position)) {
1468
- a.targetPosition = a.targetPosition._getTransformedByInsertOperation(b);
1469
- }
1470
- return [a];
1471
- });
1472
- setTransformation(MoveOperation, MoveOperation, (a, b, context) => {
1473
- //
1474
- // Setting and evaluating some variables that will be used in special cases and default algorithm.
1475
- //
1476
- // Create ranges from `MoveOperations` properties.
1477
- const rangeA = ModelRange._createFromPositionAndShift(a.sourcePosition, a.howMany);
1478
- const rangeB = ModelRange._createFromPositionAndShift(b.sourcePosition, b.howMany);
1479
- // Assign `context.aIsStrong` to a different variable, because the value may change during execution of
1480
- // this algorithm and we do not want to override original `context.aIsStrong` that will be used in later transformations.
1481
- let aIsStrong = context.aIsStrong;
1482
- // This will be used to decide the order of nodes if both operations target at the same position.
1483
- // By default, use strong/weak operation mechanism.
1484
- let insertBefore = !context.aIsStrong;
1485
- // If the relation is set, then use it to decide nodes order.
1486
- if (context.abRelation == 'insertBefore' || context.baRelation == 'insertAfter') {
1487
- insertBefore = true;
1488
- }
1489
- else if (context.abRelation == 'insertAfter' || context.baRelation == 'insertBefore') {
1490
- insertBefore = false;
1491
- }
1492
- // `a.targetPosition` could be affected by the `b` operation. We will transform it.
1493
- let newTargetPosition;
1494
- if (a.targetPosition.isEqual(b.targetPosition) && insertBefore) {
1495
- newTargetPosition = a.targetPosition._getTransformedByDeletion(b.sourcePosition, b.howMany);
1496
- }
1497
- else {
1498
- newTargetPosition = a.targetPosition._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
1499
- }
1500
- //
1501
- // Special case #1 + mirror.
1502
- //
1503
- // Special case when both move operations' target positions are inside nodes that are
1504
- // being moved by the other move operation. So in other words, we move ranges into inside of each other.
1505
- // This case can't be solved reasonably (on the other hand, it should not happen often).
1506
- if (_moveTargetIntoMovedRange(a, b) && _moveTargetIntoMovedRange(b, a)) {
1507
- // Instead of transforming operation, we return a reverse of the operation that we transform by.
1508
- // So when the results of this "transformation" will be applied, `b` MoveOperation will get reversed.
1509
- return [b.getReversed()];
1510
- }
1511
- //
1512
- // End of special case #1.
1513
- //
1514
- //
1515
- // Special case #2.
1516
- //
1517
- // Check if `b` operation targets inside `rangeA`.
1518
- const bTargetsToA = rangeA.containsPosition(b.targetPosition);
1519
- // If `b` targets to `rangeA` and `rangeA` contains `rangeB`, `b` operation has no influence on `a` operation.
1520
- // You might say that operation `b` is captured inside operation `a`.
1521
- if (bTargetsToA && rangeA.containsRange(rangeB, true)) {
1522
- // There is a mini-special case here, where `rangeB` is on other level than `rangeA`. That's why
1523
- // we need to transform `a` operation anyway.
1524
- rangeA.start = rangeA.start._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
1525
- rangeA.end = rangeA.end._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
1526
- return _makeMoveOperationsFromRanges([rangeA], newTargetPosition);
1527
- }
1528
- //
1529
- // Special case #2 mirror.
1530
- //
1531
- const aTargetsToB = rangeB.containsPosition(a.targetPosition);
1532
- if (aTargetsToB && rangeB.containsRange(rangeA, true)) {
1533
- // `a` operation is "moved together" with `b` operation.
1534
- // Here, just move `rangeA` "inside" `rangeB`.
1535
- rangeA.start = rangeA.start._getCombined(b.sourcePosition, b.getMovedRangeStart());
1536
- rangeA.end = rangeA.end._getCombined(b.sourcePosition, b.getMovedRangeStart());
1537
- return _makeMoveOperationsFromRanges([rangeA], newTargetPosition);
1538
- }
1539
- //
1540
- // End of special case #2.
1541
- //
1542
- //
1543
- // Special case #3 + mirror.
1544
- //
1545
- // `rangeA` has a node which is an ancestor of `rangeB`. In other words, `rangeB` is inside `rangeA`
1546
- // but not on the same tree level. In such case ranges have common part but we have to treat it
1547
- // differently, because in such case those ranges are not really conflicting and should be treated like
1548
- // two separate ranges. Also we have to discard two difference parts.
1549
- const aCompB = compareArrays(a.sourcePosition.getParentPath(), b.sourcePosition.getParentPath());
1550
- if (aCompB == 'prefix' || aCompB == 'extension') {
1551
- // Transform `rangeA` by `b` operation and make operation out of it, and that's all.
1552
- // Note that this is a simplified version of default case, but here we treat the common part (whole `rangeA`)
1553
- // like a one difference part.
1554
- rangeA.start = rangeA.start._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
1555
- rangeA.end = rangeA.end._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
1556
- return _makeMoveOperationsFromRanges([rangeA], newTargetPosition);
1557
- }
1558
- //
1559
- // End of special case #3.
1560
- //
1561
- //
1562
- // Default case - ranges are on the same level or are not connected with each other.
1563
- //
1564
- // Modifier for default case.
1565
- // Modifies `aIsStrong` flag in certain conditions.
1566
- //
1567
- // If only one of operations is a remove operation, we force remove operation to be the "stronger" one
1568
- // to provide more expected results.
1569
- if (a.type == 'remove' && b.type != 'remove' && !context.aWasUndone && !context.forceWeakRemove) {
1570
- aIsStrong = true;
1571
- }
1572
- else if (a.type != 'remove' && b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove) {
1573
- aIsStrong = false;
1574
- }
1575
- // Handle operation's source ranges - check how `rangeA` is affected by `b` operation.
1576
- // This will aggregate transformed ranges.
1577
- const ranges = [];
1578
- // Get the "difference part" of `a` operation source range.
1579
- // This is an array with one or two ranges. Two ranges if `rangeB` is inside `rangeA`.
1580
- const difference = rangeA.getDifference(rangeB);
1581
- for (const range of difference) {
1582
- // Transform those ranges by `b` operation. For example if `b` moved range from before those ranges, fix those ranges.
1583
- range.start = range.start._getTransformedByDeletion(b.sourcePosition, b.howMany);
1584
- range.end = range.end._getTransformedByDeletion(b.sourcePosition, b.howMany);
1585
- // If `b` operation targets into `rangeA` on the same level, spread `rangeA` into two ranges.
1586
- const shouldSpread = compareArrays(range.start.getParentPath(), b.getMovedRangeStart().getParentPath()) == 'same';
1587
- const newRanges = range._getTransformedByInsertion(b.getMovedRangeStart(), b.howMany, shouldSpread);
1588
- ranges.push(...newRanges);
1589
- }
1590
- // Then, we have to manage the "common part" of both move ranges.
1591
- const common = rangeA.getIntersection(rangeB);
1592
- if (common !== null && aIsStrong) {
1593
- // Calculate the new position of that part of original range.
1594
- common.start = common.start._getCombined(b.sourcePosition, b.getMovedRangeStart());
1595
- common.end = common.end._getCombined(b.sourcePosition, b.getMovedRangeStart());
1596
- // Take care of proper range order.
1597
- //
1598
- // Put `common` at appropriate place. Keep in mind that we are interested in original order.
1599
- // Basically there are only three cases: there is zero, one or two difference ranges.
1600
- //
1601
- // If there is zero difference ranges, just push `common` in the array.
1602
- if (ranges.length === 0) {
1603
- ranges.push(common);
1604
- }
1605
- // If there is one difference range, we need to check whether common part was before it or after it.
1606
- else if (ranges.length == 1) {
1607
- if (rangeB.start.isBefore(rangeA.start) || rangeB.start.isEqual(rangeA.start)) {
1608
- ranges.unshift(common);
1609
- }
1610
- else {
1611
- ranges.push(common);
1612
- }
1613
- }
1614
- // If there are more ranges (which means two), put common part between them. This is the only scenario
1615
- // where there could be two difference ranges so we don't have to make any comparisons.
1616
- else {
1617
- ranges.splice(1, 0, common);
1618
- }
1619
- }
1620
- if (ranges.length === 0) {
1621
- // If there are no "source ranges", nothing should be changed.
1622
- // Note that this can happen only if `aIsStrong == false` and `rangeA.isEqual( rangeB )`.
1623
- return [new NoOperation(a.baseVersion)];
1624
- }
1625
- return _makeMoveOperationsFromRanges(ranges, newTargetPosition);
1626
- });
1627
- setTransformation(MoveOperation, SplitOperation, (a, b, context) => {
1628
- let newTargetPosition = a.targetPosition.clone();
1629
- // Do not transform if target position is same as split insertion position and this split comes from undo.
1630
- // This should be done on relations but it is too much work for now as it would require relations working in collaboration.
1631
- // We need to make a decision how we will resolve such conflict and this is less harmful way.
1632
- if (!a.targetPosition.isEqual(b.insertionPosition) || !b.graveyardPosition || context.abRelation == 'moveTargetAfter') {
1633
- newTargetPosition = a.targetPosition._getTransformedBySplitOperation(b);
1634
- }
1635
- // Case 1:
1636
- //
1637
- // Split operation brings back an element that was earlier a part of the move operation (first element of the original move operation).
1638
- //
1639
- if (a.sourcePosition.isEqual(b.insertionPosition) && context.abRelation == 'firstToMoveMerged') {
1640
- a.howMany++;
1641
- a.targetPosition = newTargetPosition;
1642
- return [a];
1643
- }
1644
- // Case 2:
1645
- //
1646
- // Last element in the moved range got split.
1647
- //
1648
- // In this case the default range transformation will not work correctly as the element created by
1649
- // split operation would be outside the range. The range to move needs to be fixed manually.
1650
- //
1651
- const moveRange = ModelRange._createFromPositionAndShift(a.sourcePosition, a.howMany);
1652
- if (moveRange.end.isEqual(b.insertionPosition)) {
1653
- // Do it only if this is a "natural" split, not a one that comes from undo.
1654
- // If this is undo split, only `targetPosition` needs to be changed (if the move is a remove).
1655
- if (!b.graveyardPosition || context.abRelation == 'lastToMoveMerged') {
1656
- a.howMany++;
1657
- }
1658
- a.targetPosition = newTargetPosition;
1659
- return [a];
1660
- }
1661
- // Case 3:
1662
- //
1663
- // Split happened between the moved nodes. In this case two ranges to move need to be generated.
1664
- //
1665
- // Characters `ozba` are moved to the end of paragraph `Xyz` but split happened.
1666
- // <p>F[oz|ba]r</p><p>Xyz</p>
1667
- //
1668
- // After split:
1669
- // <p>F[oz</p><p>ba]r</p><p>Xyz</p>
1670
- //
1671
- // Correct ranges:
1672
- // <p>F[oz]</p><p>[ba]r</p><p>Xyz</p>
1673
- //
1674
- // After move:
1675
- // <p>F</p><p>r</p><p>Xyzozba</p>
1676
- //
1677
- if (moveRange.start.hasSameParentAs(b.splitPosition) && moveRange.containsPosition(b.splitPosition)) {
1678
- let rightRange = new ModelRange(b.splitPosition, moveRange.end);
1679
- rightRange = rightRange._getTransformedBySplitOperation(b);
1680
- const ranges = [
1681
- new ModelRange(moveRange.start, b.splitPosition),
1682
- rightRange
1683
- ];
1684
- return _makeMoveOperationsFromRanges(ranges, newTargetPosition);
1685
- }
1686
- // Case 4:
1687
- //
1688
- // Move operation targets at the split position. We need to decide if the nodes should be inserted
1689
- // at the end of the split element or at the beginning of the new element.
1690
- //
1691
- if (a.targetPosition.isEqual(b.splitPosition) && context.abRelation == 'insertAtSource') {
1692
- newTargetPosition = b.moveTargetPosition;
1693
- }
1694
- // Case 5:
1695
- //
1696
- // Move operation targets just after the split element. We need to decide if the nodes should be inserted
1697
- // between two parts of split element, or after the new element.
1698
- //
1699
- // Split at `|`, while move operation moves `<p>Xyz</p>` and targets at `^`:
1700
- // <p>Foo|bar</p>^<p>baz</p>
1701
- // <p>Foo</p>^<p>bar</p><p>baz</p> or <p>Foo</p><p>bar</p>^<p>baz</p>?
1702
- //
1703
- // If there is no contextual information between operations (for example, they come from collaborative
1704
- // editing), we don't want to put some unrelated content (move) between parts of related content (split parts).
1705
- // However, if the split is from undo, in the past, the moved content might be targeting between the
1706
- // split parts, meaning that was exactly user's intention:
1707
- //
1708
- // <p>Foo</p>^<p>bar</p> <--- original situation, in "past".
1709
- // <p>Foobar</p>^ <--- after merge target position is transformed.
1710
- // <p>Foo|bar</p>^ <--- then the merge is undone, and split happens, which leads us to current situation.
1711
- //
1712
- // In this case it is pretty clear that the intention was to put new paragraph between those nodes,
1713
- // so we need to transform accordingly. We can detect this scenario thanks to relations.
1714
- //
1715
- if (a.targetPosition.isEqual(b.insertionPosition) && context.abRelation == 'insertBetween') {
1716
- newTargetPosition = a.targetPosition;
1717
- }
1718
- // The default case.
1719
- //
1720
- const transformed = moveRange._getTransformedBySplitOperation(b);
1721
- const ranges = [transformed];
1722
- // Case 6:
1723
- //
1724
- // Moved range contains graveyard element used by split operation. Add extra move operation to the result.
1725
- //
1726
- if (b.graveyardPosition) {
1727
- const movesGraveyardElement = moveRange.start.isEqual(b.graveyardPosition) || moveRange.containsPosition(b.graveyardPosition);
1728
- if (a.howMany > 1 && movesGraveyardElement && !context.aWasUndone) {
1729
- ranges.push(ModelRange._createFromPositionAndShift(b.insertionPosition, 1));
1730
- }
1731
- }
1732
- return _makeMoveOperationsFromRanges(ranges, newTargetPosition);
1733
- });
1734
- setTransformation(MoveOperation, MergeOperation, (a, b, context) => {
1735
- const movedRange = ModelRange._createFromPositionAndShift(a.sourcePosition, a.howMany);
1736
- if (b.deletionPosition.hasSameParentAs(a.sourcePosition) && movedRange.containsPosition(b.sourcePosition)) {
1737
- if (a.type == 'remove' && !context.forceWeakRemove) {
1738
- // Case 1:
1739
- //
1740
- // The element to remove got merged.
1741
- //
1742
- // Merge operation does support merging elements which are not siblings. So it would not be a problem
1743
- // from technical point of view. However, if the element was removed, the intention of the user
1744
- // deleting it was to have it all deleted. From user experience point of view, moving back the
1745
- // removed nodes might be unexpected. This means that in this scenario we will reverse merging and remove the element.
1746
- //
1747
- if (!context.aWasUndone) {
1748
- const results = [];
1749
- let gyMoveSource = b.graveyardPosition.clone();
1750
- let splitNodesMoveSource = b.targetPosition._getTransformedByMergeOperation(b);
1751
- // `a.targetPosition` points to graveyard, so it was probably affected by `b` (which moved merged element to the graveyard).
1752
- const aTarget = a.targetPosition.getTransformedByOperation(b);
1753
- if (a.howMany > 1) {
1754
- results.push(new MoveOperation(a.sourcePosition, a.howMany - 1, aTarget, 0));
1755
- gyMoveSource = gyMoveSource._getTransformedByMove(a.sourcePosition, aTarget, a.howMany - 1);
1756
- splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove(a.sourcePosition, aTarget, a.howMany - 1);
1757
- }
1758
- const gyMoveTarget = b.deletionPosition._getCombined(a.sourcePosition, aTarget);
1759
- const gyMove = new MoveOperation(gyMoveSource, 1, gyMoveTarget, 0);
1760
- const splitNodesMoveTargetPath = gyMove.getMovedRangeStart().path.slice();
1761
- splitNodesMoveTargetPath.push(0);
1762
- const splitNodesMoveTarget = new ModelPosition(gyMove.targetPosition.root, splitNodesMoveTargetPath);
1763
- splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove(gyMoveSource, gyMoveTarget, 1);
1764
- const splitNodesMove = new MoveOperation(splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0);
1765
- results.push(gyMove);
1766
- results.push(splitNodesMove);
1767
- return results;
1768
- }
1769
- }
1770
- else {
1771
- // Case 2:
1772
- //
1773
- // The element to move got merged and it was the only element to move.
1774
- // In this case just don't do anything, leave the node in the graveyard. Without special case
1775
- // it would be a move operation that moves 0 nodes, so maybe it is better just to return no-op.
1776
- //
1777
- if (a.howMany == 1) {
1778
- if (!context.bWasUndone) {
1779
- return [new NoOperation(0)];
1780
- }
1781
- else {
1782
- a.sourcePosition = b.graveyardPosition.clone();
1783
- a.targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
1784
- return [a];
1785
- }
1786
- }
1787
- }
1788
- }
1789
- // The default case.
1790
- //
1791
- const moveRange = ModelRange._createFromPositionAndShift(a.sourcePosition, a.howMany);
1792
- const transformed = moveRange._getTransformedByMergeOperation(b);
1793
- a.sourcePosition = transformed.start;
1794
- a.howMany = transformed.end.offset - transformed.start.offset;
1795
- a.targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
1796
- return [a];
1797
- });
1798
- // -----------------------
1799
- setTransformation(RenameOperation, InsertOperation, (a, b) => {
1800
- a.position = a.position._getTransformedByInsertOperation(b);
1801
- return [a];
1802
- });
1803
- setTransformation(RenameOperation, MergeOperation, (a, b) => {
1804
- // Case 1:
1805
- //
1806
- // Element to rename got merged, so it was moved to `b.graveyardPosition`.
1807
- //
1808
- if (a.position.isEqual(b.deletionPosition)) {
1809
- a.position = b.graveyardPosition.clone();
1810
- a.position.stickiness = 'toNext';
1811
- return [a];
1812
- }
1813
- a.position = a.position._getTransformedByMergeOperation(b);
1814
- return [a];
1815
- });
1816
- setTransformation(RenameOperation, MoveOperation, (a, b) => {
1817
- a.position = a.position._getTransformedByMoveOperation(b);
1818
- return [a];
1819
- });
1820
- setTransformation(RenameOperation, RenameOperation, (a, b, context) => {
1821
- if (a.position.isEqual(b.position)) {
1822
- if (context.aIsStrong) {
1823
- a.oldName = b.newName;
1824
- }
1825
- else {
1826
- return [new NoOperation(0)];
1827
- }
1828
- }
1829
- return [a];
1830
- });
1831
- setTransformation(RenameOperation, SplitOperation, (a, b) => {
1832
- // Case 1:
1833
- //
1834
- // The element to rename has been split. In this case, the new element should be also renamed.
1835
- //
1836
- // User decides to change the paragraph to a list item:
1837
- // <paragraph>Foobar</paragraph>
1838
- //
1839
- // However, in meantime, split happens:
1840
- // <paragraph>Foo</paragraph><paragraph>bar</paragraph>
1841
- //
1842
- // As a result, rename both elements:
1843
- // <listItem>Foo</listItem><listItem>bar</listItem>
1844
- //
1845
- const renamePath = a.position.path;
1846
- const splitPath = b.splitPosition.getParentPath();
1847
- if (compareArrays(renamePath, splitPath) == 'same' && !b.graveyardPosition) {
1848
- const extraRename = new RenameOperation(a.position.getShiftedBy(1), a.oldName, a.newName, 0);
1849
- return [a, extraRename];
1850
- }
1851
- // The default case.
1852
- //
1853
- a.position = a.position._getTransformedBySplitOperation(b);
1854
- return [a];
1855
- });
1856
- // -----------------------
1857
- setTransformation(RootAttributeOperation, RootAttributeOperation, (a, b, context) => {
1858
- if (a.root === b.root && a.key === b.key) {
1859
- if (!context.aIsStrong) {
1860
- return [new NoOperation(0)];
1861
- }
1862
- else {
1863
- a.oldValue = b.newValue;
1864
- }
1865
- }
1866
- return [a];
1867
- });
1868
- // -----------------------
1869
- setTransformation(RootOperation, RootOperation, (a, b) => {
1870
- if (a.rootName === b.rootName && a.isAdd === b.isAdd) {
1871
- return [new NoOperation(0)];
1872
- }
1873
- return [a];
1874
- });
1875
- // -----------------------
1876
- setTransformation(SplitOperation, InsertOperation, (a, b) => {
1877
- // The default case.
1878
- //
1879
- if (a.splitPosition.hasSameParentAs(b.position) && a.splitPosition.offset < b.position.offset) {
1880
- a.howMany += b.howMany;
1881
- }
1882
- a.splitPosition = a.splitPosition._getTransformedByInsertOperation(b);
1883
- a.insertionPosition = a.insertionPosition._getTransformedByInsertOperation(b);
1884
- return [a];
1885
- });
1886
- setTransformation(SplitOperation, MergeOperation, (a, b, context) => {
1887
- // Case 1:
1888
- //
1889
- // Split element got merged. If two different elements were merged, clients will have different content.
1890
- //
1891
- // Example. Merge at `{}`, split at `[]`:
1892
- // <heading>Foo</heading>{}<paragraph>B[]ar</paragraph>
1893
- //
1894
- // On merge side it will look like this:
1895
- // <heading>FooB[]ar</heading>
1896
- // <heading>FooB</heading><heading>ar</heading>
1897
- //
1898
- // On split side it will look like this:
1899
- // <heading>Foo</heading>{}<paragraph>B</paragraph><paragraph>ar</paragraph>
1900
- // <heading>FooB</heading><paragraph>ar</paragraph>
1901
- //
1902
- // Clearly, the second element is different for both clients.
1903
- //
1904
- // We could use the removed merge element from graveyard as a split element but then clients would have a different
1905
- // model state (in graveyard), because the split side client would still have an element in graveyard (removed by merge).
1906
- //
1907
- // To overcome this, in `SplitOperation` x `MergeOperation` transformation we will add additional `SplitOperation`
1908
- // in the graveyard, which will actually clone the merged-and-deleted element. Then, that cloned element will be
1909
- // used for splitting. Example below.
1910
- //
1911
- // Original state:
1912
- // <heading>Foo</heading>{}<paragraph>B[]ar</paragraph>
1913
- //
1914
- // Merge side client:
1915
- //
1916
- // After merge:
1917
- // <heading>FooB[]ar</heading> graveyard: <paragraph></paragraph>
1918
- //
1919
- // Extra split:
1920
- // <heading>FooB[]ar</heading> graveyard: <paragraph></paragraph><paragraph></paragraph>
1921
- //
1922
- // Use the "cloned" element from graveyard:
1923
- // <heading>FooB</heading><paragraph>ar</paragraph> graveyard: <paragraph></paragraph>
1924
- //
1925
- // Split side client:
1926
- //
1927
- // After split:
1928
- // <heading>Foo</heading>{}<paragraph>B</paragraph><paragraph>ar</paragraph>
1929
- //
1930
- // After merge:
1931
- // <heading>FooB</heading><paragraph>ar</paragraph> graveyard: <paragraph></paragraph>
1932
- //
1933
- // This special case scenario only applies if the original split operation clones the split element.
1934
- // If the original split operation has `graveyardPosition` set, it all doesn't have sense because split operation
1935
- // knows exactly which element it should use. So there would be no original problem with different contents.
1936
- //
1937
- // Additionally, the special case applies only if the merge wasn't already undone.
1938
- //
1939
- if (!a.graveyardPosition && !context.bWasUndone && a.splitPosition.hasSameParentAs(b.sourcePosition)) {
1940
- const splitPath = b.graveyardPosition.path.slice();
1941
- splitPath.push(0);
1942
- const splitPosition = new ModelPosition(b.graveyardPosition.root, splitPath);
1943
- const insertionPosition = SplitOperation.getInsertionPosition(new ModelPosition(b.graveyardPosition.root, splitPath));
1944
- const additionalSplit = new SplitOperation(splitPosition, 0, insertionPosition, null, 0);
1945
- a.splitPosition = a.splitPosition._getTransformedByMergeOperation(b);
1946
- a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
1947
- a.graveyardPosition = additionalSplit.insertionPosition.clone();
1948
- a.graveyardPosition.stickiness = 'toNext';
1949
- return [additionalSplit, a];
1950
- }
1951
- // The default case.
1952
- //
1953
- if (a.splitPosition.hasSameParentAs(b.deletionPosition) && !a.splitPosition.isAfter(b.deletionPosition)) {
1954
- a.howMany--;
1955
- }
1956
- if (a.splitPosition.hasSameParentAs(b.targetPosition)) {
1957
- a.howMany += b.howMany;
1958
- }
1959
- a.splitPosition = a.splitPosition._getTransformedByMergeOperation(b);
1960
- a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
1961
- if (a.graveyardPosition) {
1962
- a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation(b);
1963
- }
1964
- return [a];
1965
- });
1966
- setTransformation(SplitOperation, MoveOperation, (a, b, context) => {
1967
- const rangeToMove = ModelRange._createFromPositionAndShift(b.sourcePosition, b.howMany);
1968
- if (a.graveyardPosition) {
1969
- // Case 1:
1970
- //
1971
- // Split operation graveyard node was moved. In this case move operation is stronger. Since graveyard element
1972
- // is already moved to the correct position, we need to only move the nodes after the split position.
1973
- // This will be done by `MoveOperation` instead of `SplitOperation`.
1974
- //
1975
- const gyElementMoved = rangeToMove.start.isEqual(a.graveyardPosition) || rangeToMove.containsPosition(a.graveyardPosition);
1976
- if (!context.bWasUndone && gyElementMoved) {
1977
- const sourcePosition = a.splitPosition._getTransformedByMoveOperation(b);
1978
- const newParentPosition = a.graveyardPosition._getTransformedByMoveOperation(b);
1979
- const newTargetPath = newParentPosition.path.slice();
1980
- newTargetPath.push(0);
1981
- const newTargetPosition = new ModelPosition(newParentPosition.root, newTargetPath);
1982
- const moveOp = new MoveOperation(sourcePosition, a.howMany, newTargetPosition, 0);
1983
- return [moveOp];
1984
- }
1985
- a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation(b);
1986
- }
1987
- // Case 2:
1988
- //
1989
- // Split is at a position where nodes were moved.
1990
- //
1991
- // This is a scenario described in `MoveOperation` x `SplitOperation` transformation but from the
1992
- // "split operation point of view".
1993
- //
1994
- const splitAtTarget = a.splitPosition.isEqual(b.targetPosition);
1995
- if (splitAtTarget && (context.baRelation == 'insertAtSource' || context.abRelation == 'splitBefore')) {
1996
- a.howMany += b.howMany;
1997
- a.splitPosition = a.splitPosition._getTransformedByDeletion(b.sourcePosition, b.howMany);
1998
- a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
1999
- return [a];
2000
- }
2001
- if (splitAtTarget && context.abRelation && context.abRelation.howMany) {
2002
- const { howMany, offset } = context.abRelation;
2003
- a.howMany += howMany;
2004
- a.splitPosition = a.splitPosition.getShiftedBy(offset);
2005
- // TODO: `insertionPosition` change missing?
2006
- return [a];
2007
- }
2008
- // Case 3:
2009
- //
2010
- // If the split position is inside the moved range, we need to shift the split position to a proper place.
2011
- // The position cannot be moved together with moved range because that would result in splitting of an incorrect element.
2012
- //
2013
- // Characters `bc` should be moved to the second paragraph while split position is between them:
2014
- // <paragraph>A[b|c]d</paragraph><paragraph>Xyz</paragraph>
2015
- //
2016
- // After move, new split position is incorrect:
2017
- // <paragraph>Ad</paragraph><paragraph>Xb|cyz</paragraph>
2018
- //
2019
- // Correct split position:
2020
- // <paragraph>A|d</paragraph><paragraph>Xbcyz</paragraph>
2021
- //
2022
- // After split:
2023
- // <paragraph>A</paragraph><paragraph>d</paragraph><paragraph>Xbcyz</paragraph>
2024
- //
2025
- if (a.splitPosition.hasSameParentAs(b.sourcePosition) && rangeToMove.containsPosition(a.splitPosition)) {
2026
- const howManyRemoved = b.howMany - (a.splitPosition.offset - b.sourcePosition.offset);
2027
- a.howMany -= howManyRemoved;
2028
- if (a.splitPosition.hasSameParentAs(b.targetPosition) && a.splitPosition.offset < b.targetPosition.offset) {
2029
- a.howMany += b.howMany;
2030
- }
2031
- a.splitPosition = b.sourcePosition.clone();
2032
- a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
2033
- return [a];
2034
- }
2035
- // The default case.
2036
- // Don't change `howMany` if move operation does not really move anything.
2037
- //
2038
- if (!b.sourcePosition.isEqual(b.targetPosition)) {
2039
- if (a.splitPosition.hasSameParentAs(b.sourcePosition) && a.splitPosition.offset <= b.sourcePosition.offset) {
2040
- a.howMany -= b.howMany;
2041
- }
2042
- if (a.splitPosition.hasSameParentAs(b.targetPosition) && a.splitPosition.offset < b.targetPosition.offset) {
2043
- a.howMany += b.howMany;
2044
- }
2045
- }
2046
- // Change position stickiness to force a correct transformation.
2047
- a.splitPosition.stickiness = 'toNone';
2048
- a.splitPosition = a.splitPosition._getTransformedByMoveOperation(b);
2049
- a.splitPosition.stickiness = 'toNext';
2050
- if (a.graveyardPosition) {
2051
- a.insertionPosition = a.insertionPosition._getTransformedByMoveOperation(b);
2052
- }
2053
- else {
2054
- a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
2055
- }
2056
- return [a];
2057
- });
2058
- setTransformation(SplitOperation, SplitOperation, (a, b, context) => {
2059
- // Case 1:
2060
- //
2061
- // Split at the same position.
2062
- //
2063
- // If there already was a split at the same position as in `a` operation, it means that the intention
2064
- // conveyed by `a` operation has already been fulfilled and `a` should not do anything (to avoid double split).
2065
- //
2066
- // However, there is a difference if these are new splits or splits created by undo. These have different
2067
- // intentions. Also splits moving back different elements from graveyard have different intentions. They
2068
- // are just different operations.
2069
- //
2070
- // So we cancel split operation only if it was really identical.
2071
- //
2072
- // Also, there is additional case, where split operations aren't identical and should not be cancelled, however the
2073
- // default transformation is incorrect too.
2074
- //
2075
- if (a.splitPosition.isEqual(b.splitPosition)) {
2076
- if (!a.graveyardPosition && !b.graveyardPosition) {
2077
- return [new NoOperation(0)];
2078
- }
2079
- if (a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual(b.graveyardPosition)) {
2080
- return [new NoOperation(0)];
2081
- }
2082
- // Use context to know that the `a.splitPosition` should stay where it is.
2083
- // This happens during undo when first a merge operation moved nodes to `a.splitPosition` and now `b` operation undoes that merge.
2084
- if (context.abRelation == 'splitBefore') {
2085
- // Since split is at the same position, there are no nodes left to split.
2086
- a.howMany = 0;
2087
- // Note: there was `if ( a.graveyardPosition )` here but it was uncovered in tests and I couldn't find any scenarios for now.
2088
- // That would have to be a `SplitOperation` that didn't come from undo but is transformed by operations that were undone.
2089
- // It could happen if `context` is enabled in collaboration.
2090
- a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation(b);
2091
- return [a];
2092
- }
2093
- }
2094
- // Case 2:
2095
- //
2096
- // Same node is using to split different elements. This happens in undo when previously same element was merged to
2097
- // two different elements. This is described in `MergeOperation` x `MergeOperation` transformation.
2098
- //
2099
- // In this case we will follow the same logic. We will assume that `insertionPosition` is same for both
2100
- // split operations. This might not always be true but in the real cases that were experienced it was. After all,
2101
- // if these splits are reverses of merge operations that were merging the same element, then the `insertionPosition`
2102
- // should be same for both of those splits.
2103
- //
2104
- // Again, we will decide which operation is stronger by checking if split happens in graveyard or in non-graveyard root.
2105
- //
2106
- if (a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual(b.graveyardPosition)) {
2107
- const aInGraveyard = a.splitPosition.root.rootName == '$graveyard';
2108
- const bInGraveyard = b.splitPosition.root.rootName == '$graveyard';
2109
- // If `aIsWeak` it means that `a` points to graveyard while `b` doesn't. Don't move nodes then.
2110
- const aIsWeak = aInGraveyard && !bInGraveyard;
2111
- // If `bIsWeak` it means that `b` points to graveyard while `a` doesn't. Force moving nodes then.
2112
- const bIsWeak = bInGraveyard && !aInGraveyard;
2113
- // Force move if `b` is weak or neither operation is weak but `a` is stronger through `context.aIsStrong`.
2114
- const forceMove = bIsWeak || (!aIsWeak && context.aIsStrong);
2115
- if (forceMove) {
2116
- const result = [];
2117
- // First we need to move any nodes split by `b` back to where they were.
2118
- // Do it only if `b` actually moved something.
2119
- if (b.howMany) {
2120
- result.push(new MoveOperation(b.moveTargetPosition, b.howMany, b.splitPosition, 0));
2121
- }
2122
- // Then we need to move nodes from `a` split position to their new element.
2123
- // Do it only if `a` actually should move something.
2124
- if (a.howMany) {
2125
- result.push(new MoveOperation(a.splitPosition, a.howMany, a.moveTargetPosition, 0));
2126
- }
2127
- return result;
2128
- }
2129
- else {
2130
- return [new NoOperation(0)];
2131
- }
2132
- }
2133
- if (a.graveyardPosition) {
2134
- a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation(b);
2135
- }
2136
- // Case 3:
2137
- //
2138
- // Position where operation `b` inserted a new node after split is the same as the operation `a` split position.
2139
- // As in similar cases, there is ambiguity if the split should be before the new node (created by `b`) or after.
2140
- //
2141
- if (a.splitPosition.isEqual(b.insertionPosition) && context.abRelation == 'splitBefore') {
2142
- a.howMany++;
2143
- return [a];
2144
- }
2145
- // Case 4:
2146
- //
2147
- // This is a mirror to the case 2. above.
2148
- //
2149
- if (b.splitPosition.isEqual(a.insertionPosition) && context.baRelation == 'splitBefore') {
2150
- const newPositionPath = b.insertionPosition.path.slice();
2151
- newPositionPath.push(0);
2152
- const newPosition = new ModelPosition(b.insertionPosition.root, newPositionPath);
2153
- const moveOp = new MoveOperation(a.insertionPosition, 1, newPosition, 0);
2154
- return [a, moveOp];
2155
- }
2156
- // The default case.
2157
- //
2158
- if (a.splitPosition.hasSameParentAs(b.splitPosition) && a.splitPosition.offset < b.splitPosition.offset) {
2159
- a.howMany -= b.howMany;
2160
- }
2161
- a.splitPosition = a.splitPosition._getTransformedBySplitOperation(b);
2162
- a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
2163
- return [a];
2164
- });
2165
- /**
2166
- * Checks whether `MoveOperation` `targetPosition` is inside a node from the moved range of the other `MoveOperation`.
2167
- */
2168
- function _moveTargetIntoMovedRange(a, b) {
2169
- return a.targetPosition._getTransformedByDeletion(b.sourcePosition, b.howMany) === null;
2170
- }
2171
- /**
2172
- * Helper function for `MoveOperation` x `MoveOperation` transformation. Converts given ranges and target position to
2173
- * move operations and returns them.
2174
- *
2175
- * Ranges and target position will be transformed on-the-fly when generating operations.
2176
- *
2177
- * Given `ranges` should be in the order of how they were in the original transformed operation.
2178
- *
2179
- * Given `targetPosition` is the target position of the first range from `ranges`.
2180
- */
2181
- function _makeMoveOperationsFromRanges(ranges, targetPosition) {
2182
- // At this moment we have some ranges and a target position, to which those ranges should be moved.
2183
- // Order in `ranges` array is the go-to order of after transformation.
2184
- //
2185
- // We are almost done. We have `ranges` and `targetPosition` to make operations from.
2186
- // Unfortunately, those operations may affect each other. Precisely, first operation after move
2187
- // may affect source range and target position of second and third operation. Same with second
2188
- // operation affecting third.
2189
- //
2190
- // We need to fix those source ranges and target positions once again, before converting `ranges` to operations.
2191
- const operations = [];
2192
- // Keep in mind that nothing will be transformed if there is just one range in `ranges`.
2193
- for (let i = 0; i < ranges.length; i++) {
2194
- // Create new operation out of a range and target position.
2195
- const range = ranges[i];
2196
- const op = new MoveOperation(range.start, range.end.offset - range.start.offset, targetPosition, 0);
2197
- operations.push(op);
2198
- // Transform other ranges by the generated operation.
2199
- for (let j = i + 1; j < ranges.length; j++) {
2200
- // All ranges in `ranges` array should be:
2201
- //
2202
- // * non-intersecting (these are part of original operation source range), and
2203
- // * `targetPosition` does not target into them (opposite would mean that transformed operation targets "inside itself").
2204
- //
2205
- // This means that the transformation will be "clean" and always return one result.
2206
- ranges[j] = ranges[j]._getTransformedByMove(op.sourcePosition, op.targetPosition, op.howMany)[0];
2207
- }
2208
- targetPosition = targetPosition._getTransformedByMove(op.sourcePosition, op.targetPosition, op.howMany);
2209
- }
2210
- return operations;
2211
- }