diff --git a/package.json b/package.json
index 7034b513..ee928650 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"@ckeditor/ckeditor5-theme-lark": "^1.0.0-alpha.2"
},
"devDependencies": {
+ "@ckeditor/ckeditor5-typing": "^1.0.0-alpha.2",
"eslint": "^4.8.0",
"eslint-config-ckeditor5": "^1.0.7",
"husky": "^0.14.3",
diff --git a/src/widget.js b/src/widget.js
index 5770a61c..30c2fa1e 100644
--- a/src/widget.js
+++ b/src/widget.js
@@ -84,6 +84,14 @@ export default class Widget extends Plugin {
// Handle custom keydown behaviour.
this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: 'high' } );
+
+ // Handle custom delete behaviour.
+ this.listenTo( viewDocument, 'delete', ( evt, data ) => {
+ if ( this._handleDelete( data.direction == 'forward' ) ) {
+ data.preventDefault();
+ evt.stop();
+ }
+ }, { priority: 'high' } );
}
/**
@@ -141,9 +149,7 @@ export default class Widget extends Plugin {
// Checks if the keys were handled and then prevents the default event behaviour and stops
// the propagation.
- if ( isDeleteKeyCode( keyCode ) ) {
- wasHandled = this._handleDelete( isForward );
- } else if ( isArrowKeyCode( keyCode ) ) {
+ if ( isArrowKeyCode( keyCode ) ) {
wasHandled = this._handleArrowKeys( isForward );
} else if ( isSelectAllKeyCode( domEventData ) ) {
wasHandled = this._selectAllNestedEditableContent() || this._selectAllContent();
@@ -356,14 +362,6 @@ function isArrowKeyCode( keyCode ) {
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;
-}
-
// Returns 'true' if provided (DOM) key event data corresponds with the Ctrl+A keystroke.
//
// @param {module:engine/view/observer/keyobserver~KeyEventData} domEventData
diff --git a/tests/widget.js b/tests/widget.js
index 71645f76..e174f650 100644
--- a/tests/widget.js
+++ b/tests/widget.js
@@ -5,6 +5,7 @@
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import Widget from '../src/widget';
+import Delete from '@ckeditor/ckeditor5-typing/src/delete';
import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
import { toWidget } from '../src/utils';
@@ -22,7 +23,7 @@ describe( 'Widget', () => {
let editor, model, doc, viewDocument;
beforeEach( () => {
- return VirtualTestEditor.create( { plugins: [ Widget ] } )
+ return VirtualTestEditor.create( { plugins: [ Widget, Delete ] } )
.then( newEditor => {
editor = newEditor;
model = editor.model;
@@ -245,431 +246,6 @@ describe( 'Widget', () => {
} );
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( model, 'foo[]' );
- viewDocument.on( 'keydown', keydownHandler );
-
- viewDocument.fire( 'keydown', domEventDataMock );
-
- expect( getModelData( model ) ).to.equal( 'foo[]' );
- sinon.assert.calledOnce( domEventDataMock.preventDefault );
- sinon.assert.notCalled( keydownHandler );
- } );
-
- test(
- 'should remove the entire empty element if it is next to a widget',
-
- 'foo' +
- '' +
- '
[]
' +
- 'foo',
-
- keyCodes.backspace,
-
- 'foo[]foo'
- );
-
- test(
- 'should remove the entire empty element (deeper structure) if it is next to a widget',
-
- 'foo' +
- '' +
- '' +
- 'foo',
-
- keyCodes.backspace,
-
- 'foo' +
- '[]' +
- 'foo'
- );
-
- test(
- 'should remove the entire empty element (deeper structure) if it is next to a widget (forward delete)',
-
- 'foo' +
- '' +
- '' +
- 'foo',
-
- keyCodes.delete,
-
- 'foo' +
- '[]' +
- 'foo'
- );
-
- test(
- 'should not remove the entire element which is not empty and the element is next to a widget',
-
- 'foo' +
- '' +
- '[]
' +
- 'foo',
-
- keyCodes.backspace,
-
- 'foo' +
- '[]' +
- '
' +
- 'foo'
- );
-
- test(
- 'should not remove the entire element which is not empty and the element is next to a widget (forward delete)',
-
- 'foo' +
- 'Foo[]
' +
- '' +
- 'foo',
-
- keyCodes.delete,
-
- 'foo' +
- 'Foo
' +
- '[]' +
- 'foo'
- );
-
- test(
- 'should not remove the entire element (deeper structure) which is not empty and the element is next to a widget',
-
- 'foo' +
- '' +
- '' +
- '' +
- '' +
- '
' +
- 'foo',
-
- keyCodes.backspace,
-
- 'foo' +
- '[]' +
- '' +
- '' +
- '
' +
- 'foo'
- );
-
- test(
- 'should do nothing if the nested element is not empty and the element is next to a widget',
-
- 'foo' +
- '' +
- '' +
- '' +
- '
' +
- 'foo',
-
- keyCodes.backspace,
-
- 'foo' +
- '' +
- '' +
- '' +
- '
' +
- 'foo'
- );
-
- it( 'does nothing when editor when read only mode is enabled (delete)', () => {
- setModelData( model,
- 'foo' +
- '' +
- '[]
' +
- 'foo'
- );
-
- editor.isReadOnly = true;
-
- viewDocument.fire( 'keydown', new DomEventData(
- viewDocument,
- { target: document.createElement( 'div' ), preventDefault: () => {} },
- { keyCode: keyCodes.backspace }
- ) );
-
- expect( getModelData( model ) ).to.equal(
- 'foo' +
- '' +
- '[]
' +
- 'foo'
- );
- } );
-
- it( 'does nothing when editor when read only mode is enabled (forward delete)', () => {
- setModelData( model,
- 'foo' +
- '[]
' +
- '' +
- 'foo'
- );
-
- editor.isReadOnly = true;
-
- viewDocument.fire( 'keydown', new DomEventData(
- viewDocument,
- { target: document.createElement( 'div' ), preventDefault: () => {} },
- { keyCode: keyCodes.delete }
- ) );
-
- expect( getModelData( model ) ).to.equal(
- 'foo' +
- '[]
' +
- '' +
- 'foo'
- );
- } );
- } );
-
describe( 'arrows', () => {
test(
'should move selection forward from selected object - right arrow',
@@ -783,6 +359,13 @@ describe( 'Widget', () => {
'[]foo'
);
+ test(
+ 'should do nothing if other key is pressed',
+ '[]foo',
+ keyCodes.a,
+ '[]foo'
+ );
+
it( 'should prevent default behaviour when there is no correct location - document end', () => {
const keydownHandler = sinon.spy();
const domEventDataMock = {
@@ -1121,4 +704,407 @@ describe( 'Widget', () => {
} );
}
} );
+
+ describe( 'delete integration', () => {
+ function test( name, input, direction, expected ) {
+ it( name, () => {
+ setModelData( model, input );
+ const scrollStub = sinon.stub( viewDocument, 'scrollToTheSelection' );
+
+ viewDocument.fire( 'delete', new DomEventData(
+ viewDocument,
+ { target: document.createElement( 'div' ), preventDefault: () => {} },
+ { direction, unit: 'character', sequence: 0 }
+ ) );
+
+ expect( getModelData( model ) ).to.equal( expected );
+ scrollStub.restore();
+ } );
+ }
+
+ test(
+ 'should select widget when backspace is pressed',
+ '[]foo',
+ 'backward',
+ '[]foo'
+ );
+
+ test(
+ 'should remove empty element after selecting widget when backspace is pressed',
+ '[]',
+ 'backward',
+ '[]'
+ );
+
+ test(
+ 'should select widget when delete is pressed',
+ 'foo[]',
+ 'forward',
+ 'foo[]'
+ );
+
+ test(
+ 'should remove empty element after selecting widget when delete is pressed',
+ '[]',
+ 'forward',
+ '[]'
+ );
+
+ test(
+ 'should not select widget on non-collapsed selection',
+ '[f]oo',
+ 'backward',
+ '[]oo'
+ );
+
+ test(
+ 'should not affect non-object elements',
+ 'foo[]bar',
+ 'backward',
+ 'foo[]bar'
+ );
+
+ test(
+ 'should not modify backward delete default behaviour in single paragraph boundaries',
+ '[]foo',
+ 'backward',
+ '[]foo'
+ );
+
+ test(
+ 'should not modify forward delete default behaviour in single paragraph boundaries',
+ 'foo[]',
+ 'forward',
+ 'foo[]'
+ );
+
+ test(
+ 'should delete selected widget with paragraph before - backward',
+ 'foo[]',
+ 'backward',
+ 'foo[]'
+ );
+
+ test(
+ 'should delete selected widget with paragraph before - forward',
+ 'foo[]',
+ 'forward',
+ 'foo[]'
+ );
+
+ test(
+ 'should delete selected widget with paragraph after - backward',
+ '[]foo',
+ 'backward',
+ '[]foo'
+ );
+
+ test(
+ 'should delete selected widget with paragraph after - forward',
+ '[]foo',
+ 'forward',
+ '[]foo'
+ );
+
+ test(
+ 'should delete selected widget between paragraphs - backward',
+ 'bar[]foo',
+ 'backward',
+ 'bar[]foo'
+ );
+
+ test(
+ 'should delete selected widget between paragraphs - forward',
+ 'bar[]foo',
+ 'forward',
+ 'bar[]foo'
+ );
+
+ test(
+ 'should delete selected widget preceded by another widget - backward',
+ '[]',
+ 'backward',
+ '[]'
+ );
+
+ test(
+ 'should delete selected widget preceded by another widget - forward',
+ '[]',
+ 'forward',
+ '[]'
+ );
+
+ test(
+ 'should delete selected widget before another widget - forward',
+ '[]',
+ 'forward',
+ '[]'
+ );
+
+ test(
+ 'should delete selected widget before another widget - backward',
+ '[]',
+ 'backward',
+ '[]'
+ );
+
+ test(
+ 'should delete selected widget between other widgets - forward',
+ '[]',
+ 'forward',
+ '[]'
+ );
+
+ test(
+ 'should delete selected widget between other widgets - backward',
+ '[]',
+ 'backward',
+ '[]'
+ );
+
+ test(
+ 'should select inline objects - backward',
+ 'foo[]bar',
+ 'backward',
+ 'foo[]bar'
+ );
+
+ test(
+ 'should select inline objects - forward',
+ 'foo[]bar',
+ 'forward',
+ 'foo[]bar'
+ );
+
+ test(
+ 'should delete selected inline objects - backward',
+ 'foo[]bar',
+ 'backward',
+ 'foo[]bar'
+ );
+
+ test(
+ 'should delete selected inline objects - forward',
+ 'foo[]bar',
+ 'forward',
+ 'foo[]bar'
+ );
+
+ test(
+ 'should use standard delete behaviour when after first letter - backward',
+ 'a[]',
+ 'backward',
+ '[]'
+ );
+
+ test(
+ 'should use standard delete behaviour when before first letter - forward',
+ '[]a',
+ 'forward',
+ '[]'
+ );
+
+ it( 'should prevent default behaviour and stop event propagation', () => {
+ setModelData( model, 'foo[]' );
+ const scrollStub = sinon.stub( viewDocument, 'scrollToTheSelection' );
+ const deleteSpy = sinon.spy();
+
+ viewDocument.on( 'delete', deleteSpy );
+ const domEventDataMock = { target: document.createElement( 'div' ), preventDefault: sinon.spy() };
+
+ viewDocument.fire( 'delete', new DomEventData(
+ viewDocument,
+ domEventDataMock,
+ { direction: 'forward', unit: 'character', sequence: 0 }
+ ) );
+
+ sinon.assert.calledOnce( domEventDataMock.preventDefault );
+ sinon.assert.notCalled( deleteSpy );
+ scrollStub.restore();
+ } );
+
+ test(
+ 'should remove the entire empty element if it is next to a widget',
+
+ 'foo' +
+ '' +
+ '[]
' +
+ 'foo',
+
+ 'backward',
+
+ 'foo[]foo'
+ );
+
+ test(
+ 'should remove the entire empty element (deeper structure) if it is next to a widget',
+
+ 'foo' +
+ '' +
+ '' +
+ 'foo',
+
+ 'backward',
+
+ 'foo' +
+ '[]' +
+ 'foo'
+ );
+
+ test(
+ 'should remove the entire empty element (deeper structure) if it is next to a widget (forward delete)',
+
+ 'foo' +
+ '' +
+ '' +
+ 'foo',
+
+ 'forward',
+
+ 'foo' +
+ '[]' +
+ 'foo'
+ );
+
+ test(
+ 'should not remove the entire element which is not empty and the element is next to a widget',
+
+ 'foo' +
+ '' +
+ '[]
' +
+ 'foo',
+
+ 'backward',
+
+ 'foo' +
+ '[]' +
+ '
' +
+ 'foo'
+ );
+
+ test(
+ 'should not remove the entire element which is not empty and the element is next to a widget (forward delete)',
+
+ 'foo' +
+ 'Foo[]
' +
+ '' +
+ 'foo',
+
+ 'forward',
+
+ 'foo' +
+ 'Foo
' +
+ '[]' +
+ 'foo'
+ );
+
+ test(
+ 'should not remove the entire element (deeper structure) which is not empty and the element is next to a widget',
+
+ 'foo' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ 'foo',
+
+ 'backward',
+
+ 'foo' +
+ '[]' +
+ '' +
+ '' +
+ '
' +
+ 'foo'
+ );
+
+ test(
+ 'should do nothing if the nested element is not empty and the element is next to a widget',
+
+ 'foo' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ 'foo',
+
+ 'backward',
+
+ 'foo' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ 'foo'
+ );
+
+ it( 'does nothing when editor when read only mode is enabled (delete)', () => {
+ const scrollStub = sinon.stub( viewDocument, 'scrollToTheSelection' );
+ setModelData( model,
+ 'foo' +
+ '' +
+ '[]
' +
+ 'foo'
+ );
+
+ editor.isReadOnly = true;
+
+ const domEventDataMock = { target: document.createElement( 'div' ), preventDefault: sinon.spy() };
+
+ viewDocument.fire( 'delete', new DomEventData(
+ viewDocument,
+ domEventDataMock,
+ { direction: 'backward', unit: 'character', sequence: 0 }
+ ) );
+
+ expect( getModelData( model ) ).to.equal(
+ 'foo' +
+ '' +
+ '[]
' +
+ 'foo'
+ );
+ scrollStub.restore();
+ } );
+
+ it( 'does nothing when editor when read only mode is enabled (forward delete)', () => {
+ const scrollStub = sinon.stub( viewDocument, 'scrollToTheSelection' );
+ setModelData( model,
+ 'foo' +
+ '' +
+ '[]
' +
+ 'foo'
+ );
+
+ editor.isReadOnly = true;
+
+ const domEventDataMock = { target: document.createElement( 'div' ), preventDefault: sinon.spy() };
+
+ viewDocument.fire( 'delete', new DomEventData(
+ viewDocument,
+ domEventDataMock,
+ { direction: 'forward', unit: 'character', sequence: 0 }
+ ) );
+
+ expect( getModelData( model ) ).to.equal(
+ 'foo' +
+ '' +
+ '[]
' +
+ 'foo'
+ );
+ scrollStub.restore();
+ } );
+ } );
} );