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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/LICENSE.md +1 -1
  2. package/{src → dist}/engineconfig.d.ts +6 -15
  3. package/dist/index-editor.css +38 -15
  4. package/dist/index.css +37 -37
  5. package/dist/index.css.map +1 -1
  6. package/{src → dist}/index.d.ts +0 -1
  7. package/dist/index.js +588 -94
  8. package/dist/index.js.map +1 -1
  9. package/{src → dist}/model/model.d.ts +10 -4
  10. package/{src → dist}/model/selection.d.ts +1 -1
  11. package/{src → dist}/view/downcastwriter.d.ts +3 -2
  12. package/{src → dist}/view/element.d.ts +2 -2
  13. package/{src → dist}/view/matcher.d.ts +4 -2
  14. package/dist/view/styles/background.d.ts +18 -0
  15. package/{src → dist}/view/styles/border.d.ts +0 -12
  16. package/{src → dist}/view/styles/margin.d.ts +0 -13
  17. package/{src → dist}/view/styles/padding.d.ts +0 -13
  18. package/{src → dist}/view/styles/utils.d.ts +12 -0
  19. package/package.json +20 -39
  20. package/src/controller/datacontroller.js +0 -522
  21. package/src/controller/editingcontroller.js +0 -181
  22. package/src/conversion/conversion.js +0 -606
  23. package/src/conversion/conversionhelpers.js +0 -33
  24. package/src/conversion/downcastdispatcher.js +0 -563
  25. package/src/conversion/downcasthelpers.js +0 -2160
  26. package/src/conversion/mapper.js +0 -1050
  27. package/src/conversion/modelconsumable.js +0 -331
  28. package/src/conversion/upcastdispatcher.js +0 -470
  29. package/src/conversion/upcasthelpers.js +0 -952
  30. package/src/conversion/viewconsumable.js +0 -541
  31. package/src/dataprocessor/basichtmlwriter.js +0 -22
  32. package/src/dataprocessor/dataprocessor.js +0 -5
  33. package/src/dataprocessor/htmldataprocessor.js +0 -107
  34. package/src/dataprocessor/htmlwriter.js +0 -5
  35. package/src/dataprocessor/xmldataprocessor.js +0 -127
  36. package/src/dev-utils/model.js +0 -396
  37. package/src/dev-utils/operationreplayer.js +0 -116
  38. package/src/dev-utils/utils.js +0 -122
  39. package/src/dev-utils/view.js +0 -990
  40. package/src/engineconfig.js +0 -5
  41. package/src/index.js +0 -134
  42. package/src/legacyerrors.js +0 -17
  43. package/src/model/batch.js +0 -98
  44. package/src/model/differ.js +0 -1288
  45. package/src/model/document.js +0 -398
  46. package/src/model/documentfragment.js +0 -332
  47. package/src/model/documentselection.js +0 -1026
  48. package/src/model/element.js +0 -323
  49. package/src/model/history.js +0 -206
  50. package/src/model/item.js +0 -5
  51. package/src/model/liveposition.js +0 -93
  52. package/src/model/liverange.js +0 -121
  53. package/src/model/markercollection.js +0 -436
  54. package/src/model/model.js +0 -866
  55. package/src/model/node.js +0 -371
  56. package/src/model/nodelist.js +0 -244
  57. package/src/model/operation/attributeoperation.js +0 -172
  58. package/src/model/operation/detachoperation.js +0 -87
  59. package/src/model/operation/insertoperation.js +0 -153
  60. package/src/model/operation/markeroperation.js +0 -136
  61. package/src/model/operation/mergeoperation.js +0 -184
  62. package/src/model/operation/moveoperation.js +0 -179
  63. package/src/model/operation/nooperation.js +0 -48
  64. package/src/model/operation/operation.js +0 -78
  65. package/src/model/operation/operationfactory.js +0 -44
  66. package/src/model/operation/renameoperation.js +0 -128
  67. package/src/model/operation/rootattributeoperation.js +0 -173
  68. package/src/model/operation/rootoperation.js +0 -106
  69. package/src/model/operation/splitoperation.js +0 -214
  70. package/src/model/operation/transform.js +0 -2211
  71. package/src/model/operation/utils.js +0 -217
  72. package/src/model/position.js +0 -1041
  73. package/src/model/range.js +0 -880
  74. package/src/model/rootelement.js +0 -82
  75. package/src/model/schema.js +0 -1542
  76. package/src/model/selection.js +0 -814
  77. package/src/model/text.js +0 -92
  78. package/src/model/textproxy.js +0 -202
  79. package/src/model/treewalker.js +0 -313
  80. package/src/model/typecheckable.js +0 -16
  81. package/src/model/utils/autoparagraphing.js +0 -63
  82. package/src/model/utils/deletecontent.js +0 -509
  83. package/src/model/utils/getselectedcontent.js +0 -126
  84. package/src/model/utils/insertcontent.js +0 -750
  85. package/src/model/utils/insertobject.js +0 -135
  86. package/src/model/utils/modifyselection.js +0 -187
  87. package/src/model/utils/selection-post-fixer.js +0 -264
  88. package/src/model/writer.js +0 -1318
  89. package/src/view/attributeelement.js +0 -220
  90. package/src/view/containerelement.js +0 -91
  91. package/src/view/datatransfer.js +0 -106
  92. package/src/view/document.js +0 -139
  93. package/src/view/documentfragment.js +0 -251
  94. package/src/view/documentselection.js +0 -270
  95. package/src/view/domconverter.js +0 -1661
  96. package/src/view/downcastwriter.js +0 -1589
  97. package/src/view/editableelement.js +0 -74
  98. package/src/view/element.js +0 -1053
  99. package/src/view/elementdefinition.js +0 -5
  100. package/src/view/emptyelement.js +0 -83
  101. package/src/view/filler.js +0 -161
  102. package/src/view/item.js +0 -5
  103. package/src/view/matcher.js +0 -437
  104. package/src/view/node.js +0 -238
  105. package/src/view/observer/arrowkeysobserver.js +0 -40
  106. package/src/view/observer/bubblingemittermixin.js +0 -215
  107. package/src/view/observer/bubblingeventinfo.js +0 -49
  108. package/src/view/observer/clickobserver.js +0 -26
  109. package/src/view/observer/compositionobserver.js +0 -64
  110. package/src/view/observer/domeventdata.js +0 -63
  111. package/src/view/observer/domeventobserver.js +0 -81
  112. package/src/view/observer/fakeselectionobserver.js +0 -95
  113. package/src/view/observer/focusobserver.js +0 -166
  114. package/src/view/observer/inputobserver.js +0 -236
  115. package/src/view/observer/keyobserver.js +0 -36
  116. package/src/view/observer/mouseobserver.js +0 -26
  117. package/src/view/observer/mutationobserver.js +0 -219
  118. package/src/view/observer/observer.js +0 -92
  119. package/src/view/observer/pointerobserver.js +0 -26
  120. package/src/view/observer/selectionobserver.js +0 -318
  121. package/src/view/observer/tabobserver.js +0 -42
  122. package/src/view/observer/touchobserver.js +0 -26
  123. package/src/view/placeholder.js +0 -285
  124. package/src/view/position.js +0 -341
  125. package/src/view/range.js +0 -451
  126. package/src/view/rawelement.js +0 -115
  127. package/src/view/renderer.js +0 -1148
  128. package/src/view/rooteditableelement.js +0 -78
  129. package/src/view/selection.js +0 -594
  130. package/src/view/styles/background.d.ts +0 -33
  131. package/src/view/styles/background.js +0 -74
  132. package/src/view/styles/border.js +0 -316
  133. package/src/view/styles/margin.js +0 -34
  134. package/src/view/styles/padding.js +0 -34
  135. package/src/view/styles/utils.js +0 -219
  136. package/src/view/stylesmap.js +0 -941
  137. package/src/view/text.js +0 -110
  138. package/src/view/textproxy.js +0 -136
  139. package/src/view/tokenlist.js +0 -194
  140. package/src/view/treewalker.js +0 -389
  141. package/src/view/typecheckable.js +0 -19
  142. package/src/view/uielement.js +0 -194
  143. package/src/view/upcastwriter.js +0 -363
  144. package/src/view/view.js +0 -579
  145. package/theme/placeholder.css +0 -36
  146. package/theme/renderer.css +0 -9
  147. /package/{src → dist}/controller/datacontroller.d.ts +0 -0
  148. /package/{src → dist}/controller/editingcontroller.d.ts +0 -0
  149. /package/{src → dist}/conversion/conversion.d.ts +0 -0
  150. /package/{src → dist}/conversion/conversionhelpers.d.ts +0 -0
  151. /package/{src → dist}/conversion/downcastdispatcher.d.ts +0 -0
  152. /package/{src → dist}/conversion/downcasthelpers.d.ts +0 -0
  153. /package/{src → dist}/conversion/mapper.d.ts +0 -0
  154. /package/{src → dist}/conversion/modelconsumable.d.ts +0 -0
  155. /package/{src → dist}/conversion/upcastdispatcher.d.ts +0 -0
  156. /package/{src → dist}/conversion/upcasthelpers.d.ts +0 -0
  157. /package/{src → dist}/conversion/viewconsumable.d.ts +0 -0
  158. /package/{src → dist}/dataprocessor/basichtmlwriter.d.ts +0 -0
  159. /package/{src → dist}/dataprocessor/dataprocessor.d.ts +0 -0
  160. /package/{src → dist}/dataprocessor/htmldataprocessor.d.ts +0 -0
  161. /package/{src → dist}/dataprocessor/htmlwriter.d.ts +0 -0
  162. /package/{src → dist}/dataprocessor/xmldataprocessor.d.ts +0 -0
  163. /package/{src → dist}/dev-utils/model.d.ts +0 -0
  164. /package/{src → dist}/dev-utils/operationreplayer.d.ts +0 -0
  165. /package/{src → dist}/dev-utils/utils.d.ts +0 -0
  166. /package/{src → dist}/dev-utils/view.d.ts +0 -0
  167. /package/{src → dist}/legacyerrors.d.ts +0 -0
  168. /package/{src → dist}/model/batch.d.ts +0 -0
  169. /package/{src → dist}/model/differ.d.ts +0 -0
  170. /package/{src → dist}/model/document.d.ts +0 -0
  171. /package/{src → dist}/model/documentfragment.d.ts +0 -0
  172. /package/{src → dist}/model/documentselection.d.ts +0 -0
  173. /package/{src → dist}/model/element.d.ts +0 -0
  174. /package/{src → dist}/model/history.d.ts +0 -0
  175. /package/{src → dist}/model/item.d.ts +0 -0
  176. /package/{src → dist}/model/liveposition.d.ts +0 -0
  177. /package/{src → dist}/model/liverange.d.ts +0 -0
  178. /package/{src → dist}/model/markercollection.d.ts +0 -0
  179. /package/{src → dist}/model/node.d.ts +0 -0
  180. /package/{src → dist}/model/nodelist.d.ts +0 -0
  181. /package/{src → dist}/model/operation/attributeoperation.d.ts +0 -0
  182. /package/{src → dist}/model/operation/detachoperation.d.ts +0 -0
  183. /package/{src → dist}/model/operation/insertoperation.d.ts +0 -0
  184. /package/{src → dist}/model/operation/markeroperation.d.ts +0 -0
  185. /package/{src → dist}/model/operation/mergeoperation.d.ts +0 -0
  186. /package/{src → dist}/model/operation/moveoperation.d.ts +0 -0
  187. /package/{src → dist}/model/operation/nooperation.d.ts +0 -0
  188. /package/{src → dist}/model/operation/operation.d.ts +0 -0
  189. /package/{src → dist}/model/operation/operationfactory.d.ts +0 -0
  190. /package/{src → dist}/model/operation/renameoperation.d.ts +0 -0
  191. /package/{src → dist}/model/operation/rootattributeoperation.d.ts +0 -0
  192. /package/{src → dist}/model/operation/rootoperation.d.ts +0 -0
  193. /package/{src → dist}/model/operation/splitoperation.d.ts +0 -0
  194. /package/{src → dist}/model/operation/transform.d.ts +0 -0
  195. /package/{src → dist}/model/operation/utils.d.ts +0 -0
  196. /package/{src → dist}/model/position.d.ts +0 -0
  197. /package/{src → dist}/model/range.d.ts +0 -0
  198. /package/{src → dist}/model/rootelement.d.ts +0 -0
  199. /package/{src → dist}/model/schema.d.ts +0 -0
  200. /package/{src → dist}/model/text.d.ts +0 -0
  201. /package/{src → dist}/model/textproxy.d.ts +0 -0
  202. /package/{src → dist}/model/treewalker.d.ts +0 -0
  203. /package/{src → dist}/model/typecheckable.d.ts +0 -0
  204. /package/{src → dist}/model/utils/autoparagraphing.d.ts +0 -0
  205. /package/{src → dist}/model/utils/deletecontent.d.ts +0 -0
  206. /package/{src → dist}/model/utils/getselectedcontent.d.ts +0 -0
  207. /package/{src → dist}/model/utils/insertcontent.d.ts +0 -0
  208. /package/{src → dist}/model/utils/insertobject.d.ts +0 -0
  209. /package/{src → dist}/model/utils/modifyselection.d.ts +0 -0
  210. /package/{src → dist}/model/utils/selection-post-fixer.d.ts +0 -0
  211. /package/{src → dist}/model/writer.d.ts +0 -0
  212. /package/{src → dist}/view/attributeelement.d.ts +0 -0
  213. /package/{src → dist}/view/containerelement.d.ts +0 -0
  214. /package/{src → dist}/view/datatransfer.d.ts +0 -0
  215. /package/{src → dist}/view/document.d.ts +0 -0
  216. /package/{src → dist}/view/documentfragment.d.ts +0 -0
  217. /package/{src → dist}/view/documentselection.d.ts +0 -0
  218. /package/{src → dist}/view/domconverter.d.ts +0 -0
  219. /package/{src → dist}/view/editableelement.d.ts +0 -0
  220. /package/{src → dist}/view/elementdefinition.d.ts +0 -0
  221. /package/{src → dist}/view/emptyelement.d.ts +0 -0
  222. /package/{src → dist}/view/filler.d.ts +0 -0
  223. /package/{src → dist}/view/item.d.ts +0 -0
  224. /package/{src → dist}/view/node.d.ts +0 -0
  225. /package/{src → dist}/view/observer/arrowkeysobserver.d.ts +0 -0
  226. /package/{src → dist}/view/observer/bubblingemittermixin.d.ts +0 -0
  227. /package/{src → dist}/view/observer/bubblingeventinfo.d.ts +0 -0
  228. /package/{src → dist}/view/observer/clickobserver.d.ts +0 -0
  229. /package/{src → dist}/view/observer/compositionobserver.d.ts +0 -0
  230. /package/{src → dist}/view/observer/domeventdata.d.ts +0 -0
  231. /package/{src → dist}/view/observer/domeventobserver.d.ts +0 -0
  232. /package/{src → dist}/view/observer/fakeselectionobserver.d.ts +0 -0
  233. /package/{src → dist}/view/observer/focusobserver.d.ts +0 -0
  234. /package/{src → dist}/view/observer/inputobserver.d.ts +0 -0
  235. /package/{src → dist}/view/observer/keyobserver.d.ts +0 -0
  236. /package/{src → dist}/view/observer/mouseobserver.d.ts +0 -0
  237. /package/{src → dist}/view/observer/mutationobserver.d.ts +0 -0
  238. /package/{src → dist}/view/observer/observer.d.ts +0 -0
  239. /package/{src → dist}/view/observer/pointerobserver.d.ts +0 -0
  240. /package/{src → dist}/view/observer/selectionobserver.d.ts +0 -0
  241. /package/{src → dist}/view/observer/tabobserver.d.ts +0 -0
  242. /package/{src → dist}/view/observer/touchobserver.d.ts +0 -0
  243. /package/{src → dist}/view/placeholder.d.ts +0 -0
  244. /package/{src → dist}/view/position.d.ts +0 -0
  245. /package/{src → dist}/view/range.d.ts +0 -0
  246. /package/{src → dist}/view/rawelement.d.ts +0 -0
  247. /package/{src → dist}/view/renderer.d.ts +0 -0
  248. /package/{src → dist}/view/rooteditableelement.d.ts +0 -0
  249. /package/{src → dist}/view/selection.d.ts +0 -0
  250. /package/{src → dist}/view/stylesmap.d.ts +0 -0
  251. /package/{src → dist}/view/text.d.ts +0 -0
  252. /package/{src → dist}/view/textproxy.d.ts +0 -0
  253. /package/{src → dist}/view/tokenlist.d.ts +0 -0
  254. /package/{src → dist}/view/treewalker.d.ts +0 -0
  255. /package/{src → dist}/view/typecheckable.d.ts +0 -0
  256. /package/{src → dist}/view/uielement.d.ts +0 -0
  257. /package/{src → dist}/view/upcastwriter.d.ts +0 -0
  258. /package/{src → dist}/view/view.d.ts +0 -0
