@ckeditor/ckeditor5-engine 33.0.0 → 34.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,173 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+
6
+ /**
7
+ * @module engine/model/utils/insertobject
8
+ */
9
+
10
+ import first from '@ckeditor/ckeditor5-utils/src/first';
11
+ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
12
+
13
+ import { findOptimalInsertionRange } from './findoptimalinsertionrange';
14
+
15
+ /**
16
+ * Inserts an {@glink framework/guides/deep-dive/schema#object-elements object element} at a specific position in the editor content.
17
+ *
18
+ * **Note:** Use {@link module:engine/model/model~Model#insertObject} instead of this function.
19
+ * This function is only exposed to be reusable in algorithms which change the {@link module:engine/model/model~Model#insertObject}
20
+ * method's behavior.
21
+ *
22
+ * **Note**: For more documentation and examples, see {@link module:engine/model/model~Model#insertObject}.
23
+ *
24
+ * @param {module:engine/model/model~Model} model The model in context of which the insertion
25
+ * should be performed.
26
+ * @param {module:engine/model/element~Element} object An object to be inserted into the model document.
27
+ * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection]
28
+ * A selectable where the content should be inserted. If not specified, the current
29
+ * {@link module:engine/model/document~Document#selection document selection} will be used instead.
30
+ * @param {Number|'before'|'end'|'after'|'on'|'in'} placeOrOffset Specifies the exact place or offset for the insertion to take place,
31
+ * relative to `selectable`.
32
+ * @param {Object} [options] Additional options.
33
+ * @param {'auto'|'before'|'after'} [options.findOptimalPosition] An option that, when set, adjusts the insertion position (relative to
34
+ * `selectable` and `placeOrOffset`) so that the content of `selectable` is not split upon insertion (a.k.a. non-destructive insertion).
35
+ * * When `'auto'`, the algorithm will decide whether to insert the object before or after `selectable` to avoid content splitting.
36
+ * * When `'before'`, the closest position before `selectable` will be used that will not result in content splitting.
37
+ * * When `'after'`, the closest position after `selectable` will be used that will not result in content splitting.
38
+ *
39
+ * Note that this option works only for block objects. Inline objects are inserted into text and do not split blocks.
40
+ * @param {'on'|'after'} [options.setSelection] An option that, when set, moves the
41
+ * {@link module:engine/model/document~Document#selection document selection} after inserting the object.
42
+ * * When `'on'`, the document selection will be set on the inserted object.
43
+ * * When `'after'`, the document selection will move to the closest text node after the inserted object. If there is no
44
+ * such text node, a paragraph will be created and the document selection will be moved inside it.
45
+ * @returns {module:engine/model/range~Range} A range which contains all the performed changes. This is a range that, if removed,
46
+ * would return the model to the state before the insertion. If no changes were preformed by `insertObject()`, returns a range collapsed
47
+ * at the insertion position.
48
+ */
49
+ export default function insertObject( model, object, selectable, placeOrOffset, options = {} ) {
50
+ if ( !model.schema.isObject( object ) ) {
51
+ /**
52
+ * Tried to insert an element with {@link module:engine/model/utils/insertobject insertObject()} function
53
+ * that is not defined as an object in schema.
54
+ * See {@link module:engine/model/schema~SchemaItemDefinition#isObject `SchemaItemDefinition`}.
55
+ * If you want to insert content that is not an object you might want to use
56
+ * {@link module:engine/model/utils/insertcontent insertContent()} function.
57
+ * @error insertobject-element-not-an-object
58
+ */
59
+ throw new CKEditorError( 'insertobject-element-not-an-object', model, { object } );
60
+ }
61
+
62
+ // Normalize selectable to a selection instance.
63
+ let originalSelection;
64
+
65
+ if ( !selectable ) {
66
+ originalSelection = model.document.selection;
67
+ } else if ( selectable.is( 'selection' ) ) {
68
+ originalSelection = selectable;
69
+ } else {
70
+ originalSelection = model.createSelection( selectable, placeOrOffset );
71
+ }
72
+
73
+ // Adjust the insertion selection.
74
+ let insertionSelection = originalSelection;
75
+
76
+ if ( options.findOptimalPosition && model.schema.isBlock( object ) ) {
77
+ insertionSelection = model.createSelection( findOptimalInsertionRange( originalSelection, model, options.findOptimalPosition ) );
78
+ }
79
+
80
+ // Collect attributes to be copied on the inserted object.
81
+ const firstSelectedBlock = first( originalSelection.getSelectedBlocks() );
82
+ const attributesToCopy = {};
83
+
84
+ if ( firstSelectedBlock ) {
85
+ Object.assign( attributesToCopy, model.schema.getAttributesWithProperty( firstSelectedBlock, 'copyOnReplace', true ) );
86
+ }
87
+
88
+ return model.change( writer => {
89
+ // Remove the selected content to find out what the parent of the inserted object would be.
90
+ // It would be removed inside model.insertContent() anyway.
91
+ if ( !insertionSelection.isCollapsed ) {
92
+ model.deleteContent( insertionSelection, { doNotAutoparagraph: true } );
93
+ }
94
+
95
+ let elementToInsert = object;
96
+ const insertionPositionParent = insertionSelection.anchor.parent;
97
+
98
+ // Autoparagraphing of an inline objects.
99
+ if (
100
+ !model.schema.checkChild( insertionPositionParent, object ) &&
101
+ model.schema.checkChild( insertionPositionParent, 'paragraph' ) &&
102
+ model.schema.checkChild( 'paragraph', object )
103
+ ) {
104
+ elementToInsert = writer.createElement( 'paragraph' );
105
+
106
+ writer.insert( object, elementToInsert );
107
+ }
108
+
109
+ // Apply attributes that are allowed on the inserted object (or paragraph if autoparagraphed).
110
+ model.schema.setAllowedAttributes( elementToInsert, attributesToCopy, writer );
111
+
112
+ // Insert the prepared content at the optionally adjusted selection.
113
+ const affectedRange = model.insertContent( elementToInsert, insertionSelection );
114
+
115
+ // Nothing got inserted.
116
+ if ( affectedRange.isCollapsed ) {
117
+ return affectedRange;
118
+ }
119
+
120
+ if ( options.setSelection ) {
121
+ updateSelection( writer, object, options.setSelection, attributesToCopy );
122
+ }
123
+
124
+ return affectedRange;
125
+ } );
126
+ }
127
+
128
+ // Updates document selection based on given `place` parameter in relation to `contextElement` element.
129
+ //
130
+ // @private
131
+ // @param {module:engine/model/writer~Writer} writer An instance of the model writer.
132
+ // @param {module:engine/model/element~Element} contextElement An element to set the attributes on.
133
+ // @param {'on'|'after'} place The place where selection should be set in relation to the `contextElement` element.
134
+ // Value `on` will set selection on the passed `contextElement`. Value `after` will set selection after `contextElement`.
135
+ // @param {Object} attributes Attributes keys and values to set on a paragraph that this function can create when
136
+ // `place` parameter is equal to `after` but there is no element with `$text` node to set selection in.
137
+ function updateSelection( writer, contextElement, place, paragraphAttributes ) {
138
+ const model = writer.model;
139
+
140
+ if ( place == 'after' ) {
141
+ let nextElement = contextElement.nextSibling;
142
+
143
+ // Check whether an element next to the inserted element is defined and can contain a text.
144
+ const canSetSelection = nextElement && model.schema.checkChild( nextElement, '$text' );
145
+
146
+ // If the element is missing, but a paragraph could be inserted next to the element, let's add it.
147
+ if ( !canSetSelection && model.schema.checkChild( contextElement.parent, 'paragraph' ) ) {
148
+ nextElement = writer.createElement( 'paragraph' );
149
+
150
+ model.schema.setAllowedAttributes( nextElement, paragraphAttributes, writer );
151
+ model.insertContent( nextElement, writer.createPositionAfter( contextElement ) );
152
+ }
153
+
154
+ // Put the selection inside the element, at the beginning.
155
+ if ( nextElement ) {
156
+ writer.setSelection( nextElement, 0 );
157
+ }
158
+ }
159
+ else if ( place == 'on' ) {
160
+ writer.setSelection( contextElement, 'on' );
161
+ }
162
+ else {
163
+ /**
164
+ * The unsupported `options.setSelection` parameter was passed
165
+ * to the {@link module:engine/model/utils/insertobject insertObject()} function.
166
+ * Check the {@link module:engine/model/utils/insertobject insertObject()} API documentation for allowed
167
+ * `options.setSelection` parameter values.
168
+ *
169
+ * @error insertobject-invalid-place-parameter-value
170
+ */
171
+ throw new CKEditorError( 'insertobject-invalid-place-parameter-value', model );
172
+ }
173
+ }
@@ -24,16 +24,6 @@ const DEFAULT_PRIORITY = 10;
24
24
  * To create a new attribute element instance use the
