GitHub Sort Content

A userscript that makes some lists & markdown tables sortable

当前为 2017-12-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Sort Content
  3. // @version 1.2.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. // @include https://github.com/*
  9. // @run-at document-idle
  10. // @grant GM.addStyle
  11. // @grant GM_addStyle
  12. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/tinysort/2.3.6/tinysort.min.js
  14. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=234970
  15. // @icon https://assets-cdn.github.com/pinned-octocat.svg
  16. // ==/UserScript==
  17. (() => {
  18. "use strict";
  19. /* example pages:
  20. tables - https://github.com/Mottie/GitHub-userscripts
  21. Contribute repos, Your Repos & Your Teams - https://github.com/
  22. organization repos - https://github.com/jquery
  23. organization members - https://github.com/orgs/jquery/people
  24. pinned & no pinned repos - https://github.com/addyosmani
  25. repos - https://github.com/addyosmani?tab=repositories
  26. stars - https://github.com/stars
  27. watching - https://github.com/watching
  28. */
  29. const sorts = ["asc", "desc"],
  30. icons = {
  31. white: {
  32. unsorted: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTE1IDhIMWw3LTh6bTAgMUgxbDcgN3oiIGZpbGw9IiNkZGQiIG9wYWNpdHk9Ii4yIi8+PC9zdmc+",
  33. asc: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTE1IDhIMWw3LTh6IiBmaWxsPSIjZGRkIi8+PHBhdGggZD0iTTE1IDlIMWw3IDd6IiBmaWxsPSIjZGRkIiBvcGFjaXR5PSIuMiIvPjwvc3ZnPg==",
  34. desc: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTE1IDhIMWw3LTh6IiBmaWxsPSIjZGRkIiBvcGFjaXR5PSIuMiIvPjxwYXRoIGQ9Ik0xNSA5SDFsNyA3eiIgZmlsbD0iI2RkZCIvPjwvc3ZnPg=="
  35. },
  36. black: {
  37. unsorted: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTE1IDhIMWw3LTh6bTAgMUgxbDcgN3oiIGZpbGw9IiMyMjIiIG9wYWNpdHk9Ii4yIi8+PC9zdmc+",
  38. asc: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTE1IDhIMWw3LTh6IiBmaWxsPSIjMjIyIi8+PHBhdGggZD0iTTE1IDlIMWw3IDd6IiBmaWxsPSIjMjIyIiBvcGFjaXR5PSIuMiIvPjwvc3ZnPg==",
  39. desc: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTE1IDhIMWw3LTh6IiBmaWxsPSIjMjIyIiBvcGFjaXR5PSIuMiIvPjxwYXRoIGQ9Ik0xNSA5SDFsNyA3eiIgZmlsbD0iIzIyMiIvPjwvc3ZnPg=="
  40. }
  41. },
  42. // toolbars - target for sort arrows
  43. regexBars = new RegExp(
  44. "\\b(" +
  45. [
  46. "TableObject", // org repos
  47. "org-toolbar", // org people
  48. "sort-bar", // https://github.com/stars
  49. "tabnav-tabs", // https://github.com/:user/follower(s|ing)
  50. "Box-header|flex-auto", // watching
  51. "user-profile-nav" // user repos
  52. ].join("|") +
  53. ")\\b"
  54. );
  55.  
  56. function addRepoFileThead() {
  57. const $table = $("table.files");
  58. if ($table && !$(".ghsc-header", $table)) {
  59. const thead = document.createElement("thead");
  60. thead.innerHTML = `<tr class="ghsc-header">
  61. <td></td>
  62. <th>Content</th>
  63. <th>Message</th>
  64. <th class="ghsc-age">Age</th>
  65. </tr>`;
  66. $table.insertBefore(thead, $table.childNodes[0]);
  67. }
  68. }
  69.  
  70. function initSortTable(el) {
  71. removeSelection();
  72. const dir = el.classList.contains(sorts[0]) ? sorts[1] : sorts[0],
  73. table = closest("table", el),
  74. options = {
  75. order: dir,
  76. natural: true,
  77. selector: `td:nth-child(${el.cellIndex + 1})`
  78. };
  79. if (el.classList.contains("ghsc-age")) {
  80. // sort repo age column using ISO 8601 datetime format
  81. options.selector += " [datetime]";
  82. options.attr = "datetime";
  83. }
  84. tinysort($$("tbody tr:not(.up-tree)", table), options);
  85. $$("th", table).forEach(elm => {
  86. elm.classList.remove(...sorts);
  87. });
  88. el.classList.add(dir);
  89. }
  90.  
  91. function initSortUl(arrows, list, selector) {
  92. if (list && list.children) {
  93. removeSelection();
  94. const dir = arrows.classList.contains(sorts[0]) ? sorts[1] : sorts[0],
  95. options = {
  96. order: dir,
  97. natural: true
  98. };
  99. if (selector) {
  100. options.selector = selector;
  101. }
  102. // using children because the big repo contains UL > DIV
  103. tinysort(list.children, options);
  104. arrows.classList.remove(...sorts);
  105. arrows.classList.add(dir);
  106. }
  107. }
  108.  
  109. function needDarkTheme() {
  110. let brightest = 0,
  111. // color will be "rgb(#, #, #)" or "rgba(#, #, #, #)"
  112. color = window.getComputedStyle(document.body).backgroundColor;
  113. const rgb = (color || "")
  114. .replace(/\s/g, "")
  115. .match(/^rgba?\((\d+),(\d+),(\d+)/i);
  116. if (rgb) {
  117. color = rgb.slice(1); // remove "rgb.." part from match
  118. color.forEach(c => {
  119. // http://stackoverflow.com/a/15794784/145346
  120. brightest = Math.max(brightest, parseInt(c, 10));
  121. });
  122. // return true if we have a dark background
  123. return brightest < 128;
  124. }
  125. // fallback to bright background
  126. return false;
  127. }
  128.  
  129. function $(str, el) {
  130. return (el || document).querySelector(str);
  131. }
  132.  
  133. function $$(str, el) {
  134. return Array.from((el || document).querySelectorAll(str));
  135. }
  136.  
  137. function closest(selector, el) {
  138. while (el && el.nodeType === 1) {
  139. if (el.matches(selector)) {
  140. return el;
  141. }
  142. el = el.parentNode;
  143. }
  144. return null;
  145. }
  146.  
  147. function removeSelection() {
  148. // remove text selection - http://stackoverflow.com/a/3171348/145346
  149. const sel = window.getSelection ?
  150. window.getSelection() :
  151. document.selection;
  152. if (sel) {
  153. if (sel.removeAllRanges) {
  154. sel.removeAllRanges();
  155. } else if (sel.empty) {
  156. sel.empty();
  157. }
  158. }
  159. }
  160.  
  161. function init() {
  162. const styles = needDarkTheme() ? icons.white : icons.black;
  163.  
  164. GM.addStyle(`
  165. /* unsorted icon */
  166. .markdown-body table thead th, table.files thead th {
  167. cursor:pointer;
  168. padding-right:22px !important;
  169. background-image:url(${styles.unsorted}) !important;
  170. background-repeat:no-repeat !important;
  171. background-position:calc(100% - 5px) center !important;
  172. text-align:left;
  173. }
  174. tr.ghsc-header th, tr.ghsc-header td {
  175. border-bottom:#eee 1px solid;
  176. padding:2px 2px 2px 10px;
  177. }
  178. div.js-pinned-repos-reorder-container > h3,
  179. .dashboard-sidebar .boxed-group > h3,
  180. .sort-bar, h2 + .tabnav > .tabnav-tabs, .org-toolbar,
  181. .org-profile .TableObject-item--primary,
  182. .subscriptions-content .Box-header, div.user-profile-nav.js-sticky {
  183. cursor:pointer;
  184. padding-right:10px;
  185. background-image:url(${styles.unsorted}) !important;
  186. background-repeat:no-repeat !important;
  187. background-position:calc(100% - 5px) center !important;
  188. }
  189. /* https://github.com/ -> your repositories */
  190. #your_repos h3 {
  191. background-position: 175px 10px !important;
  192. }
  193. /* https://github.com/:user?tab=repositories */
  194. div.user-profile-nav.js-sticky {
  195. background-position:calc(100% - 80px) 22px !important;
  196. }
  197. /* https://github.com/:organization repos & people */
  198. .org-profile .TableObject-item--primary, .org-toolbar {
  199. background-position:calc(100% - 5px) 10px !important;
  200. }
  201. .TableObject-item--primary input {
  202. width: 97.5% !important;
  203. }
  204. /* https://github.com/stars */
  205. .sort-bar {
  206. background-position:525px 10px !important;
  207. }
  208. /* https://github.com/watching */
  209. .subscriptions-content .Box-header {
  210. background-position:160px 15px !important;
  211. }
  212. /* asc/dec icons */
  213. table thead th.asc, div.boxed-group h3.asc, div.user-profile-nav.asc,
  214. div.js-repo-filter.asc, .org-toolbar.asc,
  215. .TableObject-item--primary.asc, .sort-bar.asc,
  216. h2 + .tabnav > .tabnav-tabs.asc,
  217. .subscriptions-content .Box-header.asc {
  218. background-image:url(${styles.asc}) !important;
  219. background-repeat:no-repeat !important;
  220. }
  221. table thead th.desc, div.boxed-group h3.desc, div.user-profile-nav.desc,
  222. div.js-repo-filter.desc, .org-toolbar.desc,
  223. .TableObject-item--primary.desc, .sort-bar.desc,
  224. h2 + .tabnav > .tabnav-tabs.desc,
  225. .subscriptions-content .Box-header.desc {
  226. background-image:url(${styles.desc}) !important;
  227. background-repeat:no-repeat !important;
  228. }
  229. /* remove sort arrows */
  230. .popular-repos + div.boxed-group h3 {
  231. background-image:none !important;
  232. cursor:default;
  233. }
  234. /* move "Customize your pinned..." - https://github.com/:self */
  235. .pinned-repos-setting-link {
  236. margin-right:14px;
  237. }
  238. `);
  239.  
  240. document.body.addEventListener("click", event => {
  241. let el;
  242. const target = event.target,
  243. name = target.nodeName;
  244. if (target && target.nodeType === 1 && (
  245. // nodes th|h3 & form for stars page
  246. name === "H3" || name === "TH" || name === "FORM" ||
  247. // https://github.com/:organization filter bar
  248. // filter: .TableObject-item--primary, repo wrapper: .org-profile
  249. // https://github.com/stars (sort-bar)
  250. // https://github.com/:user/followers (tabnav-tabs)
  251. // https://github.com/:user/following (tabnav-tabs)
  252. // https://github.com/:user?tab=repositories (user-profile-nav)
  253. // https://github.com/:user?tab=stars (user-profile-nav)
  254. // https://github.com/:user?tab=followers (user-profile-nav)
  255. // https://github.com/:user?tab=followering (user-profile-nav)
  256. regexBars.test(target.className)
  257. )) {
  258. // don't sort tables not inside of markdown,
  259. // except for the repo "code" tab file list
  260. if (
  261. name === "TH" && (
  262. closest(".markdown-body", target) ||
  263. closest("table.files", target)
  264. )
  265. ) {
  266. return initSortTable(target);
  267. }
  268.  
  269. // following
  270. el = $("ol.follow-list", closest(".container", target));
  271. if (el) {
  272. return initSortUl(target, el, ".follow-list-name a");
  273. }
  274.  
  275. // organization people - https://github.com/orgs/:organization/people
  276. el = $("ul.member-listing-next", target.parentNode);
  277. if (el) {
  278. return initSortUl(target, el, ".member-info a");
  279. }
  280.  
  281. // stars - https://github.com/stars
  282. el = closest(".sort-bar", target);
  283. if (el && $(".repo-list", el.parentNode)) {
  284. return initSortUl(el, $(".repo-list", el.parentNode), "h3 a");
  285. }
  286.  
  287. // org repos - https://github.com/:organization
  288. el = closest(".org-profile", target);
  289. if (el && $(".repo-list", el)) {
  290. return initSortUl(target, $(".repo-list", el), "h3 a");
  291. }
  292.  
  293. // https://github.com/watching
  294. el = closest(".subscriptions-content", target);
  295. if (el && $(".repo-list", el)) {
  296. return initSortUl($(".Box-header", el), $(".repo-list", el), "li a");
  297. }
  298.  
  299. // mini-repo listings with & without filter - https://github.com/
  300. // and pinned repo lists
  301. el = closest(".boxed-group", target);
  302. // prevent clicking on the H3 header of filtered repos
  303. if (el && name === "H3" && (
  304. el.parentNode.id === "your_teams" ||
  305. el.parentNode.id === "your_repos" ||
  306. el.classList.contains("js-repos-container") ||
  307. el.classList.contains("js-repo-filter") || // still valid?
  308. el.classList.contains("js-pinned-repos-reorder-container")
  309. )) {
  310. return initSortUl(target, $(".mini-repo-list", el));
  311. }
  312.  
  313. // user sticky navigation
  314. if (target.classList.contains("user-profile-nav")) {
  315. el = $(".underline-nav-item.selected", target);
  316. if (el) {
  317. if (el.textContent.indexOf("Overview") > -1) {
  318. return initSortUl(target, $(".pinned-repos-list"), ".repo");
  319. } else if (el.href.indexOf("tab=repo") > -1) {
  320. return initSortUl(target, $("#user-repositories-list ul"), "h3 a");
  321. } else if (el.href.indexOf("tab=stars") > -1) {
  322. return initSortUl(target, target.nextElementSibling, "h3 a");
  323. } else if (el.href.indexOf("tab=follow") > -1) {
  324. return initSortUl(target, target.nextElementSibling, "a .f4");
  325. }
  326. }
  327. }
  328. }
  329. });
  330. addRepoFileThead();
  331. }
  332.  
  333. document.addEventListener("ghmo:container", () => {
  334. // init after a short delay to allow rendering of file list
  335. setTimeout(() => {
  336. addRepoFileThead();
  337. }, 200);
  338. });
  339. init();
  340. })();