Skip to content

Commit c917bf3

Browse files
committed
wire up title input and session input movement
1 parent 13fefe0 commit c917bf3

File tree

9 files changed

+392
-284
lines changed

9 files changed

+392
-284
lines changed

apps/desktop/src/editor/index.tsx

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ import {
33
ProseMirrorDoc,
44
reactKeys,
55
useEditorEffect,
6+
useEditorEventCallback,
67
} from "@handlewithcare/react-prosemirror";
78
import { dropCursor } from "prosemirror-dropcursor";
89
import { gapCursor } from "prosemirror-gapcursor";
910
import { history } from "prosemirror-history";
1011
import { 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";
1217
import type { EditorView } from "prosemirror-view";
1318
import {
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+
6173
export interface NoteEditorRef {
6274
view: EditorView | null;
6375
searchStorage: SearchAndReplaceStorage;
76+
commands: EditorCommands;
6477
}
6578

6679
interface 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

7588
const 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+
96207
const 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
);

apps/desktop/src/editor/keymap.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ const mac =
118118
? /Mac|iP(hone|[oa]d)/.test(navigator.platform)
119119
: false;
120120

121-
export function buildKeymap(onNavigateToTitle?: () => void) {
121+
export function buildKeymap(onNavigateToTitle?: (pixelWidth?: number) => void) {
122122
const hardBreak = schema.nodes.hardBreak;
123123

124124
const keys: Record<string, Command> = {};
@@ -209,7 +209,26 @@ export function buildKeymap(onNavigateToTitle?: () => void) {
209209
};
210210

211211
if (onNavigateToTitle) {
212-
keys["ArrowUp"] = (state) => {
212+
keys["ArrowLeft"] = (state) => {
213+
const { $head, empty } = state.selection;
214+
if (!empty) return false;
215+
216+
let node = state.doc.firstChild;
217+
let firstTextBlockPos = 0;
218+
while (node && !node.isTextblock) {
219+
firstTextBlockPos += 1;
220+
node = node.firstChild;
221+
}
222+
if (!node) return false;
223+
224+
const blockStart = firstTextBlockPos + 1;
225+
if ($head.pos !== blockStart) return false;
226+
227+
onNavigateToTitle();
228+
return true;
229+
};
230+
231+
keys["ArrowUp"] = (state, _dispatch, view) => {
213232
const { $head } = state.selection;
214233

215234
let node = state.doc.firstChild;
@@ -224,6 +243,27 @@ export function buildKeymap(onNavigateToTitle?: () => void) {
224243

225244
if (!isInFirstBlock) return false;
226245

246+
if (view) {
247+
const firstBlock = state.doc.firstChild;
248+
if (firstBlock && firstBlock.textContent) {
249+
const text = firstBlock.textContent;
250+
const posInBlock = $head.pos - $head.start();
251+
const textBeforeCursor = text.slice(0, posInBlock);
252+
const firstTextNode = view.dom.querySelector(".ProseMirror > *");
253+
if (firstTextNode) {
254+
const style = window.getComputedStyle(firstTextNode);
255+
const canvas = document.createElement("canvas");
256+
const ctx = canvas.getContext("2d");
257+
if (ctx) {
258+
ctx.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
259+
const pixelWidth = ctx.measureText(textBeforeCursor).width;
260+
onNavigateToTitle(pixelWidth);
261+
return true;
262+
}
263+
}
264+
}
265+
}
266+
227267
onNavigateToTitle();
228268
return true;
229269
};

apps/desktop/src/session/components/note-input/enhanced/editor.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import * as main from "~/store/tinybase/store/main";
99

1010
export const EnhancedEditor = forwardRef<
1111
NoteEditorRef,
12-
{ sessionId: string; enhancedNoteId: string; onNavigateToTitle?: () => void }
12+
{
13+
sessionId: string;
14+
enhancedNoteId: string;
15+
onNavigateToTitle?: (pixelWidth?: number) => void;
16+
}
1317
>(({ sessionId, enhancedNoteId, onNavigateToTitle }, ref) => {
1418
const onImageUpload = useImageUpload(sessionId);
1519
const content = main.UI.useCell(

apps/desktop/src/session/components/note-input/enhanced/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import { createTaskId } from "~/store/zustand/ai-task/task-configs";
1313

1414
export const Enhanced = forwardRef<
1515
NoteEditorRef,
16-
{ sessionId: string; enhancedNoteId: string; onNavigateToTitle?: () => void }
16+
{
17+
sessionId: string;
18+
enhancedNoteId: string;
19+
onNavigateToTitle?: (pixelWidth?: number) => void;
20+
}
1721
>(({ sessionId, enhancedNoteId, onNavigateToTitle }, ref) => {
1822
const taskId = createTaskId(enhancedNoteId, "enhance");
1923
const llmStatus = useLLMConnectionStatus();

0 commit comments

Comments
 (0)