@@ -14,7 +14,20 @@ const PAPA_CONFIG = {
1414
1515export default {
1616 methods : {
17- prepareValuesForCopy ( fields , rows , includeHeader ) {
17+ /**
18+ * Prepares the values of the given fields and rows for copying to the clipboard. It
19+ * returns both a text representation and a json representation of the data. The
20+ * text representation is a 2D array of strings, where each inner array represents a
21+ * row and each string represents a cell. The json representation is a 2D array of
22+ * values, where each inner array represents a row and each value represents a cell.
23+ * The json representation can contain rich values, such as objects or arrays, that
24+ * can be later used to paste rich data back into the grid.
25+ *
26+ * @param {Array } fields The fields to copy.
27+ * @param {Array } rows The rows to copy.
28+ * @param {boolean } includeHeader Whether to include the field names as the first
29+ */
30+ prepareValuesForCopy ( fields , rows , includeHeader = false ) {
1831 const textData = [ ]
1932 const jsonData = [ ]
2033 if ( includeHeader ) {
@@ -37,6 +50,18 @@ export default {
3750 }
3851 return { textData, jsonData }
3952 } ,
53+ /**
54+ * Prepares the given text data as an HTML table. If the text data only contains
55+ * a single cell, no HTML table is returned as it would conflict with tiptap.
56+ * The html data is used when pasting into external applications that support rich
57+ * clipboard data, such as Google Sheets or Excel. If the firstRowIsHeader is true,
58+ * the first row will be wrapped in <th> tags instead of <td> tags.
59+ *
60+ * @param {Array } textData The text data to prepare.
61+ * @param {boolean } firstRowIsHeader Whether the first row should be treated as a
62+ * header row.
63+ * @returns {string|null } The HTML table or null if no table is needed.
64+ */
4065 prepareHTMLData ( textData , firstRowIsHeader ) {
4166 const table = document . createElement ( 'table' )
4267 const tbody = document . createElement ( 'tbody' )
@@ -71,11 +96,29 @@ export default {
7196 { root : true }
7297 )
7398 } ,
74- async copySelectionToClipboard ( selectionPromise , includeHeader = false ) {
75- const { textData, jsonData } = await selectionPromise . then (
76- ( [ fields , rows ] ) =>
77- this . prepareValuesForCopy ( fields , rows , includeHeader )
78- )
99+ /**
100+ * Formats the given text and json data for the clipboard and stores the rich
101+ * representation in local storage. It returns both a tsv representation and an html
102+ * representation of the text data. The tsv representation is used for plain text
103+ * clipboard data, the html representation is used when pasting into external
104+ * applications that support rich clipboard data, such as Google Sheets or Excel.
105+ * The json representation is stored in local storage to be able to paste rich
106+ * values back into the Baserow grid later. If the the stored version is the same as
107+ * the clipboard version, the rich values will be used when pasting instead of the
108+ * text values, so we can be more accurate (i.e. link row values, select options,
109+ * etc.)
110+ *
111+ * @param {Object } data An object containing the text and json data.
112+ * @param {Array } data.textData A 2D array of strings representing the text data.
113+ * @param {Array } data.jsonData A 2D array of values representing the json data.
114+ * @param {boolean } includeHeader Whether the copied data includes a header row.
115+ * @returns {Object } An object containing the tsv and html representation of the
116+ * text data. The html representation can be null if no html table is needed.
117+ */
118+ formatClipboardDataAndStoreRichCopy (
119+ { textData, jsonData } ,
120+ includeHeader = false
121+ ) {
79122 const tsvData = this . $papa . unparse ( textData , PAPA_CONFIG )
80123 const htmlData = this . prepareHTMLData ( textData , includeHeader )
81124 try {
@@ -85,43 +128,107 @@ export default {
85128 )
86129 } catch ( e ) {
87130 this . showCopyClipboardError ( )
131+ throw e
88132 }
133+ return { tsvData, htmlData }
134+ } ,
135+ /**
136+ * Copies the given selection to the clipboard. The selection is a promise that
137+ * resolves to an array containing the fields and rows to copy. The fields are
138+ * used to determine which columns to copy and in which order. The rows are the
139+ * actual data to copy. If includeHeader is true, the field names are included as
140+ * the first row of the copied data.
141+ *
142+ * @param {Promise } selectionPromise A promise that resolves to an array containing
143+ * the fields and rows to copy.
144+ * @param {boolean } includeHeader Whether to include the field names as the first
145+ * row of the copied data.
146+ */
147+ copySelectionToClipboard ( selectionPromise , includeHeader = false ) {
148+ this . $store . dispatch ( 'toast/setCopying' , true )
149+ const dataPromise = selectionPromise
150+ . then ( ( [ fields , rows ] ) => {
151+ const { textData, jsonData } = this . prepareValuesForCopy (
152+ fields ,
153+ rows ,
154+ includeHeader
155+ )
156+ this . $store . dispatch ( 'toast/setCopying' , false )
157+ return this . formatClipboardDataAndStoreRichCopy (
158+ { textData, jsonData } ,
159+ includeHeader
160+ )
161+ } )
162+ . catch ( ( error ) => {
163+ this . $store . dispatch ( 'toast/setCopying' , false )
164+ throw error
165+ } )
89166
90167 try {
91- await this . writeToClipboard ( tsvData , htmlData )
168+ this . writeToClipboard ( dataPromise )
92169 } catch ( e ) {
93170 if ( ! document . hasFocus ( ) ) {
94171 window . addEventListener (
95172 'focus' ,
96- ( ) => this . writeToClipboard ( tsvData , htmlData ) ,
173+ ( ) => this . writeToClipboard ( dataPromise ) ,
97174 { once : true }
98175 )
99176 } else {
100177 this . showCopyClipboardError ( )
101178 }
102179 }
103180 } ,
104- async writeToClipboard ( tsvData , htmlData ) {
181+ /**
182+ * Writes the given data to the clipboard. It tries to write both a plain text
183+ * representation and a html representation of the data if supported by the browser.
184+ * If the ClipboardItem API is not supported, it falls back to writing only the
185+ * plain text representation. If that is also not supported, it falls back to using
186+ * the rich clipboard utils that uses an older approach to write both a plain text
187+ * and html representation of the data.
188+ *
189+ * @param {Promise } dataPromise A promise that resolves to an object containing
190+ * the tsv and html representation of the data.
191+ */
192+ writeToClipboard ( dataPromise ) {
105193 if ( typeof ClipboardItem !== 'undefined' ) {
106- const clipboardConfig = {
107- 'text/plain' : new Blob ( [ tsvData ] , { type : 'text/plain' } ) ,
108- }
109- if ( htmlData ) {
110- clipboardConfig [ 'text/html' ] = new Blob ( [ htmlData ] , {
111- type : 'text/html' ,
112- } )
113- }
114- await navigator . clipboard . write ( [ new ClipboardItem ( clipboardConfig ) ] )
194+ const clipboardItem = new ClipboardItem ( {
195+ 'text/plain' : Promise . resolve ( dataPromise ) . then ( ( { tsvData } ) =>
196+ tsvData ? new Blob ( [ tsvData ] , { type : 'text/plain' } ) : null
197+ ) ,
198+ 'text/html' : Promise . resolve ( dataPromise ) . then ( ( { htmlData } ) =>
199+ htmlData ? new Blob ( [ htmlData ] , { type : 'text/html' } ) : null
200+ ) ,
201+ } )
202+ navigator . clipboard . write ( [ clipboardItem ] )
115203 } else if ( typeof navigator . clipboard ?. writeText !== 'undefined' ) {
116- await navigator . clipboard . writeText ( tsvData )
204+ navigator . clipboard . writeText (
205+ Promise . resolve ( dataPromise ) . then ( ( { tsvData } ) => tsvData )
206+ )
117207 } else {
118- const richClipboardConfig = { 'text/plain' : tsvData }
119- if ( htmlData ) {
120- richClipboardConfig [ 'text/html' ] = htmlData
208+ const richClipboardConfig = {
209+ 'text/plain' : Promise . resolve ( dataPromise ) . then (
210+ ( { tsvData } ) => tsvData || null
211+ ) ,
212+ 'text/html' : Promise . resolve ( dataPromise ) . then (
213+ ( { htmlData } ) => htmlData || null
214+ ) ,
121215 }
122216 setRichClipboard ( richClipboardConfig )
123217 }
124218 } ,
219+ /**
220+ * Extracts the clipboard data from the given paste event. It tries to extract
221+ * both a plain text representation and a json representation of the data. The
222+ * plain text representation is a 2D array of strings, where each inner array
223+ * represents a row and each string represents a cell. The json representation
224+ * is a 2D array of values, where each inner array represents a row and each
225+ * value represents a cell. The json representation can contain rich values,
226+ * such as objects or arrays, that can be later used to paste rich data back
227+ * into the grid, if the versions match.
228+ *
229+ * @param {Event } event The paste event.
230+ * @returns {Array } An array containing the text and json data.
231+ */
125232 async extractClipboardData ( event ) {
126233 const { textRawData, jsonRawData } = await getRichClipboard ( event )
127234 const { data : textData } = await this . $papa . parsePromise (
0 commit comments