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

Commit

Permalink
Merge pull request #864 from ckeditor/t/795
Browse files Browse the repository at this point in the history
Fix: View document is now re-rendered after focusing. Closes #795.
  • Loading branch information
Piotr Jasiun authored Mar 22, 2017
2 parents d00c973 + 81347b8 commit 115a91b
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 22 deletions.
26 changes: 26 additions & 0 deletions src/view/observer/focusobserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @module engine/view/observer/focusobserver
*/

/* globals setTimeout, clearTimeout */

import DomEventObserver from './domeventobserver';

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

document.on( 'focus', () => {
document.isFocused = true;

// Unfortunately native `selectionchange` event is fired asynchronously.
// We need to wait until `SelectionObserver` handle the event and then render. Otherwise rendering will
// overwrite new DOM selection with selection from the view.
// See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
this._renderTimeoutId = setTimeout( () => document.render(), 0 );
} );

document.on( 'blur', ( evt, data ) => {
Expand All @@ -40,11 +48,29 @@ export default class FocusObserver extends DomEventObserver {
document.render();
}
} );

/**
* Identifier of the timeout currently used by focus listener to delay rendering execution.
*
* @private
* @member {Number} #_renderTimeoutId
*/
}

onDomEvent( domEvent ) {
this.fire( domEvent.type, domEvent );
}

/**
* @inheritDoc
*/
destroy() {
if ( this._renderTimeoutId ) {
clearTimeout( this._renderTimeoutId );
}

super.destroy();
}
}

/**
Expand Down
5 changes: 5 additions & 0 deletions tests/manual/nestededitable.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
* Put selection inside `foo bar baz` nested editable. Main editable and nested one should be focused (blue outline should be visible).
* Change selection inside nested editable and see if `Model contents` change accordingly.
* Click outside the editor. Outline from main editable and nested editable should be removed.
* Check following scenario:
* put selection inside nested editable: `foo bar baz{}`,
* click outside the editor (outlines should be removed),
* put selection at exact same place as before: `foo bar baz{}`,
* both editables should be focused (blue outline should be visible).
83 changes: 81 additions & 2 deletions tests/view/observer/focusobserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
* For licensing, see LICENSE.md.
*/

/* globals document */

/* globals document, window */
import FocusObserver from '../../../src/view/observer/focusobserver';
import ViewDocument from '../../../src/view/document';
import ViewRange from '../../../src/view/range';
import { setData } from '../../../src/dev-utils/view';

describe( 'FocusObserver', () => {
let viewDocument, observer;
Expand Down Expand Up @@ -115,5 +115,84 @@ describe( 'FocusObserver', () => {

expect( viewDocument.isFocused ).to.be.true;
} );

it( 'should delay rendering to the next iteration of event loop', () => {
const renderSpy = sinon.spy( viewDocument, 'render' );
const clock = sinon.useFakeTimers();

observer.onDomEvent( { type: 'focus', target: domMain } );
sinon.assert.notCalled( renderSpy );
clock.tick( 0 );
sinon.assert.called( renderSpy );

clock.restore();
} );

it( 'should not call render if destroyed', () => {
const renderSpy = sinon.spy( viewDocument, 'render' );
const clock = sinon.useFakeTimers();

observer.onDomEvent( { type: 'focus', target: domMain } );
sinon.assert.notCalled( renderSpy );
observer.destroy();
clock.tick( 0 );
sinon.assert.notCalled( renderSpy );

clock.restore();
} );
} );

