Skip to content

Commit 0b23da1

Browse files
committed
Add URL-based code sharing to playground
Implement shareable links for playground code using LZ-String compression. Users can now share their code snippets via URL and load code from shared links. Features: - Load code from URL hash on page load - Auto-update URL hash as code changes (debounced) - Copy Link button with visual feedback - Support for browser back/forward navigation - Maintains localStorage backup for unsaved work The URL hash is updated 1 second after the user stops typing to avoid excessive history pollution. The compressed URLs are significantly shorter than base64-encoded alternatives.
1 parent 377ff44 commit 0b23da1

File tree

1 file changed

+115
-13
lines changed

1 file changed

+115
-13
lines changed

website/src/clients/playground.ts

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import "prismjs/components/prism-javascript";
88
import {CodePreview} from "../components/code-preview.js";
99
import {CodeEditor} from "../components/code-editor.js";
1010
import {extractData} from "../components/serialize-javascript.js";
11-
//import LZString from "lz-string";
11+
import LZString from "lz-string";
1212

1313
// TODO: move this to the ContentAreaElement component
1414
import {ContentAreaElement} from "@b9g/revise/contentarea.js";
@@ -38,16 +38,53 @@ const examples = extractData(
3838
document.getElementById("examples") as HTMLScriptElement,
3939
);
4040

41+
// Helper function to debounce URL hash updates
42+
function debounceHash(callback: () => void, delay: number) {
43+
let timeoutId: number | undefined;
44+
return () => {
45+
if (timeoutId !== undefined) {
46+
clearTimeout(timeoutId);
47+
}
48+
timeoutId = setTimeout(callback, delay) as any;
49+
};
50+
}
51+
4152
function* Playground(this: Context) {
42-
let code = localStorage.getItem("playground-value") || "";
53+
let code = "";
4354
let updateEditor = true;
55+
let copyButtonText = "Copy Link";
56+
57+
// Try to load code from URL hash first
58+
if (window.location.hash) {
59+
try {
60+
const compressed = window.location.hash.slice(1);
61+
const decompressed = LZString.decompressFromEncodedURIComponent(compressed);
62+
if (decompressed) {
63+
code = decompressed;
64+
}
65+
} catch (err) {
66+
console.warn("Failed to decompress code from URL:", err);
67+
}
68+
}
69+
70+
// Fall back to localStorage, then default example
71+
if (!code.trim()) {
72+
code = localStorage.getItem("playground-value") || "";
73+
}
4474
if (!code.trim()) {
4575
code = examples[0].code;
4676
}
4777

78+
// Debounced function to update URL hash
79+
const updateHash = debounceHash(() => {
80+
const compressed = LZString.compressToEncodedURIComponent(code);
81+
window.history.replaceState(null, "", `#${compressed}`);
82+
}, 1000);
83+
4884
this.addEventListener("contentchange", (ev: any) => {
4985
code = ev.target.value;
5086
localStorage.setItem("playground-value", code);
87+
updateHash();
5188
this.refresh();
5289
});
5390

@@ -59,19 +96,52 @@ function* Playground(this: Context) {
5996
);
6097
code = code1;
6198
updateEditor = true;
99+
updateHash();
62100
this.refresh();
63101
};
64102

65-
//const hashchange = (ev: HashChangeEvent) => {
66-
// console.log("hashchange", ev);
67-
// const value1 = LZString.decompressFromEncodedURIComponent("poop");
68-
// console.log(value);
69-
//};
70-
//window.addEventListener("hashchange", hashchange);
71-
//this.cleanup(() => window.removeEventListener("hashchange", hashchange))
72-
//this.flush(() => {
73-
// window.location.hash = LZString.compressToEncodedURIComponent(value);
74-
//});
103+
const onCopyLink = async () => {
104+
const compressed = LZString.compressToEncodedURIComponent(code);
105+
const url = `${window.location.origin}${window.location.pathname}#${compressed}`;
106+
107+
try {
108+
await navigator.clipboard.writeText(url);
109+
copyButtonText = "Copied!";
110+
this.refresh();
111+
setTimeout(() => {
112+
copyButtonText = "Copy Link";
113+
this.refresh();
114+
}, 2000);
115+
} catch (err) {
116+
console.error("Failed to copy to clipboard:", err);
117+
copyButtonText = "Failed";
118+
this.refresh();
119+
setTimeout(() => {
120+
copyButtonText = "Copy Link";
121+
this.refresh();
122+
}, 2000);
123+
}
124+
};
125+
126+
const hashchange = (ev: HashChangeEvent) => {
127+
if (!window.location.hash) {
128+
return;
129+
}
130+
try {
131+
const compressed = window.location.hash.slice(1);
132+
const decompressed = LZString.decompressFromEncodedURIComponent(compressed);
133+
if (decompressed && decompressed !== code) {
134+
code = decompressed;
135+
updateEditor = true;
136+
this.refresh();
137+
}
138+
} catch (err) {
139+
console.warn("Failed to decompress code from URL:", err);
140+
}
141+
};
142+
143+
window.addEventListener("hashchange", hashchange);
144+
this.cleanup(() => window.removeEventListener("hashchange", hashchange));
75145

76146
for ({} of this) {
77147
this.schedule(() => {
@@ -100,11 +170,23 @@ function* Playground(this: Context) {
100170
}
101171
`}>
102172
<${CodeEditorNavbar}>
103-
<div>
173+
<div class=${css`
174+
display: flex;
175+
gap: 1em;
176+
align-items: center;
177+
width: 100%;
178+
`}>
104179
<select
105180
name="Example"
106181
value=${exampleName}
107182
onchange=${onexamplechange}
183+
class=${css`
184+
padding: 0.25em 0.5em;
185+
border: 1px solid var(--coldark3);
186+
border-radius: 4px;
187+
background-color: var(--bg-color);
188+
color: var(--text-color);
189+
`}
108190
>
109191
<option value="">Load an example...</option>
110192
${examples.map(
@@ -113,6 +195,26 @@ function* Playground(this: Context) {
113195
`,
114196
)}
115197
</select>
198+
<button
199+
onclick=${onCopyLink}
200+
class=${css`
201+
padding: 0.25em 0.75em;
202+
border: 1px solid var(--coldark3);
203+
border-radius: 4px;
204+
background-color: var(--bg-color);
205+
color: var(--text-color);
206+
cursor: pointer;
207+
transition: all 0.2s;
208+
&:hover {
209+
background-color: var(--coldark02);
210+
}
211+
&:active {
212+
transform: scale(0.98);
213+
}
214+
`}
215+
>
216+
${copyButtonText}
217+
</button>
116218
</div>
117219
<//CodeEditorNavbar>
118220
<${CodeEditor}

0 commit comments

Comments
 (0)