First commit
This commit is contained in:
127
frontend/app/components/ArticleActionsMenu.vue
Normal file
127
frontend/app/components/ArticleActionsMenu.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<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()
|
||||
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"
|
||||
/>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user