describe( 'integration test', () => {
let viewDocument, viewRoot, domRoot, observer, domSelection;

beforeEach( () => {
domRoot = document.createElement( 'div' );
document.body.appendChild( domRoot );
viewDocument = new ViewDocument();
viewRoot = viewDocument.createRoot( domRoot );
observer = viewDocument.getObserver( FocusObserver );
domSelection = window.getSelection();
} );

it( 'should render document after selectionChange event', ( done ) => {
const selectionChangeSpy = sinon.spy();
const renderSpy = sinon.spy();

setData( viewDocument, '<div contenteditable="true">foo bar</div>' );
viewDocument.render();
const domEditable = domRoot.childNodes[ 0 ];

viewDocument.on( 'selectionChange', selectionChangeSpy );
viewDocument.on( 'render', renderSpy, { priority: 'low' } );

viewDocument.on( 'render', () => {
sinon.assert.callOrder( selectionChangeSpy, renderSpy );
done();
}, { priority: 'low' } );

observer.onDomEvent( { type: 'focus', target: domEditable } );
domSelection.collapse( domEditable );
} );

it( 'should render without selectionChange event', ( done ) => {
const selectionChangeSpy = sinon.spy();
const renderSpy = sinon.spy();

setData( viewDocument, '<div contenteditable="true">foo bar</div>' );
viewDocument.render();
const domEditable = domRoot.childNodes[ 0 ];

viewDocument.on( 'selectionChange', selectionChangeSpy );
viewDocument.on( 'render', renderSpy, { priority: 'low' } );

viewDocument.on( 'render', () => {
sinon.assert.notCalled( selectionChangeSpy );
sinon.assert.called( renderSpy );

done();
}, { priority: 'low' } );

observer.onDomEvent( { type: 'focus', target: domEditable } );
} );
} );
} );
46 changes: 26 additions & 20 deletions tests/view/observer/selectionobserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,32 @@
* For licensing, see LICENSE.md.
*/

/* globals setTimeout, document */
/* globals setTimeout, setInterval, clearInterval, document */

import ViewRange from '../../../src/view/range';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
import ViewSelection from '../../../src/view/selection';
import ViewDocument from '../../../src/view/document';
import SelectionObserver from '../../../src/view/observer/selectionobserver';
import MutationObserver from '../../../src/view/observer/mutationobserver';

import FocusObserver from '../../../src/view/observer/focusobserver';
import log from '@ckeditor/ckeditor5-utils/src/log';

import { parse } from '../../../src/dev-utils/view';

testUtils.createSinonSandbox();

describe( 'SelectionObserver', () => {
let viewDocument, viewRoot, mutationObserver, selectionObserver, domRoot;
let viewDocument, viewRoot, mutationObserver, selectionObserver, domRoot, domMain, domDocument;

beforeEach( ( done ) => {
domRoot = document.createElement( 'div' );
domRoot.innerHTML = `<div contenteditable="true" id="main"></div><div contenteditable="true" id="additional"></div>`;
document.body.appendChild( domRoot );
domDocument = document;
domRoot = domDocument.createElement( 'div' );
domRoot.innerHTML = `<div contenteditable="true"></div><div contenteditable="true" id="additional"></div>`;
domMain = domRoot.childNodes[ 0 ];
domDocument.body.appendChild( domRoot );

viewDocument = new ViewDocument();
viewDocument.createRoot( document.getElementById( 'main' ) );
viewDocument.createRoot( domMain );

mutationObserver = viewDocument.getObserver( MutationObserver );
selectionObserver = viewDocument.getObserver( SelectionObserver );
Expand All @@ -41,7 +42,7 @@ describe( 'SelectionObserver', () => {
viewDocument.render();

viewDocument.selection.removeAllRanges();
document.getSelection().removeAllRanges();
domDocument.getSelection().removeAllRanges();

viewDocument.isFocused = true;

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

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

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

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

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

let counter = 100;

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

viewDocument.on( 'selectionChangeDone', spy );

// Disable focus observer to not re-render view on each focus.
viewDocument.getObserver( FocusObserver ).disable();

// Change selection.
changeDomSelection();

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

expect( spy.calledOnce ).to.true;
expect( data ).to.have.property( 'domSelection' ).to.equal( document.getSelection() );
expect( data ).to.have.property( 'domSelection' ).to.equal( domDocument.getSelection() );

expect( data ).to.have.property( 'oldSelection' ).to.instanceof( ViewSelection );
expect( data.oldSelection.rangeCount ).to.equal( 0 );
Expand Down Expand Up @@ -295,13 +301,13 @@ describe( 'SelectionObserver', () => {
}, 110 );
}, 100 );
} );
} );

function changeDomSelection() {
const domSelection = document.getSelection();
const domFoo = document.getElementById( 'main' ).childNodes[ 0 ].childNodes[ 0 ];
const offset = domSelection.anchorOffset;
function changeDomSelection() {
const domSelection = domDocument.getSelection();
const domFoo = domMain.childNodes[ 0 ].childNodes[ 0 ];
const offset = domSelection.anchorOffset;

domSelection.removeAllRanges();
domSelection.collapse( domFoo, offset == 2 ? 3 : 2 );
}
domSelection.removeAllRanges();
domSelection.collapse( domFoo, offset == 2 ? 3 : 2 );
}
} );

0 comments on commit 115a91b

Please sign in to comment.