@ckeditor/ckeditor5-engine 40.0.0 → 40.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/CHANGELOG.md +39 -39
  2. package/LICENSE.md +3 -3
  3. package/package.json +2 -2
  4. package/src/controller/datacontroller.d.ts +334 -334
  5. package/src/controller/datacontroller.js +481 -481
  6. package/src/controller/editingcontroller.d.ts +98 -98
  7. package/src/controller/editingcontroller.js +191 -191
  8. package/src/conversion/conversion.d.ts +478 -478
  9. package/src/conversion/conversion.js +601 -601
  10. package/src/conversion/conversionhelpers.d.ts +26 -26
  11. package/src/conversion/conversionhelpers.js +32 -32
  12. package/src/conversion/downcastdispatcher.d.ts +562 -562
  13. package/src/conversion/downcastdispatcher.js +548 -547
  14. package/src/conversion/downcasthelpers.d.ts +1226 -1226
  15. package/src/conversion/downcasthelpers.js +2178 -2183
  16. package/src/conversion/mapper.d.ts +503 -503
  17. package/src/conversion/mapper.js +536 -536
  18. package/src/conversion/modelconsumable.d.ts +201 -201
  19. package/src/conversion/modelconsumable.js +333 -333
  20. package/src/conversion/upcastdispatcher.d.ts +492 -492
  21. package/src/conversion/upcastdispatcher.js +460 -460
  22. package/src/conversion/upcasthelpers.d.ts +499 -499
  23. package/src/conversion/upcasthelpers.js +950 -950
  24. package/src/conversion/viewconsumable.d.ts +369 -369
  25. package/src/conversion/viewconsumable.js +536 -532
  26. package/src/dataprocessor/basichtmlwriter.d.ts +18 -18
  27. package/src/dataprocessor/basichtmlwriter.js +20 -19
  28. package/src/dataprocessor/dataprocessor.d.ts +61 -61
  29. package/src/dataprocessor/dataprocessor.js +5 -5
  30. package/src/dataprocessor/htmldataprocessor.d.ts +76 -76
  31. package/src/dataprocessor/htmldataprocessor.js +96 -96
  32. package/src/dataprocessor/htmlwriter.d.ts +16 -16
  33. package/src/dataprocessor/htmlwriter.js +5 -5
  34. package/src/dataprocessor/xmldataprocessor.d.ts +90 -90
  35. package/src/dataprocessor/xmldataprocessor.js +108 -108
  36. package/src/dev-utils/model.d.ts +124 -124
  37. package/src/dev-utils/model.js +395 -395
  38. package/src/dev-utils/operationreplayer.d.ts +51 -51
  39. package/src/dev-utils/operationreplayer.js +112 -112
  40. package/src/dev-utils/utils.d.ts +37 -37
  41. package/src/dev-utils/utils.js +73 -73
  42. package/src/dev-utils/view.d.ts +319 -319
  43. package/src/dev-utils/view.js +967 -967
  44. package/src/index.d.ts +114 -114
  45. package/src/index.js +78 -78
  46. package/src/model/batch.d.ts +106 -106
  47. package/src/model/batch.js +96 -96
  48. package/src/model/differ.d.ts +387 -387
  49. package/src/model/differ.js +1149 -1149
  50. package/src/model/document.d.ts +272 -272
  51. package/src/model/document.js +360 -361
  52. package/src/model/documentfragment.d.ts +200 -200
  53. package/src/model/documentfragment.js +306 -306
  54. package/src/model/documentselection.d.ts +420 -420
  55. package/src/model/documentselection.js +993 -993
  56. package/src/model/element.d.ts +165 -165
  57. package/src/model/element.js +281 -281
  58. package/src/model/history.d.ts +114 -114
  59. package/src/model/history.js +207 -207
  60. package/src/model/item.d.ts +14 -14
  61. package/src/model/item.js +5 -5
  62. package/src/model/liveposition.d.ts +77 -77
  63. package/src/model/liveposition.js +93 -93
  64. package/src/model/liverange.d.ts +102 -102
  65. package/src/model/liverange.js +120 -120
  66. package/src/model/markercollection.d.ts +335 -335
  67. package/src/model/markercollection.js +403 -403
  68. package/src/model/model.d.ts +919 -919
  69. package/src/model/model.js +842 -842
  70. package/src/model/node.d.ts +256 -256
  71. package/src/model/node.js +375 -375
  72. package/src/model/nodelist.d.ts +91 -91
  73. package/src/model/nodelist.js +163 -163
  74. package/src/model/operation/attributeoperation.d.ts +103 -103
  75. package/src/model/operation/attributeoperation.js +148 -148
  76. package/src/model/operation/detachoperation.d.ts +60 -60
  77. package/src/model/operation/detachoperation.js +77 -77
  78. package/src/model/operation/insertoperation.d.ts +90 -90
  79. package/src/model/operation/insertoperation.js +135 -135
  80. package/src/model/operation/markeroperation.d.ts +91 -91
  81. package/src/model/operation/markeroperation.js +107 -107
  82. package/src/model/operation/mergeoperation.d.ts +100 -100
  83. package/src/model/operation/mergeoperation.js +167 -167
  84. package/src/model/operation/moveoperation.d.ts +96 -96
  85. package/src/model/operation/moveoperation.js +164 -164
  86. package/src/model/operation/nooperation.d.ts +38 -38
  87. package/src/model/operation/nooperation.js +48 -48
  88. package/src/model/operation/operation.d.ts +96 -96
  89. package/src/model/operation/operation.js +59 -62
  90. package/src/model/operation/operationfactory.d.ts +18 -18
  91. package/src/model/operation/operationfactory.js +44 -44
  92. package/src/model/operation/renameoperation.d.ts +83 -83
  93. package/src/model/operation/renameoperation.js +115 -115
  94. package/src/model/operation/rootattributeoperation.d.ts +98 -98
  95. package/src/model/operation/rootattributeoperation.js +155 -155
  96. package/src/model/operation/rootoperation.d.ts +76 -76
  97. package/src/model/operation/rootoperation.js +90 -90
  98. package/src/model/operation/splitoperation.d.ts +109 -109
  99. package/src/model/operation/splitoperation.js +194 -194
  100. package/src/model/operation/transform.d.ts +100 -100
  101. package/src/model/operation/transform.js +1985 -1985
  102. package/src/model/operation/utils.d.ts +71 -71
  103. package/src/model/operation/utils.js +217 -213
  104. package/src/model/position.d.ts +539 -539
  105. package/src/model/position.js +979 -979
  106. package/src/model/range.d.ts +458 -458
  107. package/src/model/range.js +875 -875
  108. package/src/model/rootelement.d.ts +60 -60
  109. package/src/model/rootelement.js +74 -74
  110. package/src/model/schema.d.ts +1186 -1186
  111. package/src/model/schema.js +1242 -1242
  112. package/src/model/selection.d.ts +482 -482
  113. package/src/model/selection.js +789 -789
  114. package/src/model/text.d.ts +66 -66
  115. package/src/model/text.js +85 -85
  116. package/src/model/textproxy.d.ts +144 -144
  117. package/src/model/textproxy.js +189 -189
  118. package/src/model/treewalker.d.ts +186 -186
  119. package/src/model/treewalker.js +244 -244
  120. package/src/model/typecheckable.d.ts +285 -285
  121. package/src/model/typecheckable.js +16 -16
  122. package/src/model/utils/autoparagraphing.d.ts +37 -37
  123. package/src/model/utils/autoparagraphing.js +63 -63
  124. package/src/model/utils/deletecontent.d.ts +58 -58
  125. package/src/model/utils/deletecontent.js +488 -488
  126. package/src/model/utils/findoptimalinsertionrange.d.ts +32 -32
  127. package/src/model/utils/findoptimalinsertionrange.js +57 -57
  128. package/src/model/utils/getselectedcontent.d.ts +30 -30
  129. package/src/model/utils/getselectedcontent.js +125 -125
  130. package/src/model/utils/insertcontent.d.ts +46 -46
  131. package/src/model/utils/insertcontent.js +705 -705
  132. package/src/model/utils/insertobject.d.ts +44 -44
  133. package/src/model/utils/insertobject.js +139 -139
  134. package/src/model/utils/modifyselection.d.ts +48 -48
  135. package/src/model/utils/modifyselection.js +186 -186
  136. package/src/model/utils/selection-post-fixer.d.ts +74 -74
  137. package/src/model/utils/selection-post-fixer.js +260 -260
  138. package/src/model/writer.d.ts +851 -851
  139. package/src/model/writer.js +1306 -1306
  140. package/src/view/attributeelement.d.ts +108 -108
  141. package/src/view/attributeelement.js +184 -184
  142. package/src/view/containerelement.d.ts +49 -49
  143. package/src/view/containerelement.js +80 -80
  144. package/src/view/datatransfer.d.ts +79 -79
  145. package/src/view/datatransfer.js +98 -98
  146. package/src/view/document.d.ts +184 -184
  147. package/src/view/document.js +122 -120
  148. package/src/view/documentfragment.d.ts +153 -149
  149. package/src/view/documentfragment.js +234 -228
  150. package/src/view/documentselection.d.ts +306 -306
  151. package/src/view/documentselection.js +256 -256
  152. package/src/view/domconverter.d.ts +652 -640
  153. package/src/view/domconverter.js +1473 -1450
  154. package/src/view/downcastwriter.d.ts +996 -996
  155. package/src/view/downcastwriter.js +1696 -1696
  156. package/src/view/editableelement.d.ts +62 -62
  157. package/src/view/editableelement.js +62 -62
  158. package/src/view/element.d.ts +468 -468
  159. package/src/view/element.js +724 -724
  160. package/src/view/elementdefinition.d.ts +87 -87
  161. package/src/view/elementdefinition.js +5 -5
  162. package/src/view/emptyelement.d.ts +41 -41
  163. package/src/view/emptyelement.js +73 -73
  164. package/src/view/filler.d.ts +111 -111
  165. package/src/view/filler.js +150 -150
  166. package/src/view/item.d.ts +14 -14
  167. package/src/view/item.js +5 -5
  168. package/src/view/matcher.d.ts +486 -486
  169. package/src/view/matcher.js +507 -507
  170. package/src/view/node.d.ts +163 -163
  171. package/src/view/node.js +228 -228
  172. package/src/view/observer/arrowkeysobserver.d.ts +45 -45
  173. package/src/view/observer/arrowkeysobserver.js +40 -40
  174. package/src/view/observer/bubblingemittermixin.d.ts +166 -166
  175. package/src/view/observer/bubblingemittermixin.js +172 -172
  176. package/src/view/observer/bubblingeventinfo.d.ts +47 -47
  177. package/src/view/observer/bubblingeventinfo.js +37 -37
  178. package/src/view/observer/clickobserver.d.ts +43 -43
  179. package/src/view/observer/clickobserver.js +29 -29
  180. package/src/view/observer/compositionobserver.d.ts +82 -82
  181. package/src/view/observer/compositionobserver.js +60 -60
  182. package/src/view/observer/domeventdata.d.ts +50 -50
  183. package/src/view/observer/domeventdata.js +47 -47
  184. package/src/view/observer/domeventobserver.d.ts +73 -73
  185. package/src/view/observer/domeventobserver.js +79 -79
  186. package/src/view/observer/fakeselectionobserver.d.ts +47 -47
  187. package/src/view/observer/fakeselectionobserver.js +91 -91
  188. package/src/view/observer/focusobserver.d.ts +82 -82
  189. package/src/view/observer/focusobserver.js +86 -86
  190. package/src/view/observer/inputobserver.d.ts +86 -86
  191. package/src/view/observer/inputobserver.js +164 -164
  192. package/src/view/observer/keyobserver.d.ts +66 -66
  193. package/src/view/observer/keyobserver.js +39 -39
  194. package/src/view/observer/mouseobserver.d.ts +89 -89
  195. package/src/view/observer/mouseobserver.js +29 -29
  196. package/src/view/observer/mutationobserver.d.ts +86 -86
  197. package/src/view/observer/mutationobserver.js +206 -206
  198. package/src/view/observer/observer.d.ts +89 -89
  199. package/src/view/observer/observer.js +84 -84
  200. package/src/view/observer/selectionobserver.d.ts +148 -148
  201. package/src/view/observer/selectionobserver.js +202 -202
  202. package/src/view/observer/tabobserver.d.ts +46 -46
  203. package/src/view/observer/tabobserver.js +42 -42
  204. package/src/view/placeholder.d.ts +96 -96
  205. package/src/view/placeholder.js +267 -267
  206. package/src/view/position.d.ts +189 -189
  207. package/src/view/position.js +324 -324
  208. package/src/view/range.d.ts +279 -279
  209. package/src/view/range.js +430 -430
  210. package/src/view/rawelement.d.ts +73 -73
  211. package/src/view/rawelement.js +105 -105
  212. package/src/view/renderer.d.ts +265 -265
  213. package/src/view/renderer.js +1000 -999
  214. package/src/view/rooteditableelement.d.ts +41 -41
  215. package/src/view/rooteditableelement.js +69 -69
  216. package/src/view/selection.d.ts +375 -375
  217. package/src/view/selection.js +559 -559
  218. package/src/view/styles/background.d.ts +33 -33
  219. package/src/view/styles/background.js +74 -74
  220. package/src/view/styles/border.d.ts +43 -43
  221. package/src/view/styles/border.js +316 -316
  222. package/src/view/styles/margin.d.ts +29 -29
  223. package/src/view/styles/margin.js +34 -34
  224. package/src/view/styles/padding.d.ts +29 -29
  225. package/src/view/styles/padding.js +34 -34
  226. package/src/view/styles/utils.d.ts +93 -93
  227. package/src/view/styles/utils.js +219 -219
  228. package/src/view/stylesmap.d.ts +675 -675
  229. package/src/view/stylesmap.js +765 -766
  230. package/src/view/text.d.ts +74 -74
  231. package/src/view/text.js +93 -93
  232. package/src/view/textproxy.d.ts +97 -97
  233. package/src/view/textproxy.js +124 -124
  234. package/src/view/treewalker.d.ts +195 -195
  235. package/src/view/treewalker.js +327 -327
  236. package/src/view/typecheckable.d.ts +448 -448
  237. package/src/view/typecheckable.js +19 -19
  238. package/src/view/uielement.d.ts +96 -96
  239. package/src/view/uielement.js +183 -182
  240. package/src/view/upcastwriter.d.ts +417 -417
  241. package/src/view/upcastwriter.js +359 -359
  242. package/src/view/view.d.ts +487 -487
  243. package/src/view/view.js +546 -546
