libs/core/devtools/dynamic-form-editor/src/lib/dynamic-form-editor/resizable-panels/resizable-panels.component.ts

Implements

AfterViewInit

Metadata

Relationships

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>

./resizable-panels.component.scss

: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
Component
Html element with directive

results matching ""

    No results matching ""