File size: 4,513 Bytes
aa3b624
 
66a740c
9db3a38
82a88c5
9bde4cc
aa3b624
edd7dca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aa3b624
 
edd7dca
aa3b624
836d49f
 
 
 
aa3b624
edd7dca
aa3b624
 
70d8f15
aa3b624
 
 
836d49f
9a5b24f
 
aa3b624
 
 
 
 
 
9a5b24f
aa3b624
 
70d8f15
aa3b624
 
66a740c
70d8f15
 
836d49f
9db3a38
 
 
70d8f15
ef09b80
 
836d49f
34b7e3b
ef09b80
 
d656141
 
eb375ea
 
d656141
ef09b80
eb375ea
 
ef09b80
 
34b7e3b
3b225cb
9db3a38
 
 
aa3b624
9bde4cc
 
4c98241
 
 
9bde4cc
aa3b624
 
 
 
3b225cb
 
 
 
70d8f15
3b225cb
 
aa3b624
66a740c
70d8f15
aa3b624
70d8f15
8480986
eb375ea
 
ef09b80
 
9bde4cc
ef09b80
 
 
 
aa3b624
 
 
 
 
9bde4cc
aa3b624
 
70d8f15
836d49f
70d8f15
9db3a38
82a88c5
70d8f15
 
aa3b624
 
 
edd7dca
aa3b624
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import { DriveStep } from "./driver";
import { refreshStage, trackActiveElement, transitionStage } from "./stage";
import { getConfig } from "./config";
import { repositionPopover, renderPopover, hidePopover } 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 = typeof element === "string" ? document.querySelector(element) : 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);
  refreshStage();
  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 = getConfig("onHighlightStarted");
  const highlightedHook = getConfig("onHighlighted");
  const deselectedHook = getConfig("onDeselected");

  if (!isFirstHighlight && deselectedHook) {
    deselectedHook(isFromDummyElement ? undefined : fromElement, fromStep!);
  }

  if (highlightStartedHook) {
    highlightStartedHook(isToDummyElement ? undefined : toElement, toStep);
  }

  const hasDelayedPopover = !isFirstHighlight && isAnimatedTour;
  let isPopoverRendered = false;

  hidePopover();

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

      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");
  toElement.classList.add("driver-active-element");
}

export function destroyHighlight() {
  document.getElementById("driver-dummy-element")?.remove();
  document.querySelectorAll(".driver-active-element").forEach(element => {
    element.classList.remove("driver-active-element");
  });
}