@angular/core#DoCheck TypeScript Examples

The following examples show how to use @angular/core#DoCheck. 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: childTest.c.ts    From ngx-dynamic-hooks with MIT License 6 votes vote down vote up
@Component({
  selector: 'dynhooks-childtest',
  templateUrl: './childTest.c.html',
  styleUrls: ['./childTest.c.scss'],
})
export class ChildTestComponent implements DoCheck, OnInit, OnChanges, AfterViewInit, OnDestroy {

  constructor(private cd: ChangeDetectorRef, @Optional() @Inject(BLUBBSERVICETOKEN) private blubbService: any) {
    console.log('CHILD_TEST', this.blubbService);
  }


  ngOnInit () {
    // console.log('textbox init');
  }

  ngOnChanges(changes: any) {
    // console.log('textbox changes');
  }

  ngDoCheck() {
    // console.log('textbox doCheck');
  }

  ngAfterViewInit() {
    // console.log('textbox afterviewinit');
  }

  ngOnDestroy() {
    // console.log('textbox destroy');
  }

}
Example #2
Source File: parentTest.c.ts    From ngx-dynamic-hooks with MIT License 6 votes vote down vote up
@Component({
  selector: 'dynhooks-parenttest',
  templateUrl: './parentTest.c.html',
  styleUrls: ['./parentTest.c.scss'],
  providers: [{provide: BLUBBSERVICETOKEN, useValue: { name: 'blubb' } }]
})
export class ParentTestComponent implements DoCheck, OnInit, OnChanges, AfterViewInit, OnDestroy {
  @ViewChild(ChildTestComponent) childTestComponent: any;

  constructor(private cd: ChangeDetectorRef, @Optional() @Inject(BLUBBSERVICETOKEN) private blubbService: any) {
    console.log('PARENT_TEST', this.blubbService);
  }


  ngOnInit () {
    // console.log('textbox init');
  }

  ngOnChanges(changes: any) {
    // console.log('textbox changes');
  }

  ngDoCheck() {
    // console.log('textbox doCheck');
  }

  ngAfterViewInit() {
    // console.log('textbox afterviewinit');
  }

  ngOnDestroy() {
    // console.log('textbox destroy');
  }

}
Example #3
Source File: eda-input.component.ts    From EDA with GNU Affero General Public License v3.0 6 votes vote down vote up
@Component({
    selector: 'eda-input',
    templateUrl: './eda-input.component.html'
})

export class EdaInputComponent implements AfterViewInit, DoCheck {
    @Input() inject: EdaInput;
    @ViewChild('in') in: NgModel;
    @ViewChild('reference') elementRef: ElementRef;

    ngAfterViewInit() {
        this.inject._model = this.in;
        if (this.elementRef) {
            this.inject.elementRef = this.elementRef;
        }

    }

    ngDoCheck() {
        if (this.in) {
            this.inject._model = this.in;
        }
    }

    public _isEdaInputText(inject: EdaInput): boolean {
        return inject instanceof EdaInputText;
    }
}
Example #4
Source File: receipt-preview-thumbnail.component.ts    From fyle-mobile-app with MIT License 5 votes vote down vote up
@Component({
  selector: 'app-receipt-preview-thumbnail',
  templateUrl: './receipt-preview-thumbnail.component.html',
  styleUrls: ['./receipt-preview-thumbnail.component.scss'],
})
export class ReceiptPreviewThumbnailComponent implements OnInit, DoCheck {
  @ViewChild('slides', { static: false }) imageSlides?: SwiperComponent;

  @Input() attachments: FileObject[];

  @Input() isUploading: boolean;

  @Input() canEdit: boolean;

  @Input() hideLabel: boolean;

  @Output() addMoreAttachments: EventEmitter<void> = new EventEmitter();

  @Output() viewAttachments: EventEmitter<void> = new EventEmitter();

  sliderOptions;

  activeIndex = 0;

  previousCount: number;

  numLoadedImage = 0;

  constructor() {}

  ngOnInit() {
    this.sliderOptions = {
      slidesPerView: 1,
      spaceBetween: 80,
    };
    this.previousCount = this.attachments.length;
  }

  goToNextSlide() {
    this.imageSlides.swiperRef.slideNext(100);
  }

  goToPrevSlide() {
    this.imageSlides.swiperRef.slidePrev(100);
  }

  addAttachments(event) {
    this.addMoreAttachments.emit(event);
  }

  previewAttachments() {
    this.viewAttachments.emit();
  }

  getActiveIndex() {
    this.activeIndex = this.imageSlides.swiperRef.activeIndex;
  }

  ngDoCheck() {
    if (this.attachments?.length !== this.previousCount) {
      this.previousCount = this.attachments.length;
      timer(100).subscribe(() => this.imageSlides.swiperRef.slideTo(this.attachments.length));
      this.getActiveIndex();
    }
  }

  onLoad() {
    this.numLoadedImage++;
  }
}
Example #5
Source File: angular-context.spec.ts    From s-libs with MIT License 5 votes vote down vote up
@Component({ template: '' })
      class LocalComponent implements DoCheck {
        ngDoCheck(): void {
          ranChangeDetectionAfterTimeout = ranTimeout;
        }
      }
Example #6
Source File: angular-context.spec.ts    From s-libs with MIT License 5 votes vote down vote up
@Component({ template: '' })
      class LocalComponent implements DoCheck {
        ngDoCheck(): void {
          ranChangeDetection = true;
        }
      }
Example #7
Source File: angular-context.spec.ts    From s-libs with MIT License 5 votes vote down vote up
@Component({ template: '' })
      class LocalComponent implements DoCheck {
        ngDoCheck(): void {
          ranChangeDetection = true;
        }
      }
Example #8
Source File: category-list.component.ts    From spurtcommerce with BSD 3-Clause "New" or "Revised" License 5 votes vote down vote up
@Component({
    selector: 'app-category-list',
    templateUrl: './category-list.component.html',
    styleUrls: ['./category-list.component.scss']
})
export class CategoryListComponent implements OnChanges, OnInit, OnDestroy, DoCheck {
     // decorator
     @Input() categories;
     @Input() categoryId;
     @Input() changeCategories;
     @Input() isClicked = [];
     @Output() change: EventEmitter<any> = new EventEmitter();
     // categories
     mainCategories;
     public subCategory = [];
     // categories selected
     public activecategory: any;
     public currentCategory: any;
     // subscriptions
     private subscriptions: Array<Subscription> = [];
     @Input() categoryParentId;
     constructor(public listSandBox: ListsSandbox, private commonService: CommonService) {
         this.subscribe();
     }

     ngOnChanges() {
         this.currentCategory = this.categoryId;
         this.isClicked = [];
         this.isClicked[this.categoryId] = true;
         this.listSandBox.getActiveCategory(this.currentCategory);
     }
     // initially calls subscribe method
     ngOnInit() {
         this.subCategory = this.categories;
     }
     ngDoCheck() {
         if (this.categories && !this.mainCategories) {
             this.subCategory = this.categories;
             this.mainCategories = this.categories.filter(category => category.parentId === this.categoryParentId);
         }
     }
     // emit the category id
     public changeCategory(id, activeId, name) {
         this.isClicked = [];
         if (id ===  +this.currentCategory) {
             this.activecategory = '';
             this.listSandBox.removeActiveCategory();
             this.change.emit('');

         } else {
             this.isClicked[id] = true;
             this.currentCategory = id;
             this.activecategory = activeId;
             this.change.emit(id);
         }
     }
     subscribe() {
         this.subscriptions.push(this.listSandBox.subCategoryList$.subscribe(data => {
             if (data && data.length > 0) {
                 this.subCategory = data;
                 console.log('sub category', this.subCategory);
             } else {
                 this.subCategory = this.categories;
             }
         }));
     }
     // OnDestroy Unsubscribe the old subscribed values
     ngOnDestroy() {
         this.subscriptions.forEach(each => {
             each.unsubscribe();
         });
     }
}
Example #9
Source File: chart.ts    From sba-angular with MIT License 5 votes vote down vote up
@Component({
    selector: "sq-ngx-chart",
    templateUrl: "./chart.html",
    styleUrls: ["./chart.scss"]
})
export class NgxChart implements OnInit, OnDestroy, OnChanges, DoCheck {
    @Input() options: ChartOptions;
    @Input() data: ChartDataPoint[];
    @Output("item-click") itemClickEvent: EventEmitter<ChartDataPoint>;
    @ViewChild("wrapper", {static: true}) wrapper: ElementRef;
    @ViewChild("tooltipTemplate", {static: false}) tooltipTemplate;
    @ViewChild("chart", {static: false}) chart: BaseChartComponent;
    localeChange: Subscription;
    attached: boolean;

    constructor(
        protected intlService: IntlService,
        private uiService: UIService) {
        this.itemClickEvent = new EventEmitter<ChartDataPoint>();
    }

    updateChart() {
        if (!!this.chart) this.chart.update();
    }

    private onResize = () => this.updateChart();

    ngOnInit() {
        this.uiService.addElementResizeListener(this.wrapper.nativeElement, this.onResize);
        this.localeChange = Utils.subscribe(this.intlService.events,
            (value) => {
                this.updateChart();
            });
    }

    ngOnDestroy() {
        this.uiService.removeElementResizeListener(this.wrapper.nativeElement, this.onResize);
        this.localeChange.unsubscribe();
    }

    get chartType(): string {
        return Utils.toLowerCase(this.options.type);
    }

    chartComponent(type:string) : Type<any> {
        switch(type){
            case "horizontalbar": return BarHorizontalComponent;
            case "verticaltalbar": return BarVerticalComponent;
            default: return BarVerticalComponent;
        }
    }

    // so we don't end up with no color scheme if the color scheme is not set
    get colorScheme(): string {
        return this.options.colorScheme || "cool";
    }

    ngOnChanges(changes: SimpleChanges) {
    }

    // This is a clunky way of avoiding (briefly) seeing ngx-charts rendering with the default 600x400 dimensions
    ngDoCheck() {
        if (!this.attached && !!this.wrapper) {
            if (document.body.contains(this.wrapper.nativeElement)) {
                this.updateChart();
                this.attached = true;
            }
        }
    }

    select = (dataPoint: ChartDataPoint): void => {
        this.itemClickEvent.emit(dataPoint);
    }
}
Example #10
Source File: content.component.ts    From open-source with MIT License 5 votes vote down vote up
@Component({
  selector: 'app-docs-content',
  templateUrl: './content.component.html',
  styleUrls: ['./content.component.styl'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentComponent implements DoCheck, OnChanges {
  @Input() metadata: DocsMetadata;

  @ViewChild(MatTabGroup) tabs: MatTabGroup;

  source = new Subject<string>();

  initialized = false;
  loadExamples = false;

  get lang(): string {
    return this.route.snapshot.queryParams.lang || this.i18n.lang;
  }

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private content: ContentService,
    private i18n: I18nService,
  ) {}

  ngDoCheck(): void {
    if (!this.initialized && this.tabs) {
      const active = this.route.snapshot.queryParamMap.get('active');
      if (active) {
        const tab = this.tabs._allTabs.find(
          tab => tab.textLabel.toLowerCase() === active
        );
        if (tab.position) {
          this.tabs.selectedIndex = tab.position;
        }
      }
      this.initialized = true;
    }
  }

  ngOnChanges(): void {
    if (this.metadata?.content) {
      // render the markdown content
      const path = this.getLocalizedPath(this.metadata?.content);
      this.content.getFile(`/static/${path}`).subscribe(markdown => {
        this.source.next(markdown);
      });
    }
  }

  onTabChange({ index, tab }: MatTabChangeEvent): void {
    // update the URL
    this.router.navigate(
      [],
      {
        relativeTo: this.route,
        queryParams: index !== 0
          ? { active: tab.textLabel.toLowerCase() } // FIXME update i18n
          : {},
      });

    if (tab.textLabel === 'Examples') { // FIXME update i18n
      this.loadExamples = true;
    }
  }

  private getLocalizedPath(paths: DocsLocalized): string {
    if (this.lang !== 'en' && paths[this.lang]) {
      return paths[this.lang];
    }
    if (!paths['en']) {
      throw new Error('Default english path not found')
    }
    return paths['en'];
  }
}
Example #11
Source File: vg-cue-points.ts    From ngx-videogular with MIT License 5 votes vote down vote up
@Directive({
  selector: '[vgCuePoints]'
})
// tslint:disable-next-line:directive-class-suffix
export class VgCuePoints implements OnInit, OnDestroy, DoCheck {
  // tslint:disable:no-output-on-prefix
  @Output() onEnterCuePoint: EventEmitter<any> = new EventEmitter();
  @Output() onUpdateCuePoint: EventEmitter<any> = new EventEmitter();
  @Output() onExitCuePoint: EventEmitter<any> = new EventEmitter();
  @Output() onCompleteCuePoint: EventEmitter<any> = new EventEmitter();

  subscriptions: Subscription[] = [];
  cuesSubscriptions: Subscription[] = [];

  onLoad$: Observable<any>;
  onEnter$: Observable<any>;
  onExit$: Observable<any>;

  totalCues = 0;

  constructor(public ref: ElementRef) {
  }

  ngOnInit() {
    this.onLoad$ = fromEvent(this.ref.nativeElement, VgEvents.VG_LOAD);
    this.subscriptions.push(this.onLoad$.subscribe(this.onLoad.bind(this)));
  }

  onLoad(event: any) {
    if (event.target && event.target.track) {
      const cues: TextTrackCue[] = event.target.track.cues;

      this.ref.nativeElement.cues = cues;

      this.updateCuePoints(cues);
    } else if (event.target && event.target.textTracks && event.target.textTracks.length) {
        const cues: TextTrackCue[] = event.target.textTracks[0].cues;
        this.ref.nativeElement.cues = cues;
        this.updateCuePoints(cues);
    }
  }

  updateCuePoints(cues: TextTrackCue[]) {
    this.cuesSubscriptions.forEach(s => s.unsubscribe());

    for (let i = 0, l = cues.length; i < l; i++) {
      this.onEnter$ = fromEvent(cues[i], VgEvents.VG_ENTER);
      this.cuesSubscriptions.push(this.onEnter$.subscribe(this.onEnter.bind(this)));

      this.onExit$ = fromEvent(cues[i], VgEvents.VG_EXIT);
      this.cuesSubscriptions.push(this.onExit$.subscribe(this.onExit.bind(this)));
    }
  }

  onEnter(event: any) {
    this.onEnterCuePoint.emit(event.target);
  }

  onExit(event: any) {
    this.onExitCuePoint.emit(event.target);
  }

  ngDoCheck() {

    if (this.ref.nativeElement.track && this.ref.nativeElement.track.cues) {
      const changes = this.totalCues !== this.ref.nativeElement.track.cues.length;

      if (changes) {
        this.totalCues = this.ref.nativeElement.track.cues.length;
        this.ref.nativeElement.cues = this.ref.nativeElement.track.cues;
        this.updateCuePoints(this.ref.nativeElement.track.cues);
      }
    }
  }

  ngOnDestroy() {
    this.subscriptions.forEach(s => s.unsubscribe());
  }
}
Example #12
Source File: vg-cue-points.directive.ts    From ngx-videogular with MIT License 5 votes vote down vote up
@Directive({
  selector: '[vgCuePoints]',
})
export class VgCuePointsDirective implements OnInit, OnDestroy, DoCheck {
  @Output() onEnterCuePoint: EventEmitter<any> = new EventEmitter();
  @Output() onUpdateCuePoint: EventEmitter<any> = new EventEmitter();
  @Output() onExitCuePoint: EventEmitter<any> = new EventEmitter();
  @Output() onCompleteCuePoint: EventEmitter<any> = new EventEmitter();

  subscriptions: Subscription[] = [];
  cuesSubscriptions: Subscription[] = [];

  onLoad$: Observable<any>;
  onEnter$: Observable<any>;
  onExit$: Observable<any>;

  totalCues = 0;

  constructor(public ref: ElementRef) {}

  ngOnInit() {
    this.onLoad$ = fromEvent(this.ref.nativeElement, VgEvents.VG_LOAD);
    this.subscriptions.push(this.onLoad$.subscribe(this.onLoad.bind(this)));
  }

  onLoad(event: any) {
    const cues: TextTrackCue[] = event.target.track.cues;

    this.ref.nativeElement.cues = cues;

    this.updateCuePoints(cues);
  }

  updateCuePoints(cues: TextTrackCue[]) {
    this.cuesSubscriptions.forEach((s) => s.unsubscribe());

    for (let i = 0, l = cues.length; i < l; i++) {
      this.onEnter$ = fromEvent(cues[i], VgEvents.VG_ENTER);
      this.cuesSubscriptions.push(
        this.onEnter$.subscribe(this.onEnter.bind(this))
      );

      this.onExit$ = fromEvent(cues[i], VgEvents.VG_EXIT);
      this.cuesSubscriptions.push(
        this.onExit$.subscribe(this.onExit.bind(this))
      );
    }
  }

  onEnter(event: any) {
    this.onEnterCuePoint.emit(event.target);
  }

  onExit(event: any) {
    this.onExitCuePoint.emit(event.target);
  }

  ngDoCheck() {
    if (this.ref.nativeElement.track && this.ref.nativeElement.track.cues) {
      const changes =
        this.totalCues !== this.ref.nativeElement.track.cues.length;

      if (changes) {
        this.totalCues = this.ref.nativeElement.track.cues.length;
        this.ref.nativeElement.cues = this.ref.nativeElement.track.cues;
        this.updateCuePoints(this.ref.nativeElement.track.cues);
      }
    }
  }

  ngOnDestroy() {
    this.subscriptions.forEach((s) => s.unsubscribe());
  }
}
Example #13
Source File: singleTagTest.c.ts    From ngx-dynamic-hooks with MIT License 5 votes vote down vote up
@Component({
  selector: 'dynhooks-singletagtest',
  templateUrl: './singleTagTest.c.html',
  styleUrls: ['./singleTagTest.c.scss']
})
export class SingleTagTestComponent implements OnDynamicMount, OnDynamicChanges, DoCheck, OnInit, OnChanges, AfterViewInit, OnDestroy {
  nonInputProperty: string = 'this is the default value';
  @Input() inputWithoutBrackets!: string;
  @Input() emptyInputWithoutBrackets!: string;
  @Input() emptyInput!: string;
  @Input() emptyStringInput!: string;
  @Input() _weird5Input$Name13!: string;
  @Input('stringPropAlias') stringProp: any;
  @Input('data-somevalue') dataSomeValue!: string;
  @Input() numberProp: any;
  @Input() booleanProp!: boolean;
  @Input() nullProp: any;
  @Input() undefinedProp: any;
  @Input() simpleObject: any;
  @Input() simpleArray: any;
  @Input() variable!: string;
  @Input() variableLookalike!: string;
  @Input() variableInObject: any;
  @Input() variableInArray!: Array<any>;
  @Input() contextWithoutAnything: any;
  @Input() nestedFunctions: any;
  @Input() nestedFunctionsInBrackets!: Array<any>;
  @Input() everythingTogether!: Array<any>;
  nonOutputEventEmitter: EventEmitter<number> = new EventEmitter();
  @Output('componentClickedAlias') componentClicked: EventEmitter<number> = new EventEmitter();
  @Output('eventTriggeredAlias') eventTriggered: EventEmitter<number> = new EventEmitter();
  @Output() httpResponseReceived: EventEmitter<number> = new EventEmitter();
  ngOnInitTriggered: boolean = false;
  ngOnChangesTriggered: boolean = false;
  mountContext: any;
  mountContentChildren!: Array<DynamicContentChild>;
  changesContext: any;
  changesContentChildren!: Array<DynamicContentChild>;


  constructor (private cd: ChangeDetectorRef, private testService: TestService, @Optional() @Inject(OUTLETCOMPONENTSERVICE) private outletComponentService: any, @Optional() @Inject(TESTSERVICETOKEN) private fakeTestService: any) {
  }

  ngDoCheck() {
  }

  ngOnInit () {
    this.ngOnInitTriggered = true;
  }

  ngOnChanges(changes: any) {
    this.ngOnChangesTriggered = true;
    // console.log(changes);
  }

  ngAfterViewInit() {
  }

  ngOnDestroy() {
  }

  onDynamicMount(data: OnDynamicData) {
    this.mountContext = data.context;
    this.mountContentChildren = (data as any).contentChildren;
  }

  onDynamicChanges(data: OnDynamicData) {
    if (data.hasOwnProperty('context')) {
      this.changesContext = data.context;
    }
    if (data.hasOwnProperty('contentChildren')) {
      this.changesContentChildren = (data as any).contentChildren;
    }
  }

}
Example #14
Source File: multiTagTest.c.ts    From ngx-dynamic-hooks with MIT License 5 votes vote down vote up
@Component({
  selector: 'dynhooks-multitagtest',
  templateUrl: './multiTagTest.c.html',
  styleUrls: ['./multiTagTest.c.scss']
})
export class MultiTagTestComponent implements OnDynamicMount, OnDynamicChanges, DoCheck, OnInit, OnChanges, AfterViewInit, OnDestroy {
  @Input() backgroundColor: string = '#4493ff40';
  @Input() nr!: number;
  @Input() fonts!: Array<string>;
  mountContext: any;
  mountContentChildren!: Array<DynamicContentChild>;
  changesContext: any;
  changesContentChildren!: Array<DynamicContentChild>;

  constructor(private cd: ChangeDetectorRef, private testService: TestService) {
  }


  ngOnInit () {
    // console.log('textbox init');
  }

  ngOnChanges(changes: any) {
    // console.log('textbox changes');
  }

  ngDoCheck() {
    // console.log('textbox doCheck');
  }

  ngAfterViewInit() {
    // console.log('textbox afterviewinit');
  }

  ngOnDestroy() {
    // console.log('textbox destroy');
  }

  onDynamicMount(data: OnDynamicData) {
    this.mountContext = data.context;
    this.mountContentChildren = (data as any).contentChildren;
  }

  onDynamicChanges(data: OnDynamicData) {
    if (data.hasOwnProperty('context')) {
      this.changesContext = data.context;
    }
    if (data.hasOwnProperty('contentChildren')) {
      this.changesContentChildren = (data as any).contentChildren;
    }
  }

}
Example #15
Source File: lazyTest.c.ts    From ngx-dynamic-hooks with MIT License 5 votes vote down vote up
@Component({
  selector: 'dynhooks-lazytest',
  templateUrl: './lazyTest.c.html',
  styleUrls: ['./lazyTest.c.scss']
})
export class LazyTestComponent implements OnDynamicMount, OnDynamicChanges, DoCheck, OnInit, OnChanges, AfterViewInit, OnDestroy {
  @Input() name!: string;
  mountContext: any;
  mountContentChildren!: Array<DynamicContentChild>;
  changesContext: any;
  changesContentChildren!: Array<DynamicContentChild>;

  constructor (private cd: ChangeDetectorRef) {
  }


  ngOnInit () {
    // console.log('textbox init');
  }

  ngOnChanges(changes: any) {
    // console.log('textbox changes');
  }

  ngDoCheck() {
    // console.log('textbox doCheck');
  }

  ngAfterViewInit() {
    // console.log('textbox afterviewinit');
  }

  ngOnDestroy() {
    // console.log('textbox destroy');
  }

  onDynamicMount(data: OnDynamicData) {
    this.mountContext = data.context;
    this.mountContentChildren = (data as any).contentChildren;
  }

  onDynamicChanges(data: OnDynamicData) {
    if (data.hasOwnProperty('context')) {
      this.changesContext = data.context;
    }
    if (data.hasOwnProperty('contentChildren')) {
      this.changesContentChildren = (data as any).contentChildren;
    }
  }

}
Example #16
Source File: inlineTest.c.ts    From ngx-dynamic-hooks with MIT License 5 votes vote down vote up
@Component({
  selector: 'dynhooks-inlinetest',
  templateUrl: './inlineTest.c.html',
  styleUrls: ['./inlineTest.c.scss'],
})
export class InlineTestComponent implements OnDynamicMount, OnDynamicChanges, DoCheck, OnInit, OnChanges, AfterViewInit, OnDestroy {
  @Input() backgroundColor: string = '#25436c';
  @Input() nr!: number;
  @Input() config: any;
  mountContext: any;
  mountContentChildren!: Array<DynamicContentChild>;
  changesContext: any;
  changesContentChildren!: Array<DynamicContentChild>;

  constructor(private cd: ChangeDetectorRef) {
  }

  ngOnInit () {
    // console.log('textbox init');
  }

  ngOnChanges(changes: any) {
    // console.log('textbox changes');
  }

  ngDoCheck() {
    // console.log('textbox doCheck');
  }

  ngAfterViewInit() {
    // console.log('textbox afterviewinit');
  }

  ngOnDestroy() {
    // console.log('textbox destroy');
  }

  onDynamicMount(data: OnDynamicData) {
    this.mountContext = data.context;
    this.mountContentChildren = (data as any).contentChildren;
  }

  onDynamicChanges(data: OnDynamicData) {
    if (data.hasOwnProperty('context')) {
      this.changesContext = data.context;
    }
    if (data.hasOwnProperty('contentChildren')) {
      this.changesContentChildren = (data as any).contentChildren;
    }
  }

}
Example #17
Source File: vg-scrub-bar-cue-points.ts    From ngx-videogular with MIT License 4 votes vote down vote up
// tslint:disable:no-conflicting-lifecycle
// tslint:disable:component-class-suffix
// tslint:disable:no-string-literal
@Component({
  selector: 'vg-scrub-bar-cue-points',
  encapsulation: ViewEncapsulation.None,
  template: `
        <div class="cue-point-container">
            <span *ngFor="let cp of cuePoints" [style.width]="cp.$$style?.width" [style.left]="cp.$$style?.left"
                  class="cue-point"></span>
        </div>
    `,
  styles: [`
        vg-scrub-bar-cue-points {
            display: flex;
            width: 100%;
            height: 5px;
            pointer-events: none;
            position: absolute;
        }
        vg-scrub-bar-cue-points .cue-point-container .cue-point {
            position: absolute;
            height: 5px;
            background-color: rgba(255, 204, 0, 0.7);
        }
        vg-controls vg-scrub-bar-cue-points {
            position: absolute;
            top: calc(50% - 3px);
        }
    ` ]
})
export class VgScrubBarCuePoints implements OnInit, OnChanges, OnDestroy, DoCheck {
  @Input() vgCuePoints: TextTrackCueList;
  @Input() vgFor: string;

  elem: HTMLElement;
  target: any;
  onLoadedMetadataCalled = false;
  cuePoints: Array<any> = [];

  subscriptions: Subscription[] = [];

  totalCues = 0;

  constructor(ref: ElementRef, public API: VgAPI) {
    this.elem = ref.nativeElement;
  }

  ngOnInit() {
    if (this.API.isPlayerReady) {
      this.onPlayerReady();
    } else {
      this.subscriptions.push(this.API.playerReadyEvent.subscribe(() => this.onPlayerReady()));
    }
  }

  onPlayerReady() {
    this.target = this.API.getMediaById(this.vgFor);

    const onTimeUpdate = this.target.subscriptions.loadedMetadata;
    this.subscriptions.push(onTimeUpdate.subscribe(this.onLoadedMetadata.bind(this)));

    if (this.onLoadedMetadataCalled) {
      this.onLoadedMetadata();
    }
  }

  onLoadedMetadata() {
    if (this.vgCuePoints) {
      // We need to transform the TextTrackCueList to Array or it doesn't work on IE11/Edge.
      // See: https://github.com/videogular/videogular2/issues/369
      this.cuePoints = [];

      for (let i = 0, l = this.vgCuePoints.length; i < l; i++) {
        const end = (this.vgCuePoints[i].endTime >= 0) ? this.vgCuePoints[i].endTime : this.vgCuePoints[i].startTime + 1;
        const cuePointDuration = (end - this.vgCuePoints[i].startTime) * 1000;
        let position = '0';
        let percentWidth = '0';

        if (typeof cuePointDuration === 'number' && this.target.time.total) {
          percentWidth = ((cuePointDuration * 100) / this.target.time.total) + '%';
          position = (this.vgCuePoints[i].startTime * 100 / (Math.round(this.target.time.total / 1000))) + '%';
        }

        (this.vgCuePoints[i] as any).$$style = {
          width: percentWidth,
          left: position
        };

        this.cuePoints.push(this.vgCuePoints[i]);
      }
    }
  }

  updateCuePoints() {
    if (!this.target) {
      this.onLoadedMetadataCalled = true;
      return;
    }
    this.onLoadedMetadata();
  }

  ngOnChanges(changes: { [propName: string]: SimpleChange }) {
    if (changes['vgCuePoints'].currentValue) {
      this.updateCuePoints();
    }
  }

  ngDoCheck() {
    if (this.vgCuePoints) {
      const changes = this.totalCues !== this.vgCuePoints.length;

      if (changes) {
        this.totalCues = this.vgCuePoints.length;
        this.updateCuePoints();
      }
    }
  }

  ngOnDestroy() {
    this.subscriptions.forEach(s => s.unsubscribe());
  }
}
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: index.ts    From nativescript-plugins with Apache License 2.0 4 votes vote down vote up
@Directive()
export abstract class AccordionItemsComponent implements DoCheck, OnDestroy, AfterContentInit {
  public abstract get nativeElement(): any;

  protected accordionItemsView: any;
  protected _items: any;
  protected _differ: IterableDiffer<KeyedTemplate>;
  protected _templateHeaderMap: Map<string, KeyedTemplate>;
  protected _templateItemHeaderMap: Map<string, KeyedTemplate>;
  protected _templateItemContentMap: Map<string, KeyedTemplate>;
  protected _templateFooterMap: Map<string, KeyedTemplate>;

  @ViewChild('loader', {read: ViewContainerRef, static: false})
  loader: ViewContainerRef;

  @Output()
  public setupItemView = new EventEmitter<SetupItemViewArgs>();

  @ContentChild(TemplateRef, {static: false})
  itemTemplateQuery: TemplateRef<ItemContext>;

  headerTemplate: TemplateRef<ItemContext>;

  itemHeaderTemplate: TemplateRef<ItemContext>;

  itemContentTemplate: TemplateRef<ItemContext>;

  footerTemplate: TemplateRef<ItemContext>;

  @Input()
  get items() {
    return this._items;
  }

  set items(value: any) {
    this._items = value;
    let needDiffer = true;
    if (value instanceof ObservableArray) {
      needDiffer = false;
    }
    if (needDiffer && !this._differ && isListLikeIterable(value)) {
      this._differ = this._iterableDiffers.find(this._items)
        .create((_index, item) => {
          return item;
        });
    }

    this.accordionItemsView.items = this._items;
  }

  constructor(_elementRef: ElementRef,
              private _iterableDiffers: IterableDiffers) {
    this.accordionItemsView = _elementRef.nativeElement;
    this.accordionItemsView.on('headerLoading', this.onHeaderLoading, this);
    this.accordionItemsView.on('itemHeaderLoading', this.onItemHeaderLoading, this);
    this.accordionItemsView.on('itemContentLoading', this.onItemContentLoading, this);
    this.accordionItemsView.on('footerLoading', this.onFooterLoading, this);

  }

  ngAfterContentInit() {
    this.setItemTemplates();
  }

  ngOnDestroy() {
    this.accordionItemsView.off('headerLoading', this.onHeaderLoading, this);
    this.accordionItemsView.off('itemHeaderLoading', this.onItemHeaderLoading, this);
    this.accordionItemsView.off('itemContentLoading', this.onItemContentLoading, this);
    this.accordionItemsView.off('footerLoading', this.onFooterLoading, this);
  }

  private setItemTemplates() {

    this.itemHeaderTemplate = this.itemTemplateQuery;

    this.accordionItemsView._getHasHeader = () => {
      return false;
    };

    this.accordionItemsView._getHasFooter = () => {
      return false;
    };
    if (this._templateHeaderMap) {
      const templates: KeyedTemplate[] = [];
      this._templateHeaderMap.forEach(value => {
        templates.push(value);
      });

      if (templates.length === 1) {
        this.accordionItemsView.headerTemplateSelector = (item: any, index: number, items: any) => {
          return 'header';
        };
      }

      if (templates.length > 0) {
        this.accordionItemsView._getHasHeader = () => {
          return true;
        };
      }
      this.accordionItemsView.headerTemplates = templates;
    }

    if (this._templateItemHeaderMap) {
      const templates: KeyedTemplate[] = [];
      this._templateItemHeaderMap.forEach(value => {
        templates.push(value);
      });

      this.accordionItemsView.itemHeaderTemplates = templates;

      if (templates.length === 1) {
        this.accordionItemsView.itemHeaderTemplateSelector = (item: any, index: number, items: any) => {
          return 'title';
        };
      }

    } else {
      this.getItemTemplateViewFactory(this.itemHeaderTemplate);
    }

    if (this._templateItemContentMap) {
      const templates: KeyedTemplate[] = [];
      this._templateItemContentMap.forEach(value => {
        templates.push(value);
      });

      if (templates.length === 1) {
        this.accordionItemsView.itemContentTemplateSelector = (item: any, parentIndex: number, index: number, items: any) => {
          return 'content';
        };
      }

      this.accordionItemsView.itemContentTemplates = templates;
    }

    if (this._templateFooterMap) {
      const templates: KeyedTemplate[] = [];
      this._templateFooterMap.forEach(value => {
        templates.push(value);
      });

      if (templates.length === 1) {
        this.accordionItemsView.footerTemplateSelector = (item: any, index: number, items: any) => {
          return 'footer';
        };
      }

      if (templates.length > 0) {
        this.accordionItemsView._getHasFooter = () => {
          return true;
        };
      }

      this.accordionItemsView.footerTemplates = templates;
    }
  }

  public registerTemplate(key: string, template: TemplateRef<ItemContext>) {

    if (key === 'header' || key.startsWith('header-')) {
      if (!this._templateHeaderMap) {
        this._templateHeaderMap = new Map<string, KeyedTemplate>();
      }

      const keyedTemplate = {
        key,
        createView: this.getItemTemplateViewFactory(template)
      };

      this._templateHeaderMap.set(key, keyedTemplate);
    }

    if (key === 'title' || key.startsWith('title-')) {
      if (!this._templateItemHeaderMap) {
        this._templateItemHeaderMap = new Map<string, KeyedTemplate>();
      }

      const keyedTemplate = {
        key,
        createView: this.getItemTemplateViewFactory(template)
      };

      this._templateItemHeaderMap.set(key, keyedTemplate);
    }

    if (key === 'content' || key.startsWith('content-')) {
      if (!this._templateItemContentMap) {
        this._templateItemContentMap = new Map<string, KeyedTemplate>();
      }

      const keyedTemplate = {
        key,
        createView: this.getChildItemTemplateViewFactory(template)
      };

      this._templateItemContentMap.set(key, keyedTemplate);
    }

    if (key === 'footer' || key.startsWith('footer-')) {
      if (!this._templateFooterMap) {
        this._templateFooterMap = new Map<string, KeyedTemplate>();
      }

      const keyedTemplate = {
        key,
        createView: this.getItemTemplateViewFactory(template)
      };

      this._templateFooterMap.set(key, keyedTemplate);
    }
  }

  @profile
  public onHeaderLoading(args: ItemEventData) {
    if (!args.view && !this.headerTemplate) {
      return;
    }

    const index = args.index;
    const items = (<any>args.object).items;
    const currentItem = typeof items.getItem === 'function' ? items.getItem(index) : items[index];
    let viewRef: EmbeddedViewRef<ItemContext>;

    if (args.view) {

      viewRef = args.view[NG_VIEW];
      // Getting angular view from original element (in cases when ProxyViewContainer
      // is used NativeScript internally wraps it in a StackLayout)
      if (!viewRef && args.view instanceof LayoutBase && args.view.getChildrenCount() > 0) {
        viewRef = args.view.getChildAt(0)[NG_VIEW];
      }
    }

    if (!viewRef) {
      viewRef = this.loader.createEmbeddedView(this.headerTemplate, new ItemContext(), 0);
      args.view = getItemViewRoot(viewRef);
      args.view[NG_VIEW] = viewRef;
    }

    this.setupViewRef(viewRef, currentItem, index);

    this.detectChangesOnChild(viewRef, index);
  }

  @profile
  public onItemHeaderLoading(args: ItemEventData) {
    if (!args.view && !this.itemHeaderTemplate) {
      return;
    }

    const index = args.index;
    const items = (<any>args.object).items;
    const currentItem = typeof items.getItem === 'function' ? items.getItem(index) : items[index];
    let viewRef: EmbeddedViewRef<ItemContext>;

    if (args.view) {

      viewRef = args.view[NG_VIEW];
      // Getting angular view from original element (in cases when ProxyViewContainer
      // is used NativeScript internally wraps it in a StackLayout)
      if (!viewRef && args.view instanceof LayoutBase && args.view.getChildrenCount() > 0) {
        viewRef = args.view.getChildAt(0)[NG_VIEW];
      }
    }

    if (!viewRef) {
      viewRef = this.loader.createEmbeddedView(this.itemHeaderTemplate, new ItemContext(), 0);
      args.view = getItemViewRoot(viewRef);
      args.view[NG_VIEW] = viewRef;
    }

    this.setupViewRef(viewRef, currentItem, index);

    this.detectChangesOnChild(viewRef, index);
  }

  @profile
  public onItemContentLoading(args: ItemEventData) {
    if (!args.view && !this.itemContentTemplate) {
      return;
    }

    const index = args.index;
    const childIndex = (args as any).childIndex;
    const childItems = this.accordionItemsView.childItems;
    const items = (<any>args.object).items;
    const currentItem = typeof items.getItem === 'function' ? items.getItem(index)[childItems][childIndex] : items[index][childItems][childIndex];

    let viewRef: EmbeddedViewRef<ChildItemContext>;

    if (args.view) {

      viewRef = args.view[NG_VIEW];
      // Getting angular view from original element (in cases when ProxyViewContainer
      // is used NativeScript internally wraps it in a StackLayout)
      if (!viewRef && args.view instanceof LayoutBase && args.view.getChildrenCount() > 0) {
        viewRef = args.view.getChildAt(0)[NG_VIEW];
      }
    }

    if (!viewRef) {
      viewRef = this.loader.createEmbeddedView(this.itemContentTemplate, new ChildItemContext(), 0);
      args.view = getItemViewRoot(viewRef);
      args.view[NG_VIEW] = viewRef;
    }

    this.setupChildViewRef(viewRef, currentItem, index, childIndex);

    this.detectChangesOnChild(viewRef, index);
  }

  @profile
  public onFooterLoading(args: ItemEventData) {
    if (!args.view && !this.footerTemplate) {
      return;
    }

    const index = args.index;
    const items = (<any>args.object).items;
    const currentItem = typeof items.getItem === 'function' ? items.getItem(index) : items[index];
    let viewRef: EmbeddedViewRef<ItemContext>;

    if (args.view) {

      viewRef = args.view[NG_VIEW];
      // Getting angular view from original element (in cases when ProxyViewContainer
      // is used NativeScript internally wraps it in a StackLayout)
      if (!viewRef && args.view instanceof LayoutBase && args.view.getChildrenCount() > 0) {
        viewRef = args.view.getChildAt(0)[NG_VIEW];
      }
    }

    if (!viewRef) {
      viewRef = this.loader.createEmbeddedView(this.footerTemplate, new ItemContext(), 0);
      args.view = getItemViewRoot(viewRef);
      args.view[NG_VIEW] = viewRef;
    }

    this.setupViewRef(viewRef, currentItem, index);

    this.detectChangesOnChild(viewRef, index);
  }


  public setupViewRef(viewRef: EmbeddedViewRef<ItemContext>, data: any, index: number): void {
    const context = viewRef.context;
    context.$implicit = data;
    context.item = data;
    context.index = index;
    context.even = (index % 2 === 0);
    context.odd = !context.even;

    this.setupItemView.next({view: viewRef, data: data, index: index, context: context});
  }

  public setupChildViewRef(viewRef: EmbeddedViewRef<ChildItemContext>, data: any, parentIndex: number, index: number): void {
    const context = viewRef.context;
    context.$implicit = data;
    context.item = data;
    context.parentIndex = parentIndex;
    context.index = index;
    context.even = (index % 2 === 0);
    context.odd = !context.even;

    this.setupItemView.next({view: viewRef, data: data, index: index, context: context});
  }

  protected getItemTemplateViewFactory(template: TemplateRef<ItemContext>): () => View {
    return () => {
      const viewRef = this.loader.createEmbeddedView(template, new ItemContext(), 0);
      const resultView = getItemViewRoot(viewRef);
      resultView[NG_VIEW] = viewRef;

      return resultView;
    };
  }

  protected getChildItemTemplateViewFactory(template: TemplateRef<ChildItemContext>): () => View {
    return () => {
      const viewRef = this.loader.createEmbeddedView(template, new ChildItemContext(), 0);
      const resultView = getItemViewRoot(viewRef);
      resultView[NG_VIEW] = viewRef;

      return resultView;
    };
  }

  @profile
  private detectChangesOnChild(viewRef: EmbeddedViewRef<ItemContext>, index: number) {

    viewRef.markForCheck();
    viewRef.detectChanges();
  }

  ngDoCheck() {
    if (this._differ) {
      const changes = this._differ.diff(this._items);
      if (changes) {
        this.accordionItemsView.refresh();
      }
    }
  }
}
Example #20
Source File: pager-items-comp.ts    From nativescript-plugins with Apache License 2.0 4 votes vote down vote up
@Directive()
export abstract class TemplatedItemsComponent
  implements DoCheck, OnDestroy, AfterContentInit {
  public abstract get nativeElement(): Pager;

  protected templatedItemsView: Pager;
  protected _items: any;
  protected _differ: IterableDiffer<KeyedTemplate>;
  protected _templateMap: Map<string, KeyedTemplate>;
  private _selectedIndex: number;
  @ViewChild("loader", { read: ViewContainerRef, static: false })
  loader: ViewContainerRef;

  @Output()
  public setupItemView = new EventEmitter<SetupItemViewArgs>();

  @ContentChild(TemplateRef, { static: false })
  itemTemplateQuery: TemplateRef<ItemContext>;

  itemTemplate: TemplateRef<ItemContext>;

  @Input()
  get items() {
    return this._items;
  }

  set items(value: any) {
    this._items = value;
    let needDiffer = true;
    if (value instanceof ObservableArray) {
      needDiffer = false;
    }
    if (needDiffer && !this._differ && isListLikeIterable(value)) {
      this._differ = this._iterableDiffers
        .find(this._items)
        .create((_index, item) => {
          return item;
        });
    }

    this.templatedItemsView.items = this._items;
  }

  @Input()
  get selectedIndex(): number {
    return this._selectedIndex;
  }

  set selectedIndex(value) {
    this._selectedIndex = value;
    this.templatedItemsView.selectedIndex = this._selectedIndex;
  }

  ngAfterViewInit() {
    if (!!(this._selectedIndex)) {
      setTimeout(() => {
        if (isIOS) {
          this.templatedItemsView.scrollToIndexAnimated(
            this._selectedIndex,
            false
          );
        }
        this.templatedItemsView.selectedIndex = this._selectedIndex;
      });
    }
  }

  constructor(
    _elementRef: ElementRef,
    private _iterableDiffers: IterableDiffers,
    private zone: NgZone
  ) {
    this.templatedItemsView = _elementRef.nativeElement;

    this.templatedItemsView.on("itemLoading", this.onItemLoading, this);
    this.templatedItemsView.on("itemDisposing", this.onItemDisposing, this);
  }

  ngAfterContentInit() {
    if (Trace.isEnabled()) {
      PagerLog("TemplatedItemsView.ngAfterContentInit()");
    }
    this.setItemTemplates();
  }

  ngOnDestroy() {
    this.templatedItemsView.off("itemLoading", this.onItemLoading, this);
    this.templatedItemsView.off(
      "itemDisposing",
      this.onItemDisposing,
      this
    );
  }

  private setItemTemplates() {
    if (!this.items) return;
    // The itemTemplateQuery may be changed after list items are added that contain <template> inside,
    // so cache and use only the original template to avoid errors.
    this.itemTemplate = this.itemTemplateQuery;

    if (this._templateMap) {
      if (Trace.isEnabled()) {
        PagerLog("Setting templates");
      }

      const templates: KeyedTemplate[] = [];
      this._templateMap.forEach((value) => {
        templates.push(value);
      });
      this.templatedItemsView.itemTemplates = templates;
    }
  }

  public registerTemplate(key: string, template: TemplateRef<ItemContext>) {
    if (Trace.isEnabled()) {
      PagerLog(`registerTemplate for key: ${key}`);
    }

    if (!this._templateMap) {
      this._templateMap = new Map<string, KeyedTemplate>();
    }

    const keyedTemplate = {
      key,
      createView: this.getItemTemplateViewFactory(template),
    };

    this._templateMap.set(key, keyedTemplate);
  }

  @profile
  public onItemLoading(args: ItemEventData) {
    if (!args.view && !this.itemTemplate) {
      return;
    }

    if (!this.items) return;

    const index = args.index;
    const items = (<any>args.object).items;
    const currentItem =
      typeof items.getItem === "function"
        ? items.getItem(index)
        : items[index];
    let viewRef: EmbeddedViewRef<ItemContext>;

    if (args.view) {
      if (Trace.isEnabled()) {
        PagerLog(`onItemLoading: ${index} - Reusing existing view`);
      }

      viewRef = args.view[NG_VIEW];
      // Getting angular view from original element (in cases when ProxyViewContainer
      // is used NativeScript internally wraps it in a StackLayout)
      if (
        !viewRef &&
        args.view instanceof LayoutBase &&
        args.view.getChildrenCount() > 0
      ) {
        viewRef = args.view.getChildAt(0)[NG_VIEW];
      }

      if (!viewRef && Trace.isEnabled()) {
        PagerError(
          `ViewReference not found for item ${index}. View recycling is not working`
        );
      }
    }

    if (!viewRef) {
      if (Trace.isEnabled()) {
        PagerLog(
          `onItemLoading: ${index} - Creating view from template`
        );
      }

      viewRef = this.loader.createEmbeddedView(
        this.itemTemplate,
        new ItemContext(),
        0
      );
      args.view = getItemViewRoot(viewRef);
      args.view[NG_VIEW] = viewRef;
    }

    this.setupViewRef(viewRef, currentItem, index);

    this.detectChangesOnChild(viewRef, index);
  }

  @profile
  public onItemDisposing(args: ItemEventData) {
    if (!args.view) {
      return;
    }
    let viewRef: EmbeddedViewRef<ItemContext>;

    if (args.view) {
      if (Trace.isEnabled()) {
        PagerLog(
          `onItemDisposing: ${args.index} - Removing angular view`
        );
      }

      viewRef = args.view[NG_VIEW];
      // Getting angular view from original element (in cases when ProxyViewContainer
      // is used NativeScript internally wraps it in a StackLayout)
      if (
        !viewRef &&
        args.view instanceof LayoutBase &&
        args.view.getChildrenCount() > 0
      ) {
        viewRef = args.view.getChildAt(0)[NG_VIEW];
      }

      if (!viewRef && Trace.isEnabled()) {
        PagerError(
          `ViewReference not found for item ${args.index}. View disposing is not working`
        );
      }
    }

    if (viewRef) {
      if (Trace.isEnabled()) {
        PagerLog(
          `onItemDisposing: ${args.index} - Disposing view reference`
        );
      }

      viewRef.destroy();
    }
  }

  public setupViewRef(
    viewRef: EmbeddedViewRef<ItemContext>,
    data: any,
    index: number
  ): void {
    const context = viewRef.context;
    context.$implicit = data;
    context.item = data;
    context.index = index;
    context.even = index % 2 === 0;
    context.odd = !context.even;

    this.setupItemView.next({
      view: viewRef,
      data: data,
      index: index,
      context: context,
    });
  }

  protected getItemTemplateViewFactory(
    template: TemplateRef<ItemContext>
  ): () => View {
    return () => {
      const viewRef = this.loader.createEmbeddedView(
        template,
        new ItemContext(),
        0
      );
      const resultView = getItemViewRoot(viewRef);
      resultView[NG_VIEW] = viewRef;

      return resultView;
    };
  }

  @profile
  private detectChangesOnChild(
    viewRef: EmbeddedViewRef<ItemContext>,
    index: number
  ) {
    if (Trace.isEnabled()) {
      PagerLog(`Manually detect changes in child: ${index}`);
    }

    this.zone.run(() => {
      viewRef.markForCheck();
      viewRef.detectChanges();
    })
  }

  ngDoCheck() {
    if (this._differ) {
      if (Trace.isEnabled()) {
        PagerLog("ngDoCheck() - execute differ");
      }

      const changes = this._differ.diff(this._items);
      if (changes) {
        if (Trace.isEnabled()) {
          PagerLog("ngDoCheck() - refresh");
        }

        this.templatedItemsView.refresh();
      }
    }
  }
}
Example #21
Source File: vg-scrub-bar-cue-points.component.ts    From ngx-videogular with MIT License 4 votes vote down vote up
// tslint:disable-next-line: no-conflicting-lifecycle
@Component({
  selector: 'vg-scrub-bar-cue-points',
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="cue-point-container">
      <span
        *ngFor="let cp of cuePoints"
        [style.width]="cp.$$style?.width"
        [style.left]="cp.$$style?.left"
        class="cue-point"
      ></span>
    </div>
  `,
  styles: [
    `
      vg-scrub-bar-cue-points {
        display: flex;
        width: 100%;
        height: 5px;
        pointer-events: none;
        position: absolute;
      }
      vg-scrub-bar-cue-points .cue-point-container .cue-point {
        position: absolute;
        height: 5px;
        background-color: rgba(255, 204, 0, 0.7);
      }
      vg-controls vg-scrub-bar-cue-points {
        position: absolute;
        top: calc(50% - 3px);
      }
    `,
  ],
})
export class VgScrubBarCuePointsComponent
  implements OnInit, OnChanges, OnDestroy, DoCheck {
  @Input() vgCuePoints: TextTrackCueList;
  @Input() vgFor: string;

  elem: HTMLElement;
  target: any;
  onLoadedMetadataCalled = false;
  cuePoints: Array<any> = [];

  subscriptions: Subscription[] = [];

  totalCues = 0;

  constructor(ref: ElementRef, public API: VgApiService) {
    this.elem = ref.nativeElement;
  }

  ngOnInit() {
    if (this.API.isPlayerReady) {
      this.onPlayerReady();
    } else {
      this.subscriptions.push(
        this.API.playerReadyEvent.subscribe(() => this.onPlayerReady())
      );
    }
  }

  onPlayerReady() {
    this.target = this.API.getMediaById(this.vgFor);

    const onTimeUpdate = this.target.subscriptions.loadedMetadata;
    this.subscriptions.push(
      onTimeUpdate.subscribe(this.onLoadedMetadata.bind(this))
    );

    if (this.onLoadedMetadataCalled) {
      this.onLoadedMetadata();
    }
  }

  onLoadedMetadata() {
    if (this.vgCuePoints) {
      // We need to transform the TextTrackCueList to Array or it doesn't work on IE11/Edge.
      // See: https://github.com/videogular/videogular2/issues/369
      this.cuePoints = [];

      for (let i = 0, l = this.vgCuePoints.length; i < l; i++) {
        const end =
          this.vgCuePoints[i].endTime >= 0
            ? this.vgCuePoints[i].endTime
            : this.vgCuePoints[i].startTime + 1;
        const cuePointDuration = (end - this.vgCuePoints[i].startTime) * 1000;
        let position = '0';
        let percentWidth = '0';

        if (typeof cuePointDuration === 'number' && this.target.time.total) {
          percentWidth =
            (cuePointDuration * 100) / this.target.time.total + '%';
          position =
            (this.vgCuePoints[i].startTime * 100) /
              Math.round(this.target.time.total / 1000) +
            '%';
        }

        (this.vgCuePoints[i] as any).$$style = {
          width: percentWidth,
          left: position,
        };

        this.cuePoints.push(this.vgCuePoints[i]);
      }
    }
  }

  updateCuePoints() {
    if (!this.target) {
      this.onLoadedMetadataCalled = true;
      return;
    }
    this.onLoadedMetadata();
  }

  ngOnChanges(changes: { [propName: string]: SimpleChange }) {
    if (changes.vgCuePoints.currentValue) {
      this.updateCuePoints();
    }
  }

  ngDoCheck() {
    if (this.vgCuePoints) {
      const changes = this.totalCues !== this.vgCuePoints.length;

      if (changes) {
        this.totalCues = this.vgCuePoints.length;
        this.updateCuePoints();
      }
    }
  }

  ngOnDestroy() {
    this.subscriptions.forEach((s) => s.unsubscribe());
  }
}
Example #22
Source File: pagination.directive.ts    From angular-custom-material-paginator with MIT License 4 votes vote down vote up
@Directive({
  selector: '[appPagination]'
})

export class PaginatorDirective implements DoCheck, AfterViewInit {
  private currentPage: number;
  private pageGapTxt: string[];
  private rangeStart: number;
  private rangeEnd: number;
  private buttons: MatButton[] = [];
  private showTotalPages: number;
  private checkPage: number[];

  constructor(
    @Host() @Self() @Optional() private readonly matPag: MatPaginator,
    private readonly ViewContainer: ViewContainerRef,
    private readonly renderer: Renderer2
  ) {
    this.currentPage = 1;
    this.pageGapTxt = ['•••', '---'];
    this.showTotalPages = 3;
    this.checkPage = [0, 0, 0];
    // Display custom range label text
    this.matPag._intl.getRangeLabel = (page: number, pageSize: number, length: number): string => {
      const startIndex = page * pageSize;
      const endIndex = startIndex < length ?
        Math.min(startIndex + pageSize, length) :
        startIndex + pageSize;
      return length > 0 ? 'Showing ' + (startIndex + 1) + ' – ' + endIndex + ' of ' + length + ' records' : 'Showing 0 – 0 of 0 records';
    };
    // Subscribe to rerender buttons when next page and last page button is used
    this.matPag.page.subscribe((paginator: PageEvent) => {
      this.currentPage = paginator.pageIndex;
      this.matPag.pageIndex = paginator.pageIndex;
      this.initPageRange();
    });
  }

  ngDoCheck(): void {
    // Reset paginator if the pageSize, pageIndex, length changes
    if (this.matPag?.length !== this.checkPage[0]
      ||
      this.matPag?.pageSize !== this.checkPage[1]
      ||
      this.matPag?.pageIndex !== this.checkPage[2]
    ) {
      const pageCount = this.matPag.getNumberOfPages();
      if (this.currentPage > pageCount && pageCount !== 0) {
        this.currentPage = 1;
        this.matPag.pageIndex = 0;
      }
      this.currentPage = this.matPag.pageIndex;
      this.initPageRange();
      this.checkPage = [this.matPag.length, this.matPag.pageSize, this.matPag.pageIndex];
    }
  }

  private buildPageNumbers = () => {
    let dots: boolean[];
    let page: number;
    let pageDifference: number;
    let startIndex: number;
    let totalPages: number;
    totalPages = this.matPag.getNumberOfPages();
    // Container div with paginator elements
    const actionContainer = this.ViewContainer.element.nativeElement.querySelector(
      'div.mat-paginator-range-actions'
    );
    // Button that triggers the next page action
    const nextPageNode = this.ViewContainer.element.nativeElement.querySelector(
      'button.mat-paginator-navigation-next'
    );
    // Label showing the page range
    const pageRange = this.ViewContainer.element.nativeElement.querySelector(
      'div.mat-paginator-range-label'
    );

    let prevButtonCount = this.buttons.length;

    // Remove buttons before creating new ones
    if (prevButtonCount > 0) {
      this.buttons.forEach(button => {
        this.renderer.removeChild(actionContainer, button);
      });
      // Empty state array
      prevButtonCount = 0;
    }

    this.renderer.addClass(pageRange, 'custom-paginator-counter');
    this.renderer.addClass(actionContainer, 'custom-paginator-container');

    // Initialize next page and last page buttons
    if (prevButtonCount === 0) {
      const nodeArray = actionContainer.childNodes;
      setTimeout(() => {
        for (const node of nodeArray) {
          if (node.nodeName === 'BUTTON') {
            // Next Button styles
            if (node.innerHTML.length > 100 && node.disabled) {
              this.renderer.addClass(node, 'custom-paginator-arrow-disabled');
              this.renderer.removeClass(node, 'custom-paginator-arrow-enabled');
            } else if (
              node.innerHTML.length > 100 &&
              !node.disabled
            ) {
              this.renderer.addClass(node, 'custom-paginator-arrow-enabled');
              this.renderer.removeClass(node, 'custom-paginator-arrow-disabled');
            }
          }
        }
      });
    }

    dots = [false, false];

    if (totalPages > 0) {
      this.renderer.insertBefore(
        actionContainer,
        this.createButton('0', this.matPag.pageIndex),
        nextPageNode
      );
    }

    page = this.showTotalPages + 2;
    pageDifference = totalPages - page;
    startIndex = Math.max(this.currentPage - this.showTotalPages - 2, 1);

    for (let index = startIndex; index < totalPages - 1; index = index + 1) {
      if (
        (index < page && this.currentPage <= this.showTotalPages)
        ||
        (index >= this.rangeStart && index <= this.rangeEnd)
        ||
        (this.currentPage > pageDifference && index >= pageDifference)
        ||
        (totalPages < this.showTotalPages + page)
      ) {
        this.renderer.insertBefore(
          actionContainer,
          this.createButton(`${index}`, this.matPag.pageIndex),
          nextPageNode
        );
      } else {
        if (index > this.rangeEnd && !dots[0]) {
          this.renderer.insertBefore(
            actionContainer,
            this.createButton(this.pageGapTxt[0], this.matPag.pageIndex),
            nextPageNode
          );
          dots[0] = true;
          break;
        }
        if (index < this.rangeEnd && !dots[1]) {
          this.renderer.insertBefore(
            actionContainer,
            this.createButton(this.pageGapTxt[1], this.matPag.pageIndex),
            nextPageNode
          );
          dots[1] = true;
        }
      }
    }

    if (totalPages > 1) {
      this.renderer.insertBefore(
        actionContainer,
        this.createButton(`${totalPages - 1}`, this.matPag.pageIndex),
        nextPageNode
      );
    }
  }

  private createButton(index: string, pageIndex: number): MatButton {
    const linkBtn: MatButton = this.renderer.createElement('button');
    this.renderer.setAttribute(linkBtn, 'class', 'custom-paginator-page');
    this.renderer.addClass(linkBtn, 'custom-paginator-page-enabled');
    if (index === this.pageGapTxt[0] || index === this.pageGapTxt[1]) {
      this.renderer.addClass(linkBtn, 'custom-paginator-arrow-enabled');
    }
    const pagingTxt = isNaN(+ index) ? this.pageGapTxt[0] : (+ index + 1);
    const text = this.renderer.createText(pagingTxt + '');
    this.renderer.addClass(linkBtn, 'mat-custom-page');
    switch (index) {
      case `${pageIndex}`:
        this.renderer.setAttribute(linkBtn, 'disabled', 'disabled');
        this.renderer.removeClass(linkBtn, 'custom-paginator-page-enabled');
        this.renderer.addClass(linkBtn, 'custom-paginator-page-disabled');
        break;
      case this.pageGapTxt[0]:
        this.renderer.listen(linkBtn, 'click', () => {
          this.switchPage(this.currentPage < this.showTotalPages + 1
             ? this.showTotalPages + 2
            : this.currentPage + this.showTotalPages - 1
          );
        });
        break;
      case this.pageGapTxt[1]:
        this.renderer.listen(linkBtn, 'click', () => {
          this.switchPage(this.currentPage > this.matPag.getNumberOfPages() - this.showTotalPages - 2
            ? this.matPag.getNumberOfPages() - this.showTotalPages - 3
            : this.currentPage - this.showTotalPages + 1
          );
        });
        break;
      default:
        this.renderer.listen(linkBtn, 'click', () => {
          this.switchPage(+ index);
        });
        break;
    }
    this.renderer.appendChild(linkBtn, text);
    // Add button to private array for state
    this.buttons.push(linkBtn);
    return linkBtn;
  }

  /**
   * @description calculates the button range based on class input parameters and based on current page index value.
   */
  private initPageRange(): void {
    this.rangeStart = this.currentPage - this.showTotalPages / 2;
    this.rangeEnd = this.currentPage + this.showTotalPages / 2;
    this.buildPageNumbers();
  }

  private switchPage(index: number): void {
    this.matPag.pageIndex = index;
    this.matPag.page.emit({
      previousPageIndex: this.currentPage,
      pageIndex: index,
      pageSize: this.matPag.pageSize,
      length: this.matPag.length
    });
    this.currentPage = index;
    this.initPageRange();
  }

  public ngAfterViewInit(): void {
    this.rangeStart = 0;
    this.rangeEnd = this.showTotalPages - 1;
    this.initPageRange();
  }
}
Example #23
Source File: search-form.component.ts    From sba-angular with MIT License 4 votes vote down vote up
@Component({
  selector: 'app-search-form',
  templateUrl: './search-form.component.html',
  styleUrls: ['./search-form.component.scss']
})
export class SearchFormComponent implements OnInit, DoCheck, OnDestroy {
  searchControl: FormControl;
  form: FormGroup;
  autofocus = 0;

  /** Expression from the query selects, if any ("simple"/"selects" field search mode) */
  fieldSearchExpression?: string;

  /** Result of query parsing ("advanced" field search mode) */
  parseResult?: ParseResult;

  /** A reference to the AutocompleteExtended directive, needed to get the field search selections, if any */
  @ViewChild(AutocompleteExtended) autocompleteDirective: AutocompleteExtended;

  @ViewChild('searchInput') searchInput: ElementRef;

  // Advanced search flags
  showAdvancedSearch: boolean;
  initAdvanced: boolean;
  enableAdvancedForm = false; // Show the advanced form button or not

  /** Define if a filter, NOT belonging to fielded & advanced search, is currently applied to the searchService.query */
  isFiltering = false;

  /** Specify if already applied filters should be kept or not while chaining searches */
  keepFilters = false;
  keepFiltersTitle = 'msg#searchForm.notKeepFilters';
  enableKeepFilters = false; // Show the "keep filters" button or not

  /** USED ALONG WITH keepFilters context, to optionally reset the advanced-search or not */
  keepAdvancedSearchFilters = false;

  /** Define if should stay on the same tab even after a new search */
  keepTab = false;

  /** Voice recognition */
  voiceRecognitionState = false;
  enableVoiceRecognition = false; // Show the voice recognition button or not

  hasScroll = false;
  @ViewChild('searchContainer') searchContainer: ElementRef;
  private timeout: any;

  private subscriptions: Subscription[] = [];

  constructor(
    public voiceService: VoiceRecognitionService,
    public searchService: SearchService,
    public loginService: LoginService,
    private formBuilder: FormBuilder,
    public appService: AppService,
    public prefs: UserPreferences,
    public firstPageService: FirstPageService,
    public advancedService: AdvancedService,
    public route: ActivatedRoute) {

    this.voiceService.init();

    this.subscriptions.push(...[
        this.voiceService.started.subscribe(state => {
        this.voiceRecognitionState = state;
      }),
        this.voiceService.text.subscribe(value => {
        this.searchControl.setValue(value);
      })
    ]);

  }

  /**
   * Initialization of the form
   */
  ngOnInit() {
    this.searchControl = new FormControl('');
    this.form = this.formBuilder.group({
      search: this.searchControl
    });

    // Every time the query changes, we want to update the search form
    this.subscriptions.push(this.searchService.queryStream.subscribe(query => {
      // Update main search bar
      this.searchControl.setValue(this.searchService.query?.text || '');
      this.fieldSearchExpression = query?.findSelect("search-form")?.expression;
      this.autofocus++;

      // Update advanced form
      this.form.get('treepath')?.setValue(this.advancedService.getValue('treepath'));
      this.form.get('authors')?.setValue(this.advancedService.getValue('authors'));
      this.form.get('size')?.setValue(this.advancedService.getRangeValue('size'));
      this.form.get('modified')?.setValue(this.advancedService.getRangeValue('modified'));
      this.form.get('person')?.setValue(this.advancedService.getValue('person'));
      this.form.get('docformat')?.setValue(this.advancedService.getValue('docformat'));

      // Update the filtering status
      this._updateFilteringStatus();

      // Check user preferences regarding keeping filters
      if(typeof this.prefs.get('keep-filters-state') !== 'undefined') {
        this.keepFilters = this.prefs.get('keep-filters-state');
        this.keepFiltersTitle = this.keepFilters ? 'msg#searchForm.keepFilters' : 'msg#searchForm.notKeepFilters';
      }
    }));

    // Initialize the search form options (either now, or when login is complete)
    if(this.appService.app) {
      this.setOptions();
    }
    else {
      this.subscriptions.push(this.loginService.events.subscribe(event => {
        if(this.appService.app) {
          this.setOptions();
        }
      }));
    }
  }

  ngDoCheck() {
    // Check if the input has a scrollbar
    this.hasScroll = this.searchContainer?.nativeElement.scrollWidth > this.searchContainer?.nativeElement.clientWidth;
  }

  ngOnDestroy() {
    this.subscriptions.map(item => item.unsubscribe());
  }

  setOptions() {
    const features = this.appService.app?.data?.features as string[] || FEATURES;
    features.forEach(feature =>{
      switch(feature){
        case "advanced-form": this.enableAdvancedForm = true; break;
        case "keep-advanced-form-filters": this.keepAdvancedSearchFilters = true; break;
        case "keep-tab": this.keepTab = true; break;
        case "keep-filters": {
          // Initialize keep filter flag, if not already in preferences
          if(typeof this.prefs.get('keep-filters-state') === 'undefined') {
            this.keepFilters = true;
          }
          break;
        }
        case "toggle-keep-filters": this.enableKeepFilters = true; break;
        case "voice-recognition": this.enableVoiceRecognition = true; break;
      }
    });
  }

  /**
   * Trigger a search query via the search service
   */
  search() {
    if(this.loginService.complete && this.form.valid) {

      /** Hide autocomplete suggestions */
      this.searchInput.nativeElement.blur();

      /** Store relevant filters (tab ...)*/
      const queryTab = this.searchService.query.tab;

      /** If this.keepFilters = false, clear the query and reset all its filters. */
      if (!this.keepFilters) {
        this.searchService.clearQuery();

        /** MUST explicitly reset the advanced form if this.keepAdvancedSearchFilters = false */
        if (!this.keepAdvancedSearchFilters && !this.showAdvancedSearch) {
          this.clearAdvancedForm();
        }
      }
      
      /** Close the advanced form */
      this.showAdvancedSearch = false;

      /** Update the new query with entered text */
      this.searchService.query.text = this.searchControl?.value || "";

      /** Update advanced search filters */
      this.advancedService.setSelect('treepath', this.form.get('treepath')?.value);
      this.advancedService.setSelect('authors', this.form.get('authors')?.value);
      this.advancedService.setRangeSelect('size', this.form.get('size')?.value);
      this.advancedService.setRangeSelect('modified', this.form.get('modified')?.value);
      this.advancedService.setSelect('person', this.form.get('person')?.value);
      this.advancedService.setSelect('docformat', this.form.get('docformat')?.value);

      /** Add select from the fielded search ("selects", aka "simple" mode) */
      if(this.getMode() === "selects") {
        this.searchService.query.removeSelect("search-form"); // Prevent having multiple instance if this.keepFilters = true
        const expr = this.autocompleteDirective.getFieldSearchExpression();
        if(expr) {
          this.searchService.query.addSelect(expr, "search-form");
        }
      }

      // if this.keepTab, stay on the same tab even after a new search
      if (this.keepTab && !!queryTab) {
        this.searchService.query.tab = queryTab;
      }

      /** Trigger the search with the new criteria */
      this.searchService.searchText("search");
    }
  }

  /**
   * Test if the search input is not empty
   */
  hasContent(): boolean {
    return this.searchControl.value
      || this.fieldSearchExpression;
  }

  /**
   * Clears the search input and the fielded search
   */
  clearForm() {
    this.searchControl.reset();
    this.fieldSearchExpression = "";
  }

  /**
   * Test if the advanced form has non-undefined values set
   */
  hasAdvancedContent(): boolean {
    return this.form.get("treepath")?.value
      || this.form.get("authors")?.value
      || this.form.get("size")?.value?.find(v => v)
      || this.form.get("modified")?.value?.find(v => v)
      || this.form.get("person")?.value
      || this.form.get("docformat")?.value;
  }

  /**
   * Clears the advanced-search form
   */
  clearAdvancedSearch(): void {
    this.advancedService.resetAdvancedValues();
    /** Close the advanced form */
    this.showAdvancedSearch = false;
  }

  /**
   * Test if the query contains advanced-search related filters
   */
  isAdvancedSearchActive(): boolean {
    return this.searchService.query.hasAdvanced();
  }

  /**
   * Clear only the advanced form
   */
  clearAdvancedForm() {
    if(this.initAdvanced) {
      this.advancedService.resetControl(this.form.get("treepath")!);
      this.advancedService.resetControl(this.form.get("authors")!);
      this.advancedService.resetRangeControl(this.form.get("size")!);
      this.advancedService.resetRangeControl(this.form.get("modified")!);
      this.advancedService.resetControl(this.form.get("person")!);
      this.advancedService.resetControl(this.form.get("docformat")!);
    }
  }

  onParse(parseResult: ParseResult) {
    this.parseResult = parseResult;
    this.searchControl.setErrors(parseResult.error? {incorrect: true} : null);
  }

  /**
   * Autocomplete icon per category
   * @param category
   */
  autocompleteIcon(category): string {
    switch(category){
      case "recent-document": return "far fa-file-alt fa-fw";
      case "recent-query": return "fas fa-history fa-fw";
      case "basket": return "fas fa-inbox fa-fw";
      case "saved-query": return "far fa-save fa-fw";
    }
    return "far fa-lightbulb fa-fw";
  }

  /**
   * Retrieve autocomplete sources, which include the standard
   */
  get autocompleteSources(): string[] {
    return this.appService.app?.data?.features as string[] || FEATURES;
  }

  /**
   * Sets the field search mode
   * event.preventDefault() to avoid the label stealing the focus
   * and closing the autocomplete...
   */
  setMode(mode: "off" | "selects" | "text", event?: Event) {
    event?.preventDefault();
    this.prefs.set('field-search-mode', mode);
  }

  /**
   * Returns the field search mode, stored in user
   * preferences
   */
  getMode(): "off" | "selects" | "text" {
    return this.prefs.get('field-search-mode') || "selects";
  }

  /**
   * Toggle the keepFilters status
   */
  toggleKeepFilters(): void {
    this.keepFilters = !this.keepFilters;
    this.keepFiltersTitle = this.keepFilters ? 'msg#searchForm.keepFilters' : 'msg#searchForm.notKeepFilters';
    /** Sets the state of keeping search's filters*/
    this.prefs.set('keep-filters-state', this.keepFilters);
  }

  /**
   * Programmatically handle opening/closing of the advanced-search form
   */
  toggleAdvancedSearch(): void {
    this.showAdvancedSearch = !this.showAdvancedSearch;
    this._instantiateAdvancedForm();
  }

  toggleVoice() {
    this.voiceService.toggleRecognition();
  }

  scrollRight() {
    this.timeout = setTimeout(() => {
      this._scrollRight()
    }, 100);
  }

  scrollLeft() {
    this.timeout = setTimeout(() => {
      this._scrollLeft();
    }, 100);
  }

  endScroll() {
    clearTimeout(this.timeout);
  }

  private _scrollRight() {
    this.searchContainer!.nativeElement.scrollLeft += 20;
    this.scrollRight();
  }

  private _scrollLeft() {
    this.searchContainer!.nativeElement.scrollLeft -= 20;
    this.scrollLeft();
  }

  /**
   * Close the advanced-search form if the search input is focused
   */
  onMouseDown(): void {
    this.showAdvancedSearch = false;
  }

  /**
   * Instantiation of the advanced search form and its dependencies/configurations
   */
  private _instantiateAdvancedForm(): void {
    if(!this.initAdvanced) {
      this.firstPageService.getFirstPage().pipe(take(1)).subscribe(
        () => {},
        () => {},
        () => {
            this.form.addControl('treepath', this.advancedService.createSelectControl('treepath'));
            this.form.addControl('authors', this.advancedService.createSelectControl('authors'));
            this.form.addControl('size', this.advancedService.createRangeControl('size',
              [ this.advancedService.validators.range('size') ]
            ));
            this.form.addControl('modified', this.advancedService.createRangeControl('modified',
              [
                this.advancedService.validators.range('modified'),
                this.advancedService.validators.date('modified')
              ]
            ));
            this.form.addControl('person', this.advancedService.createMultiInputControl('person'));
            this.form.addControl('docformat', this.advancedService.createInputControl('docformat'));

            this.initAdvanced = true;
        }
      )
    }
  }

  /**
   * Update the status of filters (other than advanced & fielded search filters) existence in this.searchService.query
   */
  private _updateFilteringStatus(): void {
    const _query =  this.searchService.query.copy();
    this.isFiltering = (_query.toStandard().select?.filter((select) => select.facet !== "search-form").length || 0) > 0;
  }
}
Example #24
Source File: validation.directive.ts    From sba-angular with MIT License 4 votes vote down vote up
/**
 * A directive to automatically add validity classes to the element to which it is attached. In addition,
 * when the associated `FormControl` is invalid a component is dynamically loaded after the element to display
 * the validation message.
 * The component to load can be specified by providing the {@link VALIDATION_MESSAGE_COMPONENT} injection token.
 * By default, the {@link ValidationMessageComponent} component is used.
 */
@Directive({
    selector: "[sqValidation]"
})
export class ValidationDirective implements OnInit, DoCheck {
    @Input("sqValidation") options: FormGroup | ValidationOptions;
    private element: HTMLElement;
    private form: FormGroup;
    private control: AbstractControl;
    private validClass?: string;
    private invalidClass?: string;
    private childSelector?: string;
    private errorMessages?: MapOf<string>;
    private validationMessage: LoadedComponent;
    private active: boolean;
    private valid: boolean;
    private dirty: boolean;
    private error?: string;
    private errorInfo?: string;

    constructor(
        @Inject(VALIDATION_MESSAGE_COMPONENT) private validationMessageComponent: Type<any>,
        private viewContainerRef: ViewContainerRef,
        private loadComponentService: LoadComponentService,
        private validationService: ValidationService) {
        this.element = viewContainerRef.element.nativeElement;
    }

    ngOnInit() {
        if (!this.options) {
            console.log("Validation.ngOnInit - no options");
            return;
        }
        let controlName;
        if (this.options instanceof FormGroup) {
            this.form = this.options;
        }
        else {
            this.form = this.options.form;
            controlName = this.options.controlName;
            this.validClass = this.options.validClass;
            this.invalidClass = this.options.invalidClass;
            this.childSelector = this.options.childSelector;
            this.errorMessages = this.options.errorMessages;
        }
        if (!this.form) {
            console.log("Validation.ngOnInit - no form model");
            return;
        }
        if (!this.form.controls) {
            console.log("Validation.ngOnInit - no form controls");
            return;
        }
        if (controlName) {
            this.control = this.form.controls[controlName];
        }
        else {
            const formControlName = this.element.getAttribute("formControlName");
            if (formControlName) {
                this.control = this.form.controls[formControlName];
            }
        }
        if (!this.control) {
            console.log("Validation.ngOnInit - no control");
            return;
        }
        if (!this.validClass) {
            this.validClass = "is-valid";
        }
        if (!this.invalidClass) {
            this.invalidClass = "is-invalid";
        }
        if (Utils.isUndefined(this.childSelector)) {
            this.childSelector = ".form-control";
        }
        this.valid = this.control.valid;
        this.dirty = this.control.dirty;
        this.active = true;
        this.error = undefined;
    }

    private getFirstError(): string | undefined {
        if (this.control.errors) {
            return Object.keys(this.control.errors)[0];
        }
        return undefined;
    }

    private getErrorText(error?: string): string {
        if (error && this.errorMessages && !!this.errorMessages[error]) {
            return this.errorMessages[error];
        }
        return this.validationService.getErrorText(error);
    }

    private getErrorInfo(error?: string): any {
        if (error && this.control.errors) {
            return this.control.errors[error];
        }
        return undefined;
    }

    private setValidityClasses() {
        const add = this.control.valid ? this.validClass : this.invalidClass;
        const remove = this.control.valid ? this.invalidClass : this.validClass;
        if (remove) {
            this.element.classList.remove(remove);
        }
        if (add) {
            this.element.classList.add(add);
        }
        if (this.childSelector) {
            const children = Array.from(this.element.querySelectorAll(this.childSelector));
            children.forEach(element => {
                if (remove) {
                    element.classList.remove(remove);
                }
                if (add) {
                    element.classList.add(add);
                }
            });
        }
    }

    private removeValidityClasses() {
        if (this.validClass) {
            this.element.classList.remove(this.validClass);
        }
        if (this.invalidClass) {
            this.element.classList.remove(this.invalidClass);
        }
        if (this.childSelector) {
            const children = Array.from(this.element.querySelectorAll(this.childSelector));
            children.forEach(element => {
                if (this.validClass) {
                    element.classList.remove(this.validClass);
                }
                if (this.invalidClass) {
                    element.classList.remove(this.invalidClass);
                }
            });
        }
    }

    /**
     * Update the validity classes on the element depending on the validity state of the
     * associated `FormControl`. If the control is invalid then the validation message component
     * is loaded to display an error message.
     */
    ngDoCheck() {
        if (!this.active) {
            return;
        }
        if (this.valid === this.control.valid && this.dirty === this.control.dirty) {
            const firstError = this.getFirstError();
            const errorInfo = this.getErrorInfo(firstError);
            if (firstError === this.error && errorInfo === this.errorInfo) {
                return;
            }
            this.error = firstError;
            this.errorInfo = errorInfo;
        }
        this.valid = this.control.valid;
        this.dirty = this.control.dirty;
        if (this.control.dirty) {
            this.setValidityClasses();
            if (this.control.valid) {
                if (this.validationMessage) {
                    this.validationMessage.componentRef.instance.text = "";
                }
            }
            else {
                if (!this.validationMessage) {
                    this.validationMessage =
                        this.loadComponentService.loadComponent({component: this.validationMessageComponent}, this.viewContainerRef);
                }
                const error = this.getFirstError();
                this.validationMessage.componentRef.instance.text = this.getErrorText(error);
                this.validationMessage.componentRef.instance.info = this.getErrorInfo(error);
            }
        }
        else {
            this.removeValidityClasses();
            if (this.validationMessage) {
                this.validationMessage.componentRef.instance.text = "";
            }
        }
    }
}
Example #25
Source File: angular-context.spec.ts    From s-libs with MIT License 4 votes vote down vote up
describe('AngularContext', () => {
  class SnackBarContext extends AngularContext {
    constructor() {
      super({ imports: [MatSnackBarModule, NoopAnimationsModule] });
    }

    protected override cleanUp(): void {
      this.inject(OverlayContainer).ngOnDestroy();
      flush();
      super.cleanUp();
    }
  }

  describe('.getCurrent()', () => {
    it('returns the currently running angular context', () => {
      expect(AngularContext.getCurrent()).toBeUndefined();

      const ctx = new AngularContext();
      ctx.run(() => {
        expect(AngularContext.getCurrent()).toBe(ctx);
      });

      expect(AngularContext.getCurrent()).toBeUndefined();
    });
  });

  describe('.startTime', () => {
    it('controls the time at which the test starts', () => {
      const ctx = new AngularContext();
      ctx.startTime = new Date('2012-07-14T21:42:17.523Z');
      ctx.run(() => {
        expect(new Date()).toEqual(new Date('2012-07-14T21:42:17.523Z'));
      });
    });

    it('defaults to the current time', () => {
      const ctx = new AngularContext();
      const now = Date.now();
      ctx.run(() => {
        expect(Date.now()).toBeCloseTo(now, -1);
      });
    });
  });

  describe('constructor', () => {
    it('accepts module metadata to be bootstrapped', () => {
      const value = Symbol();
      const token = new InjectionToken<symbol>('tok');
      const ctx = new AngularContext({
        providers: [{ provide: token, useValue: value }],
      });
      ctx.run(() => {
        expect(ctx.inject(token)).toBe(value);
      });
    });

    it('sets up HttpClientTestingModule', () => {
      const ctx = new AngularContext();
      ctx.run(() => {
        expect(ctx.inject(HttpTestingController)).toBeDefined();
      });
    });

    it('sets up MockErrorHandler', () => {
      const ctx = new AngularContext();
      ctx.run(() => {
        expect(ctx.inject(ErrorHandler)).toEqual(jasmine.any(MockErrorHandler));
      });
    });

    it('gives a nice error message if trying to use 2 at the same time', () => {
      new AngularContext().run(async () => {
        expect(() => {
          // eslint-disable-next-line no-new -- nothing more is needed for this test
          new AngularContext();
        }).toThrowError(
          'There is already another AngularContext in use (or it was not cleaned up)',
        );
      });
    });
  });

  describe('.run()', () => {
    it('uses the fakeAsync zone', () => {
      const ctx = new AngularContext();
      ctx.run(() => {
        expect(tick).not.toThrow();
      });
    });

    it('can handle async tests that call tick', () => {
      let completed = false;
      const ctx = new AngularContext();
      ctx.run(async () => {
        await sleep(0);
        setTimeout(() => {
          completed = true;
        }, 500);
        ctx.tick(500);
      });
      expect(completed).toBeTrue();
    });

    it('does not swallow errors (production bug)', () => {
      expect(() => {
        new AngularContext().run(() => {
          throw new Error();
        });
      }).toThrowError();
    });
  });

  describe('.inject()', () => {
    it('fetches from the root injector', () => {
      const ctx = new AngularContext();
      ctx.run(() => {
        expect(ctx.inject(Injector)).toBe(TestBed.inject(Injector));
      });
    });
  });

  describe('.hasHarness()', () => {
    it('returns whether a match for the harness exists', () => {
      const ctx = new SnackBarContext();
      ctx.run(async () => {
        expect(await ctx.hasHarness(MatSnackBarHarness)).toBe(false);

        ctx.inject(MatSnackBar).open('hi');
        expect(await ctx.hasHarness(MatSnackBarHarness)).toBe(true);
      });
    });
  });

  describe('.getHarness()', () => {
    it('returns a harness', () => {
      const ctx = new SnackBarContext();
      ctx.run(async () => {
        ctx.inject(MatSnackBar).open('hi');
        const bar = await ctx.getHarness(MatSnackBarHarness);
        expect(await bar.getMessage()).toBe('hi');
      });
    });
  });

  describe('.getAllHarnesses()', () => {
    it('gets an array of harnesses', () => {
      const ctx = new SnackBarContext();
      ctx.run(async () => {
        let bars = await ctx.getAllHarnesses(MatSnackBarHarness);
        expect(bars.length).toBe(0);
        ctx.inject(MatSnackBar).open('hi');
        bars = await ctx.getAllHarnesses(MatSnackBarHarness);
        expect(bars.length).toBe(1);
        expect(await bars[0].getMessage()).toBe('hi');
      });
    });
  });

  describe('.tick()', () => {
    it('defaults to not advance time', () => {
      const ctx = new AngularContext();
      const start = ctx.startTime.getTime();
      ctx.run(() => {
        ctx.tick();
        expect(Date.now()).toBe(start);
      });
    });

    it('defaults to advancing in milliseconds', () => {
      const ctx = new AngularContext();
      const start = ctx.startTime.getTime();
      ctx.run(() => {
        ctx.tick(10);
        expect(Date.now()).toBe(start + 10);
      });
    });

    it('allows specifying the units to advance', () => {
      const ctx = new AngularContext();
      const start = ctx.startTime.getTime();
      ctx.run(() => {
        ctx.tick(10, 'sec');
        expect(Date.now()).toBe(start + 10000);
      });
    });

    it('runs change detection even if no tasks are queued', () => {
      let ranChangeDetection = false;

      @Component({ template: '' })
      class LocalComponent implements DoCheck {
        ngDoCheck(): void {
          ranChangeDetection = true;
        }
      }
      TestBed.overrideComponent(LocalComponent, {});

      const ctx = new AngularContext();
      ctx.run(() => {
        const resolver = ctx.inject(ComponentFactoryResolver);
        const factory = resolver.resolveComponentFactory(LocalComponent);
        const componentRef = factory.create(ctx.inject(Injector));
        ctx.inject(ApplicationRef).attachView(componentRef.hostView);

        expect(ranChangeDetection).toBe(false);
        ctx.tick();
        expect(ranChangeDetection).toBe(true);
      });
    });

    it('flushes micro tasks before running change detection', () => {
      let ranChangeDetection = false;
      let flushedMicroTasksBeforeChangeDetection = false;

      @Component({ template: '' })
      class LocalComponent implements DoCheck {
        ngDoCheck(): void {
          ranChangeDetection = true;
        }
      }
      TestBed.overrideComponent(LocalComponent, {});

      const ctx = new AngularContext();
      ctx.run(() => {
        const resolver = ctx.inject(ComponentFactoryResolver);
        const factory = resolver.resolveComponentFactory(LocalComponent);
        const componentRef = factory.create(ctx.inject(Injector));
        ctx.inject(ApplicationRef).attachView(componentRef.hostView);

        Promise.resolve().then(() => {
          flushedMicroTasksBeforeChangeDetection = !ranChangeDetection;
        });
        ctx.tick();
        expect(flushedMicroTasksBeforeChangeDetection).toBe(true);
      });
    });

    it('runs change detection after timeouts', () => {
      let ranTimeout = false;
      let ranChangeDetectionAfterTimeout = false;

      @Component({ template: '' })
      class LocalComponent implements DoCheck {
        ngDoCheck(): void {
          ranChangeDetectionAfterTimeout = ranTimeout;
        }
      }
      TestBed.overrideComponent(LocalComponent, {});

      const ctx = new AngularContext();
      ctx.run(() => {
        const resolver = ctx.inject(ComponentFactoryResolver);
        const factory = resolver.resolveComponentFactory(LocalComponent);
        const componentRef = factory.create(ctx.inject(Injector));
        ctx.inject(ApplicationRef).attachView(componentRef.hostView);

        setTimeout(() => {
          ranTimeout = true;
        });
        ctx.tick();
        expect(ranChangeDetectionAfterTimeout).toBe(true);
      });
    });

    it('advances `performance.now()`', () => {
      const ctx = new AngularContext();
      ctx.run(() => {
        const start = performance.now();
        ctx.tick(10);
        expect(performance.now()).toBe(start + 10);
      });
    });

    it('gives a nice error message when you try to use it outside `run()`', () => {
      const ctx = new AngularContext();
      expect(() => {
        ctx.tick();
      }).toThrowError(
        '.tick() only works inside the .run() callback (because it needs to be in a fakeAsync zone)',
      );
      ctx.run(noop);
    });
  });

  describe('.verifyPostTestConditions()', () => {
    it('errs if there are unexpected http requests', () => {
      const ctx = new AngularContext();
      expect(() => {
        ctx.run(() => {
          ctx.inject(HttpClient).get('an unexpected URL').subscribe();
        });
      }).toThrowError(
        'Expected no open requests, found 1: GET an unexpected URL',
      );
    });

    it('errs if there are unexpected errors', () => {
      @Component({ template: '<button (click)="throwError()"></button>' })
      class ThrowingComponent {
        throwError(): never {
          throw new Error();
        }
      }

      const ctx = new ComponentContext(ThrowingComponent);
      expect(() => {
        ctx.run(async () => {
          // TODO: make something like `ctx.getTestElement()`?
          const loader = FakeAsyncHarnessEnvironment.documentRootLoader(ctx);
          const button = await loader.locatorFor('button')();
          await button.click();
        });
      }).toThrowError('Expected no error(s), found 1');
    });
  });

  describe('.cleanUp()', () => {
    it('discards periodic tasks', () => {
      const ctx = new AngularContext();
      expect(() => {
        ctx.run(() => {
          setInterval(noop, 10);
        });
      })
        // No error: "1 periodic timer(s) still in the queue."
        .not.toThrowError();
    });

    it('flushes pending timeouts', () => {
      const ctx = new AngularContext();
      expect(() => {
        ctx.run(() => {
          setTimeout(noop, 1);
        });
      })
        // No error: "1 timer(s) still in the queue."
        .not.toThrowError();
    });
  });
});
Example #26
Source File: echarts.component.ts    From youpez-admin with MIT License 4 votes vote down vote up
@Component({
  selector: 'youpez-echarts',
  templateUrl: './echarts.component.html',
  styleUrls: ['./echarts.component.scss']
})
export class EchartsComponent implements OnInit, OnDestroy, OnChanges, DoCheck, AfterViewInit {

  @Input() options: EChartOption
  @Input() theme: string
  @Input() loading: boolean
  @Input() initOpts: {
    devicePixelRatio?: number
    renderer?: string
    width?: number | string
    height?: number | string
  }
  @Input() merge: EChartOption
  @Input() autoResize = true
  @Input() forceResize = false
  @Input() loadingType = 'default'
  @Input() loadingOpts: object
  @Input() detectEventChanges = true // deprecated, left for compatibility reasons to avoid triggering major version

  // ngx-echarts events
  @Output() chartInit = new EventEmitter<ECharts>()

  // echarts mouse events
  @Output() chartClick = this.createLazyEvent('click')
  @Output() chartDblClick = this.createLazyEvent('dblclick')
  @Output() chartMouseDown = this.createLazyEvent('mousedown')
  @Output() chartMouseMove = this.createLazyEvent('mousemove')
  @Output() chartMouseUp = this.createLazyEvent('mouseup')
  @Output() chartMouseOver = this.createLazyEvent('mouseover')
  @Output() chartMouseOut = this.createLazyEvent('mouseout')
  @Output() chartGlobalOut = this.createLazyEvent('globalout')
  @Output() chartContextMenu = this.createLazyEvent('contextmenu')

  // echarts mouse events
  @Output() chartLegendSelectChanged = this.createLazyEvent('legendselectchanged')
  @Output() chartLegendSelected = this.createLazyEvent('legendselected')
  @Output() chartLegendUnselected = this.createLazyEvent('legendunselected')
  @Output() chartLegendScroll = this.createLazyEvent('legendscroll')
  @Output() chartDataZoom = this.createLazyEvent('datazoom')
  @Output() chartDataRangeSelected = this.createLazyEvent('datarangeselected')
  @Output() chartTimelineChanged = this.createLazyEvent('timelinechanged')
  @Output() chartTimelinePlayChanged = this.createLazyEvent('timelineplaychanged')
  @Output() chartRestore = this.createLazyEvent('restore')
  @Output() chartDataViewChanged = this.createLazyEvent('dataviewchanged')
  @Output() chartMagicTypeChanged = this.createLazyEvent('magictypechanged')
  @Output() chartPieSelectChanged = this.createLazyEvent('pieselectchanged')
  @Output() chartPieSelected = this.createLazyEvent('pieselected')
  @Output() chartPieUnselected = this.createLazyEvent('pieunselected')
  @Output() chartMapSelectChanged = this.createLazyEvent('mapselectchanged')
  @Output() chartMapSelected = this.createLazyEvent('mapselected')
  @Output() chartMapUnselected = this.createLazyEvent('mapunselected')
  @Output() chartAxisAreaSelected = this.createLazyEvent('axisareaselected')
  @Output() chartFocusNodeAdjacency = this.createLazyEvent('focusnodeadjacency')
  @Output() chartUnfocusNodeAdjacency = this.createLazyEvent('unfocusnodeadjacency')
  @Output() chartBrush = this.createLazyEvent('brush')
  @Output() chartBrushSelected = this.createLazyEvent('brushselected')
  @Output() chartRendered = this.createLazyEvent('rendered')
  @Output() chartFinished = this.createLazyEvent('finished')

  private chart: ECharts
  private currentOffsetWidth = 0
  private currentOffsetHeight = 0
  private currentWindowWidth: number
  private resizeSub: Subscription

  constructor(private el: ElementRef, private ngZone: NgZone) {

  }

  ngOnChanges(changes: SimpleChanges) {
    const {options, merge, loading, theme} = changes

    if (isChanged(options)) {
      this.onOptionsChange(options)
    }
    if (isChanged(merge)) {
      this.setOption(merge)
    }
    if (isChanged(loading)) {
      this.toggleLoading(!!loading)
    }
    if (isChanged(theme)) {
      this.refreshChart()
    }
  }

  ngOnInit() {
    this.resizeSub = fromEvent(window, 'resize').pipe(debounceTime(50)).subscribe(() => {
      if ((this.autoResize && window.innerWidth !== this.currentWindowWidth) || this.forceResize) {
        this.currentWindowWidth = window.innerWidth
        this.currentOffsetWidth = this.el.nativeElement.offsetWidth
        this.currentOffsetHeight = this.el.nativeElement.offsetHeight
        this.resize()
      }
    })
  }

  ngOnDestroy() {
    if (this.resizeSub) {
      this.resizeSub.unsubscribe()
    }
    this.dispose()
  }

  ngDoCheck() {
    // No heavy work in DoCheck!
    if (this.chart && this.autoResize) {
      const offsetWidth = this.el.nativeElement.offsetWidth
      const offsetHeight = this.el.nativeElement.offsetHeight

      if (this.currentOffsetWidth !== offsetWidth || this.currentOffsetHeight !== offsetHeight) {
        this.currentOffsetWidth = offsetWidth
        this.currentOffsetHeight = offsetHeight
        this.resize()
      }
    }
  }

  ngAfterViewInit() {
    setTimeout(() => this.initChart())
  }

  preventContextMenu($event) {
    $event.preventDefault()
  }

  dispatchAction(action) {
    this.chart.dispatchAction(action)
  }

  private dispose() {
    if (this.chart) {
      this.chart.off('contextmenu')
      this.chart.dispose()
      this.chart = null
    }
  }

  private resize() {
    if (this.chart) {
      this.chart.resize()
    }
  }

  private toggleLoading(loading: boolean) {
    if (this.chart) {
      loading ? this.chart.showLoading(this.loadingType, this.loadingOpts) : this.chart.hideLoading()
    }
  }

  private setOption(option: any, opts?: any) {
    if (this.chart) {
      this.chart.setOption(option, opts)
    }
  }

  public nextOptions(nextOptions) {
    if (this.chart) {
      this.chart.setOption(nextOptions)
    }
  }

  public refreshChart() {
    this.dispose()
    this.initChart()
  }

  private createChart() {
    this.currentWindowWidth = window.innerWidth
    this.currentOffsetWidth = this.el.nativeElement.offsetWidth
    this.currentOffsetHeight = this.el.nativeElement.offsetHeight
    const dom = this.el.nativeElement

    if (window && window.getComputedStyle) {
      const prop = window.getComputedStyle(dom, null).getPropertyValue('height')
      if ((!prop || prop === '0px') &&
        (!dom.style.height || dom.style.height === '0px')) {
        dom.style.height = '400px'
      }
    }

    return this.ngZone.runOutsideAngular(() => init(dom, this.theme ? this.theme : 'default', this.initOpts))
  }

  private initChart() {
    this.onOptionsChange(this.options)

    if (this.merge && this.chart) {
      this.setOption(this.merge)
    }
  }

  private onOptionsChange(opt: EChartOption) {
    if (opt) {
      if (!this.chart) {
        this.chart = this.createChart()
        this.chart.on('contextmenu', function (params) {
          params.event.event.preventDefault()
        })
        this.chartInit.emit(this.chart)
      }

      this.chart.setOption(this.options, true)
    }
  }

  // allows to lazily bind to only those events that are requested through the `@Output` by parent components
  // see https://stackoverflow.com/questions/51787972/optimal-reentering-the-ngzone-from-eventemitter-event for more info
  private createLazyEvent<T>(eventName: string): EventEmitter<T> {
    return this.chartInit.pipe(
      switchMap((chart: ECharts) => new Observable(observer => {
        chart.on(eventName, (data: T) => this.ngZone.run(() => observer.next(data)))
        return () => chart.off(eventName)
      }))
    ) as EventEmitter<T>
  }

}
Example #27
Source File: pager-items-comp.ts    From ui-pager with Apache License 2.0 4 votes vote down vote up
@Component({
    template: ''
})
export abstract class TemplatedItemsComponent implements DoCheck, OnDestroy, AfterContentInit {
    public abstract get nativeElement(): Pager;

    protected templatedItemsView: Pager;
    protected _items: any;
    protected _differ: IterableDiffer<KeyedTemplate>;
    protected _templateMap: Map<string, KeyedTemplate>;
    private _selectedIndex: number;
    @ViewChild('loader', { read: ViewContainerRef, static: false }) loader: ViewContainerRef;

    @Output()
    public setupItemView = new EventEmitter<SetupItemViewArgs>();

    @ContentChild(TemplateRef, { static: false }) itemTemplateQuery: TemplateRef<ItemContext>;

    itemTemplate: TemplateRef<ItemContext>;

    @Input()
    get items() {
        return this._items;
    }

    set items(value: any) {
        this._items = value;
        let needDiffer = true;
        if (value instanceof ObservableArray) {
            needDiffer = false;
        }
        if (needDiffer && !this._differ && isListLikeIterable(value)) {
            this._differ = this._iterableDiffers.find(this._items).create((_index, item) => item);
        }

        this.templatedItemsView.items = this._items;
    }

    @Input()
    get selectedIndex(): number {
        return this._selectedIndex;
    }

    set selectedIndex(value) {
        this._selectedIndex = value;
        this.templatedItemsView.selectedIndex = this._selectedIndex;
    }

    ngAfterViewInit() {
        if (!!this._selectedIndex) {
            setTimeout(() => {
                if (isIOS) {
                    this.templatedItemsView.scrollToIndexAnimated(this._selectedIndex, false);
                }
                this.templatedItemsView.selectedIndex = this._selectedIndex;
            });
        }
    }

    constructor(_elementRef: ElementRef, private _iterableDiffers: IterableDiffers) {
        this.templatedItemsView = _elementRef.nativeElement;

        this.templatedItemsView.on('itemLoading', this.onItemLoading, this);
        this.templatedItemsView.on('itemDisposing', this.onItemDisposing, this);
    }

    ngAfterContentInit() {
        if (Trace.isEnabled()) {
            PagerLog('TemplatedItemsView.ngAfterContentInit()');
        }
        this.setItemTemplates();
    }

    ngOnDestroy() {
        this.templatedItemsView.off('itemLoading', this.onItemLoading, this);
        this.templatedItemsView.off('itemDisposing', this.onItemDisposing, this);
    }

    private setItemTemplates() {
        if (!this.items) return;
        // The itemTemplateQuery may be changed after list items are added that contain <template> inside,
        // so cache and use only the original template to avoid errors.
        this.itemTemplate = this.itemTemplateQuery;

        if (this._templateMap) {
            if (Trace.isEnabled()) {
                PagerLog('Setting templates');
            }

            const templates: KeyedTemplate[] = [];
            this._templateMap.forEach((value) => {
                templates.push(value);
            });
            this.templatedItemsView.itemTemplates = templates;
        }
    }

    public registerTemplate(key: string, template: TemplateRef<ItemContext>) {
        if (Trace.isEnabled()) {
            PagerLog(`registerTemplate for key: ${key}`);
        }

        if (!this._templateMap) {
            this._templateMap = new Map<string, KeyedTemplate>();
        }

        const keyedTemplate = {
            key,
            createView: this.getItemTemplateViewFactory(template)
        };

        this._templateMap.set(key, keyedTemplate);
    }

    @profile
    public onItemLoading(args: ItemEventData) {
        if (!args.view && !this.itemTemplate) {
            return;
        }

        if (!this.items) return;

        const index = args.index;
        const items = (args.object as any).items;
        const currentItem = typeof items.getItem === 'function' ? items.getItem(index) : items[index];
        let viewRef: EmbeddedViewRef<ItemContext>;

        if (args.view) {
            if (Trace.isEnabled()) {
                PagerLog(`onItemLoading: ${index} - Reusing existing view`);
            }

            viewRef = args.view[NG_VIEW];
            // Getting angular view from original element (in cases when ProxyViewContainer
            // is used NativeScript internally wraps it in a StackLayout)
            if (!viewRef && args.view instanceof LayoutBase && args.view.getChildrenCount() > 0) {
                viewRef = args.view.getChildAt(0)[NG_VIEW];
            }

            if (!viewRef && Trace.isEnabled()) {
                PagerError(`ViewReference not found for item ${index}. View recycling is not working`);
            }
        }

        if (!viewRef) {
            if (Trace.isEnabled()) {
                PagerLog(`onItemLoading: ${index} - Creating view from template`);
            }

            viewRef = this.loader.createEmbeddedView(this.itemTemplate, new ItemContext(), 0);
            args.view = getItemViewRoot(viewRef);
            args.view[NG_VIEW] = viewRef;
        }

        this.setupViewRef(viewRef, currentItem, index);

        this.detectChangesOnChild(viewRef, index);
    }

    @profile
    public onItemDisposing(args: ItemEventData) {
        if (!args.view) {
            return;
        }
        let viewRef: EmbeddedViewRef<ItemContext>;

        if (args.view) {
            if (Trace.isEnabled()) {
                PagerLog(`onItemDisposing: ${args.index} - Removing angular view`);
            }

            viewRef = args.view[NG_VIEW];
            // Getting angular view from original element (in cases when ProxyViewContainer
            // is used NativeScript internally wraps it in a StackLayout)
            if (!viewRef && args.view instanceof LayoutBase && args.view.getChildrenCount() > 0) {
                viewRef = args.view.getChildAt(0)[NG_VIEW];
            }

            if (!viewRef && Trace.isEnabled()) {
                PagerError(`ViewReference not found for item ${args.index}. View disposing is not working`);
            }
        }

        if (viewRef) {
            if (Trace.isEnabled()) {
                PagerLog(`onItemDisposing: ${args.index} - Disposing view reference`);
            }

            viewRef.destroy();
        }
    }

    public setupViewRef(viewRef: EmbeddedViewRef<ItemContext>, data: any, index: number): void {
        const context = viewRef.context;
        context.$implicit = data;
        context.item = data;
        context.index = index;
        context.even = index % 2 === 0;
        context.odd = !context.even;

        this.setupItemView.next({
            view: viewRef,
            data,
            index,
            context
        });
    }

    protected getItemTemplateViewFactory(template: TemplateRef<ItemContext>): () => View {
        return () => {
            const viewRef = this.loader.createEmbeddedView(template, new ItemContext(), 0);
            const resultView = getItemViewRoot(viewRef);
            resultView[NG_VIEW] = viewRef;

            return resultView;
        };
    }

    @profile
    private detectChangesOnChild(viewRef: EmbeddedViewRef<ItemContext>, index: number) {
        if (Trace.isEnabled()) {
            PagerLog(`Manually detect changes in child: ${index}`);
        }

        viewRef.markForCheck();
        viewRef.detectChanges();
    }

    ngDoCheck() {
        if (this._differ) {
            if (Trace.isEnabled()) {
                PagerLog('ngDoCheck() - execute differ');
            }

            const changes = this._differ.diff(this._items);
            if (changes) {
                if (Trace.isEnabled()) {
                    PagerLog('ngDoCheck() - refresh');
                }

                this.templatedItemsView.refresh();
            }
        }
    }
}
Example #28
Source File: route-selector.component.ts    From fyle-mobile-app with MIT License 4 votes vote down vote up
@Component({
  selector: 'app-route-selector',
  templateUrl: './route-selector.component.html',
  styleUrls: ['./route-selector.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: RouteSelectorComponent,
    },
    {
      provide: NG_VALIDATORS,
      useExisting: RouteSelectorComponent,
      multi: true,
    },
  ],
})
export class RouteSelectorComponent implements OnInit, ControlValueAccessor, OnDestroy, OnChanges, DoCheck {
  @Input() unit: 'KM' | 'MILES';

  @Input() mileageConfig;

  @Input() isDistanceMandatory;

  @Input() isAmountDisabled;

  @Input() txnFields;

  @Input() formInitialized;

  @Input() isConnected;

  @Input() recentlyUsedMileageLocations: {
    recent_start_locations?: string[];
    recent_end_locations?: string[];
    recent_locations?: string[];
  };

  skipRoundTripUpdate = false;

  onChangeSub: Subscription;

  form: FormGroup = this.fb.group({
    mileageLocations: new FormArray([]),
    distance: [, Validators.required],
    roundTrip: [],
  });

  private ngControl: NgControl;

  constructor(private fb: FormBuilder, private modalController: ModalController, private injector: Injector) {}

  get mileageLocations() {
    return this.form.controls.mileageLocations as FormArray;
  }

  onTouched = () => {};

  ngDoCheck() {
    if (this.ngControl.touched) {
      this.form.markAllAsTouched();
    }
  }

  ngOnDestroy(): void {
    this.onChangeSub.unsubscribe();
  }

  customDistanceValidator(control: AbstractControl) {
    const passedInDistance = control.value && +control.value;
    if (passedInDistance !== null) {
      return passedInDistance > 0
        ? null
        : {
            invalidDistance: true,
          };
    }
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.mileageConfig && !isEqual(changes.mileageConfig.previousValue, changes.mileageConfig.currentValue)) {
      this.onMileageConfigChange();
    }

    if (changes.txnFields && !isEqual(changes.txnFields.previousValue, changes.txnFields.currentValue)) {
      this.onTxnFieldsChange();
    }
  }

  onTxnFieldsChange() {
    const keyToControlMap: { [id: string]: AbstractControl } = {
      distance: this.form.controls.distance,
    };

    for (const control of Object.values(keyToControlMap)) {
      control.clearValidators();
      control.updateValueAndValidity();
    }

    for (const txnFieldKey of intersection(['distance'], Object.keys(this.txnFields))) {
      const control = keyToControlMap[txnFieldKey];

      if (this.txnFields[txnFieldKey].is_mandatory) {
        if (txnFieldKey === 'distance') {
          control.setValidators(
            this.isConnected ? Validators.compose([Validators.required, this.customDistanceValidator]) : null
          );
        }
      }
      control.updateValueAndValidity();
    }

    this.form.updateValueAndValidity();
  }

  onMileageConfigChange() {
    this.form.controls.mileageLocations.clearValidators();
    this.form.controls.mileageLocations.updateValueAndValidity();
    if (this.mileageConfig.location_mandatory) {
      this.form.controls.mileageLocations.setValidators(Validators.required);
    }
    this.form.controls.mileageLocations.updateValueAndValidity();
    this.form.updateValueAndValidity();
  }

  writeValue(value): void {
    if (value) {
      if (value.mileageLocations) {
        value.mileageLocations.forEach((location) => {
          this.mileageLocations.push(
            new FormControl(location, this.mileageConfig.location_mandatory && Validators.required)
          );
        });
        if (value.mileageLocations.length === 1) {
          this.mileageLocations.push(
            new FormControl(null, this.mileageConfig.location_mandatory && Validators.required)
          );
        }
      }

      this.form.patchValue({
        distance: value.distance,
        roundTrip: value.roundTrip,
      });
    }
  }

  registerOnChange(onChange): void {
    this.onChangeSub = this.form.valueChanges.subscribe(onChange);
  }

  registerOnTouched(onTouched): void {
    this.onTouched = onTouched;
  }

  setDisabledState?(disabled: boolean): void {
    if (disabled) {
      this.form.disable();
    } else {
      this.form.enable();
    }
  }

  ngOnInit() {
    this.ngControl = this.injector.get(NgControl);

    this.form.controls.roundTrip.valueChanges.subscribe((roundTrip) => {
      if (!this.skipRoundTripUpdate) {
        if (this.formInitialized) {
          if (this.form.value.distance) {
            if (roundTrip) {
              this.form.controls.distance.setValue((+this.form.value.distance * 2).toFixed(2));
            } else {
              this.form.controls.distance.setValue((+this.form.value.distance / 2).toFixed(2));
            }
          }
        }
      } else {
        this.skipRoundTripUpdate = false;
      }
    });
  }

  async openModal() {
    const selectionModal = await this.modalController.create({
      component: RouteSelectorModalComponent,
      componentProps: {
        unit: this.unit,
        mileageConfig: this.mileageConfig,
        isDistanceMandatory: this.isDistanceMandatory,
        isAmountDisabled: this.isAmountDisabled,
        txnFields: this.txnFields,
        value: this.form.value,
        recentlyUsedMileageLocations: this.recentlyUsedMileageLocations,
      },
    });

    await selectionModal.present();

    const { data } = await selectionModal.onWillDismiss();

    if (data) {
      this.skipRoundTripUpdate = true;
      this.mileageLocations.clear({
        emitEvent: false,
      });

      data.mileageLocations.forEach((mileageLocation) => {
        this.mileageLocations.push(
          new FormControl(mileageLocation, this.mileageConfig.location_mandatory && Validators.required)
        );
      });

      this.form.patchValue({
        distance: parseFloat(data.distance),
        roundTrip: data.roundTrip,
      });
    }
  }

  validate(fc: FormControl) {
    if (!this.form.valid) {
      return {
        ...this.form.controls.mileageLocations.errors,
        ...this.form.controls.distance.errors,
      };
    }
    return null;
  }
}
Example #29
Source File: ngx-gallery.component.ts    From ngx-gallery-9 with MIT License 4 votes vote down vote up
@Component({
  selector: 'ngx-gallery',
  template: `
    <div class="ngx-gallery-layout {{currentOptions?.layout}}">
      <ngx-gallery-image *ngIf="currentOptions?.image" [style.height]="getImageHeight()" [images]="mediumImages" [clickable]="currentOptions?.preview" [selectedIndex]="selectedIndex" [arrows]="currentOptions?.imageArrows" [arrowsAutoHide]="currentOptions?.imageArrowsAutoHide" [arrowPrevIcon]="currentOptions?.arrowPrevIcon" [arrowNextIcon]="currentOptions?.arrowNextIcon" [swipe]="currentOptions?.imageSwipe" [animation]="currentOptions?.imageAnimation" [size]="currentOptions?.imageSize" [autoPlay]="currentOptions?.imageAutoPlay" [autoPlayInterval]="currentOptions?.imageAutoPlayInterval" [autoPlayPauseOnHover]="currentOptions?.imageAutoPlayPauseOnHover" [infinityMove]="currentOptions?.imageInfinityMove"  [lazyLoading]="currentOptions?.lazyLoading" [actions]="currentOptions?.imageActions" [descriptions]="descriptions" [showDescription]="currentOptions?.imageDescription" [bullets]="currentOptions?.imageBullets" (onClick)="openPreview($event)" (onActiveChange)="selectFromImage($event)"></ngx-gallery-image>

      <ngx-gallery-thumbnails *ngIf="currentOptions?.thumbnails" [style.marginTop]="getThumbnailsMarginTop()" [style.marginBottom]="getThumbnailsMarginBottom()" [style.height]="getThumbnailsHeight()" [images]="smallImages" [links]="currentOptions?.thumbnailsAsLinks ? links : []" [labels]="labels" [linkTarget]="currentOptions?.linkTarget" [selectedIndex]="selectedIndex" [columns]="currentOptions?.thumbnailsColumns" [rows]="currentOptions?.thumbnailsRows" [margin]="currentOptions?.thumbnailMargin" [arrows]="currentOptions?.thumbnailsArrows" [arrowsAutoHide]="currentOptions?.thumbnailsArrowsAutoHide" [arrowPrevIcon]="currentOptions?.arrowPrevIcon" [arrowNextIcon]="currentOptions?.arrowNextIcon" [clickable]="currentOptions?.image || currentOptions?.preview" [swipe]="currentOptions?.thumbnailsSwipe" [size]="currentOptions?.thumbnailSize" [moveSize]="currentOptions?.thumbnailsMoveSize" [order]="currentOptions?.thumbnailsOrder" [remainingCount]="currentOptions?.thumbnailsRemainingCount" [lazyLoading]="currentOptions?.lazyLoading" [actions]="currentOptions?.thumbnailActions"  (onActiveChange)="selectFromThumbnails($event)"></ngx-gallery-thumbnails>

      <ngx-gallery-preview [images]="bigImages" [descriptions]="descriptions" [showDescription]="currentOptions?.previewDescription" [arrowPrevIcon]="currentOptions?.arrowPrevIcon" [arrowNextIcon]="currentOptions?.arrowNextIcon" [closeIcon]="currentOptions?.closeIcon" [fullscreenIcon]="currentOptions?.fullscreenIcon" [spinnerIcon]="currentOptions?.spinnerIcon" [arrows]="currentOptions?.previewArrows" [arrowsAutoHide]="currentOptions?.previewArrowsAutoHide" [swipe]="currentOptions?.previewSwipe" [fullscreen]="currentOptions?.previewFullscreen" [forceFullscreen]="currentOptions?.previewForceFullscreen" [closeOnClick]="currentOptions?.previewCloseOnClick" [closeOnEsc]="currentOptions?.previewCloseOnEsc" [keyboardNavigation]="currentOptions?.previewKeyboardNavigation" [animation]="currentOptions?.previewAnimation" [autoPlay]="currentOptions?.previewAutoPlay" [autoPlayInterval]="currentOptions?.previewAutoPlayInterval" [autoPlayPauseOnHover]="currentOptions?.previewAutoPlayPauseOnHover" [infinityMove]="currentOptions?.previewInfinityMove" [zoom]="currentOptions?.previewZoom" [zoomStep]="currentOptions?.previewZoomStep" [zoomMax]="currentOptions?.previewZoomMax" [zoomMin]="currentOptions?.previewZoomMin" [zoomInIcon]="currentOptions?.zoomInIcon" [zoomOutIcon]="currentOptions?.zoomOutIcon" [actions]="currentOptions?.actions" [rotate]="currentOptions?.previewRotate" [rotateLeftIcon]="currentOptions?.rotateLeftIcon" [rotateRightIcon]="currentOptions?.rotateRightIcon" [download]="currentOptions?.previewDownload" [downloadIcon]="currentOptions?.downloadIcon" [bullets]="currentOptions?.previewBullets" (onClose)="onPreviewClose()" (onOpen)="onPreviewOpen()" (onActiveChange)="previewSelect($event)" [class.ngx-gallery-active]="previewEnabled"></ngx-gallery-preview>
    </div>
  `,
  styleUrls: ['./ngx-gallery.component.scss'],
  providers: [NgxGalleryHelperService]
})
export class NgxGalleryComponent implements OnInit, DoCheck, AfterViewInit {
  @Input() options: NgxGalleryOptions[];
  @Input() images: NgxGalleryImage[];

  @Output() imagesReady = new EventEmitter();
  @Output() change = new EventEmitter<{ index: number; image: NgxGalleryImage; }>();
  @Output() previewOpen = new EventEmitter();
  @Output() previewClose = new EventEmitter();
  @Output() previewChange = new EventEmitter<{ index: number; image: NgxGalleryImage; }>();

  smallImages: string[] | SafeResourceUrl[];
  mediumImages: NgxGalleryOrderedImage[];
  bigImages: string[] | SafeResourceUrl[];
  descriptions: string[];
  links: string[];
  labels: string[];

  oldImages: NgxGalleryImage[];
  oldImagesLength = 0;

  selectedIndex = 0;
  previewEnabled: boolean;

  currentOptions: NgxGalleryOptions;

  private breakpoint: number | undefined = undefined;
  private prevBreakpoint: number | undefined = undefined;
  private fullWidthTimeout: any;

  @ViewChild(NgxGalleryPreviewComponent) preview: NgxGalleryPreviewComponent;
  @ViewChild(NgxGalleryImageComponent) image: NgxGalleryImageComponent;
  @ViewChild(NgxGalleryThumbnailsComponent) thubmnails: NgxGalleryThumbnailsComponent;

  @HostBinding('style.width') width: string;
  @HostBinding('style.height') height: string;
  @HostBinding('style.left') left: string;

  constructor(private myElement: ElementRef) {}

  ngOnInit() {
      this.options = this.options.map((opt) => new NgxGalleryOptions(opt));
      this.sortOptions();
      this.setBreakpoint();
      this.setOptions();
      this.checkFullWidth();
      if (this.currentOptions) {
          this.selectedIndex = <number>this.currentOptions.startIndex;
      }
  }

  ngDoCheck(): void {
      if (this.images !== undefined && (this.images.length !== this.oldImagesLength)
          || (this.images !== this.oldImages)) {
          this.oldImagesLength = this.images.length;
          this.oldImages = this.images;
          this.setOptions();
          this.setImages();

          if (this.images && this.images.length) {
              this.imagesReady.emit();
          }

          if (this.image) {
              this.image.reset(<number>this.currentOptions.startIndex);
          }

          if (this.currentOptions.thumbnailsAutoHide && this.currentOptions.thumbnails
              && this.images.length <= 1) {
              this.currentOptions.thumbnails = false;
              this.currentOptions.imageArrows = false;
          }

          this.resetThumbnails();
      }
  }

  ngAfterViewInit(): void {
      this.checkFullWidth();
  }

  @HostListener('window:resize') onResize() {
      this.setBreakpoint();

      if (this.prevBreakpoint !== this.breakpoint) {
          this.setOptions();
          this.resetThumbnails();
      }

      if (this.currentOptions && this.currentOptions.fullWidth) {

          if (this.fullWidthTimeout) {
              clearTimeout(this.fullWidthTimeout);
          }

          this.fullWidthTimeout = setTimeout(() => {
              this.checkFullWidth();
          }, 200);
      }
  }

  getImageHeight(): string {
      return (this.currentOptions && this.currentOptions.thumbnails) ?
          this.currentOptions.imagePercent + '%' : '100%';
  }

  getThumbnailsHeight(): string {
      if (this.currentOptions && this.currentOptions.image) {
          return 'calc(' + this.currentOptions.thumbnailsPercent + '% - '
          + this.currentOptions.thumbnailsMargin + 'px)';
      } else {
          return '100%';
      }
  }

  getThumbnailsMarginTop(): string {
      if (this.currentOptions && this.currentOptions.layout === NgxGalleryLayout.ThumbnailsBottom) {
          return this.currentOptions.thumbnailsMargin + 'px';
      } else {
          return '0px';
      }
  }

  getThumbnailsMarginBottom(): string {
      if (this.currentOptions && this.currentOptions.layout === NgxGalleryLayout.ThumbnailsTop) {
          return this.currentOptions.thumbnailsMargin + 'px';
      } else {
          return '0px';
      }
  }

  openPreview(index: number): void {
      if (this.currentOptions.previewCustom) {
          this.currentOptions.previewCustom(index);
      } else {
          this.previewEnabled = true;
          this.preview.open(index);
      }
  }

  onPreviewOpen(): void {
      this.previewOpen.emit();

      if (this.image && this.image.autoPlay) {
          this.image.stopAutoPlay();
      }
  }

  onPreviewClose(): void {
      this.previewEnabled = false;
      this.previewClose.emit();

      if (this.image && this.image.autoPlay) {
          this.image.startAutoPlay();
      }
  }

  selectFromImage(index: number) {
      this.select(index);
  }

  selectFromThumbnails(index: number) {
      this.select(index);

      if (this.currentOptions && this.currentOptions.thumbnails && this.currentOptions.preview
          && (!this.currentOptions.image || this.currentOptions.thumbnailsRemainingCount)) {
          this.openPreview(this.selectedIndex);
      }
  }

  show(index: number): void {
      this.select(index);
  }

  showNext(): void {
      this.image.showNext();
  }

  showPrev(): void {
      this.image.showPrev();
  }

  canShowNext(): boolean {
      if (this.images && this.currentOptions) {
          return (this.currentOptions.imageInfinityMove || this.selectedIndex < this.images.length - 1)
              ? true : false;
      } else {
          return false;
      }
  }

  canShowPrev(): boolean {
      if (this.images && this.currentOptions) {
          return (this.currentOptions.imageInfinityMove || this.selectedIndex > 0) ? true : false;
      } else {
          return false;
      }
  }

  previewSelect(index: number) {
      this.previewChange.emit({index, image: this.images[index]});
  }

  moveThumbnailsRight() {
      this.thubmnails.moveRight();
  }

  moveThumbnailsLeft() {
      this.thubmnails.moveLeft();
  }

  canMoveThumbnailsRight() {
      return this.thubmnails.canMoveRight();
  }

  canMoveThumbnailsLeft() {
      return this.thubmnails.canMoveLeft();
  }

  private resetThumbnails() {
      if (this.thubmnails) {
          this.thubmnails.reset(<number>this.currentOptions.startIndex);
      }
  }

  private select(index: number) {
      this.selectedIndex = index;

      this.change.emit({
          index,
          image: this.images[index]
      });
  }

  private checkFullWidth(): void {
      if (this.currentOptions && this.currentOptions.fullWidth) {
          this.width = document.body.clientWidth + 'px';
          this.left = (-(document.body.clientWidth -
              this.myElement.nativeElement.parentNode.innerWidth) / 2) + 'px';
      }
  }

  private setImages(): void {
      this.smallImages = this.images.map((img) => <string>img.small);
      this.mediumImages = this.images.map((img, i) => new NgxGalleryOrderedImage({
          src: img.medium,
          index: i
      }));
      this.bigImages = this.images.map((img) => <string>img.big);
      this.descriptions = this.images.map((img) => <string>img.description);
      this.links = this.images.map((img) => <string>img.url);
      this.labels = this.images.map((img) => <string>img.label);
  }

  private setBreakpoint(): void {
      this.prevBreakpoint = this.breakpoint;
      let breakpoints;

      if (typeof window !== 'undefined') {
          breakpoints = this.options.filter((opt) => opt.breakpoint >= window.innerWidth)
              .map((opt) => opt.breakpoint);
      }

      if (breakpoints && breakpoints.length) {
          this.breakpoint = breakpoints.pop();
      } else {
          this.breakpoint = undefined;
      }
  }

  private sortOptions(): void {
      this.options = [
          ...this.options.filter((a) => a.breakpoint === undefined),
          ...this.options
              .filter((a) => a.breakpoint !== undefined)
              .sort((a, b) => b.breakpoint - a.breakpoint)
      ];
  }

  private setOptions(): void {
      this.currentOptions = new NgxGalleryOptions({});

      this.options
          .filter((opt) => opt.breakpoint === undefined || opt.breakpoint >= this.breakpoint)
          .map((opt) => this.combineOptions(this.currentOptions, opt));

      this.width = <string>this.currentOptions.width;
      this.height = <string>this.currentOptions.height;
  }

  private combineOptions(first: NgxGalleryOptions, second: NgxGalleryOptions) {
      Object.keys(second).map((val) => first[val] = second[val] !== undefined ? second[val] : first[val]);
  }
}