GitHub Diff Files Filter

A userscript that adds filters that toggle diff & PR folders, and files by extension

  1. // ==UserScript==
  2. // @name GitHub Diff Files Filter
  3. // @version 2.1.5
  4. // @description A userscript that adds filters that toggle diff & PR folders, and files by extension
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @match https://github.com/*
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // @require https://gf.qytechs.cn/scripts/28721-mutations/code/mutations.js?version=1108163
  12. // @icon https://github.githubassets.com/pinned-octocat.svg
  13. // @supportURL https://github.com/Mottie/GitHub-userscripts/issues
  14. // ==/UserScript==
  15.  
  16. (() => {
  17. "use strict";
  18.  
  19. // Example page: https://github.com/julmot/mark.js/pull/250/files
  20. GM_addStyle(".gdf-extension-hidden, .gdf-folder-hidden { display: none; }");
  21.  
  22. const allLabel = "\u00ABall\u00BB",
  23. rootLabel = "\u00ABroot\u00BB",
  24. noExtLabel = "\u00ABno-ext\u00BB",
  25. dotExtLabel = "\u00ABdot-files\u00BB",
  26. renameFileLabel = "\u00ABrenamed\u00BB",
  27. minFileLabel = "\u00ABmin\u00BB";
  28.  
  29. let exts = {};
  30. let folders = {};
  31.  
  32. function toggleBlocks({subgroup, type, show}) {
  33. if (type === allLabel) {
  34. // Toggle "all" blocks
  35. $$("#files div[id*='diff']").forEach(el => {
  36. el.classList.toggle(`gdf-${subgroup}-hidden`, !show);
  37. });
  38. // update filter buttons
  39. $$(`#files .gdf-${subgroup}-filter a`).forEach(el => {
  40. el.classList.toggle("selected", show);
  41. });
  42. } else if (subgroup === "folder") {
  43. Object.keys(folders)
  44. .reduce((acc, folder) => {
  45. if (folders[folder].length && !folder.includes("→")) {
  46. acc.push({
  47. folder,
  48. show: $(`.gdf-folder-filter a[data-item=${folder}]`).classList.contains("selected")
  49. });
  50. }
  51. return acc;
  52. }, [])
  53. // sort show:true to the end; to fix hiding files that should be shown
  54. .sort((a, b) => {
  55. if (a.show && b.show) {
  56. return 0;
  57. }
  58. return a.show && !b.show ? 1 : -1;
  59. })
  60. .forEach(({folder, show}) => {
  61. toggleGroup({group: folders[folder], subgroup, show });
  62. });
  63. } else if (exts[type]) {
  64. toggleGroup({group: exts[type], subgroup, show});
  65. }
  66. updateAllButton(subgroup);
  67. }
  68.  
  69. function toggleGroup({group, subgroup, show}) {
  70. const files = $("#files");
  71. /* group contains an array of div ids used to target the
  72. * hidden link added immediately above each file div container
  73. * <a name="diff-xxxxx"></a>
  74. * <div id="diff-#" class="file js-file js-details container">
  75. */
  76. group.forEach(id => {
  77. const file = $(`#${id}`, files);
  78. if (file) {
  79. file.classList.toggle(`gdf-${subgroup}-hidden`, !show);
  80. }
  81. });
  82. }
  83.  
  84. function updateAllButton(subgroup) {
  85. const buttons = $(`#files .gdf-${subgroup}-filter`),
  86. filters = $$(`a:not(.gdf-${subgroup}-all)`, buttons),
  87. selected = $$(`a:not(.gdf-${subgroup}-all).selected`, buttons);
  88. // set "all" button
  89. $(`.gdf-${subgroup}-all`, buttons).classList.toggle(
  90. "selected",
  91. filters.length === selected.length
  92. );
  93. }
  94.  
  95. function getSHA(file) {
  96. return file.hash
  97. // #toc points to "a"
  98. ? file.hash.slice(1)
  99. // .pr-toolbar points to "a > div > div.filename"
  100. : file.closest("a").hash.slice(1);
  101. }
  102.  
  103. function buildList() {
  104. exts = {};
  105. folders = {};
  106. // make noExtLabel the first element in the object
  107. exts[noExtLabel] = [];
  108. exts[dotExtLabel] = [];
  109. exts[renameFileLabel] = [];
  110. exts[minFileLabel] = [];
  111. folders[rootLabel] = [];
  112. // TOC in file diffs and pr-toolbar in Pull requests
  113. $$(".file-header .file-info > a").forEach(file => {
  114. let txt = (file.title || file.textContent || "").trim();
  115. if (txt) {
  116. const path = txt.split("/");
  117. const filename = path.splice(-1)[0];
  118. // test for no extension, then get extension name
  119. // regexp from https://github.com/silverwind/file-extension
  120. let ext = /\./.test(filename) ? /[^./\\]*$/.exec(filename)[0] : noExtLabel;
  121. const min = /\.min\./.test(filename);
  122. // Add filter for renamed files: {old path} → {new path}
  123. if (txt.indexOf(" → ") > -1) {
  124. ext = renameFileLabel;
  125. } else if (ext === filename.slice(1)) {
  126. ext = dotExtLabel;
  127. }
  128. const sha = getSHA(file);
  129. if (ext) {
  130. if (!exts[ext]) {
  131. exts[ext] = [];
  132. }
  133. exts[ext].push(sha);
  134. if (min) {
  135. exts[minFileLabel].push(sha);
  136. }
  137. }
  138. if (path.length > 0) {
  139. path.forEach(folder => {
  140. if (!folders[folder]) {
  141. folders[folder] = [];
  142. }
  143. folders[folder].push(sha);
  144. });
  145. } else {
  146. folders[rootLabel].push(sha);
  147. }
  148. }
  149. });
  150. }
  151.  
  152. function makeFilter({subgroup, label}) {
  153. const files = $("#files");
  154. let filters = 0;
  155. const group = subgroup === "folder" ? folders : exts;
  156. const keys = Object.keys(group);
  157. let html = `${label}: <div class="BtnGroup gdf-${subgroup}-filter">`;
  158. const btnClass = "btn btn-sm selected BtnGroup-item tooltipped tooltipped-n";
  159. // get length, but don't count empty arrays
  160. keys.forEach(item => {
  161. filters += group[item].length > 0 ? 1 : 0;
  162. });
  163. // Don't bother showing the filter if only one extension is found
  164. if (files && filters > 1) {
  165. filters = $(`.gdf-${subgroup}-filter-wrapper`);
  166. if (!filters) {
  167. filters = document.createElement("p");
  168. filters.className = `gdf-${subgroup}-filter-wrapper`;
  169. files.insertBefore(filters, files.firstChild);
  170. filters.addEventListener("click", event => {
  171. if (event.target.nodeName === "A") {
  172. event.preventDefault();
  173. event.stopPropagation();
  174. const el = event.target;
  175. el.classList.toggle("selected");
  176. toggleBlocks({
  177. subgroup: el.dataset.subgroup,
  178. type: el.textContent.trim(),
  179. show: el.classList.contains("selected")
  180. });
  181. }
  182. });
  183. }
  184. // add a filter "all" button to the beginning
  185. html += `
  186. <a class="${btnClass} gdf-${subgroup}-all" data-subgroup="${subgroup}" data-item="${allLabel}" aria-label="Toggle all files" href="#">
  187. ${allLabel}
  188. </a>`;
  189. keys.forEach(item => {
  190. if (group[item].length) {
  191. html += `
  192. <a class="${btnClass}" aria-label="${group[item].length}" data-subgroup="${subgroup}" data-item="${item}" href="#">
  193. ${item}
  194. </a>`;
  195. }
  196. });
  197. // prepend filter buttons
  198. filters.innerHTML = html + "</div>";
  199. }
  200. }
  201.  
  202. function init() {
  203. if ($("#files.diff-view") || $(".pr-toolbar")) {
  204. buildList();
  205. makeFilter({subgroup: "folder", label: "Filter file folder"});
  206. makeFilter({subgroup: "extension", label: "Filter file extension"});
  207. }
  208. }
  209.  
  210. function $(str, el) {
  211. return (el || document).querySelector(str);
  212. }
  213.  
  214. function $$(str, el) {
  215. return [...(el || document).querySelectorAll(str)];
  216. }
  217.  
  218. document.addEventListener("ghmo:container", init);
  219. document.addEventListener("ghmo:diff", init);
  220. init();
  221.  
  222. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址