@@ -203,7 +203,8 @@ export function decodeHTML(tableEl: HTMLTableElement): string[][] | undefined {
203203 return result ;
204204}
205205
206- function escape ( str : string ) : string {
206+ function escape ( str : string , actuallyEscape : boolean ) : string {
207+ if ( ! actuallyEscape ) return str ;
207208 if ( / [ \t \n " , ] / . test ( str ) ) {
208209 str = `"${ str . replace ( / " / g, '""' ) } "` ;
209210 }
@@ -229,24 +230,30 @@ const formatBoolean = (val: boolean | BooleanEmpty | BooleanIndeterminate): stri
229230 }
230231} ;
231232
232- export function formatCell ( cell : GridCell , index : number , raw : boolean , columnIndexes : readonly number [ ] ) {
233+ export function formatCell (
234+ cell : GridCell ,
235+ index : number ,
236+ raw : boolean ,
237+ columnIndexes : readonly number [ ] ,
238+ escapeValues : boolean
239+ ) {
233240 const colIndex = columnIndexes [ index ] ;
234241 if ( cell . span !== undefined && cell . span [ 0 ] !== colIndex ) return "" ;
235242 if ( cell . copyData !== undefined ) {
236- return escape ( cell . copyData ) ;
243+ return escape ( cell . copyData , escapeValues ) ;
237244 }
238245 switch ( cell . kind ) {
239246 case GridCellKind . Text :
240247 case GridCellKind . Number :
241- return escape ( raw ? cell . data ?. toString ( ) ?? "" : cell . displayData ) ;
248+ return escape ( raw ? cell . data ?. toString ( ) ?? "" : cell . displayData , escapeValues ) ;
242249 case GridCellKind . Markdown :
243250 case GridCellKind . RowID :
244251 case GridCellKind . Uri :
245- return escape ( cell . data ) ;
252+ return escape ( cell . data , escapeValues ) ;
246253 case GridCellKind . Image :
247254 case GridCellKind . Bubble :
248255 if ( cell . data . length === 0 ) return "" ;
249- return cell . data . reduce ( ( pv , cv ) => `${ escape ( pv ) } ,${ escape ( cv ) } ` ) ;
256+ return cell . data . reduce ( ( pv , cv ) => `${ escape ( pv , escapeValues ) } ,${ escape ( cv , escapeValues ) } ` ) ;
250257 case GridCellKind . Boolean :
251258 return formatBoolean ( cell . data ) ;
252259 case GridCellKind . Loading :
@@ -255,16 +262,18 @@ export function formatCell(cell: GridCell, index: number, raw: boolean, columnIn
255262 return raw ? "" : "************" ;
256263 case GridCellKind . Drilldown :
257264 if ( cell . data . length === 0 ) return "" ;
258- return cell . data . map ( i => i . text ) . reduce ( ( pv , cv ) => `${ escape ( pv ) } ,${ escape ( cv ) } ` ) ;
265+ return cell . data
266+ . map ( i => i . text )
267+ . reduce ( ( pv , cv ) => `${ escape ( pv , escapeValues ) } ,${ escape ( cv , escapeValues ) } ` ) ;
259268 case GridCellKind . Custom :
260- return escape ( cell . copyData ) ;
269+ return escape ( cell . copyData , escapeValues ) ;
261270 default :
262271 assertNever ( cell , `A cell was passed with an invalid kind: ${ ( cell as any ) . kind } ` ) ;
263272 }
264273}
265274
266275export function formatForCopy ( cells : readonly ( readonly GridCell [ ] ) [ ] , columnIndexes : readonly number [ ] ) : string {
267- return cells . map ( row => row . map ( ( a , b ) => formatCell ( a , b , false , columnIndexes ) ) . join ( "\t" ) ) . join ( "\n" ) ;
276+ return cells . map ( row => row . map ( ( a , b ) => formatCell ( a , b , false , columnIndexes , true ) ) . join ( "\t" ) ) . join ( "\n" ) ;
268277}
269278
270279export function copyToClipboard (
@@ -274,7 +283,51 @@ export function copyToClipboard(
274283) {
275284 const str = formatForCopy ( cells , columnIndexes ) ;
276285
277- if ( window . navigator . clipboard ?. write !== undefined || e !== undefined ) {
286+ const styleTag = `<style type="text/css"><!--br {mso-data-placement:same-cell;}--></style>` ;
287+
288+ // eslint-disable-next-line unicorn/consistent-function-scoping
289+ const copyWithWriteText = ( s : string ) => {
290+ void window . navigator . clipboard ?. writeText ( s ) ;
291+ } ;
292+
293+ const copyWithWrite = ( s : string , html : string ) : boolean => {
294+ if ( window . navigator . clipboard ?. write === undefined ) return false ;
295+ void window . navigator . clipboard . write ( [
296+ new ClipboardItem ( {
297+ // eslint-disable-next-line sonarjs/no-duplicate-string
298+ "text/plain" : new Blob ( [ s ] , { type : "text/plain" } ) ,
299+ "text/html" : new Blob ( [ `${ styleTag } <table>${ html } </table>` ] , {
300+ type : "text/html" ,
301+ } ) ,
302+ } ) ,
303+ ] ) ;
304+ return true ;
305+ } ;
306+
307+ const copyWithClipboardData = ( s : string , html : string ) => {
308+ try {
309+ if ( e === undefined || e . clipboardData === null ) throw new Error ( "No clipboard data" ) ;
310+
311+ // The following formatting for the `formattedHtml` variable ensures that when pasting,
312+ // spaces are preserved in both Google Sheets and Excel. This is done by:
313+ // 1. Replacing tabs with four spaces for consistency. Also google sheets disallows any tabs.
314+ // 2. Wrapping each space with a span element to prevent them from being collapsed or ignored during the
315+ // paste operation.
316+ const formattedHtml = `${ styleTag } <table>${ html
317+ . replace ( / \t / g, " " )
318+ . replace ( / / g, "<span> </span>" ) } </table>`;
319+
320+ // This might fail if we had to await the thunk
321+ e ?. clipboardData ?. setData ( "text/plain" , s ) ;
322+ e ?. clipboardData ?. setData ( "text/html" , formattedHtml ) ;
323+ } catch {
324+ if ( ! copyWithWrite ( s , html ) ) {
325+ copyWithWriteText ( s ) ;
326+ }
327+ }
328+ } ;
329+
330+ if ( window . navigator . clipboard ?. write !== undefined || e ?. clipboardData !== undefined ) {
278331 const rootEl = document . createElement ( "tbody" ) ;
279332
280333 for ( const row of cells ) {
@@ -288,31 +341,16 @@ export function copyToClipboard(
288341 link . innerText = cell . data ;
289342 cellEl . append ( link ) ;
290343 } else {
291- cellEl . innerText = formatCell ( cell , i , true , columnIndexes ) ;
344+ cellEl . innerText = formatCell ( cell , i , true , columnIndexes , false ) ;
292345 }
293346 rowEl . append ( cellEl ) ;
294347 }
295348
296349 rootEl . append ( rowEl ) ;
297350 }
298- if ( window . navigator . clipboard ?. write !== undefined ) {
299- void window . navigator . clipboard . write ( [
300- new ClipboardItem ( {
301- "text/plain" : new Blob ( [ str ] , { type : "text/plain" } ) ,
302- "text/html" : new Blob ( [ `<table>${ rootEl . outerHTML } </table>` ] , { type : "text/html" } ) ,
303- } ) ,
304- ] ) ;
305- } else if ( e !== undefined && e ?. clipboardData !== null ) {
306- try {
307- // This might fail if we had to await the thunk
308- e . clipboardData . setData ( "text/plain" , str ) ;
309- e . clipboardData . setData ( "text/html" , `<table>${ rootEl . outerHTML } </table>` ) ;
310- } catch {
311- void window . navigator . clipboard ?. writeText ( str ) ;
312- }
313- }
351+ void copyWithClipboardData ( str , rootEl . outerHTML ) ;
314352 } else {
315- void window . navigator . clipboard ?. writeText ( str ) ;
353+ void copyWithWriteText ( str ) ;
316354 }
317355
318356 e ?. preventDefault ( ) ;
0 commit comments