Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/features/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* BlockNote-based rich-text editor feature with SQLite auto-save.
*/

export type { ImageUploadResult } from './api/imageUpload'

Check failure on line 6 in src/features/editor/index.ts

View workflow job for this annotation

GitHub Actions / Frontend Lint & Format

assist/source/organizeImports

The imports and exports are not sorted.
export { resolveImageUrl, uploadImage } from './api/imageUpload'
export type { Note } from './api/notes'
export {
Expand Down Expand Up @@ -45,6 +45,7 @@
exportToMarkdown,
fixBlockNoteTableExport,
} from './lib/markdown-export'
export { checklistSplitFixExtension } from './lib/checklistSplitFix'
export { rangeCheckToggleExtension } from './lib/rangeCheckToggle'
export { searchExtension } from './lib/searchExtension'
export { slashMenuEmacsKeysExtension } from './lib/slashMenuEmacsKeys'
Expand Down
60 changes: 60 additions & 0 deletions src/features/editor/lib/checklistSplitFix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createExtension } from '@blocknote/core'

/**
* BlockNote extension that fixes the checked state when splitting a
* checkListItem block by pressing Enter at the text start.
*
* BlockNote's `splitBlockTr` passes `keepProps=undefined` (falsy) to
* `tr.split()`, which gives the new (lower) node `attrs: {}` and resets
* its `checked` to `false`. The original (upper) node retains its
* `checked: true`. After a line-start split the result is:
*
* Upper: checked=true (wrong — should be false)
* Lower: checked=false (wrong — should be true)
*
* This extension intercepts Enter on checked checkListItem blocks via
* `runsBefore: ['check-list-item-shortcuts']` and performs the split
* with corrected checked states using `editor.transact()`.
*/
export const checklistSplitFixExtension = createExtension(({ editor }) => ({
key: 'checklistSplitFix',
runsBefore: ['check-list-item-shortcuts'],
keyboardShortcuts: {
Enter: () => {
const pos = editor.getTextCursorPosition()
if (pos.block.type !== 'checkListItem') return false

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const block = editor.getBlock(pos.block.id) as any
if (!block) return false

// Check if cursor is at content start (line-head split) by inspecting
// the TipTap selection.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tiptap = (editor as any)._tiptapEditor
const { $from } = tiptap.state.selection
// $from.parent is the checkListItem node; parentOffset === 0 means
// the cursor is at the very start of its inline content.
if ($from.parentOffset !== 0) return false

// Block must have content (empty block → paragraph conversion is
// handled by BlockNote's own handler).
if (block.content?.length === 0) return false

// checked=true, non-empty, cursor at content start: perform split
// with corrected checked states.
editor.transact(() => {
const inserted = editor.insertBlocks(
[{ type: 'checkListItem', props: { checked: false } }],
pos.block.id,
'before'
)
if (inserted.length > 0) {
editor.setTextCursorPosition(inserted[0].id, 'start')
}
})

return true
},
},
}))
40 changes: 2 additions & 38 deletions src/features/editor/ui/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import { useToolbarConfig } from '@/app/providers/toolbar-config-provider'
import type { SaveStatus } from '..'
import {
checklistSplitFixExtension,
cursorCenteringExtension,
cursorVimKeysExtension,
resolveImageUrl,
Expand Down Expand Up @@ -275,6 +276,7 @@
extensions: [
cursorCenteringExtension,
searchExtension,
checklistSplitFixExtension(),
rangeCheckToggleExtension(),
slashMenuEmacsKeysExtension(),
cursorVimKeysExtension(),
Expand Down Expand Up @@ -521,52 +523,14 @@
scheduleSave(JSON.stringify(editor.document))
}, [editor, scheduleSave, backfillImageCaptions])

/**
* Handles clicks on the editor wrapper's padding area.
*
* The editor wrapper has generous bottom padding (`pb-[60vh]`) so that
* users can scroll content above the virtual keyboard on mobile devices.
* Clicking in this padding zone does not naturally focus the editor
* because the click target is outside the `.bn-editor` contenteditable
* region. This callback detects such clicks and programmatically focuses
* the editor with the cursor placed at the end of the last block.
*
* If the click originated inside `.bn-editor` (including any of its
* descendant elements), the callback returns early and lets the default
* browser behaviour handle focus normally.
*
* @param e - The mouse event from the wrapper `<div>`.
*
* @example
* ```tsx
* <div onClick={handleWrapperClick}>
* <BlockNoteView editor={editor} />
* </div>
* ```
*/
const handleWrapperClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement
if (target.closest('.bn-editor')) return

const lastBlock = editor.document[editor.document.length - 1]
if (lastBlock) {
editor.setTextCursorPosition(lastBlock, 'end')
}
editor.focus()
},
[editor]
)

return (
<>
{/* Editor wrapper — starts invisible (`opacity-0`) and transitions to
`opacity-100` once `contentReady` is true, preventing a flash of
stale/default content while the real note loads. */}
<div
className={`w-full min-h-screen px-8 pb-[60vh] ${contentReady ? 'opacity-100' : 'opacity-0'}`}

Check warning on line 532 in src/features/editor/ui/Editor.tsx

View workflow job for this annotation

GitHub Actions / Frontend Lint & Format

lint/nursery/useSortedClasses

These CSS classes should be sorted.
data-editor-root
onClick={handleWrapperClick}
style={
{
'--editor-font-size': `${fontSize}px`,
Expand Down
Loading