Greasy Fork is available in English.

GitHub Files Filter

A userscript that adds filters that toggle the view of repo files by extension

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

  1. // ==UserScript==
  2. // @name GitHub Files Filter
  3. // @version 1.0.1
  4. // @description A userscript that adds filters that toggle the view of repo 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_getValue
  11. // @grant GM_setValue
  12. // @require https://greatest.deepsurf.us/scripts/28721-mutations/code/mutations.js?version=234970
  13. // @icon https://assets-cdn.github.com/pinned-octocat.svg
  14. // ==/UserScript==
  15. (() => {
  16. "use strict";
  17.  
  18. let settings,
  19. list = {};
  20. const types = {
  21. // including ":" in key since it isn't allowed in a file name
  22. ":all": {
  23. // return false to prevent adding files under this type
  24. is: () => false,
  25. text: "\u00ABall\u00BB"
  26. },
  27. ":noExt": {
  28. is: name => !/\./.test(name),
  29. text: "\u00ABno-ext\u00BB"
  30. },
  31. ":dot": {
  32. // this will include ".travis.yml"... should we add to "yml" instead?
  33. is: name => /^\./.test(name),
  34. text: "\u00ABdot-files\u00BB"
  35. },
  36. ":min": {
  37. is: name => /\.min\./.test(name),
  38. text: "\u00ABmin\u00BB"
  39. }
  40. },
  41. // TODO: add toggle for submodule and dot-folders
  42. folderIconClasses = [
  43. ".octicon-file-directory",
  44. ".octicon-file-symlink-directory",
  45. ".octicon-file-submodule"
  46. ].join(",");
  47.  
  48. // default to all file types visible; remember settings between sessions
  49. list[":all"] = true; // list gets cleared in buildList function
  50. settings = GM_getValue("gff-filter-settings", list);
  51.  
  52. function updateFilter(event) {
  53. event.preventDefault();
  54. event.stopPropagation();
  55. const el = event.target;
  56. toggleBlocks(
  57. el.getAttribute("data-ext"),
  58. el.classList.contains("selected") ? "hide" : "show"
  59. );
  60. }
  61.  
  62. function updateSettings(name, mode) {
  63. settings[name] = mode === "show";
  64. GM_setValue("gff-filter-settings", settings);
  65. }
  66.  
  67. function updateAllButton() {
  68. if ($(".gff-filter")) {
  69. const buttons = $(".file-wrap .gff-filter"),
  70. filters = $$(".btn:not(.gff-all)", buttons),
  71. selected = $$(".btn:not(.gff-all).selected", buttons);
  72. // set "all" button
  73. $(".gff-all", buttons).classList.toggle(
  74. "selected",
  75. filters.length === selected.length
  76. );
  77. }
  78. }
  79.  
  80. function toggleImagePreview(ext, mode) {
  81. if ($(".ghip-image-previews")) {
  82. let selector = "a",
  83. hasType = types[ext];
  84. if (!hasType) {
  85. selector += `[href$="${ext}"]`;
  86. }
  87. $$(`.ghip-image-previews ${selector}`).forEach(el => {
  88. if (!$(".ghip-folder", el)) {
  89. if (hasType && ext !== ":all") {
  90. // image preview includes the filename
  91. let elm = $(".ghip-file-name", el);
  92. if (elm && !hasType.is(elm.textContent)) {
  93. return;
  94. }
  95. }
  96. el.style.display = mode === "show" ? "" : "none";
  97. }
  98. });
  99. }
  100. }
  101.  
  102. function toggleRow(el, mode) {
  103. const row = closest("tr.js-navigation-item", el);
  104. // don't toggle folders
  105. if (row && !$(folderIconClasses, row)) {
  106. row.style.display = mode === "show" ? "" : "none";
  107. }
  108. }
  109.  
  110. function toggleAll(mode) {
  111. const files = $(".file-wrap");
  112. // Toggle "all" blocks
  113. $$("td.content .js-navigation-open", files).forEach(el => {
  114. toggleRow(el, mode);
  115. });
  116. // update filter buttons
  117. $$(".gff-filter .btn", files).forEach(el => {
  118. el.classList.toggle("selected", mode === "show");
  119. });
  120. updateSettings(":all", mode);
  121. }
  122.  
  123. function toggleFilter(filter, mode) {
  124. const files = $(".file-wrap"),
  125. elm = $(`.gff-filter .btn[data-ext="${filter}"]`, files);
  126. /* list[filter] contains an array of file names */
  127. list[filter].forEach(name => {
  128. const el = $(`a[title="${name}"]`, files);
  129. if (el) {
  130. toggleRow(el, mode);
  131. }
  132. });
  133. if (elm) {
  134. elm.classList.toggle("selected", mode === "show");
  135. }
  136. updateSettings(filter, mode);
  137. }
  138.  
  139. function toggleBlocks(filter, mode) {
  140. if (filter === ":all") {
  141. toggleAll(mode);
  142. } else if (list[filter]) {
  143. toggleFilter(filter, mode);
  144. }
  145. // update view for github-image-preview.user.js
  146. toggleImagePreview(filter, mode);
  147. updateAllButton();
  148. }
  149.  
  150. function buildList() {
  151. list = {};
  152. Object.keys(types).forEach(item => {
  153. if (item !== ":all") {
  154. list[item] = [];
  155. }
  156. });
  157. // get all files
  158. $$("table.files tr.js-navigation-item").forEach(file => {
  159. if ($("td.icon .octicon-file", file)) {
  160. let ext,
  161. link = $("td.content .js-navigation-open", file),
  162. txt = (link.title || link.textContent || "").trim(),
  163. name = txt.split("/").slice(-1)[0];
  164. // test extension types; fallback to regex extraction
  165. ext = Object.keys(types).find(item => {
  166. return types[item].is(name);
  167. }) || /[^./\\]*$/.exec(name)[0];
  168. if (ext) {
  169. if (!list[ext]) {
  170. list[ext] = [];
  171. }
  172. list[ext].push(txt);
  173. }
  174. }
  175. });
  176. }
  177.  
  178. function sortList() {
  179. return Object.keys(list).sort((a, b) => {
  180. // move ":" filters to the beginning, then sort the rest of the
  181. // extensions; test on https://github.com/rbsec/sslscan, where
  182. // the ".1" extension *was* appearing between ":" filters
  183. if (a[0] === ":") {
  184. return -1;
  185. }
  186. if (b[0] === ":") {
  187. return 1;
  188. }
  189. return a > b;
  190. });
  191. }
  192.  
  193. function makeFilter() {
  194. let filters = 0;
  195. // get length, but don't count empty arrays
  196. Object.keys(list).forEach(ext => {
  197. filters += list[ext].length > 0 ? 1 : 0;
  198. });
  199. // Don't bother if only one extension is found
  200. const files = $(".file-wrap");
  201. if (files && filters > 1) {
  202. filters = $(".gff-filter-wrapper");
  203. if (!filters) {
  204. filters = document.createElement("div");
  205. // "commitinfo" allows GitHub-Dark styling
  206. filters.className = "gff-filter-wrapper commitinfo";
  207. filters.style = "padding:3px 5px 2px;border-bottom:1px solid #eaecef";
  208. files.insertBefore(filters, files.firstChild);
  209. filters.addEventListener("click", updateFilter);
  210. }
  211. fixWidth();
  212. buildHTML();
  213. applyInitSettings();
  214. }
  215. }
  216.  
  217. function buildButton(name, label, ext, text) {
  218. return `<button type="button" ` +
  219. `class="btn btn-sm selected BtnGroup-item tooltipped tooltipped-n` +
  220. (name ? name : "") + `" ` +
  221. `data-ext="${ext}" aria-label="${label}">${text}</button>`;
  222. }
  223.  
  224. function buildHTML() {
  225. let len,
  226. html = `<div class="BtnGroup gff-filter">` +
  227. // add a filter "all" button to the beginning
  228. buildButton(" gff-all", "Toggle all files", ":all", types[":all"].text);
  229. sortList().forEach(ext => {
  230. len = list[ext].length;
  231. if (len) {
  232. html += buildButton("", len, ext, types[ext] && types[ext].text || ext);
  233. }
  234. });
  235. // prepend filter buttons
  236. $(".gff-filter-wrapper").innerHTML = html + "</div>";
  237. }
  238.  
  239. function getWidth(el) {
  240. return parseFloat(window.getComputedStyle(el).width);
  241. }
  242.  
  243. // lock-in the table cell widths, or the navigation up link jumps when you
  244. // hide all files... using percentages in case someone is using GitHub wide
  245. function fixWidth() {
  246. let group, width,
  247. html = "",
  248. table = $("table.files"),
  249. tableWidth = getWidth(table),
  250. cells = $$("tbody:last-child tr:last-child td", table);
  251. if (table && cells.length > 1 && !$("colgroup", table)) {
  252. group = document.createElement("colgroup");
  253. table.insertBefore(group, table.childNodes[0]);
  254. cells.forEach(el => {
  255. // keep two decimal point accuracy
  256. width = parseInt(getWidth(el) / tableWidth * 1e4, 10) / 100;
  257. html += `<col style="width:${width}%">`;
  258. });
  259. group.innerHTML = html;
  260. }
  261. }
  262.  
  263. function applyInitSettings() {
  264. // list doesn't include type.all entry
  265. if (settings[":all"] === false) {
  266. toggleBlocks(":all", "hide");
  267. } else {
  268. Object.keys(list).forEach(name => {
  269. if (settings[name] === false) {
  270. toggleBlocks(name, "hide");
  271. }
  272. });
  273. }
  274. }
  275.  
  276. function init() {
  277. if ($("table.files")) {
  278. buildList();
  279. makeFilter();
  280. }
  281. }
  282.  
  283. function $(str, el) {
  284. return (el || document).querySelector(str);
  285. }
  286.  
  287. function $$(str, el) {
  288. return Array.from((el || document).querySelectorAll(str));
  289. }
  290.  
  291. function closest(selector, el) {
  292. while (el && el.nodeType === 1) {
  293. if (el.matches(selector)) {
  294. return el;
  295. }
  296. el = el.parentNode;
  297. }
  298. return null;
  299. }
  300.  
  301. document.addEventListener("ghmo:container", () => {
  302. // init after a short delay to allow rendering of file list
  303. setTimeout(() => {
  304. init();
  305. }, 200);
  306. });
  307. init();
  308.  
  309. })();