2023-06-15 12:55:30 +00:00
|
|
|
import Shepherd from "shepherd.js";
|
2023-07-02 10:25:48 +00:00
|
|
|
import {fromStorage, StorageKey} from "../storage/common.js";
|
2023-07-02 09:28:41 +00:00
|
|
|
import {
|
|
|
|
allTours,
|
|
|
|
introductionTour,
|
|
|
|
showTourError,
|
|
|
|
type TourData,
|
|
|
|
} from "../tours/exports.js";
|
2023-06-15 12:55:30 +00:00
|
|
|
|
|
|
|
/** The main entry point for the content script. */
|
|
|
|
async function main(): Promise<void> {
|
2023-07-02 10:25:48 +00:00
|
|
|
const introductionUnderstood = await fromStorage(
|
|
|
|
StorageKey.IntroductionUnderstood,
|
|
|
|
);
|
2023-06-15 12:55:30 +00:00
|
|
|
|
|
|
|
// Get the anchor without the leading #.
|
|
|
|
const anchor = window.location.hash.slice(1);
|
|
|
|
|
|
|
|
// We only care about anchors with our prefix.
|
|
|
|
const prefix = "tildes-shepherd-tour=";
|
2023-06-17 13:01:26 +00:00
|
|
|
const startsWithPrefix = anchor.startsWith(prefix);
|
|
|
|
|
2023-07-01 09:33:54 +00:00
|
|
|
// Get the tour ID from the anchor by removing the prefix.
|
|
|
|
const anchorTourId = anchor.slice(prefix.length);
|
|
|
|
|
2023-06-17 13:01:26 +00:00
|
|
|
// Automatically start the introduction tour if the person hasn't already
|
|
|
|
// been through it and only when on the Tildes homepage.
|
|
|
|
if (!introductionUnderstood.value && window.location.pathname === "/") {
|
|
|
|
// If a different tour is selected but the introduction hasn't happened yet,
|
|
|
|
// then the main function will be rerun once this tour finishes.
|
2023-07-01 09:33:54 +00:00
|
|
|
startTour(
|
2023-07-02 09:28:41 +00:00
|
|
|
introductionTour,
|
2023-07-01 09:33:54 +00:00
|
|
|
startsWithPrefix && anchorTourId !== "introduction",
|
|
|
|
);
|
2023-06-17 13:01:26 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!startsWithPrefix) {
|
2023-06-15 12:55:30 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-07-01 13:46:23 +00:00
|
|
|
const userIsLoggedIn =
|
|
|
|
document.querySelector(".logged-in-user-username") !== null;
|
|
|
|
|
2023-06-15 12:55:30 +00:00
|
|
|
// Then run through all of the tours we have and start the first match for the
|
|
|
|
// ID.
|
2023-07-02 09:28:41 +00:00
|
|
|
for (const tour of allTours) {
|
|
|
|
if (anchorTourId === tour.id) {
|
|
|
|
if (tour.requirements.mustBeLoggedIn && !userIsLoggedIn) {
|
2023-07-01 13:46:23 +00:00
|
|
|
showTourError(
|
2023-07-02 09:28:41 +00:00
|
|
|
`The ${tour.id} tour can only be shown with a logged in account.`,
|
2023-07-01 13:46:23 +00:00
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-07-02 09:28:41 +00:00
|
|
|
if (tour.requirements.path !== window.location.pathname) {
|
2023-07-01 13:46:23 +00:00
|
|
|
// This tour's path requirement does not match.
|
|
|
|
showTourError(
|
2023-07-02 09:28:41 +00:00
|
|
|
`The ${tour.id} tour can only be start on the ${tour.requirements.path} page.`,
|
2023-07-01 13:46:23 +00:00
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-07-02 09:28:41 +00:00
|
|
|
startTour(tour, false);
|
2023-06-15 12:55:30 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
console.error(`Unknown anchor tour id: ${anchorTourId}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Starts a new Shepherd.js Tour with the specific steps and event handlers.
|
|
|
|
* @param tourId A unique ID for this tour.
|
|
|
|
* @param steps All the steps of the tour.
|
|
|
|
* @param eventHandlers Event handlers to attach to specific steps.
|
2023-06-18 11:54:18 +00:00
|
|
|
* @param runMainAgainAfterComplete Should the `main` function be run after this
|
|
|
|
* tour is completed?
|
2023-06-15 12:55:30 +00:00
|
|
|
*/
|
2023-07-02 09:28:41 +00:00
|
|
|
function startTour(data: TourData, runMainAgainAfterComplete: boolean): void {
|
2023-06-17 13:15:10 +00:00
|
|
|
const defaultButtons: Shepherd.Step.StepOptionsButton[] = [
|
|
|
|
{
|
|
|
|
classes: "btn",
|
|
|
|
text: "Continue",
|
|
|
|
action() {
|
|
|
|
this.next();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
classes: "btn",
|
|
|
|
text: "Back",
|
|
|
|
action() {
|
|
|
|
this.back();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
classes: "btn",
|
|
|
|
text: "Exit",
|
|
|
|
action() {
|
|
|
|
this.cancel();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2023-06-15 12:55:30 +00:00
|
|
|
const tour = new Shepherd.Tour({
|
|
|
|
defaultStepOptions: {
|
2023-06-17 13:15:10 +00:00
|
|
|
buttons: [...defaultButtons],
|
2023-06-15 12:55:30 +00:00
|
|
|
},
|
|
|
|
useModalOverlay: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Add an event handler for when the tour completes.
|
|
|
|
tour.on("complete", async () => {
|
|
|
|
// Remove all mock elements that were added in the tour, just in case any
|
|
|
|
// weren't removed after their respective steps.
|
|
|
|
const mockSelector = '[data-tildes-shepherd-mock="true"]';
|
|
|
|
const mockElements = document.querySelectorAll<HTMLElement>(mockSelector);
|
|
|
|
for (const element of Array.from(mockElements)) {
|
|
|
|
element.remove();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mark the tour as completed.
|
2023-07-02 10:25:48 +00:00
|
|
|
const completedTours = await fromStorage(StorageKey.ToursCompleted);
|
|
|
|
completedTours.value.add(data.id);
|
|
|
|
await completedTours.save();
|
2023-06-17 13:01:26 +00:00
|
|
|
|
|
|
|
if (runMainAgainAfterComplete) {
|
|
|
|
await main();
|
|
|
|
}
|
2023-06-15 12:55:30 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// For every step we have, add it to the tour and subsequently add all the
|
|
|
|
// event handlers to that step.
|
2023-07-03 12:11:46 +00:00
|
|
|
for (const [stepNumber, stepOptions] of data.steps().entries()) {
|
2023-06-17 13:15:10 +00:00
|
|
|
// If the final step doesn't have buttons defined, set the "Continue" button
|
|
|
|
// text to "Finish".
|
2023-07-02 09:28:41 +00:00
|
|
|
if (
|
|
|
|
stepOptions.buttons === undefined &&
|
|
|
|
stepNumber + 1 === data.steps.length
|
|
|
|
) {
|
2023-06-17 13:15:10 +00:00
|
|
|
stepOptions.buttons = [...defaultButtons];
|
|
|
|
stepOptions.buttons[0].text = "Finish";
|
|
|
|
}
|
|
|
|
|
2023-06-15 12:55:30 +00:00
|
|
|
const step = tour.addStep(stepOptions);
|
|
|
|
|
2023-07-02 09:28:41 +00:00
|
|
|
for (const {stepId, eventHandlers} of data.eventHandlers) {
|
|
|
|
if (stepId === step.id) {
|
|
|
|
for (const {event, handler} of eventHandlers) {
|
|
|
|
step.on(event, handler);
|
|
|
|
}
|
2023-06-15 12:55:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pull the lever, Kronk!
|
|
|
|
tour.start();
|
|
|
|
}
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", main);
|