UNPKG

@sourceloop/user-onboarding-client

Version:

Library for providing a smooth user onboarding

796 lines (782 loc) 30.9 kB
import * as i0 from '@angular/core'; import { NgModule, Injectable, Inject } from '@angular/core'; import { StorageServiceModule, LOCAL_STORAGE } from 'ngx-webstorage-service'; import { VgCoreModule } from '@videogular/ngx-videogular/core'; import { VgControlsModule } from '@videogular/ngx-videogular/controls'; import { VgOverlayPlayModule } from '@videogular/ngx-videogular/overlay-play'; import { VgBufferingModule } from '@videogular/ngx-videogular/buffering'; import Shepherd from 'shepherd.js'; import * as i2 from '@angular/router'; import { NavigationEnd } from '@angular/router'; import { of, Subject } from 'rxjs'; import { v4 } from 'uuid'; // Copyright (c) 2023 Sourcefuse Technologies class UserOnboardingLibModule { } UserOnboardingLibModule.ɵfac = function UserOnboardingLibModule_Factory(t) { return new (t || UserOnboardingLibModule)(); }; UserOnboardingLibModule.ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: UserOnboardingLibModule }); UserOnboardingLibModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [StorageServiceModule, VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule] }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(UserOnboardingLibModule, [{ type: NgModule, args: [{ declarations: [], imports: [ StorageServiceModule, VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule ], exports: [] }] }], null, null); })(); (function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(UserOnboardingLibModule, { imports: [StorageServiceModule, VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule] }); })(); // Copyright (c) 2023 Sourcefuse Technologies // // This software is released under the MIT License. // https://opensource.org/licenses/MIT const prefix = `<vg-player><vg-overlay-play> </vg-overlay-play> <vg-buffering> </vg-buffering> <vg-scrub-bar> <vg-scrub-bar-current-time> </vg-scrub-bar-current-time> <vg-scrub-bar-buffering-time> </vg-scrub-bar-buffering-time> </vg-scrub-bar> <vg-controls><vg-play-pause> </vg-play-pause> <vg-playback-button> </vg-playback-button> <vg-time-display vgProperty="current" vgFormat="mm:ss"> </vg-time-display> <vg-scrub-bar style="pointer-events: none;"> </vg-scrub-bar> <vg-time-display vgProperty="left" vgFormat="mm:ss"> </vg-time-display> <vg-time-display vgProperty="total" vgFormat="mm:ss"> </vg-time-display><vg-track-selector> </vg-track-selector> <vg-mute ></vg-mute> <vg-volume> </vg-volume> <vg-fullscreen> </vg-fullscreen> </vg-controls> <video width="100%" controls disablepictureinpicture controlslist="nodownload" [vgMedia]="$any(media)" #media id= "singleVideo" preload="auto" controls> <source src = "`; const suffix = '"type = video/webm type="video/mp4"> </video></vg-player>'; // Copyright (c) 2023 Sourcefuse Technologies // // This software is released under the MIT License. // https://opensource.org/licenses/MIT var Status; (function (Status) { Status["InProgress"] = "inProgress"; Status["Complete"] = "complete"; })(Status || (Status = {})); // Copyright (c) 2023 Sourcefuse Technologies // Copyright (c) 2023 Sourcefuse Technologies // // This software is released under the MIT License. // https://opensource.org/licenses/MIT const INTERVAL = 100; const DEFAULT_MAX_WAIT_TIME = 3000; // CCopyright (c) 2023Sourcefuse Technologies class SaveSCommand { constructor(storage) { this.storage = storage; } execute() { const newTourState = this.parameters.state; this.storage.set(`${newTourState.sessionId}_${this.parameters.tourId}`, newTourState); return of(newTourState); } } class LoadSCommand { constructor(storage) { this.storage = storage; } execute() { const currentState = this.storage.get(`${this.parameters.sessionId}_${this.parameters.tourId}`); return of(currentState); } } class DeleteSCommand { constructor(storage) { this.storage = storage; } execute() { this.storage.remove(`${this.parameters.sessionId}_${this.parameters.tourId}`); } } // Copyright (c) 2023 Sourcefuse Technologies class SaveTCommand { constructor(storage) { this.storage = storage; } execute() { const newTour = { tourId: this.parameters.tourId, tourSteps: this.parameters.tourSteps, styleSheet: this.parameters.styleSheet, }; this.storage.set(this.parameters.tourId, newTour); return of(newTour); } } class LoadTCommand { constructor(storage) { this.storage = storage; } execute() { const existingTour = this.storage.get(this.parameters.tourId); return of(existingTour); } } class DeleteTCommand { constructor(storage) { this.storage = storage; } execute() { this.storage.remove(this.parameters.tourId); } } // Copyright (c) 2023 Sourcefuse Technologies class TourStoreServiceService { constructor(storage) { this.storage = storage; this.commandMap = new Map(); this.functionMap = new Map(); this.componentMap = new Map(); this.defaultSSCommand = new SaveSCommand(this.storage); this.defaultSTCommand = new SaveTCommand(this.storage); this.defaultLSCommand = new LoadSCommand(this.storage); this.defaultLTCommand = new LoadTCommand(this.storage); this.defaultDSCommand = new DeleteSCommand(this.storage); this.defaultDTCommand = new DeleteTCommand(this.storage); this.sessionGenerator = () => v4(); this.commandMap.set('SaveTourCommand', this.defaultSTCommand); this.commandMap.set('LoadTourCommand', this.defaultLTCommand); this.commandMap.set('SaveStateCommand', this.defaultSSCommand); this.commandMap.set('LoadStateCommand', this.defaultLSCommand); this.commandMap.set('DeleteStateCommand', this.defaultDSCommand); this.commandMap.set('DeleteTourCommand', this.defaultDTCommand); } registerSaveTourCommand(cmd) { this.commandMap.set('SaveTourCommand', cmd); } registerLoadTourCommand(cmd) { this.commandMap.set('LoadTourCommand', cmd); } registerSaveStateCommand(cmd) { this.commandMap.set('SaveStateCommand', cmd); } registerLoadStateCommand(cmd) { this.commandMap.set('LoadStateCommand', cmd); } registerDeleteStateCommand(cmd) { this.commandMap.set('DeleteStateCommand', cmd); } registerDeleteTourCommand(cmd) { this.commandMap.set('DeleteTourCommand', cmd); } saveTour(parameters) { const command = this.commandMap.get('SaveTourCommand'); command.parameters = parameters; return command.execute(); } loadTour(parameters) { const command = this.commandMap.get('LoadTourCommand'); command.parameters = parameters; return command.execute(); } deleteTour(parameters) { const command = this.commandMap.get('DeleteTourCommand'); command.parameters = parameters; return command.execute(); } saveState(parameters) { const command = this.commandMap.get('SaveStateCommand'); command.parameters = parameters; return command.execute(); } loadState(parameters) { const command = this.commandMap.get('LoadStateCommand'); command.parameters = parameters; return command.execute(); } deleteState(parameters) { const command = this.commandMap.get('DeleteStateCommand'); command.parameters = parameters; return command.execute(); } registerFnRef(key, fn) { this.functionMap.set(key, fn); } registerComponent(key, component) { this.componentMap.set(key, component); } getFnByKey(key) { return this.functionMap.get(key); } getComponentByKey(key) { return this.componentMap.get(key); } generateSessionId() { this.sessionId = this.sessionGenerator(); this.storage.set('TOUR_SESSION_ID', this.sessionId); } getSessionId() { return this.storage.get('TOUR_SESSION_ID'); } setSessionIdGenerator(fn) { this.sessionGenerator = fn; } } TourStoreServiceService.ɵfac = function TourStoreServiceService_Factory(t) { return new (t || TourStoreServiceService)(i0.ɵɵinject(LOCAL_STORAGE)); }; TourStoreServiceService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: TourStoreServiceService, factory: TourStoreServiceService.ɵfac, providedIn: 'root' }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(TourStoreServiceService, [{ type: Injectable, args: [{ providedIn: 'root', }] }], function () { return [{ type: undefined, decorators: [{ type: Inject, args: [LOCAL_STORAGE] }] }]; }, null); })(); class TourServiceService { constructor(tourStoreService, router, componentFactory, injector, appRef) { this.tourStoreService = tourStoreService; this.router = router; this.componentFactory = componentFactory; this.injector = injector; this.appRef = appRef; this.tourComplete = new Subject(); this.tourComplete$ = this.tourComplete.asObservable(); this.tourStepChange = new Subject(); this.tourStepChange$ = this.tourStepChange.asObservable(); this.tourCancel = new Subject(); this.tourCancel$ = this.tourCancel.asObservable(); this.interval = INTERVAL; this._maxWaitTime = DEFAULT_MAX_WAIT_TIME; this._exitOnEsc = true; this.tourFailed = new Subject(); this.tourFailed$ = this.tourFailed.asObservable(); } set maxWaitTime(maxTime) { if (maxTime > 0) { this._maxWaitTime = maxTime; } } get maxWaitTime() { return this._maxWaitTime; } set exitOnEsc(esc) { this._exitOnEsc = esc; } get exitOnEsc() { return this._exitOnEsc; } addRemovedSteps(removedSteps) { const count = removedSteps.length; for (let i = 0; i < count; i++) { this.tour.steps.splice(0, 0, this.tour.steps.pop()); } } getTour() { return this.tour; } actionAssignment(e, b, wrapperNormalNext, wrapperNormalPrev, wrapperNext, wrapperPrev, func, tourId, props) { if (b.key === 'prevAction') { b.action = e.prevRoute === e.currentRoute ? wrapperNormalPrev : wrapperPrev; } else if (b.key === 'nextAction') { b.action = e.nextRoute === e.currentRoute ? wrapperNormalNext : wrapperNext; } else { b.action = func.bind({ tour: this.tour, tourId, props }); } } waitForElement(tourStep, tourId) { if (tourStep.attachTo === undefined) { return Promise.resolve(''); } else { const startTime = new Date().getTime(); return new Promise((resolve, reject) => { const timer = setInterval(() => { const now = new Date().getTime(); const element = this.checkElement(tourStep.attachTo); if (element) { clearInterval(timer); resolve(element); } else if (now - startTime >= this._maxWaitTime) { clearInterval(timer); reject({ tourId, message: `Error in loading tour` }); } else { // do nothing } }, this.interval); }); } } triggerTour(tourInstance, props) { let removedSteps = []; const sessionId = this.tourStoreService.getSessionId(); if (!sessionId) { this.tourStoreService.generateSessionId(); } this.tourStoreService .loadState({ tourId: tourInstance.tourId, sessionId }) .subscribe(tourState => { if (tourState && Object.keys(tourState).length) { if (tourState.status === Status.Complete) { return; } removedSteps = this.getRemovedSteps(tourInstance.tourSteps, tourState); tourInstance.tourSteps = this.getTourSteps(tourInstance.tourSteps, tourState); } else { tourState = { sessionId: this.tourStoreService.getSessionId(), step: tourInstance.tourSteps[0].id, props, status: Status.InProgress, }; this.tourStoreService .saveState({ tourId: tourInstance.tourId, state: tourState, }) .subscribe(); } tourInstance.tourSteps.forEach((e, index) => { e.buttons.forEach(b => { const key = b.key; const func = this.tourStoreService.getFnByKey(key); const wrapperNext = () => { this.navigateAndMoveToNextStep(e, tourInstance, tourState, index); }; const wrapperPrev = () => { this.navigateAndMoveToPrevStep(e, tourInstance, tourState, index, removedSteps); }; const wrapperNormalNext = () => { this.moveToNextStep(tourInstance, tourState, e, index); }; const wrapperNormalPrev = () => { this.moveToPrevStep(tourInstance, tourState, e, index, removedSteps); }; this.actionAssignment(e, b, wrapperNormalNext, wrapperNormalPrev, wrapperNext, wrapperPrev, func, tourInstance.tourId, tourState.props); }); }); this.tour.addSteps(tourInstance.tourSteps); this.waitForElement(tourInstance.tourSteps[0], tourInstance.tourId).then(element => { if (element) { element.scrollIntoView(true); } this.tour.start(); if (removedSteps.length) { removedSteps.forEach((er, index) => { er.buttons.forEach(br => { const k = br.key; const funcRemoved = this.tourStoreService.getFnByKey(k); const wrapperNextRemoved = () => { this.navigateAndMoveToNextStepRemoved(er, tourInstance, tourState, index, removedSteps); }; const wrapperPrevRemoved = () => { this.navigateAndMoveToPrevStepRemoved(er, tourInstance, tourState, index, removedSteps); }; const wrapperNormalNextRemoved = () => { this.moveToNextStepRemoved(tourInstance, tourState, er, index, removedSteps); }; const wrapperNormalPrevRemoved = () => { this.moveToPrevStepRemoved(tourInstance, tourState, er, index, removedSteps); }; this.actionAssignment(er, br, wrapperNormalNextRemoved, wrapperNormalPrevRemoved, wrapperNextRemoved, wrapperPrevRemoved, funcRemoved, tourInstance.tourId, tourState.props); }); }); this.tour.addSteps(removedSteps); this.addRemovedSteps(removedSteps); } }, err => { this.tourFailed.next(err); }); }); } run(tourId, params, props, filterFn, inputs) { this.tourStoreService .loadTour({ tourId, sessionId: this.tourStoreService.getSessionId(), }) .subscribe(tourInstance => { this.checkAndThrowError(tourInstance); if (params) { let steps = JSON.stringify(tourInstance.tourSteps); Object.keys(params).forEach(key => { steps = steps.replace(new RegExp(`\\{\\{${key}\\}\\}`), params[key]); }); tourInstance.tourSteps = JSON.parse(steps); } this.tour = new Shepherd.Tour({ useModalOverlay: true, exitOnEsc: this._exitOnEsc, defaultStepOptions: { cancelIcon: { enabled: true, }, scrollTo: { behavior: 'smooth', block: 'center' }, }, }); this.tour.on('complete', (event) => { event.tourId = tourInstance.tourId; this.tourComplete.next(event); }); // on pressing esc cancel event is emitted by shepherd this.tour.on('cancel', (event) => { event.tourId = tourInstance.tourId; this.tourCancel.next(event); }); if (filterFn) { tourInstance.tourSteps = filterFn(tourInstance.tourSteps); } if (tourInstance.tourSteps[0].attachTo) { tourInstance.tourSteps[0].attachTo.scrollTo = false; } this.checkComponents(tourInstance, inputs).then(() => { this.triggerTour(tourInstance, props); }); }); } async checkComponents(tourInstance, inputs) { for (const step of tourInstance.tourSteps) { if (typeof step.text !== 'string' && typeof step.text !== 'function') { const htmlStep = await this.parseComponent({ forStep: step.id, component: this.tourStoreService.getComponentByKey(step.text.component), }, inputs); step.text = htmlStep.html; } } } checkElement(attachTo) { switch (attachTo.type) { case 'string': return document.querySelector(attachTo.element); case 'element': return attachTo.element; case 'function': const fn = this.tourStoreService.getFnByKey(attachTo.element); return fn(); default: return false; } } getRemovedSteps(tourSteps, tourState) { let f = true; return tourSteps.filter(e => { if (e.id === tourState.step || !f) { f = false; } return f; }); } getTourSteps(tourSteps, tourState) { let flag = false; return tourSteps.filter(e => { if (e.id === tourState.step || flag) { flag = true; } return flag; }); } pauseAllVideos() { document.querySelectorAll('video').forEach(vid => vid.pause()); } setTourComplete(tourId, props) { this.tourStoreService .saveState({ tourId, state: { sessionId: this.tourStoreService.getSessionId(), step: '', props, status: Status.Complete, }, }) .subscribe(); } checkAndThrowError(tourInstance) { if (!tourInstance) { throw new Error(`No Tour Present`); } else if (tourInstance.tourSteps.length === 0) { throw new Error(`No Tour Steps Found`); } else if (tourInstance.tourSteps[0].buttons) { tourInstance.tourSteps[0].buttons.forEach(button => { if (button.key === `prevAction`) { throw new Error(`Step 1 can't have a previous button`); } }); } else { //do nothing } } moveToNextStep(tourInstance, tourState, step, index) { if (index === tourInstance.tourSteps.length - 1) { this.setTourComplete(tourInstance.tourId, tourState.props); this.tour.next(); } else { this.waitForElement(tourInstance.tourSteps[index + 1], tourInstance.tourId).then(() => { this.tourStoreService .saveState({ tourId: tourInstance.tourId, state: { sessionId: this.tourStoreService.getSessionId(), step: step.nextStepId, props: tourState.props, status: Status.InProgress, }, }) .subscribe(); this.tour.show(step.nextStepId); this.tourStepChange.next({ tourId: tourInstance.tourId, currentStepId: step.nextStepId, previousStepId: step.id, moveForward: true, }); this.pauseAllVideos(); }, err => { this.tourFailed.next(err); }); } } moveToPrevStep(tourInstance, tourState, step, index, removedSteps) { let waitForLastElementOfRemovedSteps = false; if (index === 0 && removedSteps.length) { waitForLastElementOfRemovedSteps = true; } this.waitForElement(waitForLastElementOfRemovedSteps ? removedSteps[removedSteps.length - 1] : tourInstance.tourSteps[index - 1], tourInstance.tourId).then(() => { this.tourStoreService .saveState({ tourId: tourInstance.tourId, state: { sessionId: this.tourStoreService.getSessionId(), step: waitForLastElementOfRemovedSteps ? removedSteps[removedSteps.length - 1].id : step.prevStepId, props: tourState.props, status: Status.InProgress, }, }) .subscribe(); if (waitForLastElementOfRemovedSteps) { this.tour.show(removedSteps[removedSteps.length - 1].id); } else { this.tour.show(step.prevStepId); } this.tourStepChange.next({ tourId: tourInstance.tourId, currentStepId: waitForLastElementOfRemovedSteps ? removedSteps[removedSteps.length - 1].id : step.prevStepId, previousStepId: step.id, moveForward: false, }); this.pauseAllVideos(); }, err => { this.tourFailed.next(err); }); } moveToNextStepRemoved(tourInstance, tourState, step, index, removedSteps) { let waitForFirstElementOfSteps = false; if (index === removedSteps.length - 1) { waitForFirstElementOfSteps = true; } this.waitForElement(waitForFirstElementOfSteps ? tourInstance.tourSteps[0] : removedSteps[index + 1], tourInstance.tourId).then(() => { this.tourStoreService .saveState({ tourId: tourInstance.tourId, state: { sessionId: this.tourStoreService.getSessionId(), step: waitForFirstElementOfSteps ? tourInstance.tourSteps[0].id : step.nextStepId, props: tourState.props, status: Status.InProgress, }, }) .subscribe(); if (waitForFirstElementOfSteps) { this.tour.show(tourInstance.tourSteps[0].id); } else { this.tour.show(step.nextStepId); } this.tourStepChange.next({ tourId: tourInstance.tourId, currentStepId: waitForFirstElementOfSteps ? tourInstance.tourSteps[0].id : step.nextStepId, previousStepId: step.id, moveForward: true, }); this.pauseAllVideos(); }, err => { this.tourFailed.next(err); }); } moveToPrevStepRemoved(tourInstance, tourState, step, index, removedSteps) { this.waitForElement(removedSteps[index - 1], tourInstance.tourId).then(() => { this.tourStoreService .saveState({ tourId: tourInstance.tourId, state: { sessionId: this.tourStoreService.getSessionId(), step: step.prevStepId, props: tourState.props, status: Status.InProgress, }, }) .subscribe(); this.tour.show(step.prevStepId); this.tourStepChange.next({ tourId: tourInstance.tourId, currentStepId: step.prevStepId, previousStepId: step.id, moveForward: false, }); this.pauseAllVideos(); }, err => { this.tourFailed.next(err); }); } navigateAndMoveToNextStep(currentStep, tourInstance, tourState, index) { if (index < tourInstance.tourSteps.length - 1) { this.router.navigate([currentStep.nextRoute]); this.router.events.subscribe((event) => { const nextStep = tourInstance.tourSteps.filter(ts => ts.id === currentStep.nextStepId)[0]; if (event instanceof NavigationEnd && event.url === nextStep.currentRoute) { this.moveToNextStep(tourInstance, tourState, currentStep, index); } }); } else { this.moveToNextStep(tourInstance, tourState, currentStep, index); } } navigateAndMoveToPrevStep(currentStep, tourInstance, tourState, index, removedSteps) { let moveToLastRemovedStep = false; if (index === 0 && removedSteps.length) { moveToLastRemovedStep = true; } const prevRoute = moveToLastRemovedStep ? removedSteps[removedSteps.length - 1].prevRoute : currentStep.prevRoute; this.router.navigate([prevRoute]); this.router.events.subscribe((event) => { let prevStep; if (moveToLastRemovedStep) { prevStep = removedSteps[removedSteps.length - 1]; } else { prevStep = tourInstance.tourSteps.filter(ts => ts.id === currentStep.prevStepId)[0]; } if (event instanceof NavigationEnd && event.url === prevStep.currentRoute) { this.moveToPrevStep(tourInstance, tourState, currentStep, index, removedSteps); } }); } navigateAndMoveToNextStepRemoved(currentStep, tourInstance, tourState, index, removedSteps) { let moveToFirstStep = false; if (index === removedSteps.length - 1) { moveToFirstStep = true; } const nextRoute = moveToFirstStep ? tourInstance.tourSteps[0].nextRoute : currentStep.nextRoute; this.router.navigate([nextRoute]); this.router.events.subscribe((event) => { let nextStep; if (moveToFirstStep) { nextStep = tourInstance.tourSteps[0]; } else { nextStep = tourInstance.tourSteps.filter(ts => ts.id === currentStep.nextStepId)[0]; } if (event instanceof NavigationEnd && event.url === nextStep.currentRoute) { this.moveToNextStepRemoved(tourInstance, tourState, currentStep, index, removedSteps); } }); } navigateAndMoveToPrevStepRemoved(currentStep, tourInstance, tourState, index, removedSteps) { this.router.navigate([currentStep.prevRoute]); this.router.events.subscribe((event) => { const prevStep = tourInstance.tourSteps.filter(ts => ts.id === currentStep.prevStepId)[0]; if (event instanceof NavigationEnd && event.url === prevStep.currentRoute) { this.moveToPrevStepRemoved(tourInstance, tourState, currentStep, index, removedSteps); } }); } parseComponent(step, input) { return Promise.resolve({ forStep: step.forStep, html: () => { const factory = this.componentFactory.resolveComponentFactory(step.component); const constructedComponent = factory.create(this.injector); Object.keys(input ?? {}).forEach(k => { constructedComponent.instance[k] = input[k]; }); constructedComponent.instance['tour'] = this.tour; this.appRef.attachView(constructedComponent.hostView); return constructedComponent.location.nativeElement; }, }); } } TourServiceService.ɵfac = function TourServiceService_Factory(t) { return new (t || TourServiceService)(i0.ɵɵinject(TourStoreServiceService), i0.ɵɵinject(i2.Router), i0.ɵɵinject(i0.ComponentFactoryResolver), i0.ɵɵinject(i0.Injector), i0.ɵɵinject(i0.ApplicationRef)); }; TourServiceService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: TourServiceService, factory: TourServiceService.ɵfac, providedIn: 'root' }); (function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(TourServiceService, [{ type: Injectable, args: [{ providedIn: 'root', }] }], function () { return [{ type: TourStoreServiceService }, { type: i2.Router }, { type: i0.ComponentFactoryResolver }, { type: i0.Injector }, { type: i0.ApplicationRef }]; }, null); })(); // Copyright (c) 2023 Sourcefuse Technologies // Copyright (c) 2023 Sourcefuse Technologies /** * Generated bundle index. Do not edit. */ export { Status, TourServiceService, TourStoreServiceService, UserOnboardingLibModule, prefix, suffix }; //# sourceMappingURL=sourceloop-user-onboarding-client.mjs.map