@@ -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 baseTitle = "Cesium Sandcastle" ;
271
+ const dirtyIndicator = codeState . dirty ? "*" : "" ;
272
+ if ( title !== "" ) {
273
+ document . title = `${ envString } ${ title } ${ dirtyIndicator } | ${ baseTitle } ` ;
274
+ } else {
275
+ document . title = `${ envString } ${ baseTitle } ` ;
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 ) ;
@@ -277,6 +318,9 @@ function App() {
277
318
}
278
319
279
320
function resetSandcastle ( ) {
321
+ if ( ! confirmLeave ( ) ) {
322
+ return ;
323
+ }
280
324
dispatch ( { type : "reset" } ) ;
281
325
282
326
window . history . pushState ( { } , "" , getBaseUrl ( ) ) ;
@@ -292,6 +336,7 @@ function App() {
292
336
293
337
const shareUrl = `${ getBaseUrl ( ) } #c=${ base64String } ` ;
294
338
window . history . replaceState ( { } , "" , shareUrl ) ;
339
+ dispatch ( { type : "resetDirty" } ) ;
295
340
}
296
341
297
342
function openStandalone ( ) {
@@ -407,12 +452,27 @@ function App() {
407
452
useEffect ( ( ) => {
408
453
// Listen to browser forward/back navigation and try to load from URL
409
454
// this is necessary because of the pushState used for faster gallery loading
410
- function pushStateListener ( ) {
411
- loadFromUrl ( ) ;
455
+ function popStateListener ( ) {
456
+ if ( confirmLeave ( ) ) {
457
+ loadFromUrl ( ) ;
458
+ }
412
459
}
413
- window . addEventListener ( "popstate" , pushStateListener ) ;
414
- return ( ) => window . removeEventListener ( "popstate" , pushStateListener ) ;
415
- } , [ loadFromUrl ] ) ;
460
+ window . addEventListener ( "popstate" , popStateListener ) ;
461
+ return ( ) => window . removeEventListener ( "popstate" , popStateListener ) ;
462
+ } , [ loadFromUrl , confirmLeave ] ) ;
463
+
464
+ useEffect ( ( ) => {
465
+ // if the code has been edited listen for navigation away and warn
466
+ if ( codeState . dirty ) {
467
+ function beforeUnloadListener ( e : BeforeUnloadEvent ) {
468
+ e . preventDefault ( ) ;
469
+ return "" ; // modern browsers ignore the contents of this string
470
+ }
471
+ window . addEventListener ( "beforeunload" , beforeUnloadListener ) ;
472
+ return ( ) =>
473
+ window . removeEventListener ( "beforeunload" , beforeUnloadListener ) ;
474
+ }
475
+ } , [ codeState . dirty ] ) ;
416
476
417
477
return (
418
478
< Root
@@ -443,8 +503,7 @@ function App() {
443
503
</ Button >
444
504
< div className = "flex-spacer" > </ div >
445
505
< div className = "version" >
446
- { versionString && < pre > { versionString . substring ( 0 , 7 ) } - </ pre > }
447
- < pre > { cesiumVersion } </ pre >
506
+ { versionString && < pre > { versionString } </ pre > }
448
507
</ div >
449
508
</ header >
450
509
< div className = "application-bar" >
@@ -505,6 +564,9 @@ function App() {
505
564
hidden = { leftPanel !== "gallery" }
506
565
galleryItems = { galleryItems }
507
566
loadDemo = { ( item , switchToCode ) => {
567
+ if ( ! confirmLeave ( ) ) {
568
+ return ;
569
+ }
508
570
// Load the gallery item every time it's clicked
509
571
loadGalleryItem ( item . id ) ;
510
572
0 commit comments