├── .prettierignore ├── docs ├── public │ ├── og-img.png │ ├── burger.svg │ ├── cross.svg │ ├── star.svg │ ├── arrow.svg │ ├── github.svg │ ├── thumbs.svg │ ├── favicon.svg │ └── driver-head.svg ├── src │ ├── components │ │ ├── Container.astro │ │ ├── Analytics │ │ │ ├── Analytics.astro │ │ │ └── analytics.ts │ │ ├── UsecaseItem.astro │ │ ├── ExampleButton.tsx │ │ ├── FeatureMarquee.tsx │ │ ├── UsecaseList.astro │ │ ├── HeroSection.astro │ │ ├── Sidebar.astro │ │ ├── OpenSourceLove.astro │ │ ├── FormHelp.tsx │ │ ├── DocsHeader.tsx │ │ ├── CodeSample.tsx │ │ └── Examples.astro │ ├── env.d.ts │ ├── content │ │ ├── config.ts │ │ └── guides │ │ │ ├── installation.mdx │ │ │ ├── styling-overlay.mdx │ │ │ ├── prevent-destroy.mdx │ │ │ ├── api.mdx │ │ │ ├── confirm-on-exit.mdx │ │ │ ├── theming.mdx │ │ │ ├── basic-usage.mdx │ │ │ ├── async-tour.mdx │ │ │ ├── animated-tour.mdx │ │ │ ├── static-tour.mdx │ │ │ ├── tour-progress.mdx │ │ │ ├── simple-highlight.mdx │ │ │ ├── migrating-from-0x.mdx │ │ │ ├── styling-popover.mdx │ │ │ ├── popover-position.mdx │ │ │ ├── buttons.mdx │ │ │ └── configuration.mdx │ ├── lib │ │ ├── guide.ts │ │ └── github.ts │ ├── pages │ │ ├── docs │ │ │ └── [guideId].astro │ │ └── index.astro │ └── layouts │ │ ├── DocsLayout.astro │ │ └── BaseLayout.astro ├── tsconfig.json ├── .gitignore ├── tailwind.config.cjs ├── astro.config.mjs └── package.json ├── .github ├── FUNDING.yml └── images │ └── driver.svg ├── tests └── sum.test.ts ├── dts-bundle-generator.config.ts ├── .prettierrc ├── .gitignore ├── src ├── emitter.ts ├── state.ts ├── config.ts ├── utils.ts ├── events.ts ├── driver.css ├── highlight.ts ├── overlay.ts └── driver.ts ├── tsconfig.json ├── vite.config.ts ├── license ├── package.json └── readme.md /.prettierignore: -------------------------------------------------------------------------------- 1 | .history 2 | .vscode 3 | coverage 4 | dist 5 | node_modules 6 | -------------------------------------------------------------------------------- /docs/public/og-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Enerfek/driver.js/HEAD/docs/public/og-img.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kamranahmedse] 4 | -------------------------------------------------------------------------------- /docs/src/components/Container.astro: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | interface Window { 5 | driverObj: any; 6 | } 7 | -------------------------------------------------------------------------------- /docs/public/burger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "react", 6 | "strictNullChecks": true 7 | } 8 | } -------------------------------------------------------------------------------- /tests/sum.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | describe("add", () => { 4 | it("should sum of 2 and 3 equals to 5", () => { 5 | expect(5).toEqual(5); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /dts-bundle-generator.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entries: [ 3 | { 4 | filePath: "./src/driver.ts", 5 | outFile: `./dist/driver.js.d.ts`, 6 | noCheck: false, 7 | }, 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /docs/public/cross.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /docs/public/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/public/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { z, defineCollection } from "astro:content"; 2 | 3 | const guidesCollection = defineCollection({ 4 | type: "content", 5 | schema: z.object({ 6 | groupTitle: z.string(), 7 | title: z.string(), 8 | sort: z.number(), 9 | }), 10 | }); 11 | 12 | export const collections = { 13 | guides: guidesCollection, 14 | }; 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "singleQuote": false, 5 | "trailingComma": "es5", 6 | "arrowParens": "avoid", 7 | "bracketSpacing": true, 8 | "useTabs": false, 9 | "endOfLine": "auto", 10 | "singleAttributePerLine": false, 11 | "bracketSameLine": false, 12 | "jsxSingleQuote": false, 13 | "quoteProps": "as-needed", 14 | "semi": true 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | coverage 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | .history/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /docs/src/components/Analytics/Analytics.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], 4 | theme: { 5 | screens: { 6 | 'sh': { 7 | 'raw': '(min-height: 750px)', 8 | }, 9 | ...require('tailwindcss/defaultConfig').theme.screens, 10 | }, 11 | container: { 12 | }, 13 | extend: {}, 14 | }, 15 | plugins: [ 16 | require('@tailwindcss/typography'), 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /docs/src/components/UsecaseItem.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | title: string; 4 | description: string; 5 | } 6 | 7 | const { title, description } = Astro.props; 8 | --- 9 | 10 |
11 | 12 |

13 | { title } 14 |

15 |

16 | { description } 17 |

18 |
-------------------------------------------------------------------------------- /docs/src/components/ExampleButton.tsx: -------------------------------------------------------------------------------- 1 | type ExampleButtonProps = { 2 | id: string; 3 | onClick: () => void; 4 | text: string; 5 | }; 6 | 7 | export function ExampleButton(props: ExampleButtonProps) { 8 | const { id, onClick, text } = props; 9 | 10 | return ( 11 | 17 | ); 18 | } -------------------------------------------------------------------------------- /src/emitter.ts: -------------------------------------------------------------------------------- 1 | type allowedEvents = 2 | | "overlayClick" 3 | | "escapePress" 4 | | "nextClick" 5 | | "prevClick" 6 | | "closeClick" 7 | | "arrowRightPress" 8 | | "arrowLeftPress"; 9 | 10 | let registeredListeners: Partial<{ [key in allowedEvents]: () => void }> = {}; 11 | 12 | export function listen(hook: allowedEvents, callback: () => void) { 13 | registeredListeners[hook] = callback; 14 | } 15 | 16 | export function emit(hook: allowedEvents) { 17 | registeredListeners[hook]?.(); 18 | } 19 | 20 | export function destroyEmitter() { 21 | registeredListeners = {}; 22 | } 23 | -------------------------------------------------------------------------------- /docs/src/lib/guide.ts: -------------------------------------------------------------------------------- 1 | import { CollectionEntry, getCollection } from "astro:content"; 2 | 3 | export async function getAllGuides(): Promise[]>> { 4 | const allGuides: CollectionEntry<"guides">[] = await getCollection("guides"); 5 | const sortedGuides = allGuides.sort((a, b) => a.data.sort - b.data.sort); 6 | return sortedGuides.reduce((acc: Record[]>, curr: CollectionEntry<"guides">) => { 7 | const { groupTitle } = curr.data; 8 | 9 | acc[groupTitle] = acc[groupTitle] || []; 10 | acc[groupTitle].push(curr); 11 | 12 | return acc; 13 | }, {}); 14 | } 15 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import tailwind from "@astrojs/tailwind"; 3 | import react from "@astrojs/react"; 4 | import mdx from "@astrojs/mdx"; 5 | 6 | import compress from "astro-compress"; 7 | 8 | // https://astro.build/config 9 | export default defineConfig({ 10 | build: { 11 | format: "file", 12 | }, 13 | markdown: { 14 | shikiConfig: { 15 | // theme: "material-theme" 16 | theme: "monokai", 17 | // theme: 'poimandres' 18 | }, 19 | }, 20 | 21 | integrations: [ 22 | tailwind(), 23 | react(), 24 | mdx(), 25 | compress({ 26 | CSS: false, 27 | JS: false, 28 | }), 29 | ], 30 | }); 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "target": "ES2019", 5 | "useDefineForClassFields": true, 6 | "module": "CommonJS", 7 | "lib": ["ES2019", "DOM"], 8 | "moduleResolution": "Node", 9 | "strict": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "types": ["vite/client", "node"] 19 | }, 20 | "include": ["src"], 21 | "exclude": ["**/*.test.ts", "node_modules", 22 | "tests/**", ".history/**"] 23 | } 24 | -------------------------------------------------------------------------------- /docs/src/pages/docs/[guideId].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { CollectionEntry, getCollection } from "astro:content"; 3 | import DocsLayout from "../../layouts/DocsLayout.astro"; 4 | 5 | export interface Props { 6 | guide: CollectionEntry<"guides">; 7 | } 8 | 9 | export async function getStaticPaths() { 10 | const guides = await getCollection("guides"); 11 | 12 | return guides.map(guide => ({ 13 | params: { guideId: guide.slug }, 14 | props: { guide }, 15 | })); 16 | } 17 | 18 | const { guideId } = Astro.params; 19 | const { guide } = Astro.props; 20 | 21 | const { Content, headings } = await guide.render(); 22 | --- 23 | 24 | 25 |

{guide.data.title}

26 | 27 |
28 | -------------------------------------------------------------------------------- /docs/public/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "driver-docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/mdx": "^1.0.0", 14 | "@astrojs/react": "^3.0.0", 15 | "@astrojs/tailwind": "^5.0.0", 16 | "@types/react": "^18.2.21", 17 | "@types/react-dom": "^18.2.7", 18 | "astro": "^3.0.8", 19 | "astro-compress": "^2.0.15", 20 | "driver.js": "1.3.0", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-fast-marquee": "^1.6.0", 24 | "tailwindcss": "^3.3.3" 25 | }, 26 | "devDependencies": { 27 | "@tailwindcss/typography": "^0.5.9" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import path from "path"; 3 | import { defineConfig } from "vite"; 4 | 5 | const packageName = "driver.js"; 6 | 7 | const fileName = { 8 | es: `${packageName}.mjs`, 9 | cjs: `${packageName}.cjs`, 10 | iife: `${packageName}.iife.js`, 11 | }; 12 | 13 | const formats = Object.keys(fileName) as Array; 14 | 15 | module.exports = defineConfig({ 16 | base: "./", 17 | build: { 18 | target: "ES2019", 19 | lib: { 20 | entry: path.resolve(__dirname, "src/driver.ts"), 21 | name: packageName, 22 | formats, 23 | fileName: format => fileName[format], 24 | }, 25 | rollupOptions: { 26 | output: { 27 | assetFileNames: assetInfo => { 28 | return assetInfo.name === "style.css" ? `driver.css` : assetInfo.name as string; 29 | }, 30 | }, 31 | }, 32 | }, 33 | test: {}, 34 | }); 35 | -------------------------------------------------------------------------------- /docs/src/components/FeatureMarquee.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Marquee from "react-fast-marquee"; 3 | 4 | const featureList = [ 5 | "Supports all Major Browsers", 6 | "Works on Mobile Devices", 7 | "Highly Customizable", 8 | "Light-weight", 9 | "No dependencies", 10 | "Feature Rich", 11 | "MIT Licensed", 12 | "Written in TypeScript", 13 | "Vanilla JavaScript", 14 | "Easy to use", 15 | "Accessible", 16 | "Frameworks Ready", 17 | ]; 18 | 19 | export function FeatureMarquee() { 20 | return ( 21 | 22 |

23 | { featureList.map((featureItem, index) => ( 24 | 25 | { featureItem }· 26 | 27 | ))} 28 |

29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /docs/src/components/Analytics/analytics.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | gtag: any; 4 | fireEvent: (props: { 5 | action: string; 6 | category: string; 7 | label?: string; 8 | value?: string; 9 | }) => void; 10 | } 11 | } 12 | 13 | /** 14 | * Tracks the event on google analytics 15 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/events 16 | * @param props Event properties 17 | * @returns void 18 | */ 19 | window.fireEvent = (props) => { 20 | const { action, category, label, value } = props; 21 | if (!window.gtag) { 22 | console.warn('Missing GTAG - Analytics disabled'); 23 | return; 24 | } 25 | 26 | if (import.meta.env.DEV) { 27 | console.log('Analytics event fired', props); 28 | } 29 | 30 | window.gtag('event', action, { 31 | event_category: category, 32 | event_label: label, 33 | value: value, 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /docs/src/lib/github.ts: -------------------------------------------------------------------------------- 1 | const formatter = Intl.NumberFormat("en-US", { 2 | notation: "compact", 3 | }); 4 | 5 | const defaultStarCount = 17000; 6 | let starCount: number | undefined = undefined; 7 | 8 | export async function countStars(repo = "kamranahmedse/driver.js"): Promise { 9 | if (starCount) { 10 | return starCount; 11 | } 12 | 13 | try { 14 | const repoData = await fetch(`https://api.github.com/repos/${repo}`); 15 | const json = await repoData.json(); 16 | 17 | starCount = json.stargazers_count * 1 || defaultStarCount; 18 | } catch (e) { 19 | console.log("Failed to fetch stars", e); 20 | starCount = defaultStarCount; 21 | } 22 | 23 | return starCount; 24 | } 25 | 26 | export async function getFormattedStars(repo = "kamranahmedse/driver.js"): Promise { 27 | const stars = import.meta.env.DEV ? defaultStarCount : await countStars(repo); 28 | 29 | return formatter.format(stars); 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/components/UsecaseList.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import UsecaseItem from "./UsecaseItem.astro"; 3 | --- 4 |

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 |
24 | 25 |
26 |
27 | 28 |
30 | 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /src/state.ts: -------------------------------------------------------------------------------- 1 | import { StageDefinition } from "./overlay"; 2 | import { PopoverDOM } from "./popover"; 3 | import { DriveStep } from "./driver"; 4 | 5 | export type State = { 6 | isInitialized?: boolean; 7 | 8 | activeIndex?: number; 9 | activeElement?: Element; 10 | activeStep?: DriveStep; 11 | previousElement?: Element; 12 | previousStep?: DriveStep; 13 | 14 | popover?: PopoverDOM; 15 | 16 | // actual values considering the animation 17 | // and delays. These are used to determine 18 | // the positions etc. 19 | __previousElement?: Element; 20 | __activeElement?: Element; 21 | __previousStep?: DriveStep; 22 | __activeStep?: DriveStep; 23 | 24 | __activeOnDestroyed?: Element; 25 | __resizeTimeout?: number; 26 | __transitionCallback?: () => void; 27 | __activeStagePosition?: StageDefinition; 28 | __overlaySvg?: SVGSVGElement; 29 | }; 30 | 31 | let currentState: State = {}; 32 | 33 | export function setState(key: K, value: State[K]) { 34 | currentState[key] = value; 35 | } 36 | 37 | export function getState(): State; 38 | export function getState(key: K): State[K]; 39 | export function getState(key?: K) { 40 | return key ? currentState[key] : currentState; 41 | } 42 | 43 | export function resetState() { 44 | currentState = {}; 45 | } 46 | -------------------------------------------------------------------------------- /docs/src/components/HeroSection.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Container from "./Container.astro"; 3 | --- 4 |
5 | 6 |
7 |
8 |

driver.js

9 |

Product tours, highlights, contextual help and more.

10 |
11 | 14 | 17 | Get Started 18 | 19 |
20 |
21 | 24 |
25 |
26 |
-------------------------------------------------------------------------------- /docs/src/components/Sidebar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection, getEntry } from "astro:content"; 3 | import { getFormattedStars } from "../lib/github"; 4 | import { getAllGuides } from "../lib/guide"; 5 | 6 | export interface Props { 7 | activePath: string; 8 | } 9 | 10 | const { activePath, groupedGuides, activeGuideTitle } = Astro.props; 11 | --- 12 | -------------------------------------------------------------------------------- /docs/src/content/guides/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Installation" 3 | groupTitle: "Introduction" 4 | sort: 1 5 | --- 6 | 7 | Run one of the following commands to install the package: 8 | 9 | ```bash 10 | # Using npm 11 | npm install driver.js 12 | 13 | # Using pnpm 14 | pnpm install driver.js 15 | 16 | # Using yarn 17 | yarn add driver.js 18 | ``` 19 | 20 | Alternatively, you can use CDN and include the script in your HTML file: 21 | 22 | ```html 23 | 24 | 25 | ``` 26 | 27 | ## Start Using 28 | Once installed, you can import the package in your project. The following example shows how to highlight an element: 29 | 30 | ```js 31 | import { driver } from "driver.js"; 32 | import "driver.js/dist/driver.css"; 33 | 34 | const driverObj = driver(); 35 | driverObj.highlight({ 36 | element: "#some-element", 37 | popover: { 38 | title: "Title", 39 | description: "Description" 40 | } 41 | }); 42 | ``` 43 | 44 | ### Note on CDN 45 | 46 | If you are using the CDN, you will have to use the package from the `window` object: 47 | 48 | ```js 49 | const driver = window.driver.js.driver; 50 | 51 | const driverObj = driver(); 52 | 53 | driverObj.highlight({ 54 | element: "#some-element", 55 | popover: { 56 | title: "Title", 57 | description: "Description" 58 | } 59 | }); 60 | ``` 61 | 62 | Continue reading the [Getting Started](/docs/basic-usage) guide to learn more about the package. -------------------------------------------------------------------------------- /docs/src/components/OpenSourceLove.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Container from "./Container.astro"; 3 | import { getFormattedStars } from "../lib/github"; 4 | 5 | const starCount = getFormattedStars('kamranahmedse/driver.js'); 6 | --- 7 |
8 | 9 |
10 |
11 |

Loved by Many

12 |

With millions of downloads, Driver.js is an MIT licensed 13 | opensource 14 | project and is used by 15 | thousands of companies around the world.

16 | 17 | 30 |
31 | 32 |
33 |
34 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "driver.js", 3 | "license": "MIT", 4 | "private": false, 5 | "version": "1.3.1", 6 | "main": "./dist/driver.js.cjs", 7 | "module": "./dist/driver.js.mjs", 8 | "types": "./dist/driver.js.d.ts", 9 | "homepage": "https://driverjs.com", 10 | "repository": "https://github.com/kamranahmedse/driver.js", 11 | "author": "Kamran Ahmed ", 12 | "bugs": { 13 | "url": "https://github.com/kamranahmedse/driver.js/issues" 14 | }, 15 | "exports": { 16 | ".": { 17 | "types": "./dist/driver.js.d.ts", 18 | "require": "./dist/driver.js.cjs", 19 | "import": "./dist/driver.js.mjs" 20 | }, 21 | "./dist/driver.css": { 22 | "import": "./dist/driver.css", 23 | "require": "./dist/driver.css" 24 | } 25 | }, 26 | "scripts": { 27 | "dev": "vite --host", 28 | "build": "tsc && vite build && dts-bundle-generator --config ./dts-bundle-generator.config.ts", 29 | "test": "vitest", 30 | "format": "prettier . --write" 31 | }, 32 | "files": [ 33 | "!tests/**/*", 34 | "!docs/**/*", 35 | "dist/**/*", 36 | "!dist/**/*.js.map" 37 | ], 38 | "devDependencies": { 39 | "@types/jsdom": "^21.1.2", 40 | "@types/node": "^20.5.9", 41 | "@vitest/coverage-c8": "^0.32.0", 42 | "dts-bundle-generator": "^8.0.1", 43 | "postcss": "^8.4.29", 44 | "postcss-scss": "^4.0.7", 45 | "prettier": "^3.0.3", 46 | "ts-node": "^10.9.1", 47 | "typescript": "^5.2.2", 48 | "vite": "^4.4.9", 49 | "vitest": "^0.34.3" 50 | }, 51 | "keywords": [ 52 | "driver.js", 53 | "driver", 54 | "tour", 55 | "guide", 56 | "overlay", 57 | "tooltip", 58 | "walkthrough", 59 | "product tour", 60 | "product walkthrough", 61 | "product guide", 62 | "product tutorial", 63 | "product demo", 64 | "modal", 65 | "lightbox" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /docs/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseLayout from "../layouts/BaseLayout.astro"; 3 | import { FeatureMarquee } from "../components/FeatureMarquee"; 4 | import Container from "../components/Container.astro"; 5 | import UsecaseItem from "../components/UsecaseItem.astro"; 6 | import { ExampleButton } from "../components/ExampleButton"; 7 | import HeroSection from "../components/HeroSection.astro"; 8 | import Examples from "../components/Examples.astro"; 9 | import UsecaseList from "../components/UsecaseList.astro"; 10 | import OpenSourceLove from "../components/OpenSourceLove.astro"; 11 | --- 12 | 13 | 14 |
17 | 18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 | 31 |

32 | MIT Licensed © 2024 33 | 47 |

48 |
49 |
50 |
51 | -------------------------------------------------------------------------------- /docs/src/components/FormHelp.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { driver } from "driver.js"; 3 | import "driver.js/dist/driver.css"; 4 | 5 | export function FormHelp() { 6 | useEffect(() => { 7 | const driverObj = driver({ 8 | popoverClass: "driverjs-theme", 9 | stagePadding: 0, 10 | onDestroyed: () => { 11 | (document?.activeElement as any)?.blur(); 12 | } 13 | }); 14 | 15 | const nameEl = document.getElementById("name"); 16 | const educationEl = document.getElementById("education"); 17 | const ageEl = document.getElementById("age"); 18 | const addressEl = document.getElementById("address"); 19 | const submitEl = document.getElementById("submit-btn"); 20 | 21 | nameEl!.addEventListener("focus", () => { 22 | driverObj.highlight({ 23 | element: nameEl!, 24 | popover: { 25 | title: "Name", 26 | description: "Enter your name here", 27 | }, 28 | }); 29 | }); 30 | 31 | educationEl!.addEventListener("focus", () => { 32 | driverObj.highlight({ 33 | element: educationEl!, 34 | popover: { 35 | title: "Education", 36 | description: "Enter your education here", 37 | }, 38 | }); 39 | }); 40 | 41 | ageEl!.addEventListener("focus", () => { 42 | driverObj.highlight({ 43 | element: ageEl!, 44 | popover: { 45 | title: "Age", 46 | description: "Enter your age here", 47 | }, 48 | }); 49 | }); 50 | 51 | addressEl!.addEventListener("focus", () => { 52 | driverObj.highlight({ 53 | element: addressEl!, 54 | popover: { 55 | title: "Address", 56 | description: "Enter your address here", 57 | }, 58 | }); 59 | }); 60 | 61 | submitEl!.addEventListener("focus", (e) => { 62 | e.preventDefault(); 63 | driverObj.destroy(); 64 | }); 65 | }); 66 | 67 | return <>; 68 | } 69 | -------------------------------------------------------------------------------- /docs/src/components/DocsHeader.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import type { CollectionEntry } from "astro:content"; 3 | 4 | type DocsHeaderProps = { 5 | groupedGuides: Record[]>; 6 | activeGuideTitle: string; 7 | }; 8 | 9 | export function DocsHeader(props: DocsHeaderProps) { 10 | const { groupedGuides, activeGuideTitle } = props; 11 | const [isActive, setIsActive] = useState(false); 12 | 13 | return ( 14 | <> 15 |
16 | 22 |
23 | 26 |
27 |
28 |
29 | {Object.entries(groupedGuides).map(([category, guides]) => ( 30 |
31 |
{category}
32 |
33 | {guides.map(guide => ( 34 | 35 | {guide.data.title} 36 | 37 | ))} 38 |
39 |
40 | ))} 41 |
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 |


Driver.js

2 | 3 |

4 | 5 | 6 | 7 | 8 | version 9 | 10 | 11 | downloads 12 | 13 |

14 | 15 |

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 |

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 | > Please note that above documentation is for version `1.x` which is the complete rewrite of driver.js.
40 | > For `0.x` documentation, please visit [this page]([https://kamranahmed.info/driver.js/](https://github.com/kamranahmedse/driver.js/tree/4a0247ee25ba4f235bbf87434e9217c50619a1b8)) 41 | 42 |
43 | 44 | ## So, yet another tour library? 45 | 46 | **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. 47 | 48 | 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. 49 | 50 | > 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+. 51 | 52 |
53 | 54 | ## Contributions 55 | 56 | Feel free to submit pull requests, create issues or spread the word. 57 | 58 | ## License 59 | 60 | MIT © [Kamran Ahmed](https://twitter.com/kamrify) 61 | -------------------------------------------------------------------------------- /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 | 51 | -------------------------------------------------------------------------------- /docs/src/layouts/BaseLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Analytics from "../components/Analytics/Analytics.astro"; 3 | export interface Props { 4 | title: string; 5 | } 6 | 7 | export interface BaseLayoutProps extends Props { 8 | permalink?: string; 9 | title?: string; 10 | description?: string; 11 | } 12 | 13 | const { permalink = '', title = "Driver.js", description = "A light-weight, no-dependency, vanilla JavaScript library to drive user's focus across the page." } = Astro.props; 14 | 15 | --- 16 | 17 | 18 | 19 | 20 | 21 | 22 | { title } 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /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 | 53 | -------------------------------------------------------------------------------- /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!.onPopoverRendered = 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 | 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 |
79 | 80 | 81 | 82 |