mseemann/angular2-mdl

View on GitHub
projects/core/src/lib/dialog/mdl-dialog-host.component.ts

Summary

Maintainability
A
3 hrs
Test Coverage
A
100%
import {
  Component,
  ComponentRef,
  ElementRef,
  forwardRef,
  HostBinding,
  Inject,
  NgZone,
  OnInit,
  Renderer2,
  ViewChild,
  ViewContainerRef,
  ViewEncapsulation,
} from "@angular/core";

import { MDL_CONFIGUARTION, MIN_DIALOG_Z_INDEX } from "./config";
import {
  IMdlDialogConfiguration,
  IOpenCloseRect,
} from "./mdl-dialog-configuration";
import { MdlButtonComponent } from "../button/mdl-button.component";
import { InternalMdlDialogReference } from "./internal-dialog-reference";
import { Animations } from "../common/animations";

const enterTransitionDuration = 300;
const leaveTransitionDuration = 250;

const enterTransitionEasingCurve = "cubic-bezier(0.0, 0.0, 0.2, 1)";
const leaveTransitionEasingCurve = "cubic-bezier(0.0, 0.0, 0.2, 1)";

const createOpenCloseRect = (rect: ClientRect): IOpenCloseRect => ({
  height: rect.top - rect.bottom,
  left: rect.left,
  top: rect.top,
  width: rect.right - rect.left,
});

const getCenterInScreen = (rect: IOpenCloseRect) => ({
  cx: Math.round(rect.left + rect.width / 2),
  cy: Math.round(rect.top + rect.height / 2),
});

const getClientRect = (
  input: MdlButtonComponent | MouseEvent | IOpenCloseRect | undefined
): IOpenCloseRect => {
  if (input instanceof MdlButtonComponent) {
    const elRef = (input as MdlButtonComponent).elementRef;
    const rect: ClientRect = elRef.nativeElement.getBoundingClientRect();
    return createOpenCloseRect(rect);
  } else if (input instanceof MouseEvent) {
    const evt: MouseEvent = input as MouseEvent;
    // just to make it possible to test this code with a fake event - target is
    // readonly and con not be mutated.
    // eslint-disable-next-line
    const htmlElement = (evt.target || (evt as any).testtarget) as HTMLElement;
    const rect: ClientRect = htmlElement.getBoundingClientRect();
    return createOpenCloseRect(rect);
  }
  return input as IOpenCloseRect;
};

// @experimental
@Component({
  selector: "mdl-dialog-host-component",
  template: ` <div #dialogTarget></div>`,
  styles: [
    `
      mdl-dialog-host-component {
        width: fit-content;
        height: fit-content;
        padding: 1em;
        background: white;
        color: black;
        opacity: 1;
        visibility: hidden;
        display: block;
        position: fixed;
        margin: auto;
        left: 0;
        right: 0;
        transition: all;
        top: 50%;
        transform: translate(0, -50%);
      }

      mdl-dialog-host-component.open {
        visibility: visible;
      }
    `,
  ],
  encapsulation: ViewEncapsulation.None,
})
export class MdlDialogHostComponent implements OnInit {
  @ViewChild("dialogTarget", { read: ViewContainerRef, static: true })
  dialogTarget: ViewContainerRef | undefined;

  @HostBinding("class.mdl-dialog")
  isDialog = true;

  @HostBinding("class.open")
  visible = false;

  @HostBinding("style.zIndex")
  zIndex: number = MIN_DIALOG_Z_INDEX + 1;

  private showAnimationStartStyle: { [key: string]: string } = {
    top: "38%",
    opacity: "0",
  };
  private showStyle: { [key: string]: string } = {
    top: "50%",
    opacity: "1",
  };
  private hideAnimationEndStyle: { [key: string]: string } = {
    top: "63%",
    opacity: "0",
  };

  constructor(
    private ngZone: NgZone,
    private renderer: Renderer2,
    private animations: Animations,
    private elementRef: ElementRef,
    @Inject(forwardRef(() => MDL_CONFIGUARTION))
    private config: IMdlDialogConfiguration,
    private internalDialogRef: InternalMdlDialogReference
  ) {}

