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 |
+
}
|