@jvs-milkdown/crepe 1.2.16 → 1.2.18
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 +4 -0
- package/lib/cjs/builder.js.map +1 -1
- package/lib/cjs/feature/block-edit/index.js +39 -16
- package/lib/cjs/feature/block-edit/index.js.map +1 -1
- package/lib/cjs/feature/code-mirror/index.js +4 -0
- package/lib/cjs/feature/code-mirror/index.js.map +1 -1
- package/lib/cjs/feature/cursor/index.js +4 -0
- package/lib/cjs/feature/cursor/index.js.map +1 -1
- package/lib/cjs/feature/image-block/index.js +4 -0
- package/lib/cjs/feature/image-block/index.js.map +1 -1
- package/lib/cjs/feature/inline-diff/index.js +4 -0
- package/lib/cjs/feature/inline-diff/index.js.map +1 -1
- package/lib/cjs/feature/latex/index.js +4 -0
- package/lib/cjs/feature/latex/index.js.map +1 -1
- package/lib/cjs/feature/link-tooltip/index.js +4 -0
- package/lib/cjs/feature/link-tooltip/index.js.map +1 -1
- package/lib/cjs/feature/list-item/index.js +4 -0
- package/lib/cjs/feature/list-item/index.js.map +1 -1
- package/lib/cjs/feature/placeholder/index.js +4 -0
- package/lib/cjs/feature/placeholder/index.js.map +1 -1
- package/lib/cjs/feature/table/index.js +4 -0
- package/lib/cjs/feature/table/index.js.map +1 -1
- package/lib/cjs/feature/toolbar/index.js +56 -18
- package/lib/cjs/feature/toolbar/index.js.map +1 -1
- package/lib/cjs/index.js +604 -135
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/builder.js +4 -0
- package/lib/esm/builder.js.map +1 -1
- package/lib/esm/feature/block-edit/index.js +40 -17
- package/lib/esm/feature/block-edit/index.js.map +1 -1
- package/lib/esm/feature/code-mirror/index.js +4 -0
- package/lib/esm/feature/code-mirror/index.js.map +1 -1
- package/lib/esm/feature/cursor/index.js +4 -0
- package/lib/esm/feature/cursor/index.js.map +1 -1
- package/lib/esm/feature/image-block/index.js +4 -0
- package/lib/esm/feature/image-block/index.js.map +1 -1
- package/lib/esm/feature/inline-diff/index.js +4 -0
- package/lib/esm/feature/inline-diff/index.js.map +1 -1
- package/lib/esm/feature/latex/index.js +4 -0
- package/lib/esm/feature/latex/index.js.map +1 -1
- package/lib/esm/feature/link-tooltip/index.js +4 -0
- package/lib/esm/feature/link-tooltip/index.js.map +1 -1
- package/lib/esm/feature/list-item/index.js +4 -0
- package/lib/esm/feature/list-item/index.js.map +1 -1
- package/lib/esm/feature/placeholder/index.js +4 -0
- package/lib/esm/feature/placeholder/index.js.map +1 -1
- package/lib/esm/feature/table/index.js +4 -0
- package/lib/esm/feature/table/index.js.map +1 -1
- package/lib/esm/feature/toolbar/index.js +57 -19
- package/lib/esm/feature/toolbar/index.js.map +1 -1
- package/lib/esm/index.js +604 -135
- package/lib/esm/index.js.map +1 -1
- package/lib/theme/common/toolbar.css +6 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/core/crepe.d.ts +1 -1
- package/lib/types/core/crepe.d.ts.map +1 -1
- package/lib/types/core/locale.d.ts +4 -0
- package/lib/types/core/locale.d.ts.map +1 -1
- package/lib/types/feature/block-edit/menu/component.d.ts.map +1 -1
- package/lib/types/feature/fixed-toolbar/component.d.ts +2 -0
- package/lib/types/feature/fixed-toolbar/component.d.ts.map +1 -1
- package/lib/types/feature/fixed-toolbar/config.d.ts.map +1 -1
- package/lib/types/feature/fixed-toolbar/index.d.ts +16 -0
- package/lib/types/feature/fixed-toolbar/index.d.ts.map +1 -1
- package/lib/types/feature/fixed-toolbar/menu-bar.d.ts.map +1 -1
- package/lib/types/feature/fixed-toolbar/outline-panel.d.ts.map +1 -1
- package/lib/types/feature/toolbar/component.d.ts.map +1 -1
- package/lib/types/icons/export.d.ts +2 -0
- package/lib/types/icons/export.d.ts.map +1 -0
- package/lib/types/icons/import.d.ts +2 -0
- package/lib/types/icons/import.d.ts.map +1 -0
- package/lib/types/icons/index.d.ts +4 -0
- package/lib/types/icons/index.d.ts.map +1 -1
- package/lib/types/icons/redo.d.ts +2 -0
- package/lib/types/icons/redo.d.ts.map +1 -0
- package/lib/types/icons/undo.d.ts +2 -0
- package/lib/types/icons/undo.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/core/crepe.ts +60 -7
- package/src/core/locale.ts +4 -0
- package/src/feature/block-edit/menu/component.tsx +41 -17
- package/src/feature/fixed-toolbar/component.tsx +155 -51
- package/src/feature/fixed-toolbar/config.ts +70 -2
- package/src/feature/fixed-toolbar/index.ts +86 -1
- package/src/feature/fixed-toolbar/menu-bar.tsx +18 -3
- package/src/feature/fixed-toolbar/outline-panel.tsx +219 -32
- package/src/feature/toolbar/component.tsx +25 -2
- package/src/icons/export.ts +5 -0
- package/src/icons/import.ts +6 -0
- package/src/icons/index.ts +4 -0
- package/src/icons/redo.ts +5 -0
- package/src/icons/undo.ts +5 -0
- package/src/theme/common/toolbar.css +16 -0
|
@@ -29,6 +29,8 @@ import { createTable } from '@jvs-milkdown/kit/preset/gfm'
|
|
|
29
29
|
import { findNodeInSelection } from '@jvs-milkdown/kit/prose'
|
|
30
30
|
import { lift } from '@jvs-milkdown/kit/prose/commands'
|
|
31
31
|
import { wrapInList } from '@jvs-milkdown/kit/prose/schema-list'
|
|
32
|
+
import { getMarkdown } from '@jvs-milkdown/kit/utils'
|
|
33
|
+
import { replaceAll } from '@jvs-milkdown/utils'
|
|
32
34
|
|
|
33
35
|
import type { GroupBuilder } from '../../utils'
|
|
34
36
|
import type { ToolbarItem } from '../toolbar/config'
|
|
@@ -50,6 +52,9 @@ import {
|
|
|
50
52
|
highLineCodeIcon,
|
|
51
53
|
linCodeIcon,
|
|
52
54
|
linkIcon,
|
|
55
|
+
exportIcon,
|
|
56
|
+
importIcon,
|
|
57
|
+
keyboardIcon,
|
|
53
58
|
} from '../../icons'
|
|
54
59
|
import { attachmentSchema } from '../attachment/schema'
|
|
55
60
|
import { CrepeFeature } from '../index'
|
|
@@ -472,7 +477,6 @@ export function buildDefaultFixedToolbar(
|
|
|
472
477
|
},
|
|
473
478
|
})
|
|
474
479
|
|
|
475
|
-
|
|
476
480
|
if (isAttachmentEnabled) {
|
|
477
481
|
blockGroup.addItem('attachment', {
|
|
478
482
|
label: ctx ? i18n(ctx, 'menu.item.attachment') : 'Video or File',
|
|
@@ -532,7 +536,6 @@ export function buildDefaultFixedToolbar(
|
|
|
532
536
|
},
|
|
533
537
|
})
|
|
534
538
|
|
|
535
|
-
|
|
536
539
|
if (isLatexEnabled) {
|
|
537
540
|
blockGroup.addItem('math-block', {
|
|
538
541
|
label: ctx ? i18n(ctx, 'menu.item.math') : 'Math Block',
|
|
@@ -552,5 +555,70 @@ export function buildDefaultFixedToolbar(
|
|
|
552
555
|
})
|
|
553
556
|
}
|
|
554
557
|
|
|
558
|
+
const documentGroup = builder.addGroup('document', 'Document')
|
|
559
|
+
|
|
560
|
+
if (_config?.showExport !== false) {
|
|
561
|
+
documentGroup.addItem('export', {
|
|
562
|
+
label: ctx ? i18n(ctx, 'customMenu.export' as any) || '导出' : '导出',
|
|
563
|
+
icon: exportIcon,
|
|
564
|
+
active: () => false,
|
|
565
|
+
onRun: (ctx) => {
|
|
566
|
+
const markdown = getMarkdown()(ctx)
|
|
567
|
+
if (_config?.onExport) {
|
|
568
|
+
_config.onExport(markdown, ctx)
|
|
569
|
+
} else {
|
|
570
|
+
const blob = new Blob([markdown], {
|
|
571
|
+
type: 'text/markdown;charset=utf-8;',
|
|
572
|
+
})
|
|
573
|
+
const url = URL.createObjectURL(blob)
|
|
574
|
+
const link = document.createElement('a')
|
|
575
|
+
link.href = url
|
|
576
|
+
link.download = 'document.md'
|
|
577
|
+
link.click()
|
|
578
|
+
URL.revokeObjectURL(url)
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (_config?.showImport !== false) {
|
|
585
|
+
documentGroup.addItem('import', {
|
|
586
|
+
label: ctx ? i18n(ctx, 'customMenu.import' as any) || '导入' : '导入',
|
|
587
|
+
icon: importIcon,
|
|
588
|
+
active: () => false,
|
|
589
|
+
onRun: (ctx) => {
|
|
590
|
+
if (_config?.onImport) {
|
|
591
|
+
_config.onImport((markdown) => replaceAll(markdown)(ctx), ctx)
|
|
592
|
+
} else {
|
|
593
|
+
const input = document.createElement('input')
|
|
594
|
+
input.type = 'file'
|
|
595
|
+
input.accept = '.md'
|
|
596
|
+
input.onchange = (e) => {
|
|
597
|
+
const file = (e.target as HTMLInputElement).files?.[0]
|
|
598
|
+
if (!file) return
|
|
599
|
+
file
|
|
600
|
+
.text()
|
|
601
|
+
.then((text) => {
|
|
602
|
+
replaceAll(text)(ctx)
|
|
603
|
+
})
|
|
604
|
+
.catch((err) => {
|
|
605
|
+
console.error('Failed to read file:', err)
|
|
606
|
+
})
|
|
607
|
+
}
|
|
608
|
+
input.click()
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
documentGroup.addItem('shortcuts', {
|
|
615
|
+
label: ctx ? i18n(ctx, 'shortcuts.title') : 'Shortcuts',
|
|
616
|
+
icon: keyboardIcon,
|
|
617
|
+
active: () => false,
|
|
618
|
+
onRun: () => {
|
|
619
|
+
window.dispatchEvent(new CustomEvent('milkdown-show-shortcuts'))
|
|
620
|
+
},
|
|
621
|
+
})
|
|
622
|
+
|
|
555
623
|
return builder.build()
|
|
556
624
|
}
|
|
@@ -3,7 +3,8 @@ import type { PluginView, Selection } from '@jvs-milkdown/kit/prose/state'
|
|
|
3
3
|
import type { EditorView } from '@jvs-milkdown/kit/prose/view'
|
|
4
4
|
|
|
5
5
|
import { Plugin, PluginKey, TextSelection } from '@jvs-milkdown/kit/prose/state'
|
|
6
|
-
import { $ctx, $prose } from '@jvs-milkdown/kit/utils'
|
|
6
|
+
import { $ctx, $prose, getMarkdown } from '@jvs-milkdown/kit/utils'
|
|
7
|
+
import { undo, redo } from '@jvs-milkdown/prose/history'
|
|
7
8
|
// @ts-ignore
|
|
8
9
|
import {
|
|
9
10
|
createApp,
|
|
@@ -44,6 +45,21 @@ export interface FixedToolbarConfig {
|
|
|
44
45
|
outlinePosition?: 'left' | 'right'
|
|
45
46
|
onUploadCover?: (file: File) => Promise<string>
|
|
46
47
|
defaultCoverImages?: string[]
|
|
48
|
+
showMenuBar?: boolean
|
|
49
|
+
menuBarItems?: {
|
|
50
|
+
file?: boolean
|
|
51
|
+
edit?: boolean
|
|
52
|
+
view?: boolean
|
|
53
|
+
insert?: boolean
|
|
54
|
+
format?: boolean
|
|
55
|
+
}
|
|
56
|
+
showHistory?: boolean
|
|
57
|
+
showExport?: boolean
|
|
58
|
+
onExport?: (markdown: string, ctx: Ctx) => void
|
|
59
|
+
showImport?: boolean
|
|
60
|
+
onImport?: (replaceContent: (markdown: string) => void, ctx: Ctx) => void
|
|
61
|
+
useLocalStorage?: boolean
|
|
62
|
+
id?: string
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
export type FixedToolbarFeatureConfig = Partial<FixedToolbarConfig>
|
|
@@ -65,6 +81,8 @@ class FixedToolbarView implements PluginView {
|
|
|
65
81
|
#watcher?: WatchStopHandle
|
|
66
82
|
#selection: ShallowRef<Selection>
|
|
67
83
|
#show = ref(true)
|
|
84
|
+
#canUndo = ref(false)
|
|
85
|
+
#canRedo = ref(false)
|
|
68
86
|
#resizeObserver?: ResizeObserver
|
|
69
87
|
#updateOutlineGeometry?: () => void
|
|
70
88
|
#scrollContainers: Element[] = []
|
|
@@ -75,6 +93,65 @@ class FixedToolbarView implements PluginView {
|
|
|
75
93
|
constructor(ctx: Ctx, view: EditorView) {
|
|
76
94
|
this.#view = view
|
|
77
95
|
const config = ctx.get(fixedToolbarConfig.key)
|
|
96
|
+
const viewState = ctx.get(viewMenuStateCtx.key)
|
|
97
|
+
|
|
98
|
+
// Load initial view menu state from localStorage if useLocalStorage is enabled
|
|
99
|
+
if (config?.useLocalStorage) {
|
|
100
|
+
try {
|
|
101
|
+
const stored = localStorage.getItem('jvs-milkdown-data')
|
|
102
|
+
if (stored) {
|
|
103
|
+
const parsed = JSON.parse(stored)
|
|
104
|
+
if (parsed.outlineVisible !== undefined)
|
|
105
|
+
viewState.outlineVisible = parsed.outlineVisible
|
|
106
|
+
if (parsed.outlinePosition !== undefined)
|
|
107
|
+
viewState.outlinePosition = parsed.outlinePosition
|
|
108
|
+
if (parsed.outlineWidth !== undefined)
|
|
109
|
+
viewState.outlineWidth = parsed.outlineWidth
|
|
110
|
+
if (parsed.documentBackground !== undefined)
|
|
111
|
+
viewState.documentBackground = parsed.documentBackground
|
|
112
|
+
if (parsed.showTitle !== undefined)
|
|
113
|
+
viewState.showTitle = parsed.showTitle
|
|
114
|
+
if (parsed.showCover !== undefined)
|
|
115
|
+
viewState.showCover = parsed.showCover
|
|
116
|
+
if (parsed.coverUrl !== undefined)
|
|
117
|
+
viewState.coverUrl = parsed.coverUrl
|
|
118
|
+
if (parsed.editorWidth !== undefined)
|
|
119
|
+
viewState.editorWidth = parsed.editorWidth
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error('Error loading view state from localStorage:', e)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Automatically watch and persist view state changes to localStorage
|
|
126
|
+
watch(
|
|
127
|
+
viewState,
|
|
128
|
+
(newState) => {
|
|
129
|
+
try {
|
|
130
|
+
const stored = localStorage.getItem('jvs-milkdown-data')
|
|
131
|
+
let parsed = {}
|
|
132
|
+
if (stored) {
|
|
133
|
+
parsed = JSON.parse(stored)
|
|
134
|
+
}
|
|
135
|
+
const merged = {
|
|
136
|
+
...parsed,
|
|
137
|
+
outlineVisible: newState.outlineVisible,
|
|
138
|
+
outlinePosition: newState.outlinePosition,
|
|
139
|
+
outlineWidth: newState.outlineWidth,
|
|
140
|
+
documentBackground: newState.documentBackground,
|
|
141
|
+
showTitle: newState.showTitle,
|
|
142
|
+
showCover: newState.showCover,
|
|
143
|
+
coverUrl: newState.coverUrl,
|
|
144
|
+
editorWidth: newState.editorWidth,
|
|
145
|
+
}
|
|
146
|
+
localStorage.setItem('jvs-milkdown-data', JSON.stringify(merged))
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.error('Error saving view state to localStorage:', e)
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{ deep: true }
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
78
155
|
const content = document.createElement('div')
|
|
79
156
|
content.className = 'milkdown-fixed-toolbar'
|
|
80
157
|
this.#selection = shallowRef(view.state.selection)
|
|
@@ -85,6 +162,8 @@ class FixedToolbarView implements PluginView {
|
|
|
85
162
|
config,
|
|
86
163
|
selection: this.#selection,
|
|
87
164
|
show: this.#show,
|
|
165
|
+
canUndo: this.#canUndo,
|
|
166
|
+
canRedo: this.#canRedo,
|
|
88
167
|
})
|
|
89
168
|
app.mount(content)
|
|
90
169
|
this.#content = content
|
|
@@ -317,6 +396,12 @@ class FixedToolbarView implements PluginView {
|
|
|
317
396
|
|
|
318
397
|
update = (view: EditorView) => {
|
|
319
398
|
this.#selection.value = view.state.selection
|
|
399
|
+
try {
|
|
400
|
+
this.#canUndo.value = undo(view.state)
|
|
401
|
+
this.#canRedo.value = redo(view.state)
|
|
402
|
+
} catch (e) {
|
|
403
|
+
// Ignore initialization errors
|
|
404
|
+
}
|
|
320
405
|
}
|
|
321
406
|
|
|
322
407
|
destroy = () => {
|
|
@@ -2,7 +2,15 @@ import type { Ctx } from '@jvs-milkdown/kit/ctx'
|
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@jvs-milkdown/kit/component'
|
|
4
4
|
// @ts-ignore
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
defineComponent,
|
|
7
|
+
ref,
|
|
8
|
+
onUnmounted,
|
|
9
|
+
computed,
|
|
10
|
+
onMounted,
|
|
11
|
+
h,
|
|
12
|
+
watch,
|
|
13
|
+
} from 'vue'
|
|
6
14
|
|
|
7
15
|
import type { FixedToolbarConfig } from './index'
|
|
8
16
|
|
|
@@ -143,7 +151,14 @@ export const MenuBar = defineComponent({
|
|
|
143
151
|
{ name: '浅粉', value: '#FDE8E9' },
|
|
144
152
|
]
|
|
145
153
|
|
|
146
|
-
const menuKeys =
|
|
154
|
+
const menuKeys = computed(() => {
|
|
155
|
+
const allKeys = ['file', 'edit', 'view', 'insert', 'format']
|
|
156
|
+
const itemsConfig = props.config?.menuBarItems
|
|
157
|
+
if (!itemsConfig) return allKeys
|
|
158
|
+
return allKeys.filter(
|
|
159
|
+
(key) => itemsConfig[key as keyof typeof itemsConfig] !== false
|
|
160
|
+
)
|
|
161
|
+
})
|
|
147
162
|
const hasSubmenu = (key: string) => key === 'view'
|
|
148
163
|
|
|
149
164
|
return () => {
|
|
@@ -232,7 +247,7 @@ export const MenuBar = defineComponent({
|
|
|
232
247
|
e.stopPropagation()
|
|
233
248
|
}}
|
|
234
249
|
>
|
|
235
|
-
{menuKeys.map((menuKey) => {
|
|
250
|
+
{menuKeys.value.map((menuKey) => {
|
|
236
251
|
const isHovered = activeSubmenu.value === menuKey
|
|
237
252
|
const label =
|
|
238
253
|
i18n(props.ctx, `menuBar.${menuKey}` as any) || menuKey
|
|
@@ -25,6 +25,12 @@ export const OutlinePanel = defineComponent({
|
|
|
25
25
|
const activeId = ref<string>('')
|
|
26
26
|
const collapsedIds = ref<Set<string>>(new Set())
|
|
27
27
|
let scrollLock = false
|
|
28
|
+
const clickedActiveId = ref<string | null>(null)
|
|
29
|
+
|
|
30
|
+
const clearClickedActive = () => {
|
|
31
|
+
if (scrollLock) return
|
|
32
|
+
clickedActiveId.value = null
|
|
33
|
+
}
|
|
28
34
|
|
|
29
35
|
const hasChildren = (index: number) => {
|
|
30
36
|
const current = items.value[index]
|
|
@@ -97,6 +103,11 @@ export const OutlinePanel = defineComponent({
|
|
|
97
103
|
const view = props.ctx.get(editorViewCtx)
|
|
98
104
|
if (!view || !view.dom) return
|
|
99
105
|
|
|
106
|
+
if (clickedActiveId.value) {
|
|
107
|
+
activeId.value = clickedActiveId.value
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
100
111
|
const headings = Array.from(
|
|
101
112
|
view.dom.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
|
102
113
|
)
|
|
@@ -105,19 +116,61 @@ export const OutlinePanel = defineComponent({
|
|
|
105
116
|
return
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
// Find the actual scroll container
|
|
120
|
+
let scrollContainer: HTMLElement | null = view.dom.parentElement
|
|
121
|
+
while (scrollContainer && scrollContainer !== document.body) {
|
|
122
|
+
const style = window.getComputedStyle(scrollContainer)
|
|
123
|
+
const overflowY = style.overflowY
|
|
124
|
+
if (
|
|
125
|
+
(overflowY === 'auto' || overflowY === 'scroll') &&
|
|
126
|
+
scrollContainer.scrollHeight > scrollContainer.clientHeight
|
|
127
|
+
) {
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
scrollContainer = scrollContainer.parentElement
|
|
131
|
+
}
|
|
112
132
|
|
|
113
|
-
const
|
|
133
|
+
const isRootScroll =
|
|
134
|
+
!scrollContainer ||
|
|
135
|
+
scrollContainer === document.body ||
|
|
136
|
+
scrollContainer === document.documentElement
|
|
137
|
+
const actualScrollContainer = isRootScroll
|
|
138
|
+
? ((document.scrollingElement ||
|
|
139
|
+
document.documentElement ||
|
|
140
|
+
document.body) as HTMLElement)
|
|
141
|
+
: scrollContainer!
|
|
142
|
+
|
|
143
|
+
const rootNode = view.dom.getRootNode() as Document | ShadowRoot
|
|
144
|
+
let toolbar = rootNode.querySelector(
|
|
114
145
|
'.milkdown-fixed-toolbar'
|
|
115
146
|
) as HTMLElement | null
|
|
147
|
+
if (!toolbar) {
|
|
148
|
+
toolbar = document.querySelector(
|
|
149
|
+
'.milkdown-fixed-toolbar'
|
|
150
|
+
) as HTMLElement | null
|
|
151
|
+
}
|
|
116
152
|
const toolbarHeight = toolbar?.offsetHeight || 0
|
|
117
|
-
const containerRect =
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
const
|
|
153
|
+
const containerRect = actualScrollContainer.getBoundingClientRect()
|
|
154
|
+
// Use Math.max(0, containerRect.top) because when container scrolls off-screen,
|
|
155
|
+
// its visual top boundary is the top of the viewport (0)
|
|
156
|
+
const containerTop = isRootScroll ? 0 : Math.max(0, containerRect.top)
|
|
157
|
+
|
|
158
|
+
let scrollTop = actualScrollContainer.scrollTop
|
|
159
|
+
let clientHeight = actualScrollContainer.clientHeight
|
|
160
|
+
let scrollHeight = actualScrollContainer.scrollHeight
|
|
161
|
+
|
|
162
|
+
if (isRootScroll) {
|
|
163
|
+
scrollTop = window.scrollY || document.documentElement.scrollTop
|
|
164
|
+
clientHeight =
|
|
165
|
+
window.innerHeight || document.documentElement.clientHeight
|
|
166
|
+
scrollHeight =
|
|
167
|
+
document.documentElement.scrollHeight || document.body.scrollHeight
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 15
|
|
171
|
+
const threshold = isAtBottom
|
|
172
|
+
? containerTop + clientHeight
|
|
173
|
+
: containerTop + toolbarHeight + 50
|
|
121
174
|
|
|
122
175
|
let active = headings[0]
|
|
123
176
|
for (const h of headings as HTMLElement[]) {
|
|
@@ -128,57 +181,158 @@ export const OutlinePanel = defineComponent({
|
|
|
128
181
|
break
|
|
129
182
|
}
|
|
130
183
|
}
|
|
131
|
-
|
|
184
|
+
|
|
185
|
+
// Compare DOM node references directly to identify active outline item without relying on ID
|
|
186
|
+
const activeItem = items.value.find((item) => {
|
|
187
|
+
try {
|
|
188
|
+
const activePos = view.posAtDOM(active, 0)
|
|
189
|
+
const isPosMatch = item.pos === activePos - 1
|
|
190
|
+
|
|
191
|
+
let isDepthMatch = false
|
|
192
|
+
const $pos = view.state.doc.resolve(activePos)
|
|
193
|
+
for (let d = $pos.depth; d >= 0; d--) {
|
|
194
|
+
if ($pos.node(d).type.name === 'heading') {
|
|
195
|
+
if (item.pos === $pos.before(d)) {
|
|
196
|
+
isDepthMatch = true
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (isPosMatch || isDepthMatch) {
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
// Silent catch
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const dom = view.nodeDOM(item.pos)
|
|
210
|
+
const domMatch =
|
|
211
|
+
dom === active ||
|
|
212
|
+
(dom instanceof HTMLElement && active && dom.contains(active as Node))
|
|
213
|
+
if (domMatch) {
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
return false
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const newActiveId = activeItem ? activeItem.id : (active as any)?.id || ''
|
|
220
|
+
activeId.value = newActiveId
|
|
132
221
|
}
|
|
133
222
|
|
|
134
|
-
const scrollToHeading = (
|
|
223
|
+
const scrollToHeading = (item: OutlineItem) => {
|
|
135
224
|
const view = props.ctx.get(editorViewCtx)
|
|
136
225
|
if (!view) return
|
|
137
226
|
|
|
138
227
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
228
|
+
let headingEl = view.nodeDOM(item.pos) as HTMLElement | null
|
|
229
|
+
if (!headingEl) {
|
|
230
|
+
try {
|
|
231
|
+
const domAt = view.domAtPos(item.pos)
|
|
232
|
+
let node = domAt.node
|
|
233
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
234
|
+
node = node.parentElement!
|
|
235
|
+
}
|
|
236
|
+
if (node instanceof HTMLElement) {
|
|
237
|
+
headingEl = node.closest(
|
|
238
|
+
'h1, h2, h3, h4, h5, h6'
|
|
239
|
+
) as HTMLElement | null
|
|
240
|
+
if (!headingEl && domAt.offset < node.childNodes.length) {
|
|
241
|
+
const child = node.childNodes[domAt.offset]
|
|
242
|
+
if (child instanceof HTMLElement) {
|
|
243
|
+
headingEl = (child.closest('h1, h2, h3, h4, h5, h6') ||
|
|
244
|
+
child.querySelector(
|
|
245
|
+
'h1, h2, h3, h4, h5, h6'
|
|
246
|
+
)) as HTMLElement | null
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
if (!headingEl) {
|
|
253
|
+
headingEl = view.dom.querySelector(
|
|
254
|
+
`[id="${item.id}"]`
|
|
255
|
+
) as HTMLElement | null
|
|
256
|
+
}
|
|
142
257
|
if (!headingEl) return
|
|
143
258
|
|
|
144
|
-
|
|
145
|
-
|
|
259
|
+
// Find the actual scroll container
|
|
260
|
+
let scrollContainer: HTMLElement | null = view.dom.parentElement
|
|
261
|
+
while (scrollContainer && scrollContainer !== document.body) {
|
|
262
|
+
const style = window.getComputedStyle(scrollContainer)
|
|
263
|
+
const overflowY = style.overflowY
|
|
264
|
+
if (
|
|
265
|
+
(overflowY === 'auto' || overflowY === 'scroll') &&
|
|
266
|
+
scrollContainer.scrollHeight > scrollContainer.clientHeight
|
|
267
|
+
) {
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
scrollContainer = scrollContainer.parentElement
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const isRootScroll =
|
|
274
|
+
!scrollContainer ||
|
|
275
|
+
scrollContainer === document.body ||
|
|
276
|
+
scrollContainer === document.documentElement
|
|
277
|
+
const actualScrollContainer = isRootScroll
|
|
278
|
+
? ((document.scrollingElement ||
|
|
279
|
+
document.documentElement ||
|
|
280
|
+
document.body) as HTMLElement)
|
|
281
|
+
: scrollContainer!
|
|
146
282
|
|
|
147
283
|
scrollLock = true
|
|
148
284
|
|
|
149
|
-
const
|
|
285
|
+
const rootNode = view.dom.getRootNode() as Document | ShadowRoot
|
|
286
|
+
let toolbar = rootNode.querySelector(
|
|
150
287
|
'.milkdown-fixed-toolbar'
|
|
151
288
|
) as HTMLElement | null
|
|
289
|
+
if (!toolbar) {
|
|
290
|
+
toolbar = document.querySelector(
|
|
291
|
+
'.milkdown-fixed-toolbar'
|
|
292
|
+
) as HTMLElement | null
|
|
293
|
+
}
|
|
152
294
|
const toolbarHeight = toolbar?.offsetHeight || 0
|
|
153
|
-
const
|
|
295
|
+
const containerRectTop = isRootScroll
|
|
296
|
+
? 0
|
|
297
|
+
: actualScrollContainer.getBoundingClientRect().top
|
|
154
298
|
const headingRect = headingEl.getBoundingClientRect()
|
|
155
299
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
20,
|
|
163
|
-
behavior: 'smooth',
|
|
164
|
-
})
|
|
300
|
+
const targetTop =
|
|
301
|
+
actualScrollContainer.scrollTop +
|
|
302
|
+
headingRect.top -
|
|
303
|
+
containerRectTop -
|
|
304
|
+
toolbarHeight -
|
|
305
|
+
20
|
|
165
306
|
|
|
307
|
+
const scrollTarget = isRootScroll ? window : actualScrollContainer
|
|
166
308
|
let timer: ReturnType<typeof setTimeout>
|
|
167
309
|
const onScrollEnd = () => {
|
|
168
310
|
clearTimeout(timer)
|
|
169
311
|
timer = setTimeout(() => {
|
|
170
312
|
scrollLock = false
|
|
171
|
-
|
|
313
|
+
scrollTarget.removeEventListener('scroll', onScrollEnd)
|
|
172
314
|
checkActive()
|
|
173
315
|
}, 150)
|
|
174
316
|
}
|
|
175
|
-
|
|
317
|
+
scrollTarget.addEventListener('scroll', onScrollEnd)
|
|
176
318
|
|
|
177
319
|
// Safety timeout in case scroll events are not fired
|
|
178
320
|
setTimeout(() => {
|
|
179
321
|
scrollLock = false
|
|
180
|
-
|
|
322
|
+
scrollTarget.removeEventListener('scroll', onScrollEnd)
|
|
181
323
|
}, 2000)
|
|
324
|
+
|
|
325
|
+
if (isRootScroll) {
|
|
326
|
+
window.scrollTo({
|
|
327
|
+
top: targetTop,
|
|
328
|
+
behavior: 'smooth',
|
|
329
|
+
})
|
|
330
|
+
} else {
|
|
331
|
+
actualScrollContainer.scrollTo({
|
|
332
|
+
top: targetTop,
|
|
333
|
+
behavior: 'smooth',
|
|
334
|
+
})
|
|
335
|
+
}
|
|
182
336
|
} catch {
|
|
183
337
|
scrollLock = false
|
|
184
338
|
updateOutline()
|
|
@@ -192,6 +346,31 @@ export const OutlinePanel = defineComponent({
|
|
|
192
346
|
onMounted(() => {
|
|
193
347
|
updateOutline()
|
|
194
348
|
checkActive()
|
|
349
|
+
|
|
350
|
+
window.addEventListener('wheel', clearClickedActive, { passive: true })
|
|
351
|
+
window.addEventListener('touchmove', clearClickedActive, {
|
|
352
|
+
passive: true,
|
|
353
|
+
})
|
|
354
|
+
window.addEventListener('pointerdown', clearClickedActive, {
|
|
355
|
+
passive: true,
|
|
356
|
+
})
|
|
357
|
+
const keyHandler = (e: KeyboardEvent) => {
|
|
358
|
+
if (
|
|
359
|
+
[
|
|
360
|
+
'ArrowUp',
|
|
361
|
+
'ArrowDown',
|
|
362
|
+
'PageUp',
|
|
363
|
+
'PageDown',
|
|
364
|
+
'Home',
|
|
365
|
+
'End',
|
|
366
|
+
].includes(e.key)
|
|
367
|
+
) {
|
|
368
|
+
clearClickedActive()
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
window.addEventListener('keydown', keyHandler, { passive: true })
|
|
372
|
+
;(onMounted as any)._keyHandler = keyHandler
|
|
373
|
+
|
|
195
374
|
interval = setInterval(() => {
|
|
196
375
|
if (viewState.value.outlineVisible) {
|
|
197
376
|
updateOutline()
|
|
@@ -212,6 +391,13 @@ export const OutlinePanel = defineComponent({
|
|
|
212
391
|
if (interval) clearInterval(interval)
|
|
213
392
|
if (pollInterval) clearInterval(pollInterval)
|
|
214
393
|
window.removeEventListener('scroll', checkActive, true)
|
|
394
|
+
window.removeEventListener('wheel', clearClickedActive)
|
|
395
|
+
window.removeEventListener('touchmove', clearClickedActive)
|
|
396
|
+
window.removeEventListener('pointerdown', clearClickedActive)
|
|
397
|
+
const keyHandler = (onMounted as any)._keyHandler
|
|
398
|
+
if (keyHandler) {
|
|
399
|
+
window.removeEventListener('keydown', keyHandler)
|
|
400
|
+
}
|
|
215
401
|
})
|
|
216
402
|
|
|
217
403
|
// Drag to resize logic
|
|
@@ -359,7 +545,8 @@ export const OutlinePanel = defineComponent({
|
|
|
359
545
|
key={item.id}
|
|
360
546
|
onClick={() => {
|
|
361
547
|
activeId.value = item.id
|
|
362
|
-
|
|
548
|
+
clickedActiveId.value = item.id
|
|
549
|
+
scrollToHeading(item)
|
|
363
550
|
}}
|
|
364
551
|
style={{
|
|
365
552
|
display: 'flex',
|
|
@@ -429,7 +616,7 @@ export const OutlinePanel = defineComponent({
|
|
|
429
616
|
'transparent'
|
|
430
617
|
}}
|
|
431
618
|
>
|
|
432
|
-
|
|
619
|
+
{'\u25BC'}{' '}
|
|
433
620
|
</span>
|
|
434
621
|
{/* Text - click to scroll */}
|
|
435
622
|
<span
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
EditorStatus,
|
|
8
8
|
editorViewCtx,
|
|
9
9
|
commandsCtx,
|
|
10
|
+
schemaCtx,
|
|
10
11
|
} from '@jvs-milkdown/kit/core'
|
|
11
12
|
import {
|
|
12
13
|
addBlockTypeCommand,
|
|
@@ -1094,6 +1095,12 @@ export const Toolbar = defineComponent<ToolbarProps>({
|
|
|
1094
1095
|
|
|
1095
1096
|
const view = ctx.get(editorViewCtx)
|
|
1096
1097
|
const { state } = view
|
|
1098
|
+
const schema = ctx.get(schemaCtx)
|
|
1099
|
+
|
|
1100
|
+
const tcHasMark = schema.marks[textColorSchema.id]
|
|
1101
|
+
const bcHasMark = schema.marks[bgColorSchema.id]
|
|
1102
|
+
|
|
1103
|
+
if (!tcHasMark || !bcHasMark) return { textColor: null, bgColor: null }
|
|
1097
1104
|
|
|
1098
1105
|
const tcType = textColorSchema.type(ctx)
|
|
1099
1106
|
const bcType = bgColorSchema.type(ctx)
|
|
@@ -1147,6 +1154,12 @@ export const Toolbar = defineComponent<ToolbarProps>({
|
|
|
1147
1154
|
const { state, dispatch } = view
|
|
1148
1155
|
const { tr } = state
|
|
1149
1156
|
const { from, to, empty } = state.selection
|
|
1157
|
+
const schema = ctx.get(schemaCtx)
|
|
1158
|
+
|
|
1159
|
+
const tcHasMark = schema.marks[textColorSchema.id]
|
|
1160
|
+
const bcHasMark = schema.marks[bgColorSchema.id]
|
|
1161
|
+
|
|
1162
|
+
if (!tcHasMark || !bcHasMark) return
|
|
1150
1163
|
|
|
1151
1164
|
const textColorType = textColorSchema.type(ctx)
|
|
1152
1165
|
const bgColorType = bgColorSchema.type(ctx)
|
|
@@ -1173,6 +1186,12 @@ export const Toolbar = defineComponent<ToolbarProps>({
|
|
|
1173
1186
|
|
|
1174
1187
|
const view = ctx.get(editorViewCtx)
|
|
1175
1188
|
const { state } = view
|
|
1189
|
+
const schema = ctx.get(schemaCtx)
|
|
1190
|
+
|
|
1191
|
+
const ffHasMark = schema.marks[fontFamilySchema.id]
|
|
1192
|
+
const fsHasMark = schema.marks[fontSizeSchema.id]
|
|
1193
|
+
|
|
1194
|
+
if (!ffHasMark || !fsHasMark) return { fontFamily: null, fontSize: null }
|
|
1176
1195
|
|
|
1177
1196
|
const ffType = fontFamilySchema.type(ctx)
|
|
1178
1197
|
const fsType = fontSizeSchema.type(ctx)
|
|
@@ -1612,6 +1631,7 @@ export const Toolbar = defineComponent<ToolbarProps>({
|
|
|
1612
1631
|
'toolbar-item',
|
|
1613
1632
|
ctx && checkActive(item.active) && 'active'
|
|
1614
1633
|
)}
|
|
1634
|
+
data-key={item.key}
|
|
1615
1635
|
onPointerdown={(e: PointerEvent) => {
|
|
1616
1636
|
if (isTable) {
|
|
1617
1637
|
e.preventDefault()
|
|
@@ -2528,11 +2548,13 @@ export const Toolbar = defineComponent<ToolbarProps>({
|
|
|
2528
2548
|
onClick: ((e: MouseEvent) => void) | undefined,
|
|
2529
2549
|
hasDropdown = false,
|
|
2530
2550
|
onMouseEnter?: (e: MouseEvent) => void,
|
|
2531
|
-
onMouseLeave?: (e: MouseEvent) => void
|
|
2551
|
+
onMouseLeave?: (e: MouseEvent) => void,
|
|
2552
|
+
key?: string
|
|
2532
2553
|
) => (
|
|
2533
2554
|
<button
|
|
2534
2555
|
type="button"
|
|
2535
2556
|
class={clsx('toolbar-item', isActive && 'active')}
|
|
2557
|
+
data-key={key}
|
|
2536
2558
|
title={title}
|
|
2537
2559
|
onPointerdown={(e: PointerEvent) => {
|
|
2538
2560
|
e.preventDefault()
|
|
@@ -2667,7 +2689,8 @@ export const Toolbar = defineComponent<ToolbarProps>({
|
|
|
2667
2689
|
},
|
|
2668
2690
|
isTable,
|
|
2669
2691
|
isTable ? handleTableEnter : undefined,
|
|
2670
|
-
isTable ? handleTableLeave : undefined
|
|
2692
|
+
isTable ? handleTableLeave : undefined,
|
|
2693
|
+
item.key
|
|
2671
2694
|
)
|
|
2672
2695
|
)
|
|
2673
2696
|
}
|