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