@sprig-and-prose/tutorial-svelte 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/kit/Tutorial.svelte +101 -1
- package/src/lib/createTutorialLoad.js +1 -0
package/package.json
CHANGED
package/src/kit/Tutorial.svelte
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { page } from '$app/stores';
|
|
4
|
+
|
|
2
5
|
type TutorialData =
|
|
3
6
|
| {
|
|
4
7
|
kind: 'page';
|
|
@@ -6,6 +9,7 @@
|
|
|
6
9
|
segmentTitle?: string;
|
|
7
10
|
html: string;
|
|
8
11
|
nav: { previous?: string; next?: string };
|
|
12
|
+
routeBase?: string;
|
|
9
13
|
}
|
|
10
14
|
| {
|
|
11
15
|
kind: 'index';
|
|
@@ -30,10 +34,104 @@
|
|
|
30
34
|
} else {
|
|
31
35
|
indexItems = [];
|
|
32
36
|
}
|
|
37
|
+
|
|
38
|
+
let tutorialPageElement: HTMLDivElement | null = null;
|
|
39
|
+
let experientialPrevious: string | null = null;
|
|
40
|
+
|
|
41
|
+
function getCurrentPath(): string {
|
|
42
|
+
if (typeof window === 'undefined') return '';
|
|
43
|
+
const url = $page.url;
|
|
44
|
+
return normalizePath(url.pathname + url.search + url.hash);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizePath(p: string): string {
|
|
48
|
+
if (p === '/') return '/';
|
|
49
|
+
return p.replace(/\/$/, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isInternalTutorialPath(p: string, routeBase: string): boolean {
|
|
53
|
+
if (!routeBase) return false;
|
|
54
|
+
const normalized = normalizePath(p);
|
|
55
|
+
const normalizedBase = normalizePath(routeBase);
|
|
56
|
+
return normalized.startsWith(normalizedBase + '/') || normalized === normalizedBase;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleClickCapture(event: MouseEvent, routeBase: string): void {
|
|
60
|
+
if (typeof window === 'undefined') return;
|
|
61
|
+
|
|
62
|
+
if (event.button !== 0) return;
|
|
63
|
+
if (event.defaultPrevented) return;
|
|
64
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
65
|
+
|
|
66
|
+
const anchor = (event.target as HTMLElement)?.closest('a');
|
|
67
|
+
if (!anchor) return;
|
|
68
|
+
|
|
69
|
+
if (anchor.closest('.tutorial-nav')) return;
|
|
70
|
+
|
|
71
|
+
const href = anchor.getAttribute('href');
|
|
72
|
+
if (!href) return;
|
|
73
|
+
|
|
74
|
+
if (anchor.hasAttribute('download')) return;
|
|
75
|
+
if (anchor.getAttribute('target') === '_blank') return;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const currentUrl = new URL(window.location.href);
|
|
79
|
+
const targetUrl = new URL(href, currentUrl);
|
|
80
|
+
|
|
81
|
+
if (targetUrl.origin !== currentUrl.origin) return;
|
|
82
|
+
|
|
83
|
+
const targetPath = normalizePath(targetUrl.pathname + targetUrl.search + targetUrl.hash);
|
|
84
|
+
const currentPath = getCurrentPath();
|
|
85
|
+
|
|
86
|
+
if (targetPath === currentPath && targetUrl.hash) return;
|
|
87
|
+
|
|
88
|
+
if (!isInternalTutorialPath(targetPath, routeBase)) return;
|
|
89
|
+
|
|
90
|
+
const fromPath = currentPath;
|
|
91
|
+
const toPath = targetPath;
|
|
92
|
+
|
|
93
|
+
sessionStorage.setItem(`sprig:tutorial:prev:${toPath}`, fromPath);
|
|
94
|
+
} catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
$: if (data.kind === 'page' && typeof window !== 'undefined' && $page.url) {
|
|
100
|
+
const routeBase = data.routeBase;
|
|
101
|
+
if (routeBase) {
|
|
102
|
+
const currentPath = getCurrentPath();
|
|
103
|
+
const prevPath = sessionStorage.getItem(`sprig:tutorial:prev:${currentPath}`);
|
|
104
|
+
if (prevPath && isInternalTutorialPath(prevPath, routeBase)) {
|
|
105
|
+
experientialPrevious = prevPath;
|
|
106
|
+
} else {
|
|
107
|
+
experientialPrevious = null;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
experientialPrevious = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onMount(() => {
|
|
115
|
+
if (typeof window === 'undefined') return;
|
|
116
|
+
if (data.kind !== 'page' || !data.routeBase) return;
|
|
117
|
+
|
|
118
|
+
const routeBase = data.routeBase;
|
|
119
|
+
const clickHandler = (e: MouseEvent) => handleClickCapture(e, routeBase);
|
|
120
|
+
|
|
121
|
+
if (tutorialPageElement) {
|
|
122
|
+
tutorialPageElement.addEventListener('click', clickHandler);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return () => {
|
|
126
|
+
if (tutorialPageElement) {
|
|
127
|
+
tutorialPageElement.removeEventListener('click', clickHandler);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
});
|
|
33
131
|
</script>
|
|
34
132
|
|
|
35
133
|
{#if data.kind === 'page'}
|
|
36
|
-
<div class="tutorial-page">
|
|
134
|
+
<div class="tutorial-page" bind:this={tutorialPageElement}>
|
|
37
135
|
<h1>{data.title}</h1>
|
|
38
136
|
{#if data.segmentTitle}
|
|
39
137
|
<p class="subtitle">{data.segmentTitle}</p>
|
|
@@ -42,6 +140,8 @@
|
|
|
42
140
|
<nav class="tutorial-nav">
|
|
43
141
|
{#if data.nav.previous}
|
|
44
142
|
<a href={data.nav.previous} class="nav-link nav-previous">Previous</a>
|
|
143
|
+
{:else if experientialPrevious}
|
|
144
|
+
<a href={experientialPrevious} class="nav-link nav-previous">Previous</a>
|
|
45
145
|
{:else}
|
|
46
146
|
<span class="nav-link nav-previous nav-placeholder"></span>
|
|
47
147
|
{/if}
|