@ckeditor/ckeditor5-engine 47.6.1 → 48.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/{src → dist}/engineconfig.d.ts +6 -15
- package/dist/index-editor.css +38 -15
- package/dist/index.css +37 -37
- package/dist/index.css.map +1 -1
- package/{src → dist}/index.d.ts +0 -1
- package/dist/index.js +588 -94
- package/dist/index.js.map +1 -1
- package/{src → dist}/model/model.d.ts +10 -4
- package/{src → dist}/model/selection.d.ts +1 -1
- package/{src → dist}/view/downcastwriter.d.ts +3 -2
- package/{src → dist}/view/element.d.ts +2 -2
- package/{src → dist}/view/matcher.d.ts +4 -2
- package/dist/view/styles/background.d.ts +18 -0
- package/{src → dist}/view/styles/border.d.ts +0 -12
- package/{src → dist}/view/styles/margin.d.ts +0 -13
- package/{src → dist}/view/styles/padding.d.ts +0 -13
- package/{src → dist}/view/styles/utils.d.ts +12 -0
- package/package.json +20 -39
- package/src/controller/datacontroller.js +0 -522
- package/src/controller/editingcontroller.js +0 -181
- package/src/conversion/conversion.js +0 -606
- package/src/conversion/conversionhelpers.js +0 -33
- package/src/conversion/downcastdispatcher.js +0 -563
- package/src/conversion/downcasthelpers.js +0 -2160
- package/src/conversion/mapper.js +0 -1050
- package/src/conversion/modelconsumable.js +0 -331
- package/src/conversion/upcastdispatcher.js +0 -470
- package/src/conversion/upcasthelpers.js +0 -952
- package/src/conversion/viewconsumable.js +0 -541
- package/src/dataprocessor/basichtmlwriter.js +0 -22
- package/src/dataprocessor/dataprocessor.js +0 -5
- package/src/dataprocessor/htmldataprocessor.js +0 -107
- package/src/dataprocessor/htmlwriter.js +0 -5
- package/src/dataprocessor/xmldataprocessor.js +0 -127
- package/src/dev-utils/model.js +0 -396
- package/src/dev-utils/operationreplayer.js +0 -116
- package/src/dev-utils/utils.js +0 -122
- package/src/dev-utils/view.js +0 -990
- package/src/engineconfig.js +0 -5
- package/src/index.js +0 -134
- package/src/legacyerrors.js +0 -17
- package/src/model/batch.js +0 -98
- package/src/model/differ.js +0 -1288
- package/src/model/document.js +0 -398
- package/src/model/documentfragment.js +0 -332
- package/src/model/documentselection.js +0 -1026
- package/src/model/element.js +0 -323
- package/src/model/history.js +0 -206
- package/src/model/item.js +0 -5
- package/src/model/liveposition.js +0 -93
- package/src/model/liverange.js +0 -121
- package/src/model/markercollection.js +0 -436
- package/src/model/model.js +0 -866
- package/src/model/node.js +0 -371
- package/src/model/nodelist.js +0 -244
- package/src/model/operation/attributeoperation.js +0 -172
- package/src/model/operation/detachoperation.js +0 -87
- package/src/model/operation/insertoperation.js +0 -153
- package/src/model/operation/markeroperation.js +0 -136
- package/src/model/operation/mergeoperation.js +0 -184
- package/src/model/operation/moveoperation.js +0 -179
- package/src/model/operation/nooperation.js +0 -48
- package/src/model/operation/operation.js +0 -78
- package/src/model/operation/operationfactory.js +0 -44
- package/src/model/operation/renameoperation.js +0 -128
- package/src/model/operation/rootattributeoperation.js +0 -173
- package/src/model/operation/rootoperation.js +0 -106
- package/src/model/operation/splitoperation.js +0 -214
- package/src/model/operation/transform.js +0 -2211
- package/src/model/operation/utils.js +0 -217
- package/src/model/position.js +0 -1041
- package/src/model/range.js +0 -880
- package/src/model/rootelement.js +0 -82
- package/src/model/schema.js +0 -1542
- package/src/model/selection.js +0 -814
- package/src/model/text.js +0 -92
- package/src/model/textproxy.js +0 -202
- package/src/model/treewalker.js +0 -313
- package/src/model/typecheckable.js +0 -16
- package/src/model/utils/autoparagraphing.js +0 -63
- package/src/model/utils/deletecontent.js +0 -509
- package/src/model/utils/getselectedcontent.js +0 -126
- package/src/model/utils/insertcontent.js +0 -750
- package/src/model/utils/insertobject.js +0 -135
- package/src/model/utils/modifyselection.js +0 -187
- package/src/model/utils/selection-post-fixer.js +0 -264
- package/src/model/writer.js +0 -1318
- package/src/view/attributeelement.js +0 -220
- package/src/view/containerelement.js +0 -91
- package/src/view/datatransfer.js +0 -106
- package/src/view/document.js +0 -139
- package/src/view/documentfragment.js +0 -251
- package/src/view/documentselection.js +0 -270
- package/src/view/domconverter.js +0 -1661
- package/src/view/downcastwriter.js +0 -1589
- package/src/view/editableelement.js +0 -74
- package/src/view/element.js +0 -1053
- package/src/view/elementdefinition.js +0 -5
- package/src/view/emptyelement.js +0 -83
- package/src/view/filler.js +0 -161
- package/src/view/item.js +0 -5
- package/src/view/matcher.js +0 -437
- package/src/view/node.js +0 -238
- package/src/view/observer/arrowkeysobserver.js +0 -40
- package/src/view/observer/bubblingemittermixin.js +0 -215
- package/src/view/observer/bubblingeventinfo.js +0 -49
- package/src/view/observer/clickobserver.js +0 -26
- package/src/view/observer/compositionobserver.js +0 -64
- package/src/view/observer/domeventdata.js +0 -63
- package/src/view/observer/domeventobserver.js +0 -81
- package/src/view/observer/fakeselectionobserver.js +0 -95
- package/src/view/observer/focusobserver.js +0 -166
- package/src/view/observer/inputobserver.js +0 -236
- package/src/view/observer/keyobserver.js +0 -36
- package/src/view/observer/mouseobserver.js +0 -26
- package/src/view/observer/mutationobserver.js +0 -219
- package/src/view/observer/observer.js +0 -92
- package/src/view/observer/pointerobserver.js +0 -26
- package/src/view/observer/selectionobserver.js +0 -318
- package/src/view/observer/tabobserver.js +0 -42
- package/src/view/observer/touchobserver.js +0 -26
- package/src/view/placeholder.js +0 -285
- package/src/view/position.js +0 -341
- package/src/view/range.js +0 -451
- package/src/view/rawelement.js +0 -115
- package/src/view/renderer.js +0 -1148
- package/src/view/rooteditableelement.js +0 -78
- package/src/view/selection.js +0 -594
- package/src/view/styles/background.d.ts +0 -33
- package/src/view/styles/background.js +0 -74
- package/src/view/styles/border.js +0 -316
- package/src/view/styles/margin.js +0 -34
- package/src/view/styles/padding.js +0 -34
- package/src/view/styles/utils.js +0 -219
- package/src/view/stylesmap.js +0 -941
- package/src/view/text.js +0 -110
- package/src/view/textproxy.js +0 -136
- package/src/view/tokenlist.js +0 -194
- package/src/view/treewalker.js +0 -389
- package/src/view/typecheckable.js +0 -19
- package/src/view/uielement.js +0 -194
- package/src/view/upcastwriter.js +0 -363
- package/src/view/view.js +0 -579
- package/theme/placeholder.css +0 -36
- package/theme/renderer.css +0 -9
- /package/{src → dist}/controller/datacontroller.d.ts +0 -0
- /package/{src → dist}/controller/editingcontroller.d.ts +0 -0
- /package/{src → dist}/conversion/conversion.d.ts +0 -0
- /package/{src → dist}/conversion/conversionhelpers.d.ts +0 -0
- /package/{src → dist}/conversion/downcastdispatcher.d.ts +0 -0
- /package/{src → dist}/conversion/downcasthelpers.d.ts +0 -0
- /package/{src → dist}/conversion/mapper.d.ts +0 -0
- /package/{src → dist}/conversion/modelconsumable.d.ts +0 -0
- /package/{src → dist}/conversion/upcastdispatcher.d.ts +0 -0
- /package/{src → dist}/conversion/upcasthelpers.d.ts +0 -0
- /package/{src → dist}/conversion/viewconsumable.d.ts +0 -0
- /package/{src → dist}/dataprocessor/basichtmlwriter.d.ts +0 -0
- /package/{src → dist}/dataprocessor/dataprocessor.d.ts +0 -0
- /package/{src → dist}/dataprocessor/htmldataprocessor.d.ts +0 -0
- /package/{src → dist}/dataprocessor/htmlwriter.d.ts +0 -0
- /package/{src → dist}/dataprocessor/xmldataprocessor.d.ts +0 -0
- /package/{src → dist}/dev-utils/model.d.ts +0 -0
- /package/{src → dist}/dev-utils/operationreplayer.d.ts +0 -0
- /package/{src → dist}/dev-utils/utils.d.ts +0 -0
- /package/{src → dist}/dev-utils/view.d.ts +0 -0
- /package/{src → dist}/legacyerrors.d.ts +0 -0
- /package/{src → dist}/model/batch.d.ts +0 -0
- /package/{src → dist}/model/differ.d.ts +0 -0
- /package/{src → dist}/model/document.d.ts +0 -0
- /package/{src → dist}/model/documentfragment.d.ts +0 -0
- /package/{src → dist}/model/documentselection.d.ts +0 -0
- /package/{src → dist}/model/element.d.ts +0 -0
- /package/{src → dist}/model/history.d.ts +0 -0
- /package/{src → dist}/model/item.d.ts +0 -0
- /package/{src → dist}/model/liveposition.d.ts +0 -0
- /package/{src → dist}/model/liverange.d.ts +0 -0
- /package/{src → dist}/model/markercollection.d.ts +0 -0
- /package/{src → dist}/model/node.d.ts +0 -0
- /package/{src → dist}/model/nodelist.d.ts +0 -0
- /package/{src → dist}/model/operation/attributeoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/detachoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/insertoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/markeroperation.d.ts +0 -0
- /package/{src → dist}/model/operation/mergeoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/moveoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/nooperation.d.ts +0 -0
- /package/{src → dist}/model/operation/operation.d.ts +0 -0
- /package/{src → dist}/model/operation/operationfactory.d.ts +0 -0
- /package/{src → dist}/model/operation/renameoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/rootattributeoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/rootoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/splitoperation.d.ts +0 -0
- /package/{src → dist}/model/operation/transform.d.ts +0 -0
- /package/{src → dist}/model/operation/utils.d.ts +0 -0
- /package/{src → dist}/model/position.d.ts +0 -0
- /package/{src → dist}/model/range.d.ts +0 -0
- /package/{src → dist}/model/rootelement.d.ts +0 -0
- /package/{src → dist}/model/schema.d.ts +0 -0
- /package/{src → dist}/model/text.d.ts +0 -0
- /package/{src → dist}/model/textproxy.d.ts +0 -0
- /package/{src → dist}/model/treewalker.d.ts +0 -0
- /package/{src → dist}/model/typecheckable.d.ts +0 -0
- /package/{src → dist}/model/utils/autoparagraphing.d.ts +0 -0
- /package/{src → dist}/model/utils/deletecontent.d.ts +0 -0
- /package/{src → dist}/model/utils/getselectedcontent.d.ts +0 -0
- /package/{src → dist}/model/utils/insertcontent.d.ts +0 -0
- /package/{src → dist}/model/utils/insertobject.d.ts +0 -0
- /package/{src → dist}/model/utils/modifyselection.d.ts +0 -0
- /package/{src → dist}/model/utils/selection-post-fixer.d.ts +0 -0
- /package/{src → dist}/model/writer.d.ts +0 -0
- /package/{src → dist}/view/attributeelement.d.ts +0 -0
- /package/{src → dist}/view/containerelement.d.ts +0 -0
- /package/{src → dist}/view/datatransfer.d.ts +0 -0
- /package/{src → dist}/view/document.d.ts +0 -0
- /package/{src → dist}/view/documentfragment.d.ts +0 -0
- /package/{src → dist}/view/documentselection.d.ts +0 -0
- /package/{src → dist}/view/domconverter.d.ts +0 -0
- /package/{src → dist}/view/editableelement.d.ts +0 -0
- /package/{src → dist}/view/elementdefinition.d.ts +0 -0
- /package/{src → dist}/view/emptyelement.d.ts +0 -0
- /package/{src → dist}/view/filler.d.ts +0 -0
- /package/{src → dist}/view/item.d.ts +0 -0
- /package/{src → dist}/view/node.d.ts +0 -0
- /package/{src → dist}/view/observer/arrowkeysobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/bubblingemittermixin.d.ts +0 -0
- /package/{src → dist}/view/observer/bubblingeventinfo.d.ts +0 -0
- /package/{src → dist}/view/observer/clickobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/compositionobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/domeventdata.d.ts +0 -0
- /package/{src → dist}/view/observer/domeventobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/fakeselectionobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/focusobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/inputobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/keyobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/mouseobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/mutationobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/observer.d.ts +0 -0
- /package/{src → dist}/view/observer/pointerobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/selectionobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/tabobserver.d.ts +0 -0
- /package/{src → dist}/view/observer/touchobserver.d.ts +0 -0
- /package/{src → dist}/view/placeholder.d.ts +0 -0
- /package/{src → dist}/view/position.d.ts +0 -0
- /package/{src → dist}/view/range.d.ts +0 -0
- /package/{src → dist}/view/rawelement.d.ts +0 -0
- /package/{src → dist}/view/renderer.d.ts +0 -0
- /package/{src → dist}/view/rooteditableelement.d.ts +0 -0
- /package/{src → dist}/view/selection.d.ts +0 -0
- /package/{src → dist}/view/stylesmap.d.ts +0 -0
- /package/{src → dist}/view/text.d.ts +0 -0
- /package/{src → dist}/view/textproxy.d.ts +0 -0
- /package/{src → dist}/view/tokenlist.d.ts +0 -0
- /package/{src → dist}/view/treewalker.d.ts +0 -0
- /package/{src → dist}/view/typecheckable.d.ts +0 -0
- /package/{src → dist}/view/uielement.d.ts +0 -0
- /package/{src → dist}/view/upcastwriter.d.ts +0 -0
- /package/{src → dist}/view/view.d.ts +0 -0
package/src/conversion/mapper.js
DELETED
|
@@ -1,1050 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* @module engine/conversion/mapper
|
|
7
|
-
*/
|
|
8
|
-
import { ModelPosition } from '../model/position.js';
|
|
9
|
-
import { ModelRange } from '../model/range.js';
|
|
10
|
-
import { ViewPosition } from '../view/position.js';
|
|
11
|
-
import { ViewRange } from '../view/range.js';
|
|
12
|
-
import { CKEditorError, EmitterMixin } from '@ckeditor/ckeditor5-utils';
|
|
13
|
-
/**
|
|
14
|
-
* Maps elements, positions and markers between the {@link module:engine/view/document~ViewDocument view} and
|
|
15
|
-
* the {@link module:engine/model/model model}.
|
|
16
|
-
*
|
|
17
|
-
* The instance of the Mapper used for the editing pipeline is available in
|
|
18
|
-
* {@link module:engine/controller/editingcontroller~EditingController#mapper `editor.editing.mapper`}.
|
|
19
|
-
*
|
|
20
|
-
* Mapper uses bound elements to find corresponding elements and positions, so, to get proper results,
|
|
21
|
-
* all model elements should be {@link module:engine/conversion/mapper~Mapper#bindElements bound}.
|
|
22
|
-
*
|
|
23
|
-
* To map the complex model to/from view relations, you may provide custom callbacks for the
|
|
24
|
-
* {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition modelToViewPosition event} and
|
|
25
|
-
* {@link module:engine/conversion/mapper~Mapper#event:viewToModelPosition viewToModelPosition event} that are fired whenever
|
|
26
|
-
* a position mapping request occurs.
|
|
27
|
-
* Those events are fired by the {@link module:engine/conversion/mapper~Mapper#toViewPosition toViewPosition}
|
|
28
|
-
* and {@link module:engine/conversion/mapper~Mapper#toModelPosition toModelPosition} methods. `Mapper` adds its own default callbacks
|
|
29
|
-
* with `'lowest'` priority. To override default `Mapper` mapping, add custom callback with higher priority and
|
|
30
|
-
* stop the event.
|
|
31
|
-
*/
|
|
32
|
-
export class Mapper extends /* #__PURE__ */ EmitterMixin() {
|
|
33
|
-
/**
|
|
34
|
-
* Model element to view element mapping.
|
|
35
|
-
*/
|
|
36
|
-
_modelToViewMapping = new WeakMap();
|
|
37
|
-
/**
|
|
38
|
-
* View element to model element mapping.
|
|
39
|
-
*/
|
|
40
|
-
_viewToModelMapping = new WeakMap();
|
|
41
|
-
/**
|
|
42
|
-
* A map containing callbacks between view element names and functions evaluating length of view elements
|
|
43
|
-
* in model.
|
|
44
|
-
*/
|
|
45
|
-
_viewToModelLengthCallbacks = new Map();
|
|
46
|
-
/**
|
|
47
|
-
* Model marker name to view elements mapping.
|
|
48
|
-
*
|
|
49
|
-
* Keys are `String`s while values are `Set`s with {@link module:engine/view/element~ViewElement view elements}.
|
|
50
|
-
* One marker (name) can be mapped to multiple elements.
|
|
51
|
-
*/
|
|
52
|
-
_markerNameToElements = new Map();
|
|
53
|
-
/**
|
|
54
|
-
* View element to model marker names mapping.
|
|
55
|
-
*
|
|
56
|
-
* This is reverse to {@link ~Mapper#_markerNameToElements} map.
|
|
57
|
-
*/
|
|
58
|
-
_elementToMarkerNames = new Map();
|
|
59
|
-
/**
|
|
60
|
-
* The map of removed view elements with their current root (used for deferred unbinding).
|
|
61
|
-
*/
|
|
62
|
-
_deferredBindingRemovals = new Map();
|
|
63
|
-
/**
|
|
64
|
-
* Stores marker names of markers which have changed due to unbinding a view element (so it is assumed that the view element
|
|
65
|
-
* has been removed, moved or renamed).
|
|
66
|
-
*/
|
|
67
|
-
_unboundMarkerNames = new Set();
|
|
68
|
-
/**
|
|
69
|
-
* Manages dynamic cache for the `Mapper` to improve the performance.
|
|
70
|
-
*/
|
|
71
|
-
_cache = new MapperCache();
|
|
72
|
-
/**
|
|
73
|
-
* Creates an instance of the mapper.
|
|
74
|
-
*/
|
|
75
|
-
constructor() {
|
|
76
|
-
super();
|
|
77
|
-
// Default mapper algorithm for mapping model position to view position.
|
|
78
|
-
this.on('modelToViewPosition', (evt, data) => {
|
|
79
|
-
if (data.viewPosition) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const viewContainer = this._modelToViewMapping.get(data.modelPosition.parent);
|
|
83
|
-
if (!viewContainer) {
|
|
84
|
-
/**
|
|
85
|
-
* A model position could not be mapped to the view because the parent of the model position
|
|
86
|
-
* does not have a mapped view element (might have not been converted yet or it has no converter).
|
|
87
|
-
*
|
|
88
|
-
* Make sure that the model element is correctly converted to the view.
|
|
89
|
-
*
|
|
90
|
-
* @error mapping-model-position-view-parent-not-found
|
|
91
|
-
*/
|
|
92
|
-
throw new CKEditorError('mapping-model-position-view-parent-not-found', this, { modelPosition: data.modelPosition });
|
|
93
|
-
}
|
|
94
|
-
data.viewPosition = this.findPositionIn(viewContainer, data.modelPosition.offset);
|
|
95
|
-
}, { priority: 'low' });
|
|
96
|
-
// Default mapper algorithm for mapping view position to model position.
|
|
97
|
-
this.on('viewToModelPosition', (evt, data) => {
|
|
98
|
-
if (data.modelPosition) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const viewBlock = this.findMappedViewAncestor(data.viewPosition);
|
|
102
|
-
const modelParent = this._viewToModelMapping.get(viewBlock);
|
|
103
|
-
const modelOffset = this._toModelOffset(data.viewPosition.parent, data.viewPosition.offset, viewBlock);
|
|
104
|
-
data.modelPosition = ModelPosition._createAt(modelParent, modelOffset);
|
|
105
|
-
}, { priority: 'low' });
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Marks model and view elements as corresponding. Corresponding elements can be retrieved by using
|
|
109
|
-
* the {@link module:engine/conversion/mapper~Mapper#toModelElement toModelElement} and
|
|
110
|
-
* {@link module:engine/conversion/mapper~Mapper#toViewElement toViewElement} methods.
|
|
111
|
-
* The information that elements are bound is also used to translate positions.
|
|
112
|
-
*
|
|
113
|
-
* @param modelElement Model element.
|
|
114
|
-
* @param viewElement View element.
|
|
115
|
-
*/
|
|
116
|
-
bindElements(modelElement, viewElement) {
|
|
117
|
-
this._modelToViewMapping.set(modelElement, viewElement);
|
|
118
|
-
this._viewToModelMapping.set(viewElement, modelElement);
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Unbinds the given {@link module:engine/view/element~ViewElement view element} from the map.
|
|
122
|
-
*
|
|
123
|
-
* **Note:** view-to-model binding will be removed, if it existed. However, corresponding model-to-view binding
|
|
124
|
-
* will be removed only if model element is still bound to the passed `viewElement`.
|
|
125
|
-
*
|
|
126
|
-
* This behavior allows for re-binding model element to another view element without fear of losing the new binding
|
|
127
|
-
* when the previously bound view element is unbound.
|
|
128
|
-
*
|
|
129
|
-
* @param viewElement View element to unbind.
|
|
130
|
-
* @param options The options object.
|
|
131
|
-
* @param options.defer Controls whether the binding should be removed immediately or deferred until a
|
|
132
|
-
* {@link #flushDeferredBindings `flushDeferredBindings()`} call.
|
|
133
|
-
*/
|
|
134
|
-
unbindViewElement(viewElement, options = {}) {
|
|
135
|
-
const modelElement = this.toModelElement(viewElement);
|
|
136
|
-
if (this._elementToMarkerNames.has(viewElement)) {
|
|
137
|
-
for (const markerName of this._elementToMarkerNames.get(viewElement)) {
|
|
138
|
-
this._unboundMarkerNames.add(markerName);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (options.defer) {
|
|
142
|
-
this._deferredBindingRemovals.set(viewElement, viewElement.root);
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
const wasFound = this._viewToModelMapping.delete(viewElement);
|
|
146
|
-
if (wasFound) {
|
|
147
|
-
// Stop tracking after the element is no longer mapped. We want to track all mapped elements and only mapped elements.
|
|
148
|
-
this._cache.stopTracking(viewElement);
|
|
149
|
-
}
|
|
150
|
-
if (this._modelToViewMapping.get(modelElement) == viewElement) {
|
|
151
|
-
this._modelToViewMapping.delete(modelElement);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Unbinds the given {@link module:engine/model/element~ModelElement model element} from the map.
|
|
157
|
-
*
|
|
158
|
-
* **Note:** the model-to-view binding will be removed, if it existed. However, the corresponding view-to-model binding
|
|
159
|
-
* will be removed only if the view element is still bound to the passed `modelElement`.
|
|
160
|
-
*
|
|
161
|
-
* This behavior lets for re-binding view element to another model element without fear of losing the new binding
|
|
162
|
-
* when the previously bound model element is unbound.
|
|
163
|
-
*
|
|
164
|
-
* @param modelElement Model element to unbind.
|
|
165
|
-
*/
|
|
166
|
-
unbindModelElement(modelElement) {
|
|
167
|
-
const viewElement = this.toViewElement(modelElement);
|
|
168
|
-
this._modelToViewMapping.delete(modelElement);
|
|
169
|
-
if (this._viewToModelMapping.get(viewElement) == modelElement) {
|
|
170
|
-
const wasFound = this._viewToModelMapping.delete(viewElement);
|
|
171
|
-
if (wasFound) {
|
|
172
|
-
// Stop tracking after the element is no longer mapped. We want to track all mapped elements and only mapped elements.
|
|
173
|
-
this._cache.stopTracking(viewElement);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Binds the given marker name with the given {@link module:engine/view/element~ViewElement view element}. The element
|
|
179
|
-
* will be added to the current set of elements bound with the given marker name.
|
|
180
|
-
*
|
|
181
|
-
* @param element Element to bind.
|
|
182
|
-
* @param name Marker name.
|
|
183
|
-
*/
|
|
184
|
-
bindElementToMarker(element, name) {
|
|
185
|
-
const elements = this._markerNameToElements.get(name) || new Set();
|
|
186
|
-
elements.add(element);
|
|
187
|
-
const names = this._elementToMarkerNames.get(element) || new Set();
|
|
188
|
-
names.add(name);
|
|
189
|
-
this._markerNameToElements.set(name, elements);
|
|
190
|
-
this._elementToMarkerNames.set(element, names);
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Unbinds an element from given marker name.
|
|
194
|
-
*
|
|
195
|
-
* @param element Element to unbind.
|
|
196
|
-
* @param name Marker name.
|
|
197
|
-
*/
|
|
198
|
-
unbindElementFromMarkerName(element, name) {
|
|
199
|
-
const nameToElements = this._markerNameToElements.get(name);
|
|
200
|
-
if (nameToElements) {
|
|
201
|
-
nameToElements.delete(element);
|
|
202
|
-
if (nameToElements.size == 0) {
|
|
203
|
-
this._markerNameToElements.delete(name);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
const elementToNames = this._elementToMarkerNames.get(element);
|
|
207
|
-
if (elementToNames) {
|
|
208
|
-
elementToNames.delete(name);
|
|
209
|
-
if (elementToNames.size == 0) {
|
|
210
|
-
this._elementToMarkerNames.delete(element);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Returns all marker names of markers which have changed due to unbinding a view element (so it is assumed that the view element
|
|
216
|
-
* has been removed, moved or renamed) since the last flush. After returning, the marker names list is cleared.
|
|
217
|
-
*/
|
|
218
|
-
flushUnboundMarkerNames() {
|
|
219
|
-
const markerNames = Array.from(this._unboundMarkerNames);
|
|
220
|
-
this._unboundMarkerNames.clear();
|
|
221
|
-
return markerNames;
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Unbinds all deferred binding removals of view elements that in the meantime were not re-attached to some root or document fragment.
|
|
225
|
-
*
|
|
226
|
-
* See: {@link #unbindViewElement `unbindViewElement()`}.
|
|
227
|
-
*/
|
|
228
|
-
flushDeferredBindings() {
|
|
229
|
-
for (const [viewElement, root] of this._deferredBindingRemovals) {
|
|
230
|
-
// Unbind it only if it wasn't re-attached to some root or document fragment.
|
|
231
|
-
if (viewElement.root == root) {
|
|
232
|
-
this.unbindViewElement(viewElement);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
this._deferredBindingRemovals = new Map();
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Removes all model to view and view to model bindings.
|
|
239
|
-
*/
|
|
240
|
-
clearBindings() {
|
|
241
|
-
this._modelToViewMapping = new WeakMap();
|
|
242
|
-
this._viewToModelMapping = new WeakMap();
|
|
243
|
-
this._markerNameToElements = new Map();
|
|
244
|
-
this._elementToMarkerNames = new Map();
|
|
245
|
-
this._unboundMarkerNames = new Set();
|
|
246
|
-
this._deferredBindingRemovals = new Map();
|
|
247
|
-
}
|
|
248
|
-
toModelElement(viewElement) {
|
|
249
|
-
return this._viewToModelMapping.get(viewElement);
|
|
250
|
-
}
|
|
251
|
-
toViewElement(modelElement) {
|
|
252
|
-
return this._modelToViewMapping.get(modelElement);
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Gets the corresponding model range.
|
|
256
|
-
*
|
|
257
|
-
* @param viewRange View range.
|
|
258
|
-
* @returns Corresponding model range.
|
|
259
|
-
*/
|
|
260
|
-
toModelRange(viewRange) {
|
|
261
|
-
return new ModelRange(this.toModelPosition(viewRange.start), this.toModelPosition(viewRange.end));
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Gets the corresponding view range.
|
|
265
|
-
*
|
|
266
|
-
* @param modelRange Model range.
|
|
267
|
-
* @returns Corresponding view range.
|
|
268
|
-
*/
|
|
269
|
-
toViewRange(modelRange) {
|
|
270
|
-
return new ViewRange(this.toViewPosition(modelRange.start), this.toViewPosition(modelRange.end));
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Gets the corresponding model position.
|
|
274
|
-
*
|
|
275
|
-
* @fires viewToModelPosition
|
|
276
|
-
* @param viewPosition View position.
|
|
277
|
-
* @returns Corresponding model position.
|
|
278
|
-
*/
|
|
279
|
-
toModelPosition(viewPosition) {
|
|
280
|
-
const data = {
|
|
281
|
-
viewPosition,
|
|
282
|
-
mapper: this
|
|
283
|
-
};
|
|
284
|
-
this.fire('viewToModelPosition', data);
|
|
285
|
-
return data.modelPosition;
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Gets the corresponding view position.
|
|
289
|
-
*
|
|
290
|
-
* @fires modelToViewPosition
|
|
291
|
-
* @param modelPosition Model position.
|
|
292
|
-
* @param options Additional options for position mapping process.
|
|
293
|
-
* @param options.isPhantom Should be set to `true` if the model position to map is pointing to a place
|
|
294
|
-
* in model tree which no longer exists. For example, it could be an end of a removed model range.
|
|
295
|
-
* @returns Corresponding view position.
|
|
296
|
-
*/
|
|
297
|
-
toViewPosition(modelPosition, options = {}) {
|
|
298
|
-
const data = {
|
|
299
|
-
modelPosition,
|
|
300
|
-
mapper: this,
|
|
301
|
-
isPhantom: options.isPhantom
|
|
302
|
-
};
|
|
303
|
-
this.fire('modelToViewPosition', data);
|
|
304
|
-
return data.viewPosition;
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Gets all view elements bound to the given marker name.
|
|
308
|
-
*
|
|
309
|
-
* @param name Marker name.
|
|
310
|
-
* @returns View elements bound with the given marker name or `null`
|
|
311
|
-
* if no elements are bound to the given marker name.
|
|
312
|
-
*/
|
|
313
|
-
markerNameToElements(name) {
|
|
314
|
-
const boundElements = this._markerNameToElements.get(name);
|
|
315
|
-
if (!boundElements) {
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
const elements = new Set();
|
|
319
|
-
for (const element of boundElements) {
|
|
320
|
-
if (element.is('attributeElement')) {
|
|
321
|
-
for (const clone of element.getElementsWithSameId()) {
|
|
322
|
-
elements.add(clone);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
elements.add(element);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return elements;
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* **This method is deprecated and will be removed in one of the future CKEditor 5 releases.**
|
|
333
|
-
*
|
|
334
|
-
* **Using this method will turn off `Mapper` caching system and may degrade performance when operating on bigger documents.**
|
|
335
|
-
*
|
|
336
|
-
* Registers a callback that evaluates the length in the model of a view element with the given name.
|
|
337
|
-
*
|
|
338
|
-
* The callback is fired with one argument, which is a view element instance. The callback is expected to return
|
|
339
|
-
* a number representing the length of the view element in the model.
|
|
340
|
-
*
|
|
341
|
-
* ```ts
|
|
342
|
-
* // List item in view may contain nested list, which have other list items. In model though,
|
|
343
|
-
* // the lists are represented by flat structure. Because of those differences, length of list view element
|
|
344
|
-
* // may be greater than one. In the callback it's checked how many nested list items are in evaluated list item.
|
|
345
|
-
*
|
|
346
|
-
* function getViewListItemLength( element ) {
|
|
347
|
-
* let length = 1;
|
|
348
|
-
*
|
|
349
|
-
* for ( let child of element.getChildren() ) {
|
|
350
|
-
* if ( child.name == 'ul' || child.name == 'ol' ) {
|
|
351
|
-
* for ( let item of child.getChildren() ) {
|
|
352
|
-
* length += getViewListItemLength( item );
|
|
353
|
-
* }
|
|
354
|
-
* }
|
|
355
|
-
* }
|
|
356
|
-
*
|
|
357
|
-
* return length;
|
|
358
|
-
* }
|
|
359
|
-
*
|
|
360
|
-
* mapper.registerViewToModelLength( 'li', getViewListItemLength );
|
|
361
|
-
* ```
|
|
362
|
-
*
|
|
363
|
-
* @param viewElementName Name of view element for which callback is registered.
|
|
364
|
-
* @param lengthCallback Function return a length of view element instance in model.
|
|
365
|
-
* @deprecated
|
|
366
|
-
*/
|
|
367
|
-
registerViewToModelLength(viewElementName, lengthCallback) {
|
|
368
|
-
this._viewToModelLengthCallbacks.set(viewElementName, lengthCallback);
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* For the given `viewPosition`, finds and returns the closest ancestor of this position that has a mapping to
|
|
372
|
-
* the model.
|
|
373
|
-
*
|
|
374
|
-
* @param viewPosition Position for which a mapped ancestor should be found.
|
|
375
|
-
*/
|
|
376
|
-
findMappedViewAncestor(viewPosition) {
|
|
377
|
-
let parent = viewPosition.parent;
|
|
378
|
-
while (!this._viewToModelMapping.has(parent)) {
|
|
379
|
-
parent = parent.parent;
|
|
380
|
-
}
|
|
381
|
-
return parent;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Calculates model offset based on the view position and the block element.
|
|
385
|
-
*
|
|
386
|
-
* Example:
|
|
387
|
-
*
|
|
388
|
-
* ```html
|
|
389
|
-
* <p>foo<b>ba|r</b></p> // _toModelOffset( b, 2, p ) -> 5
|
|
390
|
-
* ```
|
|
391
|
-
*
|
|
392
|
-
* Is a sum of:
|
|
393
|
-
*
|
|
394
|
-
* ```html
|
|
395
|
-
* <p>foo|<b>bar</b></p> // _toModelOffset( p, 3, p ) -> 3
|
|
396
|
-
* <p>foo<b>ba|r</b></p> // _toModelOffset( b, 2, b ) -> 2
|
|
397
|
-
* ```
|
|
398
|
-
*
|
|
399
|
-
* @param viewParent Position parent.
|
|
400
|
-
* @param viewOffset Position offset.
|
|
401
|
-
* @param viewBlock Block used as a base to calculate offset.
|
|
402
|
-
* @returns Offset in the model.
|
|
403
|
-
*/
|
|
404
|
-
_toModelOffset(viewParent, viewOffset, viewBlock) {
|
|
405
|
-
if (viewBlock != viewParent) {
|
|
406
|
-
// See example.
|
|
407
|
-
const offsetToParentStart = this._toModelOffset(viewParent.parent, viewParent.index, viewBlock);
|
|
408
|
-
const offsetInParent = this._toModelOffset(viewParent, viewOffset, viewParent);
|
|
409
|
-
return offsetToParentStart + offsetInParent;
|
|
410
|
-
}
|
|
411
|
-
// viewBlock == viewParent, so we need to calculate the offset in the parent element.
|
|
412
|
-
// If the position is a text it is simple ("ba|r" -> 2).
|
|
413
|
-
if (viewParent.is('$text')) {
|
|
414
|
-
return viewOffset;
|
|
415
|
-
}
|
|
416
|
-
// If the position is in an element we need to sum lengths of siblings ( <b> bar </b> foo | -> 3 + 3 = 6 ).
|
|
417
|
-
let modelOffset = 0;
|
|
418
|
-
for (let i = 0; i < viewOffset; i++) {
|
|
419
|
-
modelOffset += this.getModelLength(viewParent.getChild(i));
|
|
420
|
-
}
|
|
421
|
-
return modelOffset;
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Gets the length of the view element in the model.
|
|
425
|
-
*
|
|
426
|
-
* The length is calculated as follows:
|
|
427
|
-
* * if a {@link #registerViewToModelLength length mapping callback} is provided for the given `viewNode`, it is used to
|
|
428
|
-
* evaluate the model length (`viewNode` is used as first and only parameter passed to the callback),
|
|
429
|
-
* * length of a {@link module:engine/view/text~ViewText text node} is equal to the length of its
|
|
430
|
-
* {@link module:engine/view/text~ViewText#data data},
|
|
431
|
-
* * length of a {@link module:engine/view/uielement~ViewUIElement ui element} is equal to 0,
|
|
432
|
-
* * length of a mapped {@link module:engine/view/element~ViewElement element} is equal to 1,
|
|
433
|
-
* * length of a non-mapped {@link module:engine/view/element~ViewElement element} is equal to the length of its children.
|
|
434
|
-
*
|
|
435
|
-
* Examples:
|
|
436
|
-
*
|
|
437
|
-
* ```
|
|
438
|
-
* foo -> 3 // Text length is equal to its data length.
|
|
439
|
-
* <p>foo</p> -> 1 // Length of an element which is mapped is by default equal to 1.
|
|
440
|
-
* <b>foo</b> -> 3 // Length of an element which is not mapped is a length of its children.
|
|
441
|
-
* <div><p>x</p><p>y</p></div> -> 2 // Assuming that <div> is not mapped and <p> are mapped.
|
|
442
|
-
* ```
|
|
443
|
-
*
|
|
444
|
-
* @param viewNode View node.
|
|
445
|
-
* @returns Length of the node in the tree model.
|
|
446
|
-
*/
|
|
447
|
-
getModelLength(viewNode) {
|
|
448
|
-
const stack = [viewNode];
|
|
449
|
-
let len = 0;
|
|
450
|
-
while (stack.length > 0) {
|
|
451
|
-
const node = stack.pop();
|
|
452
|
-
const callback = node.name &&
|
|
453
|
-
this._viewToModelLengthCallbacks.size > 0 &&
|
|
454
|
-
this._viewToModelLengthCallbacks.get(node.name);
|
|
455
|
-
if (callback) {
|
|
456
|
-
len += callback(node);
|
|
457
|
-
}
|
|
458
|
-
else if (this._viewToModelMapping.has(node)) {
|
|
459
|
-
len += 1;
|
|
460
|
-
}
|
|
461
|
-
else if (node.is('$text')) {
|
|
462
|
-
len += node.data.length;
|
|
463
|
-
}
|
|
464
|
-
else if (node.is('uiElement')) {
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
else {
|
|
468
|
-
for (const child of node.getChildren()) {
|
|
469
|
-
stack.push(child);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
return len;
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Finds the position in a view element or view document fragment node (or in its children) with the expected model offset.
|
|
477
|
-
*
|
|
478
|
-
* If the passed `viewContainer` is bound to model, `Mapper` will use caching mechanism to improve performance.
|
|
479
|
-
*
|
|
480
|
-
* @param viewContainer Tree view element in which we are looking for the position.
|
|
481
|
-
* @param modelOffset Expected offset.
|
|
482
|
-
* @returns Found position.
|
|
483
|
-
*/
|
|
484
|
-
findPositionIn(viewContainer, modelOffset) {
|
|
485
|
-
if (modelOffset === 0) {
|
|
486
|
-
// Quickly return if asked for a position at the beginning of the container. No need to fire complex mechanisms to find it.
|
|
487
|
-
return this._moveViewPositionToTextNode(new ViewPosition(viewContainer, 0));
|
|
488
|
-
}
|
|
489
|
-
// Use cache only if there are no custom view-to-model length callbacks and only for bound elements.
|
|
490
|
-
// View-to-model length callbacks are deprecated and should be removed in one of the following releases.
|
|
491
|
-
// Then it will be possible to simplify some logic inside `Mapper`.
|
|
492
|
-
// Note: we could consider requiring `viewContainer` to be a mapped item.
|
|
493
|
-
const useCache = this._viewToModelLengthCallbacks.size == 0 && this._viewToModelMapping.has(viewContainer);
|
|
494
|
-
if (useCache) {
|
|
495
|
-
const cacheItem = this._cache.getClosest(viewContainer, modelOffset);
|
|
496
|
-
return this._findPositionStartingFrom(cacheItem.viewPosition, cacheItem.modelOffset, modelOffset, viewContainer, true);
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
return this._findPositionStartingFrom(new ViewPosition(viewContainer, 0), 0, modelOffset, viewContainer, false);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* Performs most of the logic for `Mapper#findPositionIn()`.
|
|
504
|
-
*
|
|
505
|
-
* It allows to start looking for the requested model offset from a given starting position, to enable caching. Using the cache,
|
|
506
|
-
* you can set the starting point and skip all the calculations that were already previously done.
|
|
507
|
-
*
|
|
508
|
-
* This method uses recursion to find positions inside deep structures. Example:
|
|
509
|
-
*
|
|
510
|
-
* ```
|
|
511
|
-
* <p>fo<b>bar</b>bom</p> -> target offset: 4
|
|
512
|
-
* <p>|fo<b>bar</b>bom</p> -> target offset: 4, traversed offset: 0
|
|
513
|
-
* <p>fo|<b>bar</b>bom</p> -> target offset: 4, traversed offset: 2
|
|
514
|
-
* <p>fo<b>bar</b>|bom</p> -> target offset: 4, traversed offset: 5 -> we are too far, look recursively in <b>.
|
|
515
|
-
*
|
|
516
|
-
* <p>fo<b>|bar</b>bom</p> -> target offset: 4, traversed offset: 2
|
|
517
|
-
* <p>fo<b>bar|</b>bom</p> -> target offset: 4, traversed offset: 5 -> we are too far, look inside "bar".
|
|
518
|
-
*
|
|
519
|
-
* <p>fo<b>ba|r</b>bom</p> -> target offset: 4, traversed offset: 2 -> position is inside text node at offset 4-2 = 2.
|
|
520
|
-
* ```
|
|
521
|
-
*
|
|
522
|
-
* @param startViewPosition View position to start looking from.
|
|
523
|
-
* @param startModelOffset Model offset related to `startViewPosition`.
|
|
524
|
-
* @param targetModelOffset Target model offset to find.
|
|
525
|
-
* @param viewContainer Mapped ancestor of `startViewPosition`. `startModelOffset` is the offset inside a model element or model
|
|
526
|
-
* document fragment mapped to `viewContainer`.
|
|
527
|
-
* @param useCache Whether `Mapper` should cache positions while traversing the view tree looking for `expectedModelOffset`.
|
|
528
|
-
* @returns View position mapped to `targetModelOffset`.
|
|
529
|
-
*/
|
|
530
|
-
_findPositionStartingFrom(startViewPosition, startModelOffset, targetModelOffset, viewContainer, useCache) {
|
|
531
|
-
let viewParent = startViewPosition.parent;
|
|
532
|
-
let viewOffset = startViewPosition.offset;
|
|
533
|
-
// In the text node it is simple: the offset in the model equals the offset in the text.
|
|
534
|
-
if (viewParent.is('$text')) {
|
|
535
|
-
return new ViewPosition(viewParent, targetModelOffset - startModelOffset);
|
|
536
|
-
}
|
|
537
|
-
// Last scanned view node.
|
|
538
|
-
let viewNode;
|
|
539
|
-
// Total model offset of the view nodes that were visited so far.
|
|
540
|
-
let traversedModelOffset = startModelOffset;
|
|
541
|
-
// Model length of the last traversed view node.
|
|
542
|
-
let lastLength = 0;
|
|
543
|
-
while (traversedModelOffset < targetModelOffset) {
|
|
544
|
-
viewNode = viewParent.getChild(viewOffset);
|
|
545
|
-
if (!viewNode) {
|
|
546
|
-
// If we still haven't reached the model offset, but we reached end of this `viewParent`, then we need to "leave" this
|
|
547
|
-
// element and "go up", looking further for the target model offset. This can happen when cached model offset is "deeper"
|
|
548
|
-
// but target model offset is "higher" in the view tree.
|
|
549
|
-
//
|
|
550
|
-
// Example: `<p>Foo<strong><em>Bar</em>^Baz</strong>Xyz</p>`
|
|
551
|
-
//
|
|
552
|
-
// Consider `^` is last cached position, when the `targetModelOffset` is `12`. In such case, we need to "go up" from
|
|
553
|
-
// `<strong>` and continue traversing in `<p>`.
|
|
554
|
-
//
|
|
555
|
-
if (viewParent == viewContainer) {
|
|
556
|
-
/**
|
|
557
|
-
* A model position could not be mapped to the view because specified model offset was too big and could not be
|
|
558
|
-
* found inside the mapped view element or view document fragment.
|
|
559
|
-
*
|
|
560
|
-
* @error mapping-model-offset-not-found
|
|
561
|
-
*/
|
|
562
|
-
throw new CKEditorError('mapping-model-offset-not-found', this, { modelOffset: targetModelOffset, viewContainer });
|
|
563
|
-
}
|
|
564
|
-
else {
|
|
565
|
-
viewOffset = viewParent.parent.getChildIndex(viewParent) + 1;
|
|
566
|
-
viewParent = viewParent.parent;
|
|
567
|
-
// Cache view position after stepping out of the view element to make sure that all visited view positions are cached.
|
|
568
|
-
// Otherwise, cache invalidation may work incorrectly.
|
|
569
|
-
if (useCache) {
|
|
570
|
-
this._cache.save(viewParent, viewOffset, viewContainer, traversedModelOffset);
|
|
571
|
-
}
|
|
572
|
-
continue;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (useCache) {
|
|
576
|
-
lastLength = this._getModelLengthAndCache(viewNode, viewContainer, traversedModelOffset);
|
|
577
|
-
}
|
|
578
|
-
else {
|
|
579
|
-
lastLength = this.getModelLength(viewNode);
|
|
580
|
-
}
|
|
581
|
-
traversedModelOffset += lastLength;
|
|
582
|
-
viewOffset++;
|
|
583
|
-
}
|
|
584
|
-
let viewPosition = new ViewPosition(viewParent, viewOffset);
|
|
585
|
-
if (useCache) {
|
|
586
|
-
// Make sure to hoist view position and save cache for all view positions along the way that have the same `modelOffset`.
|
|
587
|
-
//
|
|
588
|
-
// Consider example view:
|
|
589
|
-
//
|
|
590
|
-
// <p>Foo<strong><i>bar<em>xyz</em></i></strong>abc</p>
|
|
591
|
-
//
|
|
592
|
-
// Lets assume that we looked for model offset `9`, starting from `6` (as it was previously cached value).
|
|
593
|
-
// In this case we will only traverse over `<em>xyz</em>` and cache view positions after "xyz" and after `</em>`.
|
|
594
|
-
// After stepping over `<em>xyz</em>`, we will stop processing this view, as we will reach target model offset `9`.
|
|
595
|
-
//
|
|
596
|
-
// However, position before `</strong>` and before `abc` are also valid positions for model offset `9`.
|
|
597
|
-
// Additionally, `Mapper` is supposed to return "hoisted" view positions, that is, we prefer positions that are closer to
|
|
598
|
-
// the mapped `viewContainer`. If a position is nested inside attribute elements, it should be "moved up" if possible.
|
|
599
|
-
//
|
|
600
|
-
// As we hoist the view position, we need to make sure that all view positions valid for model offset `9` are cached.
|
|
601
|
-
// This is necessary for cache invalidation to work correctly.
|
|
602
|
-
//
|
|
603
|
-
// To hoist a view position, we "go up" as long as the position is at the end of a non-mapped view element. We also cache
|
|
604
|
-
// all necessary values. See example:
|
|
605
|
-
//
|
|
606
|
-
// <p>Foo<strong><i>bar<em>xyz</em>^</i></strong>abc</p>
|
|
607
|
-
// <p>Foo<strong><i>bar<em>xyz</em></i>^</strong>abc</p>
|
|
608
|
-
// <p>Foo<strong><i>bar<em>xyz</em></i></strong>^abc</p>
|
|
609
|
-
//
|
|
610
|
-
while (viewPosition.isAtEnd && viewPosition.parent !== viewContainer && viewPosition.parent.parent) {
|
|
611
|
-
const cacheViewParent = viewPosition.parent.parent;
|
|
612
|
-
const cacheViewOffset = cacheViewParent.getChildIndex(viewPosition.parent) + 1;
|
|
613
|
-
this._cache.save(cacheViewParent, cacheViewOffset, viewContainer, traversedModelOffset);
|
|
614
|
-
viewPosition = new ViewPosition(cacheViewParent, cacheViewOffset);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
if (traversedModelOffset == targetModelOffset) {
|
|
618
|
-
// If it equals we found the position.
|
|
619
|
-
//
|
|
620
|
-
// Try moving view position into a text node if possible, as the editor engine prefers positions inside view text nodes.
|
|
621
|
-
//
|
|
622
|
-
// <p>Foo<strong><i>bar<em>xyz</em></i></strong>[]abc</p> --> <p>Foo<strong><i>bar<em>xyz</em></i></strong>{}abc</p>
|
|
623
|
-
//
|
|
624
|
-
return this._moveViewPositionToTextNode(viewPosition);
|
|
625
|
-
}
|
|
626
|
-
else {
|
|
627
|
-
// If it is higher we overstepped with the last traversed view node.
|
|
628
|
-
// We need to "enter" it, and look for the view position / model offset inside the last visited view node.
|
|
629
|
-
return this._findPositionStartingFrom(new ViewPosition(viewNode, 0), traversedModelOffset - lastLength, targetModelOffset, viewContainer, useCache);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Gets the length of the view element in the model and updates cache values after each view item it visits.
|
|
634
|
-
*
|
|
635
|
-
* See also {@link #getModelLength}.
|
|
636
|
-
*
|
|
637
|
-
* @param viewNode View node.
|
|
638
|
-
* @param viewContainer Ancestor of `viewNode` that is a mapped view element.
|
|
639
|
-
* @param modelOffset Model offset at which the `viewNode` starts.
|
|
640
|
-
* @returns Length of the node in the tree model.
|
|
641
|
-
*/
|
|
642
|
-
_getModelLengthAndCache(viewNode, viewContainer, modelOffset) {
|
|
643
|
-
let len = 0;
|
|
644
|
-
if (this._viewToModelMapping.has(viewNode)) {
|
|
645
|
-
len = 1;
|
|
646
|
-
}
|
|
647
|
-
else if (viewNode.is('$text')) {
|
|
648
|
-
len = viewNode.data.length;
|
|
649
|
-
}
|
|
650
|
-
else if (!viewNode.is('uiElement')) {
|
|
651
|
-
for (const child of viewNode.getChildren()) {
|
|
652
|
-
len += this._getModelLengthAndCache(child, viewContainer, modelOffset + len);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
this._cache.save(viewNode.parent, viewNode.index + 1, viewContainer, modelOffset + len);
|
|
656
|
-
return len;
|
|
657
|
-
}
|
|
658
|
-
/**
|
|
659
|
-
* Because we prefer positions in the text nodes over positions next to text nodes, if the view position was next to a text node,
|
|
660
|
-
* it moves it into the text node instead.
|
|
661
|
-
*
|
|
662
|
-
* ```
|
|
663
|
-
* <p>[]<b>foo</b></p> -> <p>[]<b>foo</b></p> // do not touch if position is not directly next to text
|
|
664
|
-
* <p>foo[]<b>foo</b></p> -> <p>foo{}<b>foo</b></p> // move to text node
|
|
665
|
-
* <p><b>[]foo</b></p> -> <p><b>{}foo</b></p> // move to text node
|
|
666
|
-
* ```
|
|
667
|
-
*
|
|
668
|
-
* @param viewPosition Position potentially next to the text node.
|
|
669
|
-
* @returns Position in the text node if possible.
|
|
670
|
-
*/
|
|
671
|
-
_moveViewPositionToTextNode(viewPosition) {
|
|
672
|
-
// If the position is just after a text node, put it at the end of that text node.
|
|
673
|
-
// If the position is just before a text node, put it at the beginning of that text node.
|
|
674
|
-
const nodeBefore = viewPosition.nodeBefore;
|
|
675
|
-
const nodeAfter = viewPosition.nodeAfter;
|
|
676
|
-
if (nodeBefore && nodeBefore.is('view:$text')) {
|
|
677
|
-
return new ViewPosition(nodeBefore, nodeBefore.data.length);
|
|
678
|
-
}
|
|
679
|
-
else if (nodeAfter && nodeAfter.is('view:$text')) {
|
|
680
|
-
return new ViewPosition(nodeAfter, 0);
|
|
681
|
-
}
|
|
682
|
-
// Otherwise, just return the given position.
|
|
683
|
-
return viewPosition;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* Cache mechanism for {@link module:engine/conversion/mapper~Mapper Mapper}.
|
|
688
|
-
*
|
|
689
|
-
* `MapperCache` improves performance for model-to-view position mapping, which is the main `Mapper` task. Asking for a mapping is much
|
|
690
|
-
* more frequent than actually performing changes, and even if the change happens, we can still partially keep the cache. This makes
|
|
691
|
-
* caching a useful strategy for `Mapper`.
|
|
692
|
-
*
|
|
693
|
-
* `MapperCache` will store some data for view elements or view document fragments that are mapped by the `Mapper`. These view items
|
|
694
|
-
* are "tracked" by the `MapperCache`. For such view items, we will keep entries of model offsets inside their mapped counterpart. For
|
|
695
|
-
* the cached model offsets, we will keep a view position that is inside the tracked item. This allows us to either get the mapping
|
|
696
|
-
* instantly, or at least in less steps than when calculating it from the beginning.
|
|
697
|
-
*
|
|
698
|
-
* Important problem related to caching is invalidating the cache. The cache must be invalidated each time the tracked view item changes.
|
|
699
|
-
* Additionally, we should invalidate as small part of the cache as possible. Since all the logic is encapsulated inside `MapperCache`,
|
|
700
|
-
* the `MapperCache` listens to view items {@link module:engine/view/node~ViewNodeChangeEvent `change` event} and reacts to it.
|
|
701
|
-
* Then, it invalidates just the part of the cache that is "after" the changed part of the view.
|
|
702
|
-
*
|
|
703
|
-
* As mentioned, `MapperCache` currently is used only for model-to-view position mapping as it was much bigger problem than view-to-model
|
|
704
|
-
* mapping. However, it should be possible to use it also for view-to-model.
|
|
705
|
-
*
|
|
706
|
-
* The main assumptions regarding `MapperCache` are:
|
|
707
|
-
*
|
|
708
|
-
* * it is an internal tool, used by `Mapper`, transparent to the outside (no additional effort when developing a plugin or a converter),
|
|
709
|
-
* * it stores all the necessary data internally, which makes it easier to disable or debug,
|
|
710
|
-
* * it is optimized for initial downcast process (long insertions), which is crucial for editor init and data save,
|
|
711
|
-
* * it does not save all possible positions mapping for memory considerations, although it is a possible improvement, which may increase
|
|
712
|
-
* performance, as well as simplify some parts of the `MapperCache` logic.
|
|
713
|
-
*
|
|
714
|
-
* @internal
|
|
715
|
-
*/
|
|
716
|
-
export class MapperCache extends /* #__PURE__ */ EmitterMixin() {
|
|
717
|
-
/**
|
|
718
|
-
* For every view element or document fragment tracked by `MapperCache`, it holds currently cached data, or more precisely,
|
|
719
|
-
* model offset to view position mappings. See also `MappingCache` and `CacheItem`.
|
|
720
|
-
*
|
|
721
|
-
* If an item is tracked by `MapperCache` it has an entry in this structure, so this structure can be used to check which items
|
|
722
|
-
* are tracked by `MapperCache`. When an item is no longer tracked, it is removed from this structure.
|
|
723
|
-
*
|
|
724
|
-
* Although `MappingCache` and `CacheItem` structures allows for caching any model offsets and view positions, we only cache
|
|
725
|
-
* values for model offsets that are after a view node. So, in essence, positions inside text nodes are not cached. However, it takes
|
|
726
|
-
* from one to at most a few steps, to get from a cached position to a position that is inside a view text node.
|
|
727
|
-
*
|
|
728
|
-
* Additionally, only one item per `modelOffset` is cached. There can be several view positions that map to the same `modelOffset`.
|
|
729
|
-
* Only the first save for `modelOffset` is stored.
|
|
730
|
-
*/
|
|
731
|
-
_cachedMapping = new WeakMap();
|
|
732
|
-
/**
|
|
733
|
-
* When `MapperCache` {@link ~MapperCache#save saves} view position -> model offset mapping, a `CacheItem` is inserted into certain
|
|
734
|
-
* `MappingCache#cacheList` at some index. Additionally, we store that index with the view node that is before the cached view position.
|
|
735
|
-
*
|
|
736
|
-
* This allows to quickly get a cache list item related to certain view node, and hence, for fast cache invalidation.
|
|
737
|
-
*
|
|
738
|
-
* For example, consider view: `<p>Some <strong>bold</strong> text.</p>`, where `<p>` is a view element tracked by `MapperCache`.
|
|
739
|
-
* If all `<p>` children were visited by `MapperCache`, then `<p>` cache list would have four items, related to following model offsets:
|
|
740
|
-
* `0`, `5`, `9`, `15`. Then, view node `"Some "` would have index `1`, `<strong>` index `2`, and `" text." index `3`.
|
|
741
|
-
*
|
|
742
|
-
* Note that the index related with a node is always greater than `0`. The first item in cache list is always for model offset `0`
|
|
743
|
-
* (and view offset `0`), and it is not related to any node.
|
|
744
|
-
*/
|
|
745
|
-
_nodeToCacheListIndex = new WeakMap();
|
|
746
|
-
/**
|
|
747
|
-
* Callback fired whenever there is a direct or indirect children change in tracked view element or tracked view document fragment.
|
|
748
|
-
*
|
|
749
|
-
* This is specified as a property to make it easier to set as an event callback and to later turn off that event.
|
|
750
|
-
*/
|
|
751
|
-
_invalidateOnChildrenChangeCallback = (evt, viewNode, data) => {
|
|
752
|
-
// View element or document fragment changed its children at `data.index`. Clear all cache starting from before that index.
|
|
753
|
-
this._clearCacheInsideParent(viewNode, data.index);
|
|
754
|
-
};
|
|
755
|
-
/**
|
|
756
|
-
* Callback fired whenever a view text node directly or indirectly inside a tracked view element or tracked view document fragment
|
|
757
|
-
* changes its text data.
|
|
758
|
-
*
|
|
759
|
-
* This is specified as a property to make it easier to set as an event callback and to later turn off that event.
|
|
760
|
-
*/
|
|
761
|
-
_invalidateOnTextChangeCallback = (evt, viewNode) => {
|
|
762
|
-
// It is enough to validate starting from "after the text node", because the first cache entry that we might want to invalidate
|
|
763
|
-
// is the position after this text node.
|
|
764
|
-
//
|
|
765
|
-
// For example - assume following view and following view positions cached (marked by `^`): `<p>Foo^<strong>bar^</strong>^abc^</p>`.
|
|
766
|
-
//
|
|
767
|
-
// If we change text "bar", we only need to invalidate cached positions after it. Cached positions before the text are not changed.
|
|
768
|
-
//
|
|
769
|
-
this._clearCacheAfter(viewNode);
|
|
770
|
-
};
|
|
771
|
-
/**
|
|
772
|
-
* Saves cache for given view position mapping <-> model offset mapping. The view position should be after a node (i.e. it cannot
|
|
773
|
-
* be the first position inside its parent, or in other words, `viewOffset` must be greater than `0`).
|
|
774
|
-
*
|
|
775
|
-
* Note, that if `modelOffset` for given `viewContainer` was already saved, the stored view position (i.e. parent+offset) will not
|
|
776
|
-
* be overwritten. However, it is important to still save it, as we still store additional data related to cached view positions.
|
|
777
|
-
*
|
|
778
|
-
* @param viewParent View position parent.
|
|
779
|
-
* @param viewOffset View position offset. Must be greater than `0`.
|
|
780
|
-
* @param viewContainer Tracked view position ascendant (it may be the direct parent of the view position).
|
|
781
|
-
* @param modelOffset Model offset in the model element or document fragment which is mapped to `viewContainer`.
|
|
782
|
-
*/
|
|
783
|
-
save(viewParent, viewOffset, viewContainer, modelOffset) {
|
|
784
|
-
// Get current cache for the tracked ancestor.
|
|
785
|
-
const cache = this._cachedMapping.get(viewContainer);
|
|
786
|
-
// See if there is already a cache defined for `modelOffset`.
|
|
787
|
-
const cacheItem = cache.cacheMap.get(modelOffset);
|
|
788
|
-
if (cacheItem) {
|
|
789
|
-
// We already cached this offset. Don't overwrite the cache.
|
|
790
|
-
// However, we still need to set a proper entry in `_nodeToCacheListIndex`. We can figure it out based on existing `cacheItem`.
|
|
791
|
-
//
|
|
792
|
-
// We have a `cacheItem` for the `modelOffset`, so we can get a `viewPosition` from there. Before that view position, there
|
|
793
|
-
// must be a node. That node must have an index set. This will be the index we will want to use.
|
|
794
|
-
// Since we expect `viewOffset` to be greater than 0, then in almost all cases `modelOffset` will be greater than 0 as well.
|
|
795
|
-
// As a result, we can expect `cacheItem.viewPosition.nodeBefore` to be set.
|
|
796
|
-
//
|
|
797
|
-
// However, in an edge case, were the tracked element contains a 0-model-length view element as the first child (UI element or
|
|
798
|
-
// an empty attribute element), then `modelOffset` will be 0, and `cacheItem.viewPosition` will be before any view node.
|
|
799
|
-
// In such edge case, `cacheItem.viewPosition.nodeBefore` is `undefined`, so we set index to `0`.
|
|
800
|
-
//
|
|
801
|
-
const viewChild = viewParent.getChild(viewOffset - 1);
|
|
802
|
-
const index = cacheItem.viewPosition.nodeBefore ? this._nodeToCacheListIndex.get(cacheItem.viewPosition.nodeBefore) : 0;
|
|
803
|
-
this._nodeToCacheListIndex.set(viewChild, index);
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
const viewPosition = new ViewPosition(viewParent, viewOffset);
|
|
807
|
-
const newCacheItem = { viewPosition, modelOffset };
|
|
808
|
-
// Extend the valid cache range.
|
|
809
|
-
cache.maxModelOffset = modelOffset > cache.maxModelOffset ? modelOffset : cache.maxModelOffset;
|
|
810
|
-
// Save the new cache item to the `cacheMap`.
|
|
811
|
-
cache.cacheMap.set(modelOffset, newCacheItem);
|
|
812
|
-
// Save the new cache item to the `cacheList`.
|
|
813
|
-
let i = cache.cacheList.length - 1;
|
|
814
|
-
// Mostly, we cache elements at the end of `cacheList` and the loop does not execute even once. But when we recursively visit nodes
|
|
815
|
-
// in `Mapper#_findPositionIn()`, then we will first cache the parent, and then it's children, and they will not be added at the
|
|
816
|
-
// end of `cacheList`. This is why we need to find correct index to insert them.
|
|
817
|
-
while (i >= 0 && cache.cacheList[i].modelOffset > modelOffset) {
|
|
818
|
-
i--;
|
|
819
|
-
}
|
|
820
|
-
cache.cacheList.splice(i + 1, 0, newCacheItem);
|
|
821
|
-
if (viewOffset > 0) {
|
|
822
|
-
const viewChild = viewParent.getChild(viewOffset - 1);
|
|
823
|
-
// There was an idea to also cache `viewContainer` here but, it could lead to wrong results. If we wanted to cache
|
|
824
|
-
// `viewContainer`, we probably would need to clear `this._nodeToCacheListIndex` when cache is cleared.
|
|
825
|
-
// Also, there was no gain from caching this value, the results were almost the same (statistical error).
|
|
826
|
-
this._nodeToCacheListIndex.set(viewChild, i + 1);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
/**
|
|
830
|
-
* For given `modelOffset` inside a model element mapped to given `viewContainer`, it returns the closest saved cache item
|
|
831
|
-
* (view position and related model offset) to the requested one.
|
|
832
|
-
*
|
|
833
|
-
* It can be exactly the requested mapping, or it can be mapping that is the closest starting point to look for the requested mapping.
|
|
834
|
-
*
|
|
835
|
-
* `viewContainer` must be a view element or document fragment that is mapped by the {@link ~Mapper Mapper}.
|
|
836
|
-
*
|
|
837
|
-
* If `viewContainer` is not yet tracked by the `MapperCache`, it will be automatically tracked after calling this method.
|
|
838
|
-
*
|
|
839
|
-
* Note: this method will automatically "hoist" cached positions, i.e. it will return a position that is closest to the tracked element.
|
|
840
|
-
*
|
|
841
|
-
* For example, if `<p>` is tracked element, and `^` is cached position:
|
|
842
|
-
*
|
|
843
|
-
* ```
|
|
844
|
-
* <p>This is <strong>some <em>heavily <u>formatted</u>^</em></strong> text.</p>
|
|
845
|
-
* ```
|
|
846
|
-
*
|
|
847
|
-
* If this position would be returned, instead, a position directly in `<p>` would be returned:
|
|
848
|
-
*
|
|
849
|
-
* ```
|
|
850
|
-
* <p>This is <strong>some <em>heavily <u>formatted</u></em></strong>^ text.</p>
|
|
851
|
-
* ```
|
|
852
|
-
*
|
|
853
|
-
* Note, that `modelOffset` for both positions is the same.
|
|
854
|
-
*
|
|
855
|
-
* @param viewContainer Tracked view element or document fragment, which cache will be used.
|
|
856
|
-
* @param modelOffset Model offset in a model element or document fragment, which is mapped to `viewContainer`.
|
|
857
|
-
*/
|
|
858
|
-
getClosest(viewContainer, modelOffset) {
|
|
859
|
-
const cache = this._cachedMapping.get(viewContainer);
|
|
860
|
-
let result;
|
|
861
|
-
if (cache) {
|
|
862
|
-
if (modelOffset > cache.maxModelOffset) {
|
|
863
|
-
result = cache.cacheList[cache.cacheList.length - 1];
|
|
864
|
-
}
|
|
865
|
-
else {
|
|
866
|
-
const cacheItem = cache.cacheMap.get(modelOffset);
|
|
867
|
-
if (cacheItem) {
|
|
868
|
-
result = cacheItem;
|
|
869
|
-
}
|
|
870
|
-
else {
|
|
871
|
-
result = this._findInCacheList(cache.cacheList, modelOffset);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
else {
|
|
876
|
-
result = this.startTracking(viewContainer);
|
|
877
|
-
}
|
|
878
|
-
return {
|
|
879
|
-
modelOffset: result.modelOffset,
|
|
880
|
-
viewPosition: result.viewPosition.clone()
|
|
881
|
-
};
|
|
882
|
-
}
|
|
883
|
-
/**
|
|
884
|
-
* Starts tracking given `viewContainer`, which must be mapped to a model element or model document fragment.
|
|
885
|
-
*
|
|
886
|
-
* Note, that this method is automatically called by
|
|
887
|
-
* {@link module:engine/conversion/mapper~MapperCache#getClosest `MapperCache#getClosest()`} and there is no need to call it manually.
|
|
888
|
-
*
|
|
889
|
-
* This method initializes the cache for `viewContainer` and adds callbacks for
|
|
890
|
-
* {@link module:engine/view/node~ViewNodeChangeEvent `change` event} fired by `viewContainer`. `MapperCache` listens to `change` event
|
|
891
|
-
* on the tracked elements to invalidate the stored cache.
|
|
892
|
-
*/
|
|
893
|
-
startTracking(viewContainer) {
|
|
894
|
-
const viewPosition = new ViewPosition(viewContainer, 0);
|
|
895
|
-
const initialCacheItem = { viewPosition, modelOffset: 0 };
|
|
896
|
-
const initialCache = {
|
|
897
|
-
maxModelOffset: 0,
|
|
898
|
-
cacheList: [initialCacheItem],
|
|
899
|
-
cacheMap: new Map([[0, initialCacheItem]])
|
|
900
|
-
};
|
|
901
|
-
this._cachedMapping.set(viewContainer, initialCache);
|
|
902
|
-
// Listen to changes in tracked view containers in order to invalidate the cache.
|
|
903
|
-
//
|
|
904
|
-
// Possible performance improvement. This event bubbles, so if there are multiple tracked (mapped) elements that are ancestors
|
|
905
|
-
// then this will be unnecessarily fired for each ancestor. This could be rewritten to listen only to roots and document fragments.
|
|
906
|
-
viewContainer.on('change:children', this._invalidateOnChildrenChangeCallback);
|
|
907
|
-
viewContainer.on('change:text', this._invalidateOnTextChangeCallback);
|
|
908
|
-
return initialCacheItem;
|
|
909
|
-
}
|
|
910
|
-
/**
|
|
911
|
-
* Stops tracking given `viewContainer`.
|
|
912
|
-
*
|
|
913
|
-
* It removes the cached data and stops listening to {@link module:engine/view/node~ViewNodeChangeEvent `change` event} on the
|
|
914
|
-
* `viewContainer`.
|
|
915
|
-
*/
|
|
916
|
-
stopTracking(viewContainer) {
|
|
917
|
-
viewContainer.off('change:children', this._invalidateOnChildrenChangeCallback);
|
|
918
|
-
viewContainer.off('change:text', this._invalidateOnTextChangeCallback);
|
|
919
|
-
this._cachedMapping.delete(viewContainer);
|
|
920
|
-
}
|
|
921
|
-
/**
|
|
922
|
-
* Invalidates cache inside `viewParent`, starting from given `index` in that parent.
|
|
923
|
-
*
|
|
924
|
-
* This method may clear a bit more cache than just what was saved after given `index`, but it is guaranteed that at least it
|
|
925
|
-
* will invalidate everything after `index`.
|
|
926
|
-
*/
|
|
927
|
-
_clearCacheInsideParent(viewParent, index) {
|
|
928
|
-
if (index == 0) {
|
|
929
|
-
// Change at the beginning of the parent.
|
|
930
|
-
if (this._cachedMapping.has(viewParent)) {
|
|
931
|
-
// If this is a tracked element, clear all cache.
|
|
932
|
-
this._clearCacheAll(viewParent);
|
|
933
|
-
}
|
|
934
|
-
else {
|
|
935
|
-
// If this is not a tracked element, remove cache starting from before this element.
|
|
936
|
-
// Since it is not a tracked element, it has to have a parent.
|
|
937
|
-
this._clearCacheInsideParent(viewParent.parent, viewParent.index);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
else {
|
|
941
|
-
// Change in the middle of the parent. Get a view node that's before the change.
|
|
942
|
-
const lastValidNode = viewParent.getChild(index - 1);
|
|
943
|
-
// Then, clear all cache after this view node.
|
|
944
|
-
//
|
|
945
|
-
this._clearCacheAfter(lastValidNode);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
/**
|
|
949
|
-
* Clears all the cache for given tracked `viewContainer`.
|
|
950
|
-
*/
|
|
951
|
-
_clearCacheAll(viewContainer) {
|
|
952
|
-
const cache = this._cachedMapping.get(viewContainer);
|
|
953
|
-
// TODO: Should clear `_nodeToCacheListIndex` too?
|
|
954
|
-
if (cache.maxModelOffset > 0) {
|
|
955
|
-
cache.maxModelOffset = 0;
|
|
956
|
-
cache.cacheList.length = 1;
|
|
957
|
-
cache.cacheMap.clear();
|
|
958
|
-
cache.cacheMap.set(0, cache.cacheList[0]);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Clears all the stored cache that is after given `viewNode`. The `viewNode` can be any node that is inside a tracked view element
|
|
963
|
-
* or view document fragment.
|
|
964
|
-
*
|
|
965
|
-
* In reality, this function may clear a bit more cache than just "starting after" `viewNode`, but it is guaranteed that at least
|
|
966
|
-
* all cache after `viewNode` is invalidated.
|
|
967
|
-
*/
|
|
968
|
-
_clearCacheAfter(viewNode) {
|
|
969
|
-
// To quickly invalidate the cache, we base on the cache list index stored with the node. See docs for `this._nodeToCacheListIndex`.
|
|
970
|
-
const cacheListIndex = this._nodeToCacheListIndex.get(viewNode);
|
|
971
|
-
// If there is no index stored, it means that this `viewNode` has not been cached yet.
|
|
972
|
-
if (cacheListIndex === undefined) {
|
|
973
|
-
// If the node is not cached, maybe it's parent is. We will try to invalidate the cache using the parent.
|
|
974
|
-
const viewParent = viewNode.parent;
|
|
975
|
-
// If the parent is a non-tracked element, try clearing the cache starting from the position before it.
|
|
976
|
-
//
|
|
977
|
-
// For example: `<p>Abc<strong>def<em>ghi</em></strong></p>`.
|
|
978
|
-
//
|
|
979
|
-
// If `viewNode` is `<em>` in this case, and it was not cached yet, we will try to clear cache starting from before `<strong>`.
|
|
980
|
-
//
|
|
981
|
-
// If the parent is a tracked element, then it means there's no cache to clear (nothing after the element is cached).
|
|
982
|
-
// In this case, there's nothing to do. We assume that there are no "holes" in caching in direct children of tracked element
|
|
983
|
-
// (that is if some children is cached, then its previous sibling is cached too, and we would not end up inside this `if`).
|
|
984
|
-
//
|
|
985
|
-
// TODO: Most probably this `if` could be removed altogether, after recent changes in Mapper.
|
|
986
|
-
// TODO: Now we cache all items one after another, so there aren't any "holes" anywhere, not only on top-level.
|
|
987
|
-
//
|
|
988
|
-
if (!this._cachedMapping.has(viewParent)) {
|
|
989
|
-
this._clearCacheInsideParent(viewParent.parent, viewParent.index);
|
|
990
|
-
}
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
let viewContainer = viewNode.parent;
|
|
994
|
-
while (!this._cachedMapping.has(viewContainer)) {
|
|
995
|
-
viewContainer = viewContainer.parent;
|
|
996
|
-
}
|
|
997
|
-
this._clearCacheFromCacheIndex(viewContainer, cacheListIndex);
|
|
998
|
-
}
|
|
999
|
-
/**
|
|
1000
|
-
* Clears all the cache in the cache list related to given `viewContainer`, starting from `index` (inclusive).
|
|
1001
|
-
*/
|
|
1002
|
-
_clearCacheFromCacheIndex(viewContainer, index) {
|
|
1003
|
-
if (index === 0) {
|
|
1004
|
-
// Don't remove the first entry in the cache (this entry is always a mapping between view offset 0 <-> model offset 0,
|
|
1005
|
-
// and it is a default value that is always expected to be in the cache list).
|
|
1006
|
-
//
|
|
1007
|
-
// The cache mechanism may ask to clear from index `0` in a case where a 0-model-length view element (UI element or empty
|
|
1008
|
-
// attribute element) was at the beginning of tracked element. In such scenario, the view element is mapped through
|
|
1009
|
-
// `nodeToCacheListIndex` to index `0`.
|
|
1010
|
-
index = 1;
|
|
1011
|
-
}
|
|
1012
|
-
// Cache is always available here because we initialize it just before adding a listener that fires `_clearCacheFromIndex()`.
|
|
1013
|
-
const cache = this._cachedMapping.get(viewContainer);
|
|
1014
|
-
const cacheItem = cache.cacheList[index - 1];
|
|
1015
|
-
if (!cacheItem) {
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
cache.maxModelOffset = cacheItem.modelOffset;
|
|
1019
|
-
// Remove from cache all `CacheItem`s that are "after" the index to clear from.
|
|
1020
|
-
const clearedItems = cache.cacheList.splice(index);
|
|
1021
|
-
// For each removed item, make sure to also remove it from `cacheMap` and clear related entry in `_nodeToCacheListIndex`.
|
|
1022
|
-
for (const item of clearedItems) {
|
|
1023
|
-
cache.cacheMap.delete(item.modelOffset);
|
|
1024
|
-
const viewNode = item.viewPosition.nodeBefore;
|
|
1025
|
-
this._nodeToCacheListIndex.delete(viewNode);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Finds a cache item in the given cache list, which `modelOffset` is closest (but smaller or equal) to given `offset`.
|
|
1030
|
-
*
|
|
1031
|
-
* Since `cacheList` is a sorted array, this uses binary search to retrieve the item quickly.
|
|
1032
|
-
*/
|
|
1033
|
-
_findInCacheList(cacheList, offset) {
|
|
1034
|
-
let start = 0;
|
|
1035
|
-
let end = cacheList.length - 1;
|
|
1036
|
-
let index = (end - start) >> 1;
|
|
1037
|
-
let item = cacheList[index];
|
|
1038
|
-
while (start < end) {
|
|
1039
|
-
if (item.modelOffset < offset) {
|
|
1040
|
-
start = index + 1;
|
|
1041
|
-
}
|
|
1042
|
-
else {
|
|
1043
|
-
end = index - 1;
|
|
1044
|
-
}
|
|
1045
|
-
index = start + ((end - start) >> 1);
|
|
1046
|
-
item = cacheList[index];
|
|
1047
|
-
}
|
|
1048
|
-
return item.modelOffset <= offset ? item : cacheList[index - 1];
|
|
1049
|
-
}
|
|
1050
|
-
}
|