From 5fe0cae2444a5b8cc9df92cb7c6c4f63de419a76 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Tue, 10 Sep 2024 12:50:55 -0700 Subject: [PATCH] wsh web (#358) --- cmd/wsh/cmd/wshcmd-web.go | 26 +++++++++-- emain/emain-web.ts | 63 +++++++++++++++++++++++++++ emain/emain.ts | 6 +++ emain/preload.ts | 8 +++- frontend/app/view/webview/webview.tsx | 24 ++++++++-- 5 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 emain/emain-web.ts diff --git a/cmd/wsh/cmd/wshcmd-web.go b/cmd/wsh/cmd/wshcmd-web.go index f5bbc990e..c85203db1 100644 --- a/cmd/wsh/cmd/wshcmd-web.go +++ b/cmd/wsh/cmd/wshcmd-web.go @@ -18,18 +18,38 @@ var webCmd = &cobra.Command{ PersistentPreRunE: preRunSetupRpcClient, } -var webOpenCommand = &cobra.Command{ +var webOpenCmd = &cobra.Command{ Use: "open url", Short: "open a url a web widget", Args: cobra.ExactArgs(1), RunE: webOpenRun, } +var webGetCmd = &cobra.Command{ + Use: "get [--inner] [--all] css-selector", + Short: "get the html for a css selector", + Args: cobra.ExactArgs(1), + Hidden: true, + RunE: webGetRun, +} + +var webGetInner bool +var webGetAll bool +var webOpenMagnified bool + func init() { - webCmd.AddCommand(webOpenCommand) + webOpenCmd.Flags().BoolVarP(&webOpenMagnified, "magnified", "m", false, "open view in magnified mode") + webCmd.AddCommand(webOpenCmd) + webGetCmd.Flags().BoolVarP(&webGetInner, "inner", "", false, "get inner html (instead of outer)") + webGetCmd.Flags().BoolVarP(&webGetAll, "all", "", false, "get all matches (querySelectorAll)") + webCmd.AddCommand(webGetCmd) rootCmd.AddCommand(webCmd) } +func webGetRun(cmd *cobra.Command, args []string) error { + return nil +} + func webOpenRun(cmd *cobra.Command, args []string) error { wshCmd := wshrpc.CommandCreateBlockData{ BlockDef: &waveobj.BlockDef{ @@ -38,7 +58,7 @@ func webOpenRun(cmd *cobra.Command, args []string) error { waveobj.MetaKey_Url: args[0], }, }, - Magnified: viewMagnified, + Magnified: webOpenMagnified, } oref, err := wshclient.CreateBlockCommand(RpcClient, wshCmd, nil) if err != nil { diff --git a/emain/emain-web.ts b/emain/emain-web.ts new file mode 100644 index 000000000..d5193c68b --- /dev/null +++ b/emain/emain-web.ts @@ -0,0 +1,63 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { BrowserWindow, ipcMain, webContents, WebContents } from "electron"; + +export async function getWebContentsByBlockId( + win: BrowserWindow, + tabId: string, + blockId: string +): Promise { + const prtn = new Promise((resolve, reject) => { + const randId = Math.floor(Math.random() * 1000000000).toString(); + const respCh = `getWebContentsByBlockId-${randId}`; + win.webContents.send("webcontentsid-from-blockid", blockId, respCh); + ipcMain.once(respCh, (event, webContentsId) => { + if (webContentsId == null) { + resolve(null); + return; + } + const wc = webContents.fromId(parseInt(webContentsId)); + resolve(wc); + }); + setTimeout(() => { + reject(new Error("timeout waiting for response")); + }, 2000); + }); + return prtn; +} + +function escapeSelector(selector: string): string { + return selector + .replace(/\\/g, "\\\\") + .replace(/"/g, '\\"') + .replace(/'/g, "\\'") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/\t/g, "\\t"); +} + +export type WebGetOpts = { + all?: boolean; + inner?: boolean; +}; + +export async function webGetSelector(wc: WebContents, selector: string, opts?: WebGetOpts): Promise { + if (!wc || !selector) { + return null; + } + try { + const escapedSelector = escapeSelector(selector); + const queryMethod = opts?.all ? "querySelectorAll" : "querySelector"; + const prop = opts?.inner ? "innerHTML" : "outerHTML"; + const execExpr = ` + Array.from(document.${queryMethod}("${escapedSelector}") || []).map(el => el.${prop}); + `; + + const results = await wc.executeJavaScript(execExpr); + return results; + } catch (e) { + console.error("webGetSelector error", e); + return null; + } +} diff --git a/emain/emain.ts b/emain/emain.ts index b10b50364..cca079f48 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -824,6 +824,12 @@ async function relaunchBrowserWindows(): Promise { } } +process.on("uncaughtException", (error) => { + console.error("Uncaught Exception:", error); + console.error("Stack Trace:", error.stack); + electron.app.quit(); +}); + async function appMain() { const startTs = Date.now(); const instanceLock = electronApp.requestSingleInstanceLock(); diff --git a/emain/preload.ts b/emain/preload.ts index b74ee7a08..e92972923 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -1,7 +1,7 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -const { contextBridge, ipcRenderer } = require("electron"); +import { contextBridge, ipcRenderer, WebviewTag } from "electron"; contextBridge.exposeInMainWorld("api", { getAuthKey: () => ipcRenderer.sendSync("get-auth-key"), @@ -42,3 +42,9 @@ ipcRenderer.on("webview-new-window", (e, webContentsId, details) => { const event = new CustomEvent("new-window", { detail: details }); document.getElementById("webview").dispatchEvent(event); }); + +ipcRenderer.on("webcontentsid-from-blockid", (e, blockId, responseCh) => { + const webviewElem: WebviewTag = document.querySelector("div[data-blockid='" + blockId + "'] webview"); + const wcId = webviewElem?.dataset?.webcontentsid; + ipcRenderer.send(responseCh, wcId); +}); diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index eca6472d0..df7c1d434 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -362,6 +362,18 @@ const WebView = memo(({ model }: WebViewProps) => { // The initial value of the block metadata URL when the component first renders. Used to set the starting src value for the webview. const [metaUrlInitial] = useState(metaUrl); + const [webContentsId, setWebContentsId] = useState(null); + const [domReady, setDomReady] = useState(false); + + useEffect(() => { + if (model.webviewRef.current && domReady) { + const wcId = model.webviewRef.current.getWebContentsId?.(); + if (wcId) { + setWebContentsId(wcId); + } + } + }, [model.webviewRef.current, domReady]); + // Load a new URL if the block metadata is updated. useEffect(() => { if (metaUrlRef.current != metaUrl) { @@ -409,6 +421,9 @@ const WebView = memo(({ model }: WebViewProps) => { const webviewBlur = () => { getApi().setWebviewFocus(null); }; + const handleDomReady = () => { + setDomReady(true); + }; webview.addEventListener("did-navigate-in-page", navigateListener); webview.addEventListener("did-navigate", navigateListener); @@ -416,9 +431,9 @@ const WebView = memo(({ model }: WebViewProps) => { webview.addEventListener("did-stop-loading", stopLoadingHandler); webview.addEventListener("new-window", newWindowHandler); webview.addEventListener("did-fail-load", failLoadHandler); - webview.addEventListener("focus", webviewFocus); webview.addEventListener("blur", webviewBlur); + webview.addEventListener("dom-ready", handleDomReady); // Clean up event listeners on component unmount return () => { @@ -428,8 +443,9 @@ const WebView = memo(({ model }: WebViewProps) => { webview.removeEventListener("did-fail-load", failLoadHandler); webview.removeEventListener("did-start-loading", startLoadingHandler); webview.removeEventListener("did-stop-loading", stopLoadingHandler); - webview.addEventListener("focus", webviewFocus); - webview.addEventListener("blur", webviewBlur); + webview.removeEventListener("focus", webviewFocus); + webview.removeEventListener("blur", webviewBlur); + webview.removeEventListener("dom-ready", handleDomReady); }; } }, []); @@ -440,6 +456,8 @@ const WebView = memo(({ model }: WebViewProps) => { className="webview" ref={model.webviewRef} src={metaUrlInitial} + data-blockid={model.blockId} + data-webcontentsid={webContentsId} // needed for emain // @ts-ignore This is a discrepancy between the React typing and the Chromium impl for webviewTag. Chrome webviewTag expects a string, while React expects a boolean. allowpopups="true" >