From 7e623e1f69a96db99c3a40417fbe7ac18ae3ec86 Mon Sep 17 00:00:00 2001 From: ishikawa999 Date: Fri, 28 Feb 2025 05:44:07 +0000 Subject: [PATCH] Add copy button to pre elements --- app/assets/images/icons.svg | 4 +++ app/assets/javascripts/application.js | 46 +++++++++++++++++++++++--- app/assets/stylesheets/application.css | 17 ++++++++++ app/helpers/application_helper.rb | 8 +++++ app/views/journals/update.js.erb | 2 ++ app/views/layouts/base.html.erb | 2 ++ app/views/queries/_filters.html.erb | 1 - config/icon_source.yml | 2 ++ config/locales/en.yml | 1 + 9 files changed, 77 insertions(+), 6 deletions(-) diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg index 55925cddd..cf5f0450f 100644 --- a/app/assets/images/icons.svg +++ b/app/assets/images/icons.svg @@ -131,6 +131,10 @@ + + + + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index c7c3e2fef..9cb082ace 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -61,6 +61,12 @@ function updateSVGIcon(element, icon) { iconElement.setAttribute('href', iconPath.replace(/#.*$/g, "#icon--" + icon)) } +function createSVGIcon(icon) { + const clonedIcon = document.querySelector('#icon-copy-source svg').cloneNode(true); + updateSVGIcon(clonedIcon, icon); + return clonedIcon +} + function collapseAllRowGroups(el) { var tbody = $(el).parents('tbody').first(); tbody.children('tr').each(function(index) { @@ -210,8 +216,7 @@ function buildFilterRow(field, operator, values) { case "list_status": case "list_subprojects": const iconType = values.length > 1 ? 'toggle-minus' : 'toggle-plus'; - const clonedIcon = document.querySelector('#icon-copy-source svg').cloneNode(true); - updateSVGIcon(clonedIcon, iconType); + const iconSvg = createSVGIcon(iconType) tr.find('.values').append( $('', { style: 'display:none;' }).append( @@ -221,7 +226,7 @@ function buildFilterRow(field, operator, values) { name: `v[${field}][]`, }), '\n', - $('', { class: `toggle-multiselect icon-only icon-${iconType}` }).append(clonedIcon) + $('', { class: `toggle-multiselect icon-only icon-${iconType}` }).append(iconSvg) ) ); select = tr.find('.values select'); @@ -642,6 +647,34 @@ function copyTextToClipboard(target) { return false; } +function setupCopyButtonsToPreElements() { + document.querySelectorAll('pre:not(.pre-wrapper pre)').forEach((pre) => { + // Wrap the
 element with a container and add a copy button
+    const wrapper = document.createElement("div");
+    wrapper.classList.add("pre-wrapper");
+
+    const copyButton = document.createElement("a");
+    copyButton.title = rm.I18n.buttonCopy;
+    copyButton.classList.add("copy-pre-content-link", "icon-only");
+    copyButton.append(createSVGIcon("copy-pre-content"));
+
+    wrapper.appendChild(copyButton);
+    wrapper.append(pre.cloneNode(true));
+    pre.replaceWith(wrapper);
+
+    // Copy the contents of the pre tag when copyButton is clicked
+    copyButton.addEventListener("click", (event) => {
+      event.preventDefault();
+      let textToCopy = (pre.querySelector("code") || pre).textContent.replace(/\n$/, '');
+      if (pre.querySelector("code.syntaxhl")) { textToCopy = textToCopy.replace(/ $/, ''); } // Workaround for half-width space issue in Textile's highlighted code
+      navigator.clipboard.writeText(textToCopy).then(() => {
+        updateSVGIcon(copyButton, "checked");
+        setTimeout(() => updateSVGIcon(copyButton, "copy-pre-content"), 2000);
+      });
+    });
+  });
+}
+
 function updateIssueFrom(url, el) {
   $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
     $(this).data('valuebeforeupdate', $(this).val());
@@ -1158,7 +1191,7 @@ function setupWikiTableSortableHeader() {
   });
 }
 
