@milkdown/crepe 7.19.1 → 7.20.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.
Files changed (100) hide show
  1. package/lib/cjs/builder.js +43 -1
  2. package/lib/cjs/builder.js.map +1 -1
  3. package/lib/cjs/feature/block-edit/index.js +8 -2
  4. package/lib/cjs/feature/block-edit/index.js.map +1 -1
  5. package/lib/cjs/feature/code-mirror/index.js +1 -0
  6. package/lib/cjs/feature/code-mirror/index.js.map +1 -1
  7. package/lib/cjs/feature/cursor/index.js +1 -0
  8. package/lib/cjs/feature/cursor/index.js.map +1 -1
  9. package/lib/cjs/feature/image-block/index.js +4 -1
  10. package/lib/cjs/feature/image-block/index.js.map +1 -1
  11. package/lib/cjs/feature/latex/index.js +5 -0
  12. package/lib/cjs/feature/latex/index.js.map +1 -1
  13. package/lib/cjs/feature/link-tooltip/index.js +1 -0
  14. package/lib/cjs/feature/link-tooltip/index.js.map +1 -1
  15. package/lib/cjs/feature/list-item/index.js +1 -0
  16. package/lib/cjs/feature/list-item/index.js.map +1 -1
  17. package/lib/cjs/feature/placeholder/index.js +1 -0
  18. package/lib/cjs/feature/placeholder/index.js.map +1 -1
  19. package/lib/cjs/feature/table/index.js +1 -0
  20. package/lib/cjs/feature/table/index.js.map +1 -1
  21. package/lib/cjs/feature/toolbar/index.js +9 -2
  22. package/lib/cjs/feature/toolbar/index.js.map +1 -1
  23. package/lib/cjs/feature/top-bar/index.js +790 -0
  24. package/lib/cjs/feature/top-bar/index.js.map +1 -0
  25. package/lib/cjs/index.js +630 -142
  26. package/lib/cjs/index.js.map +1 -1
  27. package/lib/esm/builder.js +43 -1
  28. package/lib/esm/builder.js.map +1 -1
  29. package/lib/esm/feature/block-edit/index.js +8 -2
  30. package/lib/esm/feature/block-edit/index.js.map +1 -1
  31. package/lib/esm/feature/code-mirror/index.js +1 -0
  32. package/lib/esm/feature/code-mirror/index.js.map +1 -1
  33. package/lib/esm/feature/cursor/index.js +1 -0
  34. package/lib/esm/feature/cursor/index.js.map +1 -1
  35. package/lib/esm/feature/image-block/index.js +4 -1
  36. package/lib/esm/feature/image-block/index.js.map +1 -1
  37. package/lib/esm/feature/latex/index.js +5 -0
  38. package/lib/esm/feature/latex/index.js.map +1 -1
  39. package/lib/esm/feature/link-tooltip/index.js +1 -0
  40. package/lib/esm/feature/link-tooltip/index.js.map +1 -1
  41. package/lib/esm/feature/list-item/index.js +1 -0
  42. package/lib/esm/feature/list-item/index.js.map +1 -1
  43. package/lib/esm/feature/placeholder/index.js +1 -0
  44. package/lib/esm/feature/placeholder/index.js.map +1 -1
  45. package/lib/esm/feature/table/index.js +1 -0
  46. package/lib/esm/feature/table/index.js.map +1 -1
  47. package/lib/esm/feature/toolbar/index.js +9 -2
  48. package/lib/esm/feature/toolbar/index.js.map +1 -1
  49. package/lib/esm/feature/top-bar/index.js +788 -0
  50. package/lib/esm/feature/top-bar/index.js.map +1 -0
  51. package/lib/esm/index.js +631 -143
  52. package/lib/esm/index.js.map +1 -1
  53. package/lib/theme/common/style.css +1 -0
  54. package/lib/theme/common/top-bar.css +152 -0
  55. package/lib/tsconfig.tsbuildinfo +1 -1
  56. package/lib/types/core/builder.d.ts +2 -1
  57. package/lib/types/core/builder.d.ts.map +1 -1
  58. package/lib/types/feature/block-edit/handle/component.d.ts.map +1 -1
  59. package/lib/types/feature/block-edit/menu/component.d.ts.map +1 -1
  60. package/lib/types/feature/image-block/index.d.ts +2 -0
  61. package/lib/types/feature/image-block/index.d.ts.map +1 -1
  62. package/lib/types/feature/index.d.ts +4 -1
  63. package/lib/types/feature/index.d.ts.map +1 -1
  64. package/lib/types/feature/latex/inline-tooltip/component.d.ts.map +1 -1
  65. package/lib/types/feature/loader.d.ts.map +1 -1
  66. package/lib/types/feature/toolbar/component.d.ts.map +1 -1
  67. package/lib/types/feature/toolbar/config.d.ts +1 -1
  68. package/lib/types/feature/top-bar/component.d.ts +11 -0
  69. package/lib/types/feature/top-bar/component.d.ts.map +1 -0
  70. package/lib/types/feature/top-bar/config.d.ts +34 -0
  71. package/lib/types/feature/top-bar/config.d.ts.map +1 -0
  72. package/lib/types/feature/top-bar/index.d.ts +26 -0
  73. package/lib/types/feature/top-bar/index.d.ts.map +1 -0
  74. package/lib/types/icons/code-block.d.ts +2 -0
  75. package/lib/types/icons/code-block.d.ts.map +1 -0
  76. package/lib/types/icons/index.d.ts +1 -0
  77. package/lib/types/icons/index.d.ts.map +1 -1
  78. package/lib/types/utils/group-builder.d.ts +1 -1
  79. package/lib/types/utils/group-builder.d.ts.map +1 -1
  80. package/lib/types/utils/keep-alive.d.ts +2 -0
  81. package/lib/types/utils/keep-alive.d.ts.map +1 -0
  82. package/package.json +18 -13
  83. package/src/core/builder.ts +39 -2
  84. package/src/feature/block-edit/handle/component.tsx +3 -2
  85. package/src/feature/block-edit/menu/component.tsx +3 -2
  86. package/src/feature/block-edit/menu/config.ts +1 -1
  87. package/src/feature/image-block/index.ts +4 -0
  88. package/src/feature/index.ts +6 -0
  89. package/src/feature/latex/inline-tooltip/component.tsx +4 -2
  90. package/src/feature/loader.ts +4 -0
  91. package/src/feature/toolbar/component.tsx +7 -5
  92. package/src/feature/top-bar/component.tsx +198 -0
  93. package/src/feature/top-bar/config.ts +367 -0
  94. package/src/feature/top-bar/index.ts +113 -0
  95. package/src/icons/code-block.ts +12 -0
  96. package/src/icons/index.ts +1 -0
  97. package/src/theme/common/style.css +1 -0
  98. package/src/theme/common/top-bar.css +156 -0
  99. package/src/utils/group-builder.ts +1 -1
  100. package/src/utils/keep-alive.ts +3 -0
