Changeset View
Changeset View
Standalone View
Standalone View
lib/shared/mention-utils.js
// @flow | // @flow | ||||
import type { RelativeMemberInfo } from '../types/thread-types'; | import type { RelativeMemberInfo } from '../types/thread-types'; | ||||
import { oldValidUsernameRegexString } from './account-utils'; | import { oldValidUsernameRegexString } from './account-utils'; | ||||
import SearchIndex from './search-index'; | import SearchIndex from './search-index'; | ||||
import { stringForUserExplicit } from './user-utils'; | import { stringForUserExplicit } from './user-utils'; | ||||
const mentionRegex: RegExp = new RegExp( | |||||
`(?<textPrefix>(?:^(?:.|\n)*\\s+)|^)@(?<username>${oldValidUsernameRegexString})?$`, | |||||
); | |||||
function getTypeaheadUserSuggestions( | function getTypeaheadUserSuggestions( | ||||
userSearchIndex: SearchIndex, | userSearchIndex: SearchIndex, | ||||
usersInThread: $ReadOnlyArray<RelativeMemberInfo>, | usersInThread: $ReadOnlyArray<RelativeMemberInfo>, | ||||
typedPrefix: string, | typedPrefix: string, | ||||
): $ReadOnlyArray<RelativeMemberInfo> { | ): $ReadOnlyArray<RelativeMemberInfo> { | ||||
const userIDs = userSearchIndex.getSearchResults(typedPrefix); | const userIDs = userSearchIndex.getSearchResults(typedPrefix); | ||||
return usersInThread | return usersInThread | ||||
.filter( | .filter(user => typedPrefix.length === 0 || userIDs.includes(user.id)) | ||||
(user: RelativeMemberInfo) => | |||||
typedPrefix.length === 0 || userIDs.includes(user.id), | |||||
) | |||||
.sort((userA, userB) => | .sort((userA, userB) => | ||||
stringForUserExplicit(userA).localeCompare(stringForUserExplicit(userB)), | stringForUserExplicit(userA).localeCompare(stringForUserExplicit(userB)), | ||||
); | ); | ||||
} | } | ||||
const mentionRegex: RegExp = new RegExp( | function getCaretOffsets( | ||||
`(^|.* )@(${oldValidUsernameRegexString})?$`, | 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); | |||||
return { | |||||
caretTopOffset: offsetTop - textarea.scrollTop, | |||||
caretLeftOffset: offsetLeft, | |||||
}; | |||||
} | |||||
export { getTypeaheadUserSuggestions, mentionRegex }; | export { mentionRegex, getTypeaheadUserSuggestions, getCaretOffsets }; |