All files / views/webviews webview-hooks.ts

0% Statements 0/44
0% Branches 0/29
0% Functions 0/14
0% Lines 0/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189                                                                                                                                                                                                                                                                                                                                                                                         
import { useEffect, useRef, useCallback, useState } from 'react';
import { WebviewMessaging, WebviewMessage } from './webview-messaging';
import { WebviewHost } from './webview-host';
 
/**
 * React hook for webview messaging. Provides the full messaging interface including
 * `postMessage`, `onMessage`, and `executeCommand`.
 * 
 * @example
 * ```tsx
 * function MyComponent() {
 *   const messaging = useWebviewMessaging<MyMessages>(message => {
 *     if (message.id === 'DataLoaded') {
 *       setData(message.data);
 *     }
 *   });
 * 
 *   const handleClick = () => {
 *     messaging?.postMessage({ id: 'LoadData' });
 *   };
 * }
 * ```
 * 
 * @param onMessage Callback invoked when a message is received from the extension host.
 * @param messaging Optional custom messaging instance. If not provided, uses WebviewHost (must be in webview context).
 * @returns The WebviewMessaging interface, or undefined if not in a webview context and no messaging was provided.
 */
export function useWebviewMessaging<M extends WebviewMessage>(
	onMessage?: (message: M) => void,
	messaging?: WebviewMessaging<M>
): WebviewMessaging<M> | undefined {
	const messagingRef = useRef<WebviewMessaging<M> | undefined>(
		messaging ?? (WebviewHost.isAvailable() ? WebviewHost.getMessaging<M>() : undefined)
	);
 
	useEffect(() => {
		if (onMessage && messagingRef.current) {
			const cleanup = messagingRef.current.onMessage(onMessage);
			// If onMessage returns a cleanup function, use it
			if (typeof cleanup === 'function') {
				return cleanup;
			}
		}
	}, [onMessage]);
 
	return messagingRef.current;
}
 
/**
 * React hook for webview state persistence. Wraps WebviewHost.getState/setState
 * and provides React-style state management that persists across webview lifecycle.
 * 
 * @example
 * ```tsx
 * function MyComponent() {
 *   const [state, setState] = useWebviewState({ count: 0 });
 * 
 *   return (
 *     <button onClick={() => setState({ count: state.count + 1 })}>
 *       Count: {state.count}
 *     </button>
 *   );
 * }
 * ```
 * 
 * @param initialState The initial state to use if no persisted state exists.
 * @returns A tuple of [state, setState] similar to React's useState.
 */
export function useWebviewState<T>(initialState: T): [T, (newState: T | ((prev: T) => T)) => void] {
	const [state, setStateInternal] = useState<T>(() => {
		const persisted = WebviewHost.getState();
		return persisted ?? initialState;
	});
 
	const setState = useCallback((newState: T | ((prev: T) => T)) => {
		setStateInternal(prev => {
			const next = typeof newState === 'function'
				? (newState as (prev: T) => T)(prev)
				: newState;
			WebviewHost.setState(next);
			return next;
		});
	}, []);
 
	return [state, setState];
}
 
/**
 * React hook for managing VS Code element refs with automatic event listener cleanup.
 * Use this for vscode-elements web components that emit custom events.
 * 
 * @example
 * ```tsx
 * function MyComponent() {
 *   const [selectedIndex, setSelectedIndex] = useState(0);
 *   
 *   const tabsRef = useVscodeElementRef<VscodeTabs, { selectedIndex: number }>(
 *     'vsc-tabs-select',
 *     (element, event) => setSelectedIndex(event.detail.selectedIndex)
 *   );
 * 
 *   return <vscode-tabs ref={tabsRef}>...</vscode-tabs>;
 * }
 * ```
 * 
 * @param eventName The event name to listen for.
 * @param onEvent Callback invoked when the event fires.
 * @returns A ref callback to pass to the element's ref prop.
 */
export function useVscodeElementRef<E extends HTMLElement, V = any>(
	eventName: string,
	onEvent: (element: E, event: CustomEvent<V>) => void
): (element: E | null) => void {
	const elementRef = useRef<E | null>(null);
	const handlerRef = useRef(onEvent);
 
	// Keep handler ref up to date
	useEffect(() => {
		handlerRef.current = onEvent;
	}, [onEvent]);
 
	const refCallback = useCallback((element: E | null) => {
		const handler = (event: Event) => {
			if (elementRef.current) {
				handlerRef.current(elementRef.current, event as CustomEvent<V>);
			}
		};
 
		// Cleanup previous element
		if (elementRef.current) {
			elementRef.current.removeEventListener(eventName, handler);
		}
 
		elementRef.current = element;
 
		// Setup new element
		if (element) {
			element.addEventListener(eventName, handler);
		}
 
		// Return cleanup for when component unmounts
		return () => {
			if (elementRef.current) {
				elementRef.current.removeEventListener(eventName, handler);
			}
		};
	}, [eventName]);
 
	return refCallback;
}
 
/**
 * React hook for injecting stylesheets into the document head.
 * Automatically cleans up stylesheets when the component unmounts.
 * 
 * @example
 * ```tsx
 * import styles from './my-component.css';
 * 
 * function MyComponent() {
 *   useStylesheet('my-component-styles', styles);
 *   return <div>...</div>;
 * }
 * ```
 * 
 * @param id Unique ID for the stylesheet element.
 * @param content CSS content to inject.
 * @param cleanup Whether to remove the stylesheet on unmount. Defaults to false.
 */
export function useStylesheet(id: string, content: string, cleanup = false): void {
	useEffect(() => {
		if (!document.getElementById(id)) {
			const style = document.createElement('style');
			style.id = id;
			style.textContent = content;
			document.head.appendChild(style);
		}
 
		return () => {
			if (cleanup) {
				const style = document.getElementById(id);
				if (style) {
					style.remove();
				}
			}
		};
	}, [id, content, cleanup]);
}