Skip to content

Commit 7b72cb9

Browse files
authored
Merge pull request #12794 from CesiumGS/window-title-dirty-state
New sandcastle title and dirty state tracking
2 parents 2a0aae0 + 2a07bc9 commit 7b72cb9

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>Sandcastle | CesiumJS</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 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+
249290
const [legacyIdMap, setLegacyIdMap] = useState<Record<string, string>>({});
250291
const [galleryItems, setGalleryItems] = useState<GalleryItem[]>([]);
251292
const [galleryLoaded, setGalleryLoaded] = useState(false);
@@ -280,6 +321,9 @@ function App() {
280321
}
281322

282323
function resetSandcastle() {
324+
if (!confirmLeave()) {
325+
return;
326+
}
283327
dispatch({ type: "reset" });
284328

285329
window.history.pushState({}, "", getBaseUrl());
@@ -295,6 +339,7 @@ function App() {
295339

296340
const shareUrl = `${getBaseUrl()}#c=${base64String}`;
297341
window.history.replaceState({}, "", shareUrl);
342+
dispatch({ type: "resetDirty" });
298343
}
299344

300345
function openStandalone() {
@@ -430,12 +475,27 @@ function App() {
430475
useEffect(() => {
431476
// Listen to browser forward/back navigation and try to load from URL
432477
// 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+
}
435482
}
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]);
439499

440500
return (
441501
<Root
@@ -466,8 +526,7 @@ function App() {
466526
</Button>
467527
<div className="flex-spacer"></div>
468528
<div className="version">
469-
{versionString && <pre>{versionString.substring(0, 7)} - </pre>}
470-
<pre>{cesiumVersion}</pre>
529+
{versionString && <pre>{versionString}</pre>}
471530
</div>
472531
</header>
473532
<div className="application-bar">
@@ -528,6 +587,9 @@ function App() {
528587
hidden={leftPanel !== "gallery"}
529588
galleryItems={galleryItems}
530589
loadDemo={(item, switchToCode) => {
590+
if (!confirmLeave()) {
591+
return;
592+
}
531593
// Load the gallery item every time it's clicked
532594
loadGalleryItem(item.id);
533595

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)