@ckeditor/ckeditor5-engine 35.2.1 → 35.3.1
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/package.json +23 -23
- package/src/index.js +3 -0
- package/src/model/selection.js +2 -2
- package/src/model/utils/modifyselection.js +18 -20
- package/src/model/utils/selection-post-fixer.js +1 -1
- package/src/view/datatransfer.js +95 -0
- package/src/view/domconverter.js +12 -2
- package/src/view/editableelement.js +1 -1
- package/src/view/observer/compositionobserver.js +23 -3
- package/src/view/observer/inputobserver.js +137 -10
- package/src/view/observer/mutationobserver.js +60 -88
- package/src/view/observer/selectionobserver.js +34 -8
- package/src/view/observer/tabobserver.js +0 -3
- package/src/view/placeholder.js +10 -3
- package/src/view/renderer.js +171 -21
- package/src/view/view.js +5 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ckeditor/ckeditor5-engine",
|
|
3
|
-
"version": "35.
|
|
3
|
+
"version": "35.3.1",
|
|
4
4
|
"description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wysiwyg",
|
|
@@ -23,31 +23,31 @@
|
|
|
23
23
|
],
|
|
24
24
|
"main": "src/index.js",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@ckeditor/ckeditor5-utils": "^35.
|
|
26
|
+
"@ckeditor/ckeditor5-utils": "^35.3.1",
|
|
27
27
|
"lodash-es": "^4.17.15"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@ckeditor/ckeditor5-basic-styles": "^35.
|
|
31
|
-
"@ckeditor/ckeditor5-block-quote": "^35.
|
|
32
|
-
"@ckeditor/ckeditor5-clipboard": "^35.
|
|
33
|
-
"@ckeditor/ckeditor5-cloud-services": "^35.
|
|
34
|
-
"@ckeditor/ckeditor5-core": "^35.
|
|
35
|
-
"@ckeditor/ckeditor5-editor-classic": "^35.
|
|
36
|
-
"@ckeditor/ckeditor5-enter": "^35.
|
|
37
|
-
"@ckeditor/ckeditor5-essentials": "^35.
|
|
38
|
-
"@ckeditor/ckeditor5-heading": "^35.
|
|
39
|
-
"@ckeditor/ckeditor5-image": "^35.
|
|
40
|
-
"@ckeditor/ckeditor5-link": "^35.
|
|
41
|
-
"@ckeditor/ckeditor5-list": "^35.
|
|
42
|
-
"@ckeditor/ckeditor5-mention": "^35.
|
|
43
|
-
"@ckeditor/ckeditor5-paragraph": "^35.
|
|
44
|
-
"@ckeditor/ckeditor5-table": "^35.
|
|
45
|
-
"@ckeditor/ckeditor5-theme-lark": "^35.
|
|
46
|
-
"@ckeditor/ckeditor5-typing": "^35.
|
|
47
|
-
"@ckeditor/ckeditor5-ui": "^35.
|
|
48
|
-
"@ckeditor/ckeditor5-undo": "^35.
|
|
49
|
-
"@ckeditor/ckeditor5-widget": "^35.
|
|
50
|
-
"typescript": "^4.
|
|
30
|
+
"@ckeditor/ckeditor5-basic-styles": "^35.3.1",
|
|
31
|
+
"@ckeditor/ckeditor5-block-quote": "^35.3.1",
|
|
32
|
+
"@ckeditor/ckeditor5-clipboard": "^35.3.1",
|
|
33
|
+
"@ckeditor/ckeditor5-cloud-services": "^35.3.1",
|
|
34
|
+
"@ckeditor/ckeditor5-core": "^35.3.1",
|
|
35
|
+
"@ckeditor/ckeditor5-editor-classic": "^35.3.1",
|
|
36
|
+
"@ckeditor/ckeditor5-enter": "^35.3.1",
|
|
37
|
+
"@ckeditor/ckeditor5-essentials": "^35.3.1",
|
|
38
|
+
"@ckeditor/ckeditor5-heading": "^35.3.1",
|
|
39
|
+
"@ckeditor/ckeditor5-image": "^35.3.1",
|
|
40
|
+
"@ckeditor/ckeditor5-link": "^35.3.1",
|
|
41
|
+
"@ckeditor/ckeditor5-list": "^35.3.1",
|
|
42
|
+
"@ckeditor/ckeditor5-mention": "^35.3.1",
|
|
43
|
+
"@ckeditor/ckeditor5-paragraph": "^35.3.1",
|
|
44
|
+
"@ckeditor/ckeditor5-table": "^35.3.1",
|
|
45
|
+
"@ckeditor/ckeditor5-theme-lark": "^35.3.1",
|
|
46
|
+
"@ckeditor/ckeditor5-typing": "^35.3.1",
|
|
47
|
+
"@ckeditor/ckeditor5-ui": "^35.3.1",
|
|
48
|
+
"@ckeditor/ckeditor5-undo": "^35.3.1",
|
|
49
|
+
"@ckeditor/ckeditor5-widget": "^35.3.1",
|
|
50
|
+
"typescript": "^4.8.4",
|
|
51
51
|
"webpack": "^5.58.1",
|
|
52
52
|
"webpack-cli": "^4.9.0"
|
|
53
53
|
},
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export { default as InsertOperation } from './model/operation/insertoperation';
|
|
|
14
14
|
export { default as MarkerOperation } from './model/operation/markeroperation';
|
|
15
15
|
export { default as OperationFactory } from './model/operation/operationfactory';
|
|
16
16
|
export { transformSets } from './model/operation/transform';
|
|
17
|
+
export { default as Selection } from './model/selection';
|
|
17
18
|
export { default as DocumentSelection } from './model/documentselection';
|
|
18
19
|
export { default as Range } from './model/range';
|
|
19
20
|
export { default as LiveRange } from './model/liverange';
|
|
@@ -25,8 +26,10 @@ export { default as Position } from './model/position';
|
|
|
25
26
|
export { default as DocumentFragment } from './model/documentfragment';
|
|
26
27
|
export { default as History } from './model/history';
|
|
27
28
|
export { default as Text } from './model/text';
|
|
29
|
+
export { default as Schema } from './model/schema';
|
|
28
30
|
export { default as DomConverter } from './view/domconverter';
|
|
29
31
|
export { default as Renderer } from './view/renderer';
|
|
32
|
+
export { default as View } from './view/view';
|
|
30
33
|
export { default as ViewDocument } from './view/document';
|
|
31
34
|
export { default as ViewText } from './view/text';
|
|
32
35
|
export { default as ViewElement } from './view/element';
|
package/src/model/selection.js
CHANGED
|
@@ -729,7 +729,7 @@ function isUnvisitedBlock(element, visited) {
|
|
|
729
729
|
return false;
|
|
730
730
|
}
|
|
731
731
|
visited.add(element);
|
|
732
|
-
return element.root.document.model.schema.isBlock(element) && element.parent;
|
|
732
|
+
return element.root.document.model.schema.isBlock(element) && !!element.parent;
|
|
733
733
|
}
|
|
734
734
|
// Checks if the given element is a $block was not previously visited and is a top block in a range.
|
|
735
735
|
function isUnvisitedTopBlock(element, visited, range) {
|
|
@@ -743,7 +743,7 @@ function getParentBlock(position, visited) {
|
|
|
743
743
|
const schema = element.root.document.model.schema;
|
|
744
744
|
const ancestors = position.parent.getAncestors({ parentFirst: true, includeSelf: true });
|
|
745
745
|
let hasParentLimit = false;
|
|
746
|
-
const block = ancestors.find(element => {
|
|
746
|
+
const block = ancestors.find((element) => {
|
|
747
747
|
// Stop searching after first parent node that is limit element.
|
|
748
748
|
if (hasParentLimit) {
|
|
749
749
|
return false;
|
|
@@ -144,26 +144,24 @@ function getCorrectPosition(walker, unit, treatEmojiAsSingleUnit) {
|
|
|
144
144
|
// @param {Boolean} isForward Is the direction in which the selection should be modified is forward.
|
|
145
145
|
function getCorrectWordBreakPosition(walker, isForward) {
|
|
146
146
|
let textNode = walker.position.textNode;
|
|
147
|
-
if (textNode) {
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
if (!textNode) {
|
|
148
|
+
textNode = isForward ? walker.position.nodeAfter : walker.position.nodeBefore;
|
|
149
|
+
}
|
|
150
|
+
while (textNode && textNode.is('$text')) {
|
|
151
|
+
const offset = walker.position.offset - textNode.startOffset;
|
|
152
|
+
// Check of adjacent text nodes with different attributes (like BOLD).
|
|
153
|
+
// Example : 'foofoo []bar<$text bold="true">bar</$text> bazbaz'
|
|
154
|
+
// should expand to : 'foofoo [bar<$text bold="true">bar</$text>] bazbaz'.
|
|
155
|
+
if (isAtNodeBoundary(textNode, offset, isForward)) {
|
|
156
|
+
textNode = isForward ? walker.position.nodeAfter : walker.position.nodeBefore;
|
|
157
|
+
}
|
|
158
|
+
// Check if this is a word boundary.
|
|
159
|
+
else if (isAtWordBoundary(textNode.data, offset, isForward)) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
// Maybe one more character.
|
|
163
|
+
else {
|
|
150
164
|
walker.next();
|
|
151
|
-
// Check of adjacent text nodes with different attributes (like BOLD).
|
|
152
|
-
// Example : 'foofoo []bar<$text bold="true">bar</$text> bazbaz'
|
|
153
|
-
// should expand to : 'foofoo [bar<$text bold="true">bar</$text>] bazbaz'.
|
|
154
|
-
const nextNode = isForward ? walker.position.nodeAfter : walker.position.nodeBefore;
|
|
155
|
-
// Scan only text nodes. Ignore inline elements (like `<softBreak>`).
|
|
156
|
-
if (nextNode && nextNode.is('$text')) {
|
|
157
|
-
// Check boundary char of an adjacent text node.
|
|
158
|
-
const boundaryChar = nextNode.data.charAt(isForward ? 0 : nextNode.data.length - 1);
|
|
159
|
-
// Go to the next node if the character at the boundary of that node belongs to the same word.
|
|
160
|
-
if (!wordBoundaryCharacters.includes(boundaryChar)) {
|
|
161
|
-
// If adjacent text node belongs to the same word go to it & reset values.
|
|
162
|
-
walker.next();
|
|
163
|
-
textNode = walker.position.textNode;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
offset = walker.position.offset - textNode.startOffset;
|
|
167
165
|
}
|
|
168
166
|
}
|
|
169
167
|
return walker.position;
|
|
@@ -194,5 +192,5 @@ function isAtWordBoundary(data, offset, isForward) {
|
|
|
194
192
|
// @param {Number} offset Position offset.
|
|
195
193
|
// @param {Boolean} isForward Is the direction in which the selection should be modified is forward.
|
|
196
194
|
function isAtNodeBoundary(textNode, offset, isForward) {
|
|
197
|
-
return offset === (isForward ? textNode.
|
|
195
|
+
return offset === (isForward ? textNode.offsetSize : 0);
|
|
198
196
|
}
|
|
@@ -121,7 +121,7 @@ function tryFixingCollapsedRange(range, schema) {
|
|
|
121
121
|
// In the first case, there is no need to fix the selection range.
|
|
122
122
|
// In the second, let's go up to the outer selectable element
|
|
123
123
|
if (!nearestSelectionRange) {
|
|
124
|
-
const ancestorObject = originalPosition.getAncestors().reverse().find(item => schema.isObject(item));
|
|
124
|
+
const ancestorObject = originalPosition.getAncestors().reverse().find((item) => schema.isObject(item));
|
|
125
125
|
if (ancestorObject) {
|
|
126
126
|
return Range._createOn(ancestorObject);
|
|
127
127
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* A facade over the native [`DataTransfer`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer) object.
|
|
7
|
+
*/
|
|
8
|
+
export default class DataTransfer {
|
|
9
|
+
constructor(nativeDataTransfer) {
|
|
10
|
+
/**
|
|
11
|
+
* The array of files created from the native `DataTransfer#files` or `DataTransfer#items`.
|
|
12
|
+
*
|
|
13
|
+
* @readonly
|
|
14
|
+
* @member {Array.<File>} #files
|
|
15
|
+
*/
|
|
16
|
+
this.files = getFiles(nativeDataTransfer);
|
|
17
|
+
/**
|
|
18
|
+
* The native DataTransfer object.
|
|
19
|
+
*
|
|
20
|
+
* @private
|
|
21
|
+
* @member {DataTransfer} #_native
|
|
22
|
+
*/
|
|
23
|
+
this._native = nativeDataTransfer;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns an array of available native content types.
|
|
27
|
+
*
|
|
28
|
+
* @returns {Array.<String>}
|
|
29
|
+
*/
|
|
30
|
+
get types() {
|
|
31
|
+
return this._native.types;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Gets the data from the data transfer by its MIME type.
|
|
35
|
+
*
|
|
36
|
+
* dataTransfer.getData( 'text/plain' );
|
|
37
|
+
*
|
|
38
|
+
* @param {String} type The MIME type. E.g. `text/html` or `text/plain`.
|
|
39
|
+
* @returns {String}
|
|
40
|
+
*/
|
|
41
|
+
getData(type) {
|
|
42
|
+
return this._native.getData(type);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sets the data in the data transfer.
|
|
46
|
+
*
|
|
47
|
+
* @param {String} type The MIME type. E.g. `text/html` or `text/plain`.
|
|
48
|
+
* @param {String} data
|
|
49
|
+
*/
|
|
50
|
+
setData(type, data) {
|
|
51
|
+
this._native.setData(type, data);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The effect that is allowed for a drag operation.
|
|
55
|
+
*
|
|
56
|
+
* @param {String} value
|
|
57
|
+
*/
|
|
58
|
+
set effectAllowed(value) {
|
|
59
|
+
this._native.effectAllowed = value;
|
|
60
|
+
}
|
|
61
|
+
get effectAllowed() {
|
|
62
|
+
return this._native.effectAllowed;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The actual drop effect.
|
|
66
|
+
*
|
|
67
|
+
* @param {String} value
|
|
68
|
+
*/
|
|
69
|
+
set dropEffect(value) {
|
|
70
|
+
this._native.dropEffect = value;
|
|
71
|
+
}
|
|
72
|
+
get dropEffect() {
|
|
73
|
+
return this._native.dropEffect;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether the dragging operation was canceled.
|
|
77
|
+
*
|
|
78
|
+
* @returns {Boolean}
|
|
79
|
+
*/
|
|
80
|
+
get isCanceled() {
|
|
81
|
+
return this._native.dropEffect == 'none' || !!this._native.mozUserCancelled;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function getFiles(nativeDataTransfer) {
|
|
85
|
+
// DataTransfer.files and items are array-like and might not have an iterable interface.
|
|
86
|
+
const files = Array.from(nativeDataTransfer.files || []);
|
|
87
|
+
const items = Array.from(nativeDataTransfer.items || []);
|
|
88
|
+
if (files.length) {
|
|
89
|
+
return files;
|
|
90
|
+
}
|
|
91
|
+
// Chrome has empty DataTransfer.files, but allows getting files through the items interface.
|
|
92
|
+
return items
|
|
93
|
+
.filter(item => item.kind === 'file')
|
|
94
|
+
.map(item => item.getAsFile());
|
|
95
|
+
}
|
package/src/view/domconverter.js
CHANGED
|
@@ -726,6 +726,9 @@ export default class DomConverter {
|
|
|
726
726
|
}
|
|
727
727
|
else {
|
|
728
728
|
const domBefore = domParent.childNodes[domOffset - 1];
|
|
729
|
+
if (isText(domBefore) && isInlineFiller(domBefore)) {
|
|
730
|
+
return this.domPositionToView(domBefore.parentNode, indexOf(domBefore));
|
|
731
|
+
}
|
|
729
732
|
const viewBefore = isText(domBefore) ?
|
|
730
733
|
this.findCorrespondingViewText(domBefore) :
|
|
731
734
|
this.mapDomToView(domBefore);
|
|
@@ -950,8 +953,15 @@ export default class DomConverter {
|
|
|
950
953
|
// Since it takes multiple lines of code to check whether a "DOM Position" is before/after another "DOM Position",
|
|
951
954
|
// we will use the fact that range will collapse if it's end is before it's start.
|
|
952
955
|
const range = this._domDocument.createRange();
|
|
953
|
-
|
|
954
|
-
|
|
956
|
+
try {
|
|
957
|
+
range.setStart(selection.anchorNode, selection.anchorOffset);
|
|
958
|
+
range.setEnd(selection.focusNode, selection.focusOffset);
|
|
959
|
+
}
|
|
960
|
+
catch (e) {
|
|
961
|
+
// Safari sometimes gives us a selection that makes Range.set{Start,End} throw.
|
|
962
|
+
// See https://github.com/ckeditor/ckeditor5/issues/12375.
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
955
965
|
const backward = range.collapsed;
|
|
956
966
|
range.detach();
|
|
957
967
|
return backward;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module engine/view/editableelement
|
|
8
8
|
*/
|
|
9
9
|
import ContainerElement from './containerelement';
|
|
10
|
-
import
|
|
10
|
+
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
|
|
11
11
|
/**
|
|
12
12
|
* Editable element which can be a {@link module:engine/view/rooteditableelement~RootEditableElement root}
|
|
13
13
|
* or nested editable area in the editor.
|
|
@@ -21,14 +21,34 @@ export default class CompositionObserver extends DomEventObserver {
|
|
|
21
21
|
this.domEventType = ['compositionstart', 'compositionupdate', 'compositionend'];
|
|
22
22
|
const document = this.document;
|
|
23
23
|
document.on('compositionstart', () => {
|
|
24
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
25
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[CompositionObserver] ' +
|
|
26
|
+
// @if CK_DEBUG_TYPING // '┌───────────────────────────── isComposing = true ─────────────────────────────┐',
|
|
27
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green'
|
|
28
|
+
// @if CK_DEBUG_TYPING // );
|
|
29
|
+
// @if CK_DEBUG_TYPING // }
|
|
24
30
|
document.isComposing = true;
|
|
25
|
-
});
|
|
31
|
+
}, { priority: 'low' });
|
|
26
32
|
document.on('compositionend', () => {
|
|
33
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
34
|
+
// @if CK_DEBUG_TYPING // console.log( '%c[CompositionObserver] ' +
|
|
35
|
+
// @if CK_DEBUG_TYPING // '└───────────────────────────── isComposing = false ─────────────────────────────┘',
|
|
36
|
+
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green'
|
|
37
|
+
// @if CK_DEBUG_TYPING // );
|
|
38
|
+
// @if CK_DEBUG_TYPING // }
|
|
27
39
|
document.isComposing = false;
|
|
28
|
-
});
|
|
40
|
+
}, { priority: 'low' });
|
|
29
41
|
}
|
|
30
42
|
onDomEvent(domEvent) {
|
|
31
|
-
|
|
43
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
44
|
+
// @if CK_DEBUG_TYPING // console.group( `%c[CompositionObserver]%c ${ domEvent.type }`, 'color: green', '' );
|
|
45
|
+
// @if CK_DEBUG_TYPING // }
|
|
46
|
+
this.fire(domEvent.type, domEvent, {
|
|
47
|
+
data: domEvent.data
|
|
48
|
+
});
|
|
49
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
50
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
51
|
+
// @if CK_DEBUG_TYPING // }
|
|
32
52
|
}
|
|
33
53
|
}
|
|
34
54
|
/**
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* @module engine/view/observer/inputobserver
|
|
7
7
|
*/
|
|
8
8
|
import DomEventObserver from './domeventobserver';
|
|
9
|
+
import DataTransfer from '../datatransfer';
|
|
10
|
+
import env from '@ckeditor/ckeditor5-utils/src/env';
|
|
9
11
|
/**
|
|
10
12
|
* Observer for events connected with data input.
|
|
11
13
|
*
|
|
12
|
-
* Note
|
|
14
|
+
* **Note**: This observer is attached by {@link module:engine/view/view~View} and available by default in all
|
|
15
|
+
* editor instances.
|
|
13
16
|
*
|
|
14
17
|
* @extends module:engine/view/observer/domeventobserver~DomEventObserver
|
|
15
18
|
*/
|
|
@@ -19,20 +22,144 @@ export default class InputObserver extends DomEventObserver {
|
|
|
19
22
|
this.domEventType = ['beforeinput'];
|
|
20
23
|
}
|
|
21
24
|
onDomEvent(domEvent) {
|
|
22
|
-
|
|
25
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
26
|
+
// @if CK_DEBUG_TYPING // console.group( `%c[InputObserver]%c ${ domEvent.type }: ${ domEvent.inputType }`,
|
|
27
|
+
// @if CK_DEBUG_TYPING // 'color: green', 'color: default'
|
|
28
|
+
// @if CK_DEBUG_TYPING // );
|
|
29
|
+
// @if CK_DEBUG_TYPING // }
|
|
30
|
+
const domTargetRanges = domEvent.getTargetRanges();
|
|
31
|
+
const view = this.view;
|
|
32
|
+
const viewDocument = view.document;
|
|
33
|
+
let dataTransfer = null;
|
|
34
|
+
let data = null;
|
|
35
|
+
let targetRanges = [];
|
|
36
|
+
if (domEvent.dataTransfer) {
|
|
37
|
+
dataTransfer = new DataTransfer(domEvent.dataTransfer);
|
|
38
|
+
}
|
|
39
|
+
if (domEvent.data !== null) {
|
|
40
|
+
data = domEvent.data;
|
|
41
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
42
|
+
// @if CK_DEBUG_TYPING // console.info( `%c[InputObserver]%c event data: %c${ JSON.stringify( data ) }`,
|
|
43
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', 'color: blue;'
|
|
44
|
+
// @if CK_DEBUG_TYPING // );
|
|
45
|
+
// @if CK_DEBUG_TYPING // }
|
|
46
|
+
}
|
|
47
|
+
else if (dataTransfer) {
|
|
48
|
+
data = dataTransfer.getData('text/plain');
|
|
49
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
50
|
+
// @if CK_DEBUG_TYPING // console.info( `%c[InputObserver]%c event data transfer: %c${ JSON.stringify( data ) }`,
|
|
51
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', 'color: blue;'
|
|
52
|
+
// @if CK_DEBUG_TYPING // );
|
|
53
|
+
// @if CK_DEBUG_TYPING // }
|
|
54
|
+
}
|
|
55
|
+
// If the editor selection is fake (an object is selected), the DOM range does not make sense because it is anchored
|
|
56
|
+
// in the fake selection container.
|
|
57
|
+
if (viewDocument.selection.isFake) {
|
|
58
|
+
// Future-proof: in case of multi-range fake selections being possible.
|
|
59
|
+
targetRanges = Array.from(viewDocument.selection.getRanges());
|
|
60
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
61
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[InputObserver]%c using fake selection:',
|
|
62
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', targetRanges,
|
|
63
|
+
// @if CK_DEBUG_TYPING // viewDocument.selection.isFake ? 'fake view selection' : 'fake DOM parent'
|
|
64
|
+
// @if CK_DEBUG_TYPING // );
|
|
65
|
+
// @if CK_DEBUG_TYPING // }
|
|
66
|
+
}
|
|
67
|
+
else if (domTargetRanges.length) {
|
|
68
|
+
targetRanges = domTargetRanges.map(domRange => {
|
|
69
|
+
return view.domConverter.domRangeToView(domRange);
|
|
70
|
+
});
|
|
71
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
72
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[InputObserver]%c using target ranges:',
|
|
73
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', targetRanges
|
|
74
|
+
// @if CK_DEBUG_TYPING // );
|
|
75
|
+
// @if CK_DEBUG_TYPING // }
|
|
76
|
+
}
|
|
77
|
+
// For Android devices we use a fallback to the current DOM selection, Android modifies it according
|
|
78
|
+
// to the expected target ranges of input event.
|
|
79
|
+
else if (env.isAndroid) {
|
|
80
|
+
const domSelection = domEvent.target.ownerDocument.defaultView.getSelection();
|
|
81
|
+
targetRanges = Array.from(view.domConverter.domSelectionToView(domSelection).getRanges());
|
|
82
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
83
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[InputObserver]%c using selection ranges:',
|
|
84
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', targetRanges
|
|
85
|
+
// @if CK_DEBUG_TYPING // );
|
|
86
|
+
// @if CK_DEBUG_TYPING // }
|
|
87
|
+
}
|
|
88
|
+
// Android sometimes fires insertCompositionText with a new-line character at the end of the data
|
|
89
|
+
// instead of firing insertParagraph beforeInput event.
|
|
90
|
+
// Fire the correct type of beforeInput event and ignore the replaced fragment of text because
|
|
91
|
+
// it wants to replace "test" with "test\n".
|
|
92
|
+
// https://github.com/ckeditor/ckeditor5/issues/12368.
|
|
93
|
+
if (env.isAndroid && domEvent.inputType == 'insertCompositionText' && data && data.endsWith('\n')) {
|
|
94
|
+
this.fire(domEvent.type, domEvent, {
|
|
95
|
+
inputType: 'insertParagraph',
|
|
96
|
+
targetRanges: [view.createRange(targetRanges[0].end)]
|
|
97
|
+
});
|
|
98
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
99
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
100
|
+
// @if CK_DEBUG_TYPING // }
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Fire the normalized beforeInput event.
|
|
104
|
+
this.fire(domEvent.type, domEvent, {
|
|
105
|
+
data,
|
|
106
|
+
dataTransfer,
|
|
107
|
+
targetRanges,
|
|
108
|
+
inputType: domEvent.inputType,
|
|
109
|
+
isComposing: domEvent.isComposing
|
|
110
|
+
});
|
|
111
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
112
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
113
|
+
// @if CK_DEBUG_TYPING // }
|
|
23
114
|
}
|
|
24
115
|
}
|
|
25
116
|
/**
|
|
26
|
-
*
|
|
117
|
+
* The data transfer instance of the input event. Corresponds to native `InputEvent#dataTransfer`.
|
|
27
118
|
*
|
|
28
|
-
*
|
|
119
|
+
* The value is `null` when no `dataTransfer` was passed along with the input event.
|
|
29
120
|
*
|
|
30
|
-
*
|
|
121
|
+
* @readonly
|
|
122
|
+
* @member {module:engine/view/datatransfer~DataTransfer|null} module:engine/view/observer/inputobserver~InputEventData#dataTransfer
|
|
123
|
+
*/
|
|
124
|
+
/**
|
|
125
|
+
* A flag indicating that the `beforeinput` event was fired during composition.
|
|
126
|
+
*
|
|
127
|
+
* Corresponds to the
|
|
128
|
+
* {@link module:engine/view/document~Document#event:compositionstart},
|
|
129
|
+
* {@link module:engine/view/document~Document#event:compositionupdate},
|
|
130
|
+
* and {@link module:engine/view/document~Document#event:compositionend } trio.
|
|
131
|
+
*
|
|
132
|
+
* @readonly
|
|
133
|
+
* @member {Boolean} module:engine/view/observer/inputobserver~InputEventData#isComposing
|
|
134
|
+
*/
|
|
135
|
+
/**
|
|
136
|
+
* The type of the input event (e.g. "insertText" or "deleteWordBackward"). Corresponds to native `InputEvent#inputType`.
|
|
137
|
+
*
|
|
138
|
+
* @readonly
|
|
139
|
+
* @member {String} module:engine/view/observer/inputobserver~InputEventData#inputType
|
|
140
|
+
*/
|
|
141
|
+
/**
|
|
142
|
+
* Editing {@link module:engine/view/range~Range view ranges} corresponding to DOM ranges provided by the web browser
|
|
143
|
+
* (as returned by `InputEvent#getTargetRanges()`).
|
|
144
|
+
*
|
|
145
|
+
* @readonly
|
|
146
|
+
* @member {Array.<module:engine/view/range~Range>} module:engine/view/observer/inputobserver~InputEventData#targetRanges
|
|
147
|
+
*/
|
|
148
|
+
/**
|
|
149
|
+
* A unified text data passed along with the input event. Depending on:
|
|
150
|
+
*
|
|
151
|
+
* * the web browser and input events implementation (for instance [Level 1](https://www.w3.org/TR/input-events-1/) or
|
|
152
|
+
* [Level 2](https://www.w3.org/TR/input-events-2/)),
|
|
153
|
+
* * {@link module:engine/view/observer/inputobserver~InputEventData#inputType input type}
|
|
154
|
+
*
|
|
155
|
+
* text data is sometimes passed in the `data` and sometimes in the `dataTransfer` property.
|
|
31
156
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
157
|
+
* * If `InputEvent#data` was set, this property reflects its value.
|
|
158
|
+
* * If `InputEvent#data` is unavailable, this property contains the `'text/plain'` data from
|
|
159
|
+
* {@link module:engine/view/observer/inputobserver~InputEventData#dataTransfer}.
|
|
160
|
+
* * If the event ({@link module:engine/view/observer/inputobserver~InputEventData#inputType input type})
|
|
161
|
+
* provides no data whatsoever, this property is `null`.
|
|
34
162
|
*
|
|
35
|
-
* @
|
|
36
|
-
* @
|
|
37
|
-
* @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
|
|
163
|
+
* @readonly
|
|
164
|
+
* @member {String|null} module:engine/view/observer/inputobserver~InputEventData#data
|
|
38
165
|
*/
|
|
@@ -7,20 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/* globals window */
|
|
9
9
|
import Observer from './observer';
|
|
10
|
-
import
|
|
11
|
-
import { startsWithFiller, getDataWithoutFiller } from '../filler';
|
|
10
|
+
import { startsWithFiller } from '../filler';
|
|
12
11
|
import { isEqualWith } from 'lodash-es';
|
|
13
12
|
/**
|
|
14
|
-
* Mutation observer
|
|
15
|
-
*
|
|
16
|
-
* Because all mutated nodes are marked as "to be rendered" and the
|
|
17
|
-
* {@link module:engine/view/renderer~Renderer#render} is called, all changes will be reverted, unless the mutation will be handled by the
|
|
18
|
-
* {@link module:engine/view/document~Document#event:mutations} event listener. It means user will see only handled changes, and the editor
|
|
19
|
-
* will block all changes which are not handled.
|
|
13
|
+
* Mutation observer's role is to watch for any DOM changes inside the editor that weren't
|
|
14
|
+
* done by the editor's {@link module:engine/view/renderer~Renderer} itself and reverting these changes.
|
|
20
15
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* {@link module:engine/view/
|
|
16
|
+
* It does this by observing all mutations in the DOM, marking related view elements as changed and calling
|
|
17
|
+
* {@link module:engine/view/renderer~Renderer#render}. Because all mutated nodes are marked as
|
|
18
|
+
* "to be rendered" and the {@link module:engine/view/renderer~Renderer#render `render()`} method is called,
|
|
19
|
+
* all changes are reverted in the DOM (the DOM is synced with the editor's view structure).
|
|
24
20
|
*
|
|
25
21
|
* Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
|
|
26
22
|
*
|
|
@@ -38,7 +34,6 @@ export default class MutationObserver extends Observer {
|
|
|
38
34
|
this._config = {
|
|
39
35
|
childList: true,
|
|
40
36
|
characterData: true,
|
|
41
|
-
characterDataOldValue: true,
|
|
42
37
|
subtree: true
|
|
43
38
|
};
|
|
44
39
|
/**
|
|
@@ -69,8 +64,7 @@ export default class MutationObserver extends Observer {
|
|
|
69
64
|
this._mutationObserver = new window.MutationObserver(this._onMutations.bind(this));
|
|
70
65
|
}
|
|
71
66
|
/**
|
|
72
|
-
* Synchronously
|
|
73
|
-
* At the same time empties the queue so mutations will not be fired twice.
|
|
67
|
+
* Synchronously handles mutations and empties the queue.
|
|
74
68
|
*/
|
|
75
69
|
flush() {
|
|
76
70
|
this._onMutations(this._mutationObserver.takeRecords());
|
|
@@ -108,7 +102,7 @@ export default class MutationObserver extends Observer {
|
|
|
108
102
|
this._mutationObserver.disconnect();
|
|
109
103
|
}
|
|
110
104
|
/**
|
|
111
|
-
* Handles mutations.
|
|
105
|
+
* Handles mutations. Mark view elements to sync and call render.
|
|
112
106
|
*
|
|
113
107
|
* @private
|
|
114
108
|
* @param {Array.<Object>} domMutations Array of native mutations.
|
|
@@ -120,20 +114,21 @@ export default class MutationObserver extends Observer {
|
|
|
120
114
|
}
|
|
121
115
|
const domConverter = this.domConverter;
|
|
122
116
|
// Use map and set for deduplication.
|
|
123
|
-
const
|
|
124
|
-
const
|
|
117
|
+
const mutatedTextNodes = new Set();
|
|
118
|
+
const elementsWithMutatedChildren = new Set();
|
|
125
119
|
// Handle `childList` mutations first, so we will be able to check if the `characterData` mutation is in the
|
|
126
120
|
// element with changed structure anyway.
|
|
127
121
|
for (const mutation of domMutations) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
122
|
+
const element = domConverter.mapDomToView(mutation.target);
|
|
123
|
+
if (!element) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Do not collect mutations from UIElements and RawElements.
|
|
127
|
+
if (element.is('uiElement') || element.is('rawElement')) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (mutation.type === 'childList' && !this._isBogusBrMutation(mutation)) {
|
|
131
|
+
elementsWithMutatedChildren.add(element);
|
|
137
132
|
}
|
|
138
133
|
}
|
|
139
134
|
// Handle `characterData` mutations later, when we have the full list of nodes which changed structure.
|
|
@@ -145,87 +140,48 @@ export default class MutationObserver extends Observer {
|
|
|
145
140
|
}
|
|
146
141
|
if (mutation.type === 'characterData') {
|
|
147
142
|
const text = domConverter.findCorrespondingViewText(mutation.target);
|
|
148
|
-
if (text && !
|
|
149
|
-
|
|
150
|
-
// we will have only one in the map.
|
|
151
|
-
mutatedTexts.set(text, {
|
|
152
|
-
type: 'text',
|
|
153
|
-
oldText: text.data,
|
|
154
|
-
newText: getDataWithoutFiller(mutation.target),
|
|
155
|
-
node: text
|
|
156
|
-
});
|
|
143
|
+
if (text && !elementsWithMutatedChildren.has(text.parent)) {
|
|
144
|
+
mutatedTextNodes.add(text);
|
|
157
145
|
}
|
|
158
146
|
// When we added first letter to the text node which had only inline filler, for the DOM it is mutation
|
|
159
|
-
// on text, but for the view, where filler text node did not
|
|
160
|
-
// need to
|
|
147
|
+
// on text, but for the view, where filler text node did not exist, new text node was created, so we
|
|
148
|
+
// need to handle it as a 'children' mutation instead of 'text'.
|
|
161
149
|
else if (!text && startsWithFiller(mutation.target)) {
|
|
162
|
-
|
|
150
|
+
elementsWithMutatedChildren.add(domConverter.mapDomToView(mutation.target.parentNode));
|
|
163
151
|
}
|
|
164
152
|
}
|
|
165
153
|
}
|
|
166
|
-
// Now we build the list of mutations to
|
|
154
|
+
// Now we build the list of mutations to mark elements. We did not do it earlier to avoid marking the
|
|
167
155
|
// same node multiple times in case of duplication.
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
this.renderer.markToSync('text',
|
|
172
|
-
viewMutations.push(mutatedText);
|
|
156
|
+
let hasMutations = false;
|
|
157
|
+
for (const textNode of mutatedTextNodes) {
|
|
158
|
+
hasMutations = true;
|
|
159
|
+
this.renderer.markToSync('text', textNode);
|
|
173
160
|
}
|
|
174
|
-
for (const viewElement of
|
|
161
|
+
for (const viewElement of elementsWithMutatedChildren) {
|
|
175
162
|
const domElement = domConverter.mapViewToDom(viewElement);
|
|
176
163
|
const viewChildren = Array.from(viewElement.getChildren());
|
|
177
164
|
const newViewChildren = Array.from(domConverter.domChildrenToView(domElement, { withChildren: false }));
|
|
178
165
|
// It may happen that as a result of many changes (sth was inserted and then removed),
|
|
179
166
|
// both elements haven't really changed. #1031
|
|
180
167
|
if (!isEqualWith(viewChildren, newViewChildren, sameNodes)) {
|
|
168
|
+
hasMutations = true;
|
|
181
169
|
this.renderer.markToSync('children', viewElement);
|
|
182
|
-
viewMutations.push({
|
|
183
|
-
type: 'children',
|
|
184
|
-
oldChildren: viewChildren,
|
|
185
|
-
newChildren: newViewChildren,
|
|
186
|
-
node: viewElement
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
// Retrieve `domSelection` using `ownerDocument` of one of mutated nodes.
|
|
191
|
-
// There should not be simultaneous mutation in multiple documents, so it's fine.
|
|
192
|
-
const domSelection = domMutations[0].target.ownerDocument.getSelection();
|
|
193
|
-
let viewSelection = null;
|
|
194
|
-
if (domSelection && domSelection.anchorNode) {
|
|
195
|
-
// If `domSelection` is inside a dom node that is already bound to a view node from view tree, get
|
|
196
|
-
// corresponding selection in the view and pass it together with `viewMutations`. The `viewSelection` may
|
|
197
|
-
// be used by features handling mutations.
|
|
198
|
-
// Only one range is supported.
|
|
199
|
-
const viewSelectionAnchor = domConverter.domPositionToView(domSelection.anchorNode, domSelection.anchorOffset);
|
|
200
|
-
const viewSelectionFocus = domConverter.domPositionToView(domSelection.focusNode, domSelection.focusOffset);
|
|
201
|
-
// Anchor and focus has to be properly mapped to view.
|
|
202
|
-
if (viewSelectionAnchor && viewSelectionFocus) {
|
|
203
|
-
viewSelection = new ViewSelection(viewSelectionAnchor);
|
|
204
|
-
viewSelection.setFocus(viewSelectionFocus);
|
|
205
170
|
}
|
|
206
171
|
}
|
|
207
172
|
// In case only non-relevant mutations were recorded it skips the event and force render (#5600).
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
//
|
|
173
|
+
if (hasMutations) {
|
|
174
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
175
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[MutationObserver]%c Mutations detected',
|
|
176
|
+
// @if CK_DEBUG_TYPING // 'font-weight:bold;color:green', ''
|
|
177
|
+
// @if CK_DEBUG_TYPING // );
|
|
178
|
+
// @if CK_DEBUG_TYPING // }
|
|
179
|
+
// At this point we have "dirty DOM" (changed) and de-synched view (which has not been changed).
|
|
180
|
+
// In order to "reset DOM" we render the view again.
|
|
212
181
|
this.view.forceRender();
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
//
|
|
216
|
-
if (Array.isArray(child1)) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
// Elements.
|
|
220
|
-
if (child1 === child2) {
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
// Texts.
|
|
224
|
-
else if (child1.is('$text') && child2.is('$text')) {
|
|
225
|
-
return child1.data === child2.data;
|
|
226
|
-
}
|
|
227
|
-
// Not matching types.
|
|
228
|
-
return false;
|
|
182
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
183
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
184
|
+
// @if CK_DEBUG_TYPING // }
|
|
229
185
|
}
|
|
230
186
|
}
|
|
231
187
|
/**
|
|
@@ -248,3 +204,19 @@ export default class MutationObserver extends Observer {
|
|
|
248
204
|
return addedNode && addedNode.is('element', 'br');
|
|
249
205
|
}
|
|
250
206
|
}
|
|
207
|
+
function sameNodes(child1, child2) {
|
|
208
|
+
// First level of comparison (array of children vs array of children) – use the Lodash's default behavior.
|
|
209
|
+
if (Array.isArray(child1)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Elements.
|
|
213
|
+
if (child1 === child2) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
// Texts.
|
|
217
|
+
else if (child1.is('$text') && child2.is('$text')) {
|
|
218
|
+
return child1.data === child2.data;
|
|
219
|
+
}
|
|
220
|
+
// Not matching types.
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
@@ -8,19 +8,18 @@
|
|
|
8
8
|
/* global setInterval, clearInterval */
|
|
9
9
|
import Observer from './observer';
|
|
10
10
|
import MutationObserver from './mutationobserver';
|
|
11
|
+
import env from '@ckeditor/ckeditor5-utils/src/env';
|
|
11
12
|
import { debounce } from 'lodash-es';
|
|
12
13
|
/**
|
|
13
14
|
* Selection observer class observes selection changes in the document. If a selection changes on the document this
|
|
14
|
-
* observer checks if
|
|
15
|
-
* {@link module:engine/view/document~Document#
|
|
16
|
-
*
|
|
17
|
-
* and the DOM selection is different then the view selection.
|
|
15
|
+
* observer checks if the DOM selection is different from the {@link module:engine/view/document~Document#selection view selection}.
|
|
16
|
+
* The selection observer fires {@link module:engine/view/document~Document#event:selectionChange} event only if
|
|
17
|
+
* a selection change was the only change in the document and the DOM selection is different from the view selection.
|
|
18
18
|
*
|
|
19
19
|
* This observer also manages the {@link module:engine/view/document~Document#isSelecting} property of the view document.
|
|
20
20
|
*
|
|
21
21
|
* Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
|
|
22
22
|
*
|
|
23
|
-
* @see module:engine/view/observer/mutationobserver~MutationObserver
|
|
24
23
|
* @extends module:engine/view/observer/observer~Observer
|
|
25
24
|
*/
|
|
26
25
|
export default class SelectionObserver extends Observer {
|
|
@@ -133,7 +132,30 @@ export default class SelectionObserver extends Observer {
|
|
|
133
132
|
// handler would like to check it and update (for example table multi cell selection).
|
|
134
133
|
this.listenTo(domDocument, 'mouseup', endDocumentIsSelecting, { priority: 'highest', useCapture: true });
|
|
135
134
|
this.listenTo(domDocument, 'selectionchange', (evt, domEvent) => {
|
|
135
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
136
|
+
// @if CK_DEBUG_TYPING // const domSelection = domDocument.defaultView.getSelection();
|
|
137
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[SelectionObserver]%c selectionchange', 'color:green', ''
|
|
138
|
+
// @if CK_DEBUG_TYPING // );
|
|
139
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[SelectionObserver]%c DOM Selection:', 'font-weight:bold;color:green', '',
|
|
140
|
+
// @if CK_DEBUG_TYPING // { node: domSelection.anchorNode, offset: domSelection.anchorOffset },
|
|
141
|
+
// @if CK_DEBUG_TYPING // { node: domSelection.focusNode, offset: domSelection.focusOffset }
|
|
142
|
+
// @if CK_DEBUG_TYPING // );
|
|
143
|
+
// @if CK_DEBUG_TYPING // }
|
|
144
|
+
// The Renderer is disabled while composing on non-android browsers, so we can't update the view selection
|
|
145
|
+
// because the DOM and view tree drifted apart. Position mapping could fail because of it.
|
|
146
|
+
if (this.document.isComposing && !env.isAndroid) {
|
|
147
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
148
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[SelectionObserver]%c Selection change ignored (isComposing)',
|
|
149
|
+
// @if CK_DEBUG_TYPING // 'font-weight:bold;color:green', ''
|
|
150
|
+
// @if CK_DEBUG_TYPING // );
|
|
151
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
152
|
+
// @if CK_DEBUG_TYPING // }
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
136
155
|
this._handleSelectionChange(domEvent, domDocument);
|
|
156
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
157
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
158
|
+
// @if CK_DEBUG_TYPING // }
|
|
137
159
|
// Defer the safety timeout when the selection changes (e.g. the user keeps extending the selection
|
|
138
160
|
// using their mouse).
|
|
139
161
|
this._documentIsSelectingInactivityTimeoutDebounced();
|
|
@@ -168,8 +190,6 @@ export default class SelectionObserver extends Observer {
|
|
|
168
190
|
}
|
|
169
191
|
// Ensure the mutation event will be before selection event on all browsers.
|
|
170
192
|
this.mutationObserver.flush();
|
|
171
|
-
// If there were mutations then the view will be re-rendered by the mutation observer and the selection
|
|
172
|
-
// will be updated, so the selections will equal and the event will not be fired, as expected.
|
|
173
193
|
const newViewSelection = this.domConverter.domSelectionToView(domSelection);
|
|
174
194
|
// Do not convert selection change if the new view selection has no ranges in it.
|
|
175
195
|
//
|
|
@@ -204,8 +224,14 @@ export default class SelectionObserver extends Observer {
|
|
|
204
224
|
const data = {
|
|
205
225
|
oldSelection: this.selection,
|
|
206
226
|
newSelection: newViewSelection,
|
|
207
|
-
domSelection
|
|
227
|
+
domSelection
|
|
208
228
|
};
|
|
229
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
230
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[SelectionObserver]%c Fire selection change:',
|
|
231
|
+
// @if CK_DEBUG_TYPING // 'font-weight:bold;color:green', '',
|
|
232
|
+
// @if CK_DEBUG_TYPING // newViewSelection.getFirstRange()
|
|
233
|
+
// @if CK_DEBUG_TYPING // );
|
|
234
|
+
// @if CK_DEBUG_TYPING // }
|
|
209
235
|
// Prepare data for new selection and fire appropriate events.
|
|
210
236
|
this.document.fire('selectionChange', data);
|
|
211
237
|
// Call `#_fireSelectionChangeDoneDebounced` every time when `selectionChange` event is fired.
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
6
|
-
* @module engine/view/observer/tabobserver
|
|
7
|
-
*/
|
|
8
5
|
import Observer from './observer';
|
|
9
6
|
import BubblingEventInfo from './bubblingeventinfo';
|
|
10
7
|
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
|
package/src/view/placeholder.js
CHANGED
|
@@ -36,6 +36,10 @@ export function enablePlaceholder(options) {
|
|
|
36
36
|
// If a post-fixer callback makes a change, it should return `true` so other post–fixers
|
|
37
37
|
// can re–evaluate the document again.
|
|
38
38
|
doc.registerPostFixer(writer => updateDocumentPlaceholders(doc, writer));
|
|
39
|
+
// Update placeholders on isComposing state change since rendering is disabled while in composition mode.
|
|
40
|
+
doc.on('change:isComposing', () => {
|
|
41
|
+
view.change(writer => updateDocumentPlaceholders(doc, writer));
|
|
42
|
+
}, { priority: 'high' });
|
|
39
43
|
}
|
|
40
44
|
// Store information about the element placeholder under its document.
|
|
41
45
|
documentPlaceholders.get(doc).set(element, {
|
|
@@ -137,17 +141,20 @@ export function needsPlaceholder(element, keepOnFocus) {
|
|
|
137
141
|
if (hasContent) {
|
|
138
142
|
return false;
|
|
139
143
|
}
|
|
144
|
+
const doc = element.document;
|
|
145
|
+
const viewSelection = doc.selection;
|
|
146
|
+
const selectionAnchor = viewSelection.anchor;
|
|
147
|
+
if (doc.isComposing && selectionAnchor && selectionAnchor.parent === element) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
140
150
|
// Skip the focus check and make the placeholder visible already regardless of document focus state.
|
|
141
151
|
if (keepOnFocus) {
|
|
142
152
|
return true;
|
|
143
153
|
}
|
|
144
|
-
const doc = element.document;
|
|
145
154
|
// If the document is blurred.
|
|
146
155
|
if (!doc.isFocused) {
|
|
147
156
|
return true;
|
|
148
157
|
}
|
|
149
|
-
const viewSelection = doc.selection;
|
|
150
|
-
const selectionAnchor = viewSelection.anchor;
|
|
151
158
|
// If document is focused and the element is empty but the selection is not anchored inside it.
|
|
152
159
|
return !!selectionAnchor && selectionAnchor.parent !== element;
|
|
153
160
|
}
|
package/src/view/renderer.js
CHANGED
|
@@ -114,6 +114,20 @@ export default class Renderer extends Observable {
|
|
|
114
114
|
}
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* True if composition is in progress inside the document.
|
|
119
|
+
*
|
|
120
|
+
* This property is bound to the {@link module:engine/view/document~Document#isComposing `Document#isComposing`} property.
|
|
121
|
+
*
|
|
122
|
+
* @member {Boolean}
|
|
123
|
+
* @observable
|
|
124
|
+
*/
|
|
125
|
+
this.set('isComposing', false);
|
|
126
|
+
this.on('change:isComposing', () => {
|
|
127
|
+
if (!this.isComposing) {
|
|
128
|
+
this.render();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
117
131
|
/**
|
|
118
132
|
* The text node in which the inline filler was rendered.
|
|
119
133
|
*
|
|
@@ -181,6 +195,23 @@ export default class Renderer extends Observable {
|
|
|
181
195
|
* removed as long as the selection is in the text node which needed it at first.
|
|
182
196
|
*/
|
|
183
197
|
render() {
|
|
198
|
+
// Ignore rendering while in the composition mode. Composition events are not cancellable and browser will modify the DOM tree.
|
|
199
|
+
// All marked elements, attributes, etc. will wait until next render after the composition ends.
|
|
200
|
+
// On Android composition events are immediately applied to the model, so we don't need to skip rendering,
|
|
201
|
+
// and we should not do it because the difference between view and DOM could lead to position mapping problems.
|
|
202
|
+
if (this.isComposing && !env.isAndroid) {
|
|
203
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
204
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Rendering aborted while isComposing',
|
|
205
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
|
|
206
|
+
// @if CK_DEBUG_TYPING // );
|
|
207
|
+
// @if CK_DEBUG_TYPING // }
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
211
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Rendering',
|
|
212
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
|
|
213
|
+
// @if CK_DEBUG_TYPING // );
|
|
214
|
+
// @if CK_DEBUG_TYPING // }
|
|
184
215
|
let inlineFillerPosition = null;
|
|
185
216
|
const isInlineFillerRenderingPossible = env.isBlink && !env.isAndroid ? !this.isSelecting : true;
|
|
186
217
|
// Refresh mappings.
|
|
@@ -265,6 +296,9 @@ export default class Renderer extends Observable {
|
|
|
265
296
|
this.markedTexts.clear();
|
|
266
297
|
this.markedAttributes.clear();
|
|
267
298
|
this.markedChildren.clear();
|
|
299
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
300
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
301
|
+
// @if CK_DEBUG_TYPING // }
|
|
268
302
|
}
|
|
269
303
|
/**
|
|
270
304
|
* Updates mappings of view element's children.
|
|
@@ -446,6 +480,11 @@ export default class Renderer extends Observable {
|
|
|
446
480
|
if (nodeBefore instanceof ViewText || nodeAfter instanceof ViewText) {
|
|
447
481
|
return false;
|
|
448
482
|
}
|
|
483
|
+
// Do not use inline filler while typing outside inline elements on Android.
|
|
484
|
+
// The deleteContentBackward would remove part of the inline filler instead of removing last letter in a link.
|
|
485
|
+
if (env.isAndroid && (nodeBefore || nodeAfter)) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
449
488
|
return true;
|
|
450
489
|
}
|
|
451
490
|
/**
|
|
@@ -460,23 +499,20 @@ export default class Renderer extends Observable {
|
|
|
460
499
|
_updateText(viewText, options) {
|
|
461
500
|
const domText = this.domConverter.findCorrespondingDomText(viewText);
|
|
462
501
|
const newDomText = this.domConverter.viewToDom(viewText);
|
|
463
|
-
const actualText = domText.data;
|
|
464
502
|
let expectedText = newDomText.data;
|
|
465
503
|
const filler = options.inlineFillerPosition;
|
|
466
504
|
if (filler && filler.parent == viewText.parent && filler.offset == viewText.index) {
|
|
467
505
|
expectedText = INLINE_FILLER + expectedText;
|
|
468
506
|
}
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
}
|
|
507
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
508
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update text',
|
|
509
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
|
|
510
|
+
// @if CK_DEBUG_TYPING // );
|
|
511
|
+
// @if CK_DEBUG_TYPING // }
|
|
512
|
+
updateTextNode(domText, expectedText);
|
|
513
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
514
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
515
|
+
// @if CK_DEBUG_TYPING // }
|
|
480
516
|
}
|
|
481
517
|
/**
|
|
482
518
|
* Checks if attribute list needs to be updated and possibly updates it.
|
|
@@ -510,6 +546,9 @@ export default class Renderer extends Observable {
|
|
|
510
546
|
/**
|
|
511
547
|
* Checks if elements child list needs to be updated and possibly updates it.
|
|
512
548
|
*
|
|
549
|
+
* Note that on Android, to reduce the risk of composition breaks, it tries to update data of an existing
|
|
550
|
+
* child text nodes instead of replacing them completely.
|
|
551
|
+
*
|
|
513
552
|
* @private
|
|
514
553
|
* @param {module:engine/view/element~Element} viewElement View element to update.
|
|
515
554
|
* @param {Object} options
|
|
@@ -523,8 +562,27 @@ export default class Renderer extends Observable {
|
|
|
523
562
|
// There is no need to process it. It will be processed when re-inserted.
|
|
524
563
|
return;
|
|
525
564
|
}
|
|
565
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
566
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update children',
|
|
567
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
|
|
568
|
+
// @if CK_DEBUG_TYPING // );
|
|
569
|
+
// @if CK_DEBUG_TYPING // }
|
|
570
|
+
// IME on Android inserts a new text node while typing after a link
|
|
571
|
+
// instead of updating an existing text node that follows the link.
|
|
572
|
+
// We must normalize those text nodes so the diff won't get confused.
|
|
573
|
+
// https://github.com/ckeditor/ckeditor5/issues/12574.
|
|
574
|
+
if (env.isAndroid) {
|
|
575
|
+
let previousDomNode = null;
|
|
576
|
+
for (const domNode of Array.from(domElement.childNodes)) {
|
|
577
|
+
if (previousDomNode && isText(previousDomNode) && isText(domNode)) {
|
|
578
|
+
domElement.normalize();
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
previousDomNode = domNode;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
526
584
|
const inlineFillerPosition = options.inlineFillerPosition;
|
|
527
|
-
const actualDomChildren =
|
|
585
|
+
const actualDomChildren = domElement.childNodes;
|
|
528
586
|
const expectedDomChildren = Array.from(this.domConverter.viewChildrenToDom(viewElement, { bind: true }));
|
|
529
587
|
// Inline filler element has to be created as it is present in the DOM, but not in the view. It is required
|
|
530
588
|
// during diffing so text nodes could be compared correctly and also during rendering to maintain
|
|
@@ -533,6 +591,17 @@ export default class Renderer extends Observable {
|
|
|
533
591
|
addInlineFiller(domElement.ownerDocument, expectedDomChildren, inlineFillerPosition.offset);
|
|
534
592
|
}
|
|
535
593
|
const diff = this._diffNodeLists(actualDomChildren, expectedDomChildren);
|
|
594
|
+
// The rendering is not disabled on Android in the composition mode.
|
|
595
|
+
// Composition events are not cancellable and browser will modify the DOM tree.
|
|
596
|
+
// On Android composition events are immediately applied to the model, so we don't need to skip rendering,
|
|
597
|
+
// and we should not do it because the difference between view and DOM could lead to position mapping problems.
|
|
598
|
+
// Since the composition is fragile and often breaks if the composed text node is replaced while composing
|
|
599
|
+
// we need to make sure that we update the existing text node and not replace it with another one.
|
|
600
|
+
// We don't want to change the behavior on other browsers for safety, but maybe one day cause it seems to make sense.
|
|
601
|
+
// https://github.com/ckeditor/ckeditor5/issues/12455.
|
|
602
|
+
const actions = env.isAndroid ?
|
|
603
|
+
this._findReplaceActions(diff, actualDomChildren, expectedDomChildren, { replaceText: true }) :
|
|
604
|
+
diff;
|
|
536
605
|
let i = 0;
|
|
537
606
|
const nodesToUnbind = new Set();
|
|
538
607
|
// Handle deletions first.
|
|
@@ -541,21 +610,44 @@ export default class Renderer extends Observable {
|
|
|
541
610
|
// and it disrupts the whole algorithm. See https://github.com/ckeditor/ckeditor5/issues/6367.
|
|
542
611
|
//
|
|
543
612
|
// It doesn't matter in what order we remove or add nodes, as long as we remove and add correct nodes at correct indexes.
|
|
544
|
-
for (const action of
|
|
613
|
+
for (const action of actions) {
|
|
545
614
|
if (action === 'delete') {
|
|
615
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
616
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Remove node',
|
|
617
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', actualDomChildren[ i ]
|
|
618
|
+
// @if CK_DEBUG_TYPING // );
|
|
619
|
+
// @if CK_DEBUG_TYPING // }
|
|
546
620
|
nodesToUnbind.add(actualDomChildren[i]);
|
|
547
621
|
remove(actualDomChildren[i]);
|
|
548
622
|
}
|
|
549
|
-
else if (action === 'equal') {
|
|
623
|
+
else if (action === 'equal' || action === 'replace') {
|
|
550
624
|
i++;
|
|
551
625
|
}
|
|
552
626
|
}
|
|
553
627
|
i = 0;
|
|
554
|
-
for (const action of
|
|
628
|
+
for (const action of actions) {
|
|
555
629
|
if (action === 'insert') {
|
|
630
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
631
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Insert node',
|
|
632
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', expectedDomChildren[ i ]
|
|
633
|
+
// @if CK_DEBUG_TYPING // );
|
|
634
|
+
// @if CK_DEBUG_TYPING // }
|
|
556
635
|
insertAt(domElement, i, expectedDomChildren[i]);
|
|
557
636
|
i++;
|
|
558
637
|
}
|
|
638
|
+
// Update the existing text node data. Note that replace action is generated only for Android for now.
|
|
639
|
+
else if (action === 'replace') {
|
|
640
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
641
|
+
// @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update text node',
|
|
642
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
|
|
643
|
+
// @if CK_DEBUG_TYPING // );
|
|
644
|
+
// @if CK_DEBUG_TYPING // }
|
|
645
|
+
updateTextNode(actualDomChildren[i], expectedDomChildren[i].data);
|
|
646
|
+
i++;
|
|
647
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
648
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
649
|
+
// @if CK_DEBUG_TYPING // }
|
|
650
|
+
}
|
|
559
651
|
else if (action === 'equal') {
|
|
560
652
|
// Force updating text nodes inside elements which did not change and do not need to be re-rendered (#1125).
|
|
561
653
|
// Do it here (not in the loop above) because only after insertions the `i` index is correct.
|
|
@@ -571,6 +663,9 @@ export default class Renderer extends Observable {
|
|
|
571
663
|
this.domConverter.unbindDomElement(node);
|
|
572
664
|
}
|
|
573
665
|
}
|
|
666
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
667
|
+
// @if CK_DEBUG_TYPING // console.groupEnd();
|
|
668
|
+
// @if CK_DEBUG_TYPING // }
|
|
574
669
|
}
|
|
575
670
|
/**
|
|
576
671
|
* Shorthand for diffing two arrays or node lists of DOM nodes.
|
|
@@ -597,9 +692,11 @@ export default class Renderer extends Observable {
|
|
|
597
692
|
* @param {Array.<String>} actions Actions array which is a result of the {@link module:utils/diff~diff} function.
|
|
598
693
|
* @param {Array.<ViewNode>|NodeList} actualDom Actual DOM children
|
|
599
694
|
* @param {Array.<ViewNode>} expectedDom Expected DOM children.
|
|
695
|
+
* @param {Object} [options] Options
|
|
696
|
+
* @param {Boolean} [options.replaceText] Mark text nodes replacement.
|
|
600
697
|
* @returns {Array.<String>} Actions array modified with the `replace` actions.
|
|
601
698
|
*/
|
|
602
|
-
_findReplaceActions(actions, actualDom, expectedDom) {
|
|
699
|
+
_findReplaceActions(actions, actualDom, expectedDom, options = {}) {
|
|
603
700
|
// If there is no both 'insert' and 'delete' actions, no need to check for replaced elements.
|
|
604
701
|
if (actions.indexOf('insert') === -1 || actions.indexOf('delete') === -1) {
|
|
605
702
|
return actions;
|
|
@@ -616,7 +713,8 @@ export default class Renderer extends Observable {
|
|
|
616
713
|
actualSlice.push(actualDom[counter.equal + counter.delete]);
|
|
617
714
|
}
|
|
618
715
|
else { // equal
|
|
619
|
-
newActions = newActions.concat(diff(actualSlice, expectedSlice,
|
|
716
|
+
newActions = newActions.concat(diff(actualSlice, expectedSlice, options.replaceText ? areTextNodes : areSimilar)
|
|
717
|
+
.map(x => x === 'equal' ? 'replace' : x));
|
|
620
718
|
newActions.push('equal');
|
|
621
719
|
// Reset stored elements on 'equal'.
|
|
622
720
|
actualSlice = [];
|
|
@@ -624,7 +722,8 @@ export default class Renderer extends Observable {
|
|
|
624
722
|
}
|
|
625
723
|
counter[action]++;
|
|
626
724
|
}
|
|
627
|
-
return newActions.concat(diff(actualSlice, expectedSlice,
|
|
725
|
+
return newActions.concat(diff(actualSlice, expectedSlice, options.replaceText ? areTextNodes : areSimilar)
|
|
726
|
+
.map(x => x === 'equal' ? 'replace' : x));
|
|
628
727
|
}
|
|
629
728
|
/**
|
|
630
729
|
* Marks text nodes to be synchronized.
|
|
@@ -671,14 +770,23 @@ export default class Renderer extends Observable {
|
|
|
671
770
|
if (!this.isFocused || !domRoot) {
|
|
672
771
|
return;
|
|
673
772
|
}
|
|
674
|
-
// Render selection.
|
|
773
|
+
// Render fake selection - create the fake selection container (if needed) and move DOM selection to it.
|
|
675
774
|
if (this.selection.isFake) {
|
|
676
775
|
this._updateFakeSelection(domRoot);
|
|
677
776
|
}
|
|
678
|
-
|
|
777
|
+
// There was a fake selection so remove it and update the DOM selection.
|
|
778
|
+
// This is especially important on Android because otherwise IME will try to compose over the fake selection container.
|
|
779
|
+
else if (this._fakeSelectionContainer && this._fakeSelectionContainer.isConnected) {
|
|
679
780
|
this._removeFakeSelection();
|
|
680
781
|
this._updateDomSelection(domRoot);
|
|
681
782
|
}
|
|
783
|
+
// Update the DOM selection in case of a plain selection change (no fake selection is involved).
|
|
784
|
+
// On non-Android the whole rendering is disabled in composition mode (including DOM selection update),
|
|
785
|
+
// but updating DOM selection should be also disabled on Android if in the middle of the composition
|
|
786
|
+
// (to not interrupt it).
|
|
787
|
+
else if (!(this.isComposing && env.isAndroid)) {
|
|
788
|
+
this._updateDomSelection(domRoot);
|
|
789
|
+
}
|
|
682
790
|
}
|
|
683
791
|
/**
|
|
684
792
|
* Updates the fake selection.
|
|
@@ -726,6 +834,11 @@ export default class Renderer extends Observable {
|
|
|
726
834
|
// selected. If there is any editable selected, it is okay (editable is taken from selection anchor).
|
|
727
835
|
const anchor = this.domConverter.viewPositionToDom(this.selection.anchor);
|
|
728
836
|
const focus = this.domConverter.viewPositionToDom(this.selection.focus);
|
|
837
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
838
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update DOM selection:',
|
|
839
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', anchor, focus
|
|
840
|
+
// @if CK_DEBUG_TYPING // );
|
|
841
|
+
// @if CK_DEBUG_TYPING // }
|
|
729
842
|
domSelection.collapse(anchor.parent, anchor.offset);
|
|
730
843
|
domSelection.extend(focus.parent, focus.offset);
|
|
731
844
|
// Firefox–specific hack (https://github.com/ckeditor/ckeditor5-engine/issues/1439).
|
|
@@ -873,6 +986,11 @@ function areSimilar(node1, node2) {
|
|
|
873
986
|
!isComment(node1) && !isComment(node2) &&
|
|
874
987
|
node1.tagName.toLowerCase() === node2.tagName.toLowerCase();
|
|
875
988
|
}
|
|
989
|
+
// Whether two DOM nodes are text nodes.
|
|
990
|
+
function areTextNodes(node1, node2) {
|
|
991
|
+
return isNode(node1) && isNode(node2) &&
|
|
992
|
+
isText(node1) && isText(node2);
|
|
993
|
+
}
|
|
876
994
|
// Whether two dom nodes should be considered as the same.
|
|
877
995
|
// Two nodes which are considered the same are:
|
|
878
996
|
//
|
|
@@ -953,3 +1071,35 @@ function createFakeSelectionContainer(domDocument) {
|
|
|
953
1071
|
container.textContent = '\u00A0';
|
|
954
1072
|
return container;
|
|
955
1073
|
}
|
|
1074
|
+
// Checks if text needs to be updated and possibly updates it by removing and inserting only parts
|
|
1075
|
+
// of the data from the existing text node to reduce impact on the IME composition.
|
|
1076
|
+
//
|
|
1077
|
+
// @param {Text} domText DOM text node to update.
|
|
1078
|
+
// @param {String} expectedText The expected data of a text node.
|
|
1079
|
+
function updateTextNode(domText, expectedText) {
|
|
1080
|
+
const actualText = domText.data;
|
|
1081
|
+
if (actualText == expectedText) {
|
|
1082
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
1083
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Text node does not need update:',
|
|
1084
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '',
|
|
1085
|
+
// @if CK_DEBUG_TYPING // `"${ domText.data }" (${ domText.data.length })`
|
|
1086
|
+
// @if CK_DEBUG_TYPING // );
|
|
1087
|
+
// @if CK_DEBUG_TYPING // }
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
// @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
|
|
1091
|
+
// @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update text node:',
|
|
1092
|
+
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '',
|
|
1093
|
+
// @if CK_DEBUG_TYPING // `"${ domText.data }" (${ domText.data.length }) -> "${ expectedText }" (${ expectedText.length })`
|
|
1094
|
+
// @if CK_DEBUG_TYPING // );
|
|
1095
|
+
// @if CK_DEBUG_TYPING // }
|
|
1096
|
+
const actions = fastDiff(actualText, expectedText);
|
|
1097
|
+
for (const action of actions) {
|
|
1098
|
+
if (action.type === 'insert') {
|
|
1099
|
+
domText.insertData(action.index, action.values.join(''));
|
|
1100
|
+
}
|
|
1101
|
+
else { // 'delete'
|
|
1102
|
+
domText.deleteData(action.index, action.howMany);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
package/src/view/view.js
CHANGED
|
@@ -12,9 +12,9 @@ import DomConverter from './domconverter';
|
|
|
12
12
|
import Position from './position';
|
|
13
13
|
import Range from './range';
|
|
14
14
|
import Selection from './selection';
|
|
15
|
-
import MutationObserver from './observer/mutationobserver';
|
|
16
15
|
import KeyObserver from './observer/keyobserver';
|
|
17
16
|
import FakeSelectionObserver from './observer/fakeselectionobserver';
|
|
17
|
+
import MutationObserver from './observer/mutationobserver';
|
|
18
18
|
import SelectionObserver from './observer/selectionobserver';
|
|
19
19
|
import FocusObserver from './observer/focusobserver';
|
|
20
20
|
import CompositionObserver from './observer/compositionobserver';
|
|
@@ -26,7 +26,6 @@ import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/sc
|
|
|
26
26
|
import { injectUiElementHandling } from './uielement';
|
|
27
27
|
import { injectQuirksHandling } from './filler';
|
|
28
28
|
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
|
|
29
|
-
import env from '@ckeditor/ckeditor5-utils/src/env';
|
|
30
29
|
/**
|
|
31
30
|
* Editor's view controller class. Its main responsibility is DOM - View management for editing purposes, to provide
|
|
32
31
|
* abstraction over the DOM structure and events and hide all browsers quirks.
|
|
@@ -43,12 +42,12 @@ import env from '@ckeditor/ckeditor5-utils/src/env';
|
|
|
43
42
|
* on DOM and fire events on the {@link module:engine/view/document~Document Document}.
|
|
44
43
|
* Note that the following observers are added by the class constructor and are always available:
|
|
45
44
|
*
|
|
46
|
-
* * {@link module:engine/view/observer/mutationobserver~MutationObserver},
|
|
47
45
|
* * {@link module:engine/view/observer/selectionobserver~SelectionObserver},
|
|
48
46
|
* * {@link module:engine/view/observer/focusobserver~FocusObserver},
|
|
49
47
|
* * {@link module:engine/view/observer/keyobserver~KeyObserver},
|
|
50
48
|
* * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
|
|
51
49
|
* * {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
|
|
50
|
+
* * {@link module:engine/view/observer/inputobserver~InputObserver}.
|
|
52
51
|
* * {@link module:engine/view/observer/arrowkeysobserver~ArrowKeysObserver}.
|
|
53
52
|
* * {@link module:engine/view/observer/tabobserver~TabObserver}.
|
|
54
53
|
*
|
|
@@ -109,7 +108,7 @@ export default class View extends Observable {
|
|
|
109
108
|
* @type {module:engine/view/renderer~Renderer}
|
|
110
109
|
*/
|
|
111
110
|
this._renderer = new Renderer(this.domConverter, this.document.selection);
|
|
112
|
-
this._renderer.bind('isFocused', 'isSelecting').to(this.document, 'isFocused', 'isSelecting');
|
|
111
|
+
this._renderer.bind('isFocused', 'isSelecting', 'isComposing').to(this.document, 'isFocused', 'isSelecting', 'isComposing');
|
|
113
112
|
/**
|
|
114
113
|
* A DOM root attributes cache. It saves the initial values of DOM root attributes before the DOM element
|
|
115
114
|
* is {@link module:engine/view/view~View#attachDomRoot attached} to the view so later on, when
|
|
@@ -171,10 +170,8 @@ export default class View extends Observable {
|
|
|
171
170
|
this.addObserver(FakeSelectionObserver);
|
|
172
171
|
this.addObserver(CompositionObserver);
|
|
173
172
|
this.addObserver(ArrowKeysObserver);
|
|
173
|
+
this.addObserver(InputObserver);
|
|
174
174
|
this.addObserver(TabObserver);
|
|
175
|
-
if (env.isAndroid) {
|
|
176
|
-
this.addObserver(InputObserver);
|
|
177
|
-
}
|
|
178
175
|
// Inject quirks handlers.
|
|
179
176
|
injectQuirksHandling(this);
|
|
180
177
|
injectUiElementHandling(this);
|
|
@@ -615,7 +612,7 @@ export default class View extends Observable {
|
|
|
615
612
|
}
|
|
616
613
|
}
|
|
617
614
|
/**
|
|
618
|
-
* Renders all changes. In order to avoid triggering the observers (e.g.
|
|
615
|
+
* Renders all changes. In order to avoid triggering the observers (e.g. selection) all observers are disabled
|
|
619
616
|
* before rendering and re-enabled after that.
|
|
620
617
|
*
|
|
621
618
|
* @private
|