@ckeditor/ckeditor5-engine 40.0.0 → 40.2.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 (243) hide show
  1. package/CHANGELOG.md +39 -39
  2. package/LICENSE.md +3 -3
  3. package/package.json +2 -2
  4. package/src/controller/datacontroller.d.ts +334 -334
  5. package/src/controller/datacontroller.js +481 -481
  6. package/src/controller/editingcontroller.d.ts +98 -98
  7. package/src/controller/editingcontroller.js +191 -191
  8. package/src/conversion/conversion.d.ts +478 -478
  9. package/src/conversion/conversion.js +601 -601
  10. package/src/conversion/conversionhelpers.d.ts +26 -26
  11. package/src/conversion/conversionhelpers.js +32 -32
  12. package/src/conversion/downcastdispatcher.d.ts +562 -562
  13. package/src/conversion/downcastdispatcher.js +548 -547
  14. package/src/conversion/downcasthelpers.d.ts +1226 -1226
  15. package/src/conversion/downcasthelpers.js +2178 -2183
  16. package/src/conversion/mapper.d.ts +503 -503
  17. package/src/conversion/mapper.js +536 -536
  18. package/src/conversion/modelconsumable.d.ts +201 -201
  19. package/src/conversion/modelconsumable.js +333 -333
  20. package/src/conversion/upcastdispatcher.d.ts +492 -492
  21. package/src/conversion/upcastdispatcher.js +460 -460
  22. package/src/conversion/upcasthelpers.d.ts +499 -499
  23. package/src/conversion/upcasthelpers.js +950 -950
  24. package/src/conversion/viewconsumable.d.ts +369 -369
  25. package/src/conversion/viewconsumable.js +536 -532
  26. package/src/dataprocessor/basichtmlwriter.d.ts +18 -18
  27. package/src/dataprocessor/basichtmlwriter.js +20 -19
  28. package/src/dataprocessor/dataprocessor.d.ts +61 -61
  29. package/src/dataprocessor/dataprocessor.js +5 -5
  30. package/src/dataprocessor/htmldataprocessor.d.ts +76 -76
  31. package/src/dataprocessor/htmldataprocessor.js +96 -96
  32. package/src/dataprocessor/htmlwriter.d.ts +16 -16
  33. package/src/dataprocessor/htmlwriter.js +5 -5
  34. package/src/dataprocessor/xmldataprocessor.d.ts +90 -90
  35. package/src/dataprocessor/xmldataprocessor.js +108 -108
  36. package/src/dev-utils/model.d.ts +124 -124
  37. package/src/dev-utils/model.js +395 -395
  38. package/src/dev-utils/operationreplayer.d.ts +51 -51
  39. package/src/dev-utils/operationreplayer.js +112 -112
  40. package/src/dev-utils/utils.d.ts +37 -37
  41. package/src/dev-utils/utils.js +73 -73
  42. package/src/dev-utils/view.d.ts +319 -319
  43. package/src/dev-utils/view.js +967 -967
  44. package/src/index.d.ts +114 -114
  45. package/src/index.js +78 -78
  46. package/src/model/batch.d.ts +106 -106
  47. package/src/model/batch.js +96 -96
  48. package/src/model/differ.d.ts +387 -387
  49. package/src/model/differ.js +1149 -1149
  50. package/src/model/document.d.ts +272 -272
  51. package/src/model/document.js +360 -361
  52. package/src/model/documentfragment.d.ts +200 -200
  53. package/src/model/documentfragment.js +306 -306
  54. package/src/model/documentselection.d.ts +420 -420
  55. package/src/model/documentselection.js +993 -993
  56. package/src/model/element.d.ts +165 -165
  57. package/src/model/element.js +281 -281
  58. package/src/model/history.d.ts +114 -114
  59. package/src/model/history.js +207 -207
  60. package/src/model/item.d.ts +14 -14
  61. package/src/model/item.js +5 -5
  62. package/src/model/liveposition.d.ts +77 -77
  63. package/src/model/liveposition.js +93 -93
  64. package/src/model/liverange.d.ts +102 -102
  65. package/src/model/liverange.js +120 -120
  66. package/src/model/markercollection.d.ts +335 -335
  67. package/src/model/markercollection.js +403 -403
  68. package/src/model/model.d.ts +919 -919
  69. package/src/model/model.js +842 -842
  70. package/src/model/node.d.ts +256 -256
  71. package/src/model/node.js +375 -375
  72. package/src/model/nodelist.d.ts +91 -91
  73. package/src/model/nodelist.js +163 -163
  74. package/src/model/operation/attributeoperation.d.ts +103 -103
  75. package/src/model/operation/attributeoperation.js +148 -148
  76. package/src/model/operation/detachoperation.d.ts +60 -60
  77. package/src/model/operation/detachoperation.js +77 -77
  78. package/src/model/operation/insertoperation.d.ts +90 -90
  79. package/src/model/operation/insertoperation.js +135 -135
  80. package/src/model/operation/markeroperation.d.ts +91 -91
  81. package/src/model/operation/markeroperation.js +107 -107
  82. package/src/model/operation/mergeoperation.d.ts +100 -100
  83. package/src/model/operation/mergeoperation.js +167 -167
  84. package/src/model/operation/moveoperation.d.ts +96 -96
  85. package/src/model/operation/moveoperation.js +164 -164
  86. package/src/model/operation/nooperation.d.ts +38 -38
  87. package/src/model/operation/nooperation.js +48 -48
  88. package/src/model/operation/operation.d.ts +96 -96
  89. package/src/model/operation/operation.js +59 -62
  90. package/src/model/operation/operationfactory.d.ts +18 -18
  91. package/src/model/operation/operationfactory.js +44 -44
  92. package/src/model/operation/renameoperation.d.ts +83 -83
  93. package/src/model/operation/renameoperation.js +115 -115
  94. package/src/model/operation/rootattributeoperation.d.ts +98 -98
  95. package/src/model/operation/rootattributeoperation.js +155 -155
  96. package/src/model/operation/rootoperation.d.ts +76 -76
  97. package/src/model/operation/rootoperation.js +90 -90
  98. package/src/model/operation/splitoperation.d.ts +109 -109
  99. package/src/model/operation/splitoperation.js +194 -194
  100. package/src/model/operation/transform.d.ts +100 -100
  101. package/src/model/operation/transform.js +1985 -1985
  102. package/src/model/operation/utils.d.ts +71 -71
  103. package/src/model/operation/utils.js +217 -213
  104. package/src/model/position.d.ts +539 -539
  105. package/src/model/position.js +979 -979
  106. package/src/model/range.d.ts +458 -458
  107. package/src/model/range.js +875 -875
  108. package/src/model/rootelement.d.ts +60 -60
  109. package/src/model/rootelement.js +74 -74
  110. package/src/model/schema.d.ts +1186 -1186
  111. package/src/model/schema.js +1242 -1242
  112. package/src/model/selection.d.ts +482 -482
  113. package/src/model/selection.js +789 -789
  114. package/src/model/text.d.ts +66 -66
  115. package/src/model/text.js +85 -85
  116. package/src/model/textproxy.d.ts +144 -144
  117. package/src/model/textproxy.js +189 -189
  118. package/src/model/treewalker.d.ts +186 -186
  119. package/src/model/treewalker.js +244 -244
  120. package/src/model/typecheckable.d.ts +285 -285
  121. package/src/model/typecheckable.js +16 -16
  122. package/src/model/utils/autoparagraphing.d.ts +37 -37
  123. package/src/model/utils/autoparagraphing.js +63 -63
  124. package/src/model/utils/deletecontent.d.ts +58 -58
  125. package/src/model/utils/deletecontent.js +488 -488
  126. package/src/model/utils/findoptimalinsertionrange.d.ts +32 -32
  127. package/src/model/utils/findoptimalinsertionrange.js +57 -57
  128. package/src/model/utils/getselectedcontent.d.ts +30 -30
  129. package/src/model/utils/getselectedcontent.js +125 -125
  130. package/src/model/utils/insertcontent.d.ts +46 -46
  131. package/src/model/utils/insertcontent.js +705 -705
  132. package/src/model/utils/insertobject.d.ts +44 -44
  133. package/src/model/utils/insertobject.js +139 -139
  134. package/src/model/utils/modifyselection.d.ts +48 -48
  135. package/src/model/utils/modifyselection.js +186 -186
  136. package/src/model/utils/selection-post-fixer.d.ts +74 -74
  137. package/src/model/utils/selection-post-fixer.js +260 -260
  138. package/src/model/writer.d.ts +851 -851
  139. package/src/model/writer.js +1306 -1306
  140. package/src/view/attributeelement.d.ts +108 -108
  141. package/src/view/attributeelement.js +184 -184
  142. package/src/view/containerelement.d.ts +49 -49
  143. package/src/view/containerelement.js +80 -80
  144. package/src/view/datatransfer.d.ts +79 -79
  145. package/src/view/datatransfer.js +98 -98
  146. package/src/view/document.d.ts +184 -184
  147. package/src/view/document.js +122 -120
  148. package/src/view/documentfragment.d.ts +153 -149
  149. package/src/view/documentfragment.js +234 -228
  150. package/src/view/documentselection.d.ts +306 -306
  151. package/src/view/documentselection.js +256 -256
  152. package/src/view/domconverter.d.ts +652 -640
  153. package/src/view/domconverter.js +1473 -1450
  154. package/src/view/downcastwriter.d.ts +996 -996
  155. package/src/view/downcastwriter.js +1696 -1696
  156. package/src/view/editableelement.d.ts +62 -62
  157. package/src/view/editableelement.js +62 -62
  158. package/src/view/element.d.ts +468 -468
  159. package/src/view/element.js +724 -724
  160. package/src/view/elementdefinition.d.ts +87 -87
  161. package/src/view/elementdefinition.js +5 -5
  162. package/src/view/emptyelement.d.ts +41 -41
  163. package/src/view/emptyelement.js +73 -73
  164. package/src/view/filler.d.ts +111 -111
  165. package/src/view/filler.js +150 -150
  166. package/src/view/item.d.ts +14 -14
  167. package/src/view/item.js +5 -5
  168. package/src/view/matcher.d.ts +486 -486
  169. package/src/view/matcher.js +507 -507
  170. package/src/view/node.d.ts +163 -163
  171. package/src/view/node.js +228 -228
  172. package/src/view/observer/arrowkeysobserver.d.ts +45 -45
  173. package/src/view/observer/arrowkeysobserver.js +40 -40
  174. package/src/view/observer/bubblingemittermixin.d.ts +166 -166
  175. package/src/view/observer/bubblingemittermixin.js +172 -172
  176. package/src/view/observer/bubblingeventinfo.d.ts +47 -47
  177. package/src/view/observer/bubblingeventinfo.js +37 -37
  178. package/src/view/observer/clickobserver.d.ts +43 -43
  179. package/src/view/observer/clickobserver.js +29 -29
  180. package/src/view/observer/compositionobserver.d.ts +82 -82
  181. package/src/view/observer/compositionobserver.js +60 -60
  182. package/src/view/observer/domeventdata.d.ts +50 -50
  183. package/src/view/observer/domeventdata.js +47 -47
  184. package/src/view/observer/domeventobserver.d.ts +73 -73
  185. package/src/view/observer/domeventobserver.js +79 -79
  186. package/src/view/observer/fakeselectionobserver.d.ts +47 -47
  187. package/src/view/observer/fakeselectionobserver.js +91 -91
  188. package/src/view/observer/focusobserver.d.ts +82 -82
  189. package/src/view/observer/focusobserver.js +86 -86
  190. package/src/view/observer/inputobserver.d.ts +86 -86
  191. package/src/view/observer/inputobserver.js +164 -164
  192. package/src/view/observer/keyobserver.d.ts +66 -66
  193. package/src/view/observer/keyobserver.js +39 -39
  194. package/src/view/observer/mouseobserver.d.ts +89 -89
  195. package/src/view/observer/mouseobserver.js +29 -29
  196. package/src/view/observer/mutationobserver.d.ts +86 -86
  197. package/src/view/observer/mutationobserver.js +206 -206
  198. package/src/view/observer/observer.d.ts +89 -89
  199. package/src/view/observer/observer.js +84 -84
  200. package/src/view/observer/selectionobserver.d.ts +148 -148
  201. package/src/view/observer/selectionobserver.js +202 -202
  202. package/src/view/observer/tabobserver.d.ts +46 -46
  203. package/src/view/observer/tabobserver.js +42 -42
  204. package/src/view/placeholder.d.ts +96 -96
  205. package/src/view/placeholder.js +267 -267
  206. package/src/view/position.d.ts +189 -189
  207. package/src/view/position.js +324 -324
  208. package/src/view/range.d.ts +279 -279
  209. package/src/view/range.js +430 -430
  210. package/src/view/rawelement.d.ts +73 -73
  211. package/src/view/rawelement.js +105 -105
  212. package/src/view/renderer.d.ts +265 -265
  213. package/src/view/renderer.js +1000 -999
  214. package/src/view/rooteditableelement.d.ts +41 -41
  215. package/src/view/rooteditableelement.js +69 -69
  216. package/src/view/selection.d.ts +375 -375
  217. package/src/view/selection.js +559 -559
  218. package/src/view/styles/background.d.ts +33 -33
  219. package/src/view/styles/background.js +74 -74
  220. package/src/view/styles/border.d.ts +43 -43
  221. package/src/view/styles/border.js +316 -316
  222. package/src/view/styles/margin.d.ts +29 -29
  223. package/src/view/styles/margin.js +34 -34
  224. package/src/view/styles/padding.d.ts +29 -29
  225. package/src/view/styles/padding.js +34 -34
  226. package/src/view/styles/utils.d.ts +93 -93
  227. package/src/view/styles/utils.js +219 -219
  228. package/src/view/stylesmap.d.ts +675 -675
  229. package/src/view/stylesmap.js +765 -766
  230. package/src/view/text.d.ts +74 -74
  231. package/src/view/text.js +93 -93
  232. package/src/view/textproxy.d.ts +97 -97
  233. package/src/view/textproxy.js +124 -124
  234. package/src/view/treewalker.d.ts +195 -195
  235. package/src/view/treewalker.js +327 -327
  236. package/src/view/typecheckable.d.ts +448 -448
  237. package/src/view/typecheckable.js +19 -19
  238. package/src/view/uielement.d.ts +96 -96
  239. package/src/view/uielement.js +183 -182
  240. package/src/view/upcastwriter.d.ts +417 -417
  241. package/src/view/upcastwriter.js +359 -359
  242. package/src/view/view.d.ts +487 -487
  243. package/src/view/view.js +546 -546
