Skip to content

Commit c0ea093

Browse files
committed
update page title, track dirty state, prevent navigation when dirty
1 parent b8781c4 commit c0ea093

File tree

4 files changed

+75
-18
lines changed

4 files changed

+75
-18
lines changed

packages/sandcastle/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>Sandcastle Reborn</title>
7+
<title>Cesium Sandcastle</title>
88
<style>
99
/* Load fonts for itwin-ui */
1010
@font-face {

packages/sandcastle/src/App.tsx

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ const defaultHtmlCode = `<style>
5151
`;
5252

5353
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;
5458

5559
type RightSideRef = {
5660
toggleExpanded: () => void;
@@ -164,6 +168,7 @@ function AppBarButton({
164168

165169
export type SandcastleAction =
166170
| { type: "reset" }
171+
| { type: "resetDirty" }
167172
| { type: "setCode"; code: string }
168173
| { type: "setHtml"; html: string }
169174
| { type: "runSandcastle" }
@@ -178,9 +183,6 @@ function App() {
178183
const consoleCollapsedHeight = 26;
179184
const [consoleExpanded, setConsoleExpanded] = useState(false);
180185

181-
const cesiumVersion = __CESIUM_VERSION__;
182-
const versionString = __COMMIT_SHA__ ? `Commit: ${__COMMIT_SHA__}` : "";
183-
184186
const startOnEditor = !!(window.location.search || window.location.hash);
185187
const [leftPanel, setLeftPanel] = useState<"editor" | "gallery">(
186188
startOnEditor ? "editor" : "gallery",
@@ -196,6 +198,7 @@ function App() {
196198
committedCode: string;
197199
committedHtml: string;
198200
runNumber: number;
201+
dirty: boolean;
199202
};
200203

201204
const initialState: CodeState = {
@@ -204,6 +207,7 @@ function App() {
204207
committedCode: defaultJsCode,
205208
committedHtml: defaultHtmlCode,
206209
runNumber: 0,
210+
dirty: false,
207211
};
208212

209213
const [codeState, dispatch] = useReducer(function reducer(
@@ -218,12 +222,14 @@ function App() {
218222
return {
219223
...state,
220224
code: action.code,
225+
dirty: true,
221226
};
222227
}
223228
case "setHtml": {
224229
return {
225230
...state,
226231
html: action.html,
232+
dirty: true,
227233
};
228234
}
229235
case "runSandcastle": {
@@ -241,11 +247,46 @@ function App() {
241247
committedCode: action.code ?? state.code,
242248
committedHtml: action.html ?? state.html,
243249
runNumber: state.runNumber + 1,
250+
dirty: false,
251+
};
252+
}
253+
case "resetDirty": {
254+
return {
255+
...state,
256+
dirty: false,
244257
};
245258
}
246259
}
247260
}, initialState);
248261

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+
249290
const [legacyIdMap, setLegacyIdMap] = useState<Record<string, string>>({});
250291
const [galleryItems, setGalleryItems] = useState<GalleryItem[]>([]);
251292
const [galleryLoaded, setGalleryLoaded] = useState(false);
@@ -277,6 +318,9 @@ function App() {
277318
}
278319

279320
function resetSandcastle() {
321+
if (!confirmLeave()) {
322+
return;
323+
}
280324
dispatch({ type: "reset" });
281325

282326
window.history.pushState({}, "", getBaseUrl());
@@ -292,6 +336,7 @@ function App() {
292336

293337
const shareUrl = `${getBaseUrl()}#c=${base64String}`;
294338
window.history.replaceState({}, "", shareUrl);
339+
dispatch({ type: "resetDirty" });
295340
}
296341

297342
function openStandalone() {
@@ -407,12 +452,27 @@ function App() {
407452
useEffect(() => {
408453
// Listen to browser forward/back navigation and try to load from URL
409454
// 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+
}
412459
}
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]);
416476

417477
return (
418478
<Root
@@ -443,8 +503,7 @@ function App() {
443503
</Button>
444504
<div className="flex-spacer"></div>
445505
<div className="version">
446-
{versionString && <pre>{versionString.substring(0, 7)} - </pre>}
447-
<pre>{cesiumVersion}</pre>
506+
{versionString && <pre>{versionString}</pre>}
448507
</div>
449508
</header>
450509
<div className="application-bar">
@@ -505,6 +564,9 @@ function App() {
505564
hidden={leftPanel !== "gallery"}
506565
galleryItems={galleryItems}
507566
loadDemo={(item, switchToCode) => {
567+
if (!confirmLeave()) {
568+
return;
569+
}
508570
// Load the gallery item every time it's clicked
509571
loadGalleryItem(item.id);
510572

packages/sandcastle/src/main.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,6 @@ import { createRoot } from "react-dom/client";
33
import "./reset.css"; // TODO: this may not be needed with itwin-ui
44
import App from "./App.tsx";
55

6-
const host = window.location.host;
7-
if (host.includes("localhost")) {
8-
document.title = `${host.replace("localhost:", "")} ${document.title}`;
9-
} else if (host.includes("ci")) {
10-
document.title = `CI ${document.title}`;
11-
}
12-
136
createRoot(document.getElementById("app-container")!).render(
147
<StrictMode>
158
<App />

packages/sandcastle/vite.config.ci.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { defineConfig, UserConfig } from "vite";
22
import { viteStaticCopy } from "vite-plugin-static-copy";
3+
import { env } from "process";
34

45
import baseConfig, { cesiumPathReplace } from "./vite.config.ts";
56

@@ -22,6 +23,7 @@ export default defineConfig(() => {
2223
...config.define,
2324
__PAGE_BASE_URL__: JSON.stringify(process.env.BASE_URL),
2425
__GALLERY_BASE_URL__: JSON.stringify(`${config.base}/gallery`),
26+
__COMMIT_SHA__: JSON.stringify(env.GITHUB_SHA),
2527
};
2628

2729
const copyPlugin = viteStaticCopy({

0 commit comments

Comments
 (0)