@@ -0,0 +1,198 @@
1
+ import type { Ctx } from '@milkdown/kit/ctx'
2
+
3
+ import { Icon } from '@milkdown/kit/component'
4
+ import { editorCtx, EditorStatus, editorViewCtx } from '@milkdown/kit/core'
5
+ import clsx from 'clsx'
6
+ import {
7
+ defineComponent,
8
+ type Ref,
9
+ type VNode,
10
+ h,
11
+ Fragment,
12
+ computed,
13
+ ref,
14
+ onMounted,
15
+ onUnmounted,
16
+ } from 'vue'
17
+
18
+ import type { TopBarFeatureConfig } from '.'
19
+
20
+ import { keepAlive } from '../../utils/keep-alive'
21
+ import { getGroups, type TopBarItem, type TopBarSelector } from './config'
22
+
23
+ keepAlive(h, Fragment)
24
+
25
+ type TopBarProps = {
26
+ ctx: Ctx
27
+ version: Ref<number>
28
+ config?: TopBarFeatureConfig
29
+ }
30
+
31
+ export const TopBar = defineComponent<TopBarProps>({
32
+ props: {
33
+ ctx: {
34
+ type: Object,
35
+ required: true,
36
+ },
37
+ version: {
38
+ type: Object,
39
+ required: true,
40
+ },
41
+ config: {
42
+ type: Object,
43
+ required: false,
44
+ },
45
+ },
46
+ setup(props) {
47
+ const { ctx, config } = props
48
+ const openSelectorKey = ref<string | null>(null)
49
+
50
+ const onClick = (fn: (ctx: Ctx) => void) => (e: MouseEvent) => {
51
+ e.preventDefault()
52
+ if (ctx) {
53
+ fn(ctx)
54
+ }
55
+ }
56
+
57
+ function isReady() {
58
+ const status = ctx.get(editorCtx).status
59
+ return status === EditorStatus.Created
60
+ }
61
+
62
+ function subscribeState() {
63
+ keepAlive(props.version.value)
64
+ }
65
+
66
+ function checkActive(checker: TopBarItem['active']) {
67
+ subscribeState()
68
+ if (!isReady()) return false
69
+ return checker(ctx)
70
+ }
71
+
72
+ function getSelectorLabel(selector: TopBarSelector): string {
73
+ subscribeState()
74
+ if (!isReady()) return selector.options[0]?.label ?? ''
75
+ return selector.activeLabel(ctx)
76
+ }
77
+
78
+ function onToggleSelector(key: string, e: Event) {
79
+ e.preventDefault()
80
+ e.stopPropagation()
81
+ openSelectorKey.value = openSelectorKey.value === key ? null : key
82
+ }
83
+
84
+ const clickOutsideHandler = (e: PointerEvent) => {
85
+ const target = e.target as HTMLElement
86
+ if (target.closest('.top-bar-heading-selector')) return
87
+ openSelectorKey.value = null
88
+ }
89
+
90
+ onMounted(() => {
91
+ window.addEventListener('pointerdown', clickOutsideHandler)
92
+ })
93
+
94
+ onUnmounted(() => {
95
+ window.removeEventListener('pointerdown', clickOutsideHandler)
96
+ })
97
+
98
+ const groupInfo = computed(() => getGroups(config, ctx))
99
+
100
+ function renderSelector(itemKey: string, selector: TopBarSelector): VNode {
101
+ const isOpen = openSelectorKey.value === itemKey
102
+ const activeLabel = getSelectorLabel(selector)
103
+
104
+ return (
105
+ <div key={itemKey} class="top-bar-heading-selector">
106
+ <button
107
+ type="button"
108
+ class="top-bar-heading-button"
109
+ onPointerdown={(e: Event) => onToggleSelector(itemKey, e)}
110
+ >
111
+ <span class="top-bar-heading-label">{activeLabel}</span>
112
+ {selector.chevronIcon && (
113
+ <span class="top-bar-chevron">
114
+ <Icon icon={selector.chevronIcon} />
115
+ </span>
116
+ )}
117
+ </button>
118
+ {isOpen && (
119
+ <div class="top-bar-heading-dropdown">
120
+ {selector.options.map((option, index) => (
121
+ <button
122
+ key={`${itemKey}-${index}`}
123
+ type="button"
124
+ class={clsx(
125
+ 'top-bar-heading-option',
126
+ activeLabel === option.label && 'active'
127
+ )}
128
+ onPointerdown={(e: Event) => {
129
+ e.preventDefault()
130
+ e.stopPropagation()
131
+ openSelectorKey.value = null
132
+ option.onSelect(ctx)
133
+ }}
134
+ >
135
+ {option.label}
136
+ </button>
137
+ ))}
138
+ </div>
139
+ )}
140
+ </div>
141
+ )
142
+ }
143
+
144
+ function renderButton(
145
+ item: TopBarItem & { key: string; onRun: (ctx: Ctx) => void }
146
+ ): VNode {
147
+ return (
148
+ <button
149
+ key={item.key}
150
+ type="button"
151
+ class={clsx('top-bar-item', checkActive(item.active) && 'active')}
152
+ onPointerdown={onClick(item.onRun)}
153
+ >
154
+ <Icon icon={item.icon} />
155
+ </button>
156
+ )
157
+ }
158
+
159
+ return () => {
160
+ const view = isReady() ? ctx.get(editorViewCtx) : null
161
+ const isReadonly = view ? !view.editable : false
162
+
163
+ if (isReadonly) return null
164
+
165
+ return (
166
+ <div class="top-bar-inner">
167
+ {groupInfo.value
168
+ .map((group) => {
169
+ return group.items.map((item) => {
170
+ if (item.selector) {
171
+ return renderSelector(item.key, item.selector)
172
+ }
173
+ if (!item.onRun) return null
174
+ return renderButton(
175
+ item as TopBarItem & {
176
+ key: string
177
+ onRun: (ctx: Ctx) => void
178
+ }
179
+ )
180
+ })
181
+ })
182
+ .reduce((acc, curr, index) => {
183
+ if (index === 0) {
184
+ acc.push(...curr)
185
+ } else {
186
+ const groupKey = groupInfo.value[index]?.key ?? index
187
+ acc.push(
188
+ <div key={`divider-${groupKey}`} class="top-bar-divider" />,
189
+ ...curr
190
+ )
191
+ }
192
+ return acc
193
+ }, [] as VNode[])}
194
+ </div>
195
+ )
196
+ }
197
+ },
198
+ })
@@ -0,0 +1,367 @@
1
+ import type { Ctx } from '@milkdown/kit/ctx'
2
+ import type { MarkType } from '@milkdown/kit/prose/model'
3
+
4
+ import { imageBlockSchema } from '@milkdown/kit/component/image-block'
5
+ import { toggleLinkCommand } from '@milkdown/kit/component/link-tooltip'
6
+ import { commandsCtx, editorViewCtx } from '@milkdown/kit/core'
7
+ import {
8
+ addBlockTypeCommand,
9
+ blockquoteSchema,
10
+ bulletListSchema,
11
+ codeBlockSchema,
12
+ headingSchema,
13
+ hrSchema,
14
+ listItemSchema,
15
+ orderedListSchema,
16
+ paragraphSchema,
17
+ selectTextNearPosCommand,
18
+ setBlockTypeCommand,
19
+ wrapInBlockTypeCommand,
20
+ toggleEmphasisCommand,
21
+ toggleInlineCodeCommand,
22
+ toggleStrongCommand,
23
+ emphasisSchema,
24
+ inlineCodeSchema,
25
+ strongSchema,
26
+ linkSchema,
27
+ isMarkSelectedCommand,
28
+ } from '@milkdown/kit/preset/commonmark'
29
+ import {
30
+ strikethroughSchema,
31
+ toggleStrikethroughCommand,
32
+ createTable,
33
+ } from '@milkdown/kit/preset/gfm'
34
+ import { TextSelection } from '@milkdown/kit/prose/state'
35
+
36
+ import type { TopBarFeatureConfig } from '.'
37
+
38
+ import { useCrepeFeatures } from '../../core/slice'
39
+ import { CrepeFeature } from '../../feature'
40
+ import {
41
+ boldIcon,
42
+ bulletListIcon,
43
+ chevronDownIcon,
44
+ codeBlockIcon,
45
+ codeIcon,
46
+ dividerIcon,
47
+ functionsIcon,
48
+ imageIcon,
49
+ italicIcon,
50
+ linkIcon,
51
+ orderedListIcon,
52
+ quoteIcon,
53
+ strikethroughIcon,
54
+ tableIcon,
55
+ todoListIcon,
56
+ } from '../../icons'
57
+ import { GroupBuilder } from '../../utils/group-builder'
58
+
59
+ export interface TopBarSelectorOption {
60
+ label: string
61
+ onSelect: (ctx: Ctx) => void
62
+ }
63
+
64
+ export interface TopBarSelector {
65
+ options: TopBarSelectorOption[]
66
+ activeLabel: (ctx: Ctx) => string
67
+ chevronIcon?: string
68
+ }
69
+
70
+ export type TopBarItem = {
71
+ active: (ctx: Ctx) => boolean
72
+ icon: string
73
+ selector?: TopBarSelector
74
+ }
75
+
76
+ export type HeadingOption = {
77
+ label: string
78
+ level: number | null
79
+ }
80
+
81
+ export const defaultHeadingOptions: HeadingOption[] = [
82
+ { label: 'Paragraph', level: null },
83
+ { label: 'Heading 1', level: 1 },
84
+ { label: 'Heading 2', level: 2 },
85
+ { label: 'Heading 3', level: 3 },
86
+ { label: 'Heading 4', level: 4 },
87
+ { label: 'Heading 5', level: 5 },
88
+ { label: 'Heading 6', level: 6 },
89
+ ]
90
+
91
+ export function getCurrentHeading(
92
+ ctx: Ctx,
93
+ options?: HeadingOption[]
94
+ ): HeadingOption {
95
+ const headingOptions = options ?? defaultHeadingOptions
96
+ const view = ctx.get(editorViewCtx)
97
+ const { $from } = view.state.selection
98
+ const node = $from.parent
99
+
100
+ const paragraphOption =
101
+ headingOptions.find((o) => o.level === null) ?? headingOptions[0]!
102
+
103
+ if (node.type === headingSchema.type(ctx)) {
104
+ const level = node.attrs.level as number
105
+ return headingOptions.find((o) => o.level === level) ?? paragraphOption
106
+ }
107
+
108
+ return paragraphOption
109
+ }
110
+
111
+ export function setHeading(ctx: Ctx, level: number | null) {
112
+ const commands = ctx.get(commandsCtx)
113
+ if (level === null || level === undefined) {
114
+ const paragraph = paragraphSchema.type(ctx)
115
+ commands.call(setBlockTypeCommand.key, { nodeType: paragraph })
116
+ } else {
117
+ const heading = headingSchema.type(ctx)
118
+ commands.call(setBlockTypeCommand.key, {
119
+ nodeType: heading,
120
+ attrs: { level },
121
+ })
122
+ }
123
+ }
124
+
125
+ function isMarkActive(ctx: Ctx, markType: MarkType): boolean {
126
+ const commands = ctx.get(commandsCtx)
127
+ const selected = commands.call(isMarkSelectedCommand.key, markType)
128
+ if (selected) return true
129
+
130
+ const view = ctx.get(editorViewCtx)
131
+ const { state } = view
132
+
133
+ // Check stored marks (pending marks for next input)
134
+ if (state.storedMarks) {
135
+ return state.storedMarks.some((m) => m.type === markType)
136
+ }
137
+
138
+ // Check marks at cursor position (collapsed selection inside marked text)
139
+ if (state.selection instanceof TextSelection) {
140
+ const { $cursor } = state.selection
141
+ if ($cursor) {
142
+ return $cursor.marks().some((m) => m.type === markType)
143
+ }
144
+ }
145
+
146
+ return false
147
+ }
148
+
149
+ export function buildHeadingSelector(
150
+ headingOptions?: HeadingOption[],
151
+ chevronIcon?: string
152
+ ): TopBarItem {
153
+ const options = headingOptions ?? defaultHeadingOptions
154
+ return {
155
+ icon: '',
156
+ active: () => false,
157
+ selector: {
158
+ chevronIcon: chevronIcon ?? chevronDownIcon,
159
+ activeLabel: (ctx) => getCurrentHeading(ctx, options).label,
160
+ options: options.map((opt) => ({
161
+ label: opt.label,
162
+ onSelect: (ctx) => setHeading(ctx, opt.level),
163
+ })),
164
+ },
165
+ }
166
+ }
167
+
168
+ export function getGroups(config?: TopBarFeatureConfig, ctx?: Ctx) {
169
+ const flags = ctx && useCrepeFeatures(ctx).get()
170
+ const isLatexEnabled = flags?.includes(CrepeFeature.Latex)
171
+ const isImageBlockEnabled = flags?.includes(CrepeFeature.ImageBlock)
172
+ const isTableEnabled = flags?.includes(CrepeFeature.Table)
173
+
174
+ const groupBuilder = new GroupBuilder<TopBarItem>()
175
+
176
+ // Heading selector group
177
+ groupBuilder.addGroup('heading', 'Heading').addItem('heading-selector', {
178
+ ...buildHeadingSelector(config?.headingOptions, config?.chevronDownIcon),
179
+ })
180
+
181
+ // Formatting group
182
+ groupBuilder
183
+ .addGroup('formatting', 'Formatting')
184
+ .addItem('bold', {
185
+ icon: config?.boldIcon ?? boldIcon,
186
+ active: (ctx) => isMarkActive(ctx, strongSchema.type(ctx)),
187
+ onRun: (ctx) => {
188
+ const commands = ctx.get(commandsCtx)
189
+ commands.call(toggleStrongCommand.key)
190
+ },
191
+ })
192
+ .addItem('italic', {
193
+ icon: config?.italicIcon ?? italicIcon,
194
+ active: (ctx) => isMarkActive(ctx, emphasisSchema.type(ctx)),
195
+ onRun: (ctx) => {
196
+ const commands = ctx.get(commandsCtx)
197
+ commands.call(toggleEmphasisCommand.key)
198
+ },
199
+ })
200
+ .addItem('strikethrough', {
201
+ icon: config?.strikethroughIcon ?? strikethroughIcon,
202
+ active: (ctx) => isMarkActive(ctx, strikethroughSchema.type(ctx)),
203
+ onRun: (ctx) => {
204
+ const commands = ctx.get(commandsCtx)
205
+ commands.call(toggleStrikethroughCommand.key)
206
+ },
207
+ })
208
+ .addItem('code', {
209
+ icon: config?.codeIcon ?? codeIcon,
210
+ active: (ctx) => isMarkActive(ctx, inlineCodeSchema.type(ctx)),
211
+ onRun: (ctx) => {
212
+ const view = ctx.get(editorViewCtx)
213
+ const { state } = view
214
+ if (state.selection.empty) {
215
+ // toggleInlineCodeCommand doesn't support empty selection,
216
+ // so handle stored marks manually
217
+ const markType = inlineCodeSchema.type(ctx)
218
+ const has = isMarkActive(ctx, markType)
219
+ if (has) {
220
+ view.dispatch(state.tr.removeStoredMark(markType))
221
+ } else {
222
+ view.dispatch(state.tr.addStoredMark(markType.create()))
223
+ }
224
+ } else {
225
+ const commands = ctx.get(commandsCtx)
226
+ commands.call(toggleInlineCodeCommand.key)
227
+ }
228
+ },
229
+ })
230
+
231
+ // List group
232
+ groupBuilder
233
+ .addGroup('list', 'List')
234
+ .addItem('bullet-list', {
235
+ icon: config?.bulletListIcon ?? bulletListIcon,
236
+ active: () => false,
237
+ onRun: (ctx) => {
238
+ const commands = ctx.get(commandsCtx)
239
+ const bulletList = bulletListSchema.type(ctx)
240
+ commands.call(wrapInBlockTypeCommand.key, { nodeType: bulletList })
241
+ },
242
+ })
243
+ .addItem('ordered-list', {
244
+ icon: config?.orderedListIcon ?? orderedListIcon,
245
+ active: () => false,
246
+ onRun: (ctx) => {
247
+ const commands = ctx.get(commandsCtx)
248
+ const orderedList = orderedListSchema.type(ctx)
249
+ commands.call(wrapInBlockTypeCommand.key, { nodeType: orderedList })
250
+ },
251
+ })
252
+ .addItem('task-list', {
253
+ icon: config?.taskListIcon ?? todoListIcon,
254
+ active: () => false,
255
+ onRun: (ctx) => {
256
+ const commands = ctx.get(commandsCtx)
257
+ const listItem = listItemSchema.type(ctx)
258
+ commands.call(wrapInBlockTypeCommand.key, {
259
+ nodeType: listItem,
260
+ attrs: { checked: false },
261
+ })
262
+ },
263
+ })
264
+
265
+ // Insert group
266
+ const insertGroup = groupBuilder.addGroup('insert', 'Insert')
267
+ insertGroup.addItem('link', {
268
+ icon: config?.linkIcon ?? linkIcon,
269
+ active: (ctx) => isMarkActive(ctx, linkSchema.type(ctx)),
270
+ onRun: (ctx) => {
271
+ const view = ctx.get(editorViewCtx)
272
+ const { state } = view
273
+ const markType = linkSchema.type(ctx)
274
+
275
+ // When cursor is inside a link with empty selection,
276
+ // remove the stored link mark so next input won't be a link
277
+ if (state.selection.empty && isMarkActive(ctx, markType)) {
278
+ view.dispatch(state.tr.removeStoredMark(markType))
279
+ return
280
+ }
281
+
282
+ const commands = ctx.get(commandsCtx)
283
+ commands.call(toggleLinkCommand.key)
284
+ },
285
+ })
286
+
287
+ if (isImageBlockEnabled) {
288
+ insertGroup.addItem('image', {
289
+ icon: config?.imageIcon ?? imageIcon,
290
+ active: () => false,
291
+ onRun: (ctx) => {
292
+ const commands = ctx.get(commandsCtx)
293
+ const imageBlock = imageBlockSchema.type(ctx)
294
+ commands.call(addBlockTypeCommand.key, { nodeType: imageBlock })
295
+ },
296
+ })
297
+ }
298
+
299
+ if (isTableEnabled) {
300
+ insertGroup.addItem('table', {
301
+ icon: config?.tableIcon ?? tableIcon,
302
+ active: () => false,
303
+ onRun: (ctx) => {
304
+ const commands = ctx.get(commandsCtx)
305
+ const view = ctx.get(editorViewCtx)
306
+ const { from } = view.state.selection
307
+ commands.call(addBlockTypeCommand.key, {
308
+ nodeType: createTable(ctx, 3, 3),
309
+ })
310
+ commands.call(selectTextNearPosCommand.key, { pos: from })
311
+ },
312
+ })
313
+ }
314
+
315
+ // Block group
316
+ const blockGroup = groupBuilder.addGroup('block', 'Block')
317
+ blockGroup.addItem('code-block', {
318
+ icon: config?.codeBlockIcon ?? codeBlockIcon,
319
+ active: () => false,
320
+ onRun: (ctx) => {
321
+ const commands = ctx.get(commandsCtx)
322
+ const codeBlock = codeBlockSchema.type(ctx)
323
+ commands.call(setBlockTypeCommand.key, { nodeType: codeBlock })
324
+ },
325
+ })
326
+
327
+ if (isLatexEnabled) {
328
+ blockGroup.addItem('math', {
329
+ icon: config?.mathIcon ?? functionsIcon,
330
+ active: () => false,
331
+ onRun: (ctx) => {
332
+ const commands = ctx.get(commandsCtx)
333
+ const codeBlock = codeBlockSchema.type(ctx)
334
+ commands.call(addBlockTypeCommand.key, {
335
+ nodeType: codeBlock,
336
+ attrs: { language: 'LaTeX' },
337
+ })
338
+ },
339
+ })
340
+ }
341
+
342
+ // More group
343
+ groupBuilder
344
+ .addGroup('more', 'More')
345
+ .addItem('quote', {
346
+ icon: config?.quoteIcon ?? quoteIcon,
347
+ active: () => false,
348
+ onRun: (ctx) => {
349
+ const commands = ctx.get(commandsCtx)
350
+ const blockquote = blockquoteSchema.type(ctx)
351
+ commands.call(wrapInBlockTypeCommand.key, { nodeType: blockquote })
352
+ },
353
+ })
354
+ .addItem('hr', {
355
+ icon: config?.hrIcon ?? dividerIcon,
356
+ active: () => false,
357
+ onRun: (ctx) => {
358
+ const commands = ctx.get(commandsCtx)
359
+ const hr = hrSchema.type(ctx)
360
+ commands.call(addBlockTypeCommand.key, { nodeType: hr })
361
+ },
362
+ })
363
+
364
+ config?.buildTopBar?.(groupBuilder)
365
+
366
+ return groupBuilder.build()
367
+ }
@@ -0,0 +1,113 @@
1
+ import type { Ctx } from '@milkdown/kit/ctx'
2
+ import type { EditorState, PluginView } from '@milkdown/kit/prose/state'
3
+ import type { EditorView } from '@milkdown/kit/prose/view'
4
+
5
+ import { Plugin, PluginKey } from '@milkdown/kit/prose/state'
6
+ import { $ctx, $prose } from '@milkdown/kit/utils'
7
+ import { createApp, ref, type App, type Ref } from 'vue'
8
+
9
+ import type { GroupBuilder } from '../../utils'
10
+ import type { DefineFeature } from '../shared'
11
+ import type { HeadingOption, TopBarItem } from './config'
12
+
13
+ import { crepeFeatureConfig } from '../../core/slice'
14
+ import { CrepeFeature } from '../../feature'
15
+ import { TopBar } from './component'
16
+
17
+ interface TopBarConfig {
18
+ headingOptions: HeadingOption[]
19
+ boldIcon: string
20
+ italicIcon: string
21
+ strikethroughIcon: string
22
+ codeIcon: string
23
+ linkIcon: string
24
+ imageIcon: string
25
+ tableIcon: string
26
+ codeBlockIcon: string
27
+ mathIcon: string
28
+ quoteIcon: string
29
+ hrIcon: string
30
+ bulletListIcon: string
31
+ orderedListIcon: string
32
+ taskListIcon: string
33
+ chevronDownIcon: string
34
+ buildTopBar: (builder: GroupBuilder<TopBarItem>) => void
35
+ }
36
+
37
+ export type TopBarFeatureConfig = Partial<TopBarConfig>
38
+
39
+ const topBarPluginKey = new PluginKey('CREPE_TOP_BAR')
40
+
41
+ class TopBarView implements PluginView {
42
+ #container: HTMLElement
43
+ #app: App
44
+ #version: Ref<number>
45
+
46
+ constructor(ctx: Ctx, view: EditorView, config?: TopBarFeatureConfig) {
47
+ this.#version = ref(0)
48
+
49
+ const container = document.createElement('div')
50
+ container.className = 'milkdown-top-bar'
51
+
52
+ const app = createApp(TopBar, {
53
+ ctx,
54
+ config,
55
+ version: this.#version,
56
+ })
57
+ app.mount(container)
58
+ this.#container = container
59
+ this.#app = app
60
+
61
+ // Insert the top bar before the editor content
62
+ const editorRoot = view.dom.parentElement
63
+ if (editorRoot) {
64
+ editorRoot.insertBefore(container, editorRoot.firstChild)
65
+ }
66
+
67
+ this.update(view)
68
+ }
69
+
70
+ update = (view: EditorView, _prevState?: EditorState) => {
71
+ this.#container.style.display = view.editable ? '' : 'none'
72
+ this.#version.value++
73
+ }
74
+
75
+ destroy = () => {
76
+ this.#app.unmount()
77
+ this.#container.remove()
78
+ }
79
+ }
80
+
81
+ interface TopBarPluginConfig {
82
+ view: (view: EditorView) => PluginView
83
+ }
84
+
85
+ const topBarSlice = $ctx(
86
+ {
87
+ view: () => ({
88
+ update: () => {},
89
+ destroy: () => {},
90
+ }),
91
+ } as TopBarPluginConfig,
92
+ 'topBarConfig'
93
+ )
94
+
95
+ const topBarPlugin = $prose((ctx) => {
96
+ const config = ctx.get(topBarSlice.key)
97
+ return new Plugin({
98
+ key: topBarPluginKey,
99
+ view: config.view,
100
+ })
101
+ })
102
+
103
+ export const topBar: DefineFeature<TopBarFeatureConfig> = (editor, config) => {
104
+ editor
105
+ .config(crepeFeatureConfig(CrepeFeature.TopBar))
106
+ .config((ctx) => {
107
+ ctx.set(topBarSlice.key, {
108
+ view: (view) => new TopBarView(ctx, view, config),
109
+ })
110
+ })
111
+ .use(topBarSlice)
112
+ .use(topBarPlugin)
113
+ }
@@ -0,0 +1,12 @@
1
+ export const codeBlockIcon = `
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ width="24"
5
+ height="24"
6
+ viewBox="0 0 24 24"
7
+ >
8
+ <path
9
+ d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm1 2v14h16V5H4zm8 10h6v2h-6v-2zm-3.333-3L5.838 9.172l1.415-1.415L11.495 12l-4.242 4.243-1.415-1.415L8.667 12z"
10
+ />
11
+ </svg>
12
+ `
@@ -10,6 +10,7 @@ export * from './check-box-unchecked'
10
10
  export * from './chevron-down'
11
11
  export * from './clear'
12
12
  export * from './code'
13
+ export * from './code-block'
13
14
  export * from './confirm'
14
15
  export * from './copy'
15
16
  export * from './divider'
@@ -10,3 +10,4 @@
10
10
  @import './toolbar.css';
11
11
  @import './table.css';
12
12
  @import './latex.css';
13
+ @import './top-bar.css';