@@ -1,999 +1,1000 @@
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/renderer
7
- */
8
- import ViewText from './text';
9
- import ViewPosition from './position';
10
- import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller } from './filler';
11
- import { CKEditorError, ObservableMixin, diff, env, fastDiff, insertAt, isComment, isNode, isText, remove } from '@ckeditor/ckeditor5-utils';
12
- import '../../theme/renderer.css';
13
- /**
14
- * Renderer is responsible for updating the DOM structure and the DOM selection based on
15
- * the {@link module:engine/view/renderer~Renderer#markToSync information about updated view nodes}.
16
- * In other words, it renders the view to the DOM.
17
- *
18
- * Its main responsibility is to make only the necessary, minimal changes to the DOM. However, unlike in many
19
- * virtual DOM implementations, the primary reason for doing minimal changes is not the performance but ensuring
20
- * that native editing features such as text composition, autocompletion, spell checking, selection's x-index are
21
- * affected as little as possible.
22
- *
23
- * Renderer uses {@link module:engine/view/domconverter~DomConverter} to transform view nodes and positions
24
- * to and from the DOM.
25
- */
26
- export default class Renderer extends ObservableMixin() {
27
- /**
28
- * Creates a renderer instance.
29
- *
30
- * @param domConverter Converter instance.
31
- * @param selection View selection.
32
- */
33
- constructor(domConverter, selection) {
34
- super();
35
- /**
36
- * Set of DOM Documents instances.
37
- */
38
- this.domDocuments = new Set();
39
- /**
40
- * Set of nodes which attributes changed and may need to be rendered.
41
- */
42
- this.markedAttributes = new Set();
43
- /**
44
- * Set of elements which child lists changed and may need to be rendered.
45
- */
46
- this.markedChildren = new Set();
47
- /**
48
- * Set of text nodes which text data changed and may need to be rendered.
49
- */
50
- this.markedTexts = new Set();
51
- /**
52
- * The text node in which the inline filler was rendered.
53
- */
54
- this._inlineFiller = null;
55
- /**
56
- * DOM element containing fake selection.
57
- */
58
- this._fakeSelectionContainer = null;
59
- this.domConverter = domConverter;
60
- this.selection = selection;
61
- this.set('isFocused', false);
62
- this.set('isSelecting', false);
63
- // Rendering the selection and inline filler manipulation should be postponed in (non-Android) Blink until the user finishes
64
- // creating the selection in DOM to avoid accidental selection collapsing
65
- // (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
66
- // When the user stops selecting, all pending changes should be rendered ASAP, though.
67
- if (env.isBlink && !env.isAndroid) {
68
- this.on('change:isSelecting', () => {
69
- if (!this.isSelecting) {
70
- this.render();
71
- }
72
- });
73
- }
74
- this.set('isComposing', false);
75
- this.on('change:isComposing', () => {
76
- if (!this.isComposing) {
77
- this.render();
78
- }
79
- });
80
- }
81
- /**
82
- * Marks a view node to be updated in the DOM by {@link #render `render()`}.
83
- *
84
- * Note that only view nodes whose parents have corresponding DOM elements need to be marked to be synchronized.
85
- *
86
- * @see #markedAttributes
87
- * @see #markedChildren
88
- * @see #markedTexts
89
- *
90
- * @param type Type of the change.
91
- * @param node ViewNode to be marked.
92
- */
93
- markToSync(type, node) {
94
- if (type === 'text') {
95
- if (this.domConverter.mapViewToDom(node.parent)) {
96
- this.markedTexts.add(node);
97
- }
98
- }
99
- else {
100
- // If the node has no DOM element it is not rendered yet,
101
- // its children/attributes do not need to be marked to be sync.
102
- if (!this.domConverter.mapViewToDom(node)) {
103
- return;
104
- }
105
- if (type === 'attributes') {
106
- this.markedAttributes.add(node);
107
- }
108
- else if (type === 'children') {
109
- this.markedChildren.add(node);
110
- }
111
- else {
112
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
113
- const unreachable = type;
114
- /**
115
- * Unknown type passed to Renderer.markToSync.
116
- *
117
- * @error view-renderer-unknown-type
118
- */
119
- throw new CKEditorError('view-renderer-unknown-type', this);
120
- }
121
- }
122
- }
123
- /**
124
- * Renders all buffered changes ({@link #markedAttributes}, {@link #markedChildren} and {@link #markedTexts}) and
125
- * the current view selection (if needed) to the DOM by applying a minimal set of changes to it.
126
- *
127
- * Renderer tries not to break the text composition (e.g. IME) and x-index of the selection,
128
- * so it does as little as it is needed to update the DOM.
129
- *
130
- * Renderer also handles {@link module:engine/view/filler fillers}. Especially, it checks if the inline filler is needed
131
- * at the selection position and adds or removes it. To prevent breaking text composition inline filler will not be
132
- * removed as long as the selection is in the text node which needed it at first.
133
- */
134
- render() {
135
- // Ignore rendering while in the composition mode. Composition events are not cancellable and browser will modify the DOM tree.
136
- // All marked elements, attributes, etc. will wait until next render after the composition ends.
137
- // On Android composition events are immediately applied to the model, so we don't need to skip rendering,
138
- // and we should not do it because the difference between view and DOM could lead to position mapping problems.
139
- if (this.isComposing && !env.isAndroid) {
140
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
141
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Rendering aborted while isComposing',
142
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
143
- // @if CK_DEBUG_TYPING // );
144
- // @if CK_DEBUG_TYPING // }
145
- return;
146
- }
147
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
148
- // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Rendering',
149
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
150
- // @if CK_DEBUG_TYPING // );
151
- // @if CK_DEBUG_TYPING // }
152
- let inlineFillerPosition = null;
153
- const isInlineFillerRenderingPossible = env.isBlink && !env.isAndroid ? !this.isSelecting : true;
154
- // Refresh mappings.
155
- for (const element of this.markedChildren) {
156
- this._updateChildrenMappings(element);
157
- }
158
- // Don't manipulate inline fillers while the selection is being made in (non-Android) Blink to prevent accidental
159
- // DOM selection collapsing
160
- // (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
161
- if (isInlineFillerRenderingPossible) {
162
- // There was inline filler rendered in the DOM but it's not
163
- // at the selection position any more, so we can remove it
164
- // (cause even if it's needed, it must be placed in another location).
165
- if (this._inlineFiller && !this._isSelectionInInlineFiller()) {
166
- this._removeInlineFiller();
167
- }
168
- // If we've got the filler, let's try to guess its position in the view.
169
- if (this._inlineFiller) {
170
- inlineFillerPosition = this._getInlineFillerPosition();
171
- }
172
- // Otherwise, if it's needed, create it at the selection position.
173
- else if (this._needsInlineFillerAtSelection()) {
174
- inlineFillerPosition = this.selection.getFirstPosition();
175
- // Do not use `markToSync` so it will be added even if the parent is already added.
176
- this.markedChildren.add(inlineFillerPosition.parent);
177
- }
178
- }
179
- // Make sure the inline filler has any parent, so it can be mapped to view position by DomConverter.
180
- else if (this._inlineFiller && this._inlineFiller.parentNode) {
181
- // While the user is making selection, preserve the inline filler at its original position.
182
- inlineFillerPosition = this.domConverter.domPositionToView(this._inlineFiller);
183
- // While down-casting the document selection attributes, all existing empty
184
- // attribute elements (for selection position) are removed from the view and DOM,
185
- // so make sure that we were able to map filler position.
186
- // https://github.com/ckeditor/ckeditor5/issues/12026
187
- if (inlineFillerPosition && inlineFillerPosition.parent.is('$text')) {
188
- // The inline filler position is expected to be before the text node.
189
- inlineFillerPosition = ViewPosition._createBefore(inlineFillerPosition.parent);
190
- }
191
- }
192
- for (const element of this.markedAttributes) {
193
- this._updateAttrs(element);
194
- }
195
- for (const element of this.markedChildren) {
196
- this._updateChildren(element, { inlineFillerPosition });
197
- }
198
- for (const node of this.markedTexts) {
199
- if (!this.markedChildren.has(node.parent) && this.domConverter.mapViewToDom(node.parent)) {
200
- this._updateText(node, { inlineFillerPosition });
201
- }
202
- }
203
- // * Check whether the inline filler is required and where it really is in the DOM.
204
- // At this point in most cases it will be in the DOM, but there are exceptions.
205
- // For example, if the inline filler was deep in the created DOM structure, it will not be created.
206
- // Similarly, if it was removed at the beginning of this function and then neither text nor children were updated,
207
- // it will not be present. Fix those and similar scenarios.
208
- // * Don't manipulate inline fillers while the selection is being made in (non-Android) Blink to prevent accidental
209
- // DOM selection collapsing
210
- // (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
211
- if (isInlineFillerRenderingPossible) {
212
- if (inlineFillerPosition) {
213
- const fillerDomPosition = this.domConverter.viewPositionToDom(inlineFillerPosition);
214
- const domDocument = fillerDomPosition.parent.ownerDocument;
215
- if (!startsWithFiller(fillerDomPosition.parent)) {
216
- // Filler has not been created at filler position. Create it now.
217
- this._inlineFiller = addInlineFiller(domDocument, fillerDomPosition.parent, fillerDomPosition.offset);
218
- }
219
- else {
220
- // Filler has been found, save it.
221
- this._inlineFiller = fillerDomPosition.parent;
222
- }
223
- }
224
- else {
225
- // There is no filler needed.
226
- this._inlineFiller = null;
227
- }
228
- }
229
- // First focus the new editing host, then update the selection.
230
- // Otherwise, FF may throw an error (https://github.com/ckeditor/ckeditor5/issues/721).
231
- this._updateFocus();
232
- this._updateSelection();
233
- this.markedTexts.clear();
234
- this.markedAttributes.clear();
235
- this.markedChildren.clear();
236
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
237
- // @if CK_DEBUG_TYPING // console.groupEnd();
238
- // @if CK_DEBUG_TYPING // }
239
- }
240
- /**
241
- * Updates mappings of view element's children.
242
- *
243
- * Children that were replaced in the view structure by similar elements (same tag name) are treated as 'replaced'.
244
- * This means that their mappings can be updated so the new view elements are mapped to the existing DOM elements.
245
- * Thanks to that these elements do not need to be re-rendered completely.
246
- *
247
- * @param viewElement The view element whose children mappings will be updated.
248
- */
249
- _updateChildrenMappings(viewElement) {
250
- const domElement = this.domConverter.mapViewToDom(viewElement);
251
- if (!domElement) {
252
- // If there is no `domElement` it means that it was already removed from DOM and there is no need to process it.
253
- return;
254
- }
255
- // Removing nodes from the DOM as we iterate can cause `actualDomChildren`
256
- // (which is a live-updating `NodeList`) to get out of sync with the
257
- // indices that we compute as we iterate over `actions`.
258
- // This would produce incorrect element mappings.
259
- //
260
- // Converting live list to an array to make the list static.
261
- const actualDomChildren = Array.from(domElement.childNodes);
262
- const expectedDomChildren = Array.from(this.domConverter.viewChildrenToDom(viewElement, { withChildren: false }));
263
- const diff = this._diffNodeLists(actualDomChildren, expectedDomChildren);
264
- const actions = this._findUpdateActions(diff, actualDomChildren, expectedDomChildren, areSimilarElements);
265
- if (actions.indexOf('update') !== -1) {
266
- const counter = { equal: 0, insert: 0, delete: 0 };
267
- for (const action of actions) {
268
- if (action === 'update') {
269
- const insertIndex = counter.equal + counter.insert;
270
- const deleteIndex = counter.equal + counter.delete;
271
- const viewChild = viewElement.getChild(insertIndex);
272
- // UIElement and RawElement are special cases. Their children are not stored in a view (#799)
273
- // so we cannot use them with replacing flow (since they use view children during rendering
274
- // which will always result in rendering empty elements).
275
- if (viewChild && !(viewChild.is('uiElement') || viewChild.is('rawElement'))) {
276
- this._updateElementMappings(viewChild, actualDomChildren[deleteIndex]);
277
- }
278
- remove(expectedDomChildren[insertIndex]);
279
- counter.equal++;
280
- }
281
- else {
282
- counter[action]++;
283
- }
284
- }
285
- }
286
- }
287
- /**
288
- * Updates mappings of a given view element.
289
- *
290
- * @param viewElement The view element whose mappings will be updated.
291
- * @param domElement The DOM element representing the given view element.
292
- */
293
- _updateElementMappings(viewElement, domElement) {
294
- // Remap 'DomConverter' bindings.
295
- this.domConverter.unbindDomElement(domElement);
296
- this.domConverter.bindElements(domElement, viewElement);
297
- // View element may have children which needs to be updated, but are not marked, mark them to update.
298
- this.markedChildren.add(viewElement);
299
- // Because we replace new view element mapping with the existing one, the corresponding DOM element
300
- // will not be rerendered. The new view element may have different attributes than the previous one.
301
- // Since its corresponding DOM element will not be rerendered, new attributes will not be added
302
- // to the DOM, so we need to mark it here to make sure its attributes gets updated. See #1427 for more
303
- // detailed case study.
304
- // Also there are cases where replaced element is removed from the view structure and then has
305
- // its attributes changed or removed. In such cases the element will not be present in `markedAttributes`
306
- // and also may be the same (`element.isSimilar()`) as the reused element not having its attributes updated.
307
- // To prevent such situations we always mark reused element to have its attributes rerenderd (#1560).
308
- this.markedAttributes.add(viewElement);
309
- }
310
- /**
311
- * Gets the position of the inline filler based on the current selection.
312
- * Here, we assume that we know that the filler is needed and
313
- * {@link #_isSelectionInInlineFiller is at the selection position}, and, since it is needed,
314
- * it is somewhere at the selection position.
315
- *
316
- * Note: The filler position cannot be restored based on the filler's DOM text node, because
317
- * when this method is called (before rendering), the bindings will often be broken. View-to-DOM
318
- * bindings are only dependable after rendering.
319
- */
320
- _getInlineFillerPosition() {
321
- const firstPos = this.selection.getFirstPosition();
322
- if (firstPos.parent.is('$text')) {
323
- return ViewPosition._createBefore(firstPos.parent);
324
- }
325
- else {
326
- return firstPos;
327
- }
328
- }
329
- /**
330
- * Returns `true` if the selection has not left the inline filler's text node.
331
- * If it is `true`, it means that the filler had been added for a reason and the selection did not
332
- * leave the filler's text node. For example, the user can be in the middle of a composition so it should not be touched.
333
- *
334
- * @returns `true` if the inline filler and selection are in the same place.
335
- */
336
- _isSelectionInInlineFiller() {
337
- if (this.selection.rangeCount != 1 || !this.selection.isCollapsed) {
338
- return false;
339
- }
340
- // Note, we can't check if selection's position equals position of the
341
- // this._inlineFiller node, because of #663. We may not be able to calculate
342
- // the filler's position in the view at this stage.
343
- // Instead, we check it the other way whether selection is anchored in
344
- // that text node or next to it.
345
- // Possible options are:
346
- // "FILLER{}"
347
- // "FILLERadded-text{}"
348
- const selectionPosition = this.selection.getFirstPosition();
349
- const position = this.domConverter.viewPositionToDom(selectionPosition);
350
- if (position && isText(position.parent) && startsWithFiller(position.parent)) {
351
- return true;
352
- }
353
- return false;
354
- }
355
- /**
356
- * Removes the inline filler.
357
- */
358
- _removeInlineFiller() {
359
- const domFillerNode = this._inlineFiller;
360
- // Something weird happened and the stored node doesn't contain the filler's text.
361
- if (!startsWithFiller(domFillerNode)) {
362
- /**
363
- * The inline filler node was lost. Most likely, something overwrote the filler text node
364
- * in the DOM.
365
- *
366
- * @error view-renderer-filler-was-lost
367
- */
368
- throw new CKEditorError('view-renderer-filler-was-lost', this);
369
- }
370
- if (isInlineFiller(domFillerNode)) {
371
- domFillerNode.remove();
372
- }
373
- else {
374
- domFillerNode.data = domFillerNode.data.substr(INLINE_FILLER_LENGTH);
375
- }
376
- this._inlineFiller = null;
377
- }
378
- /**
379
- * Checks if the inline {@link module:engine/view/filler filler} should be added.
380
- *
381
- * @returns `true` if the inline filler should be added.
382
- */
383
- _needsInlineFillerAtSelection() {
384
- if (this.selection.rangeCount != 1 || !this.selection.isCollapsed) {
385
- return false;
386
- }
387
- const selectionPosition = this.selection.getFirstPosition();
388
- const selectionParent = selectionPosition.parent;
389
- const selectionOffset = selectionPosition.offset;
390
- // If there is no DOM root we do not care about fillers.
391
- if (!this.domConverter.mapViewToDom(selectionParent.root)) {
392
- return false;
393
- }
394
- if (!(selectionParent.is('element'))) {
395
- return false;
396
- }
397
- // Prevent adding inline filler inside elements with contenteditable=false.
398
- // https://github.com/ckeditor/ckeditor5-engine/issues/1170
399
- if (!isEditable(selectionParent)) {
400
- return false;
401
- }
402
- // We have block filler, we do not need inline one.
403
- if (selectionOffset === selectionParent.getFillerOffset()) {
404
- return false;
405
- }
406
- const nodeBefore = selectionPosition.nodeBefore;
407
- const nodeAfter = selectionPosition.nodeAfter;
408
- if (nodeBefore instanceof ViewText || nodeAfter instanceof ViewText) {
409
- return false;
410
- }
411
- // Do not use inline filler while typing outside inline elements on Android.
412
- // The deleteContentBackward would remove part of the inline filler instead of removing last letter in a link.
413
- if (env.isAndroid && (nodeBefore || nodeAfter)) {
414
- return false;
415
- }
416
- return true;
417
- }
418
- /**
419
- * Checks if text needs to be updated and possibly updates it.
420
- *
421
- * @param viewText View text to update.
422
- * @param options.inlineFillerPosition The position where the inline filler should be rendered.
423
- */
424
- _updateText(viewText, options) {
425
- const domText = this.domConverter.findCorrespondingDomText(viewText);
426
- const newDomText = this.domConverter.viewToDom(viewText);
427
- let expectedText = newDomText.data;
428
- const filler = options.inlineFillerPosition;
429
- if (filler && filler.parent == viewText.parent && filler.offset == viewText.index) {
430
- expectedText = INLINE_FILLER + expectedText;
431
- }
432
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
433
- // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update text',
434
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
435
- // @if CK_DEBUG_TYPING // );
436
- // @if CK_DEBUG_TYPING // }
437
- updateTextNode(domText, expectedText);
438
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
439
- // @if CK_DEBUG_TYPING // console.groupEnd();
440
- // @if CK_DEBUG_TYPING // }
441
- }
442
- /**
443
- * Checks if attribute list needs to be updated and possibly updates it.
444
- *
445
- * @param viewElement The view element to update.
446
- */
447
- _updateAttrs(viewElement) {
448
- const domElement = this.domConverter.mapViewToDom(viewElement);
449
- if (!domElement) {
450
- // If there is no `domElement` it means that 'viewElement' is outdated as its mapping was updated
451
- // in 'this._updateChildrenMappings()'. There is no need to process it as new view element which
452
- // replaced old 'viewElement' mapping was also added to 'this.markedAttributes'
453
- // in 'this._updateChildrenMappings()' so it will be processed separately.
454
- return;
455
- }
456
- const domAttrKeys = Array.from(domElement.attributes).map(attr => attr.name);
457
- const viewAttrKeys = viewElement.getAttributeKeys();
458
- // Add or overwrite attributes.
459
- for (const key of viewAttrKeys) {
460
- this.domConverter.setDomElementAttribute(domElement, key, viewElement.getAttribute(key), viewElement);
461
- }
462
- // Remove from DOM attributes which do not exists in the view.
463
- for (const key of domAttrKeys) {
464
- // All other attributes not present in the DOM should be removed.
465
- if (!viewElement.hasAttribute(key)) {
466
- this.domConverter.removeDomElementAttribute(domElement, key);
467
- }
468
- }
469
- }
470
- /**
471
- * Checks if elements child list needs to be updated and possibly updates it.
472
- *
473
- * Note that on Android, to reduce the risk of composition breaks, it tries to update data of an existing
474
- * child text nodes instead of replacing them completely.
475
- *
476
- * @param viewElement View element to update.
477
- * @param options.inlineFillerPosition The position where the inline filler should be rendered.
478
- */
479
- _updateChildren(viewElement, options) {
480
- const domElement = this.domConverter.mapViewToDom(viewElement);
481
- if (!domElement) {
482
- // If there is no `domElement` it means that it was already removed from DOM.
483
- // There is no need to process it. It will be processed when re-inserted.
484
- return;
485
- }
486
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
487
- // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update children',
488
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
489
- // @if CK_DEBUG_TYPING // );
490
- // @if CK_DEBUG_TYPING // }
491
- // IME on Android inserts a new text node while typing after a link
492
- // instead of updating an existing text node that follows the link.
493
- // We must normalize those text nodes so the diff won't get confused.
494
- // https://github.com/ckeditor/ckeditor5/issues/12574.
495
- if (env.isAndroid) {
496
- let previousDomNode = null;
497
- for (const domNode of Array.from(domElement.childNodes)) {
498
- if (previousDomNode && isText(previousDomNode) && isText(domNode)) {
499
- domElement.normalize();
500
- break;
501
- }
502
- previousDomNode = domNode;
503
- }
504
- }
505
- const inlineFillerPosition = options.inlineFillerPosition;
506
- const actualDomChildren = domElement.childNodes;
507
- const expectedDomChildren = Array.from(this.domConverter.viewChildrenToDom(viewElement, { bind: true }));
508
- // Inline filler element has to be created as it is present in the DOM, but not in the view. It is required
509
- // during diffing so text nodes could be compared correctly and also during rendering to maintain
510
- // proper order and indexes while updating the DOM.
511
- if (inlineFillerPosition && inlineFillerPosition.parent === viewElement) {
512
- addInlineFiller(domElement.ownerDocument, expectedDomChildren, inlineFillerPosition.offset);
513
- }
514
- const diff = this._diffNodeLists(actualDomChildren, expectedDomChildren);
515
- // We need to make sure that we update the existing text node and not replace it with another one.
516
- // The composition and different "language" browser extensions are fragile to text node being completely replaced.
517
- const actions = this._findUpdateActions(diff, actualDomChildren, expectedDomChildren, areTextNodes);
518
- let i = 0;
519
- const nodesToUnbind = new Set();
520
- // Handle deletions first.
521
- // This is to prevent a situation where an element that already exists in `actualDomChildren` is inserted at a different
522
- // index in `actualDomChildren`. Since `actualDomChildren` is a `NodeList`, this works like move, not like an insert,
523
- // and it disrupts the whole algorithm. See https://github.com/ckeditor/ckeditor5/issues/6367.
524
- //
525
- // It doesn't matter in what order we remove or add nodes, as long as we remove and add correct nodes at correct indexes.
526
- for (const action of actions) {
527
- if (action === 'delete') {
528
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
529
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Remove node',
530
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', actualDomChildren[ i ]
531
- // @if CK_DEBUG_TYPING // );
532
- // @if CK_DEBUG_TYPING // }
533
- nodesToUnbind.add(actualDomChildren[i]);
534
- remove(actualDomChildren[i]);
535
- }
536
- else if (action === 'equal' || action === 'update') {
537
- i++;
538
- }
539
- }
540
- i = 0;
541
- for (const action of actions) {
542
- if (action === 'insert') {
543
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
544
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Insert node',
545
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', expectedDomChildren[ i ]
546
- // @if CK_DEBUG_TYPING // );
547
- // @if CK_DEBUG_TYPING // }
548
- insertAt(domElement, i, expectedDomChildren[i]);
549
- i++;
550
- }
551
- // Update the existing text node data. Note that replace action is generated only for Android for now.
552
- else if (action === 'update') {
553
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
554
- // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update text node',
555
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
556
- // @if CK_DEBUG_TYPING // );
557
- // @if CK_DEBUG_TYPING // }
558
- updateTextNode(actualDomChildren[i], expectedDomChildren[i].data);
559
- i++;
560
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
561
- // @if CK_DEBUG_TYPING // console.groupEnd();
562
- // @if CK_DEBUG_TYPING // }
563
- }
564
- else if (action === 'equal') {
565
- // Force updating text nodes inside elements which did not change and do not need to be re-rendered (#1125).
566
- // Do it here (not in the loop above) because only after insertions the `i` index is correct.
567
- this._markDescendantTextToSync(this.domConverter.domToView(expectedDomChildren[i]));
568
- i++;
569
- }
570
- }
571
- // Unbind removed nodes. When node does not have a parent it means that it was removed from DOM tree during
572
- // comparison with the expected DOM. We don't need to check child nodes, because if child node was reinserted,
573
- // it was moved to DOM tree out of the removed node.
574
- for (const node of nodesToUnbind) {
575
- if (!node.parentNode) {
576
- this.domConverter.unbindDomElement(node);
577
- }
578
- }
579
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
580
- // @if CK_DEBUG_TYPING // console.groupEnd();
581
- // @if CK_DEBUG_TYPING // }
582
- }
583
- /**
584
- * Shorthand for diffing two arrays or node lists of DOM nodes.
585
- *
586
- * @param actualDomChildren Actual DOM children
587
- * @param expectedDomChildren Expected DOM children.
588
- * @returns The list of actions based on the {@link module:utils/diff~diff} function.
589
- */
590
- _diffNodeLists(actualDomChildren, expectedDomChildren) {
591
- actualDomChildren = filterOutFakeSelectionContainer(actualDomChildren, this._fakeSelectionContainer);
592
- return diff(actualDomChildren, expectedDomChildren, sameNodes.bind(null, this.domConverter));
593
- }
594
- /**
595
- * Finds DOM nodes that were replaced with the similar nodes (same tag name) in the view. All nodes are compared
596
- * within one `insert`/`delete` action group, for example:
597
- *
598
- * ```
599
- * Actual DOM: <p><b>Foo</b>Bar<i>Baz</i><b>Bax</b></p>
600
- * Expected DOM: <p>Bar<b>123</b><i>Baz</i><b>456</b></p>
601
- * Input actions: [ insert, insert, delete, delete, equal, insert, delete ]
602
- * Output actions: [ insert, replace, delete, equal, replace ]
603
- * ```
604
- *
605
- * @param actions Actions array which is a result of the {@link module:utils/diff~diff} function.
606
- * @param actualDom Actual DOM children
607
- * @param expectedDom Expected DOM children.
608
- * @param comparator A comparator function that should return `true` if the given node should be reused
609
- * (either by the update of a text node data or an element children list for similar elements).
610
- * @returns Actions array modified with the `update` actions.
611
- */
612
- _findUpdateActions(actions, actualDom, expectedDom, comparator) {
613
- // If there is no both 'insert' and 'delete' actions, no need to check for replaced elements.
614
- if (actions.indexOf('insert') === -1 || actions.indexOf('delete') === -1) {
615
- return actions;
616
- }
617
- let newActions = [];
618
- let actualSlice = [];
619
- let expectedSlice = [];
620
- const counter = { equal: 0, insert: 0, delete: 0 };
621
- for (const action of actions) {
622
- if (action === 'insert') {
623
- expectedSlice.push(expectedDom[counter.equal + counter.insert]);
624
- }
625
- else if (action === 'delete') {
626
- actualSlice.push(actualDom[counter.equal + counter.delete]);
627
- }
628
- else { // equal
629
- newActions = newActions.concat(diff(actualSlice, expectedSlice, comparator)
630
- .map(action => action === 'equal' ? 'update' : action));
631
- newActions.push('equal');
632
- // Reset stored elements on 'equal'.
633
- actualSlice = [];
634
- expectedSlice = [];
635
- }
636
- counter[action]++;
637
- }
638
- return newActions.concat(diff(actualSlice, expectedSlice, comparator)
639
- .map(action => action === 'equal' ? 'update' : action));
640
- }
641
- /**
642
- * Marks text nodes to be synchronized.
643
- *
644
- * If a text node is passed, it will be marked. If an element is passed, all descendant text nodes inside it will be marked.
645
- *
646
- * @param viewNode View node to sync.
647
- */
648
- _markDescendantTextToSync(viewNode) {
649
- if (!viewNode) {
650
- return;
651
- }
652
- if (viewNode.is('$text')) {
653
- this.markedTexts.add(viewNode);
654
- }
655
- else if (viewNode.is('element')) {
656
- for (const child of viewNode.getChildren()) {
657
- this._markDescendantTextToSync(child);
658
- }
659
- }
660
- }
661
- /**
662
- * Checks if the selection needs to be updated and possibly updates it.
663
- */
664
- _updateSelection() {
665
- // Block updating DOM selection in (non-Android) Blink while the user is selecting to prevent accidental selection collapsing.
666
- // Note: Structural changes in DOM must trigger selection rendering, though. Nodes the selection was anchored
667
- // to, may disappear in DOM which would break the selection (e.g. in real-time collaboration scenarios).
668
- // https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723
669
- if (env.isBlink && !env.isAndroid && this.isSelecting && !this.markedChildren.size) {
670
- return;
671
- }
672
- // If there is no selection - remove DOM and fake selections.
673
- if (this.selection.rangeCount === 0) {
674
- this._removeDomSelection();
675
- this._removeFakeSelection();
676
- return;
677
- }
678
- const domRoot = this.domConverter.mapViewToDom(this.selection.editableElement);
679
- // Do nothing if there is no focus, or there is no DOM element corresponding to selection's editable element.
680
- if (!this.isFocused || !domRoot) {
681
- return;
682
- }
683
- // Render fake selection - create the fake selection container (if needed) and move DOM selection to it.
684
- if (this.selection.isFake) {
685
- this._updateFakeSelection(domRoot);
686
- }
687
- // There was a fake selection so remove it and update the DOM selection.
688
- // This is especially important on Android because otherwise IME will try to compose over the fake selection container.
689
- else if (this._fakeSelectionContainer && this._fakeSelectionContainer.isConnected) {
690
- this._removeFakeSelection();
691
- this._updateDomSelection(domRoot);
692
- }
693
- // Update the DOM selection in case of a plain selection change (no fake selection is involved).
694
- // On non-Android the whole rendering is disabled in composition mode (including DOM selection update),
695
- // but updating DOM selection should be also disabled on Android if in the middle of the composition
696
- // (to not interrupt it).
697
- else if (!(this.isComposing && env.isAndroid)) {
698
- this._updateDomSelection(domRoot);
699
- }
700
- }
701
- /**
702
- * Updates the fake selection.
703
- *
704
- * @param domRoot A valid DOM root where the fake selection container should be added.
705
- */
706
- _updateFakeSelection(domRoot) {
707
- const domDocument = domRoot.ownerDocument;
708
- if (!this._fakeSelectionContainer) {
709
- this._fakeSelectionContainer = createFakeSelectionContainer(domDocument);
710
- }
711
- const container = this._fakeSelectionContainer;
712
- // Bind fake selection container with the current selection *position*.
713
- this.domConverter.bindFakeSelection(container, this.selection);
714
- if (!this._fakeSelectionNeedsUpdate(domRoot)) {
715
- return;
716
- }
717
- if (!container.parentElement || container.parentElement != domRoot) {
718
- domRoot.appendChild(container);
719
- }
720
- container.textContent = this.selection.fakeSelectionLabel || '\u00A0';
721
- const domSelection = domDocument.getSelection();
722
- const domRange = domDocument.createRange();
723
- domSelection.removeAllRanges();
724
- domRange.selectNodeContents(container);
725
- domSelection.addRange(domRange);
726
- }
727
- /**
728
- * Updates the DOM selection.
729
- *
730
- * @param domRoot A valid DOM root where the DOM selection should be rendered.
731
- */
732
- _updateDomSelection(domRoot) {
733
- const domSelection = domRoot.ownerDocument.defaultView.getSelection();
734
- // Let's check whether DOM selection needs updating at all.
735
- if (!this._domSelectionNeedsUpdate(domSelection)) {
736
- return;
737
- }
738
- // Multi-range selection is not available in most browsers, and, at least in Chrome, trying to
739
- // set such selection, that is not continuous, throws an error. Because of that, we will just use anchor
740
- // and focus of view selection.
741
- // Since we are not supporting multi-range selection, we also do not need to check if proper editable is
742
- // selected. If there is any editable selected, it is okay (editable is taken from selection anchor).
743
- const anchor = this.domConverter.viewPositionToDom(this.selection.anchor);
744
- const focus = this.domConverter.viewPositionToDom(this.selection.focus);
745
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
746
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update DOM selection:',
747
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', anchor, focus
748
- // @if CK_DEBUG_TYPING // );
749
- // @if CK_DEBUG_TYPING // }
750
- domSelection.setBaseAndExtent(anchor.parent, anchor.offset, focus.parent, focus.offset);
751
- // Firefox–specific hack (https://github.com/ckeditor/ckeditor5-engine/issues/1439).
752
- if (env.isGecko) {
753
- fixGeckoSelectionAfterBr(focus, domSelection);
754
- }
755
- }
756
- /**
757
- * Checks whether a given DOM selection needs to be updated.
758
- *
759
- * @param domSelection The DOM selection to check.
760
- */
761
- _domSelectionNeedsUpdate(domSelection) {
762
- if (!this.domConverter.isDomSelectionCorrect(domSelection)) {
763
- // Current DOM selection is in incorrect position. We need to update it.
764
- return true;
765
- }
766
- const oldViewSelection = domSelection && this.domConverter.domSelectionToView(domSelection);
767
- if (oldViewSelection && this.selection.isEqual(oldViewSelection)) {
768
- return false;
769
- }
770
- // If selection is not collapsed, it does not need to be updated if it is similar.
771
- if (!this.selection.isCollapsed && this.selection.isSimilar(oldViewSelection)) {
772
- // Selection did not changed and is correct, do not update.
773
- return false;
774
- }
775
- // Selections are not similar.
776
- return true;
777
- }
778
- /**
779
- * Checks whether the fake selection needs to be updated.
780
- *
781
- * @param domRoot A valid DOM root where a new fake selection container should be added.
782
- */
783
- _fakeSelectionNeedsUpdate(domRoot) {
784
- const container = this._fakeSelectionContainer;
785
- const domSelection = domRoot.ownerDocument.getSelection();
786
- // Fake selection needs to be updated if there's no fake selection container, or the container currently sits
787
- // in a different root.
788
- if (!container || container.parentElement !== domRoot) {
789
- return true;
790
- }
791
- // Make sure that the selection actually is within the fake selection.
792
- if (domSelection.anchorNode !== container && !container.contains(domSelection.anchorNode)) {
793
- return true;
794
- }
795
- return container.textContent !== this.selection.fakeSelectionLabel;
796
- }
797
- /**
798
- * Removes the DOM selection.
799
- */
800
- _removeDomSelection() {
801
- for (const doc of this.domDocuments) {
802
- const domSelection = doc.getSelection();
803
- if (domSelection.rangeCount) {
804
- const activeDomElement = doc.activeElement;
805
- const viewElement = this.domConverter.mapDomToView(activeDomElement);
806
- if (activeDomElement && viewElement) {
807
- domSelection.removeAllRanges();
808
- }
809
- }
810
- }
811
- }
812
- /**
813
- * Removes the fake selection.
814
- */
815
- _removeFakeSelection() {
816
- const container = this._fakeSelectionContainer;
817
- if (container) {
818
- container.remove();
819
- }
820
- }
821
- /**
822
- * Checks if focus needs to be updated and possibly updates it.
823
- */
824
- _updateFocus() {
825
- if (this.isFocused) {
826
- const editable = this.selection.editableElement;
827
- if (editable) {
828
- this.domConverter.focus(editable);
829
- }
830
- }
831
- }
832
- }
833
- /**
834
- * Checks if provided element is editable.
835
- */
836
- function isEditable(element) {
837
- if (element.getAttribute('contenteditable') == 'false') {
838
- return false;
839
- }
840
- const parent = element.findAncestor(element => element.hasAttribute('contenteditable'));
841
- return !parent || parent.getAttribute('contenteditable') == 'true';
842
- }
843
- /**
844
- * Adds inline filler at a given position.
845
- *
846
- * The position can be given as an array of DOM nodes and an offset in that array,
847
- * or a DOM parent element and an offset in that element.
848
- *
849
- * @returns The DOM text node that contains an inline filler.
850
- */
851
- function addInlineFiller(domDocument, domParentOrArray, offset) {
852
- const childNodes = domParentOrArray instanceof Array ? domParentOrArray : domParentOrArray.childNodes;
853
- const nodeAfterFiller = childNodes[offset];
854
- if (isText(nodeAfterFiller)) {
855
- nodeAfterFiller.data = INLINE_FILLER + nodeAfterFiller.data;
856
- return nodeAfterFiller;
857
- }
858
- else {
859
- const fillerNode = domDocument.createTextNode(INLINE_FILLER);
860
- if (Array.isArray(domParentOrArray)) {
861
- childNodes.splice(offset, 0, fillerNode);
862
- }
863
- else {
864
- insertAt(domParentOrArray, offset, fillerNode);
865
- }
866
- return fillerNode;
867
- }
868
- }
869
- /**
870
- * Whether two DOM nodes should be considered as similar.
871
- * Nodes are considered similar if they have the same tag name.
872
- */
873
- function areSimilarElements(node1, node2) {
874
- return isNode(node1) && isNode(node2) &&
875
- !isText(node1) && !isText(node2) &&
876
- !isComment(node1) && !isComment(node2) &&
877
- node1.tagName.toLowerCase() === node2.tagName.toLowerCase();
878
- }
879
- /**
880
- * Whether two DOM nodes are text nodes.
881
- */
882
- function areTextNodes(node1, node2) {
883
- return isNode(node1) && isNode(node2) &&
884
- isText(node1) && isText(node2);
885
- }
886
- /**
887
- * Whether two dom nodes should be considered as the same.
888
- * Two nodes which are considered the same are:
889
- *
890
- * * Text nodes with the same text.
891
- * * Element nodes represented by the same object.
892
- * * Two block filler elements.
893
- *
894
- * @param blockFillerMode Block filler mode, see {@link module:engine/view/domconverter~DomConverter#blockFillerMode}.
895
- */
896
- function sameNodes(domConverter, actualDomChild, expectedDomChild) {
897
- // Elements.
898
- if (actualDomChild === expectedDomChild) {
899
- return true;
900
- }
901
- // Texts.
902
- else if (isText(actualDomChild) && isText(expectedDomChild)) {
903
- return actualDomChild.data === expectedDomChild.data;
904
- }
905
- // Block fillers.
906
- else if (domConverter.isBlockFiller(actualDomChild) &&
907
- domConverter.isBlockFiller(expectedDomChild)) {
908
- return true;
909
- }
910
- // Not matching types.
911
- return false;
912
- }
913
- /**
914
- * The following is a Firefox–specific hack (https://github.com/ckeditor/ckeditor5-engine/issues/1439).
915
- * When the native DOM selection is at the end of the block and preceded by <br /> e.g.
916
- *
917
- * ```html
918
- * <p>foo<br/>[]</p>
919
- * ```
920
- *
921
- * which happens a lot when using the soft line break, the browser fails to (visually) move the
922
- * caret to the new line. A quick fix is as simple as force–refreshing the selection with the same range.
923
- */
924
- function fixGeckoSelectionAfterBr(focus, domSelection) {
925
- const parent = focus.parent;
926
- // This fix works only when the focus point is at the very end of an element.
927
- // There is no point in running it in cases unrelated to the browser bug.
928
- if (parent.nodeType != Node.ELEMENT_NODE || focus.offset != parent.childNodes.length - 1) {
929
- return;
930
- }
931
- const childAtOffset = parent.childNodes[focus.offset];
932
- // To stay on the safe side, the fix being as specific as possible, it targets only the
933
- // selection which is at the very end of the element and preceded by <br />.
934
- if (childAtOffset && childAtOffset.tagName == 'BR') {
935
- domSelection.addRange(domSelection.getRangeAt(0));
936
- }
937
- }
938
- function filterOutFakeSelectionContainer(domChildList, fakeSelectionContainer) {
939
- const childList = Array.from(domChildList);
940
- if (childList.length == 0 || !fakeSelectionContainer) {
941
- return childList;
942
- }
943
- const last = childList[childList.length - 1];
944
- if (last == fakeSelectionContainer) {
945
- childList.pop();
946
- }
947
- return childList;
948
- }
949
- /**
950
- * Creates a fake selection container for a given document.
951
- */
952
- function createFakeSelectionContainer(domDocument) {
953
- const container = domDocument.createElement('div');
954
- container.className = 'ck-fake-selection-container';
955
- Object.assign(container.style, {
956
- position: 'fixed',
957
- top: 0,
958
- left: '-9999px',
959
- // See https://github.com/ckeditor/ckeditor5/issues/752.
960
- width: '42px'
961
- });
962
- // Fill it with a text node so we can update it later.
963
- container.textContent = '\u00A0';
964
- return container;
965
- }
966
- /**
967
- * Checks if text needs to be updated and possibly updates it by removing and inserting only parts
968
- * of the data from the existing text node to reduce impact on the IME composition.
969
- *
970
- * @param domText DOM text node to update.
971
- * @param expectedText The expected data of a text node.
972
- */
973
- function updateTextNode(domText, expectedText) {
974
- const actualText = domText.data;
975
- if (actualText == expectedText) {
976
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
977
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Text node does not need update:',
978
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '',
979
- // @if CK_DEBUG_TYPING // `"${ domText.data }" (${ domText.data.length })`
980
- // @if CK_DEBUG_TYPING // );
981
- // @if CK_DEBUG_TYPING // }
982
- return;
983
- }
984
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
985
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update text node:',
986
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '',
987
- // @if CK_DEBUG_TYPING // `"${ domText.data }" (${ domText.data.length }) -> "${ expectedText }" (${ expectedText.length })`
988
- // @if CK_DEBUG_TYPING // );
989
- // @if CK_DEBUG_TYPING // }
990
- const actions = fastDiff(actualText, expectedText);
991
- for (const action of actions) {
992
- if (action.type === 'insert') {
993
- domText.insertData(action.index, action.values.join(''));
994
- }
995
- else { // 'delete'
996
- domText.deleteData(action.index, action.howMany);
997
- }
998
- }
999
- }
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/renderer
7
+ */
8
+ import ViewText from './text';
9
+ import ViewPosition from './position';
10
+ import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller } from './filler';
11
+ import { CKEditorError, ObservableMixin, diff, env, fastDiff, insertAt, isComment, isNode, isText, remove } from '@ckeditor/ckeditor5-utils';
12
+ import '../../theme/renderer.css';
13
+ /**
14
+ * Renderer is responsible for updating the DOM structure and the DOM selection based on
15
+ * the {@link module:engine/view/renderer~Renderer#markToSync information about updated view nodes}.
16
+ * In other words, it renders the view to the DOM.
17
+ *
18
+ * Its main responsibility is to make only the necessary, minimal changes to the DOM. However, unlike in many
19
+ * virtual DOM implementations, the primary reason for doing minimal changes is not the performance but ensuring
20
+ * that native editing features such as text composition, autocompletion, spell checking, selection's x-index are
21
+ * affected as little as possible.
22
+ *
23
+ * Renderer uses {@link module:engine/view/domconverter~DomConverter} to transform view nodes and positions
24
+ * to and from the DOM.
25
+ */
26
+ export default class Renderer extends ObservableMixin() {
27
+ /**
28
+ * Creates a renderer instance.
29
+ *
30
+ * @param domConverter Converter instance.
31
+ * @param selection View selection.
32
+ */
33
+ constructor(domConverter, selection) {
34
+ super();
35
+ /**
36
+ * Set of DOM Documents instances.
37
+ */
38
+ this.domDocuments = new Set();
39
+ /**
40
+ * Set of nodes which attributes changed and may need to be rendered.
41
+ */
42
+ this.markedAttributes = new Set();
43
+ /**
44
+ * Set of elements which child lists changed and may need to be rendered.
45
+ */
46
+ this.markedChildren = new Set();
47
+ /**
48
+ * Set of text nodes which text data changed and may need to be rendered.
49
+ */
50
+ this.markedTexts = new Set();
51
+ /**
52
+ * The text node in which the inline filler was rendered.
53
+ */
54
+ this._inlineFiller = null;
55
+ /**
56
+ * DOM element containing fake selection.
57
+ */
58
+ this._fakeSelectionContainer = null;
59
+ this.domConverter = domConverter;
60
+ this.selection = selection;
61
+ this.set('isFocused', false);
62
+ this.set('isSelecting', false);
63
+ // Rendering the selection and inline filler manipulation should be postponed in (non-Android) Blink until the user finishes
64
+ // creating the selection in DOM to avoid accidental selection collapsing
65
+ // (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
66
+ // When the user stops selecting, all pending changes should be rendered ASAP, though.
67
+ if (env.isBlink && !env.isAndroid) {
68
+ this.on('change:isSelecting', () => {
69
+ if (!this.isSelecting) {
70
+ this.render();
71
+ }
72
+ });
73
+ }
74
+ this.set('isComposing', false);
75
+ this.on('change:isComposing', () => {
76
+ if (!this.isComposing) {
77
+ this.render();
78
+ }
79
+ });
80
+ }
81
+ /**
82
+ * Marks a view node to be updated in the DOM by {@link #render `render()`}.
83
+ *
84
+ * Note that only view nodes whose parents have corresponding DOM elements need to be marked to be synchronized.
85
+ *
86
+ * @see #markedAttributes
87
+ * @see #markedChildren
88
+ * @see #markedTexts
89
+ *
90
+ * @param type Type of the change.
91
+ * @param node ViewNode to be marked.
92
+ */
93
+ markToSync(type, node) {
94
+ if (type === 'text') {
95
+ if (this.domConverter.mapViewToDom(node.parent)) {
96
+ this.markedTexts.add(node);
97
+ }
98
+ }
99
+ else {
100
+ // If the node has no DOM element it is not rendered yet,
101
+ // its children/attributes do not need to be marked to be sync.
102
+ if (!this.domConverter.mapViewToDom(node)) {
103
+ return;
104
+ }
105
+ if (type === 'attributes') {
106
+ this.markedAttributes.add(node);
107
+ }
108
+ else if (type === 'children') {
109
+ this.markedChildren.add(node);
110
+ }
111
+ else {
112
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
113
+ const unreachable = type;
114
+ /**
115
+ * Unknown type passed to Renderer.markToSync.
116
+ *
117
+ * @error view-renderer-unknown-type
118
+ */
119
+ throw new CKEditorError('view-renderer-unknown-type', this);
120
+ }
121
+ }
122
+ }
123
+ /**
124
+ * Renders all buffered changes ({@link #markedAttributes}, {@link #markedChildren} and {@link #markedTexts}) and
125
+ * the current view selection (if needed) to the DOM by applying a minimal set of changes to it.
126
+ *
127
+ * Renderer tries not to break the text composition (e.g. IME) and x-index of the selection,
128
+ * so it does as little as it is needed to update the DOM.
129
+ *
130
+ * Renderer also handles {@link module:engine/view/filler fillers}. Especially, it checks if the inline filler is needed
131
+ * at the selection position and adds or removes it. To prevent breaking text composition inline filler will not be
132
+ * removed as long as the selection is in the text node which needed it at first.
133
+ */
134
+ render() {
135
+ // Ignore rendering while in the composition mode. Composition events are not cancellable and browser will modify the DOM tree.
136
+ // All marked elements, attributes, etc. will wait until next render after the composition ends.
137
+ // On Android composition events are immediately applied to the model, so we don't need to skip rendering,
138
+ // and we should not do it because the difference between view and DOM could lead to position mapping problems.
139
+ if (this.isComposing && !env.isAndroid) {
140
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
141
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Rendering aborted while isComposing',
142
+ // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
143
+ // @if CK_DEBUG_TYPING // );
144
+ // @if CK_DEBUG_TYPING // }
145
+ return;
146
+ }
147
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
148
+ // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Rendering',
149
+ // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
150
+ // @if CK_DEBUG_TYPING // );
151
+ // @if CK_DEBUG_TYPING // }
152
+ let inlineFillerPosition = null;
153
+ const isInlineFillerRenderingPossible = env.isBlink && !env.isAndroid ? !this.isSelecting : true;
154
+ // Refresh mappings.
155
+ for (const element of this.markedChildren) {
156
+ this._updateChildrenMappings(element);
157
+ }
158
+ // Don't manipulate inline fillers while the selection is being made in (non-Android) Blink to prevent accidental
159
+ // DOM selection collapsing
160
+ // (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
161
+ if (isInlineFillerRenderingPossible) {
162
+ // There was inline filler rendered in the DOM but it's not
163
+ // at the selection position any more, so we can remove it
164
+ // (cause even if it's needed, it must be placed in another location).
165
+ if (this._inlineFiller && !this._isSelectionInInlineFiller()) {
166
+ this._removeInlineFiller();
167
+ }
168
+ // If we've got the filler, let's try to guess its position in the view.
169
+ if (this._inlineFiller) {
170
+ inlineFillerPosition = this._getInlineFillerPosition();
171
+ }
172
+ // Otherwise, if it's needed, create it at the selection position.
173
+ else if (this._needsInlineFillerAtSelection()) {
174
+ inlineFillerPosition = this.selection.getFirstPosition();
175
+ // Do not use `markToSync` so it will be added even if the parent is already added.
176
+ this.markedChildren.add(inlineFillerPosition.parent);
177
+ }
178
+ }
179
+ // Make sure the inline filler has any parent, so it can be mapped to view position by DomConverter.
180
+ else if (this._inlineFiller && this._inlineFiller.parentNode) {
181
+ // While the user is making selection, preserve the inline filler at its original position.
182
+ inlineFillerPosition = this.domConverter.domPositionToView(this._inlineFiller);
183
+ // While down-casting the document selection attributes, all existing empty
184
+ // attribute elements (for selection position) are removed from the view and DOM,
185
+ // so make sure that we were able to map filler position.
186
+ // https://github.com/ckeditor/ckeditor5/issues/12026
187
+ if (inlineFillerPosition && inlineFillerPosition.parent.is('$text')) {
188
+ // The inline filler position is expected to be before the text node.
189
+ inlineFillerPosition = ViewPosition._createBefore(inlineFillerPosition.parent);
190
+ }
191
+ }
192
+ for (const element of this.markedAttributes) {
193
+ this._updateAttrs(element);
194
+ }
195
+ for (const element of this.markedChildren) {
196
+ this._updateChildren(element, { inlineFillerPosition });
197
+ }
198
+ for (const node of this.markedTexts) {
199
+ if (!this.markedChildren.has(node.parent) && this.domConverter.mapViewToDom(node.parent)) {
200
+ this._updateText(node, { inlineFillerPosition });
201
+ }
202
+ }
203
+ // * Check whether the inline filler is required and where it really is in the DOM.
204
+ // At this point in most cases it will be in the DOM, but there are exceptions.
205
+ // For example, if the inline filler was deep in the created DOM structure, it will not be created.
206
+ // Similarly, if it was removed at the beginning of this function and then neither text nor children were updated,
207
+ // it will not be present. Fix those and similar scenarios.
208
+ // * Don't manipulate inline fillers while the selection is being made in (non-Android) Blink to prevent accidental
209
+ // DOM selection collapsing
210
+ // (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
211
+ if (isInlineFillerRenderingPossible) {
212
+ if (inlineFillerPosition) {
213
+ const fillerDomPosition = this.domConverter.viewPositionToDom(inlineFillerPosition);
214
+ const domDocument = fillerDomPosition.parent.ownerDocument;
215
+ if (!startsWithFiller(fillerDomPosition.parent)) {
216
+ // Filler has not been created at filler position. Create it now.
217
+ this._inlineFiller = addInlineFiller(domDocument, fillerDomPosition.parent, fillerDomPosition.offset);
218
+ }
219
+ else {
220
+ // Filler has been found, save it.
221
+ this._inlineFiller = fillerDomPosition.parent;
222
+ }
223
+ }
224
+ else {
225
+ // There is no filler needed.
226
+ this._inlineFiller = null;
227
+ }
228
+ }
229
+ // First focus the new editing host, then update the selection.
230
+ // Otherwise, FF may throw an error (https://github.com/ckeditor/ckeditor5/issues/721).
231
+ this._updateFocus();
232
+ this._updateSelection();
233
+ this.domConverter._clearTemporaryCustomProperties();
234
+ this.markedTexts.clear();
235
+ this.markedAttributes.clear();
236
+ this.markedChildren.clear();
237
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
238
+ // @if CK_DEBUG_TYPING // console.groupEnd();
239
+ // @if CK_DEBUG_TYPING // }
240
+ }
241
+ /**
242
+ * Updates mappings of view element's children.
243
+ *
244
+ * Children that were replaced in the view structure by similar elements (same tag name) are treated as 'replaced'.
245
+ * This means that their mappings can be updated so the new view elements are mapped to the existing DOM elements.
246
+ * Thanks to that these elements do not need to be re-rendered completely.
247
+ *
248
+ * @param viewElement The view element whose children mappings will be updated.
249
+ */
250
+ _updateChildrenMappings(viewElement) {
251
+ const domElement = this.domConverter.mapViewToDom(viewElement);
252
+ if (!domElement) {
253
+ // If there is no `domElement` it means that it was already removed from DOM and there is no need to process it.
254
+ return;
255
+ }
256
+ // Removing nodes from the DOM as we iterate can cause `actualDomChildren`
257
+ // (which is a live-updating `NodeList`) to get out of sync with the
258
+ // indices that we compute as we iterate over `actions`.
259
+ // This would produce incorrect element mappings.
260
+ //
261
+ // Converting live list to an array to make the list static.
262
+ const actualDomChildren = Array.from(domElement.childNodes);
263
+ const expectedDomChildren = Array.from(this.domConverter.viewChildrenToDom(viewElement, { withChildren: false }));
264
+ const diff = this._diffNodeLists(actualDomChildren, expectedDomChildren);
265
+ const actions = this._findUpdateActions(diff, actualDomChildren, expectedDomChildren, areSimilarElements);
266
+ if (actions.indexOf('update') !== -1) {
267
+ const counter = { equal: 0, insert: 0, delete: 0 };
268
+ for (const action of actions) {
269
+ if (action === 'update') {
270
+ const insertIndex = counter.equal + counter.insert;
271
+ const deleteIndex = counter.equal + counter.delete;
272
+ const viewChild = viewElement.getChild(insertIndex);
273
+ // UIElement and RawElement are special cases. Their children are not stored in a view (#799)
274
+ // so we cannot use them with replacing flow (since they use view children during rendering
275
+ // which will always result in rendering empty elements).
276
+ if (viewChild && !viewChild.is('uiElement') && !viewChild.is('rawElement')) {
277
+ this._updateElementMappings(viewChild, actualDomChildren[deleteIndex]);
278
+ }
279
+ remove(expectedDomChildren[insertIndex]);
280
+ counter.equal++;
281
+ }
282
+ else {
283
+ counter[action]++;
284
+ }
285
+ }
286
+ }
287
+ }
288
+ /**
289
+ * Updates mappings of a given view element.
290
+ *
291
+ * @param viewElement The view element whose mappings will be updated.
292
+ * @param domElement The DOM element representing the given view element.
293
+ */
294
+ _updateElementMappings(viewElement, domElement) {
295
+ // Remap 'DomConverter' bindings.
296
+ this.domConverter.unbindDomElement(domElement);
297
+ this.domConverter.bindElements(domElement, viewElement);
298
+ // View element may have children which needs to be updated, but are not marked, mark them to update.
299
+ this.markedChildren.add(viewElement);
300
+ // Because we replace new view element mapping with the existing one, the corresponding DOM element
301
+ // will not be rerendered. The new view element may have different attributes than the previous one.
302
+ // Since its corresponding DOM element will not be rerendered, new attributes will not be added
303
+ // to the DOM, so we need to mark it here to make sure its attributes gets updated. See #1427 for more
304
+ // detailed case study.
305
+ // Also there are cases where replaced element is removed from the view structure and then has
306
+ // its attributes changed or removed. In such cases the element will not be present in `markedAttributes`
307
+ // and also may be the same (`element.isSimilar()`) as the reused element not having its attributes updated.
308
+ // To prevent such situations we always mark reused element to have its attributes rerenderd (#1560).
309
+ this.markedAttributes.add(viewElement);
310
+ }
311
+ /**
312
+ * Gets the position of the inline filler based on the current selection.
313
+ * Here, we assume that we know that the filler is needed and
314
+ * {@link #_isSelectionInInlineFiller is at the selection position}, and, since it is needed,
315
+ * it is somewhere at the selection position.
316
+ *
317
+ * Note: The filler position cannot be restored based on the filler's DOM text node, because
318
+ * when this method is called (before rendering), the bindings will often be broken. View-to-DOM
319
+ * bindings are only dependable after rendering.
320
+ */
321
+ _getInlineFillerPosition() {
322
+ const firstPos = this.selection.getFirstPosition();
323
+ if (firstPos.parent.is('$text')) {
324
+ return ViewPosition._createBefore(firstPos.parent);
325
+ }
326
+ else {
327
+ return firstPos;
328
+ }
329
+ }
330
+ /**
331
+ * Returns `true` if the selection has not left the inline filler's text node.
332
+ * If it is `true`, it means that the filler had been added for a reason and the selection did not
333
+ * leave the filler's text node. For example, the user can be in the middle of a composition so it should not be touched.
334
+ *
335
+ * @returns `true` if the inline filler and selection are in the same place.
336
+ */
337
+ _isSelectionInInlineFiller() {
338
+ if (this.selection.rangeCount != 1 || !this.selection.isCollapsed) {
339
+ return false;
340
+ }
341
+ // Note, we can't check if selection's position equals position of the
342
+ // this._inlineFiller node, because of #663. We may not be able to calculate
343
+ // the filler's position in the view at this stage.
344
+ // Instead, we check it the other way – whether selection is anchored in
345
+ // that text node or next to it.
346
+ // Possible options are:
347
+ // "FILLER{}"
348
+ // "FILLERadded-text{}"
349
+ const selectionPosition = this.selection.getFirstPosition();
350
+ const position = this.domConverter.viewPositionToDom(selectionPosition);
351
+ if (position && isText(position.parent) && startsWithFiller(position.parent)) {
352
+ return true;
353
+ }
354
+ return false;
355
+ }
356
+ /**
357
+ * Removes the inline filler.
358
+ */
359
+ _removeInlineFiller() {
360
+ const domFillerNode = this._inlineFiller;
361
+ // Something weird happened and the stored node doesn't contain the filler's text.
362
+ if (!startsWithFiller(domFillerNode)) {
363
+ /**
364
+ * The inline filler node was lost. Most likely, something overwrote the filler text node
365
+ * in the DOM.
366
+ *
367
+ * @error view-renderer-filler-was-lost
368
+ */
369
+ throw new CKEditorError('view-renderer-filler-was-lost', this);
370
+ }
371
+ if (isInlineFiller(domFillerNode)) {
372
+ domFillerNode.remove();
373
+ }
374
+ else {
375
+ domFillerNode.data = domFillerNode.data.substr(INLINE_FILLER_LENGTH);
376
+ }
377
+ this._inlineFiller = null;
378
+ }
379
+ /**
380
+ * Checks if the inline {@link module:engine/view/filler filler} should be added.
381
+ *
382
+ * @returns `true` if the inline filler should be added.
383
+ */
384
+ _needsInlineFillerAtSelection() {
385
+ if (this.selection.rangeCount != 1 || !this.selection.isCollapsed) {
386
+ return false;
387
+ }
388
+ const selectionPosition = this.selection.getFirstPosition();
389
+ const selectionParent = selectionPosition.parent;
390
+ const selectionOffset = selectionPosition.offset;
391
+ // If there is no DOM root we do not care about fillers.
392
+ if (!this.domConverter.mapViewToDom(selectionParent.root)) {
393
+ return false;
394
+ }
395
+ if (!(selectionParent.is('element'))) {
396
+ return false;
397
+ }
398
+ // Prevent adding inline filler inside elements with contenteditable=false.
399
+ // https://github.com/ckeditor/ckeditor5-engine/issues/1170
400
+ if (!isEditable(selectionParent)) {
401
+ return false;
402
+ }
403
+ // We have block filler, we do not need inline one.
404
+ if (selectionOffset === selectionParent.getFillerOffset()) {
405
+ return false;
406
+ }
407
+ const nodeBefore = selectionPosition.nodeBefore;
408
+ const nodeAfter = selectionPosition.nodeAfter;
409
+ if (nodeBefore instanceof ViewText || nodeAfter instanceof ViewText) {
410
+ return false;
411
+ }
412
+ // Do not use inline filler while typing outside inline elements on Android.
413
+ // The deleteContentBackward would remove part of the inline filler instead of removing last letter in a link.
414
+ if (env.isAndroid && (nodeBefore || nodeAfter)) {
415
+ return false;
416
+ }
417
+ return true;
418
+ }
419
+ /**
420
+ * Checks if text needs to be updated and possibly updates it.
421
+ *
422
+ * @param viewText View text to update.
423
+ * @param options.inlineFillerPosition The position where the inline filler should be rendered.
424
+ */
425
+ _updateText(viewText, options) {
426
+ const domText = this.domConverter.findCorrespondingDomText(viewText);
427
+ const newDomText = this.domConverter.viewToDom(viewText);
428
+ let expectedText = newDomText.data;
429
+ const filler = options.inlineFillerPosition;
430
+ if (filler && filler.parent == viewText.parent && filler.offset == viewText.index) {
431
+ expectedText = INLINE_FILLER + expectedText;
432
+ }
433
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
434
+ // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update text',
435
+ // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
436
+ // @if CK_DEBUG_TYPING // );
437
+ // @if CK_DEBUG_TYPING // }
438
+ updateTextNode(domText, expectedText);
439
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
440
+ // @if CK_DEBUG_TYPING // console.groupEnd();
441
+ // @if CK_DEBUG_TYPING // }
442
+ }
443
+ /**
444
+ * Checks if attribute list needs to be updated and possibly updates it.
445
+ *
446
+ * @param viewElement The view element to update.
447
+ */
448
+ _updateAttrs(viewElement) {
449
+ const domElement = this.domConverter.mapViewToDom(viewElement);
450
+ if (!domElement) {
451
+ // If there is no `domElement` it means that 'viewElement' is outdated as its mapping was updated
452
+ // in 'this._updateChildrenMappings()'. There is no need to process it as new view element which
453
+ // replaced old 'viewElement' mapping was also added to 'this.markedAttributes'
454
+ // in 'this._updateChildrenMappings()' so it will be processed separately.
455
+ return;
456
+ }
457
+ const domAttrKeys = Array.from(domElement.attributes).map(attr => attr.name);
458
+ const viewAttrKeys = viewElement.getAttributeKeys();
459
+ // Add or overwrite attributes.
460
+ for (const key of viewAttrKeys) {
461
+ this.domConverter.setDomElementAttribute(domElement, key, viewElement.getAttribute(key), viewElement);
462
+ }
463
+ // Remove from DOM attributes which do not exists in the view.
464
+ for (const key of domAttrKeys) {
465
+ // All other attributes not present in the DOM should be removed.
466
+ if (!viewElement.hasAttribute(key)) {
467
+ this.domConverter.removeDomElementAttribute(domElement, key);
468
+ }
469
+ }
470
+ }
471
+ /**
472
+ * Checks if elements child list needs to be updated and possibly updates it.
473
+ *
474
+ * Note that on Android, to reduce the risk of composition breaks, it tries to update data of an existing
475
+ * child text nodes instead of replacing them completely.
476
+ *
477
+ * @param viewElement View element to update.
478
+ * @param options.inlineFillerPosition The position where the inline filler should be rendered.
479
+ */
480
+ _updateChildren(viewElement, options) {
481
+ const domElement = this.domConverter.mapViewToDom(viewElement);
482
+ if (!domElement) {
483
+ // If there is no `domElement` it means that it was already removed from DOM.
484
+ // There is no need to process it. It will be processed when re-inserted.
485
+ return;
486
+ }
487
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
488
+ // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update children',
489
+ // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
490
+ // @if CK_DEBUG_TYPING // );
491
+ // @if CK_DEBUG_TYPING // }
492
+ // IME on Android inserts a new text node while typing after a link
493
+ // instead of updating an existing text node that follows the link.
494
+ // We must normalize those text nodes so the diff won't get confused.
495
+ // https://github.com/ckeditor/ckeditor5/issues/12574.
496
+ if (env.isAndroid) {
497
+ let previousDomNode = null;
498
+ for (const domNode of Array.from(domElement.childNodes)) {
499
+ if (previousDomNode && isText(previousDomNode) && isText(domNode)) {
500
+ domElement.normalize();
501
+ break;
502
+ }
503
+ previousDomNode = domNode;
504
+ }
505
+ }
506
+ const inlineFillerPosition = options.inlineFillerPosition;
507
+ const actualDomChildren = domElement.childNodes;
508
+ const expectedDomChildren = Array.from(this.domConverter.viewChildrenToDom(viewElement, { bind: true }));
509
+ // Inline filler element has to be created as it is present in the DOM, but not in the view. It is required
510
+ // during diffing so text nodes could be compared correctly and also during rendering to maintain
511
+ // proper order and indexes while updating the DOM.
512
+ if (inlineFillerPosition && inlineFillerPosition.parent === viewElement) {
513
+ addInlineFiller(domElement.ownerDocument, expectedDomChildren, inlineFillerPosition.offset);
514
+ }
515
+ const diff = this._diffNodeLists(actualDomChildren, expectedDomChildren);
516
+ // We need to make sure that we update the existing text node and not replace it with another one.
517
+ // The composition and different "language" browser extensions are fragile to text node being completely replaced.
518
+ const actions = this._findUpdateActions(diff, actualDomChildren, expectedDomChildren, areTextNodes);
519
+ let i = 0;
520
+ const nodesToUnbind = new Set();
521
+ // Handle deletions first.
522
+ // This is to prevent a situation where an element that already exists in `actualDomChildren` is inserted at a different
523
+ // index in `actualDomChildren`. Since `actualDomChildren` is a `NodeList`, this works like move, not like an insert,
524
+ // and it disrupts the whole algorithm. See https://github.com/ckeditor/ckeditor5/issues/6367.
525
+ //
526
+ // It doesn't matter in what order we remove or add nodes, as long as we remove and add correct nodes at correct indexes.
527
+ for (const action of actions) {
528
+ if (action === 'delete') {
529
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
530
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Remove node',
531
+ // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', actualDomChildren[ i ]
532
+ // @if CK_DEBUG_TYPING // );
533
+ // @if CK_DEBUG_TYPING // }
534
+ nodesToUnbind.add(actualDomChildren[i]);
535
+ remove(actualDomChildren[i]);
536
+ }
537
+ else if (action === 'equal' || action === 'update') {
538
+ i++;
539
+ }
540
+ }
541
+ i = 0;
542
+ for (const action of actions) {
543
+ if (action === 'insert') {
544
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
545
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Insert node',
546
+ // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', expectedDomChildren[ i ]
547
+ // @if CK_DEBUG_TYPING // );
548
+ // @if CK_DEBUG_TYPING // }
549
+ insertAt(domElement, i, expectedDomChildren[i]);
550
+ i++;
551
+ }
552
+ // Update the existing text node data. Note that replace action is generated only for Android for now.
553
+ else if (action === 'update') {
554
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
555
+ // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update text node',
556
+ // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
557
+ // @if CK_DEBUG_TYPING // );
558
+ // @if CK_DEBUG_TYPING // }
559
+ updateTextNode(actualDomChildren[i], expectedDomChildren[i].data);
560
+ i++;
561
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
562
+ // @if CK_DEBUG_TYPING // console.groupEnd();
563
+ // @if CK_DEBUG_TYPING // }
564
+ }
565
+ else if (action === 'equal') {
566
+ // Force updating text nodes inside elements which did not change and do not need to be re-rendered (#1125).
567
+ // Do it here (not in the loop above) because only after insertions the `i` index is correct.
568
+ this._markDescendantTextToSync(this.domConverter.domToView(expectedDomChildren[i]));
569
+ i++;
570
+ }
571
+ }
572
+ // Unbind removed nodes. When node does not have a parent it means that it was removed from DOM tree during
573
+ // comparison with the expected DOM. We don't need to check child nodes, because if child node was reinserted,
574
+ // it was moved to DOM tree out of the removed node.
575
+ for (const node of nodesToUnbind) {
576
+ if (!node.parentNode) {
577
+ this.domConverter.unbindDomElement(node);
578
+ }
579
+ }
580
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
581
+ // @if CK_DEBUG_TYPING // console.groupEnd();
582
+ // @if CK_DEBUG_TYPING // }
583
+ }
584
+ /**
585
+ * Shorthand for diffing two arrays or node lists of DOM nodes.
586
+ *
587
+ * @param actualDomChildren Actual DOM children
588
+ * @param expectedDomChildren Expected DOM children.
589
+ * @returns The list of actions based on the {@link module:utils/diff~diff} function.
590
+ */
591
+ _diffNodeLists(actualDomChildren, expectedDomChildren) {
592
+ actualDomChildren = filterOutFakeSelectionContainer(actualDomChildren, this._fakeSelectionContainer);
593
+ return diff(actualDomChildren, expectedDomChildren, sameNodes.bind(null, this.domConverter));
594
+ }
595
+ /**
596
+ * Finds DOM nodes that were replaced with the similar nodes (same tag name) in the view. All nodes are compared
597
+ * within one `insert`/`delete` action group, for example:
598
+ *
599
+ * ```
600
+ * Actual DOM: <p><b>Foo</b>Bar<i>Baz</i><b>Bax</b></p>
601
+ * Expected DOM: <p>Bar<b>123</b><i>Baz</i><b>456</b></p>
602
+ * Input actions: [ insert, insert, delete, delete, equal, insert, delete ]
603
+ * Output actions: [ insert, replace, delete, equal, replace ]
604
+ * ```
605
+ *
606
+ * @param actions Actions array which is a result of the {@link module:utils/diff~diff} function.
607
+ * @param actualDom Actual DOM children
608
+ * @param expectedDom Expected DOM children.
609
+ * @param comparator A comparator function that should return `true` if the given node should be reused
610
+ * (either by the update of a text node data or an element children list for similar elements).
611
+ * @returns Actions array modified with the `update` actions.
612
+ */
613
+ _findUpdateActions(actions, actualDom, expectedDom, comparator) {
614
+ // If there is no both 'insert' and 'delete' actions, no need to check for replaced elements.
615
+ if (actions.indexOf('insert') === -1 || actions.indexOf('delete') === -1) {
616
+ return actions;
617
+ }
618
+ let newActions = [];
619
+ let actualSlice = [];
620
+ let expectedSlice = [];
621
+ const counter = { equal: 0, insert: 0, delete: 0 };
622
+ for (const action of actions) {
623
+ if (action === 'insert') {
624
+ expectedSlice.push(expectedDom[counter.equal + counter.insert]);
625
+ }
626
+ else if (action === 'delete') {
627
+ actualSlice.push(actualDom[counter.equal + counter.delete]);
628
+ }
629
+ else { // equal
630
+ newActions = newActions.concat(diff(actualSlice, expectedSlice, comparator)
631
+ .map(action => action === 'equal' ? 'update' : action));
632
+ newActions.push('equal');
633
+ // Reset stored elements on 'equal'.
634
+ actualSlice = [];
635
+ expectedSlice = [];
636
+ }
637
+ counter[action]++;
638
+ }
639
+ return newActions.concat(diff(actualSlice, expectedSlice, comparator)
640
+ .map(action => action === 'equal' ? 'update' : action));
641
+ }
642
+ /**
643
+ * Marks text nodes to be synchronized.
644
+ *
645
+ * If a text node is passed, it will be marked. If an element is passed, all descendant text nodes inside it will be marked.
646
+ *
647
+ * @param viewNode View node to sync.
648
+ */
649
+ _markDescendantTextToSync(viewNode) {
650
+ if (!viewNode) {
651
+ return;
652
+ }
653
+ if (viewNode.is('$text')) {
654
+ this.markedTexts.add(viewNode);
655
+ }
656
+ else if (viewNode.is('element')) {
657
+ for (const child of viewNode.getChildren()) {
658
+ this._markDescendantTextToSync(child);
659
+ }
660
+ }
661
+ }
662
+ /**
663
+ * Checks if the selection needs to be updated and possibly updates it.
664
+ */
665
+ _updateSelection() {
666
+ // Block updating DOM selection in (non-Android) Blink while the user is selecting to prevent accidental selection collapsing.
667
+ // Note: Structural changes in DOM must trigger selection rendering, though. Nodes the selection was anchored
668
+ // to, may disappear in DOM which would break the selection (e.g. in real-time collaboration scenarios).
669
+ // https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723
670
+ if (env.isBlink && !env.isAndroid && this.isSelecting && !this.markedChildren.size) {
671
+ return;
672
+ }
673
+ // If there is no selection - remove DOM and fake selections.
674
+ if (this.selection.rangeCount === 0) {
675
+ this._removeDomSelection();
676
+ this._removeFakeSelection();
677
+ return;
678
+ }
679
+ const domRoot = this.domConverter.mapViewToDom(this.selection.editableElement);
680
+ // Do nothing if there is no focus, or there is no DOM element corresponding to selection's editable element.
681
+ if (!this.isFocused || !domRoot) {
682
+ return;
683
+ }
684
+ // Render fake selection - create the fake selection container (if needed) and move DOM selection to it.
685
+ if (this.selection.isFake) {
686
+ this._updateFakeSelection(domRoot);
687
+ }
688
+ // There was a fake selection so remove it and update the DOM selection.
689
+ // This is especially important on Android because otherwise IME will try to compose over the fake selection container.
690
+ else if (this._fakeSelectionContainer && this._fakeSelectionContainer.isConnected) {
691
+ this._removeFakeSelection();
692
+ this._updateDomSelection(domRoot);
693
+ }
694
+ // Update the DOM selection in case of a plain selection change (no fake selection is involved).
695
+ // On non-Android the whole rendering is disabled in composition mode (including DOM selection update),
696
+ // but updating DOM selection should be also disabled on Android if in the middle of the composition
697
+ // (to not interrupt it).
698
+ else if (!(this.isComposing && env.isAndroid)) {
699
+ this._updateDomSelection(domRoot);
700
+ }
701
+ }
702
+ /**
703
+ * Updates the fake selection.
704
+ *
705
+ * @param domRoot A valid DOM root where the fake selection container should be added.
706
+ */
707
+ _updateFakeSelection(domRoot) {
708
+ const domDocument = domRoot.ownerDocument;
709
+ if (!this._fakeSelectionContainer) {
710
+ this._fakeSelectionContainer = createFakeSelectionContainer(domDocument);
711
+ }
712
+ const container = this._fakeSelectionContainer;
713
+ // Bind fake selection container with the current selection *position*.
714
+ this.domConverter.bindFakeSelection(container, this.selection);
715
+ if (!this._fakeSelectionNeedsUpdate(domRoot)) {
716
+ return;
717
+ }
718
+ if (!container.parentElement || container.parentElement != domRoot) {
719
+ domRoot.appendChild(container);
720
+ }
721
+ container.textContent = this.selection.fakeSelectionLabel || '\u00A0';
722
+ const domSelection = domDocument.getSelection();
723
+ const domRange = domDocument.createRange();
724
+ domSelection.removeAllRanges();
725
+ domRange.selectNodeContents(container);
726
+ domSelection.addRange(domRange);
727
+ }
728
+ /**
729
+ * Updates the DOM selection.
730
+ *
731
+ * @param domRoot A valid DOM root where the DOM selection should be rendered.
732
+ */
733
+ _updateDomSelection(domRoot) {
734
+ const domSelection = domRoot.ownerDocument.defaultView.getSelection();
735
+ // Let's check whether DOM selection needs updating at all.
736
+ if (!this._domSelectionNeedsUpdate(domSelection)) {
737
+ return;
738
+ }
739
+ // Multi-range selection is not available in most browsers, and, at least in Chrome, trying to
740
+ // set such selection, that is not continuous, throws an error. Because of that, we will just use anchor
741
+ // and focus of view selection.
742
+ // Since we are not supporting multi-range selection, we also do not need to check if proper editable is
743
+ // selected. If there is any editable selected, it is okay (editable is taken from selection anchor).
744
+ const anchor = this.domConverter.viewPositionToDom(this.selection.anchor);
745
+ const focus = this.domConverter.viewPositionToDom(this.selection.focus);
746
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
747
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update DOM selection:',
748
+ // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', anchor, focus
749
+ // @if CK_DEBUG_TYPING // );
750
+ // @if CK_DEBUG_TYPING // }
751
+ domSelection.setBaseAndExtent(anchor.parent, anchor.offset, focus.parent, focus.offset);
752
+ // Firefox–specific hack (https://github.com/ckeditor/ckeditor5-engine/issues/1439).
753
+ if (env.isGecko) {
754
+ fixGeckoSelectionAfterBr(focus, domSelection);
755
+ }
756
+ }
757
+ /**
758
+ * Checks whether a given DOM selection needs to be updated.
759
+ *
760
+ * @param domSelection The DOM selection to check.
761
+ */
762
+ _domSelectionNeedsUpdate(domSelection) {
763
+ if (!this.domConverter.isDomSelectionCorrect(domSelection)) {
764
+ // Current DOM selection is in incorrect position. We need to update it.
765
+ return true;
766
+ }
767
+ const oldViewSelection = domSelection && this.domConverter.domSelectionToView(domSelection);
768
+ if (oldViewSelection && this.selection.isEqual(oldViewSelection)) {
769
+ return false;
770
+ }
771
+ // If selection is not collapsed, it does not need to be updated if it is similar.
772
+ if (!this.selection.isCollapsed && this.selection.isSimilar(oldViewSelection)) {
773
+ // Selection did not changed and is correct, do not update.
774
+ return false;
775
+ }
776
+ // Selections are not similar.
777
+ return true;
778
+ }
779
+ /**
780
+ * Checks whether the fake selection needs to be updated.
781
+ *
782
+ * @param domRoot A valid DOM root where a new fake selection container should be added.
783
+ */
784
+ _fakeSelectionNeedsUpdate(domRoot) {
785
+ const container = this._fakeSelectionContainer;
786
+ const domSelection = domRoot.ownerDocument.getSelection();
787
+ // Fake selection needs to be updated if there's no fake selection container, or the container currently sits
788
+ // in a different root.
789
+ if (!container || container.parentElement !== domRoot) {
790
+ return true;
791
+ }
792
+ // Make sure that the selection actually is within the fake selection.
793
+ if (domSelection.anchorNode !== container && !container.contains(domSelection.anchorNode)) {
794
+ return true;
795
+ }
796
+ return container.textContent !== this.selection.fakeSelectionLabel;
797
+ }
798
+ /**
799
+ * Removes the DOM selection.
800
+ */
801
+ _removeDomSelection() {
802
+ for (const doc of this.domDocuments) {
803
+ const domSelection = doc.getSelection();
804
+ if (domSelection.rangeCount) {
805
+ const activeDomElement = doc.activeElement;
806
+ const viewElement = this.domConverter.mapDomToView(activeDomElement);
807
+ if (activeDomElement && viewElement) {
808
+ domSelection.removeAllRanges();
809
+ }
810
+ }
811
+ }
812
+ }
813
+ /**
814
+ * Removes the fake selection.
815
+ */
816
+ _removeFakeSelection() {
817
+ const container = this._fakeSelectionContainer;
818
+ if (container) {
819
+ container.remove();
820
+ }
821
+ }
822
+ /**
823
+ * Checks if focus needs to be updated and possibly updates it.
824
+ */
825
+ _updateFocus() {
826
+ if (this.isFocused) {
827
+ const editable = this.selection.editableElement;
828
+ if (editable) {
829
+ this.domConverter.focus(editable);
830
+ }
831
+ }
832
+ }
833
+ }
834
+ /**
835
+ * Checks if provided element is editable.
836
+ */
837
+ function isEditable(element) {
838
+ if (element.getAttribute('contenteditable') == 'false') {
839
+ return false;
840
+ }
841
+ const parent = element.findAncestor(element => element.hasAttribute('contenteditable'));
842
+ return !parent || parent.getAttribute('contenteditable') == 'true';
843
+ }
844
+ /**
845
+ * Adds inline filler at a given position.
846
+ *
847
+ * The position can be given as an array of DOM nodes and an offset in that array,
848
+ * or a DOM parent element and an offset in that element.
849
+ *
850
+ * @returns The DOM text node that contains an inline filler.
851
+ */
852
+ function addInlineFiller(domDocument, domParentOrArray, offset) {
853
+ const childNodes = domParentOrArray instanceof Array ? domParentOrArray : domParentOrArray.childNodes;
854
+ const nodeAfterFiller = childNodes[offset];
855
+ if (isText(nodeAfterFiller)) {
856
+ nodeAfterFiller.data = INLINE_FILLER + nodeAfterFiller.data;
857
+ return nodeAfterFiller;
858
+ }
859
+ else {
860
+ const fillerNode = domDocument.createTextNode(INLINE_FILLER);
861
+ if (Array.isArray(domParentOrArray)) {
862
+ childNodes.splice(offset, 0, fillerNode);
863
+ }
864
+ else {
865
+ insertAt(domParentOrArray, offset, fillerNode);
866
+ }
867
+ return fillerNode;
868
+ }
869
+ }
870
+ /**
871
+ * Whether two DOM nodes should be considered as similar.
872
+ * Nodes are considered similar if they have the same tag name.
873
+ */
874
+ function areSimilarElements(node1, node2) {
875
+ return isNode(node1) && isNode(node2) &&
876
+ !isText(node1) && !isText(node2) &&
877
+ !isComment(node1) && !isComment(node2) &&
878
+ node1.tagName.toLowerCase() === node2.tagName.toLowerCase();
879
+ }
880
+ /**
881
+ * Whether two DOM nodes are text nodes.
882
+ */
883
+ function areTextNodes(node1, node2) {
884
+ return isNode(node1) && isNode(node2) &&
885
+ isText(node1) && isText(node2);
886
+ }
887
+ /**
888
+ * Whether two dom nodes should be considered as the same.
889
+ * Two nodes which are considered the same are:
890
+ *
891
+ * * Text nodes with the same text.
892
+ * * Element nodes represented by the same object.
893
+ * * Two block filler elements.
894
+ *
895
+ * @param blockFillerMode Block filler mode, see {@link module:engine/view/domconverter~DomConverter#blockFillerMode}.
896
+ */
897
+ function sameNodes(domConverter, actualDomChild, expectedDomChild) {
898
+ // Elements.
899
+ if (actualDomChild === expectedDomChild) {
900
+ return true;
901
+ }
902
+ // Texts.
903
+ else if (isText(actualDomChild) && isText(expectedDomChild)) {
904
+ return actualDomChild.data === expectedDomChild.data;
905
+ }
906
+ // Block fillers.
907
+ else if (domConverter.isBlockFiller(actualDomChild) &&
908
+ domConverter.isBlockFiller(expectedDomChild)) {
909
+ return true;
910
+ }
911
+ // Not matching types.
912
+ return false;
913
+ }
914
+ /**
915
+ * The following is a Firefox–specific hack (https://github.com/ckeditor/ckeditor5-engine/issues/1439).
916
+ * When the native DOM selection is at the end of the block and preceded by <br /> e.g.
917
+ *
918
+ * ```html
919
+ * <p>foo<br/>[]</p>
920
+ * ```
921
+ *
922
+ * which happens a lot when using the soft line break, the browser fails to (visually) move the
923
+ * caret to the new line. A quick fix is as simple as force–refreshing the selection with the same range.
924
+ */
925
+ function fixGeckoSelectionAfterBr(focus, domSelection) {
926
+ const parent = focus.parent;
927
+ // This fix works only when the focus point is at the very end of an element.
928
+ // There is no point in running it in cases unrelated to the browser bug.
929
+ if (parent.nodeType != Node.ELEMENT_NODE || focus.offset != parent.childNodes.length - 1) {
930
+ return;
931
+ }
932
+ const childAtOffset = parent.childNodes[focus.offset];
933
+ // To stay on the safe side, the fix being as specific as possible, it targets only the
934
+ // selection which is at the very end of the element and preceded by <br />.
935
+ if (childAtOffset && childAtOffset.tagName == 'BR') {
936
+ domSelection.addRange(domSelection.getRangeAt(0));
937
+ }
938
+ }
939
+ function filterOutFakeSelectionContainer(domChildList, fakeSelectionContainer) {
940
+ const childList = Array.from(domChildList);
941
+ if (childList.length == 0 || !fakeSelectionContainer) {
942
+ return childList;
943
+ }
944
+ const last = childList[childList.length - 1];
945
+ if (last == fakeSelectionContainer) {
946
+ childList.pop();
947
+ }
948
+ return childList;
949
+ }
950
+ /**
951
+ * Creates a fake selection container for a given document.
952
+ */
953
+ function createFakeSelectionContainer(domDocument) {
954
+ const container = domDocument.createElement('div');
955
+ container.className = 'ck-fake-selection-container';
956
+ Object.assign(container.style, {
957
+ position: 'fixed',
958
+ top: 0,
959
+ left: '-9999px',
960
+ // See https://github.com/ckeditor/ckeditor5/issues/752.
961
+ width: '42px'
962
+ });
963
+ // Fill it with a text node so we can update it later.
964
+ container.textContent = '\u00A0';
965
+ return container;
966
+ }
967
+ /**
968
+ * Checks if text needs to be updated and possibly updates it by removing and inserting only parts
969
+ * of the data from the existing text node to reduce impact on the IME composition.
970
+ *
971
+ * @param domText DOM text node to update.
972
+ * @param expectedText The expected data of a text node.
973
+ */
974
+ function updateTextNode(domText, expectedText) {
975
+ const actualText = domText.data;
976
+ if (actualText == expectedText) {
977
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
978
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Text node does not need update:',
979
+ // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '',
980
+ // @if CK_DEBUG_TYPING // `"${ domText.data }" (${ domText.data.length })`
981
+ // @if CK_DEBUG_TYPING // );
982
+ // @if CK_DEBUG_TYPING // }
983
+ return;
984
+ }
985
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
986
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update text node:',
987
+ // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '',
988
+ // @if CK_DEBUG_TYPING // `"${ domText.data }" (${ domText.data.length }) -> "${ expectedText }" (${ expectedText.length })`
989
+ // @if CK_DEBUG_TYPING // );
990
+ // @if CK_DEBUG_TYPING // }
991
+ const actions = fastDiff(actualText, expectedText);
992
+ for (const action of actions) {
993
+ if (action.type === 'insert') {
994
+ domText.insertData(action.index, action.values.join(''));
995
+ }
996
+ else { // 'delete'
997
+ domText.deleteData(action.index, action.howMany);
998
+ }
999
+ }
1000
+ }