kamrify commited on
Commit
9bde4cc
·
1 Parent(s): edd7dca

Implement the modal style popover

Browse files
Files changed (9) hide show
  1. index.html +1 -1
  2. src/config.ts +2 -0
  3. src/driver.ts +7 -14
  4. src/events.ts +4 -7
  5. src/highlight.ts +21 -15
  6. src/popover.ts +26 -11
  7. src/stage.ts +29 -19
  8. src/state.ts +28 -0
  9. src/style.css +6 -1
index.html CHANGED
@@ -292,7 +292,7 @@ npm install driver.js</pre
292
  }, 2000);
293
 
294
  window.setTimeout(() => {
295
- driverObj.highlight({ element: ".buttons button:nth-child(5)" });
296
  }, 4000);
297
 
298
  window.setTimeout(() => {
 
292
  }, 2000);
293
 
294
  window.setTimeout(() => {
295
+ driverObj.highlight({});
296
  }, 4000);
297
 
298
  window.setTimeout(() => {
src/config.ts CHANGED
@@ -5,6 +5,7 @@ export type Config = {
5
  opacity?: number;
6
  stagePadding?: number;
7
  stageRadius?: number;
 
8
  };
9
 
10
  let currentConfig: Config = {};
@@ -17,6 +18,7 @@ export function configure(config: Config = {}) {
17
  smoothScroll: false,
18
  stagePadding: 10,
19
  stageRadius: 5,
 
20
  ...config,
21
  };
22
  }
 
5
  opacity?: number;
6
  stagePadding?: number;
7
  stageRadius?: number;
8
+ popoverOffset?: number;
9
  };
10
 
11
  let currentConfig: Config = {};
 
18
  smoothScroll: false,
19
  stagePadding: 10,
20
  stageRadius: 5,
21
+ popoverOffset: 10,
22
  ...config,
23
  };
24
  }
src/driver.ts CHANGED
@@ -6,14 +6,13 @@ import { destroyHighlight, highlight } from "./highlight";
6
  import { destroyEmitter, listen } from "./emitter";
7
 
8
  import "./style.css";
 
9
 
10
  export type DriveStep = {
11
  element?: string | Element;
12
  popover?: Popover;
13
  };
14
 
