Composer: Preserve caret position on mention insert (#2849)

This commit is contained in:
Alexander Zinchuk 2023-03-30 18:25:04 -05:00
parent 4cbe9c1112
commit 2c8b22b964
3 changed files with 62 additions and 5 deletions

View File

@ -45,6 +45,12 @@ const MentionTooltip: FC<OwnProps> = ({
onInsertUserName(user, forceFocus);
}, [onInsertUserName]);
const handleClick = useCallback((e: React.MouseEvent, id: string) => {
e.preventDefault();
handleUserSelect(id);
}, [handleUserSelect]);
const handleSelectMention = useCallback((member: ApiUser) => {
handleUserSelect(member.id, true);
}, [handleUserSelect]);
@ -93,8 +99,8 @@ const MentionTooltip: FC<OwnProps> = ({
<ListItem
key={id}
className="chat-item-clickable scroll-item"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => handleUserSelect(id)}
onClick={handleClick}
clickArg={id}
focus={selectedMentionIndex === index}
>
<PrivateChatInfo

View File

@ -12,7 +12,7 @@ import { filterUsersByName, getMainUsername, getUserFirstOrLastName } from '../.
import { prepareForRegExp } from '../helpers/prepareForRegExp';
import focusEditableElement from '../../../../util/focusEditableElement';
import { pickTruthy, unique } from '../../../../util/iteratees';
import { getHtmlBeforeSelection } from '../../../../util/selection';
import { getCaretPosition, getHtmlBeforeSelection, setCaretPosition } from '../../../../util/selection';
import useFlag from '../../../../hooks/useFlag';
import useDerivedSignal from '../../../../hooks/useDerivedSignal';
@ -96,6 +96,7 @@ export default function useMentionTooltip(
}
const mainUsername = getMainUsername(user);
const userFirstOrLastName = getUserFirstOrLastName(user) || '';
const htmlToInsert = mainUsername
? `@${mainUsername}`
: `<a
@ -104,21 +105,27 @@ export default function useMentionTooltip(
data-user-id="${user.id}"
contenteditable="false"
dir="auto"
>${getUserFirstOrLastName(user)}</a>`;
>${userFirstOrLastName}</a>`;
const inputEl = inputRef.current!;
const htmlBeforeSelection = getHtmlBeforeSelection(inputEl);
const fixedHtmlBeforeSelection = cleanWebkitNewLines(htmlBeforeSelection);
const atIndex = fixedHtmlBeforeSelection.lastIndexOf('@');
const shiftCaretPosition = (mainUsername ? mainUsername.length + 1 : userFirstOrLastName.length)
- (fixedHtmlBeforeSelection.length - atIndex);
if (atIndex !== -1) {
const newHtml = `${fixedHtmlBeforeSelection.substr(0, atIndex)}${htmlToInsert}&nbsp;`;
const htmlAfterSelection = cleanWebkitNewLines(inputEl.innerHTML).substring(fixedHtmlBeforeSelection.length);
const caretPosition = getCaretPosition(inputEl);
setHtml(`${newHtml}${htmlAfterSelection}`);
requestAnimationFrame(() => {
const newCaretPosition = caretPosition + shiftCaretPosition + 1;
focusEditableElement(inputEl, forceFocus);
if (newCaretPosition >= 0) {
setCaretPosition(inputEl, newCaretPosition);
}
});
}

View File

@ -48,3 +48,47 @@ export function getHtmlBeforeSelection(container?: HTMLElement, useCommonAncesto
return extractorEl.innerHTML;
}
// https://stackoverflow.com/a/3976125
export function getCaretPosition(element: HTMLElement) {
let caretPosition = 0;
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return caretPosition;
}
const range = selection.getRangeAt(0);
const caretRange = range.cloneRange();
caretRange.selectNodeContents(element);
caretRange.setEnd(range.endContainer, range.endOffset);
caretPosition = caretRange.toString().length;
return caretPosition;
}
// https://stackoverflow.com/a/36953852
export function setCaretPosition(element: Node, position: number) {
for (const node of element.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
if ((node as Text).length >= position) {
const range = document.createRange();
const selection = window.getSelection()!;
range.setStart(node, position);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
return -1;
} else {
position -= 'length' in node ? node.length as number : 0;
}
} else {
position = setCaretPosition(node, position);
if (position === -1) {
return -1;
}
}
}
return position;
}