alessandro trinca tornidor
chore: merge tag 1.3.5 from https://github.com/kamranahmedse/driver.js
e1fa40c
import { DriveStep } from "./driver"; | |
import { refreshOverlay, trackActiveElement, transitionStage } from "./overlay"; | |
import { getConfig, getCurrentDriver } from "./config"; | |
import { hidePopover, renderPopover, repositionPopover } from "./popover"; | |
import { bringInView } from "./utils"; | |
import { getState, setState } from "./state"; | |
function mountDummyElement(): Element { | |
const existingDummy = document.getElementById("driver-dummy-element"); | |
if (existingDummy) { | |
return existingDummy; | |
} | |
let element = document.createElement("div"); | |
element.id = "driver-dummy-element"; | |
element.style.width = "0"; | |
element.style.height = "0"; | |
element.style.pointerEvents = "none"; | |
element.style.opacity = "0"; | |
element.style.position = "fixed"; | |
element.style.top = "50%"; | |
element.style.left = "50%"; | |
document.body.appendChild(element); | |
return element; | |
} | |
export function highlight(step: DriveStep) { | |
const { element } = step; | |
let elemObj: Element | null = null; | |
if (typeof element === "string") { | |
elemObj = document.querySelector(element); | |
if (! elemObj || elemObj.getBoundingClientRect().width === 0) { | |
elemObj = null; | |
document.querySelectorAll(element).forEach(function (el) { | |
var rect = el.getBoundingClientRect(); | |
if (!elemObj && rect.width > 0 && rect.height > 0) { | |
elemObj = el; | |
return; | |
} | |
}); | |
} | |
} else if (element instanceof Element) { | |
elemObj = element; | |
} | |
// If the element is not found, we mount a 1px div | |
// at the center of the screen to highlight and show | |
// the popover on top of that. This is to show a | |
// modal-like highlight. | |
if (!elemObj) { | |
elemObj = mountDummyElement(); | |
} | |
transferHighlight(elemObj, step); | |
} | |
export function refreshActiveHighlight() { | |
const activeHighlight = getState("__activeElement"); | |
const activeStep = getState("__activeStep")!; | |
if (!activeHighlight) { | |
return; | |
} | |
trackActiveElement(activeHighlight); | |
refreshOverlay(); | |
repositionPopover(activeHighlight, activeStep); | |
} | |
function transferHighlight(toElement: Element, toStep: DriveStep) { | |
const duration = 400; | |
const start = Date.now(); | |
const fromStep = getState("__activeStep"); | |
const fromElement = getState("__activeElement") || toElement; | |
// If it's the first time we're highlighting an element, we show | |
// the popover immediately. Otherwise, we wait for the animation | |
// to finish before showing the popover. | |
const isFirstHighlight = !fromElement || fromElement === toElement; | |
const isToDummyElement = toElement.id === "driver-dummy-element"; | |
const isFromDummyElement = fromElement.id === "driver-dummy-element"; | |
const isAnimatedTour = getConfig("animate"); | |
const highlightStartedHook = toStep.onHighlightStarted || getConfig("onHighlightStarted"); | |
const highlightedHook = toStep?.onHighlighted || getConfig("onHighlighted"); | |
const deselectedHook = fromStep?.onDeselected || getConfig("onDeselected"); | |
const config = getConfig(); | |
const state = getState(); | |
if (!isFirstHighlight && deselectedHook) { | |
deselectedHook(isFromDummyElement ? undefined : fromElement, fromStep!, { | |
config, | |
state, | |
driver: getCurrentDriver(), | |
}); | |
} | |
if (highlightStartedHook) { | |
highlightStartedHook(isToDummyElement ? undefined : toElement, toStep, { | |
config, | |
state, | |
driver: getCurrentDriver(), | |
}); | |
} | |
const hasDelayedPopover = !isFirstHighlight && isAnimatedTour; | |
let isPopoverRendered = false; | |
hidePopover(); | |
setState("previousStep", fromStep); | |
setState("previousElement", fromElement); | |
setState("activeStep", toStep); | |
setState("activeElement", toElement); | |
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 (transitionCallback !== animate) { | |
return; | |
} | |
const elapsed = Date.now() - start; | |
const timeRemaining = duration - elapsed; | |
const isHalfwayThrough = timeRemaining <= duration / 2; | |
if (toStep.popover && isHalfwayThrough && !isPopoverRendered && hasDelayedPopover) { | |
renderPopover(toElement, toStep); | |
isPopoverRendered = true; | |
} | |
if (getConfig("animate") && elapsed < duration) { | |
transitionStage(elapsed, duration, fromElement, toElement); | |
} else { | |
trackActiveElement(toElement); | |
if (highlightedHook) { | |
highlightedHook(isToDummyElement ? undefined : toElement, toStep, { | |
config: getConfig(), | |
state: getState(), | |
driver: getCurrentDriver(), | |
}); | |
} | |
setState("__transitionCallback", undefined); | |
setState("__previousStep", fromStep); | |
setState("__previousElement", fromElement); | |
setState("__activeStep", toStep); | |
setState("__activeElement", toElement); | |
} | |
window.requestAnimationFrame(animate); | |
}; | |
setState("__transitionCallback", animate); | |
window.requestAnimationFrame(animate); | |
bringInView(toElement); | |
if (!hasDelayedPopover && toStep.popover) { | |
renderPopover(toElement, toStep); | |
} | |
fromElement.classList.remove("driver-active-element", "driver-no-interaction"); | |
fromElement.removeAttribute("aria-haspopup"); | |
fromElement.removeAttribute("aria-expanded"); | |
fromElement.removeAttribute("aria-controls"); | |
const disableActiveInteraction = toStep.disableActiveInteraction ?? getConfig("disableActiveInteraction"); | |
if (disableActiveInteraction) { | |
toElement.classList.add("driver-no-interaction"); | |
} | |
toElement.classList.add("driver-active-element"); | |
toElement.setAttribute("aria-haspopup", "dialog"); | |
toElement.setAttribute("aria-expanded", "true"); | |
toElement.setAttribute("aria-controls", "driver-popover-content"); | |
} | |
export function destroyHighlight() { | |
document.getElementById("driver-dummy-element")?.remove(); | |
document.querySelectorAll(".driver-active-element").forEach(element => { | |
element.classList.remove("driver-active-element", "driver-no-interaction"); | |
element.removeAttribute("aria-haspopup"); | |
element.removeAttribute("aria-expanded"); | |
element.removeAttribute("aria-controls"); | |
}); | |
} | |