From 5940a09cb033d652f2b4ffc170d819fae4da3231 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 13:46:59 -0400 Subject: [PATCH 1/9] Disable GH workflow --- .github/workflows/build.yml | 94 +++++++++++------------ .github/workflows/ci.yml | 144 ++++++++++++++++++------------------ .vscode/settings.json | 2 +- 3 files changed, 120 insertions(+), 120 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 29a1e2d1f..31b732070 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,47 +1,47 @@ -name: Build - -on: - push: - branches: [main] - paths-ignore: - - "**.md" - - "**.spec.js" - - ".idea" - - ".vscode" - - ".dockerignore" - - "Dockerfile" - - ".gitignore" - - ".github/**" - - "!.github/workflows/build.yml" - -jobs: - build: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: 18 - - - name: Install Dependencies - run: npm install - - - name: Build Release Files - run: npm run build - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload Artifact - uses: actions/upload-artifact@v3 - with: - name: release_on_${{ matrix. os }} - path: release/ - retention-days: 5 \ No newline at end of file +# name: Build + +# on: +# push: +# branches: [main] +# paths-ignore: +# - "**.md" +# - "**.spec.js" +# - ".idea" +# - ".vscode" +# - ".dockerignore" +# - "Dockerfile" +# - ".gitignore" +# - ".github/**" +# - "!.github/workflows/build.yml" + +# jobs: +# build: +# runs-on: ${{ matrix.os }} + +# strategy: +# matrix: +# os: [macos-latest, ubuntu-latest, windows-latest] + +# steps: +# - name: Checkout Code +# uses: actions/checkout@v3 + +# - name: Setup Node.js +# uses: actions/setup-node@v3 +# with: +# node-version: 18 + +# - name: Install Dependencies +# run: npm install + +# - name: Build Release Files +# run: npm run build +# env: +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +# - name: Upload Artifact +# uses: actions/upload-artifact@v3 +# with: +# name: release_on_${{ matrix. os }} +# path: release/ +# retention-days: 5 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 007024491..5b45ae268 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,81 +1,81 @@ -name: CI +# name: CI -on: - pull_request_target: - branches: - - main +# on: +# pull_request_target: +# branches: +# - main -permissions: - pull-requests: write +# permissions: +# pull-requests: write -jobs: - job1: - name: Check Not Allowed File Changes - runs-on: ubuntu-latest - outputs: - markdown_change: ${{ steps.filter_markdown.outputs.change }} - markdown_files: ${{ steps.filter_markdown.outputs.change_files }} - steps: +# jobs: +# job1: +# name: Check Not Allowed File Changes +# runs-on: ubuntu-latest +# outputs: +# markdown_change: ${{ steps.filter_markdown.outputs.change }} +# markdown_files: ${{ steps.filter_markdown.outputs.change_files }} +# steps: - - name: Check Not Allowed File Changes - uses: dorny/paths-filter@v2 - id: filter_not_allowed - with: - list-files: json - filters: | - change: - - 'package-lock.json' - - 'yarn.lock' - - 'pnpm-lock.yaml' +# - name: Check Not Allowed File Changes +# uses: dorny/paths-filter@v2 +# id: filter_not_allowed +# with: +# list-files: json +# filters: | +# change: +# - 'package-lock.json' +# - 'yarn.lock' +# - 'pnpm-lock.yaml' - # ref: https://github.com/github/docs/blob/main/.github/workflows/triage-unallowed-contributions.yml - - name: Comment About Changes We Can't Accept - if: ${{ steps.filter_not_allowed.outputs.change == 'true' }} - uses: actions/github-script@v6 - with: - script: | - let workflowFailMessage = "It looks like you've modified some files that we can't accept as contributions." - try { - const badFilesArr = [ - 'package-lock.json', - 'yarn.lock', - 'pnpm-lock.yaml', - ] - const badFiles = badFilesArr.join('\n- ') - const reviewMessage = `👋 Hey there spelunker. It looks like you've modified some files that we can't accept as contributions. The complete list of files we can't accept are:\n- ${badFiles}\n\nYou'll need to revert all of the files you changed in that list using [GitHub Desktop](https://docs.github.com/en/free-pro-team@latest/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/reverting-a-commit) or \`git checkout origin/main \`. Once you get those files reverted, we can continue with the review process. :octocat:\n\nMore discussion:\n- https://github.com/onlook-dev/browser/issues/` - createdComment = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.number, - body: reviewMessage, - }) - workflowFailMessage = `${workflowFailMessage} Please see ${createdComment.data.html_url} for details.` - } catch(err) { - console.log("Error creating comment.", err) - } - core.setFailed(workflowFailMessage) +# # ref: https://github.com/github/docs/blob/main/.github/workflows/triage-unallowed-contributions.yml +# - name: Comment About Changes We Can't Accept +# if: ${{ steps.filter_not_allowed.outputs.change == 'true' }} +# uses: actions/github-script@v6 +# with: +# script: | +# let workflowFailMessage = "It looks like you've modified some files that we can't accept as contributions." +# try { +# const badFilesArr = [ +# 'package-lock.json', +# 'yarn.lock', +# 'pnpm-lock.yaml', +# ] +# const badFiles = badFilesArr.join('\n- ') +# const reviewMessage = `👋 Hey there spelunker. It looks like you've modified some files that we can't accept as contributions. The complete list of files we can't accept are:\n- ${badFiles}\n\nYou'll need to revert all of the files you changed in that list using [GitHub Desktop](https://docs.github.com/en/free-pro-team@latest/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/reverting-a-commit) or \`git checkout origin/main \`. Once you get those files reverted, we can continue with the review process. :octocat:\n\nMore discussion:\n- https://github.com/onlook-dev/browser/issues/` +# createdComment = await github.rest.issues.createComment({ +# owner: context.repo.owner, +# repo: context.repo.repo, +# issue_number: context.payload.number, +# body: reviewMessage, +# }) +# workflowFailMessage = `${workflowFailMessage} Please see ${createdComment.data.html_url} for details.` +# } catch(err) { +# console.log("Error creating comment.", err) +# } +# core.setFailed(workflowFailMessage) - - name: Check Not Linted Markdown - if: ${{ always() }} - uses: dorny/paths-filter@v2 - id: filter_markdown - with: - list-files: shell - filters: | - change: - - added|modified: '*.md' +# - name: Check Not Linted Markdown +# if: ${{ always() }} +# uses: dorny/paths-filter@v2 +# id: filter_markdown +# with: +# list-files: shell +# filters: | +# change: +# - added|modified: '*.md' - job2: - name: Lint Markdown - runs-on: ubuntu-latest - needs: job1 - if: ${{ always() && needs.job1.outputs.markdown_change == 'true' }} - steps: - - name: Checkout Code - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} +# job2: +# name: Lint Markdown +# runs-on: ubuntu-latest +# needs: job1 +# if: ${{ always() && needs.job1.outputs.markdown_change == 'true' }} +# steps: +# - name: Checkout Code +# uses: actions/checkout@v3 +# with: +# ref: ${{ github.event.pull_request.head.sha }} - - name: Lint markdown - run: npx markdownlint-cli ${{ needs.job1.outputs.markdown_files }} --ignore node_modules \ No newline at end of file +# - name: Lint markdown +# run: npx markdownlint-cli ${{ needs.job1.outputs.markdown_files }} --ignore node_modules \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index badaac053..34584f3c3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,5 @@ "url": "https://json.schemastore.org/electron-builder" } ], - "github.copilot.inlineSuggest.enable": true + "github.copilot.inlineSuggest.enable": false } \ No newline at end of file From 18e93bbf419b74e7cb68c59dd54afce3f732418d Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 14:07:11 -0400 Subject: [PATCH 2/9] Clean up --- .vscode/settings.json | 2 +- src/lib/editor/index.ts | 34 ++++++++++++++++ src/routes/project/webview/Webview.tsx | 47 ++++++---------------- src/routes/project/webview/WebviewArea.tsx | 4 +- 4 files changed, 51 insertions(+), 36 deletions(-) create mode 100644 src/lib/editor/index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 34584f3c3..badaac053 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,5 @@ "url": "https://json.schemastore.org/electron-builder" } ], - "github.copilot.inlineSuggest.enable": false + "github.copilot.inlineSuggest.enable": true } \ 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..566c61cdd --- /dev/null +++ b/src/lib/editor/index.ts @@ -0,0 +1,34 @@ +import { handleConsoleMessage, handleIpcMessage } from '@/lib'; +import { WebviewMetadata } from '@/lib/models'; + +interface WebviewContext { + handlerRemovers: (() => void)[]; +} + +export class EditorManager { + webviewMap: Map = new Map(); + + eventHandlerMap = { + 'ipc-message': handleIpcMessage, + 'console-message': handleConsoleMessage, + } + + registerWebView(webview: Electron.WebviewTag, metadata: WebviewMetadata) { + const handlerRemovers: (() => void)[] = []; + Object.entries(this.eventHandlerMap).forEach(([event, handler]) => { + webview.addEventListener(event, handler as any); + handlerRemovers.push(() => { + webview.removeEventListener(event, handler as any); + }); + }); + this.webviewMap.set(metadata.id, { handlerRemovers }); + } + + deregisterWebView(webview: Electron.WebviewTag) { + const context = this.webviewMap.get(webview.id); + if (!context) + return; + context.handlerRemovers.forEach((removeHandler) => removeHandler()); + this.webviewMap.delete(webview.id); + } +} \ No newline at end of file diff --git a/src/routes/project/webview/Webview.tsx b/src/routes/project/webview/Webview.tsx index 73faff5f5..ee4b637c0 100644 --- a/src/routes/project/webview/Webview.tsx +++ b/src/routes/project/webview/Webview.tsx @@ -1,47 +1,27 @@ import { Label } from '@/components/ui/label'; -import { handleConsoleMessage, handleIpcMessage } from '@/lib'; import { MainChannel } from '@/lib/constants'; +import { EditorManager } from '@/lib/editor'; import { WebviewMetadata } from '@/lib/models'; import { useEffect, useRef, useState } from 'react'; - -function Webview({ metadata }: { metadata: WebviewMetadata }) { - const ref = useRef(null); +function Webview({ webviewManager, metadata }: { webviewManager: EditorManager, metadata: WebviewMetadata }) { + const webviewRef = 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; - - Object.entries(eventHandlerMap).forEach(([event, handler]) => { - webview.addEventListener(event, handler as any); - handlerRemovers.push(() => { - webview.removeEventListener(event, handler as any); - }); + function fetchPreloadPath() { + window.Main.invoke(MainChannel.WEBVIEW_PRELOAD_PATH).then((preloadPath: any) => { + setWebviewPreloadPath(preloadPath); }); - - return handlerRemovers; } useEffect(() => { - window.Main.invoke(MainChannel.WEBVIEW_PRELOAD_PATH).then((preloadPath: any) => { - setWebviewPreloadPath(preloadPath); - }); + fetchPreloadPath(); + const webview = webviewRef?.current; + if (!webview) return; - const handlerRemovers = setWebviewHandlers(); - return () => { - handlerRemovers.forEach((handlerRemover) => { - handlerRemover(); - }); - }; - }, [ref, webviewPreloadPath]); + webviewManager.registerWebView(webview, metadata); + return () => webviewManager.deregisterWebView(webview); + }, [webviewRef, webviewPreloadPath]); if (webviewPreloadPath) return ( @@ -49,14 +29,13 @@ function Webview({ metadata }: { metadata: WebviewMetadata }) { - ); } diff --git a/src/routes/project/webview/WebviewArea.tsx b/src/routes/project/webview/WebviewArea.tsx index a9c712508..5b173b068 100644 --- a/src/routes/project/webview/WebviewArea.tsx +++ b/src/routes/project/webview/WebviewArea.tsx @@ -1,8 +1,10 @@ +import { EditorManager as WebviewManager } from '@/lib/editor'; import { WebviewMetadata } from '@/lib/models'; import { nanoid } from 'nanoid'; import Webview from './Webview'; function WebviewArea() { + const webviewManager = new WebviewManager(); const webviews: WebviewMetadata[] = [ { id: nanoid(), @@ -14,7 +16,7 @@ function WebviewArea() { return (
{webviews.map((metadata, index) => ( - + ))}
); From f09bed6c1a2d9ce59cbc5897325928215ebf8cc1 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 16:23:53 -0400 Subject: [PATCH 3/9] Overlay rect working --- .../preload/common => common}/constants.ts | 0 common/models.ts | 5 + electron/preload/webview/elements/index.ts | 9 +- electron/preload/webview/eventBridge.ts | 7 +- src/lib/editor/index.ts | 17 +- src/lib/editor/overlay/index.ts | 141 +++++++++ src/lib/editor/overlay/rect.ts | 277 ++++++++++++++++++ src/lib/index.ts | 7 - .../{EditorTopBar.tsx => ProjectTopBar.tsx} | 0 .../{EditorPanel.tsx => SidePanel.tsx} | 3 +- src/routes/project/index.tsx | 4 +- src/routes/project/webview/Overlay.tsx | 25 ++ src/routes/project/webview/Webview.tsx | 8 +- src/routes/project/webview/WebviewArea.tsx | 53 +++- tsconfig.json | 3 +- 15 files changed, 520 insertions(+), 39 deletions(-) rename {electron/preload/common => common}/constants.ts (100%) create mode 100644 common/models.ts create mode 100644 src/lib/editor/overlay/index.ts create mode 100644 src/lib/editor/overlay/rect.ts delete mode 100644 src/lib/index.ts rename src/routes/project/{EditorTopBar.tsx => ProjectTopBar.tsx} (100%) rename src/routes/project/{EditorPanel.tsx => SidePanel.tsx} (61%) create mode 100644 src/routes/project/webview/Overlay.tsx diff --git a/electron/preload/common/constants.ts b/common/constants.ts similarity index 100% rename from electron/preload/common/constants.ts rename to common/constants.ts diff --git a/common/models.ts b/common/models.ts new file mode 100644 index 000000000..fd5d7beb1 --- /dev/null +++ b/common/models.ts @@ -0,0 +1,5 @@ +export interface ElementMetadata { + selector: string; + rect: DOMRect; + computedStyle: CSSStyleDeclaration; +} \ No newline at end of file diff --git a/electron/preload/webview/elements/index.ts b/electron/preload/webview/elements/index.ts index d58fc0a89..41aff7e1f 100644 --- a/electron/preload/webview/elements/index.ts +++ b/electron/preload/webview/elements/index.ts @@ -1,12 +1,7 @@ -import { EditorAttributes } from "../../common/constants"; +import { EditorAttributes } from "../../../../common/constants"; +import { ElementMetadata } from "../../../../common/models"; 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 } } diff --git a/electron/preload/webview/eventBridge.ts b/electron/preload/webview/eventBridge.ts index 1b04e4697..70d89cd02 100644 --- a/electron/preload/webview/eventBridge.ts +++ b/electron/preload/webview/eventBridge.ts @@ -5,14 +5,14 @@ export class EventBridge { constructor() { } init() { - ipcRenderer.sendToHost("key", {}); - this.setForwardingToHost(); this.setListenToHostEvents(); } - eventHandlerMap: { [key: string]: (e: any) => Object } = { + eventHandlerMap: Record Object> = { 'mouseover': handleMouseEvent, + 'click': handleMouseEvent, + 'dblclick': handleMouseEvent, 'wheel': (e: WheelEvent) => { return { coordinates: { x: e.deltaX, y: e.deltaY }, @@ -48,7 +48,6 @@ export class EventBridge { } setForwardingToHost() { - ipcRenderer.sendToHost("key", {}); Object.entries(this.eventHandlerMap).forEach(([key, handler]) => { document.body.addEventListener(key, (e) => { const data = JSON.stringify(handler(e)); diff --git a/src/lib/editor/index.ts b/src/lib/editor/index.ts index 566c61cdd..53063b1b9 100644 --- a/src/lib/editor/index.ts +++ b/src/lib/editor/index.ts @@ -1,16 +1,23 @@ -import { handleConsoleMessage, handleIpcMessage } from '@/lib'; import { WebviewMetadata } from '@/lib/models'; +import { WebviewEventHandler } from '@/routes/project/webview/WebviewArea'; interface WebviewContext { handlerRemovers: (() => void)[]; } -export class EditorManager { +export class WebviewMessageBridge { webviewMap: Map = new Map(); + eventHandlerMap: Record void>; - eventHandlerMap = { - 'ipc-message': handleIpcMessage, - 'console-message': handleConsoleMessage, + constructor(webviewEventHandler: WebviewEventHandler) { + this.eventHandlerMap = { + 'ipc-message': webviewEventHandler.handleIpcMessage, + 'console-message': this.handleConsoleMessage, + } + } + + handleConsoleMessage(e: Electron.ConsoleMessageEvent) { + console.log(`%c ${e.message}`, 'background: #000; color: #AAFF00'); } registerWebView(webview: Electron.WebviewTag, metadata: WebviewMetadata) { diff --git a/src/lib/editor/overlay/index.ts b/src/lib/editor/overlay/index.ts new file mode 100644 index 000000000..22e6d0b62 --- /dev/null +++ b/src/lib/editor/overlay/index.ts @@ -0,0 +1,141 @@ +import { ClickRect, EditRect, HoverRect, ParentRect } from "./rect"; + +export class OverlayManager { + overlayContainer: HTMLElement | undefined; + hoverRect: HoverRect + clickedRects: ClickRect[] + parentRect: ParentRect + editRect: EditRect + + constructor() { + this.hoverRect = new HoverRect(); + this.parentRect = new ParentRect(); + this.editRect = new EditRect(); + this.clickedRects = []; + + this.initializeRects() + this.bindMethods() + } + + initializeRects = () => { + this.appendRectToPopover(this.hoverRect.element) + this.appendRectToPopover(this.parentRect.element) + this.appendRectToPopover(this.editRect.element) + } + + bindMethods = () => { + this.setOverlayContainer = this.setOverlayContainer.bind(this) + this.updateHoverRect = this.updateHoverRect.bind(this) + this.updateParentRect = this.updateParentRect.bind(this) + this.updateEditRect = this.updateEditRect.bind(this) + this.hideHoverRect = this.hideHoverRect.bind(this) + this.showHoverRect = this.showHoverRect.bind(this) + this.removeHoverRect = this.removeHoverRect.bind(this) + this.removeEditRect = this.removeEditRect.bind(this) + this.removeClickedRects = this.removeClickedRects.bind(this) + this.clear = this.clear.bind(this) + } + + // Helper function to calculate cumulative offset from the document body + getCumulativeOffset(element: HTMLElement) { + let top = 0, left = 0; + do { + top += element.offsetTop || 0; + left += element.offsetLeft || 0; + element = element.offsetParent as HTMLElement; + } while (element); + return { top, left }; + } + + // Updated adjustRect method + adjustRect(rect: DOMRect, sourceWebview: Electron.WebviewTag) { + // Calculate the cumulative offsets for sourceWebview and overlayContainer + const sourceOffset = this.getCumulativeOffset(sourceWebview); + const overlayOffset = this.overlayContainer ? this.getCumulativeOffset(this.overlayContainer) : { top: 0, left: 0 }; + + // Calculate the adjusted position relative to the cumulative offset differences + const adjustedRect = { + ...rect, + top: rect.top - overlayOffset.top + sourceOffset.top, + left: rect.left - overlayOffset.left + sourceOffset.left, + }; + + return adjustedRect; + }; + + + setOverlayContainer = (container: HTMLElement) => { + this.overlayContainer = container; + this.appendRectToPopover(this.hoverRect.element); + this.appendRectToPopover(this.parentRect.element); + this.appendRectToPopover(this.editRect.element); + }; + + appendRectToPopover = (rect: HTMLElement) => { + if (this.overlayContainer) { + this.overlayContainer.appendChild(rect); + } + }; + + clear = () => { + this.removeParentRect() + this.removeHoverRect() + this.removeClickedRects() + this.removeEditRect() + } + + addClickRect = (el: HTMLElement) => { + if (!el) return + const clickRect = new ClickRect() + this.appendRectToPopover(clickRect.element) + this.clickedRects.push(clickRect) + + const rect = el.getBoundingClientRect() + const margin = window.getComputedStyle(el).margin + const padding = window.getComputedStyle(el).padding + clickRect.render({ width: rect.width, height: rect.height, top: rect.top, left: rect.left, padding, margin }); + } + + updateParentRect = (el: HTMLElement) => { + if (!el) return + const rect = el.getBoundingClientRect() + this.parentRect.render(rect) + } + + updateHoverRect = (rect: DOMRect) => { + this.hoverRect.render(rect) + } + + updateEditRect = (el: HTMLElement) => { + if (!el) return + const rect = el.getBoundingClientRect() + this.editRect.render(rect) + } + + hideHoverRect = () => { + this.hoverRect.element.style.display = 'none' + } + + showHoverRect = () => { + this.hoverRect.element.style.display = 'block' + } + + removeHoverRect = () => { + this.hoverRect.render({ width: 0, height: 0, top: 0, left: 0 }) + } + + removeEditRect = () => { + this.editRect.render({ width: 0, height: 0, top: 0, left: 0 }) + } + + removeClickedRects = () => { + this.clickedRects.forEach(clickRect => { + clickRect.element.remove() + }) + this.clickedRects = [] + } + + removeParentRect = () => { + this.parentRect.render({ width: 0, height: 0, top: 0, left: 0 }) + } +} diff --git a/src/lib/editor/overlay/rect.ts b/src/lib/editor/overlay/rect.ts new file mode 100644 index 000000000..6c3c0e1d2 --- /dev/null +++ b/src/lib/editor/overlay/rect.ts @@ -0,0 +1,277 @@ +import { nanoid } from 'nanoid'; +import { EditorAttributes } from '../../../../common/constants'; + +interface RectDimensions { + width: number; + height: number; + top: number; + left: number; +} + +interface Rect { + element: HTMLElement; + svgNamespace: string; + svgElement: Element; + rectElement: Element; + render: (rectDimensions: RectDimensions) => void; +} + +export class RectImpl implements Rect { + element: HTMLElement; + svgNamespace: string = 'http://www.w3.org/2000/svg' + svgElement: Element; + rectElement: Element; + + constructor() { + this.element = document.createElement('div') + this.svgElement = document.createElementNS(this.svgNamespace, 'svg') + this.svgElement.setAttribute('overflow', 'visible') + this.rectElement = document.createElementNS(this.svgNamespace, 'rect') + this.rectElement.setAttribute('fill', 'none') + this.rectElement.setAttribute('stroke', '#FF0E48') + this.rectElement.setAttribute('stroke-width', '2') + this.rectElement.setAttribute('stroke-linecap', 'round') + this.rectElement.setAttribute('stroke-linejoin', 'round') + this.svgElement.appendChild(this.rectElement) + + this.element.style.position = 'absolute' + this.element.style.pointerEvents = 'none' // Ensure it doesn't interfere with other interactions + this.element.style.zIndex = '999' + this.element.setAttribute(EditorAttributes.DATA_ONLOOK_IGNORE, 'true'); + this.element.setAttribute('id', EditorAttributes.ONLOOK_RECT_ID) + this.element.appendChild(this.svgElement) + + } + + render({ width, height, top, left }: RectDimensions) { + this.svgElement.setAttribute('width', width.toString()) + this.svgElement.setAttribute('height', height.toString()) + this.svgElement.setAttribute('viewBox', `0 0 ${width} ${height}`) + this.rectElement.setAttribute('width', width.toString()) + this.rectElement.setAttribute('height', height.toString()) + this.element.style.top = `${top}px` + this.element.style.left = `${left}px` + } +} + +export class HoverRect extends RectImpl { + constructor() { + super() + this.rectElement.setAttribute('stroke-width', '1') + } + + render(rectDimensions: RectDimensions) { + super.render(rectDimensions) + } +} + +export class ClickRect extends RectImpl { + constructor() { + super() + this.rectElement.setAttribute('stroke-width', '2') + } + + parseCssBoxValues(boxValue: string) { + const values = boxValue.split(' ').map(parseFloat); + if (values.length === 1) { + return { top: values[0], right: values[0], bottom: values[0], left: values[0] }; + } else if (values.length === 2) { + return { top: values[0], right: values[1], bottom: values[0], left: values[1] }; + } else if (values.length === 3) { + return { top: values[0], right: values[1], bottom: values[2], left: values[1] }; + } else { + return { top: values[0], right: values[1], bottom: values[2], left: values[3] }; + } + } + + createStripePattern(color = '#FF0E48') { + // Define a larger pattern for spaced-out stripes + const pattern = document.createElementNS(this.svgNamespace, 'pattern'); + const patternId = 'pattern-' + nanoid(); + pattern.setAttribute('id', patternId); + pattern.setAttribute('patternUnits', 'userSpaceOnUse'); + pattern.setAttribute('width', '20'); // Increased pattern width for spacing + pattern.setAttribute('height', '20'); // Increased pattern height for spacing + + // Create a background rectangle for the pattern + const background = document.createElementNS(this.svgNamespace, 'rect'); + background.setAttribute('width', '20'); // Match pattern width + background.setAttribute('height', '20'); // Match pattern height + background.setAttribute('fill', color); // Background color + background.setAttribute('fill-opacity', '0.1'); // Low opacity + + // Create multiple diagonal lines for the pattern to ensure connectivity + // Adjust the number of lines and their positions if you modify the pattern size + const createLine = (x1: string, y1: string, x2: string, y2: string) => { + const line = document.createElementNS(this.svgNamespace, 'line'); + line.setAttribute('x1', x1); + line.setAttribute('y1', y1); + line.setAttribute('x2', x2); + line.setAttribute('y2', y2); + line.setAttribute('stroke', color); // Stripe color + line.setAttribute('stroke-width', '0.3'); // Adjusted for visibility in larger pattern + line.setAttribute('stroke-linecap', 'square'); + return line; + }; + + // Add the background rectangle to the pattern first + pattern.appendChild(background); + + // Add lines to the pattern for a seamless connection across repeats + // The lines are drawn from corner to corner + pattern.appendChild(createLine('0', '20', '20', '0')); // Main diagonal line + + // Add the pattern to the SVG + this.svgElement.appendChild(pattern); + + return patternId; + } + + + updateMargin(margin: string, { width, height }: { width: number; height: number; }) { + const { top: mTop, right: mRight, bottom: mBottom, left: mLeft } = this.parseCssBoxValues(margin); + // Adjust position and size based on margins + const mWidth = width + mLeft + mRight; + const mHeight = height + mTop + mBottom; + const mX = -mLeft; + const mY = -mTop; + + const patternId = this.createStripePattern('#FF00FF'); + + // Create and style the margin rectangle + const marginRect = document.createElementNS(this.svgNamespace, 'rect'); + marginRect.setAttribute('x', mX.toString()); + marginRect.setAttribute('y', mY.toString()); + marginRect.setAttribute('width', mWidth.toString()); + marginRect.setAttribute('height', mHeight.toString()); + marginRect.setAttribute('fill', `url(#${patternId})`); // Use the pattern + marginRect.setAttribute('stroke', 'none'); + + // Create a mask element + const mask = document.createElementNS(this.svgNamespace, 'mask'); + const maskId = 'mask-' + nanoid(); // Unique ID for the mask + mask.setAttribute('id', maskId); + + // Create a white rectangle for the mask that matches the element size + // This rectangle allows the content beneath to show through where it overlaps with the marginRect + const maskRect = document.createElementNS(this.svgNamespace, 'rect'); + maskRect.setAttribute('x', mX.toString()); + maskRect.setAttribute('y', mY.toString()); + maskRect.setAttribute('width', mWidth.toString()); + maskRect.setAttribute('height', mHeight.toString()); + maskRect.setAttribute('fill', 'white'); // White areas of a mask are fully visible + + // Create the cutoutRect for the mask, which will block out the center + const cutoutRect = document.createElementNS(this.svgNamespace, 'rect'); + cutoutRect.setAttribute('x', '0'); + cutoutRect.setAttribute('y', '0'); + cutoutRect.setAttribute('width', width.toString()); + cutoutRect.setAttribute('height', height.toString()); + cutoutRect.setAttribute('fill', 'black'); // Black areas of a mask are fully transparent + + // Append the maskRect and cutoutRect to the mask + mask.appendChild(maskRect); + mask.appendChild(cutoutRect); + + // Add the mask to the SVG + this.svgElement.appendChild(mask); + + // Apply the mask to the marginRect + marginRect.setAttribute('mask', `url(#${maskId})`); + + // Add the marginRect to the SVG, which is now masked + this.svgElement.appendChild(marginRect); + } + + updatePadding(padding: string, { width, height }: { width: number; height: number; }) { + const { top: pTop, right: pRight, bottom: pBottom, left: pLeft } = this.parseCssBoxValues(padding); + // Adjust position and size based on paddings + const pWidth = width - pLeft - pRight; + const pHeight = height - pTop - pBottom; + const pX = pLeft; + const pY = pTop; + + const patternId = this.createStripePattern('green'); + + // Create and style the padding rectangle + const fullRect = document.createElementNS(this.svgNamespace, 'rect'); + fullRect.setAttribute('x', '0'); + fullRect.setAttribute('y', '0'); + fullRect.setAttribute('width', width.toString()); + fullRect.setAttribute('height', height.toString()); + fullRect.setAttribute('fill', `url(#${patternId})`); // Use the pattern + fullRect.setAttribute('stroke', 'none'); + + // // Create a mask element + const mask = document.createElementNS(this.svgNamespace, 'mask'); + const maskId = 'mask-' + nanoid(); // Unique ID for the mask + mask.setAttribute('id', maskId); + + // // Create a white rectangle for the mask that matches the element size + // // This rectangle allows the content beneath to show through where it overlaps with the marginRect + const maskRect = document.createElementNS(this.svgNamespace, 'rect'); + maskRect.setAttribute('x', '0'); + maskRect.setAttribute('y', '0'); + maskRect.setAttribute('width', width.toString()); + maskRect.setAttribute('height', height.toString()); + maskRect.setAttribute('fill', 'white'); + + // // Create the cutoutRect for the mask, which will block out the center + const cutoutRect = document.createElementNS(this.svgNamespace, 'rect'); + cutoutRect.setAttribute('x', pX.toString()); + cutoutRect.setAttribute('y', pY.toString()); + cutoutRect.setAttribute('width', pWidth.toString()); + cutoutRect.setAttribute('height', pHeight.toString()); + cutoutRect.setAttribute('fill', 'black'); // Black areas of a mask are fully transparent + + // // Append the maskRect and cutoutRect to the mask + mask.appendChild(maskRect); + mask.appendChild(cutoutRect); + + // // Add the mask to the SVG + this.svgElement.appendChild(mask); + + // // Apply the mask to the marginRect + fullRect.setAttribute('mask', `url(#${maskId})`); + + // Add the marginRect to the SVG, which is now masked + this.svgElement.appendChild(fullRect); + } + + render({ width, height, top, left, margin, padding }: { width: number, height: number, top: number, left: number, margin: string, padding: string }) { + // Sometimes a selected element can be removed. We handle this gracefully. + try { + this.updateMargin(margin, { width, height, }); + this.updatePadding(padding, { width, height, }); + + // Render the base rect (the element itself) on top + super.render({ width, height, top, left, }); + } catch (error) { + console.warn(error); + } + } +} + +export class ParentRect extends RectImpl { + constructor() { + super() + this.rectElement.setAttribute('stroke-width', '1') + this.rectElement.setAttribute('stroke-dasharray', '5') + } + + render(rect: RectDimensions) { + super.render(rect) + } +} + +export class EditRect extends RectImpl { + constructor() { + super() + this.rectElement.setAttribute('stroke', '#00FF94') + this.rectElement.setAttribute('stroke-width', '2') + } + + render(rect: RectDimensions) { + super.render(rect) + } +} diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index ad0a7c06f..000000000 --- a/src/lib/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function handleIpcMessage(e: Electron.IpcMessageEvent) { - console.log("Ipc Message:", e.channel, e.args) -}; - -export function handleConsoleMessage(e: Electron.ConsoleMessageEvent) { - console.log(`%c ${e.message}`, 'background: #000; color: #AAFF00'); -} diff --git a/src/routes/project/EditorTopBar.tsx b/src/routes/project/ProjectTopBar.tsx similarity index 100% rename from src/routes/project/EditorTopBar.tsx rename to src/routes/project/ProjectTopBar.tsx diff --git a/src/routes/project/EditorPanel.tsx b/src/routes/project/SidePanel.tsx similarity index 61% rename from src/routes/project/EditorPanel.tsx rename to src/routes/project/SidePanel.tsx index 2dc4d5f85..f16b83acd 100644 --- a/src/routes/project/EditorPanel.tsx +++ b/src/routes/project/SidePanel.tsx @@ -1,8 +1,7 @@ -import React from 'react'; const EditorPanel = () => { return ( -
+
); }; diff --git a/src/routes/project/index.tsx b/src/routes/project/index.tsx index c5c77415b..ec5c4a054 100644 --- a/src/routes/project/index.tsx +++ b/src/routes/project/index.tsx @@ -1,7 +1,7 @@ import Canvas from './Canvas'; -import EditorPanel from './EditorPanel'; -import EditorTopBar from './EditorTopBar'; +import EditorPanel from './SidePanel'; +import EditorTopBar from './ProjectTopBar'; import WebviewArea from './webview/WebviewArea'; function ProjectEditor() { diff --git a/src/routes/project/webview/Overlay.tsx b/src/routes/project/webview/Overlay.tsx new file mode 100644 index 000000000..5594d8a9d --- /dev/null +++ b/src/routes/project/webview/Overlay.tsx @@ -0,0 +1,25 @@ +import { OverlayManager } from "@/lib/editor/overlay"; +import { useEffect, useRef } from "react"; + +function Overlay({ children, overlayManager }: { children: React.ReactNode, overlayManager: OverlayManager }) { + const overlayContainerRef = useRef(null); + + useEffect(() => { + if (overlayContainerRef.current) { + const overlayContainer = overlayContainerRef.current; + if (!overlayManager) return; + overlayManager.setOverlayContainer(overlayContainer); + return () => { + overlayManager.clear(); + }; + } + }, [overlayManager, overlayContainerRef]); + return ( + <> + {children} +
+ + ) +} + +export default Overlay; \ No newline at end of file diff --git a/src/routes/project/webview/Webview.tsx b/src/routes/project/webview/Webview.tsx index ee4b637c0..7afb745d8 100644 --- a/src/routes/project/webview/Webview.tsx +++ b/src/routes/project/webview/Webview.tsx @@ -1,10 +1,10 @@ import { Label } from '@/components/ui/label'; import { MainChannel } from '@/lib/constants'; -import { EditorManager } from '@/lib/editor'; +import { WebviewMessageBridge } from '@/lib/editor'; import { WebviewMetadata } from '@/lib/models'; import { useEffect, useRef, useState } from 'react'; -function Webview({ webviewManager, metadata }: { webviewManager: EditorManager, metadata: WebviewMetadata }) { +function Webview({ webviewMessageBridge, metadata }: { webviewMessageBridge: WebviewMessageBridge, metadata: WebviewMetadata }) { const webviewRef = useRef(null); const [webviewPreloadPath, setWebviewPreloadPath] = useState(''); @@ -19,8 +19,8 @@ function Webview({ webviewManager, metadata }: { webviewManager: EditorManager, const webview = webviewRef?.current; if (!webview) return; - webviewManager.registerWebView(webview, metadata); - return () => webviewManager.deregisterWebView(webview); + webviewMessageBridge.registerWebView(webview, metadata); + return () => webviewMessageBridge.deregisterWebView(webview); }, [webviewRef, webviewPreloadPath]); if (webviewPreloadPath) diff --git a/src/routes/project/webview/WebviewArea.tsx b/src/routes/project/webview/WebviewArea.tsx index 5b173b068..654b5edc7 100644 --- a/src/routes/project/webview/WebviewArea.tsx +++ b/src/routes/project/webview/WebviewArea.tsx @@ -1,10 +1,47 @@ -import { EditorManager as WebviewManager } from '@/lib/editor'; +import { WebviewMessageBridge } from '@/lib/editor'; +import { OverlayManager } from '@/lib/editor/overlay'; import { WebviewMetadata } from '@/lib/models'; +import { ElementMetadata } from 'common/models'; import { nanoid } from 'nanoid'; +import Overlay from './Overlay'; import Webview from './Webview'; +export class WebviewEventHandler { + eventCallbackMap: Record void> + + constructor(overlayManager: OverlayManager) { + this.eventCallbackMap = { + 'mouseover': (e: Electron.IpcMessageEvent) => { + if (!e.args || e.args.length === 0) { + console.error('No args found for mouseover event'); + return; + } + + const sourceWebview = e.target as Electron.WebviewTag; + const elementMetadata: ElementMetadata = JSON.parse(e.args[0]); + const adjustedRect = overlayManager.adjustRect(elementMetadata.rect, sourceWebview); + overlayManager.updateHoverRect(adjustedRect); + }, + }; + this.handleIpcMessage = this.handleIpcMessage.bind(this); + } + + handleIpcMessage(e: Electron.IpcMessageEvent) { + console.log('ipc-message', e); + const eventHandler = this.eventCallbackMap[e.channel] + if (!eventHandler) { + console.error(`No event handler found for ${e.channel}`); + return; + } + eventHandler(e); + } +} + + function WebviewArea() { - const webviewManager = new WebviewManager(); + const overlayManager = new OverlayManager(); + const webviewEventHandler = new WebviewEventHandler(overlayManager); + const webviewMessageBridge = new WebviewMessageBridge(webviewEventHandler); const webviews: WebviewMetadata[] = [ { id: nanoid(), @@ -14,11 +51,13 @@ function WebviewArea() { ]; return ( -
- {webviews.map((metadata, index) => ( - - ))} -
+ +
+ {webviews.map((metadata, index) => ( + + ))} +
+
); } diff --git a/tsconfig.json b/tsconfig.json index 9472797cd..0e47c6831 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,7 +28,8 @@ }, "include": [ "src", - "electron" + "electron", + "common" ], "references": [ { From cd84b89c0b57bf9d51b9ca7e77ab6ad3b83f1d2f Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 16:41:24 -0400 Subject: [PATCH 4/9] Add click handler --- electron/preload/webview/elements/index.ts | 4 +++ src/lib/editor/overlay/index.ts | 33 +++++++++------------- src/routes/project/webview/WebviewArea.tsx | 14 ++++++++- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/electron/preload/webview/elements/index.ts b/electron/preload/webview/elements/index.ts index 41aff7e1f..e16133e61 100644 --- a/electron/preload/webview/elements/index.ts +++ b/electron/preload/webview/elements/index.ts @@ -3,6 +3,10 @@ import { ElementMetadata } from "../../../../common/models"; import { finder } from "./finder"; export const handleMouseEvent = (e: MouseEvent): Object => { + if (!e.metaKey) { + e.preventDefault(); + e.stopPropagation(); + } const el = deepElementFromPoint(e.clientX, e.clientY) if (!el) return { coordinates: { x: e.clientX, y: e.clientY } } diff --git a/src/lib/editor/overlay/index.ts b/src/lib/editor/overlay/index.ts index 22e6d0b62..bd89e7398 100644 --- a/src/lib/editor/overlay/index.ts +++ b/src/lib/editor/overlay/index.ts @@ -36,34 +36,30 @@ export class OverlayManager { this.clear = this.clear.bind(this) } - // Helper function to calculate cumulative offset from the document body - getCumulativeOffset(element: HTMLElement) { + // Helper function to calculate the relative offset to a common ancestor + getRelativeOffset(element: HTMLElement, ancestor: HTMLElement) { let top = 0, left = 0; - do { + while (element && element !== ancestor) { top += element.offsetTop || 0; left += element.offsetLeft || 0; element = element.offsetParent as HTMLElement; - } while (element); + } return { top, left }; } - // Updated adjustRect method - adjustRect(rect: DOMRect, sourceWebview: Electron.WebviewTag) { - // Calculate the cumulative offsets for sourceWebview and overlayContainer - const sourceOffset = this.getCumulativeOffset(sourceWebview); - const overlayOffset = this.overlayContainer ? this.getCumulativeOffset(this.overlayContainer) : { top: 0, left: 0 }; - - // Calculate the adjusted position relative to the cumulative offset differences + adaptRectFromSourceElement(rect: DOMRect, sourceWebview: Electron.WebviewTag) { + const commonAncestor = this.overlayContainer?.parentElement as HTMLElement; + const sourceOffset = this.getRelativeOffset(sourceWebview, commonAncestor); + const overlayOffset = this.overlayContainer ? this.getRelativeOffset(this.overlayContainer, commonAncestor) : { top: 0, left: 0 }; const adjustedRect = { ...rect, - top: rect.top - overlayOffset.top + sourceOffset.top, - left: rect.left - overlayOffset.left + sourceOffset.left, + top: rect.top + sourceOffset.top - overlayOffset.top, + left: rect.left + sourceOffset.left - overlayOffset.left, }; return adjustedRect; }; - setOverlayContainer = (container: HTMLElement) => { this.overlayContainer = container; this.appendRectToPopover(this.hoverRect.element); @@ -84,15 +80,12 @@ export class OverlayManager { this.removeEditRect() } - addClickRect = (el: HTMLElement) => { - if (!el) return + addClickRect = (rect: DOMRect, computerStyle: CSSStyleDeclaration) => { const clickRect = new ClickRect() this.appendRectToPopover(clickRect.element) this.clickedRects.push(clickRect) - - const rect = el.getBoundingClientRect() - const margin = window.getComputedStyle(el).margin - const padding = window.getComputedStyle(el).padding + const margin = computerStyle.margin + const padding = computerStyle.padding clickRect.render({ width: rect.width, height: rect.height, top: rect.top, left: rect.left, padding, margin }); } diff --git a/src/routes/project/webview/WebviewArea.tsx b/src/routes/project/webview/WebviewArea.tsx index 654b5edc7..bcae1fca2 100644 --- a/src/routes/project/webview/WebviewArea.tsx +++ b/src/routes/project/webview/WebviewArea.tsx @@ -19,9 +19,21 @@ export class WebviewEventHandler { const sourceWebview = e.target as Electron.WebviewTag; const elementMetadata: ElementMetadata = JSON.parse(e.args[0]); - const adjustedRect = overlayManager.adjustRect(elementMetadata.rect, sourceWebview); + const adjustedRect = overlayManager.adaptRectFromSourceElement(elementMetadata.rect, sourceWebview); overlayManager.updateHoverRect(adjustedRect); }, + 'click': (e: Electron.IpcMessageEvent) => { + if (!e.args || e.args.length === 0) { + console.error('No args found for mouseover event'); + return; + } + + const sourceWebview = e.target as Electron.WebviewTag; + const elementMetadata: ElementMetadata = JSON.parse(e.args[0]); + const adjustedRect = overlayManager.adaptRectFromSourceElement(elementMetadata.rect, sourceWebview); + overlayManager.removeClickedRects(); + overlayManager.addClickRect(adjustedRect, elementMetadata.computedStyle); + }, }; this.handleIpcMessage = this.handleIpcMessage.bind(this); } From fa13345dd8091677b797c4fb8e20c4fa36bdd714 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 17:06:03 -0400 Subject: [PATCH 5/9] Remove scroll --- electron/preload/webview/eventBridge.ts | 12 ++---------- src/routes/project/Canvas.tsx | 2 +- src/routes/project/webview/WebviewArea.tsx | 6 ++++++ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/electron/preload/webview/eventBridge.ts b/electron/preload/webview/eventBridge.ts index 70d89cd02..400a6ba07 100644 --- a/electron/preload/webview/eventBridge.ts +++ b/electron/preload/webview/eventBridge.ts @@ -14,18 +14,10 @@ export class EventBridge { 'click': handleMouseEvent, 'dblclick': handleMouseEvent, 'wheel': (e: WheelEvent) => { - return { - coordinates: { x: e.deltaX, y: e.deltaY }, - innerHeight: document.body.scrollHeight, - innerWidth: window.innerWidth, - } + return {} }, 'scroll': (e: Event) => { - return { - coordinates: { x: window.scrollX, y: window.scrollY }, - innerHeight: document.body.scrollHeight, - innerWidth: window.innerWidth, - } + return {} }, 'dom-ready': () => { const { body } = document; diff --git a/src/routes/project/Canvas.tsx b/src/routes/project/Canvas.tsx index 3090de39c..fc7369fc7 100644 --- a/src/routes/project/Canvas.tsx +++ b/src/routes/project/Canvas.tsx @@ -11,7 +11,7 @@ function Canvas({ children }: Canvas) { const containerRef = useRef(null); const overlayRef = useRef(null); - const zoomSensitivity = 0.002; + const zoomSensitivity = 0.003; const panSensitivity = 0.4; const handleWheel = (event: WheelEvent) => { diff --git a/src/routes/project/webview/WebviewArea.tsx b/src/routes/project/webview/WebviewArea.tsx index bcae1fca2..df9d05944 100644 --- a/src/routes/project/webview/WebviewArea.tsx +++ b/src/routes/project/webview/WebviewArea.tsx @@ -34,6 +34,12 @@ export class WebviewEventHandler { overlayManager.removeClickedRects(); overlayManager.addClickRect(adjustedRect, elementMetadata.computedStyle); }, + 'wheel': (e: Electron.IpcMessageEvent) => { + if (!e.args || e.args.length === 0) { + console.error('No args found for mouseover event'); + return; + } + }, }; this.handleIpcMessage = this.handleIpcMessage.bind(this); } From 21835cedd25c7efaedc22e22971ed1e409fe939d Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 17:11:36 -0400 Subject: [PATCH 6/9] Clean up --- src/lib/editor/index.ts | 4 ---- src/routes/project/webview/WebviewArea.tsx | 9 ++++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/lib/editor/index.ts b/src/lib/editor/index.ts index 53063b1b9..9edf23a7b 100644 --- a/src/lib/editor/index.ts +++ b/src/lib/editor/index.ts @@ -16,10 +16,6 @@ export class WebviewMessageBridge { } } - handleConsoleMessage(e: Electron.ConsoleMessageEvent) { - console.log(`%c ${e.message}`, 'background: #000; color: #AAFF00'); - } - registerWebView(webview: Electron.WebviewTag, metadata: WebviewMetadata) { const handlerRemovers: (() => void)[] = []; Object.entries(this.eventHandlerMap).forEach(([event, handler]) => { diff --git a/src/routes/project/webview/WebviewArea.tsx b/src/routes/project/webview/WebviewArea.tsx index df9d05944..99404362c 100644 --- a/src/routes/project/webview/WebviewArea.tsx +++ b/src/routes/project/webview/WebviewArea.tsx @@ -10,6 +10,9 @@ export class WebviewEventHandler { eventCallbackMap: Record void> constructor(overlayManager: OverlayManager) { + this.handleIpcMessage = this.handleIpcMessage.bind(this); + this.handleConsoleMessage = this.handleConsoleMessage.bind(this); + this.eventCallbackMap = { 'mouseover': (e: Electron.IpcMessageEvent) => { if (!e.args || e.args.length === 0) { @@ -41,7 +44,7 @@ export class WebviewEventHandler { } }, }; - this.handleIpcMessage = this.handleIpcMessage.bind(this); + } handleIpcMessage(e: Electron.IpcMessageEvent) { @@ -53,6 +56,10 @@ export class WebviewEventHandler { } eventHandler(e); } + + handleConsoleMessage(e: Electron.ConsoleMessageEvent) { + console.log(`%c ${e.message}`, 'background: #000; color: #AAFF00'); + } } From 3456ad9bd9db571dad2a6811eca77c0cb3145d22 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 17:17:22 -0400 Subject: [PATCH 7/9] Clean up --- src/lib/editor/eventHandler.ts | 59 ++++++++++++++++++ src/lib/editor/{index.ts => messageBridge.ts} | 4 +- src/routes/project/webview/Webview.tsx | 2 +- src/routes/project/webview/WebviewArea.tsx | 61 +------------------ 4 files changed, 64 insertions(+), 62 deletions(-) create mode 100644 src/lib/editor/eventHandler.ts rename src/lib/editor/{index.ts => messageBridge.ts} (89%) diff --git a/src/lib/editor/eventHandler.ts b/src/lib/editor/eventHandler.ts new file mode 100644 index 000000000..4a6171972 --- /dev/null +++ b/src/lib/editor/eventHandler.ts @@ -0,0 +1,59 @@ +import { ElementMetadata } from 'common/models'; +import { OverlayManager } from './overlay'; + +export class WebviewEventHandler { + eventCallbackMap: Record void> + + constructor(overlayManager: OverlayManager) { + this.handleIpcMessage = this.handleIpcMessage.bind(this); + this.handleConsoleMessage = this.handleConsoleMessage.bind(this); + + this.eventCallbackMap = { + 'mouseover': (e: Electron.IpcMessageEvent) => { + if (!e.args || e.args.length === 0) { + console.error('No args found for mouseover event'); + return; + } + + const sourceWebview = e.target as Electron.WebviewTag; + const elementMetadata: ElementMetadata = JSON.parse(e.args[0]); + const adjustedRect = overlayManager.adaptRectFromSourceElement(elementMetadata.rect, sourceWebview); + overlayManager.updateHoverRect(adjustedRect); + }, + 'click': (e: Electron.IpcMessageEvent) => { + if (!e.args || e.args.length === 0) { + console.error('No args found for mouseover event'); + return; + } + + const sourceWebview = e.target as Electron.WebviewTag; + const elementMetadata: ElementMetadata = JSON.parse(e.args[0]); + const adjustedRect = overlayManager.adaptRectFromSourceElement(elementMetadata.rect, sourceWebview); + overlayManager.removeClickedRects(); + overlayManager.addClickRect(adjustedRect, elementMetadata.computedStyle); + }, + 'wheel': (e: Electron.IpcMessageEvent) => { + if (!e.args || e.args.length === 0) { + console.error('No args found for mouseover event'); + return; + } + }, + }; + + } + + handleIpcMessage(e: Electron.IpcMessageEvent) { + console.log('ipc-message', e); + const eventHandler = this.eventCallbackMap[e.channel] + if (!eventHandler) { + console.error(`No event handler found for ${e.channel}`); + return; + } + eventHandler(e); + } + + handleConsoleMessage(e: Electron.ConsoleMessageEvent) { + console.log(`%c ${e.message}`, 'background: #000; color: #AAFF00'); + } +} + diff --git a/src/lib/editor/index.ts b/src/lib/editor/messageBridge.ts similarity index 89% rename from src/lib/editor/index.ts rename to src/lib/editor/messageBridge.ts index 9edf23a7b..23127d533 100644 --- a/src/lib/editor/index.ts +++ b/src/lib/editor/messageBridge.ts @@ -1,5 +1,5 @@ import { WebviewMetadata } from '@/lib/models'; -import { WebviewEventHandler } from '@/routes/project/webview/WebviewArea'; +import { WebviewEventHandler } from './eventHandler'; interface WebviewContext { handlerRemovers: (() => void)[]; @@ -12,7 +12,7 @@ export class WebviewMessageBridge { constructor(webviewEventHandler: WebviewEventHandler) { this.eventHandlerMap = { 'ipc-message': webviewEventHandler.handleIpcMessage, - 'console-message': this.handleConsoleMessage, + 'console-message': webviewEventHandler.handleConsoleMessage, } } diff --git a/src/routes/project/webview/Webview.tsx b/src/routes/project/webview/Webview.tsx index 7afb745d8..bd157c84c 100644 --- a/src/routes/project/webview/Webview.tsx +++ b/src/routes/project/webview/Webview.tsx @@ -1,6 +1,6 @@ import { Label } from '@/components/ui/label'; import { MainChannel } from '@/lib/constants'; -import { WebviewMessageBridge } from '@/lib/editor'; +import { WebviewMessageBridge } from '@/lib/editor/messageBridge'; import { WebviewMetadata } from '@/lib/models'; import { useEffect, useRef, useState } from 'react'; diff --git a/src/routes/project/webview/WebviewArea.tsx b/src/routes/project/webview/WebviewArea.tsx index 99404362c..810884717 100644 --- a/src/routes/project/webview/WebviewArea.tsx +++ b/src/routes/project/webview/WebviewArea.tsx @@ -1,68 +1,11 @@ -import { WebviewMessageBridge } from '@/lib/editor'; +import { WebviewEventHandler } from '@/lib/editor/eventHandler'; +import { WebviewMessageBridge } from '@/lib/editor/messageBridge'; import { OverlayManager } from '@/lib/editor/overlay'; import { WebviewMetadata } from '@/lib/models'; -import { ElementMetadata } from 'common/models'; import { nanoid } from 'nanoid'; import Overlay from './Overlay'; import Webview from './Webview'; -export class WebviewEventHandler { - eventCallbackMap: Record void> - - constructor(overlayManager: OverlayManager) { - this.handleIpcMessage = this.handleIpcMessage.bind(this); - this.handleConsoleMessage = this.handleConsoleMessage.bind(this); - - this.eventCallbackMap = { - 'mouseover': (e: Electron.IpcMessageEvent) => { - if (!e.args || e.args.length === 0) { - console.error('No args found for mouseover event'); - return; - } - - const sourceWebview = e.target as Electron.WebviewTag; - const elementMetadata: ElementMetadata = JSON.parse(e.args[0]); - const adjustedRect = overlayManager.adaptRectFromSourceElement(elementMetadata.rect, sourceWebview); - overlayManager.updateHoverRect(adjustedRect); - }, - 'click': (e: Electron.IpcMessageEvent) => { - if (!e.args || e.args.length === 0) { - console.error('No args found for mouseover event'); - return; - } - - const sourceWebview = e.target as Electron.WebviewTag; - const elementMetadata: ElementMetadata = JSON.parse(e.args[0]); - const adjustedRect = overlayManager.adaptRectFromSourceElement(elementMetadata.rect, sourceWebview); - overlayManager.removeClickedRects(); - overlayManager.addClickRect(adjustedRect, elementMetadata.computedStyle); - }, - 'wheel': (e: Electron.IpcMessageEvent) => { - if (!e.args || e.args.length === 0) { - console.error('No args found for mouseover event'); - return; - } - }, - }; - - } - - handleIpcMessage(e: Electron.IpcMessageEvent) { - console.log('ipc-message', e); - const eventHandler = this.eventCallbackMap[e.channel] - if (!eventHandler) { - console.error(`No event handler found for ${e.channel}`); - return; - } - eventHandler(e); - } - - handleConsoleMessage(e: Electron.ConsoleMessageEvent) { - console.log(`%c ${e.message}`, 'background: #000; color: #AAFF00'); - } -} - - function WebviewArea() { const overlayManager = new OverlayManager(); const webviewEventHandler = new WebviewEventHandler(overlayManager); From 9be60cdc683e6a7dbc27f2ccc913ed7c8ec99030 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 21:33:53 -0400 Subject: [PATCH 8/9] Save scroll --- electron/preload/webview/elements/index.ts | 4 ++-- electron/preload/webview/eventBridge.ts | 8 ++++---- src/lib/editor/eventHandler.ts | 3 +++ src/lib/editor/overlay/index.ts | 9 +++++++++ src/lib/editor/overlay/rect.ts | 5 +++++ tsconfig.json | 3 +++ vite.config.ts | 3 ++- 7 files changed, 28 insertions(+), 7 deletions(-) diff --git a/electron/preload/webview/elements/index.ts b/electron/preload/webview/elements/index.ts index e16133e61..f64769977 100644 --- a/electron/preload/webview/elements/index.ts +++ b/electron/preload/webview/elements/index.ts @@ -1,6 +1,6 @@ -import { EditorAttributes } from "../../../../common/constants"; -import { ElementMetadata } from "../../../../common/models"; import { finder } from "./finder"; +import { EditorAttributes } from "/common/constants"; +import { ElementMetadata } from "/common/models"; export const handleMouseEvent = (e: MouseEvent): Object => { if (!e.metaKey) { diff --git a/electron/preload/webview/eventBridge.ts b/electron/preload/webview/eventBridge.ts index 400a6ba07..abb99f3d2 100644 --- a/electron/preload/webview/eventBridge.ts +++ b/electron/preload/webview/eventBridge.ts @@ -9,15 +9,14 @@ export class EventBridge { this.setListenToHostEvents(); } - eventHandlerMap: Record Object> = { + eventHandlerMap: { [key: string]: (e: any) => Object } = { 'mouseover': handleMouseEvent, 'click': handleMouseEvent, - 'dblclick': handleMouseEvent, 'wheel': (e: WheelEvent) => { - return {} + return { x: window.scrollX, y: window.scrollY } }, 'scroll': (e: Event) => { - return {} + return { x: window.scrollX, y: window.scrollY } }, 'dom-ready': () => { const { body } = document; @@ -40,6 +39,7 @@ export class EventBridge { } setForwardingToHost() { + ipcRenderer.sendToHost("key", {}); Object.entries(this.eventHandlerMap).forEach(([key, handler]) => { document.body.addEventListener(key, (e) => { const data = JSON.stringify(handler(e)); diff --git a/src/lib/editor/eventHandler.ts b/src/lib/editor/eventHandler.ts index 4a6171972..08348bd35 100644 --- a/src/lib/editor/eventHandler.ts +++ b/src/lib/editor/eventHandler.ts @@ -31,12 +31,15 @@ export class WebviewEventHandler { const adjustedRect = overlayManager.adaptRectFromSourceElement(elementMetadata.rect, sourceWebview); overlayManager.removeClickedRects(); overlayManager.addClickRect(adjustedRect, elementMetadata.computedStyle); + console.log('click', elementMetadata); }, 'wheel': (e: Electron.IpcMessageEvent) => { if (!e.args || e.args.length === 0) { console.error('No args found for mouseover event'); return; } + const scrollPosition: { x: number, y: number } = JSON.parse(e.args[0]); + overlayManager.updateScroll(scrollPosition); }, }; diff --git a/src/lib/editor/overlay/index.ts b/src/lib/editor/overlay/index.ts index bd89e7398..3a40c270d 100644 --- a/src/lib/editor/overlay/index.ts +++ b/src/lib/editor/overlay/index.ts @@ -6,6 +6,7 @@ export class OverlayManager { clickedRects: ClickRect[] parentRect: ParentRect editRect: EditRect + scrollPosition: { x: number, y: number } = { x: 0, y: 0 } constructor() { this.hoverRect = new HoverRect(); @@ -80,6 +81,14 @@ export class OverlayManager { this.removeEditRect() } + updateScroll = ({ x, y }: { x: number, y: number }) => { + this.scrollPosition = { x, y } + this.hoverRect.applyScroll(x, y) + this.clickedRects.forEach(clickRect => { + clickRect.applyScroll(x, y) + }) + } + addClickRect = (rect: DOMRect, computerStyle: CSSStyleDeclaration) => { const clickRect = new ClickRect() this.appendRectToPopover(clickRect.element) diff --git a/src/lib/editor/overlay/rect.ts b/src/lib/editor/overlay/rect.ts index 6c3c0e1d2..b7d4d8997 100644 --- a/src/lib/editor/overlay/rect.ts +++ b/src/lib/editor/overlay/rect.ts @@ -51,6 +51,11 @@ export class RectImpl implements Rect { this.rectElement.setAttribute('height', height.toString()) this.element.style.top = `${top}px` this.element.style.left = `${left}px` + this.element.style.transform = 'translate(0, 0)' + } + + applyScroll(scrollX: number, scrollY: number) { + this.element.style.transform = `translate(${-scrollX}px, ${-scrollY}px)` } } diff --git a/tsconfig.json b/tsconfig.json index 0e47c6831..ef609f172 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,9 @@ "paths": { "@/*": [ "src/*" + ], + "/*": [ + "./*" ] }, }, diff --git a/vite.config.ts b/vite.config.ts index 5cabb063b..3708aa834 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,7 +16,8 @@ export default defineConfig(({ command }) => { return { resolve: { alias: { - '@': path.join(__dirname, 'src') + '@': path.join(__dirname, 'src'), + 'common': path.join(__dirname, 'common'), }, }, plugins: [ From 6c0f3e90f3545c4b135eb0fd39c989ea6ad31b24 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 27 Jun 2024 21:37:22 -0400 Subject: [PATCH 9/9] Working scroll --- src/routes/project/webview/WebviewArea.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/project/webview/WebviewArea.tsx b/src/routes/project/webview/WebviewArea.tsx index 810884717..482ec952b 100644 --- a/src/routes/project/webview/WebviewArea.tsx +++ b/src/routes/project/webview/WebviewArea.tsx @@ -14,7 +14,7 @@ function WebviewArea() { { id: nanoid(), title: 'Desktop', - src: 'https://www.framer.com/', + src: 'https://www.github.com/', }, ];