GitHub Sort Content

A userscript that makes some lists & markdown tables sortable

目前为 2017-07-14 提交的版本。查看 最新版本

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