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

Arrow keys handling #17

Merged
merged 9 commits into from
Dec 14, 2016
146 changes: 129 additions & 17 deletions src/widget/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand All @@ -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 = getSelectedElement( modelSelection );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use the new engine function


// if object element is selected.
if ( objectElement && schema.objects.has( objectElement.name ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code has nothing to do with widgets. This is interesting, cause it means that we can move all that to the engine easily. That can be a good option. Perhaps it will be easily doable to have basic widget constructs in the engine. However, as a what? Addition to the editing controller? Maaaybe. But it's so much unnecessary code that I don't know ;|.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a valid point. We can make a discussion about it when we will extracting widgets functionalities from image plugin.

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;
}
}

Expand All @@ -140,4 +187,69 @@ 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 the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
// one range in the selection, and that range contains exactly one element.
// Returns `null` if there is no selected element.
//
// @param {module:engine/model/selection~Selection} modelSelection
// @returns {module:engine/model/element~Element|null}
function getSelectedElement( modelSelection ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering whether this should stay here or be moved as model's selection method. We have similar one in view's selection.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense moving it there. But it needs to have the same semantics like the method in the view. Are they identical?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they have same semantics. I will create an issue in ckeditor5-engine.

if ( modelSelection.rangeCount !== 1 ) {
return null;
}

const range = modelSelection.getFirstRange();
const nodeAfterStart = range.start.nodeAfter;
const nodeBeforeEnd = range.end.nodeBefore;

return ( nodeAfterStart instanceof ModelElement && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : 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;
}
9 changes: 9 additions & 0 deletions tests/manual/image.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
</head>

<div id="editor">
<figure class="image">
<img src="logo.png" alt="" />
</figure>
<figure class="image">
<img src="logo.png" alt="" />
</figure>
<p>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. </p>
<figure class="image">
<img src="logo.png" alt="CKEditor logo" />
Expand All @@ -11,4 +17,7 @@
<figure class="image">
<img src="logo.png" alt="" />
</figure>
<figure class="image">
<img src="logo.png" alt="" />
</figure>
</div>
7 changes: 7 additions & 0 deletions tests/manual/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@
* Place selection at the beginning of the second paragraph and press <kbd>backspace</kbd> (<kbd>delete</kbd> on Mac) - image should be selected. Second press should delete it.
* Place selection in an empty paragraph before image and press <kbd>delete</kbd> (<kbd>forward delete</kbd> on Mac) - image should be selected and paragraph removed.
* Place selection in an empty paragraph after image and press <kbd>backspace</kbd> (<kbd>delete</kbd> on Mac) - image should be selected and paragraph removed.

### Arrow key handling

* Click first image and press <kbd>right arrow</kbd> - second image should be selected. Same effect should happen for <kbd>down arrow</kbd>.
* Click second image and press <kbd>left arrow</kbd> - first image should be selected. Same effect should happen for <kbd>up arrow</kbd>.
* Place selection at the end of the first paragraph and press <kbd>right arrow</kbd> - third image should be selected. Same effect should happen for <kbd>down arrow</kbd>.
* Place selection at the beginning of the first paragraph and press <kbd>left arrow</kbd> - first image should be selected. Same effect should happen for <kbd>up arrow</kbd>.
Loading