11import { BlockNoteEditor , createExtension , createStore } from "@blocknote/core" ;
2- import { Selection , TextSelection } from "prosemirror-state" ;
2+ import { Selection } from "prosemirror-state" ;
33
44/**
55 * Inline-content counterpart of {@link SourceBlockWithPreviewExtension}. Drives
6- * the source popup for inline content with a preview: toggling it with the
7- * keyboard, navigating out of the (hidden) source, and preventing edits to it
8- * while it's hidden.
6+ * the source popup for inline content with a preview.
97 *
10- * Unlike the block version, its store holds a single `open` flag rather than a
11- * block ID - at most one inline content can hold the cursor, and the render
12- * component decides whether the open popup is its own by checking if the cursor
13- * is inside its source.
8+ * Unlike the block version, the popup isn't toggled with a separate state flag:
9+ * it's open exactly when the selection is inside the inline content's source.
10+ * The store therefore only tracks which inline content (by its position) holds
11+ * the selection - moving the selection in opens its popup, moving it out closes
12+ * it. Since the source popup is always laid out (just hidden via opacity), the
13+ * cursor can navigate into and out of it with the arrow keys as usual.
1414 */
1515export const SourceInlineContentWithPreviewExtension = createExtension (
1616 ( {
@@ -25,149 +25,57 @@ export const SourceInlineContentWithPreviewExtension = createExtension(
2525 } ;
2626 } ) => {
2727 const store = createStore < {
28- popupOpen : number | undefined ;
2928 selected : number | undefined ;
3029 } > ( {
31- popupOpen : undefined ,
3230 selected : undefined ,
3331 } ) ;
3432
35- const handleArrow =
36- ( direction : "before" | "after" ) =>
37- ( { editor } : { editor : BlockNoteEditor < any , any , any > } ) => {
38- if ( store . state . popupOpen ) {
39- return false ;
40- }
41-
42- const { $from } = editor . prosemirrorState . selection ;
43- const node = $from . node ( ) ;
44- if ( node . type . name !== inlineContentType ) {
45- return false ;
46- }
47-
48- const view = editor . prosemirrorView ! ;
49- const target = direction === "before" ? $from . before ( ) : $from . after ( ) ;
50- const selection = Selection . near (
51- view . state . doc . resolve ( target ) ,
52- direction === "before" ? - 1 : 1 ,
53- ) ;
54-
55- view . dispatch ( view . state . tr . setSelection ( selection ) ) ;
56-
57- return true ;
58- } ;
33+ // Moves the selection out of (just after) the inline content, which closes
34+ // the popup via the selection-change handler below. Lets the keyboard
35+ // commit-and-exit the source the same way arrowing past its end does, and
36+ // keeps Enter from splitting the block while editing the source.
37+ const moveSelectionOut = ( {
38+ editor ,
39+ } : {
40+ editor : BlockNoteEditor < any , any , any > ;
41+ } ) => {
42+ const { $from } = editor . prosemirrorState . selection ;
43+ const node = $from . node ( ) ;
44+ if ( node . type . name !== inlineContentType ) {
45+ return false ;
46+ }
47+
48+ const view = editor . prosemirrorView ! ;
49+ const selection = Selection . near (
50+ view . state . doc . resolve ( $from . after ( ) ) ,
51+ 1 ,
52+ ) ;
53+ view . dispatch ( view . state . tr . setSelection ( selection ) ) ;
54+
55+ return true ;
56+ } ;
5957
6058 return {
6159 key,
6260 store,
6361 runsBefore,
6462 keyboardShortcuts : {
65- // Toggles the popup.
66- Enter : ( { editor } ) => {
67- const { $from } = editor . prosemirrorState . selection ;
68- const node = $from . node ( ) ;
69- if ( node . type . name !== inlineContentType ) {
70- return false ;
71- }
72-
73- const view = editor . prosemirrorView ! ;
74- view . dispatch (
75- view . state . tr . setSelection (
76- TextSelection . create ( view . state . tr . doc , $from . end ( ) ) ,
77- ) ,
78- ) ;
79-
80- store . setState ( ( state ) => ( {
81- ...state ,
82- popupOpen : state . popupOpen ? undefined : $from . before ( ) ,
83- } ) ) ;
84-
85- return true ;
86- } ,
87- // Closes the popup.
88- Escape : ( { editor } ) => {
89- const { $from } = editor . prosemirrorState . selection ;
90- const node = $from . node ( ) ;
91- if ( node . type . name !== inlineContentType ) {
92- return false ;
93- }
94-
95- const view = editor . prosemirrorView ! ;
96- view . dispatch (
97- view . state . tr . setSelection (
98- TextSelection . create ( view . state . tr . doc , $from . end ( ) ) ,
99- ) ,
100- ) ;
101-
102- store . setState ( ( state ) => ( { ...state , popupOpen : undefined } ) ) ;
103-
104- return true ;
105- } ,
106- // While the popup is closed, moves the selection straight out of the inline
107- // content rather than navigating its hidden source.
108- ArrowUp : handleArrow ( "before" ) ,
109- ArrowLeft : handleArrow ( "before" ) ,
110- ArrowDown : handleArrow ( "after" ) ,
111- ArrowRight : handleArrow ( "after" ) ,
63+ Enter : moveSelectionOut ,
64+ Escape : moveSelectionOut ,
11265 } ,
113- mount : ( { dom, signal } ) => {
66+ mount : ( { signal } ) => {
67+ // The popup is open exactly when the selection is inside the inline
68+ // content, so we just track which inline content (if any) holds it.
11469 const unsubscribeSelectionChange = editor . onSelectionChange ( ( ) => {
11570 const { $from } = editor . prosemirrorState . selection ;
11671 const node = $from . node ( ) ;
117- const before = $from . before ( ) ;
11872
119- store . setState ( ( state ) => ( {
120- selected : node . type . name === inlineContentType ? before : undefined ,
121- popupOpen :
122- state . popupOpen && state . popupOpen !== before
123- ? undefined
124- : state . popupOpen ,
125- } ) ) ;
73+ store . setState ( {
74+ selected :
75+ node . type . name === inlineContentType ? $from . before ( ) : undefined ,
76+ } ) ;
12677 } ) ;
12778 signal . addEventListener ( "abort" , unsubscribeSelectionChange ) ;
128-
129- const handleKeyDown = ( event : KeyboardEvent ) => {
130- const view = editor . prosemirrorView ;
131- if ( ! editor . isEditable || ! view ) {
132- return ;
133- }
134-
135- const isTypedChar =
136- event . key . length === 1 && ! event . ctrlKey && ! event . metaKey ;
137-
138- // While the popup is open, the source is shown and editable, so edits
139- // are let through. Boundary edits on the (possibly empty) source - an
140- // empty inline node can't hold a text cursor, so typing/deleting at
141- // the boundary needs special handling - are handled generically by the
142- // editor's `InlineContentBoundaryEdit` extension.
143- if ( store . state . popupOpen !== undefined ) {
144- return ;
145- }
146-
147- // While the popup is closed, prevents editing of the (hidden) source.
148- // Handled here rather than in `keyboardShortcuts` as it needs to match
149- // any text-input key, which a keymap can't express.
150- if (
151- editor . prosemirrorState . selection . $from . node ( ) . type . name !==
152- inlineContentType
153- ) {
154- return ;
155- }
156-
157- if (
158- isTypedChar ||
159- event . key === "Backspace" ||
160- event . key === "Delete" ||
161- event . key === "Tab"
162- ) {
163- event . preventDefault ( ) ;
164- event . stopImmediatePropagation ( ) ;
165- }
166- } ;
167- dom . addEventListener ( "keydown" , handleKeyDown , {
168- capture : true ,
169- signal,
170- } ) ;
17179 } ,
17280 } ;
17381 } ,
0 commit comments