All files / src/app/core/directives scroll.directive.ts

7.5% Statements 3/40
25% Branches 6/24
0% Functions 0/5
7.89% Lines 3/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 1303x 3x                                           3x                                                                                                                                                                                                                    
import { DOCUMENT } from '@angular/common';
import { Directive, ElementRef, Inject, Input, OnChanges } from '@angular/core';
 
/**
 * Structural directive.
 * Used on an element, the elements parent will scroll to it, when the given value is true
 *
 * @example
 * <div [ishScroll]="true" [scrollDuration]="500">
 *   Parent will scroll smoothly to this element within 500 milliseconds
 * </div>
 * or
 * <div [ishScroll]="true">
 *   Parent will scroll to this element instantly
 * </div>
 * or
 * <div [ishScroll]="false">
 *   Parent will not scroll to this element
 * </div>
 */
@Directive({
  selector: '[ishScroll]',
})
export class ScrollDirective implements OnChanges {
  constructor(private el: ElementRef, @Inject(DOCUMENT) private document: Document) {}
 
  /**
   * Wether or not scrolling should happen
   */
  @Input() ishScroll: boolean;
 
  /**
   * Sets the duration for the scrolling in milliseconds.
   * If set to 0 scrolling happens instantly.
   */
  @Input() scrollDuration = 0;
 
  /**
   * Sets the scroll container
   * The scroll container must have a scroll bar.
   *
   * @usageNotes
   * Set it to "root" to use the window documentElement (default), "parent" to use the element's parent,
   * or pass in any HTMLElement that is a parent to the element.
   */
  @Input() scrollContainer: 'parent' | 'root' | HTMLElement = 'root';
 
  /**
   * Sets spacing above the element in pixel.
   * If set, scrolling will target given amount of pixel above the element.
   * Set to 0 by default
   */
  @Input() scrollSpacing = 0;
 
  ngOnChanges() {
    Iif (this.ishScroll) {
      this.scroll();
    }
  }
 
  private scroll() {
    const target: HTMLElement = this.el.nativeElement;
    const container =
      this.scrollContainer === 'parent'
        ? target.parentElement
        : this.scrollContainer === 'root'
        ? this.document.documentElement
        : this.scrollContainer;
 
    // return if there is nothing to scroll
    Iif (!target.offsetParent) {
      return;
    }
 
    // calculate the offset from target to scrollContainer
    let offset = target.offsetTop;
    let tempTarget = target.offsetParent as HTMLElement;
    while (!tempTarget.isSameNode(container) && !tempTarget.isSameNode(this.document.body)) {
      offset += tempTarget.offsetTop;
      tempTarget = tempTarget.offsetParent as HTMLElement;
    }
    offset -= this.scrollSpacing;
    Iif (offset < 0) {
      offset = 0;
    }
 
    // scroll instantly if no duration is set
    Iif (!this.scrollDuration) {
      container.scrollTop = offset;
      return;
    }
 
    // set values for animation
    let startTime: number;
    const initialScrollPosition = container.scrollTop;
    const scrollDifference = offset - initialScrollPosition;
 
    const step = (timestamp: number) => {
      // set start time at the beginning
      Iif (!startTime) {
        startTime = timestamp;
      }
 
      // get time passed
      const passed = timestamp - startTime;
 
      // exit animation when duration is reached or calculated difference is smaller than 0.5 pixel
      Iif (passed >= this.scrollDuration || Math.abs(container.scrollTop - offset) <= 0.5) {
        container.scrollTop = offset;
        return;
      }
 
      // current progress of the animation based on timing
      const progress = passed / this.scrollDuration;
 
      // cubic ease-in-out function
      const ease = (x: number) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2);
 
      // set new scroll value based on current time progression
      container.scrollTop = initialScrollPosition + scrollDifference * ease(progress);
 
      // request next animation frame
      requestAnimationFrame(step);
    };
 
    // start animation by requesting animation frame
    requestAnimationFrame(step);
  }
}