/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import List from '@ckeditor/ckeditor5-list/src/list'; import Link from '@ckeditor/ckeditor5-link/src/link'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter'; import Input from '../src/input'; import TextTransformation from '../src/texttransformation'; import Writer from '@ckeditor/ckeditor5-engine/src/model/writer'; import ViewText from '@ckeditor/ckeditor5-engine/src/view/text'; import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element'; import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement'; import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin'; import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import env from '@ckeditor/ckeditor5-utils/src/env'; /* global document */ describe( 'Input feature', () => { let editor, model, modelRoot, view, viewDocument, viewRoot, listenter, domElement; testUtils.createSinonSandbox(); beforeEach( () => { listenter = Object.create( EmitterMixin ); domElement = document.createElement( 'div' ); document.body.appendChild( domElement ); const options = { plugins: [ Input, Paragraph, Bold, Italic, List, ShiftEnter, Link, TextTransformation ], typing: { transformations: { extra: [ { from: ':)', to: 'πŸ™‚' }, { from: ':family:', to: 'πŸ‘¨β€πŸ‘©β€πŸ‘§' } ] } } }; return ClassicTestEditor.create( domElement, options ) .then( newEditor => { // Mock image feature. newEditor.model.schema.register( 'image', { allowWhere: '$text' } ); newEditor.conversion.elementToElement( { model: 'image', view: 'img' } ); editor = newEditor; model = editor.model; modelRoot = model.document.getRoot(); view = editor.editing.view; viewDocument = view.document; viewRoot = viewDocument.getRoot(); editor.setData( '

foobar

' ); model.change( writer => { writer.setSelection( modelRoot.getChild( 0 ), 3 ); } ); } ); } ); afterEach( () => { listenter.stopListening(); domElement.remove(); return editor.destroy(); } ); describe( 'isInput()', () => { let input; beforeEach( () => { input = editor.plugins.get( 'Input' ); } ); it( 'returns true for batch created using "input" command', done => { model.document.once( 'change:data', ( evt, batch ) => { expect( input.isInput( batch ) ).to.be.true; done(); } ); editor.execute( 'input', { text: 'foo' } ); } ); it( 'returns false for batch not created using "input" command', () => { const batch = model.createBatch(); expect( input.isInput( batch ) ).to.be.false; } ); } ); describe( 'mutations handling', () => { it( 'should handle text mutation', () => { viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'foobar', newText: 'fooxbar', node: viewRoot.getChild( 0 ).getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( 'foox[]bar' ); expect( getViewData( view ) ).to.equal( '

foox{}bar

' ); } ); it( 'should handle text mutation change', () => { viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'foobar', newText: 'foodar', node: viewRoot.getChild( 0 ).getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( 'food[]ar' ); expect( getViewData( view ) ).to.equal( '

food{}ar

' ); } ); it( 'should handle text node insertion', () => { editor.setData( '

' ); viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [], newChildren: [ new ViewText( viewDocument, 'x' ) ], node: viewRoot.getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( 'x[]' ); expect( getViewData( view ) ).to.equal( '

x{}

' ); } ); it( 'should apply selection attributes to the inserted text', () => { setModelData( model, '[]', { selectionAttributes: { bold: true, italic: true } } ); viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [], newChildren: [ new ViewText( viewDocument, 'x' ) ], node: viewRoot.getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( '<$text bold="true" italic="true">x[]' ); } ); it( 'should handle multiple text mutations', () => { editor.setData( '

foobar

' ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'foo', newText: 'foob', node: viewRoot.getChild( 0 ).getChild( 0 ) }, { type: 'text', oldText: 'bar', newText: 'ar', node: viewRoot.getChild( 0 ).getChild( 1 ).getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( 'foob[]<$text bold="true">ar' ); expect( getViewData( view ) ).to.equal( '

foob{}ar

' ); } ); it( 'should handle multiple text node insertion', () => { editor.setData( '

' ); viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [], newChildren: [ new ViewText( viewDocument, 'x' ) ], node: viewRoot.getChild( 0 ) }, { type: 'children', oldChildren: [], newChildren: [ new ViewText( viewDocument, 'y' ) ], node: viewRoot.getChild( 1 ) } ] ); expect( getModelData( model ) ).to.equal( 'xy[]' ); expect( getViewData( view ) ).to.equal( '

x

y{}

' ); } ); it( 'should do nothing when two nodes were inserted', () => { editor.setData( '

' ); viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [], newChildren: [ new ViewText( viewDocument, 'x' ), new ViewElement( viewDocument, 'img' ) ], node: viewRoot.getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( '[]' ); expect( getViewData( view ) ).to.equal( '

[]

' ); } ); it( 'should do nothing when two nodes were inserted and one removed', () => { viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [ new ViewText( viewDocument, 'foobar' ) ], newChildren: [ new ViewText( viewDocument, 'x' ), new ViewElement( viewDocument, 'img' ) ], node: viewRoot.getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( 'foo[]bar' ); expect( getViewData( view ) ).to.equal( '

foo{}bar

' ); } ); it( 'should handle multiple children in the node', () => { editor.setData( '

foo

' ); viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [ new ViewText( viewDocument, 'foo' ), viewRoot.getChild( 0 ).getChild( 1 ) ], newChildren: [ new ViewText( viewDocument, 'foo' ), viewRoot.getChild( 0 ).getChild( 1 ), new ViewText( viewDocument, 'x' ) ], node: viewRoot.getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( 'foox[]' ); expect( getViewData( view ) ).to.equal( '

foox{}

' ); } ); it( 'should do nothing when node was removed', () => { viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [ new ViewText( viewDocument, 'foobar' ) ], newChildren: [], node: viewRoot.getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( 'foo[]bar' ); expect( getViewData( view ) ).to.equal( '

foo{}bar

' ); } ); it( 'should do nothing when element was inserted', () => { editor.setData( '

' ); viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [], newChildren: [ new ViewElement( viewDocument, 'img' ) ], node: viewRoot.getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( '[]' ); expect( getViewData( view ) ).to.equal( '

[]

' ); } ); it( 'should set model selection appropriately to view selection passed in mutations event', () => { // This test case emulates spellchecker correction. const viewSelection = view.createSelection(); viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 6 ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'foobar', newText: 'foodar', node: viewRoot.getChild( 0 ).getChild( 0 ) } ], viewSelection ); expect( getModelData( model ) ).to.equal( 'foodar[]' ); expect( getViewData( view ) ).to.equal( '

foodar{}

' ); } ); it( 'should use up to one insert and remove operations (spellchecker)', () => { // This test case emulates spellchecker correction. const viewSelection = view.createSelection(); viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 6 ); testUtils.sinon.spy( Writer.prototype, 'insert' ); testUtils.sinon.spy( Writer.prototype, 'remove' ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'foobar', newText: 'fxobxr', node: viewRoot.getChild( 0 ).getChild( 0 ) } ], viewSelection ); expect( Writer.prototype.insert.calledOnce ).to.be.true; expect( Writer.prototype.remove.calledOnce ).to.be.true; } ); it( 'should place selection after when correcting to longer word (spellchecker)', () => { // This test case emulates spellchecker correction. editor.setData( '

Foo hous a

' ); const viewSelection = view.createSelection(); viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 9 ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'Foo hous a', newText: 'Foo house a', node: viewRoot.getChild( 0 ).getChild( 0 ) } ], viewSelection ); expect( getModelData( model ) ).to.equal( 'Foo house[] a' ); expect( getViewData( view ) ).to.equal( '

Foo house{} a

' ); } ); it( 'should place selection after when correcting to shorter word (spellchecker)', () => { // This test case emulates spellchecker correction. editor.setData( '

Bar athat foo

' ); const viewSelection = view.createSelection(); viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 8 ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'Bar athat foo', newText: 'Bar that foo', node: viewRoot.getChild( 0 ).getChild( 0 ) } ], viewSelection ); expect( getModelData( model ) ).to.equal( 'Bar that[] foo' ); expect( getViewData( view ) ).to.equal( '

Bar that{} foo

' ); } ); it( 'should place selection after when merging two words (spellchecker)', () => { // This test case emulates spellchecker correction. editor.setData( '

Foo hous e

' ); const viewSelection = view.createSelection(); viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 9 ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'Foo hous e', newText: 'Foo house', node: viewRoot.getChild( 0 ).getChild( 0 ) } ], viewSelection ); expect( getModelData( model ) ).to.equal( 'Foo house[]' ); expect( getViewData( view ) ).to.equal( '

Foo house{}

' ); } ); it( 'should place non-collapsed selection after changing single character (composition)', () => { editor.setData( '

Foo house

' ); const viewSelection = view.createSelection(); viewSelection.setTo( viewRoot.getChild( 0 ).getChild( 0 ), 8 ); viewSelection.setFocus( viewRoot.getChild( 0 ).getChild( 0 ), 9 ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'Foo house', newText: 'Foo housa', node: viewRoot.getChild( 0 ).getChild( 0 ) } ], viewSelection ); expect( getModelData( model ) ).to.equal( 'Foo hous[a]' ); expect( getViewData( view ) ).to.equal( '

Foo hous{a}

' ); } ); it( 'should replace last   with space', () => { model.change( writer => { writer.setSelection( modelRoot.getChild( 0 ), 6 ); } ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'foobar', newText: 'foobar\u00A0', node: viewRoot.getChild( 0 ).getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( 'foobar []' ); expect( getViewData( view ) ).to.equal( '

foobar {}

' ); } ); it( 'should replace first   with space', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 0 ), writer.createPositionAt( modelRoot.getChild( 0 ), 0 ) ) ); } ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'foobar', newText: '\u00A0foobar', node: viewRoot.getChild( 0 ).getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( ' []foobar' ); expect( getViewData( view ) ).to.equal( '

{}foobar

' ); } ); it( 'should replace all   with spaces', () => { model.change( writer => { writer.setSelection( modelRoot.getChild( 0 ), 6 ); } ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'foobar', newText: 'foobar\u00A0\u00A0\u00A0baz', node: viewRoot.getChild( 0 ).getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( 'foobar baz[]' ); expect( getViewData( view ) ).to.equal( '

foobar baz{}

' ); } ); // ckeditor5#718. it( 'should not crash and prevent all changes if view common ancestor of mutations cannot be mapped to model', () => { editor.setData( '

Foo

' ); const ul = viewRoot.getChild( 1 ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'Bar', newText: 'Bx', node: ul.getChild( 0 ) }, { type: 'children', oldChildren: [ ul.getChild( 0 ), ul.getChild( 1 ) ], newChildren: [ ul.getChild( 0 ) ], node: ul } ] ); expect( getViewData( view ) ).to.equal( '

{}Foo

' ); } ); it( 'should handle bogus br correctly', () => { editor.setData( '

Foo

' ); editor.model.change( writer => { writer.setSelection( editor.model.document.getRoot().getChild( 0 ), 'end' ); writer.removeSelectionAttribute( 'bold' ); } ); // We need to change the DOM content manually because typing algorithm actually does not check // `newChildren` and `oldChildren` list but takes them from DOM and model. const p = viewRoot.getChild( 0 ); const domP = editor.editing.view.domConverter.mapViewToDom( p ); domP.appendChild( document.createTextNode( ' ' ) ); domP.appendChild( document.createElement( 'br' ) ); viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [ viewRoot.getChild( 0 ).getChild( 0 ) ], newChildren: [ new ViewElement( viewDocument, 'strong', null, new ViewText( viewDocument, 'Foo' ) ), new ViewText( viewDocument, ' ' ), new ViewElement( viewDocument, 'br' ) ], node: viewRoot.getChild( 0 ) } ] ); expect( getViewData( view ) ).to.equal( '

Foo {}

' ); } ); it( 'should handle children mutation correctly if there are soft breaks in the mutated container', () => { editor.setData( '

Foo
Bar

' ); editor.model.change( writer => { writer.setSelection( editor.model.document.getRoot().getChild( 0 ), 'end' ); writer.removeSelectionAttribute( 'bold' ); } ); // We need to change the DOM content manually because typing algorithm actually does not check // `newChildren` and `oldChildren` list but takes them from DOM and model. const p = viewRoot.getChild( 0 ); const domP = editor.editing.view.domConverter.mapViewToDom( p ); domP.appendChild( document.createTextNode( ' ' ) ); domP.appendChild( document.createElement( 'br' ) ); viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [ ...viewRoot.getChild( 0 ).getChildren() ], newChildren: [ new ViewElement( viewDocument, 'strong', null, new ViewText( viewDocument, 'Foo' ) ), new ViewElement( viewDocument, 'br' ), new ViewElement( viewDocument, 'strong', null, new ViewText( viewDocument, 'Bar' ) ), new ViewText( viewDocument, ' ' ), new ViewElement( viewDocument, 'br' ) ], node: viewRoot.getChild( 0 ) } ] ); expect( getViewData( view ) ).to.equal( '

Foo

Bar {}

' ); } ); // ckeditor5-typing#170. it( 'should handle mutations correctly if there was an   in model already', () => { editor.setData( '

Foo bar

' ); model.change( writer => { writer.setSelection( modelRoot.getChild( 0 ), 1 ); } ); // We need to change the DOM content manually because typing algorithm actually does not check // `newChildren` and `oldChildren` list but takes them from DOM and model. const strong = viewRoot.getChild( 0 ).getChild( 0 ).getChild( 0 ); const domStrong = editor.editing.view.domConverter.mapViewToDom( strong ); domStrong.appendChild( document.createTextNode( 'x' ) ); // The mutation provided here is a bit different than what browser outputs, but browser outputs three mutations // which changes the order of elements in the DOM so to keep it simple, only one, key mutation is used in the test. // Also, the only thing that the typing algorithm takes from the mutations is `node`... viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: Array.from( viewRoot.getChild( 0 ).getChild( 0 ).getChildren() ), newChildren: [ new ViewElement( viewDocument, 'strong', null, new ViewText( viewDocument, 'Fx' ) ) ], node: viewRoot.getChild( 0 ).getChild( 0 ) } ] ); expect( getModelData( model ) ).to.equal( '' + '<$text bold="true" linkHref="#">Fx[]' + '<$text bold="true">oo\u00A0bar' + '' ); expect( getViewData( view ) ).to.equal( '

' + 'Fx{}' + 'oo\u00A0bar' + '

' ); } ); it( 'should handle correctly if last element is inline', () => { editor.model.schema.register( 'placeholder', { allowWhere: '$text', isInline: true } ); editor.conversion.elementToElement( { model: 'placeholder', view: 'placeholder' } ); editor.setData( '

foo

' ); editor.model.change( writer => { writer.setSelection( editor.model.document.getRoot().getChild( 0 ), 'end' ); } ); expect( getViewData( view ) ).to.equal( '

foo[]

' ); // We need to change the DOM content manually because typing algorithm actually does not check // `newChildren` and `oldChildren` list but takes them from DOM and model. const p = viewRoot.getChild( 0 ); const domP = editor.editing.view.domConverter.mapViewToDom( p ); domP.appendChild( document.createTextNode( '!' ) ); viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [ ...viewRoot.getChild( 0 ).getChildren() ], newChildren: [ new ViewText( viewDocument, 'Foo' ), new ViewContainerElement( viewDocument, 'placeholder' ), new ViewText( viewDocument, 'f' ) ], node: viewRoot.getChild( 0 ) } ] ); expect( getViewData( view ) ).to.equal( '

foo!{}

' ); } ); it( 'should handle correctly if some elements are inline', () => { editor.model.schema.register( 'placeholder', { allowWhere: '$text', isInline: true } ); editor.conversion.elementToElement( { model: 'placeholder', view: 'placeholder' } ); editor.setData( '

foobarbaz

' ); editor.model.change( writer => { writer.setSelection( editor.model.document.getRoot().getChild( 0 ), 'end' ); } ); // We need to change the DOM content manually because typing algorithm actually does not check // `newChildren` and `oldChildren` list but takes them from DOM and model. const p = viewRoot.getChild( 0 ); const domP = editor.editing.view.domConverter.mapViewToDom( p ); domP.appendChild( document.createTextNode( '!' ) ); viewDocument.fire( 'mutations', [ { type: 'children', oldChildren: [ ...viewRoot.getChild( 0 ).getChildren() ], newChildren: [ new ViewText( viewDocument, 'foo' ), new ViewContainerElement( viewDocument, 'placeholder' ), new ViewText( viewDocument, 'bar' ), new ViewContainerElement( viewDocument, 'placeholder' ), new ViewText( viewDocument, 'baz' ) ], node: viewRoot.getChild( 0 ) } ] ); expect( getViewData( view ) ).to.equal( '

foobarbaz!{}

' ); } ); // https://github.com/ckeditor/ckeditor5-typing/issues/181 it( 'should not crash if the mutation old text is same as new text', () => { // It shouldn't matter what data is here, I am putting it like it is in the test scenario, but it is really about // what mutations are generated. editor.setData( '

Foo  Bar

' ); const p = viewRoot.getChild( 0 ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: ' ', newText: ' ', node: p.getChild( 1 ) }, { type: 'text', oldText: 'Foo', newText: 'Foox', node: p.getChild( 0 ) } ] ); expect( getViewData( view ) ).to.equal( '

Foox{} Bar

' ); } ); describe( 'emoji', () => { it( 'should properly handle input after multi-byte emoji transformation (in short paragraph)', () => { testEmojiMutations( 'foo', ':)123456', 'πŸ™‚123456' ); } ); it( 'should properly handle input after multi-byte emoji transformation (in long paragraph)', () => { testEmojiMutations( createTestString( 200 ), ':)123456', 'πŸ™‚123456' ); } ); it( 'should properly handle input in text with multi-byte emoji (in short paragraph)', () => { testEmojiMutations( 'foo😱bar', '123456' ); } ); it( 'should properly handle input in text with multi-byte emoji (in long paragraph)', () => { const textInParagraph = createTestString( 200, ( v, idx ) => idx == 60 ? '😱' : v ); testEmojiMutations( textInParagraph, '123456' ); } ); it( 'should properly handle input in middle of text with multi-byte emoji (in short paragraph)', () => { testEmojiMutations( 'foo😱bar', '123456', 6 ); } ); it( 'should properly handle input in middle of text with multi-byte emoji (in long paragraph)', () => { const textInParagraph = createTestString( 200, ( v, idx ) => idx == 60 ? '😱' : v ); testEmojiMutations( textInParagraph, '123456', 100 ); } ); it( 'should properly handle input after multi-byte emoji zwj sequence transformation (in short paragraph)', () => { testEmojiMutations( 'foobar', ':family:123456', 'πŸ‘¨β€πŸ‘©β€πŸ‘§123456' ); } ); it( 'should properly handle input after multi-byte emoji zwj sequence transformation (in long paragraph)', () => { testEmojiMutations( createTestString( 200 ), ':family:123456', 'πŸ‘¨β€πŸ‘©β€πŸ‘§123456' ); } ); function createTestString( length, callback = v => v ) { const firstCharCode = 'a'.charCodeAt( 0 ); const lettersCount = 'z'.charCodeAt( 0 ) - firstCharCode + 1; return new Array( length ) .fill( '' ) .map( ( value, idx ) => callback( String.fromCharCode( firstCharCode + idx % lettersCount ), idx ) ) .join( '' ); } function testEmojiMutations( textInParagraph, typing, expectedOrIndex = typing, index = -1 ) { let expected = expectedOrIndex; if ( typeof expectedOrIndex == 'number' ) { index = expectedOrIndex; expected = typing; } setModelData( model, `${ textInParagraph }[]` ); typing.split( '' ).forEach( ( char, idx ) => simulateMutation( char, index < 0 ? index : index + idx ) ); const expectedResult = index < 0 ? textInParagraph + expected : textInParagraph.substring( 0, index ) + expected + textInParagraph.substring( index ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( `${ expectedResult }` ); } function simulateMutation( text, index = -1 ) { const placeOfMutation = viewRoot.getChild( 0 ).getChild( 0 ); const oldText = placeOfMutation.data; const newText = index < 0 ? oldText + text : oldText.substring( 0, index ) + text + oldText.substring( index ); viewDocument.fire( 'mutations', [ { type: 'text', oldText, newText, node: placeOfMutation } ] ); } } ); } ); describe( 'keystroke handling', () => { it( 'should remove contents', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 0 ), 4 ) ) ); } ); listenter.listenTo( viewDocument, 'keydown', () => { expect( getModelData( model ) ).to.equal( 'fo[]ar' ); }, { priority: 'lowest' } ); viewDocument.fire( 'keydown', { keyCode: getCode( 'y' ) } ); } ); // #97 it( 'should remove contents and merge blocks', () => { setModelData( model, 'fo[ob]ar' ); listenter.listenTo( viewDocument, 'keydown', () => { expect( getModelData( model ) ).to.equal( 'fo[]ar' ); viewDocument.fire( 'mutations', [ { type: 'text', oldText: 'foar', newText: 'foyar', node: viewRoot.getChild( 0 ).getChild( 0 ) } ] ); }, { priority: 'lowest' } ); viewDocument.fire( 'keydown', { keyCode: getCode( 'y' ) } ); expect( getModelData( model ) ).to.equal( 'foy[]ar' ); expect( getViewData( view ) ).to.equal( '

foy{}ar

' ); } ); it( 'should do nothing on arrow key', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 0 ), 4 ) ) ); } ); viewDocument.fire( 'keydown', { keyCode: getCode( 'arrowdown' ) } ); expect( getModelData( model ) ).to.equal( 'fo[ob]ar' ); } ); it( 'should do nothing on ctrl combinations', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 0 ), 4 ) ) ); } ); viewDocument.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } ); expect( getModelData( model ) ).to.equal( 'fo[ob]ar' ); } ); it( 'should do nothing on non printable keys', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 0 ), 4 ) ) ); } ); viewDocument.fire( 'keydown', { keyCode: 16 } ); // Shift viewDocument.fire( 'keydown', { keyCode: 19 } ); // Pause viewDocument.fire( 'keydown', { keyCode: 35 } ); // Home viewDocument.fire( 'keydown', { keyCode: 112 } ); // F1 viewDocument.fire( 'keydown', { keyCode: 255 } ); // Display brightness // Media control keys viewDocument.fire( 'keydown', { keyCode: 173 } ); // Mute viewDocument.fire( 'keydown', { keyCode: 174 } ); // Volume up viewDocument.fire( 'keydown', { keyCode: 175 } ); // Volume down viewDocument.fire( 'keydown', { keyCode: 176 } ); // Next song viewDocument.fire( 'keydown', { keyCode: 177 } ); // Previous song viewDocument.fire( 'keydown', { keyCode: 179 } ); // Stop expect( getModelData( model ) ).to.equal( 'fo[ob]ar' ); } ); // #69 it( 'should do nothing on tab key', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 0 ), 4 ) ) ); } ); viewDocument.fire( 'keydown', { keyCode: 9 } ); // Tab expect( getModelData( model ) ).to.equal( 'fo[ob]ar' ); } ); it( 'should do nothing if selection is collapsed', () => { viewDocument.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } ); expect( getModelData( model ) ).to.equal( 'foo[]bar' ); } ); it( 'should lock buffer if selection is not collapsed', () => { const buffer = editor.commands.get( 'input' )._buffer; const lockSpy = testUtils.sinon.spy( buffer, 'lock' ); const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' ); model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 0 ), 4 ) ) ); } ); viewDocument.fire( 'keydown', { keyCode: getCode( 'y' ) } ); expect( lockSpy.calledOnce ).to.be.true; expect( unlockSpy.calledOnce ).to.be.true; } ); it( 'should not lock buffer on non printable keys', () => { const buffer = editor.commands.get( 'input' )._buffer; const lockSpy = testUtils.sinon.spy( buffer, 'lock' ); const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' ); viewDocument.fire( 'keydown', { keyCode: 16 } ); // Shift viewDocument.fire( 'keydown', { keyCode: 35 } ); // Home viewDocument.fire( 'keydown', { keyCode: 112 } ); // F1 expect( lockSpy.callCount ).to.equal( 0 ); expect( unlockSpy.callCount ).to.equal( 0 ); } ); it( 'should not lock buffer on collapsed selection', () => { const buffer = editor.commands.get( 'input' )._buffer; const lockSpy = testUtils.sinon.spy( buffer, 'lock' ); const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' ); viewDocument.fire( 'keydown', { keyCode: getCode( 'b' ) } ); viewDocument.fire( 'keydown', { keyCode: getCode( 'a' ) } ); viewDocument.fire( 'keydown', { keyCode: getCode( 'z' ) } ); expect( lockSpy.callCount ).to.equal( 0 ); expect( unlockSpy.callCount ).to.equal( 0 ); } ); it( 'should not modify document when input command is disabled and selection is collapsed', () => { setModelData( model, 'foo[]bar' ); editor.commands.get( 'input' ).isEnabled = false; viewDocument.fire( 'keydown', { keyCode: getCode( 'b' ) } ); expect( getModelData( model ) ).to.equal( 'foo[]bar' ); } ); it( 'should not modify document when input command is disabled and selection is non-collapsed', () => { setModelData( model, 'fo[ob]ar' ); editor.commands.get( 'input' ).isEnabled = false; viewDocument.fire( 'keydown', { keyCode: getCode( 'b' ) } ); expect( getModelData( model ) ).to.equal( 'fo[ob]ar' ); } ); describe( '#83', () => { it( 'should remove contents on composition start key if not during composition', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 0 ), 4 ) ) ); } ); viewDocument.fire( 'keydown', { keyCode: 229 } ); expect( getModelData( model ) ).to.equal( 'fo[]ar' ); } ); it( 'should not remove contents on composition start key if during composition', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 0 ), 4 ) ) ); } ); viewDocument.fire( 'compositionstart' ); viewDocument.fire( 'keydown', { keyCode: 229 } ); expect( getModelData( model ) ).to.equal( 'fo[ob]ar' ); } ); it( 'should not remove contents on compositionstart event if selection is flat', () => { editor.setData( '

foo bar

' ); model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 0 ), 5 ) ) ); } ); viewDocument.fire( 'compositionstart' ); expect( getModelData( model ) ).to.equal( '<$text bold="true">fo[o <$text italic="true">b]ar' ); } ); it( 'should not remove contents on compositionstart event if no selection', () => { editor.setData( '

foo bar

' ); const documentSelection = model.document.selection; // Create empty selection. model.document.selection = model.createSelection(); viewDocument.fire( 'compositionstart' ); expect( getModelData( model ) ).to.equal( '<$text bold="true">foo <$text italic="true">bar' ); // Restore document selection. model.document.selection = documentSelection; } ); it( 'should remove contents on compositionstart event if selection is not flat', () => { editor.setData( '

foo

bar

' ); model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 1 ), 2 ) ) ); } ); viewDocument.fire( 'compositionstart' ); expect( getModelData( model ) ).to.equal( '<$text bold="true">fo[]<$text italic="true">r' ); } ); it( 'should not remove contents on keydown event after compositionend event if selection did not change', () => { editor.setData( '

foo

bar

' ); model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 1 ), 2 ) ) ); } ); viewDocument.fire( 'compositionend' ); viewDocument.fire( 'keydown', { keyCode: 229 } ); expect( getModelData( model ) ).to.equal( '<$text bold="true">fo[o<$text italic="true">ba]r' ); } ); it( 'should remove contents on keydown event after compositionend event if selection have changed', () => { editor.setData( '

foo

bar

' ); model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 1 ), 2 ) ) ); } ); viewDocument.fire( 'compositionend' ); model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 1 ), 1 ) ) ); } ); viewDocument.fire( 'keydown', { keyCode: 229 } ); expect( getModelData( model ) ).to.equal( '<$text bold="true">fo[]<$text italic="true">ar' ); } ); } ); } ); } ); describe( 'Input feature - Android', () => { let editor, model, modelRoot, view, viewDocument, listenter, oldEnvIsAndroid, domElement; testUtils.createSinonSandbox(); before( () => { oldEnvIsAndroid = env.isAndroid; env.isAndroid = true; } ); beforeEach( () => { listenter = Object.create( EmitterMixin ); domElement = document.createElement( 'div' ); document.body.appendChild( domElement ); return ClassicTestEditor.create( domElement, { plugins: [ Input, Paragraph, Bold, Italic, List, ShiftEnter, Link ] } ) .then( newEditor => { editor = newEditor; model = editor.model; modelRoot = model.document.getRoot(); view = editor.editing.view; viewDocument = view.document; editor.setData( '

foobar

' ); model.change( writer => { writer.setSelection( modelRoot.getChild( 0 ), 3 ); } ); } ); } ); afterEach( () => { listenter.stopListening(); domElement.remove(); return editor.destroy(); } ); after( () => { env.isAndroid = oldEnvIsAndroid; } ); describe( 'keystroke handling', () => { it( 'should remove contents', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionAt( modelRoot.getChild( 0 ), 2 ), writer.createPositionAt( modelRoot.getChild( 0 ), 4 ) ) ); } ); listenter.listenTo( viewDocument, 'beforeinput', () => { expect( getModelData( model ) ).to.equal( 'fo[]ar' ); }, { priority: 'lowest' } ); // On Android, `keycode` is set to `229` (in scenarios when `keydown` event is not send). viewDocument.fire( 'beforeinput', { keyCode: 229 } ); } ); it( 'should remove contents and merge blocks', () => { setModelData( model, 'fo[ob]ar' ); listenter.listenTo( viewDocument, 'beforeinput', () => { expect( getModelData( model ) ).to.equal( 'fo[]ar' ); }, { priority: 'lowest' } ); // On Android, `keycode` is set to `229` (in scenarios when `keydown` event is not send). viewDocument.fire( 'beforeinput', { keyCode: 229 } ); } ); it( 'should do nothing if selection is collapsed', () => { viewDocument.fire( 'beforeinput', { keyCode: 229 } ); expect( getModelData( model ) ).to.equal( 'foo[]bar' ); } ); it( 'should not modify document when input command is disabled and selection is collapsed', () => { setModelData( model, 'foo[]bar' ); editor.commands.get( 'input' ).isEnabled = false; // On Android, `keycode` is set to `229` (in scenarios when `keydown` event is not send). viewDocument.fire( 'beforeinput', { keyCode: 229 } ); expect( getModelData( model ) ).to.equal( 'foo[]bar' ); } ); it( 'should not modify document when input command is disabled and selection is non-collapsed', () => { setModelData( model, 'fo[ob]ar' ); editor.commands.get( 'input' ).isEnabled = false; // On Android, `keycode` is set to `229` (in scenarios when `keydown` event is not send). viewDocument.fire( 'beforeinput', { keyCode: 229 } ); expect( getModelData( model ) ).to.equal( 'fo[ob]ar' ); } ); } ); } );