feat(editor): link insertion full UI and flow

This commit is contained in:
Florian Sylvain committed 2026-10-12 17:12:36 +02:00
1 parent 03b39e64f2
commit 6f9ae6207a
16 files changed
+2865 -14

No files matched your search

+80 -8
View File
@@ -1,9 +1,14 @@
<script setup lang="ts">
import { t } from '@/i18n'
import { ref } from 'vue'
import { computed, ref, shallowRef } from 'vue'
import type { EditorToolbarItem } from '@nuxt/ui'
import type { Editor } from '@tiptap/core'
import LinkModal from '@/components/LinkModal.vue'
import LinkPopover from '@/components/LinkPopover.vue'
import { useArticleLink } from '@/composables/useArticleLink'
import { useScrollEdges } from '@/composables/useScrollEdges'
import { ArticleLink } from '@/lib/articleLink'
import { ArticleIndent } from '@/lib/articleIndent'
import { ArticleTextAlign } from '@/lib/articleAlign'
import { ArticleImage, findArticleImagePos } from '@/lib/articleImage'
@@ -25,10 +30,18 @@ const emit = defineEmits<{
'request-remove-image': [imageId: number]
}>()
const editorInstance = ref<Editor | null>(null)
const editorInstance = shallowRef<Editor | null>(null)
const toolbarRef = ref<{ $el: HTMLElement } | null>(null)
const toolbarElement = computed(() => toolbarRef.value?.$el ?? null)
const { hiddenBefore, hiddenAfter } = useScrollEdges(toolbarElement)
const link = useArticleLink(editorInstance, () => props.disabled)
const extensions = [
ArticleIndent,
ArticleTextAlign,
ArticleLink.configure({
onRequestDialog: () => link.openDialog(),
onRequestPopover: (target) => link.openPopover(target),
}),
ArticleImage.configure({
onRequestRemove: (imageId: number) => emit('request-remove-image', imageId),
}),
@@ -276,6 +289,7 @@ function buildItems(editor: Editor): EditorToolbarItem[][] {
},
],
[
link.toolbarItem(editor),
{
kind: 'clearFormatting',
icon: 'i-lucide-remove-formatting',
@@ -342,6 +356,7 @@ defineExpose({
v-slot="{ editor }"
:model-value="props.modelValue"
:extensions="extensions"
:starter-kit="{ link: false }"
:image="false"
content-type="html"
:editable="!props.disabled"
@@ -351,18 +366,51 @@ defineExpose({
>
<span class="hidden">{{ captureEditor(editor) }}</span>
<UEditorDragHandle :editor="editor" />
<UEditorToolbar
:editor="editor"
:items="buildItems(editor)"
layout="fixed"
class="col-span-full row-start-1 min-w-0 overflow-x-auto border-b border-default bg-default px-6 py-1.5 [&>*]:shrink-0"
/>
<div class="relative col-span-full row-start-1 min-w-0">
<UEditorToolbar
ref="toolbarRef"
:editor="editor"
:items="buildItems(editor)"
layout="fixed"
class="article-toolbar min-w-0 overflow-x-auto border-b border-default bg-default px-2 py-1.5 sm:px-6 [&>*]:shrink-0"
/>
<span
v-if="hiddenBefore"
data-testid="toolbar-fade-start"
aria-hidden="true"
class="pointer-events-none absolute inset-y-0 left-0 w-6 bg-linear-to-r from-default to-transparent"
/>
<span
v-if="hiddenAfter"
data-testid="toolbar-fade-end"
aria-hidden="true"
class="pointer-events-none absolute inset-y-0 right-0 w-8 bg-linear-to-l from-default to-transparent"
/>
</div>
<div class="col-start-1 row-start-3">
<slot name="footer" />
</div>
<div class="col-start-2 row-span-2 row-start-2 hidden min-h-0 lg:block">
<slot name="aside" />
</div>
<LinkModal
v-model:open="link.dialogOpen.value"
:mode="link.dialog.value.mode"
:text="link.dialog.value.text"
:href="link.dialog.value.href"
:new-tab="link.dialog.value.newTab"
@submit="link.apply"
@remove="link.removeFromDialog"
/>
<LinkPopover
v-model:open="link.popoverOpen.value"
:href="link.popover.value.href"
:text="link.popover.value.text"
:reference="link.popover.value.element"
@edit="link.editFromPopover"
@open-link="link.openFromPopover"
@remove="link.removeFromPopover"
/>
</UEditor>
</template>
@@ -380,6 +428,30 @@ defineExpose({
padding: 2.5rem 3rem;
outline: none;
}
.article-editor :deep(.ProseMirror ul),
.article-editor :deep(.ProseMirror ol) {
padding-left: 1.75rem;
}
.article-editor :deep(.ProseMirror li) {
margin: 0;
padding-left: 0;
}
.article-editor :deep(.ProseMirror h2) {
margin-top: 2rem;
}
.article-toolbar {
scrollbar-width: none;
}
.article-toolbar::-webkit-scrollbar {
display: none;
}
@media (max-width: 39.999rem) {
.article-toolbar :deep(button) {
min-width: 2.5rem;
min-height: 2.5rem;
justify-content: center;
}
}
@media (max-width: 47.999rem) {
.article-editor :deep(.ProseMirror) {
padding: 1.5rem 1rem;