Skip to content

Commit 7527383

Browse files
authored
Merge pull request #2652 from Lxstr/feature/clipboard-html
Feature/clipboard-html
2 parents 2153a68 + 9469c58 commit 7527383

File tree

3 files changed

+85
-11
lines changed

3 files changed

+85
-11
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).
77
### Added
88
- [#2695](https://github.com/plotly/dash/pull/2695) Adds `triggered_id` to `dash_clientside.callback_context`. Fixes [#2692](https://github.com/plotly/dash/issues/2692)
99

10+
## Changed
11+
- [#2652](https://github.com/plotly/dash/pull/2652) dcc.Clipboard supports htm_content and triggers a copy to clipboard when n_clicks are changed
12+
1013
## [2.14.2] - 2023-11-27
1114

1215
## Fixed

Diff for: components/dash-core-components/src/components/Clipboard.react.js

+45-10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default class Clipboard extends React.Component {
1717
constructor(props) {
1818
super(props);
1919
this.copyToClipboard = this.copyToClipboard.bind(this);
20+
this.onClickHandler = this.onClickHandler.bind(this);
2021
this.copySuccess = this.copySuccess.bind(this);
2122
this.getTargetText = this.getTargetText.bind(this);
2223
this.loading = this.loading.bind(this);
@@ -26,6 +27,22 @@ export default class Clipboard extends React.Component {
2627
};
2728
}
2829

30+
onClickHandler() {
31+
this.props.setProps({n_clicks: this.props.n_clicks + 1});
32+
}
33+
34+
componentDidUpdate(prevProps) {
35+
// If the clicks has not changed, do nothing
36+
if (
37+
!this.props.n_clicks ||
38+
this.props.n_clicks === prevProps.n_clicks
39+
) {
40+
return;
41+
}
42+
// If the clicks has changed, copy to clipboard
43+
this.copyToClipboard();
44+
}
45+
2946
// stringifies object ids used in pattern matching callbacks
3047
stringifyId(id) {
3148
if (typeof id !== 'object') {
@@ -38,9 +55,23 @@ export default class Clipboard extends React.Component {
3855
return '{' + parts.join(',') + '}';
3956
}
4057

41-
async copySuccess(content) {
58+
async copySuccess(content, htmlContent) {
4259
const showCopiedIcon = 1000;
43-
await clipboardAPI.writeText(content);
60+
if (htmlContent) {
61+
const blobHtml = new Blob([htmlContent], {type: 'text/html'});
62+
const blobText = new Blob([content ?? htmlContent], {
63+
type: 'text/plain',
64+
});
65+
const data = [
66+
new ClipboardItem({
67+
['text/plain']: blobText,
68+
['text/html']: blobHtml,
69+
}),
70+
];
71+
await navigator.clipboard.write(data);
72+
} else {
73+
await clipboardAPI.writeText(content);
74+
}
4475
this.setState({copied: true});
4576
await wait(showCopiedIcon);
4677
this.setState({copied: false});
@@ -71,20 +102,18 @@ export default class Clipboard extends React.Component {
71102
}
72103

73104
async copyToClipboard() {
74-
this.props.setProps({
75-
n_clicks: this.props.n_clicks + 1,
76-
});
77-
78105
let content;
106+
let htmlContent;
79107
if (this.props.target_id) {
80108
content = this.getTargetText();
81109
} else {
82110
await wait(100); // gives time for callback to start
83111
await this.loading();
84112
content = this.props.content;
113+
htmlContent = this.props.html_content;
85114
}
86-
if (content) {
87-
this.copySuccess(content);
115+
if (content || htmlContent) {
116+
this.copySuccess(content, htmlContent);
88117
}
89118
}
90119

@@ -106,7 +135,7 @@ export default class Clipboard extends React.Component {
106135
title={title}
107136
style={style}
108137
className={className}
109-
onClick={this.copyToClipboard}
138+
onClick={this.onClickHandler}
110139
data-dash-is-loading={
111140
(loading_state && loading_state.is_loading) || undefined
112141
}
@@ -119,6 +148,7 @@ export default class Clipboard extends React.Component {
119148

120149
Clipboard.defaultProps = {
121150
content: null,
151+
html_content: null,
122152
target_id: null,
123153
n_clicks: 0,
124154
};
@@ -137,7 +167,7 @@ Clipboard.propTypes = {
137167
target_id: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
138168

139169
/**
140-
* The text to be copied to the clipboard if the `target_id` is None.
170+
* The text to be copied to the clipboard if the `target_id` is None.
141171
*/
142172
content: PropTypes.string,
143173

@@ -146,6 +176,11 @@ Clipboard.propTypes = {
146176
*/
147177
n_clicks: PropTypes.number,
148178

179+
/**
180+
* The clipboard html text be copied to the clipboard if the `target_id` is None.
181+
*/
182+
html_content: PropTypes.string,
183+
149184
/**
150185
* The text shown as a tooltip when hovering over the copy icon.
151186
*/

Diff for: components/dash-core-components/tests/integration/clipboard/test_clipboard.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from dash import Dash, html, dcc
1+
from dash import Dash, html, dcc, callback, Output, Input, State
22

33
import dash.testing.wait as wait
44
import time
@@ -54,3 +54,39 @@ def test_clp002_clipboard_text(dash_dcc_headed):
5454
== copy_text,
5555
timeout=3,
5656
)
57+
58+
59+
def test_clp003_clipboard_text(dash_dcc_headed):
60+
copy_text = "Copy this text to the clipboard using a separate button"
61+
app = Dash(__name__, prevent_initial_callbacks=True)
62+
app.layout = html.Div(
63+
[
64+
dcc.Clipboard(id="copy_icon", content=copy_text, n_clicks=0),
65+
dcc.Textarea(id="paste"),
66+
html.Button("Copy", id="copy_button", n_clicks=0),
67+
]
68+
)
69+
70+
@callback(
71+
Output("copy_icon", "n_clicks"),
72+
State("copy_icon", "n_clicks"),
73+
Input("copy_button", "n_clicks"),
74+
prevent_initial_call=True,
75+
)
76+
def selected(icon_clicks, button_clicks):
77+
return icon_clicks + 1
78+
79+
dash_dcc_headed.start_server(app)
80+
81+
dash_dcc_headed.find_element("#copy_button").click()
82+
time.sleep(1)
83+
dash_dcc_headed.find_element("#paste").click()
84+
ActionChains(dash_dcc_headed.driver).key_down(Keys.CONTROL).send_keys("v").key_up(
85+
Keys.CONTROL
86+
).perform()
87+
88+
wait.until(
89+
lambda: dash_dcc_headed.find_element("#paste").get_attribute("value")
90+
== copy_text,
91+
timeout=3,
92+
)

0 commit comments

Comments
 (0)