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%;
|