diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index aa562a9b4..749865761 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/CHANGELOG.md b/CHANGELOG.md
index 7cee402ee..df2badd77 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
diff --git a/dev/vscode-table/shift-table-columns.html b/dev/vscode-table/shift-table-columns.html
new file mode 100644
index 000000000..998b29384
--- /dev/null
+++ b/dev/vscode-table/shift-table-columns.html
@@ -0,0 +1,96 @@
+
+
+
+
+
+ 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/eslint.config.mjs b/eslint.config.mjs
index a3106bae9..8fcb28387 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',
diff --git a/src/vscode-table/ColumnResizeController.ts b/src/vscode-table/ColumnResizeController.ts
new file mode 100644
index 000000000..e8d75af38
--- /dev/null
+++ b/src/vscode-table/ColumnResizeController.ts
@@ -0,0 +1,218 @@
+import {ReactiveController} from 'lit';
+import {type VscodeTable} from './vscode-table.js';
+import {
+ calculateColumnWidths,
+ Percent,
+ percent,
+ Px,
+ px,
+ toPercent,
+ toPx,
+} from './calculations.js';
+
+type SplitterElement = HTMLDivElement & {
+ dataset: DOMStringMap & {
+ index: string;
+ };
+};
+
+export class ColumnResizeController implements ReactiveController {
+ private _host: VscodeTable;
+ private _hostWidth = px(0);
+ private _hostX = px(0);
+ private _activeSplitter: SplitterElement | null = null;
+ private _minColumnWidth = percent(0);
+ private _columnWidths: Percent[] = [];
+ private _dragState: {
+ splitterIndex: number;
+ pointerId: number;
+ prevX: Px;
+ dragOffset: Px;
+ } | null = null;
+ private _cachedSplitterPositions: Percent[] | null = null;
+
+ constructor(host: VscodeTable) {
+ (this._host = host).addController(this);
+ }
+
+ hostConnected(): void {
+ this.saveHostDimensions();
+ }
+
+ get isDragging(): boolean {
+ return this._dragState !== null;
+ }
+
+ get splitterPositions(): Percent[] {
+ if (this._cachedSplitterPositions) {
+ return this._cachedSplitterPositions;
+ }
+
+ 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);
+ }
+
+ this._cachedSplitterPositions = result;
+
+ 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;
+ }
+
+ saveHostDimensions() {
+ const cr = this._host.getBoundingClientRect();
+ const {width, x} = cr;
+ this._hostWidth = px(width);
+ this._hostX = px(x);
+ return this;
+ }
+
+ setActiveSplitter(splitter: HTMLElement) {
+ this._activeSplitter = splitter as SplitterElement;
+ return this;
+ }
+
+ getActiveSplitter() {
+ return this._activeSplitter;
+ }
+
+ setMinColumnWidth(width: Percent) {
+ this._minColumnWidth = width;
+ return this;
+ }
+
+ setColumWidths(widths: Percent[]) {
+ this._columnWidths = widths;
+ this._cachedSplitterPositions = null;
+ this._host.requestUpdate();
+ 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 splitter = event.currentTarget as SplitterElement;
+ const splitterX = splitter!.getBoundingClientRect().x;
+ const xOffset = px(mouseX - splitterX);
+
+ this._dragState = {
+ dragOffset: px(xOffset),
+ pointerId: event.pointerId,
+ splitterIndex: +splitter.dataset.index,
+ prevX: px(mouseX - xOffset),
+ };
+
+ this._host.requestUpdate();
+ }
+
+ drag(event: PointerEvent) {
+ event.stopPropagation();
+
+ if (
+ !(event?.currentTarget as SplitterElement)?.hasPointerCapture?.(
+ event.pointerId
+ )
+ ) {
+ return;
+ }
+
+ if (!this._dragState) {
+ return;
+ }
+
+ if (event.pointerId !== this._dragState.pointerId) {
+ return;
+ }
+
+ if (!this.shouldDrag(event)) {
+ return;
+ }
+
+ const mouseX = event.pageX;
+ const x = px(mouseX - this._dragState.dragOffset);
+ const deltaPx = px(x - this._dragState.prevX);
+ const delta = this._toPercent(deltaPx);
+ this._dragState.prevX = x;
+
+ 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,
+ delta,
+ this._minColumnWidth
+ );
+ this._cachedSplitterPositions = null;
+
+ this._host.requestUpdate();
+ }
+
+ 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/calculations.test.ts b/src/vscode-table/calculations.test.ts
new file mode 100644
index 000000000..2e4a51f23
--- /dev/null
+++ b/src/vscode-table/calculations.test.ts
@@ -0,0 +1,179 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions */
+import {expect} from '@open-wc/testing';
+import {
+ calculateColumnWidths,
+ parseSizeAttributeToPercent,
+ Percent,
+ percent,
+} from './calculations.js';
+
+describe('parseSizeAttributeToPercent', () => {
+ 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('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;
+ });
+
+ // 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('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;
+ });
+
+ // 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;
+ });
+});
+
+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));
+ });
+});
diff --git a/src/vscode-table/calculations.ts b/src/vscode-table/calculations.ts
new file mode 100644
index 000000000..d47d97a7d
--- /dev/null
+++ b/src/vscode-table/calculations.ts
@@ -0,0 +1,123 @@
+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;
+}
+
+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 (!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;
+};
diff --git a/src/vscode-table/helpers.test.ts b/src/vscode-table/helpers.test.ts
deleted file mode 100644
index ffcee9787..000000000
--- a/src/vscode-table/helpers.test.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import {expect} from '@open-wc/testing';
-import {rawValueToPercentage} from './helpers.js';
-
-describe('vscode-table helpers', () => {
- it('input type is number', () => {
- expect(rawValueToPercentage(50, 200)).to.eq(25);
- expect(rawValueToPercentage(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);
- });
-
- it('input type is percentage', () => {
- expect(rawValueToPercentage('50%', 200)).to.eq(50);
- expect(rawValueToPercentage('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);
- });
-
- it('input type is invalid value', () => {
- expect(rawValueToPercentage('-50%', 200)).to.eq(null);
- expect(rawValueToPercentage('auto', 200)).to.eq(null);
- });
-});
diff --git a/src/vscode-table/helpers.ts b/src/vscode-table/helpers.ts
deleted file mode 100644
index c125fe897..000000000
--- 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 19aafe422..5f43752ab 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 = 5;
+export const SPLITTER_VISIBLE_WIDTH = 1;
+
const styles: CSSResultGroup = [
baseStyles,
css`
@@ -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,10 @@ const styles: CSSResultGroup = [
}
.sash .sash-clickable {
- background-color: transparent;
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 2ff07ca14..39a1d3582 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,
@@ -15,10 +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 {parseSizeAttributeToPercent} from './calculations.js';
import styles from './vscode-table.styles.js';
-
-const COMPONENT_WIDTH_PERCENTAGE = 100;
+import {ColumnResizeController} from './ColumnResizeController.js';
+import {percent} from './calculations.js';
/**
* @tag vscode-table
@@ -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,11 +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);
+
override connectedCallback(): void {
super.connectedCallback();
@@ -207,12 +202,13 @@ export class VscodeTable extends VscElement {
this._bodyResizeObserver?.disconnect();
}
- private _px2Percent(px: number) {
- return (px / this._componentW) * 100;
- }
-
- private _percent2Px(percent: number) {
- return (this._componentW * percent) / 100;
+ protected override willUpdate(changedProperties: PropertyValues): void {
+ if (changedProperties.has('minColumnWidth')) {
+ const value = percent(
+ parseSizeAttributeToPercent(this.minColumnWidth, this._componentW) ?? 0
+ );
+ this._columnResizeController.setMinColumnWidth(value);
+ }
}
private _memoizeComponentDimensions() {
@@ -220,7 +216,6 @@ export class VscodeTable extends VscElement {
this._componentH = cr.height;
this._componentW = cr.width;
- this._componentX = cr.x;
}
private _queryHeaderCells() {
@@ -325,7 +320,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 =
@@ -333,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';
@@ -389,7 +384,10 @@ export class VscodeTable extends VscElement {
}
private _initDefaultColumnSizes() {
- const colWidths = this._calcColWidthPercentages();
+ const colWidths = this._calculateInitialColumnWidths();
+ this._columnResizeController.setColumWidths(
+ colWidths.map((c) => percent(c))
+ );
this._initHeaderCellSizes(colWidths);
this._initBodyColumnSizes(colWidths);
@@ -454,6 +452,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') {
@@ -513,115 +537,45 @@ export class VscodeTable extends VscElement {
this.requestUpdate();
}
- private _onSashMouseDown(event: MouseEvent) {
- event.stopPropagation();
-
- 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);
+ private _resizeColumns(resizeBodyCells = true) {
+ const widths = this._columnResizeController.columnWidths;
const headerCells = this._getHeaderCells();
- this._headerCellsToResize = [];
- this._headerCellsToResize.push(headerCells[index]);
+ headerCells.forEach((h, i) => (h.style.width = `${widths[i]}%`));
- if (headerCells[index + 1]) {
- this._headerCellsToResize[1] = headerCells[index + 1];
+ if (resizeBodyCells) {
+ const firstRowCells = this._getCellsOfFirstRow();
+ firstRowCells.forEach((c, i) => (c.style.width = `${widths[i]}%`));
}
+ }
- 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]);
+ private _handleSplitterPointerDown(event: PointerEvent) {
+ event.stopPropagation();
- if (cells[index + 1]) {
- this._cellsToResize.push(cells[index + 1]);
- }
+ const activeSplitter = event.currentTarget as HTMLElement;
- document.addEventListener('mousemove', this._onResizingMouseMove);
- document.addEventListener('mouseup', this._onResizingMouseUp);
- }
+ this._columnResizeController
+ .saveHostDimensions()
+ .setActiveSplitter(activeSplitter)
+ .startDrag(event);
- private _updateActiveSashPosition(mouseX: number) {
- const {prevSashPos, nextSashPos} = this._getSashPositions();
- let minColumnWidth = rawValueToPercentage(
- this.minColumnWidth,
- this._componentW
+ activeSplitter.addEventListener(
+ 'pointermove',
+ this._handleSplitterPointerMove
);
-
- 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)
+ activeSplitter.addEventListener('pointerup', this._handleSplitterPointerUp);
+ activeSplitter.addEventListener(
+ 'pointercancel',
+ this._handleSplitterPointerCancel
);
-
- 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,
- };
}
- 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;
- }
+ private _handleSplitterPointerMove = (event: PointerEvent) => {
+ if (!this._columnResizeController.shouldDrag(event)) {
+ return;
}
- }
-
- private _onResizingMouseMove = (event: MouseEvent) => {
- event.stopPropagation();
- this._updateActiveSashPosition(event.pageX);
+ this._columnResizeController.drag(event);
if (!this.delayedResizing) {
this._resizeColumns(true);
} else {
@@ -629,19 +583,18 @@ export class VscodeTable extends VscElement {
}
};
- private _onResizingMouseUp = (event: MouseEvent) => {
- this._resizeColumns(true);
- this._updateActiveSashPosition(event.pageX);
- this._sashHovers[this._activeSashElementIndex] = false;
- this._isDragging = false;
- this._activeSashElementIndex = -1;
+ private _handleSplitterPointerUp = (event: PointerEvent) => {
+ this._stopDrag(event);
+ };
- document.removeEventListener('mousemove', this._onResizingMouseMove);
- document.removeEventListener('mouseup', this._onResizingMouseUp);
+ private _handleSplitterPointerCancel = (event: PointerEvent) => {
+ this._stopDrag(event);
};
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],
@@ -656,7 +609,7 @@ export class VscodeTable extends VscElement {
class=${classes}
data-index=${index}
.style=${stylePropertyMap({left})}
- @mousedown=${this._onSashMouseDown}
+ @pointerdown=${this._handleSplitterPointerDown}
@mouseover=${this._onSashMouseOver}
@mouseout=${this._onSashMouseOut}
>
@@ -675,8 +628,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,
});