232 lines
7.7 KiB
TypeScript
232 lines
7.7 KiB
TypeScript
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<TourStep>;
|
|
public guidedTourOrbShowingStream: Observable<boolean>;
|
|
|
|
private _guidedTourCurrentStepSubject = new Subject<TourStep>();
|
|
private _guidedTourOrbShowingSubject = new Subject<boolean>();
|
|
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;
|
|
}
|
|
}
|