@@ -6,7 +6,7 @@ import { getErrorMessage } from '@mathesar/utils/errors';
66
77import type SheetSelection from '../selection/SheetSelection' ;
88
9- import { MIME_MATHESAR_SHEET_CLIPBOARD , MIME_PLAIN_TEXT } from './constants' ;
9+ import { MIME_PLAIN_TEXT } from './constants' ;
1010import { type CopyingContext , getCopyContent } from './copy' ;
1111import { type PastingContext , paste } from './paste' ;
1212
@@ -24,6 +24,32 @@ function shouldHandleEvent(): boolean {
2424 return document . activeElement ?. hasAttribute ( 'data-active-cell' ) ?? false ;
2525}
2626
27+ /**
28+ * Convert TSV data to an HTML table with embedded structured data.
29+ *
30+ * This creates a proper HTML table for compatibility with spreadsheet
31+ * and document applications, while embedding Mathesar's structured data
32+ * in a data attribute for internal paste operations.
33+ *
34+ * @param tsv - Tab-separated values
35+ * @param structuredData - JSON-encoded structured cell data
36+ */
37+ function tsvToHtmlTable ( tsv : string , structuredData : string ) : string {
38+ const rows = tsv . split ( '\n' ) . filter ( ( row ) => row . length > 0 ) ;
39+ const tableRows = rows
40+ . map ( ( row ) => {
41+ const cells = row
42+ . split ( '\t' )
43+ . map ( ( cell ) => `<td>${ cell } </td>` )
44+ . join ( '' ) ;
45+ return `<tr>${ cells } </tr>` ;
46+ } )
47+ . join ( '' ) ;
48+ return `<html><head></head><body><table data-mathesar-content="${ encodeURIComponent (
49+ structuredData ,
50+ ) } "><tbody>${ tableRows } </tbody></table></body></html>`;
51+ }
52+
2753export class SheetClipboardHandler implements ClipboardHandler {
2854 private readonly deps : Dependencies ;
2955
@@ -47,11 +73,10 @@ export class SheetClipboardHandler implements ClipboardHandler {
4773 this . deps . showToastInfo (
4874 get ( _ ) ( 'copied_cells' , { values : { count : content . cellCount } } ) ,
4975 ) ;
76+
5077 event . clipboardData . setData ( MIME_PLAIN_TEXT , content . tsv ) ;
51- event . clipboardData . setData (
52- MIME_MATHESAR_SHEET_CLIPBOARD ,
53- content . structured ,
54- ) ;
78+ const htmlContent = tsvToHtmlTable ( content . tsv , content . structured ) ;
79+ event . clipboardData . setData ( 'text/html' , htmlContent ) ;
5580 } catch ( e ) {
5681 this . deps . showToastError ( getErrorMessage ( e ) ) ;
5782 }
@@ -67,4 +92,72 @@ export class SheetClipboardHandler implements ClipboardHandler {
6792 this . deps . showToastError ( getErrorMessage ( e ) ) ;
6893 }
6994 }
95+
96+ /**
97+ * Imperative copy method for use in context menus and other UI actions.
98+ * Uses the modern Clipboard API to write to the clipboard.
99+ */
100+ async copy ( ) : Promise < void > {
101+ try {
102+ const selection = get ( this . deps . selection ) ;
103+ const content = getCopyContent ( selection , this . deps . copyingContext ) ;
104+ const htmlContent = tsvToHtmlTable ( content . tsv , content . structured ) ;
105+
106+ const clipboardItems = [
107+ new ClipboardItem ( {
108+ [ MIME_PLAIN_TEXT ] : new Blob ( [ content . tsv ] , {
109+ type : MIME_PLAIN_TEXT ,
110+ } ) ,
111+ 'text/html' : new Blob ( [ htmlContent ] , {
112+ type : 'text/html' ,
113+ } ) ,
114+ } ) ,
115+ ] ;
116+
117+ await navigator . clipboard . write ( clipboardItems ) ;
118+
119+ this . deps . showToastInfo (
120+ get ( _ ) ( 'copied_cells' , { values : { count : content . cellCount } } ) ,
121+ ) ;
122+ } catch ( e ) {
123+ this . deps . showToastError ( getErrorMessage ( e ) ) ;
124+ }
125+ }
126+
127+ /**
128+ * Imperative paste method for use in context menus and other UI actions.
129+ * Uses the modern Clipboard API to read from the clipboard.
130+ */
131+ async paste ( ) : Promise < void > {
132+ const context = this . deps . pastingContext ;
133+ if ( ! context ) return ;
134+
135+ try {
136+ const clipboardItems = await navigator . clipboard . read ( ) ;
137+ const item = clipboardItems [ 0 ] ;
138+ if ( ! item ) return ;
139+
140+ let htmlText = '' ;
141+ if ( item . types . includes ( 'text/html' ) ) {
142+ const htmlBlob = await item . getType ( 'text/html' ) ;
143+ htmlText = await htmlBlob . text ( ) ;
144+ }
145+
146+ let plainText = '' ;
147+ if ( item . types . includes ( MIME_PLAIN_TEXT ) ) {
148+ const textBlob = await item . getType ( MIME_PLAIN_TEXT ) ;
149+ plainText = await textBlob . text ( ) ;
150+ }
151+
152+ const dataTransfer = new DataTransfer ( ) ;
153+ if ( htmlText ) {
154+ dataTransfer . setData ( 'text/html' , htmlText ) ;
155+ }
156+ dataTransfer . setData ( MIME_PLAIN_TEXT , plainText ) ;
157+
158+ await paste ( dataTransfer , this . deps . selection , context ) ;
159+ } catch ( e ) {
160+ this . deps . showToastError ( getErrorMessage ( e ) ) ;
161+ }
162+ }
70163}
0 commit comments