// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { Button } from "@/app/element/button"; import { WebviewTag } from "electron"; import React, { useEffect, useRef, useState } from "react"; import "./webview.less"; interface WebViewProps { parentRef: React.MutableRefObject; initialUrl: string; } const WebView = ({ parentRef, initialUrl }: WebViewProps) => { const [url, setUrl] = useState(initialUrl); const [inputUrl, setInputUrl] = useState(initialUrl); // Separate state for the input field const [webViewHeight, setWebViewHeight] = useState(0); const webviewRef = useRef(null); const inputRef = useRef(null); const historyStack = useRef([]); const historyIndex = useRef(-1); useEffect(() => { const inputHeight = inputRef.current?.getBoundingClientRect().height + 25; const parentHeight = parentRef.current?.getBoundingClientRect().height; setWebViewHeight(parentHeight - inputHeight); historyStack.current.push(initialUrl); historyIndex.current = 0; const webview = webviewRef.current; const handleNavigation = (newUrl: string) => { const normalizedNewUrl = normalizeUrl(newUrl); const normalizedLastUrl = normalizeUrl(historyStack.current[historyIndex.current]); if (normalizedLastUrl !== normalizedNewUrl) { setUrl(newUrl); setInputUrl(newUrl); // Update input field as well historyIndex.current += 1; historyStack.current = historyStack.current.slice(0, historyIndex.current); historyStack.current.push(newUrl); } }; if (webview) { const navigateListener = (event: any) => { handleNavigation(event.url); }; webview.addEventListener("did-navigate", navigateListener); webview.addEventListener("did-navigate-in-page", navigateListener); // Handle new-window event webview.addEventListener("new-window", (event: any) => { event.preventDefault(); const newUrl = event.url; webview.src = newUrl; }); // Suppress errors webview.addEventListener("did-fail-load", (event: any) => { if (event.errorCode === -3) { console.log("Suppressed ERR_ABORTED error"); } else { console.error(`Failed to load ${event.validatedURL}: ${event.errorDescription}`); } }); // Clean up event listeners on component unmount return () => { webview.removeEventListener("did-navigate", navigateListener); webview.removeEventListener("did-navigate-in-page", navigateListener); webview.removeEventListener("new-window", (event: any) => { webview.src = event.url; }); webview.removeEventListener("did-fail-load", (event: any) => { if (event.errorCode === -3) { console.log("Suppressed ERR_ABORTED error"); } else { console.error(`Failed to load ${event.validatedURL}: ${event.errorDescription}`); } }); }; } }, [initialUrl]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if ((event.ctrlKey || event.metaKey) && event.key === "l") { event.preventDefault(); if (inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } } }; const handleResize = () => { const parentHeight = parentRef.current?.getBoundingClientRect().height; setWebViewHeight(parentHeight); }; const parentElement = parentRef.current; if (parentElement) { parentElement.addEventListener("keydown", handleKeyDown); } window.addEventListener("resize", handleResize); return () => { if (parentElement) { parentElement.removeEventListener("keydown", handleKeyDown); } window.removeEventListener("resize", handleResize); }; }, []); const ensureUrlScheme = (url: string) => { if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) { return `https://${url}`; } return url; }; const normalizeUrl = (url: string) => { try { const parsedUrl = new URL(url); if (parsedUrl.hostname.startsWith("www.")) { parsedUrl.hostname = parsedUrl.hostname.slice(4); } parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, ""); // Remove trailing slashes parsedUrl.search = ""; // Remove query parameters return parsedUrl.href; } catch (e) { return url.replace(/\/+$/, ""); // Fallback for invalid URLs } }; const navigateTo = (newUrl: string) => { const finalUrl = ensureUrlScheme(newUrl); const normalizedFinalUrl = normalizeUrl(finalUrl); const normalizedLastUrl = normalizeUrl(historyStack.current[historyIndex.current]); if (normalizedLastUrl !== normalizedFinalUrl) { setUrl(finalUrl); setInputUrl(finalUrl); historyIndex.current += 1; historyStack.current = historyStack.current.slice(0, historyIndex.current); historyStack.current.push(finalUrl); if (webviewRef.current) { webviewRef.current.src = finalUrl; } } }; const handleBack = () => { if (historyIndex.current > 0) { historyIndex.current -= 1; const prevUrl = historyStack.current[historyIndex.current]; setUrl(prevUrl); setInputUrl(prevUrl); if (webviewRef.current) { webviewRef.current.src = prevUrl; } } }; const handleForward = () => { if (historyIndex.current < historyStack.current.length - 1) { historyIndex.current += 1; const nextUrl = historyStack.current[historyIndex.current]; setUrl(nextUrl); setInputUrl(nextUrl); if (webviewRef.current) { webviewRef.current.src = nextUrl; } } }; const handleUrlChange = (event: React.ChangeEvent) => { setInputUrl(event.target.value); }; const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Enter") { navigateTo(inputUrl); } }; const handleFocus = (event: React.FocusEvent) => { event.target.select(); }; return (
); }; export { WebView };