1
0
Fork 1
mirror of https://github.com/elk-zone/elk.git synced 2024-11-05 16:39:58 +00:00

feat(publish): add hashtag autocomplete (#778)

This commit is contained in:
Piotrek Tomczewski 2023-01-04 21:47:29 +01:00 committed by GitHub
parent 9d7b7b66ed
commit 1ff584bf8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 94 additions and 20 deletions

View file

@ -0,0 +1,68 @@
<script setup lang="ts">
import type { Tag } from 'masto'
import CommonScrollIntoView from '../common/CommonScrollIntoView.vue'
import HashtagInfo from '../search/HashtagInfo.vue'
const { items, command } = defineProps<{
items: Tag[]
command: Function
isPending?: boolean
}>()
let selectedIndex = $ref(0)
watch(items, () => {
selectedIndex = 0
})
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'ArrowUp') {
selectedIndex = ((selectedIndex + items.length) - 1) % items.length
return true
}
else if (event.key === 'ArrowDown') {
selectedIndex = (selectedIndex + 1) % items.length
return true
}
else if (event.key === 'Enter') {
selectItem(selectedIndex)
return true
}
return false
}
function selectItem(index: number) {
const item = items[index]
if (item)
command({ id: item.name })
}
defineExpose({
onKeyDown,
})
</script>
<template>
<div v-if="isPending || items.length" relative bg-base text-base shadow border="~ base rounded" text-sm py-2 overflow-x-hidden overflow-y-auto max-h-100>
<template v-if="isPending">
<div flex gap-1 items-center p2 animate-pulse>
<div i-ri:loader-2-line animate-spin />
<span>Fetching...</span>
</div>
</template>
<template v-if="items.length">
<CommonScrollIntoView
v-for="(item, index) in items" :key="index"
:active="index === selectedIndex"
as="button"
:class="index === selectedIndex ? 'bg-active' : 'text-secondary'"
block m0 w-full text-left px2 py1
@click="selectItem(index)"
>
<HashtagInfo :hashtag="item" />
</CommonScrollIntoView>
</template>
</div>
<div v-else />
</template>

View file

@ -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,

View file

@ -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<SuggestionOptions> = {
pluginKey: new PluginKey('mention'),
@ -17,29 +19,32 @@ export const MentionSuggestion: Partial<SuggestionOptions> = {
return results.value.accounts
},
render: createSuggestionRenderer(),
render: createSuggestionRenderer(TiptapMentionList),
}
export const HashSuggestion: Partial<SuggestionOptions> = {
export const HashtagSuggestion: Partial<SuggestionOptions> = {
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()
},
}
}

View file

@ -6,6 +6,7 @@
opacity: 0.4;
}
span[data-type='mention'] {
span[data-type='mention'],
span[data-type='hashtag'] {
--at-apply: text-primary;
}