Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions dev/vscode-table/shift-table-columns.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,21 @@ <h1>Basic example</h1>
style="margin-left: 200px"
>
<vscode-table-header slot="header">
<vscode-table-header-cell>id</vscode-table-header-cell>
<vscode-table-header-cell>firstname</vscode-table-header-cell>
<vscode-table-header-cell>lastname</vscode-table-header-cell>
<vscode-table-header-cell>email</vscode-table-header-cell>
<vscode-table-header-cell>company</vscode-table-header-cell>
<vscode-table-header-cell min-width="30"
>Id</vscode-table-header-cell
>
<vscode-table-header-cell min-width="70"
>First name</vscode-table-header-cell
>
<vscode-table-header-cell min-width="70"
>Last name</vscode-table-header-cell
>
<vscode-table-header-cell min-width="70"
>Email</vscode-table-header-cell
>
<vscode-table-header-cell min-width="70"
>Company</vscode-table-header-cell
>
</vscode-table-header>
<vscode-table-body slot="body">
<vscode-table-row>
Expand Down
87 changes: 87 additions & 0 deletions src/includes/sizes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
import {expect} from '@open-wc/testing';
import {parseSizeAttributeToPercent} from './sizes.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;
});
});
49 changes: 49 additions & 0 deletions src/includes/sizes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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);

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
): Percent | null => {
if (!Number.isFinite(base) || base === 0) {
return null;
}

if (typeof raw === 'number') {
return Number.isFinite(raw) ? percent((raw / base) * 100) : null;
}

const value = raw.trim();
const parser = parsers.find((p) => p.test(value));

return parser ? percent(parser.parse(value, base)) : null;
};
30 changes: 29 additions & 1 deletion src/vscode-table-header-cell/vscode-table-header-cell.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import {html, TemplateResult} from 'lit';
import {html, PropertyValues, TemplateResult} from 'lit';
import {property} from 'lit/decorators.js';
import {customElement, VscElement} from '../includes/VscElement.js';
import styles from './vscode-table-header-cell.styles.js';

export type VscTableChangeMinColumnWidthEvent = CustomEvent<{
columnIndex: number;
propertyValue: string;
}>;

/**
* @tag vscode-table-header-cell
*
Expand All @@ -14,10 +19,29 @@ import styles from './vscode-table-header-cell.styles.js';
export class VscodeTableHeaderCell extends VscElement {
static override styles = styles;

@property({attribute: 'min-width'})
minWidth = '0';

/** @internal */
@property({type: Number})
index = -1;

/** @internal */
@property({reflect: true})
override role = 'columnheader';

protected override willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has('minWidth') && this.index > -1) {
/** @internal */
this.dispatchEvent(
new CustomEvent('vsc-table-change-min-column-width', {
detail: {columnIndex: this.index, propertyValue: this.minWidth},
bubbles: true,
}) as VscTableChangeMinColumnWidthEvent
);
}
}

override render(): TemplateResult {
return html`
<div class="wrapper">
Expand All @@ -31,4 +55,8 @@ declare global {
interface HTMLElementTagNameMap {
'vscode-table-header-cell': VscodeTableHeaderCell;
}

interface GlobalEventHandlersEventMap {
'vsc-table-change-min-column-width': VscTableChangeMinColumnWidthEvent;
}
}
24 changes: 11 additions & 13 deletions src/vscode-table/ColumnResizeController.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import {ReactiveController} from 'lit';
import {type VscodeTable} from './vscode-table.js';
import {
calculateColumnWidths,
Percent,
percent,
Px,
px,
toPercent,
toPx,
} from './calculations.js';
import {Percent, percent, Px, px, toPercent, toPx} from '../includes/sizes.js';
import {calculateColumnWidths} from './calculations.js';

type SplitterElement = HTMLDivElement & {
dataset: DOMStringMap & {
Expand All @@ -21,7 +14,7 @@ export class ColumnResizeController implements ReactiveController {
private _hostWidth = px(0);
private _hostX = px(0);
private _activeSplitter: SplitterElement | null = null;
private _minColumnWidth = percent(0);
private _columnMinWidths = new Map<number, Percent>();
private _columnWidths: Percent[] = [];
private _dragState: {
splitterIndex: number;
Expand Down Expand Up @@ -79,6 +72,10 @@ export class ColumnResizeController implements ReactiveController {
return this._columnWidths;
}

get columnMinWidths() {
return new Map(this._columnMinWidths);
}

saveHostDimensions() {
const cr = this._host.getBoundingClientRect();
const {width, x} = cr;
Expand All @@ -96,8 +93,9 @@ export class ColumnResizeController implements ReactiveController {
return this._activeSplitter;
}

setMinColumnWidth(width: Percent) {
this._minColumnWidth = width;
setColumnMinWidthAt(colIndex: number, value: Percent) {
this._columnMinWidths.set(colIndex, value);
this._host.requestUpdate();
return this;
}

Expand Down Expand Up @@ -181,7 +179,7 @@ export class ColumnResizeController implements ReactiveController {
this._columnWidths,
this._dragState.splitterIndex,
delta,
this._minColumnWidth
this._columnMinWidths
);
this._cachedSplitterPositions = null;

Expand Down
Loading