3fe32a26217a69602da98b5747ab182e1e5460dd tdreszer Thu May 8 11:59:28 2014 -0700 Rewrote the car variable queuing routines, fixing one bug and ensuring the vars are updated at window unload event as a last resort. Also changed a couple stray undeclared vars that use strict found. Redmine 13164. diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js index 151344b..00e041d 100644 --- src/hg/js/hgTracks.js +++ src/hg/js/hgTracks.js @@ -259,31 +259,31 @@ pos.chromStart+1, pos.chromEnd); return newPosition; }, handleChange: function (response, status) { var json = eval("(" + response + ")"); genomePos.set(json.pos); }, changeAssemblies: function (ele) // UNUSED? Larry's experimental code { // code to update page when user changes assembly select list. $.ajax({ type: "GET", url: "../cgi-bin/hgApi", - data: cart.addUpdatesToUrl("cmd=defaultPos&db=" + getDb()), + data: cart.varsToUrlData({ 'cmd': 'defaultPos', 'db': getDb() }), dataType: "html", trueSuccess: genomePos.handleChange, success: catchErrorOrDispatch, error: errorHandler, cache: true }); return false; } } ///////////////////////////////////// //// Creating items by dragging ///// ///////////////////////////////////// var makeItemsByDrag = { @@ -441,120 +441,153 @@ if ($(obj).hasClass('noLink')) // TITLE_BUT_NO_LINK return false; if (obj.href.match('#') || obj.target.length > 0) { //alert("Matched # ["+obj.href+"] or has target:"+obj.target); return true; } var thisForm=$(obj).parents('form'); if(thisForm == undefined || $(thisForm).length == 0) thisForm=$("FORM"); if($(thisForm).length > 1 ) thisForm=$(thisForm)[0]; if(thisForm != undefined && $(thisForm).length == 1) { //alert("posting form:"+$(thisForm).attr('name')); - var href = cart.addUpdatesToUrl(obj.href); - return postTheForm($(thisForm).attr('name'),href); + return postTheForm($(thisForm).attr('name'),cart.addUpdatesToUrl(obj.href)); } return true; } } ///////////////////////// //// cart updating ///// /////////////////////// var cart = { + // Controls queuing and ultimately updating cart variables vis ajax or submit. Queued vars + // are held in an object with unique keys preventing duplicate updates and ensuring last update + // takes precedence. WARNING: be careful creating an object with variables on the fly: + // cart.setVarsObj({track: vis}) is invalid but cart.setVarsObj({'knownGene': vis}) is ok! updateQueue: {}, updatesWaiting: function () { // returns TRUE if updates are waiting. return (Object.keys(cart.updateQueue).length !== 0); }, addUpdatesToUrl: function (url) { // adds any outstanding cart updates to the url, then clears the queue if (cart.updatesWaiting()) { - var updates = ""; - for (var track in cart.updateQueue) { - updates += "&" + track + "=" + cart.updateQueue[track]; - } + //console.log('cart.addUpdatesToUrl: '+Object.keys(cart.updateQueue).length+' vars'); + var updates = cart.varsToUrlData(); // clears the queue + if (typeof url === 'undefined' || url.length === 0) + return updates; + if (updates.length > 0) { - if(url.length > 0 && url.lastIndexOf("?") == -1 && url.lastIndexOf("&") == -1) - url += "?" + updates.substring(1); + var dataOnly = (url.indexOf("cgi-bin") === -1); // all urls should be to our cgis + if (!dataOnly && url.lastIndexOf("?") === -1) + url += "?" + updates; else - url += updates + url += '&' + updates; } - cart.updateQueue = {}; } return url; }, - // NOTE: could update in background, however, failing to hit "refresh" is a user choice - // updatesViaAjax: function () - // { // Called via timer: updates the cart via setVars. - // if (!cart.updatesWaiting()) - // return; - // - // var tracks = []; - // var newVis = []; - // for (var track in cart.updateQueue) { - // tracks.push(track) - // newVis.push(cart.updateQueue[track]); - // } - // if (tracks.length === 0) - // return; - // cart.updateQueue = {}; - // setCartVars(tracks,newVis,async=false); // sync to avoid another race condition mess - // }, + beforeUnload: function () + { // named function that can be bound and unbound to beforeunload event + // Makes sure any outstanding queued updates are sent before leaving the page. + //console.log('cart.beforeUnload: '+Object.keys(cart.updateQueue).length+' vars'); + cart.setVarsObj( {}, null, false ); // synchronous + }, - addUpdateToQueue: function (track,newVis) - { // creates a string of updates to save for ajax batch or a submit - cart.updateQueue[track] = newVis; + varsToUrlData: function (varsObj) + { // creates a url data (var1=val1&var2=val2...) string from vars object and queue + // The queue will be emptied by this call. + cart.queueVarsObj(varsObj); // lay ontop of queue, to give new values precedence - // NOTE: could update in background, however, failing to hit "refresh" is a user choice - // first in queue, schedule background update - // if (Object.keys(cart.updateQueue).length === 1) - // setTimeout("cart.updatesViaAjax();",10000); + // Now convert to url data and clear queue + var urlData = ''; + if (cart.updatesWaiting()) { + //console.log('cart.varsToUrlData: '+Object.keys(cart.updateQueue).length+' vars'); + urlData = varHashToQueryString(cart.updateQueue); + cart.updateQueue = {}; + } + return urlData; }, - setVars: function (names, values, errFunc, async) - { // ajax updates the cart, and includes any queued updates. + setVarsObj: function (varsObj, errFunc, async) + { // Set all vars in a var hash, appending any queued updates + //console.log('cart.setVarsObj: were:'+Object.keys(cart.updateQueue).length + + // ' new:'+Object.keys(varsObj).length); + cart.queueVarsObj(varsObj); // lay ontop of queue, to give new values precedence + + // Now ajax update all in queue and clear queue if (cart.updatesWaiting()) { - for (var track in cart.updateQueue) { - names.push(track); - values.push(cart.updateQueue[track]); - } + setVarsFromHash(cart.updateQueue, errFunc, async); cart.updateQueue = {}; } - setCartVars(names, values, errFunc, async); }, - setVar: function (names, values, errFunc, async) - { // wraps single var update to expected arrays - vis.setVars( [ name ], [ value ], errFunc, async ); + arysToObj: function (names,values) + { // Make hash type obj with two parallel arrays. (should be moved to utils.js). + var obj = {}; + for(var ix=0; ix" + newPosition + "

