Page MenuHomePhabricator

typeahead-utils.js
No OneTemporary

typeahead-utils.js

// @flow
import * as React from 'react';
import { oldValidUsernameRegexString } from 'lib/shared/account-utils';
import { stringForUserExplicit } from 'lib/shared/user-utils';
import type { RelativeMemberInfo } from 'lib/types/thread-types';
import { typeaheadStyle } from '../chat/chat-constants';
import css from '../chat/typeahead-tooltip.css';
import Button from '../components/button.react';
const webTypeaheadRegex: RegExp = new RegExp(
`(?<textPrefix>(?:^(?:.|\n)*\\s+)|^)@(?<username>${oldValidUsernameRegexString})?$`,
);
export type TypeaheadTooltipAction = {
+key: string,
+onClick: (SyntheticEvent<HTMLButtonElement>) => mixed,
+actionButtonContent: React.Node,
};
export type TooltipPosition = {
+top: number,
+left: number,
};
function getCaretOffsets(
textarea: HTMLTextAreaElement,
text: string,
): { caretTopOffset: number, caretLeftOffset: number } {
if (!textarea) {
return { caretTopOffset: 0, caretLeftOffset: 0 };
}
// terribly hacky but it works I guess :D
// we had to use it, as it's hard to count lines in textarea
// and track cursor position within it as
// lines can be wrapped into new lines without \n character
// as result of overflow
const textareaStyle: CSSStyleDeclaration = window.getComputedStyle(
textarea,
null,
);
const div = document.createElement('div');
for (const styleName of textareaStyle) {
div.style.setProperty(styleName, textareaStyle.getPropertyValue(styleName));
}
div.style.display = 'inline-block';
div.style.position = 'absolute';
div.textContent = text;
const span = document.createElement('span');
span.textContent = textarea.value.slice(text.length);
div.appendChild(span);
document.body?.appendChild(div);
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,
};
}
export type GetTypeaheadTooltipActionsParams = {
+inputStateDraft: string,
+inputStateSetDraft: (draft: string) => mixed,
+inputStateSetTextCursorPosition: (newPosition: number) => mixed,
+suggestedUsers: $ReadOnlyArray<RelativeMemberInfo>,
+textBeforeAtSymbol: string,
+usernamePrefix: string,
};
function getTypeaheadTooltipActions(
params: GetTypeaheadTooltipActionsParams,
): $ReadOnlyArray<TypeaheadTooltipAction> {
const {
inputStateDraft,
inputStateSetDraft,
inputStateSetTextCursorPosition,
suggestedUsers,
textBeforeAtSymbol,
usernamePrefix,
} = params;
return suggestedUsers
.filter(
suggestedUser => stringForUserExplicit(suggestedUser) !== 'anonymous',
)
.map(suggestedUser => ({
key: suggestedUser.id,
onClick: () => {
const newPrefixText = textBeforeAtSymbol;
const totalMatchLength =
textBeforeAtSymbol.length + usernamePrefix.length + 1; // 1 for @ char
let newSuffixText = inputStateDraft.slice(totalMatchLength);
newSuffixText = (newSuffixText[0] !== ' ' ? ' ' : '') + newSuffixText;
const newText =
newPrefixText +
'@' +
stringForUserExplicit(suggestedUser) +
newSuffixText;
inputStateSetDraft(newText);
inputStateSetTextCursorPosition(
newText.length - newSuffixText.length + 1,
);
},
actionButtonContent: stringForUserExplicit(suggestedUser),
}));
}
function getTypeaheadTooltipButtons(
actions: $ReadOnlyArray<TypeaheadTooltipAction>,
): $ReadOnlyArray<React.Node> {
return actions.map(({ key, onClick, actionButtonContent }) => (
<Button key={key} onClick={onClick} className={css.suggestion}>
<span>@{actionButtonContent}</span>
</Button>
));
}
function getTypeaheadTooltipPosition(
textarea: HTMLTextAreaElement,
actionsLength: number,
textBeforeAtSymbol: string,
): TooltipPosition {
const { caretTopOffset, caretLeftOffset } = getCaretOffsets(
textarea,
textBeforeAtSymbol,
);
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 {
webTypeaheadRegex,
getCaretOffsets,
getTypeaheadTooltipActions,
getTypeaheadTooltipButtons,
getTypeaheadTooltipPosition,
};

File Metadata

Mime Type
text/x-java
Expires
Mon, Dec 23, 11:52 AM (19 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690854
Default Alt Text
typeahead-utils.js (4 KB)

Event Timeline