(
event: KeyboardEvent,
row: number,
col: number,
editable: HTMLElement
)
| 592 | } |
| 593 | |
| 594 | private onCellKeydown( |
| 595 | event: KeyboardEvent, |
| 596 | row: number, |
| 597 | col: number, |
| 598 | editable: HTMLElement |
| 599 | ): void { |
| 600 | const cols = this.model.headers.length |
| 601 | const rowsCount = this.model.rows.length |
| 602 | |
| 603 | if (vimEnabled()) { |
| 604 | if (this.cellMode === 'insert') { |
| 605 | // INSERT: Escape commits the cell text and drops back to NORMAL, staying |
| 606 | // put (classic Vim). Everything else types normally / falls through to |
| 607 | // the shared Tab / Enter handling below. |
| 608 | if (event.key === 'Escape') { |
| 609 | event.preventDefault() |
| 610 | event.stopPropagation() |
| 611 | const sel = window.getSelection() |
| 612 | const caret = |
| 613 | sel && sel.anchorNode && editable.contains(sel.anchorNode) ? sel.anchorOffset : 0 |
| 614 | editable.dataset.raw = editable.textContent ?? '' |
| 615 | editable.setAttribute('contenteditable', 'false') |
| 616 | this.cellMode = 'normal' |
| 617 | // Vim leaves the cursor on the char left of the caret. |
| 618 | this.cursorOffset = Math.max(0, caret - 1) |
| 619 | this.enterNormalCell(editable) |
| 620 | return |
| 621 | } |
| 622 | } else { |
| 623 | // NORMAL: h/j/k/l move between cells, i/a start editing, Esc leaves the |
| 624 | // table. Stray printable keys are swallowed so they never reach the cell. |
| 625 | // Stop unmodified keys from leaking to global shortcuts (the cell is not |
| 626 | // contenteditable in NORMAL mode, so app-level handlers — e.g. the `m` |
| 627 | // sidebar menu — would otherwise also fire). Modified combos (⌘S etc.) |
| 628 | // still pass through. |
| 629 | if (!event.metaKey && !event.ctrlKey && !event.altKey) { |
| 630 | event.stopPropagation() |
| 631 | } |
| 632 | const cellText = editable.dataset.raw ?? '' |
| 633 | if (this.visualMode) { |
| 634 | event.preventDefault() |
| 635 | this.handleVisualKey(editable, event.key, cellText) |
| 636 | return |
| 637 | } |
| 638 | if (this.pendingOp) { |
| 639 | event.preventDefault() |
| 640 | const op = this.pendingOp |
| 641 | if (this.pendingScope) { |
| 642 | // Third key: the text-object char (diw, ca", di(, …). |
| 643 | const scope = this.pendingScope |
| 644 | this.pendingOp = null |
| 645 | this.pendingScope = null |
| 646 | const r = textObjectRange(cellText, this.cursorOffset, scope, event.key) |
| 647 | if (r) { |
| 648 | this.deleteRange(editable, r.from, r.to) |
| 649 | if (op === 'c') this.enterInsertMode(editable, r.from) |
| 650 | } |
| 651 | return |
no test coverage detected