Skip to content

Commit

Permalink
Code refactoring: Split the Widget keydown listener into two separate…
Browse files Browse the repository at this point in the history
… listeners for more flexibility when integrating with other features (TableKeyboard, WidgetTypeAround).
  • Loading branch information
oleq committed Jun 10, 2020
1 parent 9717d21 commit 73c2272
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 87 deletions.
36 changes: 1 addition & 35 deletions packages/ckeditor5-table/src/tablekeyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default class TableKeyboard extends Plugin {
// (like Widgets), which take over the keydown events with the "high" priority. Table navigation takes precedence
// over Widgets in that matter (widget arrow handler stops propagation of event if object element was selected
// but getNearestSelectionRange didn't returned any range).
this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: priorities.get( 'high' ) + 1 } );
this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: priorities.get( 'high' ) - 10 } );
}

/**
Expand Down Expand Up @@ -230,19 +230,6 @@ export default class TableKeyboard extends Plugin {
return true;
}

// If this is an object selected and it's not at the start or the end of cell content
// then let's allow widget handler to take care of it.
const objectElement = selection.getSelectedElement();

if ( objectElement && model.schema.isObject( objectElement ) ) {
return false;
}

// If next to the selection there is an object then this is not the cell boundary (widget handler should handle this).
if ( this._isObjectElementNextToSelection( selection, isForward ) ) {
return false;
}

// If there isn't any $text position between cell edge and selection then we shall move the selection to next cell.
const textRange = this._findTextRangeFromSelection( cellRange, selection, isForward );

Expand Down Expand Up @@ -307,27 +294,6 @@ export default class TableKeyboard extends Plugin {
return focus.isEqual( probe.focus );
}

/**
* Checks if there is an {@link module:engine/model/element~Element element} next to the current
* {@link module:engine/model/selection~Selection model selection} marked in the
* {@link module:engine/model/schema~Schema schema} as an `object`.
*
* @private
* @param {module:engine/model/selection~Selection} modelSelection The selection.
* @param {Boolean} isForward The direction of checking.
* @returns {Boolean}
*/
_isObjectElementNextToSelection( modelSelection, isForward ) {
const model = this.editor.model;
const schema = model.schema;

const probe = model.createSelection( modelSelection );
model.modifySelection( probe, { direction: isForward ? 'forward' : 'backward' } );
const objectElement = isForward ? probe.focus.nodeBefore : probe.focus.nodeAfter;

return objectElement && schema.isObject( objectElement );
}

