Skip to content

Commit b4a96de

Browse files
igauchfengtianze
authored andcommitted
feat: table support resize column width
1 parent d31a3c4 commit b4a96de

19 files changed

+447
-99
lines changed

.changeset/fifty-hounds-stare.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@alauda/ui': minor
3+
---
4+
5+
- feat: table support resize column width

angular.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
"browserTarget": "storybook:build",
7575
"compodoc": true,
7676
"compodocArgs": ["-e", "json", "-d", "."],
77-
"outputDir": "dist"
77+
"outputDir": "dist",
78+
"enableProdMode": false // FIXME: https://github.com/storybookjs/storybook/issues/23534
7879
}
7980
}
8081
}

scripts/build.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ const watch = process.argv.includes('--watch');
1111

1212
const debugNgPackage = '../ng-package.debug.json';
1313

14-
const dest = (isDebug ? require(debugNgPackage).dest : 'release') + '/theme';
14+
const releaseDest = isDebug ? require(debugNgPackage).dest : 'release';
1515

1616
function copyResources() {
17+
const themeDest = path.resolve(releaseDest, 'theme');
1718
gulp
1819
.src([
1920
'src/theme/_base-var.scss',
@@ -22,12 +23,12 @@ function copyResources() {
2223
'src/theme/_theme-preset.scss',
2324
'src/theme/_mixin.scss',
2425
])
25-
.pipe(gulp.dest(dest));
26+
.pipe(gulp.dest(themeDest));
2627

2728
gulp
2829
.src('src/theme/style.scss')
2930
.pipe(sass().on('error', sass.logError))
30-
.pipe(gulp.dest(dest));
31+
.pipe(gulp.dest(themeDest));
3132
}
3233

3334
const packagr = ngPackagr

src/table/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
export * from './table.component';
1+
export { TableComponent } from './table.component';
22
export * from './table.module';
33
export * from './table-cell.component';
44
export * from './table-cell.directive';
55
export * from './table-cell-def.directive';
66
export * from './table-column-def.directive';
7+
export * from './table-column-resizable.directive';
78
export * from './table-header-cell.directive';
89
export * from './table-header-cell-def.directive';
910
export * from './table-header-row.component';

src/table/table-cell.directive.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { CdkCell, CdkColumnDef } from '@angular/cdk/table';
22
import { Directive, ElementRef, Input } from '@angular/core';
33

4-
import { buildBem } from '../utils';
5-
6-
const bem = buildBem('aui-table');
4+
import { tableBem } from './table.component';
75

86
/** Cell template container that adds the right classes and role. */
97
@Directive({
@@ -23,7 +21,7 @@ export class TableCellDirective extends CdkCell {
2321
constructor(columnDef: CdkColumnDef, elementRef: ElementRef<HTMLElement>) {
2422
super(columnDef, elementRef);
2523
elementRef.nativeElement.classList.add(
26-
bem.element(`column-${columnDef.cssClassFriendlyName}`),
24+
tableBem.element(`column-${columnDef.cssClassFriendlyName}`),
2725
);
2826
}
2927
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import {
2+
AfterViewInit,
3+
Directive,
4+
ElementRef,
5+
inject,
6+
Input,
7+
OnDestroy,
8+
OnInit,
9+
Renderer2,
10+
} from '@angular/core';
11+
import {
12+
fromEvent,
13+
map,
14+
merge,
15+
Subscription,
16+
switchMap,
17+
take,
18+
takeUntil,
19+
} from 'rxjs';
20+
21+
import { buildBem, getCompatibleStylesRenderer } from '../utils';
22+
23+
import { TableColumnDefDirective } from './table-column-def.directive';
24+
import { tableBem, TableComponent } from './table.component';
25+
26+
let tableColumnResizableID = 0;
27+
28+
const resizableBem = buildBem('aui-table-column-resizable');
29+
const markLineWidth = 1;
30+
31+
@Directive({
32+
selector: '[auiTableColumnResizable]',
33+
standalone: true,
34+
})
35+
export class TableColumnResizableDirective
36+
implements OnInit, AfterViewInit, OnDestroy
37+
{
38+
@Input()
39+
minWidth = '40px';
40+
41+
@Input()
42+
maxWidth = '80%';
43+
44+
private readonly renderer2 = inject(Renderer2);
45+
private readonly tableColumnDefDirective = inject(TableColumnDefDirective);
46+
private readonly tableComponent = inject(TableComponent);
47+
48+
private readonly columnElement: HTMLElement =
49+
inject(ElementRef).nativeElement;
50+
51+
private readonly containerElement: HTMLElement =
52+
this.tableComponent.elementRef.nativeElement;
53+
54+
private readonly hostAttr = `table-column-resizable-${tableColumnResizableID++}`;
55+
private readonly stylesRenderer = getCompatibleStylesRenderer();
56+
private resizeSubscription: Subscription;
57+
58+
ngOnInit() {
59+
this.containerElement.setAttribute(this.hostAttr, '');
60+
}
61+
62+
ngAfterViewInit() {
63+
const resizeHandle = this.createResizeHandle();
64+
this.bindResizable(resizeHandle);
65+
}
66+
67+
ngOnDestroy() {
68+
this.resizeSubscription?.unsubscribe();
69+
this.containerElement.removeAttribute(this.hostAttr);
70+
this.stylesRenderer.cleanup();
71+
}
72+
73+
private bindResizable(resizeHandle: HTMLElement) {
74+
const mouseUp$ = fromEvent<MouseEvent>(document, 'mouseup').pipe(take(1));
75+
76+
const mouseMove$ = fromEvent<MouseEvent>(document, 'mousemove').pipe(
77+
takeUntil(mouseUp$),
78+
);
79+
80+
this.resizeSubscription = fromEvent<MouseEvent>(resizeHandle, 'mousedown')
81+
.pipe(
82+
switchMap(mouseDownEvent => {
83+
mouseDownEvent.preventDefault();
84+
mouseDownEvent.stopPropagation();
85+
86+
this.renderer2.setStyle(resizeHandle, 'visibility', 'hidden');
87+
const resizeRange = this.getResizeRange();
88+
const initialMouseX = mouseDownEvent.clientX;
89+
const columnWidth = this.getColumnWidth();
90+
const columnOffset = this.getColumnOffset();
91+
const resizeMarkLine = this.createResizeMarkLine(
92+
columnOffset + columnWidth,
93+
);
94+
const resizeOverlay = this.createResizeOverlay();
95+
96+
return merge(
97+
mouseMove$.pipe(
98+
map(
99+
mouseMoveEvent => () =>
100+
resizeMarkLine.updateOffset(
101+
columnOffset +
102+
this.getWidthInRange(
103+
resizeRange,
104+
columnWidth + mouseMoveEvent.clientX - initialMouseX,
105+
),
106+
),
107+
),
108+
),
109+
mouseUp$.pipe(
110+
map(mouseUpEvent => () => {
111+
this.renderer2.removeStyle(resizeHandle, 'visibility');
112+
resizeMarkLine.destroy();
113+
resizeOverlay.destroy();
114+
115+
this.renderWidthStyles(
116+
this.getWidthInRange(
117+
resizeRange,
118+
columnWidth + mouseUpEvent.clientX - initialMouseX,
119+
),
120+
);
121+
}),
122+
),
123+
);
124+
}),
125+
)
126+
.subscribe(exec => {
127+
exec();
128+
});
129+
}
130+
131+
private createResizeHandle() {
132+
const resizeHandle: HTMLDivElement = this.renderer2.createElement('div');
133+
this.renderer2.addClass(resizeHandle, resizableBem.element('handle'));
134+
this.renderer2.appendChild(this.columnElement, resizeHandle);
135+
136+
return resizeHandle;
137+
}
138+
139+
private createResizeMarkLine(initialOffset: number) {
140+
const markLine: HTMLElement = this.renderer2.createElement('div');
141+
this.renderer2.addClass(markLine, resizableBem.element('mark-line'));
142+
this.renderer2.setStyle(
143+
markLine,
144+
'left',
145+
initialOffset - markLineWidth + 'px',
146+
);
147+
if (this.isStickyLeftBorderColumn()) {
148+
this.renderer2.addClass(markLine, 'inStickyBorderElemLeft');
149+
}
150+
this.renderer2.appendChild(this.containerElement, markLine);
151+
return {
152+
element: markLine,
153+
updateOffset: (offset: number) => {
154+
this.renderer2.setStyle(
155+
markLine,
156+
'left',
157+
offset - markLineWidth + 'px',
158+
);
159+
},
160+
destroy: () => {
161+
this.renderer2.removeChild(this.containerElement, markLine);
162+
},
163+
};
164+
}
165+
166+
private createResizeOverlay() {
167+
const resizeOverlay = this.renderer2.createElement('div');
168+
this.renderer2.addClass(resizeOverlay, resizableBem.element('overlay'));
169+
this.renderer2.appendChild(this.containerElement, resizeOverlay);
170+
return {
171+
element: resizeOverlay,
172+
destroy: () => {
173+
this.renderer2.removeChild(this.containerElement, resizeOverlay);
174+
},
175+
};
176+
}
177+
178+
private getColumnWidth() {
179+
return this.columnElement.clientWidth;
180+
}
181+
182+
private getColumnOffset() {
183+
return (
184+
this.columnElement.getBoundingClientRect().left -
185+
this.containerElement.getBoundingClientRect().left
186+
);
187+
}
188+
189+
private getWidthInRange(
190+
[minWidth, maxWidth]: [number, number],
191+
width: number,
192+
): number {
193+
return Math.min(Math.max(width, minWidth), maxWidth);
194+
}
195+
196+
private getResizeRange(): [number, number] {
197+
const minWidth = this.getActualWidth(this.minWidth);
198+
const maxWidth = this.getActualWidth(this.maxWidth);
199+
return [minWidth, maxWidth];
200+
}
201+
202+
private getActualWidth(width: number | string): number {
203+
if (typeof width === 'number') {
204+
return width;
205+
}
206+
if (width.endsWith('%')) {
207+
return (
208+
(this.containerElement.clientWidth * parseInt(width.slice(0, -1))) / 100
209+
);
210+
}
211+
if (width.endsWith('px')) {
212+
return parseInt(width.slice(0, -2));
213+
}
214+
return parseInt(width);
215+
}
216+
217+
private isStickyLeftBorderColumn() {
218+
return this.columnElement.classList.contains(
219+
'aui-table-sticky-border-elem-left',
220+
);
221+
}
222+
223+
private renderWidthStyles(width: number) {
224+
const className = tableBem.element(
225+
`column-${this.tableColumnDefDirective.cssClassFriendlyName}`,
226+
);
227+
228+
const styleString = `[${this.hostAttr}] .${className} {
229+
flex: none !important;
230+
width: ${width}px !important;
231+
min-width: ${width}px !important;
232+
max-width: ${width}px !important;
233+
}`;
234+
235+
this.stylesRenderer.render(styleString);
236+
this.tableComponent.updateStickyColumnStyles();
237+
}
238+
}

src/table/table-header-cell.directive.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { CdkColumnDef, CdkHeaderCell } from '@angular/cdk/table';
22
import { Directive, ElementRef } from '@angular/core';
33

4-
import { buildBem } from '../utils';
5-
6-
const bem = buildBem('aui-table');
4+
import { tableBem } from './table.component';
75

86
/** Header cell template container that adds the right classes and role. */
97
@Directive({
@@ -19,7 +17,7 @@ export class TableHeaderCellDirective extends CdkHeaderCell {
1917
constructor(columnDef: CdkColumnDef, elementRef: ElementRef<HTMLElement>) {
2018
super(columnDef, elementRef);
2119
elementRef.nativeElement.classList.add(
22-
bem.element(`column-${columnDef.cssClassFriendlyName}`),
20+
tableBem.element(`column-${columnDef.cssClassFriendlyName}`),
2321
);
2422
}
2523
}

0 commit comments

Comments
 (0)