Files
rio/frontend/code/app.ts
2024-05-26 14:15:04 +02:00

162 lines
5.7 KiB
TypeScript

import { getComponentByElement } from './componentManagement';
import { updateLayout } from './layouting';
import { callRemoteMethodDiscardResponse, initWebsocket } from './rpc';
import { scrollToUrlFragment } from './utils';
// Most of these don't have to be available in the global scope, however, since
// these are injected by Python after the build process, there have been issues
// with some build tool inlining their placeholders, which in turn lead to
// incorrect code.
//
// Assigning them to `globalThis` convinces the build tool to leave them alone.
globalThis.SESSION_TOKEN = '{session_token}';
globalThis.PING_PONG_INTERVAL_SECONDS = '{ping_pong_interval}';
globalThis.RIO_DEBUG_MODE = '{debug_mode}';
globalThis.CHILD_ATTRIBUTE_NAMES = '{child_attribute_names}';
globalThis.RUNNING_IN_WINDOW = '{running_in_window}';
// If a the devtools are present it is exposed here so the codebase can notify it as
// needed. This is an instance of `DevToolsConnectorComponent`.
globalThis.RIO_DEV_TOOLS = null;
// Set to indicate that we're intentionally leaving the page. This can be used
// to suppress the connection lost popup, reconnects, or similar
export let goingAway: boolean = false;
function getScrollBarWidthInPixels(): number {
let outer = document.createElement('div');
outer.style.position = 'absolute';
outer.style.top = '0px';
outer.style.left = '0px';
outer.style.visibility = 'hidden';
outer.style.width = '200px';
outer.style.height = '150px';
outer.style.overflow = 'hidden';
let inner = document.createElement('p');
inner.style.width = '100%';
inner.style.height = '200px';
outer.appendChild(inner);
document.body.appendChild(outer);
let w1 = inner.offsetWidth;
outer.style.overflow = 'scroll';
let w2 = inner.offsetWidth;
if (w1 == w2) w2 = outer.clientWidth;
document.body.removeChild(outer);
return w1 - w2;
}
const SCROLL_BAR_SIZE_IN_PIXELS = getScrollBarWidthInPixels();
export let pixelsPerRem = 16;
export let scrollBarSize = SCROLL_BAR_SIZE_IN_PIXELS / pixelsPerRem;
function main(): void {
if (typeof globalThis.PING_PONG_INTERVAL_SECONDS !== 'number') {
console.error(
`Received erroneous HTML from the server: The ping pong interval is ${globalThis.PING_PONG_INTERVAL_SECONDS} instead of a number`
);
return;
}
// Display a warning if running in debug mode
if (globalThis.RIO_DEBUG_MODE) {
console.warn(
'Rio is running in DEBUG mode.\nDebug mode includes helpful tools' +
' for development, but is slower and disables some safety checks.' +
' Never use it in production!'
);
}
// Determine the browser's font size
var measure = document.createElement('div');
measure.style.height = '10rem';
document.body.appendChild(measure);
pixelsPerRem = measure.offsetHeight / 10;
document.body.removeChild(measure);
scrollBarSize = SCROLL_BAR_SIZE_IN_PIXELS / pixelsPerRem;
// TEMP, for debugging
globalThis.pixelsPerRem = pixelsPerRem;
globalThis.scrollBarSize = scrollBarSize;
window.addEventListener('beforeunload', () => {
goingAway = true;
});
// Note: I'm not sure if this event is ever triggered. The smooth scrolling
// is actually implemented in `popstate` and in the CSS.
window.addEventListener('hashchange', (event) => {
console.log(
`hashchange event triggered; new URL is ${window.location.href}`
);
scrollToUrlFragment('smooth');
});
// Listen for URL changes, so the session can switch page
let urlWithoutHash = window.location.href.split('#')[0];
window.addEventListener('popstate', (event: PopStateEvent) => {
console.log(
`popstate event triggered; new URL is ${window.location.href}`
);
// For some reason, this event is also triggered if the user manually
// types in a URL fragment. So we need to check which part of the url
// has changed and act accordingly. If it was only the url fragment,
// we'll simply scroll the relevant ScrollTarget into view.
//
// NOTE: If we send a `onUrlChange` message to the server, it'll cause a
// rebuild of all PageViews and scroll to the top of the page. This is
// why we *EITHER* send a `onUrlChange` message *OR* scroll to the
// ScrollTarget, but not both.
let oldUrlWithoutHash = urlWithoutHash;
urlWithoutHash = window.location.href.split('#')[0];
if (urlWithoutHash === oldUrlWithoutHash) {
scrollToUrlFragment('smooth');
} else {
console.log(`URL changed to ${window.location.href}`);
callRemoteMethodDiscardResponse('onUrlChange', {
newUrl: window.location.href.toString(),
});
}
});
// Listen for resize events
window.addEventListener('resize', (event) => {
// Notify the backend
try {
callRemoteMethodDiscardResponse('onWindowResize', {
newWidth: window.innerWidth / pixelsPerRem,
newHeight: window.innerHeight / pixelsPerRem,
});
} catch (e) {
console.warn(`Couldn't notify backend of window resize: ${e}`);
}
// Re-layout, but only if a root component already exists
let rootElement = document.body.querySelector(
'.rio-fundamental-root-component'
);
if (rootElement !== null) {
let rootInstance = getComponentByElement(
rootElement as HTMLElement
);
rootInstance.makeLayoutDirty();
updateLayout();
}
});
// Connect to the websocket
initWebsocket();
}
main();