25
25
  * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement `DowncastWriter#createAttributeElement()`} method.
26
26
  *
27
- * **Note:** Attribute elements by default can wrap {@link module:engine/view/text~Text},
28
- * {@link module:engine/view/emptyelement~EmptyElement}, {@link module:engine/view/uielement~UIElement},
29
- * {@link module:engine/view/rawelement~RawElement} and other attribute elements with higher priority. Other elements while placed inside
30
- * an attribute element will split it (or nest in case of an `AttributeElement`). This behavior can be modified by changing
31
- * the `isAllowedInsideAttributeElement` option while creating
32
- * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement},
33
- * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement},
34
- * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement} or
35
- * {@link module:engine/view/downcastwriter~DowncastWriter#createRawElement}.
36
- *
37
27
  * @extends module:engine/view/element~Element
38
28
  */
39
29
  export default class AttributeElement extends Element {
@@ -35,7 +35,6 @@ const NBSP_FILLER_REF = NBSP_FILLER( document ); // eslint-disable-line new-cap
35
35
  const MARKED_NBSP_FILLER_REF = MARKED_NBSP_FILLER( document ); // eslint-disable-line new-cap
36
36
  const UNSAFE_ATTRIBUTE_NAME_PREFIX = 'data-ck-unsafe-attribute-';
37
37
  const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
38
- const UNSAFE_ELEMENTS = [ 'script', 'style' ];
39
38
 
40
39
  /**
41
40
  * `DomConverter` is a set of tools to do transformations between DOM nodes and view nodes. It also handles
@@ -127,6 +126,15 @@ export default class DomConverter {
127
126
  'object', 'iframe', 'input', 'button', 'textarea', 'select', 'option', 'video', 'embed', 'audio', 'img', 'canvas'
128
127
  ];
129
128
 
129
+ /**
130
+ * A list of elements which may affect the editing experience. To avoid this, those elements are replaced with
131
+ * `<span data-ck-unsafe-element="[element name]"></span>` while rendering in the editing mode.
132
+ *
133
+ * @readonly
134
+ * @member {Array.<String>} module:engine/view/domconverter~DomConverter#unsafeElements
135
+ */
136
+ this.unsafeElements = [ 'script', 'style' ];
137
+
130
138
  /**
131
139
  * The DOM-to-view mapping.
132
140
  *
@@ -493,7 +501,22 @@ export default class DomConverter {
493
501
  yield this._getBlockFiller( domDocument );
494
502
  }
495
503
 
496
- yield this.viewToDom( childView, domDocument, options );
504
+ const transparentRendering = childView.is( 'element' ) && childView.getCustomProperty( 'dataPipeline:transparentRendering' );
505
+
506
+ if ( transparentRendering && this.renderingMode == 'data' ) {
507
+ yield* this.viewChildrenToDom( childView, domDocument, options );
508
+ } else {
509
+ if ( transparentRendering ) {
510
+ /**
511
+ * The `dataPipeline:transparentRendering` flag is supported only in the data pipeline.
512
+ *
513
+ * @error domconverter-transparent-rendering-unsupported-in-editing-pipeline
514
+ */
515
+ logWarning( 'domconverter-transparent-rendering-unsupported-in-editing-pipeline', { viewElement: childView } );
516
+ }
517
+
518
+ yield this.viewToDom( childView, domDocument, options );
519
+ }
497
520
 
