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 `<span class="blatIdPct" style="color:${c}">${hit.identity.toFixed(1)}%</span>`; } 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 `<span class="blatCov" title="${tip}"><i style="left:${left.toFixed(1)}%;` + `width:${width.toFixed(1)}%"></i></span>`; } // ---- summary strip + detail panel --------------------------------------- function blatSummaryStrip(cfg, queryCount) { - var stat = (k, v) => `<div class="blatStat"><span class="k">${k}</span>` + + var stat = (k, v) => `<div class="gbStat"><span class="k">${k}</span>` + `<span class="v">${v}</span></div>`; - var div = '<span class="blatDiv"></span>'; + var div = '<span class="gbDiv"></span>'; 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 += `<a class="blatPill" title="Open the Genome Browser with all these BLAT hits shown together as one custom track" href="${htmlEncode(cfg.viewAllUrl)}">View all in browser</a>`; + actions += `<a class="gbPill" title="Open the Genome Browser with all these BLAT hits shown together as one custom track" href="${htmlEncode(cfg.viewAllUrl)}">View all in browser</a>`; } // "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 += '<button type="button" class="blatPill" id="blatSeqBtn" ' + + actions += '<button type="button" class="gbPill" id="blatSeqBtn" ' + 'title="Show the sequence you searched with, in FASTA format">Show Query Sequence</button>'; } // "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 = '<svg class="blatShareIcon" viewBox="0 0 24 24" width="13" height="13" ' + 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" ' + 'stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"></circle>' + '<circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle>' + '<line x1="8.6" y1="10.5" x2="15.4" y2="6.5"></line>' + '<line x1="8.6" y1="13.5" x2="15.4" y2="17.5"></line></svg>'; - actions += '<button type="button" class="blatPill" id="blatShareBtn" ' + + actions += '<button type="button" class="gbPill" id="blatShareBtn" ' + 'title="Show a link that reopens these results (works for a limited time)">' + shareIcon + 'Share a link</button>'; } // "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 += '<button type="button" class="blatPill" id="blatRenameBtn" ' + + actions += '<button type="button" class="gbPill" id="blatRenameBtn" ' + 'title="Rename this BLAT results custom track and its description">Rename BLAT Track</button>'; } - return `<div class="blatStrip">${stats}<span class="blatStripActions">${actions}</span></div>`; + return `<div class="gbStrip">${stats}<span class="gbStripActions">${actions}</span></div>`; } 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 = `<div class="dhead"><span class="lab">Selected hit</span>` + `<span class="loc" id="dvLoc"></span></div>` + `<div class="blatDetailCard"><div class="blatDetailCol">` + `<div class="blatTiles">${tiles}</div>` + `<div class="blatDetailActions">` + - `<a class="blatPill" id="dvBrowser" title="Open this hit in the Genome Browser" href="#">Open in browser</a>` + - `<a class="blatPill" id="dvNewTab" target="_blank" title="Open this hit in the Genome Browser in a new tab" href="#">Open in new tab</a></div></div>` + + `<a class="gbPill" id="dvBrowser" title="Open this hit in the Genome Browser" href="#">Open in browser</a>` + + `<a class="gbPill" id="dvNewTab" target="_blank" title="Open this hit in the Genome Browser in a new tab" href="#">Open in new tab</a></div></div>` + `<div id="dvAlignBox" class="blatAlignBox">` + `<div class="blatTile"><div class="k">Alignment</div></div>` + `<div id="dvAlign" class="blatAlignText"></div>` + - `<a class="blatPill" id="dvViewAlign" ` + + `<a class="gbPill" id="dvViewAlign" ` + `title="See the base-by-base alignment of your query against this hit" href="#">` + `View alignment</a></div></div>`; 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 = - '<span class="blatShareMsg blatShareFull">Shareable link — anyone with it can reopen ' + + '<span class="gbShareMsg gbShareFull">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.</span>' + - '<input id="blatShareInput" class="blatShareInput" type="text" readonly>' + - '<button type="button" class="blatPill" id="blatShareCopy" title="Copy the link to the clipboard">Copy</button>'; - var inp = document.getElementById('blatShareInput'); + '<input id="gbShareInput" class="gbShareInput" type="text" readonly>' + + '<button type="button" class="gbPill" id="blatShareCopy" title="Copy the link to the clipboard">Copy</button>'; + 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 '<div id="blatModalBg" class="blatModalBg" style="display:none">' + - '<div class="blatModal" role="dialog" aria-modal="true" aria-labelledby="blatModalTitle">' + - '<div class="blatModalTitle" id="blatModalTitle">Rename BLAT Track</div>' + - '<div class="blatModalText">Every BLAT result is stored in its own track in the Genome ' + + return '<div id="gbModalBg" class="gbModalBg" style="display:none">' + + '<div class="gbModal" role="dialog" aria-modal="true" aria-labelledby="gbModalTitle">' + + '<div class="gbModalTitle" id="gbModalTitle">Rename BLAT Track</div>' + + '<div class="gbModalText">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 <a href="${sessionUrl}">Session link</a>.</div>` + - '<label class="blatModalLabel" for="blatRenameName">Track name</label>' + - '<input id="blatRenameName" class="blatModalInput" type="text" maxlength="80">' + - '<label class="blatModalLabel" for="blatRenameDesc">Description</label>' + - '<input id="blatRenameDesc" class="blatModalInput" type="text" maxlength="120">' + - '<div class="blatModalBtns">' + - '<button type="button" class="blatPill" id="blatRenameCancel">Cancel</button>' + - '<button type="button" class="blatPill primary" id="blatRenameOk">OK</button>' + + '<label class="gbModalLabel" for="blatRenameName">Track name</label>' + + '<input id="blatRenameName" class="gbModalInput" type="text" maxlength="80">' + + '<label class="gbModalLabel" for="blatRenameDesc">Description</label>' + + '<input id="blatRenameDesc" class="gbModalInput" type="text" maxlength="120">' + + '<div class="gbModalBtns">' + + '<button type="button" class="gbPill" id="blatRenameCancel">Cancel</button>' + + '<button type="button" class="gbPill primary" id="blatRenameOk">OK</button>' + '</div></div></div>'; } 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 = '<div class="blatSeqBar">' + - '<span class="blatShareMsg">Query sequence (FASTA):</span>' + - '<button type="button" class="blatPill" id="blatSeqCopy" title="Copy the FASTA to the clipboard">Copy to Clipboard</button>' + - '<button type="button" class="blatPill" id="blatSeqDownload" title="Download the FASTA as a .fa file">Download</button>' + - '<button type="button" class="blatPill" id="blatSeqClose" title="Hide the query sequence">Close</button>' + + '<span class="gbShareMsg">Query sequence (FASTA):</span>' + + '<button type="button" class="gbPill" id="blatSeqCopy" title="Copy the FASTA to the clipboard">Copy to Clipboard</button>' + + '<button type="button" class="gbPill" id="blatSeqDownload" title="Download the FASTA as a .fa file">Download</button>' + + '<button type="button" class="gbPill" id="blatSeqClose" title="Hide the query sequence">Close</button>' + '</div><textarea id="blatSeqText" class="blatSeqText" readonly></textarea>'; 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 ? - `<a class="blatPill" title="Return to the Genome Browser at your previous location (${htmlEncode(cfg.backPos)})" ` + + `<a class="gbPill" title="Return to the Genome Browser at your previous location (${htmlEncode(cfg.backPos)})" ` + `href="${htmlEncode(cfg.backUrl)}">Back to Genome Browser</a>` : ''; // 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}<a class="blatPill primary" title="Start a new BLAT search" href="${htmlEncode(cfg.newSearchUrl)}">New BLAT search</a>`; + `${back}<a class="gbPill primary" title="Start a new BLAT search" href="${htmlEncode(cfg.newSearchUrl)}">New BLAT search</a>`; // 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 <a title="Show these results on the classic BLAT results page" ` + `href="hgBlat?blatNewPage=0&blatReopen=1&hgsid=${encodeURIComponent(cfg.hgsid)}">the original page</a> anytime.` : ''; var bannerHtml = - `<div class="blatBanner">We are testing a new BLAT output page.${origPage} ` + + `<div class="gbBanner">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 ` + `<a href="mailto:genome@soe.ucsc.edu">genome@soe.ucsc.edu</a>.</div>`; var queryCount = new Set(hits.map(h => h.qName)).size; var th = []; th.push('<th class="num">#</th>'); if (cfg.multiQuery) { th.push('<th>Query</th>'); } th.push('<th>Open in Genome Browser</th>'); th.push('<th>Show</th>'); th.push('<th>Query coverage</th>'); if (cfg.hasLocus) { th.push('<th>Locus</th>'); } th.push('<th class="num">Score</th>'); th.push('<th class="num">Identity</th>'); th.push('<th>Strand</th>'); th.push('<th class="num">Span</th>'); // detail dock sits above the table: with long hit lists a bottom dock scrolls out of view document.getElementById('blatResults').innerHTML = bannerHtml + - `<div class="blatCard">${blatSummaryStrip(cfg, queryCount)}` + - `<div id="blatShareBox" class="blatShareBox" style="display:none"></div>` + - `<div id="blatSeqBox" class="blatShareBox" style="display:none"></div>` + + `<div class="gbCard">${blatSummaryStrip(cfg, queryCount)}` + + `<div id="gbShareBox" class="gbShareBox" style="display:none"></div>` + + `<div id="blatSeqBox" class="gbShareBox" style="display:none"></div>` + `<div id="blatDetail" class="blatDetail"></div>` + `<table id="blatTable" class="display"><thead><tr>${th.join('')}</tr></thead></table></div>` + (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) => `<div class="blatLimitRow"><span>${k}</span><strong>${v}</strong></div>`; - return '<div id="blatLimitsBg" class="blatModalBg" style="display:none">' + - '<div class="blatModal" role="dialog" aria-modal="true" aria-labelledby="blatLimitsTitle">' + - '<div class="blatModalTitle" id="blatLimitsTitle">Input limits</div>' + + return '<div id="blatLimitsBg" class="gbModalBg" style="display:none">' + + '<div class="gbModal" role="dialog" aria-modal="true" aria-labelledby="blatLimitsTitle">' + + '<div class="gbModalTitle" id="blatLimitsTitle">Input limits</div>' + 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') + - '<div class="blatModalText blatModalNote">Queries above these limits are rejected ' + + '<div class="gbModalText gbModalNote">Queries above these limits are rejected ' + 'before alignment. For larger jobs, run BLAT from the ' + '<a target="_blank" href="https://hgdownload.soe.ucsc.edu/downloads.html#utilities_downloads">' + 'command line</a> on your own server.</div>' + - '<div class="blatModalBtns"><button type="button" class="blatPill" id="blatLimitsClose">Close</button></div>' + + '<div class="gbModalBtns"><button type="button" class="gbPill" id="blatLimitsClose">Close</button></div>' + '</div></div>'; } 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 `<a data-urltpl="${htmlEncode(tpl)}" href="${htmlEncode(href)}">${label}</a>`; }; if (cfg.pcrUrlTpl) { tools += `<div>${tplLink(cfg.pcrUrlTpl, 'In-Silico PCR')} — better than BLAT for ` + 'locating PCR primers.</div>'; } if (cfg.oligoMatchUrlTpl) { tools += `<div>${tplLink(cfg.oligoMatchUrlTpl, 'Short Sequence Match')} — for ` + 'sequences under 20 bp, within the region shown in the Genome Browser.</div>'; } tools += '<div><a target="_blank" href="https://hgdownload.soe.ucsc.edu/downloads.html#utilities_downloads">' + 'findMotifs</a> — command-line search across a whole genome.</div>'; return '<div>' + - (tools ? `<div class="blatCard"><h3>Similar tools</h3>${tools}</div>` : '') + - '<div class="blatCard"><h3>Help</h3>' + + (tools ? `<div class="gbCard"><h3>Similar tools</h3>${tools}</div>` : '') + + '<div class="gbCard"><h3>Help</h3>' + '<div><a href="../FAQ/FAQblat.html">BLAT FAQ</a></div>' + '<div><a href="../goldenPath/help/hgTracksHelp.html#BLATAlign">BLAT documentation</a></div>' + // No "Search all genomes FAQ" here: that link now lives in the "Search many genomes" // tooltip, next to the checkbox it actually explains. '<div><a href="../FAQ/FAQblat.html#blat14">Programmatic / batch BLAT</a></div>' + '</div>' + - '<div class="blatCard"><h3>About BLAT</h3>' + + '<div class="gbCard"><h3>About BLAT</h3>' + '<div>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.</div>' + '<div>Kent WJ. <a target="_blank" href="https://genome.cshlp.org/content/12/4/656.abstract">' + 'BLAT — the BLAST-like alignment tool</a>. Genome Res. 2002 Apr;12(4):656-64.</div>' + '</div></div>'; } function blatFormBuild() { var cfg = hgBlatFormData; var banner = ''; if (cfg.classicUrl) { - banner = '<div class="blatBanner">We are testing a new BLAT search page. You can go back to ' + + banner = '<div class="gbBanner">We are testing a new BLAT search page. You can go back to ' + `<a href="${htmlEncode(cfg.classicUrl)}">the original page</a> anytime. If you have feedback ` + 'on this new page, do not hesitate to let us know via ' + '<a href="mailto:genome@soe.ucsc.edu">genome@soe.ucsc.edu</a>.</div>'; } // 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) => `<span class="blatCheck"><label><input type="checkbox" name="${name}" ` + `id="blat_${name}"${on ? ' checked' : ''}>${label}</label>` + `<span class="blatInfo" title="${htmlEncode(tip)}">${BLAT_INFO_SVG}</span></span>`; // "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 + '<div class="blatFormGrid"><div>' + - '<div class="blatSection">Search – type keywords to find the target assembly</div>' + + '<div class="gbSection">Search – type keywords to find the target assembly</div>' + '<div class="blatRow">' + '<div class="blatField blatGenomeSlot"><span>Genome or assembly ' + `<span class="blatInfo" title="${BLAT_GENOME_SEARCH_HELP}">${BLAT_INFO_SVG}</span>` + '</span>' + '<div id="blatGenomeSlot"></div></div>' + `<label class="blatField"><span>Query type</span><select name="type">${blatOpts(cfg.types, cfg.type)}</select></label>` + // 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. `<input type="hidden" name="sort" value="${htmlEncode(cfg.sort)}">` + `<input type="hidden" name="output" value="${htmlEncode(cfg.output)}">` + '</div>' + '<div class="blatChecks">' + // 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 ? '<span class="blatCheck">' + `<input type="hidden" name="blatKeepResults" id="blatKeepResultsVal" value="${keepResultsInit ? 1 : 0}">` + `<label><input type="checkbox" id="blat_keepResults"${keepResultsInit ? ' checked' : ''}>` + 'Keep results</label>' + `<span class="blatInfo" title="${htmlEncode( 'A new BLAT search always overrides your previous BLAT results: each search ' + 'replaces the result track of the one before it in the Genome Browser. Check ' + 'this box to keep earlier results instead, so every search adds its own track ' + 'and results accumulate. Your choice is remembered for next time.')}">` + `${BLAT_INFO_SVG}</span></span>` : '') + '</div>' + - '<div class="blatSection">Query sequence</div>' + + '<div class="gbSection">Query sequence</div>' + '<div class="blatTabs">' + '<button type="button" class="blatTab on" id="blatTabPaste">Paste sequence</button>' + '<button type="button" class="blatTab" id="blatTabUpload">Upload file</button>' + '</div>' + '<div id="blatPanePaste">' + '<div class="blatPaneHint"><span>Separate multiple sequences with a >name line. ' + 'Up to 25 sequences.</span>' + `<a href="#" id="blatExample">${htmlEncode(cfg.exampleLabel)}</a></div>` + '<textarea class="blatSeq" name="userSeq" id="blatUserSeq" spellcheck="false" ' + 'aria-label="Paste in a query sequence"></textarea>' + '<div class="blatCount"><span id="blatCountText"></span>' + '<a href="#" id="blatLimitLink">Show input limits</a></div>' + '</div>' + '<div id="blatPaneUpload" style="display:none">' + '<div class="blatDrop" id="blatDrop">' + '<div class="blatDropTitle">Drop a sequence file here</div>' + '<div class="blatDropSub">Plain text or FASTA, up to 50,000 bases total</div>' + '<input type="file" name="seqFile" id="blatSeqFile">' + '<div class="blatFileName" id="blatFileName"></div>' + '</div>' + '</div>' + '<div class="blatActions">' + - '<input type="submit" class="blatPill primary" name="Submit" value="Submit" ' + + '<input type="submit" class="gbPill primary" name="Submit" value="Submit" ' + `title="${htmlEncode('Align the sequence and show all matches')}">` + - '<input type="submit" class="blatPill" name="Lucky" value="I'm feeling lucky" ' + + '<input type="submit" class="gbPill" name="Lucky" value="I'm feeling lucky" ' + `title="${htmlEncode('Skip the list of matches and open the best-scoring one straight ' + 'away in the Genome Browser. Ignored when "Search many genomes" is ticked.')}">` + - '<input type="submit" class="blatPill" name="Clear" value="Clear" ' + + '<input type="submit" class="gbPill" name="Clear" value="Clear" ' + `title="${htmlEncode('Empty the query sequence box')}">` + '</div>' + '</div>' + blatFormSidebar(cfg) + '</div>' + 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