Fine-tune editing logic within message composition box

This commit is contained in:
Ken Powers 2019-11-06 18:29:19 -05:00 committed by Scott Nonnenberg
parent b85943b688
commit 0fc384cfa3

View file

@ -57,11 +57,9 @@ export type InputApi = {
submit: () => void; submit: () => void;
}; };
export type SelectionKeys = 'Shift-End' | 'End' | 'Shift-Home' | 'Home';
export type CompositionInputEditorCommand = export type CompositionInputEditorCommand =
| DraftEditorCommand | DraftEditorCommand
| ('enter-emoji' | 'next-emoji' | 'prev-emoji' | 'submit') | ('enter-emoji' | 'next-emoji' | 'prev-emoji' | 'submit');
| SelectionKeys;
function getTrimmedMatchAtIndex(str: string, index: number, pattern: RegExp) { function getTrimmedMatchAtIndex(str: string, index: number, pattern: RegExp) {
let match; let match;
@ -161,7 +159,6 @@ function replaceBareEmojis(state: EditorState, focus: boolean): EditorState {
const content = state.getCurrentContent(); const content = state.getCurrentContent();
const initialSelection = state.getSelection(); const initialSelection = state.getSelection();
let selectionOffset = 0;
content.getBlockMap().forEach(block => { content.getBlockMap().forEach(block => {
if (!block) { if (!block) {
@ -180,7 +177,8 @@ function replaceBareEmojis(state: EditorState, focus: boolean): EditorState {
focusOffset: end, focusOffset: end,
}) as SelectionState; }) as SelectionState;
const emojiData = emojiToData(match[0]); const emojiData = emojiToData(match[0]);
// If there is not entity at this location and emoji data exists for the emoji at this location, track it for replacement // If there is no entity at this location and emoji data exists for the
// emoji at this location, track it for replacement
if (!block.getEntityAt(start) && emojiData) { if (!block.getEntityAt(start) && emojiData) {
selections.push([blockSelection, emojiData]); selections.push([blockSelection, emojiData]);
} }
@ -197,13 +195,6 @@ function replaceBareEmojis(state: EditorState, focus: boolean): EditorState {
}) })
.getLastCreatedEntityKey(); .getLastCreatedEntityKey();
// Keep track of selection offsets caused by replaced emojis
if (sel.getAnchorOffset() < initialSelection.getAnchorOffset()) {
selectionOffset += Math.abs(
sel.getAnchorOffset() - sel.getFocusOffset()
);
}
return Modifier.replaceText( return Modifier.replaceText(
accContent, accContent,
sel, sel,
@ -223,8 +214,8 @@ function replaceBareEmojis(state: EditorState, focus: boolean): EditorState {
if (focus) { if (focus) {
const newSelection = initialSelection.merge({ const newSelection = initialSelection.merge({
anchorOffset: initialSelection.getAnchorOffset() + selectionOffset, anchorOffset: initialSelection.getAnchorOffset(),
focusOffset: initialSelection.getFocusOffset() + selectionOffset, focusOffset: initialSelection.getFocusOffset(),
}) as SelectionState; }) as SelectionState;
return EditorState.forceSelection(pushState, newSelection); return EditorState.forceSelection(pushState, newSelection);
@ -398,6 +389,20 @@ export const CompositionInput = ({
const handleEditorStateChange = React.useCallback( const handleEditorStateChange = React.useCallback(
(newState: EditorState) => { (newState: EditorState) => {
// If this is an undo, we don't want to trigger any other custom logic
if (newState.getLastChangeType() === 'undo') {
// Does this undo result in the same state as before?
const pointlessUndo =
newState.getCurrentContent().getPlainText() ===
editorStateRef.current.getCurrentContent().getPlainText();
// If so, we need to apply another undo
const pushState = pointlessUndo ? EditorState.undo(newState) : newState;
// Update state
setAndTrackEditorState(pushState);
resetEmojiResults();
return;
}
// Does the current position have any emojiable text? // Does the current position have any emojiable text?
const selection = newState.getSelection(); const selection = newState.getSelection();
const caretLocation = selection.getStartOffset(); const caretLocation = selection.getStartOffset();
@ -435,6 +440,7 @@ export const CompositionInput = ({
updateExternalStateListeners(modifiedState); updateExternalStateListeners(modifiedState);
}, },
[ [
editorStateRef,
focusRef, focusRef,
latestKeyRef, latestKeyRef,
resetEmojiResults, resetEmojiResults,
@ -555,27 +561,180 @@ export const CompositionInput = ({
[emojiResultsIndex, emojiResults] [emojiResultsIndex, emojiResults]
); );
const setCursor = React.useCallback( const modKeySelection = React.useCallback(
(key: SelectionKeys) => { // tslint:disable-next-line cyclomatic-complexity max-func-body-length
(e: React.KeyboardEvent) => {
e.preventDefault();
const { current: state } = editorStateRef; const { current: state } = editorStateRef;
const selection = state.getSelection(); const selection = state.getSelection();
const offset =
key === 'Shift-Home' || key === 'Home' const newSelectionDesc: Partial<{
? 0 anchorKey: string;
: state anchorOffset: number;
focusKey: string;
focusOffset: number;
}> = {};
if (
(e.shiftKey && (e.metaKey && e.key === 'ArrowUp')) ||
e.key === 'Home'
) {
const block = state.getCurrentContent().getFirstBlock();
newSelectionDesc.anchorKey = block.getKey();
newSelectionDesc.anchorOffset = 0;
} else if (
(e.shiftKey && (e.metaKey && e.key === 'ArrowDown')) ||
e.key === 'End'
) {
const block = state.getCurrentContent().getLastBlock();
newSelectionDesc.focusKey = block.getKey();
newSelectionDesc.focusOffset = block.getText().length;
} else if (
e.shiftKey &&
((e.metaKey && e.key === 'ArrowLeft') || e.key === 'Home')
) {
newSelectionDesc.anchorOffset = 0;
} else if (
e.shiftKey &&
((e.metaKey && e.key === 'ArrowRight') || e.key === 'End')
) {
newSelectionDesc.focusOffset = state
.getCurrentContent()
.getBlockForKey(selection.getFocusKey())
.getText().length;
} else if (e.shiftKey && e.key === 'ArrowLeft') {
newSelectionDesc.anchorOffset = selection.getAnchorOffset() - 1;
if (newSelectionDesc.anchorOffset < 0) {
newSelectionDesc.anchorOffset = 0;
const block = state
.getCurrentContent()
.getBlockBefore(selection.getAnchorKey());
if (block) {
newSelectionDesc.anchorKey = block.getKey();
}
}
} else if (e.shiftKey && e.key === 'ArrowRight') {
newSelectionDesc.focusOffset = selection.getFocusOffset() + 1;
const { length } = state
.getCurrentContent()
.getBlockForKey(selection.getFocusKey())
.getText();
if (newSelectionDesc.focusOffset > length) {
newSelectionDesc.focusOffset = length;
const block = state
.getCurrentContent()
.getBlockAfter(selection.getAnchorKey());
if (block) {
newSelectionDesc.anchorKey = block.getKey();
}
}
} else if (e.shiftKey && e.key === 'ArrowUp') {
if (selection.getIsBackward()) {
const block = state
.getCurrentContent()
.getBlockBefore(selection.getFocusKey());
newSelectionDesc.focusOffset = 0;
if (block) {
newSelectionDesc.focusKey = block.getKey();
}
} else {
const block = state
.getCurrentContent()
.getBlockBefore(selection.getAnchorKey());
newSelectionDesc.anchorOffset = 0;
if (block) {
newSelectionDesc.anchorKey = block.getKey();
}
}
} else if (e.shiftKey && e.key === 'ArrowDown') {
if (selection.getIsBackward()) {
const block = state
.getCurrentContent()
.getBlockAfter(selection.getAnchorKey());
if (block) {
newSelectionDesc.anchorKey = block.getKey();
newSelectionDesc.anchorOffset = block.getText().length;
}
} else {
const block = state
.getCurrentContent()
.getBlockAfter(selection.getFocusKey());
if (block) {
newSelectionDesc.focusKey = block.getKey();
newSelectionDesc.focusOffset = block.getText().length;
}
}
} else if ((e.metaKey && e.key === 'ArrowLeft') || e.key === 'Home') {
newSelectionDesc.anchorOffset = 0;
newSelectionDesc.focusOffset = 0;
} else if ((e.metaKey && e.key === 'ArrowRight') || e.key === 'End') {
const { length } = state
.getCurrentContent() .getCurrentContent()
.getBlockForKey(selection.getAnchorKey()) .getBlockForKey(selection.getAnchorKey())
.getText().length; .getText();
newSelectionDesc.anchorOffset = length;
const desc: { focusOffset?: number; anchorOffset?: number } = { newSelectionDesc.focusOffset = length;
focusOffset: offset, } else if (e.key === 'ArrowLeft') {
}; newSelectionDesc.anchorOffset = selection.getAnchorOffset() - 1;
if (newSelectionDesc.anchorOffset < 0) {
if (key === 'Home' || key === 'End') { newSelectionDesc.anchorOffset = 0;
desc.anchorOffset = offset; const block = state
.getCurrentContent()
.getBlockBefore(selection.getAnchorKey());
if (block) {
newSelectionDesc.anchorKey = block.getKey();
}
}
} else if (e.key === 'ArrowRight') {
newSelectionDesc.focusOffset = selection.getFocusOffset() + 1;
const { length } = state
.getCurrentContent()
.getBlockForKey(selection.getFocusKey())
.getText();
if (newSelectionDesc.focusOffset > length) {
newSelectionDesc.anchorOffset = length;
const block = state
.getCurrentContent()
.getBlockAfter(selection.getFocusKey());
if (block) {
newSelectionDesc.focusKey = block.getKey();
}
}
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
const content = state.getCurrentContent();
const anchorKey = selection.getAnchorKey();
const block =
e.key === 'ArrowUp'
? content.getBlockBefore(anchorKey)
: content.getBlockAfter(anchorKey);
if (block) {
const key = block.getKey();
const length = block.getText().length;
newSelectionDesc.anchorKey = key;
newSelectionDesc.focusKey = key;
const offset = selection.getAnchorOffset();
newSelectionDesc.anchorOffset = Math.min(length, offset);
newSelectionDesc.focusOffset = Math.min(length, offset);
} else {
if (e.key === 'ArrowUp') {
const key = content.getFirstBlock().getKey();
newSelectionDesc.anchorKey = key;
newSelectionDesc.focusKey = key;
newSelectionDesc.anchorOffset = 0;
newSelectionDesc.focusOffset = 0;
} else {
const lastBlock = content.getLastBlock();
const key = lastBlock.getKey();
const { length } = lastBlock.getText();
newSelectionDesc.anchorKey = key;
newSelectionDesc.focusKey = key;
newSelectionDesc.anchorOffset = length;
newSelectionDesc.focusOffset = length;
}
}
} }
const newSelection = selection.merge(desc) as SelectionState; const newSelection = selection.merge(newSelectionDesc) as SelectionState;
setAndTrackEditorState(EditorState.forceSelection(state, newSelection)); setAndTrackEditorState(EditorState.forceSelection(state, newSelection));
}, },
[editorStateRef, setAndTrackEditorState] [editorStateRef, setAndTrackEditorState]
@ -586,32 +745,28 @@ export const CompositionInput = ({
latestKeyRef.current = e.key; latestKeyRef.current = e.key;
if (e.key === 'ArrowUp') { if (e.key === 'ArrowUp') {
modKeySelection(e);
if (!e.shiftKey) {
selectEmojiResult('prev', e); selectEmojiResult('prev', e);
} }
}
if (e.key === 'ArrowDown') { if (e.key === 'ArrowDown') {
modKeySelection(e);
if (!e.shiftKey) {
selectEmojiResult('next', e); selectEmojiResult('next', e);
} }
}
if (e.key === 'ArrowLeft' && e.metaKey) { if (e.key === 'ArrowLeft' && e.metaKey) {
e.preventDefault(); modKeySelection(e);
if (e.shiftKey) {
setCursor('Shift-Home');
} else {
setCursor('Home');
}
} }
if (e.key === 'ArrowRight' && e.metaKey) { if (e.key === 'ArrowRight' && e.metaKey) {
e.preventDefault(); modKeySelection(e);
if (e.shiftKey) {
setCursor('Shift-End');
} else {
setCursor('End');
}
} }
}, },
[latestKeyRef, selectEmojiResult, setCursor] [latestKeyRef, selectEmojiResult, modKeySelection]
); );
const handleEscapeKey = React.useCallback( const handleEscapeKey = React.useCallback(
@ -753,15 +908,6 @@ export const CompositionInput = ({
selectEmojiResult('prev'); selectEmojiResult('prev');
} }
if (
command === 'Shift-End' ||
command === 'End' ||
command === 'Shift-Home' ||
command === 'Home'
) {
setCursor(command);
}
return 'not-handled'; return 'not-handled';
}, },
[ [
@ -770,7 +916,7 @@ export const CompositionInput = ({
resetEmojiResults, resetEmojiResults,
selectEmojiResult, selectEmojiResult,
setAndTrackEditorState, setAndTrackEditorState,
setCursor, modKeySelection,
skinTone, skinTone,
submit, submit,
] ]
@ -810,27 +956,27 @@ export const CompositionInput = ({
} }
if (e.shiftKey && e.key === 'End') { if (e.shiftKey && e.key === 'End') {
e.preventDefault(); modKeySelection(e);
return 'Shift-End'; return null;
} }
if (e.key === 'End') { if (e.key === 'End') {
e.preventDefault(); modKeySelection(e);
return 'End'; return null;
} }
if (e.shiftKey && e.key === 'Home') { if (e.shiftKey && e.key === 'Home') {
e.preventDefault(); modKeySelection(e);
return 'Shift-Home'; return null;
} }
if (e.key === 'Home') { if (e.key === 'Home') {
e.preventDefault(); modKeySelection(e);
return 'Home'; return null;
} }
if (e.key === 'n' && e.ctrlKey) { if (e.key === 'n' && e.ctrlKey) {
@ -859,7 +1005,7 @@ export const CompositionInput = ({
return getDefaultKeyBinding(e); return getDefaultKeyBinding(e);
}, },
[latestKeyRef, emojiResults, large] [latestKeyRef, emojiResults, large, modKeySelection]
); );
// Create popper root // Create popper root