GitHub Diff Files Filter

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

As of 2017-03-25. See the latest version.

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