diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ 'names' => array( 'conpherence.pkg.css' => '3144a5e2', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '10815c8e', + 'core.pkg.css' => '6a84521c', 'core.pkg.js' => 'f58c3c6e', 'dark-console.pkg.js' => '187792c2', 'differential.pkg.css' => '91ac6214', @@ -109,7 +109,7 @@ 'rsrc/css/application/tokens/tokens.css' => 'ce5a50bd', 'rsrc/css/application/uiexample/example.css' => 'b4795059', 'rsrc/css/core/core.css' => '531ad849', - 'rsrc/css/core/remarkup.css' => '03b6c819', + 'rsrc/css/core/remarkup.css' => 'b4f2c357', 'rsrc/css/core/syntax.css' => '548567f6', 'rsrc/css/core/z-index.css' => 'ac3bfcd4', 'rsrc/css/diviner/diviner-shared.css' => '4bd263b0', @@ -460,6 +460,7 @@ 'rsrc/js/core/MultirowRowManager.js' => '5b54c823', 'rsrc/js/core/Notification.js' => 'a9b91e3f', 'rsrc/js/core/Prefab.js' => '5793d835', + 'rsrc/js/core/RemarkupCodeblockCopy.js' => '22a8ed50', 'rsrc/js/core/RemarkupMetadata.js' => 'e40c4991', 'rsrc/js/core/ShapedRequest.js' => '995f5102', 'rsrc/js/core/TextAreaUtils.js' => 'f340a484', @@ -532,6 +533,7 @@ 'rsrc/js/phuix/PHUIXIconView.js' => 'a5257c4e', ), 'symbols' => array( + 'RemarkupCodeblockCopy' => '22a8ed50', 'almanac-css' => '2e050f4f', 'aphront-bars' => '4a327b4a', 'aphront-dark-console-css' => '7f06cda2', @@ -797,7 +799,7 @@ 'phabricator-object-selector-css' => 'ee77366f', 'phabricator-phtize' => '2f1db1ed', 'phabricator-prefab' => '5793d835', - 'phabricator-remarkup-css' => '03b6c819', + 'phabricator-remarkup-css' => 'b4f2c357', 'phabricator-remarkup-metadata' => 'e40c4991', 'phabricator-search-results-css' => '9ea70ace', 'phabricator-shaped-request' => '995f5102', diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -87,6 +87,8 @@ ), $engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY))); + require_celerity_resource('RemarkupCodeblockCopy'); + $blogger = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($post->getBloggerPHID())) diff --git a/src/infrastructure/markup/view/PHUIRemarkupView.php b/src/infrastructure/markup/view/PHUIRemarkupView.php --- a/src/infrastructure/markup/view/PHUIRemarkupView.php +++ b/src/infrastructure/markup/view/PHUIRemarkupView.php @@ -89,6 +89,7 @@ $viewer, $context); + require_celerity_resource('RemarkupCodeblockCopy'); return $content; } diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -913,3 +913,39 @@ left: 0; right: 0; } + +.phui-font-fa.fa-spinner { + animation: spin .5s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.remarkup-code-block { + position: relative; +} + +.remarkup-code-block .phui-font-fa.btn-clipboard { + position: absolute; + top: -16px; + right: 0px; + text-decoration: none; +} + +.remarkup-code-block .phui-font-fa.btn-clipboard.fa-spinner { + color: {$darkbluetext}; +} + +.remarkup-code-block .phui-font-fa.btn-clipboard.fa-clipboard { + color: {$darkgreybackground}; +} + +.remarkup-code-block.has-header .phui-font-fa.btn-clipboard { + top: 16px; +} + +.remarkup-code-block .phui-font-fa.btn-clipboard:hover { + color: {$darkbluetext}; +} diff --git a/webroot/rsrc/js/core/RemarkupCodeblockCopy.js b/webroot/rsrc/js/core/RemarkupCodeblockCopy.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/core/RemarkupCodeblockCopy.js @@ -0,0 +1,126 @@ +/** + * @provides RemarkupCodeblockCopy + */ + +JX.onload(function() { + var pageBody = document.querySelector('.phabricator-standard-page-body'); + + var codeBlocks = Array.from( + document.querySelectorAll('.remarkup-code-block') + ); + + for (var codeBlockIndex in codeBlocks) { + var codeBlock = codeBlocks[codeBlockIndex]; + + var button = document.createElement('a'); + button.id = 'codeBlock-' + codeBlockIndex; + button.href = '#'; + button.classList.add('phui-font-fa'); + button.classList.add('btn-clipboard'); + button.classList.add('fa-clipboard'); + codeBlock.appendChild(button); + + if (codeBlock.querySelector('.remarkup-code-header')) { + codeBlock.classList.add('has-header'); + } + + button.addEventListener('click', function(e) { + var button = e.target; + var codeBlock = button.closest('.remarkup-code-block'); + + e.preventDefault(); + + // show animated icon + button.classList.remove('fa-clipboard'); + button.classList.add('fa-spinner'); + + // start copy to clipboard + var code = codeBlock.querySelector('pre'); + if (!code) { + code = ''; + } else { + code = code.innerText; + } + + var temp = document.createElement('div'); + temp.innerHTML = code; + var text = temp.textContent || temp.innerText; + if (!text) { + return; + } + + if (navigator.clipboard) { + navigator.clipboard.writeText(text.trim()).catch(function(err) { + // restore icon + button.classList.remove('fa-spinner'); + button.classList.add('fa-clipboard'); + }); + } else { + var textarea = document.createElement('textarea'); + textarea.value = text.trim(); + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + try { + var success = document.execCommand('copy'); + if (success) { + setTimeout(function() { + // restore icon + button.classList.remove('fa-spinner'); + button.classList.add('fa-clipboard'); + }, 500); + } else { + // restore icon + button.classList.remove('fa-spinner'); + button.classList.add('fa-clipboard'); + } + } catch (err) { + // restore icon + button.classList.remove('fa-spinner'); + button.classList.add('fa-clipboard'); + } + document.body.removeChild(textarea); + } + + }); + + button.addEventListener('mouseenter', function(e) { + var button = e.target; + + var tooltipContainer = document.createElement('div'); + tooltipContainer.id = 'tooltip-' + button.id; + tooltipContainer.classList.add('jx-tooltip-container'); + tooltipContainer.classList.add('jx-tooltip-align-N'); + tooltipContainer.classList.add('jx-tooltip-appear'); + tooltipContainer.style.maxWidth = '160px'; + + var tooltipInner = document.createElement('div'); + tooltipInner.classList.add('jx-tooltip-inner'); + tooltipContainer.appendChild(tooltipInner); + + var tooltip = document.createElement('div'); + tooltip.classList.add('jx-tooltip'); + tooltip.innerText = 'Copy to clipboard'; + tooltipInner.appendChild(tooltip); + + var rect = button.getBoundingClientRect(); + tooltipContainer.style.left = (rect.x + window.scrollX - 58) + 'px'; + tooltipContainer.style.top = (rect.y + window.scrollY - 51) + 'px'; + + tooltipContainer.style.display = 'block'; + + pageBody.appendChild(tooltipContainer); + }); + + button.addEventListener('mouseleave', function(e) { + var button = e.target; + + var tooltipContainer = pageBody.querySelector('#tooltip-' + button.id); + if (tooltipContainer){ + pageBody.removeChild(tooltipContainer); + } + }); + } +});