GitHub Sort Content

A userscript that makes some lists & markdown tables sortable

目前为 2016-09-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Sort Content
  3. // @version 1.1.0
  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 esnext:true, unused: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+PHBhdGggZD0iTTE1IDggMSA4IDggMHpNMTUgOSAxIDkgOCAxNnoiIHN0eWxlPSJmaWxsOiNkZGQ7b3BhY2l0eTowLjIiLz48L3N2Zz4=",
  31. asc: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTE1IDggMSA4IDggMHoiIHN0eWxlPSJmaWxsOiNkZGQiLz48cGF0aCBkPSJNMTUgOSAxIDkgOCAxNnoiIHN0eWxlPSJmaWxsOiNkZGQ7b3BhY2l0eTowLjIiLz48L3N2Zz4=",
  32. desc: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTE1IDggMSA4IDggMHoiIHN0eWxlPSJmaWxsOiNkZGQ7b3BhY2l0eTowLjIiLz48cGF0aCBkPSJNMTUgOSAxIDkgOCAxNnoiIHN0eWxlPSJmaWxsOiNkZGQiLz48L3N2Zz4="
  33. },
  34. black: {
  35. unsorted: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTE1IDggMSA4IDggMHpNMTUgOSAxIDkgOCAxNnoiIHN0eWxlPSJmaWxsOiMyMjI7b3BhY2l0eTowLjIiLz48L3N2Zz4=",
  36. asc: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTE1IDggMSA4IDggMHoiIHN0eWxlPSJmaWxsOiMyMjIiLz48cGF0aCBkPSJNMTUgOSAxIDkgOCAxNnoiIHN0eWxlPSJmaWxsOiMyMjI7b3BhY2l0eTowLjIiLz48L3N2Zz4=",
  37. desc: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTE1IDggMSA4IDggMHoiIHN0eWxlPSJmaWxsOiMyMjI7b3BhY2l0eTowLjIiLz48cGF0aCBkPSJNMTUgOSAxIDkgOCAxNnoiIHN0eWxlPSJmaWxsOiMyMjIiLz48L3N2Zz4="
  38. }
  39. },
  40. // toolbars - target for sort arrows
  41. regexBars = /\b(filter-bar|org-toolbar|sort-bar|tabnav-tabs|user-profile-nav)\b/;
  42.  
  43. function initSortTable(el) {
  44. removeSelection();
  45. const dir = el.classList.contains(sorts[0]) ? sorts[1] : sorts[0],
  46. table = closest(el, "table");
  47. tinysort($$("tbody tr", table), {
  48. order: dir,
  49. natural: true,
  50. selector: `td:nth-child(${el.cellIndex + 1})`
  51. });
  52. $$("th", table).forEach(elm => {
  53. elm.classList.remove(...sorts);
  54. });
  55. el.classList.add(dir);
  56. }
  57.  
  58. function initSortUl(arrows, list, selector) {
  59. if (list && list.children) {
  60. removeSelection();
  61. const dir = arrows.classList.contains(sorts[0]) ? sorts[1] : sorts[0],
  62. options = {
  63. order: dir,
  64. natural: true
  65. };
  66. if (selector) {
  67. options.selector = selector;
  68. }
  69. // using children because the big repo contains UL > DIV
  70. tinysort(list.children, options);
  71. arrows.classList.remove(...sorts);
  72. arrows.classList.add(dir);
  73. }
  74. }
  75.  
  76. function needDarkTheme() {
  77. let brightest = 0,
  78. // color will be "rgb(#, #, #)" or "rgba(#, #, #, #)"
  79. color = window.getComputedStyle(document.body).backgroundColor;
  80. const rgb = (color || "").replace(/\s/g, "").match(/^rgba?\((\d+),(\d+),(\d+)/i);
  81. if (rgb) {
  82. color = rgb.slice(1); // remove "rgb.." part from match
  83. color.forEach(c => {
  84. // http://stackoverflow.com/a/15794784/145346
  85. brightest = Math.max(brightest, parseInt(c, 10));
  86. });
  87. // return true if we have a dark background
  88. return brightest < 128;
  89. }
  90. // fallback to bright background
  91. return false;
  92. }
  93.  
  94. function $(str, el) {
  95. return (el || document).querySelector(str);
  96. }
  97.  
  98. function $$(str, el) {
  99. return Array.from((el || document).querySelectorAll(str));
  100. }
  101.  
  102. function closest(el, selector) {
  103. while (el && el.nodeName !== "BODY" && !el.matches(selector)) {
  104. el = el.parentNode;
  105. }
  106. return el && el.matches(selector) ? el : null;
  107. }
  108.  
  109. function removeSelection() {
  110. // remove text selection - http://stackoverflow.com/a/3171348/145346
  111. const sel = window.getSelection ? window.getSelection() : document.selection;
  112. if (sel) {
  113. if (sel.removeAllRanges) {
  114. sel.removeAllRanges();
  115. } else if (sel.empty) {
  116. sel.empty();
  117. }
  118. }
  119. }
  120.  
  121. function init() {
  122. const styles = needDarkTheme() ? icons.white : icons.black;
  123.  
  124. GM_addStyle(`
  125. /* unsorted icon */
  126. .markdown-body table thead th {
  127. cursor:pointer;
  128. padding-right:22px !important;
  129. background:url(${styles.unsorted}) no-repeat calc(100% - 5px) center !important;
  130. }
  131. div.js-pinned-repos-reorder-container > h3, .dashboard-sidebar .boxed-group > h3,
  132. div.filter-repos, div.js-repo-filter .filter-bar, .org-toolbar, .sort-bar,
  133. h2 + .tabnav > .tabnav-tabs, .subscriptions-content .boxed-group > h3,
  134. div.user-profile-nav {
  135. cursor:pointer;
  136. padding-right:10px;
  137. background-image:url(${styles.unsorted}) !important;
  138. background-repeat:no-repeat !important;
  139. background-position:calc(100% - 5px) center !important;
  140. }
  141. /* https://github.com/ -> your repositories */
  142. .dashboard-sidebar .user-repos h3 { background-position: 175px 10px !important; }
  143. /* https://github.com/:user?tab=repositories */
  144. div.user-profile-nav { background-position:calc(100% - 80px) 22px !important; }
  145. /* https://github.com/:organization */
  146. .org-toolbar { background-position:calc(100% - 5px) 10px !important; }
  147. /* https://github.com/stars */
  148. .sort-bar { background-position:525px 10px !important; }
  149. /* https://github.com/watching */
  150. .subscriptions-content .boxed-group > h3 {
  151. background-position:150px 10px !important;
  152. }
  153. /* asc/dec icons */
  154. table thead th.asc, div.boxed-group h3.asc, div.user-profile-nav.asc,
  155. div.js-repo-filter.asc, div.filter-bar.asc, .org-toolbar.asc,
  156. .sort-bar.asc, h2 + .tabnav > .tabnav-tabs.asc,
  157. .subscriptions-content .boxed-group > h3.asc {
  158. background-image:url(${styles.asc}) !important;
  159. background-repeat:no-repeat !important;
  160. }
  161. table thead th.desc, div.boxed-group h3.desc, div.user-profile-nav.desc,
  162. div.js-repo-filter.desc, div.filter-bar.desc, .org-toolbar.desc,
  163. .sort-bar.desc, h2 + .tabnav > .tabnav-tabs.desc,
  164. .subscriptions-content .boxed-group > h3.desc {
  165. background-image:url(${styles.desc}) !important;
  166. background-repeat:no-repeat !important;
  167. }
  168. /* remove sort arrows */
  169. .popular-repos + div.boxed-group h3, .dashboard-sidebar div.filter-bar {
  170. background-image:none !important;
  171. cursor:default;
  172. }
  173. /* move "Customize your pinned..." - https://github.com/:self */
  174. .pinned-repos-setting-link { margin-right:14px; }
  175. `);
  176.  
  177. document.body.addEventListener("click", event => {
  178. let el;
  179. const target = event.target,
  180. name = target.nodeName;
  181. if (target && target.nodeType === 1 && (
  182. // nodes th|h3 - form for stars page
  183. name === "H3" || name === "TH" || name === "FORM" ||
  184. // mini-repo & https://github.com/:user?tab=repositories (filter-bar)
  185. // https://github.com/:organization filter bar (org-toolbar)
  186. // https://github.com/stars (sort-bar)
  187. // https://github.com/:user/followers (tabnav-tabs)
  188. // https://github.com/:user/following (tabnav-tabs)
  189. // https://github.com/:user?tab=repositories (user-profile-nav)
  190. // https://github.com/:user?tab=stars (user-profile-nav)
  191. // https://github.com/:user?tab=followers (user-profile-nav)
  192. // https://github.com/:user?tab=followering (user-profile-nav)
  193. regexBars.test(target.className)
  194. )) {
  195. // don't sort tables not inside of markdown
  196. if (name === "TH" && closest(target, ".markdown-body")) {
  197. return initSortTable(target);
  198. }
  199.  
  200. // following
  201. el = $("ol.follow-list", closest(target, ".container"));
  202. if (el) {
  203. return initSortUl(target, el, ".follow-list-name a");
  204. }
  205.  
  206. // organization people - https://github.com/orgs/:organization/people
  207. el = $("ul.member-listing", target.parentNode);
  208. if (el) {
  209. return initSortUl(target, el, ".member-link");
  210. }
  211.  
  212. // big repo list - https://github.com/:user?tab=repositories
  213. // stars - https://github.com/stars
  214. el = closest(target, ".sort-bar, .filter-bar, .org-toolbar");
  215. if (el && $(".repo-list", el.parentNode)) {
  216. return initSortUl(el, $(".repo-list", el.parentNode), ".repo-list-name a");
  217. }
  218.  
  219. // https://github.com/watching
  220. el = closest(target, ".subscriptions-content");
  221. if (el && $(".repo-list", el)) {
  222. return initSortUl(target, $(".repo-list", el), "li a");
  223. }
  224.  
  225. // mini-repo listings with & without filter - https://github.com/
  226. // and pinned repo lists
  227. el = closest(target, ".boxed-group");
  228. // prevent clicking on the H3 header of filtered repos
  229. if (el && name === "H3" && (
  230. el.classList.contains("js-repo-filter") ||
  231. el.classList.contains("js-pinned-repos-reorder-container")
  232. )) {
  233. return initSortUl(target, $(".mini-repo-list", el));
  234. }
  235.  
  236. // user sticky navigation
  237. if (target.classList.contains("user-profile-nav")) {
  238. el = $(".underline-nav-item.selected", target);
  239. if (el) {
  240. console.log(el.textContent.trim(), el.href);
  241. if (el.textContent.indexOf('Overview') > -1) {
  242. return initSortUl(target, $(".pinned-repos-list"), ".repo");
  243. } else if (el.href.indexOf("tab=repo") > -1) {
  244. return initSortUl(target, $(".js-repo-list"), "h3 a");
  245. } else if (el.href.indexOf("tab=stars") > -1) {
  246. return initSortUl(target, $(".js-repo-filter"), "h3 a");
  247. } else if (el.href.indexOf("tab=follow") > -1) {
  248. return initSortUl(target, $(".js-repo-filter"), "a .f4");
  249. }
  250. }
  251. }
  252. }
  253. });
  254. }
  255.  
  256. init();
  257. })();