Skip to content

Commit 38765c3

Browse files
feat(Editor): new component (#5407)
Co-authored-by: Baptiste Leproux <[email protected]>
1 parent cb3cec2 commit 38765c3

File tree

104 files changed

+7921
-209
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+7921
-209
lines changed

docs/app/assets/icons/tiptap.svg

Lines changed: 7 additions & 0 deletions
Loading

docs/app/components/VersionMenu.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const items = computed(() => {
2525
trailing-icon="i-lucide-chevron-down"
2626
size="xs"
2727
class="-mb-[6px] font-semibold rounded-full truncate"
28-
:class="[open && 'bg-primary/15 ']"
28+
:class="[open && 'bg-primary/15']"
2929
:ui="{
3030
trailingIcon: ['transition-transform duration-200', open ? 'rotate-180' : undefined].filter(Boolean).join(' ')
3131
}"

docs/app/components/content/ComponentCode.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ const props = defineProps<{
9797
* Whether to add overflow-hidden to wrapper
9898
*/
9999
overflowHidden?: boolean
100+
/**
101+
* Whether to add background-elevated to wrapper
102+
*/
103+
elevated?: boolean
100104
}>()
101105
102106
const route = useRoute()
@@ -416,7 +420,7 @@ const { data: ast } = await useAsyncData(codeKey, async () => {
416420
</template>
417421
</div>
418422

419-
<div v-if="component" class="flex justify-center border border-b-0 border-muted relative p-4 z-[1]" :class="[!options.length && 'rounded-t-md', props.class, { 'overflow-hidden': props.overflowHidden }]">
423+
<div v-if="component" class="flex justify-center border border-b-0 border-muted relative p-4 z-[1]" :class="[!options.length && 'rounded-t-md', props.class, { 'overflow-hidden': props.overflowHidden, 'dark:bg-neutral-950/50': props.elevated }]">
420424
<component :is="component" v-bind="{ ...componentProps, ...componentEvents }">
421425
<template v-for="slot in Object.keys(slots || {})" :key="slot" #[slot]>
422426
<slot :name="slot" mdc-unwrap="p">

docs/app/components/content/ComponentExample.vue

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,15 @@ const props = withDefaults(defineProps<{
5959
* Whether to add overflow-hidden to wrapper
6060
*/
6161
overflowHidden?: boolean
62+
/**
63+
* Whether to add background-elevated to wrapper
64+
*/
65+
elevated?: boolean
66+
lang?: string
6267
}>(), {
6368
preview: true,
64-
source: true
69+
source: true,
70+
lang: 'vue'
6571
})
6672
6773
const slots = defineSlots<{
@@ -88,7 +94,7 @@ const code = computed(() => {
8894
`
8995
}
9096
91-
code += `\`\`\`vue ${props.preview ? '' : ` [${data.pascalName}.vue]`}${props.highlights?.length ? `{${props.highlights.join('-')}}` : ''}
97+
code += `\`\`\`${props.lang} ${props.preview ? '' : ` [${data.pascalName}.${props.lang}]`}${props.highlights?.length ? `{${props.highlights.join('-')}}` : ''}
9298
${data?.code ?? ''}
9399
\`\`\``
94100
@@ -208,9 +214,9 @@ const urlSearchParams = computed(() => {
208214
v-bind="typeof iframe === 'object' ? iframe : {}"
209215
:src="`/examples/${name}?${urlSearchParams}`"
210216
class="relative w-full"
211-
:class="[props.class, !iframeMobile && 'lg:left-1/2 lg:-translate-x-1/2 lg:w-[1024px]']"
217+
:class="[props.class, { 'dark:bg-neutral-950/50 rounded-t-md': props.elevated }, !iframeMobile && 'lg:left-1/2 lg:-translate-x-1/2 lg:w-[1024px]']"
212218
/>
213-
<div v-else class="flex justify-center p-4" :class="props.class">
219+
<div v-else class="flex justify-center p-4" :class="[props.class, { 'dark:bg-neutral-950/50 rounded-t-md': props.elevated }]">
214220
<component :is="camelName" v-bind="{ ...componentProps, ...optionsValues }" />
215221
</div>
216222
</div>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<script setup lang="ts">
2+
import { upperFirst } from 'scule'
3+
import type { DropdownMenuItem } from '@nuxt/ui'
4+
import { mapEditorItems } from '@nuxt/ui/utils/editor'
5+
import type { Editor, JSONContent } from '@tiptap/vue-3'
6+
7+
const value = ref(`Hover over the left side to see both drag handle and menu button.
8+
9+
Click the menu to see block actions. Try duplicating or deleting a block.`)
10+
11+
const selectedNode = ref<{ node: JSONContent, pos: number }>()
12+
13+
const items = (editor: Editor): DropdownMenuItem[][] => {
14+
if (!selectedNode.value?.node?.type) {
15+
return []
16+
}
17+
18+
return mapEditorItems(editor, [[
19+
{
20+
type: 'label',
21+
label: upperFirst(selectedNode.value.node.type)
22+
},
23+
{
24+
label: 'Turn into',
25+
icon: 'i-lucide-repeat-2',
26+
children: [
27+
{ kind: 'paragraph', label: 'Paragraph', icon: 'i-lucide-type' },
28+
{ kind: 'heading', level: 1, label: 'Heading 1', icon: 'i-lucide-heading-1' },
29+
{ kind: 'heading', level: 2, label: 'Heading 2', icon: 'i-lucide-heading-2' },
30+
{ kind: 'heading', level: 3, label: 'Heading 3', icon: 'i-lucide-heading-3' },
31+
{ kind: 'heading', level: 4, label: 'Heading 4', icon: 'i-lucide-heading-4' },
32+
{ kind: 'bulletList', label: 'Bullet List', icon: 'i-lucide-list' },
33+
{ kind: 'orderedList', label: 'Ordered List', icon: 'i-lucide-list-ordered' },
34+
{ kind: 'blockquote', label: 'Blockquote', icon: 'i-lucide-text-quote' },
35+
{ kind: 'codeBlock', label: 'Code Block', icon: 'i-lucide-square-code' }
36+
]
37+
},
38+
{
39+
kind: 'clearFormatting',
40+
pos: selectedNode.value?.pos,
41+
label: 'Reset formatting',
42+
icon: 'i-lucide-rotate-ccw'
43+
}
44+
], [
45+
{
46+
kind: 'duplicate',
47+
pos: selectedNode.value?.pos,
48+
label: 'Duplicate',
49+
icon: 'i-lucide-copy'
50+
},
51+
{
52+
label: 'Copy to clipboard',
53+
icon: 'i-lucide-clipboard',
54+
onSelect: async () => {
55+
if (!selectedNode.value) return
56+
57+
const pos = selectedNode.value.pos
58+
const node = editor.state.doc.nodeAt(pos)
59+
if (node) {
60+
await navigator.clipboard.writeText(node.textContent)
61+
}
62+
}
63+
}
64+
], [
65+
{
66+
kind: 'moveUp',
67+
pos: selectedNode.value?.pos,
68+
label: 'Move up',
69+
icon: 'i-lucide-arrow-up'
70+
},
71+
{
72+
kind: 'moveDown',
73+
pos: selectedNode.value?.pos,
74+
label: 'Move down',
75+
icon: 'i-lucide-arrow-down'
76+
}
77+
], [
78+
{
79+
kind: 'delete',
80+
pos: selectedNode.value?.pos,
81+
label: 'Delete',
82+
icon: 'i-lucide-trash'
83+
}
84+
]]) as DropdownMenuItem[][]
85+
}
86+
</script>
87+
88+
<template>
89+
<UEditor
90+
v-slot="{ editor }"
91+
v-model="value"
92+
content-type="markdown"
93+
class="w-full min-h-19"
94+
>
95+
<UEditorDragHandle v-slot="{ ui }" :editor="editor" @node-change="selectedNode = $event">
96+
<UDropdownMenu
97+
v-slot="{ open }"
98+
:modal="false"
99+
:items="items(editor)"
100+
:content="{ side: 'left' }"
101+
:ui="{ content: 'w-48', label: 'text-xs' }"
102+
@update:open="editor.chain().setMeta('lockDragHandle', $event).run()"
103+
>
104+
<UButton
105+
icon="i-lucide-grip-vertical"
106+
color="neutral"
107+
variant="ghost"
108+
active-variant="soft"
109+
size="sm"
110+
:active="open"
111+
:class="ui.handle()"
112+
/>
113+
</UDropdownMenu>
114+
</UEditorDragHandle>
115+
</UEditor>
116+
</template>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script setup lang="ts">
2+
const value = ref(`# Drag Handle
3+
4+
Hover over the left side of this block to see the drag handle appear and reorder blocks.`)
5+
</script>
6+
7+
<template>
8+
<UEditor
9+
v-slot="{ editor }"
10+
v-model="value"
11+
content-type="markdown"
12+
class="w-full min-h-21"
13+
>
14+
<UEditorDragHandle :editor="editor" />
15+
</UEditor>
16+
</template>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<script setup lang="ts">
2+
import type { EditorSuggestionMenuItem } from '@nuxt/ui'
3+
4+
const value = ref(`Click the plus button to open the suggestion menu and add new blocks.
5+
6+
The button appears when hovering over blocks.`)
7+
8+
const suggestionItems: EditorSuggestionMenuItem[][] = [[{
9+
kind: 'heading',
10+
level: 1,
11+
label: 'Heading 1',
12+
icon: 'i-lucide-heading-1'
13+
}, {
14+
kind: 'heading',
15+
level: 2,
16+
label: 'Heading 2',
17+
icon: 'i-lucide-heading-2'
18+
}, {
19+
kind: 'bulletList',
20+
label: 'Bullet List',
21+
icon: 'i-lucide-list'
22+
}, {
23+
kind: 'blockquote',
24+
label: 'Blockquote',
25+
icon: 'i-lucide-text-quote'
26+
}]]
27+
</script>
28+
29+
<template>
30+
<UEditor
31+
v-slot="{ editor, handlers }"
32+
v-model="value"
33+
content-type="markdown"
34+
class="w-full min-h-35"
35+
:ui="{ base: 'p-8 sm:px-16' }"
36+
>
37+
<UEditorDragHandle v-slot="{ ui, onClick }" :editor="editor">
38+
<UButton
39+
icon="i-lucide-plus"
40+
color="neutral"
41+
variant="ghost"
42+
size="sm"
43+
:class="ui.handle()"
44+
@click="(e) => {
45+
e.stopPropagation()
46+
47+
const selected = onClick()
48+
handlers.suggestion?.execute(editor, { pos: selected?.pos }).run()
49+
}"
50+
/>
51+
52+
<UButton
53+
icon="i-lucide-grip-vertical"
54+
color="neutral"
55+
variant="ghost"
56+
size="sm"
57+
:class="ui.handle()"
58+
/>
59+
</UEditorDragHandle>
60+
61+
<UEditorSuggestionMenu :editor="editor" :items="suggestionItems" />
62+
</UEditor>
63+
</template>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script setup lang="ts">
2+
import type { EditorEmojiMenuItem } from '@nuxt/ui'
3+
import { Emoji, gitHubEmojis } from '@tiptap/extension-emoji'
4+
5+
const value = ref(`# Emoji Menu
6+
7+
Type : to insert emojis and select from the list of available emojis.`)
8+
9+
const items: EditorEmojiMenuItem[] = gitHubEmojis.filter(emoji => !emoji.name.startsWith('regional_indicator_'))
10+
11+
// SSR-safe function to append menus to body (avoids z-index issues in docs)
12+
const appendToBody = import.meta.client ? () => document.body : undefined
13+
</script>
14+
15+
<template>
16+
<UEditor
17+
v-slot="{ editor }"
18+
v-model="value"
19+
:extensions="[Emoji]"
20+
content-type="markdown"
21+
placeholder="Type : to add emojis..."
22+
class="w-full min-h-21"
23+
>
24+
<UEditorEmojiMenu :editor="editor" :items="items" :append-to="appendToBody" />
25+
</UEditor>
26+
</template>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<script setup lang="ts">
2+
import type { EditorEmojiMenuItem } from '@nuxt/ui'
3+
import { Emoji } from '@tiptap/extension-emoji'
4+
5+
const value = ref(`Type : to see a custom emoji set.
6+
7+
You can also install the \`@tiptap/extension-emoji\` extension to use a comprehensive set with over 1800 emojis.`)
8+
9+
const items: EditorEmojiMenuItem[] = [{
10+
name: 'smile',
11+
emoji: '😄',
12+
shortcodes: ['smile'],
13+
tags: ['happy', 'joy', 'pleased']
14+
}, {
15+
name: 'heart',
16+
emoji: '❤️',
17+
shortcodes: ['heart'],
18+
tags: ['love', 'like']
19+
}, {
20+
name: 'thumbsup',
21+
emoji: '👍',
22+
shortcodes: ['thumbsup', '+1'],
23+
tags: ['approve', 'ok']
24+
}, {
25+
name: 'fire',
26+
emoji: '🔥',
27+
shortcodes: ['fire'],
28+
tags: ['hot', 'burn']
29+
}, {
30+
name: 'rocket',
31+
emoji: '🚀',
32+
shortcodes: ['rocket'],
33+
tags: ['ship', 'launch']
34+
}, {
35+
name: 'eyes',
36+
emoji: '👀',
37+
shortcodes: ['eyes'],
38+
tags: ['look', 'watch']
39+
}, {
40+
name: 'tada',
41+
emoji: '🎉',
42+
shortcodes: ['tada'],
43+
tags: ['party', 'celebration']
44+
}, {
45+
name: 'thinking',
46+
emoji: '🤔',
47+
shortcodes: ['thinking'],
48+
tags: ['hmm', 'think', 'consider']
49+
}]
50+
</script>
51+
52+
<template>
53+
<UEditor
54+
v-slot="{ editor }"
55+
v-model="value"
56+
:extensions="[Emoji]"
57+
content-type="markdown"
58+
placeholder="Type : to add emojis..."
59+
class="w-full min-h-26"
60+
>
61+
<UEditorEmojiMenu :editor="editor" :items="items" />
62+
</UEditor>
63+
</template>

0 commit comments

Comments
 (0)