@@ -186,10 +186,11 @@ const caretToEnd = (el) => {
186186 sel . addRange ( range )
187187}
188188
189- // Move the caret to the cell directly above/below the currently focused
190- // markdown-table cell (same column). No-op if focus isn't in a cell, or
191- // at the table edge. Invoked from Logseq command-shortcut callbacks so
192- // the keybinding is editable in the host's keymap.
189+ // Move the caret to a neighbouring cell of the currently focused markdown-
190+ // table cell. `direction` ∈ 'up' | 'down' | 'left' | 'right'. No-op if
191+ // focus isn't in a cell, or at the table edge. Invoked from Logseq
192+ // command-shortcut callbacks so the keybinding is editable in the host's
193+ // keymap.
193194export const moveCaretInFocusedTableCell = ( direction ) => {
194195 let hostDoc = null
195196 try { hostDoc = ( window . top || window . parent ) ?. document } catch ( _ ) { /* cross-origin */ }
@@ -200,9 +201,15 @@ export const moveCaretInFocusedTableCell = (direction) => {
200201 const table = cell . closest ( 'table.lsp-mdt' )
201202 const tr = cell . closest ( 'tr' )
202203 const rows = Array . from ( table . querySelectorAll ( 'tr' ) )
203- const colIdx = Array . from ( tr . querySelectorAll ( 'th,td' ) ) . indexOf ( cell )
204- const target = rows [ rows . indexOf ( tr ) + ( direction === 'up' ? - 1 : 1 ) ]
205- const next = target && target . querySelectorAll ( 'th,td' ) [ colIdx ]
204+ const cells = Array . from ( tr . querySelectorAll ( 'th,td' ) )
205+ const colIdx = cells . indexOf ( cell )
206+ let next = null
207+ if ( direction === 'up' || direction === 'down' ) {
208+ const target = rows [ rows . indexOf ( tr ) + ( direction === 'up' ? - 1 : 1 ) ]
209+ next = target && target . querySelectorAll ( 'th,td' ) [ colIdx ]
210+ } else if ( direction === 'left' || direction === 'right' ) {
211+ next = cells [ colIdx + ( direction === 'left' ? - 1 : 1 ) ]
212+ }
206213 if ( next ) caretToEnd ( next )
207214}
208215
@@ -223,7 +230,53 @@ export const insertInFocusedTableCell = (which) => {
223230 doInsertOp ( root , opts , cell , which )
224231}
225232
226- // Shared by the local Alt+Shift+Arrow handler and the registered insert
233+ // Move the focused cell's row/column. `which` ∈ 'rowUp' | 'rowDown' |
234+ // 'colLeft' | 'colRight'. No-op if focus isn't in a cell or the op is
235+ // invalid at this edge (e.g. moving the first body row up would swap it
236+ // with the header).
237+ export const moveInFocusedTableCell = ( which ) => {
238+ let hostDoc = null
239+ try { hostDoc = ( window . top || window . parent ) ?. document } catch ( _ ) { /* cross-origin */ }
240+ if ( ! hostDoc ) return
241+ const active = hostDoc . activeElement
242+ const cell = active && active . closest && active . closest ( 'table.lsp-mdt th, table.lsp-mdt td' )
243+ if ( ! cell ) return
244+ const root = cell . closest ( '.lsp-mdtable-renderer' )
245+ const opts = root && root . _lspInlineOpts
246+ if ( ! opts ) return
247+ doMoveOp ( root , opts , cell , which )
248+ }
249+
250+ // Shared by the local Alt+Shift+Arrow handler and the registered move
251+ // commands. Stashes a `pendingToolbar` entry at the moved cell's new
252+ // position so `resumePinnedToolbar` follows the cell after re-render.
253+ const doMoveOp = ( root , opts , cell , which ) => {
254+ const tableEl = cell . closest ( 'table.lsp-mdt' )
255+ const tr = cell . closest ( 'tr' )
256+ const rows = Array . from ( tableEl . querySelectorAll ( 'tr' ) )
257+ const rowIdx = rows . indexOf ( tr )
258+ const cells = Array . from ( tr . querySelectorAll ( 'th,td' ) )
259+ const colIdx = cells . indexOf ( cell )
260+ const ord = Array . from ( root . querySelectorAll ( 'table.lsp-mdt' ) ) . indexOf ( tableEl )
261+ let op , newRow = rowIdx , newCol = colIdx
262+ if ( which === 'rowUp' ) {
263+ if ( rowIdx < 2 ) return // can't swap a body row over the header
264+ op = tableOps . moveRowUp ; newRow = rowIdx - 1
265+ } else if ( which === 'rowDown' ) {
266+ if ( rowIdx < 1 || rowIdx >= rows . length - 1 ) return
267+ op = tableOps . moveRowDown ; newRow = rowIdx + 1
268+ } else if ( which === 'colLeft' ) {
269+ if ( colIdx < 1 ) return
270+ op = tableOps . moveColLeft ; newCol = colIdx - 1
271+ } else if ( which === 'colRight' ) {
272+ if ( colIdx >= cells . length - 1 ) return
273+ op = tableOps . moveColRight ; newCol = colIdx + 1
274+ } else return
275+ pendingToolbar . set ( opts . blockId , { ord, rowIdx : newRow , colIdx : newCol } )
276+ commitStructural ( root , opts , ( m , i ) => ( i === ord ? op ( m , rowIdx , colIdx ) : m ) )
277+ }
278+
279+ // Shared by the local Alt+Ctrl+Shift+Arrow handler and the registered insert
227280// commands. Stashes a `pendingToolbar` entry at the new cell's position
228281// so `resumePinnedToolbar` refocuses it after the re-render (and re-pins
229282// the toolbar if the user has it pinned).
@@ -398,24 +451,24 @@ const buildItems = (root, opts, cell) => {
398451 const L = opts . menuLabels || { }
399452 const items = [
400453 { icon : ICONS . insertRowAbove , label : L . insertRowAbove || 'Insert row above' , enabled : rowIdx >= 1 ,
401- shortcut : 'Alt+Shift+Up' , run : m => tableOps . insertRowAbove ( m , rowIdx ) } ,
454+ shortcut : 'Alt+Ctrl+ Shift+Up' , run : m => tableOps . insertRowAbove ( m , rowIdx ) } ,
402455 { icon : ICONS . insertRowBelow , label : L . insertRowBelow || 'Insert row below' , enabled : true ,
403- shortcut : 'Alt+Shift+Down' , run : m => tableOps . insertRowBelow ( m , rowIdx ) } ,
456+ shortcut : 'Alt+Ctrl+ Shift+Down' , run : m => tableOps . insertRowBelow ( m , rowIdx ) } ,
404457 { icon : ICONS . moveRowUp , label : L . moveRowUp || 'Move row up' , enabled : rowIdx >= 2 ,
405- run : m => tableOps . moveRowUp ( m , rowIdx ) } ,
458+ shortcut : 'Alt+Shift+Up' , run : m => tableOps . moveRowUp ( m , rowIdx ) } ,
406459 { icon : ICONS . moveRowDown , label : L . moveRowDown || 'Move row down' , enabled : rowIdx >= 1 && rowIdx < rowCount - 1 ,
407- run : m => tableOps . moveRowDown ( m , rowIdx ) } ,
460+ shortcut : 'Alt+Shift+Down' , run : m => tableOps . moveRowDown ( m , rowIdx ) } ,
408461 { icon : ICONS . deleteRow , label : L . deleteRow || 'Delete row' , enabled : rowCount >= 2 ,
409462 run : m => tableOps . deleteRow ( m , rowIdx ) } ,
410463 { sep : true } ,
411464 { icon : ICONS . insertColLeft , label : L . insertColLeft || 'Insert column left' , enabled : true ,
412- shortcut : 'Alt+Shift+Left' , run : m => tableOps . insertColLeft ( m , rowIdx , colIdx ) } ,
465+ shortcut : 'Alt+Ctrl+ Shift+Left' , run : m => tableOps . insertColLeft ( m , rowIdx , colIdx ) } ,
413466 { icon : ICONS . insertColRight , label : L . insertColRight || 'Insert column right' , enabled : true ,
414- shortcut : 'Alt+Shift+Right' , run : m => tableOps . insertColRight ( m , rowIdx , colIdx ) } ,
467+ shortcut : 'Alt+Ctrl+ Shift+Right' , run : m => tableOps . insertColRight ( m , rowIdx , colIdx ) } ,
415468 { icon : ICONS . moveColLeft , label : L . moveColLeft || 'Move column left' , enabled : colIdx >= 1 ,
416- run : m => tableOps . moveColLeft ( m , rowIdx , colIdx ) } ,
469+ shortcut : 'Alt+Shift+Left' , run : m => tableOps . moveColLeft ( m , rowIdx , colIdx ) } ,
417470 { icon : ICONS . moveColRight , label : L . moveColRight || 'Move column right' , enabled : colIdx < colCount - 1 ,
418- run : m => tableOps . moveColRight ( m , rowIdx , colIdx ) } ,
471+ shortcut : 'Alt+Shift+Right' , run : m => tableOps . moveColRight ( m , rowIdx , colIdx ) } ,
419472 { icon : ICONS . deleteCol , label : L . deleteCol || 'Delete column' , enabled : colCount >= 2 ,
420473 run : m => tableOps . deleteCol ( m , rowIdx , colIdx ) } ,
421474 { sep : true } ,
@@ -836,13 +889,35 @@ export const attachInlineEditing = (root, opts) => {
836889 moveCaretInFocusedTableCell ( e . shiftKey ? 'up' : 'down' )
837890 } , true )
838891
839- // Alt+Shift+Arrow: insert a row above/below or a column left/right of
840- // the focused cell. Local handler for the same reason as Ctrl+Enter —
892+ // Alt+Ctrl+Arrow: caret nav between adjacent cells in the table. Local
893+ // handler for the same reason as the Ctrl+Enter / Alt+Ctrl+Shift+Arrow paths
894+ // (capture-phase swallowing hides the keys from Logseq's dispatcher);
895+ // matching commands are registered with Logseq so they appear in the
896+ // command palette / keymap UI.
897+ const caretNavKey = ( e ) => {
898+ if ( ! e . altKey || ! e . ctrlKey || e . shiftKey || e . metaKey ) return null
899+ if ( e . key === 'ArrowDown' ) return 'down'
900+ if ( e . key === 'ArrowUp' ) return 'up'
901+ if ( e . key === 'ArrowRight' ) return 'right'
902+ if ( e . key === 'ArrowLeft' ) return 'left'
903+ return null
904+ }
905+ root . addEventListener ( 'keydown' , ( e ) => {
906+ const dir = caretNavKey ( e )
907+ if ( ! dir ) return
908+ const cell = e . target . closest && e . target . closest ( 'table.lsp-mdt th, table.lsp-mdt td' )
909+ if ( ! cell ) return
910+ e . preventDefault ( ) ; e . stopPropagation ( )
911+ moveCaretInFocusedTableCell ( dir )
912+ } , true )
913+
914+ // Alt+Ctrl+Shift+Arrow: insert a row above/below or a column left/right
915+ // of the focused cell. Local handler for the same reason as Ctrl+Enter —
841916 // capture-phase swallowing means Logseq never sees the key, so a
842917 // global shortcut would be unreliable; the registered commands below
843918 // mirror these actions for the palette / keymap UI.
844919 const insertKey = ( e ) => {
845- if ( ! e . altKey || ! e . shiftKey || e . ctrlKey || e . metaKey ) return null
920+ if ( ! e . altKey || ! e . shiftKey || ! e . ctrlKey || e . metaKey ) return null
846921 if ( e . key === 'ArrowDown' ) return 'rowBelow'
847922 if ( e . key === 'ArrowUp' ) return 'rowAbove'
848923 if ( e . key === 'ArrowRight' ) return 'colRight'
@@ -858,6 +933,26 @@ export const attachInlineEditing = (root, opts) => {
858933 doInsertOp ( root , opts , cell , which )
859934 } , true )
860935
936+ // Alt+Shift+Arrow: move the focused row/column. Local handler (same
937+ // capture-phase reason as the other in-table keybinds); registered
938+ // commands below mirror these for the palette / keymap UI.
939+ const moveKey = ( e ) => {
940+ if ( ! e . altKey || ! e . shiftKey || e . ctrlKey || e . metaKey ) return null
941+ if ( e . key === 'ArrowDown' ) return 'rowDown'
942+ if ( e . key === 'ArrowUp' ) return 'rowUp'
943+ if ( e . key === 'ArrowRight' ) return 'colRight'
944+ if ( e . key === 'ArrowLeft' ) return 'colLeft'
945+ return null
946+ }
947+ root . addEventListener ( 'keydown' , ( e ) => {
948+ const which = moveKey ( e )
949+ if ( ! which ) return
950+ const cell = e . target . closest && e . target . closest ( 'table.lsp-mdt th, table.lsp-mdt td' )
951+ if ( ! cell ) return
952+ e . preventDefault ( ) ; e . stopPropagation ( )
953+ doMoveOp ( root , opts , cell , which )
954+ } , true )
955+
861956 // Force plain-text paste so markup can't smuggle structure into a cell.
862957 root . addEventListener ( 'paste' , ( e ) => {
863958 const cell = e . target . closest ( 'table.lsp-mdt th, table.lsp-mdt td' )
0 commit comments