Skip to content

Commit 382cbca

Browse files
committed
Cleanup.
1 parent 2914146 commit 382cbca

File tree

6 files changed

+179
-103
lines changed

6 files changed

+179
-103
lines changed

build/cjs/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build/esm/index.mjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

published/14.2.0/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

published/latest/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/GleapCopilotTours.js

Lines changed: 172 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,46 @@ function htmlToPlainText(html) {
1616
return tempDiv.textContent || ""; // Extract and return plain text
1717
}
1818

19+
function scrollToElement(element) {
20+
if (element) {
21+
element.scrollIntoView({
22+
behavior: "smooth", // Ensures smooth scrolling
23+
block: "center", // Aligns the element in the center of the viewport
24+
inline: "center", // Aligns inline elements in the center horizontally
25+
});
26+
}
27+
}
28+
29+
function waitForElement(selector, timeout = 5000) {
30+
const pollInterval = 100;
31+
const maxAttempts = timeout / pollInterval;
32+
let attempts = 0;
33+
34+
return new Promise((resolve, reject) => {
35+
const interval = setInterval(() => {
36+
const element = document.querySelector(selector);
37+
if (element) {
38+
clearInterval(interval);
39+
resolve(element);
40+
} else if (attempts >= maxAttempts) {
41+
clearInterval(interval);
42+
reject(new Error(`Element not found for selector: ${selector}`));
43+
}
44+
attempts++;
45+
}, pollInterval);
46+
});
47+
}
48+
49+
function smoothScrollToY(yPosition) {
50+
const viewportHeight = window.innerHeight;
51+
const targetScrollPosition = yPosition - viewportHeight / 2;
52+
53+
window.scrollTo({
54+
top: targetScrollPosition,
55+
behavior: "smooth", // Ensures smooth scrolling
56+
});
57+
}
58+
1959
export default class GleapCopilotTours {
2060
productTourData = undefined;
2161
productTourId = undefined;
@@ -34,7 +74,33 @@ export default class GleapCopilotTours {
3474
}
3575
}
3676

37-
constructor() {}
77+
constructor() {
78+
const self = this;
79+
// Add on window resize listener.
80+
window.addEventListener("resize", () => {
81+
// Check if we currently have a tour.
82+
if (
83+
self.productTourId &&
84+
self.currentActiveIndex >= 0 &&
85+
self.productTourData &&
86+
self.productTourData.steps
87+
) {
88+
const steps = self.productTourData.steps;
89+
const currentStep = steps[self.currentActiveIndex];
90+
91+
if (
92+
currentStep &&
93+
currentStep.selector &&
94+
currentStep.selector !== ""
95+
) {
96+
// Wait for the element to be rendered.
97+
self.updatePointerPosition(
98+
document.querySelector(currentStep.selector)
99+
);
100+
}
101+
}
102+
});
103+
}
38104

