Files
mathew/frontend/app/components/ArticleActionsMenu.vue
2026-10-01 02:29:21 +02:00

133 lines
4.4 KiB
Vue

<script setup lang="ts">
/* The article's actions folded into one ⋮ button: Edit, the language menu,
and Delete — so the title row keeps its space to itself. The menu behaves
like the app's other popups: closed by a click outside, by esc (unless the
language submenu is open, since that claims the key first), by navigating,
and by search or settings opening over it. The language menu rides inside
as a menu item of its own — its list drops over the rest of this menu. */
import type { ArticleVersion } from './ArticleCard.vue'
defineProps<{
/** the article the versions belong to — its canonical slug */
identity: string
versions: ArticleVersion[]
/** the version in view and the language it is written in */
currentSlug: string
language: string
/** the slug the editor answers to */
editorSlug: string
}>()
const emit = defineEmits<{
remove: []
}>()
const route = useRoute()
const { t } = useI18n()
/* deleting is an admin's call: a member's changes to an article go through
proposals, and dropping the page outright is not among what they may do */
const { isAdmin } = useAuth()
const { open, hide } = useArticleActionsMenu()
const { open: langMenuOpen } = useLanguageMenu()
const { open: searchOpen } = useSearch()
const { open: settingsOpen } = useSettings()
const { open: profileOpen } = useProfileMenu()
function toggle() {
open.value = !open.value
}
const wrapEl = ref<HTMLElement | null>(null)
function onDocClick(e: MouseEvent) {
if (open.value && !wrapEl.value?.contains(e.target as Node)) hide()
}
/* Captured so this runs before the book's own Esc handler sees the key: while
the menu is open, esc folds it away instead of closing the page. The
language submenu sits on top of this menu, so it gets the key first. */
function onKeydown(e: KeyboardEvent) {
if (e.key !== 'Escape' || !open.value) return
if (langMenuOpen.value) return
hide()
e.stopPropagation()
}
onMounted(() => {
document.addEventListener('click', onDocClick)
window.addEventListener('keydown', onKeydown, true)
})
onBeforeUnmount(() => {
document.removeEventListener('click', onDocClick)
window.removeEventListener('keydown', onKeydown, true)
})
watch(() => route.fullPath, hide)
watch([searchOpen, settingsOpen, profileOpen], ([search, settings, profile]) => {
if (search || settings || profile) hide()
})
</script>
<template>
<div ref="wrapEl" class="kebab">
<button
type="button"
class="kebab-toggle"
:class="{ open }"
:aria-expanded="String(open)"
:title="t('articleActions.aria')"
:aria-label="t('articleActions.aria')"
@click="toggle"
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true">
<circle cx="12" cy="5" r="0.6" />
<circle cx="12" cy="12" r="0.6" />
<circle cx="12" cy="19" r="0.6" />
</svg>
</button>
<div v-if="open" class="kebab-dropdown" role="menu" :aria-label="t('articleActions.aria')">
<NuxtLink
:to="{ path: `/wiki/${editorSlug}/edit`, query: { lang: language } }"
class="kebab-item"
role="menuitem"
@click="hide"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z" />
</svg>
{{ t('common.edit') }}
</NuxtLink>
<div class="kebab-sep" />
<ArticleLanguageMenu
:identity="identity"
:versions="versions"
:current-slug="currentSlug"
:language="language"
:editor-slug="editorSlug"
/>
<template v-if="isAdmin">
<div class="kebab-sep" />
<button
type="button"
class="kebab-item kebab-danger"
role="menuitem"
@click="hide(); emit('remove')"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 6h18" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
<path d="M10 11v6M14 11v6" />
</svg>
{{ t('common.delete') }}
</button>
</template>
</div>
</div>
</template>