/**
* Truncates the range so that it spans from the last selection position
* to the last allowed `$text` position (mirrored if `isForward` is false).
Expand Down
145 changes: 93 additions & 52 deletions packages/ckeditor5-widget/src/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import env from '@ckeditor/ckeditor5-utils/src/env';

import '../theme/widget.css';
import priorities from '@ckeditor/ckeditor5-utils/src/priorities';

/**
* The widget plugin. It enables base support for widgets.
Expand Down Expand Up @@ -99,8 +100,24 @@ export default class Widget extends Plugin {
view.addObserver( MouseObserver );
this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) );

// Handle custom keydown behaviour.
this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: 'high' } );
// There are two keydown listeners working on different priorities. This allows other
// features such as WidgetTypeAround or TableKeyboard to attach their listeners in between
// and customize the behavior even further in different content/selection scenarios.
//
// * The first listener handles changing the selection on arrow key press
// if the widget is selected or if the selection is next to a widget and the widget
// should become selected upon the arrow key press.
//
// * The second (late) listener makes sure the default browser action on arrow key press is
// prevented when a widget is selected. This prevents the selection from being moved
// from a fake selection container.
this.listenTo( viewDocument, 'keydown', ( ...args ) => {
this._handleSelectionChangeOnArrowKeyPress( ...args );
}, { priority: 'high' } );

this.listenTo( viewDocument, 'keydown', ( ...args ) => {
this._preventDefaultOnArrowKeyPress( ...args );
}, { priority: priorities.get( 'high' ) - 20 } );

// Handle custom delete behaviour.
this.listenTo( viewDocument, 'delete', ( evt, data ) => {
Expand Down Expand Up @@ -165,25 +182,92 @@ export default class Widget extends Plugin {
}

/**
* Handles {@link module:engine/view/document~Document#event:keydown keydown} events.
* Handles {@link module:engine/view/document~Document#event:keydown keydown} events and changes
* the model selection when:
*
* * arrow key is pressed when the widget is selected,
* * the selection is next to a widget and the widget should become selected upon the arrow key press.
*
* See {@link #_preventDefaultOnArrowKeyPress}.
*
* @private
* @param {module:utils/eventinfo~EventInfo} eventInfo
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
*/
_onKeydown( eventInfo, domEventData ) {
_handleSelectionChangeOnArrowKeyPress( eventInfo, domEventData ) {
const keyCode = domEventData.keyCode;
let wasHandled = false;

// Checks if the keys were handled and then prevents the default event behaviour and stops
// the propagation.
if ( isArrowKeyCode( keyCode ) ) {
const isForward = isForwardArrowKeyCode( keyCode, this.editor.locale.contentLanguageDirection );
if ( !isArrowKeyCode( keyCode ) ) {
return;
}

const model = this.editor.model;
const schema = model.schema;
const modelSelection = model.document.selection;
const objectElement = modelSelection.getSelectedElement();
const isForward = isForwardArrowKeyCode( keyCode, this.editor.locale.contentLanguageDirection );

// If object element is selected.
if ( objectElement && schema.isObject( objectElement ) ) {
const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
const newRange = schema.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' );

if ( newRange ) {
model.change( writer => {
writer.setSelection( newRange );
} );

domEventData.preventDefault();
eventInfo.stop();

return;
}
}

// If selection is next to object element.
// Return if not collapsed.
if ( !modelSelection.isCollapsed ) {
return;
}

const objectElement2 = this._getObjectElementNextToSelection( isForward );

if ( !!objectElement2 && schema.isObject( objectElement2 ) ) {
this._setSelectionOverElement( objectElement2 );

domEventData.preventDefault();
eventInfo.stop();
}
}

/**
* Handles {@link module:engine/view/document~Document#event:keydown keydown} events and prevents
* the default browser behavior to make sure the fake selection is not being moved from a fake selection
* container.
*
* See {@link #_handleSelectionChangeOnArrowKeyPress}.
*
* @private
* @param {module:utils/eventinfo~EventInfo} eventInfo
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
*/
_preventDefaultOnArrowKeyPress( eventInfo, domEventData ) {
const keyCode = domEventData.keyCode;

wasHandled = this._handleArrowKeys( isForward );
// Checks if the keys were handled and then prevents the default event behaviour and stops
// the propagation.
if ( !isArrowKeyCode( keyCode ) ) {
return;
}

if ( wasHandled ) {
const model = this.editor.model;
const schema = model.schema;
const objectElement = model.document.selection.getSelectedElement();

// If object element is selected.
if ( objectElement && schema.isObject( objectElement ) ) {
domEventData.preventDefault();
eventInfo.stop();
}
Expand Down Expand Up @@ -231,49 +315,6 @@ export default class Widget extends Plugin {
}
}

/**
* Handles arrow keys.
*
* @private
* @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 model = this.editor.model;
const schema = model.schema;
const modelDocument = model.document;
const modelSelection = modelDocument.selection;
const objectElement = modelSelection.getSelectedElement();

// If object element is selected.
if ( objectElement && schema.isObject( objectElement ) ) {
const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
const newRange = schema.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' );

if ( newRange ) {
model.change( writer => {
writer.setSelection( 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 && schema.isObject( objectElement2 ) ) {
this._setSelectionOverElement( objectElement2 );

return true;
}
}

/**
* Sets {@link module:engine/model/selection~Selection document's selection} over given element.
*
Expand Down

0 comments on commit 73c2272

Please sign in to comment.