c5a326f3cc42beca0b59c284a2819763548c166a
max
Wed Jul 8 05:31:13 2026 -0700
Add top-right "Share a link" and "Login" links to the menu bar, refs #10138
New js/topLinks.js drives two links at the top-right of the menu bar on all pages:
- Login: links to the login page when logged out; when logged in it shows the
username and opens an account dialog (My Sessions / My Custom Tracks /
My Track Hubs, change password, sign out).
- Share a link: on hgTracks, one click saves the current view as a session and
shows a copyable short link, with an optional "Specify name" rename step; works
logged-in or anonymously (reserved user "l"). On hgTrackUi and the hgc/hgGene
item-details popup it instead shares the page URL with the hgsid stripped and
the db argument kept.
hgSession gains two JSON endpoints (hgS_doSaveSessionJson, hgS_doRenameSessionJson)
that reuse saveCartAsSession() and addSessionLink(). The menu bar is patched in
lib/web.c (CGI pages) and hgMenubar.c (static pages) via comment placeholders in
globalNavBar.inc. On narrow/phone screens the links collapse into a menu icon with
a dropdown so they no longer overlap the menu.
diff --git src/hg/js/topLinks.js src/hg/js/topLinks.js
new file mode 100644
index 00000000000..62cca2e458f
--- /dev/null
+++ src/hg/js/topLinks.js
@@ -0,0 +1,383 @@
+// topLinks.js - behavior for the two top-right menu-bar links, "Login" and "Share a link".
+//
+// Login: when the user is logged in, the menu item shows their username and opens a small
+// account dialog (change password, sign out). When logged out it is a plain link to hgSession,
+// so this script does nothing for it.
+//
+// Share a link (browser pages only): saves the current view as a session and shows a copyable
+// short link. Logged-in users get to name the session and choose whether others may open it;
+// logged-out users get an anonymous link created on the fly.
+//
+// The C code that builds the menu bar (lib/web.c menuBar() for CGI pages, hgMenubar.c for static
+// pages) fills in the data-* attributes that this script reads. The dialog is a self-contained
+// overlay so it works on static pages too (no jQuery UI dependency).
+
+/* jshint esversion: 8 */
+/* global $, document, window, URL, getHgsid, copyToClipboard */
+
+var topLinks = (function() {
+ "use strict";
+
+ // Small helper to build an element with properties and inline styles set from the DOM
+ // (assigning to element.style.* is CSP-safe, unlike a style="" attribute in markup).
+ function el(tag, props, styles) {
+ var e = document.createElement(tag);
+ var k;
+ if (props)
+ for (k in props)
+ if (props.hasOwnProperty(k)) e[k] = props[k];
+ if (styles)
+ for (k in styles)
+ if (styles.hasOwnProperty(k)) e.style[k] = styles[k];
+ return e;
+ }
+
+ var closeCurrent = null; // closer for the currently open modal, if any
+
+ function closeModal() {
+ if (closeCurrent) { closeCurrent(); closeCurrent = null; }
+ }
+
+ // Show a centered modal with the given title and body node. Returns the body element so the
+ // caller can replace its contents later (e.g. swap a spinner for the resulting link).
+ function showModal(titleText, bodyNode, widthPx) {
+ closeModal();
+ var overlay = el("div", {}, {
+ position: "fixed", top: "0", left: "0", right: "0", bottom: "0",
+ background: "rgba(0,0,0,0.4)", zIndex: "10000"
+ });
+ var box = el("div", {}, {
+ position: "fixed", top: "90px", left: "50%", transform: "translateX(-50%)",
+ background: "#fff", color: "#000", border: "1px solid #888", borderRadius: "4px",
+ boxShadow: "0 2px 12px rgba(0,0,0,0.4)", width: (widthPx || 430) + "px", maxWidth: "92%",
+ zIndex: "10001", fontSize: "14px"
+ });
+ var titleBar = el("div", {}, {
+ background: "#00457c", color: "#fff", padding: "8px 12px", fontWeight: "bold",
+ borderTopLeftRadius: "4px", borderTopRightRadius: "4px", position: "relative"
+ });
+ titleBar.appendChild(document.createTextNode(titleText));
+ var x = el("span", {title: "Close", textContent: "×"}, {
+ position: "absolute", right: "10px", top: "5px", cursor: "pointer",
+ fontSize: "18px", lineHeight: "18px"
+ });
+ x.addEventListener("click", closeModal);
+ titleBar.appendChild(x);
+ var body = el("div", {}, {padding: "14px"});
+ if (bodyNode) body.appendChild(bodyNode);
+ box.appendChild(titleBar);
+ box.appendChild(body);
+ overlay.appendChild(box);
+ document.body.appendChild(overlay);
+
+ // Swallow Escape and clicks so they don't also reach an underlying dialog's own
+ // close-on-escape / click-outside handlers (e.g. the hgc item-details popup).
+ function onKey(ev) {
+ if (ev.key === "Escape") {
+ ev.preventDefault();
+ ev.stopImmediatePropagation();
+ closeModal();
+ }
+ }
+ overlay.addEventListener("click", function(ev) {
+ if (ev.target === overlay)
+ closeModal();
+ ev.stopPropagation();
+ });
+ document.addEventListener("keydown", onKey, true); // capture: run before other handlers
+ closeCurrent = function() {
+ document.removeEventListener("keydown", onKey, true);
+ if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
+ };
+ return body;
+ }
+
+ // ---- Login / account dialog --------------------------------------------------------------
+
+ function showLoginDialog(link) {
+ var user = link.getAttribute("data-username");
+ var logoutUrl = link.getAttribute("data-logouturl");
+ var changePwUrl = link.getAttribute("data-changepwurl");
+ var body = document.createElement("div");
+ var p = el("p");
+ p.appendChild(document.createTextNode("Signed in as "));
+ p.appendChild(el("b", {textContent: user})); // textContent avoids HTML injection
+ p.appendChild(document.createTextNode("."));
+ body.appendChild(p);
+
+ // helper: add an