feat(editor): add EmojiList popup component

This commit is contained in:
kolaente 2026-04-14 13:01:46 +02:00 committed by kolaente
parent 542cab5ef6
commit 88136ed45e
2 changed files with 155 additions and 0 deletions

View File

@ -0,0 +1,152 @@
<template>
<div class="emoji-items">
<template v-if="items.length">
<button
v-for="(item, index) in items"
:key="item.shortcode"
:ref="el => setItemRef(el, index)"
type="button"
class="emoji-item"
:class="{ 'is-selected': index === selectedIndex }"
@click="selectItem(index)"
>
<span class="emoji-glyph">{{ item.emoji }}</span>
<div class="emoji-info">
<p class="emoji-shortcode">:{{ item.shortcode }}:</p>
<p class="emoji-annotation">{{ item.annotation }}</p>
</div>
</button>
</template>
<div
v-else
class="emoji-item no-results"
>
{{ $t('input.editor.emoji.empty') }}
</div>
</div>
</template>
<script lang="ts" setup>
import {ref, watch, nextTick} from 'vue'
import type {EmojiEntry} from './emojiData'
const props = defineProps<{
items: EmojiEntry[]
command: (item: EmojiEntry) => void
}>()
const selectedIndex = ref(0)
const itemEls = ref<HTMLElement[]>([])
function setItemRef(el: Element | null, index: number) {
if (el instanceof HTMLElement) {
itemEls.value[index] = el
}
}
watch(() => props.items, () => {
selectedIndex.value = 0
itemEls.value = []
})
watch(selectedIndex, async idx => {
await nextTick()
itemEls.value[idx]?.scrollIntoView({block: 'nearest'})
})
function selectItem(index: number) {
const item = props.items[index]
if (item) props.command(item)
}
function onKeyDown({event}: {event: KeyboardEvent}): boolean {
if (props.items.length === 0) return false
if (event.key === 'ArrowUp') {
selectedIndex.value = ((selectedIndex.value + props.items.length) - 1) % props.items.length
return true
}
if (event.key === 'ArrowDown') {
selectedIndex.value = (selectedIndex.value + 1) % props.items.length
return true
}
if (event.key === 'Enter' || event.key === 'Tab') {
if (event.isComposing) return false
selectItem(selectedIndex.value)
return true
}
return false
}
defineExpose({onKeyDown})
</script>
<style lang="scss" scoped>
.emoji-items {
padding: 0.2rem;
position: relative;
border-radius: 0.5rem;
background: var(--white);
color: var(--grey-900);
overflow: hidden;
font-size: 0.9rem;
box-shadow: var(--shadow-md);
min-inline-size: 240px;
max-block-size: 300px;
overflow-y: auto;
}
.emoji-item {
display: flex;
align-items: center;
margin: 0;
inline-size: 100%;
text-align: start;
background: transparent;
border-radius: $radius;
border: 0;
padding: 0.4rem 0.6rem;
transition: background-color $transition;
&.is-selected, &:hover {
background: var(--grey-100);
cursor: pointer;
}
&.no-results {
color: var(--grey-500);
cursor: default;
}
}
.emoji-glyph {
font-size: 1.4rem;
margin-inline-end: 0.75rem;
flex-shrink: 0;
}
.emoji-info {
display: flex;
flex-direction: column;
min-inline-size: 0;
flex: 1;
p {
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.emoji-shortcode {
font-family: monospace;
font-weight: 500;
color: var(--grey-800);
}
.emoji-annotation {
font-size: 0.75rem;
color: var(--grey-500);
}
</style>

View File

@ -844,6 +844,9 @@
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
},
"emoji": {
"empty": "No emoji found"
}
},
"multiselect": {