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"); | |
| }); | |
| } | |