@@ -1,979 +1,979 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
- */
5
- /**
6
- * @module engine/model/position
7
- */
8
- import TypeCheckable from './typecheckable';
9
- import TreeWalker from './treewalker';
10
- import { CKEditorError, compareArrays } from '@ckeditor/ckeditor5-utils';
11
- // To check if component is loaded more than once.
12
- import '@ckeditor/ckeditor5-utils/src/version';
13
- /**
14
- * Represents a position in the model tree.
15
- *
16
- * A position is represented by its {@link module:engine/model/position~Position#root} and
17
- * a {@link module:engine/model/position~Position#path} in that root.
18
- *
19
- * You can create position instances via its constructor or the `createPosition*()` factory methods of
20
- * {@link module:engine/model/model~Model} and {@link module:engine/model/writer~Writer}.
21
- *
22
- * **Note:** Position is based on offsets, not indexes. This means that a position between two text nodes
23
- * `foo` and `bar` has offset `3`, not `1`. See {@link module:engine/model/position~Position#path} for more information.
24
- *
25
- * Since a position in the model is represented by a {@link module:engine/model/position~Position#root position root} and
26
- * {@link module:engine/model/position~Position#path position path} it is possible to create positions placed in non-existing places.
27
- * This requirement is important for operational transformation algorithms.
28
- *
29
- * Also, {@link module:engine/model/operation/operation~Operation operations}
30
- * kept in the {@link module:engine/model/document~Document#history document history}
31
- * are storing positions (and ranges) which were correct when those operations were applied, but may not be correct
32
- * after the document has changed.
33
- *
34
- * When changes are applied to the model, it may also happen that {@link module:engine/model/position~Position#parent position parent}
35
- * will change even if position path has not changed. Keep in mind, that if a position leads to non-existing element,
36
- * {@link module:engine/model/position~Position#parent} and some other properties and methods will throw errors.
37
- *
38
- * In most cases, position with wrong path is caused by an error in code, but it is sometimes needed, as described above.
39
- */
40
- export default class Position extends TypeCheckable {
41
- /**
42
- * Creates a position.
43
- *
44
- * @param root Root of the position.
45
- * @param path Position path. See {@link module:engine/model/position~Position#path}.
46
- * @param stickiness Position stickiness. See {@link module:engine/model/position~PositionStickiness}.
47
- */
48
- constructor(root, path, stickiness = 'toNone') {
49
- super();
50
- if (!root.is('element') && !root.is('documentFragment')) {
51
- /**
52
- * Position root is invalid.
53
- *
54
- * Positions can only be anchored in elements or document fragments.
55
- *
56
- * @error model-position-root-invalid
57
- */
58
- throw new CKEditorError('model-position-root-invalid', root);
59
- }
60
- if (!(path instanceof Array) || path.length === 0) {
61
- /**
62
- * Position path must be an array with at least one item.
63
- *
64
- * @error model-position-path-incorrect-format
65
- * @param path
66
- */
67
- throw new CKEditorError('model-position-path-incorrect-format', root, { path });
68
- }
69
- // Normalize the root and path when element (not root) is passed.
70
- if (root.is('rootElement')) {
71
- path = path.slice();
72
- }
73
- else {
74
- path = [...root.getPath(), ...path];
75
- root = root.root;
76
- }
77
- this.root = root;
78
- this.path = path;
79
- this.stickiness = stickiness;
80
- }
81
- /**
82
- * Offset at which this position is located in its {@link module:engine/model/position~Position#parent parent}. It is equal
83
- * to the last item in position {@link module:engine/model/position~Position#path path}.
84
- *
85
- * @type {Number}
86
- */
87
- get offset() {
88
- return this.path[this.path.length - 1];
89
- }
90
- set offset(newOffset) {
91
- this.path[this.path.length - 1] = newOffset;
92
- }
93
- /**
94
- * Parent element of this position.
95
- *
96
- * Keep in mind that `parent` value is calculated when the property is accessed.
97
- * If {@link module:engine/model/position~Position#path position path}
98
- * leads to a non-existing element, `parent` property will throw error.
99
- *
100
- * Also it is a good idea to cache `parent` property if it is used frequently in an algorithm (i.e. in a long loop).
101
- */
102
- get parent() {
103
- let parent = this.root;
104
- for (let i = 0; i < this.path.length - 1; i++) {
105
- parent = parent.getChild(parent.offsetToIndex(this.path[i]));
106
- if (!parent) {
107
- /**
108
- * The position's path is incorrect. This means that a position does not point to
109
- * a correct place in the tree and hence, some of its methods and getters cannot work correctly.
110
- *
111
- * **Note**: Unlike DOM and view positions, in the model, the
112
- * {@link module:engine/model/position~Position#parent position's parent} is always an element or a document fragment.
113
- * The last offset in the {@link module:engine/model/position~Position#path position's path} is the point in this element
114
- * where this position points.
115
- *
116
- * Read more about model positions and offsets in
117
- * the {@glink framework/architecture/editing-engine#indexes-and-offsets Editing engine architecture} guide.
118
- *
119
- * @error model-position-path-incorrect
120
- * @param position The incorrect position.
121
- */
122
- throw new CKEditorError('model-position-path-incorrect', this, { position: this });
123
- }
124
- }
125
- if (parent.is('$text')) {
126
- throw new CKEditorError('model-position-path-incorrect', this, { position: this });
127
- }
128
- return parent;
129
- }
130
- /**
131
- * Position {@link module:engine/model/position~Position#offset offset} converted to an index in position's parent node. It is
132
- * equal to the {@link module:engine/model/node~Node#index index} of a node after this position. If position is placed
133
- * in text node, position index is equal to the index of that text node.
134
- */
135
- get index() {
136
- return this.parent.offsetToIndex(this.offset);
137
- }
138
- /**
139
- * Returns {@link module:engine/model/text~Text text node} instance in which this position is placed or `null` if this
140
- * position is not in a text node.
141
- */
142
- get textNode() {
143
- return getTextNodeAtPosition(this, this.parent);
144
- }
145
- /**
146
- * Node directly after this position or `null` if this position is in text node.
147
- */
148
- get nodeAfter() {
149
- // Cache the parent and reuse for performance reasons. See #6579 and #6582.
150
- const parent = this.parent;
151
- return getNodeAfterPosition(this, parent, getTextNodeAtPosition(this, parent));
152
- }
153
- /**
154
- * Node directly before this position or `null` if this position is in text node.
155
- */
156
- get nodeBefore() {
157
- // Cache the parent and reuse for performance reasons. See #6579 and #6582.
158
- const parent = this.parent;
159
- return getNodeBeforePosition(this, parent, getTextNodeAtPosition(this, parent));
160
- }
161
- /**
162
- * Is `true` if position is at the beginning of its {@link module:engine/model/position~Position#parent parent}, `false` otherwise.
163
- */
164
- get isAtStart() {
165
- return this.offset === 0;
166
- }
167
- /**
168
- * Is `true` if position is at the end of its {@link module:engine/model/position~Position#parent parent}, `false` otherwise.
169
- */
170
- get isAtEnd() {
171
- return this.offset == this.parent.maxOffset;
172
- }
173
- /**
174
- * Checks whether this position is before or after given position.
175
- *
176
- * This method is safe to use it on non-existing positions (for example during operational transformation).
177
- */
178
- compareWith(otherPosition) {
179
- if (this.root != otherPosition.root) {
180
- return 'different';
181
- }
182
- const result = compareArrays(this.path, otherPosition.path);
183
- switch (result) {
184
- case 'same':
185
- return 'same';
186
- case 'prefix':
187
- return 'before';
188
- case 'extension':
189
- return 'after';
190
- default:
191
- return this.path[result] < otherPosition.path[result] ? 'before' : 'after';
192
- }
193
- }
194
- /**
195
- * Gets the farthest position which matches the callback using
196
- * {@link module:engine/model/treewalker~TreeWalker TreeWalker}.
197
- *
198
- * For example:
199
- *
200
- * ```ts
201
- * getLastMatchingPosition( value => value.type == 'text' );
202
- * // <paragraph>[]foo</paragraph> -> <paragraph>foo[]</paragraph>
203
- *
204
- * getLastMatchingPosition( value => value.type == 'text', { direction: 'backward' } );
205
- * // <paragraph>foo[]</paragraph> -> <paragraph>[]foo</paragraph>
206
- *
207
- * getLastMatchingPosition( value => false );
208
- * // Do not move the position.
209
- * ```
210
- *
211
- * @param skip Callback function. Gets {@link module:engine/model/treewalker~TreeWalkerValue} and should
212
- * return `true` if the value should be skipped or `false` if not.
213
- * @param options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
214
- *
215
- * @returns The position after the last item which matches the `skip` callback test.
216
- */
217
- getLastMatchingPosition(skip, options = {}) {
218
- options.startPosition = this;
219
- const treeWalker = new TreeWalker(options);
220
- treeWalker.skip(skip);
221
- return treeWalker.position;
222
- }
223
- /**
224
- * Returns a path to this position's parent. Parent path is equal to position {@link module:engine/model/position~Position#path path}
225
- * but without the last item.
226
- *
227
- * This method is safe to use it on non-existing positions (for example during operational transformation).
228
- *
229
- * @returns Path to the parent.
230
- */
231
- getParentPath() {
232
- return this.path.slice(0, -1);
233
- }
234
- /**
235
- * Returns ancestors array of this position, that is this position's parent and its ancestors.
236
- *
237
- * @returns Array with ancestors.
238
- */
239
- getAncestors() {
240
- const parent = this.parent;
241
- if (parent.is('documentFragment')) {
242
- return [parent];
243
- }
244
- else {
245
- return parent.getAncestors({ includeSelf: true });
246
- }
247
- }
248
- /**
249
- * Returns the parent element of the given name. Returns null if the position is not inside the desired parent.
250
- *
251
- * @param parentName The name of the parent element to find.
252
- */
253
- findAncestor(parentName) {
254
- const parent = this.parent;
255
- if (parent.is('element')) {
256
- return parent.findAncestor(parentName, { includeSelf: true });
257
- }
258
- return null;
259
- }
260
- /**
261
- * Returns the slice of two position {@link #path paths} which is identical. The {@link #root roots}
262
- * of these two paths must be identical.
263
- *
264
- * This method is safe to use it on non-existing positions (for example during operational transformation).
265
- *
266
- * @param position The second position.
267
- * @returns The common path.
268
- */
269
- getCommonPath(position) {
270
- if (this.root != position.root) {
271
- return [];
272
- }
273
- // We find on which tree-level start and end have the lowest common ancestor
274
- const cmp = compareArrays(this.path, position.path);
275
- // If comparison returned string it means that arrays are same.
276
- const diffAt = (typeof cmp == 'string') ? Math.min(this.path.length, position.path.length) : cmp;
277
- return this.path.slice(0, diffAt);
278
- }
279
- /**
280
- * Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
281
- * which is a common ancestor of both positions. The {@link #root roots} of these two positions must be identical.
282
- *
283
- * @param position The second position.
284
- */
285
- getCommonAncestor(position) {
286
- const ancestorsA = this.getAncestors();
287
- const ancestorsB = position.getAncestors();
288
- let i = 0;
289
- while (ancestorsA[i] == ancestorsB[i] && ancestorsA[i]) {
290
- i++;
291
- }
292
- return i === 0 ? null : ancestorsA[i - 1];
293
- }
294
- /**
295
- * Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
296
- * is shifted by `shift` value (can be a negative value).
297
- *
298
- * This method is safe to use it on non-existing positions (for example during operational transformation).
299
- *
300
- * @param shift Offset shift. Can be a negative value.
301
- * @returns Shifted position.
302
- */
303
- getShiftedBy(shift) {
304
- const shifted = this.clone();
305
- const offset = shifted.offset + shift;
306
- shifted.offset = offset < 0 ? 0 : offset;
307
- return shifted;
308
- }
309
- /**
310
- * Checks whether this position is after given position.
311
- *
312
- * This method is safe to use it on non-existing positions (for example during operational transformation).
313
- *
314
- * @see module:engine/model/position~Position#isBefore
315
- * @param otherPosition Position to compare with.
316
- * @returns True if this position is after given position.
317
- */
318
- isAfter(otherPosition) {
319
- return this.compareWith(otherPosition) == 'after';
320
- }
321
- /**
322
- * Checks whether this position is before given position.
323
- *
324
- * **Note:** watch out when using negation of the value returned by this method, because the negation will also
325
- * be `true` if positions are in different roots and you might not expect this. You should probably use
326
- * `a.isAfter( b ) || a.isEqual( b )` or `!a.isBefore( p ) && a.root == b.root` in most scenarios. If your
327
- * condition uses multiple `isAfter` and `isBefore` checks, build them so they do not use negated values, i.e.:
328
- *
329
- * ```ts
330
- * if ( a.isBefore( b ) && c.isAfter( d ) ) {
331
- * // do A.
332
- * } else {
333
- * // do B.
334
- * }
335
- * ```
336
- *
337
- * or, if you have only one if-branch:
338
- *
339
- * ```ts
340
- * if ( !( a.isBefore( b ) && c.isAfter( d ) ) {
341
- * // do B.
342
- * }
343
- * ```
344
- *
345
- * rather than:
346
- *
347
- * ```ts
348
- * if ( !a.isBefore( b ) || && !c.isAfter( d ) ) {
349
- * // do B.
350
- * } else {
351
- * // do A.
352
- * }
353
- * ```
354
- *
355
- * This method is safe to use it on non-existing positions (for example during operational transformation).
356
- *
357
- * @param otherPosition Position to compare with.
358
- * @returns True if this position is before given position.
359
- */
360
- isBefore(otherPosition) {
361
- return this.compareWith(otherPosition) == 'before';
362
- }
363
- /**
364
- * Checks whether this position is equal to given position.
365
- *
366
- * This method is safe to use it on non-existing positions (for example during operational transformation).
367
- *
368
- * @param otherPosition Position to compare with.
369
- * @returns True if positions are same.
370
- */
371
- isEqual(otherPosition) {
372
- return this.compareWith(otherPosition) == 'same';
373
- }
374
- /**
375
- * Checks whether this position is touching given position. Positions touch when there are no text nodes
376
- * or empty nodes in a range between them. Technically, those positions are not equal but in many cases
377
- * they are very similar or even indistinguishable.
378
- *
379
- * @param otherPosition Position to compare with.
380
- * @returns True if positions touch.
381
- */
382
- isTouching(otherPosition) {
383
- if (this.root !== otherPosition.root) {
384
- return false;
385
- }
386
- const commonLevel = Math.min(this.path.length, otherPosition.path.length);
387
- for (let level = 0; level < commonLevel; level++) {
388
- const diff = this.path[level] - otherPosition.path[level];
389
- // Positions are spread by a node, so they are not touching.
390
- if (diff < -1 || diff > 1) {
391
- return false;
392
- }
393
- else if (diff === 1) {
394
- // `otherPosition` is on the left.
395
- // `this` is on the right.
396
- return checkTouchingBranch(otherPosition, this, level);
397
- }
398
- else if (diff === -1) {
399
- // `this` is on the left.
400
- // `otherPosition` is on the right.
401
- return checkTouchingBranch(this, otherPosition, level);
402
- }
403
- // `diff === 0`.
404
- // Positions are inside the same element on this level, compare deeper.
405
- }
406
- // If we ended up here, it means that positions paths have the same beginning.
407
- // If the paths have the same length, then it means that they are identical, so the positions are same.
408
- if (this.path.length === otherPosition.path.length) {
409
- return true;
410
- }
411
- // If positions have different length of paths, then the common part is the same.
412
- // In this case, the "shorter" position is on the left, the "longer" position is on the right.
413
- //
414
- // If the positions are touching, the "longer" position must have only zeroes. For example:
415
- // [ 1, 2 ] vs [ 1, 2, 0 ]
416
- // [ 1, 2 ] vs [ 1, 2, 0, 0, 0 ]
417
- else if (this.path.length > otherPosition.path.length) {
418
- return checkOnlyZeroes(this.path, commonLevel);
419
- }
420
- else {
421
- return checkOnlyZeroes(otherPosition.path, commonLevel);
422
- }
423
- }
424
- /**
425
- * Checks if two positions are in the same parent.
426
- *
427
- * This method is safe to use it on non-existing positions (for example during operational transformation).
428
- *
429
- * @param position Position to compare with.
430
- * @returns `true` if positions have the same parent, `false` otherwise.
431
- */
432
- hasSameParentAs(position) {
433
- if (this.root !== position.root) {
434
- return false;
435
- }
436
- const thisParentPath = this.getParentPath();
437
- const posParentPath = position.getParentPath();
438
- return compareArrays(thisParentPath, posParentPath) == 'same';
439
- }
440
- /**
441
- * Returns a copy of this position that is transformed by given `operation`.
442
- *
443
- * The new position's parameters are updated accordingly to the effect of the `operation`.
444
- *
445
- * For example, if `n` nodes are inserted before the position, the returned position {@link ~Position#offset} will be
446
- * increased by `n`. If the position was in a merged element, it will be accordingly moved to the new element, etc.
447
- *
448
- * This method is safe to use it on non-existing positions (for example during operational transformation).
449
- *
450
- * @param operation Operation to transform by.
451
- * @returns Transformed position.
452
- */
453
- getTransformedByOperation(operation) {
454
- let result;
455
- switch (operation.type) {
456
- case 'insert':
457
- result = this._getTransformedByInsertOperation(operation);
458
- break;
459
- case 'move':
460
- case 'remove':
461
- case 'reinsert':
462
- result = this._getTransformedByMoveOperation(operation);
463
- break;
464
- case 'split':
465
- result = this._getTransformedBySplitOperation(operation);
466
- break;
467
- case 'merge':
468
- result = this._getTransformedByMergeOperation(operation);
469
- break;
470
- default:
471
- result = Position._createAt(this);
472
- break;
473
- }
474
- return result;
475
- }
476
- /**
477
- * Returns a copy of this position transformed by an insert operation.
478
- *
479
- * @internal
480
- */
481
- _getTransformedByInsertOperation(operation) {
482
- return this._getTransformedByInsertion(operation.position, operation.howMany);
483
- }
484
- /**
485
- * Returns a copy of this position transformed by a move operation.
486
- *
487
- * @internal
488
- */
489
- _getTransformedByMoveOperation(operation) {
490
- return this._getTransformedByMove(operation.sourcePosition, operation.targetPosition, operation.howMany);
491
- }
492
- /**
493
- * Returns a copy of this position transformed by a split operation.
494
- *
495
- * @internal
496
- */
497
- _getTransformedBySplitOperation(operation) {
498
- const movedRange = operation.movedRange;
499
- const isContained = movedRange.containsPosition(this) ||
500
- (movedRange.start.isEqual(this) && this.stickiness == 'toNext');
501
- if (isContained) {
502
- return this._getCombined(operation.splitPosition, operation.moveTargetPosition);
503
- }
504
- else {
505
- if (operation.graveyardPosition) {
506
- return this._getTransformedByMove(operation.graveyardPosition, operation.insertionPosition, 1);
507
- }
508
- else {
509
- return this._getTransformedByInsertion(operation.insertionPosition, 1);
510
- }
511
- }
512
- }
513
- /**
514
- * Returns a copy of this position transformed by merge operation.
515
- *
516
- * @internal
517
- */
518
- _getTransformedByMergeOperation(operation) {
519
- const movedRange = operation.movedRange;
520
- const isContained = movedRange.containsPosition(this) || movedRange.start.isEqual(this);
521
- let pos;
522
- if (isContained) {
523
- pos = this._getCombined(operation.sourcePosition, operation.targetPosition);
524
- if (operation.sourcePosition.isBefore(operation.targetPosition)) {
525
- // Above happens during OT when the merged element is moved before the merged-to element.
526
- pos = pos._getTransformedByDeletion(operation.deletionPosition, 1);
527
- }
528
- }
529
- else if (this.isEqual(operation.deletionPosition)) {
530
- pos = Position._createAt(operation.deletionPosition);
531
- }
532
- else {
533
- pos = this._getTransformedByMove(operation.deletionPosition, operation.graveyardPosition, 1);
534
- }
535
- return pos;
536
- }
537
- /**
538
- * Returns a copy of this position that is updated by removing `howMany` nodes starting from `deletePosition`.
539
- * It may happen that this position is in a removed node. If that is the case, `null` is returned instead.
540
- *
541
- * @internal
542
- * @param deletePosition Position before the first removed node.
543
- * @param howMany How many nodes are removed.
544
- * @returns Transformed position or `null`.
545
- */
546
- _getTransformedByDeletion(deletePosition, howMany) {
547
- const transformed = Position._createAt(this);
548
- // This position can't be affected if deletion was in a different root.
549
- if (this.root != deletePosition.root) {
550
- return transformed;
551
- }
552
- if (compareArrays(deletePosition.getParentPath(), this.getParentPath()) == 'same') {
553
- // If nodes are removed from the node that is pointed by this position...
554
- if (deletePosition.offset < this.offset) {
555
- // And are removed from before an offset of that position...
556
- if (deletePosition.offset + howMany > this.offset) {
557
- // Position is in removed range, it's no longer in the tree.
558
- return null;
559
- }
560
- else {
561
- // Decrement the offset accordingly.
562
- transformed.offset -= howMany;
563
- }
564
- }
565
- }
566
- else if (compareArrays(deletePosition.getParentPath(), this.getParentPath()) == 'prefix') {
567
- // If nodes are removed from a node that is on a path to this position...
568
- const i = deletePosition.path.length - 1;
569
- if (deletePosition.offset <= this.path[i]) {
570
- // And are removed from before next node of that path...
571
- if (deletePosition.offset + howMany > this.path[i]) {
572
- // If the next node of that path is removed return null
573
- // because the node containing this position got removed.
574
- return null;
575
- }
576
- else {
577
- // Otherwise, decrement index on that path.
578
- transformed.path[i] -= howMany;
579
- }
580
- }
581
- }
582
- return transformed;
583
- }
584
- /**
585
- * Returns a copy of this position that is updated by inserting `howMany` nodes at `insertPosition`.
586
- *
587
- * @internal
588
- * @param insertPosition Position where nodes are inserted.
589
- * @param howMany How many nodes are inserted.
590
- * @returns Transformed position.
591
- */
592
- _getTransformedByInsertion(insertPosition, howMany) {
593
- const transformed = Position._createAt(this);
594
- // This position can't be affected if insertion was in a different root.
595
- if (this.root != insertPosition.root) {
596
- return transformed;
597
- }
598
- if (compareArrays(insertPosition.getParentPath(), this.getParentPath()) == 'same') {
599
- // If nodes are inserted in the node that is pointed by this position...
600
- if (insertPosition.offset < this.offset || (insertPosition.offset == this.offset && this.stickiness != 'toPrevious')) {
601
- // And are inserted before an offset of that position...
602
- // "Push" this positions offset.
603
- transformed.offset += howMany;
604
- }
605
- }
606
- else if (compareArrays(insertPosition.getParentPath(), this.getParentPath()) == 'prefix') {
607
- // If nodes are inserted in a node that is on a path to this position...
608
- const i = insertPosition.path.length - 1;
609
- if (insertPosition.offset <= this.path[i]) {
610
- // And are inserted before next node of that path...
611
- // "Push" the index on that path.
612
- transformed.path[i] += howMany;
613
- }
614
- }
615
- return transformed;
616
- }
617
- /**
618
- * Returns a copy of this position that is updated by moving `howMany` nodes from `sourcePosition` to `targetPosition`.
619
- *
620
- * @internal
621
- * @param sourcePosition Position before the first element to move.
622
- * @param targetPosition Position where moved elements will be inserted.
623
- * @param howMany How many consecutive nodes to move, starting from `sourcePosition`.
624
- * @returns Transformed position.
625
- */
626
- _getTransformedByMove(sourcePosition, targetPosition, howMany) {
627
- // Update target position, as it could be affected by nodes removal.
628
- targetPosition = targetPosition._getTransformedByDeletion(sourcePosition, howMany);
629
- if (sourcePosition.isEqual(targetPosition)) {
630
- // If `targetPosition` is equal to `sourcePosition` this isn't really any move. Just return position as it is.
631
- return Position._createAt(this);
632
- }
633
- // Moving a range removes nodes from their original position. We acknowledge this by proper transformation.
634
- const transformed = this._getTransformedByDeletion(sourcePosition, howMany);
635
- const isMoved = transformed === null ||
636
- (sourcePosition.isEqual(this) && this.stickiness == 'toNext') ||
637
- (sourcePosition.getShiftedBy(howMany).isEqual(this) && this.stickiness == 'toPrevious');
638
- if (isMoved) {
639
- // This position is inside moved range (or sticks to it).
640
- // In this case, we calculate a combination of this position, move source position and target position.
641
- return this._getCombined(sourcePosition, targetPosition);
642
- }
643
- else {
644
- // This position is not inside a removed range.
645
- //
646
- // In next step, we simply reflect inserting `howMany` nodes, which might further affect the position.
647
- return transformed._getTransformedByInsertion(targetPosition, howMany);
648
- }
649
- }
650
- /**
651
- * Returns a new position that is a combination of this position and given positions.
652
- *
653
- * The combined position is a copy of this position transformed by moving a range starting at `source` position
654
- * to the `target` position. It is expected that this position is inside the moved range.
655
- *
656
- * Example:
657
- *
658
- * ```ts
659
- * let original = model.createPositionFromPath( root, [ 2, 3, 1 ] );
660
- * let source = model.createPositionFromPath( root, [ 2, 2 ] );
661
- * let target = model.createPositionFromPath( otherRoot, [ 1, 1, 3 ] );
662
- * original._getCombined( source, target ); // path is [ 1, 1, 4, 1 ], root is `otherRoot`
663
- * ```
664
- *
665
- * Explanation:
666
- *
667
- * We have a position `[ 2, 3, 1 ]` and move some nodes from `[ 2, 2 ]` to `[ 1, 1, 3 ]`. The original position
668
- * was inside moved nodes and now should point to the new place. The moved nodes will be after
669
- * positions `[ 1, 1, 3 ]`, `[ 1, 1, 4 ]`, `[ 1, 1, 5 ]`. Since our position was in the second moved node,
670
- * the transformed position will be in a sub-tree of a node at `[ 1, 1, 4 ]`. Looking at original path, we
671
- * took care of `[ 2, 3 ]` part of it. Now we have to add the rest of the original path to the transformed path.
672
- * Finally, the transformed position will point to `[ 1, 1, 4, 1 ]`.
673
- *
674
- * @internal
675
- * @param source Beginning of the moved range.
676
- * @param target Position where the range is moved.
677
- * @returns Combined position.
678
- */
679
- _getCombined(source, target) {
680
- const i = source.path.length - 1;
681
- // The first part of a path to combined position is a path to the place where nodes were moved.
682
- const combined = Position._createAt(target);
683
- combined.stickiness = this.stickiness;
684
- // Then we have to update the rest of the path.
685
- // Fix the offset because this position might be after `from` position and we have to reflect that.
686
- combined.offset = combined.offset + this.path[i] - source.offset;
687
- // Then, add the rest of the path.
688
- // If this position is at the same level as `from` position nothing will get added.
689
- combined.path = [...combined.path, ...this.path.slice(i + 1)];
690
- return combined;
691
- }
692
- /**
693
- * @inheritDoc
694
- */
695
- toJSON() {
696
- return {
697
- root: this.root.toJSON(),
698
- path: Array.from(this.path),
699
- stickiness: this.stickiness
700
- };
701
- }
702
- /**
703
- * Returns a new position that is equal to current position.
704
- */
705
- clone() {
706
- return new this.constructor(this.root, this.path, this.stickiness);
707
- }
708
- /**
709
- * Creates position at the given location. The location can be specified as:
710
- *
711
- * * a {@link module:engine/model/position~Position position},
712
- * * parent element and offset (offset defaults to `0`),
713
- * * parent element and `'end'` (sets position at the end of that element),
714
- * * {@link module:engine/model/item~Item model item} and `'before'` or `'after'` (sets position before or after given model item).
715
- *
716
- * This method is a shortcut to other factory methods such as:
717
- *
718
- * * {@link module:engine/model/position~Position._createBefore},
719
- * * {@link module:engine/model/position~Position._createAfter}.
720
- *
721
- * @internal
722
- * @param offset Offset or one of the flags. Used only when the first parameter is a {@link module:engine/model/item~Item model item}.
723
- * @param stickiness Position stickiness. Used only when the first parameter is a {@link module:engine/model/item~Item model item}.
724
- */
725
- static _createAt(itemOrPosition, offset, stickiness = 'toNone') {
726
- if (itemOrPosition instanceof Position) {
727
- return new Position(itemOrPosition.root, itemOrPosition.path, itemOrPosition.stickiness);
728
- }
729
- else {
730
- const node = itemOrPosition;
731
- if (offset == 'end') {
732
- offset = node.maxOffset;
733
- }
734
- else if (offset == 'before') {
735
- return this._createBefore(node, stickiness);
736
- }
737
- else if (offset == 'after') {
738
- return this._createAfter(node, stickiness);
739
- }
740
- else if (offset !== 0 && !offset) {
741
- /**
742
- * {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}
743
- * requires the offset to be specified when the first parameter is a model item.
744
- *
745
- * @error model-createpositionat-offset-required
746
- */
747
- throw new CKEditorError('model-createpositionat-offset-required', [this, itemOrPosition]);
748
- }
749
- if (!node.is('element') && !node.is('documentFragment')) {
750
- /**
751
- * Position parent have to be a model element or model document fragment.
752
- *
753
- * @error model-position-parent-incorrect
754
- */
755
- throw new CKEditorError('model-position-parent-incorrect', [this, itemOrPosition]);
756
- }
757
- const path = node.getPath();
758
- path.push(offset);
759
- return new this(node.root, path, stickiness);
760
- }
761
- }
762
- /**
763
- * Creates a new position, after given {@link module:engine/model/item~Item model item}.
764
- *
765
- * @internal
766
- * @param item Item after which the position should be placed.
767
- * @param stickiness Position stickiness.
768
- */
769
- static _createAfter(item, stickiness) {
770
- if (!item.parent) {
771
- /**
772
- * You can not make a position after a root element.
773
- *
774
- * @error model-position-after-root
775
- * @param root
776
- */
777
- throw new CKEditorError('model-position-after-root', [this, item], { root: item });
778
- }
779
- return this._createAt(item.parent, item.endOffset, stickiness);
780
- }
781
- /**
782
- * Creates a new position, before the given {@link module:engine/model/item~Item model item}.
783
- *
784
- * @internal
785
- * @param item Item before which the position should be placed.
786
- * @param stickiness Position stickiness.
787
- */
788
- static _createBefore(item, stickiness) {
789
- if (!item.parent) {
790
- /**
791
- * You can not make a position before a root element.
792
- *
793
- * @error model-position-before-root
794
- * @param root
795
- */
796
- throw new CKEditorError('model-position-before-root', item, { root: item });
797
- }
798
- return this._createAt(item.parent, item.startOffset, stickiness);
799
- }
800
- /**
801
- * Creates a `Position` instance from given plain object (i.e. parsed JSON string).
802
- *
803
- * @param json Plain object to be converted to `Position`.
804
- * @param doc Document object that will be position owner.
805
- * @returns `Position` instance created using given plain object.
806
- */
807
- static fromJSON(json, doc) {
808
- if (json.root === '$graveyard') {
809
- const pos = new Position(doc.graveyard, json.path);
810
- pos.stickiness = json.stickiness;
811
- return pos;
812
- }
813
- if (!doc.getRoot(json.root)) {
814
- /**
815
- * Cannot create position for document. Root with specified name does not exist.
816
- *
817
- * @error model-position-fromjson-no-root
818
- * @param rootName
819
- */
820
- throw new CKEditorError('model-position-fromjson-no-root', doc, { rootName: json.root });
821
- }
822
- return new Position(doc.getRoot(json.root), json.path, json.stickiness);
823
- }
824
- }
825
- // The magic of type inference using `is` method is centralized in `TypeCheckable` class.
826
- // Proper overload would interfere with that.
827
- Position.prototype.is = function (type) {
828
- return type === 'position' || type === 'model:position';
829
- };
830
- /**
831
- * Returns a text node at the given position.
832
- *
833
- * This is a helper function optimized to reuse the position parent instance for performance reasons.
834
- *
835
- * Normally, you should use {@link module:engine/model/position~Position#textNode `Position#textNode`}.
836
- * If you start hitting performance issues with {@link module:engine/model/position~Position#parent `Position#parent`}
837
- * check if your algorithm does not access it multiple times (which can happen directly or indirectly via other position properties).
838
- *
839
- * See https://github.com/ckeditor/ckeditor5/issues/6579.
840
- *
841
- * See also:
842
- *
843
- * * {@link module:engine/model/position~getNodeAfterPosition}
844
- * * {@link module:engine/model/position~getNodeBeforePosition}
845
- *
846
- * @param positionParent The parent of the given position.
847
- */
848
- export function getTextNodeAtPosition(position, positionParent) {
849
- const node = positionParent.getChild(positionParent.offsetToIndex(position.offset));
850
- if (node && node.is('$text') && node.startOffset < position.offset) {
851
- return node;
852
- }
853
- return null;
854
- }
855
- /**
856
- * Returns the node after the given position.
857
- *
858
- * This is a helper function optimized to reuse the position parent instance and the calculation of the text node at the
859
- * specific position for performance reasons.
860
- *
861
- * Normally, you should use {@link module:engine/model/position~Position#nodeAfter `Position#nodeAfter`}.
862
- * If you start hitting performance issues with {@link module:engine/model/position~Position#parent `Position#parent`} and/or
863
- * {@link module:engine/model/position~Position#textNode `Position#textNode`}
864
- * check if your algorithm does not access those properties multiple times
865
- * (which can happen directly or indirectly via other position properties).
866
- *
867
- * See https://github.com/ckeditor/ckeditor5/issues/6579 and https://github.com/ckeditor/ckeditor5/issues/6582.
868
- *
869
- * See also:
870
- *
871
- * * {@link module:engine/model/position~getTextNodeAtPosition}
872
- * * {@link module:engine/model/position~getNodeBeforePosition}
873
- *
874
- * @param positionParent The parent of the given position.
875
- * @param textNode Text node at the given position.
876
- */
877
- export function getNodeAfterPosition(position, positionParent, textNode) {
878
- if (textNode !== null) {
879
- return null;
880
- }
881
- return positionParent.getChild(positionParent.offsetToIndex(position.offset));
882
- }
883
- /**
884
- * Returns the node before the given position.
885
- *
886
- * Refer to {@link module:engine/model/position~getNodeBeforePosition} for documentation on when to use this util method.
887
- *
888
- * See also:
889
- *
890
- * * {@link module:engine/model/position~getTextNodeAtPosition}
891
- * * {@link module:engine/model/position~getNodeAfterPosition}
892
- *
893
- * @param positionParent The parent of the given position.
894
- * @param textNode Text node at the given position.
895
- */
896
- export function getNodeBeforePosition(position, positionParent, textNode) {
897
- if (textNode !== null) {
898
- return null;
899
- }
900
- return positionParent.getChild(positionParent.offsetToIndex(position.offset) - 1);
901
- }
902
- /**
903
- * This is a helper function for `Position#isTouching()`.
904
- *
905
- * It checks whether to given positions are touching, considering that they have the same root and paths
906
- * until given level, and at given level they differ by 1 (so they are branching at `level` point).
907
- *
908
- * The exact requirements for touching positions are described in `Position#isTouching()` and also
909
- * in the body of this function.
910
- *
911
- * @param left Position "on the left" (it is before `right`).
912
- * @param right Position "on the right" (it is after `left`).
913
- * @param level Level on which the positions are different.
914
- */
915
- function checkTouchingBranch(left, right, level) {
916
- if (level + 1 === left.path.length) {
917
- // Left position does not have any more entries after the point where the positions differ.
918
- // [ 2 ] vs [ 3 ]
919
- // [ 2 ] vs [ 3, 0, 0 ]
920
- // The positions are spread by node at [ 2 ].
921
- return false;
922
- }
923
- if (!checkOnlyZeroes(right.path, level + 1)) {
924
- // Right position does not have only zeroes, so we have situation like:
925
- // [ 2, maxOffset ] vs [ 3, 1 ]
926
- // [ 2, maxOffset ] vs [ 3, 1, 0, 0 ]
927
- // The positions are spread by node at [ 3, 0 ].
928
- return false;
929
- }
930
- if (!checkOnlyMaxOffset(left, level + 1)) {
931
- // Left position does not have only max offsets, so we have situation like:
932
- // [ 2, 4 ] vs [ 3 ]
933
- // [ 2, 4 ] vs [ 3, 0, 0 ]
934
- // The positions are spread by node at [ 2, 5 ].
935
- return false;
936
- }
937
- // Left position has only max offsets and right position has only zeroes or nothing.
938
- // [ 2, maxOffset ] vs [ 3 ]
939
- // [ 2, maxOffset, maxOffset ] vs [ 3, 0 ]
940
- // There are not elements between positions. The positions are touching.
941
- return true;
942
- }
943
- /**
944
- * Checks whether for given array, starting from given index until the end of the array, all items are `0`s.
945
- *
946
- * This is a helper function for `Position#isTouching()`.
947
- */
948
- function checkOnlyZeroes(arr, idx) {
949
- while (idx < arr.length) {
950
- if (arr[idx] !== 0) {
951
- return false;
952
- }
953
- idx++;
954
- }
955
- return true;
956
- }
957
- /**
958
- * Checks whether for given position, starting from given path level, whether the position is at the end of
959
- * its parent and whether each element on the path to the position is also at at the end of its parent.
960
- *
961
- * This is a helper function for `Position#isTouching()`.
962
- */
963
- function checkOnlyMaxOffset(pos, level) {
964
- let parent = pos.parent;
965
- let idx = pos.path.length - 1;
966
- let add = 0;
967
- while (idx >= level) {
968
- if (pos.path[idx] + add !== parent.maxOffset) {
969
- return false;
970
- }
971
- // After the first check, we "go up", and check whether the position's parent-parent is the last element.
972
- // However, we need to add 1 to the value in the path to "simulate" moving the path after the parent.
973
- // It happens just once.
974
- add = 1;
975
- idx--;
976
- parent = parent.parent;
977
- }
978
- return true;
979
- }
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module engine/model/position
7
+ */
8
+ import TypeCheckable from './typecheckable';
9
+ import TreeWalker from './treewalker';
10
+ import { CKEditorError, compareArrays } from '@ckeditor/ckeditor5-utils';
11
+ // To check if component is loaded more than once.
12
+ import '@ckeditor/ckeditor5-utils/src/version';
13
+ /**
14
+ * Represents a position in the model tree.
15
+ *
16
+ * A position is represented by its {@link module:engine/model/position~Position#root} and
17
+ * a {@link module:engine/model/position~Position#path} in that root.
18
+ *
19
+ * You can create position instances via its constructor or the `createPosition*()` factory methods of
20
+ * {@link module:engine/model/model~Model} and {@link module:engine/model/writer~Writer}.
21
+ *
22
+ * **Note:** Position is based on offsets, not indexes. This means that a position between two text nodes
23
+ * `foo` and `bar` has offset `3`, not `1`. See {@link module:engine/model/position~Position#path} for more information.
24
+ *
25
+ * Since a position in the model is represented by a {@link module:engine/model/position~Position#root position root} and
26
+ * {@link module:engine/model/position~Position#path position path} it is possible to create positions placed in non-existing places.
27
+ * This requirement is important for operational transformation algorithms.
28
+ *
29
+ * Also, {@link module:engine/model/operation/operation~Operation operations}
30
+ * kept in the {@link module:engine/model/document~Document#history document history}
31
+ * are storing positions (and ranges) which were correct when those operations were applied, but may not be correct
32
+ * after the document has changed.
33
+ *
34
+ * When changes are applied to the model, it may also happen that {@link module:engine/model/position~Position#parent position parent}
35
+ * will change even if position path has not changed. Keep in mind, that if a position leads to non-existing element,
36
+ * {@link module:engine/model/position~Position#parent} and some other properties and methods will throw errors.
37
+ *
38
+ * In most cases, position with wrong path is caused by an error in code, but it is sometimes needed, as described above.
39
+ */
40
+ export default class Position extends TypeCheckable {
41
+ /**
42
+ * Creates a position.
43
+ *
44
+ * @param root Root of the position.
45
+ * @param path Position path. See {@link module:engine/model/position~Position#path}.
46
+ * @param stickiness Position stickiness. See {@link module:engine/model/position~PositionStickiness}.
47
+ */
48
+ constructor(root, path, stickiness = 'toNone') {
49
+ super();
50
+ if (!root.is('element') && !root.is('documentFragment')) {
51
+ /**
52
+ * Position root is invalid.
53
+ *
54
+ * Positions can only be anchored in elements or document fragments.
55
+ *
56
+ * @error model-position-root-invalid
57
+ */
58
+ throw new CKEditorError('model-position-root-invalid', root);
59
+ }
60
+ if (!(path instanceof Array) || path.length === 0) {
61
+ /**
62
+ * Position path must be an array with at least one item.
63
+ *
64
+ * @error model-position-path-incorrect-format
65
+ * @param path
66
+ */
67
+ throw new CKEditorError('model-position-path-incorrect-format', root, { path });
68
+ }
69
+ // Normalize the root and path when element (not root) is passed.
70
+ if (root.is('rootElement')) {
71
+ path = path.slice();
72
+ }
73
+ else {
74
+ path = [...root.getPath(), ...path];
75
+ root = root.root;
76
+ }
77
+ this.root = root;
78
+ this.path = path;
79
+ this.stickiness = stickiness;
80
+ }
81
+ /**
82
+ * Offset at which this position is located in its {@link module:engine/model/position~Position#parent parent}. It is equal
83
+ * to the last item in position {@link module:engine/model/position~Position#path path}.
84
+ *
85
+ * @type {Number}
86
+ */
87
+ get offset() {
88
+ return this.path[this.path.length - 1];
89
+ }
90
+ set offset(newOffset) {
91
+ this.path[this.path.length - 1] = newOffset;
92
+ }
93
+ /**
94
+ * Parent element of this position.
95
+ *
96
+ * Keep in mind that `parent` value is calculated when the property is accessed.
97
+ * If {@link module:engine/model/position~Position#path position path}
98
+ * leads to a non-existing element, `parent` property will throw error.
99
+ *
100
+ * Also it is a good idea to cache `parent` property if it is used frequently in an algorithm (i.e. in a long loop).
101
+ */
102
+ get parent() {
103
+ let parent = this.root;
104
+ for (let i = 0; i < this.path.length - 1; i++) {
105
+ parent = parent.getChild(parent.offsetToIndex(this.path[i]));
106
+ if (!parent) {
107
+ /**
108
+ * The position's path is incorrect. This means that a position does not point to
109
+ * a correct place in the tree and hence, some of its methods and getters cannot work correctly.
110
+ *
111
+ * **Note**: Unlike DOM and view positions, in the model, the
112
+ * {@link module:engine/model/position~Position#parent position's parent} is always an element or a document fragment.
113
+ * The last offset in the {@link module:engine/model/position~Position#path position's path} is the point in this element
114
+ * where this position points.
115
+ *
116
+ * Read more about model positions and offsets in
117
+ * the {@glink framework/architecture/editing-engine#indexes-and-offsets Editing engine architecture} guide.
118
+ *
119
+ * @error model-position-path-incorrect
120
+ * @param position The incorrect position.
121
+ */
122
+ throw new CKEditorError('model-position-path-incorrect', this, { position: this });
123
+ }
124
+ }
125
+ if (parent.is('$text')) {
126
+ throw new CKEditorError('model-position-path-incorrect', this, { position: this });
127
+ }
128
+ return parent;
129
+ }
130
+ /**
131
+ * Position {@link module:engine/model/position~Position#offset offset} converted to an index in position's parent node. It is
132
+ * equal to the {@link module:engine/model/node~Node#index index} of a node after this position. If position is placed
133
+ * in text node, position index is equal to the index of that text node.
134
+ */
135
+ get index() {
136
+ return this.parent.offsetToIndex(this.offset);
137
+ }
138
+ /**
139
+ * Returns {@link module:engine/model/text~Text text node} instance in which this position is placed or `null` if this
140
+ * position is not in a text node.
141
+ */
142
+ get textNode() {
143
+ return getTextNodeAtPosition(this, this.parent);
144
+ }
145
+ /**
146
+ * Node directly after this position or `null` if this position is in text node.
147
+ */
148
+ get nodeAfter() {
149
+ // Cache the parent and reuse for performance reasons. See #6579 and #6582.
150
+ const parent = this.parent;
151
+ return getNodeAfterPosition(this, parent, getTextNodeAtPosition(this, parent));
152
+ }
153
+ /**
154
+ * Node directly before this position or `null` if this position is in text node.
155
+ */
156
+ get nodeBefore() {
157
+ // Cache the parent and reuse for performance reasons. See #6579 and #6582.
158
+ const parent = this.parent;
159
+ return getNodeBeforePosition(this, parent, getTextNodeAtPosition(this, parent));
160
+ }
161
+ /**
162
+ * Is `true` if position is at the beginning of its {@link module:engine/model/position~Position#parent parent}, `false` otherwise.
163
+ */
164
+ get isAtStart() {
165
+ return this.offset === 0;
166
+ }
167
+ /**
168
+ * Is `true` if position is at the end of its {@link module:engine/model/position~Position#parent parent}, `false` otherwise.
169
+ */
170
+ get isAtEnd() {
171
+ return this.offset == this.parent.maxOffset;
172
+ }
173
+ /**
174
+ * Checks whether this position is before or after given position.
175
+ *
176
+ * This method is safe to use it on non-existing positions (for example during operational transformation).
177
+ */
178
+ compareWith(otherPosition) {
179
+ if (this.root != otherPosition.root) {
180
+ return 'different';
181
+ }
182
+ const result = compareArrays(this.path, otherPosition.path);
183
+ switch (result) {
184
+ case 'same':
185
+ return 'same';
186
+ case 'prefix':
187
+ return 'before';
188
+ case 'extension':
189
+ return 'after';
190
+ default:
191
+ return this.path[result] < otherPosition.path[result] ? 'before' : 'after';
192
+ }
193
+ }
194
+ /**
195
+ * Gets the farthest position which matches the callback using
196
+ * {@link module:engine/model/treewalker~TreeWalker TreeWalker}.
197
+ *
198
+ * For example:
199
+ *
200
+ * ```ts
201
+ * getLastMatchingPosition( value => value.type == 'text' );
202
+ * // <paragraph>[]foo</paragraph> -> <paragraph>foo[]</paragraph>
203
+ *
204
+ * getLastMatchingPosition( value => value.type == 'text', { direction: 'backward' } );
205
+ * // <paragraph>foo[]</paragraph> -> <paragraph>[]foo</paragraph>
206
+ *
207
+ * getLastMatchingPosition( value => false );
208
+ * // Do not move the position.
209
+ * ```
210
+ *
211
+ * @param skip Callback function. Gets {@link module:engine/model/treewalker~TreeWalkerValue} and should
212
+ * return `true` if the value should be skipped or `false` if not.
213
+ * @param options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
214
+ *
215
+ * @returns The position after the last item which matches the `skip` callback test.
216
+ */
217
+ getLastMatchingPosition(skip, options = {}) {
218
+ options.startPosition = this;
219
+ const treeWalker = new TreeWalker(options);
220
+ treeWalker.skip(skip);
221
+ return treeWalker.position;
222
+ }
223
+ /**
224
+ * Returns a path to this position's parent. Parent path is equal to position {@link module:engine/model/position~Position#path path}
225
+ * but without the last item.
226
+ *
227
+ * This method is safe to use it on non-existing positions (for example during operational transformation).
228
+ *
229
+ * @returns Path to the parent.
230
+ */
231
+ getParentPath() {
232
+ return this.path.slice(0, -1);
233
+ }
234
+ /**
235
+ * Returns ancestors array of this position, that is this position's parent and its ancestors.
236
+ *
237
+ * @returns Array with ancestors.
238
+ */
239
+ getAncestors() {
240
+ const parent = this.parent;
241
+ if (parent.is('documentFragment')) {
242
+ return [parent];
243
+ }
244
+ else {
245
+ return parent.getAncestors({ includeSelf: true });
246
+ }
247
+ }
248
+ /**
249
+ * Returns the parent element of the given name. Returns null if the position is not inside the desired parent.
250
+ *
251
+ * @param parentName The name of the parent element to find.
252
+ */
253
+ findAncestor(parentName) {
254
+ const parent = this.parent;
255
+ if (parent.is('element')) {
256
+ return parent.findAncestor(parentName, { includeSelf: true });
257
+ }
258
+ return null;
259
+ }
260
+ /**
261
+ * Returns the slice of two position {@link #path paths} which is identical. The {@link #root roots}
262
+ * of these two paths must be identical.
263
+ *
264
+ * This method is safe to use it on non-existing positions (for example during operational transformation).
265
+ *
266
+ * @param position The second position.
267
+ * @returns The common path.
268
+ */
269
+ getCommonPath(position) {
270
+ if (this.root != position.root) {
271
+ return [];
272
+ }
273
+ // We find on which tree-level start and end have the lowest common ancestor
274
+ const cmp = compareArrays(this.path, position.path);
275
+ // If comparison returned string it means that arrays are same.
276
+ const diffAt = (typeof cmp == 'string') ? Math.min(this.path.length, position.path.length) : cmp;
277
+ return this.path.slice(0, diffAt);
278
+ }
279
+ /**
280
+ * Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
281
+ * which is a common ancestor of both positions. The {@link #root roots} of these two positions must be identical.
282
+ *
283
+ * @param position The second position.
284
+ */
285
+ getCommonAncestor(position) {
286
+ const ancestorsA = this.getAncestors();
287
+ const ancestorsB = position.getAncestors();
288
+ let i = 0;
289
+ while (ancestorsA[i] == ancestorsB[i] && ancestorsA[i]) {
290
+ i++;
291
+ }
292
+ return i === 0 ? null : ancestorsA[i - 1];
293
+ }
294
+ /**
295
+ * Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
296
+ * is shifted by `shift` value (can be a negative value).
297
+ *
298
+ * This method is safe to use it on non-existing positions (for example during operational transformation).
299
+ *
300
+ * @param shift Offset shift. Can be a negative value.
301
+ * @returns Shifted position.
302
+ */
303
+ getShiftedBy(shift) {
304
+ const shifted = this.clone();
305
+ const offset = shifted.offset + shift;
306
+ shifted.offset = offset < 0 ? 0 : offset;
307
+ return shifted;
308
+ }
309
+ /**
310
+ * Checks whether this position is after given position.
311
+ *
312
+ * This method is safe to use it on non-existing positions (for example during operational transformation).
313
+ *
314
+ * @see module:engine/model/position~Position#isBefore
315
+ * @param otherPosition Position to compare with.
316
+ * @returns True if this position is after given position.
317
+ */
318
+ isAfter(otherPosition) {
319
+ return this.compareWith(otherPosition) == 'after';
320
+ }
321
+ /**
322
+ * Checks whether this position is before given position.
323
+ *
324
+ * **Note:** watch out when using negation of the value returned by this method, because the negation will also
325
+ * be `true` if positions are in different roots and you might not expect this. You should probably use
326
+ * `a.isAfter( b ) || a.isEqual( b )` or `!a.isBefore( p ) && a.root == b.root` in most scenarios. If your
327
+ * condition uses multiple `isAfter` and `isBefore` checks, build them so they do not use negated values, i.e.:
328
+ *
329
+ * ```ts
330
+ * if ( a.isBefore( b ) && c.isAfter( d ) ) {
331
+ * // do A.
332
+ * } else {
333
+ * // do B.
334
+ * }
335
+ * ```
336
+ *
337
+ * or, if you have only one if-branch:
338
+ *
339
+ * ```ts
340
+ * if ( !( a.isBefore( b ) && c.isAfter( d ) ) {
341
+ * // do B.
342
+ * }
343
+ * ```
344
+ *
345
+ * rather than:
346
+ *
347
+ * ```ts
348
+ * if ( !a.isBefore( b ) || && !c.isAfter( d ) ) {
349
+ * // do B.
350
+ * } else {
351
+ * // do A.
352
+ * }
353
+ * ```
354
+ *
355
+ * This method is safe to use it on non-existing positions (for example during operational transformation).
356
+ *
357
+ * @param otherPosition Position to compare with.
358
+ * @returns True if this position is before given position.
359
+ */
360
+ isBefore(otherPosition) {
361
+ return this.compareWith(otherPosition) == 'before';
362
+ }
363
+ /**
364
+ * Checks whether this position is equal to given position.
365
+ *
366
+ * This method is safe to use it on non-existing positions (for example during operational transformation).
367
+ *
368
+ * @param otherPosition Position to compare with.
369
+ * @returns True if positions are same.
370
+ */
371
+ isEqual(otherPosition) {
372
+ return this.compareWith(otherPosition) == 'same';
373
+ }
374
+ /**
375
+ * Checks whether this position is touching given position. Positions touch when there are no text nodes
376
+ * or empty nodes in a range between them. Technically, those positions are not equal but in many cases
377
+ * they are very similar or even indistinguishable.
378
+ *
379
+ * @param otherPosition Position to compare with.
380
+ * @returns True if positions touch.
381
+ */
382
+ isTouching(otherPosition) {
383
+ if (this.root !== otherPosition.root) {
384
+ return false;
385
+ }
386
+ const commonLevel = Math.min(this.path.length, otherPosition.path.length);
387
+ for (let level = 0; level < commonLevel; level++) {
388
+ const diff = this.path[level] - otherPosition.path[level];
389
+ // Positions are spread by a node, so they are not touching.
390
+ if (diff < -1 || diff > 1) {
391
+ return false;
392
+ }
393
+ else if (diff === 1) {
394
+ // `otherPosition` is on the left.
395
+ // `this` is on the right.
396
+ return checkTouchingBranch(otherPosition, this, level);
397
+ }
398
+ else if (diff === -1) {
399
+ // `this` is on the left.
400
+ // `otherPosition` is on the right.
401
+ return checkTouchingBranch(this, otherPosition, level);
402
+ }
403
+ // `diff === 0`.
404
+ // Positions are inside the same element on this level, compare deeper.
405
+ }
406
+ // If we ended up here, it means that positions paths have the same beginning.
407
+ // If the paths have the same length, then it means that they are identical, so the positions are same.
408
+ if (this.path.length === otherPosition.path.length) {
409
+ return true;
410
+ }
411
+ // If positions have different length of paths, then the common part is the same.
412
+ // In this case, the "shorter" position is on the left, the "longer" position is on the right.
413
+ //
414
+ // If the positions are touching, the "longer" position must have only zeroes. For example:
415
+ // [ 1, 2 ] vs [ 1, 2, 0 ]
416
+ // [ 1, 2 ] vs [ 1, 2, 0, 0, 0 ]
417
+ else if (this.path.length > otherPosition.path.length) {
418
+ return checkOnlyZeroes(this.path, commonLevel);
419
+ }
420
+ else {
421
+ return checkOnlyZeroes(otherPosition.path, commonLevel);
422
+ }
423
+ }
424
+ /**
425
+ * Checks if two positions are in the same parent.
426
+ *
427
+ * This method is safe to use it on non-existing positions (for example during operational transformation).
428
+ *
429
+ * @param position Position to compare with.
430
+ * @returns `true` if positions have the same parent, `false` otherwise.
431
+ */
432
+ hasSameParentAs(position) {
433
+ if (this.root !== position.root) {
434
+ return false;
435
+ }
436
+ const thisParentPath = this.getParentPath();
437
+ const posParentPath = position.getParentPath();
438
+ return compareArrays(thisParentPath, posParentPath) == 'same';
439
+ }
440
+ /**
441
+ * Returns a copy of this position that is transformed by given `operation`.
442
+ *
443
+ * The new position's parameters are updated accordingly to the effect of the `operation`.
444
+ *
445
+ * For example, if `n` nodes are inserted before the position, the returned position {@link ~Position#offset} will be
446
+ * increased by `n`. If the position was in a merged element, it will be accordingly moved to the new element, etc.
447
+ *
448
+ * This method is safe to use it on non-existing positions (for example during operational transformation).
449
+ *
450
+ * @param operation Operation to transform by.
451
+ * @returns Transformed position.
452
+ */
453
+ getTransformedByOperation(operation) {
454
+ let result;
455
+ switch (operation.type) {
456
+ case 'insert':
457
+ result = this._getTransformedByInsertOperation(operation);
458
+ break;
459
+ case 'move':
460
+ case 'remove':
461
+ case 'reinsert':
462
+ result = this._getTransformedByMoveOperation(operation);
463
+ break;
464
+ case 'split':
465
+ result = this._getTransformedBySplitOperation(operation);
466
+ break;
467
+ case 'merge':
468
+ result = this._getTransformedByMergeOperation(operation);
469
+ break;
470
+ default:
471
+ result = Position._createAt(this);
472
+ break;
473
+ }
474
+ return result;
475
+ }
476
+ /**
477
+ * Returns a copy of this position transformed by an insert operation.
478
+ *
479
+ * @internal
480
+ */
481
+ _getTransformedByInsertOperation(operation) {
482
+ return this._getTransformedByInsertion(operation.position, operation.howMany);
483
+ }
484
+ /**
485
+ * Returns a copy of this position transformed by a move operation.
486
+ *
487
+ * @internal
488
+ */
489
+ _getTransformedByMoveOperation(operation) {
490
+ return this._getTransformedByMove(operation.sourcePosition, operation.targetPosition, operation.howMany);
491
+ }
492
+ /**
493
+ * Returns a copy of this position transformed by a split operation.
494
+ *
495
+ * @internal
496
+ */
497
+ _getTransformedBySplitOperation(operation) {
498
+ const movedRange = operation.movedRange;
499
+ const isContained = movedRange.containsPosition(this) ||
500
+ (movedRange.start.isEqual(this) && this.stickiness == 'toNext');
501
+ if (isContained) {
502
+ return this._getCombined(operation.splitPosition, operation.moveTargetPosition);
503
+ }
504
+ else {
505
+ if (operation.graveyardPosition) {
506
+ return this._getTransformedByMove(operation.graveyardPosition, operation.insertionPosition, 1);
507
+ }
508
+ else {
509
+ return this._getTransformedByInsertion(operation.insertionPosition, 1);
510
+ }
511
+ }
512
+ }
513
+ /**
514
+ * Returns a copy of this position transformed by merge operation.
515
+ *
516
+ * @internal
517
+ */
518
+ _getTransformedByMergeOperation(operation) {
519
+ const movedRange = operation.movedRange;
520
+ const isContained = movedRange.containsPosition(this) || movedRange.start.isEqual(this);
521
+ let pos;
522
+ if (isContained) {
523
+ pos = this._getCombined(operation.sourcePosition, operation.targetPosition);
524
+ if (operation.sourcePosition.isBefore(operation.targetPosition)) {
525
+ // Above happens during OT when the merged element is moved before the merged-to element.
526
+ pos = pos._getTransformedByDeletion(operation.deletionPosition, 1);
527
+ }
528
+ }
529
+ else if (this.isEqual(operation.deletionPosition)) {
530
+ pos = Position._createAt(operation.deletionPosition);
531
+ }
532
+ else {
533
+ pos = this._getTransformedByMove(operation.deletionPosition, operation.graveyardPosition, 1);
534
+ }
535
+ return pos;
536
+ }
537
+ /**
538
+ * Returns a copy of this position that is updated by removing `howMany` nodes starting from `deletePosition`.
539
+ * It may happen that this position is in a removed node. If that is the case, `null` is returned instead.
540
+ *
541
+ * @internal
542
+ * @param deletePosition Position before the first removed node.
543
+ * @param howMany How many nodes are removed.
544
+ * @returns Transformed position or `null`.
545
+ */
546
+ _getTransformedByDeletion(deletePosition, howMany) {
547
+ const transformed = Position._createAt(this);
548
+ // This position can't be affected if deletion was in a different root.
549
+ if (this.root != deletePosition.root) {
550
+ return transformed;
551
+ }
552
+ if (compareArrays(deletePosition.getParentPath(), this.getParentPath()) == 'same') {
553
+ // If nodes are removed from the node that is pointed by this position...
554
+ if (deletePosition.offset < this.offset) {
555
+ // And are removed from before an offset of that position...
556
+ if (deletePosition.offset + howMany > this.offset) {
557
+ // Position is in removed range, it's no longer in the tree.
558
+ return null;
559
+ }
560
+ else {
561
+ // Decrement the offset accordingly.
562
+ transformed.offset -= howMany;
563
+ }
564
+ }
565
+ }
566
+ else if (compareArrays(deletePosition.getParentPath(), this.getParentPath()) == 'prefix') {
567
+ // If nodes are removed from a node that is on a path to this position...
568
+ const i = deletePosition.path.length - 1;
569
+ if (deletePosition.offset <= this.path[i]) {
570
+ // And are removed from before next node of that path...
571
+ if (deletePosition.offset + howMany > this.path[i]) {
572
+ // If the next node of that path is removed return null
573
+ // because the node containing this position got removed.
574
+ return null;
575
+ }
576
+ else {
577
+ // Otherwise, decrement index on that path.
578
+ transformed.path[i] -= howMany;
579
+ }
580
+ }
581
+ }
582
+ return transformed;
583
+ }
584
+ /**
585
+ * Returns a copy of this position that is updated by inserting `howMany` nodes at `insertPosition`.
586
+ *
587
+ * @internal
588
+ * @param insertPosition Position where nodes are inserted.
589
+ * @param howMany How many nodes are inserted.
590
+ * @returns Transformed position.
591
+ */
592
+ _getTransformedByInsertion(insertPosition, howMany) {
593
+ const transformed = Position._createAt(this);
594
+ // This position can't be affected if insertion was in a different root.
595
+ if (this.root != insertPosition.root) {
596
+ return transformed;
597
+ }
598
+ if (compareArrays(insertPosition.getParentPath(), this.getParentPath()) == 'same') {
599
+ // If nodes are inserted in the node that is pointed by this position...
600
+ if (insertPosition.offset < this.offset || (insertPosition.offset == this.offset && this.stickiness != 'toPrevious')) {
601
+ // And are inserted before an offset of that position...
602
+ // "Push" this positions offset.
603
+ transformed.offset += howMany;
604
+ }
605
+ }
606
+ else if (compareArrays(insertPosition.getParentPath(), this.getParentPath()) == 'prefix') {
607
+ // If nodes are inserted in a node that is on a path to this position...
608
+ const i = insertPosition.path.length - 1;
609
+ if (insertPosition.offset <= this.path[i]) {
610
+ // And are inserted before next node of that path...
611
+ // "Push" the index on that path.
612
+ transformed.path[i] += howMany;
613
+ }
614
+ }
615
+ return transformed;
616
+ }
617
+ /**
618
+ * Returns a copy of this position that is updated by moving `howMany` nodes from `sourcePosition` to `targetPosition`.
619
+ *
620
+ * @internal
621
+ * @param sourcePosition Position before the first element to move.
622
+ * @param targetPosition Position where moved elements will be inserted.
623
+ * @param howMany How many consecutive nodes to move, starting from `sourcePosition`.
624
+ * @returns Transformed position.
625
+ */
626
+ _getTransformedByMove(sourcePosition, targetPosition, howMany) {
627
+ // Update target position, as it could be affected by nodes removal.
628
+ targetPosition = targetPosition._getTransformedByDeletion(sourcePosition, howMany);
629
+ if (sourcePosition.isEqual(targetPosition)) {
630
+ // If `targetPosition` is equal to `sourcePosition` this isn't really any move. Just return position as it is.
631
+ return Position._createAt(this);
632
+ }
633
+ // Moving a range removes nodes from their original position. We acknowledge this by proper transformation.
634
+ const transformed = this._getTransformedByDeletion(sourcePosition, howMany);
635
+ const isMoved = transformed === null ||
636
+ (sourcePosition.isEqual(this) && this.stickiness == 'toNext') ||
637
+ (sourcePosition.getShiftedBy(howMany).isEqual(this) && this.stickiness == 'toPrevious');
638
+ if (isMoved) {
639
+ // This position is inside moved range (or sticks to it).
640
+ // In this case, we calculate a combination of this position, move source position and target position.
641
+ return this._getCombined(sourcePosition, targetPosition);
642
+ }
643
+ else {
644
+ // This position is not inside a removed range.
645
+ //
646
+ // In next step, we simply reflect inserting `howMany` nodes, which might further affect the position.
647
+ return transformed._getTransformedByInsertion(targetPosition, howMany);
648
+ }
649
+ }
650
+ /**
651
+ * Returns a new position that is a combination of this position and given positions.
652
+ *
653
+ * The combined position is a copy of this position transformed by moving a range starting at `source` position
654
+ * to the `target` position. It is expected that this position is inside the moved range.
655
+ *
656
+ * Example:
657
+ *
658
+ * ```ts
659
+ * let original = model.createPositionFromPath( root, [ 2, 3, 1 ] );
660
+ * let source = model.createPositionFromPath( root, [ 2, 2 ] );
661
+ * let target = model.createPositionFromPath( otherRoot, [ 1, 1, 3 ] );
662
+ * original._getCombined( source, target ); // path is [ 1, 1, 4, 1 ], root is `otherRoot`
663
+ * ```
664
+ *
665
+ * Explanation:
666
+ *
667
+ * We have a position `[ 2, 3, 1 ]` and move some nodes from `[ 2, 2 ]` to `[ 1, 1, 3 ]`. The original position
668
+ * was inside moved nodes and now should point to the new place. The moved nodes will be after
669
+ * positions `[ 1, 1, 3 ]`, `[ 1, 1, 4 ]`, `[ 1, 1, 5 ]`. Since our position was in the second moved node,
670
+ * the transformed position will be in a sub-tree of a node at `[ 1, 1, 4 ]`. Looking at original path, we
671
+ * took care of `[ 2, 3 ]` part of it. Now we have to add the rest of the original path to the transformed path.
672
+ * Finally, the transformed position will point to `[ 1, 1, 4, 1 ]`.
673
+ *
674
+ * @internal
675
+ * @param source Beginning of the moved range.
676
+ * @param target Position where the range is moved.
677
+ * @returns Combined position.
678
+ */
679
+ _getCombined(source, target) {
680
+ const i = source.path.length - 1;
681
+ // The first part of a path to combined position is a path to the place where nodes were moved.
682
+ const combined = Position._createAt(target);
683
+ combined.stickiness = this.stickiness;
684
+ // Then we have to update the rest of the path.
685
+ // Fix the offset because this position might be after `from` position and we have to reflect that.
686
+ combined.offset = combined.offset + this.path[i] - source.offset;
687
+ // Then, add the rest of the path.
688
+ // If this position is at the same level as `from` position nothing will get added.
689
+ combined.path = [...combined.path, ...this.path.slice(i + 1)];
690
+ return combined;
691
+ }
692
+ /**
693
+ * @inheritDoc
694
+ */
695
+ toJSON() {
696
+ return {
697
+ root: this.root.toJSON(),
698
+ path: Array.from(this.path),
699
+ stickiness: this.stickiness
700
+ };
701
+ }
702
+ /**
703
+ * Returns a new position that is equal to current position.
704
+ */
705
+ clone() {
706
+ return new this.constructor(this.root, this.path, this.stickiness);
707
+ }
708
+ /**
709
+ * Creates position at the given location. The location can be specified as:
710
+ *
711
+ * * a {@link module:engine/model/position~Position position},
712
+ * * parent element and offset (offset defaults to `0`),
713
+ * * parent element and `'end'` (sets position at the end of that element),
714
+ * * {@link module:engine/model/item~Item model item} and `'before'` or `'after'` (sets position before or after given model item).
715
+ *
716
+ * This method is a shortcut to other factory methods such as:
717
+ *
718
+ * * {@link module:engine/model/position~Position._createBefore},
719
+ * * {@link module:engine/model/position~Position._createAfter}.
720
+ *
721
+ * @internal
722
+ * @param offset Offset or one of the flags. Used only when the first parameter is a {@link module:engine/model/item~Item model item}.
723
+ * @param stickiness Position stickiness. Used only when the first parameter is a {@link module:engine/model/item~Item model item}.
724
+ */
725
+ static _createAt(itemOrPosition, offset, stickiness = 'toNone') {
726
+ if (itemOrPosition instanceof Position) {
727
+ return new Position(itemOrPosition.root, itemOrPosition.path, itemOrPosition.stickiness);
728
+ }
729
+ else {
730
+ const node = itemOrPosition;
731
+ if (offset == 'end') {
732
+ offset = node.maxOffset;
733
+ }
734
+ else if (offset == 'before') {
735
+ return this._createBefore(node, stickiness);
736
+ }
737
+ else if (offset == 'after') {
738
+ return this._createAfter(node, stickiness);
739
+ }
740
+ else if (offset !== 0 && !offset) {
741
+ /**
742
+ * {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}
743
+ * requires the offset to be specified when the first parameter is a model item.
744
+ *
745
+ * @error model-createpositionat-offset-required
746
+ */
747
+ throw new CKEditorError('model-createpositionat-offset-required', [this, itemOrPosition]);
748
+ }
749
+ if (!node.is('element') && !node.is('documentFragment')) {
750
+ /**
751
+ * Position parent have to be a model element or model document fragment.
752
+ *
753
+ * @error model-position-parent-incorrect
754
+ */
755
+ throw new CKEditorError('model-position-parent-incorrect', [this, itemOrPosition]);
756
+ }
757
+ const path = node.getPath();
758
+ path.push(offset);
759
+ return new this(node.root, path, stickiness);
760
+ }
761
+ }
762
+ /**
763
+ * Creates a new position, after given {@link module:engine/model/item~Item model item}.
764
+ *
765
+ * @internal
766
+ * @param item Item after which the position should be placed.
767
+ * @param stickiness Position stickiness.
768
+ */
769
+ static _createAfter(item, stickiness) {
770
+ if (!item.parent) {
771
+ /**
772
+ * You can not make a position after a root element.
773
+ *
774
+ * @error model-position-after-root
775
+ * @param root
776
+ */
777
+ throw new CKEditorError('model-position-after-root', [this, item], { root: item });
778
+ }
779
+ return this._createAt(item.parent, item.endOffset, stickiness);
780
+ }
781
+ /**
782
+ * Creates a new position, before the given {@link module:engine/model/item~Item model item}.
783
+ *
784
+ * @internal
785
+ * @param item Item before which the position should be placed.
786
+ * @param stickiness Position stickiness.
787
+ */
788
+ static _createBefore(item, stickiness) {
789
+ if (!item.parent) {
790
+ /**
791
+ * You can not make a position before a root element.
792
+ *
793
+ * @error model-position-before-root
794
+ * @param root
795
+ */
796
+ throw new CKEditorError('model-position-before-root', item, { root: item });
797
+ }
798
+ return this._createAt(item.parent, item.startOffset, stickiness);
799
+ }
800
+ /**
801
+ * Creates a `Position` instance from given plain object (i.e. parsed JSON string).
802
+ *
803
+ * @param json Plain object to be converted to `Position`.
804
+ * @param doc Document object that will be position owner.
805
+ * @returns `Position` instance created using given plain object.
806
+ */
807
+ static fromJSON(json, doc) {
808
+ if (json.root === '$graveyard') {
809
+ const pos = new Position(doc.graveyard, json.path);
810
+ pos.stickiness = json.stickiness;
811
+ return pos;
812
+ }
813
+ if (!doc.getRoot(json.root)) {
814
+ /**
815
+ * Cannot create position for document. Root with specified name does not exist.
816
+ *
817
+ * @error model-position-fromjson-no-root
818
+ * @param rootName
819
+ */
820
+ throw new CKEditorError('model-position-fromjson-no-root', doc, { rootName: json.root });
821
+ }
822
+ return new Position(doc.getRoot(json.root), json.path, json.stickiness);
823
+ }
824
+ }
825
+ // The magic of type inference using `is` method is centralized in `TypeCheckable` class.
826
+ // Proper overload would interfere with that.
827
+ Position.prototype.is = function (type) {
828
+ return type === 'position' || type === 'model:position';
829
+ };
830
+ /**
831
+ * Returns a text node at the given position.
832
+ *
833
+ * This is a helper function optimized to reuse the position parent instance for performance reasons.
834
+ *
835
+ * Normally, you should use {@link module:engine/model/position~Position#textNode `Position#textNode`}.
836
+ * If you start hitting performance issues with {@link module:engine/model/position~Position#parent `Position#parent`}
837
+ * check if your algorithm does not access it multiple times (which can happen directly or indirectly via other position properties).
838
+ *
839
+ * See https://github.com/ckeditor/ckeditor5/issues/6579.
840
+ *
841
+ * See also:
842
+ *
843
+ * * {@link module:engine/model/position~getNodeAfterPosition}
844
+ * * {@link module:engine/model/position~getNodeBeforePosition}
845
+ *
846
+ * @param positionParent The parent of the given position.
847
+ */
848
+ export function getTextNodeAtPosition(position, positionParent) {
849
+ const node = positionParent.getChild(positionParent.offsetToIndex(position.offset));
850
+ if (node && node.is('$text') && node.startOffset < position.offset) {
851
+ return node;
852
+ }
853
+ return null;
854
+ }
855
+ /**
856
+ * Returns the node after the given position.
857
+ *
858
+ * This is a helper function optimized to reuse the position parent instance and the calculation of the text node at the
859
+ * specific position for performance reasons.
860
+ *
861
+ * Normally, you should use {@link module:engine/model/position~Position#nodeAfter `Position#nodeAfter`}.
862
+ * If you start hitting performance issues with {@link module:engine/model/position~Position#parent `Position#parent`} and/or
863
+ * {@link module:engine/model/position~Position#textNode `Position#textNode`}
864
+ * check if your algorithm does not access those properties multiple times
865
+ * (which can happen directly or indirectly via other position properties).
866
+ *
867
+ * See https://github.com/ckeditor/ckeditor5/issues/6579 and https://github.com/ckeditor/ckeditor5/issues/6582.
868
+ *
869
+ * See also:
870
+ *
871
+ * * {@link module:engine/model/position~getTextNodeAtPosition}
872
+ * * {@link module:engine/model/position~getNodeBeforePosition}
873
+ *
874
+ * @param positionParent The parent of the given position.
875
+ * @param textNode Text node at the given position.
876
+ */
877
+ export function getNodeAfterPosition(position, positionParent, textNode) {
878
+ if (textNode !== null) {
879
+ return null;
880
+ }
881
+ return positionParent.getChild(positionParent.offsetToIndex(position.offset));
882
+ }
883
+ /**
884
+ * Returns the node before the given position.
885
+ *
886
+ * Refer to {@link module:engine/model/position~getNodeBeforePosition} for documentation on when to use this util method.
887
+ *
888
+ * See also:
889
+ *
890
+ * * {@link module:engine/model/position~getTextNodeAtPosition}
891
+ * * {@link module:engine/model/position~getNodeAfterPosition}
892
+ *
893
+ * @param positionParent The parent of the given position.
894
+ * @param textNode Text node at the given position.
895
+ */
896
+ export function getNodeBeforePosition(position, positionParent, textNode) {
897
+ if (textNode !== null) {
898
+ return null;
899
+ }
900
+ return positionParent.getChild(positionParent.offsetToIndex(position.offset) - 1);
901
+ }
902
+ /**
903
+ * This is a helper function for `Position#isTouching()`.
904
+ *
905
+ * It checks whether to given positions are touching, considering that they have the same root and paths
906
+ * until given level, and at given level they differ by 1 (so they are branching at `level` point).
907
+ *
908
+ * The exact requirements for touching positions are described in `Position#isTouching()` and also
909
+ * in the body of this function.
910
+ *
911
+ * @param left Position "on the left" (it is before `right`).
912
+ * @param right Position "on the right" (it is after `left`).
913
+ * @param level Level on which the positions are different.
914
+ */
915
+ function checkTouchingBranch(left, right, level) {
916
+ if (level + 1 === left.path.length) {
917
+ // Left position does not have any more entries after the point where the positions differ.
918
+ // [ 2 ] vs [ 3 ]
919
+ // [ 2 ] vs [ 3, 0, 0 ]
920
+ // The positions are spread by node at [ 2 ].
921
+ return false;
922
+ }
923
+ if (!checkOnlyZeroes(right.path, level + 1)) {
924
+ // Right position does not have only zeroes, so we have situation like:
925
+ // [ 2, maxOffset ] vs [ 3, 1 ]
926
+ // [ 2, maxOffset ] vs [ 3, 1, 0, 0 ]
927
+ // The positions are spread by node at [ 3, 0 ].
928
+ return false;
929
+ }
930
+ if (!checkOnlyMaxOffset(left, level + 1)) {
931
+ // Left position does not have only max offsets, so we have situation like:
932
+ // [ 2, 4 ] vs [ 3 ]
933
+ // [ 2, 4 ] vs [ 3, 0, 0 ]
934
+ // The positions are spread by node at [ 2, 5 ].
935
+ return false;
936
+ }
937
+ // Left position has only max offsets and right position has only zeroes or nothing.
938
+ // [ 2, maxOffset ] vs [ 3 ]
939
+ // [ 2, maxOffset, maxOffset ] vs [ 3, 0 ]
940
+ // There are not elements between positions. The positions are touching.
941
+ return true;
942
+ }
943
+ /**
944
+ * Checks whether for given array, starting from given index until the end of the array, all items are `0`s.
945
+ *
946
+ * This is a helper function for `Position#isTouching()`.
947
+ */
948
+ function checkOnlyZeroes(arr, idx) {
949
+ while (idx < arr.length) {
950
+ if (arr[idx] !== 0) {
951
+ return false;
952
+ }
953
+ idx++;
954
+ }
955
+ return true;
956
+ }
957
+ /**
958
+ * Checks whether for given position, starting from given path level, whether the position is at the end of
959
+ * its parent and whether each element on the path to the position is also at at the end of its parent.
960
+ *
961
+ * This is a helper function for `Position#isTouching()`.
962
+ */
963
+ function checkOnlyMaxOffset(pos, level) {
964
+ let parent = pos.parent;
965
+ let idx = pos.path.length - 1;
966
+ let add = 0;
967
+ while (idx >= level) {
968
+ if (pos.path[idx] + add !== parent.maxOffset) {
969
+ return false;
970
+ }
971
+ // After the first check, we "go up", and check whether the position's parent-parent is the last element.
972
+ // However, we need to add 1 to the value in the path to "simulate" moving the path after the parent.
973
+ // It happens just once.
974
+ add = 1;
975
+ idx--;
976
+ parent = parent.parent;
977
+ }
978
+ return true;
979
+ }