Feature #29214 » 0001-Add-copy-button-to-pre-elements.patch
| app/assets/images/icons.svg | ||
|---|---|---|
| 131 | 131 |
<path d="M13 17v-1a1 1 0 0 1 1 -1h1m3 0h1a1 1 0 0 1 1 1v1m0 3v1a1 1 0 0 1 -1 1h-1m-3 0h-1a1 1 0 0 1 -1 -1v-1"/> |
| 132 | 132 |
<path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"/> |
| 133 | 133 |
</symbol> |
| 134 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--copy-pre-content"> |
|
| 135 |
<path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2"/> |
|
| 136 |
<path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"/> |
|
| 137 |
</symbol> |
|
| 134 | 138 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--custom-fields"> |
| 135 | 139 |
<path d="M20 13v-4a2 2 0 0 0 -2 -2h-12a2 2 0 0 0 -2 2v5a2 2 0 0 0 2 2h6"/> |
| 136 | 140 |
<path d="M15 19l2 2l4 -4"/> |
| app/assets/javascripts/application.js | ||
|---|---|---|
| 61 | 61 |
iconElement.setAttribute('href', iconPath.replace(/#.*$/g, "#icon--" + icon))
|
| 62 | 62 |
} |
| 63 | 63 | |
| 64 |
function createSVGIcon(icon) {
|
|
| 65 |
const clonedIcon = document.querySelector('#icon-copy-source svg').cloneNode(true);
|
|
| 66 |
updateSVGIcon(clonedIcon, icon); |
|
| 67 |
return clonedIcon |
|
| 68 |
} |
|
| 69 | ||
| 64 | 70 |
function collapseAllRowGroups(el) {
|
| 65 | 71 |
var tbody = $(el).parents('tbody').first();
|
| 66 | 72 |
tbody.children('tr').each(function(index) {
|
| ... | ... | |
| 210 | 216 |
case "list_status": |
| 211 | 217 |
case "list_subprojects": |
| 212 | 218 |
const iconType = values.length > 1 ? 'toggle-minus' : 'toggle-plus'; |
| 213 |
const clonedIcon = document.querySelector('#icon-copy-source svg').cloneNode(true);
|
|
| 214 |
updateSVGIcon(clonedIcon, iconType); |
|
| 219 |
const iconSvg = createSVGIcon(iconType) |
|
| 215 | 220 | |
| 216 | 221 |
tr.find('.values').append(
|
| 217 | 222 |
$('<span>', { style: 'display:none;' }).append(
|
| ... | ... | |
| 221 | 226 |
name: `v[${field}][]`,
|
| 222 | 227 |
}), |
| 223 | 228 |
'\n', |
| 224 |
$('<span>', { class: `toggle-multiselect icon-only icon-${iconType}` }).append(clonedIcon)
|
|
| 229 |
$('<span>', { class: `toggle-multiselect icon-only icon-${iconType}` }).append(iconSvg)
|
|
| 225 | 230 |
) |
| 226 | 231 |
); |
| 227 | 232 |
select = tr.find('.values select');
|
| ... | ... | |
| 642 | 647 |
return false; |
| 643 | 648 |
} |
| 644 | 649 | |
| 650 |
function setupCopyButtonsToPreElements() {
|
|
| 651 |
document.querySelectorAll('pre:not(.pre-wrapper pre)').forEach((pre) => {
|
|
| 652 |
// Wrap the <pre> element with a container and add a copy button |
|
| 653 |
const wrapper = document.createElement("div");
|
|
| 654 |
wrapper.classList.add("pre-wrapper");
|
|
| 655 | ||
| 656 |
const copyButton = document.createElement("a");
|
|
| 657 |
copyButton.title = rm.I18n.buttonCopy; |
|
| 658 |
copyButton.classList.add("copy-pre-content-link", "icon-only");
|
|
| 659 |
copyButton.append(createSVGIcon("copy-pre-content"));
|
|
| 660 | ||
| 661 |
wrapper.appendChild(copyButton); |
|
| 662 |
wrapper.append(pre.cloneNode(true)); |
|
| 663 |
pre.replaceWith(wrapper); |
|
| 664 | ||
| 665 |
// Copy the contents of the pre tag when copyButton is clicked |
|
| 666 |
copyButton.addEventListener("click", (event) => {
|
|
| 667 |
event.preventDefault(); |
|
| 668 |
let textToCopy = (pre.querySelector("code") || pre).textContent.replace(/\n$/, '');
|
|
| 669 |
if (pre.querySelector("code.syntaxhl")) { textToCopy = textToCopy.replace(/ $/, ''); } // Workaround for half-width space issue in Textile's highlighted code
|
|
| 670 |
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
| 671 |
updateSVGIcon(copyButton, "checked"); |
|
| 672 |
setTimeout(() => updateSVGIcon(copyButton, "copy-pre-content"), 2000); |
|
| 673 |
}); |
|
| 674 |
}); |
|
| 675 |
}); |
|
| 676 |
} |
|
| 677 | ||
| 645 | 678 |
function updateIssueFrom(url, el) {
|
| 646 | 679 |
$('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
|
| 647 | 680 |
$(this).data('valuebeforeupdate', $(this).val());
|
| ... | ... | |
| 1158 | 1191 |
}); |
| 1159 | 1192 |
} |
| 1160 | 1193 | |
| 1161 |
$(function () {
|
|
| 1194 |
function setupHoverTooltips() {
|
|
| 1162 | 1195 |
$("[title]:not(.no-tooltip)").tooltip({
|
| 1163 | 1196 |
show: {
|
| 1164 | 1197 |
delay: 400 |
| ... | ... | |
| 1168 | 1201 |
at: "center top" |
| 1169 | 1202 |
} |
| 1170 | 1203 |
}); |
| 1171 |
}); |
|
| 1204 |
} |
|
| 1205 | ||
| 1206 |
$(function() { setupHoverTooltips(); });
|
|
| 1172 | 1207 | |
| 1173 | 1208 |
function inlineAutoComplete(element) {
|
| 1174 | 1209 |
'use strict'; |
| ... | ... | |
| 1362 | 1397 |
$(document).on('focus', '[data-auto-complete=true]', function(event) {
|
| 1363 | 1398 |
inlineAutoComplete(event.target); |
| 1364 | 1399 |
}); |
| 1400 |
document.addEventListener("DOMContentLoaded", () => { setupCopyButtonsToPreElements(); });
|
|
| app/assets/stylesheets/application.css | ||
|---|---|---|
| 1498 | 1498 |
div.wiki li {line-height: 1.6; margin-bottom: 0.125rem;}
|
| 1499 | 1499 |
div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;}
|
| 1500 | 1500 | |
| 1501 |
div.wiki div.pre-wrapper {
|
|
| 1502 |
position: relative; |
|
| 1503 |
padding-right: 10px; |
|
| 1504 |
} |
|
| 1505 | ||
| 1501 | 1506 |
div.wiki pre {
|
| 1502 | 1507 |
margin: 1em 1em 1em 1.6em; |
| 1503 | 1508 |
padding: 8px; |
| ... | ... | |
| 1515 | 1520 |
border-radius: 0.1em; |
| 1516 | 1521 |
} |
| 1517 | 1522 | |
| 1523 |
div.pre-wrapper a.copy-pre-content-link {
|
|
| 1524 |
position: absolute; |
|
| 1525 |
top: 0px; |
|
| 1526 |
right: 8px; |
|
| 1527 |
cursor: pointer; |
|
| 1528 |
display: none; |
|
| 1529 |
} |
|
| 1530 | ||
| 1531 |
div.pre-wrapper:hover a.copy-pre-content-link {
|
|
| 1532 |
display: block; |
|
| 1533 |
} |
|
| 1534 | ||
| 1518 | 1535 |
div.wiki ul.toc {
|
| 1519 | 1536 |
background-color: #ffffdd; |
| 1520 | 1537 |
border: 1px solid #e4e4e4; |
| app/helpers/application_helper.rb | ||
|---|---|---|
| 1915 | 1915 |
end |
| 1916 | 1916 |
end |
| 1917 | 1917 | |
| 1918 |
def heads_for_i18n |
|
| 1919 |
javascript_tag( |
|
| 1920 |
"rm = window.rm || {};" \
|
|
| 1921 |
"rm.I18n = rm.I18n || {};" \
|
|
| 1922 |
"rm.I18n = Object.freeze({buttonCopy: '#{l(:button_copy)}'});"
|
|
| 1923 |
) |
|
| 1924 |
end |
|
| 1925 | ||
| 1918 | 1926 |
def heads_for_auto_complete(project) |
| 1919 | 1927 |
data_sources = autocomplete_data_sources(project) |
| 1920 | 1928 |
javascript_tag( |
| app/views/journals/update.js.erb | ||
|---|---|---|
| 15 | 15 |
journal_header.append('<%= escape_javascript(render_journal_update_info(@journal)) %>');
|
| 16 | 16 |
} |
| 17 | 17 |
setupWikiTableSortableHeader(); |
| 18 |
setupCopyButtonsToPreElements(); |
|
| 19 |
setupHoverTooltips(); |
|
| 18 | 20 |
<% end %> |
| 19 | 21 | |
| 20 | 22 |
<%= call_hook(:view_journals_update_js_bottom, { :journal => @journal }) %>
|
| app/views/layouts/base.html.erb | ||
|---|---|---|
| 12 | 12 |
<%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %> |
| 13 | 13 |
<%= javascript_heads %> |
| 14 | 14 |
<%= heads_for_theme %> |
| 15 |
<%= heads_for_i18n %> |
|
| 15 | 16 |
<%= heads_for_auto_complete(@project) %> |
| 16 | 17 |
<%= call_hook :view_layouts_base_html_head %> |
| 17 | 18 |
<!-- page specific tags --> |
| ... | ... | |
| 129 | 130 | |
| 130 | 131 |
<div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div> |
| 131 | 132 |
<div id="ajax-modal" style="display:none;"></div> |
| 133 |
<div id="icon-copy-source" style="display: none;"><%= sprite_icon('') %></div>
|
|
| 132 | 134 | |
| 133 | 135 |
</div> |
| 134 | 136 |
<%= call_hook :view_layouts_base_body_bottom %> |
| app/views/queries/_filters.html.erb | ||
|---|---|---|
| 22 | 22 |
<%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %> |
| 23 | 23 |
</div> |
| 24 | 24 | |
| 25 |
<div id="icon-copy-source" style="display: none;"><%= sprite_icon('') %></div>
|
|
| 26 | 25 |
<%= hidden_field_tag 'f[]', '' %> |
| 27 | 26 |
<% include_calendar_headers_tags %> |
| config/icon_source.yml | ||
|---|---|---|
| 209 | 209 |
svg: square-rounded-plus |
| 210 | 210 |
- name: toggle-minus |
| 211 | 211 |
svg: square-rounded-minus |
| 212 |
- name: copy-pre-content |
|
| 213 |
svg: clipboard |
|
| config/locales/en.yml | ||
|---|---|---|
| 1193 | 1193 |
button_copy: Copy |
| 1194 | 1194 |
button_copy_and_follow: Copy and follow |
| 1195 | 1195 |
button_copy_link: Copy link |
| 1196 |
button_copied: Copied |
|
| 1196 | 1197 |
button_annotate: Annotate |
| 1197 | 1198 |
button_fetch_changesets: Fetch commits |
| 1198 | 1199 |
button_update: Update |