feat(editor): link insertion full UI and flow
This commit is contained in:
1 parent
03b39e64f2
commit
6f9ae6207a
16 files changed
+2865
-14
No files matched your search
@@ -11,6 +11,7 @@ import ArticleEditor from '@/components/ArticleEditor.vue'
|
||||
import ArticleDetails from '@/components/ArticleDetails.vue'
|
||||
import SaveStatus from '@/components/SaveStatus.vue'
|
||||
import PublishControl from '@/components/PublishControl.vue'
|
||||
import { neutralizeUnsafeLinks } from '@/lib/articleLinkUrl'
|
||||
import EditorStatusBar from '@/components/EditorStatusBar.vue'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import ImageUploader from '@/components/ImageUploader.vue'
|
||||
@@ -90,6 +91,25 @@ async function loadArticle() {
|
||||
}
|
||||
|
||||
const counts = computed(() => bodyCounts(draft.body.value))
|
||||
const previewBody = computed(() => neutralizeUnsafeLinks(draft.body.value))
|
||||
const countText = computed(() =>
|
||||
t('{0} words · {1} characters', [
|
||||
counts.value.words.toLocaleString('en-US'),
|
||||
counts.value.characters.toLocaleString('en-US'),
|
||||
]),
|
||||
)
|
||||
const overflowItems = computed(() => [
|
||||
[
|
||||
{
|
||||
icon: 'i-lucide-trash-2',
|
||||
color: 'error' as const,
|
||||
label: t('Delete article'),
|
||||
onSelect: () => {
|
||||
handleDelete()
|
||||
},
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const saveEnabled = computed(() => draft.state.value === 'dirty' && loadState.value === 'loaded')
|
||||
|
||||
@@ -403,7 +423,24 @@ onBeforeRouteLeave(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DashboardShell :mobile-title="t('Edit article')" full-bleed>
|
||||
<DashboardShell
|
||||
:mobile-title="t('Edit article')"
|
||||
back-to="/article"
|
||||
:back-label="t('Back to Articles')"
|
||||
full-bleed
|
||||
>
|
||||
<template v-if="loadState === 'loaded'" #navbar-right>
|
||||
<UDropdownMenu :items="overflowItems">
|
||||
<UButton
|
||||
icon="i-lucide-ellipsis"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
class="size-11 justify-center lg:hidden"
|
||||
:ui="{ leadingIcon: 'size-5' }"
|
||||
:aria-label="t('More actions')"
|
||||
/>
|
||||
</UDropdownMenu>
|
||||
</template>
|
||||
<template v-if="loadState === 'loaded'" #breadcrumb>
|
||||
<UBreadcrumb
|
||||
:items="[
|
||||
@@ -467,17 +504,19 @@ onBeforeRouteLeave(async () => {
|
||||
|
||||
<div v-else-if="loadState === 'loaded' && article" class="flex min-h-0 flex-1 flex-col">
|
||||
<div
|
||||
class="flex flex-shrink-0 flex-wrap items-center gap-3 border-b border-default bg-default px-6 py-3.5"
|
||||
class="flex flex-shrink-0 flex-wrap items-center gap-x-3 gap-y-2 border-b border-default bg-default px-4 py-3 lg:gap-3 lg:px-6 lg:py-3.5"
|
||||
>
|
||||
<UButton
|
||||
icon="i-lucide-arrow-left"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
class="size-8 justify-center text-toned"
|
||||
class="hidden size-8 justify-center text-toned lg:inline-flex"
|
||||
:aria-label="t('Back to Articles')"
|
||||
@click="goBack"
|
||||
/>
|
||||
<div class="flex min-w-50 flex-1 flex-wrap items-center gap-2.5">
|
||||
<div
|
||||
class="flex min-w-0 flex-1 basis-full flex-wrap items-center gap-2.5 lg:min-w-50 lg:basis-50"
|
||||
>
|
||||
<h1
|
||||
class="text-[1.125rem]/6.5 font-semibold text-highlighted"
|
||||
:title="t('The title can\'t be changed after creation.')"
|
||||
@@ -491,6 +530,7 @@ onBeforeRouteLeave(async () => {
|
||||
:saved-at="draft.savedAt.value"
|
||||
@retry="retrySave"
|
||||
/>
|
||||
<span class="ml-auto font-mono text-xs text-muted lg:hidden">{{ countText }}</span>
|
||||
<div class="hidden items-center gap-3 lg:flex">
|
||||
<USwitch
|
||||
v-model="previewOn"
|
||||
@@ -545,7 +585,7 @@ onBeforeRouteLeave(async () => {
|
||||
<h2 class="text-2xl font-semibold text-highlighted">
|
||||
{{ article.title }}
|
||||
</h2>
|
||||
<div v-html="draft.body.value" />
|
||||
<div v-html="previewBody" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -566,6 +606,7 @@ onBeforeRouteLeave(async () => {
|
||||
>
|
||||
<template #footer>
|
||||
<EditorStatusBar
|
||||
class="max-lg:hidden"
|
||||
:words="counts.words"
|
||||
:characters="counts.characters"
|
||||
:state="draft.state.value"
|
||||
|
||||
Reference in new issue
Block a user