Skip to content

Commit

Permalink
Rich text: move format boundaries to ref callback and separate file (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed May 3, 2021
1 parent 74d6b30 commit 1390bd7
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 126 deletions.
129 changes: 3 additions & 126 deletions packages/rich-text/src/component/index.js
Expand Up @@ -9,15 +9,7 @@ import {
useMemo,
useLayoutEffect,
} from '@wordpress/element';
import {
BACKSPACE,
DELETE,
ENTER,
LEFT,
RIGHT,
SPACE,
ESCAPE,
} from '@wordpress/keycodes';
import { BACKSPACE, DELETE, ENTER, SPACE, ESCAPE } from '@wordpress/keycodes';
import { getFilesFromDataTransfer } from '@wordpress/dom';
import { useMergeRefs } from '@wordpress/compose';

Expand All @@ -42,6 +34,7 @@ import { useBoundaryStyle } from './use-boundary-style';
import { useInlineWarning } from './use-inline-warning';
import { insert } from '../insert';
import { useCopyHandler } from './use-copy-handler';
import { useFormatBoundaries } from './use-format-boundaries';

/** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */

Expand Down Expand Up @@ -537,122 +530,6 @@ function RichText(
event.preventDefault();
}

/**
* Handles horizontal keyboard navigation when no modifiers are pressed. The
* navigation is handled separately to move correctly around format
* boundaries.
*
* @param {WPSyntheticEvent} event A synthetic keyboard event.
*/
function handleHorizontalNavigation( event ) {
const { keyCode, shiftKey, altKey, metaKey, ctrlKey } = event;

if (
// Only override left and right keys without modifiers pressed.
shiftKey ||
altKey ||
metaKey ||
ctrlKey ||
( keyCode !== LEFT && keyCode !== RIGHT )
) {
return;
}

const {
text,
formats,
start,
end,
activeFormats: currentActiveFormats = [],
} = record.current;
const collapsed = isCollapsed( record.current );
// To do: ideally, we should look at visual position instead.
const { direction } = getWin().getComputedStyle( ref.current );
const reverseKey = direction === 'rtl' ? RIGHT : LEFT;
const isReverse = event.keyCode === reverseKey;

// If the selection is collapsed and at the very start, do nothing if
// navigating backward.
// If the selection is collapsed and at the very end, do nothing if
// navigating forward.
if ( collapsed && currentActiveFormats.length === 0 ) {
if ( start === 0 && isReverse ) {
return;
}

if ( end === text.length && ! isReverse ) {
return;
}
}

// If the selection is not collapsed, let the browser handle collapsing
// the selection for now. Later we could expand this logic to set
// boundary positions if needed.
if ( ! collapsed ) {
return;
}

const formatsBefore = formats[ start - 1 ] || EMPTY_ACTIVE_FORMATS;
const formatsAfter = formats[ start ] || EMPTY_ACTIVE_FORMATS;

let newActiveFormatsLength = currentActiveFormats.length;
let source = formatsAfter;

if ( formatsBefore.length > formatsAfter.length ) {
source = formatsBefore;
}

// If the amount of formats before the caret and after the caret is
// different, the caret is at a format boundary.
if ( formatsBefore.length < formatsAfter.length ) {
if (
! isReverse &&
currentActiveFormats.length < formatsAfter.length
) {
newActiveFormatsLength++;
}

if (
isReverse &&
currentActiveFormats.length > formatsBefore.length
) {
newActiveFormatsLength--;
}
} else if ( formatsBefore.length > formatsAfter.length ) {
if (
! isReverse &&
currentActiveFormats.length > formatsAfter.length
) {
newActiveFormatsLength--;
}

if (
isReverse &&
currentActiveFormats.length < formatsBefore.length
) {
newActiveFormatsLength++;
}
}

if ( newActiveFormatsLength === currentActiveFormats.length ) {
record.current._newActiveFormats = isReverse
? formatsBefore
: formatsAfter;
return;
}

event.preventDefault();

const newActiveFormats = source.slice( 0, newActiveFormatsLength );
const newValue = {
...record.current,
activeFormats: newActiveFormats,
};
record.current = newValue;
applyRecord( newValue );
setActiveFormats( newActiveFormats );
}

function handleKeyDown( event ) {
if ( event.defaultPrevented ) {
return;
Expand All @@ -661,7 +538,6 @@ function RichText(
handleDelete( event );
handleEnter( event );
handleSpace( event );
handleHorizontalNavigation( event );
}

const lastHistoryValue = useRef( value );
Expand Down Expand Up @@ -1064,6 +940,7 @@ function RichText(
forwardedRef,
ref,
useCopyHandler( { record, multilineTag, preserveWhiteSpace } ),
useFormatBoundaries( { record, applyRecord, setActiveFormats } ),
] ),
style: defaultStyle,
className: 'rich-text',
Expand Down
144 changes: 144 additions & 0 deletions packages/rich-text/src/component/use-format-boundaries.js
@@ -0,0 +1,144 @@
/**
* WordPress dependencies
*/
import { useRefEffect } from '@wordpress/compose';

import { LEFT, RIGHT } from '@wordpress/keycodes';

/**
* Internal dependencies
*/
import { isCollapsed } from '../is-collapsed';

const EMPTY_ACTIVE_FORMATS = [];

export function useFormatBoundaries( {
record,
applyRecord,
setActiveFormats,
} ) {
return useRefEffect(
( element ) => {
function onKeyDown( event ) {
const { keyCode, shiftKey, altKey, metaKey, ctrlKey } = event;

if (
// Only override left and right keys without modifiers pressed.
shiftKey ||
altKey ||
metaKey ||
ctrlKey ||
( keyCode !== LEFT && keyCode !== RIGHT )
) {
return;
}

const {
text,
formats,
start,
end,
activeFormats: currentActiveFormats = [],
} = record.current;
const collapsed = isCollapsed( record.current );
const { ownerDocument } = element;
const { defaultView } = ownerDocument;
// To do: ideally, we should look at visual position instead.
const { direction } = defaultView.getComputedStyle( element );
const reverseKey = direction === 'rtl' ? RIGHT : LEFT;
const isReverse = event.keyCode === reverseKey;

// If the selection is collapsed and at the very start, do nothing if
// navigating backward.
// If the selection is collapsed and at the very end, do nothing if
// navigating forward.
if ( collapsed && currentActiveFormats.length === 0 ) {
if ( start === 0 && isReverse ) {
return;
}

if ( end === text.length && ! isReverse ) {
return;
}
}

// If the selection is not collapsed, let the browser handle collapsing
// the selection for now. Later we could expand this logic to set
// boundary positions if needed.
if ( ! collapsed ) {
return;
}

const formatsBefore =
formats[ start - 1 ] || EMPTY_ACTIVE_FORMATS;
const formatsAfter = formats[ start ] || EMPTY_ACTIVE_FORMATS;

let newActiveFormatsLength = currentActiveFormats.length;
let source = formatsAfter;

if ( formatsBefore.length > formatsAfter.length ) {
source = formatsBefore;
}

// If the amount of formats before the caret and after the caret is
// different, the caret is at a format boundary.
if ( formatsBefore.length < formatsAfter.length ) {
if (
! isReverse &&
currentActiveFormats.length < formatsAfter.length
) {
newActiveFormatsLength++;
}

if (
isReverse &&
currentActiveFormats.length > formatsBefore.length
) {
newActiveFormatsLength--;
}
} else if ( formatsBefore.length > formatsAfter.length ) {
if (
! isReverse &&
currentActiveFormats.length > formatsAfter.length
) {
newActiveFormatsLength--;
}

if (
isReverse &&
currentActiveFormats.length < formatsBefore.length
) {
newActiveFormatsLength++;
}
}

if ( newActiveFormatsLength === currentActiveFormats.length ) {
record.current._newActiveFormats = isReverse
? formatsBefore
: formatsAfter;
return;
}

event.preventDefault();

const newActiveFormats = source.slice(
0,
newActiveFormatsLength
);
const newValue = {
...record.current,
activeFormats: newActiveFormats,
};
record.current = newValue;
applyRecord( newValue );
setActiveFormats( newActiveFormats );
}

element.addEventListener( 'keydown', onKeyDown );
return () => {
element.removeEventListener( 'keydown', onKeyDown );
};
},
[ record, applyRecord, setActiveFormats ]
);
}

0 comments on commit 1390bd7

Please sign in to comment.