|
76 | 76 | table: wp.element.createElement( 'svg', { width: 24, height: 24, viewBox: '0 0 24 24' }, |
77 | 77 | wp.element.createElement( 'path', { d: 'M3 3v18h18V3H3zm8 16H5v-6h6v6zm0-8H5V5h6v6zm8 8h-6v-6h6v6zm0-8h-6V5h6v6z' } ) |
78 | 78 | ), |
| 79 | + formatTable: wp.element.createElement( 'svg', { width: 24, height: 24, viewBox: '0 0 24 24' }, |
| 80 | + wp.element.createElement( 'path', { d: 'M3 3v18h18V3H3zm8 16H5v-6h6v6zm0-8H5V5h6v6zm8 8h-6v-6h6v6zm0-8h-6V5h6v6z' } ), |
| 81 | + wp.element.createElement( 'path', { d: 'M17 17l4 4m0-4l-4 4', stroke: 'currentColor', strokeWidth: 2, fill: 'none' } ) |
| 82 | + ), |
79 | 83 | }; |
80 | 84 |
|
81 | 85 | registerBlockType( 'wp-djot/djot', { |
|
94 | 98 | const [ imageAlt, setImageAlt ] = useState( '' ); |
95 | 99 | const [ tableCols, setTableCols ] = useState( 3 ); |
96 | 100 | const [ tableRows, setTableRows ] = useState( 2 ); |
| 101 | + const [ cursorInTable, setCursorInTable ] = useState( false ); |
97 | 102 | const textareaRef = useRef( null ); |
98 | 103 | const [ selectionStart, setSelectionStart ] = useState( 0 ); |
99 | 104 | const [ selectionEnd, setSelectionEnd ] = useState( 0 ); |
|
102 | 107 | className: 'wp-djot-block', |
103 | 108 | } ); |
104 | 109 |
|
105 | | - // Track selection in textarea |
| 110 | + // Track selection in textarea and check if in table |
106 | 111 | function updateSelection() { |
107 | 112 | if ( textareaRef.current ) { |
108 | 113 | const textarea = textareaRef.current.querySelector( 'textarea' ); |
109 | 114 | if ( textarea ) { |
110 | 115 | setSelectionStart( textarea.selectionStart ); |
111 | 116 | setSelectionEnd( textarea.selectionEnd ); |
| 117 | + |
| 118 | + // Check if cursor is in a table |
| 119 | + var text = content || ''; |
| 120 | + var cursorPos = textarea.selectionStart; |
| 121 | + var lineStart = cursorPos; |
| 122 | + while ( lineStart > 0 && text[ lineStart - 1 ] !== '\n' ) { |
| 123 | + lineStart--; |
| 124 | + } |
| 125 | + var lineEnd = cursorPos; |
| 126 | + while ( lineEnd < text.length && text[ lineEnd ] !== '\n' ) { |
| 127 | + lineEnd++; |
| 128 | + } |
| 129 | + var currentLine = text.substring( lineStart, lineEnd ).trim(); |
| 130 | + setCursorInTable( currentLine.startsWith( '|' ) && currentLine.endsWith( '|' ) ); |
112 | 131 | } |
113 | 132 | } |
114 | 133 | } |
|
428 | 447 | setShowTableModal( false ); |
429 | 448 | } |
430 | 449 |
|
| 450 | + // Format table at cursor position |
| 451 | + function onFormatTable() { |
| 452 | + var textarea = textareaRef.current ? textareaRef.current.querySelector( 'textarea' ) : null; |
| 453 | + if ( ! textarea ) return; |
| 454 | + |
| 455 | + var text = content || ''; |
| 456 | + var cursorPos = textarea.selectionStart; |
| 457 | + |
| 458 | + // Find table boundaries |
| 459 | + var lines = text.split( '\n' ); |
| 460 | + var lineIndex = 0; |
| 461 | + var charCount = 0; |
| 462 | + for ( var i = 0; i < lines.length; i++ ) { |
| 463 | + if ( charCount + lines[ i ].length >= cursorPos ) { |
| 464 | + lineIndex = i; |
| 465 | + break; |
| 466 | + } |
| 467 | + charCount += lines[ i ].length + 1; // +1 for newline |
| 468 | + } |
| 469 | + |
| 470 | + // Find table start (go up until non-table line) |
| 471 | + var tableStart = lineIndex; |
| 472 | + while ( tableStart > 0 && lines[ tableStart - 1 ].trim().startsWith( '|' ) ) { |
| 473 | + tableStart--; |
| 474 | + } |
| 475 | + |
| 476 | + // Find table end (go down until non-table line) |
| 477 | + var tableEnd = lineIndex; |
| 478 | + while ( tableEnd < lines.length - 1 && lines[ tableEnd + 1 ].trim().startsWith( '|' ) ) { |
| 479 | + tableEnd++; |
| 480 | + } |
| 481 | + |
| 482 | + // Extract table lines |
| 483 | + var tableLines = lines.slice( tableStart, tableEnd + 1 ); |
| 484 | + if ( tableLines.length < 2 ) return; // Need at least header + separator |
| 485 | + |
| 486 | + // Parse cells |
| 487 | + var parsedRows = tableLines.map( function( line ) { |
| 488 | + // Remove leading/trailing pipes and split |
| 489 | + var trimmed = line.trim(); |
| 490 | + if ( trimmed.startsWith( '|' ) ) trimmed = trimmed.substring( 1 ); |
| 491 | + if ( trimmed.endsWith( '|' ) ) trimmed = trimmed.substring( 0, trimmed.length - 1 ); |
| 492 | + return trimmed.split( '|' ).map( function( cell ) { |
| 493 | + return cell.trim(); |
| 494 | + } ); |
| 495 | + } ); |
| 496 | + |
| 497 | + // Find max width per column |
| 498 | + var colWidths = []; |
| 499 | + parsedRows.forEach( function( row, rowIdx ) { |
| 500 | + row.forEach( function( cell, colIdx ) { |
| 501 | + // Skip separator row for width calculation (use dashes count) |
| 502 | + var width = cell.length; |
| 503 | + if ( rowIdx === 1 && /^[-:]+$/.test( cell ) ) { |
| 504 | + width = 3; // minimum for separator |
| 505 | + } |
| 506 | + if ( ! colWidths[ colIdx ] || width > colWidths[ colIdx ] ) { |
| 507 | + colWidths[ colIdx ] = Math.max( width, 3 ); |
| 508 | + } |
| 509 | + } ); |
| 510 | + } ); |
| 511 | + |
| 512 | + // Rebuild table with padding |
| 513 | + var formattedLines = parsedRows.map( function( row, rowIdx ) { |
| 514 | + var cells = row.map( function( cell, colIdx ) { |
| 515 | + var width = colWidths[ colIdx ] || 3; |
| 516 | + if ( rowIdx === 1 && /^[-:]+$/.test( cell ) ) { |
| 517 | + // Separator row - preserve alignment markers |
| 518 | + var leftAlign = cell.startsWith( ':' ); |
| 519 | + var rightAlign = cell.endsWith( ':' ); |
| 520 | + var dashes = '-'.repeat( width ); |
| 521 | + if ( leftAlign && rightAlign ) { |
| 522 | + return ':' + '-'.repeat( width - 2 ) + ':'; |
| 523 | + } else if ( leftAlign ) { |
| 524 | + return ':' + '-'.repeat( width - 1 ); |
| 525 | + } else if ( rightAlign ) { |
| 526 | + return '-'.repeat( width - 1 ) + ':'; |
| 527 | + } |
| 528 | + return dashes; |
| 529 | + } |
| 530 | + // Pad cell with spaces |
| 531 | + return cell + ' '.repeat( width - cell.length ); |
| 532 | + } ); |
| 533 | + return '| ' + cells.join( ' | ' ) + ' |'; |
| 534 | + } ); |
| 535 | + |
| 536 | + // Replace table in content |
| 537 | + var newLines = lines.slice( 0, tableStart ).concat( formattedLines ).concat( lines.slice( tableEnd + 1 ) ); |
| 538 | + var newText = newLines.join( '\n' ); |
| 539 | + |
| 540 | + // Calculate new cursor position (keep it roughly in same place) |
| 541 | + var newCursorPos = 0; |
| 542 | + for ( var i = 0; i < tableStart; i++ ) { |
| 543 | + newCursorPos += newLines[ i ].length + 1; |
| 544 | + } |
| 545 | + newCursorPos += formattedLines[ 0 ].length; // Put cursor at end of first table line |
| 546 | + |
| 547 | + setAttributes( { content: newText } ); |
| 548 | + restoreFocus( textarea, newCursorPos ); |
| 549 | + } |
| 550 | + |
431 | 551 | // Keyboard shortcut handler for textarea |
432 | 552 | function handleTextareaKeyDown( e ) { |
433 | 553 | const isMod = e.ctrlKey || e.metaKey; |
|
677 | 797 | label: __( 'Table', 'wp-djot' ), |
678 | 798 | onClick: onTable, |
679 | 799 | } ), |
| 800 | + cursorInTable && wp.element.createElement( ToolbarButton, { |
| 801 | + icon: icons.formatTable, |
| 802 | + label: __( 'Format Table', 'wp-djot' ), |
| 803 | + onClick: onFormatTable, |
| 804 | + } ), |
680 | 805 | wp.element.createElement( ToolbarButton, { |
681 | 806 | icon: icons.div, |
682 | 807 | label: __( 'Div Block (::: class)', 'wp-djot' ), |
|
0 commit comments