Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 150 additions & 48 deletions editor/modes/visual-editor/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import classnames from 'classnames';
import { Slot } from 'react-slot-fill';
import { partial } from 'lodash';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import tinymce from 'tinymce';

/**
* WordPress dependencies
Expand Down Expand Up @@ -58,6 +59,89 @@ function FirstChild( { children } ) {
return childrenArray[ 0 ] || null;
}

function isVerticalEdge( { editor, reverse } ) {
const rangeRect = editor.selection.getBoundingClientRect();
const buffer = rangeRect.height / 2;
const editableRect = editor.getBody().getBoundingClientRect();

isVerticalEdge.firstRect = isVerticalEdge.firstRect || rangeRect;

// Too low.
if ( reverse && rangeRect.top - buffer > editableRect.top ) {
return false;
}

// Too high.
if ( ! reverse && rangeRect.bottom + buffer < editableRect.bottom ) {
return false;
}

return true;
}

function isHorizontalEdge( { editor, reverse } ) {
const position = reverse ? 'start' : 'end';
const order = reverse ? 'first' : 'last';
const range = editor.selection.getRng();
const offset = range[ `${ position }Offset` ];
const rootNode = editor.getBody();
let node = range.startContainer;

if ( ! range.collapsed ) {
return false;
}

if ( reverse && offset !== 0 ) {
return false;
}

if ( ! reverse && offset !== node.data.length ) {
return false;
}

while ( node !== rootNode ) {
const parentNode = node.parentNode;

if ( parentNode[ `${ order }Child` ] !== node ) {
return false;
}

node = parentNode;
}

return true;
}

function getNextEditor( { node, target, reverse } ) {
const selector = '.editor-visual-editor [contenteditable="true"]';
const editableNodes = Array.from( document.querySelectorAll( selector ) );

if ( reverse ) {
editableNodes.reverse();
}

const index = editableNodes.indexOf( target );
const nextEditableNode = editableNodes[ index + 1 ];
const direction = reverse ? 'previous' : 'next';
const nextBlockNode = node[ `${ direction }Sibling` ];

if ( ! nextBlockNode || ! nextEditableNode ) {
return;
}

const editor = tinymce.get( nextEditableNode.id );

if ( ! editor ) {
return;
}

if ( ! node.contains( nextEditableNode ) && ! nextBlockNode.contains( nextEditableNode ) ) {
return;
}

return editor;
}

class VisualEditorBlock extends Component {
constructor() {
super( ...arguments );
Expand All @@ -73,7 +157,6 @@ class VisualEditorBlock extends Component {
this.onPointerDown = this.onPointerDown.bind( this );
this.onKeyDown = this.onKeyDown.bind( this );
this.onKeyUp = this.onKeyUp.bind( this );
this.handleArrowKey = this.handleArrowKey.bind( this );
this.toggleMobileControls = this.toggleMobileControls.bind( this );
this.onBlockError = this.onBlockError.bind( this );

Expand Down Expand Up @@ -240,6 +323,9 @@ class VisualEditorBlock extends Component {
}

onPointerDown( event ) {
// Discard the arrow key position.
delete isVerticalEdge.firstRect;

// Not the main button (usually the left button on pointer device).
if ( event.buttons !== 1 ) {
return;
Expand All @@ -251,12 +337,71 @@ class VisualEditorBlock extends Component {

onKeyDown( event ) {
const { keyCode, target } = event;
const { previousBlock, nextBlock, onFocus } = this.props;
const up = keyCode === UP;
const down = keyCode === DOWN;
const left = keyCode === LEFT;
const right = keyCode === RIGHT;

if ( up || down || left || right ) {
const editor = target.id && tinymce.get( target.id );

if ( target === this.node ) {
const reverse = up || left;
const followingBlock = reverse ? previousBlock : nextBlock;

if ( followingBlock ) {
event.preventDefault();
onFocus( followingBlock.uid, { offset: reverse ? -1 : 0 } );
}
}

if ( editor ) {
const reverse = up || left;
const horizontal = left || right;
const followingBlock = reverse ? previousBlock : nextBlock;
const isEdge = horizontal ? isHorizontalEdge : isVerticalEdge;

if ( ! isEdge( { editor, reverse } ) ) {
return;
}

event.preventDefault();

const followingEditor = getNextEditor( { node: this.node, target, reverse } );

if ( ! followingEditor ) {
if ( followingBlock ) {
onFocus( followingBlock.uid, { offset: reverse ? -1 : 0 } );
}

return;
}

followingEditor.focus();

if ( horizontal ) {
if ( reverse ) {
followingEditor.selection.select( followingEditor.getBody(), true );
followingEditor.selection.collapse( false );
}
} else {
const rect = isVerticalEdge.firstRect;

this.handleArrowKey( event );
window.setTimeout( () => {
const buffer = rect.height / 2;
const editorRect = followingEditor.getBody().getBoundingClientRect();
const y = reverse ? editorRect.bottom - buffer : editorRect.top + buffer;

if ( keyCode === UP || keyCode === LEFT || keyCode === DOWN || keyCode === RIGHT ) {
const selection = window.getSelection();
this.lastRange = selection.rangeCount ? selection.getRangeAt( 0 ) : null;
followingEditor.selection.placeCaretAt( rect.left, y );
} );
}
}
}

if ( ! up && ! down ) {
// Discard the arrow key position if any other key is pressed.
delete isVerticalEdge.firstRect;
}

if ( ENTER === keyCode && target === this.node ) {
Expand All @@ -270,49 +415,6 @@ class VisualEditorBlock extends Component {

onKeyUp( event ) {
this.removeOrDeselect( event );
this.handleArrowKey( event );
}

handleArrowKey( event ) {
const { keyCode, target } = event;
const moveUp = ( keyCode === UP || keyCode === LEFT );
const moveDown = ( keyCode === DOWN || keyCode === RIGHT );
const wrapperClassname = '.editor-visual-editor';
const selectors = [
'*[contenteditable="true"]',
'*[tabindex]',
'textarea',
'input',
].map( ( selector ) => `${ wrapperClassname } ${ selector }` ).join( ',' );

if ( moveUp || moveDown ) {
const selection = window.getSelection();
const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;

// If there's no movement, so we're either at the end of start, or
// no text input at all.
if ( range !== this.lastRange ) {
return;
}

const focusableNodes = Array.from( document.querySelectorAll( selectors ) );

if ( moveUp ) {
focusableNodes.reverse();
}

const targetNode = focusableNodes
.slice( focusableNodes.indexOf( target ) )
.reduce( ( result, node ) => {
return result || ( node.contains( target ) ? null : node );
}, null );

if ( targetNode ) {
targetNode.focus();
}
}

delete this.lastRange;
}

toggleMobileControls() {
Expand Down