GitHub Custom Hotkeys

A userscript that allows you to add custom GitHub keyboard hotkeys

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

  1. // ==UserScript==
  2. // @name GitHub Custom Hotkeys
  3. // @version 1.1.5
  4. // @description A userscript that allows you to add custom GitHub keyboard hotkeys
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @match https://github.com/*
  9. // @match https://*.github.com/*
  10. // @run-at document-idle
  11. // @grant GM_addStyle
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @require https://greatest.deepsurf.us/scripts/398877-utils-js/code/utilsjs.js?version=1079637
  15. // @require https://greatest.deepsurf.us/scripts/28721-mutations/code/mutations.js?version=1108163
  16. // @icon https://github.githubassets.com/pinned-octocat.svg
  17. // @supportURL https://github.com/Mottie/GitHub-userscripts/issues
  18. // ==/UserScript==
  19.  
  20. /* global $ $$ on */
  21. (() => {
  22. "use strict";
  23. /* "g p" here overrides the GitHub default "g p" which takes you to the Pull Requests page
  24. {
  25. "all": [
  26. { "f1" : "#hotkey-settings" },
  27. { "g g": "{repo}/graphs/code-frequency" },
  28. { "g p": "{repo}/pulse" },
  29. { "g u": [ "{user}", true ] },
  30. { "g s": "{upstream}" }
  31. ],
  32. "{repo}/issues": [
  33. { "g right": "{issue+1}" },
  34. { "g left" : "{issue-1}" }
  35. ],
  36. "{root}/search": [
  37. { "g right": "{page+1}" },
  38. { "g left" : "{page-1}" }
  39. ]
  40. }
  41. */
  42. let data = GM_getValue("github-hotkeys", {
  43. all: [{
  44. f1: "#hotkey-settings"
  45. }]
  46. });
  47. let lastHref = window.location.href;
  48.  
  49. const openHash = "#hotkey-settings";
  50.  
  51. const templates = {
  52. remove: `<svg class="octicon" fill="currentColor" 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"/></svg>`,
  53. hotkey: `
  54. <label class="tooltipped tooltipped-n" aria-label="hotkey"><input type="text" class="ghch-hotkey form-control"></label>
  55. <label class="tooltipped tooltipped-n" aria-label="URL"><input type="text" class="ghch-url form-control"></label>
  56. <label class="tooltipped tooltipped-w" aria-label="Open in a new tab?"><input type="checkbox" class="ghch-new-tab"></label>`,
  57. scope: "<ul><li><button class='ghch-hotkey-add'>+ Click to add a new hotkey</button></li></ul>"
  58. };
  59.  
  60. // https://github.com/{nonUser}
  61. // see https://github.com/Mottie/github-reserved-names
  62. const nonUser = new RegExp("^(" + [
  63. /* BUILD:RESERVED-NAMES-START (v2.0.4) */
  64. "400", "401", "402", "403", "404", "405", "406", "407", "408", "409",
  65. "410", "411", "412", "413", "414", "415", "416", "417", "418", "419",
  66. "420", "421", "422", "423", "424", "425", "426", "427", "428", "429",
  67. "430", "431", "500", "501", "502", "503", "504", "505", "506", "507",
  68. "508", "509", "510", "511", "about", "access", "account", "admin",
  69. "advisories", "anonymous", "any", "api", "apps", "attributes", "auth",
  70. "billing", "blob", "blog", "bounty", "branches", "business", "businesses",
  71. "c", "cache", "case-studies", "categories", "central", "certification",
  72. "changelog", "cla", "cloud", "codereview", "collection", "collections",
  73. "comments", "commit", "commits", "community", "companies", "compare",
  74. "contact", "contributing", "cookbook", "coupons", "customer-stories",
  75. "customer", "customers", "dashboard", "dashboards", "design", "develop",
  76. "developer", "diff", "discover", "discussions", "docs", "downloads",
  77. "downtime", "editor", "editors", "edu", "enterprise", "events", "explore",
  78. "featured", "features", "files", "fixtures", "forked", "garage", "ghost",
  79. "gist", "gists", "graphs", "guide", "guides", "help", "help-wanted",
  80. "home", "hooks", "hosting", "hovercards", "identity", "images", "inbox",
  81. "individual", "info", "integration", "interfaces", "introduction",
  82. "invalid-email-address", "investors", "issues", "jobs", "join", "journal",
  83. "journals", "lab", "labs", "languages", "launch", "layouts", "learn",
  84. "legal", "library", "linux", "listings", "lists", "login", "logos",
  85. "logout", "mac", "maintenance", "malware", "man", "marketplace", "mention",
  86. "mentioned", "mentioning", "mentions", "migrating", "milestones", "mine",
  87. "mirrors", "mobile", "navigation", "network", "new", "news", "none",
  88. "nonprofit", "nonprofits", "notices", "notifications", "oauth", "offer",
  89. "open-source", "organisations", "organizations", "orgs", "pages",
  90. "partners", "payments", "personal", "plans", "plugins", "popular",
  91. "popularity", "posts", "press", "pricing", "professional", "projects",
  92. "pulls", "raw", "readme", "recommendations", "redeem", "releases",
  93. "render", "reply", "repositories", "resources", "restore", "revert",
  94. "save-net-neutrality", "saved", "scraping", "search", "security",
  95. "services", "sessions", "settings", "shareholders", "shop", "showcases",
  96. "signin", "signup", "site", "spam", "sponsors", "ssh", "staff", "starred",
  97. "stars", "static", "status", "statuses", "storage", "store", "stories",
  98. "styleguide", "subscriptions", "suggest", "suggestion", "suggestions",
  99. "support", "suspended", "talks", "teach", "teacher", "teachers",
  100. "teaching", "team", "teams", "ten", "terms", "timeline", "topic", "topics",
  101. "tos", "tour", "train", "training", "translations", "tree", "trending",
  102. "updates", "username", "users", "visualization", "w", "watching", "wiki",
  103. "windows", "works-with", "www0", "www1", "www2", "www3", "www4", "www5",
  104. "www6", "www7", "www8", "www9"
  105. /* BUILD:RESERVED-NAMES-END */
  106. ].join("|") + ")$");
  107.  
  108. function getUrlParts() {
  109. const loc = window.location;
  110. const root = "https://github.com";
  111. const parts = {
  112. root,
  113. origin: loc.origin,
  114. page: ""
  115. };
  116. // me
  117. let tmp = $("meta[name='user-login']");
  118. parts.m = tmp && tmp.getAttribute("content") || "";
  119. parts.me = parts.m ? parts.root + "/" + parts.m : "";
  120.  
  121. // pathname "should" always start with a "/"
  122. tmp = loc.pathname.split("/");
  123.  
  124. // user name
  125. if (nonUser.test(tmp[1] || "")) {
  126. // invalid user! clear out the values
  127. tmp = [];
  128. }
  129. parts.u = tmp[1] || "";
  130. parts.user = tmp[1] ? root + "/" + tmp[1] : "";
  131. // repo name
  132. parts.r = tmp[2] || "";
  133. parts.repo = tmp[1] && tmp[2] ? parts.user + "/" + tmp[2] : "";
  134. // tab?
  135. parts.t = tmp[3] || "";
  136. parts.tab = tmp[3] ? parts.repo + "/" + tmp[3] : "";
  137. if (parts.t === "issues" || parts.t === "pulls") {
  138. // issue number
  139. parts.issue = tmp[4] || "";
  140. }
  141. // branch/tag?
  142. if (parts.t === "tree" || parts.t === "blob") {
  143. parts.branch = tmp[4] || "";
  144. } else if (parts.t === "releases" && tmp[4] === "tag") {
  145. parts.branch = tmp[5] || "";
  146. }
  147. // commit hash?
  148. if (parts.t === "commit") {
  149. parts.commit = tmp[4] || "";
  150. }
  151. // forked from
  152. tmp = $(".repohead .fork-flag a");
  153. parts.upstream = tmp ? tmp.getAttribute("href") : "";
  154. // current page
  155. tmp = loc.search.match(/[&?]p(?:age)?=(\d+)/);
  156. parts.page = tmp ? tmp[1] || "1" : "";
  157. return parts;
  158. }
  159.  
  160. // pass true to initialize; false to remove everything
  161. function checkScope() {
  162. removeElms($("body"), ".ghch-link");
  163. const parts = getUrlParts();
  164. Object.keys(data).forEach(key => {
  165. const url = fixUrl(parts, key === "all" ? "{root}" : key);
  166. if (window.location.href.indexOf(url) > -1) {
  167. debug("Checking custom hotkeys for " + key);
  168. addHotkeys(parts, url, data[key]);
  169. }
  170. });
  171. }
  172.  
  173. function fixUrl(parts, url = "") {
  174. let valid = true; // use true in case a full URL is used
  175. url = url
  176. // allow {issues+#} to go inc or desc
  177. .replace(/\{issue([\-+]\d+)?\}/, (s, n) => {
  178. const val = n ? parseInt(parts.issue || "", 10) + parseInt(n, 10) : "";
  179. valid = val !== "" && val > 0;
  180. return valid ? parts.tab + "/" + val : "";
  181. })
  182. // allow {page+#} to change results page
  183. .replace(/\{page([\-+]\d+)?\}/, (s, n) => {
  184. const loc = window.location,
  185. val = n ? parseInt(parts.page || "", 10) + parseInt(n, 10) : "";
  186. let search = "";
  187. valid = val !== "" && val > 0;
  188. if (valid) {
  189. search = loc.origin + loc.pathname;
  190. if (loc.search.match(/[&?]p?=\d+/)) {
  191. search += loc.search.replace(/([&?]p=)\d+/, (s, n) => {
  192. return n + val;
  193. });
  194. } else {
  195. // started on page 1 (no &p=1) available to replace
  196. search += loc.search + "&p=" + val;
  197. }
  198. }
  199. return valid ? search : "";
  200. })
  201. // replace placeholders
  202. .replace(/\{\w+\}/gi, matches => {
  203. const val = parts[matches.replace(/[{}]/g, "")] || "";
  204. valid = val !== "";
  205. return val;
  206. });
  207. return valid ? url : "";
  208. }
  209.  
  210. function removeElms(src, selector) {
  211. const links = $$(selector, src);
  212. let len = links.length;
  213. while (len-- > 0) {
  214. src.removeChild(links[len]);
  215. }
  216. }
  217.  
  218. function addHotkeys(parts, scope, hotkeys) {
  219. // Shhh, don't tell anyone, but GitHub checks the data-hotkey attribute
  220. // of any link on the page, so we only need to add dummy links :P
  221. let indx, url, key, entry, link, isArray;
  222. const len = hotkeys.length;
  223. const body = $("body");
  224. for (indx = 0; indx < len; indx++) {
  225. key = Object.keys(hotkeys[indx])[0];
  226. entry = hotkeys[indx][key];
  227. isArray = Array.isArray(entry);
  228. url = fixUrl(parts, isArray ? entry[0] : entry);
  229. if (url) {
  230. link = document.createElement("a");
  231. link.className = "ghch-link";
  232. link.href = url;
  233. if (isArray) {
  234. link.target = "_blank";
  235. }
  236. link.setAttribute("data-hotkey", key);
  237. body.appendChild(link);
  238. debug(`Adding "${key}" keyboard hotkey linked to "${url}"`);
  239. }
  240. }
  241. }
  242.  
  243. function addHotkey(el) {
  244. const li = document.createElement("li");
  245. li.className = "ghch-hotkey-set";
  246. li.innerHTML = `
  247. <div class="ghch-hotkey-wrap">
  248. ${templates.hotkey}
  249. <button class="ghch-remove">${templates.remove}</button>
  250. </div>`;
  251. el.parentElement.before(li);
  252. return li;
  253. }
  254.  
  255. function addScope(el) {
  256. const scope = document.createElement("fieldset");
  257. scope.className = "ghch-scope-custom";
  258. scope.innerHTML = `
  259. <legend>
  260. <span class="simple-box" contenteditable>Enter Scope</span>&nbsp;
  261. <button class="ghch-remove">${templates.remove}</button>
  262. </legend>
  263. ${templates.scope}
  264. `;
  265. el.parentNode.insertBefore(scope, el);
  266. return scope;
  267. }
  268.  
  269. function addMenu() {
  270. GM_addStyle(`
  271. #ghch-open-menu { cursor:pointer; }
  272. #ghch-menu { position:fixed; z-index:65535; top:0; bottom:0; left:0; right:0; opacity:0; display:none; }
  273. #ghch-menu.ghch-open { opacity:1; display:block; background:rgba(0,0,0,.5); }
  274. #ghch-settings-inner { position:fixed; left:50%; top:50%; transform:translate(-50%,-50%); width:25rem; box-shadow:0 .5rem 1rem #111; }
  275. #ghch-settings-inner h3 .btn { float:right; font-size:.8em; padding:0 6px 2px 6px; margin-left:3px; }
  276. .ghch-remove { background:transparent; border:0; white-space:initial; margin-bottom:6px; }
  277. .ghch-remove svg, #ghch-settings-inner .ghch-close svg { vertical-align:middle; pointer-events:none; }
  278. .ghch-menu-inner li .ghch-remove { margin-left:0; padding:0; }
  279. .ghch-menu-inner li .ghch-remove:hover, .ghch-menu-inner legend .ghch-remove:hover { color:#800; }
  280. .ghch-menu-inner { max-height:60vh; overflow-y:auto; }
  281. .ghch-menu-inner ul { list-style:none; }
  282. .ghch-hotkey-wrap, .ghch-hotkey-add { width:100%; display:flex; align-items:center; justify-content:space-evenly; white-space:pre; margin-bottom:4px; }
  283. .ghch-scope-all, .ghch-scope-add, .ghch-scope-custom { width:100%; border:2px solid rgba(85,85,85,0.5); border-radius:4px; padding:10px; margin:0; }
  284. .ghch-scope-add, .ghch-hotkey-add { background:transparent; border:2px dashed #555; border-radius:4px; opacity:0.6; text-align:center; cursor:pointer; margin-top:10px; }
  285. .ghch-scope-add:hover, .ghch-hotkey-add:hover { opacity:1; }
  286. .ghch-menu-inner legend span { padding:0 6px; min-width:30px; border:0; }
  287. .ghch-hotkey { width:80px; }
  288. .ghch-json-code { display:none; font-family:Menlo, Inconsolata, "Droid Mono", monospace; font-size:1em; }
  289. .ghch-json-code.ghch-open { position:absolute; top:37px; bottom:0; left:2px; right:2px; z-index:0; width:396px; max-width:396px; max-height:calc(100% - 37px); display:block; }
  290. .ghch-menu-inner textarea { resize:none; }
  291. `);
  292.  
  293. // add menu
  294. const menu = document.createElement("div");
  295. menu.id = "ghch-menu";
  296. menu.innerHTML = `
  297. <div id="ghch-settings-inner" class="boxed-group">
  298. <h3>
  299. GitHub Custom Hotkey Settings
  300. <button type="button" class="btn btn-sm ghch-close tooltipped tooltipped-n" aria-label="Close">
  301. ${templates.remove}
  302. </button>
  303. <button type="button" class="ghch-code btn btn-sm tooltipped tooltipped-n" aria-label="Toggle JSON data view">{ }</button>
  304. <a href="https://github.com/Mottie/GitHub-userscripts/wiki/GitHub-custom-hotkeys" class="ghch-help btn btn-sm tooltipped tooltipped-n" aria-label="Get Help">?</a>
  305. </h3>
  306. <div class="ghch-menu-inner boxed-group-inner">
  307. <fieldset class="ghch-scope-all">
  308. <legend>
  309. <span class="simple-box" data-scope="all">All of GitHub &amp; subdomains</span>
  310. </legend>
  311. ${templates.scope}
  312. </fieldset>
  313. <button class="ghch-scope-add">+ Click to add a new scope</button>
  314. <textarea class="ghch-json-code form-control"></textarea>
  315. </div>
  316. </div>
  317. `;
  318. $("body").appendChild(menu);
  319. addBindings();
  320. }
  321.  
  322. function openPanel() {
  323. updateMenu();
  324. $("#ghch-menu").classList.add("ghch-open");
  325. return false;
  326. }
  327.  
  328. function closePanel() {
  329. const menu = $("#ghch-menu");
  330. if (menu?.classList.contains("ghch-open")) {
  331. // update data in case a "change" event didn't fire
  332. refreshData();
  333. checkScope();
  334. menu.classList.remove("ghch-open");
  335. $(".ghch-json-code", menu).classList.remove("ghch-open");
  336. window.location.hash = "";
  337. return false;
  338. }
  339. }
  340.  
  341. function addJSON() {
  342. const textarea = $(".ghch-json-code");
  343. textarea.value = JSON
  344. .stringify(data, null, 2)
  345. // compress JSON a little
  346. .replace(/\n\s{4}\}/g, " }")
  347. .replace(/\{\n\s{6}/g, "{ ")
  348. .replace(/\[\s{9}/g, "[ ")
  349. .replace(/\,\s{9}/g, ", ")
  350. .replace(/\s{7}\]/g, " ]");
  351. }
  352.  
  353. function processJSON() {
  354. let val;
  355. const textarea = $(".ghch-json-code");
  356. try {
  357. val = JSON.parse(textarea.value);
  358. data = val;
  359. } catch (err) {}
  360. }
  361.  
  362. function updateMenu() {
  363. const menu = $(".ghch-menu-inner");
  364. if (menu) {
  365. removeElms(menu, ".ghch-scope-custom");
  366. removeElms($(".ghch-scope-all ul", menu), ".ghch-hotkey-set");
  367. let scope, selector;
  368. // Add scopes
  369. Object.keys(data).forEach(key => {
  370. if (key === "all") {
  371. selector = "all";
  372. scope = $(".ghch-scope-all .ghch-hotkey-add", menu);
  373. } else if (key !== selector) {
  374. selector = key;
  375. scope = addScope($(".ghch-scope-add"));
  376. $("legend span", scope).innerHTML = key;
  377. scope = $(".ghch-hotkey-add", scope);
  378. }
  379. // add hotkey entries
  380. // eslint-disable-next-line no-loop-func
  381. data[key].forEach(val => {
  382. const target = addHotkey(scope);
  383. const tmp = Object.keys(val)[0];
  384. const entry = val[tmp];
  385. $(".ghch-hotkey", target).value = tmp;
  386. if (Array.isArray(entry)) {
  387. $(".ghch-url", target).value = entry[0];
  388. $(".ghch-new-tab", target).checked = entry[1]
  389. } else {
  390. $(".ghch-url", target).value = entry;
  391. }
  392. });
  393. });
  394. }
  395. }
  396.  
  397. function refreshData() {
  398. data = {};
  399. let tmp, scope, sIndx, hotkeys, scIndx, scLen, val;
  400. const menu = $(".ghch-menu-inner");
  401. const scopes = $$("fieldset", menu);
  402. const sLen = scopes.length;
  403. for (sIndx = 0; sIndx < sLen; sIndx++) {
  404. tmp = $("legend span", scopes[sIndx]);
  405. if (tmp) {
  406. scope = tmp.getAttribute("data-scope") || tmp.textContent.trim();
  407. hotkeys = $$(".ghch-hotkey-set", scopes[sIndx]);
  408. scLen = hotkeys.length;
  409. data[scope] = [];
  410. for (scIndx = 0; scIndx < scLen; scIndx++) {
  411. tmp = $$("input", hotkeys[scIndx]);
  412. val = (tmp[0] && tmp[0].value) || "";
  413. if (val) {
  414. data[scope][scIndx] = {};
  415. if (tmp[2].checked) {
  416. data[scope][scIndx][val] = [tmp[1].value || "", true];
  417. } else {
  418. data[scope][scIndx][val] = tmp[1].value || "";
  419. }
  420. }
  421. }
  422. }
  423. }
  424. GM_setValue("github-hotkeys", data);
  425. debug("Data refreshed", data);
  426. }
  427.  
  428. function addDropdownLink() {
  429. if (!$("#ghch-open-menu")) {
  430. // Create our menu entry
  431. const menu = document.createElement("a");
  432. menu.id = "ghch-open-menu";
  433. menu.role = "menuitem";
  434. menu.className = "dropdown-item";
  435. menu.innerHTML = "GitHub Hotkey Settings";
  436. menu.onclick = openPanel;
  437.  
  438. const els = $$(".Header-item .dropdown-item[href='/settings/profile']");
  439. if (els.length) {
  440. els[els.length - 1].after(menu);
  441. }
  442. }
  443. }
  444.  
  445. function addBindings() {
  446. let tmp;
  447. const menu = $("#ghch-menu");
  448. if (!menu) {
  449. return;
  450. }
  451.  
  452. // close menu
  453. on(menu, "click", closePanel);
  454. on($("body"), "keydown", event => {
  455. if (event.which === 27) {
  456. closePanel();
  457. }
  458. });
  459. // stop propagation
  460. on($("#ghch-settings-inner", menu), "keydown", event => {
  461. event.stopPropagation();
  462. });
  463. on($("#ghch-settings-inner", menu), "click", event => {
  464. event.stopPropagation();
  465. let target = event.target;
  466. // add hotkey
  467. if (target.classList.contains("ghch-hotkey-add")) {
  468. addHotkey(target);
  469. } else if (target.classList.contains("ghch-scope-add")) {
  470. addScope(target);
  471. }
  472. // svg & path nodeName may be lowercase
  473. tmp = target.nodeName.toLowerCase();
  474. if (tmp === "path") {
  475. target = target.parentNode;
  476. }
  477. // target should now point at svg
  478. if (target.classList.contains("ghch-remove")) {
  479. tmp = target.parentNode;
  480. // remove fieldset
  481. if (tmp.nodeName === "LEGEND") {
  482. tmp = tmp.parentNode;
  483. }
  484. // remove li; but not the button in the header
  485. if (tmp.nodeName !== "BUTTON") {
  486. tmp.parentNode.removeChild(tmp);
  487. refreshData();
  488. }
  489. }
  490. });
  491. on(menu, "change", refreshData);
  492. // contenteditable scope title
  493. on(menu, "input", event => {
  494. if (event.target.classList.contains("simple-box")) {
  495. refreshData();
  496. }
  497. });
  498. on($("button.ghch-close", menu), "click", closePanel);
  499. // open JSON code textarea
  500. on($(".ghch-code", menu), "click", () => {
  501. $(".ghch-json-code", menu).classList.toggle("ghch-open");
  502. addJSON();
  503. });
  504. // close JSON code textarea
  505. tmp = $(".ghch-json-code", menu);
  506. on(tmp, "focus", function () {
  507. this.select();
  508. });
  509. on(tmp, "paste", () => {
  510. setTimeout(() => {
  511. processJSON();
  512. updateMenu();
  513. $(".ghch-json-code").classList.remove("ghch-open");
  514. }, 200);
  515. });
  516.  
  517. // This is crazy! But window.location.search changes do not fire the
  518. // "popstate" or "hashchange" event, so we're stuck with a setInterval
  519. setInterval(() => {
  520. const loc = window.location;
  521. if (lastHref !== loc.href) {
  522. lastHref = loc.href;
  523. checkScope();
  524. // open panel via hash
  525. if (loc.hash === openHash) {
  526. openPanel();
  527. }
  528. }
  529. }, 1000);
  530. }
  531.  
  532. // include a "debug" anywhere in the browser URL search parameter to enable
  533. // debugging
  534. function debug() {
  535. if (/debug/.test(window.location.search)) {
  536. console.log.apply(console, arguments);
  537. }
  538. }
  539.  
  540. on(document, "ghmo:menu", () => {
  541. // user menu needs to call an API now
  542. addDropdownLink();
  543. });
  544.  
  545. // initialize
  546. checkScope();
  547. addMenu();
  548. })();