|
import Overlay from './core/overlay'; |
|
import Element from './core/element'; |
|
import Popover from './core/popover'; |
|
import { |
|
CLASS_CLOSE_BTN, |
|
CLASS_NEXT_STEP_BTN, |
|
CLASS_PREV_STEP_BTN, |
|
ESC_KEY_CODE, |
|
ID_POPOVER, |
|
LEFT_KEY_CODE, |
|
OVERLAY_OPACITY, |
|
OVERLAY_PADDING, |
|
RIGHT_KEY_CODE, |
|
SHOULD_ANIMATE_OVERLAY, |
|
SHOULD_OUTSIDE_CLICK_CLOSE, |
|
SHOULD_OUTSIDE_CLICK_NEXT, |
|
ALLOW_KEYBOARD_CONTROL, |
|
} from './common/constants'; |
|
import Stage from './core/stage'; |
|
import { isDomElement } from './common/utils'; |
|
|
|
|
|
|
|
|
|
export default class Driver { |
|
|
|
|
|
|
|
constructor(options = {}) { |
|
this.options = { |
|
animate: SHOULD_ANIMATE_OVERLAY, |
|
opacity: OVERLAY_OPACITY, |
|
padding: OVERLAY_PADDING, |
|
scrollIntoViewOptions: null, |
|
allowClose: SHOULD_OUTSIDE_CLICK_CLOSE, |
|
keyboardControl: ALLOW_KEYBOARD_CONTROL, |
|
overlayClickNext: SHOULD_OUTSIDE_CLICK_NEXT, |
|
stageBackground: '#ffffff', |
|
onHighlightStarted: () => null, |
|
onHighlighted: () => null, |
|
onDeselected: () => null, |
|
onReset: () => null, |
|
onNext: () => null, |
|
onPrevious: () => null, |
|
...options, |
|
}; |
|
|
|
this.document = document; |
|
this.window = window; |
|
this.isActivated = false; |
|
this.steps = []; |
|
this.currentStep = 0; |
|
this.currentMovePrevented = false; |
|
|
|
this.overlay = new Overlay(this.options, window, document); |
|
|
|
this.onResize = this.onResize.bind(this); |
|
this.onKeyUp = this.onKeyUp.bind(this); |
|
this.onClick = this.onClick.bind(this); |
|
this.moveNext = this.moveNext.bind(this); |
|
this.movePrevious = this.movePrevious.bind(this); |
|
this.preventMove = this.preventMove.bind(this); |
|
|
|
|
|
this.bind(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
getSteps() { |
|
return this.steps; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
setSteps(steps) { |
|
this.steps = steps; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
bind() { |
|
this.window.addEventListener('resize', this.onResize, false); |
|
this.window.addEventListener('keyup', this.onKeyUp, false); |
|
this.window.addEventListener('click', this.onClick, false); |
|
this.window.addEventListener('touchstart', this.onClick, false); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onClick(e) { |
|
if (!this.isActivated || !this.hasHighlightedElement()) { |
|
return; |
|
} |
|
|
|
|
|
|
|
|
|
e.stopPropagation(); |
|
|
|
const highlightedElement = this.overlay.getHighlightedElement(); |
|
const popover = this.document.getElementById(ID_POPOVER); |
|
|
|
const clickedHighlightedElement = highlightedElement.node.contains(e.target); |
|
const clickedPopover = popover && popover.contains(e.target); |
|
|
|
if (!clickedHighlightedElement && !clickedPopover && this.options.overlayClickNext) { |
|
this.handleNext(); |
|
return; |
|
} |
|
|
|
|
|
if (!clickedHighlightedElement && !clickedPopover && this.options.allowClose) { |
|
this.reset(); |
|
return; |
|
} |
|
|
|
const nextClicked = e.target.classList.contains(CLASS_NEXT_STEP_BTN); |
|
const prevClicked = e.target.classList.contains(CLASS_PREV_STEP_BTN); |
|
const closeClicked = e.target.classList.contains(CLASS_CLOSE_BTN); |
|
|
|
if (closeClicked) { |
|
this.reset(); |
|
return; |
|
} |
|
|
|
if (nextClicked) { |
|
this.handleNext(); |
|
} else if (prevClicked) { |
|
this.handlePrevious(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onResize() { |
|
if (!this.isActivated) { |
|
return; |
|
} |
|
|
|
this.refresh(); |
|
} |
|
|
|
|
|
|
|
|
|
refresh() { |
|
this.overlay.refresh(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
onKeyUp(event) { |
|
|
|
if (!this.isActivated || !this.options.keyboardControl) { |
|
return; |
|
} |
|
|
|
|
|
if (event.keyCode === ESC_KEY_CODE) { |
|
this.reset(); |
|
return; |
|
} |
|
|
|
|
|
|
|
const highlightedElement = this.getHighlightedElement(); |
|
if (!highlightedElement || !highlightedElement.popover) { |
|
return; |
|
} |
|
|
|
if (event.keyCode === RIGHT_KEY_CODE) { |
|
this.handleNext(); |
|
} else if (event.keyCode === LEFT_KEY_CODE) { |
|
this.handlePrevious(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
movePrevious() { |
|
const previousStep = this.steps[this.currentStep - 1]; |
|
if (!previousStep) { |
|
this.reset(); |
|
return; |
|
} |
|
|
|
this.overlay.highlight(previousStep); |
|
this.currentStep -= 1; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
preventMove() { |
|
this.currentMovePrevented = true; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
handleNext() { |
|
this.currentMovePrevented = false; |
|
|
|
|
|
const currentStep = this.steps[this.currentStep]; |
|
if (currentStep && currentStep.options && currentStep.options.onNext) { |
|
currentStep.options.onNext(this.overlay.highlightedElement); |
|
} |
|
|
|
if (this.currentMovePrevented) { |
|
return; |
|
} |
|
|
|
this.moveNext(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
handlePrevious() { |
|
this.currentMovePrevented = false; |
|
|
|
|
|
const currentStep = this.steps[this.currentStep]; |
|
if (currentStep && currentStep.options && currentStep.options.onPrevious) { |
|
currentStep.options.onPrevious(this.overlay.highlightedElement); |
|
} |
|
|
|
if (this.currentMovePrevented) { |
|
return; |
|
} |
|
|
|
this.movePrevious(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
moveNext() { |
|
const nextStep = this.steps[this.currentStep + 1]; |
|
if (!nextStep) { |
|
this.reset(); |
|
return; |
|
} |
|
|
|
this.overlay.highlight(nextStep); |
|
this.currentStep += 1; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
hasNextStep() { |
|
return !!this.steps[this.currentStep + 1]; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
hasPreviousStep() { |
|
return !!this.steps[this.currentStep - 1]; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
reset(immediate = false) { |
|
this.currentStep = 0; |
|
this.isActivated = false; |
|
this.overlay.clear(immediate); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
hasHighlightedElement() { |
|
const highlightedElement = this.overlay.getHighlightedElement(); |
|
return highlightedElement && highlightedElement.node; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
getHighlightedElement() { |
|
return this.overlay.getHighlightedElement(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
getLastHighlightedElement() { |
|
return this.overlay.getLastHighlightedElement(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
defineSteps(steps) { |
|
this.steps = []; |
|
|
|
for (let counter = 0; counter < steps.length; counter++) { |
|
const element = this.prepareElementFromStep(steps[counter], steps, counter); |
|
if (!element) { |
|
continue; |
|
} |
|
|
|
this.steps.push(element); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prepareElementFromStep(currentStep, allSteps = [], index = 0) { |
|
let elementOptions = { ...this.options }; |
|
let querySelector = currentStep; |
|
|
|
|
|
|
|
const isStepDefinition = typeof currentStep !== 'string' && !isDomElement(currentStep); |
|
|
|
if (!currentStep || (isStepDefinition && !currentStep.element)) { |
|
throw new Error(`Element is required in step ${index}`); |
|
} |
|
|
|
if (isStepDefinition) { |
|
querySelector = currentStep.element; |
|
elementOptions = { ...this.options, ...currentStep }; |
|
} |
|
|
|
|
|
const domElement = isDomElement(querySelector) ? querySelector : this.document.querySelector(querySelector); |
|
if (!domElement) { |
|
console.warn(`Element to highlight ${querySelector} not found`); |
|
return null; |
|
} |
|
|
|
let popover = null; |
|
if (elementOptions.popover && elementOptions.popover.title) { |
|
const mergedClassNames = [ |
|
this.options.className, |
|
elementOptions.popover.className, |
|
].filter(c => c).join(' '); |
|
|
|
const popoverOptions = { |
|
...elementOptions, |
|
...elementOptions.popover, |
|
className: mergedClassNames, |
|
totalCount: allSteps.length, |
|
currentIndex: index, |
|
isFirst: index === 0, |
|
isLast: allSteps.length === 0 || index === allSteps.length - 1, |
|
}; |
|
|
|
popover = new Popover(popoverOptions, this.window, this.document); |
|
} |
|
|
|
const stageOptions = { ...elementOptions }; |
|
const stage = new Stage(stageOptions, this.window, this.document); |
|
|
|
return new Element({ |
|
node: domElement, |
|
options: elementOptions, |
|
popover, |
|
stage, |
|
overlay: this.overlay, |
|
window: this.window, |
|
document: this.document, |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
start(index = 0) { |
|
if (!this.steps || this.steps.length === 0) { |
|
throw new Error('There are no steps defined to iterate'); |
|
} |
|
|
|
this.isActivated = true; |
|
this.currentStep = index; |
|
this.overlay.highlight(this.steps[index]); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
highlight(selector) { |
|
this.isActivated = true; |
|
|
|
const element = this.prepareElementFromStep(selector); |
|
if (!element) { |
|
return; |
|
} |
|
|
|
this.overlay.highlight(element); |
|
} |
|
} |
|
|