GitHub Custom Navigation

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

当前为 2018-02-11 提交的版本,查看 最新版本

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