@ckeditor/ckeditor5-engine 35.0.1 → 35.1.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.
- package/CHANGELOG.md +4 -4
- package/package.json +30 -24
- package/src/controller/datacontroller.js +467 -561
- package/src/controller/editingcontroller.js +168 -204
- package/src/conversion/conversion.js +541 -565
- package/src/conversion/conversionhelpers.js +24 -28
- package/src/conversion/downcastdispatcher.js +457 -686
- package/src/conversion/downcasthelpers.js +1583 -1965
- package/src/conversion/mapper.js +518 -707
- package/src/conversion/modelconsumable.js +240 -283
- package/src/conversion/upcastdispatcher.js +372 -718
- package/src/conversion/upcasthelpers.js +707 -818
- package/src/conversion/viewconsumable.js +524 -581
- package/src/dataprocessor/basichtmlwriter.js +12 -16
- package/src/dataprocessor/dataprocessor.js +5 -0
- package/src/dataprocessor/htmldataprocessor.js +100 -116
- package/src/dataprocessor/htmlwriter.js +1 -18
- package/src/dataprocessor/xmldataprocessor.js +116 -137
- package/src/dev-utils/model.js +260 -352
- package/src/dev-utils/operationreplayer.js +106 -126
- package/src/dev-utils/utils.js +34 -51
- package/src/dev-utils/view.js +632 -753
- package/src/index.js +0 -11
- package/src/model/batch.js +111 -127
- package/src/model/differ.js +988 -1233
- package/src/model/document.js +340 -449
- package/src/model/documentfragment.js +327 -364
- package/src/model/documentselection.js +996 -1189
- package/src/model/element.js +306 -410
- package/src/model/history.js +224 -262
- package/src/model/item.js +5 -0
- package/src/model/liveposition.js +84 -145
- package/src/model/liverange.js +108 -185
- package/src/model/markercollection.js +379 -480
- package/src/model/model.js +883 -1034
- package/src/model/node.js +419 -463
- package/src/model/nodelist.js +175 -201
- package/src/model/operation/attributeoperation.js +153 -182
- package/src/model/operation/detachoperation.js +64 -83
- package/src/model/operation/insertoperation.js +135 -166
- package/src/model/operation/markeroperation.js +114 -140
- package/src/model/operation/mergeoperation.js +163 -191
- package/src/model/operation/moveoperation.js +157 -187
- package/src/model/operation/nooperation.js +28 -38
- package/src/model/operation/operation.js +106 -125
- package/src/model/operation/operationfactory.js +30 -34
- package/src/model/operation/renameoperation.js +109 -135
- package/src/model/operation/rootattributeoperation.js +155 -188
- package/src/model/operation/splitoperation.js +196 -232
- package/src/model/operation/transform.js +1833 -2204
- package/src/model/operation/utils.js +140 -204
- package/src/model/position.js +899 -1053
- package/src/model/range.js +910 -1028
- package/src/model/rootelement.js +77 -97
- package/src/model/schema.js +1189 -1835
- package/src/model/selection.js +745 -862
- package/src/model/text.js +90 -114
- package/src/model/textproxy.js +204 -240
- package/src/model/treewalker.js +316 -397
- package/src/model/typecheckable.js +16 -0
- package/src/model/utils/autoparagraphing.js +32 -44
- package/src/model/utils/deletecontent.js +334 -418
- package/src/model/utils/findoptimalinsertionrange.js +25 -36
- package/src/model/utils/getselectedcontent.js +96 -118
- package/src/model/utils/insertcontent.js +654 -773
- package/src/model/utils/insertobject.js +96 -119
- package/src/model/utils/modifyselection.js +120 -158
- package/src/model/utils/selection-post-fixer.js +153 -201
- package/src/model/writer.js +1305 -1474
- package/src/view/attributeelement.js +189 -225
- package/src/view/containerelement.js +75 -85
- package/src/view/document.js +172 -215
- package/src/view/documentfragment.js +200 -249
- package/src/view/documentselection.js +338 -367
- package/src/view/domconverter.js +1370 -1617
- package/src/view/downcastwriter.js +1747 -2076
- package/src/view/editableelement.js +81 -97
- package/src/view/element.js +739 -890
- package/src/view/elementdefinition.js +5 -0
- package/src/view/emptyelement.js +82 -92
- package/src/view/filler.js +35 -50
- package/src/view/item.js +5 -0
- package/src/view/matcher.js +260 -559
- package/src/view/node.js +274 -360
- package/src/view/observer/arrowkeysobserver.js +19 -28
- package/src/view/observer/bubblingemittermixin.js +120 -263
- package/src/view/observer/bubblingeventinfo.js +47 -55
- package/src/view/observer/clickobserver.js +7 -13
- package/src/view/observer/compositionobserver.js +14 -24
- package/src/view/observer/domeventdata.js +57 -67
- package/src/view/observer/domeventobserver.js +40 -64
- package/src/view/observer/fakeselectionobserver.js +81 -96
- package/src/view/observer/focusobserver.js +45 -61
- package/src/view/observer/inputobserver.js +7 -13
- package/src/view/observer/keyobserver.js +17 -27
- package/src/view/observer/mouseobserver.js +7 -14
- package/src/view/observer/mutationobserver.js +220 -315
- package/src/view/observer/observer.js +81 -102
- package/src/view/observer/selectionobserver.js +191 -246
- package/src/view/observer/tabobserver.js +23 -36
- package/src/view/placeholder.js +128 -173
- package/src/view/position.js +350 -401
- package/src/view/range.js +453 -513
- package/src/view/rawelement.js +85 -112
- package/src/view/renderer.js +874 -1018
- package/src/view/rooteditableelement.js +80 -90
- package/src/view/selection.js +608 -689
- package/src/view/styles/background.js +43 -44
- package/src/view/styles/border.js +220 -276
- package/src/view/styles/margin.js +8 -17
- package/src/view/styles/padding.js +8 -16
- package/src/view/styles/utils.js +127 -160
- package/src/view/stylesmap.js +728 -905
- package/src/view/text.js +102 -126
- package/src/view/textproxy.js +144 -170
- package/src/view/treewalker.js +383 -479
- package/src/view/typecheckable.js +19 -0
- package/src/view/uielement.js +166 -187
- package/src/view/upcastwriter.js +395 -449
- package/src/view/view.js +569 -664
- package/src/dataprocessor/dataprocessor.jsdoc +0 -64
- package/src/model/item.jsdoc +0 -14
- package/src/view/elementdefinition.jsdoc +0 -59
- package/src/view/item.jsdoc +0 -14
package/src/view/domconverter.js
CHANGED
|
@@ -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
|
|
34
|
-
const
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
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 ` ` 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 ` ` 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 ` ` (e.g. `'x x'` becomes `'x 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> bar</span> <-- bad.
|
|
1094
|
+
// Foo <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 ` `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 chars, that were in DOM text data because of rendering reasons, to spaces.
|
|
1170
|
+
// First, change all ` \u00A0` pairs (space + ) 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 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 created for rendering reasons should be
|
|
1185
|
+
// changed to normal space. All left are 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(
|
|
1619
|
-
|
|
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
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
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 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(
|
|
1644
|
-
|
|
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(
|
|
1655
|
-
|
|
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(
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
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` – For the `<br data-cke-filler="true">` block filler used in the editing view.
|
|
1680
|
-
* * `nbsp` – For the ` ` block fillers used in the data.
|
|
1681
|
-
* * `markedNbsp` – For the ` ` block fillers wrapped in `<span>` elements: `<span data-cke-filler="true"> </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
|