From e847f8ef1ded6c818bd147e2398f6a3cfc494985 Mon Sep 17 00:00:00 2001 From: Yuexun Jiang Date: Mon, 16 Jan 2023 18:22:26 +0800 Subject: [PATCH] feat(tiptap): add discord-style emoji suggestion support (#1066) Co-authored-by: Daniel Roe --- components/search/SearchEmojiInfo.vue | 29 +++++++++ components/tiptap/TiptapEmojiList.vue | 92 +++++++++++++++++++++++++++ composables/tiptap.ts | 5 ++ composables/tiptap/suggestion.ts | 46 ++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 components/search/SearchEmojiInfo.vue create mode 100644 components/tiptap/TiptapEmojiList.vue diff --git a/components/search/SearchEmojiInfo.vue b/components/search/SearchEmojiInfo.vue new file mode 100644 index 00000000..82aa5807 --- /dev/null +++ b/components/search/SearchEmojiInfo.vue @@ -0,0 +1,29 @@ + + + diff --git a/components/tiptap/TiptapEmojiList.vue b/components/tiptap/TiptapEmojiList.vue new file mode 100644 index 00000000..56fdb320 --- /dev/null +++ b/components/tiptap/TiptapEmojiList.vue @@ -0,0 +1,92 @@ + + + diff --git a/composables/tiptap.ts b/composables/tiptap.ts index d623315a..9b05f66b 100644 --- a/composables/tiptap.ts +++ b/composables/tiptap.ts @@ -58,6 +58,11 @@ export function useTiptap(options: UseTiptapOptions) { .configure({ suggestion: HashtagSuggestion, }), + Mention + .extend({ name: 'emoji' }) + .configure({ + suggestion: EmojiSuggestion, + }), Placeholder.configure({ placeholder: () => placeholder.value!, }), diff --git a/composables/tiptap/suggestion.ts b/composables/tiptap/suggestion.ts index f81ef141..cd0708dd 100644 --- a/composables/tiptap/suggestion.ts +++ b/composables/tiptap/suggestion.ts @@ -4,8 +4,17 @@ import { VueRenderer } from '@tiptap/vue-3' import type { SuggestionOptions } from '@tiptap/suggestion' import { PluginKey } from 'prosemirror-state' import type { Component } from 'vue' +import type { Emoji, EmojiMartData } from '@emoji-mart/data' +import type { mastodon } from 'masto' +import { currentCustomEmojis, updateCustomEmojis } from '~/composables/emojis' import TiptapMentionList from '~/components/tiptap/TiptapMentionList.vue' import TiptapHashtagList from '~/components/tiptap/TiptapHashtagList.vue' +import TiptapEmojiList from '~/components/tiptap/TiptapEmojiList.vue' + +export { Emoji } + +export type CustomEmoji = (mastodon.v1.CustomEmoji & { custom: true }) +export const isCustomEmoji = (emoji: CustomEmoji | Emoji): emoji is CustomEmoji => !!(emoji as CustomEmoji).custom export const MentionSuggestion: Partial = { pluginKey: new PluginKey('mention'), @@ -39,6 +48,43 @@ export const HashtagSuggestion: Partial = { render: createSuggestionRenderer(TiptapHashtagList), } +export const EmojiSuggestion: Partial = { + pluginKey: new PluginKey('emoji'), + char: ':', + async items({ query }): Promise<(CustomEmoji | Emoji)[]> { + if (query.length === 0) + return [] + + if (currentCustomEmojis.value.emojis.length === 0) + await updateCustomEmojis() + + const emojis = await import('@emoji-mart/data') + .then(r => r.default as EmojiMartData) + .then(data => Object.values(data.emojis).filter(({ id }) => id.startsWith(query))) + + const customEmojis: CustomEmoji[] = currentCustomEmojis.value.emojis + .filter(emoji => emoji.shortcode.startsWith(query)) + .map(emoji => ({ ...emoji, custom: true })) + return [...emojis, ...customEmojis] + }, + command: ({ editor, props, range }) => { + const emoji: CustomEmoji | Emoji = props.emoji + editor.commands.deleteRange(range) + if (isCustomEmoji(emoji)) { + editor.commands.insertCustomEmoji({ + title: emoji.shortcode, + src: emoji.url, + }) + } + else { + const skin = emoji.skins.find(skin => skin.native !== undefined) + if (skin) + editor.commands.insertEmoji(skin.native) + } + }, + render: createSuggestionRenderer(TiptapEmojiList), +} + function createSuggestionRenderer(component: Component): SuggestionOptions['render'] { return () => { let renderer: VueRenderer