GitHub Sort Content

A userscript that makes some lists & markdown tables sortable

当前为 2019-01-29 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Sort Content
  3. // @version 2.0.3
  4. // @description A userscript that makes some lists & markdown tables sortable
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://github.com/*
  9. // @include https://gist.github.com/*
  10. // @run-at document-idle
  11. // @grant GM.addStyle
  12. // @grant GM_addStyle
  13. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?updated=20180103
  14. // @require https://cdnjs.cloudflare.com/ajax/libs/tinysort/2.3.6/tinysort.min.js
  15. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=666427
  16. // @icon https://assets-cdn.github.com/pinned-octocat.svg
  17. // ==/UserScript==
  18. (() => {
  19. "use strict";
  20. /** Example pages:
  21. * Tables (Readme & wikis) - https://github.com/Mottie/GitHub-userscripts
  22. * Repo files - https://github.com/Mottie/GitHub-userscripts (sort content, message or age)
  23. * Your Repos & Your Teams - https://github.com/
  24. * Pinned repos (org & user)- https://github.com/:org
  25. * Organization repos - https://github.com/:org
  26. * Organization people - https://github.com/orgs/:org/people
  27. * Organization outside collaborators (own orgs) - https://github.com/orgs/:org/outside-collaborators
  28. * Organization teams - https://github.com/orgs/:org/teams
  29. * Repo stargazers - https://github.com/:user/:repo/stargazers
  30. * Repo watchers - https://github.com/:user/:repo/watchers
  31. * User repos - https://github.com/:user?tab=repositories
  32. * User stars - https://github.com/:user?tab=stars
  33. * User Followers - https://github.com/:user?tab=followers & https://github.com/:user/followers(/you_know)
  34. * User Following - https://github.com/:user?tab=following & https://github.com/:user/following(/you_know)
  35. * watching - https://github.com/watching
  36. */
  37. /**
  38. * sortables[entry].setup - exec on userscript init (optional)
  39. * sortables[entry].check - exec on doc.body click; return truthy/falsy or
  40. * header element (passed to the sort)
  41. * sortables[entry].sort - exec if check returns true or a header element;
  42. * el param is the element returned by check or original click target
  43. */
  44. const sortables = {
  45. // markdown tables
  46. "tables": {
  47. // init after a short delay to allow rendering of file list
  48. setup: () => setTimeout(() => addRepoFileThead(), 200),
  49. check: el => el.nodeName === "TH" &&
  50. el.matches(".markdown-body table thead th, table.files thead th"),
  51. sort: el => initSortTable(el)
  52. },
  53. // https://github.com (repo list & teams list)
  54. "feed": {
  55. check: el => el.classList.contains("Box-title") &&
  56. el.closest(".Box.js-repos-container"),
  57. sort: el => initSortUl(el, $$(".Box-body li", el.closest(".Box")))
  58. },
  59. // https://github.com/orgs/:org/dashboard (repo list)
  60. "org-feed": {
  61. check: el => el.classList.contains("Box-title") &&
  62. el.closest("#org_your_repos.js-repos-container"),
  63. sort: el => initSortUl(el, $$(".boxed-group-inner li", el))
  64. },
  65. // https://github.com/(:user|:org) (pinned repos)
  66. "pinned": {
  67. check: el => $(".js-pinned-repos-reorder-container") &&
  68. el.matches(".org-profile.js-pinned-repos-reorder-container h2, .user-profile-nav"),
  69. sort: el => initSortUl(el, $(".pinned-repos-list").children)
  70. },
  71. // https://github.com/:org
  72. "org-repos": {
  73. check: el => {
  74. // Org repos have weirdly nested forms if there are pinned repos
  75. let wrap = false;
  76. if ($(".org-repos.repo-list") && el.matches(".TableObject, .TableObject-item")) {
  77. wrap = el.closest("form[data-pjax='#org-repositories']");
  78. if (wrap) {
  79. wrap = wrap.parentNode;
  80. } else {
  81. wrap = el;
  82. }
  83. return wrap && wrap.classList.contains("TableObject") ? wrap : false;
  84. }
  85. return wrap;
  86. },
  87. sort: el => {
  88. const list = $(".org-repos.repo-list");
  89. initSortUl(el, list.children);
  90. movePaginate(list);
  91. }
  92. },
  93. // https://github.com/orgs/:org/people
  94. "org-people": {
  95. setup: () => checkOwnOrg(),
  96. check: (el, loc) => loc.href.indexOf("/people") > -1 &&
  97. $("#org-members-table") && el.matches(".org-toolbar.ghsc-org-people"),
  98. sort: el => initSortUl(el, $$("#org-members-table li"), ".member-info a")
  99. },
  100. // https://github.com/orgs/:org/outside-collaborators (own org)
  101. "org-collab-own": {
  102. check: (el, loc) => loc.href.indexOf("/outside-collaborators") > -1 &&
  103. $("#org-outside-collaborators") && el.matches(".org-toolbar.ghsc-org-outside_collaborators"),
  104. sort: el => initSortUl(el, $$("#org-outside-collaborators li"), ".member-info a")
  105. },
  106. // https://github.com/orgs/:org/teams
  107. "org-teams": {
  108. check: el => $("#org-teams") && el.matches(".ghsc-org-teams.subnav.org-toolbar"),
  109. sort: el => initSortUl(el, $$("#org-teams li"), ".team-name")
  110. },
  111. // https://github.com/:user?tab=repositories
  112. "user-repos": {
  113. check: (el, loc) => loc.search.indexOf("tab=repositories") > -1 &&
  114. el.classList.contains("user-profile-nav"),
  115. sort: el => initSortUl(el, $$("#user-repositories-list li"))
  116. },
  117. // https://github.com/:user?tab=stars
  118. "user-stars": {
  119. check: (el, loc) => loc.search.indexOf("tab=stars") > -1 &&
  120. el.classList.contains("user-profile-nav"),
  121. sort: el => {
  122. const list = $(".TableObject").parentNode;
  123. initSortUl(el, $$(".col-12", list), "h3 a");
  124. movePaginate(list);
  125. }
  126. },
  127. // https://github.com/:user?tab=follow(ers|ing)
  128. "user-tab-follow": {
  129. check: (el, loc) => loc.search.indexOf("tab=follow") > -1 &&
  130. el.classList.contains("user-profile-nav"),
  131. sort: el => {
  132. const list = $(".table-fixed").parentNode;
  133. initSortUl(el, $$(".col-12", list), ".col-9 a.no-underline");
  134. movePaginate(list);
  135. }
  136. },
  137. // https://github.com/:user/follow(ers|ing)
  138. // https://github.com/:user/follow(ers|ing)/you_know
  139. "user-follow": {
  140. setup: () => {
  141. if (window.location.href.indexOf("/follow") > -1) {
  142. const repo = $(".userrepos, .follow-list");
  143. const wrap = repo && repo.closest(".container");
  144. if (wrap) {
  145. $("h2", wrap).classList.add("ghsc-header");
  146. repo.classList.add("ghsc-active");
  147. }
  148. }
  149. },
  150. check: el => $(".userrepos.ghsc-active, .follow-list.ghsc-active") && el.matches("h2.ghsc-header"),
  151. sort: el => initSortUl(el, $$(".userrepos li, .follow-list li"), ".follow-list-name")
  152. },
  153. // https://github.com/watching
  154. "user-watch": {
  155. check: (el, loc) => loc.href.indexOf("/watching") > -1 &&
  156. el.matches(".subscriptions-content .Box-header h3, .subscriptions-content .Box-header .text-right"),
  157. sort: el => initSortUl(el.closest(".Box-header"), $$(".standalone.repo-list li"))
  158. },
  159. // https://github.com/:user/repo/(stargazers|watchers)
  160. "repo-stars-or-watchers": {
  161. check: (el, loc) => (loc.href.indexOf("/stargazers") > -1 ||
  162. loc.href.indexOf("/watchers") > -1) &&
  163. $(".follow-list") && el.matches("#repos > h2"),
  164. sort: el => initSortUl(el, $$(".follow-list-item"), ".follow-list-name")
  165. }
  166. };
  167.  
  168. const sorts = ["asc", "desc"];
  169.  
  170. const icons = {
  171. unsorted: color => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="${color}">
  172. <path d="M15 8H1l7-8zm0 1H1l7 7z" opacity=".2"/>
  173. </svg>`,
  174. asc: color => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="${color}">
  175. <path d="M15 8H1l7-8z"/>
  176. <path d="M15 9H1l7 7z" opacity=".2"/>
  177. </svg>`,
  178. desc: color => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="${color}">
  179. <path d="M15 8H1l7-8z" opacity=".2"/>
  180. <path d="M15 9H1l7 7z"/>
  181. </svg>`
  182. };
  183.  
  184. function getIcon(type, color) {
  185. return "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(icons[type](color));
  186. }
  187.  
  188. function needDarkTheme() {
  189. // color will be "rgb(#, #, #)" or "rgba(#, #, #, #)"
  190. let color = window.getComputedStyle(document.body).backgroundColor;
  191. const rgb = (color || "")
  192. .replace(/\s/g, "")
  193. .match(/^rgba?\((\d+),(\d+),(\d+)/i);
  194. if (rgb) {
  195. // remove "rgb.." part from match & parse
  196. const colors = rgb.slice(1).map(Number);
  197. // http://stackoverflow.com/a/15794784/145346
  198. const brightest = Math.max.apply(null, colors);
  199. // return true if we have a dark background
  200. return brightest < 128;
  201. }
  202. // fallback to bright background
  203. return false;
  204. }
  205.  
  206. function addRepoFileThead() {
  207. const $table = $("table.files");
  208. if ($table && !$(".ghsc-header", $table)) {
  209. const thead = document.createElement("thead");
  210. thead.innerHTML = `<tr class="ghsc-header">
  211. <td></td>
  212. <th>Content</th>
  213. <th>Message</th>
  214. <th class="ghsc-age">Age</th>
  215. </tr>`;
  216. $table.insertBefore(thead, $table.childNodes[0]);
  217. }
  218. }
  219.  
  220. function initSortTable(el) {
  221. removeSelection();
  222. const dir = el.classList.contains(sorts[0]) ? sorts[1] : sorts[0],
  223. table = el.closest("table"),
  224. options = {
  225. order: dir,
  226. natural: true,
  227. selector: `td:nth-child(${el.cellIndex + 1})`
  228. };
  229. if (el.classList.contains("ghsc-age")) {
  230. // sort repo age column using ISO 8601 datetime format
  231. options.selector += " [datetime]";
  232. options.attr = "datetime";
  233. }
  234. tinysort($$("tbody tr:not(.up-tree)", table), options);
  235. $$("th", table).forEach(elm => {
  236. elm.classList.remove(...sorts);
  237. });
  238. el.classList.add(dir);
  239. }
  240.  
  241. function initSortUl(arrows, list, selector) {
  242. if (list) {
  243. removeSelection();
  244. const dir = arrows.classList.contains(sorts[0]) ? sorts[1] : sorts[0],
  245. options = {
  246. order: dir,
  247. natural: true
  248. };
  249. if (selector) {
  250. options.selector = selector;
  251. }
  252. tinysort(list, options);
  253. arrows.classList.remove(...sorts);
  254. arrows.classList.add(dir);
  255. }
  256. }
  257.  
  258. function getFixedHeader() {
  259. // Is https://github.com/StylishThemes/GitHub-FixedHeader active?
  260. const header = window.getComputedStyle($(".Header"));
  261. const height = header.position === "fixed" && parseInt(header.height, 10);
  262. // Adjust sort arrow position
  263. return height ?
  264. `.user-profile-nav.js-sticky.is-stuck {
  265. background-position:calc(100% - 5px) ${height + 20}px !important;
  266. }` : "";
  267. }
  268.  
  269. // The paginate block is a sibling along with the items in the list...
  270. // it needs to be moved to the end
  271. function movePaginate(list) {
  272. list.appendChild($(".paginate-container", list));
  273. }
  274.  
  275. // Own organization repo has admin stuff, so the layout needs to be
  276. // adjusted slightly
  277. function checkOwnOrg() {
  278. // div[data-bulk-actions-url$="people/toolbar_actions"] .subnav.org-toolbar
  279. const el = $(".subnav.org-toolbar");
  280. const wrapper = el && el.closest("div[data-bulk-actions-url]");
  281. if (wrapper) {
  282. // "/orgs/:org/people/toolbar_actions"
  283. const type = wrapper.getAttribute("data-bulk-actions-url").split("/")[3]
  284. el.classList.add("ghsc-org", `ghsc-org-${type}`);
  285. }
  286. // Own org people
  287. if (
  288. sortables["org-people"].check(el, window.location) &&
  289. $(".member-list-item.adminable")
  290. ) {
  291. // Own org shows an admin table
  292. el.classList.add("ghsc-own-org");
  293. }
  294. }
  295.  
  296. function $(str, el) {
  297. return (el || document).querySelector(str);
  298. }
  299.  
  300. function $$(str, el) {
  301. return [...(el || document).querySelectorAll(str)];
  302. }
  303.  
  304. function removeSelection() {
  305. // remove text selection - http://stackoverflow.com/a/3171348/145346
  306. const sel = window.getSelection ?
  307. window.getSelection() :
  308. document.selection;
  309. if (sel) {
  310. if (sel.removeAllRanges) {
  311. sel.removeAllRanges();
  312. } else if (sel.empty) {
  313. sel.empty();
  314. }
  315. }
  316. }
  317.  
  318. function update() {
  319. Object.keys(sortables).forEach(item => {
  320. if (sortables[item].setup) {
  321. sortables[item].setup();
  322. }
  323. });
  324. }
  325.  
  326. function init() {
  327. const color = needDarkTheme() ? "#ddd" : "#222";
  328. const userSortPosition = getFixedHeader();
  329.  
  330. GM.addStyle(`
  331. tr.ghsc-header th, tr.ghsc-header td {
  332. border-bottom: #eee 1px solid;
  333. padding: 2px 2px 2px 10px;
  334. }
  335. /* unsorted icon */
  336. .markdown-body table thead th, table.files thead th,
  337. .markdown-body table.csv-data thead th {
  338. cursor: pointer;
  339. padding-right: 22px !important;
  340. background-image: url(${getIcon("unsorted", color)}) !important;
  341. background-repeat: no-repeat !important;
  342. background-position: calc(100% - 5px) center !important;
  343. text-align: left;
  344. }
  345. .js-repos-container h3.Box-title,
  346. #org_your_repos h3.Box-title,
  347. .org-profile .TableObject:first-child,
  348. .ghsc-org.subnav.org-toolbar,
  349. .user-profile-nav.js-sticky,
  350. .user-profile-nav.js-sticky.is-stuck,
  351. .org-profile.js-pinned-repos-reorder-container h2,
  352. .subscriptions-content .Box-header .text-right,
  353. #repos > h2,
  354. h2.ghsc-header {
  355. cursor:pointer;
  356. background-image: url(${getIcon("unsorted", color)}) !important;
  357. background-repeat: no-repeat !important;
  358. background-position: calc(100% - 5px) center !important;
  359. }
  360. /* https://github.com/ -> your repositories */
  361. .dashboard-sidebar .js-repos-container h3 {
  362. background-position: 115px 5px !important;
  363. }
  364. /* https://github.com/ -> your teams */
  365. .dashboard-sidebar #your_teams h3 {
  366. background-position: 240px 10px !important;
  367. }
  368. /* pinned repos */
  369. .org-profile.js-pinned-repos-reorder-container h2 {
  370. background-position: 150px 5px !important;
  371. }
  372. /* https://github.com/:user?tab=repositories */
  373. .user-profile-nav.js-sticky {
  374. background-position: calc(100% - 5px) 22px !important;
  375. }
  376. ${userSortPosition}
  377. /* https://github.com/:org repos */
  378. .org-profile > div > .TableObject {
  379. width: 100%; /* Fix width of org with no pinned repos */
  380. padding-right: 30px;
  381. background-position: right 10px !important;
  382. }
  383. .org-profile form + .TableObject-item .ml-6,
  384. .org-profile .TableObject-item .mr-6 {
  385. margin-left: 2px !important;
  386. margin-right: 2px !important;
  387. }
  388. .org-profile .TableObject {
  389. background-position: calc(100% - 12px) 10px !important;
  390. }
  391. /* Own org people; collaborators page doesn't need adjusting */
  392. .ghsc-org-people.ghsc-own-org.subnav.org-toolbar,
  393. .ghsc-org-teams.subnav.org-toolbar,
  394. #org_your_repos h3.Box-title {
  395. background-position: calc(100% - 135px) center !important;
  396. }
  397. /* https://github.com/watching */
  398. .subscriptions-content .Box-header .text-right {
  399. background-position: 5px 7px !important;
  400. }
  401. /* Hide "Sorted by most recently watched" text when sorted */
  402. .subscriptions-content .Box-header.asc .text-right > .text-small,
  403. .subscriptions-content .Box-header.desc .text-right > .text-small {
  404. display: none;
  405. }
  406. /* https://github.com/watching */
  407. .subscriptions-content .Box-header {
  408. background-position: 160px 15px !important;
  409. }
  410. /* asc/dec icons */
  411. table thead th.asc,
  412. .markdown-body table.csv-data thead th.asc,
  413. .js-repos-container.asc .Box-title,
  414. #org_your_repos.asc .Box-title,
  415. .org-profile .TableObject.asc,
  416. .js-bulk-actions-container .subnav.org-toolbar.asc,
  417. .user-profile-nav.asc,
  418. .user-profile-nav.is-stuck.asc,
  419. .org-profile.js-pinned-repos-reorder-container h2.asc,
  420. .subscriptions-content .Box-header.asc .text-right,
  421. #repos > h2.asc,
  422. h2.ghsc-header.asc {
  423. background-image: url(${getIcon("asc", color)}) !important;
  424. background-repeat: no-repeat !important;
  425. }
  426. table thead th.desc,
  427. .markdown-body table.csv-data thead th.desc,
  428. .js-repos-container.desc .Box-title,
  429. #org_your_repos.desc .Box-title,
  430. .org-profile .TableObject.desc,
  431. .js-bulk-actions-container .subnav.org-toolbar.desc,
  432. .user-profile-nav.desc,
  433. .user-profile-nav.is-stuck.desc,
  434. .org-profile.js-pinned-repos-reorder-container h2.desc,
  435. .subscriptions-content .Box-header.desc .text-right,
  436. #repos > h2.desc,
  437. h2.ghsc-header.desc {
  438. background-image: url(${getIcon("desc", color)}) !important;
  439. background-repeat: no-repeat !important;
  440. }
  441. `);
  442.  
  443. document.body.addEventListener("click", event => {
  444. const target = event.target;
  445. const loc = window.location;
  446. if (target && target.nodeType === 1) {
  447. Object.keys(sortables).some(item => {
  448. const el = sortables[item].check(target, loc);
  449. if (el) {
  450. sortables[item].sort(el instanceof HTMLElement ? el : target);
  451. event.preventDefault();
  452. return true;
  453. }
  454. return false;
  455. });
  456. }
  457. });
  458. update();
  459. }
  460.  
  461. document.addEventListener("ghmo:container", () => update());
  462. init();
  463. })();