@@ -51,6 +51,10 @@ const defaultHtmlCode = `<style>
51
51
` ;
52
52
53
53
const GALLERY_BASE = __GALLERY_BASE_URL__ ;
54
+ const cesiumVersion = __CESIUM_VERSION__ ;
55
+ const versionString = __COMMIT_SHA__
56
+ ? `Commit: ${ __COMMIT_SHA__ . substring ( 0 , 7 ) } - ${ cesiumVersion } `
57
+ : cesiumVersion ;
54
58
55
59
type RightSideRef = {
56
60
toggleExpanded : ( ) => void ;
@@ -164,6 +168,7 @@ function AppBarButton({
164
168
165
169
export type SandcastleAction =
166
170
| { type : "reset" }
171
+ | { type : "resetDirty" }
167
172
| { type : "setCode" ; code : string }
168
173
| { type : "setHtml" ; html : string }
169
174
| { type : "runSandcastle" }
@@ -178,9 +183,6 @@ function App() {
178
183
const consoleCollapsedHeight = 26 ;
179
184
const [ consoleExpanded , setConsoleExpanded ] = useState ( false ) ;
180
185
181
- const cesiumVersion = __CESIUM_VERSION__ ;
182
- const versionString = __COMMIT_SHA__ ? `Commit: ${ __COMMIT_SHA__ } ` : "" ;
183
-
184
186
const startOnEditor = ! ! ( window . location . search || window . location . hash ) ;
185
187
const [ leftPanel , setLeftPanel ] = useState < "editor" | "gallery" > (
186
188
startOnEditor ? "editor" : "gallery" ,
@@ -196,6 +198,7 @@ function App() {
196
198
committedCode : string ;
197
199
committedHtml : string ;
198
200
runNumber : number ;
201
+ dirty : boolean ;
199
202
} ;
200
203
201
204
const initialState : CodeState = {
@@ -204,6 +207,7 @@ function App() {
204
207
committedCode : defaultJsCode ,
205
208
committedHtml : defaultHtmlCode ,
206
209
runNumber : 0 ,
210
+ dirty : false ,
207
211
} ;
208
212
209
213
const [ codeState , dispatch ] = useReducer ( function reducer (
@@ -218,12 +222,14 @@ function App() {
218
222
return {
219
223
...state ,
220
224
code : action . code ,
225
+ dirty : true ,
221
226
} ;
222
227
}
223
228
case "setHtml" : {
224
229
return {
225
230
...state ,
226
231
html : action . html ,
232
+ dirty : true ,
227
233
} ;
228
234
}
229
235
case "runSandcastle" : {
@@ -241,11 +247,46 @@ function App() {
241
247
committedCode : action . code ?? state . code ,
242
248
committedHtml : action . html ?? state . html ,
243
249
runNumber : state . runNumber + 1 ,
250
+ dirty : false ,
251
+ } ;
252
+ }
253
+ case "resetDirty" : {
254
+ return {
255
+ ...state ,
256
+ dirty : false ,
244
257
} ;
245
258
}
246
259
}
247
260
} , initialState ) ;
248
261
262
+ useEffect ( ( ) => {
263
+ const host = window . location . host ;
264
+ let envString = "" ;
265
+ if ( host . includes ( "localhost" ) && host !== "localhost:8080" ) {
266
+ // this helps differentiate tabs for local sandcastle development or other testing
267
+ envString = `${ host . replace ( "localhost:" , "" ) } ` ;
268
+ }
269
+
270
+ const dirtyIndicator = codeState . dirty ? "*" : "" ;
271
+ if ( title === "" || title === "New Sandcastle" ) {
272
+ // No need to clutter the window/tab with a name if not viewing a named gallery demo
273
+ document . title = `${ envString } Sandcastle${ dirtyIndicator } | CesiumJS` ;
274
+ } else {
275
+ document . title = `${ envString } ${ title } ${ dirtyIndicator } | Sandcastle | CesiumJS` ;
276
+ }
277
+ } , [ title , codeState . dirty ] ) ;
278
+
279
+ const confirmLeave = useCallback ( ( ) => {
280
+ if ( ! codeState . dirty ) {
281
+ return true ;
282
+ }
283
+
284
+ /* eslint-disable-next-line no-alert */
285
+ return window . confirm (
286
+ "You have unsaved changes. Are you sure you want to navigate away from this demo?" ,
287
+ ) ;
288
+ } , [ codeState . dirty ] ) ;
289
+
249
290
const [ legacyIdMap , setLegacyIdMap ] = useState < Record < string , string > > ( { } ) ;
250
291
const [ galleryItems , setGalleryItems ] = useState < GalleryItem [ ] > ( [ ] ) ;
251
292
const [ galleryLoaded , setGalleryLoaded ] = useState ( false ) ;
@@ -280,6 +321,9 @@ function App() {
280
321
}
281
322
282
323
function resetSandcastle ( ) {
324
+ if ( ! confirmLeave ( ) ) {
325
+ return ;
326
+ }
283
327
dispatch ( { type : "reset" } ) ;
284
328
285
329
window . history . pushState ( { } , "" , getBaseUrl ( ) ) ;
@@ -295,6 +339,7 @@ function App() {
295
339
296
340
const shareUrl = `${ getBaseUrl ( ) } #c=${ base64String } ` ;
297
341
window . history . replaceState ( { } , "" , shareUrl ) ;
342
+ dispatch ( { type : "resetDirty" } ) ;
298
343
}
299
344
300
345
function openStandalone ( ) {
@@ -430,12 +475,27 @@ function App() {
430
475
useEffect ( ( ) => {
431
476
// Listen to browser forward/back navigation and try to load from URL
432
477
// this is necessary because of the pushState used for faster gallery loading
433
- function pushStateListener ( ) {
434
- loadFromUrl ( ) ;
478
+ function popStateListener ( ) {
479
+ if ( confirmLeave ( ) ) {
480
+ loadFromUrl ( ) ;
481
+ }
435
482
}
436
- window . addEventListener ( "popstate" , pushStateListener ) ;
437
- return ( ) => window . removeEventListener ( "popstate" , pushStateListener ) ;
438
- } , [ loadFromUrl ] ) ;
483
+ window . addEventListener ( "popstate" , popStateListener ) ;
484
+ return ( ) => window . removeEventListener ( "popstate" , popStateListener ) ;
485
+ } , [ loadFromUrl , confirmLeave ] ) ;
486
+
487
+ useEffect ( ( ) => {
488
+ // if the code has been edited listen for navigation away and warn
489
+ if ( codeState . dirty ) {
490
+ function beforeUnloadListener ( e : BeforeUnloadEvent ) {
491
+ e . preventDefault ( ) ;
492
+ return "" ; // modern browsers ignore the contents of this string
493
+ }
494
+ window . addEventListener ( "beforeunload" , beforeUnloadListener ) ;
495
+ return ( ) =>
496
+ window . removeEventListener ( "beforeunload" , beforeUnloadListener ) ;
497
+ }
498
+ } , [ codeState . dirty ] ) ;
439
499
440
500
return (
441
501
< Root
@@ -466,8 +526,7 @@ function App() {
466
526
</ Button >
467
527
< div className = "flex-spacer" > </ div >
468
528
< div className = "version" >
469
- { versionString && < pre > { versionString . substring ( 0 , 7 ) } - </ pre > }
470
- < pre > { cesiumVersion } </ pre >
529
+ { versionString && < pre > { versionString } </ pre > }
471
530
</ div >
472
531
</ header >
473
532
< div className = "application-bar" >
@@ -528,6 +587,9 @@ function App() {
528
587
hidden = { leftPanel !== "gallery" }
529
588
galleryItems = { galleryItems }
530
589
loadDemo = { ( item , switchToCode ) => {
590
+ if ( ! confirmLeave ( ) ) {
591
+ return ;
592
+ }
531
593
// Load the gallery item every time it's clicked
532
594
loadGalleryItem ( item . id ) ;
533
595
0 commit comments