Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

I/5826: Add a helper to handle custom Ctrl+A behaviour #16

Merged
merged 7 commits into from
Jan 13, 2020
30 changes: 29 additions & 1 deletion src/restrictededitingmodeediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default class RestrictedEditingModeEditing extends Plugin {
editor.commands.add( 'goToNextRestrictedEditingException', new RestrictedEditingNavigationCommand( editor, 'forward' ) );
editor.keystrokes.set( 'Tab', getCommandExecuter( editor, 'goToNextRestrictedEditingException' ) );
editor.keystrokes.set( 'Shift+Tab', getCommandExecuter( editor, 'goToPreviousRestrictedEditingException' ) );
panr marked this conversation as resolved.
Show resolved Hide resolved
editor.keystrokes.set( 'Ctrl+A', getSelectAllHandler( editor ) );

editingView.change( writer => {
for ( const root of editingView.document.roots ) {
Expand Down Expand Up @@ -308,6 +309,33 @@ function getCommandExecuter( editor, commandName ) {
};
}

// Helper for handling Ctrl+A keydown behaviour.
function getSelectAllHandler( editor ) {
return ( data, cancel ) => {
const model = editor.model;
const selection = editor.model.document.selection;
const marker = getMarkerAtPosition( editor, selection.focus );

if ( !marker ) {
return;
}

// If selection range is inside a restricted editing exception, select text only within the exception.
//
// Note: Second Ctrl+A press is also blocked and it won't select the entire text in the editor.
const selectionRange = selection.getFirstRange();
const markerRange = marker.getRange();

if ( markerRange.containsRange( selectionRange, true ) || selection.isCollapsed ) {
cancel();

model.change( writer => {
writer.setSelection( marker.getRange() );
} );
}
};
}

// Additional filtering rule for enabling "delete" and "forwardDelete" commands if selection is on range boundaries:
//
// Does not allow to enable command when selection focus is:
Expand All @@ -319,7 +347,7 @@ function filterDeleteCommandsOnMarkerBoundaries( selection, markerRange ) {
return false;
}

// Only for collapsed selection - non-collapsed seleciton that extends over a marker is handled elsewhere.
// Only for collapsed selection - non-collapsed selection that extends over a marker is handled elsewhere.
if ( name == 'forwardDelete' && selection.isCollapsed && markerRange.end.isEqual( selection.focus ) ) {
return false;
}
Expand Down
182 changes: 182 additions & 0 deletions tests/restrictededitingmodeediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,188 @@ describe( 'RestrictedEditingModeEditing', () => {
} );
} );

describe( 'custom keydown behaviour', () => {
let view, evtData;

beforeEach( async () => {
editor = await VirtualTestEditor.create( {
plugins: [ Paragraph, RestrictedEditingModeEditing, BoldEditing ]
} );

model = editor.model;
view = editor.editing.view;
} );

afterEach( () => {
return editor.destroy();
} );

describe( 'Ctrl+A handler', () => {
beforeEach( async () => {
evtData = {
keyCode: getCode( 'A' ),
ctrlKey: true,
preventDefault: sinon.spy(),
stopPropagation: sinon.spy()
};
} );

describe( 'collapsed selection', () => {
it( 'should select text only within an exception when selection is inside an exception', () => {
setModelData( model, '<paragraph>foo ba[]r baz</paragraph>' );

const paragraph = model.document.getRoot().getChild( 0 );

// <paragraph>foo <marker>ba[]r</marker> baz</paragraph>
addExceptionMarker( 4, 7, paragraph );

view.document.fire( 'keydown', evtData );

sinon.assert.calledOnce( evtData.preventDefault );
sinon.assert.calledOnce( evtData.stopPropagation );
expect( getModelData( model ) ).to.be.equal( '<paragraph>foo [bar] baz</paragraph>' );
} );

it( 'should select text only within an exception when selection is at the begining of an exception', () => {
setModelData( model, '<paragraph>foo []bar baz</paragraph>' );

const paragraph = model.document.getRoot().getChild( 0 );

// <paragraph>foo <marker>[]bar</marker> baz</paragraph>
addExceptionMarker( 4, 7, paragraph );

view.document.fire( 'keydown', evtData );

sinon.assert.calledOnce( evtData.preventDefault );
sinon.assert.calledOnce( evtData.stopPropagation );
expect( getModelData( model ) ).to.be.equal( '<paragraph>foo [bar] baz</paragraph>' );
} );

it( 'should select text only within an exception when selection is at the end of an exception', () => {
setModelData( model, '<paragraph>foo bar[] baz</paragraph>' );

const paragraph = model.document.getRoot().getChild( 0 );

// <paragraph>foo <marker>bar[]</marker> baz</paragraph>
addExceptionMarker( 4, 7, paragraph );

view.document.fire( 'keydown', evtData );

sinon.assert.calledOnce( evtData.preventDefault );
sinon.assert.calledOnce( evtData.stopPropagation );
expect( getModelData( model ) ).to.be.equal( '<paragraph>foo [bar] baz</paragraph>' );
} );

it( 'should not change the selection if the caret is not inside an exception', () => {
setModelData( model, '<paragraph>foo ba[]r baz</paragraph>' );

// no markers
// <paragraph>foo ba[]r baz</paragraph>

view.document.fire( 'keydown', evtData );

sinon.assert.notCalled( evtData.preventDefault );
sinon.assert.notCalled( evtData.stopPropagation );
expect( getModelData( model ) ).to.be.equal( '<paragraph>foo ba[]r baz</paragraph>' );
} );

it( 'should not extend the selection outside an exception when press Ctrl+A second time', () => {
setModelData( model, '<paragraph>foo b[]ar baz</paragraph>' );

const paragraph = model.document.getRoot().getChild( 0 );

// <paragraph>foo <marker>b[]ar</marker> baz</paragraph>
addExceptionMarker( 4, 7, paragraph );

view.document.fire( 'keydown', evtData );
view.document.fire( 'keydown', evtData );

sinon.assert.calledTwice( evtData.preventDefault );
sinon.assert.calledTwice( evtData.stopPropagation );
expect( getModelData( model ) ).to.be.equal( '<paragraph>foo [bar] baz</paragraph>' );
} );
} );

describe( 'non-collapsed selection', () => {
it( 'should select text within an exception when a whole selection range is inside an exception', () => {
setModelData( model, '<paragraph>fo[o ba]r baz</paragraph>' );

const paragraph = model.document.getRoot().getChild( 0 );

// <paragraph><marker>fo[o ba]r</marker> baz</paragraph>
panr marked this conversation as resolved.
Show resolved Hide resolved
addExceptionMarker( 0, 7, paragraph );

view.document.fire( 'keydown', evtData );

sinon.assert.calledOnce( evtData.preventDefault );
sinon.assert.calledOnce( evtData.stopPropagation );
expect( getModelData( model ) ).to.be.equal( '<paragraph>[foo bar] baz</paragraph>' );
} );

it( 'should select text within an exception when end of selection range is equal exception end', () => {
setModelData( model, '<paragraph>foo b[ar] baz</paragraph>' );

const paragraph = model.document.getRoot().getChild( 0 );

// <paragraph>foo <marker>b[ar]</marker> baz</paragraph>
addExceptionMarker( 4, 7, paragraph );

view.document.fire( 'keydown', evtData );

sinon.assert.calledOnce( evtData.preventDefault );
sinon.assert.calledOnce( evtData.stopPropagation );
expect( getModelData( model ) ).to.be.equal( '<paragraph>foo [bar] baz</paragraph>' );
} );

it( 'should select text within an exception when start of selection range is equal exception start', () => {
setModelData( model, '<paragraph>foo [ba]r baz</paragraph>' );

const paragraph = model.document.getRoot().getChild( 0 );

// <paragraph>foo <marker>[ba]r</marker> baz</paragraph>
addExceptionMarker( 4, 7, paragraph );

view.document.fire( 'keydown', evtData );

sinon.assert.calledOnce( evtData.preventDefault );
sinon.assert.calledOnce( evtData.stopPropagation );
expect( getModelData( model ) ).to.be.equal( '<paragraph>foo [bar] baz</paragraph>' );
} );

it( 'should not select text within an exception when a part of the selection range is outside an exception', () => {
setModelData( model, '<paragraph>fo[o ba]r baz</paragraph>' );

const paragraph = model.document.getRoot().getChild( 0 );

// <paragraph>fo[o <marker>ba]r</marker> baz</paragraph>
addExceptionMarker( 4, 7, paragraph );

view.document.fire( 'keydown', evtData );

sinon.assert.notCalled( evtData.preventDefault );
sinon.assert.notCalled( evtData.stopPropagation );
expect( getModelData( model ) ).to.be.equal( '<paragraph>fo[o ba]r baz</paragraph>' );
} );

it( 'should not extend the selection outside an exception when press Ctrl+A second time', () => {
setModelData( model, '<paragraph>foo [bar] baz</paragraph>' );

const paragraph = model.document.getRoot().getChild( 0 );

// <paragraph>foo <marker>[bar]</marker> baz</paragraph>
addExceptionMarker( 4, 7, paragraph );

view.document.fire( 'keydown', evtData );
view.document.fire( 'keydown', evtData );

sinon.assert.calledTwice( evtData.preventDefault );
sinon.assert.calledTwice( evtData.stopPropagation );
expect( getModelData( model ) ).to.be.equal( '<paragraph>foo [bar] baz</paragraph>' );
} );
} );
} );
} );

// Helper method that creates an exception marker inside given parent.
// Marker range is set to given position offsets (start, end).
function addExceptionMarker( startOffset, endOffset = startOffset, parent, id = 1 ) {
Expand Down