@ckeditor/ckeditor5-engine 39.0.2 → 40.0.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 (241) hide show
  1. package/package.json +2 -2
  2. package/src/controller/datacontroller.d.ts +334 -334
  3. package/src/controller/datacontroller.js +481 -481
  4. package/src/controller/editingcontroller.d.ts +98 -98
  5. package/src/controller/editingcontroller.js +191 -191
  6. package/src/conversion/conversion.d.ts +478 -478
  7. package/src/conversion/conversion.js +601 -601
  8. package/src/conversion/conversionhelpers.d.ts +26 -26
  9. package/src/conversion/conversionhelpers.js +32 -32
  10. package/src/conversion/downcastdispatcher.d.ts +562 -562
  11. package/src/conversion/downcastdispatcher.js +547 -547
  12. package/src/conversion/downcasthelpers.d.ts +1226 -1226
  13. package/src/conversion/downcasthelpers.js +2183 -2183
  14. package/src/conversion/mapper.d.ts +503 -503
  15. package/src/conversion/mapper.js +536 -536
  16. package/src/conversion/modelconsumable.d.ts +201 -201
  17. package/src/conversion/modelconsumable.js +333 -333
  18. package/src/conversion/upcastdispatcher.d.ts +492 -492
  19. package/src/conversion/upcastdispatcher.js +460 -460
  20. package/src/conversion/upcasthelpers.d.ts +499 -499
  21. package/src/conversion/upcasthelpers.js +950 -950
  22. package/src/conversion/viewconsumable.d.ts +369 -369
  23. package/src/conversion/viewconsumable.js +532 -532
  24. package/src/dataprocessor/basichtmlwriter.d.ts +18 -18
  25. package/src/dataprocessor/basichtmlwriter.js +19 -19
  26. package/src/dataprocessor/dataprocessor.d.ts +61 -61
  27. package/src/dataprocessor/dataprocessor.js +5 -5
  28. package/src/dataprocessor/htmldataprocessor.d.ts +76 -76
  29. package/src/dataprocessor/htmldataprocessor.js +96 -96
  30. package/src/dataprocessor/htmlwriter.d.ts +16 -16
  31. package/src/dataprocessor/htmlwriter.js +5 -5
  32. package/src/dataprocessor/xmldataprocessor.d.ts +90 -90
  33. package/src/dataprocessor/xmldataprocessor.js +108 -108
  34. package/src/dev-utils/model.d.ts +124 -124
  35. package/src/dev-utils/model.js +395 -395
  36. package/src/dev-utils/operationreplayer.d.ts +51 -51
  37. package/src/dev-utils/operationreplayer.js +112 -112
  38. package/src/dev-utils/utils.d.ts +37 -37
  39. package/src/dev-utils/utils.js +73 -73
  40. package/src/dev-utils/view.d.ts +319 -319
  41. package/src/dev-utils/view.js +967 -967
  42. package/src/index.d.ts +114 -114
  43. package/src/index.js +78 -78
  44. package/src/model/batch.d.ts +106 -106
  45. package/src/model/batch.js +96 -96
  46. package/src/model/differ.d.ts +387 -387
  47. package/src/model/differ.js +1149 -1149
  48. package/src/model/document.d.ts +272 -272
  49. package/src/model/document.js +361 -361
  50. package/src/model/documentfragment.d.ts +200 -200
  51. package/src/model/documentfragment.js +306 -306
  52. package/src/model/documentselection.d.ts +420 -420
  53. package/src/model/documentselection.js +993 -993
  54. package/src/model/element.d.ts +165 -165
  55. package/src/model/element.js +281 -281
  56. package/src/model/history.d.ts +114 -114
  57. package/src/model/history.js +207 -207
  58. package/src/model/item.d.ts +14 -14
  59. package/src/model/item.js +5 -5
  60. package/src/model/liveposition.d.ts +77 -77
  61. package/src/model/liveposition.js +93 -93
  62. package/src/model/liverange.d.ts +102 -102
  63. package/src/model/liverange.js +120 -120
  64. package/src/model/markercollection.d.ts +335 -335
  65. package/src/model/markercollection.js +403 -403
  66. package/src/model/model.d.ts +919 -919
  67. package/src/model/model.js +842 -842
  68. package/src/model/node.d.ts +256 -256
  69. package/src/model/node.js +375 -375
  70. package/src/model/nodelist.d.ts +91 -91
  71. package/src/model/nodelist.js +163 -163
  72. package/src/model/operation/attributeoperation.d.ts +103 -103
  73. package/src/model/operation/attributeoperation.js +148 -148
  74. package/src/model/operation/detachoperation.d.ts +60 -60
  75. package/src/model/operation/detachoperation.js +77 -77
  76. package/src/model/operation/insertoperation.d.ts +90 -90
  77. package/src/model/operation/insertoperation.js +135 -135
  78. package/src/model/operation/markeroperation.d.ts +91 -91
  79. package/src/model/operation/markeroperation.js +107 -107
  80. package/src/model/operation/mergeoperation.d.ts +100 -100
  81. package/src/model/operation/mergeoperation.js +167 -167
  82. package/src/model/operation/moveoperation.d.ts +96 -96
  83. package/src/model/operation/moveoperation.js +164 -164
  84. package/src/model/operation/nooperation.d.ts +38 -38
  85. package/src/model/operation/nooperation.js +48 -48
  86. package/src/model/operation/operation.d.ts +96 -96
  87. package/src/model/operation/operation.js +62 -62
  88. package/src/model/operation/operationfactory.d.ts +18 -18
  89. package/src/model/operation/operationfactory.js +44 -44
  90. package/src/model/operation/renameoperation.d.ts +83 -83
  91. package/src/model/operation/renameoperation.js +115 -115
  92. package/src/model/operation/rootattributeoperation.d.ts +98 -98
  93. package/src/model/operation/rootattributeoperation.js +155 -155
  94. package/src/model/operation/rootoperation.d.ts +76 -76
  95. package/src/model/operation/rootoperation.js +90 -90
  96. package/src/model/operation/splitoperation.d.ts +109 -109
  97. package/src/model/operation/splitoperation.js +194 -194
  98. package/src/model/operation/transform.d.ts +100 -100
  99. package/src/model/operation/transform.js +1985 -1985
  100. package/src/model/operation/utils.d.ts +71 -71
  101. package/src/model/operation/utils.js +213 -213
  102. package/src/model/position.d.ts +539 -539
  103. package/src/model/position.js +979 -979
  104. package/src/model/range.d.ts +458 -458
  105. package/src/model/range.js +875 -875
  106. package/src/model/rootelement.d.ts +60 -60
  107. package/src/model/rootelement.js +74 -74
  108. package/src/model/schema.d.ts +1186 -1186
  109. package/src/model/schema.js +1242 -1242
  110. package/src/model/selection.d.ts +482 -482
  111. package/src/model/selection.js +789 -789
  112. package/src/model/text.d.ts +66 -66
  113. package/src/model/text.js +85 -85
  114. package/src/model/textproxy.d.ts +144 -144
  115. package/src/model/textproxy.js +189 -189
  116. package/src/model/treewalker.d.ts +186 -186
  117. package/src/model/treewalker.js +244 -244
  118. package/src/model/typecheckable.d.ts +285 -285
  119. package/src/model/typecheckable.js +16 -16
  120. package/src/model/utils/autoparagraphing.d.ts +37 -37
  121. package/src/model/utils/autoparagraphing.js +63 -63
  122. package/src/model/utils/deletecontent.d.ts +58 -58
  123. package/src/model/utils/deletecontent.js +488 -488
  124. package/src/model/utils/findoptimalinsertionrange.d.ts +32 -32
  125. package/src/model/utils/findoptimalinsertionrange.js +57 -57
  126. package/src/model/utils/getselectedcontent.d.ts +30 -30
  127. package/src/model/utils/getselectedcontent.js +125 -125
  128. package/src/model/utils/insertcontent.d.ts +46 -46
  129. package/src/model/utils/insertcontent.js +705 -705
  130. package/src/model/utils/insertobject.d.ts +44 -44
  131. package/src/model/utils/insertobject.js +139 -139
  132. package/src/model/utils/modifyselection.d.ts +48 -48
  133. package/src/model/utils/modifyselection.js +186 -186
  134. package/src/model/utils/selection-post-fixer.d.ts +74 -74
  135. package/src/model/utils/selection-post-fixer.js +260 -260
  136. package/src/model/writer.d.ts +851 -851
  137. package/src/model/writer.js +1306 -1306
  138. package/src/view/attributeelement.d.ts +108 -108
  139. package/src/view/attributeelement.js +184 -184
  140. package/src/view/containerelement.d.ts +49 -49
  141. package/src/view/containerelement.js +80 -80
  142. package/src/view/datatransfer.d.ts +79 -79
  143. package/src/view/datatransfer.js +98 -98
  144. package/src/view/document.d.ts +184 -184
  145. package/src/view/document.js +120 -120
  146. package/src/view/documentfragment.d.ts +149 -149
  147. package/src/view/documentfragment.js +228 -228
  148. package/src/view/documentselection.d.ts +306 -306
  149. package/src/view/documentselection.js +256 -256
  150. package/src/view/domconverter.d.ts +640 -640
  151. package/src/view/domconverter.js +1450 -1425
  152. package/src/view/downcastwriter.d.ts +996 -996
  153. package/src/view/downcastwriter.js +1696 -1696
  154. package/src/view/editableelement.d.ts +62 -62
  155. package/src/view/editableelement.js +62 -62
  156. package/src/view/element.d.ts +468 -468
  157. package/src/view/element.js +724 -724
  158. package/src/view/elementdefinition.d.ts +87 -87
  159. package/src/view/elementdefinition.js +5 -5
  160. package/src/view/emptyelement.d.ts +41 -41
  161. package/src/view/emptyelement.js +73 -73
  162. package/src/view/filler.d.ts +111 -111
  163. package/src/view/filler.js +150 -150
  164. package/src/view/item.d.ts +14 -14
  165. package/src/view/item.js +5 -5
  166. package/src/view/matcher.d.ts +486 -486
  167. package/src/view/matcher.js +507 -507
  168. package/src/view/node.d.ts +163 -163
  169. package/src/view/node.js +228 -228
  170. package/src/view/observer/arrowkeysobserver.d.ts +45 -45
  171. package/src/view/observer/arrowkeysobserver.js +40 -40
  172. package/src/view/observer/bubblingemittermixin.d.ts +166 -166
  173. package/src/view/observer/bubblingemittermixin.js +172 -172
  174. package/src/view/observer/bubblingeventinfo.d.ts +47 -47
  175. package/src/view/observer/bubblingeventinfo.js +37 -37
  176. package/src/view/observer/clickobserver.d.ts +43 -43
  177. package/src/view/observer/clickobserver.js +29 -29
  178. package/src/view/observer/compositionobserver.d.ts +82 -82
  179. package/src/view/observer/compositionobserver.js +60 -60
  180. package/src/view/observer/domeventdata.d.ts +50 -50
  181. package/src/view/observer/domeventdata.js +47 -47
  182. package/src/view/observer/domeventobserver.d.ts +73 -73
  183. package/src/view/observer/domeventobserver.js +79 -79
  184. package/src/view/observer/fakeselectionobserver.d.ts +47 -47
  185. package/src/view/observer/fakeselectionobserver.js +91 -91
  186. package/src/view/observer/focusobserver.d.ts +82 -82
  187. package/src/view/observer/focusobserver.js +86 -86
  188. package/src/view/observer/inputobserver.d.ts +86 -86
  189. package/src/view/observer/inputobserver.js +164 -164
  190. package/src/view/observer/keyobserver.d.ts +66 -66
  191. package/src/view/observer/keyobserver.js +39 -39
  192. package/src/view/observer/mouseobserver.d.ts +89 -89
  193. package/src/view/observer/mouseobserver.js +29 -29
  194. package/src/view/observer/mutationobserver.d.ts +86 -86
  195. package/src/view/observer/mutationobserver.js +206 -206
  196. package/src/view/observer/observer.d.ts +89 -89
  197. package/src/view/observer/observer.js +84 -84
  198. package/src/view/observer/selectionobserver.d.ts +148 -148
  199. package/src/view/observer/selectionobserver.js +202 -202
  200. package/src/view/observer/tabobserver.d.ts +46 -46
  201. package/src/view/observer/tabobserver.js +42 -42
  202. package/src/view/placeholder.d.ts +96 -96
  203. package/src/view/placeholder.js +267 -267
  204. package/src/view/position.d.ts +189 -189
  205. package/src/view/position.js +324 -324
  206. package/src/view/range.d.ts +279 -279
  207. package/src/view/range.js +430 -430
  208. package/src/view/rawelement.d.ts +73 -73
  209. package/src/view/rawelement.js +105 -105
  210. package/src/view/renderer.d.ts +265 -265
  211. package/src/view/renderer.js +999 -999
  212. package/src/view/rooteditableelement.d.ts +41 -41
  213. package/src/view/rooteditableelement.js +69 -69
  214. package/src/view/selection.d.ts +375 -375
  215. package/src/view/selection.js +559 -559
  216. package/src/view/styles/background.d.ts +33 -33
  217. package/src/view/styles/background.js +74 -74
  218. package/src/view/styles/border.d.ts +43 -43
  219. package/src/view/styles/border.js +316 -316
  220. package/src/view/styles/margin.d.ts +29 -29
  221. package/src/view/styles/margin.js +34 -34
  222. package/src/view/styles/padding.d.ts +29 -29
  223. package/src/view/styles/padding.js +34 -34
  224. package/src/view/styles/utils.d.ts +93 -93
  225. package/src/view/styles/utils.js +219 -219
  226. package/src/view/stylesmap.d.ts +675 -675
  227. package/src/view/stylesmap.js +766 -766
  228. package/src/view/text.d.ts +74 -74
  229. package/src/view/text.js +93 -93
  230. package/src/view/textproxy.d.ts +97 -97
  231. package/src/view/textproxy.js +124 -124
  232. package/src/view/treewalker.d.ts +195 -195
  233. package/src/view/treewalker.js +327 -327
  234. package/src/view/typecheckable.d.ts +448 -448
  235. package/src/view/typecheckable.js +19 -19
  236. package/src/view/uielement.d.ts +96 -96
  237. package/src/view/uielement.js +182 -182
  238. package/src/view/upcastwriter.d.ts +417 -417
  239. package/src/view/upcastwriter.js +359 -359
  240. package/src/view/view.d.ts +487 -487
  241. package/src/view/view.js +546 -546
