@kungal/editor-nuxt 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/nuxt.config.ts +17 -8
- package/package.json +5 -2
- package/runtime/KunEditorToolbar.vue +246 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @kungal/editor-nuxt
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5786594: Add a `#toolbar` slot and a KunUI toolbar — the toolbar is now a swappable layer.
|
|
8
|
+
|
|
9
|
+
`<KunEditor>` exposes a `#toolbar` scoped slot whose props are the command API
|
|
10
|
+
(`KunEditorToolbarApi`: `run(cmdKey, payload)` / `insertText` / `insertQuote` /
|
|
11
|
+
`insertMention` / `focus` / `adapters` / `features` / `locale`). The API is
|
|
12
|
+
computed inside the Milkdown providers and handed down, so a custom toolbar can
|
|
13
|
+
live in the consumer's tree without `useInstance`/`inject`. The hand-rolled
|
|
14
|
+
default toolbar is the slot's fallback — omit the slot and nothing changes.
|
|
15
|
+
|
|
16
|
+
`@kungal/editor-nuxt` ships `<KunEditorToolbar>` (auto-imported): the same
|
|
17
|
+
toolbar built with **KunButton / KunIcon / KunTooltip / KunPopover** (real tooltip
|
|
18
|
+
delay, popover focus/collision, a11y):
|
|
19
|
+
|
|
20
|
+
```vue
|
|
21
|
+
<KunEditor v-model="md" :adapters="adapters">
|
|
22
|
+
<template #toolbar="api"><KunEditorToolbar v-bind="api" /></template>
|
|
23
|
+
</KunEditor>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This keeps `@kungal/editor-vue` **headless / zero UI-kit dependency** (any Vue app
|
|
27
|
+
can use it with the default toolbar) while the KunUI ecosystem (forum, moyu) gets
|
|
28
|
+
native chrome — the "headless core + optional UI layer" pattern (TipTap UI,
|
|
29
|
+
BlockNote's swappable toolbar). Also exports `EMOJI` from `@kungal/editor-vue` for
|
|
30
|
+
building custom pickers.
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- Updated dependencies [5786594]
|
|
35
|
+
- @kungal/editor-vue@0.12.0
|
|
36
|
+
- @kungal/editor-core@0.12.0
|
|
37
|
+
|
|
3
38
|
## 0.11.0
|
|
4
39
|
|
|
5
40
|
### Patch Changes
|
package/nuxt.config.ts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import { defineNuxtModule, addComponent } from '@nuxt/kit'
|
|
1
|
+
import { defineNuxtModule, addComponent, createResolver } from '@nuxt/kit'
|
|
2
2
|
|
|
3
|
-
// KunEditor Nuxt layer.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// KunEditor Nuxt layer. <KunEditor> itself lives in the framework-agnostic,
|
|
4
|
+
// headless @kungal/editor-vue; this layer registers it as a Nuxt auto-import so
|
|
5
|
+
// downstream templates use `<KunEditor>` with no import (and Nuxt generates its
|
|
6
|
+
// types, keeping the tag type-checked in consumer templates).
|
|
7
|
+
//
|
|
8
|
+
// It also ships <KunEditorToolbar>: the KunUI-built toolbar for <KunEditor>'s
|
|
9
|
+
// #toolbar slot. It belongs HERE (this layer already assumes @kungal/ui-nuxt),
|
|
10
|
+
// not in headless @kungal/editor-vue — so editor-vue stays UI-kit-free while the
|
|
11
|
+
// KunUI ecosystem gets native chrome. Same shape as TipTap's optional UI pkg.
|
|
8
12
|
export default defineNuxtConfig({
|
|
9
13
|
modules: [
|
|
10
14
|
defineNuxtModule({
|
|
11
15
|
meta: { name: 'kun-editor-components' },
|
|
12
16
|
setup() {
|
|
17
|
+
const { resolve } = createResolver(import.meta.url)
|
|
13
18
|
addComponent({
|
|
14
19
|
name: 'KunEditor',
|
|
15
20
|
export: 'KunEditor',
|
|
16
21
|
filePath: '@kungal/editor-vue'
|
|
17
22
|
})
|
|
23
|
+
addComponent({
|
|
24
|
+
name: 'KunEditorToolbar',
|
|
25
|
+
filePath: resolve('./runtime/KunEditorToolbar.vue')
|
|
26
|
+
})
|
|
18
27
|
}
|
|
19
28
|
})
|
|
20
29
|
]
|
|
@@ -24,5 +33,5 @@ export default defineNuxtConfig({
|
|
|
24
33
|
// consuming app owns one stylesheet and adds `@kungal/editor-vue/style.css`
|
|
25
34
|
// alongside its @kungal/ui-tokens + @kungal/ui-vue setup — the @source scan
|
|
26
35
|
// path is node_modules-layout-specific, so only the app can write it. It also
|
|
27
|
-
// assumes @kungal/ui-nuxt is already extended (
|
|
28
|
-
// See this package's README.
|
|
36
|
+
// assumes @kungal/ui-nuxt is already extended: <KunEditorToolbar> (and the
|
|
37
|
+
// reference editor styles) render KunUI chrome. See this package's README.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungal/editor-nuxt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "KunEditor Nuxt Layer — wraps @kungal/editor-vue and auto-imports <KunEditor> so an existing Nuxt app keeps its exact DX (no import needed).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -26,13 +26,16 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"CHANGELOG.md",
|
|
28
28
|
"app",
|
|
29
|
+
"runtime",
|
|
29
30
|
"nuxt.config.ts"
|
|
30
31
|
],
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"@kungal/editor-
|
|
33
|
+
"@kungal/editor-core": "0.12.0",
|
|
34
|
+
"@kungal/editor-vue": "0.12.0"
|
|
33
35
|
},
|
|
34
36
|
"peerDependencies": {
|
|
35
37
|
"@kungal/ui-vue": "^2.7.0",
|
|
38
|
+
"@milkdown/kit": "^7.21.2",
|
|
36
39
|
"nuxt": "^4.0.0",
|
|
37
40
|
"vue": "^3.5.0"
|
|
38
41
|
},
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// A KunUI-built toolbar for <KunEditor>, dropped into its #toolbar scoped slot:
|
|
3
|
+
//
|
|
4
|
+
// <KunEditor v-model="md" :adapters="a">
|
|
5
|
+
// <template #toolbar="api"><KunEditorToolbar v-bind="api" /></template>
|
|
6
|
+
// </KunEditor>
|
|
7
|
+
//
|
|
8
|
+
// This lives in @kungal/editor-nuxt (the layer that already assumes @kungal/
|
|
9
|
+
// ui-nuxt), NOT in the headless @kungal/editor-vue — so editor-vue keeps its
|
|
10
|
+
// zero-UI-dep promise while the KunUI ecosystem (forum, moyu) gets native
|
|
11
|
+
// KunButton / KunIcon / KunTooltip / KunPopover chrome. It drives the editor
|
|
12
|
+
// purely through the slot API props (KunEditorToolbarApi) — no useInstance.
|
|
13
|
+
//
|
|
14
|
+
// KunButton/KunIcon/KunTooltip/KunPopover are auto-imported by @kungal/ui-nuxt in
|
|
15
|
+
// the consuming app; icons use the app's iconify set (e.g. lucide via @nuxt/icon).
|
|
16
|
+
import { computed, ref } from 'vue'
|
|
17
|
+
import {
|
|
18
|
+
createCodeBlockCommand,
|
|
19
|
+
insertHrCommand,
|
|
20
|
+
insertImageCommand,
|
|
21
|
+
toggleEmphasisCommand,
|
|
22
|
+
toggleInlineCodeCommand,
|
|
23
|
+
toggleStrongCommand,
|
|
24
|
+
wrapInBlockquoteCommand,
|
|
25
|
+
wrapInBulletListCommand,
|
|
26
|
+
wrapInHeadingCommand,
|
|
27
|
+
wrapInOrderedListCommand
|
|
28
|
+
} from '@milkdown/kit/preset/commonmark'
|
|
29
|
+
import { toggleStrikethroughCommand } from '@milkdown/kit/preset/gfm'
|
|
30
|
+
import {
|
|
31
|
+
insertKunSpoilerCommand,
|
|
32
|
+
toggleLatexCommand
|
|
33
|
+
} from '@kungal/editor-core/preset'
|
|
34
|
+
import { EMOJI } from '@kungal/editor-vue'
|
|
35
|
+
import type { KunEditorToolbarApi, StickerPack } from '@kungal/editor-vue'
|
|
36
|
+
|
|
37
|
+
const props = defineProps<KunEditorToolbarApi>()
|
|
38
|
+
|
|
39
|
+
const en = computed(() => props.locale.toLowerCase().startsWith('en'))
|
|
40
|
+
const t = computed(() => ({
|
|
41
|
+
bold: en.value ? 'Bold' : '加粗',
|
|
42
|
+
italic: en.value ? 'Italic' : '斜体',
|
|
43
|
+
strike: en.value ? 'Strikethrough' : '删除线',
|
|
44
|
+
code: en.value ? 'Inline code' : '行内代码',
|
|
45
|
+
h1: en.value ? 'Heading 1' : '一级标题',
|
|
46
|
+
h2: en.value ? 'Heading 2' : '二级标题',
|
|
47
|
+
h3: en.value ? 'Heading 3' : '三级标题',
|
|
48
|
+
bulletList: en.value ? 'Bullet list' : '无序列表',
|
|
49
|
+
orderedList: en.value ? 'Ordered list' : '有序列表',
|
|
50
|
+
quote: en.value ? 'Blockquote' : '引用块',
|
|
51
|
+
codeBlock: en.value ? 'Code block' : '代码块',
|
|
52
|
+
hr: en.value ? 'Divider' : '分隔线',
|
|
53
|
+
spoiler: en.value ? 'Spoiler' : '隐藏文本',
|
|
54
|
+
latex: en.value ? 'Math (LaTeX)' : '公式',
|
|
55
|
+
image: en.value ? 'Upload image' : '上传图片',
|
|
56
|
+
picker: en.value ? 'Emoji & stickers' : '表情与贴纸',
|
|
57
|
+
emoji: en.value ? 'Emoji' : '表情',
|
|
58
|
+
sticker: en.value ? 'Stickers' : '贴纸',
|
|
59
|
+
empty: en.value ? 'No stickers' : '暂无贴纸',
|
|
60
|
+
failed: en.value ? 'Failed to load stickers' : '贴纸加载失败'
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
interface Tool {
|
|
64
|
+
icon: string
|
|
65
|
+
title: string
|
|
66
|
+
run: () => void
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Same command set + grouping as the default toolbar; `.key` is read at click
|
|
70
|
+
// time via api.run (populated once the editor is created).
|
|
71
|
+
const groups = computed<Tool[][]>(() => [
|
|
72
|
+
[
|
|
73
|
+
{ icon: 'lucide:bold', title: t.value.bold, run: () => props.run(toggleStrongCommand.key) },
|
|
74
|
+
{ icon: 'lucide:italic', title: t.value.italic, run: () => props.run(toggleEmphasisCommand.key) },
|
|
75
|
+
{ icon: 'lucide:strikethrough', title: t.value.strike, run: () => props.run(toggleStrikethroughCommand.key) },
|
|
76
|
+
{ icon: 'lucide:code', title: t.value.code, run: () => props.run(toggleInlineCodeCommand.key) }
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
{ icon: 'lucide:heading-1', title: t.value.h1, run: () => props.run(wrapInHeadingCommand.key, 1) },
|
|
80
|
+
{ icon: 'lucide:heading-2', title: t.value.h2, run: () => props.run(wrapInHeadingCommand.key, 2) },
|
|
81
|
+
{ icon: 'lucide:heading-3', title: t.value.h3, run: () => props.run(wrapInHeadingCommand.key, 3) }
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
{ icon: 'lucide:list', title: t.value.bulletList, run: () => props.run(wrapInBulletListCommand.key) },
|
|
85
|
+
{ icon: 'lucide:list-ordered', title: t.value.orderedList, run: () => props.run(wrapInOrderedListCommand.key) },
|
|
86
|
+
{ icon: 'lucide:text-quote', title: t.value.quote, run: () => props.run(wrapInBlockquoteCommand.key) },
|
|
87
|
+
{ icon: 'lucide:square-code', title: t.value.codeBlock, run: () => props.run(createCodeBlockCommand.key, '') },
|
|
88
|
+
{ icon: 'lucide:minus', title: t.value.hr, run: () => props.run(insertHrCommand.key) }
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
{ icon: 'lucide:eye-off', title: t.value.spoiler, run: () => props.run(insertKunSpoilerCommand.key) },
|
|
92
|
+
{ icon: 'lucide:sigma', title: t.value.latex, run: () => props.run(toggleLatexCommand.key) }
|
|
93
|
+
]
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
// Image upload — only when the host supplies uploadImage. Same adapter as
|
|
97
|
+
// paste/drop; on success inserts an image node at the cursor.
|
|
98
|
+
const uploadImage = computed(() => props.adapters.uploadImage)
|
|
99
|
+
const fileInput = ref<HTMLInputElement | null>(null)
|
|
100
|
+
const pickImage = () => fileInput.value?.click()
|
|
101
|
+
const onFileChange = async (e: Event) => {
|
|
102
|
+
const input = e.target as HTMLInputElement
|
|
103
|
+
const file = input.files?.[0]
|
|
104
|
+
input.value = ''
|
|
105
|
+
const upload = uploadImage.value
|
|
106
|
+
if (!file || !upload) return
|
|
107
|
+
try {
|
|
108
|
+
const src = await upload(file)
|
|
109
|
+
props.run(insertImageCommand.key, { src, title: file.name, alt: file.name })
|
|
110
|
+
} catch {
|
|
111
|
+
props.adapters.notify?.(en.value ? 'Image upload failed' : '图片上传失败', 'error')
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Emoji (built-in) + sticker (adapter) picker in a KunPopover.
|
|
116
|
+
const showPicker = computed(() => props.features.sticker !== false)
|
|
117
|
+
const stickerSource = computed(() => props.adapters.stickerSource)
|
|
118
|
+
const hasSticker = computed(() => !!stickerSource.value)
|
|
119
|
+
const tab = ref<'emoji' | 'sticker'>('emoji')
|
|
120
|
+
const packs = ref<StickerPack[]>([])
|
|
121
|
+
const loaded = ref(false)
|
|
122
|
+
const loadFailed = ref(false)
|
|
123
|
+
const loadStickers = async () => {
|
|
124
|
+
const source = stickerSource.value
|
|
125
|
+
if (loaded.value || !source) return
|
|
126
|
+
loaded.value = true
|
|
127
|
+
try {
|
|
128
|
+
packs.value = await source()
|
|
129
|
+
} catch {
|
|
130
|
+
loadFailed.value = true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const insertSticker = (src: string, name: string) =>
|
|
134
|
+
props.run(insertImageCommand.key, { src, title: name, alt: name })
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
<template>
|
|
138
|
+
<div class="flex flex-wrap items-center gap-0.5">
|
|
139
|
+
<template v-for="(group, gi) in groups" :key="gi">
|
|
140
|
+
<span v-if="gi > 0" class="bg-default-200 mx-1 h-5 w-px" aria-hidden="true" />
|
|
141
|
+
<KunTooltip v-for="(b, bi) in group" :key="bi" :text="b.title" :delay-show="300">
|
|
142
|
+
<KunButton
|
|
143
|
+
variant="light"
|
|
144
|
+
size="sm"
|
|
145
|
+
:is-icon-only="true"
|
|
146
|
+
:aria-label="b.title"
|
|
147
|
+
@click="b.run()"
|
|
148
|
+
>
|
|
149
|
+
<KunIcon :name="b.icon" />
|
|
150
|
+
</KunButton>
|
|
151
|
+
</KunTooltip>
|
|
152
|
+
</template>
|
|
153
|
+
|
|
154
|
+
<template v-if="uploadImage">
|
|
155
|
+
<span class="bg-default-200 mx-1 h-5 w-px" aria-hidden="true" />
|
|
156
|
+
<KunTooltip :text="t.image" :delay-show="300">
|
|
157
|
+
<KunButton
|
|
158
|
+
variant="light"
|
|
159
|
+
size="sm"
|
|
160
|
+
:is-icon-only="true"
|
|
161
|
+
:aria-label="t.image"
|
|
162
|
+
@click="pickImage"
|
|
163
|
+
>
|
|
164
|
+
<KunIcon name="lucide:image" />
|
|
165
|
+
</KunButton>
|
|
166
|
+
</KunTooltip>
|
|
167
|
+
<input
|
|
168
|
+
ref="fileInput"
|
|
169
|
+
type="file"
|
|
170
|
+
accept=".jpg,.jpeg,.png,.webp,.gif"
|
|
171
|
+
hidden
|
|
172
|
+
@change="onFileChange"
|
|
173
|
+
/>
|
|
174
|
+
</template>
|
|
175
|
+
|
|
176
|
+
<template v-if="showPicker">
|
|
177
|
+
<span class="bg-default-200 mx-1 h-5 w-px" aria-hidden="true" />
|
|
178
|
+
<KunPopover position="bottom-start" :opaque="true" inner-class="w-72 p-2">
|
|
179
|
+
<template #trigger>
|
|
180
|
+
<KunButton
|
|
181
|
+
variant="light"
|
|
182
|
+
size="sm"
|
|
183
|
+
:is-icon-only="true"
|
|
184
|
+
:aria-label="t.picker"
|
|
185
|
+
@click="hasSticker && loadStickers()"
|
|
186
|
+
>
|
|
187
|
+
<KunIcon name="lucide:smile-plus" />
|
|
188
|
+
</KunButton>
|
|
189
|
+
</template>
|
|
190
|
+
|
|
191
|
+
<div v-if="hasSticker" class="mb-2 flex gap-1">
|
|
192
|
+
<KunButton
|
|
193
|
+
:variant="tab === 'emoji' ? 'flat' : 'light'"
|
|
194
|
+
size="sm"
|
|
195
|
+
@click="tab = 'emoji'"
|
|
196
|
+
>{{ t.emoji }}</KunButton
|
|
197
|
+
>
|
|
198
|
+
<KunButton
|
|
199
|
+
:variant="tab === 'sticker' ? 'flat' : 'light'"
|
|
200
|
+
size="sm"
|
|
201
|
+
@click="tab = 'sticker'"
|
|
202
|
+
>{{ t.sticker }}</KunButton
|
|
203
|
+
>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div
|
|
207
|
+
v-show="!hasSticker || tab === 'emoji'"
|
|
208
|
+
class="grid max-h-56 grid-cols-8 gap-0.5 overflow-y-auto"
|
|
209
|
+
>
|
|
210
|
+
<button
|
|
211
|
+
v-for="(e, i) in EMOJI"
|
|
212
|
+
:key="i"
|
|
213
|
+
type="button"
|
|
214
|
+
class="hover:bg-default-100 rounded p-1 text-lg leading-none"
|
|
215
|
+
@click="props.insertText(e)"
|
|
216
|
+
>
|
|
217
|
+
{{ e }}
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<div v-if="hasSticker" v-show="tab === 'sticker'" class="max-h-56 overflow-y-auto">
|
|
222
|
+
<template v-if="packs.length">
|
|
223
|
+
<div v-for="pack in packs" :key="pack.name" class="mb-2">
|
|
224
|
+
<div class="text-default-500 mb-1 text-xs">{{ pack.name }}</div>
|
|
225
|
+
<div class="grid grid-cols-4 gap-1">
|
|
226
|
+
<button
|
|
227
|
+
v-for="(s, i) in pack.stickers"
|
|
228
|
+
:key="i"
|
|
229
|
+
type="button"
|
|
230
|
+
class="hover:bg-default-100 rounded p-1"
|
|
231
|
+
:title="s.name"
|
|
232
|
+
@click="insertSticker(s.src, s.name)"
|
|
233
|
+
>
|
|
234
|
+
<img :src="s.src" :alt="s.name" loading="lazy" class="h-14 w-14 object-contain" />
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</template>
|
|
239
|
+
<div v-else class="text-default-500 p-4 text-center text-sm">
|
|
240
|
+
{{ loadFailed ? t.failed : t.empty }}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</KunPopover>
|
|
244
|
+
</template>
|
|
245
|
+
</div>
|
|
246
|
+
</template>
|