UNPKG

ngx-joyride

Version:

[![npm version](https://badge.fury.io/js/ngx-joyride.svg)](https://badge.fury.io/js/ngx-joyride) [![Build Status](https://travis-ci.org/tnicola/ngx-joyride.svg?branch=master)](https://travis-ci.org/tnicola/ngx-joyride) [![codecov](https://codecov.io/gh/

1,224 lines (1,211 loc) 72.4 kB
import { Injectable, Inject, PLATFORM_ID, EventEmitter, Directive, ViewContainerRef, Input, Output, RendererFactory2, Component, ViewEncapsulation, Injector, Renderer2, ViewChild, HostListener, ComponentFactoryResolver, ApplicationRef, NgModule } from '@angular/core'; import { isPlatformBrowser, CommonModule } from '@angular/common'; import { ReplaySubject, of, Observable, Subject } from 'rxjs'; import { Router, RouterModule } from '@angular/router'; import { finalize } from 'rxjs/operators'; class JoyrideStep { constructor() { this.title = new ReplaySubject(); this.text = new ReplaySubject(); } } const DEFAULT_THEME_COLOR = '#3b5560'; const STEP_DEFAULT_POSITION = 'bottom'; const DEFAULT_TIMEOUT_BETWEEN_STEPS = 1; class ObservableCustomTexts { } const DEFAULT_TEXTS = { prev: of('prev'), next: of('next'), done: of('done'), close: of(null) }; class JoyrideOptionsService { constructor() { this.themeColor = DEFAULT_THEME_COLOR; this.stepDefaultPosition = STEP_DEFAULT_POSITION; this.logsEnabled = false; this.showCounter = true; this.showPrevButton = true; this.stepsOrder = []; } setOptions(options) { this.stepsOrder = options.steps; this.stepDefaultPosition = options.stepDefaultPosition ? options.stepDefaultPosition : this.stepDefaultPosition; this.logsEnabled = typeof options.logsEnabled !== 'undefined' ? options.logsEnabled : this.logsEnabled; this.showCounter = typeof options.showCounter !== 'undefined' ? options.showCounter : this.showCounter; this.showPrevButton = typeof options.showPrevButton !== 'undefined' ? options.showPrevButton : this.showPrevButton; this.themeColor = options.themeColor ? options.themeColor : this.themeColor; this.firstStep = options.startWith; this.waitingTime = typeof options.waitingTime !== 'undefined' ? options.waitingTime : DEFAULT_TIMEOUT_BETWEEN_STEPS; typeof options.customTexts !== 'undefined' ? this.setCustomText(options.customTexts) : this.setCustomText(DEFAULT_TEXTS); } getBackdropColor() { return this.hexToRgb(this.themeColor); } getThemeColor() { return this.themeColor; } getStepDefaultPosition() { return this.stepDefaultPosition; } getStepsOrder() { return this.stepsOrder; } getFirstStep() { return this.firstStep; } getWaitingTime() { return this.waitingTime; } areLogsEnabled() { return this.logsEnabled; } isCounterVisible() { return this.showCounter; } isPrevButtonVisible() { return this.showPrevButton; } getCustomTexts() { return this.customTexts; } setCustomText(texts) { let prev; let next; let done; let close; prev = texts.prev ? texts.prev : DEFAULT_TEXTS.prev; next = texts.next ? texts.next : DEFAULT_TEXTS.next; done = texts.done ? texts.done : DEFAULT_TEXTS.done; close = texts.close ? texts.close : DEFAULT_TEXTS.close; this.customTexts = { prev: this.toObservable(prev), next: this.toObservable(next), done: this.toObservable(done), close: this.toObservable(close) }; } toObservable(value) { return value instanceof Observable ? value : of(value); } hexToRgb(hex) { const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, (m, r, g, b) => { return r + r + g + g + b + b; }); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}` : null; } } JoyrideOptionsService.decorators = [ { type: Injectable } ]; const JOYRIDE = 'ngx-joyride:::'; class LoggerService { constructor(optionService) { this.optionService = optionService; } debug(message, data = "") { if (this.optionService.areLogsEnabled()) { console.debug(JOYRIDE + message, data); } } info(message, data = "") { if (this.optionService.areLogsEnabled()) { console.info(JOYRIDE + message, data); } } warn(message, data = "") { if (this.optionService.areLogsEnabled()) { console.warn(JOYRIDE + message, data); } } error(message, data = "") { if (this.optionService.areLogsEnabled()) { console.error(JOYRIDE + message, data); } } } LoggerService.decorators = [ { type: Injectable } ]; LoggerService.ctorParameters = () => [ { type: JoyrideOptionsService } ]; class JoyrideError extends Error { constructor(message) { super(message); Object.setPrototypeOf(this, JoyrideError.prototype); } } class JoyrideStepDoesNotExist extends Error { constructor(message) { super(message); Object.setPrototypeOf(this, JoyrideStepDoesNotExist.prototype); } } class JoyrideStepOutOfRange extends Error { constructor(message) { super(message); Object.setPrototypeOf(this, JoyrideStepOutOfRange.prototype); } } const ROUTE_SEPARATOR = '@'; class Step { } var StepActionType; (function (StepActionType) { StepActionType["NEXT"] = "NEXT"; StepActionType["PREV"] = "PREV"; })(StepActionType || (StepActionType = {})); class JoyrideStepsContainerService { constructor(stepOptions, logger) { this.stepOptions = stepOptions; this.logger = logger; this.tempSteps = []; this.currentStepIndex = -2; this.stepHasBeenModified = new Subject(); } getFirstStepIndex() { const firstStep = this.stepOptions.getFirstStep(); const stepIds = this.stepOptions.getStepsOrder(); let index = stepIds.indexOf(firstStep); if (index < 0) { index = 0; if (firstStep !== undefined) this.logger.warn(`The step ${firstStep} does not exist. Check in your step list if it's present.`); } return index; } init() { this.logger.info('Initializing the steps array.'); this.steps = []; this.currentStepIndex = this.getFirstStepIndex() - 1; let stepIds = this.stepOptions.getStepsOrder(); stepIds.forEach(stepId => this.steps.push({ id: stepId, step: null })); } addStep(stepToAdd) { let stepExist = this.tempSteps.filter(step => step.name === stepToAdd.name).length > 0; if (!stepExist) { this.logger.info(`Adding step ${stepToAdd.name} to the steps list.`); this.tempSteps.push(stepToAdd); } else { let stepIndexToReplace = this.tempSteps.findIndex(step => step.name === stepToAdd.name); this.tempSteps[stepIndexToReplace] = stepToAdd; } } get(action) { if (action === StepActionType.NEXT) this.currentStepIndex++; else this.currentStepIndex--; if (this.currentStepIndex < 0 || this.currentStepIndex >= this.steps.length) throw new JoyrideStepOutOfRange('The first or last step of the tour cannot be found!'); const stepName = this.getStepName(this.steps[this.currentStepIndex].id); const index = this.tempSteps.findIndex(step => step.name === stepName); let stepFound = this.tempSteps[index]; this.steps[this.currentStepIndex].step = stepFound; if (stepFound == null) { this.logger.warn(`Step ${this.steps[this.currentStepIndex].id} not found in the DOM. Check if it's hidden by *ngIf directive.`); } return stepFound; } getStepRoute(action) { let stepID; if (action === StepActionType.NEXT) { stepID = this.steps[this.currentStepIndex + 1] ? this.steps[this.currentStepIndex + 1].id : null; } else { stepID = this.steps[this.currentStepIndex - 1] ? this.steps[this.currentStepIndex - 1].id : null; } let stepRoute = stepID && stepID.includes(ROUTE_SEPARATOR) ? stepID.split(ROUTE_SEPARATOR)[1] : ''; return stepRoute; } updatePosition(stepName, position) { let index = this.getStepIndex(stepName); if (this.steps[index].step) { this.steps[index].step.position = position; this.stepHasBeenModified.next(this.steps[index].step); } else { this.logger.warn(`Trying to modify the position of ${stepName} to ${position}. Step not found!Is this step located in a different route?`); } } getStepNumber(stepName) { return this.getStepIndex(stepName) + 1; } getStepsCount() { let stepsOrder = this.stepOptions.getStepsOrder(); return stepsOrder.length; } getStepIndex(stepName) { const index = this.steps .map(step => (step.id.includes(ROUTE_SEPARATOR) ? step.id.split(ROUTE_SEPARATOR)[0] : step.id)) .findIndex(name => stepName === name); if (index === -1) throw new JoyrideError(`The step with name: ${stepName} does not exist in the step list.`); return index; } getStepName(stepID) { let stepName = stepID && stepID.includes(ROUTE_SEPARATOR) ? stepID.split(ROUTE_SEPARATOR)[0] : stepID; return stepName; } } JoyrideStepsContainerService.decorators = [ { type: Injectable } ]; JoyrideStepsContainerService.ctorParameters = () => [ { type: JoyrideOptionsService }, { type: LoggerService } ]; class DomRefService { constructor(platformId) { this.platformId = platformId; this.fakeDocument = { body: {}, documentElement: {} }; this.fakeWindow = { document: this.fakeDocument, navigator: {} }; } getNativeWindow() { if (isPlatformBrowser(this.platformId)) return window; else return this.fakeWindow; } getNativeDocument() { if (isPlatformBrowser(this.platformId)) return document; else return this.fakeDocument; } } DomRefService.decorators = [ { type: Injectable } ]; DomRefService.ctorParameters = () => [ { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] } ]; class TemplatesService { setPrevButton(template) { this._prevButton = template; } getPrevButton() { return this._prevButton; } setNextButton(template) { this._nextButton = template; } getNextButton() { return this._nextButton; } setDoneButton(template) { this._doneButton = template; } getDoneButton() { return this._doneButton; } setCounter(template) { this._counter = template; } getCounter() { return this._counter; } } TemplatesService.decorators = [ { type: Injectable } ]; const NO_POSITION = 'NO_POSITION'; class JoyrideDirective { constructor(joyrideStepsContainer, viewContainerRef, domService, router, templateService, platformId) { this.joyrideStepsContainer = joyrideStepsContainer; this.viewContainerRef = viewContainerRef; this.domService = domService; this.router = router; this.templateService = templateService; this.platformId = platformId; this.stepPosition = NO_POSITION; this.prev = new EventEmitter(); this.next = new EventEmitter(); this.done = new EventEmitter(); this.subscriptions = []; this.windowRef = this.domService.getNativeWindow(); this.step = new JoyrideStep(); } ngAfterViewInit() { if (!isPlatformBrowser(this.platformId)) return; if (this.prevTemplate) this.templateService.setPrevButton(this.prevTemplate); if (this.nextTemplate) this.templateService.setNextButton(this.nextTemplate); if (this.doneTemplate) this.templateService.setDoneButton(this.doneTemplate); if (this.counterTemplate) this.templateService.setCounter(this.counterTemplate); this.step.position = this.stepPosition; this.step.targetViewContainer = this.viewContainerRef; this.setAsyncFields(this.step); this.step.stepContent = this.stepContent; this.step.stepContentParams = this.stepContentParams; this.step.nextClicked = this.next; this.step.prevCliked = this.prev; this.step.tourDone = this.done; if (!this.name) throw new JoyrideError("All the steps should have the 'joyrideStep' property set with a custom name."); this.step.name = this.name; this.step.route = this.router.url.substr(0, 1) === '/' ? this.router.url.substr(1) : this.router.url; this.step.transformCssStyle = this.windowRef.getComputedStyle(this.viewContainerRef.element.nativeElement).transform; this.step.isElementOrAncestorFixed = this.isElementFixed(this.viewContainerRef.element) || this.isAncestorsFixed(this.viewContainerRef.element.nativeElement.parentElement); this.joyrideStepsContainer.addStep(this.step); } ngOnChanges(changes) { if (changes['title'] || changes['text']) { this.setAsyncFields(this.step); } } isElementFixed(element) { return this.windowRef.getComputedStyle(element.nativeElement).position === 'fixed'; } setAsyncFields(step) { if (this.title instanceof Observable) { this.subscriptions.push(this.title.subscribe(title => { step.title.next(title); })); } else { step.title.next(this.title); } if (this.text instanceof Observable) { this.subscriptions.push(this.text.subscribe(text => { step.text.next(text); })); } else { step.text.next(this.text); } } isAncestorsFixed(nativeElement) { if (!nativeElement || !nativeElement.parentElement) return false; let isElementFixed = this.windowRef.getComputedStyle(nativeElement.parentElement).position === 'fixed'; if (nativeElement.nodeName === 'BODY') { return isElementFixed; } if (isElementFixed) return true; else return this.isAncestorsFixed(nativeElement.parentElement); } ngOnDestroy() { this.subscriptions.forEach(sub => { sub.unsubscribe(); }); } } JoyrideDirective.decorators = [ { type: Directive, args: [{ selector: 'joyrideStep, [joyrideStep]' },] } ]; JoyrideDirective.ctorParameters = () => [ { type: JoyrideStepsContainerService }, { type: ViewContainerRef }, { type: DomRefService }, { type: Router }, { type: TemplatesService }, { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] } ]; JoyrideDirective.propDecorators = { name: [{ type: Input, args: ['joyrideStep',] }], nextStep: [{ type: Input }], title: [{ type: Input }], text: [{ type: Input }], stepPosition: [{ type: Input }], stepContent: [{ type: Input }], stepContentParams: [{ type: Input }], prevTemplate: [{ type: Input }], nextTemplate: [{ type: Input }], doneTemplate: [{ type: Input }], counterTemplate: [{ type: Input }], prev: [{ type: Output }], next: [{ type: Output }], done: [{ type: Output }] }; class DocumentService { constructor(DOMService, platformId) { this.DOMService = DOMService; if (!isPlatformBrowser(platformId)) { return; } this.setDocumentHeight(); var doc = DOMService.getNativeDocument(); if (doc && !doc.elementsFromPoint) { // IE 11 - Edge browsers doc.elementsFromPoint = this.elementsFromPoint.bind(this); } } getElementFixedTop(elementRef) { return elementRef.nativeElement.getBoundingClientRect().top; } getElementFixedLeft(elementRef) { return elementRef.nativeElement.getBoundingClientRect().left; } getElementAbsoluteTop(elementRef) { const scrollOffsets = this.getScrollOffsets(); return (elementRef.nativeElement.getBoundingClientRect().top + scrollOffsets.y); } getElementAbsoluteLeft(elementRef) { const scrollOffsets = this.getScrollOffsets(); return (elementRef.nativeElement.getBoundingClientRect().left + scrollOffsets.x); } setDocumentHeight() { this.documentHeight = this.calculateDocumentHeight(); } getDocumentHeight() { return this.documentHeight; } isParentScrollable(elementRef) { return (this.getFirstScrollableParent(elementRef.nativeElement) !== this.DOMService.getNativeDocument().body); } isElementBeyondOthers(elementRef, isElementFixed, keywordToDiscard) { const x1 = isElementFixed ? this.getElementFixedLeft(elementRef) : this.getElementAbsoluteLeft(elementRef); const y1 = isElementFixed ? this.getElementFixedTop(elementRef) : this.getElementAbsoluteTop(elementRef); const x2 = x1 + elementRef.nativeElement.getBoundingClientRect().width - 1; const y2 = y1 + elementRef.nativeElement.getBoundingClientRect().height - 1; const elements1 = this.DOMService.getNativeDocument().elementsFromPoint(x1, y1); const elements2 = this.DOMService.getNativeDocument().elementsFromPoint(x2, y2); if (elements1.length === 0 && elements2.length === 0) return 1; if (this.getFirstElementWithoutKeyword(elements1, keywordToDiscard) !== elementRef.nativeElement || this.getFirstElementWithoutKeyword(elements2, keywordToDiscard) !== elementRef.nativeElement) { return 2; } return 3; } scrollIntoView(elementRef, isElementFixed) { const firstScrollableParent = this.getFirstScrollableParent(elementRef.nativeElement); const top = isElementFixed ? this.getElementFixedTop(elementRef) : this.getElementAbsoluteTop(elementRef); if (firstScrollableParent !== this.DOMService.getNativeDocument().body) { if (firstScrollableParent.scrollTo) { firstScrollableParent.scrollTo(0, top - 150); } else { // IE 11 - Edge browsers firstScrollableParent.scrollTop = top - 150; } } else { this.DOMService.getNativeWindow().scrollTo(0, top - 150); } } scrollToTheTop(elementRef) { const firstScrollableParent = this.getFirstScrollableParent(elementRef.nativeElement); if (firstScrollableParent !== this.DOMService.getNativeDocument().body) { if (firstScrollableParent.scrollTo) { firstScrollableParent.scrollTo(0, 0); } else { // IE 11 - Edge browsers firstScrollableParent.scrollTop = 0; } } else { this.DOMService.getNativeWindow().scrollTo(0, 0); } } scrollToTheBottom(elementRef) { const firstScrollableParent = this.getFirstScrollableParent(elementRef.nativeElement); if (firstScrollableParent !== this.DOMService.getNativeDocument().body) { if (firstScrollableParent.scrollTo) { firstScrollableParent.scrollTo(0, this.DOMService.getNativeDocument().body.scrollHeight); } else { // IE 11 - Edge browsers firstScrollableParent.scrollTop = firstScrollableParent.scrollHeight - firstScrollableParent.clientHeight; } } else { this.DOMService.getNativeWindow().scrollTo(0, this.DOMService.getNativeDocument().body.scrollHeight); } } getFirstScrollableParent(node) { const regex = /(auto|scroll|overlay)/; const style = (node, prop) => this.DOMService.getNativeWindow() .getComputedStyle(node, null) .getPropertyValue(prop); const scroll = (node) => regex.test(style(node, 'overflow') + style(node, 'overflow-y') + style(node, 'overflow-x')); const scrollparent = (node) => { return !node || node === this.DOMService.getNativeDocument().body ? this.DOMService.getNativeDocument().body : scroll(node) ? node : scrollparent(node.parentNode); }; return scrollparent(node); } calculateDocumentHeight() { const documentRef = this.DOMService.getNativeDocument(); return Math.max(documentRef.body.scrollHeight, documentRef.documentElement.scrollHeight, documentRef.body.offsetHeight, documentRef.documentElement.offsetHeight, documentRef.body.clientHeight, documentRef.documentElement.clientHeight); } getScrollOffsets() { const winReference = this.DOMService.getNativeWindow(); const docReference = this.DOMService.getNativeDocument(); // This works for all browsers except IE versions 8 and before if (winReference.pageXOffset != null) return { x: winReference.pageXOffset, y: winReference.pageYOffset }; // For IE (or any browser) in Standards mode if (docReference.compatMode == 'CSS1Compat') return { x: docReference.documentElement.scrollLeft, y: docReference.documentElement.scrollTop }; // For browsers in Quirks mode return { x: docReference.body.scrollLeft, y: docReference.body.scrollTop }; } elementsFromPoint(x, y) { var parents = []; var parent = void 0; do { const elem = this.DOMService.getNativeDocument().elementFromPoint(x, y); if (elem && parent !== elem) { parent = elem; parents.push(parent); parent.style.pointerEvents = 'none'; } else { parent = false; } } while (parent); parents.forEach(function (parent) { return (parent.style.pointerEvents = 'all'); }); return parents; } getFirstElementWithoutKeyword(elements, keyword) { while (elements[0] && elements[0].classList.toString().includes(keyword)) { elements.shift(); } return elements[0]; } } DocumentService.decorators = [ { type: Injectable } ]; DocumentService.ctorParameters = () => [ { type: DomRefService }, { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] } ]; class JoyrideBackdropService { constructor(documentService, optionsService, rendererFactory) { this.documentService = documentService; this.optionsService = optionsService; this.rendererFactory = rendererFactory; this.lastXScroll = 0; this.lastYScroll = 0; this.setRenderer(); } setRenderer() { this.renderer = this.rendererFactory.createRenderer(null, null); } draw(step) { this.elementRef = step.targetViewContainer; this.targetAbsoluteTop = this.getTargetTotalTop(step); this.targetAbsoluteLeft = this.getTargetTotalLeft(step); this.currentBackdropContainer = this.renderer.createElement('div'); this.renderer.addClass(this.currentBackdropContainer, 'backdrop-container'); this.renderer.setStyle(this.currentBackdropContainer, 'position', 'fixed'); this.renderer.setStyle(this.currentBackdropContainer, 'top', '0px'); this.renderer.setStyle(this.currentBackdropContainer, 'left', '0px'); this.renderer.setStyle(this.currentBackdropContainer, 'width', '100%'); this.renderer.setStyle(this.currentBackdropContainer, 'height', '100%'); this.renderer.setStyle(this.currentBackdropContainer, 'z-index', '1000'); this.renderer.setAttribute(this.currentBackdropContainer, 'id', 'backdrop-' + step.name); this.backdropContent = this.renderer.createElement('div'); this.renderer.addClass(this.backdropContent, 'backdrop-content'); this.renderer.setStyle(this.backdropContent, 'position', 'relative'); this.renderer.setStyle(this.backdropContent, 'height', '100%'); this.renderer.setStyle(this.backdropContent, 'display', 'flex'); this.renderer.setStyle(this.backdropContent, 'flex-direction', 'column'); this.renderer.appendChild(this.currentBackdropContainer, this.backdropContent); this.backdropTop = this.renderer.createElement('div'); this.renderer.addClass(this.backdropTop, 'joyride-backdrop'); this.renderer.addClass(this.backdropTop, 'backdrop-top'); this.renderer.setStyle(this.backdropTop, 'width', '100%'); this.renderer.setStyle(this.backdropTop, 'height', this.targetAbsoluteTop - this.lastYScroll + 'px'); this.renderer.setStyle(this.backdropTop, 'flex-shrink', '0'); this.renderer.setStyle(this.backdropTop, 'background-color', `rgba(${this.optionsService.getBackdropColor()}, 0.7)`); this.renderer.appendChild(this.backdropContent, this.backdropTop); this.backdropMiddleContainer = this.renderer.createElement('div'); this.renderer.addClass(this.backdropMiddleContainer, 'backdrop-middle-container'); this.renderer.setStyle(this.backdropMiddleContainer, 'height', this.elementRef.element.nativeElement.offsetHeight + 'px'); this.renderer.setStyle(this.backdropMiddleContainer, 'width', '100%'); this.renderer.setStyle(this.backdropMiddleContainer, 'flex-shrink', '0'); this.renderer.appendChild(this.backdropContent, this.backdropMiddleContainer); this.backdropMiddleContent = this.renderer.createElement('div'); this.renderer.addClass(this.backdropMiddleContent, 'backdrop-middle-content'); this.renderer.setStyle(this.backdropMiddleContent, 'display', 'flex'); this.renderer.setStyle(this.backdropMiddleContent, 'width', '100%'); this.renderer.setStyle(this.backdropMiddleContent, 'height', '100%'); this.renderer.appendChild(this.backdropMiddleContainer, this.backdropMiddleContent); this.leftBackdrop = this.renderer.createElement('div'); this.renderer.addClass(this.leftBackdrop, 'joyride-backdrop'); this.renderer.addClass(this.leftBackdrop, 'backdrop-left'); this.renderer.setStyle(this.leftBackdrop, 'flex-shrink', '0'); this.renderer.setStyle(this.leftBackdrop, 'width', this.targetAbsoluteLeft - this.lastXScroll + 'px'); this.renderer.setStyle(this.leftBackdrop, 'background-color', `rgba(${this.optionsService.getBackdropColor()}, 0.7)`); this.renderer.appendChild(this.backdropMiddleContent, this.leftBackdrop); this.targetBackdrop = this.renderer.createElement('div'); this.renderer.addClass(this.targetBackdrop, 'backdrop-target'); this.renderer.setStyle(this.targetBackdrop, 'flex-shrink', '0'); this.renderer.setStyle(this.targetBackdrop, 'width', this.elementRef.element.nativeElement.offsetWidth + 'px'); this.renderer.appendChild(this.backdropMiddleContent, this.targetBackdrop); this.rightBackdrop = this.renderer.createElement('div'); this.renderer.addClass(this.rightBackdrop, 'joyride-backdrop'); this.renderer.addClass(this.rightBackdrop, 'backdrop-right'); this.renderer.setStyle(this.rightBackdrop, 'width', '100%'); this.renderer.setStyle(this.rightBackdrop, 'background-color', `rgba(${this.optionsService.getBackdropColor()}, 0.7)`); this.renderer.appendChild(this.backdropMiddleContent, this.rightBackdrop); this.backdropBottom = this.renderer.createElement('div'); this.renderer.addClass(this.backdropBottom, 'joyride-backdrop'); this.renderer.addClass(this.backdropBottom, 'backdrop-bottom'); this.renderer.setStyle(this.backdropBottom, 'width', '100%'); this.renderer.setStyle(this.backdropBottom, 'height', '100%'); this.renderer.setStyle(this.backdropBottom, 'background-color', `rgba(${this.optionsService.getBackdropColor()}, 0.7)`); this.renderer.appendChild(this.backdropContent, this.backdropBottom); this.removeLastBackdrop(); this.drawCurrentBackdrop(); this.lastBackdropContainer = this.currentBackdropContainer; } remove() { this.removeLastBackdrop(); } redrawTarget(step) { this.targetAbsoluteLeft = this.getTargetTotalLeft(step); this.targetAbsoluteTop = this.getTargetTotalTop(step); this.handleVerticalScroll(step); this.handleHorizontalScroll(step); } getTargetTotalTop(step) { let targetVC = step.targetViewContainer; return step.isElementOrAncestorFixed ? this.documentService.getElementFixedTop(targetVC.element) : this.documentService.getElementAbsoluteTop(targetVC.element); } getTargetTotalLeft(step) { let targetVC = step.targetViewContainer; return step.isElementOrAncestorFixed ? this.documentService.getElementFixedLeft(targetVC.element) : this.documentService.getElementAbsoluteLeft(targetVC.element); } redraw(step, scroll) { if (this.lastYScroll !== scroll.scrollY) { this.lastYScroll = scroll.scrollY; if (this.elementRef) { this.handleVerticalScroll(step); } } if (this.lastXScroll !== scroll.scrollX) { this.lastXScroll = scroll.scrollX; if (this.elementRef) { this.handleHorizontalScroll(step); } } } handleHorizontalScroll(step) { let newBackdropLeftWidth = step.isElementOrAncestorFixed ? this.targetAbsoluteLeft : this.targetAbsoluteLeft - this.lastXScroll; if (newBackdropLeftWidth >= 0) { this.renderer.setStyle(this.leftBackdrop, 'width', newBackdropLeftWidth + 'px'); this.renderer.setStyle(this.targetBackdrop, 'width', this.elementRef.element.nativeElement.offsetWidth + 'px'); } else { this.handleTargetPartialWidth(newBackdropLeftWidth); } } handleTargetPartialWidth(newBackdropLeftWidth) { this.renderer.setStyle(this.leftBackdrop, 'width', 0 + 'px'); let visibleTargetWidth = this.elementRef.element.nativeElement.offsetWidth + newBackdropLeftWidth; if (visibleTargetWidth >= 0) { this.renderer.setStyle(this.targetBackdrop, 'width', visibleTargetWidth + 'px'); } else { this.renderer.setStyle(this.targetBackdrop, 'width', 0 + 'px'); } } handleVerticalScroll(step) { let newBackdropTopHeight = step.isElementOrAncestorFixed ? this.targetAbsoluteTop : this.targetAbsoluteTop - this.lastYScroll; if (newBackdropTopHeight >= 0) { this.renderer.setStyle(this.backdropTop, 'height', newBackdropTopHeight + 'px'); this.renderer.setStyle(this.backdropMiddleContainer, 'height', this.elementRef.element.nativeElement.offsetHeight + 'px'); } else { this.handleTargetPartialHeight(newBackdropTopHeight); } } handleTargetPartialHeight(newBackdropTopHeight) { this.renderer.setStyle(this.backdropTop, 'height', 0 + 'px'); let visibleTargetHeight = this.elementRef.element.nativeElement.offsetHeight + newBackdropTopHeight; if (visibleTargetHeight >= 0) { this.renderer.setStyle(this.backdropMiddleContainer, 'height', visibleTargetHeight + 'px'); } else { this.renderer.setStyle(this.backdropMiddleContainer, 'height', 0 + 'px'); } } removeLastBackdrop() { if (this.lastBackdropContainer) { this.renderer.removeChild(document.body, this.lastBackdropContainer); this.lastBackdropContainer = undefined; } } drawCurrentBackdrop() { this.renderer.appendChild(document.body, this.currentBackdropContainer); } } JoyrideBackdropService.decorators = [ { type: Injectable } ]; JoyrideBackdropService.ctorParameters = () => [ { type: DocumentService }, { type: JoyrideOptionsService }, { type: RendererFactory2 } ]; class Scroll { } class EventListenerService { constructor(rendererFactory, DOMService) { this.rendererFactory = rendererFactory; this.DOMService = DOMService; this.scrollEvent = new Subject(); this.resizeEvent = new Subject(); this.renderer = rendererFactory.createRenderer(null, null); } startListeningScrollEvents() { this.scrollUnlisten = this.renderer.listen('document', 'scroll', evt => { this.scrollEvent.next({ scrollX: this.DOMService.getNativeWindow().pageXOffset, scrollY: this.DOMService.getNativeWindow().pageYOffset }); }); } startListeningResizeEvents() { this.resizeUnlisten = this.renderer.listen('window', 'resize', evt => { this.resizeEvent.next(evt); }); } stopListeningScrollEvents() { this.scrollUnlisten(); } stopListeningResizeEvents() { this.resizeUnlisten(); } } EventListenerService.decorators = [ { type: Injectable } ]; EventListenerService.ctorParameters = () => [ { type: RendererFactory2 }, { type: DomRefService } ]; const STEP_MIN_WIDTH = 200; const STEP_MAX_WIDTH = 400; const CUSTOM_STEP_MAX_WIDTH_VW = 90; const STEP_HEIGHT = 200; const ASPECT_RATIO = 1.212; const DEFAULT_DISTANCE_FROM_MARGIN_TOP = 2; const DEFAULT_DISTANCE_FROM_MARGIN_LEFT = 2; const DEFAULT_DISTANCE_FROM_MARGIN_BOTTOM = 5; const DEFAULT_DISTANCE_FROM_MARGIN_RIGHT = 5; var KEY_CODE; (function (KEY_CODE) { KEY_CODE[KEY_CODE["RIGHT_ARROW"] = 39] = "RIGHT_ARROW"; KEY_CODE[KEY_CODE["LEFT_ARROW"] = 37] = "LEFT_ARROW"; KEY_CODE[KEY_CODE["ESCAPE_KEY"] = 27] = "ESCAPE_KEY"; })(KEY_CODE || (KEY_CODE = {})); class JoyrideStepComponent { constructor(injector, stepsContainerService, eventListenerService, documentService, renderer, logger, optionsService, templateService) { this.injector = injector; this.stepsContainerService = stepsContainerService; this.eventListenerService = eventListenerService; this.documentService = documentService; this.renderer = renderer; this.logger = logger; this.optionsService = optionsService; this.templateService = templateService; this.stepWidth = STEP_MIN_WIDTH; this.stepHeight = STEP_HEIGHT; this.showArrow = true; this.arrowSize = ARROW_SIZE; this.subscriptions = []; } ngOnInit() { // Need to Inject here otherwise you will obtain a circular dependency this.joyrideStepService = this.injector.get(JoyrideStepService); this.documentHeight = this.documentService.getDocumentHeight(); this.subscriptions.push(this.subscribeToResizeEvents()); this.title = this.step.title.asObservable(); this.text = this.step.text.asObservable(); this.setCustomTemplates(); this.setCustomTexts(); this.counter = this.getCounter(); this.isCounterVisible = this.optionsService.isCounterVisible(); this.isPrevButtonVisible = this.optionsService.isPrevButtonVisible(); this.themeColor = this.optionsService.getThemeColor(); if (this.text) this.text.subscribe(val => this.checkRedraw(val)); if (this.title) this.title.subscribe(val => this.checkRedraw(val)); } ngAfterViewInit() { if (this.isCustomized()) { this.renderer.setStyle(this.stepContainer.nativeElement, 'max-width', CUSTOM_STEP_MAX_WIDTH_VW + 'vw'); this.updateStepDimensions(); } else { this.renderer.setStyle(this.stepContainer.nativeElement, 'max-width', STEP_MAX_WIDTH + 'px'); let dimensions = this.getDimensionsByAspectRatio(this.stepContainer.nativeElement.clientWidth, this.stepContainer.nativeElement.clientHeight, ASPECT_RATIO); dimensions = this.adjustDimensions(dimensions.width, dimensions.height); this.stepWidth = dimensions.width; this.stepHeight = dimensions.height; this.renderer.setStyle(this.stepContainer.nativeElement, 'width', this.stepWidth + 'px'); this.renderer.setStyle(this.stepContainer.nativeElement, 'height', this.stepHeight + 'px'); } this.drawStep(); } checkRedraw(val) { if (val != null) { // Need to wait that the change is rendered before redrawing setTimeout(() => { this.redrawStep(); }, 2); } } isCustomized() { return (this.step.stepContent || this.templateService.getCounter() || this.templateService.getPrevButton() || this.templateService.getNextButton() || this.templateService.getDoneButton()); } setCustomTexts() { const customeTexts = this.optionsService.getCustomTexts(); this.prevText = customeTexts.prev; this.nextText = customeTexts.next; this.doneText = customeTexts.done; } drawStep() { let position = this.step.isElementOrAncestorFixed ? 'fixed' : 'absolute'; this.renderer.setStyle(this.stepHolder.nativeElement, 'position', position); this.renderer.setStyle(this.stepHolder.nativeElement, 'transform', this.step.transformCssStyle); this.targetWidth = this.step.targetViewContainer.element.nativeElement.getBoundingClientRect().width; this.targetHeight = this.step.targetViewContainer.element.nativeElement.getBoundingClientRect().height; this.targetAbsoluteLeft = position === 'fixed' ? this.documentService.getElementFixedLeft(this.step.targetViewContainer.element) : this.documentService.getElementAbsoluteLeft(this.step.targetViewContainer.element); this.targetAbsoluteTop = position === 'fixed' ? this.documentService.getElementFixedTop(this.step.targetViewContainer.element) : this.documentService.getElementAbsoluteTop(this.step.targetViewContainer.element); this.setStepStyle(); } getCounter() { let stepPosition = this.stepsContainerService.getStepNumber(this.step.name); let numberOfSteps = this.stepsContainerService.getStepsCount(); this.counterData = { step: stepPosition, total: numberOfSteps }; return stepPosition + '/' + numberOfSteps; } setCustomTemplates() { this.customContent = this.step.stepContent; this.ctx = this.step.stepContentParams; this.customPrevButton = this.templateService.getPrevButton(); this.customNextButton = this.templateService.getNextButton(); this.customDoneButton = this.templateService.getDoneButton(); this.customCounter = this.templateService.getCounter(); } keyEvent(event) { console.log(event); if (event.keyCode === KEY_CODE.RIGHT_ARROW) { if (this.isLastStep()) { this.close(); } else { this.next(); } } else if (event.keyCode === KEY_CODE.LEFT_ARROW) { this.prev(); } else if (event.keyCode === KEY_CODE.ESCAPE_KEY) { this.close(); } } prev() { this.joyrideStepService.prev(); } next() { this.joyrideStepService.next(); } close() { this.joyrideStepService.close(); } isFirstStep() { return this.stepsContainerService.getStepNumber(this.step.name) === 1; } isLastStep() { return (this.stepsContainerService.getStepNumber(this.step.name) === this.stepsContainerService.getStepsCount()); } setStepStyle() { switch (this.step.position) { case 'top': { this.setStyleTop(); break; } case 'bottom': { this.setStyleBottom(); break; } case 'right': { this.setStyleRight(); break; } case 'left': { this.setStyleLeft(); break; } case 'center': { this.setStyleCenter(); break; } default: { this.setStyleBottom(); } } } setStyleTop() { this.stepsContainerService.updatePosition(this.step.name, 'top'); this.topPosition = this.targetAbsoluteTop - DISTANCE_FROM_TARGET - this.stepHeight; this.stepAbsoluteTop = this.targetAbsoluteTop - DISTANCE_FROM_TARGET - this.stepHeight; this.arrowTopPosition = this.stepHeight; this.leftPosition = this.targetWidth / 2 - this.stepWidth / 2 + this.targetAbsoluteLeft; this.stepAbsoluteLeft = this.targetWidth / 2 - this.stepWidth / 2 + this.targetAbsoluteLeft; this.arrowLeftPosition = this.stepWidth / 2 - this.arrowSize; this.adjustLeftPosition(); this.adjustRightPosition(); this.arrowPosition = 'bottom'; this.autofixTopPosition(); } setStyleRight() { this.stepsContainerService.updatePosition(this.step.name, 'right'); this.topPosition = this.targetAbsoluteTop + this.targetHeight / 2 - this.stepHeight / 2; this.stepAbsoluteTop = this.targetAbsoluteTop + this.targetHeight / 2 - this.stepHeight / 2; this.arrowTopPosition = this.stepHeight / 2 - this.arrowSize; this.leftPosition = this.targetAbsoluteLeft + this.targetWidth + DISTANCE_FROM_TARGET; this.stepAbsoluteLeft = this.targetAbsoluteLeft + this.targetWidth + DISTANCE_FROM_TARGET; this.arrowLeftPosition = -this.arrowSize; this.adjustTopPosition(); this.adjustBottomPosition(); this.arrowPosition = 'left'; this.autofixRightPosition(); } setStyleBottom() { this.stepsContainerService.updatePosition(this.step.name, 'bottom'); this.topPosition = this.targetAbsoluteTop + this.targetHeight + DISTANCE_FROM_TARGET; this.stepAbsoluteTop = this.targetAbsoluteTop + this.targetHeight + DISTANCE_FROM_TARGET; this.arrowTopPosition = -this.arrowSize; this.arrowLeftPosition = this.stepWidth / 2 - this.arrowSize; this.leftPosition = this.targetWidth / 2 - this.stepWidth / 2 + this.targetAbsoluteLeft; this.stepAbsoluteLeft = this.targetWidth / 2 - this.stepWidth / 2 + this.targetAbsoluteLeft; this.adjustLeftPosition(); this.adjustRightPosition(); this.arrowPosition = 'top'; this.autofixBottomPosition(); } setStyleLeft() { this.stepsContainerService.updatePosition(this.step.name, 'left'); this.topPosition = this.targetAbsoluteTop + this.targetHeight / 2 - this.stepHeight / 2; this.stepAbsoluteTop = this.targetAbsoluteTop + this.targetHeight / 2 - this.stepHeight / 2; this.arrowTopPosition = this.stepHeight / 2 - this.arrowSize; this.leftPosition = this.targetAbsoluteLeft - this.stepWidth - DISTANCE_FROM_TARGET; this.stepAbsoluteLeft = this.targetAbsoluteLeft - this.stepWidth - DISTANCE_FROM_TARGET; this.arrowLeftPosition = this.stepWidth; this.adjustTopPosition(); this.adjustBottomPosition(); this.arrowPosition = 'right'; this.autofixLeftPosition(); } setStyleCenter() { this.renderer.setStyle(this.stepHolder.nativeElement, 'position', 'fixed'); this.renderer.setStyle(this.stepHolder.nativeElement, 'top', '50%'); this.renderer.setStyle(this.stepHolder.nativeElement, 'left', '50%'); this.updateStepDimensions(); this.renderer.setStyle(this.stepHolder.nativeElement, 'transform', `translate(-${this.stepWidth / 2}px, -${this.stepHeight / 2}px)`); this.showArrow = false; } adjustLeftPosition() { if (this.leftPosition < 0) { this.arrowLeftPosition = this.arrowLeftPosition + this.leftPosition - DEFAULT_DISTANCE_FROM_MARGIN_LEFT; this.leftPosition = DEFAULT_DISTANCE_FROM_MARGIN_LEFT; } } adjustRightPosition() { let currentWindowWidth = document.body.clientWidth; if (this.stepAbsoluteLeft + this.stepWidth > currentWindowWidth) { let newLeftPos = this.leftPosition - (this.stepAbsoluteLeft + this.stepWidth + DEFAULT_DISTANCE_FROM_MARGIN_RIGHT - currentWindowWidth); let deltaLeftPosition = newLeftPos - this.leftPosition; this.leftPosition = newLeftPos; this.arrowLeftPosition = this.arrowLeftPosition - deltaLeftPosition; } } adjustTopPosition() { if (this.stepAbsoluteTop < 0) { this.arrowTopPosition = this.arrowTopPosition + this.topPosition - DEFAULT_DISTANCE_FROM_MARGIN_TOP; this.topPosition = DEFAULT_DISTANCE_FROM_MARGIN_TOP; } } adjustBottomPosition() { if (this.stepAbsoluteTop + this.stepHeight > this.documentHeight) { let newTopPos = this.topPosition - (this.stepAbsoluteTop + this.stepHeight + DEFAULT_DISTANCE_FROM_MARGIN_BOTTOM - this.documentHeight); let deltaTopPosition = newTopPos - this.topPosition; this.topPosition = newTopPos; this.arrowTopPosition = this.arrowTopPosition - deltaTopPosition; } } autofixTopPosition() { if (this.positionAlreadyFixed) { this.logger.warn('No step positions found for this step. The step will be centered.'); } else if (this.targetAbsoluteTop - this.stepHeight - this.arrowSize < 0) { this.positionAlreadyFixed = true; this.setStyleRight(); } } autofixRightPosition() { if (this.targetAbsoluteLeft + this.targetWidth + this.stepWidth + this.arrowSize > document.body.clientWidth) { this.setStyleBottom(); } } autofixBottomPosition() { if (this.targetAbsoluteTop + this.stepHeight + this.arrowSize + this.targetHeight > this.documentHeight) { this.setStyleLeft(); } } autofixLeftPosition() { if (this.targetAbsoluteLeft - this.stepWidth - this.arrowSize < 0) { this.setStyleTop(); } } subscribeToResizeEvents() { return this.eventListenerService.resizeEvent.subscribe(() => { this.redrawStep(); }); } redrawStep() { this.updateStepDimensions(); this.drawStep(); } getDimensionsByAspectRatio(width, height, aspectRatio) { let calcHeight = (width + height) / (1 + aspectRatio); let calcWidth = calcHeight * aspectRatio; return { width: calcWidth, height: calcHeight }; } adjustDimensions(width, height) { let area = width * height; let newWidth = width; let newHeight = height; if (width > STEP_MAX_WIDTH) { newWidth = STEP_MAX_WIDTH; newHeight = area / newWidth;