Compare commits
No commits in common. "866d8238200b7276e228ecd579ca3770ce3849bf" and "48742a98641e75b1d14858cb474c8ea14a80696b" have entirely different histories.
866d823820
...
48742a9864
|
@ -1,17 +1,14 @@
|
||||||
import Shepherd from "shepherd.js";
|
import Shepherd from "shepherd.js";
|
||||||
import {fromStorage, StorageKey} from "../storage/common.js";
|
|
||||||
import {
|
import {
|
||||||
allTours,
|
addCompletedTour,
|
||||||
introductionTour,
|
createIntroductionUnderstood,
|
||||||
showTourError,
|
} from "../storage/common.js";
|
||||||
type TourData,
|
import {introductionSteps} from "../tours/introduction.js";
|
||||||
} from "../tours/exports.js";
|
import {TourId, tourIdsAndSteps} from "../tours/exports.js";
|
||||||
|
|
||||||
/** The main entry point for the content script. */
|
/** The main entry point for the content script. */
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const introductionUnderstood = await fromStorage(
|
const introductionUnderstood = await createIntroductionUnderstood();
|
||||||
StorageKey.IntroductionUnderstood,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the anchor without the leading #.
|
// Get the anchor without the leading #.
|
||||||
const anchor = window.location.hash.slice(1);
|
const anchor = window.location.hash.slice(1);
|
||||||
|
@ -29,7 +26,9 @@ async function main(): Promise<void> {
|
||||||
// If a different tour is selected but the introduction hasn't happened yet,
|
// If a different tour is selected but the introduction hasn't happened yet,
|
||||||
// then the main function will be rerun once this tour finishes.
|
// then the main function will be rerun once this tour finishes.
|
||||||
startTour(
|
startTour(
|
||||||
introductionTour,
|
TourId.Introduction,
|
||||||
|
introductionSteps,
|
||||||
|
[],
|
||||||
startsWithPrefix && anchorTourId !== "introduction",
|
startsWithPrefix && anchorTourId !== "introduction",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -39,29 +38,11 @@ async function main(): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userIsLoggedIn =
|
|
||||||
document.querySelector(".logged-in-user-username") !== null;
|
|
||||||
|
|
||||||
// Then run through all of the tours we have and start the first match for the
|
// Then run through all of the tours we have and start the first match for the
|
||||||
// ID.
|
// ID.
|
||||||
for (const tour of allTours) {
|
for (const [id, steps, eventHandlers] of tourIdsAndSteps) {
|
||||||
if (anchorTourId === tour.id) {
|
if (anchorTourId === id) {
|
||||||
if (tour.requirements.mustBeLoggedIn && !userIsLoggedIn) {
|
startTour(id, steps, eventHandlers, false);
|
||||||
showTourError(
|
|
||||||
`The ${tour.id} tour can only be shown with a logged in account.`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tour.requirements.path !== window.location.pathname) {
|
|
||||||
// This tour's path requirement does not match.
|
|
||||||
showTourError(
|
|
||||||
`The ${tour.id} tour can only be start on the ${tour.requirements.path} page.`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
startTour(tour, false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +58,12 @@ async function main(): Promise<void> {
|
||||||
* @param runMainAgainAfterComplete Should the `main` function be run after this
|
* @param runMainAgainAfterComplete Should the `main` function be run after this
|
||||||
* tour is completed?
|
* tour is completed?
|
||||||
*/
|
*/
|
||||||
function startTour(data: TourData, runMainAgainAfterComplete: boolean): void {
|
function startTour(
|
||||||
|
tourId: TourId,
|
||||||
|
steps: TourStepOptions[],
|
||||||
|
eventHandlers: TourStepEventHandler[],
|
||||||
|
runMainAgainAfterComplete: boolean,
|
||||||
|
): void {
|
||||||
const defaultButtons: Shepherd.Step.StepOptionsButton[] = [
|
const defaultButtons: Shepherd.Step.StepOptionsButton[] = [
|
||||||
{
|
{
|
||||||
classes: "btn",
|
classes: "btn",
|
||||||
|
@ -120,9 +106,7 @@ function startTour(data: TourData, runMainAgainAfterComplete: boolean): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark the tour as completed.
|
// Mark the tour as completed.
|
||||||
const completedTours = await fromStorage(StorageKey.ToursCompleted);
|
await addCompletedTour(tourId);
|
||||||
completedTours.value.add(data.id);
|
|
||||||
await completedTours.save();
|
|
||||||
|
|
||||||
if (runMainAgainAfterComplete) {
|
if (runMainAgainAfterComplete) {
|
||||||
await main();
|
await main();
|
||||||
|
@ -131,24 +115,19 @@ function startTour(data: TourData, runMainAgainAfterComplete: boolean): void {
|
||||||
|
|
||||||
// For every step we have, add it to the tour and subsequently add all the
|
// For every step we have, add it to the tour and subsequently add all the
|
||||||
// event handlers to that step.
|
// event handlers to that step.
|
||||||
for (const [stepNumber, stepOptions] of data.steps.entries()) {
|
for (const [stepNumber, stepOptions] of steps.entries()) {
|
||||||
// If the final step doesn't have buttons defined, set the "Continue" button
|
// If the final step doesn't have buttons defined, set the "Continue" button
|
||||||
// text to "Finish".
|
// text to "Finish".
|
||||||
if (
|
if (stepOptions.buttons === undefined && stepNumber + 1 === steps.length) {
|
||||||
stepOptions.buttons === undefined &&
|
|
||||||
stepNumber + 1 === data.steps.length
|
|
||||||
) {
|
|
||||||
stepOptions.buttons = [...defaultButtons];
|
stepOptions.buttons = [...defaultButtons];
|
||||||
stepOptions.buttons[0].text = "Finish";
|
stepOptions.buttons[0].text = "Finish";
|
||||||
}
|
}
|
||||||
|
|
||||||
const step = tour.addStep(stepOptions);
|
const step = tour.addStep(stepOptions);
|
||||||
|
|
||||||
for (const {stepId, eventHandlers} of data.eventHandlers) {
|
for (const [targetStepId, [eventName, eventHandler]] of eventHandlers) {
|
||||||
if (stepId === step.id) {
|
if (targetStepId === step.id) {
|
||||||
for (const {event, handler} of eventHandlers) {
|
step.on(eventName, eventHandler);
|
||||||
step.on(event, handler);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,59 @@
|
||||||
import {Component} from "preact";
|
import {Component, type JSX} from "preact";
|
||||||
import {type TourData} from "../../tours/exports.js";
|
import {TourId} from "../../tours/exports.js";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
hasBeenCompleted: boolean;
|
hasBeenCompleted: boolean;
|
||||||
tour: TourData;
|
name: string;
|
||||||
|
tourId: TourId;
|
||||||
};
|
};
|
||||||
|
|
||||||
function tourLink(tour: TourData): string {
|
function tourDescription(tourId: Props["tourId"]): JSX.Element {
|
||||||
const anchor = `#tildes-shepherd-tour=${tour.id}`;
|
if (tourId === TourId.Introduction) {
|
||||||
|
return (
|
||||||
|
<p class="tour-description">
|
||||||
|
A short introduction to Tildes Shepherd and how the tours work. Normally
|
||||||
|
this is automatically shown when you first installed the extension.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tourId === TourId.InterfaceHomepage) {
|
||||||
|
return (
|
||||||
|
<p class="tour-description">
|
||||||
|
Let's take a look at the home page and all we can do there.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p class="tour-description">
|
||||||
|
Tour ID "{tourId}" does not have a description, this should probably be
|
||||||
|
fixed!
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tourLink(tourId: Props["tourId"]): string {
|
||||||
|
const anchor = `#tildes-shepherd-tour=${tourId}`;
|
||||||
const baseUrl = "https://tildes.net";
|
const baseUrl = "https://tildes.net";
|
||||||
const path = tour.requirements.path;
|
let path = "";
|
||||||
|
|
||||||
|
switch (tourId) {
|
||||||
|
case TourId.InterfaceHomepage:
|
||||||
|
case TourId.Introduction: {
|
||||||
|
path = "/";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
return `${baseUrl}${path}${anchor}`;
|
return `${baseUrl}${path}${anchor}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Tour extends Component<Props> {
|
export class Tour extends Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const {hasBeenCompleted, tour} = this.props;
|
const {hasBeenCompleted, name, tourId} = this.props;
|
||||||
const classes = ["tour", hasBeenCompleted ? "completed" : ""].join(" ");
|
const classes = ["tour", hasBeenCompleted ? "completed" : ""].join(" ");
|
||||||
const completed = hasBeenCompleted ? (
|
const completed = hasBeenCompleted ? (
|
||||||
<p class="tour-completed" title="You've completed this tour before!">
|
<p class="tour-completed" title="You've completed this tour before!">
|
||||||
|
@ -26,11 +63,11 @@ export class Tour extends Component<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={classes.trim()}>
|
<div class={classes.trim()}>
|
||||||
<h3>{tour.title}</h3>
|
<h3>{name}</h3>
|
||||||
{completed}
|
{completed}
|
||||||
{tour.description}
|
{tourDescription(tourId)}
|
||||||
<p class="tour-link">
|
<p class="tour-link">
|
||||||
<a href={tourLink(tour)}>Take this tour</a>
|
<a href={tourLink(tourId)}>Take this tour</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import {Component, type JSX} from "preact";
|
import {Component, type JSX} from "preact";
|
||||||
import {
|
import {createToursCompleted} from "../../storage/common.js";
|
||||||
fromStorage,
|
import {TourId} from "../../tours/exports.js";
|
||||||
StorageKey,
|
|
||||||
type StorageValues,
|
|
||||||
} from "../../storage/common.js";
|
|
||||||
import {allTours} from "../../tours/exports.js";
|
|
||||||
import {Tour} from "./tour.js";
|
import {Tour} from "./tour.js";
|
||||||
|
|
||||||
type Props = Record<string, unknown>;
|
type Props = Record<string, unknown>;
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
toursCompleted: Awaited<StorageValues[StorageKey.ToursCompleted]>;
|
toursCompleted: Awaited<ReturnType<typeof createToursCompleted>>["value"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Tours extends Component<Props, State> {
|
export class Tours extends Component<Props, State> {
|
||||||
|
@ -18,29 +14,39 @@ export class Tours extends Component<Props, State> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
toursCompleted: undefined!,
|
toursCompleted: new Set(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount(): Promise<void> {
|
async componentDidMount(): Promise<void> {
|
||||||
const toursCompleted = await fromStorage(StorageKey.ToursCompleted);
|
const toursCompleted = await createToursCompleted();
|
||||||
this.setState({toursCompleted});
|
this.setState({toursCompleted: toursCompleted.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
const {toursCompleted} = this.state;
|
const {toursCompleted} = this.state;
|
||||||
if (toursCompleted === undefined) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tours = allTours.map((tour) => (
|
const createTour = (tourId: TourId, name: string): Tour["props"] => {
|
||||||
<Tour hasBeenCompleted={toursCompleted.value.has(tour.id)} tour={tour} />
|
return {
|
||||||
));
|
hasBeenCompleted: toursCompleted.has(tourId),
|
||||||
|
name,
|
||||||
|
tourId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const tourProps: Array<Tour["props"]> = [
|
||||||
|
createTour(TourId.Introduction, "Introduction"),
|
||||||
|
createTour(TourId.InterfaceHomepage, "The Homepage"),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<h2>Tours</h2>
|
<h2>Tours</h2>
|
||||||
<div class="tours">{tours}</div>
|
<div class="tours">
|
||||||
|
{tourProps.map((props) => (
|
||||||
|
<Tour {...props} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,39 +2,33 @@ import browser from "webextension-polyfill";
|
||||||
import {createValue} from "@holllo/webextension-storage";
|
import {createValue} from "@holllo/webextension-storage";
|
||||||
import {type TourId} from "../tours/exports.js";
|
import {type TourId} from "../tours/exports.js";
|
||||||
|
|
||||||
/** All available storage keys. */
|
|
||||||
export enum StorageKey {
|
export enum StorageKey {
|
||||||
IntroductionUnderstood = "introduction-understood",
|
IntroductionUnderstood = "introduction-understood",
|
||||||
ToursCompleted = "tours-completed",
|
ToursCompleted = "tours-completed",
|
||||||
}
|
}
|
||||||
|
|
||||||
/** All values we want to save in storage. */
|
export async function createIntroductionUnderstood() {
|
||||||
const storageValues = {
|
return createValue<boolean>({
|
||||||
[StorageKey.IntroductionUnderstood]: createValue<boolean>({
|
|
||||||
deserialize: (input) => input === "true",
|
deserialize: (input) => input === "true",
|
||||||
serialize: (input) => JSON.stringify(input),
|
serialize: (input) => JSON.stringify(input),
|
||||||
key: StorageKey.IntroductionUnderstood,
|
key: StorageKey.IntroductionUnderstood,
|
||||||
storage: browser.storage.local,
|
storage: browser.storage.local,
|
||||||
value: false,
|
value: false,
|
||||||
}),
|
});
|
||||||
[StorageKey.ToursCompleted]: createValue<Set<TourId>>({
|
}
|
||||||
|
|
||||||
|
export async function createToursCompleted() {
|
||||||
|
return createValue<Set<TourId>>({
|
||||||
deserialize: (input) => new Set(JSON.parse(input) as TourId[]),
|
deserialize: (input) => new Set(JSON.parse(input) as TourId[]),
|
||||||
serialize: (input) => JSON.stringify(Array.from(input)),
|
serialize: (input) => JSON.stringify(Array.from(input)),
|
||||||
key: StorageKey.ToursCompleted,
|
key: StorageKey.ToursCompleted,
|
||||||
storage: browser.storage.local,
|
storage: browser.storage.local,
|
||||||
value: new Set([]),
|
value: new Set([]),
|
||||||
}),
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
/** Alias to get the inferred type shape of {@link storageValues}. */
|
export async function addCompletedTour(tourId: TourId): Promise<void> {
|
||||||
export type StorageValues = typeof storageValues;
|
const toursCompleted = await createToursCompleted();
|
||||||
|
toursCompleted.value.add(tourId);
|
||||||
/**
|
await toursCompleted.save();
|
||||||
* Get the stored value for a given key.
|
|
||||||
* @param key The key to get from storage.
|
|
||||||
*/
|
|
||||||
export async function fromStorage<K extends StorageKey>(
|
|
||||||
key: K,
|
|
||||||
): Promise<StorageValues[K]> {
|
|
||||||
return storageValues[key];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import {accountSettingsTour, homepageTour} from "./interface/exports.js";
|
import {homepageEventHandlers, homepageSteps} from "./interface/exports.js";
|
||||||
import {introductionTour} from "./introduction.js";
|
import {introductionSteps} from "./introduction.js";
|
||||||
import {type TourData} from "./types.js";
|
|
||||||
|
|
||||||
export * from "./introduction.js";
|
export enum TourId {
|
||||||
export * from "./shared/exports.js";
|
InterfaceHomepage = "interface-homepage",
|
||||||
export * from "./types.js";
|
Introduction = "introduction",
|
||||||
|
}
|
||||||
|
|
||||||
/** All tours available in a single array. */
|
export const tourIdsAndSteps: Array<
|
||||||
export const allTours: TourData[] = [
|
[TourId, TourStepOptions[], TourStepEventHandler[]]
|
||||||
introductionTour,
|
> = [
|
||||||
accountSettingsTour,
|
[TourId.Introduction, introductionSteps, []],
|
||||||
homepageTour,
|
[TourId.InterfaceHomepage, homepageSteps, homepageEventHandlers],
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import {type TourData, TourId} from "../types.js";
|
|
||||||
import {renderInContainer} from "../utilities.js";
|
|
||||||
|
|
||||||
const step01 = renderInContainer(
|
|
||||||
<>
|
|
||||||
<h1>Your Account Settings</h1>
|
|
||||||
</>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const steps: TourData["steps"] = [
|
|
||||||
{
|
|
||||||
id: "account-settings-01",
|
|
||||||
text: step01,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const eventHandlers: TourData["eventHandlers"] = [];
|
|
||||||
|
|
||||||
const requirements: TourData["requirements"] = {
|
|
||||||
mustBeLoggedIn: true,
|
|
||||||
path: "/settings",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const accountSettingsTour: TourData = {
|
|
||||||
id: TourId.InterfaceAccountSettings,
|
|
||||||
title: "Your Account Settings",
|
|
||||||
description: "View your account settings and all that you can customize.",
|
|
||||||
displayInOptionsPage: true,
|
|
||||||
eventHandlers,
|
|
||||||
requirements,
|
|
||||||
steps,
|
|
||||||
};
|
|
|
@ -1,2 +1,4 @@
|
||||||
export * from "./account-settings.js";
|
export {
|
||||||
export * from "./homepage.js";
|
eventHandlers as homepageEventHandlers,
|
||||||
|
steps as homepageSteps,
|
||||||
|
} from "./homepage.js";
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import {LoggedOutWarning} from "../shared/logged-out-warning.js";
|
import type Shepherd from "shepherd.js";
|
||||||
import {type TourData, TourId} from "../types.js";
|
|
||||||
import {
|
import {
|
||||||
addDatasetCounter,
|
addDatasetCounter,
|
||||||
encapsulateElements,
|
encapsulateElements,
|
||||||
removeAllDatasetCounters,
|
removeAllDatasetCounters,
|
||||||
renderInContainer,
|
renderInContainer,
|
||||||
} from "../utilities.js";
|
} from "../utilities.js";
|
||||||
|
import {LoggedOutWarning} from "../shared/logged-out-warning.js";
|
||||||
|
|
||||||
const step01 = renderInContainer(
|
const step01 = renderInContainer(
|
||||||
<>
|
<>
|
||||||
|
@ -345,7 +345,7 @@ const step10 = renderInContainer(
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const steps: TourData["steps"] = [
|
export const steps: Shepherd.Step.StepOptions[] = [
|
||||||
{
|
{
|
||||||
id: "homepage-01",
|
id: "homepage-01",
|
||||||
text: step01,
|
text: step01,
|
||||||
|
@ -433,101 +433,79 @@ const steps: TourData["steps"] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const eventHandlers: TourData["eventHandlers"] = [
|
export const eventHandlers: TourStepEventHandler[] = [
|
||||||
{
|
[
|
||||||
stepId: "homepage-04",
|
"homepage-04",
|
||||||
eventHandlers: [
|
[
|
||||||
{
|
"show",
|
||||||
event: "show",
|
() => {
|
||||||
handler() {
|
const topic = ".topic-listing > li:first-child";
|
||||||
const topic = ".topic-listing > li:first-child";
|
const counters = [
|
||||||
const counters = [
|
".topic-title",
|
||||||
".topic-title",
|
".topic-metadata",
|
||||||
".topic-metadata",
|
".topic-info-comments",
|
||||||
".topic-info-comments",
|
".topic-info-source",
|
||||||
".topic-info-source",
|
"time",
|
||||||
"time",
|
".topic-voting",
|
||||||
".topic-voting",
|
".topic-actions",
|
||||||
".topic-actions",
|
];
|
||||||
];
|
|
||||||
|
|
||||||
for (const [count, selector] of counters.entries()) {
|
for (const [count, selector] of counters.entries()) {
|
||||||
addDatasetCounter(`${topic} ${selector}`, count + 1);
|
addDatasetCounter(`${topic} ${selector}`, count + 1);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
],
|
||||||
{
|
[
|
||||||
stepId: "homepage-05",
|
"homepage-05",
|
||||||
eventHandlers: [
|
[
|
||||||
{
|
"destroy",
|
||||||
event: "destroy",
|
() => {
|
||||||
handler() {
|
removeAllDatasetCounters();
|
||||||
removeAllDatasetCounters();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "show",
|
|
||||||
handler() {
|
|
||||||
encapsulateElements(
|
|
||||||
"homepage-06",
|
|
||||||
"#sidebar .sidebar-controls",
|
|
||||||
"afterend",
|
|
||||||
["#sidebar .form-search", "#sidebar h2", "#sidebar p"],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
],
|
||||||
{
|
[
|
||||||
stepId: "homepage-06",
|
"homepage-05",
|
||||||
eventHandlers: [
|
[
|
||||||
{
|
"show",
|
||||||
event: "show",
|
() => {
|
||||||
handler() {
|
encapsulateElements(
|
||||||
encapsulateElements(
|
"homepage-06",
|
||||||
"homepage-07",
|
"#sidebar .sidebar-controls",
|
||||||
"#sidebar .divider",
|
"afterend",
|
||||||
"beforebegin",
|
["#sidebar .form-search", "#sidebar h2", "#sidebar p"],
|
||||||
["#sidebar .nav", '#sidebar [href="/groups"'],
|
);
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
],
|
||||||
{
|
[
|
||||||
stepId: "homepage-08",
|
"homepage-06",
|
||||||
eventHandlers: [
|
[
|
||||||
{
|
"show",
|
||||||
event: "show",
|
() => {
|
||||||
handler() {
|
encapsulateElements("homepage-07", "#sidebar .divider", "beforebegin", [
|
||||||
const filteredTags =
|
"#sidebar .nav",
|
||||||
document.querySelector<HTMLDetailsElement>("#sidebar details") ??
|
'#sidebar [href="/groups"',
|
||||||
undefined;
|
]);
|
||||||
if (filteredTags === undefined) {
|
},
|
||||||
console.warn("Element is unexpectedly undefined");
|
],
|
||||||
return;
|
],
|
||||||
}
|
[
|
||||||
|
"homepage-08",
|
||||||
|
[
|
||||||
|
"show",
|
||||||
|
() => {
|
||||||
|
const filteredTags =
|
||||||
|
document.querySelector<HTMLDetailsElement>("#sidebar details") ??
|
||||||
|
undefined;
|
||||||
|
if (filteredTags === undefined) {
|
||||||
|
console.warn("Element is unexpectedly undefined");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
filteredTags.open = true;
|
filteredTags.open = true;
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
const requirements: TourData["requirements"] = {
|
|
||||||
mustBeLoggedIn: false,
|
|
||||||
path: "/",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const homepageTour: TourData = {
|
|
||||||
id: TourId.InterfaceHomepage,
|
|
||||||
title: "The Tildes Homepage",
|
|
||||||
description: "Let's take a look at the home page and all we can do there.",
|
|
||||||
displayInOptionsPage: true,
|
|
||||||
eventHandlers,
|
|
||||||
requirements,
|
|
||||||
steps,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {fromStorage, StorageKey} from "../storage/common.js";
|
import type Shepherd from "shepherd.js";
|
||||||
import {TourId, type TourData} from "./types.js";
|
import {createIntroductionUnderstood} from "../storage/common.js";
|
||||||
import {openOptionsPageFromBackground, renderInContainer} from "./utilities.js";
|
import {openOptionsPageFromBackground, renderInContainer} from "./utilities.js";
|
||||||
|
|
||||||
const step01 = renderInContainer(
|
const step01 = renderInContainer(
|
||||||
|
@ -82,7 +82,7 @@ const step03 = renderInContainer(
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const steps: TourData["steps"] = [
|
export const introductionSteps: Shepherd.Step.StepOptions[] = [
|
||||||
{
|
{
|
||||||
canClickTarget: false,
|
canClickTarget: false,
|
||||||
id: "introduction-01",
|
id: "introduction-01",
|
||||||
|
@ -103,9 +103,7 @@ const steps: TourData["steps"] = [
|
||||||
classes: "btn",
|
classes: "btn",
|
||||||
text: "I understand",
|
text: "I understand",
|
||||||
async action() {
|
async action() {
|
||||||
const introductionUnderstood = await fromStorage(
|
const introductionUnderstood = await createIntroductionUnderstood();
|
||||||
StorageKey.IntroductionUnderstood,
|
|
||||||
);
|
|
||||||
introductionUnderstood.value = true;
|
introductionUnderstood.value = true;
|
||||||
await introductionUnderstood.save();
|
await introductionUnderstood.save();
|
||||||
this.complete();
|
this.complete();
|
||||||
|
@ -124,21 +122,3 @@ const steps: TourData["steps"] = [
|
||||||
text: step03,
|
text: step03,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const eventHandlers: TourData["eventHandlers"] = [];
|
|
||||||
|
|
||||||
const requirements: TourData["requirements"] = {
|
|
||||||
mustBeLoggedIn: false,
|
|
||||||
path: "/",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const introductionTour: TourData = {
|
|
||||||
id: TourId.Introduction,
|
|
||||||
title: "Tildes Shepherd Introduction",
|
|
||||||
description:
|
|
||||||
"A short introduction to Tildes Shepherd and how the tours work.",
|
|
||||||
displayInOptionsPage: true,
|
|
||||||
eventHandlers,
|
|
||||||
requirements,
|
|
||||||
steps,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
export * from "./logged-out-warning.js";
|
export * from "./logged-out-warning.js";
|
||||||
export * from "./tour-error.js";
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {type JSX} from "preact";
|
import {type JSX} from "preact";
|
||||||
|
|
||||||
/** Check if the user is logged in and return a warning element if they aren't. */
|
|
||||||
export function LoggedOutWarning(): JSX.Element {
|
export function LoggedOutWarning(): JSX.Element {
|
||||||
const userIsLoggedIn =
|
const userIsLoggedIn =
|
||||||
document.querySelector(".logged-in-user-username") !== null;
|
document.querySelector(".logged-in-user-username") !== null;
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import Shepherd from "shepherd.js";
|
|
||||||
import {renderInContainer} from "../utilities.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start an ad-hoc tour to display an error message.
|
|
||||||
* @param text The message to show.
|
|
||||||
*/
|
|
||||||
export function showTourError(text: string) {
|
|
||||||
const tour = new Shepherd.Tour({
|
|
||||||
defaultStepOptions: {
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
classes: "btn",
|
|
||||||
text: "Continue",
|
|
||||||
action() {
|
|
||||||
this.complete();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
useModalOverlay: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
tour.addStep({
|
|
||||||
text: renderInContainer(<p class="tish-warning">{text}</p>),
|
|
||||||
});
|
|
||||||
|
|
||||||
tour.start();
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
import type Shepherd from "shepherd.js";
|
|
||||||
|
|
||||||
/** All available tour IDs. */
|
|
||||||
export enum TourId {
|
|
||||||
InterfaceAccountSettings = "interface-account-settings",
|
|
||||||
InterfaceHomepage = "interface-homepage",
|
|
||||||
Introduction = "introduction",
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Requirements of a tour to be checked before the tour is started. */
|
|
||||||
export type TourRequirement = {
|
|
||||||
/**
|
|
||||||
* This tour requires that the user must be logged in. Only set this to true
|
|
||||||
* if the tour goes to pages only accessible by logged in users.
|
|
||||||
*/
|
|
||||||
mustBeLoggedIn: boolean;
|
|
||||||
|
|
||||||
/** The {@link URL.pathname} to run the tour at. */
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** An individual tour step event handler. */
|
|
||||||
export type TourStepEvent = {
|
|
||||||
/**
|
|
||||||
* - The "show" event will be called when the step is displayed.
|
|
||||||
* - The "destroy" event will be called when the step is finished.
|
|
||||||
*/
|
|
||||||
event: "show" | "destroy";
|
|
||||||
/** The handler for this step event. */
|
|
||||||
handler: Parameters<Shepherd.Step["on"]>[1];
|
|
||||||
};
|
|
||||||
|
|
||||||
/** All the tour data collected in one place. */
|
|
||||||
export type TourData = {
|
|
||||||
/** A short description of the tour for use in the options page. */
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
/** Whether this tour should be shown in the options page. */
|
|
||||||
displayInOptionsPage: boolean;
|
|
||||||
|
|
||||||
/** All event handlers to be added to this tour's steps. */
|
|
||||||
eventHandlers: Array<{
|
|
||||||
eventHandlers: TourStepEvent[];
|
|
||||||
stepId: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
/** The unique ID for this tour. */
|
|
||||||
id: TourId;
|
|
||||||
|
|
||||||
/** The requirements this tour must match before starting it. */
|
|
||||||
requirements: TourRequirement;
|
|
||||||
|
|
||||||
/** All the steps this tour will take. */
|
|
||||||
steps: Shepherd.Step.StepOptions[];
|
|
||||||
|
|
||||||
/** The title of the tour for use in the options page. */
|
|
||||||
title: string;
|
|
||||||
};
|
|
|
@ -5,7 +5,7 @@ import browser from "webextension-polyfill";
|
||||||
* Adds a `[data-tildes-shepherd-counter]` attribute to a specified element. For
|
* Adds a `[data-tildes-shepherd-counter]` attribute to a specified element. For
|
||||||
* the associated CSS, see `source/content-scripts/scss/main.scss`.
|
* the associated CSS, see `source/content-scripts/scss/main.scss`.
|
||||||
*
|
*
|
||||||
* @param selector The selector of the element to apply the counter to, if the
|
* @param selector The selector of element to apply the counter to, if the
|
||||||
* target element can't be selected an error will be thrown.
|
* target element can't be selected an error will be thrown.
|
||||||
* @param count The number to display in the counter.
|
* @param count The number to display in the counter.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
export {};
|
import type Shepherd from "shepherd.js";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
const $browser: "chromium" | "firefox";
|
const $browser: "chromium" | "firefox";
|
||||||
const $dev: boolean;
|
const $dev: boolean;
|
||||||
const $test: boolean;
|
const $test: boolean;
|
||||||
|
|
||||||
|
type TourStepEvent = "show" | "destroy";
|
||||||
|
type TourStepEventFunction = Parameters<Shepherd.Step["on"]>[1];
|
||||||
|
type TourStepEventHandler = [string, [TourStepEvent, TourStepEventFunction]];
|
||||||
|
type TourStepOptions = Shepherd.Step.StepOptions;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue