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

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