diff --git a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx index 591f1bf..9479a69 100644 --- a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx @@ -64,9 +64,7 @@ export function EmoticonAutocomplete({ }, [imagePacks]); const [result, search, resetSearch] = useAsyncSearch(searchList, getEmoticonStr, SEARCH_OPTIONS); - const autoCompleteEmoticon = (result ? result.items : recentEmoji).sort((a, b) => - a.shortcode.localeCompare(b.shortcode) - ); + const autoCompleteEmoticon = result ? result.items : recentEmoji; useEffect(() => { if (query.text) search(query.text); diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index f3bd551..77e56a9 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -471,36 +471,34 @@ export function SearchEmojiGroup({ return ( {tab === EmojiBoardTab.Emoji - ? searchResult - .sort((a, b) => a.shortcode.localeCompare(b.shortcode)) - .map((emoji) => - 'unicode' in emoji ? ( - - {emoji.unicode} - - ) : ( - - {emoji.body - - ) + ? searchResult.map((emoji) => + 'unicode' in emoji ? ( + + {emoji.unicode} + + ) : ( + + {emoji.body + ) + ) : searchResult.map((emoji) => 'unicode' in emoji ? null : ( = export type SearchResetHandler = () => void; +const performMatch = ( + target: string | string[], + query: string, + options?: UseAsyncSearchOptions +): string | undefined => { + if (Array.isArray(target)) { + const matchTarget = target.find((i) => + matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions) + ); + return matchTarget ? normalize(matchTarget, options?.normalizeOptions) : undefined; + } + + const normalizedTargetStr = normalize(target, options?.normalizeOptions); + const matches = matchQuery(normalizedTargetStr, query, options?.matchOptions); + return matches ? normalizedTargetStr : undefined; +}; + +export const orderSearchItems = ( + query: string, + items: TSearchItem[], + getItemStr: SearchItemStrGetter, + options?: UseAsyncSearchOptions +): TSearchItem[] => { + const orderedItems: TSearchItem[] = Array.from(items); + + // we will consider "_" as word boundary char. + // because in more use-cases it is used. (like: emojishortcode) + const boundaryRegex = new RegExp(`(\\b|_)${query}`); + const perfectBoundaryRegex = new RegExp(`(\\b|_)${query}(\\b|_)`); + + orderedItems.sort((i1, i2) => { + const str1 = performMatch(getItemStr(i1, query), query, options); + const str2 = performMatch(getItemStr(i2, query), query, options); + + if (str1 === undefined && str2 === undefined) return 0; + if (str1 === undefined) return 1; + if (str2 === undefined) return -1; + + let points1 = 0; + let points2 = 0; + + // short string should score more + const pointsToSmallStr = (points: number) => { + if (str1.length < str2.length) points1 += points; + else if (str2.length < str1.length) points2 += points; + }; + pointsToSmallStr(1); + + // closes query match should score more + const indexIn1 = str1.indexOf(query); + const indexIn2 = str2.indexOf(query); + if (indexIn1 < indexIn2) points1 += 2; + else if (indexIn2 < indexIn1) points2 += 2; + else pointsToSmallStr(2); + + // query match word start on boundary should score more + const boundaryIn1 = str1.match(boundaryRegex); + const boundaryIn2 = str2.match(boundaryRegex); + if (boundaryIn1 && boundaryIn2) pointsToSmallStr(4); + else if (boundaryIn1) points1 += 4; + else if (boundaryIn2) points2 += 4; + + // query match word start and end on boundary should score more + const perfectBoundaryIn1 = str1.match(perfectBoundaryRegex); + const perfectBoundaryIn2 = str2.match(perfectBoundaryRegex); + if (perfectBoundaryIn1 && perfectBoundaryIn2) pointsToSmallStr(8); + else if (perfectBoundaryIn1) points1 += 8; + else if (perfectBoundaryIn2) points2 += 8; + + return points2 - points1; + }); + + return orderedItems; +}; + export const useAsyncSearch = ( list: TSearchItem[], getItemStr: SearchItemStrGetter, @@ -40,21 +115,15 @@ export const useAsyncSearch = ( const handleMatch: MatchHandler = (item, query) => { const itemStr = getItemStr(item, query); - if (Array.isArray(itemStr)) - return !!itemStr.find((i) => - matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions) - ); - return matchQuery( - normalize(itemStr, options?.normalizeOptions), - query, - options?.matchOptions - ); + + const strWithMatch = performMatch(itemStr, query, options); + return typeof strWithMatch === 'string'; }; const handleResult: ResultHandler = (results, query) => setResult({ query, - items: [...results], + items: orderSearchItems(query, results, getItemStr, options), }); return AsyncSearch(list, handleMatch, handleResult, options);