Skip to content

Commit 2914146

Browse files
committed
Cleanup.
1 parent 94d514c commit 2914146

6 files changed

Lines changed: 400 additions & 326 deletions

File tree

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: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
const localStorageKey = "gleap-tour-data";
2+
const pointerContainerId = "copilot-pointer-container";
3+
const styleId = "copilot-tour-styles";
4+
const copilotInfoContainerId = "copilot-info-container";
5+
6+
function estimateReadTime(text) {
7+
const wordsPerSecond = 3.5; // Average reading speed
8+
const wordCount = text.split(/\s+/).filter((word) => word.length > 0).length;
9+
const readTimeInSeconds = Math.ceil(wordCount / wordsPerSecond);
10+
return readTimeInSeconds + 1.5;
11+
}
12+
13+
function htmlToPlainText(html) {
14+
const tempDiv = document.createElement("div"); // Create a temporary div
15+
tempDiv.innerHTML = html; // Set the HTML content
16+
return tempDiv.textContent || ""; // Extract and return plain text
17+
}
18+
19+
export default class GleapCopilotTours {
20+
productTourData = undefined;
21+
productTourId = undefined;
22+
currentActiveIndex = undefined;
23+
lastArrowPositionX = undefined;
24+
lastArrowPositionY = undefined;
25+
26+
// GleapReplayRecorder singleton
27+
static instance;
28+
static getInstance() {
29+
if (!this.instance) {
30+
this.instance = new GleapCopilotTours();
31+
return this.instance;
32+
} else {
33+
return this.instance;
34+
}
35+
}
36+
37+
constructor() {}
38+
39+
startWithConfig(tourId, config, delay = 0) {
40+
// Prevent multiple tours from being started.
41+
if (this.productTourId) {
42+
return;
43+
}
44+
45+
this.productTourId = tourId;
46+
this.productTourData = config;
47+
this.currentActiveIndex = 0;
48+
49+
const self = this;
50+
51+
if (delay > 0) {
52+
return setTimeout(() => {
53+
self.start();
54+
}, delay);
55+
} else {
56+
return this.start();
57+
}
58+
}
59+
60+
loadUncompletedTour() {
61+
try {
62+
const data = JSON.parse(localStorage.getItem(localStorageKey));
63+
if (data?.tourData && data?.tourId) {
64+
return data;
65+
}
66+
} catch (e) {}
67+
68+
return null;
69+
}
70+
71+
storeUncompletedTour() {
72+
if (this.productTourId && this.productTourData) {
73+
try {
74+
let data = JSON.parse(
75+
JSON.stringify({
76+
tourData: this.productTourData,
77+
tourId: this.productTourId,
78+
})
79+
);
80+
81+
data.tourData.steps = data.tourData.steps.slice(
82+
this.currentActiveIndex || 0,
83+
data.tourData.steps.length
84+
);
85+
86+
console.log("Storing uncompleted tour:", data);
87+
88+
localStorage.setItem(localStorageKey, JSON.stringify(data));
89+
} catch (e) {}
90+
} else {
91+
this.clearUncompletedTour();
92+
}
93+
}
94+
95+
clearUncompletedTour() {
96+
try {
97+
localStorage.removeItem(localStorageKey);
98+
} catch (e) {}
99+
}
100+
101+
updatePointerPosition(anchor) {
102+
const container = document.getElementById(pointerContainerId);
103+
if (!container) {
104+
return;
105+
}
106+
107+
const infoBubble = container.querySelector("#info-bubble");
108+
109+
// If no anchor, center on screen.
110+
if (!anchor) {
111+
const scrollX = window.scrollX || 0;
112+
const scrollY = window.scrollY || 0;
113+
114+
// The center of the *viewport* in document coordinates:
115+
const centerX = scrollX + window.innerWidth / 2;
116+
const centerY = scrollY + window.innerHeight / 2;
117+
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+
}
124+
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;
159+
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.
165+
166+
infoBubble.style.marginLeft = "";
167+
infoBubble.style.marginRight = "10px";
168+
// Or do something like:
169+
// infoBubble.style.transform = `translateX(-${bubbleRect.width + 10}px)`;
170+
}
171+
}
172+
173+
console.log("Pointer placed at:", containerLeft, containerTop);
174+
}
175+
176+
cleanup() {
177+
this.removePointerUI();
178+
this.clearUncompletedTour();
179+
}
180+
181+
removePointerUI() {
182+
const container = document.getElementById(pointerContainerId);
183+
if (container) {
184+
container.remove();
185+
}
186+
187+
// Remove style node.
188+
const styleNode = document.getElementById(styleId);
189+
if (styleNode) {
190+
styleNode.remove();
191+
}
192+
193+
// Remove copilot info container.
194+
const copilotInfoContainer = document.getElementById(
195+
copilotInfoContainerId
196+
);
197+
if (copilotInfoContainer) {
198+
copilotInfoContainer.remove();
199+
}
200+
}
201+
202+
setupCopilotTour() {
203+
// Inject CSS into the document head
204+
let styleNode = document.getElementById(styleId);
205+
if (!styleNode) {
206+
styleNode = document.createElement("style");
207+
styleNode.id = styleId;
208+
styleNode.type = "text/css";
209+
styleNode.textContent = `
210+
#${pointerContainerId} {
211+
position: absolute;
212+
top: 0;
213+
left: 0;
214+
display: flex;
215+
align-items: center;
216+
pointer-events: none;
217+
z-index: 9999;
218+
transition: transform 0.5s ease, top 0.5s ease, left 0.5s ease;;
219+
}
220+
221+
#${pointerContainerId} svg {
222+
width: 20px;
223+
height: auto;
224+
fill: none;
225+
}
226+
227+
#info-bubble {
228+
margin-left: 10px;
229+
padding: 10px 15px;
230+
border-radius: 20px;
231+
background-color: black;
232+
color: white;
233+
font-family: Arial, sans-serif;
234+
font-size: 14px;
235+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
236+
}
237+
238+
body::before {
239+
content: "";
240+
position: fixed;
241+
top: 0;
242+
left: 0;
243+
width: 100vw;
244+
height: 100vh;
245+
pointer-events: all;
246+
z-index: 2147483610;
247+
box-sizing: border-box;
248+
border: 6px solid transparent;
249+
filter: blur(15px);
250+
border-image-slice: 1;
251+
border-image-source: linear-gradient(45deg, #2142e7, #e721b3);
252+
animation: animateBorder 4s infinite alternate ease-in-out;
253+
}
254+
255+
body::after {
256+
content: "";
257+
position: fixed;
258+
top: 0;
259+
left: 0;
260+
width: 100vw;
261+
height: 100vh;
262+
pointer-events: all;
263+
z-index: 2147483610;
264+
opacity: 0.5;
265+
box-sizing: border-box;
266+
border: 2px solid transparent;
267+
border-image-slice: 1;
268+
border-image-source: linear-gradient(45deg, #2142e7, #e721b3);
269+
animation: animateBorder 4s infinite alternate ease-in-out;
270+
}
271+
272+
@keyframes animateBorder {
273+
0% {
274+
border-image-source: linear-gradient(45deg, #2142e7, #e721b3);
275+
}
276+
50% {
277+
border-image-source: linear-gradient(135deg, #e721b3, #ff8a00);
278+
}
279+
100% {
280+
border-image-source: linear-gradient(225deg, #ff8a00, #2142e7);
281+
}
282+
}
283+
284+
.copilot-info-container {
285+
position: fixed;
286+
top: 20px;
287+
right: 20px;
288+
z-index: 2147483610;
289+
background: #fff;
290+
padding: 5px;
291+
padding-left: 10px;
292+
border-radius: 10px;
293+
box-shadow: 0 0 20px 0 #e721b263;
294+
font-family: sans-serif;
295+
font-size: 13px;
296+
color: #000;
297+
display: flex;
298+
align-items: center;
299+
gap: 10px;
300+
border: 1px solid #e721b3;
301+
}
302+
303+
.copilot-info-container svg {
304+
width: 24px;
305+
height: 24px;
306+
}
307+
`;
308+
document.head.appendChild(styleNode);
309+
}
310+
311+
// Create the container div
312+
const container = document.createElement("div");
313+
container.id = pointerContainerId;
314+
container.style.opacity = 0;
315+
316+
// Create the SVG mouse pointer
317+
const svgMouse = document.createElementNS(
318+
"http://www.w3.org/2000/svg",
319+
"svg"
320+
);
321+
svgMouse.setAttribute("viewBox", "0 0 380 476");
322+
svgMouse.innerHTML =
323+
'<path d="M352.595 268.315L352.581 268.302L352.566 268.29L78.6092 24.7278C71.6245 18.433 62.5487 15 53.2 15C32.1157 15 15 32.1157 15 53.2V424C15 444.34 31.4714 461 52 461C62.6797 461 72.8089 456.467 79.8863 448.38C79.8871 448.379 79.8879 448.378 79.8886 448.378L180.804 333.1H327.9C348.384 333.1 365 316.484 365 296C365 285.404 360.46 275.344 352.595 268.315Z" fill="black" stroke="white" stroke-width="30"/>';
324+
325+
// Create the info bubble
326+
const infoBubble = document.createElement("div");
327+
infoBubble.id = "info-bubble";
328+
infoBubble.textContent = "";
329+
330+
// Append elements
331+
container.appendChild(svgMouse);
332+
container.appendChild(infoBubble);
333+
document.body.appendChild(container);
334+
}
335+
336+
start() {
337+
const config = this.productTourData;
338+
if (!config) {
339+
return;
340+
}
341+
342+
// Setup the copilot tour.
343+
this.setupCopilotTour();
344+
345+
// Render the first step.
346+
this.renderNextStep();
347+
}
348+
349+
renderNextStep() {
350+
const config = this.productTourData;
351+
const steps = config.steps;
352+
const self = this;
353+
354+
// Check if we have reached the end of the tour.
355+
if (this.currentActiveIndex >= steps.length) {
356+
this.cleanup();
357+
return;
358+
}
359+
360+
const currentStep = steps[this.currentActiveIndex];
361+
const element = document.querySelector(currentStep.selector);
362+
363+
// Wait for the pointer to be rendered. (by checking if the pointer container exists)
364+
setTimeout(() => {
365+
this.updatePointerPosition(element);
366+
}, 100);
367+
368+
const message =
369+
currentStep && currentStep.message
370+
? htmlToPlainText(currentStep.message)
371+
: "🤔";
372+
373+
// Set content of info bubble.
374+
document.getElementById("info-bubble").textContent = message;
375+
document.getElementById(pointerContainerId).style.opacity = 1;
376+
377+
// Estimate readtime in seconds.
378+
const readTime = estimateReadTime(message);
379+
380+
// Automatically move to next step after 3 seconds.
381+
setTimeout(() => {
382+
self.currentActiveIndex++;
383+
self.renderNextStep();
384+
self.storeUncompletedTour();
385+
386+
if (currentStep.mode === "CLICK") {
387+
// Perform click on element.
388+
setTimeout(() => {
389+
try {
390+
element.click();
391+
} catch (e) {}
392+
}, 1000 * 60);
393+
}
394+
}, readTime * 1000);
395+
}
396+
}

0 commit comments

Comments
 (0)