ad2c455833e131ab3fcfb22889803e88f6cadb96
kate
  Wed Nov 9 17:03:34 2016 -0800
Major JS code cleanup and refactor.  Also fixed some event handling. refs #17369

diff --git src/hg/js/hgGtexTrackSettings.js src/hg/js/hgGtexTrackSettings.js
index ceb18c9..e78edc4 100644
--- src/hg/js/hgGtexTrackSettings.js
+++ src/hg/js/hgGtexTrackSettings.js
@@ -1,308 +1,350 @@
-// gtexTrackSettings - Interactive features for GTEX Body Map version of GTEx Gene track UI page
+// gexTrackSettings - Interactive features for GTEX Body Map version of GTEx Gene track UI page
 
 // Copyright (C) 2016 The Regents of the University of California
 
-// This file contains a single module that encapsulates all data and functions used to manage
-// the hgGtexTrackSettings page.
-
+// This file contains a single module that encapsulates all javascript data and functions used to manage
+// the hgGtexTrackSettings page, which is initially generated by the hgGtexTrackSettings CGI.  
+// The page contains an SVG format illustration of a human body with tissues, 
+// and a companion list of the same tissues. The tissues are based on the  GTEx Project, and total 53.
+//
+// Users can interact with either the illustration or the list, and any selections they make will be 
+// reflected in the other.  The interactions are click (select/unselect) or hover (highlight the tissue
+// in the body map).  There are 53 total tissues -- the GTEx Consortium set.  Tissue abbreviations and
+// colors used in the UI are from the hgFixed.gtexTissue table.
+//
+// Each tissue is represented in the SVG by:
+//      1. shape                <path id=$tissue_Pic_Lo>
+//      2. highlighted shape    <path id=$tissue_Pic_Hi>
+//      3. highlight surround   <path id=$tissue_Aura_Hi>
+//      4. label                <text id=$tissue_Text_Hi>
+//      5. leader line          <line | polyline id=$tissue_Lead_Hi>
+//
+// Implementation note: jQuery is used below where effective.  Some SVG manipulation (e.g. add/remove/toggle 
+// classes) don't work from jQuery, so code for that is raw javascript
 
 var gtexTrackSettings = (function() {
 
     // Globals
     
-    var _htmlDoc;
-    var _htmlRoot;
     var _svgDoc;        // SVG has its own DOM
     var _svgRoot;
-    var _topTissue;
+
+    var _topTissue;     // Highlighted tissue is drawn last so it is on top
     var _topAura;
 
+    var TEXT_HI = '_Text_Hi';
+    var LEAD_HI = '_Lead_Hi';
+    var AURA_HI = '_Aura_Hi';
+    var PIC_HI = '_Pic_Hi';
+    var PIC_LO = '_Pic_Lo';
+
+    var COLOR_BLACK = 'black';
+    var COLOR_SELECTED = COLOR_BLACK;
+    var COLOR_BLUE = 'blue';
+    var COLOR_HIGHLIGHT = COLOR_BLUE;
+    var COLOR_GRAY = '#737373';
+    var COLOR_UNSELECTED = COLOR_GRAY;
+    var COLOR_PINK = '#F69296';
+    var COLOR_LEADER = COLOR_PINK;
+
+    var CLASS_TISSUE_SELECTED = 'tissueSelected';
+    var CLASS_TISSUE_HOVERED = 'tissueHovered';
+
+    // 53 tissues from GTEx, as in hgTracks.gtexTissue table
+    // TODO: Consider generating this list during make, to an auxiliary .js file
     var tissues = [
         'adiposeSubcut', 'adiposeVisceral', 'adrenalGland', 'arteryAorta', 'arteryCoronary', 
         'arteryTibial', 'bladder', 'brainAmygdala', 'brainAnCinCortex', 'brainCaudate', 
         'brainCerebelHemi', 'brainCerebellum', 'brainCortex', 'brainFrontCortex', 
         'brainHippocampus', 'brainHypothalamus', 'brainNucAccumbens', 'brainPutamen', 
         'brainSpinalcord', 'brainSubstanNigra', 'breastMamTissue', 'xformedlymphocytes',
         'xformedfibroblasts', 'ectocervix', 'endocervix', 'colonSigmoid', 'colonTransverse',
         'esophagusJunction', 'esophagusMucosa', 'esophagusMuscular', 'fallopianTube', 
         'heartAtrialAppend', 'heartLeftVentricl', 'kidneyCortex', 'liver', 'lung', 
         'minorSalivGland', 'muscleSkeletal', 'nerveTibial', 'ovary', 'pancreas', 'pituitary', 
         'prostate', 'skinNotExposed', 'skinExposed', 'smallIntestine', 'spleen', 'stomach', 
         'testis', 'thyroid', 'uterus', 'vagina', 'wholeBlood'
     ];
 
     // Convenience functions
 
     function tissueFromSvgId(svgId) {
         // Get tissue name from an SVG id. Convention here is <tis>_*
         return svgId.split('_')[0];
     }
 
-    function initTissue(tis) {
-        // Set tissue to unhighlighted state
-        $("#" + tis + "_Pic_Hi", _svgRoot).hide();
-
-        // Mark tissue labels in svg
-        var el = _svgDoc.getElementById(tis + "_Text_Hi");
-        if (el !== null) {
-            el.classList.add('tissueLabel');
-        }
-        $("#" + tis + "_Aura_Hi", _svgRoot).hide();
-    }
-
-    function initBodyMap(svg, doc) {
-        // Set organs to unhighlighted state
-        tissues.forEach(initTissue);
-    }
-
-    function onClickToggleTissue(tis) {
-        // mark selected in tissue table
-        tis = this.id;  // arg bad
-        $(this).toggleClass('tissueSelected');
-
-        // jQuery addClass doesn't work on SVG elements, using classList
-        // May need a shim so this works on IE9 and Opera mini (as of Dec 2014)
-        // https://martinwolf.org/blog/2014/12/adding-and-removing-classes-from-svg-elements-with-jquery
-        var el = _svgDoc.getElementById(this.id + '_Text_Hi');
-        if (el !== null) {
-            el.classList.toggle('tissueSelected');
-            if ($(this).hasClass('tissueSelected')) {
-                onClickSetTissue(tis);
-            } else {
-                onClickClearTissue(tis);
+    function setMapTissueElColor(el) {
+        // Change appearance of label in body map. This function is part of setTissue(),
+        // used at initialization time (when other element attributes are already set by CGI)
+        // NOTE: label may be consist of multiple text elements, so traverse children
+        // TODO: Try replacing with CSS (First attempt resulted in black only after mouseover!)
+        if (el === null) {
+            return;
         }
+        el.style.fill = COLOR_SELECTED;
+        var count = el.childElementCount;
+        for (var i = 0; i < count; i++) {
+            el.children[i].style.fill = COLOR_SELECTED;
         }
     }
 
-    function onMapClickToggleTissue(ev) {
-        var svgId = ev.target.id;
-        var el = _svgDoc.getElementById(svgId);
-        if (el !== null) {
-            el.classList.toggle('tissueSelected');
-        }
-        var tis = tissueFromSvgId(svgId);
-        el = _htmlDoc.getElementById(tis);
-        if (el !== null) {
-            el.classList.toggle('tissueSelected');
-            if (el.classList.contains('tissueSelected')) {
-                onClickSetTissue(tis);
-            } else {
-               onClickClearTissue(tis);
-            }
-        }
+    function clearMapTissueElColor(el) {
+        // Change appearance of label in body map.
+        // NOTE: label may be consist of multiple text elements, so traverse children
+        // TODO: Try replacing with CSS
+        if (el === null) {
+            return;
         }
-
-    function setMapTissueEl(el) {
-        // set label dark
-        if (el !== null) {
-            el.classList.add('tissueSelected');
-            el.style.fill = "black";
+        el.style.fill = COLOR_UNSELECTED;
         var count = el.childElementCount;
         for (var i = 0; i < count; i++) {
-                el.children[i].style.fill = "black";
-            }
+            el.children[i].style.fill = COLOR_UNSELECTED;
         }
     }
 
-    function onClickSetTissue(tis) {
-        // mark selected in tissue table
+    function setTissue(tis) {
+        // Mark selected in tissue list and body map
         var $tis = $('#' + tis);
-        $tis.addClass('tissueSelected');
-        var colorPatch = $tis.prev(".tissueColor");
-        colorPatch.removeClass('tissueNotSelectedColor');
+        $tis.addClass(CLASS_TISSUE_SELECTED);
+
+        // Change appearance of color patch in tissue list
+        var colorPatch = $tis.prev('.tissueColor');
         var tisColor = colorPatch.data('tissueColor');
         colorPatch.css('background-color', tisColor);
-        var $checkbox = $('#' + tis + ' > input');
-        $checkbox.attr("checked", true);
-        var el = _svgDoc.getElementById(tis + "_Text_Hi");
-        setMapTissueEl(el);
+        colorPatch.removeClass('tissueNotSelectedColor'); // TODO: less obfuscated approach to colored border
+
+        // Set hidden input checkbox, for cart
+        $('#' + tis + ' > input').attr('checked', true);
+
+        // Change appearance of label in body map
+        var el = _svgDoc.getElementById(tis + TEXT_HI);
+        if (el !== null) {
+            el.classList.add(CLASS_TISSUE_SELECTED);
+            setMapTissueElColor(el);
+        }
     }
 
-    function onClickClearTissue(tis) {
-        // unselect in tissue table
+    function clearTissue(tis) {
+        // Unselect in tissue table and body map
         var $tis = $('#' + tis);
-        $tis.removeClass('tissueSelected');
-        colorPatch = $tis.prev(".tissueColor");
+        $tis.removeClass(CLASS_TISSUE_SELECTED);
+
+        // Change appearance of color patch in tissue list
+        colorPatch = $tis.prev('.tissueColor');
         colorPatch.addClass('tissueNotSelectedColor');
-        var $checkbox = $('#' + tis + ' > input');
-        $checkbox.attr("checked", false);
-        var el = _svgDoc.getElementById(tis + "_Text_Hi");
+
+        // Clear hidden input checkbox
+        $('#' + tis + ' > input').attr('checked', false);
+
+        // Change appearance of label in body map
+        var el = _svgDoc.getElementById(tis + TEXT_HI);
         if (el !== null) {
-            el.classList.remove('tissueSelected');
-            el.style.fill = "#737373";
-            var count = el.childElementCount;
-            for (var i = 0; i < count; i++) {
-                el.children[i].style.fill = "#737373";
+            el.classList.remove(CLASS_TISSUE_SELECTED);
+            clearMapTissueElColor(el);
         }
     }
+
+    function toggleTissue(tis) {
+        // Select/unselect tissue in list and body map
+        var $tis = $('#' + tis);
+        $tis.toggleClass(CLASS_TISSUE_SELECTED);
+        if ($tis.hasClass(CLASS_TISSUE_SELECTED)) {
+            setTissue(tis);
+        } else {
+            clearTissue(tis);
+        }
     }
 
     function toggleHighlightTissue(tis) {
-        var i;
-        var isHovered = false;
-        var el = _htmlDoc.getElementById(tis);
-        var $tis = $("#" + tis);
-        if (el !== null) {
-            el.classList.toggle('tissueHovered');
-            var colorPatch = $tis.prev(".tissueColor");
-            colorPatch.toggleClass('tissueHoveredColor');
-            if (el.classList.contains('tissueHovered')) {
-                isHovered = true;
-            }
+        // Highlight tissue in body map and tissue table
+        // TODO: simplify me
+
+        // Highlight tissue label and color patch in tissue table
+        var $tis = $('#' + tis);
+        if ($tis !== null) {
+            $tis.toggleClass(CLASS_TISSUE_HOVERED);
+            var $colorPatch = $tis.prev('.tissueColor');
+            $colorPatch.toggleClass('tissueHoveredColor');
         }
-        // below can likely replace 3 lines after
-        //this.classList.toggle('tissueSelected');
-        textEl = _svgDoc.getElementById(tis + '_Text_Hi');
+        var isHovered = $tis.hasClass(CLASS_TISSUE_HOVERED) ? true : false;
+
+        // Highlight tissue in body map by changing text appearance, visually moving organ to top
+        // and adding a white (or sometimes blue if white background) surround ('aura')
+
+        // Highlight tissue label in body map
+        // TODO:  Try jQuery here
+        textEl = _svgDoc.getElementById(tis + TEXT_HI);
         if (textEl === null) {
             return;
         }
-        textEl.classList.toggle('tissueHovered');
-        var line = $("#" + tis + "_Lead_Hi", _svgRoot);
-        var lineEl = _svgDoc.getElementById(tis + '_Lead_Hi');
-        var pic = $("#" + tis + "_Pic_Hi", _svgRoot);
-        var picEl = _svgDoc.getElementById(tis + '_Pic_Hi');
-        var aura = $("#" + tis + "_Aura_Hi", _svgRoot);
-        var auraEl = _svgDoc.getElementById(tis + '_Aura_Hi');
+        // TODO: unify with text styling below.  Perhaps just add class to children will do it.
+        textEl.classList.toggle(CLASS_TISSUE_HOVERED);
+
+        var lineEl = _svgDoc.getElementById(tis + LEAD_HI);
+        var pic = $('#' + tis + PIC_HI, _svgRoot);
+        var picEl = _svgDoc.getElementById(tis + PIC_HI);
+        var aura = $('#' + tis + AURA_HI, _svgRoot);
+        var auraEl = _svgDoc.getElementById(tis + AURA_HI);
         var textLineCount = textEl.childElementCount;
+        var i;
+
         if (isHovered) {
-            textEl.style.fill = 'blue';
+            textEl.style.fill = COLOR_HIGHLIGHT;
             for (i = 0; i < textLineCount; i++) {
-                textEl.children[i].style.fill = "blue";
+                textEl.children[i].style.fill = COLOR_HIGHLIGHT;
             }
             if (lineEl !== null) {     // cell types lack leader lines
-            lineEl.style.stroke = 'blue';
+            lineEl.style.stroke = COLOR_HIGHLIGHT;
             }
             $(pic).show();
             $(aura).show();
 
             var topAura = auraEl.cloneNode(true);
-            topAura.id = "topAura";
+            topAura.id = 'topAura';
             _topAura = _svgRoot.appendChild(topAura);
         
             var topTissue = picEl.cloneNode(true);
-            topTissue.id = "topTissue";
+            topTissue.id = 'topTissue';
+            topTissue.classList.add(tis);
             _topTissue = _svgRoot.appendChild(topTissue);
         } else {
-            var color = textEl.classList.contains('tissueSelected') ? 'black' : '#737373';
+            var color = textEl.classList.contains(CLASS_TISSUE_SELECTED) ? COLOR_SELECTED : COLOR_UNSELECTED;
             textEl.style.fill = color;
             for (i = 0; i < textLineCount; i++) {
                 textEl.children[i].style.fill = color;
             }
             $(aura).hide();
             $(pic).hide();
             if (lineEl !== null) {     // cell types lack leader lines
-                lineEl.style.stroke = '#F69296';      // pink
+                lineEl.style.stroke = COLOR_LEADER;      // pink
             }
             _svgRoot.removeChild(_topTissue);
             _svgRoot.removeChild(_topAura);
         }
     }
 
-    function onMapHoverTissue(ev) {
-        var svgId = ev.target.id;
+    // Event handlers
+
+    function onClickSetAll() {
+        // Set all on body map and tissue list
+        tissues.forEach(setTissue);
+    }
+
+    function onClickClearAll() {
+        // Clear all on body map and tissue list
+        tissues.forEach(clearTissue);
+    }
+
+    function onClickToggleTissue(tis) {
+        // Select/unselect from tissue list
+        tis = this.id;  // arg bad
+        toggleTissue(tis);
+    }
+
+    function onMapClickToggleTissue(ev) {
+        // Select/unselect from illustration
+        var svgId = ev.currentTarget.id;
         var tis = tissueFromSvgId(svgId);
-        toggleHighlightTissue(tis);
+        toggleTissue(tis);
     }
 
     function onHoverTissue() {
-        var tis = this.id;
+        // Mouseover on label in tissue list
+        toggleHighlightTissue(this.id);
+    }
+
+    function onMapHoverTissue(ev) {
+        // Mouseover on tissue shape or label in illustration
+        var svgId = ev.currentTarget.id;
+        var tis = tissueFromSvgId(svgId);
         toggleHighlightTissue(tis);
     }
 
+    function submitForm() {
+    // Submit the form (from GO button -- as in hgGateway.js)
+    // Show a spinner -- sometimes it takes a while for hgTracks to start displaying.
+    $('.gbGoIcon').removeClass('fa-play').addClass('fa-spinner fa-spin');
+    $form = $('form');
+    $form.submit();
+    }
+
+    // Initialization
+
+    function initTissue(tis) {
+        // Set tissue to unhighlighted state
+        $('#' + tis + PIC_HI, _svgRoot).hide();
+        $('#' + tis + AURA_HI, _svgRoot).hide();
+
+        // Mark tissue labels in svg
+        var textEl = _svgDoc.getElementById(tis + TEXT_HI);
+        if (textEl !== null) {
+            textEl.classList.add('tissueLabel');
+            if ($('#' + tis).hasClass(CLASS_TISSUE_SELECTED)) {
+                textEl.classList.add(CLASS_TISSUE_SELECTED);
+                setMapTissueElColor(textEl);
+            }
+        }
+    }
+
+    function initBodyMap() {
+        // Set organs to unhighlighted state
+        tissues.forEach(initTissue);
+    }
+
     function animateTissue(tis) {
-        // Add handlers to tissue table
-        var textEl;
-        var picEl;
+        // Add event handlers to body map and tissue list
+
+        // Add click and mouseover handler to tissue label in tissue list
         $('#' + tis).click(tis, onClickToggleTissue);
         $('#' + tis).hover(onHoverTissue, onHoverTissue);
 
-        // Add mouseover and click handlers to tissue label
-        textEl = _svgDoc.getElementById(tis + "_Text_Hi");
+        // Add mouseover and click handlers to tissue label in body map
+        var textEl = _svgDoc.getElementById(tis + TEXT_HI);
         if (textEl !== null) {
-            if ($('#' + tis).hasClass('tissueSelected')) {
-                setMapTissueEl(textEl);
-            }
-            textEl.addEventListener("click", onMapClickToggleTissue);
-            textEl.addEventListener("mouseenter", onMapHoverTissue);
-            textEl.addEventListener("mouseleave", onMapHoverTissue);
-            // mouseover, mouseout ?
+            textEl.addEventListener('click', onMapClickToggleTissue);
+            textEl.addEventListener('mouseenter', onMapHoverTissue);
+            textEl.addEventListener('mouseleave', onMapHoverTissue);
         }
         // add mouseover and click handlers to tissue shape
-        picEl = _svgDoc.getElementById(tis + "_Pic_Lo");
+        var picEl = _svgDoc.getElementById(tis + PIC_LO);
         if (picEl !== null) {
-            picEl.addEventListener("click", onMapClickToggleTissue);
-            picEl.addEventListener("mouseenter", onMapHoverTissue);
-            picEl.addEventListener("mouseleave", onMapHoverTissue);
-            // mouseover, mouseout ?
+            picEl.addEventListener('mouseenter', onMapHoverTissue);
+            picEl.addEventListener('mouseleave', onMapHoverTissue);
+            picEl.addEventListener('mouseup', onMapClickToggleTissue);
         }
     }
 
     function animateTissues() {
         // Add event handlers to tissue table and body map SVG
-        tissues.forEach(animateTissue);
-
         $('#setAll').click(onClickSetAll);
         $('#clearAll').click(onClickClearAll);
+        tissues.forEach(animateTissue);
     }
 
-    // UI event handlers
-
-    function onClickSetAll() {
-        // Set all on body map
-        tissues.forEach(onClickSetTissue);
-        // set all on tissue table
-        // NOTE: this shouldn't be needed (needs debugging in onClickSet)
-        $('.tissueLabel').addClass('tissueSelected');
-    }
-
-    function onClickClearAll() {
-        tissues.forEach(onClickClearTissue);
-        $('.tissueLabel').removeClass("tissueSelected");
-    }
-
-    function submitForm() {
-    // Submit the form (from GO button -- as in hgGateway.js)
-    // Show a spinner -- sometimes it takes a while for hgTracks to start displaying.
-    $('.gbGoIcon').removeClass('fa-play').addClass('fa-spinner fa-spin');
-    $form = $('form');
-    $form.submit();
-    }
-
-    // Initialization
-
     function init() {
         // cart.setCgi('gtexTrackSettings');
 
         $(function() {
             // After page load, tweak layout and initialize event handlers
             // TODO: need to wait onready ?
-            var bodyMapSvg = document.getElementById("bodyMapSvg");
-            globalSvg = bodyMapSvg;
-
-            _htmlDoc = document;
-            _htmlRoot = document.documentElement;
+            var bodyMapSvg = document.getElementById('bodyMapSvg');
 
             // Wait for SVG to load
-            bodyMapSvg.addEventListener("load", function() {
-                var svg = bodyMapSvg.contentDocument.documentElement;
-                _svgRoot = svg;
+            bodyMapSvg.addEventListener('load', function() {
+                _svgRoot = bodyMapSvg.contentDocument.documentElement;
                 _svgDoc = bodyMapSvg.contentDocument;
                 
-                initBodyMap(svg, bodyMapSvg.contentDocument);
+                initBodyMap();
                 animateTissues();
             }, false);
             $('.goButtonContainer').click(submitForm);
         });
     }
 
     return {
             init: init
            };
     
 }()); // gtexTrackSettings
 
-$(document).ready(function() {
 gtexTrackSettings.init();
-});
-
-//gtexTrackSettings.init();