Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 77 additions & 5 deletions packages/widgets/src/data/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// @termuijs/widgets — Table widget
// ─────────────────────────────────────────────────────

import { type Screen, type Style, type Color, type KeyEvent, styleToCellAttrs, stringWidth, truncate, wordWrap, caps } from '@termuijs/core';
import { type Screen, type Style, type Color, type KeyEvent, type MouseEvent, styleToCellAttrs, stringWidth, truncate, wordWrap, caps } from '@termuijs/core';
import { Widget } from '../base/Widget.js';
import { type TableState } from './TableState.js';
import { computeVariableRange } from '../input/virtual-scroll.js';
Expand Down Expand Up @@ -35,6 +35,8 @@ export interface TableOptions {
separator?: string;
/** Callback when a column header is activated to sort */
onSort?: (colIndex: number, direction: 'asc' | 'desc') => void;
/** Whether columns can be resized by dragging separators */
resizable?: boolean;
}

export interface TableProps {
Expand Down Expand Up @@ -71,6 +73,12 @@ export class Table extends Widget {
protected _stripe: boolean;
protected _stripeColor: Color;
protected _separator: string;
protected _resizable: boolean;
protected _columnWidths: number[] = [];
private _lastWidth = -1;
private _isDragging = false;
private _dragColIndex = -1;

private _state?: TableState;
private _onStateChange?: (state: TableState) => void;
private _selectedRow = 0;
Expand Down Expand Up @@ -110,9 +118,15 @@ export class Table extends Widget {
this._stripe = options.stripe ?? true;
this._stripeColor = options.stripeColor ?? { type: 'named', name: 'brightBlack' };
this._separator = options.separator ?? ' │ ';
this._resizable = options.resizable ?? false;
this._state = state;
this._onStateChange = onStateChange;
this._tableOnSort = options.onSort;

this.events.on('key', this.handleKey.bind(this));
if (this._resizable) {
this.events.on('mouse', this.handleMouse.bind(this));
}
}

// ── Public API ────────────────────────────────────
Expand Down Expand Up @@ -170,6 +184,54 @@ export class Table extends Widget {
}
}

handleMouse(event: MouseEvent): void {
if (!this._resizable) return;

// Ensure we have computed widths at least once
const rect = this._getContentRect();
const sepWidth = stringWidth(this._separator);
this._getColWidths(rect.width, sepWidth);

if (event.type === 'mousedown') {
const colIndex = this._findColumnBoundaryAt(event.x);
if (colIndex !== -1) {
this._isDragging = true;
this._dragColIndex = colIndex;
}
} else if (event.type === 'mouseup' || event.type === 'dragend') {
this._isDragging = false;
this._dragColIndex = -1;
} else if (event.type === 'drag' && this._isDragging) {
let colStartX = rect.x;
for (let i = 0; i < this._dragColIndex; i++) {
colStartX += (this._columnWidths[i] ?? 0) + sepWidth;
}

const newWidth = Math.max(1, event.x - colStartX);
this._columnWidths[this._dragColIndex] = newWidth;

// To prevent table width from changing uncontrollably, we can subtract dx from the next column
// if there's enough space, but a simpler free-resizing UX works better for terminal tables
this.markDirty();
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private _findColumnBoundaryAt(screenX: number): number {
const rect = this._getContentRect();
let cx = rect.x;
const sepWidth = stringWidth(this._separator);

for (let c = 0; c < this._columns.length - 1; c++) {
cx += this._columnWidths[c] ?? 0;
// The separator is from cx to cx + sepWidth
if (screenX >= cx && screenX < cx + sepWidth) {
return c;
}
cx += sepWidth;
}
return -1;
}

handleKey(event: KeyEvent): void {
const minRow = this._showHeader ? -1 : 0;

Expand Down Expand Up @@ -225,7 +287,7 @@ export class Table extends Widget {
if (visibleHeight <= 0) { this._scrollOffset = 0; return; }

const sepWidth = stringWidth(this._separator);
const colWidths = this._computeColumnWidths(Math.max(0, rect.width - (this._columns.length - 1) * sepWidth));
const colWidths = this._getColWidths(rect.width, sepWidth);
const sizes = this._computeRowHeights(colWidths);

let selectedTop = 0;
Expand Down Expand Up @@ -253,9 +315,7 @@ export class Table extends Widget {
const sepWidth = stringWidth(this._separator);

// Calculate column widths
const colWidths = this._computeColumnWidths(
width - (this._columns.length - 1) * sepWidth,
);
const colWidths = this._getColWidths(width, sepWidth);

let headerOffset = 0;

Expand Down Expand Up @@ -367,6 +427,18 @@ export class Table extends Widget {
}
}

private _getColWidths(rectWidth: number, sepWidth: number): number[] {
if (!this._resizable) {
return this._computeColumnWidths(Math.max(0, rectWidth - (this._columns.length - 1) * sepWidth));
}

if (this._columnWidths.length !== this._columns.length || this._lastWidth !== rectWidth) {
this._columnWidths = this._computeColumnWidths(Math.max(0, rectWidth - (this._columns.length - 1) * sepWidth));
this._lastWidth = rectWidth;
}
return this._columnWidths;
}

protected _computeColumnWidths(totalWidth: number): number[] {
const fixedCols = this._columns.filter(c => c.width !== undefined);
const flexCols = this._columns.filter(c => c.width === undefined);
Expand Down
Loading