@ckeditor/ckeditor5-engine 30.0.0

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