From 89a5d3132c94dec8de47f5052fd0441e44365751 Mon Sep 17 00:00:00 2001 From: Benjamin Kausch Date: Thu, 15 Feb 2024 19:30:57 +0100 Subject: [PATCH] Implements copy button in clone repo modal Summary: This diff adds a copy button to every repo uri in the clone repo modal. I have made the button to select the text to a merely structural span before the input - it just shows the type of the repository uri. When you click inside the input, the entire uri will be selected. Also I have uncluttered the HTML structure. A table is not needed here, nothing a flex block can't handle. | Before | After | |-----------|-----------| | {F1360344} | {F1368592} | While at it, I have extended the used javascript copy behavior. First of all: `document.execCommand('copy')` [[ https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand | could stop working every moment in every browser ]]. The [[ https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard | new clipboard API ]] is the way to go, so I have implemented it as the preferred method. The old method is kept as a fallback. And I have added a very nice feature: If defined, the behavior will now issue success or error notifications. See the changed UIExamples for that. To support the shrinking of JS code with async functions I have patched the JsShrink source. Test Plan: Go to a repository, hit the clone button and use the new copy button. You will see a shiny notification as a reward. Reviewers: O1 Blessed Committers, avivey, valerio.bozzolan Reviewed By: O1 Blessed Committers, avivey, valerio.bozzolan Subscribers: avivey, tobiaswiese, valerio.bozzolan, Matthew, Cigaryno Differential Revision: https://we.phorge.it/D25536 --- externals/JsShrink/jsShrink.php | 2 +- resources/celerity/map.php | 35 ++++----- .../diffusion/view/DiffusionCloneURIView.php | 46 ++++++++---- .../uiexample/examples/PHUIButtonExample.php | 2 + .../application/diffusion/diffusion-icons.css | 33 +++++---- webroot/rsrc/js/core/behavior-copy.js | 72 ++++++++++++++----- .../rsrc/js/core/behavior-select-content.js | 8 +++ 7 files changed, 138 insertions(+), 60 deletions(-) diff --git a/externals/JsShrink/jsShrink.php b/externals/JsShrink/jsShrink.php index 239e6d7978..34a5a5b8e8 100644 --- a/externals/JsShrink/jsShrink.php +++ b/externals/JsShrink/jsShrink.php @@ -35,7 +35,7 @@ function jsShrinkCallback($match) { list(, $context, $regexp, $result, $word, $operator) = $match; if ($word != '') { $result = ($last == 'word' ? "\n" : ($last == 'return' ? " " : "")) . $result; - $last = ($word == 'return' || $word == 'throw' || $word == 'break' ? 'return' : 'word'); + $last = ($word == 'return' || $word == 'throw' || $word == 'break' || $word == 'async' ? 'return' : 'word'); } elseif ($operator) { $result = ($last == $operator[0] ? "\n" : "") . $result; $last = $operator[0]; diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0a15330eec..a9919fc998 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -14,7 +14,7 @@ return array( 'dark-console.pkg.js' => '187792c2', 'differential.pkg.css' => '6d3700f0', 'differential.pkg.js' => '46fcb3af', - 'diffusion.pkg.css' => '42c75c37', + 'diffusion.pkg.css' => '354279ea', 'diffusion.pkg.js' => '78c9885d', 'maniphest.pkg.css' => '35995d6d', 'maniphest.pkg.js' => 'c9308721', @@ -69,7 +69,7 @@ return array( 'rsrc/css/application/differential/revision-history.css' => '237a2979', 'rsrc/css/application/differential/revision-list.css' => '93d2df7d', 'rsrc/css/application/differential/table-of-contents.css' => 'bba788b9', - 'rsrc/css/application/diffusion/diffusion-icons.css' => '23b31a1b', + 'rsrc/css/application/diffusion/diffusion-icons.css' => 'e812add2', 'rsrc/css/application/diffusion/diffusion-readme.css' => 'b68a76e4', 'rsrc/css/application/diffusion/diffusion-repository.css' => 'b89e8c6c', 'rsrc/css/application/diffusion/diffusion.css' => 'e46232d6', @@ -470,7 +470,7 @@ return array( 'rsrc/js/core/behavior-badge-view.js' => '92cdd7b6', 'rsrc/js/core/behavior-bulk-editor.js' => 'aa6d2308', 'rsrc/js/core/behavior-choose-control.js' => '04f8a1e3', - 'rsrc/js/core/behavior-copy.js' => 'cf32921f', + 'rsrc/js/core/behavior-copy.js' => '96b63a02', 'rsrc/js/core/behavior-detect-timezone.js' => '78bc5d94', 'rsrc/js/core/behavior-device.js' => 'ac2b1e01', 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '6bc7ccf7', @@ -499,7 +499,7 @@ return array( 'rsrc/js/core/behavior-reveal-content.js' => 'b105a3a6', 'rsrc/js/core/behavior-scrollbar.js' => '92388bae', 'rsrc/js/core/behavior-search-typeahead.js' => '1cb7d027', - 'rsrc/js/core/behavior-select-content.js' => 'e8240b50', + 'rsrc/js/core/behavior-select-content.js' => 'c538cbfc', 'rsrc/js/core/behavior-select-on-click.js' => '66365ee2', 'rsrc/js/core/behavior-setup-check-https.js' => '01384686', 'rsrc/js/core/behavior-time-typeahead.js' => '5803b9e7', @@ -567,7 +567,7 @@ return array( 'differential-revision-list-css' => '93d2df7d', 'differential-table-of-contents-css' => 'bba788b9', 'diffusion-css' => 'e46232d6', - 'diffusion-icons-css' => '23b31a1b', + 'diffusion-icons-css' => 'e812add2', 'diffusion-readme-css' => 'b68a76e4', 'diffusion-repository-css' => 'b89e8c6c', 'diviner-shared-css' => '4bd263b0', @@ -644,7 +644,7 @@ return array( 'javelin-behavior-owners-path-editor' => 'ff688a7a', 'javelin-behavior-passphrase-credential-control' => '48fe33d0', 'javelin-behavior-phabricator-autofocus' => '65bb0011', - 'javelin-behavior-phabricator-clipboard-copy' => 'cf32921f', + 'javelin-behavior-phabricator-clipboard-copy' => '96b63a02', 'javelin-behavior-phabricator-gesture' => 'b58d1a2a', 'javelin-behavior-phabricator-gesture-example' => '242dedd0', 'javelin-behavior-phabricator-keyboard-pager' => '1325b731', @@ -687,7 +687,7 @@ return array( 'javelin-behavior-repository-crossreference' => '44d48cd1', 'javelin-behavior-scrollbar' => '92388bae', 'javelin-behavior-search-reorder-queries' => 'b86f297f', - 'javelin-behavior-select-content' => 'e8240b50', + 'javelin-behavior-select-content' => 'c538cbfc', 'javelin-behavior-select-on-click' => '66365ee2', 'javelin-behavior-setup-check-https' => '01384686', 'javelin-behavior-stripe-payment-form' => '02cb4398', @@ -1770,6 +1770,12 @@ return array( 'javelin-dom', 'javelin-router', ), + '96b63a02' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'phabricator-notification', + ), '98ef467f' => array( 'javelin-behavior', 'javelin-dom', @@ -2022,6 +2028,11 @@ return array( 'javelin-workboard-card', 'javelin-workboard-header', ), + 'c538cbfc' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + ), 'c687e867' => array( 'javelin-behavior', 'javelin-dom', @@ -2063,11 +2074,6 @@ return array( 'phuix-formation-column-view', 'phuix-formation-flank-view', ), - 'cf32921f' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - ), 'd12d214f' => array( 'javelin-install', 'javelin-dom', @@ -2146,11 +2152,6 @@ return array( 'javelin-dom', 'phabricator-draggable-list', ), - 'e8240b50' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - ), 'e9a2940f' => array( 'javelin-behavior', 'javelin-request', diff --git a/src/applications/diffusion/view/DiffusionCloneURIView.php b/src/applications/diffusion/view/DiffusionCloneURIView.php index c9dae57563..3a79e8d72c 100644 --- a/src/applications/diffusion/view/DiffusionCloneURIView.php +++ b/src/applications/diffusion/view/DiffusionCloneURIView.php @@ -40,6 +40,7 @@ final class DiffusionCloneURIView extends AphrontView { require_celerity_resource('diffusion-icons-css'); Javelin::initBehavior('select-content'); + Javelin::initBehavior('phabricator-clipboard-copy'); $uri_id = celerity_generate_unique_node_id(); @@ -53,6 +54,11 @@ final class DiffusionCloneURIView extends AphrontView { 'value' => $display, 'class' => 'diffusion-clone-uri', 'readonly' => 'true', + 'sigil' => 'select-content', + 'meta' => array( + 'selectID' => $uri_id, + 'once' => true, + ), )); $uri = $this->getRepositoryURI(); @@ -71,17 +77,30 @@ final class DiffusionCloneURIView extends AphrontView { break; } - $io = id(new PHUIButtonView()) + $io = javelin_tag( + 'span', + array( + 'class' => 'diffusion-clone-uri-io', + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $io_tip, + ), + ), + id(new PHUIIconView())->setIcon($io_icon)); + + $copy = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) - ->setIcon($io_icon) + ->setIcon('fa-clipboard') ->setHref('#') - ->addSigil('select-content') + ->addSigil('clipboard-copy') ->addSigil('has-tooltip') ->setMetadata( array( - 'tip' => $io_tip, - 'selectID' => $uri_id, + 'tip' => pht('Copy repository URI'), + 'text' => $display, + 'successMessage' => pht('Repository URI copied.'), + 'errorMessage' => pht('Copy of Repository URI failed.'), )); switch ($uri->getEffectiveIOType()) { @@ -121,19 +140,18 @@ final class DiffusionCloneURIView extends AphrontView { ->setHref($auth_uri) ->setDisabled($auth_disabled); - $cells = array(); - $cells[] = phutil_tag('td', array(), $input); - $cells[] = phutil_tag('th', array(), $io); - $cells[] = phutil_tag('th', array(), $credentials); - - $row = phutil_tag('tr', array(), $cells); + $elements = array(); + $elements[] = $io; + $elements[] = $input; + $elements[] = $copy; + $elements[] = $credentials; return phutil_tag( - 'table', + 'div', array( - 'class' => 'diffusion-clone-uri-table', + 'class' => 'diffusion-clone-uri-wrapper', ), - $row); + $elements); } } diff --git a/src/applications/uiexample/examples/PHUIButtonExample.php b/src/applications/uiexample/examples/PHUIButtonExample.php index 89f8e15fe8..7d00290505 100644 --- a/src/applications/uiexample/examples/PHUIButtonExample.php +++ b/src/applications/uiexample/examples/PHUIButtonExample.php @@ -115,6 +115,8 @@ final class PHUIButtonExample extends PhabricatorUIExample { $button->setMetadata( array( 'text' => $copy, + 'successMessage' => pht('Text copied into clipboard.'), + 'errorMessage' => pht('Failed to copy text into clipboard.'), )); } diff --git a/webroot/rsrc/css/application/diffusion/diffusion-icons.css b/webroot/rsrc/css/application/diffusion/diffusion-icons.css index 072db01660..1b1887e904 100644 --- a/webroot/rsrc/css/application/diffusion/diffusion-icons.css +++ b/webroot/rsrc/css/application/diffusion/diffusion-icons.css @@ -2,11 +2,6 @@ * @provides diffusion-icons-css */ -input.diffusion-clone-uri { - display: block; - width: 100%; -} - .diffusion-clone-extras { font-size: 11px; text-align: right; @@ -35,21 +30,35 @@ input.diffusion-clone-uri { padding: 0; } -.diffusion-clone-uri-table { - width: 100%; +.diffusion-clone-uri-wrapper { + display: flex; } -.diffusion-clone-uri-table th { - width: 24px; - padding: 0 0 0 4px; +.diffusion-clone-uri-wrapper .diffusion-clone-uri-io { + align-items: center; + justify-content: center; + border: 1px solid {$greyborder}; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + display: flex; + width: 38px; } -.diffusion-clone-uri-table th a.button { +.diffusion-clone-uri-wrapper a.button { width: 12px; height: 19px; + margin-left: 4px; } -.diffusion-clone-uri-table th a.button .phui-icon-view { +.diffusion-clone-uri-wrapper a.button .phui-icon-view { left: 15px; top: 7px; } + +.diffusion-clone-uri-wrapper input.diffusion-clone-uri { + border-left: none; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + display: block; + width: 100%; +} diff --git a/webroot/rsrc/js/core/behavior-copy.js b/webroot/rsrc/js/core/behavior-copy.js index 4457f6e022..210d9ebc56 100644 --- a/webroot/rsrc/js/core/behavior-copy.js +++ b/webroot/rsrc/js/core/behavior-copy.js @@ -3,41 +3,81 @@ * @requires javelin-behavior * javelin-dom * javelin-stratcom + * phabricator-notification * @javelin */ JX.behavior('phabricator-clipboard-copy', function() { - if (!document.queryCommandSupported) { - return; - } + var fallback_working = document.queryCommandSupported && + document.queryCommandSupported('copy'); - if (!document.queryCommandSupported('copy')) { + if (!navigator.clipboard && !fallback_working) { return; } JX.DOM.alterClass(document.body, 'supports-clipboard', true); - JX.Stratcom.listen('click', 'clipboard-copy', function(e) { - e.kill(); - - var data = e.getNodeData('clipboard-copy'); + var copy_fallback = function(text) { var attr = { - value: data.text || '', + value: text || '', className: 'clipboard-buffer' }; var node = JX.$N('textarea', attr); document.body.appendChild(node); - try { - node.select(); - document.execCommand('copy'); - } catch (ignored) { - // Ignore any errors we hit. - } + node.select(); + document.execCommand('copy'); - JX.DOM.remove(node); + JX.DOM.remove(node); + }; + + var show_success_message = function(message) { + if (!message) { + return; + } + new JX.Notification() + .setContent(message) + .alterClassName('jx-notification-done', true) + .setDuration(8000) + .show(); + }; + + var show_error_message = function(message) { + if (!message) { + return; + } + new JX.Notification() + .setContent(message) + .alterClassName('jx-notification-error', true) + .setDuration(8000) + .show(); + }; + + JX.Stratcom.listen('click', 'clipboard-copy', function(e) { + var data = e.getNodeData('clipboard-copy'); + var text = data.text || ''; + + var copy = async function( // jshint ignore:line + text, + successMessage, + errorMessage + ) { + try { + if (navigator.clipboard) { + await navigator.clipboard.writeText(text); + } else { + copy_fallback(text); + } + show_success_message(successMessage); + } catch (ex) { + show_error_message(errorMessage); + } + }; + + e.kill(); + copy(text, data.successMessage, data.errorMessage); }); }); diff --git a/webroot/rsrc/js/core/behavior-select-content.js b/webroot/rsrc/js/core/behavior-select-content.js index b54b6308cf..92e4dcfc0b 100644 --- a/webroot/rsrc/js/core/behavior-select-content.js +++ b/webroot/rsrc/js/core/behavior-select-content.js @@ -16,8 +16,16 @@ JX.behavior('select-content', function() { var node = e.getNode('select-content'); var data = JX.Stratcom.getData(node); + if (data.once && data.selected) { + return; + } + var target = JX.$(data.selectID); JX.DOM.focus(target); target.select(); + + if (data.once) { + JX.Stratcom.addData(node, {selected: true}); + } }); });