diff --git a/src/view/document.js b/src/view/document.js
index 4b848d31c..2b7b49af7 100644
--- a/src/view/document.js
+++ b/src/view/document.js
@@ -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.
*
diff --git a/src/view/observer/compositionobserver.js b/src/view/observer/compositionobserver.js
new file mode 100644
index 000000000..1d863aacc
--- /dev/null
+++ b/src/view/observer/compositionobserver.js
@@ -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.
+ */
diff --git a/src/view/view.js b/src/view/view.js
index a62a2224f..978615a4a 100644
--- a/src/view/view.js
+++ b/src/view/view.js
@@ -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';
@@ -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}.
*
@@ -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 );
diff --git a/tests/view/manual/compositionobserver.html b/tests/view/manual/compositionobserver.html
new file mode 100644
index 000000000..295deed50
--- /dev/null
+++ b/tests/view/manual/compositionobserver.html
@@ -0,0 +1,3 @@
+
diff --git a/tests/view/manual/compositionobserver.js b/tests/view/manual/compositionobserver.js
new file mode 100644
index 000000000..d4a8392f6
--- /dev/null
+++ b/tests/view/manual/compositionobserver.js
@@ -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 );
+ } );
diff --git a/tests/view/manual/compositionobserver.md b/tests/view/manual/compositionobserver.md
new file mode 100644
index 000000000..d048dd37f
--- /dev/null
+++ b/tests/view/manual/compositionobserver.md
@@ -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)
diff --git a/tests/view/observer/compositionobserver.js b/tests/view/observer/compositionobserver.js
new file mode 100644
index 000000000..2a16f3968
--- /dev/null
+++ b/tests/view/observer/compositionobserver.js
@@ -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 );
+ } );
+ } );
+} );
diff --git a/tests/view/view/view.js b/tests/view/view/view.js
index bb5369e8e..2bc122526 100644
--- a/tests/view/view/view.js
+++ b/tests/view/view/view.js
@@ -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';
@@ -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();
@@ -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()', () => {