diff --git a/src/widget/widget.js b/src/widget/widget.js index 63ce7719..8f9dd7ac 100644 --- a/src/widget/widget.js +++ b/src/widget/widget.js @@ -89,13 +89,25 @@ export default class Widget extends Plugin { */ _onKeydown( eventInfo, domEventData ) { const keyCode = domEventData.keyCode; + const isForward = keyCode == keyCodes.delete || keyCode == keyCodes.arrowdown || keyCode == keyCodes.arrowright; - // Handle only delete and backspace. - if ( keyCode !== keyCodes.delete && keyCode !== keyCodes.backspace ) { - return; + // Checks if delete/backspace or arrow keys were handled and then prevents default event behaviour and stops + // event propagation. + if ( ( isDeleteKeyCode( keyCode ) && this._handleDelete( isForward ) ) || + ( isArrowKeyCode( keyCode ) && this._handleArrowKeys( isForward ) ) ) { + domEventData.preventDefault(); + eventInfo.stop(); } + } - const dataController = this.editor.data; + /** + * Handles delete keys: backspace and delete. + * + * @private + * @param {Boolean} isForward Set to true if delete was performed in forward direction. + * @returns {Boolean|undefined} Returns `true` if keys were handled correctly. + */ + _handleDelete( isForward ) { const modelDocument = this.editor.document; const modelSelection = modelDocument.selection; @@ -104,22 +116,12 @@ export default class Widget extends Plugin { return; } - // Clone current selection to use it as a probe. We must leave default selection as it is so it can return - // to its current state after undo. - const probe = ModelSelection.createFromSelection( modelSelection ); - const isForward = ( keyCode == keyCodes.delete ); - - dataController.modifySelection( probe, { direction: isForward ? 'forward' : 'backward' } ); - - const objectElement = isForward ? probe.focus.nodeBefore : probe.focus.nodeAfter; - - if ( objectElement instanceof ModelElement && modelDocument.schema.objects.has( objectElement.name ) ) { - domEventData.preventDefault(); - eventInfo.stop(); + const objectElement = this._getObjectElementNextToSelection( isForward ); + if ( objectElement ) { modelDocument.enqueueChanges( () => { // Remove previous element if empty. - const previousNode = probe.anchor.parent; + const previousNode = modelSelection.anchor.parent; if ( previousNode.isEmpty ) { const batch = modelDocument.batch(); @@ -128,6 +130,51 @@ export default class Widget extends Plugin { this._setSelectionOverElement( objectElement ); } ); + + return true; + } + } + + /** + * Handles arrow keys. + * + * @param {Boolean} isForward Set to true if arrow key should be handled in forward direction. + * @returns {Boolean|undefined} Returns `true` if keys were handled correctly. + */ + _handleArrowKeys( isForward ) { + const modelDocument = this.editor.document; + const schema = modelDocument.schema; + const modelSelection = modelDocument.selection; + const objectElement = modelSelection.getSelectedElement(); + + // if object element is selected. + if ( objectElement && schema.objects.has( objectElement.name ) ) { + const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); + const newRange = modelDocument.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' ); + + if ( newRange ) { + modelDocument.enqueueChanges( () => { + modelSelection.setRanges( [ newRange ] ); + } ); + } + + return true; + } + + // If selection is next to object element. + // Return if not collapsed. + if ( !modelSelection.isCollapsed ) { + return; + } + + const objectElement2 = this._getObjectElementNextToSelection( isForward ); + + if ( objectElement2 instanceof ModelElement && modelDocument.schema.objects.has( objectElement2.name ) ) { + modelDocument.enqueueChanges( () => { + this._setSelectionOverElement( objectElement2 ); + } ); + + return true; } } @@ -140,4 +187,51 @@ export default class Widget extends Plugin { _setSelectionOverElement( element ) { this.editor.document.selection.setRanges( [ ModelRange.createOn( element ) ] ); } + + /** + * Checks if {@link module:engine/model/element~Element element} placed next to the current + * {@link module:engine/model/selection~Selection model selection} exists and is marked in + * {@link module:engine/model/schema~Schema schema} as `object`. + * + * @private + * @param {Boolean} forward Direction of checking. + * @returns {module:engine/model/element~Element|null} + */ + _getObjectElementNextToSelection( forward ) { + const modelDocument = this.editor.document; + const schema = modelDocument.schema; + const modelSelection = modelDocument.selection; + const dataController = this.editor.data; + + // Clone current selection to use it as a probe. We must leave default selection as it is so it can return + // to its current state after undo. + const probe = ModelSelection.createFromSelection( modelSelection ); + dataController.modifySelection( probe, { direction: forward ? 'forward' : 'backward' } ); + const objectElement = forward ? probe.focus.nodeBefore : probe.focus.nodeAfter; + + if ( objectElement instanceof ModelElement && schema.objects.has( objectElement.name ) ) { + return objectElement; + } + + return null; + } +} + +// Returns 'true' if provided key code represents one of the arrow keys. +// +// @param {Number} keyCode +// @returns {Boolean} +function isArrowKeyCode( keyCode ) { + return keyCode == keyCodes.arrowright || + keyCode == keyCodes.arrowleft || + keyCode == keyCodes.arrowup || + keyCode == keyCodes.arrowdown; +} + +//Returns 'true' if provided key code represents one of the delete keys: delete or backspace. +// +//@param {Number} keyCode +//@returns {Boolean} +function isDeleteKeyCode( keyCode ) { + return keyCode == keyCodes.delete || keyCode == keyCodes.backspace; } diff --git a/tests/manual/image.html b/tests/manual/image.html index f537c00f..e0dc63b6 100644 --- a/tests/manual/image.html +++ b/tests/manual/image.html @@ -5,10 +5,14 @@

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

- CKEditor logo +
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

+
+ CKEditor logo +
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

diff --git a/tests/manual/image.md b/tests/manual/image.md index 5ac19872..6fa69ba6 100644 --- a/tests/manual/image.md +++ b/tests/manual/image.md @@ -1,6 +1,6 @@ ## Image feature -* Two images with CKEditor logo should be loaded. +* Images with CKEditor logo should be loaded. * Hovering over image should apply yellow outline. * Clicking on image should apply blue outline which should not change when hovering over. @@ -12,3 +12,10 @@ * Place selection at the beginning of the second paragraph and press backspace (delete on Mac) - image should be selected. Second press should delete it. * Place selection in an empty paragraph before image and press delete (forward delete on Mac) - image should be selected and paragraph removed. * Place selection in an empty paragraph after image and press backspace (delete on Mac) - image should be selected and paragraph removed. + +### Arrow key handling + +* Click first image and press right arrow - second image should be selected. Same effect should happen for down arrow. +* Click second image and press left arrow - first image should be selected. Same effect should happen for up arrow. +* Place selection at the end of the first paragraph and press right arrow - third image should be selected. Same effect should happen for down arrow. +* Place selection at the beginning of the first paragraph and press left arrow - first image should be selected. Same effect should happen for up arrow. diff --git a/tests/widget/widget.js b/tests/widget/widget.js index 84f701a5..f703d24b 100644 --- a/tests/widget/widget.js +++ b/tests/widget/widget.js @@ -14,8 +14,10 @@ import AttributeContainer from 'ckeditor5/engine/view/attributeelement.js'; import { setData as setModelData, getData as getModelData } from 'ckeditor5/engine/dev-utils/model.js'; import { keyCodes } from 'ckeditor5/utils/keyboard.js'; +/* global document */ + describe( 'Widget', () => { - let editor, document, viewDocument; + let editor, doc, viewDocument; beforeEach( () => { return VirtualTestEditor.create( { @@ -23,14 +25,14 @@ describe( 'Widget', () => { } ) .then( newEditor => { editor = newEditor; - document = editor.document; + doc = editor.document; viewDocument = editor.editing.view; - document.schema.registerItem( 'widget', '$block' ); - document.schema.objects.add( 'widget' ); - document.schema.registerItem( 'paragraph', '$block' ); - document.schema.registerItem( 'inline', '$inline' ); - document.schema.objects.add( 'inline' ); + doc.schema.registerItem( 'widget', '$block' ); + doc.schema.objects.add( 'widget' ); + doc.schema.registerItem( 'paragraph', '$block' ); + doc.schema.registerItem( 'inline', '$inline' ); + doc.schema.objects.add( 'inline' ); buildModelConverter().for( editor.editing.modelToView ) .fromElement( 'paragraph' ) @@ -60,7 +62,7 @@ describe( 'Widget', () => { } ); it( 'should create selection over clicked widget', () => { - setModelData( document, '[]' ); + setModelData( doc, '[]' ); const viewDiv = viewDocument.getRoot().getChild( 0 ); const domEventDataMock = { target: viewDiv, @@ -69,12 +71,12 @@ describe( 'Widget', () => { viewDocument.fire( 'mousedown', domEventDataMock ); - expect( getModelData( document ) ).to.equal( '[]' ); + expect( getModelData( doc ) ).to.equal( '[]' ); sinon.assert.calledOnce( domEventDataMock.preventDefault ); } ); it( 'should create selection when clicked in nested element', () => { - setModelData( document, '[]' ); + setModelData( doc, '[]' ); const viewDiv = viewDocument.getRoot().getChild( 0 ); const viewB = viewDiv.getChild( 0 ); const domEventDataMock = { @@ -84,12 +86,12 @@ describe( 'Widget', () => { viewDocument.fire( 'mousedown', domEventDataMock ); - expect( getModelData( document ) ).to.equal( '[]' ); + expect( getModelData( doc ) ).to.equal( '[]' ); sinon.assert.calledOnce( domEventDataMock.preventDefault ); } ); it( 'should do nothing if clicked in non-widget element', () => { - setModelData( document, '[]foo bar' ); + setModelData( doc, '[]foo bar' ); const viewP = viewDocument.getRoot().getChild( 0 ); const domEventDataMock = { target: viewP, @@ -99,12 +101,12 @@ describe( 'Widget', () => { viewDocument.focus(); viewDocument.fire( 'mousedown', domEventDataMock ); - expect( getModelData( document ) ).to.equal( '[]foo bar' ); + expect( getModelData( doc ) ).to.equal( '[]foo bar' ); sinon.assert.notCalled( domEventDataMock.preventDefault ); } ); it( 'should not focus editable if already is focused', () => { - setModelData( document, '' ); + setModelData( doc, '' ); const widget = viewDocument.getRoot().getChild( 0 ); const domEventDataMock = { target: widget, @@ -117,255 +119,550 @@ describe( 'Widget', () => { sinon.assert.calledOnce( domEventDataMock.preventDefault ); sinon.assert.notCalled( focusSpy ); - expect( getModelData( document ) ).to.equal( '[]' ); + expect( getModelData( doc ) ).to.equal( '[]' ); } ); - describe( 'delete and backspace handling', () => { - test( - 'should select widget when backspace is pressed', - '[]foo', - keyCodes.backspace, - '[]foo' - ); - - test( - 'should remove empty element after selecting widget when backspace is pressed', - '[]', - keyCodes.backspace, - '[]' - ); - - test( - 'should select widget when delete is pressed', - 'foo[]', - keyCodes.delete, - 'foo[]' - ); - - test( - 'should remove empty element after selecting widget when delete is pressed', - '[]', - keyCodes.delete, - '[]' - ); - - test( - 'should not respond to other keys', - '[]foo', - 65, - '[]foo' - ); - - test( - 'should do nothing on non-collapsed selection', - '[f]oo', - keyCodes.backspace, - '[f]oo' - ); - - test( - 'should do nothing on non-object elements', - 'foo[]bar', - keyCodes.backspace, - 'foo[]bar' - ); - - test( - 'should work correctly with modifier key: backspace + ctrl', - '[]foo', - { keyCode: keyCodes.backspace, ctrlKey: true, preventDefault: () => {} }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: backspace + alt', - '[]foo', - { keyCode: keyCodes.backspace, altKey: true, preventDefault: () => {} }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: backspace + shift', - '[]foo', - { keyCode: keyCodes.backspace, shiftKey: true, preventDefault: () => {} }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: delete + ctrl', - 'foo[]', - { keyCode: keyCodes.delete, ctrlKey: true, preventDefault: () => {} }, - 'foo[]' - ); - - test( - 'should work correctly with modifier key: delete + alt', - 'foo[]', - { keyCode: keyCodes.delete, altKey: true, preventDefault: () => {} }, - 'foo[]' - ); - - test( - 'should work correctly with modifier key: delete + shift', - 'foo[]', - { keyCode: keyCodes.delete, shiftKey: true, preventDefault: () => {} }, - 'foo[]' - ); - - test( - 'should not modify backspace default behaviour in single paragraph boundaries', - '[]foo', - keyCodes.backspace, - '[]foo' - ); - - test( - 'should not modify delete default behaviour in single paragraph boundaries', - 'foo[]', - keyCodes.delete, - 'foo[]' - ); - - test( - 'should do nothing on selected widget preceded by a paragraph - backspace', - 'foo[]', - keyCodes.backspace, - 'foo[]' - ); - - test( - 'should do nothing on selected widget preceded by another widget - backspace', - '[]', - keyCodes.backspace, - '[]' - ); - - test( - 'should do nothing on selected widget before paragraph - backspace', - '[]foo', - keyCodes.backspace, - '[]foo' - ); - - test( - 'should do nothing on selected widget before another widget - backspace', - '[]', - keyCodes.backspace, - '[]' - ); - - test( - 'should do nothing on selected widget between paragraphs - backspace', - 'bar[]foo', - keyCodes.backspace, - 'bar[]foo' - ); - - test( - 'should do nothing on selected widget between other widgets - backspace', - '[]', - keyCodes.backspace, - '[]' - ); - - test( - 'should do nothing on selected widget preceded by a paragraph - delete', - 'foo[]', - keyCodes.delete, - 'foo[]' - ); - - test( - 'should do nothing on selected widget preceded by another widget - delete', - '[]', - keyCodes.delete, - '[]' - ); - - test( - 'should do nothing on selected widget before paragraph - delete', - '[]foo', - keyCodes.delete, - '[]foo' - ); - - test( - 'should do nothing on selected widget before another widget - delete', - '[]', - keyCodes.delete, - '[]' - ); - - test( - 'should do nothing on selected widget between paragraphs - delete', - 'bar[]foo', - keyCodes.delete, - 'bar[]foo' - ); - - test( - 'should do nothing on selected widget between other widgets - delete', - '[]', - keyCodes.delete, - '[]' - ); - - test( - 'should select inline objects - backspace', - 'foo[]bar', - keyCodes.backspace, - 'foo[]bar' - ); - - test( - 'should select inline objects - delete', - 'foo[]bar', - keyCodes.delete, - 'foo[]bar' - ); - - test( - 'should do nothing on selected inline objects - backspace', - 'foo[]bar', - keyCodes.backspace, - 'foo[]bar' - ); - - test( - 'should do nothing on selected inline objects - delete', - 'foo[]bar', - keyCodes.delete, - 'foo[]bar' - ); - - test( - 'should do nothing if selection is placed after first letter - backspace', - 'a[]', - keyCodes.backspace, - 'a[]' - ); - - test( - 'should do nothing if selection is placed before first letter - delete', - '[]a', - keyCodes.delete, - '[]a' - ); - - it( 'should prevent default behaviour and stop event propagation', () => { - const keydownHandler = sinon.spy(); - const domEventDataMock = { - keyCode: keyCodes.delete, - preventDefault: sinon.spy(), - }; - setModelData( document, 'foo[]' ); - viewDocument.on( 'keydown', keydownHandler ); - - viewDocument.fire( 'keydown', domEventDataMock ); - - expect( getModelData( document ) ).to.equal( 'foo[]' ); - sinon.assert.calledOnce( domEventDataMock.preventDefault ); - sinon.assert.notCalled( keydownHandler ); + describe( 'keys handling', () => { + describe( 'delete and backspace', () => { + test( + 'should select widget when backspace is pressed', + '[]foo', + keyCodes.backspace, + '[]foo' + ); + + test( + 'should remove empty element after selecting widget when backspace is pressed', + '[]', + keyCodes.backspace, + '[]' + ); + + test( + 'should select widget when delete is pressed', + 'foo[]', + keyCodes.delete, + 'foo[]' + ); + + test( + 'should remove empty element after selecting widget when delete is pressed', + '[]', + keyCodes.delete, + '[]' + ); + + test( + 'should not respond to other keys', + '[]foo', + 65, + '[]foo' + ); + + test( + 'should do nothing on non-collapsed selection', + '[f]oo', + keyCodes.backspace, + '[f]oo' + ); + + test( + 'should do nothing on non-object elements', + 'foo[]bar', + keyCodes.backspace, + 'foo[]bar' + ); + + test( + 'should work correctly with modifier key: backspace + ctrl', + '[]foo', + { keyCode: keyCodes.backspace, ctrlKey: true }, + '[]foo' + ); + + test( + 'should work correctly with modifier key: backspace + alt', + '[]foo', + { keyCode: keyCodes.backspace, altKey: true }, + '[]foo' + ); + + test( + 'should work correctly with modifier key: backspace + shift', + '[]foo', + { keyCode: keyCodes.backspace, shiftKey: true }, + '[]foo' + ); + + test( + 'should work correctly with modifier key: delete + ctrl', + 'foo[]', + { keyCode: keyCodes.delete, ctrlKey: true }, + 'foo[]' + ); + + test( + 'should work correctly with modifier key: delete + alt', + 'foo[]', + { keyCode: keyCodes.delete, altKey: true }, + 'foo[]' + ); + + test( + 'should work correctly with modifier key: delete + shift', + 'foo[]', + { keyCode: keyCodes.delete, shiftKey: true }, + 'foo[]' + ); + + test( + 'should not modify backspace default behaviour in single paragraph boundaries', + '[]foo', + keyCodes.backspace, + '[]foo' + ); + + test( + 'should not modify delete default behaviour in single paragraph boundaries', + 'foo[]', + keyCodes.delete, + 'foo[]' + ); + + test( + 'should do nothing on selected widget preceded by a paragraph - backspace', + 'foo[]', + keyCodes.backspace, + 'foo[]' + ); + + test( + 'should do nothing on selected widget preceded by another widget - backspace', + '[]', + keyCodes.backspace, + '[]' + ); + + test( + 'should do nothing on selected widget before paragraph - backspace', + '[]foo', + keyCodes.backspace, + '[]foo' + ); + + test( + 'should do nothing on selected widget before another widget - backspace', + '[]', + keyCodes.backspace, + '[]' + ); + + test( + 'should do nothing on selected widget between paragraphs - backspace', + 'bar[]foo', + keyCodes.backspace, + 'bar[]foo' + ); + + test( + 'should do nothing on selected widget between other widgets - backspace', + '[]', + keyCodes.backspace, + '[]' + ); + + test( + 'should do nothing on selected widget preceded by a paragraph - delete', + 'foo[]', + keyCodes.delete, + 'foo[]' + ); + + test( + 'should do nothing on selected widget preceded by another widget - delete', + '[]', + keyCodes.delete, + '[]' + ); + + test( + 'should do nothing on selected widget before paragraph - delete', + '[]foo', + keyCodes.delete, + '[]foo' + ); + + test( + 'should do nothing on selected widget before another widget - delete', + '[]', + keyCodes.delete, + '[]' + ); + + test( + 'should do nothing on selected widget between paragraphs - delete', + 'bar[]foo', + keyCodes.delete, + 'bar[]foo' + ); + + test( + 'should do nothing on selected widget between other widgets - delete', + '[]', + keyCodes.delete, + '[]' + ); + + test( + 'should select inline objects - backspace', + 'foo[]bar', + keyCodes.backspace, + 'foo[]bar' + ); + + test( + 'should select inline objects - delete', + 'foo[]bar', + keyCodes.delete, + 'foo[]bar' + ); + + test( + 'should do nothing on selected inline objects - backspace', + 'foo[]bar', + keyCodes.backspace, + 'foo[]bar' + ); + + test( + 'should do nothing on selected inline objects - delete', + 'foo[]bar', + keyCodes.delete, + 'foo[]bar' + ); + + test( + 'should do nothing if selection is placed after first letter - backspace', + 'a[]', + keyCodes.backspace, + 'a[]' + ); + + test( + 'should do nothing if selection is placed before first letter - delete', + '[]a', + keyCodes.delete, + '[]a' + ); + + it( 'should prevent default behaviour and stop event propagation', () => { + const keydownHandler = sinon.spy(); + const domEventDataMock = { + keyCode: keyCodes.delete, + preventDefault: sinon.spy(), + }; + setModelData( doc, 'foo[]' ); + viewDocument.on( 'keydown', keydownHandler ); + + viewDocument.fire( 'keydown', domEventDataMock ); + + expect( getModelData( doc ) ).to.equal( 'foo[]' ); + sinon.assert.calledOnce( domEventDataMock.preventDefault ); + sinon.assert.notCalled( keydownHandler ); + } ); + } ); + + describe( 'arrows', () => { + test( + 'should move selection forward from selected object - right arrow', + '[]foo', + keyCodes.arrowright, + '[]foo' + ); + + test( + 'should move selection forward from selected object - down arrow', + '[]foo', + keyCodes.arrowdown, + '[]foo' + ); + + test( + 'should move selection backward from selected object - left arrow', + 'foo[]', + keyCodes.arrowleft, + 'foo[]' + ); + + test( + 'should move selection backward from selected object - up arrow', + 'foo[]', + keyCodes.arrowup, + 'foo[]' + ); + + test( + 'should move selection to next widget - right arrow', + '[]', + keyCodes.arrowright, + '[]' + ); + + test( + 'should move selection to next widget - down arrow', + '[]', + keyCodes.arrowdown, + '[]' + ); + + test( + 'should move selection to previous widget - left arrow', + '[]', + keyCodes.arrowleft, + '[]' + ); + + test( + 'should move selection to previous widget - up arrow', + '[]', + keyCodes.arrowup, + '[]' + ); + + test( + 'should do nothing on non-collapsed selection next to object - right arrow', + 'ba[r]', + keyCodes.arrowright, + 'ba[r]' + ); + + test( + 'should do nothing on non-collapsed selection next to object - down arrow', + 'ba[r]', + keyCodes.arrowdown, + 'ba[r]' + ); + + test( + 'should do nothing on non-collapsed selection next to object - left arrow', + '[b]ar', + keyCodes.arrowleft, + '[b]ar' + ); + + test( + 'should do nothing on non-collapsed selection next to object - up arrow', + '[b]ar', + keyCodes.arrowup, + '[b]ar' + ); + + test( + 'should not move selection if there is no correct location - right arrow', + 'foo[]', + keyCodes.arrowright, + 'foo[]' + ); + + test( + 'should not move selection if there is no correct location - down arrow', + 'foo[]', + keyCodes.arrowdown, + 'foo[]' + ); + + test( + 'should not move selection if there is no correct location - left arrow', + '[]foo', + keyCodes.arrowleft, + '[]foo' + ); + + test( + 'should not move selection if there is no correct location - up arrow', + '[]foo', + keyCodes.arrowup, + '[]foo' + ); + + it( 'should prevent default behaviour when there is no correct location - document end', () => { + const keydownHandler = sinon.spy(); + const domEventDataMock = { + keyCode: keyCodes.arrowright, + preventDefault: sinon.spy(), + }; + setModelData( doc, 'foo[]' ); + viewDocument.on( 'keydown', keydownHandler ); + + viewDocument.fire( 'keydown', domEventDataMock ); + + expect( getModelData( doc ) ).to.equal( 'foo[]' ); + sinon.assert.calledOnce( domEventDataMock.preventDefault ); + sinon.assert.notCalled( keydownHandler ); + } ); + + it( 'should prevent default behaviour when there is no correct location - document start', () => { + const keydownHandler = sinon.spy(); + const domEventDataMock = { + keyCode: keyCodes.arrowleft, + preventDefault: sinon.spy(), + }; + setModelData( doc, '[]foo' ); + viewDocument.on( 'keydown', keydownHandler ); + + viewDocument.fire( 'keydown', domEventDataMock ); + + expect( getModelData( doc ) ).to.equal( '[]foo' ); + sinon.assert.calledOnce( domEventDataMock.preventDefault ); + sinon.assert.notCalled( keydownHandler ); + } ); + + test( + 'should move selection to object element - right arrow', + 'foo[]', + keyCodes.arrowright, + 'foo[]' + ); + + test( + 'should move selection to object element - down arrow', + 'foo[]', + keyCodes.arrowdown, + 'foo[]' + ); + + test( + 'should move selection to object element - left arrow', + '[]foo', + keyCodes.arrowleft, + '[]foo' + ); + + test( + 'should move selection to object element - up arrow', + '[]foo', + keyCodes.arrowup, + '[]foo' + ); + + test( + 'do nothing on non objects - right arrow', + 'foo[]bar', + keyCodes.arrowright, + 'foo[]bar' + ); + + test( + 'do nothing on non objects - down arrow', + 'foo[]bar', + keyCodes.arrowdown, + 'foo[]bar' + ); + + test( + 'do nothing on non objects - left arrow', + 'foo[]bar', + keyCodes.arrowleft, + 'foo[]bar' + ); + + test( + 'do nothing on non objects - up arrow', + 'foo[]bar', + keyCodes.arrowup, + 'foo[]bar' + ); + + test( + 'should work correctly with modifier key: right arrow + ctrl', + '[]foo', + { keyCode: keyCodes.arrowright, ctrlKey: true }, + '[]foo' + ); + + test( + 'should work correctly with modifier key: right arrow + alt', + '[]foo', + { keyCode: keyCodes.arrowright, altKey: true }, + '[]foo' + ); + + test( + 'should work correctly with modifier key: right arrow + shift', + '[]foo', + { keyCode: keyCodes.arrowright, shiftKey: true }, + '[]foo' + ); + + test( + 'should work correctly with modifier key: down arrow + ctrl', + '[]foo', + { keyCode: keyCodes.arrowdown, ctrlKey: true }, + '[]foo' + ); + + test( + 'should work correctly with modifier key: down arrow + alt', + '[]foo', + { keyCode: keyCodes.arrowdown, altKey: true }, + '[]foo' + ); + + test( + 'should work correctly with modifier key: down arrow + shift', + '[]foo', + { keyCode: keyCodes.arrowdown, shiftKey: true }, + '[]foo' + ); + + test( + 'should work correctly with modifier key: left arrow + ctrl', + 'foo[]', + { keyCode: keyCodes.arrowleft, ctrlKey: true }, + 'foo[]' + ); + + test( + 'should work correctly with modifier key: left arrow + alt', + 'foo[]', + { keyCode: keyCodes.arrowleft, altKey: true }, + 'foo[]' + ); + + test( + 'should work correctly with modifier key: left arrow + shift', + 'foo[]', + { keyCode: keyCodes.arrowleft, shiftKey: true }, + 'foo[]' + ); + + test( + 'should work correctly with modifier key: up arrow + ctrl', + 'foo[]', + { keyCode: keyCodes.arrowup, ctrlKey: true }, + 'foo[]' + ); + + test( + 'should work correctly with modifier key: up arrow + alt', + 'foo[]', + { keyCode: keyCodes.arrowup, altKey: true }, + 'foo[]' + ); + + test( + 'should work correctly with modifier key: up arrow + shift', + 'foo[]', + { keyCode: keyCodes.arrowup, shiftKey: true }, + 'foo[]' + ); + + test( + 'should do nothing if there is more than one selection in model', + '[foo][bar]', + keyCodes.arrowright, + '[foo][bar]' + ); } ); function test( name, data, keyCodeOrMock, expected ) { @@ -374,14 +671,14 @@ describe( 'Widget', () => { keyCode: keyCodeOrMock }; - setModelData( document, data ); + setModelData( doc, data ); viewDocument.fire( 'keydown', new DomEventData( viewDocument, - { target: null, preventDefault: () => {} }, + { target: document.createElement( 'div' ), preventDefault: () => {} }, domEventDataMock ) ); - expect( getModelData( document ) ).to.equal( expected ); + expect( getModelData( doc ) ).to.equal( expected ); } ); } } );