|
initialLeftRatio
|
Type : number
|
Default value : 2
|
|
|
|
initialRightRatio
|
Type : number
|
Default value : 1
|
|
|
|
maxWidthPercent
|
Type : number
|
Default value : 80
|
|
|
|
minWidthPercent
|
Type : number
|
Default value : 20
|
|
|
|
resizeHandle
|
Type : unknown
|
Default value : viewChild<ElementRef<HTMLElement>>('resizeHandle')
|
|
|
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
DestroyRef,
ElementRef,
inject,
input,
linkedSignal,
Renderer2,
viewChild
} from '@angular/core';
import { fromEvent } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'taly-resizable-panels',
templateUrl: './resizable-panels.component.html',
styleUrl: './resizable-panels.component.scss',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ResizablePanelsComponent implements AfterViewInit {
resizeHandle = viewChild<ElementRef<HTMLElement>>('resizeHandle');
initialLeftRatio = input<number>(2); // Default: 2fr (66.6%)
initialRightRatio = input<number>(1); // Default: 1fr (33.3%)
minWidthPercent = input<number>(20);
maxWidthPercent = input<number>(80);
protected leftRatio = linkedSignal(this.initialLeftRatio);
protected rightRatio = linkedSignal(this.initialRightRatio);
private renderer = inject(Renderer2);
private elementRef = inject(ElementRef<HTMLElement>);
private destroyRef = inject(DestroyRef);
private isDragging = false;
private startX = 0;
private startLeftWidth = 0;
private containerWidth = 0;
ngAfterViewInit() {
this.setupEventListeners();
}
private setupEventListeners() {
const handle = this.resizeHandle();
if (!handle) return;
const handleElement = handle.nativeElement;
fromEvent<MouseEvent>(handleElement, 'mousedown')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((event) => this.onDragStart(event));
fromEvent<MouseEvent>(document, 'mousemove')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((event) => this.onDragMove(event));
fromEvent<MouseEvent>(document, 'mouseup')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.onDragEnd());
}
private onDragStart(event: MouseEvent) {
this.startDrag(event.clientX);
event.preventDefault();
}
private startDrag(clientX: number) {
this.isDragging = true;
this.startX = clientX;
const container = this.elementRef.nativeElement;
const leftPanel = container.querySelector('.left-panel') as HTMLElement;
this.containerWidth = container.clientWidth;
this.startLeftWidth = (leftPanel.offsetWidth / this.containerWidth) * 100;
this.renderer.addClass(document.body, 'resizing');
}
private onDragMove(event: MouseEvent) {
if (!this.isDragging) return;
this.updateWidth(event.clientX);
}
private updateWidth(clientX: number) {
const deltaX = clientX - this.startX;
const deltaPercent = (deltaX / this.containerWidth) * 100;
let newLeftWidth = this.startLeftWidth + deltaPercent;
// Apply constraints
newLeftWidth = Math.max(this.minWidthPercent(), Math.min(this.maxWidthPercent(), newLeftWidth));
// Convert to fr units (assuming total of 3fr)
const totalRatio = this.initialLeftRatio() + this.initialRightRatio();
this.leftRatio.set((newLeftWidth / 100) * totalRatio);
this.rightRatio.set(totalRatio - this.leftRatio());
}
private onDragEnd() {
if (!this.isDragging) return;
this.isDragging = false;
this.renderer.removeClass(document.body, 'resizing');
}
}
<div
class="resizable-container"
[style.--left-width]="leftRatio() + 'fr'"
[style.--right-width]="rightRatio() + 'fr'"
>
<div class="left-panel">
<ng-content select="[left]"></ng-content>
</div>
<div class="resize-handle" #resizeHandle></div>
<div class="right-panel">
<ng-content select="[right]"></ng-content>
</div>
</div>
:host {
display: block;
height: 100%;
width: 100%;
}
.resizable-container {
display: grid;
grid-template-columns: var(--left-width, 2fr) 4px var(--right-width, 1fr);
grid-template-rows: auto 1fr;
height: 100%;
width: 100%;
gap: 0;
}
.left-panel,
.right-panel {
display: grid;
grid-template-rows: subgrid;
grid-row: span 2;
}
.right-panel {
overflow: auto;
}
.resize-handle {
display: grid;
grid-template-rows: subgrid;
grid-row: span 2;
width: 10px;
background: #ffffff;
cursor: col-resize;
position: relative;
transition: background-color 0.2s ease;
border-right: 1px solid #d9d9d9;
user-select: none;
&:hover {
background: #e2e2e2;
}
// Visual indicator for better UX
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
width: 2px;
height: 14px;
background: #c2c2c2;
border-radius: 1px;
transform: translate(-50%, -50%);
transition: background-color 0.2s ease;
}
&::before {
left: 2px;
}
&::after {
right: 2px;
}
&:hover::before,
&:hover::after {
background: #757575;
}
}
// Global style for dragging state
::ng-deep body.resizing {
cursor: col-resize !important;
user-select: none !important;
* {
cursor: col-resize !important;
user-select: none !important;
}
}
Legend
Html element with directive