import type { ContextMenuItem } from "@t3tools/contracts"; export type ExternalLinkContextMenuAction = "open-in-preview" | "open-external" | "copy-link"; export type ExternalLinkContextMenuFailureOperation = | "show-link-context-menu" | "open-link-in-preview" | "open-link-external" | "copy-link"; const FAILURE_OPERATION_BY_ACTION = { "open-in-preview": "open-link-in-preview", "open-external": "open-link-external", "copy-link": "copy-link", } as const satisfies Record; const EXTERNAL_LINK_CONTEXT_MENU_ITEMS = [ { id: "open-in-preview", label: "Open in integrated browser" }, { id: "open-external", label: "Open in system browser" }, { id: "copy-link", label: "Copy Link" }, ] as const satisfies readonly ContextMenuItem[]; /** * The integrated browser is not always there to offer — it needs a thread to open beside and a * runtime that can show it — but the other two answers hold wherever a link does. Dropping the * whole menu with the one item that cannot be honoured is what left a right-click on a link * showing the platform's cut-and-paste menu instead of a way to copy the link. */ export function externalLinkContextMenuItems(options: { readonly canOpenInPreview: boolean; }): readonly ContextMenuItem[] { return options.canOpenInPreview ? EXTERNAL_LINK_CONTEXT_MENU_ITEMS : EXTERNAL_LINK_CONTEXT_MENU_ITEMS.filter((item) => item.id !== "open-in-preview"); } interface ShowExternalLinkContextMenuOptions { readonly href: string; readonly position: { readonly x: number; readonly y: number }; /** Absent means yes, which is what every caller before the browser could be missing meant. */ readonly canOpenInPreview?: boolean; readonly showContextMenu: ( items: readonly ContextMenuItem[], position: { readonly x: number; readonly y: number }, ) => Promise; readonly openInPreview: (href: string) => Promise; readonly openExternal: (href: string) => Promise; readonly copyLink: (href: string) => Promise; readonly reportFailure: ( operation: ExternalLinkContextMenuFailureOperation, cause: unknown, ) => void; } export function resolveExternalWebLinkHost(href: string | undefined): string | null { if (!href) return null; try { const url = new URL(href); if (url.protocol !== "http:" && url.protocol !== "https:") return null; return url.hostname || null; } catch { return null; } } export async function showExternalLinkContextMenu({ href, position, canOpenInPreview = true, showContextMenu, openInPreview, openExternal, copyLink, reportFailure, }: ShowExternalLinkContextMenuOptions): Promise { let action: ExternalLinkContextMenuAction | null; try { action = await showContextMenu(externalLinkContextMenuItems({ canOpenInPreview }), position); } catch (cause) { reportFailure("show-link-context-menu", cause); return; } try { if (action === "open-in-preview") { await openInPreview(href); } else if (action === "open-external") { await openExternal(href); } else if (action === "copy-link") { await copyLink(href); } } catch (cause) { if (action) reportFailure(FAILURE_OPERATION_BY_ACTION[action], cause); } }