tour-navigator
Version:
Streamline website tours with Tour Navigator: Customizable and intuitive.
523 lines (522 loc) • 26 kB
JavaScript
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;