GitHub Gist Copier

Adds copy button to Gist files for easy code copying.

Ajankohdalta 7.4.2025. Katso uusin versio.

  1. // ==UserScript==
  2. // @name GitHub Gist Copier
  3. // @description Adds copy button to Gist files for easy code copying.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.1
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @run-at document-end
  11. // @match https://gist.github.com/*
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_setClipboard
  14. // @connect githubusercontent.com
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. function noop() { }
  21.  
  22. function debounce(f, delay) {
  23. let timeoutId = null;
  24. return function (...args) {
  25. if (timeoutId) {
  26. clearTimeout(timeoutId);
  27. }
  28. timeoutId = setTimeout(() => {
  29. f.apply(this, args);
  30. }, delay);
  31. };
  32. }
  33.  
  34. function createCopyButton(fileElement) {
  35. const fileActionElement = fileElement.querySelector('.file-actions');
  36. if (!fileActionElement) {
  37. return noop;
  38. }
  39.  
  40. const rawButton = fileActionElement.querySelector('a[href*="/raw/"]');
  41. if (!rawButton) {
  42. return noop;
  43. }
  44. const button = document.createElement('button');
  45. button.className = 'btn-octicon gist-copy-button';
  46. button.style.marginRight = '5px';
  47.  
  48. button.innerHTML = `
  49. <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-copy">
  50. <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path>
  51. </svg>
  52.  
  53. <svg style="display: none;" aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-check color-fg-success">
  54. <path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path>
  55. </svg>
  56. `;
  57.  
  58. const copyIcon = button.querySelector('.octicon-copy');
  59. const checkIcon = button.querySelector('.octicon-check');
  60.  
  61. let timeoutId = null;
  62. const copyHandler = (e) => {
  63. if (timeoutId) {
  64. return;
  65. }
  66. e.preventDefault();
  67.  
  68. const rawUrl = rawButton.href;
  69. GM_xmlhttpRequest({
  70. method: 'GET',
  71. url: rawUrl,
  72. onload: function(response) {
  73. if (response.status === 200) {
  74. GM_setClipboard(response.responseText, { type: 'text', mimetype: 'text/plain' });
  75. copyIcon.style.display = 'none';
  76. checkIcon.style.display = 'inline-block';
  77. timeoutId = setTimeout(() => {
  78. copyIcon.style.display = 'inline-block';
  79. checkIcon.style.display = 'none';
  80. timeoutId = null;
  81. }, 500);
  82. }
  83. },
  84. onerror: function(error) {
  85. timeoutId = null;
  86. }
  87. });
  88. };
  89.  
  90. button.addEventListener('click', copyHandler);
  91. fileActionElement.insertBefore(button, fileActionElement.firstChild);
  92.  
  93. return () => {
  94. button.removeEventListener('click', copyHandler);
  95. if (timeoutId) {
  96. clearTimeout(timeoutId);
  97. }
  98. button.remove();
  99. };
  100. }
  101.  
  102. function runGistCopy() {
  103. let removeAllListeners = noop;
  104. function tryCreateCopyButtons() {
  105. removeAllListeners();
  106. const fileElements = [...document.querySelectorAll('.file')];
  107. const removeListeners = fileElements.map(createCopyButton);
  108. removeAllListeners = () => {
  109. removeListeners.map((f) => f());
  110. [...document.querySelectorAll('.gist-copy-button')].forEach((el) => {
  111. el.remove();
  112. });
  113. };
  114. }
  115.  
  116. setTimeout(tryCreateCopyButtons, 300);
  117.  
  118. const observer = new MutationObserver(debounce(() => {
  119. if (document.querySelectorAll('.file').length > 0 &&
  120. document.querySelectorAll('.gist-copy-button').length === 0) {
  121. tryCreateCopyButtons();
  122. }
  123. }, 100));
  124.  
  125. observer.observe(document.body, {
  126. childList: true,
  127. subtree: true
  128. });
  129. if (window.onurlchange === null) {
  130. window.addEventListener('urlchange', debounce(tryCreateCopyButtons, 16));
  131. }
  132. }
  133.  
  134. runGistCopy();
  135. })();