" + "Don't show this dialog again and always zoom.
" + "(Re-enable highlight via the 'configure' menu at any time.)

"); dragSelectDialog = $("#dragSelectDialog")[0]; } $(dragSelectDialog).dialog({ @@ -712,31 +745,31 @@ minWidth: 400, buttons: { "Zoom In": function() { // Zoom to selection $(this).dialog("option", "revertToOriginalPos", false); if ($("#disableDragHighlight").attr('checked')) hgTracks.enableHighlightingDialog = false; if (imageV2.inPlaceUpdate) { var params = "position=" + newPosition; if (!hgTracks.enableHighlightingDialog) params += "&enableHighlightingDialog=0" imageV2.navigateInPlace(params, null, true); } else { $('body').css('cursor', 'wait'); if (!hgTracks.enableHighlightingDialog) - cart.setVars(['enableHighlightingDialog'],[0]); + cart.setVarsObj({'enableHighlightingDialog': 0 }); document.TrackHeaderForm.submit(); } $(this).dialog("close"); }, "Highlight": function() { // Highlight selection $(imageV2.imgTbl).imgAreaSelect({hide:true}); if ($("#disableDragHighlight").attr('checked')) hgTracks.enableHighlightingDialog = false; dragSelect.highlightThisRegion(newPosition); $(this).dialog("close"); }, "Cancel": function() { $(this).dialog("close"); } @@ -1157,42 +1190,40 @@ img.height = $(chrImg).height(); img.width = $(chrImg).width(); } return img; } }); } /////////////////////////// //// Drag Reorder Code //// /////////////////////////// var dragReorder = { setOrder: function (table) { // Sets the 'order' value for the image table after a drag reorder - var names = []; - var values = []; + var varsToUpdate = {}; $("tr.imgOrd").each(function (i) { if ($(this).attr('abbr') != $(this).attr('rowIndex').toString()) { $(this).attr('abbr',$(this).attr('rowIndex').toString()); var name = this.id.substring('tr_'.length) + '_imgOrd'; - names.push(name); - values.push($(this).attr('abbr')); + varsToUpdate[name] = $(this).attr('abbr'); } }); - if(names.length > 0) { - cart.setVars(names,values); + if (Object.keys(varsToUpdate).length !== 0) { + cart.setVarsObj(varsToUpdate); imageV2.markAsDirtyPage(); } }, sort: function (table) { // Sets the table row order to match the order of the abbr attribute. // This is needed for back-button, and for visBox changes combined with refresh. var tbody = $(table).find('tbody')[0]; if(tbody == undefined) tbody = table; // Do we need to sort? var trs = tbody.rows; var needToSort = false; $(trs).each(function(ix) { @@ -1216,31 +1247,31 @@ // most efficient reload of sorted rows I have found var sortedRows = jQuery.map(ary, function(ary, i) { return ary.row; }); $(tbody).append( sortedRows ); // removes tr from current position and adds to end. return true; }, showCenterLabel: function (tr, show) { // Will show or hide centerlabel as requested // adjust button, sideLabel height, sideLabelOffset and centerlabel display if (!$(tr).hasClass('clOpt')) return; var center = $(tr).find(".sliceDiv.cntrLab"); if($(center) == undefined) return; - seen = ($(center).css('display') != 'none'); + var seen = ($(center).css('display') != 'none'); if(show == seen) return; var centerHeight = $(center).height(); var btn = $(tr).find("p.btn"); var side = $(tr).find(".sliceDiv.sideLab"); if($(btn) != undefined && $(side) != undefined) { var sideImg = $(side).find("img"); if($(sideImg) != undefined) { var top = parseInt($(sideImg).css('top')); if(show) { $(btn).css('height',$(btn).height() + centerHeight); $(side).css('height',$(side).height() + centerHeight); top += centerHeight; // top is a negative number @@ -1495,30 +1526,31 @@ } } ////////////////////////// //// Drag Scroll code //// ////////////////////////// jQuery.fn.panImages = function(){ // globals across all panImages genomePos.original = genomePos.getOriginalPos(); // XXXX what is this for? (this already happened in initVars). var leftLimit = hgTracks.imgBoxLeftLabel * -1; var rightLimit = (hgTracks.imgBoxPortalWidth - hgTracks.imgBoxWidth + leftLimit); var only1xScrolling = ((hgTracks.imgBoxWidth - hgTracks.imgBoxPortalWidth) == 0);//< hgTracks.imgBoxLeftLabel); var prevX = (hgTracks.imgBoxPortalOffsetX + hgTracks.imgBoxLeftLabel) * -1; var portalWidth = 0; + var portalAbsoluteX = 0; var savedPosition; var highlightArea = null; // Used to ensure dragSelect highlight will scroll. this.each(function(){ var pic; var pan; if ( $(this).is("img") ) { pan = $(this).parent("div"); pic = $(this); } else if ( $(this).is("div.scroller") ) { pan = $(this); pic = $(this).children("img#panImg"); // Get the real pic @@ -2054,32 +2086,32 @@ } } } else if (cmd == 'zoomCodon' || cmd == 'zoomExon') { var num, ajaxCmd, msg; if(cmd == 'zoomCodon') { msg = "Please enter the codon number to jump to:"; ajaxCmd = 'codonToPos'; } else { msg = "Please enter the exon number to jump to:"; ajaxCmd = 'exonToPos'; } rightClick.myPrompt(msg, function(results) { $.ajax({ type: "GET", url: "../cgi-bin/hgApi", - data: cart.addUpdatesToUrl("db=" + getDb() + "&cmd=" + ajaxCmd + "&num=" + - results + "&table=" + args.table + "&name=" + args.name), + data: cart.varsToUrlData({ 'db': getDb(), 'cmd': ajaxCmd, 'num': results, + 'table': args.table, 'name': args.name }), trueSuccess: rightClick.handleZoomCodon, success: catchErrorOrDispatch, error: errorHandler, cache: true }); }); } else if (cmd == 'hgTrackUi_popup') { popUp.hgTrackUi( rightClick.selectedMenuItem.id, false ); // Launches the popup but shields the ajax with a waitOnFunction } else if (cmd == 'hgTrackUi_follow') { var url = "hgTrackUi?hgsid=" + getHgsid() + "&g="; var rec = hgTracks.trackDb[id]; if (tdbHasParent(rec) && tdbIsLeaf(rec)) @@ -2090,36 +2122,36 @@ url = $(link).attr('href'); else url += rightClick.selectedMenuItem.id; } location.assign(url); } else if (cmd == 'viewImg') { // Fetch a new copy of track img and show it to the user in another window. This code assume we have updated // remote cart with all relevant chages (e.g. drag-reorder). /* Here's how to do this more directly with hgRenderTracks: if(window.open("../cgi-bin/hgRenderTracks?hgt.internal=1&hgsid=" + getHgsid()) == null) { rightClick.windowOpenFailedMsg(); } return; */ - var data = "hgt.imageV1=1&hgt.trackImgOnly=1&hgsid=" + getHgsid(); jQuery('body').css('cursor', 'wait'); $.ajax({ type: "GET", url: "../cgi-bin/hgTracks", - data: cart.addUpdatesToUrl(data), + data: cart.varsToUrlData({ 'hgt.imageV1': '1','hgt.trackImgOnly': '1', + 'hgsid': getHgsid() }), dataType: "html", trueSuccess: rightClick.handleViewImg, success: catchErrorOrDispatch, error: errorHandler, cmd: cmd, cache: false }); } else if (cmd == 'openLink' || cmd == 'followLink') { var href = rightClick.selectedMenuItem.href; var vars = new Array("c", "l", "r", "db"); var valNames = new Array("chromName", "winStart", "winEnd"); for (var i in vars) { // make sure the link contains chrom and window width info (necessary b/c we are stripping hgsid and/or the cart may be empty); // but don't add chrom to wikiTrack links (see redmine #2476). var v = vars[i]; @@ -2153,104 +2185,101 @@ } else { if(rightClick.floatingMenuItem) { // This doesn't work. $('#img_data_' + rightClick.floatingMenuItem).parent().restartFloat(); // This does work $.floatMgr.FOArray = new Array(); } rightClick.floatingMenuItem = id; rightClick.reloadFloatingItem(); imageV2.requestImgUpdate(id, "hgt.transparentImage=0", ""); } } else if (cmd == 'hideSet') { var row = $( 'tr#tr_' + id ); var rows = dragReorder.getContiguousRowSet(row); if (rows && rows.length > 0) { - var vars = new Array(); - var vals = new Array(); + var varsToUpdate = {}; for (var ix=rows.length - 1; ix >= 0; ix--) { // from bottom, just in case remove screws with us var rowId = $(rows[ix]).attr('id').substring('tr_'.length); - //if (tdbIsSubtrack(hgTracks.trackDb[rowId]) == false) - // warn('What went wrong?'); - - vars.push(rowId, rowId+'_sel'); // Remove subtrack level vis and explicitly uncheck. - vals.push('[]', 0); + // Remove subtrack level vis and explicitly uncheck. + varsToUpdate[rowId] = '[]'; + varsToUpdate[rowId+'_sel'] = 0; $(rows[ix]).remove(); } - if (vars.length > 0) { - cart.setVars( vars, vals ); + if (Object.keys(varsToUpdate).length !== 0) { + cart.setVarsObj(varsToUpdate); } imageV2.afterImgChange(true); } } else if (cmd == 'hideComposite') { var rec = hgTracks.trackDb[id]; if (tdbIsSubtrack(rec)) { var row = $( 'tr#tr_' + id ); var rows = dragReorder.getCompositeSet(row); if (rows && rows.length > 0) { for (var ix=rows.length - 1; ix >= 0; ix--) { // from bottom, just in case remove screws with us $(rows[ix]).remove(); } var selectUpdated = vis.update(rec.parentTrack, 'hide'); - cart.setVar(rec.parentTrack, 'hide' ); + cart.setVars( [rec.parentTrack], ['hide']); imageV2.afterImgChange(true); } } } else if (cmd == 'jumpToHighlight') { // If highlight exists for this assembly, jump to it if (hgTracks.highlight) { var newPos = parsePositionWithDb(hgTracks.highlight); if (newPos && newPos.db == getDb()) { if ( $('#highlightItem').length == 0) { // not visible? jump to it var curPos = parsePosition(genomePos.get()); var diff = ((curPos.end - curPos.start) - (newPos.end - newPos.start)); if (diff > 0) { // new position is smaller then current, then center it newPos.start = Math.max( Math.floor(newPos.start - (diff/2) ), 0 ); newPos.end = newPos.start + (curPos.end - curPos.start); } } if (imageV2.inPlaceUpdate) { var params = "position=" + newPos.chrom+':'+newPos.start+'-'+newPos.end; imageV2.navigateInPlace(params, null, true); } else { genomePos.setByCoordinates(newPos.chrom, newPos.start, newPos.end); jQuery('body').css('cursor', 'wait'); document.TrackHeaderForm.submit(); } } } } else if (cmd == 'removeHighlight') { hgTracks.highlight = null; - cart.setVars(['highlight'], ['[]']); + cart.setVarsObj({ 'highlight': '[]' }); imageV2.highlightRegion(); } else { // if ( cmd in 'hide','dense','squish','pack','full','show' ) // Change visibility settings: // // First change the select on our form: var rec = hgTracks.trackDb[id]; var selectUpdated = vis.update(id, cmd); // Now change the track image if(imageV2.enabled && cmd == 'hide') { // Hide local display of this track and update server side cart. // Subtracks controlled by 2 settings so del vis and set sel=0. Others, just set vis hide. if(tdbIsSubtrack(rec)) cart.setVars( [ id, id+"_sel" ], [ '[]', 0 ] ); // Remove subtrack level vis and explicitly uncheck. else if(tdbIsFolderContent(rec)) cart.setVars( [ id, id+"_sel" ], [ 'hide', 0 ] ); // supertrack children need to have _sel set to trigger superttrack reshaping else - cart.setVar(id, 'hide' ); + cart.setVars([id], ['hide']); $(document.getElementById('tr_' + id)).remove(); imageV2.afterImgChange(true); } else if (!imageV2.mapIsUpdateable) { jQuery('body').css('cursor', 'wait'); if(selectUpdated) { // assert(document.TrackForm); document.TrackForm.submit(); } else { // add a hidden with new visibility value var form = $(document.TrackHeaderForm); $("").appendTo(form); document.TrackHeaderForm.submit(); } } else { imageV2.requestImgUpdate(id, id + "=" + cmd, "", cmd); @@ -2634,31 +2663,31 @@ _uiDialigRequest: function (trackName,descriptionOnly) { // popup cfg dialog popUp.trackName = trackName; var myLink = "../cgi-bin/hgTrackUi?g=" + trackName + "&hgsid=" + getHgsid() + "&db=" + getDb(); popUp.trackDescriptionOnly = descriptionOnly; if(popUp.trackDescriptionOnly) myLink += "&descriptionOnly=1"; var rec = hgTracks.trackDb[trackName]; if (!descriptionOnly && rec != null && rec["configureBy"] != null) { if (rec["configureBy"] == 'none') return; else if (rec["configureBy"] == 'clickThrough') { jQuery('body').css('cursor', 'wait'); - window.location = myLink; + window.location = cart.addUpdatesToUrl(myLink); return; } // default falls through to configureBy popup } myLink += "&ajax=1"; $.ajax({ type: "GET", url: cart.addUpdatesToUrl(myLink), dataType: "html", trueSuccess: popUp.uiDialog, success: catchErrorOrDispatch, error: errorHandler, cmd: rightClick.selectedMenuItem, cache: false }); }, @@ -2666,50 +2695,51 @@ hgTrackUi: function (trackName,descriptionOnly) { waitOnFunction( popUp._uiDialigRequest, trackName, descriptionOnly ); // Launches the popup but shields the ajax with a waitOnFunction }, uiDialogOk: function (popObj, trackName) { // When hgTrackUi Cfg popup closes with ok, then update cart and refresh parts of page var rec = hgTracks.trackDb[trackName]; var subtrack = tdbIsSubtrack(rec) ? trackName :undefined; // If subtrack then vis rules differ var allVars = getAllVars($('#hgTrackUiDialog'), subtrack );// For unknown reasons IE8 fails to find $('#pop'), occasionally var changedVars = varHashChanges(allVars,popUp.saveAllVars); //warn("cfgVars:"+varHashToQueryString(changedVars)); var newVis = changedVars[trackName]; var hide = (newVis != null && (newVis == 'hide' || newVis == '[]')); // subtracks do not have "hide", thus '[]' if($('#imgTbl') == undefined) { // On findTracks or config page - setVarsFromHash(changedVars); + if (Object.keys(changedVars).length !== 0) + cart.setVarsObj(changedVars); //if(hide) // TODO: When findTracks or config page has cfg popup, then vis change needs to be handled in page here } else { // On image page if(hide) { - setVarsFromHash(changedVars); + if (Object.keys(changedVars).length !== 0) + cart.setVarsObj(changedVars); $(document.getElementById('tr_' + trackName)).remove(); imageV2.afterImgChange(true); } else { // Keep local state in sync if user changed visibility if(newVis != null) { vis.update(trackName, newVis); } - var urlData = varHashToQueryString(changedVars); - if(urlData.length > 0) { + if (Object.keys(changedVars).length !== 0) { + var urlData = cart.varsToUrlData(changedVars); if(imageV2.mapIsUpdateable) { imageV2.requestImgUpdate(trackName,urlData,""); } else { - window.location = "../cgi-bin/hgTracks?" + urlData + - "&hgsid=" + getHgsid(); + window.location = "../cgi-bin/hgTracks?" + urlData + "&hgsid=" + getHgsid(); } } } } }, uiDialog: function (response, status) { // Take html from hgTrackUi and put it up as a modal dialog. // make sure all links (e.g. help links) open up in a new window response = response.replace(/= $(window).scrollTop() || top < $(window).scrollTop() + $(window).height()) { // don't bother if the item is not currently visible. currentId = item.id; currentIdYOffset = top - $(window).scrollTop(); } } } $.ajax({ type: "GET", url: "../cgi-bin/hgTracks", data: cart.addUpdatesToUrl(params + - "&hgt.trackImgOnly=1&hgt.ideogramToo=1&hgsid=" + - getHgsid()), + "&hgt.trackImgOnly=1&hgt.ideogramToo=1&hgsid=" + getHgsid()), dataType: "html", trueSuccess: imageV2.updateImgAndMap, success: catchErrorOrDispatch, error: errorHandler, cmd: 'wholeImage', loadingId: showLoadingImage("imgTbl"), disabledEle: disabledEle, currentId: currentId, currentIdYOffset: currentIdYOffset, cache: false }); }, highlightRegion: function() // highlight vertical region in imgTbl based on hgTracks.highlight (#709). @@ -3400,32 +3429,32 @@ // With history support it is best that most position changes will ajax-update the image // This ensures that the 'go' and 'refresh' button will do so unless the chrom changes. $("input[value='go'],input[value='refresh']").click(function () { var newPos = genomePos.get().replace(/,/g,''); var newChrom = newPos.split(':')[0]; var oldChrom = genomePos.getOriginalPos().split(':')[0]; if (newChrom == oldChrom) { imageV2.markAsDirtyPage(); imageV2.navigateInPlace("position="+encodeURIComponent(newPos), null, false); window.scrollTo(0,0); return false; } // If chrom changed AND there are vis updates waiting... if (cart.updatesWaiting()) { - var url = "hgTracks?db=" + getDb() + "&position=" + newPos + "&hgsid="+getHgsid(); - url = cart.addUpdatesToUrl(url) + var url = "../cgi-bin/hgTracks?" + cart.varsToUrlData({ 'db': getDb(), + 'position': newPos, 'hgsid': getHgsid() }); window.location.assign(url) return false; } return true; }); // Have vis box changes update cart through ajax. This helps keep page/cart in sync. vis.initForAjax(); // We reach here from these possible paths: // A) Forward: Full page retrieval: hgTracks is first navigated to (or chrom change) // B) Back-button past a full retrieval (B in: ->A,->b,->c(full page),->d,<-c,<-B(again)) // B1) Dirty page: at least one non-position change (e.g. 1 track vis changed in b) // B2) Clean page: only position changes from A->b->| var curPos = encodeURIComponent(genomePos.get().replace(/,/g,''));