From c074cbbb1046ff7d52c5fb686900cf5f5d198c9e Mon Sep 17 00:00:00 2001 From: Lex Date: Fri, 29 Sep 2023 16:41:11 +1000 Subject: [PATCH 1/3] Allow Clipboard to respond to new content directly --- .../src/components/Clipboard.react.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/dash-core-components/src/components/Clipboard.react.js b/components/dash-core-components/src/components/Clipboard.react.js index 0a1274caab..22002b83a6 100644 --- a/components/dash-core-components/src/components/Clipboard.react.js +++ b/components/dash-core-components/src/components/Clipboard.react.js @@ -26,6 +26,15 @@ export default class Clipboard extends React.Component { }; } + componentDidUpdate(prevProps) { + // If the data hasn't changed, do nothing. + if (!this.props.content || this.props.content === prevProps.content) { + return; + } + // If the data has changed, copy to clipboard + this.copyToClipboard(); + } + // stringifies object ids used in pattern matching callbacks stringifyId(id) { if (typeof id !== 'object') { From 262c621937b9dcc083e6ae012117ba34437f0c67 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 3 Oct 2023 20:05:49 +1000 Subject: [PATCH 2/3] Test clipboard to respond to new content directly --- .../integration/clipboard/test_clipboard.py | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/components/dash-core-components/tests/integration/clipboard/test_clipboard.py b/components/dash-core-components/tests/integration/clipboard/test_clipboard.py index c45a7f401c..ee8ca6e358 100644 --- a/components/dash-core-components/tests/integration/clipboard/test_clipboard.py +++ b/components/dash-core-components/tests/integration/clipboard/test_clipboard.py @@ -1,4 +1,4 @@ -from dash import Dash, html, dcc +from dash import Dash, html, dcc, callback, Output, Input import dash.testing.wait as wait import time @@ -54,3 +54,32 @@ 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), dcc.Textarea(id="paste"), html.Button("Copy", id="copy_button")] + ) + @callback( + Output("copy_icon", "content"), + Input("copy_button", "n_clicks"), + prevent_initial_call=True, + ) + def selected(clicks): + return f"{clicks}" + + 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, + ) From 26bf6abbbdd9942189a451253fb7ea8f78fb46e5 Mon Sep 17 00:00:00 2001 From: Lex Date: Tue, 3 Oct 2023 20:16:28 +1000 Subject: [PATCH 3/3] Clipboard support for text/html MIME type --- .../src/components/Clipboard.react.js | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/components/dash-core-components/src/components/Clipboard.react.js b/components/dash-core-components/src/components/Clipboard.react.js index 22002b83a6..26b5ec729f 100644 --- a/components/dash-core-components/src/components/Clipboard.react.js +++ b/components/dash-core-components/src/components/Clipboard.react.js @@ -47,9 +47,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}); @@ -85,15 +99,17 @@ export default class Clipboard extends React.Component { }); 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); } } @@ -128,6 +144,7 @@ export default class Clipboard extends React.Component { Clipboard.defaultProps = { content: null, + html_content: null, target_id: null, n_clicks: 0, }; @@ -146,7 +163,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, @@ -155,6 +172,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. */