Skip to content

Commit cf90f8a

Browse files
committed
feat: sync scroll
1 parent 645b375 commit cf90f8a

File tree

18 files changed

+169
-228
lines changed

18 files changed

+169
-228
lines changed

packages/markdown-editor/css/nextra/scrollbar.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
.nextra-scrollbar {
1+
.markdown-editor-scrollbar {
22
scrollbar-width: thin; /* Firefox */
33
scrollbar-color: oklch(55.55% 0 0 / 40%) transparent; /* Firefox */
44

55
scrollbar-gutter: stable;
66
&::-webkit-scrollbar {
7-
@apply nx-w-3 nx-h-3;
7+
@apply nx-h-3 nx-w-3;
88
}
99
&::-webkit-scrollbar-track {
1010
@apply nx-bg-transparent;

packages/markdown-editor/css/styles.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ summary {
5959
contain: layout style;
6060
backface-visibility: hidden;
6161

62-
& > .nextra-scrollbar {
62+
& > .markdown-editor-scrollbar {
6363
mask-image: linear-gradient(to bottom, transparent, #000 20px),
6464
linear-gradient(to left, #000 10px, transparent 10px);
6565
}

packages/markdown-editor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@theguild/remark-mermaid": "0.0.6",
5959
"@theguild/remark-npm2yarn": "0.2.0",
6060
"@uiw/codemirror-extensions-basic-setup": "^4.22.1",
61+
"@uiw/codemirror-extensions-events": "^4.23.0",
6162
"@uiw/codemirror-extensions-hyper-link": "^4.22.1",
6263
"@uiw/codemirror-themes": "^4.22.1",
6364
"@uiw/codemirror-themes-all": "^4.22.1",

packages/markdown-editor/src/components/head.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export function Head(): ReactElement {
5353
--nextra-navbar-height: 4rem;
5454
--nextra-menu-height: 3.75rem;
5555
--nextra-banner-height: 2.5rem;
56+
--nextra-editor-toolbar-height: 3rem;
5657
}
5758
5859
.dark {

packages/markdown-editor/src/components/markdown-editor/hooks/useCodemirror.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { getDefaultExtensions } from './../lib/getDefaultExtensions'
2-
import { Annotation, EditorState, StateEffect } from '@codemirror/state'
2+
import { Annotation, EditorState, Extension, StateEffect } from '@codemirror/state'
33
import { EditorView, ViewUpdate } from '@codemirror/view'
44
import { useTheme } from 'next-themes'
5-
import { ForwardedRef, RefObject, useEffect, useState } from 'react'
5+
import { RefObject, useEffect, useState } from 'react'
66
import { useMarkdownEditor } from '../../../contexts/markdown-editor'
77
import { getEditorStat } from '../lib/getEditorStat'
88
import { hyperLink } from '@uiw/codemirror-extensions-hyper-link'
@@ -20,11 +20,12 @@ type Config = {
2020
width?: string | null
2121
minWidth?: string | null
2222
maxWidth?: string | null
23+
extension?: Extension[]
2324
}
2425

2526
const External = Annotation.define<boolean>()
2627

27-
export const useCodemirror = (container: ForwardedRef<HTMLDivElement>, config: Config = {}) => {
28+
export const useCodemirror = (container: RefObject<HTMLDivElement>, config: Config = {}) => {
2829
const { theme: currentTheme } = useTheme()
2930
const { value, setValue, setStat } = useMarkdownEditor()
3031
const [state, setState] = useState<EditorState | null>(null)
@@ -38,6 +39,7 @@ export const useCodemirror = (container: ForwardedRef<HTMLDivElement>, config: C
3839
width = null,
3940
minWidth = null,
4041
maxWidth = null,
42+
extension = [],
4143
} = config
4244

4345
const eventHandlers = EditorView.domEventHandlers({
@@ -56,7 +58,7 @@ export const useCodemirror = (container: ForwardedRef<HTMLDivElement>, config: C
5658
maxWidth,
5759
},
5860
'& .cm-editor': {
59-
// height: '100%',
61+
height: '100%',
6062
},
6163
'& .cm-scroller': {
6264
width: '100%',
@@ -98,6 +100,7 @@ export const useCodemirror = (container: ForwardedRef<HTMLDivElement>, config: C
98100
defaultThemeOption,
99101
eventHandlers,
100102
...defaultExtensions,
103+
...extension,
101104
]
102105

103106
// create state
Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,57 @@
1-
import { forwardRef, useEffect } from 'react'
1+
import cn from 'clsx'
2+
import { forwardRef, useEffect, useRef } from 'react'
23
import { useCodemirror } from './hooks/useCodemirror'
34
import { Toolbar } from './toolbar'
5+
import { ReactCodeMirrorRef } from '@/types'
6+
import { Extension } from '@codemirror/state'
47

5-
export const MarkdownEditor = forwardRef<HTMLDivElement>(({}, container) => {
6-
const containerHeight = 'calc(100vh - (var(--nextra-navbar-height)))'
7-
const { state, view } = useCodemirror(container, {
8-
autoFocus: true,
9-
minHeight: '100%',
10-
maxHeight: '100%',
11-
})
8+
interface MarkdownEditorProps {
9+
codeMirrorExtensions?: Extension[]
10+
}
1211

13-
const onClick = () => {
14-
console.log('clicked!')
15-
view?.focus()
16-
}
12+
export const MarkdownEditor = forwardRef<ReactCodeMirrorRef, MarkdownEditorProps>(
13+
({ codeMirrorExtensions = [] }, editorRef) => {
14+
const codemirror = useRef<HTMLDivElement | null>(null)
15+
const { state, view } = useCodemirror(codemirror, {
16+
autoFocus: true,
17+
minHeight: '100%',
18+
maxHeight: '100%',
19+
extension: codeMirrorExtensions,
20+
})
1721

18-
useEffect(() => {
19-
onClick()
20-
}, [])
22+
useEffect(() => {
23+
if (!codemirror.current) return
24+
if (!view || !state) return
25+
if (typeof editorRef === 'function') {
26+
editorRef({ editor: codemirror.current, view, state })
27+
} else if (editorRef && 'current' in editorRef) {
28+
editorRef.current = {
29+
editor: codemirror.current,
30+
view,
31+
state,
32+
}
33+
}
34+
}, [editorRef, view, state])
2135

22-
return (
23-
<>
24-
<Toolbar state={state} view={view} />
25-
<div onClick={onClick}>
36+
const onClick = () => {
37+
view?.focus()
38+
}
39+
40+
return (
41+
<>
42+
<Toolbar state={state} view={view} />
2643
<div
27-
ref={container}
28-
style={{ height: containerHeight }}
44+
className={cn('markdown-editor-codemirror')}
45+
onClick={onClick}
46+
ref={codemirror}
2947
suppressHydrationWarning={true}
3048
suppressContentEditableWarning={true}
49+
style={{
50+
height:
51+
'calc(100vh - (var(--nextra-navbar-height)) - var(--nextra-editor-toolbar-height))',
52+
}}
3153
/>
32-
</div>
33-
</>
34-
)
35-
})
54+
</>
55+
)
56+
},
57+
)

packages/markdown-editor/src/components/markdown-editor/toolbar/toolbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export const Toolbar = ({ state, view }: Props) => {
114114
}
115115

116116
return (
117-
<div className={cn('nx-flex nx-flex-row nx-items-center')}>
117+
<div className={cn('markdown-editor-toolbar nx-flex nx-flex-row nx-items-center')}>
118118
{commands.map((command, index) => {
119119
const key = `${command.name}-${index}`
120120
if (!isValidElement(command.icon)) {

packages/markdown-editor/src/components/markdown-preview/markdown-preview.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useMarkdownEditor } from '@/contexts/markdown-editor'
88
interface MarkdownPreviewProps {}
99

1010
export const MarkdownPreview = forwardRef<HTMLDivElement, MarkdownPreviewProps>(
11-
({}, ref): ReactElement => {
11+
(props, ref): ReactElement => {
1212
const config = useConfig()
1313
const { mdxSource } = useMarkdownEditor()
1414

@@ -20,7 +20,8 @@ export const MarkdownPreview = forwardRef<HTMLDivElement, MarkdownPreviewProps>(
2020
<div
2121
ref={ref}
2222
className={cn(
23-
'nextra-scrollbar nx-h-screen nx-overflow-y-auto nx-break-words nx-pb-16',
23+
'markdown-editor-preview',
24+
'markdown-editor-scrollbar nx-h-screen nx-overflow-y-auto nx-break-words nx-pb-16',
2425
)}
2526
>
2627
<main className="nx-mt-6 nx-w-full nx-min-w-0 nx-max-w-6xl nx-px-6 nx-pt-6">

packages/markdown-editor/src/components/search.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export function Search({
223223
>
224224
<ul
225225
className={cn(
226-
'nextra-scrollbar',
226+
'markdown-editor-scrollbar',
227227
// Using bg-white as background-color when the browser didn't support backdrop-filter
228228
'nx-border nx-border-gray-200 nx-bg-white nx-text-gray-100 dark:nx-border-neutral-800 dark:nx-bg-neutral-900',
229229
'nx-absolute nx-top-full nx-z-20 nx-mt-2 nx-overflow-auto nx-overscroll-contain nx-rounded-xl nx-py-2.5 nx-shadow-xl',

packages/markdown-editor/src/components/sidebar/sortable-tree/sortable-tree.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ export function SortableTree({ items, sidebarRef, showSidebar, onItemsChanged }:
308308
'nx-overflow-y-auto',
309309
'nx-grow nx-px-4 md:nx-h-[calc(100vh-var(--nextra-navbar-height)-var(--nextra-menu-height))]',
310310
'nx-pb-4',
311-
showSidebar ? 'nextra-scrollbar' : 'no-scrollbar',
311+
showSidebar ? 'markdown-editor-scrollbar' : 'no-scrollbar',
312312
)}
313313
ref={sidebarRef}
314314
>

packages/markdown-editor/src/components/toc.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function TOC({ headings, filePath }: TOCProps): ReactElement {
5151
<div
5252
ref={tocRef}
5353
className={cn(
54-
'nextra-scrollbar nx-sticky nx-top-16 nx-overflow-y-auto nx-pr-4 nx-pt-6 nx-text-sm [hyphens:auto]',
54+
'markdown-editor-scrollbar nx-sticky nx-top-16 nx-overflow-y-auto nx-pr-4 nx-pt-6 nx-text-sm [hyphens:auto]',
5555
'nx-max-h-[calc(100vh-var(--nextra-navbar-height)-env(safe-area-inset-bottom))] ltr:-nx-mr-4 rtl:-nx-ml-4',
5656
)}
5757
>

packages/markdown-editor/src/layouts/main.tsx

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,32 @@ import { ActiveAnchorProvider } from '@/contexts'
44
import { useFSRoute } from '@/nextra/hooks'
55
import { normalizePages } from '@/nextra/normalize-pages'
66
import type { PageOpts } from '@/nextra/types'
7-
import { useEffect, useMemo, useRef, useState, type ReactElement, type ReactNode } from 'react'
7+
import {
8+
useCallback,
9+
useEffect,
10+
useMemo,
11+
useRef,
12+
useState,
13+
type ReactElement,
14+
type ReactNode,
15+
} from 'react'
816
import { Banner, Head, Header, Sidebar } from '@/components'
917
import { MarkdownPreview } from '@/components/markdown-preview'
1018
import { MarkdownEditor } from '@/components/markdown-editor'
19+
import { useMarkdownEditor } from '@/contexts/markdown-editor'
20+
import { ReactCodeMirrorRef } from '@/types'
21+
import * as events from '@uiw/codemirror-extensions-events'
1122

1223
type MainProps = PageOpts & {
1324
children: ReactNode
1425
}
1526

1627
export const Main = ({ frontMatter, headings, pageMap }: MainProps): ReactElement => {
1728
const fsPath = useFSRoute()
18-
const editorRef = useRef<HTMLDivElement>(null)
29+
const { mdxSource } = useMarkdownEditor()
30+
const editorRef = useRef<ReactCodeMirrorRef>(null)
1931
const previewRef = useRef<HTMLDivElement>(null)
20-
21-
const rerender = useState({})[1]
22-
23-
useEffect(() => {
24-
const trigger = () => rerender({})
25-
trigger()
26-
}, [])
27-
28-
useEffect(() => {
29-
console.log('editorRef:', editorRef)
30-
console.log('previewRef:', previewRef)
31-
}, [editorRef, previewRef])
32+
const active = useRef<'editor' | 'preview'>('editor')
3233

3334
const { activeThemeContext, docsDirectories, flatDirectories, directories, topLevelNavbarItems } =
3435
useMemo(
@@ -46,6 +47,48 @@ export const Main = ({ frontMatter, headings, pageMap }: MainProps): ReactElemen
4647
const direction = 'ltr'
4748
const mainHeight = 'calc(100vh - (var(--nextra-navbar-height)))'
4849

50+
const previewScrollHandle = useCallback((event: Event) => {
51+
const target = event.target as HTMLDivElement
52+
53+
const percent = target.scrollTop / target.scrollHeight
54+
55+
console.log('percent', percent)
56+
console.log('active.current', active.current)
57+
58+
if (active.current === 'editor' && previewRef.current) {
59+
const previewHeight = previewRef.current?.scrollHeight || 0
60+
previewRef.current.scrollTop = previewHeight
61+
}
62+
// else if (editorRef.current && editorRef.current.view) {
63+
// const editorScrollDom = editorRef.current.view.scrollDOM
64+
// const editorScrollHeihgt = editorRef.current.view.scrollDOM.scrollHeight || 0
65+
// editorScrollDom.scrollTop = editorScrollHeihgt * percent
66+
// }
67+
}, [])
68+
69+
const mouseoverHandle = () => (active.current = 'preview')
70+
const mouseleaveHandle = () => (active.current = 'editor')
71+
useEffect(() => {
72+
const $preview = previewRef.current
73+
if ($preview) {
74+
console.log('add event listener')
75+
$preview.addEventListener('mouseover', mouseoverHandle, false)
76+
$preview.addEventListener('mouseleave', mouseleaveHandle, false)
77+
$preview.addEventListener('scroll', previewScrollHandle, false)
78+
}
79+
return () => {
80+
if ($preview) {
81+
$preview.removeEventListener('mouseover', mouseoverHandle)
82+
$preview.removeEventListener('mouseleave', mouseoverHandle)
83+
$preview.addEventListener('mouseleave', previewScrollHandle, false)
84+
}
85+
}
86+
}, [previewRef, previewScrollHandle])
87+
88+
const scrollExtensions = events.scroll({
89+
scroll: previewScrollHandle,
90+
})
91+
4992
return (
5093
<div
5194
dir={direction}
@@ -75,10 +118,10 @@ export const Main = ({ frontMatter, headings, pageMap }: MainProps): ReactElemen
75118
includePlaceholder={themeContext.layout === 'default'}
76119
/>
77120
<div className={cn('nx-flex nx-overflow-hidden')} style={{ width: 'calc(100% - 320px)' }}>
78-
<div className={cn('nextra-editor-container nx-w-1/2')}>
79-
<MarkdownEditor ref={editorRef} />
121+
<div className={cn('nextra-editor-container nx-h-[100%] nx-w-1/2')}>
122+
<MarkdownEditor ref={editorRef} codeMirrorExtensions={[scrollExtensions]} />
80123
</div>
81-
<div className={cn('nextra-preview-container nx-w-1/2')}>
124+
<div className={cn('nextra-preview-container nx-h-[100%] nx-w-1/2')}>
82125
<MarkdownPreview ref={previewRef} />
83126
</div>
84127
</div>

packages/markdown-editor/src/mdx-components.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export const getComponents = ({
224224
),
225225
a: Link,
226226
table: (props) => (
227-
<Table className="nextra-scrollbar nx-mt-6 nx-p-0 first:nx-mt-0" {...props} />
227+
<Table className="markdown-editor-scrollbar nx-mt-6 nx-p-0 first:nx-mt-0" {...props} />
228228
),
229229
p: (props) => <p className="nx-mt-6 nx-leading-7 first:nx-mt-0" {...props} />,
230230
tr: Tr,

packages/markdown-editor/src/nextra/components/tabs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function _Tabs({
7878
defaultIndex={defaultIndex}
7979
onChange={handleChange}
8080
>
81-
<div className="nextra-scrollbar nx-overflow-x-auto nx-overflow-y-hidden nx-overscroll-x-contain">
81+
<div className="markdown-editor-scrollbar nx-overflow-x-auto nx-overflow-y-hidden nx-overscroll-x-contain">
8282
<HeadlessTab.List className="nx-mt-4 nx-flex nx-w-max nx-min-w-full nx-border-b nx-border-gray-200 nx-pb-px dark:nx-border-neutral-800">
8383
{items.map((item, index) => {
8484
const disabled = isTabObjectItem(item) && item.disabled

packages/markdown-editor/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { DocsThemeConfig } from './constants'
44
import type { ProcessorOptions } from '@mdx-js/mdx'
55
import type { UniqueIdentifier } from '@dnd-kit/core'
66
import type { PageItem, SortableItem } from './nextra/normalize-pages'
7+
import type { EditorState } from '@codemirror/state'
8+
import type { EditorView } from 'codemirror'
79

810
export type Context = {
911
pageOpts: PageOpts
@@ -163,3 +165,9 @@ export type ItemChangedReason<T = PageItem> =
163165
*/
164166
item: TreeItem<T>
165167
}
168+
169+
export interface ReactCodeMirrorRef {
170+
editor?: HTMLDivElement | null
171+
state?: EditorState
172+
view?: EditorView
173+
}

packages/markdown-editor/style.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/markdown-editor/tsup.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { defineConfig } from 'tsup'
22

33
export default defineConfig({
4-
name: 'nextra-editor',
4+
name: 'markdown-editor',
55
entry: ['src/index.tsx'],
66
format: 'esm',
77
dts: true,

0 commit comments

Comments
 (0)