From 5c3d61679cdfea28bf8ac41e9550c2a4f88d523d Mon Sep 17 00:00:00 2001 From: Bauke Date: Thu, 15 Jun 2023 14:55:30 +0200 Subject: [PATCH] Add the content scripts and styling. --- source/content-scripts/scss/main.scss | 22 ++++ .../content-scripts/scss/shepherd-custom.scss | 71 +++++++++++ .../scss/shepherd-defaults.scss | 53 ++++++++ source/content-scripts/setup.ts | 113 ++++++++++++++++++ 4 files changed, 259 insertions(+) create mode 100644 source/content-scripts/scss/main.scss create mode 100644 source/content-scripts/scss/shepherd-custom.scss create mode 100644 source/content-scripts/scss/shepherd-defaults.scss create mode 100644 source/content-scripts/setup.ts diff --git a/source/content-scripts/scss/main.scss b/source/content-scripts/scss/main.scss new file mode 100644 index 0000000..55c3325 --- /dev/null +++ b/source/content-scripts/scss/main.scss @@ -0,0 +1,22 @@ +@use "shepherd-defaults"; +@use "shepherd-custom"; + +[data-tildes-shepherd-counter] { + position: relative; + + &::before { + align-items: center; + border: 1px solid var(--alert-color); + border-radius: 100%; + color: var(--alert-color); + content: attr(data-tildes-shepherd-counter); + display: inline-flex; + font-family: sans-serif; + font-size: 0.8rem; + font-weight: normal; + height: 1rem; + justify-content: center; + position: relative; + width: 1rem; + } +} diff --git a/source/content-scripts/scss/shepherd-custom.scss b/source/content-scripts/scss/shepherd-custom.scss new file mode 100644 index 0000000..1b3c3c5 --- /dev/null +++ b/source/content-scripts/scss/shepherd-custom.scss @@ -0,0 +1,71 @@ +// Extra styles for Shepherd's elements and all the elements we use inside. + +.shepherd-element { + background-color: var(--background-primary-color); + border: 1px dashed var(--foreground-primary-color); + max-width: 600px; + + // For the data-popper-placement styling, since the element here has + // `position: absolute` with `top` and `left` set, margin-right and + // margin-bottom will have no effect. So to push them away slightly from the + // highlighted element, use margin-top and margin-left with negative margins. + &[data-popper-placement="bottom"] { + margin-top: 8px; + } + + &[data-popper-placement="top"] { + margin-top: -8px; + } + + &[data-popper-placement="left-start"] { + margin-left: -8px; + } + + a { + text-decoration: underline; + } + + ol, + ul { + margin-left: 1rem; + + &:not(:last-child) { + margin-bottom: 0.4rem; + } + } + + ol { + list-style: decimal; + } + + ul { + list-style: disc; + } + + .highlight-red { + color: var(--error-color); + } + + .highlight-green { + color: var(--success-color); + } +} + +.shepherd-text { + color: var(--foreground-primary-color); + margin-bottom: 0.4rem; +} + +.shepherd-button { + margin-right: 0.4rem; + + &:last-child { + margin-right: 0; + } +} + +.shepherd-target:not(body) { + border: 2px solid var(--alert-color); + margin: 8px; + padding: 8px !important; +} diff --git a/source/content-scripts/scss/shepherd-defaults.scss b/source/content-scripts/scss/shepherd-defaults.scss new file mode 100644 index 0000000..8aeec13 --- /dev/null +++ b/source/content-scripts/scss/shepherd-defaults.scss @@ -0,0 +1,53 @@ +// Styles copied almost verbatim from Shepherd.js's default CSS. +// Since we have the Tildes CSS available in the content scripts anyway, we can +// use its classes for a lot of what we need. So instead of importing the full +// default styling from Shepherd.js and then overriding everything, only copy +// what we want and trim the rest. + +.shepherd-modal-overlay-container { + height: 0; + left: 0; + opacity: 0; + overflow: hidden; + pointer-events: none; + position: fixed; + top: 0; + transition: all 0.3s ease-out, height 0ms 0.3s, opacity 0.3s 0ms; + width: 100vw; + z-index: 9997; + + &.shepherd-modal-is-visible { + height: 100vh; + opacity: 0.5; + transform: translateZ(0); + transition: all 0.3s ease-out, height 0s 0s, opacity 0.3s 0s; + + path { + pointer-events: all; + } + } +} + +.shepherd-element { + opacity: 0; + outline: none; + padding: 16px; + transition: opacity 0.3s, visibility 0.3s; + visibility: hidden; + width: 100%; + z-index: 9999; + + &.shepherd-enabled { + opacity: 1; + visibility: visible; + } +} + +.shepherd-target { + &.shepherd-target-click-disabled { + &, + & * { + pointer-events: none; + } + } +} diff --git a/source/content-scripts/setup.ts b/source/content-scripts/setup.ts new file mode 100644 index 0000000..ffcb5f2 --- /dev/null +++ b/source/content-scripts/setup.ts @@ -0,0 +1,113 @@ +import Shepherd from "shepherd.js"; +import { + addCompletedTour, + createIntroductionUnderstood, +} from "../storage/common.js"; +import {introductionSteps} from "../tours/introduction.js"; +import {tourIdsAndSteps} from "../tours/exports.js"; + +/** The main entry point for the content script. */ +async function main(): Promise { + // Automatically start the introduction tour if the person hasn't already been + // through it and only when on the Tildes homepage. + const introductionUnderstood = await createIntroductionUnderstood(); + if (!introductionUnderstood.value && window.location.pathname === "/") { + startTour("introduction", introductionSteps, []); + return; + } + + // 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="; + if (!anchor.startsWith(prefix)) { + return; + } + + // Get the tour ID from the anchor by removing the prefix. + const anchorTourId = anchor.slice(prefix.length); + + // Then run through all of the tours we have and start the first match for the + // ID. + for (const [id, steps, eventHandlers] of tourIdsAndSteps) { + if (anchorTourId === id) { + startTour(id, steps, eventHandlers); + 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. + */ +function startTour( + tourId: TourId, + steps: TourStepOptions[], + eventHandlers: TourStepEventHandler[], +): void { + const tour = new Shepherd.Tour({ + defaultStepOptions: { + buttons: [ + { + classes: "btn", + text: "Continue", + action() { + this.next(); + }, + }, + { + classes: "btn", + text: "Back", + action() { + this.back(); + }, + }, + { + classes: "btn", + text: "Exit", + action() { + this.complete(); + }, + }, + ], + }, + 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(mockSelector); + for (const element of Array.from(mockElements)) { + element.remove(); + } + + // Mark the tour as completed. + await addCompletedTour(tourId); + }); + + // For every step we have, add it to the tour and subsequently add all the + // event handlers to that step. + for (const stepOptions of steps) { + const step = tour.addStep(stepOptions); + + for (const [targetStepId, [eventName, eventHandler]] of eventHandlers) { + if (targetStepId === step.id) { + step.on(eventName, eventHandler); + } + } + } + + // Pull the lever, Kronk! + tour.start(); +} + +document.addEventListener("DOMContentLoaded", main);