39105
startWithConfig(tourId, config, delay = 0) {
40106
// Prevent multiple tours from being started.
@@ -99,78 +165,68 @@ export default class GleapCopilotTours {
99165
}
100166

101167
updatePointerPosition(anchor) {
102-
const container = document.getElementById(pointerContainerId);
103-
if (!container) {
104-
return;
105-
}
168+
try {
169+
const container = document.getElementById(pointerContainerId);
170+
if (!container) {
171+
return;
172+
}
106173

107-
const infoBubble = container.querySelector("#info-bubble");
174+
// If no anchor, center on screen.
175+
if (!anchor) {
176+
const scrollX = window.scrollX || 0;
177+
const scrollY = window.scrollY || 0;
108178

109-
// If no anchor, center on screen.
110-
if (!anchor) {
111-
const scrollX = window.scrollX || 0;
112-
const scrollY = window.scrollY || 0;
179+
// The center of the *viewport* in document coordinates:
180+
const centerX = scrollX + window.innerWidth / 2;
181+
const centerY = scrollY + window.innerHeight / 2;
113182

114-
// The center of the *viewport* in document coordinates:
115-
const centerX = scrollX + window.innerWidth / 2;
116-
const centerY = scrollY + window.innerHeight / 2;
183+
container.style.position = "absolute";
184+
container.style.left = `${centerX}px`;
185+
container.style.top = `${centerY}px`;
186+
container.style.transform = `translate(-50%, -50%)`;
117187

118-
container.style.position = "absolute";
119-
container.style.left = `${centerX}px`;
120-
container.style.top = `${centerY}px`;
121-
container.style.transform = `translate(-50%, -50%)`;
122-
return;
123-
}
188+
smoothScrollToY(centerY);
124189

125-
// 1) Calculate the anchor’s position on the page (not just viewport).
126-
const anchorRect = anchor.getBoundingClientRect();
127-
const containerRect = container.getBoundingClientRect();
128-
129-
// Suppose the arrow’s tip is ~15px from the top and left (tweak as needed).
130-
const arrowTipOffsetX = 15;
131-
const arrowTipOffsetY = 15;
132-
133-
// Center of anchor:
134-
const anchorCenterX =
135-
anchorRect.left + anchorRect.width / 2 + window.scrollX;
136-
const anchorCenterY =
137-
anchorRect.top + anchorRect.height / 2 + window.scrollY;
138-
139-
// We want the arrow’s tip at the anchorCenter.
140-
// So offset the pointer container so that (container’s top-left + arrowTipOffset) = anchor center
141-
const containerLeft = anchorCenterX - arrowTipOffsetX;
142-
const containerTop = anchorCenterY - arrowTipOffsetY;
143-
144-
// Position the pointer container (arrow + bubble).
145-
container.style.left = `${containerLeft}px`;
146-
container.style.top = `${containerTop}px`;
147-
container.style.transform = ""; // no translate needed, or clear any you might have
148-
149-
// 2) Check if the info bubble goes off the right edge
150-
if (infoBubble) {
151-
// Reset bubble style so we can measure it properly
152-
infoBubble.style.marginLeft = "10px"; // default to the right side
153-
infoBubble.style.marginRight = ""; // clear any previous override
154-
infoBubble.style.transform = "none";
155-
156-
const bubbleRect = infoBubble.getBoundingClientRect();
157-
const bubbleRightEdge = bubbleRect.right;
158-
const windowWidth = window.innerWidth;
190+
return;
191+
}
192+
193+
// 1) Calculate the anchor’s position on the page (not just viewport).
194+
const anchorRect = anchor.getBoundingClientRect();
159195

160-
// If bubble extends past the right edge by any amount, flip it to the left side
161-
if (bubbleRightEdge > windowWidth) {
162-
// Move bubble to the left side of the arrow
163-
// One approach: negative margin-left by the bubble’s width + some padding
164-
// Another approach: transform: translateX(-100%) etc.
196+
// Center of anchor:
197+
let anchorCenterX =
198+
anchorRect.left + anchorRect.width / 2 + window.scrollX;
199+
let anchorCenterY =
200+
anchorRect.top + anchorRect.height / 2 + window.scrollY;
165201

166-
infoBubble.style.marginLeft = "";
167-
infoBubble.style.marginRight = "10px";
168-
// Or do something like:
169-
// infoBubble.style.transform = `translateX(-${bubbleRect.width + 10}px)`;
202+
let containerWidthSpace = 330;
203+
if (containerWidthSpace > window.innerWidth - 40) {
204+
containerWidthSpace = window.innerWidth - 40;
205+
}
206+
207+
const windowWidth = window.innerWidth;
208+
const isTooFarRight =
209+
anchorCenterX + containerWidthSpace > windowWidth - 20;
210+
211+
container.style.transform = "";
212+
213+
if (isTooFarRight) {
214+
container.classList.add("copilot-pointer-container-right");
215+
216+
// Reverse the arrow direction and recalculate the position.
217+
container.style.right = `${windowWidth - anchorCenterX}px`;
218+
container.style.top = `${anchorCenterY}px`;
219+
container.style.left = "";
220+
} else {
221+
container.classList.remove("copilot-pointer-container-right");
222+
container.style.left = `${anchorCenterX}px`;
223+
container.style.top = `${anchorCenterY}px`;
170224
}
171-
}
172225

173-
console.log("Pointer placed at:", containerLeft, containerTop);
226+
scrollToElement(anchor);
227+
} catch (e) {
228+
console.error("Error updating pointer position:", e);
229+
}
174230
}
175231

176232
cleanup() {
@@ -212,20 +268,36 @@ export default class GleapCopilotTours {
212268
top: 0;
213269
left: 0;
214270
display: flex;
215-
align-items: center;
271+
align-items: flex-start;
216272
pointer-events: none;
217273
z-index: 9999;
218-
transition: transform 0.5s ease, top 0.5s ease, left 0.5s ease;;
274+
transition: all 0.5s ease;;
219275
}
220276
221277
#${pointerContainerId} svg {
222278
width: 20px;
223279
height: auto;
224280
fill: none;
225281
}
282+
283+
.${pointerContainerId}-right {
284+
left: auto;
285+
right: 0;
286+
flex-direction: row-reverse;
287+
}
288+
289+
.${pointerContainerId}-right svg {
290+
transform: scaleX(-1);
291+
}
292+
293+
.${pointerContainerId}-right #info-bubble {
294+
margin-left: 0px;
295+
margin-right: 5px;
296+
}
226297
227298
#info-bubble {
228-
margin-left: 10px;
299+
margin-left: 5px;
300+
margin-top: 18px;
229301
padding: 10px 15px;
230302
border-radius: 20px;
231303
background-color: black;
@@ -245,11 +317,11 @@ export default class GleapCopilotTours {
245317
pointer-events: all;
246318
z-index: 2147483610;
247319
box-sizing: border-box;
248-
border: 6px solid transparent;
249-
filter: blur(15px);
320+
border: 8px solid transparent;
321+
filter: blur(20px);
250322
border-image-slice: 1;
251323
border-image-source: linear-gradient(45deg, #2142e7, #e721b3);
252-
animation: animateBorder 4s infinite alternate ease-in-out;
324+
animation: animateBorder 3s infinite alternate ease-in-out;
253325
}
254326
255327
body::after {
@@ -266,7 +338,7 @@ export default class GleapCopilotTours {
266338
border: 2px solid transparent;
267339
border-image-slice: 1;
268340
border-image-source: linear-gradient(45deg, #2142e7, #e721b3);
269-
animation: animateBorder 4s infinite alternate ease-in-out;
341+
animation: animateBorder 3s infinite alternate ease-in-out;
270342
}
271343
272344
@keyframes animateBorder {
@@ -298,11 +370,13 @@ export default class GleapCopilotTours {
298370
align-items: center;
299371
gap: 10px;
300372
border: 1px solid #e721b3;
373+
max-width: min(330px, 100vw - 40px);
301374
}
302375
303376
.copilot-info-container svg {
304377
width: 24px;
305378
height: 24px;
379+
flex-shrink: 0;
306380
}
307381
`;
308382
document.head.appendChild(styleNode);
@@ -349,7 +423,6 @@ export default class GleapCopilotTours {
349423
renderNextStep() {
350424
const config = this.productTourData;
351425
const steps = config.steps;
352-
const self = this;
353426

354427
// Check if we have reached the end of the tour.
355428
if (this.currentActiveIndex >= steps.length) {
@@ -358,39 +431,43 @@ export default class GleapCopilotTours {
358431
}
359432

360433
const currentStep = steps[this.currentActiveIndex];
361-
const element = document.querySelector(currentStep.selector);
362434

363-
// Wait for the pointer to be rendered. (by checking if the pointer container exists)
364-
setTimeout(() => {
435+
const handleStep = (element) => {
436+
// Update pointer position, even if element is null.
365437
this.updatePointerPosition(element);
366-
}, 100);
367438

368-
const message =
369-
currentStep && currentStep.message
439+
const message = currentStep?.message
370440
? htmlToPlainText(currentStep.message)
371441
: "🤔";
372442

373-
// Set content of info bubble.
374-
document.getElementById("info-bubble").textContent = message;
375-
document.getElementById(pointerContainerId).style.opacity = 1;
443+
// Set content of info bubble.
444+
document.getElementById("info-bubble").textContent = message;
445+
document.getElementById(pointerContainerId).style.opacity = 1;
376446

377-
// Estimate readtime in seconds.
378-
const readTime = estimateReadTime(message);
447+
// Estimate read time in seconds.
448+
const readTime = estimateReadTime(message);
379449

380-
// Automatically move to next step after 3 seconds.
381-
setTimeout(() => {
382-
self.currentActiveIndex++;
383-
self.renderNextStep();
384-
self.storeUncompletedTour();
450+
// Automatically move to the next step after the estimated read time.
451+
setTimeout(() => {
452+
this.currentActiveIndex++;
453+
this.storeUncompletedTour();
385454

386-
if (currentStep.mode === "CLICK") {
387-
// Perform click on element.
388-
setTimeout(() => {
455+
if (currentStep.mode === "CLICK" && element) {
389456
try {
390457
element.click();
391-
} catch (e) {}
392-
}, 1000 * 60);
393-
}
394-
}, readTime * 1000);
458+
} catch (e) {
459+
console.error("Error clicking the element:", e);
460+
}
461+
}
462+
463+
this.renderNextStep();
464+
}, readTime * 1000);
465+
};
466+
467+
const elementPromise = currentStep.selector
468+
? waitForElement(currentStep.selector)
469+
: Promise.resolve(null);
470+
471+
elementPromise.then(handleStep).catch(() => handleStep(null));
395472
}
396473
}

0 commit comments

Comments
 (0)