@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.
Files changed (93) hide show
  1. package/lib/cjs/builder.js +4 -0
  2. package/lib/cjs/builder.js.map +1 -1
  3. package/lib/cjs/feature/block-edit/index.js +39 -16
  4. package/lib/cjs/feature/block-edit/index.js.map +1 -1
  5. package/lib/cjs/feature/code-mirror/index.js +4 -0
  6. package/lib/cjs/feature/code-mirror/index.js.map +1 -1
  7. package/lib/cjs/feature/cursor/index.js +4 -0
  8. package/lib/cjs/feature/cursor/index.js.map +1 -1
  9. package/lib/cjs/feature/image-block/index.js +4 -0
  10. package/lib/cjs/feature/image-block/index.js.map +1 -1
  11. package/lib/cjs/feature/inline-diff/index.js +4 -0
  12. package/lib/cjs/feature/inline-diff/index.js.map +1 -1
  13. package/lib/cjs/feature/latex/index.js +4 -0
  14. package/lib/cjs/feature/latex/index.js.map +1 -1
  15. package/lib/cjs/feature/link-tooltip/index.js +4 -0
  16. package/lib/cjs/feature/link-tooltip/index.js.map +1 -1
  17. package/lib/cjs/feature/list-item/index.js +4 -0
  18. package/lib/cjs/feature/list-item/index.js.map +1 -1
  19. package/lib/cjs/feature/placeholder/index.js +4 -0
  20. package/lib/cjs/feature/placeholder/index.js.map +1 -1
  21. package/lib/cjs/feature/table/index.js +4 -0
  22. package/lib/cjs/feature/table/index.js.map +1 -1
  23. package/lib/cjs/feature/toolbar/index.js +56 -18
  24. package/lib/cjs/feature/toolbar/index.js.map +1 -1
  25. package/lib/cjs/index.js +604 -135
  26. package/lib/cjs/index.js.map +1 -1
  27. package/lib/esm/builder.js +4 -0
  28. package/lib/esm/builder.js.map +1 -1
  29. package/lib/esm/feature/block-edit/index.js +40 -17
  30. package/lib/esm/feature/block-edit/index.js.map +1 -1
  31. package/lib/esm/feature/code-mirror/index.js +4 -0
  32. package/lib/esm/feature/code-mirror/index.js.map +1 -1
  33. package/lib/esm/feature/cursor/index.js +4 -0
  34. package/lib/esm/feature/cursor/index.js.map +1 -1
  35. package/lib/esm/feature/image-block/index.js +4 -0
  36. package/lib/esm/feature/image-block/index.js.map +1 -1
  37. package/lib/esm/feature/inline-diff/index.js +4 -0
  38. package/lib/esm/feature/inline-diff/index.js.map +1 -1
  39. package/lib/esm/feature/latex/index.js +4 -0
  40. package/lib/esm/feature/latex/index.js.map +1 -1
  41. package/lib/esm/feature/link-tooltip/index.js +4 -0
  42. package/lib/esm/feature/link-tooltip/index.js.map +1 -1
  43. package/lib/esm/feature/list-item/index.js +4 -0
  44. package/lib/esm/feature/list-item/index.js.map +1 -1
  45. package/lib/esm/feature/placeholder/index.js +4 -0
  46. package/lib/esm/feature/placeholder/index.js.map +1 -1
  47. package/lib/esm/feature/table/index.js +4 -0
  48. package/lib/esm/feature/table/index.js.map +1 -1
  49. package/lib/esm/feature/toolbar/index.js +57 -19
  50. package/lib/esm/feature/toolbar/index.js.map +1 -1
  51. package/lib/esm/index.js +604 -135
  52. package/lib/esm/index.js.map +1 -1
  53. package/lib/theme/common/toolbar.css +6 -0
  54. package/lib/tsconfig.tsbuildinfo +1 -1
  55. package/lib/types/core/crepe.d.ts +1 -1
  56. package/lib/types/core/crepe.d.ts.map +1 -1
  57. package/lib/types/core/locale.d.ts +4 -0
  58. package/lib/types/core/locale.d.ts.map +1 -1
  59. package/lib/types/feature/block-edit/menu/component.d.ts.map +1 -1
  60. package/lib/types/feature/fixed-toolbar/component.d.ts +2 -0
  61. package/lib/types/feature/fixed-toolbar/component.d.ts.map +1 -1
  62. package/lib/types/feature/fixed-toolbar/config.d.ts.map +1 -1
  63. package/lib/types/feature/fixed-toolbar/index.d.ts +16 -0
  64. package/lib/types/feature/fixed-toolbar/index.d.ts.map +1 -1
  65. package/lib/types/feature/fixed-toolbar/menu-bar.d.ts.map +1 -1
  66. package/lib/types/feature/fixed-toolbar/outline-panel.d.ts.map +1 -1
  67. package/lib/types/feature/toolbar/component.d.ts.map +1 -1
  68. package/lib/types/icons/export.d.ts +2 -0
  69. package/lib/types/icons/export.d.ts.map +1 -0
  70. package/lib/types/icons/import.d.ts +2 -0
  71. package/lib/types/icons/import.d.ts.map +1 -0
  72. package/lib/types/icons/index.d.ts +4 -0
  73. package/lib/types/icons/index.d.ts.map +1 -1
  74. package/lib/types/icons/redo.d.ts +2 -0
  75. package/lib/types/icons/redo.d.ts.map +1 -0
  76. package/lib/types/icons/undo.d.ts +2 -0
  77. package/lib/types/icons/undo.d.ts.map +1 -0
  78. package/package.json +4 -4
  79. package/src/core/crepe.ts +60 -7
  80. package/src/core/locale.ts +4 -0
  81. package/src/feature/block-edit/menu/component.tsx +41 -17
  82. package/src/feature/fixed-toolbar/component.tsx +155 -51
  83. package/src/feature/fixed-toolbar/config.ts +70 -2
  84. package/src/feature/fixed-toolbar/index.ts +86 -1
  85. package/src/feature/fixed-toolbar/menu-bar.tsx +18 -3
  86. package/src/feature/fixed-toolbar/outline-panel.tsx +219 -32
  87. package/src/feature/toolbar/component.tsx +25 -2
  88. package/src/icons/export.ts +5 -0
  89. package/src/icons/import.ts +6 -0
  90. package/src/icons/index.ts +4 -0
  91. package/src/icons/redo.ts +5 -0
  92. package/src/icons/undo.ts +5 -0
  93. 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 { defineComponent, ref, onUnmounted, computed, onMounted, h, watch } from 'vue'
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 = ['file', 'edit', 'view', 'insert', 'format']
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
- // Use the scroll container's visible area, not the editor element
109
- // (editor top goes negative when scrolled, breaking threshold calculation)
110
- const scrollContainer = view.dom.parentElement
111
- if (!scrollContainer) return
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 toolbar = scrollContainer.querySelector(
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 = scrollContainer.getBoundingClientRect()
118
- // 50px buffer: tight enough to exclude the next heading
119
- // (min gap between consecutive headings ~44px for h5/h6)
120
- const threshold = containerRect.top + toolbarHeight + 50
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
- activeId.value = (active as any)?.id || ''
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 = (id: string) => {
223
+ const scrollToHeading = (item: OutlineItem) => {
135
224
  const view = props.ctx.get(editorViewCtx)
136
225
  if (!view) return
137
226
 
138
227
  try {
139
- const headingEl = view.dom.querySelector(
140
- `[id="${id}"]`
141
- ) as HTMLElement | null
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
- const scrollContainer = view.dom.parentElement
145
- if (!scrollContainer) return
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 toolbar = scrollContainer.querySelector(
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 containerRect = scrollContainer.getBoundingClientRect()
295
+ const containerRectTop = isRootScroll
296
+ ? 0
297
+ : actualScrollContainer.getBoundingClientRect().top
154
298
  const headingRect = headingEl.getBoundingClientRect()
155
299
 
156
- scrollContainer.scrollTo({
157
- top:
158
- scrollContainer.scrollTop +
159
- headingRect.top -
160
- containerRect.top -
161
- toolbarHeight -
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
- scrollContainer.removeEventListener('scroll', onScrollEnd)
313
+ scrollTarget.removeEventListener('scroll', onScrollEnd)
172
314
  checkActive()
173
315
  }, 150)
174
316
  }
175
- scrollContainer.addEventListener('scroll', onScrollEnd)
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
- scrollContainer.removeEventListener('scroll', onScrollEnd)
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
- scrollToHeading(item.id)
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
  }