@@ -3,12 +3,18 @@ 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+ Selection ,
15+ TextSelection ,
16+ type Transaction ,
17+ } from "prosemirror-state" ;
1218import type { EditorView } from "prosemirror-view" ;
1319import {
1420 forwardRef ,
@@ -58,9 +64,17 @@ export interface JSONContent {
5864 text ?: string ;
5965}
6066
67+ export interface EditorCommands {
68+ focus : ( ) => void ;
69+ focusAtStart : ( ) => void ;
70+ focusAtPixelWidth : ( pixelWidth : number ) => void ;
71+ insertAtStartAndFocus : ( content : string ) => void ;
72+ }
73+
6174export interface NoteEditorRef {
6275 view : EditorView | null ;
6376 searchStorage : SearchAndReplaceStorage ;
77+ commands : EditorCommands ;
6478}
6579
6680interface EditorProps {
@@ -69,7 +83,7 @@ interface EditorProps {
6983 mentionConfig ?: MentionConfig ;
7084 placeholderComponent ?: PlaceholderFunction ;
7185 fileHandlerConfig ?: FileHandlerConfig ;
72- onNavigateToTitle ?: ( ) => void ;
86+ onNavigateToTitle ?: ( pixelWidth ?: number ) => void ;
7387}
7488
7589const nodeViews = {
@@ -93,6 +107,91 @@ function ViewCapture({
93107 return null ;
94108}
95109
110+ const noopCommands : EditorCommands = {
111+ focus : ( ) => { } ,
112+ focusAtStart : ( ) => { } ,
113+ focusAtPixelWidth : ( ) => { } ,
114+ insertAtStartAndFocus : ( ) => { } ,
115+ } ;
116+
117+ function EditorCommandsBridge ( {
118+ commandsRef,
119+ } : {
120+ commandsRef : React . RefObject < EditorCommands > ;
121+ } ) {
122+ commandsRef . current . focus = useEditorEventCallback ( ( view ) => {
123+ if ( ! view ) return ;
124+ view . focus ( ) ;
125+ } ) ;
126+
127+ commandsRef . current . focusAtStart = useEditorEventCallback ( ( view ) => {
128+ if ( ! view ) return ;
129+ view . dispatch (
130+ view . state . tr . setSelection ( Selection . atStart ( view . state . doc ) ) ,
131+ ) ;
132+ view . focus ( ) ;
133+ } ) ;
134+
135+ commandsRef . current . focusAtPixelWidth = useEditorEventCallback (
136+ ( view , pixelWidth : number ) => {
137+ if ( ! view ) return ;
138+
139+ const blockStart = Selection . atStart ( view . state . doc ) . from ;
140+ const firstTextNode = view . dom . querySelector ( ".ProseMirror > *" ) ;
141+ if ( firstTextNode ) {
142+ const editorStyle = window . getComputedStyle ( firstTextNode ) ;
143+ const canvas = document . createElement ( "canvas" ) ;
144+ const ctx = canvas . getContext ( "2d" ) ;
145+ if ( ctx ) {
146+ ctx . font = `${ editorStyle . fontWeight } ${ editorStyle . fontSize } ${ editorStyle . fontFamily } ` ;
147+ const firstBlock = view . state . doc . firstChild ;
148+ if ( firstBlock && firstBlock . textContent ) {
149+ const text = firstBlock . textContent ;
150+ let charPos = 0 ;
151+ for ( let i = 0 ; i <= text . length ; i ++ ) {
152+ const currentWidth = ctx . measureText ( text . slice ( 0 , i ) ) . width ;
153+ if ( currentWidth >= pixelWidth ) {
154+ charPos = i ;
155+ break ;
156+ }
157+ charPos = i ;
158+ }
159+ const targetPos = Math . min (
160+ blockStart + charPos ,
161+ view . state . doc . content . size - 1 ,
162+ ) ;
163+ view . dispatch (
164+ view . state . tr . setSelection (
165+ TextSelection . create ( view . state . doc , targetPos ) ,
166+ ) ,
167+ ) ;
168+ view . focus ( ) ;
169+ return ;
170+ }
171+ }
172+ }
173+
174+ view . dispatch (
175+ view . state . tr . setSelection ( Selection . atStart ( view . state . doc ) ) ,
176+ ) ;
177+ view . focus ( ) ;
178+ } ,
179+ ) ;
180+
181+ commandsRef . current . insertAtStartAndFocus = useEditorEventCallback (
182+ ( view , content : string ) => {
183+ if ( ! view || ! content ) return ;
184+ const pos = Selection . atStart ( view . state . doc ) . from ;
185+ const tr = view . state . tr . insertText ( content , pos ) ;
186+ tr . setSelection ( TextSelection . create ( tr . doc , pos ) ) ;
187+ view . dispatch ( tr ) ;
188+ view . focus ( ) ;
189+ } ,
190+ ) ;
191+
192+ return null ;
193+ }
194+
96195const NoteEditor = forwardRef < NoteEditorRef , EditorProps > ( ( props , ref ) => {
97196 const {
98197 handleChange,
@@ -106,10 +205,21 @@ const NoteEditor = forwardRef<NoteEditorRef, EditorProps>((props, ref) => {
106205 const previousContentRef = useRef < JSONContent | undefined > ( initialContent ) ;
107206 const searchStorage = useMemo ( ( ) => createSearchStorage ( ) , [ ] ) ;
108207 const viewRef = useRef < EditorView | null > ( null ) ;
208+ const commandsRef = useRef < EditorCommands > ( noopCommands ) ;
109209
110- useImperativeHandle ( ref , ( ) => ( { view : viewRef . current , searchStorage } ) , [
111- searchStorage ,
112- ] ) ;
210+ useImperativeHandle (
211+ ref ,
212+ ( ) => ( {
213+ get view ( ) {
214+ return viewRef . current ;
215+ } ,
216+ searchStorage,
217+ get commands ( ) {
218+ return commandsRef . current ;
219+ } ,
220+ } ) ,
221+ [ searchStorage ] ,
222+ ) ;
113223
114224 const onUpdate = useDebounceCallback ( ( view : EditorView ) => {
115225 if ( ! handleChange ) return ;
@@ -208,6 +318,7 @@ const NoteEditor = forwardRef<NoteEditorRef, EditorProps>((props, ref) => {
208318 >
209319 < ProseMirrorDoc />
210320 < ViewCapture viewRef = { viewRef } onViewReady = { onViewReady } />
321+ < EditorCommandsBridge commandsRef = { commandsRef } />
211322 { mentionConfig && < MentionSuggestion config = { mentionConfig } /> }
212323 </ ProseMirror >
213324 ) ;
0 commit comments