@@ -33,7 +33,6 @@ import {
3333 setBlockType ,
3434 wrapIn ,
3535 chainCommands ,
36- exitCode ,
3736 newlineInCode ,
3837 createParagraphNear ,
3938 liftEmptyBlock ,
@@ -288,6 +287,9 @@ function tableInputRulePlugin() {
288287
289288 const table = buildTableNode ( tableRange . headerCells , tableRange . aligns ) ;
290289 const tr = state . tr . replaceWith ( tableRange . from , tableRange . to , table ) ;
290+ const afterTable = tr . mapping . map ( tableRange . from ) + table . nodeSize ;
291+ tr . insert ( afterTable , schema . node ( "paragraph" ) ) ;
292+ tr . setSelection ( TextSelection . near ( tr . doc . resolve ( afterTable + 1 ) ) ) ;
291293 view . dispatch ( tr ) ;
292294 event . preventDefault ( ) ;
293295 return true ;
@@ -367,8 +369,12 @@ function tableCellEnterCommand(state, dispatch) {
367369
368370function compactLineBreak ( state , dispatch ) {
369371 const { $head } = state . selection ;
370- if ( $head . parent . type . name !== "paragraph" ) return false ;
371- if ( dispatch ) dispatch ( state . tr . insertText ( "\n" ) ) ;
372+ if ( $head . parent . type . name === "code_block" ) {
373+ if ( dispatch ) dispatch ( state . tr . insertText ( "\n" ) ) ;
374+ return true ;
375+ }
376+ if ( ! $head . parent . inlineContent ) return false ;
377+ if ( dispatch ) dispatch ( state . tr . replaceSelectionWith ( schema . nodes . hard_break . create ( ) ) . scrollIntoView ( ) ) ;
372378 return true ;
373379}
374380
@@ -431,6 +437,13 @@ function pastePlugin() {
431437// ── Floating Table Toolbar ──
432438function tableToolbarPlugin ( ) {
433439 let toolbarEl = null ;
440+ let blurHandler = null ;
441+ let focusHandler = null ;
442+
443+ function hideToolbar ( ) {
444+ if ( toolbarEl ) toolbarEl . style . display = "none" ;
445+ }
446+
434447 function createToolbar ( ) {
435448 const el = document . createElement ( "div" ) ;
436449 el . className = "table-toolbar" ;
@@ -452,6 +465,29 @@ function tableToolbarPlugin() {
452465 }
453466 const cmds = { addColumnBefore, addColumnAfter, deleteColumn,
454467 addRowBefore, addRowAfter, deleteRow, deleteTable } ;
468+
469+ function positionForSelection ( view ) {
470+ if ( ! view . hasFocus ( ) ) { hideToolbar ( ) ; return ; }
471+
472+ const $h = view . state . selection . $head ;
473+ let inTbl = false ;
474+ let tblDom = null ;
475+ for ( let d = $h . depth ; d > 0 ; d -- ) {
476+ if ( $h . node ( d ) . type . name === "table" ) {
477+ inTbl = true ;
478+ tblDom = view . nodeDOM ( $h . before ( d ) ) ;
479+ break ;
480+ }
481+ }
482+
483+ if ( ! inTbl || ! tblDom ) { hideToolbar ( ) ; return ; }
484+
485+ const r = tblDom . getBoundingClientRect ( ) ;
486+ toolbarEl . style . display = "flex" ;
487+ toolbarEl . style . left = r . left + "px" ;
488+ toolbarEl . style . top = ( r . top - toolbarEl . offsetHeight - 6 + window . scrollY ) + "px" ;
489+ }
490+
455491 return new Plugin ( {
456492 view ( editorView ) {
457493 toolbarEl = createToolbar ( ) ;
@@ -460,29 +496,128 @@ function tableToolbarPlugin() {
460496 const btn = e . target . closest ( "button[data-cmd]" ) ;
461497 if ( btn ) { const c = cmds [ btn . dataset . cmd ] ; if ( c ) c ( editorView . state , editorView . dispatch ) ; }
462498 } ) ;
499+
500+ blurHandler = ( ) => hideToolbar ( ) ;
501+ focusHandler = ( ) => requestAnimationFrame ( ( ) => positionForSelection ( editorView ) ) ;
502+ editorView . dom . addEventListener ( "focusout" , blurHandler ) ;
503+ editorView . dom . addEventListener ( "focusin" , focusHandler ) ;
504+
463505 return {
464506 update ( view ) {
465- const $h = view . state . selection . $head ;
466- let inTbl = false , tblDom = null ;
467- for ( let d = $h . depth ; d > 0 ; d -- ) {
468- if ( $h . node ( d ) . type . name === "table" ) {
469- inTbl = true ;
470- tblDom = view . nodeDOM ( $h . before ( d ) ) ;
471- break ;
472- }
473- }
474- if ( ! inTbl || ! tblDom ) { toolbarEl . style . display = "none" ; return ; }
475- const r = tblDom . getBoundingClientRect ( ) ;
476- toolbarEl . style . display = "flex" ;
477- toolbarEl . style . left = r . left + "px" ;
478- toolbarEl . style . top = ( r . top - toolbarEl . offsetHeight - 6 + window . scrollY ) + "px" ;
507+ positionForSelection ( view ) ;
479508 } ,
480- destroy ( ) { if ( toolbarEl ) toolbarEl . remove ( ) ; }
509+ destroy ( ) {
510+ if ( blurHandler ) editorView . dom . removeEventListener ( "focusout" , blurHandler ) ;
511+ if ( focusHandler ) editorView . dom . removeEventListener ( "focusin" , focusHandler ) ;
512+ if ( toolbarEl ) toolbarEl . remove ( ) ;
513+ }
481514 } ;
482515 }
483516 } ) ;
484517}
485518
519+ // ── Floating Link Toolbar ──
520+ function linkToolbarPlugin ( ) {
521+ let toolbarEl = null ;
522+ let currentHref = "" ;
523+ let blurHandler = null ;
524+ let focusHandler = null ;
525+
526+ function hideToolbar ( ) {
527+ if ( toolbarEl ) toolbarEl . style . display = "none" ;
528+ currentHref = "" ;
529+ }
530+
531+ function getActiveLinkHref ( state ) {
532+ const { selection } = state ;
533+ const linkMarkType = schema . marks . link ;
534+ if ( ! linkMarkType ) return null ;
535+
536+ if ( selection . empty ) {
537+ const { $from } = selection ;
538+ const marks = $from . marks ( ) ;
539+ const direct = marks . find ( ( m ) => m . type === linkMarkType ) ;
540+ if ( direct ?. attrs ?. href ) return direct . attrs . href ;
541+
542+ const before = $from . nodeBefore ;
543+ if ( before ?. marks ) {
544+ const m = before . marks . find ( ( mark ) => mark . type === linkMarkType ) ;
545+ if ( m ?. attrs ?. href ) return m . attrs . href ;
546+ }
547+
548+ const after = $from . nodeAfter ;
549+ if ( after ?. marks ) {
550+ const m = after . marks . find ( ( mark ) => mark . type === linkMarkType ) ;
551+ if ( m ?. attrs ?. href ) return m . attrs . href ;
552+ }
553+
554+ return null ;
555+ }
556+
557+ let href = null ;
558+ state . doc . nodesBetween ( selection . from , selection . to , ( node ) => {
559+ if ( ! node . isText || ! node . marks || href ) return ;
560+ const mark = node . marks . find ( ( m ) => m . type === linkMarkType ) ;
561+ if ( mark ?. attrs ?. href ) href = mark . attrs . href ;
562+ } ) ;
563+ return href ;
564+ }
565+
566+ function createToolbar ( ) {
567+ const el = document . createElement ( "div" ) ;
568+ el . className = "link-toolbar" ;
569+ el . innerHTML = `<button type="button" data-cmd="openLink">Open Link</button>` ;
570+ el . style . display = "none" ;
571+ document . body . appendChild ( el ) ;
572+ el . addEventListener ( "mousedown" , ( event ) => {
573+ event . preventDefault ( ) ;
574+ if ( ! currentHref ) return ;
575+ window . open ( currentHref , "_blank" , "noopener,noreferrer" ) ;
576+ } ) ;
577+ return el ;
578+ }
579+
580+ function positionForSelection ( view ) {
581+ if ( ! view . hasFocus ( ) ) {
582+ hideToolbar ( ) ;
583+ return ;
584+ }
585+
586+ const href = getActiveLinkHref ( view . state ) ;
587+ if ( ! href ) {
588+ hideToolbar ( ) ;
589+ return ;
590+ }
591+
592+ currentHref = href ;
593+ const coords = view . coordsAtPos ( view . state . selection . from ) ;
594+ toolbarEl . style . display = "flex" ;
595+ toolbarEl . style . left = coords . left + "px" ;
596+ toolbarEl . style . top = ( coords . top - toolbarEl . offsetHeight - 6 + window . scrollY ) + "px" ;
597+ }
598+
599+ return new Plugin ( {
600+ view ( editorView ) {
601+ toolbarEl = createToolbar ( ) ;
602+ blurHandler = ( ) => hideToolbar ( ) ;
603+ focusHandler = ( ) => requestAnimationFrame ( ( ) => positionForSelection ( editorView ) ) ;
604+ editorView . dom . addEventListener ( "focusout" , blurHandler ) ;
605+ editorView . dom . addEventListener ( "focusin" , focusHandler ) ;
606+
607+ return {
608+ update ( view ) {
609+ positionForSelection ( view ) ;
610+ } ,
611+ destroy ( ) {
612+ if ( blurHandler ) editorView . dom . removeEventListener ( "focusout" , blurHandler ) ;
613+ if ( focusHandler ) editorView . dom . removeEventListener ( "focusin" , focusHandler ) ;
614+ if ( toolbarEl ) toolbarEl . remove ( ) ;
615+ } ,
616+ } ;
617+ } ,
618+ } ) ;
619+ }
620+
486621// ── Slash Command Menu ──
487622const slashKey = new PluginKey ( "slashMenu" ) ;
488623const SLASH_ITEMS = [
@@ -509,13 +644,16 @@ const SLASH_ITEMS = [
509644 if ( ! d ) return true ;
510645 const hdr = ( ) => schema . nodes . table_header . createAndFill ( ) ;
511646 const cel = ( ) => schema . nodes . table_cell . createAndFill ( ) ;
512- d ( s . tr . replaceSelectionWith (
513- schema . nodes . table . create ( null , [
514- schema . nodes . table_row . create ( null , [ hdr ( ) , hdr ( ) , hdr ( ) ] ) ,
515- schema . nodes . table_row . create ( null , [ cel ( ) , cel ( ) , cel ( ) ] ) ,
516- schema . nodes . table_row . create ( null , [ cel ( ) , cel ( ) , cel ( ) ] ) ,
517- ] )
518- ) ) ;
647+ const table = schema . nodes . table . create ( null , [
648+ schema . nodes . table_row . create ( null , [ hdr ( ) , hdr ( ) , hdr ( ) ] ) ,
649+ schema . nodes . table_row . create ( null , [ cel ( ) , cel ( ) , cel ( ) ] ) ,
650+ schema . nodes . table_row . create ( null , [ cel ( ) , cel ( ) , cel ( ) ] ) ,
651+ ] ) ;
652+ const tr = s . tr . replaceSelectionWith ( table ) ;
653+ const afterTable = tr . selection . from ;
654+ tr . insert ( afterTable , schema . node ( "paragraph" ) ) ;
655+ tr . setSelection ( TextSelection . near ( tr . doc . resolve ( afterTable + 1 ) ) ) ;
656+ d ( tr . scrollIntoView ( ) ) ;
519657 return true ;
520658 } } ,
521659] ;
@@ -547,6 +685,8 @@ function slashMenuPlugin() {
547685 menuEl . querySelectorAll ( ".slash-menu-item" ) . forEach ( el => {
548686 el . addEventListener ( "mousedown" , e => { e . preventDefault ( ) ; execute ( view , items [ + el . dataset . i ] ) ; } ) ;
549687 } ) ;
688+ const activeEl = menuEl . querySelector ( ".slash-menu-item.active" ) ;
689+ if ( activeEl ) activeEl . scrollIntoView ( { block : "nearest" } ) ;
550690 }
551691 function execute ( view , item ) {
552692 view . dispatch ( view . state . tr . delete ( slashPos , view . state . selection . from ) ) ;
@@ -627,7 +767,8 @@ function createPlugins() {
627767 "Mod-z" : undo ,
628768 "Shift-Mod-z" : redo ,
629769 "Mod-y" : redo ,
630- "Mod-Enter" : chainCommands ( exitCode , compactLineBreak ) ,
770+ "Mod-Enter" : compactLineBreak ,
771+ "Shift-Enter" : compactLineBreak ,
631772 Enter : chainCommands (
632773 newlineInCode ,
633774 tableCellEnterCommand ,
@@ -646,6 +787,7 @@ function createPlugins() {
646787 tableEditing ( ) ,
647788 keymap ( { Tab : goToNextCell ( 1 ) , "Shift-Tab" : goToNextCell ( - 1 ) } ) ,
648789 tableToolbarPlugin ( ) ,
790+ linkToolbarPlugin ( ) ,
649791 pastePlugin ( ) ,
650792 ] ;
651793}
0 commit comments