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 #1330 from ckeditor/t/1329
Browse files Browse the repository at this point in the history
Feature: Introduced composition observer. Closes #1329.
  • Loading branch information
szymonkups authored Mar 5, 2018
2 parents 2462d27 + 309f010 commit a0ad8fe
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 1 deletion.
12 changes: 12 additions & 0 deletions src/view/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ export default class Document {
*/
this.set( 'isFocused', false );

/**
* True if composition is in progress inside the document.
*
* This property is updated by the {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
* If the {@link module:engine/view/observer/compositionobserver~CompositionObserver} is disabled this property will not change.
*
* @readonly
* @observable
* @member {Boolean} module:engine/view/document~Document#isComposing
*/
this.set( 'isComposing', false );

/**
* Post-fixer callbacks registered to the view document.
*
Expand Down
79 changes: 79 additions & 0 deletions src/view/observer/compositionobserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module engine/view/observer/compositionobserver
*/

import DomEventObserver from './domeventobserver';

/**
* {@link module:engine/view/document~Document#event:compositionstart Compositionstart},
* {@link module:engine/view/document~Document#event:compositionupdate compositionupdate} and
* {@link module:engine/view/document~Document#event:compositionend compositionend} events observer.
*
* Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
*
* @extends module:engine/view/observer/domeventobserver~DomEventObserver
*/
export default class CompositionObserver extends DomEventObserver {
constructor( view ) {
super( view );

this.domEventType = [ 'compositionstart', 'compositionupdate', 'compositionend' ];
const document = this.document;

document.on( 'compositionstart', () => {
document.isComposing = true;
} );

document.on( 'compositionend', () => {
document.isComposing = false;
} );
}

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

/**
* Fired when composition starts inside one of the editables.
*
* Introduced by {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
*
* Note that because {@link module:engine/view/observer/compositionobserver~CompositionObserver} is attached by the
* {@link module:engine/view/view~View} this event is available by default.
*
* @see module:engine/view/observer/compositionobserver~CompositionObserver
* @event module:engine/view/document~Document#event:compositionstart
* @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
*/

/**
* Fired when composition is updated inside one of the editables.
*
* Introduced by {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
*
* Note that because {@link module:engine/view/observer/compositionobserver~CompositionObserver} is attached by the
* {@link module:engine/view/view~View} this event is available by default.
*
* @see module:engine/view/observer/compositionobserver~CompositionObserver
* @event module:engine/view/document~Document#event:compositionupdate
* @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
*/

/**
* Fired when composition ends inside one of the editables.
*
* Introduced by {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
*
* Note that because {@link module:engine/view/observer/compositionobserver~CompositionObserver} is attached by the
* {@link module:engine/view/view~View} this event is available by default.
*
* @see module:engine/view/observer/compositionobserver~CompositionObserver
* @event module:engine/view/document~Document#event:compositionend
* @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
*/
3 changes: 3 additions & 0 deletions src/view/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import KeyObserver from './observer/keyobserver';
import FakeSelectionObserver from './observer/fakeselectionobserver';
import SelectionObserver from './observer/selectionobserver';
import FocusObserver from './observer/focusobserver';
import CompositionObserver from './observer/compositionobserver';

import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import log from '@ckeditor/ckeditor5-utils/src/log';
Expand Down Expand Up @@ -47,6 +48,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
* * {@link module:engine/view/observer/focusobserver~FocusObserver},
* * {@link module:engine/view/observer/keyobserver~KeyObserver},
* * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
* * {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
*
* This class also {@link module:engine/view/view~View#attachDomRoot bind DOM and View elements}.
*
Expand Down Expand Up @@ -138,6 +140,7 @@ export default class View {
this.addObserver( FocusObserver );
this.addObserver( KeyObserver );
this.addObserver( FakeSelectionObserver );
this.addObserver( CompositionObserver );

// Inject quirks handlers.
injectQuirksHandling( this );
Expand Down
3 changes: 3 additions & 0 deletions tests/view/manual/compositionobserver.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div id="editor">
<p>foo bar</p>
</div>
30 changes: 30 additions & 0 deletions tests/view/manual/compositionobserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/* globals console, window, document */

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Typing, Paragraph ]
} )
.then( editor => {
window.editor = editor;

const view = editor.editing.view;
const viewDocument = view.document;

viewDocument.on( 'compositionstart', ( evt, data ) => console.log( 'compositionstart', data ) );
viewDocument.on( 'compositionupdate', ( evt, data ) => console.log( 'compositionupdate', data ) );
viewDocument.on( 'compositionend', ( evt, data ) => console.log( 'compositionend', data ) );

view.focus();
} )
.catch( err => {
console.error( err.stack );
} );
10 changes: 10 additions & 0 deletions tests/view/manual/compositionobserver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
* Expected initialization: `{}foo bar`.
* Check whether composition events are logged to the console with proper data:
* `compositionstart`,
* `compositionupdate`,
* `compositionend`

**Composition events are fired while typing:**
* Hiragana,
* Spanish-ISO: accent `' + a`,
* MacOS: long `a` press (accent balloon)
109 changes: 109 additions & 0 deletions tests/view/observer/compositionobserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/* globals document */
import CompositionObserver from '../../../src/view/observer/compositionobserver';
import View from '../../../src/view/view';

describe( 'CompositionObserver', () => {
let view, viewDocument, observer;

beforeEach( () => {
view = new View();
viewDocument = view.document;
observer = view.getObserver( CompositionObserver );
} );

afterEach( () => {
view.destroy();
} );

it( 'should define domEventType', () => {
expect( observer.domEventType ).to.deep.equal( [ 'compositionstart', 'compositionupdate', 'compositionend' ] );
} );

describe( 'onDomEvent', () => {
it( 'should fire compositionstart with the right event data', () => {
const spy = sinon.spy();

viewDocument.on( 'compositionstart', spy );

observer.onDomEvent( { type: 'compositionstart', target: document.body } );

expect( spy.calledOnce ).to.be.true;

const data = spy.args[ 0 ][ 1 ];
expect( data.domTarget ).to.equal( document.body );
} );

it( 'should fire compositionupdate with the right event data', () => {
const spy = sinon.spy();

viewDocument.on( 'compositionupdate', spy );

observer.onDomEvent( { type: 'compositionupdate', target: document.body } );

expect( spy.calledOnce ).to.be.true;

const data = spy.args[ 0 ][ 1 ];
expect( data.domTarget ).to.equal( document.body );
} );

it( 'should fire compositionend with the right event data', () => {
const spy = sinon.spy();

viewDocument.on( 'compositionend', spy );

observer.onDomEvent( { type: 'compositionend', target: document.body } );

expect( spy.calledOnce ).to.be.true;

const data = spy.args[ 0 ][ 1 ];
expect( data.domTarget ).to.equal( document.body );
} );
} );

describe( 'handle isComposing property of the document', () => {
let domMain;

beforeEach( () => {
domMain = document.createElement( 'div' );
} );

it( 'should set isComposing to true on compositionstart', () => {
observer.onDomEvent( { type: 'compositionstart', target: domMain } );

expect( viewDocument.isComposing ).to.equal( true );
} );

it( 'should set isComposing to false on compositionend', () => {
observer.onDomEvent( { type: 'compositionstart', target: domMain } );

expect( viewDocument.isComposing ).to.equal( true );

observer.onDomEvent( { type: 'compositionend', target: domMain } );

expect( viewDocument.isComposing ).to.equal( false );
} );

it( 'should not change isComposing on compositionupdate during composition', () => {
observer.onDomEvent( { type: 'compositionstart', target: domMain } );

expect( viewDocument.isComposing ).to.equal( true );

observer.onDomEvent( { type: 'compositionupdate', target: domMain } );

expect( viewDocument.isComposing ).to.equal( true );
} );

it( 'should not change isComposing on compositionupdate outside composition', () => {
expect( viewDocument.isComposing ).to.equal( false );

observer.onDomEvent( { type: 'compositionupdate', target: domMain } );

expect( viewDocument.isComposing ).to.equal( false );
} );
} );
} );
4 changes: 3 additions & 1 deletion tests/view/view/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import KeyObserver from '../../../src/view/observer/keyobserver';
import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
import SelectionObserver from '../../../src/view/observer/selectionobserver';
import FocusObserver from '../../../src/view/observer/focusobserver';
import CompositionObserver from '../../../src/view/observer/compositionobserver';
import createViewRoot from '../_utils/createroot';
import Observer from '../../../src/view/observer/observer';
import log from '@ckeditor/ckeditor5-utils/src/log';
Expand All @@ -21,7 +22,7 @@ import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';

describe( 'view', () => {
const DEFAULT_OBSERVERS_COUNT = 5;
const DEFAULT_OBSERVERS_COUNT = 6;
let domRoot, view, viewDocument, ObserverMock, instantiated, enabled, ObserverMockGlobalCount;

testUtils.createSinonSandbox();
Expand Down Expand Up @@ -76,6 +77,7 @@ describe( 'view', () => {
expect( view.getObserver( FocusObserver ) ).to.be.instanceof( FocusObserver );
expect( view.getObserver( KeyObserver ) ).to.be.instanceof( KeyObserver );
expect( view.getObserver( FakeSelectionObserver ) ).to.be.instanceof( FakeSelectionObserver );
expect( view.getObserver( CompositionObserver ) ).to.be.instanceof( CompositionObserver );
} );

describe( 'attachDomRoot()', () => {
Expand Down

0 comments on commit a0ad8fe

Please sign in to comment.