Skip to content

Commit

Permalink
Merge pull request #2652 from Lxstr/feature/clipboard-html
Browse files Browse the repository at this point in the history
Feature/clipboard-html
  • Loading branch information
alexcjohnson committed Dec 16, 2023
2 parents 2153a68 + 9469c58 commit 7527383
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 11 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Added
- [#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)

## Changed
- [#2652](https://github.com/plotly/dash/pull/2652) dcc.Clipboard supports htm_content and triggers a copy to clipboard when n_clicks are changed

## [2.14.2] - 2023-11-27

## Fixed
Expand Down
55 changes: 45 additions & 10 deletions components/dash-core-components/src/components/Clipboard.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class Clipboard extends React.Component {
constructor(props) {
super(props);
this.copyToClipboard = this.copyToClipboard.bind(this);
this.onClickHandler = this.onClickHandler.bind(this);
this.copySuccess = this.copySuccess.bind(this);
this.getTargetText = this.getTargetText.bind(this);
this.loading = this.loading.bind(this);
Expand All @@ -26,6 +27,22 @@ export default class Clipboard extends React.Component {
};
}

onClickHandler() {
this.props.setProps({n_clicks: this.props.n_clicks + 1});
}

componentDidUpdate(prevProps) {
// If the clicks has not changed, do nothing
if (
!this.props.n_clicks ||
this.props.n_clicks === prevProps.n_clicks
) {
return;
}
// If the clicks has changed, copy to clipboard
this.copyToClipboard();
}

// stringifies object ids used in pattern matching callbacks
stringifyId(id) {
if (typeof id !== 'object') {
Expand All @@ -38,9 +55,23 @@ export default class Clipboard extends React.Component {
return '{' + parts.join(',') + '}';
}

async copySuccess(content) {
async copySuccess(content, htmlContent) {
const showCopiedIcon = 1000;
await clipboardAPI.writeText(content);
if (htmlContent) {
const blobHtml = new Blob([htmlContent], {type: 'text/html'});
const blobText = new Blob([content ?? htmlContent], {
type: 'text/plain',
});
const data = [
new ClipboardItem({
['text/plain']: blobText,
['text/html']: blobHtml,
}),
];
await navigator.clipboard.write(data);
} else {
await clipboardAPI.writeText(content);
}
this.setState({copied: true});
await wait(showCopiedIcon);
this.setState({copied: false});
Expand Down Expand Up @@ -71,20 +102,18 @@ export default class Clipboard extends React.Component {
}

async copyToClipboard() {
this.props.setProps({
n_clicks: this.props.n_clicks + 1,
});

let content;
let htmlContent;
if (this.props.target_id) {
content = this.getTargetText();
} else {
await wait(100); // gives time for callback to start
await this.loading();
content = this.props.content;
htmlContent = this.props.html_content;
}
if (content) {
this.copySuccess(content);
if (content || htmlContent) {
this.copySuccess(content, htmlContent);
}
}

Expand All @@ -106,7 +135,7 @@ export default class Clipboard extends React.Component {
title={title}
style={style}
className={className}
onClick={this.copyToClipboard}
onClick={this.onClickHandler}
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
Expand All @@ -119,6 +148,7 @@ export default class Clipboard extends React.Component {

Clipboard.defaultProps = {
content: null,
html_content: null,
target_id: null,
n_clicks: 0,
};
Expand All @@ -137,7 +167,7 @@ Clipboard.propTypes = {
target_id: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),

/**
* The text to be copied to the clipboard if the `target_id` is None.
* The text to be copied to the clipboard if the `target_id` is None.
*/
content: PropTypes.string,

Expand All @@ -146,6 +176,11 @@ Clipboard.propTypes = {
*/
n_clicks: PropTypes.number,

/**
* The clipboard html text be copied to the clipboard if the `target_id` is None.
*/
html_content: PropTypes.string,

/**
* The text shown as a tooltip when hovering over the copy icon.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dash import Dash, html, dcc
from dash import Dash, html, dcc, callback, Output, Input, State

import dash.testing.wait as wait
import time
Expand Down Expand Up @@ -54,3 +54,39 @@ def test_clp002_clipboard_text(dash_dcc_headed):
== copy_text,
timeout=3,
)


def test_clp003_clipboard_text(dash_dcc_headed):
copy_text = "Copy this text to the clipboard using a separate button"
app = Dash(__name__, prevent_initial_callbacks=True)
app.layout = html.Div(
[
dcc.Clipboard(id="copy_icon", content=copy_text, n_clicks=0),
dcc.Textarea(id="paste"),
html.Button("Copy", id="copy_button", n_clicks=0),
]
)

@callback(
Output("copy_icon", "n_clicks"),
State("copy_icon", "n_clicks"),
Input("copy_button", "n_clicks"),
prevent_initial_call=True,
)
def selected(icon_clicks, button_clicks):
return icon_clicks + 1

dash_dcc_headed.start_server(app)

dash_dcc_headed.find_element("#copy_button").click()
time.sleep(1)
dash_dcc_headed.find_element("#paste").click()
ActionChains(dash_dcc_headed.driver).key_down(Keys.CONTROL).send_keys("v").key_up(
Keys.CONTROL
).perform()

wait.until(
lambda: dash_dcc_headed.find_element("#paste").get_attribute("value")
== copy_text,
timeout=3,
)

0 comments on commit 7527383

Please sign in to comment.