11import type { MaybeRefOrGetter } from 'vue'
22import { EditorView } from '@codemirror/view'
33
4+ /**
5+ * 点击预览区元素时,定位回编辑器对应位置。
6+ */
47export function useCursorSync (
58 codeMirrorViewRef : MaybeRefOrGetter < EditorView | null > ,
6- previewContainerRef : MaybeRefOrGetter < HTMLElement | null > ,
79) {
8- const cursorSyncTimer = ref < ReturnType < typeof setTimeout > > ( )
9- const skipCursorDrivenPreviewSync = ref ( false )
10-
1110 const getEditorView = ( ) => toValue ( codeMirrorViewRef )
12- const getPreviewContainer = ( ) => toValue ( previewContainerRef )
1311
1412 function normalizeText ( text : string ) {
1513 return text
@@ -18,62 +16,23 @@ export function useCursorSync(
1816 }
1917
2018 function parseMarkdownHeadingLine ( line : string ) : { level : number , title : string } | null {
21- if ( ! line . startsWith ( `#` ) ) {
19+ if ( ! line . startsWith ( `#` ) )
2220 return null
23- }
2421
2522 let level = 0
26- while ( level < line . length && line [ level ] === `#` && level < 6 ) {
23+ while ( level < line . length && line [ level ] === `#` && level < 6 )
2724 level ++
28- }
2925
30- if ( level === 0 || line [ level ] !== ` ` ) {
26+ if ( level === 0 || line [ level ] !== ` ` )
3127 return null
32- }
3328
3429 const title = normalizeText ( line . slice ( level + 1 ) . replace ( / # + \s * $ / , `` ) )
35- if ( ! title ) {
30+ if ( ! title )
3631 return null
37- }
3832
3933 return { level, title }
4034 }
4135
42- function scrollPreviewToElement ( el : HTMLElement , behavior : ScrollBehavior = `auto` ) {
43- const container = getPreviewContainer ( )
44- if ( ! container )
45- return
46-
47- const cRect = container . getBoundingClientRect ( )
48- const eRect = el . getBoundingClientRect ( )
49- const inView = eRect . top >= cRect . top + 32 && eRect . bottom <= cRect . bottom - 32
50-
51- if ( ! inView ) {
52- el . scrollIntoView ( { behavior, block : `center` } )
53- }
54- }
55-
56- function findHeadingElementInPreview ( title : string , level ?: number ) {
57- const headings = document . querySelectorAll < HTMLElement > ( `#output [data-heading]` )
58- const normalizedTitle = normalizeText ( title )
59-
60- for ( const heading of headings ) {
61- if ( level && Number ( heading . tagName . slice ( 1 ) ) !== level )
62- continue
63- if ( normalizeText ( heading . textContent || `` ) === normalizedTitle ) {
64- return heading
65- }
66- }
67-
68- for ( const heading of headings ) {
69- if ( level && Number ( heading . tagName . slice ( 1 ) ) !== level )
70- continue
71- if ( normalizeText ( heading . textContent || `` ) . includes ( normalizedTitle ) ) {
72- return heading
73- }
74- }
75- }
76-
7736 function findHeadingPosInEditor ( title : string , level ?: number ) {
7837 const view = getEditorView ( )
7938 if ( ! view )
@@ -119,73 +78,13 @@ export function useCursorSync(
11978
12079 for ( const candidate of candidates ) {
12180 const pos = docText . indexOf ( candidate )
122- if ( pos !== - 1 ) {
81+ if ( pos !== - 1 )
12382 return pos
124- }
12583 }
12684
12785 return null
12886 }
12987
130- function focusEditorAtPos ( pos : number ) {
131- const view = getEditorView ( )
132- if ( ! view )
133- return
134-
135- skipCursorDrivenPreviewSync . value = true
136- view . dispatch ( {
137- selection : { anchor : pos } ,
138- effects : EditorView . scrollIntoView ( pos , { y : `center` } ) ,
139- } )
140- view . focus ( )
141-
142- setTimeout ( ( ) => {
143- skipCursorDrivenPreviewSync . value = false
144- } , 180 )
145- }
146-
147- function syncPreviewToEditorCursor ( ) {
148- if ( skipCursorDrivenPreviewSync . value )
149- return
150-
151- const view = getEditorView ( )
152- if ( ! view )
153- return
154-
155- const cursorPos = view . state . selection . main . head
156- const doc = view . state . doc
157- const cursorLineNo = doc . lineAt ( cursorPos ) . number
158-
159- // 优先按"最近标题"进行语义定位,避免图片/代码块造成的高度失真。
160- for ( let lineNo = cursorLineNo ; lineNo >= 1 ; lineNo -- ) {
161- const text = doc . line ( lineNo ) . text
162- const parsed = parseMarkdownHeadingLine ( text )
163- if ( ! parsed )
164- continue
165-
166- const headingEl = findHeadingElementInPreview ( parsed . title , parsed . level )
167- if ( headingEl ) {
168- scrollPreviewToElement ( headingEl )
169- return
170- }
171- }
172-
173- // 无可用语义锚点时,退化为轻量比例定位。
174- const container = getPreviewContainer ( )
175- if ( ! container )
176- return
177- const maxScrollTop = container . scrollHeight - container . offsetHeight
178- const ratio = doc . length > 0 ? cursorPos / doc . length : 0
179- container . scrollTo ( { top : Math . max ( 0 , maxScrollTop * ratio ) , behavior : `auto` } )
180- }
181-
182- function scheduleSyncPreviewToEditorCursor ( ) {
183- clearTimeout ( cursorSyncTimer . value )
184- cursorSyncTimer . value = setTimeout ( ( ) => {
185- syncPreviewToEditorCursor ( )
186- } , 100 )
187- }
188-
18988 function syncEditorToPreviewElement ( el : HTMLElement ) {
19089 const tag = el . tagName . toLowerCase ( )
19190 let pos : number | null = null
@@ -209,7 +108,15 @@ export function useCursorSync(
209108 }
210109
211110 if ( pos != null ) {
212- focusEditorAtPos ( pos )
111+ const view = getEditorView ( )
112+ if ( ! view )
113+ return
114+
115+ view . dispatch ( {
116+ selection : { anchor : pos } ,
117+ effects : EditorView . scrollIntoView ( pos , { y : `center` } ) ,
118+ } )
119+ view . focus ( )
213120 }
214121 }
215122
@@ -225,14 +132,7 @@ export function useCursorSync(
225132 syncEditorToPreviewElement ( block )
226133 }
227134
228- function cleanup ( ) {
229- clearTimeout ( cursorSyncTimer . value )
230- }
231-
232135 return {
233- skipCursorDrivenPreviewSync,
234- scheduleSyncPreviewToEditorCursor,
235136 handlePreviewContentClick,
236- cleanup,
237137 }
238138}
0 commit comments