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/hgBlat.js src/hg/js/hgBlat.js
index d3f66718551..aae33b07a69 100644
--- src/hg/js/hgBlat.js
+++ src/hg/js/hgBlat.js
@@ -77,77 +77,77 @@
var c = blatIdColor(hit.identity);
return `${hit.identity.toFixed(1)}%`;
}
function blatCoverageCell(hit) {
var left = (hit.qStart - 1) / hit.qSize * 100;
var width = (hit.qEnd - hit.qStart + 1) / hit.qSize * 100;
var tip = `Query matches the genome at ${blatFmt(hit.qStart)}-${blatFmt(hit.qEnd)}bp out of ${blatFmt(hit.qSize)}bp`;
return ``;
}
// ---- summary strip + detail panel ---------------------------------------
function blatSummaryStrip(cfg, queryCount) {
- var stat = (k, v) => `
${k}` +
+ var stat = (k, v) => `
${k}` +
`${v}
`;
- var div = '';
+ var div = '';
var assembly = stat('Assembly', htmlEncode(cfg.organism) + ' / ' + htmlEncode(cfg.db)) + div +
stat('Matches', blatFmt(cfg.hitCount));
var stats;
if (cfg.multiQuery) {
// With more than one query sequence a single query name/length would be wrong, so show the
// number of distinct queries; each hit's own query is in the table's Query column.
stats = stat('Queries', blatFmt(queryCount)) + div + assembly;
} else {
stats = stat('Query', htmlEncode(cfg.queryName)) + div +
stat('Length', blatFmt(cfg.querySize) + ' bp') + div + assembly;
}
var actions = '';
// "View all in browser" is the primary action, so it comes first.
if (cfg.viewAllUrl) {
- actions += `View all in browser`;
+ actions += `View all in browser`;
}
// "Show Query Sequence" opens the query FASTA in a panel (with Download / Copy). Only on a fresh
// search, where the uploaded sequence is available (cfg.querySeqs emitted by hgBlat.c).
if (cfg.querySeqs && cfg.querySeqs.length) {
- actions += '';
}
// "Share a link" just reveals the page's stable URL (cfg.shareUrl, a trash-backed reopen link).
// cfg.canShare covers old session-based links (?u=&s=), where the current URL is already shareable.
if (cfg.shareUrl || cfg.canShare) {
// A small share-nodes icon precedes the label so users learn to associate it with sharing.
var shareIcon = '';
- actions += '';
}
// "Rename BLAT Track" opens a modal to rename the results custom track. This is a JS-native
// button (renders immediately with the strip) that replaces the old C-emitted inline form, which
// only appeared after the buildBigPsl AJAX finished and reflowed the page when clicked.
if (cfg.canRename) {
- actions += '';
}
- return `
${stats}${actions}
`;
+ return `
${stats}${actions}
`;
}
var BLAT_TILE_TIPS = {
'Score': 'BLAT score: matches minus mismatches and gap penalties. Higher is better.',
'Identity': 'Percent identity of the aligned bases.',
'Matches': 'Query bases that match the genome.',
'Mismatch': 'Bases that differ between query and genome.',
'Gaps': 'Number of gaps (insertions or deletions) in the alignment.',
'Blocks': 'Number of ungapped aligned blocks.',
'Strand': 'Genome strand the query matched (+ or -).',
'Q span': 'Range of the query sequence that aligned (1-based).'
};
function blatTileSkeleton(label, id, color) {
var style = color ? ` style="color:${color}"` : '';
@@ -162,36 +162,36 @@
var tiles =
blatTileSkeleton('Score', 'dvScore') +
blatTileSkeleton('Identity', 'dvIdentity') +
blatTileSkeleton('Matches', 'dvMatches') +
blatTileSkeleton('Mismatch', 'dvMismatch') +
blatTileSkeleton('Gaps', 'dvGaps') +
blatTileSkeleton('Blocks', 'dvBlocks') +
blatTileSkeleton('Strand', 'dvStrand') +
blatTileSkeleton('Q span', 'dvQspan');
document.getElementById('blatDetail').innerHTML =
`
`;
if (typeof convertTitleTagsToMouseovers === 'function') { convertTitleTagsToMouseovers(); }
}
function blatSet(id, prop, val) {
var e = document.getElementById(id);
if (!e) { return; }
if (prop === 'text') { e.textContent = val; }
else if (prop === 'href') { e.setAttribute('href', val); }
else if (prop === 'color') { e.style.color = val; }
}
function blatRenderDetail(hit) {
if (!hit || !document.getElementById('blatDetail')) { return; }
@@ -258,106 +258,106 @@
var tip = BLAT_HEADER_TIPS[$(this).text().trim()];
if (tip) { $(this).attr('title', tip); }
});
if (typeof convertTitleTagsToMouseovers === 'function') {
convertTitleTagsToMouseovers();
}
}
// ---- share a link --------------------------------------------------------
function blatShareLink() {
// No session, no AJAX: the results page already has a stable, shareable URL (hgBlat.c emits it as
// cfg.shareUrl and blatBuild() pins it into the address bar with history.replaceState), so this
// just shows/copies window.location. The link reopens straight from the trash .pslx/.fa, so it
// works until those trash files are cleaned - hence the retention note.
- var box = document.getElementById('blatShareBox');
+ var box = document.getElementById('gbShareBox');
if (!box) { return; }
if (box.style.display === 'flex') { box.style.display = 'none'; return; } // toggle off
var url = window.location.href;
box.style.display = 'flex';
box.innerHTML =
- 'Shareable link — anyone with it can reopen ' +
+ 'Shareable link — anyone with it can reopen ' +
'these results. The results are stored temporarily, so the link works for at least 48 hours ' +
'after they were last viewed.' +
- '' +
- '';
- var inp = document.getElementById('blatShareInput');
+ '' +
+ '';
+ var inp = document.getElementById('gbShareInput');
inp.value = url;
inp.focus();
inp.select();
$('#blatShareCopy').on('click', function() {
inp.select();
if (navigator.clipboard) { navigator.clipboard.writeText(url); }
else { document.execCommand('copy'); }
this.textContent = 'Copied';
});
}
// ---- Rename BLAT track (modal) -------------------------------------------
// The results custom track is built (and renamed) by hgBlat.c's inline code, which exposes a small
// window.blatRenameCt(name, description) helper (it POSTs to hgc's buildBigPsl and rebuilds the
// track). We reuse that helper (no new endpoint), just swapping its old inline toggle-form UI for a
// proper modal dialog. The current name/description come from cfg (hgBlat.c), not a global, so this
// does not depend on any generic page-global.
function blatRenameModalHtml(cfg) {
// hgSession link is relative (same /cgi-bin/), carrying db + hgsid so the session page opens in
// this assembly and cart.
var sessionUrl = `hgSession?db=${encodeURIComponent(cfg.db)}&hgsid=${encodeURIComponent(cfg.hgsid)}`;
- return '
' +
- '
' +
- '
Rename BLAT Track
' +
- '
Every BLAT result is stored in its own track in the Genome ' +
+ return '
' +
+ '
' +
+ '
Rename BLAT Track
' +
+ '
Every BLAT result is stored in its own track in the Genome ' +
'Browser. You can rename the track here. Results will disappear after 2–3 days, unless ' +
`they are saved into a Session link.
` +
- '' +
- '' +
- '' +
- '' +
- '
' +
- '' +
- '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '' +
'
';
}
function blatCloseRename() {
- var bg = document.getElementById('blatModalBg');
+ var bg = document.getElementById('gbModalBg');
if (bg) { bg.style.display = 'none'; }
}
function blatOpenRename() {
- var bg = document.getElementById('blatModalBg');
+ var bg = document.getElementById('gbModalBg');
if (!bg) { return; }
// Pre-fill with the track's current name/description (emitted by hgBlat.c in cfg).
var cfg = hgBlatData.config;
document.getElementById('blatRenameName').value = cfg.trackName || '';
document.getElementById('blatRenameDesc').value = cfg.trackDescription || '';
bg.style.display = 'flex';
document.getElementById('blatRenameName').focus();
document.getElementById('blatRenameName').select();
}
function blatWireRename() {
$('#blatRenameBtn').on('click', blatOpenRename);
$('#blatRenameCancel').on('click', blatCloseRename);
// Click on the dark backdrop (but not the dialog itself) closes.
- $('#blatModalBg').on('click', function(ev) {
+ $('#gbModalBg').on('click', function(ev) {
if (ev.target === this) { blatCloseRename(); }
});
$(document).on('keydown.blatRename', function(ev) {
- var bg = document.getElementById('blatModalBg');
+ var bg = document.getElementById('gbModalBg');
if (bg && bg.style.display !== 'none' && ev.key === 'Escape') { blatCloseRename(); }
});
$('#blatRenameOk').on('click', function() {
var name = document.getElementById('blatRenameName').value.trim();
var desc = document.getElementById('blatRenameDesc').value.trim();
if (!name) { document.getElementById('blatRenameName').focus(); return; }
// Reuse hgBlat.c's window.blatRenameCt(name, description): rebuilds the custom track under the
// new name via the existing hgc buildBigPsl call. Keep cfg in sync so a re-open of the modal
// shows the new values.
if (typeof window.blatRenameCt === 'function') {
hgBlatData.config.trackName = name;
hgBlatData.config.trackDescription = desc;
window.blatRenameCt(name, desc);
}
blatCloseRename();
@@ -369,34 +369,34 @@
function blatToFasta(seqs) {
// seqs: [{name, seq}, ...] -> FASTA text, sequence wrapped at 60 chars per line.
return seqs.map(function(s) {
var body = String(s.seq || '').toUpperCase().replace(/(.{60})/g, '$1\n').replace(/\n$/, '');
return '>' + s.name + '\n' + body;
}).join('\n');
}
function blatShowFasta(box, seqs, fileName) {
// Render seqs as FASTA inside `box`, with Copy-to-clipboard and Download buttons. Generic — takes
// any [{name, seq}] list so it can be reused for other sequences later.
var fasta = blatToFasta(seqs);
box.style.display = 'flex';
box.innerHTML =
'
';
var ta = document.getElementById('blatSeqText');
ta.value = fasta;
document.getElementById('blatSeqCopy').addEventListener('click', function() {
ta.select();
if (navigator.clipboard) { navigator.clipboard.writeText(fasta); }
else { document.execCommand('copy'); }
this.textContent = 'Copied';
});
document.getElementById('blatSeqDownload').addEventListener('click', function() {
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([fasta], { type: 'text/plain' }));
a.download = fileName || 'query.fa';
document.body.appendChild(a);
a.click();
@@ -415,68 +415,68 @@
}
// ---- build ---------------------------------------------------------------
function blatBuild() {
var cfg = hgBlatData.config;
var hits = hgBlatData.hits;
// Pin a stable, shareable URL into the address bar (no server redirect) so refresh, bookmark and
// "Share a link" all use the trash-backed reopen link instead of the transient POST/search URL.
if (cfg.shareUrl) {
try { history.replaceState(null, '', cfg.shareUrl); } catch (e) { /* older browsers: ignore */ }
}
var back = cfg.backUrl ?
- `Back to Genome Browser` : '';
// The page actions live in the gold main-header bar (framework #sectTtl), next to the title -
// so there is no separate toolbar (.blatHead is gone). Injected into #sectTtl below.
var headActions =
- `${back}New BLAT search`;
+ `${back}New BLAT search`;
// Top banner: note this is the new page, link back to the classic page (fresh searches only,
// where the trash files still exist), and invite feedback. The old page also clears the
// blatNewPage preference so later searches use the classic page until the user opts back in.
var origPage = cfg.canOldPage ?
` You can go back to the original page anytime.` : '';
var bannerHtml =
- `
We are testing a new BLAT output page.${origPage} ` +
+ `
We are testing a new BLAT output page.${origPage} ` +
`If you have feedback on this new page, do not hesitate to let us know via ` +
`genome@soe.ucsc.edu.
`;
var queryCount = new Set(hits.map(h => h.qName)).size;
var th = [];
th.push('
#
');
if (cfg.multiQuery) { th.push('
Query
'); }
th.push('
Open in Genome Browser
');
th.push('
Show
');
th.push('
Query coverage
');
if (cfg.hasLocus) { th.push('
Locus
'); }
th.push('
Score
');
th.push('
Identity
');
th.push('
Strand
');
th.push('
Span
');
// detail dock sits above the table: with long hit lists a bottom dock scrolls out of view
document.getElementById('blatResults').innerHTML =
bannerHtml +
- `
` +
(cfg.canRename ? blatRenameModalHtml(cfg) : '');
// Put the page actions in the gold main-header bar, to the right of the title (framework #sectTtl).
var sectTtl = document.getElementById('sectTtl');
if (sectTtl) {
var acts = document.createElement('span');
acts.className = 'blatHeadActions';
acts.innerHTML = headActions;
sectTtl.appendChild(acts);
}
$('#blatShareBtn').on('click', blatShareLink);
$('#blatSeqBtn').on('click', blatShowQuerySeq);
@@ -602,42 +602,42 @@
if (!ta || !out) { return; }
var n = ta.value.replace(/[^A-Za-z*]/g, '').length;
out.textContent = blatFmt(n) + ' of 25,000 characters';
$('#blatLimitLink').toggleClass('over', n > 25000);
}
function blatFormTab(showUpload) {
$('#blatTabPaste').toggleClass('on', !showUpload);
$('#blatTabUpload').toggleClass('on', showUpload);
$('#blatPanePaste').toggle(!showUpload);
$('#blatPaneUpload').toggle(showUpload);
}
function blatFormLimitsModal() {
var row = (k, v) => `
${k}${v}
`;
- return '
' +
- '
' +
- '
Input limits
' +
+ return '
' +
+ '
' +
+ '
Input limits
' +
row('DNA per sequence', '25,000 bases') +
row('Protein / translated', '10,000 letters') +
row('Sequences per run', '25') +
row('Total per submission', '50,000 bases') +
- '
Queries above these limits are rejected ' +
+ '
Queries above these limits are rejected ' +
'before alignment. For larger jobs, run BLAT from the ' +
'' +
'command line on your own server.
' +
- '' +
+ '' +
'
';
}
function blatFormSetDb(db) {
// Called by hgBlat.c's setupGenomeSearchBar onSelect. Picking a genome does not reload the
// page, so everything on it that depends on db is updated here instead: the hidden field that
// the search is submitted with, and the sidebar links that carry a db= parameter. The current
// assembly label is updated by setupGenomeSearchBar itself.
document.mainForm.db.value = db;
$('#blatFormBox a[data-urltpl]').each(function() {
this.href = this.getAttribute('data-urltpl').replace('$DB$', encodeURIComponent(db));
});
}
function blatFormSidebar(cfg) {
@@ -647,84 +647,84 @@
var tplLink = (tpl, label) => {
var href = tpl.replace('$DB$', encodeURIComponent(hgBlatFormData.db));
return `${label}`;
};
if (cfg.pcrUrlTpl) {
tools += `
${tplLink(cfg.pcrUrlTpl, 'In-Silico PCR')} — better than BLAT for ` +
'locating PCR primers.
';
}
if (cfg.oligoMatchUrlTpl) {
tools += `
${tplLink(cfg.oligoMatchUrlTpl, 'Short Sequence Match')} — for ` +
'sequences under 20 bp, within the region shown in the Genome Browser.
';
}
tools += '
' +
'findMotifs — command-line search across a whole genome.
DNA BLAT quickly finds sequences of 95% and greater similarity that are at least 25 bases ' +
'long; it finds perfect matches down to 20 bases, and may miss shorter or more divergent ' +
'alignments. Protein BLAT finds sequences of 80% and greater similarity at least 20 amino acids ' +
'long.
';
}
function blatFormBuild() {
var cfg = hgBlatFormData;
var banner = '';
if (cfg.classicUrl) {
- banner = '
We are testing a new BLAT search page. You can go back to ' +
+ banner = '
We are testing a new BLAT search page. You can go back to ' +
`the original page anytime. If you have feedback ` +
'on this new page, do not hesitate to let us know via ' +
'genome@soe.ucsc.edu.
';
}
// Checkbox plus the browser's standard info icon. Same SVG and same title +
// convertTitleTagsToMouseovers mechanism as printInfoIcon()/printInfoIconSvg() in hg/lib/hui.c,
// so these read identically to the info icons on every other Genome Browser page.
var check = (name, on, label, tip) =>
`` +
`${BLAT_INFO_SVG}`;
// "Keep results" starting state. The cart (cfg.keepResults) only remembers the choice within a
// session; localStorage carries it across sessions so a user who wants their BLAT results to
// accumulate does not have to re-tick the box on every visit. localStorage wins when set (it is
// the more durable record of the user's own preference); the cart is the fallback for a browser
// that has never stored one. Only consulted where the box is actually shown (blatOldTracks=
// delete); elsewhere the choice has no effect, so there is nothing worth persisting.
var keepResultsInit = cfg.keepResults;
if (cfg.showKeepResults) {
var storedKeep = blatGetKeepResultsPref();
if (storedKeep !== null) { keepResultsInit = storedKeep; }
}
document.getElementById('blatFormBox').innerHTML =
banner +
'
' +
- '
Search – type keywords to find the target assembly
' +
+ '
Search – type keywords to find the target assembly
' +
'
' +
'
Genome or assembly ' +
`${BLAT_INFO_SVG}` +
'' +
'
' +
`` +
// Sort and output are submitted but not offered: sorting by anything other than score
// is rarely useful, and this page always wants the hyperlink (results table) output.
// Kept as hidden fields so the request hgBlat receives is unchanged.
`` +
`` +
'
' +
'
' +
// The mouseover popup keeps itself open while the pointer is inside it (see the
@@ -751,62 +751,62 @@
// nothing when unticked, so cartUsualBoolean would never see it go back to false and
// "Keep results" could not be switched off again once used.
(cfg.showKeepResults ?
'' +
`` +
`' +
`` +
`${BLAT_INFO_SVG}` : '') +
'
' +
blatFormLimitsModal();
// Move the C-generated genome search bar (real autocomplete over every assembly, already wired
// by setupGenomeSearchBar) into its slot, rather than reimplementing it with a hardcoded list.
var holder = document.getElementById('blatGenomeHolder');
if (holder) { document.getElementById('blatGenomeSlot').appendChild(holder); }
// Show the current assembly in the search bar itself instead of in a separate "Current genome:"
// line - the bar is wide enough for the whole description. setupGenomeSearchBar writes the new
// one in on each pick, and focusing the bar selects all of it, so it reads as a filled-in search