From 85527cfde8369a42c3d51645d898b1403741ec32 Mon Sep 17 00:00:00 2001 From: luxick Date: Fri, 24 Jul 2026 11:38:15 +0200 Subject: [PATCH] Improve table editing --- assets/editor/main.js | 21 ++++++++++ assets/editor/tables.js | 86 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/assets/editor/main.js b/assets/editor/main.js index 484e236..18ca2e3 100644 --- a/assets/editor/main.js +++ b/assets/editor/main.js @@ -23,9 +23,30 @@ }; } + // Enter/Tab cell navigation must yield to an open autocomplete popup so those + // keys still accept a suggestion (e.g. a [[wiki-link]] typed inside a cell). + // The table keymap sits at Prec.highest, so without this guard it would + // swallow the key before the completion keymap ever sees it. Detect the popup + // via its tooltip element rather than exporting completionStatus from the + // vendored bundle (which would force a bundle rebuild). + function completionOpen() { + return !!document.querySelector('.cm-tooltip-autocomplete'); + } + + function tableNavKey(fn) { + var handler = tableKey(fn); + return function (view) { + if (completionOpen()) return false; + return handler(view); + }; + } + var tableKeymap = [ { key: 'Shift-Enter', run: tableKey(T.insertRowBelow) }, { key: 'Shift-Delete', run: tableKey(T.deleteRow) }, + { key: 'Enter', run: tableNavKey(T.nextRowSameColumn) }, + { key: 'Tab', run: tableNavKey(T.nextCell) }, + { key: 'Shift-Tab', run: tableNavKey(T.prevCell) }, ]; var state = CM.EditorState.create({ diff --git a/assets/editor/tables.js b/assets/editor/tables.js index ce17765..785eb44 100644 --- a/assets/editor/tables.js +++ b/assets/editor/tables.js @@ -262,8 +262,94 @@ window.EditorTables = (function () { return formatTableText(newLines.join('\n'), Math.min(newCursor, newLines.join('\n').length)); } + // Absolute cursor offset at the start of the `col`-th cell's content on + // `lineIdx` of a *formatted* table (i.e. just past the "| " / " | " that + // opens the cell). Clamps to the last cell if col overflows the row. + function cellCursor(text, lineIdx, col) { + var lines = text.split('\n'); + var offset = 0; + for (var i = 0; i < lineIdx; i++) offset += lines[i].length + 1; + var line = lines[lineIdx] || ''; + var pipes = []; + for (var c = 0; c < line.length; c++) if (line.charAt(c) === '|') pipes.push(c); + if (pipes.length === 0) return Math.min(offset, text.length); + var p = pipes[Math.min(col, pipes.length - 1)]; + return Math.min(offset + p + 2, text.length); + } + + // Shared engine for the Enter / Tab / Shift-Tab cell navigation. Reformats + // the table and drops the cursor into the target cell, appending an empty + // row when navigation runs past the last row. Returns null when the cursor + // is not in a table (so the caller can fall back to the editor default). + function moveInTable(text, cursorPos, mode) { + var range = findTableRange(text, cursorPos); + if (!range) return null; + var colIdx = getCursorColumn(text, cursorPos); + if (colIdx === null) return null; + + var sepRel = -1, colCount = 0; + for (var i = range.start; i <= range.end; i++) { + var cells = parseTableRow(range.lines[i]); + if (cells.length > colCount) colCount = cells.length; + if (sepRel === -1 && isSeparatorRow(cells)) sepRel = i - range.start; + } + if (sepRel === -1) return null; + + var lastRel = range.end - range.start; + var rowRel = range.cursorLine - range.start; + var targetRow = rowRel, targetCol = colIdx; + + if (mode === 'enter') { + targetRow = rowRel + 1; + if (targetRow === sepRel) targetRow++; + } else if (mode === 'tab') { + targetCol = colIdx + 1; + if (targetCol >= colCount) { + targetCol = 0; + targetRow = rowRel + 1; + if (targetRow === sepRel) targetRow++; + } + } else if (mode === 'shifttab') { + targetCol = colIdx - 1; + if (targetCol < 0) { + targetCol = colCount - 1; + targetRow = rowRel - 1; + if (targetRow === sepRel) targetRow--; + } + if (targetRow < 0) return null; + } + + var lines = range.lines.slice(); + if (targetRow > lastRel) { + var emptyCells = []; + for (var c = 0; c < colCount; c++) emptyCells.push(''); + var emptyLine = '| ' + emptyCells.join(' | ') + ' |'; + while (lastRel < targetRow) { + lines.splice(range.start + lastRel + 1, 0, emptyLine); + lastRel++; + } + } + + var tableStartOffset = 0; + for (var i = 0; i < range.start; i++) tableStartOffset += lines[i].length + 1; + var formatted = formatTableText(lines.join('\n'), tableStartOffset); + if (!formatted) return null; + + return { + text: formatted.text, + cursor: cellCursor(formatted.text, range.start + targetRow, targetCol), + }; + } + + function nextRowSameColumn(text, cursorPos) { return moveInTable(text, cursorPos, 'enter'); } + function nextCell(text, cursorPos) { return moveInTable(text, cursorPos, 'tab'); } + function prevCell(text, cursorPos) { return moveInTable(text, cursorPos, 'shifttab'); } + return { formatTableText: formatTableText, + nextRowSameColumn: nextRowSameColumn, + nextCell: nextCell, + prevCell: prevCell, setColumnAlignment: setColumnAlignment, insertColumn: insertColumn, deleteColumn: deleteColumn,