diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte index 0cc338cb..069fb820 100644 --- a/packages/web/src/datagrid/DataGridCore.svelte +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -97,6 +97,15 @@ onClick: () => get(currentDataGrid).reconnect(), }); + registerCommand({ + id: 'dataGrid.copyToClipboard', + category: 'Data grid', + name: 'Copy to clipboard', + keyText: 'Ctrl+C', + enabledStore: derived(currentDataGrid, grid => grid != null), + onClick: () => get(currentDataGrid).copyToClipboard(), + }); + function getRowCountInfo(selectedCells, grider, realColumnUniqueNames, selectedRowData, allRowCount) { if (selectedCells.length > 1 && selectedCells.every(x => _.isNumber(x[0]) && _.isNumber(x[1]))) { let sum = _.sumBy(selectedCells, cell => { @@ -153,6 +162,7 @@ import { nullStore } from '../stores'; import memberStore from '../utility/memberStore'; import axios from '../utility/axios'; + import { copyTextToClipboard } from '../utility/clipboard'; export let loadNextData = undefined; export let grider = undefined; @@ -259,6 +269,23 @@ display.reload(); } + export function copyToClipboard() { + const cells = cellsToRegularCells(selectedCells); + const rowIndexes = _.sortBy(_.uniq(cells.map(x => x[0]))); + const lines = rowIndexes.map(rowIndex => { + let colIndexes = _.sortBy(cells.filter(x => x[0] == rowIndex).map(x => x[1])); + const rowData = grider.getRowData(rowIndex); + if (!rowData) return ''; + const line = colIndexes + .map(col => realColumnUniqueNames[col]) + .map(col => (rowData[col] == null ? '(NULL)' : rowData[col])) + .join('\t'); + return line; + }); + const text = lines.join('\r\n'); + copyTextToClipboard(text); + } + $: autofillMarkerCell = selectedCells && selectedCells.length > 0 && _.uniq(selectedCells.map(x => x[0])).length == 1 ? [_.max(selectedCells.map(x => x[0])), _.max(selectedCells.map(x => x[1]))] @@ -662,6 +689,26 @@ return [row, col]; } + function cellsToRegularCells(cells) { + cells = _.flatten( + cells.map(cell => { + if (cell[1] == 'header') { + return _.range(0, columnSizes.count).map(col => [cell[0], col]); + } + return [cell]; + }) + ); + cells = _.flatten( + cells.map(cell => { + if (cell[0] == 'header') { + return _.range(0, grider.rowCount).map(row => [row, cell[1]]); + } + return [cell]; + }) + ); + return cells.filter(isRegularCell); + } + const [inplaceEditorState, dispatchInsplaceEditor] = createReducer((state, action) => { switch (action.type) { case 'show': @@ -692,6 +739,7 @@ function createMenu() { return [ { command: 'dataGrid.refresh' }, + { command: 'dataGrid.copyToClipboard' }, { divider: true }, { command: 'dataGrid.save' }, { command: 'dataGrid.revertRowChanges' }, diff --git a/packages/web/src/utility/clipboard.js b/packages/web/src/utility/clipboard.js new file mode 100644 index 00000000..dabbe22d --- /dev/null +++ b/packages/web/src/utility/clipboard.js @@ -0,0 +1,56 @@ +export function copyTextToClipboard(text) { + const textArea = document.createElement('textarea'); + + // + // *** This styling is an extra step which is likely not required. *** + // + // Why is it here? To ensure: + // 1. the element is able to have focus and selection. + // 2. if element was to flash render it has minimal visual impact. + // 3. less flakyness with selection and copying which **might** occur if + // the textarea element is not visible. + // + // The likelihood is the element won't even render, not even a flash, + // so some of these are just precautions. However in IE the element + // is visible whilst the popup box asking the user for permission for + // the web page to copy to the clipboard. + // + + // Place in top-left corner of screen regardless of scroll position. + textArea.style.position = 'fixed'; + textArea.style.top = '0'; + textArea.style.left = '0'; + + // Ensure it has a small width and height. Setting to 1px / 1em + // doesn't work as this gives a negative w/h on some browsers. + textArea.style.width = '2em'; + textArea.style.height = '2em'; + + // We don't need padding, reducing the size if it does flash render. + textArea.style.padding = '0'; + + // Clean up any borders. + textArea.style.border = 'none'; + textArea.style.outline = 'none'; + textArea.style.boxShadow = 'none'; + + // Avoid flash of white box if rendered for any reason. + textArea.style.background = 'transparent'; + + textArea.value = text; + + document.body.appendChild(textArea); + + textArea.select(); + + try { + let successful = document.execCommand('copy'); + if (!successful) { + console.log('Failed copy to clipboard'); + } + } catch (err) { + console.log('Failed copy to clipboard: ' + err); + } + + document.body.removeChild(textArea); +}