9f8d33c8b2b6bc61f6d02d781c4e02836f7099f9
max
Fri Aug 21 02:05:42 2026 -0700
hgSession: new opt-in JavaScript "My Sessions" page; share gbModern.css with hgBlat. refs #38157
Applies the hgBlat facelift strategy (#37996) to hgSession: an opt-in,
client-rendered "My Sessions" page gated by the sessionNewPage /
sessionNewPageBanner hg.conf flags (mirroring blatNewForm / blatNewFormBanner),
with a banner linking between the classic and new pages so neither is a one-way
door. sessionNewPage also flips the site default.
hgSession.c stays the data/action backend: it emits the session list and page
config as an inline JSON global (hgSessionData) into an empty #sessionApp
container, and the new hgSession.js builds the UI - a save-current-view card
(name + optional description + "only I can load it"; empty name saves under a
random share_ name), a "most recently saved session" one-click Update, a
searchable/sortable/paged DataTable of sessions (assembly + position, created
with last-used on hover, views, a lock icon on private sessions), inline Share
(copy link / email / gallery), Edit (rename + description + private), Overwrite
and Delete, and a bulk Select -> Delete-all-selected mode. The mutating actions
POST to new JSON endpoints (hgS_doDeleteJson / doShareJson / doGalleryJson /
doOverwriteJson / doDescribeJson) that run the same SQL as the classic full-page
handlers and return JSON, so the table updates in place; loads, file up/downloads
and custom-track backup stay as ordinary form submits/links. The Advanced panel
keeps feature parity with the classic page (load another user's session, load
from URL/file, save to file, back up custom tracks, reset), minus the login/
change-password links that now live in the top menu.
Shared UCSC house-style components (design tokens, .gbPill, .gbCard, .gbStrip,
.gbSection, .gbShareBox, .gbBanner, the .gbModal* dialog and a .gbTable) are
factored into a new gbModern.css. hgBlat is migrated onto it: its generic
.blat* classes are renamed to the shared .gb* names in hgBlat.css / hgBlat.js
and the #blatResults / #blatFormBox containers get class="gbApp"; verified
pixel-clean against the previous search form and results pages, including the
rename modal. hgSession.css holds only session-specific layout.
diff --git src/hg/js/hgSession.js src/hg/js/hgSession.js
new file mode 100644
index 00000000000..1229d32ab16
--- /dev/null
+++ src/hg/js/hgSession.js
@@ -0,0 +1,792 @@
+// hgSession.js - the experimental client-rendered "My Sessions" page.
+//
+// An opt-in modern alternative to the classic server-rendered hgSession page, applying hgBlat's
+// facelift strategy (#37996): hgSession.c emits the session list and page config as an inline JSON
+// global (hgSessionData) into an empty #sessionApp container, and this file builds the UI - a
+// save-current-view card, a searchable/sortable DataTable of saved sessions with inline
+// Overwrite/Share/Edit/Delete, and an "Advanced" panel for loading and backup.
+//
+// The inline table actions POST to small JSON endpoints in hgSession.c (hgS_doDeleteJson, etc.) and
+// update the table in place. Navigation actions (load a session, load from URL/file, save to file,
+// reset) are ordinary form submits/links against the existing hgSession actions.
+//
+// Styling: shared house-style components in gbModern.css (.gbPill, .gbCard, .gbModal*, .gbTable,
+// .gbBanner, .gbSection), session-specific layout in hgSession.css.
+
+/* global $, hgSessionData, convertTitleTagsToMouseovers, htmlEncode, commify */
+
+// Cart action variables (must match the hgs* defines in hgSession.h; hgSessionPrefix is "hgS_").
+var SESS_ACT = {
+ save: 'hgS_doSaveSessionJson',
+ rename: 'hgS_doRenameSessionJson',
+ del: 'hgS_doDeleteJson',
+ share: 'hgS_doShareJson',
+ gallery: 'hgS_doGalleryJson',
+ overwrite: 'hgS_doOverwriteJson',
+ describe: 'hgS_doDescribeJson'
+};
+var SESS_P = {
+ oldName: 'hgS_oldSessionName',
+ newName: 'hgS_newSessionName',
+ share: 'hgS_newSessionShare',
+ descr: 'hgS_newSessionDescription',
+ shareAnon:'hgS_shareAnon'
+};
+
+var sessData = null; // set in sessionBuild: {config, sessions}
+var sessDt = null; // the DataTable API
+var sessSelectMode = false; // bulk-select (checkbox column) shown?
+
+function sessEnc(s) {
+ // HTML-escape via the shared utils.js helper (escapes quotes too, so it is attribute-safe).
+ return (typeof htmlEncode === 'function') ? htmlEncode(String(s == null ? '' : s)) : String(s);
+}
+
+function sessNum(n) {
+ return (typeof commify === 'function') ? commify(n) : String(n);
+}
+
+function sessRandomShareName() {
+ // Mirror the server's auto/anonymous share-name convention ("share_" + 8 URL-safe alphanumeric
+ // chars). hgSession.c's doSaveSessionJson generates the same style server-side for the top-right
+ // "Share a link"; we generate it here so the confirm dialog can show the name before saving.
+ var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ var s = '';
+ for (var i = 0; i < 8; i++) {
+ s += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return 'share_' + s;
+}
+
+function sessCommifyPos(pos) {
+ // Add thousands separators to each number in a position string ("chr7:155799529-155812871" ->
+ // "chr7:155,799,529-155,812,871") using utils.js commify.
+ if (typeof commify !== 'function') { return String(pos); }
+ return String(pos).replace(/\d+/g, function(n) { return commify(n); });
+}
+
+function sessMsg(text, cls) {
+ // Show a transient status line at the top of the app (cls: 'ok' | 'err' | '').
+ var el = document.getElementById('sessMsg');
+ if (!el) { return; }
+ el.className = 'sessMsg' + (cls ? ' ' + cls : '');
+ el.innerHTML = text ? sessEnc(text) : '';
+}
+
+// ---- AJAX ----------------------------------------------------------------
+// Post an action to hgSession and get JSON back. Always carry the session id so the CGI loads the
+// right cart. onOk(resp) is called for {success:true,...} or {name,url}; onErr(msg) for {error}.
+
+function sessAjax(params, onOk, onErr) {
+ var data = $.extend({}, params);
+ data[sessData.config.cartVar] = sessData.config.hgsid;
+ $.ajax({
+ type: 'POST',
+ url: 'hgSession',
+ data: data,
+ dataType: 'json',
+ cache: false,
+ success: function(resp) {
+ if (resp && resp.error) {
+ if (onErr) { onErr(resp.error); } else { sessMsg(resp.error, 'err'); }
+ } else {
+ if (onOk) { onOk(resp); }
+ }
+ },
+ error: function() {
+ var m = 'Sorry, that action could not be completed. Please try again.';
+ if (onErr) { onErr(m); } else { sessMsg(m, 'err'); }
+ }
+ });
+}
+
+// ---- modal (generic) -----------------------------------------------------
+// One reusable overlay lives inside #sessionApp (so the gbModern tokens inherit into it). Open on
+// backdrop click and Esc close, matching hgBlat's modal behavior.
+
+function sessModalEnsure() {
+ if (document.getElementById('sessModalBg')) { return; }
+ var bg = document.createElement('div');
+ bg.id = 'sessModalBg';
+ bg.className = 'gbModalBg';
+ bg.style.display = 'none';
+ bg.innerHTML = '
';
+ document.getElementById('sessionApp').appendChild(bg);
+ $(bg).on('click', function(ev) { if (ev.target === this) { sessModalClose(); } });
+ $(document).on('keydown.sessModal', function(ev) {
+ var b = document.getElementById('sessModalBg');
+ if (b && b.style.display !== 'none' && ev.key === 'Escape') { sessModalClose(); }
+ });
+}
+
+function sessModalOpen(html) {
+ sessModalEnsure();
+ document.getElementById('sessModal').innerHTML = html;
+ document.getElementById('sessModalBg').style.display = 'flex';
+}
+
+function sessModalClose() {
+ var bg = document.getElementById('sessModalBg');
+ if (bg) { bg.style.display = 'none'; }
+}
+
+// A confirmation dialog with Cancel + a primary/danger OK. opts: {title, bodyHtml, okLabel,
+// okClass, onOk}. bodyHtml is caller-built safe HTML. The OK button is focused on open so the
+// user can confirm with just the Enter key.
+function sessConfirm(opts) {
+ var okClass = opts.okClass || 'primary';
+ sessModalOpen(
+ '
' + sessEnc(opts.title) + '
' +
+ '
' + opts.bodyHtml + '
' +
+ '
' +
+ '' +
+ '
');
+ $('#sessCfCancel').on('click', sessModalClose);
+ $('#sessCfOk').on('click', function() { opts.onOk(); });
+ document.getElementById('sessCfOk').focus();
+}
+
+// ---- session lookup / row helpers ---------------------------------------
+
+function sessByEnc(enc) {
+ var list = sessData.sessions;
+ for (var i = 0; i < list.length; i++) {
+ if (list[i].encName === enc) { return list[i]; }
+ }
+ return null;
+}
+
+function sessRowByEnc(enc) {
+ // Return the DataTables row API for the session with this encName, or null.
+ var found = null;
+ sessDt.rows().every(function() {
+ if (this.data().encName === enc) { found = this; }
+ });
+ return found;
+}
+
+// ---- table cell rendering ------------------------------------------------
+
+// Inline icons (Font Awesome solid paths, embedded as SVG so they do not depend on the site's Font
+// Awesome version). fill:currentColor picks up the button's text/danger color.
+var SESS_TRASH_SVG = '';
+// Font Awesome "floppy-disk" (save) solid path.
+var SESS_SAVE_SVG = '';
+// Font Awesome "lock" solid path - marks a private (not-shared) session, since sharing is the default.
+var SESS_LOCK_SVG = '';
+
+function sessActionsHtml(row) {
+ // Order: Share, Edit, then the icon-only Overwrite (floppy) and Delete (trash).
+ var e = sessEnc(row.encName);
+ return '' +
+ '' +
+ '' +
+ '';
+}
+
+function sessNameCellHtml(row) {
+ var html = '' + sessEnc(row.name) + '';
+ if (row.description) {
+ html += ' ⓘ';
+ }
+ // Sessions are shared by default; mark only the exceptions: a lock for private, a badge for the
+ // public gallery. A plain shared-by-link session gets no marker.
+ if (row.shared === 0) {
+ html += ' ' +
+ SESS_LOCK_SVG + '';
+ } else if (row.shared >= 2) {
+ html += ' Public';
+ }
+ return html;
+}
+
+// ---- Overwrite -----------------------------------------------------------
+
+function sessDoOverwrite(row) {
+ sessAjax(sessActParams(SESS_ACT.overwrite, row), function(resp) {
+ row.useCount = resp.useCount;
+ if (resp.created) { row.created = resp.created; }
+ if (resp.db) { row.db = resp.db; }
+ var r = sessRowByEnc(row.encName);
+ if (r) { r.data(row).draw(false); }
+ sessModalClose();
+ sessMsg('Overwrote “' + row.name + '” with your current browser view.', 'ok');
+ });
+}
+
+function sessOpenOverwrite(row) {
+ sessConfirm({
+ title: 'Overwrite session',
+ bodyHtml: 'Replace the saved session ' + sessEnc(row.name) + ' with the view you are ' +
+ 'looking at now? The name and description stay the same; the previously saved view is lost.',
+ okLabel: 'Overwrite',
+ onOk: function() { sessDoOverwrite(row); }
+ });
+}
+
+// ---- Delete --------------------------------------------------------------
+
+function sessOpenDelete(row) {
+ sessConfirm({
+ title: 'Delete session',
+ bodyHtml: 'Delete the session ' + sessEnc(row.name) + '? This cannot be undone. ' +
+ 'Any shared links to it will stop working.',
+ okLabel: 'Delete',
+ okClass: 'danger',
+ onOk: function() {
+ sessAjax(sessActParams(SESS_ACT.del, row), function() {
+ var r = sessRowByEnc(row.encName);
+ if (r) { r.remove().draw(false); }
+ var list = sessData.sessions;
+ for (var i = 0; i < list.length; i++) {
+ if (list[i].encName === row.encName) { list.splice(i, 1); break; }
+ }
+ sessModalClose();
+ sessMsg('Deleted “' + row.name + '”.', 'ok');
+ });
+ }
+ });
+}
+
+// ---- Edit (rename + description) ----------------------------------------
+
+function sessOpenEdit(row) {
+ var html = '
Edit session
' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '
';
+ sessModalOpen(html);
+ document.getElementById('sessEditName').value = row.name;
+ document.getElementById('sessEditDesc').value = row.description || '';
+ document.getElementById('sessEditPrivate').checked = (row.shared === 0);
+ document.getElementById('sessEditName').focus();
+ $('#sessEditCancel').on('click', sessModalClose);
+ $('#sessEditOk').on('click', function() { sessSaveEdit(row); });
+}
+
+function sessSaveEdit(row) {
+ var newName = document.getElementById('sessEditName').value.trim();
+ var newDesc = document.getElementById('sessEditDesc').value;
+ if (!newName) {
+ var err = document.getElementById('sessEditErr');
+ err.style.display = 'block';
+ err.innerHTML = sessEnc('Please enter a name.');
+ return;
+ }
+ var descChanged = (newDesc !== (row.description || ''));
+ var nameChanged = (newName !== row.name);
+ var wantPrivate = document.getElementById('sessEditPrivate').checked;
+ var privChanged = (wantPrivate !== (row.shared === 0));
+
+ // Order matters: apply description and privacy while the session still has its OLD name, then
+ // rename last (renaming changes the DB key the other endpoints look up by).
+ function finish() {
+ if (nameChanged) {
+ // encName changes on rename; reload for authoritative state.
+ window.location.reload();
+ } else {
+ var r = sessRowByEnc(row.encName);
+ if (r) { r.data(row).draw(false); }
+ sessModalClose();
+ sessMsg('Saved changes to “' + row.name + '”.', 'ok');
+ }
+ }
+ function doRename() {
+ if (nameChanged) {
+ var rp = sessActParams(SESS_ACT.rename, row);
+ rp[SESS_P.newName] = newName;
+ sessAjax(rp, finish, function(m) { sessEditError(m); });
+ } else { finish(); }
+ }
+ function doPriv() {
+ if (privChanged) {
+ var sp = sessActParams(SESS_ACT.share, row);
+ sp[SESS_P.share] = wantPrivate ? 0 : 1;
+ sessAjax(sp, function(resp) { row.shared = resp.shared; doRename(); },
+ function(m) { sessEditError(m); });
+ } else { doRename(); }
+ }
+ function doDesc() {
+ if (descChanged) {
+ var dp = sessActParams(SESS_ACT.describe, row);
+ dp[SESS_P.descr] = newDesc;
+ sessAjax(dp, function() { row.description = newDesc; doPriv(); },
+ function(m) { sessEditError(m); });
+ } else { doPriv(); }
+ }
+ doDesc();
+}
+
+function sessEditError(msg) {
+ var err = document.getElementById('sessEditErr');
+ if (err) { err.style.display = 'block'; err.innerHTML = sessEnc(msg); }
+ else { sessMsg(msg, 'err'); }
+}
+
+// ---- Share (copy link + sharing level) ----------------------------------
+
+function sessOpenShare(row) {
+ var mailBody = encodeURIComponent('Here is a UCSC Genome Browser session I would like to ' +
+ 'share with you: ') + encodeURIComponent(row.shareUrl);
+ var mailto = 'mailto:?subject=' + encodeURIComponent('UCSC Genome Browser session ' + row.name) +
+ '&body=' + mailBody;
+ var html = '
';
+}
+
+// ---- build the whole page -----------------------------------------------
+
+function sessAccountHtml(C) {
+ // The signed-in account line (Signed in as X · Sign out · Change password) now lives in the
+ // top-right menu bar, so it is intentionally not rendered here. Kept commented out so QA can
+ // add it back if wanted:
+ /*
+ if (C.loggedIn) {
+ var s = 'Signed in as ' + sessEnc(C.userName) + '';
+ if (C.logoutUrl) { s += ' · Sign out'; }
+ if (C.changePasswordUrl) {
+ s += ' · Change password';
+ }
+ return s;
+ }
+ */
+ if (!C.loggedIn && C.loginAvail && C.loginUrl) {
+ return 'You are not signed in. Sign in to save and manage named sessions.';
+ }
+ return '';
+}
+
+function sessSaveCardHtml(C) {
+ if (!C.loggedIn) { return ''; }
+ // Assembly and position separated by a colon (e.g. "hg38: chr7:1-1,000"); assembly is already
+ // the accession for hubs (trackHubSkipHubName on the server).
+ var loc = '';
+ if (C.db && C.position) { loc = sessEnc(C.db) + ': ' + sessEnc(C.position); }
+ else if (C.db) { loc = sessEnc(C.db); }
+ else if (C.position) { loc = sessEnc(C.position); }
+ if (loc && C.trackCount) {
+ loc += ', ' + sessNum(C.trackCount) + ' track' + (C.trackCount === 1 ? '' : 's') + ' shown';
+ }
+ var what = loc ? '' + loc + '' : '';
+ return '
' +
+ '
' +
+ 'Save the current view as a stable session link' + what + '
' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '
';
+}
+
+function sessRecentHtml(recent) {
+ // A quick shortcut to re-save the session the user most recently saved, keeping its name and
+ // description. Hidden when there are no saved sessions.
+ if (!recent) { return ''; }
+ return '
';
+}
+
+function sessionBuild() {
+ sessData = hgSessionData;
+ var C = sessData.config;
+ var app = $('#sessionApp');
+
+ var intro = '
' + sessAccountHtml(C) +
+ (sessAccountHtml(C) ? ' ' : '') +
+ 'A session is a stable link to a Genome Browser view that you can save, load later, share ' +
+ 'or copy into a manuscript. See the ' +
+ 'Sessions User’s Guide and the ' +
+ 'Session Gallery.
';
+
+ // The session most recently saved/overwritten (max lastUse), for the one-click "Update now".
+ var recent = null;
+ (sessData.sessions || []).forEach(function(s) {
+ if (!recent || s.lastUseEpoch > recent.lastUseEpoch) { recent = s; }
+ });
+
+ app.html(
+ intro +
+ '' +
+ sessRecentHtml(recent) +
+ sessSaveCardHtml(C) +
+ sessAdvancedHtml(C) +
+ sessTableHtml(C)
+ );
+
+ // Save current view. Enter in either the name or the description field saves.
+ $('#sessSaveBtn').on('click', sessDoSave);
+ $('#sessSaveName, #sessSaveDesc').on('keydown', function(ev) {
+ if (ev.key === 'Enter') { ev.preventDefault(); sessDoSave(); }
+ });
+
+ // One-click update of the most recently saved session.
+ if (recent) {
+ $('#sessUpdateNow').on('click', function() {
+ sessConfirm({
+ title: 'Update session',
+ bodyHtml: 'Overwrite ' + sessEnc(recent.name) + ' with the view you are ' +
+ 'looking at now? The session name and description stay the same.',
+ okLabel: 'Update now',
+ onOk: function() { sessDoOverwrite(recent); }
+ });
+ });
+ }
+
+ // Advanced toggle.
+ $('#sessAdvHead').on('click', function() {
+ var body = document.getElementById('sessAdvBody');
+ var open = body.style.display !== 'none';
+ body.style.display = open ? 'none' : 'grid';
+ $(this).find('.caret').html(open ? '▸' : '▾');
+ });
+
+ // Session table.
+ if (C.loggedIn) { sessBuildTable(); }
+
+ if (typeof convertTitleTagsToMouseovers === 'function') { convertTitleTagsToMouseovers(); }
+}
+
+// ---- bulk select / delete ------------------------------------------------
+
+function sessToggleSelect() {
+ if (sessSelectMode) { sessDeleteSelected(); } else { sessSetSelectMode(true); }
+}
+
+function sessSetSelectMode(on) {
+ sessSelectMode = on;
+ sessDt.column(0).visible(on); // the leading checkbox column
+ var b = document.getElementById('sessSelectBtn');
+ if (on) {
+ b.textContent = 'Delete all selected';
+ b.classList.add('primary');
+ } else {
+ b.textContent = 'Select';
+ b.classList.remove('primary');
+ $('#sessionAppTable .sessSelChk, #sessSelAll').prop('checked', false);
+ }
+}
+
+function sessDeleteSelected() {
+ var encs = [];
+ $('#sessionAppTable tbody .sessSelChk:checked').each(function() {
+ encs.push(this.getAttribute('data-enc'));
+ });
+ if (encs.length === 0) { sessSetSelectMode(false); return; } // nothing picked: just exit
+ sessConfirm({
+ title: 'Delete selected sessions',
+ bodyHtml: 'Delete ' + encs.length + ' selected session' +
+ (encs.length === 1 ? '' : 's') + '? This cannot be undone.',
+ okLabel: 'Delete ' + encs.length,
+ okClass: 'danger',
+ onOk: function() {
+ sessModalClose();
+ var remaining = encs.length;
+ function done() { if (--remaining === 0) { window.location.reload(); } }
+ encs.forEach(function(enc) {
+ var row = sessByEnc(enc);
+ if (!row) { done(); return; }
+ sessAjax(sessActParams(SESS_ACT.del, row), done, done);
+ });
+ }
+ });
+}
+
+function sessBuildTable() {
+ sessDt = $('#sessionAppTable').DataTable({
+ data: sessData.sessions,
+ pageLength: 15,
+ lengthChange: true,
+ lengthMenu: [[15, 50, 100, -1], [15, 50, 100, 'All']],
+ order: [[3, 'desc']], // newest (Created) first by default
+ dom: '<"sessToolbar"fl>rt<"sessFoot"ip>',
+ language: { searchPlaceholder: 'Search sessions', search: '', lengthMenu: 'Show _MENU_',
+ emptyTable: 'No saved sessions yet.' },
+ columns: [
+ { title: '',
+ className: 'sessSelCol', data: null, orderable: false, searchable: false, visible: false,
+ render: function(d, type, row) {
+ return ''; } },
+ { title: 'Session', className: 'sessNameCol', data: 'name',
+ render: function(d, type, row) {
+ return (type === 'display') ? sessNameCellHtml(row) : row.name; } },
+ { title: 'Assembly', data: 'db',
+ render: function(d, type, row) {
+ if (type !== 'display') { return row.db || ''; }
+ var html = sessEnc(row.db || 'n/a');
+ if (row.position) {
+ html += ' ' + sessEnc(sessCommifyPos(row.position)) +
+ '';
+ }
+ return html; } },
+ { title: 'Created', data: 'createdEpoch',
+ render: function(d, type, row) {
+ if (type !== 'display') { return row.createdEpoch; }
+ return '' +
+ sessEnc(row.created) + ''; } },
+ { title: 'Views', data: 'useCount',
+ render: function(d, type) { return (type === 'display') ? sessNum(d) : d; } },
+ { title: 'Actions', className: 'sessActionsCol', data: null, orderable: false,
+ searchable: false,
+ render: function(d, type, row) { return (type === 'display') ? sessActionsHtml(row) : ''; } }
+ ]
+ });
+
+ // Delegate the row action buttons.
+ $('#sessionAppTable tbody').on('click', 'button[data-act]', function() {
+ var act = this.getAttribute('data-act');
+ var row = sessByEnc(this.getAttribute('data-enc'));
+ if (!row) { return; }
+ if (act === 'overwrite') { sessOpenOverwrite(row); }
+ else if (act === 'share') { sessOpenShare(row); }
+ else if (act === 'edit') { sessOpenEdit(row); }
+ else if (act === 'delete') { sessOpenDelete(row); }
+ });
+
+ // Bulk-select control: a "Select" button after the length dropdown reveals a checkbox column and
+ // becomes a primary "Delete all selected" button.
+ var selBtn = document.createElement('button');
+ selBtn.type = 'button';
+ selBtn.id = 'sessSelectBtn';
+ selBtn.className = 'gbPill';
+ selBtn.textContent = 'Select';
+ selBtn.title = 'Select multiple sessions to delete them at once';
+ document.querySelector('#sessionApp .sessToolbar').appendChild(selBtn);
+ $(selBtn).on('click', sessToggleSelect);
+ // Header "select all" toggles every checkbox on the current page.
+ $('#sessionAppTable').on('change', '#sessSelAll', function() {
+ $('#sessionAppTable tbody .sessSelChk').prop('checked', this.checked);
+ });
+
+ // Column-header tooltips (set title, then let utils.js convert to styled mouseovers).
+ var tips = {
+ 'Session': 'Click a name to load that session in the Genome Browser',
+ 'Assembly': 'The genome assembly this session was saved on',
+ 'Created': 'When the session was first saved',
+ 'Views': 'How many times this session has been loaded',
+ 'Actions': 'Overwrite with your current view, share, edit, or delete'
+ };
+ $('#sessionAppTable thead th').each(function() {
+ var t = tips[$(this).text().trim()];
+ if (t) { $(this).attr('title', t); }
+ });
+}
+
+$(document).ready(function() {
+ if (typeof hgSessionData !== 'undefined' && document.getElementById('sessionApp')) {
+ sessionBuild();
+ }
+});