@angular/core#AfterViewChecked TypeScript Examples

The following examples show how to use @angular/core#AfterViewChecked. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: accordion-panel-heading.component.ts    From canopy with Apache License 2.0 7 votes vote down vote up
@Component({
  selector: 'lg-accordion-panel-heading',
  templateUrl: './accordion-panel-heading.component.html',
  styleUrls: [ './accordion-panel-heading.component.scss' ],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LgAccordionPanelHeadingComponent implements AfterViewChecked {
  @Input() headingLevel: HeadingLevel;
  @Input()
  get isActive() {
    return this._isActive;
  }
  set isActive(isActive: boolean) {
    this._isActive = isActive;
    this.cdr.markForCheck();
  }
  @Output() toggleActive = new EventEmitter<boolean>();

  _id = nextUniqueId++;
  _toggleId = `lg-accordion-panel-heading-${this._id}`;
  _panelId = `lg-accordion-panel-${this._id}`;
  _isActive = false;

  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewChecked() {
    this.cdr.detectChanges();
  }

  toggle() {
    this.isActive = !this.isActive;
    this.toggleActive.emit(this.isActive);
  }
}
Example #2
Source File: static.component.ts    From ng-event-plugins with Apache License 2.0 6 votes vote down vote up
@Component({
    selector: 'static',
    templateUrl: './static.template.html',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StaticComponent implements AfterViewChecked {
    readonly typescript = typescript;

    readonly html = html;

    readonly items = ['Option 1', 'Option 2', 'Option 3', 'Option 4'];

    popup = false;

    ngAfterViewChecked() {
        // tslint:disable-next-line:no-console
        console.log('change detection cycle', Date.now());
    }

    onOpened() {
        this.popup = true;
    }

    onClosed() {
        this.popup = false;
    }
}
Example #3
Source File: alert-dialog.component.ts    From WowUp with GNU General Public License v3.0 6 votes vote down vote up
@Component({
  selector: "app-alert-dialog",
  templateUrl: "./alert-dialog.component.html",
  styleUrls: ["./alert-dialog.component.scss"],
})
export class AlertDialogComponent implements AfterViewChecked {
  @ViewChild("dialogContent", { read: ElementRef }) public dialogContent!: ElementRef;

  public constructor(
    public dialogRef: MatDialogRef<AlertDialogComponent>,
    @Inject(MAT_DIALOG_DATA) public data: AlertDialogData,
    private _linkService: LinkService
  ) {}

  public ngAfterViewChecked(): void {
    const descriptionContainer: HTMLDivElement = this.dialogContent?.nativeElement;
    formatDynamicLinks(descriptionContainer, this.onOpenLink);
  }

  private onOpenLink = (element: HTMLAnchorElement): boolean => {
    
    this._linkService.confirmLinkNavigation(element.href).subscribe();

    return false;
  };
}
Example #4
Source File: patch-notes-dialog.component.ts    From WowUp with GNU General Public License v3.0 6 votes vote down vote up
@Component({
  selector: "app-patch-notes-dialog",
  templateUrl: "./patch-notes-dialog.component.html",
  styleUrls: ["./patch-notes-dialog.component.scss"],
})
export class PatchNotesDialogComponent implements OnInit, AfterViewChecked {
  @ViewChild("descriptionContainer", { read: ElementRef }) public descriptionContainer!: ElementRef;

  public title = from(this._electronService.getVersionNumber()).pipe(
    first(),
    switchMap((versionNumber) => this._translateService.get("DIALOGS.NEW_VERSION_POPUP.TITLE", { versionNumber }))
  );

  public changeLog: ChangeLog;

  public constructor(
    private _translateService: TranslateService,
    private _electronService: ElectronService,
    private _patchNotesService: PatchNotesService,
    private _linkService: LinkService
  ) {
    this.changeLog = _.first(this._patchNotesService.changeLogs) ?? { Version: "" };
  }

  public ngOnInit(): void {}

  public ngAfterViewChecked(): void {
    const descriptionContainer: HTMLDivElement = this.descriptionContainer?.nativeElement;
    formatDynamicLinks(descriptionContainer, this.onOpenLink);
  }

  private onOpenLink = (element: HTMLAnchorElement): boolean => {
    this._linkService.confirmLinkNavigation(element.href).subscribe();

    return false;
  };
}
Example #5
Source File: eda-map.component.ts    From EDA with GNU Affero General Public License v3.0 5 votes vote down vote up
@Component({
  selector: 'eda-map',
  templateUrl: './eda-map.component.html',
  styleUrls: ['./eda-map.component.css']
})
export class EdaMapComponent implements OnInit, AfterViewInit, AfterViewChecked {

  @Input() inject: EdaMap;

  private map: any;

  constructor(
    private mapUtilsService: MapUtilsService
  ) {

  }
  ngOnInit(): void {
  }

  ngAfterViewInit(): void {

    this.initMap();
  }
  ngAfterViewChecked() {
    if (this.map) {
      this.map.invalidateSize();
    }

  }

  private initMap = (): void => {
    let validData = [];// Faig això per treure els nulls i resta de bruticia del dataset
    for(let i = 0; i < this.inject.data.length; i++ ) { 
      if(    this.inject.data[i][0]  !== null   &&  this.inject.data[i][1]  !== null  ) {
        validData.push(this.inject.data[i]);
      }
    }  


    if (L.DomUtil.get(this.inject.div_name) !== null) {
      this.map = L.map(this.inject.div_name, {
        //center: [41.38879, 2.15899],
        center: this.getCenter(validData),
        zoom: this.inject.zoom ? this.inject.zoom : 12,
        dragging: !L.Browser.mobile,
        tap: !L.Browser.mobile
      });
      const tiles = L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>'
      });

      tiles.addTo(this.map);
      this.mapUtilsService.makeMarkers(this.map, validData, this.inject.labels, this.inject.linkedDashboard);
      this.map.on('moveend', (event) => {
        let c = this.map.getCenter();
        this.inject.coordinates = [c.lat, c.lng];
      });
      this.map.on('zoomend', (event) => {
        this.inject.zoom = this.map.getZoom();
      });
    }

  }

  private getCenter(data: Array<any>) {
    let x: number, y: number;
    if (!this.inject.coordinates) {
      let minX = data.reduce((min, v) => min >= parseFloat(v[0]) ? parseFloat(v[0]) : min, Infinity);
      let minY = data.reduce((min, v) => min >= parseFloat(v[1]) ? parseFloat(v[1]) : min, Infinity);
      let maxX = data.reduce((max, v) => max >= parseFloat(v[0]) ? max : parseFloat(v[0]), -Infinity);
      let maxY = data.reduce((max, v) => max >= parseFloat(v[1]) ? max : parseFloat(v[1]), -Infinity);
      x = minX + ((maxX - minX) / 2);
      y = minY + ((maxY - minY) / 2);
    }
    let coordinates = this.inject.coordinates ? this.inject.coordinates : [y, x];
    return coordinates as LatLngExpression
  }

}
Example #6
Source File: consent-dialog.component.ts    From WowUp with GNU General Public License v3.0 5 votes vote down vote up
@Component({
  selector: "app-consent-dialog",
  templateUrl: "./consent-dialog.component.html",
  styleUrls: ["./consent-dialog.component.scss"],
})
export class ConsentDialogComponent implements AfterViewChecked {
  @ViewChild("dialogContent", { read: ElementRef }) public dialogContent!: ElementRef;

  public consentOptions: FormGroup;

  public readonly wagoTermsUrl = AppConfig.wago.termsUrl;
  public readonly wagoDataUrl = AppConfig.wago.dataConsentUrl;

  public constructor(public dialogRef: MatDialogRef<ConsentDialogComponent>, private _linkService: LinkService) {
    this.consentOptions = new FormGroup({
      telemetry: new FormControl(true),
      wagoProvider: new FormControl(true),
    });
  }

  public ngAfterViewChecked(): void {
    const descriptionContainer: HTMLDivElement = this.dialogContent?.nativeElement;
    formatDynamicLinks(descriptionContainer, this.onOpenLink);
  }

  public onNoClick(): void {
    this.dialogRef.close();
  }

  public onSubmit(evt: any): void {
    evt.preventDefault();

    console.log(this.consentOptions.value);

    this.dialogRef.close(this.consentOptions.value);
  }

  private onOpenLink = (element: HTMLAnchorElement): boolean => {
    this._linkService.confirmLinkNavigation(element.href).subscribe();

    return false;
  };
}
Example #7
Source File: curse-migration-dialog.component.ts    From WowUp with GNU General Public License v3.0 5 votes vote down vote up
@Component({
  selector: "app-curse-migration-dialog",
  templateUrl: "./curse-migration-dialog.component.html",
  styleUrls: ["./curse-migration-dialog.component.scss"],
})
export class CurseMigrationDialogComponent implements AfterViewChecked {
  @ViewChild("dialogContent", { read: ElementRef }) public dialogContent!: ElementRef;

  public readonly isBusy$ = new BehaviorSubject<boolean>(false);
  public readonly autoError$ = new BehaviorSubject<Error | undefined>(undefined);
  public readonly autoComplete$ = new BehaviorSubject<boolean>(false);
  public readonly autoIncomplete$ = this.autoComplete$.pipe(map((complete) => !complete));

  public constructor(
    public dialogRef: MatDialogRef<CurseMigrationDialogComponent>,
    private _addonService: AddonService,
    private _linkService: LinkService,
    private _sessionService: SessionService,
    private _warcraftInstallationService: WarcraftInstallationService
  ) {}

  public ngAfterViewChecked(): void {
    const descriptionContainer: HTMLDivElement = this.dialogContent?.nativeElement;
    formatDynamicLinks(descriptionContainer, this.onOpenLink);
  }

  public onNoClick(): void {
    this.dialogRef.close();
  }

  public async onAutomaticClick(): Promise<void> {
    this.isBusy$.next(true);

    try {
      // Fetch all installations
      let scanCompleted = false;
      const wowInstallations = await this._warcraftInstallationService.getWowInstallationsAsync();
      for (const wowInstall of wowInstallations) {
        // If there are any old Curse addons, re-scan that installation
        let addons = await this._addonService.getAddons(wowInstall);
        addons = addons.filter(
          (addon) =>
            addon.isIgnored === false &&
            (addon.providerName === ADDON_PROVIDER_CURSEFORGE || addon.providerName === ADDON_PROVIDER_CURSEFORGEV2)
        );
        if (addons.length > 0) {
          await this._addonService.rescanInstallation(wowInstall);
          scanCompleted = true;
        }
      }

      if (scanCompleted) {
        this._sessionService.rescanCompleted();
      }
    } catch (e) {
      console.error(e);
      this.autoError$.next(e as Error);
    } finally {
      this.isBusy$.next(false);
      this.autoComplete$.next(true);
    }
  }

  private onOpenLink = (element: HTMLAnchorElement): boolean => {
    this._linkService.confirmLinkNavigation(element.href).subscribe();

    return false;
  };
}
Example #8
Source File: about.component.ts    From WowUp with GNU General Public License v3.0 5 votes vote down vote up
@Component({
  selector: "app-about",
  templateUrl: "./about.component.html",
  styleUrls: ["./about.component.scss"],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AboutComponent implements OnDestroy, AfterViewChecked {
  @Input("tabIndex") public tabIndex!: number;

  @ViewChild("changelogContainer", { read: ElementRef }) public changelogContainer!: ElementRef;

  private _subscriptions: Subscription[] = [];

  public changeLogs: ChangeLog[] = [];
  public versionNumber = from(this.electronService.getVersionNumber());

  public constructor(
    public wowUpService: WowUpService,
    public electronService: ElectronService,
    private _sessionService: SessionService,
    private _patchNotesService: PatchNotesService,
    private _sanitizer: DomSanitizer,
    private _linkService: LinkService
  ) {
    this.changeLogs = this._patchNotesService.changeLogs;
    const tabIndexSub = this._sessionService.selectedHomeTab$
      .pipe(
        filter((newTabIndex) => newTabIndex === this.tabIndex),
        map(() => {
          window.getSelection()?.empty();
        })
      )
      .subscribe();

    this._subscriptions.push(tabIndexSub);
  }

  public ngOnDestroy(): void {
    this._subscriptions.forEach((subscription) => subscription.unsubscribe());
    this._subscriptions = [];
  }

  public ngAfterViewChecked(): void {
    const descriptionContainer: HTMLDivElement = this.changelogContainer?.nativeElement;
    formatDynamicLinks(descriptionContainer, this.onOpenLink);
  }

  private onOpenLink = (element: HTMLAnchorElement): boolean => {
    this._linkService.confirmLinkNavigation(element.href).subscribe();

    return false;
  };

  public formatChanges(changeLog: ChangeLog): string {
    return (changeLog.changes ?? []).join("\n");
  }

  public trustHtml(html: string): SafeHtml {
    return this._sanitizer.bypassSecurityTrustHtml(html);
  }
}
Example #9
Source File: no-check.ts    From casual-chess with GNU General Public License v3.0 5 votes vote down vote up
@Directive({
  selector: '[ngNoCheck]',
})
export class NgNoCheck implements AfterViewChecked {
  @Input() set ngNoCheck(value: boolean) {
    this.noCheck = value !== false;

    if (this.detached && !this.noCheck) {
      this.view.reattach();
      this.detached = false;
    }
  }

  @Input() set ngNoCheckNotifier(value: Observable<void>) {
    if (value !== this.notifier) {
      if (this.subscription) {
        this.subscription.unsubscribe();
        this.subscription = null;
      }
      if (value) {
        this.notifier = value;
        this.subscription = this.notifier.subscribe(() => {
          this.check();
        });
      } else {
        this.notifier = null;
      }
    }
  }

  private noCheck: boolean = true;
  private notifier: Observable<void>|null = null;
  private subscription: Subscription|null = null;
  private detached: boolean = false;
  private view: EmbeddedViewRef<NgNoCheckContext>;

  constructor(private template: TemplateRef<NgNoCheckContext>, private vcRef: ViewContainerRef) {
    this.view = this.vcRef.createEmbeddedView(this.template, { $implicit: this });
  }

  ngAfterViewChecked(): void {
    if (!this.detached && this.noCheck) {
      this.view.detach();
      this.detached = true;
    }
  }

  check(): void {
    if (this.detached) {
      this.view.detectChanges();
    }
  }
}
Example #10
Source File: employee-details-card.component.ts    From fyle-mobile-app with MIT License 5 votes vote down vote up
@Component({
  selector: 'app-employee-details-card',
  templateUrl: './employee-details-card.component.html',
  styleUrls: ['./employee-details-card.component.scss'],
})
export class EmployeeDetailsCardComponent implements OnInit, AfterViewChecked {
  @ViewChild('employeeIdRef', { read: ElementRef }) employeeIdRef: ElementRef;

  @Input() eou: ExtendedOrgUser;

  isMobileNumberHidden: boolean;

  isEmployeeIdHidden: boolean;

  constructor(
    private authService: AuthService,
    private popoverController: PopoverController,
    private orgUserService: OrgUserService,
    private trackingService: TrackingService,
    private matSnackBar: MatSnackBar,
    private snackbarProperties: SnackbarPropertiesService,
    private cdRef: ChangeDetectorRef
  ) {}

  ngOnInit(): void {}

  ngAfterViewChecked() {
    const employeeIdEl = this.employeeIdRef?.nativeElement;
    this.isEmployeeIdHidden = employeeIdEl && employeeIdEl.scrollWidth > employeeIdEl.offsetWidth;
    this.cdRef.detectChanges();
  }

  async updateMobileNumber() {
    const updateMobileNumberPopover = await this.popoverController.create({
      component: FyInputPopoverComponent,
      componentProps: {
        title: (this.eou.ou.mobile?.length ? 'Edit' : 'Add') + ' Mobile Number',
        ctaText: 'Save',
        inputLabel: 'Mobile Number',
        inputValue: this.eou.ou.mobile,
        inputType: 'tel',
        isRequired: false,
      },
      cssClass: 'fy-dialog-popover',
    });

    await updateMobileNumberPopover.present();
    const { data } = await updateMobileNumberPopover.onWillDismiss();

    if (data) {
      this.eou.ou.mobile = data.newValue;
      this.orgUserService
        .postOrgUser(this.eou.ou)
        .pipe(
          concatMap(() =>
            this.authService.refreshEou().pipe(
              tap(() => this.trackingService.activated()),
              map(() => {
                const message = 'Profile saved successfully';
                this.matSnackBar.openFromComponent(ToastMessageComponent, {
                  ...this.snackbarProperties.setSnackbarProperties('success', { message }),
                  panelClass: ['msb-success'],
                });
                this.trackingService.showToastMessage({ ToastContent: message });
              })
            )
          )
        )
        .subscribe(noop);
    }
  }

  showTooltip(tooltipRef: MatTooltip) {
    tooltipRef.show();
    setTimeout(() => tooltipRef.hide(), 3000);
  }
}
Example #11
Source File: eda-geoJsonMap.component.ts    From EDA with GNU Affero General Public License v3.0 4 votes vote down vote up
@Component({
  selector: 'eda-geomap',
  templateUrl: './eda-geojson-map.component.html',
  styleUrls: ['./eda-map.component.css'],
  animations: [
    trigger(
      'inOutAnimation',
      [
        transition(
          ':leave',
          [
            style({ height: 500, opacity: 1 }),
            animate('1s ease-in',
              style({ height: 0, opacity: 0 }))
          ]
        )
      ]
    )
  ]
})
export class EdaGeoJsonMapComponent implements OnInit, AfterViewInit, AfterViewChecked {

  @Input() inject: EdaMap;

  private map: any;
  private geoJson: any;
  private shapes: any;
  private groups: Array<number>;
  private customOptions: any;
  private legend: any;
  private labelIdx: number;
  private dataIndex: number;
  private serverMap: any = null;
  public color: string = '#006400';
  public logarithmicScale: boolean;
  public BASE_COLOR: string = '#006400';
  public loading: boolean;
  public legendPosition: string;
  public boundaries: Array<any> = [];

  constructor(
    private mapUtilsService: MapUtilsService, private _sanitizer: DomSanitizer
  ) {
    this.customOptions = { 'className': 'custom', offset: [-20, -20], autoPan: false, closeButton: false };
  }
  ngOnInit(): void {
    this.loading = true;
    this.labelIdx = this.inject.query.findIndex(e => e.column_type === 'text' || e.column_type === 'text');
    this.dataIndex = this.inject.query.findIndex(e => e.column_type === 'numeric');
    if (this.inject.query) {
      this.serverMap = this.inject.maps.filter(map => map['mapID'] === this.inject.query[this.labelIdx].linkedMap)[0];
      this.mapUtilsService.initShapes(this.serverMap['mapID']); /** to delete */
    }
    this.color = this.inject.color ? this.inject.color : this.BASE_COLOR;
    this.logarithmicScale = this.inject.logarithmicScale ? this.inject.logarithmicScale : false;
    this.legendPosition = this.inject.legendPosition ? this.inject.legendPosition : 'bottomright';
    this.legend = new L.Control({ position: this.legendPosition });
  }

  ngAfterViewInit(): void {
    if (this.serverMap) {
      this.setGroups();
      this.checkElement( '#' + this.inject.div_name) 
        .then((element) => {
            this.initMap();
            console.log('map started');
            this.mapUtilsService.getShapes(this.serverMap['mapID']).subscribe(shapes => {
              this.shapes = shapes.file;
              this.initShapesLayer();
            });
        });
    }
  }

  private initShapesLayer = () => {

    const field = this.serverMap['field'];
    const totalSum = this.inject.data.map(row => row[this.dataIndex]).reduce((a, b) => a + b);

    this.geoJson = new L.TopoJSON(this.shapes, {
      style: (feature) => this.style(feature, this.color),
      onEachFeature: this.onEachFeature
    });
  
    this.geoJson.eachLayer((layer) => {
      this.boundaries.push(layer._bounds);
      layer.bindPopup(this.mapUtilsService.makeGeoJsonPopup(layer.feature.properties[field], this.inject.data,
        this.inject.labels, this.labelIdx, totalSum), this.customOptions);
      layer.on('mouseover', function () { layer.openPopup(); });
      layer.on('mouseout', function () { layer.closePopup(); });
      layer.on("click", () => this.openLinkedDashboard(layer.feature.properties.name))
    });
    if (this.map) {

      this.geoJson.addTo(this.map);
      if (this.inject.zoom) this.map.zoom = this.inject.zoom
      else this.map.fitBounds(this.boundaries);

    }else{
      console.log('map not yet ready');
    }

    this.geoJson.on('add', this.onloadLayer());
  }

  private onloadLayer = () => {
    setTimeout(() => {
      this.loading = false;
    }, 0);

  }


  private  rafAsync() {
    return new Promise(resolve => {
        requestAnimationFrame(resolve); //faster than set time out
      });
  }

  private checkElement(selector) {
      if (document.querySelector(selector) === null) {
          return this.rafAsync().then(() => this.checkElement(selector));
      } else {
          return Promise.resolve(true);
      }
  } 

  private initMap = (): void => {
    if (L.DomUtil.get(this.inject.div_name) !== null) {
      this.map = L.map(this.inject.div_name, {
        minZoom: 0,
        center: this.getCenter(this.inject.data),
        zoom: this.inject.zoom ? this.inject.zoom : 0,
        dragging: !L.Browser.mobile,
        tap: !L.Browser.mobile
      });
      this.map.options.minZoom = this.setMinZoom();
      //tiles.addTo(this.map);
      this.initLegend(this.groups, this.inject.labels[this.dataIndex], this.color);
      this.map.on('moveend', (event) => {
        let c = this.map.getCenter();
        this.inject.coordinates = [c.lat, c.lng];
      });
      this.map.on('zoomend', (event) => {
        this.inject.zoom = this.map.getZoom();
      });
    }else{
      console.log('Div not yet ready');
    }

  }

  private setMinZoom(): number {

    let bounds = this.map.getBounds();
    let NE = bounds._northEast;
    let SW = bounds._southWest;
    let lng = NE.lng - SW.lng;

    /**Zomm level for world map */
    let zoomLevel = 0;
    let bound = 200;

    if (lng > bound) return zoomLevel;
    else
    {
      while(lng < bound){
        bound = bound / 2;
        zoomLevel ++;
      }
      return zoomLevel;
    }

  }

  setGroups(): void {
    if (this.logarithmicScale) {
      this.groups = this.getLogarithmicGroups(this.inject.data.map(row => row[this.dataIndex]));
    } else {
      this.groups = this.getGroups(this.inject.data.map(row => row[this.dataIndex]));
    }
  }

  ngAfterViewChecked() {
    if (this.map) {
      this.map.invalidateSize();
    }

  }


  /**
   * Get coordinates to center map
   * @param data 
   */
  private getCenter(data: Array<any>) {
    let coordinates = this.inject.coordinates ? this.inject.coordinates : [this.serverMap.center[1], this.serverMap.center[0]]
    if(coordinates[0]===null || coordinates[1]===null){
      let x: number, y: number;
      x=41.972330;
      y=2.8116391;
      coordinates[0]=x;
      coordinates[1]=y;
    }
    return coordinates as LatLngExpression;

  }

  private onEachFeature = (feature, layer) => {
    layer.on({
      /**Disabled cause pop up is displayed on hover  */
      //mouseover: this.highlightFeature,
      //mouseout: this.resetHighlight
    });
  }

  private highlightFeature = (e) => {
    let layer = e.target;
    layer.setStyle({
      weight: 2,
      color: '#FFFF',
      dashArray: '',
      fillOpacity: 0.7
    });

    if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
      layer.bringToFront();
    }
  }

  private resetHighlight = (e) => {
    this.geoJson.resetStyle(e.target);
  }

  private style = (feature, color) => {
    const field = this.serverMap['field'];
    let fillColor = this.getColor(this.groups, this.bindDataToProperties(feature.properties[field]), color);
    return {
      weight: 0.1,
      opacity: 1,
      color: '#FFFFFF',
      fillOpacity: 0.5,
      fillColor: fillColor //'#6DB65B'

    }
  }

  /**
   * bind data to feature_property, returns one row
   * @param feature_property 
   */
  private bindDataToProperties = (feature_property: string) => {
    const clean = (s: string) => s ? s.toUpperCase().replace(/\s/g, '') : s;
    let data = this.inject.data.filter(row => clean(row[this.labelIdx]) === clean(feature_property))
      .map(row => row[this.dataIndex]);
    return data[0];
  }

  /**
   * Get color shade depending on wich group belongs value
   * @param groups 
   * @param value 
   */
  private getColor = (groups: Array<number>, value: number, color: string) => {
    if (!value) return '#eef0f3';
    let group = [value, ...groups].sort((a, b) => a - b).indexOf(value);
    let shade = group === 0 ? 80 : group === 1 ? 40 : group === 2 ? 0 : group = 3 ? -40 : -80;
    return this.colorShade(color, shade);
  }

  /**
   * Divide data in n groups by value, and returns values limits f.e.[0, 20, 40, 60, 80, 100]
   * @param data 
   * @param n 
   */
  private getGroups = (data: any, n = 5) => {
    let max = data.reduce((a: number, b: number) => Math.max(a, b));
    let min = data.reduce((a: number, b: number) => Math.min(a, b));
    // El primer rang comença amb el número mes petit
    let div = (max - min) / n;
    let groups = [max];
    while (groups.length < 5) {
      max -= div;
      groups.push(max);
    }
    return groups;
  }

  private getLogarithmicGroups = (data: any, n = 5) => {
    let max = data.reduce((a: number, b: number) => Math.max(a, b));
    let order = Math.floor(Math.log(max) / Math.LN10 + 0.000000001);
    order = Math.pow(10, order) * 10;
    let groups = [order];
    while (groups.length < 5) {
      order /= 10;
      groups.push(order);
    }
    return groups;
  }

  public reStyleGeoJsonLayer = (color: string) => {
    this.color = color;
    this.map.removeLayer(this.geoJson);
    this.initShapesLayer();
    this.initLegend(this.groups, this.inject.labels[this.dataIndex], this.color);
  }
  public changeScale = (logarithmicScale: boolean) => {
    this.logarithmicScale = logarithmicScale;
    this.map.removeLayer(this.geoJson);
    this.setGroups();
    this.initShapesLayer();
    this.initLegend(this.groups, this.inject.labels[this.dataIndex], this.color);
  }

  public changeLegend = (legendPosition: string) => {
    // this.map.removeLayer(this.geoJson);
    this.map.removeControl(this.legend);
    // this.setGroups();
    //this.initShapesLayer();
    this.legend = new L.Control({ position: legendPosition });
    this.initLegend(this.groups, this.inject.labels[this.dataIndex], this.color);
  }

  public openLinkedDashboard(value: string) {
    if (this.inject.linkedDashboard) {
      const props = this.inject.linkedDashboard;
      const url = window.location.href.substr(0, window.location.href.indexOf('/dashboard')) + `/dashboard/${props.dashboardID}?${props.table}.${props.col}=${value}`
      window.open(url, "_blank");
    }
  }

  /**
   * Color shade, credit to https://stackoverflow.com/users/2012407/antoni :)
   * @param col 
   * @param amt 
   */
  private colorShade = (col, amt) => {
    col = col.replace(/^#/, '')
    if (col.length === 3) col = col[0] + col[0] + col[1] + col[1] + col[2] + col[2]

    let [r, g, b] = col.match(/.{2}/g);
    ([r, g, b] = [parseInt(r, 16) + amt, parseInt(g, 16) + amt, parseInt(b, 16) + amt])

    r = Math.max(Math.min(255, r), 0).toString(16)
    g = Math.max(Math.min(255, g), 0).toString(16)
    b = Math.max(Math.min(255, b), 0).toString(16)

    const rr = (r.length < 2 ? '0' : '') + r;
    const gg = (g.length < 2 ? '0' : '') + g;
    const bb = (b.length < 2 ? '0' : '') + b;
    return `#${rr}${gg}${bb}`
  }

  private initLegend = (groups: Array<number>, label: string, color: string): void => {
    let me = this;
    label = me._sanitizer.sanitize(SecurityContext.HTML, label)
    this.legend.onAdd = function (map) {
      var div = L.DomUtil.create("div", "legend");
      L.DomUtil.addClass(div, 'map-legend');
      div.style.backgroundColor = "#ffffff38";
      div.style.borderRadius = "5%";
      div.innerHTML += `<h6 style="padding : 5px; padding-top:10px; padding-bottom:0px;font-weight:bold">
                        ${(label.charAt(0).toUpperCase() + label.slice(1)).replace(new RegExp('_', 'g'), ' ')} </h6>`;
      var div2 = L.DomUtil.create("div", "innerlegend", div);
      div2.style.padding = "3px";
      div2.style.textAlign = "left";
      div2.style.lineHeight = "1";
      let g = [...groups]; g.push(0);
      for (let i = g.length - 1; i > 0; i--) {
        let shade = i === 0 ? -80 : i === 1 ? -40 : i === 2 ? 0 : i === 3 ? 40 : 80;
        div2.innerHTML += `<span class="circle" style="color: ${me.colorShade(color, shade)}">
                          </span><span>&nbsp ${new Intl.NumberFormat("de-DE").format(Math.floor(g[i]))} - 
                          ${new Intl.NumberFormat("de-DE").format(Math.floor(g[i - 1]))}</span><br>`;
      }
      return div
    };

    this.legend.addTo(this.map);
  }

}
Example #12
Source File: team-advance.page.ts    From fyle-mobile-app with MIT License 4 votes vote down vote up
@Component({
  selector: 'app-team-advance',
  templateUrl: './team-advance.page.html',
  styleUrls: ['./team-advance.page.scss'],
})
export class TeamAdvancePage implements AfterViewChecked {
  teamAdvancerequests$: Observable<any[]>;

  loadData$: Subject<{
    pageNumber: number;
    state: AdvancesStates[];
    sortParam: SortingParam;
    sortDir: SortingDirection;
  }> = new Subject();

  count$: Observable<number>;

  totalTaskCount = 0;

  currentPageNumber = 1;

  isInfiniteScrollRequired$: Observable<boolean>;

  filters: Filters;

  filterPills = [];

  isLoading = false;

  projectFieldName = 'Project';

  constructor(
    private advanceRequestService: AdvanceRequestService,
    private tasksService: TasksService,
    private trackingService: TrackingService,
    private cdRef: ChangeDetectorRef,
    private router: Router,
    private filtersHelperService: FiltersHelperService,
    private offlineService: OfflineService,
    private titleCasePipe: TitleCasePipe
  ) {}

  ionViewWillEnter() {
    this.tasksService.getTotalTaskCount().subscribe((totalTaskCount) => (this.totalTaskCount = totalTaskCount));

    this.setupDefaultFilters();
    this.currentPageNumber = 1;
    this.isLoading = true;

    this.teamAdvancerequests$ = this.loadData$.pipe(
      concatMap(({ pageNumber, state, sortParam, sortDir }) =>
        this.advanceRequestService.getTeamAdvanceRequests({
          offset: (pageNumber - 1) * 10,
          limit: 10,
          queryParams: {
            ...this.getExtraParams(state),
          },
          filter: {
            state,
            sortParam,
            sortDir,
          },
        })
      ),
      map((res) => res.data),
      scan((acc, curr) => {
        if (this.currentPageNumber === 1) {
          return curr;
        }
        return acc.concat(curr);
      }, [] as ExtendedAdvanceRequest[]),
      shareReplay(1),
      finalize(() => (this.isLoading = false))
    );

    this.count$ = this.loadData$.pipe(
      switchMap(({ state, sortParam, sortDir }) =>
        this.advanceRequestService.getTeamAdvanceRequestsCount(
          {
            ...this.getExtraParams(state),
          },
          {
            state,
            sortParam,
            sortDir,
          }
        )
      ),
      shareReplay(1),
      finalize(() => (this.isLoading = false))
    );

    this.isInfiniteScrollRequired$ = this.teamAdvancerequests$.pipe(
      concatMap((teamAdvancerequests) =>
        this.count$.pipe(
          take(1),
          map((count) => count > teamAdvancerequests.length)
        )
      )
    );

    this.loadData$.subscribe(noop);
    this.teamAdvancerequests$.subscribe(noop);
    this.count$.subscribe(noop);
    this.isInfiniteScrollRequired$.subscribe(noop);
    this.loadData$.next({
      pageNumber: this.currentPageNumber,
      state: this.filters.state || [],
      sortParam: this.filters.sortParam,
      sortDir: this.filters.sortDir,
    });

    this.getAndUpdateProjectName();
  }

  ngAfterViewChecked() {
    this.cdRef.detectChanges();
  }

  onAdvanceClick(areq: ExtendedAdvanceRequest) {
    this.router.navigate(['/', 'enterprise', 'view_team_advance', { id: areq.areq_id }]);
  }

  changeState(event?, incrementPageNumber = false) {
    this.currentPageNumber = incrementPageNumber ? this.currentPageNumber + 1 : 1;
    this.advanceRequestService.destroyAdvanceRequestsCacheBuster().subscribe(() => {
      this.loadData$.next({
        pageNumber: this.currentPageNumber,
        state: this.filters.state || [],
        sortParam: this.filters.sortParam,
        sortDir: this.filters.sortDir,
      });
    });
    if (event) {
      event.target.complete();
    }
  }

  getAndUpdateProjectName() {
    this.offlineService.getAllEnabledExpenseFields().subscribe((expenseFields) => {
      const projectField = expenseFields.find((expenseField) => expenseField.column_name === 'project_id');
      this.projectFieldName = projectField?.field_name;
    });
  }

  async openFilters(activeFilterInitialName?: string) {
    const filterOptions = [
      {
        name: 'State',
        optionType: FilterOptionType.multiselect,
        options: [
          {
            label: 'Approval Pending',
            value: AdvancesStates.pending,
          },
          {
            label: 'Approved',
            value: AdvancesStates.approved,
          },
        ],
      } as FilterOptions<string>,
      {
        name: 'Sort By',
        optionType: FilterOptionType.singleselect,
        options: [
          {
            label: 'Requested On - New to Old',
            value: SortingValue.creationDateAsc,
          },
          {
            label: 'Requested On - Old to New',
            value: SortingValue.creationDateDesc,
          },
          {
            label: `${this.titleCasePipe.transform(this.projectFieldName)} - A to Z`,
            value: SortingValue.projectAsc,
          },
          {
            label: `${this.titleCasePipe.transform(this.projectFieldName)} - Z to A`,
            value: SortingValue.projectDesc,
          },
        ],
      } as FilterOptions<string>,
    ];

    const filters = await this.filtersHelperService.openFilterModal(
      this.filters,
      filterOptions,
      activeFilterInitialName
    );

    if (filters) {
      this.filters = filters;
      this.filterPills = this.filtersHelperService.generateFilterPills(this.filters, this.projectFieldName);
      this.changeState();
    }
  }

  onFilterClose(filterType: string) {
    if (filterType === 'sort') {
      this.filters = {
        ...this.filters,
        sortParam: null,
        sortDir: null,
      };
    } else if (filterType === 'state') {
      this.filters = {
        ...this.filters,
        state: null,
      };
    }
    this.filterPills = this.filtersHelperService.generateFilterPills(this.filters);
    this.changeState();
  }

  async onFilterClick(filterType: string) {
    const filterTypes = {
      state: 'State',
      sort: 'Sort By',
    };
    await this.openFilters(filterTypes[filterType]);
  }

  onFilterPillsClearAll() {
    this.filters = {};
    this.filterPills = this.filtersHelperService.generateFilterPills(this.filters);
    this.changeState();
  }

  setupDefaultFilters() {
    this.filters = {
      state: [AdvancesStates.pending],
    };
    this.filterPills = this.filtersHelperService.generateFilterPills(this.filters);
  }

  getExtraParams(state: AdvancesStates[]) {
    const isPending = state.includes(AdvancesStates.pending);
    const isApproved = state.includes(AdvancesStates.approved);
    let extraParams;

    if (isPending && isApproved) {
      extraParams = {
        areq_state: ['not.eq.DRAFT'],
        areq_approval_state: ['ov.{APPROVAL_PENDING,APPROVAL_DONE}'],
        or: ['(areq_is_sent_back.is.null,areq_is_sent_back.is.false)'],
      };
    } else if (isPending) {
      extraParams = {
        areq_state: ['eq.APPROVAL_PENDING'],
        or: ['(areq_is_sent_back.is.null,areq_is_sent_back.is.false)'],
      };
    } else if (isApproved) {
      extraParams = {
        areq_approval_state: ['ov.{APPROVAL_PENDING,APPROVAL_DONE}'],
      };
    } else {
      extraParams = {
        areq_approval_state: ['ov.{APPROVAL_PENDING,APPROVAL_DONE,APPROVAL_REJECTED}'],
      };
    }

    return extraParams;
  }

  onHomeClicked() {
    const queryParams: Params = { state: 'home' };
    this.router.navigate(['/', 'enterprise', 'my_dashboard'], {
      queryParams,
    });
  }

  onTaskClicked() {
    const queryParams: Params = { state: 'tasks' };
    this.router.navigate(['/', 'enterprise', 'my_dashboard'], {
      queryParams,
    });
    this.trackingService.tasksPageOpened({
      Asset: 'Mobile',
      from: 'Team Advances',
    });
  }

  onCameraClicked() {
    this.router.navigate(['/', 'enterprise', 'camera_overlay', { navigate_back: true }]);
  }
}
Example #13
Source File: cytoscape-style-tool.component.ts    From cytoscape-angular with MIT License 4 votes vote down vote up
@Component({
  selector: 'cytoscape-style-tool',
  template: `
    <div>
      <div style="display: flex;">
        <h3 class="style-header">Edit Styles</h3>
      </div>
      <div class="selectors-container">
        <button class="add-button" pButton label="&nbsp;&nbsp;Add&nbsp;&nbsp;"
                [disabled]="!enableAdd" (click)="onAddSelector()"></button>
        <span>&nbsp;&nbsp;</span>
        <label for="selectorDropDown"><a href="https://js.cytoscape.org/#selectors">Selectors</a></label>
        <p-autoComplete #selectorDropDown id="selectorDropDown" class="selector-drop-down"
                        placeholder="Selector"
                        [(ngModel)]="selectedStyleSheet"
                        [suggestions]="selectors"
                        field="selector"
                        dataKey="selector"
                        completeOnFocus="true"
                        dropdown="true"
                        autofocus="true"
                        [style]="{'width':'70%'}"
                        [inputStyle]="{'width':'70%'}"
                        (completeMethod)="search($event)"
                        (ngModelChange)="onSelectorModelChange($event)">
        </p-autoComplete>
      </div>
    </div>
    <hr>
    <div class="apply-div">
      <button class="apply-button" pButton label="Apply" [disabled]="!changed" (click)="onApplyStyle()"></button>
      <span class="selector-span">Selector </span><span>{{selectedStyleSheet.selector}}</span>
    </div>
    <ng-container *ngIf="!selectedStyleSheet">
      <div> Please select a <a href="https://js.cytoscape.org/#selectors">selector</a> above or type a selector name
            and click "Add" to create a new stylesheet for that selector.</div>
    </ng-container>
    <ng-container *ngIf="selectedStyleSheet?.selector?.startsWith('node')">
      <cyng-fluid-form [model]="selectedStyleSheet.style"
                       [formInfo]="nodeFormInfo"
                       (modelChange)="onFormModelChange()">
      </cyng-fluid-form>
    </ng-container>
    <ng-container *ngIf="selectedStyleSheet?.selector?.startsWith('edge')">
      <cyng-fluid-form [model]="selectedStyleSheet.style"
                       [formInfo]="edgeFormInfo"
                       (modelChange)="onFormModelChange()">
      </cyng-fluid-form>
    </ng-container>
    <ng-container *ngIf="selectedStyleSheet?.selector?.startsWith('core')">
      <cyng-fluid-form [model]="selectedStyleSheet.style"
                       [formInfo]="coreFormInfo"
                       (modelChange)="onFormModelChange()">
      </cyng-fluid-form>
    </ng-container>
  `,
  styles: [`
    .selectors-container {
      display: flex;
      align-items: baseline;
      flex-wrap: nowrap;
      flex-grow: 0;
    }

    label[for="selectorDropDown"] {
      font-size: 125%;
      font-weight: bold;
    }

    .selector-drop-down {
      flex-grow: 1;
      padding: 10px;
    }

    .selector-span {
      padding: 10px;
      font-size: 125%;
      font-weight: bold;
    }
    .add-button {

    }
    .apply-button {
    }
  `]
})
export class CytoscapeStyleToolComponent implements OnInit, OnChanges, AfterViewInit, AfterViewChecked {
  @ViewChild('styleForm') styleForm;
  @ViewChild('selectorDropDown') selectorDropDown;

  _styles: StylesheetStyle[]
  enableAdd = false
  private lastValidSelectorModelText: string

  @Input()
  get styles() : StylesheetStyle[] {
    return this._styles
  }
  set styles(styles: StylesheetStyle[]) {
    this._styles = styles
  }
  @Output()
  stylesChange:EventEmitter<StylesheetStyle[]> = new EventEmitter<StylesheetStyle[]>()

  @Output()
  styleSelectorChange: EventEmitter<string> = new EventEmitter<string>()

  nodeFormInfo: FormInfo
  edgeFormInfo: FormInfo
  coreFormInfo: FormInfo

  selectedStyleSheet: StylesheetStyle
  selectors: StylesheetStyle[]
  changed = false

  constructor() {
  }

  ngOnInit(): void {
    this.coreFormInfo = createStyleCoreFormInfo()
    this.nodeFormInfo = new FormInfo('Node', createStyleNodeFieldSets())
    this.edgeFormInfo = new FormInfo('Edge', createStyleEdgeFieldSets())
    if (!this.styles) {
      this.styles = [new StylesheetImpl()]
    }
    this.setSelectorsFromStyles(null)
    this.selectedStyleSheet = this.selectors[0]
    console.log(`this.selectors.length:`, this.selectors.length)
  }

  ngOnChanges(changes: SimpleChanges): void {
    console.log('ngOnChanges style changes:', JSON.stringify(changes))
    if (changes['styles']) {
      this.setSelectorsFromStyles(null)
      this.selectedStyleSheet = this.selectors[0]
      console.log(`styles updated this.selectors.length:`, this.selectors.length)
    }
  }

  ngAfterViewInit(): void {
    // console.debug("ngAfterViewInit")
  }

  ngAfterViewChecked(): void {
    // console.debug("ngAfterViewChecked")
  }

  onFormModelChange() {
    console.log('onFormModelChange')
    this.changed = true
  }

  onApplyStyle() {
    this.changed = false
    this.stylesChange.emit(this.styles)
  }

  search(event) {
    let searchString = event.query
    this.setSelectorsFromStyles(searchString)
  }

  private setSelectorsFromStyles(searchString: string) {
    this.selectors = this.styles.filter((stylesheet: StylesheetStyle) => {
      return searchString ? stylesheet.selector.includes(searchString) : true
    })
  }


  onAddSelector() {
    const newStylesheetStyle: StylesheetStyle = new StylesheetImpl()
    newStylesheetStyle.selector = this.lastValidSelectorModelText
    console.log('Adding new style with selector:', this.lastValidSelectorModelText)
    this.styles.unshift(newStylesheetStyle)
    this.selectedStyleSheet = newStylesheetStyle
  }

  /*
   * The param can be a selector object when the user selects a stylesheet entry from the dropdown and changes the
   * selection (which will fire a selection change to let the graph, say, focus on the selected node).
   * or the param is text if the user is just typing in the field, which doens't change the selector untl the user
   * clicks Add.
   */
  onSelectorModelChange(param: any) {
    console.log(`selectorModelChanged:${JSON.stringify(param)}`)
    const selector = param.selector ? param.selector : param
    const stylesheet = param.selector ? param : this.getStylesheetForSelector(selector)
    if (stylesheet) {
      if (this.changed) {
        this.onApplyStyle()
      }
      this.styleSelectorChange.emit(selector)
    } else {
      this.enableAdd =  this.isValidSelector(selector)
      if (this.enableAdd) {
        this.lastValidSelectorModelText = selector
      } else {
        this.lastValidSelectorModelText = null
      }
    }
  }

  getStylesheetForSelector(selectorName): Stylesheet {
    this.selectors.forEach(selector => {
      if (selector.selector === selectorName) {
        return selector
      }
    })
    return null
  }

  isValidSelector(text: string) {
    console.log('isValidSelector:', text)
    if (text?.startsWith('node') || text?.startsWith('edge') || text?.startsWith('core')) {
      const openBracket = text.indexOf('[')
      if (openBracket > -1) {
        if (text.indexOf(']') < openBracket) {
          return false
        }
      }
      const openQuote = text.indexOf('\'')
      if (openQuote > -1) {
        let closeQuote = text.indexOf('\'', openQuote +1)
        if (closeQuote < openQuote) {
          return false
        }
      }
      return true
    }
    return false
  }
}
Example #14
Source File: fluid-form.component.ts    From cytoscape-angular with MIT License 4 votes vote down vote up
@Component({
  selector: 'cyng-fluid-form',
  template: `
    <form [formGroup]="formGroup" [title]="formInfo?.title" (ngSubmit)="onSubmit()">
      <ng-container *ngFor="let fieldSetInfo of formInfo?.fieldsets">
        <p-fieldset *ngIf="fieldSetInfo.showFieldsetForModel(model)" class="fieldset" legend="{{fieldSetInfo.legend}}">
          <div class="ui-g ui-fluid">
            <div class="ui-g-12 ui-md-4 field" *ngFor="let fieldInfo of fieldSetInfo.fieldInfos">
              <div class="ui-inputgroup">
                <ng-container *ngIf="fieldInfo.fieldType(model) === 'boolean'">
                  <span class="ui-chkbox-label">
                    {{fieldInfo.label}}
                  </span>
                  <p-inputSwitch
                    name="{{fieldInfo.modelProperty}}"
                    pTooltip="{{fieldInfo.tooltip}}"
                    formControlName="{{fieldInfo.modelProperty}}"
                  >
                  </p-inputSwitch>
                </ng-container>
                <ng-container
                  *ngIf="fieldInfo.fieldType(model) === 'string' || fieldInfo.fieldType(model) === 'number'">
                  <span class="ui-float-label">
                    <input pInputText
                           id="{{fieldInfo.modelProperty}}"
                           name="{{fieldInfo.modelProperty}}"
                           formControlName="{{fieldInfo.modelProperty}}"
                           [pTooltip]="fieldInfo.tooltip"
                           [type]="fieldInfo.inputType"
                           [size]="fieldInfo.inputSize"
                    />
                    <label for="{{fieldInfo.modelProperty}}">{{fieldInfo.label}}</label>
                  </span>
                </ng-container>
                <ng-container *ngIf="fieldInfo.fieldType(model) === 'options'">
                  <span class="ui-float-label">
                    <p-dropdown
                      formControlName="{{fieldInfo.modelProperty}}"
                      [name]="fieldInfo.modelProperty"
                      [options]="fieldInfo.options"
                      [optionLabel]="fieldInfo.optionArrayLabelField"
                      [pTooltip]="fieldInfo.tooltip"
                    ></p-dropdown>
                    <label for="{{fieldInfo.modelProperty}}">{{fieldInfo.label}}</label>
                  </span>
                </ng-container>
              </div>
            </div>
          </div>
        </p-fieldset>
      </ng-container>
    </form>
    <button *ngIf="formInfo.showSubmitButton" pButton
            [disabled]="formInfo.disableSubmitOnFormInvalid && !formGroup.valid"
            (submit)="onSubmit()">{{formInfo.submitText || 'Submit' }}</button>
  `,
  styles: [`
    .ui-chkbox-label {
      padding-right: 0.5em;
    }

    .ui-dropdown-label {
      align-self: center;
      padding-right: 0.5em
    }

    .field:nth-child(n+4) {
        margin-top: 1em; // otherwise overlap betwen a field and a floating label of the field below it
    }

    .fieldset {
    }
  `]
})
export class FluidFormComponent implements OnInit, OnChanges, AfterViewInit, AfterViewChecked {
  @Input()
  model: object
  @Output()
  modelChange: EventEmitter<object> = new EventEmitter<object>()

  @Input()
  modelProperty: string
  @Input()
  formInfo: FormInfo

  formGroup: FormGroup

  constructor() {
  }

  ngOnInit(): void {
    console.debug('FluidFormComponent this.formInfo:', JSON.stringify(this.formInfo))
    let controls = {}
    this.formInfo.fieldsets.forEach(fieldsetInfo => {
      fieldsetInfo.fieldInfos.forEach(fieldInfo => {
        let modelValue = this.model[fieldInfo.modelProperty]
        // console.log('fieldInfo.modelProperty:', fieldInfo.modelProperty, ', modelValue:', modelValue)
        const validators: ValidatorFn[] = typeof fieldInfo.validators === 'function' ? fieldInfo.validators() : fieldInfo.validators
        const asyncValidators: AsyncValidatorFn[] = typeof fieldInfo.asyncValidators === 'function' ? fieldInfo.asyncValidators() : fieldInfo.asyncValidators
        const { updateOn } = fieldInfo
        let formControl = new FormControl(modelValue, {validators, asyncValidators, updateOn })
        formControl.valueChanges.subscribe( (change) => {
          console.debug('form control change ', JSON.stringify(change), ' for prop ', fieldInfo.modelProperty,
            ', changing current model value ', this.model[fieldInfo.modelProperty], ' to ', change)
          fieldInfo.setValue(change, this.model, this.modelChange)
        })
        controls[fieldInfo.modelProperty] = formControl
      })
    })
    this.formGroup = new FormGroup(controls)
  }

  ngOnChanges(changes: SimpleChanges): void {
    console.debug('ngOnChanges fluid-form changes:', JSON.stringify(changes))
    if (changes['model']) {
      const model = changes['model'].currentValue
      for (let key of Object.keys(model)) {
        console.debug('ngOnChanges model key copying to form:', key)
        const control = this.formGroup?.controls[key]
        control ? control.setValue(model[key], { emitEvent: false }) : console.warn('no control for model key ', key)
      }
    }
  }

  ngAfterViewInit(): void {
    // console.debug("ngAfterViewInit")
  }

  ngAfterViewChecked(): void {
    // console.debug("ngAfterViewChecked")
  }

  onSubmit() {
    console.log(`Form submitted`)
  }
}
Example #15
Source File: app-sidenav.component.ts    From youpez-admin with MIT License 4 votes vote down vote up
@Component({
  selector: 'youpez-sidenav',
  templateUrl: './app-sidenav.component.html',
  styleUrls: ['./app-sidenav.component.scss'],
})
export class AppSidenavComponent implements OnInit, OnDestroy, AfterContentInit, OnChanges, AfterViewInit, AfterViewChecked {

  @Input('opened') originalOpened: boolean
  @Input() direction: DirectionType = 'left'
  @Input() size: SizeType = 'md'
  @Input('mode') originalMode: ModeType = 'over'
  @Input() breakpoint: string = ''
  @Input() transparent: boolean = false
  @Input() toggleableBtn: boolean = false
  @Input() toggleableBtnAlwaysVisible: boolean = false
  @Input('hoverAble') originalHoverAble: boolean = false
  @Input() hoverAbleBreakpoint: string = 'md'
  @Input() optionalClass: string = ''
  @Input() initWidth: string = ''
  @Input() hoverDelay: number = 100
  @Input() minDimension: number = null

  @Output() open: EventEmitter<any> = new EventEmitter<any>()
  @Output() close: EventEmitter<any> = new EventEmitter<any>()
  @Output() init: EventEmitter<any> = new EventEmitter<any>()
  @Output() change: EventEmitter<any> = new EventEmitter<any>()
  @Output() resizeEnd: EventEmitter<any> = new EventEmitter<any>()
  @Output() resizing: EventEmitter<any> = new EventEmitter<any>()
  @Output() visibleChange: EventEmitter<any> = new EventEmitter<any>()

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

  public rendered: boolean = false
  public width: string = ''
  public height: string = ''
  public mode: string = 'over'
  public hoverEvent: boolean = false
  public hoverAble: boolean = false
  public opened: boolean = false
  public openedMemo:boolean = null
  public isMouseEntered: boolean = false
  public resizeEdges = {
    bottom: false,
    right: false,
    top: false,
    left: false,
  }

  private lock: boolean = false
  private hoverTimeout = null
  private breakpointSub: Subscription
  private dimensions = {
    width: {
      docs: '170',
      xsm: '200',
      sm: '300',
      md: '300',
      lg: '500',
      xl: '800',
      custom1: '260',
      sideBar1: '240',
      sideBar2: '300',
      mail: '380',
      mini: '100',
    },
    height: {},
  }

  constructor(public element: ElementRef,
              private appBreakpointService: AppBreakpointService,
              private appSidenavService: AppSidenavService,) {
  }

  ngOnInit() {
    this.firstInit()
    //CURRENTLY: https://github.com/angular/flex-layout/issues/1059
    this.breakpointSub = this.appBreakpointService.$windowWidth
      .subscribe((width: any) => {
        if (!width) {
          return
        }
        if (this.breakpoint && this.breakpoint === 'md') {
          if (width <= 960) {
            this.mode = 'over'
            this.hoverAble = false
            this.opened = false
            this.openedMemo = this.originalOpened

            setTimeout(() => {
              this.visibleChange.emit(false)
            }, 1)
          }
          else {
            this.mode = this.originalMode
            this.hoverAble = this.originalHoverAble
            this.opened = this.openedMemo || this.originalOpened

            setTimeout(() => {
              this.visibleChange.emit(this.openedMemo || this.originalOpened)
            }, 1)
          }
          this.change.emit()
        }
      })
  }

  private firstInit() {
    let {initWidth, size, originalMode, originalHoverAble, originalOpened, direction} = this

    if (initWidth) {
      this.setWidth(initWidth)
      this.setHeight(initWidth)
    }
    else {
      this.setWidth(this.dimensions.width[size])
      this.setHeight(this.dimensions.width[size])
    }

    this.mode = originalMode
    this.hoverAble = originalHoverAble
    this.opened = originalOpened

    if (originalMode === 'side') {
      if (direction === 'right') {
        this.resizeEdges.left = true
      }
      if (direction === 'left') {
        this.resizeEdges.right = true
      }
      if (direction === 'top') {
        this.resizeEdges.bottom = true
      }
      if (direction === 'bottom') {
        this.resizeEdges.top = true
      }
    }
  }

  ngOnChanges(changes: SimpleChanges): void {
    const opened: SimpleChange = changes.originalOpened

    if (opened !== undefined && opened.previousValue !== undefined) {
      const currentOpened = opened.currentValue
      if (currentOpened) {
        this.onOpen()
      }
      else {
        this.onClose()
      }
    }
  }

  ngOnDestroy(): void {
    if (this.breakpointSub) {
      this.breakpointSub.unsubscribe()
    }
  }

  ngAfterContentInit() {
    if (this.originalMode !== 'side') {
      setTimeout(() => {
        this.rendered = true
      }, 450)
    }
    else {
      this.rendered = true
    }
  }

  ngAfterViewInit() {

  }

  ngAfterViewChecked() {

  }

  setWidth(width) {
    this.width = width
    if (Number(this.width) <= 200) {
      this.onClose()
      setTimeout(() => {
        this.width = '200'
      }, 1000)
    }
  }

  setHeight(height) {
    this.height = height
  }

  sendChange(changes = null) {
    this.change.emit()
  }

  setPanelStyle(panel: PanelType) {
    let panelCss = ''
    if (panel === 'panel') {
      panelCss = 'app-sidenav-v2--panel'
    }
    if (panel === 'solid') {
      panelCss = 'app-sidenav-v2--solid-border'
    }
    this.optionalClass = this.optionalClass + ' ' + panelCss
  }

  getWidth() {
    return this.width
  }

  getMainCSSclass() {
    return `app-sidenav-v2 ${this.optionalClass}`
  }

  getHeight() {
    return this.height
  }

  getMinDimension() {
    return this.minDimension
  }

  onClose() {
    this.opened = false
    this.visibleChange.emit(false)
    this.close.emit()
  }

  onOpen() {
    this.opened = true
    this.visibleChange.emit(true)
    this.open.emit()
  }

  onToggle() {
    if (this.opened) {
      this.onClose()
      this.onMouseLeave('')
    }
    else {
      this.onOpen()
    }
  }

  isOpened() {
    return this.opened
  }

  emitInitForParent() {
    this.init.emit()
  }

  onForceHover(bool = true) {
    if (this.hoverAble) {
      if (!this.isMouseEntered) {
        this.hoverEvent = bool
      }
    }
    else {
      this.onOpen()
    }
  }

  onMouseEnter(event) {
    this.isMouseEntered = true
    if (this.hoverTimeout) {
      clearTimeout(this.hoverTimeout)
    }
    this.hoverTimeout = setTimeout(() => {
      const element = event.target
      if (element.classList.contains('app-sidenav-v2__inner')) {
        this.hoverEvent = true
      }
    }, this.hoverDelay)
  }

  onMouseLeave(event) {
    if (this.lock) {
      return
    }
    this.isMouseEntered = false
    if (this.hoverTimeout) {
      clearTimeout(this.hoverTimeout)
    }
    this.hoverEvent = false
  }

  onResizeEnd(event: ResizeEvent) {
    const {edges, rectangle} = event
    const {right, left, bottom, top} = edges
    const {width, height} = rectangle

    const calcWidth = Number(width)
    const calcHeight = Number(height)

    this.setWidth(calcWidth)
    this.setHeight(calcHeight)

    this.sendChange()


    if (this.direction === 'top' || this.direction === 'bottom') {
      this.resizeEnd.next(calcHeight)
    }
    else {
      this.resizeEnd.next(calcWidth)
    }

    this.lock = false
  }

  onResize(event: ResizeEvent) {
    const {edges, rectangle} = event
    const {right, left, bottom, top} = edges
    const {width, height} = rectangle

    const calcWidth = Number(width)
    const calcHeight = Number(height)

    this.setWidth(calcWidth)
    this.setHeight(calcHeight)

    this.sendChange({windowResize: false})

    if (this.direction === 'top' || this.direction === 'bottom') {
      this.resizing.next(calcHeight)
    }
    else {
      this.resizing.next(calcWidth)
    }
  }

  onResizeStart(event: ResizeEvent) {
    this.onForceHover(true)
    this.lock = true
  }

  onSetDefaultWidth(event) {
    this.setWidth(this.dimensions.width[this.size])
  }
}
Example #16
Source File: app-sidenav-container.component.ts    From youpez-admin with MIT License 4 votes vote down vote up
@Component({
  selector: 'youpez-sidenav-container',
  templateUrl: './app-sidenav-container.component.html',
  styleUrls: ['./app-sidenav-container.component.scss'],
})
export class AppSidenavContainerComponent implements OnInit, OnDestroy, AfterContentInit, AfterViewChecked {

  @ContentChildren(AppSidenavComponent) sidenavs: QueryList<AppSidenavComponent>

  @Input() overlayOpacity: number = 1
  @Input() panel: PanelType = ''

  public overlayVisible: boolean = false
  public sideMode: boolean = false

  public contentMargins: any = {
    top: '0',
    right: '0',
    left: '0',
    bottom: '0',
  }

  private sideNavOpenEventSub: Subscription
  private sideNavCloseEventSub: Subscription
  private sideNavChangeEventSub: Subscription

  constructor(public element: ElementRef,
              private windowRefService: WindowRefService) {
  }

  ngOnInit() {

  }

  ngAfterContentInit() {
    this.sidenavs.forEach((sidenav: AppSidenavComponent) => {
      this.sideNavOpenEventSub = sidenav.open.subscribe((changes) => {
        this.setContent(sidenav, changes)
      })
      this.sideNavCloseEventSub = sidenav.close.subscribe((changes) => {
        this.setContent(sidenav, changes)
      })
      this.sideNavChangeEventSub = sidenav.change.subscribe((changes) => {
        this.setContent(sidenav, changes)
      })
      this.setContent(sidenav)
      this.setStyleClass(sidenav)
    })
  }

  ngAfterViewChecked(): void {
  }

  ngOnDestroy(): void {
    if (this.sideNavOpenEventSub) {
      this.sideNavOpenEventSub.unsubscribe()
    }
    if (this.sideNavCloseEventSub) {
      this.sideNavCloseEventSub.unsubscribe()
    }
    if (this.sideNavChangeEventSub) {
      this.sideNavChangeEventSub.unsubscribe()
    }
  }

  setStyleClass(sidenav: AppSidenavComponent) {
    sidenav.setPanelStyle(this.panel)
  }

  setContent(sidenav: AppSidenavComponent, options = null) {
    const mode = sidenav.mode
    const direction = sidenav.direction
    const opened = sidenav.opened
    const hoverAble = sidenav.hoverAble
    const minDimension = sidenav.getMinDimension() || 20
    const calcWidthMargin = `${opened ? sidenav.getWidth() : (hoverAble ? minDimension : 0)}px`
    const calcHeightMargin = `${opened ? sidenav.getHeight() : (hoverAble ? minDimension : 0)}px`

    this.toggleOverlay(sidenav.opened, sidenav.mode)

    if (direction === 'right') {
      if (mode === 'side') {
        this.contentMargins.right = calcWidthMargin
      }
      else {
        this.contentMargins.right = `0px`
      }
    }
    if (direction === 'left') {
      if (mode === 'side') {
        this.contentMargins.left = calcWidthMargin
      }
      else {
        this.contentMargins.left = `0px`
      }
    }
    if (direction === 'top') {
      if (mode === 'side') {
        this.contentMargins.top = calcHeightMargin
      }
      else {
        this.contentMargins.top = `0px`
      }
    }
    if (direction === 'bottom') {
      if (mode === 'side') {
        this.contentMargins.bottom = calcHeightMargin
      }
      else {
        this.contentMargins.bottom = `0px`
      }
    }

    if (options) {
      if (options.windowResize !== false) {
        this.windowRefService.callWindowResize()
      }
    }
    else {
      this.windowRefService.callWindowResize()
    }
  }

  toggleOverlay(bool, mode) {
    this.sideMode = (mode === 'side')
    this.overlayVisible = bool
  }

  onCloseAll() {
    this.sidenavs.forEach((sidenav: AppSidenavComponent) => {
      sidenav.onClose()
    })
  }

}
Example #17
Source File: slick.component.ts    From flingo with MIT License 4 votes vote down vote up
/**
 * Slick component
 */
@Component({
    selector: 'roi-slick-carousel',
    exportAs: 'slick-carousel',
    template: '<ng-content></ng-content>'
})
export class SlickCarouselComponent implements OnDestroy, OnChanges, AfterViewChecked {
    @Input() config: any;
    @Output() afterChange: EventEmitter<{
        event: any;
        slick: any;
        currentSlide: number;
        first: boolean;
        last: boolean;
    }> = new EventEmitter();
    @Output() beforeChange: EventEmitter<{
        event: any;
        slick: any;
        currentSlide: number;
        nextSlide: number;
    }> = new EventEmitter();
    @Output() breakpoint: EventEmitter<{ event: any; slick: any; breakpoint: any }> = new EventEmitter();
    @Output() destroy: EventEmitter<{ event: any; slick: any }> = new EventEmitter();
    @Output() init: EventEmitter<{ event: any; slick: any }> = new EventEmitter();

    public slick: Slicker;

    // access from parent component can be a problem with change detection timing. Please use afterChange output
    private currentIndex = 0;

    public slides: any[] = [];
    public initialized = false;
    private _removedSlides: SlickItemDirective[] = [];
    private _addedSlides: SlickItemDirective[] = [];

    /**
     * Constructor
     */
    constructor(private el: ElementRef, @Inject(PLATFORM_ID) private platformId: string) {}

    /**
     * On component destroy
     */
    ngOnDestroy() {
        this.unslick();
    }

    /**
     * On component view checked
     */
    ngAfterViewChecked() {
        if (isPlatformServer(this.platformId)) {
            return;
        }
        if (this._addedSlides.length > 0 || this._removedSlides.length > 0) {
            const nextSlidesLength = this.slides.length - this._removedSlides.length + this._addedSlides.length;
            if (!this.initialized) {
                if (nextSlidesLength > 0) {
                    this.initSlick();
                }
                // if nextSlidesLength is zere, do nothing
            } else if (nextSlidesLength === 0) {
                // unslick case
                this.unslick();
            } else {
                this._addedSlides.forEach((slickItem) => {
                    this.slides.push(slickItem);

                    this.slick.addSlide(slickItem.el.nativeElement);
                });
                this._addedSlides = [];

                this._removedSlides.forEach((slickItem) => {
                    const idx = this.slides.indexOf(slickItem);
                    this.slides = this.slides.filter((s) => s !== slickItem);

                    this.slick.removeSlide(idx);
                });
                this._removedSlides = [];
            }
        }
    }

    /**
     * init slick
     */
    initSlick() {
        this.slides = this._addedSlides;
        this._addedSlides = [];
        this._removedSlides = [];

        this.slick = new Slicker(this.el.nativeElement, this.config);
        this.initialized = true;

        this.slick.$slider.addEventListener('afterChange', (a, b, c) => {
            if (a) {
            }
        });

        this.slick.$slider.addEventListener('init', (a, b, c) => {
            if (a) {
            }
        });
    }

    addSlide(slickItem: SlickItemDirective) {
        this._addedSlides.push(slickItem);
    }

    removeSlide(slickItem: SlickItemDirective) {
        this._removedSlides.push(slickItem);
    }

    public unslick() {
        if (this.slick) {
            this.slick.unslick();
            this.slick = undefined;
        }
        this.initialized = false;
    }

    ngOnChanges(changes: SimpleChanges): void {
        if (this.initialized) {
            const config = changes['config'];
            /*if (config.previousValue !== config.currentValue && config.currentValue !== undefined) {
                const refresh = config.currentValue['refresh'];
                const newOptions = Object.assign({}, config.currentValue);
                delete newOptions['refresh'];

                this.zone.runOutsideAngular(() => {
                    this.$instance.slick('slickSetOption', newOptions, refresh);
                });
            }*/
        }
    }
}
Example #18
Source File: editable.component.ts    From slate-angular with MIT License 4 votes vote down vote up
@Component({
    selector: 'slate-editable',
    host: {
        class: 'slate-editable-container',
        '[attr.contenteditable]': 'readonly ? undefined : true',
        '[attr.role]': `readonly ? undefined : 'textbox'`,
        '[attr.spellCheck]': `!hasBeforeInputSupport ? false : spellCheck`,
        '[attr.autoCorrect]': `!hasBeforeInputSupport ? 'false' : autoCorrect`,
        '[attr.autoCapitalize]': `!hasBeforeInputSupport ? 'false' : autoCapitalize`
    },
    templateUrl: 'editable.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => SlateEditableComponent),
        multi: true
    }]
})
export class SlateEditableComponent implements OnInit, OnChanges, OnDestroy, AfterViewChecked, DoCheck {
    viewContext: SlateViewContext;
    context: SlateChildrenContext;

    private destroy$ = new Subject();

    isComposing = false;
    isDraggingInternally = false;
    isUpdatingSelection = false;
    latestElement = null as DOMElement | null;

    protected manualListeners: (() => void)[] = [];

    private initialized: boolean;

    private onTouchedCallback: () => void = () => { };

    private onChangeCallback: (_: any) => void = () => { };

    @Input() editor: AngularEditor;

    @Input() renderElement: (element: Element) => ViewType | null;

    @Input() renderLeaf: (text: SlateText) => ViewType | null;

    @Input() renderText: (text: SlateText) => ViewType | null;

    @Input() decorate: (entry: NodeEntry) => Range[] = () => [];

    @Input() placeholderDecorate: (editor: Editor) => SlatePlaceholder[];

    @Input() isStrictDecorate: boolean = true;

    @Input() trackBy: (node: Element) => any = () => null;

    @Input() readonly = false;

    @Input() placeholder: string;

    //#region input event handler
    @Input() beforeInput: (event: Event) => void;
    @Input() blur: (event: Event) => void;
    @Input() click: (event: MouseEvent) => void;
    @Input() compositionEnd: (event: CompositionEvent) => void;
    @Input() compositionStart: (event: CompositionEvent) => void;
    @Input() copy: (event: ClipboardEvent) => void;
    @Input() cut: (event: ClipboardEvent) => void;
    @Input() dragOver: (event: DragEvent) => void;
    @Input() dragStart: (event: DragEvent) => void;
    @Input() dragEnd: (event: DragEvent) => void;
    @Input() drop: (event: DragEvent) => void;
    @Input() focus: (event: Event) => void;
    @Input() keydown: (event: KeyboardEvent) => void;
    @Input() paste: (event: ClipboardEvent) => void;
    //#endregion

    //#region DOM attr
    @Input() spellCheck = false;
    @Input() autoCorrect = false;
    @Input() autoCapitalize = false;

    @HostBinding('attr.data-slate-editor') dataSlateEditor = true;
    @HostBinding('attr.data-slate-node') dataSlateNode = 'value';
    @HostBinding('attr.data-gramm') dataGramm = false;

    get hasBeforeInputSupport() {
        return HAS_BEFORE_INPUT_SUPPORT;
    }
    //#endregion

    @ViewChild('templateComponent', { static: true }) templateComponent: SlateStringTemplateComponent;
    @ViewChild('templateComponent', { static: true, read: ElementRef }) templateElementRef: ElementRef<any>;

    constructor(
        public elementRef: ElementRef,
        public renderer2: Renderer2,
        public cdr: ChangeDetectorRef,
        private ngZone: NgZone,
        private injector: Injector
    ) { }

    ngOnInit() {
        this.editor.injector = this.injector;
        this.editor.children = [];
        let window = getDefaultView(this.elementRef.nativeElement);
        EDITOR_TO_WINDOW.set(this.editor, window);
        EDITOR_TO_ELEMENT.set(this.editor, this.elementRef.nativeElement);
        NODE_TO_ELEMENT.set(this.editor, this.elementRef.nativeElement);
        ELEMENT_TO_NODE.set(this.elementRef.nativeElement, this.editor);
        IS_READONLY.set(this.editor, this.readonly);
        EDITOR_TO_ON_CHANGE.set(this.editor, () => {
            this.ngZone.run(() => {
                this.onChange();
            });
        });
        this.ngZone.runOutsideAngular(() => {
            this.initialize();
        });
        this.initializeViewContext();
        this.initializeContext();

        // remove unused DOM, just keep templateComponent instance
        this.templateElementRef.nativeElement.remove();

        // add browser class
        let browserClass = IS_FIREFOX ? 'firefox' : (IS_SAFARI ? 'safari' : '');
        browserClass && this.elementRef.nativeElement.classList.add(browserClass);
    }

    ngOnChanges(simpleChanges: SimpleChanges) {
        if (!this.initialized) {
            return;
        }
        const decorateChange = simpleChanges['decorate'];
        if (decorateChange) {
            this.forceFlush();
        }
        const readonlyChange = simpleChanges['readonly'];
        if (readonlyChange) {
            IS_READONLY.set(this.editor, this.readonly);
            this.detectContext();
            this.toSlateSelection();
        }
    }

    registerOnChange(fn: any) {
        this.onChangeCallback = fn;
    }
    registerOnTouched(fn: any) {
        this.onTouchedCallback = fn;
    }

    writeValue(value: Element[]) {
        if (value && value.length) {
            if (check(value)) {
                this.editor.children = value;
            } else {
                this.editor.onError({
                    code: SlateErrorCode.InvalidValueError,
                    name: 'initialize invalid data',
                    data: value
                });
                this.editor.children = normalize(value);
            }
            this.initializeContext();
            this.cdr.markForCheck();
        }
    }

    initialize() {
        this.initialized = true;
        const window = AngularEditor.getWindow(this.editor);
        this.addEventListener(
            'selectionchange',
            event => {
                this.toSlateSelection();
            },
            window.document
        );
        if (HAS_BEFORE_INPUT_SUPPORT) {
            this.addEventListener('beforeinput', this.onDOMBeforeInput.bind(this));
        }
        this.addEventListener('blur', this.onDOMBlur.bind(this));
        this.addEventListener('click', this.onDOMClick.bind(this));
        this.addEventListener('compositionend', this.onDOMCompositionEnd.bind(this));
        this.addEventListener('compositionstart', this.onDOMCompositionStart.bind(this));
        this.addEventListener('copy', this.onDOMCopy.bind(this));
        this.addEventListener('cut', this.onDOMCut.bind(this));
        this.addEventListener('dragover', this.onDOMDragOver.bind(this));
        this.addEventListener('dragstart', this.onDOMDragStart.bind(this));
        this.addEventListener('dragend', this.onDOMDragEnd.bind(this));
        this.addEventListener('drop', this.onDOMDrop.bind(this));
        this.addEventListener('focus', this.onDOMFocus.bind(this));
        this.addEventListener('keydown', this.onDOMKeydown.bind(this));
        this.addEventListener('paste', this.onDOMPaste.bind(this));
        BEFORE_INPUT_EVENTS.forEach(event => {
            this.addEventListener(event.name, () => { });
        });
    }

    toNativeSelection() {
        try {
            const { selection } = this.editor;
            const root = AngularEditor.findDocumentOrShadowRoot(this.editor)
            const domSelection = (root as Document).getSelection();

            if (this.isComposing || !domSelection || !AngularEditor.isFocused(this.editor)) {
                return;
            }

            const hasDomSelection = domSelection.type !== 'None';

            // If the DOM selection is properly unset, we're done.
            if (!selection && !hasDomSelection) {
                return;
            }

            // If the DOM selection is already correct, we're done.
            // verify that the dom selection is in the editor
            const editorElement = EDITOR_TO_ELEMENT.get(this.editor)!;
            let hasDomSelectionInEditor = false;
            if (editorElement.contains(domSelection.anchorNode) && editorElement.contains(domSelection.focusNode)) {
                hasDomSelectionInEditor = true;
            }

            // If the DOM selection is in the editor and the editor selection is already correct, we're done.
            if (
                hasDomSelection &&
                hasDomSelectionInEditor &&
                selection &&
                hasStringTarget(domSelection) &&
                Range.equals(AngularEditor.toSlateRange(this.editor, domSelection), selection)
            ) {
                return;
            }

            // when <Editable/> is being controlled through external value
            // then its children might just change - DOM responds to it on its own
            // but Slate's value is not being updated through any operation
            // and thus it doesn't transform selection on its own
            if (selection && !AngularEditor.hasRange(this.editor, selection)) {
                this.editor.selection = AngularEditor.toSlateRange(this.editor, domSelection);
                return
            }

            // Otherwise the DOM selection is out of sync, so update it.
            const el = AngularEditor.toDOMNode(this.editor, this.editor);
            this.isUpdatingSelection = true;

            const newDomRange = selection && AngularEditor.toDOMRange(this.editor, selection);

            if (newDomRange) {
                // COMPAT: Since the DOM range has no concept of backwards/forwards
                // we need to check and do the right thing here.
                if (Range.isBackward(selection)) {
                    // eslint-disable-next-line max-len
                    domSelection.setBaseAndExtent(
                        newDomRange.endContainer,
                        newDomRange.endOffset,
                        newDomRange.startContainer,
                        newDomRange.startOffset
                    );
                } else {
                    // eslint-disable-next-line max-len
                    domSelection.setBaseAndExtent(
                        newDomRange.startContainer,
                        newDomRange.startOffset,
                        newDomRange.endContainer,
                        newDomRange.endOffset
                    );
                }
            } else {
                domSelection.removeAllRanges();
            }

            setTimeout(() => {
                // COMPAT: In Firefox, it's not enough to create a range, you also need
                // to focus the contenteditable element too. (2016/11/16)
                if (newDomRange && IS_FIREFOX) {
                    el.focus();
                }

                this.isUpdatingSelection = false;
            });
        } catch (error) {
            this.editor.onError({ code: SlateErrorCode.ToNativeSelectionError, nativeError: error })
        }
    }

    onChange() {
        this.forceFlush();
        this.onChangeCallback(this.editor.children);
    }

    ngAfterViewChecked() {
        timeDebug('editable ngAfterViewChecked');
    }

    ngDoCheck() {
        timeDebug('editable ngDoCheck');
    }

    forceFlush() {
        timeDebug('start data sync');
        this.detectContext();
        this.cdr.detectChanges();
        // repair collaborative editing when Chinese input is interrupted by other users' cursors
        // when the DOMElement where the selection is located is removed
        // the compositionupdate and compositionend events will no longer be fired
        // so isComposing needs to be corrected
        // need exec after this.cdr.detectChanges() to render HTML
        // need exec before this.toNativeSelection() to correct native selection
        if (this.isComposing) {
            // Composition input text be not rendered when user composition input with selection is expanded
            // At this time, the following matching conditions are met, assign isComposing to false, and the status is wrong
            // this time condition is true and isComposiing is assigned false
            // Therefore, need to wait for the composition input text to be rendered before performing condition matching
            setTimeout(() => {
                const textNode = Node.get(this.editor, this.editor.selection.anchor.path);
                const textDOMNode = AngularEditor.toDOMNode(this.editor, textNode);
                let textContent = '';
                // skip decorate text
                textDOMNode.querySelectorAll('[editable-text]').forEach((stringDOMNode) => {
                    let text = stringDOMNode.textContent;
                    const zeroChar = '\uFEFF';
                    // remove zero with char
                    if (text.startsWith(zeroChar)) {
                        text = text.slice(1);
                    }
                    if (text.endsWith(zeroChar)) {
                        text = text.slice(0, text.length - 1);
                    }
                    textContent += text;
                });
                if (Node.string(textNode).endsWith(textContent)) {
                    this.isComposing = false;
                }
            }, 0);
        }
        this.toNativeSelection();
        timeDebug('end data sync');
    }

    initializeContext() {
        this.context = {
            parent: this.editor,
            selection: this.editor.selection,
            decorations: this.generateDecorations(),
            decorate: this.decorate,
            readonly: this.readonly
        };
    }

    initializeViewContext() {
        this.viewContext = {
            editor: this.editor,
            renderElement: this.renderElement,
            renderLeaf: this.renderLeaf,
            renderText: this.renderText,
            trackBy: this.trackBy,
            isStrictDecorate: this.isStrictDecorate,
            templateComponent: this.templateComponent
        };
    }

    detectContext() {
        const decorations = this.generateDecorations();
        if (this.context.selection !== this.editor.selection ||
            this.context.decorate !== this.decorate ||
            this.context.readonly !== this.readonly ||
            !isDecoratorRangeListEqual(this.context.decorations, decorations)) {
            this.context = {
                parent: this.editor,
                selection: this.editor.selection,
                decorations: decorations,
                decorate: this.decorate,
                readonly: this.readonly
            };
        }
    }

    composePlaceholderDecorate(editor: Editor) {
        if (this.placeholderDecorate) {
            return this.placeholderDecorate(editor) || [];
        }

        if (
            this.placeholder &&
            editor.children.length === 1 &&
            Array.from(Node.texts(editor)).length === 1 &&
            Node.string(editor) === ''
        ) {
            const start = Editor.start(editor, [])
            return [
                {
                    placeholder: this.placeholder,
                    anchor: start,
                    focus: start,
                },
            ]
        } else {
            return []
        }
    }

    generateDecorations() {
        const decorations = this.decorate([this.editor, []]);
        const placeholderDecorations = this.isComposing
            ? []
            : this.composePlaceholderDecorate(this.editor)
        decorations.push(...placeholderDecorations);
        return decorations;
    }

    //#region event proxy
    private addEventListener(eventName: string, listener: EventListener, target: HTMLElement | Document = this.elementRef.nativeElement) {
        this.manualListeners.push(
            this.renderer2.listen(target, eventName, (event: Event) => {
                const beforeInputEvent = extractBeforeInputEvent(event.type, null, event, event.target);
                if (beforeInputEvent) {
                    this.onFallbackBeforeInput(beforeInputEvent);
                }
                listener(event);
            })
        );
    }

    private toSlateSelection() {
        if (!this.readonly && !this.isComposing && !this.isUpdatingSelection && !this.isDraggingInternally) {
            try {
                const root = AngularEditor.findDocumentOrShadowRoot(this.editor)
                const { activeElement } = root;
                const el = AngularEditor.toDOMNode(this.editor, this.editor);
                const domSelection = (root as Document).getSelection();

                if (activeElement === el || hasEditableTarget(this.editor, activeElement)) {
                    this.latestElement = activeElement;
                    IS_FOCUSED.set(this.editor, true);
                } else {
                    IS_FOCUSED.delete(this.editor);
                }

                if (!domSelection) {
                    return Transforms.deselect(this.editor);
                }

                const editorElement = EDITOR_TO_ELEMENT.get(this.editor);
                const hasDomSelectionInEditor = editorElement.contains(domSelection.anchorNode) && editorElement.contains(domSelection.focusNode);
                if (!hasDomSelectionInEditor) {
                    Transforms.deselect(this.editor);
                    return;
                }

                // try to get the selection directly, because some terrible case can be normalize for normalizeDOMPoint
                // for example, double-click the last cell of the table to select a non-editable DOM
                const range = AngularEditor.toSlateRange(this.editor, domSelection);
                if (this.editor.selection && Range.equals(range, this.editor.selection) && !hasStringTarget(domSelection)) {
                    // force adjust DOMSelection
                    this.toNativeSelection();
                } else {
                    Transforms.select(this.editor, range);
                }
            } catch (error) {
                this.editor.onError({ code: SlateErrorCode.ToSlateSelectionError, nativeError: error })
            }
        }
    }

    private onDOMBeforeInput(
        event: Event & {
            inputType: string;
            isComposing: boolean;
            data: string | null;
            dataTransfer: DataTransfer | null;
            getTargetRanges(): DOMStaticRange[];
        }
    ) {
        const editor = this.editor;
        if (!this.readonly && hasEditableTarget(editor, event.target) && !this.isDOMEventHandled(event, this.beforeInput)) {
            try {
                const { selection } = editor;
                const { inputType: type } = event;
                const data = event.dataTransfer || event.data || undefined;
                event.preventDefault();

                // COMPAT: If the selection is expanded, even if the command seems like
                // a delete forward/backward command it should delete the selection.
                if (selection && Range.isExpanded(selection) && type.startsWith('delete')) {
                    const direction = type.endsWith('Backward') ? 'backward' : 'forward';
                    Editor.deleteFragment(editor, { direction });
                    return;
                }

                switch (type) {
                    case 'deleteByComposition':
                    case 'deleteByCut':
                    case 'deleteByDrag': {
                        Editor.deleteFragment(editor);
                        break;
                    }

                    case 'deleteContent':
                    case 'deleteContentForward': {
                        Editor.deleteForward(editor);
                        break;
                    }

                    case 'deleteContentBackward': {
                        Editor.deleteBackward(editor);
                        break;
                    }

                    case 'deleteEntireSoftLine': {
                        Editor.deleteBackward(editor, { unit: 'line' });
                        Editor.deleteForward(editor, { unit: 'line' });
                        break;
                    }

                    case 'deleteHardLineBackward': {
                        Editor.deleteBackward(editor, { unit: 'block' });
                        break;
                    }

                    case 'deleteSoftLineBackward': {
                        Editor.deleteBackward(editor, { unit: 'line' });
                        break;
                    }

                    case 'deleteHardLineForward': {
                        Editor.deleteForward(editor, { unit: 'block' });
                        break;
                    }

                    case 'deleteSoftLineForward': {
                        Editor.deleteForward(editor, { unit: 'line' });
                        break;
                    }

                    case 'deleteWordBackward': {
                        Editor.deleteBackward(editor, { unit: 'word' });
                        break;
                    }

                    case 'deleteWordForward': {
                        Editor.deleteForward(editor, { unit: 'word' });
                        break;
                    }

                    case 'insertLineBreak':
                    case 'insertParagraph': {
                        Editor.insertBreak(editor);
                        break;
                    }

                    case 'insertFromComposition': {
                        // COMPAT: in safari, `compositionend` event is dispatched after
                        // the beforeinput event with the inputType "insertFromComposition" has been dispatched.
                        // https://www.w3.org/TR/input-events-2/
                        // so the following code is the right logic
                        // because DOM selection in sync will be exec before `compositionend` event
                        // isComposing is true will prevent DOM selection being update correctly.
                        this.isComposing = false;
                        preventInsertFromComposition(event, this.editor);
                    }
                    case 'insertFromDrop':
                    case 'insertFromPaste':
                    case 'insertFromYank':
                    case 'insertReplacementText':
                    case 'insertText': {
                        // use a weak comparison instead of 'instanceof' to allow
                        // programmatic access of paste events coming from external windows
                        // like cypress where cy.window does not work realibly
                        if (data?.constructor.name === 'DataTransfer') {
                            AngularEditor.insertData(editor, data as DataTransfer);
                        } else if (typeof data === 'string') {
                            Editor.insertText(editor, data);
                        }
                        break;
                    }
                }
            } catch (error) {
                this.editor.onError({ code: SlateErrorCode.OnDOMBeforeInputError, nativeError: error });
            }
        }
    }

    private onDOMBlur(event: FocusEvent) {
        if (
            this.readonly ||
            this.isUpdatingSelection ||
            !hasEditableTarget(this.editor, event.target) ||
            this.isDOMEventHandled(event, this.blur)
        ) {
            return;
        }

        const window = AngularEditor.getWindow(this.editor);

        // COMPAT: If the current `activeElement` is still the previous
        // one, this is due to the window being blurred when the tab
        // itself becomes unfocused, so we want to abort early to allow to
        // editor to stay focused when the tab becomes focused again.
        const root = AngularEditor.findDocumentOrShadowRoot(this.editor);
        if (this.latestElement === root.activeElement) {
            return;
        }

        const { relatedTarget } = event;
        const el = AngularEditor.toDOMNode(this.editor, this.editor);

        // COMPAT: The event should be ignored if the focus is returning
        // to the editor from an embedded editable element (eg. an <input>
        // element inside a void node).
        if (relatedTarget === el) {
            return;
        }

        // COMPAT: The event should be ignored if the focus is moving from
        // the editor to inside a void node's spacer element.
        if (isDOMElement(relatedTarget) && relatedTarget.hasAttribute('data-slate-spacer')) {
            return;
        }

        // COMPAT: The event should be ignored if the focus is moving to a
        // non- editable section of an element that isn't a void node (eg.
        // a list item of the check list example).
        if (relatedTarget != null && isDOMNode(relatedTarget) && AngularEditor.hasDOMNode(this.editor, relatedTarget)) {
            const node = AngularEditor.toSlateNode(this.editor, relatedTarget);

            if (Element.isElement(node) && !this.editor.isVoid(node)) {
                return;
            }
        }

        IS_FOCUSED.delete(this.editor);
    }

    private onDOMClick(event: MouseEvent) {
        if (
            !this.readonly &&
            hasTarget(this.editor, event.target) &&
            !this.isDOMEventHandled(event, this.click) &&
            isDOMNode(event.target)
        ) {
            const node = AngularEditor.toSlateNode(this.editor, event.target);
            const path = AngularEditor.findPath(this.editor, node);
            const start = Editor.start(this.editor, path);
            const end = Editor.end(this.editor, path);

            const startVoid = Editor.void(this.editor, { at: start });
            const endVoid = Editor.void(this.editor, { at: end });

            if (startVoid && endVoid && Path.equals(startVoid[1], endVoid[1])) {
                const range = Editor.range(this.editor, start);
                Transforms.select(this.editor, range);
            }
        }
    }

    private onDOMCompositionEnd(event: CompositionEvent) {
        if (!event.data && !Range.isCollapsed(this.editor.selection)) {
            Transforms.delete(this.editor);
        }
        if (hasEditableTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.compositionEnd)) {
            // COMPAT: In Chrome/Firefox, `beforeinput` events for compositions
            // aren't correct and never fire the "insertFromComposition"
            // type that we need. So instead, insert whenever a composition
            // ends since it will already have been committed to the DOM.
            if (this.isComposing === true && !IS_SAFARI && event.data) {
                preventInsertFromComposition(event, this.editor);
                Editor.insertText(this.editor, event.data);
            }

            // COMPAT: In Firefox 87.0 CompositionEnd fire twice
            // so we need avoid repeat isnertText by isComposing === true,
            this.isComposing = false;
        }
        this.detectContext();
        this.cdr.detectChanges();
    }

    private onDOMCompositionStart(event: CompositionEvent) {
        const { selection } = this.editor;

        if (selection) {
            // solve the problem of cross node Chinese input
            if (Range.isExpanded(selection)) {
                Editor.deleteFragment(this.editor);
                this.forceFlush();
            }
        }
        if (hasEditableTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.compositionStart)) {
            this.isComposing = true;
        }
        this.detectContext();
        this.cdr.detectChanges();
    }

    private onDOMCopy(event: ClipboardEvent) {
        const window = AngularEditor.getWindow(this.editor);
        const isOutsideSlate = !hasStringTarget(window.getSelection()) && isTargetInsideVoid(this.editor, event.target);
        if (!isOutsideSlate && hasTarget(this.editor, event.target) && !this.readonly && !this.isDOMEventHandled(event, this.copy)) {
            event.preventDefault();
            AngularEditor.setFragmentData(this.editor, event.clipboardData, 'copy');
        }
    }

    private onDOMCut(event: ClipboardEvent) {
        if (!this.readonly && hasEditableTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.cut)) {
            event.preventDefault();
            AngularEditor.setFragmentData(this.editor, event.clipboardData, 'cut');
            const { selection } = this.editor;

            if (selection) {
                AngularEditor.deleteCutData(this.editor);
            }
        }
    }

    private onDOMDragOver(event: DragEvent) {
        if (hasTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.dragOver)) {
            // Only when the target is void, call `preventDefault` to signal
            // that drops are allowed. Editable content is droppable by
            // default, and calling `preventDefault` hides the cursor.
            const node = AngularEditor.toSlateNode(this.editor, event.target);

            if (Editor.isVoid(this.editor, node)) {
                event.preventDefault();
            }
        }
    }

    private onDOMDragStart(event: DragEvent) {
        if (!this.readonly && hasTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.dragStart)) {
            const node = AngularEditor.toSlateNode(this.editor, event.target);
            const path = AngularEditor.findPath(this.editor, node);
            const voidMatch =
                Editor.isVoid(this.editor, node) ||
                Editor.void(this.editor, { at: path, voids: true });

            // If starting a drag on a void node, make sure it is selected
            // so that it shows up in the selection's fragment.
            if (voidMatch) {
                const range = Editor.range(this.editor, path);
                Transforms.select(this.editor, range);
            }

            this.isDraggingInternally = true;

            AngularEditor.setFragmentData(this.editor, event.dataTransfer, 'drag');
        }
    }

    private onDOMDrop(event: DragEvent) {
        const editor = this.editor;
        if (!this.readonly && hasTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.drop)) {
            event.preventDefault();
            // Keep a reference to the dragged range before updating selection
            const draggedRange = editor.selection;

            // Find the range where the drop happened
            const range = AngularEditor.findEventRange(editor, event);
            const data = event.dataTransfer;

            Transforms.select(editor, range);

            if (this.isDraggingInternally) {
                if (draggedRange) {
                    Transforms.delete(editor, {
                        at: draggedRange,
                    });
                }

                this.isDraggingInternally = false;
            }

            AngularEditor.insertData(editor, data);

            // When dragging from another source into the editor, it's possible
            // that the current editor does not have focus.
            if (!AngularEditor.isFocused(editor)) {
                AngularEditor.focus(editor);
            }
        }
    }

    private onDOMDragEnd(event: DragEvent) {
        if (!this.readonly && this.isDraggingInternally && hasTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.dragEnd)) {
            this.isDraggingInternally = false;
        }
    }

    private onDOMFocus(event: Event) {
        if (
            !this.readonly &&
            !this.isUpdatingSelection &&
            hasEditableTarget(this.editor, event.target) &&
            !this.isDOMEventHandled(event, this.focus)
        ) {
            const el = AngularEditor.toDOMNode(this.editor, this.editor);
            const root = AngularEditor.findDocumentOrShadowRoot(this.editor);
            this.latestElement = root.activeElement

            // COMPAT: If the editor has nested editable elements, the focus
            // can go to them. In Firefox, this must be prevented because it
            // results in issues with keyboard navigation. (2017/03/30)
            if (IS_FIREFOX && event.target !== el) {
                el.focus();
                return;
            }

            IS_FOCUSED.set(this.editor, true);
        }
    }

    private onDOMKeydown(event: KeyboardEvent) {
        const editor = this.editor;
        if (
            !this.readonly &&
            hasEditableTarget(editor, event.target) &&
            !this.isComposing &&
            !this.isDOMEventHandled(event, this.keydown)
        ) {
            const nativeEvent = event;
            const { selection } = editor;

            const element =
                editor.children[
                selection !== null ? selection.focus.path[0] : 0
                ]
            const isRTL = getDirection(Node.string(element)) === 'rtl';

            try {
                // COMPAT: Since we prevent the default behavior on
                // `beforeinput` events, the browser doesn't think there's ever
                // any history stack to undo or redo, so we have to manage these
                // hotkeys ourselves. (2019/11/06)
                if (Hotkeys.isRedo(nativeEvent)) {
                    event.preventDefault();

                    if (HistoryEditor.isHistoryEditor(editor)) {
                        editor.redo();
                    }

                    return;
                }

                if (Hotkeys.isUndo(nativeEvent)) {
                    event.preventDefault();

                    if (HistoryEditor.isHistoryEditor(editor)) {
                        editor.undo();
                    }

                    return;
                }

                // COMPAT: Certain browsers don't handle the selection updates
                // properly. In Chrome, the selection isn't properly extended.
                // And in Firefox, the selection isn't properly collapsed.
                // (2017/10/17)
                if (Hotkeys.isMoveLineBackward(nativeEvent)) {
                    event.preventDefault();
                    Transforms.move(editor, { unit: 'line', reverse: true });
                    return;
                }

                if (Hotkeys.isMoveLineForward(nativeEvent)) {
                    event.preventDefault();
                    Transforms.move(editor, { unit: 'line' });
                    return;
                }

                if (Hotkeys.isExtendLineBackward(nativeEvent)) {
                    event.preventDefault();
                    Transforms.move(editor, {
                        unit: 'line',
                        edge: 'focus',
                        reverse: true
                    });
                    return;
                }

                if (Hotkeys.isExtendLineForward(nativeEvent)) {
                    event.preventDefault();
                    Transforms.move(editor, { unit: 'line', edge: 'focus' });
                    return;
                }

                // COMPAT: If a void node is selected, or a zero-width text node
                // adjacent to an inline is selected, we need to handle these
                // hotkeys manually because browsers won't be able to skip over
                // the void node with the zero-width space not being an empty
                // string.
                if (Hotkeys.isMoveBackward(nativeEvent)) {
                    event.preventDefault();

                    if (selection && Range.isCollapsed(selection)) {
                        Transforms.move(editor, { reverse: !isRTL });
                    } else {
                        Transforms.collapse(editor, { edge: 'start' });
                    }

                    return;
                }

                if (Hotkeys.isMoveForward(nativeEvent)) {
                    event.preventDefault();

                    if (selection && Range.isCollapsed(selection)) {
                        Transforms.move(editor, { reverse: isRTL });
                    } else {
                        Transforms.collapse(editor, { edge: 'end' });
                    }

                    return;
                }

                if (Hotkeys.isMoveWordBackward(nativeEvent)) {
                    event.preventDefault();

                    if (selection && Range.isExpanded(selection)) {
                        Transforms.collapse(editor, { edge: 'focus' })
                    }

                    Transforms.move(editor, { unit: 'word', reverse: !isRTL });
                    return;
                }

                if (Hotkeys.isMoveWordForward(nativeEvent)) {
                    event.preventDefault();

                    if (selection && Range.isExpanded(selection)) {
                        Transforms.collapse(editor, { edge: 'focus' })
                    }

                    Transforms.move(editor, { unit: 'word', reverse: isRTL });
                    return;
                }

                // COMPAT: Certain browsers don't support the `beforeinput` event, so we
                // fall back to guessing at the input intention for hotkeys.
                // COMPAT: In iOS, some of these hotkeys are handled in the
                if (!HAS_BEFORE_INPUT_SUPPORT) {
                    // We don't have a core behavior for these, but they change the
                    // DOM if we don't prevent them, so we have to.
                    if (Hotkeys.isBold(nativeEvent) || Hotkeys.isItalic(nativeEvent) || Hotkeys.isTransposeCharacter(nativeEvent)) {
                        event.preventDefault();
                        return;
                    }

                    if (Hotkeys.isSplitBlock(nativeEvent)) {
                        event.preventDefault();
                        Editor.insertBreak(editor);
                        return;
                    }

                    if (Hotkeys.isDeleteBackward(nativeEvent)) {
                        event.preventDefault();

                        if (selection && Range.isExpanded(selection)) {
                            Editor.deleteFragment(editor, { direction: 'backward' });
                        } else {
                            Editor.deleteBackward(editor);
                        }

                        return;
                    }

                    if (Hotkeys.isDeleteForward(nativeEvent)) {
                        event.preventDefault();

                        if (selection && Range.isExpanded(selection)) {
                            Editor.deleteFragment(editor, { direction: 'forward' });
                        } else {
                            Editor.deleteForward(editor);
                        }

                        return;
                    }

                    if (Hotkeys.isDeleteLineBackward(nativeEvent)) {
                        event.preventDefault();

                        if (selection && Range.isExpanded(selection)) {
                            Editor.deleteFragment(editor, { direction: 'backward' });
                        } else {
                            Editor.deleteBackward(editor, { unit: 'line' });
                        }

                        return;
                    }

                    if (Hotkeys.isDeleteLineForward(nativeEvent)) {
                        event.preventDefault();

                        if (selection && Range.isExpanded(selection)) {
                            Editor.deleteFragment(editor, { direction: 'forward' });
                        } else {
                            Editor.deleteForward(editor, { unit: 'line' });
                        }

                        return;
                    }

                    if (Hotkeys.isDeleteWordBackward(nativeEvent)) {
                        event.preventDefault();

                        if (selection && Range.isExpanded(selection)) {
                            Editor.deleteFragment(editor, { direction: 'backward' });
                        } else {
                            Editor.deleteBackward(editor, { unit: 'word' });
                        }

                        return;
                    }

                    if (Hotkeys.isDeleteWordForward(nativeEvent)) {
                        event.preventDefault();

                        if (selection && Range.isExpanded(selection)) {
                            Editor.deleteFragment(editor, { direction: 'forward' });
                        } else {
                            Editor.deleteForward(editor, { unit: 'word' });
                        }

                        return;
                    }
                } else {
                    if (IS_CHROME || IS_SAFARI) {
                        // COMPAT: Chrome and Safari support `beforeinput` event but do not fire
                        // an event when deleting backwards in a selected void inline node
                        if (
                            selection &&
                            (Hotkeys.isDeleteBackward(nativeEvent) ||
                                Hotkeys.isDeleteForward(nativeEvent)) &&
                            Range.isCollapsed(selection)
                        ) {
                            const currentNode = Node.parent(
                                editor,
                                selection.anchor.path
                            )
                            if (
                                Element.isElement(currentNode) &&
                                Editor.isVoid(editor, currentNode) &&
                                Editor.isInline(editor, currentNode)
                            ) {
                                event.preventDefault()
                                Editor.deleteBackward(editor, { unit: 'block' })
                                return
                            }
                        }
                    }
                }
            } catch (error) {
                this.editor.onError({ code: SlateErrorCode.OnDOMKeydownError, nativeError: error });
            }
        }
    }

    private onDOMPaste(event: ClipboardEvent) {
        // COMPAT: Certain browsers don't support the `beforeinput` event, so we
        // fall back to React's `onPaste` here instead.
        // COMPAT: Firefox, Chrome and Safari are not emitting `beforeinput` events
        // when "paste without formatting" option is used.
        // This unfortunately needs to be handled with paste events instead.
        if (
            !this.isDOMEventHandled(event, this.paste) &&
            (!HAS_BEFORE_INPUT_SUPPORT || isPlainTextOnlyPaste(event) || forceOnDOMPaste) &&
            !this.readonly &&
            hasEditableTarget(this.editor, event.target)
        ) {
            event.preventDefault();
            AngularEditor.insertData(this.editor, event.clipboardData);
        }
    }

    private onFallbackBeforeInput(event: BeforeInputEvent) {
        // COMPAT: Certain browsers don't support the `beforeinput` event, so we
        // fall back to React's leaky polyfill instead just for it. It
        // only works for the `insertText` input type.
        if (
            !HAS_BEFORE_INPUT_SUPPORT &&
            !this.readonly &&
            !this.isDOMEventHandled(event.nativeEvent, this.beforeInput) &&
            hasEditableTarget(this.editor, event.nativeEvent.target)
        ) {
            event.nativeEvent.preventDefault();
            try {
                const text = event.data;
                if (!Range.isCollapsed(this.editor.selection)) {
                    Editor.deleteFragment(this.editor);
                }
                // just handle Non-IME input
                if (!this.isComposing) {
                    Editor.insertText(this.editor, text);
                }
            } catch (error) {
                this.editor.onError({ code: SlateErrorCode.ToNativeSelectionError, nativeError: error });
            }
        }
    }

    private isDOMEventHandled(event: Event, handler?: (event: Event) => void) {
        if (!handler) {
            return false;
        }
        handler(event);
        return event.defaultPrevented;
    }
    //#endregion

    ngOnDestroy() {
        NODE_TO_ELEMENT.delete(this.editor);
        this.manualListeners.forEach(manualListener => {
            manualListener();
        });
        this.destroy$.complete();
        EDITOR_TO_ON_CHANGE.delete(this.editor);
    }
}
Example #19
Source File: calendar.ts    From ngx-mat-datetime-picker with MIT License 4 votes vote down vote up
/**
 * A calendar that is used as part of the datepicker.
 * @docs-private
 */
