GitHub Sort Content

A userscript that makes some lists & markdown tables sortable

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

  1. // ==UserScript==
  2. // @name GitHub Sort Content
  3. // @version 1.1.3
  4. // @description A userscript that makes some lists & markdown tables sortable
  5. // @license https://creativecommons.org/licenses/by-sa/4.0/
  6. // @namespace https://github.com/Mottie
  7. // @include https://github.com/*
  8. // @grant GM_addStyle
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/tinysort/2.3.6/tinysort.min.js
  10. // @run-at document-idle
  11. // @author Rob Garrison
  12. // ==/UserScript==
  13. /* global GM_addStyle, tinysort */
  14. /* jshint unused:true, esnext:true */
  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 initSortTable(el) {
  55. removeSelection();
  56. const dir = el.classList.contains(sorts[0]) ? sorts[1] : sorts[0],
  57. table = closest("table", el);
  58. tinysort($$("tbody tr", table), {
  59. order: dir,
  60. natural: true,
  61. selector: `td:nth-child(${el.cellIndex + 1})`
  62. });
  63. $$("th", table).forEach(elm => {
  64. elm.classList.remove(...sorts);
  65. });
  66. el.classList.add(dir);
  67. }
  68.  
  69. function initSortUl(arrows, list, selector) {
  70. if (list && list.children) {
  71. removeSelection();
  72. const dir = arrows.classList.contains(sorts[0]) ? sorts[1] : sorts[0],
  73. options = {
  74. order: dir,
  75. natural: true
  76. };
  77. if (selector) {
  78. options.selector = selector;
  79. }
  80. // using children because the big repo contains UL > DIV
  81. tinysort(list.children, options);
  82. arrows.classList.remove(...sorts);
  83. arrows.classList.add(dir);
  84. }
  85. }
  86.  
  87. function needDarkTheme() {
  88. let brightest = 0,
  89. // color will be "rgb(#, #, #)" or "rgba(#, #, #, #)"
  90. color = window.getComputedStyle(document.body).backgroundColor;
  91. const rgb = (color || "")
  92. .replace(/\s/g, "")
  93. .match(/^rgba?\((\d+),(\d+),(\d+)/i);
  94. if (rgb) {
  95. color = rgb.slice(1); // remove "rgb.." part from match
  96. color.forEach(c => {
  97. // http://stackoverflow.com/a/15794784/145346
  98. brightest = Math.max(brightest, parseInt(c, 10));
  99. });
  100. // return true if we have a dark background
  101. return brightest < 128;
  102. }
  103. // fallback to bright background
  104. return false;
  105. }
  106.  
  107. function $(str, el) {
  108. return (el || document).querySelector(str);
  109. }
  110.  
  111. function $$(str, el) {
  112. return Array.from((el || document).querySelectorAll(str));
  113. }
  114.  
  115. function closest(selector, el) {
  116. while (el && el.nodeType === 1) {
  117. if (el.matches(selector)) {
  118. return el;
  119. }
  120. el = el.parentNode;
  121. }
  122. return null;
  123. }
  124.  
  125. function removeSelection() {
  126. // remove text selection - http://stackoverflow.com/a/3171348/145346
  127. const sel = window.getSelection ?
  128. window.getSelection() :
  129. document.selection;
  130. if (sel) {
  131. if (sel.removeAllRanges) {
  132. sel.removeAllRanges();
  133. } else if (sel.empty) {
  134. sel.empty();
  135. }
  136. }
  137. }
  138.  
  139. function init() {
  140. const styles = needDarkTheme() ? icons.white : icons.black;
  141.  
  142. GM_addStyle(`
  143. /* unsorted icon */
  144. .markdown-body table thead th {
  145. cursor:pointer;
  146. padding-right:22px !important;
  147. background-image:url(${styles.unsorted}) !important;
  148. background-repeat:no-repeat !important;
  149. background-position:calc(100% - 5px) center !important;
  150. }
  151. div.js-pinned-repos-reorder-container > h3, div.filter-repos,
  152. .dashboard-sidebar .boxed-group > h3,
  153. .sort-bar, h2 + .tabnav > .tabnav-tabs, .org-toolbar,
  154. .org-profile .TableObject-item--primary,
  155. .subscriptions-content .Box-header, div.user-profile-nav.js-sticky {
  156. cursor:pointer;
  157. padding-right:10px;
  158. background-image:url(${styles.unsorted}) !important;
  159. background-repeat:no-repeat !important;
  160. background-position:calc(100% - 5px) center !important;
  161. }
  162. /* https://github.com/ -> your repositories */
  163. .dashboard-sidebar .user-repos h3 {
  164. background-position: 175px 10px !important;
  165. }
  166. /* https://github.com/:user?tab=repositories */
  167. div.user-profile-nav.js-sticky {
  168. background-position:calc(100% - 80px) 22px !important;
  169. }
  170. /* https://github.com/:organization repos & people */
  171. .org-profile .TableObject-item--primary, .org-toolbar {
  172. background-position:calc(100% - 5px) 10px !important;
  173. }
  174. .TableObject-item--primary input {
  175. width: 97.5% !important;
  176. }
  177. /* https://github.com/stars */
  178. .sort-bar {
  179. background-position:525px 10px !important;
  180. }
  181. /* https://github.com/watching */
  182. .subscriptions-content .Box-header {
  183. background-position:160px 15px !important;
  184. }
  185. /* asc/dec icons */
  186. table thead th.asc, div.boxed-group h3.asc, div.user-profile-nav.asc,
  187. div.js-repo-filter.asc, .org-toolbar.asc,
  188. .TableObject-item--primary.asc, .sort-bar.asc,
  189. h2 + .tabnav > .tabnav-tabs.asc,
  190. .subscriptions-content .Box-header.asc {
  191. background-image:url(${styles.asc}) !important;
  192. background-repeat:no-repeat !important;
  193. }
  194. table thead th.desc, div.boxed-group h3.desc, div.user-profile-nav.desc,
  195. div.js-repo-filter.desc, .org-toolbar.desc,
  196. .TableObject-item--primary.desc, .sort-bar.desc,
  197. h2 + .tabnav > .tabnav-tabs.desc,
  198. .subscriptions-content .Box-header.desc {
  199. background-image:url(${styles.desc}) !important;
  200. background-repeat:no-repeat !important;
  201. }
  202. /* remove sort arrows */
  203. .popular-repos + div.boxed-group h3 {
  204. background-image:none !important;
  205. cursor:default;
  206. }
  207. /* move "Customize your pinned..." - https://github.com/:self */
  208. .pinned-repos-setting-link {
  209. margin-right:14px;
  210. }
  211. `);
  212.  
  213. document.body.addEventListener("click", event => {
  214. let el;
  215. const target = event.target,
  216. name = target.nodeName;
  217. if (target && target.nodeType === 1 && (
  218. // nodes th|h3 & form for stars page
  219. name === "H3" || name === "TH" || name === "FORM" ||
  220. // https://github.com/:organization filter bar
  221. // filter: .TableObject-item--primary, repo wrapper: .org-profile
  222. // https://github.com/stars (sort-bar)
  223. // https://github.com/:user/followers (tabnav-tabs)
  224. // https://github.com/:user/following (tabnav-tabs)
  225. // https://github.com/:user?tab=repositories (user-profile-nav)
  226. // https://github.com/:user?tab=stars (user-profile-nav)
  227. // https://github.com/:user?tab=followers (user-profile-nav)
  228. // https://github.com/:user?tab=followering (user-profile-nav)
  229. regexBars.test(target.className)
  230. )) {
  231. // don't sort tables not inside of markdown
  232. if (name === "TH" && closest(".markdown-body", target)) {
  233. return initSortTable(target);
  234. }
  235.  
  236. // following
  237. el = $("ol.follow-list", closest(".container", target));
  238. if (el) {
  239. return initSortUl(target, el, ".follow-list-name a");
  240. }
  241.  
  242. // organization people - https://github.com/orgs/:organization/people
  243. el = $("ul.member-listing-next", target.parentNode);
  244. if (el) {
  245. return initSortUl(target, el, ".member-info a");
  246. }
  247.  
  248. // stars - https://github.com/stars
  249. el = closest(".sort-bar", target);
  250. if (el && $(".repo-list", el.parentNode)) {
  251. return initSortUl(el, $(".repo-list", el.parentNode), "h3 a");
  252. }
  253.  
  254. // org repos - https://github.com/:organization
  255. el = closest(".org-profile", target);
  256. if (el && $(".repo-list", el)) {
  257. return initSortUl(target, $(".repo-list", el), "h3 a");
  258. }
  259.  
  260. // https://github.com/watching
  261. el = closest(".subscriptions-content", target);
  262. if (el && $(".repo-list", el)) {
  263. return initSortUl($(".Box-header", el), $(".repo-list", el), "li a");
  264. }
  265.  
  266. // mini-repo listings with & without filter - https://github.com/
  267. // and pinned repo lists
  268. el = closest(".boxed-group", target);
  269. // prevent clicking on the H3 header of filtered repos
  270. if (el && name === "H3" && (
  271. el.classList.contains("js-repo-filter") ||
  272. el.classList.contains("js-pinned-repos-reorder-container")
  273. )) {
  274. return initSortUl(target, $(".mini-repo-list", el));
  275. }
  276.  
  277. // user sticky navigation
  278. if (target.classList.contains("user-profile-nav")) {
  279. el = $(".underline-nav-item.selected", target);
  280. if (el) {
  281. if (el.textContent.indexOf("Overview") > -1) {
  282. return initSortUl(target, $(".pinned-repos-list"), ".repo");
  283. } else if (el.href.indexOf("tab=repo") > -1) {
  284. return initSortUl(target, $(".js-repo-list"), "h3 a");
  285. } else if (el.href.indexOf("tab=stars") > -1) {
  286. return initSortUl(target, $(".js-repo-filter"), "h3 a");
  287. } else if (el.href.indexOf("tab=follow") > -1) {
  288. return initSortUl(target, $(".js-repo-filter"), "a .f4");
  289. }
  290. }
  291. }
  292. }
  293. });
  294. }
  295.  
  296. init();
  297. })();