skip to Main Content

I am creating a dynamic component that can contain multiple full-height panel components, where their widths can be resized and adjusted.

For example:

enter image description here

First of all, the container width must always be filled, you can’t have free space.

If I want to increase the size of A, it will decrease the size of B accordingly. If B has reached it’s minimum width, it will try and resize C, and so on till it can’t resize anyone else and the resize function will stop.

This works perfectly fine. However, now I want to do the same for decreasing width.
Let’s say I want to decrease the width of B, what happens now is that it will automatically increase the width of C, but what if B has reached it’s minimum width? it should try and decrease the previous siblings sizes, so A’s width should be decreased accordingly as much as possible.

This creates the effect of pushing panels when resizing.

when I say "decrease B’s width", it means increasing C. same as if I want to increase A’s width and it will decrease everything else one after another until it cannot resize anymore and reached the max available space.

Stackblitz: https://stackblitz.com/edit/stackblitz-starters-ftujzx?file=src%2Fapp%2Fpages%2Fpages%2Fplatform%2Fhelpers%2Fproject.helper.ts

You can resize by hovering the side borders of every panel

What I did

MouseDown to initialize the resizing:

  @HostListener('mousedown', ['$event'])
  onMouseDown($event: MouseEvent): void {
    $event.preventDefault();
    if (!$event.target) {
      return;
    }

    if (!($event.target instanceof HTMLElement)) {
      return;
    }

    const target = $event.target as HTMLElement;

    if (target.className !== 'split-resize-toggle') { // Todo use the IDynamicHorizontalSplitChild abstraction to get the toggle element to make it dynamic
      return;
    }

    const id = target.id;
    if (!id) {
      return;
    }

    this.currentResizingId = id;
    this.currentResizingElement = this.containerCards.find(card => card.getUniqueId() === id)?.getElement();
    this.startX = $event.pageX;

    this.elementStartWidth = this.currentResizingElement?.clientWidth;
  }

mouseMove handles the resizing:

  @HostListener('mousemove', ['$event'])
  onMouseMove($event: MouseEvent): void {
    if (!this.currentResizingId || !this.currentResizingElement || !this.startX) {
      return;
    }
    $event.preventDefault();

    const currentWidth = this.currentResizingElement.clientWidth;
    let newWidth = currentWidth + $event.movementX;

    // Get computed styles for the current element
    const currentStyles = window.getComputedStyle(this.currentResizingElement);
    const currentMinWidth = parseFloat(currentStyles.minWidth) || 0;
    const currentMaxWidth = parseFloat(currentStyles.maxWidth) || Infinity;

    // Constrain the new width of the current element
    newWidth = Math.max(currentMinWidth, Math.min(newWidth, currentMaxWidth));

    let widthDelta = newWidth - currentWidth;

    if (widthDelta !== 0) {
      let remainingDelta = this.adjustNextSiblings(this.currentResizingElement, widthDelta);

      // If we couldn't distribute all the delta, adjust the current element's width
      if (remainingDelta !== 0) {
        newWidth = currentWidth + (widthDelta - remainingDelta);
        this.disposeResizeHelperProperties();
      }
    }

    // Update the current element's width
    this.currentResizingElement.style.width = `${newWidth}px`;
  }

And the adjustNextSiblings function that tries to resize the next siblings accordingly to fill up the free space.

private adjustNextSiblings(element: HTMLElement, delta: number): number {
    let currentElement = element.nextElementSibling as HTMLElement | null;
    let remainingDelta = delta;

    while (currentElement && remainingDelta !== 0) {
      const currentWidth = currentElement.clientWidth;
      const newWidth = currentWidth - remainingDelta;

      const styles = window.getComputedStyle(currentElement);
      const minWidth = parseFloat(styles.minWidth) || 0;
      const maxWidth = parseFloat(styles.maxWidth) || Infinity;

      const constrainedWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));
      const actualDelta = currentWidth - constrainedWidth;

      currentElement.style.width = `${constrainedWidth}px`;
      remainingDelta -= actualDelta;

      currentElement = currentElement.nextElementSibling as HTMLElement | null;
    }

    return remainingDelta;
  }

The component name is DynamicHorizontalSplitContainerComponent

How can I get the opposite resizing (decreasing width) affect previous siblings + next siblings?

I have a feeling there is a shorter generic way to handle this, but I am over thinking.

2

