Implement the modal style popover
Browse files- index.html +1 -1
- src/config.ts +2 -0
- src/driver.ts +7 -14
- src/events.ts +4 -7
- src/highlight.ts +21 -15
- src/popover.ts +26 -11
- src/stage.ts +29 -19
- src/state.ts +28 -0
- 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({
|
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
|
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
|
48 |
-
|
49 |
-
|
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
|
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 |
-
|
|
|
|
|
|
|
|
|
43 |
|
44 |
-
|
|
|
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 (
|
73 |
return;
|
74 |
}
|
75 |
|
@@ -84,13 +89,13 @@ function transferHighlight(from: Element, to: Element) {
|
|
84 |
renderPopover(to);
|
85 |
}
|
86 |
|
87 |
-
|
88 |
}
|
89 |
|
90 |
window.requestAnimationFrame(animate);
|
91 |
};
|
92 |
|
93 |
-
|
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
|
107 |
-
|
108 |
-
|
109 |
-
|
|
|
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 +
|
77 |
-
height: boundingClientRect.height + stagePadding +
|
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 (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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 |
-
|
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",
|
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
|
126 |
|
127 |
-
|
128 |
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
|
134 |
-
svg.appendChild(
|
135 |
|
136 |
return svg;
|
137 |
}
|
138 |
|
139 |
-
function
|
140 |
const windowX = window.innerWidth;
|
141 |
const windowY = window.innerHeight;
|
142 |
|
143 |
-
const stagePadding = getConfig(
|
144 |
-
const stageRadius = getConfig(
|
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
|
168 |
}
|
169 |
|
170 |
-
activeStagePosition
|
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,
|
|
|
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%;
|