mirror of
https://github.com/kamranahmedse/driver.js.git
synced 2026-11-03 14:32:52 +08:00
Implement the modal style popover
This commit is contained in:
+1
-1
@@ -292,7 +292,7 @@ npm install driver.js</pre
|
||||
}, 2000);
|
||||
|
||||
window.setTimeout(() => {
|
||||
driverObj.highlight({ element: ".buttons button:nth-child(5)" });
|
||||
driverObj.highlight({});
|
||||
}, 4000);
|
||||
|
||||
window.setTimeout(() => {
|
||||
|
||||
@@ -5,6 +5,7 @@ export type Config = {
|
||||
opacity?: number;
|
||||
stagePadding?: number;
|
||||
stageRadius?: number;
|
||||
popoverOffset?: number;
|
||||
};
|
||||
|
||||
let currentConfig: Config = {};
|
||||
@@ -17,6 +18,7 @@ export function configure(config: Config = {}) {
|
||||
smoothScroll: false,
|
||||
stagePadding: 10,
|
||||
stageRadius: 5,
|
||||
popoverOffset: 10,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
+7
-14
@@ -6,14 +6,13 @@ import { destroyHighlight, highlight } from "./highlight";
|
||||
import { destroyEmitter, listen } from "./emitter";
|
||||
|
||||
import "./style.css";
|
||||
import { getState, setState } from "./state";
|
||||
|
||||
export type DriveStep = {
|
||||
element?: string | Element;
|
||||
popover?: Popover;
|
||||
};
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
export function driver(options: Config = {}) {
|
||||
configure(options);
|
||||
|
||||
@@ -26,15 +25,12 @@ export function driver(options: Config = {}) {
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (isInitialized) {
|
||||
if (getState("isInitialized")) {
|
||||
return;
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
document.body.classList.add(
|
||||
"driver-active",
|
||||
getConfig("animate") ? "driver-fade" : "driver-simple"
|
||||
);
|
||||
setState("isInitialized", true);
|
||||
document.body.classList.add("driver-active", getConfig("animate") ? "driver-fade" : "driver-simple");
|
||||
|
||||
initEvents();
|
||||
|
||||
@@ -44,11 +40,9 @@ export function driver(options: Config = {}) {
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
isInitialized = false;
|
||||
document.body.classList.remove(
|
||||
"driver-active",
|
||||
getConfig("animate") ? "driver-fade" : "driver-simple"
|
||||
);
|
||||
setState("isInitialized", false);
|
||||
|
||||
document.body.classList.remove("driver-active", "driver-fade", "driver-simple");
|
||||
|
||||
destroyEvents();
|
||||
destroyPopover();
|
||||
@@ -57,7 +51,6 @@ export function driver(options: Config = {}) {
|
||||
destroyEmitter();
|
||||
}
|
||||
|
||||
// @todo make popover selectable
|
||||
return {
|
||||
drive: (steps: DriveStep[]) => console.log(steps),
|
||||
highlight: (step: DriveStep) => {
|
||||
|
||||
+4
-7
@@ -1,14 +1,14 @@
|
||||
import { refreshActiveHighlight } from "./highlight";
|
||||
import { emit } from "./emitter";
|
||||
|
||||
let resizeTimeout: number;
|
||||
import { getState, setState } from "./state";
|
||||
|
||||
function requireRefresh() {
|
||||
const resizeTimeout = getState("resizeTimeout");
|
||||
if (resizeTimeout) {
|
||||
window.cancelAnimationFrame(resizeTimeout);
|
||||
}
|
||||
|
||||
resizeTimeout = window.requestAnimationFrame(refreshActiveHighlight);
|
||||
setState("resizeTimeout", window.requestAnimationFrame(refreshActiveHighlight));
|
||||
}
|
||||
|
||||
function onKeyup(e: KeyboardEvent) {
|
||||
@@ -32,10 +32,7 @@ export function onDriverClick(
|
||||
listener: (pointer: MouseEvent | PointerEvent) => void,
|
||||
shouldPreventDefault?: (target: HTMLElement) => boolean
|
||||
) {
|
||||
const listenerWrapper = (
|
||||
e: MouseEvent | PointerEvent,
|
||||
listener?: (pointer: MouseEvent | PointerEvent) => void
|
||||
) => {
|
||||
const listenerWrapper = (e: MouseEvent | PointerEvent, listener?: (pointer: MouseEvent | PointerEvent) => void) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!element.contains(target)) {
|
||||
return;
|
||||
|
||||
+21
-15
@@ -3,10 +3,7 @@ import { refreshStage, trackActiveElement, transitionStage } from "./stage";
|
||||
import { getConfig } from "./config";
|
||||
import { repositionPopover, renderPopover, hidePopover } from "./popover";
|
||||
import { bringInView } from "./utils";
|
||||
|
||||
let previousHighlight: Element | undefined;
|
||||
let activeHighlight: Element | undefined;
|
||||
let currentTransitionCallback: undefined | (() => void);
|
||||
import { getState, setState } from "./state";
|
||||
|
||||
function mountDummyElement(): Element {
|
||||
const existingDummy = document.getElementById("driver-dummy-element");
|
||||
@@ -38,13 +35,19 @@ export function highlight(step: DriveStep) {
|
||||
elemObj = mountDummyElement();
|
||||
}
|
||||
|
||||
previousHighlight = activeHighlight;
|
||||
activeHighlight = elemObj;
|
||||
const previousHighlight = getState("activeHighlight");
|
||||
|
||||
transferHighlight(previousHighlight || elemObj, elemObj);
|
||||
const transferHighlightFrom = previousHighlight || elemObj;
|
||||
const transferHighlightTo = elemObj;
|
||||
|
||||
transferHighlight(transferHighlightFrom, transferHighlightTo);
|
||||
|
||||
setState("previousHighlight", transferHighlightFrom);
|
||||
setState("activeHighlight", transferHighlightTo);
|
||||
}
|
||||
|
||||
export function refreshActiveHighlight() {
|
||||
const activeHighlight = getState("activeHighlight");
|
||||
if (!activeHighlight) {
|
||||
return;
|
||||
}
|
||||
@@ -61,15 +64,17 @@ function transferHighlight(from: Element, to: Element) {
|
||||
// If it's the first time we're highlighting an element, we show
|
||||
// the popover immediately. Otherwise, we wait for the animation
|
||||
// to finish before showing the popover.
|
||||
const hasDelayedPopover = !from || from !== to;
|
||||
const hasDelayedPopover = to && (!from || from !== to);
|
||||
|
||||
hidePopover();
|
||||
|
||||
const animate = () => {
|
||||
const transitionCallback = getState("transitionCallback");
|
||||
|
||||
// This makes sure that the repeated calls to transferHighlight
|
||||
// don't interfere with each other. Only the last call will be
|
||||
// executed.
|
||||
if (currentTransitionCallback !== animate) {
|
||||
if (transitionCallback !== animate) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,13 +89,13 @@ function transferHighlight(from: Element, to: Element) {
|
||||
renderPopover(to);
|
||||
}
|
||||
|
||||
currentTransitionCallback = undefined;
|
||||
setState("transitionCallback", undefined);
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
currentTransitionCallback = animate;
|
||||
setState("transitionCallback", animate);
|
||||
window.requestAnimationFrame(animate);
|
||||
|
||||
bringInView(to);
|
||||
@@ -103,10 +108,11 @@ function transferHighlight(from: Element, to: Element) {
|
||||
}
|
||||
|
||||
export function destroyHighlight() {
|
||||
activeHighlight = undefined;
|
||||
currentTransitionCallback = undefined;
|
||||
previousHighlight = undefined;
|
||||
activeHighlight = undefined;
|
||||
setState("activeHighlight", undefined);
|
||||
setState("previousHighlight", undefined);
|
||||
|
||||
setState("transitionCallback", undefined);
|
||||
|
||||
document.getElementById("driver-dummy-element")?.remove();
|
||||
|
||||
document.querySelectorAll(".driver-active-element").forEach(element => {
|
||||
|
||||
+26
-11
@@ -1,11 +1,10 @@
|
||||
import { bringInView } from "./utils";
|
||||
import { getConfig } from "./config";
|
||||
import { getState, setState } from "./state";
|
||||
|
||||
export type Side = "top" | "right" | "bottom" | "left";
|
||||
export type Side = "top" | "right" | "bottom" | "left" | "over";
|
||||
export type Alignment = "start" | "center" | "end";
|
||||
|
||||
const POPOVER_OFFSET = 10;
|
||||
|
||||
export type Popover = {
|
||||
title?: string;
|
||||
description: string;
|
||||
@@ -13,7 +12,7 @@ export type Popover = {
|
||||
align?: Alignment;
|
||||
};
|
||||
|
||||
type PopoverDOM = {
|
||||
export type PopoverDOM = {
|
||||
wrapper: HTMLElement;
|
||||
arrow: HTMLElement;
|
||||
title: HTMLElement;
|
||||
@@ -25,9 +24,8 @@ type PopoverDOM = {
|
||||
footerButtons: HTMLElement;
|
||||
};
|
||||
|
||||
let popover: PopoverDOM | undefined;
|
||||
|
||||
export function hidePopover() {
|
||||
const popover = getState("popover");
|
||||
if (!popover) {
|
||||
return;
|
||||
}
|
||||
@@ -36,6 +34,7 @@ export function hidePopover() {
|
||||
}
|
||||
|
||||
export function renderPopover(element: Element) {
|
||||
let popover = getState("popover");
|
||||
if (!popover) {
|
||||
popover = createPopover();
|
||||
document.body.appendChild(popover.wrapper);
|
||||
@@ -53,6 +52,8 @@ export function renderPopover(element: Element) {
|
||||
const popoverArrow = popover.arrow;
|
||||
popoverArrow.className = "driver-popover-arrow";
|
||||
|
||||
setState("popover", popover);
|
||||
|
||||
repositionPopover(element);
|
||||
bringInView(popoverWrapper);
|
||||
}
|
||||
@@ -65,16 +66,19 @@ type PopoverDimensions = {
|
||||
};
|
||||
|
||||
function getPopoverDimensions(): PopoverDimensions | undefined {
|
||||
const popover = getState("popover");
|
||||
if (!popover?.wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const boundingClientRect = popover.wrapper.getBoundingClientRect();
|
||||
|
||||
const stagePadding = getConfig("stagePadding") || 0;
|
||||
const popoverOffset = getConfig("popoverOffset") || 0;
|
||||
|
||||
return {
|
||||
width: boundingClientRect.width + stagePadding + POPOVER_OFFSET,
|
||||
height: boundingClientRect.height + stagePadding + POPOVER_OFFSET,
|
||||
width: boundingClientRect.width + stagePadding + popoverOffset,
|
||||
height: boundingClientRect.height + stagePadding + popoverOffset,
|
||||
|
||||
realWidth: boundingClientRect.width,
|
||||
realHeight: boundingClientRect.height,
|
||||
@@ -171,6 +175,7 @@ function calculateLeftForTopBottom(
|
||||
}
|
||||
|
||||
export function repositionPopover(element: Element) {
|
||||
const popover = getState("popover");
|
||||
if (!popover) {
|
||||
return;
|
||||
}
|
||||
@@ -178,7 +183,7 @@ export function repositionPopover(element: Element) {
|
||||
// @TODO These values will come from the config
|
||||
// Configure the popover positioning
|
||||
const requiredAlignment: Alignment = "start";
|
||||
const requiredSide: Side = "left" as Side;
|
||||
const requiredSide: Side = element.id === "driver-dummy-element" ? "over" : "left" as Side;
|
||||
const popoverPadding = getConfig('stagePadding') || 0;
|
||||
|
||||
const popoverDimensions = getPopoverDimensions()!;
|
||||
@@ -210,7 +215,15 @@ export function repositionPopover(element: Element) {
|
||||
isLeftOptimal = isTopOptimal = isBottomOptimal = false;
|
||||
}
|
||||
|
||||
if (noneOptimal) {
|
||||
if (requiredSide === "over") {
|
||||
const leftToSet = window.innerWidth / 2 - popoverDimensions!.realWidth / 2;
|
||||
const topToSet = window.innerHeight / 2 - popoverDimensions!.realHeight / 2;
|
||||
|
||||
popover.wrapper.style.left = `${leftToSet}px`;
|
||||
popover.wrapper.style.right = `auto`;
|
||||
popover.wrapper.style.top = `${topToSet}px`;
|
||||
popover.wrapper.style.bottom = `auto`;
|
||||
} else if (noneOptimal) {
|
||||
const leftValue = window.innerWidth / 2 - popoverDimensions?.realWidth! / 2;
|
||||
const bottomValue = 10;
|
||||
|
||||
@@ -303,6 +316,7 @@ export function repositionPopover(element: Element) {
|
||||
}
|
||||
|
||||
function renderPopoverArrow(alignment: Alignment, side: Side, element: Element) {
|
||||
const popover = getState("popover");
|
||||
if (!popover) {
|
||||
return;
|
||||
}
|
||||
@@ -458,10 +472,11 @@ function createPopover(): PopoverDOM {
|
||||
}
|
||||
|
||||
export function destroyPopover() {
|
||||
const popover = getState("popover");
|
||||
if (!popover) {
|
||||
return;
|
||||
}
|
||||
|
||||
popover.wrapper.parentElement?.removeChild(popover.wrapper);
|
||||
popover = undefined;
|
||||
setState("popover", undefined);
|
||||
}
|
||||
|
||||
+29
-19
@@ -2,6 +2,7 @@ import { easeInOutQuad } from "./utils";
|
||||
import { onDriverClick } from "./events";
|
||||
import { emit } from "./emitter";
|
||||
import { getConfig } from "./config";
|
||||
import { getState, setState } from "./state";
|
||||
|
||||
export type StageDefinition = {
|
||||
x: number;
|
||||
@@ -10,14 +11,12 @@ export type StageDefinition = {
|
||||
height: number;
|
||||
};
|
||||
|
||||
let activeStagePosition: StageDefinition | undefined;
|
||||
let stageSvg: SVGSVGElement | undefined;
|
||||
|
||||
// This method calculates the animated new position of the
|
||||
// stage (called for each frame by requestAnimationFrame)
|
||||
export function transitionStage(elapsed: number, duration: number, from: Element, to: Element) {
|
||||
const fromDefinition = activeStagePosition ? activeStagePosition : from.getBoundingClientRect();
|
||||
let activeStagePosition = getState("activeStagePosition");
|
||||
|
||||
const fromDefinition = activeStagePosition ? activeStagePosition : from.getBoundingClientRect();
|
||||
const toDefinition = to.getBoundingClientRect();
|
||||
|
||||
const x = easeInOutQuad(elapsed, fromDefinition.x, toDefinition.x - fromDefinition.x, duration);
|
||||
@@ -33,6 +32,7 @@ export function transitionStage(elapsed: number, duration: number, from: Element
|
||||
};
|
||||
|
||||
renderStage(activeStagePosition);
|
||||
setState("activeStagePosition", activeStagePosition);
|
||||
}
|
||||
|
||||
export function trackActiveElement(element: Element) {
|
||||
@@ -42,17 +42,22 @@ export function trackActiveElement(element: Element) {
|
||||
|
||||
const definition = element.getBoundingClientRect();
|
||||
|
||||
activeStagePosition = {
|
||||
const activeStagePosition: StageDefinition = {
|
||||
x: definition.x,
|
||||
y: definition.y,
|
||||
width: definition.width,
|
||||
height: definition.height,
|
||||
};
|
||||
|
||||
setState("activeStagePosition", activeStagePosition);
|
||||
|
||||
renderStage(activeStagePosition);
|
||||
}
|
||||
|
||||
export function refreshStage() {
|
||||
const activeStagePosition = getState("activeStagePosition");
|
||||
const stageSvg = getState("stageSvg");
|
||||
|
||||
if (!activeStagePosition) {
|
||||
return;
|
||||
}
|
||||
@@ -69,7 +74,7 @@ export function refreshStage() {
|
||||
}
|
||||
|
||||
function mountStage(stagePosition: StageDefinition) {
|
||||
stageSvg = createStageSvg(stagePosition);
|
||||
const stageSvg = createStageSvg(stagePosition);
|
||||
document.body.appendChild(stageSvg);
|
||||
|
||||
onDriverClick(stageSvg, e => {
|
||||
@@ -80,9 +85,13 @@ function mountStage(stagePosition: StageDefinition) {
|
||||
|
||||
emit("overlayClick");
|
||||
});
|
||||
|
||||
setState("stageSvg", stageSvg);
|
||||
}
|
||||
|
||||
function renderStage(stagePosition: StageDefinition) {
|
||||
const stageSvg = getState("stageSvg");
|
||||
|
||||
// TODO: cancel rendering if element is not visible
|
||||
if (!stageSvg) {
|
||||
mountStage(stagePosition);
|
||||
@@ -95,7 +104,7 @@ function renderStage(stagePosition: StageDefinition) {
|
||||
throw new Error("no path element found in stage svg");
|
||||
}
|
||||
|
||||
pathElement.setAttribute("d", generateSvgCutoutPathString(stagePosition));
|
||||
pathElement.setAttribute("d", generateStageSvgPathString(stagePosition));
|
||||
}
|
||||
|
||||
function createStageSvg(stage: StageDefinition): SVGSVGElement {
|
||||
@@ -122,26 +131,26 @@ function createStageSvg(stage: StageDefinition): SVGSVGElement {
|
||||
svg.style.width = "100%";
|
||||
svg.style.height = "100%";
|
||||
|
||||
const cutoutPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
const stagePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
|
||||
cutoutPath.setAttribute("d", generateSvgCutoutPathString(stage));
|
||||
stagePath.setAttribute("d", generateStageSvgPathString(stage));
|
||||
|
||||
cutoutPath.style.fill = "rgb(0,0,0)";
|
||||
cutoutPath.style.opacity = `${getConfig("opacity")}`;
|
||||
cutoutPath.style.pointerEvents = "auto";
|
||||
cutoutPath.style.cursor = "auto";
|
||||
stagePath.style.fill = "rgb(0,0,0)";
|
||||
stagePath.style.opacity = `${getConfig("opacity")}`;
|
||||
stagePath.style.pointerEvents = "auto";
|
||||
stagePath.style.cursor = "auto";
|
||||
|
||||
svg.appendChild(cutoutPath);
|
||||
svg.appendChild(stagePath);
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
function generateSvgCutoutPathString(stage: StageDefinition) {
|
||||
function generateStageSvgPathString(stage: StageDefinition) {
|
||||
const windowX = window.innerWidth;
|
||||
const windowY = window.innerHeight;
|
||||
|
||||
const stagePadding = getConfig('stagePadding') || 0;
|
||||
const stageRadius = getConfig('stageRadius') || 0;
|
||||
const stagePadding = getConfig("stagePadding") || 0;
|
||||
const stageRadius = getConfig("stageRadius") || 0;
|
||||
|
||||
const stageWidth = stage.width + stagePadding * 2;
|
||||
const stageHeight = stage.height + stagePadding * 2;
|
||||
@@ -162,10 +171,11 @@ function generateSvgCutoutPathString(stage: StageDefinition) {
|
||||
}
|
||||
|
||||
export function destroyStage() {
|
||||
const stageSvg = getState("stageSvg");
|
||||
if (stageSvg) {
|
||||
stageSvg.remove();
|
||||
stageSvg = undefined;
|
||||
setState("stageSvg", undefined);
|
||||
}
|
||||
|
||||
activeStagePosition = undefined;
|
||||
setState("activeStagePosition", undefined);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { StageDefinition } from "./stage";
|
||||
import { PopoverDOM } from "./popover";
|
||||
|
||||
export type State = {
|
||||
isInitialized?: boolean;
|
||||
resizeTimeout?: number;
|
||||
|
||||
previousHighlight?: Element;
|
||||
activeHighlight?: Element;
|
||||
transitionCallback?: () => void;
|
||||
|
||||
activeStagePosition?: StageDefinition;
|
||||
stageSvg?: SVGSVGElement;
|
||||
|
||||
popover?: PopoverDOM;
|
||||
};
|
||||
|
||||
let currentState: State = {};
|
||||
|
||||
export function setState<K extends keyof State>(key: K, value: State[K]) {
|
||||
currentState[key] = value;
|
||||
}
|
||||
|
||||
export function getState(): State;
|
||||
export function getState<K extends keyof State>(key: K): State[K];
|
||||
export function getState<K extends keyof State>(key?: K) {
|
||||
return key ? currentState[key] : currentState;
|
||||
}
|
||||
+6
-1
@@ -8,7 +8,8 @@
|
||||
|
||||
.driver-active .driver-active-element,
|
||||
.driver-active .driver-active-element *,
|
||||
.driver-popover, .driver-popover *{
|
||||
.driver-popover,
|
||||
.driver-popover * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -51,6 +52,10 @@
|
||||
border: 5px solid #fff;
|
||||
}
|
||||
|
||||
.driver-popover-arrow-side-over {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/** Popover Arrow Sides **/
|
||||
.driver-popover-arrow-side-left {
|
||||
left: 100%;
|
||||
|
||||
Reference in New Issue
Block a user