GitHub Custom Navigation

A userscript that allows you to customize GitHub's main navigation bar

目前為 2021-02-21 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name GitHub Custom Navigation
  3. // @version 1.1.9
  4. // @description A userscript that allows you to customize GitHub's main navigation bar
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://github.com/*
  9. // @include https://gist.github.com/*
  10. // @run-at document-end
  11. // @grant GM_addStyle
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @icon https://github.githubassets.com/pinned-octocat.svg
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.js
  16. // @supportURL https://github.com/Mottie/GitHub-userscripts/issues
  17. // ==/UserScript==
  18. /* global dragula */
  19. (() => {
  20. "use strict";
  21.  
  22. // open menu via hash
  23. const panelHash = "#github-custom-nav-settings";
  24. const buildSvg = (w, h, path, vw, vh) =>
  25. `<svg class='octicon' xmlns='http://www.w3.org/2000/svg' aria-hidden='true'
  26. width='${w}' height='${h}' viewBox='0 0 ${vw || w} ${vh || h}'>
  27. <path d='${path}' />
  28. </svg>`;
  29.  
  30. // get user name; or empty string if not logged in
  31. const user = $("meta[name='user-login']") &&
  32. $("meta[name='user-login']").getAttribute("content") || "";
  33.  
  34. const defaults = {
  35. github: [
  36. "pr", "issues", "gist", "separator", "stars", "watching", "separator",
  37. "profile", "blog", "marketplace", "explore", "menu"
  38. ],
  39. gists: [
  40. "gistall", "giststars", "github", "separator", "pr", "issues", "stars",
  41. "watching", "separator", "profile", "blog", "marketplace", "explore", "menu"
  42. ],
  43.  
  44. currentLink: "pr",
  45. // using full length url so the links work from any subdomain (e.g. gist pages)
  46. items: {
  47. "advsearch": {
  48. url: "https://github.com/search/advanced",
  49. tooltip: "Advanced Search",
  50. hotkey: "",
  51. content: buildSvg(16, 16, `M15.7 14.3L11.89 10.47c0.7-0.98 1.11-2.17
  52. 1.11-3.47 0-3.31-2.69-6-6-6S1 3.69 1 7s2.69 6 6 6c1.3 0 2.48-0.41
  53. 3.47-1.11l3.83 3.81c0.19 0.2 0.45 0.3 0.7 0.3s0.52-0.09
  54. 0.7-0.3c0.39-0.39 0.39-1.02 0-1.41zM7 11.7c-2.59
  55. 0-4.7-2.11-4.7-4.7s2.11-4.7 4.7-4.7 4.7 2.11 4.7 4.7-2.11 4.7-4.7 4.7z`
  56. )
  57. },
  58. "blog": {
  59. url: "https://github.blog",
  60. tooltip: "Blog",
  61. hotkey: "",
  62. content: buildSvg(16, 16, `M9 9H8c.55 0 1-.45 1-1V7c0-.55-.45-1-1-1H7c-.55
  63. 0-1 .45-1 1v1c0 .55.45 1 1 1H6c-.55 0-1 .45-1 1v2h1v3c0 .55.45 1 1
  64. 1h1c.55 0 1-.45 1-1v-3h1v-2c0-.55-.45-1-1-1zM7 7h1v1H7V7zm2
  65. 4H8v4H7v-4H6v-1h3v1zm2.09-3.5c0-1.98-1.61-3.59-3.59-3.59A3.593 3.593 0
  66. 0 0 4 8.31v1.98c-.61-.77-1-1.73-1-2.8 0-2.48 2.02-4.5 4.5-4.5S12 5.01
  67. 12 7.49c0 1.06-.39 2.03-1 2.8V8.31c.06-.27.09-.53.09-.81zm3.91 0c0
  68. 2.88-1.63 5.38-4 6.63v-1.05a6.553 6.553 0 0 0 3.09-5.58A6.59 6.59 0 0
  69. 0 7.5.91 6.59 6.59 0 0 0 .91 7.5c0 2.36 1.23 4.42 3.09 5.58v1.05A7.497
  70. 7.497 0 0 1 7.5 0C11.64 0 15 3.36 15 7.5z`
  71. )
  72. },
  73. "explore": {
  74. url: "https://github.com/explore",
  75. tooltip: "Explore",
  76. hotkey: "",
  77. content: buildSvg(16, 16, `M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0zm.6
  78. 14.5v-1H7.2v1a6.5 6.5 0 0 1-5.7-6h1V7.4h-1a6.5 6.5 0 0 1
  79. 5.7-5.8v1h1.4v-1a6.6 6.6 0 0 1 6 5.8h-1v1.3h1a6.5 6.5 0 0 1-6
  80. 6zm2.7-10.8l-4.5 3-2.2 4.8 4.7-3z`
  81. )
  82. },
  83. "gist": {
  84. url: "https://gist.github.com/",
  85. tooltip: "Gist",
  86. hotkey: "",
  87. content: buildSvg(12, 16, `M7.5 5L10 7.5 7.5 10l-.75-.75L8.5 7.5 6.75
  88. 5.75 7.5 5zm-3 0L2 7.5 4.5 10l.75-.75L3.5 7.5l1.75-1.75L4.5 5zM0
  89. 13V2c0-.55.45-1 1-1h10c.55 0 1 .45 1 1v11c0 .55-.45 1-1 1H1c-.55
  90. 0-1-.45-1-1zm1 0h10V2H1v11z`
  91. )
  92. },
  93. "gistall": {
  94. url: "https://gist.github.com/discover",
  95. tooltip: "Discover Gists",
  96. hotkey: "",
  97. content: buildSvg(12, 16, `M7.5 5L10 7.5 7.5 10l-.75-.75L8.5 7.5 6.75
  98. 5.75 7.5 5zm-3 0L2 7.5 4.5 10l.75-.75L3.5 7.5l1.75-1.75L4.5 5zM0
  99. 13V2c0-.55.45-1 1-1h10c.55 0 1 .45 1 1v11c0 .55-.45 1-1 1H1c-.55
  100. 0-1-.45-1-1zm1 0h10V2H1v11z`
  101. )
  102. },
  103. "giststars": {
  104. url: "https://gist.github.com/${me}/starred",
  105. tooltip: "Starred Gists",
  106. hotkey: "",
  107. content: buildSvg(14, 16, `M14 6l-4.9-.64L7 1 4.9 5.36 0 6l3.6 3.26L2.67
  108. 14 7 11.67 11.33 14l-.93-4.74z`
  109. )
  110. },
  111. "github": {
  112. url: "https://github.com",
  113. tooltip: "GitHub",
  114. hotkey: "",
  115. content: buildSvg(16, 16, `M14.7 5.34c.13-.32.55-1.59-.13-3.31 0
  116. 0-1.05-.33-3.44
  117. 1.3-1-.28-2.07-.32-3.13-.32s-2.13.04-3.13.32c-2.39-1.64-3.44-1.3-3.44-1.3-.68
  118. 1.72-.26 2.99-.13 3.31C.49 6.21 0 7.33 0 8.69 0 13.84 3.33 15 7.98
  119. 15S16 13.84 16 8.69c0-1.36-.49-2.48-1.3-3.35zM8 14.02c-3.3
  120. 0-5.98-.15-5.98-3.35 0-.76.38-1.48 1.02-2.07 1.07-.98 2.9-.46 4.96-.46
  121. 2.07 0 3.88-.52 4.96.46.65.59 1.02 1.3 1.02 2.07 0 3.19-2.68 3.35-5.98
  122. 3.35zM5.49 9.01c-.66 0-1.2.8-1.2 1.78s.54 1.79 1.2 1.79c.66 0 1.2-.8
  123. 1.2-1.79s-.54-1.78-1.2-1.78zm5.02 0c-.66 0-1.2.79-1.2 1.78s.54 1.79
  124. 1.2 1.79c.66 0 1.2-.8 1.2-1.79s-.53-1.78-1.2-1.78z`
  125. )
  126. },
  127. "issues": {
  128. url: "https://github.com/issues",
  129. tooltip: "Issues",
  130. hotkey: "g i",
  131. content: buildSvg(14, 16, `M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7
  132. 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0
  133. 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z`
  134. )
  135. },
  136. "marketplace": {
  137. url: "https://github.com/marketplace",
  138. tooltip: "Marketplace",
  139. hotkey: "",
  140. content: buildSvg(14, 16, `M0 0v16h14v-2h-1v1H1V1h12v.5h1V0H0zm3
  141. 3v1h8.8l2.5 2.5-.7.6.8.8 1.3-1.4L12.2 3H3zm1.4 2.1l-.8.8 2 2
  142. .8-.8-2-2zm4.5 0L8 6l2 2 1-1-2.1-1.9z' />
  143. <path d='M6.5 15h-1v-4H4v4H3v-5h3.5zM12 13H8v-3h4v3zm-3-1h2v-1H9v1z`
  144. )
  145. },
  146. "menu": {
  147. url: panelHash,
  148. tooltip: "Open Custom Navigation Settings",
  149. hotkey: "",
  150. content: buildSvg(14, 16, `M8.79
  151. 15H6.553l-.7-1.91-.608-.247-1.835.905-1.585-1.556.892-1.83-.25-.595L.5
  152. 9.127V6.933l1.944-.676.25-.597-.922-1.802L3.358
  153. 2.3l1.865.876.624-.248.638-1.93H8.73l.697 1.91.61.246 1.838-.905 1.58
  154. 1.555-1.114 2.317-2.714.65-.203-.24c-.444-.524-1.098-.824-1.794-.824C6.34
  155. 5.708 5.294 6.736 5.294 8c0 1.264 1.047 2.292 2.334 2.292.6 0 1.17-.224
  156. 1.604-.63l.18-.165 2.93.4 1.156 2.24-1.58 1.564-1.868-.88-.625.25L8.79
  157. 15zm-1.52-1h.78l.556-1.68 1.48-.592
  158. 1.62.765.553-.547-.583-1.13-1.93-.264c-.597.48-1.34.74-2.118.74-1.85
  159. 0-3.354-1.477-3.354-3.292 0-1.815 1.503-3.292 3.353-3.292.89 0
  160. 1.73.342 2.356.95l1.643-.394.6-1.25-.555-.546-1.598.786-1.455-.592L8.014
  161. 2h-.79L6.67 3.68l-1.48.59-1.622-.762-.556.546.802 1.566-.603
  162. 1.432-1.692.59v.763l1.71.558.603 1.43-.775 1.593.556.546 1.596-.788
  163. 1.456.593L7.27 14z`
  164. )
  165. },
  166. "pr": {
  167. url: "https://github.com/pulls",
  168. tooltip: "Pull Requests",
  169. hotkey: "g p",
  170. content: buildSvg(12, 16, `M11 11.28V5c-.03-.78-.34-1.47-.94-2.06C9.46
  171. 2.35 8.78 2.03 8 2H7V0L4 3l3
  172. 3V4h1c.27.02.48.11.69.31.21.2.3.42.31.69v6.28A1.993 1.993 0 0 0 10
  173. 15a1.993 1.993 0 0 0 1-3.72zm-1 2.92c-.66 0-1.2-.55-1.2-1.2 0-.65.55-1.2
  174. 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2zM4
  175. 3c0-1.11-.89-2-2-2a1.993 1.993 0 0 0-1 3.72v6.56A1.993 1.993 0 0 0 2
  176. 15a1.993 1.993 0 0 0 1-3.72V4.72c.59-.34 1-.98 1-1.72zm-.8 10c0 .66-.55
  177. 1.2-1.2 1.2-.65 0-1.2-.55-1.2-1.2 0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2
  178. 1.2zM2 4.2C1.34 4.2.8 3.65.8 3c0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2
  179. 1.2 0 .65-.55 1.2-1.2 1.2z`
  180. )
  181. },
  182. "profile": {
  183. url: "https://github.com/${me}",
  184. tooltip: "Profile",
  185. hotkey: "",
  186. content: buildSvg(8, 16, `M7 6H1c-.55 0-1 .45-1 1v5h2v3c0 .55.45 1 1
  187. 1h2c.55 0 1-.45 1-1v-3h2V7c0-.55-.45-1-1-1zm0
  188. 5H6V9H5v6H3V9H2v2H1V7h6v4zm0-8c0-1.66-1.34-3-3-3S1 1.34 1 3s1.34 3 3 3
  189. 3-1.34 3-3zM4 5c-1.11 0-2-.89-2-2 0-1.11.89-2 2-2 1.11 0 2 .89 2 2 0
  190. 1.11-.89 2-2 2z' fill-rule='evenodd`
  191. )
  192. },
  193. "settings": {
  194. url: "https://github.com/settings/profile",
  195. tooltip: "Settings",
  196. hotkey: "",
  197. content: buildSvg(14, 16, `M14
  198. 8.77v-1.6l-1.94-.64-.45-1.09.88-1.84-1.13-1.13-1.81.91-1.09-.45-.69-1.92h-1.6l-.63
  199. 1.94-1.11.45-1.84-.88-1.13 1.13.91 1.81-.45 1.09L0 7.23v1.59l1.94.64.45
  200. 1.09-.88 1.84 1.13 1.13 1.81-.91 1.09.45.69 1.92h1.59l.63-1.94 1.11-.45
  201. 1.84.88 1.13-1.13-.92-1.81.47-1.09L14 8.75v.02zM7 11c-1.66
  202. 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z`
  203. )
  204. },
  205. "stars": {
  206. url: "https://github.com/stars",
  207. tooltip: "Stars",
  208. hotkey: "",
  209. content: buildSvg(14, 16, `M14 6l-4.9-.64L7 1 4.9 5.36 0 6l3.6 3.26L2.67
  210. 14 7 11.67 11.33 14l-.93-4.74z`
  211. )
  212. },
  213. "watching": {
  214. url: "https://github.com/watching",
  215. tooltip: "Watching",
  216. hotkey: "",
  217. content: buildSvg(16, 16, `M8.06 2C3 2 0 8 0 8s3 6 8.06 6C13 14 16 8 16
  218. 8s-3-6-7.94-6zM8 12c-2.2 0-4-1.78-4-4 0-2.2 1.8-4 4-4 2.22 0 4 1.8 4 4
  219. 0 2.22-1.78 4-4 4zm2-4c0 1.11-.89 2-2 2-1.11 0-2-.89-2-2 0-1.11.89-2
  220. 2-2 1.11 0 2 .89 2 2z`
  221. )
  222. },
  223. "zenhub": {
  224. url: "#todo",
  225. tooltip: "ZenHub ToDo",
  226. hotkey: "",
  227. content: buildSvg(16, 16, `M29.17 45.988L13.82 21.218h10.56l-1.1-17.206
  228. 13.498 24.77h-9.514'/>`, 50, 50
  229. )
  230. }
  231. }
  232. };
  233.  
  234. const icons = {
  235. add: buildSvg(12, 14, `M12 9H7v5H5V9H0V7h5V2h2v5h5`, 12, 16),
  236. close: buildSvg(9, 9, `M9 1L5.4 4.4 9 8 8 9 4.6 5.4 1 9 0 8l3.6-3.5L0 1l1-1
  237. 3.5 3.6L8 0l1 1z`
  238. ),
  239. info: buildSvg(14, 16, `M6 10h2v2H6V10z m4-3.5c0 2.14-2 2.5-2 2.5H6c0-0.55
  240. 0.45-1 1-1h0.5c0.28 0 0.5-0.22 0.5-0.5v-1c0-0.28-0.22-0.5-0.5-0.5h-1c-0.28
  241. 0-0.5 0.22-0.5 0.5v0.5H4c0-1.5 1.5-3 3-3s3 1 3 2.5zM7 2.3c3.14 0 5.7 2.56
  242. 5.7 5.7S10.14 13.7 7 13.7 1.3 11.14 1.3 8s2.56-5.7 5.7-5.7m0-1.3C3.14 1 0
  243. 4.14 0 8s3.14 7 7 7 7-3.14 7-7S10.86 1 7 1z`
  244. ),
  245. separator: buildSvg(12, 16, `M7 16H5V0h2`)
  246. };
  247.  
  248. let drake;
  249. let editMode = false;
  250. let panelHashTriggered = false;
  251. // remember scrollTop when settings panel opens (if using sticky nav header style)
  252. let scrollTop = 0;
  253. let settings = GM_getValue("custom-links", defaults);
  254.  
  255. function addPanel() {
  256. GM_addStyle(`
  257. /* Use border right when a vertical bar is added */
  258. .Header-link.ghcn-separator { border-right:#777 1px solid;
  259. padding:4px 0; }
  260. /* settings panel */
  261. #ghcn-overlay { position:fixed; top:50px; left:0; right:0; bottom:0;
  262. z-index:45; background:rgba(0,0,0,.5); display:none; }
  263. #ghcn-menu { cursor:pointer; }
  264. .ghcn-close, .ghcn-code { float:right; cursor:pointer; font-size:.8em;
  265. margin-left:3px; padding:0 6px 2px 6px; }
  266. .ghcn-close .octicon { vertical-align:middle; fill:currentColor; }
  267. #ghcn-settings-inner { position:fixed; left:50%; top:60px; z-index:50;
  268. width:30rem; transform:translate(-50%,0); box-shadow:0 .5rem 1rem #111;
  269. color:#c0c0c0; display:none; }
  270. #ghcn-settings-inner input { width:85%; float:right; border-style:solid;
  271. border-width:1px; max-height:35px; }
  272. .ghcn-settings-wrapper div { line-height:38px; }
  273. #ghcn-nav-items { min-height: 38px; }
  274. #ghcn-nav-items .Header-item, .ghcn-nav .Header-item { margin-bottom:4px;
  275. margin-right:2px; }
  276. .ghcn-settings-wrapper hr { margin: 10px 0; }
  277. .ghcn-footer { margin-top:4px; border-top:#555 solid 1px; }
  278. li[data-ghcn] a { min-width:25px; text-align: center; }
  279. .Header-link { padding:2px 5px; margin:6px 0; }
  280. .ghcn-nav .Header-link svg, .ghcn-nav .Header-link img,
  281. #ghcn-nav-items .Header-link svg, #ghcn-nav-items .Header-link img,
  282. .gu-mirror svg, .gu-mirror img {
  283. max-height:16px; fill:currentColor; vertical-align:middle;
  284. overflow:visible; }
  285. /* override white text when settings panel is open*/
  286. body.ghcn-settings-open #ghcn-nav-items .text-emphasized {
  287. color: #24292e; }
  288. /* panel open */
  289. body.ghcn-settings-open {
  290. overflow:hidden !important; /* !important overrides wiki style */ }
  291. /* hide other header elements while settings is open (overflow issues) */
  292. body.ghcn-settings-open .header-search,
  293. body.ghcn-settings-open #user-links.d-flex,
  294. body.ghcn-settings-open .header-logo-invertocat,
  295. body.ghcn-settings-open .header-logo-wordmark,
  296. .gist-header .octicon-logo-github, /* hide GitHub logo on Gist page */
  297. .zh-todo-link { display:none !important; }
  298. body.ghcn-settings-open .ghcn-nav { width:100%; }
  299. body.ghcn-settings-open .Header-link > * { pointer-events:none; }
  300. body.ghcn-settings-open #ghcn-overlay,
  301. body.ghcn-settings-open #ghcn-settings-inner,
  302. #ghcn-nav-items { display:block; }
  303. body.ghcn-settings-open .ghcn-nav .Header-item,
  304. .ghcn-settings-wrapper .Header-item { cursor:move;
  305. border:#555 1px solid; border-radius:4px; margin-left: 2px;
  306. display:inline-block; }
  307. body.ghcn-settings-open .Header-link,
  308. .ghcn-settings-wrapper .Header-link { min-height:auto;
  309. min-width:16px; padding-top:1px; }
  310. body.ghcn-settings-open .js-header-wrapper .Header-link.form-control,
  311. body.ghcn-settings-open .Header .Header-link.form-control {
  312. background-color: transparent; border: 1px solid #444; }
  313. body.ghcn-settings-open .Header-link svg,
  314. body.ghcn-settings-open .Header-link img { margin-bottom:4px; }
  315. /* JSON code block */
  316. .ghcn-json-code { display:none; font-family:Menlo, Inconsolata,
  317. "Droid Mono", monospace; font-size:1em; height:calc(100% - 40px);
  318. resize:none; }
  319. .ghcn-visible { display:block; position:absolute; top:38px; bottom:0;
  320. left:2px; right:2px; z-index:1; width:476px; max-width:476px; }
  321. /* Dragula.min.css v3.7.2 (Microsoft definitions removed) */
  322. .gu-mirror { position:fixed !important; margin:0 !important;
  323. z-index:9999 !important; opacity:.8; list-style:none; }
  324. .gu-hide { display:none !important; }
  325. .gu-unselectable { -webkit-user-select:none !important;
  326. -moz-user-select:none !important; user-select:none !important; }
  327. .gu-transit { opacity:.2; }
  328. `);
  329.  
  330. make({
  331. el: "div",
  332. appendTo: "body",
  333. attr: {
  334. id: "ghcn-settings"
  335. },
  336. html: `
  337. <div id="ghcn-overlay"></div>
  338. <div id="ghcn-settings-inner" class="boxed-group">
  339. <h3 class="text-emphasized">GitHub Custom Navigation Settings
  340. <button type="button" class="ghcn-close btn btn-sm">
  341. ${icons.close}
  342. </button>
  343. <button type="button" class="ghcn-code btn btn-sm tooltipped tooltipped-w" aria-label="Toggle JSON data view">
  344. { }
  345. </button>
  346. </h3>
  347. <div class="ghcn-settings-wrapper boxed-group-inner">
  348. <ul id="ghcn-nav-items" class="BtnGroup ghcn-nav"></ul>
  349. <hr>
  350. <form>
  351. <p>Click an link above to edit its properties
  352. <a href="https://github.com/Mottie/GitHub-userscripts/wiki/GitHub-custom-navigation" class="tooltipped tooltipped-e" aria-label="Click to learn about the properties below">
  353. ${icons.info}
  354. </a>
  355. </p>
  356. <div>URL
  357. <span class="tooltipped tooltipped-e" aria-label="Enter a full URL, or hash">
  358. ${icons.info}
  359. </span>
  360. <input class="form-control ghcn-url" type="text"/>
  361. </div>
  362. <div>Tooltip<input class="form-control ghcn-tooltip" type="text"/></div>
  363. <div>Hotkey
  364. <a href="https://github.com/Mottie/GitHub-userscripts/wiki/GitHub-custom-navigation#hotkey" class="tooltipped tooltipped-e ghcn-hotkey-link" aria-label="Click to learn about hotkeys">
  365. ${icons.info}
  366. </a>
  367. <input class="form-control ghcn-hotkey" type="text"/>
  368. </div>
  369. <div>Content
  370. <span class="tooltipped tooltipped-e" aria-label="Include text and/or HTML (&lt;svg&gt; or &lt;img&gt;)">
  371. ${icons.info}
  372. </span>
  373. <input class="form-control ghcn-content" type="text"/>
  374. </div>
  375. </form>
  376. <textarea class="ghcn-json-code"></textarea>
  377. <div class="ghcn-footer">
  378. <span class="btn btn-sm ghcn-add">
  379. ${icons.add} New Link
  380. </span>
  381. <span class="btn btn-sm ghcn-destroy btn-danger tooltipped tooltipped-n" aria-label="Completely remove selected link">
  382. Destroy
  383. </span>
  384. <span class="btn btn-sm ghcn-reset btn-danger tooltipped tooltipped-n tooltipped-multiline" aria-label="Reset to default
  385. (Removes all custom entries!)">
  386. Reset
  387. </span>
  388. <span class="btn btn-sm ghcn-restore tooltipped tooltipped-n" aria-label="Restore missing default entries">
  389. Restore
  390. </span>
  391. </div>
  392. </div>
  393. </div>`
  394. });
  395. }
  396.  
  397. function updatePanel() {
  398. let indx, item, inNav, inSettings,
  399. panelStr = "#ghcn-nav-items",
  400. panel = $(panelStr),
  401. setItems = settings[getLocation()],
  402. keys = Object.keys(settings.items),
  403. len = keys.length;
  404. for (indx = 0; indx < len; indx++) {
  405. item = keys[indx];
  406. inNav = setItems.indexOf(item) > -1;
  407. inSettings = $(panelStr + ` .Header-item[data-ghcn="${item}"]`);
  408. // customize adds stuff to main nav
  409. if (inNav && inSettings) {
  410. panel.removeChild(inSettings);
  411. } else if (!inNav && !inSettings) {
  412. addToMenu(item, panelStr);
  413. }
  414. }
  415. if (!$(panelStr + " .Header-item[data-ghcn='separator']")) {
  416. addToMenu("separator", panelStr);
  417. }
  418. selectItem();
  419. }
  420.  
  421. function openPanel() {
  422. scrollTop = document.documentElement.scrollTop;
  423. window.scrollTo(0, 0);
  424. $("body").classList.add("ghcn-settings-open");
  425. editMode = true;
  426. customize();
  427. $(".modal-backdrop").click();
  428. $(".ghcn-json-code").classList.remove("ghcn-visible");
  429. }
  430.  
  431. function openPanelOnHash() {
  432. if (!panelHashTriggered && window.location.hash === panelHash) {
  433. panelHashTriggered = true;
  434. openPanel();
  435. // immediately remove the hash because I noticed issues where the "#" was
  436. // removed; and upon reload, a 404 page is shown because
  437. // "https://github.com/github-custom-navigation-settings" does not exist
  438. history.pushState("", document.title, window.location.pathname);
  439. panelHashTriggered = false;
  440. }
  441. }
  442.  
  443. function closePanel() {
  444. if (editMode) {
  445. window.scrollTo(0, scrollTop);
  446. $("body").classList.remove("ghcn-settings-open");
  447. editMode = false;
  448. customize();
  449. $(".ghcn-json-code").classList.remove("ghcn-visible");
  450. }
  451. }
  452.  
  453. function getLocation() {
  454. // used by "settings" object
  455. return window.location.hostname === "gist.github.com" ? "gists" : "github";
  456. }
  457.  
  458. // continually destroying & reapplying Dragula sometimes ignores elements;
  459. // so just leave it always applied
  460. function addDragula() {
  461. let topNav = $(".ghcn-nav");
  462. drake = dragula($$(".ghcn-nav, #ghcn-nav-items"), {
  463. invalid: () => {
  464. return !editMode;
  465. }
  466. });
  467. drake.on("drop", () => {
  468. let indx, link,
  469. temp = [],
  470. list = topNav.childNodes,
  471. len = list.length;
  472. for (indx = 0; indx < len; indx++) {
  473. link = list[indx].getAttribute("data-ghcn");
  474. if (link) {
  475. temp[temp.length] = link;
  476. }
  477. }
  478. settings[getLocation()] = temp;
  479. GM_setValue("custom-links", settings);
  480. updatePanel();
  481.  
  482. });
  483. }
  484.  
  485. // Clicked item; show selection
  486. function selectItem() {
  487. // highlight current link
  488. let temp = $$(".Header-link.focus");
  489. removeClass(temp, "focus");
  490. temp = $$(".Header-item[data-ghcn='" + (settings.currentLink || "") +
  491. "'] .Header-link");
  492. if (temp[0]) {
  493. addClass(temp, "focus");
  494. updateLink(temp[0].parentNode);
  495. }
  496. }
  497.  
  498. // New Link button pressed
  499. function createLink() {
  500. let name = findUniqueId("custom");
  501. settings.items[name] = {
  502. url: "",
  503. tooltip: "",
  504. hotkey: "",
  505. content: "*"
  506. };
  507. addToMenu(name, "#ghcn-nav-items");
  508. settings.currentLink = name;
  509. selectItem();
  510. }
  511.  
  512. // append named list item to menu
  513. function addToMenu(name, target) {
  514. let html,
  515. item = settings.items[name] || {},
  516. url = (item.url || "").replace(/\$\{me\}/g, user),
  517. linkClass = "text-emphasized Header-link " +
  518. (editMode ? "" : "js-selected-navigation-item");
  519. // only show tooltip if defined
  520. if (item.tooltip) {
  521. linkClass += " tooltipped tooltipped-s";
  522. if (/(&#10;|&#xA;)/g.test(item.tooltip)) {
  523. linkClass += " tooltipped-multiline";
  524. }
  525. }
  526. if (name === "separator") {
  527. html = editMode
  528. // *** Separator (icon in editMode; zero-width-space when not)
  529. ? `<span class="${linkClass} tooltipped tooltipped-s" aria-label="Menu separator">${icons.separator}</span>`
  530. : `<span class="Header-link ghcn-separator linkable-line-number">&#8203;</span>`;
  531. } else {
  532. html = editMode ?
  533. `<span class="${linkClass}" aria-label="${item.tooltip}">${item.content}</span>` :
  534. // GitHub might get upset, but we're not going to bother with analytics;
  535. // not including "data-ga-click" nor "data-selected-links" attributes
  536. `<a href="${url}" class="${linkClass}" aria-label="${item.tooltip}" data-hotkey="${item.hotkey}">
  537. ${item.content}
  538. </a>`;
  539. }
  540. make({
  541. el: "span",
  542. appendTo: target,
  543. attr: {
  544. "data-ghcn": name
  545. },
  546. cl4ss: "Header-item",
  547. html: html
  548. });
  549. }
  550.  
  551. // Destroy button pressed
  552. function destroyLink(item) {
  553. if (item) {
  554. delete settings.items[item];
  555. GM_setValue("custom-links", settings);
  556. let el,
  557. indx = settings.github.indexOf(item);
  558. if (indx >= 0) {
  559. settings.github.splice(indx, 1);
  560. }
  561. indx = settings.gists.indexOf(item);
  562. if (indx >= 0) {
  563. settings.gists.splice(indx, 1);
  564. }
  565. el = $(`.Header-item[data-ghcn="${item}"]`);
  566. if (el) {
  567. el.parentNode.removeChild(el);
  568. }
  569. if ((settings.currentLink || "") === item) {
  570. settings.currentLink = "";
  571. }
  572. updateLink();
  573. }
  574. }
  575.  
  576. // Reset button pressed or new JSON added
  577. function resetLinks(newSettings) {
  578. if (newSettings) {
  579. settings = newSettings;
  580. } else {
  581. // quick n'dirty deep merge
  582. let str = JSON.stringify(defaults);
  583. settings = JSON.parse(str);
  584. }
  585. GM_setValue("custom-links", settings);
  586. // remove extra items individually; dragula doesn't seem to like it when we
  587. // use innerHTML = ""
  588. let item,
  589. els = $$(".Header-item"),
  590. indx = els.length;
  591. while (indx--) {
  592. item = els[indx].getAttribute("data-ghcn");
  593. if (item !== "separator" && !settings.items.hasOwnProperty(item)) {
  594. destroyLink(item);
  595. }
  596. }
  597. customize();
  598. }
  599.  
  600. function restoreLinks() {
  601. Object.assign(settings.items, defaults.items);
  602. GM_setValue("custom-links", settings);
  603. updatePanel();
  604. }
  605.  
  606. // Clicked item; update input values
  607. function updateLink(el) {
  608. let item = el && el.getAttribute("data-ghcn") || "",
  609. link = settings.items[item] || {};
  610. settings.currentLink = item;
  611. $(".ghcn-url").value = link.url || "";
  612. $(".ghcn-tooltip").value = link.tooltip || "";
  613. $(".ghcn-hotkey").value = link.hotkey || "";
  614. $(".ghcn-content").value = link.content || "";
  615.  
  616. // "separator" shouldn't show options
  617. $(".ghcn-settings-wrapper form").style.visibility = item === "separator" ?
  618. "hidden" :
  619. "visible";
  620. }
  621.  
  622. // save changes on-the-fly
  623. function saveLink() {
  624. let name = settings.currentLink || "",
  625. item = $(`.Header-item[data-ghcn="${name}"] .Header-link`);
  626. if (name) {
  627. settings.items[name] = {
  628. url: $(".ghcn-url").value,
  629. tooltip: $(".ghcn-tooltip").value,
  630. hotkey: $(".ghcn-hotkey").value,
  631. content: $(".ghcn-content").value
  632. };
  633. GM_setValue("custom-links", settings);
  634. // update item (should be unique)
  635. if (item) {
  636. // "\n" is the only thing that works as a carriage return for
  637. // javascript's setAttribute; see
  638. // http://wowmotty.blogspot.com/2014/04/methods-to-add-multi-line-css-content.html
  639. item.setAttribute(
  640. "aria-label",
  641. settings.items[name].tooltip.replace(/(&#10;|&#xA;)/g, "\n")
  642. );
  643. item.innerHTML = settings.items[name].content;
  644. }
  645. }
  646. }
  647.  
  648. function addJSON() {
  649. $(".ghcn-json-code").value = JSON.stringify(settings, null, 2);
  650. }
  651.  
  652. function processJSON() {
  653. let val,
  654. txt = $(".ghcn-json-code").value;
  655. try {
  656. val = JSON.parse(txt);
  657. } catch (err) {
  658. console.error("GitHub Custom Navigation: Invalid JSON!");
  659. }
  660. return val;
  661. }
  662.  
  663. function addBindings() {
  664. // Create a menu entry
  665. let el,
  666. menu = make({
  667. el: "a",
  668. cl4ss: "dropdown-item",
  669. html: "Custom Nav Settings",
  670. attr: {
  671. id: "ghcn-menu"
  672. }
  673. });
  674.  
  675. el = $$(`
  676. .Header .dropdown-item[href='/settings/profile'],
  677. .Header .dropdown-item[data-ga-click*='go to profile'],
  678. .js-header-wrapper .dropdown-item[href='/settings/profile'],
  679. .js-header-wrapper .dropdown-item[data-ga-click*='go to profile']`
  680. );
  681. // get last found item - gists only have the "go to profile" item; GitHub
  682. // has both
  683. el = el[el.length - 1];
  684. if (el) {
  685. // insert after
  686. el.parentNode.insertBefore(menu, el.nextSibling);
  687. on($("#ghcn-menu"), "click", openPanel);
  688. }
  689.  
  690. on(window, "hashchange", openPanelOnHash);
  691. on($("#ghcn-overlay"), "click", event => {
  692. // ignore bubbled up events
  693. if (event.target.id === "ghcn-overlay") {
  694. closePanel();
  695. }
  696. });
  697. on($("body"), "keyup", event => {
  698. // using F2 key for testing
  699. if (editMode && event.keyCode === 27) {
  700. closePanel();
  701. }
  702. });
  703. on($("body"), "click", event => {
  704. const target = event.target;
  705. if (editMode && target.classList.contains("Header-link")) {
  706. // Header-link is a child of Header-item, but is the same size
  707. settings.currentLink = target.parentNode.getAttribute("data-ghcn");
  708. selectItem();
  709. }
  710. });
  711. on($$(".ghcn-settings-wrapper input"), "input change", saveLink);
  712. on($(".ghcn-add"), "click", createLink);
  713. on($(".ghcn-destroy"), "click", () => destroyLink(settings.currentLink));
  714. on($(".ghcn-reset"), "click", resetLinks);
  715. on($(".ghcn-restore"), "click", restoreLinks);
  716. // close panel when hotkey link is clicked or the page scrolls on the
  717. // documentation wiki
  718. on($$(".ghcn-close, .ghcn-hotkey-link"), "click", closePanel);
  719.  
  720. // Code
  721. on($(".ghcn-code"), "click", () => {
  722. // open JSON code textarea
  723. $(".ghcn-json-code").classList.toggle("ghcn-visible");
  724. addJSON();
  725. });
  726. // close JSON code textarea
  727. on($(".ghcn-json-code"), "focus", function() {
  728. this.select();
  729. });
  730. on($(".ghcn-json-code"), "paste", () => {
  731. setTimeout(() => {
  732. checkJSON(processJSON());
  733. }, 200);
  734. });
  735.  
  736. }
  737.  
  738. function checkJSON(val, init) {
  739. let hasGitHub = false,
  740. hasGists = false,
  741. hasItems = false;
  742. if (val) {
  743. hasGitHub = val.hasOwnProperty("github");
  744. hasGists = val.hasOwnProperty("gists");
  745. hasItems = val.hasOwnProperty("items");
  746. // simple validation
  747. if (hasGitHub && hasGists && hasItems) {
  748. if (!init) {
  749. resetLinks(val);
  750. $(".ghcn-json-code").classList.remove("ghcn-visible");
  751. selectItem();
  752. }
  753. return true;
  754. }
  755. }
  756. let msg = [];
  757. if (!hasGitHub) {
  758. msg.push(`"github"`);
  759. }
  760. if (!hasGists) {
  761. msg.push(`"gists"`);
  762. }
  763. if (!hasItems) {
  764. msg.push(`"items"`);
  765. }
  766. msg = msg.length ? "JSON is missing " + msg.join(" & ") : "Invalid JSON";
  767. console.error("GitHub Custom Navigation: " + msg, val);
  768. return false;
  769. }
  770.  
  771. // add new link; needs a unique ID
  772. function findUniqueId(prefix) {
  773. let indx = 0,
  774. id = prefix + indx;
  775. if (settings.items[id]) {
  776. while (settings.items[id]) {
  777. id = prefix + indx++;
  778. }
  779. }
  780. return id;
  781. }
  782.  
  783. // Main process - adds links to header navigation
  784. function customize() {
  785. let nav = $(".Header nav");
  786. if (nav) {
  787. nav.classList.add("ghcn-nav");
  788. let indx, els,
  789. navStr = ".ghcn-nav",
  790. setItems = settings[getLocation()],
  791. len = setItems.length;
  792. if (!len) {
  793. return;
  794. }
  795.  
  796. els = nav.childNodes;
  797. indx = els.length;
  798. while (indx--) {
  799. nav.removeChild(els[indx]);
  800. }
  801.  
  802. for (indx = 0; indx < len; indx++) {
  803. addToMenu(setItems[indx], navStr);
  804. }
  805. // make sure all svg's have an "octicon" class name
  806. addClass($$(navStr + " svg"), "octicon");
  807.  
  808. if (editMode) {
  809. updatePanel();
  810. }
  811. }
  812. }
  813.  
  814. function $(selector, el) {
  815. return (el || document).querySelector(selector);
  816. }
  817.  
  818. function $$(selector, el) {
  819. return [...(el || document).querySelectorAll(selector)];
  820. }
  821.  
  822. function addClass(els, name) {
  823. let indx = els.length;
  824. while (indx--) {
  825. els[indx].classList.add(name);
  826. }
  827. }
  828.  
  829. function removeClass(els, name) {
  830. let indx = els.length;
  831. while (indx--) {
  832. els[indx].classList.remove(name);
  833. }
  834. }
  835.  
  836. function on(els, name, callback) {
  837. els = Array.isArray(els) ? els : [els];
  838. let events = name.split(/\s+/);
  839. els.forEach(el => {
  840. events.forEach(ev => {
  841. el.addEventListener(ev, callback);
  842. });
  843. });
  844. }
  845.  
  846. function make(obj) {
  847. let key,
  848. el = document.createElement(obj.el);
  849. if (obj.cl4ss) {
  850. el.className = obj.cl4ss;
  851. }
  852. if (obj.html) {
  853. el.innerHTML = obj.html;
  854. }
  855. if (obj.attr) {
  856. for (key in obj.attr) {
  857. if (obj.attr.hasOwnProperty(key)) {
  858. el.setAttribute(key, obj.attr[key]);
  859. }
  860. }
  861. }
  862. if (obj.appendTo) {
  863. $(obj.appendTo).appendChild(el);
  864. }
  865. return el;
  866. }
  867.  
  868. let isValid = checkJSON(settings, "init");
  869. if (!isValid) {
  870. resetLinks();
  871. }
  872. customize();
  873. addPanel();
  874. addBindings();
  875. addDragula();
  876. openPanelOnHash();
  877.  
  878. })();

QingJ © 2025

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