mirror of
https://github.com/vercel/streamdown.git
synced 2026-09-19 02:18:37 +08:00
d3ed120364
* Initial extraction * Split up files * Misc cleanup * Break apart test file * Add vitest/coverage-v8 * Increase coverage to 99.63% * Fix cognitive complexity issues * Run Ultracite * Delete parse-incomplete-markdown.ts * Add postbuild script for use client directive * Update README.md * Add default export * Update docs * Rename package * Create wicked-badgers-decide.md * Fix remend tests * Fix remend benchmarks * Fix remaining Streamdown tests * Update underscore-bug.test.tsx * Update test.yml * Update test.yml
130 lines
3.7 KiB
TypeScript
130 lines
3.7 KiB
TypeScript
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
import { useContext, useEffect, useRef, useState } from "react";
|
|
import { StreamdownContext } from "../../index";
|
|
import { cn } from "../utils";
|
|
import {
|
|
extractTableDataFromElement,
|
|
tableDataToCSV,
|
|
tableDataToTSV,
|
|
} from "./utils";
|
|
|
|
export type TableCopyDropdownProps = {
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
onCopy?: (format: "csv" | "tsv") => void;
|
|
onError?: (error: Error) => void;
|
|
timeout?: number;
|
|
};
|
|
|
|
export const TableCopyDropdown = ({
|
|
children,
|
|
className,
|
|
onCopy,
|
|
onError,
|
|
timeout = 2000,
|
|
}: TableCopyDropdownProps) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [isCopied, setIsCopied] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
const timeoutRef = useRef(0);
|
|
const { isAnimating } = useContext(StreamdownContext);
|
|
|
|
const copyTableData = async (format: "csv" | "tsv") => {
|
|
if (typeof window === "undefined" || !navigator?.clipboard?.write) {
|
|
onError?.(new Error("Clipboard API not available"));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const tableWrapper = dropdownRef.current?.closest(
|
|
'[data-streamdown="table-wrapper"]'
|
|
);
|
|
const tableElement = tableWrapper?.querySelector(
|
|
"table"
|
|
) as HTMLTableElement;
|
|
|
|
if (!tableElement) {
|
|
onError?.(new Error("Table not found"));
|
|
return;
|
|
}
|
|
|
|
const tableData = extractTableDataFromElement(tableElement);
|
|
const content =
|
|
format === "csv"
|
|
? tableDataToCSV(tableData)
|
|
: tableDataToTSV(tableData);
|
|
|
|
const clipboardItemData = new ClipboardItem({
|
|
"text/plain": new Blob([content], { type: "text/plain" }),
|
|
"text/html": new Blob([tableElement.outerHTML], {
|
|
type: "text/html",
|
|
}),
|
|
});
|
|
|
|
await navigator.clipboard.write([clipboardItemData]);
|
|
setIsCopied(true);
|
|
setIsOpen(false);
|
|
onCopy?.(format);
|
|
timeoutRef.current = window.setTimeout(() => setIsCopied(false), timeout);
|
|
} catch (error) {
|
|
onError?.(error as Error);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(event.target as Node)
|
|
) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
window.clearTimeout(timeoutRef.current);
|
|
};
|
|
}, []);
|
|
|
|
const Icon = isCopied ? CheckIcon : CopyIcon;
|
|
|
|
return (
|
|
<div className="relative" ref={dropdownRef}>
|
|
<button
|
|
className={cn(
|
|
"cursor-pointer p-1 text-muted-foreground transition-all hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
className
|
|
)}
|
|
disabled={isAnimating}
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
title="Copy table"
|
|
type="button"
|
|
>
|
|
{children ?? <Icon size={14} />}
|
|
</button>
|
|
{isOpen && (
|
|
<div className="absolute top-full right-0 z-10 mt-1 min-w-[120px] overflow-hidden rounded-md border border-border bg-background shadow-lg">
|
|
<button
|
|
className="w-full px-3 py-2 text-left text-sm transition-colors hover:bg-muted/40"
|
|
onClick={() => copyTableData("csv")}
|
|
title="Copy table as CSV"
|
|
type="button"
|
|
>
|
|
CSV
|
|
</button>
|
|
<button
|
|
className="w-full px-3 py-2 text-left text-sm transition-colors hover:bg-muted/40"
|
|
onClick={() => copyTableData("tsv")}
|
|
title="Copy table as TSV"
|
|
type="button"
|
|
>
|
|
TSV
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|