//// // Our tiny browser, only for NOSE apps. // Check /public/browser-nav.js for the 2nd part. import { $, $$ } from "./dom" import { focusInput } from "./focus" import { scrollback } from "./dom" import { addInput } from "./scrollback" import { randomId } from "../shared/utils" const HEIGHT = 540 const WIDTH = 960 const controls = $("browser-controls") as HTMLDivElement const address = $("browser-address") as HTMLSpanElement let iframe: HTMLIFrameElement let realUrl = "" let showInput = true export function isBrowsing(): boolean { return document.querySelector("iframe.browser.active") !== null } export function openBrowser(url: string, openedVia: "click" | "command" = "click") { showInput = openedVia === "click" iframe = $$("iframe.browser.active") as HTMLIFrameElement iframe.src = url iframe.sandbox.add("allow-scripts", "allow-same-origin", "allow-forms") iframe.height = String(HEIGHT) iframe.width = String(WIDTH) iframe.tabIndex = 0 window.addEventListener("message", handleAppMessage) window.addEventListener("keydown", handleBrowserKeydown) controls.addEventListener("click", handleClick) iframe.addEventListener("load", handlePageLoad) controls.style.display = "" const main = document.querySelector("#content") main?.prepend(iframe) setAddress(url) } function closeBrowser() { window.removeEventListener("keydown", handleBrowserKeydown) window.removeEventListener("message", handleAppMessage) controls.removeEventListener("click", handleClick) iframe.removeEventListener("load", handlePageLoad) const id = randomId() if (showInput) addInput(id, "browse " + realUrl, "ok") iframe.height = String(HEIGHT / 2) iframe.width = String(WIDTH / 2) iframe.style.pointerEvents = "none" iframe.tabIndex = -1 iframe.classList.remove("fullscreen", "active") iframe.style.border = "2px solid var(--c64-light-blue)" scrollback.append(iframe) controls.style.display = "none" focusInput() } function handleAppMessage(event: MessageEvent) { const origin = event.origin if (!origin.includes('localhost') && !origin.match(/\.local$/)) { return } const { type, data } = event.data switch (type) { case 'NAV_READY': break case 'URL_CHANGED': setAddress(data.url) break case 'TITLE_CHANGED': // TODO: browser titles console.log('Page title:', data.title) break case 'NAV_BLOCKED': showNavigationError(data.url, data.reason) break case 'KEYDOWN': const keyEvent = new KeyboardEvent('keydown', { key: data.key, ctrlKey: data.ctrlKey, shiftKey: data.shiftKey, altKey: data.altKey, metaKey: data.metaKey, bubbles: true }) window.dispatchEvent(keyEvent) break } } function handleBrowserKeydown(e: KeyboardEvent) { if (e.key === "Escape" || (e.ctrlKey && e.key === "c")) { e.preventDefault() closeBrowser() } } function handleClick(e: MouseEvent) { const target = e.target if (!(target instanceof HTMLElement)) return switch (target.id) { case "back-button": navigateBack(); break case "forward-button": navigateForward(); break case "stop-button": stopLoading(); break case "reload-button": reloadBrowser(); break case "fullscreen-button": fullscreenBrowser(); break case "close-button": closeBrowser(); break default: return } e.preventDefault() } function handlePageLoad() { } function setAddress(url: string) { realUrl = url address.textContent = url.replace(/https?:\/\//, "") } function navigateBack() { sendNavCommand('back') } function navigateForward() { sendNavCommand('forward') } function reloadBrowser() { sendNavCommand('reload') } function stopLoading() { sendNavCommand('stop') } function fullscreenBrowser() { controls.style.display = 'none' iframe.classList.add('fullscreen') document.body.append(iframe) } function sendNavCommand(action: 'back' | 'forward' | 'reload' | 'stop') { if (!iframe.contentWindow) return iframe.contentWindow.postMessage({ type: 'NAV_COMMAND', action }, '*') } function showNavigationError(url: string, reason: string) { alert(`NAVIGATION BLOCKED\n\n${url}\n\n${reason}`) }