498
521
  offset++;
499
522
  }
@@ -657,7 +680,7 @@ export default class DomConverter {
657
680
  const attrs = domNode.attributes;
658
681
 
659
682
  if ( attrs ) {
660
- for ( let i = attrs.length - 1; i >= 0; i-- ) {
683
+ for ( let l = attrs.length, i = 0; i < l; i++ ) {
661
684
  viewElement._setAttribute( attrs[ i ].name, attrs[ i ].value );
662
685
  }
663
686
  }
@@ -1549,7 +1572,7 @@ export default class DomConverter {
1549
1572
  _shouldRenameElement( elementName ) {
1550
1573
  const name = elementName.toLowerCase();
1551
1574
 
1552
- return this.renderingMode === 'editing' && UNSAFE_ELEMENTS.includes( name );
1575
+ return this.renderingMode === 'editing' && this.unsafeElements.includes( name );
1553
1576
  }
1554
1577
 
1555
1578
  /**
@@ -185,10 +185,6 @@ export default class DowncastWriter {
185
185
  * // Set `id` of a marker element so it is not joined or merged with "normal" elements.
186
186
  * writer.createAttributeElement( 'span', { class: 'my-marker' }, { id: 'marker:my' } );
187
187
  *
188
- * **Note:** By default an `AttributeElement` is split by a
189
- * {@link module:engine/view/containerelement~ContainerElement `ContainerElement`} but this behavior can be modified
190
- * with `isAllowedInsideAttributeElement` option set while {@link #createContainerElement creating the element}.
191
- *
192
188
  * @param {String} name Name of the element.
193
189
  * @param {Object} [attributes] Element's attributes.
194
190
  * @param {Object} [options] Element's options.
@@ -237,7 +233,7 @@ export default class DowncastWriter {
237
233
  * ] );
238
234
  *
239
235
  * // Create element with specific options.
240
- * writer.createContainerElement( 'span', { class: 'placeholder' }, { isAllowedInsideAttributeElement: true } );
236
+ * writer.createContainerElement( 'span', { class: 'placeholder' }, { renderUnsafeAttributes: [ 'foo' ] } );
241
237
  *
242
238
  * @param {String} name Name of the element.
243
239
  * @param {Object} [attributes] Elements attributes.
@@ -245,9 +241,6 @@ export default class DowncastWriter {
245
241
  * A node or a list of nodes to be inserted into the created element. If no children were specified, element's `options`
246
242
  * can be passed in this argument.
247
243
  * @param {Object} [options] Element's options.
248
- * @param {Boolean} [options.isAllowedInsideAttributeElement=false] Whether an element is
249
- * {@link module:engine/view/element~Element#isAllowedInsideAttributeElement allowed inside an AttributeElement} and can be wrapped
250
- * with {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
251
244
  * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
252
245
  * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
253
246
  * @returns {module:engine/view/containerelement~ContainerElement} Created element.
@@ -263,10 +256,6 @@ export default class DowncastWriter {
263
256
 
264
257
  const containerElement = new ContainerElement( this.document, name, attributes, children );
265
258
 
266
- if ( options.isAllowedInsideAttributeElement !== undefined ) {
267
- containerElement._isAllowedInsideAttributeElement = options.isAllowedInsideAttributeElement;
268
- }
269
-
270
259
  if ( options.renderUnsafeAttributes ) {
271
260
  containerElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
272
261
  }
@@ -310,9 +299,6 @@ export default class DowncastWriter {
310
299
  * @param {String} name Name of the element.
311
300
  * @param {Object} [attributes] Elements attributes.
312
301
  * @param {Object} [options] Element's options.
313
- * @param {Boolean} [options.isAllowedInsideAttributeElement=true] Whether an element is
314
- * {@link module:engine/view/element~Element#isAllowedInsideAttributeElement allowed inside an AttributeElement} and can be wrapped
315
- * with {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
316
302
  * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
317
303
  * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
318
304
  * @returns {module:engine/view/emptyelement~EmptyElement} Created element.
@@ -320,10 +306,6 @@ export default class DowncastWriter {
320
306
  createEmptyElement( name, attributes, options = {} ) {
321
307
  const emptyElement = new EmptyElement( this.document, name, attributes );
322
308
 
323
- if ( options.isAllowedInsideAttributeElement !== undefined ) {
324
- emptyElement._isAllowedInsideAttributeElement = options.isAllowedInsideAttributeElement;
325
- }
326
-
327
309
  if ( options.renderUnsafeAttributes ) {
328
310
  emptyElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
329
311
  }
@@ -354,23 +336,15 @@ export default class DowncastWriter {
354
336
  * @param {String} name The name of the element.
355
337
  * @param {Object} [attributes] Element attributes.
356
338
  * @param {Function} [renderFunction] A custom render function.
357
- * @param {Object} [options] Element's options.
358
- * @param {Boolean} [options.isAllowedInsideAttributeElement=true] Whether an element is
359
- * {@link module:engine/view/element~Element#isAllowedInsideAttributeElement allowed inside an AttributeElement} and can be wrapped
360
- * with {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
361
339
  * @returns {module:engine/view/uielement~UIElement} The created element.
362
340
  */
363
- createUIElement( name, attributes, renderFunction, options = {} ) {
341
+ createUIElement( name, attributes, renderFunction ) {
364
342
  const uiElement = new UIElement( this.document, name, attributes );
365
343
 
366
344
  if ( renderFunction ) {
367
345
  uiElement.render = renderFunction;
368
346
  }
369
347
 
370
- if ( options.isAllowedInsideAttributeElement !== undefined ) {
371
- uiElement._isAllowedInsideAttributeElement = options.isAllowedInsideAttributeElement;
372
- }
373
-
374
348
  return uiElement;
375
349
  }
376
350
 
@@ -397,9 +371,6 @@ export default class DowncastWriter {
397
371
  * @param {Object} [attributes] Element attributes.
398
372
  * @param {Function} [renderFunction] A custom render function.
399
373
  * @param {Object} [options] Element's options.
400
- * @param {Boolean} [options.isAllowedInsideAttributeElement=true] Whether an element is
401
- * {@link module:engine/view/element~Element#isAllowedInsideAttributeElement allowed inside an AttributeElement} and can be wrapped
402
- * with {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
403
374
  * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
404
375
  * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
405
376
  * @returns {module:engine/view/rawelement~RawElement} The created element.
@@ -409,10 +380,6 @@ export default class DowncastWriter {
409
380
 
410
381
  rawElement.render = renderFunction || ( () => {} );
411
382
 
412
- if ( options.isAllowedInsideAttributeElement !== undefined ) {
413
- rawElement._isAllowedInsideAttributeElement = options.isAllowedInsideAttributeElement;
414
- }
415
-
416
383
  if ( options.renderUnsafeAttributes ) {
417
384
  rawElement._unsafeAttributesToRender.push( ...options.renderUnsafeAttributes );
418
385
  }
@@ -790,7 +757,7 @@ export default class DowncastWriter {
790
757
 
791
758
  // Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
792
759
  // can't have an attribute in model and won't get wrapped with an AttributeElement while down-casted.
793
- const breakAttributes = !( node.is( 'uiElement' ) && node.isAllowedInsideAttributeElement );
760
+ const breakAttributes = !node.is( 'uiElement' );
794
761
 
795
762
  if ( !lastGroup || lastGroup.breakAttributes != breakAttributes ) {
796
763
  groups.push( {
@@ -979,16 +946,6 @@ export default class DowncastWriter {
979
946
  * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
980
947
  * is collapsed and different than view selection.
981
948
  *
982
- * **Note:** Attribute elements by default can wrap {@link module:engine/view/text~Text},
983
- * {@link module:engine/view/emptyelement~EmptyElement}, {@link module:engine/view/uielement~UIElement},
984
- * {@link module:engine/view/rawelement~RawElement} and other attribute elements with higher priority. Other elements while placed
985
- * inside an attribute element will split it (or nest it in case of an `AttributeElement`). This behavior can be modified by changing
986
- * the `isAllowedInsideAttributeElement` option while using
987
- * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement},
988
- * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement},
989
- * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement} or
990
- * {@link module:engine/view/downcastwriter~DowncastWriter#createRawElement}.
991
- *
992
949
  * @param {module:engine/view/range~Range} range Range to wrap.
993
950
  * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
994
951
  * @returns {module:engine/view/range~Range} range Range after wrapping, spanning over wrapping attribute element.
@@ -1399,7 +1356,6 @@ export default class DowncastWriter {
1399
1356
  const child = parent.getChild( i );
1400
1357
  const isText = child.is( '$text' );
1401
1358
  const isAttribute = child.is( 'attributeElement' );
1402
- const isAllowedInsideAttributeElement = child.isAllowedInsideAttributeElement;
1403
1359
 
1404
1360
  //
1405
1361
  // (In all examples, assume that `wrapElement` is `<span class="foo">` element.)
@@ -1418,7 +1374,7 @@ export default class DowncastWriter {
1418
1374
  //
1419
1375
  // <p>abc</p> --> <p><span class="foo">abc</span></p>
1420
1376
  // <p><strong>abc</strong></p> --> <p><span class="foo"><strong>abc</strong></span></p>
1421
- else if ( isText || isAllowedInsideAttributeElement || ( isAttribute && shouldABeOutsideB( wrapElement, child ) ) ) {
1377
+ else if ( isText || !isAttribute || shouldABeOutsideB( wrapElement, child ) ) {
1422
1378
  // Clone attribute.
1423
1379
  const newAttribute = wrapElement._clone();
1424
1380
 
@@ -1436,7 +1392,7 @@ export default class DowncastWriter {
1436
1392
  //
1437
1393
  // <p><a href="foo.html">abc</a></p> --> <p><a href="foo.html"><span class="foo">abc</span></a></p>
1438
1394
  //
1439
- else if ( isAttribute ) {
1395
+ else /* if ( isAttribute ) */ {
1440
1396
  this._wrapChildren( child, 0, child.childCount, wrapElement );
1441
1397
  }
1442
1398
 
@@ -130,15 +130,6 @@ export default class Element extends Node {
130
130
  */
131
131
  this._customProperties = new Map();
132
132
 
133
- /**
134
- * Whether an element is allowed inside an AttributeElement and can be wrapped with
135
- * {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
136
- *
137
- * @protected
138
- * @member {Boolean}
139
- */
140
- this._isAllowedInsideAttributeElement = false;
141
-
142
133
  /**
143
134
  * A list of attribute names that should be rendered in the editing pipeline even though filtering mechanisms
144
135
  * implemented in the {@link module:engine/view/domconverter~DomConverter} (for instance,
@@ -175,17 +166,6 @@ export default class Element extends Node {
175
166
  return this._children.length === 0;
176
167
  }
177
168
 
178
- /**
179
- * Whether the element is allowed inside an AttributeElement and can be wrapped with
180
- * {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
181
- *
182
- * @readonly
183
- * @type {Boolean}
184
- */
185
- get isAllowedInsideAttributeElement() {
186
- return this._isAllowedInsideAttributeElement;
187
- }
188
-
189
169
  /**
190
170
  * Checks whether this object is of the given.
191
171
  *
@@ -350,11 +330,6 @@ export default class Element extends Node {
350
330
  return false;
351
331
  }
352
332
 
353
- // Check isAllowedInsideAttributeElement property.
354
- if ( this.isAllowedInsideAttributeElement != otherElement.isAllowedInsideAttributeElement ) {
355
- return false;
356
- }
357
-
358
333
  // Check number of attributes, classes and styles.
359
334
  if ( this._attrs.size !== otherElement._attrs.size || this._classes.size !== otherElement._classes.size ||
360
335
  this._styles.size !== otherElement._styles.size ) {
@@ -633,7 +608,8 @@ export default class Element extends Node {
633
608
  // is changed by e.g. toWidget() function from ckeditor5-widget. Perhaps this should be one of custom props.
634
609
  cloned.getFillerOffset = this.getFillerOffset;
635
610
 
636
- cloned._isAllowedInsideAttributeElement = this.isAllowedInsideAttributeElement;
611
+ // Clone unsafe attributes list.
612
+ cloned._unsafeAttributesToRender = this._unsafeAttributesToRender;
637
613
 
638
614
  return cloned;
639
615
  }
@@ -37,9 +37,6 @@ export default class EmptyElement extends Element {
37
37
  constructor( document, name, attrs, children ) {
38
38
  super( document, name, attrs, children );
39
39
 
40
- // Override the default of the base class.
41
- this._isAllowedInsideAttributeElement = true;
42
-
43
40
  /**
44
41
  * Returns `null` because filler is not needed for EmptyElements.
45
42
  *
@@ -56,7 +56,7 @@ export const NBSP_FILLER = domDocument => domDocument.createTextNode( '\u00A0' )
56
56
  export const MARKED_NBSP_FILLER = domDocument => {
57
57
  const span = domDocument.createElement( 'span' );
58
58
  span.dataset.ckeFiller = true;
59
- span.innerHTML = '\u00A0';
59
+ span.innerText = '\u00A0';
60
60
 
61
61
  return span;
62
62
  };
@@ -739,7 +739,7 @@ function matchStyles( patterns, element ) {
739
739
  * styles: /^border.*$/
740
740
  * }
741
741
  *
742
- * Refer to the {@glink builds/guides/migration/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
742
+ * Refer to the {@glink updating/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
743
743
  * and {@link module:engine/view/matcher~MatcherPattern} documentation.
744
744
  *
745
745
  * @param {Object} pattern Pattern with missing properties.
@@ -769,7 +769,7 @@ function matchStyles( patterns, element ) {
769
769
  * classes: 'foobar'
770
770
  * }
771
771
  *
772
- * Refer to the {@glink builds/guides/migration/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
772
+ * Refer to the {@glink updating/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
773
773
  * and the {@link module:engine/view/matcher~MatcherPattern} documentation.
774
774
  *
775
775
  * @param {Object} pattern Pattern with missing properties.
@@ -1,4 +1,3 @@
1
-
2
1
  /**
3
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
4
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
 
6
6
  /**
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+
6
+ /**
7
+ * @module engine/view/observer/tabobserver
8
+ */
9
+
10
+ import Observer from './observer';
11
+ import BubblingEventInfo from './bubblingeventinfo';
12
+
13
+ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
14
+
15
+ /**
16
+ * Tab observer introduces the {@link module:engine/view/document~Document#event:tab `Document#tab`} event.
17
+ *
18
+ * Note that because {@link module:engine/view/observer/tabobserver~TabObserver} is attached by the
19
+ * {@link module:engine/view/view~View}, this event is available by default.
20
+ *
21
+ * @extends module:engine/view/observer/observer~Observer
22
+ */
23
+ export default class TabObserver extends Observer {
24
+ /**
25
+ * @inheritDoc
26
+ */
27
+ constructor( view ) {
28
+ super( view );
29
+
30
+ const doc = this.document;
31
+
32
+ doc.on( 'keydown', ( evt, data ) => {
33
+ if (
34
+ !this.isEnabled ||
35
+ data.keyCode != keyCodes.tab ||
36
+ data.ctrlKey
37
+ ) {
38
+ return;
39
+ }
40
+
41
+ const event = new BubblingEventInfo( doc, 'tab', doc.selection.getFirstRange() );
42
+
43
+ doc.fire( event, data );
44
+
45
+ if ( event.stop.called ) {
46
+ evt.stop();
47
+ }
48
+ } );
49
+ }
50
+
51
+ /**
52
+ * @inheritDoc
53
+ */
54
+ observe() {}
55
+ }
56
+
57
+ /**
58
+ * Event fired when the user presses a tab key.
59
+ *
60
+ * Introduced by {@link module:engine/view/observer/tabobserver~TabObserver}.
61
+ *
62
+ * Note that because {@link module:engine/view/observer/tabobserver~TabObserver} is attached by the
63
+ * {@link module:engine/view/view~View}, this event is available by default.
64
+ *
65
+ * @event module:engine/view/document~Document#event:tab
66
+ *
67
+ * @param {module:engine/view/observer/domeventdata~DomEventData} data
68
+ */
@@ -276,7 +276,7 @@ function getChildPlaceholderHostSubstitute( parent ) {
276
276
  if ( parent.childCount ) {
277
277
  const firstChild = parent.getChild( 0 );
278
278
 
279
- if ( firstChild.is( 'element' ) && !firstChild.is( 'uiElement' ) ) {
279
+ if ( firstChild.is( 'element' ) && !firstChild.is( 'uiElement' ) && !firstChild.is( 'attributeElement' ) ) {
280
280
  return firstChild;
281
281
  }
282
282
  }
@@ -47,9 +47,6 @@ export default class RawElement extends Element {
47
47
  constructor( document, name, attrs, children ) {
48
48
  super( document, name, attrs, children );
49
49
 
50
- // Override the default of the base class.
51
- this._isAllowedInsideAttributeElement = true;
52
-
53
50
  /**
54
51
  * Returns `null` because filler is not needed for raw elements.
55
52
  *
@@ -234,6 +234,10 @@ export default class Renderer {
234
234
  else if ( this._inlineFiller && this._inlineFiller.parentNode ) {
235
235
  // While the user is making selection, preserve the inline filler at its original position.
236
236
  inlineFillerPosition = this.domConverter.domPositionToView( this._inlineFiller );
237
+
238
+ if ( inlineFillerPosition.parent.is( '$text' ) ) {
239
+ inlineFillerPosition = ViewPosition._createBefore( inlineFillerPosition.parent );
240
+ }
237
241
  }
238
242
 
239
243
  for ( const element of this.markedAttributes ) {
@@ -50,9 +50,6 @@ export default class UIElement extends Element {
50
50
  constructor( document, name, attributes, children ) {
51
51
  super( document, name, attributes, children );
52
52
 
53
- // Override the default of the base class.
54
- this._isAllowedInsideAttributeElement = true;
55
-
56
53
  /**
57
54
  * Returns `null` because filler is not needed for UIElements.
58
55
  *
package/src/view/view.js CHANGED
@@ -23,6 +23,7 @@ import FocusObserver from './observer/focusobserver';
23
23
  import CompositionObserver from './observer/compositionobserver';
24
24
  import InputObserver from './observer/inputobserver';
25
25
  import ArrowKeysObserver from './observer/arrowkeysobserver';
26
+ import TabObserver from './observer/tabobserver';
26
27
 
27
28
  import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
28
29
  import mix from '@ckeditor/ckeditor5-utils/src/mix';
@@ -54,6 +55,8 @@ import env from '@ckeditor/ckeditor5-utils/src/env';
54
55
  * * {@link module:engine/view/observer/keyobserver~KeyObserver},
55
56
  * * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
56
57
  * * {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
58
+ * * {@link module:engine/view/observer/arrowkeysobserver~ArrowKeysObserver}.
59
+ * * {@link module:engine/view/observer/tabobserver~TabObserver}.
57
60
  *
58
61
  * This class also {@link module:engine/view/view~View#attachDomRoot binds the DOM and the view elements}.
59
62
  *
@@ -186,6 +189,7 @@ export default class View {
186
189
  this.addObserver( FakeSelectionObserver );
187
190
  this.addObserver( CompositionObserver );
188
191
  this.addObserver( ArrowKeysObserver );
192
+ this.addObserver( TabObserver );
189
193
 
190
194
  if ( env.isAndroid ) {
191
195
  this.addObserver( InputObserver );