kamrify commited on
Commit
f86a242
·
1 Parent(s): a65a41e

Repositioning of popover

Browse files
Files changed (4) hide show
  1. src/highlight.ts +1 -1
  2. src/popover.ts +60 -37
  3. src/stage.ts +18 -48
  4. 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 { refreshPopover, renderPopover } from "./popover";
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 = "0";
37
- popoverWrapper.style.top = "0";
38
  popoverWrapper.style.bottom = "";
39
  popoverWrapper.style.right = "";
40
 
41
- refreshPopover(element);
 
 
 
 
42
  bringInView(popoverWrapper);
43
  }
44
 
45
- export function refreshPopover(element: Element) {
46
- if (!popover) {
47
  return;
48
  }
49
 
50
- const popoverArrow = popover.arrow;
 
 
 
 
51
 
52
- // const position = calculatePopoverPosition(element);
53
- popoverArrow?.classList.add("driver-popover-arrow-side-bottom", "driver-popover-arrow-align-center");
 
54
  }
55
 
56
- function calculatePopoverPosition(element: Element) {
57
  if (!popover) {
58
  return;
59
  }
60
 
61
- const popoverPadding = 10;
 
62
 
63
- const popoverDimensions = popover.wrapper.getBoundingClientRect();
64
  const popoverArrowDimensions = popover.arrow.getBoundingClientRect();
65
  const elementDimensions = element.getBoundingClientRect();
66
 
67
- const popoverPaddedWidth = popoverDimensions.width + popoverPadding;
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 + popoverPaddedHeight);
74
  const isBottomOptimal = bottomValue >= 0;
75
 
76
- const leftValue = elementDimensions.left - popoverPaddedWidth;
77
  const isLeftOptimal = leftValue >= 0;
78
 
79
- const rightValue = window.innerWidth - (elementDimensions.right + popoverPaddedWidth);
80
  const isRightOptimal = rightValue >= 0;
81
 
82
  const noneOptimal = !isTopOptimal && !isBottomOptimal && !isLeftOptimal && !isRightOptimal;
 
83
  if (noneOptimal) {
84
- return {
85
- left: window.innerWidth / 2 - popoverDimensions.width / 2,
86
- bottom: 10,
87
- };
88
- }
89
 
90
- // @todo placement based on the side and alignment
91
- }
 
 
 
 
92
 
93
- function getLeftValueAfterAlignment(element: Element) {
94
- if (!popover) {
95
  return;
96
  }
97
 
98
- const popoverRect = popover.wrapper.getBoundingClientRect();
99
- const elementRect = element.getBoundingClientRect();
100
 
101
- const requiredAlignment = 'left';
102
- const popoverWidth = popoverRect.width;
103
- const pos = element.getBoundingClientRect().left;
104
- const end = window.innerWidth;
105
- const elementLength = elementRect.width;
106
- const extraPadding = popover.arrow.getBoundingClientRect().width;
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Description";
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
- elapsed: number,
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
- elapsed,
32
- fromDefinition.x,
33
- toDefinition.x - fromDefinition.x,
34
- duration
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 + padding * 2;
179
- const stageHeight = stage.height + padding * 2;
180
 
181
  // prevent glitches when stage is too small for radius
182
- const limitedRadius = Math.min(radius, stageWidth / 2, stageHeight / 2);
183
 
184
  // no value below 0 allowed + round down
185
  const normalizedRadius = Math.floor(Math.max(limitedRadius, 0));
186
 
187
- const highlightBoxX = stage.x - padding + normalizedRadius;
188
- const highlightBoxY = stage.y - padding;
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
+ }