@Component({
  selector: 'ngx-mat-calendar',
  templateUrl: 'calendar.html',
  styleUrls: ['calendar.scss'],
  host: {
    'class': 'mat-calendar',
  },
  exportAs: 'ngxMatCalendar',
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NgxMatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDestroy, OnChanges {
  /** An input indicating the type of the header component, if set. */
  @Input() headerComponent: ComponentType<any>;

  /** A portal containing the header component type for this calendar. */
  _calendarHeaderPortal: Portal<any>;

  private _intlChanges: Subscription;

  /**
   * Used for scheduling that focus should be moved to the active cell on the next tick.
   * We need to schedule it, rather than do it immediately, because we have to wait
   * for Angular to re-evaluate the view children.
   */
  private _moveFocusOnNextTick = false;

  /** A date representing the period (month or year) to start the calendar in. */
  @Input()
  get startAt(): D | null { return this._startAt; }
  set startAt(value: D | null) {
    this._startAt = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
  }
  private _startAt: D | null;

  /** Whether the calendar should be started in month or year view. */
  @Input() startView: MatCalendarView = 'month';

  /** The currently selected date. */
  @Input()
  get selected(): D | null { return this._selected; }
  set selected(value: D | null) {
    this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
  }
  private _selected: D | null;

  /** The minimum selectable date. */
  @Input()
  get minDate(): D | null { return this._minDate; }
  set minDate(value: D | null) {
    this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
  }
  private _minDate: D | null;

  /** The maximum selectable date. */
  @Input()
  get maxDate(): D | null { return this._maxDate; }
  set maxDate(value: D | null) {
    this._maxDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
  }
  private _maxDate: D | null;

  /** Function used to filter which dates are selectable. */
  @Input() dateFilter: (date: D) => boolean;

  /** Function that can be used to add custom CSS classes to dates. */
  @Input() dateClass: (date: D) => MatCalendarCellCssClasses;

  /** Emits when the currently selected date changes. */
  @Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();

  /**
   * Emits the year chosen in multiyear view.
   * This doesn't imply a change on the selected date.
   */
  @Output() readonly yearSelected: EventEmitter<D> = new EventEmitter<D>();

  /**
   * Emits the month chosen in year view.
   * This doesn't imply a change on the selected date.
   */
  @Output() readonly monthSelected: EventEmitter<D> = new EventEmitter<D>();

  /** Emits when any date is selected. */
  @Output() readonly _userSelection: EventEmitter<void> = new EventEmitter<void>();

  /** Reference to the current month view component. */
  @ViewChild(NgxMatMonthView) monthView: NgxMatMonthView<D>;

  /** Reference to the current year view component. */
  @ViewChild(NgxMatYearView) yearView: NgxMatYearView<D>;

  /** Reference to the current multi-year view component. */
  @ViewChild(NgxMatMultiYearView) multiYearView: NgxMatMultiYearView<D>;

  /**
   * The current active date. This determines which time period is shown and which date is
   * highlighted when using keyboard navigation.
   */
  get activeDate(): D { return this._clampedActiveDate; }
  set activeDate(value: D) {
    this._clampedActiveDate = this._dateAdapter.clampDate(value, this.minDate, this.maxDate);
    this.stateChanges.next();
    this._changeDetectorRef.markForCheck();
  }
  private _clampedActiveDate: D;

  /** Whether the calendar is in month view. */
  get currentView(): MatCalendarView { return this._currentView; }
  set currentView(value: MatCalendarView) {
    this._currentView = value;
    this._moveFocusOnNextTick = true;
    this._changeDetectorRef.markForCheck();
  }
  private _currentView: MatCalendarView;

  /**
   * Emits whenever there is a state change that the header may need to respond to.
   */
  stateChanges = new Subject<void>();

  constructor(_intl: MatDatepickerIntl,
    @Optional() private _dateAdapter: NgxMatDateAdapter<D>,
    @Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats,
    private _changeDetectorRef: ChangeDetectorRef) {

    if (!this._dateAdapter) {
      throw createMissingDateImplError('NgxDateAdapter');
    }

    if (!this._dateFormats) {
      throw createMissingDateImplError('MAT_DATE_FORMATS');
    }

    this._intlChanges = _intl.changes.subscribe(() => {
      _changeDetectorRef.markForCheck();
      this.stateChanges.next();
    });
  }

  ngAfterContentInit() {
    this._calendarHeaderPortal = new ComponentPortal(this.headerComponent || NgxMatCalendarHeader);
    this.activeDate = this.startAt || this._dateAdapter.today();

    // Assign to the private property since we don't want to move focus on init.
    this._currentView = this.startView;
  }

  ngAfterViewChecked() {
    if (this._moveFocusOnNextTick) {
      this._moveFocusOnNextTick = false;
      this.focusActiveCell();
    }
  }

  ngOnDestroy() {
    this._intlChanges.unsubscribe();
    this.stateChanges.complete();
  }

  ngOnChanges(changes: SimpleChanges) {
    const change =
      changes['minDate'] || changes['maxDate'] || changes['dateFilter'];

    if (change && !change.firstChange) {
      const view = this._getCurrentViewComponent();

      if (view) {
        // We need to `detectChanges` manually here, because the `minDate`, `maxDate` etc. are
        // passed down to the view via data bindings which won't be up-to-date when we call `_init`.
        this._changeDetectorRef.detectChanges();
        view._init();
      }
    }

    this.stateChanges.next();
  }

  focusActiveCell() {
    this._getCurrentViewComponent()._focusActiveCell();
  }

  /** Updates today's date after an update of the active date */
  updateTodaysDate() {
    let view = this.currentView == 'month' ? this.monthView :
      (this.currentView == 'year' ? this.yearView : this.multiYearView);

    view.ngAfterContentInit();
  }

  /** Handles date selection in the month view. */
  _dateSelected(date: D | null): void {
    if (date && !this._dateAdapter.sameDate(date, this.selected)) {
      this.selectedChange.emit(date);
    }
  }

  /** Handles year selection in the multiyear view. */
  _yearSelectedInMultiYearView(normalizedYear: D) {
    this.yearSelected.emit(normalizedYear);
  }

  /** Handles month selection in the year view. */
  _monthSelectedInYearView(normalizedMonth: D) {
    this.monthSelected.emit(normalizedMonth);
  }

  _userSelected(): void {
    this._userSelection.emit();
  }

  /** Handles year/month selection in the multi-year/year views. */
  _goToDateInView(date: D, view: 'month' | 'year' | 'multi-year'): void {
    this.activeDate = date;
    this.currentView = view;
  }

  /**
   * @param obj The object to check.
   * @returns The given object if it is both a date instance and valid, otherwise null.
   */
  private _getValidDateOrNull(obj: any): D | null {
    return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
  }

  /** Returns the component instance that corresponds to the current calendar view. */
  private _getCurrentViewComponent() {
    return this.monthView || this.yearView || this.multiYearView;
  }
}
Example #20
Source File: calendar.ts    From angular-material-components with MIT License 4 votes vote down vote up
/**
 * A calendar that is used as part of the datepicker.
 * @docs-private
 */
@Component({
  selector: 'ngx-mat-calendar',
  templateUrl: 'calendar.html',
  styleUrls: ['calendar.scss'],
  host: {
    'class': 'mat-calendar',
  },
  exportAs: 'ngxMatCalendar',
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NgxMatCalendar<D> implements AfterContentInit, AfterViewChecked, OnDestroy, OnChanges {
  /** An input indicating the type of the header component, if set. */
  @Input() headerComponent: ComponentType<any>;

  /** A portal containing the header component type for this calendar. */
  _calendarHeaderPortal: Portal<any>;

  private _intlChanges: Subscription;

  /**
   * Used for scheduling that focus should be moved to the active cell on the next tick.
   * We need to schedule it, rather than do it immediately, because we have to wait
   * for Angular to re-evaluate the view children.
   */
  private _moveFocusOnNextTick = false;

  /** A date representing the period (month or year) to start the calendar in. */
  @Input()
  get startAt(): D | null { return this._startAt; }
  set startAt(value: D | null) {
    this._startAt = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
  }
  private _startAt: D | null;

  /** Whether the calendar should be started in month or year view. */
  @Input() startView: MatCalendarView = 'month';

  /** The currently selected date. */
  @Input()
  get selected(): D | null { return this._selected; }
  set selected(value: D | null) {
    this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
  }
  private _selected: D | null;

  /** The minimum selectable date. */
  @Input()
  get minDate(): D | null { return this._minDate; }
  set minDate(value: D | null) {
    this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
  }
  private _minDate: D | null;

  /** The maximum selectable date. */
  @Input()
  get maxDate(): D | null { return this._maxDate; }
  set maxDate(value: D | null) {
    this._maxDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
  }
  private _maxDate: D | null;

  /** Function used to filter which dates are selectable. */
  @Input() dateFilter: (date: D) => boolean;

  /** Function that can be used to add custom CSS classes to dates. */
  @Input() dateClass: (date: D) => MatCalendarCellCssClasses;

  /** Emits when the currently selected date changes. */
  @Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();

  /**
   * Emits the year chosen in multiyear view.
   * This doesn't imply a change on the selected date.
   */
  @Output() readonly yearSelected: EventEmitter<D> = new EventEmitter<D>();

  /**
   * Emits the month chosen in year view.
   * This doesn't imply a change on the selected date.
   */
  @Output() readonly monthSelected: EventEmitter<D> = new EventEmitter<D>();

  /** Emits when any date is selected. */
  @Output() readonly _userSelection: EventEmitter<void> = new EventEmitter<void>();

  /** Reference to the current month view component. */
  @ViewChild(NgxMatMonthView) monthView: NgxMatMonthView<D>;

  /** Reference to the current year view component. */
  @ViewChild(NgxMatYearView) yearView: NgxMatYearView<D>;

  /** Reference to the current multi-year view component. */
  @ViewChild(NgxMatMultiYearView) multiYearView: NgxMatMultiYearView<D>;

  /**
   * The current active date. This determines which time period is shown and which date is
   * highlighted when using keyboard navigation.
   */
  get activeDate(): D { return this._clampedActiveDate; }
  set activeDate(value: D) {
    this._clampedActiveDate = this._dateAdapter.clampDate(value, this.minDate, this.maxDate);
    this.stateChanges.next();
    this._changeDetectorRef.markForCheck();
  }
  private _clampedActiveDate: D;

  /** Whether the calendar is in month view. */
  get currentView(): MatCalendarView { return this._currentView; }
  set currentView(value: MatCalendarView) {
    this._currentView = value;
    this._moveFocusOnNextTick = true;
    this._changeDetectorRef.markForCheck();
  }
  private _currentView: MatCalendarView;

  /**
   * Emits whenever there is a state change that the header may need to respond to.
   */
  stateChanges = new Subject<void>();

  constructor(_intl: MatDatepickerIntl,
    @Optional() private _dateAdapter: NgxMatDateAdapter<D>,
    @Optional() @Inject(NGX_MAT_DATE_FORMATS) private _dateFormats: NgxMatDateFormats,
    private _changeDetectorRef: ChangeDetectorRef) {

    if (!this._dateAdapter) {
      throw createMissingDateImplError('NgxDateAdapter');
    }

    if (!this._dateFormats) {
      throw createMissingDateImplError('NGX_MAT_DATE_FORMATS');
    }

    this._intlChanges = _intl.changes.subscribe(() => {
      _changeDetectorRef.markForCheck();
      this.stateChanges.next();
    });
  }

  ngAfterContentInit() {
    this._calendarHeaderPortal = new ComponentPortal(this.headerComponent || NgxMatCalendarHeader);
    this.activeDate = this.startAt || this._dateAdapter.today();

    // Assign to the private property since we don't want to move focus on init.
    this._currentView = this.startView;
  }

  ngAfterViewChecked() {
    if (this._moveFocusOnNextTick) {
      this._moveFocusOnNextTick = false;
      this.focusActiveCell();
    }
  }

  ngOnDestroy() {
    this._intlChanges.unsubscribe();
    this.stateChanges.complete();
  }

  ngOnChanges(changes: SimpleChanges) {
    const change =
      changes['minDate'] || changes['maxDate'] || changes['dateFilter'];

    if (change && !change.firstChange) {
      const view = this._getCurrentViewComponent();

      if (view) {
        // We need to `detectChanges` manually here, because the `minDate`, `maxDate` etc. are
        // passed down to the view via data bindings which won't be up-to-date when we call `_init`.
        this._changeDetectorRef.detectChanges();
        view._init();
      }
    }

    this.stateChanges.next();
  }

  focusActiveCell() {
    this._getCurrentViewComponent()._focusActiveCell();
  }

  /** Updates today's date after an update of the active date */
  updateTodaysDate() {
    let view = this.currentView == 'month' ? this.monthView :
      (this.currentView == 'year' ? this.yearView : this.multiYearView);

    view.ngAfterContentInit();
  }

  /** Handles date selection in the month view. */
  _dateSelected(date: D | null): void {
    if (date && !this._dateAdapter.sameDate(date, this.selected)) {
      this.selectedChange.emit(date);
    }
  }

  /** Handles year selection in the multiyear view. */
  _yearSelectedInMultiYearView(normalizedYear: D) {
    this.yearSelected.emit(normalizedYear);
  }

  /** Handles month selection in the year view. */
  _monthSelectedInYearView(normalizedMonth: D) {
    this.monthSelected.emit(normalizedMonth);
  }

  _userSelected(): void {
    this._userSelection.emit();
  }

  /** Handles year/month selection in the multi-year/year views. */
  _goToDateInView(date: D, view: 'month' | 'year' | 'multi-year'): void {
    this.activeDate = date;
    this.currentView = view;
  }

  /**
   * @param obj The object to check.
   * @returns The given object if it is both a date instance and valid, otherwise null.
   */
  private _getValidDateOrNull(obj: any): D | null {
    return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
  }

  /** Returns the component instance that corresponds to the current calendar view. */
  private _getCurrentViewComponent() {
    return this.monthView || this.yearView || this.multiYearView;
  }
}
Example #21
Source File: interview-pane.component.ts    From mysteryofthreebots with Apache License 2.0 4 votes vote down vote up
@Component({
  selector: 'app-interview-pane',
  templateUrl: './interview-pane.component.html',
  styleUrls: ['./interview-pane.component.scss']
})
export class InterviewPaneComponent implements AfterViewChecked, AfterViewInit, OnDestroy {
  @ViewChild('messageContainer') private scrollContainer: ElementRef;
  @HostBinding('attr.data-bot-name') @Input() botName: BotName;
  question = '';
  messages: IMessage[] = [];
  private shouldScroll = false;

  constructor(private botResponseService: BotResponseService) {
    this.handleScroll = this.handleScroll.bind(this);
  }

  ngAfterViewChecked() {
    if (this.shouldScroll) {
      setTimeout(() => {
        this.scrollContainer.nativeElement.scrollTop = this.scrollContainer.nativeElement.scrollHeight;
        this.toggleScrollClasses();
      }, 0);
      this.shouldScroll = false;
    }
  }

  ngAfterViewInit() {
    this.scrollContainer.nativeElement.addEventListener(
      'scroll',
      this.handleScroll,
      arePassiveListenersSupported ?
        {
          passive: true,
        } :
        false
    );
  }

  ngOnDestroy() {
    if (this.scrollContainer) {
      this.scrollContainer.nativeElement.removeEventListener('scroll', this.handleScroll);
    }
  }

  handleScroll() {
    this.toggleScrollClasses();
  }

  handleQuestionSubmit(event: any) {
    event.preventDefault();
    this.messages.push({
      text: this.question,
      isPlayer: true,
    });
    const question = this.question;
    this.question = '';
    this.messages.push({
      text: '',
      isPlayer: false,
      isWaiting: true,
    });
    this.scrollToBottom();
    setTimeout(async () => {
      let response = await this.botResponseService.getResponse(question, this.botName);
      if (!response) {
        response = 'I\'m sorry, I didn\'t understand that. Can you ask that another way?';
      }
      const pendingMessage =  this.messages.find(({isWaiting, isPlayer}) => !isPlayer && isWaiting);
      if (pendingMessage) {
        pendingMessage.text = response;
        pendingMessage.isWaiting = false;
      } else {
        this.messages.push({
          text: response,
          isPlayer: false,
        });
        this.scrollToBottom();
      }
    }, 0);
  }

  handleQuestionInput(event: any) {
    this.question = event.target.value;
  }

  private scrollToBottom() {
    this.shouldScroll = true;
  }

  private toggleScrollClasses() {
    this.scrollContainer.nativeElement.classList.toggle(
      'at-top',
      this.scrollContainer.nativeElement.scrollTop === 0
    );
    this.scrollContainer.nativeElement.classList.toggle(
      'at-bottom',
      this.scrollContainer.nativeElement.scrollTop +
        this.scrollContainer.nativeElement.clientHeight >= this.scrollContainer.nativeElement.scrollHeight
    );
  }
}
Example #22
Source File: sticky-header.component.ts    From geonetwork-ui with GNU General Public License v2.0 4 votes vote down vote up
/**
 * This component will make a block that will stay sticky on the top of the page,
 * but will also shrink down to a minimum height when scrolling.
 * The ratio between minimum and nominal height is stored in the shrinkRatio
 * property, and can be used to change e.g. the opacity or size of child elements.
 * This component renders at z-index: 50 by default.
 * The first parent element with an overflow of `scroll` or `overflow` is considered
 * for scroll events.
 */

@Component({
  selector: 'gn-ui-sticky-header',
  templateUrl: './sticky-header.component.html',
  styleUrls: ['./sticky-header.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StickyHeaderComponent
  implements AfterViewInit, OnDestroy, OnInit, AfterViewChecked
{
  @Input() minHeightPx: number
  @Input() fullHeightPx: number
  @ContentChild(TemplateRef) template: TemplateRef<{ $implicit: number }>
  @ViewChild('outerContainer') outerContainer: ElementRef
  @ViewChild('innerContainer') innerContainer: ElementRef
  placeholderEl: HTMLElement
  scrollableParent: HTMLElement
  expandRatio: number // 1 means height is full, 0 means height is minimum
  scrollSub: Subscription
  parentScroll: number

  constructor(
    private changeDetector: ChangeDetectorRef,
    @Host() private hostEl: ElementRef,
    private zone: NgZone
  ) {}

  ngAfterViewInit() {
    this.scrollSub = fromEvent(this.scrollableParent, 'scroll', {
      passive: true,
    })
      .pipe(
        throttleTime(0, animationFrameScheduler, {
          leading: true,
          trailing: true,
        })
      )
      .subscribe(this.updateSize.bind(this))
  }

  ngOnInit() {
    this.scrollableParent = findScrollableParent(this.hostEl.nativeElement)
    this.placeholderEl = document.createElement('div')
    this.placeholderEl.style.position = 'absolute'
    this.placeholderEl.classList.add('sticky-header-placeholder')
    this.hostEl.nativeElement.insertAdjacentElement(
      'beforebegin',
      this.placeholderEl
    )
  }

  ngAfterViewChecked() {
    this.updateSize()
  }

  ngOnDestroy() {
    this.scrollSub.unsubscribe()
    this.placeholderEl.remove()
  }

  updateSize() {
    this.zone.runOutsideAngular(() => {
      if (this.scrollableParent.scrollTop === this.parentScroll) return
      this.parentScroll = this.scrollableParent.scrollTop

      const offsetTop = Math.max(
        0,
        this.parentScroll - this.placeholderEl.offsetTop
      )
      const newHeightPx = Math.max(
        this.minHeightPx,
        this.fullHeightPx - offsetTop
      )
      this.innerContainer.nativeElement.style.transform = `translate(0, ${
        newHeightPx - this.fullHeightPx
      }px)`
      this.expandRatio =
        (newHeightPx - this.minHeightPx) /
        (this.fullHeightPx - this.minHeightPx)
      this.changeDetector.detectChanges()
    })
  }
}
Example #23
Source File: my-advances.page.ts    From fyle-mobile-app with MIT License 4 votes vote down vote up
@Component({
  selector: 'app-my-advances',
  templateUrl: './my-advances.page.html',
  styleUrls: ['./my-advances.page.scss'],
})
export class MyAdvancesPage implements AfterViewChecked {
  myAdvancerequests$: Observable<any[]>;

  myAdvances$: Observable<any>;

  loadData$: Subject<number> = new Subject();

  isLoading = false;

  navigateBack = false;

  totalTaskCount = 0;

  refreshAdvances$: Subject<void> = new Subject();

  advances$: Observable<any>;

  isConnected$: Observable<boolean>;

  onPageExit = new Subject();

  filterPills = [];

  filterParams$ = new BehaviorSubject<Filters>({});

  advancesTaskCount = 0;

  projectFieldName = 'Project';

  constructor(
    private advanceRequestService: AdvanceRequestService,
    private activatedRoute: ActivatedRoute,
    private router: Router,
    private advanceService: AdvanceService,
    private networkService: NetworkService,
    private offlineService: OfflineService,
    private filtersHelperService: FiltersHelperService,
    private utilityService: UtilityService,
    private titleCasePipe: TitleCasePipe,
    private trackingService: TrackingService,
    private tasksService: TasksService,
    private cdr: ChangeDetectorRef
  ) {}

  ionViewWillLeave() {
    this.onPageExit.next();
  }

  setupNetworkWatcher() {
    const networkWatcherEmitter = new EventEmitter<boolean>();
    this.networkService.connectivityWatcher(networkWatcherEmitter);
    this.isConnected$ = concat(this.networkService.isOnline(), networkWatcherEmitter.asObservable()).pipe(
      takeUntil(this.onPageExit),
      shareReplay(1)
    );

    this.isConnected$.subscribe((isOnline) => {
      if (!isOnline) {
        this.router.navigate(['/', 'enterprise', 'my_dashboard']);
      }
    });
  }

  getAndUpdateProjectName() {
    this.offlineService.getAllEnabledExpenseFields().subscribe((expenseFields) => {
      const projectField = expenseFields.find((expenseField) => expenseField.column_name === 'project_id');
      this.projectFieldName = projectField?.field_name;
    });
  }

  ionViewWillEnter() {
    this.setupNetworkWatcher();

    this.tasksService.getAdvancesTaskCount().subscribe((advancesTaskCount) => {
      this.advancesTaskCount = advancesTaskCount;
    });

    this.navigateBack = !!this.activatedRoute.snapshot.params.navigateBack;
    this.tasksService.getTotalTaskCount().subscribe((totalTaskCount) => (this.totalTaskCount = totalTaskCount));

    const oldFilters = this.activatedRoute.snapshot.queryParams.filters;
    if (oldFilters) {
      this.filterParams$.next(JSON.parse(oldFilters));
      this.filterPills = this.filtersHelperService.generateFilterPills(this.filterParams$.value);
    }

    this.isLoading = true;

    this.myAdvancerequests$ = this.advanceRequestService
      .getMyAdvanceRequestsCount({
        areq_advance_id: 'is.null',
      })
      .pipe(
        concatMap((count) => {
          count = count > 10 ? count / 10 : 1;
          return range(0, count);
        }),
        concatMap((count) =>
          this.advanceRequestService.getMyadvanceRequests({
            offset: 10 * count,
            limit: 10,
            queryParams: {
              areq_advance_id: 'is.null',
              order: 'areq_created_at.desc,areq_id.desc',
            },
          })
        ),
        map((res) => res.data),
        reduce((acc, curr) => acc.concat(curr))
      );

    this.myAdvances$ = this.advanceService.getMyAdvancesCount().pipe(
      concatMap((count) => {
        count = count > 10 ? count / 10 : 1;
        return range(0, count);
      }),
      concatMap((count) =>
        this.advanceService.getMyadvances({
          offset: 10 * count,
          limit: 10,
          queryParams: { order: 'adv_created_at.desc,adv_id.desc' },
        })
      ),
      map((res) => res.data),
      reduce((acc, curr) => acc.concat(curr))
    );

    const sortResults = map((res: any[]) => res.sort((a, b) => (a.created_at < b.created_at ? 1 : -1)));
    this.advances$ = this.refreshAdvances$.pipe(
      startWith(0),
      concatMap(() => this.offlineService.getOrgSettings()),
      switchMap((orgSettings) =>
        combineLatest([
          iif(() => orgSettings.advance_requests.enabled, this.myAdvancerequests$, of(null)),
          iif(() => orgSettings.advances.enabled, this.myAdvances$, of(null)),
        ]).pipe(
          map((res) => {
            const [myAdvancerequestsRes, myAdvancesRes] = res;
            let myAdvancerequests = myAdvancerequestsRes || [];
            let myAdvances = myAdvancesRes || [];
            myAdvancerequests = this.updateMyAdvanceRequests(myAdvancerequests);
            myAdvances = this.updateMyAdvances(myAdvances);
            return myAdvances.concat(myAdvancerequests);
          }),
          sortResults
        )
      ),
      switchMap((advArray) =>
        //piping through filterParams so that filtering and sorting happens whenever we call next() on filterParams
        this.filterParams$.pipe(
          map((filters) => {
            let newArr = cloneDeep(advArray);

            if (filters && filters.state && filters.state.length > 0) {
              newArr = advArray.filter((adv) => {
                const sentBackAdvance =
                  filters.state.includes(AdvancesStates.sentBack) &&
                  adv.areq_state === 'DRAFT' &&
                  adv.areq_is_sent_back;

                const plainDraft =
                  filters.state.includes(AdvancesStates.draft) &&
                  adv.areq_state === 'DRAFT' &&
                  !adv.areq_is_sent_back &&
                  !adv.areq_is_pulled_back;

                return sentBackAdvance || plainDraft;
              });
            }
            newArr = this.utilityService.sortAllAdvances(filters.sortDir, filters.sortParam, newArr);
            return newArr;
          })
        )
      ),
      tap((res) => {
        if (res && res.length >= 0) {
          this.isLoading = false;
        }
      })
    );

    this.getAndUpdateProjectName();
  }

  ngAfterViewChecked() {
    this.cdr.detectChanges();
  }

  updateMyAdvances(myAdvances: any) {
    myAdvances = myAdvances.map((data) => ({
      ...data,
      type: 'advance',
      amount: data.adv_amount,
      orig_amount: data.adv_orig_amount,
      created_at: data.adv_created_at,
      currency: data.adv_currency,
      orig_currency: data.adv_orig_currency,
      purpose: data.adv_purpose,
    }));
    return myAdvances;
  }

  updateMyAdvanceRequests(myAdvancerequests: any) {
    myAdvancerequests = myAdvancerequests.map((data) => ({
      ...data,
      type: 'request',
      currency: data.areq_currency,
      amount: data.areq_amount,
      created_at: data.areq_created_at,
      purpose: data.areq_purpose,
      state: data.areq_state,
    }));
    return myAdvancerequests;
  }

  doRefresh(event) {
    forkJoin({
      destroyAdvanceRequestsCacheBuster: this.advanceRequestService.destroyAdvanceRequestsCacheBuster(),
      destroyAdvancesCacheBuster: this.advanceService.destroyAdvancesCacheBuster(),
    })
      .pipe(
        map(() => {
          this.refreshAdvances$.next();
          if (event) {
            event.target.complete();
          }
        })
      )
      .subscribe(noop);
  }

  onAdvanceClick(clickedAdvance: any) {
    const id = clickedAdvance.advanceRequest.adv_id || clickedAdvance.advanceRequest.areq_id;
    let route = 'my_view_advance_request';
    if (
      clickedAdvance.advanceRequest.type === 'request' &&
      clickedAdvance.internalState.state.toLowerCase() === 'inquiry'
    ) {
      route = 'add_edit_advance_request';
    }

    if (clickedAdvance.advanceRequest.type === 'advance') {
      route = 'my_view_advance';
    }

    this.router.navigate(['/', 'enterprise', route, { id }]);
  }

  onHomeClicked() {
    const queryParams: Params = { state: 'home' };
    this.router.navigate(['/', 'enterprise', 'my_dashboard'], {
      queryParams,
    });
  }

  onTaskClicked() {
    const queryParams: Params = { state: 'tasks', tasksFilters: 'advances' };
    this.router.navigate(['/', 'enterprise', 'my_dashboard'], {
      queryParams,
    });
    this.trackingService.tasksPageOpened({
      Asset: 'Mobile',
      from: 'My Advances',
    });
  }

  onCameraClicked() {
    this.router.navigate(['/', 'enterprise', 'camera_overlay', { navigate_back: true }]);
  }

  onFilterClose(filterType: string) {
    const filters = this.filterParams$.value;
    if (filterType === 'sort') {
      this.filterParams$.next({
        ...filters,
        sortParam: null,
        sortDir: null,
      });
    } else if (filterType === 'state') {
      this.filterParams$.next({
        ...filters,
        state: null,
      });
    }
    this.filterPills = this.filtersHelperService.generateFilterPills(this.filterParams$.value);
  }

  async onFilterClick(filterType: string) {
    const filterTypes = {
      state: 'State',
      sort: 'Sort By',
    };
    await this.openFilters(filterTypes[filterType]);
  }

  onFilterPillsClearAll() {
    this.filterParams$.next({});
    this.filterPills = this.filtersHelperService.generateFilterPills(this.filterParams$.value);
  }

  async openFilters(activeFilterInitialName?: string) {
    const filterOptions = [
      {
        name: 'State',
        optionType: FilterOptionType.multiselect,
        options: [
          {
            label: 'Draft',
            value: AdvancesStates.draft,
          },

          {
            label: 'Sent Back',
            value: AdvancesStates.sentBack,
          },
        ],
      } as FilterOptions<string>,
      {
        name: 'Sort By',
        optionType: FilterOptionType.singleselect,
        options: [
          {
            label: 'Created At - New to Old',
            value: SortingValue.creationDateAsc,
          },
          {
            label: 'Created At - Old to New',
            value: SortingValue.creationDateDesc,
          },
          {
            label: 'Approved At - New to Old',
            value: SortingValue.approvalDateAsc,
          },
          {
            label: 'Approved At - Old to New',
            value: SortingValue.approvalDateDesc,
          },
          {
            label: `${this.titleCasePipe.transform(this.projectFieldName)} - A to Z`,
            value: SortingValue.projectAsc,
          },
          {
            label: `${this.titleCasePipe.transform(this.projectFieldName)} - Z to A`,
            value: SortingValue.projectDesc,
          },
        ],
      } as FilterOptions<string>,
    ];
    const filters = await this.filtersHelperService.openFilterModal(
      this.filterParams$.value,
      filterOptions,
      activeFilterInitialName
    );
    if (filters) {
      this.filterParams$.next(filters);
      this.filterPills = this.filtersHelperService.generateFilterPills(this.filterParams$.value, this.projectFieldName);
    }
  }
}
Example #24
Source File: switch-org.page.ts    From fyle-mobile-app with MIT License 4 votes vote down vote up
@Component({
  selector: 'app-switch-org',
  templateUrl: './switch-org.page.html',
  styleUrls: ['./switch-org.page.scss'],
})
export class SwitchOrgPage implements OnInit, AfterViewChecked {
  @ViewChild('search') searchRef: ElementRef;

  @ViewChild('content') contentRef: ElementRef;

  @ViewChild('searchOrgsInput') searchOrgsInput: ElementRef;

  orgs$: Observable<Org[]>;

  filteredOrgs$: Observable<Org[]>;

  searchInput = '';

  isLoading = false;

  activeOrg$: Observable<Org>;

  primaryOrg$: Observable<Org>;

  navigateBack = false;

  isIos = false;

  constructor(
    private platform: Platform,
    private offlineService: OfflineService,
    private loaderService: LoaderService,
    private userService: UserService,
    private activatedRoute: ActivatedRoute,
    private authService: AuthService,
    private secureStorageService: SecureStorageService,
    private storageService: StorageService,
    private router: Router,
    private networkService: NetworkService,
    private orgService: OrgService,
    private userEventService: UserEventService,
    private recentLocalStorageItemsService: RecentLocalStorageItemsService,
    private cdRef: ChangeDetectorRef,
    private trackingService: TrackingService,
    private deviceService: DeviceService
  ) {}

  ngOnInit() {
    this.isIos = this.platform.is('ios');
  }

  ngAfterViewChecked() {
    this.cdRef.detectChanges();
  }

  ionViewWillEnter() {
    const that = this;
    that.searchInput = '';
    that.isLoading = true;
    that.orgs$ = that.offlineService.getOrgs().pipe(shareReplay(1));
    this.navigateBack = !!this.activatedRoute.snapshot.params.navigate_back;

    that.orgs$.subscribe(() => {
      that.cdRef.detectChanges();
    });

    const choose = that.activatedRoute.snapshot.params.choose && JSON.parse(that.activatedRoute.snapshot.params.choose);

    if (!choose) {
      from(that.loaderService.showLoader())
        .pipe(switchMap(() => from(that.proceed())))
        .subscribe(noop);
    } else {
      that.orgs$.subscribe((orgs) => {
        if (orgs.length === 1) {
          from(that.loaderService.showLoader())
            .pipe(switchMap(() => from(that.proceed())))
            .subscribe(noop);
        }
      });
    }
    this.activeOrg$ = this.offlineService.getCurrentOrg();
    this.primaryOrg$ = this.offlineService.getPrimaryOrg();

    const currentOrgs$ = forkJoin([this.orgs$, this.primaryOrg$, this.activeOrg$]).pipe(
      map(([orgs, primaryOrg, activeOrg]) => {
        const currentOrgs = [primaryOrg, ...orgs.filter((org) => org.id !== primaryOrg.id)];
        if (this.navigateBack) {
          return currentOrgs.filter((org) => org.id !== activeOrg.id);
        }
        return currentOrgs;
      }),
      shareReplay(1)
    );

    currentOrgs$.subscribe(() => (this.isLoading = false));

    this.filteredOrgs$ = fromEvent(this.searchOrgsInput.nativeElement, 'keyup').pipe(
      map((event: any) => event.srcElement.value),
      startWith(''),
      distinctUntilChanged(),
      switchMap((searchText) => currentOrgs$.pipe(map((orgs) => this.getOrgsWhichContainSearchText(orgs, searchText))))
    );
  }

  async proceed() {
    const offlineData$ = this.offlineService.load().pipe(shareReplay(1));
    const pendingDetails$ = this.userService.isPendingDetails().pipe(shareReplay(1));
    const eou$ = from(this.authService.getEou());
    const roles$ = from(this.authService.getRoles().pipe(shareReplay(1)));
    const isOnline$ = this.networkService.isOnline().pipe(shareReplay(1));

    forkJoin([offlineData$, pendingDetails$, eou$, roles$, isOnline$])
      .pipe(finalize(() => from(this.loaderService.hideLoader())))
      .subscribe((aggregatedResults) => {
        const [
          [
            orgSettings,
            orgUserSettings,
            allCategories,
            allEnabledCategories,
            costCenters,
            projects,
            perDiemRates,
            customInputs,
            currentOrg,
            orgs,
            accounts,
            currencies,
            homeCurrency,
          ],
          isPendingDetails,
          eou,
          roles,
          isOnline,
        ] = aggregatedResults;

        const pendingDetails = !(currentOrg.lite === true || currentOrg.lite === false) || isPendingDetails;

        if (eou) {
          Sentry.setUser({
            id: eou.us.email + ' - ' + eou.ou.id,
            email: eou.us.email,
            orgUserId: eou.ou.id,
          });
        }

        if (pendingDetails) {
          if (roles.indexOf('OWNER') > -1) {
            this.router.navigate(['/', 'post_verification', 'setup_account']);
          } else {
            this.router.navigate(['/', 'post_verification', 'invited_user']);
          }
        } else if (eou.ou.status === 'ACTIVE') {
          this.router.navigate(['/', 'enterprise', 'my_dashboard']);
        } else if (eou.ou.status === 'DISABLED') {
          this.router.navigate(['/', 'auth', 'disabled']);
        }
      });
  }

  trackSwitchOrg(org: Org, originalEou) {
    const isDestinationOrgActive = originalEou.ou && originalEou.ou.org_id === org.id;
    const isCurrentOrgPrimary = originalEou.ou && originalEou.ou.is_primary;
    from(this.authService.getEou()).subscribe((currentEou) => {
      const properties = {
        Asset: 'Mobile',
        'Switch To': org.name,
        'Is Destination Org Active': isDestinationOrgActive,
        'Is Destination Org Primary': currentEou && currentEou.ou && currentEou.ou.is_primary,
        'Is Current Org Primary': isCurrentOrgPrimary,
        Source: 'User Clicked',
        'User Email': originalEou.us && originalEou.us.email,
        'User Org Name': originalEou.ou && originalEou.ou.org_name,
        'User Org ID': originalEou.ou && originalEou.ou.org_id,
        'User Full Name': originalEou.us && originalEou.us.full_name,
      };
      this.trackingService.onSwitchOrg(properties);
    });
  }

  async switchOrg(org: Org) {
    const originalEou = await this.authService.getEou();
    from(this.loaderService.showLoader())
      .pipe(switchMap(() => this.orgService.switchOrg(org.id)))
      .subscribe(
        () => {
          globalCacheBusterNotifier.next();
          if (originalEou) {
            this.trackSwitchOrg(org, originalEou);
          }
          this.userEventService.clearTaskCache();
          this.recentLocalStorageItemsService.clearRecentLocalStorageCache();
          from(this.proceed()).subscribe(noop);
        },
        async (err) => {
          await this.secureStorageService.clearAll();
          await this.storageService.clearAll();
          this.userEventService.logout();
          globalCacheBusterNotifier.next();
          await this.loaderService.hideLoader();
        }
      );
  }

  signOut() {
    try {
      forkJoin({
        device: this.deviceService.getDeviceInfo(),
        eou: from(this.authService.getEou()),
      })
        .pipe(
          switchMap(({ device, eou }) =>
            this.authService.logout({
              device_id: device.uuid,
              user_id: eou.us.id,
            })
          ),
          finalize(() => {
            this.secureStorageService.clearAll();
            this.storageService.clearAll();
            globalCacheBusterNotifier.next();
            this.userEventService.logout();
          })
        )
        .subscribe(noop);
    } catch (e) {
      this.secureStorageService.clearAll();
      this.storageService.clearAll();
      globalCacheBusterNotifier.next();
    }
  }

  getOrgsWhichContainSearchText(orgs: Org[], searchText: string) {
    return orgs.filter((org) =>
      Object.values(org)
        .map((value) => value && value.toString().toLowerCase())
        .filter((value) => !!value)
        .some((value) => value.toLowerCase().includes(searchText.toLowerCase()))
    );
  }

  resetSearch() {
    this.searchInput = '';
    const searchInputElement = this.searchOrgsInput.nativeElement as HTMLInputElement;
    searchInputElement.value = '';
    searchInputElement.dispatchEvent(new Event('keyup'));
  }

  openSearchBar() {
    this.contentRef.nativeElement.classList.add('switch-org__content-container__content-block--hide');
    this.searchRef.nativeElement.classList.add('switch-org__content-container__search-block--show');
    setTimeout(() => this.searchOrgsInput.nativeElement.focus(), 200);
  }

  cancelSearch() {
    this.resetSearch();
    this.searchOrgsInput.nativeElement.blur();
    this.contentRef.nativeElement.classList.remove('switch-org__content-container__content-block--hide');
    this.searchRef.nativeElement.classList.remove('switch-org__content-container__search-block--show');
  }
}
Example #25
Source File: dxc-input-text.component.ts    From halstack-angular with Apache License 2.0 4 votes vote down vote up
@Component({
  selector: "dxc-input-text",
  templateUrl: "./dxc-input-text.component.html",
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [CssUtils, InputTextService],
})
export class DxcInputTextComponent implements OnInit, OnChanges, AfterViewChecked
{
  @HostBinding("class") className;
  @HostBinding("class.disabled") isDisabled: boolean = false;
  @HostBinding("class.light") lightBackground: boolean = true;
  @HostBinding("class.dark") darkBackground: boolean = false;
  @HostBinding("class.prefixIcon") hasPrefixIcon: boolean = false;

  @Input() public prefix: string;
  @Input() public suffix: string;
  @Input() public prefixIconSrc: string;
  @Input() public suffixIconSrc: string;
  @Input()
  get disabled(): boolean {
    return this._disabled;
  }
  set disabled(value: boolean) {
    this._disabled = coerceBooleanProperty(value);
  }
  private _disabled = false;
  @Input()
  get required(): boolean {
    return this._required;
  }
  set required(value: boolean) {
    this._required = coerceBooleanProperty(value);
  }
  private _required = false;
  @Input()
  get invalid(): boolean {
    return this._invalid;
  }
  set invalid(value: boolean) {
    this._invalid = coerceBooleanProperty(value);
  }
  private _invalid = false;
  @Input()
  get isMasked(): boolean {
    return this._isMasked;
  }
  set isMasked(value: boolean) {
    this._isMasked = coerceBooleanProperty(value);
  }
  private _isMasked;
  @Input() public label: String;
  @Input() public assistiveText: string;
  @Input() public name: string;
  @Input() public value: string;
  @Input() public placeholder: string;
  @Input() public autocompleteOptions: any;

  @Input() public margin: any;
  @Input() public size: string;
  @Input()
  get tabIndexValue(): number {
    return this._tabIndexValue;
  }
  set tabIndexValue(value: number) {
    this._tabIndexValue = coerceNumberProperty(value);
  }
  private _tabIndexValue;

  @Output() public onClickSuffix: EventEmitter<any> = new EventEmitter<any>();
  @Output() public onClickPrefix: EventEmitter<any> = new EventEmitter<any>();
  @Output() public onChange: EventEmitter<string> = new EventEmitter<string>();
  @Output() public onBlur: EventEmitter<any> = new EventEmitter<any>();

  prefixPointer = false;
  suffixPointer = false;

  loading = new BehaviorSubject(false);
  isError = new BehaviorSubject(false);
  renderedValue = "";
  private _valueChangeTrack: boolean;
  options;
  type: string;
  dxcAutocompleteMenu = this.getAutoCompleteStyle();

  @ViewChild("dxcSingleInput", { static: false }) singleInput: ElementRef;

  @ContentChildren(DxcInputPrefixIconComponent)
  dxcInputPrefixIcon: QueryList<DxcInputPrefixIconComponent>;

  @ContentChildren(DxcInputSuffixIconComponent)
  dxcInputSuffixIcon: QueryList<DxcInputSuffixIconComponent>;

  selectionStart: number = 0;
  selectionEnd: number = 0;
  clicked: boolean = false;

  sizes = {
    small: "42px",
    medium: "240px",
    large: "480px",
    fillParent: "100%",
  };

  defaultInputs = new BehaviorSubject<any>({
    prefix: null,
    suffix: null,
    prefixIconSrc: null,
    suffixIconSrc: null,
    disabled: false,
    required: false,
    invalid: false,
    label: null,
    assistiveText: null,
    placeholder: null,
    name: null,
    value: null,
    margin: null,
    size: "medium",
    isMasked: false,
    tabIndexValue: 0,
  });

  constructor(
    private utils: CssUtils,
    private ref: ChangeDetectorRef,
    private service: InputTextService,
    @Optional() public bgProviderService?: BackgroundProviderService
  ) {
    this.bgProviderService.$changeColor.subscribe((value) => {
      if (value === "dark") {
        this.lightBackground = false;
        this.darkBackground = true;
      } else if (value === "light") {
        this.lightBackground = true;
        this.darkBackground = false;
      }
    });
  }

  ngOnInit() {
    this.renderedValue = this.value || "";
    this.bindAutocompleteOptions();
    this.autocompleteFunction("");

    this.service.hasPrefixIcon.subscribe((value) => {
      if (value) {
        this.hasPrefixIcon = value;
      }
    });
  }

  private bindAutocompleteOptions() {
    if (this.autocompleteOptions && Array.isArray(this.autocompleteOptions)) {
      this.options = this.autocompleteOptions;
    }
  }

  ngAfterViewChecked(): void {
    if (this._valueChangeTrack) {
      this._valueChangeTrack = false;
      this.setCursorSelection(this.singleInput);
    }

    if (this.dxcInputPrefixIcon && this.dxcInputPrefixIcon.length !== 0) {
      this.prefixIconSrc = "";
    }

    if (this.dxcInputSuffixIcon && this.dxcInputSuffixIcon.length !== 0) {
      this.suffixIconSrc = "";
    }

    this.ref.detectChanges();
  }

  public ngOnChanges(changes: SimpleChanges): void {
    if (this.isMasked) {
      this.type = "password";
    } else {
      this.type = "text";
    }
    this.isDisabled = this.disabled;

    this.renderedValue = this.value || "";
    this.label = this.label || "";

    this.service.setIsDisabled(this.disabled);

    if (
      this.onClickSuffix.observers !== undefined &&
      this.onClickSuffix.observers.length !== 0
    ) {
      this.suffixPointer = true;
    }

    if (
      this.onClickPrefix.observers !== undefined &&
      this.onClickPrefix.observers.length !== 0
    ) {
      this.prefixPointer = true;
    }

    const inputs = Object.keys(changes).reduce((result, item) => {
      result[item] = changes[item].currentValue;
      return result;
    }, {});

    this.defaultInputs.next({ ...this.defaultInputs.getValue(), ...inputs });
    this.className = `${this.getDynamicStyle(this.defaultInputs.getValue())}`;
    this._valueChangeTrack = true;
  }

  public onChanged($event: any): void {
    this.clicked = false;
    this.selectionStart = $event.target.selectionStart;
    this.selectionEnd = $event.target.selectionEnd;
    this.onChange.emit($event.target.value);
    this.autocompleteFunction($event.target.value);
    if (this.value === undefined || this.value === null) {
      this.renderedValue = $event.target.value;
    } else {
      $event.target.value = this.renderedValue;
    }
  }

  public onClickOption($event: any) {
    this.onChange.emit($event);
    if (this.value === undefined || this.value === null) {
      this.renderedValue = $event;
    } else {
      this.singleInput.nativeElement.value = this.renderedValue;
    }
  }

  autocompleteFunction(value) {
    if (
      value !== undefined &&
      this.autocompleteOptions &&
      Array.isArray(this.autocompleteOptions)
    ) {
      const inputValue = value.toLowerCase();
      this.options = this.autocompleteOptions.filter((option) =>
        option.toLowerCase().includes(inputValue)
      );
    } else if (
      this.autocompleteOptions &&
      typeof this.autocompleteOptions === "function"
    ) {
      this.loading.next(true);
      this.isError.next(false);
      this.autocompleteOptions(value).subscribe(
        (autocompleteOptionsList) => {
          this.options = autocompleteOptionsList;
          this.ref.markForCheck();
          this.loading.next(false);
        },
        (err) => {
          this.isError.next(true);
          this.loading.next(false);
          this.ref.markForCheck();
        }
      );
    } else if (this.autocompleteOptions) {
      this.isError.next(true);
      this.loading.next(false);
      this.ref.markForCheck();
    }
  }

  /**
   * internal click event handler
   *
   * @param $event
   */
  public onClickHandle($event): void {
    this.clicked = true;
  }

  /**
   *Executed when input lost the focus
   */
  public onBlurHandle($event: any): void {
    this.onBlur.emit(this.renderedValue);
  }

  public onClickSuffixHandler($event: any): void {
    this.onClickSuffix.emit($event);
  }

  public onClickPrefixHandler($event: any): void {
    this.onClickPrefix.emit($event);
  }

  private setCursorSelection(input: ElementRef) {
    if (!this.clicked && input) {
      input.nativeElement.selectionStart = this.selectionStart;
      input.nativeElement.selectionEnd = this.selectionEnd;
    }
  }

  calculateWidth(inputs) {
    if (inputs.size === "fillParent") {
      return this.utils.calculateWidth(this.sizes, inputs);
    }
    return css`
      width: ${this.sizes[inputs.size]};
    `;
  }

  getNoIconStyle() {
    return css`
      &:focus {
        outline: none;
      }
      cursor: default;
    `;
  }

  getOnClickPrefixIconStyle() {
    return css`
      &:focus {
        outline: none;
      }
      cursor: default;
    `;
  }

  getStyleOnClickPrefixIconLight() {
    if (this.prefixPointer) {
      return css`
        &:focus {
          outline: -webkit-focus-ring-color auto 1px;
          outline-color: #005FCC;
        }
        cursor: pointer;
      `;
    } else {
      this.getNoIconStyle();
    }
  }

  getStyleOnClickSuffixIconLight() {
    if (this.suffixPointer) {
      return css`
        &:focus {
          outline: -webkit-focus-ring-color auto 1px;
          outline-color: #005FCC;
        }
        cursor: pointer;
      `;
    } else {
      this.getNoIconStyle();
    }
  }

  getStyleOnClickPrefixIconDark() {
    if (this.prefixPointer) {
      return css`
        &:focus {
          outline: -webkit-focus-ring-color auto 1px;
          outline-color: #005FCC;
        }
        cursor: pointer;
      `;
    } else {
      this.getNoIconStyle();
    }
  }

  getStyleOnClickSuffixIconDark() {
    if (this.suffixPointer) {
      return css`
        &:focus {
          outline: -webkit-focus-ring-color auto 1px;
          outline-color: #005FCC;
        }
        cursor: pointer;
      `;
    } else {
      this.getNoIconStyle();
    }
  }

  getDynamicStyle(inputs) {
    return css`
      min-height: 34px;
      max-height: 74px;
      ${this.calculateWidth(inputs)}
      ${this.utils.getMargins(inputs.margin)}
      ${this.getLightStyle()}
      ${this.getDarkStyle()}
      display: inline-flex;
      font-family: var(--inputText-fontFamily);
      &.prefixIcon {
        .mat-form-field .mat-form-field-label-wrapper .mat-form-field-label {
          margin-left: 32px;
          width: 100%;
        }
      }
      dxc-input-prefix-icon,
      dxc-input-suffix-icon {
        display: flex;
        cursor: default;
        &:focus {
          outline: none;
        }
        .containerIcon {
          &:focus {
            outline: none;
          }
        }
      }
      dxc-input-prefix-icon {
        margin-right: 12px;
      }
      dxc-input-suffix-icon {
        margin-left: 8px;
        margin-right: 8px;
      }
      .onClickIconElement {
        cursor: pointer !important;
      }
      .prefixElement {
        margin-right: 12px;
      }
      .suffixElement {
        margin-left: 8px;
        margin-right: 8px;
      }
      .mat-form-field-prefix span {
        font-size: var(--inputText-prefixLabelFontSize);
        font-style: var(--inputText-prefixLabelFontStyle);
        font-weight: var(--inputText-prefixLabelFontWeight);
      }
      .mat-form-field-suffix span {
        font-size: var(--inputText-suffixLabelFontSize);
        font-style: var(--inputText-suffixLabelFontStyle);
        font-weight: var(--inputText-suffixLabelFontWeight);
      }
      .mat-form-field.mat-focused .mat-form-field-ripple {
        height: calc(var(--inputText-underlineThickness) * 2) !important;
      }
      .mat-form-field {
        font-family: var(--inputText-fontFamily);
        line-height: unset;
        width: 100%;
        max-height: 74px;
        input {
          color: var(--inputText-valueFontColor);
          font-size: var(--inputText-valueFontSize);
          font-style: var(--inputText-valueFontStyle);
          font-weight: var(--inputText-valueFontWeight);
          min-height: 22px;
          text-overflow: ellipsis;
        }
        img,
        svg {
          width: 20px;
          height: 20px;
        }
        &.disabled {
          pointer-events: none;
        }
      }
      .mat-form-field {
        &.mat-form-field-should-float {
          .mat-form-field-infix {
            padding-bottom: 6px;
          }
          mat-label {
            font-size: var(--inputText-labelFontSize);
            font-family: var(--inputText-fontFamily);
            font-style: var(--inputText-labelFontStyle);
            font-weight: var(--inputText-labelFontWeight);
          }
        }
        .mat-form-field-label-wrapper {
          display: flex;
          .mat-form-field-label {
            flex-direction: row-reverse;
            justify-content: flex-end;
            display: flex;
          }
        }
        .mat-form-field-subscript-wrapper {
          margin-top: 7px;
        }
        .mat-form-field-infix {
          padding-top: 6px;
          display: flex;
        }
      }
      .mat-form-field-flex {
        align-items: center;
        .mat-form-field-infix {
          color: var(--inputText-labelFontColor);
          font-size: var(--inputText-labelFontSize);
          font-family: var(--inputText-fontFamily);
          font-style: var(--inputText-labelFontStyle);
          font-weight: var(--inputText-labelFontWeight);
          border-top: unset;
        }
      }
      .mat-hint {
        font-family: var(--inputText-fontFamily);
        font-size: var(--inputText-assistiveTextFontSize);
        font-style: var(--inputText-assistiveTextFontStyle);
        font-weight: var(--inputText-assistiveTextFontWeight);
      }
      .mat-form-field-appearance-standard .mat-form-field-underline {
        height: var(--inputText-underlineThickness);
      }
    `;
  }

  getInvalidLightStyle() {
    return css`
      .mat-hint {
        color: var(--inputText-errorColor);
      }
      .mat-form-field-ripple {
        background-color: var(--inputText-errorColor) !important;
        height: 0px !important;
      }
      .mat-form-field-underline {
        background-color: var(--inputText-underlineColor) !important;
        &:focus {
          outline: -webkit-focus-ring-color auto 1px;
          outline-color: var(--inputText-errorColor);
        }
      }
      .mat-form-field.mat-form-field-should-float mat-label {
        color: var(--inputText-errorColor) !important;
      }
      &.mat-focused .mat-form-field-empty mat-label {
        color: var(--inputText-errorColor);
      }
      .mat-form-field-label:not(.mat-form-field-empty) mat-label {
        color: var(--inputText-errorColor);
      }
    `;
  }

  getInvalidDarkStyle() {
    return css`
      .mat-hint {
        color: var(--inputText-errorColorOnDark);
      }
      .mat-form-field-ripple {
        background-color: var(--inputText-errorColorOnDark) !important;
        height: 0px !important;
      }
      .mat-form-field-underline {
        background-color: var(--inputText-underlineColorOnDark) !important;
        &:focus {
          outline: -webkit-focus-ring-color auto 1px;
          outline-color: var(--inputText-errorColorOnDark);
          outline-style: solid !important;
        }
      }
      .mat-form-field.mat-form-field-should-float mat-label {
        color: var(--inputText-errorColorOnDark) !important;
      }
      &.mat-focused .mat-form-field-empty mat-label {
        color: var(--inputText-errorColorOnDark);
      }
      .mat-form-field-label:not(.mat-form-field-empty) mat-label {
        color: var(--inputText-errorColorOnDark);
      }
    `;
  }

  getAutoCompleteStyle() {
    return css`
      padding: 0px 2px;
      border-color: var(--inputText-optionBorderColor);
      border-width: var(--inputText-optionBorderThickness);
      border-style: var(--inputText-optionBorderStyle);
      &::-webkit-scrollbar {
        width: 3px;
      }
      &::-webkit-scrollbar-track {
        background-color: var(--inputText-scrollBarTrackColor);
        opacity: 0.34;
        border-radius: 3px;
      }
      &::-webkit-scrollbar-thumb {
        background-color: var(--inputText-scrollBarThumbColor);
        border-radius: 3px;
      }
      .mat-option {
        background-color: var(--inputText-optionBackgroundColor);
        padding-bottom:  var(--inputText-optionPaddingBottom);
        padding-top:  var(--inputText-optionPaddingTop);
        height: 36px;
        .mat-option-text {
          color: var(--inputText-optionFontColor);
          font-family: var(--inputText-fontFamily);
          font-size: var(--inputText-optionFontSize);
          font-style: var(--inputText-optionFontStyle);
          font-weight: var(--inputText-optionFontWeight);
        }
      }
      .mat-option.mat-selected:not(:hover):not(.mat-option-disabled) {
        color: var(--inputText-optionFontColor);
      }
      .mat-option:hover:not(.mat-option-disabled) {
        background-color: var(--inputText-hoverOptionBackgroundColor);
        .mat-option-text{
          color: var(--inputText-hoverOptionColor);
        }
      }
      .mat-option:focus:not(.mat-option-disabled) {
        outline: -webkit-focus-ring-color auto 2px;
        outline-color: #005FCC;
        outline-style: solid !important;
        outline-offset: -1px;
      }
      .mat-option:active:not(.mat-option-disabled) {
        background-color: var(--inputText-selectedOptionBackgroundColor);
      }
      .errorOption {
        .mat-option-text {
          display: flex;
          align-items: center;
          justify-content: space-between;
        }
      }
    `;
  }

  getLightStyle() {
    return css`
      &.light {
        &.disabled {
          cursor: not-allowed;
          dxc-input-prefix-icon,
          dxc-input-suffix-icon {
            .containerIcon {
              fill: var(--inputText-disabledColor);
            }
          }
          .prefixElement,
          .suffixElement {
            fill: var(--inputText-disabledColor);
            color: var(--inputText-disabledColor);
          }
          .mat-hint {
            color: var(--inputText-disabledColor);
          }
          .mat-form-field-underline {
            background-color: var(--inputText-disabledColor) !important;
          }
          .mat-form-field-empty mat-label {
            color: var(--inputText-disabledColor);
          }
          &.mat-focused .mat-form-field-empty mat-label {
            color: var(--inputText-disabledColor);
          }
          .mat-form-field-label:not(.mat-form-field-empty) mat-label {
            color: var(--inputText-disabledColor);
          }
          .mat-form-field-wrapper {
            .mat-form-field-flex {
              .mat-form-field-infix input {
                color: var(--inputText-disabledColor);
              }
            }
          }
        }
        .onClickIconElement {
          .containerIcon {
            &:focus {
              outline: -webkit-focus-ring-color auto 1px;
              outline-color: #005FCC;
            }
          }
        }
        .mat-form-field.mat-focused .mat-form-field-label {
          color: var(--inputText-labelFontColor) !important;
        }
        .mat-form-field.mat-focused .mat-form-field-ripple {
          background-color: ${this.invalid
            ? "var(--inputText-errorColor) !important"
            : "var(--inputText-underlineFocusColor) !important"};
        }
        .mat-form-field {
          input {
            caret-color: var(--inputText-valueFontColor);
            color: var(--inputText-valueFontColor);
          }
        }
        dxc-input-suffix-icon {
          color: var(--inputText-suffixIconColor);
        }
        dxc-input-prefix-icon {
          color: var(--inputText-prefixIconColor);
        }
        .mat-form-field-prefix span {
          color: var(--inputText-prefixLabelFontColor);
        }
        .mat-form-field-suffix span {
          color: var(--inputText-suffixLabelFontColor);
        }
        label.mat-form-field-label {
          color: var(--inputText-labelFontColor);
        }
        input::placeholder {
          color: var(--inputText-labelFontColor);
        }
        .mat-form-field {
          .mat-form-field-label-wrapper {
            .mat-form-field-label {
              span {
                color: var(--inputText-errorColor);
              }
            }
          }
        }
        ${this.invalid
          ? this.getInvalidLightStyle()
          : css`
              .mat-hint {
                color: var(--inputText-assistiveTextFontColor);
              }
              .mat-form-field-underline {
                background-color: var(--inputText-underlineColor) !important;
                .mat-form-field-ripple {
                  height: 0px;
                  background-color: var(
                    --inputText-underlineFocusColor
                  ) !important;
                }
              }
            `}
        .prefixElement {
          ${this.getStyleOnClickPrefixIconLight()}
          color: var(--inputText-prefixIconColor);
          fill: var(--inputText-prefixIconColor);
        }
        .suffixElement {
          ${this.getStyleOnClickSuffixIconLight()}
          color: var(--inputText-suffixIconColor);
          fill: var(--inputText-suffixIconColor);
        }
      }
    `;
  }

  getDarkStyle() {
    return css`
      &.dark {
        &.disabled {
          cursor: not-allowed;
          dxc-input-prefix-icon,
          dxc-input-suffix-icon {
            .containerIcon {
              fill: var(--inputText-disabledColorOnDark);
            }
          }
          .prefixElement,
          .suffixElement {
            fill: var(--inputText-disabledColorOnDark);
            color: var(--inputText-disabledColorOnDark);
          }
          .mat-hint {
            color: var(--inputText-disabledColorOnDark);
          }
          .mat-form-field-underline {
            background-color: var(
              --inputText-disabledColorOnDark
            ) !important;
          }
          .mat-form-field-empty mat-label {
            color: var(--inputText-disabledColorOnDark);
          }
          &.mat-focused .mat-form-field-empty mat-label {
            color: var(--inputText-disabledColorOnDark);
          }
          .mat-form-field-label:not(.mat-form-field-empty) mat-label {
            color: var(--inputText-disabledColorOnDark);
          }
          .mat-form-field-wrapper {
            .mat-form-field-flex {
              .mat-form-field-infix input {
                color: var(--inputText-disabledColorOnDark);
              }
            }
          }
        }
        .onClickIconElement {
          .containerIcon {
            &:focus {
              outline: -webkit-focus-ring-color auto 1px;
              outline-color: #005FCC;
            }
          }
        }
        .mat-form-field.mat-focused .mat-form-field-label {
          color: var(--inputText-labelFontColorOnDark) !important;
        }
        .mat-form-field.mat-focused .mat-form-field-ripple {
          background-color: ${this.invalid
            ? "var(--inputText-errorColorOnDark) !important"
            : "var(--inputText-underlineFocusColorOnDark) !important"};
        }
        .mat-form-field {
          input {
            caret-color: var(--inputText-valueFontColorOnDark);
            color: var(--inputText-valueFontColorOnDark);
          }
        }
        label.mat-form-field-label {
          color: var(--inputText-labelFontColorOnDark);
        }
        dxc-input-suffix-icon {
          color: var(--inputText-suffixIconColorOnDark);
          fill: var(--inputText-suffixIconColorOnDark);
        }
        dxc-input-prefix-icon {
          color: var(--inputText-prefixIconColorOnDark);
          fill: var(--inputText-prefixIconColorOnDark);
        }
        .mat-form-field-prefix span {
          color: var(--inputText-prefixLabelFontColorOnDark);
        }
        .mat-form-field-suffix span {
          color: var(--inputText-suffixLabelFontColorOnDark);
        }
        label.mat-form-field-label {
          color: var(--inputText-labelFontColorOnDark);
        }
        input::placeholder {
          color: var(--inputText-labelFontColorOnDark);
        }
        .mat-form-field {
          .mat-form-field-label-wrapper {
            .mat-form-field-label {
              span {
                color: var(--inputText-errorColorOnDark);
              }
            }
          }
        }
        ${this.invalid
          ? this.getInvalidDarkStyle()
          : css`
              .mat-hint {
                color: var(--inputText-assistiveTextFontColorOnDark);
              }
              .mat-form-field-underline {
                background-color: var(
                  --inputText-underlineColorOnDark
                ) !important;
                .mat-form-field-ripple {
                  height: 0px;
                  background-color: var(
                    --inputText-underlineColorOnDark
                  ) !important;
                }
              }
            `}
        .prefixElement {
          ${this.getStyleOnClickPrefixIconDark()}
        }
        .suffixElement {
          ${this.getStyleOnClickSuffixIconDark()}
        }
      }
    `;
  }
}
Example #26
Source File: addon-detail.component.ts    From WowUp with GNU General Public License v3.0 4 votes vote down vote up
@Component({
  selector: "app-addon-detail",
  templateUrl: "./addon-detail.component.html",
  styleUrls: ["./addon-detail.component.scss"],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddonDetailComponent implements OnInit, OnDestroy, AfterViewChecked, AfterViewInit {
  @ViewChild("descriptionContainer", { read: ElementRef }) public descriptionContainer!: ElementRef;
  @ViewChild("changelogContainer", { read: ElementRef }) public changelogContainer!: ElementRef;
  @ViewChild("providerLink", { read: ElementRef }) public providerLink!: ElementRef;
  @ViewChild("tabs", { static: false }) public tabGroup!: MatTabGroup;

  private readonly _dependencies: AddonSearchResultDependency[];
  private readonly _changelogSrc = new BehaviorSubject<string>("");
  private readonly _descriptionSrc = new BehaviorSubject<string>("");
  private readonly _destroy$ = new Subject<boolean>();

  public readonly changelog$ = this._changelogSrc.asObservable();
  public readonly description$ = this._descriptionSrc.asObservable();
  public fetchingChangelog = true;
  public fetchingFullDescription = true;
  public selectedTabIndex;
  public requiredDependencyCount = 0;
  public canShowChangelog = true;
  public hasIconUrl = false;
  public thumbnailLetter = "";
  public hasChangeLog = false;
  public showInstallButton = false;
  public showUpdateButton = false;
  public showRemoveButton = false;
  public hasRequiredDependencies = false;
  public title = "";
  public subtitle = "";
  public provider = "";
  public summary = "";
  public externalUrl = "";
  public defaultImageUrl = "";
  public version = "";
  public fundingLinks: AddonFundingLink[] = [];
  public hasFundingLinks = false;
  public fullExternalId = "";
  public externalId = "";
  public displayExternalId = "";
  public isUnknownProvider = false;
  public isMissingUnknownDependencies = false;
  public missingDependencies: string[] = [];
  public previewItems: GalleryItem[] = [];

  public constructor(
    @Inject(MAT_DIALOG_DATA) public model: AddonDetailModel,
    private _dialogRef: MatDialogRef<AddonDetailComponent>,
    private _addonService: AddonService,
    private _addonProviderService: AddonProviderFactory,
    private _cdRef: ChangeDetectorRef,
    private _snackbarService: SnackbarService,
    private _translateService: TranslateService,
    private _sessionService: SessionService,
    private _linkService: LinkService,
    private _addonUiService: AddonUiService,
    private _wowupService: WowUpService,
    public gallery: Gallery
  ) {
    this._dependencies = this.getDependencies();

    this._addonService.addonInstalled$
      .pipe(takeUntil(this._destroy$), filter(this.isSameAddon))
      .subscribe(this.onAddonInstalledUpdate);

    from(this.getChangelog())
      .pipe(
        takeUntil(this._destroy$),
        tap(() => (this.fetchingChangelog = false))
      )
      .subscribe((changelog) => {
        this.hasChangeLog = !!changelog;
        this._changelogSrc.next(changelog);
      });

    from(this.getFullDescription())
      .pipe(
        takeUntil(this._destroy$),
        tap(() => (this.fetchingFullDescription = false))
      )
      .subscribe((description) => this._descriptionSrc.next(description));
  }

  public ngOnInit(): void {
    console.log('model', this.model);
    
    this.canShowChangelog = this._addonProviderService.canShowChangelog(this.getProviderName());

    this.thumbnailLetter = this.getThumbnailLetter();

    this.showInstallButton = !!this.model.searchResult;

    this.showUpdateButton = !!this.model.listItem;

    this.isAddonInstalled()
      .then((isInstalled) => (this.showRemoveButton = isInstalled))
      .catch((e) => console.error(e));

    this.title = this.model.listItem?.addon?.name || this.model.searchResult?.name || "UNKNOWN";

    this.subtitle = this.model.listItem?.addon?.author || this.model.searchResult?.author || "UNKNOWN";

    this.provider = this.model.listItem?.addon?.providerName || this.model.searchResult?.providerName || "UNKNOWN";

    this.summary = this.model.listItem?.addon?.summary || this.model.searchResult?.summary || "";

    this.externalUrl = this.model.listItem?.addon?.externalUrl || this.model.searchResult?.externalUrl || "UNKNOWN";

    this.defaultImageUrl = this.model.listItem?.addon?.thumbnailUrl || this.model.searchResult?.thumbnailUrl || "";

    this.hasIconUrl = !!this.defaultImageUrl;

    this.hasRequiredDependencies = this._dependencies.length > 0;

    this.requiredDependencyCount = this._dependencies.length;

    this.version =
      (this.model.searchResult
        ? this.getLatestSearchResultFile()?.version
        : this.model.listItem?.addon?.installedVersion) ?? "";

    this.fundingLinks = this.model.listItem?.addon?.fundingLinks ?? [];

    this.hasFundingLinks = !!this.model.listItem?.addon?.fundingLinks?.length;

    this.fullExternalId =
      (this.model.searchResult ? this.model.searchResult?.externalId : this.model.listItem?.addon?.externalId) ?? "";

    this.displayExternalId = this.getDisplayExternalId(this.fullExternalId);

    this.isUnknownProvider = this.model.listItem?.addon?.providerName === ADDON_PROVIDER_UNKNOWN;

    this.missingDependencies = this.model.listItem?.addon?.missingDependencies ?? [];

    this.isMissingUnknownDependencies = !!this.missingDependencies.length;

    const imageUrlList = this.model.listItem?.addon?.screenshotUrls ?? this.model.searchResult?.screenshotUrls ?? [];

    this.previewItems = imageUrlList.map((url) => {
      return new ImageItem({ src: url, thumb: url });
    });

    this.gallery.ref().load(this.previewItems);

    this.selectInitialTab().subscribe();
  }

  public ngAfterViewInit(): void {}

  public ngAfterViewChecked(): void {
    const descriptionContainer: HTMLDivElement = this.descriptionContainer?.nativeElement;
    const changelogContainer: HTMLDivElement = this.changelogContainer?.nativeElement;
    formatDynamicLinks(descriptionContainer, this.onOpenLink);
    formatDynamicLinks(changelogContainer, this.onOpenLink);
  }

  public ngOnDestroy(): void {
    this._destroy$.next(true);
    this._destroy$.complete();
    window.getSelection()?.empty();
  }

  public onInstallUpdated(): void {
    this._cdRef.detectChanges();
  }

  public async onSelectedTabChange(evt: MatTabChangeEvent): Promise<void> {
    await this._sessionService.setSelectedDetailsTab(this.getSelectedTabTypeFromIndex(evt.index));
  }

  public onClickExternalId(): void {
    this._snackbarService.showSuccessSnackbar("DIALOGS.ADDON_DETAILS.COPY_ADDON_ID_SNACKBAR", {
      timeout: 2000,
    });
  }

  public async onClickRemoveAddon(): Promise<void> {
    let addon: Addon = null;

    // Addon is expected to be available through the model when browsing My Addons tab
    if (this._sessionService.getSelectedHomeTab() === TAB_INDEX_MY_ADDONS) {
      if (!this.model.listItem?.addon.name) {
        console.warn("Invalid model list item addon");
        return;
      }

      addon = this.model.listItem?.addon;
    } else {
      const selectedInstallation = this._sessionService.getSelectedWowInstallation();
      const externalId = this.model.searchResult?.externalId ?? "";
      const providerName = this.model.searchResult?.providerName ?? "";

      if (!externalId || !providerName || !selectedInstallation) {
        console.warn("Invalid search result when identifying which addon to remove", {
          selectedInstallation,
          externalId,
          providerName,
        });
        return;
      }

      addon = await this._addonService.getByExternalId(externalId, providerName, selectedInstallation.id);
    }

    if (!addon) {
      console.warn("Invalid addon when attempting removal");
      return;
    }

    this._addonUiService
      .handleRemoveAddon(addon)
      .pipe(
        takeUntil(this._destroy$),
        first(),
        map((result) => {
          if (result.removed) {
            this._dialogRef.close();
          }
        })
      )
      .subscribe();
  }

  private getSelectedTabTypeFromIndex(index: number): DetailsTabType {
    switch (index) {
      case 0:
        return "description";
      case 1:
        return "changelog";
      case 2:
        return "previews";
      default:
        return "description";
    }
  }

  private getSelectedTabTypeIndex(tabType: DetailsTabType): number {
    switch (tabType) {
      case "description":
        return 0;
      case "changelog":
        return 1;
      case "previews":
        return this.previewItems.length === 0 ? 0 : 2;
      default:
        return 0;
    }
  }

  private getThumbnailLetter(): string {
    return this.model?.listItem?.thumbnailLetter ?? this.model.searchResult?.name?.charAt(0).toUpperCase() ?? "";
  }

  private getProviderName(): string {
    return this.model.listItem?.addon?.providerName ?? this.model.searchResult?.providerName ?? "";
  }

  private onAddonInstalledUpdate = (evt: AddonUpdateEvent): void => {
    if (this.model.listItem) {
      this.model.listItem.addon = evt.addon;
      this.model.listItem.installState = evt.installState;
    }

    this._cdRef.detectChanges();
  };

  private isSameAddon = (evt: AddonUpdateEvent): boolean => {
    return (
      evt.addon.id === this.model.listItem?.addon?.id || evt.addon.externalId === this.model.searchResult?.externalId
    );
  };

  private onOpenLink = (element: HTMLAnchorElement): boolean => {
    this.confirmLinkNavigation(element.href);

    return false;
  };

  private confirmLinkNavigation(href: string) {
    this._linkService.confirmLinkNavigation(href).subscribe();
  }

  private getChangelog = (): Promise<string> => {
    if (this.model.listItem) {
      return this.getMyAddonChangelog();
    } else if (this.model.searchResult) {
      return this.getSearchResultChangelog();
    }

    return Promise.resolve("");
  };

  private getFullDescription = async (): Promise<string> => {
    const externalId = this.model.searchResult?.externalId ?? this.model.listItem?.addon?.externalId ?? "";
    const providerName = this.model.searchResult?.providerName ?? this.model.listItem?.addon?.providerName ?? "";

    try {
      if (providerName === ADDON_PROVIDER_GITHUB) {
        if (this.model.listItem?.addon?.summary) {
          return this.model.listItem?.addon?.summary;
        }

        throw new Error("Invalid model list item addon");
      }

      const selectedInstallation = this._sessionService.getSelectedWowInstallation();
      if (!selectedInstallation) {
        throw new Error("No selected installation");
      }

      const description = await this._addonService.getFullDescription(
        selectedInstallation,
        providerName,
        externalId,
        this.model?.listItem?.addon
      );

      return description || this._translateService.instant("DIALOGS.ADDON_DETAILS.DESCRIPTION_NOT_FOUND");
    } catch (e) {
      return "";
    }
  };

  private getDependencies(): AddonDependency[] {
    if (this.model.searchResult) {
      return SearchResult.getDependencyType(
        this.model.searchResult,
        this.model.channelType ?? AddonChannelType.Stable,
        AddonDependencyType.Required
      );
    } else if (this.model.listItem) {
      return this.model.listItem.getDependencies(AddonDependencyType.Required);
    }

    return [];
  }

  private async getSearchResultChangelog() {
    const selectedInstallation = this._sessionService.getSelectedWowInstallation();
    if (!selectedInstallation) {
      console.warn("No selected installation");
      return "";
    }
    if (!this.model.searchResult) {
      console.warn("Invalid model searchResult");
      return "";
    }

    return await this._addonService.getChangelogForSearchResult(
      selectedInstallation,
      this.model.channelType ?? AddonChannelType.Stable,
      this.model.searchResult
    );
  }

  private async getMyAddonChangelog() {
    const selectedInstallation = this._sessionService.getSelectedWowInstallation();
    if (!selectedInstallation) {
      console.warn("No selected installation");
      return "";
    }

    if (!this.model.listItem?.addon) {
      console.warn("Invalid list item addon");
      return "";
    }

    return await this._addonService.getChangelogForAddon(selectedInstallation, this.model.listItem.addon);
  }

  private getDisplayExternalId(externalId: string): string {
    if (externalId.indexOf("/") !== -1) {
      return `...${last(externalId.split("/")) ?? ""}`;
    }

    return externalId;
  }

  private getLatestSearchResultFile() {
    return SearchResult.getLatestFile(this.model.searchResult, this.model.channelType ?? AddonChannelType.Stable);
  }

  private async isAddonInstalled(): Promise<boolean> {
    const selectedInstallation = this._sessionService.getSelectedWowInstallation();
    if (!selectedInstallation) {
      console.warn("No selected installation");
      return;
    }

    const externalId = this.model.searchResult?.externalId ?? this.model.listItem?.addon?.externalId ?? "";
    const providerName = this.model.searchResult?.providerName ?? this.model.listItem?.addon?.providerName ?? "";

    if (externalId && providerName) {
      return await this._addonService.isInstalled(externalId, providerName, selectedInstallation);
    }

    console.warn("Invalid list item addon when verifying if installed");
  }

  private selectInitialTab(): Observable<void> {
    return from(this._wowupService.getKeepLastAddonDetailTab()).pipe(
      takeUntil(this._destroy$),
      first(),
      map((shouldUseLastTab) => {
        this.selectedTabIndex = shouldUseLastTab
          ? this.getSelectedTabTypeIndex(this._sessionService.getSelectedDetailsTab())
          : 0;
        this._cdRef.detectChanges();
      }),
      catchError((e) => {
        console.error(e);
        return of(undefined);
      })
    );
  }
}
Example #27
Source File: outletComponent.c.ts    From ngx-dynamic-hooks with MIT License 4 votes vote down vote up
/**
 * The main component of the DynamicHooksModule. Accepts a string of text and replaces all hooks inside of it with dynamically created
 * components that behave just like any other Angular component.
 *
 * Explanation in a nutshell:
 *
 *  1. A dynamic string of content is passed in as @Input() and an array of parsers is retrieved either as @Input() or from the global settings.
 *
 *  2. The content is given to all registered parsers who will find their respective hooks. The hooks are then replaced with dynamic component
 *     placeholder elements in the content string.
 *
 *  3. The content string is then parsed by the native browser HTML parser to create a DOM tree, which is then inserted as the innerHTML of the
 *     OutletComponent.
 *
 *  4. The corresponding components for each hook are dynamically loaded into the previously created placeholder elements as fully-functional
 *     Angular components via ComponentFactory.create().
 *
 *  5. Any @Inputs() & @Outputs() for the components will be registered with them and automatically updated on following change detection runs.
 */

@Component({
  selector: 'ngx-dynamic-hooks',
  template: '',
  styles: []
})
export class OutletComponent implements DoCheck, OnInit, OnChanges, AfterViewInit, AfterViewChecked, OnDestroy {
  @Input() content: string;
  @Input() context: any;
  @Input() globalParsersBlacklist: Array<string>;
  @Input() globalParsersWhitelist: Array<string>;
  @Input() parsers: Array<HookParserEntry>;
  @Input() options: OutletOptions;
  @Output() componentsLoaded: EventEmitter<LoadedComponent[]> = new EventEmitter();
  hookIndex: HookIndex = {};
  activeOptions: OutletOptions = outletOptionDefaults;
  activeParsers: Array<HookParser> = [];
  token = Math.random().toString(36).substr(2, 10);
  initialized: boolean = false;

  // Lifecycle methods
  // ----------------------------------------------------------------------

  constructor(
    private hostElement: ElementRef,
    private outletService: OutletService,
    private componentUpdater: ComponentUpdater,
    private platform: PlatformService,
    private injector: Injector
  ) {}

  ngDoCheck(): void {
    // Update bindings on every change detection run?
    if (this.initialized && !this.activeOptions.updateOnPushOnly) {
      this.refresh(false);
    }
  }

  ngOnInit(): void {
  }

  ngOnChanges(changes): void {
    // If text or options change, reset and parse from scratch
    if (
      changes.hasOwnProperty('content') ||
      changes.hasOwnProperty('globalParsersBlacklist') ||
      changes.hasOwnProperty('globalParsersWhitelist') ||
      changes.hasOwnProperty('parsers') ||
      changes.hasOwnProperty('options')
    ) {
      this.reset();
      this.parse(this.content);
      return;

    // If only context changed, just refresh hook inputs/outputs
    } else if (changes.hasOwnProperty('context')) {
      this.refresh(true);
    }
  }

  ngAfterViewInit(): void {
  }

  ngAfterViewChecked(): void {
  }

  ngOnDestroy(): void {
    this.reset();
  }

  // ----------------------------------------------------------------------

  /**
   * Empties the state of this component
   */
  reset(): void {
    this.outletService.destroy(this.hookIndex);

    // Reset state
    this.platform.setInnerContent(this.hostElement.nativeElement, '');
    this.hookIndex = {};
    this.activeOptions = undefined;
    this.activeParsers = undefined;
    this.initialized = false;
  }

  /**
   * The main method of this component to initialize it
   *
   * @param content - The input content to parse
   */
  parse(content: string): void {
    this.outletService.parse(
      content,
      this.context,
      this.globalParsersBlacklist,
      this.globalParsersWhitelist,
      this.parsers,
      this.options,
      this.hostElement.nativeElement,
      this.hookIndex,
      this.injector
    ).subscribe((outletParseResult: OutletParseResult) => {
      // hostElement and hookIndex are automatically filled
      this.activeParsers = outletParseResult.resolvedParsers;
      this.activeOptions = outletParseResult.resolvedOptions;
      this.initialized = true;

      // Return all loaded components
      const loadedComponents: LoadedComponent[] = Object.values(this.hookIndex).map((hook: Hook) => {
        return {
          hookId: hook.id,
          hookValue: hook.value,
          hookParser: hook.parser,
          componentRef: hook.componentRef
        };
      });
      this.componentsLoaded.emit(loadedComponents);
    });
  }

  /**
   * Updates the bindings for all existing components
   *
   * @param triggerOnDynamicChanges - Whether to trigger the OnDynamicChanges method of dynamically loaded components
   */
  refresh(triggerOnDynamicChanges: boolean): void {
    this.componentUpdater.refresh(this.hookIndex, this.context, this.activeOptions, triggerOnDynamicChanges);
  }
}