Files
outline/app/editor/components/EmojiMenu.tsx

87 lines
2.2 KiB
TypeScript

import data, { type Emoji as TEmoji } from "@emoji-mart/data";
import { init, Data } from "emoji-mart";
import FuzzySearch from "fuzzy-search";
import capitalize from "lodash/capitalize";
import sortBy from "lodash/sortBy";
import React from "react";
import { emojiMartToGemoji, snakeCase } from "@shared/editor/lib/emoji";
import { isMac } from "@shared/utils/browser";
import EmojiMenuItem from "./EmojiMenuItem";
import SuggestionsMenu, {
Props as SuggestionsMenuProps,
} from "./SuggestionsMenu";
type Emoji = {
name: string;
title: string;
emoji: string;
description: string;
attrs: { markup: string; "data-name": string };
};
init({
data,
noCountryFlags: isMac() ? false : undefined,
});
let searcher: FuzzySearch<TEmoji>;
type Props = Omit<
SuggestionsMenuProps<Emoji>,
"renderMenuItem" | "items" | "embeds" | "trigger"
>;
const EmojiMenu = (props: Props) => {
const { search = "" } = props;
if (!searcher) {
searcher = new FuzzySearch(Object.values(Data.emojis), ["search"], {
caseSensitive: false,
sort: true,
});
}
const items = React.useMemo(() => {
const n = search.toLowerCase();
return sortBy(searcher.search(n), (item) => {
const nlc = item.name.toLowerCase();
return nlc === n ? -1 : nlc.startsWith(n) ? 0 : 1;
})
.map((item) => {
// We snake_case the shortcode for backwards compatability with gemoji to
// avoid multiple formats being written into documents.
const shortcode = snakeCase(emojiMartToGemoji[item.id] || item.id);
const emoji = item.skins[0].native;
return {
name: "emoji",
title: emoji,
description: capitalize(item.name.toLowerCase()),
emoji,
attrs: { markup: shortcode, "data-name": shortcode },
};
})
.slice(0, 15);
}, [search]);
return (
<SuggestionsMenu
{...props}
trigger=":"
filterable={false}
renderMenuItem={(item, _index, options) => (
<EmojiMenuItem
onClick={options.onClick}
selected={options.selected}
title={item.description}
emoji={item.emoji}
/>
)}
items={items}
/>
);
};
export default EmojiMenu;