First commit
This commit is contained in:
143
frontend/app/components/ArticleLanguageMenu.vue
Normal file
143
frontend/app/components/ArticleLanguageMenu.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
/* The reader's language menu: one control for turning the article to another
|
||||
language — and for writing one it doesn't exist in yet. Every language the
|
||||
wiki keeps is listed: the ones this article speaks navigate to that
|
||||
version, the rest lead straight into the translate editor with the language
|
||||
already chosen. */
|
||||
import type { ArticleVersion } from './ArticleCard.vue'
|
||||
|
||||
const props = 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 translate flow answers to: the canonical document's, or
|
||||
this version's own when the canonical one is gone */
|
||||
editorSlug: string
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const { open, hide } = useLanguageMenu()
|
||||
const { open: searchOpen } = useSearch()
|
||||
const { open: settingsOpen } = useSettings()
|
||||
const { open: profileOpen } = useProfileMenu()
|
||||
const { loggedIn } = useAuth()
|
||||
|
||||
/* the languages this article exists in, and the ones still left to write —
|
||||
writing takes an account, so a visitor who isn't signed in only ever sees
|
||||
the versions there are to read */
|
||||
const existing = computed(() => props.versions)
|
||||
const missing = computed(() =>
|
||||
loggedIn.value
|
||||
? LANGUAGES.filter((l) => !props.versions.some((v) => (v.language || 'en') === l.code))
|
||||
: []
|
||||
)
|
||||
|
||||
function toggle() {
|
||||
open.value = !open.value
|
||||
}
|
||||
|
||||
/* the menu is one of the app's popups: closed by a click outside, by esc, by
|
||||
navigating, and by search or settings opening over it */
|
||||
const wrapEl = ref<HTMLElement | null>(null)
|
||||
|
||||
function onDocClick(e: MouseEvent) {
|
||||
if (open.value && !wrapEl.value?.contains(e.target as Node)) hide()
|
||||
}
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && open.value) hide()
|
||||
}
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onDocClick)
|
||||
window.addEventListener('keydown', onKeydown)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onDocClick)
|
||||
window.removeEventListener('keydown', onKeydown)
|
||||
})
|
||||
|
||||
watch(() => route.fullPath, hide)
|
||||
watch([searchOpen, settingsOpen, profileOpen], ([search, settings, profile]) => {
|
||||
if (search || settings || profile) hide()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="wrapEl" class="lang-menu">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm"
|
||||
:aria-expanded="String(open)"
|
||||
:title="loggedIn ? t('langMenu.hintSignedIn') : t('langMenu.hint')"
|
||||
@click="toggle"
|
||||
>
|
||||
<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">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M2 12h20" />
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||
</svg>
|
||||
<span :lang="language">{{ languageName(language) }}</span>
|
||||
<svg class="lang-menu-caret" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div v-if="open" class="lang-menu-dropdown" role="menu" :aria-label="t('langMenu.aria')">
|
||||
<template v-if="existing.length">
|
||||
<div class="lang-menu-section">{{ t('langMenu.readIn') }}</div>
|
||||
<NuxtLink
|
||||
v-for="v in existing"
|
||||
:key="v.slug"
|
||||
:to="versionHref(identity, v)"
|
||||
class="lang-menu-item"
|
||||
:lang="v.language"
|
||||
role="menuitem"
|
||||
:aria-current="v.slug === currentSlug ? 'true' : undefined"
|
||||
@click="hide"
|
||||
>
|
||||
<span class="lmi-name">{{ languageName(v.language) }}</span>
|
||||
<span class="lmi-sub">{{ v.title }}</span>
|
||||
<svg
|
||||
v-if="v.slug === currentSlug"
|
||||
class="lmi-check"
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<template v-if="missing.length">
|
||||
<div class="lang-menu-section" :class="{ 'lang-menu-gap': existing.length }">
|
||||
{{ t('langMenu.writeIn') }}
|
||||
</div>
|
||||
<NuxtLink
|
||||
v-for="l in missing"
|
||||
:key="l.code"
|
||||
:to="{ path: '/new', query: { translate: editorSlug, lang: l.code } }"
|
||||
class="lang-menu-item lang-menu-write"
|
||||
:lang="l.code"
|
||||
role="menuitem"
|
||||
:title="t('langMenu.writeArticleIn', { name: l.name })"
|
||||
@click="hide"
|
||||
>
|
||||
<span class="lmi-name">{{ l.name }}</span>
|
||||
<svg class="lmi-pencil" width="12" height="12" 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>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user