GitHub Diff Files Filter

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

As of 05.10.2018. See ბოლო ვერსია.

  1. // ==UserScript==
  2. // @name GitHub Diff Files Filter
  3. // @version 1.1.1
  4. // @description A userscript that adds filters that toggle diff & PR 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 none
  11. // @require https://greatest.deepsurf.us/scripts/28721-mutations/code/mutations.js?version=634242
  12. // @icon https://assets-cdn.github.com/pinned-octocat.svg
  13. // ==/UserScript==
  14. (() => {
  15. "use strict";
  16.  
  17. const allExtLabel = "\u00ABall\u00BB",
  18. noExtLabel = "\u00ABno-ext\u00BB",
  19. dotExtLabel = "\u00ABdot-files\u00BB",
  20. renameFileLabel = "\u00ABrenamed\u00BB";
  21.  
  22. let list = {};
  23.  
  24. function toggleBlocks(extension, type) {
  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. }
  51.  
  52. function updateAllButton() {
  53. const buttons = $("#files .gdf-filter"),
  54. filters = $$("a:not(.gdf-all)", buttons),
  55. selected = $$("a:not(.gdf-all).selected", buttons);
  56. // set "all" button
  57. $(".gdf-all", buttons).classList.toggle(
  58. "selected",
  59. filters.length === selected.length
  60. );
  61. }
  62.  
  63. function buildList() {
  64. list = {};
  65. // make noExtLabel the first element in the object
  66. list[noExtLabel] = [];
  67. list[dotExtLabel] = [];
  68. list[renameFileLabel] = [];
  69. // TOC in file diffs and pr-toolbar in Pull requests
  70. $$(".file-header .file-info > a").forEach(file => {
  71. let txt = (file.title || file.textContent || "").trim(),
  72. filename = txt.split("/").splice(-1)[0],
  73. // test for no extension, then get extension name
  74. // regexp from https://github.com/silverwind/file-extension
  75. ext = /\./.test(filename) ? /[^./\\]*$/.exec(filename)[0] : noExtLabel;
  76. // Add filter for renamed files: {old path} → {new path}
  77. if (txt.indexOf(" → ") > -1) {
  78. ext = renameFileLabel;
  79. } else if (ext === filename.slice(1)) {
  80. ext = dotExtLabel;
  81. }
  82. if (ext) {
  83. if (!list[ext]) {
  84. list[ext] = [];
  85. }
  86. list[ext].push(
  87. file.hash
  88. // #toc points to "a"
  89. ? file.hash.slice(1)
  90. // .pr-toolbar points to "a > div > div.filename"
  91. : closest("a", file).hash.slice(1)
  92. );
  93. }
  94. });
  95. }
  96.  
  97. function makeFilter() {
  98. const files = $("#files");
  99. let filters = 0,
  100. keys = Object.keys(list),
  101. html = "Filter file extension: <div class='BtnGroup gdf-filter'>",
  102. btnClass = "btn btn-sm selected BtnGroup-item tooltipped tooltipped-n";
  103. // get length, but don't count empty arrays
  104. keys.forEach(ext => {
  105. filters += list[ext].length > 0 ? 1 : 0;
  106. });
  107. // Don't bother if only one extension is found
  108. if (files && filters > 1) {
  109. filters = $(".gdf-filter-wrapper");
  110. if (!filters) {
  111. filters = document.createElement("p");
  112. filters.className = "gdf-filter-wrapper";
  113. files.insertBefore(filters, files.firstChild);
  114. filters.addEventListener("click", event => {
  115. event.preventDefault();
  116. event.stopPropagation();
  117. const el = event.target;
  118. el.classList.toggle("selected");
  119. toggleBlocks(
  120. el.textContent.trim(),
  121. el.classList.contains("selected") ? "show" : "hide"
  122. );
  123. });
  124. }
  125. // add a filter "all" button to the beginning
  126. html += `
  127. <a class="${btnClass} gdf-all" aria-label="Toggle all files" href="#">
  128. ${allExtLabel}
  129. </a>`;
  130. keys.forEach(ext => {
  131. if (list[ext].length) {
  132. html += `
  133. <a class="${btnClass}" aria-label="${list[ext].length}" href="#">
  134. ${ext}
  135. </a>`;
  136. }
  137. });
  138. // prepend filter buttons
  139. filters.innerHTML = html + "</div>";
  140. }
  141. }
  142.  
  143. function init() {
  144. if ($("#files.diff-view") || $(".pr-toolbar")) {
  145. buildList();
  146. makeFilter();
  147. }
  148. }
  149.  
  150. function $(str, el) {
  151. return (el || document).querySelector(str);
  152. }
  153.  
  154. function $$(str, el) {
  155. return Array.from((el || document).querySelectorAll(str));
  156. }
  157.  
  158. function closest(selector, el) {
  159. while (el && el.nodeType === 1) {
  160. if (el.matches(selector)) {
  161. return el;
  162. }
  163. el = el.parentNode;
  164. }
  165. return null;
  166. }
  167.  
  168. document.addEventListener("ghmo:container", init);
  169. document.addEventListener("ghmo:diff", init);
  170. init();
  171.  
  172. })();