Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit 115a91b

Browse files
author
Piotr Jasiun
authored
Merge pull request #864 from ckeditor/t/795
Fix: View document is now re-rendered after focusing. Closes #795.
2 parents d00c973 + 81347b8 commit 115a91b

File tree

4 files changed

+138
-22
lines changed

4 files changed

+138
-22
lines changed

src/view/observer/focusobserver.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
* @module engine/view/observer/focusobserver
88
*/
99

10+
/* globals setTimeout, clearTimeout */
11+
1012
import DomEventObserver from './domeventobserver';
1113

1214
/**
@@ -28,6 +30,12 @@ export default class FocusObserver extends DomEventObserver {
2830

2931
document.on( 'focus', () => {
3032
document.isFocused = true;
33+
34+
// Unfortunately native `selectionchange` event is fired asynchronously.
35+
// We need to wait until `SelectionObserver` handle the event and then render. Otherwise rendering will
36+
// overwrite new DOM selection with selection from the view.
37+
// See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
38+
this._renderTimeoutId = setTimeout( () => document.render(), 0 );
3139
} );
3240

3341
document.on( 'blur', ( evt, data ) => {
@@ -40,11 +48,29 @@ export default class FocusObserver extends DomEventObserver {
4048
document.render();
4149
}
4250
} );
51+
52+
/**
53+
* Identifier of the timeout currently used by focus listener to delay rendering execution.
54+
*
55+
* @private
56+
* @member {Number} #_renderTimeoutId
57+
*/
4358
}
4459

4560
onDomEvent( domEvent ) {
4661
this.fire( domEvent.type, domEvent );
4762
}
63+
64+
/**
65+
* @inheritDoc
66+
*/
67+
destroy() {
68+
if ( this._renderTimeoutId ) {
69+
clearTimeout( this._renderTimeoutId );
70+
}
71+
72+
super.destroy();
73+
}
4874
}
4975

5076
/**

tests/manual/nestededitable.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@
33
* Put selection inside `foo bar baz` nested editable. Main editable and nested one should be focused (blue outline should be visible).
44
* Change selection inside nested editable and see if `Model contents` change accordingly.
55
* Click outside the editor. Outline from main editable and nested editable should be removed.
6+
* Check following scenario:
7+
* put selection inside nested editable: `foo bar baz{}`,
8+
* click outside the editor (outlines should be removed),
9+
* put selection at exact same place as before: `foo bar baz{}`,
10+
* both editables should be focused (blue outline should be visible).

tests/view/observer/focusobserver.js

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
* For licensing, see LICENSE.md.
44
*/
55

6-
/* globals document */
7-
6+
/* globals document, window */
87
import FocusObserver from '../../../src/view/observer/focusobserver';
98
import ViewDocument from '../../../src/view/document';
109
import ViewRange from '../../../src/view/range';
10+
import { setData } from '../../../src/dev-utils/view';
1111

