@@ -2,6 +2,7 @@ import { GapCursor } from "@tiptap/pm/gapcursor";
22import { NodeSelection , TextSelection } from "prosemirror-state" ;
33import { afterEach , describe , expect , it } from "vite-plus/test" ;
44
5+ import type { PartialBlock } from "../../blocks/defaultBlocks.js" ;
56import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js" ;
67
78const editors : BlockNoteEditor [ ] = [ ] ;
@@ -13,111 +14,177 @@ afterEach(() => {
1314 editors . length = 0 ;
1415} ) ;
1516
16- function createEditor ( gapCursor ?: boolean , trailingBlock = true ) {
17- const editor = BlockNoteEditor . create ( {
18- gapCursor,
19- trailingBlock,
20- initialContent : [
21- { id : "first" , type : "image" } ,
22- { id : "second" , type : "file" } ,
23- { id : "text" , type : "paragraph" , content : "hello" } ,
24- { id : "last" , type : "image" } ,
25- ] ,
26- } ) ;
17+ function createEditor ( initialContent : PartialBlock [ ] , trailingBlock = true ) {
18+ const editor = BlockNoteEditor . create ( { initialContent, trailingBlock } ) ;
2719 editor . mount ( document . createElement ( "div" ) ) ;
2820 editors . push ( editor ) ;
2921 return editor ;
3022}
3123
32- function pressKey ( editor : BlockNoteEditor , key : "ArrowUp" | "ArrowDown" ) {
33- const view = editor . prosemirrorView ;
24+ function pressKey (
25+ editor : BlockNoteEditor ,
26+ key : "ArrowUp" | "ArrowDown" | "ArrowRight" ,
27+ modifiers : KeyboardEventInit = { } ,
28+ ) {
3429 const event = new KeyboardEvent ( "keydown" , {
3530 key,
36- keyCode : key === " ArrowUp" ? 38 : 40 ,
31+ keyCode : { ArrowUp : 38 , ArrowDown : 40 , ArrowRight : 39 } [ key ] ,
3732 bubbles : true ,
3833 cancelable : true ,
34+ ...modifiers ,
3935 } ) ;
40- // Dispatch through the DOM so ProseMirror's built-in keyboard handling runs
41- // after plugin handlers, just as it does for a real key press.
42- view . dom . dispatchEvent ( event ) ;
36+ // Include ProseMirror's built-in handling as well as the plugin handlers.
37+ editor . prosemirrorView . dom . dispatchEvent ( event ) ;
4338 return event . defaultPrevented ;
4439}
4540
46- describe ( "gap cursor configuration" , ( ) => {
47- it . each ( [ undefined , true ] ) (
48- "preserves gap navigation when gapCursor is %s" ,
49- ( gapCursor ) => {
50- const editor = createEditor ( gapCursor ) ;
41+ describe ( "ArrowDown after a selected contentless block" , ( ) => {
42+ it . each ( [ "image" , "file" , "video" , "audio" ] as const ) (
43+ "creates and selects a paragraph after a final %s" ,
44+ ( type ) => {
45+ const editor = createEditor ( [ { id : "last" , type } ] ) ;
46+ editor . setTextCursorPosition ( "last" ) ;
47+ const original = editor . document [ 0 ] ;
48+
49+ expect ( pressKey ( editor , "ArrowDown" ) ) . toBe ( true ) ;
50+
51+ expect ( editor . document ) . toHaveLength ( 2 ) ;
52+ expect ( editor . document [ 0 ] ) . toEqual ( original ) ;
53+ expect ( editor . document [ 1 ] ) . toMatchObject ( {
54+ type : "paragraph" ,
55+ content : [ ] ,
56+ } ) ;
57+ expect ( editor . getTextCursorPosition ( ) . block . id ) . toBe (
58+ editor . document [ 1 ] . id ,
59+ ) ;
60+ expect ( editor . prosemirrorState . selection ) . toBeInstanceOf ( TextSelection ) ;
61+ expect ( editor . prosemirrorState . selection . $from . parentOffset ) . toBe ( 0 ) ;
62+ expect ( editor . prosemirrorState . selection . empty ) . toBe ( true ) ;
63+
64+ pressKey ( editor , "ArrowDown" ) ;
65+ expect ( editor . document ) . toHaveLength ( 2 ) ;
66+ } ,
67+ ) ;
68+
69+ it ( "works with the trailing block decoration disabled" , ( ) => {
70+ const editor = createEditor ( [ { id : "last" , type : "image" } ] , false ) ;
71+ editor . setTextCursorPosition ( "last" ) ;
72+ pressKey ( editor , "ArrowDown" ) ;
73+ expect ( editor . document ) . toHaveLength ( 2 ) ;
74+ expect ( editor . getTextCursorPosition ( ) . block . type ) . toBe ( "paragraph" ) ;
75+ } ) ;
76+
77+ it ( "inserts after a final nested block at the same nesting level" , ( ) => {
78+ const editor = createEditor ( [
79+ {
80+ id : "parent" ,
81+ type : "paragraph" ,
82+ content : "parent" ,
83+ children : [ { id : "last" , type : "image" } ] ,
84+ } ,
85+ ] ) ;
86+ editor . setTextCursorPosition ( "last" ) ;
87+ pressKey ( editor , "ArrowDown" ) ;
88+ expect ( editor . document ) . toHaveLength ( 1 ) ;
89+ const children = editor . document [ 0 ] . children ;
90+ expect ( children ) . toHaveLength ( 2 ) ;
91+ expect ( children [ 1 ] ) . toMatchObject ( { type : "paragraph" , content : [ ] } ) ;
92+ expect ( editor . getTextCursorPosition ( ) . block . id ) . toBe ( children [ 1 ] . id ) ;
93+ } ) ;
94+
95+ it . each ( [ "image" , "paragraph" ] as const ) (
96+ "does not insert when a %s follows" ,
97+ ( type ) => {
98+ const editor = createEditor ( [
99+ { id : "first" , type : "image" } ,
100+ { id : "next" , type } ,
101+ ] ) ;
51102 editor . setTextCursorPosition ( "first" ) ;
103+ const doc = editor . document ;
52104 pressKey ( editor , "ArrowDown" ) ;
53- expect ( editor . prosemirrorState . selection ) . toBeInstanceOf ( GapCursor ) ;
105+ expect ( editor . document ) . toEqual ( doc ) ;
54106 } ,
55107 ) ;
56108
57- it . each ( [
58- [ "first" , "ArrowDown" , "second" ] ,
59- [ "second" , "ArrowUp" , "first" ] ,
60- ] as const ) ( "moves directly from %s with %s" , ( from , key , to ) => {
61- const editor = createEditor ( false ) ;
62- const doc = editor . prosemirrorState . doc ;
63- editor . setTextCursorPosition ( from ) ;
64- expect ( pressKey ( editor , key ) ) . toBe ( true ) ;
65- expect ( editor . prosemirrorState . selection ) . toBeInstanceOf ( NodeSelection ) ;
66- expect ( editor . getTextCursorPosition ( ) . block . id ) . toBe ( to ) ;
67- expect ( editor . prosemirrorState . doc . eq ( doc ) ) . toBe ( true ) ;
68- expect (
69- editor . prosemirrorView . dom . querySelector ( ".ProseMirror-gapcursor" ) ,
70- ) . toBeNull ( ) ;
109+ it ( "does not insert when the selected block has children" , ( ) => {
110+ const editor = createEditor ( [
111+ {
112+ id : "parent" ,
113+ type : "image" ,
114+ children : [ { id : "child" , type : "image" } ] ,
115+ } ,
116+ ] ) ;
117+ editor . setTextCursorPosition ( "parent" ) ;
118+ const doc = editor . document ;
119+ pressKey ( editor , "ArrowDown" ) ;
120+ expect ( editor . document ) . toEqual ( doc ) ;
71121 } ) ;
72122
73- it . each ( [
74- [ "second" , "ArrowDown" , "start" ] ,
75- [ "last" , "ArrowUp" , "end" ] ,
76- ] as const ) ( "moves from %s into adjacent text" , ( from , key , placement ) => {
77- const editor = createEditor ( false ) ;
78- editor . setTextCursorPosition ( "text" , placement ) ;
79- const expected = editor . prosemirrorState . selection ;
80- editor . setTextCursorPosition ( from ) ;
81- pressKey ( editor , key ) ;
82- expect ( editor . prosemirrorState . selection ) . toBeInstanceOf ( TextSelection ) ;
83- expect ( editor . prosemirrorState . selection . eq ( expected ) ) . toBe ( true ) ;
123+ it . each ( [ "" , "hello" ] ) (
124+ "does not append after a text cursor in a paragraph containing %j" ,
125+ ( content ) => {
126+ const editor = createEditor ( [ { id : "last" , type : "paragraph" , content } ] ) ;
127+ editor . setTextCursorPosition ( "last" , "end" ) ;
128+ const doc = editor . document ;
129+ pressKey ( editor , "ArrowDown" ) ;
130+ expect ( editor . document ) . toEqual ( doc ) ;
131+ } ,
132+ ) ;
133+
134+ it ( "does not append after a node-selected block with inline content" , ( ) => {
135+ const editor = createEditor ( [
136+ { id : "last" , type : "paragraph" , content : "hello" } ,
137+ ] ) ;
138+ editor . setTextCursorPosition ( "last" ) ;
139+ editor . transact ( ( tr ) =>
140+ tr . setSelection (
141+ NodeSelection . create ( tr . doc , tr . selection . $from . before ( ) ) ,
142+ ) ,
143+ ) ;
144+ const doc = editor . document ;
145+ pressKey ( editor , "ArrowDown" ) ;
146+ expect ( editor . document ) . toEqual ( doc ) ;
84147 } ) ;
85148
86- it . each ( [ true , false ] ) (
87- "stays at document boundaries with trailingBlock=%s" ,
88- ( trailingBlock ) => {
89- const editor = createEditor ( false , trailingBlock ) ;
90- const doc = editor . prosemirrorState . doc ;
91- for ( const [ id , key ] of [
92- [ "first" , "ArrowUp" ] ,
93- [ "last" , "ArrowDown" ] ,
94- ] as const ) {
95- editor . setTextCursorPosition ( id ) ;
96- const selection = editor . prosemirrorState . selection ;
97- // Repeated presses must neither move the selection nor add a block.
98- expect ( pressKey ( editor , key ) ) . toBe ( true ) ;
99- expect ( pressKey ( editor , key ) ) . toBe ( true ) ;
100- expect ( editor . prosemirrorState . selection . eq ( selection ) ) . toBe ( true ) ;
101- expect ( editor . prosemirrorState . doc . eq ( doc ) ) . toBe ( true ) ;
149+ it . each ( [
150+ { shiftKey : true } ,
151+ { altKey : true } ,
152+ { ctrlKey : true } ,
153+ { metaKey : true } ,
154+ { isComposing : true } ,
155+ ] ) (
156+ "does not insert for modified or composing key presses: %j" ,
157+ ( modifiers ) => {
158+ const editor = createEditor ( [ { id : "last" , type : "image" } ] ) ;
159+ editor . setTextCursorPosition ( "last" ) ;
160+ const doc = editor . document ;
161+ pressKey ( editor , "ArrowDown" , modifiers ) ;
162+ expect ( editor . document ) . toEqual ( doc ) ;
163+ } ,
164+ ) ;
165+
166+ it . each ( [ "ArrowUp" , "ArrowRight" ] as const ) (
167+ "does not insert a paragraph for %s" ,
168+ ( key ) => {
169+ const editor = createEditor ( [ { id : "last" , type : "image" } ] ) ;
170+ editor . setTextCursorPosition ( "last" ) ;
171+ const doc = editor . document ;
172+ pressKey ( editor , key ) ;
173+ if ( key === "ArrowUp" ) {
174+ expect ( editor . prosemirrorState . selection ) . toBeInstanceOf ( GapCursor ) ;
102175 }
176+ expect ( editor . document ) . toEqual ( doc ) ;
103177 } ,
104178 ) ;
105179
106- it ( "navigates into and out of nested blocks" , ( ) => {
107- const editor = createEditor ( false ) ;
108- editor . updateBlock ( "first" , { children : [ { id : "child" , type : "image" } ] } ) ;
180+ it ( "preserves gap cursors between contentless blocks" , ( ) => {
181+ const editor = createEditor ( [
182+ { id : "first" , type : "image" } ,
183+ { id : "last" , type : "file" } ,
184+ ] ) ;
109185 editor . setTextCursorPosition ( "first" ) ;
110- const doc = editor . prosemirrorState . doc ;
111- for ( const [ key , id ] of [
112- [ "ArrowDown" , "child" ] ,
113- [ "ArrowDown" , "second" ] ,
114- [ "ArrowUp" , "child" ] ,
115- [ "ArrowUp" , "first" ] ,
116- ] as const ) {
117- expect ( pressKey ( editor , key ) ) . toBe ( true ) ;
118- expect ( editor . getTextCursorPosition ( ) . block . id ) . toBe ( id ) ;
119- expect ( editor . prosemirrorState . selection ) . toBeInstanceOf ( NodeSelection ) ;
120- }
121- expect ( editor . prosemirrorState . doc . eq ( doc ) ) . toBe ( true ) ;
186+ pressKey ( editor , "ArrowDown" ) ;
187+ expect ( editor . prosemirrorState . selection ) . toBeInstanceOf ( GapCursor ) ;
188+ expect ( editor . document ) . toHaveLength ( 2 ) ;
122189 } ) ;
123190} ) ;
0 commit comments