@kungal/editor-nuxt 0.20.0 → 0.22.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 CHANGED
@@ -1,5 +1,45 @@
1
1
  # @kungal/editor-nuxt
2
2
 
3
+ ## 0.22.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [fbbb761]
8
+ - @kungal/editor-vue@0.22.0
9
+ - @kungal/editor-core@0.22.0
10
+
11
+ ## 0.21.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 9556a51: `<KunEditorToolbar>` gains an `items` prop to reorder / subset the buttons.
16
+
17
+ Previously the toolbar's button order was fixed, so a host that wanted a different
18
+ order had to rebuild a whole custom toolbar via the `#toolbar` slot — which drifts
19
+ from the default toolbar used elsewhere (e.g. a topic-edit page vs reply/comment).
20
+
21
+ Now pass `:items` — an ordered `KunToolbarItem[]` (`'heading' | 'bold' | 'italic'
22
+ | 'strike' | 'code' | 'bulletList' | 'orderedList' | 'quote' | 'codeBlock' | 'hr'
23
+ | 'spoiler' | 'image' | 'picker' | '|'`, where `'|'` is a divider):
24
+
25
+ ```vue
26
+ <KunEditorToolbar
27
+ v-bind="api"
28
+ :items="['picker', '|', 'bold', 'italic', '|', 'image', '|', 'heading']"
29
+ />
30
+ ```
31
+
32
+ Apply the same array to every editor for a consistent site-wide order without
33
+ rebuilding a toolbar. Defaults to the standard layout. `image`/`picker` are still
34
+ auto-dropped without their adapter/feature, and dividers collapse around removed
35
+ items. `KunToolbarItem` is exported from `@kungal/editor-vue`.
36
+
37
+ ### Patch Changes
38
+
39
+ - Updated dependencies [9556a51]
40
+ - @kungal/editor-vue@0.21.0
41
+ - @kungal/editor-core@0.21.0
42
+
3
43
  ## 0.20.0
4
44
 
5
45
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungal/editor-nuxt",
3
- "version": "0.20.0",
3
+ "version": "0.22.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": [
@@ -31,8 +31,8 @@
31
31
  "nuxt.config.ts"
32
32
  ],
