Repositioning of popover
Browse files- src/highlight.ts +1 -1
- src/popover.ts +60 -37
- src/stage.ts +18 -48
- src/style.css +5 -0
src/highlight.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
import { DriveStep } from "./driver";
|
2 |
import { refreshStage, trackActiveElement, transitionStage } from "./stage";
|
3 |
import { getConfig } from "./config";
|
4 |
-
import {
|
5 |
import { bringInView } from "./utils";
|
6 |
|
7 |
let previousHighlight: Element | undefined;
|
|
|
1 |
import { DriveStep } from "./driver";
|
2 |
import { refreshStage, trackActiveElement, transitionStage } from "./stage";
|
3 |
import { getConfig } from "./config";
|
4 |
+
import { repositionPopover, renderPopover } from "./popover";
|
5 |
import { bringInView } from "./utils";
|
6 |
|
7 |
let previousHighlight: Element | undefined;
|
src/popover.ts
CHANGED
@@ -1,8 +1,11 @@
|
|
1 |
import { bringInView } from "./utils";
|
|
|
2 |
|
3 |
export type Side = "top" | "right" | "bottom" | "left";
|
4 |
export type Alignment = "start" | "center" | "end";
|
5 |
|
|
|
|
|
6 |
export type Popover = {
|
7 |
title?: string;
|
8 |
description: string;
|
@@ -30,80 +33,100 @@ export function renderPopover(element: Element) {
|
|
30 |
document.body.appendChild(popover.wrapper);
|
31 |
}
|
32 |
|
|
|
33 |
const popoverWrapper = popover.wrapper;
|
34 |
-
|
35 |
popoverWrapper.style.display = "block";
|
36 |
-
popoverWrapper.style.left = "
|
37 |
-
popoverWrapper.style.top = "
|
38 |
popoverWrapper.style.bottom = "";
|
39 |
popoverWrapper.style.right = "";
|
40 |
|
41 |
-
|
|
|
|
|
|
|
|
|
42 |
bringInView(popoverWrapper);
|
43 |
}
|
44 |
|
45 |
-
|
46 |
-
if (!popover) {
|
47 |
return;
|
48 |
}
|
49 |
|
50 |
-
const
|
|
|
|
|
|
|
|
|
51 |
|
52 |
-
|
53 |
-
|
|
|
54 |
}
|
55 |
|
56 |
-
function
|
57 |
if (!popover) {
|
58 |
return;
|
59 |
}
|
60 |
|
61 |
-
const
|
|
|
62 |
|
63 |
-
const popoverDimensions =
|
64 |
const popoverArrowDimensions = popover.arrow.getBoundingClientRect();
|
65 |
const elementDimensions = element.getBoundingClientRect();
|
66 |
|
67 |
-
const
|
68 |
-
const popoverPaddedHeight = popoverDimensions.height + popoverPadding;
|
69 |
-
|
70 |
-
const topValue = elementDimensions.top - popoverPaddedHeight;
|
71 |
const isTopOptimal = topValue >= 0;
|
72 |
|
73 |
-
const bottomValue = window.innerHeight - (elementDimensions.bottom +
|
74 |
const isBottomOptimal = bottomValue >= 0;
|
75 |
|
76 |
-
const leftValue = elementDimensions.left -
|
77 |
const isLeftOptimal = leftValue >= 0;
|
78 |
|
79 |
-
const rightValue = window.innerWidth - (elementDimensions.right +
|
80 |
const isRightOptimal = rightValue >= 0;
|
81 |
|
82 |
const noneOptimal = !isTopOptimal && !isBottomOptimal && !isLeftOptimal && !isRightOptimal;
|
|
|
83 |
if (noneOptimal) {
|
84 |
-
|
85 |
-
|
86 |
-
bottom: 10,
|
87 |
-
};
|
88 |
-
}
|
89 |
|
90 |
-
|
91 |
-
|
|
|
|
|
|
|
|
|
92 |
|
93 |
-
function getLeftValueAfterAlignment(element: Element) {
|
94 |
-
if (!popover) {
|
95 |
return;
|
96 |
}
|
97 |
|
98 |
-
|
99 |
-
|
100 |
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
}
|
108 |
|
109 |
function createPopover(): PopoverDOM {
|
@@ -119,7 +142,7 @@ function createPopover(): PopoverDOM {
|
|
119 |
|
120 |
const description = document.createElement("div");
|
121 |
description.classList.add("driver-popover-description");
|
122 |
-
description.innerText = "Popover
|
123 |
|
124 |
const footer = document.createElement("div");
|
125 |
footer.classList.add("driver-popover-footer");
|
|
|
1 |
import { bringInView } from "./utils";
|
2 |
+
import { STAGE_PADDING } from "./stage";
|
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;
|
|
|
33 |
document.body.appendChild(popover.wrapper);
|
34 |
}
|
35 |
|
36 |
+
// Reset the popover position
|
37 |
const popoverWrapper = popover.wrapper;
|
|
|
38 |
popoverWrapper.style.display = "block";
|
39 |
+
popoverWrapper.style.left = "";
|
40 |
+
popoverWrapper.style.top = "";
|
41 |
popoverWrapper.style.bottom = "";
|
42 |
popoverWrapper.style.right = "";
|
43 |
|
44 |
+
// Reset the classes responsible for the arrow position
|
45 |
+
const popoverArrow = popover.arrow;
|
46 |
+
popoverArrow.className = "driver-popover-arrow";
|
47 |
+
|
48 |
+
repositionPopover(element);
|
49 |
bringInView(popoverWrapper);
|
50 |
}
|
51 |
|
52 |
+
function getPopoverDimensions() {
|
53 |
+
if (!popover?.wrapper) {
|
54 |
return;
|
55 |
}
|
56 |
|
57 |
+
const boundingClientRect = popover.wrapper.getBoundingClientRect();
|
58 |
+
|
59 |
+
return {
|
60 |
+
width: boundingClientRect.width + STAGE_PADDING + POPOVER_OFFSET,
|
61 |
+
height: boundingClientRect.height + STAGE_PADDING + POPOVER_OFFSET,
|
62 |
|
63 |
+
realWidth: boundingClientRect.width,
|
64 |
+
realHeight: boundingClientRect.height,
|
65 |
+
};
|
66 |
}
|
67 |
|
68 |
+
export function repositionPopover(element: Element) {
|
69 |
if (!popover) {
|
70 |
return;
|
71 |
}
|
72 |
|
73 |
+
const requiredAlignment: Alignment = "start";
|
74 |
+
const popoverPadding = STAGE_PADDING;
|
75 |
|
76 |
+
const popoverDimensions = getPopoverDimensions();
|
77 |
const popoverArrowDimensions = popover.arrow.getBoundingClientRect();
|
78 |
const elementDimensions = element.getBoundingClientRect();
|
79 |
|
80 |
+
const topValue = elementDimensions.top - popoverDimensions!.height;
|
|
|
|
|
|
|
81 |
const isTopOptimal = topValue >= 0;
|
82 |
|
83 |
+
const bottomValue = window.innerHeight - (elementDimensions.bottom + popoverDimensions!.height);
|
84 |
const isBottomOptimal = bottomValue >= 0;
|
85 |
|
86 |
+
const leftValue = elementDimensions.left - popoverDimensions!.width;
|
87 |
const isLeftOptimal = leftValue >= 0;
|
88 |
|
89 |
+
const rightValue = window.innerWidth - (elementDimensions.right + popoverDimensions!.width);
|
90 |
const isRightOptimal = rightValue >= 0;
|
91 |
|
92 |
const noneOptimal = !isTopOptimal && !isBottomOptimal && !isLeftOptimal && !isRightOptimal;
|
93 |
+
|
94 |
if (noneOptimal) {
|
95 |
+
const leftValue = window.innerWidth / 2 - popoverDimensions?.realWidth! / 2;
|
96 |
+
const bottomValue = 10;
|
|
|
|
|
|
|
97 |
|
98 |
+
popover.wrapper.style.left = `${leftValue}px`;
|
99 |
+
popover.wrapper.style.right = `auto`;
|
100 |
+
popover.wrapper.style.bottom = `${bottomValue}px`;
|
101 |
+
popover.wrapper.style.top = `auto`;
|
102 |
+
|
103 |
+
popover.arrow.classList.add("driver-popover-arrow-none");
|
104 |
|
|
|
|
|
105 |
return;
|
106 |
}
|
107 |
|
108 |
+
if (isTopOptimal) {
|
109 |
+
const topToSet = Math.min(topValue, window.innerHeight - popoverDimensions.height - popoverArrowDimensions.width);
|
110 |
|
111 |
+
let leftToSet = 0;
|
112 |
+
|
113 |
+
if (requiredAlignment === "start") {
|
114 |
+
leftToSet = Math.max(
|
115 |
+
Math.min(
|
116 |
+
elementDimensions.left - popoverPadding,
|
117 |
+
window.innerWidth - popoverDimensions.width - popoverArrowDimensions.width
|
118 |
+
),
|
119 |
+
popoverArrowDimensions.width
|
120 |
+
);
|
121 |
+
}
|
122 |
+
|
123 |
+
// popover.arrow.classList.add("driver-popover-arrow-bottom");
|
124 |
+
|
125 |
+
popover.wrapper.style.top = `${topToSet}px`;
|
126 |
+
popover.wrapper.style.left = `${leftToSet}px`;
|
127 |
+
popover.wrapper.style.bottom = `auto`;
|
128 |
+
popover.wrapper.style.right = "auto";
|
129 |
+
}
|
130 |
}
|
131 |
|
132 |
function createPopover(): PopoverDOM {
|
|
|
142 |
|
143 |
const description = document.createElement("div");
|
144 |
description.classList.add("driver-popover-description");
|
145 |
+
description.innerText = "Popover description is here";
|
146 |
|
147 |
const footer = document.createElement("div");
|
148 |
footer.classList.add("driver-popover-footer");
|
src/stage.ts
CHANGED
@@ -3,6 +3,9 @@ import { onDriverClick } from "./events";
|
|
3 |
import { emit } from "./emitter";
|
4 |
import { getConfig } from "./config";
|
5 |
|
|
|
|
|
|
|
6 |
export type StageDefinition = {
|
7 |
x: number;
|
8 |
y: number;
|
@@ -15,45 +18,18 @@ 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(
|
19 |
-
|
20 |
-
duration: number,
|
21 |
-
from: Element,
|
22 |
-
to: Element
|
23 |
-
) {
|
24 |
-
const fromDefinition = activeStagePosition
|
25 |
-
? activeStagePosition
|
26 |
-
: from.getBoundingClientRect();
|
27 |
|
28 |
const toDefinition = to.getBoundingClientRect();
|
29 |
|
30 |
-
const x = easeInOutQuad(
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
const y = easeInOutQuad(
|
38 |
-
elapsed,
|
39 |
-
fromDefinition.y,
|
40 |
-
toDefinition.y - fromDefinition.y,
|
41 |
-
duration
|
42 |
-
);
|
43 |
-
|
44 |
-
const width = easeInOutQuad(
|
45 |
-
elapsed,
|
46 |
-
fromDefinition.width,
|
47 |
-
toDefinition.width - fromDefinition.width,
|
48 |
-
duration
|
49 |
-
);
|
50 |
-
|
51 |
-
const height = easeInOutQuad(
|
52 |
-
elapsed,
|
53 |
-
fromDefinition.height,
|
54 |
-
toDefinition.height - fromDefinition.height,
|
55 |
-
duration
|
56 |
-
);
|
57 |
|
58 |
activeStagePosition = {
|
59 |
x,
|
@@ -151,10 +127,7 @@ function createStageSvg(stage: StageDefinition): SVGSVGElement {
|
|
151 |
svg.style.width = "100%";
|
152 |
svg.style.height = "100%";
|
153 |
|
154 |
-
const cutoutPath = document.createElementNS(
|
155 |
-
"http://www.w3.org/2000/svg",
|
156 |
-
"path"
|
157 |
-
);
|
158 |
|
159 |
cutoutPath.setAttribute("d", generateSvgCutoutPathString(stage));
|
160 |
|
@@ -169,23 +142,20 @@ function createStageSvg(stage: StageDefinition): SVGSVGElement {
|
|
169 |
}
|
170 |
|
171 |
function generateSvgCutoutPathString(stage: StageDefinition) {
|
172 |
-
const padding = 4;
|
173 |
-
const radius = 5;
|
174 |
-
|
175 |
const windowX = window.innerWidth;
|
176 |
const windowY = window.innerHeight;
|
177 |
|
178 |
-
const stageWidth = stage.width +
|
179 |
-
const stageHeight = stage.height +
|
180 |
|
181 |
// prevent glitches when stage is too small for radius
|
182 |
-
const limitedRadius = Math.min(
|
183 |
|
184 |
// no value below 0 allowed + round down
|
185 |
const normalizedRadius = Math.floor(Math.max(limitedRadius, 0));
|
186 |
|
187 |
-
const highlightBoxX = stage.x -
|
188 |
-
const highlightBoxY = stage.y -
|
189 |
const highlightBoxWidth = stageWidth - normalizedRadius * 2;
|
190 |
const highlightBoxHeight = stageHeight - normalizedRadius * 2;
|
191 |
|
|
|
3 |
import { emit } from "./emitter";
|
4 |
import { getConfig } from "./config";
|
5 |
|
6 |
+
export const STAGE_PADDING = 10;
|
7 |
+
export const STAGE_RADIUS = 5;
|
8 |
+
|
9 |
export type StageDefinition = {
|
10 |
x: number;
|
11 |
y: number;
|
|
|
18 |
|
19 |
// This method calculates the animated new position of the
|
20 |
// stage (called for each frame by requestAnimationFrame)
|
21 |
+
export function transitionStage(elapsed: number, duration: number, from: Element, to: Element) {
|
22 |
+
const fromDefinition = activeStagePosition ? activeStagePosition : from.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
const toDefinition = to.getBoundingClientRect();
|
25 |
|
26 |
+
const x = easeInOutQuad(elapsed, fromDefinition.x, toDefinition.x - fromDefinition.x, duration);
|
27 |
+
|
28 |
+
const y = easeInOutQuad(elapsed, fromDefinition.y, toDefinition.y - fromDefinition.y, duration);
|
29 |
+
|
30 |
+
const width = easeInOutQuad(elapsed, fromDefinition.width, toDefinition.width - fromDefinition.width, duration);
|
31 |
+
|
32 |
+
const height = easeInOutQuad(elapsed, fromDefinition.height, toDefinition.height - fromDefinition.height, duration);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
activeStagePosition = {
|
35 |
x,
|
|
|
127 |
svg.style.width = "100%";
|
128 |
svg.style.height = "100%";
|
129 |
|
130 |
+
const cutoutPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
|
|
|
|
|
131 |
|
132 |
cutoutPath.setAttribute("d", generateSvgCutoutPathString(stage));
|
133 |
|
|
|
142 |
}
|
143 |
|
144 |
function generateSvgCutoutPathString(stage: StageDefinition) {
|
|
|
|
|
|
|
145 |
const windowX = window.innerWidth;
|
146 |
const windowY = window.innerHeight;
|
147 |
|
148 |
+
const stageWidth = stage.width + STAGE_PADDING * 2;
|
149 |
+
const stageHeight = stage.height + STAGE_PADDING * 2;
|
150 |
|
151 |
// prevent glitches when stage is too small for radius
|
152 |
+
const limitedRadius = Math.min(STAGE_RADIUS, stageWidth / 2, stageHeight / 2);
|
153 |
|
154 |
// no value below 0 allowed + round down
|
155 |
const normalizedRadius = Math.floor(Math.max(limitedRadius, 0));
|
156 |
|
157 |
+
const highlightBoxX = stage.x - STAGE_PADDING + normalizedRadius;
|
158 |
+
const highlightBoxY = stage.y - STAGE_PADDING;
|
159 |
const highlightBoxWidth = stageWidth - normalizedRadius * 2;
|
160 |
const highlightBoxHeight = stageHeight - normalizedRadius * 2;
|
161 |
|
src/style.css
CHANGED
@@ -116,3 +116,8 @@
|
|
116 |
left: 50%;
|
117 |
margin-left: -5px;
|
118 |
}
|
|
|
|
|
|
|
|
|
|
|
|
116 |
left: 50%;
|
117 |
margin-left: -5px;
|
118 |
}
|
119 |
+
|
120 |
+
/* No arrow */
|
121 |
+
.driver-popover-arrow-none {
|
122 |
+
display: none;
|
123 |
+
}
|