Greasy Fork is available in English.

GitHub Custom Navigation

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

Od 10.08.2016.. Pogledajte najnovija verzija.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

  1. // ==UserScript==
  2. // @name GitHub Custom Navigation
  3. // @version 1.0.4
  4. // @description A userscript that allows you to customize GitHub's main navigation bar
  5. // @license https://creativecommons.org/licenses/by-sa/4.0/
  6. // @namespace http://github.com/Mottie
  7. // @include https://github.com/*
  8. // @include https://gist.github.com/*
  9. // @grant GM_addStyle
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.1/dragula.js
  13. // @run-at document-end
  14. // @author Rob Garrison
  15. // ==/UserScript==
  16. /* global GM_addStyle, GM_getValue, GM_setValue, dragula */
  17. /* jshint esnext:true, unused:true */
  18. (function() {
  19. "use strict";
  20.  
  21. let drake,
  22. editMode = false,
  23. // remember scrollTop when settings panel opens (if using sticky nav header style)
  24. scrollTop = 0,
  25. // when the bar gets wider than this number, adjust the width of the search input
  26. adjustWidth = 460,
  27. // when search input width is adjusted, this is the minimum width
  28. minSearchInputWidth = 200,
  29.  
  30. // open menu via hash
  31. panelHash = "#github-custom-nav-settings",
  32. panelHashTriggered = false,
  33.  
  34. defaults = {
  35. github: ["pr", "issues", "gist", "separator", "stars", "watching", "separator", "profile", "blog", "menu"],
  36. gists: ["gistall", "giststars", "github", "separator", "pr", "issues", "stars", "watching", "separator", "profile", "blog", "menu"],
  37.  
  38. currentLink: "pr",
  39. // using full length url so the links work from any subdomain (e.g. gist pages)
  40. items: {
  41. "blog": {
  42. url: "https://github.com/blog",
  43. tooltip: "Blog",
  44. hotkey: "",
  45. 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>"
  46. },
  47. "explore": {
  48. url: "https://github.com/explore",
  49. tooltip: "Explore",
  50. hotkey: "",
  51. 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='M10 1c-.17 0-.36.05-.52.14C8.04 2.02 4.5 4.58 3 5c-1.38 0-3 .67-3 2.5S1.63 10 3 10c.3.08.64.23 1 .41V15h2v-3.45c1.34.86 2.69 1.83 3.48 2.31.16.09.34.14.52.14.52 0 1-.42 1-1V2c0-.58-.48-1-1-1zm0 12c-.38-.23-.89-.58-1.5-1-.16-.11-.33-.22-.5-.34V3.31c.16-.11.31-.2.47-.31.61-.41 1.16-.77 1.53-1v11zm2-6h4v1h-4V7zm0 2l4 2v1l-4-2V9zm4-6v1l-4 2V5l4-2z'></path></svg>"
  52. },
  53. "gist": {
  54. url: "https://gist.github.com/",
  55. tooltip: "Gist",
  56. hotkey: "",
  57. 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>"
  58. },
  59. "gistall": {
  60. url: "https://gist.github.com/discover",
  61. tooltip: "Discover Gists",
  62. hotkey: "",
  63. 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>"
  64. },
  65. "giststars": {
  66. url: "https://gist.github.com/${me}/starred",
  67. tooltip: "Starred Gists",
  68. hotkey: "",
  69. 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>"
  70. },
  71. "github": {
  72. url: "https://github.com",
  73. tooltip: "GitHub",
  74. hotkey: "",
  75. 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>"
  76. },
  77. "integrations": {
  78. url: "https://github.com/integrations",
  79. tooltip: "Integrations",
  80. hotkey: "",
  81. 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='M3 6c-.55 0-1 .45-1 1v2c0 .55.45 1 1 1h8c.55 0 1-.45 1-1V7c0-.55-.45-1-1-1H3zm8 1.75L9.75 9h-1.5L7 7.75 5.75 9h-1.5L3 7.75V7h.75L5 8.25 6.25 7h1.5L9 8.25 10.25 7H11v.75zM5 11h4v1H5v-1zm2-9C3.14 2 0 4.91 0 8.5V13c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V8.5C14 4.91 10.86 2 7 2zm6 11H1V8.5c0-3.09 2.64-5.59 6-5.59s6 2.5 6 5.59V13z'></path></svg>"
  82. },
  83. "issues": {
  84. url: "https://github.com/issues",
  85. tooltip: "Issues",
  86. hotkey: "g i",
  87. 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>"
  88. },
  89. "menu": {
  90. url: panelHash,
  91. tooltip: "Open Custom Navigation Settings",
  92. hotkey: "",
  93. 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>"
  94. },
  95. "pr": {
  96. url: "https://github.com/pulls",
  97. tooltip: "Pull Requests",
  98. hotkey: "g p",
  99. 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>"
  100. },
  101. "profile": {
  102. url: "https://github.com/${me}",
  103. tooltip: "Profile",
  104. hotkey: "",
  105. 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>"
  106. },
  107. "settings": {
  108. url: "https://github.com/settings/profile",
  109. tooltip: "Settings",
  110. hotkey: "",
  111. 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>"
  112. },
  113. "stars": {
  114. url: "https://github.com/stars",
  115. tooltip: "Stars",
  116. hotkey: "",
  117. 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>"
  118. },
  119. "trending": {
  120. url: "https://github.com/trending",
  121. tooltip: "Trending",
  122. hotkey: "",
  123. 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>"
  124. },
  125. "watching": {
  126. url: "https://github.com/watching",
  127. tooltip: "Watching",
  128. hotkey: "",
  129. 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>"
  130. },
  131. "zenhub": {
  132. url: "#todo",
  133. tooltip: "ZenHub ToDo",
  134. hotkey: "",
  135. 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>"
  136. }
  137. }
  138. },
  139.  
  140. // get user name; or empty string if not logged in
  141. user = $("meta[name='user-login']").getAttribute("content") || "",
  142. settings = GM_getValue("custom-links", defaults),
  143.  
  144. icons = {
  145. 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>",
  146. 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>",
  147. 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>",
  148. 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>"
  149. };
  150.  
  151. function addPanel() {
  152. GM_addStyle(`
  153. /* Use border right when a vertical bar is added */
  154. .header-nav-link.ghcn-separator { border-right:#e5e5e5 2px solid; padding:4px 0; }
  155. /* settings panel */
  156. #ghcn-overlay { position:fixed; top:50px; left:0; right:0; bottom:0; z-index:5;
  157. background:rgba(0,0,0,.5); display:none; }
  158. #ghcn-menu { cursor:pointer; }
  159. .ghcn-close, .ghcn-code { float:right; cursor:pointer; font-size:.8em; margin-left:3px;
  160. padding:0 6px 2px 6px; }
  161. .ghcn-close .octicon { vertical-align:middle; fill:currentColor; }
  162. #ghcn-settings-inner { position:fixed; left:50%; top:55px; z-index:10; width:30rem;
  163. transform:translate(-50%,0); box-shadow:0 .5rem 1rem #111; color:#c0c0c0; display:none; }
  164. #ghcn-settings-inner input { width:85%; float:right; border-style:solid; border-width:1px;
  165. max-height:35px; }
  166. .ghcn-settings-wrapper div { line-height:38px; }
  167. #ghcn-nav-items { min-height: 38px; }
  168. #ghcn-nav-items .header-nav-item { margin-bottom:4px; }
  169. .ghcn-settings-wrapper hr { margin: 10px 0; }
  170. .ghcn-footer { margin-top:4px; border-top:#555 solid 1px; }
  171. .header-nav-link { height:28px; }
  172. .header-nav.left .header-nav-link svg, .header-nav.left .header-nav-link img,
  173. #ghcn-nav-items .header-nav-link svg, #ghcn-nav-items .header-nav-link img,
  174. .gu-mirror svg, .gu-mirror img { max-height:16px; fill:currentColor; vertical-align:middle;
  175. overflow:visible; }
  176. /* panel open */
  177. body.ghcn-settings-open { overflow:hidden !important; /* !important overrides wiki style */ }
  178. /* hide other header elements while settings is open (overflow issues) */
  179. body.ghcn-settings-open .header-search,
  180. body.ghcn-settings-open .header-nav.right,
  181. body.ghcn-settings-open .header-logo-invertocat,
  182. body.ghcn-settings-open .header-logo-wordmark,
  183. .gist-header .octicon-logo-github, /* hide GitHub logo on Gist page */
  184. .zh-todo-link { display:none; }
  185. body.ghcn-settings-open .header-nav.left { width:100%; }
  186. body.ghcn-settings-open .header-nav-link > * { pointer-events:none; }
  187. body.ghcn-settings-open #ghcn-overlay,
  188. body.ghcn-settings-open #ghcn-settings-inner,
  189. #ghcn-nav-items { display:block; }
  190. body.ghcn-settings-open .header-nav.left .header-nav-item,
  191. .ghcn-settings-wrapper .header-nav-item { cursor:move; border:#555 1px solid; border-radius:4px;
  192. margin-left: 2px; }
  193. body.ghcn-settings-open .header-nav-link,
  194. .ghcn-settings-wrapper .header-nav-link { min-height:auto; min-width:16px; }
  195. /* JSON code block */
  196. .ghcn-json-code { display:none; font-family:Menlo, Inconsolata, "Droid Mono", monospace; font-size:1em; }
  197. .ghcn-visible { display:block; position:absolute; top:38px; bottom:0; left:2px; right:2px; z-index:1;
  198. width:476px; max-width:476px; }
  199. /* Dragula.min.css v3.7.1 (Microsoft definitions removed) */
  200. .gu-mirror { position:fixed !important; margin:0 !important; z-index:9999 !important; opacity:.8;
  201. list-style:none; }
  202. .gu-hide { display:none !important; }
  203. .gu-unselectable { -webkit-user-select:none !important; -moz-user-select:none !important;
  204. user-select:none !important; }
  205. .gu-transit { opacity:.2; }
  206. `);
  207.  
  208. make({
  209. el: "div",
  210. appendTo: "body",
  211. attr: { id: "ghcn-settings" },
  212. html: `
  213. <div id="ghcn-overlay"></div>
  214. <div id="ghcn-settings-inner" class="boxed-group">
  215. <h3>GitHub Custom Navigation Settings
  216. <button type="button" class="ghcn-close btn btn-sm">${icons.close}</button>
  217. <button type="button" class="ghcn-code btn btn-sm tooltipped tooltipped-n" aria-label="Toggle JSON data view">{ }</button>
  218. </h3>
  219. <div class="ghcn-settings-wrapper boxed-group-inner">
  220. <ul id="ghcn-nav-items" class="btn-group header-nav"></ul>
  221. <hr>
  222. <form>
  223. <p>Click an link above to edit its properties
  224. <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>
  225. </p>
  226. <div>URL
  227. <span class="tooltipped tooltipped-e" aria-label="Enter a full URL, or hash">${icons.info}</span>
  228. <input class="form-control ghcn-url" type="text"/>
  229. </div>
  230. <div>Tooltip<input class="form-control ghcn-tooltip" type="text"/></div>
  231. <div>Hotkey
  232. <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>
  233. <input class="form-control ghcn-hotkey" type="text"/>
  234. </div>
  235. <div>Content
  236. <span class="tooltipped tooltipped-e" aria-label="Include text and/or HTML (&lt;svg&gt; or &lt;img&gt;)">${icons.info}</span>
  237. <input class="form-control ghcn-content" type="text"/>
  238. </div>
  239. </form>
  240. <textarea class="ghcn-json-code"></textarea>
  241. <div class="ghcn-footer">
  242. <span class="btn btn-sm ghcn-add">${icons.add} New Link</span>
  243. <span class="btn btn-sm ghcn-destroy btn-danger tooltipped tooltipped-n" aria-label="Completely remove selected link">Destroy</span>
  244. <span class="btn btn-sm ghcn-reset tooltipped tooltipped-n" aria-label="Restore Defaults">Reset</span>
  245. </div>
  246. </div>
  247. </div>`
  248. });
  249. }
  250.  
  251. function updatePanel() {
  252. let indx, item, inNav, inSettings,
  253. panelStr = "#ghcn-nav-items",
  254. panel = $(panelStr),
  255. setItems = settings[getLocation()],
  256. keys = Object.keys(settings.items),
  257. len = keys.length;
  258. for (indx = 0; indx < len; indx++) {
  259. item = keys[indx];
  260. inNav = setItems.indexOf(item) > -1;
  261. inSettings = $(panelStr + ` .header-nav-item[data-ghcn="${item}"]`);
  262. // customize adds stuff to main nav
  263. if (inNav && inSettings) {
  264. panel.removeChild(inSettings);
  265. } else if (!inNav && !inSettings) {
  266. addToMenu(item, panelStr);
  267. }
  268. }
  269. if (!$(panelStr + " .header-nav-item[data-ghcn='separator']")) {
  270. addToMenu("separator", panelStr);
  271. }
  272. selectItem();
  273. }
  274.  
  275. function openPanel() {
  276. scrollTop = document.documentElement.scrollTop;
  277. window.scrollTo(0,0);
  278. $("body").classList.add("ghcn-settings-open");
  279. editMode = true;
  280. customize();
  281. $(".modal-backdrop").click();
  282. $(".ghcn-json-code").classList.remove("ghcn-visible");
  283. }
  284.  
  285. function openPanelOnHash() {
  286. if (!panelHashTriggered && window.location.hash === panelHash) {
  287. panelHashTriggered = true;
  288. openPanel();
  289. // immediately remove the hash because I noticed issues where the "#" was removed; and upon
  290. // reload, a 404 page is shown because "https://github.com/github-custom-navigation-settings"
  291. // does not exist
  292. history.pushState("", document.title, window.location.pathname);
  293. panelHashTriggered = false;
  294. }
  295. }
  296.  
  297. function closePanel() {
  298. if (editMode) {
  299. window.scrollTo(0, scrollTop);
  300. $("body").classList.remove("ghcn-settings-open");
  301. editMode = false;
  302. customize();
  303. $(".ghcn-json-code").classList.remove("ghcn-visible");
  304. }
  305. }
  306.  
  307. function getLocation() {
  308. // used by "settings" object
  309. return window.location.hostname === "gist.github.com" ? "gists" : "github";
  310. }
  311.  
  312. // continually destroying & reapplying Dragula sometimes ignores elements;
  313. // so just leave it always applied
  314. function addDragula() {
  315. let topNav = $(".header-nav");
  316. drake = dragula($$(".header-nav, #ghcn-nav-items"), {
  317. invalid: function() {
  318. return !editMode;
  319. }
  320. });
  321. drake.on("drop", function() {
  322. let indx, link,
  323. temp = [],
  324. list = topNav.childNodes,
  325. len = list.length;
  326. for (indx = 0; indx < len; indx++) {
  327. link = list[indx].getAttribute("data-ghcn");
  328. if (link) {
  329. temp[temp.length] = link;
  330. }
  331. }
  332. settings[getLocation()] = temp;
  333. GM_setValue("custom-links", settings);
  334. updatePanel();
  335.  
  336. });
  337. }
  338.  
  339. // Clicked item; show selection
  340. function selectItem() {
  341. // highlight current link
  342. let temp = $$(".header-nav-link.focus");
  343. removeClass(temp, "focus");
  344. temp = $$(".header-nav-item[data-ghcn='" + (settings.currentLink || "") + "'] .header-nav-link");
  345. if (temp[0]) {
  346. addClass(temp, "focus");
  347. updateLink(temp[0].parentNode);
  348. }
  349. }
  350.  
  351. // New Link button pressed
  352. function createLink() {
  353. let name = findUniqueId("custom");
  354. settings.items[name] = { url: "", tooltip: "", hotkey: "", content: "*" };
  355. addToMenu(name, "#ghcn-nav-items");
  356. settings.currentLink = name;
  357. selectItem();
  358. }
  359.  
  360. // append named list item to menu
  361. function addToMenu(name, target) {
  362. let html,
  363. item = settings.items[name] || {},
  364. url = (item.url || "").replace(/\$\{me\}/g, user),
  365. linkClass = editMode ?
  366. "header-nav-link form-control" :
  367. "js-selected-navigation-item header-nav-link";
  368. // only show tooltip if defined
  369. if (item.tooltip) {
  370. linkClass += " tooltipped tooltipped-s";
  371. if (/(&#10;|&#xA;)/g.test(item.tooltip)) {
  372. linkClass += " tooltipped-multiline";
  373. }
  374. }
  375. if (name === "separator") {
  376. html = editMode ?
  377. // *** Separator (icon in editMode; zero-width-space when not)
  378. `<span class="${linkClass} tooltipped tooltipped-s" aria-label="Menu separator">${icons.separator}</span>` :
  379. `<span class="header-nav-link ghcn-separator linkable-line-number">&#8203;</span>`;
  380. } else {
  381. html = editMode ?
  382. `<span class="${linkClass}" aria-label="${item.tooltip}">${item.content}</span>` :
  383. // GitHub might get upset, but we're not going to bother with analytics;
  384. // not including "data-ga-click" nor "data-selected-links" attributes
  385. `<a href="${url}" class="${linkClass}" aria-label="${item.tooltip}" data-hotkey="${item.hotkey}">
  386. ${item.content}
  387. </a>`;
  388. }
  389. make({
  390. el: "li",
  391. appendTo: target,
  392. attr: { "data-ghcn": name },
  393. cl4ss: "header-nav-item",
  394. html: html
  395. });
  396. }
  397.  
  398. // Destroy button pressed
  399. function destroyLink(item) {
  400. if (item) {
  401. delete settings.items[item];
  402. GM_setValue("custom-links", settings);
  403. let el,
  404. indx = settings.github.indexOf(item);
  405. if (indx >= 0) {
  406. settings.github.splice(indx, 1);
  407. }
  408. indx = settings.gists.indexOf(item);
  409. if (indx >= 0) {
  410. settings.gists.splice(indx, 1);
  411. }
  412. el = $(`.header-nav-item[data-ghcn="${item}"]`);
  413. el.parentNode.removeChild(el);
  414. if ((settings.currentLink || "") === item) {
  415. settings.currentLink = "";
  416. }
  417. updateLink();
  418. }
  419. }
  420.  
  421. // Reset button pressed or new JSON added
  422. function resetLinks(newSettings) {
  423. if (newSettings) {
  424. settings = newSettings;
  425. } else {
  426. // quick n'dirty deep merge
  427. let str = JSON.stringify(defaults);
  428. settings = JSON.parse(str);
  429. }
  430. GM_setValue("custom-links", settings);
  431. // remove extra items individually; dragula doesn't seem to like it when we use innerHTML = ""
  432. let item,
  433. els = $$(".header-nav-item"),
  434. indx = els.length;
  435. while (indx--) {
  436. item = els[indx].getAttribute("data-ghcn");
  437. if (item !== "separator" && !settings.items.hasOwnProperty(item)) {
  438. destroyLink(item);
  439. }
  440. }
  441. customize();
  442. }
  443.  
  444. // Clicked item; update input values
  445. function updateLink(el) {
  446. let item = el && el.getAttribute("data-ghcn") || "",
  447. link = settings.items[item] || {};
  448. settings.currentLink = item;
  449. $(".ghcn-url").value = link.url || "";
  450. $(".ghcn-tooltip").value = link.tooltip || "";
  451. $(".ghcn-hotkey").value = link.hotkey || "";
  452. $(".ghcn-content").value = link.content || "";
  453.  
  454. // "separator" shouldn't show options
  455. $(".ghcn-settings-wrapper form").style.visibility = item === "separator" ? "hidden" : "visible";
  456. }
  457.  
  458. // save changes on-the-fly
  459. function saveLink() {
  460. let name = settings.currentLink || "",
  461. item = $(`.header-nav-item[data-ghcn="${name}"] .header-nav-link`);
  462. if (name) {
  463. settings.items[name] = {
  464. url : $(".ghcn-url").value,
  465. tooltip : $(".ghcn-tooltip").value,
  466. hotkey : $(".ghcn-hotkey").value,
  467. content : $(".ghcn-content").value
  468. };
  469. GM_setValue("custom-links", settings);
  470. // update item (should be unique)
  471. if (item) {
  472. // "\n" is the only thing that works as a carriage return for javascript's setAttribute
  473. // See http://wowmotty.blogspot.com/2014/04/methods-to-add-multi-line-css-content.html
  474. item.setAttribute("aria-label", settings.items[name].tooltip.replace(/(&#10;|&#xA;)/g, "\n"));
  475. item.innerHTML = settings.items[name].content;
  476. }
  477. }
  478. }
  479.  
  480. function addJSON() {
  481. $(".ghcn-json-code").value = JSON.stringify(settings, null, 2);
  482. }
  483.  
  484. function processJSON() {
  485. var val,
  486. txt = $(".ghcn-json-code").value;
  487. try {
  488. val = JSON.parse(txt);
  489. } catch (err) {
  490. console.error("GitHub Custom Navigation: Invalid JSON!");
  491. }
  492. return val;
  493. }
  494.  
  495. function addBindings() {
  496. // Create a menu entry
  497. let el,
  498. menu = make({
  499. el : "a",
  500. cl4ss : "dropdown-item",
  501. html : "Custom Nav Settings",
  502. attr : { id: "ghcn-menu" }
  503. });
  504.  
  505. el = $$(".header .dropdown-item[href='/settings/profile'], .header .dropdown-item[data-ga-click*='go to profile']");
  506. // get last found item - gists only have the "go to profile" item; GitHub has both
  507. el = el[el.length - 1];
  508. if (el) {
  509. // insert after
  510. el.parentNode.insertBefore(menu, el.nextSibling);
  511. on($("#ghcn-menu"), "click", function() {
  512. openPanel();
  513. });
  514. }
  515.  
  516. on(window, "hashchange", function() {
  517. openPanelOnHash();
  518. });
  519.  
  520. on($("#ghcn-overlay"), "click", function(event) {
  521. // ignore bubbled up events
  522. if (event.target.id === "ghcn-overlay") {
  523. closePanel();
  524. }
  525. });
  526. on($("body"), "keyup", function(event) {
  527. // using F2 key for testing
  528. if (editMode && event.keyCode === 27) {
  529. closePanel();
  530. }
  531. });
  532. on($("body"), "click", function(event) {
  533. if (editMode && event.target.classList.contains("header-nav-link")) {
  534. // header-nav-link is a child of header-nav-item, but is the same size
  535. settings.currentLink = event.target.parentNode.getAttribute("data-ghcn");
  536. selectItem();
  537. }
  538. });
  539. on($$(".ghcn-settings-wrapper input"), "input change", function() {
  540. saveLink();
  541. });
  542. on($(".ghcn-add"), "click", function() {
  543. createLink();
  544. });
  545. on($(".ghcn-destroy"), "click", function() {
  546. destroyLink(settings.currentLink);
  547. });
  548. on($(".ghcn-reset"), "click", function() {
  549. resetLinks();
  550. });
  551. // close panel when hotkey link is clicked or the page scrolls on the documentation wiki
  552. on($$(".ghcn-close, .ghcn-hotkey-link"), "click", function() {
  553. closePanel();
  554. });
  555.  
  556. // Code
  557. on($(".ghcn-code"), "click", function(){
  558. // open JSON code textarea
  559. $(".ghcn-json-code").classList.toggle("ghcn-visible");
  560. addJSON();
  561. });
  562. // close JSON code textarea
  563. on($(".ghcn-json-code"), "focus", function() {
  564. this.select();
  565. });
  566. on($(".ghcn-json-code"), "paste", function() {
  567. setTimeout(function() {
  568. checkJSON(processJSON());
  569. }, 200);
  570. });
  571.  
  572. }
  573.  
  574. function checkJSON(val, init) {
  575. let hasGitHub = false,
  576. hasGists = false,
  577. hasItems = false;
  578. if (val) {
  579. hasGitHub = val.hasOwnProperty("github");
  580. hasGists = val.hasOwnProperty("gists");
  581. hasItems = val.hasOwnProperty("items");
  582. // simple validation
  583. if (hasGitHub && hasGists && hasItems) {
  584. if (!init) {
  585. resetLinks(val);
  586. $(".ghcn-json-code").classList.remove("ghcn-visible");
  587. selectItem();
  588. }
  589. return true;
  590. }
  591. }
  592. let msg = [];
  593. if (!hasGitHub) { msg.push(`"github"`); }
  594. if (!hasGists) { msg.push(`"gists"`); }
  595. if (!hasItems) { msg.push(`"items"`); }
  596. msg = msg.length ? "JSON is missing " + msg.join(" & ") : "Invalid JSON";
  597. console.error("GitHub Custom Navigation: " + msg, val);
  598. return false;
  599. }
  600.  
  601. // add new link; needs a unique ID
  602. function findUniqueId(prefix) {
  603. let indx = 0,
  604. id = prefix + indx;
  605. if (settings.items[id]) {
  606. while (settings.items[id]) {
  607. id = prefix + indx++;
  608. }
  609. }
  610. return id;
  611. }
  612.  
  613. // Main process - adds links to header navigation
  614. function customize() {
  615. let nav = $(".header-nav");
  616. if (nav) {
  617. let indx, els,
  618. navStr = ".header-nav",
  619. setItems = settings[getLocation()],
  620. len = setItems.length;
  621. if (!len) { return; }
  622.  
  623. els = nav.childNodes;
  624. indx = els.length;
  625. while (indx--) {
  626. nav.removeChild(els[indx]);
  627. }
  628.  
  629. for (indx = 0; indx < len; indx++) {
  630. addToMenu(setItems[indx], navStr);
  631. }
  632. // make sure all svg's have an "octicon" class name
  633. addClass($$(navStr + " svg"), "octicon");
  634.  
  635. if (editMode) {
  636. updatePanel();
  637. } else {
  638. // narrow the search bar when there are a bunch of links added
  639. // add delay to allow browser to complete reflow
  640. setTimeout(function() {
  641. let width = nav.offsetWidth,
  642. // don't let search bar get narrower than 200px; 360 = starting width
  643. adjust = width > adjustWidth ?
  644. Math.round(Math.max(minSearchInputWidth, 360 - (width - adjustWidth))) + "px" : "";
  645. // default search width is 360px; we don't want to get narrower than 200px
  646. $(".header-search").style.width = adjust;
  647. }, 100);
  648.  
  649.  
  650. }
  651. }
  652. }
  653.  
  654. function $(selector, el) {
  655. return (el || document).querySelector(selector);
  656. }
  657. function $$(selector, el) {
  658. return Array.from((el || document).querySelectorAll(selector));
  659. }
  660. function addClass(els, name) {
  661. let indx = els.length;
  662. while (indx--) {
  663. els[indx].classList.add(name);
  664. }
  665. }
  666. function removeClass(els, name) {
  667. let indx = els.length;
  668. while (indx--) {
  669. els[indx].classList.remove(name);
  670. }
  671. }
  672. function on(els, name, callback) {
  673. els = Array.isArray(els) ? els : [els];
  674. let events = name.split(/\s+/);
  675. els.forEach(function(el) {
  676. events.forEach(function(ev) {
  677. el.addEventListener(ev, callback);
  678. });
  679. });
  680. }
  681. function make(obj) {
  682. let key,
  683. el = document.createElement(obj.el);
  684. if (obj.cl4ss) { el.className = obj.cl4ss; }
  685. if (obj.html) { el.innerHTML = obj.html; }
  686. if (obj.attr) {
  687. for (key in obj.attr) {
  688. if (obj.attr.hasOwnProperty(key)) {
  689. el.setAttribute(key, obj.attr[key]);
  690. }
  691. }
  692. }
  693. if (obj.appendTo) {
  694. $(obj.appendTo).appendChild(el);
  695. }
  696. return el;
  697. }
  698.  
  699. let isValid = checkJSON(settings, 'init');
  700. if (!isValid) {
  701. resetLinks();
  702. }
  703. customize();
  704. addPanel();
  705. addBindings();
  706. addDragula();
  707. openPanelOnHash();
  708.  
  709. })();