@ckeditor/ckeditor5-engine 30.0.0 → 31.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +22 -20
- package/src/controller/datacontroller.js +50 -1
- package/src/dataprocessor/htmldataprocessor.js +10 -13
- package/src/dataprocessor/xmldataprocessor.js +12 -16
- package/src/dev-utils/view.js +21 -3
- package/src/model/utils/selection-post-fixer.js +10 -2
- package/src/view/document.js +12 -0
- package/src/view/domconverter.js +152 -9
- package/src/view/observer/selectionobserver.js +48 -1
- package/src/view/rawelement.js +3 -2
- package/src/view/renderer.js +101 -37
- package/src/view/uielement.js +5 -2
- package/src/view/view.js +1 -1
- package/theme/renderer.css +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ckeditor/ckeditor5-engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "31.0.0",
|
|
4
4
|
"description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wysiwyg",
|
|
@@ -23,28 +23,30 @@
|
|
|
23
23
|
],
|
|
24
24
|
"main": "src/index.js",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@ckeditor/ckeditor5-utils": "^
|
|
26
|
+
"@ckeditor/ckeditor5-utils": "^31.0.0",
|
|
27
27
|
"lodash-es": "^4.17.15"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@ckeditor/ckeditor5-basic-styles": "^
|
|
31
|
-
"@ckeditor/ckeditor5-block-quote": "^
|
|
32
|
-
"@ckeditor/ckeditor5-clipboard": "^
|
|
33
|
-
"@ckeditor/ckeditor5-
|
|
34
|
-
"@ckeditor/ckeditor5-
|
|
35
|
-
"@ckeditor/ckeditor5-
|
|
36
|
-
"@ckeditor/ckeditor5-
|
|
37
|
-
"@ckeditor/ckeditor5-
|
|
38
|
-
"@ckeditor/ckeditor5-
|
|
39
|
-
"@ckeditor/ckeditor5-
|
|
40
|
-
"@ckeditor/ckeditor5-
|
|
41
|
-
"@ckeditor/ckeditor5-
|
|
42
|
-
"@ckeditor/ckeditor5-
|
|
43
|
-
"@ckeditor/ckeditor5-
|
|
44
|
-
"@ckeditor/ckeditor5-
|
|
45
|
-
"@ckeditor/ckeditor5-
|
|
46
|
-
"@ckeditor/ckeditor5-
|
|
47
|
-
"@ckeditor/ckeditor5-
|
|
30
|
+
"@ckeditor/ckeditor5-basic-styles": "^31.0.0",
|
|
31
|
+
"@ckeditor/ckeditor5-block-quote": "^31.0.0",
|
|
32
|
+
"@ckeditor/ckeditor5-clipboard": "^31.0.0",
|
|
33
|
+
"@ckeditor/ckeditor5-cloud-services": "^31.0.0",
|
|
34
|
+
"@ckeditor/ckeditor5-core": "^31.0.0",
|
|
35
|
+
"@ckeditor/ckeditor5-editor-classic": "^31.0.0",
|
|
36
|
+
"@ckeditor/ckeditor5-enter": "^31.0.0",
|
|
37
|
+
"@ckeditor/ckeditor5-essentials": "^31.0.0",
|
|
38
|
+
"@ckeditor/ckeditor5-heading": "^31.0.0",
|
|
39
|
+
"@ckeditor/ckeditor5-image": "^31.0.0",
|
|
40
|
+
"@ckeditor/ckeditor5-link": "^31.0.0",
|
|
41
|
+
"@ckeditor/ckeditor5-list": "^31.0.0",
|
|
42
|
+
"@ckeditor/ckeditor5-mention": "^31.0.0",
|
|
43
|
+
"@ckeditor/ckeditor5-paragraph": "^31.0.0",
|
|
44
|
+
"@ckeditor/ckeditor5-table": "^31.0.0",
|
|
45
|
+
"@ckeditor/ckeditor5-theme-lark": "^31.0.0",
|
|
46
|
+
"@ckeditor/ckeditor5-typing": "^31.0.0",
|
|
47
|
+
"@ckeditor/ckeditor5-ui": "^31.0.0",
|
|
48
|
+
"@ckeditor/ckeditor5-undo": "^31.0.0",
|
|
49
|
+
"@ckeditor/ckeditor5-widget": "^31.0.0",
|
|
48
50
|
"webpack": "^4.43.0",
|
|
49
51
|
"webpack-cli": "^3.3.11"
|
|
50
52
|
},
|
|
@@ -145,6 +145,7 @@ export default class DataController {
|
|
|
145
145
|
|
|
146
146
|
this.decorate( 'init' );
|
|
147
147
|
this.decorate( 'set' );
|
|
148
|
+
this.decorate( 'get' );
|
|
148
149
|
|
|
149
150
|
// Fire the `ready` event when the initialization has completed. Such low-level listener gives possibility
|
|
150
151
|
// to plug into the initialization pipeline without interrupting the initialization flow.
|
|
@@ -163,6 +164,7 @@ export default class DataController {
|
|
|
163
164
|
* Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and
|
|
164
165
|
* formatted by the {@link #processor data processor}.
|
|
165
166
|
*
|
|
167
|
+
* @fires get
|
|
166
168
|
* @param {Object} [options] Additional configuration for the retrieved data. `DataController` provides two optional
|
|
167
169
|
* properties: `rootName` and `trim`. Other properties of this object are specified by various editor features.
|
|
168
170
|
* @param {String} [options.rootName='main'] Root name.
|
|
@@ -523,6 +525,15 @@ export default class DataController {
|
|
|
523
525
|
*
|
|
524
526
|
* @event set
|
|
525
527
|
*/
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Event fired after {@link #get get() method} has been run.
|
|
531
|
+
*
|
|
532
|
+
* The `get` event is fired by decorated {@link #get} method.
|
|
533
|
+
* See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
|
|
534
|
+
*
|
|
535
|
+
* @event get
|
|
536
|
+
*/
|
|
526
537
|
}
|
|
527
538
|
|
|
528
539
|
mix( DataController, ObservableMixin );
|
|
@@ -559,5 +570,43 @@ function _getMarkersRelativeToElement( element ) {
|
|
|
559
570
|
}
|
|
560
571
|
}
|
|
561
572
|
|
|
562
|
-
|
|
573
|
+
// Sort the markers in a stable fashion to ensure that the order that they are
|
|
574
|
+
// added to the model's marker collection does not affect how they are
|
|
575
|
+
// downcast. One particular use case that we're targeting here is one where
|
|
576
|
+
// two markers are adjacent but not overlapping, such as an insertion/deletion
|
|
577
|
+
// suggestion pair represting the replacement of a range of text. In this
|
|
578
|
+
// case, putting the markers in DOM order causes the first marker's end to be
|
|
579
|
+
// serialized right after the second marker's start, while putting the markers
|
|
580
|
+
// in reverse DOM order causes it to be right before the second marker's
|
|
581
|
+
// start. So, we sort in a way that ensures non-intersecting ranges are in
|
|
582
|
+
// reverse DOM order, and intersecting ranges are in something approximating
|
|
583
|
+
// reverse DOM order (since reverse DOM order doesn't have a precise meaning
|
|
584
|
+
// when working with intersectng ranges).
|
|
585
|
+
return result.sort( ( [ n1, r1 ], [ n2, r2 ] ) => {
|
|
586
|
+
if ( r1.end.compareWith( r2.start ) !== 'after' ) {
|
|
587
|
+
// m1.end <= m2.start -- m1 is entirely <= m2
|
|
588
|
+
return 1;
|
|
589
|
+
} else if ( r1.start.compareWith( r2.end ) !== 'before' ) {
|
|
590
|
+
// m1.start >= m2.end -- m1 is entirely >= m2
|
|
591
|
+
return -1;
|
|
592
|
+
} else {
|
|
593
|
+
// they overlap, so use their start positions as the primary sort key and
|
|
594
|
+
// end positions as the secondary sort key
|
|
595
|
+
switch ( r1.start.compareWith( r2.start ) ) {
|
|
596
|
+
case 'before':
|
|
597
|
+
return 1;
|
|
598
|
+
case 'after':
|
|
599
|
+
return -1;
|
|
600
|
+
default:
|
|
601
|
+
switch ( r1.end.compareWith( r2.end ) ) {
|
|
602
|
+
case 'before':
|
|
603
|
+
return 1;
|
|
604
|
+
case 'after':
|
|
605
|
+
return -1;
|
|
606
|
+
default:
|
|
607
|
+
return n2.localeCompare( n1 );
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
} );
|
|
563
612
|
}
|
|
@@ -28,26 +28,23 @@ export default class HtmlDataProcessor {
|
|
|
28
28
|
/**
|
|
29
29
|
* A DOM parser instance used to parse an HTML string to an HTML document.
|
|
30
30
|
*
|
|
31
|
-
* @private
|
|
32
31
|
* @member {DOMParser}
|
|
33
32
|
*/
|
|
34
|
-
this.
|
|
33
|
+
this.domParser = new DOMParser();
|
|
35
34
|
|
|
36
35
|
/**
|
|
37
36
|
* A DOM converter used to convert DOM elements to view elements.
|
|
38
37
|
*
|
|
39
|
-
* @private
|
|
40
38
|
* @member {module:engine/view/domconverter~DomConverter}
|
|
41
39
|
*/
|
|
42
|
-
this.
|
|
40
|
+
this.domConverter = new DomConverter( document, { renderingMode: 'data' } );
|
|
43
41
|
|
|
44
42
|
/**
|
|
45
43
|
* A basic HTML writer instance used to convert DOM elements to an HTML string.
|
|
46
44
|
*
|
|
47
|
-
* @
|
|
48
|
-
* @member {module:engine/dataprocessor/basichtmlwriter~BasicHtmlWriter}
|
|
45
|
+
* @member {module:engine/dataprocessor/htmlwriter~HtmlWriter}
|
|
49
46
|
*/
|
|
50
|
-
this.
|
|
47
|
+
this.htmlWriter = new BasicHtmlWriter();
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
/**
|
|
@@ -59,10 +56,10 @@ export default class HtmlDataProcessor {
|
|
|
59
56
|
*/
|
|
60
57
|
toData( viewFragment ) {
|
|
61
58
|
// Convert view DocumentFragment to DOM DocumentFragment.
|
|
62
|
-
const domFragment = this.
|
|
59
|
+
const domFragment = this.domConverter.viewToDom( viewFragment, document );
|
|
63
60
|
|
|
64
61
|
// Convert DOM DocumentFragment to HTML output.
|
|
65
|
-
return this.
|
|
62
|
+
return this.htmlWriter.getHtml( domFragment );
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
/**
|
|
@@ -76,7 +73,7 @@ export default class HtmlDataProcessor {
|
|
|
76
73
|
const domFragment = this._toDom( data );
|
|
77
74
|
|
|
78
75
|
// Convert DOM DocumentFragment to view DocumentFragment.
|
|
79
|
-
return this.
|
|
76
|
+
return this.domConverter.domToView( domFragment );
|
|
80
77
|
}
|
|
81
78
|
|
|
82
79
|
/**
|
|
@@ -90,7 +87,7 @@ export default class HtmlDataProcessor {
|
|
|
90
87
|
* be treated as raw data.
|
|
91
88
|
*/
|
|
92
89
|
registerRawContentMatcher( pattern ) {
|
|
93
|
-
this.
|
|
90
|
+
this.domConverter.registerRawContentMatcher( pattern );
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
/**
|
|
@@ -105,7 +102,7 @@ export default class HtmlDataProcessor {
|
|
|
105
102
|
* @param {'default'|'marked'} type Whether to use the default or the marked ` ` block fillers.
|
|
106
103
|
*/
|
|
107
104
|
useFillerType( type ) {
|
|
108
|
-
this.
|
|
105
|
+
this.domConverter.blockFillerMode = type == 'marked' ? 'markedNbsp' : 'nbsp';
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
/**
|
|
@@ -117,7 +114,7 @@ export default class HtmlDataProcessor {
|
|
|
117
114
|
* @returns {DocumentFragment}
|
|
118
115
|
*/
|
|
119
116
|
_toDom( data ) {
|
|
120
|
-
const document = this.
|
|
117
|
+
const document = this.domParser.parseFromString( data, 'text/html' );
|
|
121
118
|
const fragment = document.createDocumentFragment();
|
|
122
119
|
|
|
123
120
|
// The rules for parsing an HTML string can be read on https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhtml.
|
|
@@ -26,7 +26,7 @@ export default class XmlDataProcessor {
|
|
|
26
26
|
*
|
|
27
27
|
* @param {module:engine/view/document~Document} document The view document instance.
|
|
28
28
|
* @param {Object} options Configuration options.
|
|
29
|
-
* @param {Array
|
|
29
|
+
* @param {Array.<String>} [options.namespaces=[]] A list of namespaces allowed to use in the XML input.
|
|
30
30
|
*/
|
|
31
31
|
constructor( document, options = {} ) {
|
|
32
32
|
/**
|
|
@@ -35,35 +35,31 @@ export default class XmlDataProcessor {
|
|
|
35
35
|
* For example, registering namespaces [ 'attribute', 'container' ] allows to use `<attirbute:tagName></attribute:tagName>`
|
|
36
36
|
* and `<container:tagName></container:tagName>` input. It is mainly for debugging.
|
|
37
37
|
*
|
|
38
|
-
* @
|
|
39
|
-
* @member {DOMParser}
|
|
38
|
+
* @member {Array.<String>}
|
|
40
39
|
*/
|
|
41
40
|
this.namespaces = options.namespaces || [];
|
|
42
41
|
|
|
43
42
|
/**
|
|
44
43
|
* DOM parser instance used to parse an XML string to an XML document.
|
|
45
44
|
*
|
|
46
|
-
* @private
|
|
47
45
|
* @member {DOMParser}
|
|
48
46
|
*/
|
|
49
|
-
this.
|
|
47
|
+
this.domParser = new DOMParser();
|
|
50
48
|
|
|
51
49
|
/**
|
|
52
50
|
* DOM converter used to convert DOM elements to view elements.
|
|
53
51
|
*
|
|
54
|
-
* @private
|
|
55
52
|
* @member {module:engine/view/domconverter~DomConverter}
|
|
56
53
|
*/
|
|
57
|
-
this.
|
|
54
|
+
this.domConverter = new DomConverter( document, { renderingMode: 'data' } );
|
|
58
55
|
|
|
59
56
|
/**
|
|
60
57
|
* A basic HTML writer instance used to convert DOM elements to an XML string.
|
|
61
58
|
* There is no need to use a dedicated XML writer because the basic HTML writer works well in this case.
|
|
62
59
|
*
|
|
63
|
-
* @
|
|
64
|
-
* @member {module:engine/dataprocessor/basichtmlwriter~BasicHtmlWriter}
|
|
60
|
+
* @member {module:engine/dataprocessor/htmlwriter~HtmlWriter}
|
|
65
61
|
*/
|
|
66
|
-
this.
|
|
62
|
+
this.htmlWriter = new BasicHtmlWriter();
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
/**
|
|
@@ -75,11 +71,11 @@ export default class XmlDataProcessor {
|
|
|
75
71
|
*/
|
|
76
72
|
toData( viewFragment ) {
|
|
77
73
|
// Convert view DocumentFragment to DOM DocumentFragment.
|
|
78
|
-
const domFragment = this.
|
|
74
|
+
const domFragment = this.domConverter.viewToDom( viewFragment, document );
|
|
79
75
|
|
|
80
76
|
// Convert DOM DocumentFragment to XML output.
|
|
81
77
|
// There is no need to use dedicated for XML serializing method because BasicHtmlWriter works well in this case.
|
|
82
|
-
return this.
|
|
78
|
+
return this.htmlWriter.getHtml( domFragment );
|
|
83
79
|
}
|
|
84
80
|
|
|
85
81
|
/**
|
|
@@ -93,7 +89,7 @@ export default class XmlDataProcessor {
|
|
|
93
89
|
const domFragment = this._toDom( data );
|
|
94
90
|
|
|
95
91
|
// Convert DOM DocumentFragment to view DocumentFragment.
|
|
96
|
-
return this.
|
|
92
|
+
return this.domConverter.domToView( domFragment, { keepOriginalCase: true } );
|
|
97
93
|
}
|
|
98
94
|
|
|
99
95
|
/**
|
|
@@ -107,7 +103,7 @@ export default class XmlDataProcessor {
|
|
|
107
103
|
* be treated as raw data.
|
|
108
104
|
*/
|
|
109
105
|
registerRawContentMatcher( pattern ) {
|
|
110
|
-
this.
|
|
106
|
+
this.domConverter.registerRawContentMatcher( pattern );
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
/**
|
|
@@ -122,7 +118,7 @@ export default class XmlDataProcessor {
|
|
|
122
118
|
* @param {'default'|'marked'} type Whether to use the default or the marked ` ` block fillers.
|
|
123
119
|
*/
|
|
124
120
|
useFillerType( type ) {
|
|
125
|
-
this.
|
|
121
|
+
this.domConverter.blockFillerMode = type == 'marked' ? 'markedNbsp' : 'nbsp';
|
|
126
122
|
}
|
|
127
123
|
|
|
128
124
|
/**
|
|
@@ -140,7 +136,7 @@ export default class XmlDataProcessor {
|
|
|
140
136
|
// Wrap data into root element with optional namespace definitions.
|
|
141
137
|
data = `<xml ${ namespaces }>${ data }</xml>`;
|
|
142
138
|
|
|
143
|
-
const parsedDocument = this.
|
|
139
|
+
const parsedDocument = this.domParser.parseFromString( data, 'text/xml' );
|
|
144
140
|
|
|
145
141
|
// Parse validation.
|
|
146
142
|
const parserError = parsedDocument.querySelector( 'parsererror' );
|
package/src/dev-utils/view.js
CHANGED
|
@@ -39,6 +39,13 @@ const allowedTypes = {
|
|
|
39
39
|
'ui': UIElement,
|
|
40
40
|
'raw': RawElement
|
|
41
41
|
};
|
|
42
|
+
// Returns simplified implementation of {@link module:engine/view/domconverter~DomConverter#setContentOf DomConverter.setContentOf} method.
|
|
43
|
+
// Used to render UIElement and RawElement.
|
|
44
|
+
const domConverterStub = {
|
|
45
|
+
setContentOf: ( node, html ) => {
|
|
46
|
+
node.innerHTML = html;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
42
49
|
|
|
43
50
|
/**
|
|
44
51
|
* Writes the content of the {@link module:engine/view/document~Document document} to an HTML-like string.
|
|
@@ -59,6 +66,9 @@ const allowedTypes = {
|
|
|
59
66
|
* {@link module:engine/view/uielement~UIElement} will be printed.
|
|
60
67
|
* @param {Boolean} [options.renderRawElements=false] When set to `true`, the inner content of each
|
|
61
68
|
* {@link module:engine/view/rawelement~RawElement} will be printed.
|
|
69
|
+
* @param {Object} [options.domConverter=null] When set to an actual {@link module:engine/view/domconverter~DomConverter DomConverter}
|
|
70
|
+
* instance it lets the conversion go through exactly the same flow the editing view is going, i.e. with view data
|
|
71
|
+
* filtering. Otherwise the simple stub is used.
|
|
62
72
|
* @returns {String} The stringified data.
|
|
63
73
|
*/
|
|
64
74
|
export function getData( view, options = {} ) {
|
|
@@ -75,7 +85,8 @@ export function getData( view, options = {} ) {
|
|
|
75
85
|
showPriority: options.showPriority,
|
|
76
86
|
renderUIElements: options.renderUIElements,
|
|
77
87
|
renderRawElements: options.renderRawElements,
|
|
78
|
-
ignoreRoot: true
|
|
88
|
+
ignoreRoot: true,
|
|
89
|
+
domConverter: options.domConverter
|
|
79
90
|
};
|
|
80
91
|
|
|
81
92
|
return withoutSelection ?
|
|
@@ -241,6 +252,9 @@ setData._parse = parse;
|
|
|
241
252
|
* {@link module:engine/view/uielement~UIElement} will be printed.
|
|
242
253
|
* @param {Boolean} [options.renderRawElements=false] When set to `true`, the inner content of each
|
|
243
254
|
* {@link module:engine/view/rawelement~RawElement} will be printed.
|
|
255
|
+
* @param {Object} [options.domConverter={}] When set to an actual {@link module:engine/view/domconverter~DomConverter DomConverter}
|
|
256
|
+
* instance it lets the conversion go through exactly the same flow the editing view is going, i.e. with view data
|
|
257
|
+
* filtering. Otherwise the simple stub is used.
|
|
244
258
|
* @returns {String} An HTML-like string representing the view.
|
|
245
259
|
*/
|
|
246
260
|
export function stringify( node, selectionOrPositionOrRange = null, options = {} ) {
|
|
@@ -630,6 +644,9 @@ class ViewStringify {
|
|
|
630
644
|
* @param {Boolean} [options.renderUIElements=false] When set to `true`, the inner content of each
|
|
631
645
|
* {@link module:engine/view/uielement~UIElement} will be printed.
|
|
632
646
|
* @param {Boolean} [options.renderRawElements=false] When set to `true`, the inner content of each
|
|
647
|
+
* @param {Object} [options.domConverter={}] When set to an actual {@link module:engine/view/domconverter~DomConverter DomConverter}
|
|
648
|
+
* instance it lets the conversion go through exactly the same flow the editing view is going, i.e. with view data
|
|
649
|
+
* filtering. Otherwise the simple stub is used.
|
|
633
650
|
* {@link module:engine/view/rawelement~RawElement} will be printed.
|
|
634
651
|
*/
|
|
635
652
|
constructor( root, selection, options ) {
|
|
@@ -648,6 +665,7 @@ class ViewStringify {
|
|
|
648
665
|
this.sameSelectionCharacters = !!options.sameSelectionCharacters;
|
|
649
666
|
this.renderUIElements = !!options.renderUIElements;
|
|
650
667
|
this.renderRawElements = !!options.renderRawElements;
|
|
668
|
+
this.domConverter = options.domConverter || domConverterStub;
|
|
651
669
|
}
|
|
652
670
|
|
|
653
671
|
/**
|
|
@@ -681,12 +699,12 @@ class ViewStringify {
|
|
|
681
699
|
}
|
|
682
700
|
|
|
683
701
|
if ( ( this.renderUIElements && root.is( 'uiElement' ) ) ) {
|
|
684
|
-
callback( root.render( document ).innerHTML );
|
|
702
|
+
callback( root.render( document, this.domConverter ).innerHTML );
|
|
685
703
|
} else if ( this.renderRawElements && root.is( 'rawElement' ) ) {
|
|
686
704
|
// There's no DOM element for "root" to pass to render(). Creating
|
|
687
705
|
// a surrogate container to render the children instead.
|
|
688
706
|
const rawContentContainer = document.createElement( 'div' );
|
|
689
|
-
root.render( rawContentContainer );
|
|
707
|
+
root.render( rawContentContainer, this.domConverter );
|
|
690
708
|
|
|
691
709
|
callback( rawContentContainer.innerHTML );
|
|
692
710
|
} else {
|
|
@@ -128,9 +128,17 @@ function tryFixingCollapsedRange( range, schema ) {
|
|
|
128
128
|
|
|
129
129
|
const nearestSelectionRange = schema.getNearestSelectionRange( originalPosition );
|
|
130
130
|
|
|
131
|
-
// This might be null ie when editor data is empty
|
|
132
|
-
//
|
|
131
|
+
// This might be null ie when editor data is empty or the selection is inside limit element
|
|
132
|
+
// that doesn't allow text inside.
|
|
133
|
+
// In the first case there is no need to fix the selection range.
|
|
134
|
+
// In the second let's go up to the outer selectable element
|
|
133
135
|
if ( !nearestSelectionRange ) {
|
|
136
|
+
const ancestorObject = originalPosition.getAncestors().reverse().find( item => schema.isObject( item ) );
|
|
137
|
+
|
|
138
|
+
if ( ancestorObject ) {
|
|
139
|
+
return Range._createOn( ancestorObject );
|
|
140
|
+
}
|
|
141
|
+
|
|
134
142
|
return null;
|
|
135
143
|
}
|
|
136
144
|
|
package/src/view/document.js
CHANGED
|
@@ -80,6 +80,18 @@ export default class Document {
|
|
|
80
80
|
*/
|
|
81
81
|
this.set( 'isFocused', false );
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* `true` while the user is making a selection in the document (e.g. holding the mouse button and moving the cursor).
|
|
85
|
+
* When they stop selecting, the property goes back to `false`.
|
|
86
|
+
*
|
|
87
|
+
* This property is updated by the {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
|
|
88
|
+
*
|
|
89
|
+
* @readonly
|
|
90
|
+
* @observable
|
|
91
|
+
* @member {Boolean} module:engine/view/document~Document#isSelecting
|
|
92
|
+
*/
|
|
93
|
+
this.set( 'isSelecting', false );
|
|
94
|
+
|
|
83
95
|
/**
|
|
84
96
|
* True if composition is in progress inside the document.
|
|
85
97
|
*
|
package/src/view/domconverter.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module engine/view/domconverter
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
/* globals document, Node, Text */
|
|
10
|
+
/* globals document, Node, NodeFilter, DOMParser, Text */
|
|
11
11
|
|
|
12
12
|
import ViewText from './text';
|
|
13
13
|
import ViewElement from './element';
|
|
@@ -51,7 +51,12 @@ export default class DomConverter {
|
|
|
51
51
|
*
|
|
52
52
|
* @param {module:engine/view/document~Document} document The view document instance.
|
|
53
53
|
* @param {Object} options An object with configuration options.
|
|
54
|
-
* @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode
|
|
54
|
+
* @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode] The type of the block filler to use.
|
|
55
|
+
* Default value depends on the options.renderingMode:
|
|
56
|
+
* 'nbsp' when options.renderingMode == 'data',
|
|
57
|
+
* 'br' when options.renderingMode == 'editing'.
|
|
58
|
+
* @param {'data'|'editing'} [options.renderingMode='editing'] Whether to leave the View-to-DOM conversion result unchanged
|
|
59
|
+
* or improve editing experience by filtering out interactive data.
|
|
55
60
|
*/
|
|
56
61
|
constructor( document, options = {} ) {
|
|
57
62
|
/**
|
|
@@ -60,12 +65,27 @@ export default class DomConverter {
|
|
|
60
65
|
*/
|
|
61
66
|
this.document = document;
|
|
62
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Whether to leave the View-to-DOM conversion result unchanged or improve editing experience by filtering out interactive data.
|
|
70
|
+
*
|
|
71
|
+
* @member {'data'|'editing'} module:engine/view/domconverter~DomConverter#renderingMode
|
|
72
|
+
*/
|
|
73
|
+
this.renderingMode = options.renderingMode || 'editing';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Main switch for new rendering approach in the editing view.
|
|
77
|
+
*
|
|
78
|
+
* @protected
|
|
79
|
+
* @member {Boolean}
|
|
80
|
+
*/
|
|
81
|
+
this.experimentalRenderingMode = false;
|
|
82
|
+
|
|
63
83
|
/**
|
|
64
84
|
* The mode of a block filler used by the DOM converter.
|
|
65
85
|
*
|
|
66
86
|
* @member {'br'|'nbsp'|'markedNbsp'} module:engine/view/domconverter~DomConverter#blockFillerMode
|
|
67
87
|
*/
|
|
68
|
-
this.blockFillerMode = options.blockFillerMode || 'br';
|
|
88
|
+
this.blockFillerMode = options.blockFillerMode || ( this.renderingMode === 'editing' ? 'br' : 'nbsp' );
|
|
69
89
|
|
|
70
90
|
/**
|
|
71
91
|
* Elements which are considered pre-formatted elements.
|
|
@@ -221,6 +241,82 @@ export default class DomConverter {
|
|
|
221
241
|
this._viewToDomMapping.set( viewFragment, domFragment );
|
|
222
242
|
}
|
|
223
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Decides whether given pair of attribute key and value should be passed further down the pipeline.
|
|
246
|
+
*
|
|
247
|
+
* @param {String} attributeKey
|
|
248
|
+
* @param {String} attributeValue
|
|
249
|
+
* @returns {Boolean}
|
|
250
|
+
*/
|
|
251
|
+
shouldRenderAttribute( attributeKey, attributeValue ) {
|
|
252
|
+
if ( !this.experimentalRenderingMode || this.renderingMode === 'data' ) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return !( attributeKey.toLowerCase().startsWith( 'on' ) ||
|
|
257
|
+
attributeValue.match( /(\b)(on\S+)(\s*)=|javascript:|(<\s*)(\/*)script/i ) ||
|
|
258
|
+
attributeValue.match( /data:(?!image\/(png|jpeg|gif|webp))/i )
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Set `domElement`'s content using provided `html` argument. Apply necessary filtering for the editing pipeline.
|
|
264
|
+
*
|
|
265
|
+
* @param {Element} domElement DOM element that should have `html` set as its content.
|
|
266
|
+
* @param {String} html Textual representation of the HTML that will be set on `domElement`.
|
|
267
|
+
*/
|
|
268
|
+
setContentOf( domElement, html ) {
|
|
269
|
+
// For data pipeline we pass the HTML as-is.
|
|
270
|
+
if ( !this.experimentalRenderingMode || this.renderingMode === 'data' ) {
|
|
271
|
+
domElement.innerHTML = html;
|
|
272
|
+
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const document = new DOMParser().parseFromString( html, 'text/html' );
|
|
277
|
+
const fragment = document.createDocumentFragment();
|
|
278
|
+
const bodyChildNodes = document.body.childNodes;
|
|
279
|
+
|
|
280
|
+
while ( bodyChildNodes.length > 0 ) {
|
|
281
|
+
fragment.appendChild( bodyChildNodes[ 0 ] );
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const treeWalker = document.createTreeWalker( fragment, NodeFilter.SHOW_ELEMENT );
|
|
285
|
+
const nodes = [];
|
|
286
|
+
|
|
287
|
+
let currentNode;
|
|
288
|
+
|
|
289
|
+
// eslint-disable-next-line no-cond-assign
|
|
290
|
+
while ( currentNode = treeWalker.nextNode() ) {
|
|
291
|
+
nodes.push( currentNode );
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for ( const currentNode of nodes ) {
|
|
295
|
+
// Go through nodes to remove those that are prohibited in editing pipeline.
|
|
296
|
+
for ( const attributeName of currentNode.getAttributeNames() ) {
|
|
297
|
+
const attributeValue = currentNode.getAttribute( attributeName );
|
|
298
|
+
|
|
299
|
+
if ( !this.shouldRenderAttribute( attributeName, attributeValue ) ) {
|
|
300
|
+
currentNode.removeAttribute( attributeName );
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const elementName = currentNode.tagName.toLowerCase();
|
|
305
|
+
|
|
306
|
+
// There are certain nodes, that should be renamed to <span> in editing pipeline.
|
|
307
|
+
if ( this._shouldRenameElement( elementName ) ) {
|
|
308
|
+
currentNode.replaceWith( this._createReplacementDomElement( elementName, currentNode ) );
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Empty the target element.
|
|
313
|
+
while ( domElement.firstChild ) {
|
|
314
|
+
domElement.firstChild.remove();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
domElement.append( fragment );
|
|
318
|
+
}
|
|
319
|
+
|
|
224
320
|
/**
|
|
225
321
|
* Converts the view to the DOM. For all text nodes, not bound elements and document fragments new items will
|
|
226
322
|
* be created. For bound elements and document fragments the method will return corresponding items.
|
|
@@ -257,7 +353,7 @@ export default class DomConverter {
|
|
|
257
353
|
domElement = domDocument.createComment( viewNode.getCustomProperty( '$rawContent' ) );
|
|
258
354
|
} else {
|
|
259
355
|
// UIElement has its own render() method (see #799).
|
|
260
|
-
domElement = viewNode.render( domDocument );
|
|
356
|
+
domElement = viewNode.render( domDocument, this );
|
|
261
357
|
}
|
|
262
358
|
|
|
263
359
|
if ( options.bind ) {
|
|
@@ -267,7 +363,9 @@ export default class DomConverter {
|
|
|
267
363
|
return domElement;
|
|
268
364
|
} else {
|
|
269
365
|
// Create DOM element.
|
|
270
|
-
if (
|
|
366
|
+
if ( this._shouldRenameElement( viewNode.name ) ) {
|
|
367
|
+
domElement = this._createReplacementDomElement( viewNode.name );
|
|
368
|
+
} else if ( viewNode.hasAttribute( 'xmlns' ) ) {
|
|
271
369
|
domElement = domDocument.createElementNS( viewNode.getAttribute( 'xmlns' ), viewNode.name );
|
|
272
370
|
} else {
|
|
273
371
|
domElement = domDocument.createElement( viewNode.name );
|
|
@@ -276,7 +374,7 @@ export default class DomConverter {
|
|
|
276
374
|
// RawElement take care of their children in RawElement#render() method which can be customized
|
|
277
375
|
// (see https://github.com/ckeditor/ckeditor5/issues/4469).
|
|
278
376
|
if ( viewNode.is( 'rawElement' ) ) {
|
|
279
|
-
viewNode.render( domElement );
|
|
377
|
+
viewNode.render( domElement, this );
|
|
280
378
|
}
|
|
281
379
|
|
|
282
380
|
if ( options.bind ) {
|
|
@@ -285,7 +383,13 @@ export default class DomConverter {
|
|
|
285
383
|
|
|
286
384
|
// Copy element's attributes.
|
|
287
385
|
for ( const key of viewNode.getAttributeKeys() ) {
|
|
288
|
-
|
|
386
|
+
const value = viewNode.getAttribute( key );
|
|
387
|
+
|
|
388
|
+
if ( !this.shouldRenderAttribute( key, value ) ) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
domElement.setAttribute( key, value );
|
|
289
393
|
}
|
|
290
394
|
}
|
|
291
395
|
|
|
@@ -603,10 +707,10 @@ export default class DomConverter {
|
|
|
603
707
|
* If structures are too different and it is not possible to find corresponding position then `null` will be returned.
|
|
604
708
|
*
|
|
605
709
|
* @param {Node} domParent DOM position parent.
|
|
606
|
-
* @param {Number} domOffset DOM position offset.
|
|
710
|
+
* @param {Number} [domOffset=0] DOM position offset. You can skip it when converting the inline filler node.
|
|
607
711
|
* @returns {module:engine/view/position~Position} viewPosition View position.
|
|
608
712
|
*/
|
|
609
|
-
domPositionToView( domParent, domOffset ) {
|
|
713
|
+
domPositionToView( domParent, domOffset = 0 ) {
|
|
610
714
|
if ( this.isBlockFiller( domParent ) ) {
|
|
611
715
|
return this.domPositionToView( domParent.parentNode, indexOf( domParent ) );
|
|
612
716
|
}
|
|
@@ -1373,6 +1477,45 @@ export default class DomConverter {
|
|
|
1373
1477
|
_isViewElementWithRawContent( viewElement, options ) {
|
|
1374
1478
|
return options.withChildren !== false && this._rawContentElementMatcher.match( viewElement );
|
|
1375
1479
|
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Checks whether given element name should be renamed in a current rendering mode.
|
|
1483
|
+
*
|
|
1484
|
+
* @private
|
|
1485
|
+
* @param {String} elementName The name of view element.
|
|
1486
|
+
* @returns {Boolean}
|
|
1487
|
+
*/
|
|
1488
|
+
_shouldRenameElement( elementName ) {
|
|
1489
|
+
return this.experimentalRenderingMode && this.renderingMode == 'editing' && elementName == 'script';
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Return a <span> element with special attribute holding the name of the original element.
|
|
1494
|
+
* Optionally, copy all the attributes of the original element if that element is provided.
|
|
1495
|
+
*
|
|
1496
|
+
* @private
|
|
1497
|
+
* @param {String} elementName The name of view element.
|
|
1498
|
+
* @param {Element} [originalDomElement] The original DOM element to copy attributes and content from.
|
|
1499
|
+
* @returns {Element}
|
|
1500
|
+
*/
|
|
1501
|
+
_createReplacementDomElement( elementName, originalDomElement = null ) {
|
|
1502
|
+
const newDomElement = document.createElement( 'span' );
|
|
1503
|
+
|
|
1504
|
+
// Mark the span replacing a script as hidden.
|
|
1505
|
+
newDomElement.setAttribute( 'data-ck-hidden', elementName );
|
|
1506
|
+
|
|
1507
|
+
if ( originalDomElement ) {
|
|
1508
|
+
while ( originalDomElement.firstChild ) {
|
|
1509
|
+
newDomElement.appendChild( originalDomElement.firstChild );
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
for ( const attributeName of originalDomElement.getAttributeNames() ) {
|
|
1513
|
+
newDomElement.setAttribute( attributeName, originalDomElement.getAttribute( attributeName ) );
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
return newDomElement;
|
|
1518
|
+
}
|
|
1376
1519
|
}
|
|
1377
1520
|
|
|
1378
1521
|
// Helper function.
|
|
@@ -20,6 +20,8 @@ import { debounce } from 'lodash-es';
|
|
|
20
20
|
* {@link module:engine/view/document~Document#event:selectionChange} event only if a selection change was the only change in the document
|
|
21
21
|
* and the DOM selection is different then the view selection.
|
|
22
22
|
*
|
|
23
|
+
* This observer also manages the {@link module:engine/view/document~Document#isSelecting} property of the view document.
|
|
24
|
+
*
|
|
23
25
|
* Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
|
|
24
26
|
*
|
|
25
27
|
* @see module:engine/view/observer/mutationobserver~MutationObserver
|
|
@@ -78,8 +80,26 @@ export default class SelectionObserver extends Observer {
|
|
|
78
80
|
*/
|
|
79
81
|
this._fireSelectionChangeDoneDebounced = debounce( data => this.document.fire( 'selectionChangeDone', data ), 200 );
|
|
80
82
|
|
|
83
|
+
/**
|
|
84
|
+
* When called, starts clearing the {@link #_loopbackCounter} counter in intervals of time. When the number of selection
|
|
85
|
+
* changes exceeds a certain limit within the interval of time, the observer will not fire `selectionChange` but warn about
|
|
86
|
+
* possible infinite selection loop.
|
|
87
|
+
*
|
|
88
|
+
* @private
|
|
89
|
+
* @member {Number} #_clearInfiniteLoopInterval
|
|
90
|
+
*/
|
|
81
91
|
this._clearInfiniteLoopInterval = setInterval( () => this._clearInfiniteLoop(), 1000 );
|
|
82
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Unlocks the `isSelecting` state of the view document in case the selection observer did not record this fact
|
|
95
|
+
* correctly (for whatever the reason). It is a safeguard (paranoid check) that returns document to the normal state
|
|
96
|
+
* after a certain period of time (debounced, postponed by each selectionchange event).
|
|
97
|
+
*
|
|
98
|
+
* @private
|
|
99
|
+
* @method #_documentIsSelectingInactivityTimeoutDebounced
|
|
100
|
+
*/
|
|
101
|
+
this._documentIsSelectingInactivityTimeoutDebounced = debounce( () => ( this.document.isSelecting = false ), 5000 );
|
|
102
|
+
|
|
83
103
|
/**
|
|
84
104
|
* Private property to check if the code does not enter infinite loop.
|
|
85
105
|
*
|
|
@@ -95,13 +115,39 @@ export default class SelectionObserver extends Observer {
|
|
|
95
115
|
observe( domElement ) {
|
|
96
116
|
const domDocument = domElement.ownerDocument;
|
|
97
117
|
|
|
98
|
-
|
|
118
|
+
const startDocumentIsSelecting = () => {
|
|
119
|
+
this.document.isSelecting = true;
|
|
120
|
+
|
|
121
|
+
// Let's activate the safety timeout each time the document enters the "is selecting" state.
|
|
122
|
+
this._documentIsSelectingInactivityTimeoutDebounced();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const endDocumentIsSelecting = () => {
|
|
126
|
+
this.document.isSelecting = false;
|
|
127
|
+
|
|
128
|
+
// The safety timeout can be canceled when the document leaves the "is selecting" state.
|
|
129
|
+
this._documentIsSelectingInactivityTimeoutDebounced.cancel();
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// The document has the "is selecting" state while the user keeps making (extending) the selection
|
|
133
|
+
// (e.g. by holding the mouse button and moving the cursor). The state resets when they either released
|
|
134
|
+
// the mouse button or interrupted the process by pressing or releasing any key.
|
|
135
|
+
this.listenTo( domElement, 'selectstart', startDocumentIsSelecting, { priority: 'highest' } );
|
|
136
|
+
this.listenTo( domElement, 'keydown', endDocumentIsSelecting, { priority: 'highest' } );
|
|
137
|
+
this.listenTo( domElement, 'keyup', endDocumentIsSelecting, { priority: 'highest' } );
|
|
138
|
+
|
|
139
|
+
// Add document-wide listeners only once. This method could be called for multiple editing roots.
|
|
99
140
|
if ( this._documents.has( domDocument ) ) {
|
|
100
141
|
return;
|
|
101
142
|
}
|
|
102
143
|
|
|
144
|
+
this.listenTo( domDocument, 'mouseup', endDocumentIsSelecting, { priority: 'highest' } );
|
|
103
145
|
this.listenTo( domDocument, 'selectionchange', ( evt, domEvent ) => {
|
|
104
146
|
this._handleSelectionChange( domEvent, domDocument );
|
|
147
|
+
|
|
148
|
+
// Defer the safety timeout when the selection changes (e.g. the user keeps extending the selection
|
|
149
|
+
// using their mouse).
|
|
150
|
+
this._documentIsSelectingInactivityTimeoutDebounced();
|
|
105
151
|
} );
|
|
106
152
|
|
|
107
153
|
this._documents.add( domDocument );
|
|
@@ -115,6 +161,7 @@ export default class SelectionObserver extends Observer {
|
|
|
115
161
|
|
|
116
162
|
clearInterval( this._clearInfiniteLoopInterval );
|
|
117
163
|
this._fireSelectionChangeDoneDebounced.cancel();
|
|
164
|
+
this._documentIsSelectingInactivityTimeoutDebounced.cancel();
|
|
118
165
|
}
|
|
119
166
|
|
|
120
167
|
/**
|
package/src/view/rawelement.js
CHANGED
|
@@ -131,12 +131,13 @@ export default class RawElement extends Element {
|
|
|
131
131
|
*
|
|
132
132
|
* const myRawElement = downcastWriter.createRawElement( 'div' );
|
|
133
133
|
*
|
|
134
|
-
* myRawElement.render = function( domElement ) {
|
|
135
|
-
*
|
|
134
|
+
* myRawElement.render = function( domElement, domConverter ) {
|
|
135
|
+
* domConverter.setContentOf( domElement, '<b>This is the raw content of myRawElement.</b>' );
|
|
136
136
|
* };
|
|
137
137
|
*
|
|
138
138
|
* @method #render
|
|
139
139
|
* @param {HTMLElement} domElement The native DOM element representing the raw view element.
|
|
140
|
+
* @param {module:engine/view/domconverter~DomConverter} domConverter Instance of the DomConverter used to optimize the output.
|
|
140
141
|
*/
|
|
141
142
|
}
|
|
142
143
|
|
package/src/view/renderer.js
CHANGED
|
@@ -24,6 +24,8 @@ import isNode from '@ckeditor/ckeditor5-utils/src/dom/isnode';
|
|
|
24
24
|
import fastDiff from '@ckeditor/ckeditor5-utils/src/fastdiff';
|
|
25
25
|
import env from '@ckeditor/ckeditor5-utils/src/env';
|
|
26
26
|
|
|
27
|
+
import '../../theme/renderer.css';
|
|
28
|
+
|
|
27
29
|
/**
|
|
28
30
|
* Renderer is responsible for updating the DOM structure and the DOM selection based on
|
|
29
31
|
* the {@link module:engine/view/renderer~Renderer#markToSync information about updated view nodes}.
|
|
@@ -98,8 +100,34 @@ export default class Renderer {
|
|
|
98
100
|
* this is set to `false`.
|
|
99
101
|
*
|
|
100
102
|
* @member {Boolean}
|
|
103
|
+
* @observable
|
|
104
|
+
*/
|
|
105
|
+
this.set( 'isFocused', false );
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Indicates whether the user is making a selection in the document (e.g. holding the mouse button and moving the cursor).
|
|
109
|
+
* When they stop selecting, the property goes back to `false`.
|
|
110
|
+
*
|
|
111
|
+
* Note: In some browsers, the renderer will stop rendering the selection and inline fillers while the user is making
|
|
112
|
+
* a selection to avoid glitches in DOM selection
|
|
113
|
+
* (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
|
|
114
|
+
*
|
|
115
|
+
* @member {Boolean}
|
|
116
|
+
* @observable
|
|
101
117
|
*/
|
|
102
|
-
this.
|
|
118
|
+
this.set( 'isSelecting', false );
|
|
119
|
+
|
|
120
|
+
// Rendering the selection and inline filler manipulation should be postponed in (non-Android) Blink until the user finishes
|
|
121
|
+
// creating the selection in DOM to avoid accidental selection collapsing
|
|
122
|
+
// (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
|
|
123
|
+
// When the user stops, selecting, all pending changes should be rendered ASAP, though.
|
|
124
|
+
if ( env.isBlink && !env.isAndroid ) {
|
|
125
|
+
this.on( 'change:isSelecting', () => {
|
|
126
|
+
if ( !this.isSelecting ) {
|
|
127
|
+
this.render();
|
|
128
|
+
}
|
|
129
|
+
} );
|
|
130
|
+
}
|
|
103
131
|
|
|
104
132
|
/**
|
|
105
133
|
* The text node in which the inline filler was rendered.
|
|
@@ -170,29 +198,41 @@ export default class Renderer {
|
|
|
170
198
|
*/
|
|
171
199
|
render() {
|
|
172
200
|
let inlineFillerPosition;
|
|
201
|
+
const isInlineFillerRenderingPossible = env.isBlink && !env.isAndroid ? !this.isSelecting : true;
|
|
173
202
|
|
|
174
203
|
// Refresh mappings.
|
|
175
204
|
for ( const element of this.markedChildren ) {
|
|
176
205
|
this._updateChildrenMappings( element );
|
|
177
206
|
}
|
|
178
207
|
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
// (
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
208
|
+
// Don't manipulate inline fillers while the selection is being made in (non-Android) Blink to prevent accidental
|
|
209
|
+
// DOM selection collapsing
|
|
210
|
+
// (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
|
|
211
|
+
if ( isInlineFillerRenderingPossible ) {
|
|
212
|
+
// There was inline filler rendered in the DOM but it's not
|
|
213
|
+
// at the selection position any more, so we can remove it
|
|
214
|
+
// (cause even if it's needed, it must be placed in another location).
|
|
215
|
+
if ( this._inlineFiller && !this._isSelectionInInlineFiller() ) {
|
|
216
|
+
this._removeInlineFiller();
|
|
217
|
+
}
|
|
185
218
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
219
|
+
// If we've got the filler, let's try to guess its position in the view.
|
|
220
|
+
if ( this._inlineFiller ) {
|
|
221
|
+
inlineFillerPosition = this._getInlineFillerPosition();
|
|
222
|
+
}
|
|
223
|
+
// Otherwise, if it's needed, create it at the selection position.
|
|
224
|
+
else if ( this._needsInlineFillerAtSelection() ) {
|
|
225
|
+
inlineFillerPosition = this.selection.getFirstPosition();
|
|
193
226
|
|
|
194
|
-
|
|
195
|
-
|
|
227
|
+
// Do not use `markToSync` so it will be added even if the parent is already added.
|
|
228
|
+
this.markedChildren.add( inlineFillerPosition.parent );
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Paranoid check: we make sure the inline filler has any parent so it can be mapped to view position
|
|
232
|
+
// by DomConverter.
|
|
233
|
+
else if ( this._inlineFiller && this._inlineFiller.parentNode ) {
|
|
234
|
+
// While the user is making selection, preserve the inline filler at its original position.
|
|
235
|
+
inlineFillerPosition = this.domConverter.domPositionToView( this._inlineFiller );
|
|
196
236
|
}
|
|
197
237
|
|
|
198
238
|
for ( const element of this.markedAttributes ) {
|
|
@@ -209,26 +249,30 @@ export default class Renderer {
|
|
|
209
249
|
}
|
|
210
250
|
}
|
|
211
251
|
|
|
212
|
-
// Check whether the inline filler is required and where it really is in the DOM.
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
252
|
+
// * Check whether the inline filler is required and where it really is in the DOM.
|
|
253
|
+
// At this point in most cases it will be in the DOM, but there are exceptions.
|
|
254
|
+
// For example, if the inline filler was deep in the created DOM structure, it will not be created.
|
|
255
|
+
// Similarly, if it was removed at the beginning of this function and then neither text nor children were updated,
|
|
256
|
+
// it will not be present. Fix those and similar scenarios.
|
|
257
|
+
// * Don't manipulate inline fillers while the selection is being made in (non-Android) Blink to prevent accidental
|
|
258
|
+
// DOM selection collapsing
|
|
259
|
+
// (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
|
|
260
|
+
if ( isInlineFillerRenderingPossible ) {
|
|
261
|
+
if ( inlineFillerPosition ) {
|
|
262
|
+
const fillerDomPosition = this.domConverter.viewPositionToDom( inlineFillerPosition );
|
|
263
|
+
const domDocument = fillerDomPosition.parent.ownerDocument;
|
|
264
|
+
|
|
265
|
+
if ( !startsWithFiller( fillerDomPosition.parent ) ) {
|
|
266
|
+
// Filler has not been created at filler position. Create it now.
|
|
267
|
+
this._inlineFiller = addInlineFiller( domDocument, fillerDomPosition.parent, fillerDomPosition.offset );
|
|
268
|
+
} else {
|
|
269
|
+
// Filler has been found, save it.
|
|
270
|
+
this._inlineFiller = fillerDomPosition.parent;
|
|
271
|
+
}
|
|
225
272
|
} else {
|
|
226
|
-
//
|
|
227
|
-
this._inlineFiller =
|
|
273
|
+
// There is no filler needed.
|
|
274
|
+
this._inlineFiller = null;
|
|
228
275
|
}
|
|
229
|
-
} else {
|
|
230
|
-
// There is no filler needed.
|
|
231
|
-
this._inlineFiller = null;
|
|
232
276
|
}
|
|
233
277
|
|
|
234
278
|
// First focus the new editing host, then update the selection.
|
|
@@ -401,7 +445,7 @@ export default class Renderer {
|
|
|
401
445
|
}
|
|
402
446
|
|
|
403
447
|
if ( isInlineFiller( domFillerNode ) ) {
|
|
404
|
-
domFillerNode.
|
|
448
|
+
domFillerNode.remove();
|
|
405
449
|
} else {
|
|
406
450
|
domFillerNode.data = domFillerNode.data.substr( INLINE_FILLER_LENGTH );
|
|
407
451
|
}
|
|
@@ -511,11 +555,23 @@ export default class Renderer {
|
|
|
511
555
|
|
|
512
556
|
// Add or overwrite attributes.
|
|
513
557
|
for ( const key of viewAttrKeys ) {
|
|
514
|
-
|
|
558
|
+
const value = viewElement.getAttribute( key );
|
|
559
|
+
|
|
560
|
+
if ( !this.domConverter.shouldRenderAttribute( key, value ) ) {
|
|
561
|
+
domElement.removeAttribute( key );
|
|
562
|
+
} else {
|
|
563
|
+
domElement.setAttribute( key, value );
|
|
564
|
+
}
|
|
515
565
|
}
|
|
516
566
|
|
|
517
567
|
// Remove from DOM attributes which do not exists in the view.
|
|
518
568
|
for ( const key of domAttrKeys ) {
|
|
569
|
+
// Do not remove attributes on `script` elements with special data attributes `data-ck-hidden`.
|
|
570
|
+
if ( viewElement.name === 'script' && key === 'data-ck-hidden' ) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// All other attributes not present in the DOM should be removed.
|
|
519
575
|
if ( !viewElement.hasAttribute( key ) ) {
|
|
520
576
|
domElement.removeAttribute( key );
|
|
521
577
|
}
|
|
@@ -543,7 +599,7 @@ export default class Renderer {
|
|
|
543
599
|
const inlineFillerPosition = options.inlineFillerPosition;
|
|
544
600
|
const actualDomChildren = this.domConverter.mapViewToDom( viewElement ).childNodes;
|
|
545
601
|
const expectedDomChildren = Array.from(
|
|
546
|
-
this.domConverter.viewChildrenToDom( viewElement, domElement.ownerDocument, { bind: true
|
|
602
|
+
this.domConverter.viewChildrenToDom( viewElement, domElement.ownerDocument, { bind: true } )
|
|
547
603
|
);
|
|
548
604
|
|
|
549
605
|
// Inline filler element has to be created as it is present in the DOM, but not in the view. It is required
|
|
@@ -684,6 +740,14 @@ export default class Renderer {
|
|
|
684
740
|
* @private
|
|
685
741
|
*/
|
|
686
742
|
_updateSelection() {
|
|
743
|
+
// Block updating DOM selection in (non-Android) Blink while the user is selecting to prevent accidental selection collapsing.
|
|
744
|
+
// Note: Structural changes in DOM must trigger selection rendering, though. Nodes the selection was anchored
|
|
745
|
+
// to may disappear in DOM which would break the selection (e.g. in real-time collaboration scenarios).
|
|
746
|
+
// https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723
|
|
747
|
+
if ( env.isBlink && !env.isAndroid && this.isSelecting && !this.markedChildren.size ) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
687
751
|
// If there is no selection - remove DOM and fake selections.
|
|
688
752
|
if ( this.selection.rangeCount === 0 ) {
|
|
689
753
|
this._removeDomSelection();
|
package/src/view/uielement.js
CHANGED
|
@@ -126,9 +126,10 @@ export default class UIElement extends Element {
|
|
|
126
126
|
* Do not use inheritance to create custom rendering method, replace `render()` method instead:
|
|
127
127
|
*
|
|
128
128
|
* const myUIElement = downcastWriter.createUIElement( 'span' );
|
|
129
|
-
* myUIElement.render = function( domDocument ) {
|
|
129
|
+
* myUIElement.render = function( domDocument, domConverter ) {
|
|
130
130
|
* const domElement = this.toDomElement( domDocument );
|
|
131
|
-
*
|
|
131
|
+
*
|
|
132
|
+
* domConverter.setContentOf( domElement, '<b>this is ui element</b>' );
|
|
132
133
|
*
|
|
133
134
|
* return domElement;
|
|
134
135
|
* };
|
|
@@ -138,9 +139,11 @@ export default class UIElement extends Element {
|
|
|
138
139
|
* after rendering your UI element.
|
|
139
140
|
*
|
|
140
141
|
* @param {Document} domDocument
|
|
142
|
+
* @param {module:engine/view/domconverter~DomConverter} domConverter Instance of the DomConverter used to optimize the output.
|
|
141
143
|
* @returns {HTMLElement}
|
|
142
144
|
*/
|
|
143
145
|
render( domDocument ) {
|
|
146
|
+
// Provide basic, default output.
|
|
144
147
|
return this.toDomElement( domDocument );
|
|
145
148
|
}
|
|
146
149
|
|
package/src/view/view.js
CHANGED
|
@@ -116,7 +116,7 @@ export default class View {
|
|
|
116
116
|
* @type {module:engine/view/renderer~Renderer}
|
|
117
117
|
*/
|
|
118
118
|
this._renderer = new Renderer( this.domConverter, this.document.selection );
|
|
119
|
-
this._renderer.bind( 'isFocused' ).to( this.document );
|
|
119
|
+
this._renderer.bind( 'isFocused', 'isSelecting' ).to( this.document );
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
122
|
* A DOM root attributes cache. It saves the initial values of DOM root attributes before the DOM element
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* Elements marked by the Renderer as hidden should be invisible in the editor. */
|
|
7
|
+
.ck.ck-editor__editable span[data-ck-hidden] {
|
|
8
|
+
display: none;
|
|
9
|
+
}
|