/**
* Core Bridge - handles communication between VS Code extension and webview
* Export registerHandler() for custom handlers in main.ts
*/
interface VsCodeApi {
postMessage(message: any): void;
getState(): any;
setState(state: any): void;
}
declare function acquireVsCodeApi(): VsCodeApi;
const vscode = acquireVsCodeApi();
// Handler registry
type Handler = (payload: any) => any;
const handlers: Map<string, Handler> = new Map();
/**
* Register a command handler
*/
export function registerHandler(command: string, handler: Handler): void {
handlers.set(command, handler);
}
/**
* Register multiple handlers at once
*/
export function registerHandlers(handlerMap: Record<string, Handler>): void {
for (const [command, handler] of Object.entries(handlerMap)) {
handlers.set(command, handler);
}
}
/**
* Send response back to extension
*/
function sendResponse(id: string, success: boolean, data?: any, error?: string | null): void {
vscode.postMessage({ id, success, data, error });
}
// ============================================================
// BUILT-IN HANDLERS (common handlers, không cần sửa)
// ============================================================
function handleNavigate(payload: { route: string }): boolean {
try {
const route = payload.route;
const page = route.replace(/^[#/]+/, '') || 'home';
window.location.hash = page;
return true;
} catch (error) {
console.error('Navigate error:', error);
return false;
}
}
function handleClick(payload: { selector: string }): boolean {
try {
const element = document.querySelector(payload.selector) as HTMLElement;
if (element) {
element.click();
return true;
}
return false;
} catch (error) {
console.error('Click error:', error);
return false;
}
}
function handleInput(payload: { selector: string; value: string }): boolean {
try {
const element = document.querySelector(payload.selector) as HTMLInputElement | HTMLTextAreaElement;
if (element && (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA')) {
element.value = payload.value;
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
return false;
} catch (error) {
console.error('Input error:', error);
return false;
}
}
function isVisible(element: Element): boolean {
const style = window.getComputedStyle(element);
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0';
}
function getVisibleText(): string {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
let text = '';
let node: Node | null;
while (node = walker.nextNode()) {
const parent = node.parentElement;
if (parent && isVisible(parent)) {
const trimmed = node.textContent?.trim();
if (trimmed) text += trimmed + ' ';
}
}
return text.trim().substring(0, 5000);
}
function getForms(): Array<{ id: string; action: string; inputs: Array<any> }> {
const forms: Array<{ id: string; action: string; inputs: Array<any> }> = [];
document.querySelectorAll('form').forEach((form) => {
const inputs: Array<any> = [];
form.querySelectorAll('input, textarea, select').forEach((input: Element) => {
const el = input as HTMLInputElement;
inputs.push({
type: el.type || el.tagName.toLowerCase(),
name: el.name,
id: el.id,
placeholder: el.placeholder
});
});
forms.push({ id: form.id, action: form.action, inputs });
});
return forms;
}
function getLinks(): Array<{ text: string; href: string | null; id: string }> {
const links: Array<{ text: string; href: string | null; id: string }> = [];
document.querySelectorAll('a[href]').forEach((link: Element) => {
const anchor = link as HTMLAnchorElement;
links.push({
text: anchor.textContent?.trim().substring(0, 100) || '',
href: anchor.getAttribute('href'),
id: anchor.id
});
});
return links.slice(0, 50);
}
function handleGetState(): object {
return {
url: window.location.href,
hash: window.location.hash,
title: document.title,
readyState: document.readyState,
visibleText: getVisibleText(),
forms: getForms(),
links: getLinks()
};
}
function handleGetDom(): string {
return document.documentElement.outerHTML;
}
// Register built-in handlers
registerHandlers({
'navigate': handleNavigate,
'click': handleClick,
'input': handleInput,
'get_state': handleGetState,
'getState': handleGetState,
'getDom': handleGetDom,
});
// ============================================================
// MESSAGE LISTENER (core, không sửa)
// ============================================================
window.addEventListener('message', (event: MessageEvent) => {
const message = event.data;
const { type, id, payload } = message;
let result: any;
let success = true;
let error: string | null = null;
try {
const handler = handlers.get(type);
if (handler) {
result = handler(payload);
if (typeof result === 'boolean') {
success = result;
if (!result) error = `${type} failed`;
}
} else {
success = false;
error = 'Unknown command: ' + type;
}
} catch (e) {
success = false;
error = (e as Error).message;
}
sendResponse(id, success, result, error);
});
console.log('Web viewer Bridge initialized');