c8fdb2541c9758c0db06e4cf7fff3e4a0160d54b tdreszer Fri May 2 16:48:41 2014 -0700 Took this one step further: moved methods to their own cart object and now append queued cart updates to any hgTracks ajax update. This will provide greater flexibilty for future cart update queuing. Redmone #13164. diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js index 1174e25..151344b 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: "cmd=defaultPos&db=" + getDb(), + data: cart.addUpdatesToUrl("cmd=defaultPos&db=" + getDb()), dataType: "html", trueSuccess: genomePos.handleChange, success: catchErrorOrDispatch, error: errorHandler, cache: true }); return false; } } ///////////////////////////////////// //// Creating items by dragging ///// ///////////////////////////////////// var makeItemsByDrag = { @@ -441,37 +441,113 @@ 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 = vis.cartUpdatesAddToUrl(obj.href); + var href = cart.addUpdatesToUrl(obj.href); return postTheForm($(thisForm).attr('name'),href); } return true; } } +///////////////////////// +//// cart updating ///// +/////////////////////// +var cart = { + + 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]; + } + if (updates.length > 0) { + if(url.length > 0 && url.lastIndexOf("?") == -1 && url.lastIndexOf("&") == -1) + url += "?" + updates.substring(1); + else + 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 + // }, + + addUpdateToQueue: function (track,newVis) + { // creates a string of updates to save for ajax batch or a submit + cart.updateQueue[track] = newVis; + + // 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); + }, + + setVars: function (names, values, errFunc, async) + { // ajax updates the cart, and includes any queued updates. + if (cart.updatesWaiting()) { + for (var track in cart.updateQueue) { + names.push(track); + values.push(cart.updateQueue[track]); + } + 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 ); + } +} + /////////////////////////////////////////////// //// visibility (mixed with group toggle) ///// /////////////////////////////////////////////// var vis = { enumOrder: new Array("hide", "dense", "full", "pack", "squish"), // map cgi enum visibility codes to strings cartUpdateQueue: {}, update: function (track, visibility) { // Updates visibility state in hgTracks.trackDb and any visible elements on the page. // returns true if we modify at least one select in the group list var rec = hgTracks.trackDb[track]; var selectUpdated = false; $("select[name=" + escapeJQuerySelectorChars(track) + "]").each(function(t) { $(this).attr('class', visibility == 'hide' ? 'hiddenText' : 'normalText'); @@ -515,97 +591,48 @@ return setTableRowVisibility(button, prefix, "hgtgroup", "group",false,arguments[2]); else return setTableRowVisibility(button, prefix, "hgtgroup", "group",false); }, expandAllGroups: function (newState) { // Set visibility of all track groups to newState (true means expanded). // This code also modifies the corresponding hidden fields and the gif's of the +/- img tag. imageV2.markAsDirtyPage(); $(".toggleButton[id$='_button']").each( function (i) { // works for old img type AND new BUTTONS_BY_CSS vis.toggleForGroup(this,this.id.substring(0,this.id.length - 7),newState); // clip '_button' suffix }); return false; }, - cartUpdatesWaiting: function () - { // returns TRUE if updates are waiting. - return (Object.keys(vis.cartUpdateQueue).length !== 0); - }, - - cartUpdatesAddToUrl: function (url) - { // adds any outstanding cart updates to the url, then clears the queue - var updates = ""; - for (var track in vis.cartUpdateQueue) { - updates += "&" + track + "=" + vis.cartUpdateQueue[track]; - } - if (updates.length > 0) { - if(url.length > 0 && url.lastIndexOf("?") == -1 && url.lastIndexOf("&") == -1) - url += "?" + updates.substring(1); - else - url += updates - } - vis.cartUpdateQueue = {}; - return url; - }, - - // NOTE: could update in background, however, failing to hit "refresh" is a user choice - // cartUpdatesViaAjax: function () - // { // Called via timer: updates the cart via setVars. - // if (!vis.cartUpdatesWaiting()) - // return; - // - // var tracks = []; - // var newVis = []; - // for (var track in vis.cartUpdateQueue) { - // tracks.push(track) - // newVis.push(vis.cartUpdateQueue[track]); - // } - // if (tracks.length === 0) - // return; - // vis.cartUpdateQueue = {}; - // setCartVars(tracks,newVis,async=false); // sync to avoid another race condition mess - // }, - - cartUpdateAddToQueue: function (track,newVis) - { // creates a string of updates to save for ajax batch or a submit - vis.cartUpdateQueue[track] = newVis; - - // NOTE: could update in background, however, failing to hit "refresh" is a user choice - // first in queue, schedule background update - // if (Object.keys(vis.cartUpdateQueue).length === 1) - // setTimeout("vis.cartUpdatesViaAjax();",10000); - }, - initForAjax: function() { // To better support the back-button, it is good to eliminate extraneous form puts // Towards that end, we support visBoxes making ajax calls to update cart. var sels = $('select.normalText,select.hiddenText'); $(sels).change(function() { var track = $(this).attr('name'); if ($(this).val() == 'hide') { var rec = hgTracks.trackDb[track]; if(rec) rec.visibility = 0; // else Would be nice to hide subtracks as well but that may be overkill $(document.getElementById('tr_' + track)).remove(); imageV2.highlightRegion(); $(this).attr('class', 'hiddenText'); } else $(this).attr('class', 'normalText'); - vis.cartUpdateAddToQueue(track,$(this).val()); + cart.addUpdateToQueue(track,$(this).val()); return false; }); // Now we can rid the submt of the burden of all those vis boxes var form = $('form#TrackForm'); $(form).submit(function () { $('select.normalText,select.hiddenText').attr('disabled',true); }); $(form).attr('method','get'); }, restoreFromBackButton: function() // Re-enabling vis dropdowns is necessarty because intiForAjax() disables them on submit. { $('select.normalText,select.hiddenText').attr('disabled',false); } @@ -647,31 +674,31 @@ highlightThisRegion: function(newPosition) // set highlighting newPosition in server-side cart and apply the highlighting in local UI. { var start, end; if (arguments.length == 2) { start = arguments[0]; end = arguments[1]; } else { var pos = parsePosition(newPosition); start = pos.start; end = pos.end; } hgTracks.highlight = getDb() + "." + hgTracks.chromName + ":" + start + "-" + end; hgTracks.highlight += '#AAFFFF'; // Also include highlight color // we include enableHighlightingDialog because it may have been changed by the dialog - setCartVars(['highlight', 'enableHighlightingDialog'], + cart.setVars(['highlight', 'enableHighlightingDialog'], [hgTracks.highlight, hgTracks.enableHighlightingDialog ? 1 : 0]); imageV2.highlightRegion(); }, selectionEndDialog: function (newPosition) // Let user choose between zoom-in and highlighting. { var dragSelectDialog = $("#dragSelectDialog")[0]; if (!dragSelectDialog) { $("body").append("<div id='dragSelectDialog'>" + newPosition + "<p><input type='checkbox' id='disableDragHighlight'>" + "Don't show this dialog again and always zoom.<BR>" + "(Re-enable highlight via the 'configure' menu at any time.)</p>"); dragSelectDialog = $("#dragSelectDialog")[0]; } @@ -685,31 +712,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) - setCartVars(['enableHighlightingDialog'],[0]); + cart.setVars(['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"); } @@ -1141,31 +1168,31 @@ var dragReorder = { setOrder: function (table) { // Sets the 'order' value for the image table after a drag reorder var names = []; var values = []; $("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')); } }); if(names.length > 0) { - setCartVars(names,values); + cart.setVars(names,values); 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) { @@ -1990,31 +2017,31 @@ } } if(imageV2.inPlaceUpdate) { // XXXX This attempt to "update whole track image in place" didn't work for a variety of reasons // (e.g. safari doesn't parse map when we update on the client side), so this is currently dead code. // However, this now works in all other browsers, so we may turn this on for non-safari browsers // (see redmine #4667). jQuery('body').css('cursor', ''); var data = "hgt.trackImgOnly=1&hgt.ideogramToo=1&position=" + newPosition + "&hgsid=" + getHgsid(); if(name) data += "&hgFind.matches=" + name; $.ajax({ type: "GET", url: "../cgi-bin/hgTracks", - data: data, + data: cart.addUpdatesToUrl(data), dataType: "html", trueSuccess: imageV2.updateImgAndMap, success: catchErrorOrDispatch, error: errorHandler, cmd: cmd, loadingId: showLoadingImage("imgTbl"), cache: false }); } else { // do a full page refresh to update hgTracks image jQuery('body').css('cursor', 'wait'); var ele; if(document.TrackForm) ele = document.TrackForm; else @@ -2027,32 +2054,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: "db=" + getDb() + "&cmd=" + ajaxCmd + "&num=" + results + - "&table=" + args.table + "&name=" + args.name, + data: cart.addUpdatesToUrl("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)) @@ -2068,31 +2095,31 @@ } 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: data, + data: cart.addUpdatesToUrl(data), 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]; @@ -2138,92 +2165,92 @@ var row = $( 'tr#tr_' + id ); var rows = dragReorder.getContiguousRowSet(row); if (rows && rows.length > 0) { var vars = new Array(); var vals = new Array(); 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); $(rows[ix]).remove(); } if (vars.length > 0) { - setCartVars( vars, vals ); + cart.setVars( vars, vals ); } 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'); - setCartVar(rec.parentTrack, 'hide' ); + cart.setVar(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; - setCartVars(['highlight'], ['[]']); + cart.setVars(['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)) - setCartVars( [ id, id+"_sel" ], [ '[]', 0 ] ); // Remove subtrack level vis and explicitly uncheck. + cart.setVars( [ id, id+"_sel" ], [ '[]', 0 ] ); // Remove subtrack level vis and explicitly uncheck. else if(tdbIsFolderContent(rec)) - setCartVars( [ id, id+"_sel" ], [ 'hide', 0 ] ); // supertrack children need to have _sel set to trigger superttrack reshaping + cart.setVars( [ id, id+"_sel" ], [ 'hide', 0 ] ); // supertrack children need to have _sel set to trigger superttrack reshaping else - setCartVar(id, 'hide' ); + cart.setVar(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); $("<input type='hidden' name='"+id+"'value='"+cmd+"'>").appendTo(form); document.TrackHeaderForm.submit(); } } else { imageV2.requestImgUpdate(id, id + "=" + cmd, "", cmd); @@ -2614,31 +2641,31 @@ 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; return; } // default falls through to configureBy popup } myLink += "&ajax=1"; $.ajax({ type: "GET", - url: myLink, + url: cart.addUpdatesToUrl(myLink), dataType: "html", trueSuccess: popUp.uiDialog, success: catchErrorOrDispatch, error: errorHandler, cmd: rightClick.selectedMenuItem, cache: false }); }, hgTrackUi: function (trackName,descriptionOnly) { waitOnFunction( popUp._uiDialigRequest, trackName, descriptionOnly ); // Launches the popup but shields the ajax with a waitOnFunction }, uiDialogOk: function (popObj, trackName) @@ -3008,31 +3035,31 @@ requestImgUpdate: function (trackName,extraData,loadingId,newVisibility) { // extraData, loadingId and newVisibility are optional var data = "hgt.trackImgOnly=1&hgsid=" + getHgsid() + "&hgt.trackNameFilter=" + trackName; if(extraData != undefined && extraData != "") data += "&" + extraData; if(loadingId == undefined || loadingId == "") loadingId = showLoadingImage("tr_" + trackName); var getOrPost = "GET"; if ((data.length) > 2000) // extraData could contain a bunch of changes from the cfg dialog getOrPost = "POST"; $.ajax({ type: getOrPost, url: "../cgi-bin/hgTracks", - data: data, + data: cart.addUpdatesToUrl(data), dataType: "html", trueSuccess: imageV2.updateImgAndMap, success: catchErrorOrDispatch, error: errorHandler, cmd: 'refresh', loadingId: loadingId, id: trackName, newVisibility: newVisibility, cache: false }); }, fullReload: function() { // force reload of whole page via trackform submit @@ -3255,31 +3282,31 @@ if(keepCurrentTrackVisible) { var item = rightClick.currentMapItem || imageV2.lastTrack; if(item) { var top = $(document.getElementById("tr_" + item.id)).position().top; if(top >= $(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: vis.cartUpdatesAddToUrl(params + + data: cart.addUpdatesToUrl(params + "&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 }); }, @@ -3372,33 +3399,33 @@ // TODO: move elsewhere? // 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 (vis.cartUpdatesWaiting()) { + if (cart.updatesWaiting()) { var url = "hgTracks?db=" + getDb() + "&position=" + newPos + "&hgsid="+getHgsid(); - url = vis.cartUpdatesAddToUrl(url) + url = cart.addUpdatesToUrl(url) 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,''));