opf/openproject

View on GitHub
frontend/src/app/shared/components/modal/modal-overlay.component.ts

Summary

Maintainability
A
40 mins
Test Coverage
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//
// See COPYRIGHT and LICENSE files for more details.
//++

import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ComponentRef,
  ElementRef,
  ViewChild,
} from '@angular/core';
import { CdkPortalOutlet, ComponentPortal } from '@angular/cdk/portal';
import { combineLatest } from 'rxjs';
import { distinctUntilChanged, filter } from 'rxjs/operators';

import { I18nService } from 'core-app/core/i18n/i18n.service';
import { OpModalComponent } from 'core-app/shared/components/modal/modal.component';
import { ModalData, OpModalService } from 'core-app/shared/components/modal/modal.service';
import { PortalOutletTarget } from 'core-app/shared/components/modal/portal-outlet-target.enum';


@Component({
  selector: 'opce-modal-overlay',
  templateUrl: './modal-overlay.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OpModalOverlayComponent {
  public notFullscreen = false;

  mobileTopPosition = false;

  public portalOutlet:CdkPortalOutlet;

  @ViewChild(CdkPortalOutlet) set portalOutletContainer(v:CdkPortalOutlet) {
    // ViewChild reference may be undefined initially
    // due to ngIf
    if (v !== undefined) {
      this.portalOutlet = v;
      this.setupListener();
    }
  }

  @ViewChild('overlay', { static: true }) overlay:ElementRef;

  activeModalData$ = this.modalService.activeModalData$;

  activeModalInstance$ = this.modalService.activeModalInstance$;

  constructor(
    readonly modalService:OpModalService,
    readonly I18n:I18nService,
    readonly cdRef:ChangeDetectorRef,
  ) {
  }

  setupListener():void {
    combineLatest([
      this.activeModalInstance$,
      // multiple 'closing' events in a row are squashed
      this.activeModalData$.pipe(distinctUntilChanged(), filter(this.isDefaultTarget.bind(this))),
    ])
      .subscribe(([instance, data]) => {
        if (instance === null && data === null) {
          // do nothing
          return;
        }

        if (instance !== null && data === null) {
          this.detachPortalInstance();
          return;
        }

        if (instance === null && data !== null) {
          this.createAndAttachPortalInstance(data);
        }
      });
  }

  protected isDefaultTarget(modalData:ModalData|null):boolean {
    if (modalData === null) return true;

    return modalData.target === PortalOutletTarget.Default;
  }

  public close($event:MouseEvent, includeChildClicks = true):void {
    if (!includeChildClicks && $event.currentTarget !== $event.target) {
      return;
    }
    this.modalService.close();
  }

  private detachPortalInstance():void {
    const ref = (this.portalOutlet.attachedRef as ComponentRef<OpModalComponent>);
    if (!ref) {
      return;
    }

    if (!ref.instance.onClose()) {
      return;
    }

    ref.instance.closingEvent.emit(ref.instance);
    this.portalOutlet.detach();
    this.modalService.activeModalInstance$.next(null);
  }

  private createAndAttachPortalInstance(data:ModalData):void {
    const { modal, injector, notFullscreen, mobileTopPosition } = data;
    this.notFullscreen = notFullscreen;
    this.mobileTopPosition = mobileTopPosition;
    const portal = new ComponentPortal(modal, null, injector);
    const ref = this.portalOutlet.attach(portal);
    const instance = ref.instance;

    this.modalService.activeModalInstance$.next(instance);
    this.cdRef.detectChanges();

    // Focus on wrapper by default
    (this.overlay.nativeElement as HTMLElement).focus();

    // Focus on the first element
    instance && instance.onOpen();
  }
}