@@ -3,12 +3,17 @@ import {
33 ProseMirrorDoc ,
44 reactKeys ,
55 useEditorEffect ,
6+ useEditorEventCallback ,
67} from "@handlewithcare/react-prosemirror" ;
78import { dropCursor } from "prosemirror-dropcursor" ;
89import { gapCursor } from "prosemirror-gapcursor" ;
910import { history } from "prosemirror-history" ;
1011import { Node as PMNode } from "prosemirror-model" ;
11- import { EditorState , type Transaction } from "prosemirror-state" ;
12+ import {
13+ EditorState ,
14+ TextSelection ,
15+ type Transaction ,
16+ } from "prosemirror-state" ;
1217import type { EditorView } from "prosemirror-view" ;
1318import {
1419 forwardRef ,
@@ -58,9 +63,17 @@ export interface JSONContent {
5863 text ?: string ;
5964}
6065
66+ export interface EditorCommands {
67+ focus : ( ) => void ;
68+ focusAtStart : ( ) => void ;
69+ focusAtPixelWidth : ( pixelWidth : number ) => void ;
70+ insertAtStartAndFocus : ( content : string ) => void ;
71+ }
72+
6173export interface NoteEditorRef {
6274 view : EditorView | null ;
6375 searchStorage : SearchAndReplaceStorage ;
76+ commands : EditorCommands ;
6477}
6578
6679interface EditorProps {
@@ -69,7 +82,7 @@ interface EditorProps {
6982 mentionConfig ?: MentionConfig ;
7083 placeholderComponent ?: PlaceholderFunction ;
7184 fileHandlerConfig ?: FileHandlerConfig ;
72- onNavigateToTitle ?: ( ) => void ;
85+ onNavigateToTitle ?: ( pixelWidth ?: number ) => void ;
7386}
7487
7588const nodeViews = {
@@ -93,6 +106,104 @@ function ViewCapture({
93106 return null ;
94107}
95108
109+ const noopCommands : EditorCommands = {
110+ focus : ( ) => { } ,
111+ focusAtStart : ( ) => { } ,
112+ focusAtPixelWidth : ( ) => { } ,
113+ insertAtStartAndFocus : ( ) => { } ,
114+ } ;
115+
116+ function firstTextBlockStart ( doc : PMNode ) : number {
117+ let node = doc . firstChild ;
118+ let pos = 0 ;
119+ while ( node && ! node . isTextblock ) {
120+ pos += 1 ;
121+ node = node . firstChild ;
122+ }
123+ return pos + 1 ;
124+ }
125+
126+ function EditorCommandsBridge ( {
127+ commandsRef,
128+ } : {
129+ commandsRef : React . RefObject < EditorCommands > ;
130+ } ) {
131+ commandsRef . current . focus = useEditorEventCallback ( ( view ) => {
132+ if ( ! view ) return ;
133+ view . focus ( ) ;
134+ } ) ;
135+
136+ commandsRef . current . focusAtStart = useEditorEventCallback ( ( view ) => {
137+ if ( ! view ) return ;
138+ const pos = firstTextBlockStart ( view . state . doc ) ;
139+ view . dispatch (
140+ view . state . tr . setSelection ( TextSelection . create ( view . state . doc , pos ) ) ,
141+ ) ;
142+ view . focus ( ) ;
143+ } ) ;
144+
145+ commandsRef . current . focusAtPixelWidth = useEditorEventCallback (
146+ ( view , pixelWidth : number ) => {
147+ if ( ! view ) return ;
148+
149+ const blockStart = firstTextBlockStart ( view . state . doc ) ;
150+ const firstTextNode = view . dom . querySelector ( ".ProseMirror > *" ) ;
151+ if ( firstTextNode ) {
152+ const editorStyle = window . getComputedStyle ( firstTextNode ) ;
153+ const canvas = document . createElement ( "canvas" ) ;
154+ const ctx = canvas . getContext ( "2d" ) ;
155+ if ( ctx ) {
156+ ctx . font = `${ editorStyle . fontWeight } ${ editorStyle . fontSize } ${ editorStyle . fontFamily } ` ;
157+ const firstBlock = view . state . doc . firstChild ;
158+ if ( firstBlock && firstBlock . textContent ) {
159+ const text = firstBlock . textContent ;
160+ let charPos = 0 ;
161+ for ( let i = 0 ; i <= text . length ; i ++ ) {
162+ const currentWidth = ctx . measureText ( text . slice ( 0 , i ) ) . width ;
163+ if ( currentWidth >= pixelWidth ) {
164+ charPos = i ;
165+ break ;
166+ }
167+ charPos = i ;
168+ }
169+ const targetPos = Math . min (
170+ blockStart + charPos ,
171+ view . state . doc . content . size - 1 ,
172+ ) ;
173+ view . dispatch (
174+ view . state . tr . setSelection (
175+ TextSelection . create ( view . state . doc , targetPos ) ,
176+ ) ,
177+ ) ;
178+ view . focus ( ) ;
179+ return ;
180+ }
181+ }
182+ }
183+
184+ view . dispatch (
185+ view . state . tr . setSelection (
186+ TextSelection . create ( view . state . doc , blockStart ) ,
187+ ) ,
188+ ) ;
189+ view . focus ( ) ;
190+ } ,
191+ ) ;
192+
193+ commandsRef . current . insertAtStartAndFocus = useEditorEventCallback (
194+ ( view , content : string ) => {
195+ if ( ! view || ! content ) return ;
196+ const pos = firstTextBlockStart ( view . state . doc ) ;
197+ const tr = view . state . tr . insertText ( content , pos ) ;
198+ tr . setSelection ( TextSelection . create ( tr . doc , pos ) ) ;
199+ view . dispatch ( tr ) ;
200+ view . focus ( ) ;
201+ } ,
202+ ) ;
203+
204+ return null ;
205+ }
206+
96207const NoteEditor = forwardRef < NoteEditorRef , EditorProps > ( ( props , ref ) => {
97208 const {
98209 handleChange,
@@ -106,10 +217,21 @@ const NoteEditor = forwardRef<NoteEditorRef, EditorProps>((props, ref) => {
106217 const previousContentRef = useRef < JSONContent | undefined > ( initialContent ) ;
107218 const searchStorage = useMemo ( ( ) => createSearchStorage ( ) , [ ] ) ;
108219 const viewRef = useRef < EditorView | null > ( null ) ;
220+ const commandsRef = useRef < EditorCommands > ( noopCommands ) ;
109221
110- useImperativeHandle ( ref , ( ) => ( { view : viewRef . current , searchStorage } ) , [
111- searchStorage ,
112- ] ) ;
222+ useImperativeHandle (
223+ ref ,
224+ ( ) => ( {
225+ get view ( ) {
226+ return viewRef . current ;
227+ } ,
228+ searchStorage,
229+ get commands ( ) {
230+ return commandsRef . current ;
231+ } ,
232+ } ) ,
233+ [ searchStorage ] ,
234+ ) ;
113235
114236 const onUpdate = useDebounceCallback ( ( view : EditorView ) => {
115237 if ( ! handleChange ) return ;
@@ -208,6 +330,7 @@ const NoteEditor = forwardRef<NoteEditorRef, EditorProps>((props, ref) => {
208330 >
209331 < ProseMirrorDoc />
210332 < ViewCapture viewRef = { viewRef } onViewReady = { onViewReady } />
333+ < EditorCommandsBridge commandsRef = { commandsRef } />
211334 { mentionConfig && < MentionSuggestion config = { mentionConfig } /> }
212335 </ ProseMirror >
213336 ) ;
0 commit comments