nose-pluto/src/js/browser.ts

179 lines
4.2 KiB
TypeScript

////
// 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}`)
}