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 1, 2024
1 parent 9cb0c10 commit 53e74ec
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 26 deletions.
1 change: 1 addition & 0 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function copyResources() {
'src/theme/_var.scss',
'src/theme/_theme-preset.scss',
'src/theme/_mixin.scss',
'src/resizable/resizable.scss',
])
.pipe(gulp.dest(dest));

Expand Down
65 changes: 40 additions & 25 deletions src/resizable/resizable.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,55 @@ import {
Directive,
ElementRef,
EventEmitter,
inject,
Inject,
Input,
OnDestroy,
Optional,
Output,
Renderer2,
} from '@angular/core';
import { fromEvent, Subject, Subscription, 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; // 要插入参考线的容器元素
minWidth: string | number;

@Input()
minWidth: string;

@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;
Expand All @@ -50,15 +63,20 @@ export class ResizableDirective implements AfterViewInit, OnDestroy {
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 +148,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,7 +161,7 @@ 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();
Expand All @@ -160,16 +178,14 @@ export class ResizableDirective implements AfterViewInit, OnDestroy {
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);
const movementX = event.clientX - this.mouseDownScreenX;
const newWidth = this.initialWidth + movementX;
this.resizeEnd.emit(this.getFinalWidth(newWidth));

if (this.mouseUpSubscription && !this.mouseUpSubscription.closed) {
this._destroySubscription();
Expand All @@ -189,14 +205,14 @@ export class ResizableDirective implements AfterViewInit, OnDestroy {
this.renderer2.setStyle(
this.resizeBar,
'left',
`${finalWidth + this.getOffset()}px`,
`${finalWidth - 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,18 +222,17 @@ 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;
Expand Down
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
2 changes: 1 addition & 1 deletion src/table/table-col-resizable.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class TableColResizableDirective

const bem = buildBem(TABLE_PREFIX_CLASSNAME);

this.resizeEndEvent.pipe(takeUntil(this.destroy$$)).subscribe(width => {
this.resizeEnd.pipe(takeUntil(this.destroy$$)).subscribe(width => {
const className = bem.element(
`column-${this.tableColumnDefDirective.cssClassFriendlyName}`,
);
Expand Down
77 changes: 77 additions & 0 deletions stories/resizable/basic.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
QueryList,
ViewChildren,
} from '@angular/core';

import { ContainerForResizableDirective } from '../../src/resizable/resizable.directive';

import { ResizableDirective } from '@alauda/ui';

@Component({
template: `
默认拖动容器为指令的宿主元素
<div style="display: flex; background: #ccc">
<div
#resizable
style="width: 100px; height: 100px; background: red;"
auiResizable
(resizeEnd)="resizeEndHandle($event, 0)"
></div>
<div style="width: 100px; height: 100px; background: yellow"></div>
</div>
但也可以通过 auiContainerForResizable 指定容器,例如在灰色 div 上加这个指令
<div
style="display: flex; background: #ccc;"
auiContainerForResizable
>
<div
#resizable
style="width: 100px; height: 100px; background: red;"
auiResizable
(resizeEnd)="resizeEndHandle($event, 1)"
></div>
<div style="width: 100px; height: 100px; background: yellow"></div>
</div>
你会发现你可以在更大的范围内进行拖动,因为 container
变大了,默认可拖动区域是限定在 container 里的。
如果你想自定义拖动区域,可以通过设定 minWidth 和 maxWidth
完成,除了支持常规的 number 也就是 XXpx 这种,也支持
XX%,百分比是相对于容器宽度的,例如下面设定最小60,最大80%
<div
style="display: flex; background: #ccc;"
auiContainerForResizable
>
<div
#resizable
style="width: 100px; height: 100px; background: red;"
auiResizable
(resizeEnd)="resizeEndHandle($event, 2)"
[minWidth]="60"
maxWidth="80%"
></div>
<div style="width: 100px; height: 100px; background: yellow"></div>
</div>
是的,就这么简单,你要考虑的只有——怎么处理 resizeEnd 发出的拖动后的宽度。
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ResizableDirective, ContainerForResizableDirective],
styles: [
`
@import '../../src/resizable/resizable';
`,
],
standalone: true,
})
export default class BasicComponent {
@ViewChildren('resizable')
resizable: QueryList<ElementRef>;

resizeEndHandle(width: number, idx: number) {
this.resizable.get(idx).nativeElement.style.width = `${width}px`;
}
}
15 changes: 15 additions & 0 deletions stories/resizable/basic.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Meta, StoryObj } from '@storybook/angular';

import BasicComponent from './basic.component';

const meta: Meta<BasicComponent> = {
title: 'Example/Resizable',
component: BasicComponent,
};

export default meta;
type Story = StoryObj<BasicComponent>;

export const Basic: Story = {
name: 'Basic',
};
37 changes: 37 additions & 0 deletions stories/resizable/resizable.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Canvas, Meta } from '@storybook/blocks';

import * as basic from './basic.stories';

<Meta title="Example/Resizable" />

# Resizable

Resizable 指令提供了一种通过左右拖动来调整宽度的能力

## 使用

需要在使用时手动引入 resizable.scss

```
@import 'node_modules/@alauda/ui/resizable/resizable';
```

<Canvas
of={basic.Basic}
meta={basic}
/>

## Inputs

| 名称 | 类型 | 默认值 | 描述 |
| -------- | ---------------- | ------ | --------------------------------------------- |
| minWidth | string \| number | - | 限定拖动后的最小宽度,为%时以容器的宽度为基数 |
| maxWidth | string \| number | - | 限定拖动后的最大宽度,为%时以容器的宽度为基数 |

## Outputs

| 名称 | 回调参数 | 描述 |
| ----------- | ------------- | ---------------------- |
| resizeStart | width: number | 开始调整宽度事件 |
| resizing | width: number | 正在进行宽度调整的事件 |
| resizeEnd | width: number | 调整宽度结束事件 |

0 comments on commit 53e74ec

Please sign in to comment.