@ckeditor/ckeditor5-engine 30.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +17 -0
- package/README.md +30 -0
- package/package.json +70 -0
- package/src/controller/datacontroller.js +563 -0
- package/src/controller/editingcontroller.js +149 -0
- package/src/conversion/conversion.js +644 -0
- package/src/conversion/conversionhelpers.js +40 -0
- package/src/conversion/downcastdispatcher.js +914 -0
- package/src/conversion/downcasthelpers.js +1706 -0
- package/src/conversion/mapper.js +696 -0
- package/src/conversion/modelconsumable.js +329 -0
- package/src/conversion/upcastdispatcher.js +807 -0
- package/src/conversion/upcasthelpers.js +997 -0
- package/src/conversion/viewconsumable.js +623 -0
- package/src/dataprocessor/basichtmlwriter.js +32 -0
- package/src/dataprocessor/dataprocessor.jsdoc +64 -0
- package/src/dataprocessor/htmldataprocessor.js +159 -0
- package/src/dataprocessor/htmlwriter.js +22 -0
- package/src/dataprocessor/xmldataprocessor.js +161 -0
- package/src/dev-utils/model.js +482 -0
- package/src/dev-utils/operationreplayer.js +140 -0
- package/src/dev-utils/utils.js +103 -0
- package/src/dev-utils/view.js +1091 -0
- package/src/index.js +52 -0
- package/src/model/batch.js +82 -0
- package/src/model/differ.js +1282 -0
- package/src/model/document.js +483 -0
- package/src/model/documentfragment.js +390 -0
- package/src/model/documentselection.js +1261 -0
- package/src/model/element.js +438 -0
- package/src/model/history.js +138 -0
- package/src/model/item.jsdoc +14 -0
- package/src/model/liveposition.js +182 -0
- package/src/model/liverange.js +221 -0
- package/src/model/markercollection.js +553 -0
- package/src/model/model.js +934 -0
- package/src/model/node.js +507 -0
- package/src/model/nodelist.js +217 -0
- package/src/model/operation/attributeoperation.js +202 -0
- package/src/model/operation/detachoperation.js +103 -0
- package/src/model/operation/insertoperation.js +188 -0
- package/src/model/operation/markeroperation.js +154 -0
- package/src/model/operation/mergeoperation.js +216 -0
- package/src/model/operation/moveoperation.js +209 -0
- package/src/model/operation/nooperation.js +58 -0
- package/src/model/operation/operation.js +139 -0
- package/src/model/operation/operationfactory.js +49 -0
- package/src/model/operation/renameoperation.js +155 -0
- package/src/model/operation/rootattributeoperation.js +211 -0
- package/src/model/operation/splitoperation.js +254 -0
- package/src/model/operation/transform.js +2389 -0
- package/src/model/operation/utils.js +292 -0
- package/src/model/position.js +1164 -0
- package/src/model/range.js +1049 -0
- package/src/model/rootelement.js +111 -0
- package/src/model/schema.js +1851 -0
- package/src/model/selection.js +902 -0
- package/src/model/text.js +138 -0
- package/src/model/textproxy.js +279 -0
- package/src/model/treewalker.js +414 -0
- package/src/model/utils/autoparagraphing.js +77 -0
- package/src/model/utils/deletecontent.js +528 -0
- package/src/model/utils/getselectedcontent.js +150 -0
- package/src/model/utils/insertcontent.js +824 -0
- package/src/model/utils/modifyselection.js +229 -0
- package/src/model/utils/selection-post-fixer.js +297 -0
- package/src/model/writer.js +1574 -0
- package/src/view/attributeelement.js +274 -0
- package/src/view/containerelement.js +123 -0
- package/src/view/document.js +221 -0
- package/src/view/documentfragment.js +273 -0
- package/src/view/documentselection.js +387 -0
- package/src/view/domconverter.js +1437 -0
- package/src/view/downcastwriter.js +2121 -0
- package/src/view/editableelement.js +118 -0
- package/src/view/element.js +945 -0
- package/src/view/elementdefinition.jsdoc +59 -0
- package/src/view/emptyelement.js +119 -0
- package/src/view/filler.js +161 -0
- package/src/view/item.jsdoc +14 -0
- package/src/view/matcher.js +776 -0
- package/src/view/node.js +391 -0
- package/src/view/observer/arrowkeysobserver.js +58 -0
- package/src/view/observer/bubblingemittermixin.js +307 -0
- package/src/view/observer/bubblingeventinfo.js +71 -0
- package/src/view/observer/clickobserver.js +46 -0
- package/src/view/observer/compositionobserver.js +79 -0
- package/src/view/observer/domeventdata.js +82 -0
- package/src/view/observer/domeventobserver.js +99 -0
- package/src/view/observer/fakeselectionobserver.js +118 -0
- package/src/view/observer/focusobserver.js +106 -0
- package/src/view/observer/inputobserver.js +44 -0
- package/src/view/observer/keyobserver.js +83 -0
- package/src/view/observer/mouseobserver.js +56 -0
- package/src/view/observer/mutationobserver.js +345 -0
- package/src/view/observer/observer.js +118 -0
- package/src/view/observer/selectionobserver.js +242 -0
- package/src/view/placeholder.js +285 -0
- package/src/view/position.js +426 -0
- package/src/view/range.js +533 -0
- package/src/view/rawelement.js +148 -0
- package/src/view/renderer.js +1037 -0
- package/src/view/rooteditableelement.js +107 -0
- package/src/view/selection.js +718 -0
- package/src/view/styles/background.js +73 -0
- package/src/view/styles/border.js +362 -0
- package/src/view/styles/margin.js +41 -0
- package/src/view/styles/padding.js +40 -0
- package/src/view/styles/utils.js +277 -0
- package/src/view/stylesmap.js +938 -0
- package/src/view/text.js +147 -0
- package/src/view/textproxy.js +199 -0
- package/src/view/treewalker.js +496 -0
- package/src/view/uielement.js +238 -0
- package/src/view/upcastwriter.js +484 -0
- package/src/view/view.js +721 -0
- package/theme/placeholder.css +27 -0
|
@@ -0,0 +1,2121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module module:engine/view/downcastwriter
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Position from './position';
|
|
11
|
+
import Range from './range';
|
|
12
|
+
import Selection from './selection';
|
|
13
|
+
import ContainerElement from './containerelement';
|
|
14
|
+
import AttributeElement from './attributeelement';
|
|
15
|
+
import EmptyElement from './emptyelement';
|
|
16
|
+
import UIElement from './uielement';
|
|
17
|
+
import RawElement from './rawelement';
|
|
18
|
+
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
|
|
19
|
+
import DocumentFragment from './documentfragment';
|
|
20
|
+
import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
|
|
21
|
+
import Text from './text';
|
|
22
|
+
import EditableElement from './editableelement';
|
|
23
|
+
import { isPlainObject } from 'lodash-es';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* View downcast writer.
|
|
27
|
+
*
|
|
28
|
+
* It provides a set of methods used to manipulate view nodes.
|
|
29
|
+
*
|
|
30
|
+
* Do not create an instance of this writer manually. To modify a view structure, use
|
|
31
|
+
* the {@link module:engine/view/view~View#change `View#change()`} block.
|
|
32
|
+
*
|
|
33
|
+
* The `DowncastWriter` is designed to work with semantic views which are the views that were/are being downcasted from the model.
|
|
34
|
+
* To work with ordinary views (e.g. parsed from a pasted content) use the
|
|
35
|
+
* {@link module:engine/view/upcastwriter~UpcastWriter upcast writer}.
|
|
36
|
+
*
|
|
37
|
+
* Read more about changing the view in the {@glink framework/guides/architecture/editing-engine#changing-the-view Changing the view}
|
|
38
|
+
* section of the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide.
|
|
39
|
+
*/
|
|
40
|
+
export default class DowncastWriter {
|
|
41
|
+
/**
|
|
42
|
+
* @param {module:engine/view/document~Document} document The view document instance.
|
|
43
|
+
*/
|
|
44
|
+
constructor( document ) {
|
|
45
|
+
/**
|
|
46
|
+
* The view document instance in which this writer operates.
|
|
47
|
+
*
|
|
48
|
+
* @readonly
|
|
49
|
+
* @type {module:engine/view/document~Document}
|
|
50
|
+
*/
|
|
51
|
+
this.document = document;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Holds references to the attribute groups that share the same {@link module:engine/view/attributeelement~AttributeElement#id id}.
|
|
55
|
+
* The keys are `id`s, the values are `Set`s holding {@link module:engine/view/attributeelement~AttributeElement}s.
|
|
56
|
+
*
|
|
57
|
+
* @private
|
|
58
|
+
* @type {Map.<String,Set>}
|
|
59
|
+
*/
|
|
60
|
+
this._cloneGroups = new Map();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sets {@link module:engine/view/documentselection~DocumentSelection selection's} ranges and direction to the
|
|
65
|
+
* specified location based on the given {@link module:engine/view/selection~Selectable selectable}.
|
|
66
|
+
*
|
|
67
|
+
* Usage:
|
|
68
|
+
*
|
|
69
|
+
* // Sets selection to the given range.
|
|
70
|
+
* const range = writer.createRange( start, end );
|
|
71
|
+
* writer.setSelection( range );
|
|
72
|
+
*
|
|
73
|
+
* // Sets backward selection to the given range.
|
|
74
|
+
* const range = writer.createRange( start, end );
|
|
75
|
+
* writer.setSelection( range );
|
|
76
|
+
*
|
|
77
|
+
* // Sets selection to given ranges.
|
|
78
|
+
* const ranges = [ writer.createRange( start1, end2 ), writer.createRange( start2, end2 ) ];
|
|
79
|
+
* writer.setSelection( range );
|
|
80
|
+
*
|
|
81
|
+
* // Sets selection to the other selection.
|
|
82
|
+
* const otherSelection = writer.createSelection();
|
|
83
|
+
* writer.setSelection( otherSelection );
|
|
84
|
+
*
|
|
85
|
+
* // Sets collapsed selection at the given position.
|
|
86
|
+
* const position = writer.createPositionFromPath( root, path );
|
|
87
|
+
* writer.setSelection( position );
|
|
88
|
+
*
|
|
89
|
+
* // Sets collapsed selection at the position of given item and offset.
|
|
90
|
+
* const paragraph = writer.createContainerElement( 'p' );
|
|
91
|
+
* writer.setSelection( paragraph, offset );
|
|
92
|
+
*
|
|
93
|
+
* Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
|
|
94
|
+
* that element and ends after the last child of that element.
|
|
95
|
+
*
|
|
96
|
+
* writer.setSelection( paragraph, 'in' );
|
|
97
|
+
*
|
|
98
|
+
* Creates a range on the {@link module:engine/view/item~Item item} which starts before the item and ends just after the item.
|
|
99
|
+
*
|
|
100
|
+
* writer.setSelection( paragraph, 'on' );
|
|
101
|
+
*
|
|
102
|
+
* // Removes all ranges.
|
|
103
|
+
* writer.setSelection( null );
|
|
104
|
+
*
|
|
105
|
+
* `DowncastWriter#setSelection()` allow passing additional options (`backward`, `fake` and `label`) as the last argument.
|
|
106
|
+
*
|
|
107
|
+
* // Sets selection as backward.
|
|
108
|
+
* writer.setSelection( range, { backward: true } );
|
|
109
|
+
*
|
|
110
|
+
* // Sets selection as fake.
|
|
111
|
+
* // Fake selection does not render as browser native selection over selected elements and is hidden to the user.
|
|
112
|
+
* // This way, no native selection UI artifacts are displayed to the user and selection over elements can be
|
|
113
|
+
* // represented in other way, for example by applying proper CSS class.
|
|
114
|
+
* writer.setSelection( range, { fake: true } );
|
|
115
|
+
*
|
|
116
|
+
* // Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
|
|
117
|
+
* // (and be properly handled by screen readers).
|
|
118
|
+
* writer.setSelection( range, { fake: true, label: 'foo' } );
|
|
119
|
+
*
|
|
120
|
+
* @param {module:engine/view/selection~Selectable} selectable
|
|
121
|
+
* @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
|
|
122
|
+
* @param {Object} [options]
|
|
123
|
+
* @param {Boolean} [options.backward] Sets this selection instance to be backward.
|
|
124
|
+
* @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
|
|
125
|
+
* @param {String} [options.label] Label for the fake selection.
|
|
126
|
+
*/
|
|
127
|
+
setSelection( selectable, placeOrOffset, options ) {
|
|
128
|
+
this.document.selection._setTo( selectable, placeOrOffset, options );
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Moves {@link module:engine/view/documentselection~DocumentSelection#focus selection's focus} to the specified location.
|
|
133
|
+
*
|
|
134
|
+
* The location can be specified in the same form as {@link module:engine/view/view~View#createPositionAt view.createPositionAt()}
|
|
135
|
+
* parameters.
|
|
136
|
+
*
|
|
137
|
+
* @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
|
|
138
|
+
* @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
|
|
139
|
+
* first parameter is a {@link module:engine/view/item~Item view item}.
|
|
140
|
+
*/
|
|
141
|
+
setSelectionFocus( itemOrPosition, offset ) {
|
|
142
|
+
this.document.selection._setFocus( itemOrPosition, offset );
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Creates a new {@link module:engine/view/documentfragment~DocumentFragment} instance.
|
|
147
|
+
*
|
|
148
|
+
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
|
|
149
|
+
* A list of nodes to be inserted into the created document fragment.
|
|
150
|
+
* @returns {module:engine/view/documentfragment~DocumentFragment} The created document fragment.
|
|
151
|
+
*/
|
|
152
|
+
createDocumentFragment( children ) {
|
|
153
|
+
return new DocumentFragment( this.document, children );
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Creates a new {@link module:engine/view/text~Text text node}.
|
|
158
|
+
*
|
|
159
|
+
* writer.createText( 'foo' );
|
|
160
|
+
*
|
|
161
|
+
* @param {String} data The text's data.
|
|
162
|
+
* @returns {module:engine/view/text~Text} The created text node.
|
|
163
|
+
*/
|
|
164
|
+
createText( data ) {
|
|
165
|
+
return new Text( this.document, data );
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Creates a new {@link module:engine/view/attributeelement~AttributeElement}.
|
|
170
|
+
*
|
|
171
|
+
* writer.createAttributeElement( 'strong' );
|
|
172
|
+
* writer.createAttributeElement( 'a', { href: 'foo.bar' } );
|
|
173
|
+
*
|
|
174
|
+
* // Make `<a>` element contain other attributes element so the `<a>` element is not broken.
|
|
175
|
+
* writer.createAttributeElement( 'a', { href: 'foo.bar' }, { priority: 5 } );
|
|
176
|
+
*
|
|
177
|
+
* // Set `id` of a marker element so it is not joined or merged with "normal" elements.
|
|
178
|
+
* writer.createAttributeElement( 'span', { class: 'my-marker' }, { id: 'marker:my' } );
|
|
179
|
+
*
|
|
180
|
+
* **Note:** By default an `AttributeElement` is split by a
|
|
181
|
+
* {@link module:engine/view/containerelement~ContainerElement `ContainerElement`} but this behavior can be modified
|
|
182
|
+
* with `isAllowedInsideAttributeElement` option set while {@link #createContainerElement creating the element}.
|
|
183
|
+
*
|
|
184
|
+
* @param {String} name Name of the element.
|
|
185
|
+
* @param {Object} [attributes] Element's attributes.
|
|
186
|
+
* @param {Object} [options] Element's options.
|
|
187
|
+
* @param {Number} [options.priority] Element's {@link module:engine/view/attributeelement~AttributeElement#priority priority}.
|
|
188
|
+
* @param {Number|String} [options.id] Element's {@link module:engine/view/attributeelement~AttributeElement#id id}.
|
|
189
|
+
* @returns {module:engine/view/attributeelement~AttributeElement} Created element.
|
|
190
|
+
*/
|
|
191
|
+
createAttributeElement( name, attributes, options = {} ) {
|
|
192
|
+
const attributeElement = new AttributeElement( this.document, name, attributes );
|
|
193
|
+
|
|
194
|
+
if ( typeof options.priority === 'number' ) {
|
|
195
|
+
attributeElement._priority = options.priority;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if ( options.id ) {
|
|
199
|
+
attributeElement._id = options.id;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return attributeElement;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Creates a new {@link module:engine/view/containerelement~ContainerElement}.
|
|
207
|
+
*
|
|
208
|
+
* writer.createContainerElement( 'p' );
|
|
209
|
+
*
|
|
210
|
+
* // Create element with custom attributes.
|
|
211
|
+
* writer.createContainerElement( 'div', { id: 'foo-bar', 'data-baz': '123' } );
|
|
212
|
+
*
|
|
213
|
+
* // Create element with custom styles.
|
|
214
|
+
* writer.createContainerElement( 'p', { style: 'font-weight: bold; padding-bottom: 10px' } );
|
|
215
|
+
*
|
|
216
|
+
* // Create element with custom classes.
|
|
217
|
+
* writer.createContainerElement( 'p', { class: 'foo bar baz' } );
|
|
218
|
+
*
|
|
219
|
+
* @param {String} name Name of the element.
|
|
220
|
+
* @param {Object} [attributes] Elements attributes.
|
|
221
|
+
* @param {Object} [options] Element's options.
|
|
222
|
+
* @param {Boolean} [options.isAllowedInsideAttributeElement=false] Whether an element is
|
|
223
|
+
* {@link module:engine/view/element~Element#isAllowedInsideAttributeElement allowed inside an AttributeElement} and can be wrapped
|
|
224
|
+
* with {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
|
|
225
|
+
* @returns {module:engine/view/containerelement~ContainerElement} Created element.
|
|
226
|
+
*/
|
|
227
|
+
createContainerElement( name, attributes, options = {} ) {
|
|
228
|
+
const containerElement = new ContainerElement( this.document, name, attributes );
|
|
229
|
+
|
|
230
|
+
if ( options.isAllowedInsideAttributeElement !== undefined ) {
|
|
231
|
+
containerElement._isAllowedInsideAttributeElement = options.isAllowedInsideAttributeElement;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return containerElement;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Creates a new {@link module:engine/view/editableelement~EditableElement}.
|
|
239
|
+
*
|
|
240
|
+
* writer.createEditableElement( 'div' );
|
|
241
|
+
* writer.createEditableElement( 'div', { id: 'foo-1234' } );
|
|
242
|
+
*
|
|
243
|
+
* Note: The editable element is to be used in the editing pipeline. Usually, together with
|
|
244
|
+
* {@link module:widget/utils~toWidgetEditable `toWidgetEditable()`}.
|
|
245
|
+
*
|
|
246
|
+
* @param {String} name Name of the element.
|
|
247
|
+
* @param {Object} [attributes] Elements attributes.
|
|
248
|
+
* @returns {module:engine/view/editableelement~EditableElement} Created element.
|
|
249
|
+
*/
|
|
250
|
+
createEditableElement( name, attributes ) {
|
|
251
|
+
const editableElement = new EditableElement( this.document, name, attributes );
|
|
252
|
+
editableElement._document = this.document;
|
|
253
|
+
|
|
254
|
+
return editableElement;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Creates a new {@link module:engine/view/emptyelement~EmptyElement}.
|
|
259
|
+
*
|
|
260
|
+
* writer.createEmptyElement( 'img' );
|
|
261
|
+
* writer.createEmptyElement( 'img', { id: 'foo-1234' } );
|
|
262
|
+
*
|
|
263
|
+
* @param {String} name Name of the element.
|
|
264
|
+
* @param {Object} [attributes] Elements attributes.
|
|
265
|
+
* @param {Object} [options] Element's options.
|
|
266
|
+
* @param {Boolean} [options.isAllowedInsideAttributeElement=true] Whether an element is
|
|
267
|
+
* {@link module:engine/view/element~Element#isAllowedInsideAttributeElement allowed inside an AttributeElement} and can be wrapped
|
|
268
|
+
* with {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
|
|
269
|
+
* @returns {module:engine/view/emptyelement~EmptyElement} Created element.
|
|
270
|
+
*/
|
|
271
|
+
createEmptyElement( name, attributes, options = {} ) {
|
|
272
|
+
const emptyElement = new EmptyElement( this.document, name, attributes );
|
|
273
|
+
|
|
274
|
+
if ( options.isAllowedInsideAttributeElement !== undefined ) {
|
|
275
|
+
emptyElement._isAllowedInsideAttributeElement = options.isAllowedInsideAttributeElement;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return emptyElement;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Creates a new {@link module:engine/view/uielement~UIElement}.
|
|
283
|
+
*
|
|
284
|
+
* writer.createUIElement( 'span' );
|
|
285
|
+
* writer.createUIElement( 'span', { id: 'foo-1234' } );
|
|
286
|
+
*
|
|
287
|
+
* A custom render function can be provided as the third parameter:
|
|
288
|
+
*
|
|
289
|
+
* writer.createUIElement( 'span', null, function( domDocument ) {
|
|
290
|
+
* const domElement = this.toDomElement( domDocument );
|
|
291
|
+
* domElement.innerHTML = '<b>this is ui element</b>';
|
|
292
|
+
*
|
|
293
|
+
* return domElement;
|
|
294
|
+
* } );
|
|
295
|
+
*
|
|
296
|
+
* Unlike {@link #createRawElement raw elements}, UI elements are by no means editor content, for instance,
|
|
297
|
+
* they are ignored by the editor selection system.
|
|
298
|
+
*
|
|
299
|
+
* You should not use UI elements as data containers. Check out {@link #createRawElement} instead.
|
|
300
|
+
*
|
|
301
|
+
* @param {String} name The name of the element.
|
|
302
|
+
* @param {Object} [attributes] Element attributes.
|
|
303
|
+
* @param {Function} [renderFunction] A custom render function.
|
|
304
|
+
* @param {Object} [options] Element's options.
|
|
305
|
+
* @param {Boolean} [options.isAllowedInsideAttributeElement=true] Whether an element is
|
|
306
|
+
* {@link module:engine/view/element~Element#isAllowedInsideAttributeElement allowed inside an AttributeElement} and can be wrapped
|
|
307
|
+
* with {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
|
|
308
|
+
* @returns {module:engine/view/uielement~UIElement} The created element.
|
|
309
|
+
*/
|
|
310
|
+
createUIElement( name, attributes, renderFunction, options = {} ) {
|
|
311
|
+
const uiElement = new UIElement( this.document, name, attributes );
|
|
312
|
+
|
|
313
|
+
if ( renderFunction ) {
|
|
314
|
+
uiElement.render = renderFunction;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if ( options.isAllowedInsideAttributeElement !== undefined ) {
|
|
318
|
+
uiElement._isAllowedInsideAttributeElement = options.isAllowedInsideAttributeElement;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return uiElement;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Creates a new {@link module:engine/view/rawelement~RawElement}.
|
|
326
|
+
*
|
|
327
|
+
* writer.createRawElement( 'span', { id: 'foo-1234' }, function( domElement ) {
|
|
328
|
+
* domElement.innerHTML = '<b>This is the raw content of the raw element.</b>';
|
|
329
|
+
* } );
|
|
330
|
+
*
|
|
331
|
+
* Raw elements work as data containers ("wrappers", "sandboxes") but their children are not managed or
|
|
332
|
+
* even recognized by the editor. This encapsulation allows integrations to maintain custom DOM structures
|
|
333
|
+
* in the editor content without, for instance, worrying about compatibility with other editor features.
|
|
334
|
+
* Raw elements are a perfect tool for integration with external frameworks and data sources.
|
|
335
|
+
*
|
|
336
|
+
* Unlike {@link #createUIElement UI elements}, raw elements act like "real" editor content (similar to
|
|
337
|
+
* {@link module:engine/view/containerelement~ContainerElement} or {@link module:engine/view/emptyelement~EmptyElement}),
|
|
338
|
+
* and they are considered by the editor selection.
|
|
339
|
+
*
|
|
340
|
+
* You should not use raw elements to render the UI in the editor content. Check out {@link #createUIElement `#createUIElement()`}
|
|
341
|
+
* instead.
|
|
342
|
+
*
|
|
343
|
+
* @param {String} name The name of the element.
|
|
344
|
+
* @param {Object} [attributes] Element attributes.
|
|
345
|
+
* @param {Function} [renderFunction] A custom render function.
|
|
346
|
+
* @param {Object} [options] Element's options.
|
|
347
|
+
* @param {Boolean} [options.isAllowedInsideAttributeElement=true] Whether an element is
|
|
348
|
+
* {@link module:engine/view/element~Element#isAllowedInsideAttributeElement allowed inside an AttributeElement} and can be wrapped
|
|
349
|
+
* with {@link module:engine/view/attributeelement~AttributeElement} by {@link module:engine/view/downcastwriter~DowncastWriter}.
|
|
350
|
+
* @returns {module:engine/view/rawelement~RawElement} The created element.
|
|
351
|
+
*/
|
|
352
|
+
createRawElement( name, attributes, renderFunction, options = {} ) {
|
|
353
|
+
const rawElement = new RawElement( this.document, name, attributes );
|
|
354
|
+
|
|
355
|
+
rawElement.render = renderFunction || ( () => {} );
|
|
356
|
+
|
|
357
|
+
if ( options.isAllowedInsideAttributeElement !== undefined ) {
|
|
358
|
+
rawElement._isAllowedInsideAttributeElement = options.isAllowedInsideAttributeElement;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return rawElement;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Adds or overwrites the element's attribute with a specified key and value.
|
|
366
|
+
*
|
|
367
|
+
* writer.setAttribute( 'href', 'http://ckeditor.com', linkElement );
|
|
368
|
+
*
|
|
369
|
+
* @param {String} key The attribute key.
|
|
370
|
+
* @param {String} value The attribute value.
|
|
371
|
+
* @param {module:engine/view/element~Element} element
|
|
372
|
+
*/
|
|
373
|
+
setAttribute( key, value, element ) {
|
|
374
|
+
element._setAttribute( key, value );
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Removes attribute from the element.
|
|
379
|
+
*
|
|
380
|
+
* writer.removeAttribute( 'href', linkElement );
|
|
381
|
+
*
|
|
382
|
+
* @param {String} key Attribute key.
|
|
383
|
+
* @param {module:engine/view/element~Element} element
|
|
384
|
+
*/
|
|
385
|
+
removeAttribute( key, element ) {
|
|
386
|
+
element._removeAttribute( key );
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Adds specified class to the element.
|
|
391
|
+
*
|
|
392
|
+
* writer.addClass( 'foo', linkElement );
|
|
393
|
+
* writer.addClass( [ 'foo', 'bar' ], linkElement );
|
|
394
|
+
*
|
|
395
|
+
* @param {Array.<String>|String} className
|
|
396
|
+
* @param {module:engine/view/element~Element} element
|
|
397
|
+
*/
|
|
398
|
+
addClass( className, element ) {
|
|
399
|
+
element._addClass( className );
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Removes specified class from the element.
|
|
404
|
+
*
|
|
405
|
+
* writer.removeClass( 'foo', linkElement );
|
|
406
|
+
* writer.removeClass( [ 'foo', 'bar' ], linkElement );
|
|
407
|
+
*
|
|
408
|
+
* @param {Array.<String>|String} className
|
|
409
|
+
* @param {module:engine/view/element~Element} element
|
|
410
|
+
*/
|
|
411
|
+
removeClass( className, element ) {
|
|
412
|
+
element._removeClass( className );
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Adds style to the element.
|
|
417
|
+
*
|
|
418
|
+
* writer.setStyle( 'color', 'red', element );
|
|
419
|
+
* writer.setStyle( {
|
|
420
|
+
* color: 'red',
|
|
421
|
+
* position: 'fixed'
|
|
422
|
+
* }, element );
|
|
423
|
+
*
|
|
424
|
+
* **Note**: The passed style can be normalized if
|
|
425
|
+
* {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
|
|
426
|
+
* See {@link module:engine/view/stylesmap~StylesMap#set `StylesMap#set()`} for details.
|
|
427
|
+
*
|
|
428
|
+
* @param {String|Object} property Property name or object with key - value pairs.
|
|
429
|
+
* @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter.
|
|
430
|
+
* @param {module:engine/view/element~Element} element Element to set styles on.
|
|
431
|
+
*/
|
|
432
|
+
setStyle( property, value, element ) {
|
|
433
|
+
if ( isPlainObject( property ) && element === undefined ) {
|
|
434
|
+
element = value;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
element._setStyle( property, value );
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Removes specified style from the element.
|
|
442
|
+
*
|
|
443
|
+
* writer.removeStyle( 'color', element ); // Removes 'color' style.
|
|
444
|
+
* writer.removeStyle( [ 'color', 'border-top' ], element ); // Removes both 'color' and 'border-top' styles.
|
|
445
|
+
*
|
|
446
|
+
* **Note**: This method can work with normalized style names if
|
|
447
|
+
* {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
|
|
448
|
+
* See {@link module:engine/view/stylesmap~StylesMap#remove `StylesMap#remove()`} for details.
|
|
449
|
+
*
|
|
450
|
+
* @param {Array.<String>|String} property
|
|
451
|
+
* @param {module:engine/view/element~Element} element
|
|
452
|
+
*/
|
|
453
|
+
removeStyle( property, element ) {
|
|
454
|
+
element._removeStyle( property );
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Sets a custom property on element. Unlike attributes, custom properties are not rendered to the DOM,
|
|
459
|
+
* so they can be used to add special data to elements.
|
|
460
|
+
*
|
|
461
|
+
* @param {String|Symbol} key
|
|
462
|
+
* @param {*} value
|
|
463
|
+
* @param {module:engine/view/element~Element} element
|
|
464
|
+
*/
|
|
465
|
+
setCustomProperty( key, value, element ) {
|
|
466
|
+
element._setCustomProperty( key, value );
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Removes a custom property stored under the given key.
|
|
471
|
+
*
|
|
472
|
+
* @param {String|Symbol} key
|
|
473
|
+
* @param {module:engine/view/element~Element} element
|
|
474
|
+
* @returns {Boolean} Returns true if property was removed.
|
|
475
|
+
*/
|
|
476
|
+
removeCustomProperty( key, element ) {
|
|
477
|
+
return element._removeCustomProperty( key );
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Breaks attribute elements at the provided position or at the boundaries of a provided range. It breaks attribute elements
|
|
482
|
+
* up to their first ancestor that is a container element.
|
|
483
|
+
*
|
|
484
|
+
* In following examples `<p>` is a container, `<b>` and `<u>` are attribute elements:
|
|
485
|
+
*
|
|
486
|
+
* <p>foo<b><u>bar{}</u></b></p> -> <p>foo<b><u>bar</u></b>[]</p>
|
|
487
|
+
* <p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p>
|
|
488
|
+
* <p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
|
|
489
|
+
* <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>
|
|
490
|
+
*
|
|
491
|
+
* **Note:** {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
|
|
492
|
+
*
|
|
493
|
+
* **Note:** The difference between {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes()} and
|
|
494
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
|
|
495
|
+
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of a given `position`,
|
|
496
|
+
* up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
|
|
497
|
+
* `breakContainer()` assumes that a given `position` is directly in the container element and breaks that container element.
|
|
498
|
+
*
|
|
499
|
+
* Throws the `view-writer-invalid-range-container` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
|
|
500
|
+
* when the {@link module:engine/view/range~Range#start start}
|
|
501
|
+
* and {@link module:engine/view/range~Range#end end} positions of a passed range are not placed inside same parent container.
|
|
502
|
+
*
|
|
503
|
+
* Throws the `view-writer-cannot-break-empty-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
|
|
504
|
+
* when trying to break attributes inside an {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
|
|
505
|
+
*
|
|
506
|
+
* Throws the `view-writer-cannot-break-ui-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
|
|
507
|
+
* when trying to break attributes inside a {@link module:engine/view/uielement~UIElement UIElement}.
|
|
508
|
+
*
|
|
509
|
+
* @see module:engine/view/attributeelement~AttributeElement
|
|
510
|
+
* @see module:engine/view/containerelement~ContainerElement
|
|
511
|
+
* @see module:engine/view/downcastwriter~DowncastWriter#breakContainer
|
|
512
|
+
* @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange The position where
|
|
513
|
+
* to break attribute elements.
|
|
514
|
+
* @returns {module:engine/view/position~Position|module:engine/view/range~Range} The new position or range, after breaking the
|
|
515
|
+
* attribute elements.
|
|
516
|
+
*/
|
|
517
|
+
breakAttributes( positionOrRange ) {
|
|
518
|
+
if ( positionOrRange instanceof Position ) {
|
|
519
|
+
return this._breakAttributes( positionOrRange );
|
|
520
|
+
} else {
|
|
521
|
+
return this._breakAttributesRange( positionOrRange );
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Breaks a {@link module:engine/view/containerelement~ContainerElement container view element} into two, at the given position.
|
|
527
|
+
* The position has to be directly inside the container element and cannot be in the root. It does not break the conrainer view element
|
|
528
|
+
* if the position is at the beginning or at the end of its parent element.
|
|
529
|
+
*
|
|
530
|
+
* <p>foo^bar</p> -> <p>foo</p><p>bar</p>
|
|
531
|
+
* <div><p>foo</p>^<p>bar</p></div> -> <div><p>foo</p></div><div><p>bar</p></div>
|
|
532
|
+
* <p>^foobar</p> -> ^<p>foobar</p>
|
|
533
|
+
* <p>foobar^</p> -> <p>foobar</p>^
|
|
534
|
+
*
|
|
535
|
+
* **Note:** The difference between {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes()} and
|
|
536
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
|
|
537
|
+
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of a given `position`,
|
|
538
|
+
* up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
|
|
539
|
+
* `breakContainer()` assumes that the given `position` is directly in the container element and breaks that container element.
|
|
540
|
+
*
|
|
541
|
+
* @see module:engine/view/attributeelement~AttributeElement
|
|
542
|
+
* @see module:engine/view/containerelement~ContainerElement
|
|
543
|
+
* @see module:engine/view/downcastwriter~DowncastWriter#breakAttributes
|
|
544
|
+
* @param {module:engine/view/position~Position} position The position where to break the element.
|
|
545
|
+
* @returns {module:engine/view/position~Position} The position between broken elements. If an element has not been broken,
|
|
546
|
+
* the returned position is placed either before or after it.
|
|
547
|
+
*/
|
|
548
|
+
breakContainer( position ) {
|
|
549
|
+
const element = position.parent;
|
|
550
|
+
|
|
551
|
+
if ( !( element.is( 'containerElement' ) ) ) {
|
|
552
|
+
/**
|
|
553
|
+
* Trying to break an element which is not a container element.
|
|
554
|
+
*
|
|
555
|
+
* @error view-writer-break-non-container-element
|
|
556
|
+
*/
|
|
557
|
+
throw new CKEditorError( 'view-writer-break-non-container-element', this.document );
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if ( !element.parent ) {
|
|
561
|
+
/**
|
|
562
|
+
* Trying to break root element.
|
|
563
|
+
*
|
|
564
|
+
* @error view-writer-break-root
|
|
565
|
+
*/
|
|
566
|
+
throw new CKEditorError( 'view-writer-break-root', this.document );
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if ( position.isAtStart ) {
|
|
570
|
+
return Position._createBefore( element );
|
|
571
|
+
} else if ( !position.isAtEnd ) {
|
|
572
|
+
const newElement = element._clone( false );
|
|
573
|
+
|
|
574
|
+
this.insert( Position._createAfter( element ), newElement );
|
|
575
|
+
|
|
576
|
+
const sourceRange = new Range( position, Position._createAt( element, 'end' ) );
|
|
577
|
+
const targetPosition = new Position( newElement, 0 );
|
|
578
|
+
|
|
579
|
+
this.move( sourceRange, targetPosition );
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return Position._createAfter( element );
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Merges {@link module:engine/view/attributeelement~AttributeElement attribute elements}. It also merges text nodes if needed.
|
|
587
|
+
* Only {@link module:engine/view/attributeelement~AttributeElement#isSimilar similar} attribute elements can be merged.
|
|
588
|
+
*
|
|
589
|
+
* In following examples `<p>` is a container and `<b>` is an attribute element:
|
|
590
|
+
*
|
|
591
|
+
* <p>foo[]bar</p> -> <p>foo{}bar</p>
|
|
592
|
+
* <p><b>foo</b>[]<b>bar</b></p> -> <p><b>foo{}bar</b></p>
|
|
593
|
+
* <p><b foo="bar">a</b>[]<b foo="baz">b</b></p> -> <p><b foo="bar">a</b>[]<b foo="baz">b</b></p>
|
|
594
|
+
*
|
|
595
|
+
* It will also take care about empty attributes when merging:
|
|
596
|
+
*
|
|
597
|
+
* <p><b>[]</b></p> -> <p>[]</p>
|
|
598
|
+
* <p><b>foo</b><i>[]</i><b>bar</b></p> -> <p><b>foo{}bar</b></p>
|
|
599
|
+
*
|
|
600
|
+
* **Note:** Difference between {@link module:engine/view/downcastwriter~DowncastWriter#mergeAttributes mergeAttributes} and
|
|
601
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
|
|
602
|
+
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
|
|
603
|
+
* while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
|
|
604
|
+
*
|
|
605
|
+
* @see module:engine/view/attributeelement~AttributeElement
|
|
606
|
+
* @see module:engine/view/containerelement~ContainerElement
|
|
607
|
+
* @see module:engine/view/downcastwriter~DowncastWriter#mergeContainers
|
|
608
|
+
* @param {module:engine/view/position~Position} position Merge position.
|
|
609
|
+
* @returns {module:engine/view/position~Position} Position after merge.
|
|
610
|
+
*/
|
|
611
|
+
mergeAttributes( position ) {
|
|
612
|
+
const positionOffset = position.offset;
|
|
613
|
+
const positionParent = position.parent;
|
|
614
|
+
|
|
615
|
+
// When inside text node - nothing to merge.
|
|
616
|
+
if ( positionParent.is( '$text' ) ) {
|
|
617
|
+
return position;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// When inside empty attribute - remove it.
|
|
621
|
+
if ( positionParent.is( 'attributeElement' ) && positionParent.childCount === 0 ) {
|
|
622
|
+
const parent = positionParent.parent;
|
|
623
|
+
const offset = positionParent.index;
|
|
624
|
+
|
|
625
|
+
positionParent._remove();
|
|
626
|
+
this._removeFromClonedElementsGroup( positionParent );
|
|
627
|
+
|
|
628
|
+
return this.mergeAttributes( new Position( parent, offset ) );
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const nodeBefore = positionParent.getChild( positionOffset - 1 );
|
|
632
|
+
const nodeAfter = positionParent.getChild( positionOffset );
|
|
633
|
+
|
|
634
|
+
// Position should be placed between two nodes.
|
|
635
|
+
if ( !nodeBefore || !nodeAfter ) {
|
|
636
|
+
return position;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// When position is between two text nodes.
|
|
640
|
+
if ( nodeBefore.is( '$text' ) && nodeAfter.is( '$text' ) ) {
|
|
641
|
+
return mergeTextNodes( nodeBefore, nodeAfter );
|
|
642
|
+
}
|
|
643
|
+
// When position is between two same attribute elements.
|
|
644
|
+
else if ( nodeBefore.is( 'attributeElement' ) && nodeAfter.is( 'attributeElement' ) && nodeBefore.isSimilar( nodeAfter ) ) {
|
|
645
|
+
// Move all children nodes from node placed after selection and remove that node.
|
|
646
|
+
const count = nodeBefore.childCount;
|
|
647
|
+
nodeBefore._appendChild( nodeAfter.getChildren() );
|
|
648
|
+
|
|
649
|
+
nodeAfter._remove();
|
|
650
|
+
this._removeFromClonedElementsGroup( nodeAfter );
|
|
651
|
+
|
|
652
|
+
// New position is located inside the first node, before new nodes.
|
|
653
|
+
// Call this method recursively to merge again if needed.
|
|
654
|
+
return this.mergeAttributes( new Position( nodeBefore, count ) );
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return position;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Merges two {@link module:engine/view/containerelement~ContainerElement container elements} that are before and after given position.
|
|
662
|
+
* Precisely, the element after the position is removed and it's contents are moved to element before the position.
|
|
663
|
+
*
|
|
664
|
+
* <p>foo</p>^<p>bar</p> -> <p>foo^bar</p>
|
|
665
|
+
* <div>foo</div>^<p>bar</p> -> <div>foo^bar</div>
|
|
666
|
+
*
|
|
667
|
+
* **Note:** Difference between {@link module:engine/view/downcastwriter~DowncastWriter#mergeAttributes mergeAttributes} and
|
|
668
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
|
|
669
|
+
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
|
|
670
|
+
* while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
|
|
671
|
+
*
|
|
672
|
+
* @see module:engine/view/attributeelement~AttributeElement
|
|
673
|
+
* @see module:engine/view/containerelement~ContainerElement
|
|
674
|
+
* @see module:engine/view/downcastwriter~DowncastWriter#mergeAttributes
|
|
675
|
+
* @param {module:engine/view/position~Position} position Merge position.
|
|
676
|
+
* @returns {module:engine/view/position~Position} Position after merge.
|
|
677
|
+
*/
|
|
678
|
+
mergeContainers( position ) {
|
|
679
|
+
const prev = position.nodeBefore;
|
|
680
|
+
const next = position.nodeAfter;
|
|
681
|
+
|
|
682
|
+
if ( !prev || !next || !prev.is( 'containerElement' ) || !next.is( 'containerElement' ) ) {
|
|
683
|
+
/**
|
|
684
|
+
* Element before and after given position cannot be merged.
|
|
685
|
+
*
|
|
686
|
+
* @error view-writer-merge-containers-invalid-position
|
|
687
|
+
*/
|
|
688
|
+
throw new CKEditorError( 'view-writer-merge-containers-invalid-position', this.document );
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const lastChild = prev.getChild( prev.childCount - 1 );
|
|
692
|
+
const newPosition = lastChild instanceof Text ? Position._createAt( lastChild, 'end' ) : Position._createAt( prev, 'end' );
|
|
693
|
+
|
|
694
|
+
this.move( Range._createIn( next ), Position._createAt( prev, 'end' ) );
|
|
695
|
+
this.remove( Range._createOn( next ) );
|
|
696
|
+
|
|
697
|
+
return newPosition;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Inserts a node or nodes at specified position. Takes care about breaking attributes before insertion
|
|
702
|
+
* and merging them afterwards.
|
|
703
|
+
*
|
|
704
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
|
|
705
|
+
* contains instances that are not {@link module:engine/view/text~Text Texts},
|
|
706
|
+
* {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
|
|
707
|
+
* {@link module:engine/view/containerelement~ContainerElement ContainerElements},
|
|
708
|
+
* {@link module:engine/view/emptyelement~EmptyElement EmptyElements},
|
|
709
|
+
* {@link module:engine/view/rawelement~RawElement RawElements} or
|
|
710
|
+
* {@link module:engine/view/uielement~UIElement UIElements}.
|
|
711
|
+
*
|
|
712
|
+
* @param {module:engine/view/position~Position} position Insertion position.
|
|
713
|
+
* @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
|
|
714
|
+
* module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
|
|
715
|
+
* module:engine/view/rawelement~RawElement|module:engine/view/uielement~UIElement|
|
|
716
|
+
* Iterable.<module:engine/view/text~Text|
|
|
717
|
+
* module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement|
|
|
718
|
+
* module:engine/view/emptyelement~EmptyElement|module:engine/view/rawelement~RawElement|
|
|
719
|
+
* module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
|
|
720
|
+
* @returns {module:engine/view/range~Range} Range around inserted nodes.
|
|
721
|
+
*/
|
|
722
|
+
insert( position, nodes ) {
|
|
723
|
+
nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
|
|
724
|
+
|
|
725
|
+
// Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
|
|
726
|
+
validateNodesToInsert( nodes, this.document );
|
|
727
|
+
|
|
728
|
+
// Group nodes in batches of nodes that require or do not require breaking an AttributeElements.
|
|
729
|
+
const nodeGroups = nodes.reduce( ( groups, node ) => {
|
|
730
|
+
const lastGroup = groups[ groups.length - 1 ];
|
|
731
|
+
|
|
732
|
+
// Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
|
|
733
|
+
// can't have an attribute in model and won't get wrapped with an AttributeElement while down-casted.
|
|
734
|
+
const breakAttributes = !( node.is( 'uiElement' ) && node.isAllowedInsideAttributeElement );
|
|
735
|
+
|
|
736
|
+
if ( !lastGroup || lastGroup.breakAttributes != breakAttributes ) {
|
|
737
|
+
groups.push( {
|
|
738
|
+
breakAttributes,
|
|
739
|
+
nodes: [ node ]
|
|
740
|
+
} );
|
|
741
|
+
} else {
|
|
742
|
+
lastGroup.nodes.push( node );
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return groups;
|
|
746
|
+
}, [] );
|
|
747
|
+
|
|
748
|
+
// Insert nodes in batches.
|
|
749
|
+
let start = null;
|
|
750
|
+
let end = position;
|
|
751
|
+
|
|
752
|
+
for ( const { nodes, breakAttributes } of nodeGroups ) {
|
|
753
|
+
const range = this._insertNodes( end, nodes, breakAttributes );
|
|
754
|
+
|
|
755
|
+
if ( !start ) {
|
|
756
|
+
start = range.start;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
end = range.end;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// When no nodes were inserted - return collapsed range.
|
|
763
|
+
if ( !start ) {
|
|
764
|
+
return new Range( position );
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return new Range( start, end );
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Removes provided range from the container.
|
|
772
|
+
*
|
|
773
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
|
|
774
|
+
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
|
|
775
|
+
* same parent container.
|
|
776
|
+
*
|
|
777
|
+
* @param {module:engine/view/range~Range|module:engine/view/item~Item} rangeOrItem Range to remove from container
|
|
778
|
+
* or an {@link module:engine/view/item~Item item} to remove. If range is provided, after removing, it will be updated
|
|
779
|
+
* to a collapsed range showing the new position.
|
|
780
|
+
* @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
|
|
781
|
+
*/
|
|
782
|
+
remove( rangeOrItem ) {
|
|
783
|
+
const range = rangeOrItem instanceof Range ? rangeOrItem : Range._createOn( rangeOrItem );
|
|
784
|
+
|
|
785
|
+
validateRangeContainer( range, this.document );
|
|
786
|
+
|
|
787
|
+
// If range is collapsed - nothing to remove.
|
|
788
|
+
if ( range.isCollapsed ) {
|
|
789
|
+
return new DocumentFragment( this.document );
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Break attributes at range start and end.
|
|
793
|
+
const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
|
|
794
|
+
const parentContainer = breakStart.parent;
|
|
795
|
+
|
|
796
|
+
const count = breakEnd.offset - breakStart.offset;
|
|
797
|
+
|
|
798
|
+
// Remove nodes in range.
|
|
799
|
+
const removed = parentContainer._removeChildren( breakStart.offset, count );
|
|
800
|
+
|
|
801
|
+
for ( const node of removed ) {
|
|
802
|
+
this._removeFromClonedElementsGroup( node );
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Merge after removing.
|
|
806
|
+
const mergePosition = this.mergeAttributes( breakStart );
|
|
807
|
+
range.start = mergePosition;
|
|
808
|
+
range.end = mergePosition.clone();
|
|
809
|
+
|
|
810
|
+
// Return removed nodes.
|
|
811
|
+
return new DocumentFragment( this.document, removed );
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Removes matching elements from given range.
|
|
816
|
+
*
|
|
817
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
|
|
818
|
+
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
|
|
819
|
+
* same parent container.
|
|
820
|
+
*
|
|
821
|
+
* @param {module:engine/view/range~Range} range Range to clear.
|
|
822
|
+
* @param {module:engine/view/element~Element} element Element to remove.
|
|
823
|
+
*/
|
|
824
|
+
clear( range, element ) {
|
|
825
|
+
validateRangeContainer( range, this.document );
|
|
826
|
+
|
|
827
|
+
// Create walker on given range.
|
|
828
|
+
// We walk backward because when we remove element during walk it modifies range end position.
|
|
829
|
+
const walker = range.getWalker( {
|
|
830
|
+
direction: 'backward',
|
|
831
|
+
ignoreElementEnd: true
|
|
832
|
+
} );
|
|
833
|
+
|
|
834
|
+
// Let's walk.
|
|
835
|
+
for ( const current of walker ) {
|
|
836
|
+
const item = current.item;
|
|
837
|
+
let rangeToRemove;
|
|
838
|
+
|
|
839
|
+
// When current item matches to the given element.
|
|
840
|
+
if ( item.is( 'element' ) && element.isSimilar( item ) ) {
|
|
841
|
+
// Create range on this element.
|
|
842
|
+
rangeToRemove = Range._createOn( item );
|
|
843
|
+
// When range starts inside Text or TextProxy element.
|
|
844
|
+
} else if ( !current.nextPosition.isAfter( range.start ) && item.is( '$textProxy' ) ) {
|
|
845
|
+
// We need to check if parent of this text matches to given element.
|
|
846
|
+
const parentElement = item.getAncestors().find( ancestor => {
|
|
847
|
+
return ancestor.is( 'element' ) && element.isSimilar( ancestor );
|
|
848
|
+
} );
|
|
849
|
+
|
|
850
|
+
// If it is then create range inside this element.
|
|
851
|
+
if ( parentElement ) {
|
|
852
|
+
rangeToRemove = Range._createIn( parentElement );
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// If we have found element to remove.
|
|
857
|
+
if ( rangeToRemove ) {
|
|
858
|
+
// We need to check if element range stick out of the given range and truncate if it is.
|
|
859
|
+
if ( rangeToRemove.end.isAfter( range.end ) ) {
|
|
860
|
+
rangeToRemove.end = range.end;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if ( rangeToRemove.start.isBefore( range.start ) ) {
|
|
864
|
+
rangeToRemove.start = range.start;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// At the end we remove range with found element.
|
|
868
|
+
this.remove( rangeToRemove );
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Moves nodes from provided range to target position.
|
|
875
|
+
*
|
|
876
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
|
|
877
|
+
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
|
|
878
|
+
* same parent container.
|
|
879
|
+
*
|
|
880
|
+
* @param {module:engine/view/range~Range} sourceRange Range containing nodes to move.
|
|
881
|
+
* @param {module:engine/view/position~Position} targetPosition Position to insert.
|
|
882
|
+
* @returns {module:engine/view/range~Range} Range in target container. Inserted nodes are placed between
|
|
883
|
+
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions.
|
|
884
|
+
*/
|
|
885
|
+
move( sourceRange, targetPosition ) {
|
|
886
|
+
let nodes;
|
|
887
|
+
|
|
888
|
+
if ( targetPosition.isAfter( sourceRange.end ) ) {
|
|
889
|
+
targetPosition = this._breakAttributes( targetPosition, true );
|
|
890
|
+
|
|
891
|
+
const parent = targetPosition.parent;
|
|
892
|
+
const countBefore = parent.childCount;
|
|
893
|
+
|
|
894
|
+
sourceRange = this._breakAttributesRange( sourceRange, true );
|
|
895
|
+
|
|
896
|
+
nodes = this.remove( sourceRange );
|
|
897
|
+
|
|
898
|
+
targetPosition.offset += ( parent.childCount - countBefore );
|
|
899
|
+
} else {
|
|
900
|
+
nodes = this.remove( sourceRange );
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return this.insert( targetPosition, nodes );
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
|
|
908
|
+
* If a collapsed range is provided, it will be wrapped only if it is equal to view selection.
|
|
909
|
+
*
|
|
910
|
+
* If a collapsed range was passed and is same as selection, the selection
|
|
911
|
+
* will be moved to the inside of the wrapped attribute element.
|
|
912
|
+
*
|
|
913
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
|
|
914
|
+
* when {@link module:engine/view/range~Range#start}
|
|
915
|
+
* and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container.
|
|
916
|
+
*
|
|
917
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
|
|
918
|
+
* an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
|
|
919
|
+
*
|
|
920
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
|
|
921
|
+
* is collapsed and different than view selection.
|
|
922
|
+
*
|
|
923
|
+
* **Note:** Attribute elements by default can wrap {@link module:engine/view/text~Text},
|
|
924
|
+
* {@link module:engine/view/emptyelement~EmptyElement}, {@link module:engine/view/uielement~UIElement},
|
|
925
|
+
* {@link module:engine/view/rawelement~RawElement} and other attribute elements with higher priority. Other elements while placed
|
|
926
|
+
* inside an attribute element will split it (or nest it in case of an `AttributeElement`). This behavior can be modified by changing
|
|
927
|
+
* the `isAllowedInsideAttributeElement` option while using
|
|
928
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement},
|
|
929
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement},
|
|
930
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement} or
|
|
931
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#createRawElement}.
|
|
932
|
+
*
|
|
933
|
+
* @param {module:engine/view/range~Range} range Range to wrap.
|
|
934
|
+
* @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
|
|
935
|
+
* @returns {module:engine/view/range~Range} range Range after wrapping, spanning over wrapping attribute element.
|
|
936
|
+
*/
|
|
937
|
+
wrap( range, attribute ) {
|
|
938
|
+
if ( !( attribute instanceof AttributeElement ) ) {
|
|
939
|
+
throw new CKEditorError(
|
|
940
|
+
'view-writer-wrap-invalid-attribute',
|
|
941
|
+
this.document
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
validateRangeContainer( range, this.document );
|
|
946
|
+
|
|
947
|
+
if ( !range.isCollapsed ) {
|
|
948
|
+
// Non-collapsed range. Wrap it with the attribute element.
|
|
949
|
+
return this._wrapRange( range, attribute );
|
|
950
|
+
} else {
|
|
951
|
+
// Collapsed range. Wrap position.
|
|
952
|
+
let position = range.start;
|
|
953
|
+
|
|
954
|
+
if ( position.parent.is( 'element' ) && !_hasNonUiChildren( position.parent ) ) {
|
|
955
|
+
position = position.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
position = this._wrapPosition( position, attribute );
|
|
959
|
+
const viewSelection = this.document.selection;
|
|
960
|
+
|
|
961
|
+
// If wrapping position is equal to view selection, move view selection inside wrapping attribute element.
|
|
962
|
+
if ( viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual( range.start ) ) {
|
|
963
|
+
this.setSelection( position );
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return new Range( position );
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Unwraps nodes within provided range from attribute element.
|
|
972
|
+
*
|
|
973
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
|
|
974
|
+
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
|
|
975
|
+
* same parent container.
|
|
976
|
+
*
|
|
977
|
+
* @param {module:engine/view/range~Range} range
|
|
978
|
+
* @param {module:engine/view/attributeelement~AttributeElement} attribute
|
|
979
|
+
*/
|
|
980
|
+
unwrap( range, attribute ) {
|
|
981
|
+
if ( !( attribute instanceof AttributeElement ) ) {
|
|
982
|
+
/**
|
|
983
|
+
* The `attribute` passed to {@link module:engine/view/downcastwriter~DowncastWriter#unwrap `DowncastWriter#unwrap()`}
|
|
984
|
+
* must be an instance of {@link module:engine/view/attributeelement~AttributeElement `AttributeElement`}.
|
|
985
|
+
*
|
|
986
|
+
* @error view-writer-unwrap-invalid-attribute
|
|
987
|
+
*/
|
|
988
|
+
throw new CKEditorError(
|
|
989
|
+
'view-writer-unwrap-invalid-attribute',
|
|
990
|
+
this.document
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
validateRangeContainer( range, this.document );
|
|
995
|
+
|
|
996
|
+
// If range is collapsed - nothing to unwrap.
|
|
997
|
+
if ( range.isCollapsed ) {
|
|
998
|
+
return range;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Break attributes at range start and end.
|
|
1002
|
+
const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
|
|
1003
|
+
const parentContainer = breakStart.parent;
|
|
1004
|
+
|
|
1005
|
+
// Unwrap children located between break points.
|
|
1006
|
+
const newRange = this._unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
|
|
1007
|
+
|
|
1008
|
+
// Merge attributes at the both ends and return a new range.
|
|
1009
|
+
const start = this.mergeAttributes( newRange.start );
|
|
1010
|
+
|
|
1011
|
+
// If start position was merged - move end position back.
|
|
1012
|
+
if ( !start.isEqual( newRange.start ) ) {
|
|
1013
|
+
newRange.end.offset--;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const end = this.mergeAttributes( newRange.end );
|
|
1017
|
+
|
|
1018
|
+
return new Range( start, end );
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Renames element by creating a copy of renamed element but with changed name and then moving contents of the
|
|
1023
|
+
* old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~Position positions} which
|
|
1024
|
+
* has renamed element as {@link module:engine/view/position~Position#parent a parent}.
|
|
1025
|
+
*
|
|
1026
|
+
* New element has to be created because `Element#tagName` property in DOM is readonly.
|
|
1027
|
+
*
|
|
1028
|
+
* Since this function creates a new element and removes the given one, the new element is returned to keep reference.
|
|
1029
|
+
*
|
|
1030
|
+
* @param {String} newName New name for element.
|
|
1031
|
+
* @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
|
|
1032
|
+
* @returns {module:engine/view/containerelement~ContainerElement} Element created due to rename.
|
|
1033
|
+
*/
|
|
1034
|
+
rename( newName, viewElement ) {
|
|
1035
|
+
const newElement = new ContainerElement( this.document, newName, viewElement.getAttributes() );
|
|
1036
|
+
|
|
1037
|
+
this.insert( Position._createAfter( viewElement ), newElement );
|
|
1038
|
+
this.move( Range._createIn( viewElement ), Position._createAt( newElement, 0 ) );
|
|
1039
|
+
this.remove( Range._createOn( viewElement ) );
|
|
1040
|
+
|
|
1041
|
+
return newElement;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Cleans up memory by removing obsolete cloned elements group from the writer.
|
|
1046
|
+
*
|
|
1047
|
+
* Should be used whenever all {@link module:engine/view/attributeelement~AttributeElement attribute elements}
|
|
1048
|
+
* with the same {@link module:engine/view/attributeelement~AttributeElement#id id} are going to be removed from the view and
|
|
1049
|
+
* the group will no longer be needed.
|
|
1050
|
+
*
|
|
1051
|
+
* Cloned elements group are not removed automatically in case if the group is still needed after all its elements
|
|
1052
|
+
* were removed from the view.
|
|
1053
|
+
*
|
|
1054
|
+
* Keep in mind that group names are equal to the `id` property of the attribute element.
|
|
1055
|
+
*
|
|
1056
|
+
* @param {String} groupName Name of the group to clear.
|
|
1057
|
+
*/
|
|
1058
|
+
clearClonedElementsGroup( groupName ) {
|
|
1059
|
+
this._cloneGroups.delete( groupName );
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Creates position at the given location. The location can be specified as:
|
|
1064
|
+
*
|
|
1065
|
+
* * a {@link module:engine/view/position~Position position},
|
|
1066
|
+
* * parent element and offset (offset defaults to `0`),
|
|
1067
|
+
* * parent element and `'end'` (sets position at the end of that element),
|
|
1068
|
+
* * {@link module:engine/view/item~Item view item} and `'before'` or `'after'` (sets position before or after given view item).
|
|
1069
|
+
*
|
|
1070
|
+
* This method is a shortcut to other constructors such as:
|
|
1071
|
+
*
|
|
1072
|
+
* * {@link #createPositionBefore},
|
|
1073
|
+
* * {@link #createPositionAfter},
|
|
1074
|
+
*
|
|
1075
|
+
* @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
|
|
1076
|
+
* @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
|
|
1077
|
+
* first parameter is a {@link module:engine/view/item~Item view item}.
|
|
1078
|
+
* @returns {module:engine/view/position~Position}
|
|
1079
|
+
*/
|
|
1080
|
+
createPositionAt( itemOrPosition, offset ) {
|
|
1081
|
+
return Position._createAt( itemOrPosition, offset );
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Creates a new position after given view item.
|
|
1086
|
+
*
|
|
1087
|
+
* @param {module:engine/view/item~Item} item View item after which the position should be located.
|
|
1088
|
+
* @returns {module:engine/view/position~Position}
|
|
1089
|
+
*/
|
|
1090
|
+
createPositionAfter( item ) {
|
|
1091
|
+
return Position._createAfter( item );
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Creates a new position before given view item.
|
|
1096
|
+
*
|
|
1097
|
+
* @param {module:engine/view/item~Item} item View item before which the position should be located.
|
|
1098
|
+
* @returns {module:engine/view/position~Position}
|
|
1099
|
+
*/
|
|
1100
|
+
createPositionBefore( item ) {
|
|
1101
|
+
return Position._createBefore( item );
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Creates a range spanning from `start` position to `end` position.
|
|
1106
|
+
*
|
|
1107
|
+
* **Note:** This factory method creates its own {@link module:engine/view/position~Position} instances basing on passed values.
|
|
1108
|
+
*
|
|
1109
|
+
* @param {module:engine/view/position~Position} start Start position.
|
|
1110
|
+
* @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
|
|
1111
|
+
* @returns {module:engine/view/range~Range}
|
|
1112
|
+
*/
|
|
1113
|
+
createRange( start, end ) {
|
|
1114
|
+
return new Range( start, end );
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
|
|
1119
|
+
*
|
|
1120
|
+
* @param {module:engine/view/item~Item} item
|
|
1121
|
+
* @returns {module:engine/view/range~Range}
|
|
1122
|
+
*/
|
|
1123
|
+
createRangeOn( item ) {
|
|
1124
|
+
return Range._createOn( item );
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
|
|
1129
|
+
* that element and ends after the last child of that element.
|
|
1130
|
+
*
|
|
1131
|
+
* @param {module:engine/view/element~Element} element Element which is a parent for the range.
|
|
1132
|
+
* @returns {module:engine/view/range~Range}
|
|
1133
|
+
*/
|
|
1134
|
+
createRangeIn( element ) {
|
|
1135
|
+
return Range._createIn( element );
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
Creates new {@link module:engine/view/selection~Selection} instance.
|
|
1140
|
+
*
|
|
1141
|
+
* // Creates empty selection without ranges.
|
|
1142
|
+
* const selection = writer.createSelection();
|
|
1143
|
+
*
|
|
1144
|
+
* // Creates selection at the given range.
|
|
1145
|
+
* const range = writer.createRange( start, end );
|
|
1146
|
+
* const selection = writer.createSelection( range );
|
|
1147
|
+
*
|
|
1148
|
+
* // Creates selection at the given ranges
|
|
1149
|
+
* const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
|
|
1150
|
+
* const selection = writer.createSelection( ranges );
|
|
1151
|
+
*
|
|
1152
|
+
* // Creates selection from the other selection.
|
|
1153
|
+
* const otherSelection = writer.createSelection();
|
|
1154
|
+
* const selection = writer.createSelection( otherSelection );
|
|
1155
|
+
*
|
|
1156
|
+
* // Creates selection from the document selection.
|
|
1157
|
+
* const selection = writer.createSelection( editor.editing.view.document.selection );
|
|
1158
|
+
*
|
|
1159
|
+
* // Creates selection at the given position.
|
|
1160
|
+
* const position = writer.createPositionFromPath( root, path );
|
|
1161
|
+
* const selection = writer.createSelection( position );
|
|
1162
|
+
*
|
|
1163
|
+
* // Creates collapsed selection at the position of given item and offset.
|
|
1164
|
+
* const paragraph = writer.createContainerElement( 'p' );
|
|
1165
|
+
* const selection = writer.createSelection( paragraph, offset );
|
|
1166
|
+
*
|
|
1167
|
+
* // Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
|
|
1168
|
+
* // first child of that element and ends after the last child of that element.
|
|
1169
|
+
* const selection = writer.createSelection( paragraph, 'in' );
|
|
1170
|
+
*
|
|
1171
|
+
* // Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
|
|
1172
|
+
* // just after the item.
|
|
1173
|
+
* const selection = writer.createSelection( paragraph, 'on' );
|
|
1174
|
+
*
|
|
1175
|
+
* `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
|
|
1176
|
+
*
|
|
1177
|
+
* // Creates backward selection.
|
|
1178
|
+
* const selection = writer.createSelection( range, { backward: true } );
|
|
1179
|
+
*
|
|
1180
|
+
* Fake selection does not render as browser native selection over selected elements and is hidden to the user.
|
|
1181
|
+
* This way, no native selection UI artifacts are displayed to the user and selection over elements can be
|
|
1182
|
+
* represented in other way, for example by applying proper CSS class.
|
|
1183
|
+
*
|
|
1184
|
+
* Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
|
|
1185
|
+
* (and be properly handled by screen readers).
|
|
1186
|
+
*
|
|
1187
|
+
* // Creates fake selection with label.
|
|
1188
|
+
* const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
|
|
1189
|
+
*
|
|
1190
|
+
* @param {module:engine/view/selection~Selectable} [selectable=null]
|
|
1191
|
+
* @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
|
|
1192
|
+
* @param {Object} [options]
|
|
1193
|
+
* @param {Boolean} [options.backward] Sets this selection instance to be backward.
|
|
1194
|
+
* @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
|
|
1195
|
+
* @param {String} [options.label] Label for the fake selection.
|
|
1196
|
+
* @returns {module:engine/view/selection~Selection}
|
|
1197
|
+
*/
|
|
1198
|
+
createSelection( selectable, placeOrOffset, options ) {
|
|
1199
|
+
return new Selection( selectable, placeOrOffset, options );
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Inserts a node or nodes at the specified position. Takes care of breaking attributes before insertion
|
|
1204
|
+
* and merging them afterwards if requested by the breakAttributes param.
|
|
1205
|
+
*
|
|
1206
|
+
* @private
|
|
1207
|
+
* @param {module:engine/view/position~Position} position Insertion position.
|
|
1208
|
+
* @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
|
|
1209
|
+
* module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
|
|
1210
|
+
* module:engine/view/rawelement~RawElement|module:engine/view/uielement~UIElement|
|
|
1211
|
+
* Iterable.<module:engine/view/text~Text|
|
|
1212
|
+
* module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement|
|
|
1213
|
+
* module:engine/view/emptyelement~EmptyElement|module:engine/view/rawelement~RawElement|
|
|
1214
|
+
* module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
|
|
1215
|
+
* @param {Boolean} breakAttributes Whether attributes should be broken.
|
|
1216
|
+
* @returns {module:engine/view/range~Range} Range around inserted nodes.
|
|
1217
|
+
*/
|
|
1218
|
+
_insertNodes( position, nodes, breakAttributes ) {
|
|
1219
|
+
let parentElement;
|
|
1220
|
+
|
|
1221
|
+
// Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
|
|
1222
|
+
// can't have an attribute in model and won't get wrapped with an AttributeElement while down-casted.
|
|
1223
|
+
if ( breakAttributes ) {
|
|
1224
|
+
parentElement = getParentContainer( position );
|
|
1225
|
+
} else {
|
|
1226
|
+
parentElement = position.parent.is( '$text' ) ? position.parent.parent : position.parent;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if ( !parentElement ) {
|
|
1230
|
+
/**
|
|
1231
|
+
* Position's parent container cannot be found.
|
|
1232
|
+
*
|
|
1233
|
+
* @error view-writer-invalid-position-container
|
|
1234
|
+
*/
|
|
1235
|
+
throw new CKEditorError(
|
|
1236
|
+
'view-writer-invalid-position-container',
|
|
1237
|
+
this.document
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
let insertionPosition;
|
|
1242
|
+
|
|
1243
|
+
if ( breakAttributes ) {
|
|
1244
|
+
insertionPosition = this._breakAttributes( position, true );
|
|
1245
|
+
} else {
|
|
1246
|
+
insertionPosition = position.parent.is( '$text' ) ? breakTextNode( position ) : position;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const length = parentElement._insertChild( insertionPosition.offset, nodes );
|
|
1250
|
+
|
|
1251
|
+
for ( const node of nodes ) {
|
|
1252
|
+
this._addToClonedElementsGroup( node );
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const endPosition = insertionPosition.getShiftedBy( length );
|
|
1256
|
+
const start = this.mergeAttributes( insertionPosition );
|
|
1257
|
+
|
|
1258
|
+
// If start position was merged - move end position.
|
|
1259
|
+
if ( !start.isEqual( insertionPosition ) ) {
|
|
1260
|
+
endPosition.offset--;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const end = this.mergeAttributes( endPosition );
|
|
1264
|
+
|
|
1265
|
+
return new Range( start, end );
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Wraps children with provided `wrapElement`. Only children contained in `parent` element between
|
|
1270
|
+
* `startOffset` and `endOffset` will be wrapped.
|
|
1271
|
+
*
|
|
1272
|
+
* @private
|
|
1273
|
+
* @param {module:engine/view/element~Element} parent
|
|
1274
|
+
* @param {Number} startOffset
|
|
1275
|
+
* @param {Number} endOffset
|
|
1276
|
+
* @param {module:engine/view/element~Element} wrapElement
|
|
1277
|
+
*/
|
|
1278
|
+
_wrapChildren( parent, startOffset, endOffset, wrapElement ) {
|
|
1279
|
+
let i = startOffset;
|
|
1280
|
+
const wrapPositions = [];
|
|
1281
|
+
|
|
1282
|
+
while ( i < endOffset ) {
|
|
1283
|
+
const child = parent.getChild( i );
|
|
1284
|
+
const isText = child.is( '$text' );
|
|
1285
|
+
const isAttribute = child.is( 'attributeElement' );
|
|
1286
|
+
const isAllowedInsideAttributeElement = child.isAllowedInsideAttributeElement;
|
|
1287
|
+
|
|
1288
|
+
//
|
|
1289
|
+
// (In all examples, assume that `wrapElement` is `<span class="foo">` element.)
|
|
1290
|
+
//
|
|
1291
|
+
// Check if `wrapElement` can be joined with the wrapped element. One of requirements is having same name.
|
|
1292
|
+
// If possible, join elements.
|
|
1293
|
+
//
|
|
1294
|
+
// <p><span class="bar">abc</span></p> --> <p><span class="foo bar">abc</span></p>
|
|
1295
|
+
//
|
|
1296
|
+
if ( isAttribute && this._wrapAttributeElement( wrapElement, child ) ) {
|
|
1297
|
+
wrapPositions.push( new Position( parent, i ) );
|
|
1298
|
+
}
|
|
1299
|
+
//
|
|
1300
|
+
// Wrap the child if it is not an attribute element or if it is an attribute element that should be inside
|
|
1301
|
+
// `wrapElement` (due to priority).
|
|
1302
|
+
//
|
|
1303
|
+
// <p>abc</p> --> <p><span class="foo">abc</span></p>
|
|
1304
|
+
// <p><strong>abc</strong></p> --> <p><span class="foo"><strong>abc</strong></span></p>
|
|
1305
|
+
else if ( isText || isAllowedInsideAttributeElement || ( isAttribute && shouldABeOutsideB( wrapElement, child ) ) ) {
|
|
1306
|
+
// Clone attribute.
|
|
1307
|
+
const newAttribute = wrapElement._clone();
|
|
1308
|
+
|
|
1309
|
+
// Wrap current node with new attribute.
|
|
1310
|
+
child._remove();
|
|
1311
|
+
newAttribute._appendChild( child );
|
|
1312
|
+
|
|
1313
|
+
parent._insertChild( i, newAttribute );
|
|
1314
|
+
this._addToClonedElementsGroup( newAttribute );
|
|
1315
|
+
|
|
1316
|
+
wrapPositions.push( new Position( parent, i ) );
|
|
1317
|
+
}
|
|
1318
|
+
//
|
|
1319
|
+
// If other nested attribute is found and it wasn't wrapped (see above), continue wrapping inside it.
|
|
1320
|
+
//
|
|
1321
|
+
// <p><a href="foo.html">abc</a></p> --> <p><a href="foo.html"><span class="foo">abc</span></a></p>
|
|
1322
|
+
//
|
|
1323
|
+
else if ( isAttribute ) {
|
|
1324
|
+
this._wrapChildren( child, 0, child.childCount, wrapElement );
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
i++;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Merge at each wrap.
|
|
1331
|
+
let offsetChange = 0;
|
|
1332
|
+
|
|
1333
|
+
for ( const position of wrapPositions ) {
|
|
1334
|
+
position.offset -= offsetChange;
|
|
1335
|
+
|
|
1336
|
+
// Do not merge with elements outside selected children.
|
|
1337
|
+
if ( position.offset == startOffset ) {
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const newPosition = this.mergeAttributes( position );
|
|
1342
|
+
|
|
1343
|
+
// If nodes were merged - other merge offsets will change.
|
|
1344
|
+
if ( !newPosition.isEqual( position ) ) {
|
|
1345
|
+
offsetChange++;
|
|
1346
|
+
endOffset--;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
return Range._createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Unwraps children from provided `unwrapElement`. Only children contained in `parent` element between
|
|
1355
|
+
* `startOffset` and `endOffset` will be unwrapped.
|
|
1356
|
+
*
|
|
1357
|
+
* @private
|
|
1358
|
+
* @param {module:engine/view/element~Element} parent
|
|
1359
|
+
* @param {Number} startOffset
|
|
1360
|
+
* @param {Number} endOffset
|
|
1361
|
+
* @param {module:engine/view/element~Element} unwrapElement
|
|
1362
|
+
*/
|
|
1363
|
+
_unwrapChildren( parent, startOffset, endOffset, unwrapElement ) {
|
|
1364
|
+
let i = startOffset;
|
|
1365
|
+
const unwrapPositions = [];
|
|
1366
|
+
|
|
1367
|
+
// Iterate over each element between provided offsets inside parent.
|
|
1368
|
+
// We don't use tree walker or range iterator because we will be removing and merging potentially multiple nodes,
|
|
1369
|
+
// so it could get messy. It is safer to it manually in this case.
|
|
1370
|
+
while ( i < endOffset ) {
|
|
1371
|
+
const child = parent.getChild( i );
|
|
1372
|
+
|
|
1373
|
+
// Skip all text nodes. There should be no container element's here either.
|
|
1374
|
+
if ( !child.is( 'attributeElement' ) ) {
|
|
1375
|
+
i++;
|
|
1376
|
+
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
//
|
|
1381
|
+
// (In all examples, assume that `unwrapElement` is `<span class="foo">` element.)
|
|
1382
|
+
//
|
|
1383
|
+
// If the child is similar to the given attribute element, unwrap it - it will be completely removed.
|
|
1384
|
+
//
|
|
1385
|
+
// <p><span class="foo">abc</span>xyz</p> --> <p>abcxyz</p>
|
|
1386
|
+
//
|
|
1387
|
+
if ( child.isSimilar( unwrapElement ) ) {
|
|
1388
|
+
const unwrapped = child.getChildren();
|
|
1389
|
+
const count = child.childCount;
|
|
1390
|
+
|
|
1391
|
+
// Replace wrapper element with its children
|
|
1392
|
+
child._remove();
|
|
1393
|
+
parent._insertChild( i, unwrapped );
|
|
1394
|
+
|
|
1395
|
+
this._removeFromClonedElementsGroup( child );
|
|
1396
|
+
|
|
1397
|
+
// Save start and end position of moved items.
|
|
1398
|
+
unwrapPositions.push(
|
|
1399
|
+
new Position( parent, i ),
|
|
1400
|
+
new Position( parent, i + count )
|
|
1401
|
+
);
|
|
1402
|
+
|
|
1403
|
+
// Skip elements that were unwrapped. Assuming there won't be another element to unwrap in child elements.
|
|
1404
|
+
i += count;
|
|
1405
|
+
endOffset += count - 1;
|
|
1406
|
+
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
//
|
|
1411
|
+
// If the child is not similar but is an attribute element, try partial unwrapping - remove the same attributes/styles/classes.
|
|
1412
|
+
// Partial unwrapping will happen only if the elements have the same name.
|
|
1413
|
+
//
|
|
1414
|
+
// <p><span class="foo bar">abc</span>xyz</p> --> <p><span class="bar">abc</span>xyz</p>
|
|
1415
|
+
// <p><i class="foo">abc</i>xyz</p> --> <p><i class="foo">abc</i>xyz</p>
|
|
1416
|
+
//
|
|
1417
|
+
if ( this._unwrapAttributeElement( unwrapElement, child ) ) {
|
|
1418
|
+
unwrapPositions.push(
|
|
1419
|
+
new Position( parent, i ),
|
|
1420
|
+
new Position( parent, i + 1 )
|
|
1421
|
+
);
|
|
1422
|
+
|
|
1423
|
+
i++;
|
|
1424
|
+
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
//
|
|
1429
|
+
// If other nested attribute is found, look through it's children for elements to unwrap.
|
|
1430
|
+
//
|
|
1431
|
+
// <p><i><span class="foo">abc</span></i><p> --> <p><i>abc</i><p>
|
|
1432
|
+
//
|
|
1433
|
+
this._unwrapChildren( child, 0, child.childCount, unwrapElement );
|
|
1434
|
+
|
|
1435
|
+
i++;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// Merge at each unwrap.
|
|
1439
|
+
let offsetChange = 0;
|
|
1440
|
+
|
|
1441
|
+
for ( const position of unwrapPositions ) {
|
|
1442
|
+
position.offset -= offsetChange;
|
|
1443
|
+
|
|
1444
|
+
// Do not merge with elements outside selected children.
|
|
1445
|
+
if ( position.offset == startOffset || position.offset == endOffset ) {
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const newPosition = this.mergeAttributes( position );
|
|
1450
|
+
|
|
1451
|
+
// If nodes were merged - other merge offsets will change.
|
|
1452
|
+
if ( !newPosition.isEqual( position ) ) {
|
|
1453
|
+
offsetChange++;
|
|
1454
|
+
endOffset--;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
return Range._createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* Helper function for `view.writer.wrap`. Wraps range with provided attribute element.
|
|
1463
|
+
* This method will also merge newly added attribute element with its siblings whenever possible.
|
|
1464
|
+
*
|
|
1465
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
|
|
1466
|
+
* an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
|
|
1467
|
+
*
|
|
1468
|
+
* @private
|
|
1469
|
+
* @param {module:engine/view/range~Range} range
|
|
1470
|
+
* @param {module:engine/view/attributeelement~AttributeElement} attribute
|
|
1471
|
+
* @returns {module:engine/view/range~Range} New range after wrapping, spanning over wrapping attribute element.
|
|
1472
|
+
*/
|
|
1473
|
+
_wrapRange( range, attribute ) {
|
|
1474
|
+
// Break attributes at range start and end.
|
|
1475
|
+
const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
|
|
1476
|
+
const parentContainer = breakStart.parent;
|
|
1477
|
+
|
|
1478
|
+
// Wrap all children with attribute.
|
|
1479
|
+
const newRange = this._wrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
|
|
1480
|
+
|
|
1481
|
+
// Merge attributes at the both ends and return a new range.
|
|
1482
|
+
const start = this.mergeAttributes( newRange.start );
|
|
1483
|
+
|
|
1484
|
+
// If start position was merged - move end position back.
|
|
1485
|
+
if ( !start.isEqual( newRange.start ) ) {
|
|
1486
|
+
newRange.end.offset--;
|
|
1487
|
+
}
|
|
1488
|
+
const end = this.mergeAttributes( newRange.end );
|
|
1489
|
+
|
|
1490
|
+
return new Range( start, end );
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Helper function for {@link #wrap}. Wraps position with provided attribute element.
|
|
1495
|
+
* This method will also merge newly added attribute element with its siblings whenever possible.
|
|
1496
|
+
*
|
|
1497
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
|
|
1498
|
+
* an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
|
|
1499
|
+
*
|
|
1500
|
+
* @private
|
|
1501
|
+
* @param {module:engine/view/position~Position} position
|
|
1502
|
+
* @param {module:engine/view/attributeelement~AttributeElement} attribute
|
|
1503
|
+
* @returns {module:engine/view/position~Position} New position after wrapping.
|
|
1504
|
+
*/
|
|
1505
|
+
_wrapPosition( position, attribute ) {
|
|
1506
|
+
// Return same position when trying to wrap with attribute similar to position parent.
|
|
1507
|
+
if ( attribute.isSimilar( position.parent ) ) {
|
|
1508
|
+
return movePositionToTextNode( position.clone() );
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// When position is inside text node - break it and place new position between two text nodes.
|
|
1512
|
+
if ( position.parent.is( '$text' ) ) {
|
|
1513
|
+
position = breakTextNode( position );
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// Create fake element that will represent position, and will not be merged with other attributes.
|
|
1517
|
+
const fakePosition = this.createAttributeElement();
|
|
1518
|
+
fakePosition._priority = Number.POSITIVE_INFINITY;
|
|
1519
|
+
fakePosition.isSimilar = () => false;
|
|
1520
|
+
|
|
1521
|
+
// Insert fake element in position location.
|
|
1522
|
+
position.parent._insertChild( position.offset, fakePosition );
|
|
1523
|
+
|
|
1524
|
+
// Range around inserted fake attribute element.
|
|
1525
|
+
const wrapRange = new Range( position, position.getShiftedBy( 1 ) );
|
|
1526
|
+
|
|
1527
|
+
// Wrap fake element with attribute (it will also merge if possible).
|
|
1528
|
+
this.wrap( wrapRange, attribute );
|
|
1529
|
+
|
|
1530
|
+
// Remove fake element and place new position there.
|
|
1531
|
+
const newPosition = new Position( fakePosition.parent, fakePosition.index );
|
|
1532
|
+
fakePosition._remove();
|
|
1533
|
+
|
|
1534
|
+
// If position is placed between text nodes - merge them and return position inside.
|
|
1535
|
+
const nodeBefore = newPosition.nodeBefore;
|
|
1536
|
+
const nodeAfter = newPosition.nodeAfter;
|
|
1537
|
+
|
|
1538
|
+
if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
|
|
1539
|
+
return mergeTextNodes( nodeBefore, nodeAfter );
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// If position is next to text node - move position inside.
|
|
1543
|
+
return movePositionToTextNode( newPosition );
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/**
|
|
1547
|
+
* Wraps one {@link module:engine/view/attributeelement~AttributeElement AttributeElement} into another by
|
|
1548
|
+
* merging them if possible. When merging is possible - all attributes, styles and classes are moved from wrapper
|
|
1549
|
+
* element to element being wrapped.
|
|
1550
|
+
*
|
|
1551
|
+
* @private
|
|
1552
|
+
* @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
|
|
1553
|
+
* @param {module:engine/view/attributeelement~AttributeElement} toWrap AttributeElement to wrap using wrapper element.
|
|
1554
|
+
* @returns {Boolean} Returns `true` if elements are merged.
|
|
1555
|
+
*/
|
|
1556
|
+
_wrapAttributeElement( wrapper, toWrap ) {
|
|
1557
|
+
if ( !canBeJoined( wrapper, toWrap ) ) {
|
|
1558
|
+
return false;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// Can't merge if name or priority differs.
|
|
1562
|
+
if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) {
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// Check if attributes can be merged.
|
|
1567
|
+
for ( const key of wrapper.getAttributeKeys() ) {
|
|
1568
|
+
// Classes and styles should be checked separately.
|
|
1569
|
+
if ( key === 'class' || key === 'style' ) {
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// If some attributes are different we cannot wrap.
|
|
1574
|
+
if ( toWrap.hasAttribute( key ) && toWrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
|
|
1575
|
+
return false;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Check if styles can be merged.
|
|
1580
|
+
for ( const key of wrapper.getStyleNames() ) {
|
|
1581
|
+
if ( toWrap.hasStyle( key ) && toWrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
|
|
1582
|
+
return false;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Move all attributes/classes/styles from wrapper to wrapped AttributeElement.
|
|
1587
|
+
for ( const key of wrapper.getAttributeKeys() ) {
|
|
1588
|
+
// Classes and styles should be checked separately.
|
|
1589
|
+
if ( key === 'class' || key === 'style' ) {
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// Move only these attributes that are not present - other are similar.
|
|
1594
|
+
if ( !toWrap.hasAttribute( key ) ) {
|
|
1595
|
+
this.setAttribute( key, wrapper.getAttribute( key ), toWrap );
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
for ( const key of wrapper.getStyleNames() ) {
|
|
1600
|
+
if ( !toWrap.hasStyle( key ) ) {
|
|
1601
|
+
this.setStyle( key, wrapper.getStyle( key ), toWrap );
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
for ( const key of wrapper.getClassNames() ) {
|
|
1606
|
+
if ( !toWrap.hasClass( key ) ) {
|
|
1607
|
+
this.addClass( key, toWrap );
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
return true;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
/**
|
|
1615
|
+
* Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing
|
|
1616
|
+
* corresponding attributes, classes and styles. All attributes, classes and styles from wrapper should be present
|
|
1617
|
+
* inside element being unwrapped.
|
|
1618
|
+
*
|
|
1619
|
+
* @private
|
|
1620
|
+
* @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
|
|
1621
|
+
* @param {module:engine/view/attributeelement~AttributeElement} toUnwrap AttributeElement to unwrap using wrapper element.
|
|
1622
|
+
* @returns {Boolean} Returns `true` if elements are unwrapped.
|
|
1623
|
+
**/
|
|
1624
|
+
_unwrapAttributeElement( wrapper, toUnwrap ) {
|
|
1625
|
+
if ( !canBeJoined( wrapper, toUnwrap ) ) {
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// Can't unwrap if name or priority differs.
|
|
1630
|
+
if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) {
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// Check if AttributeElement has all wrapper attributes.
|
|
1635
|
+
for ( const key of wrapper.getAttributeKeys() ) {
|
|
1636
|
+
// Classes and styles should be checked separately.
|
|
1637
|
+
if ( key === 'class' || key === 'style' ) {
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// If some attributes are missing or different we cannot unwrap.
|
|
1642
|
+
if ( !toUnwrap.hasAttribute( key ) || toUnwrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
|
|
1643
|
+
return false;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// Check if AttributeElement has all wrapper classes.
|
|
1648
|
+
if ( !toUnwrap.hasClass( ...wrapper.getClassNames() ) ) {
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Check if AttributeElement has all wrapper styles.
|
|
1653
|
+
for ( const key of wrapper.getStyleNames() ) {
|
|
1654
|
+
// If some styles are missing or different we cannot unwrap.
|
|
1655
|
+
if ( !toUnwrap.hasStyle( key ) || toUnwrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Remove all wrapper's attributes from unwrapped element.
|
|
1661
|
+
for ( const key of wrapper.getAttributeKeys() ) {
|
|
1662
|
+
// Classes and styles should be checked separately.
|
|
1663
|
+
if ( key === 'class' || key === 'style' ) {
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
this.removeAttribute( key, toUnwrap );
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// Remove all wrapper's classes from unwrapped element.
|
|
1671
|
+
this.removeClass( Array.from( wrapper.getClassNames() ), toUnwrap );
|
|
1672
|
+
|
|
1673
|
+
// Remove all wrapper's styles from unwrapped element.
|
|
1674
|
+
this.removeStyle( Array.from( wrapper.getStyleNames() ), toUnwrap );
|
|
1675
|
+
|
|
1676
|
+
return true;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Helper function used by other `DowncastWriter` methods. Breaks attribute elements at the boundaries of given range.
|
|
1681
|
+
*
|
|
1682
|
+
* @private
|
|
1683
|
+
* @param {module:engine/view/range~Range} range Range which `start` and `end` positions will be used to break attributes.
|
|
1684
|
+
* @param {Boolean} [forceSplitText=false] If set to `true`, will break text nodes even if they are directly in container element.
|
|
1685
|
+
* This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
|
|
1686
|
+
* @returns {module:engine/view/range~Range} New range with located at break positions.
|
|
1687
|
+
*/
|
|
1688
|
+
_breakAttributesRange( range, forceSplitText = false ) {
|
|
1689
|
+
const rangeStart = range.start;
|
|
1690
|
+
const rangeEnd = range.end;
|
|
1691
|
+
|
|
1692
|
+
validateRangeContainer( range, this.document );
|
|
1693
|
+
|
|
1694
|
+
// Break at the collapsed position. Return new collapsed range.
|
|
1695
|
+
if ( range.isCollapsed ) {
|
|
1696
|
+
const position = this._breakAttributes( range.start, forceSplitText );
|
|
1697
|
+
|
|
1698
|
+
return new Range( position, position );
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const breakEnd = this._breakAttributes( rangeEnd, forceSplitText );
|
|
1702
|
+
const count = breakEnd.parent.childCount;
|
|
1703
|
+
const breakStart = this._breakAttributes( rangeStart, forceSplitText );
|
|
1704
|
+
|
|
1705
|
+
// Calculate new break end offset.
|
|
1706
|
+
breakEnd.offset += breakEnd.parent.childCount - count;
|
|
1707
|
+
|
|
1708
|
+
return new Range( breakStart, breakEnd );
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* Helper function used by other `DowncastWriter` methods. Breaks attribute elements at given position.
|
|
1713
|
+
*
|
|
1714
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
|
|
1715
|
+
* is placed inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
|
|
1716
|
+
*
|
|
1717
|
+
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
|
|
1718
|
+
* is placed inside {@link module:engine/view/uielement~UIElement UIElement}.
|
|
1719
|
+
*
|
|
1720
|
+
* @private
|
|
1721
|
+
* @param {module:engine/view/position~Position} position Position where to break attributes.
|
|
1722
|
+
* @param {Boolean} [forceSplitText=false] If set to `true`, will break text nodes even if they are directly in container element.
|
|
1723
|
+
* This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
|
|
1724
|
+
* @returns {module:engine/view/position~Position} New position after breaking the attributes.
|
|
1725
|
+
*/
|
|
1726
|
+
_breakAttributes( position, forceSplitText = false ) {
|
|
1727
|
+
const positionOffset = position.offset;
|
|
1728
|
+
const positionParent = position.parent;
|
|
1729
|
+
|
|
1730
|
+
// If position is placed inside EmptyElement - throw an exception as we cannot break inside.
|
|
1731
|
+
if ( position.parent.is( 'emptyElement' ) ) {
|
|
1732
|
+
/**
|
|
1733
|
+
* Cannot break an `EmptyElement` instance.
|
|
1734
|
+
*
|
|
1735
|
+
* This error is thrown if
|
|
1736
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
|
|
1737
|
+
* was executed in an incorrect position.
|
|
1738
|
+
*
|
|
1739
|
+
* @error view-writer-cannot-break-empty-element
|
|
1740
|
+
*/
|
|
1741
|
+
throw new CKEditorError( 'view-writer-cannot-break-empty-element', this.document );
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// If position is placed inside UIElement - throw an exception as we cannot break inside.
|
|
1745
|
+
if ( position.parent.is( 'uiElement' ) ) {
|
|
1746
|
+
/**
|
|
1747
|
+
* Cannot break a `UIElement` instance.
|
|
1748
|
+
*
|
|
1749
|
+
* This error is thrown if
|
|
1750
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
|
|
1751
|
+
* was executed in an incorrect position.
|
|
1752
|
+
*
|
|
1753
|
+
* @error view-writer-cannot-break-ui-element
|
|
1754
|
+
*/
|
|
1755
|
+
throw new CKEditorError( 'view-writer-cannot-break-ui-element', this.document );
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// If position is placed inside RawElement - throw an exception as we cannot break inside.
|
|
1759
|
+
if ( position.parent.is( 'rawElement' ) ) {
|
|
1760
|
+
/**
|
|
1761
|
+
* Cannot break a `RawElement` instance.
|
|
1762
|
+
*
|
|
1763
|
+
* This error is thrown if
|
|
1764
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
|
|
1765
|
+
* was executed in an incorrect position.
|
|
1766
|
+
*
|
|
1767
|
+
* @error view-writer-cannot-break-raw-element
|
|
1768
|
+
*/
|
|
1769
|
+
throw new CKEditorError( 'view-writer-cannot-break-raw-element', this.document );
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// There are no attributes to break and text nodes breaking is not forced.
|
|
1773
|
+
if ( !forceSplitText && positionParent.is( '$text' ) && isContainerOrFragment( positionParent.parent ) ) {
|
|
1774
|
+
return position.clone();
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// Position's parent is container, so no attributes to break.
|
|
1778
|
+
if ( isContainerOrFragment( positionParent ) ) {
|
|
1779
|
+
return position.clone();
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// Break text and start again in new position.
|
|
1783
|
+
if ( positionParent.is( '$text' ) ) {
|
|
1784
|
+
return this._breakAttributes( breakTextNode( position ), forceSplitText );
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
const length = positionParent.childCount;
|
|
1788
|
+
|
|
1789
|
+
// <p>foo<b><u>bar{}</u></b></p>
|
|
1790
|
+
// <p>foo<b><u>bar</u>[]</b></p>
|
|
1791
|
+
// <p>foo<b><u>bar</u></b>[]</p>
|
|
1792
|
+
if ( positionOffset == length ) {
|
|
1793
|
+
const newPosition = new Position( positionParent.parent, positionParent.index + 1 );
|
|
1794
|
+
|
|
1795
|
+
return this._breakAttributes( newPosition, forceSplitText );
|
|
1796
|
+
} else {
|
|
1797
|
+
// <p>foo<b><u>{}bar</u></b></p>
|
|
1798
|
+
// <p>foo<b>[]<u>bar</u></b></p>
|
|
1799
|
+
// <p>foo{}<b><u>bar</u></b></p>
|
|
1800
|
+
if ( positionOffset === 0 ) {
|
|
1801
|
+
const newPosition = new Position( positionParent.parent, positionParent.index );
|
|
1802
|
+
|
|
1803
|
+
return this._breakAttributes( newPosition, forceSplitText );
|
|
1804
|
+
}
|
|
1805
|
+
// <p>foo<b><u>b{}ar</u></b></p>
|
|
1806
|
+
// <p>foo<b><u>b[]ar</u></b></p>
|
|
1807
|
+
// <p>foo<b><u>b</u>[]<u>ar</u></b></p>
|
|
1808
|
+
// <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
|
|
1809
|
+
else {
|
|
1810
|
+
const offsetAfter = positionParent.index + 1;
|
|
1811
|
+
|
|
1812
|
+
// Break element.
|
|
1813
|
+
const clonedNode = positionParent._clone();
|
|
1814
|
+
|
|
1815
|
+
// Insert cloned node to position's parent node.
|
|
1816
|
+
positionParent.parent._insertChild( offsetAfter, clonedNode );
|
|
1817
|
+
this._addToClonedElementsGroup( clonedNode );
|
|
1818
|
+
|
|
1819
|
+
// Get nodes to move.
|
|
1820
|
+
const count = positionParent.childCount - positionOffset;
|
|
1821
|
+
const nodesToMove = positionParent._removeChildren( positionOffset, count );
|
|
1822
|
+
|
|
1823
|
+
// Move nodes to cloned node.
|
|
1824
|
+
clonedNode._appendChild( nodesToMove );
|
|
1825
|
+
|
|
1826
|
+
// Create new position to work on.
|
|
1827
|
+
const newPosition = new Position( positionParent.parent, offsetAfter );
|
|
1828
|
+
|
|
1829
|
+
return this._breakAttributes( newPosition, forceSplitText );
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/**
|
|
1835
|
+
* Stores the information that an {@link module:engine/view/attributeelement~AttributeElement attribute element} was
|
|
1836
|
+
* added to the tree. Saves the reference to the group in the given element and updates the group, so other elements
|
|
1837
|
+
* from the group now keep a reference to the given attribute element.
|
|
1838
|
+
*
|
|
1839
|
+
* The clones group can be obtained using {@link module:engine/view/attributeelement~AttributeElement#getElementsWithSameId}.
|
|
1840
|
+
*
|
|
1841
|
+
* Does nothing if added element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
|
|
1842
|
+
*
|
|
1843
|
+
* @private
|
|
1844
|
+
* @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to save.
|
|
1845
|
+
*/
|
|
1846
|
+
_addToClonedElementsGroup( element ) {
|
|
1847
|
+
// Add only if the element is in document tree.
|
|
1848
|
+
if ( !element.root.is( 'rootElement' ) ) {
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// Traverse the element's children recursively to find other attribute elements that also might got inserted.
|
|
1853
|
+
// The loop is at the beginning so we can make fast returns later in the code.
|
|
1854
|
+
if ( element.is( 'element' ) ) {
|
|
1855
|
+
for ( const child of element.getChildren() ) {
|
|
1856
|
+
this._addToClonedElementsGroup( child );
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
const id = element.id;
|
|
1861
|
+
|
|
1862
|
+
if ( !id ) {
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
let group = this._cloneGroups.get( id );
|
|
1867
|
+
|
|
1868
|
+
if ( !group ) {
|
|
1869
|
+
group = new Set();
|
|
1870
|
+
this._cloneGroups.set( id, group );
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
group.add( element );
|
|
1874
|
+
element._clonesGroup = group;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
/**
|
|
1878
|
+
* Removes all the information about the given {@link module:engine/view/attributeelement~AttributeElement attribute element}
|
|
1879
|
+
* from its clones group.
|
|
1880
|
+
*
|
|
1881
|
+
* Keep in mind, that the element will still keep a reference to the group (but the group will not keep a reference to it).
|
|
1882
|
+
* This allows to reference the whole group even if the element was already removed from the tree.
|
|
1883
|
+
*
|
|
1884
|
+
* Does nothing if the element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
|
|
1885
|
+
*
|
|
1886
|
+
* @private
|
|
1887
|
+
* @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to remove.
|
|
1888
|
+
*/
|
|
1889
|
+
_removeFromClonedElementsGroup( element ) {
|
|
1890
|
+
// Traverse the element's children recursively to find other attribute elements that also got removed.
|
|
1891
|
+
// The loop is at the beginning so we can make fast returns later in the code.
|
|
1892
|
+
if ( element.is( 'element' ) ) {
|
|
1893
|
+
for ( const child of element.getChildren() ) {
|
|
1894
|
+
this._removeFromClonedElementsGroup( child );
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
const id = element.id;
|
|
1899
|
+
|
|
1900
|
+
if ( !id ) {
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const group = this._cloneGroups.get( id );
|
|
1905
|
+
|
|
1906
|
+
if ( !group ) {
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
group.delete( element );
|
|
1911
|
+
// Not removing group from element on purpose!
|
|
1912
|
+
// If other parts of code have reference to this element, they will be able to get references to other elements from the group.
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// Helper function for `view.writer.wrap`. Checks if given element has any children that are not ui elements.
|
|
1917
|
+
function _hasNonUiChildren( parent ) {
|
|
1918
|
+
return Array.from( parent.getChildren() ).some( child => !child.is( 'uiElement' ) );
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/**
|
|
1922
|
+
* The `attribute` passed to {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#wrap()`}
|
|
1923
|
+
* must be an instance of {@link module:engine/view/attributeelement~AttributeElement `AttributeElement`}.
|
|
1924
|
+
*
|
|
1925
|
+
* @error view-writer-wrap-invalid-attribute
|
|
1926
|
+
*/
|
|
1927
|
+
|
|
1928
|
+
// Returns first parent container of specified {@link module:engine/view/position~Position Position}.
|
|
1929
|
+
// Position's parent node is checked as first, then next parents are checked.
|
|
1930
|
+
// Note that {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
|
|
1931
|
+
//
|
|
1932
|
+
// @param {module:engine/view/position~Position} position Position used as a start point to locate parent container.
|
|
1933
|
+
// @returns {module:engine/view/containerelement~ContainerElement|module:engine/view/documentfragment~DocumentFragment|undefined}
|
|
1934
|
+
// Parent container element or `undefined` if container is not found.
|
|
1935
|
+
function getParentContainer( position ) {
|
|
1936
|
+
let parent = position.parent;
|
|
1937
|
+
|
|
1938
|
+
while ( !isContainerOrFragment( parent ) ) {
|
|
1939
|
+
if ( !parent ) {
|
|
1940
|
+
return undefined;
|
|
1941
|
+
}
|
|
1942
|
+
parent = parent.parent;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
return parent;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// Checks if first {@link module:engine/view/attributeelement~AttributeElement AttributeElement} provided to the function
|
|
1949
|
+
// can be wrapped outside second element. It is done by comparing elements'
|
|
1950
|
+
// {@link module:engine/view/attributeelement~AttributeElement#priority priorities}, if both have same priority
|
|
1951
|
+
// {@link module:engine/view/element~Element#getIdentity identities} are compared.
|
|
1952
|
+
//
|
|
1953
|
+
// @param {module:engine/view/attributeelement~AttributeElement} a
|
|
1954
|
+
// @param {module:engine/view/attributeelement~AttributeElement} b
|
|
1955
|
+
// @returns {Boolean}
|
|
1956
|
+
function shouldABeOutsideB( a, b ) {
|
|
1957
|
+
if ( a.priority < b.priority ) {
|
|
1958
|
+
return true;
|
|
1959
|
+
} else if ( a.priority > b.priority ) {
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// When priorities are equal and names are different - use identities.
|
|
1964
|
+
return a.getIdentity() < b.getIdentity();
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// Returns new position that is moved to near text node. Returns same position if there is no text node before of after
|
|
1968
|
+
// specified position.
|
|
1969
|
+
//
|
|
1970
|
+
// <p>foo[]</p> -> <p>foo{}</p>
|
|
1971
|
+
// <p>[]foo</p> -> <p>{}foo</p>
|
|
1972
|
+
//
|
|
1973
|
+
// @param {module:engine/view/position~Position} position
|
|
1974
|
+
// @returns {module:engine/view/position~Position} Position located inside text node or same position if there is no text nodes
|
|
1975
|
+
// before or after position location.
|
|
1976
|
+
function movePositionToTextNode( position ) {
|
|
1977
|
+
const nodeBefore = position.nodeBefore;
|
|
1978
|
+
|
|
1979
|
+
if ( nodeBefore && nodeBefore.is( '$text' ) ) {
|
|
1980
|
+
return new Position( nodeBefore, nodeBefore.data.length );
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
const nodeAfter = position.nodeAfter;
|
|
1984
|
+
|
|
1985
|
+
if ( nodeAfter && nodeAfter.is( '$text' ) ) {
|
|
1986
|
+
return new Position( nodeAfter, 0 );
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
return position;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// Breaks text node into two text nodes when possible.
|
|
1993
|
+
//
|
|
1994
|
+
// <p>foo{}bar</p> -> <p>foo[]bar</p>
|
|
1995
|
+
// <p>{}foobar</p> -> <p>[]foobar</p>
|
|
1996
|
+
// <p>foobar{}</p> -> <p>foobar[]</p>
|
|
1997
|
+
//
|
|
1998
|
+
// @param {module:engine/view/position~Position} position Position that need to be placed inside text node.
|
|
1999
|
+
// @returns {module:engine/view/position~Position} New position after breaking text node.
|
|
2000
|
+
function breakTextNode( position ) {
|
|
2001
|
+
if ( position.offset == position.parent.data.length ) {
|
|
2002
|
+
return new Position( position.parent.parent, position.parent.index + 1 );
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
if ( position.offset === 0 ) {
|
|
2006
|
+
return new Position( position.parent.parent, position.parent.index );
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// Get part of the text that need to be moved.
|
|
2010
|
+
const textToMove = position.parent.data.slice( position.offset );
|
|
2011
|
+
|
|
2012
|
+
// Leave rest of the text in position's parent.
|
|
2013
|
+
position.parent._data = position.parent.data.slice( 0, position.offset );
|
|
2014
|
+
|
|
2015
|
+
// Insert new text node after position's parent text node.
|
|
2016
|
+
position.parent.parent._insertChild( position.parent.index + 1, new Text( position.root.document, textToMove ) );
|
|
2017
|
+
|
|
2018
|
+
// Return new position between two newly created text nodes.
|
|
2019
|
+
return new Position( position.parent.parent, position.parent.index + 1 );
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// Merges two text nodes into first node. Removes second node and returns merge position.
|
|
2023
|
+
//
|
|
2024
|
+
// @param {module:engine/view/text~Text} t1 First text node to merge. Data from second text node will be moved at the end of
|
|
2025
|
+
// this text node.
|
|
2026
|
+
// @param {module:engine/view/text~Text} t2 Second text node to merge. This node will be removed after merging.
|
|
2027
|
+
// @returns {module:engine/view/position~Position} Position after merging text nodes.
|
|
2028
|
+
function mergeTextNodes( t1, t2 ) {
|
|
2029
|
+
// Merge text data into first text node and remove second one.
|
|
2030
|
+
const nodeBeforeLength = t1.data.length;
|
|
2031
|
+
t1._data += t2.data;
|
|
2032
|
+
t2._remove();
|
|
2033
|
+
|
|
2034
|
+
return new Position( t1, nodeBeforeLength );
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Checks if provided nodes are valid to insert.
|
|
2038
|
+
//
|
|
2039
|
+
// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
|
|
2040
|
+
// contains instances that are not supported ones (see error description for valid ones.
|
|
2041
|
+
//
|
|
2042
|
+
// @param Iterable.<module:engine/view/text~Text|module:engine/view/element~Element> nodes
|
|
2043
|
+
// @param {Object} errorContext
|
|
2044
|
+
function validateNodesToInsert( nodes, errorContext ) {
|
|
2045
|
+
for ( const node of nodes ) {
|
|
2046
|
+
if ( !validNodesToInsert.some( ( validNode => node instanceof validNode ) ) ) { // eslint-disable-line no-use-before-define
|
|
2047
|
+
/**
|
|
2048
|
+
* One of the nodes to be inserted is of an invalid type.
|
|
2049
|
+
*
|
|
2050
|
+
* Nodes to be inserted with {@link module:engine/view/downcastwriter~DowncastWriter#insert `DowncastWriter#insert()`} should be
|
|
2051
|
+
* of the following types:
|
|
2052
|
+
*
|
|
2053
|
+
* * {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
|
|
2054
|
+
* * {@link module:engine/view/containerelement~ContainerElement ContainerElement},
|
|
2055
|
+
* * {@link module:engine/view/emptyelement~EmptyElement EmptyElement},
|
|
2056
|
+
* * {@link module:engine/view/uielement~UIElement UIElement},
|
|
2057
|
+
* * {@link module:engine/view/rawelement~RawElement RawElement},
|
|
2058
|
+
* * {@link module:engine/view/text~Text Text}.
|
|
2059
|
+
*
|
|
2060
|
+
* @error view-writer-insert-invalid-node-type
|
|
2061
|
+
*/
|
|
2062
|
+
throw new CKEditorError( 'view-writer-insert-invalid-node-type', errorContext );
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
if ( !node.is( '$text' ) ) {
|
|
2066
|
+
validateNodesToInsert( node.getChildren(), errorContext );
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
const validNodesToInsert = [ Text, AttributeElement, ContainerElement, EmptyElement, RawElement, UIElement ];
|
|
2072
|
+
|
|
2073
|
+
// Checks if node is ContainerElement or DocumentFragment, because in most cases they should be treated the same way.
|
|
2074
|
+
//
|
|
2075
|
+
// @param {module:engine/view/node~Node} node
|
|
2076
|
+
// @returns {Boolean} Returns `true` if node is instance of ContainerElement or DocumentFragment.
|
|
2077
|
+
function isContainerOrFragment( node ) {
|
|
2078
|
+
return node && ( node.is( 'containerElement' ) || node.is( 'documentFragment' ) );
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// Checks if {@link module:engine/view/range~Range#start range start} and {@link module:engine/view/range~Range#end range end} are placed
|
|
2082
|
+
// inside same {@link module:engine/view/containerelement~ContainerElement container element}.
|
|
2083
|
+
// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
|
|
2084
|
+
//
|
|
2085
|
+
// @param {module:engine/view/range~Range} range
|
|
2086
|
+
// @param {Object} errorContext
|
|
2087
|
+
function validateRangeContainer( range, errorContext ) {
|
|
2088
|
+
const startContainer = getParentContainer( range.start );
|
|
2089
|
+
const endContainer = getParentContainer( range.end );
|
|
2090
|
+
|
|
2091
|
+
if ( !startContainer || !endContainer || startContainer !== endContainer ) {
|
|
2092
|
+
/**
|
|
2093
|
+
* The container of the given range is invalid.
|
|
2094
|
+
*
|
|
2095
|
+
* This may happen if {@link module:engine/view/range~Range#start range start} and
|
|
2096
|
+
* {@link module:engine/view/range~Range#end range end} positions are not placed inside the same container element or
|
|
2097
|
+
* a parent container for these positions cannot be found.
|
|
2098
|
+
*
|
|
2099
|
+
* Methods like {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#remove()`},
|
|
2100
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#clean()`},
|
|
2101
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#wrap()`},
|
|
2102
|
+
* {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#unwrap()`} need to be called
|
|
2103
|
+
* on a range that has its start and end positions located in the same container element. Both positions can be
|
|
2104
|
+
* nested within other elements (e.g. an attribute element) but the closest container ancestor must be the same.
|
|
2105
|
+
*
|
|
2106
|
+
* @error view-writer-invalid-range-container
|
|
2107
|
+
*/
|
|
2108
|
+
throw new CKEditorError( 'view-writer-invalid-range-container', errorContext );
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// Checks if two attribute elements can be joined together. Elements can be joined together if, and only if
|
|
2113
|
+
// they do not have ids specified.
|
|
2114
|
+
//
|
|
2115
|
+
// @private
|
|
2116
|
+
// @param {module:engine/view/element~Element} a
|
|
2117
|
+
// @param {module:engine/view/element~Element} b
|
|
2118
|
+
// @returns {Boolean}
|
|
2119
|
+
function canBeJoined( a, b ) {
|
|
2120
|
+
return a.id === null && b.id === null;
|
|
2121
|
+
}
|