Due to its extensive API, driver.js can be used for a wide range of use
5 | cases.
6 |
7 |
11 |
15 |
19 |
23 |
24 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) Kamran Ahmed
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/docs/src/layouts/DocsLayout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import BaseLayout from "./BaseLayout.astro";
3 | import { DocsHeader } from "../components/DocsHeader";
4 | import Container from "../components/Container.astro";
5 | import { getFormattedStars } from "../lib/github";
6 | import Sidebar from "../components/Sidebar.astro";
7 | import type { CollectionEntry } from "astro:content";
8 | import { getAllGuides } from "../lib/guide";
9 |
10 | type GuideType = CollectionEntry<"guides">;
11 |
12 | export interface Props {
13 | guide: GuideType;
14 | }
15 |
16 | const groupedGuides = await getAllGuides();
17 |
18 | const { guide } = Astro.props;
19 | const { groupTitle, sort, title } = guide.data;
20 | ---
21 |
22 |
23 |
42 | >
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/docs/src/content/guides/styling-overlay.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Styling Overlay"
3 | groupTitle: "Examples"
4 | sort: 5
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | You can customize the overlay opacity and color using `overlayOpacity` and `overlayColor` options to change the look of the overlay.
10 |
11 | > **Note:** In the examples below we have used `highlight` method to highlight the elements. The same configuration applies to the tour steps as well.
12 |
13 | ## Overlay Color
14 |
15 | Here are some driver.js examples with different overlay colors.
16 |
17 | ```js
18 | import { driver } from "driver.js";
19 | import "driver.js/dist/driver.css";
20 |
21 | const driverObj = driver({
22 | overlayColor: 'red'
23 | });
24 |
25 | driverObj.highlight({
26 | popover: {
27 | title: 'Pass any RGB Color',
28 | description: 'Here we have set the overlay color to be red. You can pass any RGB color to overlayColor option.'
29 | }
30 | });
31 | ```
32 |
33 |
34 |
49 |
50 |
65 |
66 |
81 |
82 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import { DriveStep } from "./driver";
2 | import { AllowedButtons, PopoverDOM } from "./popover";
3 | import { State } from "./state";
4 |
5 | export type DriverHook = (element: Element | undefined, step: DriveStep, opts: { config: Config; state: State }) => void;
6 |
7 | export type Config = {
8 | steps?: DriveStep[];
9 |
10 | animate?: boolean;
11 | overlayColor?: string;
12 | overlayOpacity?: number;
13 | smoothScroll?: boolean;
14 | allowClose?: boolean;
15 | overlayClickBehavior?: 'close' | 'nextStep';
16 | stagePadding?: number;
17 | stageRadius?: number;
18 |
19 | disableActiveInteraction?: boolean;
20 |
21 | allowKeyboardControl?: boolean;
22 |
23 | // Popover specific configuration
24 | popoverClass?: string;
25 | popoverOffset?: number;
26 | showButtons?: AllowedButtons[];
27 | disableButtons?: AllowedButtons[];
28 | showProgress?: boolean;
29 |
30 | // Button texts
31 | progressText?: string;
32 | nextBtnText?: string;
33 | prevBtnText?: string;
34 | doneBtnText?: string;
35 |
36 | // Called after the popover is rendered
37 | onPopoverRender?: (popover: PopoverDOM, opts: { config: Config; state: State }) => void;
38 |
39 | // State based callbacks, called upon state changes
40 | onHighlightStarted?: DriverHook;
41 | onHighlighted?: DriverHook;
42 | onDeselected?: DriverHook;
43 | onDestroyStarted?: DriverHook;
44 | onDestroyed?: DriverHook;
45 |
46 | // Event based callbacks, called upon events
47 | onNextClick?: DriverHook;
48 | onPrevClick?: DriverHook;
49 | onCloseClick?: DriverHook;
50 | };
51 |
52 | let currentConfig: Config = {};
53 |
54 | export function configure(config: Config = {}) {
55 | currentConfig = {
56 | animate: true,
57 | allowClose: true,
58 | overlayClickBehavior: 'close',
59 | overlayOpacity: 0.7,
60 | smoothScroll: false,
61 | disableActiveInteraction: false,
62 | showProgress: false,
63 | stagePadding: 10,
64 | stageRadius: 5,
65 | popoverOffset: 10,
66 | showButtons: ["next", "previous", "close"],
67 | disableButtons: [],
68 | overlayColor: "#000",
69 | ...config,
70 | };
71 | }
72 |
73 | export function getConfig(): Config;
74 | export function getConfig(key: K): Config[K];
75 | export function getConfig(key?: K) {
76 | return key ? currentConfig[key] : currentConfig;
77 | }
78 |
--------------------------------------------------------------------------------
/docs/src/content/guides/prevent-destroy.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Prevent Tour Exit"
3 | groupTitle: "Examples"
4 | sort: 3
5 | ---
6 |
7 | import { CodeSample } from "../../components/CodeSample.tsx";
8 |
9 | You can also prevent the user from exiting the tour using `allowClose` option. This option is useful when you want to force the user to complete the tour before they can exit.
10 |
11 | In the example below, you won't be able to exit the tour until you reach the last step.
12 |
13 |
29 | ```js
30 | import { driver } from "driver.js";
31 | import "driver.js/dist/driver.css";
32 |
33 | const driverObj = driver({
34 | showProgress: true,
35 | allowClose: false,
36 | steps: [
37 | { element: '#prevent-exit', popover: { title: 'Animated Tour Example', description: 'Here is the code example showing animated tour. Let\'s walk you through it.', side: "left", align: 'start' }},
38 | { 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' }},
39 | { 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' }},
40 | { popover: { title: 'Happy Coding', description: 'And that is all, go ahead and start adding tours to your applications.' } }
41 | ],
42 | });
43 |
44 | driverObj.drive();
45 | ```
46 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { getConfig } from "./config";
2 |
3 | export function easeInOutQuad(elapsed: number, initialValue: number, amountOfChange: number, duration: number): number {
4 | if ((elapsed /= duration / 2) < 1) {
5 | return (amountOfChange / 2) * elapsed * elapsed + initialValue;
6 | }
7 | return (-amountOfChange / 2) * (--elapsed * (elapsed - 2) - 1) + initialValue;
8 | }
9 |
10 | export function getFocusableElements(parentEls: Element[] | HTMLElement[]) {
11 | const focusableQuery =
12 | 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])';
13 |
14 | return parentEls
15 | .flatMap(parentEl => {
16 | const isParentFocusable = parentEl.matches(focusableQuery);
17 | const focusableEls: HTMLElement[] = Array.from(parentEl.querySelectorAll(focusableQuery));
18 |
19 | return [...(isParentFocusable ? [parentEl as HTMLElement] : []), ...focusableEls];
20 | })
21 | .filter(el => {
22 | return getComputedStyle(el).pointerEvents !== "none" && isElementVisible(el);
23 | });
24 | }
25 |
26 | export function bringInView(element: Element) {
27 | if (!element || isElementInView(element)) {
28 | return;
29 | }
30 |
31 | const shouldSmoothScroll = getConfig("smoothScroll");
32 |
33 | element.scrollIntoView({
34 | // Removing the smooth scrolling for elements which exist inside the scrollable parent
35 | // This was causing the highlight to not properly render
36 | behavior: !shouldSmoothScroll || hasScrollableParent(element) ? "auto" : "smooth",
37 | inline: "center",
38 | block: "center",
39 | });
40 | }
41 |
42 | function hasScrollableParent(e: Element) {
43 | if (!e || !e.parentElement) {
44 | return;
45 | }
46 |
47 | const parent = e.parentElement as HTMLElement & { scrollTopMax?: number };
48 |
49 | return parent.scrollHeight > parent.clientHeight;
50 | }
51 |
52 | function isElementInView(element: Element) {
53 | const rect = element.getBoundingClientRect();
54 |
55 | return (
56 | rect.top >= 0 &&
57 | rect.left >= 0 &&
58 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
59 | rect.right <= (window.innerWidth || document.documentElement.clientWidth)
60 | );
61 | }
62 |
63 | export function isElementVisible(el: HTMLElement) {
64 | return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
65 | }
66 |
--------------------------------------------------------------------------------
/docs/src/content/guides/api.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "API Reference"
3 | groupTitle: "Introduction"
4 | sort: 4
5 | ---
6 |
7 | Here is the list of methods provided by `driver` when you initialize it.
8 |
9 | > **Note:** We have omitted the configuration options for brevity. Please look at the configuration section for the options. Links are provided in the description below.
10 |
11 | ```javascript
12 | import { driver } from "driver.js";
13 | import "driver.js/dist/driver.css";
14 |
15 | // Look at the configuration section for the options
16 | // https://driverjs.com/docs/configuration#driver-configuration
17 | const driverObj = driver({ /* ... */ });
18 |
19 | // --------------------------------------------------
20 | // driverObj is an object with the following methods
21 | // --------------------------------------------------
22 |
23 | // Start the tour using `steps` given in the configuration
24 | driverObj.drive(); // Starts at step 0
25 | driverObj.drive(4); // Starts at step 4
26 |
27 | driverObj.moveNext(); // Move to the next step
28 | driverObj.movePrevious(); // Move to the previous step
29 | driverObj.moveTo(4); // Move to the step 4
30 | driverObj.hasNextStep(); // Is there a next step
31 | driverObj.hasPreviousStep() // Is there a previous step
32 |
33 | driverObj.isFirstStep(); // Is the current step the first step
34 | driverObj.isLastStep(); // Is the current step the last step
35 |
36 | driverObj.getActiveIndex(); // Gets the active step index
37 |
38 | driverObj.getActiveStep(); // Gets the active step configuration
39 | driverObj.getPreviousStep(); // Gets the previous step configuration
40 | driverObj.getActiveElement(); // Gets the active HTML element
41 | driverObj.getPreviousElement(); // Gets the previous HTML element
42 |
43 | // Is the tour or highlight currently active
44 | driverObj.isActive();
45 |
46 | // Recalculate and redraw the highlight
47 | driverObj.refresh();
48 |
49 | // Look at the configuration section for configuration options
50 | // https://driverjs.com/docs/configuration#driver-configuration
51 | driverObj.getConfig();
52 | driverObj.setConfig({ /* ... */ });
53 |
54 | driverObj.setSteps([ /* ... */ ]); // Set the steps
55 |
56 | // Look at the state section of configuration for format of the state
57 | // https://driverjs.com/docs/configuration#state
58 | driverObj.getState();
59 |
60 | // Look at the DriveStep section of configuration for format of the step
61 | // https://driverjs.com/docs/configuration/#drive-step-configuration
62 | driverObj.highlight({ /* ... */ }); // Highlight an element
63 |
64 | driverObj.destroy(); // Destroy the tour
65 | ```
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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).
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
16 | Powerful, highly customizable vanilla JavaScript engine to drive the user's focus across the page
17 | No external dependencies, light-weight, supports all major browsers and highly customizable
18 |
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/public/thumbs.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/src/highlight.ts:
--------------------------------------------------------------------------------
1 | import { DriveStep } from "./driver";
2 | import { refreshOverlay, trackActiveElement, transitionStage } from "./overlay";
3 | import { getConfig } 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 = typeof element === "string" ? document.querySelector(element) : element;
33 |
34 | // If the element is not found, we mount a 1px div
35 | // at the center of the screen to highlight and show
36 | // the popover on top of that. This is to show a
37 | // modal-like highlight.
38 | if (!elemObj) {
39 | elemObj = mountDummyElement();
40 | }
41 |
42 | transferHighlight(elemObj, step);
43 | }
44 |
45 | export function refreshActiveHighlight() {
46 | const activeHighlight = getState("__activeElement");
47 | const activeStep = getState("__activeStep")!;
48 |
49 | if (!activeHighlight) {
50 | return;
51 | }
52 |
53 | trackActiveElement(activeHighlight);
54 | refreshOverlay();
55 | repositionPopover(activeHighlight, activeStep);
56 | }
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 | });
86 | }
87 |
88 | if (highlightStartedHook) {
89 | highlightStartedHook(isToDummyElement ? undefined : toElement, toStep, {
90 | config,
91 | state,
92 | });
93 | }
94 |
95 | const hasDelayedPopover = !isFirstHighlight && isAnimatedTour;
96 | let isPopoverRendered = false;
97 |
98 | hidePopover();
99 |
100 | setState("previousStep", fromStep);
101 | setState("previousElement", fromElement);
102 | setState("activeStep", toStep);
103 | setState("activeElement", toElement);
104 |
105 | const animate = () => {
106 | const transitionCallback = getState("__transitionCallback");
107 |
108 | // This makes sure that the repeated calls to transferHighlight
109 | // don't interfere with each other. Only the last call will be
110 | // executed.
111 | if (transitionCallback !== animate) {
112 | return;
113 | }
114 |
115 | const elapsed = Date.now() - start;
116 | const timeRemaining = duration - elapsed;
117 | const isHalfwayThrough = timeRemaining <= duration / 2;
118 |
119 | if (toStep.popover && isHalfwayThrough && !isPopoverRendered && hasDelayedPopover) {
120 | renderPopover(toElement, toStep);
121 | isPopoverRendered = true;
122 | }
123 |
124 | if (getConfig("animate") && elapsed < duration) {
125 | transitionStage(elapsed, duration, fromElement, toElement);
126 | } else {
127 | trackActiveElement(toElement);
128 |
129 | if (highlightedHook) {
130 | highlightedHook(isToDummyElement ? undefined : toElement, toStep, {
131 | config: getConfig(),
132 | state: getState(),
133 | });
134 | }
135 |
136 | setState("__transitionCallback", undefined);
137 | setState("__previousStep", fromStep);
138 | setState("__previousElement", fromElement);
139 | setState("__activeStep", toStep);
140 | setState("__activeElement", toElement);
141 | }
142 |
143 | window.requestAnimationFrame(animate);
144 | };
145 |
146 | setState("__transitionCallback", animate);
147 |
148 | window.requestAnimationFrame(animate);
149 |
150 | bringInView(toElement);
151 | if (!hasDelayedPopover && toStep.popover) {
152 | renderPopover(toElement, toStep);
153 | }
154 |
155 | fromElement.classList.remove("driver-active-element", "driver-no-interaction");
156 | fromElement.removeAttribute("aria-haspopup");
157 | fromElement.removeAttribute("aria-expanded");
158 | fromElement.removeAttribute("aria-controls");
159 |
160 | const disableActiveInteraction = getConfig("disableActiveInteraction");
161 | if (disableActiveInteraction) {
162 | toElement.classList.add("driver-no-interaction");
163 | }
164 |
165 | toElement.classList.add("driver-active-element");
166 | toElement.setAttribute("aria-haspopup", "dialog");
167 | toElement.setAttribute("aria-expanded", "true");
168 | toElement.setAttribute("aria-controls", "driver-popover-content");
169 | }
170 |
171 | export function destroyHighlight() {
172 | document.getElementById("driver-dummy-element")?.remove();
173 | document.querySelectorAll(".driver-active-element").forEach(element => {
174 | element.classList.remove("driver-active-element", "driver-no-interaction");
175 | element.removeAttribute("aria-haspopup");
176 | element.removeAttribute("aria-expanded");
177 | element.removeAttribute("aria-controls");
178 | });
179 | }
180 |
--------------------------------------------------------------------------------
/docs/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/docs/public/driver-head.svg:
--------------------------------------------------------------------------------
1 |
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.
--------------------------------------------------------------------------------
/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 |
128 |
129 | ## Change Button Text
130 |
131 | You can also change the text of buttons using `nextBtnText`, `prevBtnText` and `doneBtnText` options.
132 |
133 |