-$(function () {
+function setupHoverTooltips() {
   $("[title]:not(.no-tooltip)").tooltip({
     show: {
       delay: 400
@@ -1168,7 +1201,9 @@ $(function () {
       at: "center top"
     }
   });
-});
+}
+
+$(function() { setupHoverTooltips(); });
 
 function inlineAutoComplete(element) {
     'use strict';
@@ -1362,3 +1397,4 @@ $(document).ready(setupWikiTableSortableHeader);
 $(document).on('focus', '[data-auto-complete=true]', function(event) {
   inlineAutoComplete(event.target);
 });
+document.addEventListener("DOMContentLoaded", () => { setupCopyButtonsToPreElements(); });
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index a17be1e81..8d7af7dab 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -1498,6 +1498,11 @@ div.wiki ul, div.wiki ol {margin-bottom:1em;}
 div.wiki li {line-height: 1.6; margin-bottom: 0.125rem;}
 div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;}
 
+div.wiki div.pre-wrapper {
+  position: relative;
+  padding-right: 10px;
+}
+
 div.wiki pre {
   margin: 1em 1em 1em 1.6em;
   padding: 8px;
@@ -1515,6 +1520,18 @@ div.wiki *:not(pre)>code, div.wiki>code {
   border-radius: 0.1em;
 }
 
+div.pre-wrapper a.copy-pre-content-link {
+  position: absolute;
+  top: 0px;
+  right: 8px;
+  cursor: pointer;
+  display: none;
+}
+
+div.pre-wrapper:hover a.copy-pre-content-link {
+  display: block;
+}
+
 div.wiki ul.toc {
   background-color: #ffffdd;
   border: 1px solid #e4e4e4;
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 50519c890..0f1ea1615 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1915,6 +1915,14 @@ module ApplicationHelper
     end
   end
 
+  def heads_for_i18n
+    javascript_tag(
+      "rm = window.rm || {};" \
+      "rm.I18n = rm.I18n || {};" \
+      "rm.I18n = Object.freeze({buttonCopy: '#{l(:button_copy)}'});"
+    )
+  end
+
   def heads_for_auto_complete(project)
     data_sources = autocomplete_data_sources(project)
     javascript_tag(
diff --git a/app/views/journals/update.js.erb b/app/views/journals/update.js.erb
index 227d169fc..1c7da09a1 100644
--- a/app/views/journals/update.js.erb
+++ b/app/views/journals/update.js.erb
@@ -15,6 +15,8 @@
     journal_header.append('<%= escape_javascript(render_journal_update_info(@journal)) %>');
   }
   setupWikiTableSortableHeader();
+  setupCopyButtonsToPreElements();
+  setupHoverTooltips();
 <% end %>
 
 <%= call_hook(:view_journals_update_js_bottom, { :journal => @journal }) %>
diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb
index 3432cb655..af2f850ad 100644
--- a/app/views/layouts/base.html.erb
+++ b/app/views/layouts/base.html.erb
@@ -12,6 +12,7 @@
 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
 <%= javascript_heads %>
 <%= heads_for_theme %>
+<%= heads_for_i18n %>
 <%= heads_for_auto_complete(@project) %>
 <%= call_hook :view_layouts_base_html_head %>
 
@@ -129,6 +130,7 @@
 
 
 
+
 
 
 <%= call_hook :view_layouts_base_body_bottom %>
diff --git a/app/views/queries/_filters.html.erb b/app/views/queries/_filters.html.erb
index a1118f6ab..42756775a 100644
--- a/app/views/queries/_filters.html.erb
+++ b/app/views/queries/_filters.html.erb
@@ -22,6 +22,5 @@ $(document).ready(function(){
 <%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %>
 
 
-
 <%= hidden_field_tag 'f[]', '' %>
 <% include_calendar_headers_tags %>
diff --git a/config/icon_source.yml b/config/icon_source.yml
index 8769a3212..ca4753dd7 100644
--- a/config/icon_source.yml
+++ b/config/icon_source.yml
@@ -209,3 +209,5 @@
   svg: square-rounded-plus
 - name: toggle-minus
   svg: square-rounded-minus
+- name: copy-pre-content
+  svg: clipboard
\ No newline at end of file
diff --git a/config/locales/en.yml b/config/locales/en.yml
index aa15095c6..3f48fe420 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1193,6 +1193,7 @@ en:
   button_copy: Copy
   button_copy_and_follow: Copy and follow
   button_copy_link: Copy link
+  button_copied: Copied
   button_annotate: Annotate
   button_fetch_changesets: Fetch commits
   button_update: Update
-- 
2.47.1