1212
describe( 'FocusObserver', () => {
1313
let viewDocument, observer;
@@ -115,5 +115,84 @@ describe( 'FocusObserver', () => {
115115

116116
expect( viewDocument.isFocused ).to.be.true;
117117
} );
118+
119+
it( 'should delay rendering to the next iteration of event loop', () => {
120+
const renderSpy = sinon.spy( viewDocument, 'render' );
121+
const clock = sinon.useFakeTimers();
122+
123+
observer.onDomEvent( { type: 'focus', target: domMain } );
124+
sinon.assert.notCalled( renderSpy );
125+
clock.tick( 0 );
126+
sinon.assert.called( renderSpy );
127+
128+
clock.restore();
129+
} );
130+
131+
it( 'should not call render if destroyed', () => {
132+
const renderSpy = sinon.spy( viewDocument, 'render' );
133+
const clock = sinon.useFakeTimers();
134+
135+
observer.onDomEvent( { type: 'focus', target: domMain } );
136+
sinon.assert.notCalled( renderSpy );
137+
observer.destroy();
138+
clock.tick( 0 );
139+
sinon.assert.notCalled( renderSpy );
140+
141+
clock.restore();
142+
} );
143+
} );
144+
145+
describe( 'integration test', () => {
146+
let viewDocument, viewRoot, domRoot, observer, domSelection;
147+
148+
beforeEach( () => {
149+
domRoot = document.createElement( 'div' );
150+
document.body.appendChild( domRoot );
151+
viewDocument = new ViewDocument();
152+
viewRoot = viewDocument.createRoot( domRoot );
153+
observer = viewDocument.getObserver( FocusObserver );
154+
domSelection = window.getSelection();
155+
} );
156+
157+
it( 'should render document after selectionChange event', ( done ) => {
158+
const selectionChangeSpy = sinon.spy();
159+
const renderSpy = sinon.spy();
160+
161+
setData( viewDocument, '<div contenteditable="true">foo bar</div>' );
162+
viewDocument.render();
163+
const domEditable = domRoot.childNodes[ 0 ];
164+
165+
viewDocument.on( 'selectionChange', selectionChangeSpy );
166+
viewDocument.on( 'render', renderSpy, { priority: 'low' } );
167+
168+
viewDocument.on( 'render', () => {
169+
sinon.assert.callOrder( selectionChangeSpy, renderSpy );
170+
done();
171+
}, { priority: 'low' } );
172+
173+
observer.onDomEvent( { type: 'focus', target: domEditable } );
174+
domSelection.collapse( domEditable );
175+
} );
176+
177+
it( 'should render without selectionChange event', ( done ) => {
178+
const selectionChangeSpy = sinon.spy();
179+
const renderSpy = sinon.spy();
180+
181+
setData( viewDocument, '<div contenteditable="true">foo bar</div>' );
182+
viewDocument.render();
183+
const domEditable = domRoot.childNodes[ 0 ];
184+
185+
viewDocument.on( 'selectionChange', selectionChangeSpy );
186+
viewDocument.on( 'render', renderSpy, { priority: 'low' } );
187+
188+
viewDocument.on( 'render', () => {
189+
sinon.assert.notCalled( selectionChangeSpy );
190+
sinon.assert.called( renderSpy );
191+
192+
done();
193+
}, { priority: 'low' } );
194+
195+
observer.onDomEvent( { type: 'focus', target: domEditable } );
196+
} );
118197
} );
119198
} );

tests/view/observer/selectionobserver.js

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,32 @@
33
* For licensing, see LICENSE.md.
44
*/
55

6-
/* globals setTimeout, document */
6+
/* globals setTimeout, setInterval, clearInterval, document */
77

88
import ViewRange from '../../../src/view/range';
99
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
1010
import ViewSelection from '../../../src/view/selection';
1111
import ViewDocument from '../../../src/view/document';
1212
import SelectionObserver from '../../../src/view/observer/selectionobserver';
1313
import MutationObserver from '../../../src/view/observer/mutationobserver';
14-
14+
import FocusObserver from '../../../src/view/observer/focusobserver';
1515
import log from '@ckeditor/ckeditor5-utils/src/log';
16-
1716
import { parse } from '../../../src/dev-utils/view';
1817

1918
testUtils.createSinonSandbox();
2019

