@milkdown/crepe 7.19.2 → 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.
- package/lib/cjs/builder.js +43 -1
- package/lib/cjs/builder.js.map +1 -1
- package/lib/cjs/feature/block-edit/index.js +8 -2
- package/lib/cjs/feature/block-edit/index.js.map +1 -1
- package/lib/cjs/feature/code-mirror/index.js +1 -0
- package/lib/cjs/feature/code-mirror/index.js.map +1 -1
- package/lib/cjs/feature/cursor/index.js +1 -0
- package/lib/cjs/feature/cursor/index.js.map +1 -1
- package/lib/cjs/feature/image-block/index.js +4 -1
- package/lib/cjs/feature/image-block/index.js.map +1 -1
- package/lib/cjs/feature/latex/index.js +5 -0
- package/lib/cjs/feature/latex/index.js.map +1 -1
- package/lib/cjs/feature/link-tooltip/index.js +1 -0
- package/lib/cjs/feature/link-tooltip/index.js.map +1 -1
- package/lib/cjs/feature/list-item/index.js +1 -0
- package/lib/cjs/feature/list-item/index.js.map +1 -1
- package/lib/cjs/feature/placeholder/index.js +1 -0
- package/lib/cjs/feature/placeholder/index.js.map +1 -1
- package/lib/cjs/feature/table/index.js +1 -0
- package/lib/cjs/feature/table/index.js.map +1 -1
- package/lib/cjs/feature/toolbar/index.js +9 -2
- package/lib/cjs/feature/toolbar/index.js.map +1 -1
- package/lib/cjs/feature/top-bar/index.js +790 -0
- package/lib/cjs/feature/top-bar/index.js.map +1 -0
- package/lib/cjs/index.js +630 -142
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/builder.js +43 -1
- package/lib/esm/builder.js.map +1 -1
- package/lib/esm/feature/block-edit/index.js +8 -2
- package/lib/esm/feature/block-edit/index.js.map +1 -1
- package/lib/esm/feature/code-mirror/index.js +1 -0
- package/lib/esm/feature/code-mirror/index.js.map +1 -1
- package/lib/esm/feature/cursor/index.js +1 -0
- package/lib/esm/feature/cursor/index.js.map +1 -1
- package/lib/esm/feature/image-block/index.js +4 -1
- package/lib/esm/feature/image-block/index.js.map +1 -1
- package/lib/esm/feature/latex/index.js +5 -0
- package/lib/esm/feature/latex/index.js.map +1 -1
- package/lib/esm/feature/link-tooltip/index.js +1 -0
- package/lib/esm/feature/link-tooltip/index.js.map +1 -1
- package/lib/esm/feature/list-item/index.js +1 -0
- package/lib/esm/feature/list-item/index.js.map +1 -1
- package/lib/esm/feature/placeholder/index.js +1 -0
- package/lib/esm/feature/placeholder/index.js.map +1 -1
- package/lib/esm/feature/table/index.js +1 -0
- package/lib/esm/feature/table/index.js.map +1 -1
- package/lib/esm/feature/toolbar/index.js +9 -2
- package/lib/esm/feature/toolbar/index.js.map +1 -1
- package/lib/esm/feature/top-bar/index.js +788 -0
- package/lib/esm/feature/top-bar/index.js.map +1 -0
- package/lib/esm/index.js +631 -143
- package/lib/esm/index.js.map +1 -1
- package/lib/theme/common/style.css +1 -0
- package/lib/theme/common/top-bar.css +152 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/core/builder.d.ts +2 -1
- package/lib/types/core/builder.d.ts.map +1 -1
- package/lib/types/feature/block-edit/handle/component.d.ts.map +1 -1
- package/lib/types/feature/block-edit/menu/component.d.ts.map +1 -1
- package/lib/types/feature/image-block/index.d.ts +2 -0
- package/lib/types/feature/image-block/index.d.ts.map +1 -1
- package/lib/types/feature/index.d.ts +4 -1
- package/lib/types/feature/index.d.ts.map +1 -1
- package/lib/types/feature/latex/inline-tooltip/component.d.ts.map +1 -1
- package/lib/types/feature/loader.d.ts.map +1 -1
- package/lib/types/feature/toolbar/component.d.ts.map +1 -1
- package/lib/types/feature/toolbar/config.d.ts +1 -1
- package/lib/types/feature/top-bar/component.d.ts +11 -0
- package/lib/types/feature/top-bar/component.d.ts.map +1 -0
- package/lib/types/feature/top-bar/config.d.ts +34 -0
- package/lib/types/feature/top-bar/config.d.ts.map +1 -0
- package/lib/types/feature/top-bar/index.d.ts +26 -0
- package/lib/types/feature/top-bar/index.d.ts.map +1 -0
- package/lib/types/icons/code-block.d.ts +2 -0
- package/lib/types/icons/code-block.d.ts.map +1 -0
- package/lib/types/icons/index.d.ts +1 -0
- package/lib/types/icons/index.d.ts.map +1 -1
- package/lib/types/utils/group-builder.d.ts +1 -1
- package/lib/types/utils/group-builder.d.ts.map +1 -1
- package/lib/types/utils/keep-alive.d.ts +2 -0
- package/lib/types/utils/keep-alive.d.ts.map +1 -0
- package/package.json +18 -13
- package/src/core/builder.ts +39 -2
- package/src/feature/block-edit/handle/component.tsx +3 -2
- package/src/feature/block-edit/menu/component.tsx +3 -2
- package/src/feature/block-edit/menu/config.ts +1 -1
- package/src/feature/image-block/index.ts +4 -0
- package/src/feature/index.ts +6 -0
- package/src/feature/latex/inline-tooltip/component.tsx +4 -2
- package/src/feature/loader.ts +4 -0
- package/src/feature/toolbar/component.tsx +7 -5
- package/src/feature/top-bar/component.tsx +198 -0
- package/src/feature/top-bar/config.ts +367 -0
- package/src/feature/top-bar/index.ts +113 -0
- package/src/icons/code-block.ts +12 -0
- package/src/icons/index.ts +1 -0
- package/src/theme/common/style.css +1 -0
- package/src/theme/common/top-bar.css +156 -0
- package/src/utils/group-builder.ts +1 -1
- 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
|
+
`
|
package/src/icons/index.ts
CHANGED