  show(): void {
    this.visible = true;
    // give the dialogs time to draw so that a focus can be set
    setTimeout(() => {
      this.internalDialogRef.visible();
    });

    if (this.isAnimateEnabled()) {
      if (this.config.openFrom || this.config.closeTo) {
        // transform is modified during anmiation and must be part of each animation keyframe.
        this.showStyle["transform"] = "translate(0, -50%) scale(1.0)";

        const targetClientRect =
          this.elementRef.nativeElement.getBoundingClientRect();

        const openFromRect = getClientRect(this.config?.openFrom);
        const closeToRect = this.config.closeTo
          ? getClientRect(this.config.closeTo)
          : openFromRect;

        const centerTarget = getCenterInScreen(targetClientRect);
        const centerFrom = getCenterInScreen(openFromRect);
        const centerTo = getCenterInScreen(closeToRect);

        const translationFrom = {
          x: Math.round(centerFrom.cx - centerTarget.cx),
          y: Math.round(centerFrom.cy - centerTarget.cy),
          scaleX:
            Math.round(
              100 * Math.min(0.25, openFromRect.width / targetClientRect.width)
            ) / 100,
          scaleY:
            Math.round(
              100 *
                Math.min(0.25, openFromRect.height / targetClientRect.height)
            ) / 100,
        };

        this.showAnimationStartStyle = {
          top: `${targetClientRect.top}px`,
          opacity: "0",
          transform: `translate(${translationFrom.x}px, ${translationFrom.y}px) scale(${translationFrom.scaleX}, ${translationFrom.scaleY})`,
        };

        const translationTo = {
          x: Math.round(centerTo.cx - centerTarget.cx),
          y: Math.round(centerTo.cy - centerTarget.cy),
          scaleX:
            Math.round(
              100 * Math.min(0.25, closeToRect.width / targetClientRect.width)
            ) / 100,
          scaleY:
            Math.round(
              100 * Math.min(0.25, closeToRect.height / targetClientRect.height)
            ) / 100,
        };

        this.hideAnimationEndStyle = {
          top: `${targetClientRect.top}px`,
          opacity: "0",
          transform: `translate(${translationTo.x}px, ${translationTo.y}px) scale(${translationTo.scaleX}, ${translationTo.scaleY})`,
        };
      }

      const animation = this.animations.animate(
        this.elementRef.nativeElement,
        [this.showAnimationStartStyle, this.showStyle],
        this.config.enterTransitionDuration || enterTransitionDuration,
        this.config.enterTransitionEasingCurve || enterTransitionEasingCurve
      );

      animation.play();
    }
  }

  hide(selfComponentRef: ComponentRef<MdlDialogHostComponent>): void {
    if (this.isAnimateEnabled()) {
      const animation = this.animations.animate(
        this.elementRef.nativeElement,
        [this.showStyle, this.hideAnimationEndStyle],
        this.config.leaveTransitionDuration || leaveTransitionDuration,
        this.config.leaveTransitionEasingCurve || leaveTransitionEasingCurve
      );

      animation.onDone(() => {
        selfComponentRef.destroy();
      });

      animation.play();
    } else {
      selfComponentRef.destroy();
    }
  }

  ngOnInit(): void {
    this.applyStyle(this.config.styles);
    this.applyClasses(this.config.classes ? this.config.classes : "");
  }

  private applyStyle(styles: { [p: string]: string } | undefined) {
    if (styles) {
      for (const style of Object.keys(styles)) {
        this.renderer.setStyle(
          this.elementRef.nativeElement,
          style,
          styles[style]
        );
      }
    }
  }

  private applyClasses(classes: string) {
    classes
      .split(" ")
      .filter((cssClass) => !!cssClass)
      .forEach((cssClass) => {
        this.renderer.addClass(this.elementRef.nativeElement, cssClass);
      });
  }

  private isAnimateEnabled() {
    // not present?  assume it is true.
    if (typeof this.config.animate === "undefined") {
      return true;
    }
    return this.config.animate;
  }
}