From eb9e28b3b5ba02597b49d8d1c5deb9966dc7582a Mon Sep 17 00:00:00 2001 From: bendera Date: Sun, 28 Dec 2025 02:08:51 +0100 Subject: [PATCH 01/16] Rename --- src/vscode-table/vscode-table.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vscode-table/vscode-table.ts b/src/vscode-table/vscode-table.ts index 2ff07ca1..d6bf84c4 100644 --- a/src/vscode-table/vscode-table.ts +++ b/src/vscode-table/vscode-table.ts @@ -325,7 +325,7 @@ export class VscodeTable extends VscElement { this._resizeTableBody(); }; - private _calcColWidthPercentages(): number[] { + private _calculateInitialColumnWidths(): number[] { const numCols = this._getHeaderCells().length; let cols: (string | number)[] = this.columns.slice(0, numCols); const numAutoCols = @@ -389,7 +389,7 @@ export class VscodeTable extends VscElement { } private _initDefaultColumnSizes() { - const colWidths = this._calcColWidthPercentages(); + const colWidths = this._calculateInitialColumnWidths(); this._initHeaderCellSizes(colWidths); this._initBodyColumnSizes(colWidths); From 4c5fa08527cff6610da054284fa800a0b0a6393c Mon Sep 17 00:00:00 2001 From: bendera Date: Tue, 30 Dec 2025 02:57:01 +0100 Subject: [PATCH 02/16] wip shift table columns --- dev/vscode-table/shift-table-columns.html | 95 +++++++++ src/vscode-table/ColumnResizeController.ts | 221 +++++++++++++++++++++ src/vscode-table/vscode-table.ts | 43 +++- 3 files changed, 355 insertions(+), 4 deletions(-) create mode 100644 dev/vscode-table/shift-table-columns.html create mode 100644 src/vscode-table/ColumnResizeController.ts diff --git a/dev/vscode-table/shift-table-columns.html b/dev/vscode-table/shift-table-columns.html new file mode 100644 index 00000000..aa0f870b --- /dev/null +++ b/dev/vscode-table/shift-table-columns.html @@ -0,0 +1,95 @@ + + + + + + VSCode Elements + + + + + + + + +

Basic example

+
+ + + + id + firstname + lastname + email + company + + + + 30b1851f-393a-4462-ba28-133df9951cae + Leonel + Feeney + Jarrod.Beatty@hotmail.com + Adams, Kozey and Dooley + + + a7deaa18-0475-468f-a4e3-046df5ad2878 + Emerson + Collins + Kiarra_Predovic@hotmail.com + Yost LLC + + + 31b666d3-765d-45c9-bbbc-9d2cb64b9ff0 + Damien + Bednar + Clement57@yahoo.com + Welch and Sons + + + 72c76b94-c878-4046-85d6-28f4ebbca913 + Filomena + Dach + Morton_Nienow26@gmail.com + Rosenbaum - Wilkinson + + + + +
+ + diff --git a/src/vscode-table/ColumnResizeController.ts b/src/vscode-table/ColumnResizeController.ts new file mode 100644 index 00000000..e8749814 --- /dev/null +++ b/src/vscode-table/ColumnResizeController.ts @@ -0,0 +1,221 @@ +import {ReactiveController} from 'lit'; +import {VscodeTableHeaderCell} from '../vscode-table-header-cell/vscode-table-header-cell.js'; +import {VscodeTableCell} from '../vscode-table-cell/vscode-table-cell.js'; +import {VscodeTable} from './vscode-table.js'; + +type Px = number & {readonly __unit: 'px'}; +type Percent = number & {readonly __unit: '%'}; + +export const px = (value: number): Px => value as Px; +export const percent = (value: number): Percent => value as Percent; + +const toPercent = (px: Px, container: Px): Percent => + percent((px / container) * 100); + +const toPx = (p: Percent, container: Px): Px => px((p / 100) * container); + +function calculateColumnWidths( + widths: Percent[], + splitterIndex: number, + delta: Percent, + minWidth: Percent +): Percent[] { + const result = [...widths]; + + if (delta === 0 || splitterIndex < 0 || splitterIndex >= widths.length - 1) { + return result; + } + + const absDelta = Math.abs(delta); + let remaining: Percent = percent(absDelta); + + const leftIndices: number[] = []; + const rightIndices: number[] = []; + + // Bal oldal (splitterIndex-től balra) + for (let i = splitterIndex; i >= 0; i--) { + leftIndices.push(i); + } + + // Jobb oldal (splitterIndex+1-től jobbra) + for (let i = splitterIndex + 1; i < widths.length; i++) { + rightIndices.push(i); + } + + // Meghatározzuk, melyik oldalról VESZÜNK el + const shrinkingSide = delta > 0 ? rightIndices : leftIndices; + + // És melyik NŐ + const growingSide = delta > 0 ? leftIndices : rightIndices; + + // Van-e elég hely az elvonáshoz? + let totalAvailable: Percent = percent(0); + + for (const i of shrinkingSide) { + const available = Math.max(0, result[i] - minWidth); + totalAvailable = percent(totalAvailable + available); + } + + if (totalAvailable < remaining) { + return result; // nincs elég hely + } + + // Elvonás láncszerűen + for (const i of shrinkingSide) { + if (remaining === 0) break; + + const available = Math.max(0, result[i] - minWidth); + const take = Math.min(available, remaining); + + result[i] = percent(result[i] - take); + remaining = percent(remaining - take); + } + + // Növelés a másik oldalon (pont ugyanennyivel) + let toAdd: Percent = percent(absDelta); + + for (const i of growingSide) { + if (toAdd === 0) break; + + result[i] = percent(result[i] + toAdd); + toAdd = percent(0); // az egész az első oszlopba megy + } + + return result; +} + +export class ColumnResizeController implements ReactiveController { + private _host: VscodeTable; + private _hostHeight = px(0); + private _hostWidth = px(0); + private _hostX = px(0); + private _activeSplitter: HTMLDivElement | null = null; + private _activeSplitterIndex: number = -1; + private _splitters: HTMLDivElement[] = []; + private _headerCells: VscodeTableHeaderCell[] = []; + private _firstRowCells: VscodeTableCell[] = []; + private _minColumnWidth = percent(0); + private _isDragging = false; + private _startColumnWidths: Percent[] = []; + private _prevColumnWidths: Percent[] = []; + private _columnWidths: Percent[] = []; + private _dragStartX = px(0); + private _prevX = px(0); + private _activeSplitterCursorOffset = px(0); + + constructor(host: VscodeTable) { + (this._host = host).addController(this); + } + + hostConnected(): void { + this.saveHostDimensions(); + } + + get isDragging(): boolean { + return this._isDragging; + } + + set isDragging(newValue: boolean) { + this._isDragging = newValue; + } + + get splitterPositions(): Percent[] { + const result: Percent[] = []; + + let acc = percent(0); + + for (let i = 0; i < this._columnWidths.length - 1; i++) { + acc = percent(acc + this._columnWidths[i]); + result.push(acc); + } + + return result; + } + + get columnWidths() { + return this._columnWidths; + } + + saveHostDimensions() { + const cr = this._host.getBoundingClientRect(); + const {height, width, x} = cr; + this._hostHeight = px(height); + this._hostWidth = px(width); + this._hostX = px(x); + } + + setActiveSplitter(splitterElement: HTMLDivElement) { + this._activeSplitter = splitterElement; + const index = +(splitterElement.dataset?.index ?? ''); + this._activeSplitterIndex = index > -1 ? index : -1; + return this; + } + + setSplitters(splitterElements: HTMLDivElement[]) { + this._splitters = splitterElements; + return this; + } + + setHeaderCells(cells: VscodeTableHeaderCell[]) { + this._headerCells = cells; + return this; + } + + setFirstRowCells(cells: VscodeTableCell[]) { + this._firstRowCells = cells; + return this; + } + + setMinColumnWidth(width: Percent) { + this._minColumnWidth = width; + return this; + } + + setColumWidths(widths: Percent[]) { + this._columnWidths = widths; + this._host.requestUpdate(); + return this; + } + + startDrag(event: PointerEvent) { + const {pageX} = event; + const cr = this._activeSplitter!.getBoundingClientRect(); + + this._dragStartX = px(pageX); + this._startColumnWidths = [...this._columnWidths]; + this._prevColumnWidths = [...this._columnWidths]; + this._activeSplitterCursorOffset = px(pageX - cr.x); + this._prevX = px(pageX); + } + + drag(event: MouseEvent) { + const {pageX} = event; + // const deltaPx = px( + // pageX - this._dragStartX - this._activeSplitterCursorOffset + // ); + const deltaPx = px(pageX - this._prevX); + this._prevX = px(pageX); + // const deltaPx = px(pageX - this._hostX - this._activeSplitterCursorOffset); + const delta = this._toPercent(deltaPx); + this._columnWidths = calculateColumnWidths( + this._columnWidths, + this._activeSplitterIndex, + delta, + this._minColumnWidth + ); + + // this._prevColumnWidths = [...this._columnWidths]; + + this._host.requestUpdate(); + } + + stopDrag() {} + + private _toPercent(px: Px) { + return toPercent(px, this._hostWidth); + } + + private _toPx(percent: Percent) { + return toPx(percent, this._hostWidth); + } +} diff --git a/src/vscode-table/vscode-table.ts b/src/vscode-table/vscode-table.ts index d6bf84c4..5b08b025 100644 --- a/src/vscode-table/vscode-table.ts +++ b/src/vscode-table/vscode-table.ts @@ -1,4 +1,4 @@ -import {html, TemplateResult} from 'lit'; +import {html, PropertyValues, TemplateResult} from 'lit'; import { property, query, @@ -17,6 +17,7 @@ import {VscodeTableHeader} from '../vscode-table-header/index.js'; import {VscodeTableHeaderCell} from '../vscode-table-header-cell/index.js'; import {rawValueToPercentage} from './helpers.js'; import styles from './vscode-table.styles.js'; +import {ColumnResizeController, percent} from './ColumnResizeController.js'; const COMPONENT_WIDTH_PERCENTAGE = 100; @@ -193,6 +194,8 @@ export class VscodeTable extends VscElement { private _prevHeaderHeight = 0; private _prevComponentHeight = 0; + private _columnResizeController = new ColumnResizeController(this); + override connectedCallback(): void { super.connectedCallback(); @@ -207,6 +210,16 @@ export class VscodeTable extends VscElement { this._bodyResizeObserver?.disconnect(); } + protected override willUpdate(changedProperties: PropertyValues): void { + if (changedProperties.has('minColumnWidth')) { + console.log(rawValueToPercentage(this.minColumnWidth, this._componentW)); + const value = percent( + rawValueToPercentage(this.minColumnWidth, this._componentW) ?? 0 + ); + this._columnResizeController.setMinColumnWidth(value); + } + } + private _px2Percent(px: number) { return (px / this._componentW) * 100; } @@ -390,6 +403,9 @@ export class VscodeTable extends VscElement { private _initDefaultColumnSizes() { const colWidths = this._calculateInitialColumnWidths(); + this._columnResizeController.setColumWidths( + colWidths.map((c) => percent(c)) + ); this._initHeaderCellSizes(colWidths); this._initBodyColumnSizes(colWidths); @@ -513,9 +529,15 @@ export class VscodeTable extends VscElement { this.requestUpdate(); } - private _onSashMouseDown(event: MouseEvent) { + private _onSashMouseDown(event: PointerEvent) { event.stopPropagation(); + this._columnResizeController.saveHostDimensions(); + this._columnResizeController.setActiveSplitter( + event.currentTarget as HTMLDivElement + ); + this._columnResizeController.startDrag(event); + const {pageX, currentTarget} = event; const el = currentTarget as HTMLDivElement; const index = Number(el.dataset.index); @@ -606,7 +628,7 @@ export class VscodeTable extends VscElement { this._headerCellsToResize[0].style.width = prevColCss; if (this._headerCellsToResize[1]) { - this._headerCellsToResize[1].style.width = nextColCss; + // this._headerCellsToResize[1].style.width = nextColCss; } if (resizeBodyCells && this._cellsToResize[0]) { @@ -616,10 +638,21 @@ export class VscodeTable extends VscElement { this._cellsToResize[1].style.width = nextColCss; } } + + const widths = this._columnResizeController.columnWidths; + + const headerCells = this._getHeaderCells(); + headerCells.forEach((h, i) => (h.style.width = `${widths[i]}%`)); + + const firstRowCells = this._getCellsOfFirstRow(); + firstRowCells.forEach((c, i) => (c.style.width = `${widths[i]}%`)); } private _onResizingMouseMove = (event: MouseEvent) => { event.stopPropagation(); + + this._columnResizeController.drag(event); + this._updateActiveSashPosition(event.pageX); if (!this.delayedResizing) { @@ -641,7 +674,9 @@ export class VscodeTable extends VscElement { }; override render(): TemplateResult { - const sashes = this._sashPositions.map((val, index) => { + const splitterPositions = this._columnResizeController.splitterPositions; + + const sashes = splitterPositions.map((val, index) => { const classes = classMap({ sash: true, hover: this._sashHovers[index], From 19317cc9ffdaaf8cff2f6d21538785eb75e62287 Mon Sep 17 00:00:00 2001 From: bendera Date: Tue, 30 Dec 2025 18:02:23 +0100 Subject: [PATCH 03/16] Do not allow omit curly braces --- .github/workflows/verify.yml | 6 +++--- eslint.config.mjs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index aa562a9b..74986576 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -32,11 +32,11 @@ jobs: - name: Transpile files run: npm run build:ts - - name: Prettier - run: npm run prettier - - name: ESLint run: npm run lint + - name: Prettier + run: npm run prettier + - name: Test run: npm run test diff --git a/eslint.config.mjs b/eslint.config.mjs index a3106bae..8fcb2838 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -39,6 +39,7 @@ export default [ rules: { 'no-console': 'error', 'no-unexpected-multiline': 'off', + curly: ['error', 'all'], '@typescript-eslint/indent': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-non-null-assertion': 'off', From b94557735469d1f28a4b379b11511e5f21d71aa6 Mon Sep 17 00:00:00 2001 From: bendera Date: Tue, 30 Dec 2025 18:04:24 +0100 Subject: [PATCH 04/16] wip resize table columns --- dev/vscode-table/shift-table-columns.html | 2 +- src/vscode-table/ColumnResizeController.ts | 149 ++++++----------- .../{helpers.test.ts => calculations.test.ts} | 2 +- src/vscode-table/calculations.ts | 104 ++++++++++++ src/vscode-table/helpers.ts | 18 -- src/vscode-table/vscode-table.styles.ts | 15 +- src/vscode-table/vscode-table.ts | 156 ++++-------------- 7 files changed, 194 insertions(+), 252 deletions(-) rename src/vscode-table/{helpers.test.ts => calculations.test.ts} (94%) create mode 100644 src/vscode-table/calculations.ts delete mode 100644 src/vscode-table/helpers.ts diff --git a/dev/vscode-table/shift-table-columns.html b/dev/vscode-table/shift-table-columns.html index aa0f870b..d5cc4383 100644 --- a/dev/vscode-table/shift-table-columns.html +++ b/dev/vscode-table/shift-table-columns.html @@ -40,7 +40,7 @@

Basic example

diff --git a/src/vscode-table/ColumnResizeController.ts b/src/vscode-table/ColumnResizeController.ts index e8749814..06daeee9 100644 --- a/src/vscode-table/ColumnResizeController.ts +++ b/src/vscode-table/ColumnResizeController.ts @@ -2,87 +2,25 @@ import {ReactiveController} from 'lit'; import {VscodeTableHeaderCell} from '../vscode-table-header-cell/vscode-table-header-cell.js'; import {VscodeTableCell} from '../vscode-table-cell/vscode-table-cell.js'; import {VscodeTable} from './vscode-table.js'; - -type Px = number & {readonly __unit: 'px'}; -type Percent = number & {readonly __unit: '%'}; - -export const px = (value: number): Px => value as Px; -export const percent = (value: number): Percent => value as Percent; - -const toPercent = (px: Px, container: Px): Percent => - percent((px / container) * 100); - -const toPx = (p: Percent, container: Px): Px => px((p / 100) * container); - -function calculateColumnWidths( - widths: Percent[], - splitterIndex: number, - delta: Percent, - minWidth: Percent -): Percent[] { - const result = [...widths]; - - if (delta === 0 || splitterIndex < 0 || splitterIndex >= widths.length - 1) { - return result; - } - - const absDelta = Math.abs(delta); - let remaining: Percent = percent(absDelta); - - const leftIndices: number[] = []; - const rightIndices: number[] = []; - - // Bal oldal (splitterIndex-től balra) - for (let i = splitterIndex; i >= 0; i--) { - leftIndices.push(i); - } - - // Jobb oldal (splitterIndex+1-től jobbra) - for (let i = splitterIndex + 1; i < widths.length; i++) { - rightIndices.push(i); - } - - // Meghatározzuk, melyik oldalról VESZÜNK el - const shrinkingSide = delta > 0 ? rightIndices : leftIndices; - - // És melyik NŐ - const growingSide = delta > 0 ? leftIndices : rightIndices; - - // Van-e elég hely az elvonáshoz? - let totalAvailable: Percent = percent(0); - - for (const i of shrinkingSide) { - const available = Math.max(0, result[i] - minWidth); - totalAvailable = percent(totalAvailable + available); - } - - if (totalAvailable < remaining) { - return result; // nincs elég hely - } - - // Elvonás láncszerűen - for (const i of shrinkingSide) { - if (remaining === 0) break; - - const available = Math.max(0, result[i] - minWidth); - const take = Math.min(available, remaining); - - result[i] = percent(result[i] - take); - remaining = percent(remaining - take); - } - - // Növelés a másik oldalon (pont ugyanennyivel) - let toAdd: Percent = percent(absDelta); - - for (const i of growingSide) { - if (toAdd === 0) break; - - result[i] = percent(result[i] + toAdd); - toAdd = percent(0); // az egész az első oszlopba megy - } - - return result; -} +import { + calculateColumnWidths, + Percent, + percent, + Px, + px, + toPercent, + toPx, +} from './calculations.js'; +import { + SPLITTER_HIT_WIDTH, + SPLITTER_VISIBLE_WIDTH, +} from './vscode-table.styles.js'; + +const calculateOffset = (mouseX: number, splitterX: number) => { + const centerLineRelativeX = + SPLITTER_HIT_WIDTH / 2 - SPLITTER_VISIBLE_WIDTH / 2; + const relativeX = mouseX - splitterX; +}; export class ColumnResizeController implements ReactiveController { private _host: VscodeTable; @@ -151,6 +89,10 @@ export class ColumnResizeController implements ReactiveController { return this; } + getActiveSplitter() { + return this._activeSplitter; + } + setSplitters(splitterElements: HTMLDivElement[]) { this._splitters = splitterElements; return this; @@ -178,25 +120,28 @@ export class ColumnResizeController implements ReactiveController { } startDrag(event: PointerEvent) { - const {pageX} = event; - const cr = this._activeSplitter!.getBoundingClientRect(); - - this._dragStartX = px(pageX); - this._startColumnWidths = [...this._columnWidths]; - this._prevColumnWidths = [...this._columnWidths]; - this._activeSplitterCursorOffset = px(pageX - cr.x); - this._prevX = px(pageX); - } - - drag(event: MouseEvent) { - const {pageX} = event; - // const deltaPx = px( - // pageX - this._dragStartX - this._activeSplitterCursorOffset - // ); - const deltaPx = px(pageX - this._prevX); - this._prevX = px(pageX); - // const deltaPx = px(pageX - this._hostX - this._activeSplitterCursorOffset); + const mouseX = event.pageX; + const splitterX = this._activeSplitter!.getBoundingClientRect().x; + const xOffset = px(mouseX - splitterX); + + this._isDragging = true; + this._dragStartX = px(mouseX - xOffset); + this._activeSplitterCursorOffset = px(xOffset); + this._prevX = this._dragStartX; + } + + drag(event: PointerEvent) { + const mouseX = event.pageX; + + if (mouseX > this._hostX + this._hostWidth) { + return; + } + + const x = px(mouseX - this._activeSplitterCursorOffset); + const deltaPx = px(x - this._prevX); + this._prevX = x; const delta = this._toPercent(deltaPx); + this._columnWidths = calculateColumnWidths( this._columnWidths, this._activeSplitterIndex, @@ -204,12 +149,12 @@ export class ColumnResizeController implements ReactiveController { this._minColumnWidth ); - // this._prevColumnWidths = [...this._columnWidths]; - this._host.requestUpdate(); } - stopDrag() {} + stopDrag() { + this._isDragging = false; + } private _toPercent(px: Px) { return toPercent(px, this._hostWidth); diff --git a/src/vscode-table/helpers.test.ts b/src/vscode-table/calculations.test.ts similarity index 94% rename from src/vscode-table/helpers.test.ts rename to src/vscode-table/calculations.test.ts index ffcee978..4215492d 100644 --- a/src/vscode-table/helpers.test.ts +++ b/src/vscode-table/calculations.test.ts @@ -1,5 +1,5 @@ import {expect} from '@open-wc/testing'; -import {rawValueToPercentage} from './helpers.js'; +import {rawValueToPercentage} from './calculations.js'; describe('vscode-table helpers', () => { it('input type is number', () => { diff --git a/src/vscode-table/calculations.ts b/src/vscode-table/calculations.ts new file mode 100644 index 00000000..b59a63fb --- /dev/null +++ b/src/vscode-table/calculations.ts @@ -0,0 +1,104 @@ +export type Px = number & {readonly __unit: 'px'}; +export type Percent = number & {readonly __unit: '%'}; + +export const px = (value: number): Px => value as Px; +export const percent = (value: number): Percent => value as Percent; + +export const toPercent = (px: Px, container: Px): Percent => + percent((px / container) * 100); + +export const toPx = (p: Percent, container: Px): Px => + px((p / 100) * container); + +export function calculateColumnWidths( + widths: Percent[], + splitterIndex: number, + delta: Percent, + minWidth: Percent +): Percent[] { + const result = [...widths]; + + // No-op for invalid splitter position or zero delta + if (delta === 0 || splitterIndex < 0 || splitterIndex >= widths.length - 1) { + return result; + } + + const absDelta = Math.abs(delta); + let remaining: Percent = percent(absDelta); + + const leftIndices: number[] = []; + const rightIndices: number[] = []; + + // Collect column indices to the left of the splitter (inclusive) + for (let i = splitterIndex; i >= 0; i--) { + leftIndices.push(i); + } + + // Collect column indices to the right of the splitter + for (let i = splitterIndex + 1; i < widths.length; i++) { + rightIndices.push(i); + } + + // One side shrinks, the other grows depending on drag direction + const shrinkingSide = delta > 0 ? rightIndices : leftIndices; + const growingSide = delta > 0 ? leftIndices : rightIndices; + + // Calculate total shrinkable space respecting minWidth + let totalAvailable: Percent = percent(0); + + for (const i of shrinkingSide) { + const available = Math.max(0, result[i] - minWidth); + totalAvailable = percent(totalAvailable + available); + } + + // Abort if the requested delta cannot be fully satisfied + if (totalAvailable < remaining) { + return result; + } + + // Shrink columns sequentially until the delta is fully consumed + for (const i of shrinkingSide) { + if (remaining === 0) { + break; + } + + const available = Math.max(0, result[i] - minWidth); + const take = Math.min(available, remaining); + + result[i] = percent(result[i] - take); + remaining = percent(remaining - take); + } + + // Apply the exact opposite delta to the growing side + let toAdd: Percent = percent(absDelta); + + for (const i of growingSide) { + if (toAdd === 0) { + break; + } + + result[i] = percent(result[i] + toAdd); + toAdd = percent(0); // all growth is applied to the nearest column + } + + return result; +} + +export const rawValueToPercentage = ( + raw: string | number, + base: number +): number | null => { + if (typeof raw === 'number' && !Number.isNaN(raw)) { + return (raw / base) * 100; + } else if (typeof raw === 'string' && /^[0-9.]+$/.test(raw)) { + const val = Number(raw); + return (val / base) * 100; + } else if (typeof raw === 'string' && /^[0-9.]+%$/.test(raw)) { + return Number(raw.substring(0, raw.length - 1)); + } else if (typeof raw === 'string' && /^[0-9.]+px$/.test(raw)) { + const val = Number(raw.substring(0, raw.length - 2)); + return (val / base) * 100; + } else { + return null; + } +}; diff --git a/src/vscode-table/helpers.ts b/src/vscode-table/helpers.ts deleted file mode 100644 index c125fe89..00000000 --- a/src/vscode-table/helpers.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const rawValueToPercentage = ( - raw: string | number, - base: number -): number | null => { - if (typeof raw === 'number' && !Number.isNaN(raw)) { - return (raw / base) * 100; - } else if (typeof raw === 'string' && /^[0-9.]+$/.test(raw)) { - const val = Number(raw); - return (val / base) * 100; - } else if (typeof raw === 'string' && /^[0-9.]+%$/.test(raw)) { - return Number(raw.substring(0, raw.length - 1)); - } else if (typeof raw === 'string' && /^[0-9.]+px$/.test(raw)) { - const val = Number(raw.substring(0, raw.length - 2)); - return (val / base) * 100; - } else { - return null; - } -}; diff --git a/src/vscode-table/vscode-table.styles.ts b/src/vscode-table/vscode-table.styles.ts index 19aafe42..d6bf923b 100644 --- a/src/vscode-table/vscode-table.styles.ts +++ b/src/vscode-table/vscode-table.styles.ts @@ -1,6 +1,9 @@ import {css, CSSResultGroup} from 'lit'; import baseStyles from '../includes/default.styles.js'; +export const SPLITTER_HIT_WIDTH = 21; +export const SPLITTER_VISIBLE_WIDTH = 1; + const styles: CSSResultGroup = [ baseStyles, css` @@ -88,7 +91,7 @@ const styles: CSSResultGroup = [ } .sash { - visibility: hidden; + visibility: visible; } :host([bordered-columns]) .sash, @@ -120,10 +123,10 @@ const styles: CSSResultGroup = [ --vscode-editorGroup-border, rgba(255, 255, 255, 0.09) ); - height: 100%; + height: calc(100% - 30px); position: absolute; top: 30px; - width: 1px; + width: ${SPLITTER_VISIBLE_WIDTH}px; } .sash.hover .sash-visible { @@ -132,11 +135,11 @@ const styles: CSSResultGroup = [ } .sash .sash-clickable { - background-color: transparent; + background-color: rgba(255, 0, 0, 0.2); height: 100%; - left: -2px; + left: ${0 - (SPLITTER_HIT_WIDTH - SPLITTER_VISIBLE_WIDTH) / 2}px; position: absolute; - width: 5px; + width: ${SPLITTER_HIT_WIDTH}px; } `, ]; diff --git a/src/vscode-table/vscode-table.ts b/src/vscode-table/vscode-table.ts index 5b08b025..64ff73f0 100644 --- a/src/vscode-table/vscode-table.ts +++ b/src/vscode-table/vscode-table.ts @@ -15,11 +15,10 @@ import {VscodeTableBody} from '../vscode-table-body/index.js'; import {VscodeTableCell} from '../vscode-table-cell/index.js'; import {VscodeTableHeader} from '../vscode-table-header/index.js'; import {VscodeTableHeaderCell} from '../vscode-table-header-cell/index.js'; -import {rawValueToPercentage} from './helpers.js'; +import {rawValueToPercentage} from './calculations.js'; import styles from './vscode-table.styles.js'; -import {ColumnResizeController, percent} from './ColumnResizeController.js'; - -const COMPONENT_WIDTH_PERCENTAGE = 100; +import {ColumnResizeController} from './ColumnResizeController.js'; +import {percent} from './calculations.js'; /** * @tag vscode-table @@ -196,6 +195,8 @@ export class VscodeTable extends VscElement { private _columnResizeController = new ColumnResizeController(this); + private _activePointerId = 0; + override connectedCallback(): void { super.connectedCallback(); @@ -220,14 +221,6 @@ export class VscodeTable extends VscElement { } } - private _px2Percent(px: number) { - return (px / this._componentW) * 100; - } - - private _percent2Px(percent: number) { - return (this._componentW * percent) / 100; - } - private _memoizeComponentDimensions() { const cr = this.getBoundingClientRect(); @@ -529,132 +522,40 @@ export class VscodeTable extends VscElement { this.requestUpdate(); } - private _onSashMouseDown(event: PointerEvent) { + private _onSashPointerDown(event: PointerEvent) { event.stopPropagation(); + const activeSplitter = event.currentTarget as HTMLDivElement; + this._activePointerId = event.pointerId; + activeSplitter.setPointerCapture(this._activePointerId); + this._columnResizeController.saveHostDimensions(); - this._columnResizeController.setActiveSplitter( - event.currentTarget as HTMLDivElement - ); + this._columnResizeController.setActiveSplitter(activeSplitter); this._columnResizeController.startDrag(event); - const {pageX, currentTarget} = event; - const el = currentTarget as HTMLDivElement; - const index = Number(el.dataset.index); - const cr = el.getBoundingClientRect(); - const elX = cr.x; - - this._isDragging = true; - this._activeSashElementIndex = index; - this._sashHovers[this._activeSashElementIndex] = true; - this._activeSashCursorOffset = this._px2Percent(pageX - elX); - - const headerCells = this._getHeaderCells(); - this._headerCellsToResize = []; - this._headerCellsToResize.push(headerCells[index]); - - if (headerCells[index + 1]) { - this._headerCellsToResize[1] = headerCells[index + 1]; - } - - const tbody = this._bodySlot.assignedElements()[0]; - const cells = tbody.querySelectorAll( - 'vscode-table-row:first-child > vscode-table-cell' - ); - this._cellsToResize = []; - this._cellsToResize.push(cells[index]); - - if (cells[index + 1]) { - this._cellsToResize.push(cells[index + 1]); - } - - document.addEventListener('mousemove', this._onResizingMouseMove); - document.addEventListener('mouseup', this._onResizingMouseUp); - } - - private _updateActiveSashPosition(mouseX: number) { - const {prevSashPos, nextSashPos} = this._getSashPositions(); - let minColumnWidth = rawValueToPercentage( - this.minColumnWidth, - this._componentW - ); - - if (minColumnWidth === null) { - minColumnWidth = 0; - } - - const minX = prevSashPos ? prevSashPos + minColumnWidth : minColumnWidth; - const maxX = nextSashPos - ? nextSashPos - minColumnWidth - : COMPONENT_WIDTH_PERCENTAGE - minColumnWidth; - let newX = this._px2Percent( - mouseX - this._componentX - this._percent2Px(this._activeSashCursorOffset) - ); - - newX = Math.max(newX, minX); - newX = Math.min(newX, maxX); - - this._sashPositions[this._activeSashElementIndex] = newX; - this.requestUpdate(); - } - - private _getSashPositions(): { - sashPos: number; - prevSashPos: number; - nextSashPos: number; - } { - const sashPos = this._sashPositions[this._activeSashElementIndex]; - const prevSashPos = - this._sashPositions[this._activeSashElementIndex - 1] || 0; - const nextSashPos = - this._sashPositions[this._activeSashElementIndex + 1] || - COMPONENT_WIDTH_PERCENTAGE; - - return { - sashPos, - prevSashPos, - nextSashPos, - }; + activeSplitter.addEventListener('pointermove', this._onResizingMouseMove); + activeSplitter.addEventListener('pointerup', this._onResizingMouseUp); + activeSplitter.addEventListener('pointercancel', this._onResizingMouseUp); } private _resizeColumns(resizeBodyCells = true) { - const {sashPos, prevSashPos, nextSashPos} = this._getSashPositions(); - - const prevColW = sashPos - prevSashPos; - const nextColW = nextSashPos - sashPos; - const prevColCss = `${prevColW}%`; - const nextColCss = `${nextColW}%`; - - this._headerCellsToResize[0].style.width = prevColCss; - - if (this._headerCellsToResize[1]) { - // this._headerCellsToResize[1].style.width = nextColCss; - } - - if (resizeBodyCells && this._cellsToResize[0]) { - this._cellsToResize[0].style.width = prevColCss; - - if (this._cellsToResize[1]) { - this._cellsToResize[1].style.width = nextColCss; - } - } - const widths = this._columnResizeController.columnWidths; const headerCells = this._getHeaderCells(); headerCells.forEach((h, i) => (h.style.width = `${widths[i]}%`)); - const firstRowCells = this._getCellsOfFirstRow(); - firstRowCells.forEach((c, i) => (c.style.width = `${widths[i]}%`)); + if (resizeBodyCells) { + const firstRowCells = this._getCellsOfFirstRow(); + firstRowCells.forEach((c, i) => (c.style.width = `${widths[i]}%`)); + } } - private _onResizingMouseMove = (event: MouseEvent) => { + private _onResizingMouseMove = (event: PointerEvent) => { + console.log('pointermove'); event.stopPropagation(); this._columnResizeController.drag(event); - this._updateActiveSashPosition(event.pageX); - if (!this.delayedResizing) { this._resizeColumns(true); } else { @@ -662,15 +563,22 @@ export class VscodeTable extends VscElement { } }; - private _onResizingMouseUp = (event: MouseEvent) => { + private _onResizingMouseUp = (event: PointerEvent) => { + console.log('event type', event.type); + const activeSplitter = this._columnResizeController.getActiveSplitter(); + activeSplitter?.releasePointerCapture?.(event.pointerId); + this._columnResizeController.stopDrag(); + this._resizeColumns(true); - this._updateActiveSashPosition(event.pageX); this._sashHovers[this._activeSashElementIndex] = false; this._isDragging = false; this._activeSashElementIndex = -1; - document.removeEventListener('mousemove', this._onResizingMouseMove); - document.removeEventListener('mouseup', this._onResizingMouseUp); + activeSplitter?.removeEventListener?.( + 'pointermove', + this._onResizingMouseMove + ); + activeSplitter?.removeEventListener?.('pointerup', this._onResizingMouseUp); }; override render(): TemplateResult { @@ -691,7 +599,7 @@ export class VscodeTable extends VscElement { class=${classes} data-index=${index} .style=${stylePropertyMap({left})} - @mousedown=${this._onSashMouseDown} + @pointerdown=${this._onSashPointerDown} @mouseover=${this._onSashMouseOver} @mouseout=${this._onSashMouseOut} > From ba86eb4bb3fa192354b8cc8c8d9d5f4ef6ea6473 Mon Sep 17 00:00:00 2001 From: bendera Date: Tue, 30 Dec 2025 23:10:12 +0100 Subject: [PATCH 05/16] wip --- src/vscode-table/ColumnResizeController.ts | 143 ++++++++++++--------- src/vscode-table/vscode-table.ts | 86 +++++++++++-- 2 files changed, 156 insertions(+), 73 deletions(-) diff --git a/src/vscode-table/ColumnResizeController.ts b/src/vscode-table/ColumnResizeController.ts index 06daeee9..43678bf3 100644 --- a/src/vscode-table/ColumnResizeController.ts +++ b/src/vscode-table/ColumnResizeController.ts @@ -1,7 +1,5 @@ import {ReactiveController} from 'lit'; -import {VscodeTableHeaderCell} from '../vscode-table-header-cell/vscode-table-header-cell.js'; -import {VscodeTableCell} from '../vscode-table-cell/vscode-table-cell.js'; -import {VscodeTable} from './vscode-table.js'; +import {type VscodeTable} from './vscode-table.js'; import { calculateColumnWidths, Percent, @@ -9,37 +7,30 @@ import { Px, px, toPercent, - toPx, } from './calculations.js'; -import { - SPLITTER_HIT_WIDTH, - SPLITTER_VISIBLE_WIDTH, -} from './vscode-table.styles.js'; - -const calculateOffset = (mouseX: number, splitterX: number) => { - const centerLineRelativeX = - SPLITTER_HIT_WIDTH / 2 - SPLITTER_VISIBLE_WIDTH / 2; - const relativeX = mouseX - splitterX; + +type SplitterElement = HTMLDivElement & { + dataset: DOMStringMap & { + index: string; + }; }; export class ColumnResizeController implements ReactiveController { private _host: VscodeTable; - private _hostHeight = px(0); private _hostWidth = px(0); private _hostX = px(0); - private _activeSplitter: HTMLDivElement | null = null; - private _activeSplitterIndex: number = -1; - private _splitters: HTMLDivElement[] = []; - private _headerCells: VscodeTableHeaderCell[] = []; - private _firstRowCells: VscodeTableCell[] = []; + private _activeSplitter: SplitterElement | null = null; private _minColumnWidth = percent(0); - private _isDragging = false; - private _startColumnWidths: Percent[] = []; - private _prevColumnWidths: Percent[] = []; private _columnWidths: Percent[] = []; private _dragStartX = px(0); private _prevX = px(0); private _activeSplitterCursorOffset = px(0); + private _dragState: { + splitterIndex: number; + pointerId: number; + startX: Px; + dragOffset: Px; + } | null = null; constructor(host: VscodeTable) { (this._host = host).addController(this); @@ -50,11 +41,7 @@ export class ColumnResizeController implements ReactiveController { } get isDragging(): boolean { - return this._isDragging; - } - - set isDragging(newValue: boolean) { - this._isDragging = newValue; + return this._dragState !== null; } get splitterPositions(): Percent[] { @@ -76,16 +63,14 @@ export class ColumnResizeController implements ReactiveController { saveHostDimensions() { const cr = this._host.getBoundingClientRect(); - const {height, width, x} = cr; - this._hostHeight = px(height); + const {width, x} = cr; this._hostWidth = px(width); this._hostX = px(x); + return this; } - setActiveSplitter(splitterElement: HTMLDivElement) { - this._activeSplitter = splitterElement; - const index = +(splitterElement.dataset?.index ?? ''); - this._activeSplitterIndex = index > -1 ? index : -1; + setActiveSplitter(splitter: HTMLElement) { + this._activeSplitter = splitter as SplitterElement; return this; } @@ -93,21 +78,6 @@ export class ColumnResizeController implements ReactiveController { return this._activeSplitter; } - setSplitters(splitterElements: HTMLDivElement[]) { - this._splitters = splitterElements; - return this; - } - - setHeaderCells(cells: VscodeTableHeaderCell[]) { - this._headerCells = cells; - return this; - } - - setFirstRowCells(cells: VscodeTableCell[]) { - this._firstRowCells = cells; - return this; - } - setMinColumnWidth(width: Percent) { this._minColumnWidth = width; return this; @@ -119,24 +89,67 @@ export class ColumnResizeController implements ReactiveController { return this; } + shouldDrag(event: PointerEvent) { + return ( + +(event.currentTarget as SplitterElement).dataset.index === + this._dragState?.splitterIndex + ); + } + startDrag(event: PointerEvent) { + event.stopPropagation(); + + if (this._dragState) { + return; + } + + this._activeSplitter?.setPointerCapture(event.pointerId); + const mouseX = event.pageX; - const splitterX = this._activeSplitter!.getBoundingClientRect().x; + const splitter = event.currentTarget as SplitterElement; + const splitterX = splitter!.getBoundingClientRect().x; const xOffset = px(mouseX - splitterX); - this._isDragging = true; + this._dragState = { + dragOffset: px(xOffset), + pointerId: event.pointerId, + splitterIndex: +splitter.dataset.index, + startX: px(mouseX - xOffset), + }; + + console.log(event.currentTarget); + this._dragStartX = px(mouseX - xOffset); this._activeSplitterCursorOffset = px(xOffset); this._prevX = this._dragStartX; + this._host.requestUpdate(); } drag(event: PointerEvent) { - const mouseX = event.pageX; + event.stopPropagation(); + + if ( + !(event?.currentTarget as SplitterElement)?.hasPointerCapture?.( + event.pointerId + ) + ) { + return; + } + + if (!this._dragState) { + return; + } + + if (event.pointerId !== this._dragState.pointerId) { + return; + } - if (mouseX > this._hostX + this._hostWidth) { + if (!this.shouldDrag(event)) { return; } + const mouseX = event.pageX; + const x = px(mouseX - this._activeSplitterCursorOffset); const deltaPx = px(x - this._prevX); this._prevX = x; @@ -144,7 +157,7 @@ export class ColumnResizeController implements ReactiveController { this._columnWidths = calculateColumnWidths( this._columnWidths, - this._activeSplitterIndex, + this._dragState.splitterIndex, delta, this._minColumnWidth ); @@ -152,15 +165,27 @@ export class ColumnResizeController implements ReactiveController { this._host.requestUpdate(); } - stopDrag() { - this._isDragging = false; + stopDrag(event: PointerEvent) { + event.stopPropagation(); + + if (!this._dragState) { + return; + } + + const el = event.currentTarget as SplitterElement; + + try { + el.releasePointerCapture(this._dragState.pointerId); + } catch (e) { + // ignore + } + + this._dragState = null; + this._activeSplitter = null; + this._host.requestUpdate(); } private _toPercent(px: Px) { return toPercent(px, this._hostWidth); } - - private _toPx(percent: Percent) { - return toPx(percent, this._hostWidth); - } } diff --git a/src/vscode-table/vscode-table.ts b/src/vscode-table/vscode-table.ts index 64ff73f0..f00ce82e 100644 --- a/src/vscode-table/vscode-table.ts +++ b/src/vscode-table/vscode-table.ts @@ -213,7 +213,6 @@ export class VscodeTable extends VscElement { protected override willUpdate(changedProperties: PropertyValues): void { if (changedProperties.has('minColumnWidth')) { - console.log(rawValueToPercentage(this.minColumnWidth, this._componentW)); const value = percent( rawValueToPercentage(this.minColumnWidth, this._componentW) ?? 0 ); @@ -463,6 +462,32 @@ export class VscodeTable extends VscElement { } } + private _stopDrag(event: PointerEvent) { + const activeSplitter = this._columnResizeController.getActiveSplitter(); + + if (activeSplitter) { + activeSplitter.removeEventListener( + 'pointermove', + this._handleSplitterPointerMove + ); + activeSplitter.removeEventListener( + 'pointerup', + this._handleSplitterPointerUp + ); + activeSplitter.removeEventListener( + 'pointercancel', + this._handleSplitterPointerCancel + ); + } + + this._columnResizeController.stopDrag(event); + this._resizeColumns(true); + + this._sashHovers[this._activeSashElementIndex] = false; + this._isDragging = false; + this._activeSashElementIndex = -1; + } + private _onDefaultSlotChange() { this._assignedElements.forEach((el) => { if (el.tagName.toLowerCase() === 'vscode-table-header') { @@ -522,7 +547,7 @@ export class VscodeTable extends VscElement { this.requestUpdate(); } - private _onSashPointerDown(event: PointerEvent) { + /* private _onSashPointerDown(event: PointerEvent) { event.stopPropagation(); const activeSplitter = event.currentTarget as HTMLDivElement; @@ -530,13 +555,12 @@ export class VscodeTable extends VscElement { activeSplitter.setPointerCapture(this._activePointerId); this._columnResizeController.saveHostDimensions(); - this._columnResizeController.setActiveSplitter(activeSplitter); this._columnResizeController.startDrag(event); activeSplitter.addEventListener('pointermove', this._onResizingMouseMove); activeSplitter.addEventListener('pointerup', this._onResizingMouseUp); activeSplitter.addEventListener('pointercancel', this._onResizingMouseUp); - } + } */ private _resizeColumns(resizeBodyCells = true) { const widths = this._columnResizeController.columnWidths; @@ -550,7 +574,7 @@ export class VscodeTable extends VscElement { } } - private _onResizingMouseMove = (event: PointerEvent) => { + /* private _onResizingMouseMove = (event: PointerEvent) => { console.log('pointermove'); event.stopPropagation(); @@ -565,20 +589,54 @@ export class VscodeTable extends VscElement { private _onResizingMouseUp = (event: PointerEvent) => { console.log('event type', event.type); - const activeSplitter = this._columnResizeController.getActiveSplitter(); - activeSplitter?.releasePointerCapture?.(event.pointerId); - this._columnResizeController.stopDrag(); + this._columnResizeController.stopDrag(event); this._resizeColumns(true); this._sashHovers[this._activeSashElementIndex] = false; this._isDragging = false; this._activeSashElementIndex = -1; + }; */ + + private _handleSplitterPointerDown(event: PointerEvent) { + event.stopPropagation(); - activeSplitter?.removeEventListener?.( + const activeSplitter = event.currentTarget as HTMLElement; + + this._columnResizeController + .saveHostDimensions() + .setActiveSplitter(activeSplitter) + .startDrag(event); + + activeSplitter.addEventListener( 'pointermove', - this._onResizingMouseMove + this._handleSplitterPointerMove + ); + activeSplitter.addEventListener('pointerup', this._handleSplitterPointerUp); + activeSplitter.addEventListener( + 'pointercancel', + this._handleSplitterPointerCancel ); - activeSplitter?.removeEventListener?.('pointerup', this._onResizingMouseUp); + } + + private _handleSplitterPointerMove = (event: PointerEvent) => { + if (!this._columnResizeController.shouldDrag(event)) { + return; + } + + this._columnResizeController.drag(event); + if (!this.delayedResizing) { + this._resizeColumns(true); + } else { + this._resizeColumns(false); + } + }; + + private _handleSplitterPointerUp = (event: PointerEvent) => { + this._stopDrag(event); + }; + + private _handleSplitterPointerCancel = (event: PointerEvent) => { + this._stopDrag(event); }; override render(): TemplateResult { @@ -599,7 +657,7 @@ export class VscodeTable extends VscElement { class=${classes} data-index=${index} .style=${stylePropertyMap({left})} - @pointerdown=${this._onSashPointerDown} + @pointerdown=${this._handleSplitterPointerDown} @mouseover=${this._onSashMouseOver} @mouseout=${this._onSashMouseOut} > @@ -618,8 +676,8 @@ export class VscodeTable extends VscElement { const wrapperClasses = classMap({ wrapper: true, - 'select-disabled': this._isDragging, - 'resize-cursor': this._isDragging, + 'select-disabled': this._columnResizeController.isDragging, + 'resize-cursor': this._columnResizeController.isDragging, 'compact-view': this.compact, }); From 2d15572a578f75386b3e5d52b1f825f721042bea Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 31 Dec 2025 02:29:24 +0100 Subject: [PATCH 06/16] Fix splitter dragging limits --- dev/vscode-table/shift-table-columns.html | 1 + src/vscode-table/ColumnResizeController.ts | 49 ++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/dev/vscode-table/shift-table-columns.html b/dev/vscode-table/shift-table-columns.html index d5cc4383..998b2938 100644 --- a/dev/vscode-table/shift-table-columns.html +++ b/dev/vscode-table/shift-table-columns.html @@ -42,6 +42,7 @@

Basic example

columns='["100px", "200px"]' min-column-width="50" resizable + style="margin-left: 200px" > id diff --git a/src/vscode-table/ColumnResizeController.ts b/src/vscode-table/ColumnResizeController.ts index 43678bf3..3272eb66 100644 --- a/src/vscode-table/ColumnResizeController.ts +++ b/src/vscode-table/ColumnResizeController.ts @@ -7,7 +7,9 @@ import { Px, px, toPercent, + toPx, } from './calculations.js'; +import {SPLITTER_HIT_WIDTH} from './vscode-table.styles.js'; type SplitterElement = HTMLDivElement & { dataset: DOMStringMap & { @@ -22,6 +24,7 @@ export class ColumnResizeController implements ReactiveController { private _activeSplitter: SplitterElement | null = null; private _minColumnWidth = percent(0); private _columnWidths: Percent[] = []; + private _startColumnWidths: Percent[] = []; private _dragStartX = px(0); private _prevX = px(0); private _activeSplitterCursorOffset = px(0); @@ -29,6 +32,7 @@ export class ColumnResizeController implements ReactiveController { splitterIndex: number; pointerId: number; startX: Px; + prevX: Px; dragOffset: Px; } | null = null; @@ -57,6 +61,19 @@ export class ColumnResizeController implements ReactiveController { return result; } + getActiveSplitterCalculatedPosition() { + const splitterPositions = this.splitterPositions; + + if (!this._dragState) { + return px(0); + } + + const activeSplitterPos = splitterPositions[this._dragState.splitterIndex]; + const activeSplitterPosPx = this._toPx(activeSplitterPos); + + return activeSplitterPosPx; + } + get columnWidths() { return this._columnWidths; } @@ -115,9 +132,9 @@ export class ColumnResizeController implements ReactiveController { pointerId: event.pointerId, splitterIndex: +splitter.dataset.index, startX: px(mouseX - xOffset), + prevX: px(mouseX - xOffset), }; - - console.log(event.currentTarget); + this._startColumnWidths = this._columnWidths; this._dragStartX = px(mouseX - xOffset); this._activeSplitterCursorOffset = px(xOffset); @@ -150,11 +167,23 @@ export class ColumnResizeController implements ReactiveController { const mouseX = event.pageX; - const x = px(mouseX - this._activeSplitterCursorOffset); + const x = px(mouseX - this._dragState.dragOffset); const deltaPx = px(x - this._prevX); + this._prevX = x; const delta = this._toPercent(deltaPx); + /// + const splitterPos = this.getActiveSplitterCalculatedPosition(); + + if ( + (deltaPx <= 0 && mouseX > splitterPos + this._hostX) || + (deltaPx > 0 && mouseX < splitterPos + this._hostX) + ) { + return; + } + /// + this._columnWidths = calculateColumnWidths( this._columnWidths, this._dragState.splitterIndex, @@ -188,4 +217,18 @@ export class ColumnResizeController implements ReactiveController { private _toPercent(px: Px) { return toPercent(px, this._hostWidth); } + + private _toPx(percent: Percent) { + return toPx(percent, this._hostWidth); + } + + private _isCursorOverTheActiveSplitter(event: PointerEvent) { + const localMouseX = event.pageX - this._hostX; + const localSplitterX = this.getActiveSplitterCalculatedPosition(); + + return ( + localMouseX >= localSplitterX - SPLITTER_HIT_WIDTH / 2 && + localMouseX <= localSplitterX + SPLITTER_HIT_WIDTH / 2 + ); + } } From 7a9c5ecd87a5d486a5efa6ca9aef6ff0d4bb9eb7 Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 31 Dec 2025 03:33:48 +0100 Subject: [PATCH 07/16] Add final splitter styles --- src/vscode-table/vscode-table.styles.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vscode-table/vscode-table.styles.ts b/src/vscode-table/vscode-table.styles.ts index d6bf923b..d7accac7 100644 --- a/src/vscode-table/vscode-table.styles.ts +++ b/src/vscode-table/vscode-table.styles.ts @@ -1,7 +1,7 @@ import {css, CSSResultGroup} from 'lit'; import baseStyles from '../includes/default.styles.js'; -export const SPLITTER_HIT_WIDTH = 21; +export const SPLITTER_HIT_WIDTH = 5; export const SPLITTER_VISIBLE_WIDTH = 1; const styles: CSSResultGroup = [ @@ -135,7 +135,6 @@ const styles: CSSResultGroup = [ } .sash .sash-clickable { - background-color: rgba(255, 0, 0, 0.2); height: 100%; left: ${0 - (SPLITTER_HIT_WIDTH - SPLITTER_VISIBLE_WIDTH) / 2}px; position: absolute; From 7b8ac67d666fd1581fd941492a8e0f60284f5e15 Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 31 Dec 2025 03:49:18 +0100 Subject: [PATCH 08/16] Cleanup --- src/vscode-table/ColumnResizeController.ts | 27 ++-------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/src/vscode-table/ColumnResizeController.ts b/src/vscode-table/ColumnResizeController.ts index 3272eb66..469f419f 100644 --- a/src/vscode-table/ColumnResizeController.ts +++ b/src/vscode-table/ColumnResizeController.ts @@ -9,7 +9,6 @@ import { toPercent, toPx, } from './calculations.js'; -import {SPLITTER_HIT_WIDTH} from './vscode-table.styles.js'; type SplitterElement = HTMLDivElement & { dataset: DOMStringMap & { @@ -24,10 +23,6 @@ export class ColumnResizeController implements ReactiveController { private _activeSplitter: SplitterElement | null = null; private _minColumnWidth = percent(0); private _columnWidths: Percent[] = []; - private _startColumnWidths: Percent[] = []; - private _dragStartX = px(0); - private _prevX = px(0); - private _activeSplitterCursorOffset = px(0); private _dragState: { splitterIndex: number; pointerId: number; @@ -134,11 +129,7 @@ export class ColumnResizeController implements ReactiveController { startX: px(mouseX - xOffset), prevX: px(mouseX - xOffset), }; - this._startColumnWidths = this._columnWidths; - this._dragStartX = px(mouseX - xOffset); - this._activeSplitterCursorOffset = px(xOffset); - this._prevX = this._dragStartX; this._host.requestUpdate(); } @@ -166,14 +157,11 @@ export class ColumnResizeController implements ReactiveController { } const mouseX = event.pageX; - const x = px(mouseX - this._dragState.dragOffset); - const deltaPx = px(x - this._prevX); - - this._prevX = x; + const deltaPx = px(x - this._dragState.prevX); const delta = this._toPercent(deltaPx); + this._dragState.prevX = x; - /// const splitterPos = this.getActiveSplitterCalculatedPosition(); if ( @@ -182,7 +170,6 @@ export class ColumnResizeController implements ReactiveController { ) { return; } - /// this._columnWidths = calculateColumnWidths( this._columnWidths, @@ -221,14 +208,4 @@ export class ColumnResizeController implements ReactiveController { private _toPx(percent: Percent) { return toPx(percent, this._hostWidth); } - - private _isCursorOverTheActiveSplitter(event: PointerEvent) { - const localMouseX = event.pageX - this._hostX; - const localSplitterX = this.getActiveSplitterCalculatedPosition(); - - return ( - localMouseX >= localSplitterX - SPLITTER_HIT_WIDTH / 2 && - localMouseX <= localSplitterX + SPLITTER_HIT_WIDTH / 2 - ); - } } From b2ecc26c0b93ae14dfdb7b9ffac236d844b7000e Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 31 Dec 2025 03:51:00 +0100 Subject: [PATCH 09/16] Remove an unused prop --- src/vscode-table/ColumnResizeController.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vscode-table/ColumnResizeController.ts b/src/vscode-table/ColumnResizeController.ts index 469f419f..6791ce2b 100644 --- a/src/vscode-table/ColumnResizeController.ts +++ b/src/vscode-table/ColumnResizeController.ts @@ -26,7 +26,6 @@ export class ColumnResizeController implements ReactiveController { private _dragState: { splitterIndex: number; pointerId: number; - startX: Px; prevX: Px; dragOffset: Px; } | null = null; @@ -126,7 +125,6 @@ export class ColumnResizeController implements ReactiveController { dragOffset: px(xOffset), pointerId: event.pointerId, splitterIndex: +splitter.dataset.index, - startX: px(mouseX - xOffset), prevX: px(mouseX - xOffset), }; From 9c9186bcab86cf6e2d9fcf468a709f0698743188 Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 31 Dec 2025 03:54:38 +0100 Subject: [PATCH 10/16] Cache splitter positions --- src/vscode-table/ColumnResizeController.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vscode-table/ColumnResizeController.ts b/src/vscode-table/ColumnResizeController.ts index 6791ce2b..e8d75af3 100644 --- a/src/vscode-table/ColumnResizeController.ts +++ b/src/vscode-table/ColumnResizeController.ts @@ -29,6 +29,7 @@ export class ColumnResizeController implements ReactiveController { prevX: Px; dragOffset: Px; } | null = null; + private _cachedSplitterPositions: Percent[] | null = null; constructor(host: VscodeTable) { (this._host = host).addController(this); @@ -43,6 +44,10 @@ export class ColumnResizeController implements ReactiveController { } get splitterPositions(): Percent[] { + if (this._cachedSplitterPositions) { + return this._cachedSplitterPositions; + } + const result: Percent[] = []; let acc = percent(0); @@ -52,6 +57,8 @@ export class ColumnResizeController implements ReactiveController { result.push(acc); } + this._cachedSplitterPositions = result; + return result; } @@ -96,6 +103,7 @@ export class ColumnResizeController implements ReactiveController { setColumWidths(widths: Percent[]) { this._columnWidths = widths; + this._cachedSplitterPositions = null; this._host.requestUpdate(); return this; } @@ -175,6 +183,7 @@ export class ColumnResizeController implements ReactiveController { delta, this._minColumnWidth ); + this._cachedSplitterPositions = null; this._host.requestUpdate(); } From f40fb3a6c766a0d132edff1a5eb97a32c5af22d8 Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 31 Dec 2025 03:56:58 +0100 Subject: [PATCH 11/16] Cleanup --- src/vscode-table/vscode-table.ts | 48 -------------------------------- 1 file changed, 48 deletions(-) diff --git a/src/vscode-table/vscode-table.ts b/src/vscode-table/vscode-table.ts index f00ce82e..23a4ec83 100644 --- a/src/vscode-table/vscode-table.ts +++ b/src/vscode-table/vscode-table.ts @@ -124,9 +124,6 @@ export class VscodeTable extends VscElement { @property({type: Boolean, reflect: true, attribute: 'zebra-odd'}) zebraOdd = false; - @query('slot[name="body"]') - private _bodySlot!: HTMLSlotElement; - @query('.header') private _headerElement!: HTMLDivElement; @@ -174,8 +171,6 @@ export class VscodeTable extends VscElement { private _headerResizeObserver?: ResizeObserver; private _bodyResizeObserver?: ResizeObserver; private _activeSashElementIndex = -1; - private _activeSashCursorOffset = 0; - private _componentX = 0; private _componentH = 0; private _componentW = 0; /** @@ -188,15 +183,11 @@ export class VscodeTable extends VscElement { * It shouldn't be used directly, check the "_getCellsOfFirstRow" function. */ private _cellsOfFirstRow: VscodeTableCell[] = []; - private _cellsToResize!: VscodeTableCell[]; - private _headerCellsToResize!: VscodeTableHeaderCell[]; private _prevHeaderHeight = 0; private _prevComponentHeight = 0; private _columnResizeController = new ColumnResizeController(this); - private _activePointerId = 0; - override connectedCallback(): void { super.connectedCallback(); @@ -225,7 +216,6 @@ export class VscodeTable extends VscElement { this._componentH = cr.height; this._componentW = cr.width; - this._componentX = cr.x; } private _queryHeaderCells() { @@ -547,21 +537,6 @@ export class VscodeTable extends VscElement { this.requestUpdate(); } - /* private _onSashPointerDown(event: PointerEvent) { - event.stopPropagation(); - - const activeSplitter = event.currentTarget as HTMLDivElement; - this._activePointerId = event.pointerId; - activeSplitter.setPointerCapture(this._activePointerId); - - this._columnResizeController.saveHostDimensions(); - this._columnResizeController.startDrag(event); - - activeSplitter.addEventListener('pointermove', this._onResizingMouseMove); - activeSplitter.addEventListener('pointerup', this._onResizingMouseUp); - activeSplitter.addEventListener('pointercancel', this._onResizingMouseUp); - } */ - private _resizeColumns(resizeBodyCells = true) { const widths = this._columnResizeController.columnWidths; @@ -574,29 +549,6 @@ export class VscodeTable extends VscElement { } } - /* private _onResizingMouseMove = (event: PointerEvent) => { - console.log('pointermove'); - event.stopPropagation(); - - this._columnResizeController.drag(event); - - if (!this.delayedResizing) { - this._resizeColumns(true); - } else { - this._resizeColumns(false); - } - }; - - private _onResizingMouseUp = (event: PointerEvent) => { - console.log('event type', event.type); - this._columnResizeController.stopDrag(event); - - this._resizeColumns(true); - this._sashHovers[this._activeSashElementIndex] = false; - this._isDragging = false; - this._activeSashElementIndex = -1; - }; */ - private _handleSplitterPointerDown(event: PointerEvent) { event.stopPropagation(); From 8b36c8f8625c0daaaff71f0a5de30711d31d88d6 Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 31 Dec 2025 04:10:26 +0100 Subject: [PATCH 12/16] Revert original style --- src/vscode-table/vscode-table.styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vscode-table/vscode-table.styles.ts b/src/vscode-table/vscode-table.styles.ts index d7accac7..5f43752a 100644 --- a/src/vscode-table/vscode-table.styles.ts +++ b/src/vscode-table/vscode-table.styles.ts @@ -91,7 +91,7 @@ const styles: CSSResultGroup = [ } .sash { - visibility: visible; + visibility: hidden; } :host([bordered-columns]) .sash, From c7620e8cb4dc60a55292d98e2d8ba9221cddf06c Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 31 Dec 2025 13:59:51 +0100 Subject: [PATCH 13/16] Rename attribute parser --- src/vscode-table/calculations.test.ts | 24 ++++++++++++------------ src/vscode-table/calculations.ts | 2 +- src/vscode-table/vscode-table.ts | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/vscode-table/calculations.test.ts b/src/vscode-table/calculations.test.ts index 4215492d..f266f384 100644 --- a/src/vscode-table/calculations.test.ts +++ b/src/vscode-table/calculations.test.ts @@ -1,29 +1,29 @@ import {expect} from '@open-wc/testing'; -import {rawValueToPercentage} from './calculations.js'; +import {parseSizeAttributeToPercent} from './calculations.js'; -describe('vscode-table helpers', () => { +describe('parseSizeAttributeToPercent', () => { it('input type is number', () => { - expect(rawValueToPercentage(50, 200)).to.eq(25); - expect(rawValueToPercentage(10.5, 200)).to.eq(5.25); + expect(parseSizeAttributeToPercent(50, 200)).to.eq(25); + expect(parseSizeAttributeToPercent(10.5, 200)).to.eq(5.25); }); it('input type is string', () => { - expect(rawValueToPercentage('50', 200)).to.eq(25); - expect(rawValueToPercentage('10.5', 200)).to.eq(5.25); + expect(parseSizeAttributeToPercent('50', 200)).to.eq(25); + expect(parseSizeAttributeToPercent('10.5', 200)).to.eq(5.25); }); it('input type is percentage', () => { - expect(rawValueToPercentage('50%', 200)).to.eq(50); - expect(rawValueToPercentage('10.5%', 200)).to.eq(10.5); + expect(parseSizeAttributeToPercent('50%', 200)).to.eq(50); + expect(parseSizeAttributeToPercent('10.5%', 200)).to.eq(10.5); }); it('input type is pixel', () => { - expect(rawValueToPercentage('50px', 200)).to.eq(25); - expect(rawValueToPercentage('10.5px', 200)).to.eq(5.25); + expect(parseSizeAttributeToPercent('50px', 200)).to.eq(25); + expect(parseSizeAttributeToPercent('10.5px', 200)).to.eq(5.25); }); it('input type is invalid value', () => { - expect(rawValueToPercentage('-50%', 200)).to.eq(null); - expect(rawValueToPercentage('auto', 200)).to.eq(null); + expect(parseSizeAttributeToPercent('-50%', 200)).to.eq(null); + expect(parseSizeAttributeToPercent('auto', 200)).to.eq(null); }); }); diff --git a/src/vscode-table/calculations.ts b/src/vscode-table/calculations.ts index b59a63fb..758823f0 100644 --- a/src/vscode-table/calculations.ts +++ b/src/vscode-table/calculations.ts @@ -84,7 +84,7 @@ export function calculateColumnWidths( return result; } -export const rawValueToPercentage = ( +export const parseSizeAttributeToPercent = ( raw: string | number, base: number ): number | null => { diff --git a/src/vscode-table/vscode-table.ts b/src/vscode-table/vscode-table.ts index 23a4ec83..39a1d358 100644 --- a/src/vscode-table/vscode-table.ts +++ b/src/vscode-table/vscode-table.ts @@ -15,7 +15,7 @@ import {VscodeTableBody} from '../vscode-table-body/index.js'; import {VscodeTableCell} from '../vscode-table-cell/index.js'; import {VscodeTableHeader} from '../vscode-table-header/index.js'; import {VscodeTableHeaderCell} from '../vscode-table-header-cell/index.js'; -import {rawValueToPercentage} from './calculations.js'; +import {parseSizeAttributeToPercent} from './calculations.js'; import styles from './vscode-table.styles.js'; import {ColumnResizeController} from './ColumnResizeController.js'; import {percent} from './calculations.js'; @@ -205,7 +205,7 @@ export class VscodeTable extends VscElement { protected override willUpdate(changedProperties: PropertyValues): void { if (changedProperties.has('minColumnWidth')) { const value = percent( - rawValueToPercentage(this.minColumnWidth, this._componentW) ?? 0 + parseSizeAttributeToPercent(this.minColumnWidth, this._componentW) ?? 0 ); this._columnResizeController.setMinColumnWidth(value); } @@ -328,7 +328,7 @@ export class VscodeTable extends VscElement { let availablePercent = 100; cols = cols.map((col) => { - const percentage = rawValueToPercentage(col, this._componentW); + const percentage = parseSizeAttributeToPercent(col, this._componentW); if (percentage === null) { return 'auto'; From 22dd2eb5d2b6816f6414b1c70cb2107ec9fe4afb Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 31 Dec 2025 15:19:57 +0100 Subject: [PATCH 14/16] Refactor the attibute parser --- src/vscode-table/calculations.test.ts | 88 ++++++++++++++++++++++----- src/vscode-table/calculations.ts | 41 +++++++++---- 2 files changed, 103 insertions(+), 26 deletions(-) diff --git a/src/vscode-table/calculations.test.ts b/src/vscode-table/calculations.test.ts index f266f384..6430ee14 100644 --- a/src/vscode-table/calculations.test.ts +++ b/src/vscode-table/calculations.test.ts @@ -1,29 +1,87 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ import {expect} from '@open-wc/testing'; import {parseSizeAttributeToPercent} from './calculations.js'; describe('parseSizeAttributeToPercent', () => { - it('input type is number', () => { - expect(parseSizeAttributeToPercent(50, 200)).to.eq(25); - expect(parseSizeAttributeToPercent(10.5, 200)).to.eq(5.25); + const base = 200; + + // number input + it('should parse valid number input', () => { + expect(parseSizeAttributeToPercent(50, base)).to.equal(25); + expect(parseSizeAttributeToPercent(0, base)).to.equal(0); + expect(parseSizeAttributeToPercent(200, base)).to.equal(100); + expect(parseSizeAttributeToPercent(-50, base)).to.equal(-25); + }); + + it('should return null for invalid number input', () => { + expect(parseSizeAttributeToPercent(NaN, base)).to.be.null; + expect(parseSizeAttributeToPercent(Infinity, base)).to.be.null; + expect(parseSizeAttributeToPercent(-Infinity, base)).to.be.null; + }); + + // string number input + it('should parse valid string number', () => { + expect(parseSizeAttributeToPercent('50', base)).to.equal(25); + expect(parseSizeAttributeToPercent('0', base)).to.equal(0); + expect(parseSizeAttributeToPercent('100.5', base)).to.be.closeTo( + 50.25, + 0.0001 + ); + expect(parseSizeAttributeToPercent('-50', base)).to.equal(-25); + expect(parseSizeAttributeToPercent(' 50 ', base)).to.equal(25); + }); + + it('should return null for invalid string number', () => { + expect(parseSizeAttributeToPercent('abc', base)).to.be.null; + expect(parseSizeAttributeToPercent('50abc', base)).to.be.null; + expect(parseSizeAttributeToPercent('', base)).to.be.null; + expect(parseSizeAttributeToPercent(' ', base)).to.be.null; + expect(parseSizeAttributeToPercent('NaN', base)).to.be.null; + }); + + // px input + it('should parse valid px input', () => { + expect(parseSizeAttributeToPercent('50px', base)).to.equal(25); + expect(parseSizeAttributeToPercent('0px', base)).to.equal(0); + expect(parseSizeAttributeToPercent('100.5px', base)).to.be.closeTo( + 50.25, + 0.0001 + ); + expect(parseSizeAttributeToPercent('-50px', base)).to.equal(-25); + expect(parseSizeAttributeToPercent(' 50px ', base)).to.equal(25); }); - it('input type is string', () => { - expect(parseSizeAttributeToPercent('50', 200)).to.eq(25); - expect(parseSizeAttributeToPercent('10.5', 200)).to.eq(5.25); + it('should return null for invalid px input', () => { + expect(parseSizeAttributeToPercent('50p', base)).to.be.null; + expect(parseSizeAttributeToPercent('px', base)).to.be.null; + expect(parseSizeAttributeToPercent('50px%', base)).to.be.null; }); - it('input type is percentage', () => { - expect(parseSizeAttributeToPercent('50%', 200)).to.eq(50); - expect(parseSizeAttributeToPercent('10.5%', 200)).to.eq(10.5); + // percent input + it('should parse valid percent input', () => { + expect(parseSizeAttributeToPercent('25%', base)).to.equal(25); + expect(parseSizeAttributeToPercent('0%', base)).to.equal(0); + expect(parseSizeAttributeToPercent('100%', base)).to.equal(100); + expect(parseSizeAttributeToPercent('50.5%', base)).to.be.closeTo( + 50.5, + 0.0001 + ); + expect(parseSizeAttributeToPercent('-20%', base)).to.equal(-20); + expect(parseSizeAttributeToPercent(' 30% ', base)).to.equal(30); }); - it('input type is pixel', () => { - expect(parseSizeAttributeToPercent('50px', 200)).to.eq(25); - expect(parseSizeAttributeToPercent('10.5px', 200)).to.eq(5.25); + it('should return null for invalid percent input', () => { + expect(parseSizeAttributeToPercent('%', base)).to.be.null; + expect(parseSizeAttributeToPercent('20%%', base)).to.be.null; + expect(parseSizeAttributeToPercent('abc%', base)).to.be.null; + expect(parseSizeAttributeToPercent('50%px', base)).to.be.null; }); - it('input type is invalid value', () => { - expect(parseSizeAttributeToPercent('-50%', 200)).to.eq(null); - expect(parseSizeAttributeToPercent('auto', 200)).to.eq(null); + // invalid base + it('should return null for invalid base', () => { + expect(parseSizeAttributeToPercent('50', 0)).to.be.null; + expect(parseSizeAttributeToPercent('50', NaN)).to.be.null; + expect(parseSizeAttributeToPercent('50', Infinity)).to.be.null; + expect(parseSizeAttributeToPercent(50, 0)).to.be.null; }); }); diff --git a/src/vscode-table/calculations.ts b/src/vscode-table/calculations.ts index 758823f0..d47d97a7 100644 --- a/src/vscode-table/calculations.ts +++ b/src/vscode-table/calculations.ts @@ -84,21 +84,40 @@ export function calculateColumnWidths( return result; } +type Parser = { + test: (value: string) => boolean; + parse: (value: string, base: number) => number; +}; + +const parsers: Parser[] = [ + { + test: (v) => /^-?\d+(\.\d+)?%$/.test(v), + parse: (v) => Number(v.slice(0, -1)), + }, + { + test: (v) => /^-?\d+(\.\d+)?px$/.test(v), + parse: (v, base) => (Number(v.slice(0, -2)) / base) * 100, + }, + { + test: (v) => /^-?\d+(\.\d+)?$/.test(v), + parse: (v, base) => (Number(v) / base) * 100, + }, +]; + export const parseSizeAttributeToPercent = ( raw: string | number, base: number ): number | null => { - if (typeof raw === 'number' && !Number.isNaN(raw)) { - return (raw / base) * 100; - } else if (typeof raw === 'string' && /^[0-9.]+$/.test(raw)) { - const val = Number(raw); - return (val / base) * 100; - } else if (typeof raw === 'string' && /^[0-9.]+%$/.test(raw)) { - return Number(raw.substring(0, raw.length - 1)); - } else if (typeof raw === 'string' && /^[0-9.]+px$/.test(raw)) { - const val = Number(raw.substring(0, raw.length - 2)); - return (val / base) * 100; - } else { + if (!Number.isFinite(base) || base === 0) { return null; } + + if (typeof raw === 'number') { + return Number.isFinite(raw) ? (raw / base) * 100 : null; + } + + const value = raw.trim(); + const parser = parsers.find((p) => p.test(value)); + + return parser ? parser.parse(value, base) : null; }; From 1ab2e6267ae20d275161204da6c6c2db0bf0ba8b Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 31 Dec 2025 15:32:25 +0100 Subject: [PATCH 15/16] Add tests --- src/vscode-table/calculations.test.ts | 94 ++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/src/vscode-table/calculations.test.ts b/src/vscode-table/calculations.test.ts index 6430ee14..2e4a51f2 100644 --- a/src/vscode-table/calculations.test.ts +++ b/src/vscode-table/calculations.test.ts @@ -1,6 +1,11 @@ /* eslint-disable @typescript-eslint/no-unused-expressions */ import {expect} from '@open-wc/testing'; -import {parseSizeAttributeToPercent} from './calculations.js'; +import { + calculateColumnWidths, + parseSizeAttributeToPercent, + Percent, + percent, +} from './calculations.js'; describe('parseSizeAttributeToPercent', () => { const base = 200; @@ -85,3 +90,90 @@ describe('parseSizeAttributeToPercent', () => { expect(parseSizeAttributeToPercent(50, 0)).to.be.null; }); }); + +describe('calculateColumnWidths', () => { + it('returns unchanged widths when delta is 0', () => { + const widths = [percent(25), percent(25), percent(50)]; + + const result = calculateColumnWidths(widths, 1, percent(0), percent(10)); + + expect(result).to.deep.equal(widths); + }); + + it('returns unchanged widths for invalid splitter index', () => { + const widths = [percent(30), percent(30), percent(40)]; + + expect( + calculateColumnWidths(widths, -1, percent(10), percent(10)) + ).to.deep.equal(widths); + expect( + calculateColumnWidths(widths, 2, percent(10), percent(10)) + ).to.deep.equal(widths); + }); + + it('shrinks right column and grows left column when dragging right (delta > 0)', () => { + const widths = [percent(30), percent(30), percent(40)]; + + const result = calculateColumnWidths(widths, 1, percent(10), percent(10)); + + expect(result).to.deep.equal([percent(30), percent(40), percent(30)]); + }); + + it('shrinks left column and grows right column when dragging left (delta < 0)', () => { + const widths = [percent(30), percent(30), percent(40)]; + + const result = calculateColumnWidths(widths, 1, percent(-10), percent(10)); + + expect(result).to.deep.equal([percent(30), percent(20), percent(50)]); + }); + + it('respects minWidth when shrinking', () => { + const widths = [percent(30), percent(20), percent(50)]; + + const result = calculateColumnWidths(widths, 0, percent(15), percent(20)); + + // right side shrinks, left side grows + expect(result).to.deep.equal([percent(45), percent(20), percent(35)]); + }); + + it('shrinks multiple columns sequentially when needed', () => { + const widths = [percent(40), percent(30), percent(30)]; + + const result = calculateColumnWidths(widths, 0, percent(25), percent(10)); + + expect(result).to.deep.equal([percent(65), percent(10), percent(25)]); + }); + + it('aborts if total available shrink space is insufficient', () => { + const widths = [percent(40), percent(15), percent(45)]; + + const result = calculateColumnWidths(widths, 0, percent(20), percent(10)); + expect(result).to.not.deep.equal(widths); + + const impossible = calculateColumnWidths( + widths, + 0, + percent(50), + percent(10) + ); + expect(impossible).to.deep.equal(widths); + }); + + it('only grows the nearest column on the growing side', () => { + const widths = [percent(20), percent(40), percent(40)]; + + const result = calculateColumnWidths(widths, 1, percent(10), percent(10)); + + expect(result).to.deep.equal([percent(20), percent(50), percent(30)]); + }); + + it('preserves total width sum', () => { + const widths = [percent(25), percent(25), percent(50)]; + + const result = calculateColumnWidths(widths, 0, percent(15), percent(10)); + + const sum = (arr: Percent[]) => arr.reduce((a, b) => a + b, 0); + + expect(sum(result)).to.equal(sum(widths)); + }); +}); From a6569c4be9f4790c1efae2588cbb424a457bb1eb Mon Sep 17 00:00:00 2001 From: bendera Date: Wed, 31 Dec 2025 15:38:10 +0100 Subject: [PATCH 16/16] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cee402e..df2badd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +## [Unreleased] + +- **Table**: The column resizing algorithm has been rewritten. Columns can now push each other. + ## [2.4.0] - 2025-12-26 ### Fixed