UNPKG

ngx-ui-tour-core

Version:
896 lines (879 loc) 36.5 kB
import * as i0 from '@angular/core'; import { inject, PLATFORM_ID, Injectable, RendererFactory2, Component, ChangeDetectionStrategy, HostListener, afterNextRender, AfterRenderPhase, ElementRef, Directive } from '@angular/core'; import { Router, NavigationStart } from '@angular/router'; import { firstValueFrom, fromEvent, timeout, of, debounceTime, map, Subject, merge, filter, takeUntil, first, delay } from 'rxjs'; import { isPlatformBrowser, DOCUMENT } from '@angular/common'; function deepMerge(...objects) { return objects.reduce((acc, cur) => { cur ??= {}; const keys = Object.keys(cur); for (const key of keys) { const accValue = acc[key], curValue = cur[key]; if (isPlainObject(accValue) && isPlainObject(curValue)) { acc[key] = deepMerge(accValue, curValue); } else { acc[key] = curValue; } } return acc; }, {}); } function isPlainObject(value) { return value instanceof Object && value.constructor === Object; } function isCovered(htmlElement, sidesToCheck = 2 /* ElementSides.All */) { const rect = htmlElement.getBoundingClientRect(), topEl = document.elementFromPoint(rect.left, rect.top), bottomEl = document.elementFromPoint(rect.right, rect.bottom), isTopCovered = !!topEl && topEl !== htmlElement && !areElementsRelated(topEl, htmlElement), isBottomCovered = !!bottomEl && bottomEl !== htmlElement && !areElementsRelated(bottomEl, htmlElement); if (sidesToCheck === 0 /* ElementSides.Top */) { return isTopCovered; } if (sidesToCheck === 1 /* ElementSides.Bottom */) { return isBottomCovered; } return isTopCovered || isBottomCovered; } function areElementsRelated(el1, el2) { return el1.contains(el2) || el2.contains(el1); } function isInViewport(htmlElement, sidesToCheck = 2 /* ElementSides.All */) { const viewportWidth = window.innerWidth, viewportHeight = window.innerHeight, boundingRectangle = htmlElement.getBoundingClientRect(), areCornersInViewport = boundingRectangle.left >= 0 && boundingRectangle.right <= viewportWidth, isTopInViewport = boundingRectangle.top >= 0, isBottomInViewport = boundingRectangle.bottom <= viewportHeight; if (sidesToCheck === 0 /* ElementSides.Top */) { return isTopInViewport && areCornersInViewport; } if (sidesToCheck === 1 /* ElementSides.Bottom */) { return isBottomInViewport && areCornersInViewport; } return isTopInViewport && isBottomInViewport && areCornersInViewport; } class OverflowUtils { static getVisibleSection(childRect, containerRect) { return OverflowUtils._isHeightOverflowing(childRect, containerRect) ? OverflowUtils._getOverlap(childRect, containerRect) : childRect; } static isHeightOverflowing(child, container) { return OverflowUtils._isHeightOverflowing(child instanceof HTMLElement ? child.getBoundingClientRect() : child, container instanceof HTMLElement ? container.getBoundingClientRect() : container); } static _isHeightOverflowing(childRect, containerRect) { return containerRect.height < childRect.height; } static _getOverlap(a, b) { const top = Math.max(a.top, b.top), left = Math.max(a.left, b.left), right = Math.min(a.right, b.right), bottom = Math.min(a.bottom, b.bottom); return new DOMRect(left, top, right - left, bottom - top); } } function getScrollableParent(node) { if (!(node instanceof HTMLElement || node instanceof ShadowRoot)) { return null; } const element = node instanceof ShadowRoot ? node.host : node; const style = getComputedStyle(element), isScrollable = element.scrollHeight > element.clientHeight, overflow = style.overflowY, scrollableOverflow = ['scroll', 'auto']; if (isScrollable && scrollableOverflow.includes(overflow)) { return element; } return getScrollableParent(element.parentNode); } class ScrollUtils { static getScrollContainer(anchorEl, userScrollContainer) { if (typeof userScrollContainer === 'string') { return document.documentElement.querySelector(userScrollContainer); } if (userScrollContainer instanceof HTMLElement) { return userScrollContainer; } return getScrollableParent(anchorEl); } } class ScrollingService { constructor() { this.platformId = inject(PLATFORM_ID); this.isBrowser = isPlatformBrowser(this.platformId); this.document = inject(DOCUMENT); this.window = this.document.defaultView; } ensureVisible(anchorElement, options) { this.scrollOptions = options; this.anchorEl = anchorElement; const behavior = options.smoothScroll && this.isBrowser ? 'smooth' : 'auto'; const userScrollContainer = this.scrollOptions.scrollContainer, scrollContainer = ScrollUtils.getScrollContainer(anchorElement, userScrollContainer) ?? document.documentElement; if (OverflowUtils.isHeightOverflowing(anchorElement, scrollContainer)) { anchorElement.scrollIntoView({ block: 'start', inline: 'start', behavior }); } else if (options.center && !('safari' in this.window)) { anchorElement.scrollIntoView({ block: 'center', inline: 'center', behavior }); } else if (!isInViewport(anchorElement, 1 /* ElementSides.Bottom */) || isCovered(anchorElement, 1 /* ElementSides.Bottom */)) { anchorElement.scrollIntoView({ block: 'end', inline: 'nearest', behavior }); } else if (!isInViewport(anchorElement, 0 /* ElementSides.Top */) || isCovered(anchorElement, 0 /* ElementSides.Top */)) { anchorElement.scrollIntoView({ block: 'start', inline: 'nearest', behavior }); } else { return Promise.resolve(); } return behavior === 'smooth' ? firstValueFrom(this.waitForScrollFinish$) : Promise.resolve(); } get waitForScrollFinish$() { const userScrollContainer = this.scrollOptions.scrollContainer, // Default here is "document" instead of "document.documentElement" on purpose scrollContainer = ScrollUtils.getScrollContainer(this.anchorEl, userScrollContainer) ?? document; return fromEvent(scrollContainer, 'scroll') .pipe(timeout({ each: 75, with: () => of(undefined) }), debounceTime(50), map(() => undefined)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ScrollingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ScrollingService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ScrollingService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class TourResizeObserverService { constructor() { this.resizeElSubject = new Subject(); this.platformId = inject(PLATFORM_ID); this.isResizeObserverSupported = isPlatformBrowser(this.platformId) && !!ResizeObserver; this.document = inject(DOCUMENT); this.window = this.document.defaultView; this.resize$ = merge(this.resizeElSubject, fromEvent(this.window, 'resize')).pipe(debounceTime(10)); } observeElement(target) { if (this.isResizeObserverSupported && !this.resizeObserver) { this.resizeObserver = new ResizeObserver(() => this.resizeElSubject.next()); } this.resizeObserver?.observe(target); } unobserveElement(target) { this.resizeObserver?.unobserve(target); } disconnect() { this.resizeObserver?.disconnect(); this.resizeObserver = undefined; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: TourResizeObserverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: TourResizeObserverService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: TourResizeObserverService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class TourBackdropService { constructor() { this.isSpotlightClosed = false; this.rendererFactory = inject(RendererFactory2); this.renderer = this.rendererFactory.createRenderer(null, null); this.resizeObserverService = inject(TourResizeObserverService); this.scrollingService = inject(ScrollingService); this.document = inject(DOCUMENT); } show(targetElement, step) { if (this.targetHtmlElement) { this.resizeObserverService.unobserveElement(this.targetHtmlElement); } this.targetHtmlElement = targetElement.nativeElement; this.step = step; this.resizeObserverService.observeElement(this.targetHtmlElement); if (!this.backdropElements) { this.backdropElements = this.createBackdropElements(); this.subscribeToResizeEvents(); } this.isSpotlightClosed = false; this.setBackdropPosition(); } closeSpotlight() { if (!this.backdropElements) { return; } const targetRect = this.targetHtmlElement.getBoundingClientRect(), centerX = targetRect.left + (targetRect.width / 2), centerY = targetRect.top + (targetRect.height / 2), centerRect = { top: centerY, right: centerX, bottom: centerY, left: centerX, width: 0, height: 0 }; this.isSpotlightClosed = true; this.setBackdropPosition(centerRect); } setBackdropPosition(rectangle = null) { const docEl = this.document.documentElement, scrollContainer = ScrollUtils.getScrollContainer(this.targetHtmlElement, this.step.scrollContainer) ?? docEl, elementBoundingRect = rectangle ?? this.targetHtmlElement.getBoundingClientRect(), scrollContainerRect = scrollContainer.getBoundingClientRect(), visibleSection = OverflowUtils.getVisibleSection(elementBoundingRect, scrollContainerRect), scrollHeight = docEl.scrollHeight, scrollWidth = docEl.scrollWidth, window = this.document.defaultView, scrollX = window.scrollX, scrollY = window.scrollY, offset = this.isSpotlightClosed ? 0 : this.step.backdropConfig?.offset ?? 0, leftRect = { width: visibleSection.left + scrollX - offset, height: scrollHeight, top: 0, left: 0 }, topRect = { width: visibleSection.width + offset * 2, height: visibleSection.top + scrollY - offset, top: 0, left: visibleSection.left + scrollX - offset }, bottomRect = { width: visibleSection.width + offset * 2, height: scrollHeight - (visibleSection.bottom + scrollY) - offset, top: visibleSection.bottom + scrollY + offset, left: visibleSection.left + scrollX - offset }, rightRect = { width: scrollWidth - (visibleSection.right + scrollX) - offset, height: scrollHeight, top: 0, left: visibleSection.right + scrollX + offset }, rectangles = [leftRect, topRect, bottomRect, rightRect]; for (let i = 0; i < rectangles.length; i++) { const styles = this.createBackdropStyles(rectangles[i]); this.applyStyles(styles, this.backdropElements[i]); } } subscribeToResizeEvents() { this.resizeSubscription = this.resizeObserverService.resize$ .subscribe(() => { this.setBackdropPosition(); if (!this.step.disableScrollToAnchor) { this.scrollingService.ensureVisible(this.targetHtmlElement, { center: this.step.centerAnchorOnScroll, smoothScroll: false }); } }); } close() { if (this.backdropElements) { this.resizeObserverService.unobserveElement(this.targetHtmlElement); this.removeBackdropElement(); this.resizeSubscription.unsubscribe(); } } disconnectResizeObserver() { this.resizeObserverService.disconnect(); } removeBackdropElement() { this.backdropElements.forEach(backdropElement => this.renderer.removeChild(this.parentContainer, backdropElement)); this.backdropElements = undefined; } applyStyles(styles, element) { for (const name of Object.keys(styles)) { this.renderer.setStyle(element, name, styles[name]); } } createBackdropStyles(rectangle) { const config = this.step.backdropConfig, normalizedRect = { ...rectangle, width: Math.max(rectangle.width, 0), height: Math.max(rectangle.height, 0) }; return { position: 'absolute', width: `${normalizedRect.width}px`, height: `${normalizedRect.height}px`, top: `${normalizedRect.top}px`, left: `${normalizedRect.left}px`, backgroundColor: config?.backgroundColor ?? 'rgba(0, 0, 0, 0.7)', zIndex: config?.zIndex ?? '101' }; } createBackdropElement() { const backdropElement = this.renderer.createElement('div'); this.renderer.addClass(backdropElement, 'ngx-ui-tour_backdrop'); this.renderer.appendChild(this.parentContainer, backdropElement); return backdropElement; } createBackdropElements() { return Array .from({ length: 4 }) .map(() => this.createBackdropElement()); } get parentContainer() { const parent = this.step.backdropConfig?.parentContainer; if (parent instanceof HTMLElement) { return parent; } if (typeof parent === 'string') { const queryResult = this.document.documentElement.querySelector(parent); return queryResult ?? this.document.body; } return this.document.body; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: TourBackdropService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: TourBackdropService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: TourBackdropService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class AnchorClickService { constructor() { this.rendererFactory = inject(RendererFactory2); this.renderer = this.rendererFactory.createRenderer(null, null); } removeListener() { if (this.unListenToAnchorClickFn) { this.unListenToAnchorClickFn(); this.unListenToAnchorClickFn = undefined; } } addListener(anchorEl, callback) { this.unListenToAnchorClickFn = this.renderer.listen(anchorEl, 'click', callback); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: AnchorClickService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: AnchorClickService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: AnchorClickService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class ScrollBlockingService { constructor() { this.isEnabled = false; this.platformId = inject(PLATFORM_ID); this.isBrowser = isPlatformBrowser(this.platformId); this.rendererFactory = inject(RendererFactory2); this.renderer = this.rendererFactory.createRenderer(null, null); } enable(scrollContainer) { if (!this.isBrowser || this.isEnabled) { return; } this.userScrollContainer = scrollContainer; this.toggleOverflow(); this.isEnabled = true; } disable() { if (!this.isEnabled) { return; } this.toggleOverflow(); this.isEnabled = false; } toggleOverflow() { // Don't try to automatically detect scroll container here since that breaks smooth scrolling const scrollContainer = ScrollUtils.getScrollContainer(null, this.userScrollContainer) ?? document.documentElement; if (this.isEnabled) { this.renderer.removeStyle(scrollContainer, 'overflow'); } else { this.renderer.setStyle(scrollContainer, 'overflow', 'hidden'); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ScrollBlockingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ScrollBlockingService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: ScrollBlockingService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); var TourState; (function (TourState) { TourState[TourState["OFF"] = 0] = "OFF"; TourState[TourState["ON"] = 1] = "ON"; TourState[TourState["PAUSED"] = 2] = "PAUSED"; })(TourState || (TourState = {})); var Direction; (function (Direction) { Direction[Direction["Forwards"] = 0] = "Forwards"; Direction[Direction["Backwards"] = 1] = "Backwards"; })(Direction || (Direction = {})); const DEFAULT_STEP_OPTIONS = { disableScrollToAnchor: false, prevBtnTitle: 'Prev', nextBtnTitle: 'Next', endBtnTitle: 'End', enableBackdrop: false, isAsync: false, isOptional: false, delayAfterNavigation: 100, delayBeforeStepShow: 0, nextOnAnchorClick: false, duplicateAnchorHandling: 'error', centerAnchorOnScroll: true, disablePageScrolling: true, smoothScroll: true, allowUserInitiatedNavigation: false, stepDimensions: { minWidth: '250px', maxWidth: '280px', width: 'auto' }, showProgress: true }; // noinspection JSUnusedGlobalSymbols class TourService { constructor() { this.stepShow$ = new Subject(); this.stepHide$ = new Subject(); this.initialize$ = new Subject(); this.start$ = new Subject(); this.end$ = new Subject(); this.pause$ = new Subject(); this.resume$ = new Subject(); this.anchorRegister$ = new Subject(); this.anchorUnregister$ = new Subject(); this.events$ = merge(this.stepShow$.pipe(map(value => ({ name: 'stepShow', value }))), this.stepHide$.pipe(map(value => ({ name: 'stepHide', value }))), this.initialize$.pipe(map(value => ({ name: 'initialize', value }))), this.start$.pipe(map(value => ({ name: 'start', value }))), this.end$.pipe(map(value => ({ name: 'end', value }))), this.pause$.pipe(map(value => ({ name: 'pause', value }))), this.resume$.pipe(map(value => ({ name: 'resume', value }))), this.anchorRegister$.pipe(map(value => ({ name: 'anchorRegister', value }))), this.anchorUnregister$.pipe(map(value => ({ name: 'anchorUnregister', value })))); this.steps = []; this.anchors = {}; this.status = TourState.OFF; this.isHotKeysEnabled = true; this.direction = Direction.Forwards; this.waitingForScroll = false; this.navigationStarted = false; this.router = inject(Router); this.backdrop = inject(TourBackdropService); this.anchorClickService = inject(AnchorClickService); this.scrollBlockingService = inject(ScrollBlockingService); this.scrollingService = inject(ScrollingService); } initialize(steps, stepDefaults) { if (this.status === TourState.ON) { console.warn('Can not re-initialize the UI tour while it\'s still active'); return; } if (steps && steps.length > 0) { this.status = TourState.OFF; this.steps = steps.map(step => deepMerge(DEFAULT_STEP_OPTIONS, this.userDefaults, stepDefaults, step)); this.validateSteps(); this.initialize$.next(this.steps); this.subscribeToNavigationStartEvent(); } } setDefaults(defaultOptions) { this.userDefaults = defaultOptions; } getDefaults() { return this.userDefaults; } validateSteps() { for (const step of this.steps) { if (step.isAsync && step.isOptional && !step.asyncStepTimeout) { throw new Error(`Tour step with anchor id "${step.anchorId}" can only be both "async" and ` + `"optional" when "asyncStepTimeout" is specified!`); } } } subscribeToNavigationStartEvent() { this.router.events .pipe(filter((event) => event instanceof NavigationStart), takeUntil(this.end$)) .subscribe((event) => { if (!this.currentStep) { return; } const browserBackBtnPressed = event.navigationTrigger === 'popstate', userNavigationAllowed = this.currentStep.allowUserInitiatedNavigation; if (!this.navigationStarted && (browserBackBtnPressed || !userNavigationAllowed)) { this.end(); } }); } disableHotkeys() { this.isHotKeysEnabled = false; } enableHotkeys() { this.isHotKeysEnabled = true; } start() { if (this.status === TourState.ON) { console.warn('tourService.start() called while the tour is already running.'); return; } this.startAt(0); } startAt(stepId) { this.status = TourState.ON; this.goToStep(this.loadStep(stepId)); this.start$.next(); } end() { if (this.waitingForScroll) { return; } if (this.status === TourState.OFF) { return; } this.status = TourState.OFF; this.disableTour(); this.currentStep = undefined; this.direction = Direction.Forwards; this.end$.next(); } pause() { this.status = TourState.PAUSED; this.disableTour(); this.pause$.next(); } disableTour() { this.hideStep(this.currentStep); this.anchorClickService.removeListener(); this.backdrop.close(); this.backdrop.disconnectResizeObserver(); this.scrollBlockingService.disable(); } resume() { this.status = TourState.ON; this.showStep(this.currentStep); this.resume$.next(); } toggle(pause) { if (pause) { if (this.currentStep) { this.pause(); } else { this.resume(); } } else { if (this.currentStep) { this.end(); } else { this.start(); } } } next() { if (this.waitingForScroll) { return; } this.direction = Direction.Forwards; if (this.hasNext(this.currentStep)) { this.goToStep(this.loadStep(this.currentStep.nextStep ?? this.getStepIndex(this.currentStep) + 1)); } } getStepIndex(step) { const index = this.steps.indexOf(step); return index < 0 ? 0 : index; } hasNext(step) { if (!step) { console.warn('Can\'t get next step. No currentStep.'); return false; } return (step.nextStep !== undefined || (this.getStepIndex(step) < this.steps.length - 1 && !this.isNextOptionalAnchorMissing(step))); } isNextOptionalAnchorMissing(step) { const stepIndex = this.getStepIndex(step); for (let i = stepIndex + 1; i < this.steps.length; i++) { const nextStep = this.steps[i]; if (!nextStep.isOptional || this.anchors[nextStep.anchorId]) return false; } return true; } prev() { if (this.waitingForScroll) { return; } this.direction = Direction.Backwards; if (this.hasPrev(this.currentStep)) { this.goToStep(this.loadStep(this.currentStep.prevStep ?? this.getStepIndex(this.currentStep) - 1)); } } hasPrev(step) { if (!step) { console.warn('Can\'t get previous step. No currentStep.'); return false; } return step.prevStep !== undefined || (this.getStepIndex(step) > 0 && !this.isPrevOptionalAnchorMising(step)); } isPrevOptionalAnchorMising(step) { const stepIndex = this.getStepIndex(step); for (let i = stepIndex - 1; i > -1; i--) { const prevStep = this.steps[i]; if (!prevStep.isOptional || this.anchors[prevStep.anchorId]) return false; } return true; } goto(stepId) { this.goToStep(this.loadStep(stepId)); } register(anchorId, anchor) { if (!anchorId) { return; } if (this.anchors[anchorId]) { const step = this.findStepByAnchorId(anchorId), duplicateAnchorHandling = step?.duplicateAnchorHandling ?? this.userDefaults?.duplicateAnchorHandling ?? 'error'; switch (duplicateAnchorHandling) { case 'error': throw new Error(`Tour anchor with id "${anchorId}" already registered!`); case 'registerFirst': return; } } this.anchors[anchorId] = anchor; this.anchorRegister$.next(anchorId); } findStepByAnchorId(anchorId) { return this.steps.find(step => step.anchorId === anchorId); } unregister(anchorId) { if (!anchorId) { return; } delete this.anchors[anchorId]; this.anchorUnregister$.next(anchorId); } getStatus() { return this.status; } isHotkeysEnabled() { return this.isHotKeysEnabled; } goToStep(step) { if (!step) { console.warn('Can\'t go to non-existent step'); this.end(); return; } if (this.currentStep) { this.backdrop.closeSpotlight(); this.hideStep(this.currentStep); } this.anchorClickService.removeListener(); if (step.route !== undefined && step.route !== null) { this.navigateToRouteAndSetStep(step); } else { this.setCurrentStepAsync(step); } } listenToOnAnchorClick(step) { if (step.nextOnAnchorClick) { const anchorEl = this.anchors[step.anchorId].element.nativeElement; this.anchorClickService.addListener(anchorEl, () => this.next()); } } async navigateToRouteAndSetStep(step) { const url = typeof step.route === 'string' ? step.route : this.router.createUrlTree(step.route), matchOptions = { paths: 'exact', matrixParams: 'exact', queryParams: 'subset', fragment: 'exact' }; const isActive = this.router.isActive(url, matchOptions); if (isActive) { this.setCurrentStepAsync(step); return; } this.navigationStarted = true; const navigated = await this.router.navigateByUrl(url); this.navigationStarted = false; if (!navigated) { console.warn('Navigation to route failed: ', step.route); this.end(); } else { this.setCurrentStepAsync(step, step.delayAfterNavigation); } } loadStep(stepId) { if (typeof stepId === 'number') { return this.steps[stepId]; } else { return this.steps.find(step => step.stepId === stepId); } } setCurrentStep(step) { this.currentStep = step; this.showStep(this.currentStep); } setCurrentStepAsync(step, delay = 0) { delay = delay || step.delayBeforeStepShow; setTimeout(() => this.setCurrentStep(step), delay); } async showStep(step, skipAsync = false) { const anchor = this.anchors[step && step.anchorId]; if (!anchor) { if (step.isAsync && !skipAsync) { let anchorRegistered$ = this.anchorRegister$ .pipe(filter(anchorId => anchorId === step.anchorId), first(), delay(0)); if (step.asyncStepTimeout) { anchorRegistered$ = anchorRegistered$ .pipe(timeout({ each: step.asyncStepTimeout, with: () => of(null) })); } anchorRegistered$ .subscribe(() => this.showStep(step, true)); return; } if (step.isOptional) { this[this.direction === Direction.Forwards ? 'next' : 'prev'](); return; } console.warn(`Can't attach to unregistered anchor with id "${step.anchorId}"`); this.end(); return; } this.listenToOnAnchorClick(step); this.waitingForScroll = true; await this.scrollToAnchor(step); this.waitingForScroll = false; anchor.showTourStep(step); this.toggleBackdrop(step); this.togglePageScrolling(step); this.stepShow$.next({ step, direction: this.direction }); } hideStep(step) { const anchor = this.anchors[step && step.anchorId]; if (!anchor) { return; } anchor.hideTourStep(); this.stepHide$.next({ step, direction: this.direction }); } scrollToAnchor(step) { if (step.disableScrollToAnchor) { return Promise.resolve(); } const anchor = this.anchors[step?.anchorId], htmlElement = anchor.element.nativeElement; return this.scrollingService.ensureVisible(htmlElement, { center: step.centerAnchorOnScroll, smoothScroll: step.smoothScroll, scrollContainer: step.scrollContainer }); } toggleBackdrop(step) { const anchor = this.anchors[step?.anchorId]; if (step.enableBackdrop) { this.backdrop.show(anchor.element, step); } else { this.backdrop.close(); } } togglePageScrolling(step) { if (step.disablePageScrolling) { this.scrollBlockingService.enable(step.scrollContainer); } else { this.scrollBlockingService.disable(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: TourService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: TourService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: TourService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class TourHotkeyListenerComponent { constructor() { this.tourService = inject(TourService); } /** * Configures hot keys for controlling the tour with the keyboard */ onEscapeKey() { if (this.tourService.getStatus() === TourState.ON && this.tourService.isHotkeysEnabled()) { this.tourService.end(); } } onArrowRightKey() { const step = this.tourService.currentStep; if (this.tourService.getStatus() === TourState.ON && this.tourService.hasNext(this.tourService.currentStep) && this.tourService.isHotkeysEnabled() && !step?.nextOnAnchorClick) { this.tourService.next(); } } onArrowLeftKey() { if (this.tourService.getStatus() === TourState.ON && this.tourService.hasPrev(this.tourService.currentStep) && this.tourService.isHotkeysEnabled()) { this.tourService.prev(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: TourHotkeyListenerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.0.5", type: TourHotkeyListenerComponent, isStandalone: true, selector: "tour-hotkey-listener", host: { listeners: { "window:keydown.Escape": "onEscapeKey()", "window:keydown.ArrowRight": "onArrowRightKey()", "window:keydown.ArrowLeft": "onArrowLeftKey()" } }, ngImport: i0, template: `<ng-content></ng-content>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: TourHotkeyListenerComponent, decorators: [{ type: Component, args: [{ selector: 'tour-hotkey-listener', template: `<ng-content></ng-content>`, changeDetection: ChangeDetectionStrategy.OnPush }] }], propDecorators: { onEscapeKey: [{ type: HostListener, args: ['window:keydown.Escape'] }], onArrowRightKey: [{ type: HostListener, args: ['window:keydown.ArrowRight'] }], onArrowLeftKey: [{ type: HostListener, args: ['window:keydown.ArrowLeft'] }] } }); class BaseTourProxyAnchor { constructor() { this.document = inject(DOCUMENT); afterNextRender(() => this.setAnchorElement(), { phase: AfterRenderPhase.Read }); } setAnchorElement() { if (this.anchorEl instanceof HTMLElement) { this.anchorDirective.element = new ElementRef(this.anchorEl); return; } const htmlElement = this.document.querySelector(this.anchorEl); if (!htmlElement) { throw new Error(`Element with "${this.anchorEl}" CSS selector could not be found!`); } this.anchorDirective.element = new ElementRef(htmlElement); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: BaseTourProxyAnchor, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.5", type: BaseTourProxyAnchor, isStandalone: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: BaseTourProxyAnchor, decorators: [{ type: Directive }], ctorParameters: () => [] }); /** * Generated bundle index. Do not edit. */ export { BaseTourProxyAnchor, Direction, TourHotkeyListenerComponent, TourService, TourState, isCovered, isInViewport }; //# sourceMappingURL=ngx-ui-tour-core.mjs.map