返回首頁 

Greasy Fork is available in English.

GitHub Sort Content

A userscript that makes some lists & markdown tables sortable

  1. // ==UserScript==// @name GitHub Sort Content// @version 3.1.4// @description A userscript that makes some lists & markdown tables sortable// @license MIT// @author Rob Garrison// @namespace https://github.com/Mottie// @match https://github.com/*// @match https://gist.github.com/*// @run-at document-idle// @grant GM.addStyle// @grant GM_addStyle// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?updated=20180103// @require https://cdnjs.cloudflare.com/ajax/libs/tinysort/3.2.5/tinysort.min.js// @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163// @icon https://github.githubassets.com/pinned-octocat.svg// @supportURL https://github.com/Mottie/GitHub-userscripts/issues// ==/UserScript==/* global GM tinysort */(() => {"use strict";/** Example pages:* Tables (Readme & wikis) - https://github.com/Mottie/GitHub-userscripts* Repo files table - https://github.com/Mottie/GitHub-userscripts (sort content, message or age)* Activity - https://github.com (recent & all)* Sidebar - https://github.com/ (Repositories & Your teams)* Pinned repos (user & org)- https://github.com/(:user|:org)* Org Repos - https://github.com/:org* Org people - https://github.com/orgs/:org/people* Org outside collaborators (own orgs) - https://github.com/orgs/:org/outside-collaborators* Org teams - https://github.com/orgs/:org/teams & https://github.com/orgs/:org/teams/:team/teams* Org team repos - https://github.com/orgs/:org/teams/:team/repositories* Org team members - https://github.com/orgs/:org/teams/:team/members* Org projects - https://github.com/:org/projects* User repos - https://github.com/:user?tab=repositories* User stars - https://github.com/:user?tab=stars* watching - https://github.com/watching* User subscriptions - https://github.com/notifications/subscriptions* Repo stargazers - https://github.com/:user/:repo/stargazers* Repo watchers - https://github.com/:user/:repo/watchers*//*** sortables[entry].setup - exec on userscript init (optional);* param = window.location* sortables[entry].check - exec on doc.body click; return truthy/falsy or* header element (passed to the sort);* param = (event.target, window.location)* sortables[entry].sort - exec if check returns true or a header element;* param = (el) - the element returned by check or original click target* sortables[entry].css - specific css as an array of selectors, applied to* the entry elements; "unsorted", "ascending" (optional),* "descending" (optional), "tweaks" (optional)*/const sortables = {// markdown tables"tables": {check: el => el.nodeName === "TH" &&el.matches(".markdown-body table thead th"),sort: el => initSortTable(el),css: {unsorted: [".markdown-body table thead th",".markdown-body table.csv-data thead th"],tweaks: [`body .markdown-body table thead th {text-align: left;background-position: 3px center !important;}`]}},// repo files"repo-files": {check: el => el.classList.contains("ghsc-header-cell"),// init after a short delay to allow rendering of file listsetup: () => setTimeout(() => addRepoFileHeader(), 1e3),sort: el => initSortFiles(el),css: {unsorted: [".ghsc-header-cell"],tweaks: [`body .ghsc-header-cell {text-align: left;background-position: 3px center !important;}`]}},// github.com (all activity list)"all-activity": {check: el => $("#dashboard") &&el.classList.contains("js-all-activity-header"),sort: el => {const list = $$("div[data-repository-hovercards-enabled]:not(.js-details-container) > div");const wrap = list.parentElement;initSortList(el,list,{ selector: "relative-time", attr: "datetime" });// Move "More" button to bottomsetTimeout(() => {movePaginate(wrap);});},css: {unsorted: [".js-all-activity-header"],extras: ["div[data-repository-hovercards-enabled] div:empty { display: none; }"]}},// github.com (recent activity list)"recent-activity": {check: el => $("#dashboard") &&el.matches(".news > h2:not(.js-all-activity-header)"),sort: el => {initSortList(el,$$(".js-recent-activity-container ul li"),{ selector: "relative-time", attr: "datetime" });// Not sure why, but sorting shows all recent activity; so, hide the// "Show more" button$(".js-show-more-recent-items").classList.add("d-none");},css: {unsorted: [".news h2:not(.js-all-activity-header)"]}},// github.com (sidebar repos & teams)"sidebar": {check: el => $(".dashboard-sidebar") &&el.matches(".dashboard-sidebar h2"),sort: el => initSortList(el,$$(".list-style-none li", el.closest(".js-repos-container")),{ selector: "a" }),css: {unsorted: [".dashboard-sidebar h2"],tweaks: [`.dashboard-sidebar h2.pt-3 {background-position: left bottom !important;}`]}},// github.com/(:user|:org) (pinned repos)"pinned": {check: el => el.matches(".js-pinned-items-reorder-container h2"),sort: el => initSortList(el,// org li, own repos li$$(".js-pinned-items-reorder-list li, #choose-pinned-repositories ~ ol li"),{ selector: "a.text-bold" }),css: {unsorted: [".js-pinned-items-reorder-container h2"]}},// github.com/:org"org-repos": {setup: () => {const form = $("form[data-autosearch-r###lts-container='org-repositories']");if (form) {form.parentElement.classList.add("ghsc-org-repos-header");}},check: el => el.matches(".ghsc-org-repos-header"),sort: el => initSortList(el,$$(".org-repos li"),{ selector: "a[itemprop*='name']" }),css: {unsorted: [".ghsc-org-repos-header"],tweaks: [`form[data-autosearch-r###lts-container='org-repositories'] {cursor: default;}`]}},// github.com/orgs/:org/people// github.com/orgs/:org/outside-collaborators// github.com/orgs/:org/teams// github.com/orgs/:org/teams/:team/teams// github.com/orgs/:org/teams/:team/repositories"org-people+teams": {check: el => el.matches(".org-toolbar"),sort: el => {const lists = ["#org-members-table li","#org-outside-collaborators li","#org-teams li", // for :org/teams & :org/teams/:team/teams"#org-team-repositories li"].join(",");// Using a[id] returns a (possibly) truncated full name instead of// the GitHub handleinitSortList(el, $$(lists), { selector: "a[id], a.f4" });},css: {unsorted: [".org-toolbar"]}},// github.com/orgs/:org/teams/:team/members"team-members": {// no ".org-toolbar" on this page :(setup: () => {const form = $("form[data-autosearch-r###lts-container='team-members']");if (form) {form.parentElement.classList.add("ghsc-team-members-header");}},check: el => el.matches(".ghsc-team-members-header"),sort: el => initSortList(el, $$("#team-members li")),css: {unsorted: [".ghsc-team-members-header"]}},// github.com/orgs/:org/projects"org-projects": {setup: () => {const form = $("form[action$='/projects']");if (form) {form.parentElement.classList.add("ghsc-project-header");}},check: el => el.matches(".ghsc-project-header"),sort: el => initSortList(el,$$("#projects-r###lts > div"),{ selector: "h4 a" }),css: {unsorted: [".ghsc-project-header"]}},// github.com/:user?tab=repositories"user-repos": {setup: () => {const form = $("form[data-autosearch-r###lts-container='user-repositories-list']");if (form) {form.parentElement.classList.add("ghsc-repos-header");}},check: el => el.matches(".ghsc-repos-header"),sort: el => initSortList(el,$$("#user-repositories-list li"),{ selector: "a[itemprop*='name']" }),css: {unsorted: [".ghsc-repos-header"],tweaks: [`form[data-autosearch-r###lts-container='user-repositories-list'] {cursor: default;}`]}},// github.com/:user?tab=stars"user-stars": {setup: () => {const form = $("form[action$='?tab=stars']");if (form) {// filter form is wrapped in a details/summaryconst details = form.closest("details");if (details) {details.parentElement.classList.add("ghsc-stars-header");details.parentElement.title = "Sort list by repo name";}}},check: el => el.matches(".ghsc-stars-header"),sort: el => {const wrap = el.parentElement;const list = $$(".d-block", wrap);list.forEach(elm => {const a = $("h3 a", elm);a.dataset.text = a.textContent.split("/")[1];});initSortList(el, list, { selector: "h3 a", attr: "data-text" });movePaginate(wrap);},css: {unsorted: [".ghsc-stars-header"],tweaks: [`.ghsc-stars-header {background-position: left top !important;}`]}},// github.com/:user?tab=follow(ers|ing)"user-tab-follow": {setup: loc => {if (loc.search.includes("tab=follow")) {const tab = $("nav.UnderlineNav-body");if (tab) {tab.classList.add("ghsc-follow-nav");}}},check: (el, loc) => loc.search.indexOf("tab=follow") > -1 &&el.matches(".ghsc-follow-nav"),sort: el => {initSortList(el,$$(".position-relative .d-table"),{ selector: ".col-9 .link-gray" } // GitHub user name);movePaginate(wrap);},css: {unsorted: ["nav.ghsc-follow-nav"]}},// github.com/watching (watching table only)"user-watch": {setup: loc => {if (loc.href.indexOf("/watching") > -1) {const header = $(".tabnav");header.classList.add("ghsc-watching-header");header.title = "Sort list by repo name";}},check: el => el.matches(".ghsc-watching-header"),sort: el => {const list = $$(".standalone.repo-list li");list.forEach(elm => {const link = $("a", elm);link.dataset.sort = link.title.split("/")[1];});initSortList(el, list, { selector: "a", attr: "data-sort" });},css: {unsorted: [".ghsc-watching-header"]}},// github.com/notifications/subscriptions"user-subscriptions": {setup: loc => {if (loc.href.indexOf("/subscriptions") > -1) {const header = $(".tabnav");header.classList.add("ghsc-subs-header");header.title = "Sort list by repo name plus issue title";}},check: el => el.matches(".ghsc-subs-header"),sort: el => {const list = $$("li.notification-thread-subscription");initSortList(el, list, { selector: ".flex-auto" });},css: {unsorted: [".ghsc-subs-header"]}},// github.com/(:user|:org)/:repo/(stargazers|watchers)"repo-stars-or-watchers": {setup: loc => {if (loc.href.indexOf("/stargazers") > -1 ||loc.href.indexOf("/watchers") > -1) {$("#repos > h2").classList.add("ghsc-gazer-header");}},check: el => el.matches(".ghsc-gazer-header"),sort: el => initSortList(el,$$(".follow-list-item"),{ selector: ".follow-list-name" }),css: {unsorted: [".ghsc-gazer-header"]}}};const sorts = ["asc", "desc"];const icons = {unsorted: color => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="${color}"><path d="M15 8H1l7-8zm0 1H1l7 7z" opacity=".2"/></svg>`,ascending: color => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="${color}"><path d="M15 8H1l7-8z"/><path d="M15 9H1l7 7z" opacity=".2"/></svg>`,descending: color => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="${color}"><path d="M15 8H1l7-8z" opacity=".2"/><path d="M15 9H1l7 7z"/></svg>`};function getIcon(type, color) {return "data:image/svg+xml;charset=UTF-8," +encodeURIComponent(icons[type](color));}function needDarkTheme() {// color will be "rgb(#, #, #)" or "rgba(#, #, #, #)"let color = window.getComputedStyle(document.body).backgroundColor;const rgb = (color || "").replace(/\s/g, "").match(/^rgba?\((\d+),(\d+),(\d+)/i);if (rgb) {// remove "rgb.." part from match & parseconst colors = rgb.slice(1).map(Number);// http://stackoverflow.com/a/15794784/145346const brightest = Math.max(...colors);// return true if we have a dark backgroundreturn brightest < 128;}// fallback to bright backgroundreturn false;}function getDirection(el) {return (el.getAttribute("aria-sort") || "").includes(sorts[0])? sorts[1]: sorts[0];}function setDirection(els, currentElm, dir) {els.forEach(elm => {// aria-sort uses "ascending", "descending" or "none"const cellDir = currentElm === elm ? `${dir}ending` : "none";elm.setAttribute("aria-sort", cellDir);});}function initSortTable(el) {removeSelection();const dir = getDirection(el);const table = el.closest("table");const options = {order: dir,natural: true,selector: `td:nth-child(${el.cellIndex + 1})`};tinysort($$("tbody tr", table), options);setDirection($$("th", table), el, dir);}function addRepoFileHeader() {const $header = $("#files");// h2#files is a sibling of the grid wrapperconst $target = $header &&$("div[role='grid'] .sr-only", $header.parentElement);if ($header && $target) {$target.className = "Box-row Box-row--focus-gray py-2 d-flex position-relative js-navigation-item ghsc-header";$target.innerHTML = `<div role="gridcell" class="mr-3 flex-shrink-0" style="width: 16px;"></div><div role="columnheader" aria-sort="none" data-index="2" class="flex-auto min-width-0 col-md-2 mr-3 ghsc-header-cell">Content</div><div role="columnheader" aria-sort="none" data-index="3" class="flex-auto min-width-0 d-none d-md-block col-5 mr-3 ghsc-header-cell">Message</div><div role="columnheader" aria-sort="none" data-index="4" class="text-gray-light ghsc-age ghsc-header-cell" style="width:100px;">Age&nbsp;</div>`;}}function initSortFiles(el) {removeSelection();const dir = getDirection(el);const grid = el.closest("[role='grid']");const options = {order: dir,natural: true,selector: `div:nth-child(${el.dataset.index})`};if (el.classList.contains("ghsc-age")) {// sort repo age column using ISO 8601 datetime formatoptions.selector += " [datetime]";options.attr = "datetime";}// check for parent directory link; don't sort itconst parentDir = $("a[title*='parent dir']", grid);if (parentDir) {parentDir.closest("div[role='row']").classList.add("ghsc-header");}tinysort($$(".Box-row:not(.ghsc-header)", grid), options);setDirection($$(".ghsc-header-cell", grid), el, dir);}function initSortList(header, list, opts = {}) {if (list) {removeSelection();const dir = getDirection(header);const options = {order: dir,natural: true,place: "first", // Fixes nested ajax of main feed...opts};tinysort(list, options);setDirection([header], header, dir);}}function getCss(type) {return Object.keys(sortables).reduce((acc, block) => {const css = sortables[block].css || {};const selectors = css[type];if (selectors) {acc.push(...selectors);} else if (type !== "unsorted" && type !== "tweaks") {const useUnsorted = css.unsorted || [];if (useUnsorted.length) {// if "ascending" or "descending" isn't defined, then append// that class to the unsorted valueacc.push(`${useUnsorted.join(`[aria-sort='${type}'],`)}[aria-sort='${type}']`);}}return acc;}, []).join(type === "tweaks" ? "" : ",");}// The paginate block is a sibling along with the items in the list...// it needs to be moved to the endfunction movePaginate(wrapper) {const pager = wrapper &&$(".paginate-container, .ajax-pagination-form", wrapper);if (pager) {wrapper.append(pager);}}function $(str, el) {return (el || document).querySelector(str);}function $$(str, el) {return [...(el || document).querySelectorAll(str)];}function removeSelection() {// remove text selection - http://stackoverflow.com/a/3171348/145346const sel = window.getSelection ?window.getSelection() :document.selection;if (sel) {if (sel.removeAllRanges) {sel.removeAllRanges();} else if (sel.empty) {sel.empty();}}}function update() {Object.keys(sortables).forEach(item => {if (sortables[item].setup) {sortables[item].setup(window.location);}});}function init() {const color = needDarkTheme() ? "#ddd" : "#222";GM.addStyle(`/* Added table header */tr.ghsc-header th, tr.ghsc-header td {border-bottom: #eee 1px solid;padding: 2px 2px 2px 10px;}/* sort icons */${getCss("unsorted")} {cursor: pointer;padding-left: 22px !important;background-image: url(${getIcon("unsorted", color)}) !important;background-repeat: no-repeat !important;background-position: left center !important;}${getCss("ascending")} {background-image: url(${getIcon("ascending", color)}) !important;background-repeat: no-repeat !important;}${getCss("descending")} {background-image: url(${getIcon("descending", color)}) !important;background-repeat: no-repeat !important;}/* specific tweaks */${getCss("tweaks")}`);document.body.addEventListener("click", event => {const target = event.target;if (target && target.nodeType === 1) {Object.keys(sortables).some(item => {const el = sortables[item].check(target, window.location);if (el) {sortables[item].sort(el instanceof HTMLElement ? el : target);event.preventDefault();return true;}return false;});}});update();}document.addEventListener("ghmo:container", () => update());init();})();