UNPKG

tour-navigator

Version:

Streamline website tours with Tour Navigator: Customizable and intuitive.

523 lines (522 loc) 26 kB
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { PureComponent, useEffect } from 'react'; import ReactDOM from 'react-dom'; import './tourNavigator.css'; import { Align, Position } from './types'; var fitPriority = { left: [Position.RIGHT, Position.BOTTOM, Position.TOP], right: [Position.LEFT, Position.BOTTOM, Position.TOP], top: [Position.BOTTOM, Position.LEFT, Position.RIGHT], bottom: [Position.TOP, Position.LEFT, Position.RIGHT] }; function TourNavigatorWrapper(_a) { var children = _a.children, onAfterOpen = _a.onAfterOpen, onBeforeClose = _a.onBeforeClose; useEffect(function () { onAfterOpen === null || onAfterOpen === void 0 ? void 0 : onAfterOpen(); return function () { onBeforeClose === null || onBeforeClose === void 0 ? void 0 : onBeforeClose(); }; }, []); return children; } var TourNavigator = /** @class */ (function (_super) { __extends(TourNavigator, _super); function TourNavigator(props) { var _a; var _this = _super.call(this, props) || this; _this.renderElement = document.body; _this.scrollingElement = document; _this.helper = null; _this.lastAvailableStepIndex = 0; _this.subscribe = new Map; _this.state = { currentStepIndex: (_a = props.startAt) !== null && _a !== void 0 ? _a : 0, previousStepIndex: -1, x: 0, y: 0, height: 0, width: 0, isScrollingIntoView: false, elementsMap: {} }; _this.lastAvailableStepIndex = _this.state.currentStepIndex; if (props.renderElement && props.renderElement instanceof HTMLElement) _this.renderElement = props.renderElement; if (props.scrollingElement && props.scrollingElement instanceof HTMLElement) _this.scrollingElement = props.scrollingElement; return _this; } Object.defineProperty(TourNavigator.prototype, "currentStepIndex", { get: function () { var _a, _b; var _c = this.state, elementsMap = _c.elementsMap, currentStepIndex = _c.currentStepIndex; var dontWaitForElement = elementsMap[((_b = (_a = this.props.steps) === null || _a === void 0 ? void 0 : _a[currentStepIndex]) === null || _b === void 0 ? void 0 : _b.selector) || ''] || this.props.waitForElementRendered == false; if (dontWaitForElement) return this.lastAvailableStepIndex = currentStepIndex; return this.lastAvailableStepIndex; }, enumerable: false, configurable: true }); Object.defineProperty(TourNavigator.prototype, "currentStep", { get: function () { if (this.props.steps.length == 0) return null; return this.props.steps[this.currentStepIndex]; }, enumerable: false, configurable: true }); Object.defineProperty(TourNavigator.prototype, "currentElement", { get: function () { if (this.currentStep == null) return null; return this.state.elementsMap[this.currentStep.selector] || document.querySelector(this.currentStep.selector); }, enumerable: false, configurable: true }); TourNavigator.prototype.componentDidMount = function () { this.mount(); }; TourNavigator.prototype.componentWillUnmount = function () { this.unmount(); }; TourNavigator.prototype.componentDidUpdate = function (prevProps, prevState, snapshot) { if (prevProps.isOpen && this.props.isOpen === false) this.unmount(); if (prevProps.isOpen == false && this.props.isOpen) this.mount(); }; TourNavigator.prototype.mount = function () { var _this = this; if (typeof this.props.renderElement == 'string') this.renderElement = document.querySelector(this.props.renderElement) || document.body; if (typeof this.props.scrollingElement == 'string') this.scrollingElement = (document.querySelector(this.props.scrollingElement) || (document.scrollingElement || document.documentElement)); if (this.props.resizeListener) this.addListener(window, ['resize', this.updateBoundingClientRect.bind(this, undefined)]); if (this.props.scrollListener) this.addListener(this.scrollingElement, ['scroll', this.updateBoundingClientRect.bind(this, { behavior: 'auto' })]); this.mapElements(function () { _this.updateBoundingClientRect(); }); this.initMutationObserver(); }; TourNavigator.prototype.unmount = function () { this.subscribe.forEach(function (unsubscribe) { return unsubscribe(); }); if (this.mutationObserver) this.mutationObserver.disconnect(); }; TourNavigator.prototype.addListener = function (element, args) { var _this = this; if (typeof element === 'string') { var element_ = document.querySelector(element); if (element_) element = element_; else return; } var target = element; if (this.subscribe.has(target)) { var unsubscribe_1 = this.subscribe.get(target); unsubscribe_1 === null || unsubscribe_1 === void 0 ? void 0 : unsubscribe_1(); } var subscribe = function () { target.addEventListener.apply(target, args); _this.subscribe.set(target, unsubscribe); return unsubscribe; }; var unsubscribe = function () { target.removeEventListener.apply(target, args); _this.subscribe.delete(target); return subscribe; }; subscribe(); }; TourNavigator.prototype.initMutationObserver = function () { var _this = this; if (this.props.mutationObserve) { this.mutationObserver = new MutationObserver(function (entries) { _this.mapElements(function () { _this.updateBoundingClientRect(); }); }); var addToObserveList_1 = function (target, config) { var _a; var targetElement = null; config = __assign({ childList: true }, config); if (typeof target === 'string') { targetElement = document.querySelector(target); } else if (target instanceof HTMLElement) { targetElement = target; } if (targetElement) { (_a = _this.mutationObserver) === null || _a === void 0 ? void 0 : _a.observe(targetElement, config); } }; var config = this.props.mutationObserve; if (typeof config === 'string' || config instanceof HTMLElement) { addToObserveList_1(config); } else if (Array.isArray(config)) { config.forEach(function (item) { if (typeof item === 'string' || item instanceof HTMLElement) { addToObserveList_1(item); } else if (Array.isArray(item)) { var _a = item, target = _a[0], options = _a[1]; addToObserveList_1(target, options); } }); } } }; TourNavigator.prototype.mapElements = function (callback) { this.setState({ elementsMap: this.props.steps.reduce(function (acc, cur) { acc[cur.selector] = document.querySelector(cur.selector); return acc; }, {}) }, callback); }; TourNavigator.prototype.updateBoundingClientRect = function (scrollIntoViewOptions) { var _this = this; var _a; if (this.currentElement == null) return; var subscriptions = []; if (this.props.scrollListener && this.subscribe.has(this.scrollingElement)) { var unsubscribe = this.subscribe.get(this.scrollingElement); unsubscribe && subscriptions.push(unsubscribe); } var updateFocus = function () { if (_this.currentElement == null) return; var _a = _this.currentElement.getBoundingClientRect(), x = _a.x, y = _a.y, height = _a.height, width = _a.width; _this.setState({ x: x, y: y, height: height, width: width, isScrollingIntoView: false }); }; if (((_a = this.currentStep) === null || _a === void 0 ? void 0 : _a.scrollIntoView) === false) return updateFocus(); var observerCallback = function (entries, observer) { if (entries[0].isIntersecting) { _this.stopCurrentScrollIntoView(); updateFocus(); subscriptions.forEach(function (subscribe) { return subscribe(); }); observer.disconnect(); } }; var observer = new IntersectionObserver(observerCallback, this.getDynamicIntersectionOption()); observer.observe(this.currentElement); subscriptions.forEach(function (unsubscribe, index) { var subscribe = unsubscribe(); subscriptions[index] = subscribe; }); this.setState({ isScrollingIntoView: true }); this.currentElement.scrollIntoView({ behavior: (scrollIntoViewOptions === null || scrollIntoViewOptions === void 0 ? void 0 : scrollIntoViewOptions.behavior) || this.props.scrollBehavior, block: 'nearest', inline: 'nearest' }); }; TourNavigator.prototype.stopCurrentScrollIntoView = function () { var _a; var currentElement = (_a = this.currentElement) === null || _a === void 0 ? void 0 : _a.parentElement; while (currentElement) { var hasScrolling = currentElement.scrollHeight > currentElement.clientHeight || currentElement.scrollWidth > currentElement.clientWidth; if (hasScrolling) { currentElement.scrollTo({ top: currentElement.scrollTop, left: currentElement.scrollLeft }); } currentElement = currentElement.parentElement; } }; TourNavigator.prototype.getDynamicIntersectionOption = function () { var _a, _b, _c; if (this.currentElement == null) return; var computedStyle = window.getComputedStyle(this.currentElement); var borderTopWidth = parseInt(computedStyle.borderTopWidth, 10) || 1; var borderRightWidth = parseInt(computedStyle.borderRightWidth, 10) || 1; var borderBottomWidth = parseInt(computedStyle.borderBottomWidth, 10) || 1; var borderLeftWidth = parseInt(computedStyle.borderLeftWidth, 10) || 1; var threshold = 1; if (this.currentElement.offsetHeight > window.innerHeight) { threshold = Math.min(threshold, window.innerHeight / this.currentElement.offsetHeight); } if (this.currentElement.offsetWidth > window.innerWidth) { threshold = Math.min(threshold, window.innerWidth / this.currentElement.offsetWidth); } var intersectionOption = { threshold: Math.floor(threshold), rootMargin: "".concat(borderTopWidth, "px ").concat(borderRightWidth, "px ").concat(borderBottomWidth, "px ").concat(borderLeftWidth, "px"), }; var currentIntersectionOption; if (typeof ((_a = this.currentStep) === null || _a === void 0 ? void 0 : _a.intersectionOption) === 'function') { currentIntersectionOption = (_b = this.currentStep) === null || _b === void 0 ? void 0 : _b.intersectionOption(__assign({}, intersectionOption)); } else { currentIntersectionOption = (_c = this.currentStep) === null || _c === void 0 ? void 0 : _c.intersectionOption; } if (currentIntersectionOption === null || currentIntersectionOption === void 0 ? void 0 : currentIntersectionOption.rootMargin) { intersectionOption.rootMargin = currentIntersectionOption.rootMargin; } if (currentIntersectionOption === null || currentIntersectionOption === void 0 ? void 0 : currentIntersectionOption.threshold) { intersectionOption.threshold = currentIntersectionOption.threshold; } if (typeof (currentIntersectionOption === null || currentIntersectionOption === void 0 ? void 0 : currentIntersectionOption.root) === 'string') { intersectionOption.root = document.querySelector(currentIntersectionOption === null || currentIntersectionOption === void 0 ? void 0 : currentIntersectionOption.root); } else if (currentIntersectionOption === null || currentIntersectionOption === void 0 ? void 0 : currentIntersectionOption.root) { intersectionOption.root = currentIntersectionOption === null || currentIntersectionOption === void 0 ? void 0 : currentIntersectionOption.root; } return intersectionOption; }; TourNavigator.prototype.getMaskBoundingClientRect = function () { var _a = this.state, x = _a.x, y = _a.y, height = _a.height, width = _a.width; var _b = this.props.maskPadding, maskPadding = _b === void 0 ? 0 : _b; return { x: x - maskPadding, y: y - maskPadding, height: height + maskPadding + maskPadding, width: width + maskPadding + maskPadding }; }; TourNavigator.prototype.getHelperBoundingClientRect = function () { var rect = (this.helper && this.helper.getBoundingClientRect()) || { x: 0, y: 0, height: 0, width: 0 }; return { x: rect.x, y: rect.y, height: rect.height, width: rect.width }; }; TourNavigator.prototype.focus = function (scrollBehavior) { if (scrollBehavior === void 0) { scrollBehavior = 'auto'; } this.updateBoundingClientRect(scrollBehavior && { behavior: scrollBehavior }); }; TourNavigator.prototype.goto = function (stepIndex) { var _this = this; var currentStepIndex = Math.max(0, Math.min(stepIndex, this.props.steps.length - 1)); this.setState({ currentStepIndex: currentStepIndex, previousStepIndex: this.state.currentStepIndex }, function () { _this.updateBoundingClientRect(); var props = { id: _this.props.id, currentStep: _this.currentStep, currentStepIndex: _this.currentStepIndex, previousStepIndex: _this.state.previousStepIndex, steps: _this.props.steps, target: _this.currentElement, focus: _this.focus.bind(_this), goto: _this.goto.bind(_this), next: _this.next.bind(_this), prev: _this.prev.bind(_this), isScrollingIntoView: _this.state.isScrollingIntoView, onRequestClose: _this.props.onRequestClose || null }; _this.props.onMove && _this.props.onMove(props); }); }; TourNavigator.prototype.next = function () { var _this = this; var currentStepIndex = Math.min(this.state.currentStepIndex + 1, this.props.steps.length - 1); this.setState({ currentStepIndex: currentStepIndex, previousStepIndex: this.state.currentStepIndex }, function () { _this.updateBoundingClientRect(); var props = { id: _this.props.id, currentStep: _this.currentStep, currentStepIndex: _this.currentStepIndex, previousStepIndex: _this.state.previousStepIndex, steps: _this.props.steps, target: _this.currentElement, focus: _this.focus.bind(_this), goto: _this.goto.bind(_this), next: _this.next.bind(_this), prev: _this.prev.bind(_this), isScrollingIntoView: _this.state.isScrollingIntoView, onRequestClose: _this.props.onRequestClose || null }; _this.props.onNext && _this.props.onNext(props); _this.props.onMove && _this.props.onMove(props); }); }; TourNavigator.prototype.prev = function () { var _this = this; var currentStepIndex = Math.max(this.state.currentStepIndex - 1, 0); this.setState({ currentStepIndex: currentStepIndex, previousStepIndex: this.state.currentStepIndex }, function () { _this.updateBoundingClientRect(); var props = { id: _this.props.id, currentStep: _this.currentStep, currentStepIndex: _this.currentStepIndex, previousStepIndex: _this.state.previousStepIndex, steps: _this.props.steps, target: _this.currentElement, focus: _this.focus.bind(_this), goto: _this.goto.bind(_this), next: _this.next.bind(_this), prev: _this.prev.bind(_this), isScrollingIntoView: _this.state.isScrollingIntoView, onRequestClose: _this.props.onRequestClose || null }; _this.props.onPrev && _this.props.onPrev(props); _this.props.onMove && _this.props.onMove(props); }); }; TourNavigator.prototype.renderOverLay = function () { var _this = this; var _a = this.getMaskBoundingClientRect(), x = _a.x, y = _a.y, height = _a.height, width = _a.width; var overlayRef = function (ref) { if (ref == null) return; ref.onclick = function (event) { var _a, _b; var isMask = (event.x >= x && event.x <= (x + width)) && (event.y >= y && event.y <= (y + height)); (_b = (_a = _this.props).onRequestClose) === null || _b === void 0 ? void 0 : _b.call(_a, { event: event, isMask: isMask, isOverlay: true }); }; }; return (_jsx("div", { className: '__tourNavigator-overlay', ref: overlayRef, children: this.props.overlay ? this.props.overlay({ x: x, y: y, height: height, width: width, isScrollingIntoView: this.state.isScrollingIntoView }) : (_jsxs("svg", { height: '100%', width: '100%', children: [_jsx("defs", { children: _jsxs("mask", { id: "__tourNavigator-mask-".concat(this.props.id), children: [_jsx("rect", { x: 0, y: 0, height: '100%', width: '100%', fill: 'white' }), _jsx("rect", { x: x, y: y, height: height, width: width, fill: 'black', rx: this.props.maskRadius, opacity: this.props.maskOpacity, style: __assign(__assign({}, this.props.maskStyle), (this.state.isScrollingIntoView ? this.props.maskStyleDuringScroll : undefined)) })] }) }), _jsx("rect", { x: 0, y: 0, height: '100%', width: '100%', fill: this.props.overlayFill, opacity: this.props.overlayOpacity, mask: "url(#__tourNavigator-mask-".concat(this.props.id, ")") })] })) })); }; TourNavigator.prototype.renderHelper = function () { var _this = this; var currentStepIndex = this.currentStepIndex; var helper = this.props.helper; if (!helper) return null; var adjustPosition = function (helperElm) { if ((_this.helper = helperElm) == null || _this.currentStep == null) return; var maskRect = _this.getMaskBoundingClientRect(); var boundingRect = _this.getHelperBoundingClientRect(); var _a = _this.props, _b = _a.maskHelperDistance, maskHelperDistance = _b === void 0 ? 0 : _b, _c = _a.screenHelperDistance, screenHelperDistance = _c === void 0 ? 0 : _c; var boundingRectDistance = maskHelperDistance + screenHelperDistance; var canFit = { left: maskRect.x >= (boundingRect.width + boundingRectDistance), right: (window.innerWidth - (maskRect.x + maskRect.width)) >= (boundingRect.width + boundingRectDistance), top: maskRect.y >= (boundingRect.height + boundingRectDistance), bottom: (window.innerHeight - (maskRect.y + maskRect.height)) >= (boundingRect.height + boundingRectDistance) }; var position = _this.currentStep.position || Position.LEFT; var align = _this.currentStep.align || Align.START; if (Array.isArray(position)) position = position.find(function (key) { return canFit[key]; }) || Position.LEFT; else position = (canFit[position] ? position : fitPriority[position].find(function (key) { return canFit[key]; })) || Position.LEFT; var x = 0, y = 0; if (position == 'top') { x = maskRect.x; y = maskRect.y - boundingRect.height - maskHelperDistance; } if (position == 'bottom') { x = maskRect.x; y = maskRect.y + maskRect.height + maskHelperDistance; } if (position == 'left') { x = maskRect.x - boundingRect.width - maskHelperDistance; y = maskRect.y; } if (position == 'right') { x = maskRect.x + maskRect.width + maskHelperDistance; y = maskRect.y; } if (position == 'left' || position == 'right') { if (align == 'end') y = (y + maskRect.height) - boundingRect.height; if (align == 'center') y = (y + maskRect.height / 2) - boundingRect.height / 2; } if (position == 'top' || position == 'bottom') { if (align == 'end') x = (x + maskRect.width) - boundingRect.width; if (align == 'center') x = (x + maskRect.width / 2) - boundingRect.width / 2; } x = Math.max(screenHelperDistance, Math.min(window.innerWidth - boundingRect.width - screenHelperDistance, x)); y = Math.max(screenHelperDistance, Math.min(window.innerHeight - boundingRect.height - screenHelperDistance, y)); _this.helper.dataset.position = position.toLowerCase(); _this.helper.dataset.align = align.toString().toLowerCase(); _this.helper.style.setProperty('--x', String(x)); _this.helper.style.setProperty('--y', String(y)); }; return (_jsx("div", { className: '__tourNavigator-helper', ref: adjustPosition, children: helper({ id: this.props.id, currentStep: this.currentStep, currentStepIndex: currentStepIndex, previousStepIndex: this.state.previousStepIndex, steps: this.props.steps, target: this.currentElement, focus: this.focus.bind(this), goto: this.goto.bind(this), next: this.next.bind(this), prev: this.prev.bind(this), isScrollingIntoView: this.state.isScrollingIntoView, onRequestClose: this.props.onRequestClose || null }) })); }; TourNavigator.prototype.render = function () { if (!this.props.isOpen) return null; return ReactDOM.createPortal(_jsx(TourNavigatorWrapper, { onAfterOpen: this.props.onAfterOpen, onBeforeClose: this.props.onBeforeClose, children: _jsxs("div", { className: "__tourNavigator ".concat(this.props.className), style: this.props.style, children: [this.props.renderOverlay && this.renderOverLay(), this.props.renderHelper && this.renderHelper()] }) }), this.renderElement); }; TourNavigator.defaultProps = { id: "___tournavigator-".concat(Date.now()), steps: [], maskRadius: 5, maskPadding: 5, startAt: 0, maskHelperDistance: 10, screenHelperDistance: 10, onAfterOpen: null, onBeforeClose: null, helper: null, isOpen: true, onRequestClose: null, onNext: null, onPrev: null, scrollBehavior: 'smooth', resizeListener: true, scrollListener: true, overlayFill: 'black', overlayOpacity: 0.5, maskOpacity: 1, overlay: null, className: '', renderHelper: true, renderOverlay: true, renderElement: document.body, scrollingElement: document, waitForElementRendered: false }; return TourNavigator; }(PureComponent)); export default TourNavigator;