question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

(GitHub Files Filter) Feature suggestion: invert select

See original GitHub issue

Greetings @Mottie 🙂

Currently, ‘GitHub Files Filter’, only hides the files matching the activated filters.

My suggestion is to also be able to invert select, i.e. only show the matching files, e.g. with Ctrl+click. For example, in your repo, when Ctrl+clicking MD: 2019-08-03_132328 to only display .md files, i.e. to strikethrough the rest filters. And, if you keep Ctrl+clicking, e.g. also Ctrl+click «dot files» to only display .md and dot files .

I hope you like my suggestion !

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:17 (7 by maintainers)

github_iconTop GitHub Comments

2reactions
Mottiecommented, Sep 23, 2019

I just updated the files filter userscript.

I didn’t:

  • Move the invert button to the right. It’ll look weird when there are a bunch of filter buttons that wrap around to the next line. I like it on the left - and I changed the icon.
  • Add a method to allow you to continue to use <kbd>Ctrl</kbd> + click. I mean, when you use it, it enables the selected filter and disables all the others. Continuing to use <kbd>Ctrl</kbd> + click will continue this behavior. To enable more file types, use a plain click.
2reactions
Mottiecommented, Sep 6, 2019

Ok try out this script; if it works like you want, I’ll go ahead and commit it:

