mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-19 18:35:18 +02:00
fix: cmd+z behaviour on notes
This commit is contained in:
parent
a3e74931bf
commit
72534052e0
2 changed files with 87 additions and 2 deletions
|
|
@ -452,6 +452,7 @@ function ContentHeader({
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
type ShortcutPane = 'left' | 'right'
|
type ShortcutPane = 'left' | 'right'
|
||||||
|
type MarkdownHistoryHandlers = { undo: () => boolean; redo: () => boolean }
|
||||||
|
|
||||||
// File browser state (for Knowledge section)
|
// File browser state (for Knowledge section)
|
||||||
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
||||||
|
|
@ -596,6 +597,8 @@ function App() {
|
||||||
// File tab state
|
// File tab state
|
||||||
const [fileTabs, setFileTabs] = useState<FileTab[]>([])
|
const [fileTabs, setFileTabs] = useState<FileTab[]>([])
|
||||||
const [activeFileTabId, setActiveFileTabId] = useState<string | null>(null)
|
const [activeFileTabId, setActiveFileTabId] = useState<string | null>(null)
|
||||||
|
const [editorSessionByTabId, setEditorSessionByTabId] = useState<Record<string, number>>({})
|
||||||
|
const fileHistoryHandlersRef = useRef<Map<string, MarkdownHistoryHandlers>>(new Map())
|
||||||
const fileTabIdCounterRef = useRef(0)
|
const fileTabIdCounterRef = useRef(0)
|
||||||
const newFileTabId = () => `file-tab-${++fileTabIdCounterRef.current}`
|
const newFileTabId = () => `file-tab-${++fileTabIdCounterRef.current}`
|
||||||
|
|
||||||
|
|
@ -2036,6 +2039,13 @@ function App() {
|
||||||
}
|
}
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
setEditorSessionByTabId((prev) => {
|
||||||
|
if (!(tabId in prev)) return prev
|
||||||
|
const next = { ...prev }
|
||||||
|
delete next[tabId]
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
fileHistoryHandlersRef.current.delete(tabId)
|
||||||
}, [activeFileTabId, fileTabs, removeEditorCacheForPath])
|
}, [activeFileTabId, fileTabs, removeEditorCacheForPath])
|
||||||
|
|
||||||
const handleNewChatTab = useCallback(() => {
|
const handleNewChatTab = useCallback(() => {
|
||||||
|
|
@ -2136,6 +2146,11 @@ function App() {
|
||||||
setFileTabs((prev) => prev.map((tab) => (
|
setFileTabs((prev) => prev.map((tab) => (
|
||||||
tab.id === activeFileTabId ? { ...tab, path } : tab
|
tab.id === activeFileTabId ? { ...tab, path } : tab
|
||||||
)))
|
)))
|
||||||
|
// Rebinds this tab to a different note path: reset editor session to clear undo history.
|
||||||
|
setEditorSessionByTabId((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[activeFileTabId]: (prev[activeFileTabId] ?? 0) + 1,
|
||||||
|
}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2352,6 +2367,46 @@ function App() {
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Route undo/redo to the active markdown tab only (prevents cross-tab browser undo behavior).
|
||||||
|
useEffect(() => {
|
||||||
|
const handleHistoryKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const mod = e.metaKey || e.ctrlKey
|
||||||
|
if (!mod || e.altKey) return
|
||||||
|
|
||||||
|
const key = e.key.toLowerCase()
|
||||||
|
const wantsUndo = key === 'z' && !e.shiftKey
|
||||||
|
const wantsRedo = (key === 'z' && e.shiftKey) || (!isMac && key === 'y')
|
||||||
|
if (!wantsUndo && !wantsRedo) return
|
||||||
|
|
||||||
|
if (!selectedPath || !selectedPath.endsWith('.md') || !activeFileTabId) return
|
||||||
|
|
||||||
|
const target = e.target as EventTarget | null
|
||||||
|
if (target instanceof HTMLElement) {
|
||||||
|
const inTipTapEditor = Boolean(target.closest('.tiptap-editor'))
|
||||||
|
const inOtherTextInput = (
|
||||||
|
target instanceof HTMLInputElement
|
||||||
|
|| target instanceof HTMLTextAreaElement
|
||||||
|
|| target.isContentEditable
|
||||||
|
) && !inTipTapEditor
|
||||||
|
if (inOtherTextInput) return
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = fileHistoryHandlersRef.current.get(activeFileTabId)
|
||||||
|
if (!handlers) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
if (wantsUndo) {
|
||||||
|
handlers.undo()
|
||||||
|
} else {
|
||||||
|
handlers.redo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleHistoryKeyDown, true)
|
||||||
|
return () => document.removeEventListener('keydown', handleHistoryKeyDown, true)
|
||||||
|
}, [activeFileTabId, isMac, selectedPath])
|
||||||
|
|
||||||
// Keyboard shortcuts for tab management
|
// Keyboard shortcuts for tab management
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleTabKeyDown = (e: KeyboardEvent) => {
|
const handleTabKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|
@ -3136,6 +3191,14 @@ function App() {
|
||||||
placeholder="Start writing..."
|
placeholder="Start writing..."
|
||||||
wikiLinks={wikiLinkConfig}
|
wikiLinks={wikiLinkConfig}
|
||||||
onImageUpload={handleImageUpload}
|
onImageUpload={handleImageUpload}
|
||||||
|
editorSessionKey={editorSessionByTabId[tab.id] ?? 0}
|
||||||
|
onHistoryHandlersChange={(handlers) => {
|
||||||
|
if (handlers) {
|
||||||
|
fileHistoryHandlersRef.current.set(tab.id, handlers)
|
||||||
|
} else {
|
||||||
|
fileHistoryHandlersRef.current.delete(tab.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,8 @@ interface MarkdownEditorProps {
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
wikiLinks?: WikiLinkConfig
|
wikiLinks?: WikiLinkConfig
|
||||||
onImageUpload?: (file: File) => Promise<string | null>
|
onImageUpload?: (file: File) => Promise<string | null>
|
||||||
|
editorSessionKey?: number
|
||||||
|
onHistoryHandlersChange?: (handlers: { undo: () => boolean; redo: () => boolean } | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type WikiLinkMatch = {
|
type WikiLinkMatch = {
|
||||||
|
|
@ -278,6 +280,8 @@ export function MarkdownEditor({
|
||||||
placeholder = 'Start writing...',
|
placeholder = 'Start writing...',
|
||||||
wikiLinks,
|
wikiLinks,
|
||||||
onImageUpload,
|
onImageUpload,
|
||||||
|
editorSessionKey = 0,
|
||||||
|
onHistoryHandlersChange,
|
||||||
}: MarkdownEditorProps) {
|
}: MarkdownEditorProps) {
|
||||||
const isInternalUpdate = useRef(false)
|
const isInternalUpdate = useRef(false)
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
@ -400,7 +404,7 @@ export function MarkdownEditor({
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}, [editorSessionKey])
|
||||||
|
|
||||||
const orderedFiles = useMemo(() => {
|
const orderedFiles = useMemo(() => {
|
||||||
if (!wikiLinks) return []
|
if (!wikiLinks) return []
|
||||||
|
|
@ -489,12 +493,30 @@ export function MarkdownEditor({
|
||||||
isInternalUpdate.current = true
|
isInternalUpdate.current = true
|
||||||
// Pre-process to preserve blank lines
|
// Pre-process to preserve blank lines
|
||||||
const preprocessed = preprocessMarkdown(content)
|
const preprocessed = preprocessMarkdown(content)
|
||||||
editor.commands.setContent(preprocessed)
|
// Treat tab-open content as baseline: do not add hydration to undo history.
|
||||||
|
editor.chain().setMeta('addToHistory', false).setContent(preprocessed).run()
|
||||||
isInternalUpdate.current = false
|
isInternalUpdate.current = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [editor, content])
|
}, [editor, content])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onHistoryHandlersChange) return
|
||||||
|
if (!editor) {
|
||||||
|
onHistoryHandlersChange(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onHistoryHandlersChange({
|
||||||
|
undo: () => editor.chain().focus().undo().run(),
|
||||||
|
redo: () => editor.chain().focus().redo().run(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
onHistoryHandlersChange(null)
|
||||||
|
}
|
||||||
|
}, [editor, onHistoryHandlersChange])
|
||||||
|
|
||||||
// Force re-render decorations when selection highlight changes
|
// Force re-render decorations when selection highlight changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue