import { debounceTime, delay } from 'rxjs/operators'; import { ErrorHandler, Inject, Injectable } from '@angular/core'; import { Observable, Subject, fromEvent } from 'rxjs'; import { GuidedTour, TourStep, Orientation, OrientationConfiguration } from './guided-tour.constants'; import { cloneDeep } from 'lodash'; import { DOCUMENT } from "@angular/common"; import { WindowRefService } from "./windowref.service"; @Injectable() export class GuidedTourService { public guidedTourCurrentStepStream: Observable; public guidedTourOrbShowingStream: Observable; private _guidedTourCurrentStepSubject = new Subject(); private _guidedTourOrbShowingSubject = new Subject(); private _currentTourStepIndex = 0; private _currentTour: GuidedTour = null; private _onFirstStep = true; private _onLastStep = true; private _onResizeMessage = false; constructor( public errorHandler: ErrorHandler, private windowRef: WindowRefService, @Inject(DOCUMENT) private dom ) { this.guidedTourCurrentStepStream = this._guidedTourCurrentStepSubject.asObservable(); this.guidedTourOrbShowingStream = this._guidedTourOrbShowingSubject.asObservable(); fromEvent(this.windowRef.nativeWindow, 'resize').pipe(debounceTime(200)).subscribe(() => { if (this._currentTour && this._currentTourStepIndex > -1) { if (this._currentTour.minimumScreenSize && this._currentTour.minimumScreenSize >= this.windowRef.nativeWindow.innerWidth) { this._onResizeMessage = true; const dialog = this._currentTour.resizeDialog || { title: 'Please resize', content: 'You have resized the tour to a size that is too small to continue. Please resize the browser to a larger size to continue the tour or close the tour.' }; this._guidedTourCurrentStepSubject.next(dialog); } else { this._onResizeMessage = false; this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); } } }); } public nextStep(): void { if (this._currentTour.steps[this._currentTourStepIndex].closeAction) { this._currentTour.steps[this._currentTourStepIndex].closeAction(); } if (this._currentTour.steps[this._currentTourStepIndex + 1]) { this._currentTourStepIndex++; this._setFirstAndLast(); if (this._currentTour.steps[this._currentTourStepIndex].action) { this._currentTour.steps[this._currentTourStepIndex].action(); // Usually an action is opening something so we need to give it time to render. setTimeout(() => { if (this._checkSelectorValidity()) { this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); } else { this.nextStep(); } }); } else { setTimeout(() => { if (this._checkSelectorValidity()) { this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); } else { this.nextStep(); } }, 500); } } else { if (this._currentTour.completeCallback) { this._currentTour.completeCallback(); } this.resetTour(); } } public backStep(): void { if (this._currentTour.steps[this._currentTourStepIndex].closeAction) { this._currentTour.steps[this._currentTourStepIndex].closeAction(); } if (this._currentTour.steps[this._currentTourStepIndex - 1]) { this._currentTourStepIndex--; this._setFirstAndLast(); if (this._currentTour.steps[this._currentTourStepIndex].action) { this._currentTour.steps[this._currentTourStepIndex].action(); setTimeout(() => { if (this._checkSelectorValidity()) { this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); } else { this.backStep(); } }); } else { if (this._checkSelectorValidity()) { this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); } else { this.backStep(); } } } else { this.resetTour(); } } public skipTour(): void { if (this._currentTour.skipCallback) { this._currentTour.skipCallback(this._currentTourStepIndex); } this.resetTour(); } public resetTour(): void { this.dom.body.classList.remove('tour-open'); this._currentTour = null; this._currentTourStepIndex = 0; this._guidedTourCurrentStepSubject.next(null); } public startTour(tour: GuidedTour): void { this._currentTour = cloneDeep(tour); this._currentTour.steps = this._currentTour.steps.filter(step => !step.skipStep); this._currentTourStepIndex = 0; this._setFirstAndLast(); this._guidedTourOrbShowingSubject.next(this._currentTour.useOrb); if ( this._currentTour.steps.length > 0 && (!this._currentTour.minimumScreenSize || (this.windowRef.nativeWindow.innerWidth >= this._currentTour.minimumScreenSize)) ) { if (!this._currentTour.useOrb) { this.dom.body.classList.add('tour-open'); } if (this._currentTour.steps[this._currentTourStepIndex].action) { this._currentTour.steps[this._currentTourStepIndex].action(); } if (this._checkSelectorValidity()) { this._guidedTourCurrentStepSubject.next(this.getPreparedTourStep(this._currentTourStepIndex)); } else { this.nextStep(); } } } public activateOrb(): void { this._guidedTourOrbShowingSubject.next(false); this.dom.body.classList.add('tour-open'); } private _setFirstAndLast(): void { this._onLastStep = (this._currentTour.steps.length - 1) === this._currentTourStepIndex; this._onFirstStep = this._currentTourStepIndex === 0; } private _checkSelectorValidity(): boolean { if (this._currentTour.steps[this._currentTourStepIndex].selector) { const selectedElement = this.dom.querySelector(this._currentTour.steps[this._currentTourStepIndex].selector); if (!selectedElement) { this.errorHandler.handleError( // If error handler is configured this should not block the browser. new Error(`Error finding selector ${this._currentTour.steps[this._currentTourStepIndex].selector} on step ${this._currentTourStepIndex + 1} during guided tour: ${this._currentTour.tourId}`) ); return false; } } return true; } public get onLastStep(): boolean { return this._onLastStep; } public get onFirstStep(): boolean { return this._onFirstStep; } public get onResizeMessage(): boolean { return this._onResizeMessage; } public get currentTourStepDisplay(): number { return this._currentTourStepIndex + 1; } public get currentTourStepCount(): number { return this._currentTour && this._currentTour.steps ? this._currentTour.steps.length : 0; } public get preventBackdropFromAdvancing(): boolean { return this._currentTour && this._currentTour.preventBackdropFromAdvancing; } private getPreparedTourStep(index: number): TourStep { return this.setTourOrientation(this._currentTour.steps[index]); } private setTourOrientation(step: TourStep): TourStep { const convertedStep = cloneDeep(step); if ( convertedStep.orientation && !(typeof convertedStep.orientation === 'string') && (convertedStep.orientation as OrientationConfiguration[]).length ) { (convertedStep.orientation as OrientationConfiguration[]).sort((a: OrientationConfiguration, b: OrientationConfiguration) => { if (!b.maximumSize) { return 1; } if (!a.maximumSize) { return -1; } return b.maximumSize - a.maximumSize; }); let currentOrientation: Orientation = Orientation.Top; (convertedStep.orientation as OrientationConfiguration[]).forEach( (orientationConfig: OrientationConfiguration) => { if (!orientationConfig.maximumSize || this.windowRef.nativeWindow.innerWidth <= orientationConfig.maximumSize) { currentOrientation = orientationConfig.orientationDirection; } } ); convertedStep.orientation = currentOrientation; } return convertedStep; } }