forked from BRETT/BRETT
Initial Commit
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { activeMention } from '~~/shared/utils/mentions'
|
||||
import type { MentionUser } from '~~/shared/utils/mentions'
|
||||
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const model = defineModel<string>({ required: true })
|
||||
const props = defineProps<{ placeholder?: string }>()
|
||||
const { data: users } = useMentionUsers()
|
||||
const listId = useId()
|
||||
const caret = shallowRef(0)
|
||||
const focused = shallowRef(false)
|
||||
const dismissed = shallowRef(false)
|
||||
const selected = shallowRef(0)
|
||||
let textarea: HTMLTextAreaElement | null = null
|
||||
const mention = computed(() => activeMention(model.value, caret.value))
|
||||
const suggestions = computed(() => {
|
||||
if (!mention.value) return []
|
||||
const query = mention.value.query.toLowerCase()
|
||||
return users.value.filter(u => u.name.toLowerCase().startsWith(query)).slice(0, 8)
|
||||
})
|
||||
const open = computed(() => focused.value && !dismissed.value && suggestions.value.length > 0)
|
||||
|
||||
function updateCaret(event: Event) {
|
||||
textarea = event.target as HTMLTextAreaElement
|
||||
caret.value = textarea.selectionStart
|
||||
focused.value = true
|
||||
dismissed.value = false
|
||||
selected.value = 0
|
||||
}
|
||||
|
||||
async function choose(user: MentionUser) {
|
||||
if (!mention.value || !textarea) return
|
||||
const { start, end } = mention.value
|
||||
const suffix = model.value.slice(end).replace(/^[\p{L}\p{N}_-]*/u, '')
|
||||
const insertion = `@${user.name} `
|
||||
model.value = model.value.slice(0, start) + insertion + suffix
|
||||
dismissed.value = true
|
||||
await nextTick()
|
||||
textarea.focus({ preventScroll: true })
|
||||
textarea.setSelectionRange(start + insertion.length, start + insertion.length)
|
||||
caret.value = start + insertion.length
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (!open.value || event.isComposing) return
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
selected.value = (selected.value + (event.key === 'ArrowDown' ? 1 : -1) + suggestions.value.length) % suggestions.value.length
|
||||
}
|
||||
else if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
const user = suggestions.value[selected.value]
|
||||
if (user) choose(user)
|
||||
}
|
||||
else if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
dismissed.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover :open="open" @update:open="value => { if (!value) dismissed = true }">
|
||||
<PopoverAnchor as-child>
|
||||
<div class="relative">
|
||||
<Textarea
|
||||
v-model="model"
|
||||
:placeholder="props.placeholder"
|
||||
class="min-h-32 pb-9"
|
||||
aria-label="Kommentar"
|
||||
aria-autocomplete="list"
|
||||
:aria-expanded="open"
|
||||
:aria-controls="open ? listId : undefined"
|
||||
:aria-activedescendant="open ? `${listId}-${selected}` : undefined"
|
||||
@input="updateCaret"
|
||||
@click="updateCaret"
|
||||
@keyup="event => { if (!['ArrowUp', 'ArrowDown', 'Enter', 'Escape'].includes(event.key)) updateCaret(event) }"
|
||||
@focus="updateCaret"
|
||||
@blur="focused = false"
|
||||
@keydown="onKeydown"
|
||||
/>
|
||||
<slot />
|
||||
</div>
|
||||
</PopoverAnchor>
|
||||
<PopoverContent
|
||||
side="top"
|
||||
align="start"
|
||||
class="gap-1 rounded-md p-1"
|
||||
@open-auto-focus.prevent
|
||||
@close-auto-focus.prevent
|
||||
@interact-outside="event => { if (event.target === textarea) event.preventDefault() }"
|
||||
>
|
||||
<div :id="listId" role="listbox" aria-label="Benutzer erwähnen">
|
||||
<button
|
||||
v-for="(user, index) in suggestions"
|
||||
:id="`${listId}-${index}`"
|
||||
:key="user.id"
|
||||
type="button"
|
||||
role="option"
|
||||
:aria-selected="index === selected"
|
||||
:class="cn('flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-2 text-left', index === selected && 'bg-accent text-accent-foreground')"
|
||||
@pointerdown.prevent
|
||||
@pointermove="selected = index"
|
||||
@click="choose(user)"
|
||||
>
|
||||
<Icon name="ph:user-fill" class="size-4 shrink-0" :style="user.color ? { color: user.color } : undefined" />
|
||||
<span class="truncate">@{{ user.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
Reference in New Issue
Block a user