Project

General

Profile

Feature #29214 » 0001-Add-copy-button-to-pre-elements.patch

Mizuki ISHIKAWA, 2025-02-28 07:24

View differences:

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
(5-5/9)