33
33
  "dependencies": {
34
- "@kungal/editor-core": "0.20.0",
35
- "@kungal/editor-vue": "0.20.0"
34
+ "@kungal/editor-core": "0.22.0",
35
+ "@kungal/editor-vue": "0.22.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@kungal/ui-vue": "^2.7.0",
@@ -11,18 +11,15 @@
11
11
  // KunButton / KunIcon / KunTooltip / KunPopover chrome. It drives the editor
12
12
  // purely through the slot API props (KunEditorToolbarApi) — no useInstance.
13
13
  //
14
- // Headings are ONE "text size" KunPopover (Paragraph / H1–H6, each shown at its
15
- // own size like the forum's header menu), driven by setHeadingCommand (absolute
16
- // set). There is deliberately NO math button an empty inline-math atom is a
17
- // broken node; math is entered via the `$…$` / `$$` input rules.
14
+ // Button order/set is customizable via `:items` (a list of KunToolbarItem ids,
15
+ // '|' = divider) so a host can reorder or subset the built-in buttons and apply
16
+ // the SAME order across every editor, instead of rebuilding a custom toolbar.
17
+ // Defaults to the standard layout. image/picker are auto-dropped without their
18
+ // adapter/feature, and dividers collapse around removed items.
18
19
  //
19
- // The emoji/sticker picker switches tabs with a <KunTab variant="underlined">
20
- // (matching <KunEditorViewSwitch> and the forum's picker).
21
- //
22
- // KunButton/KunIcon/KunTooltip/KunPopover/KunTab are auto-imported by ui-nuxt in
23
- // the consuming app; icons use the app's iconify set (e.g. lucide via @nuxt/icon).
24
- // The container/menu utility classes are generated via @kungal/editor-nuxt's
25
- // shipped tailwind.css (@source) — the consumer @imports it once.
20
+ // Headings are ONE "text size" KunPopover (Paragraph / H1–H6). There is NO math
21
+ // button an empty inline-math atom is a broken node; math is via `$…$` / `$$`.
22
+ // KunButton/KunIcon/KunTooltip/KunPopover/KunTab are auto-imported by ui-nuxt.
26
23
  import { computed, ref } from 'vue'
27
24
  import {
28
25
  createCodeBlockCommand,
@@ -41,9 +38,39 @@ import {
41
38
  setHeadingCommand
42
39
  } from '@kungal/editor-core/preset'
43
40
  import { EMOJI } from '@kungal/editor-vue'
44
- import type { KunEditorToolbarApi, StickerPack } from '@kungal/editor-vue'
41
+ import type {
42
+ KunEditorToolbarApi,
43
+ KunToolbarItem,
44
+ StickerPack
45
+ } from '@kungal/editor-vue'
46
+
47
+ const props = defineProps<
48
+ KunEditorToolbarApi & {
49
+ /** Ordered button ids ('|' = divider). Defaults to the standard layout. */
50
+ items?: KunToolbarItem[]
51
+ }
52
+ >()
45
53
 
46
- const props = defineProps<KunEditorToolbarApi>()
54
+ const DEFAULT_ITEMS: KunToolbarItem[] = [
55
+ 'heading',
56
+ '|',
57
+ 'bold',
58
+ 'italic',
59
+ 'strike',
60
+ 'code',
61
+ '|',
62
+ 'bulletList',
63
+ 'orderedList',
64
+ 'quote',
65
+ 'codeBlock',
66
+ 'hr',
67
+ '|',
68
+ 'spoiler',
69
+ '|',
70
+ 'image',
71
+ '|',
72
+ 'picker'
73
+ ]
47
74
 
48
75
  const en = computed(() => props.locale.toLowerCase().startsWith('en'))
49
76
  const t = computed(() => ({
@@ -66,6 +93,66 @@ const t = computed(() => ({
66
93
  failed: en.value ? 'Failed to load stickers' : '贴纸加载失败'
67
94
  }))
68
95
 
96
+ interface Tool {
97
+ icon: string
98
+ title: string
99
+ run: () => void
100
+ }
101
+
102
+ // The plain command buttons (everything except heading / image / picker).
103
+ // `.key` is read at click time via props.run (populated once the editor exists).
104
+ const commandButtons = computed<Record<string, Tool>>(() => ({
105
+ bold: { icon: 'lucide:bold', title: t.value.bold, run: () => props.run(toggleStrongCommand.key) },
106
+ italic: { icon: 'lucide:italic', title: t.value.italic, run: () => props.run(toggleEmphasisCommand.key) },
107
+ strike: { icon: 'lucide:strikethrough', title: t.value.strike, run: () => props.run(toggleStrikethroughCommand.key) },
108
+ code: { icon: 'lucide:code', title: t.value.code, run: () => props.run(toggleInlineCodeCommand.key) },
109
+ bulletList: { icon: 'lucide:list', title: t.value.bulletList, run: () => props.run(wrapInBulletListCommand.key) },
110
+ orderedList: { icon: 'lucide:list-ordered', title: t.value.orderedList, run: () => props.run(wrapInOrderedListCommand.key) },
111
+ quote: { icon: 'lucide:text-quote', title: t.value.quote, run: () => props.run(wrapInBlockquoteCommand.key) },
112
+ codeBlock: { icon: 'lucide:square-code', title: t.value.codeBlock, run: () => props.run(createCodeBlockCommand.key, '') },
113
+ hr: { icon: 'lucide:minus', title: t.value.hr, run: () => props.run(insertHrCommand.key) },
114
+ spoiler: { icon: 'lucide:eye-off', title: t.value.spoiler, run: () => props.run(insertKunSpoilerCommand.key) }
115
+ }))
116
+
117
+ type RenderItem =
118
+ | { kind: 'divider' }
119
+ | { kind: 'heading' }
120
+ | { kind: 'image' }
121
+ | { kind: 'picker' }
122
+ | { kind: 'button'; tool: Tool }
123
+
124
+ // Resolve `items` → renderables: drop image/picker when unavailable, map command
125
+ // ids to their button, then collapse consecutive/leading/trailing dividers so a
126
+ // removed item never leaves a doubled or dangling separator.
127
+ const resolvedItems = computed<RenderItem[]>(() => {
128
+ const buttons = commandButtons.value
129
+ const mapped = (props.items ?? DEFAULT_ITEMS)
130
+ .filter((it) =>
131
+ it === 'image' ? canUpload.value : it === 'picker' ? showPicker.value : true
132
+ )
133
+ .map<RenderItem | null>((it) => {
134
+ if (it === '|') return { kind: 'divider' }
135
+ if (it === 'heading') return { kind: 'heading' }
136
+ if (it === 'image') return { kind: 'image' }
137
+ if (it === 'picker') return { kind: 'picker' }
138
+ const tool = buttons[it]
139
+ return tool ? { kind: 'button', tool } : null
140
+ })
141
+ .filter((x): x is RenderItem => x !== null)
142
+
143
+ const out: RenderItem[] = []
144
+ for (const it of mapped) {
145
+ if (it.kind === 'divider' && (out.length === 0 || out[out.length - 1]?.kind === 'divider')) {
146
+ continue
147
+ }
148
+ out.push(it)
149
+ }
150
+ while (out.length && out[out.length - 1]?.kind === 'divider') {
151
+ out.pop()
152
+ }
153
+ return out
154
+ })
155
+
69
156
  // Paragraph (level 0) + H1–H6, each rendered at its own size in the menu.
70
157
  const headingOptions = computed(() => {
71
158
  const labels = en.value
@@ -74,44 +161,25 @@ const headingOptions = computed(() => {
74
161
  const sizes = ['1rem', '1.6rem', '1.4rem', '1.25rem', '1.1rem', '1rem', '0.9rem']
75
162
  return labels.map((label, level) => ({ level, label, size: sizes[level] }))
76
163
  })
77
- const headingPopover = ref<{ close: () => void } | null>(null)
164
+ // Function refs (these elements live inside a v-for, where string refs collect
165
+ // into arrays). Each renders at most once.
166
+ let headingPopoverEl: { close: () => void } | null = null
167
+ const setHeadingPopover = (el: unknown) => {
168
+ headingPopoverEl = el as { close: () => void } | null
169
+ }
78
170
  const setHeading = (level: number) => {
79
171
  props.run(setHeadingCommand.key, level)
80
- headingPopover.value?.close()
172
+ headingPopoverEl?.close()
81
173
  }
82
174
 
83
- interface Tool {
84
- icon: string
85
- title: string
86
- run: () => void
87
- }
88
-
89
- // `.key` is read at click time via props.run (populated once the editor exists).
90
- const groups = computed<Tool[][]>(() => [
91
- [
92
- { icon: 'lucide:bold', title: t.value.bold, run: () => props.run(toggleStrongCommand.key) },
93
- { icon: 'lucide:italic', title: t.value.italic, run: () => props.run(toggleEmphasisCommand.key) },
94
- { icon: 'lucide:strikethrough', title: t.value.strike, run: () => props.run(toggleStrikethroughCommand.key) },
95
- { icon: 'lucide:code', title: t.value.code, run: () => props.run(toggleInlineCodeCommand.key) }
96
- ],
97
- [
98
- { icon: 'lucide:list', title: t.value.bulletList, run: () => props.run(wrapInBulletListCommand.key) },
99
- { icon: 'lucide:list-ordered', title: t.value.orderedList, run: () => props.run(wrapInOrderedListCommand.key) },
100
- { icon: 'lucide:text-quote', title: t.value.quote, run: () => props.run(wrapInBlockquoteCommand.key) },
101
- { icon: 'lucide:square-code', title: t.value.codeBlock, run: () => props.run(createCodeBlockCommand.key, '') },
102
- { icon: 'lucide:minus', title: t.value.hr, run: () => props.run(insertHrCommand.key) }
103
- ],
104
- [
105
- { icon: 'lucide:eye-off', title: t.value.spoiler, run: () => props.run(insertKunSpoilerCommand.key) }
106
- ]
107
- ])
108
-
109
- // Image upload button — shown only when the host supplies uploadImage. The
110
- // upload itself goes through api.uploadImage, which shows an in-document
111
- // "uploading…" placeholder at the caret (same UX as paste/drop).
175
+ // Image upload — via api.uploadImage (shows the in-document "uploading…"
176
+ // placeholder). Only rendered when the host supplies uploadImage.
112
177
  const canUpload = computed(() => !!props.adapters.uploadImage)
113
- const fileInput = ref<HTMLInputElement | null>(null)
114
- const pickImage = () => fileInput.value?.click()
178
+ let fileInputEl: HTMLInputElement | null = null
179
+ const setFileInput = (el: unknown) => {
180
+ fileInputEl = el as HTMLInputElement | null
181
+ }
182
+ const pickImage = () => fileInputEl?.click()
115
183
  const onFileChange = (e: Event) => {
116
184
  const input = e.target as HTMLInputElement
117
185
  const file = input.files?.[0]
@@ -145,65 +213,65 @@ const insertSticker = (src: string, name: string) =>
145
213
 
146
214
  <template>
147
215
  <div class="flex flex-wrap items-center gap-0.5">
148
- <KunPopover ref="headingPopover" position="bottom-start" :opaque="true" inner-class="min-w-40 p-1">
149
- <template #trigger>
150
- <KunButton variant="light" size="sm" :aria-label="t.textSize">
151
- {{ t.textSize }}
152
- <KunIcon name="lucide:chevron-down" />
153
- </KunButton>
154
- </template>
155
- <button
156
- v-for="o in headingOptions"
157
- :key="o.level"
158
- type="button"
159
- class="hover:bg-default-100 flex w-full items-center rounded px-3 py-1.5 text-left leading-tight"
160
- :style="{ fontSize: o.size }"
161
- @click="setHeading(o.level)"
162
- >
163
- {{ o.label }}
164
- </button>
165
- </KunPopover>
216
+ <template v-for="(item, i) in resolvedItems" :key="i">
217
+ <span
218
+ v-if="item.kind === 'divider'"
219
+ class="bg-default-200 mx-1 h-5 w-px"
220
+ aria-hidden="true"
221
+ />
166
222
 
167
- <template v-for="(group, gi) in groups" :key="gi">
168
- <span class="bg-default-200 mx-1 h-5 w-px" aria-hidden="true" />
169
- <KunTooltip v-for="(b, bi) in group" :key="bi" :text="b.title" :delay-show="300">
170
- <KunButton
171
- variant="light"
172
- size="sm"
173
- :is-icon-only="true"
174
- :aria-label="b.title"
175
- @click="b.run()"
223
+ <KunPopover
224
+ v-else-if="item.kind === 'heading'"
225
+ :ref="setHeadingPopover"
226
+ position="bottom-start"
227
+ :opaque="true"
228
+ inner-class="min-w-40 p-1"
229
+ >
230
+ <template #trigger>
231
+ <KunButton variant="light" size="sm" :aria-label="t.textSize">
232
+ {{ t.textSize }}
233
+ <KunIcon name="lucide:chevron-down" />
234
+ </KunButton>
235
+ </template>
236
+ <button
237
+ v-for="o in headingOptions"
238
+ :key="o.level"
239
+ type="button"
240
+ class="hover:bg-default-100 flex w-full items-center rounded px-3 py-1.5 text-left leading-tight"
241
+ :style="{ fontSize: o.size }"
242
+ @click="setHeading(o.level)"
176
243
  >
177
- <KunIcon :name="b.icon" />
178
- </KunButton>
179
- </KunTooltip>
180
- </template>
244
+ {{ o.label }}
245
+ </button>
246
+ </KunPopover>
181
247
 
182
- <template v-if="canUpload">
183
- <span class="bg-default-200 mx-1 h-5 w-px" aria-hidden="true" />
184
- <KunTooltip :text="t.image" :delay-show="300">
185
- <KunButton
186
- variant="light"
187
- size="sm"
188
- :is-icon-only="true"
189
- :aria-label="t.image"
190
- @click="pickImage"
191
- >
192
- <KunIcon name="lucide:image" />
193
- </KunButton>
194
- </KunTooltip>
195
- <input
196
- ref="fileInput"
197
- type="file"
198
- accept=".jpg,.jpeg,.png,.webp,.gif"
199
- hidden
200
- @change="onFileChange"
201
- />
202
- </template>
248
+ <template v-else-if="item.kind === 'image'">
249
+ <KunTooltip :text="t.image" :delay-show="300">
250
+ <KunButton
251
+ variant="light"
252
+ size="sm"
253
+ :is-icon-only="true"
254
+ :aria-label="t.image"
255
+ @click="pickImage"
256
+ >
257
+ <KunIcon name="lucide:image" />
258
+ </KunButton>
259
+ </KunTooltip>
260
+ <input
261
+ :ref="setFileInput"
262
+ type="file"
263
+ accept=".jpg,.jpeg,.png,.webp,.gif"
264
+ hidden
265
+ @change="onFileChange"
266
+ />
267
+ </template>
203
268
 
204
- <template v-if="showPicker">
205
- <span class="bg-default-200 mx-1 h-5 w-px" aria-hidden="true" />
206
- <KunPopover position="bottom-start" :opaque="true" inner-class="w-72 p-2">
269
+ <KunPopover
270
+ v-else-if="item.kind === 'picker'"
271
+ position="bottom-start"
272
+ :opaque="true"
273
+ inner-class="w-72 p-2"
274
+ >
207
275
  <template #trigger>
208
276
  <KunButton
209
277
  variant="light"
@@ -234,8 +302,8 @@ const insertSticker = (src: string, name: string) =>
234
302
  class="grid max-h-56 grid-cols-8 gap-0.5 overflow-y-auto"
235
303
  >
236
304
  <button
237
- v-for="(e, i) in EMOJI"
238
- :key="i"
305
+ v-for="(e, ei) in EMOJI"
306
+ :key="ei"
239
307
  type="button"
240
308
  class="hover:bg-default-100 rounded p-1 text-lg leading-none"
241
309
  @click="props.insertText(e)"
@@ -250,8 +318,8 @@ const insertSticker = (src: string, name: string) =>
250
318
  <div class="text-default-500 mb-1 text-xs">{{ pack.name }}</div>
251
319
  <div class="grid grid-cols-4 gap-1">
252
320
  <button
253
- v-for="(s, i) in pack.stickers"
254
- :key="i"
321
+ v-for="(s, si) in pack.stickers"
322
+ :key="si"
255
323
  type="button"
256
324
  class="hover:bg-default-100 rounded p-1"
257
325
  :title="s.name"
@@ -267,6 +335,22 @@ const insertSticker = (src: string, name: string) =>
267
335
  </div>
268
336
  </div>
269
337
  </KunPopover>
338
+
339
+ <KunTooltip
340
+ v-else-if="item.kind === 'button'"
341
+ :text="item.tool.title"
342
+ :delay-show="300"
343
+ >
344
+ <KunButton
345
+ variant="light"
346
+ size="sm"
347
+ :is-icon-only="true"
348
+ :aria-label="item.tool.title"
349
+ @click="item.tool.run()"
350
+ >
351
+ <KunIcon :name="item.tool.icon" />
352
+ </KunButton>
353
+ </KunTooltip>
270
354
  </template>
271
355
  </div>
272
356
  </template>