From 3d89d003505508d81a90647b63fcd3ac4cc8de97 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 26 Jun 2024 21:56:32 -0400 Subject: [PATCH 1/5] Cleanup --- electron/preload/webview.ts | 59 ++++++++++++++++++--------------- src/lib/editor/index.ts | 3 ++ src/routes/editor/FrameList.tsx | 30 ++++++++--------- 3 files changed, 48 insertions(+), 44 deletions(-) create mode 100644 src/lib/editor/index.ts diff --git a/electron/preload/webview.ts b/electron/preload/webview.ts index 5a336aba5..35f4a82a8 100644 --- a/electron/preload/webview.ts +++ b/electron/preload/webview.ts @@ -1,23 +1,26 @@ import { ipcRenderer } from 'electron'; -const documentBodyInit = () => { - // Context Menu - window.addEventListener('contextmenu', (e) => { - e.preventDefault(); - ipcRenderer.send('show-context-menu', { - contextMenuMeta: { x: e.x, y: e.y }, - }); - }); - - window.addEventListener('wheel', (e) => { - ipcRenderer.sendToHost('pass-scroll-data', { +const eventHandlerMap: { [key: string]: (e: any) => Object } = { + 'mouseover': (e: MouseEvent) => { + return { + el: e.target + } + }, + 'wheel': (e: WheelEvent) => { + return { coordinates: { x: e.deltaX, y: e.deltaY }, innerHeight: document.body.scrollHeight, innerWidth: window.innerWidth, - }); - }); - - window.addEventListener('dom-ready', () => { + } + }, + 'scroll': (e: Event) => { + return { + coordinates: { x: window.scrollX, y: window.scrollY }, + innerHeight: document.body.scrollHeight, + innerWidth: window.innerWidth, + } + }, + 'dom-ready': () => { const { body } = document; const html = document.documentElement; @@ -29,33 +32,35 @@ const documentBodyInit = () => { html.offsetHeight ); - ipcRenderer.sendToHost('pass-scroll-data', { + return { coordinates: { x: 0, y: 0 }, innerHeight: height, innerWidth: window.innerWidth, + } + } +} + +function handleBodyReady() { + Object.entries(eventHandlerMap).forEach(([key, handler]) => { + document.body.addEventListener(key, (e) => { + const data = handler(e); + ipcRenderer.sendToHost(key, data); }); - }); + }) }; -ipcRenderer.on('context-menu-command', (_, command) => { - ipcRenderer.sendToHost('context-menu-command', command); -}); - -const documentBodyWaitHandle = setInterval(() => { +const handleDocumentBody = setInterval(() => { window.onerror = function logError(errorMsg, url, lineNumber) { - // eslint-disable-next-line no-console console.log(`Unhandled error: ${errorMsg} ${url} ${lineNumber}`); // Code to run when an error has occurred on the page }; if (window?.document?.body) { - clearInterval(documentBodyWaitHandle); + clearInterval(handleDocumentBody); try { - documentBodyInit(); + handleBodyReady(); } catch (err) { console.log('Error in documentBodyInit:', err); } - return; } - console.log('document.body not ready'); }, 300); \ No newline at end of file diff --git a/src/lib/editor/index.ts b/src/lib/editor/index.ts new file mode 100644 index 000000000..53e386919 --- /dev/null +++ b/src/lib/editor/index.ts @@ -0,0 +1,3 @@ +export function ipcMessageHandler(e: Electron.IpcMessageEvent) { + console.log("🚀 ~ ipcMessageHandler ~ e.channel:", e.channel) +}; \ No newline at end of file diff --git a/src/routes/editor/FrameList.tsx b/src/routes/editor/FrameList.tsx index 69e42aadc..e0ab248fa 100644 --- a/src/routes/editor/FrameList.tsx +++ b/src/routes/editor/FrameList.tsx @@ -1,34 +1,30 @@ import { MainChannel } from '@/lib/constants'; +import { ipcMessageHandler } from '@/lib/editor'; import { useEffect, useRef, useState } from 'react'; function FrameList() { const ref = useRef(null); const [webviewPreloadPath, setWebviewPreloadPath] = useState(''); - useEffect(() => { - window.Main.invoke(MainChannel.WEBVIEW_PRELOAD_PATH).then((preloadPath: any) => { - setWebviewPreloadPath(preloadPath); - }); - - if (!ref.current) { - return; - } - const webview = ref.current as Electron.WebviewTag; + function setWebviewHandlers(): (() => void)[] { const handlerRemovers: (() => void)[] = []; - - webview.addEventListener('dom-ready', () => { - console.log('dom-ready'); - }); - - const ipcMessageHandler = (e: Electron.IpcMessageEvent) => { - console.log("🚀 ~ ipcMessageHandler ~ e.channel:", e.channel) - }; + const webview = ref.current as Electron.WebviewTag | null; + if (!webview) + return handlerRemovers; webview.addEventListener('ipc-message', ipcMessageHandler); handlerRemovers.push(() => { webview.removeEventListener('ipc-message', ipcMessageHandler); }); + return handlerRemovers; + } + + useEffect(() => { + window.Main.invoke(MainChannel.WEBVIEW_PRELOAD_PATH).then((preloadPath: any) => { + setWebviewPreloadPath(preloadPath); + }); + const handlerRemovers = setWebviewHandlers(); return () => { handlerRemovers.forEach((handlerRemover) => { handlerRemover(); From 24af8d7e12bbe1c86dff22e925bc66d5a6b5b1e7 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 11:14:49 -0400 Subject: [PATCH 2/5] Add multiple webviews --- bun.lockb | Bin 181476 -> 181845 bytes package.json | 1 + src/App.tsx | 2 +- src/lib/{editor => browserview}/index.ts | 0 src/lib/webview/models.ts | 4 +++ src/routes/{editor => project}/Canvas.tsx | 4 +-- .../{editor => project}/EditorPanel.tsx | 0 .../{editor => project}/EditorTopBar.tsx | 0 src/routes/{editor => project}/index.tsx | 4 +-- .../webview/Webview.tsx} | 11 +++++--- src/routes/project/webview/WebviewArea.tsx | 26 ++++++++++++++++++ 11 files changed, 43 insertions(+), 9 deletions(-) rename src/lib/{editor => browserview}/index.ts (100%) create mode 100644 src/lib/webview/models.ts rename src/routes/{editor => project}/Canvas.tsx (96%) rename src/routes/{editor => project}/EditorPanel.tsx (100%) rename src/routes/{editor => project}/EditorTopBar.tsx (100%) rename src/routes/{editor => project}/index.tsx (86%) rename src/routes/{editor/FrameList.tsx => project/webview/Webview.tsx} (81%) create mode 100644 src/routes/project/webview/WebviewArea.tsx diff --git a/bun.lockb b/bun.lockb index bb436aa37c2d419143b4580c06b7603b6ba48c93..369f57a23070ebfd176bd167066797952128ea0f 100755 GIT binary patch delta 25970 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 G^8W%9gQlba diff --git a/package.json b/package.json index 1ab22a81f..3b04e32e6 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "electron-updater": "^6.1.8", + "nanoid": "^5.0.7", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7" }, diff --git a/src/App.tsx b/src/App.tsx index e71524df4..c4a4fa5fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import AppBar from './AppBar'; import { ThemeProvider } from './components/theme-provider'; -import ProjectEditor from './routes/editor'; +import ProjectEditor from './routes/project'; function App() { return ( diff --git a/src/lib/editor/index.ts b/src/lib/browserview/index.ts similarity index 100% rename from src/lib/editor/index.ts rename to src/lib/browserview/index.ts diff --git a/src/lib/webview/models.ts b/src/lib/webview/models.ts new file mode 100644 index 000000000..099d05af4 --- /dev/null +++ b/src/lib/webview/models.ts @@ -0,0 +1,4 @@ +export interface WebviewMetadata { + id: string; + src: string; +} diff --git a/src/routes/editor/Canvas.tsx b/src/routes/project/Canvas.tsx similarity index 96% rename from src/routes/editor/Canvas.tsx rename to src/routes/project/Canvas.tsx index 5ae152fbd..3090de39c 100644 --- a/src/routes/editor/Canvas.tsx +++ b/src/routes/project/Canvas.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useEffect, useRef, useState } from 'react'; +import { ReactNode, useEffect, useRef, useState } from 'react'; interface Canvas { children: ReactNode; @@ -83,7 +83,7 @@ function Canvas({ children }: Canvas) { style={{ transition: 'transform ease', transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`, - transformOrigin: '0 0' + transformOrigin: '0 0', }} > {children} diff --git a/src/routes/editor/EditorPanel.tsx b/src/routes/project/EditorPanel.tsx similarity index 100% rename from src/routes/editor/EditorPanel.tsx rename to src/routes/project/EditorPanel.tsx diff --git a/src/routes/editor/EditorTopBar.tsx b/src/routes/project/EditorTopBar.tsx similarity index 100% rename from src/routes/editor/EditorTopBar.tsx rename to src/routes/project/EditorTopBar.tsx diff --git a/src/routes/editor/index.tsx b/src/routes/project/index.tsx similarity index 86% rename from src/routes/editor/index.tsx rename to src/routes/project/index.tsx index c7e236cbb..c5c77415b 100644 --- a/src/routes/editor/index.tsx +++ b/src/routes/project/index.tsx @@ -2,7 +2,7 @@ import Canvas from './Canvas'; import EditorPanel from './EditorPanel'; import EditorTopBar from './EditorTopBar'; -import FrameList from './FrameList'; +import WebviewArea from './webview/WebviewArea'; function ProjectEditor() { return ( @@ -13,7 +13,7 @@ function ProjectEditor() {
- +
diff --git a/src/routes/editor/FrameList.tsx b/src/routes/project/webview/Webview.tsx similarity index 81% rename from src/routes/editor/FrameList.tsx rename to src/routes/project/webview/Webview.tsx index e0ab248fa..2427b01bc 100644 --- a/src/routes/editor/FrameList.tsx +++ b/src/routes/project/webview/Webview.tsx @@ -1,8 +1,10 @@ +import { ipcMessageHandler } from '@/lib/browserview'; import { MainChannel } from '@/lib/constants'; -import { ipcMessageHandler } from '@/lib/editor'; +import { WebviewMetadata } from '@/lib/webview/models'; import { useEffect, useRef, useState } from 'react'; -function FrameList() { +// TODO: Manage multiple web views +function Webview({ metadata }: { metadata: WebviewMetadata }) { const ref = useRef(null); const [webviewPreloadPath, setWebviewPreloadPath] = useState(''); @@ -35,13 +37,14 @@ function FrameList() { if (webviewPreloadPath) return ( ); } -export default FrameList; +export default Webview; diff --git a/src/routes/project/webview/WebviewArea.tsx b/src/routes/project/webview/WebviewArea.tsx new file mode 100644 index 000000000..ce94fb8c4 --- /dev/null +++ b/src/routes/project/webview/WebviewArea.tsx @@ -0,0 +1,26 @@ +import { WebviewMetadata } from '@/lib/webview/models'; +import { nanoid } from 'nanoid'; +import Webview from './Webview'; + +function WebviewArea() { + const webviews: WebviewMetadata[] = [ + { + id: nanoid(), + src: 'https://www.framer.com/', + }, + { + id: nanoid(), + src: 'https://www.github.com/', + }, + ]; + + return ( +
+ {webviews.map((metadata, index) => ( + + ))} +
+ ); +} + +export default WebviewArea; From bdcba5637f332db1f568b6b03ddc8466cfaf37e7 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 12:45:00 -0400 Subject: [PATCH 3/5] Clean up --- electron/preload/{ => browserview}/index.ts | 0 electron/preload/webview.ts | 66 --------------------- electron/preload/webview/eventBridge.ts | 63 ++++++++++++++++++++ electron/preload/webview/index.ts | 22 +++++++ src/lib/{browserview => }/index.ts | 0 src/lib/{webview => }/models.ts | 1 + src/routes/project/webview/Webview.tsx | 26 ++++---- src/routes/project/webview/WebviewArea.tsx | 7 +-- vite.config.ts | 4 +- 9 files changed, 105 insertions(+), 84 deletions(-) rename electron/preload/{ => browserview}/index.ts (100%) delete mode 100644 electron/preload/webview.ts create mode 100644 electron/preload/webview/eventBridge.ts create mode 100644 electron/preload/webview/index.ts rename src/lib/{browserview => }/index.ts (100%) rename src/lib/{webview => }/models.ts (78%) diff --git a/electron/preload/index.ts b/electron/preload/browserview/index.ts similarity index 100% rename from electron/preload/index.ts rename to electron/preload/browserview/index.ts diff --git a/electron/preload/webview.ts b/electron/preload/webview.ts deleted file mode 100644 index 35f4a82a8..000000000 --- a/electron/preload/webview.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ipcRenderer } from 'electron'; - -const eventHandlerMap: { [key: string]: (e: any) => Object } = { - 'mouseover': (e: MouseEvent) => { - return { - el: e.target - } - }, - 'wheel': (e: WheelEvent) => { - return { - coordinates: { x: e.deltaX, y: e.deltaY }, - innerHeight: document.body.scrollHeight, - innerWidth: window.innerWidth, - } - }, - 'scroll': (e: Event) => { - return { - coordinates: { x: window.scrollX, y: window.scrollY }, - innerHeight: document.body.scrollHeight, - innerWidth: window.innerWidth, - } - }, - 'dom-ready': () => { - const { body } = document; - const html = document.documentElement; - - const height = Math.max( - body.scrollHeight, - body.offsetHeight, - html.clientHeight, - html.scrollHeight, - html.offsetHeight - ); - - return { - coordinates: { x: 0, y: 0 }, - innerHeight: height, - innerWidth: window.innerWidth, - } - } -} - -function handleBodyReady() { - Object.entries(eventHandlerMap).forEach(([key, handler]) => { - document.body.addEventListener(key, (e) => { - const data = handler(e); - ipcRenderer.sendToHost(key, data); - }); - }) -}; - -const handleDocumentBody = setInterval(() => { - window.onerror = function logError(errorMsg, url, lineNumber) { - console.log(`Unhandled error: ${errorMsg} ${url} ${lineNumber}`); - // Code to run when an error has occurred on the page - }; - - if (window?.document?.body) { - clearInterval(handleDocumentBody); - try { - handleBodyReady(); - } catch (err) { - console.log('Error in documentBodyInit:', err); - } - } -}, 300); \ No newline at end of file diff --git a/electron/preload/webview/eventBridge.ts b/electron/preload/webview/eventBridge.ts new file mode 100644 index 000000000..556eb75cf --- /dev/null +++ b/electron/preload/webview/eventBridge.ts @@ -0,0 +1,63 @@ +import { ipcRenderer } from 'electron'; + +export class EventBridge { + constructor() { } + + init() { + this.setForwardToHostEvents(); + this.setListenToHostEvents(); + } + + eventHandlerMap: { [key: string]: (e: any) => Object } = { + 'mouseover': (e: MouseEvent) => { + return { + el: e.target + } + }, + 'wheel': (e: WheelEvent) => { + return { + coordinates: { x: e.deltaX, y: e.deltaY }, + innerHeight: document.body.scrollHeight, + innerWidth: window.innerWidth, + } + }, + 'scroll': (e: Event) => { + return { + coordinates: { x: window.scrollX, y: window.scrollY }, + innerHeight: document.body.scrollHeight, + innerWidth: window.innerWidth, + } + }, + 'dom-ready': () => { + const { body } = document; + const html = document.documentElement; + + const height = Math.max( + body.scrollHeight, + body.offsetHeight, + html.clientHeight, + html.scrollHeight, + html.offsetHeight + ); + + return { + coordinates: { x: 0, y: 0 }, + innerHeight: height, + innerWidth: window.innerWidth, + } + } + } + + setForwardToHostEvents() { + Object.entries(this.eventHandlerMap).forEach(([key, handler]) => { + document.body.addEventListener(key, (e) => { + const data = handler(e); + ipcRenderer.sendToHost(key, data); + }); + }) + } + + setListenToHostEvents() { + + } +} \ No newline at end of file diff --git a/electron/preload/webview/index.ts b/electron/preload/webview/index.ts new file mode 100644 index 000000000..277315890 --- /dev/null +++ b/electron/preload/webview/index.ts @@ -0,0 +1,22 @@ +import { EventBridge } from "./eventBridge"; + +function handleBodyReady() { + const eventBridge = new EventBridge(); + eventBridge.init(); +}; + +const handleDocumentBody = setInterval(() => { + window.onerror = function logError(errorMsg, url, lineNumber) { + console.log(`Unhandled error: ${errorMsg} ${url} ${lineNumber}`); + // Code to run when an error has occurred on the page + }; + + if (window?.document?.body) { + clearInterval(handleDocumentBody); + try { + handleBodyReady(); + } catch (err) { + console.log('Error in documentBodyInit:', err); + } + } +}, 300); \ No newline at end of file diff --git a/src/lib/browserview/index.ts b/src/lib/index.ts similarity index 100% rename from src/lib/browserview/index.ts rename to src/lib/index.ts diff --git a/src/lib/webview/models.ts b/src/lib/models.ts similarity index 78% rename from src/lib/webview/models.ts rename to src/lib/models.ts index 099d05af4..a9908ddb0 100644 --- a/src/lib/webview/models.ts +++ b/src/lib/models.ts @@ -1,4 +1,5 @@ export interface WebviewMetadata { id: string; + title: string; src: string; } diff --git a/src/routes/project/webview/Webview.tsx b/src/routes/project/webview/Webview.tsx index 2427b01bc..99b7b2912 100644 --- a/src/routes/project/webview/Webview.tsx +++ b/src/routes/project/webview/Webview.tsx @@ -1,9 +1,9 @@ -import { ipcMessageHandler } from '@/lib/browserview'; +import { Label } from '@/components/ui/label'; +import { ipcMessageHandler } from '@/lib'; import { MainChannel } from '@/lib/constants'; -import { WebviewMetadata } from '@/lib/webview/models'; +import { WebviewMetadata } from '@/lib/models'; import { useEffect, useRef, useState } from 'react'; -// TODO: Manage multiple web views function Webview({ metadata }: { metadata: WebviewMetadata }) { const ref = useRef(null); const [webviewPreloadPath, setWebviewPreloadPath] = useState(''); @@ -36,14 +36,18 @@ function Webview({ metadata }: { metadata: WebviewMetadata }) { if (webviewPreloadPath) return ( - +
+ + +
+ ); } diff --git a/src/routes/project/webview/WebviewArea.tsx b/src/routes/project/webview/WebviewArea.tsx index ce94fb8c4..a9c712508 100644 --- a/src/routes/project/webview/WebviewArea.tsx +++ b/src/routes/project/webview/WebviewArea.tsx @@ -1,4 +1,4 @@ -import { WebviewMetadata } from '@/lib/webview/models'; +import { WebviewMetadata } from '@/lib/models'; import { nanoid } from 'nanoid'; import Webview from './Webview'; @@ -6,12 +6,9 @@ function WebviewArea() { const webviews: WebviewMetadata[] = [ { id: nanoid(), + title: 'Desktop', src: 'https://www.framer.com/', }, - { - id: nanoid(), - src: 'https://www.github.com/', - }, ]; return ( diff --git a/vite.config.ts b/vite.config.ts index 0fa240d9d..5cabb063b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -45,8 +45,8 @@ export default defineConfig(({ command }) => { }, preload: { input: { - index: 'electron/preload/index.ts', - webview: 'electron/preload/webview.ts', + index: 'electron/preload/browserview/index.ts', + webview: 'electron/preload/webview/index.ts', }, vite: { build: { From 9f6f5b9e715665e4aa34e8ca23d2c3f16fd5320e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 13:31:31 -0400 Subject: [PATCH 4/5] Logging from webview --- electron/preload/common/constants.ts | 12 + electron/preload/webview/elements/finder.ts | 333 ++++++++++++++++++++ electron/preload/webview/elements/index.ts | 55 ++++ electron/preload/webview/eventBridge.ts | 14 +- src/lib/index.ts | 10 +- src/routes/project/webview/Webview.tsx | 17 +- 6 files changed, 427 insertions(+), 14 deletions(-) create mode 100644 electron/preload/common/constants.ts create mode 100644 electron/preload/webview/elements/finder.ts create mode 100644 electron/preload/webview/elements/index.ts diff --git a/electron/preload/common/constants.ts b/electron/preload/common/constants.ts new file mode 100644 index 000000000..592e2899c --- /dev/null +++ b/electron/preload/common/constants.ts @@ -0,0 +1,12 @@ +export enum EditorAttributes { + ONLOOK_TOOLBAR = "onlook-toolbar", + ONLOOK_RECT_ID = "onlook-rect", + ONLOOK_GLOBAL_STYLES = "onlook-global-styles", + + DATA_ONLOOK_ID = "data-onlook-id", + DATA_ONLOOK_IGNORE = "data-onlook-ignore", + DATA_ONLOOK_SAVED = "data-onlook-saved", + DATA_ONLOOK_SNAPSHOT = "data-onlook-snapshot", + DATA_ONLOOK_OLD_VALS = "data-onlook-old-vals", + DATA_ONLOOK_COMPONENT_ID = "data-onlook-component-id", +} diff --git a/electron/preload/webview/elements/finder.ts b/electron/preload/webview/elements/finder.ts new file mode 100644 index 000000000..6418f6115 --- /dev/null +++ b/electron/preload/webview/elements/finder.ts @@ -0,0 +1,333 @@ +// License: MIT +// Author: Anton Medvedev +// Source: https://github.com/antonmedv/finder + +type Knot = { + name: string + penalty: number + level?: number +} + +type Path = Knot[] + +export type Options = { + root: Element + idName: (name: string) => boolean + className: (name: string) => boolean + tagName: (name: string) => boolean + attr: (name: string, value: string) => boolean + seedMinLength: number + optimizedMinLength: number + threshold: number + maxNumberOfTries: number +} + +let config: Options +let rootDocument: Document | Element + +export function finder(input: Element, options?: Partial) { + if (input.nodeType !== Node.ELEMENT_NODE) { + throw new Error(`Can't generate CSS selector for non-element node type.`) + } + if ('html' === input.tagName.toLowerCase()) { + return 'html' + } + const defaults: Options = { + root: document.body, + idName: (name: string) => true, + className: (name: string) => true, + tagName: (name: string) => true, + attr: (name: string, value: string) => false, + seedMinLength: 1, + optimizedMinLength: 2, + threshold: 1000, + maxNumberOfTries: 10000, + } + + config = { ...defaults, ...options } + rootDocument = findRootDocument(config.root, defaults) + + let path = + bottomUpSearch(input, 'all', + () => bottomUpSearch(input, 'two', + () => bottomUpSearch(input, 'one', + () => bottomUpSearch(input, 'none')))) + + if (path) { + const optimized = sort(optimize(path, input)) + if (optimized.length > 0) { + path = optimized[0] + } + return selector(path) + } else { + throw new Error(`Selector was not found.`) + } +} + +function findRootDocument(rootNode: Element | Document, defaults: Options) { + if (rootNode.nodeType === Node.DOCUMENT_NODE) { + return rootNode + } + if (rootNode === defaults.root) { + return rootNode.ownerDocument as Document + } + return rootNode +} + +function bottomUpSearch( + input: Element, + limit: 'all' | 'two' | 'one' | 'none', + fallback?: () => Path | null +): Path | null { + let path: Path | null = null + let stack: Knot[][] = [] + let current: Element | null = input + let i = 0 + while (current) { + let level: Knot[] = maybe(id(current)) || + maybe(...attr(current)) || + maybe(...classNames(current)) || + maybe(tagName(current)) || [any()] + const nth = index(current) + if (limit == 'all') { + if (nth) { + level = level.concat( + level.filter(dispensableNth).map((node) => nthChild(node, nth)) + ) + } + } else if (limit == 'two') { + level = level.slice(0, 1) + if (nth) { + level = level.concat( + level.filter(dispensableNth).map((node) => nthChild(node, nth)) + ) + } + } else if (limit == 'one') { + const [node] = (level = level.slice(0, 1)) + if (nth && dispensableNth(node)) { + level = [nthChild(node, nth)] + } + } else if (limit == 'none') { + level = [any()] + if (nth) { + level = [nthChild(level[0], nth)] + } + } + for (let node of level) { + node.level = i + } + stack.push(level) + if (stack.length >= config.seedMinLength) { + path = findUniquePath(stack, fallback) + if (path) { + break + } + } + current = current.parentElement + i++ + } + if (!path) { + path = findUniquePath(stack, fallback) + } + if (!path && fallback) { + return fallback() + } + return path +} + +function findUniquePath( + stack: Knot[][], + fallback?: () => Path | null +): Path | null { + const paths = sort(combinations(stack)) + if (paths.length > config.threshold) { + return fallback ? fallback() : null + } + for (let candidate of paths) { + if (unique(candidate)) { + return candidate + } + } + return null +} + +function selector(path: Path): string { + let node = path[0] + let query = node.name + for (let i = 1; i < path.length; i++) { + const level = path[i].level || 0 + if (node.level === level - 1) { + query = `${path[i].name} > ${query}` + } else { + query = `${path[i].name} ${query}` + } + node = path[i] + } + return query +} + +function penalty(path: Path): number { + return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0) +} + +function unique(path: Path) { + const css = selector(path) + switch (rootDocument.querySelectorAll(css).length) { + case 0: + throw new Error( + `Can't select any node with this selector: ${css}` + ) + case 1: + return true + default: + return false + } +} + +function id(input: Element): Knot | null { + const elementId = input.getAttribute('id') + if (elementId && config.idName(elementId)) { + return { + name: '#' + CSS.escape(elementId), + penalty: 0, + } + } + return null +} + +function attr(input: Element): Knot[] { + const attrs = Array.from(input.attributes).filter((attr) => + config.attr(attr.name, attr.value) + ) + return attrs.map( + (attr): Knot => ({ + name: `[${CSS.escape(attr.name)}="${CSS.escape(attr.value)}"]`, + penalty: 0.5, + }) + ) +} + +function classNames(input: Element): Knot[] { + const names = Array.from(input.classList).filter(config.className) + return names.map( + (name): Knot => ({ + name: '.' + CSS.escape(name), + penalty: 1, + }) + ) +} + +function tagName(input: Element): Knot | null { + const name = input.tagName.toLowerCase() + if (config.tagName(name)) { + return { + name, + penalty: 2, + } + } + return null +} + +function any(): Knot { + return { + name: '*', + penalty: 3, + } +} + +function index(input: Element): number | null { + const parent = input.parentNode + if (!parent) { + return null + } + let child = parent.firstChild + if (!child) { + return null + } + let i = 0 + while (child) { + if (child.nodeType === Node.ELEMENT_NODE) { + i++ + } + if (child === input) { + break + } + child = child.nextSibling + } + return i +} + +function nthChild(node: Knot, i: number): Knot { + return { + name: node.name + `:nth-child(${i})`, + penalty: node.penalty + 1, + } +} + +function dispensableNth(node: Knot) { + return node.name !== 'html' && !node.name.startsWith('#') +} + +function maybe(...level: (Knot | null)[]): Knot[] | null { + const list = level.filter(notEmpty) + if (list.length > 0) { + return list + } + return null +} + +function notEmpty(value: T | null | undefined): value is T { + return value !== null && value !== undefined +} + +function* combinations(stack: Knot[][], path: Knot[] = []): Generator { + if (stack.length > 0) { + for (let node of stack[0]) { + yield* combinations(stack.slice(1, stack.length), path.concat(node)) + } + } else { + yield path + } +} + +function sort(paths: Iterable): Path[] { + return [...paths].sort((a, b) => penalty(a) - penalty(b)) +} + +type Scope = { + counter: number + visited: Map +} + +function* optimize( + path: Path, + input: Element, + scope: Scope = { + counter: 0, + visited: new Map(), + } +): Generator { + if (path.length > 2 && path.length > config.optimizedMinLength) { + for (let i = 1; i < path.length - 1; i++) { + if (scope.counter > config.maxNumberOfTries) { + return // Okay At least I tried! + } + scope.counter += 1 + const newPath = [...path] + newPath.splice(i, 1) + const newPathKey = selector(newPath) + if (scope.visited.has(newPathKey)) { + return + } + if (unique(newPath) && same(newPath, input)) { + yield newPath + scope.visited.set(newPathKey, true) + yield* optimize(newPath, input, scope) + } + } + } +} + +function same(path: Path, input: Element) { + return rootDocument.querySelector(selector(path)) === input +} diff --git a/electron/preload/webview/elements/index.ts b/electron/preload/webview/elements/index.ts new file mode 100644 index 000000000..ba40b5365 --- /dev/null +++ b/electron/preload/webview/elements/index.ts @@ -0,0 +1,55 @@ +import { EditorAttributes } from "../../common/constants"; +import { finder } from "./finder"; + +export interface ElementMetadata { + selector: string; + rect: DOMRect; + computedStyle: CSSStyleDeclaration; +} + +export const handleMouseEvent = (e: MouseEvent): Object => { + const el = deepElementFromPoint(e.clientX, e.clientY) + if (!el) return { coordinates: { x: e.clientX, y: e.clientY } } + + const rect = el.getBoundingClientRect() + const computedStyle = window.getComputedStyle(el) + const selector = getUniqueSelector(el as HTMLElement) + return { + selector: selector, + rect, + computedStyle, + } as ElementMetadata +} + +export const getUniqueSelector = (el: HTMLElement): string => { + let selector = el.tagName.toLowerCase() + // If data-onlook-component-id exists, use that + if (el.hasAttribute(EditorAttributes.DATA_ONLOOK_COMPONENT_ID)) { + return `[${EditorAttributes.DATA_ONLOOK_COMPONENT_ID}="${el.getAttribute(EditorAttributes.DATA_ONLOOK_COMPONENT_ID)}"]` + } + + try { + if (el.nodeType !== Node.ELEMENT_NODE) { return selector } + selector = finder(el, { className: () => false }) + } catch (e) { + console.error("Error creating selector ", e); + } + return selector +} + +export const deepElementFromPoint = (x: number, y: number): Element | undefined => { + const el = document.elementFromPoint(x, y) + if (!el) return + const crawlShadows = (node: Element): Element => { + if (node?.shadowRoot) { + const potential = node.shadowRoot.elementFromPoint(x, y) + if (potential == node) return node + else if (potential?.shadowRoot) return crawlShadows(potential) + else return potential || node + } + else return node + } + + const nested_shadow = crawlShadows(el) + return (nested_shadow || el) +} \ No newline at end of file diff --git a/electron/preload/webview/eventBridge.ts b/electron/preload/webview/eventBridge.ts index 556eb75cf..032a3841f 100644 --- a/electron/preload/webview/eventBridge.ts +++ b/electron/preload/webview/eventBridge.ts @@ -1,19 +1,18 @@ import { ipcRenderer } from 'electron'; +import { handleMouseEvent } from './elements'; export class EventBridge { constructor() { } init() { - this.setForwardToHostEvents(); + ipcRenderer.sendToHost("key", {}); + + this.setForwardingToHost(); this.setListenToHostEvents(); } eventHandlerMap: { [key: string]: (e: any) => Object } = { - 'mouseover': (e: MouseEvent) => { - return { - el: e.target - } - }, + 'mouseover': handleMouseEvent, 'wheel': (e: WheelEvent) => { return { coordinates: { x: e.deltaX, y: e.deltaY }, @@ -48,7 +47,8 @@ export class EventBridge { } } - setForwardToHostEvents() { + setForwardingToHost() { + ipcRenderer.sendToHost("key", {}); Object.entries(this.eventHandlerMap).forEach(([key, handler]) => { document.body.addEventListener(key, (e) => { const data = handler(e); diff --git a/src/lib/index.ts b/src/lib/index.ts index 53e386919..ae98daa94 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,3 +1,7 @@ -export function ipcMessageHandler(e: Electron.IpcMessageEvent) { - console.log("🚀 ~ ipcMessageHandler ~ e.channel:", e.channel) -}; \ No newline at end of file +export function handleIpcMessage(e: Electron.IpcMessageEvent) { + console.log("Ipc Message:", e.channel) +}; + +export function handleConsoleMessage(e: Electron.ConsoleMessageEvent) { + console.log(`%c ${e.message}`, 'background: #000; color: #AAFF00'); +} diff --git a/src/routes/project/webview/Webview.tsx b/src/routes/project/webview/Webview.tsx index 99b7b2912..73faff5f5 100644 --- a/src/routes/project/webview/Webview.tsx +++ b/src/routes/project/webview/Webview.tsx @@ -1,23 +1,32 @@ import { Label } from '@/components/ui/label'; -import { ipcMessageHandler } from '@/lib'; +import { handleConsoleMessage, handleIpcMessage } from '@/lib'; import { MainChannel } from '@/lib/constants'; import { WebviewMetadata } from '@/lib/models'; import { useEffect, useRef, useState } from 'react'; + function Webview({ metadata }: { metadata: WebviewMetadata }) { const ref = useRef(null); const [webviewPreloadPath, setWebviewPreloadPath] = useState(''); + const eventHandlerMap = { + 'ipc-message': handleIpcMessage, + 'console-message': handleConsoleMessage, + } + function setWebviewHandlers(): (() => void)[] { const handlerRemovers: (() => void)[] = []; const webview = ref.current as Electron.WebviewTag | null; if (!webview) return handlerRemovers; - webview.addEventListener('ipc-message', ipcMessageHandler); - handlerRemovers.push(() => { - webview.removeEventListener('ipc-message', ipcMessageHandler); + Object.entries(eventHandlerMap).forEach(([event, handler]) => { + webview.addEventListener(event, handler as any); + handlerRemovers.push(() => { + webview.removeEventListener(event, handler as any); + }); }); + return handlerRemovers; } From ea8cfed64ead305ffaabcb513fb65783769c23e5 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 13:37:45 -0400 Subject: [PATCH 5/5] Successful data passing --- electron/preload/webview/elements/index.ts | 2 +- electron/preload/webview/eventBridge.ts | 2 +- src/lib/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electron/preload/webview/elements/index.ts b/electron/preload/webview/elements/index.ts index ba40b5365..d58fc0a89 100644 --- a/electron/preload/webview/elements/index.ts +++ b/electron/preload/webview/elements/index.ts @@ -15,7 +15,7 @@ export const handleMouseEvent = (e: MouseEvent): Object => { const computedStyle = window.getComputedStyle(el) const selector = getUniqueSelector(el as HTMLElement) return { - selector: selector, + selector, rect, computedStyle, } as ElementMetadata diff --git a/electron/preload/webview/eventBridge.ts b/electron/preload/webview/eventBridge.ts index 032a3841f..1b04e4697 100644 --- a/electron/preload/webview/eventBridge.ts +++ b/electron/preload/webview/eventBridge.ts @@ -51,7 +51,7 @@ export class EventBridge { ipcRenderer.sendToHost("key", {}); Object.entries(this.eventHandlerMap).forEach(([key, handler]) => { document.body.addEventListener(key, (e) => { - const data = handler(e); + const data = JSON.stringify(handler(e)); ipcRenderer.sendToHost(key, data); }); }) diff --git a/src/lib/index.ts b/src/lib/index.ts index ae98daa94..ad0a7c06f 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,5 +1,5 @@ export function handleIpcMessage(e: Electron.IpcMessageEvent) { - console.log("Ipc Message:", e.channel) + console.log("Ipc Message:", e.channel, e.args) }; export function handleConsoleMessage(e: Electron.ConsoleMessageEvent) {