Revert custom caret navigation logic

This commit is contained in:
Ken Powers 2019-11-12 18:44:41 -05:00 committed by Scott Nonnenberg
parent fa70fa2ecf
commit 7064691696
2 changed files with 36 additions and 353 deletions

View file

@ -23,9 +23,8 @@ import { Emoji } from './emoji/Emoji';
import { EmojiPickDataType } from './emoji/EmojiPicker'; import { EmojiPickDataType } from './emoji/EmojiPicker';
import { import {
convertShortName, convertShortName,
DataFromEmojiText,
EmojiData, EmojiData,
emojiToData, replaceColons,
search, search,
} from './emoji/lib'; } from './emoji/lib';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
@ -152,78 +151,6 @@ function getWordAtIndex(
}; };
} }
// Replace bare (non-entitied) emojis with draft entities
function replaceBareEmojis(state: EditorState, focus: boolean): EditorState {
// Track emoji positions
const selections: Array<[SelectionState, DataFromEmojiText]> = [];
const content = state.getCurrentContent();
const initialSelection = state.getSelection();
content.getBlockMap().forEach(block => {
if (!block) {
return;
}
const pat = emojiRegex();
const text = block.getText();
let match;
// tslint:disable-next-line
while ((match = pat.exec(text)) !== null) {
const start = match.index;
const end = start + match[0].length;
const blockKey = block.getKey();
const blockSelection = SelectionState.createEmpty(blockKey).merge({
anchorOffset: start,
focusOffset: end,
}) as SelectionState;
const emojiData = emojiToData(match[0]);
// 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) {
selections.push([blockSelection, emojiData]);
}
}
});
const newContent = selections.reduce(
(accContent, [sel, { shortName, tone }]) => {
const emojiContent = convertShortName(shortName);
const emojiEntityKey = accContent
.createEntity('emoji', 'IMMUTABLE', {
shortName: shortName,
skinTone: tone,
})
.getLastCreatedEntityKey();
return Modifier.replaceText(
accContent,
sel,
emojiContent,
undefined,
emojiEntityKey
);
},
content
);
const pushState = EditorState.push(
state,
newContent,
'replace-emoji' as EditorChangeType
);
if (focus) {
const newSelection = initialSelection.merge({
anchorOffset: initialSelection.getAnchorOffset(),
focusOffset: initialSelection.getFocusOffset(),
}) as SelectionState;
return EditorState.forceSelection(pushState, newSelection);
}
return pushState;
}
const compositeDecorator = new CompositeDecorator([ const compositeDecorator = new CompositeDecorator([
{ {
strategy: (block, cb) => { strategy: (block, cb) => {
@ -280,15 +207,18 @@ const getInitialEditorState = (startingText?: string) => {
return EditorState.createEmpty(compositeDecorator); return EditorState.createEmpty(compositeDecorator);
} }
const state = replaceBareEmojis( const end = startingText.length;
EditorState.createWithContent( const state = EditorState.createWithContent(
ContentState.createFromText(startingText), ContentState.createFromText(startingText),
compositeDecorator compositeDecorator
),
false
); );
const selection = state.getSelection();
const selectionAtEnd = selection.merge({
anchorOffset: end,
focusOffset: end,
}) as SelectionState;
return EditorState.moveFocusToEnd(state); return EditorState.forceSelection(state, selectionAtEnd);
}; };
// tslint:disable-next-line max-func-body-length // tslint:disable-next-line max-func-body-length
@ -321,7 +251,6 @@ export const CompositionInput = ({
const focusRef = React.useRef(false); const focusRef = React.useRef(false);
const editorStateRef = React.useRef<EditorState>(editorRenderState); const editorStateRef = React.useRef<EditorState>(editorRenderState);
const rootElRef = React.useRef<HTMLDivElement>(); const rootElRef = React.useRef<HTMLDivElement>();
const latestKeyRef = React.useRef<string>();
// This function sets editorState and also keeps a reference to the newly set // This function sets editorState and also keeps a reference to the newly set
// state so we can reference the state in effects and callbacks without // state so we can reference the state in effects and callbacks without
@ -336,10 +265,7 @@ export const CompositionInput = ({
const updateExternalStateListeners = React.useCallback( const updateExternalStateListeners = React.useCallback(
(newState: EditorState) => { (newState: EditorState) => {
const plainText = newState const plainText = newState.getCurrentContent().getPlainText();
.getCurrentContent()
.getPlainText()
.trim();
const cursorBlockKey = newState.getSelection().getStartKey(); const cursorBlockKey = newState.getSelection().getStartKey();
const cursorBlockIndex = editorStateRef.current const cursorBlockIndex = editorStateRef.current
.getCurrentContent() .getCurrentContent()
@ -389,20 +315,6 @@ 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();
@ -414,7 +326,7 @@ export const CompositionInput = ({
// Update the state to indicate emojiable text at the current position. // Update the state to indicate emojiable text at the current position.
const newSearchText = match ? match.trim().substr(1) : ''; const newSearchText = match ? match.trim().substr(1) : '';
if (newSearchText.endsWith(':') && latestKeyRef.current === ':') { if (newSearchText.endsWith(':')) {
const bareText = trimEnd(newSearchText, ':'); const bareText = trimEnd(newSearchText, ':');
const emoji = head(search(bareText)); const emoji = head(search(bareText));
if (emoji && bareText === emoji.short_name) { if (emoji && bareText === emoji.short_name) {
@ -433,16 +345,12 @@ export const CompositionInput = ({
resetEmojiResults(); resetEmojiResults();
} }
const modifiedState = replaceBareEmojis(newState, focusRef.current);
// Finally, update the editor state // Finally, update the editor state
setAndTrackEditorState(modifiedState); setAndTrackEditorState(newState);
updateExternalStateListeners(modifiedState); updateExternalStateListeners(newState);
}, },
[ [
editorStateRef,
focusRef, focusRef,
latestKeyRef,
resetEmojiResults, resetEmojiResults,
setAndTrackEditorState, setAndTrackEditorState,
setSearchText, setSearchText,
@ -508,8 +416,8 @@ export const CompositionInput = ({
() => { () => {
const { current: state } = editorStateRef; const { current: state } = editorStateRef;
const text = state.getCurrentContent().getPlainText(); const text = state.getCurrentContent().getPlainText();
const trimmedText = text.trim(); const emojidText = replaceColons(text);
onSubmit(trimmedText); onSubmit(emojidText);
}, },
[editorStateRef, onSubmit] [editorStateRef, onSubmit]
); );
@ -561,212 +469,17 @@ export const CompositionInput = ({
[emojiResultsIndex, emojiResults] [emojiResultsIndex, emojiResults]
); );
const modKeySelection = React.useCallback(
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
(e: React.KeyboardEvent) => {
e.preventDefault();
const { current: state } = editorStateRef;
const selection = state.getSelection();
const newSelectionDesc: Partial<{
anchorKey: string;
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()
.getBlockForKey(selection.getAnchorKey())
.getText();
newSelectionDesc.anchorOffset = length;
newSelectionDesc.focusOffset = length;
} else if (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.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(newSelectionDesc) as SelectionState;
setAndTrackEditorState(EditorState.forceSelection(state, newSelection));
},
[editorStateRef, setAndTrackEditorState]
);
const handleEditorArrowKey = React.useCallback( const handleEditorArrowKey = React.useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
latestKeyRef.current = e.key;
if (e.key === 'ArrowUp') { if (e.key === 'ArrowUp') {
modKeySelection(e); selectEmojiResult('prev', e);
if (!e.shiftKey) {
selectEmojiResult('prev', e);
}
} }
if (e.key === 'ArrowDown') { if (e.key === 'ArrowDown') {
modKeySelection(e); selectEmojiResult('next', e);
if (!e.shiftKey) {
selectEmojiResult('next', e);
}
}
if (e.key === 'ArrowLeft' && e.metaKey) {
modKeySelection(e);
}
if (e.key === 'ArrowRight' && e.metaKey) {
modKeySelection(e);
} }
}, },
[latestKeyRef, selectEmojiResult, modKeySelection] [selectEmojiResult]
); );
const handleEscapeKey = React.useCallback( const handleEscapeKey = React.useCallback(
@ -916,7 +629,6 @@ export const CompositionInput = ({
resetEmojiResults, resetEmojiResults,
selectEmojiResult, selectEmojiResult,
setAndTrackEditorState, setAndTrackEditorState,
modKeySelection,
skinTone, skinTone,
submit, submit,
] ]
@ -935,10 +647,7 @@ export const CompositionInput = ({
); );
const editorKeybindingFn = React.useCallback( const editorKeybindingFn = React.useCallback(
// tslint:disable-next-line cyclomatic-complexity
(e: React.KeyboardEvent): CompositionInputEditorCommand | null => { (e: React.KeyboardEvent): CompositionInputEditorCommand | null => {
latestKeyRef.current = e.key;
if (e.key === 'Enter' && emojiResults.length > 0) { if (e.key === 'Enter' && emojiResults.length > 0) {
e.preventDefault(); e.preventDefault();
@ -955,30 +664,6 @@ export const CompositionInput = ({
return 'submit'; return 'submit';
} }
if (e.shiftKey && e.key === 'End') {
modKeySelection(e);
return null;
}
if (e.key === 'End') {
modKeySelection(e);
return null;
}
if (e.shiftKey && e.key === 'Home') {
modKeySelection(e);
return null;
}
if (e.key === 'Home') {
modKeySelection(e);
return null;
}
if (e.key === 'n' && e.ctrlKey) { if (e.key === 'n' && e.ctrlKey) {
e.preventDefault(); e.preventDefault();
@ -1005,7 +690,7 @@ export const CompositionInput = ({
return getDefaultKeyBinding(e); return getDefaultKeyBinding(e);
}, },
[latestKeyRef, emojiResults, large, modKeySelection] [emojiResults, large]
); );
// Create popper root // Create popper root
@ -1057,7 +742,6 @@ export const CompositionInput = ({
setAndTrackEditorState( setAndTrackEditorState(
EditorState.forceSelection(oldState, oldState.getSelection()) EditorState.forceSelection(oldState, oldState.getSelection())
); );
onFocus();
}; };
rootEl.addEventListener('focusin', onFocusIn); rootEl.addEventListener('focusin', onFocusIn);
@ -1069,7 +753,7 @@ export const CompositionInput = ({
return noop; return noop;
}, },
[editorStateRef, onFocus, rootElRef, setAndTrackEditorState] [editorStateRef, rootElRef, setAndTrackEditorState]
); );
if (inputApi) { if (inputApi) {
@ -1106,8 +790,6 @@ export const CompositionInput = ({
placeholder={i18n('sendMessage')} placeholder={i18n('sendMessage')}
onUpArrow={handleEditorArrowKey} onUpArrow={handleEditorArrowKey}
onDownArrow={handleEditorArrowKey} onDownArrow={handleEditorArrowKey}
onLeftArrow={handleEditorArrowKey}
onRightArrow={handleEditorArrowKey}
onEscape={handleEscapeKey} onEscape={handleEscapeKey}
onTab={onTab} onTab={onTab}
handleKeyCommand={handleEditorCommand} handleKeyCommand={handleEditorCommand}

View file

@ -38,11 +38,6 @@ export type EmojiSkinVariation = {
has_img_messenger: boolean; has_img_messenger: boolean;
}; };
export type DataFromEmojiText = {
shortName: string;
tone?: SkinToneKey;
};
export type EmojiData = { export type EmojiData = {
name: string; name: string;
unified: string; unified: string;
@ -121,7 +116,6 @@ export const preloadImages = async () => {
const dataByShortName = keyBy(data, 'short_name'); const dataByShortName = keyBy(data, 'short_name');
const imageByEmoji: { [key: string]: string } = {}; const imageByEmoji: { [key: string]: string } = {};
const dataByEmoji: { [key: string]: DataFromEmojiText } = {};
export const dataByCategory = mapValues( export const dataByCategory = mapValues(
groupBy(data, ({ category }) => { groupBy(data, ({ category }) => {
@ -248,8 +242,20 @@ export function emojiToImage(emoji: string): string | undefined {
return imageByEmoji[emoji]; return imageByEmoji[emoji];
} }
export function emojiToData(emoji: string): DataFromEmojiText | undefined { export function replaceColons(str: string) {
return dataByEmoji[emoji]; return str.replace(/:[a-z0-9-_+]+:(?::skin-tone-[1-5]:)?/gi, m => {
const [shortName = '', skinTone = '0'] = m
.replace('skin-tone-', '')
.toLowerCase()
.split(':')
.filter(Boolean);
if (shortName && isShortName(shortName)) {
return convertShortName(shortName, parseInt(skinTone, 10));
}
return m;
});
} }
function getCountOfAllMatches(str: string, regex: RegExp) { function getCountOfAllMatches(str: string, regex: RegExp) {
@ -299,17 +305,12 @@ data.forEach(emoji => {
} }
imageByEmoji[convertShortName(short_name)] = makeImagePath(image); imageByEmoji[convertShortName(short_name)] = makeImagePath(image);
dataByEmoji[convertShortName(short_name)] = { shortName: short_name };
if (skin_variations) { if (skin_variations) {
Object.entries(skin_variations).forEach(([tone, variation]) => { Object.entries(skin_variations).forEach(([tone, variation]) => {
imageByEmoji[ imageByEmoji[
convertShortName(short_name, tone as SkinToneKey) convertShortName(short_name, tone as SkinToneKey)
] = makeImagePath(variation.image); ] = makeImagePath(variation.image);
dataByEmoji[convertShortName(short_name, tone as SkinToneKey)] = {
shortName: short_name,
tone: tone as SkinToneKey,
};
}); });
} }
}); });