GitHub Diff Files Filter

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

Pada tanggal 11 Juli 2020. Lihat %(latest_version_link).

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