GitHub Diff Files Filter

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

As of 2017-04-14. See the latest version.

  1. // ==UserScript==
  2. // @name GitHub Diff Files Filter
  3. // @version 0.1.6
  4. // @description A userscript that adds filters that toggle diff & PR files by extension
  5. // @license https://creativecommons.org/licenses/by-sa/4.0/
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://github.com/*
  9. // @run-at document-idle
  10. // @grant none
  11. // @require https://greatest.deepsurf.us/scripts/28721-mutations/code/mutations.js?version=188090
  12. // @icon https://github.com/fluidicon.png
  13. // ==/UserScript==
  14. (() => {
  15. "use strict";
  16.  
  17. const allExtLabel = "\u00ABall\u00BB",
  18. noExtLabel = "\u00ABno-ext\u00BB",
  19. dotExtLabel = "\u00ABdot-files\u00BB";
  20.  
  21. let list = {};
  22.  
  23. function toggleBlocks(extension, type) {
  24. const files = $("#files"),
  25. view = type === "show" ? "" : "none";
  26. if (extension === allExtLabel) {
  27. // Toggle "all" blocks
  28. $$("#files div[id*='diff']").forEach(el => {
  29. el.style.display = view;
  30. });
  31. // update filter buttons
  32. $$("#files .gdf-filter a").forEach(el => {
  33. el.classList.toggle("selected", type === "show");
  34. });
  35. } else if (list[extension]) {
  36. /* list[extension] contains an array of anchor names used to target the
  37. * hidden link added immediately above each file div container
  38. * <a name="diff-xxxxx"></a>
  39. * <div id="diff-#" class="file js-file js-details container">
  40. */
  41. list[extension].forEach(anchor => {
  42. const file = $(`a[name="${anchor}"]`, files);
  43. if (file && file.nextElementSibling) {
  44. file.nextElementSibling.style.display = view;
  45. }
  46. });
  47. }
  48. updateAllButton();
  49. }
  50.  
  51. function updateAllButton() {
  52. const buttons = $("#files .gdf-filter"),
  53. filters = $$("a:not(.gdf-all)", buttons),
  54. selected = $$("a:not(.gdf-all).selected", buttons);
  55. // set "all" button
  56. $(".gdf-all", buttons).classList.toggle(
  57. "selected",
  58. filters.length === selected.length
  59. );
  60. }
  61.  
  62. function buildList() {
  63. list = {};
  64. // make noExtLabel the first element in the object
  65. list[noExtLabel] = [];
  66. list[dotExtLabel] = [];
  67. // TOC in file diffs and pr-toolbar in Pull requests
  68. $$(".file-header .file-info > a").forEach(file => {
  69. let ext,
  70. txt = (file.textContent || "").trim(),
  71. filename = txt.split("/").slice(-1)[0];
  72. // test for no extension, then get extension name
  73. // regexp from https://github.com/silverwind/file-extension
  74. ext = /\./.test(filename) ? /[^./\\]*$/.exec(filename)[0] : noExtLabel;
  75. if (ext === filename.slice(1)) {
  76. ext = dotExtLabel;
  77. }
  78. if (ext) {
  79. if (!list[ext]) {
  80. list[ext] = [];
  81. }
  82. list[ext].push(
  83. file.hash ?
  84. // #toc points to "a"
  85. file.hash.slice(1) :
  86. // .pr-toolbar points to "a > div > div.filename"
  87. closest("a", file).hash.slice(1)
  88. );
  89. }
  90. });
  91. }
  92.  
  93. function makeFilter() {
  94. buildList();
  95. const files = $("#files");
  96. let filters = 0,
  97. keys = Object.keys(list),
  98. html = "Filter file extension: <div class='BtnGroup gdf-filter'>",
  99. btnClass = "btn btn-sm selected BtnGroup-item tooltipped tooltipped-n";
  100. // get length, but don't count empty arrays
  101. keys.forEach(ext => {
  102. filters += list[ext].length > 0 ? 1 : 0;
  103. });
  104. // Don't bother if only one extension is found
  105. if (files && filters > 1) {
  106. filters = $(".gdf-filter-wrapper");
  107. if (!filters) {
  108. filters = document.createElement("p");
  109. filters.className = "gdf-filter-wrapper";
  110. files.insertBefore(filters, files.firstChild);
  111. filters.addEventListener("click", event => {
  112. event.preventDefault();
  113. event.stopPropagation();
  114. const el = event.target;
  115. el.classList.toggle("selected");
  116. toggleBlocks(
  117. el.textContent.trim(),
  118. el.classList.contains("selected") ? "show" : "hide"
  119. );
  120. });
  121. }
  122. // add a filter "all" button to the beginning
  123. html += `
  124. <a class="${btnClass} gdf-all" aria-label="Toggle all files" href="#">
  125. ${allExtLabel}
  126. </a>`;
  127. keys.forEach(ext => {
  128. if (list[ext].length) {
  129. html += `
  130. <a class="${btnClass}" aria-label="${list[ext].length}" href="#">
  131. ${ext}
  132. </a>`;
  133. }
  134. });
  135. // prepend filter buttons
  136. filters.innerHTML = html + "</div>";
  137. }
  138. }
  139.  
  140. function init() {
  141. if ($("#files.diff-view") || $(".pr-toolbar")) {
  142. makeFilter();
  143. }
  144. }
  145.  
  146. function $(str, el) {
  147. return (el || document).querySelector(str);
  148. }
  149.  
  150. function $$(str, el) {
  151. return Array.from((el || document).querySelectorAll(str));
  152. }
  153.  
  154. function closest(selector, el) {
  155. while (el && el.nodeType === 1) {
  156. if (el.matches(selector)) {
  157. return el;
  158. }
  159. el = el.parentNode;
  160. }
  161. return null;
  162. }
  163.  
  164. document.addEventListener("ghmo:container", init);
  165. document.addEventListener("ghmo:diff", init);
  166. init();
  167.  
  168. })();