@@ -1,1589 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
- */
5
- /**
6
- * @module engine/view/downcastwriter
7
- */
8
- import { ViewPosition } from './position.js';
9
- import { ViewRange } from './range.js';
10
- import { ViewSelection } from './selection.js';
11
- import { ViewContainerElement } from './containerelement.js';
12
- import { ViewAttributeElement } from './attributeelement.js';
13
- import { ViewEmptyElement } from './emptyelement.js';
14
- import { ViewUIElement } from './uielement.js';
15
- import { ViewRawElement } from './rawelement.js';
16
- import { CKEditorError, isIterable } from '@ckeditor/ckeditor5-utils';
17
- import { ViewDocumentFragment } from './documentfragment.js';
18
- import { ViewText } from './text.js';
19
- import { ViewEditableElement } from './editableelement.js';
20
- import { isPlainObject } from 'es-toolkit/compat';
21
- /**
22
- * View downcast writer.
23
- *
24
- * It provides a set of methods used to manipulate view nodes.
25
- *
26
- * Do not create an instance of this writer manually. To modify a view structure, use
27
- * the {@link module:engine/view/view~EditingView#change `View#change()`} block.
28
- *
29
- * The `ViewDowncastWriter` is designed to work with semantic views which are the views that were/are being downcasted from the model.
30
- * To work with ordinary views (e.g. parsed from a pasted content) use the
31
- * {@link module:engine/view/upcastwriter~ViewUpcastWriter upcast writer}.
32
- *
33
- * Read more about changing the view in the {@glink framework/architecture/editing-engine#changing-the-view Changing the view}
34
- * section of the {@glink framework/architecture/editing-engine Editing engine architecture} guide.
35
- */
36
- export class ViewDowncastWriter {
37
- /**
38
- * The view document instance in which this writer operates.
39
- */
40
- document;
41
- /**
42
- * Holds references to the attribute groups that share the same {@link module:engine/view/attributeelement~ViewAttributeElement#id id}.
43
- * The keys are `id`s, the values are `Set`s holding {@link module:engine/view/attributeelement~ViewAttributeElement}s.
44
- */
45
- _cloneGroups = new Map();
46
- /**
47
- * The slot factory used by the `elementToStructure` downcast helper.
48
- */
49
- _slotFactory = null;
50
- /**
51
- * @param document The view document instance.
52
- */
53
- constructor(document) {
54
- this.document = document;
55
- }
56
- setSelection(...args) {
57
- this.document.selection._setTo(...args);
58
- }
59
- /**
60
- * Moves {@link module:engine/view/documentselection~ViewDocumentSelection#focus selection's focus} to the specified location.
61
- *
62
- * The location can be specified in the same form as
63
- * {@link module:engine/view/view~EditingView#createPositionAt view.createPositionAt()}
64
- * parameters.
65
- *
66
- * @param itemOrPosition
67
- * @param offset Offset or one of the flags. Used only when the first parameter is a {@link module:engine/view/item~ViewItem view item}.
68
- */
69
- setSelectionFocus(itemOrPosition, offset) {
70
- this.document.selection._setFocus(itemOrPosition, offset);
71
- }
72
- /**
73
- * Creates a new {@link module:engine/view/documentfragment~ViewDocumentFragment} instance.
74
- *
75
- * @param children A list of nodes to be inserted into the created document fragment.
76
- * @returns The created document fragment.
77
- */
78
- createDocumentFragment(children) {
79
- return new ViewDocumentFragment(this.document, children);
80
- }
81
- /**
82
- * Creates a new {@link module:engine/view/text~ViewText text node}.
83
- *
84
- * ```ts
85
- * writer.createText( 'foo' );
86
- * ```
87
- *
88
- * @param data The text's data.
89
- * @returns The created text node.
90
- */
91
- createText(data) {
92
- return new ViewText(this.document, data);
93
- }
94
- /**
95
- * Creates a new {@link module:engine/view/attributeelement~ViewAttributeElement}.
96
- *
97
- * ```ts
98
- * writer.createAttributeElement( 'strong' );
99
- * writer.createAttributeElement( 'a', { href: 'foo.bar' } );
100
- *
101
- * // Make `<a>` element contain other attributes element so the `<a>` element is not broken.
102
- * writer.createAttributeElement( 'a', { href: 'foo.bar' }, { priority: 5 } );
103
- *
104
- * // Set `id` of a marker element so it is not joined or merged with "normal" elements.
105
- * writer.createAttributeElement( 'span', { class: 'my-marker' }, { id: 'marker:my' } );
106
- * ```
107
- *
108
- * @param name Name of the element.
109
- * @param attributes Element's attributes.
110
- * @param options Element's options.
111
- * @param options.priority Element's {@link module:engine/view/attributeelement~ViewAttributeElement#priority priority}.
112
- * @param options.id Element's {@link module:engine/view/attributeelement~ViewAttributeElement#id id}.
113
- * @param options.renderUnsafeAttributes A list of attribute names that should be rendered in the editing
114
- * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
115
- * @returns Created element.
116
- */
117
- createAttributeElement(name, attributes, options = {}) {
118
- const attributeElement = new ViewAttributeElement(this.document, name, attributes);
119
- if (typeof options.priority === 'number') {
120
- attributeElement._priority = options.priority;
121
- }
122
- if (options.id) {
123
- attributeElement._id = options.id;
124
- }
125
- if (options.renderUnsafeAttributes) {
126
- attributeElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
127
- }
128
- return attributeElement;
129
- }
130
- createContainerElement(name, attributes, childrenOrOptions = {}, options = {}) {
131
- let children = undefined;
132
- if (isContainerOptions(childrenOrOptions)) {
133
- options = childrenOrOptions;
134
- }
135
- else {
136
- children = childrenOrOptions;
137
- }
138
- const containerElement = new ViewContainerElement(this.document, name, attributes, children);
139
- if (options.renderUnsafeAttributes) {
140
- containerElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
141
- }
142
- return containerElement;
143
- }
144
- /**
145
- * Creates a new {@link module:engine/view/editableelement~ViewEditableElement}.
146
- *
147
- * ```ts
148
- * writer.createEditableElement( 'div' );
149
- * writer.createEditableElement( 'div', { id: 'foo-1234' } );
150
- * ```
151
- *
152
- * Note: The editable element is to be used in the editing pipeline. Usually, together with
153
- * {@link module:widget/utils~toWidgetEditable `toWidgetEditable()`}.
154
- *
155
- * @param name Name of the element.
156
- * @param attributes Elements attributes.
157
- * @param options Element's options.
158
- * @param options.renderUnsafeAttributes A list of attribute names that should be rendered in the editing
159
- * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
160
- * @returns Created element.
161
- */
162
- createEditableElement(name, attributes, options = {}) {
163
- const editableElement = new ViewEditableElement(this.document, name, attributes);
164
- if (options.renderUnsafeAttributes) {
165
- editableElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
166
- }
167
- return editableElement;
168
- }
169
- /**
170
- * Creates a new {@link module:engine/view/emptyelement~ViewEmptyElement}.
171
- *
172
- * ```ts
173
- * writer.createEmptyElement( 'img' );
174
- * writer.createEmptyElement( 'img', { id: 'foo-1234' } );
175
- * ```
176
- *
177
- * @param name Name of the element.
178
- * @param attributes Elements attributes.
179
- * @param options Element's options.
180
- * @param options.renderUnsafeAttributes A list of attribute names that should be rendered in the editing
181
- * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
182
- * @returns Created element.
183
- */
184
- createEmptyElement(name, attributes, options = {}) {
185
- const emptyElement = new ViewEmptyElement(this.document, name, attributes);
186
- if (options.renderUnsafeAttributes) {
187
- emptyElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
188
- }
189
- return emptyElement;
190
- }
191
- /**
192
- * Creates a new {@link module:engine/view/uielement~ViewUIElement}.
193
- *
194
- * ```ts
195
- * writer.createUIElement( 'span' );
196
- * writer.createUIElement( 'span', { id: 'foo-1234' } );
197
- * ```
198
- *
199
- * A custom render function can be provided as the third parameter:
200
- *
201
- * ```ts
202
- * writer.createUIElement( 'span', null, function( domDocument ) {
203
- * const domElement = this.toDomElement( domDocument );
204
- * domElement.innerHTML = '<b>this is ui element</b>';
205
- *
206
- * return domElement;
207
- * } );
208
- * ```
209
- *
210
- * Unlike {@link #createRawElement raw elements}, UI elements are by no means editor content, for instance,
211
- * they are ignored by the editor selection system.
212
- *
213
- * You should not use UI elements as data containers. Check out {@link #createRawElement} instead.
214
- *
215
- * @param name The name of the element.
216
- * @param attributes Element attributes.
217
- * @param renderFunction A custom render function.
218
- * @returns The created element.
219
- */
220
- createUIElement(name, attributes, renderFunction) {
221
- const uiElement = new ViewUIElement(this.document, name, attributes);
222
- if (renderFunction) {
223
- uiElement.render = renderFunction;
224
- }
225
- return uiElement;
226
- }
227
- /**
228
- * Creates a new {@link module:engine/view/rawelement~ViewRawElement}.
229
- *
230
- * ```ts
231
- * writer.createRawElement( 'span', { id: 'foo-1234' }, function( domElement ) {
232
- * domElement.innerHTML = '<b>This is the raw content of the raw element.</b>';
233
- * } );
234
- * ```
235
- *
236
- * Raw elements work as data containers ("wrappers", "sandboxes") but their children are not managed or
237
- * even recognized by the editor. This encapsulation allows integrations to maintain custom DOM structures
238
- * in the editor content without, for instance, worrying about compatibility with other editor features.
239
- * Raw elements are a perfect tool for integration with external frameworks and data sources.
240
- *
241
- * Unlike {@link #createUIElement UI elements}, raw elements act like "real" editor content (similar to
242
- * {@link module:engine/view/containerelement~ViewContainerElement} or {@link module:engine/view/emptyelement~ViewEmptyElement}),
243
- * and they are considered by the editor selection.
244
- *
245
- * You should not use raw elements to render the UI in the editor content. Check out {@link #createUIElement `#createUIElement()`}
246
- * instead.
247
- *
248
- * @param name The name of the element.
249
- * @param attributes Element attributes.
250
- * @param renderFunction A custom render function.
251
- * @param options Element's options.
252
- * @param options.renderUnsafeAttributes A list of attribute names that should be rendered in the editing
253
- * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
254
- * @returns The created element.
255
- */
256
- createRawElement(name, attributes, renderFunction, options = {}) {
257
- const rawElement = new ViewRawElement(this.document, name, attributes);
258
- if (renderFunction) {
259
- rawElement.render = renderFunction;
260
- }
261
- if (options.renderUnsafeAttributes) {
262
- rawElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
263
- }
264
- return rawElement;
265
- }
266
- setAttribute(key, value, elementOrOverwrite, element) {
267
- if (element !== undefined) {
268
- element._setAttribute(key, value, elementOrOverwrite);
269
- }
270
- else {
271
- elementOrOverwrite._setAttribute(key, value);
272
- }
273
- }
274
- removeAttribute(key, elementOrTokens, element) {
275
- if (element !== undefined) {
276
- element._removeAttribute(key, elementOrTokens);
277
- }
278
- else {
279
- elementOrTokens._removeAttribute(key);
280
- }
281
- }
282
- /**
283
- * Adds specified class to the element.
284
- *
285
- * ```ts
286
- * writer.addClass( 'foo', linkElement );
287
- * writer.addClass( [ 'foo', 'bar' ], linkElement );
288
- * ```
289
- */
290
- addClass(className, element) {
291
- element._addClass(className);
292
- }
293
- /**
294
- * Removes specified class from the element.
295
- *
296
- * ```ts
297
- * writer.removeClass( 'foo', linkElement );
298
- * writer.removeClass( [ 'foo', 'bar' ], linkElement );
299
- * ```
300
- */
301
- removeClass(className, element) {
302
- element._removeClass(className);
303
- }
304
- setStyle(property, value, element) {
305
- if (isPlainObject(property) && element === undefined) {
306
- value._setStyle(property);
307
- }
308
- else {
309
- element._setStyle(property, value);
310
- }
311
- }
312
- /**
313
- * Removes specified style from the element.
314
- *
315
- * ```ts
316
- * writer.removeStyle( 'color', element ); // Removes 'color' style.
317
- * writer.removeStyle( [ 'color', 'border-top' ], element ); // Removes both 'color' and 'border-top' styles.
318
- * ```
319
- *
320
- * **Note**: This method can work with normalized style names if
321
- * {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
322
- * See {@link module:engine/view/stylesmap~StylesMap#remove `StylesMap#remove()`} for details.
323
- */
324
- removeStyle(property, element) {
325
- element._removeStyle(property);
326
- }
327
- /**
328
- * Sets a custom property on element. Unlike attributes, custom properties are not rendered to the DOM,
329
- * so they can be used to add special data to elements.
330
- */
331
- setCustomProperty(key, value, element) {
332
- element._setCustomProperty(key, value);
333
- }
334
- /**
335
- * Removes a custom property stored under the given key.
336
- *
337
- * @returns Returns true if property was removed.
338
- */
339
- removeCustomProperty(key, element) {
340
- return element._removeCustomProperty(key);
341
- }
342
- /**
343
- * Breaks attribute elements at the provided position or at the boundaries of a provided range. It breaks attribute elements
344
- * up to their first ancestor that is a container element.
345
- *
346
- * In following examples `<p>` is a container, `<b>` and `<u>` are attribute elements:
347
- *
348
- * ```html
349
- * <p>foo<b><u>bar{}</u></b></p> -> <p>foo<b><u>bar</u></b>[]</p>
350
- * <p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p>
351
- * <p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
352
- * <p><b>fo{o</b><u>ba}r</u></p> -> <p><b>fo</b><b>o</b><u>ba</u><u>r</u></b></p>
353
- * ```
354
- *
355
- * **Note:** {@link module:engine/view/documentfragment~ViewDocumentFragment DocumentFragment} is treated like a container.
356
- *
357
- * **Note:** The difference between {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes breakAttributes()} and
358
- * {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
359
- * {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements} that are ancestors of a given `position`,
360
- * up to the first encountered {@link module:engine/view/containerelement~ViewContainerElement container element}.
361
- * `breakContainer()` assumes that a given `position` is directly in the container element and breaks that container element.
362
- *
363
- * Throws the `view-writer-invalid-range-container` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
364
- * when the {@link module:engine/view/range~ViewRange#start start}
365
- * and {@link module:engine/view/range~ViewRange#end end} positions of a passed range are not placed inside same parent container.
366
- *
367
- * Throws the `view-writer-cannot-break-empty-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
368
- * when trying to break attributes inside an {@link module:engine/view/emptyelement~ViewEmptyElement ViewEmptyElement}.
369
- *
370
- * Throws the `view-writer-cannot-break-ui-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
371
- * when trying to break attributes inside a {@link module:engine/view/uielement~ViewUIElement UIElement}.
372
- *
373
- * @see module:engine/view/attributeelement~ViewAttributeElement
374
- * @see module:engine/view/containerelement~ViewContainerElement
375
- * @see module:engine/view/downcastwriter~ViewDowncastWriter#breakContainer
376
- * @param positionOrRange The position where to break attribute elements.
377
- * @returns The new position or range, after breaking the attribute elements.
378
- */
379
- breakAttributes(positionOrRange) {
380
- if (positionOrRange instanceof ViewPosition) {
381
- return this._breakAttributes(positionOrRange);
382
- }
383
- else {
384
- return this._breakAttributesRange(positionOrRange);
385
- }
386
- }
387
- /**
388
- * Breaks a {@link module:engine/view/containerelement~ViewContainerElement container view element} into two, at the given position.
389
- * The position has to be directly inside the container element and cannot be in the root. It does not break the conrainer view element
390
- * if the position is at the beginning or at the end of its parent element.
391
- *
392
- * ```html
393
- * <p>foo^bar</p> -> <p>foo</p><p>bar</p>
394
- * <div><p>foo</p>^<p>bar</p></div> -> <div><p>foo</p></div><div><p>bar</p></div>
395
- * <p>^foobar</p> -> ^<p>foobar</p>
396
- * <p>foobar^</p> -> <p>foobar</p>^
397
- * ```
398
- *
399
- * **Note:** The difference between {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes breakAttributes()} and
400
- * {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
401
- * {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements} that are ancestors of a given `position`,
402
- * up to the first encountered {@link module:engine/view/containerelement~ViewContainerElement container element}.
403
- * `breakContainer()` assumes that the given `position` is directly in the container element and breaks that container element.
404
- *
405
- * @see module:engine/view/attributeelement~ViewAttributeElement
406
- * @see module:engine/view/containerelement~ViewContainerElement
407
- * @see module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes
408
- * @param position The position where to break the element.
409
- * @returns The position between broken elements. If an element has not been broken,
410
- * the returned position is placed either before or after it.
411
- */
412
- breakContainer(position) {
413
- const element = position.parent;
414
- if (!(element.is('containerElement'))) {
415
- /**
416
- * Trying to break an element which is not a container element.
417
- *
418
- * @error view-writer-break-non-container-element
419
- */
420
- throw new CKEditorError('view-writer-break-non-container-element', this.document);
421
- }
422
- if (!element.parent) {
423
- /**
424
- * Trying to break root element.
425
- *
426
- * @error view-writer-break-root
427
- */
428
- throw new CKEditorError('view-writer-break-root', this.document);
429
- }
430
- if (position.isAtStart) {
431
- return ViewPosition._createBefore(element);
432
- }
433
- else if (!position.isAtEnd) {
434
- const newElement = element._clone(false);
435
- this.insert(ViewPosition._createAfter(element), newElement);
436
- const sourceRange = new ViewRange(position, ViewPosition._createAt(element, 'end'));
437
- const targetPosition = new ViewPosition(newElement, 0);
438
- this.move(sourceRange, targetPosition);
439
- }
440
- return ViewPosition._createAfter(element);
441
- }
442
- /**
443
- * Merges {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements}. It also merges text nodes if needed.
444
- * Only {@link module:engine/view/attributeelement~ViewAttributeElement#isSimilar similar} attribute elements can be merged.
445
- *
446
- * In following examples `<p>` is a container and `<b>` is an attribute element:
447
- *
448
- * ```html
449
- * <p>foo[]bar</p> -> <p>foo{}bar</p>
450
- * <p><b>foo</b>[]<b>bar</b></p> -> <p><b>foo{}bar</b></p>
451
- * <p><b foo="bar">a</b>[]<b foo="baz">b</b></p> -> <p><b foo="bar">a</b>[]<b foo="baz">b</b></p>
452
- * ```
453
- *
454
- * It will also take care about empty attributes when merging:
455
- *
456
- * ```html
457
- * <p><b>[]</b></p> -> <p>[]</p>
458
- * <p><b>foo</b><i>[]</i><b>bar</b></p> -> <p><b>foo{}bar</b></p>
459
- * ```
460
- *
461
- * **Note:** Difference between {@link module:engine/view/downcastwriter~ViewDowncastWriter#mergeAttributes mergeAttributes} and
462
- * {@link module:engine/view/downcastwriter~ViewDowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
463
- * {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements} or
464
- * {@link module:engine/view/text~ViewText text nodes} while `mergeContainer` merges two
465
- * {@link module:engine/view/containerelement~ViewContainerElement container elements}.
466
- *
467
- * @see module:engine/view/attributeelement~ViewAttributeElement
468
- * @see module:engine/view/containerelement~ViewContainerElement
469
- * @see module:engine/view/downcastwriter~ViewDowncastWriter#mergeContainers
470
- * @param position Merge position.
471
- * @returns Position after merge.
472
- */
473
- mergeAttributes(position) {
474
- const positionOffset = position.offset;
475
- const positionParent = position.parent;
476
- // When inside text node - nothing to merge.
477
- if (positionParent.is('$text')) {
478
- return position;
479
- }
480
- // When inside empty attribute - remove it.
481
- if (positionParent.is('attributeElement') && positionParent.childCount === 0) {
482
- const parent = positionParent.parent;
483
- const offset = positionParent.index;
484
- positionParent._remove();
485
- this._removeFromClonedElementsGroup(positionParent);
486
- return this.mergeAttributes(new ViewPosition(parent, offset));
487
- }
488
- const nodeBefore = positionParent.getChild(positionOffset - 1);
489
- const nodeAfter = positionParent.getChild(positionOffset);
490
- // Position should be placed between two nodes.
491
- if (!nodeBefore || !nodeAfter) {
492
- return position;
493
- }
494
- // When position is between two text nodes.
495
- if (nodeBefore.is('$text') && nodeAfter.is('$text')) {
496
- return mergeTextNodes(nodeBefore, nodeAfter);
497
- }
498
- // When position is between two same attribute elements.
499
- else if (nodeBefore.is('attributeElement') && nodeAfter.is('attributeElement') && nodeBefore.isSimilar(nodeAfter)) {
500
- // Move all children nodes from node placed after selection and remove that node.
501
- const count = nodeBefore.childCount;
502
- nodeBefore._appendChild(nodeAfter.getChildren());
503
- nodeAfter._remove();
504
- this._removeFromClonedElementsGroup(nodeAfter);
505
- // New position is located inside the first node, before new nodes.
506
- // Call this method recursively to merge again if needed.
507
- return this.mergeAttributes(new ViewPosition(nodeBefore, count));
508
- }
509
- return position;
510
- }
511
- /**
512
- * Merges two {@link module:engine/view/containerelement~ViewContainerElement container elements} that are
513
- * before and after given position. Precisely, the element after the position is removed and it's contents are
514
- * moved to element before the position.
515
- *
516
- * ```html
517
- * <p>foo</p>^<p>bar</p> -> <p>foo^bar</p>
518
- * <div>foo</div>^<p>bar</p> -> <div>foo^bar</div>
519
- * ```
520
- *
521
- * **Note:** Difference between {@link module:engine/view/downcastwriter~ViewDowncastWriter#mergeAttributes mergeAttributes} and
522
- * {@link module:engine/view/downcastwriter~ViewDowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
523
- * {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements} or
524
- * {@link module:engine/view/text~ViewText text nodes} while `mergeContainer` merges two
525
- * {@link module:engine/view/containerelement~ViewContainerElement container elements}.
526
- *
527
- * @see module:engine/view/attributeelement~ViewAttributeElement
528
- * @see module:engine/view/containerelement~ViewContainerElement
529
- * @see module:engine/view/downcastwriter~ViewDowncastWriter#mergeAttributes
530
- * @param position Merge position.
531
- * @returns Position after merge.
532
- */
533
- mergeContainers(position) {
534
- const prev = position.nodeBefore;
535
- const next = position.nodeAfter;
536
- if (!prev || !next || !prev.is('containerElement') || !next.is('containerElement')) {
537
- /**
538
- * Element before and after given position cannot be merged.
539
- *
540
- * @error view-writer-merge-containers-invalid-position
541
- */
542
- throw new CKEditorError('view-writer-merge-containers-invalid-position', this.document);
543
- }
544
- const lastChild = prev.getChild(prev.childCount - 1);
545
- const newPosition = lastChild instanceof ViewText ?
546
- ViewPosition._createAt(lastChild, 'end') :
547
- ViewPosition._createAt(prev, 'end');
548
- this.move(ViewRange._createIn(next), ViewPosition._createAt(prev, 'end'));
549
- this.remove(ViewRange._createOn(next));
550
- return newPosition;
551
- }
552
- /**
553
- * Inserts a node or nodes at specified position. Takes care about breaking attributes before insertion
554
- * and merging them afterwards.
555
- *
556
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
557
- * contains instances that are not {@link module:engine/view/text~ViewText Texts},
558
- * {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElements},
559
- * {@link module:engine/view/containerelement~ViewContainerElement ViewContainerElements},
560
- * {@link module:engine/view/emptyelement~ViewEmptyElement ViewEmptyElements},
561
- * {@link module:engine/view/rawelement~ViewRawElement RawElements} or
562
- * {@link module:engine/view/uielement~ViewUIElement UIElements}.
563
- *
564
- * @param position Insertion position.
565
- * @param nodes Node or nodes to insert.
566
- * @returns Range around inserted nodes.
567
- */
568
- insert(position, nodes) {
569
- nodes = isIterable(nodes) ? [...nodes] : [nodes];
570
- // Check if nodes to insert are instances of ViewAttributeElements, ViewContainerElements, ViewEmptyElements, UIElements or Text.
571
- validateNodesToInsert(nodes, this.document);
572
- // Group nodes in batches of nodes that require or do not require breaking an ViewAttributeElements.
573
- const nodeGroups = nodes.reduce((groups, node) => {
574
- const lastGroup = groups[groups.length - 1];
575
- // Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
576
- // can't have an attribute in model and won't get wrapped with an ViewAttributeElement while down-casted.
577
- const breakAttributes = !node.is('uiElement');
578
- if (!lastGroup || lastGroup.breakAttributes != breakAttributes) {
579
- groups.push({
580
- breakAttributes,
581
- nodes: [node]
582
- });
583
- }
584
- else {
585
- lastGroup.nodes.push(node);
586
- }
587
- return groups;
588
- }, []);
589
- // Insert nodes in batches.
590
- let start = null;
591
- let end = position;
592
- for (const { nodes, breakAttributes } of nodeGroups) {
593
- const range = this._insertNodes(end, nodes, breakAttributes);
594
- if (!start) {
595
- start = range.start;
596
- }
597
- end = range.end;
598
- }
599
- // When no nodes were inserted - return collapsed range.
600
- if (!start) {
601
- return new ViewRange(position);
602
- }
603
- return new ViewRange(start, end);
604
- }
605
- /**
606
- * Removes provided range from the container.
607
- *
608
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
609
- * {@link module:engine/view/range~ViewRange#start start} and {@link module:engine/view/range~ViewRange#end end}
610
- * positions are not placed inside same parent container.
611
- *
612
- * @param rangeOrItem Range to remove from container
613
- * or an {@link module:engine/view/item~ViewItem item} to remove. If range is provided, after removing, it will be updated
614
- * to a collapsed range showing the new position.
615
- * @returns Document fragment containing removed nodes.
616
- */
617
- remove(rangeOrItem) {
618
- const range = rangeOrItem instanceof ViewRange ? rangeOrItem : ViewRange._createOn(rangeOrItem);
619
- validateRangeContainer(range, this.document);
620
- // If range is collapsed - nothing to remove.
621
- if (range.isCollapsed) {
622
- return new ViewDocumentFragment(this.document);
623
- }
624
- // Break attributes at range start and end.
625
- const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
626
- const parentContainer = breakStart.parent;
627
- const count = breakEnd.offset - breakStart.offset;
628
- // Remove nodes in range.
629
- const removed = parentContainer._removeChildren(breakStart.offset, count);
630
- for (const node of removed) {
631
- this._removeFromClonedElementsGroup(node);
632
- }
633
- // Merge after removing.
634
- const mergePosition = this.mergeAttributes(breakStart);
635
- range.start = mergePosition;
636
- range.end = mergePosition.clone();
637
- // Return removed nodes.
638
- return new ViewDocumentFragment(this.document, removed);
639
- }
640
- /**
641
- * Removes matching elements from given range.
642
- *
643
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
644
- * {@link module:engine/view/range~ViewRange#start start} and {@link module:engine/view/range~ViewRange#end end}
645
- * positions are not placed inside same parent container.
646
- *
647
- * @param range Range to clear.
648
- * @param element Element to remove.
649
- */
650
- clear(range, element) {
651
- validateRangeContainer(range, this.document);
652
- // Create walker on given range.
653
- // We walk backward because when we remove element during walk it modifies range end position.
654
- const walker = range.getWalker({
655
- direction: 'backward',
656
- ignoreElementEnd: true
657
- });
658
- // Let's walk.
659
- for (const current of walker) {
660
- const item = current.item;
661
- let rangeToRemove;
662
- // When current item matches to the given element.
663
- if (item.is('element') && element.isSimilar(item)) {
664
- // Create range on this element.
665
- rangeToRemove = ViewRange._createOn(item);
666
- // When range starts inside Text or TextProxy element.
667
- }
668
- else if (!current.nextPosition.isAfter(range.start) && item.is('$textProxy')) {
669
- // We need to check if parent of this text matches to given element.
670
- const parentElement = item.getAncestors().find(ancestor => {
671
- return ancestor.is('element') && element.isSimilar(ancestor);
672
- });
673
- // If it is then create range inside this element.
674
- if (parentElement) {
675
- rangeToRemove = ViewRange._createIn(parentElement);
676
- }
677
- }
678
- // If we have found element to remove.
679
- if (rangeToRemove) {
680
- // We need to check if element range stick out of the given range and truncate if it is.
681
- if (rangeToRemove.end.isAfter(range.end)) {
682
- rangeToRemove.end = range.end;
683
- }
684
- if (rangeToRemove.start.isBefore(range.start)) {
685
- rangeToRemove.start = range.start;
686
- }
687
- // At the end we remove range with found element.
688
- this.remove(rangeToRemove);
689
- }
690
- }
691
- }
692
- /**
693
- * Moves nodes from provided range to target position.
694
- *
695
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
696
- * {@link module:engine/view/range~ViewRange#start start} and {@link module:engine/view/range~ViewRange#end end}
697
- * positions are not placed inside same parent container.
698
- *
699
- * @param sourceRange Range containing nodes to move.
700
- * @param targetPosition Position to insert.
701
- * @returns Range in target container. Inserted nodes are placed between
702
- * {@link module:engine/view/range~ViewRange#start start} and {@link module:engine/view/range~ViewRange#end end} positions.
703
- */
704
- move(sourceRange, targetPosition) {
705
- let nodes;
706
- if (targetPosition.isAfter(sourceRange.end)) {
707
- targetPosition = this._breakAttributes(targetPosition, true);
708
- const parent = targetPosition.parent;
709
- const countBefore = parent.childCount;
710
- sourceRange = this._breakAttributesRange(sourceRange, true);
711
- nodes = this.remove(sourceRange);
712
- targetPosition.offset += (parent.childCount - countBefore);
713
- }
714
- else {
715
- nodes = this.remove(sourceRange);
716
- }
717
- return this.insert(targetPosition, nodes);
718
- }
719
- /**
720
- * Wraps elements within range with provided {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement}.
721
- * If a collapsed range is provided, it will be wrapped only if it is equal to view selection.
722
- *
723
- * If a collapsed range was passed and is same as selection, the selection
724
- * will be moved to the inside of the wrapped attribute element.
725
- *
726
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
727
- * when {@link module:engine/view/range~ViewRange#start}
728
- * and {@link module:engine/view/range~ViewRange#end} positions are not placed inside same parent container.
729
- *
730
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
731
- * an instance of {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement}.
732
- *
733
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
734
- * is collapsed and different than view selection.
735
- *
736
- * @param range Range to wrap.
737
- * @param attribute Attribute element to use as wrapper.
738
- * @returns range Range after wrapping, spanning over wrapping attribute element.
739
- */
740
- wrap(range, attribute) {
741
- if (!(attribute instanceof ViewAttributeElement)) {
742
- throw new CKEditorError('view-writer-wrap-invalid-attribute', this.document);
743
- }
744
- validateRangeContainer(range, this.document);
745
- if (!range.isCollapsed) {
746
- // Non-collapsed range. Wrap it with the attribute element.
747
- return this._wrapRange(range, attribute);
748
- }
749
- else {
750
- // Collapsed range. Wrap position.
751
- let position = range.start;
752
- if (position.parent.is('element') && !_hasNonUiChildren(position.parent)) {
753
- position = position.getLastMatchingPosition(value => value.item.is('uiElement'));
754
- }
755
- position = this._wrapPosition(position, attribute);
756
- const viewSelection = this.document.selection;
757
- // If wrapping position is equal to view selection, move view selection inside wrapping attribute element.
758
- if (viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual(range.start)) {
759
- this.setSelection(position);
760
- }
761
- return new ViewRange(position);
762
- }
763
- }
764
- /**
765
- * Unwraps nodes within provided range from attribute element.
766
- *
767
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
768
- * {@link module:engine/view/range~ViewRange#start start} and {@link module:engine/view/range~ViewRange#end end}
769
- * positions are not placed inside same parent container.
770
- */
771
- unwrap(range, attribute) {
772
- if (!(attribute instanceof ViewAttributeElement)) {
773
- /**
774
- * The `attribute` passed to {@link module:engine/view/downcastwriter~ViewDowncastWriter#unwrap `ViewDowncastWriter#unwrap()`}
775
- * must be an instance of {@link module:engine/view/attributeelement~ViewAttributeElement `AttributeElement`}.
776
- *
777
- * @error view-writer-unwrap-invalid-attribute
778
- */
779
- throw new CKEditorError('view-writer-unwrap-invalid-attribute', this.document);
780
- }
781
- validateRangeContainer(range, this.document);
782
- // If range is collapsed - nothing to unwrap.
783
- if (range.isCollapsed) {
784
- return range;
785
- }
786
- // Break attributes at range start and end.
787
- const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
788
- const parentContainer = breakStart.parent;
789
- // Unwrap children located between break points.
790
- const newRange = this._unwrapChildren(parentContainer, breakStart.offset, breakEnd.offset, attribute);
791
- // Merge attributes at the both ends and return a new range.
792
- const start = this.mergeAttributes(newRange.start);
793
- // If start position was merged - move end position back.
794
- if (!start.isEqual(newRange.start)) {
795
- newRange.end.offset--;
796
- }
797
- const end = this.mergeAttributes(newRange.end);
798
- return new ViewRange(start, end);
799
- }
800
- /**
801
- * Renames element by creating a copy of renamed element but with changed name and then moving contents of the
802
- * old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~ViewPosition positions}
803
- * which has renamed element as {@link module:engine/view/position~ViewPosition#parent a parent}.
804
- *
805
- * New element has to be created because `Element#tagName` property in DOM is readonly.
806
- *
807
- * Since this function creates a new element and removes the given one, the new element is returned to keep reference.
808
- *
809
- * @param newName New name for element.
810
- * @param viewElement Element to be renamed.
811
- * @returns Element created due to rename.
812
- */
813
- rename(newName, viewElement) {
814
- const newElement = new ViewContainerElement(this.document, newName, viewElement.getAttributes());
815
- this.insert(ViewPosition._createAfter(viewElement), newElement);
816
- this.move(ViewRange._createIn(viewElement), ViewPosition._createAt(newElement, 0));
817
- this.remove(ViewRange._createOn(viewElement));
818
- return newElement;
819
- }
820
- /**
821
- * Cleans up memory by removing obsolete cloned elements group from the writer.
822
- *
823
- * Should be used whenever all {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements}
824
- * with the same {@link module:engine/view/attributeelement~ViewAttributeElement#id id} are going to be removed from the view and
825
- * the group will no longer be needed.
826
- *
827
- * Cloned elements group are not removed automatically in case if the group is still needed after all its elements
828
- * were removed from the view.
829
- *
830
- * Keep in mind that group names are equal to the `id` property of the attribute element.
831
- *
832
- * @param groupName Name of the group to clear.
833
- */
834
- clearClonedElementsGroup(groupName) {
835
- this._cloneGroups.delete(groupName);
836
- }
837
- /**
838
- * Creates position at the given location. The location can be specified as:
839
- *
840
- * * a {@link module:engine/view/position~ViewPosition position},
841
- * * parent element and offset (offset defaults to `0`),
842
- * * parent element and `'end'` (sets position at the end of that element),
843
- * * {@link module:engine/view/item~ViewItem view item} and `'before'` or `'after'` (sets position before or after given view item).
844
- *
845
- * This method is a shortcut to other constructors such as:
846
- *
847
- * * {@link #createPositionBefore},
848
- * * {@link #createPositionAfter},
849
- *
850
- * @param offset Offset or one of the flags. Used only when the first parameter is a {@link module:engine/view/item~ViewItem view item}.
851
- */
852
- createPositionAt(itemOrPosition, offset) {
853
- return ViewPosition._createAt(itemOrPosition, offset);
854
- }
855
- /**
856
- * Creates a new position after given view item.
857
- *
858
- * @param item View item after which the position should be located.
859
- */
860
- createPositionAfter(item) {
861
- return ViewPosition._createAfter(item);
862
- }
863
- /**
864
- * Creates a new position before given view item.
865
- *
866
- * @param item View item before which the position should be located.
867
- */
868
- createPositionBefore(item) {
869
- return ViewPosition._createBefore(item);
870
- }
871
- /**
872
- * Creates a range spanning from `start` position to `end` position.
873
- *
874
- * **Note:** This factory method creates its own {@link module:engine/view/position~ViewPosition} instances basing on passed values.
875
- *
876
- * @param start Start position.
877
- * @param end End position. If not set, range will be collapsed at `start` position.
878
- */
879
- createRange(start, end) {
880
- return new ViewRange(start, end);
881
- }
882
- /**
883
- * Creates a range that starts before given {@link module:engine/view/item~ViewItem view item} and ends after it.
884
- */
885
- createRangeOn(item) {
886
- return ViewRange._createOn(item);
887
- }
888
- /**
889
- * Creates a range inside an {@link module:engine/view/element~ViewElement element} which starts before the first child of
890
- * that element and ends after the last child of that element.
891
- *
892
- * @param element Element which is a parent for the range.
893
- */
894
- createRangeIn(element) {
895
- return ViewRange._createIn(element);
896
- }
897
- createSelection(...args) {
898
- return new ViewSelection(...args);
899
- }
900
- /**
901
- * Creates placeholders for child elements of the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
902
- * `elementToStructure()`} conversion helper.
903
- *
904
- * ```ts
905
- * const viewSlot = conversionApi.writer.createSlot();
906
- * const viewPosition = conversionApi.writer.createPositionAt( viewElement, 0 );
907
- *
908
- * conversionApi.writer.insert( viewPosition, viewSlot );
909
- * ```
910
- *
911
- * It could be filtered down to a specific subset of children (only `<foo>` model elements in this case):
912
- *
913
- * ```ts
914
- * const viewSlot = conversionApi.writer.createSlot( node => node.is( 'element', 'foo' ) );
915
- * const viewPosition = conversionApi.writer.createPositionAt( viewElement, 0 );
916
- *
917
- * conversionApi.writer.insert( viewPosition, viewSlot );
918
- * ```
919
- *
920
- * While providing a filtered slot, make sure to provide slots for all child nodes. A single node cannot be downcasted into
921
- * multiple slots.
922
- *
923
- * **Note**: You should not change the order of nodes. View elements should be in the same order as model nodes.
924
- *
925
- * @param modeOrFilter The filter for child nodes.
926
- * @returns The slot element to be placed in to the view structure while processing
927
- * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure `elementToStructure()`}.
928
- */
929
- createSlot(modeOrFilter = 'children') {
930
- if (!this._slotFactory) {
931
- /**
932
- * The `createSlot()` method is only allowed inside the `elementToStructure` downcast helper callback.
933
- *
934
- * @error view-writer-invalid-create-slot-context
935
- */
936
- throw new CKEditorError('view-writer-invalid-create-slot-context', this.document);
937
- }
938
- return this._slotFactory(this, modeOrFilter);
939
- }
940
- /**
941
- * Registers a slot factory.
942
- *
943
- * @internal
944
- * @param slotFactory The slot factory.
945
- */
946
- _registerSlotFactory(slotFactory) {
947
- this._slotFactory = slotFactory;
948
- }
949
- /**
950
- * Clears the registered slot factory.
951
- *
952
- * @internal
953
- */
954
- _clearSlotFactory() {
955
- this._slotFactory = null;
956
- }
957
- /**
958
- * Inserts a node or nodes at the specified position. Takes care of breaking attributes before insertion
959
- * and merging them afterwards if requested by the breakAttributes param.
960
- *
961
- * @param position Insertion position.
962
- * @param nodes Node or nodes to insert.
963
- * @param breakAttributes Whether attributes should be broken.
964
- * @returns Range around inserted nodes.
965
- */
966
- _insertNodes(position, nodes, breakAttributes) {
967
- let parentElement;
968
- // Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
969
- // can't have an attribute in model and won't get wrapped with an ViewAttributeElement while down-casted.
970
- if (breakAttributes) {
971
- parentElement = getParentContainer(position);
972
- }
973
- else {
974
- parentElement = position.parent.is('$text') ? position.parent.parent : position.parent;
975
- }
976
- if (!parentElement) {
977
- /**
978
- * Position's parent container cannot be found.
979
- *
980
- * @error view-writer-invalid-position-container
981
- */
982
- throw new CKEditorError('view-writer-invalid-position-container', this.document);
983
- }
984
- let insertionPosition;
985
- if (breakAttributes) {
986
- insertionPosition = this._breakAttributes(position, true);
987
- }
988
- else {
989
- insertionPosition = position.parent.is('$text') ? breakTextNode(position) : position;
990
- }
991
- const length = parentElement._insertChild(insertionPosition.offset, nodes);
992
- for (const node of nodes) {
993
- this._addToClonedElementsGroup(node);
994
- }
995
- const endPosition = insertionPosition.getShiftedBy(length);
996
- const start = this.mergeAttributes(insertionPosition);
997
- // If start position was merged - move end position.
998
- if (!start.isEqual(insertionPosition)) {
999
- endPosition.offset--;
1000
- }
1001
- const end = this.mergeAttributes(endPosition);
1002
- return new ViewRange(start, end);
1003
- }
1004
- /**
1005
- * Wraps children with provided `wrapElement`. Only children contained in `parent` element between
1006
- * `startOffset` and `endOffset` will be wrapped.
1007
- */
1008
- _wrapChildren(parent, startOffset, endOffset, wrapElement) {
1009
- let i = startOffset;
1010
- const wrapPositions = [];
1011
- while (i < endOffset) {
1012
- const child = parent.getChild(i);
1013
- const isText = child.is('$text');
1014
- const isAttribute = child.is('attributeElement');
1015
- //
1016
- // (In all examples, assume that `wrapElement` is `<span class="foo">` element.)
1017
- //
1018
- // Check if `wrapElement` can be joined with the wrapped element. One of requirements is having same name.
1019
- // If possible, join elements.
1020
- //
1021
- // <p><span class="bar">abc</span></p> --> <p><span class="foo bar">abc</span></p>
1022
- //
1023
- if (isAttribute && child._canMergeAttributesFrom(wrapElement)) {
1024
- child._mergeAttributesFrom(wrapElement);
1025
- wrapPositions.push(new ViewPosition(parent, i));
1026
- }
1027
- //
1028
- // Wrap the child if it is not an attribute element or if it is an attribute element that should be inside
1029
- // `wrapElement` (due to priority).
1030
- //
1031
- // <p>abc</p> --> <p><span class="foo">abc</span></p>
1032
- // <p><strong>abc</strong></p> --> <p><span class="foo"><strong>abc</strong></span></p>
1033
- else if (isText || !isAttribute || shouldABeOutsideB(wrapElement, child)) {
1034
- // Clone attribute.
1035
- const newAttribute = wrapElement._clone();
1036
- // Wrap current node with new attribute.
1037
- child._remove();
1038
- newAttribute._appendChild(child);
1039
- parent._insertChild(i, newAttribute);
1040
- this._addToClonedElementsGroup(newAttribute);
1041
- wrapPositions.push(new ViewPosition(parent, i));
1042
- }
1043
- //
1044
- // If other nested attribute is found and it wasn't wrapped (see above), continue wrapping inside it.
1045
- //
1046
- // <p><a href="foo.html">abc</a></p> --> <p><a href="foo.html"><span class="foo">abc</span></a></p>
1047
- //
1048
- else /* if ( isAttribute ) */ {
1049
- this._wrapChildren(child, 0, child.childCount, wrapElement);
1050
- }
1051
- i++;
1052
- }
1053
- // Merge at each wrap.
1054
- let offsetChange = 0;
1055
- for (const position of wrapPositions) {
1056
- position.offset -= offsetChange;
1057
- // Do not merge with elements outside selected children.
1058
- if (position.offset == startOffset) {
1059
- continue;
1060
- }
1061
- const newPosition = this.mergeAttributes(position);
1062
- // If nodes were merged - other merge offsets will change.
1063
- if (!newPosition.isEqual(position)) {
1064
- offsetChange++;
1065
- endOffset--;
1066
- }
1067
- }
1068
- return ViewRange._createFromParentsAndOffsets(parent, startOffset, parent, endOffset);
1069
- }
1070
- /**
1071
- * Unwraps children from provided `unwrapElement`. Only children contained in `parent` element between
1072
- * `startOffset` and `endOffset` will be unwrapped.
1073
- */
1074
- _unwrapChildren(parent, startOffset, endOffset, unwrapElement) {
1075
- let i = startOffset;
1076
- const unwrapPositions = [];
1077
- // Iterate over each element between provided offsets inside parent.
1078
- // We don't use tree walker or range iterator because we will be removing and merging potentially multiple nodes,
1079
- // so it could get messy. It is safer to it manually in this case.
1080
- while (i < endOffset) {
1081
- const child = parent.getChild(i);
1082
- // Skip all text nodes. There should be no container element's here either.
1083
- if (!child.is('attributeElement')) {
1084
- i++;
1085
- continue;
1086
- }
1087
- //
1088
- // (In all examples, assume that `unwrapElement` is `<span class="foo">` element.)
1089
- //
1090
- // If the child is similar to the given attribute element, unwrap it - it will be completely removed.
1091
- //
1092
- // <p><span class="foo">abc</span>xyz</p> --> <p>abcxyz</p>
1093
- //
1094
- if (child.isSimilar(unwrapElement)) {
1095
- const unwrapped = child.getChildren();
1096
- const count = child.childCount;
1097
- // Replace wrapper element with its children
1098
- child._remove();
1099
- parent._insertChild(i, unwrapped);
1100
- this._removeFromClonedElementsGroup(child);
1101
- // Save start and end position of moved items.
1102
- unwrapPositions.push(new ViewPosition(parent, i), new ViewPosition(parent, i + count));
1103
- // Skip elements that were unwrapped. Assuming there won't be another element to unwrap in child elements.
1104
- i += count;
1105
- endOffset += count - 1;
1106
- continue;
1107
- }
1108
- //
1109
- // If the child is not similar but is an attribute element, try partial unwrapping - remove the same attributes/styles/classes.
1110
- // Partial unwrapping will happen only if the elements have the same name.
1111
- //
1112
- // <p><span class="foo bar">abc</span>xyz</p> --> <p><span class="bar">abc</span>xyz</p>
1113
- // <p><i class="foo">abc</i>xyz</p> --> <p><i class="foo">abc</i>xyz</p>
1114
- //
1115
- if (child._canSubtractAttributesOf(unwrapElement)) {
1116
- child._subtractAttributesOf(unwrapElement);
1117
- unwrapPositions.push(new ViewPosition(parent, i), new ViewPosition(parent, i + 1));
1118
- i++;
1119
- continue;
1120
- }
1121
- //
1122
- // If other nested attribute is found, look through it's children for elements to unwrap.
1123
- //
1124
- // <p><i><span class="foo">abc</span></i><p> --> <p><i>abc</i><p>
1125
- //
1126
- this._unwrapChildren(child, 0, child.childCount, unwrapElement);
1127
- i++;
1128
- }
1129
- // Merge at each unwrap.
1130
- let offsetChange = 0;
1131
- for (const position of unwrapPositions) {
1132
- position.offset -= offsetChange;
1133
- // Do not merge with elements outside selected children.
1134
- if (position.offset == startOffset || position.offset == endOffset) {
1135
- continue;
1136
- }
1137
- const newPosition = this.mergeAttributes(position);
1138
- // If nodes were merged - other merge offsets will change.
1139
- if (!newPosition.isEqual(position)) {
1140
- offsetChange++;
1141
- endOffset--;
1142
- }
1143
- }
1144
- return ViewRange._createFromParentsAndOffsets(parent, startOffset, parent, endOffset);
1145
- }
1146
- /**
1147
- * Helper function for `view.writer.wrap`. Wraps range with provided attribute element.
1148
- * This method will also merge newly added attribute element with its siblings whenever possible.
1149
- *
1150
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
1151
- * an instance of {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement}.
1152
- *
1153
- * @returns New range after wrapping, spanning over wrapping attribute element.
1154
- */
1155
- _wrapRange(range, attribute) {
1156
- // Break attributes at range start and end.
1157
- const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
1158
- const parentContainer = breakStart.parent;
1159
- // Wrap all children with attribute.
1160
- const newRange = this._wrapChildren(parentContainer, breakStart.offset, breakEnd.offset, attribute);
1161
- // Merge attributes at the both ends and return a new range.
1162
- const start = this.mergeAttributes(newRange.start);
1163
- // If start position was merged - move end position back.
1164
- if (!start.isEqual(newRange.start)) {
1165
- newRange.end.offset--;
1166
- }
1167
- const end = this.mergeAttributes(newRange.end);
1168
- return new ViewRange(start, end);
1169
- }
1170
- /**
1171
- * Helper function for {@link #wrap}. Wraps position with provided attribute element.
1172
- * This method will also merge newly added attribute element with its siblings whenever possible.
1173
- *
1174
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
1175
- * an instance of {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement}.
1176
- *
1177
- * @returns New position after wrapping.
1178
- */
1179
- _wrapPosition(position, attribute) {
1180
- // Return same position when trying to wrap with attribute similar to position parent.
1181
- if (attribute.isSimilar(position.parent)) {
1182
- return movePositionToTextNode(position.clone());
1183
- }
1184
- // When position is inside text node - break it and place new position between two text nodes.
1185
- if (position.parent.is('$text')) {
1186
- position = breakTextNode(position);
1187
- }
1188
- // Create fake element that will represent position, and will not be merged with other attributes.
1189
- const fakeElement = this.createAttributeElement('_wrapPosition-fake-element');
1190
- fakeElement._priority = Number.POSITIVE_INFINITY;
1191
- fakeElement.isSimilar = () => false;
1192
- // Insert fake element in position location.
1193
- position.parent._insertChild(position.offset, fakeElement);
1194
- // Range around inserted fake attribute element.
1195
- const wrapRange = new ViewRange(position, position.getShiftedBy(1));
1196
- // Wrap fake element with attribute (it will also merge if possible).
1197
- this.wrap(wrapRange, attribute);
1198
- // Remove fake element and place new position there.
1199
- const newPosition = new ViewPosition(fakeElement.parent, fakeElement.index);
1200
- fakeElement._remove();
1201
- // If position is placed between text nodes - merge them and return position inside.
1202
- const nodeBefore = newPosition.nodeBefore;
1203
- const nodeAfter = newPosition.nodeAfter;
1204
- if (nodeBefore && nodeBefore.is('view:$text') && nodeAfter && nodeAfter.is('view:$text')) {
1205
- return mergeTextNodes(nodeBefore, nodeAfter);
1206
- }
1207
- // If position is next to text node - move position inside.
1208
- return movePositionToTextNode(newPosition);
1209
- }
1210
- /**
1211
- * Helper function used by other `ViewDowncastWriter` methods. Breaks attribute elements at the boundaries of given range.
1212
- *
1213
- * @param range Range which `start` and `end` positions will be used to break attributes.
1214
- * @param forceSplitText If set to `true`, will break text nodes even if they are directly in container element.
1215
- * This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
1216
- * @returns New range with located at break positions.
1217
- */
1218
- _breakAttributesRange(range, forceSplitText = false) {
1219
- const rangeStart = range.start;
1220
- const rangeEnd = range.end;
1221
- validateRangeContainer(range, this.document);
1222
- // Break at the collapsed position. Return new collapsed range.
1223
- if (range.isCollapsed) {
1224
- const position = this._breakAttributes(range.start, forceSplitText);
1225
- return new ViewRange(position, position);
1226
- }
1227
- const breakEnd = this._breakAttributes(rangeEnd, forceSplitText);
1228
- const count = breakEnd.parent.childCount;
1229
- const breakStart = this._breakAttributes(rangeStart, forceSplitText);
1230
- // Calculate new break end offset.
1231
- breakEnd.offset += breakEnd.parent.childCount - count;
1232
- return new ViewRange(breakStart, breakEnd);
1233
- }
1234
- /**
1235
- * Helper function used by other `ViewDowncastWriter` methods. Breaks attribute elements at given position.
1236
- *
1237
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
1238
- * is placed inside {@link module:engine/view/emptyelement~ViewEmptyElement ViewEmptyElement}.
1239
- *
1240
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
1241
- * is placed inside {@link module:engine/view/uielement~ViewUIElement UIElement}.
1242
- *
1243
- * @param position Position where to break attributes.
1244
- * @param forceSplitText If set to `true`, will break text nodes even if they are directly in container element.
1245
- * This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
1246
- * @returns New position after breaking the attributes.
1247
- */
1248
- _breakAttributes(position, forceSplitText = false) {
1249
- const positionOffset = position.offset;
1250
- const positionParent = position.parent;
1251
- // If position is placed inside ViewEmptyElement - throw an exception as we cannot break inside.
1252
- if (position.parent.is('emptyElement')) {
1253
- /**
1254
- * Cannot break an `EmptyElement` instance.
1255
- *
1256
- * This error is thrown if
1257
- * {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes `ViewDowncastWriter#breakAttributes()`}
1258
- * was executed in an incorrect position.
1259
- *
1260
- * @error view-writer-cannot-break-empty-element
1261
- */
1262
- throw new CKEditorError('view-writer-cannot-break-empty-element', this.document);
1263
- }
1264
- // If position is placed inside UIElement - throw an exception as we cannot break inside.
1265
- if (position.parent.is('uiElement')) {
1266
- /**
1267
- * Cannot break a `UIElement` instance.
1268
- *
1269
- * This error is thrown if
1270
- * {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes `ViewDowncastWriter#breakAttributes()`}
1271
- * was executed in an incorrect position.
1272
- *
1273
- * @error view-writer-cannot-break-ui-element
1274
- */
1275
- throw new CKEditorError('view-writer-cannot-break-ui-element', this.document);
1276
- }
1277
- // If position is placed inside RawElement - throw an exception as we cannot break inside.
1278
- if (position.parent.is('rawElement')) {
1279
- /**
1280
- * Cannot break a `RawElement` instance.
1281
- *
1282
- * This error is thrown if
1283
- * {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes `ViewDowncastWriter#breakAttributes()`}
1284
- * was executed in an incorrect position.
1285
- *
1286
- * @error view-writer-cannot-break-raw-element
1287
- */
1288
- throw new CKEditorError('view-writer-cannot-break-raw-element', this.document);
1289
- }
1290
- // There are no attributes to break and text nodes breaking is not forced.
1291
- if (!forceSplitText && positionParent.is('$text') && isContainerOrFragment(positionParent.parent)) {
1292
- return position.clone();
1293
- }
1294
- // Position's parent is container, so no attributes to break.
1295
- if (isContainerOrFragment(positionParent)) {
1296
- return position.clone();
1297
- }
1298
- // Break text and start again in new position.
1299
- if (positionParent.is('$text')) {
1300
- return this._breakAttributes(breakTextNode(position), forceSplitText);
1301
- }
1302
- const length = positionParent.childCount;
1303
- // <p>foo<b><u>bar{}</u></b></p>
1304
- // <p>foo<b><u>bar</u>[]</b></p>
1305
- // <p>foo<b><u>bar</u></b>[]</p>
1306
- if (positionOffset == length) {
1307
- const newPosition = new ViewPosition(positionParent.parent, positionParent.index + 1);
1308
- return this._breakAttributes(newPosition, forceSplitText);
1309
- }
1310
- else {
1311
- // <p>foo<b><u>{}bar</u></b></p>
1312
- // <p>foo<b>[]<u>bar</u></b></p>
1313
- // <p>foo{}<b><u>bar</u></b></p>
1314
- if (positionOffset === 0) {
1315
- const newPosition = new ViewPosition(positionParent.parent, positionParent.index);
1316
- return this._breakAttributes(newPosition, forceSplitText);
1317
- }
1318
- // <p>foo<b><u>b{}ar</u></b></p>
1319
- // <p>foo<b><u>b[]ar</u></b></p>
1320
- // <p>foo<b><u>b</u>[]<u>ar</u></b></p>
1321
- // <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
1322
- else {
1323
- const offsetAfter = positionParent.index + 1;
1324
- // Break element.
1325
- const clonedNode = positionParent._clone();
1326
- // Insert cloned node to position's parent node.
1327
- positionParent.parent._insertChild(offsetAfter, clonedNode);
1328
- this._addToClonedElementsGroup(clonedNode);
1329
- // Get nodes to move.
1330
- const count = positionParent.childCount - positionOffset;
1331
- const nodesToMove = positionParent._removeChildren(positionOffset, count);
1332
- // Move nodes to cloned node.
1333
- clonedNode._appendChild(nodesToMove);
1334
- // Create new position to work on.
1335
- const newPosition = new ViewPosition(positionParent.parent, offsetAfter);
1336
- return this._breakAttributes(newPosition, forceSplitText);
1337
- }
1338
- }
1339
- }
1340
- /**
1341
- * Stores the information that an {@link module:engine/view/attributeelement~ViewAttributeElement attribute element} was
1342
- * added to the tree. Saves the reference to the group in the given element and updates the group, so other elements
1343
- * from the group now keep a reference to the given attribute element.
1344
- *
1345
- * The clones group can be obtained using {@link module:engine/view/attributeelement~ViewAttributeElement#getElementsWithSameId}.
1346
- *
1347
- * Does nothing if added element has no {@link module:engine/view/attributeelement~ViewAttributeElement#id id}.
1348
- *
1349
- * @param element Attribute element to save.
1350
- */
1351
- _addToClonedElementsGroup(element) {
1352
- // Add only if the element is in document tree.
1353
- if (!element.root.is('rootElement')) {
1354
- return;
1355
- }
1356
- // Traverse the element's children recursively to find other attribute elements that also might got inserted.
1357
- // The loop is at the beginning so we can make fast returns later in the code.
1358
- if (element.is('element')) {
1359
- for (const child of element.getChildren()) {
1360
- this._addToClonedElementsGroup(child);
1361
- }
1362
- }
1363
- const id = element.id;
1364
- if (!id) {
1365
- return;
1366
- }
1367
- let group = this._cloneGroups.get(id);
1368
- if (!group) {
1369
- group = new Set();
1370
- this._cloneGroups.set(id, group);
1371
- }
1372
- group.add(element);
1373
- element._clonesGroup = group;
1374
- }
1375
- /**
1376
- * Removes all the information about the given {@link module:engine/view/attributeelement~ViewAttributeElement attribute element}
1377
- * from its clones group.
1378
- *
1379
- * Keep in mind, that the element will still keep a reference to the group (but the group will not keep a reference to it).
1380
- * This allows to reference the whole group even if the element was already removed from the tree.
1381
- *
1382
- * Does nothing if the element has no {@link module:engine/view/attributeelement~ViewAttributeElement#id id}.
1383
- *
1384
- * @param element Attribute element to remove.
1385
- */
1386
- _removeFromClonedElementsGroup(element) {
1387
- // Traverse the element's children recursively to find other attribute elements that also got removed.
1388
- // The loop is at the beginning so we can make fast returns later in the code.
1389
- if (element.is('element')) {
1390
- for (const child of element.getChildren()) {
1391
- this._removeFromClonedElementsGroup(child);
1392
- }
1393
- }
1394
- const id = element.id;
1395
- if (!id) {
1396
- return;
1397
- }
1398
- const group = this._cloneGroups.get(id);
1399
- if (!group) {
1400
- return;
1401
- }
1402
- group.delete(element);
1403
- // Not removing group from element on purpose!
1404
- // If other parts of code have reference to this element, they will be able to get references to other elements from the group.
1405
- }
1406
- }
1407
- // Helper function for `view.writer.wrap`. Checks if given element has any children that are not ui elements.
1408
- function _hasNonUiChildren(parent) {
1409
- return Array.from(parent.getChildren()).some(child => !child.is('uiElement'));
1410
- }
1411
- /**
1412
- * The `attribute` passed to {@link module:engine/view/downcastwriter~ViewDowncastWriter#wrap `ViewDowncastWriter#wrap()`}
1413
- * must be an instance of {@link module:engine/view/attributeelement~ViewAttributeElement `AttributeElement`}.
1414
- *
1415
- * @error view-writer-wrap-invalid-attribute
1416
- */
1417
- /**
1418
- * Returns first parent container of specified {@link module:engine/view/position~ViewPosition Position}.
1419
- * Position's parent node is checked as first, then next parents are checked.
1420
- * Note that {@link module:engine/view/documentfragment~ViewDocumentFragment DocumentFragment} is treated like a container.
1421
- *
1422
- * @param position Position used as a start point to locate parent container.
1423
- * @returns Parent container element or `undefined` if container is not found.
1424
- */
1425
- function getParentContainer(position) {
1426
- let parent = position.parent;
1427
- while (!isContainerOrFragment(parent)) {
1428
- if (!parent) {
1429
- return undefined;
1430
- }
1431
- parent = parent.parent;
1432
- }
1433
- return parent;
1434
- }
1435
- /**
1436
- * Checks if first {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement} provided to the function
1437
- * can be wrapped outside second element. It is done by comparing elements'
1438
- * {@link module:engine/view/attributeelement~ViewAttributeElement#priority priorities}, if both have same priority
1439
- * {@link module:engine/view/element~ViewElement#getIdentity identities} are compared.
1440
- */
1441
- function shouldABeOutsideB(a, b) {
1442
- if (a.priority < b.priority) {
1443
- return true;
1444
- }
1445
- else if (a.priority > b.priority) {
1446
- return false;
1447
- }
1448
- // When priorities are equal and names are different - use identities.
1449
- return a.getIdentity() < b.getIdentity();
1450
- }
1451
- /**
1452
- * Returns new position that is moved to near text node. Returns same position if there is no text node before of after
1453
- * specified position.
1454
- *
1455
- * ```html
1456
- * <p>foo[]</p> -> <p>foo{}</p>
1457
- * <p>[]foo</p> -> <p>{}foo</p>
1458
- * ```
1459
- *
1460
- * @returns Position located inside text node or same position if there is no text nodes
1461
- * before or after position location.
1462
- */
1463
- function movePositionToTextNode(position) {
1464
- const nodeBefore = position.nodeBefore;
1465
- if (nodeBefore && nodeBefore.is('$text')) {
1466
- return new ViewPosition(nodeBefore, nodeBefore.data.length);
1467
- }
1468
- const nodeAfter = position.nodeAfter;
1469
- if (nodeAfter && nodeAfter.is('$text')) {
1470
- return new ViewPosition(nodeAfter, 0);
1471
- }
1472
- return position;
1473
- }
1474
- /**
1475
- * Breaks text node into two text nodes when possible.
1476
- *
1477
- * ```html
1478
- * <p>foo{}bar</p> -> <p>foo[]bar</p>
1479
- * <p>{}foobar</p> -> <p>[]foobar</p>
1480
- * <p>foobar{}</p> -> <p>foobar[]</p>
1481
- * ```
1482
- *
1483
- * @param position Position that need to be placed inside text node.
1484
- * @returns New position after breaking text node.
1485
- */
1486
- function breakTextNode(position) {
1487
- if (position.offset == position.parent.data.length) {
1488
- return new ViewPosition(position.parent.parent, position.parent.index + 1);
1489
- }
1490
- if (position.offset === 0) {
1491
- return new ViewPosition(position.parent.parent, position.parent.index);
1492
- }
1493
- // Get part of the text that need to be moved.
1494
- const textToMove = position.parent.data.slice(position.offset);
1495
- // Leave rest of the text in position's parent.
1496
- position.parent._data = position.parent.data.slice(0, position.offset);
1497
- // Insert new text node after position's parent text node.
1498
- position.parent.parent._insertChild(position.parent.index + 1, new ViewText(position.root.document, textToMove));
1499
- // Return new position between two newly created text nodes.
1500
- return new ViewPosition(position.parent.parent, position.parent.index + 1);
1501
- }
1502
- /**
1503
- * Merges two text nodes into first node. Removes second node and returns merge position.
1504
- *
1505
- * @param t1 First text node to merge. Data from second text node will be moved at the end of this text node.
1506
- * @param t2 Second text node to merge. This node will be removed after merging.
1507
- * @returns Position after merging text nodes.
1508
- */
1509
- function mergeTextNodes(t1, t2) {
1510
- // Merge text data into first text node and remove second one.
1511
- const nodeBeforeLength = t1.data.length;
1512
- t1._data += t2.data;
1513
- t2._remove();
1514
- return new ViewPosition(t1, nodeBeforeLength);
1515
- }
1516
- const validNodesToInsert = [ViewText, ViewAttributeElement, ViewContainerElement, ViewEmptyElement, ViewRawElement, ViewUIElement];
1517
- /**
1518
- * Checks if provided nodes are valid to insert.
1519
- *
1520
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
1521
- * contains instances that are not supported ones (see error description for valid ones.
1522
- */
1523
- function validateNodesToInsert(nodes, errorContext) {
1524
- for (const node of nodes) {
1525
- if (!validNodesToInsert.some(validNode => node instanceof validNode)) {
1526
- /**
1527
- * One of the nodes to be inserted is of an invalid type.
1528
- *
1529
- * Nodes to be inserted with {@link module:engine/view/downcastwriter~ViewDowncastWriter#insert `ViewDowncastWriter#insert()`}
1530
- * should be of the following types:
1531
- *
1532
- * * {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement},
1533
- * * {@link module:engine/view/containerelement~ViewContainerElement ViewContainerElement},
1534
- * * {@link module:engine/view/emptyelement~ViewEmptyElement ViewEmptyElement},
1535
- * * {@link module:engine/view/uielement~ViewUIElement UIElement},
1536
- * * {@link module:engine/view/rawelement~ViewRawElement RawElement},
1537
- * * {@link module:engine/view/text~ViewText Text}.
1538
- *
1539
- * @error view-writer-insert-invalid-node-type
1540
- */
1541
- throw new CKEditorError('view-writer-insert-invalid-node-type', errorContext);
1542
- }
1543
- if (!node.is('$text')) {
1544
- validateNodesToInsert(node.getChildren(), errorContext);
1545
- }
1546
- }
1547
- }
1548
- /**
1549
- * Checks if node is ViewContainerElement or DocumentFragment, because in most cases they should be treated the same way.
1550
- *
1551
- * @returns Returns `true` if node is instance of ViewContainerElement or DocumentFragment.
1552
- */
1553
- function isContainerOrFragment(node) {
1554
- return node && (node.is('containerElement') || node.is('documentFragment'));
1555
- }
1556
- /**
1557
- * Checks if {@link module:engine/view/range~ViewRange#start range start} and {@link module:engine/view/range~ViewRange#end range end}
1558
- * are placed inside same {@link module:engine/view/containerelement~ViewContainerElement container element}.
1559
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
1560
- */
1561
- function validateRangeContainer(range, errorContext) {
1562
- const startContainer = getParentContainer(range.start);
1563
- const endContainer = getParentContainer(range.end);
1564
- if (!startContainer || !endContainer || startContainer !== endContainer) {
1565
- /**
1566
- * The container of the given range is invalid.
1567
- *
1568
- * This may happen if {@link module:engine/view/range~ViewRange#start range start} and
1569
- * {@link module:engine/view/range~ViewRange#end range end} positions are not placed inside the same container element or
1570
- * a parent container for these positions cannot be found.
1571
- *
1572
- * Methods like {@link module:engine/view/downcastwriter~ViewDowncastWriter#wrap `ViewDowncastWriter#remove()`},
1573
- * {@link module:engine/view/downcastwriter~ViewDowncastWriter#wrap `ViewDowncastWriter#clean()`},
1574
- * {@link module:engine/view/downcastwriter~ViewDowncastWriter#wrap `ViewDowncastWriter#wrap()`},
1575
- * {@link module:engine/view/downcastwriter~ViewDowncastWriter#wrap `ViewDowncastWriter#unwrap()`} need to be called
1576
- * on a range that has its start and end positions located in the same container element. Both positions can be
1577
- * nested within other elements (e.g. an attribute element) but the closest container ancestor must be the same.
1578
- *
1579
- * @error view-writer-invalid-range-container
1580
- */
1581
- throw new CKEditorError('view-writer-invalid-range-container', errorContext);
1582
- }
1583
- }
1584
- /**
1585
- * Checks if the provided argument is a plain object that can be used as options for container element.
1586
- */
1587
- function isContainerOptions(childrenOrOptions) {
1588
- return isPlainObject(childrenOrOptions);
1589
- }