Fine-tune editing logic within message composition box
This commit is contained in:
parent
b85943b688
commit
0fc384cfa3
1 changed files with 209 additions and 63 deletions
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue