Skip to content

Commit

Permalink
feat: add resizable directive 2
Browse files Browse the repository at this point in the history
  • Loading branch information
igauch committed Jan 3, 2024
1 parent 9cb0c10 commit e30c12e
Show file tree
Hide file tree
Showing 15 changed files with 229 additions and 94 deletions.
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
}
}
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"JounQin <[email protected]> (https://www.1stG.me)"
],
"license": "MIT",
"packageManager": "[email protected]",
"keywords": [
"alauda",
"angular",
Expand Down
11 changes: 8 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,16 @@ function copyResources() {
'src/theme/_theme-preset.scss',
'src/theme/_mixin.scss',
])
.pipe(gulp.dest(dest));
.pipe(gulp.dest(themeDest));

gulp
.src(['src/resizable/resizable.scss'])
.pipe(gulp.dest(path.resolve(releaseDest + '/resizable')));

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
103 changes: 53 additions & 50 deletions src/resizable/resizable.directive.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,75 @@
import { DOCUMENT } from '@angular/common';
import {
AfterViewInit,
Directive,
ElementRef,
EventEmitter,
Inject,
inject,
Input,
OnDestroy,
Optional,
Output,
Renderer2,
} from '@angular/core';
import { fromEvent, Subject, Subscription, takeUntil } from 'rxjs';
import { fromEvent, Subject, takeUntil } from 'rxjs';

@Directive({
selector: '[auiContainerForResizable]',
host: {
class: 'aui-container-for-resizable',
},
standalone: true,
})
export class ContainerForResizableDirective {
el = inject(ElementRef);
}

/**
* 使用此指令需要引入resizable.scss
* 因为参考线是基于absolute定位的,所以需要在预期的地方加上relative(不自动加是为了避免修改业务布局而导致使用者出现意外的异常)
*/
@Directive({
selector: '[auiResizable]',
host: {
class: 'aui-resizable',
},
standalone: true,
})
export class ResizableDirective implements AfterViewInit, OnDestroy {
@Input()
containerElement: Element; // 要插入参考线的容器元素

@Input()
minWidth: string;
minWidth: string | number;

@Input()
maxWidth: string;
maxWidth: string | number;

@Output()
resizeStartEvent = new EventEmitter<number>();
resizeStart = new EventEmitter<number>();

@Output()
resizingEvent = new EventEmitter<number>();
resizing = new EventEmitter<number>();

@Output()
resizeEndEvent = new EventEmitter<number>();
resizeEnd = new EventEmitter<number>();

destroy$$ = new Subject<void>();
containerElement: Element;

private readonly element: HTMLElement;
private initialWidth: number;
private mouseDownScreenX: number;
resizeHandle: HTMLElement; // 在指令作用的元素上插入引导分割线,hover到作用元素后出现,拖动事件的宿主
resizeOverlay: HTMLElement; // 在拖动时创建出一个透明的遮罩层,用以鼠标样式在拖动时总是拖动样式
resizeBar: HTMLElement; // 拖动时的参考线,该参考线会被插入到 containerElement
private mouseUpSubscription: Subscription;
private readonly document: Document;
private handleHasUp: boolean;
private readonly BAR_WIDTH = 2;

constructor(
element: ElementRef,
readonly renderer2: Renderer2,
@Inject(DOCUMENT)
private readonly doc: Document,
@Optional()
private readonly containerDirective: ContainerForResizableDirective,
) {
this.element = element.nativeElement;
this.document = this.doc;
this.containerElement =
this.containerDirective?.el.nativeElement || this.element;
}

ngAfterViewInit() {
Expand Down Expand Up @@ -130,7 +141,7 @@ export class ResizableDirective implements AfterViewInit, OnDestroy {
this.renderer2.setStyle(
this.resizeBar,
'left',
this.element.clientWidth + this.getOffset() + 'px',
this.element.clientWidth + this.getOffset() - this.BAR_WIDTH + 'px',
);
this.renderer2.appendChild(this.containerElement, this.resizeBar);
}
Expand All @@ -143,39 +154,39 @@ export class ResizableDirective implements AfterViewInit, OnDestroy {
this.setResizeHandleVisible(false);

this.initialWidth = this.element.clientWidth;
this.resizeStartEvent.emit(this.initialWidth);
this.resizeStart.emit(this.initialWidth);
this.mouseDownScreenX = event.clientX;
this.createResizeOverlay();
this.createResizeBar();

this.mouseUpSubscription = this.binEvent<MouseEvent>(
const mouseMoveSubscription = this.binEvent(
document,
'mouseup',
ev => this.onMouseup(ev),
'mousemove',
this.move,
);

this.binEvent(document, 'mousemove', this.move);
const mouseUpSubscription = this.binEvent<MouseEvent>(
document,
'mouseup',
ev => {
this.onMouseup(ev);
mouseUpSubscription.unsubscribe();
mouseMoveSubscription.unsubscribe();
},
);
}

private onMouseup(event: MouseEvent): void {
this.handleHasUp && this.setResizeHandleVisible(true);

const movementX = event.clientX - this.mouseDownScreenX;
const newWidth = this.initialWidth + movementX;
const finalWidth = this.getFinalWidth(newWidth);

this.renderer2.removeChild(this.element, this.resizeOverlay);
this.renderer2.removeChild(this.containerElement, this.resizeOverlay);
this.resizeOverlay = null;
this.renderer2.removeChild(this.containerElement, this.resizeBar);
this.resizeBar = null;

this.resizeEndEvent.emit(finalWidth);

if (this.mouseUpSubscription && !this.mouseUpSubscription.closed) {
this._destroySubscription();
}

this.document.removeEventListener('mousemove', this.move);
const movementX = event.clientX - this.mouseDownScreenX;
const newWidth = this.initialWidth + movementX;
this.resizeEnd.emit(this.getFinalWidth(newWidth));
}

private readonly move = (event: MouseEvent) => {
Expand All @@ -189,14 +200,14 @@ export class ResizableDirective implements AfterViewInit, OnDestroy {
this.renderer2.setStyle(
this.resizeBar,
'left',
`${finalWidth + this.getOffset()}px`,
`${finalWidth + this.getOffset() - this.BAR_WIDTH}px`,
);
this.resizingEvent.emit(finalWidth);
this.resizing.emit(finalWidth);
};

private getFinalWidth(newWidth: number): number {
// 不能超出边界
const _max = this.containerElement.clientWidth - this.getOffset();
const _max = this.containerElement.clientWidth + this.getOffset();
const minWidth = this.handleWidth(this.minWidth || this.getOffset());

const maxWidth = this.handleWidth(this.maxWidth || _max);
Expand All @@ -206,29 +217,21 @@ export class ResizableDirective implements AfterViewInit, OnDestroy {
private getOffset() {
return (
this.element.getBoundingClientRect().left -
this.containerElement.getBoundingClientRect().left -
2 // 2 是resizeBar的宽度
this.containerElement.getBoundingClientRect().left
);
}

private handleWidth(width: string | number) {
if (!width) {
return;
}
if (typeof width === 'number') {
return width;
}
if (!width) {
return;
}
if (width.includes('%')) {
const tableWidth = this.containerElement.clientWidth;
return (tableWidth * Number.parseFloat(width)) / 100;
}
return Number.parseFloat(width.replace(/\D+/, ''));
}

private _destroySubscription() {
if (this.mouseUpSubscription) {
this.mouseUpSubscription.unsubscribe();
this.mouseUpSubscription = null;
}
}
}
5 changes: 5 additions & 0 deletions src/resizable/resizable.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
@import '../theme/var';

.aui-resizable,
.aui-container-for-resizable {
position: relative;
}

.resize-handle {
display: inline-block;
position: absolute;
Expand Down
8 changes: 2 additions & 6 deletions src/table/table-cell.directive.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { CdkCell, CdkColumnDef } from '@angular/cdk/table';
import { Directive, ElementRef, Input } from '@angular/core';

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

import { TABLE_PREFIX_CLASSNAME } from './table.component';

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

/** Cell template container that adds the right classes and role. */
@Directive({
Expand All @@ -25,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}`),
);
}
}
10 changes: 4 additions & 6 deletions src/table/table-col-resizable.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { AfterViewInit, Directive, inject, Input } from '@angular/core';
import { takeUntil } from 'rxjs';

import { ResizableDirective } from '../resizable';
import { buildBem, handlePixel } from '../utils';
import { handlePixel } from '../utils';

import {
TABLE_PREFIX_CLASSNAME,
tableBem,
TableColumnDefDirective,
TableComponent,
TableScrollWrapperDirective,
Expand Down Expand Up @@ -41,10 +41,8 @@ export class TableColResizableDirective
this.tableScrollWrapperDirective || this.tableComponent
).el.nativeElement;

const bem = buildBem(TABLE_PREFIX_CLASSNAME);

this.resizeEndEvent.pipe(takeUntil(this.destroy$$)).subscribe(width => {
const className = bem.element(
this.resizeEnd.pipe(takeUntil(this.destroy$$)).subscribe(width => {
const className = tableBem.element(
`column-${this.tableColumnDefDirective.cssClassFriendlyName}`,
);
if (!this.hostAttr) {
Expand Down
8 changes: 2 additions & 6 deletions src/table/table-header-cell.directive.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { CdkColumnDef, CdkHeaderCell } from '@angular/cdk/table';
import { Directive, ElementRef } from '@angular/core';

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

import { TABLE_PREFIX_CLASSNAME } from './table.component';

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

/** Header cell template container that adds the right classes and role. */
@Directive({
Expand All @@ -21,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}`),
);
}
}
21 changes: 10 additions & 11 deletions src/table/table-scroll.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@ import {
BehaviorSubject,
} from 'rxjs';

import { coerceAttrBoolean, observeResizeOn } from '../utils';
import { buildBem, coerceAttrBoolean, observeResizeOn } from '../utils';

import { TABLE_PREFIX_CLASSNAME, TableComponent } from './table.component';
import { tableBem, TableComponent } from './table.component';

const SHADOW_CLASS = `${TABLE_PREFIX_CLASSNAME}__scroll-shadow`;
const HAS_SCROLL_CLASS = `${SHADOW_CLASS}--has-scroll`;
const SCROLLING_CLASS = `${SHADOW_CLASS}--scrolling`;
const SCROLL_BEFORE_END_CLASS = `${SHADOW_CLASS}--before-end`;
const shadowClass = tableBem.element('scroll-shadow');
const shadowBem = buildBem(shadowClass);
const scrollBeforeEndClass = shadowBem.modifier('before-end');

const HAS_TABLE_TOP_SHADOW = 'hasTableTopShadow';
const HAS_TABLE_BOTTOM_SHADOW = 'hasTableBottomShadow';
Expand Down Expand Up @@ -88,10 +87,10 @@ export class TableScrollableDirective
super(el, scrollDispatcher, ngZone, dir);
}

@HostBinding(`class.${SCROLL_BEFORE_END_CLASS}`)
@HostBinding(`class.${scrollBeforeEndClass}`)
SCROLL_BEFORE_END_CLASS = true;

@HostBinding(`class.${SHADOW_CLASS}`)
@HostBinding(`class.${shadowClass}`)
SHADOW_CLASS = true;

get containerEl() {
Expand Down Expand Up @@ -170,19 +169,19 @@ export class TableScrollableDirective
this.placeClassList(
this.containerEl.classList,
scrollDis > 0,
HAS_SCROLL_CLASS,
shadowBem.modifier('has-scroll'),
);

const scrollLeft = this.containerEl.scrollLeft;
this.placeClassList(
this.containerEl.classList,
scrollLeft > 0,
SCROLLING_CLASS,
shadowBem.modifier('scrolling'),
);
this.placeClassList(
this.containerEl.classList,
scrollLeft < scrollDis,
SCROLL_BEFORE_END_CLASS,
scrollBeforeEndClass,
);
}

Expand Down
Loading

0 comments on commit e30c12e

Please sign in to comment.