5a249cd50f592a3b7598110eec792cfc942fab3f
max
Wed Sep 9 08:52:06 2026 -0700
Address the v504 code review
hgSession: an anonymous share name that arrives with the request is saved only
when it is not already in the table. Every anonymous link sits under the one
reserved user "l", so a name already there stays as it is and the caller is told
so. The top-right Share dialog is unaffected, since it passes a name it has
just reserved and such a name does not exist yet. Snapshot names are now left
out of both My Sessions listings, which is what their "__" prefix has claimed
all along.
Share dialog: "Create link & copy" reports the copy instead of promising it.
copyToClipboard says whether the text reached the clipboard, the dialog passes
that on when a browser refuses, and it tries the asynchronous clipboard API
before giving up. The preview is built with the same encoding the server uses,
so a name holding a hyphen or a slash previews as the link that really gets
made. Cancelling out of the name editor no longer copies a second time, and a
reply with no link in it says so rather than showing "undefined".
hgBlat: a second click on the share button while the first request is still out
no longer mints a second snapshot session, and a box dismissed during the wait
stays closed.
Also: a snapshot moves a cart value into durable storage only when it is a
trash path, the way sessionData's own callers check; sqlAddressMatch keeps to
its own documented precondition when handed an empty address; alphaGenomeToWig
compares its output with its input rather than with itself, rejects a position
that is not all digits and skips an empty score; and hgc's default iframe width
reaches the browser as one percent sign.
refs #38294
diff --git src/hg/js/topLinks.js src/hg/js/topLinks.js
index b16377e50a7..312058fa024 100644
--- src/hg/js/topLinks.js
+++ src/hg/js/topLinks.js
@@ -1,577 +1,619 @@
// 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();
// Clicks on a link are the exception: the page may have a handler that needs to see
// them (hgHubConnect.js watches for its own links and switches tab instead of
// reloading). A link click ends the dialog anyway, so nothing is lost by letting an
// underlying popup notice it too.
if (!ev.target.closest("a"))
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 changeEmailUrl = link.getAttribute("data-changeemailurl");
var changeRecovEmailUrl = link.getAttribute("data-changerecovemailurl");
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
to a list
function addLink(ul, href, text) {
var li = el("li", {}, {margin: "6px 0"});
var a = el("a", {href: href, textContent: text});
// Most of these links leave the page, which takes the dialog with it. "My Track Hubs"
// does not when we are already on hgHubConnect: there hgHubConnect.js cancels the click
// and just switches a tab, so the dialog would stay up. Close it ourselves, deferred
// to the next tick so the click is fully dispatched first -- detaching the link while
// it is still being handled would rob hgHubConnect.js of the tab switch.
a.addEventListener("click", function() { setTimeout(closeModal, 0); });
li.appendChild(a);
ul.appendChild(li);
}
// "My Data" navigation: sessions, custom tracks, uploaded track hubs (hubSpace).
var hgsid = getHgsidSafe();
var navUl = el("ul", {}, {listStyle: "none", margin: "0 0 10px 0", padding: "0"});
addLink(navUl, "../cgi-bin/hgSession?hgS_doMainPage=1&hgsid=" + hgsid, "My Sessions");
addLink(navUl, "../cgi-bin/hgCustom?hgsid=" + hgsid, "My Custom Tracks");
addLink(navUl, "../cgi-bin/hgHubConnect?hgsid=" + hgsid + "#unlistedHubs", "My Track Hubs");
body.appendChild(navUl);
// Account actions.
var ul = el("ul", {}, {listStyle: "none", margin: "0", padding: "0",
borderTop: "1px solid #ddd", paddingTop: "8px"});
if (changePwUrl)
addLink(ul, changePwUrl, "Change password");
if (changeEmailUrl)
addLink(ul, changeEmailUrl, "Change email");
if (changeRecovEmailUrl)
addLink(ul, changeRecovEmailUrl, "Recovery email");
addLink(ul, logoutUrl, "Sign out");
body.appendChild(ul);
showModal("Account", body);
}
// ---- Share a link ------------------------------------------------------------------------
function getHgsidSafe() {
if (typeof getHgsid === "function")
return getHgsid();
var m = /[?&]hgsid=([^&]+)/.exec(window.location.search);
return m ? m[1] : "";
}
// Clipboard SVG icon (same FontAwesome glyph used by printCopyToClipboardButton in hgSession.c).
var clipboardSvg = " ";
// POST to the hgSession JSON endpoint. On a {url} or {exists} response call onResult(data); on
// {error} or transport failure show the message in statusEl and call onFail (if given) so the
// caller can re-enable its controls.
function postJson(params, statusEl, onResult, onFail) {
statusEl.style.color = "#000";
statusEl.textContent = "Working…";
$.ajax({
type: "POST",
url: "../cgi-bin/hgSession",
data: params,
dataType: "json",
success: function(data) {
if (data && (data.url || data.exists))
onResult(data);
else {
statusEl.style.color = "#a00";
statusEl.textContent = (data && data.error) ? data.error : "Could not create link.";
if (onFail) onFail();
}
},
error: function() {
statusEl.style.color = "#a00";
statusEl.textContent = "Could not reach the server. Please try again.";
if (onFail) onFail();
}
});
}
// Append the "how to manage this link" note (session mode only). Logged in: point to the
// My Sessions page. Logged out: invite the user to log in (via the top-right Login link).
function appendManageNote(body, loggedIn) {
var note = el("p", {}, {marginTop: "14px"});
if (loggedIn) {
note.appendChild(document.createTextNode("All of your saved links appear on your "));
note.appendChild(el("a", {href: "../cgi-bin/hgSession?hgS_doMainPage=1&hgsid=" +
getHgsidSafe(),
target: "_blank", textContent: "My Sessions"}));
note.appendChild(document.createTextNode(
" page, where you can rename, update, or remove them at any time."));
} else {
note.appendChild(document.createTextNode("You are not logged in. "));
var loginEl = document.getElementById("loginLink");
note.appendChild(el("a", {href: loginEl ? loginEl.getAttribute("href") : "../cgi-bin/hgLogin",
textContent: "Log in"}));
note.appendChild(document.createTextNode(
" to give your links a name and to edit or update them later."));
}
body.appendChild(note);
}
// Show the resulting share URL with a "Copy to clipboard" button. opts (session mode):
// {name: , session: true, loggedIn: }. A logged-in session link
// also gets a "Specify name" button. url mode passes no opts → just the link + Copy.
function showResult(body, url, opts) {
opts = opts || {};
+ // No link to show. postJson also routes a name-clash reply here, and only the create
+ // step knows what to do with one, so say so rather than printing "undefined" as the link.
+ if (!url) {
+ body.innerHTML = "";
+ body.appendChild(el("p", {textContent: "Could not create the link. Please try again."},
+ {marginTop: "0", color: "#a00"}));
+ return;
+ }
var canRename = opts.session && opts.loggedIn;
body.innerHTML = "";
// Saved sessions and plain page URLs never expire; an anonymous snapshot link (the BLAT
// alignment page's share, opts.snapshot) is durable while used but cleaned after years of
// no use, so only that case makes the softer promise and points at sessions for permanence.
var durability = opts.snapshot ?
"The link remains valid for years; to keep your results permanently, save them " +
"into a Session:" : "Links never time out:";
body.appendChild(el("p", {textContent: "You can share this link with collaborators, put " +
"it into figure legends or manuscripts. " + durability}, {marginTop: "0"}));
// Read-only text region (not an ) so it's clear the URL isn't meant to be edited.
var urlBox = el("div", {id: "tlShareUrl", textContent: url},
{background: "#f0f0f0", padding: "6px 8px", borderRadius: "4px",
wordBreak: "break-all", userSelect: "all", fontFamily: "monospace"});
urlBox.setAttribute("data-copy", url); // copyToClipboard() reads data-copy or innerText
body.appendChild(urlBox);
var btnRow = el("div", {}, {marginTop: "8px"});
var copyBtn = el("button", {title: "Copy URL to clipboard"});
copyBtn.setAttribute("data-target", "tlShareUrl");
copyBtn.innerHTML = clipboardSvg + "Copy to clipboard";
+ var copied = false;
copyBtn.addEventListener("click", function(ev) {
- if (typeof copyToClipboard === "function") copyToClipboard(ev);
+ if (typeof copyToClipboard === "function")
+ copied = copyToClipboard(ev);
});
btnRow.appendChild(copyBtn);
- // One-click "Create link & copy": copy right after the session is created. execCommand copy
- // still runs while the modal is focused; if a browser blocks it the URL box and Copy button
- // above are the manual fallback.
- if (opts.autoCopy)
- copyBtn.click();
-
if (canRename) {
var nameBtn = el("button", {textContent: "Specify name"}, {marginLeft: "8px"});
nameBtn.addEventListener("click", function() { showRename(body, url, opts); });
btnRow.appendChild(nameBtn);
}
body.appendChild(btnRow);
+ // One-click "Create link & copy": copy now that the button is on the page. The copy runs
+ // out of the reply to the save request rather than out of a click of the user's own, and a
+ // browser may refuse it on those grounds, so say which of the two happened instead of
+ // promising the clipboard either way. autoCopy is spent here: coming back to this view,
+ // e.g. by cancelling out of the name editor, must not copy a second time.
+ if (opts.autoCopy) {
+ opts.autoCopy = false;
+ copyBtn.click();
+ if (!copied) {
+ var note = el("p", {textContent: "Your browser did not allow the copy. Use the " +
+ "button above to copy the link."}, {marginTop: "8px", color: "#a00"});
+ body.appendChild(note);
+ // The asynchronous clipboard API does not need a click of the user's, so it can
+ // still get there in a browser that grants the permission. Only then is the
+ // warning wrong, so take it back.
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(url).then(function() {
+ copyBtn.innerHTML = "Copied";
+ note.remove();
+ }, function() { });
+ }
+ }
+ }
+
if (opts.session)
appendManageNote(body, opts.loggedIn);
else if (opts.pageNote)
body.appendChild(el("p", {textContent: "This link shows the page only. It does not " +
"restore the tracks you currently have displayed."}, {marginTop: "14px"}));
}
// The "Specify name" editor: rename the session, then show the updated link.
function showRename(body, currentUrl, opts) {
body.innerHTML = "";
body.appendChild(el("p", {textContent: "Name this link:"}, {marginTop: "0"}));
var nameInput = el("input", {type: "text", value: "", placeholder: "fig3b"},
{width: "100%", padding: "4px", margin: "6px 0", boxSizing: "border-box"});
body.appendChild(nameInput);
var status = el("div", {}, {margin: "6px 0"});
body.appendChild(status);
var row = el("div", {});
var saveBtn = el("button", {textContent: "Save"});
saveBtn.addEventListener("click", function() {
var newName = nameInput.value.trim();
if (!newName) {
status.style.color = "#a00";
status.textContent = "Please enter a name.";
return;
}
postJson({hgsid: getHgsidSafe(), hgS_doRenameSessionJson: 1,
hgS_oldSessionName: opts.name, hgS_newSessionName: newName}, status,
function(data) {
showResult(body, data.url, {name: data.name, session: true, loggedIn: true});
});
});
row.appendChild(saveBtn);
var cancelBtn = el("button", {textContent: "Cancel"}, {marginLeft: "8px"});
cancelBtn.addEventListener("click", function() { showResult(body, currentUrl, opts); });
row.appendChild(cancelBtn);
body.appendChild(row);
nameInput.focus();
}
// Return the current page URL with the hgsid query parameter removed (for hgTrackUi sharing).
function stripHgsid(url) {
try {
var u = new URL(url);
u.searchParams.delete("hgsid");
return u.toString();
} catch (e) {
return url.replace(/([?&])hgsid=[^&]*/, "$1").replace(/[?&]$/, "");
}
}
// Add name=val to url if it isn't already present (used to keep db= after stripping hgsid).
function ensureParam(url, name, val) {
try {
var u = new URL(url);
if (!u.searchParams.has(name))
u.searchParams.set(name, val);
return u.toString();
} catch (e) {
if (new RegExp("[?&]" + name + "=").test(url))
return url;
return url + (url.indexOf("?") >= 0 ? "&" : "?") + name + "=" + encodeURIComponent(val);
}
}
// Open a simple "here is the link" dialog for an arbitrary URL, with the hgsid stripped.
// Used by hgTrackUi (the current page) and by the hgc item-details popup in hgTracks.js.
// opts (optional): {ensureDb: to add db= if missing, pageNote: true to note it's page-only}.
function shareUrlDialog(url, opts) {
opts = opts || {};
var clean = stripHgsid(url);
if (opts.ensureDb)
clean = ensureParam(clean, "db", opts.ensureDb);
var body = document.createElement("div");
showModal("Share a link", body, 720);
showResult(body, clean, {pageNote: opts.pageNote, snapshot: opts.snapshot});
}
function showShareDialog(link) {
var mode = link.getAttribute("data-sharemode") || "session";
// hgTrackUi etc.: the shareable thing is just this page's URL without the hgsid. Note
// that it opens the page, not the user's session/tracks (like the hgc popup link).
if (mode === "url") {
shareUrlDialog(window.location.href, {pageNote: true});
return;
}
// Session mode (hgTracks): don't create the session yet. Opening the dialog and closing it
// should not litter the user's session list with unused links, so we only create the session
// when the user clicks the button (which then also copies the link in one step). We still
// show the final link right away as a preview, so the user knows what it will look like.
var opts = {
loggedIn: link.getAttribute("data-loggedin") === "1",
shortLink: link.getAttribute("data-shortlink") === "1",
userName: link.getAttribute("data-username") || ""
};
var body = document.createElement("div");
showModal("Share a link", body, 720);
if (opts.loggedIn) {
// Logged in: the share is saved under the user's account; we generate its default name
// client-side so the preview updates live as the user edits it.
showCreatePrompt(body, opts, "");
} else {
// Anonymous: every anonymous link's name is generated server-side (unique, crypto-strong),
// so reserve one first, then preview the exact link it will become.
var prep = el("p", {textContent: "Preparing link…"}, {marginTop: "0"});
body.appendChild(prep);
reserveAnonName(function(name) {
showCreatePrompt(body, opts, "", name);
}, function(msg) {
prep.style.color = "#a00";
prep.textContent = msg || "Could not reach the server. Please try again.";
});
}
}
// Ask the server to reserve (generate, guarantee-unique, not yet save) an anonymous snapshot
// name; call onName(name) with it, or onFail(msg) on error.
function reserveAnonName(onName, onFail) {
$.ajax({
type: "POST", url: "../cgi-bin/hgSession", dataType: "json",
data: {hgsid: getHgsidSafe(), hgS_doAnonName: 1},
success: function(data) {
if (data && data.name) onName(data.name);
else onFail(data && data.error);
},
error: function() { onFail(); }
});
}
// A string of n URL-safe alphanumeric characters. Used only for the logged-in default name;
// anonymous names come from the server (reserveAnonName).
function randChars(n) {
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var s = "";
for (var i = 0; i < n; i++)
s += chars.charAt(Math.floor(Math.random() * chars.length));
return s;
}
// The default session name for a logged-in share: a leading "_" (marking it machine-generated,
// kept verbatim by the short-link encoder) plus 8 chars, mirroring sessRandomShareName() in
// hgSession.js. Anonymous names are server-generated, not built here.
function shareName() {
return "_" + randChars(8);
}
+ // Encode a session name the way the server does, i.e. cgiEncodeFull() in lib/cheapcgi.c: a
+ // letter, a digit, "." and "_" stay, everything else becomes %XX. encodeURIComponent keeps
+ // seven more characters than that, "-" among them, so a name like "fig3-b" would be previewed
+ // as a link the server never makes.
+ function cgiEncodeFull(s) {
+ return encodeURIComponent(s).replace(/[-!~*'()]/g, function(c) {
+ return "%" + c.charCodeAt(0).toString(16).toUpperCase();
+ });
+ }
+
// Build the exact share URL for a given session name, matching addSessionLink() in hgSession.c:
// a short "/s//" link when hgSession.shortLink is on, otherwise the long hgTracks
// hgS_doOtherUser form. Logged out, the owner is the reserved anonymous user "l".
function shareUrlFor(opts, name) {
var user = opts.loggedIn ? opts.userName : "l";
+ var encUser = cgiEncodeFull(user);
+ var encName = cgiEncodeFull(name);
var origin = window.location.protocol + "//" + window.location.host;
- if (opts.shortLink)
- return origin + "/s/" + encodeURIComponent(user) + "/" + encodeURIComponent(name);
+ // A "/" in either name rules out the short form, since apache's redirect splits the path on
+ // it; addSessionLink() falls back to the long link in that case, so the preview must too.
+ if (opts.shortLink && encUser.indexOf("%2F") < 0 && encName.indexOf("%2F") < 0)
+ return origin + "/s/" + encUser + "/" + encName;
return origin + "/cgi-bin/hgTracks?hgS_doOtherUser=submit&hgS_otherUserName=" +
- encodeURIComponent(user) + "&hgS_otherUserSessionName=" + encodeURIComponent(name);
+ encUser + "&hgS_otherUserSessionName=" + encName;
}
// The initial session-mode view: preview the final link (not yet active), then a single button
// that creates the shared session and copies the link in one step. We generate the session name
// here and pass it to the server so the previewed link is exactly the one that gets created.
// Logged in, the user may type a custom name before creating; the preview updates as they type.
// initialName (optional) pre-fills the name field, e.g. when returning from the overwrite prompt.
// presetAutoName (optional) is the name to use when none is typed - for anonymous shares this is
// the unique name reserved from the server; logged in we generate a client-side "_XXXX" name.
function showCreatePrompt(body, opts, initialName, presetAutoName) {
body.innerHTML = "";
var autoName = presetAutoName || shareName();
body.appendChild(el("p", {textContent: "This link points to your current view and never " +
- "times out. It becomes active — and is copied to your clipboard — when you click the " +
- "button below."}, {marginTop: "0"}));
+ "times out. It becomes active when you click the button below, which also copies it " +
+ "to your clipboard."}, {marginTop: "0"}));
// Optional custom name (logged in only): leave it blank to use the generated name above.
// A short, space-free name keeps the link tidy (it becomes part of the URL).
var nameInput = null;
if (opts.loggedIn) {
body.appendChild(el("label", {textContent: "Name this link (optional):"},
{display: "block", margin: "10px 0 4px", fontWeight: "bold"}));
nameInput = el("input", {type: "text", placeholder: "fig3b", value: initialName || ""},
{width: "100%", padding: "4px", boxSizing: "border-box"});
body.appendChild(nameInput);
}
// Preview of the link, styled muted to signal it is not active yet; updated live below.
var urlBox = el("div", {}, {background: "#f0f0f0", padding: "6px 8px", borderRadius: "4px",
wordBreak: "break-all", fontFamily: "monospace", color: "#666", margin: "8px 0"});
body.appendChild(urlBox);
function typedName() { return nameInput ? nameInput.value.trim() : ""; }
function chosenName() { return typedName() || autoName; }
function refreshPreview() { urlBox.textContent = shareUrlFor(opts, chosenName()); }
refreshPreview();
if (nameInput)
nameInput.addEventListener("input", refreshPreview);
var status = el("div", {}, {margin: "6px 0"});
var createBtn = el("button", {title: "Create the shareable link and copy it to your clipboard"},
{marginTop: "8px"});
createBtn.innerHTML = clipboardSvg + "Create link & copy";
var busy = false;
// Create the session (and copy its link). allowOverwrite skips the "name already exists"
// guard, used after the user confirms they want to replace their existing link.
function submitCreate(allowOverwrite) {
if (busy) return;
busy = true;
createBtn.disabled = true;
var name = chosenName();
var params = {hgsid: getHgsidSafe(), hgS_doSaveSessionJson: 1, hgS_newSessionName: name};
if (!opts.loggedIn)
params.hgS_shareAnon = 1; // anonymous token link; no naming
else if (typedName() && !allowOverwrite)
params.hgS_failIfExists = 1; // warn before clobbering a same-named session
postJson(params, status, function(data) {
busy = false;
if (data.exists) {
showOverwriteConfirm(name);
return;
}
showResult(body, data.url, {name: data.name, session: true, loggedIn: opts.loggedIn,
autoCopy: true});
}, function() { busy = false; createBtn.disabled = false; });
}
createBtn.addEventListener("click", function() { submitCreate(false); });
// The typed name is already taken: ask before replacing it. Cancel returns to this prompt
// with the name kept; Replace re-submits allowing the overwrite.
function showOverwriteConfirm(name) {
body.innerHTML = "";
body.appendChild(el("p", {textContent: 'You already have a link named "' + name + '". ' +
"Replace it so this name points to your current view?"}, {marginTop: "0"}));
var cstatus = el("div", {}, {margin: "6px 0"});
var replaceBtn = el("button", {textContent: "Replace and copy"});
replaceBtn.addEventListener("click", function() {
replaceBtn.disabled = true;
var params = {hgsid: getHgsidSafe(), hgS_doSaveSessionJson: 1, hgS_newSessionName: name};
postJson(params, cstatus, function(data) {
showResult(body, data.url, {name: data.name, session: true,
loggedIn: opts.loggedIn, autoCopy: true});
}, function() { replaceBtn.disabled = false; });
});
var cancelBtn = el("button", {textContent: "Cancel"}, {marginLeft: "8px"});
cancelBtn.addEventListener("click", function() { showCreatePrompt(body, opts, name); });
var crow = el("div", {});
crow.appendChild(replaceBtn);
crow.appendChild(cancelBtn);
body.appendChild(crow);
body.appendChild(cstatus);
}
var row = el("div", {});
row.appendChild(createBtn);
body.appendChild(row);
body.appendChild(status);
appendManageNote(body, opts.loggedIn);
}
// ---- Wire up the menu items --------------------------------------------------------------
function init() {
var login = document.getElementById("loginLink");
// Only intercept the click when logged in (data-username present); otherwise it is a
// plain link to hgSession and should navigate normally.
if (login && login.getAttribute("data-username")) {
login.addEventListener("click", function(ev) {
ev.preventDefault();
showLoginDialog(login);
});
}
var share = document.getElementById("shareLink");
if (share) {
share.addEventListener("click", function(ev) {
ev.preventDefault();
showShareDialog(share);
});
}
// Narrow-screen hamburger: toggle the dropdown of top-right links (CSS shows it only on
// narrow viewports). Close on an outside click or after a link inside is chosen.
var toggle = document.getElementById("trToggle");
var container = document.getElementById("topRightLinks");
if (toggle && container) {
// Nothing to show, e.g. a mirror with no login system: leave the icon out entirely
// rather than offer a menu that opens empty.
if (container.getElementsByClassName("topRightLink").length === 0)
toggle.style.display = "none";
toggle.addEventListener("click", function(ev) {
ev.preventDefault();
ev.stopPropagation();
var open = container.classList.toggle("trOpen");
toggle.setAttribute("aria-expanded", open ? "true" : "false");
});
container.addEventListener("click", function(ev) {
if (ev.target.tagName === "A") {
container.classList.remove("trOpen");
toggle.setAttribute("aria-expanded", "false");
}
});
document.addEventListener("click", function() {
container.classList.remove("trOpen");
toggle.setAttribute("aria-expanded", "false");
});
}
}
if (document.readyState === "loading")
document.addEventListener("DOMContentLoaded", init);
else
init();
return {showLoginDialog: showLoginDialog, showShareDialog: showShareDialog,
shareUrl: shareUrlDialog};
})();