15
- let isInitialized = false;
16
-
17
  export function driver(options: Config = {}) {
18
  configure(options);
19
 
@@ -26,15 +25,12 @@ export function driver(options: Config = {}) {
26
  }
27
 
28
  function init() {
29
- if (isInitialized) {
30
  return;
31
  }
32
 
33
- isInitialized = true;
34
- document.body.classList.add(
35
- "driver-active",
36
- getConfig("animate") ? "driver-fade" : "driver-simple"
37
- );
38
 
39
  initEvents();
40
 
@@ -44,11 +40,9 @@ export function driver(options: Config = {}) {
44
  }
45
 
46
  function destroy() {
47
- isInitialized = false;
48
- document.body.classList.remove(
49
- "driver-active",
50
- getConfig("animate") ? "driver-fade" : "driver-simple"
51
- );
52
 
53
  destroyEvents();
54
  destroyPopover();
@@ -57,7 +51,6 @@ export function driver(options: Config = {}) {
57
  destroyEmitter();
58
  }
59
 
60
- // @todo make popover selectable
61
  return {
62
  drive: (steps: DriveStep[]) => console.log(steps),
63
  highlight: (step: DriveStep) => {
 
6
  import { destroyEmitter, listen } from "./emitter";
7
 
8
  import "./style.css";
9
+ import { getState, setState } from "./state";
10
 
11
  export type DriveStep = {
12
  element?: string | Element;
13
  popover?: Popover;
14
  };
15
 
 
 
16
  export function driver(options: Config = {}) {
17
  configure(options);
18
 
 
25
  }
26
 
27
  function init() {
28
+ if (getState("isInitialized")) {
29
  return;
30
  }
31
 
32
+ setState("isInitialized", true);
33
+ document.body.classList.add("driver-active", getConfig("animate") ? "driver-fade" : "driver-simple");
 
 
 
34
 
35
  initEvents();
36
 
 
40
  }
41
 
42
  function destroy() {
43
+ setState("isInitialized", false);
44
+
45
+ document.body.classList.remove("driver-active", "driver-fade", "driver-simple");
 
 
46
 
47
  destroyEvents();
48
  destroyPopover();
 
51
  destroyEmitter();
52
  }
53
 
 
54
  return {
55
  drive: (steps: DriveStep[]) => console.log(steps),
56
  highlight: (step: DriveStep) => {
src/events.ts CHANGED
@@ -1,14 +1,14 @@
1
  import { refreshActiveHighlight } from "./highlight";
2
  import { emit } from "./emitter";
3
-
4
- let resizeTimeout: number;
5
 
6
  function requireRefresh() {
 
7
  if (resizeTimeout) {
8
  window.cancelAnimationFrame(resizeTimeout);
9
  }
10
 
11
- resizeTimeout = window.requestAnimationFrame(refreshActiveHighlight);
12
  }
13
 
14
  function onKeyup(e: KeyboardEvent) {
@@ -32,10 +32,7 @@ export function onDriverClick(
32
  listener: (pointer: MouseEvent | PointerEvent) => void,
33
  shouldPreventDefault?: (target: HTMLElement) => boolean
34
  ) {
35
- const listenerWrapper = (
36
- e: MouseEvent | PointerEvent,
37
- listener?: (pointer: MouseEvent | PointerEvent) => void
38
- ) => {
39
  const target = e.target as HTMLElement;
40
  if (!element.contains(target)) {
41
  return;
 
1
  import { refreshActiveHighlight } from "./highlight";
2
  import { emit } from "./emitter";
3
+ import { getState, setState } from "./state";
 
4
 
5
  function requireRefresh() {
6
+ const resizeTimeout = getState("resizeTimeout");
7
  if (resizeTimeout) {
8
  window.cancelAnimationFrame(resizeTimeout);
9
  }
10
 
11
+ setState("resizeTimeout", window.requestAnimationFrame(refreshActiveHighlight));
12
  }
13
 
14
  function onKeyup(e: KeyboardEvent) {
 
32
  listener: (pointer: MouseEvent | PointerEvent) => void,
33
  shouldPreventDefault?: (target: HTMLElement) => boolean
34
  ) {
35
+ const listenerWrapper = (e: MouseEvent | PointerEvent, listener?: (pointer: MouseEvent | PointerEvent) => void) => {
 
 
 
36
  const target = e.target as HTMLElement;
37
  if (!element.contains(target)) {
38
  return;
src/highlight.ts CHANGED
@@ -3,10 +3,7 @@ import { refreshStage, trackActiveElement, transitionStage } from "./stage";
3
  import { getConfig } from "./config";
4
  import { repositionPopover, renderPopover, hidePopover } from "./popover";
5
  import { bringInView } from "./utils";
6
-
7
- let previousHighlight: Element | undefined;
8
- let activeHighlight: Element | undefined;
9
- let currentTransitionCallback: undefined | (() => void);
10
 
11
  function mountDummyElement(): Element {
12
  const existingDummy = document.getElementById("driver-dummy-element");
@@ -38,13 +35,19 @@ export function highlight(step: DriveStep) {
38
  elemObj = mountDummyElement();
39
  }
40
 
41
- previousHighlight = activeHighlight;
42
- activeHighlight = elemObj;
 
 
 
 
43
 
44
- transferHighlight(previousHighlight || elemObj, elemObj);
 
45
  }
46
 
47
  export function refreshActiveHighlight() {
 
48
  if (!activeHighlight) {
49
  return;
50
  }
@@ -61,15 +64,17 @@ function transferHighlight(from: Element, to: Element) {
61
  // If it's the first time we're highlighting an element, we show
62
  // the popover immediately. Otherwise, we wait for the animation
63
  // to finish before showing the popover.
64
- const hasDelayedPopover = !from || from !== to;
65
 
66
  hidePopover();
67
 
68
  const animate = () => {
 
 
69
  // This makes sure that the repeated calls to transferHighlight
70
  // don't interfere with each other. Only the last call will be
71
  // executed.
72
- if (currentTransitionCallback !== animate) {
73
  return;
74
  }
75
 
@@ -84,13 +89,13 @@ function transferHighlight(from: Element, to: Element) {
84
  renderPopover(to);
85
  }
86
 
87
- currentTransitionCallback = undefined;
88
  }
89
 
90
  window.requestAnimationFrame(animate);
91
  };
92
 
93
- currentTransitionCallback = animate;
94
  window.requestAnimationFrame(animate);
95
 
96
  bringInView(to);
@@ -103,10 +108,11 @@ function transferHighlight(from: Element, to: Element) {
103
  }
104
 
105
  export function destroyHighlight() {
106
- activeHighlight = undefined;
107
- currentTransitionCallback = undefined;
108
- previousHighlight = undefined;
109
- activeHighlight = undefined;
 
110
  document.getElementById("driver-dummy-element")?.remove();
111
 
112
  document.querySelectorAll(".driver-active-element").forEach(element => {
 
3
  import { getConfig } from "./config";
4
  import { repositionPopover, renderPopover, hidePopover } from "./popover";
5
  import { bringInView } from "./utils";
6
+ import { getState, setState } from "./state";
 
 
 
7
 
8
  function mountDummyElement(): Element {
9
  const existingDummy = document.getElementById("driver-dummy-element");
 
35
  elemObj = mountDummyElement();
36
  }
37
 
38
+ const previousHighlight = getState("activeHighlight");
39
+
40
+ const transferHighlightFrom = previousHighlight || elemObj;
41
+ const transferHighlightTo = elemObj;
42
+
43
+ transferHighlight(transferHighlightFrom, transferHighlightTo);
44
 
45
+ setState("previousHighlight", transferHighlightFrom);
46
+ setState("activeHighlight", transferHighlightTo);
47
  }
48
 
49
  export function refreshActiveHighlight() {
50
+ const activeHighlight = getState("activeHighlight");
51
  if (!activeHighlight) {
52
  return;
53
  }
 
64
  // If it's the first time we're highlighting an element, we show
65
  // the popover immediately. Otherwise, we wait for the animation
66
  // to finish before showing the popover.
67
+ const hasDelayedPopover = to && (!from || from !== to);
68
 
69
  hidePopover();
70
 
71
  const animate = () => {
72
+ const transitionCallback = getState("transitionCallback");
73
+
74
  // This makes sure that the repeated calls to transferHighlight
75
  // don't interfere with each other. Only the last call will be
76
  // executed.
77
+ if (transitionCallback !== animate) {
78
  return;
79
  }
80
 
 
89
  renderPopover(to);
90
  }
91
 
92
+ setState("transitionCallback", undefined);
93
  }
94
 
95
  window.requestAnimationFrame(animate);
96
  };
97
 
98
+ setState("transitionCallback", animate);
99
  window.requestAnimationFrame(animate);
100
 
101
  bringInView(to);
 
108
  }
109
 
110
  export function destroyHighlight() {
111
+ setState("activeHighlight", undefined);
112
+ setState("previousHighlight", undefined);
113
+
114
+ setState("transitionCallback", undefined);
115
+
116
  document.getElementById("driver-dummy-element")?.remove();
117
 
118
  document.querySelectorAll(".driver-active-element").forEach(element => {
src/popover.ts CHANGED
@@ -1,11 +1,10 @@
1
  import { bringInView } from "./utils";
2
  import { getConfig } from "./config";
 
3
 
4
- export type Side = "top" | "right" | "bottom" | "left";
5
  export type Alignment = "start" | "center" | "end";
6
 
7
- const POPOVER_OFFSET = 10;
8
-
9
  export type Popover = {
10
  title?: string;
11
  description: string;
@@ -13,7 +12,7 @@ export type Popover = {
13
  align?: Alignment;
14
  };
15
 
16
- type PopoverDOM = {
17
  wrapper: HTMLElement;
18
  arrow: HTMLElement;
19
  title: HTMLElement;
@@ -25,9 +24,8 @@ type PopoverDOM = {
25
  footerButtons: HTMLElement;
26
  };
27
 
28
- let popover: PopoverDOM | undefined;
29
-
30
  export function hidePopover() {
 
31
  if (!popover) {
32
  return;
33
  }
@@ -36,6 +34,7 @@ export function hidePopover() {
36
  }
37
 
38
  export function renderPopover(element: Element) {
 
39
  if (!popover) {
40
  popover = createPopover();
41
  document.body.appendChild(popover.wrapper);
@@ -53,6 +52,8 @@ export function renderPopover(element: Element) {
53
  const popoverArrow = popover.arrow;
54
  popoverArrow.className = "driver-popover-arrow";
55
 
 
 
56
  repositionPopover(element);
57
  bringInView(popoverWrapper);
58
  }
@@ -65,16 +66,19 @@ type PopoverDimensions = {
65
  };
66
 
67
  function getPopoverDimensions(): PopoverDimensions | undefined {
 
68
  if (!popover?.wrapper) {
69
  return;
70
  }
71
 
72
  const boundingClientRect = popover.wrapper.getBoundingClientRect();
 
73
  const stagePadding = getConfig("stagePadding") || 0;
 
74
 
75
  return {
76
- width: boundingClientRect.width + stagePadding + POPOVER_OFFSET,
77
- height: boundingClientRect.height + stagePadding + POPOVER_OFFSET,
78
 
79
  realWidth: boundingClientRect.width,
80
  realHeight: boundingClientRect.height,
@@ -171,6 +175,7 @@ function calculateLeftForTopBottom(
171
  }
172
 
173
  export function repositionPopover(element: Element) {
 
174
  if (!popover) {
175
  return;
176
  }
@@ -178,7 +183,7 @@ export function repositionPopover(element: Element) {
178
  // @TODO These values will come from the config
179
  // Configure the popover positioning
180
  const requiredAlignment: Alignment = "start";
181
- const requiredSide: Side = "left" as Side;
182
  const popoverPadding = getConfig('stagePadding') || 0;
183
 
184
  const popoverDimensions = getPopoverDimensions()!;
@@ -210,7 +215,15 @@ export function repositionPopover(element: Element) {
210
  isLeftOptimal = isTopOptimal = isBottomOptimal = false;
211
  }
212
 
213
- if (noneOptimal) {
 
 
 
 
 
 
 
 
214
  const leftValue = window.innerWidth / 2 - popoverDimensions?.realWidth! / 2;
215
  const bottomValue = 10;
216
 
@@ -303,6 +316,7 @@ export function repositionPopover(element: Element) {
303
  }
304
 
305
  function renderPopoverArrow(alignment: Alignment, side: Side, element: Element) {
 
306
  if (!popover) {
307
  return;
308
  }
@@ -458,10 +472,11 @@ function createPopover(): PopoverDOM {
458
  }
459
 
460
  export function destroyPopover() {
 
461
  if (!popover) {
462
  return;
463
  }
464
 
465
  popover.wrapper.parentElement?.removeChild(popover.wrapper);
466
- popover = undefined;
467
  }
 
1
  import { bringInView } from "./utils";
2
  import { getConfig } from "./config";
3
+ import { getState, setState } from "./state";
4
 
5
+ export type Side = "top" | "right" | "bottom" | "left" | "over";
6
  export type Alignment = "start" | "center" | "end";
7
 
 
 
8
  export type Popover = {
9
  title?: string;
10
  description: string;
 
12
  align?: Alignment;
13
  };
14
 
15
+ export type PopoverDOM = {
16
  wrapper: HTMLElement;
17
  arrow: HTMLElement;
18
  title: HTMLElement;
 
24
  footerButtons: HTMLElement;
25
  };
26
 
 
 
27
  export function hidePopover() {
28
+ const popover = getState("popover");
29
  if (!popover) {
30
  return;
31
  }
 
34
  }
35
 
36
  export function renderPopover(element: Element) {
37
+ let popover = getState("popover");
38
  if (!popover) {
39
  popover = createPopover();
40
  document.body.appendChild(popover.wrapper);
 
52
  const popoverArrow = popover.arrow;
53
  popoverArrow.className = "driver-popover-arrow";
54
 
55
+ setState("popover", popover);
56
+
57
  repositionPopover(element);
58
  bringInView(popoverWrapper);
59
  }
 
66
  };
67
 
68
  function getPopoverDimensions(): PopoverDimensions | undefined {
69
+ const popover = getState("popover");
70
  if (!popover?.wrapper) {
71
  return;
72
  }
73
 
74
  const boundingClientRect = popover.wrapper.getBoundingClientRect();
75
+
76
  const stagePadding = getConfig("stagePadding") || 0;
77
+ const popoverOffset = getConfig("popoverOffset") || 0;
78
 
79
  return {
80
+ width: boundingClientRect.width + stagePadding + popoverOffset,
81
+ height: boundingClientRect.height + stagePadding + popoverOffset,
82
 
83
  realWidth: boundingClientRect.width,
84
  realHeight: boundingClientRect.height,
 
175
  }
176
 
177
  export function repositionPopover(element: Element) {
178
+ const popover = getState("popover");
179
  if (!popover) {
180
  return;
181
  }
 
183
  // @TODO These values will come from the config
184
  // Configure the popover positioning
185
  const requiredAlignment: Alignment = "start";
186
+ const requiredSide: Side = element.id === "driver-dummy-element" ? "over" : "left" as Side;
187
  const popoverPadding = getConfig('stagePadding') || 0;
188
 
189
  const popoverDimensions = getPopoverDimensions()!;
 
215
  isLeftOptimal = isTopOptimal = isBottomOptimal = false;
216
  }
217
 
218
+ if (requiredSide === "over") {
219
+ const leftToSet = window.innerWidth / 2 - popoverDimensions!.realWidth / 2;
220
+ const topToSet = window.innerHeight / 2 - popoverDimensions!.realHeight / 2;
221
+
222
+ popover.wrapper.style.left = `${leftToSet}px`;
223
+ popover.wrapper.style.right = `auto`;
224
+ popover.wrapper.style.top = `${topToSet}px`;
225
+ popover.wrapper.style.bottom = `auto`;
226
+ } else if (noneOptimal) {
227
  const leftValue = window.innerWidth / 2 - popoverDimensions?.realWidth! / 2;
228
  const bottomValue = 10;
229
 
 
316
  }
317
 
318
  function renderPopoverArrow(alignment: Alignment, side: Side, element: Element) {
319
+ const popover = getState("popover");
320
  if (!popover) {
321
  return;
322
  }
 
472
  }
473
 
474
  export function destroyPopover() {
475
+ const popover = getState("popover");
476
  if (!popover) {
477
  return;
478
  }
479
 
480
  popover.wrapper.parentElement?.removeChild(popover.wrapper);
481
+ setState("popover", undefined);
482
  }
src/stage.ts CHANGED
@@ -2,6 +2,7 @@ import { easeInOutQuad } from "./utils";
2
  import { onDriverClick } from "./events";
3
  import { emit } from "./emitter";
4
  import { getConfig } from "./config";
 
5
 
6
  export type StageDefinition = {
7
  x: number;
@@ -10,14 +11,12 @@ export type StageDefinition = {
10
  height: number;
11
  };
12
 
13
- let activeStagePosition: StageDefinition | undefined;
14
- let stageSvg: SVGSVGElement | undefined;
15
-
16
  // This method calculates the animated new position of the
17
  // stage (called for each frame by requestAnimationFrame)
18
  export function transitionStage(elapsed: number, duration: number, from: Element, to: Element) {
19
- const fromDefinition = activeStagePosition ? activeStagePosition : from.getBoundingClientRect();
20
 
 
21
  const toDefinition = to.getBoundingClientRect();
22
 
23
  const x = easeInOutQuad(elapsed, fromDefinition.x, toDefinition.x - fromDefinition.x, duration);
@@ -33,6 +32,7 @@ export function transitionStage(elapsed: number, duration: number, from: Element
33
  };
34
 
35
  renderStage(activeStagePosition);
 
36
  }
37
 
38
  export function trackActiveElement(element: Element) {
@@ -42,17 +42,22 @@ export function trackActiveElement(element: Element) {
42
 
43
  const definition = element.getBoundingClientRect();
44
 
45
- activeStagePosition = {
46
  x: definition.x,
47
  y: definition.y,
48
  width: definition.width,
49
  height: definition.height,
50
  };
51
 
 
 
52
  renderStage(activeStagePosition);
53
  }
54
 
55
  export function refreshStage() {
 
 
 
56
  if (!activeStagePosition) {
57
  return;
58
  }
@@ -69,7 +74,7 @@ export function refreshStage() {
69
  }
70
 
71
  function mountStage(stagePosition: StageDefinition) {
72
- stageSvg = createStageSvg(stagePosition);
73
  document.body.appendChild(stageSvg);
74
 
75
  onDriverClick(stageSvg, e => {
@@ -80,9 +85,13 @@ function mountStage(stagePosition: StageDefinition) {
80
 
81
  emit("overlayClick");
82
  });
 
 
83
  }
84
 
85
  function renderStage(stagePosition: StageDefinition) {
 
 
86
  // TODO: cancel rendering if element is not visible
87
  if (!stageSvg) {
88
  mountStage(stagePosition);
@@ -95,7 +104,7 @@ function renderStage(stagePosition: StageDefinition) {
95
  throw new Error("no path element found in stage svg");
96
  }
97
 
98
- pathElement.setAttribute("d", generateSvgCutoutPathString(stagePosition));
99
  }
100
 
101
  function createStageSvg(stage: StageDefinition): SVGSVGElement {
@@ -122,26 +131,26 @@ function createStageSvg(stage: StageDefinition): SVGSVGElement {
122
  svg.style.width = "100%";
123
  svg.style.height = "100%";
124
 
125
- const cutoutPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
126
 
127
- cutoutPath.setAttribute("d", generateSvgCutoutPathString(stage));
128
 
129
- cutoutPath.style.fill = "rgb(0,0,0)";
130
- cutoutPath.style.opacity = `${getConfig("opacity")}`;
131
- cutoutPath.style.pointerEvents = "auto";
132
- cutoutPath.style.cursor = "auto";
133
 
134
- svg.appendChild(cutoutPath);
135
 
136
  return svg;
137
  }
138
 
139
- function generateSvgCutoutPathString(stage: StageDefinition) {
140
  const windowX = window.innerWidth;
141
  const windowY = window.innerHeight;
142
 
143
- const stagePadding = getConfig('stagePadding') || 0;
144
- const stageRadius = getConfig('stageRadius') || 0;
145
 
146
  const stageWidth = stage.width + stagePadding * 2;
147
  const stageHeight = stage.height + stagePadding * 2;
@@ -162,10 +171,11 @@ function generateSvgCutoutPathString(stage: StageDefinition) {
162
  }
163
 
164
  export function destroyStage() {
 
165
  if (stageSvg) {
166
  stageSvg.remove();
167
- stageSvg = undefined;
168
  }
169
 
170
- activeStagePosition = undefined;
171
  }
 
2
  import { onDriverClick } from "./events";
3
  import { emit } from "./emitter";
4
  import { getConfig } from "./config";
5
+ import { getState, setState } from "./state";
6
 
7
  export type StageDefinition = {
8
  x: number;
 
11
  height: number;
12
  };
13
 
 
 
 
14
  // This method calculates the animated new position of the
15
  // stage (called for each frame by requestAnimationFrame)
16
  export function transitionStage(elapsed: number, duration: number, from: Element, to: Element) {
17
+ let activeStagePosition = getState("activeStagePosition");
18
 
19
+ const fromDefinition = activeStagePosition ? activeStagePosition : from.getBoundingClientRect();
20
  const toDefinition = to.getBoundingClientRect();
21
 
22
  const x = easeInOutQuad(elapsed, fromDefinition.x, toDefinition.x - fromDefinition.x, duration);
 
32
  };
33
 
34
  renderStage(activeStagePosition);
35
+ setState("activeStagePosition", activeStagePosition);
36
  }
37
 
38
  export function trackActiveElement(element: Element) {
 
42
 
43
  const definition = element.getBoundingClientRect();
44
 
45
+ const activeStagePosition: StageDefinition = {
46
  x: definition.x,
47
  y: definition.y,
48
  width: definition.width,
49
  height: definition.height,
50
  };
51
 
52
+ setState("activeStagePosition", activeStagePosition);
53
+
54
  renderStage(activeStagePosition);
55
  }
56
 
57
  export function refreshStage() {
58
+ const activeStagePosition = getState("activeStagePosition");
59
+ const stageSvg = getState("stageSvg");
60
+
61
  if (!activeStagePosition) {
62
  return;
63
  }
 
74
  }
75
 
76
  function mountStage(stagePosition: StageDefinition) {
77
+ const stageSvg = createStageSvg(stagePosition);
78
  document.body.appendChild(stageSvg);
79
 
80
  onDriverClick(stageSvg, e => {
 
85
 
86
  emit("overlayClick");
87
  });
88
+
89
+ setState("stageSvg", stageSvg);
90
  }
91
 
92
  function renderStage(stagePosition: StageDefinition) {
93
+ const stageSvg = getState("stageSvg");
94
+
95
  // TODO: cancel rendering if element is not visible
96
  if (!stageSvg) {
97
  mountStage(stagePosition);
 
104
  throw new Error("no path element found in stage svg");
105
  }
106
 
107
+ pathElement.setAttribute("d", generateStageSvgPathString(stagePosition));
108
  }
109
 
110
  function createStageSvg(stage: StageDefinition): SVGSVGElement {
 
131
  svg.style.width = "100%";
132
  svg.style.height = "100%";
133
 
134
+ const stagePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
135
 
136
+ stagePath.setAttribute("d", generateStageSvgPathString(stage));
137
 
138
+ stagePath.style.fill = "rgb(0,0,0)";
139
+ stagePath.style.opacity = `${getConfig("opacity")}`;
140
+ stagePath.style.pointerEvents = "auto";
141
+ stagePath.style.cursor = "auto";
142
 
143
+ svg.appendChild(stagePath);
144
 
145
  return svg;
146
  }
147
 
148
+ function generateStageSvgPathString(stage: StageDefinition) {
149
  const windowX = window.innerWidth;
150
  const windowY = window.innerHeight;
151
 
152
+ const stagePadding = getConfig("stagePadding") || 0;
153
+ const stageRadius = getConfig("stageRadius") || 0;
154
 
155
  const stageWidth = stage.width + stagePadding * 2;
156
  const stageHeight = stage.height + stagePadding * 2;
 
171
  }
172
 
173
  export function destroyStage() {
174
+ const stageSvg = getState("stageSvg");
175
  if (stageSvg) {
176
  stageSvg.remove();
177
+ setState("stageSvg", undefined);
178
  }
179
 
180
+ setState("activeStagePosition", undefined);
181
  }
src/state.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StageDefinition } from "./stage";
2
+ import { PopoverDOM } from "./popover";
3
+
4
+ export type State = {
5
+ isInitialized?: boolean;
6
+ resizeTimeout?: number;
7
+
8
+ previousHighlight?: Element;
9
+ activeHighlight?: Element;
10
+ transitionCallback?: () => void;
11
+
12
+ activeStagePosition?: StageDefinition;
13
+ stageSvg?: SVGSVGElement;
14
+
15
+ popover?: PopoverDOM;
16
+ };
17
+
18
+ let currentState: State = {};
19
+
20
+ export function setState<K extends keyof State>(key: K, value: State[K]) {
21
+ currentState[key] = value;
22
+ }
23
+
24
+ export function getState(): State;
25
+ export function getState<K extends keyof State>(key: K): State[K];
26
+ export function getState<K extends keyof State>(key?: K) {
27
+ return key ? currentState[key] : currentState;
28
+ }
src/style.css CHANGED
@@ -8,7 +8,8 @@
8
 
9
  .driver-active .driver-active-element,
10
  .driver-active .driver-active-element *,
11
- .driver-popover, .driver-popover *{
 
12
  pointer-events: auto;
13
  }
14
 
@@ -51,6 +52,10 @@
51
  border: 5px solid #fff;
52
  }
53
 
 
 
 
 
54
  /** Popover Arrow Sides **/
55
  .driver-popover-arrow-side-left {
56
  left: 100%;
 
8
 
9
  .driver-active .driver-active-element,
10
  .driver-active .driver-active-element *,
11
+ .driver-popover,
12
+ .driver-popover * {
13
  pointer-events: auto;
14
  }
15
 
 
52
  border: 5px solid #fff;
53
  }
54
 
55
+ .driver-popover-arrow-side-over {
56
+ display: none;
57
+ }
58
+
59
  /** Popover Arrow Sides **/
60
  .driver-popover-arrow-side-left {
61
  left: 100%;