@@ -1,1425 +1,1450 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
- */
5
- /**
6
- * @module engine/view/domconverter
7
- */
8
- /* globals Node, NodeFilter, DOMParser, Text */
9
- import ViewText from './text';
10
- import ViewElement from './element';
11
- import ViewUIElement from './uielement';
12
- import ViewPosition from './position';
13
- import ViewRange from './range';
14
- import ViewSelection from './selection';
15
- import ViewDocumentFragment from './documentfragment';
16
- import ViewTreeWalker from './treewalker';
17
- import { default as Matcher } from './matcher';
18
- import { BR_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER, MARKED_NBSP_FILLER, getDataWithoutFiller, isInlineFiller, startsWithFiller } from './filler';
19
- import { global, logWarning, indexOf, getAncestors, isText, isComment, isValidAttributeName, first } from '@ckeditor/ckeditor5-utils';
20
- const BR_FILLER_REF = BR_FILLER(global.document); // eslint-disable-line new-cap
21
- const NBSP_FILLER_REF = NBSP_FILLER(global.document); // eslint-disable-line new-cap
22
- const MARKED_NBSP_FILLER_REF = MARKED_NBSP_FILLER(global.document); // eslint-disable-line new-cap
23
- const UNSAFE_ATTRIBUTE_NAME_PREFIX = 'data-ck-unsafe-attribute-';
24
- const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
25
- /**
26
- * `DomConverter` is a set of tools to do transformations between DOM nodes and view nodes. It also handles
27
- * {@link module:engine/view/domconverter~DomConverter#bindElements bindings} between these nodes.
28
- *
29
- * An instance of the DOM converter is available under
30
- * {@link module:engine/view/view~View#domConverter `editor.editing.view.domConverter`}.
31
- *
32
- * The DOM converter does not check which nodes should be rendered (use {@link module:engine/view/renderer~Renderer}), does not keep the
33
- * state of a tree nor keeps the synchronization between the tree view and the DOM tree (use {@link module:engine/view/document~Document}).
34
- *
35
- * The DOM converter keeps DOM elements to view element bindings, so when the converter gets destroyed, the bindings are lost.
36
- * Two converters will keep separate binding maps, so one tree view can be bound with two DOM trees.
37
- */
38
- export default class DomConverter {
39
- /**
40
- * Creates a DOM converter.
41
- *
42
- * @param document The view document instance.
43
- * @param options An object with configuration options.
44
- * @param options.blockFillerMode The type of the block filler to use.
45
- * Default value depends on the options.renderingMode:
46
- * 'nbsp' when options.renderingMode == 'data',
47
- * 'br' when options.renderingMode == 'editing'.
48
- * @param options.renderingMode Whether to leave the View-to-DOM conversion result unchanged
49
- * or improve editing experience by filtering out interactive data.
50
- */
51
- constructor(document, { blockFillerMode, renderingMode = 'editing' } = {}) {
52
- /**
53
- * The DOM-to-view mapping.
54
- */
55
- this._domToViewMapping = new WeakMap();
56
- /**
57
- * The view-to-DOM mapping.
58
- */
59
- this._viewToDomMapping = new WeakMap();
60
- /**
61
- * Holds the mapping between fake selection containers and corresponding view selections.
62
- */
63
- this._fakeSelectionMapping = new WeakMap();
64
- /**
65
- * Matcher for view elements whose content should be treated as raw data
66
- * and not processed during the conversion from DOM nodes to view elements.
67
- */
68
- this._rawContentElementMatcher = new Matcher();
69
- /**
70
- * Matcher for inline object view elements. This is an extension of a simple {@link #inlineObjectElements} array of element names.
71
- */
72
- this._inlineObjectElementMatcher = new Matcher();
73
- this.document = document;
74
- this.renderingMode = renderingMode;
75
- this.blockFillerMode = blockFillerMode || (renderingMode === 'editing' ? 'br' : 'nbsp');
76
- this.preElements = ['pre'];
77
- this.blockElements = [
78
- 'address', 'article', 'aside', 'blockquote', 'caption', 'center', 'dd', 'details', 'dir', 'div',
79
- 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header',
80
- 'hgroup', 'legend', 'li', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'section', 'summary', 'table', 'tbody',
81
- 'td', 'tfoot', 'th', 'thead', 'tr', 'ul'
82
- ];
83
- this.inlineObjectElements = [
84
- 'object', 'iframe', 'input', 'button', 'textarea', 'select', 'option', 'video', 'embed', 'audio', 'img', 'canvas'
85
- ];
86
- this.unsafeElements = ['script', 'style'];
87
- this._domDocument = this.renderingMode === 'editing' ? global.document : global.document.implementation.createHTMLDocument('');
88
- }
89
- /**
90
- * Binds a given DOM element that represents fake selection to a **position** of a
91
- * {@link module:engine/view/documentselection~DocumentSelection document selection}.
92
- * Document selection copy is stored and can be retrieved by the
93
- * {@link module:engine/view/domconverter~DomConverter#fakeSelectionToView} method.
94
- */
95
- bindFakeSelection(domElement, viewDocumentSelection) {
96
- this._fakeSelectionMapping.set(domElement, new ViewSelection(viewDocumentSelection));
97
- }
98
- /**
99
- * Returns a {@link module:engine/view/selection~Selection view selection} instance corresponding to a given
100
- * DOM element that represents fake selection. Returns `undefined` if binding to the given DOM element does not exist.
101
- */
102
- fakeSelectionToView(domElement) {
103
- return this._fakeSelectionMapping.get(domElement);
104
- }
105
- /**
106
- * Binds DOM and view elements, so it will be possible to get corresponding elements using
107
- * {@link module:engine/view/domconverter~DomConverter#mapDomToView} and
108
- * {@link module:engine/view/domconverter~DomConverter#mapViewToDom}.
109
- *
110
- * @param domElement The DOM element to bind.
111
- * @param viewElement The view element to bind.
112
- */
113
- bindElements(domElement, viewElement) {
114
- this._domToViewMapping.set(domElement, viewElement);
115
- this._viewToDomMapping.set(viewElement, domElement);
116
- }
117
- /**
118
- * Unbinds a given DOM element from the view element it was bound to. Unbinding is deep, meaning that all children of
119
- * the DOM element will be unbound too.
120
- *
121
- * @param domElement The DOM element to unbind.
122
- */
123
- unbindDomElement(domElement) {
124
- const viewElement = this._domToViewMapping.get(domElement);
125
- if (viewElement) {
126
- this._domToViewMapping.delete(domElement);
127
- this._viewToDomMapping.delete(viewElement);
128
- for (const child of Array.from(domElement.children)) {
129
- this.unbindDomElement(child);
130
- }
131
- }
132
- }
133
- /**
134
- * Binds DOM and view document fragments, so it will be possible to get corresponding document fragments using
135
- * {@link module:engine/view/domconverter~DomConverter#mapDomToView} and
136
- * {@link module:engine/view/domconverter~DomConverter#mapViewToDom}.
137
- *
138
- * @param domFragment The DOM document fragment to bind.
139
- * @param viewFragment The view document fragment to bind.
140
- */
141
- bindDocumentFragments(domFragment, viewFragment) {
142
- this._domToViewMapping.set(domFragment, viewFragment);
143
- this._viewToDomMapping.set(viewFragment, domFragment);
144
- }
145
- /**
146
- * Decides whether a given pair of attribute key and value should be passed further down the pipeline.
147
- *
148
- * @param elementName Element name in lower case.
149
- */
150
- shouldRenderAttribute(attributeKey, attributeValue, elementName) {
151
- if (this.renderingMode === 'data') {
152
- return true;
153
- }
154
- attributeKey = attributeKey.toLowerCase();
155
- if (attributeKey.startsWith('on')) {
156
- return false;
157
- }
158
- if (attributeKey === 'srcdoc' &&
159
- attributeValue.match(/\bon\S+\s*=|javascript:|<\s*\/*script/i)) {
160
- return false;
161
- }
162
- if (elementName === 'img' &&
163
- (attributeKey === 'src' || attributeKey === 'srcset')) {
164
- return true;
165
- }
166
- if (elementName === 'source' && attributeKey === 'srcset') {
167
- return true;
168
- }
169
- if (attributeValue.match(/^\s*(javascript:|data:(image\/svg|text\/x?html))/i)) {
170
- return false;
171
- }
172
- return true;
173
- }
174
- /**
175
- * Set `domElement`'s content using provided `html` argument. Apply necessary filtering for the editing pipeline.
176
- *
177
- * @param domElement DOM element that should have `html` set as its content.
178
- * @param html Textual representation of the HTML that will be set on `domElement`.
179
- */
180
- setContentOf(domElement, html) {
181
- // For data pipeline we pass the HTML as-is.
182
- if (this.renderingMode === 'data') {
183
- domElement.innerHTML = html;
184
- return;
185
- }
186
- const document = new DOMParser().parseFromString(html, 'text/html');
187
- const fragment = document.createDocumentFragment();
188
- const bodyChildNodes = document.body.childNodes;
189
- while (bodyChildNodes.length > 0) {
190
- fragment.appendChild(bodyChildNodes[0]);
191
- }
192
- const treeWalker = document.createTreeWalker(fragment, NodeFilter.SHOW_ELEMENT);
193
- const nodes = [];
194
- let currentNode;
195
- // eslint-disable-next-line no-cond-assign
196
- while (currentNode = treeWalker.nextNode()) {
197
- nodes.push(currentNode);
198
- }
199
- for (const currentNode of nodes) {
200
- // Go through nodes to remove those that are prohibited in editing pipeline.
201
- for (const attributeName of currentNode.getAttributeNames()) {
202
- this.setDomElementAttribute(currentNode, attributeName, currentNode.getAttribute(attributeName));
203
- }
204
- const elementName = currentNode.tagName.toLowerCase();
205
- // There are certain nodes, that should be renamed to <span> in editing pipeline.
206
- if (this._shouldRenameElement(elementName)) {
207
- _logUnsafeElement(elementName);
208
- currentNode.replaceWith(this._createReplacementDomElement(elementName, currentNode));
209
- }
210
- }
211
- // Empty the target element.
212
- while (domElement.firstChild) {
213
- domElement.firstChild.remove();
214
- }
215
- domElement.append(fragment);
216
- }
217
- /**
218
- * Converts the view to the DOM. For all text nodes, not bound elements and document fragments new items will
219
- * be created. For bound elements and document fragments the method will return corresponding items.
220
- *
221
- * @param viewNode View node or document fragment to transform.
222
- * @param options Conversion options.
223
- * @param options.bind Determines whether new elements will be bound.
224
- * @param options.withChildren If `false`, node's and document fragment's children will not be converted.
225
- * @returns Converted node or DocumentFragment.
226
- */
227
- viewToDom(viewNode, options = {}) {
228
- if (viewNode.is('$text')) {
229
- const textData = this._processDataFromViewText(viewNode);
230
- return this._domDocument.createTextNode(textData);
231
- }
232
- else {
233
- if (this.mapViewToDom(viewNode)) {
234
- return this.mapViewToDom(viewNode);
235
- }
236
- let domElement;
237
- if (viewNode.is('documentFragment')) {
238
- // Create DOM document fragment.
239
- domElement = this._domDocument.createDocumentFragment();
240
- if (options.bind) {
241
- this.bindDocumentFragments(domElement, viewNode);
242
- }
243
- }
244
- else if (viewNode.is('uiElement')) {
245
- if (viewNode.name === '$comment') {
246
- domElement = this._domDocument.createComment(viewNode.getCustomProperty('$rawContent'));
247
- }
248
- else {
249
- // UIElement has its own render() method (see #799).
250
- domElement = viewNode.render(this._domDocument, this);
251
- }
252
- if (options.bind) {
253
- this.bindElements(domElement, viewNode);
254
- }
255
- return domElement;
256
- }
257
- else {
258
- // Create DOM element.
259
- if (this._shouldRenameElement(viewNode.name)) {
260
- _logUnsafeElement(viewNode.name);
261
- domElement = this._createReplacementDomElement(viewNode.name);
262
- }
263
- else if (viewNode.hasAttribute('xmlns')) {
264
- domElement = this._domDocument.createElementNS(viewNode.getAttribute('xmlns'), viewNode.name);
265
- }
266
- else {
267
- domElement = this._domDocument.createElement(viewNode.name);
268
- }
269
- // RawElement take care of their children in RawElement#render() method which can be customized
270
- // (see https://github.com/ckeditor/ckeditor5/issues/4469).
271
- if (viewNode.is('rawElement')) {
272
- viewNode.render(domElement, this);
273
- }
274
- if (options.bind) {
275
- this.bindElements(domElement, viewNode);
276
- }
277
- // Copy element's attributes.
278
- for (const key of viewNode.getAttributeKeys()) {
279
- this.setDomElementAttribute(domElement, key, viewNode.getAttribute(key), viewNode);
280
- }
281
- }
282
- if (options.withChildren !== false) {
283
- for (const child of this.viewChildrenToDom(viewNode, options)) {
284
- domElement.appendChild(child);
285
- }
286
- }
287
- return domElement;
288
- }
289
- }
290
- /**
291
- * Sets the attribute on a DOM element.
292
- *
293
- * **Note**: To remove the attribute, use {@link #removeDomElementAttribute}.
294
- *
295
- * @param domElement The DOM element the attribute should be set on.
296
- * @param key The name of the attribute.
297
- * @param value The value of the attribute.
298
- * @param relatedViewElement The view element related to the `domElement` (if there is any).
299
- * It helps decide whether the attribute set is unsafe. For instance, view elements created via the
300
- * {@link module:engine/view/downcastwriter~DowncastWriter} methods can allow certain attributes that would normally be filtered out.
301
- */
302
- setDomElementAttribute(domElement, key, value, relatedViewElement) {
303
- const shouldRenderAttribute = this.shouldRenderAttribute(key, value, domElement.tagName.toLowerCase()) ||
304
- relatedViewElement && relatedViewElement.shouldRenderUnsafeAttribute(key);
305
- if (!shouldRenderAttribute) {
306
- logWarning('domconverter-unsafe-attribute-detected', { domElement, key, value });
307
- }
308
- if (!isValidAttributeName(key)) {
309
- /**
310
- * Invalid attribute name was ignored during rendering.
311
- *
312
- * @error domconverter-invalid-attribute-detected
313
- */
314
- logWarning('domconverter-invalid-attribute-detected', { domElement, key, value });
315
- return;
316
- }
317
- // The old value was safe but the new value is unsafe.
318
- if (domElement.hasAttribute(key) && !shouldRenderAttribute) {
319
- domElement.removeAttribute(key);
320
- }
321
- // The old value was unsafe (but prefixed) but the new value will be safe (will be unprefixed).
322
- else if (domElement.hasAttribute(UNSAFE_ATTRIBUTE_NAME_PREFIX + key) && shouldRenderAttribute) {
323
- domElement.removeAttribute(UNSAFE_ATTRIBUTE_NAME_PREFIX + key);
324
- }
325
- // If the attribute should not be rendered, rename it (instead of removing) to give developers some idea of what
326
- // is going on (https://github.com/ckeditor/ckeditor5/issues/10801).
327
- domElement.setAttribute(shouldRenderAttribute ? key : UNSAFE_ATTRIBUTE_NAME_PREFIX + key, value);
328
- }
329
- /**
330
- * Removes an attribute from a DOM element.
331
- *
332
- * **Note**: To set the attribute, use {@link #setDomElementAttribute}.
333
- *
334
- * @param domElement The DOM element the attribute should be removed from.
335
- * @param key The name of the attribute.
336
- */
337
- removeDomElementAttribute(domElement, key) {
338
- // See #_createReplacementDomElement() to learn what this is.
339
- if (key == UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE) {
340
- return;
341
- }
342
- domElement.removeAttribute(key);
343
- // See setDomElementAttribute() to learn what this is.
344
- domElement.removeAttribute(UNSAFE_ATTRIBUTE_NAME_PREFIX + key);
345
- }
346
- /**
347
- * Converts children of the view element to DOM using the
348
- * {@link module:engine/view/domconverter~DomConverter#viewToDom} method.
349
- * Additionally, this method adds block {@link module:engine/view/filler filler} to the list of children, if needed.
350
- *
351
- * @param viewElement Parent view element.
352
- * @param options See {@link module:engine/view/domconverter~DomConverter#viewToDom} options parameter.
353
- * @returns DOM nodes.
354
- */
355
- *viewChildrenToDom(viewElement, options = {}) {
356
- const fillerPositionOffset = viewElement.getFillerOffset && viewElement.getFillerOffset();
357
- let offset = 0;
358
- for (const childView of viewElement.getChildren()) {
359
- if (fillerPositionOffset === offset) {
360
- yield this._getBlockFiller();
361
- }
362
- const transparentRendering = childView.is('element') &&
363
- !!childView.getCustomProperty('dataPipeline:transparentRendering') &&
364
- !first(childView.getAttributes());
365
- if (transparentRendering && this.renderingMode == 'data') {
366
- yield* this.viewChildrenToDom(childView, options);
367
- }
368
- else {
369
- if (transparentRendering) {
370
- /**
371
- * The `dataPipeline:transparentRendering` flag is supported only in the data pipeline.
372
- *
373
- * @error domconverter-transparent-rendering-unsupported-in-editing-pipeline
374
- */
375
- logWarning('domconverter-transparent-rendering-unsupported-in-editing-pipeline', { viewElement: childView });
376
- }
377
- yield this.viewToDom(childView, options);
378
- }
379
- offset++;
380
- }
381
- if (fillerPositionOffset === offset) {
382
- yield this._getBlockFiller();
383
- }
384
- }
385
- /**
386
- * Converts view {@link module:engine/view/range~Range} to DOM range.
387
- * Inline and block {@link module:engine/view/filler fillers} are handled during the conversion.
388
- *
389
- * @param viewRange View range.
390
- * @returns DOM range.
391
- */
392
- viewRangeToDom(viewRange) {
393
- const domStart = this.viewPositionToDom(viewRange.start);
394
- const domEnd = this.viewPositionToDom(viewRange.end);
395
- const domRange = this._domDocument.createRange();
396
- domRange.setStart(domStart.parent, domStart.offset);
397
- domRange.setEnd(domEnd.parent, domEnd.offset);
398
- return domRange;
399
- }
400
- /**
401
- * Converts view {@link module:engine/view/position~Position} to DOM parent and offset.
402
- *
403
- * Inline and block {@link module:engine/view/filler fillers} are handled during the conversion.
404
- * If the converted position is directly before inline filler it is moved inside the filler.
405
- *
406
- * @param viewPosition View position.
407
- * @returns DOM position or `null` if view position could not be converted to DOM.
408
- * DOM position has two properties:
409
- * * `parent` - DOM position parent.
410
- * * `offset` - DOM position offset.
411
- */
412
- viewPositionToDom(viewPosition) {
413
- const viewParent = viewPosition.parent;
414
- if (viewParent.is('$text')) {
415
- const domParent = this.findCorrespondingDomText(viewParent);
416
- if (!domParent) {
417
- // Position is in a view text node that has not been rendered to DOM yet.
418
- return null;
419
- }
420
- let offset = viewPosition.offset;
421
- if (startsWithFiller(domParent)) {
422
- offset += INLINE_FILLER_LENGTH;
423
- }
424
- return { parent: domParent, offset };
425
- }
426
- else {
427
- // viewParent is instance of ViewElement.
428
- let domParent, domBefore, domAfter;
429
- if (viewPosition.offset === 0) {
430
- domParent = this.mapViewToDom(viewParent);
431
- if (!domParent) {
432
- // Position is in a view element that has not been rendered to DOM yet.
433
- return null;
434
- }
435
- domAfter = domParent.childNodes[0];
436
- }
437
- else {
438
- const nodeBefore = viewPosition.nodeBefore;
439
- domBefore = nodeBefore.is('$text') ?
440
- this.findCorrespondingDomText(nodeBefore) :
441
- this.mapViewToDom(nodeBefore);
442
- if (!domBefore) {
443
- // Position is after a view element that has not been rendered to DOM yet.
444
- return null;
445
- }
446
- domParent = domBefore.parentNode;
447
- domAfter = domBefore.nextSibling;
448
- }
449
- // If there is an inline filler at position return position inside the filler. We should never return
450
- // the position before the inline filler.
451
- if (isText(domAfter) && startsWithFiller(domAfter)) {
452
- return { parent: domAfter, offset: INLINE_FILLER_LENGTH };
453
- }
454
- const offset = domBefore ? indexOf(domBefore) + 1 : 0;
455
- return { parent: domParent, offset };
456
- }
457
- }
458
- /**
459
- * Converts DOM to view. For all text nodes, not bound elements and document fragments new items will
460
- * be created. For bound elements and document fragments function will return corresponding items. For
461
- * {@link module:engine/view/filler fillers} `null` will be returned.
462
- * For all DOM elements rendered by {@link module:engine/view/uielement~UIElement} that UIElement will be returned.
463
- *
464
- * @param domNode DOM node or document fragment to transform.
465
- * @param options Conversion options.
466
- * @param options.bind Determines whether new elements will be bound. False by default.
467
- * @param options.withChildren If `true`, node's and document fragment's children will be converted too. True by default.
468
- * @param options.keepOriginalCase If `false`, node's tag name will be converted to lower case. False by default.
469
- * @param options.skipComments If `false`, comment nodes will be converted to `$comment`
470
- * {@link module:engine/view/uielement~UIElement view UI elements}. False by default.
471
- * @returns Converted node or document fragment or `null` if DOM node is a {@link module:engine/view/filler filler}
472
- * or the given node is an empty text node.
473
- */
474
- domToView(domNode, options = {}) {
475
- const inlineNodes = [];
476
- const generator = this._domToView(domNode, options, inlineNodes);
477
- // Get the first yielded value or a returned value.
478
- const node = generator.next().value;
479
- if (!node) {
480
- return null;
481
- }
482
- // Trigger children handling.
483
- generator.next();
484
- // Whitespace cleaning.
485
- this._processDomInlineNodes(null, inlineNodes, options);
486
- // Text not got trimmed to an empty string so there is no result node.
487
- if (node.is('$text') && node.data.length == 0) {
488
- return null;
489
- }
490
- return node;
491
- }
492
- /**
493
- * Converts children of the DOM element to view nodes using
494
- * the {@link module:engine/view/domconverter~DomConverter#domToView} method.
495
- * Additionally this method omits block {@link module:engine/view/filler filler}, if it exists in the DOM parent.
496
- *
497
- * @param domElement Parent DOM element.
498
- * @param options See {@link module:engine/view/domconverter~DomConverter#domToView} options parameter.
499
- * @param inlineNodes An array that will be populated with inline nodes. It's used internally for whitespace processing.
500
- * @returns View nodes.
501
- */
502
- *domChildrenToView(domElement, options = {}, inlineNodes = []) {
503
- for (let i = 0; i < domElement.childNodes.length; i++) {
504
- const domChild = domElement.childNodes[i];
505
- const generator = this._domToView(domChild, options, inlineNodes);
506
- // Get the first yielded value or a returned value.
507
- const viewChild = generator.next().value;
508
- if (viewChild !== null) {
509
- // Whitespace cleaning before entering a block element (between block elements).
510
- if (this._isBlockViewElement(viewChild)) {
511
- this._processDomInlineNodes(domElement, inlineNodes, options);
512
- }
513
- yield viewChild;
514
- // Trigger children handling.
515
- generator.next();
516
- }
517
- }
518
- // Whitespace cleaning before leaving a block element (content of block element).
519
- this._processDomInlineNodes(domElement, inlineNodes, options);
520
- }
521
- /**
522
- * Converts DOM selection to view {@link module:engine/view/selection~Selection}.
523
- * Ranges which cannot be converted will be omitted.
524
- *
525
- * @param domSelection DOM selection.
526
- * @returns View selection.
527
- */
528
- domSelectionToView(domSelection) {
529
- // DOM selection might be placed in fake selection container.
530
- // If container contains fake selection - return corresponding view selection.
531
- if (domSelection.rangeCount === 1) {
532
- let container = domSelection.getRangeAt(0).startContainer;
533
- // The DOM selection might be moved to the text node inside the fake selection container.
534
- if (isText(container)) {
535
- container = container.parentNode;
536
- }
537
- const viewSelection = this.fakeSelectionToView(container);
538
- if (viewSelection) {
539
- return viewSelection;
540
- }
541
- }
542
- const isBackward = this.isDomSelectionBackward(domSelection);
543
- const viewRanges = [];
544
- for (let i = 0; i < domSelection.rangeCount; i++) {
545
- // DOM Range have correct start and end, no matter what is the DOM Selection direction. So we don't have to fix anything.
546
- const domRange = domSelection.getRangeAt(i);
547
- const viewRange = this.domRangeToView(domRange);
548
- if (viewRange) {
549
- viewRanges.push(viewRange);
550
- }
551
- }
552
- return new ViewSelection(viewRanges, { backward: isBackward });
553
- }
554
- /**
555
- * Converts DOM Range to view {@link module:engine/view/range~Range}.
556
- * If the start or end position can not be converted `null` is returned.
557
- *
558
- * @param domRange DOM range.
559
- * @returns View range.
560
- */
561
- domRangeToView(domRange) {
562
- const viewStart = this.domPositionToView(domRange.startContainer, domRange.startOffset);
563
- const viewEnd = this.domPositionToView(domRange.endContainer, domRange.endOffset);
564
- if (viewStart && viewEnd) {
565
- return new ViewRange(viewStart, viewEnd);
566
- }
567
- return null;
568
- }
569
- /**
570
- * Converts DOM parent and offset to view {@link module:engine/view/position~Position}.
571
- *
572
- * If the position is inside a {@link module:engine/view/filler filler} which has no corresponding view node,
573
- * position of the filler will be converted and returned.
574
- *
575
- * If the position is inside DOM element rendered by {@link module:engine/view/uielement~UIElement}
576
- * that position will be converted to view position before that UIElement.
577
- *
578
- * If structures are too different and it is not possible to find corresponding position then `null` will be returned.
579
- *
580
- * @param domParent DOM position parent.
581
- * @param domOffset DOM position offset. You can skip it when converting the inline filler node.
582
- * @returns View position.
583
- */
584
- domPositionToView(domParent, domOffset = 0) {
585
- if (this.isBlockFiller(domParent)) {
586
- return this.domPositionToView(domParent.parentNode, indexOf(domParent));
587
- }
588
- // If position is somewhere inside UIElement or a RawElement - return position before that element.
589
- const viewElement = this.mapDomToView(domParent);
590
- if (viewElement && (viewElement.is('uiElement') || viewElement.is('rawElement'))) {
591
- return ViewPosition._createBefore(viewElement);
592
- }
593
- if (isText(domParent)) {
594
- if (isInlineFiller(domParent)) {
595
- return this.domPositionToView(domParent.parentNode, indexOf(domParent));
596
- }
597
- const viewParent = this.findCorrespondingViewText(domParent);
598
- let offset = domOffset;
599
- if (!viewParent) {
600
- return null;
601
- }
602
- if (startsWithFiller(domParent)) {
603
- offset -= INLINE_FILLER_LENGTH;
604
- offset = offset < 0 ? 0 : offset;
605
- }
606
- return new ViewPosition(viewParent, offset);
607
- }
608
- // domParent instanceof HTMLElement.
609
- else {
610
- if (domOffset === 0) {
611
- const viewParent = this.mapDomToView(domParent);
612
- if (viewParent) {
613
- return new ViewPosition(viewParent, 0);
614
- }
615
- }
616
- else {
617
- const domBefore = domParent.childNodes[domOffset - 1];
618
- // Jump over an inline filler (and also on Firefox jump over a block filler while pressing backspace in an empty paragraph).
619
- if (isText(domBefore) && isInlineFiller(domBefore) || domBefore && this.isBlockFiller(domBefore)) {
620
- return this.domPositionToView(domBefore.parentNode, indexOf(domBefore));
621
- }
622
- const viewBefore = isText(domBefore) ?
623
- this.findCorrespondingViewText(domBefore) :
624
- this.mapDomToView(domBefore);
625
- // TODO #663
626
- if (viewBefore && viewBefore.parent) {
627
- return new ViewPosition(viewBefore.parent, viewBefore.index + 1);
628
- }
629
- }
630
- return null;
631
- }
632
- }
633
- /**
634
- * Returns corresponding view {@link module:engine/view/element~Element Element} or
635
- * {@link module:engine/view/documentfragment~DocumentFragment} for provided DOM element or
636
- * document fragment. If there is no view item {@link module:engine/view/domconverter~DomConverter#bindElements bound}
637
- * to the given DOM - `undefined` is returned.
638
- *
639
- * For all DOM elements rendered by a {@link module:engine/view/uielement~UIElement} or
640
- * a {@link module:engine/view/rawelement~RawElement}, the parent `UIElement` or `RawElement` will be returned.
641
- *
642
- * @param domElementOrDocumentFragment DOM element or document fragment.
643
- * @returns Corresponding view element, document fragment or `undefined` if no element was bound.
644
- */
645
- mapDomToView(domElementOrDocumentFragment) {
646
- const hostElement = this.getHostViewElement(domElementOrDocumentFragment);
647
- return hostElement || this._domToViewMapping.get(domElementOrDocumentFragment);
648
- }
649
- /**
650
- * Finds corresponding text node. Text nodes are not {@link module:engine/view/domconverter~DomConverter#bindElements bound},
651
- * corresponding text node is returned based on the sibling or parent.
652
- *
653
- * If the directly previous sibling is a {@link module:engine/view/domconverter~DomConverter#bindElements bound} element, it is used
654
- * to find the corresponding text node.
655
- *
656
- * If this is a first child in the parent and the parent is a {@link module:engine/view/domconverter~DomConverter#bindElements bound}
657
- * element, it is used to find the corresponding text node.
658
- *
659
- * For all text nodes rendered by a {@link module:engine/view/uielement~UIElement} or
660
- * a {@link module:engine/view/rawelement~RawElement}, the parent `UIElement` or `RawElement` will be returned.
661
- *
662
- * Otherwise `null` is returned.
663
- *
664
- * Note that for the block or inline {@link module:engine/view/filler filler} this method returns `null`.
665
- *
666
- * @param domText DOM text node.
667
- * @returns Corresponding view text node or `null`, if it was not possible to find a corresponding node.
668
- */
669
- findCorrespondingViewText(domText) {
670
- if (isInlineFiller(domText)) {
671
- return null;
672
- }
673
- // If DOM text was rendered by a UIElement or a RawElement - return this parent element.
674
- const hostElement = this.getHostViewElement(domText);
675
- if (hostElement) {
676
- return hostElement;
677
- }
678
- const previousSibling = domText.previousSibling;
679
- // Try to use previous sibling to find the corresponding text node.
680
- if (previousSibling) {
681
- if (!(this.isElement(previousSibling))) {
682
- // The previous is text or comment.
683
- return null;
684
- }
685
- const viewElement = this.mapDomToView(previousSibling);
686
- if (viewElement) {
687
- const nextSibling = viewElement.nextSibling;
688
- // It might be filler which has no corresponding view node.
689
- if (nextSibling instanceof ViewText) {
690
- return nextSibling;
691
- }
692
- else {
693
- return null;
694
- }
695
- }
696
- }
697
- // Try to use parent to find the corresponding text node.
698
- else {
699
- const viewElement = this.mapDomToView(domText.parentNode);
700
- if (viewElement) {
701
- const firstChild = viewElement.getChild(0);
702
- // It might be filler which has no corresponding view node.
703
- if (firstChild instanceof ViewText) {
704
- return firstChild;
705
- }
706
- else {
707
- return null;
708
- }
709
- }
710
- }
711
- return null;
712
- }
713
- mapViewToDom(documentFragmentOrElement) {
714
- return this._viewToDomMapping.get(documentFragmentOrElement);
715
- }
716
- /**
717
- * Finds corresponding text node. Text nodes are not {@link module:engine/view/domconverter~DomConverter#bindElements bound},
718
- * corresponding text node is returned based on the sibling or parent.
719
- *
720
- * If the directly previous sibling is a {@link module:engine/view/domconverter~DomConverter#bindElements bound} element, it is used
721
- * to find the corresponding text node.
722
- *
723
- * If this is a first child in the parent and the parent is a {@link module:engine/view/domconverter~DomConverter#bindElements bound}
724
- * element, it is used to find the corresponding text node.
725
- *
726
- * Otherwise `null` is returned.
727
- *
728
- * @param viewText View text node.
729
- * @returns Corresponding DOM text node or `null`, if it was not possible to find a corresponding node.
730
- */
731
- findCorrespondingDomText(viewText) {
732
- const previousSibling = viewText.previousSibling;
733
- // Try to use previous sibling to find the corresponding text node.
734
- if (previousSibling && this.mapViewToDom(previousSibling)) {
735
- return this.mapViewToDom(previousSibling).nextSibling;
736
- }
737
- // If this is a first node, try to use parent to find the corresponding text node.
738
- if (!previousSibling && viewText.parent && this.mapViewToDom(viewText.parent)) {
739
- return this.mapViewToDom(viewText.parent).childNodes[0];
740
- }
741
- return null;
742
- }
743
- /**
744
- * Focuses DOM editable that is corresponding to provided {@link module:engine/view/editableelement~EditableElement}.
745
- */
746
- focus(viewEditable) {
747
- const domEditable = this.mapViewToDom(viewEditable);
748
- if (domEditable && domEditable.ownerDocument.activeElement !== domEditable) {
749
- // Save the scrollX and scrollY positions before the focus.
750
- const { scrollX, scrollY } = global.window;
751
- const scrollPositions = [];
752
- // Save all scrollLeft and scrollTop values starting from domEditable up to
753
- // document#documentElement.
754
- forEachDomElementAncestor(domEditable, node => {
755
- const { scrollLeft, scrollTop } = node;
756
- scrollPositions.push([scrollLeft, scrollTop]);
757
- });
758
- domEditable.focus();
759
- // Restore scrollLeft and scrollTop values starting from domEditable up to
760
- // document#documentElement.
761
- // https://github.com/ckeditor/ckeditor5-engine/issues/951
762
- // https://github.com/ckeditor/ckeditor5-engine/issues/957
763
- forEachDomElementAncestor(domEditable, node => {
764
- const [scrollLeft, scrollTop] = scrollPositions.shift();
765
- node.scrollLeft = scrollLeft;
766
- node.scrollTop = scrollTop;
767
- });
768
- // Restore the scrollX and scrollY positions after the focus.
769
- // https://github.com/ckeditor/ckeditor5-engine/issues/951
770
- global.window.scrollTo(scrollX, scrollY);
771
- }
772
- }
773
- /**
774
- * Remove DOM selection from blurred editable, so it won't interfere with clicking on dropdowns (especially on iOS).
775
- *
776
- * @internal
777
- */
778
- _clearDomSelection() {
779
- const domEditable = this.mapViewToDom(this.document.selection.editableElement);
780
- if (!domEditable) {
781
- return;
782
- }
783
- // Check if DOM selection is inside editor editable element.
784
- const domSelection = domEditable.ownerDocument.defaultView.getSelection();
785
- const newViewSelection = this.domSelectionToView(domSelection);
786
- const selectionInEditable = newViewSelection && newViewSelection.rangeCount > 0;
787
- if (selectionInEditable) {
788
- domSelection.removeAllRanges();
789
- }
790
- }
791
- /**
792
- * Returns `true` when `node.nodeType` equals `Node.ELEMENT_NODE`.
793
- *
794
- * @param node Node to check.
795
- */
796
- isElement(node) {
797
- return node && node.nodeType == Node.ELEMENT_NODE;
798
- }
799
- /**
800
- * Returns `true` when `node.nodeType` equals `Node.DOCUMENT_FRAGMENT_NODE`.
801
- *
802
- * @param node Node to check.
803
- */
804
- isDocumentFragment(node) {
805
- return node && node.nodeType == Node.DOCUMENT_FRAGMENT_NODE;
806
- }
807
- /**
808
- * Checks if the node is an instance of the block filler for this DOM converter.
809
- *
810
- * ```ts
811
- * const converter = new DomConverter( viewDocument, { blockFillerMode: 'br' } );
812
- *
813
- * converter.isBlockFiller( BR_FILLER( document ) ); // true
814
- * converter.isBlockFiller( NBSP_FILLER( document ) ); // false
815
- * ```
816
- *
817
- * **Note:**: For the `'nbsp'` mode the method also checks context of a node so it cannot be a detached node.
818
- *
819
- * **Note:** A special case in the `'nbsp'` mode exists where the `<br>` in `<p><br></p>` is treated as a block filler.
820
- *
821
- * @param domNode DOM node to check.
822
- * @returns True if a node is considered a block filler for given mode.
823
- */
824
- isBlockFiller(domNode) {
825
- if (this.blockFillerMode == 'br') {
826
- return domNode.isEqualNode(BR_FILLER_REF);
827
- }
828
- // Special case for <p><br></p> in which <br> should be treated as filler even when we are not in the 'br' mode. See ckeditor5#5564.
829
- if (domNode.tagName === 'BR' &&
830
- hasBlockParent(domNode, this.blockElements) &&
831
- domNode.parentNode.childNodes.length === 1) {
832
- return true;
833
- }
834
- // If not in 'br' mode, try recognizing both marked and regular nbsp block fillers.
835
- return domNode.isEqualNode(MARKED_NBSP_FILLER_REF) || isNbspBlockFiller(domNode, this.blockElements);
836
- }
837
- /**
838
- * Returns `true` if given selection is a backward selection, that is, if it's `focus` is before `anchor`.
839
- *
840
- * @param DOM Selection instance to check.
841
- */
842
- isDomSelectionBackward(selection) {
843
- if (selection.isCollapsed) {
844
- return false;
845
- }
846
- // Since it takes multiple lines of code to check whether a "DOM Position" is before/after another "DOM Position",
847
- // we will use the fact that range will collapse if it's end is before it's start.
848
- const range = this._domDocument.createRange();
849
- try {
850
- range.setStart(selection.anchorNode, selection.anchorOffset);
851
- range.setEnd(selection.focusNode, selection.focusOffset);
852
- }
853
- catch (e) {
854
- // Safari sometimes gives us a selection that makes Range.set{Start,End} throw.
855
- // See https://github.com/ckeditor/ckeditor5/issues/12375.
856
- return false;
857
- }
858
- const backward = range.collapsed;
859
- range.detach();
860
- return backward;
861
- }
862
- /**
863
- * Returns a parent {@link module:engine/view/uielement~UIElement} or {@link module:engine/view/rawelement~RawElement}
864
- * that hosts the provided DOM node. Returns `null` if there is no such parent.
865
- */
866
- getHostViewElement(domNode) {
867
- const ancestors = getAncestors(domNode);
868
- // Remove domNode from the list.
869
- ancestors.pop();
870
- while (ancestors.length) {
871
- const domNode = ancestors.pop();
872
- const viewNode = this._domToViewMapping.get(domNode);
873
- if (viewNode && (viewNode.is('uiElement') || viewNode.is('rawElement'))) {
874
- return viewNode;
875
- }
876
- }
877
- return null;
878
- }
879
- /**
880
- * Checks if the given selection's boundaries are at correct places.
881
- *
882
- * The following places are considered as incorrect for selection boundaries:
883
- *
884
- * * before or in the middle of an inline filler sequence,
885
- * * inside a DOM element which represents {@link module:engine/view/uielement~UIElement a view UI element},
886
- * * inside a DOM element which represents {@link module:engine/view/rawelement~RawElement a view raw element}.
887
- *
888
- * @param domSelection The DOM selection object to be checked.
889
- * @returns `true` if the given selection is at a correct place, `false` otherwise.
890
- */
891
- isDomSelectionCorrect(domSelection) {
892
- return this._isDomSelectionPositionCorrect(domSelection.anchorNode, domSelection.anchorOffset) &&
893
- this._isDomSelectionPositionCorrect(domSelection.focusNode, domSelection.focusOffset);
894
- }
895
- /**
896
- * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as raw data
897
- * and not processed during the conversion from DOM nodes to view elements.
898
- *
899
- * This is affecting how {@link module:engine/view/domconverter~DomConverter#domToView} and
900
- * {@link module:engine/view/domconverter~DomConverter#domChildrenToView} process DOM nodes.
901
- *
902
- * The raw data can be later accessed by a
903
- * {@link module:engine/view/element~Element#getCustomProperty custom property of a view element} called `"$rawContent"`.
904
- *
905
- * @param pattern Pattern matching a view element whose content should
906
- * be treated as raw data.
907
- */
908
- registerRawContentMatcher(pattern) {
909
- this._rawContentElementMatcher.add(pattern);
910
- }
911
- /**
912
- * Registers a {@link module:engine/view/matcher~MatcherPattern} for inline object view elements.
913
- *
914
- * This is affecting how {@link module:engine/view/domconverter~DomConverter#domToView} and
915
- * {@link module:engine/view/domconverter~DomConverter#domChildrenToView} process DOM nodes.
916
- *
917
- * This is an extension of a simple {@link #inlineObjectElements} array of element names.
918
- *
919
- * @param pattern Pattern matching a view element which should be treated as an inline object.
920
- */
921
- registerInlineObjectMatcher(pattern) {
922
- this._inlineObjectElementMatcher.add(pattern);
923
- }
924
- /**
925
- * Returns the block {@link module:engine/view/filler filler} node based on the current {@link #blockFillerMode} setting.
926
- */
927
- _getBlockFiller() {
928
- switch (this.blockFillerMode) {
929
- case 'nbsp':
930
- return NBSP_FILLER(this._domDocument); // eslint-disable-line new-cap
931
- case 'markedNbsp':
932
- return MARKED_NBSP_FILLER(this._domDocument); // eslint-disable-line new-cap
933
- case 'br':
934
- return BR_FILLER(this._domDocument); // eslint-disable-line new-cap
935
- }
936
- }
937
- /**
938
- * Checks if the given DOM position is a correct place for selection boundary. See {@link #isDomSelectionCorrect}.
939
- *
940
- * @param domParent Position parent.
941
- * @param offset Position offset.
942
- * @returns `true` if given position is at a correct place for selection boundary, `false` otherwise.
943
- */
944
- _isDomSelectionPositionCorrect(domParent, offset) {
945
- // If selection is before or in the middle of inline filler string, it is incorrect.
946
- if (isText(domParent) && startsWithFiller(domParent) && offset < INLINE_FILLER_LENGTH) {
947
- // Selection in a text node, at wrong position (before or in the middle of filler).
948
- return false;
949
- }
950
- if (this.isElement(domParent) && startsWithFiller(domParent.childNodes[offset])) {
951
- // Selection in an element node, before filler text node.
952
- return false;
953
- }
954
- const viewParent = this.mapDomToView(domParent);
955
- // The position is incorrect when anchored inside a UIElement or a RawElement.
956
- // Note: In case of UIElement and RawElement, mapDomToView() returns a parent element for any DOM child
957
- // so there's no need to perform any additional checks.
958
- if (viewParent && (viewParent.is('uiElement') || viewParent.is('rawElement'))) {
959
- return false;
960
- }
961
- return true;
962
- }
963
- /**
964
- * Internal generator for {@link #domToView}. Also used by {@link #domChildrenToView}.
965
- * Separates DOM nodes conversion from whitespaces processing.
966
- *
967
- * @param domNode DOM node or document fragment to transform.
968
- * @param inlineNodes An array of recently encountered inline nodes truncated to the block element boundaries.
969
- * Used later to process whitespaces.
970
- */
971
- *_domToView(domNode, options, inlineNodes) {
972
- if (this.isBlockFiller(domNode)) {
973
- return null;
974
- }
975
- // When node is inside a UIElement or a RawElement return that parent as it's view representation.
976
- const hostElement = this.getHostViewElement(domNode);
977
- if (hostElement) {
978
- return hostElement;
979
- }
980
- if (isComment(domNode) && options.skipComments) {
981
- return null;
982
- }
983
- if (isText(domNode)) {
984
- if (isInlineFiller(domNode)) {
985
- return null;
986
- }
987
- else {
988
- const textData = domNode.data;
989
- if (textData === '') {
990
- return null;
991
- }
992
- const textNode = new ViewText(this.document, textData);
993
- inlineNodes.push(textNode);
994
- return textNode;
995
- }
996
- }
997
- else {
998
- let viewElement = this.mapDomToView(domNode);
999
- if (viewElement) {
1000
- if (this._isInlineObjectElement(viewElement)) {
1001
- inlineNodes.push(viewElement);
1002
- }
1003
- return viewElement;
1004
- }
1005
- if (this.isDocumentFragment(domNode)) {
1006
- // Create view document fragment.
1007
- viewElement = new ViewDocumentFragment(this.document);
1008
- if (options.bind) {
1009
- this.bindDocumentFragments(domNode, viewElement);
1010
- }
1011
- }
1012
- else {
1013
- // Create view element.
1014
- viewElement = this._createViewElement(domNode, options);
1015
- if (options.bind) {
1016
- this.bindElements(domNode, viewElement);
1017
- }
1018
- // Copy element's attributes.
1019
- const attrs = domNode.attributes;
1020
- if (attrs) {
1021
- for (let l = attrs.length, i = 0; i < l; i++) {
1022
- viewElement._setAttribute(attrs[i].name, attrs[i].value);
1023
- }
1024
- }
1025
- // Treat this element's content as a raw data if it was registered as such.
1026
- if (this._isViewElementWithRawContent(viewElement, options)) {
1027
- viewElement._setCustomProperty('$rawContent', domNode.innerHTML);
1028
- if (!this._isBlockViewElement(viewElement)) {
1029
- inlineNodes.push(viewElement);
1030
- }
1031
- return viewElement;
1032
- }
1033
- // Comment node is also treated as an element with raw data.
1034
- if (isComment(domNode)) {
1035
- viewElement._setCustomProperty('$rawContent', domNode.data);
1036
- return viewElement;
1037
- }
1038
- }
1039
- // Yield the element first so the flow of nested inline nodes is not reversed inside elements.
1040
- yield viewElement;
1041
- const nestedInlineNodes = [];
1042
- if (options.withChildren !== false) {
1043
- for (const child of this.domChildrenToView(domNode, options, nestedInlineNodes)) {
1044
- viewElement._appendChild(child);
1045
- }
1046
- }
1047
- // Check if this is an inline object after processing child nodes so matcher
1048
- // for inline objects can verify if the element is empty.
1049
- if (this._isInlineObjectElement(viewElement)) {
1050
- inlineNodes.push(viewElement);
1051
- }
1052
- else {
1053
- // It's an inline element that is not an object (like <b>, <i>) or a block element.
1054
- for (const inlineNode of nestedInlineNodes) {
1055
- inlineNodes.push(inlineNode);
1056
- }
1057
- }
1058
- }
1059
- }
1060
- /**
1061
- * Internal helper that walks the list of inline view nodes already generated from DOM nodes
1062
- * and handles whitespaces and NBSPs.
1063
- *
1064
- * @param domParent The DOM parent of the given inline nodes. This should be a document fragment or
1065
- * a block element to whitespace processing start cleaning.
1066
- * @param inlineNodes An array of recently encountered inline nodes truncated to the block element boundaries.
1067
- */
1068
- _processDomInlineNodes(domParent, inlineNodes, options) {
1069
- if (!inlineNodes.length) {
1070
- return;
1071
- }
1072
- // Process text nodes only after reaching a block or document fragment,
1073
- // do not alter whitespaces while processing an inline element like <b> or <i>.
1074
- if (domParent && !this.isDocumentFragment(domParent) && !this._isBlockDomElement(domParent)) {
1075
- return;
1076
- }
1077
- let prevNodeEndsWithSpace = false;
1078
- for (let i = 0; i < inlineNodes.length; i++) {
1079
- const node = inlineNodes[i];
1080
- if (!node.is('$text')) {
1081
- prevNodeEndsWithSpace = false;
1082
- continue;
1083
- }
1084
- let data;
1085
- let nodeEndsWithSpace = false;
1086
- if (_hasViewParentOfType(node, this.preElements)) {
1087
- data = getDataWithoutFiller(node.data);
1088
- }
1089
- else {
1090
- // Change all consecutive whitespace characters (from the [ \n\t\r] set –
1091
- // see https://github.com/ckeditor/ckeditor5-engine/issues/822#issuecomment-311670249) to a single space character.
1092
- // That's how multiple whitespaces are treated when rendered, so we normalize those whitespaces.
1093
- // We're replacing 1+ (and not 2+) to also normalize singular \n\t\r characters (#822).
1094
- data = node.data.replace(/[ \n\t\r]{1,}/g, ' ');
1095
- nodeEndsWithSpace = /[^\S\u00A0]/.test(data.charAt(data.length - 1));
1096
- const prevNode = i > 0 ? inlineNodes[i - 1] : null;
1097
- const nextNode = i + 1 < inlineNodes.length ? inlineNodes[i + 1] : null;
1098
- const shouldLeftTrim = !prevNode || prevNode.is('element') && prevNode.name == 'br' || prevNodeEndsWithSpace;
1099
- const shouldRightTrim = nextNode ? false : !startsWithFiller(node.data);
1100
- // Do not try to clear whitespaces if this is flat mapping for the purpose of mutation observer and differ in rendering.
1101
- if (options.withChildren !== false) {
1102
- // If the previous dom text node does not exist or it ends by whitespace character, remove space character from the
1103
- // beginning of this text node. Such space character is treated as a whitespace.
1104
- if (shouldLeftTrim) {
1105
- data = data.replace(/^ /, '');
1106
- }
1107
- // If the next text node does not exist remove space character from the end of this text node.
1108
- if (shouldRightTrim) {
1109
- data = data.replace(/ $/, '');
1110
- }
1111
- }
1112
- // At the beginning and end of a block element, Firefox inserts normal space + <br> instead of non-breaking space.
1113
- // This means that the text node starts/end with normal space instead of non-breaking space.
1114
- // This causes a problem because the normal space would be removed in `.replace` calls above. To prevent that,
1115
- // the inline filler is removed only after the data is initially processed (by the `.replace` above). See ckeditor5#692.
1116
- data = getDataWithoutFiller(data);
1117
- // At this point we should have removed all whitespaces from DOM text data.
1118
- //
1119
- // Now, We will reverse the process that happens in `_processDataFromViewText`.
1120
- //
1121
- // We have to change &nbsp; chars, that were in DOM text data because of rendering reasons, to spaces.
1122
- // First, change all ` \u00A0` pairs (space + &nbsp;) to two spaces. DOM converter changes two spaces from model/view to
1123
- // ` \u00A0` to ensure proper rendering. Since here we convert back, we recognize those pairs and change them back to ` `.
1124
- data = data.replace(/ \u00A0/g, ' ');
1125
- const isNextNodeInlineObjectElement = nextNode && nextNode.is('element') && nextNode.name != 'br';
1126
- const isNextNodeStartingWithSpace = nextNode && nextNode.is('$text') && nextNode.data.charAt(0) == ' ';
1127
- // Then, let's change the last nbsp to a space.
1128
- if (/[ \u00A0]\u00A0$/.test(data) || !nextNode || isNextNodeInlineObjectElement || isNextNodeStartingWithSpace) {
1129
- data = data.replace(/\u00A0$/, ' ');
1130
- }
1131
- // Then, change &nbsp; character that is at the beginning of the text node to space character.
1132
- // We do that replacement only if this is the first node or the previous node ends on whitespace character.
1133
- if (shouldLeftTrim || prevNode && prevNode.is('element') && prevNode.name != 'br') {
1134
- data = data.replace(/^\u00A0/, ' ');
1135
- }
1136
- }
1137
- // At this point, all whitespaces should be removed and all &nbsp; created for rendering reasons should be
1138
- // changed to normal space. All left &nbsp; are &nbsp; inserted intentionally.
1139
- if (data.length == 0 && node.parent) {
1140
- node._remove();
1141
- inlineNodes.splice(i, 1);
1142
- i--;
1143
- }
1144
- else {
1145
- node._data = data;
1146
- prevNodeEndsWithSpace = nodeEndsWithSpace;
1147
- }
1148
- }
1149
- inlineNodes.length = 0;
1150
- }
1151
- /**
1152
- * Takes text data from a given {@link module:engine/view/text~Text#data} and processes it so
1153
- * it is correctly displayed in the DOM.
1154
- *
1155
- * Following changes are done:
1156
- *
1157
- * * a space at the beginning is changed to `&nbsp;` if this is the first text node in its container
1158
- * element or if a previous text node ends with a space character,
1159
- * * space at the end of the text node is changed to `&nbsp;` if there are two spaces at the end of a node or if next node
1160
- * starts with a space or if it is the last text node in its container,
1161
- * * remaining spaces are replaced to a chain of spaces and `&nbsp;` (e.g. `'x x'` becomes `'x &nbsp; x'`).
1162
- *
1163
- * Content of {@link #preElements} is not processed.
1164
- *
1165
- * @param node View text node to process.
1166
- * @returns Processed text data.
1167
- */
1168
- _processDataFromViewText(node) {
1169
- let data = node.data;
1170
- // If any of node ancestors has a name which is in `preElements` array, then currently processed
1171
- // view text node is (will be) in preformatted element. We should not change whitespaces then.
1172
- if (node.getAncestors().some(parent => this.preElements.includes(parent.name))) {
1173
- return data;
1174
- }
1175
- // 1. Replace the first space with a nbsp if the previous node ends with a space or there is no previous node
1176
- // (container element boundary).
1177
- if (data.charAt(0) == ' ') {
1178
- const prevNode = this._getTouchingInlineViewNode(node, false);
1179
- const prevEndsWithSpace = prevNode && prevNode.is('$textProxy') && this._nodeEndsWithSpace(prevNode);
1180
- if (prevEndsWithSpace || !prevNode) {
1181
- data = '\u00A0' + data.substr(1);
1182
- }
1183
- }
1184
- // 2. Replace the last space with nbsp if there are two spaces at the end or if the next node starts with space or there is no
1185
- // next node (container element boundary).
1186
- //
1187
- // Keep in mind that Firefox prefers $nbsp; before tag, not inside it:
1188
- //
1189
- // Foo <span>&nbsp;bar</span> <-- bad.
1190
- // Foo&nbsp;<span> bar</span> <-- good.
1191
- //
1192
- // More here: https://github.com/ckeditor/ckeditor5-engine/issues/1747.
1193
- if (data.charAt(data.length - 1) == ' ') {
1194
- const nextNode = this._getTouchingInlineViewNode(node, true);
1195
- const nextStartsWithSpace = nextNode && nextNode.is('$textProxy') && nextNode.data.charAt(0) == ' ';
1196
- if (data.charAt(data.length - 2) == ' ' || !nextNode || nextStartsWithSpace) {
1197
- data = data.substr(0, data.length - 1) + '\u00A0';
1198
- }
1199
- }
1200
- // 3. Create space+nbsp pairs.
1201
- return data.replace(/ {2}/g, ' \u00A0');
1202
- }
1203
- /**
1204
- * Checks whether given node ends with a space character after changing appropriate space characters to `&nbsp;`s.
1205
- *
1206
- * @param node Node to check.
1207
- * @returns `true` if given `node` ends with space, `false` otherwise.
1208
- */
1209
- _nodeEndsWithSpace(node) {
1210
- if (node.getAncestors().some(parent => this.preElements.includes(parent.name))) {
1211
- return false;
1212
- }
1213
- const data = this._processDataFromViewText(node);
1214
- return data.charAt(data.length - 1) == ' ';
1215
- }
1216
- /**
1217
- * Helper function. For given {@link module:engine/view/text~Text view text node}, it finds previous or next sibling
1218
- * that is contained in the same container element. If there is no such sibling, `null` is returned.
1219
- *
1220
- * @param node Reference node.
1221
- * @returns Touching text node, an inline object
1222
- * or `null` if there is no next or previous touching text node.
1223
- */
1224
- _getTouchingInlineViewNode(node, getNext) {
1225
- const treeWalker = new ViewTreeWalker({
1226
- startPosition: getNext ? ViewPosition._createAfter(node) : ViewPosition._createBefore(node),
1227
- direction: getNext ? 'forward' : 'backward'
1228
- });
1229
- for (const value of treeWalker) {
1230
- // <br> found it works like a block boundary, so do not scan further.
1231
- if (value.item.is('element', 'br')) {
1232
- return null;
1233
- }
1234
- // Found an inline object (for example an image).
1235
- else if (this._isInlineObjectElement(value.item)) {
1236
- return value.item;
1237
- }
1238
- // ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last
1239
- // text node in its container element.
1240
- else if (value.item.is('containerElement')) {
1241
- return null;
1242
- }
1243
- // Found a text node in the same container element.
1244
- else if (value.item.is('$textProxy')) {
1245
- return value.item;
1246
- }
1247
- }
1248
- return null;
1249
- }
1250
- /**
1251
- * Returns `true` if a DOM node belongs to {@link #blockElements}. `false` otherwise.
1252
- */
1253
- _isBlockDomElement(node) {
1254
- return this.isElement(node) && this.blockElements.includes(node.tagName.toLowerCase());
1255
- }
1256
- /**
1257
- * Returns `true` if a view node belongs to {@link #blockElements}. `false` otherwise.
1258
- */
1259
- _isBlockViewElement(node) {
1260
- return node.is('element') && this.blockElements.includes(node.name);
1261
- }
1262
- /**
1263
- * Returns `true` if a DOM node belongs to {@link #inlineObjectElements}. `false` otherwise.
1264
- */
1265
- _isInlineObjectElement(node) {
1266
- if (!node.is('element')) {
1267
- return false;
1268
- }
1269
- return node.name == 'br' ||
1270
- this.inlineObjectElements.includes(node.name) ||
1271
- !!this._inlineObjectElementMatcher.match(node);
1272
- }
1273
- /**
1274
- * Creates view element basing on the node type.
1275
- *
1276
- * @param node DOM node to check.
1277
- * @param options Conversion options. See {@link module:engine/view/domconverter~DomConverter#domToView} options parameter.
1278
- */
1279
- _createViewElement(node, options) {
1280
- if (isComment(node)) {
1281
- return new ViewUIElement(this.document, '$comment');
1282
- }
1283
- const viewName = options.keepOriginalCase ? node.tagName : node.tagName.toLowerCase();
1284
- return new ViewElement(this.document, viewName);
1285
- }
1286
- /**
1287
- * Checks if view element's content should be treated as a raw data.
1288
- *
1289
- * @param viewElement View element to check.
1290
- * @param options Conversion options. See {@link module:engine/view/domconverter~DomConverter#domToView} options parameter.
1291
- */
1292
- _isViewElementWithRawContent(viewElement, options) {
1293
- return options.withChildren !== false && viewElement.is('element') && !!this._rawContentElementMatcher.match(viewElement);
1294
- }
1295
- /**
1296
- * Checks whether a given element name should be renamed in a current rendering mode.
1297
- *
1298
- * @param elementName The name of view element.
1299
- */
1300
- _shouldRenameElement(elementName) {
1301
- const name = elementName.toLowerCase();
1302
- return this.renderingMode === 'editing' && this.unsafeElements.includes(name);
1303
- }
1304
- /**
1305
- * Return a <span> element with a special attribute holding the name of the original element.
1306
- * Optionally, copy all the attributes of the original element if that element is provided.
1307
- *
1308
- * @param elementName The name of view element.
1309
- * @param originalDomElement The original DOM element to copy attributes and content from.
1310
- */
1311
- _createReplacementDomElement(elementName, originalDomElement) {
1312
- const newDomElement = this._domDocument.createElement('span');
1313
- // Mark the span replacing a script as hidden.
1314
- newDomElement.setAttribute(UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE, elementName);
1315
- if (originalDomElement) {
1316
- while (originalDomElement.firstChild) {
1317
- newDomElement.appendChild(originalDomElement.firstChild);
1318
- }
1319
- for (const attributeName of originalDomElement.getAttributeNames()) {
1320
- newDomElement.setAttribute(attributeName, originalDomElement.getAttribute(attributeName));
1321
- }
1322
- }
1323
- return newDomElement;
1324
- }
1325
- }
1326
- /**
1327
- * Helper function.
1328
- * Used to check if given native `Element` or `Text` node has parent with tag name from `types` array.
1329
- *
1330
- * @returns`true` if such parent exists or `false` if it does not.
1331
- */
1332
- function _hasViewParentOfType(node, types) {
1333
- return node.getAncestors().some(parent => parent.is('element') && types.includes(parent.name));
1334
- }
1335
- /**
1336
- * A helper that executes given callback for each DOM node's ancestor, starting from the given node
1337
- * and ending in document#documentElement.
1338
- *
1339
- * @param callback A callback to be executed for each ancestor.
1340
- */
1341
- function forEachDomElementAncestor(element, callback) {
1342
- let node = element;
1343
- while (node) {
1344
- callback(node);
1345
- node = node.parentElement;
1346
- }
1347
- }
1348
- /**
1349
- * Checks if given node is a nbsp block filler.
1350
- *
1351
- * A &nbsp; is a block filler only if it is a single child of a block element.
1352
- *
1353
- * @param domNode DOM node.
1354
- */
1355
- function isNbspBlockFiller(domNode, blockElements) {
1356
- const isNBSP = domNode.isEqualNode(NBSP_FILLER_REF);
1357
- return isNBSP && hasBlockParent(domNode, blockElements) && domNode.parentNode.childNodes.length === 1;
1358
- }
1359
- /**
1360
- * Checks if domNode has block parent.
1361
- *
1362
- * @param domNode DOM node.
1363
- */
1364
- function hasBlockParent(domNode, blockElements) {
1365
- const parent = domNode.parentNode;
1366
- return !!parent && !!parent.tagName && blockElements.includes(parent.tagName.toLowerCase());
1367
- }
1368
- /**
1369
- * Log to console the information about element that was replaced.
1370
- * Check UNSAFE_ELEMENTS for all recognized unsafe elements.
1371
- *
1372
- * @param elementName The name of the view element.
1373
- */
1374
- function _logUnsafeElement(elementName) {
1375
- if (elementName === 'script') {
1376
- logWarning('domconverter-unsafe-script-element-detected');
1377
- }
1378
- if (elementName === 'style') {
1379
- logWarning('domconverter-unsafe-style-element-detected');
1380
- }
1381
- }
1382
- /**
1383
- * While rendering the editor content, the {@link module:engine/view/domconverter~DomConverter} detected a `<script>` element that may
1384
- * disrupt the editing experience. To avoid this, the `<script>` element was replaced with `<span data-ck-unsafe-element="script"></span>`.
1385
- *
1386
- * @error domconverter-unsafe-script-element-detected
1387
- */
1388
- /**
1389
- * While rendering the editor content, the {@link module:engine/view/domconverter~DomConverter} detected a `<style>` element that may affect
1390
- * the editing experience. To avoid this, the `<style>` element was replaced with `<span data-ck-unsafe-element="style"></span>`.
1391
- *
1392
- * @error domconverter-unsafe-style-element-detected
1393
- */
1394
- /**
1395
- * The {@link module:engine/view/domconverter~DomConverter} detected an interactive attribute in the
1396
- * {@glink framework/architecture/editing-engine#editing-pipeline editing pipeline}. For the best
1397
- * editing experience, the attribute was renamed to `data-ck-unsafe-attribute-[original attribute name]`.
1398
- *
1399
- * If you are the author of the plugin that generated this attribute and you want it to be preserved
1400
- * in the editing pipeline, you can configure this when creating the element
1401
- * using {@link module:engine/view/downcastwriter~DowncastWriter} during the
1402
- * {@glink framework/architecture/editing-engine#conversion model–view conversion}. Methods such as
1403
- * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement},
1404
- * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement}, or
1405
- * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement}
1406
- * accept an option that will disable filtering of specific attributes:
1407
- *
1408
- * ```ts
1409
- * const paragraph = writer.createContainerElement( 'p',
1410
- * {
1411
- * class: 'clickable-paragraph',
1412
- * onclick: 'alert( "Paragraph clicked!" )'
1413
- * },
1414
- * {
1415
- * // Make sure the "onclick" attribute will pass through.
1416
- * renderUnsafeAttributes: [ 'onclick' ]
1417
- * }
1418
- * );
1419
- * ```
1420
- *
1421
- * @error domconverter-unsafe-attribute-detected
1422
- * @param domElement The DOM element the attribute was set on.
1423
- * @param key The original name of the attribute
1424
- * @param value The value of the original attribute
1425
- */
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module engine/view/domconverter
7
+ */
8
+ /* globals Node, NodeFilter, DOMParser, Text */
9
+ import ViewText from './text';
10
+ import ViewElement from './element';
11
+ import ViewUIElement from './uielement';
12
+ import ViewPosition from './position';
13
+ import ViewRange from './range';
14
+ import ViewSelection from './selection';
15
+ import ViewDocumentFragment from './documentfragment';
16
+ import ViewTreeWalker from './treewalker';
17
+ import { default as Matcher } from './matcher';
18
+ import { BR_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER, MARKED_NBSP_FILLER, getDataWithoutFiller, isInlineFiller, startsWithFiller } from './filler';
19
+ import { global, logWarning, indexOf, getAncestors, isText, isComment, isValidAttributeName, first, env } from '@ckeditor/ckeditor5-utils';
20
+ const BR_FILLER_REF = BR_FILLER(global.document); // eslint-disable-line new-cap
21
+ const NBSP_FILLER_REF = NBSP_FILLER(global.document); // eslint-disable-line new-cap
22
+ const MARKED_NBSP_FILLER_REF = MARKED_NBSP_FILLER(global.document); // eslint-disable-line new-cap
23
+ const UNSAFE_ATTRIBUTE_NAME_PREFIX = 'data-ck-unsafe-attribute-';
24
+ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
25
+ /**
26
+ * `DomConverter` is a set of tools to do transformations between DOM nodes and view nodes. It also handles
27
+ * {@link module:engine/view/domconverter~DomConverter#bindElements bindings} between these nodes.
28
+ *
29
+ * An instance of the DOM converter is available under
30
+ * {@link module:engine/view/view~View#domConverter `editor.editing.view.domConverter`}.
31
+ *
32
+ * The DOM converter does not check which nodes should be rendered (use {@link module:engine/view/renderer~Renderer}), does not keep the
33
+ * state of a tree nor keeps the synchronization between the tree view and the DOM tree (use {@link module:engine/view/document~Document}).
34
+ *
35
+ * The DOM converter keeps DOM elements to view element bindings, so when the converter gets destroyed, the bindings are lost.
36
+ * Two converters will keep separate binding maps, so one tree view can be bound with two DOM trees.
37
+ */
38
+ export default class DomConverter {
39
+ /**
40
+ * Creates a DOM converter.
41
+ *
42
+ * @param document The view document instance.
43
+ * @param options An object with configuration options.
44
+ * @param options.blockFillerMode The type of the block filler to use.
45
+ * Default value depends on the options.renderingMode:
46
+ * 'nbsp' when options.renderingMode == 'data',
47
+ * 'br' when options.renderingMode == 'editing'.
48
+ * @param options.renderingMode Whether to leave the View-to-DOM conversion result unchanged
49
+ * or improve editing experience by filtering out interactive data.
50
+ */
51
+ constructor(document, { blockFillerMode, renderingMode = 'editing' } = {}) {
52
+ /**
53
+ * The DOM-to-view mapping.
54
+ */
55
+ this._domToViewMapping = new WeakMap();
56
+ /**
57
+ * The view-to-DOM mapping.
58
+ */
59
+ this._viewToDomMapping = new WeakMap();
60
+ /**
61
+ * Holds the mapping between fake selection containers and corresponding view selections.
62
+ */
63
+ this._fakeSelectionMapping = new WeakMap();
64
+ /**
65
+ * Matcher for view elements whose content should be treated as raw data
66
+ * and not processed during the conversion from DOM nodes to view elements.
67
+ */
68
+ this._rawContentElementMatcher = new Matcher();
69
+ /**
70
+ * Matcher for inline object view elements. This is an extension of a simple {@link #inlineObjectElements} array of element names.
71
+ */
72
+ this._inlineObjectElementMatcher = new Matcher();
73
+ this.document = document;
74
+ this.renderingMode = renderingMode;
75
+ this.blockFillerMode = blockFillerMode || (renderingMode === 'editing' ? 'br' : 'nbsp');
76
+ this.preElements = ['pre'];
77
+ this.blockElements = [
78
+ 'address', 'article', 'aside', 'blockquote', 'caption', 'center', 'dd', 'details', 'dir', 'div',
79
+ 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header',
80
+ 'hgroup', 'legend', 'li', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'section', 'summary', 'table', 'tbody',
81
+ 'td', 'tfoot', 'th', 'thead', 'tr', 'ul'
82
+ ];
83
+ this.inlineObjectElements = [
84
+ 'object', 'iframe', 'input', 'button', 'textarea', 'select', 'option', 'video', 'embed', 'audio', 'img', 'canvas'
85
+ ];
86
+ this.unsafeElements = ['script', 'style'];
87
+ this._domDocument = this.renderingMode === 'editing' ? global.document : global.document.implementation.createHTMLDocument('');
88
+ }
89
+ /**
90
+ * Binds a given DOM element that represents fake selection to a **position** of a
91
+ * {@link module:engine/view/documentselection~DocumentSelection document selection}.
92
+ * Document selection copy is stored and can be retrieved by the
93
+ * {@link module:engine/view/domconverter~DomConverter#fakeSelectionToView} method.
94
+ */
95
+ bindFakeSelection(domElement, viewDocumentSelection) {
96
+ this._fakeSelectionMapping.set(domElement, new ViewSelection(viewDocumentSelection));
97
+ }
98
+ /**
99
+ * Returns a {@link module:engine/view/selection~Selection view selection} instance corresponding to a given
100
+ * DOM element that represents fake selection. Returns `undefined` if binding to the given DOM element does not exist.
101
+ */
102
+ fakeSelectionToView(domElement) {
103
+ return this._fakeSelectionMapping.get(domElement);
104
+ }
105
+ /**
106
+ * Binds DOM and view elements, so it will be possible to get corresponding elements using
107
+ * {@link module:engine/view/domconverter~DomConverter#mapDomToView} and
108
+ * {@link module:engine/view/domconverter~DomConverter#mapViewToDom}.
109
+ *
110
+ * @param domElement The DOM element to bind.
111
+ * @param viewElement The view element to bind.
112
+ */
113
+ bindElements(domElement, viewElement) {
114
+ this._domToViewMapping.set(domElement, viewElement);
115
+ this._viewToDomMapping.set(viewElement, domElement);
116
+ }
117
+ /**
118
+ * Unbinds a given DOM element from the view element it was bound to. Unbinding is deep, meaning that all children of
119
+ * the DOM element will be unbound too.
120
+ *
121
+ * @param domElement The DOM element to unbind.
122
+ */
123
+ unbindDomElement(domElement) {
124
+ const viewElement = this._domToViewMapping.get(domElement);
125
+ if (viewElement) {
126
+ this._domToViewMapping.delete(domElement);
127
+ this._viewToDomMapping.delete(viewElement);
128
+ for (const child of Array.from(domElement.children)) {
129
+ this.unbindDomElement(child);
130
+ }
131
+ }
132
+ }
133
+ /**
134
+ * Binds DOM and view document fragments, so it will be possible to get corresponding document fragments using
135
+ * {@link module:engine/view/domconverter~DomConverter#mapDomToView} and
136
+ * {@link module:engine/view/domconverter~DomConverter#mapViewToDom}.
137
+ *
138
+ * @param domFragment The DOM document fragment to bind.
139
+ * @param viewFragment The view document fragment to bind.
140
+ */
141
+ bindDocumentFragments(domFragment, viewFragment) {
142
+ this._domToViewMapping.set(domFragment, viewFragment);
143
+ this._viewToDomMapping.set(viewFragment, domFragment);
144
+ }
145
+ /**
146
+ * Decides whether a given pair of attribute key and value should be passed further down the pipeline.
147
+ *
148
+ * @param elementName Element name in lower case.
149
+ */
150
+ shouldRenderAttribute(attributeKey, attributeValue, elementName) {
151
+ if (this.renderingMode === 'data') {
152
+ return true;
153
+ }
154
+ attributeKey = attributeKey.toLowerCase();
155
+ if (attributeKey.startsWith('on')) {
156
+ return false;
157
+ }
158
+ if (attributeKey === 'srcdoc' &&
159
+ attributeValue.match(/\bon\S+\s*=|javascript:|<\s*\/*script/i)) {
160
+ return false;
161
+ }
162
+ if (elementName === 'img' &&
163
+ (attributeKey === 'src' || attributeKey === 'srcset')) {
164
+ return true;
165
+ }
166
+ if (elementName === 'source' && attributeKey === 'srcset') {
167
+ return true;
168
+ }
169
+ if (attributeValue.match(/^\s*(javascript:|data:(image\/svg|text\/x?html))/i)) {
170
+ return false;
171
+ }
172
+ return true;
173
+ }
174
+ /**
175
+ * Set `domElement`'s content using provided `html` argument. Apply necessary filtering for the editing pipeline.
176
+ *
177
+ * @param domElement DOM element that should have `html` set as its content.
178
+ * @param html Textual representation of the HTML that will be set on `domElement`.
179
+ */
180
+ setContentOf(domElement, html) {
181
+ // For data pipeline we pass the HTML as-is.
182
+ if (this.renderingMode === 'data') {
183
+ domElement.innerHTML = html;
184
+ return;
185
+ }
186
+ const document = new DOMParser().parseFromString(html, 'text/html');
187
+ const fragment = document.createDocumentFragment();
188
+ const bodyChildNodes = document.body.childNodes;
189
+ while (bodyChildNodes.length > 0) {
190
+ fragment.appendChild(bodyChildNodes[0]);
191
+ }
192
+ const treeWalker = document.createTreeWalker(fragment, NodeFilter.SHOW_ELEMENT);
193
+ const nodes = [];
194
+ let currentNode;
195
+ // eslint-disable-next-line no-cond-assign
196
+ while (currentNode = treeWalker.nextNode()) {
197
+ nodes.push(currentNode);
198
+ }
199
+ for (const currentNode of nodes) {
200
+ // Go through nodes to remove those that are prohibited in editing pipeline.
201
+ for (const attributeName of currentNode.getAttributeNames()) {
202
+ this.setDomElementAttribute(currentNode, attributeName, currentNode.getAttribute(attributeName));
203
+ }
204
+ const elementName = currentNode.tagName.toLowerCase();
205
+ // There are certain nodes, that should be renamed to <span> in editing pipeline.
206
+ if (this._shouldRenameElement(elementName)) {
207
+ _logUnsafeElement(elementName);
208
+ currentNode.replaceWith(this._createReplacementDomElement(elementName, currentNode));
209
+ }
210
+ }
211
+ // Empty the target element.
212
+ while (domElement.firstChild) {
213
+ domElement.firstChild.remove();
214
+ }
215
+ domElement.append(fragment);
216
+ }
217
+ /**
218
+ * Converts the view to the DOM. For all text nodes, not bound elements and document fragments new items will
219
+ * be created. For bound elements and document fragments the method will return corresponding items.
220
+ *
221
+ * @param viewNode View node or document fragment to transform.
222
+ * @param options Conversion options.
223
+ * @param options.bind Determines whether new elements will be bound.
224
+ * @param options.withChildren If `false`, node's and document fragment's children will not be converted.
225
+ * @returns Converted node or DocumentFragment.
226
+ */
227
+ viewToDom(viewNode, options = {}) {
228
+ if (viewNode.is('$text')) {
229
+ const textData = this._processDataFromViewText(viewNode);
230
+ return this._domDocument.createTextNode(textData);
231
+ }
232
+ else {
233
+ if (this.mapViewToDom(viewNode)) {
234
+ return this.mapViewToDom(viewNode);
235
+ }
236
+ let domElement;
237
+ if (viewNode.is('documentFragment')) {
238
+ // Create DOM document fragment.
239
+ domElement = this._domDocument.createDocumentFragment();
240
+ if (options.bind) {
241
+ this.bindDocumentFragments(domElement, viewNode);
242
+ }
243
+ }
244
+ else if (viewNode.is('uiElement')) {
245
+ if (viewNode.name === '$comment') {
246
+ domElement = this._domDocument.createComment(viewNode.getCustomProperty('$rawContent'));
247
+ }
248
+ else {
249
+ // UIElement has its own render() method (see #799).
250
+ domElement = viewNode.render(this._domDocument, this);
251
+ }
252
+ if (options.bind) {
253
+ this.bindElements(domElement, viewNode);
254
+ }
255
+ return domElement;
256
+ }
257
+ else {
258
+ // Create DOM element.
259
+ if (this._shouldRenameElement(viewNode.name)) {
260
+ _logUnsafeElement(viewNode.name);
261
+ domElement = this._createReplacementDomElement(viewNode.name);
262
+ }
263
+ else if (viewNode.hasAttribute('xmlns')) {
264
+ domElement = this._domDocument.createElementNS(viewNode.getAttribute('xmlns'), viewNode.name);
265
+ }
266
+ else {
267
+ domElement = this._domDocument.createElement(viewNode.name);
268
+ }
269
+ // RawElement take care of their children in RawElement#render() method which can be customized
270
+ // (see https://github.com/ckeditor/ckeditor5/issues/4469).
271
+ if (viewNode.is('rawElement')) {
272
+ viewNode.render(domElement, this);
273
+ }
274
+ if (options.bind) {
275
+ this.bindElements(domElement, viewNode);
276
+ }
277
+ // Copy element's attributes.
278
+ for (const key of viewNode.getAttributeKeys()) {
279
+ this.setDomElementAttribute(domElement, key, viewNode.getAttribute(key), viewNode);
280
+ }
281
+ }
282
+ if (options.withChildren !== false) {
283
+ for (const child of this.viewChildrenToDom(viewNode, options)) {
284
+ domElement.appendChild(child);
285
+ }
286
+ }
287
+ return domElement;
288
+ }
289
+ }
290
+ /**
291
+ * Sets the attribute on a DOM element.
292
+ *
293
+ * **Note**: To remove the attribute, use {@link #removeDomElementAttribute}.
294
+ *
295
+ * @param domElement The DOM element the attribute should be set on.
296
+ * @param key The name of the attribute.
297
+ * @param value The value of the attribute.
298
+ * @param relatedViewElement The view element related to the `domElement` (if there is any).
299
+ * It helps decide whether the attribute set is unsafe. For instance, view elements created via the
300
+ * {@link module:engine/view/downcastwriter~DowncastWriter} methods can allow certain attributes that would normally be filtered out.
301
+ */
302
+ setDomElementAttribute(domElement, key, value, relatedViewElement) {
303
+ const shouldRenderAttribute = this.shouldRenderAttribute(key, value, domElement.tagName.toLowerCase()) ||
304
+ relatedViewElement && relatedViewElement.shouldRenderUnsafeAttribute(key);
305
+ if (!shouldRenderAttribute) {
306
+ logWarning('domconverter-unsafe-attribute-detected', { domElement, key, value });
307
+ }
308
+ if (!isValidAttributeName(key)) {
309
+ /**
310
+ * Invalid attribute name was ignored during rendering.
311
+ *
312
+ * @error domconverter-invalid-attribute-detected
313
+ */
314
+ logWarning('domconverter-invalid-attribute-detected', { domElement, key, value });
315
+ return;
316
+ }
317
+ // The old value was safe but the new value is unsafe.
318
+ if (domElement.hasAttribute(key) && !shouldRenderAttribute) {
319
+ domElement.removeAttribute(key);
320
+ }
321
+ // The old value was unsafe (but prefixed) but the new value will be safe (will be unprefixed).
322
+ else if (domElement.hasAttribute(UNSAFE_ATTRIBUTE_NAME_PREFIX + key) && shouldRenderAttribute) {
323
+ domElement.removeAttribute(UNSAFE_ATTRIBUTE_NAME_PREFIX + key);
324
+ }
325
+ // If the attribute should not be rendered, rename it (instead of removing) to give developers some idea of what
326
+ // is going on (https://github.com/ckeditor/ckeditor5/issues/10801).
327
+ domElement.setAttribute(shouldRenderAttribute ? key : UNSAFE_ATTRIBUTE_NAME_PREFIX + key, value);
328
+ }
329
+ /**
330
+ * Removes an attribute from a DOM element.
331
+ *
332
+ * **Note**: To set the attribute, use {@link #setDomElementAttribute}.
333
+ *
334
+ * @param domElement The DOM element the attribute should be removed from.
335
+ * @param key The name of the attribute.
336
+ */
337
+ removeDomElementAttribute(domElement, key) {
338
+ // See #_createReplacementDomElement() to learn what this is.
339
+ if (key == UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE) {
340
+ return;
341
+ }
342
+ domElement.removeAttribute(key);
343
+ // See setDomElementAttribute() to learn what this is.
344
+ domElement.removeAttribute(UNSAFE_ATTRIBUTE_NAME_PREFIX + key);
345
+ }
346
+ /**
347
+ * Converts children of the view element to DOM using the
348
+ * {@link module:engine/view/domconverter~DomConverter#viewToDom} method.
349
+ * Additionally, this method adds block {@link module:engine/view/filler filler} to the list of children, if needed.
350
+ *
351
+ * @param viewElement Parent view element.
352
+ * @param options See {@link module:engine/view/domconverter~DomConverter#viewToDom} options parameter.
353
+ * @returns DOM nodes.
354
+ */
355
+ *viewChildrenToDom(viewElement, options = {}) {
356
+ const fillerPositionOffset = viewElement.getFillerOffset && viewElement.getFillerOffset();
357
+ let offset = 0;
358
+ for (const childView of viewElement.getChildren()) {
359
+ if (fillerPositionOffset === offset) {
360
+ yield this._getBlockFiller();
361
+ }
362
+ const transparentRendering = childView.is('element') &&
363
+ !!childView.getCustomProperty('dataPipeline:transparentRendering') &&
364
+ !first(childView.getAttributes());
365
+ if (transparentRendering && this.renderingMode == 'data') {
366
+ yield* this.viewChildrenToDom(childView, options);
367
+ }
368
+ else {
369
+ if (transparentRendering) {
370
+ /**
371
+ * The `dataPipeline:transparentRendering` flag is supported only in the data pipeline.
372
+ *
373
+ * @error domconverter-transparent-rendering-unsupported-in-editing-pipeline
374
+ */
375
+ logWarning('domconverter-transparent-rendering-unsupported-in-editing-pipeline', { viewElement: childView });
376
+ }
377
+ yield this.viewToDom(childView, options);
378
+ }
379
+ offset++;
380
+ }
381
+ if (fillerPositionOffset === offset) {
382
+ yield this._getBlockFiller();
383
+ }
384
+ }
385
+ /**
386
+ * Converts view {@link module:engine/view/range~Range} to DOM range.
387
+ * Inline and block {@link module:engine/view/filler fillers} are handled during the conversion.
388
+ *
389
+ * @param viewRange View range.
390
+ * @returns DOM range.
391
+ */
392
+ viewRangeToDom(viewRange) {
393
+ const domStart = this.viewPositionToDom(viewRange.start);
394
+ const domEnd = this.viewPositionToDom(viewRange.end);
395
+ const domRange = this._domDocument.createRange();
396
+ domRange.setStart(domStart.parent, domStart.offset);
397
+ domRange.setEnd(domEnd.parent, domEnd.offset);
398
+ return domRange;
399
+ }
400
+ /**
401
+ * Converts view {@link module:engine/view/position~Position} to DOM parent and offset.
402
+ *
403
+ * Inline and block {@link module:engine/view/filler fillers} are handled during the conversion.
404
+ * If the converted position is directly before inline filler it is moved inside the filler.
405
+ *
406
+ * @param viewPosition View position.
407
+ * @returns DOM position or `null` if view position could not be converted to DOM.
408
+ * DOM position has two properties:
409
+ * * `parent` - DOM position parent.
410
+ * * `offset` - DOM position offset.
411
+ */
412
+ viewPositionToDom(viewPosition) {
413
+ const viewParent = viewPosition.parent;
414
+ if (viewParent.is('$text')) {
415
+ const domParent = this.findCorrespondingDomText(viewParent);
416
+ if (!domParent) {
417
+ // Position is in a view text node that has not been rendered to DOM yet.
418
+ return null;
419
+ }
420
+ let offset = viewPosition.offset;
421
+ if (startsWithFiller(domParent)) {
422
+ offset += INLINE_FILLER_LENGTH;
423
+ }
424
+ return { parent: domParent, offset };
425
+ }
426
+ else {
427
+ // viewParent is instance of ViewElement.
428
+ let domParent, domBefore, domAfter;
429
+ if (viewPosition.offset === 0) {
430
+ domParent = this.mapViewToDom(viewParent);
431
+ if (!domParent) {
432
+ // Position is in a view element that has not been rendered to DOM yet.
433
+ return null;
434
+ }
435
+ domAfter = domParent.childNodes[0];
436
+ }
437
+ else {
438
+ const nodeBefore = viewPosition.nodeBefore;
439
+ domBefore = nodeBefore.is('$text') ?
440
+ this.findCorrespondingDomText(nodeBefore) :
441
+ this.mapViewToDom(nodeBefore);
442
+ if (!domBefore) {
443
+ // Position is after a view element that has not been rendered to DOM yet.
444
+ return null;
445
+ }
446
+ domParent = domBefore.parentNode;
447
+ domAfter = domBefore.nextSibling;
448
+ }
449
+ // If there is an inline filler at position return position inside the filler. We should never return
450
+ // the position before the inline filler.
451
+ if (isText(domAfter) && startsWithFiller(domAfter)) {
452
+ return { parent: domAfter, offset: INLINE_FILLER_LENGTH };
453
+ }
454
+ const offset = domBefore ? indexOf(domBefore) + 1 : 0;
455
+ return { parent: domParent, offset };
456
+ }
457
+ }
458
+ /**
459
+ * Converts DOM to view. For all text nodes, not bound elements and document fragments new items will
460
+ * be created. For bound elements and document fragments function will return corresponding items. For
461
+ * {@link module:engine/view/filler fillers} `null` will be returned.
462
+ * For all DOM elements rendered by {@link module:engine/view/uielement~UIElement} that UIElement will be returned.
463
+ *
464
+ * @param domNode DOM node or document fragment to transform.
465
+ * @param options Conversion options.
466
+ * @param options.bind Determines whether new elements will be bound. False by default.
467
+ * @param options.withChildren If `true`, node's and document fragment's children will be converted too. True by default.
468
+ * @param options.keepOriginalCase If `false`, node's tag name will be converted to lower case. False by default.
469
+ * @param options.skipComments If `false`, comment nodes will be converted to `$comment`
470
+ * {@link module:engine/view/uielement~UIElement view UI elements}. False by default.
471
+ * @returns Converted node or document fragment or `null` if DOM node is a {@link module:engine/view/filler filler}
472
+ * or the given node is an empty text node.
473
+ */
474
+ domToView(domNode, options = {}) {
475
+ const inlineNodes = [];
476
+ const generator = this._domToView(domNode, options, inlineNodes);
477
+ // Get the first yielded value or a returned value.
478
+ const node = generator.next().value;
479
+ if (!node) {
480
+ return null;
481
+ }
482
+ // Trigger children handling.
483
+ generator.next();
484
+ // Whitespace cleaning.
485
+ this._processDomInlineNodes(null, inlineNodes, options);
486
+ // Text not got trimmed to an empty string so there is no result node.
487
+ if (node.is('$text') && node.data.length == 0) {
488
+ return null;
489
+ }
490
+ return node;
491
+ }
492
+ /**
493
+ * Converts children of the DOM element to view nodes using
494
+ * the {@link module:engine/view/domconverter~DomConverter#domToView} method.
495
+ * Additionally this method omits block {@link module:engine/view/filler filler}, if it exists in the DOM parent.
496
+ *
497
+ * @param domElement Parent DOM element.
498
+ * @param options See {@link module:engine/view/domconverter~DomConverter#domToView} options parameter.
499
+ * @param inlineNodes An array that will be populated with inline nodes. It's used internally for whitespace processing.
500
+ * @returns View nodes.
501
+ */
502
+ *domChildrenToView(domElement, options = {}, inlineNodes = []) {
503
+ for (let i = 0; i < domElement.childNodes.length; i++) {
504
+ const domChild = domElement.childNodes[i];
505
+ const generator = this._domToView(domChild, options, inlineNodes);
506
+ // Get the first yielded value or a returned value.
507
+ const viewChild = generator.next().value;
508
+ if (viewChild !== null) {
509
+ // Whitespace cleaning before entering a block element (between block elements).
510
+ if (this._isBlockViewElement(viewChild)) {
511
+ this._processDomInlineNodes(domElement, inlineNodes, options);
512
+ }
513
+ yield viewChild;
514
+ // Trigger children handling.
515
+ generator.next();
516
+ }
517
+ }
518
+ // Whitespace cleaning before leaving a block element (content of block element).
519
+ this._processDomInlineNodes(domElement, inlineNodes, options);
520
+ }
521
+ /**
522
+ * Converts DOM selection to view {@link module:engine/view/selection~Selection}.
523
+ * Ranges which cannot be converted will be omitted.
524
+ *
525
+ * @param domSelection DOM selection.
526
+ * @returns View selection.
527
+ */
528
+ domSelectionToView(domSelection) {
529
+ // See: https://github.com/ckeditor/ckeditor5/issues/9635.
530
+ if (isGeckoRestrictedDomSelection(domSelection)) {
531
+ return new ViewSelection([]);
532
+ }
533
+ // DOM selection might be placed in fake selection container.
534
+ // If container contains fake selection - return corresponding view selection.
535
+ if (domSelection.rangeCount === 1) {
536
+ let container = domSelection.getRangeAt(0).startContainer;
537
+ // The DOM selection might be moved to the text node inside the fake selection container.
538
+ if (isText(container)) {
539
+ container = container.parentNode;
540
+ }
541
+ const viewSelection = this.fakeSelectionToView(container);
542
+ if (viewSelection) {
543
+ return viewSelection;
544
+ }
545
+ }
546
+ const isBackward = this.isDomSelectionBackward(domSelection);
547
+ const viewRanges = [];
548
+ for (let i = 0; i < domSelection.rangeCount; i++) {
549
+ // DOM Range have correct start and end, no matter what is the DOM Selection direction. So we don't have to fix anything.
550
+ const domRange = domSelection.getRangeAt(i);
551
+ const viewRange = this.domRangeToView(domRange);
552
+ if (viewRange) {
553
+ viewRanges.push(viewRange);
554
+ }
555
+ }
556
+ return new ViewSelection(viewRanges, { backward: isBackward });
557
+ }
558
+ /**
559
+ * Converts DOM Range to view {@link module:engine/view/range~Range}.
560
+ * If the start or end position can not be converted `null` is returned.
561
+ *
562
+ * @param domRange DOM range.
563
+ * @returns View range.
564
+ */
565
+ domRangeToView(domRange) {
566
+ const viewStart = this.domPositionToView(domRange.startContainer, domRange.startOffset);
567
+ const viewEnd = this.domPositionToView(domRange.endContainer, domRange.endOffset);
568
+ if (viewStart && viewEnd) {
569
+ return new ViewRange(viewStart, viewEnd);
570
+ }
571
+ return null;
572
+ }
573
+ /**
574
+ * Converts DOM parent and offset to view {@link module:engine/view/position~Position}.
575
+ *
576
+ * If the position is inside a {@link module:engine/view/filler filler} which has no corresponding view node,
577
+ * position of the filler will be converted and returned.
578
+ *
579
+ * If the position is inside DOM element rendered by {@link module:engine/view/uielement~UIElement}
580
+ * that position will be converted to view position before that UIElement.
581
+ *
582
+ * If structures are too different and it is not possible to find corresponding position then `null` will be returned.
583
+ *
584
+ * @param domParent DOM position parent.
585
+ * @param domOffset DOM position offset. You can skip it when converting the inline filler node.
586
+ * @returns View position.
587
+ */
588
+ domPositionToView(domParent, domOffset = 0) {
589
+ if (this.isBlockFiller(domParent)) {
590
+ return this.domPositionToView(domParent.parentNode, indexOf(domParent));
591
+ }
592
+ // If position is somewhere inside UIElement or a RawElement - return position before that element.
593
+ const viewElement = this.mapDomToView(domParent);
594
+ if (viewElement && (viewElement.is('uiElement') || viewElement.is('rawElement'))) {
595
+ return ViewPosition._createBefore(viewElement);
596
+ }
597
+ if (isText(domParent)) {
598
+ if (isInlineFiller(domParent)) {
599
+ return this.domPositionToView(domParent.parentNode, indexOf(domParent));
600
+ }
601
+ const viewParent = this.findCorrespondingViewText(domParent);
602
+ let offset = domOffset;
603
+ if (!viewParent) {
604
+ return null;
605
+ }
606
+ if (startsWithFiller(domParent)) {
607
+ offset -= INLINE_FILLER_LENGTH;
608
+ offset = offset < 0 ? 0 : offset;
609
+ }
610
+ return new ViewPosition(viewParent, offset);
611
+ }
612
+ // domParent instanceof HTMLElement.
613
+ else {
614
+ if (domOffset === 0) {
615
+ const viewParent = this.mapDomToView(domParent);
616
+ if (viewParent) {
617
+ return new ViewPosition(viewParent, 0);
618
+ }
619
+ }
620
+ else {
621
+ const domBefore = domParent.childNodes[domOffset - 1];
622
+ // Jump over an inline filler (and also on Firefox jump over a block filler while pressing backspace in an empty paragraph).
623
+ if (isText(domBefore) && isInlineFiller(domBefore) || domBefore && this.isBlockFiller(domBefore)) {
624
+ return this.domPositionToView(domBefore.parentNode, indexOf(domBefore));
625
+ }
626
+ const viewBefore = isText(domBefore) ?
627
+ this.findCorrespondingViewText(domBefore) :
628
+ this.mapDomToView(domBefore);
629
+ // TODO #663
630
+ if (viewBefore && viewBefore.parent) {
631
+ return new ViewPosition(viewBefore.parent, viewBefore.index + 1);
632
+ }
633
+ }
634
+ return null;
635
+ }
636
+ }
637
+ /**
638
+ * Returns corresponding view {@link module:engine/view/element~Element Element} or
639
+ * {@link module:engine/view/documentfragment~DocumentFragment} for provided DOM element or
640
+ * document fragment. If there is no view item {@link module:engine/view/domconverter~DomConverter#bindElements bound}
641
+ * to the given DOM - `undefined` is returned.
642
+ *
643
+ * For all DOM elements rendered by a {@link module:engine/view/uielement~UIElement} or
644
+ * a {@link module:engine/view/rawelement~RawElement}, the parent `UIElement` or `RawElement` will be returned.
645
+ *
646
+ * @param domElementOrDocumentFragment DOM element or document fragment.
647
+ * @returns Corresponding view element, document fragment or `undefined` if no element was bound.
648
+ */
649
+ mapDomToView(domElementOrDocumentFragment) {
650
+ const hostElement = this.getHostViewElement(domElementOrDocumentFragment);
651
+ return hostElement || this._domToViewMapping.get(domElementOrDocumentFragment);
652
+ }
653
+ /**
654
+ * Finds corresponding text node. Text nodes are not {@link module:engine/view/domconverter~DomConverter#bindElements bound},
655
+ * corresponding text node is returned based on the sibling or parent.
656
+ *
657
+ * If the directly previous sibling is a {@link module:engine/view/domconverter~DomConverter#bindElements bound} element, it is used
658
+ * to find the corresponding text node.
659
+ *
660
+ * If this is a first child in the parent and the parent is a {@link module:engine/view/domconverter~DomConverter#bindElements bound}
661
+ * element, it is used to find the corresponding text node.
662
+ *
663
+ * For all text nodes rendered by a {@link module:engine/view/uielement~UIElement} or
664
+ * a {@link module:engine/view/rawelement~RawElement}, the parent `UIElement` or `RawElement` will be returned.
665
+ *
666
+ * Otherwise `null` is returned.
667
+ *
668
+ * Note that for the block or inline {@link module:engine/view/filler filler} this method returns `null`.
669
+ *
670
+ * @param domText DOM text node.
671
+ * @returns Corresponding view text node or `null`, if it was not possible to find a corresponding node.
672
+ */
673
+ findCorrespondingViewText(domText) {
674
+ if (isInlineFiller(domText)) {
675
+ return null;
676
+ }
677
+ // If DOM text was rendered by a UIElement or a RawElement - return this parent element.
678
+ const hostElement = this.getHostViewElement(domText);
679
+ if (hostElement) {
680
+ return hostElement;
681
+ }
682
+ const previousSibling = domText.previousSibling;
683
+ // Try to use previous sibling to find the corresponding text node.
684
+ if (previousSibling) {
685
+ if (!(this.isElement(previousSibling))) {
686
+ // The previous is text or comment.
687
+ return null;
688
+ }
689
+ const viewElement = this.mapDomToView(previousSibling);
690
+ if (viewElement) {
691
+ const nextSibling = viewElement.nextSibling;
692
+ // It might be filler which has no corresponding view node.
693
+ if (nextSibling instanceof ViewText) {
694
+ return nextSibling;
695
+ }
696
+ else {
697
+ return null;
698
+ }
699
+ }
700
+ }
701
+ // Try to use parent to find the corresponding text node.
702
+ else {
703
+ const viewElement = this.mapDomToView(domText.parentNode);
704
+ if (viewElement) {
705
+ const firstChild = viewElement.getChild(0);
706
+ // It might be filler which has no corresponding view node.
707
+ if (firstChild instanceof ViewText) {
708
+ return firstChild;
709
+ }
710
+ else {
711
+ return null;
712
+ }
713
+ }
714
+ }
715
+ return null;
716
+ }
717
+ mapViewToDom(documentFragmentOrElement) {
718
+ return this._viewToDomMapping.get(documentFragmentOrElement);
719
+ }
720
+ /**
721
+ * Finds corresponding text node. Text nodes are not {@link module:engine/view/domconverter~DomConverter#bindElements bound},
722
+ * corresponding text node is returned based on the sibling or parent.
723
+ *
724
+ * If the directly previous sibling is a {@link module:engine/view/domconverter~DomConverter#bindElements bound} element, it is used
725
+ * to find the corresponding text node.
726
+ *
727
+ * If this is a first child in the parent and the parent is a {@link module:engine/view/domconverter~DomConverter#bindElements bound}
728
+ * element, it is used to find the corresponding text node.
729
+ *
730
+ * Otherwise `null` is returned.
731
+ *
732
+ * @param viewText View text node.
733
+ * @returns Corresponding DOM text node or `null`, if it was not possible to find a corresponding node.
734
+ */
735
+ findCorrespondingDomText(viewText) {
736
+ const previousSibling = viewText.previousSibling;
737
+ // Try to use previous sibling to find the corresponding text node.
738
+ if (previousSibling && this.mapViewToDom(previousSibling)) {
739
+ return this.mapViewToDom(previousSibling).nextSibling;
740
+ }
741
+ // If this is a first node, try to use parent to find the corresponding text node.
742
+ if (!previousSibling && viewText.parent && this.mapViewToDom(viewText.parent)) {
743
+ return this.mapViewToDom(viewText.parent).childNodes[0];
744
+ }
745
+ return null;
746
+ }
747
+ /**
748
+ * Focuses DOM editable that is corresponding to provided {@link module:engine/view/editableelement~EditableElement}.
749
+ */
750
+ focus(viewEditable) {
751
+ const domEditable = this.mapViewToDom(viewEditable);
752
+ if (domEditable && domEditable.ownerDocument.activeElement !== domEditable) {
753
+ // Save the scrollX and scrollY positions before the focus.
754
+ const { scrollX, scrollY } = global.window;
755
+ const scrollPositions = [];
756
+ // Save all scrollLeft and scrollTop values starting from domEditable up to
757
+ // document#documentElement.
758
+ forEachDomElementAncestor(domEditable, node => {
759
+ const { scrollLeft, scrollTop } = node;
760
+ scrollPositions.push([scrollLeft, scrollTop]);
761
+ });
762
+ domEditable.focus();
763
+ // Restore scrollLeft and scrollTop values starting from domEditable up to
764
+ // document#documentElement.
765
+ // https://github.com/ckeditor/ckeditor5-engine/issues/951
766
+ // https://github.com/ckeditor/ckeditor5-engine/issues/957
767
+ forEachDomElementAncestor(domEditable, node => {
768
+ const [scrollLeft, scrollTop] = scrollPositions.shift();
769
+ node.scrollLeft = scrollLeft;
770
+ node.scrollTop = scrollTop;
771
+ });
772
+ // Restore the scrollX and scrollY positions after the focus.
773
+ // https://github.com/ckeditor/ckeditor5-engine/issues/951
774
+ global.window.scrollTo(scrollX, scrollY);
775
+ }
776
+ }
777
+ /**
778
+ * Remove DOM selection from blurred editable, so it won't interfere with clicking on dropdowns (especially on iOS).
779
+ *
780
+ * @internal
781
+ */
782
+ _clearDomSelection() {
783
+ const domEditable = this.mapViewToDom(this.document.selection.editableElement);
784
+ if (!domEditable) {
785
+ return;
786
+ }
787
+ // Check if DOM selection is inside editor editable element.
788
+ const domSelection = domEditable.ownerDocument.defaultView.getSelection();
789
+ const newViewSelection = this.domSelectionToView(domSelection);
790
+ const selectionInEditable = newViewSelection && newViewSelection.rangeCount > 0;
791
+ if (selectionInEditable) {
792
+ domSelection.removeAllRanges();
793
+ }
794
+ }
795
+ /**
796
+ * Returns `true` when `node.nodeType` equals `Node.ELEMENT_NODE`.
797
+ *
798
+ * @param node Node to check.
799
+ */
800
+ isElement(node) {
801
+ return node && node.nodeType == Node.ELEMENT_NODE;
802
+ }
803
+ /**
804
+ * Returns `true` when `node.nodeType` equals `Node.DOCUMENT_FRAGMENT_NODE`.
805
+ *
806
+ * @param node Node to check.
807
+ */
808
+ isDocumentFragment(node) {
809
+ return node && node.nodeType == Node.DOCUMENT_FRAGMENT_NODE;
810
+ }
811
+ /**
812
+ * Checks if the node is an instance of the block filler for this DOM converter.
813
+ *
814
+ * ```ts
815
+ * const converter = new DomConverter( viewDocument, { blockFillerMode: 'br' } );
816
+ *
817
+ * converter.isBlockFiller( BR_FILLER( document ) ); // true
818
+ * converter.isBlockFiller( NBSP_FILLER( document ) ); // false
819
+ * ```
820
+ *
821
+ * **Note:**: For the `'nbsp'` mode the method also checks context of a node so it cannot be a detached node.
822
+ *
823
+ * **Note:** A special case in the `'nbsp'` mode exists where the `<br>` in `<p><br></p>` is treated as a block filler.
824
+ *
825
+ * @param domNode DOM node to check.
826
+ * @returns True if a node is considered a block filler for given mode.
827
+ */
828
+ isBlockFiller(domNode) {
829
+ if (this.blockFillerMode == 'br') {
830
+ return domNode.isEqualNode(BR_FILLER_REF);
831
+ }
832
+ // Special case for <p><br></p> in which <br> should be treated as filler even when we are not in the 'br' mode. See ckeditor5#5564.
833
+ if (domNode.tagName === 'BR' &&
834
+ hasBlockParent(domNode, this.blockElements) &&
835
+ domNode.parentNode.childNodes.length === 1) {
836
+ return true;
837
+ }
838
+ // If not in 'br' mode, try recognizing both marked and regular nbsp block fillers.
839
+ return domNode.isEqualNode(MARKED_NBSP_FILLER_REF) || isNbspBlockFiller(domNode, this.blockElements);
840
+ }
841
+ /**
842
+ * Returns `true` if given selection is a backward selection, that is, if it's `focus` is before `anchor`.
843
+ *
844
+ * @param DOM Selection instance to check.
845
+ */
846
+ isDomSelectionBackward(selection) {
847
+ if (selection.isCollapsed) {
848
+ return false;
849
+ }
850
+ // Since it takes multiple lines of code to check whether a "DOM Position" is before/after another "DOM Position",
851
+ // we will use the fact that range will collapse if it's end is before it's start.
852
+ const range = this._domDocument.createRange();
853
+ try {
854
+ range.setStart(selection.anchorNode, selection.anchorOffset);
855
+ range.setEnd(selection.focusNode, selection.focusOffset);
856
+ }
857
+ catch (e) {
858
+ // Safari sometimes gives us a selection that makes Range.set{Start,End} throw.
859
+ // See https://github.com/ckeditor/ckeditor5/issues/12375.
860
+ return false;
861
+ }
862
+ const backward = range.collapsed;
863
+ range.detach();
864
+ return backward;
865
+ }
866
+ /**
867
+ * Returns a parent {@link module:engine/view/uielement~UIElement} or {@link module:engine/view/rawelement~RawElement}
868
+ * that hosts the provided DOM node. Returns `null` if there is no such parent.
869
+ */
870
+ getHostViewElement(domNode) {
871
+ const ancestors = getAncestors(domNode);
872
+ // Remove domNode from the list.
873
+ ancestors.pop();
874
+ while (ancestors.length) {
875
+ const domNode = ancestors.pop();
876
+ const viewNode = this._domToViewMapping.get(domNode);
877
+ if (viewNode && (viewNode.is('uiElement') || viewNode.is('rawElement'))) {
878
+ return viewNode;
879
+ }
880
+ }
881
+ return null;
882
+ }
883
+ /**
884
+ * Checks if the given selection's boundaries are at correct places.
885
+ *
886
+ * The following places are considered as incorrect for selection boundaries:
887
+ *
888
+ * * before or in the middle of an inline filler sequence,
889
+ * * inside a DOM element which represents {@link module:engine/view/uielement~UIElement a view UI element},
890
+ * * inside a DOM element which represents {@link module:engine/view/rawelement~RawElement a view raw element}.
891
+ *
892
+ * @param domSelection The DOM selection object to be checked.
893
+ * @returns `true` if the given selection is at a correct place, `false` otherwise.
894
+ */
895
+ isDomSelectionCorrect(domSelection) {
896
+ return this._isDomSelectionPositionCorrect(domSelection.anchorNode, domSelection.anchorOffset) &&
897
+ this._isDomSelectionPositionCorrect(domSelection.focusNode, domSelection.focusOffset);
898
+ }
899
+ /**
900
+ * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as raw data
901
+ * and not processed during the conversion from DOM nodes to view elements.
902
+ *
903
+ * This is affecting how {@link module:engine/view/domconverter~DomConverter#domToView} and
904
+ * {@link module:engine/view/domconverter~DomConverter#domChildrenToView} process DOM nodes.
905
+ *
906
+ * The raw data can be later accessed by a
907
+ * {@link module:engine/view/element~Element#getCustomProperty custom property of a view element} called `"$rawContent"`.
908
+ *
909
+ * @param pattern Pattern matching a view element whose content should
910
+ * be treated as raw data.
911
+ */
912
+ registerRawContentMatcher(pattern) {
913
+ this._rawContentElementMatcher.add(pattern);
914
+ }
915
+ /**
916
+ * Registers a {@link module:engine/view/matcher~MatcherPattern} for inline object view elements.
917
+ *
918
+ * This is affecting how {@link module:engine/view/domconverter~DomConverter#domToView} and
919
+ * {@link module:engine/view/domconverter~DomConverter#domChildrenToView} process DOM nodes.
920
+ *
921
+ * This is an extension of a simple {@link #inlineObjectElements} array of element names.
922
+ *
923
+ * @param pattern Pattern matching a view element which should be treated as an inline object.
924
+ */
925
+ registerInlineObjectMatcher(pattern) {
926
+ this._inlineObjectElementMatcher.add(pattern);
927
+ }
928
+ /**
929
+ * Returns the block {@link module:engine/view/filler filler} node based on the current {@link #blockFillerMode} setting.
930
+ */
931
+ _getBlockFiller() {
932
+ switch (this.blockFillerMode) {
933
+ case 'nbsp':
934
+ return NBSP_FILLER(this._domDocument); // eslint-disable-line new-cap
935
+ case 'markedNbsp':
936
+ return MARKED_NBSP_FILLER(this._domDocument); // eslint-disable-line new-cap
937
+ case 'br':
938
+ return BR_FILLER(this._domDocument); // eslint-disable-line new-cap
939
+ }
940
+ }
941
+ /**
942
+ * Checks if the given DOM position is a correct place for selection boundary. See {@link #isDomSelectionCorrect}.
943
+ *
944
+ * @param domParent Position parent.
945
+ * @param offset Position offset.
946
+ * @returns `true` if given position is at a correct place for selection boundary, `false` otherwise.
947
+ */
948
+ _isDomSelectionPositionCorrect(domParent, offset) {
949
+ // If selection is before or in the middle of inline filler string, it is incorrect.
950
+ if (isText(domParent) && startsWithFiller(domParent) && offset < INLINE_FILLER_LENGTH) {
951
+ // Selection in a text node, at wrong position (before or in the middle of filler).
952
+ return false;
953
+ }
954
+ if (this.isElement(domParent) && startsWithFiller(domParent.childNodes[offset])) {
955
+ // Selection in an element node, before filler text node.
956
+ return false;
957
+ }
958
+ const viewParent = this.mapDomToView(domParent);
959
+ // The position is incorrect when anchored inside a UIElement or a RawElement.
960
+ // Note: In case of UIElement and RawElement, mapDomToView() returns a parent element for any DOM child
961
+ // so there's no need to perform any additional checks.
962
+ if (viewParent && (viewParent.is('uiElement') || viewParent.is('rawElement'))) {
963
+ return false;
964
+ }
965
+ return true;
966
+ }
967
+ /**
968
+ * Internal generator for {@link #domToView}. Also used by {@link #domChildrenToView}.
969
+ * Separates DOM nodes conversion from whitespaces processing.
970
+ *
971
+ * @param domNode DOM node or document fragment to transform.
972
+ * @param inlineNodes An array of recently encountered inline nodes truncated to the block element boundaries.
973
+ * Used later to process whitespaces.
974
+ */
975
+ *_domToView(domNode, options, inlineNodes) {
976
+ if (this.isBlockFiller(domNode)) {
977
+ return null;
978
+ }
979
+ // When node is inside a UIElement or a RawElement return that parent as it's view representation.
980
+ const hostElement = this.getHostViewElement(domNode);
981
+ if (hostElement) {
982
+ return hostElement;
983
+ }
984
+ if (isComment(domNode) && options.skipComments) {
985
+ return null;
986
+ }
987
+ if (isText(domNode)) {
988
+ if (isInlineFiller(domNode)) {
989
+ return null;
990
+ }
991
+ else {
992
+ const textData = domNode.data;
993
+ if (textData === '') {
994
+ return null;
995
+ }
996
+ const textNode = new ViewText(this.document, textData);
997
+ inlineNodes.push(textNode);
998
+ return textNode;
999
+ }
1000
+ }
1001
+ else {
1002
+ let viewElement = this.mapDomToView(domNode);
1003
+ if (viewElement) {
1004
+ if (this._isInlineObjectElement(viewElement)) {
1005
+ inlineNodes.push(viewElement);
1006
+ }
1007
+ return viewElement;
1008
+ }
1009
+ if (this.isDocumentFragment(domNode)) {
1010
+ // Create view document fragment.
1011
+ viewElement = new ViewDocumentFragment(this.document);
1012
+ if (options.bind) {
1013
+ this.bindDocumentFragments(domNode, viewElement);
1014
+ }
1015
+ }
1016
+ else {
1017
+ // Create view element.
1018
+ viewElement = this._createViewElement(domNode, options);
1019
+ if (options.bind) {
1020
+ this.bindElements(domNode, viewElement);
1021
+ }
1022
+ // Copy element's attributes.
1023
+ const attrs = domNode.attributes;
1024
+ if (attrs) {
1025
+ for (let l = attrs.length, i = 0; i < l; i++) {
1026
+ viewElement._setAttribute(attrs[i].name, attrs[i].value);
1027
+ }
1028
+ }
1029
+ // Treat this element's content as a raw data if it was registered as such.
1030
+ if (this._isViewElementWithRawContent(viewElement, options)) {
1031
+ viewElement._setCustomProperty('$rawContent', domNode.innerHTML);
1032
+ if (!this._isBlockViewElement(viewElement)) {
1033
+ inlineNodes.push(viewElement);
1034
+ }
1035
+ return viewElement;
1036
+ }
1037
+ // Comment node is also treated as an element with raw data.
1038
+ if (isComment(domNode)) {
1039
+ viewElement._setCustomProperty('$rawContent', domNode.data);
1040
+ return viewElement;
1041
+ }
1042
+ }
1043
+ // Yield the element first so the flow of nested inline nodes is not reversed inside elements.
1044
+ yield viewElement;
1045
+ const nestedInlineNodes = [];
1046
+ if (options.withChildren !== false) {
1047
+ for (const child of this.domChildrenToView(domNode, options, nestedInlineNodes)) {
1048
+ viewElement._appendChild(child);
1049
+ }
1050
+ }
1051
+ // Check if this is an inline object after processing child nodes so matcher
1052
+ // for inline objects can verify if the element is empty.
1053
+ if (this._isInlineObjectElement(viewElement)) {
1054
+ inlineNodes.push(viewElement);
1055
+ }
1056
+ else {
1057
+ // It's an inline element that is not an object (like <b>, <i>) or a block element.
1058
+ for (const inlineNode of nestedInlineNodes) {
1059
+ inlineNodes.push(inlineNode);
1060
+ }
1061
+ }
1062
+ }
1063
+ }
1064
+ /**
1065
+ * Internal helper that walks the list of inline view nodes already generated from DOM nodes
1066
+ * and handles whitespaces and NBSPs.
1067
+ *
1068
+ * @param domParent The DOM parent of the given inline nodes. This should be a document fragment or
1069
+ * a block element to whitespace processing start cleaning.
1070
+ * @param inlineNodes An array of recently encountered inline nodes truncated to the block element boundaries.
1071
+ */
1072
+ _processDomInlineNodes(domParent, inlineNodes, options) {
1073
+ if (!inlineNodes.length) {
1074
+ return;
1075
+ }
1076
+ // Process text nodes only after reaching a block or document fragment,
1077
+ // do not alter whitespaces while processing an inline element like <b> or <i>.
1078
+ if (domParent && !this.isDocumentFragment(domParent) && !this._isBlockDomElement(domParent)) {
1079
+ return;
1080
+ }
1081
+ let prevNodeEndsWithSpace = false;
1082
+ for (let i = 0; i < inlineNodes.length; i++) {
1083
+ const node = inlineNodes[i];
1084
+ if (!node.is('$text')) {
1085
+ prevNodeEndsWithSpace = false;
1086
+ continue;
1087
+ }
1088
+ let data;
1089
+ let nodeEndsWithSpace = false;
1090
+ if (_hasViewParentOfType(node, this.preElements)) {
1091
+ data = getDataWithoutFiller(node.data);
1092
+ }
1093
+ else {
1094
+ // Change all consecutive whitespace characters (from the [ \n\t\r] set
1095
+ // see https://github.com/ckeditor/ckeditor5-engine/issues/822#issuecomment-311670249) to a single space character.
1096
+ // That's how multiple whitespaces are treated when rendered, so we normalize those whitespaces.
1097
+ // We're replacing 1+ (and not 2+) to also normalize singular \n\t\r characters (#822).
1098
+ data = node.data.replace(/[ \n\t\r]{1,}/g, ' ');
1099
+ nodeEndsWithSpace = /[^\S\u00A0]/.test(data.charAt(data.length - 1));
1100
+ const prevNode = i > 0 ? inlineNodes[i - 1] : null;
1101
+ const nextNode = i + 1 < inlineNodes.length ? inlineNodes[i + 1] : null;
1102
+ const shouldLeftTrim = !prevNode || prevNode.is('element') && prevNode.name == 'br' || prevNodeEndsWithSpace;
1103
+ const shouldRightTrim = nextNode ? false : !startsWithFiller(node.data);
1104
+ // Do not try to clear whitespaces if this is flat mapping for the purpose of mutation observer and differ in rendering.
1105
+ if (options.withChildren !== false) {
1106
+ // If the previous dom text node does not exist or it ends by whitespace character, remove space character from the
1107
+ // beginning of this text node. Such space character is treated as a whitespace.
1108
+ if (shouldLeftTrim) {
1109
+ data = data.replace(/^ /, '');
1110
+ }
1111
+ // If the next text node does not exist remove space character from the end of this text node.
1112
+ if (shouldRightTrim) {
1113
+ data = data.replace(/ $/, '');
1114
+ }
1115
+ }
1116
+ // At the beginning and end of a block element, Firefox inserts normal space + <br> instead of non-breaking space.
1117
+ // This means that the text node starts/end with normal space instead of non-breaking space.
1118
+ // This causes a problem because the normal space would be removed in `.replace` calls above. To prevent that,
1119
+ // the inline filler is removed only after the data is initially processed (by the `.replace` above). See ckeditor5#692.
1120
+ data = getDataWithoutFiller(data);
1121
+ // At this point we should have removed all whitespaces from DOM text data.
1122
+ //
1123
+ // Now, We will reverse the process that happens in `_processDataFromViewText`.
1124
+ //
1125
+ // We have to change &nbsp; chars, that were in DOM text data because of rendering reasons, to spaces.
1126
+ // First, change all ` \u00A0` pairs (space + &nbsp;) to two spaces. DOM converter changes two spaces from model/view to
1127
+ // ` \u00A0` to ensure proper rendering. Since here we convert back, we recognize those pairs and change them back to ` `.
1128
+ data = data.replace(/ \u00A0/g, ' ');
1129
+ const isNextNodeInlineObjectElement = nextNode && nextNode.is('element') && nextNode.name != 'br';
1130
+ const isNextNodeStartingWithSpace = nextNode && nextNode.is('$text') && nextNode.data.charAt(0) == ' ';
1131
+ // Then, let's change the last nbsp to a space.
1132
+ if (/[ \u00A0]\u00A0$/.test(data) || !nextNode || isNextNodeInlineObjectElement || isNextNodeStartingWithSpace) {
1133
+ data = data.replace(/\u00A0$/, ' ');
1134
+ }
1135
+ // Then, change &nbsp; character that is at the beginning of the text node to space character.
1136
+ // We do that replacement only if this is the first node or the previous node ends on whitespace character.
1137
+ if (shouldLeftTrim || prevNode && prevNode.is('element') && prevNode.name != 'br') {
1138
+ data = data.replace(/^\u00A0/, ' ');
1139
+ }
1140
+ }
1141
+ // At this point, all whitespaces should be removed and all &nbsp; created for rendering reasons should be
1142
+ // changed to normal space. All left &nbsp; are &nbsp; inserted intentionally.
1143
+ if (data.length == 0 && node.parent) {
1144
+ node._remove();
1145
+ inlineNodes.splice(i, 1);
1146
+ i--;
1147
+ }
1148
+ else {
1149
+ node._data = data;
1150
+ prevNodeEndsWithSpace = nodeEndsWithSpace;
1151
+ }
1152
+ }
1153
+ inlineNodes.length = 0;
1154
+ }
1155
+ /**
1156
+ * Takes text data from a given {@link module:engine/view/text~Text#data} and processes it so
1157
+ * it is correctly displayed in the DOM.
1158
+ *
1159
+ * Following changes are done:
1160
+ *
1161
+ * * a space at the beginning is changed to `&nbsp;` if this is the first text node in its container
1162
+ * element or if a previous text node ends with a space character,
1163
+ * * space at the end of the text node is changed to `&nbsp;` if there are two spaces at the end of a node or if next node
1164
+ * starts with a space or if it is the last text node in its container,
1165
+ * * remaining spaces are replaced to a chain of spaces and `&nbsp;` (e.g. `'x x'` becomes `'x &nbsp; x'`).
1166
+ *
1167
+ * Content of {@link #preElements} is not processed.
1168
+ *
1169
+ * @param node View text node to process.
1170
+ * @returns Processed text data.
1171
+ */
1172
+ _processDataFromViewText(node) {
1173
+ let data = node.data;
1174
+ // If any of node ancestors has a name which is in `preElements` array, then currently processed
1175
+ // view text node is (will be) in preformatted element. We should not change whitespaces then.
1176
+ if (node.getAncestors().some(parent => this.preElements.includes(parent.name))) {
1177
+ return data;
1178
+ }
1179
+ // 1. Replace the first space with a nbsp if the previous node ends with a space or there is no previous node
1180
+ // (container element boundary).
1181
+ if (data.charAt(0) == ' ') {
1182
+ const prevNode = this._getTouchingInlineViewNode(node, false);
1183
+ const prevEndsWithSpace = prevNode && prevNode.is('$textProxy') && this._nodeEndsWithSpace(prevNode);
1184
+ if (prevEndsWithSpace || !prevNode) {
1185
+ data = '\u00A0' + data.substr(1);
1186
+ }
1187
+ }
1188
+ // 2. Replace the last space with nbsp if there are two spaces at the end or if the next node starts with space or there is no
1189
+ // next node (container element boundary).
1190
+ //
1191
+ // Keep in mind that Firefox prefers $nbsp; before tag, not inside it:
1192
+ //
1193
+ // Foo <span>&nbsp;bar</span> <-- bad.
1194
+ // Foo&nbsp;<span> bar</span> <-- good.
1195
+ //
1196
+ // More here: https://github.com/ckeditor/ckeditor5-engine/issues/1747.
1197
+ if (data.charAt(data.length - 1) == ' ') {
1198
+ const nextNode = this._getTouchingInlineViewNode(node, true);
1199
+ const nextStartsWithSpace = nextNode && nextNode.is('$textProxy') && nextNode.data.charAt(0) == ' ';
1200
+ if (data.charAt(data.length - 2) == ' ' || !nextNode || nextStartsWithSpace) {
1201
+ data = data.substr(0, data.length - 1) + '\u00A0';
1202
+ }
1203
+ }
1204
+ // 3. Create space+nbsp pairs.
1205
+ return data.replace(/ {2}/g, ' \u00A0');
1206
+ }
1207
+ /**
1208
+ * Checks whether given node ends with a space character after changing appropriate space characters to `&nbsp;`s.
1209
+ *
1210
+ * @param node Node to check.
1211
+ * @returns `true` if given `node` ends with space, `false` otherwise.
1212
+ */
1213
+ _nodeEndsWithSpace(node) {
1214
+ if (node.getAncestors().some(parent => this.preElements.includes(parent.name))) {
1215
+ return false;
1216
+ }
1217
+ const data = this._processDataFromViewText(node);
1218
+ return data.charAt(data.length - 1) == ' ';
1219
+ }
1220
+ /**
1221
+ * Helper function. For given {@link module:engine/view/text~Text view text node}, it finds previous or next sibling
1222
+ * that is contained in the same container element. If there is no such sibling, `null` is returned.
1223
+ *
1224
+ * @param node Reference node.
1225
+ * @returns Touching text node, an inline object
1226
+ * or `null` if there is no next or previous touching text node.
1227
+ */
1228
+ _getTouchingInlineViewNode(node, getNext) {
1229
+ const treeWalker = new ViewTreeWalker({
1230
+ startPosition: getNext ? ViewPosition._createAfter(node) : ViewPosition._createBefore(node),
1231
+ direction: getNext ? 'forward' : 'backward'
1232
+ });
1233
+ for (const value of treeWalker) {
1234
+ // <br> found it works like a block boundary, so do not scan further.
1235
+ if (value.item.is('element', 'br')) {
1236
+ return null;
1237
+ }
1238
+ // Found an inline object (for example an image).
1239
+ else if (this._isInlineObjectElement(value.item)) {
1240
+ return value.item;
1241
+ }
1242
+ // ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last
1243
+ // text node in its container element.
1244
+ else if (value.item.is('containerElement')) {
1245
+ return null;
1246
+ }
1247
+ // Found a text node in the same container element.
1248
+ else if (value.item.is('$textProxy')) {
1249
+ return value.item;
1250
+ }
1251
+ }
1252
+ return null;
1253
+ }
1254
+ /**
1255
+ * Returns `true` if a DOM node belongs to {@link #blockElements}. `false` otherwise.
1256
+ */
1257
+ _isBlockDomElement(node) {
1258
+ return this.isElement(node) && this.blockElements.includes(node.tagName.toLowerCase());
1259
+ }
1260
+ /**
1261
+ * Returns `true` if a view node belongs to {@link #blockElements}. `false` otherwise.
1262
+ */
1263
+ _isBlockViewElement(node) {
1264
+ return node.is('element') && this.blockElements.includes(node.name);
1265
+ }
1266
+ /**
1267
+ * Returns `true` if a DOM node belongs to {@link #inlineObjectElements}. `false` otherwise.
1268
+ */
1269
+ _isInlineObjectElement(node) {
1270
+ if (!node.is('element')) {
1271
+ return false;
1272
+ }
1273
+ return node.name == 'br' ||
1274
+ this.inlineObjectElements.includes(node.name) ||
1275
+ !!this._inlineObjectElementMatcher.match(node);
1276
+ }
1277
+ /**
1278
+ * Creates view element basing on the node type.
1279
+ *
1280
+ * @param node DOM node to check.
1281
+ * @param options Conversion options. See {@link module:engine/view/domconverter~DomConverter#domToView} options parameter.
1282
+ */
1283
+ _createViewElement(node, options) {
1284
+ if (isComment(node)) {
1285
+ return new ViewUIElement(this.document, '$comment');
1286
+ }
1287
+ const viewName = options.keepOriginalCase ? node.tagName : node.tagName.toLowerCase();
1288
+ return new ViewElement(this.document, viewName);
1289
+ }
1290
+ /**
1291
+ * Checks if view element's content should be treated as a raw data.
1292
+ *
1293
+ * @param viewElement View element to check.
1294
+ * @param options Conversion options. See {@link module:engine/view/domconverter~DomConverter#domToView} options parameter.
1295
+ */
1296
+ _isViewElementWithRawContent(viewElement, options) {
1297
+ return options.withChildren !== false && viewElement.is('element') && !!this._rawContentElementMatcher.match(viewElement);
1298
+ }
1299
+ /**
1300
+ * Checks whether a given element name should be renamed in a current rendering mode.
1301
+ *
1302
+ * @param elementName The name of view element.
1303
+ */
1304
+ _shouldRenameElement(elementName) {
1305
+ const name = elementName.toLowerCase();
1306
+ return this.renderingMode === 'editing' && this.unsafeElements.includes(name);
1307
+ }
1308
+ /**
1309
+ * Return a <span> element with a special attribute holding the name of the original element.
1310
+ * Optionally, copy all the attributes of the original element if that element is provided.
1311
+ *
1312
+ * @param elementName The name of view element.
1313
+ * @param originalDomElement The original DOM element to copy attributes and content from.
1314
+ */
1315
+ _createReplacementDomElement(elementName, originalDomElement) {
1316
+ const newDomElement = this._domDocument.createElement('span');
1317
+ // Mark the span replacing a script as hidden.
1318
+ newDomElement.setAttribute(UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE, elementName);
1319
+ if (originalDomElement) {
1320
+ while (originalDomElement.firstChild) {
1321
+ newDomElement.appendChild(originalDomElement.firstChild);
1322
+ }
1323
+ for (const attributeName of originalDomElement.getAttributeNames()) {
1324
+ newDomElement.setAttribute(attributeName, originalDomElement.getAttribute(attributeName));
1325
+ }
1326
+ }
1327
+ return newDomElement;
1328
+ }
1329
+ }
1330
+ /**
1331
+ * Helper function.
1332
+ * Used to check if given native `Element` or `Text` node has parent with tag name from `types` array.
1333
+ *
1334
+ * @returns`true` if such parent exists or `false` if it does not.
1335
+ */
1336
+ function _hasViewParentOfType(node, types) {
1337
+ return node.getAncestors().some(parent => parent.is('element') && types.includes(parent.name));
1338
+ }
1339
+ /**
1340
+ * A helper that executes given callback for each DOM node's ancestor, starting from the given node
1341
+ * and ending in document#documentElement.
1342
+ *
1343
+ * @param callback A callback to be executed for each ancestor.
1344
+ */
1345
+ function forEachDomElementAncestor(element, callback) {
1346
+ let node = element;
1347
+ while (node) {
1348
+ callback(node);
1349
+ node = node.parentElement;
1350
+ }
1351
+ }
1352
+ /**
1353
+ * Checks if given node is a nbsp block filler.
1354
+ *
1355
+ * A &nbsp; is a block filler only if it is a single child of a block element.
1356
+ *
1357
+ * @param domNode DOM node.
1358
+ */
1359
+ function isNbspBlockFiller(domNode, blockElements) {
1360
+ const isNBSP = domNode.isEqualNode(NBSP_FILLER_REF);
1361
+ return isNBSP && hasBlockParent(domNode, blockElements) && domNode.parentNode.childNodes.length === 1;
1362
+ }
1363
+ /**
1364
+ * Checks if domNode has block parent.
1365
+ *
1366
+ * @param domNode DOM node.
1367
+ */
1368
+ function hasBlockParent(domNode, blockElements) {
1369
+ const parent = domNode.parentNode;
1370
+ return !!parent && !!parent.tagName && blockElements.includes(parent.tagName.toLowerCase());
1371
+ }
1372
+ /**
1373
+ * Log to console the information about element that was replaced.
1374
+ * Check UNSAFE_ELEMENTS for all recognized unsafe elements.
1375
+ *
1376
+ * @param elementName The name of the view element.
1377
+ */
1378
+ function _logUnsafeElement(elementName) {
1379
+ if (elementName === 'script') {
1380
+ logWarning('domconverter-unsafe-script-element-detected');
1381
+ }
1382
+ if (elementName === 'style') {
1383
+ logWarning('domconverter-unsafe-style-element-detected');
1384
+ }
1385
+ }
1386
+ /**
1387
+ * In certain cases, Firefox mysteriously assigns so called "restricted objects" to native DOM Range properties.
1388
+ * Any attempt at accessing restricted object's properties causes errors.
1389
+ * See: https://github.com/ckeditor/ckeditor5/issues/9635.
1390
+ */
1391
+ function isGeckoRestrictedDomSelection(domSelection) {
1392
+ if (!env.isGecko) {
1393
+ return false;
1394
+ }
1395
+ if (!domSelection.rangeCount) {
1396
+ return false;
1397
+ }
1398
+ const container = domSelection.getRangeAt(0).startContainer;
1399
+ try {
1400
+ Object.prototype.toString.call(container);
1401
+ }
1402
+ catch (error) {
1403
+ return true;
1404
+ }
1405
+ return false;
1406
+ }
1407
+ /**
1408
+ * While rendering the editor content, the {@link module:engine/view/domconverter~DomConverter} detected a `<script>` element that may
1409
+ * disrupt the editing experience. To avoid this, the `<script>` element was replaced with `<span data-ck-unsafe-element="script"></span>`.
1410
+ *
1411
+ * @error domconverter-unsafe-script-element-detected
1412
+ */
1413
+ /**
1414
+ * While rendering the editor content, the {@link module:engine/view/domconverter~DomConverter} detected a `<style>` element that may affect
1415
+ * the editing experience. To avoid this, the `<style>` element was replaced with `<span data-ck-unsafe-element="style"></span>`.
1416
+ *
1417
+ * @error domconverter-unsafe-style-element-detected
1418
+ */
1419
+ /**
1420
+ * The {@link module:engine/view/domconverter~DomConverter} detected an interactive attribute in the
1421
+ * {@glink framework/architecture/editing-engine#editing-pipeline editing pipeline}. For the best
1422
+ * editing experience, the attribute was renamed to `data-ck-unsafe-attribute-[original attribute name]`.
1423
+ *
1424
+ * If you are the author of the plugin that generated this attribute and you want it to be preserved
1425
+ * in the editing pipeline, you can configure this when creating the element
1426
+ * using {@link module:engine/view/downcastwriter~DowncastWriter} during the
1427
+ * {@glink framework/architecture/editing-engine#conversion model–view conversion}. Methods such as
1428
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement},
1429
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement}, or
1430
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement}
1431
+ * accept an option that will disable filtering of specific attributes:
1432
+ *
1433
+ * ```ts
1434
+ * const paragraph = writer.createContainerElement( 'p',
1435
+ * {
1436
+ * class: 'clickable-paragraph',
1437
+ * onclick: 'alert( "Paragraph clicked!" )'
1438
+ * },
1439
+ * {
1440
+ * // Make sure the "onclick" attribute will pass through.
1441
+ * renderUnsafeAttributes: [ 'onclick' ]
1442
+ * }
1443
+ * );
1444
+ * ```
1445
+ *
1446
+ * @error domconverter-unsafe-attribute-detected
1447
+ * @param domElement The DOM element the attribute was set on.
1448
+ * @param key The original name of the attribute
1449
+ * @param value The value of the original attribute
1450
+ */