diff --git a/web/chat/chat-constants.js b/web/chat/chat-constants.js --- a/web/chat/chat-constants.js +++ b/web/chat/chat-constants.js @@ -16,3 +16,12 @@ width: 30, height: 38, }; + +export const typeaheadStyle = { + tooltipWidth: 296, + tooltipMaxHeight: 268, + tooltipVerticalPadding: 16, + tooltipLeftOffset: 16, + tooltipTopOffset: 4, + rowHeight: 40, +}; diff --git a/web/chat/mention-utils.js b/web/chat/mention-utils.js --- a/web/chat/mention-utils.js +++ b/web/chat/mention-utils.js @@ -1,5 +1,7 @@ // @flow +import * as React from 'react'; + import { oldValidUsernameRegexString } from 'lib/shared/account-utils'; import SearchIndex from 'lib/shared/search-index'; import { stringForUserExplicit } from 'lib/shared/user-utils'; @@ -9,6 +11,20 @@ `(?(?:^(?:.|\n)*\\s+)|^)@(?${oldValidUsernameRegexString})?$`, ); +import { type InputState } from '../input/input-state'; +import { typeaheadStyle } from './chat-constants'; + +export type MentionSuggestionTooltipAction = { + +key: string, + +onClick: (SyntheticEvent) => mixed, + +actionButtonContent: React.Node, +}; + +export type TooltipPosition = { + +top: number, + +left: number, +}; + function getTypeaheadUserSuggestions( userSearchIndex: SearchIndex, usersInThread: $ReadOnlyArray, @@ -24,7 +40,7 @@ } function getCaretOffsets( - textarea: ?HTMLTextAreaElement, + textarea: HTMLTextAreaElement, text: string, ): { caretTopOffset: number, caretLeftOffset: number } { if (!textarea) { @@ -58,10 +74,86 @@ const { offsetTop, offsetLeft } = span; document.body?.removeChild(div); + const textareaWidth = parseInt(textareaStyle.getPropertyValue('width')); + + const caretLeftOffset = + offsetLeft + typeaheadStyle.tooltipWidth > textareaWidth + ? textareaWidth - typeaheadStyle.tooltipWidth + : offsetLeft; + return { caretTopOffset: offsetTop - textarea.scrollTop, - caretLeftOffset: offsetLeft, + caretLeftOffset, }; } -export { mentionRegex, getTypeaheadUserSuggestions, getCaretOffsets }; +function getTypeaheadTooltipActions( + inputState: InputState, + textarea: HTMLTextAreaElement, + suggestedUsers: $ReadOnlyArray, + matchedTextBefore: string, + wholeMatch: string, +): $ReadOnlyArray { + return suggestedUsers + .filter( + suggestedUser => stringForUserExplicit(suggestedUser) !== 'anonymous', + ) + .map(suggestedUser => ({ + key: suggestedUser.id, + onClick: () => { + const newPrefixText = matchedTextBefore; + + let newSuffixText = inputState.draft.slice(wholeMatch.length); + newSuffixText = (newSuffixText[0] !== ' ' ? ' ' : '') + newSuffixText; + + const newText = + newPrefixText + + '@' + + stringForUserExplicit(suggestedUser) + + newSuffixText; + + inputState.setDraft(newText); + inputState.setTextCursorPosition( + newText.length - newSuffixText.length + 1, + ); + }, + actionButtonContent: stringForUserExplicit(suggestedUser), + })); +} +function getTypeaheadTooltipPosition( + textarea: HTMLTextAreaElement, + actionsLength: number, + matchedTextBefore: string, +): TooltipPosition { + const { caretTopOffset, caretLeftOffset } = getCaretOffsets( + textarea, + matchedTextBefore, + ); + + const textareaBoundingClientRect = textarea.getBoundingClientRect(); + + const top: number = + textareaBoundingClientRect.top - + Math.min( + typeaheadStyle.tooltipVerticalPadding + + actionsLength * typeaheadStyle.rowHeight, + typeaheadStyle.tooltipMaxHeight, + ) - + typeaheadStyle.tooltipTopOffset + + caretTopOffset; + + const left: number = + textareaBoundingClientRect.left - + typeaheadStyle.tooltipLeftOffset + + caretLeftOffset; + + return { top, left }; +} + +export { + mentionRegex, + getTypeaheadUserSuggestions, + getCaretOffsets, + getTypeaheadTooltipActions, + getTypeaheadTooltipPosition, +};