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 @@ +
+

foo bar

+
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()', () => {