2120
describe( 'SelectionObserver', () => {
22-
let viewDocument, viewRoot, mutationObserver, selectionObserver, domRoot;
21+
let viewDocument, viewRoot, mutationObserver, selectionObserver, domRoot, domMain, domDocument;
2322

2423
beforeEach( ( done ) => {
25-
domRoot = document.createElement( 'div' );
26-
domRoot.innerHTML = `<div contenteditable="true" id="main"></div><div contenteditable="true" id="additional"></div>`;
27-
document.body.appendChild( domRoot );
24+
domDocument = document;
25+
domRoot = domDocument.createElement( 'div' );
26+
domRoot.innerHTML = `<div contenteditable="true"></div><div contenteditable="true" id="additional"></div>`;
27+
domMain = domRoot.childNodes[ 0 ];
28+
domDocument.body.appendChild( domRoot );
2829

2930
viewDocument = new ViewDocument();
30-
viewDocument.createRoot( document.getElementById( 'main' ) );
31+
viewDocument.createRoot( domMain );
3132

3233
mutationObserver = viewDocument.getObserver( MutationObserver );
3334
selectionObserver = viewDocument.getObserver( SelectionObserver );
@@ -41,7 +42,7 @@ describe( 'SelectionObserver', () => {
4142
viewDocument.render();
4243

4344
viewDocument.selection.removeAllRanges();
44-
document.getSelection().removeAllRanges();
45+
domDocument.getSelection().removeAllRanges();
4546

4647
viewDocument.isFocused = true;
4748

@@ -59,7 +60,7 @@ describe( 'SelectionObserver', () => {
5960

6061
it( 'should fire selectionChange when it is the only change', ( done ) => {
6162
viewDocument.on( 'selectionChange', ( evt, data ) => {
62-
expect( data ).to.have.property( 'domSelection' ).that.equals( document.getSelection() );
63+
expect( data ).to.have.property( 'domSelection' ).that.equals( domDocument.getSelection() );
6364

6465
expect( data ).to.have.property( 'oldSelection' ).that.is.instanceof( ViewSelection );
6566
expect( data.oldSelection.rangeCount ).to.equal( 0 );
@@ -83,7 +84,7 @@ describe( 'SelectionObserver', () => {
8384

8485
it( 'should add only one listener to one document', ( done ) => {
8586
// Add second roots to ensure that listener is added once.
86-
viewDocument.createRoot( document.getElementById( 'additional' ), 'additional' );
87+
viewDocument.createRoot( domDocument.getElementById( 'additional' ), 'additional' );
8788

8889
viewDocument.on( 'selectionChange', () => {
8990
done();
@@ -138,6 +139,8 @@ describe( 'SelectionObserver', () => {
138139
it( 'should warn and not enter infinite loop', ( done ) => {
139140
// Reset infinite loop counters so other tests won't mess up with this test.
140141
selectionObserver._clearInfiniteLoop();
142+
clearInterval( selectionObserver._clearInfiniteLoopInterval );
143+
selectionObserver._clearInfiniteLoopInterval = setInterval( () => selectionObserver._clearInfiniteLoop(), 2000 );
141144

142145
let counter = 100;
143146

@@ -232,6 +235,9 @@ describe( 'SelectionObserver', () => {
232235

233236
viewDocument.on( 'selectionChangeDone', spy );
234237

238+
// Disable focus observer to not re-render view on each focus.
239+
viewDocument.getObserver( FocusObserver ).disable();
240+
235241
// Change selection.
236242
changeDomSelection();
237243

@@ -250,7 +256,7 @@ describe( 'SelectionObserver', () => {
250256
const data = spy.firstCall.args[ 1 ];
251257

252258
expect( spy.calledOnce ).to.true;
253-
expect( data ).to.have.property( 'domSelection' ).to.equal( document.getSelection() );
259+
expect( data ).to.have.property( 'domSelection' ).to.equal( domDocument.getSelection() );
254260

255261
expect( data ).to.have.property( 'oldSelection' ).to.instanceof( ViewSelection );
256262
expect( data.oldSelection.rangeCount ).to.equal( 0 );
@@ -295,13 +301,13 @@ describe( 'SelectionObserver', () => {
295301
}, 110 );
296302
}, 100 );
297303
} );
298-
} );
299304

300-
function changeDomSelection() {
301-
const domSelection = document.getSelection();
302-
const domFoo = document.getElementById( 'main' ).childNodes[ 0 ].childNodes[ 0 ];
303-
const offset = domSelection.anchorOffset;
305+
function changeDomSelection() {
306+
const domSelection = domDocument.getSelection();
307+
const domFoo = domMain.childNodes[ 0 ].childNodes[ 0 ];
308+
const offset = domSelection.anchorOffset;
304309

305-
domSelection.removeAllRanges();
306-
domSelection.collapse( domFoo, offset == 2 ? 3 : 2 );
307-
}
310+
domSelection.removeAllRanges();
311+
domSelection.collapse( domFoo, offset == 2 ? 3 : 2 );
312+
}
313+
} );

0 commit comments

Comments
 (0)