mirror of
https://github.com/BloopAI/vibe-kanban-web-companion.git
synced 2026-08-23 11:58:32 +00:00
Iframe communication (vibe-kanban e1be4a05)
As well as triggering the context menu to open, when the user clicks on a react item we should communicate with the parent page (this is all loaded in an iframe). As it's a dev environment we need to keep security low, so don't check domains.
This commit is contained in:
@@ -8,7 +8,9 @@ import { html } from 'htm/react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { ContextMenu } from './ContextMenu.js'
|
||||
import { getDisplayNameForInstance } from './getDisplayNameFromReactInstance.js'
|
||||
import { getPathToSource } from './getPathToSource.js'
|
||||
import { getPropsForInstance } from './getPropsForInstance.js'
|
||||
import { getReactInstancesForElement } from './getReactInstancesForElement.js'
|
||||
import { getSourceForInstance } from './getSourceForInstance.js'
|
||||
import { getUrl } from './getUrl.js'
|
||||
@@ -24,6 +26,123 @@ export const Trigger = /** @type {const} */ ({
|
||||
BUTTON: 'button',
|
||||
})
|
||||
|
||||
// Message source and version for iframe communication
|
||||
const MESSAGE_SOURCE = 'click-to-component'
|
||||
const MESSAGE_VERSION = 1
|
||||
|
||||
/**
|
||||
* Extract component instances data for a target element
|
||||
* @param {HTMLElement} target
|
||||
* @param {Function} pathModifier
|
||||
* @returns {Array}
|
||||
*/
|
||||
function getComponentInstances(target, pathModifier) {
|
||||
if (!target) return []
|
||||
|
||||
const instances = getReactInstancesForElement(target).filter((instance) =>
|
||||
getSourceForInstance(instance)
|
||||
)
|
||||
|
||||
return instances.map((instance) => {
|
||||
const name = getDisplayNameForInstance(instance)
|
||||
const source = getSourceForInstance(instance)
|
||||
const path = getPathToSource(source, pathModifier)
|
||||
const props = getPropsForInstance(instance)
|
||||
|
||||
return {
|
||||
name,
|
||||
props,
|
||||
source: {
|
||||
fileName: source.fileName,
|
||||
lineNumber: source.lineNumber,
|
||||
columnNumber: source.columnNumber
|
||||
},
|
||||
pathToSource: path
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the parent window when opening in editor.
|
||||
* No-ops when not inside an iframe.
|
||||
* @param {Object} args
|
||||
* @param {string} args.editor
|
||||
* @param {string} args.pathToSource
|
||||
* @param {string} args.url
|
||||
* @param {'alt-click'|'context-menu'} args.trigger
|
||||
* @param {MouseEvent} [args.event]
|
||||
* @param {HTMLElement} [args.element]
|
||||
* @param {Function} [args.pathModifier]
|
||||
* @param {string} [args.selectedComponent] - Name of the selected component
|
||||
*/
|
||||
function postOpenToParent({ editor, pathToSource, url, trigger, event, element, pathModifier, selectedComponent }) {
|
||||
try {
|
||||
const el = element || (event && event.target instanceof HTMLElement ? event.target : null)
|
||||
|
||||
// Get all component instances for the clicked element
|
||||
const allComponents = el ? getComponentInstances(el, pathModifier) : []
|
||||
|
||||
// Find the selected component in the list (or use the first one)
|
||||
const selected = selectedComponent
|
||||
? allComponents.find(comp => comp.name === selectedComponent)
|
||||
: allComponents.find(comp => comp.pathToSource === pathToSource) || allComponents[0]
|
||||
|
||||
const elementInfo = el
|
||||
? {
|
||||
tag: el.tagName?.toLowerCase?.() || undefined,
|
||||
id: el.id || undefined,
|
||||
className:
|
||||
typeof el.className === 'string'
|
||||
? el.className
|
||||
: String(el.className || ''),
|
||||
role: el.getAttribute('role') || undefined,
|
||||
dataset: { ...el.dataset },
|
||||
}
|
||||
: undefined
|
||||
|
||||
const message = {
|
||||
source: MESSAGE_SOURCE,
|
||||
version: MESSAGE_VERSION,
|
||||
type: 'open-in-editor',
|
||||
payload: {
|
||||
selected: selected ? {
|
||||
editor,
|
||||
pathToSource: selected.pathToSource,
|
||||
url,
|
||||
name: selected.name,
|
||||
props: selected.props,
|
||||
source: selected.source
|
||||
} : {
|
||||
editor,
|
||||
pathToSource,
|
||||
url,
|
||||
name: selectedComponent || 'Unknown',
|
||||
props: {},
|
||||
source: {}
|
||||
},
|
||||
components: allComponents,
|
||||
trigger,
|
||||
coords: event
|
||||
? { x: event.clientX ?? undefined, y: event.clientY ?? undefined }
|
||||
: undefined,
|
||||
clickedElement: elementInfo,
|
||||
},
|
||||
}
|
||||
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.parent &&
|
||||
window.parent !== window &&
|
||||
typeof window.parent.postMessage === 'function'
|
||||
) {
|
||||
window.parent.postMessage(message, '*') // dev-only, permissive
|
||||
}
|
||||
} catch (err) {
|
||||
// Never break product flows due to messaging
|
||||
console.warn('[click-to-component] postMessage failed', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Props} props
|
||||
*/
|
||||
@@ -185,6 +304,19 @@ export function ClickToComponent({ editor = 'vscode', port, pathModifier }) {
|
||||
})
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
// Notify parent window via postMessage
|
||||
postOpenToParent({
|
||||
editor,
|
||||
pathToSource: path,
|
||||
url,
|
||||
trigger: 'alt-click',
|
||||
event,
|
||||
element: target,
|
||||
pathModifier,
|
||||
selectedComponent: getDisplayNameForInstance(instance)
|
||||
})
|
||||
|
||||
window.location.assign(url)
|
||||
|
||||
setState(State.IDLE)
|
||||
@@ -202,13 +334,23 @@ export function ClickToComponent({ editor = 'vscode', port, pathModifier }) {
|
||||
pathToSource: returnValue,
|
||||
})
|
||||
|
||||
// Notify parent window via postMessage
|
||||
postOpenToParent({
|
||||
editor,
|
||||
pathToSource: returnValue,
|
||||
url,
|
||||
trigger: 'context-menu',
|
||||
element: target,
|
||||
pathModifier,
|
||||
})
|
||||
|
||||
window.location.assign(url)
|
||||
}
|
||||
|
||||
setState(State.IDLE)
|
||||
setTrigger(null)
|
||||
},
|
||||
[editor]
|
||||
[editor, target, pathModifier]
|
||||
)
|
||||
|
||||
const onKeyDown = React.useCallback(
|
||||
@@ -329,6 +471,29 @@ export function ClickToComponent({ editor = 'vscode', port, pathModifier }) {
|
||||
[state, target, trigger]
|
||||
)
|
||||
|
||||
// Send ready message to parent when component mounts
|
||||
React.useEffect(function sendReadyMessage() {
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.parent &&
|
||||
window.parent !== window &&
|
||||
typeof window.parent.postMessage === 'function'
|
||||
) {
|
||||
try {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
source: MESSAGE_SOURCE,
|
||||
version: MESSAGE_VERSION,
|
||||
type: 'ready'
|
||||
},
|
||||
'*'
|
||||
)
|
||||
} catch (err) {
|
||||
console.warn('[click-to-component] ready message failed', err)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(
|
||||
function addEventListenersToWindow() {
|
||||
window.addEventListener('click', onClick, { capture: true })
|
||||
|
||||
105
parent-example.html
Normal file
105
parent-example.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Parent Page - Iframe Communication Example</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
#messages {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
border: 1px solid #ccc;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Parent Page - Click-to-Component Iframe Communication</h1>
|
||||
|
||||
<p>This page demonstrates iframe communication with the click-to-component tool.</p>
|
||||
|
||||
<iframe src="your-app-with-click-to-component.html" id="dev-iframe"></iframe>
|
||||
|
||||
<h2>Messages from iframe:</h2>
|
||||
<div id="messages"></div>
|
||||
|
||||
<script>
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
|
||||
function logMessage(message) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
messagesDiv.textContent += `[${timestamp}] ${JSON.stringify(message, null, 2)}\n\n`;
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
|
||||
// Listen for messages from the iframe
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = event.data;
|
||||
|
||||
// Only handle messages from our click-to-component tool
|
||||
if (!data || data.source !== 'click-to-component') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case 'ready':
|
||||
console.log('Click-to-Component iframe ready');
|
||||
logMessage({
|
||||
type: 'ready',
|
||||
message: 'Click-to-Component tool loaded and ready'
|
||||
});
|
||||
break;
|
||||
|
||||
case 'open-in-editor':
|
||||
const { selected, components, trigger, coords, clickedElement } = data.payload;
|
||||
|
||||
console.log('Open in editor:', data.payload);
|
||||
|
||||
logMessage({
|
||||
type: 'open-in-editor',
|
||||
trigger: trigger,
|
||||
selected: {
|
||||
component: selected.name,
|
||||
file: selected.pathToSource,
|
||||
editor: selected.editor
|
||||
},
|
||||
allComponents: components.map(c => ({
|
||||
name: c.name,
|
||||
file: c.pathToSource,
|
||||
props: Object.keys(c.props)
|
||||
})),
|
||||
clickPosition: coords,
|
||||
htmlElement: {
|
||||
tag: clickedElement?.tag,
|
||||
id: clickedElement?.id,
|
||||
className: clickedElement?.className
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown message type:', data.type);
|
||||
logMessage({
|
||||
type: 'unknown',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Example: You could also send messages TO the iframe if needed
|
||||
function sendMessageToIframe(message) {
|
||||
const iframe = document.getElementById('dev-iframe');
|
||||
if (iframe && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage(message, '*');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
21675
pnpm-lock.yaml
generated
21675
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user