diff --git a/index.html b/index.html
index 6a35359d..7987adc1 100644
--- a/index.html
+++ b/index.html
@@ -177,6 +177,10 @@
Highlight Feature
+
+
+
+
@@ -409,6 +413,77 @@ Usage and Demo
},
];
+
+ const sameElementSteps = [
+ {
+ element: ".page-header h1",
+ popover: {
+ title: "1 popup on the same element",
+ description:
+ "Unlike the older version, new version doesn't work with z-indexes so no more positional issues.",
+ side: "left",
+ align: "start",
+ },
+ },
+ {
+ element: ".page-header h1",
+ popover: {
+ title: "2 popup on the same element",
+ description:
+ "Unlike the older version, new version doesn't work with z-indexes so no more positional issues.",
+ side: "left",
+ align: "start",
+ },
+ },
+ {
+ element: ".page-header h1",
+ popover: {
+ title: "3 popup on the same element",
+ description:
+ "Unlike the older version, new version doesn't work with z-indexes so no more positional issues.",
+ side: "left",
+ align: "start",
+ },
+ },
+ {
+ element: ".page-header sup",
+ popover: {
+ title: "Improved Hooks",
+ description:
+ "Unlike the older version, new version doesn't work with z-indexes so no more positional issues.",
+ side: "bottom",
+ align: "start",
+ },
+ },
+ {
+ popover: {
+ title: "No Element",
+ description: "You can now have popovers without elements as well",
+ },
+ },
+ {
+ element: '.buttons',
+ popover: {
+ title: "Buttons",
+ description: "Here are some buttons",
+ },
+ },
+ {
+ element: "#scrollable-area",
+ popover: {
+ title: "Scrollable Areas",
+ description: "There are no issues with scrollable element tours as well.",
+ },
+ },
+ {
+ element: "#third-scroll-paragraph",
+ popover: {
+ title: "Nested Scrolls",
+ description: "Even the nested scrollable elements work now.",
+ },
+ },
+ ];
+
document.getElementById("non-animated-tour").addEventListener("click", () => {
const driverObj = driver({
animate: false,
@@ -877,6 +952,47 @@ Usage and Demo
});
});
+ document.getElementById("backdrop-disabled-btn").addEventListener("click", () => {
+ driver({
+ overlayEnable: false,
+ }).highlight({
+ element: "h2",
+ popover: {
+ title: "MIT License",
+ description: "A lightweight, no-dependency JavaScript engine to drive user's focus.",
+ side: "bottom",
+ align: "start",
+ },
+ });;
+ });
+
+ document.getElementById("disabled-stick-to-viewport").addEventListener("click", () => {
+ const driverObj = driver({
+ popoverStickToViewport: false,
+ steps: basicTourSteps,
+ });
+
+ driverObj.drive();
+ });
+
+ document.getElementById("disabled-scroll").addEventListener("click", () => {
+ const driverObj = driver({
+ allowScroll: false,
+ steps: basicTourSteps,
+ });
+
+ driverObj.drive();
+ });
+
+ document.getElementById("disabled-same-element-animation").addEventListener("click", () => {
+ const driverObj = driver({
+ animateBetweenSameElements: false,
+ steps: sameElementSteps,
+ });
+
+ driverObj.drive();
+ });
+
document.getElementById("activate-check-btn").addEventListener("click", () => {
const driverObj = driver({
showButtons: false,
diff --git a/src/config.ts b/src/config.ts
index ed1ce772..a12f1f85 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -8,10 +8,13 @@ export type Config = {
steps?: DriveStep[];
animate?: boolean;
+ animateBetweenSameElements?: boolean
+ overlayEnable?: boolean;
overlayColor?: string;
overlayOpacity?: number;
smoothScroll?: boolean;
allowClose?: boolean;
+ allowScroll?: boolean;
stagePadding?: number;
stageRadius?: number;
@@ -22,6 +25,7 @@ export type Config = {
// Popover specific configuration
popoverClass?: string;
popoverOffset?: number;
+ popoverStickToViewport?: boolean;
showButtons?: AllowedButtons[];
disableButtons?: AllowedButtons[];
showProgress?: boolean;
@@ -53,14 +57,18 @@ let currentConfig: Config = {};
export function configure(config: Config = {}) {
currentConfig = {
animate: true,
+ animateBetweenSameElements: true,
allowClose: true,
+ allowScroll: true,
overlayOpacity: 0.7,
+ overlayEnable: true,
smoothScroll: false,
disableActiveInteraction: false,
showProgress: false,
stagePadding: 10,
stageRadius: 5,
popoverOffset: 10,
+ popoverStickToViewport: true,
showButtons: ["next", "previous", "close"],
disableButtons: [],
overlayColor: "#000",
diff --git a/src/driver.css b/src/driver.css
index 0128f76d..f47fb149 100644
--- a/src/driver.css
+++ b/src/driver.css
@@ -2,14 +2,18 @@
pointer-events: none;
}
-.driver-active * {
+.driver-active.driver-no-scroll {
+ overflow: hidden;
+}
+
+.driver-active:not(.driver-no-overlay) * {
pointer-events: none;
}
.driver-active .driver-active-element,
.driver-active .driver-active-element *,
-.driver-popover,
-.driver-popover * {
+.driver-active .driver-popover,
+.driver-active .driver-popover * {
pointer-events: auto;
}
@@ -27,7 +31,7 @@
animation: animate-fade-in 200ms ease-in-out;
}
-.driver-fade .driver-popover {
+.driver-fade .driver-popover:not(.driver-no-animation) {
animation: animate-fade-in 200ms;
}
diff --git a/src/driver.ts b/src/driver.ts
index 564f55b0..2aeab45c 100644
--- a/src/driver.ts
+++ b/src/driver.ts
@@ -8,7 +8,7 @@ import { getState, resetState, setState } from "./state";
import "./driver.css";
export type DriveStep = {
- element?: string | Element;
+ element?: string | Element | (() => Element | null);
onHighlightStarted?: DriverHook;
onHighlighted?: DriverHook;
onDeselected?: DriverHook;
@@ -131,6 +131,7 @@ export function driver(options: Config = {}) {
listen("overlayClick", handleClose);
listen("escapePress", handleClose);
+ listen("windowClick", handleClose);
listen("arrowLeftPress", handleArrowLeft);
listen("arrowRightPress", handleArrowRight);
}
@@ -234,7 +235,7 @@ export function driver(options: Config = {}) {
const onDeselected = activeStep?.onDeselected || getConfig("onDeselected");
const onDestroyed = getConfig("onDestroyed");
- document.body.classList.remove("driver-active", "driver-fade", "driver-simple");
+ document.body.classList.remove("driver-active", "driver-fade", "driver-simple", "driver-no-overlay", "driver-no-scroll");
destroyEvents();
destroyPopover();
diff --git a/src/emitter.ts b/src/emitter.ts
index 7964dfd7..7a514aa6 100644
--- a/src/emitter.ts
+++ b/src/emitter.ts
@@ -5,7 +5,8 @@ type allowedEvents =
| "prevClick"
| "closeClick"
| "arrowRightPress"
- | "arrowLeftPress";
+ | "arrowLeftPress"
+ | "windowClick";
let registeredListeners: Partial<{ [key in allowedEvents]: () => void }> = {};
diff --git a/src/events.ts b/src/events.ts
index ae643f71..17a60061 100644
--- a/src/events.ts
+++ b/src/events.ts
@@ -4,6 +4,19 @@ import { getState, setState } from "./state";
import { getConfig } from "./config";
import { getFocusableElements } from "./utils";
+function onWindowClick(e: MouseEvent) {
+ const isOverlayEnabled = getConfig('overlayEnable')
+ if (isOverlayEnabled) return;
+
+ const isPopoverContent = (e.target as HTMLElement)?.closest('#driver-popover-content')
+ const isActive = getState('activeStep')
+ const isTransitioning = getState("__transitionCallback")
+
+ if (isTransitioning || !isActive || isPopoverContent) return;
+
+ emit('windowClick')
+}
+
export function requireRefresh() {
const resizeTimeout = getState("__resizeTimeout");
if (resizeTimeout) {
@@ -118,10 +131,14 @@ export function initEvents() {
window.addEventListener("keydown", trapFocus, false);
window.addEventListener("resize", requireRefresh);
window.addEventListener("scroll", requireRefresh);
+ if (!getConfig('overlayEnable')) {
+ window.addEventListener('click', onWindowClick)
+ }
}
export function destroyEvents() {
window.removeEventListener("keyup", onKeyup);
window.removeEventListener("resize", requireRefresh);
window.removeEventListener("scroll", requireRefresh);
+ window.removeEventListener("click", onWindowClick);
}
diff --git a/src/highlight.ts b/src/highlight.ts
index 961a26af..f3057517 100644
--- a/src/highlight.ts
+++ b/src/highlight.ts
@@ -27,9 +27,30 @@ function mountDummyElement(): Element {
return element;
}
-export function highlight(step: DriveStep) {
+function getStepElement(step: DriveStep): Element | null {
const { element } = step;
- let elemObj = typeof element === "string" ? document.querySelector(element) : element;
+
+ if (typeof element === 'function') {
+ return element();
+ }
+
+ if (typeof element === 'string') {
+ return document.querySelector(element);
+ }
+
+ return element ?? null;
+}
+
+export function highlight(step: DriveStep) {
+ let elemObj = getStepElement(step);
+
+ if (!getConfig("overlayEnable")) {
+ document.body.classList.add("driver-no-overlay");
+ }
+
+ if (!getConfig("allowScroll")) {
+ document.body.classList.add("driver-no-scroll");
+ }
// If the element is not found, we mount a 1px div
// at the center of the screen to highlight and show
@@ -62,11 +83,12 @@ function transferHighlight(toElement: Element, toStep: DriveStep) {
const fromStep = getState("__activeStep");
const fromElement = getState("__activeElement") || toElement;
+ const isSameElement = fromElement === toElement;
// If it's the first time we're highlighting an element, we show
// the popover immediately. Otherwise, we wait for the animation
// to finish before showing the popover.
- const isFirstHighlight = !fromElement || fromElement === toElement;
+ const isFirstHighlight = !fromElement || isSameElement;
const isToDummyElement = toElement.id === "driver-dummy-element";
const isFromDummyElement = fromElement.id === "driver-dummy-element";
@@ -102,6 +124,8 @@ function transferHighlight(toElement: Element, toStep: DriveStep) {
setState("activeStep", toStep);
setState("activeElement", toElement);
+ const isSameElementAnimationEnabled = !isSameElement || !!getConfig("animateBetweenSameElements");
+
const animate = () => {
const transitionCallback = getState("__transitionCallback");
@@ -112,12 +136,15 @@ function transferHighlight(toElement: Element, toStep: DriveStep) {
return;
}
+ setState("__activeElement", toElement);
+
const elapsed = Date.now() - start;
const timeRemaining = duration - elapsed;
const isHalfwayThrough = timeRemaining <= duration / 2;
+
if (toStep.popover && isHalfwayThrough && !isPopoverRendered && hasDelayedPopover) {
- renderPopover(toElement, toStep);
+ renderPopover(toElement, toStep, isSameElementAnimationEnabled);
isPopoverRendered = true;
}
@@ -137,7 +164,6 @@ function transferHighlight(toElement: Element, toStep: DriveStep) {
setState("__previousStep", fromStep);
setState("__previousElement", fromElement);
setState("__activeStep", toStep);
- setState("__activeElement", toElement);
}
window.requestAnimationFrame(animate);
@@ -149,7 +175,7 @@ function transferHighlight(toElement: Element, toStep: DriveStep) {
bringInView(toElement);
if (!hasDelayedPopover && toStep.popover) {
- renderPopover(toElement, toStep);
+ renderPopover(toElement, toStep, isSameElementAnimationEnabled);
}
fromElement.classList.remove("driver-active-element", "driver-no-interaction");
diff --git a/src/overlay.ts b/src/overlay.ts
index cffb4e16..36da707d 100644
--- a/src/overlay.ts
+++ b/src/overlay.ts
@@ -55,6 +55,9 @@ export function trackActiveElement(element: Element) {
}
export function refreshOverlay() {
+ const isOverlayEnabled = getConfig('overlayEnable')
+ if (!isOverlayEnabled) return;
+
const activeStagePosition = getState("__activeStagePosition");
const overlaySvg = getState("__overlaySvg");
@@ -90,6 +93,9 @@ function mountOverlay(stagePosition: StageDefinition) {
}
function renderOverlay(stagePosition: StageDefinition) {
+ const isOverlayEnabled = getConfig('overlayEnable')
+ if (!isOverlayEnabled) return;
+
const overlaySvg = getState("__overlaySvg");
// TODO: cancel rendering if element is not visible
diff --git a/src/popover.ts b/src/popover.ts
index a11ca97c..ccd45395 100644
--- a/src/popover.ts
+++ b/src/popover.ts
@@ -1,4 +1,4 @@
-import { bringInView, getFocusableElements } from "./utils";
+import { getFocusableElements } from "./utils";
import { Config, DriverHook, getConfig } from "./config";
import { getState, setState, State } from "./state";
import { DriveStep } from "./driver";
@@ -58,7 +58,7 @@ export function hidePopover() {
popover.wrapper.style.display = "none";
}
-export function renderPopover(element: Element, step: DriveStep) {
+export function renderPopover(element: Element, step: DriveStep, isAnimationEnabled: boolean = true) {
let popover = getState("popover");
if (popover) {
document.body.removeChild(popover.wrapper);
@@ -151,6 +151,10 @@ export function renderPopover(element: Element, step: DriveStep) {
const customPopoverClass = step.popover?.popoverClass || getConfig("popoverClass") || "";
popoverWrapper.className = `driver-popover ${customPopoverClass}`.trim();
+ if (!isAnimationEnabled) {
+ popoverWrapper.classList.add("driver-no-animation");
+ }
+
// Handles the popover button clicks
onDriverClick(
popover.wrapper,
@@ -221,7 +225,6 @@ export function renderPopover(element: Element, step: DriveStep) {
}
repositionPopover(element, step);
- bringInView(popoverWrapper);
// Focus on the first focusable element in active element or popover
const isToDummyElement = element.classList.contains("driver-dummy-element");
@@ -268,6 +271,8 @@ function calculateTopForLeftRight(
}
): number {
const { elementDimensions, popoverDimensions, popoverPadding, popoverArrowDimensions } = config;
+ const shouldStickToViewport = getConfig('popoverStickToViewport');
+ const safeZone = shouldStickToViewport ? popoverArrowDimensions.width : Number.MIN_SAFE_INTEGER
if (alignment === "start") {
return Math.max(
@@ -275,7 +280,7 @@ function calculateTopForLeftRight(
elementDimensions.top - popoverPadding,
window.innerHeight - popoverDimensions!.realHeight - popoverArrowDimensions.width
),
- popoverArrowDimensions.width
+ safeZone
);
}
@@ -285,7 +290,7 @@ function calculateTopForLeftRight(
elementDimensions.top - popoverDimensions?.realHeight + elementDimensions.height + popoverPadding,
window.innerHeight - popoverDimensions?.realHeight - popoverArrowDimensions.width
),
- popoverArrowDimensions.width
+ safeZone
);
}
@@ -295,7 +300,7 @@ function calculateTopForLeftRight(
elementDimensions.top + elementDimensions.height / 2 - popoverDimensions?.realHeight / 2,
window.innerHeight - popoverDimensions?.realHeight - popoverArrowDimensions.width
),
- popoverArrowDimensions.width
+ safeZone
);
}
@@ -313,6 +318,8 @@ function calculateLeftForTopBottom(
}
): number {
const { elementDimensions, popoverDimensions, popoverPadding, popoverArrowDimensions } = config;
+ const shouldStickToViewport = getConfig('popoverStickToViewport');
+ const safeZone = shouldStickToViewport ? popoverArrowDimensions.width : Number.MIN_SAFE_INTEGER
if (alignment === "start") {
return Math.max(
@@ -320,7 +327,7 @@ function calculateLeftForTopBottom(
elementDimensions.left - popoverPadding,
window.innerWidth - popoverDimensions!.realWidth - popoverArrowDimensions.width
),
- popoverArrowDimensions.width
+ safeZone
);
}
@@ -330,7 +337,7 @@ function calculateLeftForTopBottom(
elementDimensions.left - popoverDimensions?.realWidth + elementDimensions.width + popoverPadding,
window.innerWidth - popoverDimensions?.realWidth - popoverArrowDimensions.width
),
- popoverArrowDimensions.width
+ safeZone
);
}
@@ -340,7 +347,7 @@ function calculateLeftForTopBottom(
elementDimensions.left + elementDimensions.width / 2 - popoverDimensions?.realWidth / 2,
window.innerWidth - popoverDimensions?.realWidth - popoverArrowDimensions.width
),
- popoverArrowDimensions.width
+ safeZone
);
}
@@ -379,6 +386,8 @@ export function repositionPopover(element: Element, step: DriveStep) {
const noneOptimal = !isTopOptimal && !isBottomOptimal && !isLeftOptimal && !isRightOptimal;
let popoverRenderedSide: Side = requiredSide;
+ const shouldStickToViewport = getConfig('popoverStickToViewport');
+
if (requiredSide === "top" && isTopOptimal) {
isRightOptimal = isLeftOptimal = isBottomOptimal = false;
} else if (requiredSide === "bottom" && isBottomOptimal) {
@@ -443,10 +452,11 @@ export function repositionPopover(element: Element, step: DriveStep) {
popoverRenderedSide = "right";
} else if (isTopOptimal) {
- const topToSet = Math.min(
+ const topToSet = shouldStickToViewport ? Math.min(
topValue,
window.innerHeight - popoverDimensions!.realHeight - popoverArrowDimensions.width
- );
+ ) : topValue;
+
let leftToSet = calculateLeftForTopBottom(requiredAlignment, {
elementDimensions,
popoverDimensions,
@@ -461,10 +471,10 @@ export function repositionPopover(element: Element, step: DriveStep) {
popoverRenderedSide = "top";
} else if (isBottomOptimal) {
- const bottomToSet = Math.min(
+ const bottomToSet = shouldStickToViewport ? Math.min(
bottomValue,
window.innerHeight - popoverDimensions?.realHeight - popoverArrowDimensions.width
- );
+ ) : bottomValue;
let leftToSet = calculateLeftForTopBottom(requiredAlignment, {
elementDimensions,