Answers


  1. Add this function to resize previous siblings when decreasing width:

    private adjustPreviousSiblings(element: HTMLElement, delta: number): number {
        let currentElement = element.previousElementSibling as HTMLElement | null;
        let remainingDelta = delta;
    
        while (currentElement && remainingDelta !== 0) {
            const currentWidth = currentElement.clientWidth;
            const newWidth = currentWidth + remainingDelta;
    
            const styles = window.getComputedStyle(currentElement);
            const minWidth = parseFloat(styles.minWidth) || 0;
            const maxWidth = parseFloat(styles.maxWidth) || Infinity;
    
            const constrainedWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));
            const actualDelta = constrainedWidth - currentWidth;
    
            currentElement.style.width = `${constrainedWidth}px`;
            remainingDelta -= actualDelta;
    
            currentElement = currentElement.previousElementSibling as HTMLElement | null;
        }
    
        return remainingDelta;
    }
    

    Modify your onMouseMove method to handle both resizing directions:

    @HostListener('mousemove', ['$event'])
    onMouseMove($event: MouseEvent): void {
        if (!this.currentResizingId || !this.currentResizingElement || !this.startX) {
            return;
        }
        $event.preventDefault();
    
        const currentWidth = this.currentResizingElement.clientWidth;
        let newWidth = currentWidth + $event.movementX;
    
        const currentStyles = window.getComputedStyle(this.currentResizingElement);
        const currentMinWidth = parseFloat(currentStyles.minWidth) || 0;
        const currentMaxWidth = parseFloat(currentStyles.maxWidth) || Infinity;
    
        newWidth = Math.max(currentMinWidth, Math.min(newWidth, currentMaxWidth));
        let widthDelta = newWidth - currentWidth;
    
        if (widthDelta !== 0) {
            let remainingDelta = 0;
    
            if (widthDelta > 0) {
                remainingDelta = this.adjustNextSiblings(this.currentResizingElement, widthDelta);
            } else {
                remainingDelta = this.adjustNextSiblings(this.currentResizingElement, widthDelta);
                if (remainingDelta !== 0) {
                    remainingDelta = this.adjustPreviousSiblings(this.currentResizingElement, remainingDelta);
                }
            }
    
            if (remainingDelta !== 0) {
                newWidth = currentWidth + (widthDelta - remainingDelta);
                this.disposeResizeHelperProperties();
            }
        }
    
        this.currentResizingElement.style.width = `${newWidth}px`;
    }
    
    Login or Signup to reply.
  2. Imagine a layout like

    <div class="content">
       <div class="left">
          left
        </div>
      <div class="middle">
          middle
      </div>
      <div class="right">
          right
      </div>
    </div>
    

    Using css

      .content{
        display:flex;
        min-height:200px;
        align-items: stretch
      }
      .content>*{      
      border-right:1px solid silver;
      }
    
      .first{
        width:what-ever //<---can be 30% or 10px or 120px
       }    
      .second{
        flex-grow:1;
        min-width:80px; 
      }
      .third{
        min-width:80px;  
        width:33%;
      }
    

    Any value of the "width" of the "first" div makes the "third" maintance 30% -and fisrt + second maintance the 60%

    If third insted of width:33% have flex-grow:2 fill the rest of the row.

    In Angular we can use [style]="variable" (or [ngStyle]="variable"). So we can imagine some like:

    <div class="content">
      <div class="first" [style]="styleFirst">left</div>
      <div #midle class="second">midle</div>
      <div [style]="styleLast" class="right">right</div>
    </div>
    
      styleFirst:any={width:"33%"};
      styleLast:any={"width":"33%"};
      @ViewChild('midle',{static:true}) midle!:ElementRef
    
      changeStyle({width}:{width:number})
      {
         //we check to left 160px for second and last div
         width=width<document.documentElement.clientWidth-160?width:
                     document.documentElement.width-160
        this.styleFirst={width:width+'px'}
        this.styleLast=this.midle.nativeElement
             .getBoundingClientRect().width>80?{"width":"30%"}:{"flex-grow":2};
      }
    

    The only we need is a way to call the function changeStyle with a "width"

    There’re severals ways to makes resizable a div, e.g. based in this SO, I create a component that, instead of change the size, only emit an output with width and height

    @Component({
      selector: 'split-component',
      standalone:true,
      imports:[NgStyle],
      template: `
        <div #content class="resizable" [class.onDrag]="this.onDrag" [ngStyle]="style" >
          <ng-content></ng-content>
          <div #cellTop class="cell-border-top"></div>
          <div #cellBottom class="cell-border-bottom"></div>
          <div #cellLeft class="cell-border-left"></div>
          <div #cellRight class="cell-border-right"></div>
        </div>
      `,
      styles:[`
       ..omit for abreviate..
      `]
    })
    export class SplitComponent implements OnInit {
      rect: any;
      incr: number[] = [0, 0, 0, 0];
      nativeElement: any;
      typeDrag!: TypeDrag;
      origin: any;
      onDrag: boolean = false;
      moveSubscription: any;
      @ViewChild('content') content!:ElementRef
      @ViewChild('cellTop',{static:true}) cellTop!:ElementRef
      @ViewChild('cellBottom',{static:true})cellBottom!:ElementRef
      @ViewChild('cellLeft',{static:true})cellLeft!:ElementRef
      @ViewChild('cellRight',{static:true})cellRight!:ElementRef
    
      @Input() set split(value:'left'|'right'|'top'|'bottom')
      {
        setTimeout(()=>{
    
        this.cellTop.nativeElement.style.display=value=='top'?null:'none'
        this.cellBottom.nativeElement.style.display=value=='bottom'?null:'none'
        this.cellLeft.nativeElement.style.display=value=='left'?null:'none'
        this.cellRight.nativeElement.style.display=value=='right'?null:'none'
      })
    }
      @Output() change:EventEmitter<{width:number,height:number}>=new EventEmitter<{width:number,height:number}>()
    
      classNames = [
        'cell-top',
        'cell-border-top',
        'cell-border-bottom',
        'cell-border-left',
        'cell-border-right',
        'cell-top-right',
        'cell-bottom-right',
        'cell-top-left',
        'cell-bottom-left'
      ];
    
      style: any = null;
      constructor(private elementRef: ElementRef) {}
    
      
      ngOnInit(): void {
        merge(
        fromEvent(this.elementRef.nativeElement, 'mousedown'),
        fromEvent(this.elementRef.nativeElement, 'touchstart').pipe(map((event:any)=>({
          target:event.target,
          screenX:event.touches[0].screenX,
          screenY:event.touches[0].screenY
        }))
        ))
          .pipe(
            filter((event: any) => {
              const classs = (event.target as any).className;
              if (classs && typeof classs === 'string') {
                const className = classs.split(' ');
                console.log(className.indexOf(classs))
                return this.classNames.indexOf(classs) >= 0;
              }
              return false;
            })
          )
          .subscribe((event: MouseEvent) => {
            this.rect = this.content.nativeElement.getBoundingClientRect();
            console.log(typeof(event))
            this.origin =  { x: event.screenX, y: event.screenY };
    
            this.onDrag = true;
            const className = (event.target as any).className.split(' ');
            this.typeDrag =(this.classNames.indexOf(className[0]) as TypeDrag);
    
            this.incr =
                this.typeDrag == TypeDrag.Top
                ? [1, -1, 0, 0]
                : this.typeDrag == TypeDrag.Bottom
                ? [0, 1, 0, 0]
                : this.typeDrag == TypeDrag.Right
                ? [0, 0, 0, 1]
                : this.typeDrag == TypeDrag.Left
                ? [0, 0, 1, -1]
                : [0, 1, 1, -1];
    
            this.onDrag = true;
    
            merge(fromEvent(document, 'mouseup'),
                  fromEvent(document,'touchend')
            )
              .pipe(take(1))
              .subscribe(() => {
                if (this.moveSubscription) {
                  this.moveSubscription.unsubscribe();
                  this.moveSubscription = undefined;
                  this.onDrag = false;
                }
              });
    
            if (!this.moveSubscription) {
              this.moveSubscription = merge(
                fromEvent(document, 'mousemove'),
                fromEvent(document, 'touchmove').pipe(map((event:any)=>({
                  target:event.target,
                  screenX:event.touches[0].screenX,
                  screenY:event.touches[0].screenY
                }))
                ))
                .pipe(startWith({ screenY: this.origin.y, screenX: this.origin.x }))
                .subscribe((moveEvent: any) => {
                  const incrTop = moveEvent.screenY - this.origin.y;
                  const incrLeft = moveEvent.screenX - this.origin.x;
                  const width = this.rect.width - this.incr[3] * incrLeft;
                  const heigth = this.rect.height - this.incr[1] * incrTop;
                  this.change.emit(
                  {
                    width: (width < 50 ? 50 : width - 1),
                    height: (heigth < 75 ? 75 : heigth - 1)
                  });
                });
            }
          });
      }
    }
    

    Now we can use some like

    <div class="content">
      <split-component
        split="right"
        (change)="changeStyle($event)"
        [style]="styleFirst"
      >
        <div class="first">left</div>
      </split-component>
      <div #midle class="second">midle</div>
      <div [style]="styleLast" class="right">right</div>
    </div>
    

    You can see the result in this stackblitz

    NOTE: I write in styles.css

    body{
      padding: 0;
      margin:0;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search