GitHub Diff Files Filter

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

As of 2017-01-02. See the latest version.

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