Skip to content

Commit

Permalink
feat: table support resize column width
Browse files Browse the repository at this point in the history
  • Loading branch information
igauch authored and fengtianze committed Jan 18, 2024
1 parent d31a3c4 commit ce26bce
Show file tree
Hide file tree
Showing 18 changed files with 437 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-hounds-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@alauda/ui': minor
---

- feat: table support resize column width
3 changes: 2 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
"browserTarget": "storybook:build",
"compodoc": true,
"compodocArgs": ["-e", "json", "-d", "."],
"outputDir": "dist"
"outputDir": "dist",
"enableProdMode": false // FIXME: https://github.com/storybookjs/storybook/issues/23534
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ const watch = process.argv.includes('--watch');

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

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

function copyResources() {
const themeDest = path.resolve(releaseDest, 'theme');
gulp
.src([
'src/theme/_base-var.scss',
Expand All @@ -22,12 +23,12 @@ function copyResources() {
'src/theme/_theme-preset.scss',
'src/theme/_mixin.scss',
])
.pipe(gulp.dest(dest));
.pipe(gulp.dest(themeDest));

gulp
.src('src/theme/style.scss')
.pipe(sass().on('error', sass.logError))
.pipe(gulp.dest(dest));
.pipe(gulp.dest(themeDest));
}

const packagr = ngPackagr
Expand Down
3 changes: 2 additions & 1 deletion src/table/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export * from './table.component';
export { TableComponent } from './table.component';
export * from './table.module';
export * from './table-cell.component';
export * from './table-cell.directive';
export * from './table-cell-def.directive';
export * from './table-column-def.directive';
export * from './table-column-resizable.directive';
export * from './table-header-cell.directive';
export * from './table-header-cell-def.directive';
export * from './table-header-row.component';
Expand Down
6 changes: 2 additions & 4 deletions src/table/table-cell.directive.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { CdkCell, CdkColumnDef } from '@angular/cdk/table';
import { Directive, ElementRef, Input } from '@angular/core';

import { buildBem } from '../utils';

const bem = buildBem('aui-table');
import { tableBem } from './table.component';

/** Cell template container that adds the right classes and role. */
@Directive({
Expand All @@ -23,7 +21,7 @@ export class TableCellDirective extends CdkCell {
constructor(columnDef: CdkColumnDef, elementRef: ElementRef<HTMLElement>) {
super(columnDef, elementRef);
elementRef.nativeElement.classList.add(
bem.element(`column-${columnDef.cssClassFriendlyName}`),
tableBem.element(`column-${columnDef.cssClassFriendlyName}`),
);
}
}
239 changes: 239 additions & 0 deletions src/table/table-column-resizable.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import {
AfterViewInit,
Directive,
ElementRef,
inject,
Input,
OnDestroy,
OnInit,
Renderer2,
} from '@angular/core';
import {
fromEvent,
map,
merge,
Subscription,
switchMap,
take,
takeUntil,
} from 'rxjs';

import { buildBem, getCompatibleStylesRenderer } from '../utils';

import { TableColumnDefDirective } from './table-column-def.directive';
import { tableBem, TableComponent } from './table.component';

let tableColumnResizableID = 0;

const resizableBem = buildBem('aui-table-column-resizable');
const markLineWidth = 1;

@Directive({
selector: '[auiTableColumnResizable]',
standalone: true,
})
export class TableColumnResizableDirective
implements OnInit, AfterViewInit, OnDestroy
{
@Input()
minWidth = '40px';

@Input()
maxWidth = '80%';

private readonly renderer2 = inject(Renderer2);
private readonly tableColumnDefDirective = inject(TableColumnDefDirective);
private readonly tableComponent = inject(TableComponent);

private readonly columnElement: HTMLElement =
inject(ElementRef).nativeElement;

private readonly containerElement: HTMLElement =
this.tableComponent.elementRef.nativeElement;

private readonly hostAttr = `table-column-resizable-${tableColumnResizableID++}`;
private readonly stylesRenderer = getCompatibleStylesRenderer();
private resizeSubscription: Subscription;

ngOnInit() {
this.containerElement.setAttribute(this.hostAttr, '');
}

ngAfterViewInit() {
const resizeHandle = this.createResizeHandle();
this.bindResizable(resizeHandle);
}

ngOnDestroy() {
this.resizeSubscription?.unsubscribe();
this.containerElement.removeAttribute(this.hostAttr);
this.stylesRenderer.cleanup();
}

private bindResizable(resizeHandle: HTMLElement) {
this.resizeSubscription = fromEvent<MouseEvent>(resizeHandle, 'mousedown')
.pipe(
switchMap(mouseDownEvent => {
mouseDownEvent.preventDefault();
mouseDownEvent.stopPropagation();

this.renderer2.setStyle(resizeHandle, 'visibility', 'hidden');
const resizeRange = this.getResizeRange();
const initialMouseX = mouseDownEvent.clientX;
const columnWidth = this.getColumnWidth();
const columnOffset = this.getColumnOffset();
const resizeMarkLine = this.createResizeMarkLine(
columnOffset + columnWidth,
);
const resizeOverlay = this.createResizeOverlay();

const mouseUp$ = fromEvent<MouseEvent>(document, 'mouseup').pipe(
take(1),
);
const mouseMove$ = fromEvent<MouseEvent>(document, 'mousemove').pipe(
takeUntil(mouseUp$),
);

return merge(
mouseMove$.pipe(
map(
mouseMoveEvent => () =>
resizeMarkLine.updateOffset(
columnOffset +
this.getWidthInRange(
resizeRange,
columnWidth + mouseMoveEvent.clientX - initialMouseX,
),
),
),
),
mouseUp$.pipe(
map(mouseUpEvent => () => {
this.renderer2.removeStyle(resizeHandle, 'visibility');
resizeMarkLine.destroy();
resizeOverlay.destroy();

this.renderWidthStyles(
this.getWidthInRange(
resizeRange,
columnWidth + mouseUpEvent.clientX - initialMouseX,
),
);
}),
),
);
}),
)
.subscribe(exec => {
exec();
});
}

private createResizeHandle() {
const resizeHandle: HTMLDivElement = this.renderer2.createElement('div');
this.renderer2.addClass(resizeHandle, resizableBem.element('handle'));
this.renderer2.appendChild(this.columnElement, resizeHandle);

return resizeHandle;
}

private createResizeMarkLine(initialOffset: number) {
const markLine: HTMLElement = this.renderer2.createElement('div');
this.renderer2.addClass(markLine, resizableBem.element('mark-line'));
this.renderer2.setStyle(
markLine,
'left',
initialOffset - markLineWidth + 'px',
);
if (this.isStickyLeftBorderColumn()) {
this.renderer2.addClass(markLine, 'inStickyBorderElemLeft');
}
this.renderer2.appendChild(this.containerElement, markLine);
return {
element: markLine,
updateOffset: (offset: number) => {
this.renderer2.setStyle(
markLine,
'left',
offset - markLineWidth + 'px',
);
},
destroy: () => {
this.renderer2.removeChild(this.containerElement, markLine);
},
};
}

private createResizeOverlay() {
const resizeOverlay = this.renderer2.createElement('div');
this.renderer2.addClass(resizeOverlay, resizableBem.element('overlay'));
this.renderer2.appendChild(this.containerElement, resizeOverlay);
return {
element: resizeOverlay,
destroy: () => {
this.renderer2.removeChild(this.containerElement, resizeOverlay);
},
};
}

private getColumnWidth() {
return this.columnElement.clientWidth;
}

private getColumnOffset() {
return (
this.columnElement.getBoundingClientRect().left -
this.containerElement.getBoundingClientRect().left
);
}

private getWidthInRange(
[minWidth, maxWidth]: [number, number],
width: number,
): number {
return Math.min(Math.max(width, minWidth), maxWidth);
}

private getResizeRange(): [number, number] {
const minWidth = this.getActualWidth(this.minWidth);
const maxWidth = this.getActualWidth(this.maxWidth);
return [minWidth, maxWidth];
}

private getActualWidth(width: number | string): number {
if (typeof width === 'number') {
return width;
}
if (width.endsWith('%')) {
return (
(this.containerElement.clientWidth * parseInt(width.slice(0, -1))) / 100
);
}
if (width.endsWith('px')) {
return parseInt(width.slice(0, -2));
}
return parseInt(width);
}

private isStickyLeftBorderColumn() {
return this.columnElement.classList.contains(
'aui-table-sticky-border-elem-left',
);
}

private renderWidthStyles(width: number) {
const className = tableBem.element(
`column-${this.tableColumnDefDirective.cssClassFriendlyName}`,
);

const styleString = `[${this.hostAttr}] .${className} {
flex: none !important;
width: ${width}px !important;
min-width: ${width}px !important;
max-width: ${width}px !important;
}`;

this.stylesRenderer.render(styleString);
this.tableComponent.updateStickyColumnStyles();
}
}
6 changes: 2 additions & 4 deletions src/table/table-header-cell.directive.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { CdkColumnDef, CdkHeaderCell } from '@angular/cdk/table';
import { Directive, ElementRef } from '@angular/core';

import { buildBem } from '../utils';

const bem = buildBem('aui-table');
import { tableBem } from './table.component';

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

0 comments on commit ce26bce

Please sign in to comment.