mirror of
https://github.com/docmost/docmost.git
synced 2026-05-19 16:04:17 +08:00
6046d04375
* feat(editor): show emoji name in suggestion list Replace the fixed-column emoji grid with a vertical list that displays each emoji alongside its :shortcode: name. This makes the picker more discoverable—users can see and learn shortcodes without prior knowledge. Changes: - EmojiList: switch from SimpleGrid/ActionIcon to UnstyledButton list rows showing emoji glyph + monospace 🆔 label - Navigation simplified to ArrowUp/ArrowDown (list has no columns) - Results capped at 8 items for a focused, scannable dropdown - CSS module: rename menuBtn -> menuItem, tighten padding * feat(editor): replace SearchIndex with name/id includes search Port the exact search algorithm from the original extension: - Build a flat index from @emoji-mart/data: { id, name (lowercase), native } - Filter with name.includes(q) || id.includes(q) — predictable, no keyword indirection - Results capped at 5 (same as extension) - Frequently-used emojis (sorted by usage) shown when query is empty - Remove emoji-mart init() / SearchIndex / getEmojiDataFromNative dependencies; index is built lazily and cached in memory - Remove unused GRID_COLUMNS constant * feat(editor): emoji picker with browse and search modes When the query is empty the picker shows a category bar with 8 tabs (people, nature, food…) and a scrollable emoji grid. Typing after ':' switches to a compact list that shows the glyph and :shortcode: side by side, making it easy to discover emoji names while you type. - Category data is loaded lazily from @emoji-mart/data and cached, so opening the picker more than once has no overhead - Grid keyboard nav: arrow keys move by cell/row, Enter picks - List keyboard nav: up/down through results, Enter picks - Mouse hover syncs the keyboard selection index in both modes - incrementEmojiUsage tracks picks so frequently used ones bubble up in future sessions * fix(editor): polish emoji picker copy and loading * feat: add emoji to slash command * Add keyboard support to emoji group navigation --------- Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
91 lines
2.9 KiB
TypeScript
91 lines
2.9 KiB
TypeScript
import { CommandProps, EmojiMartFrequentlyType, EmojiMenuItemType } from "./types";
|
|
|
|
export const LOCAL_STORAGE_FREQUENT_KEY = "emoji-mart.frequently";
|
|
|
|
export const DEFAULT_FREQUENTLY_USED_EMOJI_MART = `{
|
|
"+1": 10,
|
|
"grinning": 9,
|
|
"kissing_heart": 8,
|
|
"heart_eyes": 7,
|
|
"laughing": 6,
|
|
"stuck_out_tongue_winking_eye": 5,
|
|
"sweat_smile": 4,
|
|
"joy": 3,
|
|
"scream": 2,
|
|
"rocket": 1
|
|
}`;
|
|
|
|
export type EmojiIndexEntry = { id: string; name: string; native: string };
|
|
|
|
let _emojiIndex: EmojiIndexEntry[] | null = null;
|
|
|
|
export const buildEmojiIndex = async (): Promise<EmojiIndexEntry[]> => {
|
|
if (_emojiIndex) return _emojiIndex;
|
|
const { default: data } = await import("@emoji-mart/data");
|
|
_emojiIndex = (Object.values((data as any).emojis) as any[])
|
|
.filter((e) => e.id && e.name && e.skins?.[0]?.native)
|
|
.map((e) => ({
|
|
id: e.id as string,
|
|
name: (e.name as string).toLowerCase(),
|
|
native: e.skins[0].native as string,
|
|
}));
|
|
return _emojiIndex;
|
|
};
|
|
|
|
export const incrementEmojiUsage = (emojiId: string) => {
|
|
const stored = JSON.parse(
|
|
localStorage.getItem(LOCAL_STORAGE_FREQUENT_KEY) || DEFAULT_FREQUENTLY_USED_EMOJI_MART,
|
|
);
|
|
stored[emojiId] = (stored[emojiId] ?? 0) + 1;
|
|
localStorage.setItem(LOCAL_STORAGE_FREQUENT_KEY, JSON.stringify(stored));
|
|
};
|
|
|
|
export const sortFrequentlyUsedEmoji = async (
|
|
frequentlyUsedEmoji: EmojiMartFrequentlyType,
|
|
): Promise<EmojiMenuItemType[]> => {
|
|
const index = await buildEmojiIndex();
|
|
const results: EmojiMenuItemType[] = Object.entries(frequentlyUsedEmoji)
|
|
.map(([id, count]): EmojiMenuItemType | null => {
|
|
const entry = index.find((e) => e.id === id);
|
|
if (!entry) return null;
|
|
return {
|
|
id,
|
|
count,
|
|
emoji: entry.native,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor.chain().focus().deleteRange(range).insertContent(entry.native + " ").run();
|
|
},
|
|
};
|
|
})
|
|
.filter((e): e is EmojiMenuItemType => e !== null);
|
|
return results.sort((a, b) => (b.count ?? 0) - (a.count ?? 0)).slice(0, 5);
|
|
};
|
|
|
|
export const getFrequentlyUsedEmoji = (): EmojiMartFrequentlyType => {
|
|
return JSON.parse(
|
|
localStorage.getItem(LOCAL_STORAGE_FREQUENT_KEY) || DEFAULT_FREQUENTLY_USED_EMOJI_MART,
|
|
);
|
|
};
|
|
|
|
export type EmojiCategory = { id: string; emojis: EmojiIndexEntry[] };
|
|
|
|
let _cats: EmojiCategory[] | null = null;
|
|
|
|
export const getEmojiCategories = async (): Promise<EmojiCategory[]> => {
|
|
if (_cats) return _cats;
|
|
const [{ default: data }, index] = await Promise.all([
|
|
import("@emoji-mart/data"),
|
|
buildEmojiIndex(),
|
|
]);
|
|
const byId = new Map(index.map((e) => [e.id, e]));
|
|
_cats = ((data as any).categories as { id: string; emojis: string[] }[])
|
|
.map((cat) => ({
|
|
id: cat.id,
|
|
emojis: cat.emojis
|
|
.map((id) => byId.get(id))
|
|
.filter((e): e is EmojiIndexEntry => !!e),
|
|
}))
|
|
.filter((c) => c.emojis.length > 0);
|
|
return _cats;
|
|
};
|