@@ -8,7 +8,7 @@ import "prismjs/components/prism-javascript";
88import { CodePreview } from "../components/code-preview.js" ;
99import { CodeEditor } from "../components/code-editor.js" ;
1010import { 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
1414import { 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+
4152function * 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