@ckeditor/ckeditor5-engine 47.6.1 → 48.0.0-alpha.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/LICENSE.md +1 -1
- package/{src → dist}/engineconfig.d.ts +6 -15
- package/dist/index-editor.css +38 -15
- package/dist/index.css +37 -37
- package/dist/index.css.map +1 -1
- package/{src → dist}/index.d.ts +0 -1
- package/dist/index.js +588 -94
- package/dist/index.js.map +1 -1
- package/{src → dist}/model/model.d.ts +10 -4
- package/{src → dist}/model/selection.d.ts +1 -1
- package/{src → dist}/view/downcastwriter.d.ts +3 -2
- package/{src → dist}/view/element.d.ts +2 -2
- package/{src → dist}/view/matcher.d.ts +4 -2
- package/dist/view/styles/background.d.ts +18 -0
- package/{src → dist}/view/styles/border.d.ts +0 -12
- package/{src → dist}/view/styles/margin.d.ts +0 -13
- package/{src → dist}/view/styles/padding.d.ts +0 -13
- package/{src → dist}/view/styles/utils.d.ts +12 -0
- package/package.json +20 -39
- package/src/controller/datacontroller.js +0 -522
- package/src/controller/editingcontroller.js +0 -181
- package/src/conversion/conversion.js +0 -606
- package/src/conversion/conversionhelpers.js +0 -33
- package/src/conversion/downcastdispatcher.js +0 -563
- package/src/conversion/downcasthelpers.js +0 -2160
- package/src/conversion/mapper.js +0 -1050
- package/src/conversion/modelconsumable.js +0 -331
- package/src/conversion/upcastdispatcher.js +0 -470
- package/src/conversion/upcasthelpers.js +0 -952
- package/src/conversion/viewconsumable.js +0 -541
- package/src/dataprocessor/basichtmlwriter.js +0 -22
- package/src/dataprocessor/dataprocessor.js +0 -5
- package/src/dataprocessor/htmldataprocessor.js +0 -107
- package/src/dataprocessor/htmlwriter.js +0 -5
- package/src/dataprocessor/xmldataprocessor.js +0 -127
- package/src/dev-utils/model.js +0 -396
- package/src/dev-utils/operationreplayer.js +0 -116
- package/src/dev-utils/utils.js +0 -122
- package/src/dev-utils/view.js +0 -990
- package/src/engineconfig.js +0 -5
- package/src/index.js +0 -134
- package/src/legacyerrors.js +0 -17
- package/src/model/batch.js +0 -98
- package/src/model/differ.js +0 -1288
- package/src/model/document.js +0 -398
- package/src/model/documentfragment.js +0 -332
- package/src/model/documentselection.js +0 -1026
- package/src/model/element.js +0 -323
- package/src/model/history.js +0 -206
- package/src/model/item.js +0 -5
- package/src/model/liveposition.js +0 -93
- package/src/model/liverange.js +0 -121
- package/src/model/markercollection.js +0 -436
- package/src/model/model.js +0 -866
- package/src/model/node.js +0 -371
- package/src/model/nodelist.js +0 -244
- package/src/model/operation/attributeoperation.js +0 -172
- package/src/model/operation/detachoperation.js +0 -87
- package/src/model/operation/insertoperation.js +0 -153
- package/src/model/operation/markeroperation.js +0 -136
- package/src/model/operation/mergeoperation.js +0 -184
- package/src/model/operation/moveoperation.js +0 -179
- package/src/model/operation/nooperation.js +0 -48
- package/src/model/operation/operation.js +0 -78
- package/src/model/operation/operationfactory.js +0 -44
- package/src/model/operation/renameoperation.js +0 -128
- package/src/model/operation/rootattributeoperation.js +0 -173
- package/src/model/operation/rootoperation.js +0 -106
- package/src/model/operation/splitoperation.js +0 -214
- package/src/model/operation/transform.js +0 -2211
- package/src/model/operation/utils.js +0 -217
- package/src/model/position.js +0 -1041
- package/src/model/range.js +0 -880
- package/src/model/rootelement.js +0 -82
- package/src/model/schema.js +0 -1542
- package/src/model/selection.js +0 -814
- package/src/model/text.js +0 -92
- package/src/model/textproxy.js +0 -202
- package/src/model/treewalker.js +0 -313
- package/src/model/typecheckable.js +0 -16
- package/src/model/utils/autoparagraphing.js +0 -63
- package/src/model/utils/deletecontent.js +0 -509
- package/src/model/utils/getselectedcontent.js +0 -126
- package/src/model/utils/insertcontent.js +0 -750
- package/src/model/utils/insertobject.js +0 -135
- package/src/model/utils/modifyselection.js +0 -187
- package/src/model/utils/selection-post-fixer.js +0 -264
- package/src/model/writer.js +0 -1318
- package/src/view/attributeelement.js +0 -220
- package/src/view/containerelement.js +0 -91
- package/src/view/datatransfer.js +0 -106
- package/src/view/document.js +0 -139
- package/src/view/documentfragment.js +0 -251
- package/src/view/documentselection.js +0 -270
- package/src/view/domconverter.js +0 -1661
- package/src/view/downcastwriter.js +0 -1589
- package/src/view/editableelement.js +0 -74
- package/src/view/element.js +0 -1053
- package/src/view/elementdefinition.js +0 -5
- package/src/view/emptyelement.js +0 -83
- package/src/view/filler.js +0 -161
- package/src/view/item.js +0 -5
- package/src/view/matcher.js +0 -437
- package/src/view/node.js +0 -238
- package/src/view/observer/arrowkeysobserver.js +0 -40
- package/src/view/observer/bubblingemittermixin.js +0 -215
- package/src/view/observer/bubblingeventinfo.js +0 -49
- package/src/view/observer/clickobserver.js +0 -26
- package/src/view/observer/compositionobserver.js +0 -64
- package/src/view/observer/domeventdata.js +0 -63
- package/src/view/observer/domeventobserver.js +0 -81
- package/src/view/observer/fakeselectionobserver.js +0 -95
- package/src/view/observer/focusobserver.js +0 -166
- package/src/view/observer/inputobserver.js +0 -236
- package/src/view/observer/keyobserver.js +0 -36
- package/src/view/observer/mouseobserver.js +0 -26
- package/src/view/observer/mutationobserver.js +0 -219
- package/src/view/observer/observer.js +0 -92
- package/src/view/observer/pointerobserver.js +0 -26
- package/src/view/observer/selectionobserver.js +0 -318
- package/src/view/observer/tabobserver.js +0 -42
- package/src/view/observer/touchobserver.js +0 -26
- package/src/view/placeholder.js +0 -285
- package/src/view/position.js +0 -341
- package/src/view/range.js +0 -451
- package/src/view/rawelement.js +0 -115
- package/src/view/renderer.js +0 -1148
- package/src/view/rooteditableelement.js +0 -78
- package/src/view/selection.js +0 -594
- package/src/view/styles/background.d.ts +0 -33
- package/src/view/styles/background.js +0 -74
- package/src/view/styles/border.js +0 -316
- package/src/view/styles/margin.js +0 -34
- package/src/view/styles/padding.js +0 -34
- package/src/view/styles/utils.js +0 -219
- package/src/view/stylesmap.js +0 -941
- package/src/view/text.js +0 -110
- package/src/view/textproxy.js +0 -136
- package/src/view/tokenlist.js +0 -194
- package/src/view/treewalker.js +0 -389
- package/src/view/typecheckable.js +0 -19
- package/src/view/uielement.js +0 -194
- package/src/view/upcastwriter.js +0 -363
- package/src/view/view.js +0 -579
- package/theme/placeholder.css +0 -36
- package/theme/renderer.css +0 -9
- /package/{src → dist}/controller/datacontroller.d.ts +0 -0
- /package/{src → dist}/controller/editingcontroller.d.ts +0 -0
- /package/{src → dist}/conversion/conversion.d.ts +0 -0
- /package/{src → dist}/conversion/conversionhelpers.d.ts +0 -0
- /package/{src → dist}/conversion/downcastdispatcher.d.ts +0 -0
- /package/{src → dist}/conversion/downcasthelpers.d.ts +0 -0
- /package/{src → dist}/conversion/mapper.d.ts +0 -0
- /package/{src → dist}/conversion/modelconsumable.d.ts +0 -0
- /package/{src → dist}/conversion/upcastdispatcher.d.ts +0 -0
- /package/{src → dist}/conversion/upcasthelpers.d.ts +0 -0
- /package/{src → dist}/conversion/viewconsumable.d.ts +0 -0
- /package/{src → dist}/dataprocessor/basichtmlwriter.d.ts +0 -0
- /package/{src → dist}/dataprocessor/dataprocessor.d.ts +0 -0
- /package/{src → dist}/dataprocessor/htmldataprocessor.d.ts +0 -0
- /package/{src → dist}/dataprocessor/htmlwriter.d.ts +0 -0
- /package/{src → dist}/dataprocessor/xmldataprocessor.d.ts +0 -0
- /package/{src → dist}/dev-utils/model.d.ts +0 -0
- /package/{src → dist}/dev-utils/operationreplayer.d.ts +0 -0
- /package/{src → dist}/dev-utils/utils.d.ts +0 -0
- /package/{src → dist}/dev-utils/view.d.ts +0 -0
- /package/{src → dist}/legacyerrors.d.ts +0 -0
- /package/{src → dist}/model/batch.d.ts +0 -0
- /package/{src → dist}/model/differ.d.ts +0 -0
- /package/{src → dist}/model/document.d.ts +0 -0
- /package/{src → dist}/model/documentfragment.d.ts +0 -0
- /package/{src → dist}/model/documentselection.d.ts +0 -0
- /package/{src → dist}/model/element.d.ts +0 -0
- /package/{src → dist}/model/history.d.ts +0 -0
- /package/{src → dist}/model/item.d.ts +0 -0
- /package/{src → dist}/model/liveposition.d.ts +0 -0
- /package/{src → dist}/model/liverange.d.ts +0 -0
- /package/{src → dist}/model/markercollection.d.ts +0 -0
- /package/{src → dist}/model/node.d.ts +0 -0
- /package/{src → dist}/model/nodelist.d.ts +0 -0
- /package/{src → dist}/model/operation/attributeoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/detachoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/insertoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/markeroperation.d.ts +0 -0
- /package/{src → dist}/model/operation/mergeoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/moveoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/nooperation.d.ts +0 -0
- /package/{src → dist}/model/operation/operation.d.ts +0 -0
- /package/{src → dist}/model/operation/operationfactory.d.ts +0 -0
- /package/{src → dist}/model/operation/renameoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/rootattributeoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/rootoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/splitoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/transform.d.ts +0 -0
- /package/{src → dist}/model/operation/utils.d.ts +0 -0
- /package/{src → dist}/model/position.d.ts +0 -0
- /package/{src → dist}/model/range.d.ts +0 -0
- /package/{src → dist}/model/rootelement.d.ts +0 -0
- /package/{src → dist}/model/schema.d.ts +0 -0
- /package/{src → dist}/model/text.d.ts +0 -0
- /package/{src → dist}/model/textproxy.d.ts +0 -0
- /package/{src → dist}/model/treewalker.d.ts +0 -0
- /package/{src → dist}/model/typecheckable.d.ts +0 -0
- /package/{src → dist}/model/utils/autoparagraphing.d.ts +0 -0
- /package/{src → dist}/model/utils/deletecontent.d.ts +0 -0
- /package/{src → dist}/model/utils/getselectedcontent.d.ts +0 -0
- /package/{src → dist}/model/utils/insertcontent.d.ts +0 -0
- /package/{src → dist}/model/utils/insertobject.d.ts +0 -0
- /package/{src → dist}/model/utils/modifyselection.d.ts +0 -0
- /package/{src → dist}/model/utils/selection-post-fixer.d.ts +0 -0
- /package/{src → dist}/model/writer.d.ts +0 -0
- /package/{src → dist}/view/attributeelement.d.ts +0 -0
- /package/{src → dist}/view/containerelement.d.ts +0 -0
- /package/{src → dist}/view/datatransfer.d.ts +0 -0
- /package/{src → dist}/view/document.d.ts +0 -0
- /package/{src → dist}/view/documentfragment.d.ts +0 -0
- /package/{src → dist}/view/documentselection.d.ts +0 -0
- /package/{src → dist}/view/domconverter.d.ts +0 -0
- /package/{src → dist}/view/editableelement.d.ts +0 -0
- /package/{src → dist}/view/elementdefinition.d.ts +0 -0
- /package/{src → dist}/view/emptyelement.d.ts +0 -0
- /package/{src → dist}/view/filler.d.ts +0 -0
- /package/{src → dist}/view/item.d.ts +0 -0
- /package/{src → dist}/view/node.d.ts +0 -0
- /package/{src → dist}/view/observer/arrowkeysobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/bubblingemittermixin.d.ts +0 -0
- /package/{src → dist}/view/observer/bubblingeventinfo.d.ts +0 -0
- /package/{src → dist}/view/observer/clickobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/compositionobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/domeventdata.d.ts +0 -0
- /package/{src → dist}/view/observer/domeventobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/fakeselectionobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/focusobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/inputobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/keyobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/mouseobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/mutationobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/observer.d.ts +0 -0
- /package/{src → dist}/view/observer/pointerobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/selectionobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/tabobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/touchobserver.d.ts +0 -0
- /package/{src → dist}/view/placeholder.d.ts +0 -0
- /package/{src → dist}/view/position.d.ts +0 -0
- /package/{src → dist}/view/range.d.ts +0 -0
- /package/{src → dist}/view/rawelement.d.ts +0 -0
- /package/{src → dist}/view/renderer.d.ts +0 -0
- /package/{src → dist}/view/rooteditableelement.d.ts +0 -0
- /package/{src → dist}/view/selection.d.ts +0 -0
- /package/{src → dist}/view/stylesmap.d.ts +0 -0
- /package/{src → dist}/view/text.d.ts +0 -0
- /package/{src → dist}/view/textproxy.d.ts +0 -0
- /package/{src → dist}/view/tokenlist.d.ts +0 -0
- /package/{src → dist}/view/treewalker.d.ts +0 -0
- /package/{src → dist}/view/typecheckable.d.ts +0 -0
- /package/{src → dist}/view/uielement.d.ts +0 -0
- /package/{src → dist}/view/upcastwriter.d.ts +0 -0
- /package/{src → dist}/view/view.d.ts +0 -0
|
@@ -1,1589 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @module engine/view/downcastwriter
|
|
7
|
-
*/
|
|
8
|
-
import { ViewPosition } from './position.js';
|
|
9
|
-
import { ViewRange } from './range.js';
|
|
10
|
-
import { ViewSelection } from './selection.js';
|
|
11
|
-
import { ViewContainerElement } from './containerelement.js';
|
|
12
|
-
import { ViewAttributeElement } from './attributeelement.js';
|
|
13
|
-
import { ViewEmptyElement } from './emptyelement.js';
|
|
14
|
-
import { ViewUIElement } from './uielement.js';
|
|
15
|
-
import { ViewRawElement } from './rawelement.js';
|
|
16
|
-
import { CKEditorError, isIterable } from '@ckeditor/ckeditor5-utils';
|
|
17
|
-
import { ViewDocumentFragment } from './documentfragment.js';
|
|
18
|
-
import { ViewText } from './text.js';
|
|
19
|
-
import { ViewEditableElement } from './editableelement.js';
|
|
20
|
-
import { isPlainObject } from 'es-toolkit/compat';
|
|
21
|
-
/**
|
|
22
|
-
* View downcast writer.
|
|
23
|
-
*
|
|
24
|
-
* It provides a set of methods used to manipulate view nodes.
|
|
25
|
-
*
|
|
26
|
-
* Do not create an instance of this writer manually. To modify a view structure, use
|
|
27
|
-
* the {@link module:engine/view/view~EditingView#change `View#change()`} block.
|
|
28
|
-
*
|
|
29
|
-
* The `ViewDowncastWriter` is designed to work with semantic views which are the views that were/are being downcasted from the model.
|
|
30
|
-
* To work with ordinary views (e.g. parsed from a pasted content) use the
|
|
31
|
-
* {@link module:engine/view/upcastwriter~ViewUpcastWriter upcast writer}.
|
|
32
|
-
*
|
|
33
|
-
* Read more about changing the view in the {@glink framework/architecture/editing-engine#changing-the-view Changing the view}
|
|
34
|
-
* section of the {@glink framework/architecture/editing-engine Editing engine architecture} guide.
|
|
35
|
-
*/
|
|
36
|
-
export class ViewDowncastWriter {
|
|
37
|
-
/**
|
|
38
|
-
* The view document instance in which this writer operates.
|
|
39
|
-
*/
|
|
40
|
-
document;
|
|
41
|
-
/**
|
|
42
|
-
* Holds references to the attribute groups that share the same {@link module:engine/view/attributeelement~ViewAttributeElement#id id}.
|
|
43
|
-
* The keys are `id`s, the values are `Set`s holding {@link module:engine/view/attributeelement~ViewAttributeElement}s.
|
|
44
|
-
*/
|
|
45
|
-
_cloneGroups = new Map();
|
|
46
|
-
/**
|
|
47
|
-
* The slot factory used by the `elementToStructure` downcast helper.
|
|
48
|
-
*/
|
|
49
|
-
_slotFactory = null;
|
|
50
|
-
/**
|
|
51
|
-
* @param document The view document instance.
|
|
52
|
-
*/
|
|
53
|
-
constructor(document) {
|
|
54
|
-
this.document = document;
|
|
55
|
-
}
|
|
56
|
-
setSelection(...args) {
|
|
57
|
-
this.document.selection._setTo(...args);
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Moves {@link module:engine/view/documentselection~ViewDocumentSelection#focus selection's focus} to the specified location.
|
|
61
|
-
*
|
|
62
|
-
* The location can be specified in the same form as
|
|
63
|
-
* {@link module:engine/view/view~EditingView#createPositionAt view.createPositionAt()}
|
|
64
|
-
* parameters.
|
|
65
|
-
*
|
|
66
|
-
* @param itemOrPosition
|
|
67
|
-
* @param offset Offset or one of the flags. Used only when the first parameter is a {@link module:engine/view/item~ViewItem view item}.
|
|
68
|
-
*/
|
|
69
|
-
setSelectionFocus(itemOrPosition, offset) {
|
|
70
|
-
this.document.selection._setFocus(itemOrPosition, offset);
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Creates a new {@link module:engine/view/documentfragment~ViewDocumentFragment} instance.
|
|
74
|
-
*
|
|
75
|
-
* @param children A list of nodes to be inserted into the created document fragment.
|
|
76
|
-
* @returns The created document fragment.
|
|
77
|
-
*/
|
|
78
|
-
createDocumentFragment(children) {
|
|
79
|
-
return new ViewDocumentFragment(this.document, children);
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Creates a new {@link module:engine/view/text~ViewText text node}.
|
|
83
|
-
*
|
|
84
|
-
* ```ts
|
|
85
|
-
* writer.createText( 'foo' );
|
|
86
|
-
* ```
|
|
87
|
-
*
|
|
88
|
-
* @param data The text's data.
|
|
89
|
-
* @returns The created text node.
|
|
90
|
-
*/
|
|
91
|
-
createText(data) {
|
|
92
|
-
return new ViewText(this.document, data);
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Creates a new {@link module:engine/view/attributeelement~ViewAttributeElement}.
|
|
96
|
-
*
|
|
97
|
-
* ```ts
|
|
98
|
-
* writer.createAttributeElement( 'strong' );
|
|
99
|
-
* writer.createAttributeElement( 'a', { href: 'foo.bar' } );
|
|
100
|
-
*
|
|
101
|
-
* // Make `<a>` element contain other attributes element so the `<a>` element is not broken.
|
|
102
|
-
* writer.createAttributeElement( 'a', { href: 'foo.bar' }, { priority: 5 } );
|
|
103
|
-
*
|
|
104
|
-
* // Set `id` of a marker element so it is not joined or merged with "normal" elements.
|
|
105
|
-
* writer.createAttributeElement( 'span', { class: 'my-marker' }, { id: 'marker:my' } );
|
|
106
|
-
* ```
|
|
107
|
-
*
|
|
108
|
-
* @param name Name of the element.
|
|
109
|
-
* @param attributes Element's attributes.
|
|
110
|
-
* @param options Element's options.
|
|
111
|
-
* @param options.priority Element's {@link module:engine/view/attributeelement~ViewAttributeElement#priority priority}.
|
|
112
|
-
* @param options.id Element's {@link module:engine/view/attributeelement~ViewAttributeElement#id id}.
|
|
113
|
-
* @param options.renderUnsafeAttributes A list of attribute names that should be rendered in the editing
|
|
114
|
-
* pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
|
|
115
|
-
* @returns Created element.
|
|
116
|
-
*/
|
|
117
|
-
createAttributeElement(name, attributes, options = {}) {
|
|
118
|
-
const attributeElement = new ViewAttributeElement(this.document, name, attributes);
|
|
119
|
-
if (typeof options.priority === 'number') {
|
|
120
|
-
attributeElement._priority = options.priority;
|
|
121
|
-
}
|
|
122
|
-
if (options.id) {
|
|
123
|
-
attributeElement._id = options.id;
|
|
124
|
-
}
|
|
125
|
-
if (options.renderUnsafeAttributes) {
|
|
126
|
-
attributeElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
|
|
127
|
-
}
|
|
128
|
-
return attributeElement;
|
|
129
|
-
}
|
|
130
|
-
createContainerElement(name, attributes, childrenOrOptions = {}, options = {}) {
|
|
131
|
-
let children = undefined;
|
|
132
|
-
if (isContainerOptions(childrenOrOptions)) {
|
|
133
|
-
options = childrenOrOptions;
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
children = childrenOrOptions;
|
|
137
|
-
}
|
|
138
|
-
const containerElement = new ViewContainerElement(this.document, name, attributes, children);
|
|
139
|
-
if (options.renderUnsafeAttributes) {
|
|
140
|
-
containerElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
|
|
141
|
-
}
|
|
142
|
-
return containerElement;
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Creates a new {@link module:engine/view/editableelement~ViewEditableElement}.
|
|
146
|
-
*
|
|
147
|
-
* ```ts
|
|
148
|
-
* writer.createEditableElement( 'div' );
|
|
149
|
-
* writer.createEditableElement( 'div', { id: 'foo-1234' } );
|
|
150
|
-
* ```
|
|
151
|
-
*
|
|
152
|
-
* Note: The editable element is to be used in the editing pipeline. Usually, together with
|
|
153
|
-
* {@link module:widget/utils~toWidgetEditable `toWidgetEditable()`}.
|
|
154
|
-
*
|
|
155
|
-
* @param name Name of the element.
|
|
156
|
-
* @param attributes Elements attributes.
|
|
157
|
-
* @param options Element's options.
|
|
158
|
-
* @param options.renderUnsafeAttributes A list of attribute names that should be rendered in the editing
|
|
159
|
-
* pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
|
|
160
|
-
* @returns Created element.
|
|
161
|
-
*/
|
|
162
|
-
createEditableElement(name, attributes, options = {}) {
|
|
163
|
-
const editableElement = new ViewEditableElement(this.document, name, attributes);
|
|
164
|
-
if (options.renderUnsafeAttributes) {
|
|
165
|
-
editableElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
|
|
166
|
-
}
|
|
167
|
-
return editableElement;
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Creates a new {@link module:engine/view/emptyelement~ViewEmptyElement}.
|
|
171
|
-
*
|
|
172
|
-
* ```ts
|
|
173
|
-
* writer.createEmptyElement( 'img' );
|
|
174
|
-
* writer.createEmptyElement( 'img', { id: 'foo-1234' } );
|
|
175
|
-
* ```
|
|
176
|
-
*
|
|
177
|
-
* @param name Name of the element.
|
|
178
|
-
* @param attributes Elements attributes.
|
|
179
|
-
* @param options Element's options.
|
|
180
|
-
* @param options.renderUnsafeAttributes A list of attribute names that should be rendered in the editing
|
|
181
|
-
* pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
|
|
182
|
-
* @returns Created element.
|
|
183
|
-
*/
|
|
184
|
-
createEmptyElement(name, attributes, options = {}) {
|
|
185
|
-
const emptyElement = new ViewEmptyElement(this.document, name, attributes);
|
|
186
|
-
if (options.renderUnsafeAttributes) {
|
|
187
|
-
emptyElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
|
|
188
|
-
}
|
|
189
|
-
return emptyElement;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Creates a new {@link module:engine/view/uielement~ViewUIElement}.
|
|
193
|
-
*
|
|
194
|
-
* ```ts
|
|
195
|
-
* writer.createUIElement( 'span' );
|
|
196
|
-
* writer.createUIElement( 'span', { id: 'foo-1234' } );
|
|
197
|
-
* ```
|
|
198
|
-
*
|
|
199
|
-
* A custom render function can be provided as the third parameter:
|
|
200
|
-
*
|
|
201
|
-
* ```ts
|
|
202
|
-
* writer.createUIElement( 'span', null, function( domDocument ) {
|
|
203
|
-
* const domElement = this.toDomElement( domDocument );
|
|
204
|
-
* domElement.innerHTML = '<b>this is ui element</b>';
|
|
205
|
-
*
|
|
206
|
-
* return domElement;
|
|
207
|
-
* } );
|
|
208
|
-
* ```
|
|
209
|
-
*
|
|
210
|
-
* Unlike {@link #createRawElement raw elements}, UI elements are by no means editor content, for instance,
|
|
211
|
-
* they are ignored by the editor selection system.
|
|
212
|
-
*
|
|
213
|
-
* You should not use UI elements as data containers. Check out {@link #createRawElement} instead.
|
|
214
|
-
*
|
|
215
|
-
* @param name The name of the element.
|
|
216
|
-
* @param attributes Element attributes.
|
|
217
|
-
* @param renderFunction A custom render function.
|
|
218
|
-
* @returns The created element.
|
|
219
|
-
*/
|
|
220
|
-
createUIElement(name, attributes, renderFunction) {
|
|
221
|
-
const uiElement = new ViewUIElement(this.document, name, attributes);
|
|
222
|
-
if (renderFunction) {
|
|
223
|
-
uiElement.render = renderFunction;
|
|
224
|
-
}
|
|
225
|
-
return uiElement;
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Creates a new {@link module:engine/view/rawelement~ViewRawElement}.
|
|
229
|
-
*
|
|
230
|
-
* ```ts
|
|
231
|
-
* writer.createRawElement( 'span', { id: 'foo-1234' }, function( domElement ) {
|
|
232
|
-
* domElement.innerHTML = '<b>This is the raw content of the raw element.</b>';
|
|
233
|
-
* } );
|
|
234
|
-
* ```
|
|
235
|
-
*
|
|
236
|
-
* Raw elements work as data containers ("wrappers", "sandboxes") but their children are not managed or
|
|
237
|
-
* even recognized by the editor. This encapsulation allows integrations to maintain custom DOM structures
|
|
238
|
-
* in the editor content without, for instance, worrying about compatibility with other editor features.
|
|
239
|
-
* Raw elements are a perfect tool for integration with external frameworks and data sources.
|
|
240
|
-
*
|
|
241
|
-
* Unlike {@link #createUIElement UI elements}, raw elements act like "real" editor content (similar to
|
|
242
|
-
* {@link module:engine/view/containerelement~ViewContainerElement} or {@link module:engine/view/emptyelement~ViewEmptyElement}),
|
|
243
|
-
* and they are considered by the editor selection.
|
|
244
|
-
*
|
|
245
|
-
* You should not use raw elements to render the UI in the editor content. Check out {@link #createUIElement `#createUIElement()`}
|
|
246
|
-
* instead.
|
|
247
|
-
*
|
|
248
|
-
* @param name The name of the element.
|
|
249
|
-
* @param attributes Element attributes.
|
|
250
|
-
* @param renderFunction A custom render function.
|
|
251
|
-
* @param options Element's options.
|
|
252
|
-
* @param options.renderUnsafeAttributes A list of attribute names that should be rendered in the editing
|
|
253
|
-
* pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
|
|
254
|
-
* @returns The created element.
|
|
255
|
-
*/
|
|
256
|
-
createRawElement(name, attributes, renderFunction, options = {}) {
|
|
257
|
-
const rawElement = new ViewRawElement(this.document, name, attributes);
|
|
258
|
-
if (renderFunction) {
|
|
259
|
-
rawElement.render = renderFunction;
|
|
260
|
-
}
|
|
261
|
-
if (options.renderUnsafeAttributes) {
|
|
262
|
-
rawElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
|
|
263
|
-
}
|
|
264
|
-
return rawElement;
|
|
265
|
-
}
|
|
266
|
-
setAttribute(key, value, elementOrOverwrite, element) {
|
|
267
|
-
if (element !== undefined) {
|
|
268
|
-
element._setAttribute(key, value, elementOrOverwrite);
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
elementOrOverwrite._setAttribute(key, value);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
removeAttribute(key, elementOrTokens, element) {
|
|
275
|
-
if (element !== undefined) {
|
|
276
|
-
element._removeAttribute(key, elementOrTokens);
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
elementOrTokens._removeAttribute(key);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Adds specified class to the element.
|
|
284
|
-
*
|
|
285
|
-
* ```ts
|
|
286
|
-
* writer.addClass( 'foo', linkElement );
|
|
287
|
-
* writer.addClass( [ 'foo', 'bar' ], linkElement );
|
|
288
|
-
* ```
|
|
289
|
-
*/
|
|
290
|
-
addClass(className, element) {
|
|
291
|
-
element._addClass(className);
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Removes specified class from the element.
|
|
295
|
-
*
|
|
296
|
-
* ```ts
|
|
297
|
-
* writer.removeClass( 'foo', linkElement );
|
|
298
|
-
* writer.removeClass( [ 'foo', 'bar' ], linkElement );
|
|
299
|
-
* ```
|
|
300
|
-
*/
|
|
301
|
-
removeClass(className, element) {
|
|
302
|
-
element._removeClass(className);
|
|
303
|
-
}
|
|
304
|
-
setStyle(property, value, element) {
|
|
305
|
-
if (isPlainObject(property) && element === undefined) {
|
|
306
|
-
value._setStyle(property);
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
element._setStyle(property, value);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Removes specified style from the element.
|
|
314
|
-
*
|
|
315
|
-
* ```ts
|
|
316
|
-
* writer.removeStyle( 'color', element ); // Removes 'color' style.
|
|
317
|
-
* writer.removeStyle( [ 'color', 'border-top' ], element ); // Removes both 'color' and 'border-top' styles.
|
|
318
|
-
* ```
|
|
319
|
-
*
|
|
320
|
-
* **Note**: This method can work with normalized style names if
|
|
321
|
-
* {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
|
|
322
|
-
* See {@link module:engine/view/stylesmap~StylesMap#remove `StylesMap#remove()`} for details.
|
|
323
|
-
*/
|
|
324
|
-
removeStyle(property, element) {
|
|
325
|
-
element._removeStyle(property);
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Sets a custom property on element. Unlike attributes, custom properties are not rendered to the DOM,
|
|
329
|
-
* so they can be used to add special data to elements.
|
|
330
|
-
*/
|
|
331
|
-
setCustomProperty(key, value, element) {
|
|
332
|
-
element._setCustomProperty(key, value);
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Removes a custom property stored under the given key.
|
|
336
|
-
*
|
|
337
|
-
* @returns Returns true if property was removed.
|
|
338
|
-
*/
|
|
339
|
-
removeCustomProperty(key, element) {
|
|
340
|
-
return element._removeCustomProperty(key);
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Breaks attribute elements at the provided position or at the boundaries of a provided range. It breaks attribute elements
|
|
344
|
-
* up to their first ancestor that is a container element.
|
|
345
|
-
*
|
|
346
|
-
* In following examples `<p>` is a container, `<b>` and `<u>` are attribute elements:
|
|
347
|
-
*
|
|
348
|
-
* ```html
|
|
349
|
-
* <p>foo<b><u>bar{}</u></b></p> -> <p>foo<b><u>bar</u></b>[]</p>
|
|
350
|
-
* <p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p>
|
|
351
|
-
* <p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
|
|
352
|
-
* <p><b>fo{o</b><u>ba}r</u></p> -> <p><b>fo</b><b>o</b><u>ba</u><u>r</u></b></p>
|
|
353
|
-
* ```
|
|
354
|
-
*
|
|
355
|
-
* **Note:** {@link module:engine/view/documentfragment~ViewDocumentFragment DocumentFragment} is treated like a container.
|
|
356
|
-
*
|
|
357
|
-
* **Note:** The difference between {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes breakAttributes()} and
|
|
358
|
-
* {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
|
|
359
|
-
* {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements} that are ancestors of a given `position`,
|
|
360
|
-
* up to the first encountered {@link module:engine/view/containerelement~ViewContainerElement container element}.
|
|
361
|
-
* `breakContainer()` assumes that a given `position` is directly in the container element and breaks that container element.
|
|
362
|
-
*
|
|
363
|
-
* Throws the `view-writer-invalid-range-container` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
|
|
364
|
-
* when the {@link module:engine/view/range~ViewRange#start start}
|
|
365
|
-
* and {@link module:engine/view/range~ViewRange#end end} positions of a passed range are not placed inside same parent container.
|
|
366
|
-
*
|
|
367
|
-
* Throws the `view-writer-cannot-break-empty-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
|
|
368
|
-
* when trying to break attributes inside an {@link module:engine/view/emptyelement~ViewEmptyElement ViewEmptyElement}.
|
|
369
|
-
*
|
|
370
|
-
* Throws the `view-writer-cannot-break-ui-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
|
|
371
|
-
* when trying to break attributes inside a {@link module:engine/view/uielement~ViewUIElement UIElement}.
|
|
372
|
-
*
|
|
373
|
-
* @see module:engine/view/attributeelement~ViewAttributeElement
|
|
374
|
-
* @see module:engine/view/containerelement~ViewContainerElement
|
|
375
|
-
* @see module:engine/view/downcastwriter~ViewDowncastWriter#breakContainer
|
|
376
|
-
* @param positionOrRange The position where to break attribute elements.
|
|
377
|
-
* @returns The new position or range, after breaking the attribute elements.
|
|
378
|
-
*/
|
|
379
|
-
breakAttributes(positionOrRange) {
|
|
380
|
-
if (positionOrRange instanceof ViewPosition) {
|
|
381
|
-
return this._breakAttributes(positionOrRange);
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
return this._breakAttributesRange(positionOrRange);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
/**
|
|
388
|
-
* Breaks a {@link module:engine/view/containerelement~ViewContainerElement container view element} into two, at the given position.
|
|
389
|
-
* The position has to be directly inside the container element and cannot be in the root. It does not break the conrainer view element
|
|
390
|
-
* if the position is at the beginning or at the end of its parent element.
|
|
391
|
-
*
|
|
392
|
-
* ```html
|
|
393
|
-
* <p>foo^bar</p> -> <p>foo</p><p>bar</p>
|
|
394
|
-
* <div><p>foo</p>^<p>bar</p></div> -> <div><p>foo</p></div><div><p>bar</p></div>
|
|
395
|
-
* <p>^foobar</p> -> ^<p>foobar</p>
|
|
396
|
-
* <p>foobar^</p> -> <p>foobar</p>^
|
|
397
|
-
* ```
|
|
398
|
-
*
|
|
399
|
-
* **Note:** The difference between {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes breakAttributes()} and
|
|
400
|
-
* {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
|
|
401
|
-
* {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements} that are ancestors of a given `position`,
|
|
402
|
-
* up to the first encountered {@link module:engine/view/containerelement~ViewContainerElement container element}.
|
|
403
|
-
* `breakContainer()` assumes that the given `position` is directly in the container element and breaks that container element.
|
|
404
|
-
*
|
|
405
|
-
* @see module:engine/view/attributeelement~ViewAttributeElement
|
|
406
|
-
* @see module:engine/view/containerelement~ViewContainerElement
|
|
407
|
-
* @see module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes
|
|
408
|
-
* @param position The position where to break the element.
|
|
409
|
-
* @returns The position between broken elements. If an element has not been broken,
|
|
410
|
-
* the returned position is placed either before or after it.
|
|
411
|
-
*/
|
|
412
|
-
breakContainer(position) {
|
|
413
|
-
const element = position.parent;
|
|
414
|
-
if (!(element.is('containerElement'))) {
|
|
415
|
-
/**
|
|
416
|
-
* Trying to break an element which is not a container element.
|
|
417
|
-
*
|
|
418
|
-
* @error view-writer-break-non-container-element
|
|
419
|
-
*/
|
|
420
|
-
throw new CKEditorError('view-writer-break-non-container-element', this.document);
|
|
421
|
-
}
|
|
422
|
-
if (!element.parent) {
|
|
423
|
-
/**
|
|
424
|
-
* Trying to break root element.
|
|
425
|
-
*
|
|
426
|
-
* @error view-writer-break-root
|
|
427
|
-
*/
|
|
428
|
-
throw new CKEditorError('view-writer-break-root', this.document);
|
|
429
|
-
}
|
|
430
|
-
if (position.isAtStart) {
|
|
431
|
-
return ViewPosition._createBefore(element);
|
|
432
|
-
}
|
|
433
|
-
else if (!position.isAtEnd) {
|
|
434
|
-
const newElement = element._clone(false);
|
|
435
|
-
this.insert(ViewPosition._createAfter(element), newElement);
|
|
436
|
-
const sourceRange = new ViewRange(position, ViewPosition._createAt(element, 'end'));
|
|
437
|
-
const targetPosition = new ViewPosition(newElement, 0);
|
|
438
|
-
this.move(sourceRange, targetPosition);
|
|
439
|
-
}
|
|
440
|
-
return ViewPosition._createAfter(element);
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Merges {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements}. It also merges text nodes if needed.
|
|
444
|
-
* Only {@link module:engine/view/attributeelement~ViewAttributeElement#isSimilar similar} attribute elements can be merged.
|
|
445
|
-
*
|
|
446
|
-
* In following examples `<p>` is a container and `<b>` is an attribute element:
|
|
447
|
-
*
|
|
448
|
-
* ```html
|
|
449
|
-
* <p>foo[]bar</p> -> <p>foo{}bar</p>
|
|
450
|
-
* <p><b>foo</b>[]<b>bar</b></p> -> <p><b>foo{}bar</b></p>
|
|
451
|
-
* <p><b foo="bar">a</b>[]<b foo="baz">b</b></p> -> <p><b foo="bar">a</b>[]<b foo="baz">b</b></p>
|
|
452
|
-
* ```
|
|
453
|
-
*
|
|
454
|
-
* It will also take care about empty attributes when merging:
|
|
455
|
-
*
|
|
456
|
-
* ```html
|
|
457
|
-
* <p><b>[]</b></p> -> <p>[]</p>
|
|
458
|
-
* <p><b>foo</b><i>[]</i><b>bar</b></p> -> <p><b>foo{}bar</b></p>
|
|
459
|
-
* ```
|
|
460
|
-
*
|
|
461
|
-
* **Note:** Difference between {@link module:engine/view/downcastwriter~ViewDowncastWriter#mergeAttributes mergeAttributes} and
|
|
462
|
-
* {@link module:engine/view/downcastwriter~ViewDowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
|
|
463
|
-
* {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements} or
|
|
464
|
-
* {@link module:engine/view/text~ViewText text nodes} while `mergeContainer` merges two
|
|
465
|
-
* {@link module:engine/view/containerelement~ViewContainerElement container elements}.
|
|
466
|
-
*
|
|
467
|
-
* @see module:engine/view/attributeelement~ViewAttributeElement
|
|
468
|
-
* @see module:engine/view/containerelement~ViewContainerElement
|
|
469
|
-
* @see module:engine/view/downcastwriter~ViewDowncastWriter#mergeContainers
|
|
470
|
-
* @param position Merge position.
|
|
471
|
-
* @returns Position after merge.
|
|
472
|
-
*/
|
|
473
|
-
mergeAttributes(position) {
|
|
474
|
-
const positionOffset = position.offset;
|
|
475
|
-
const positionParent = position.parent;
|
|
476
|
-
// When inside text node - nothing to merge.
|
|
477
|
-
if (positionParent.is('$text')) {
|
|
478
|
-
return position;
|
|
479
|
-
}
|
|
480
|
-
// When inside empty attribute - remove it.
|
|
481
|
-
if (positionParent.is('attributeElement') && positionParent.childCount === 0) {
|
|
482
|
-
const parent = positionParent.parent;
|
|
483
|
-
const offset = positionParent.index;
|
|
484
|
-
positionParent._remove();
|
|
485
|
-
this._removeFromClonedElementsGroup(positionParent);
|
|
486
|
-
return this.mergeAttributes(new ViewPosition(parent, offset));
|
|
487
|
-
}
|
|
488
|
-
const nodeBefore = positionParent.getChild(positionOffset - 1);
|
|
489
|
-
const nodeAfter = positionParent.getChild(positionOffset);
|
|
490
|
-
// Position should be placed between two nodes.
|
|
491
|
-
if (!nodeBefore || !nodeAfter) {
|
|
492
|
-
return position;
|
|
493
|
-
}
|
|
494
|
-
// When position is between two text nodes.
|
|
495
|
-
if (nodeBefore.is('$text') && nodeAfter.is('$text')) {
|
|
496
|
-
return mergeTextNodes(nodeBefore, nodeAfter);
|
|
497
|
-
}
|
|
498
|
-
// When position is between two same attribute elements.
|
|
499
|
-
else if (nodeBefore.is('attributeElement') && nodeAfter.is('attributeElement') && nodeBefore.isSimilar(nodeAfter)) {
|
|
500
|
-
// Move all children nodes from node placed after selection and remove that node.
|
|
501
|
-
const count = nodeBefore.childCount;
|
|
502
|
-
nodeBefore._appendChild(nodeAfter.getChildren());
|
|
503
|
-
nodeAfter._remove();
|
|
504
|
-
this._removeFromClonedElementsGroup(nodeAfter);
|
|
505
|
-
// New position is located inside the first node, before new nodes.
|
|
506
|
-
// Call this method recursively to merge again if needed.
|
|
507
|
-
return this.mergeAttributes(new ViewPosition(nodeBefore, count));
|
|
508
|
-
}
|
|
509
|
-
return position;
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Merges two {@link module:engine/view/containerelement~ViewContainerElement container elements} that are
|
|
513
|
-
* before and after given position. Precisely, the element after the position is removed and it's contents are
|
|
514
|
-
* moved to element before the position.
|
|
515
|
-
*
|
|
516
|
-
* ```html
|
|
517
|
-
* <p>foo</p>^<p>bar</p> -> <p>foo^bar</p>
|
|
518
|
-
* <div>foo</div>^<p>bar</p> -> <div>foo^bar</div>
|
|
519
|
-
* ```
|
|
520
|
-
*
|
|
521
|
-
* **Note:** Difference between {@link module:engine/view/downcastwriter~ViewDowncastWriter#mergeAttributes mergeAttributes} and
|
|
522
|
-
* {@link module:engine/view/downcastwriter~ViewDowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
|
|
523
|
-
* {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements} or
|
|
524
|
-
* {@link module:engine/view/text~ViewText text nodes} while `mergeContainer` merges two
|
|
525
|
-
* {@link module:engine/view/containerelement~ViewContainerElement container elements}.
|
|
526
|
-
*
|
|
527
|
-
* @see module:engine/view/attributeelement~ViewAttributeElement
|
|
528
|
-
* @see module:engine/view/containerelement~ViewContainerElement
|
|
529
|
-
* @see module:engine/view/downcastwriter~ViewDowncastWriter#mergeAttributes
|
|
530
|
-
* @param position Merge position.
|
|
531
|
-
* @returns Position after merge.
|
|
532
|
-
*/
|
|
533
|
-
mergeContainers(position) {
|
|
534
|
-
const prev = position.nodeBefore;
|
|
535
|
-
const next = position.nodeAfter;
|
|
536
|
-
if (!prev || !next || !prev.is('containerElement') || !next.is('containerElement')) {
|
|
537
|
-
/**
|
|
538
|
-
* Element before and after given position cannot be merged.
|
|
539
|
-
*
|
|
540
|
-
* @error view-writer-merge-containers-invalid-position
|
|
541
|
-
*/
|
|
542
|
-
throw new CKEditorError('view-writer-merge-containers-invalid-position', this.document);
|
|
543
|
-
}
|
|
544
|
-
const lastChild = prev.getChild(prev.childCount - 1);
|
|
545
|
-
const newPosition = lastChild instanceof ViewText ?
|
|
546
|
-
ViewPosition._createAt(lastChild, 'end') :
|
|
547
|
-
ViewPosition._createAt(prev, 'end');
|
|
548
|
-
this.move(ViewRange._createIn(next), ViewPosition._createAt(prev, 'end'));
|
|
549
|
-
this.remove(ViewRange._createOn(next));
|
|
550
|
-
return newPosition;
|
|
551
|
-
}
|
|
552
|
-
/**
|
|
553
|
-
* Inserts a node or nodes at specified position. Takes care about breaking attributes before insertion
|
|
554
|
-
* and merging them afterwards.
|
|
555
|
-
*
|
|
556
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
|
|
557
|
-
* contains instances that are not {@link module:engine/view/text~ViewText Texts},
|
|
558
|
-
* {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElements},
|
|
559
|
-
* {@link module:engine/view/containerelement~ViewContainerElement ViewContainerElements},
|
|
560
|
-
* {@link module:engine/view/emptyelement~ViewEmptyElement ViewEmptyElements},
|
|
561
|
-
* {@link module:engine/view/rawelement~ViewRawElement RawElements} or
|
|
562
|
-
* {@link module:engine/view/uielement~ViewUIElement UIElements}.
|
|
563
|
-
*
|
|
564
|
-
* @param position Insertion position.
|
|
565
|
-
* @param nodes Node or nodes to insert.
|
|
566
|
-
* @returns Range around inserted nodes.
|
|
567
|
-
*/
|
|
568
|
-
insert(position, nodes) {
|
|
569
|
-
nodes = isIterable(nodes) ? [...nodes] : [nodes];
|
|
570
|
-
// Check if nodes to insert are instances of ViewAttributeElements, ViewContainerElements, ViewEmptyElements, UIElements or Text.
|
|
571
|
-
validateNodesToInsert(nodes, this.document);
|
|
572
|
-
// Group nodes in batches of nodes that require or do not require breaking an ViewAttributeElements.
|
|
573
|
-
const nodeGroups = nodes.reduce((groups, node) => {
|
|
574
|
-
const lastGroup = groups[groups.length - 1];
|
|
575
|
-
// Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
|
|
576
|
-
// can't have an attribute in model and won't get wrapped with an ViewAttributeElement while down-casted.
|
|
577
|
-
const breakAttributes = !node.is('uiElement');
|
|
578
|
-
if (!lastGroup || lastGroup.breakAttributes != breakAttributes) {
|
|
579
|
-
groups.push({
|
|
580
|
-
breakAttributes,
|
|
581
|
-
nodes: [node]
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
else {
|
|
585
|
-
lastGroup.nodes.push(node);
|
|
586
|
-
}
|
|
587
|
-
return groups;
|
|
588
|
-
}, []);
|
|
589
|
-
// Insert nodes in batches.
|
|
590
|
-
let start = null;
|
|
591
|
-
let end = position;
|
|
592
|
-
for (const { nodes, breakAttributes } of nodeGroups) {
|
|
593
|
-
const range = this._insertNodes(end, nodes, breakAttributes);
|
|
594
|
-
if (!start) {
|
|
595
|
-
start = range.start;
|
|
596
|
-
}
|
|
597
|
-
end = range.end;
|
|
598
|
-
}
|
|
599
|
-
// When no nodes were inserted - return collapsed range.
|
|
600
|
-
if (!start) {
|
|
601
|
-
return new ViewRange(position);
|
|
602
|
-
}
|
|
603
|
-
return new ViewRange(start, end);
|
|
604
|
-
}
|
|
605
|
-
/**
|
|
606
|
-
* Removes provided range from the container.
|
|
607
|
-
*
|
|
608
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
|
|
609
|
-
* {@link module:engine/view/range~ViewRange#start start} and {@link module:engine/view/range~ViewRange#end end}
|
|
610
|
-
* positions are not placed inside same parent container.
|
|
611
|
-
*
|
|
612
|
-
* @param rangeOrItem Range to remove from container
|
|
613
|
-
* or an {@link module:engine/view/item~ViewItem item} to remove. If range is provided, after removing, it will be updated
|
|
614
|
-
* to a collapsed range showing the new position.
|
|
615
|
-
* @returns Document fragment containing removed nodes.
|
|
616
|
-
*/
|
|
617
|
-
remove(rangeOrItem) {
|
|
618
|
-
const range = rangeOrItem instanceof ViewRange ? rangeOrItem : ViewRange._createOn(rangeOrItem);
|
|
619
|
-
validateRangeContainer(range, this.document);
|
|
620
|
-
// If range is collapsed - nothing to remove.
|
|
621
|
-
if (range.isCollapsed) {
|
|
622
|
-
return new ViewDocumentFragment(this.document);
|
|
623
|
-
}
|
|
624
|
-
// Break attributes at range start and end.
|
|
625
|
-
const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
|
|
626
|
-
const parentContainer = breakStart.parent;
|
|
627
|
-
const count = breakEnd.offset - breakStart.offset;
|
|
628
|
-
// Remove nodes in range.
|
|
629
|
-
const removed = parentContainer._removeChildren(breakStart.offset, count);
|
|
630
|
-
for (const node of removed) {
|
|
631
|
-
this._removeFromClonedElementsGroup(node);
|
|
632
|
-
}
|
|
633
|
-
// Merge after removing.
|
|
634
|
-
const mergePosition = this.mergeAttributes(breakStart);
|
|
635
|
-
range.start = mergePosition;
|
|
636
|
-
range.end = mergePosition.clone();
|
|
637
|
-
// Return removed nodes.
|
|
638
|
-
return new ViewDocumentFragment(this.document, removed);
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* Removes matching elements from given range.
|
|
642
|
-
*
|
|
643
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
|
|
644
|
-
* {@link module:engine/view/range~ViewRange#start start} and {@link module:engine/view/range~ViewRange#end end}
|
|
645
|
-
* positions are not placed inside same parent container.
|
|
646
|
-
*
|
|
647
|
-
* @param range Range to clear.
|
|
648
|
-
* @param element Element to remove.
|
|
649
|
-
*/
|
|
650
|
-
clear(range, element) {
|
|
651
|
-
validateRangeContainer(range, this.document);
|
|
652
|
-
// Create walker on given range.
|
|
653
|
-
// We walk backward because when we remove element during walk it modifies range end position.
|
|
654
|
-
const walker = range.getWalker({
|
|
655
|
-
direction: 'backward',
|
|
656
|
-
ignoreElementEnd: true
|
|
657
|
-
});
|
|
658
|
-
// Let's walk.
|
|
659
|
-
for (const current of walker) {
|
|
660
|
-
const item = current.item;
|
|
661
|
-
let rangeToRemove;
|
|
662
|
-
// When current item matches to the given element.
|
|
663
|
-
if (item.is('element') && element.isSimilar(item)) {
|
|
664
|
-
// Create range on this element.
|
|
665
|
-
rangeToRemove = ViewRange._createOn(item);
|
|
666
|
-
// When range starts inside Text or TextProxy element.
|
|
667
|
-
}
|
|
668
|
-
else if (!current.nextPosition.isAfter(range.start) && item.is('$textProxy')) {
|
|
669
|
-
// We need to check if parent of this text matches to given element.
|
|
670
|
-
const parentElement = item.getAncestors().find(ancestor => {
|
|
671
|
-
return ancestor.is('element') && element.isSimilar(ancestor);
|
|
672
|
-
});
|
|
673
|
-
// If it is then create range inside this element.
|
|
674
|
-
if (parentElement) {
|
|
675
|
-
rangeToRemove = ViewRange._createIn(parentElement);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
// If we have found element to remove.
|
|
679
|
-
if (rangeToRemove) {
|
|
680
|
-
// We need to check if element range stick out of the given range and truncate if it is.
|
|
681
|
-
if (rangeToRemove.end.isAfter(range.end)) {
|
|
682
|
-
rangeToRemove.end = range.end;
|
|
683
|
-
}
|
|
684
|
-
if (rangeToRemove.start.isBefore(range.start)) {
|
|
685
|
-
rangeToRemove.start = range.start;
|
|
686
|
-
}
|
|
687
|
-
// At the end we remove range with found element.
|
|
688
|
-
this.remove(rangeToRemove);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Moves nodes from provided range to target position.
|
|
694
|
-
*
|
|
695
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
|
|
696
|
-
* {@link module:engine/view/range~ViewRange#start start} and {@link module:engine/view/range~ViewRange#end end}
|
|
697
|
-
* positions are not placed inside same parent container.
|
|
698
|
-
*
|
|
699
|
-
* @param sourceRange Range containing nodes to move.
|
|
700
|
-
* @param targetPosition Position to insert.
|
|
701
|
-
* @returns Range in target container. Inserted nodes are placed between
|
|
702
|
-
* {@link module:engine/view/range~ViewRange#start start} and {@link module:engine/view/range~ViewRange#end end} positions.
|
|
703
|
-
*/
|
|
704
|
-
move(sourceRange, targetPosition) {
|
|
705
|
-
let nodes;
|
|
706
|
-
if (targetPosition.isAfter(sourceRange.end)) {
|
|
707
|
-
targetPosition = this._breakAttributes(targetPosition, true);
|
|
708
|
-
const parent = targetPosition.parent;
|
|
709
|
-
const countBefore = parent.childCount;
|
|
710
|
-
sourceRange = this._breakAttributesRange(sourceRange, true);
|
|
711
|
-
nodes = this.remove(sourceRange);
|
|
712
|
-
targetPosition.offset += (parent.childCount - countBefore);
|
|
713
|
-
}
|
|
714
|
-
else {
|
|
715
|
-
nodes = this.remove(sourceRange);
|
|
716
|
-
}
|
|
717
|
-
return this.insert(targetPosition, nodes);
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Wraps elements within range with provided {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement}.
|
|
721
|
-
* If a collapsed range is provided, it will be wrapped only if it is equal to view selection.
|
|
722
|
-
*
|
|
723
|
-
* If a collapsed range was passed and is same as selection, the selection
|
|
724
|
-
* will be moved to the inside of the wrapped attribute element.
|
|
725
|
-
*
|
|
726
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
|
|
727
|
-
* when {@link module:engine/view/range~ViewRange#start}
|
|
728
|
-
* and {@link module:engine/view/range~ViewRange#end} positions are not placed inside same parent container.
|
|
729
|
-
*
|
|
730
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
|
|
731
|
-
* an instance of {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement}.
|
|
732
|
-
*
|
|
733
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
|
|
734
|
-
* is collapsed and different than view selection.
|
|
735
|
-
*
|
|
736
|
-
* @param range Range to wrap.
|
|
737
|
-
* @param attribute Attribute element to use as wrapper.
|
|
738
|
-
* @returns range Range after wrapping, spanning over wrapping attribute element.
|
|
739
|
-
*/
|
|
740
|
-
wrap(range, attribute) {
|
|
741
|
-
if (!(attribute instanceof ViewAttributeElement)) {
|
|
742
|
-
throw new CKEditorError('view-writer-wrap-invalid-attribute', this.document);
|
|
743
|
-
}
|
|
744
|
-
validateRangeContainer(range, this.document);
|
|
745
|
-
if (!range.isCollapsed) {
|
|
746
|
-
// Non-collapsed range. Wrap it with the attribute element.
|
|
747
|
-
return this._wrapRange(range, attribute);
|
|
748
|
-
}
|
|
749
|
-
else {
|
|
750
|
-
// Collapsed range. Wrap position.
|
|
751
|
-
let position = range.start;
|
|
752
|
-
if (position.parent.is('element') && !_hasNonUiChildren(position.parent)) {
|
|
753
|
-
position = position.getLastMatchingPosition(value => value.item.is('uiElement'));
|
|
754
|
-
}
|
|
755
|
-
position = this._wrapPosition(position, attribute);
|
|
756
|
-
const viewSelection = this.document.selection;
|
|
757
|
-
// If wrapping position is equal to view selection, move view selection inside wrapping attribute element.
|
|
758
|
-
if (viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual(range.start)) {
|
|
759
|
-
this.setSelection(position);
|
|
760
|
-
}
|
|
761
|
-
return new ViewRange(position);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* Unwraps nodes within provided range from attribute element.
|
|
766
|
-
*
|
|
767
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
|
|
768
|
-
* {@link module:engine/view/range~ViewRange#start start} and {@link module:engine/view/range~ViewRange#end end}
|
|
769
|
-
* positions are not placed inside same parent container.
|
|
770
|
-
*/
|
|
771
|
-
unwrap(range, attribute) {
|
|
772
|
-
if (!(attribute instanceof ViewAttributeElement)) {
|
|
773
|
-
/**
|
|
774
|
-
* The `attribute` passed to {@link module:engine/view/downcastwriter~ViewDowncastWriter#unwrap `ViewDowncastWriter#unwrap()`}
|
|
775
|
-
* must be an instance of {@link module:engine/view/attributeelement~ViewAttributeElement `AttributeElement`}.
|
|
776
|
-
*
|
|
777
|
-
* @error view-writer-unwrap-invalid-attribute
|
|
778
|
-
*/
|
|
779
|
-
throw new CKEditorError('view-writer-unwrap-invalid-attribute', this.document);
|
|
780
|
-
}
|
|
781
|
-
validateRangeContainer(range, this.document);
|
|
782
|
-
// If range is collapsed - nothing to unwrap.
|
|
783
|
-
if (range.isCollapsed) {
|
|
784
|
-
return range;
|
|
785
|
-
}
|
|
786
|
-
// Break attributes at range start and end.
|
|
787
|
-
const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
|
|
788
|
-
const parentContainer = breakStart.parent;
|
|
789
|
-
// Unwrap children located between break points.
|
|
790
|
-
const newRange = this._unwrapChildren(parentContainer, breakStart.offset, breakEnd.offset, attribute);
|
|
791
|
-
// Merge attributes at the both ends and return a new range.
|
|
792
|
-
const start = this.mergeAttributes(newRange.start);
|
|
793
|
-
// If start position was merged - move end position back.
|
|
794
|
-
if (!start.isEqual(newRange.start)) {
|
|
795
|
-
newRange.end.offset--;
|
|
796
|
-
}
|
|
797
|
-
const end = this.mergeAttributes(newRange.end);
|
|
798
|
-
return new ViewRange(start, end);
|
|
799
|
-
}
|
|
800
|
-
/**
|
|
801
|
-
* Renames element by creating a copy of renamed element but with changed name and then moving contents of the
|
|
802
|
-
* old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~ViewPosition positions}
|
|
803
|
-
* which has renamed element as {@link module:engine/view/position~ViewPosition#parent a parent}.
|
|
804
|
-
*
|
|
805
|
-
* New element has to be created because `Element#tagName` property in DOM is readonly.
|
|
806
|
-
*
|
|
807
|
-
* Since this function creates a new element and removes the given one, the new element is returned to keep reference.
|
|
808
|
-
*
|
|
809
|
-
* @param newName New name for element.
|
|
810
|
-
* @param viewElement Element to be renamed.
|
|
811
|
-
* @returns Element created due to rename.
|
|
812
|
-
*/
|
|
813
|
-
rename(newName, viewElement) {
|
|
814
|
-
const newElement = new ViewContainerElement(this.document, newName, viewElement.getAttributes());
|
|
815
|
-
this.insert(ViewPosition._createAfter(viewElement), newElement);
|
|
816
|
-
this.move(ViewRange._createIn(viewElement), ViewPosition._createAt(newElement, 0));
|
|
817
|
-
this.remove(ViewRange._createOn(viewElement));
|
|
818
|
-
return newElement;
|
|
819
|
-
}
|
|
820
|
-
/**
|
|
821
|
-
* Cleans up memory by removing obsolete cloned elements group from the writer.
|
|
822
|
-
*
|
|
823
|
-
* Should be used whenever all {@link module:engine/view/attributeelement~ViewAttributeElement attribute elements}
|
|
824
|
-
* with the same {@link module:engine/view/attributeelement~ViewAttributeElement#id id} are going to be removed from the view and
|
|
825
|
-
* the group will no longer be needed.
|
|
826
|
-
*
|
|
827
|
-
* Cloned elements group are not removed automatically in case if the group is still needed after all its elements
|
|
828
|
-
* were removed from the view.
|
|
829
|
-
*
|
|
830
|
-
* Keep in mind that group names are equal to the `id` property of the attribute element.
|
|
831
|
-
*
|
|
832
|
-
* @param groupName Name of the group to clear.
|
|
833
|
-
*/
|
|
834
|
-
clearClonedElementsGroup(groupName) {
|
|
835
|
-
this._cloneGroups.delete(groupName);
|
|
836
|
-
}
|
|
837
|
-
/**
|
|
838
|
-
* Creates position at the given location. The location can be specified as:
|
|
839
|
-
*
|
|
840
|
-
* * a {@link module:engine/view/position~ViewPosition position},
|
|
841
|
-
* * parent element and offset (offset defaults to `0`),
|
|
842
|
-
* * parent element and `'end'` (sets position at the end of that element),
|
|
843
|
-
* * {@link module:engine/view/item~ViewItem view item} and `'before'` or `'after'` (sets position before or after given view item).
|
|
844
|
-
*
|
|
845
|
-
* This method is a shortcut to other constructors such as:
|
|
846
|
-
*
|
|
847
|
-
* * {@link #createPositionBefore},
|
|
848
|
-
* * {@link #createPositionAfter},
|
|
849
|
-
*
|
|
850
|
-
* @param offset Offset or one of the flags. Used only when the first parameter is a {@link module:engine/view/item~ViewItem view item}.
|
|
851
|
-
*/
|
|
852
|
-
createPositionAt(itemOrPosition, offset) {
|
|
853
|
-
return ViewPosition._createAt(itemOrPosition, offset);
|
|
854
|
-
}
|
|
855
|
-
/**
|
|
856
|
-
* Creates a new position after given view item.
|
|
857
|
-
*
|
|
858
|
-
* @param item View item after which the position should be located.
|
|
859
|
-
*/
|
|
860
|
-
createPositionAfter(item) {
|
|
861
|
-
return ViewPosition._createAfter(item);
|
|
862
|
-
}
|
|
863
|
-
/**
|
|
864
|
-
* Creates a new position before given view item.
|
|
865
|
-
*
|
|
866
|
-
* @param item View item before which the position should be located.
|
|
867
|
-
*/
|
|
868
|
-
createPositionBefore(item) {
|
|
869
|
-
return ViewPosition._createBefore(item);
|
|
870
|
-
}
|
|
871
|
-
/**
|
|
872
|
-
* Creates a range spanning from `start` position to `end` position.
|
|
873
|
-
*
|
|
874
|
-
* **Note:** This factory method creates its own {@link module:engine/view/position~ViewPosition} instances basing on passed values.
|
|
875
|
-
*
|
|
876
|
-
* @param start Start position.
|
|
877
|
-
* @param end End position. If not set, range will be collapsed at `start` position.
|
|
878
|
-
*/
|
|
879
|
-
createRange(start, end) {
|
|
880
|
-
return new ViewRange(start, end);
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Creates a range that starts before given {@link module:engine/view/item~ViewItem view item} and ends after it.
|
|
884
|
-
*/
|
|
885
|
-
createRangeOn(item) {
|
|
886
|
-
return ViewRange._createOn(item);
|
|
887
|
-
}
|
|
888
|
-
/**
|
|
889
|
-
* Creates a range inside an {@link module:engine/view/element~ViewElement element} which starts before the first child of
|
|
890
|
-
* that element and ends after the last child of that element.
|
|
891
|
-
*
|
|
892
|
-
* @param element Element which is a parent for the range.
|
|
893
|
-
*/
|
|
894
|
-
createRangeIn(element) {
|
|
895
|
-
return ViewRange._createIn(element);
|
|
896
|
-
}
|
|
897
|
-
createSelection(...args) {
|
|
898
|
-
return new ViewSelection(...args);
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* Creates placeholders for child elements of the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
|
|
902
|
-
* `elementToStructure()`} conversion helper.
|
|
903
|
-
*
|
|
904
|
-
* ```ts
|
|
905
|
-
* const viewSlot = conversionApi.writer.createSlot();
|
|
906
|
-
* const viewPosition = conversionApi.writer.createPositionAt( viewElement, 0 );
|
|
907
|
-
*
|
|
908
|
-
* conversionApi.writer.insert( viewPosition, viewSlot );
|
|
909
|
-
* ```
|
|
910
|
-
*
|
|
911
|
-
* It could be filtered down to a specific subset of children (only `<foo>` model elements in this case):
|
|
912
|
-
*
|
|
913
|
-
* ```ts
|
|
914
|
-
* const viewSlot = conversionApi.writer.createSlot( node => node.is( 'element', 'foo' ) );
|
|
915
|
-
* const viewPosition = conversionApi.writer.createPositionAt( viewElement, 0 );
|
|
916
|
-
*
|
|
917
|
-
* conversionApi.writer.insert( viewPosition, viewSlot );
|
|
918
|
-
* ```
|
|
919
|
-
*
|
|
920
|
-
* While providing a filtered slot, make sure to provide slots for all child nodes. A single node cannot be downcasted into
|
|
921
|
-
* multiple slots.
|
|
922
|
-
*
|
|
923
|
-
* **Note**: You should not change the order of nodes. View elements should be in the same order as model nodes.
|
|
924
|
-
*
|
|
925
|
-
* @param modeOrFilter The filter for child nodes.
|
|
926
|
-
* @returns The slot element to be placed in to the view structure while processing
|
|
927
|
-
* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure `elementToStructure()`}.
|
|
928
|
-
*/
|
|
929
|
-
createSlot(modeOrFilter = 'children') {
|
|
930
|
-
if (!this._slotFactory) {
|
|
931
|
-
/**
|
|
932
|
-
* The `createSlot()` method is only allowed inside the `elementToStructure` downcast helper callback.
|
|
933
|
-
*
|
|
934
|
-
* @error view-writer-invalid-create-slot-context
|
|
935
|
-
*/
|
|
936
|
-
throw new CKEditorError('view-writer-invalid-create-slot-context', this.document);
|
|
937
|
-
}
|
|
938
|
-
return this._slotFactory(this, modeOrFilter);
|
|
939
|
-
}
|
|
940
|
-
/**
|
|
941
|
-
* Registers a slot factory.
|
|
942
|
-
*
|
|
943
|
-
* @internal
|
|
944
|
-
* @param slotFactory The slot factory.
|
|
945
|
-
*/
|
|
946
|
-
_registerSlotFactory(slotFactory) {
|
|
947
|
-
this._slotFactory = slotFactory;
|
|
948
|
-
}
|
|
949
|
-
/**
|
|
950
|
-
* Clears the registered slot factory.
|
|
951
|
-
*
|
|
952
|
-
* @internal
|
|
953
|
-
*/
|
|
954
|
-
_clearSlotFactory() {
|
|
955
|
-
this._slotFactory = null;
|
|
956
|
-
}
|
|
957
|
-
/**
|
|
958
|
-
* Inserts a node or nodes at the specified position. Takes care of breaking attributes before insertion
|
|
959
|
-
* and merging them afterwards if requested by the breakAttributes param.
|
|
960
|
-
*
|
|
961
|
-
* @param position Insertion position.
|
|
962
|
-
* @param nodes Node or nodes to insert.
|
|
963
|
-
* @param breakAttributes Whether attributes should be broken.
|
|
964
|
-
* @returns Range around inserted nodes.
|
|
965
|
-
*/
|
|
966
|
-
_insertNodes(position, nodes, breakAttributes) {
|
|
967
|
-
let parentElement;
|
|
968
|
-
// Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
|
|
969
|
-
// can't have an attribute in model and won't get wrapped with an ViewAttributeElement while down-casted.
|
|
970
|
-
if (breakAttributes) {
|
|
971
|
-
parentElement = getParentContainer(position);
|
|
972
|
-
}
|
|
973
|
-
else {
|
|
974
|
-
parentElement = position.parent.is('$text') ? position.parent.parent : position.parent;
|
|
975
|
-
}
|
|
976
|
-
if (!parentElement) {
|
|
977
|
-
/**
|
|
978
|
-
* Position's parent container cannot be found.
|
|
979
|
-
*
|
|
980
|
-
* @error view-writer-invalid-position-container
|
|
981
|
-
*/
|
|
982
|
-
throw new CKEditorError('view-writer-invalid-position-container', this.document);
|
|
983
|
-
}
|
|
984
|
-
let insertionPosition;
|
|
985
|
-
if (breakAttributes) {
|
|
986
|
-
insertionPosition = this._breakAttributes(position, true);
|
|
987
|
-
}
|
|
988
|
-
else {
|
|
989
|
-
insertionPosition = position.parent.is('$text') ? breakTextNode(position) : position;
|
|
990
|
-
}
|
|
991
|
-
const length = parentElement._insertChild(insertionPosition.offset, nodes);
|
|
992
|
-
for (const node of nodes) {
|
|
993
|
-
this._addToClonedElementsGroup(node);
|
|
994
|
-
}
|
|
995
|
-
const endPosition = insertionPosition.getShiftedBy(length);
|
|
996
|
-
const start = this.mergeAttributes(insertionPosition);
|
|
997
|
-
// If start position was merged - move end position.
|
|
998
|
-
if (!start.isEqual(insertionPosition)) {
|
|
999
|
-
endPosition.offset--;
|
|
1000
|
-
}
|
|
1001
|
-
const end = this.mergeAttributes(endPosition);
|
|
1002
|
-
return new ViewRange(start, end);
|
|
1003
|
-
}
|
|
1004
|
-
/**
|
|
1005
|
-
* Wraps children with provided `wrapElement`. Only children contained in `parent` element between
|
|
1006
|
-
* `startOffset` and `endOffset` will be wrapped.
|
|
1007
|
-
*/
|
|
1008
|
-
_wrapChildren(parent, startOffset, endOffset, wrapElement) {
|
|
1009
|
-
let i = startOffset;
|
|
1010
|
-
const wrapPositions = [];
|
|
1011
|
-
while (i < endOffset) {
|
|
1012
|
-
const child = parent.getChild(i);
|
|
1013
|
-
const isText = child.is('$text');
|
|
1014
|
-
const isAttribute = child.is('attributeElement');
|
|
1015
|
-
//
|
|
1016
|
-
// (In all examples, assume that `wrapElement` is `<span class="foo">` element.)
|
|
1017
|
-
//
|
|
1018
|
-
// Check if `wrapElement` can be joined with the wrapped element. One of requirements is having same name.
|
|
1019
|
-
// If possible, join elements.
|
|
1020
|
-
//
|
|
1021
|
-
// <p><span class="bar">abc</span></p> --> <p><span class="foo bar">abc</span></p>
|
|
1022
|
-
//
|
|
1023
|
-
if (isAttribute && child._canMergeAttributesFrom(wrapElement)) {
|
|
1024
|
-
child._mergeAttributesFrom(wrapElement);
|
|
1025
|
-
wrapPositions.push(new ViewPosition(parent, i));
|
|
1026
|
-
}
|
|
1027
|
-
//
|
|
1028
|
-
// Wrap the child if it is not an attribute element or if it is an attribute element that should be inside
|
|
1029
|
-
// `wrapElement` (due to priority).
|
|
1030
|
-
//
|
|
1031
|
-
// <p>abc</p> --> <p><span class="foo">abc</span></p>
|
|
1032
|
-
// <p><strong>abc</strong></p> --> <p><span class="foo"><strong>abc</strong></span></p>
|
|
1033
|
-
else if (isText || !isAttribute || shouldABeOutsideB(wrapElement, child)) {
|
|
1034
|
-
// Clone attribute.
|
|
1035
|
-
const newAttribute = wrapElement._clone();
|
|
1036
|
-
// Wrap current node with new attribute.
|
|
1037
|
-
child._remove();
|
|
1038
|
-
newAttribute._appendChild(child);
|
|
1039
|
-
parent._insertChild(i, newAttribute);
|
|
1040
|
-
this._addToClonedElementsGroup(newAttribute);
|
|
1041
|
-
wrapPositions.push(new ViewPosition(parent, i));
|
|
1042
|
-
}
|
|
1043
|
-
//
|
|
1044
|
-
// If other nested attribute is found and it wasn't wrapped (see above), continue wrapping inside it.
|
|
1045
|
-
//
|
|
1046
|
-
// <p><a href="foo.html">abc</a></p> --> <p><a href="foo.html"><span class="foo">abc</span></a></p>
|
|
1047
|
-
//
|
|
1048
|
-
else /* if ( isAttribute ) */ {
|
|
1049
|
-
this._wrapChildren(child, 0, child.childCount, wrapElement);
|
|
1050
|
-
}
|
|
1051
|
-
i++;
|
|
1052
|
-
}
|
|
1053
|
-
// Merge at each wrap.
|
|
1054
|
-
let offsetChange = 0;
|
|
1055
|
-
for (const position of wrapPositions) {
|
|
1056
|
-
position.offset -= offsetChange;
|
|
1057
|
-
// Do not merge with elements outside selected children.
|
|
1058
|
-
if (position.offset == startOffset) {
|
|
1059
|
-
continue;
|
|
1060
|
-
}
|
|
1061
|
-
const newPosition = this.mergeAttributes(position);
|
|
1062
|
-
// If nodes were merged - other merge offsets will change.
|
|
1063
|
-
if (!newPosition.isEqual(position)) {
|
|
1064
|
-
offsetChange++;
|
|
1065
|
-
endOffset--;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
return ViewRange._createFromParentsAndOffsets(parent, startOffset, parent, endOffset);
|
|
1069
|
-
}
|
|
1070
|
-
/**
|
|
1071
|
-
* Unwraps children from provided `unwrapElement`. Only children contained in `parent` element between
|
|
1072
|
-
* `startOffset` and `endOffset` will be unwrapped.
|
|
1073
|
-
*/
|
|
1074
|
-
_unwrapChildren(parent, startOffset, endOffset, unwrapElement) {
|
|
1075
|
-
let i = startOffset;
|
|
1076
|
-
const unwrapPositions = [];
|
|
1077
|
-
// Iterate over each element between provided offsets inside parent.
|
|
1078
|
-
// We don't use tree walker or range iterator because we will be removing and merging potentially multiple nodes,
|
|
1079
|
-
// so it could get messy. It is safer to it manually in this case.
|
|
1080
|
-
while (i < endOffset) {
|
|
1081
|
-
const child = parent.getChild(i);
|
|
1082
|
-
// Skip all text nodes. There should be no container element's here either.
|
|
1083
|
-
if (!child.is('attributeElement')) {
|
|
1084
|
-
i++;
|
|
1085
|
-
continue;
|
|
1086
|
-
}
|
|
1087
|
-
//
|
|
1088
|
-
// (In all examples, assume that `unwrapElement` is `<span class="foo">` element.)
|
|
1089
|
-
//
|
|
1090
|
-
// If the child is similar to the given attribute element, unwrap it - it will be completely removed.
|
|
1091
|
-
//
|
|
1092
|
-
// <p><span class="foo">abc</span>xyz</p> --> <p>abcxyz</p>
|
|
1093
|
-
//
|
|
1094
|
-
if (child.isSimilar(unwrapElement)) {
|
|
1095
|
-
const unwrapped = child.getChildren();
|
|
1096
|
-
const count = child.childCount;
|
|
1097
|
-
// Replace wrapper element with its children
|
|
1098
|
-
child._remove();
|
|
1099
|
-
parent._insertChild(i, unwrapped);
|
|
1100
|
-
this._removeFromClonedElementsGroup(child);
|
|
1101
|
-
// Save start and end position of moved items.
|
|
1102
|
-
unwrapPositions.push(new ViewPosition(parent, i), new ViewPosition(parent, i + count));
|
|
1103
|
-
// Skip elements that were unwrapped. Assuming there won't be another element to unwrap in child elements.
|
|
1104
|
-
i += count;
|
|
1105
|
-
endOffset += count - 1;
|
|
1106
|
-
continue;
|
|
1107
|
-
}
|
|
1108
|
-
//
|
|
1109
|
-
// If the child is not similar but is an attribute element, try partial unwrapping - remove the same attributes/styles/classes.
|
|
1110
|
-
// Partial unwrapping will happen only if the elements have the same name.
|
|
1111
|
-
//
|
|
1112
|
-
// <p><span class="foo bar">abc</span>xyz</p> --> <p><span class="bar">abc</span>xyz</p>
|
|
1113
|
-
// <p><i class="foo">abc</i>xyz</p> --> <p><i class="foo">abc</i>xyz</p>
|
|
1114
|
-
//
|
|
1115
|
-
if (child._canSubtractAttributesOf(unwrapElement)) {
|
|
1116
|
-
child._subtractAttributesOf(unwrapElement);
|
|
1117
|
-
unwrapPositions.push(new ViewPosition(parent, i), new ViewPosition(parent, i + 1));
|
|
1118
|
-
i++;
|
|
1119
|
-
continue;
|
|
1120
|
-
}
|
|
1121
|
-
//
|
|
1122
|
-
// If other nested attribute is found, look through it's children for elements to unwrap.
|
|
1123
|
-
//
|
|
1124
|
-
// <p><i><span class="foo">abc</span></i><p> --> <p><i>abc</i><p>
|
|
1125
|
-
//
|
|
1126
|
-
this._unwrapChildren(child, 0, child.childCount, unwrapElement);
|
|
1127
|
-
i++;
|
|
1128
|
-
}
|
|
1129
|
-
// Merge at each unwrap.
|
|
1130
|
-
let offsetChange = 0;
|
|
1131
|
-
for (const position of unwrapPositions) {
|
|
1132
|
-
position.offset -= offsetChange;
|
|
1133
|
-
// Do not merge with elements outside selected children.
|
|
1134
|
-
if (position.offset == startOffset || position.offset == endOffset) {
|
|
1135
|
-
continue;
|
|
1136
|
-
}
|
|
1137
|
-
const newPosition = this.mergeAttributes(position);
|
|
1138
|
-
// If nodes were merged - other merge offsets will change.
|
|
1139
|
-
if (!newPosition.isEqual(position)) {
|
|
1140
|
-
offsetChange++;
|
|
1141
|
-
endOffset--;
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
return ViewRange._createFromParentsAndOffsets(parent, startOffset, parent, endOffset);
|
|
1145
|
-
}
|
|
1146
|
-
/**
|
|
1147
|
-
* Helper function for `view.writer.wrap`. Wraps range with provided attribute element.
|
|
1148
|
-
* This method will also merge newly added attribute element with its siblings whenever possible.
|
|
1149
|
-
*
|
|
1150
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
|
|
1151
|
-
* an instance of {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement}.
|
|
1152
|
-
*
|
|
1153
|
-
* @returns New range after wrapping, spanning over wrapping attribute element.
|
|
1154
|
-
*/
|
|
1155
|
-
_wrapRange(range, attribute) {
|
|
1156
|
-
// Break attributes at range start and end.
|
|
1157
|
-
const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
|
|
1158
|
-
const parentContainer = breakStart.parent;
|
|
1159
|
-
// Wrap all children with attribute.
|
|
1160
|
-
const newRange = this._wrapChildren(parentContainer, breakStart.offset, breakEnd.offset, attribute);
|
|
1161
|
-
// Merge attributes at the both ends and return a new range.
|
|
1162
|
-
const start = this.mergeAttributes(newRange.start);
|
|
1163
|
-
// If start position was merged - move end position back.
|
|
1164
|
-
if (!start.isEqual(newRange.start)) {
|
|
1165
|
-
newRange.end.offset--;
|
|
1166
|
-
}
|
|
1167
|
-
const end = this.mergeAttributes(newRange.end);
|
|
1168
|
-
return new ViewRange(start, end);
|
|
1169
|
-
}
|
|
1170
|
-
/**
|
|
1171
|
-
* Helper function for {@link #wrap}. Wraps position with provided attribute element.
|
|
1172
|
-
* This method will also merge newly added attribute element with its siblings whenever possible.
|
|
1173
|
-
*
|
|
1174
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
|
|
1175
|
-
* an instance of {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement}.
|
|
1176
|
-
*
|
|
1177
|
-
* @returns New position after wrapping.
|
|
1178
|
-
*/
|
|
1179
|
-
_wrapPosition(position, attribute) {
|
|
1180
|
-
// Return same position when trying to wrap with attribute similar to position parent.
|
|
1181
|
-
if (attribute.isSimilar(position.parent)) {
|
|
1182
|
-
return movePositionToTextNode(position.clone());
|
|
1183
|
-
}
|
|
1184
|
-
// When position is inside text node - break it and place new position between two text nodes.
|
|
1185
|
-
if (position.parent.is('$text')) {
|
|
1186
|
-
position = breakTextNode(position);
|
|
1187
|
-
}
|
|
1188
|
-
// Create fake element that will represent position, and will not be merged with other attributes.
|
|
1189
|
-
const fakeElement = this.createAttributeElement('_wrapPosition-fake-element');
|
|
1190
|
-
fakeElement._priority = Number.POSITIVE_INFINITY;
|
|
1191
|
-
fakeElement.isSimilar = () => false;
|
|
1192
|
-
// Insert fake element in position location.
|
|
1193
|
-
position.parent._insertChild(position.offset, fakeElement);
|
|
1194
|
-
// Range around inserted fake attribute element.
|
|
1195
|
-
const wrapRange = new ViewRange(position, position.getShiftedBy(1));
|
|
1196
|
-
// Wrap fake element with attribute (it will also merge if possible).
|
|
1197
|
-
this.wrap(wrapRange, attribute);
|
|
1198
|
-
// Remove fake element and place new position there.
|
|
1199
|
-
const newPosition = new ViewPosition(fakeElement.parent, fakeElement.index);
|
|
1200
|
-
fakeElement._remove();
|
|
1201
|
-
// If position is placed between text nodes - merge them and return position inside.
|
|
1202
|
-
const nodeBefore = newPosition.nodeBefore;
|
|
1203
|
-
const nodeAfter = newPosition.nodeAfter;
|
|
1204
|
-
if (nodeBefore && nodeBefore.is('view:$text') && nodeAfter && nodeAfter.is('view:$text')) {
|
|
1205
|
-
return mergeTextNodes(nodeBefore, nodeAfter);
|
|
1206
|
-
}
|
|
1207
|
-
// If position is next to text node - move position inside.
|
|
1208
|
-
return movePositionToTextNode(newPosition);
|
|
1209
|
-
}
|
|
1210
|
-
/**
|
|
1211
|
-
* Helper function used by other `ViewDowncastWriter` methods. Breaks attribute elements at the boundaries of given range.
|
|
1212
|
-
*
|
|
1213
|
-
* @param range Range which `start` and `end` positions will be used to break attributes.
|
|
1214
|
-
* @param forceSplitText If set to `true`, will break text nodes even if they are directly in container element.
|
|
1215
|
-
* This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
|
|
1216
|
-
* @returns New range with located at break positions.
|
|
1217
|
-
*/
|
|
1218
|
-
_breakAttributesRange(range, forceSplitText = false) {
|
|
1219
|
-
const rangeStart = range.start;
|
|
1220
|
-
const rangeEnd = range.end;
|
|
1221
|
-
validateRangeContainer(range, this.document);
|
|
1222
|
-
// Break at the collapsed position. Return new collapsed range.
|
|
1223
|
-
if (range.isCollapsed) {
|
|
1224
|
-
const position = this._breakAttributes(range.start, forceSplitText);
|
|
1225
|
-
return new ViewRange(position, position);
|
|
1226
|
-
}
|
|
1227
|
-
const breakEnd = this._breakAttributes(rangeEnd, forceSplitText);
|
|
1228
|
-
const count = breakEnd.parent.childCount;
|
|
1229
|
-
const breakStart = this._breakAttributes(rangeStart, forceSplitText);
|
|
1230
|
-
// Calculate new break end offset.
|
|
1231
|
-
breakEnd.offset += breakEnd.parent.childCount - count;
|
|
1232
|
-
return new ViewRange(breakStart, breakEnd);
|
|
1233
|
-
}
|
|
1234
|
-
/**
|
|
1235
|
-
* Helper function used by other `ViewDowncastWriter` methods. Breaks attribute elements at given position.
|
|
1236
|
-
*
|
|
1237
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
|
|
1238
|
-
* is placed inside {@link module:engine/view/emptyelement~ViewEmptyElement ViewEmptyElement}.
|
|
1239
|
-
*
|
|
1240
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
|
|
1241
|
-
* is placed inside {@link module:engine/view/uielement~ViewUIElement UIElement}.
|
|
1242
|
-
*
|
|
1243
|
-
* @param position Position where to break attributes.
|
|
1244
|
-
* @param forceSplitText If set to `true`, will break text nodes even if they are directly in container element.
|
|
1245
|
-
* This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
|
|
1246
|
-
* @returns New position after breaking the attributes.
|
|
1247
|
-
*/
|
|
1248
|
-
_breakAttributes(position, forceSplitText = false) {
|
|
1249
|
-
const positionOffset = position.offset;
|
|
1250
|
-
const positionParent = position.parent;
|
|
1251
|
-
// If position is placed inside ViewEmptyElement - throw an exception as we cannot break inside.
|
|
1252
|
-
if (position.parent.is('emptyElement')) {
|
|
1253
|
-
/**
|
|
1254
|
-
* Cannot break an `EmptyElement` instance.
|
|
1255
|
-
*
|
|
1256
|
-
* This error is thrown if
|
|
1257
|
-
* {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes `ViewDowncastWriter#breakAttributes()`}
|
|
1258
|
-
* was executed in an incorrect position.
|
|
1259
|
-
*
|
|
1260
|
-
* @error view-writer-cannot-break-empty-element
|
|
1261
|
-
*/
|
|
1262
|
-
throw new CKEditorError('view-writer-cannot-break-empty-element', this.document);
|
|
1263
|
-
}
|
|
1264
|
-
// If position is placed inside UIElement - throw an exception as we cannot break inside.
|
|
1265
|
-
if (position.parent.is('uiElement')) {
|
|
1266
|
-
/**
|
|
1267
|
-
* Cannot break a `UIElement` instance.
|
|
1268
|
-
*
|
|
1269
|
-
* This error is thrown if
|
|
1270
|
-
* {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes `ViewDowncastWriter#breakAttributes()`}
|
|
1271
|
-
* was executed in an incorrect position.
|
|
1272
|
-
*
|
|
1273
|
-
* @error view-writer-cannot-break-ui-element
|
|
1274
|
-
*/
|
|
1275
|
-
throw new CKEditorError('view-writer-cannot-break-ui-element', this.document);
|
|
1276
|
-
}
|
|
1277
|
-
// If position is placed inside RawElement - throw an exception as we cannot break inside.
|
|
1278
|
-
if (position.parent.is('rawElement')) {
|
|
1279
|
-
/**
|
|
1280
|
-
* Cannot break a `RawElement` instance.
|
|
1281
|
-
*
|
|
1282
|
-
* This error is thrown if
|
|
1283
|
-
* {@link module:engine/view/downcastwriter~ViewDowncastWriter#breakAttributes `ViewDowncastWriter#breakAttributes()`}
|
|
1284
|
-
* was executed in an incorrect position.
|
|
1285
|
-
*
|
|
1286
|
-
* @error view-writer-cannot-break-raw-element
|
|
1287
|
-
*/
|
|
1288
|
-
throw new CKEditorError('view-writer-cannot-break-raw-element', this.document);
|
|
1289
|
-
}
|
|
1290
|
-
// There are no attributes to break and text nodes breaking is not forced.
|
|
1291
|
-
if (!forceSplitText && positionParent.is('$text') && isContainerOrFragment(positionParent.parent)) {
|
|
1292
|
-
return position.clone();
|
|
1293
|
-
}
|
|
1294
|
-
// Position's parent is container, so no attributes to break.
|
|
1295
|
-
if (isContainerOrFragment(positionParent)) {
|
|
1296
|
-
return position.clone();
|
|
1297
|
-
}
|
|
1298
|
-
// Break text and start again in new position.
|
|
1299
|
-
if (positionParent.is('$text')) {
|
|
1300
|
-
return this._breakAttributes(breakTextNode(position), forceSplitText);
|
|
1301
|
-
}
|
|
1302
|
-
const length = positionParent.childCount;
|
|
1303
|
-
// <p>foo<b><u>bar{}</u></b></p>
|
|
1304
|
-
// <p>foo<b><u>bar</u>[]</b></p>
|
|
1305
|
-
// <p>foo<b><u>bar</u></b>[]</p>
|
|
1306
|
-
if (positionOffset == length) {
|
|
1307
|
-
const newPosition = new ViewPosition(positionParent.parent, positionParent.index + 1);
|
|
1308
|
-
return this._breakAttributes(newPosition, forceSplitText);
|
|
1309
|
-
}
|
|
1310
|
-
else {
|
|
1311
|
-
// <p>foo<b><u>{}bar</u></b></p>
|
|
1312
|
-
// <p>foo<b>[]<u>bar</u></b></p>
|
|
1313
|
-
// <p>foo{}<b><u>bar</u></b></p>
|
|
1314
|
-
if (positionOffset === 0) {
|
|
1315
|
-
const newPosition = new ViewPosition(positionParent.parent, positionParent.index);
|
|
1316
|
-
return this._breakAttributes(newPosition, forceSplitText);
|
|
1317
|
-
}
|
|
1318
|
-
// <p>foo<b><u>b{}ar</u></b></p>
|
|
1319
|
-
// <p>foo<b><u>b[]ar</u></b></p>
|
|
1320
|
-
// <p>foo<b><u>b</u>[]<u>ar</u></b></p>
|
|
1321
|
-
// <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
|
|
1322
|
-
else {
|
|
1323
|
-
const offsetAfter = positionParent.index + 1;
|
|
1324
|
-
// Break element.
|
|
1325
|
-
const clonedNode = positionParent._clone();
|
|
1326
|
-
// Insert cloned node to position's parent node.
|
|
1327
|
-
positionParent.parent._insertChild(offsetAfter, clonedNode);
|
|
1328
|
-
this._addToClonedElementsGroup(clonedNode);
|
|
1329
|
-
// Get nodes to move.
|
|
1330
|
-
const count = positionParent.childCount - positionOffset;
|
|
1331
|
-
const nodesToMove = positionParent._removeChildren(positionOffset, count);
|
|
1332
|
-
// Move nodes to cloned node.
|
|
1333
|
-
clonedNode._appendChild(nodesToMove);
|
|
1334
|
-
// Create new position to work on.
|
|
1335
|
-
const newPosition = new ViewPosition(positionParent.parent, offsetAfter);
|
|
1336
|
-
return this._breakAttributes(newPosition, forceSplitText);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
/**
|
|
1341
|
-
* Stores the information that an {@link module:engine/view/attributeelement~ViewAttributeElement attribute element} was
|
|
1342
|
-
* added to the tree. Saves the reference to the group in the given element and updates the group, so other elements
|
|
1343
|
-
* from the group now keep a reference to the given attribute element.
|
|
1344
|
-
*
|
|
1345
|
-
* The clones group can be obtained using {@link module:engine/view/attributeelement~ViewAttributeElement#getElementsWithSameId}.
|
|
1346
|
-
*
|
|
1347
|
-
* Does nothing if added element has no {@link module:engine/view/attributeelement~ViewAttributeElement#id id}.
|
|
1348
|
-
*
|
|
1349
|
-
* @param element Attribute element to save.
|
|
1350
|
-
*/
|
|
1351
|
-
_addToClonedElementsGroup(element) {
|
|
1352
|
-
// Add only if the element is in document tree.
|
|
1353
|
-
if (!element.root.is('rootElement')) {
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
// Traverse the element's children recursively to find other attribute elements that also might got inserted.
|
|
1357
|
-
// The loop is at the beginning so we can make fast returns later in the code.
|
|
1358
|
-
if (element.is('element')) {
|
|
1359
|
-
for (const child of element.getChildren()) {
|
|
1360
|
-
this._addToClonedElementsGroup(child);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
const id = element.id;
|
|
1364
|
-
if (!id) {
|
|
1365
|
-
return;
|
|
1366
|
-
}
|
|
1367
|
-
let group = this._cloneGroups.get(id);
|
|
1368
|
-
if (!group) {
|
|
1369
|
-
group = new Set();
|
|
1370
|
-
this._cloneGroups.set(id, group);
|
|
1371
|
-
}
|
|
1372
|
-
group.add(element);
|
|
1373
|
-
element._clonesGroup = group;
|
|
1374
|
-
}
|
|
1375
|
-
/**
|
|
1376
|
-
* Removes all the information about the given {@link module:engine/view/attributeelement~ViewAttributeElement attribute element}
|
|
1377
|
-
* from its clones group.
|
|
1378
|
-
*
|
|
1379
|
-
* Keep in mind, that the element will still keep a reference to the group (but the group will not keep a reference to it).
|
|
1380
|
-
* This allows to reference the whole group even if the element was already removed from the tree.
|
|
1381
|
-
*
|
|
1382
|
-
* Does nothing if the element has no {@link module:engine/view/attributeelement~ViewAttributeElement#id id}.
|
|
1383
|
-
*
|
|
1384
|
-
* @param element Attribute element to remove.
|
|
1385
|
-
*/
|
|
1386
|
-
_removeFromClonedElementsGroup(element) {
|
|
1387
|
-
// Traverse the element's children recursively to find other attribute elements that also got removed.
|
|
1388
|
-
// The loop is at the beginning so we can make fast returns later in the code.
|
|
1389
|
-
if (element.is('element')) {
|
|
1390
|
-
for (const child of element.getChildren()) {
|
|
1391
|
-
this._removeFromClonedElementsGroup(child);
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
const id = element.id;
|
|
1395
|
-
if (!id) {
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
const group = this._cloneGroups.get(id);
|
|
1399
|
-
if (!group) {
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
group.delete(element);
|
|
1403
|
-
// Not removing group from element on purpose!
|
|
1404
|
-
// If other parts of code have reference to this element, they will be able to get references to other elements from the group.
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
// Helper function for `view.writer.wrap`. Checks if given element has any children that are not ui elements.
|
|
1408
|
-
function _hasNonUiChildren(parent) {
|
|
1409
|
-
return Array.from(parent.getChildren()).some(child => !child.is('uiElement'));
|
|
1410
|
-
}
|
|
1411
|
-
/**
|
|
1412
|
-
* The `attribute` passed to {@link module:engine/view/downcastwriter~ViewDowncastWriter#wrap `ViewDowncastWriter#wrap()`}
|
|
1413
|
-
* must be an instance of {@link module:engine/view/attributeelement~ViewAttributeElement `AttributeElement`}.
|
|
1414
|
-
*
|
|
1415
|
-
* @error view-writer-wrap-invalid-attribute
|
|
1416
|
-
*/
|
|
1417
|
-
/**
|
|
1418
|
-
* Returns first parent container of specified {@link module:engine/view/position~ViewPosition Position}.
|
|
1419
|
-
* Position's parent node is checked as first, then next parents are checked.
|
|
1420
|
-
* Note that {@link module:engine/view/documentfragment~ViewDocumentFragment DocumentFragment} is treated like a container.
|
|
1421
|
-
*
|
|
1422
|
-
* @param position Position used as a start point to locate parent container.
|
|
1423
|
-
* @returns Parent container element or `undefined` if container is not found.
|
|
1424
|
-
*/
|
|
1425
|
-
function getParentContainer(position) {
|
|
1426
|
-
let parent = position.parent;
|
|
1427
|
-
while (!isContainerOrFragment(parent)) {
|
|
1428
|
-
if (!parent) {
|
|
1429
|
-
return undefined;
|
|
1430
|
-
}
|
|
1431
|
-
parent = parent.parent;
|
|
1432
|
-
}
|
|
1433
|
-
return parent;
|
|
1434
|
-
}
|
|
1435
|
-
/**
|
|
1436
|
-
* Checks if first {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement} provided to the function
|
|
1437
|
-
* can be wrapped outside second element. It is done by comparing elements'
|
|
1438
|
-
* {@link module:engine/view/attributeelement~ViewAttributeElement#priority priorities}, if both have same priority
|
|
1439
|
-
* {@link module:engine/view/element~ViewElement#getIdentity identities} are compared.
|
|
1440
|
-
*/
|
|
1441
|
-
function shouldABeOutsideB(a, b) {
|
|
1442
|
-
if (a.priority < b.priority) {
|
|
1443
|
-
return true;
|
|
1444
|
-
}
|
|
1445
|
-
else if (a.priority > b.priority) {
|
|
1446
|
-
return false;
|
|
1447
|
-
}
|
|
1448
|
-
// When priorities are equal and names are different - use identities.
|
|
1449
|
-
return a.getIdentity() < b.getIdentity();
|
|
1450
|
-
}
|
|
1451
|
-
/**
|
|
1452
|
-
* Returns new position that is moved to near text node. Returns same position if there is no text node before of after
|
|
1453
|
-
* specified position.
|
|
1454
|
-
*
|
|
1455
|
-
* ```html
|
|
1456
|
-
* <p>foo[]</p> -> <p>foo{}</p>
|
|
1457
|
-
* <p>[]foo</p> -> <p>{}foo</p>
|
|
1458
|
-
* ```
|
|
1459
|
-
*
|
|
1460
|
-
* @returns Position located inside text node or same position if there is no text nodes
|
|
1461
|
-
* before or after position location.
|
|
1462
|
-
*/
|
|
1463
|
-
function movePositionToTextNode(position) {
|
|
1464
|
-
const nodeBefore = position.nodeBefore;
|
|
1465
|
-
if (nodeBefore && nodeBefore.is('$text')) {
|
|
1466
|
-
return new ViewPosition(nodeBefore, nodeBefore.data.length);
|
|
1467
|
-
}
|
|
1468
|
-
const nodeAfter = position.nodeAfter;
|
|
1469
|
-
if (nodeAfter && nodeAfter.is('$text')) {
|
|
1470
|
-
return new ViewPosition(nodeAfter, 0);
|
|
1471
|
-
}
|
|
1472
|
-
return position;
|
|
1473
|
-
}
|
|
1474
|
-
/**
|
|
1475
|
-
* Breaks text node into two text nodes when possible.
|
|
1476
|
-
*
|
|
1477
|
-
* ```html
|
|
1478
|
-
* <p>foo{}bar</p> -> <p>foo[]bar</p>
|
|
1479
|
-
* <p>{}foobar</p> -> <p>[]foobar</p>
|
|
1480
|
-
* <p>foobar{}</p> -> <p>foobar[]</p>
|
|
1481
|
-
* ```
|
|
1482
|
-
*
|
|
1483
|
-
* @param position Position that need to be placed inside text node.
|
|
1484
|
-
* @returns New position after breaking text node.
|
|
1485
|
-
*/
|
|
1486
|
-
function breakTextNode(position) {
|
|
1487
|
-
if (position.offset == position.parent.data.length) {
|
|
1488
|
-
return new ViewPosition(position.parent.parent, position.parent.index + 1);
|
|
1489
|
-
}
|
|
1490
|
-
if (position.offset === 0) {
|
|
1491
|
-
return new ViewPosition(position.parent.parent, position.parent.index);
|
|
1492
|
-
}
|
|
1493
|
-
// Get part of the text that need to be moved.
|
|
1494
|
-
const textToMove = position.parent.data.slice(position.offset);
|
|
1495
|
-
// Leave rest of the text in position's parent.
|
|
1496
|
-
position.parent._data = position.parent.data.slice(0, position.offset);
|
|
1497
|
-
// Insert new text node after position's parent text node.
|
|
1498
|
-
position.parent.parent._insertChild(position.parent.index + 1, new ViewText(position.root.document, textToMove));
|
|
1499
|
-
// Return new position between two newly created text nodes.
|
|
1500
|
-
return new ViewPosition(position.parent.parent, position.parent.index + 1);
|
|
1501
|
-
}
|
|
1502
|
-
/**
|
|
1503
|
-
* Merges two text nodes into first node. Removes second node and returns merge position.
|
|
1504
|
-
*
|
|
1505
|
-
* @param t1 First text node to merge. Data from second text node will be moved at the end of this text node.
|
|
1506
|
-
* @param t2 Second text node to merge. This node will be removed after merging.
|
|
1507
|
-
* @returns Position after merging text nodes.
|
|
1508
|
-
*/
|
|
1509
|
-
function mergeTextNodes(t1, t2) {
|
|
1510
|
-
// Merge text data into first text node and remove second one.
|
|
1511
|
-
const nodeBeforeLength = t1.data.length;
|
|
1512
|
-
t1._data += t2.data;
|
|
1513
|
-
t2._remove();
|
|
1514
|
-
return new ViewPosition(t1, nodeBeforeLength);
|
|
1515
|
-
}
|
|
1516
|
-
const validNodesToInsert = [ViewText, ViewAttributeElement, ViewContainerElement, ViewEmptyElement, ViewRawElement, ViewUIElement];
|
|
1517
|
-
/**
|
|
1518
|
-
* Checks if provided nodes are valid to insert.
|
|
1519
|
-
*
|
|
1520
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
|
|
1521
|
-
* contains instances that are not supported ones (see error description for valid ones.
|
|
1522
|
-
*/
|
|
1523
|
-
function validateNodesToInsert(nodes, errorContext) {
|
|
1524
|
-
for (const node of nodes) {
|
|
1525
|
-
if (!validNodesToInsert.some(validNode => node instanceof validNode)) {
|
|
1526
|
-
/**
|
|
1527
|
-
* One of the nodes to be inserted is of an invalid type.
|
|
1528
|
-
*
|
|
1529
|
-
* Nodes to be inserted with {@link module:engine/view/downcastwriter~ViewDowncastWriter#insert `ViewDowncastWriter#insert()`}
|
|
1530
|
-
* should be of the following types:
|
|
1531
|
-
*
|
|
1532
|
-
* * {@link module:engine/view/attributeelement~ViewAttributeElement ViewAttributeElement},
|
|
1533
|
-
* * {@link module:engine/view/containerelement~ViewContainerElement ViewContainerElement},
|
|
1534
|
-
* * {@link module:engine/view/emptyelement~ViewEmptyElement ViewEmptyElement},
|
|
1535
|
-
* * {@link module:engine/view/uielement~ViewUIElement UIElement},
|
|
1536
|
-
* * {@link module:engine/view/rawelement~ViewRawElement RawElement},
|
|
1537
|
-
* * {@link module:engine/view/text~ViewText Text}.
|
|
1538
|
-
*
|
|
1539
|
-
* @error view-writer-insert-invalid-node-type
|
|
1540
|
-
*/
|
|
1541
|
-
throw new CKEditorError('view-writer-insert-invalid-node-type', errorContext);
|
|
1542
|
-
}
|
|
1543
|
-
if (!node.is('$text')) {
|
|
1544
|
-
validateNodesToInsert(node.getChildren(), errorContext);
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
/**
|
|
1549
|
-
* Checks if node is ViewContainerElement or DocumentFragment, because in most cases they should be treated the same way.
|
|
1550
|
-
*
|
|
1551
|
-
* @returns Returns `true` if node is instance of ViewContainerElement or DocumentFragment.
|
|
1552
|
-
*/
|
|
1553
|
-
function isContainerOrFragment(node) {
|
|
1554
|
-
return node && (node.is('containerElement') || node.is('documentFragment'));
|
|
1555
|
-
}
|
|
1556
|
-
/**
|
|
1557
|
-
* Checks if {@link module:engine/view/range~ViewRange#start range start} and {@link module:engine/view/range~ViewRange#end range end}
|
|
1558
|
-
* are placed inside same {@link module:engine/view/containerelement~ViewContainerElement container element}.
|
|
1559
|
-
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
|
|
1560
|
-
*/
|
|
1561
|
-
function validateRangeContainer(range, errorContext) {
|
|
1562
|
-
const startContainer = getParentContainer(range.start);
|
|
1563
|
-
const endContainer = getParentContainer(range.end);
|
|
1564
|
-
if (!startContainer || !endContainer || startContainer !== endContainer) {
|
|
1565
|
-
/**
|
|
1566
|
-
* The container of the given range is invalid.
|
|
1567
|
-
*
|
|
1568
|
-
* This may happen if {@link module:engine/view/range~ViewRange#start range start} and
|
|
1569
|
-
* {@link module:engine/view/range~ViewRange#end range end} positions are not placed inside the same container element or
|
|
1570
|
-
* a parent container for these positions cannot be found.
|
|
1571
|
-
*
|
|
1572
|
-
* Methods like {@link module:engine/view/downcastwriter~ViewDowncastWriter#wrap `ViewDowncastWriter#remove()`},
|
|
1573
|
-
* {@link module:engine/view/downcastwriter~ViewDowncastWriter#wrap `ViewDowncastWriter#clean()`},
|
|
1574
|
-
* {@link module:engine/view/downcastwriter~ViewDowncastWriter#wrap `ViewDowncastWriter#wrap()`},
|
|
1575
|
-
* {@link module:engine/view/downcastwriter~ViewDowncastWriter#wrap `ViewDowncastWriter#unwrap()`} need to be called
|
|
1576
|
-
* on a range that has its start and end positions located in the same container element. Both positions can be
|
|
1577
|
-
* nested within other elements (e.g. an attribute element) but the closest container ancestor must be the same.
|
|
1578
|
-
*
|
|
1579
|
-
* @error view-writer-invalid-range-container
|
|
1580
|
-
*/
|
|
1581
|
-
throw new CKEditorError('view-writer-invalid-range-container', errorContext);
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
/**
|
|
1585
|
-
* Checks if the provided argument is a plain object that can be used as options for container element.
|
|
1586
|
-
*/
|
|
1587
|
-
function isContainerOptions(childrenOrOptions) {
|
|
1588
|
-
return isPlainObject(childrenOrOptions);
|
|
1589
|
-
}
|