alessandro trinca tornidor
chore: merge tag 1.3.5 from https://github.com/kamranahmedse/driver.js
e1fa40c
import { AllowedButtons, destroyPopover, Popover } from "./popover"; | |
import { destroyOverlay } from "./overlay"; | |
import { destroyEvents, initEvents, requireRefresh } from "./events"; | |
import { Config, configure, DriverHook, getConfig, getCurrentDriver, setCurrentDriver } from "./config"; | |
import { destroyHighlight, highlight } from "./highlight"; | |
import { destroyEmitter, listen } from "./emitter"; | |
import { getState, resetState, setState } from "./state"; | |
import "./driver.css"; | |
export type DriveStep = { | |
element?: string | Element | (() => Element); | |
onHighlightStarted?: DriverHook; | |
onHighlighted?: DriverHook; | |
onDeselected?: DriverHook; | |
popover?: Popover; | |
disableActiveInteraction?: boolean; | |
}; | |
export interface Driver { | |
isActive: () => boolean; | |
refresh: () => void; | |
drive: (stepIndex?: number) => void; | |
setConfig: (config: Config) => void; | |
setSteps: (steps: DriveStep[]) => void; | |
getConfig: () => Config; | |
getState: (key?: string) => any; | |
getActiveIndex: () => number | undefined; | |
isFirstStep: () => boolean; | |
isLastStep: () => boolean; | |
getActiveStep: () => DriveStep | undefined; | |
getActiveElement: () => Element | undefined; | |
getPreviousElement: () => Element | undefined; | |
getPreviousStep: () => DriveStep | undefined; | |
moveNext: () => void; | |
movePrevious: () => void; | |
moveTo: (index: number) => void; | |
hasNextStep: () => boolean; | |
hasPreviousStep: () => boolean; | |
highlight: (step: DriveStep) => void; | |
destroy: () => void; | |
} | |
export function driver(options: Config = {}): Driver { | |
configure(options); | |
function handleClose() { | |
if (!getConfig("allowClose")) { | |
return; | |
} | |
destroy(); | |
} | |
function handleOverlayClick() { | |
const overlayClickBehavior = getConfig("overlayClickBehavior"); | |
if (getConfig("allowClose") && overlayClickBehavior === "close") { | |
destroy(); | |
return; | |
} | |
if (overlayClickBehavior === "nextStep") { | |
moveNext(); | |
} | |
} | |
function moveNext() { | |
const activeIndex = getState("activeIndex"); | |
const steps = getConfig("steps") || []; | |
if (typeof activeIndex === "undefined") { | |
return; | |
} | |
const nextStepIndex = activeIndex + 1; | |
if (steps[nextStepIndex]) { | |
drive(nextStepIndex); | |
} else { | |
destroy(); | |
} | |
} | |
function movePrevious() { | |
const activeIndex = getState("activeIndex"); | |
const steps = getConfig("steps") || []; | |
if (typeof activeIndex === "undefined") { | |
return; | |
} | |
const previousStepIndex = activeIndex - 1; | |
if (steps[previousStepIndex]) { | |
drive(previousStepIndex); | |
} else { | |
destroy(); | |
} | |
} | |
function moveTo(index: number) { | |
const steps = getConfig("steps") || []; | |
if (steps[index]) { | |
drive(index); | |
} else { | |
destroy(); | |
} | |
} | |
function handleArrowLeft() { | |
const isTransitioning = getState("__transitionCallback"); | |
if (isTransitioning) { | |
return; | |
} | |
const activeIndex = getState("activeIndex"); | |
const activeStep = getState("__activeStep"); | |
const activeElement = getState("__activeElement"); | |
if (typeof activeIndex === "undefined" || typeof activeStep === "undefined") { | |
return; | |
} | |
const currentStepIndex = getState("activeIndex"); | |
if (typeof currentStepIndex === "undefined") { | |
return; | |
} | |
const onPrevClick = activeStep.popover?.onPrevClick || getConfig("onPrevClick"); | |
if (onPrevClick) { | |
return onPrevClick(activeElement, activeStep, { | |
config: getConfig(), | |
state: getState(), | |
driver: getCurrentDriver(), | |
}); | |
} | |
movePrevious(); | |
} | |
function handleArrowRight() { | |
const isTransitioning = getState("__transitionCallback"); | |
if (isTransitioning) { | |
return; | |
} | |
const activeIndex = getState("activeIndex"); | |
const activeStep = getState("__activeStep"); | |
const activeElement = getState("__activeElement"); | |
if (typeof activeIndex === "undefined" || typeof activeStep === "undefined") { | |
return; | |
} | |
const onNextClick = activeStep.popover?.onNextClick || getConfig("onNextClick"); | |
if (onNextClick) { | |
return onNextClick(activeElement, activeStep, { | |
config: getConfig(), | |
state: getState(), | |
driver: getCurrentDriver(), | |
}); | |
} | |
moveNext(); | |
} | |
function init() { | |
if (getState("isInitialized")) { | |
return; | |
} | |
setState("isInitialized", true); | |
document.body.classList.add("driver-active", getConfig("animate") ? "driver-fade" : "driver-simple"); | |
initEvents(); | |
listen("overlayClick", handleOverlayClick); | |
listen("escapePress", handleClose); | |
listen("arrowLeftPress", handleArrowLeft); | |
listen("arrowRightPress", handleArrowRight); | |
} | |
function drive(stepIndex: number = 0) { | |
const steps = getConfig("steps"); | |
if (!steps) { | |
console.error("No steps to drive through"); | |
destroy(); | |
return; | |
} | |
if (!steps[stepIndex]) { | |
destroy(); | |
return; | |
} | |
setState("__activeOnDestroyed", document.activeElement as HTMLElement); | |
setState("activeIndex", stepIndex); | |
const currentStep = steps[stepIndex]; | |
const hasNextStep = steps[stepIndex + 1]; | |
const hasPreviousStep = steps[stepIndex - 1]; | |
const doneBtnText = currentStep.popover?.doneBtnText || getConfig("doneBtnText") || "Done"; | |
const allowsClosing = getConfig("allowClose"); | |
const showProgress = | |
typeof currentStep.popover?.showProgress !== "undefined" | |
? currentStep.popover?.showProgress | |
: getConfig("showProgress"); | |
const progressText = currentStep.popover?.progressText || getConfig("progressText") || "{{current}} of {{total}}"; | |
const progressTextReplaced = progressText | |
.replace("{{current}}", `${stepIndex + 1}`) | |
.replace("{{total}}", `${steps.length}`); | |
const configuredButtons = currentStep.popover?.showButtons || getConfig("showButtons"); | |
const calculatedButtons: AllowedButtons[] = [ | |
"next", | |
"previous", | |
...(allowsClosing ? ["close" as AllowedButtons] : []), | |
].filter(b => { | |
return !configuredButtons?.length || configuredButtons.includes(b as AllowedButtons); | |
}) as AllowedButtons[]; | |
const onNextClick = currentStep.popover?.onNextClick || getConfig("onNextClick"); | |
const onPrevClick = currentStep.popover?.onPrevClick || getConfig("onPrevClick"); | |
const onCloseClick = currentStep.popover?.onCloseClick || getConfig("onCloseClick"); | |
highlight({ | |
...currentStep, | |
popover: { | |
showButtons: calculatedButtons, | |
nextBtnText: !hasNextStep ? doneBtnText : undefined, | |
disableButtons: [...(!hasPreviousStep ? ["previous" as AllowedButtons] : [])], | |
showProgress: showProgress, | |
progressText: progressTextReplaced, | |
onNextClick: onNextClick | |
? onNextClick | |
: () => { | |
if (!hasNextStep) { | |
destroy(); | |
} else { | |
drive(stepIndex + 1); | |
} | |
}, | |
onPrevClick: onPrevClick | |
? onPrevClick | |
: () => { | |
drive(stepIndex - 1); | |
}, | |
onCloseClick: onCloseClick | |
? onCloseClick | |
: () => { | |
destroy(); | |
}, | |
...(currentStep?.popover || {}), | |
}, | |
}); | |
} | |
function destroy(withOnDestroyStartedHook = true) { | |
const activeElement = getState("__activeElement"); | |
const activeStep = getState("__activeStep"); | |
const activeOnDestroyed = getState("__activeOnDestroyed"); | |
const onDestroyStarted = getConfig("onDestroyStarted"); | |
// `onDestroyStarted` is used to confirm the exit of tour. If we trigger | |
// the hook for when user calls `destroy`, driver will get into infinite loop | |
// not causing tour to be destroyed. | |
if (withOnDestroyStartedHook && onDestroyStarted) { | |
const isActiveDummyElement = !activeElement || activeElement?.id === "driver-dummy-element"; | |
onDestroyStarted(isActiveDummyElement ? undefined : activeElement, activeStep!, { | |
config: getConfig(), | |
state: getState(), | |
driver: getCurrentDriver(), | |
}); | |
return; | |
} | |
const onDeselected = activeStep?.onDeselected || getConfig("onDeselected"); | |
const onDestroyed = getConfig("onDestroyed"); | |
document.body.classList.remove("driver-active", "driver-fade", "driver-simple"); | |
destroyEvents(); | |
destroyPopover(); | |
destroyHighlight(); | |
destroyOverlay(); | |
destroyEmitter(); | |
resetState(); | |
if (activeElement && activeStep) { | |
const isActiveDummyElement = activeElement.id === "driver-dummy-element"; | |
if (onDeselected) { | |
onDeselected(isActiveDummyElement ? undefined : activeElement, activeStep, { | |
config: getConfig(), | |
state: getState(), | |
driver: getCurrentDriver(), | |
}); | |
} | |
if (onDestroyed) { | |
onDestroyed(isActiveDummyElement ? undefined : activeElement, activeStep, { | |
config: getConfig(), | |
state: getState(), | |
driver: getCurrentDriver(), | |
}); | |
} | |
} | |
if (activeOnDestroyed) { | |
(activeOnDestroyed as HTMLElement).focus(); | |
} | |
} | |
const api: Driver = { | |
isActive: () => getState("isInitialized") || false, | |
refresh: requireRefresh, | |
drive: (stepIndex: number = 0) => { | |
init(); | |
drive(stepIndex); | |
}, | |
setConfig: configure, | |
setSteps: (steps: DriveStep[]) => { | |
resetState(); | |
configure({ | |
...getConfig(), | |
steps, | |
}); | |
}, | |
getConfig, | |
getState, | |
getActiveIndex: () => getState("activeIndex"), | |
isFirstStep: () => getState("activeIndex") === 0, | |
isLastStep: () => { | |
const steps = getConfig("steps") || []; | |
const activeIndex = getState("activeIndex"); | |
return activeIndex !== undefined && activeIndex === steps.length - 1; | |
}, | |
getActiveStep: () => getState("activeStep"), | |
getActiveElement: () => getState("activeElement"), | |
getPreviousElement: () => getState("previousElement"), | |
getPreviousStep: () => getState("previousStep"), | |
moveNext, | |
movePrevious, | |
moveTo, | |
hasNextStep: () => { | |
const steps = getConfig("steps") || []; | |
const activeIndex = getState("activeIndex"); | |
return activeIndex !== undefined && !!steps[activeIndex + 1]; | |
}, | |
hasPreviousStep: () => { | |
const steps = getConfig("steps") || []; | |
const activeIndex = getState("activeIndex"); | |
return activeIndex !== undefined && !!steps[activeIndex - 1]; | |
}, | |
highlight: (step: DriveStep) => { | |
init(); | |
highlight({ | |
...step, | |
popover: step.popover | |
? { | |
showButtons: [], | |
showProgress: false, | |
progressText: "", | |
...step.popover!, | |
} | |
: undefined, | |
}); | |
}, | |
destroy: () => { | |
destroy(false); | |
}, | |
}; | |
setCurrentDriver(api); | |
return api; | |
} | |