(key?: K) {
81 | return key ? currentConfig[key] : currentConfig;
82 | }
83 |
84 | export function setCurrentDriver(driver: Driver) {
85 | currentDriver = driver;
86 | }
87 |
88 | export function getCurrentDriver() {
89 | return currentDriver;
90 | }
91 |
--------------------------------------------------------------------------------
/docs/src/content/guides/confirm-on-exit.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Confirm on Exit"
3 | groupTitle: "Examples"
4 | sort: 3
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | You can use the `onDestroyStarted` hook to add a confirmation dialog or some other logic when the user tries to exit the tour. In the example below, upon exit we check if there are any tour steps left and ask for confirmation before we exit.
10 |
11 |
26 | ```js
27 | import { driver } from "driver.js";
28 | import "driver.js/dist/driver.css";
29 |
30 | const driverObj = driver({
31 | showProgress: true,
32 | steps: [
33 | { element: '#confirm-destroy-example', popover: { title: 'Animated Tour Example', description: 'Here is the code example showing animated tour. Let\'s walk you through it.', side: "left", align: 'start' }},
34 | { element: 'code .line:nth-child(1)', popover: { title: 'Import the Library', description: 'It works the same in vanilla JavaScript as well as frameworks.', side: "bottom", align: 'start' }},
35 | { element: 'code .line:nth-child(2)', popover: { title: 'Importing CSS', description: 'Import the CSS which gives you the default styling for popover and overlay.', side: "bottom", align: 'start' }},
36 | { popover: { title: 'Happy Coding', description: 'And that is all, go ahead and start adding tours to your applications.' } }
37 | ],
38 | // onDestroyStarted is called when the user tries to exit the tour
39 | onDestroyStarted: () => {
40 | if (!driverObj.hasNextStep() || confirm("Are you sure?")) {
41 | driverObj.destroy();
42 | }
43 | },
44 | });
45 |
46 | driverObj.drive();
47 | ```
48 |
49 |
50 | > **Note:** By overriding the `onDestroyStarted` hook, you are responsible for calling `driverObj.destroy()` to exit the tour.
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | Driver.js
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Powerful, highly customizable vanilla JavaScript engine to drive user's focus on the page
17 | No external dependencies, light-weight, supports all major browsers and highly customizable
18 |
19 |
20 |
21 |
22 | - **Simple**: is simple to use and has no external dependency at all
23 | - **Light-weight**: is just 5kb gzipped as compared to other libraries which are +12kb gzipped
24 | - **Highly customizable**: has a powerful API and can be used however you want
25 | - **Highlight anything**: highlight any (literally any) element on page
26 | - **Feature introductions**: create powerful feature introductions for your web applications
27 | - **Focus shifters**: add focus shifters for users
28 | - **User friendly**: Everything is controllable by keyboard
29 | - **TypeScript**: Written in TypeScript
30 | - **Consistent behavior**: usable across all browsers
31 | - **MIT Licensed**: free for personal and commercial use
32 |
33 |
34 |
35 | ## Documentation
36 |
37 | For demos and documentation, visit [driverjs.com](https://driverjs.com)
38 |
39 |
40 |
41 | ## So, yet another tour library?
42 |
43 | **No**, it's more than a tour library. **Tours are just one of the many use-cases**. Driver.js can be used wherever you need some sort of overlay for the page; some common usecases could be: [highlighting a page component](https://i.imgur.com/TS0LSK9.png) when user is interacting with some component to keep them focused, providing contextual help e.g. popover with dimmed background when user is filling a form, using it as a focus shifter to bring user's attention to some component on page, using it to simulate those "Turn off the Lights" widgets that you might have seen on video players online, usage as a simple modal, and of-course product tours etc.
44 |
45 | Driver.js is written in Vanilla TypeScript, has zero dependencies and is highly customizable. It has several options allowing you to change how it behaves and also **provides you the hooks** to manipulate the elements as they are highlighted, about to be highlighted, or deselected.
46 |
47 | > Also, comparing the size of Driver.js with other libraries, it's the most light-weight, it is **just ~5kb gzipped** while others are 12kb+.
48 |
49 |
50 |
51 | ## Contributions
52 |
53 | Feel free to submit pull requests, create issues or spread the word.
54 |
55 | ## License
56 |
57 | MIT © [Kamran Ahmed](https://twitter.com/kamrify)
58 |
--------------------------------------------------------------------------------
/docs/src/content/guides/theming.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Theming"
3 | groupTitle: "Introduction"
4 | sort: 5
5 | ---
6 |
7 | You can customize the look and feel of the driver by adding custom class to popover or applying CSS to different classes used by driver.js.
8 |
9 | ## Styling Popover
10 |
11 | You can set the `popoverClass` option globally in the driver configuration or at the step level to apply custom class to the popover and then use CSS to apply styles.
12 |
13 | ```js
14 | const driverObj = driver({
15 | popoverClass: 'my-custom-popover-class'
16 | });
17 |
18 | // or you can also have different classes for different steps
19 | const driverObj2 = driver({
20 | steps: [
21 | {
22 | element: '#some-element',
23 | popover: {
24 | title: 'Title',
25 | description: 'Description',
26 | popoverClass: 'my-custom-popover-class'
27 | }
28 | }
29 | ],
30 | })
31 | ```
32 |
33 | Here is the list of classes applied to the popover which you can use in conjunction with `popoverClass` option to apply custom styles on the popover.
34 |
35 | ```css
36 | /* Class assigned to popover wrapper */
37 | .driver-popover {}
38 |
39 | /* Arrow pointing towards the highlighted element */
40 | .driver-popover-arrow {}
41 |
42 | /* Title and description */
43 | .driver-popover-title {}
44 | .driver-popover-description {}
45 |
46 | /* Close button displayed on the top right corner */
47 | .driver-popover-close-btn {}
48 |
49 | /* Footer of the popover displaying progress and navigation buttons */
50 | .driver-popover-footer {}
51 | .driver-popover-progress-text {}
52 | .driver-popover-prev-btn {}
53 | .driver-popover-next-btn {}
54 | ```
55 |
56 | Visit the [example page](/docs/styling-popover) for an example that modifies the popover styles.
57 |
58 | ## Modifying Popover DOM
59 |
60 | Alternatively, you can also use the `onPopoverRender` hook to modify the popover DOM before it is displayed. The hook is called with the popover DOM as the first argument.
61 |
62 | ```typescript
63 | type PopoverDOM = {
64 | wrapper: HTMLElement;
65 | arrow: HTMLElement;
66 | title: HTMLElement;
67 | description: HTMLElement;
68 | footer: HTMLElement;
69 | progress: HTMLElement;
70 | previousButton: HTMLElement;
71 | nextButton: HTMLElement;
72 | closeButton: HTMLElement;
73 | footerButtons: HTMLElement;
74 | };
75 |
76 | onPopoverRender?: (popover: PopoverDOM, opts: { config: Config; state: State }) => void;
77 | ```
78 |
79 | ## Styling Page
80 |
81 | Following classes are applied to the page when the driver is active.
82 |
83 | ```css
84 | /* Applied to the `body` when the driver: */
85 | .driver-active {} /* is active */
86 | .driver-fade {} /* is animated */
87 | .driver-simple {} /* is not animated */
88 | ```
89 |
90 | Following classes are applied to the overlay i.e. the lightbox displayed over the page.
91 |
92 | ```css
93 | .driver-overlay {}
94 | ```
95 |
96 | ## Styling Highlighted Element
97 |
98 | Whenever an element is highlighted, the following classes are applied to it.
99 |
100 | ```css
101 | .driver-active-element {}
102 | ```
--------------------------------------------------------------------------------
/docs/src/content/guides/basic-usage.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Basic Usage"
3 | groupTitle: "Introduction"
4 | sort: 2
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | Once installed, you can import and start using the library. There are several different configuration options available to customize the library. You can find more details about the options in the [configuration section](/docs/configuration). Given below are the basic steps to get started.
10 |
11 | Here is a simple example of how to create a tour with multiple steps.
12 |
13 |
28 | ```js
29 | import { driver } from "driver.js";
30 | import "driver.js/dist/driver.css";
31 |
32 | const driverObj = driver({
33 | showProgress: true,
34 | steps: [
35 | { element: '.page-header', popover: { title: 'Title', description: 'Description' } },
36 | { element: '.top-nav', popover: { title: 'Title', description: 'Description' } },
37 | { element: '.sidebar', popover: { title: 'Title', description: 'Description' } },
38 | { element: '.footer', popover: { title: 'Title', description: 'Description' } },
39 | ]
40 | });
41 |
42 | driverObj.drive();
43 | ```
44 |
45 |
46 | You can pass a single step configuration to the `highlight` method to highlight a single element. Given below is a simple example of how to highlight a single element.
47 |
48 |
55 | ```js
56 | import { driver } from "driver.js";
57 | import "driver.js/dist/driver.css";
58 |
59 | const driverObj = driver();
60 | driverObj.highlight({
61 | element: '#some-element',
62 | popover: {
63 | title: 'Title for the Popover',
64 | description: 'Description for it',
65 | },
66 | });
67 | ```
68 |
69 |
70 | The same configuration passed to the `highlight` method can be used to create a tour. Given below is a simple example of how to create a tour with a single step.
71 |
72 | Examples above show the basic usage of the library. Find more details about the configuration options in the [configuration section](/docs/configuration) and the examples in the [examples section](/docs/examples).
--------------------------------------------------------------------------------
/docs/src/content/guides/async-tour.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Async Tour"
3 | groupTitle: "Examples"
4 | sort: 3
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | You can also have async steps in your tour. This is useful when you want to load some data from the server and then show the tour.
10 |
11 |
39 | ```js
40 | import { driver } from "driver.js";
41 | import "driver.js/dist/driver.css";
42 |
43 | const driverObj = driver({
44 | showProgress: true,
45 | steps: [
46 | {
47 | popover: {
48 | title: 'First Step',
49 | description: 'This is the first step. Next element will be loaded dynamically.'
50 | // By passing onNextClick, you can override the default behavior of the next button.
51 | // This will prevent the driver from moving to the next step automatically.
52 | // You can then manually call driverObj.moveNext() to move to the next step.
53 | onNextClick: () => {
54 | // .. load element dynamically
55 | // .. and then call
56 | driverObj.moveNext();
57 | },
58 | },
59 | },
60 | {
61 | element: '.dynamic-el',
62 | popover: {
63 | title: 'Async Element',
64 | description: 'This element is loaded dynamically.'
65 | },
66 | // onDeselected is called when the element is deselected.
67 | // Here we are simply removing the element from the DOM.
68 | onDeselected: () => {
69 | // .. remove element
70 | document.querySelector(".dynamic-el")?.remove();
71 | }
72 | },
73 | { popover: { title: 'Last Step', description: 'This is the last step.' } }
74 | ]
75 |
76 | });
77 |
78 | driverObj.drive();
79 |
80 | ```
81 |
82 |
83 | > **Note**: By overriding `onNextClick`, and `onPrevClick` hooks you control the navigation of the driver. This means that user won't be able to navigate using the buttons and you will have to either call `driverObj.moveNext()` or `driverObj.movePrevious()` to navigate to the next/previous step.
84 | >
85 | > You can use this to implement custom logic for navigating between steps. This is also useful when you are dealing with dynamic content and want to highlight the next/previous element based on some logic.
86 | >
87 | > `onNextClick` and `onPrevClick` hooks can be configured at driver level as well as step level. When configured at the driver level, you control the navigation for all the steps. When configured at the step level, you control the navigation for that particular step only.
88 |
--------------------------------------------------------------------------------
/docs/src/content/guides/animated-tour.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Animated Tour"
3 | groupTitle: "Examples"
4 | sort: 2
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | The following example shows how to create a simple tour with a few steps. Click the button below the code sample to see the tour in action.
10 |
11 |
31 | ```js
32 | import { driver } from "driver.js";
33 | import "driver.js/dist/driver.css";
34 |
35 | const driverObj = driver({
36 | showProgress: true,
37 | steps: [
38 | { element: '#tour-example', popover: { title: 'Animated Tour Example', description: 'Here is the code example showing animated tour. Let\'s walk you through it.', side: "left", align: 'start' }},
39 | { element: 'code .line:nth-child(1)', popover: { title: 'Import the Library', description: 'It works the same in vanilla JavaScript as well as frameworks.', side: "bottom", align: 'start' }},
40 | { element: 'code .line:nth-child(2)', popover: { title: 'Importing CSS', description: 'Import the CSS which gives you the default styling for popover and overlay.', side: "bottom", align: 'start' }},
41 | { element: 'code .line:nth-child(4) span:nth-child(7)', popover: { title: 'Create Driver', description: 'Simply call the driver function to create a driver.js instance', side: "left", align: 'start' }},
42 | { element: 'code .line:nth-child(18)', popover: { title: 'Start Tour', description: 'Call the drive method to start the tour and your tour will be started.', side: "top", align: 'start' }},
43 | { element: 'a[href="/docs/configuration"]', popover: { title: 'More Configuration', description: 'Look at this page for all the configuration options you can pass.', side: "right", align: 'start' }},
44 | { popover: { title: 'Happy Coding', description: 'And that is all, go ahead and start adding tours to your applications.' } }
45 | ]
46 | });
47 |
48 | driverObj.drive();
49 | ```
50 |
51 |
--------------------------------------------------------------------------------
/docs/src/content/guides/static-tour.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Static Tour"
3 | groupTitle: "Examples"
4 | sort: 2
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | You can simply set `animate` option to `false` to make the tour static. This will make the tour not animate between steps and will just show the popover.
10 |
11 |
31 | ```js
32 | import { driver } from "driver.js";
33 | import "driver.js/dist/driver.css";
34 |
35 | const driverObj = driver({
36 | animate: false,
37 | showProgress: false,
38 | showButtons: ['next', 'previous', 'close'],
39 | steps: [
40 | { element: '#tour-example', popover: { title: 'Animated Tour Example', description: 'Here is the code example showing animated tour. Let\'s walk you through it.', side: "left", align: 'start' }},
41 | { element: 'code .line:nth-child(1)', popover: { title: 'Import the Library', description: 'It works the same in vanilla JavaScript as well as frameworks.', side: "bottom", align: 'start' }},
42 | { element: 'code .line:nth-child(2)', popover: { title: 'Importing CSS', description: 'Import the CSS which gives you the default styling for popover and overlay.', side: "bottom", align: 'start' }},
43 | { element: 'code .line:nth-child(4) span:nth-child(7)', popover: { title: 'Create Driver', description: 'Simply call the driver function to create a driver.js instance', side: "left", align: 'start' }},
44 | { element: 'code .line:nth-child(18)', popover: { title: 'Start Tour', description: 'Call the drive method to start the tour and your tour will be started.', side: "top", align: 'start' }},
45 | { element: '#docs-sidebar a[href="/docs/configuration"]', popover: { title: 'More Configuration', description: 'Look at this page for all the configuration options you can pass.', side: "right", align: 'start' }},
46 | { popover: { title: 'Happy Coding', description: 'And that is all, go ahead and start adding tours to your applications.' } }
47 | ]
48 | });
49 |
50 | driverObj.drive();
51 | ```
52 |
53 |
--------------------------------------------------------------------------------
/docs/src/layouts/BaseLayout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Analytics from "../components/Analytics/Analytics.astro";
3 | export interface Props {
4 | permalink?: string;
5 | title?: string;
6 | description?: string;
7 | }
8 |
9 | const {
10 | permalink = "",
11 | title = "driver.js",
12 | description = "A light-weight, no-dependency, vanilla JavaScript library to drive user's focus across the page.",
13 | } = Astro.props;
14 | ---
15 |
16 |
17 |
18 |
19 |
20 |
21 | {title}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/src/events.ts:
--------------------------------------------------------------------------------
1 | import { refreshActiveHighlight } from "./highlight";
2 | import { emit } from "./emitter";
3 | import { getState, setState } from "./state";
4 | import { getConfig } from "./config";
5 | import { getFocusableElements } from "./utils";
6 |
7 | export function requireRefresh() {
8 | const resizeTimeout = getState("__resizeTimeout");
9 | if (resizeTimeout) {
10 | window.cancelAnimationFrame(resizeTimeout);
11 | }
12 |
13 | setState("__resizeTimeout", window.requestAnimationFrame(refreshActiveHighlight));
14 | }
15 |
16 | function trapFocus(e: KeyboardEvent) {
17 | const isActivated = getState("isInitialized");
18 | if (!isActivated) {
19 | return;
20 | }
21 |
22 | const isTabKey = e.key === "Tab" || e.keyCode === 9;
23 | if (!isTabKey) {
24 | return;
25 | }
26 |
27 | const activeElement = getState("__activeElement");
28 | const popoverEl = getState("popover")?.wrapper;
29 |
30 | const focusableEls = getFocusableElements([
31 | ...(popoverEl ? [popoverEl] : []),
32 | ...(activeElement ? [activeElement] : []),
33 | ]);
34 |
35 | const firstFocusableEl = focusableEls[0];
36 | const lastFocusableEl = focusableEls[focusableEls.length - 1];
37 |
38 | e.preventDefault();
39 |
40 | if (e.shiftKey) {
41 | const previousFocusableEl =
42 | focusableEls[focusableEls.indexOf(document.activeElement as HTMLElement) - 1] || lastFocusableEl;
43 | previousFocusableEl?.focus();
44 | } else {
45 | const nextFocusableEl =
46 | focusableEls[focusableEls.indexOf(document.activeElement as HTMLElement) + 1] || firstFocusableEl;
47 | nextFocusableEl?.focus();
48 | }
49 | }
50 |
51 | function onKeyup(e: KeyboardEvent) {
52 | const allowKeyboardControl = getConfig("allowKeyboardControl") ?? true;
53 |
54 | if (!allowKeyboardControl) {
55 | return;
56 | }
57 |
58 | if (e.key === "Escape") {
59 | emit("escapePress");
60 | } else if (e.key === "ArrowRight") {
61 | emit("arrowRightPress");
62 | } else if (e.key === "ArrowLeft") {
63 | emit("arrowLeftPress");
64 | }
65 | }
66 |
67 | /**
68 | * Attaches click handler to the elements created by driver.js. It makes
69 | * sure to give the listener the first chance to handle the event, and
70 | * prevents all other pointer-events to make sure no external-library
71 | * ever knows the click happened.
72 | *
73 | * @param {Element} element Element to listen for click events
74 | * @param {(pointer: MouseEvent | PointerEvent) => void} listener Click handler
75 | * @param {(target: HTMLElement) => boolean} shouldPreventDefault Whether to prevent default action i.e. link clicks etc
76 | */
77 | export function onDriverClick(
78 | element: Element,
79 | listener: (pointer: MouseEvent | PointerEvent) => void,
80 | shouldPreventDefault?: (target: HTMLElement) => boolean
81 | ) {
82 | const listenerWrapper = (e: MouseEvent | PointerEvent, listener?: (pointer: MouseEvent | PointerEvent) => void) => {
83 | const target = e.target as HTMLElement;
84 | if (!element.contains(target)) {
85 | return;
86 | }
87 |
88 | if (!shouldPreventDefault || shouldPreventDefault(target)) {
89 | e.preventDefault();
90 | e.stopPropagation();
91 | e.stopImmediatePropagation();
92 | }
93 |
94 | listener?.(e);
95 | };
96 |
97 | // We want to be the absolute first one to hear about the event
98 | const useCapture = true;
99 |
100 | // Events to disable
101 | document.addEventListener("pointerdown", listenerWrapper, useCapture);
102 | document.addEventListener("mousedown", listenerWrapper, useCapture);
103 | document.addEventListener("pointerup", listenerWrapper, useCapture);
104 | document.addEventListener("mouseup", listenerWrapper, useCapture);
105 |
106 | // Actual click handler
107 | document.addEventListener(
108 | "click",
109 | e => {
110 | listenerWrapper(e, listener);
111 | },
112 | useCapture
113 | );
114 | }
115 |
116 | export function initEvents() {
117 | window.addEventListener("keyup", onKeyup, false);
118 | window.addEventListener("keydown", trapFocus, false);
119 | window.addEventListener("resize", requireRefresh);
120 | window.addEventListener("scroll", requireRefresh);
121 | }
122 |
123 | export function destroyEvents() {
124 | window.removeEventListener("keyup", onKeyup);
125 | window.removeEventListener("resize", requireRefresh);
126 | window.removeEventListener("scroll", requireRefresh);
127 | }
128 |
--------------------------------------------------------------------------------
/docs/src/components/CodeSample.tsx:
--------------------------------------------------------------------------------
1 | import type { Config, DriveStep, PopoverDOM } from "driver.js";
2 | import { driver } from "driver.js";
3 | import "driver.js/dist/driver.css";
4 |
5 | type CodeSampleProps = {
6 | heading?: string;
7 |
8 | config?: Config;
9 | highlight?: DriveStep;
10 | tour?: DriveStep[];
11 |
12 | id?: string;
13 | className?: string;
14 | children?: any;
15 | buttonText?: string;
16 | };
17 |
18 | export function removeDummyElement() {
19 | const el = document.querySelector(".dynamic-el");
20 | if (el) {
21 | el.remove();
22 | }
23 | }
24 |
25 | export function mountDummyElement() {
26 | const newDiv = (document.querySelector(".dynamic-el") || document.createElement("div")) as HTMLElement;
27 |
28 | newDiv.innerHTML = "This is a new Element";
29 | newDiv.style.display = "block";
30 | newDiv.style.padding = "20px";
31 | newDiv.style.backgroundColor = "black";
32 | newDiv.style.color = "white";
33 | newDiv.style.fontSize = "14px";
34 | newDiv.style.position = "fixed";
35 | newDiv.style.top = `${Math.random() * (500 - 30) + 30}px`;
36 | newDiv.style.left = `${Math.random() * (500 - 30) + 30}px`;
37 | newDiv.className = "dynamic-el";
38 |
39 | document.body.appendChild(newDiv);
40 | }
41 |
42 | function attachFirstButton(popover: PopoverDOM) {
43 | const firstButton = document.createElement("button");
44 | firstButton.innerText = "Go to First";
45 | popover.footerButtons.appendChild(firstButton);
46 |
47 | firstButton.addEventListener("click", () => {
48 | window.driverObj.drive(0);
49 | });
50 | }
51 |
52 | export function CodeSample(props: CodeSampleProps) {
53 | const { heading, id, children, buttonText = "Show me an Example", className, config, highlight, tour } = props;
54 |
55 | if (id === "demo-hook-theme") {
56 | config!.onPopoverRender = attachFirstButton;
57 | }
58 |
59 | function onClick() {
60 | if (highlight) {
61 | const driverObj = driver({
62 | ...config,
63 | });
64 |
65 | window.driverObj = driverObj;
66 | driverObj.highlight(highlight);
67 | } else if (tour) {
68 | if (id === "confirm-destroy") {
69 | config!.onDestroyStarted = () => {
70 | if (!driverObj.hasNextStep() || confirm("Are you sure?")) {
71 | driverObj.destroy();
72 | }
73 | };
74 | }
75 |
76 | if (id === "logger-events") {
77 | config!.onNextClick = () => {
78 | console.log("next clicked");
79 | };
80 |
81 | config!.onNextClick = () => {
82 | console.log("Next Button Clicked");
83 | // Implement your own functionality here
84 | driverObj.moveNext();
85 | };
86 | config!.onPrevClick = () => {
87 | console.log("Previous Button Clicked");
88 | // Implement your own functionality here
89 | driverObj.movePrevious();
90 | };
91 | config!.onCloseClick = () => {
92 | console.log("Close Button Clicked");
93 | // Implement your own functionality here
94 | driverObj.destroy();
95 | };
96 | }
97 |
98 | if (tour?.[2]?.popover?.title === "Next Step is Async") {
99 | tour[2].popover.onNextClick = () => {
100 | mountDummyElement();
101 | driverObj.moveNext();
102 | };
103 |
104 | if (tour?.[3]?.element === ".dynamic-el") {
105 | tour[3].onDeselected = () => {
106 | removeDummyElement();
107 | };
108 |
109 | // @ts-ignore
110 | tour[4].popover.onPrevClick = () => {
111 | mountDummyElement();
112 | driverObj.movePrevious();
113 | };
114 |
115 | // @ts-ignore
116 | tour[3].popover.onPrevClick = () => {
117 | removeDummyElement();
118 | driverObj.movePrevious();
119 | };
120 | }
121 | }
122 |
123 | const driverObj = driver({
124 | ...config,
125 | steps: tour,
126 | });
127 |
128 | window.driverObj = driverObj;
129 | driverObj.drive();
130 | }
131 | }
132 |
133 | return (
134 |
135 | {heading &&
{heading}
}
136 | {children &&
{children}
}
137 |
138 | {buttonText}
139 |
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/docs/src/content/guides/tour-progress.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Tour Progress"
3 | groupTitle: "Examples"
4 | sort: 2
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | You can use `showProgress` option to show the progress of the tour. It is shown in the bottom left corner of the screen. There is also `progressText` option which can be used to customize the text shown for the progress.
10 |
11 | Please note that `showProgress` is `false` by default. Also the default text for `progressText` is `{{current}} of {{total}}`. You can use `{{current}}` and `{{total}}` in your `progressText` template to show the current and total steps.
12 |
13 |
28 | ```js
29 | import { driver } from "driver.js";
30 | import "driver.js/dist/driver.css";
31 |
32 | const driverObj = driver({
33 | showProgress: true,
34 | showButtons: ['next', 'previous'],
35 | steps: [
36 | { element: '#tour-example', popover: { title: 'Animated Tour Example', description: 'Here is the code example showing animated tour. Let\'s walk you through it.', side: "left", align: 'start' }},
37 | { element: 'code .line:nth-child(1)', popover: { title: 'Import the Library', description: 'It works the same in vanilla JavaScript as well as frameworks.', side: "bottom", align: 'start' }},
38 | { element: 'code .line:nth-child(2)', popover: { title: 'Importing CSS', description: 'Import the CSS which gives you the default styling for popover and overlay.', side: "bottom", align: 'start' }},
39 | { element: 'code .line:nth-child(4) span:nth-child(7)', popover: { title: 'Create Driver', description: 'Simply call the driver function to create a driver.js instance', side: "left", align: 'start' }},
40 | { element: 'code .line:nth-child(16)', popover: { title: 'Start Tour', description: 'Call the drive method to start the tour and your tour will be started.', side: "top", align: 'start' }},
41 | ]
42 | });
43 |
44 | driverObj.drive();
45 | ```
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/docs/src/content/guides/simple-highlight.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Simple Highlight"
3 | groupTitle: "Examples"
4 | sort: 11
5 | ---
6 |
7 | import { FormHelp } from "../../components/FormHelp.tsx";
8 | import { CodeSample } from "../../components/CodeSample.tsx";
9 |
10 | Product tours is not the only usecase for Driver.js. You can use it to highlight any element on the page and show a popover with a description. This is useful for providing contextual help to the user e.g. help the user fill a form or explain a feature.
11 |
12 | Example below shows how to highlight an element and simply show a popover.
13 |
14 |
31 |
32 | Here is the code for above example:
33 |
34 | ```js
35 | const driverObj = driver({
36 | popoverClass: "driverjs-theme",
37 | stagePadding: 4,
38 | });
39 |
40 | driverObj.highlight({
41 | element: "#highlight-me",
42 | popover: {
43 | side: "bottom",
44 | title: "This is a title",
45 | description: "This is a description",
46 | }
47 | })
48 | ```
49 |
50 | You can also use it to show a simple modal without highlighting any element.
51 |
52 | Yet another highlight example. ",
59 | },
60 | }}
61 | client:load
62 | />
63 |
64 | Here is the code for above example:
65 |
66 | ```js
67 | const driverObj = driver();
68 |
69 | driverObj.highlight({
70 | popover: {
71 | description: "Yet another highlight example. ",
72 | }
73 | })
74 | ```
75 |
76 | Focus on the input below and see how the popover is shown.
77 |
78 |
85 |
86 |
87 |
88 | Here is the code for the above example.
89 |
90 | ```js
91 | const driverObj = driver({
92 | popoverClass: "driverjs-theme",
93 | stagePadding: 0,
94 | onDestroyed: () => {
95 | document?.activeElement?.blur();
96 | }
97 | });
98 |
99 | const nameEl = document.getElementById("name");
100 | const educationEl = document.getElementById("education");
101 | const ageEl = document.getElementById("age");
102 | const addressEl = document.getElementById("address");
103 | const formEl = document.querySelector("form");
104 |
105 | nameEl.addEventListener("focus", () => {
106 | driverObj.highlight({
107 | element: nameEl,
108 | popover: {
109 | title: "Name",
110 | description: "Enter your name here",
111 | },
112 | });
113 | });
114 |
115 | educationEl.addEventListener("focus", () => {
116 | driverObj.highlight({
117 | element: educationEl,
118 | popover: {
119 | title: "Education",
120 | description: "Enter your education here",
121 | },
122 | });
123 | });
124 |
125 | ageEl.addEventListener("focus", () => {
126 | driverObj.highlight({
127 | element: ageEl,
128 | popover: {
129 | title: "Age",
130 | description: "Enter your age here",
131 | },
132 | });
133 | });
134 |
135 | addressEl.addEventListener("focus", () => {
136 | driverObj.highlight({
137 | element: addressEl,
138 | popover: {
139 | title: "Address",
140 | description: "Enter your address here",
141 | },
142 | });
143 | });
144 |
145 | formEl.addEventListener("blur", () => {
146 | driverObj.destroy();
147 | });
148 | ```
--------------------------------------------------------------------------------
/docs/src/components/Features.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Earth, Smartphone, Settings, Feather, Code2, Layers, Keyboard } from "lucide-react";
3 | import Container from "./Container.astro";
4 | ---
5 |
6 |
7 |
8 |
9 |
Nothing else like it
10 |
11 | Lightweight with no external dependencies, supports all major browsers and is highly customizable.
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
Browser Support
20 |
21 | Works in all modern browsers including Chrome, IE9+, Safari, Firefox and Opera
22 |
23 |
24 |
25 |
26 |
27 |
28 |
Mobile Ready
29 |
Works on desktop, tablets and mobile devices
30 |
31 |
32 |
33 |
34 |
35 |
Highly Customizable
36 |
Powerful API that allows you to customize it to your needs
37 |
38 |
39 |
40 |
41 |
42 |
Lightweight
43 |
44 | Only 5KB minified, compared to other libraries which are typically >12KB minified
45 |
46 |
47 |
48 |
49 |
50 |
51 |
No Dependencies
52 |
Simple to use with absolutely no external dependencies
53 |
54 |
55 |
56 |
57 |
58 |
Feature Rich
59 |
Create powerful feature introductions for your web applications
60 |
61 |
62 |
63 | MIT
64 |
65 |
MIT License
66 |
Free for both personal and commercial use
67 |
68 |
69 |
70 |
71 |
72 |
Keyboard Control
73 |
All actions can be controlled via keyboard
74 |
75 |
76 |
77 | ALL
78 |
79 |
Highlight Anything
80 |
Highlight any element on the page
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/docs/public/thumbs.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/driver.css:
--------------------------------------------------------------------------------
1 | .driver-active .driver-overlay {
2 | pointer-events: none;
3 | }
4 |
5 | .driver-active * {
6 | pointer-events: none;
7 | }
8 |
9 | .driver-active .driver-active-element,
10 | .driver-active .driver-active-element *,
11 | .driver-popover,
12 | .driver-popover * {
13 | pointer-events: auto;
14 | }
15 |
16 | @keyframes animate-fade-in {
17 | 0% {
18 | opacity: 0;
19 | }
20 |
21 | to {
22 | opacity: 1;
23 | }
24 | }
25 |
26 | .driver-fade .driver-overlay {
27 | animation: animate-fade-in 200ms ease-in-out;
28 | }
29 |
30 | .driver-fade .driver-popover {
31 | animation: animate-fade-in 200ms;
32 | }
33 |
34 | /* Popover styles */
35 | .driver-popover {
36 | all: unset;
37 | box-sizing: border-box;
38 | color: #2d2d2d;
39 | margin: 0;
40 | padding: 15px;
41 | border-radius: 5px;
42 | min-width: 250px;
43 | max-width: 300px;
44 | box-shadow: 0 1px 10px #0006;
45 | z-index: 1000000000;
46 | position: fixed;
47 | top: 0;
48 | right: 0;
49 | background-color: #fff;
50 | }
51 |
52 | .driver-popover * {
53 | font-family: "Helvetica Neue", Inter, ui-sans-serif, "Apple Color Emoji", Helvetica, Arial, sans-serif;
54 | }
55 |
56 | .driver-popover-title {
57 | font: 19px / normal sans-serif;
58 | font-weight: 700;
59 | display: block;
60 | position: relative;
61 | line-height: 1.5;
62 | zoom: 1;
63 | margin: 0;
64 | }
65 |
66 | .driver-popover-close-btn {
67 | all: unset;
68 | position: absolute;
69 | top: 0;
70 | right: 0;
71 | width: 32px;
72 | height: 28px;
73 | cursor: pointer;
74 | font-size: 18px;
75 | font-weight: 500;
76 | color: #d2d2d2;
77 | z-index: 1;
78 | text-align: center;
79 | transition: color;
80 | transition-duration: 200ms;
81 | }
82 |
83 | .driver-popover-close-btn:hover,
84 | .driver-popover-close-btn:focus {
85 | color: #2d2d2d;
86 | }
87 |
88 | .driver-popover-title[style*="block"] + .driver-popover-description {
89 | margin-top: 5px;
90 | }
91 |
92 | .driver-popover-description {
93 | margin-bottom: 0;
94 | font: 14px / normal sans-serif;
95 | line-height: 1.5;
96 | font-weight: 400;
97 | zoom: 1;
98 | }
99 |
100 | .driver-popover-footer {
101 | margin-top: 15px;
102 | text-align: right;
103 | zoom: 1;
104 | display: flex;
105 | align-items: center;
106 | justify-content: space-between;
107 | }
108 |
109 | .driver-popover-progress-text {
110 | font-size: 13px;
111 | font-weight: 400;
112 | color: #727272;
113 | zoom: 1;
114 | }
115 |
116 | .driver-popover-footer button {
117 | all: unset;
118 | display: inline-block;
119 | box-sizing: border-box;
120 | padding: 3px 7px;
121 | text-decoration: none;
122 | text-shadow: 1px 1px 0 #fff;
123 | background-color: #ffffff;
124 | color: #2d2d2d;
125 | font: 12px / normal sans-serif;
126 | cursor: pointer;
127 | outline: 0;
128 | zoom: 1;
129 | line-height: 1.3;
130 | border: 1px solid #ccc;
131 | border-radius: 3px;
132 | }
133 |
134 | .driver-popover-footer .driver-popover-btn-disabled {
135 | opacity: 0.5;
136 | pointer-events: none;
137 | }
138 |
139 | /* Disable the scrolling of parent element if it has an active element*/
140 | :not(body):has(> .driver-active-element) {
141 | overflow: hidden !important;
142 | }
143 |
144 | .driver-no-interaction, .driver-no-interaction * {
145 | pointer-events: none !important;
146 | }
147 |
148 | .driver-popover-footer button:hover,
149 | .driver-popover-footer button:focus {
150 | background-color: #f7f7f7;
151 | }
152 |
153 | .driver-popover-navigation-btns {
154 | display: flex;
155 | flex-grow: 1;
156 | justify-content: flex-end;
157 | }
158 |
159 | .driver-popover-navigation-btns button + button {
160 | margin-left: 4px;
161 | }
162 |
163 | .driver-popover-arrow {
164 | content: "";
165 | position: absolute;
166 | border: 5px solid #fff;
167 | }
168 |
169 | .driver-popover-arrow-side-over {
170 | display: none;
171 | }
172 |
173 | /** Popover Arrow Sides **/
174 | .driver-popover-arrow-side-left {
175 | left: 100%;
176 | border-right-color: transparent;
177 | border-bottom-color: transparent;
178 | border-top-color: transparent;
179 | }
180 |
181 | .driver-popover-arrow-side-right {
182 | right: 100%;
183 | border-left-color: transparent;
184 | border-bottom-color: transparent;
185 | border-top-color: transparent;
186 | }
187 |
188 | .driver-popover-arrow-side-top {
189 | top: 100%;
190 | border-right-color: transparent;
191 | border-bottom-color: transparent;
192 | border-left-color: transparent;
193 | }
194 |
195 | .driver-popover-arrow-side-bottom {
196 | bottom: 100%;
197 | border-left-color: transparent;
198 | border-top-color: transparent;
199 | border-right-color: transparent;
200 | }
201 |
202 | .driver-popover-arrow-side-center {
203 | display: none;
204 | }
205 |
206 | /* Left/Start + Right/Start */
207 | .driver-popover-arrow-side-left.driver-popover-arrow-align-start,
208 | .driver-popover-arrow-side-right.driver-popover-arrow-align-start {
209 | top: 15px;
210 | }
211 |
212 | /* Top/Start + Bottom/Start */
213 | .driver-popover-arrow-side-top.driver-popover-arrow-align-start,
214 | .driver-popover-arrow-side-bottom.driver-popover-arrow-align-start {
215 | left: 15px;
216 | }
217 |
218 | /* End/Left + End/Right */
219 | .driver-popover-arrow-align-end.driver-popover-arrow-side-left,
220 | .driver-popover-arrow-align-end.driver-popover-arrow-side-right {
221 | bottom: 15px;
222 | }
223 |
224 | /* Top/End + Bottom/End */
225 | .driver-popover-arrow-side-top.driver-popover-arrow-align-end,
226 | .driver-popover-arrow-side-bottom.driver-popover-arrow-align-end {
227 | right: 15px;
228 | }
229 |
230 | /* Left/Center + Right/Center */
231 | .driver-popover-arrow-side-left.driver-popover-arrow-align-center,
232 | .driver-popover-arrow-side-right.driver-popover-arrow-align-center {
233 | top: 50%;
234 | margin-top: -5px;
235 | }
236 |
237 | /* Top/Center + Bottom/Center */
238 | .driver-popover-arrow-side-top.driver-popover-arrow-align-center,
239 | .driver-popover-arrow-side-bottom.driver-popover-arrow-align-center {
240 | left: 50%;
241 | margin-left: -5px;
242 | }
243 |
244 | /* No arrow */
245 | .driver-popover-arrow-none {
246 | display: none;
247 | }
248 |
--------------------------------------------------------------------------------
/docs/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/docs/public/driver-head.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/overlay.ts:
--------------------------------------------------------------------------------
1 | import { easeInOutQuad } from "./utils";
2 | import { onDriverClick } from "./events";
3 | import { emit } from "./emitter";
4 | import { getConfig } from "./config";
5 | import { getState, setState } from "./state";
6 |
7 | export type StageDefinition = {
8 | x: number;
9 | y: number;
10 | width: number;
11 | height: number;
12 | };
13 |
14 | // This method calculates the animated new position of the
15 | // stage (called for each frame by requestAnimationFrame)
16 | export function transitionStage(elapsed: number, duration: number, from: Element, to: Element) {
17 | let activeStagePosition = getState("__activeStagePosition");
18 |
19 | const fromDefinition = activeStagePosition ? activeStagePosition : from.getBoundingClientRect();
20 | const toDefinition = to.getBoundingClientRect();
21 |
22 | const x = easeInOutQuad(elapsed, fromDefinition.x, toDefinition.x - fromDefinition.x, duration);
23 | const y = easeInOutQuad(elapsed, fromDefinition.y, toDefinition.y - fromDefinition.y, duration);
24 | const width = easeInOutQuad(elapsed, fromDefinition.width, toDefinition.width - fromDefinition.width, duration);
25 | const height = easeInOutQuad(elapsed, fromDefinition.height, toDefinition.height - fromDefinition.height, duration);
26 |
27 | activeStagePosition = {
28 | x,
29 | y,
30 | width,
31 | height,
32 | };
33 |
34 | renderOverlay(activeStagePosition);
35 | setState("__activeStagePosition", activeStagePosition);
36 | }
37 |
38 | export function trackActiveElement(element: Element) {
39 | if (!element) {
40 | return;
41 | }
42 |
43 | const definition = element.getBoundingClientRect();
44 |
45 | const activeStagePosition: StageDefinition = {
46 | x: definition.x,
47 | y: definition.y,
48 | width: definition.width,
49 | height: definition.height,
50 | };
51 |
52 | setState("__activeStagePosition", activeStagePosition);
53 |
54 | renderOverlay(activeStagePosition);
55 | }
56 |
57 | export function refreshOverlay() {
58 | const activeStagePosition = getState("__activeStagePosition");
59 | const overlaySvg = getState("__overlaySvg");
60 |
61 | if (!activeStagePosition) {
62 | return;
63 | }
64 |
65 | if (!overlaySvg) {
66 | console.warn("No stage svg found.");
67 | return;
68 | }
69 |
70 | const windowX = window.innerWidth;
71 | const windowY = window.innerHeight;
72 |
73 | overlaySvg.setAttribute("viewBox", `0 0 ${windowX} ${windowY}`);
74 | }
75 |
76 | function mountOverlay(stagePosition: StageDefinition) {
77 | const overlaySvg = createOverlaySvg(stagePosition);
78 | document.body.appendChild(overlaySvg);
79 |
80 | onDriverClick(overlaySvg, e => {
81 | const target = e.target as SVGElement;
82 | if (target.tagName !== "path") {
83 | return;
84 | }
85 |
86 | emit("overlayClick");
87 | });
88 |
89 | setState("__overlaySvg", overlaySvg);
90 | }
91 |
92 | function renderOverlay(stagePosition: StageDefinition) {
93 | const overlaySvg = getState("__overlaySvg");
94 |
95 | // TODO: cancel rendering if element is not visible
96 | if (!overlaySvg) {
97 | mountOverlay(stagePosition);
98 |
99 | return;
100 | }
101 |
102 | const pathElement = overlaySvg.firstElementChild as SVGPathElement | null;
103 | if (pathElement?.tagName !== "path") {
104 | throw new Error("no path element found in stage svg");
105 | }
106 |
107 | pathElement.setAttribute("d", generateStageSvgPathString(stagePosition));
108 | }
109 |
110 | function createOverlaySvg(stage: StageDefinition): SVGSVGElement {
111 | const windowX = window.innerWidth;
112 | const windowY = window.innerHeight;
113 |
114 | const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
115 | svg.classList.add("driver-overlay", "driver-overlay-animated");
116 |
117 | svg.setAttribute("viewBox", `0 0 ${windowX} ${windowY}`);
118 | svg.setAttribute("xmlSpace", "preserve");
119 | svg.setAttribute("xmlnsXlink", "http://www.w3.org/1999/xlink");
120 | svg.setAttribute("version", "1.1");
121 | svg.setAttribute("preserveAspectRatio", "xMinYMin slice");
122 |
123 | svg.style.fillRule = "evenodd";
124 | svg.style.clipRule = "evenodd";
125 | svg.style.strokeLinejoin = "round";
126 | svg.style.strokeMiterlimit = "2";
127 | svg.style.zIndex = "10000";
128 | svg.style.position = "fixed";
129 | svg.style.top = "0";
130 | svg.style.left = "0";
131 | svg.style.width = "100%";
132 | svg.style.height = "100%";
133 |
134 | const stagePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
135 |
136 | stagePath.setAttribute("d", generateStageSvgPathString(stage));
137 |
138 | stagePath.style.fill = getConfig("overlayColor") || "rgb(0,0,0)";
139 | stagePath.style.opacity = `${getConfig("overlayOpacity")}`;
140 | stagePath.style.pointerEvents = "auto";
141 | stagePath.style.cursor = "auto";
142 |
143 | svg.appendChild(stagePath);
144 |
145 | return svg;
146 | }
147 |
148 | function generateStageSvgPathString(stage: StageDefinition) {
149 | const windowX = window.innerWidth;
150 | const windowY = window.innerHeight;
151 |
152 | const stagePadding = getConfig("stagePadding") || 0;
153 | const stageRadius = getConfig("stageRadius") || 0;
154 |
155 | const stageWidth = stage.width + stagePadding * 2;
156 | const stageHeight = stage.height + stagePadding * 2;
157 |
158 | // prevent glitches when stage is too small for radius
159 | const limitedRadius = Math.min(stageRadius, stageWidth / 2, stageHeight / 2);
160 |
161 | // no value below 0 allowed + round down
162 | const normalizedRadius = Math.floor(Math.max(limitedRadius, 0));
163 |
164 | const highlightBoxX = stage.x - stagePadding + normalizedRadius;
165 | const highlightBoxY = stage.y - stagePadding;
166 | const highlightBoxWidth = stageWidth - normalizedRadius * 2;
167 | const highlightBoxHeight = stageHeight - normalizedRadius * 2;
168 |
169 | return `M${windowX},0L0,0L0,${windowY}L${windowX},${windowY}L${windowX},0Z
170 | M${highlightBoxX},${highlightBoxY} h${highlightBoxWidth} a${normalizedRadius},${normalizedRadius} 0 0 1 ${normalizedRadius},${normalizedRadius} v${highlightBoxHeight} a${normalizedRadius},${normalizedRadius} 0 0 1 -${normalizedRadius},${normalizedRadius} h-${highlightBoxWidth} a${normalizedRadius},${normalizedRadius} 0 0 1 -${normalizedRadius},-${normalizedRadius} v-${highlightBoxHeight} a${normalizedRadius},${normalizedRadius} 0 0 1 ${normalizedRadius},-${normalizedRadius} z`;
171 | }
172 |
173 | export function destroyOverlay() {
174 | const overlaySvg = getState("__overlaySvg");
175 | if (overlaySvg) {
176 | overlaySvg.remove();
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/docs/src/content/guides/migrating-from-0x.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Migrate to 1.x"
3 | groupTitle: "Introduction"
4 | sort: 6
5 | ---
6 |
7 | Drivers 1.x is a major release that introduces a new API and a new architecture. This page will help you migrate your code from 0.x to 1.x.
8 |
9 | > Change in how you import the library
10 | ```diff
11 | - import Driver from 'driver.js';
12 | - import 'driver.js/dist/driver.min.css';
13 | + import { driver } from 'driver.js';
14 | + import "driver.js/dist/driver.css";
15 | ```
16 |
17 | > Change in how you initialize the library
18 | ```diff
19 | - const driverObj = new Driver(config);
20 | - driverObj.setSteps(steps);
21 |
22 | + // Steps can be passed in the constructor
23 | + const driverObj = driver({
24 | + ...config,
25 | + steps
26 | + });
27 | ```
28 |
29 | > Changes in configuration
30 |
31 | ```diff
32 | const config = {
33 | - overlayClickNext: false, // Option has been removed
34 | - closeBtnText: 'Close', // Option has been removed (close button is now an icon)
35 | - scrollIntoViewOptions: {}, // Option has been renamed
36 | - opacity: 0.75,
37 | + overlayOpacity: 0.75,
38 | - className: 'scoped-class',
39 | + popoverClass: 'scoped-class',
40 | - padding: 10,
41 | + stagePadding: 10,
42 | - showButtons: false,
43 | + showButtons: ['next', 'prev', 'close'], // pass an array of buttons to show
44 | - keyboardControl: true,
45 | + allowKeyboardControl: true,
46 | - onHighlightStarted: (Element) {},
47 | + onHighlightStarted?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void;
48 | - onHighlighted: (Element) {},
49 | + onHighlighted?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void;
50 | - onDeselected: (Element) {}, // Called when element has been deselected
51 | + onDeselected?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void;
52 |
53 | - onReset: (Element) {}, // Called when overlay is about to be cleared
54 | + onDestroyStarted?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void;
55 | + onDestroyed?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void;
56 | + onCloseClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void;
57 |
58 | - onNext: (Element) => {}, // Called when moving to next step on any step
59 | - onPrevious: (Element) => {}, // Called when moving to next step on any step
60 | + // By overriding the default onNextClick and onPrevClick, you control the flow of the driver
61 | + // Visit for more details: https://driverjs.com/docs/configuration
62 | + onNextClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void;
63 | + onPrevClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void;
64 |
65 | + // New options added
66 | + overlayColor?: string;
67 | + stageRadius?: number;
68 | + popoverOffset?: number;
69 | + disableButtons?: ["next", "prev", "close"];
70 | + showProgress?: boolean;
71 | + progressText?: string;
72 | + onPopoverRender?: (popover: PopoverDOM, options: { config: Config; state: State }) => void;
73 | }
74 | ```
75 |
76 | > Changes in step and popover definition
77 |
78 | ```diff
79 | const stepDefinition = {
80 | popover: {
81 | - closeBtnText: 'Close', // Removed, close button is an icon
82 | - element: '.some-element', // Required
83 | + element: '.some-element', // Optional, if not provided, step will be shown as modal
84 | - className: 'popover-class',
85 | + popoverClass: string;
86 | - showButtons: false,
87 | + showButtons: ["next", "previous", "close"]; // Array of buttons to show
88 | - title: ''; // Required
89 | + title: ''; // Optional
90 | - description: ''; // Required
91 | + description: ''; // Optional
92 |
93 | - // position can be left, left-center, left-bottom, top,
94 | - // top-center, top-right, right, right-center, right-bottom,
95 | - // bottom, bottom-center, bottom-right, mid-center
96 | - position: 'left',
97 | + // Now you need to specify the side and align separately
98 | + side?: "top" | "right" | "bottom" | "left";
99 | + align?: "start" | "center" | "end";
100 |
101 | + // New options
102 | + showProgress?: boolean;
103 | + progressText?: string;
104 | + onPopoverRender?: (popover: PopoverDOM, options: { config: Config; state: State }) => void;
105 | + onNextClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void
106 | + onPrevClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void
107 | + onCloseClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void
108 | }
109 |
110 | + // New hook to control the flow of the driver
111 | + onDeselected?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void;
112 | + onHighlightStarted?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void;
113 | + onHighlighted?: (element?: Element, step: DriveStep, options: { config: Config; state: State }) => void;
114 | };
115 | ```
116 |
117 | > Changes in API methods.
118 |
119 | ```diff
120 | - driverObj.preventMove(); // async support is built-in, no longer need to call this
121 | - activeElement.getCalculatedPosition();
122 | - activeElement.hidePopover();
123 | - activeElement.showPopover();
124 | - activeElement.getNode();
125 |
126 | - const isActivated = driverObj.isActivated;
127 | + const isActivated = driverObj.isActive();
128 |
129 | - driverObj.start(stepNumber = 0);
130 | + driverObj.drive(stepNumber = 0);
131 |
132 | - driverObj.highlight(string|stepDefinition);
133 | + driverObj.highlight(stepDefinition)
134 |
135 | - driverObj.reset();
136 | + driverObj.destroy();
137 |
138 | - driverObj.hasHighlightedElement();
139 | + typeof driverObj.getActiveElement() !== 'undefined';
140 |
141 | - driverObj.getHighlightedElement();
142 | + driverObj.getActiveElement();
143 |
144 | - driverObj.getLastHighlightedElement();
145 | + driverObj.getPreviousElement();
146 |
147 | + // New options added
148 | + driverObj.moveTo(stepIndex)
149 | + driverObj.getActiveStep(); // returns the configured step definition
150 | + driverObj.getPreviousStep(); // returns the previous step definition
151 | + driverObj.isLastStep();
152 | + driverObj.isFirstStep();
153 | + driverObj.getState();
154 | + driverObj.getConfig();
155 | + driverObj.setConfig(config);
156 | + driverObj.refresh();
157 | ```
158 |
159 | Please make sure to visit the [documentation](https://driverjs.com/docs/configuration) for more details.
--------------------------------------------------------------------------------
/src/highlight.ts:
--------------------------------------------------------------------------------
1 | import { DriveStep } from "./driver";
2 | import { refreshOverlay, trackActiveElement, transitionStage } from "./overlay";
3 | import { getConfig, getCurrentDriver } from "./config";
4 | import { hidePopover, renderPopover, repositionPopover } from "./popover";
5 | import { bringInView } from "./utils";
6 | import { getState, setState } from "./state";
7 |
8 | function mountDummyElement(): Element {
9 | const existingDummy = document.getElementById("driver-dummy-element");
10 | if (existingDummy) {
11 | return existingDummy;
12 | }
13 |
14 | let element = document.createElement("div");
15 |
16 | element.id = "driver-dummy-element";
17 | element.style.width = "0";
18 | element.style.height = "0";
19 | element.style.pointerEvents = "none";
20 | element.style.opacity = "0";
21 | element.style.position = "fixed";
22 | element.style.top = "50%";
23 | element.style.left = "50%";
24 |
25 | document.body.appendChild(element);
26 |
27 | return element;
28 | }
29 |
30 | export function highlight(step: DriveStep) {
31 | const { element } = step;
32 | let elemObj =
33 | typeof element === "function" ? element() : typeof element === "string" ? document.querySelector(element) : element;
34 |
35 | // If the element is not found, we mount a 1px div
36 | // at the center of the screen to highlight and show
37 | // the popover on top of that. This is to show a
38 | // modal-like highlight.
39 | if (!elemObj) {
40 | elemObj = mountDummyElement();
41 | }
42 |
43 | transferHighlight(elemObj, step);
44 | }
45 |
46 | export function refreshActiveHighlight() {
47 | const activeHighlight = getState("__activeElement");
48 | const activeStep = getState("__activeStep")!;
49 |
50 | if (!activeHighlight) {
51 | return;
52 | }
53 |
54 | trackActiveElement(activeHighlight);
55 | refreshOverlay();
56 | repositionPopover(activeHighlight, activeStep);
57 | }
58 |
59 | function transferHighlight(toElement: Element, toStep: DriveStep) {
60 | const duration = 400;
61 | const start = Date.now();
62 |
63 | const fromStep = getState("__activeStep");
64 | const fromElement = getState("__activeElement") || toElement;
65 |
66 | // If it's the first time we're highlighting an element, we show
67 | // the popover immediately. Otherwise, we wait for the animation
68 | // to finish before showing the popover.
69 | const isFirstHighlight = !fromElement || fromElement === toElement;
70 | const isToDummyElement = toElement.id === "driver-dummy-element";
71 | const isFromDummyElement = fromElement.id === "driver-dummy-element";
72 |
73 | const isAnimatedTour = getConfig("animate");
74 | const highlightStartedHook = toStep.onHighlightStarted || getConfig("onHighlightStarted");
75 | const highlightedHook = toStep?.onHighlighted || getConfig("onHighlighted");
76 | const deselectedHook = fromStep?.onDeselected || getConfig("onDeselected");
77 |
78 | const config = getConfig();
79 | const state = getState();
80 |
81 | if (!isFirstHighlight && deselectedHook) {
82 | deselectedHook(isFromDummyElement ? undefined : fromElement, fromStep!, {
83 | config,
84 | state,
85 | driver: getCurrentDriver(),
86 | });
87 | }
88 |
89 | if (highlightStartedHook) {
90 | highlightStartedHook(isToDummyElement ? undefined : toElement, toStep, {
91 | config,
92 | state,
93 | driver: getCurrentDriver(),
94 | });
95 | }
96 |
97 | const hasDelayedPopover = !isFirstHighlight && isAnimatedTour;
98 | let isPopoverRendered = false;
99 |
100 | hidePopover();
101 |
102 | setState("previousStep", fromStep);
103 | setState("previousElement", fromElement);
104 | setState("activeStep", toStep);
105 | setState("activeElement", toElement);
106 |
107 | const animate = () => {
108 | const transitionCallback = getState("__transitionCallback");
109 |
110 | // This makes sure that the repeated calls to transferHighlight
111 | // don't interfere with each other. Only the last call will be
112 | // executed.
113 | if (transitionCallback !== animate) {
114 | return;
115 | }
116 |
117 | const elapsed = Date.now() - start;
118 | const timeRemaining = duration - elapsed;
119 | const isHalfwayThrough = timeRemaining <= duration / 2;
120 |
121 | if (toStep.popover && isHalfwayThrough && !isPopoverRendered && hasDelayedPopover) {
122 | renderPopover(toElement, toStep);
123 | isPopoverRendered = true;
124 | }
125 |
126 | if (getConfig("animate") && elapsed < duration) {
127 | transitionStage(elapsed, duration, fromElement, toElement);
128 | } else {
129 | trackActiveElement(toElement);
130 |
131 | if (highlightedHook) {
132 | highlightedHook(isToDummyElement ? undefined : toElement, toStep, {
133 | config: getConfig(),
134 | state: getState(),
135 | driver: getCurrentDriver(),
136 | });
137 | }
138 |
139 | setState("__transitionCallback", undefined);
140 | setState("__previousStep", fromStep);
141 | setState("__previousElement", fromElement);
142 | setState("__activeStep", toStep);
143 | setState("__activeElement", toElement);
144 | }
145 |
146 | window.requestAnimationFrame(animate);
147 | };
148 |
149 | setState("__transitionCallback", animate);
150 |
151 | window.requestAnimationFrame(animate);
152 |
153 | bringInView(toElement);
154 | if (!hasDelayedPopover && toStep.popover) {
155 | renderPopover(toElement, toStep);
156 | }
157 |
158 | fromElement.classList.remove("driver-active-element", "driver-no-interaction");
159 | fromElement.removeAttribute("aria-haspopup");
160 | fromElement.removeAttribute("aria-expanded");
161 | fromElement.removeAttribute("aria-controls");
162 |
163 | const disableActiveInteraction = toStep.disableActiveInteraction ?? getConfig("disableActiveInteraction");
164 | if (disableActiveInteraction) {
165 | toElement.classList.add("driver-no-interaction");
166 | }
167 |
168 | toElement.classList.add("driver-active-element");
169 | toElement.setAttribute("aria-haspopup", "dialog");
170 | toElement.setAttribute("aria-expanded", "true");
171 | toElement.setAttribute("aria-controls", "driver-popover-content");
172 | }
173 |
174 | export function destroyHighlight() {
175 | document.getElementById("driver-dummy-element")?.remove();
176 | document.querySelectorAll(".driver-active-element").forEach(element => {
177 | element.classList.remove("driver-active-element", "driver-no-interaction");
178 | element.removeAttribute("aria-haspopup");
179 | element.removeAttribute("aria-expanded");
180 | element.removeAttribute("aria-controls");
181 | });
182 | }
183 |
--------------------------------------------------------------------------------
/docs/src/content/guides/styling-popover.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Styling Popover"
3 | groupTitle: "Examples"
4 | sort: 2
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | You can either use the default class names and override the styles or you can pass a custom class name to the `popoverClass` option either globally or per step.
10 |
11 | Alternatively, if want to modify the Popover DOM, you can use the `onPopoverRender` callback to get the popover DOM element and do whatever you want with it before popover is rendered.
12 |
13 | We have added a few examples below but have a look at the [theming section](/docs/theming#styling-popover) for detailed guide including class names to target etc.
14 |
15 |
57 | ```js
58 | import { driver } from "driver.js";
59 | import "driver.js/dist/driver.css";
60 |
61 | const driverObj = driver({
62 | popoverClass: 'driverjs-theme'
63 | });
64 |
65 | driverObj.highlight({
66 | element: '#demo-theme',
67 | popover: {
68 | title: 'Style However You Want',
69 | description: 'You can use the default class names and override the styles or you can pass a custom class name to the popoverClass option either globally or per step.'
70 | }
71 | });
72 | ```
73 |
74 |
75 | Here is the CSS used for the above example:
76 |
77 | ```css
78 | .driver-popover.driverjs-theme {
79 | background-color: #fde047;
80 | color: #000;
81 | }
82 |
83 | .driver-popover.driverjs-theme .driver-popover-title {
84 | font-size: 20px;
85 | }
86 |
87 | .driver-popover.driverjs-theme .driver-popover-title,
88 | .driver-popover.driverjs-theme .driver-popover-description,
89 | .driver-popover.driverjs-theme .driver-popover-progress-text {
90 | color: #000;
91 | }
92 |
93 | .driver-popover.driverjs-theme button {
94 | flex: 1;
95 | text-align: center;
96 | background-color: #000;
97 | color: #ffffff;
98 | border: 2px solid #000;
99 | text-shadow: none;
100 | font-size: 14px;
101 | padding: 5px 8px;
102 | border-radius: 6px;
103 | }
104 |
105 | .driver-popover.driverjs-theme button:hover {
106 | background-color: #000;
107 | color: #ffffff;
108 | }
109 |
110 | .driver-popover.driverjs-theme .driver-popover-navigation-btns {
111 | justify-content: space-between;
112 | gap: 3px;
113 | }
114 |
115 | .driver-popover.driverjs-theme .driver-popover-close-btn {
116 | color: #9b9b9b;
117 | }
118 |
119 | .driver-popover.driverjs-theme .driver-popover-close-btn:hover {
120 | color: #000;
121 | }
122 |
123 | .driver-popover.driverjs-theme .driver-popover-arrow-side-left.driver-popover-arrow {
124 | border-left-color: #fde047;
125 | }
126 |
127 | .driver-popover.driverjs-theme .driver-popover-arrow-side-right.driver-popover-arrow {
128 | border-right-color: #fde047;
129 | }
130 |
131 | .driver-popover.driverjs-theme .driver-popover-arrow-side-top.driver-popover-arrow {
132 | border-top-color: #fde047;
133 | }
134 |
135 | .driver-popover.driverjs-theme .driver-popover-arrow-side-bottom.driver-popover-arrow {
136 | border-bottom-color: #fde047;
137 | }
138 | ```
139 |
140 |
141 |
142 |
183 | ```js
184 | import { driver } from "driver.js";
185 | import "driver.js/dist/driver.css";
186 |
187 | const driverObj = driver({
188 | // Get full control over the popover rendering.
189 | // Here we are adding a custom button that takes
190 | // the user to the first step.
191 | onPopoverRender: (popover, { config, state }) => {
192 | const firstButton = document.createElement("button");
193 | firstButton.innerText = "Go to First";
194 | popover.footerButtons.appendChild(firstButton);
195 |
196 | firstButton.addEventListener("click", () => {
197 | driverObj.drive(0);
198 | });
199 | },
200 | steps: [
201 | // ..
202 | ]
203 | });
204 |
205 | driverObj.drive();
206 | ```
207 |
--------------------------------------------------------------------------------
/docs/src/content/guides/popover-position.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Popover Position"
3 | groupTitle: "Examples"
4 | sort: 7
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | You can control the popover position using the `side` and `align` options. The `side` option controls the side of the element where the popover will be shown and the `align` option controls the alignment of the popover with the element.
10 |
11 | > **Note:** Popover is intelligent enough to adjust itself to fit in the viewport. So, if you set `side` to `left` and `align` to `start`, but the popover doesn't fit in the viewport, it will automatically adjust itself to fit in the viewport. Consider highlighting and scrolling the browser to the element below to see this in action.
12 |
13 | ```js
14 | import { driver } from "driver.js";
15 | import "driver.js/dist/driver.css";
16 |
17 | const driverObj = driver();
18 | driverObj.highlight({
19 | element: '#left-start',
20 | popover: {
21 | title: 'Animated Tour Example',
22 | description: 'Here is the code example showing animated tour. Let\'s walk you through it.',
23 | side: "left",
24 | align: 'start'
25 | }
26 | });
27 | ```
28 |
29 |
30 |
Use the buttons below to show the popover.
31 |
32 |
33 |
34 | left and align set to start . PS, we can use HTML in the title and descriptions of popover.',
41 | side: "left",
42 | align: 'start'
43 | }
44 | }}
45 | id={"left-start"}
46 | client:load
47 | />
48 |
49 | left and align set to center . PS, we can use HTML in the title and descriptions of popover.',
56 | side: "left",
57 | align: 'center'
58 | }
59 | }}
60 | id={"left-start"}
61 | client:load
62 | />
63 |
64 | left and align set to end . PS, we can use HTML in the title and descriptions of popover.',
71 | side: "left",
72 | align: 'end'
73 | }
74 | }}
75 | id={"left-start"}
76 | client:load
77 | />
78 |
79 | top and align set to start . PS, we can use HTML in the title and descriptions of popover.',
86 | side: "top",
87 | align: 'start'
88 | }
89 | }}
90 | id={"top-start"}
91 | client:load
92 | />
93 |
94 | top and align set to center . PS, we can use HTML in the title and descriptions of popover.',
101 | side: "top",
102 | align: 'center'
103 | }
104 | }}
105 | id={"top-start"}
106 | client:load
107 | />
108 |
109 | top and align set to end . PS, we can use HTML in the title and descriptions of popover.',
116 | side: "top",
117 | align: 'end'
118 | }
119 | }}
120 | id={"top-start"}
121 | client:load
122 | />
123 |
124 | right and align set to start . PS, we can use HTML in the title and descriptions of popover.',
131 | side: "right",
132 | align: 'start'
133 | }
134 | }}
135 | id={"right-start"}
136 | client:load
137 | />
138 |
139 | right and align set to center . PS, we can use HTML in the title and descriptions of popover.',
146 | side: "right",
147 | align: 'center'
148 | }
149 | }}
150 | id={"right-start"}
151 | client:load
152 | />
153 |
154 | right and align set to end . PS, we can use HTML in the title and descriptions of popover.',
161 | side: "right",
162 | align: 'end'
163 | }
164 | }}
165 | id={"right-start"}
166 | client:load
167 | />
168 |
169 | bottom and align set to start . PS, we can use HTML in the title and descriptions of popover.',
176 | side: "bottom",
177 | align: 'start'
178 | }
179 | }}
180 | id={"bottom-start"}
181 | client:load
182 | />
183 |
184 | bottom and align set to center . PS, we can use HTML in the title and descriptions of popover.',
191 | side: "bottom",
192 | align: 'center'
193 | }
194 | }}
195 | id={"bottom-start"}
196 | client:load
197 | />
198 |
199 | bottom and align set to end . PS, we can use HTML in the title and descriptions of popover.',
206 | side: "bottom",
207 | align: 'end'
208 | }
209 | }}
210 | id={"right-start"}
211 | client:load
212 | />
213 |
--------------------------------------------------------------------------------
/docs/src/content/guides/buttons.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Popover Buttons"
3 | groupTitle: "Examples"
4 | sort: 9
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | You can use the `showButtons` option to choose which buttons to show in the popover. The default value is `['next', 'previous', 'close']`.
10 |
11 |
12 | > **Note:** When using the `highlight` method to highlight a single element, the only button shown is the `close`
13 | button. However, you can use the `showButtons` option to show other buttons as well. But the buttons won't do
14 | anything. You will have to use the `onNextClick` and `onPreviousClick` callbacks to implement the functionality.
15 |
16 |
17 |
18 |
45 | ```js
46 | import { driver } from "driver.js";
47 | import "driver.js/dist/driver.css";
48 |
49 | const driverObj = driver({
50 | showButtons: [
51 | 'next',
52 | 'previous',
53 | 'close'
54 | ],
55 | steps: [
56 | {
57 | element: '#first-element',
58 | popover: {
59 | title: 'Popover Title',
60 | description: 'Popover Description'
61 | }
62 | },
63 | {
64 | element: '#second-element',
65 | popover: {
66 | title: 'Popover Title',
67 | description: 'Popover Description'
68 | }
69 | }
70 | ]
71 | });
72 |
73 | driverObj.drive();
74 | ```
75 |
76 |
102 |
127 |
128 |
129 | ## Change Button Text
130 |
131 | You can also change the text of buttons using `nextBtnText`, `prevBtnText` and `doneBtnText` options.
132 |
133 |
134 | ',
140 | prevBtnText: '<--',
141 | doneBtnText: 'X',
142 | }}
143 | tour={[
144 | {
145 | element: '#code-sample-3',
146 | popover: {
147 | title: 'Popover Title',
148 | description: 'Popover Description'
149 | }
150 | },
151 | {
152 | element: '#code-sample-3 code',
153 | popover: {
154 | title: 'Popover Title',
155 | description: 'Popover Description'
156 | }
157 | }
158 | ]}
159 | id={"code-sample-3"}
160 | client:load>
161 | ```js
162 | import { driver } from "driver.js";
163 | import "driver.js/dist/driver.css";
164 |
165 | const driverObj = driver({
166 | nextBtnText: '—›',
167 | prevBtnText: '‹—',
168 | doneBtnText: '✕',
169 | showProgress: true,
170 | steps: [
171 | // ...
172 | ]
173 | });
174 |
175 | driverObj.drive();
176 | ```
177 |
178 |
179 |
180 | ## Event Handlers
181 |
182 | You can use the `onNextClick`, `onPreviousClick` and `onCloseClick` callbacks to implement custom functionality when the user clicks on the next and previous buttons.
183 |
184 | > Please note that when you configure these callbacks, the default functionality of the buttons will be disabled. You will have to implement the functionality yourself.
185 |
186 |
207 | ```js
208 | import { driver } from "driver.js";
209 | import "driver.js/dist/driver.css";
210 |
211 | const driverObj = driver({
212 | onNextClick:() => {
213 | console.log('Next Button Clicked');
214 | // Implement your own functionality here
215 | driverObj.moveNext();
216 | },
217 | onPrevClick:() => {
218 | console.log('Previous Button Clicked');
219 | // Implement your own functionality here
220 | driverObj.movePrevious();
221 | },
222 | onCloseClick:() => {
223 | console.log('Close Button Clicked');
224 | // Implement your own functionality here
225 | driverObj.destroy();
226 | },
227 | steps: [
228 | // ...
229 | ]
230 | });
231 |
232 | driverObj.drive();
233 | ```
234 |
235 |
236 | ## Custom Buttons
237 |
238 | You can add custom buttons using `onPopoverRender` callback. This callback is called before the popover is rendered. In the following example, we are adding a custom button that takes the user to the first step.
239 |
240 |
241 |
281 | ```js
282 | import { driver } from "driver.js";
283 | import "driver.js/dist/driver.css";
284 |
285 | const driverObj = driver({
286 | // Get full control over the popover rendering.
287 | // Here we are adding a custom button that takes
288 | // user to the first step.
289 | onPopoverRender: (popover, { config, state }) => {
290 | const firstButton = document.createElement("button");
291 | firstButton.innerText = "Go to First";
292 | popover.footerButtons.appendChild(firstButton);
293 |
294 | firstButton.addEventListener("click", () => {
295 | driverObj.drive(0);
296 | });
297 | },
298 | steps: [
299 | // ..
300 | ]
301 | });
302 |
303 | driverObj.drive();
304 | ```
305 |
--------------------------------------------------------------------------------
/docs/src/content/guides/configuration.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Configuration"
3 | groupTitle: "Introduction"
4 | sort: 3
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | Driver.js is built to be highly configurable. You can configure the driver globally, or per step. You can also configure the driver on the fly, while it's running.
10 |
11 | > Driver.js is written in TypeScript. Configuration options are mostly self-explanatory. Also, if you're using an IDE like WebStorm or VSCode, you'll get autocomplete and documentation for all the configuration options.
12 |
13 | ## Driver Configuration
14 |
15 | You can configure the driver globally by passing the configuration object to the `driver` call or by using the `setConfig` method. Given below are some of the available configuration options.
16 |
17 | ```typescript
18 | type Config = {
19 | // Array of steps to highlight. You should pass
20 | // this when you want to setup a product tour.
21 | steps?: DriveStep[];
22 |
23 | // Whether to animate the product tour. (default: true)
24 | animate?: boolean;
25 | // Overlay color. (default: black)
26 | // This is useful when you have a dark background
27 | // and want to highlight elements with a light
28 | // background color.
29 | overlayColor?: string;
30 | // Whether to smooth scroll to the highlighted element. (default: false)
31 | smoothScroll?: boolean;
32 | // Whether to allow closing the popover by clicking on the backdrop. (default: true)
33 | allowClose?: boolean;
34 | // Opacity of the backdrop. (default: 0.5)
35 | overlayOpacity?: number;
36 | // What to do when the overlay backdrop is clicked.
37 | // Possible options are 'close' and 'nextStep'. (default: 'close')
38 | overlayClickBehavior?: string,
39 | // Distance between the highlighted element and the cutout. (default: 10)
40 | stagePadding?: number;
41 | // Radius of the cutout around the highlighted element. (default: 5)
42 | stageRadius?: number;
43 |
44 | // Whether to allow keyboard navigation. (default: true)
45 | allowKeyboardControl?: boolean;
46 |
47 | // Whether to disable interaction with the highlighted element. (default: false)
48 | // Can be configured at the step level as well
49 | disableActiveInteraction?: boolean;
50 |
51 | // If you want to add custom class to the popover
52 | popoverClass?: string;
53 | // Distance between the popover and the highlighted element. (default: 10)
54 | popoverOffset?: number;
55 | // Array of buttons to show in the popover. Defaults to ["next", "previous", "close"]
56 | // for product tours and [] for single element highlighting.
57 | showButtons?: AllowedButtons[];
58 | // Array of buttons to disable. This is useful when you want to show some of the
59 | // buttons, but disable some of them.
60 | disableButtons?: AllowedButtons[];
61 |
62 | // Whether to show the progress text in popover. (default: false)
63 | showProgress?: boolean;
64 | // Template for the progress text. You can use the following placeholders in the template:
65 | // - {{current}}: The current step number
66 | // - {{total}}: Total number of steps
67 | progressText?: string;
68 |
69 | // Text to show in the buttons. `doneBtnText`
70 | // is used on the last step of a tour.
71 | nextBtnText?: string;
72 | prevBtnText?: string;
73 | doneBtnText?: string;
74 |
75 | // Called after the popover is rendered.
76 | // PopoverDOM is an object with references to
77 | // the popover DOM elements such as buttons
78 | // title, descriptions, body, container etc.
79 | onPopoverRender?: (popover: PopoverDOM, options: { config: Config; state: State, driver: Driver }) => void;
80 |
81 | // Hooks to run before and after highlighting
82 | // each step. Each hook receives the following
83 | // parameters:
84 | // - element: The target DOM element of the step
85 | // - step: The step object configured for the step
86 | // - options.config: The current configuration options
87 | // - options.state: The current state of the driver
88 | // - options.driver: Current driver object
89 | onHighlightStarted?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void;
90 | onHighlighted?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void;
91 | onDeselected?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void;
92 |
93 | // Hooks to run before and after the driver
94 | // is destroyed. Each hook receives
95 | // the following parameters:
96 | // - element: Currently active element
97 | // - step: The step object configured for the currently active
98 | // - options.config: The current configuration options
99 | // - options.state: The current state of the driver
100 | // - options.driver: Current driver object
101 | onDestroyStarted?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void;
102 | onDestroyed?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void;
103 |
104 | // Hooks to run on button clicks. Each hook receives
105 | // the following parameters:
106 | // - element: The current DOM element of the step
107 | // - step: The step object configured for the step
108 | // - options.config: The current configuration options
109 | // - options.state: The current state of the driver
110 | // - options.driver: Current driver object
111 | onNextClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void;
112 | onPrevClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void;
113 | onCloseClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void;
114 | };
115 | ```
116 |
117 | > **Note**: By overriding `onNextClick`, and `onPrevClick` hooks you control the navigation of the driver. This means that user won't be able to navigate using the buttons and you will have to either call `driverObj.moveNext()` or `driverObj.movePrevious()` to navigate to the next/previous step.
118 | >
119 | > You can use this to implement custom logic for navigating between steps. This is also useful when you are dealing with dynamic content and want to highlight the next/previous element based on some logic.
120 | >
121 | > `onNextClick` and `onPrevClick` hooks can be configured at the step level as well. When configured at the driver level, you control the navigation for all the steps. When configured at the step level, you control the navigation for that particular step only.
122 |
123 | ## Popover Configuration
124 |
125 | The popover is the main UI element of Driver.js. It's the element that highlights the target element, and shows the step content. You can configure the popover globally, or per step. Given below are some of the available configuration options.
126 |
127 | ```typescript
128 | type Popover = {
129 | // Title and descriptions shown in the popover.
130 | // You can use HTML in these. Also, you can
131 | // omit one of these to show only the other.
132 | title?: string;
133 | description?: string;
134 |
135 | // The position and alignment of the popover
136 | // relative to the target element.
137 | side?: "top" | "right" | "bottom" | "left";
138 | align?: "start" | "center" | "end";
139 |
140 | // Array of buttons to show in the popover.
141 | // When highlighting a single element, there
142 | // are no buttons by default. When showing
143 | // a tour, the default buttons are "next",
144 | // "previous" and "close".
145 | showButtons?: ("next" | "previous" | "close")[];
146 | // An array of buttons to disable. This is
147 | // useful when you want to show some of the
148 | // buttons, but disable some of them.
149 | disableButtons?: ("next" | "previous" | "close")[];
150 |
151 | // Text to show in the buttons. `doneBtnText`
152 | // is used on the last step of a tour.
153 | nextBtnText?: string;
154 | prevBtnText?: string;
155 | doneBtnText?: string;
156 |
157 | // Whether to show the progress text in popover.
158 | showProgress?: boolean;
159 | // Template for the progress text. You can use
160 | // the following placeholders in the template:
161 | // - {{current}}: The current step number
162 | // - {{total}}: Total number of steps
163 | // Defaults to following if `showProgress` is true:
164 | // - "{{current}} of {{total}}"
165 | progressText?: string;
166 |
167 | // Custom class to add to the popover element.
168 | // This can be used to style the popover.
169 | popoverClass?: string;
170 |
171 | // Hook to run after the popover is rendered.
172 | // You can modify the popover element here.
173 | // Parameter is an object with references to
174 | // the popover DOM elements such as buttons
175 | // title, descriptions, body, etc.
176 | onPopoverRender?: (popover: PopoverDOM, options: { config: Config; state: State, driver: Driver }) => void;
177 |
178 | // Callbacks for button clicks. You can use
179 | // these to add custom behavior to the buttons.
180 | // Each callback receives the following parameters:
181 | // - element: The current DOM element of the step
182 | // - step: The step object configured for the step
183 | // - options.config: The current configuration options
184 | // - options.state: The current state of the driver
185 | // - options.driver: Current driver object
186 | onNextClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void
187 | onPrevClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void
188 | onCloseClick?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void
189 | }
190 | ```
191 |
192 | ## Drive Step Configuration
193 |
194 | Drive step is the configuration object passed to the `highlight` method or the `steps` array of the `drive` method. You can configure the popover and the target element for each step. Given below are some of the available configuration options.
195 |
196 | ```typescript
197 | type DriveStep = {
198 | // The target element to highlight.
199 | // This can be a DOM element,
200 | // a function that returns a DOM Element, or a CSS selector.
201 | // If this is a selector, the first matching
202 | // element will be highlighted.
203 | element?: Element | string | (() => Element);
204 |
205 | // The popover configuration for this step.
206 | // Look at the Popover Configuration section
207 | popover?: Popover;
208 |
209 | // Whether to disable interaction with the highlighted element. (default: false)
210 | disableActiveInteraction?: boolean;
211 |
212 | // Callback when the current step is deselected,
213 | // about to be highlighted or highlighted.
214 | // Each callback receives the following parameters:
215 | // - element: The current DOM element of the step
216 | // - step: The step object configured for the step
217 | // - options.config: The current configuration options
218 | // - options.state: The current state of the driver
219 | // - options.driver: Current driver object
220 | onDeselected?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void;
221 | onHighlightStarted?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void;
222 | onHighlighted?: (element?: Element, step: DriveStep, options: { config: Config; state: State, driver: Driver }) => void;
223 | }
224 | ```
225 |
226 | ## State
227 |
228 | You can access the current state of the driver by calling the `getState` method. It's also passed to the hooks and callbacks.
229 |
230 | ```typescript
231 | type State = {
232 | // Whether the driver is currently active or not
233 | isInitialized?: boolean;
234 |
235 | // Index of the currently active step if using
236 | // as a product tour and have configured the
237 | // steps array.
238 | activeIndex?: number;
239 | // DOM element of the currently active step
240 | activeElement?: Element;
241 | // Step object of the currently active step
242 | activeStep?: DriveStep;
243 |
244 | // DOM element that was previously active
245 | previousElement?: Element;
246 | // Step object of the previously active step
247 | previousStep?: DriveStep;
248 |
249 | // DOM elements for the popover i.e. including
250 | // container, title, description, buttons etc.
251 | popover?: PopoverDOM;
252 | }
253 | ```
254 |
--------------------------------------------------------------------------------
/src/driver.ts:
--------------------------------------------------------------------------------
1 | import { AllowedButtons, destroyPopover, Popover } from "./popover";
2 | import { destroyOverlay } from "./overlay";
3 | import { destroyEvents, initEvents, requireRefresh } from "./events";
4 | import { Config, configure, DriverHook, getConfig, getCurrentDriver, setCurrentDriver } from "./config";
5 | import { destroyHighlight, highlight } from "./highlight";
6 | import { destroyEmitter, listen } from "./emitter";
7 | import { getState, resetState, setState } from "./state";
8 | import "./driver.css";
9 |
10 | export type DriveStep = {
11 | element?: string | Element | (() => Element);
12 | onHighlightStarted?: DriverHook;
13 | onHighlighted?: DriverHook;
14 | onDeselected?: DriverHook;
15 | popover?: Popover;
16 | disableActiveInteraction?: boolean;
17 | };
18 |
19 | export interface Driver {
20 | isActive: () => boolean;
21 | refresh: () => void;
22 | drive: (stepIndex?: number) => void;
23 | setConfig: (config: Config) => void;
24 | setSteps: (steps: DriveStep[]) => void;
25 | getConfig: () => Config;
26 | getState: (key?: string) => any;
27 | getActiveIndex: () => number | undefined;
28 | isFirstStep: () => boolean;
29 | isLastStep: () => boolean;
30 | getActiveStep: () => DriveStep | undefined;
31 | getActiveElement: () => Element | undefined;
32 | getPreviousElement: () => Element | undefined;
33 | getPreviousStep: () => DriveStep | undefined;
34 | moveNext: () => void;
35 | movePrevious: () => void;
36 | moveTo: (index: number) => void;
37 | hasNextStep: () => boolean;
38 | hasPreviousStep: () => boolean;
39 | highlight: (step: DriveStep) => void;
40 | destroy: () => void;
41 | }
42 |
43 | export function driver(options: Config = {}): Driver {
44 | configure(options);
45 |
46 | function handleClose() {
47 | if (!getConfig("allowClose")) {
48 | return;
49 | }
50 |
51 | destroy();
52 | }
53 |
54 | function handleOverlayClick() {
55 | const overlayClickBehavior = getConfig("overlayClickBehavior");
56 |
57 | if (getConfig("allowClose") && overlayClickBehavior === "close") {
58 | destroy();
59 | return;
60 | }
61 |
62 | if (overlayClickBehavior === "nextStep") {
63 | moveNext();
64 | }
65 | }
66 |
67 | function moveNext() {
68 | const activeIndex = getState("activeIndex");
69 | const steps = getConfig("steps") || [];
70 | if (typeof activeIndex === "undefined") {
71 | return;
72 | }
73 |
74 | const nextStepIndex = activeIndex + 1;
75 | if (steps[nextStepIndex]) {
76 | drive(nextStepIndex);
77 | } else {
78 | destroy();
79 | }
80 | }
81 |
82 | function movePrevious() {
83 | const activeIndex = getState("activeIndex");
84 | const steps = getConfig("steps") || [];
85 | if (typeof activeIndex === "undefined") {
86 | return;
87 | }
88 |
89 | const previousStepIndex = activeIndex - 1;
90 | if (steps[previousStepIndex]) {
91 | drive(previousStepIndex);
92 | } else {
93 | destroy();
94 | }
95 | }
96 |
97 | function moveTo(index: number) {
98 | const steps = getConfig("steps") || [];
99 |
100 | if (steps[index]) {
101 | drive(index);
102 | } else {
103 | destroy();
104 | }
105 | }
106 |
107 | function handleArrowLeft() {
108 | const isTransitioning = getState("__transitionCallback");
109 | if (isTransitioning) {
110 | return;
111 | }
112 |
113 | const activeIndex = getState("activeIndex");
114 | const activeStep = getState("__activeStep");
115 | const activeElement = getState("__activeElement");
116 | if (typeof activeIndex === "undefined" || typeof activeStep === "undefined") {
117 | return;
118 | }
119 |
120 | const currentStepIndex = getState("activeIndex");
121 | if (typeof currentStepIndex === "undefined") {
122 | return;
123 | }
124 |
125 | const onPrevClick = activeStep.popover?.onPrevClick || getConfig("onPrevClick");
126 | if (onPrevClick) {
127 | return onPrevClick(activeElement, activeStep, {
128 | config: getConfig(),
129 | state: getState(),
130 | driver: getCurrentDriver(),
131 | });
132 | }
133 |
134 | movePrevious();
135 | }
136 |
137 | function handleArrowRight() {
138 | const isTransitioning = getState("__transitionCallback");
139 | if (isTransitioning) {
140 | return;
141 | }
142 |
143 | const activeIndex = getState("activeIndex");
144 | const activeStep = getState("__activeStep");
145 | const activeElement = getState("__activeElement");
146 | if (typeof activeIndex === "undefined" || typeof activeStep === "undefined") {
147 | return;
148 | }
149 |
150 | const onNextClick = activeStep.popover?.onNextClick || getConfig("onNextClick");
151 | if (onNextClick) {
152 | return onNextClick(activeElement, activeStep, {
153 | config: getConfig(),
154 | state: getState(),
155 | driver: getCurrentDriver(),
156 | });
157 | }
158 |
159 | moveNext();
160 | }
161 |
162 | function init() {
163 | if (getState("isInitialized")) {
164 | return;
165 | }
166 |
167 | setState("isInitialized", true);
168 | document.body.classList.add("driver-active", getConfig("animate") ? "driver-fade" : "driver-simple");
169 |
170 | initEvents();
171 |
172 | listen("overlayClick", handleOverlayClick);
173 | listen("escapePress", handleClose);
174 | listen("arrowLeftPress", handleArrowLeft);
175 | listen("arrowRightPress", handleArrowRight);
176 | }
177 |
178 | function drive(stepIndex: number = 0) {
179 | const steps = getConfig("steps");
180 | if (!steps) {
181 | console.error("No steps to drive through");
182 | destroy();
183 | return;
184 | }
185 |
186 | if (!steps[stepIndex]) {
187 | destroy();
188 |
189 | return;
190 | }
191 |
192 | setState("__activeOnDestroyed", document.activeElement as HTMLElement);
193 | setState("activeIndex", stepIndex);
194 |
195 | const currentStep = steps[stepIndex];
196 | const hasNextStep = steps[stepIndex + 1];
197 | const hasPreviousStep = steps[stepIndex - 1];
198 |
199 | const doneBtnText = currentStep.popover?.doneBtnText || getConfig("doneBtnText") || "Done";
200 | const allowsClosing = getConfig("allowClose");
201 | const showProgress =
202 | typeof currentStep.popover?.showProgress !== "undefined"
203 | ? currentStep.popover?.showProgress
204 | : getConfig("showProgress");
205 | const progressText = currentStep.popover?.progressText || getConfig("progressText") || "{{current}} of {{total}}";
206 | const progressTextReplaced = progressText
207 | .replace("{{current}}", `${stepIndex + 1}`)
208 | .replace("{{total}}", `${steps.length}`);
209 |
210 | const configuredButtons = currentStep.popover?.showButtons || getConfig("showButtons");
211 | const calculatedButtons: AllowedButtons[] = [
212 | "next",
213 | "previous",
214 | ...(allowsClosing ? ["close" as AllowedButtons] : []),
215 | ].filter(b => {
216 | return !configuredButtons?.length || configuredButtons.includes(b as AllowedButtons);
217 | }) as AllowedButtons[];
218 |
219 | const onNextClick = currentStep.popover?.onNextClick || getConfig("onNextClick");
220 | const onPrevClick = currentStep.popover?.onPrevClick || getConfig("onPrevClick");
221 | const onCloseClick = currentStep.popover?.onCloseClick || getConfig("onCloseClick");
222 |
223 | highlight({
224 | ...currentStep,
225 | popover: {
226 | showButtons: calculatedButtons,
227 | nextBtnText: !hasNextStep ? doneBtnText : undefined,
228 | disableButtons: [...(!hasPreviousStep ? ["previous" as AllowedButtons] : [])],
229 | showProgress: showProgress,
230 | progressText: progressTextReplaced,
231 | onNextClick: onNextClick
232 | ? onNextClick
233 | : () => {
234 | if (!hasNextStep) {
235 | destroy();
236 | } else {
237 | drive(stepIndex + 1);
238 | }
239 | },
240 | onPrevClick: onPrevClick
241 | ? onPrevClick
242 | : () => {
243 | drive(stepIndex - 1);
244 | },
245 | onCloseClick: onCloseClick
246 | ? onCloseClick
247 | : () => {
248 | destroy();
249 | },
250 | ...(currentStep?.popover || {}),
251 | },
252 | });
253 | }
254 |
255 | function destroy(withOnDestroyStartedHook = true) {
256 | const activeElement = getState("__activeElement");
257 | const activeStep = getState("__activeStep");
258 |
259 | const activeOnDestroyed = getState("__activeOnDestroyed");
260 |
261 | const onDestroyStarted = getConfig("onDestroyStarted");
262 | // `onDestroyStarted` is used to confirm the exit of tour. If we trigger
263 | // the hook for when user calls `destroy`, driver will get into infinite loop
264 | // not causing tour to be destroyed.
265 | if (withOnDestroyStartedHook && onDestroyStarted) {
266 | const isActiveDummyElement = !activeElement || activeElement?.id === "driver-dummy-element";
267 | onDestroyStarted(isActiveDummyElement ? undefined : activeElement, activeStep!, {
268 | config: getConfig(),
269 | state: getState(),
270 | driver: getCurrentDriver(),
271 | });
272 | return;
273 | }
274 |
275 | const onDeselected = activeStep?.onDeselected || getConfig("onDeselected");
276 | const onDestroyed = getConfig("onDestroyed");
277 |
278 | document.body.classList.remove("driver-active", "driver-fade", "driver-simple");
279 |
280 | destroyEvents();
281 | destroyPopover();
282 | destroyHighlight();
283 | destroyOverlay();
284 | destroyEmitter();
285 |
286 | resetState();
287 |
288 | if (activeElement && activeStep) {
289 | const isActiveDummyElement = activeElement.id === "driver-dummy-element";
290 | if (onDeselected) {
291 | onDeselected(isActiveDummyElement ? undefined : activeElement, activeStep, {
292 | config: getConfig(),
293 | state: getState(),
294 | driver: getCurrentDriver(),
295 | });
296 | }
297 |
298 | if (onDestroyed) {
299 | onDestroyed(isActiveDummyElement ? undefined : activeElement, activeStep, {
300 | config: getConfig(),
301 | state: getState(),
302 | driver: getCurrentDriver(),
303 | });
304 | }
305 | }
306 |
307 | if (activeOnDestroyed) {
308 | (activeOnDestroyed as HTMLElement).focus();
309 | }
310 | }
311 |
312 | const api: Driver = {
313 | isActive: () => getState("isInitialized") || false,
314 | refresh: requireRefresh,
315 | drive: (stepIndex: number = 0) => {
316 | init();
317 | drive(stepIndex);
318 | },
319 | setConfig: configure,
320 | setSteps: (steps: DriveStep[]) => {
321 | resetState();
322 | configure({
323 | ...getConfig(),
324 | steps,
325 | });
326 | },
327 | getConfig,
328 | getState,
329 | getActiveIndex: () => getState("activeIndex"),
330 | isFirstStep: () => getState("activeIndex") === 0,
331 | isLastStep: () => {
332 | const steps = getConfig("steps") || [];
333 | const activeIndex = getState("activeIndex");
334 |
335 | return activeIndex !== undefined && activeIndex === steps.length - 1;
336 | },
337 | getActiveStep: () => getState("activeStep"),
338 | getActiveElement: () => getState("activeElement"),
339 | getPreviousElement: () => getState("previousElement"),
340 | getPreviousStep: () => getState("previousStep"),
341 | moveNext,
342 | movePrevious,
343 | moveTo,
344 | hasNextStep: () => {
345 | const steps = getConfig("steps") || [];
346 | const activeIndex = getState("activeIndex");
347 |
348 | return activeIndex !== undefined && !!steps[activeIndex + 1];
349 | },
350 | hasPreviousStep: () => {
351 | const steps = getConfig("steps") || [];
352 | const activeIndex = getState("activeIndex");
353 |
354 | return activeIndex !== undefined && !!steps[activeIndex - 1];
355 | },
356 | highlight: (step: DriveStep) => {
357 | init();
358 | highlight({
359 | ...step,
360 | popover: step.popover
361 | ? {
362 | showButtons: [],
363 | showProgress: false,
364 | progressText: "",
365 | ...step.popover!,
366 | }
367 | : undefined,
368 | });
369 | },
370 | destroy: () => {
371 | destroy(false);
372 | },
373 | };
374 |
375 | setCurrentDriver(api);
376 |
377 | return api;
378 | }
379 |
--------------------------------------------------------------------------------
/.github/images/driver.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
37 |
40 |
41 |
42 |
43 |
44 |
46 |
47 |
48 |
49 |
50 |
52 |
53 |
54 |
55 |
56 |
59 |
60 |
61 |
62 |
63 |
65 |
66 |
67 |
68 |
69 |
71 |
72 |
73 |
74 |
75 |
78 |
79 |
80 |
81 |
82 |
85 |
86 |
87 |
88 |
89 |
92 |
93 |
94 |
95 |
96 |
99 |
100 |
101 |
102 |
103 |
106 |
107 |
108 |
109 |
110 |
114 |
115 |
116 |
117 |
118 |
121 |
122 |
123 |
124 |
125 |
194 |
195 |
196 |
197 |
198 |
--------------------------------------------------------------------------------
/docs/src/components/Examples.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { ExampleButton } from "./ExampleButton";
3 | ---
4 | Examples
5 | Here are just a few examples; find more in the documentation .
7 |
8 |
27 |
28 |
--------------------------------------------------------------------------------