@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,2160 +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
- * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
7
- *
8
- * @module engine/conversion/downcasthelpers
9
- */
10
- import { ModelRange } from '../model/range.js';
11
- import { ModelSelection } from '../model/selection.js';
12
- import { ModelDocumentSelection } from '../model/documentselection.js';
13
- import { ModelElement } from '../model/element.js';
14
- import { ModelPosition } from '../model/position.js';
15
- import { ViewAttributeElement } from '../view/attributeelement.js';
16
- import { ConversionHelpers } from './conversionhelpers.js';
17
- import { StylesMap } from '../view/stylesmap.js';
18
- import { CKEditorError, toArray } from '@ckeditor/ckeditor5-utils';
19
- import { cloneDeep } from 'es-toolkit/compat';
20
- /**
21
- * Downcast conversion helper functions.
22
- *
23
- * Learn more about {@glink framework/deep-dive/conversion/downcast downcast helpers}.
24
- *
25
- * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
26
- */
27
- export class DowncastHelpers extends ConversionHelpers {
28
- /**
29
- * Model element to view element conversion helper.
30
- *
31
- * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
32
- *
33
- * ```ts
34
- * editor.conversion.for( 'downcast' ).elementToElement( {
35
- * model: 'paragraph',
36
- * view: 'p'
37
- * } );
38
- *
39
- * editor.conversion.for( 'downcast' ).elementToElement( {
40
- * model: 'paragraph',
41
- * view: 'div',
42
- * converterPriority: 'high'
43
- * } );
44
- *
45
- * editor.conversion.for( 'downcast' ).elementToElement( {
46
- * model: 'fancyParagraph',
47
- * view: {
48
- * name: 'p',
49
- * classes: 'fancy'
50
- * }
51
- * } );
52
- *
53
- * editor.conversion.for( 'downcast' ).elementToElement( {
54
- * model: 'heading',
55
- * view: ( modelElement, conversionApi ) => {
56
- * const { writer } = conversionApi;
57
- *
58
- * return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
59
- * }
60
- * } );
61
- * ```
62
- *
63
- * The element-to-element conversion supports the reconversion mechanism. It can be enabled by using either the `attributes` or
64
- * the `children` props on a model description. You will find a couple examples below.
65
- *
66
- * In order to reconvert an element if any of its direct children have been added or removed, use the `children` property on a `model`
67
- * description. For example, this model:
68
- *
69
- * ```xml
70
- * <box>
71
- * <paragraph>Some text.</paragraph>
72
- * </box>
73
- * ```
74
- *
75
- * will be converted into this structure in the view:
76
- *
77
- * ```html
78
- * <div class="box" data-type="single">
79
- * <p>Some text.</p>
80
- * </div>
81
- * ```
82
- *
83
- * But if more items were inserted in the model:
84
- *
85
- * ```xml
86
- * <box>
87
- * <paragraph>Some text.</paragraph>
88
- * <paragraph>Other item.</paragraph>
89
- * </box>
90
- * ```
91
- *
92
- * it will be converted into this structure in the view (note the element `data-type` change):
93
- *
94
- * ```html
95
- * <div class="box" data-type="multiple">
96
- * <p>Some text.</p>
97
- * <p>Other item.</p>
98
- * </div>
99
- * ```
100
- *
101
- * Such a converter would look like this (note that the `paragraph` elements are converted separately):
102
- *
103
- * ```ts
104
- * editor.conversion.for( 'downcast' ).elementToElement( {
105
- * model: {
106
- * name: 'box',
107
- * children: true
108
- * },
109
- * view: ( modelElement, conversionApi ) => {
110
- * const { writer } = conversionApi;
111
- *
112
- * return writer.createContainerElement( 'div', {
113
- * class: 'box',
114
- * 'data-type': modelElement.childCount == 1 ? 'single' : 'multiple'
115
- * } );
116
- * }
117
- * } );
118
- * ```
119
- *
120
- * In order to reconvert element if any of its attributes have been updated, use the `attributes` property on a `model`
121
- * description. For example, this model:
122
- *
123
- * ```xml
124
- * <heading level="2">Some text.</heading>
125
- * ```
126
- *
127
- * will be converted into this structure in the view:
128
- *
129
- * ```html
130
- * <h2>Some text.</h2>
131
- * ```
132
- *
133
- * But if the `heading` element's `level` attribute has been updated to `3` for example, then
134
- * it will be converted into this structure in the view:
135
- *
136
- * ```html
137
- * <h3>Some text.</h3>
138
- * ```
139
- *
140
- * Such a converter would look as follows:
141
- *
142
- * ```ts
143
- * editor.conversion.for( 'downcast' ).elementToElement( {
144
- * model: {
145
- * name: 'heading',
146
- * attributes: 'level'
147
- * },
148
- * view: ( modelElement, conversionApi ) => {
149
- * const { writer } = conversionApi;
150
- *
151
- * return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
152
- * }
153
- * } );
154
- * ```
155
- *
156
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
157
- * to the conversion process.
158
- *
159
- * You can read more about the element-to-element conversion in the
160
- * {@glink framework/deep-dive/conversion/downcast downcast conversion} guide.
161
- *
162
- * @param config Conversion configuration.
163
- * @param config.model The description or a name of the model element to convert.
164
- * @param config.view A view element definition or a function that takes the model element and
165
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
166
- * as parameters and returns a view container element.
167
- * @param config.converterPriority Converter priority.
168
- */
169
- elementToElement(config) {
170
- return this.add(downcastElementToElement(config));
171
- }
172
- /**
173
- * The model element to view structure (several elements) conversion helper.
174
- *
175
- * This conversion results in creating a view structure with one or more slots defined for the child nodes.
176
- * For example, a model `<table>` may become this structure in the view:
177
- *
178
- * ```html
179
- * <figure class="table">
180
- * <table>
181
- * <tbody>${ slot for table rows }</tbody>
182
- * </table>
183
- * </figure>
184
- * ```
185
- *
186
- * The children of the model's `<table>` element will be inserted into the `<tbody>` element.
187
- * If the `elementToElement()` helper was used, the children would be inserted into the `<figure>`.
188
- *
189
- * Imagine a table feature where for this model structure:
190
- *
191
- * ```xml
192
- * <table headingRows="1">
193
- * <tableRow> ... table cells 1 ... </tableRow>
194
- * <tableRow> ... table cells 2 ... </tableRow>
195
- * <tableRow> ... table cells 3 ... </tableRow>
196
- * <caption>Caption text</caption>
197
- * </table>
198
- * ```
199
- *
200
- * we want to generate this view structure:
201
- *
202
- * ```html
203
- * <figure class="table">
204
- * <table>
205
- * <thead>
206
- * <tr> ... table cells 1 ... </tr>
207
- * </thead>
208
- * <tbody>
209
- * <tr> ... table cells 2 ... </tr>
210
- * <tr> ... table cells 3 ... </tr>
211
- * </tbody>
212
- * </table>
213
- * <figcaption>Caption text</figcaption>
214
- * </figure>
215
- * ```
216
- *
217
- * The converter has to take the `headingRows` attribute into consideration when allocating the `<tableRow>` elements
218
- * into the `<tbody>` and `<thead>` elements. Hence, we need two slots and need to define proper filter callbacks for them.
219
- *
220
- * Additionally, all elements other than `<tableRow>` should be placed outside the `<table>` tag.
221
- * In the example above, this will handle the table caption.
222
- *
223
- * Such a converter would look like this:
224
- *
225
- * ```ts
226
- * editor.conversion.for( 'downcast' ).elementToStructure( {
227
- * model: {
228
- * name: 'table',
229
- * attributes: [ 'headingRows' ]
230
- * },
231
- * view: ( modelElement, conversionApi ) => {
232
- * const { writer } = conversionApi;
233
- *
234
- * const figureElement = writer.createContainerElement( 'figure', { class: 'table' } );
235
- * const tableElement = writer.createContainerElement( 'table' );
236
- *
237
- * writer.insert( writer.createPositionAt( figureElement, 0 ), tableElement );
238
- *
239
- * const headingRows = modelElement.getAttribute( 'headingRows' ) || 0;
240
- *
241
- * if ( headingRows > 0 ) {
242
- * const tableHead = writer.createContainerElement( 'thead' );
243
- *
244
- * const headSlot = writer.createSlot( node => node.is( 'element', 'tableRow' ) && node.index < headingRows );
245
- *
246
- * writer.insert( writer.createPositionAt( tableElement, 'end' ), tableHead );
247
- * writer.insert( writer.createPositionAt( tableHead, 0 ), headSlot );
248
- * }
249
- *
250
- * if ( headingRows < tableUtils.getRows( table ) ) {
251
- * const tableBody = writer.createContainerElement( 'tbody' );
252
- *
253
- * const bodySlot = writer.createSlot( node => node.is( 'element', 'tableRow' ) && node.index >= headingRows );
254
- *
255
- * writer.insert( writer.createPositionAt( tableElement, 'end' ), tableBody );
256
- * writer.insert( writer.createPositionAt( tableBody, 0 ), bodySlot );
257
- * }
258
- *
259
- * const restSlot = writer.createSlot( node => !node.is( 'element', 'tableRow' ) );
260
- *
261
- * writer.insert( writer.createPositionAt( figureElement, 'end' ), restSlot );
262
- *
263
- * return figureElement;
264
- * }
265
- * } );
266
- * ```
267
- *
268
- * Note: The children of a model element that's being converted must be allocated in the same order in the view
269
- * in which they are placed in the model.
270
- *
271
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
272
- * to the conversion process.
273
- *
274
- * @param config Conversion configuration.
275
- * @param config.model The description or a name of the model element to convert.
276
- * @param config.view A function that takes the model element and
277
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as parameters
278
- * and returns a view container element with slots for model child nodes to be converted into.
279
- * @param config.converterPriority Converter priority.
280
- */
281
- elementToStructure(config) {
282
- return this.add(downcastElementToStructure(config));
283
- }
284
- /**
285
- * Model attribute to view element conversion helper.
286
- *
287
- * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with
288
- * `"Foo"` as data and the `bold` attribute becomes `<strong>Foo</strong>` in the view.
289
- *
290
- * ```ts
291
- * editor.conversion.for( 'downcast' ).attributeToElement( {
292
- * model: 'bold',
293
- * view: 'strong'
294
- * } );
295
- *
296
- * editor.conversion.for( 'downcast' ).attributeToElement( {
297
- * model: 'bold',
298
- * view: 'b',
299
- * converterPriority: 'high'
300
- * } );
301
- *
302
- * editor.conversion.for( 'downcast' ).attributeToElement( {
303
- * model: 'invert',
304
- * view: {
305
- * name: 'span',
306
- * classes: [ 'font-light', 'bg-dark' ]
307
- * }
308
- * } );
309
- *
310
- * editor.conversion.for( 'downcast' ).attributeToElement( {
311
- * model: {
312
- * key: 'fontSize',
313
- * values: [ 'big', 'small' ]
314
- * },
315
- * view: {
316
- * big: {
317
- * name: 'span',
318
- * styles: {
319
- * 'font-size': '1.2em'
320
- * }
321
- * },
322
- * small: {
323
- * name: 'span',
324
- * styles: {
325
- * 'font-size': '0.8em'
326
- * }
327
- * }
328
- * }
329
- * } );
330
- *
331
- * editor.conversion.for( 'downcast' ).attributeToElement( {
332
- * model: 'bold',
333
- * view: ( modelAttributeValue, conversionApi ) => {
334
- * const { writer } = conversionApi;
335
- *
336
- * return writer.createAttributeElement( 'span', {
337
- * style: 'font-weight:' + modelAttributeValue
338
- * } );
339
- * }
340
- * } );
341
- *
342
- * editor.conversion.for( 'downcast' ).attributeToElement( {
343
- * model: {
344
- * key: 'color',
345
- * name: '$text'
346
- * },
347
- * view: ( modelAttributeValue, conversionApi ) => {
348
- * const { writer } = conversionApi;
349
- *
350
- * return writer.createAttributeElement( 'span', {
351
- * style: 'color:' + modelAttributeValue
352
- * } );
353
- * }
354
- * } );
355
- * ```
356
- *
357
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
358
- * to the conversion process.
359
- *
360
- * @param config Conversion configuration.
361
- * @param config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
362
- * of `String`s with possible values if the model attribute is an enumerable.
363
- * @param config.view A view element definition or a function
364
- * that takes the model attribute value and
365
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as parameters and returns a view
366
- * attribute element. If `config.model.values` is given, `config.view` should be an object assigning values from `config.model.values`
367
- * to view element definitions or functions.
368
- * @param config.converterPriority Converter priority.
369
- */
370
- attributeToElement(config) {
371
- return this.add(downcastAttributeToElement(config));
372
- }
373
- /**
374
- * Model attribute to view attribute conversion helper.
375
- *
376
- * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example,
377
- * `<imageInline src='foo.jpg'></imageInline>` is converted to `<img src='foo.jpg'></img>`.
378
- *
379
- * ```ts
380
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
381
- * model: 'source',
382
- * view: 'src'
383
- * } );
384
- *
385
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
386
- * model: 'source',
387
- * view: 'href',
388
- * converterPriority: 'high'
389
- * } );
390
- *
391
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
392
- * model: {
393
- * name: 'imageInline',
394
- * key: 'source'
395
- * },
396
- * view: 'src'
397
- * } );
398
- *
399
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
400
- * model: {
401
- * name: 'styled',
402
- * values: [ 'dark', 'light' ]
403
- * },
404
- * view: {
405
- * dark: {
406
- * key: 'class',
407
- * value: [ 'styled', 'styled-dark' ]
408
- * },
409
- * light: {
410
- * key: 'class',
411
- * value: [ 'styled', 'styled-light' ]
412
- * }
413
- * }
414
- * } );
415
- *
416
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
417
- * model: 'styled',
418
- * view: modelAttributeValue => ( {
419
- * key: 'class',
420
- * value: 'styled-' + modelAttributeValue
421
- * } )
422
- * } );
423
- * ```
424
- *
425
- * **Note**: Downcasting to a style property requires providing `value` as an object:
426
- *
427
- * ```ts
428
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
429
- * model: 'lineHeight',
430
- * view: modelAttributeValue => ( {
431
- * key: 'style',
432
- * value: {
433
- * 'line-height': modelAttributeValue,
434
- * 'border-bottom': '1px dotted #ba2'
435
- * }
436
- * } )
437
- * } );
438
- * ```
439
- *
440
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
441
- * to the conversion process.
442
- *
443
- * @param config Conversion configuration.
444
- * @param config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
445
- * the attribute key, possible values and, optionally, an element name to convert from.
446
- * @param config.view A view attribute key, or a `{ key, value }` object or a function that takes the model attribute value and
447
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
448
- * as parameters and returns a `{ key, value }` object. If the `key` is `'class'`, the `value` can be a `String` or an
449
- * array of `String`s. If the `key` is `'style'`, the `value` is an object with key-value pairs. In other cases, `value` is a `String`.
450
- * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
451
- * `{ key, value }` objects or a functions.
452
- * @param config.converterPriority Converter priority.
453
- */
454
- attributeToAttribute(config) {
455
- return this.add(downcastAttributeToAttribute(config));
456
- }
457
- /**
458
- * Model marker to view element conversion helper.
459
- *
460
- * **Note**: This method should be used mainly for editing the downcast and it is recommended
461
- * to use the {@link #markerToData `#markerToData()`} helper instead.
462
- *
463
- * This helper may produce invalid HTML code (e.g. a span between table cells).
464
- * It should only be used when you are sure that the produced HTML will be semantically correct.
465
- *
466
- * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker
467
- * is collapsed, only one element is created. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>`
468
- * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
469
- *
470
- * ```ts
471
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
472
- * model: 'search',
473
- * view: 'marker-search'
474
- * } );
475
- *
476
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
477
- * model: 'search',
478
- * view: 'search-result',
479
- * converterPriority: 'high'
480
- * } );
481
- *
482
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
483
- * model: 'search',
484
- * view: {
485
- * name: 'span',
486
- * attributes: {
487
- * 'data-marker': 'search'
488
- * }
489
- * }
490
- * } );
491
- *
492
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
493
- * model: 'search',
494
- * view: ( markerData, conversionApi ) => {
495
- * const { writer } = conversionApi;
496
- *
497
- * return writer.createUIElement( 'span', {
498
- * 'data-marker': 'search',
499
- * 'data-start': markerData.isOpening
500
- * } );
501
- * }
502
- * } );
503
- * ```
504
- *
505
- * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function
506
- * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
507
- * as a parameters and should return an instance of the
508
- * {@link module:engine/view/uielement~ViewUIElement view UI element}. The `data` object and
509
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi `conversionApi`} are passed from
510
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
511
- * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` for
512
- * the marker end boundary element.
513
- *
514
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
515
- * to the conversion process.
516
- *
517
- * @param config Conversion configuration.
518
- * @param config.model The name of the model marker (or model marker group) to convert.
519
- * @param config.view A view element definition or a function that takes the model marker data and
520
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
521
- * and returns a view UI element.
522
- * @param config.converterPriority Converter priority.
523
- */
524
- markerToElement(config) {
525
- return this.add(downcastMarkerToElement(config));
526
- }
527
- /**
528
- * Model marker to highlight conversion helper.
529
- *
530
- * This conversion results in creating a highlight on view nodes. For this kind of conversion,
531
- * the {@link module:engine/conversion/downcasthelpers~DowncastHighlightDescriptor} should be provided.
532
- *
533
- * For text nodes, a `<span>` {@link module:engine/view/attributeelement~ViewAttributeElement} is created and it wraps all text nodes
534
- * in the converted marker range. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>` becomes
535
- * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
536
- *
537
- * {@link module:engine/view/containerelement~ViewContainerElement} may provide a custom way of handling highlight. Most often,
538
- * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `<span>`).
539
- * For example, a model marker set like this:
540
- * `[<imageInline src="foo.jpg"></imageInline>]` becomes `<img src="foo.jpg" class="comment"></img>` in the view.
541
- *
542
- * For container elements, the conversion is two-step. While the converter processes the highlight descriptor and passes it
543
- * to a container element, it is the container element instance itself that applies values from the highlight descriptor.
544
- * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that.
545
- *
546
- * ```ts
547
- * editor.conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
548
- *
549
- * editor.conversion.for( 'downcast' ).markerToHighlight( {
550
- * model: 'comment',
551
- * view: { classes: 'comment' },
552
- * converterPriority: 'high'
553
- * } );
554
- *
555
- * editor.conversion.for( 'downcast' ).markerToHighlight( {
556
- * model: 'comment',
557
- * view: ( data, conversionApi ) => {
558
- * // Assuming that the marker name is in a form of comment:commentType:commentId.
559
- * const [ , commentType, commentId ] = data.markerName.split( ':' );
560
- *
561
- * return {
562
- * classes: [ 'comment', 'comment-' + commentType ],
563
- * attributes: { 'data-comment-id': commentId }
564
- * };
565
- * }
566
- * } );
567
- * ```
568
- *
569
- * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function
570
- * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
571
- * as the parameters and should return a
572
- * {@link module:engine/conversion/downcasthelpers~DowncastHighlightDescriptor highlight descriptor}.
573
- * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
574
- *
575
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
576
- * to the conversion process.
577
- *
578
- * @param config Conversion configuration.
579
- * @param config.model The name of the model marker (or model marker group) to convert.
580
- * @param config.view A highlight descriptor that will be used for highlighting or a function that takes the model marker data and
581
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
582
- * and returns a highlight descriptor.
583
- * @param config.converterPriority Converter priority.
584
- */
585
- markerToHighlight(config) {
586
- return this.add(downcastMarkerToHighlight(config));
587
- }
588
- /**
589
- * Model marker converter for data downcast.
590
- *
591
- * This conversion creates a representation for model marker boundaries in the view:
592
- *
593
- * * If the marker boundary is before or after a model element, a view attribute is set on a corresponding view element.
594
- * * In other cases, a view element with the specified tag name is inserted at the corresponding view position.
595
- *
596
- * Typically, the marker names use the `group:uniqueId:otherData` convention. For example: `comment:e34zfk9k2n459df53sjl34:zx32c`.
597
- * The default configuration for this conversion is that the first part is the `group` part and the rest of
598
- * the marker name becomes the `name` part.
599
- *
600
- * Tag and attribute names and values are generated from the marker name:
601
- *
602
- * * The templates for attributes are `data-[group]-start-before="[name]"`, `data-[group]-start-after="[name]"`,
603
- * `data-[group]-end-before="[name]"` and `data-[group]-end-after="[name]"`.
604
- * * The templates for view elements are `<[group]-start name="[name]">` and `<[group]-end name="[name]">`.
605
- *
606
- * Attributes mark whether the given marker's start or end boundary is before or after the given element.
607
- * The `data-[group]-start-before` and `data-[group]-end-after` attributes are favored.
608
- * The other two are used when the former two cannot be used.
609
- *
610
- * The conversion configuration can take a function that will generate different group and name parts.
611
- * If such a function is set as the `config.view` parameter, it is passed a marker name and it is expected to return an object with two
612
- * properties: `group` and `name`. If the function returns a falsy value, the conversion will not take place.
613
- *
614
- * Basic usage:
615
- *
616
- * ```ts
617
- * // Using the default conversion.
618
- * // In this case, all markers with names starting with 'comment:' will be converted.
619
- * // The `group` parameter will be set to `comment`.
620
- * // The `name` parameter will be the rest of the marker name (without the `:`).
621
- * editor.conversion.for( 'dataDowncast' ).markerToData( {
622
- * model: 'comment'
623
- * } );
624
- * ```
625
- *
626
- * An example of a view that may be generated by this conversion (assuming a marker with the name `comment:commentId:uid` marked
627
- * by `[]`):
628
- *
629
- * ```
630
- * // Model:
631
- * <paragraph>Foo[bar</paragraph>
632
- * <imageBlock src="abc.jpg"></imageBlock>]
633
- *
634
- * // View:
635
- * <p>Foo<comment-start name="commentId:uid"></comment-start>bar</p>
636
- * <figure data-comment-end-after="commentId:uid" class="image"><img src="abc.jpg" /></figure>
637
- * ```
638
- *
639
- * In the example above, the comment starts before "bar" and ends after the image.
640
- *
641
- * If the `name` part is empty, the following view may be generated:
642
- *
643
- * ```html
644
- * <p>Foo <myMarker-start></myMarker-start>bar</p>
645
- * <figure data-myMarker-end-after="" class="image"><img src="abc.jpg" /></figure>
646
- * ```
647
- *
648
- * **Note:** A situation where some markers have the `name` part and some do not, is incorrect and should be avoided.
649
- *
650
- * Examples where `data-group-start-after` and `data-group-end-before` are used:
651
- *
652
- * ```
653
- * // Model:
654
- * <blockQuote>[]<paragraph>Foo</paragraph></blockQuote>
655
- *
656
- * // View:
657
- * <blockquote><p data-group-end-before="name" data-group-start-before="name">Foo</p></blockquote>
658
- * ```
659
- *
660
- * Similarly, when a marker is collapsed after the last element:
661
- *
662
- * ```
663
- * // Model:
664
- * <blockQuote><paragraph>Foo</paragraph>[]</blockQuote>
665
- *
666
- * // View:
667
- * <blockquote><p data-group-end-after="name" data-group-start-after="name">Foo</p></blockquote>
668
- * ```
669
- *
670
- * When there are multiple markers from the same group stored in the same attribute of the same element, their
671
- * name parts are put together in the attribute value, for example: `data-group-start-before="name1,name2,name3"`.
672
- *
673
- * Other examples of usage:
674
- *
675
- * ```ts
676
- * // Using a custom function which is the same as the default conversion:
677
- * editor.conversion.for( 'dataDowncast' ).markerToData( {
678
- * model: 'comment',
679
- * view: markerName => ( {
680
- * group: 'comment',
681
- * name: markerName.substr( 8 ) // Removes 'comment:' part.
682
- * } )
683
- * } );
684
- *
685
- * // Using the converter priority:
686
- * editor.conversion.for( 'dataDowncast' ).markerToData( {
687
- * model: 'comment',
688
- * view: markerName => ( {
689
- * group: 'comment',
690
- * name: markerName.substr( 8 ) // Removes 'comment:' part.
691
- * } ),
692
- * converterPriority: 'high'
693
- * } );
694
- * ```
695
- *
696
- * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline.
697
- *
698
- * See the {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} API guide to learn how to
699
- * add a converter to the conversion process.
700
- *
701
- * @param config Conversion configuration.
702
- * @param config.model The name of the model marker (or the model marker group) to convert.
703
- * @param config.view A function that takes the model marker name and
704
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as the parameters
705
- * and returns an object with the `group` and `name` properties.
706
- * @param config.converterPriority Converter priority.
707
- */
708
- markerToData(config) {
709
- return this.add(downcastMarkerToData(config));
710
- }
711
- }
712
- /**
713
- * Function factory that creates a default downcast converter for text insertion changes.
714
- *
715
- * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
716
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
717
- *
718
- * ```ts
719
- * modelDispatcher.on( 'insert:$text', insertText() );
720
- * ```
721
- *
722
- * @returns Insert text event converter.
723
- * @internal
724
- */
725
- export function insertText() {
726
- return (evt, data, conversionApi) => {
727
- if (!conversionApi.consumable.consume(data.item, evt.name)) {
728
- return;
729
- }
730
- const viewWriter = conversionApi.writer;
731
- const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
732
- const viewText = viewWriter.createText(data.item.data);
733
- viewWriter.insert(viewPosition, viewText);
734
- };
735
- }
736
- /**
737
- * Function factory that creates a default downcast converter for triggering attributes and children conversion.
738
- *
739
- * @returns The converter.
740
- * @internal
741
- */
742
- export function insertAttributesAndChildren() {
743
- return (evt, data, conversionApi) => {
744
- conversionApi.convertAttributes(data.item);
745
- // Start converting children of the current item.
746
- // In case of reconversion children were already re-inserted or converted separately.
747
- if (!data.reconversion && data.item.is('element') && !data.item.isEmpty) {
748
- conversionApi.convertChildren(data.item);
749
- }
750
- };
751
- }
752
- /**
753
- * Function factory that creates a default downcast converter for node remove changes.
754
- *
755
- * ```ts
756
- * modelDispatcher.on( 'remove', remove() );
757
- * ```
758
- *
759
- * @returns Remove event converter.
760
- * @internal
761
- */
762
- export function remove() {
763
- return (evt, data, conversionApi) => {
764
- // Ignore reconversion related remove as it is handled in the `insert` of reconversion.
765
- if (data.reconversion) {
766
- return;
767
- }
768
- // Find the view range start position by mapping the model position at which the remove happened.
769
- const viewStart = conversionApi.mapper.toViewPosition(data.position);
770
- const modelEnd = data.position.getShiftedBy(data.length);
771
- const viewEnd = conversionApi.mapper.toViewPosition(modelEnd, { isPhantom: true });
772
- const viewRange = conversionApi.writer.createRange(viewStart, viewEnd);
773
- // Trim the range to remove in case some UI elements are on the view range boundaries.
774
- removeRangeAndUnbind(viewRange.getTrimmed(), conversionApi);
775
- };
776
- }
777
- /**
778
- * Creates a `<span>` {@link module:engine/view/attributeelement~ViewAttributeElement view attribute element} from the information
779
- * provided by the {@link module:engine/conversion/downcasthelpers~DowncastHighlightDescriptor highlight descriptor} object. If the priority
780
- * is not provided in the descriptor, the default priority will be used.
781
- *
782
- * @internal
783
- */
784
- export function createViewElementFromDowncastHighlightDescriptor(writer, descriptor) {
785
- const viewElement = writer.createAttributeElement('span', descriptor.attributes);
786
- if (descriptor.classes) {
787
- viewElement._addClass(descriptor.classes);
788
- }
789
- if (typeof descriptor.priority === 'number') {
790
- viewElement._priority = descriptor.priority;
791
- }
792
- viewElement._id = descriptor.id;
793
- return viewElement;
794
- }
795
- /**
796
- * Function factory that creates a converter which converts a non-collapsed
797
- * {@link module:engine/model/selection~ModelSelection model selection}
798
- * to a {@link module:engine/view/documentselection~ViewDocumentSelection view selection}. The converter consumes appropriate
799
- * value from the `consumable` object and maps model positions from the selection to view positions.
800
- *
801
- * ```ts
802
- * modelDispatcher.on( 'selection', convertRangeSelection() );
803
- * ```
804
- *
805
- * @returns Selection converter.
806
- * @internal
807
- */
808
- export function convertRangeSelection() {
809
- return (evt, data, conversionApi) => {
810
- const selection = data.selection;
811
- if (selection.isCollapsed) {
812
- return;
813
- }
814
- if (!conversionApi.consumable.consume(selection, 'selection')) {
815
- return;
816
- }
817
- const viewRanges = [];
818
- for (const range of selection.getRanges()) {
819
- viewRanges.push(conversionApi.mapper.toViewRange(range));
820
- }
821
- conversionApi.writer.setSelection(viewRanges, { backward: selection.isBackward });
822
- };
823
- }
824
- /**
825
- * Function factory that creates a converter which converts a collapsed
826
- * {@link module:engine/model/selection~ModelSelection model selection} to
827
- * a {@link module:engine/view/documentselection~ViewDocumentSelection view selection}. The converter consumes appropriate
828
- * value from the `consumable` object, maps the model selection position to the view position and breaks
829
- * {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements} at the selection position.
830
- *
831
- * ```ts
832
- * modelDispatcher.on( 'selection', convertCollapsedSelection() );
833
- * ```
834
- *
835
- * An example of the view state before and after converting the collapsed selection:
836
- *
837
- * ```
838
- * <p><strong>f^oo<strong>bar</p>
839
- * -> <p><strong>f</strong>^<strong>oo</strong>bar</p>
840
- * ```
841
- *
842
- * By breaking attribute elements like `<strong>`, the selection is in a correct element. Then, when the selection attribute is
843
- * converted, broken attributes might be merged again, or the position where the selection is may be wrapped
844
- * with different, appropriate attribute elements.
845
- *
846
- * See also {@link module:engine/conversion/downcasthelpers~cleanSelection} which does a clean-up
847
- * by merging attributes.
848
- *
849
- * @returns Selection converter.
850
- * @internal
851
- */
852
- export function convertCollapsedSelection() {
853
- return (evt, data, conversionApi) => {
854
- const selection = data.selection;
855
- if (!selection.isCollapsed) {
856
- return;
857
- }
858
- if (!conversionApi.consumable.consume(selection, 'selection')) {
859
- return;
860
- }
861
- const viewWriter = conversionApi.writer;
862
- const modelPosition = selection.getFirstPosition();
863
- const viewPosition = conversionApi.mapper.toViewPosition(modelPosition);
864
- const brokenPosition = viewWriter.breakAttributes(viewPosition);
865
- viewWriter.setSelection(brokenPosition);
866
- };
867
- }
868
- /**
869
- * Function factory that creates a converter which cleans artifacts after the previous
870
- * {@link module:engine/model/selection~ModelSelection model selection} conversion. It removes all empty
871
- * {@link module:engine/view/attributeelement~ViewAttributeElement view attribute elements} and merges
872
- * sibling attributes at all start and end positions of all ranges.
873
- *
874
- * ```
875
- * <p><strong>^</strong></p>
876
- * -> <p>^</p>
877
- *
878
- * <p><strong>foo</strong>^<strong>bar</strong>bar</p>
879
- * -> <p><strong>foo^bar<strong>bar</p>
880
- *
881
- * <p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
882
- * -> <p><strong>foo^bar<strong>bar</p>
883
- * ```
884
- *
885
- * This listener should be assigned before any converter for the new selection:
886
- *
887
- * ```ts
888
- * modelDispatcher.on( 'cleanSelection', cleanSelection() );
889
- * ```
890
- *
891
- * See {@link module:engine/conversion/downcasthelpers~convertCollapsedSelection}
892
- * which does the opposite by breaking attributes in the selection position.
893
- *
894
- * @returns Selection converter.
895
- * @internal
896
- */
897
- export function cleanSelection() {
898
- return (evt, data, conversionApi) => {
899
- const viewWriter = conversionApi.writer;
900
- const viewSelection = viewWriter.document.selection;
901
- for (const range of viewSelection.getRanges()) {
902
- // Not collapsed selection should not have artifacts.
903
- if (range.isCollapsed) {
904
- // Position might be in the node removed by the view writer.
905
- if (range.end.parent.isAttached()) {
906
- conversionApi.writer.mergeAttributes(range.start);
907
- }
908
- }
909
- }
910
- viewWriter.setSelection(null);
911
- };
912
- }
913
- /**
914
- * Function factory that creates a converter which converts the set/change/remove attribute changes from the model to the view.
915
- * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the
916
- * selection will be put inside it.
917
- *
918
- * Attributes from the model are converted to a view element that will be wrapping these view nodes that are bound to
919
- * model elements having the given attribute. This is useful for attributes like `bold` that may be set on text nodes in the model
920
- * but are represented as an element in the view:
921
- *
922
- * ```
923
- * [paragraph] MODEL ====> VIEW <p>
924
- * |- a {bold: true} |- <b>
925
- * |- b {bold: true} | |- ab
926
- * |- c |- c
927
- * ```
928
- *
929
- * Passed `Function` will be provided with the attribute value and then all the parameters of the
930
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute` event}.
931
- * It is expected that the function returns an {@link module:engine/view/element~ViewElement}.
932
- * The result of the function will be the wrapping element.
933
- * When the provided `Function` does not return any element, no conversion will take place.
934
- *
935
- * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
936
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
937
- *
938
- * ```ts
939
- * modelDispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, { writer } ) => {
940
- * return writer.createAttributeElement( 'strong' );
941
- * } );
942
- * ```
943
- *
944
- * @internal
945
- * @param elementCreator Function returning a view element that will be used for wrapping.
946
- * @returns Set/change attribute converter.
947
- */
948
- export function wrap(elementCreator) {
949
- return (evt, data, conversionApi) => {
950
- if (!conversionApi.consumable.test(data.item, evt.name)) {
951
- return;
952
- }
953
- // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
954
- // or the attribute was removed.
955
- const oldViewElement = elementCreator(data.attributeOldValue, conversionApi, data);
956
- // Create node to wrap with.
957
- const newViewElement = elementCreator(data.attributeNewValue, conversionApi, data);
958
- if (!oldViewElement && !newViewElement) {
959
- return;
960
- }
961
- conversionApi.consumable.consume(data.item, evt.name);
962
- const viewWriter = conversionApi.writer;
963
- const viewSelection = viewWriter.document.selection;
964
- if (data.item instanceof ModelSelection || data.item instanceof ModelDocumentSelection) {
965
- // Selection attribute conversion.
966
- viewWriter.wrap(viewSelection.getFirstRange(), newViewElement);
967
- }
968
- else {
969
- // Node attribute conversion.
970
- let viewRange = conversionApi.mapper.toViewRange(data.range);
971
- // First, unwrap the range from current wrapper.
972
- if (data.attributeOldValue !== null && oldViewElement) {
973
- viewRange = viewWriter.unwrap(viewRange, oldViewElement);
974
- }
975
- if (data.attributeNewValue !== null && newViewElement) {
976
- viewWriter.wrap(viewRange, newViewElement);
977
- }
978
- }
979
- };
980
- }
981
- /**
982
- * Function factory that creates a converter which converts node insertion changes from the model to the view.
983
- * The function passed will be provided with all the parameters of the dispatcher's
984
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}.
985
- * It is expected that the function returns an {@link module:engine/view/element~ViewElement}.
986
- * The result of the function will be inserted into the view.
987
- *
988
- * The converter automatically consumes the corresponding value from the consumables list and binds the model and view elements.
989
- *
990
- * ```ts
991
- * downcastDispatcher.on(
992
- * 'insert:myElem',
993
- * insertElement( ( modelItem, { writer } ) => {
994
- * const text = writer.createText( 'myText' );
995
- * const myElem = writer.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
996
- *
997
- * // Do something fancy with `myElem` using `modelItem` or other parameters.
998
- *
999
- * return myElem;
1000
- * }
1001
- * ) );
1002
- * ```
1003
- *
1004
- * @internal
1005
- * @param elementCreator Function returning a view element, which will be inserted.
1006
- * @param consumer Function defining element consumption process.
1007
- * By default this function just consume passed item insertion.
1008
- * @returns Insert element event converter.
1009
- */
1010
- export function insertElement(elementCreator, consumer = defaultConsumer) {
1011
- return (evt, data, conversionApi) => {
1012
- if (!consumer(data.item, conversionApi.consumable, { preflight: true })) {
1013
- return;
1014
- }
1015
- const viewElement = elementCreator(data.item, conversionApi, data);
1016
- if (!viewElement) {
1017
- return;
1018
- }
1019
- // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
1020
- consumer(data.item, conversionApi.consumable);
1021
- const viewPosition = data.reconversion && removeElementAndUnbind(data.item, conversionApi) ||
1022
- conversionApi.mapper.toViewPosition(data.range.start);
1023
- conversionApi.mapper.bindElements(data.item, viewElement);
1024
- conversionApi.writer.insert(viewPosition, viewElement);
1025
- // Convert attributes before converting children.
1026
- conversionApi.convertAttributes(data.item);
1027
- // Convert children or reinsert previous view elements.
1028
- reinsertOrConvertNodes(viewElement, data.item.getChildren(), conversionApi, { reconversion: data.reconversion });
1029
- };
1030
- }
1031
- /**
1032
- * Function factory that creates a converter which converts a single model node insertion to a view structure.
1033
- *
1034
- * It is expected that the passed element creator function returns an {@link module:engine/view/element~ViewElement} with attached slots
1035
- * created with `writer.createSlot()` to indicate where child nodes should be converted.
1036
- *
1037
- * @see module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
1038
- *
1039
- * @internal
1040
- * @param elementCreator Function returning a view structure, which will be inserted.
1041
- * @param consumer A callback that is expected to consume all the consumables
1042
- * that were used by the element creator.
1043
- * @returns Insert element event converter.
1044
- */
1045
- export function insertStructure(elementCreator, consumer) {
1046
- return (evt, data, conversionApi) => {
1047
- if (!consumer(data.item, conversionApi.consumable, { preflight: true })) {
1048
- return;
1049
- }
1050
- const slotsMap = new Map();
1051
- conversionApi.writer._registerSlotFactory(createSlotFactory(data.item, slotsMap, conversionApi));
1052
- // View creation.
1053
- const viewElement = elementCreator(data.item, conversionApi, data);
1054
- conversionApi.writer._clearSlotFactory();
1055
- if (!viewElement) {
1056
- return;
1057
- }
1058
- // Check if all children are covered by slots and there is no child that landed in multiple slots.
1059
- validateSlotsChildren(data.item, slotsMap, conversionApi);
1060
- // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
1061
- consumer(data.item, conversionApi.consumable);
1062
- const viewPosition = data.reconversion && removeElementAndUnbind(data.item, conversionApi) ||
1063
- conversionApi.mapper.toViewPosition(data.range.start);
1064
- conversionApi.mapper.bindElements(data.item, viewElement);
1065
- conversionApi.writer.insert(viewPosition, viewElement);
1066
- // Convert attributes before converting children.
1067
- conversionApi.convertAttributes(data.item);
1068
- // Fill view slots with previous view elements or create new ones.
1069
- fillSlots(viewElement, slotsMap, conversionApi, { reconversion: data.reconversion });
1070
- };
1071
- }
1072
- /**
1073
- * Function factory that creates a converter which converts marker adding change to the
1074
- * {@link module:engine/view/uielement~ViewUIElement view UI element}.
1075
- *
1076
- * The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}.
1077
- * In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning
1078
- * and at the end of the range.
1079
- *
1080
- * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
1081
- *
1082
- * @internal
1083
- * @param elementCreator A view UI element or a function returning the view element that will be inserted.
1084
- * @returns Insert element event converter.
1085
- */
1086
- export function insertUIElement(elementCreator) {
1087
- return (evt, data, conversionApi) => {
1088
- // Create two view elements. One will be inserted at the beginning of marker, one at the end.
1089
- // If marker is collapsed, only "opening" element will be inserted.
1090
- data.isOpening = true;
1091
- const viewStartElement = elementCreator(data, conversionApi);
1092
- data.isOpening = false;
1093
- const viewEndElement = elementCreator(data, conversionApi);
1094
- if (!viewStartElement || !viewEndElement) {
1095
- return;
1096
- }
1097
- const markerRange = data.markerRange;
1098
- // Marker that is collapsed has consumable build differently that non-collapsed one.
1099
- // For more information see `addMarker` event description.
1100
- // If marker's range is collapsed - check if it can be consumed.
1101
- if (markerRange.isCollapsed && !conversionApi.consumable.consume(markerRange, evt.name)) {
1102
- return;
1103
- }
1104
- // If marker's range is not collapsed - consume all items inside.
1105
- for (const value of markerRange) {
1106
- if (!conversionApi.consumable.consume(value.item, evt.name)) {
1107
- return;
1108
- }
1109
- }
1110
- const mapper = conversionApi.mapper;
1111
- const viewWriter = conversionApi.writer;
1112
- // Add "opening" element.
1113
- viewWriter.insert(mapper.toViewPosition(markerRange.start), viewStartElement);
1114
- conversionApi.mapper.bindElementToMarker(viewStartElement, data.markerName);
1115
- // Add "closing" element only if range is not collapsed.
1116
- if (!markerRange.isCollapsed) {
1117
- viewWriter.insert(mapper.toViewPosition(markerRange.end), viewEndElement);
1118
- conversionApi.mapper.bindElementToMarker(viewEndElement, data.markerName);
1119
- }
1120
- evt.stop();
1121
- };
1122
- }
1123
- /**
1124
- * Removes given view range content and unbinds removed elements.
1125
- */
1126
- function removeRangeAndUnbind(viewRange, conversionApi) {
1127
- const removed = conversionApi.writer.remove(viewRange);
1128
- // After the range is removed, unbind all view elements from the model.
1129
- // Range inside view document fragment is used to unbind deeply.
1130
- for (const child of conversionApi.writer.createRangeIn(removed).getItems()) {
1131
- conversionApi.mapper.unbindViewElement(child, { defer: true });
1132
- }
1133
- return viewRange.start;
1134
- }
1135
- /**
1136
- * Removes view element for given model element and unbinds removed view elements.
1137
- */
1138
- function removeElementAndUnbind(modelElement, conversionApi) {
1139
- const viewElement = conversionApi.mapper.toViewElement(modelElement);
1140
- return viewElement && removeRangeAndUnbind(conversionApi.writer.createRangeOn(viewElement), conversionApi);
1141
- }
1142
- /**
1143
- * Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~ViewUIElement UI element}
1144
- * based on marker remove change.
1145
- *
1146
- * This converter unbinds elements from the marker name.
1147
- *
1148
- * @returns Removed UI element converter.
1149
- */
1150
- function removeUIElement() {
1151
- return (evt, data, conversionApi) => {
1152
- const elements = conversionApi.mapper.markerNameToElements(data.markerName);
1153
- if (!elements) {
1154
- return;
1155
- }
1156
- for (const element of elements) {
1157
- conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
1158
- conversionApi.writer.clear(conversionApi.writer.createRangeOn(element), element);
1159
- }
1160
- conversionApi.writer.clearClonedElementsGroup(data.markerName);
1161
- evt.stop();
1162
- };
1163
- }
1164
- /**
1165
- * Function factory that creates a default converter for model markers.
1166
- *
1167
- * See {@link DowncastHelpers#markerToData} for more information what type of view is generated.
1168
- *
1169
- * This converter binds created UI elements and affected view elements with the marker name
1170
- * using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
1171
- *
1172
- * @returns Add marker converter.
1173
- */
1174
- function insertMarkerData(viewCreator) {
1175
- return (evt, data, conversionApi) => {
1176
- const viewMarkerData = viewCreator(data.markerName, conversionApi);
1177
- if (!viewMarkerData) {
1178
- return;
1179
- }
1180
- const markerRange = data.markerRange;
1181
- if (!conversionApi.consumable.consume(markerRange, evt.name)) {
1182
- return;
1183
- }
1184
- // Adding closing data first to keep the proper order in the view.
1185
- handleMarkerBoundary(markerRange, false, conversionApi, data, viewMarkerData);
1186
- handleMarkerBoundary(markerRange, true, conversionApi, data, viewMarkerData);
1187
- evt.stop();
1188
- };
1189
- }
1190
- /**
1191
- * Helper function for `insertMarkerData()` that marks a marker boundary at the beginning or end of given `range`.
1192
- */
1193
- function handleMarkerBoundary(range, isStart, conversionApi, data, viewMarkerData) {
1194
- const modelPosition = isStart ? range.start : range.end;
1195
- const elementAfter = modelPosition.nodeAfter && modelPosition.nodeAfter.is('element') ? modelPosition.nodeAfter : null;
1196
- const elementBefore = modelPosition.nodeBefore && modelPosition.nodeBefore.is('element') ? modelPosition.nodeBefore : null;
1197
- if (elementAfter || elementBefore) {
1198
- let modelElement;
1199
- let isBefore;
1200
- // If possible, we want to add `data-group-start-before` and `data-group-end-after` attributes.
1201
- if (isStart && elementAfter || !isStart && !elementBefore) {
1202
- // [<elementAfter>...</elementAfter> -> <elementAfter data-group-start-before="...">...</elementAfter>
1203
- // <parent>]<elementAfter> -> <parent><elementAfter data-group-end-before="...">
1204
- modelElement = elementAfter;
1205
- isBefore = true;
1206
- }
1207
- else {
1208
- // <elementBefore>...</elementBefore>] -> <elementBefore data-group-end-after="...">...</elementBefore>
1209
- // </elementBefore>[</parent> -> </elementBefore data-group-start-after="..."></parent>
1210
- modelElement = elementBefore;
1211
- isBefore = false;
1212
- }
1213
- const viewElement = conversionApi.mapper.toViewElement(modelElement);
1214
- // In rare circumstances, the model element may be not mapped to any view element and that would cause an error.
1215
- // One of those situations is a soft break inside code block.
1216
- if (viewElement) {
1217
- insertMarkerAsAttribute(viewElement, isStart, isBefore, conversionApi, data, viewMarkerData);
1218
- return;
1219
- }
1220
- }
1221
- const viewPosition = conversionApi.mapper.toViewPosition(modelPosition);
1222
- insertMarkerAsElement(viewPosition, isStart, conversionApi, data, viewMarkerData);
1223
- }
1224
- /**
1225
- * Helper function for `insertMarkerData()` that marks a marker boundary in the view as an attribute on a view element.
1226
- */
1227
- function insertMarkerAsAttribute(viewElement, isStart, isBefore, conversionApi, data, viewMarkerData) {
1228
- const attributeName = `data-${viewMarkerData.group}-${isStart ? 'start' : 'end'}-${isBefore ? 'before' : 'after'}`;
1229
- const markerNames = viewElement.hasAttribute(attributeName) ? viewElement.getAttribute(attributeName).split(',') : [];
1230
- // Adding marker name at the beginning to have the same order in the attribute as there is with marker elements.
1231
- markerNames.unshift(viewMarkerData.name);
1232
- conversionApi.writer.setAttribute(attributeName, markerNames.join(','), viewElement);
1233
- conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
1234
- }
1235
- /**
1236
- * Helper function for `insertMarkerData()` that marks a marker boundary in the view as a separate view ui element.
1237
- */
1238
- function insertMarkerAsElement(position, isStart, conversionApi, data, viewMarkerData) {
1239
- const viewElementName = `${viewMarkerData.group}-${isStart ? 'start' : 'end'}`;
1240
- const attrs = viewMarkerData.name ? { 'name': viewMarkerData.name } : null;
1241
- const viewElement = conversionApi.writer.createUIElement(viewElementName, attrs);
1242
- conversionApi.writer.insert(position, viewElement);
1243
- conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
1244
- }
1245
- /**
1246
- * Function factory that creates a converter for removing a model marker data added by the {@link #insertMarkerData} converter.
1247
- *
1248
- * @returns Remove marker converter.
1249
- */
1250
- function removeMarkerData(viewCreator) {
1251
- return (evt, data, conversionApi) => {
1252
- const viewData = viewCreator(data.markerName, conversionApi);
1253
- if (!viewData) {
1254
- return;
1255
- }
1256
- const elements = conversionApi.mapper.markerNameToElements(data.markerName);
1257
- if (!elements) {
1258
- return;
1259
- }
1260
- for (const element of elements) {
1261
- conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
1262
- if (element.is('containerElement')) {
1263
- removeMarkerFromAttribute(`data-${viewData.group}-start-before`, element);
1264
- removeMarkerFromAttribute(`data-${viewData.group}-start-after`, element);
1265
- removeMarkerFromAttribute(`data-${viewData.group}-end-before`, element);
1266
- removeMarkerFromAttribute(`data-${viewData.group}-end-after`, element);
1267
- }
1268
- else {
1269
- conversionApi.writer.clear(conversionApi.writer.createRangeOn(element), element);
1270
- }
1271
- }
1272
- conversionApi.writer.clearClonedElementsGroup(data.markerName);
1273
- evt.stop();
1274
- function removeMarkerFromAttribute(attributeName, element) {
1275
- if (element.hasAttribute(attributeName)) {
1276
- const markerNames = new Set(element.getAttribute(attributeName).split(','));
1277
- markerNames.delete(viewData.name);
1278
- if (markerNames.size == 0) {
1279
- conversionApi.writer.removeAttribute(attributeName, element);
1280
- }
1281
- else {
1282
- conversionApi.writer.setAttribute(attributeName, Array.from(markerNames).join(','), element);
1283
- }
1284
- }
1285
- }
1286
- };
1287
- }
1288
- /**
1289
- * Function factory that creates a converter which converts the set/change/remove attribute changes from the model to the view.
1290
- *
1291
- * Attributes from the model are converted to the view element attributes in the view. You may provide a custom function to generate
1292
- * a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view element
1293
- * attributes on a one-to-one basis.
1294
- *
1295
- * *Note:** The provided attribute creator should always return the same `key` for a given attribute from the model.
1296
- *
1297
- * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
1298
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
1299
- *
1300
- * ```ts
1301
- * modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( value, data ) => {
1302
- * // Change attribute key from `customAttr` to `class` in the view.
1303
- * const key = 'class';
1304
- * let value = data.attributeNewValue;
1305
- *
1306
- * // Force attribute value to 'empty' if the model element is empty.
1307
- * if ( data.item.childCount === 0 ) {
1308
- * value = 'empty';
1309
- * }
1310
- *
1311
- * // Return the key-value pair.
1312
- * return { key, value };
1313
- * } ) );
1314
- * ```
1315
- *
1316
- * @param attributeCreator Function returning an object with two properties: `key` and `value`, which
1317
- * represent the attribute key and attribute value to be set on a {@link module:engine/view/element~ViewElement view element}.
1318
- * The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter.
1319
- * @returns Set/change attribute converter.
1320
- */
1321
- function changeAttribute(attributeCreator) {
1322
- return (evt, data, conversionApi) => {
1323
- if (!conversionApi.consumable.test(data.item, evt.name)) {
1324
- return;
1325
- }
1326
- const oldAttribute = attributeCreator(data.attributeOldValue, conversionApi, data);
1327
- const newAttribute = attributeCreator(data.attributeNewValue, conversionApi, data);
1328
- if (!oldAttribute && !newAttribute) {
1329
- return;
1330
- }
1331
- conversionApi.consumable.consume(data.item, evt.name);
1332
- const viewElement = conversionApi.mapper.toViewElement(data.item);
1333
- const viewWriter = conversionApi.writer;
1334
- // If model item cannot be mapped to a view element, it means item is not an `Element` instance but a `ModelTextProxy` node.
1335
- // Only elements can have attributes in a view so do not proceed for anything else (#1587).
1336
- if (!viewElement) {
1337
- /**
1338
- * This error occurs when a {@link module:engine/model/textproxy~ModelTextProxy text node's} attribute is to be downcasted
1339
- * by an {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `Attribute to Attribute converter`}.
1340
- * In most cases it is caused by converters misconfiguration when only "generic" converter is defined:
1341
- *
1342
- * ```ts
1343
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
1344
- * model: 'attribute-name',
1345
- * view: 'attribute-name'
1346
- * } ) );
1347
- * ```
1348
- *
1349
- * and given attribute is used on text node, for example:
1350
- *
1351
- * ```ts
1352
- * model.change( writer => {
1353
- * writer.insertText( 'Foo', { 'attribute-name': 'bar' }, parent, 0 );
1354
- * } );
1355
- * ```
1356
- *
1357
- * In such cases, to convert the same attribute for both {@link module:engine/model/element~ModelElement}
1358
- * and {@link module:engine/model/textproxy~ModelTextProxy `Text`} nodes, text specific
1359
- * {@link module:engine/conversion/conversion~Conversion#attributeToElement `Attribute to Element converter`}
1360
- * with higher {@link module:utils/priorities~PriorityString priority} must also be defined:
1361
- *
1362
- * ```ts
1363
- * editor.conversion.for( 'downcast' ).attributeToElement( {
1364
- * model: {
1365
- * key: 'attribute-name',
1366
- * name: '$text'
1367
- * },
1368
- * view: ( value, { writer } ) => {
1369
- * return writer.createAttributeElement( 'span', { 'attribute-name': value } );
1370
- * },
1371
- * converterPriority: 'high'
1372
- * } ) );
1373
- * ```
1374
- *
1375
- * @error conversion-attribute-to-attribute-on-text
1376
- * @param {object} data The conversion data.
1377
- */
1378
- throw new CKEditorError('conversion-attribute-to-attribute-on-text', conversionApi.dispatcher, data);
1379
- }
1380
- // First remove the old attribute if there was one.
1381
- if (data.attributeOldValue !== null && oldAttribute) {
1382
- let value = oldAttribute.value;
1383
- if (oldAttribute.key == 'style') {
1384
- if (typeof oldAttribute.value == 'string') {
1385
- value = new StylesMap(viewWriter.document.stylesProcessor)
1386
- .setTo(oldAttribute.value)
1387
- .getStylesEntries()
1388
- .map(([key]) => key);
1389
- }
1390
- else {
1391
- value = Object.keys(oldAttribute.value);
1392
- }
1393
- }
1394
- viewWriter.removeAttribute(oldAttribute.key, value, viewElement);
1395
- }
1396
- // Then set the new attribute.
1397
- if (data.attributeNewValue !== null && newAttribute) {
1398
- let value = newAttribute.value;
1399
- if (newAttribute.key == 'style' && typeof newAttribute.value == 'string') {
1400
- value = Object.fromEntries(new StylesMap(viewWriter.document.stylesProcessor)
1401
- .setTo(newAttribute.value)
1402
- .getStylesEntries());
1403
- }
1404
- viewWriter.setAttribute(newAttribute.key, value, false, viewElement);
1405
- }
1406
- };
1407
- }
1408
- /**
1409
- * Function factory that creates a converter which converts the text inside marker's range. The converter wraps the text with
1410
- * {@link module:engine/view/attributeelement~ViewAttributeElement} created from the provided descriptor.
1411
- * See {link module:engine/conversion/downcasthelpers~createViewElementFromDowncastHighlightDescriptor}.
1412
- *
1413
- * It can also be used to convert the selection that is inside a marker. In that case, an empty attribute element will be
1414
- * created and the selection will be put inside it.
1415
- *
1416
- * If the highlight descriptor does not provide the `priority` property, `10` will be used.
1417
- *
1418
- * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1419
- *
1420
- * This converter binds the created {@link module:engine/view/attributeelement~ViewAttributeElement attribute elemens} with the marker name
1421
- * using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
1422
- */
1423
- function highlightText(highlightDescriptor) {
1424
- return (evt, data, conversionApi) => {
1425
- if (!data.item) {
1426
- return;
1427
- }
1428
- if (!(data.item instanceof ModelSelection || data.item instanceof ModelDocumentSelection) && !data.item.is('$textProxy')) {
1429
- return;
1430
- }
1431
- const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
1432
- if (!descriptor) {
1433
- return;
1434
- }
1435
- if (!conversionApi.consumable.consume(data.item, evt.name)) {
1436
- return;
1437
- }
1438
- const viewWriter = conversionApi.writer;
1439
- const viewElement = createViewElementFromDowncastHighlightDescriptor(viewWriter, descriptor);
1440
- const viewSelection = viewWriter.document.selection;
1441
- if (data.item instanceof ModelSelection || data.item instanceof ModelDocumentSelection) {
1442
- viewWriter.wrap(viewSelection.getFirstRange(), viewElement);
1443
- }
1444
- else {
1445
- const viewRange = conversionApi.mapper.toViewRange(data.range);
1446
- const rangeAfterWrap = viewWriter.wrap(viewRange, viewElement);
1447
- for (const element of rangeAfterWrap.getItems()) {
1448
- if (element.is('attributeElement') && element.isSimilar(viewElement)) {
1449
- conversionApi.mapper.bindElementToMarker(element, data.markerName);
1450
- // One attribute element is enough, because all of them are bound together by the view writer.
1451
- // Mapper uses this binding to get all the elements no matter how many of them are registered in the mapper.
1452
- break;
1453
- }
1454
- }
1455
- }
1456
- };
1457
- }
1458
- /**
1459
- * Converter function factory. It creates a function which applies the marker's highlight to an element inside the marker's range.
1460
- *
1461
- * The converter checks if an element has the `addHighlight` function stored as a
1462
- * {@link module:engine/view/element~ViewElement#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
1463
- * In such case the converter will consume all element's children, assuming that they were handled by the element itself.
1464
- *
1465
- * When the `addHighlight` custom property is not present, the element is not converted in any special way.
1466
- * This means that converters will proceed to convert the element's child nodes.
1467
- *
1468
- * If the highlight descriptor does not provide the `priority` property, `10` will be used.
1469
- *
1470
- * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1471
- *
1472
- * This converter binds altered {@link module:engine/view/containerelement~ViewContainerElement container elements}
1473
- * with the marker name using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
1474
- */
1475
- function highlightElement(highlightDescriptor) {
1476
- return (evt, data, conversionApi) => {
1477
- if (!data.item) {
1478
- return;
1479
- }
1480
- if (!(data.item instanceof ModelElement)) {
1481
- return;
1482
- }
1483
- const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
1484
- if (!descriptor) {
1485
- return;
1486
- }
1487
- if (!conversionApi.consumable.test(data.item, evt.name)) {
1488
- return;
1489
- }
1490
- const viewElement = conversionApi.mapper.toViewElement(data.item);
1491
- if (viewElement && viewElement.getCustomProperty('addHighlight')) {
1492
- // Consume element itself.
1493
- conversionApi.consumable.consume(data.item, evt.name);
1494
- // Consume all children nodes.
1495
- for (const value of ModelRange._createIn(data.item)) {
1496
- conversionApi.consumable.consume(value.item, evt.name);
1497
- }
1498
- const addHighlightCallback = viewElement.getCustomProperty('addHighlight');
1499
- addHighlightCallback(viewElement, descriptor, conversionApi.writer);
1500
- conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
1501
- }
1502
- };
1503
- }
1504
- /**
1505
- * Function factory that creates a converter which converts the removing model marker to the view.
1506
- *
1507
- * Both text nodes and elements are handled by this converter but they are handled a bit differently.
1508
- *
1509
- * Text nodes are unwrapped using the {@link module:engine/view/attributeelement~ViewAttributeElement attribute element} created from the
1510
- * provided highlight descriptor. See {link module:engine/conversion/downcasthelpers~DowncastHighlightDescriptor}.
1511
- *
1512
- * For elements, the converter checks if an element has the `removeHighlight` function stored as a
1513
- * {@link module:engine/view/element~ViewElement#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
1514
- * In such case, the children of that element will not be converted.
1515
- *
1516
- * When `removeHighlight` is not present, the element is not converted in any special way.
1517
- * The converter will proceed to convert the element's child nodes instead.
1518
- *
1519
- * If the highlight descriptor does not provide the `priority` property, `10` will be used.
1520
- *
1521
- * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1522
- *
1523
- * This converter unbinds elements from the marker name.
1524
- */
1525
- function removeHighlight(highlightDescriptor) {
1526
- return (evt, data, conversionApi) => {
1527
- // This conversion makes sense only for non-collapsed range.
1528
- if (data.markerRange.isCollapsed) {
1529
- return;
1530
- }
1531
- const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
1532
- if (!descriptor) {
1533
- return;
1534
- }
1535
- // View element that will be used to unwrap `AttributeElement`s.
1536
- const viewHighlightElement = createViewElementFromDowncastHighlightDescriptor(conversionApi.writer, descriptor);
1537
- // Get all elements bound with given marker name.
1538
- const elements = conversionApi.mapper.markerNameToElements(data.markerName);
1539
- if (!elements) {
1540
- return;
1541
- }
1542
- for (const element of elements) {
1543
- conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
1544
- if (element.is('attributeElement')) {
1545
- conversionApi.writer.unwrap(conversionApi.writer.createRangeOn(element), viewHighlightElement);
1546
- }
1547
- else {
1548
- // if element.is( 'containerElement' ).
1549
- const removeHighlightCallback = element.getCustomProperty('removeHighlight');
1550
- removeHighlightCallback(element, descriptor.id, conversionApi.writer);
1551
- }
1552
- }
1553
- conversionApi.writer.clearClonedElementsGroup(data.markerName);
1554
- evt.stop();
1555
- };
1556
- }
1557
- /**
1558
- * Model element to view element conversion helper.
1559
- *
1560
- * See {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper} for examples and config params description.
1561
- *
1562
- * @param config Conversion configuration.
1563
- * @param config.model The description or a name of the model element to convert.
1564
- * @param config.model.attributes List of attributes triggering element reconversion.
1565
- * @param config.model.children Should reconvert element if the list of model child nodes changed.
1566
- * @returns Conversion helper.
1567
- */
1568
- function downcastElementToElement(config) {
1569
- const model = normalizeModelElementConfig(config.model);
1570
- const view = normalizeToElementConfig(config.view, 'container');
1571
- // Trigger reconversion on children list change if element is a subject to any reconversion.
1572
- // This is required to be able to trigger Differ#refreshItem() on a direct child of the reconverted element.
1573
- if (model.attributes.length) {
1574
- model.children = true;
1575
- }
1576
- return (dispatcher) => {
1577
- dispatcher.on(`insert:${model.name}`, insertElement(view, createConsumer(model)), { priority: config.converterPriority || 'normal' });
1578
- if (model.children || model.attributes.length) {
1579
- dispatcher.on('reduceChanges', createChangeReducer(model), { priority: 'low' });
1580
- }
1581
- };
1582
- }
1583
- /**
1584
- * Model element to view structure conversion helper.
1585
- *
1586
- * See {@link ~DowncastHelpers#elementToStructure `.elementToStructure()` downcast helper} for examples and config params description.
1587
- *
1588
- * @param config Conversion configuration.
1589
- * @returns Conversion helper.
1590
- */
1591
- function downcastElementToStructure(config) {
1592
- const model = normalizeModelElementConfig(config.model);
1593
- const view = normalizeToElementConfig(config.view, 'container');
1594
- // Trigger reconversion on children list change because it always needs to use slots to put children in proper places.
1595
- // This is required to be able to trigger Differ#refreshItem() on a direct child of the reconverted element.
1596
- model.children = true;
1597
- return (dispatcher) => {
1598
- if (dispatcher._conversionApi.schema.checkChild(model.name, '$text')) {
1599
- /**
1600
- * This error occurs when a {@link module:engine/model/element~ModelElement model element} is downcasted
1601
- * via {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure} helper but the element was
1602
- * allowed to host `$text` by the {@link module:engine/model/schema~ModelSchema model schema}.
1603
- *
1604
- * For instance, this may be the result of `myElement` allowing the content of
1605
- * {@glink framework/deep-dive/schema#generic-items `$block`} in its schema definition:
1606
- *
1607
- * ```ts
1608
- * // Element definition in schema.
1609
- * schema.register( 'myElement', {
1610
- * allowContentOf: '$block',
1611
- *
1612
- * // ...
1613
- * } );
1614
- *
1615
- * // ...
1616
- *
1617
- * // Conversion of myElement with the use of elementToStructure().
1618
- * editor.conversion.for( 'downcast' ).elementToStructure( {
1619
- * model: 'myElement',
1620
- * view: ( modelElement, { writer } ) => {
1621
- * // ...
1622
- * }
1623
- * } );
1624
- * ```
1625
- *
1626
- * In such case, {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} helper
1627
- * can be used instead to get around this problem:
1628
- *
1629
- * ```ts
1630
- * editor.conversion.for( 'downcast' ).elementToElement( {
1631
- * model: 'myElement',
1632
- * view: ( modelElement, { writer } ) => {
1633
- * // ...
1634
- * }
1635
- * } );
1636
- * ```
1637
- *
1638
- * @error conversion-element-to-structure-disallowed-text
1639
- * @param {string} elementName The name of the element the structure is to be created for.
1640
- */
1641
- throw new CKEditorError('conversion-element-to-structure-disallowed-text', dispatcher, { elementName: model.name });
1642
- }
1643
- dispatcher.on(`insert:${model.name}`, insertStructure(view, createConsumer(model)), { priority: config.converterPriority || 'normal' });
1644
- dispatcher.on('reduceChanges', createChangeReducer(model), { priority: 'low' });
1645
- };
1646
- }
1647
- /**
1648
- * Model attribute to view element conversion helper.
1649
- *
1650
- * See {@link ~DowncastHelpers#attributeToElement `.attributeToElement()` downcast helper} for examples.
1651
- *
1652
- * @param config Conversion configuration.
1653
- * @param config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
1654
- * of `String`s with possible values if the model attribute is an enumerable.
1655
- * @param config.view A view element definition or a function that takes the model attribute value and
1656
- * {@link module:engine/view/downcastwriter~ViewDowncastWriter view downcast writer} as parameters and returns a view attribute element.
1657
- * If `config.model.values` is given, `config.view` should be an object assigning values from `config.model.values` to view element
1658
- * definitions or functions.
1659
- * @param config.converterPriority Converter priority.
1660
- * @returns Conversion helper.
1661
- */
1662
- function downcastAttributeToElement(config) {
1663
- config = cloneDeep(config);
1664
- let model = config.model;
1665
- if (typeof model == 'string') {
1666
- model = { key: model };
1667
- }
1668
- let eventName = `attribute:${model.key}`;
1669
- if (model.name) {
1670
- eventName += ':' + model.name;
1671
- }
1672
- if (model.values) {
1673
- for (const modelValue of model.values) {
1674
- config.view[modelValue] = normalizeToElementConfig(config.view[modelValue], 'attribute');
1675
- }
1676
- }
1677
- else {
1678
- config.view = normalizeToElementConfig(config.view, 'attribute');
1679
- }
1680
- const elementCreator = getFromAttributeCreator(config);
1681
- return (dispatcher) => {
1682
- dispatcher.on(eventName, wrap(elementCreator), { priority: config.converterPriority || 'normal' });
1683
- };
1684
- }
1685
- /**
1686
- * Model attribute to view attribute conversion helper.
1687
- *
1688
- * See {@link ~DowncastHelpers#attributeToAttribute `.attributeToAttribute()` downcast helper} for examples.
1689
- *
1690
- * @param config Conversion configuration.
1691
- * @param config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
1692
- * the attribute key, possible values and, optionally, an element name to convert from.
1693
- * @param config.view A view attribute key, or a `{ key, value }` object or a function that takes the model attribute value and returns
1694
- * a `{ key, value }` object.
1695
- * If `key` is `'class'`, `value` can be a `String` or an array of `String`s. If `key` is `'style'`, `value` is an object with
1696
- * key-value pairs. In other cases, `value` is a `String`.
1697
- * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
1698
- * `{ key, value }` objects or a functions.
1699
- * @param config.converterPriority Converter priority.
1700
- * @returns Conversion helper.
1701
- */
1702
- function downcastAttributeToAttribute(config) {
1703
- config = cloneDeep(config);
1704
- let model = config.model;
1705
- if (typeof model == 'string') {
1706
- model = { key: model };
1707
- }
1708
- let eventName = `attribute:${model.key}`;
1709
- if (model.name) {
1710
- eventName += ':' + model.name;
1711
- }
1712
- if (model.values) {
1713
- for (const modelValue of model.values) {
1714
- config.view[modelValue] = normalizeToAttributeConfig(config.view[modelValue]);
1715
- }
1716
- }
1717
- else {
1718
- config.view = normalizeToAttributeConfig(config.view);
1719
- }
1720
- const elementCreator = getFromAttributeCreator(config);
1721
- return (dispatcher) => {
1722
- dispatcher.on(eventName, changeAttribute(elementCreator), { priority: config.converterPriority || 'normal' });
1723
- };
1724
- }
1725
- /**
1726
- * Model marker to view element conversion helper.
1727
- *
1728
- * See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
1729
- *
1730
- * @param config Conversion configuration.
1731
- * @param config.model The name of the model marker (or model marker group) to convert.
1732
- * @param config.view A view element definition or a function that takes the model marker data as a parameter and returns a view UI element.
1733
- * @param config.converterPriority Converter priority.
1734
- * @returns Conversion helper.
1735
- */
1736
- function downcastMarkerToElement(config) {
1737
- const view = normalizeToElementConfig(config.view, 'ui');
1738
- return (dispatcher) => {
1739
- dispatcher.on(`addMarker:${config.model}`, insertUIElement(view), { priority: config.converterPriority || 'normal' });
1740
- dispatcher.on(`removeMarker:${config.model}`, removeUIElement(), { priority: config.converterPriority || 'normal' });
1741
- };
1742
- }
1743
- /**
1744
- * Model marker to view data conversion helper.
1745
- *
1746
- * See {@link ~DowncastHelpers#markerToData `markerToData()` downcast helper} to learn more.
1747
- *
1748
- * @returns Conversion helper.
1749
- */
1750
- function downcastMarkerToData(config) {
1751
- config = cloneDeep(config);
1752
- const group = config.model;
1753
- let view = config.view;
1754
- // Default conversion.
1755
- if (!view) {
1756
- view = markerName => ({
1757
- group,
1758
- name: markerName.substr(config.model.length + 1)
1759
- });
1760
- }
1761
- return (dispatcher) => {
1762
- dispatcher.on(`addMarker:${group}`, insertMarkerData(view), { priority: config.converterPriority || 'normal' });
1763
- dispatcher.on(`removeMarker:${group}`, removeMarkerData(view), { priority: config.converterPriority || 'normal' });
1764
- };
1765
- }
1766
- /**
1767
- * Model marker to highlight conversion helper.
1768
- *
1769
- * See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
1770
- *
1771
- * @param config Conversion configuration.
1772
- * @param config.model The name of the model marker (or model marker group) to convert.
1773
- * @param config.view A highlight descriptor that will be used for highlighting or a function that takes
1774
- * the model marker data as a parameter and returns a highlight descriptor.
1775
- * @param config.converterPriority Converter priority.
1776
- * @returns Conversion helper.
1777
- */
1778
- function downcastMarkerToHighlight(config) {
1779
- return (dispatcher) => {
1780
- dispatcher.on(`addMarker:${config.model}`, highlightText(config.view), { priority: config.converterPriority || 'normal' });
1781
- dispatcher.on(`addMarker:${config.model}`, highlightElement(config.view), { priority: config.converterPriority || 'normal' });
1782
- dispatcher.on(`removeMarker:${config.model}`, removeHighlight(config.view), { priority: config.converterPriority || 'normal' });
1783
- };
1784
- }
1785
- /**
1786
- * Takes `config.model`, and converts it to an object with normalized structure.
1787
- *
1788
- * @param model Model configuration or element name.
1789
- */
1790
- function normalizeModelElementConfig(model) {
1791
- if (typeof model == 'string') {
1792
- model = { name: model };
1793
- }
1794
- return {
1795
- name: model.name,
1796
- attributes: model.attributes ? toArray(model.attributes) : [],
1797
- children: !!model.children
1798
- };
1799
- }
1800
- /**
1801
- * Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ViewElementDefinition}, converts it
1802
- * to a function (because lower level converters accept only element creator functions).
1803
- *
1804
- * @param view View configuration.
1805
- * @param viewElementType View element type to create.
1806
- * @returns Element creator function to use in lower level converters.
1807
- */
1808
- function normalizeToElementConfig(view, viewElementType) {
1809
- if (typeof view == 'function') {
1810
- // If `view` is already a function, don't do anything.
1811
- return view;
1812
- }
1813
- return ((modelData, conversionApi) => createViewElementFromDefinition(view, conversionApi, viewElementType));
1814
- }
1815
- /**
1816
- * Creates a view element instance from the provided {@link module:engine/view/elementdefinition~ViewElementDefinition} and class.
1817
- */
1818
- function createViewElementFromDefinition(viewElementDefinition, conversionApi, viewElementType) {
1819
- if (typeof viewElementDefinition == 'string') {
1820
- // If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property.
1821
- viewElementDefinition = { name: viewElementDefinition };
1822
- }
1823
- let element;
1824
- const viewWriter = conversionApi.writer;
1825
- const attributes = Object.assign({}, viewElementDefinition.attributes);
1826
- if (viewElementType == 'container') {
1827
- element = viewWriter.createContainerElement(viewElementDefinition.name, attributes);
1828
- }
1829
- else if (viewElementType == 'attribute') {
1830
- const options = {
1831
- priority: viewElementDefinition.priority || ViewAttributeElement.DEFAULT_PRIORITY
1832
- };
1833
- element = viewWriter.createAttributeElement(viewElementDefinition.name, attributes, options);
1834
- }
1835
- else {
1836
- // 'ui'.
1837
- element = viewWriter.createUIElement(viewElementDefinition.name, attributes);
1838
- }
1839
- if (viewElementDefinition.styles) {
1840
- const keys = Object.keys(viewElementDefinition.styles);
1841
- for (const key of keys) {
1842
- viewWriter.setStyle(key, viewElementDefinition.styles[key], element);
1843
- }
1844
- }
1845
- if (viewElementDefinition.classes) {
1846
- const classes = viewElementDefinition.classes;
1847
- if (typeof classes == 'string') {
1848
- viewWriter.addClass(classes, element);
1849
- }
1850
- else {
1851
- for (const className of classes) {
1852
- viewWriter.addClass(className, element);
1853
- }
1854
- }
1855
- }
1856
- return element;
1857
- }
1858
- function getFromAttributeCreator(config) {
1859
- if (config.model.values) {
1860
- return ((modelAttributeValue, conversionApi, data) => {
1861
- const view = config.view[modelAttributeValue];
1862
- if (view) {
1863
- return view(modelAttributeValue, conversionApi, data);
1864
- }
1865
- return null;
1866
- });
1867
- }
1868
- else {
1869
- return config.view;
1870
- }
1871
- }
1872
- /**
1873
- * Takes the configuration, adds default parameters if they do not exist and normalizes other parameters to be used in downcast converters
1874
- * for generating a view attribute.
1875
- *
1876
- * @param view View configuration.
1877
- */
1878
- function normalizeToAttributeConfig(view) {
1879
- if (typeof view == 'string') {
1880
- return modelAttributeValue => ({ key: view, value: modelAttributeValue });
1881
- }
1882
- else if (typeof view == 'object') {
1883
- // { key, value, ... }
1884
- if (view.value) {
1885
- return () => view;
1886
- }
1887
- // { key, ... }
1888
- else {
1889
- return modelAttributeValue => ({ key: view.key, value: modelAttributeValue });
1890
- }
1891
- }
1892
- else {
1893
- // function.
1894
- return view;
1895
- }
1896
- }
1897
- /**
1898
- * Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter.
1899
- */
1900
- function prepareDescriptor(highlightDescriptor, data, conversionApi) {
1901
- // If passed descriptor is a creator function, call it. If not, just use passed value.
1902
- const descriptor = typeof highlightDescriptor == 'function' ?
1903
- highlightDescriptor(data, conversionApi) :
1904
- { ...highlightDescriptor };
1905
- if (!descriptor) {
1906
- return null;
1907
- }
1908
- // Apply default descriptor priority.
1909
- if (!descriptor.priority) {
1910
- descriptor.priority = 10;
1911
- }
1912
- // Default descriptor id is marker name.
1913
- if (!descriptor.id) {
1914
- descriptor.id = data.markerName;
1915
- }
1916
- return descriptor;
1917
- }
1918
- /**
1919
- * Creates a function that checks a single differ diff item whether it should trigger reconversion.
1920
- *
1921
- * @param model A normalized `config.model` converter configuration.
1922
- * @param model.name The name of element.
1923
- * @param model.attributes The list of attribute names that should trigger reconversion.
1924
- * @param model.children Whether the child list change should trigger reconversion.
1925
- */
1926
- function createChangeReducerCallback(model) {
1927
- return (node, change) => {
1928
- if (!node.is('element', model.name)) {
1929
- return false;
1930
- }
1931
- if (change.type == 'attribute') {
1932
- if (model.attributes.includes(change.attributeKey)) {
1933
- return true;
1934
- }
1935
- }
1936
- else {
1937
- /* istanbul ignore else: This is always true because otherwise it would not register a reducer callback. -- @preserve */
1938
- if (model.children) {
1939
- return true;
1940
- }
1941
- }
1942
- return false;
1943
- };
1944
- }
1945
- /**
1946
- * Creates a `reduceChanges` event handler for reconversion.
1947
- *
1948
- * @param model A normalized `config.model` converter configuration.
1949
- * @param model.name The name of element.
1950
- * @param model.attributes The list of attribute names that should trigger reconversion.
1951
- * @param model.children Whether the child list change should trigger reconversion.
1952
- */
1953
- function createChangeReducer(model) {
1954
- const shouldReplace = createChangeReducerCallback(model);
1955
- return (evt, data) => {
1956
- const reducedChanges = [];
1957
- if (!data.reconvertedElements) {
1958
- data.reconvertedElements = new Set();
1959
- }
1960
- for (const change of data.changes) {
1961
- // For attribute use node affected by the change.
1962
- // For insert or remove use parent element because we need to check if it's added/removed child.
1963
- const node = change.type == 'attribute' ? change.range.start.nodeAfter : change.position.parent;
1964
- if (!node || !shouldReplace(node, change) || change.type == 'reinsert') {
1965
- reducedChanges.push(change);
1966
- continue;
1967
- }
1968
- // Force to not-reuse view elements renamed in model.
1969
- if (change.type == 'insert' && change.action == 'rename') {
1970
- data.refreshedItems.add(change.position.nodeAfter);
1971
- }
1972
- // If it's already marked for reconversion, so skip this change, otherwise add the diff items.
1973
- if (!data.reconvertedElements.has(node)) {
1974
- data.reconvertedElements.add(node);
1975
- const position = ModelPosition._createBefore(node);
1976
- let changeIndex = reducedChanges.length;
1977
- // We need to insert remove+reinsert before any other change on and inside the re-converted element.
1978
- // This is important because otherwise we would remove element that had already been modified by the previous change.
1979
- // Note that there could be some element removed before the re-converted element, so we must not break this behavior.
1980
- for (let i = reducedChanges.length - 1; i >= 0; i--) {
1981
- const change = reducedChanges[i];
1982
- const changePosition = change.type == 'attribute' ? change.range.start : change.position;
1983
- const positionRelation = changePosition.compareWith(position);
1984
- if (positionRelation == 'before' || change.type == 'remove' && positionRelation == 'same') {
1985
- break;
1986
- }
1987
- changeIndex = i;
1988
- }
1989
- reducedChanges.splice(changeIndex, 0, {
1990
- type: 'reinsert',
1991
- name: node.name,
1992
- position,
1993
- length: 1
1994
- });
1995
- }
1996
- }
1997
- data.changes = reducedChanges;
1998
- };
1999
- }
2000
- /**
2001
- * Creates a function that checks if an element and its watched attributes can be consumed and consumes them.
2002
- *
2003
- * @param model A normalized `config.model` converter configuration.
2004
- * @param model.name The name of element.
2005
- * @param model.attributes The list of attribute names that should trigger reconversion.
2006
- * @param model.children Whether the child list change should trigger reconversion.
2007
- */
2008
- function createConsumer(model) {
2009
- return (node, consumable, options = {}) => {
2010
- const events = ['insert'];
2011
- // Collect all set attributes that are triggering conversion.
2012
- for (const attributeName of model.attributes) {
2013
- if (node.hasAttribute(attributeName)) {
2014
- events.push(`attribute:${attributeName}`);
2015
- }
2016
- }
2017
- if (!events.every(event => consumable.test(node, event))) {
2018
- return false;
2019
- }
2020
- if (!options.preflight) {
2021
- events.forEach(event => consumable.consume(node, event));
2022
- }
2023
- return true;
2024
- };
2025
- }
2026
- /**
2027
- * Creates a function that creates view slots.
2028
- *
2029
- * @returns Function exposed by the writer as `createSlot()`.
2030
- */
2031
- function createSlotFactory(element, slotsMap, conversionApi) {
2032
- return (writer, modeOrFilter) => {
2033
- const slot = writer.createContainerElement('$slot');
2034
- let children = null;
2035
- if (modeOrFilter === 'children') {
2036
- children = Array.from(element.getChildren());
2037
- }
2038
- else if (typeof modeOrFilter == 'function') {
2039
- children = Array.from(element.getChildren()).filter(element => modeOrFilter(element));
2040
- }
2041
- else {
2042
- /**
2043
- * Unknown slot mode was provided to `writer.createSlot()` in the downcast converter.
2044
- *
2045
- * @error conversion-slot-mode-unknown
2046
- * @param {never} modeOrFilter The specified mode or filter.
2047
- */
2048
- throw new CKEditorError('conversion-slot-mode-unknown', conversionApi.dispatcher, { modeOrFilter });
2049
- }
2050
- slotsMap.set(slot, children);
2051
- return slot;
2052
- };
2053
- }
2054
- /**
2055
- * Checks if all children are covered by slots and there is no child that landed in multiple slots.
2056
- */
2057
- function validateSlotsChildren(element, slotsMap, conversionApi) {
2058
- const childrenInSlots = Array.from(slotsMap.values()).flat();
2059
- const uniqueChildrenInSlots = new Set(childrenInSlots);
2060
- if (uniqueChildrenInSlots.size != childrenInSlots.length) {
2061
- /**
2062
- * Filters provided to `writer.createSlot()` overlap (at least two filters accept the same child element).
2063
- *
2064
- * @error conversion-slot-filter-overlap
2065
- * @param {module:engine/model/element~ModelElement} element The element of which children would not be properly
2066
- * allocated to multiple slots.
2067
- */
2068
- throw new CKEditorError('conversion-slot-filter-overlap', conversionApi.dispatcher, { element });
2069
- }
2070
- if (uniqueChildrenInSlots.size != element.childCount) {
2071
- /**
2072
- * Filters provided to `writer.createSlot()` are incomplete and exclude at least one children element (one of
2073
- * the children elements would not be assigned to any of the slots).
2074
- *
2075
- * @error conversion-slot-filter-incomplete
2076
- * @param {module:engine/model/element~ModelElement} element The element of which children would not be properly
2077
- * allocated to multiple slots.
2078
- */
2079
- throw new CKEditorError('conversion-slot-filter-incomplete', conversionApi.dispatcher, { element });
2080
- }
2081
- }
2082
- /**
2083
- * Fill slots with appropriate view elements.
2084
- */
2085
- function fillSlots(viewElement, slotsMap, conversionApi, options) {
2086
- // Set temporary position mapping to redirect child view elements into a proper slots.
2087
- conversionApi.mapper.on('modelToViewPosition', toViewPositionMapping, { priority: 'highest' });
2088
- let currentSlot = null;
2089
- let currentSlotNodes = null;
2090
- // Fill slots with nested view nodes.
2091
- for ([currentSlot, currentSlotNodes] of slotsMap) {
2092
- reinsertOrConvertNodes(viewElement, currentSlotNodes, conversionApi, options);
2093
- conversionApi.writer.setCustomProperty('$structureSlotParent', true, currentSlot.parent);
2094
- conversionApi.writer.move(conversionApi.writer.createRangeIn(currentSlot), conversionApi.writer.createPositionBefore(currentSlot));
2095
- conversionApi.writer.remove(currentSlot);
2096
- }
2097
- conversionApi.mapper.off('modelToViewPosition', toViewPositionMapping);
2098
- function toViewPositionMapping(evt, data) {
2099
- const element = data.modelPosition.nodeAfter;
2100
- // Find the proper offset within the slot.
2101
- const index = currentSlotNodes.indexOf(element);
2102
- if (index < 0) {
2103
- return;
2104
- }
2105
- data.viewPosition = data.mapper.findPositionIn(currentSlot, index);
2106
- }
2107
- }
2108
- /**
2109
- * Inserts view representation of `nodes` into the `viewElement` either by bringing back just removed view nodes
2110
- * or by triggering conversion for them.
2111
- */
2112
- function reinsertOrConvertNodes(viewElement, modelNodes, conversionApi, options) {
2113
- // Fill with nested view nodes.
2114
- for (const modelChildNode of modelNodes) {
2115
- // Try reinserting the view node for the specified model node...
2116
- if (!reinsertNode(viewElement.root, modelChildNode, conversionApi, options)) {
2117
- // ...or else convert the model element to the view.
2118
- conversionApi.convertItem(modelChildNode);
2119
- }
2120
- }
2121
- }
2122
- /**
2123
- * Checks if the view for the given model element could be reused and reinserts it to the view.
2124
- *
2125
- * @returns `false` if view element can't be reused.
2126
- */
2127
- function reinsertNode(viewRoot, modelNode, conversionApi, options) {
2128
- const { writer, mapper } = conversionApi;
2129
- // Don't reinsert if this is not a reconversion...
2130
- if (!options.reconversion) {
2131
- return false;
2132
- }
2133
- const viewChildNode = mapper.toViewElement(modelNode);
2134
- // ...or there is no view to reinsert or it was already inserted to the view structure...
2135
- if (!viewChildNode || viewChildNode.root == viewRoot) {
2136
- return false;
2137
- }
2138
- // ...or it was strictly marked as not to be reused.
2139
- if (!conversionApi.canReuseView(viewChildNode)) {
2140
- return false;
2141
- }
2142
- // Otherwise reinsert the view node.
2143
- writer.move(writer.createRangeOn(viewChildNode), mapper.toViewPosition(ModelPosition._createBefore(modelNode)));
2144
- return true;
2145
- }
2146
- /**
2147
- * The default consumer for insert events.
2148
- *
2149
- * @param item Model item.
2150
- * @param consumable The model consumable.
2151
- * @param options.preflight Whether should consume or just check if can be consumed.
2152
- */
2153
- function defaultConsumer(item, consumable, { preflight } = {}) {
2154
- if (preflight) {
2155
- return consumable.test(item, 'insert');
2156
- }
2157
- else {
2158
- return consumable.consume(item, 'insert');
2159
- }
2160
- }