From 1ff584bf8bb0df54cda5965186192ce6a8a08594 Mon Sep 17 00:00:00 2001 From: Piotrek Tomczewski Date: Wed, 4 Jan 2023 21:47:29 +0100 Subject: [PATCH] feat(publish): add hashtag autocomplete (#778) --- components/tiptap/TiptapHashtagList.vue | 68 +++++++++++++++++++++++++ composables/tiptap.ts | 6 +-- composables/tiptap/suggestion.ts | 37 ++++++++------ styles/tiptap.css | 3 +- 4 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 components/tiptap/TiptapHashtagList.vue diff --git a/components/tiptap/TiptapHashtagList.vue b/components/tiptap/TiptapHashtagList.vue new file mode 100644 index 00000000..15e57c92 --- /dev/null +++ b/components/tiptap/TiptapHashtagList.vue @@ -0,0 +1,68 @@ + + + diff --git a/composables/tiptap.ts b/composables/tiptap.ts index fa610a31..fe3d9ae0 100644 --- a/composables/tiptap.ts +++ b/composables/tiptap.ts @@ -12,7 +12,7 @@ import Code from '@tiptap/extension-code' import { Plugin } from 'prosemirror-state' import type { Ref } from 'vue' -import { HashSuggestion, MentionSuggestion } from './tiptap/suggestion' +import { HashtagSuggestion, MentionSuggestion } from './tiptap/suggestion' import { CodeBlockShiki } from './tiptap/shiki' import { CustomEmoji } from './tiptap/custom-emoji' import { Emoji } from './tiptap/emoji' @@ -54,9 +54,9 @@ export function useTiptap(options: UseTiptapOptions) { suggestion: MentionSuggestion, }), Mention - .extend({ name: 'hastag' }) + .extend({ name: 'hashtag' }) .configure({ - suggestion: HashSuggestion, + suggestion: HashtagSuggestion, }), Placeholder.configure({ placeholder: placeholder.value, diff --git a/composables/tiptap/suggestion.ts b/composables/tiptap/suggestion.ts index 7b8975e7..7445164a 100644 --- a/composables/tiptap/suggestion.ts +++ b/composables/tiptap/suggestion.ts @@ -3,7 +3,9 @@ import tippy from 'tippy.js' import { VueRenderer } from '@tiptap/vue-3' import type { SuggestionOptions } from '@tiptap/suggestion' import { PluginKey } from 'prosemirror-state' +import type { Component } from 'vue' import TiptapMentionList from '~/components/tiptap/TiptapMentionList.vue' +import TiptapHashtagList from '~/components/tiptap/TiptapHashtagList.vue' export const MentionSuggestion: Partial = { pluginKey: new PluginKey('mention'), @@ -17,29 +19,32 @@ export const MentionSuggestion: Partial = { return results.value.accounts }, - render: createSuggestionRenderer(), + render: createSuggestionRenderer(TiptapMentionList), } -export const HashSuggestion: Partial = { +export const HashtagSuggestion: Partial = { pluginKey: new PluginKey('hashtag'), char: '#', - items({ query }) { - // TODO: query - return [ - 'TODO HASH QUERY', - ].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5) + async items({ query }) { + if (query.length === 0) + return [] + + const paginator = useMasto().search({ q: query, type: 'hashtags', limit: 25, resolve: true }) + const results = await paginator.next() + + return results.value.hashtags }, - render: createSuggestionRenderer(), + render: createSuggestionRenderer(TiptapHashtagList), } -function createSuggestionRenderer(): SuggestionOptions['render'] { +function createSuggestionRenderer(component: Component): SuggestionOptions['render'] { return () => { - let component: VueRenderer + let renderer: VueRenderer let popup: Instance return { onStart(props) { - component = new VueRenderer(TiptapMentionList, { + renderer = new VueRenderer(component, { props, editor: props.editor, }) @@ -50,7 +55,7 @@ function createSuggestionRenderer(): SuggestionOptions['render'] { popup = tippy(document.body, { getReferenceClientRect: props.clientRect as GetReferenceClientRect, appendTo: () => document.body, - content: component.element, + content: renderer.element, showOnCreate: true, interactive: true, trigger: 'manual', @@ -60,11 +65,11 @@ function createSuggestionRenderer(): SuggestionOptions['render'] { // Use arrow function here because Nuxt will transform it incorrectly as Vue hook causing the build to fail onBeforeUpdate: (props) => { - component.updateProps({ ...props, isPending: true }) + renderer.updateProps({ ...props, isPending: true }) }, onUpdate(props) { - component.updateProps({ ...props, isPending: false }) + renderer.updateProps({ ...props, isPending: false }) if (!props.clientRect) return @@ -79,12 +84,12 @@ function createSuggestionRenderer(): SuggestionOptions['render'] { popup?.hide() return true } - return component?.ref?.onKeyDown(props.event) + return renderer?.ref?.onKeyDown(props.event) }, onExit() { popup?.destroy() - component?.destroy() + renderer?.destroy() }, } } diff --git a/styles/tiptap.css b/styles/tiptap.css index 606e19f9..5ba87138 100644 --- a/styles/tiptap.css +++ b/styles/tiptap.css @@ -6,6 +6,7 @@ opacity: 0.4; } -span[data-type='mention'] { +span[data-type='mention'], +span[data-type='hashtag'] { --at-apply: text-primary; }