@ckeditor/ckeditor5-engine 29.1.0 → 31.1.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.
@@ -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 time intervals. 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 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
- // Add listener once per each document.
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
  /**
@@ -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
- * domElement.innerHTML = '<b>This is the raw content of myRawElement.</b>';
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
 
@@ -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
101
104
  */
102
- this.isFocused = false;
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
117
+ */
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
- // There was inline filler rendered in the DOM but it's not
180
- // at the selection position any more, so we can remove it
181
- // (cause even if it's needed, it must be placed in another location).
182
- if ( this._inlineFiller && !this._isSelectionInInlineFiller() ) {
183
- this._removeInlineFiller();
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
- // If we've got the filler, let's try to guess its position in the view.
187
- if ( this._inlineFiller ) {
188
- inlineFillerPosition = this._getInlineFillerPosition();
189
- }
190
- // Otherwise, if it's needed, create it at the selection position.
191
- else if ( this._needsInlineFillerAtSelection() ) {
192
- inlineFillerPosition = this.selection.getFirstPosition();
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
- // Do not use `markToSync` so it will be added even if the parent is already added.
195
- this.markedChildren.add( inlineFillerPosition.parent );
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
- // At this point in most cases it will be in the DOM, but there are exceptions.
214
- // For example, if the inline filler was deep in the created DOM structure, it will not be created.
215
- // Similarly, if it was removed at the beginning of this function and then neither text nor children were updated,
216
- // it will not be present.
217
- // Fix those and similar scenarios.
218
- if ( inlineFillerPosition ) {
219
- const fillerDomPosition = this.domConverter.viewPositionToDom( inlineFillerPosition );
220
- const domDocument = fillerDomPosition.parent.ownerDocument;
221
-
222
- if ( !startsWithFiller( fillerDomPosition.parent ) ) {
223
- // Filler has not been created at filler position. Create it now.
224
- this._inlineFiller = addInlineFiller( domDocument, fillerDomPosition.parent, fillerDomPosition.offset );
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
- // Filler has been found, save it.
227
- this._inlineFiller = fillerDomPosition.parent;
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.parentNode.removeChild( domFillerNode );
448
+ domFillerNode.remove();
405
449
  } else {
406
450
  domFillerNode.data = domFillerNode.data.substr( INLINE_FILLER_LENGTH );
407
451
  }
@@ -511,13 +555,14 @@ export default class Renderer {
511
555
 
512
556
  // Add or overwrite attributes.
513
557
  for ( const key of viewAttrKeys ) {
514
- domElement.setAttribute( key, viewElement.getAttribute( key ) );
558
+ this.domConverter.setDomElementAttribute( domElement, key, viewElement.getAttribute( key ), viewElement );
515
559
  }
516
560
 
517
561
  // Remove from DOM attributes which do not exists in the view.
518
562
  for ( const key of domAttrKeys ) {
563
+ // All other attributes not present in the DOM should be removed.
519
564
  if ( !viewElement.hasAttribute( key ) ) {
520
- domElement.removeAttribute( key );
565
+ this.domConverter.removeDomElementAttribute( domElement, key );
521
566
  }
522
567
  }
523
568
  }
@@ -543,7 +588,7 @@ export default class Renderer {
543
588
  const inlineFillerPosition = options.inlineFillerPosition;
544
589
  const actualDomChildren = this.domConverter.mapViewToDom( viewElement ).childNodes;
545
590
  const expectedDomChildren = Array.from(
546
- this.domConverter.viewChildrenToDom( viewElement, domElement.ownerDocument, { bind: true, inlineFillerPosition } )
591
+ this.domConverter.viewChildrenToDom( viewElement, domElement.ownerDocument, { bind: true } )
547
592
  );
548
593
 
549
594
  // Inline filler element has to be created as it is present in the DOM, but not in the view. It is required
@@ -684,6 +729,14 @@ export default class Renderer {
684
729
  * @private
685
730
  */
686
731
  _updateSelection() {
732
+ // Block updating DOM selection in (non-Android) Blink while the user is selecting to prevent accidental selection collapsing.
733
+ // Note: Structural changes in DOM must trigger selection rendering, though. Nodes the selection was anchored
734
+ // to, may disappear in DOM which would break the selection (e.g. in real-time collaboration scenarios).
735
+ // https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723
736
+ if ( env.isBlink && !env.isAndroid && this.isSelecting && !this.markedChildren.size ) {
737
+ return;
738
+ }
739
+
687
740
  // If there is no selection - remove DOM and fake selections.
688
741
  if ( this.selection.rangeCount === 0 ) {
689
742
  this._removeDomSelection();
@@ -36,6 +36,11 @@ const COLOR_NAMES = new Set( [
36
36
  'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon',
37
37
  'sandybrown', 'seagreen', 'seashell', 'sienna', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow',
38
38
  'springgreen', 'steelblue', 'tan', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'whitesmoke', 'yellowgreen',
39
+ // CSS Color Module Level 3 (System Colors)
40
+ 'activeborder', 'activecaption', 'appworkspace', 'background', 'buttonface', 'buttonhighlight', 'buttonshadow',
41
+ 'buttontext', 'captiontext', 'graytext', 'highlight', 'highlighttext', 'inactiveborder', 'inactivecaption',
42
+ 'inactivecaptiontext', 'infobackground', 'infotext', 'menu', 'menutext', 'scrollbar', 'threeddarkshadow',
43
+ 'threedface', 'threedhighlight', 'threedlightshadow', 'threedshadow', 'window', 'windowframe', 'windowtext',
39
44
  // CSS Color Module Level 4
40
45
  'rebeccapurple',
41
46
  // Keywords
@@ -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
- * domElement.innerHTML = '<b>this is ui element</b>';
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-unsafe-element] {
8
+ display: none;
9
+ }