| import { AllowedButtons, destroyPopover, Popover } from "./popover"; | |
| import { destroyStage } from "./stage"; | |
| import { destroyEvents, initEvents, requireRefresh } from "./events"; | |
| import { Config, configure, getConfig } from "./config"; | |
| import { destroyHighlight, highlight } from "./highlight"; | |
| import { destroyEmitter, listen } from "./emitter"; | |
| import "./style.css"; | |
| import { getState, resetState, setState } from "./state"; | |
| export type DriveStep = { | |
| element?: string | Element; | |
| popover?: Popover; | |
| }; | |
| export function driver(options: Config = {}) { | |
| configure(options); | |
| function handleClose() { | |
| if (!getConfig("allowClose")) { | |
| return; | |
| } | |
| destroy(); | |
| } | |
| 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 handleArrowLeft() { | |
| const steps = getConfig("steps") || []; | |
| const currentStepIndex = getState("activeIndex"); | |
| if (typeof currentStepIndex === "undefined") { | |
| return; | |
| } | |
| const previousStepIndex = currentStepIndex - 1; | |
| if (steps[previousStepIndex]) { | |
| drive(previousStepIndex); | |
| } | |
| } | |
| function handleArrowRight() { | |
| 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); | |
| } | |
| 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", handleClose); | |
| 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]) { | |
| console.warn(`Step not found at index: ${stepIndex}`); | |
| destroy(); | |
| } | |
| 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"); | |
| highlight({ | |
| ...currentStep, | |
| popover: { | |
| showButtons: ["next", "previous", ...(allowsClosing ? ["close" as AllowedButtons] : [])], | |
| nextBtnText: !hasNextStep ? doneBtnText : undefined, | |
| disableButtons: [...(!hasPreviousStep ? ["previous" as AllowedButtons] : [])], | |
| onNextClick: () => { | |
| if (!hasNextStep) { | |
| destroy(); | |
| } else { | |
| drive(stepIndex + 1); | |
| } | |
| }, | |
| onPrevClick: () => { | |
| drive(stepIndex - 1); | |
| }, | |
| onCloseClick: () => { | |
| destroy(); | |
| }, | |
| ...(currentStep?.popover || {}), | |
| }, | |
| }); | |
| } | |
| function destroy() { | |
| const activeElement = getState("activeElement"); | |
| const activeStep = getState("activeStep"); | |
| const onDeselected = getConfig("onDeselected"); | |
| const onDestroyed = getConfig("onDestroyed"); | |
| document.body.classList.remove("driver-active", "driver-fade", "driver-simple"); | |
| destroyEvents(); | |
| destroyPopover(); | |
| destroyHighlight(); | |
| destroyStage(); | |
| destroyEmitter(); | |
| resetState(); | |
| if (activeElement && activeStep) { | |
| const isActiveDummyElement = activeElement.id === "driver-dummy-element"; | |
| if (onDeselected) { | |
| onDeselected(isActiveDummyElement ? undefined : activeElement, activeStep); | |
| } | |
| if (onDestroyed) { | |
| onDestroyed(isActiveDummyElement ? undefined : activeElement, activeStep); | |
| } | |
| } | |
| } | |
| return { | |
| isActive: () => getState("isInitialized") || false, | |
| refresh: () => { | |
| requireRefresh(); | |
| }, | |
| drive: (stepIndex: number = 0) => { | |
| init(); | |
| drive(stepIndex); | |
| }, | |
| moveNext, | |
| movePrevious, | |
| highlight: (step: DriveStep) => { | |
| init(); | |
| highlight({ | |
| ...step, | |
| popover: step.popover | |
| ? { | |
| showButtons: [], | |
| ...step.popover!, | |
| } | |
| : undefined, | |
| }); | |
| }, | |
| destroy, | |
| }; | |
| } | |