Files-filter
// ==UserScript==
// @name        GitHub Files Filter
// @version     1.1.8
// @description A userscript that adds filters that toggle the view of repo files by extension
// @license     MIT
// @author      Rob Garrison
// @namespace   https://github.com/Mottie
// @include     https://github.com/*
// @run-at      document-idle
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @require     https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=666427
// @icon        https://github.githubassets.com/pinned-octocat.svg
// @updateURL   https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-files-filter.user.js
// @downloadURL https://raw.githubusercontent.com/Mottie/GitHub-userscripts/master/github-files-filter.user.js
// ==/UserScript==
(() => {
	"use strict";

	// Emphasize selected buttons, disable hover when all selected and remove
	// animation delay; See #46
	GM_addStyle(`
		.gff-filter .btn.selected { font-variant: small-caps; }
		.gff-filter .btn:not(.selected):not(:first-child) {
			text-decoration: line-through;
		}
		.gff-filter .gff-all:not(.selected):focus,
		.gff-filter .gff-all:not(.selected) ~ .btn:focus,
		.gff-filter .gff-all:not(.selected) ~ .btn.selected:focus,
		.gff-filter .gff-all:not(.selected):hover,
		.gff-filter .gff-all:not(.selected) ~ .btn:hover,
		.gff-filter .gff-all:not(.selected) ~ .btn.selected:hover {
			border-color: #777 !important;
		}
		.gff-filter .btn:before, .gff-filter .btn:after {
			animation-delay: unset !important;
			filter: invert(10%);
		}
	`);

	let settings,
		inverted = [],
		list = {};
	const types = {
			// including ":" in key since it isn't allowed in a file name
			":all": {
				// return false to prevent adding files under this type
				is: () => false,
				text: "\u00ABtoggle\u00BB"
			},
			":noExt": {
				is: name => !/\./.test(name),
				text: "\u00ABno-ext\u00BB"
			},
			":dot": {
				// this will include ".travis.yml"... should we add to "yml" instead?
				is: name => /^\./.test(name),
				text: "\u00ABdot-files\u00BB"
			},
			":min": {
				is: name => /\.min\./.test(name),
				text: "\u00ABmin\u00BB"
			}
		},
		// TODO: add toggle for submodule and dot-folders
		folderIconClasses = [
			".octicon-file-directory",
			".octicon-file-symlink-directory",
			".octicon-file-submodule"
		].join(",");

	// default to all file types visible; remember settings between sessions
	list[":all"] = true; // list gets cleared in buildList function
	settings = GM_getValue("gff-filter-settings", list);

	function updateFilter(event) {
		event.preventDefault();
		event.stopPropagation();
		const el = event.target;
		toggleBlocks(
			el.getAttribute("data-ext"),
			el.classList.contains("selected") ? "hide" : "show",
			event.ctrlKey
		);
	}

	function updateSettings(name, mode) {
		settings[name] = mode === "show";
		GM_setValue("gff-filter-settings", settings);
	}

	function toggleImagePreview(ext, mode) {
		if ($(".ghip-image-previews")) {
			let selector = "a",
				hasType = types[ext];
			if (!hasType) {
				selector += `[href$="${ext}"]`;
			}
			$$(`.ghip-image-previews ${selector}`).forEach(el => {
				if (!$(".ghip-folder, .ghip-up-tree", el)) {
					if (hasType && ext !== ":all") {
						// image preview includes the filename
						let elm = $(".ghip-file-name", el);
						if (elm && !hasType.is(elm.textContent)) {
							return;
						}
					}
					el.style.display = mode === "show" ? "" : "none";
				}
			});
		}
	}

	function toggleRow(el, mode) {
		const row = el.closest("tr.js-navigation-item");
		// don't toggle folders
		if (row && !$(folderIconClasses, row)) {
			let state;
			if (mode) {
				state = mode === "show" ? "" : "none";
			} else {
				// toggle
				state = row.style.display === "none" ? "" : "none";
			}
			row.style.display = state;
		}
	}

	function toggleAll() {
		const files = $(".file-wrap");
		// Toggle all blocks
		$$("td.content .js-navigation-open", files).forEach(el => {
			toggleRow(el);
		});
		// update filter buttons
		$$(".gff-filter .btn", files).forEach(el => {
			el.classList.toggle("selected");
		});
	}

	function toggleFilter(filter, mode) {
		const files = $(".file-wrap"),
			elm = $(`.gff-filter .btn[data-ext="${filter}"]`, files);
		/* list[filter] contains an array of file names */
		list[filter].forEach(name => {
			const el = $(`a[title="${name}"]`, files);
			if (el) {
				toggleRow(el, mode);
			}
		});
		if (elm) {
			elm.classList.toggle("selected", mode === "show");
		}
		updateSettings(filter, mode);
	}

	function toggleBlocks(filter, mode) {
		if (filter === ":all") {
			toggleAll();
		} else if (list[filter]) {
			toggleFilter(filter, mode);
		}
		// update view for github-image-preview.user.js
		toggleImagePreview(filter, mode);
	}

	function addExt(ext, txt) {
		if (ext) {
			if (!list[ext]) {
				list[ext] = [];
			}
			list[ext].push(txt);
		}
	}

	function buildList() {
		list = {};
		Object.keys(types).forEach(item => {
			if (item !== ":all") {
				list[item] = [];
			}
		});
		// get all files
		$$("table.files tr.js-navigation-item").forEach(file => {
			if ($("td.icon .octicon-file", file)) {
				let ext, parts, sub,
					link = $("td.content .js-navigation-open", file),
					txt = (link.title || link.textContent || "").trim(),
					name = txt.split("/").slice(-1)[0];
				// test extension types; fallback to regex extraction
				ext = Object.keys(types).find(item => {
					return types[item].is(name);
				}) || /[^./\\]*$/.exec(name)[0];
				parts = name.split(".");
				if (!ext.startsWith(":") && parts.length > 2 && parts[0] !== "") {
					sub = parts.slice(0, -1).join(".");
					// Prevent version numbers & "vs. " from adding a filter button
					// See https://github.com/tpn/pdfs
					if (!/[()]/.test(sub) && !/[\b\w]\.[\b\d]/.test(sub)) {
						addExt(ext, txt);
						ext = parts.slice(-2).join(".");
					}
				}
				addExt(ext, txt);
			}
		});
	}

	function sortList() {
		return Object.keys(list).sort((a, b) => {
			// move ":" filters to the beginning, then sort the rest of the
			// extensions; test on https://github.com/rbsec/sslscan, where
			// the ".1" extension *was* appearing between ":" filters
			if (a[0] === ":") {
				return -1;
			}
			if (b[0] === ":") {
				return 1;
			}
			return a > b;
		});
	}

	function makeFilter() {
		let filters = 0;
		// get length, but don't count empty arrays
		Object.keys(list).forEach(ext => {
			filters += list[ext].length > 0 ? 1 : 0;
		});
		// Don't bother if only one extension is found
		const files = $(".file-wrap");
		if (files && filters > 1) {
			filters = $(".gff-filter-wrapper");
			if (!filters) {
				filters = document.createElement("div");
				// "commitinfo" allows GitHub-Dark styling
				filters.className = "gff-filter-wrapper commitinfo";
				filters.style = "padding:3px 5px 2px;border-bottom:1px solid #eaecef";
				files.insertBefore(filters, files.firstChild);
			}
			fixWidth();
			buildHTML();
			applyInitSettings();
		}
	}

	function buildButton(name, label, ext, text) {
		return `<button type="button" ` +
			`class="btn btn-sm selected BtnGroup-item tooltipped tooltipped-n` +
			(name ? name : "") + ` gff-btn" ` +
			`data-ext="${ext}" aria-label="${label}">${text}</button>`;
	}

	function buildHTML() {
		let len,
			html = `<div class="BtnGroup gff-filter">` +
				// add a filter "all" button to the beginning
				buildButton(" gff-all", "Toggle all files", ":all", types[":all"].text);
		sortList().forEach(ext => {
			len = list[ext].length;
			if (len) {
				html += buildButton("", len, ext, types[ext] && types[ext].text || ext);
			}
		});
		// prepend filter buttons
		$(".gff-filter-wrapper").innerHTML = html + "</div>";
	}

	function getWidth(el) {
		return parseFloat(window.getComputedStyle(el).width);
	}

	// lock-in the table cell widths, or the navigation up link jumps when you
	// hide all files... using percentages in case someone is using GitHub wide
	function fixWidth() {
		let group, width,
			html = "",
			table = $("table.files"),
			tableWidth = getWidth(table),
			cells = $$("tbody:last-child tr:last-child td", table);
		if (table && cells.length > 1 && !$("colgroup", table)) {
			group = document.createElement("colgroup");
			table.insertBefore(group, table.childNodes[0]);
			cells.forEach(el => {
				// keep two decimal point accuracy
				width = parseInt(getWidth(el) / tableWidth * 1e4, 10) / 100;
				html += `<col style="width:${width}%">`;
			});
			group.innerHTML = html;
		}
	}

	function applyInitSettings() {
		Object.keys(list).forEach(name => {
			if (settings[name] === false) {
				toggleBlocks(name, "hide");
			}
		});
	}

	function init() {
		if ($("table.files")) {
			buildList();
			makeFilter();
		}
	}

	function $(str, el) {
		return (el || document).querySelector(str);
	}

	function $$(str, el) {
		return Array.from((el || document).querySelectorAll(str));
	}

	document.addEventListener("click", e => {
		if (e.target.classList.contains("gff-btn")) {
			updateFilter(e);
		}
	});

	document.addEventListener("ghmo:container", () => {
		// init after a short delay to allow rendering of file list
		setTimeout(() => {
			init();
		}, 200);
	});
	init();

})();
Read more comments on GitHub >

github_iconTop Results From Across the Web

Filtering files in a pull request - GitHub Docs
You can filter files in a pull request by file extension type, ... Use the File filter drop-down menu, and select, unselect, or...
Read more >
Filtering and searching issues and pull requests - GitHub Docs
Under your repository name, click Issues or Pull requests. Issues and pull requests tab selection; Click Filters to choose the type of filter...
Read more >
feat: inversion selection support for the resource filter on sync ...
Implements the --resource flag enhancement. Sync and Wait commands to manage applications can now be filtered with negative selection of the resources. Matching ......
Read more >
filter/hide/invert selection #27 - IFCjs/web-ifc-viewer - GitHub
filter out a named item/outline by clicking to get information about the item/name. invert selection (select everything but the selection); hide items based ......
Read more >
Filtering files in a pull request - GitHub AE Docs
You can filter files in a pull request by file extension type, ... Use the File filter drop-down menu, and select, unselect, or...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found