Skip to content

Commit 1784d95

Browse files
3.0 issue75 global copy paste (plotly#87)
* global copy/paste * fix tests * fix tests * update bundle * fix regression (partial) * fix regression, fix additional inserted line breaks
1 parent c2c17fe commit 1784d95

File tree

12 files changed

+100
-101
lines changed

12 files changed

+100
-101
lines changed

packages/dash-table/@Types/modules.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ declare module 'sheetclip' {
22
const value: {
33
prototype: {
44
parse: (text: string) => string[][];
5+
stringify: (arr: any[][]) => string;
56
}
67
};
78

packages/dash-table/CHANGELOG.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,12 @@
108108

109109
First click selects the cell's content and will cause user input to override the cell content.
110110
Second click into the cell will remove the selection and position the cursor accordingly.
111-
111+
112112
## RC14 - Empty dropdown setting value regression fix
113113

114114
Issue: https://github.com/plotly/dash-table/issues/83
115+
116+
## RC15 - Global copy/paste (through browser menu), incorrect pasted data fix
117+
118+
Issue: https://github.com/plotly/dash-table/issues/75
119+
Issue: https://github.com/plotly/dash-table/issues/88

packages/dash-table/dash_table/bundle.js

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/dash-table/dash_table/demo.js

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/dash-table/dash_table/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dash-table",
3-
"version": "3.0.0rc14",
3+
"version": "3.0.0rc15",
44
"description": "Dash table",
55
"main": "build/index.js",
66
"scripts": {

packages/dash-table/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dash-table",
3-
"version": "3.0.0rc14",
3+
"version": "3.0.0rc15",
44
"description": "Dash table",
55
"main": "build/index.js",
66
"scripts": {

packages/dash-table/src/core/Clipboard.ts

+4-34
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,13 @@ export default class Clipboard {
33
private static value: string;
44
/*#endif*/
55

6-
public static set(value: string): void {
6+
public static set(_ev: any, value: string): void {
77
/*#if TEST_COPY_PASTE*/
88
Clipboard.value = value;
9+
/*#else*/
10+
_ev.clipboardData.setData('text/plain', value);
11+
_ev.preventDefault();
912
/*#endif*/
10-
11-
const el = document.createElement('textarea');
12-
el.value = value;
13-
14-
// (Adapted from https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f)
15-
// Make it readonly to be tamper-proof
16-
el.setAttribute('readonly', '');
17-
// el.style.position = 'absolute';
18-
// Move outside the screen to make it invisible
19-
// el.style.left = '-9999px';
20-
// Append the <textarea> element to the HTML document
21-
document.body.appendChild(el);
22-
23-
// Check if there is any content selected previously
24-
let selected;
25-
if (document.getSelection().rangeCount > 0) {
26-
// Store selection if found
27-
selected = document.getSelection().getRangeAt(0);
28-
}
29-
30-
// Select the <textarea> content
31-
el.select();
32-
// Copy - only works as a result of a user action (e.g. click events)
33-
document.execCommand('copy');
34-
// Remove the <textarea> element
35-
document.body.removeChild(el);
36-
// If a selection existed before copying
37-
if (selected) {
38-
// Unselect everything on the HTML document
39-
document.getSelection().removeAllRanges();
40-
// Restore the original selection
41-
document.getSelection().addRange(selected);
42-
}
4313
}
4414

4515
public static get(_ev: ClipboardEvent) {

packages/dash-table/src/dash-table/components/Cell/index.tsx

+3-9
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,13 @@ export default class Cell extends Component<ICellProps, ICellState> {
9999
/>);
100100
}
101101

102-
private onPaste = (e: React.ClipboardEvent<Element>) => {
103-
const { onPaste } = this.propsWithDefaults;
104-
105-
onPaste(e);
106-
e.stopPropagation();
107-
}
108-
109102
private renderInput() {
110103
const {
111104
active,
112105
focused,
113106
onClick,
114-
onDoubleClick
107+
onDoubleClick,
108+
onPaste
115109
} = this.propsWithDefaults;
116110

117111
const classes = [
@@ -135,7 +129,7 @@ export default class Cell extends Component<ICellProps, ICellState> {
135129
onBlur={this.propagateChange}
136130
onChange={this.handleChange}
137131
onKeyDown={this.handleKeyDown}
138-
onPaste={this.onPaste}
132+
onPaste={onPaste}
139133
{...attributes}
140134
/>);
141135
}

packages/dash-table/src/dash-table/components/CellFactory.tsx

+4-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as R from 'ramda';
2-
import React from 'react';
2+
import React, { ClipboardEvent } from 'react';
33

44
import Cell from 'dash-table/components/Cell';
55
import { ICellFactoryOptions, SelectedCells } from 'dash-table/components/Table/props';
@@ -152,17 +152,8 @@ export default class CellFactory {
152152
});
153153
}
154154

155-
private handlePaste = (idx: number, i: number, e: any) => {
156-
const {
157-
is_focused,
158-
selected_cell
159-
} = this.props;
160-
161-
const selected = this.isCellSelected(selected_cell, idx, i);
162-
163-
if (!(selected && is_focused)) {
164-
e.preventDefault();
165-
}
155+
private handlePaste = (e: ClipboardEvent) => {
156+
e.preventDefault();
166157
}
167158

168159
private rowSelectCell(idx: number) {
@@ -273,7 +264,7 @@ export default class CellFactory {
273264
focused={!!is_focused}
274265
onClick={this.getEventHandler(this.handleClick, virtualIdx, index)}
275266
onDoubleClick={this.getEventHandler(this.handleDoubleClick, virtualIdx, index)}
276-
onPaste={this.getEventHandler(this.handlePaste, virtualIdx, index)}
267+
onPaste={this.handlePaste}
277268
onChange={this.getEventHandler(this.handleChange, realIdx, index)}
278269
property={column.id}
279270
selected={R.contains([virtualIdx, index + offset], selected_cell)}

packages/dash-table/src/dash-table/components/ControlledTable/index.tsx

+20-20
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { colIsEditable } from 'dash-table/components/derivedState';
55
import {
66
KEY_CODES,
77
isCtrlMetaKey,
8+
/*#if TEST_COPY_PASTE*/
89
isCtrlDown,
10+
/*#endif*/
911
isMetaKey,
1012
isNavKey
1113
} from 'dash-table/utils/unicode';
@@ -128,28 +130,27 @@ export default class ControlledTable extends Component<ControlledTableProps> {
128130
editable
129131
} = this.props;
130132

131-
Logger.warning(`handleKeyDown: ${e.key}`);
132-
133-
const ctrlDown = isCtrlDown(e);
133+
Logger.trace(`handleKeyDown: ${e.key}`);
134134

135135
// if this is the initial CtrlMeta keydown with no modifiers then pass
136136
if (isCtrlMetaKey(e.keyCode)) {
137137
return;
138138
}
139139

140-
// if paste event onPaste handler registered in Table jsx handles it
140+
/*#if TEST_COPY_PASTE*/
141+
const ctrlDown = isCtrlDown(e);
142+
141143
if (ctrlDown && e.keyCode === KEY_CODES.V) {
142-
/*#if TEST_COPY_PASTE*/
143144
this.onPaste({} as any);
144-
/*#endif*/
145+
e.preventDefault();
145146
return;
146147
}
147148

148-
// copy
149149
if (e.keyCode === KEY_CODES.C && ctrlDown && !is_focused) {
150-
this.onCopy(e);
150+
this.onCopy(e as any);
151151
return;
152152
}
153+
/*#endif*/
153154

154155
if (e.keyCode === KEY_CODES.ESCAPE) {
155156
setProps({ is_focused: false });
@@ -462,7 +463,7 @@ export default class ControlledTable extends Component<ControlledTableProps> {
462463
selected_cell
463464
);
464465

465-
TableClipboardHelper.toClipboard(noOffsetSelectedCells, columns, dataframe);
466+
TableClipboardHelper.toClipboard(e, noOffsetSelectedCells, columns, dataframe);
466467
this.$el.focus();
467468
}
468469

@@ -473,15 +474,14 @@ export default class ControlledTable extends Component<ControlledTableProps> {
473474
dataframe,
474475
editable,
475476
filtering_settings,
476-
is_focused,
477477
row_deletable,
478478
row_selectable,
479479
setProps,
480480
sorting_settings,
481481
virtual_dataframe_indices
482482
} = this.props;
483483

484-
if (is_focused || !editable) {
484+
if (!editable) {
485485
return;
486486
}
487487

@@ -491,6 +491,7 @@ export default class ControlledTable extends Component<ControlledTableProps> {
491491

492492
const noOffsetActiveCell: [number, number] = [active_cell[0], active_cell[1] - columnIndexOffset];
493493

494+
494495
const result = TableClipboardHelper.fromClipboard(
495496
e,
496497
noOffsetActiveCell,
@@ -595,10 +596,7 @@ export default class ControlledTable extends Component<ControlledTableProps> {
595596

596597
renderFragment = (cells: any[][] | null) => (
597598
cells ?
598-
(<table
599-
onPaste={this.onPaste}
600-
tabIndex={-1}
601-
>
599+
(<table tabIndex={-1}>
602600
<tbody>
603601
{cells.map(
604602
(row, idx) => <tr key={`row-${idx}`}>{row}</tr>)
@@ -673,12 +671,14 @@ export default class ControlledTable extends Component<ControlledTableProps> {
673671

674672
const grid = this.getFragments(n_fixed_columns, n_fixed_rows);
675673

676-
return (<div id={id}>
674+
return (<div
675+
id={id}
676+
onCopy={this.onCopy}
677+
onKeyDown={this.handleKeyDown}
678+
onPaste={this.onPaste}
679+
>
677680
<div className='dash-spreadsheet-container'>
678-
<div
679-
className={classes.join(' ')}
680-
onKeyDown={this.handleKeyDown}
681-
>
681+
<div className={classes.join(' ')}>
682682
{grid.map((row, rowIndex) => (<div
683683
key={`r${rowIndex}`}
684684
ref={`r${rowIndex}`}

packages/dash-table/src/dash-table/utils/TableClipboardHelper.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,23 @@ import { colIsEditable } from 'dash-table/components/derivedState';
88
import { ActiveCell, Columns, Dataframe, SelectedCells } from 'dash-table/components/Table/props';
99

1010
export default class TableClipboardHelper {
11-
public static toClipboard(selectedCells: SelectedCells, columns: Columns, dataframe: Dataframe) {
11+
public static toClipboard(e: any, selectedCells: SelectedCells, columns: Columns, dataframe: Dataframe) {
1212
const selectedRows = R.uniq(R.pluck(0, selectedCells).sort());
1313
const selectedCols: any = R.uniq(R.pluck(1, selectedCells).sort());
1414

15-
const value = R.slice(
15+
const df = R.slice(
1616
R.head(selectedRows) as any,
1717
R.last(selectedRows) as any + 1,
1818
dataframe
1919
).map(row =>
2020
R.props(selectedCols, R.props(R.pluck('id', columns) as any, row) as any)
21-
).map(row => R.values(row).join('\t')
22-
).join('\r\n');
21+
);
22+
23+
const value = SheetClip.prototype.stringify(df);
24+
25+
Logger.trace('TableClipboard -- set clipboard data: ', value);
2326

24-
Clipboard.set(value);
27+
Clipboard.set(e, value);
2528
}
2629

2730
public static fromClipboard(
@@ -34,7 +37,7 @@ export default class TableClipboardHelper {
3437
overflowRows: boolean = true
3538
): { dataframe: Dataframe, columns: Columns } | void {
3639
const text = Clipboard.get(ev);
37-
Logger.warning('clipboard data: ', text);
40+
Logger.trace('TableClipboard -- get clipboard data: ', text);
3841

3942
if (!text) {
4043
return;

packages/dash-table/tests/e2e/cypress/integration/copy_paste_test.ts

+45-10
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,57 @@ describe('copy paste', () => {
77
cy.visit('http://localhost:8082');
88
});
99

10-
it('can do BE roundtrip on cell modification', () => {
10+
it('can copy multiple rows', () => {
1111
DashTable.getCell(0, 0).click();
12-
DOM.focused.type(`10${Key.Enter}`);
13-
14-
DashTable
15-
.getCell(0, 0)
16-
.within(() => cy.get('.cell-value').should('have.html', '10'))
17-
.then(() => {
18-
DashTable.getCell(0, 1)
19-
.within(() => cy.get('.cell-value').should('have.html', 'MODIFIED'));
20-
});
12+
DOM.focused.type(Key.Shift, { release: false });
13+
DashTable.getCell(2, 0).click();
14+
15+
DOM.focused.type(`${Key.Meta}c`);
16+
DashTable.getCell(3, 0).click();
17+
DOM.focused.type(`${Key.Meta}v`);
18+
DashTable.getCell(0, 0).click();
19+
20+
for (let row = 0; row <= 2; ++row) {
21+
DashTable.getCell(row + 3, 0).within(() => cy.get('.cell-value').should('have.html', `${row}`));
22+
}
23+
});
24+
25+
it('can copy multiple rows and columns', () => {
26+
DashTable.getCell(0, 1).click();
27+
DOM.focused.type(Key.Shift, { release: false });
28+
DashTable.getCell(2, 2).click();
29+
30+
DOM.focused.type(`${Key.Meta}c`);
31+
DashTable.getCell(3, 1).click();
32+
DOM.focused.type(`${Key.Meta}v`);
33+
DashTable.getCell(0, 0).click();
34+
35+
for (let row = 0; row <= 2; ++row) {
36+
for (let column = 1; column <= 2; ++column) {
37+
let initialValue: string;
38+
39+
DashTable.getCell(row, column).within(() => cy.get('.cell-value').then($cells => initialValue = $cells[0].innerHTML));
40+
DashTable.getCell(row + 3, column).within(() => cy.get('.cell-value').should('have.html', initialValue));
41+
}
42+
}
2143
});
2244

2345
// Commenting this test as Cypress team is having issues with the copy/paste scenario
2446
// LINK: https://github.com/cypress-io/cypress/issues/2386
2547
describe('BE roundtrip on copy-paste', () => {
48+
it('on cell modification', () => {
49+
DashTable.getCell(0, 0).click();
50+
DOM.focused.type(`10${Key.Enter}`);
51+
52+
DashTable
53+
.getCell(0, 0)
54+
.within(() => cy.get('.cell-value').should('have.html', '10'))
55+
.then(() => {
56+
DashTable.getCell(0, 1)
57+
.within(() => cy.get('.cell-value').should('have.html', 'MODIFIED'));
58+
});
59+
});
60+
2661
it('with unsorted, unfiltered data', () => {
2762
DashTable.getCell(0, 0).click();
2863
DOM.focused.type(`${Key.Meta}c`);

0 commit comments

Comments
 (0)