GitHub Sort Content

A userscript that makes some lists & markdown tables sortable

  1. // ==UserScript==
  2. // @name GitHub Sort Content
  3. // @version 3.1.4
  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. // @match https://github.com/*
  9. // @match 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/3.2.5/tinysort.min.js
  15. // @require https://gf.qytechs.cn/scripts/28721-mutations/code/mutations.js?version=1108163
  16. // @icon https://github.githubassets.com/pinned-octocat.svg
  17. // @supportURL https://github.com/Mottie/GitHub-userscripts/issues
  18. // ==/UserScript==
  19.  
  20. /* global GM tinysort */
  21. (() => {
  22. "use strict";
  23. /** Example pages:
  24. * Tables (Readme & wikis) - https://github.com/Mottie/GitHub-userscripts
  25. * Repo files table - https://github.com/Mottie/GitHub-userscripts (sort content, message or age)
  26. * Activity - https://github.com (recent & all)
  27. * Sidebar - https://github.com/ (Repositories & Your teams)
  28. * Pinned repos (user & org)- https://github.com/(:user|:org)
  29. * Org Repos - https://github.com/:org
  30. * Org people - https://github.com/orgs/:org/people
  31. * Org outside collaborators (own orgs) - https://github.com/orgs/:org/outside-collaborators
  32. * Org teams - https://github.com/orgs/:org/teams & https://github.com/orgs/:org/teams/:team/teams
  33. * Org team repos - https://github.com/orgs/:org/teams/:team/repositories
  34. * Org team members - https://github.com/orgs/:org/teams/:team/members
  35. * Org projects - https://github.com/:org/projects
  36. * User repos - https://github.com/:user?tab=repositories
  37. * User stars - https://github.com/:user?tab=stars
  38. * watching - https://github.com/watching
  39. * User subscriptions - https://github.com/notifications/subscriptions
  40. * Repo stargazers - https://github.com/:user/:repo/stargazers
  41. * Repo watchers - https://github.com/:user/:repo/watchers
  42. */
  43. /**
  44. * sortables[entry].setup - exec on userscript init (optional);
  45. * param = window.location
  46. * sortables[entry].check - exec on doc.body click; return truthy/falsy or
  47. * header element (passed to the sort);
  48. * param = (event.target, window.location)
  49. * sortables[entry].sort - exec if check returns true or a header element;
  50. * param = (el) - the element returned by check or original click target
  51. * sortables[entry].css - specific css as an array of selectors, applied to
  52. * the entry elements; "unsorted", "ascending" (optional),
  53. * "descending" (optional), "tweaks" (optional)
  54. */
  55. const sortables = {
  56. // markdown tables
  57. "tables": {
  58. check: el => el.nodeName === "TH" &&
  59. el.matches(".markdown-body table thead th"),
  60. sort: el => initSortTable(el),
  61. css: {
  62. unsorted: [
  63. ".markdown-body table thead th",
  64. ".markdown-body table.csv-data thead th"
  65. ],
  66. tweaks: [
  67. `body .markdown-body table thead th {
  68. text-align: left;
  69. background-position: 3px center !important;
  70. }`
  71. ]
  72. }
  73. },
  74. // repo files
  75. "repo-files": {
  76. check: el => el.classList.contains("ghsc-header-cell"),
  77. // init after a short delay to allow rendering of file list
  78. setup: () => setTimeout(() => addRepoFileHeader(), 1e3),
  79. sort: el => initSortFiles(el),
  80. css: {
  81. unsorted: [
  82. ".ghsc-header-cell"
  83. ],
  84. tweaks: [
  85. `body .ghsc-header-cell {
  86. text-align: left;
  87. background-position: 3px center !important;
  88. }`
  89. ]
  90. }
  91. },
  92. // github.com (all activity list)
  93. "all-activity": {
  94. check: el => $("#dashboard") &&
  95. el.classList.contains("js-all-activity-header"),
  96. sort: el => {
  97. const list = $$("div[data-repository-hovercards-enabled]:not(.js-details-container) > div");
  98. const wrap = list.parentElement;
  99. initSortList(
  100. el,
  101. list,
  102. { selector: "relative-time", attr: "datetime" }
  103. );
  104. // Move "More" button to bottom
  105. setTimeout(() => {
  106. movePaginate(wrap);
  107. });
  108. },
  109. css: {
  110. unsorted: [
  111. ".js-all-activity-header"
  112. ],
  113. extras: [
  114. "div[data-repository-hovercards-enabled] div:empty { display: none; }"
  115. ]
  116. }
  117. },
  118. // github.com (recent activity list)
  119. "recent-activity": {
  120. check: el => $("#dashboard") &&
  121. el.matches(".news > h2:not(.js-all-activity-header)"),
  122. sort: el => {
  123. initSortList(
  124. el,
  125. $$(".js-recent-activity-container ul li"),
  126. { selector: "relative-time", attr: "datetime" }
  127. );
  128. // Not sure why, but sorting shows all recent activity; so, hide the
  129. // "Show more" button
  130. $(".js-show-more-recent-items").classList.add("d-none");
  131. },
  132. css: {
  133. unsorted: [
  134. ".news h2:not(.js-all-activity-header)"
  135. ]
  136. }
  137. },
  138. // github.com (sidebar repos & teams)
  139. "sidebar": {
  140. check: el => $(".dashboard-sidebar") &&
  141. el.matches(".dashboard-sidebar h2"),
  142. sort: el => initSortList(
  143. el,
  144. $$(".list-style-none li", el.closest(".js-repos-container")),
  145. { selector: "a" }
  146. ),
  147. css: {
  148. unsorted: [
  149. ".dashboard-sidebar h2"
  150. ],
  151. tweaks: [
  152. `.dashboard-sidebar h2.pt-3 {
  153. background-position: left bottom !important;
  154. }`
  155. ]
  156. }
  157. },
  158. // github.com/(:user|:org) (pinned repos)
  159. "pinned": {
  160. check: el => el.matches(".js-pinned-items-reorder-container h2"),
  161. sort: el => initSortList(
  162. el,
  163. // org li, own repos li
  164. $$(".js-pinned-items-reorder-list li, #choose-pinned-repositories ~ ol li"),
  165. { selector: "a.text-bold" }
  166. ),
  167. css: {
  168. unsorted: [
  169. ".js-pinned-items-reorder-container h2"
  170. ]
  171. }
  172. },
  173. // github.com/:org
  174. "org-repos": {
  175. setup: () => {
  176. const form = $("form[data-autosearch-results-container='org-repositories']");
  177. if (form) {
  178. form.parentElement.classList.add("ghsc-org-repos-header");
  179. }
  180. },
  181. check: el => el.matches(".ghsc-org-repos-header"),
  182. sort: el => initSortList(
  183. el,
  184. $$(".org-repos li"),
  185. { selector: "a[itemprop*='name']" }
  186. ),
  187. css: {
  188. unsorted: [
  189. ".ghsc-org-repos-header"
  190. ],
  191. tweaks: [
  192. `form[data-autosearch-results-container='org-repositories'] {
  193. cursor: default;
  194. }`
  195. ]
  196. }
  197. },
  198. // github.com/orgs/:org/people
  199. // github.com/orgs/:org/outside-collaborators
  200. // github.com/orgs/:org/teams
  201. // github.com/orgs/:org/teams/:team/teams
  202. // github.com/orgs/:org/teams/:team/repositories
  203. "org-people+teams": {
  204. check: el => el.matches(".org-toolbar"),
  205. sort: el => {
  206. const lists = [
  207. "#org-members-table li",
  208. "#org-outside-collaborators li",
  209. "#org-teams li", // for :org/teams & :org/teams/:team/teams
  210. "#org-team-repositories li"
  211. ].join(",");
  212. // Using a[id] returns a (possibly) truncated full name instead of
  213. // the GitHub handle
  214. initSortList(el, $$(lists), { selector: "a[id], a.f4" });
  215. },
  216. css: {
  217. unsorted: [
  218. ".org-toolbar"
  219. ]
  220. }
  221. },
  222. // github.com/orgs/:org/teams/:team/members
  223. "team-members": {
  224. // no ".org-toolbar" on this page :(
  225. setup: () => {
  226. const form = $("form[data-autosearch-results-container='team-members']");
  227. if (form) {
  228. form.parentElement.classList.add("ghsc-team-members-header");
  229. }
  230. },
  231. check: el => el.matches(".ghsc-team-members-header"),
  232. sort: el => initSortList(el, $$("#team-members li")),
  233. css: {
  234. unsorted: [
  235. ".ghsc-team-members-header"
  236. ]
  237. }
  238. },
  239. // github.com/orgs/:org/projects
  240. "org-projects": {
  241. setup: () => {
  242. const form = $("form[action$='/projects']");
  243. if (form) {
  244. form.parentElement.classList.add("ghsc-project-header");
  245. }
  246. },
  247. check: el => el.matches(".ghsc-project-header"),
  248. sort: el => initSortList(
  249. el,
  250. $$("#projects-results > div"),
  251. { selector: "h4 a" }
  252. ),
  253. css: {
  254. unsorted: [
  255. ".ghsc-project-header"
  256. ]
  257. }
  258. },
  259. // github.com/:user?tab=repositories
  260. "user-repos": {
  261. setup: () => {
  262. const form = $("form[data-autosearch-results-container='user-repositories-list']");
  263. if (form) {
  264. form.parentElement.classList.add("ghsc-repos-header");
  265. }
  266. },
  267. check: el => el.matches(".ghsc-repos-header"),
  268. sort: el => initSortList(
  269. el,
  270. $$("#user-repositories-list li"),
  271. { selector: "a[itemprop*='name']" }
  272. ),
  273. css: {
  274. unsorted: [
  275. ".ghsc-repos-header"
  276. ],
  277. tweaks: [
  278. `form[data-autosearch-results-container='user-repositories-list'] {
  279. cursor: default;
  280. }`
  281. ]
  282. }
  283. },
  284. // github.com/:user?tab=stars
  285. "user-stars": {
  286. setup: () => {
  287. const form = $("form[action$='?tab=stars']");
  288. if (form) {
  289. // filter form is wrapped in a details/summary
  290. const details = form.closest("details");
  291. if (details) {
  292. details.parentElement.classList.add("ghsc-stars-header");
  293. details.parentElement.title = "Sort list by repo name";
  294. }
  295. }
  296. },
  297. check: el => el.matches(".ghsc-stars-header"),
  298. sort: el => {
  299. const wrap = el.parentElement;
  300. const list = $$(".d-block", wrap);
  301. list.forEach(elm => {
  302. const a = $("h3 a", elm);
  303. a.dataset.text = a.textContent.split("/")[1];
  304. });
  305. initSortList(el, list, { selector: "h3 a", attr: "data-text" });
  306. movePaginate(wrap);
  307. },
  308. css: {
  309. unsorted: [
  310. ".ghsc-stars-header"
  311. ],
  312. tweaks: [
  313. `.ghsc-stars-header {
  314. background-position: left top !important;
  315. }`
  316. ]
  317. }
  318. },
  319. // github.com/:user?tab=follow(ers|ing)
  320. "user-tab-follow": {
  321. setup: loc => {
  322. if (loc.search.includes("tab=follow")) {
  323. const tab = $("nav.UnderlineNav-body");
  324. if (tab) {
  325. tab.classList.add("ghsc-follow-nav");
  326. }
  327. }
  328. },
  329. check: (el, loc) => loc.search.indexOf("tab=follow") > -1 &&
  330. el.matches(".ghsc-follow-nav"),
  331. sort: el => {
  332. initSortList(
  333. el,
  334. $$(".position-relative .d-table"),
  335. { selector: ".col-9 .link-gray" } // GitHub user name
  336. );
  337. movePaginate(wrap);
  338. },
  339. css: {
  340. unsorted: [
  341. "nav.ghsc-follow-nav"
  342. ]
  343. }
  344. },
  345. // github.com/watching (watching table only)
  346. "user-watch": {
  347. setup: loc => {
  348. if (loc.href.indexOf("/watching") > -1) {
  349. const header = $(".tabnav");
  350. header.classList.add("ghsc-watching-header");
  351. header.title = "Sort list by repo name";
  352. }
  353. },
  354. check: el => el.matches(".ghsc-watching-header"),
  355. sort: el => {
  356. const list = $$(".standalone.repo-list li");
  357. list.forEach(elm => {
  358. const link = $("a", elm);
  359. link.dataset.sort = link.title.split("/")[1];
  360. });
  361. initSortList(el, list, { selector: "a", attr: "data-sort" });
  362. },
  363. css: {
  364. unsorted: [
  365. ".ghsc-watching-header"
  366. ]
  367. }
  368. },
  369. // github.com/notifications/subscriptions
  370. "user-subscriptions": {
  371. setup: loc => {
  372. if (loc.href.indexOf("/subscriptions") > -1) {
  373. const header = $(".tabnav");
  374. header.classList.add("ghsc-subs-header");
  375. header.title = "Sort list by repo name plus issue title";
  376. }
  377. },
  378. check: el => el.matches(".ghsc-subs-header"),
  379. sort: el => {
  380. const list = $$("li.notification-thread-subscription");
  381. initSortList(el, list, { selector: ".flex-auto" });
  382. },
  383. css: {
  384. unsorted: [
  385. ".ghsc-subs-header"
  386. ]
  387. }
  388. },
  389. // github.com/(:user|:org)/:repo/(stargazers|watchers)
  390. "repo-stars-or-watchers": {
  391. setup: loc => {
  392. if (
  393. loc.href.indexOf("/stargazers") > -1 ||
  394. loc.href.indexOf("/watchers") > -1
  395. ) {
  396. $("#repos > h2").classList.add("ghsc-gazer-header");
  397. }
  398. },
  399. check: el => el.matches(".ghsc-gazer-header"),
  400. sort: el => initSortList(
  401. el,
  402. $$(".follow-list-item"),
  403. { selector: ".follow-list-name" }
  404. ),
  405. css: {
  406. unsorted: [
  407. ".ghsc-gazer-header"
  408. ]
  409. }
  410. }
  411. };
  412.  
  413. const sorts = ["asc", "desc"];
  414.  
  415. const icons = {
  416. unsorted: color => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="${color}">
  417. <path d="M15 8H1l7-8zm0 1H1l7 7z" opacity=".2"/>
  418. </svg>`,
  419. ascending: color => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="${color}">
  420. <path d="M15 8H1l7-8z"/>
  421. <path d="M15 9H1l7 7z" opacity=".2"/>
  422. </svg>`,
  423. descending: color => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="${color}">
  424. <path d="M15 8H1l7-8z" opacity=".2"/>
  425. <path d="M15 9H1l7 7z"/>
  426. </svg>`
  427. };
  428.  
  429. function getIcon(type, color) {
  430. return "data:image/svg+xml;charset=UTF-8," +
  431. encodeURIComponent(icons[type](color));
  432. }
  433.  
  434. function needDarkTheme() {
  435. // color will be "rgb(#, #, #)" or "rgba(#, #, #, #)"
  436. let color = window.getComputedStyle(document.body).backgroundColor;
  437. const rgb = (color || "")
  438. .replace(/\s/g, "")
  439. .match(/^rgba?\((\d+),(\d+),(\d+)/i);
  440. if (rgb) {
  441. // remove "rgb.." part from match & parse
  442. const colors = rgb.slice(1).map(Number);
  443. // http://stackoverflow.com/a/15794784/145346
  444. const brightest = Math.max(...colors);
  445. // return true if we have a dark background
  446. return brightest < 128;
  447. }
  448. // fallback to bright background
  449. return false;
  450. }
  451.  
  452. function getDirection(el) {
  453. return (el.getAttribute("aria-sort") || "").includes(sorts[0])
  454. ? sorts[1]
  455. : sorts[0];
  456. }
  457.  
  458. function setDirection(els, currentElm, dir) {
  459. els.forEach(elm => {
  460. // aria-sort uses "ascending", "descending" or "none"
  461. const cellDir = currentElm === elm ? `${dir}ending` : "none";
  462. elm.setAttribute("aria-sort", cellDir);
  463. });
  464. }
  465.  
  466. function initSortTable(el) {
  467. removeSelection();
  468. const dir = getDirection(el);
  469. const table = el.closest("table");
  470. const options = {
  471. order: dir,
  472. natural: true,
  473. selector: `td:nth-child(${el.cellIndex + 1})`
  474. };
  475. tinysort($$("tbody tr", table), options);
  476. setDirection($$("th", table), el, dir);
  477. }
  478.  
  479. function addRepoFileHeader() {
  480. const $header = $("#files");
  481. // h2#files is a sibling of the grid wrapper
  482. const $target = $header &&
  483. $("div[role='grid'] .sr-only", $header.parentElement);
  484. if ($header && $target) {
  485. $target.className = "Box-row Box-row--focus-gray py-2 d-flex position-relative js-navigation-item ghsc-header";
  486. $target.innerHTML = `
  487. <div role="gridcell" class="mr-3 flex-shrink-0" style="width: 16px;"></div>
  488. <div role="columnheader" aria-sort="none" data-index="2" class="flex-auto min-width-0 col-md-2 mr-3 ghsc-header-cell">
  489. Content
  490. </div>
  491. <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">
  492. Message
  493. </div>
  494. <div role="columnheader" aria-sort="none" data-index="4" class="text-gray-light ghsc-age ghsc-header-cell" style="width:100px;">
  495. Age&nbsp;
  496. </div>
  497. `;
  498. }
  499. }
  500.  
  501. function initSortFiles(el) {
  502. removeSelection();
  503. const dir = getDirection(el);
  504. const grid = el.closest("[role='grid']");
  505. const options = {
  506. order: dir,
  507. natural: true,
  508. selector: `div:nth-child(${el.dataset.index})`
  509. };
  510. if (el.classList.contains("ghsc-age")) {
  511. // sort repo age column using ISO 8601 datetime format
  512. options.selector += " [datetime]";
  513. options.attr = "datetime";
  514. }
  515. // check for parent directory link; don't sort it
  516. const parentDir = $("a[title*='parent dir']", grid);
  517. if (parentDir) {
  518. parentDir.closest("div[role='row']").classList.add("ghsc-header");
  519. }
  520. tinysort($$(".Box-row:not(.ghsc-header)", grid), options);
  521. setDirection($$(".ghsc-header-cell", grid), el, dir);
  522. }
  523.  
  524. function initSortList(header, list, opts = {}) {
  525. if (list) {
  526. removeSelection();
  527. const dir = getDirection(header);
  528. const options = {
  529. order: dir,
  530. natural: true,
  531. place: "first", // Fixes nested ajax of main feed
  532. ...opts
  533. };
  534. tinysort(list, options);
  535. setDirection([header], header, dir);
  536. }
  537. }
  538.  
  539. function getCss(type) {
  540. return Object.keys(sortables).reduce((acc, block) => {
  541. const css = sortables[block].css || {};
  542. const selectors = css[type];
  543. if (selectors) {
  544. acc.push(...selectors);
  545. } else if (type !== "unsorted" && type !== "tweaks") {
  546. const useUnsorted = css.unsorted || [];
  547. if (useUnsorted.length) {
  548. // if "ascending" or "descending" isn't defined, then append
  549. // that class to the unsorted value
  550. acc.push(
  551. `${useUnsorted.join(`[aria-sort='${type}'],`)}[aria-sort='${type}']`
  552. );
  553. }
  554. }
  555. return acc;
  556. }, []).join(type === "tweaks" ? "" : ",");
  557. }
  558.  
  559. // The paginate block is a sibling along with the items in the list...
  560. // it needs to be moved to the end
  561. function movePaginate(wrapper) {
  562. const pager = wrapper &&
  563. $(".paginate-container, .ajax-pagination-form", wrapper);
  564. if (pager) {
  565. wrapper.append(pager);
  566. }
  567. }
  568.  
  569. function $(str, el) {
  570. return (el || document).querySelector(str);
  571. }
  572.  
  573. function $$(str, el) {
  574. return [...(el || document).querySelectorAll(str)];
  575. }
  576.  
  577. function removeSelection() {
  578. // remove text selection - http://stackoverflow.com/a/3171348/145346
  579. const sel = window.getSelection ?
  580. window.getSelection() :
  581. document.selection;
  582. if (sel) {
  583. if (sel.removeAllRanges) {
  584. sel.removeAllRanges();
  585. } else if (sel.empty) {
  586. sel.empty();
  587. }
  588. }
  589. }
  590.  
  591. function update() {
  592. Object.keys(sortables).forEach(item => {
  593. if (sortables[item].setup) {
  594. sortables[item].setup(window.location);
  595. }
  596. });
  597. }
  598.  
  599. function init() {
  600. const color = needDarkTheme() ? "#ddd" : "#222";
  601.  
  602. GM.addStyle(`
  603. /* Added table header */
  604. tr.ghsc-header th, tr.ghsc-header td {
  605. border-bottom: #eee 1px solid;
  606. padding: 2px 2px 2px 10px;
  607. }
  608. /* sort icons */
  609. ${getCss("unsorted")} {
  610. cursor: pointer;
  611. padding-left: 22px !important;
  612. background-image: url(${getIcon("unsorted", color)}) !important;
  613. background-repeat: no-repeat !important;
  614. background-position: left center !important;
  615. }
  616. ${getCss("ascending")} {
  617. background-image: url(${getIcon("ascending", color)}) !important;
  618. background-repeat: no-repeat !important;
  619. }
  620. ${getCss("descending")} {
  621. background-image: url(${getIcon("descending", color)}) !important;
  622. background-repeat: no-repeat !important;
  623. }
  624. /* specific tweaks */
  625. ${getCss("tweaks")}`
  626. );
  627.  
  628. document.body.addEventListener("click", event => {
  629. const target = event.target;
  630. if (target && target.nodeType === 1) {
  631. Object.keys(sortables).some(item => {
  632. const el = sortables[item].check(target, window.location);
  633. if (el) {
  634. sortables[item].sort(el instanceof HTMLElement ? el : target);
  635. event.preventDefault();
  636. return true;
  637. }
  638. return false;
  639. });
  640. }
  641. });
  642. update();
  643. }
  644.  
  645. document.addEventListener("ghmo:container", () => update());
  646. init();
  647. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址